use super::tables::{build_grid, Slot};
use super::{FormattingRules, LexSerializer};
use crate::format::Format;
use lex_core::lex::ast::text_content::TextContent;
use lex_core::lex::ast::traits::AstNode;
use lex_core::lex::ast::{ContentItem, TableCell, TableRow};
use lex_core::lex::testing::lexplore::{ElementType, Lexplore};
use lex_core::lex::testing::text_diff::assert_text_eq;
fn format_source(source: &str) -> String {
let format = super::super::LexFormat::default();
let doc = format.parse(source).unwrap();
let rules = FormattingRules::default();
let mut serializer = LexSerializer::new(rules);
doc.accept(&mut serializer);
serializer.output
}
fn format_full(source: &str) -> String {
use crate::format::Format;
let format = super::super::LexFormat::default();
let doc = format.parse(source).unwrap();
format.serialize(&doc).unwrap()
}
#[test]
fn shortcut_form_round_trips_to_shortcut_spelling() {
let formatted = format_source(":: author :: Alice\n\nBody.\n");
assert!(
formatted.contains(":: author"),
"shortcut spelling should round-trip; got: {formatted}"
);
assert!(
!formatted.contains("lex.metadata.author"),
"canonical spelling must not leak into output: {formatted}"
);
}
#[test]
fn stripped_form_round_trips_to_stripped_spelling() {
let formatted = format_source(":: metadata.category :: tech\n\nBody.\n");
assert!(
formatted.contains(":: metadata.category"),
"stripped spelling should round-trip; got: {formatted}"
);
assert!(
!formatted.contains("lex.metadata.category"),
"canonical spelling must not leak: {formatted}"
);
}
#[test]
fn canonical_form_round_trips_unchanged() {
let formatted = format_source(":: lex.metadata.title :: My Doc\n\nBody.\n");
assert!(
formatted.contains(":: lex.metadata.title"),
"canonical spelling should round-trip; got: {formatted}"
);
}
#[test]
fn community_form_round_trips_unchanged() {
let formatted = format_source(":: acme.task id=42 :: foo\n\nBody.\n");
assert!(
formatted.contains(":: acme.task"),
"community label should round-trip; got: {formatted}"
);
}
#[test]
fn verbatim_shortcut_closer_round_trips() {
let formatted = format_source("Photo subject:\n alt text\n:: image src=\"x.png\" ::\n");
assert!(
formatted.contains(":: image"),
"verbatim closer should preserve shortcut: {formatted}"
);
assert!(
!formatted.contains("lex.media.image"),
"canonical must not leak: {formatted}"
);
}
#[test]
fn test_paragraph_01_oneline() {
let source = Lexplore::load(ElementType::Paragraph, 1).source();
let formatted = format_source(&source);
assert_text_eq(
&formatted,
"This is a simple paragraph with just one line.\n",
);
}
#[test]
fn test_paragraph_02_multiline() {
let source = Lexplore::load(ElementType::Paragraph, 2).source();
let formatted = format_source(&source);
assert!(formatted.contains("This is a multi-line paragraph"));
assert!(formatted.contains("second line"));
assert!(formatted.contains("third line"));
}
#[test]
fn test_paragraph_03_special_chars() {
let source = Lexplore::load(ElementType::Paragraph, 3).source();
let formatted = format_source(&source);
assert!(formatted.contains("!@#$%^&*()"));
}
#[test]
fn test_session_01_simple() {
let source = Lexplore::load(ElementType::Session, 1).source();
let formatted = format_source(&source);
assert!(formatted.contains("Introduction\n"));
assert!(formatted.contains(" This is a simple session"));
}
#[test]
fn test_session_02_numbered_title() {
let source = Lexplore::load(ElementType::Session, 2).source();
let formatted = format_source(&source);
assert!(formatted.contains("1. Introduction:\n"));
}
#[test]
fn test_session_05_nested() {
let source = Lexplore::load(ElementType::Session, 5).source();
let formatted = format_source(&source);
assert!(formatted.contains("1. Introduction {{session-title}}\n"));
assert!(formatted.contains(" This is the content of the session"));
}
#[test]
fn test_list_01_dash() {
let source = Lexplore::load(ElementType::List, 1).source();
let formatted = format_source(&source);
assert!(formatted.contains("- First item\n"));
assert!(formatted.contains("- Second item\n"));
}
#[test]
fn test_list_02_numbered() {
let source = Lexplore::load(ElementType::List, 2).source();
let formatted = format_source(&source);
assert!(formatted.contains("1. "));
assert!(formatted.contains("2. "));
assert!(formatted.contains("3. "));
}
#[test]
fn test_list_03_alphabetical() {
let source = Lexplore::load(ElementType::List, 3).source();
let formatted = format_source(&source);
assert!(formatted.contains("a. "));
assert!(formatted.contains("b. "));
assert!(formatted.contains("c. "));
}
#[test]
fn test_list_04_mixed_markers() {
let source = Lexplore::load(ElementType::List, 4).source();
let formatted = format_source(&source);
assert!(formatted.contains("1. First item\n"));
assert!(formatted.contains("2. Second item\n"));
assert!(formatted.contains("3. Third item\n"));
}
#[test]
fn test_list_07_nested_simple() {
let source = Lexplore::load(ElementType::List, 7).source();
let formatted = format_source(&source);
assert!(formatted.contains("- First outer item\n"));
assert!(formatted.contains(" - First nested item\n"));
}
#[test]
fn test_list_extended_markers_preserved() {
let source = "1.2.3 Item one\n1.2.4 Item two\n";
let formatted = format_source(source);
assert!(formatted.contains("1. Item one\n"));
assert!(formatted.contains("2. Item two\n"));
}
#[test]
fn test_list_extended_markers_nested_normalization() {
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";
let formatted = format_source(source);
assert!(
formatted.contains("1. Outer level one"),
"Expected '1. Outer level one' in: {formatted}"
);
assert!(
formatted.contains("2. Outer level two"),
"Expected '2. Outer level two' in: {formatted}"
);
}
#[test]
fn test_list_12_extended_form_fixture() {
let source = Lexplore::load(ElementType::List, 12).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_definition_01_simple() {
let source = Lexplore::load(ElementType::Definition, 1).source();
let formatted = format_source(&source);
assert!(formatted.contains("Cache:\n"));
assert!(formatted.contains(" Temporary storage"));
}
#[test]
fn test_definition_02_multi_paragraph() {
let source = Lexplore::load(ElementType::Definition, 2).source();
let formatted = format_source(&source);
assert!(formatted.contains("Microservice:\n"));
assert!(formatted.contains(" An architectural style"));
assert!(formatted.contains(" Each service is independently"));
}
#[test]
fn test_verbatim_01_simple_code() {
let source = Lexplore::load(ElementType::Verbatim, 1).source();
let formatted = format_source(&source);
assert!(formatted.contains(":: javascript"));
assert!(formatted.contains("function hello()"));
}
#[test]
fn test_verbatim_02_with_caption() {
let source = Lexplore::load(ElementType::Verbatim, 2).source();
let formatted = format_source(&source);
assert!(formatted.contains("API Response:"));
}
#[test]
fn test_annotation_01_marker_simple() {
let source = Lexplore::load(ElementType::Annotation, 1).source();
let formatted = format_full(&source);
assert_eq!(formatted, ":: note ::\n");
}
#[test]
fn test_annotation_02_with_params() {
let source = Lexplore::load(ElementType::Annotation, 2).source();
let formatted = format_full(&source);
assert_eq!(formatted, ":: warning severity=high ::\n");
}
#[test]
fn test_annotation_multi_param_keeps_comma_separator() {
let formatted = format_full(":: warning type=critical, id=123 ::\n");
assert_eq!(formatted, ":: warning type=critical, id=123 ::\n");
assert_eq!(format_full(&formatted), formatted, "must be idempotent");
}
#[test]
fn test_annotation_05_block_paragraph() {
let source = Lexplore::load(ElementType::Annotation, 5).source();
let formatted = format_full(&source);
assert_eq!(
formatted,
":: note ::\n This is an important note that requires a detailed explanation.\n"
);
}
#[test]
fn test_round_trip_paragraph_01() {
let source = Lexplore::load(ElementType::Paragraph, 1).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_paragraph_02_multiline() {
let source = Lexplore::load(ElementType::Paragraph, 2).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_session_01() {
let source = Lexplore::load(ElementType::Session, 1).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_session_02_numbered() {
let source = Lexplore::load(ElementType::Session, 2).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_list_01_dash() {
let source = Lexplore::load(ElementType::List, 1).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_list_02_numbered() {
let source = Lexplore::load(ElementType::List, 2).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_list_03_alphabetical() {
let source = Lexplore::load(ElementType::List, 3).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_list_04_mixed_markers() {
let source = Lexplore::load(ElementType::List, 4).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_list_07_nested() {
let source = Lexplore::load(ElementType::List, 7).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn table_cell_with_block_content_does_not_strand_children() {
let source = "\
Data:
| Name | Skills |
| Alice | - Python |
| | - Rust |
| Bob | Text |
";
let formatted = format_source(source);
for line in formatted.lines() {
assert!(
!line.trim_start().starts_with("- ") || line.contains('|'),
"stranded list line escaped the table:\n{formatted}"
);
}
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_definition_01() {
let source = Lexplore::load(ElementType::Definition, 1).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_definition_02_multi() {
let source = Lexplore::load(ElementType::Definition, 2).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_verbatim_01() {
let source = Lexplore::load(ElementType::Verbatim, 1).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_round_trip_verbatim_02_caption() {
let source = Lexplore::load(ElementType::Verbatim, 2).source();
let formatted = format_source(&source);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_verbatim_03_table_formatting() {
let source = "Table Example:\n | A | B |\n |---|---|\n | 1 | 2 |\n:: table ::\n";
let formatted = format_source(source);
assert!(formatted.contains("| A | B |"));
assert!(formatted.contains("| --- | --- |"));
assert!(formatted.contains("| 1 | 2 |"));
let unformatted = "Table Example:\n |A|B|\n |-|-|\n |1|2|\n:: table ::\n";
let formatted_2 = format_source(unformatted);
assert!(formatted_2.contains("| A | B |"));
assert!(formatted_2.contains("| --- | --- |"));
assert!(formatted_2.contains("| 1 | 2 |"));
}
#[test]
fn test_round_trip_paragraph_then_verbatim_lex505() {
let source =
"Doc\n===\n\nIntro paragraph.\n\nCode Example:\n\n fn main() {}\n\n:: rust ::\n";
let formatted = format_source(source);
assert!(
formatted.contains("Intro paragraph.\n\nCode Example:"),
"expected blank line between paragraph and verbatim subject, got:\n{formatted}"
);
let formatted_again = format_source(&formatted);
assert_text_eq(&formatted, &formatted_again);
}
#[test]
fn test_verbatim_04_user_repro() {
let source = " The Table:\n | Markup Language | Great |\n |--------------------|--------|\n | Markdown | No |\n | Lex | Yes |\n :: table ::\n";
let formatted = format_source(source);
let table_start = formatted
.find("| Markup Language | Great |")
.expect("Table start not found");
let separator = formatted
.find("| --------------- | ----- |")
.expect("Separator not found");
let footer_start = formatted.find(":: table ::").expect("Footer not found");
assert!(table_start < separator);
assert!(separator < footer_start);
}
#[test]
fn build_grid_pads_hole_before_trailing_rowspan() {
let tc = |s: &str| TextContent::from_string(s.to_string(), None);
let row0 = TableRow::new(vec![
TableCell::new(tc("a")),
TableCell::new(tc("b")),
TableCell::new(tc("c")).with_span(1, 2),
]);
let row1 = TableRow::new(vec![TableCell::new(tc("d"))]);
let grid = build_grid(&[&row0, &row1]);
let render = |slots: &[Slot]| {
slots
.iter()
.map(|s| s.text().to_string())
.collect::<Vec<_>>()
};
assert_eq!(render(&grid[0]), ["a", "b", "c"]);
assert_eq!(
render(&grid[1]),
["d", "", "^^"],
"hole padded empty, trailing rowspan marker kept"
);
}
#[test]
fn test_leading_annotation_stays_above_title_with_subtitle() {
let source = "\
:: author :: Yuval Noah Harari
Sapiens:
A Brief History of Humankind
This document has annotations before the title with subtitle.
";
let formatted = format_full(source);
let author_at = formatted.find(":: author").expect("annotation emitted");
let title_at = formatted.find("Sapiens:").expect("title emitted");
assert!(
author_at < title_at,
"leading annotation must stay above the title; got:\n{formatted}"
);
assert_eq!(
format_full(&formatted),
formatted,
"leading-annotation/title ordering must be idempotent"
);
}
#[test]
fn test_multiple_leading_annotations_keep_first_paragraph_below() {
let source = "\
:: foo ::
:: bar ::
Some text here.
There is something in the way she moves.
";
let formatted = format_full(source);
let foo_at = formatted.find(":: foo").expect("foo emitted");
let bar_at = formatted.find(":: bar").expect("bar emitted");
let text_at = formatted
.find("Some text here.")
.expect("paragraph emitted");
assert!(
foo_at < bar_at && bar_at < text_at,
"leading annotations must stay above the first paragraph; got:\n{formatted}"
);
assert_eq!(
format_full(&formatted),
formatted,
"ordering must be idempotent"
);
}
#[test]
fn test_title_without_leading_annotations_stays_at_head() {
let formatted = format_full("Sapiens:\nA Brief History of Humankind\n\nBody line.\n");
assert_eq!(
formatted,
"Sapiens:\nA Brief History of Humankind\n\nBody line.\n"
);
}
#[test]
fn verbatim_subject_with_trailing_space_round_trips_lex790() {
let source = "Doc\n===\n\nThe Tower of Babel: \n Body line one.\n:: image ref=x.jpg ::\n";
let formatted = format_source(source);
assert!(
formatted.contains("The Tower of Babel:\n"),
"subject must keep exactly one colon; got:\n{formatted}"
);
assert!(
!formatted.contains("Babel::"),
"subject colon must not be doubled; got:\n{formatted}"
);
let again = format_source(&formatted);
assert_text_eq(&formatted, &again);
}
#[test]
fn multiline_table_cell_stacks_and_round_trips_lex790() {
let source = "Log:\n | Trial | Result |\n\n | Trial 1 | Successful |\n | | after 48 hours |\n\n | Trial 2 | No growth |\n:: table ::\n";
let formatted = format_source(source);
assert!(
formatted.contains("| Trial 1 | Successful |"),
"first physical line of the multi-line row missing:\n{formatted}"
);
assert!(
formatted.contains("| | after 48 hours |"),
"continuation line of the multi-line cell missing:\n{formatted}"
);
for line in formatted.lines() {
let t = line.trim();
if t.starts_with('|') {
assert!(
t.ends_with('|'),
"pipe row split across lines (lex#790):\n{formatted}"
);
}
}
let format = super::super::LexFormat::default();
let doc = format.parse(&formatted).unwrap();
let table = doc
.root
.children
.iter()
.find_map(|item| match item {
ContentItem::Table(t) => Some(t),
_ => None,
})
.expect("table must survive round-trip, not collapse to paragraphs");
assert_eq!(table.header_rows.len(), 1);
assert_eq!(table.body_rows.len(), 2);
assert_eq!(
table.body_rows[0].cells[1].content.as_string(),
"Successful\nafter 48 hours",
"multi-line cell content must survive the round-trip"
);
let again = format_source(&formatted);
assert_text_eq(&formatted, &again);
}