1use super::formatting_rules::FormattingRules;
2use lex_core::lex::ast::{
3 elements::{
4 blank_line_group::BlankLineGroup, paragraph::TextLine, sequence_marker::Form,
5 verbatim::VerbatimGroupItemRef, VerbatimLine,
6 },
7 traits::{AstNode, Visitor},
8 Annotation, Definition, Document, List, ListItem, Paragraph, Session, Verbatim,
9};
10
11#[derive(Debug, Clone, Copy, PartialEq)]
12enum MarkerType {
13 Bullet,
14 Numeric,
15 AlphaLower,
16 AlphaUpper,
17 RomanUpper,
18}
19
20struct ListContext {
21 index: usize,
22 marker_type: MarkerType,
23 marker_form: Option<Form>,
24}
25
26impl MarkerType {}
27
28fn format_marker_index(marker_type: MarkerType, index: usize) -> String {
29 match marker_type {
30 MarkerType::Bullet => "-".to_string(),
31 MarkerType::Numeric => index.to_string(),
32 MarkerType::AlphaLower => to_alpha_lower(index),
33 MarkerType::AlphaUpper => to_alpha_upper(index),
34 MarkerType::RomanUpper => to_roman_upper(index),
35 }
36}
37
38fn to_alpha_lower(n: usize) -> String {
39 if (1..=26).contains(&n) {
40 char::from_u32((n as u32) + 96).unwrap().to_string()
41 } else {
42 n.to_string()
43 }
44}
45fn to_alpha_upper(n: usize) -> String {
46 if (1..=26).contains(&n) {
47 char::from_u32((n as u32) + 64).unwrap().to_string()
48 } else {
49 n.to_string()
50 }
51}
52
53fn to_roman_upper(n: usize) -> String {
54 match n {
57 1 => "I".to_string(),
58 2 => "II".to_string(),
59 3 => "III".to_string(),
60 4 => "IV".to_string(),
61 5 => "V".to_string(),
62 6 => "VI".to_string(),
63 7 => "VII".to_string(),
64 8 => "VIII".to_string(),
65 9 => "IX".to_string(),
66 10 => "X".to_string(),
67 11 => "XI".to_string(),
68 12 => "XII".to_string(),
69 13 => "XIII".to_string(),
70 14 => "XIV".to_string(),
71 15 => "XV".to_string(),
72 16 => "XVI".to_string(),
73 17 => "XVII".to_string(),
74 18 => "XVIII".to_string(),
75 19 => "XIX".to_string(),
76 20 => "XX".to_string(),
77 _ => n.to_string(), }
79}
80
81use crate::common::verbatim::VerbatimRegistry;
82
83pub struct LexSerializer {
84 rules: FormattingRules,
85 output: String,
86 indent_level: usize,
87 consecutive_newlines: usize,
88 list_stack: Vec<ListContext>,
89 verbatim_registry: VerbatimRegistry,
90 skip_verbatim_lines: bool,
91 formatted_verbatim_content: Option<String>,
92}
93
94impl LexSerializer {
95 pub fn new(rules: FormattingRules) -> Self {
96 Self {
97 rules,
98 output: String::new(),
99 indent_level: 0,
100 consecutive_newlines: 2, list_stack: Vec::new(),
102 verbatim_registry: VerbatimRegistry::default_with_standard(),
103 skip_verbatim_lines: false,
104 formatted_verbatim_content: None,
105 }
106 }
107
108 pub fn serialize(mut self, doc: &Document) -> Result<String, String> {
109 doc.root.accept(&mut self);
110 Ok(self.output)
111 }
112
113 fn indent(&self) -> String {
114 self.rules.indent_string.repeat(self.indent_level)
115 }
116
117 fn write_line(&mut self, text: &str) {
118 self.output.push_str(&self.indent());
119 self.output.push_str(text);
120 self.output.push('\n');
121 self.consecutive_newlines = 1;
122 }
123
124 fn build_extended_marker(&self) -> String {
129 let mut parts = Vec::new();
130 let len = self.list_stack.len();
131 for (i, ctx) in self.list_stack.iter().enumerate() {
132 let idx = if i < len - 1 {
133 ctx.index - 1
135 } else {
136 ctx.index
138 };
139 parts.push(format_marker_index(ctx.marker_type, idx));
140 }
141 format!("{}.", parts.join("."))
142 }
143
144 fn ensure_blank_lines(&mut self, count: usize) {
145 let target_newlines = count + 1;
146 while self.consecutive_newlines < target_newlines {
147 self.output.push('\n');
148 self.consecutive_newlines += 1;
149 }
150 }
151}
152
153impl Visitor for LexSerializer {
154 fn visit_session(&mut self, session: &Session) {
155 let title = session.title.as_string();
156 if !title.is_empty() {
157 self.ensure_blank_lines(self.rules.session_blank_lines_before);
158 self.write_line(title);
159 self.ensure_blank_lines(self.rules.session_blank_lines_after);
160 self.indent_level += 1;
161 }
162 }
163
164 fn leave_session(&mut self, session: &Session) {
165 if !session.title.as_string().is_empty() {
166 self.indent_level -= 1;
167 }
168 }
169
170 fn visit_paragraph(&mut self, _paragraph: &Paragraph) {
171 }
175
176 fn visit_text_line(&mut self, text_line: &TextLine) {
177 let text = text_line.text().trim_end();
178 self.write_line(text);
179 }
180
181 fn visit_blank_line_group(&mut self, group: &BlankLineGroup) {
182 if group.count == 0 {
183 return;
184 }
185
186 let count = if self.rules.max_blank_lines > 0 {
187 std::cmp::min(group.count, self.rules.max_blank_lines)
188 } else {
189 group.count
190 };
191 self.ensure_blank_lines(count);
192 }
193
194 fn visit_list(&mut self, list: &List) {
195 let marker_type = if let Some(marker) = &list.marker {
197 use lex_core::lex::ast::elements::DecorationStyle;
198 match marker.style {
199 DecorationStyle::Plain => MarkerType::Bullet,
200 DecorationStyle::Numerical => MarkerType::Numeric,
201 DecorationStyle::Alphabetical => {
202 let text = marker.as_str();
203 if text.chars().next().is_some_and(|c| c.is_uppercase()) {
204 MarkerType::AlphaUpper
205 } else {
206 MarkerType::AlphaLower
207 }
208 }
209 DecorationStyle::Roman => MarkerType::RomanUpper,
210 }
211 } else {
212 MarkerType::Bullet
213 };
214
215 let marker_form = list.marker.as_ref().map(|marker| marker.form);
217
218 self.list_stack.push(ListContext {
219 marker_type,
220 marker_form,
221 index: 1,
222 });
223 }
224
225 fn leave_list(&mut self, _list: &List) {
226 self.list_stack.pop();
227 }
228
229 fn visit_list_item(&mut self, list_item: &ListItem) {
230 let is_extended = self
231 .list_stack
232 .iter()
233 .any(|ctx| matches!(ctx.marker_form, Some(Form::Extended)));
234
235 let marker = if self.rules.normalize_seq_markers {
236 if is_extended {
237 self.build_extended_marker()
239 } else {
240 let context = self
241 .list_stack
242 .last()
243 .expect("List stack empty in list item");
244 match context.marker_type {
245 MarkerType::Bullet => self.rules.unordered_seq_marker.to_string(),
246 MarkerType::Numeric => format!("{}.", context.index),
247 MarkerType::AlphaLower => format!("{}.", to_alpha_lower(context.index)),
248 MarkerType::AlphaUpper => format!("{}.", to_alpha_upper(context.index)),
249 MarkerType::RomanUpper => format!("{}.", to_roman_upper(context.index)),
250 }
251 }
252 } else {
253 list_item.marker.as_string().to_string()
254 };
255
256 let context = self
257 .list_stack
258 .last_mut()
259 .expect("List stack empty in list item");
260 context.index += 1;
261
262 let text = if !list_item.text.is_empty() {
264 list_item.text[0].as_string().trim_end()
265 } else {
266 ""
267 };
268
269 let line = if text.is_empty() {
270 marker
271 } else {
272 format!("{marker} {text}")
273 };
274
275 self.write_line(&line);
276 self.indent_level += 1;
277 }
278
279 fn leave_list_item(&mut self, _list_item: &ListItem) {
280 self.indent_level -= 1;
281 }
282
283 fn visit_definition(&mut self, definition: &Definition) {
284 let subject = definition.subject.as_string();
285 self.write_line(&format!("{subject}:"));
286 self.indent_level += 1;
287 }
288
289 fn leave_definition(&mut self, _definition: &Definition) {
290 self.indent_level -= 1;
291 }
292
293 fn visit_annotation(&mut self, annotation: &Annotation) {
294 let label = &annotation.data.label.value;
295 let params = &annotation.data.parameters;
296
297 let mut header = format!(":: {label}");
298 if !params.is_empty() {
299 for param in params {
300 header.push(' ');
301 header.push_str(¶m.key);
302 header.push('=');
303 header.push_str(¶m.value);
304 }
305 }
306
307 if annotation.children.is_empty() {
309 header.push_str(" ::");
310 }
311
312 self.write_line(&header);
313
314 if !annotation.children.is_empty() {
315 self.indent_level += 1;
316 }
317 }
318
319 fn leave_annotation(&mut self, annotation: &Annotation) {
320 if !annotation.children.is_empty() {
321 self.indent_level -= 1;
322 self.write_line("::");
323 }
324 }
325
326 fn visit_verbatim_block(&mut self, verbatim: &Verbatim) {
327 let label = &verbatim.closing_data.label.value;
328
329 if let Some(handler) = self.verbatim_registry.get(label) {
331 if let Ok(Some(content)) = handler.format_content(verbatim) {
333 self.formatted_verbatim_content = Some(content);
334 self.skip_verbatim_lines = true;
335 } else {
336 self.formatted_verbatim_content = None;
337 self.skip_verbatim_lines = false;
338 }
339 } else {
340 self.formatted_verbatim_content = None;
341 self.skip_verbatim_lines = false;
342 }
343 }
344
345 fn visit_verbatim_group(&mut self, group: &VerbatimGroupItemRef) {
346 let subject = group.subject.as_string();
347 self.write_line(&format!("{subject}:"));
348 self.indent_level += 1;
349 }
350
351 fn leave_verbatim_group(&mut self, _group: &VerbatimGroupItemRef) {
352 self.indent_level -= 1;
353 }
354
355 fn visit_verbatim_line(&mut self, verbatim_line: &VerbatimLine) {
356 if !self.skip_verbatim_lines {
357 self.write_line(verbatim_line.content.as_string());
358 }
359 }
360
361 fn leave_verbatim_block(&mut self, verbatim: &Verbatim) {
362 if let Some(content) = self.formatted_verbatim_content.take() {
364 self.output.push_str(&content);
365 if !content.ends_with('\n') {
367 self.output.push('\n');
368 }
369 }
370
371 let label = &verbatim.closing_data.label.value;
372 let mut footer = format!(":: {label}");
373 if !verbatim.closing_data.parameters.is_empty() {
374 for param in &verbatim.closing_data.parameters {
375 footer.push(' ');
376 footer.push_str(¶m.key);
377 footer.push('=');
378 footer.push_str(¶m.value);
379 }
380 }
381 footer.push_str(" ::");
382 self.write_line(&footer);
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::format::Format;
390 use lex_core::lex::testing::lexplore::{ElementType, Lexplore};
391 use lex_core::lex::testing::text_diff::assert_text_eq;
392
393 fn format_source(source: &str) -> String {
394 let format = super::super::LexFormat::default();
395 let doc = format.parse(source).unwrap();
396 let rules = FormattingRules::default();
397 let mut serializer = LexSerializer::new(rules);
398 doc.accept(&mut serializer);
399 serializer.output
400 }
401
402 #[test]
405 fn test_paragraph_01_oneline() {
406 let source = Lexplore::load(ElementType::Paragraph, 1).source();
407 let formatted = format_source(&source);
408 assert_text_eq(
409 &formatted,
410 "This is a simple paragraph with just one line.\n",
411 );
412 }
413
414 #[test]
415 fn test_paragraph_02_multiline() {
416 let source = Lexplore::load(ElementType::Paragraph, 2).source();
417 let formatted = format_source(&source);
418 assert!(formatted.contains("This is a multi-line paragraph"));
419 assert!(formatted.contains("second line"));
420 assert!(formatted.contains("third line"));
421 }
422
423 #[test]
424 fn test_paragraph_03_special_chars() {
425 let source = Lexplore::load(ElementType::Paragraph, 3).source();
426 let formatted = format_source(&source);
427 assert!(formatted.contains("!@#$%^&*()"));
428 }
429
430 #[test]
433 fn test_session_01_simple() {
434 let source = Lexplore::load(ElementType::Session, 1).source();
435 let formatted = format_source(&source);
436 assert!(formatted.contains("Introduction\n"));
437 assert!(formatted.contains(" This is a simple session"));
438 }
439
440 #[test]
441 fn test_session_02_numbered_title() {
442 let source = Lexplore::load(ElementType::Session, 2).source();
443 let formatted = format_source(&source);
444 assert!(formatted.contains("1. Introduction:\n"));
445 }
446
447 #[test]
448 fn test_session_05_nested() {
449 let source = Lexplore::load(ElementType::Session, 5).source();
450 let formatted = format_source(&source);
451 assert!(formatted.contains("1. Introduction {{session-title}}\n"));
453 assert!(formatted.contains(" This is the content of the session"));
454 }
455
456 #[test]
459 fn test_list_01_dash() {
460 let source = Lexplore::load(ElementType::List, 1).source();
461 let formatted = format_source(&source);
462 assert!(formatted.contains("- First item\n"));
463 assert!(formatted.contains("- Second item\n"));
464 }
465
466 #[test]
467 fn test_list_02_numbered() {
468 let source = Lexplore::load(ElementType::List, 2).source();
469 let formatted = format_source(&source);
470 assert!(formatted.contains("1. "));
472 assert!(formatted.contains("2. "));
473 assert!(formatted.contains("3. "));
474 }
475
476 #[test]
477 fn test_list_03_alphabetical() {
478 let source = Lexplore::load(ElementType::List, 3).source();
479 let formatted = format_source(&source);
480 assert!(formatted.contains("a. "));
481 assert!(formatted.contains("b. "));
482 assert!(formatted.contains("c. "));
483 }
484
485 #[test]
486 fn test_list_04_mixed_markers() {
487 let source = Lexplore::load(ElementType::List, 4).source();
488 let formatted = format_source(&source);
489 assert!(formatted.contains("1. First item\n"));
491 assert!(formatted.contains("2. Second item\n"));
492 assert!(formatted.contains("3. Third item\n"));
493 }
494
495 #[test]
496 fn test_list_07_nested_simple() {
497 let source = Lexplore::load(ElementType::List, 7).source();
498 let formatted = format_source(&source);
499 assert!(formatted.contains("- First outer item\n"));
501 assert!(formatted.contains(" - First nested item\n"));
502 }
503
504 #[test]
505 fn test_list_extended_markers_preserved() {
506 let source = "1.2.3 Item one\n1.2.4 Item two\n";
510 let formatted = format_source(source);
511 assert!(formatted.contains("1. Item one\n"));
512 assert!(formatted.contains("2. Item two\n"));
513 }
514
515 #[test]
516 fn test_list_extended_markers_nested_normalization() {
517 let source = "Test:\n\n1. Outer level one\n 1.a Middle level one\n 1.a.1 Inner level one\n 1.a.2 Inner level two\n 1.b Middle level two\n2. Outer level two\n";
519 let formatted = format_source(source);
520 assert!(
522 formatted.contains("1. Outer level one"),
523 "Expected '1. Outer level one' in: {formatted}"
524 );
525 assert!(
526 formatted.contains("2. Outer level two"),
527 "Expected '2. Outer level two' in: {formatted}"
528 );
529 }
530
531 #[test]
532 fn test_list_12_extended_form_fixture() {
533 let source = Lexplore::load(ElementType::List, 12).source();
534 let formatted = format_source(&source);
535 let formatted_again = format_source(&formatted);
536 assert_text_eq(&formatted, &formatted_again);
537 }
538
539 #[test]
542 fn test_definition_01_simple() {
543 let source = Lexplore::load(ElementType::Definition, 1).source();
544 let formatted = format_source(&source);
545 assert!(formatted.contains("Cache:\n"));
546 assert!(formatted.contains(" Temporary storage"));
547 }
548
549 #[test]
550 fn test_definition_02_multi_paragraph() {
551 let source = Lexplore::load(ElementType::Definition, 2).source();
552 let formatted = format_source(&source);
553 assert!(formatted.contains("Microservice:\n"));
555 assert!(formatted.contains(" An architectural style"));
556 assert!(formatted.contains(" Each service is independently"));
557 }
558
559 #[test]
562 fn test_verbatim_01_simple_code() {
563 let source = Lexplore::load(ElementType::Verbatim, 1).source();
564 let formatted = format_source(&source);
565 assert!(formatted.contains(":: javascript"));
566 assert!(formatted.contains("function hello()"));
567 }
568
569 #[test]
570 fn test_verbatim_02_with_caption() {
571 let source = Lexplore::load(ElementType::Verbatim, 2).source();
572 let formatted = format_source(&source);
573 assert!(formatted.contains("API Response:"));
575 }
576
577 #[test]
580 fn test_annotation_01_marker_simple() {
581 let source = Lexplore::load(ElementType::Annotation, 1).source();
582 let formatted = format_source(&source);
583 assert_eq!(formatted, ":: note\n::\n");
585 }
586
587 #[test]
588 fn test_annotation_02_with_params() {
589 let source = Lexplore::load(ElementType::Annotation, 2).source();
590 let formatted = format_source(&source);
591 assert_eq!(formatted, ":: warning severity=high\n::\n");
593 }
594
595 #[test]
596 fn test_annotation_05_block_paragraph() {
597 let source = Lexplore::load(ElementType::Annotation, 5).source();
598 let formatted = format_source(&source);
599 assert_eq!(
601 formatted,
602 ":: note\n This is an important note that requires a detailed explanation.\n::\n"
603 );
604 }
605
606 #[test]
610 fn test_round_trip_paragraph_01() {
611 let source = Lexplore::load(ElementType::Paragraph, 1).source();
612 let formatted = format_source(&source);
613 let formatted_again = format_source(&formatted);
614 assert_text_eq(&formatted, &formatted_again);
615 }
616
617 #[test]
618 fn test_round_trip_paragraph_02_multiline() {
619 let source = Lexplore::load(ElementType::Paragraph, 2).source();
620 let formatted = format_source(&source);
621 let formatted_again = format_source(&formatted);
622 assert_text_eq(&formatted, &formatted_again);
623 }
624
625 #[test]
626 fn test_round_trip_session_01() {
627 let source = Lexplore::load(ElementType::Session, 1).source();
628 let formatted = format_source(&source);
629 let formatted_again = format_source(&formatted);
630 assert_text_eq(&formatted, &formatted_again);
631 }
632
633 #[test]
634 fn test_round_trip_session_02_numbered() {
635 let source = Lexplore::load(ElementType::Session, 2).source();
636 let formatted = format_source(&source);
637 let formatted_again = format_source(&formatted);
638 assert_text_eq(&formatted, &formatted_again);
639 }
640
641 #[test]
642 fn test_round_trip_list_01_dash() {
643 let source = Lexplore::load(ElementType::List, 1).source();
644 let formatted = format_source(&source);
645 let formatted_again = format_source(&formatted);
646 assert_text_eq(&formatted, &formatted_again);
647 }
648
649 #[test]
650 fn test_round_trip_list_02_numbered() {
651 let source = Lexplore::load(ElementType::List, 2).source();
652 let formatted = format_source(&source);
653 let formatted_again = format_source(&formatted);
654 assert_text_eq(&formatted, &formatted_again);
655 }
656
657 #[test]
658 fn test_round_trip_list_03_alphabetical() {
659 let source = Lexplore::load(ElementType::List, 3).source();
660 let formatted = format_source(&source);
661 let formatted_again = format_source(&formatted);
662 assert_text_eq(&formatted, &formatted_again);
663 }
664
665 #[test]
666 fn test_round_trip_list_04_mixed_markers() {
667 let source = Lexplore::load(ElementType::List, 4).source();
668 let formatted = format_source(&source);
669 let formatted_again = format_source(&formatted);
670 assert_text_eq(&formatted, &formatted_again);
671 }
672
673 #[test]
674 fn test_round_trip_list_07_nested() {
675 let source = Lexplore::load(ElementType::List, 7).source();
676 let formatted = format_source(&source);
677 let formatted_again = format_source(&formatted);
678 assert_text_eq(&formatted, &formatted_again);
679 }
680
681 #[test]
682 fn test_round_trip_definition_01() {
683 let source = Lexplore::load(ElementType::Definition, 1).source();
684 let formatted = format_source(&source);
685 let formatted_again = format_source(&formatted);
686 assert_text_eq(&formatted, &formatted_again);
687 }
688
689 #[test]
690 fn test_round_trip_definition_02_multi() {
691 let source = Lexplore::load(ElementType::Definition, 2).source();
692 let formatted = format_source(&source);
693 let formatted_again = format_source(&formatted);
694 assert_text_eq(&formatted, &formatted_again);
695 }
696
697 #[test]
698 fn test_round_trip_verbatim_01() {
699 let source = Lexplore::load(ElementType::Verbatim, 1).source();
700 let formatted = format_source(&source);
701 let formatted_again = format_source(&formatted);
702 assert_text_eq(&formatted, &formatted_again);
703 }
704
705 #[test]
706 fn test_round_trip_verbatim_02_caption() {
707 let source = Lexplore::load(ElementType::Verbatim, 2).source();
708 let formatted = format_source(&source);
709 let formatted_again = format_source(&formatted);
710 assert_text_eq(&formatted, &formatted_again);
711 }
712
713 #[test]
714 fn test_verbatim_03_table_formatting() {
715 let source =
717 "Table Example:\n | A | B |\n |---|---|\n | 1 | 2 |\n:: doc.table ::\n";
718 let formatted = format_source(source);
720
721 assert!(formatted.contains("| A | B |"));
725 assert!(formatted.contains("| --- | --- |"));
726 assert!(formatted.contains("| 1 | 2 |"));
727
728 let unformatted = "Table Example:\n |A|B|\n |-|-|\n |1|2|\n:: doc.table ::\n";
730 let formatted_2 = format_source(unformatted);
731
732 assert!(formatted_2.contains("| A | B |"));
734 assert!(formatted_2.contains("| --- | --- |"));
735 assert!(formatted_2.contains("| 1 | 2 |"));
736 }
737
738 #[test]
739 fn test_verbatim_04_user_repro() {
740 let source = " The Table:\n | Markup Language | Great |\n |--------------------|--------|\n | Markdown | No |\n | Lex | Yes |\n :: doc.table ::\n";
744
745 let formatted = format_source(source);
746
747 let table_start = formatted
750 .find("| Markup Language | Great |")
751 .expect("Table start not found");
752 let separator = formatted
753 .find("| --------------- | ----- |")
754 .expect("Separator not found");
755 let footer_start = formatted.find(":: doc.table").expect("Footer not found");
756
757 assert!(table_start < separator);
758 assert!(separator < footer_start);
759 }
760}