use adoctra_core::{
parse, parse_with_options, tokens_with_options, Attributes, BlockKind, Inline, ParseOptions,
SourceMap, TokenKind,
};
#[test]
fn section_ids_remove_raw_entities_but_keep_protected_spellings() {
for (value, expected) in [
("pass:c,r[(C) ->]", ""),
("pass:[A © A A B]", "_a_b"),
("pass:[<b>Alpha</b> © Beta]", "_alpha_beta"),
("pass:c[<b>Alpha</b> ©]", "_balphab"),
("pass:[&copy;]", ""),
(
"pass:[ �  � &a; &ab123;]",
"_1_1234567_x1_x123456_a_ab123",
),
] {
let source = format!(":v: {value}\n\n== {{v}}");
let result = parse(&source);
assert_eq!(
result.document.flat_blocks()[0].id.as_deref(),
Some(expected),
"{value}"
);
let BlockKind::Heading { inlines, .. } = &result.document.flat_blocks()[0].kind else {
panic!()
};
let Inline::AttributeRef { span, .. } = &inlines[0] else {
panic!()
};
assert_eq!(&source[span.start..span.end], "{v}");
}
}
#[test]
fn malformed_tags_and_entity_grammar_follow_the_reference_without_decoding() {
for (value, expected) in [
("<>", ""),
("<b", "_b"),
("<<b>X</b>", "_x"),
("A <b > B", "_a_b_b"),
("A <b>Ж</b> &amp;copy;", "_a_ж_copy"),
("&AB12; A 󴈿 ", ""),
("&a; &abc123; � ", "_a_abc123_0_x1"),
] {
let result = parse(&format!(":v: pass:[{value}]\n\n== {{v}}"));
assert_eq!(
result.document.flat_blocks()[0].id.as_deref(),
Some(expected),
"{value}"
);
}
let source = format!(":v: pass:[{}A]\n\n== {{v}}", "<".repeat(20_000));
assert_eq!(
parse(&source).document.into_flat_blocks()[0].id.as_deref(),
Some("_a")
);
}
#[test]
fn sanitized_ids_deduplicate_and_forward_references_keep_original_title_nodes() {
let source = ":v: pass:[<b>Alpha</b> ©]\n\n<<_alpha>> <<_alpha_2>>\n\n== {v}\n\n== Alpha";
let result = parse(source);
assert!(result.diagnostics.is_empty(), "{:?}", result.diagnostics);
let blocks = &result.document.flat_blocks();
assert_eq!(blocks[1].id.as_deref(), Some("_alpha"));
assert_eq!(blocks[2].id.as_deref(), Some("_alpha_2"));
let BlockKind::Paragraph { inlines } = &blocks[0].kind else {
panic!()
};
let Inline::CrossReference {
xref,
children,
span,
} = &inlines[0]
else {
panic!()
};
assert_eq!(xref.target, "#_alpha");
assert_eq!(&source[span.start..span.end], "<<_alpha>>");
let Inline::AttributeRef {
value, parts, span, ..
} = &children[0]
else {
panic!()
};
assert_eq!(value.as_deref(), Some("<b>Alpha</b> ©"));
assert_eq!(parts, result.document.attributes.raw_parts("v").unwrap());
assert_eq!(&source[span.start..span.end], "{v}");
}
#[test]
fn formatting_and_anchor_boundaries_do_not_join_a_new_entity_for_id_generation() {
for title in ["{v}*py*;", "{v}link:target[py];", "{v}[[point]]py;"] {
let source = format!(":v: pass:[&co]\n\n== {title}");
assert_eq!(
parse(&source).document.into_flat_blocks()[0].id.as_deref(),
Some("_copy"),
"{title}"
);
}
}
#[test]
fn changed_attribute_snapshots_and_host_raw_values_determine_each_id() {
let source = ":v: pass:[Alpha ©]\n\n== {v}\n\n:v: pass:[Beta A]\n\n== {v}";
let result = parse(source);
assert_eq!(result.document.attributes.get("v"), Some("Alpha ©"));
for (block, value, id) in [
(&result.document.flat_blocks()[0], "Alpha ©", "_alpha"),
(&result.document.flat_blocks()[1], "Beta A", "_beta"),
] {
assert_eq!(block.id.as_deref(), Some(id));
assert_eq!(block.attribute_context.get("v"), Some(value));
}
let mut attributes = Attributes::new();
attributes.set("v", "<b>Host</b> A");
let result = parse_with_options(
":v: ignored\n\n== {v}",
&ParseOptions {
attributes,
..Default::default()
},
);
assert_eq!(
result.document.flat_blocks()[0].id.as_deref(),
Some("_host")
);
assert_eq!(
result.document.attributes.get("v"),
Some("<b>Host</b> A")
);
}
#[test]
fn unicode_crlf_and_partial_input_keep_physical_spans_and_tokens() {
let source = "\u{feff}:v: pass:[<b>Жёлтый</b> A café]\r\n\r\n== {v}";
let mut sources = SourceMap::new();
let source_id = sources.add("section.adoc", source);
let options = ParseOptions {
source_id,
..Default::default()
};
let result = parse_with_options(source, &options);
let block = &result.document.flat_blocks()[0];
assert_eq!(block.id.as_deref(), Some("_жёлтый_café"));
assert_eq!(sources.slice(block.span), Some("== {v}"));
let BlockKind::Heading { inlines, .. } = &block.kind else {
panic!()
};
assert_eq!(sources.slice(inlines[0].span()), Some("{v}"));
assert!(tokens_with_options(source, &options)
.iter()
.any(|t| t.kind == TokenKind::Heading && t.span == block.span));
for end in (0..=source.len()).filter(|&end| source.is_char_boundary(end)) {
let partial = &source[..end];
let _ = parse_with_options(partial, &options);
for token in tokens_with_options(partial, &options) {
assert_eq!(token.span.source_id, source_id);
assert!(token.span.end <= end);
assert!(partial.is_char_boundary(token.span.start));
assert!(partial.is_char_boundary(token.span.end));
}
}
}