use super::{heading_level, list_marker, parse, Block, MdStyles};
pub struct StreamSession {
styles: MdStyles,
closed: Vec<Block>,
closed_revision: u64,
tail: String,
open_cache: Vec<Block>,
finished: bool,
bytes_reparsed: u64,
}
impl StreamSession {
pub fn new(styles: MdStyles) -> StreamSession {
StreamSession {
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, "StreamSession::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<Block> {
if !self.finished {
if !self.tail.is_empty() {
self.bytes_reparsed += self.tail.len() as u64;
let mut rest = parse(&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) -> &[Block] {
&self.closed
}
pub fn closed_revision(&self) -> u64 {
self.closed_revision
}
pub fn open_blocks(&self) -> &[Block] {
&self.open_cache
}
pub fn blocks(&self) -> Vec<Block> {
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 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 splits_paragraph = para_open && line_class(line) == LineClass::ParaText;
if !in_fence && !splits_paragraph {
best_cut = line_start;
}
if in_fence {
if line.trim_end().trim_start().starts_with("```") {
in_fence = false; }
para_open = false;
} else {
match line_class(line) {
LineClass::FenceOpen => {
in_fence = true;
para_open = false;
}
LineClass::ParaText => para_open = true,
LineClass::Boundary => para_open = false,
}
}
}
let fragment = &self.tail[pos..];
if !in_fence {
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(&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(&self.tail, &self.styles);
}
}
}
impl std::fmt::Debug for StreamSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StreamSession")
.field("closed", &self.closed.len())
.field("open_len", &self.tail.len())
.field("finished", &self.finished)
.finish()
}
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum LineClass {
FenceOpen,
ParaText,
Boundary,
}
fn line_class(raw: &str) -> LineClass {
let line = raw.trim_end();
let trimmed = line.trim_start();
if trimmed.starts_with("```") {
return LineClass::FenceOpen;
}
if trimmed.is_empty()
|| trimmed == "---"
|| trimmed == "***"
|| heading_level(trimmed).is_some()
|| trimmed.starts_with('>')
|| list_marker(trimmed).is_some()
{
return LineClass::Boundary;
}
LineClass::ParaText
}
fn fragment_committed_non_para(fragment: &str) -> bool {
let trimmed = fragment.trim_start();
trimmed.starts_with("```")
|| trimmed.starts_with('>')
|| heading_level(trimmed).is_some()
|| list_marker(trimmed).is_some()
}
#[cfg(test)]
#[path = "md_stream_tests.rs"]
mod tests;