1use crate::config::E2eConfig;
4use crate::escape::{escape_r, sanitize_filename, sanitize_ident};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup};
7use alef_core::backend::GeneratedFile;
8use alef_core::config::AlefConfig;
9use alef_core::hash::{self, CommentStyle};
10use anyhow::Result;
11use std::fmt::Write as FmtWrite;
12use std::path::PathBuf;
13
14use super::E2eCodegen;
15
16pub struct RCodegen;
18
19impl E2eCodegen for RCodegen {
20 fn generate(
21 &self,
22 groups: &[FixtureGroup],
23 e2e_config: &E2eConfig,
24 _alef_config: &AlefConfig,
25 ) -> Result<Vec<GeneratedFile>> {
26 let lang = self.language_name();
27 let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
28
29 let mut files = Vec::new();
30
31 let call = &e2e_config.call;
33 let overrides = call.overrides.get(lang);
34 let module_path = overrides
35 .and_then(|o| o.module.as_ref())
36 .cloned()
37 .unwrap_or_else(|| call.module.clone());
38 let _function_name = overrides
39 .and_then(|o| o.function.as_ref())
40 .cloned()
41 .unwrap_or_else(|| call.function.clone());
42 let result_is_simple = call.result_is_simple || overrides.is_some_and(|o| o.result_is_simple);
43 let _result_var = &call.result_var;
44
45 let r_pkg = e2e_config.resolve_package("r");
47 let pkg_name = r_pkg
48 .as_ref()
49 .and_then(|p| p.name.as_ref())
50 .cloned()
51 .unwrap_or_else(|| module_path.clone());
52 let pkg_path = r_pkg
53 .as_ref()
54 .and_then(|p| p.path.as_ref())
55 .cloned()
56 .unwrap_or_else(|| "../../packages/r".to_string());
57 let pkg_version = r_pkg
58 .as_ref()
59 .and_then(|p| p.version.as_ref())
60 .cloned()
61 .unwrap_or_else(|| "0.1.0".to_string());
62
63 files.push(GeneratedFile {
65 path: output_base.join("DESCRIPTION"),
66 content: render_description(&pkg_name, &pkg_version, e2e_config.dep_mode),
67 generated_header: false,
68 });
69
70 files.push(GeneratedFile {
72 path: output_base.join("run_tests.R"),
73 content: render_test_runner(&pkg_path, e2e_config.dep_mode),
74 generated_header: true,
75 });
76
77 for group in groups {
79 let active: Vec<&Fixture> = group
80 .fixtures
81 .iter()
82 .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
83 .collect();
84
85 if active.is_empty() {
86 continue;
87 }
88
89 let filename = format!("test_{}.R", sanitize_filename(&group.category));
90 let field_resolver = FieldResolver::new(
91 &e2e_config.fields,
92 &e2e_config.fields_optional,
93 &e2e_config.result_fields,
94 &e2e_config.fields_array,
95 );
96 let content = render_test_file(&group.category, &active, &field_resolver, result_is_simple, e2e_config);
97 files.push(GeneratedFile {
98 path: output_base.join("tests").join(filename),
99 content,
100 generated_header: true,
101 });
102 }
103
104 Ok(files)
105 }
106
107 fn language_name(&self) -> &'static str {
108 "r"
109 }
110}
111
112fn render_description(pkg_name: &str, pkg_version: &str, dep_mode: crate::config::DependencyMode) -> String {
113 let dep_line = match dep_mode {
114 crate::config::DependencyMode::Registry => {
115 format!("Imports: {pkg_name} ({pkg_version})\n")
116 }
117 crate::config::DependencyMode::Local => String::new(),
118 };
119 format!(
120 r#"Package: e2e.r
121Title: E2E Tests for {pkg_name}
122Version: 0.1.0
123Description: End-to-end test suite.
124{dep_line}Suggests: testthat (>= 3.0.0)
125Config/testthat/edition: 3
126"#
127 )
128}
129
130fn render_test_runner(pkg_path: &str, dep_mode: crate::config::DependencyMode) -> String {
131 let mut out = String::new();
132 out.push_str(&hash::header(CommentStyle::Hash));
133 let _ = writeln!(out, "library(testthat)");
134 match dep_mode {
135 crate::config::DependencyMode::Registry => {
136 let _ = writeln!(out, "# Package loaded via library() from CRAN install.");
138 }
139 crate::config::DependencyMode::Local => {
140 let _ = writeln!(out, "devtools::load_all(\"{pkg_path}\")");
143 }
144 }
145 let _ = writeln!(out);
146 let _ = writeln!(out, "test_dir(\"tests\")");
147 out
148}
149
150fn render_test_file(
151 category: &str,
152 fixtures: &[&Fixture],
153 field_resolver: &FieldResolver,
154 result_is_simple: bool,
155 e2e_config: &E2eConfig,
156) -> String {
157 let mut out = String::new();
158 out.push_str(&hash::header(CommentStyle::Hash));
159 let _ = writeln!(out, "# E2e tests for category: {category}");
160 let _ = writeln!(out);
161
162 for (i, fixture) in fixtures.iter().enumerate() {
163 render_test_case(&mut out, fixture, e2e_config, field_resolver, result_is_simple);
164 if i + 1 < fixtures.len() {
165 let _ = writeln!(out);
166 }
167 }
168
169 while out.ends_with("\n\n") {
171 out.pop();
172 }
173 if !out.ends_with('\n') {
174 out.push('\n');
175 }
176 out
177}
178
179fn render_test_case(
180 out: &mut String,
181 fixture: &Fixture,
182 e2e_config: &E2eConfig,
183 field_resolver: &FieldResolver,
184 result_is_simple: bool,
185) {
186 let call_config = e2e_config.resolve_call(fixture.call.as_deref());
187 let function_name = &call_config.function;
188 let result_var = &call_config.result_var;
189
190 let test_name = sanitize_ident(&fixture.id);
191 let description = fixture.description.replace('"', "\\\"");
192
193 let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
194
195 let args_str = build_args_string(&fixture.input, &call_config.args);
196
197 let mut setup_lines = Vec::new();
199 let final_args = if let Some(visitor_spec) = &fixture.visitor {
200 build_r_visitor(&mut setup_lines, visitor_spec);
201 if args_str.is_empty() {
202 "visitor".to_string()
203 } else {
204 format!("{args_str}, visitor = visitor")
205 }
206 } else {
207 args_str
208 };
209
210 if expects_error {
211 let _ = writeln!(out, "test_that(\"{test_name}: {description}\", {{");
212 for line in &setup_lines {
213 let _ = writeln!(out, " {line}");
214 }
215 let _ = writeln!(out, " expect_error({function_name}({final_args}))");
216 let _ = writeln!(out, "}})");
217 return;
218 }
219
220 let _ = writeln!(out, "test_that(\"{test_name}: {description}\", {{");
221 for line in &setup_lines {
222 let _ = writeln!(out, " {line}");
223 }
224 let _ = writeln!(out, " {result_var} <- {function_name}({final_args})");
225
226 for assertion in &fixture.assertions {
227 render_assertion(out, assertion, result_var, field_resolver, result_is_simple, e2e_config);
228 }
229
230 let _ = writeln!(out, "}})");
231}
232
233fn build_args_string(input: &serde_json::Value, args: &[crate::config::ArgMapping]) -> String {
234 if args.is_empty() {
235 return json_to_r(input, true);
236 }
237
238 let parts: Vec<String> = args
239 .iter()
240 .filter_map(|arg| {
241 let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
242 let val = input.get(field)?;
243 if val.is_null() && arg.optional {
244 return None;
245 }
246 Some(format!("{} = {}", arg.name, json_to_r(val, true)))
247 })
248 .collect();
249
250 parts.join(", ")
251}
252
253fn render_assertion(
254 out: &mut String,
255 assertion: &Assertion,
256 result_var: &str,
257 field_resolver: &FieldResolver,
258 result_is_simple: bool,
259 _e2e_config: &E2eConfig,
260) {
261 if let Some(f) = &assertion.field {
264 match f.as_str() {
265 "chunks_have_content" => {
266 let pred = format!("all(sapply({result_var}$chunks %||% list(), function(c) nchar(c$content) > 0))");
267 match assertion.assertion_type.as_str() {
268 "is_true" => {
269 let _ = writeln!(out, " expect_true({pred})");
270 }
271 "is_false" => {
272 let _ = writeln!(out, " expect_false({pred})");
273 }
274 _ => {
275 let _ = writeln!(out, " # skipped: unsupported assertion type on synthetic field '{f}'");
276 }
277 }
278 return;
279 }
280 "chunks_have_embeddings" => {
281 let pred = format!(
282 "all(sapply({result_var}$chunks %||% list(), function(c) !is.null(c$embedding) && length(c$embedding) > 0))"
283 );
284 match assertion.assertion_type.as_str() {
285 "is_true" => {
286 let _ = writeln!(out, " expect_true({pred})");
287 }
288 "is_false" => {
289 let _ = writeln!(out, " expect_false({pred})");
290 }
291 _ => {
292 let _ = writeln!(out, " # skipped: unsupported assertion type on synthetic field '{f}'");
293 }
294 }
295 return;
296 }
297 "embeddings" => {
301 match assertion.assertion_type.as_str() {
302 "count_equals" => {
303 if let Some(val) = &assertion.value {
304 let r_val = json_to_r(val, false);
305 let _ = writeln!(out, " expect_equal(length({result_var}), {r_val})");
306 }
307 }
308 "count_min" => {
309 if let Some(val) = &assertion.value {
310 let r_val = json_to_r(val, false);
311 let _ = writeln!(out, " expect_gte(length({result_var}), {r_val})");
312 }
313 }
314 "not_empty" => {
315 let _ = writeln!(out, " expect_gt(length({result_var}), 0)");
316 }
317 "is_empty" => {
318 let _ = writeln!(out, " expect_equal(length({result_var}), 0)");
319 }
320 _ => {
321 let _ = writeln!(
322 out,
323 " # skipped: unsupported assertion type on synthetic field 'embeddings'"
324 );
325 }
326 }
327 return;
328 }
329 "embedding_dimensions" => {
330 let expr = format!("(if (length({result_var}) == 0) 0L else length({result_var}[[1]]))");
331 match assertion.assertion_type.as_str() {
332 "equals" => {
333 if let Some(val) = &assertion.value {
334 let r_val = json_to_r(val, false);
335 let _ = writeln!(out, " expect_equal({expr}, {r_val})");
336 }
337 }
338 "greater_than" => {
339 if let Some(val) = &assertion.value {
340 let r_val = json_to_r(val, false);
341 let _ = writeln!(out, " expect_gt({expr}, {r_val})");
342 }
343 }
344 _ => {
345 let _ = writeln!(
346 out,
347 " # skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
348 );
349 }
350 }
351 return;
352 }
353 "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
354 let pred = match f.as_str() {
355 "embeddings_valid" => {
356 format!("all(sapply({result_var}, function(e) length(e) > 0))")
357 }
358 "embeddings_finite" => {
359 format!("all(sapply({result_var}, function(e) all(is.finite(e))))")
360 }
361 "embeddings_non_zero" => {
362 format!("all(sapply({result_var}, function(e) any(e != 0.0)))")
363 }
364 "embeddings_normalized" => {
365 format!("all(sapply({result_var}, function(e) abs(sum(e * e) - 1.0) < 1e-3))")
366 }
367 _ => unreachable!(),
368 };
369 match assertion.assertion_type.as_str() {
370 "is_true" => {
371 let _ = writeln!(out, " expect_true({pred})");
372 }
373 "is_false" => {
374 let _ = writeln!(out, " expect_false({pred})");
375 }
376 _ => {
377 let _ = writeln!(out, " # skipped: unsupported assertion type on synthetic field '{f}'");
378 }
379 }
380 return;
381 }
382 "keywords" | "keywords_count" => {
385 let _ = writeln!(out, " # skipped: field '{f}' not available on R ExtractionResult");
386 return;
387 }
388 _ => {}
389 }
390 }
391
392 if let Some(f) = &assertion.field {
394 if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
395 let _ = writeln!(out, " # skipped: field '{f}' not available on result type");
396 return;
397 }
398 }
399
400 if result_is_simple {
403 if let Some(f) = &assertion.field {
404 let f_lower = f.to_lowercase();
405 if !f.is_empty()
406 && f_lower != "content"
407 && (f_lower.starts_with("metadata")
408 || f_lower.starts_with("document")
409 || f_lower.starts_with("structure"))
410 {
411 let _ = writeln!(
412 out,
413 " # skipped: result_is_simple for field '{f}' not available on result type"
414 );
415 return;
416 }
417 }
418 }
419
420 let field_expr = if result_is_simple {
421 result_var.to_string()
422 } else {
423 match &assertion.field {
424 Some(f) if !f.is_empty() => field_resolver.accessor(f, "r", result_var),
425 _ => result_var.to_string(),
426 }
427 };
428
429 match assertion.assertion_type.as_str() {
430 "equals" => {
431 if let Some(expected) = &assertion.value {
432 let r_val = json_to_r(expected, false);
433 let _ = writeln!(out, " expect_equal(trimws({field_expr}), {r_val})");
434 }
435 }
436 "contains" => {
437 if let Some(expected) = &assertion.value {
438 let r_val = json_to_r(expected, false);
439 let _ = writeln!(out, " expect_true(grepl({r_val}, {field_expr}, fixed = TRUE))");
440 }
441 }
442 "contains_all" => {
443 if let Some(values) = &assertion.values {
444 for val in values {
445 let r_val = json_to_r(val, false);
446 let _ = writeln!(out, " expect_true(grepl({r_val}, {field_expr}, fixed = TRUE))");
447 }
448 }
449 }
450 "not_contains" => {
451 if let Some(expected) = &assertion.value {
452 let r_val = json_to_r(expected, false);
453 let _ = writeln!(out, " expect_false(grepl({r_val}, {field_expr}, fixed = TRUE))");
454 }
455 }
456 "not_empty" => {
457 let _ = writeln!(
458 out,
459 " expect_true(if (is.character({field_expr})) nchar({field_expr}) > 0 else length({field_expr}) > 0)"
460 );
461 }
462 "is_empty" => {
463 let _ = writeln!(out, " expect_equal({field_expr}, \"\")");
464 }
465 "contains_any" => {
466 if let Some(values) = &assertion.values {
467 let items: Vec<String> = values.iter().map(|v| json_to_r(v, false)).collect();
468 let vec_str = items.join(", ");
469 let _ = writeln!(
470 out,
471 " expect_true(any(sapply(c({vec_str}), function(v) grepl(v, {field_expr}, fixed = TRUE))))"
472 );
473 }
474 }
475 "greater_than" => {
476 if let Some(val) = &assertion.value {
477 let r_val = json_to_r(val, false);
478 let _ = writeln!(out, " expect_true({field_expr} > {r_val})");
479 }
480 }
481 "less_than" => {
482 if let Some(val) = &assertion.value {
483 let r_val = json_to_r(val, false);
484 let _ = writeln!(out, " expect_true({field_expr} < {r_val})");
485 }
486 }
487 "greater_than_or_equal" => {
488 if let Some(val) = &assertion.value {
489 let r_val = json_to_r(val, false);
490 let _ = writeln!(out, " expect_true({field_expr} >= {r_val})");
491 }
492 }
493 "less_than_or_equal" => {
494 if let Some(val) = &assertion.value {
495 let r_val = json_to_r(val, false);
496 let _ = writeln!(out, " expect_true({field_expr} <= {r_val})");
497 }
498 }
499 "starts_with" => {
500 if let Some(expected) = &assertion.value {
501 let r_val = json_to_r(expected, false);
502 let _ = writeln!(out, " expect_true(startsWith({field_expr}, {r_val}))");
503 }
504 }
505 "ends_with" => {
506 if let Some(expected) = &assertion.value {
507 let r_val = json_to_r(expected, false);
508 let _ = writeln!(out, " expect_true(endsWith({field_expr}, {r_val}))");
509 }
510 }
511 "min_length" => {
512 if let Some(val) = &assertion.value {
513 if let Some(n) = val.as_u64() {
514 let _ = writeln!(out, " expect_true(nchar({field_expr}) >= {n})");
515 }
516 }
517 }
518 "max_length" => {
519 if let Some(val) = &assertion.value {
520 if let Some(n) = val.as_u64() {
521 let _ = writeln!(out, " expect_true(nchar({field_expr}) <= {n})");
522 }
523 }
524 }
525 "count_min" => {
526 if let Some(val) = &assertion.value {
527 if let Some(n) = val.as_u64() {
528 let _ = writeln!(out, " expect_true(length({field_expr}) >= {n})");
529 }
530 }
531 }
532 "count_equals" => {
533 if let Some(val) = &assertion.value {
534 if let Some(n) = val.as_u64() {
535 let _ = writeln!(out, " expect_equal(length({field_expr}), {n})");
536 }
537 }
538 }
539 "is_true" => {
540 let _ = writeln!(out, " expect_true({field_expr})");
541 }
542 "is_false" => {
543 let _ = writeln!(out, " expect_false({field_expr})");
544 }
545 "method_result" => {
546 if let Some(method_name) = &assertion.method {
547 let call_expr = build_r_method_call(result_var, method_name, assertion.args.as_ref());
548 let check = assertion.check.as_deref().unwrap_or("is_true");
549 match check {
550 "equals" => {
551 if let Some(val) = &assertion.value {
552 if val.is_boolean() {
553 if val.as_bool() == Some(true) {
554 let _ = writeln!(out, " expect_true({call_expr})");
555 } else {
556 let _ = writeln!(out, " expect_false({call_expr})");
557 }
558 } else {
559 let r_val = json_to_r(val, false);
560 let _ = writeln!(out, " expect_equal({call_expr}, {r_val})");
561 }
562 }
563 }
564 "is_true" => {
565 let _ = writeln!(out, " expect_true({call_expr})");
566 }
567 "is_false" => {
568 let _ = writeln!(out, " expect_false({call_expr})");
569 }
570 "greater_than_or_equal" => {
571 if let Some(val) = &assertion.value {
572 let r_val = json_to_r(val, false);
573 let _ = writeln!(out, " expect_true({call_expr} >= {r_val})");
574 }
575 }
576 "count_min" => {
577 if let Some(val) = &assertion.value {
578 let n = val.as_u64().unwrap_or(0);
579 let _ = writeln!(out, " expect_true(length({call_expr}) >= {n})");
580 }
581 }
582 "is_error" => {
583 let _ = writeln!(out, " expect_error({call_expr})");
584 }
585 "contains" => {
586 if let Some(val) = &assertion.value {
587 let r_val = json_to_r(val, false);
588 let _ = writeln!(out, " expect_true(grepl({r_val}, {call_expr}, fixed = TRUE))");
589 }
590 }
591 other_check => {
592 panic!("R e2e generator: unsupported method_result check type: {other_check}");
593 }
594 }
595 } else {
596 panic!("R e2e generator: method_result assertion missing 'method' field");
597 }
598 }
599 "matches_regex" => {
600 if let Some(expected) = &assertion.value {
601 let r_val = json_to_r(expected, false);
602 let _ = writeln!(out, " expect_true(grepl({r_val}, {field_expr}))");
603 }
604 }
605 "not_error" => {
606 }
608 "error" => {
609 }
611 other => {
612 panic!("R e2e generator: unsupported assertion type: {other}");
613 }
614 }
615}
616
617fn json_to_r(value: &serde_json::Value, lowercase_enum_values: bool) -> String {
625 match value {
626 serde_json::Value::String(s) => {
627 let normalized = if lowercase_enum_values && s.chars().next().is_some_and(|c| c.is_uppercase()) {
629 s.to_lowercase()
630 } else {
631 s.clone()
632 };
633 format!("\"{}\"", escape_r(&normalized))
634 }
635 serde_json::Value::Bool(true) => "TRUE".to_string(),
636 serde_json::Value::Bool(false) => "FALSE".to_string(),
637 serde_json::Value::Number(n) => n.to_string(),
638 serde_json::Value::Null => "NULL".to_string(),
639 serde_json::Value::Array(arr) => {
640 let items: Vec<String> = arr.iter().map(|v| json_to_r(v, lowercase_enum_values)).collect();
641 format!("c({})", items.join(", "))
642 }
643 serde_json::Value::Object(map) => {
644 let entries: Vec<String> = map
645 .iter()
646 .map(|(k, v)| format!("\"{}\" = {}", escape_r(k), json_to_r(v, lowercase_enum_values)))
647 .collect();
648 format!("list({})", entries.join(", "))
649 }
650 }
651}
652
653fn build_r_visitor(setup_lines: &mut Vec<String>, visitor_spec: &crate::fixture::VisitorSpec) {
655 use std::fmt::Write as FmtWrite;
656 let mut visitor_obj = String::new();
657 let _ = writeln!(visitor_obj, "list(");
658 for (method_name, action) in &visitor_spec.callbacks {
659 emit_r_visitor_method(&mut visitor_obj, method_name, action);
660 }
661 let _ = writeln!(visitor_obj, " )");
662
663 setup_lines.push(format!("visitor <- {visitor_obj}"));
664}
665
666fn build_r_method_call(result_var: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
669 match method_name {
670 "root_child_count" => format!("{result_var}$root_child_count()"),
671 "root_node_type" => format!("{result_var}$root_node_type()"),
672 "named_children_count" => format!("{result_var}$named_children_count()"),
673 "has_error_nodes" => format!("tree_has_error_nodes({result_var})"),
674 "error_count" | "tree_error_count" => format!("tree_error_count({result_var})"),
675 "tree_to_sexp" => format!("tree_to_sexp({result_var})"),
676 "contains_node_type" => {
677 let node_type = args
678 .and_then(|a| a.get("node_type"))
679 .and_then(|v| v.as_str())
680 .unwrap_or("");
681 format!("tree_contains_node_type({result_var}, \"{node_type}\")")
682 }
683 "find_nodes_by_type" => {
684 let node_type = args
685 .and_then(|a| a.get("node_type"))
686 .and_then(|v| v.as_str())
687 .unwrap_or("");
688 format!("find_nodes_by_type({result_var}, \"{node_type}\")")
689 }
690 "run_query" => {
691 let query_source = args
692 .and_then(|a| a.get("query_source"))
693 .and_then(|v| v.as_str())
694 .unwrap_or("");
695 let language = args
696 .and_then(|a| a.get("language"))
697 .and_then(|v| v.as_str())
698 .unwrap_or("");
699 format!("run_query({result_var}, \"{language}\", \"{query_source}\", source)")
700 }
701 _ => {
702 if let Some(args_val) = args {
703 let arg_str = args_val
704 .as_object()
705 .map(|obj| {
706 obj.iter()
707 .map(|(k, v)| {
708 let r_val = json_to_r(v, false);
709 format!("{k} = {r_val}")
710 })
711 .collect::<Vec<_>>()
712 .join(", ")
713 })
714 .unwrap_or_default();
715 format!("{result_var}${method_name}({arg_str})")
716 } else {
717 format!("{result_var}${method_name}()")
718 }
719 }
720 }
721}
722
723fn emit_r_visitor_method(out: &mut String, method_name: &str, action: &CallbackAction) {
725 use std::fmt::Write as FmtWrite;
726
727 let params = match method_name {
729 "visit_link" => "ctx, href, text, title",
730 "visit_image" => "ctx, src, alt, title",
731 "visit_heading" => "ctx, level, text, id",
732 "visit_code_block" => "ctx, lang, code",
733 "visit_code_inline"
734 | "visit_strong"
735 | "visit_emphasis"
736 | "visit_strikethrough"
737 | "visit_underline"
738 | "visit_subscript"
739 | "visit_superscript"
740 | "visit_mark"
741 | "visit_button"
742 | "visit_summary"
743 | "visit_figcaption"
744 | "visit_definition_term"
745 | "visit_definition_description" => "ctx, text",
746 "visit_text" => "ctx, text",
747 "visit_list_item" => "ctx, ordered, marker, text",
748 "visit_blockquote" => "ctx, content, depth",
749 "visit_table_row" => "ctx, cells, is_header",
750 "visit_custom_element" => "ctx, tag_name, html",
751 "visit_form" => "ctx, action_url, method",
752 "visit_input" => "ctx, input_type, name, value",
753 "visit_audio" | "visit_video" | "visit_iframe" => "ctx, src",
754 "visit_details" => "ctx, is_open",
755 "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => "ctx, output",
756 "visit_list_start" => "ctx, ordered",
757 "visit_list_end" => "ctx, ordered, output",
758 _ => "ctx",
759 };
760
761 let _ = writeln!(out, " {method_name} = function({params}) {{");
762 match action {
763 CallbackAction::Skip => {
764 let _ = writeln!(out, " \"skip\"");
765 }
766 CallbackAction::Continue => {
767 let _ = writeln!(out, " \"continue\"");
768 }
769 CallbackAction::PreserveHtml => {
770 let _ = writeln!(out, " \"preserve_html\"");
771 }
772 CallbackAction::Custom { output } => {
773 let escaped = escape_r(output);
774 let _ = writeln!(out, " list(custom = {escaped})");
775 }
776 CallbackAction::CustomTemplate { template } => {
777 let escaped = escape_r(template);
778 let _ = writeln!(out, " list(custom = {escaped})");
779 }
780 }
781 let _ = writeln!(out, " }},");
782}