use super::doc::{doc_line_class, parse_doc, table_opens, DocLineClass};
use super::stream::fragment_committed_non_para;
use super::{DocBlock, MdStyles};
pub struct DocStreamSession {
styles: MdStyles,
closed: Vec<DocBlock>,
closed_revision: u64,
tail: String,
open_cache: Vec<DocBlock>,
finished: bool,
bytes_reparsed: u64,
}
impl DocStreamSession {
pub fn new(styles: MdStyles) -> DocStreamSession {
DocStreamSession {
styles,
closed: Vec::new(),
closed_revision: 0,
tail: String::new(),
open_cache: Vec::new(),
finished: false,
bytes_reparsed: 0,
}
}
pub fn append(&mut self, delta: &str) {
debug_assert!(!self.finished, "DocStreamSession::append after finish()");
if self.finished || delta.is_empty() {
return;
}
self.tail.push_str(delta);
self.seal();
self.reparse_tail();
}
pub fn finish(&mut self) -> Vec<DocBlock> {
if !self.finished {
if !self.tail.is_empty() {
self.bytes_reparsed += self.tail.len() as u64;
let mut rest = parse_doc(&self.tail, &self.styles);
if !rest.is_empty() {
self.closed.append(&mut rest);
self.closed_revision += 1;
}
self.tail.clear();
}
self.open_cache.clear();
self.finished = true;
}
self.closed.clone()
}
pub fn closed_blocks(&self) -> &[DocBlock] {
&self.closed
}
pub fn closed_revision(&self) -> u64 {
self.closed_revision
}
pub fn open_blocks(&self) -> &[DocBlock] {
&self.open_cache
}
pub fn blocks(&self) -> Vec<DocBlock> {
let mut out = self.closed.clone();
out.extend(self.open_cache.iter().cloned());
out
}
pub fn is_finished(&self) -> bool {
self.finished
}
pub fn open_len(&self) -> usize {
self.tail.len()
}
pub fn bytes_reparsed_total(&self) -> u64 {
self.bytes_reparsed
}
fn seal(&mut self) {
let mut in_fence = false;
let mut para_open = false;
let mut table_open = false;
let mut candidate_pending = false;
let mut best_cut = 0usize;
let mut pos = 0usize;
while let Some(rel) = self.tail[pos..].find('\n') {
let line_start = pos;
let line = &self.tail[line_start..line_start + rel];
pos = line_start + rel + 1;
let next_complete: Option<&str> = self.tail[pos..]
.find('\n')
.map(|nrel| &self.tail[pos..pos + nrel]);
if in_fence {
if line.trim_end().trim_start().starts_with("```") {
in_fence = false;
}
para_open = false;
continue;
}
let cls = doc_line_class(line);
if table_open {
if cls == DocLineClass::PipeText {
continue; }
table_open = false; }
let opens_table = cls == DocLineClass::PipeText
&& next_complete.is_some_and(|n| table_opens(line.trim_end().trim_start(), n));
candidate_pending = cls == DocLineClass::PipeText && next_complete.is_none();
let joins_para = para_open
&& (cls == DocLineClass::ParaText
|| (cls == DocLineClass::PipeText && !opens_table));
if !joins_para {
best_cut = line_start;
}
match cls {
DocLineClass::FenceOpen => {
in_fence = true;
para_open = false;
}
DocLineClass::Image | DocLineClass::Task | DocLineClass::Boundary => {
para_open = false;
}
DocLineClass::PipeText => {
if opens_table {
table_open = true;
para_open = false;
} else {
para_open = true;
}
}
DocLineClass::ParaText => para_open = true,
}
}
let fragment = &self.tail[pos..];
if !in_fence && !table_open && !candidate_pending {
if fragment.is_empty() {
if !para_open {
best_cut = self.tail.len();
}
} else if !para_open || fragment_committed_non_para(fragment) {
best_cut = pos;
}
}
if best_cut > 0 {
self.bytes_reparsed += best_cut as u64;
let mut sealed = parse_doc(&self.tail[..best_cut], &self.styles);
if !sealed.is_empty() {
self.closed.append(&mut sealed);
self.closed_revision += 1;
}
self.tail.drain(..best_cut);
}
}
fn reparse_tail(&mut self) {
if self.tail.is_empty() {
self.open_cache.clear();
} else {
self.bytes_reparsed += self.tail.len() as u64;
self.open_cache = parse_doc(&self.tail, &self.styles);
}
}
}
impl std::fmt::Debug for DocStreamSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DocStreamSession")
.field("closed", &self.closed.len())
.field("open_len", &self.tail.len())
.field("finished", &self.finished)
.finish()
}
}
#[cfg(test)]
#[path = "md_doc_stream_tests.rs"]
mod tests;