use super::{escape_table_pipes, has_matching_ticks, write_table_pipes, Info, State};
use crate::{
markdown::Options,
stream::{EventKind, Tag, Text},
Document, NodeId, NodeSyntax, ParseOptions,
};
use alloc::string::String;
fn empty() -> Document<String> {
Document::parse(String::new(), &ParseOptions::default()).unwrap()
}
fn paragraph(document: &mut Document<String>, parent: NodeId, value: &str) -> NodeId {
let node = document
.append_child(parent, EventKind::Start(Tag::Paragraph { tight: false }))
.unwrap();
document
.append_child(node, EventKind::Text(Text::from(String::from(value))))
.unwrap();
node
}
#[test]
fn serializes_inline_documents_and_footnote_subtrees() {
let options = Options::default();
let mut state = State::new(&options);
let mut document = empty();
let root = document.root();
document
.append_child(root, EventKind::Text(Text::Literal("inline")))
.unwrap();
assert_eq!(
state.handle(&document, root, &Info::new("", "")).unwrap(),
"inline"
);
let mut document = empty();
let note = document
.append_child(
document.root(),
EventKind::Start(Tag::FootnoteDefinition(Text::Literal("note"))),
)
.unwrap();
paragraph(&mut document, note, "first");
paragraph(&mut document, note, "second");
let rendered = state.handle(&document, note, &Info::new("", "")).unwrap();
assert!(rendered.contains(" second"), "{:?}", rendered);
}
#[test]
fn handles_adjacent_text_nodes_and_phrasing_lookahead_errors() {
let options = Options::default();
let mut state = State::new(&options);
let mut document = empty();
let root = document.root();
let node = paragraph(&mut document, root, "first");
document
.append_child(node, EventKind::Text(Text::Literal("second")))
.unwrap();
state
.container_phrasing(&document, node, &Info::new("", ""))
.unwrap();
assert_eq!(core::mem::take(&mut state.output), "firstsecond");
document
.append_child(node, EventKind::Start(Tag::Item { block_end: false }))
.unwrap();
let options = Options {
bullet: 'x',
..Options::default()
};
let error = State::new(&options)
.container_phrasing(&document, node, &Info::new("", ""))
.unwrap_err();
assert_eq!(error.rule_id, "unexpected-marker");
}
#[test]
fn literal_lookahead_matches_rendering_across_scopes_and_wrapping_modes() {
use crate::markdown::{construct_name::ConstructName, LineWrapping};
let mut document = empty();
let text = document
.append_child(document.root(), EventKind::Text(Text::Literal("")))
.unwrap();
assert_eq!(
State::new(&Options::default()).peek_node(&document, text),
None
);
let scopes = [
ConstructName::Phrasing,
ConstructName::Label,
ConstructName::DestinationRaw,
ConstructName::HeadingAtx,
ConstructName::CodeFencedLangGraveAccent,
];
for line_wrapping in [LineWrapping::Preserve, LineWrapping::Unwrap] {
let options = Options {
line_wrapping,
..Options::default()
};
for scope in scopes {
for first in (0..=127).map(char::from).chain(['é', 'α', '\u{a0}', '世']) {
for after in ["", "ordinary", "*", "\\!", "\r\n# two", "\n12. two"] {
document
.set_kind(
text,
EventKind::Text(Text::from(alloc::format!("{first}{after}"))),
)
.unwrap();
let mut state = State::new(&options);
state.enter(scope);
if let Some(prefix) = state.peek_node(&document, text) {
state
.write_node(&document, text, &Info::new("", ""))
.unwrap();
assert_eq!(state.output.chars().next(), Some(prefix));
}
}
}
}
}
}
#[test]
fn joins_code_and_spread_items_consistently() {
let options = Options {
fences: false,
..Options::default()
};
let state = State::new(&options);
let mut document = empty();
let root = document.root();
let code = document
.append_child(
root,
EventKind::CodeBlock {
value: Text::Literal("indented\n"),
language: None,
fence: None,
math: false,
unclosed: false,
},
)
.unwrap();
assert!(matches!(
state.default_separator(&document, code, code, root),
"\n\n<!---->\n\n"
));
let item = document
.append_child(root, EventKind::Start(Tag::Item { block_end: false }))
.unwrap();
let left = paragraph(&mut document, item, "one");
let right = document
.append_child(item, EventKind::Text(Text::Literal("other")))
.unwrap();
assert!(matches!(
state.default_separator(&document, left, right, item),
"\n"
));
document
.set_syntax(item, Some(NodeSyntax::ListItem { spread: true }))
.unwrap();
assert!(matches!(
state.default_separator(&document, left, right, item),
"\n\n"
));
let right = paragraph(&mut document, item, "two");
assert!(matches!(
state.default_separator(&document, left, right, item),
"\n\n"
));
}
#[test]
fn inline_literals_normalize_block_openers_and_preserve_other_line_endings() {
let options = Options::default();
let mut state = State::new(&options);
state.inline_code("one\r\n# two", None).unwrap();
assert_eq!(core::mem::take(&mut state.output), "`one # two`");
state.inline_math("one\r\n> two").unwrap();
assert_eq!(core::mem::take(&mut state.output), "$one > two$");
state.inline_code("one\r\ntwo", None).unwrap();
assert_eq!(core::mem::take(&mut state.output), "`one\r\ntwo`");
state.inline_code("é ` α", None).unwrap();
assert_eq!(core::mem::take(&mut state.output), "``é ` α``");
state.inline_math(" $α$ ").unwrap();
assert_eq!(core::mem::take(&mut state.output), "$$ $α$ $$");
state.inline_code("one\n12. two", None).unwrap();
assert_eq!(core::mem::take(&mut state.output), "`one 12. two`");
}
#[test]
fn detects_matching_and_nonmatching_code_fences() {
assert!(has_matching_ticks(b"`code`", 1, 1));
assert!(!has_matching_ticks(b"plain", 0, 1));
assert!(!has_matching_ticks(b"``", 0, 1));
}
#[test]
fn escapes_pipes_outside_unmatched_code_spans_and_after_even_backslashes() {
assert_eq!(escape_table_pipes(r"``code`| value"), r"``code`\| value");
assert_eq!(escape_table_pipes(r"two \\|"), r"two \\\|");
assert_eq!(escape_table_pipes(r"one \|"), r"one \|");
assert_eq!(escape_table_pipes("α|🙂||ω"), "α\\|🙂\\|\\|ω");
assert_eq!(
escape_table_pipes(r"```α|🙂```|`x|y`|``z|w``"),
r"```α|🙂```\|`x|y`\|``z|w``"
);
assert_eq!(escape_table_pipes(r"`α|🙂``|ω"), r"`α\|🙂``\|ω");
}
#[test]
fn table_pipe_runs_preserve_the_scalar_writer() {
fn scalar(value: &str, result: &mut String) {
let bytes = value.as_bytes();
let start_output = result.len();
let mut index = 0;
let mut code_ticks = None;
while index < bytes.len() {
if bytes[index] == b'`' {
let start = index;
index = super::super::util::scan::marker_run_end(bytes, index + 1, b'`');
let ticks = index - start;
if code_ticks == Some(ticks) {
code_ticks = None;
} else if code_ticks.is_none() && has_matching_ticks(bytes, index, ticks) {
code_ticks = Some(ticks);
}
result.push_str(&value[start..index]);
} else if bytes[index] == b'|' && code_ticks.is_none() {
let slashes = result[start_output..]
.chars()
.rev()
.take_while(|character| *character == '\\')
.count();
if slashes % 2 == 0 {
result.push('\\');
}
result.push('|');
index += 1;
} else {
let character = value[index..].chars().next().unwrap();
result.push(character);
index += character.len_utf8();
}
}
}
const TOKENS: [&str; 6] = ["a", "é", "🙂", "`", "|", "\\"];
for length in 0..=6 {
for mut variant in 0..TOKENS.len().pow(length) {
let mut value = String::new();
for _ in 0..length {
value.push_str(TOKENS[variant % TOKENS.len()]);
variant /= TOKENS.len();
}
for prefix in ["", "previous|\\"] {
let mut expected = String::from(prefix);
let mut actual = String::from(prefix);
scalar(&value, &mut expected);
write_table_pipes(&value, &mut actual);
assert_eq!(actual, expected, "{value:?}, prefix {prefix:?}");
}
}
}
}
#[test]
fn serializes_table_cells_with_and_without_pipe_rewrites() {
let mut document = empty();
let row = document
.append_child(document.root(), EventKind::Start(Tag::TableRow))
.unwrap();
for kind in [
EventKind::Text(Text::Literal("plain α")),
EventKind::Code {
value: Text::Literal("a`b"),
language: None,
},
EventKind::Text(Text::Literal("a|b")),
EventKind::Code {
value: Text::Literal("a|b"),
language: None,
},
EventKind::Text(Text::Literal("")),
] {
let cell = document
.append_child(
row,
EventKind::Start(Tag::TableCell {
alignment: crate::AlignKind::None,
header: false,
}),
)
.unwrap();
document.append_child(cell, kind).unwrap();
}
let options = Options::default();
let mut state = State::new(&options);
state.render_table_row(&document, row).unwrap();
assert_eq!(state.output, "| plain α | ``a`b`` | a\\|b | `a|b` | |");
}