use lex_babel::format::Format;
use lex_babel::formats::lex::LexFormat;
use lex_babel::formats::markdown::serializer::serialize_to_markdown_with_registry;
use lex_babel::formats::markdown::MarkdownFormat;
use lex_core::lex::loader::DocumentLoader;
use lex_extension::schema::{
BodyKind, BodyPresence, BodyShape, Capabilities, HookSet, RenderHook, Schema,
};
use lex_extension::wire::{Format as WireFormat, LabelCtx, RenderOut};
use lex_extension::{HandlerError, LexHandler};
use lex_extension_host::Registry;
use std::collections::BTreeMap;
#[test]
fn test_annotation_round_trip() {
let md = r#"
<!-- lex:note type=warning -->
This is a warning.
<!-- /lex:note -->
"#;
let doc = MarkdownFormat.parse(md).expect("Failed to parse markdown");
let output = MarkdownFormat
.serialize(&doc)
.expect("Failed to serialize markdown");
println!("Output:\n{output}");
assert!(output.contains("<!-- lex:note type=warning"));
assert!(output.contains("This is a warning."));
assert!(output.contains("-->"));
}
#[test]
fn markdown_to_lex_preserves_blessed_shortcut_form() {
let md = "<!-- lex:title -->\nMy Doc\n<!-- /lex:title -->\n";
let doc = MarkdownFormat.parse(md).expect("parse markdown");
let lex_output = LexFormat::default().serialize(&doc).expect("serialize lex");
assert!(
lex_output.contains(":: title"),
"expected blessed shortcut spelling, got:\n{lex_output}"
);
assert!(
lex_output.contains("My Doc"),
"annotation body lost in output:\n{lex_output}"
);
assert!(
!lex_output.contains("lex.metadata.title"),
"canonical form leaked into output:\n{lex_output}"
);
}
#[test]
fn test_nested_annotations() {
let md = r#"
<!-- lex:outer -->
<!-- lex:inner -->
Nested content
<!-- /lex:inner -->
<!-- /lex:outer -->
"#;
let doc = MarkdownFormat.parse(md).expect("Failed to parse markdown");
let output = MarkdownFormat
.serialize(&doc)
.expect("Failed to serialize markdown");
assert!(output.contains("<!-- lex:outer -->"));
assert!(output.contains("<!-- lex:inner -->"));
assert!(output.contains("Nested content"));
assert!(output.contains("<!-- /lex:inner -->"));
assert!(output.contains("<!-- /lex:outer -->"));
}
fn schema(label: &str, formats: &[&str]) -> Schema {
Schema {
schema_version: 1,
label: label.into(),
description: None,
params: BTreeMap::new(),
attaches_to: vec![
"annotation".into(),
"document".into(),
"session".into(),
"paragraph".into(),
],
body: BodyShape {
kind: BodyKind::None,
presence: BodyPresence::Optional,
description: None,
},
verbatim_label: false,
capabilities: Capabilities::default(),
hooks: HookSet {
render: formats.iter().map(|s| RenderHook::new(*s)).collect(),
..HookSet::default()
},
handler: None,
diagnostics: Vec::new(),
}
}
#[test]
fn note_with_rich_body_round_trips_lex_markdown_lex() {
let lex_src = "1. Heading\n\n \
:: note ::\n \
First paragraph of the note.\n\n \
Second paragraph references rust.\n\n \
- bullet one\n \
- bullet two\n\n \
:: warning ::\n \
Nested annotation body.\n";
let lex_doc = DocumentLoader::from_string(lex_src)
.parse()
.expect("parse original lex");
let md = MarkdownFormat.serialize(&lex_doc).expect("lex → markdown");
assert!(
md.contains("<!-- lex:note -->"),
"outer note open comment missing in:\n{md}"
);
assert!(
md.contains("<!-- /lex:note -->"),
"outer note close comment missing in:\n{md}"
);
assert!(
md.contains("<!-- lex:warning -->"),
"nested warning open comment missing in:\n{md}"
);
assert!(
md.contains("<!-- /lex:warning -->"),
"nested warning close comment missing in:\n{md}"
);
assert!(
md.contains("First paragraph"),
"first paragraph body missing in:\n{md}"
);
assert!(
md.contains("Second paragraph"),
"second paragraph body missing in:\n{md}"
);
assert!(md.contains("bullet one"), "list item lost in:\n{md}");
assert!(
md.contains("Nested annotation body"),
"nested body lost in:\n{md}"
);
let re_imported = MarkdownFormat
.parse(&md)
.expect("markdown → lex roundtrip parse");
let back_to_lex = LexFormat::default()
.serialize(&re_imported)
.expect("lex serialise");
assert!(
back_to_lex.contains(":: note"),
"outer note annotation missing after round trip:\n{back_to_lex}"
);
assert!(
back_to_lex.contains(":: warning"),
"nested warning missing after round trip:\n{back_to_lex}"
);
assert!(
back_to_lex.contains("First paragraph"),
"first paragraph lost after round trip:\n{back_to_lex}"
);
assert!(
back_to_lex.contains("Second paragraph"),
"second paragraph lost after round trip:\n{back_to_lex}"
);
assert!(
back_to_lex.contains("bullet one"),
"list item lost after round trip:\n{back_to_lex}"
);
assert!(
back_to_lex.contains("Nested annotation body"),
"nested body lost after round trip:\n{back_to_lex}"
);
}
struct FixedRender(&'static str);
impl LexHandler for FixedRender {
fn on_render(&self, _: &LabelCtx, _: WireFormat) -> Result<Option<RenderOut>, HandlerError> {
Ok(Some(RenderOut::String {
string: self.0.to_string(),
}))
}
}
#[test]
fn markdown_render_hook_splices_handler_output_into_export() {
let lex_src = "1. Heading\n\n :: acme.task ::\n Default body content.\n";
let lex_doc = DocumentLoader::from_string(lex_src).parse().expect("parse");
let registry = Registry::new();
registry
.register_namespace(
"acme",
vec![schema("acme.task", &["markdown"])],
Box::new(FixedRender("**HANDLER MARKDOWN**")),
)
.expect("register namespace");
let outcome = serialize_to_markdown_with_registry(&lex_doc, ®istry).expect("md serialise");
assert!(
outcome.markdown.contains("**HANDLER MARKDOWN**"),
"handler markdown should be spliced into the export. got:\n{}",
outcome.markdown
);
assert!(
!outcome.markdown.contains("<!-- lex:acme.task"),
"default open comment should be replaced by splice. got:\n{}",
outcome.markdown
);
assert!(
!outcome.markdown.contains("<!-- /lex:acme.task"),
"default close comment should be replaced by splice. got:\n{}",
outcome.markdown
);
assert!(
!outcome.markdown.contains("Default body content."),
"annotation body must be suppressed inside the handler-owned region. got:\n{}",
outcome.markdown
);
}
#[test]
fn unregistered_body_annotation_emits_default_comment_pair() {
let lex_src = "1. Heading\n\n :: acme.task ::\n Annotation body.\n";
let lex_doc = DocumentLoader::from_string(lex_src).parse().expect("parse");
let registry = Registry::new();
let outcome = serialize_to_markdown_with_registry(&lex_doc, ®istry).expect("md serialise");
assert!(
outcome.markdown.contains("<!-- lex:acme.task -->"),
"default open comment missing. got:\n{}",
outcome.markdown
);
assert!(
outcome.markdown.contains("<!-- /lex:acme.task -->"),
"default close comment missing. got:\n{}",
outcome.markdown
);
assert!(
outcome.markdown.contains("Annotation body."),
"body should render between the comment pair. got:\n{}",
outcome.markdown
);
}
#[test]
fn doc_metadata_schemas_fire_during_markdown_serialization() {
let lex_src = ":: doc.title :: My Doc\n\n:: doc.author :: Alice\n\nBody.\n";
let lex_doc = DocumentLoader::from_string(lex_src).parse().expect("parse");
let md = MarkdownFormat
.serialize(&lex_doc)
.expect("markdown serialise");
assert!(
md.starts_with("---\n"),
"YAML frontmatter missing at start of output:\n{md}"
);
assert!(
md.contains("title: \"My Doc\"\n"),
"expected handler-rendered title line (`title: \"My Doc\"`). got:\n{md}"
);
assert!(
md.contains("author: \"Alice\"\n"),
"expected handler-rendered author line. got:\n{md}"
);
}
#[test]
fn lex_metadata_shortcut_falls_back_to_yaml_synthesis() {
let lex_src = ":: title :: My Doc\n\n:: author :: Alice\n\nBody.\n";
let lex_doc = DocumentLoader::from_string(lex_src).parse().expect("parse");
let md = MarkdownFormat
.serialize(&lex_doc)
.expect("markdown serialise");
assert!(md.starts_with("---\n"), "frontmatter missing in:\n{md}");
assert!(
md.contains("title: My Doc"),
"synthesised title missing in:\n{md}"
);
assert!(
md.contains("author: Alice"),
"synthesised author missing in:\n{md}"
);
}