use tracing::debug;
use super::{FENCE_RE, is_null_lang};
use crate::wrap::{FenceObservation, FenceTracker, ObservedFence};
#[derive(Clone, Copy)]
pub(super) enum Strategy {
Compress,
Preserve,
}
struct CachedLine {
line: String,
compressed: Option<String>,
}
struct PendingFenceBlock {
opening_marker: String,
has_conflicting_interior_fence: bool,
lines: Vec<CachedLine>,
}
fn marker_char(marker: &str) -> Option<char> { marker.chars().next() }
pub(super) fn rewrite_marker(line: &str, strategy: Strategy) -> Option<String> {
let cap = FENCE_RE.captures(line)?;
let indent = cap.get(1).map_or("", |m| m.as_str());
let original_marker = cap.get(2).map_or("", |m| m.as_str());
let lang = cap.get(3).map_or("", |m| m.as_str());
let marker = match strategy {
Strategy::Compress => "```",
Strategy::Preserve => original_marker,
};
Some(if is_null_lang(lang) {
format!("{indent}{marker}")
} else {
format!("{indent}{marker}{lang}")
})
}
fn interior_fence_requires_preserved_delimiters(
opening_marker: &str,
parsed: Option<(&str, &str, &str)>,
) -> bool {
let Some((_indent, marker, _info)) = parsed else {
return false;
};
let Some(opening_ch) = marker_char(opening_marker) else {
return false;
};
let Some(marker_ch) = marker_char(marker) else {
return false;
};
marker_ch == opening_ch || marker_ch == '`'
}
fn opening_rewrite(has_conflicting_interior_fence: bool) -> Strategy {
if has_conflicting_interior_fence {
Strategy::Preserve
} else {
Strategy::Compress
}
}
fn rewrite_fence_line(cached: CachedLine, strategy: Strategy) -> String {
let CachedLine { line, compressed } = cached;
match strategy {
Strategy::Compress => compressed.unwrap_or(line),
Strategy::Preserve => rewrite_marker(&line, Strategy::Preserve).unwrap_or(line),
}
}
fn flush_unmatched_block(block: PendingFenceBlock, out: &mut Vec<String>) {
debug!(
block_lines = block.lines.len(),
has_conflicting_interior_fence = block.has_conflicting_interior_fence,
"preserving an unclosed fence block"
);
let rewrite = opening_rewrite(block.has_conflicting_interior_fence);
for (index, cached) in block.lines.into_iter().enumerate() {
let emitted = if index == 0 {
rewrite_fence_line(cached, rewrite)
} else {
cached.line
};
out.push(emitted);
}
}
fn flush_matched_block(block: PendingFenceBlock, out: &mut Vec<String>) {
let rewrite = opening_rewrite(block.has_conflicting_interior_fence);
let closing_index = block.lines.len() - 1;
for (index, cached) in block.lines.into_iter().enumerate() {
let emitted = if index == 0 || index == closing_index {
rewrite_fence_line(cached, rewrite)
} else {
cached.line
};
out.push(emitted);
}
}
fn flush_original_block(block: PendingFenceBlock, out: &mut Vec<String>) {
out.extend(block.lines.into_iter().map(|cached| cached.line));
}
fn flush_completed_block(block: PendingFenceBlock, out: &mut Vec<String>) {
let opening_rewritable = block.lines.first().is_some_and(|c| c.compressed.is_some());
let closing_rewritable = block.lines.last().is_some_and(|c| c.compressed.is_some());
if opening_rewritable && closing_rewritable {
flush_matched_block(block, out);
} else {
flush_original_block(block, out);
}
}
struct ParsedLine<'a> {
line: &'a str,
observation: FenceObservation,
fence: Option<(&'a str, &'a str, &'a str)>,
compressed: Option<String>,
}
impl<'a> ParsedLine<'a> {
fn observe(tracker: &mut FenceTracker, line: &'a str) -> Self {
let observed: ObservedFence<'a> = tracker.observe_source_fence(line);
Self {
line,
observation: observed.observation,
fence: observed.fence,
compressed: rewrite_marker(line, Strategy::Compress),
}
}
fn into_cached(self) -> CachedLine {
CachedLine {
line: self.line.to_owned(),
compressed: self.compressed,
}
}
}
fn start_fence_block(
previous: Option<PendingFenceBlock>,
parsed: ParsedLine<'_>,
out: &mut Vec<String>,
) -> Option<PendingFenceBlock> {
if let Some(block) = previous {
flush_original_block(block, out);
}
let Some((_indent, opening_marker, _info)) = parsed.fence else {
out.push(parsed.compressed.unwrap_or_else(|| parsed.line.to_owned()));
return None;
};
let opening_marker = opening_marker.to_owned();
Some(PendingFenceBlock {
opening_marker,
has_conflicting_interior_fence: false,
lines: vec![parsed.into_cached()],
})
}
fn advance_fence_block(
pending: Option<PendingFenceBlock>,
parsed: ParsedLine<'_>,
out: &mut Vec<String>,
) -> Option<PendingFenceBlock> {
let Some(mut block) = pending else {
out.push(parsed.line.to_owned());
return None;
};
let observation = parsed.observation;
if observation.is_fence_marker
&& observation.is_in_fence
&& interior_fence_requires_preserved_delimiters(&block.opening_marker, parsed.fence)
{
block.has_conflicting_interior_fence = true;
}
let keep_open = !observation.is_fence_marker || observation.is_in_fence;
block.lines.push(parsed.into_cached());
if keep_open {
return Some(block);
}
flush_completed_block(block, out);
None
}
#[must_use]
pub fn compress_fences(lines: &[String]) -> Vec<String> {
let mut tracker = FenceTracker::new();
let mut pending_block = None;
let mut out = Vec::with_capacity(lines.len());
for line in lines {
let parsed = ParsedLine::observe(&mut tracker, line);
pending_block = if parsed.observation.was_in_fence {
advance_fence_block(pending_block.take(), parsed, &mut out)
} else {
start_fence_block(pending_block.take(), parsed, &mut out)
};
}
if let Some(block) = pending_block {
flush_unmatched_block(block, &mut out);
}
out
}