use super::common::{parse_blocks_from, MdBlock};
#[derive(Default)]
pub(crate) struct BlockStream {
pending: String,
next_index: usize,
}
impl BlockStream {
pub fn new() -> BlockStream {
BlockStream::default()
}
pub fn push(&mut self, text: &str) -> Vec<MdBlock> {
self.pending.push_str(text);
let cut = resume_point(&self.pending);
if cut == 0 {
return Vec::new();
}
let rest = self.pending.split_off(cut);
let ready = std::mem::replace(&mut self.pending, rest);
self.emit(&ready)
}
pub fn finish(&mut self) -> Vec<MdBlock> {
let ready = std::mem::take(&mut self.pending);
self.emit(&ready)
}
fn emit(&mut self, text: &str) -> Vec<MdBlock> {
if text.trim().is_empty() {
return Vec::new();
}
let blocks = parse_blocks_from(text, self.next_index);
self.next_index += blocks.len();
blocks
}
}
pub(crate) fn resume_point(text: &str) -> usize {
let mut in_fence = false;
let (mut cut, mut offset) = (0usize, 0usize);
for line in text.split_inclusive('\n') {
let compact = line.trim();
if compact.starts_with("```") {
in_fence = !in_fence;
} else if compact.is_empty() && !in_fence && line.ends_with('\n') {
cut = offset + line.len();
}
offset += line.len();
}
cut
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::md::common::parse_markdown_blocks;
fn whole(text: &str) -> Vec<(String, usize)> {
parse_markdown_blocks(text).into_iter().map(|b| (b.content, b.index)).collect()
}
fn in_pieces(text: &str, at: &[usize]) -> Vec<(String, usize)> {
let mut stream = BlockStream::new();
let mut out = Vec::new();
let mut last = 0;
for cut in at.iter().copied().chain(std::iter::once(text.len())) {
let cut = cut.min(text.len());
if !text.is_char_boundary(cut) || cut < last {
continue;
}
out.extend(stream.push(&text[last..cut]));
last = cut;
}
out.extend(stream.push(&text[last..]));
out.extend(stream.finish());
out.into_iter().map(|b| (b.content, b.index)).collect()
}
#[test]
fn feeding_a_document_in_pieces_matches_parsing_it_whole() {
let documents = [
"# Title\n\nA paragraph that runs on.\n\nAnother one.\n",
"Setext\n======\n\nbody text here\n",
"- one\n- two\n\n- three\n\ntrailing prose\n",
"| a | b |\n|---|---|\n| 1 | 2 |\n\nafter the table\n",
"intro\n\n```rust\nfn main() {\n\n // ---\n}\n```\n\nafter\n",
"just one paragraph with no newline at the end",
"# A\n## B\n### C\n\ntext\n\n---\n\nmore\n",
];
for document in documents {
let expected = whole(document);
for cut in 0..=document.len() {
assert_eq!(in_pieces(document, &[cut]), expected, "split at {cut} of {document:?}");
}
let every: Vec<usize> = (0..document.len()).collect();
assert_eq!(in_pieces(document, &every), expected, "byte-at-a-time {document:?}");
}
}
#[test]
fn a_fence_is_never_cut_even_though_it_contains_blank_lines() {
let text = "```\n\n\n```\n\nafter\n";
assert_eq!(resume_point(text), text.find("after").unwrap());
}
#[test]
fn an_incomplete_final_line_is_never_treated_as_a_boundary() {
assert_eq!(resume_point("a\n\nb"), 3);
assert_eq!(resume_point("a\nb"), 0);
}
}