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::{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 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"
);
}