use super::*;
type Frozen = (&'static str, &'static str, &'static [(u64, u64)]);
const CORPUS: &[Frozen] = &[
(
"ideographic-indent",
"\u{3000}\u{3000}全角空格缩进的段落,中文写作常用。\n\n第二段也缩进。\n",
&[(0, 57), (59, 80)],
),
(
"half-width-indent",
" four spaces open this line\n\nplain.\n",
&[(0, 30), (32, 38)],
),
(
"consecutive-blank-lines",
"一\n\n\n\n二\n\n\n三\n",
&[(0, 3), (7, 10), (13, 16)],
),
(
"fence-holding-a-blank-line",
"前言\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\n后记\n",
&[(0, 6), (8, 44), (46, 52)],
),
("tilde-fence", "~~~\n\n~~~\n\n之后。\n", &[(0, 8), (10, 19)]),
(
"nested-fence-markers",
"````md\n```ts\n\nconst a = 1;\n```\n````\n",
&[(0, 35)],
),
(
"hard-line-break",
"行尾双空格 \n接续行。\n\n第二段。\n",
&[(0, 30), (32, 44)],
),
(
"crlf",
"第一段。\r\n\r\n第二段。\r\n\r\n\r\n第三段。\r\n",
&[(0, 12), (16, 28), (34, 46)],
),
("no-trailing-newline", "只有一段,末尾无换行", &[(0, 30)]),
(
"byte-order-mark",
"\u{feff}第一段。\n\n第二段。\n",
&[(0, 15), (17, 29)],
),
("leading-blank-lines", "\n\n\n开头前有空行。\n", &[(3, 24)]),
("trailing-blank-lines", "结尾后有空行。\n\n\n\n", &[(0, 21)]),
(
"astral-characters",
"𝄞 音乐符号,非 BMP。\n\n😀 表情符号。\n",
&[(0, 30), (32, 52)],
),
(
"mixed-scripts",
"漢字とかなの段落。\n\n\u{3000}\u{3000}全角インデント。\n\nLatin paragraph.\n",
&[(0, 27), (29, 59), (61, 77)],
),
(
"blockquote-and-list",
"> 引用。\n> 续行。\n\n- 一\n- 二\n\n1. 甲\n2. 乙\n",
&[(0, 23), (25, 36), (38, 51)],
),
(
"table",
"| a | b |\n|---|---|\n| 1 | 2 |\n\n后文。\n",
&[(0, 29), (31, 40)],
),
(
"tabs",
"\t制表符开头。\n\n\t\t两个制表符。\n",
&[(0, 19), (21, 41)],
),
("empty-file", "", &[]),
("only-whitespace", "\n\n \n\n", &[]),
(
"everything-at-once",
"# 标题\n\n\u{3000}\u{3000}全角空格缩进的段落。\n\n\n\n行尾双空格 \n接续行。\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\n 四空格缩进的码块\n\n> 引用。\n",
&[
(0, 8),
(10, 46),
(50, 80),
(82, 118),
(120, 148),
(150, 161),
],
),
];
fn reproduce(source: &[u8], spans: &[Span]) -> Vec<u8> {
let mut out = Vec::with_capacity(source.len());
let mut cursor = 0_usize;
for span in spans {
let (start, end) = span.within(source.len()).unwrap();
out.extend_from_slice(&source[cursor..start]);
out.extend_from_slice(&source[start..end]);
cursor = end;
}
out.extend_from_slice(&source[cursor..]);
out
}
#[test]
fn every_frozen_corpus_keeps_its_block_spans_and_its_bytes() {
for (name, text, frozen) in CORPUS {
let source = text.as_bytes();
let spans = blocks(Format::Markdown, source);
let found: Vec<(u64, u64)> = spans
.iter()
.map(|span| (span.start(), span.end()))
.collect();
assert_eq!(found, frozen.to_vec(), "{name}");
assert_eq!(reproduce(source, &spans), source, "{name}");
}
}
#[test]
fn plain_lines_reproduce_mixed_endings_trailing_spaces_and_foreign_bytes() {
let source = b"one \r\ntwo\n\r\ncaf\xe9\t \nlast";
let spans = blocks(Format::Plain, source);
let found: Vec<(u64, u64)> = spans
.iter()
.map(|span| (span.start(), span.end()))
.collect();
assert_eq!(found, vec![(0, 5), (7, 10), (11, 11), (13, 19), (20, 24)]);
assert_eq!(reproduce(source, &spans), source);
assert_eq!(reproduce(source, &blocks(Format::Markdown, source)), source);
}
#[test]
fn an_empty_plain_document_is_one_empty_line() {
assert_eq!(blocks(Format::Plain, b""), vec![Span::at(0)]);
assert_eq!(blocks(Format::Markdown, b""), Vec::<Span>::new());
}
#[test]
fn closed_ends_after_the_last_line_that_closes_a_block() {
let rows: &[(&str, &str)] = &[
("", ""),
("still saying", ""),
("a line break joins\n", ""),
("a paragraph\n\n", "a paragraph\n\n"),
("one\n\ntwo, still", "one\n\n"),
("crlf\r\n\r\nnext", "crlf\r\n\r\n"),
("# Title\n", "# Title\n"),
("# Title", ""),
("#\n", "#\n"),
("#hashtag\n", ""),
("####### seven\n", ""),
(" # indented\n", ""),
("text\n# Heading\nmore", "text\n# Heading\n"),
("```\ncode\n\nmore\n", ""),
("```\ncode\n```", ""),
(
"```rust\nlet a = 1;\n```\nafter",
"```rust\nlet a = 1;\n```\n",
),
("~~~\ncode\n~~~\n", "~~~\ncode\n~~~\n"),
("````\n```\n\n", ""),
("- item\n ```\n code\n ```\n", ""),
(
"- item\n ```\n code\n ```\n\nafter",
"- item\n ```\n code\n ```\n\n",
),
("```\n# not a heading\n", ""),
];
let found: Vec<(&str, u64)> = rows
.iter()
.map(|(text, _)| (*text, closed(text.as_bytes())))
.collect();
let wanted: Vec<(&str, u64)> = rows
.iter()
.map(|(text, prefix)| {
assert!(text.starts_with(prefix), "{text:?}");
(*text, u64::try_from(prefix.len()).unwrap())
})
.collect();
assert_eq!(found, wanted);
}
#[test]
fn closed_never_moves_back_as_a_text_grows() {
let (_, text, _) = CORPUS
.iter()
.find(|(name, _, _)| *name == "everything-at-once")
.unwrap();
let bytes = text.as_bytes();
let mut last = 0;
for end in 0..=bytes.len() {
let at = closed(&bytes[..end]);
assert!(at >= last && at <= u64::try_from(end).unwrap(), "{end}");
last = at;
}
assert!(last > 0, "the corpus closes blocks");
}