use std::borrow::Cow;
use std::collections::HashMap;
use std::ops::Range;
use crate::diagram::DiagramSource;
use crate::markdown::ast::{to_u32, Block, Inline, SrcLines};
pub fn promote_image_paragraphs(
blocks: &mut [Block],
_real_ranges: Option<&mut Vec<Range<usize>>>,
) {
for block in blocks.iter_mut() {
if let Block::Paragraph { inlines, src } = block {
if let Some((alt, url)) = extract_lone_image(inlines) {
let src = std::mem::take(src);
*block = Block::ImageBlock { alt, url, src };
}
}
}
}
pub fn promote_diagram_code_blocks(blocks: &mut [Block]) -> HashMap<String, DiagramSource> {
let mut sources = HashMap::new();
for block in blocks.iter_mut() {
let is_mermaid = matches!(
block,
Block::CodeBlock { language: Some(lang), .. } if lang.eq_ignore_ascii_case("mermaid")
);
if !is_mermaid {
continue;
}
let placeholder = Block::HorizontalRule {
src: SrcLines::default(),
};
let Block::CodeBlock { content, src, .. } = std::mem::replace(block, placeholder) else {
continue;
};
let source = DiagramSource::Mermaid(content);
let url = crate::diagram::synthetic_url(&source);
sources.insert(url.clone(), source);
*block = Block::ImageBlock {
alt: "mermaid diagram".to_string(),
url,
src,
};
}
sources
}
pub fn reconstruct_broken_display_math(
blocks: &mut [Block],
real_ranges: &[Range<usize>],
source: &str,
) {
for (block, range) in blocks.iter_mut().zip(real_ranges) {
let Block::Paragraph { inlines, src } = block else {
continue;
};
if collect_display_math_only(inlines).is_some() {
continue;
}
let Some(raw) = source.get(range.clone()) else {
continue;
};
let trimmed = raw.trim_matches(|c: char| c.is_ascii_whitespace());
let Some(inner) = trimmed
.strip_prefix("$$")
.and_then(|body| body.strip_suffix("$$"))
else {
continue;
};
if inner.is_empty() || inner.contains("$$") {
continue;
}
*inlines = vec![Inline::Math {
source: inner.to_string(),
display: true,
}];
let lines = range_lines(source, range);
let first = src.first as usize;
*src = with_formula_cols(src, formula_block_body(inner).as_deref(), |k| {
lines.get(first + k).copied().unwrap_or("")
});
}
}
pub fn promote_display_math_paragraphs(
blocks: &mut Vec<Block>,
real_ranges: &mut Vec<Range<usize>>,
source: &str,
) -> HashMap<String, DiagramSource> {
let mut sources = HashMap::new();
let mut out: Vec<Block> = Vec::with_capacity(blocks.len());
let mut out_ranges: Vec<Range<usize>> = Vec::with_capacity(real_ranges.len());
for (block, range) in blocks.drain(..).zip(real_ranges.drain(..)) {
let Block::Paragraph { inlines, src } = &block else {
out.push(block);
out_ranges.push(range);
continue;
};
let Some(math_sources) = collect_display_math_only(inlines) else {
out.push(block);
out_ranges.push(range);
continue;
};
let Some(piece_ranges) = split_math_ranges(source, &range, math_sources.len()) else {
out.push(block);
out_ranges.push(range);
continue;
};
for (i, &formula) in math_sources.iter().enumerate() {
let diagram_source = DiagramSource::Latex(formula.to_owned());
let url = crate::diagram::synthetic_url(&diagram_source);
sources.insert(url.clone(), diagram_source);
out.push(Block::ImageBlock {
alt: "math".to_string(),
url,
src: piece_src_lines(src, source, &range, &piece_ranges[i], formula),
});
out_ranges.push(piece_ranges[i].clone());
}
}
*blocks = out;
*real_ranges = out_ranges;
sources
}
pub fn split_display_math_paragraphs(
blocks: &mut Vec<Block>,
real_ranges: &mut Vec<Range<usize>>,
source: &str,
) {
let mut out: Vec<Block> = Vec::with_capacity(blocks.len());
let mut out_ranges: Vec<Range<usize>> = Vec::with_capacity(real_ranges.len());
for (block, range) in blocks.drain(..).zip(real_ranges.drain(..)) {
let split = match &block {
Block::Paragraph { inlines, .. } => collect_display_math_only(inlines)
.filter(|formulas| formulas.len() >= 2)
.and_then(|formulas| {
split_math_ranges(source, &range, formulas.len())
.map(|ranges| (formulas, ranges))
}),
_ => None,
};
match split {
Some((formulas, piece_ranges)) => {
let para_src = block.src().cloned().unwrap_or_default();
for (i, formula) in formulas.into_iter().enumerate() {
let src = piece_src_lines(¶_src, source, &range, &piece_ranges[i], formula);
out.push(Block::Paragraph {
inlines: vec![Inline::Math {
source: formula.to_owned(),
display: true,
}],
src,
});
out_ranges.push(piece_ranges[i].clone());
}
}
None => {
out.push(block);
out_ranges.push(range);
}
}
}
*blocks = out;
*real_ranges = out_ranges;
}
fn piece_src_lines(
para: &SrcLines,
source: &str,
para_range: &Range<usize>,
piece: &Range<usize>,
formula: &str,
) -> SrcLines {
let lines_between = |from: usize, to: usize| {
source
.get(from..to)
.map_or(0, |s| s.bytes().filter(|&b| b == b'\n').count())
};
let offset = lines_between(para_range.start, piece.start);
let body = source.get(piece.clone()).unwrap_or("");
let len = body
.trim_end_matches('\n')
.bytes()
.filter(|&b| b == b'\n')
.count()
+ 1;
let skip = offset.saturating_sub(para.first as usize);
let cols: Vec<Option<u32>> = para
.cols()
.chain(std::iter::repeat(None))
.skip(skip)
.take(len)
.collect();
let lines = range_lines(source, piece);
with_formula_cols(
&SrcLines::new(0, &cols),
formula_block_body(formula).as_deref(),
|k| lines.get(k).copied().unwrap_or(""),
)
}
fn collect_display_math_only(inlines: &[Inline]) -> Option<Vec<&str>> {
let mut formulas: Vec<&str> = Vec::new();
for inline in inlines {
match inline {
Inline::Math {
source,
display: true,
} => formulas.push(source),
Inline::Math { .. } => return None, Inline::Text(t) if t.trim().is_empty() => {}
Inline::SoftBreak | Inline::HardBreak => {}
_ => return None,
}
}
(!formulas.is_empty()).then_some(formulas)
}
pub fn display_math_block_body(block: &Block) -> Option<Cow<'_, str>> {
let Block::Paragraph { inlines, .. } = block else {
return None;
};
math_block_body(inlines)
}
pub(crate) fn math_block_body(inlines: &[Inline]) -> Option<Cow<'_, str>> {
let mut formula: Option<&str> = None;
for inline in inlines {
match inline {
Inline::Math {
source,
display: true,
} if formula.is_none() => formula = Some(source),
Inline::Math { .. } => return None, Inline::Text(t) if t.trim().is_empty() => {}
Inline::SoftBreak | Inline::HardBreak => {}
_ => return None,
}
}
formula_block_body(formula?)
}
fn formula_block_body(formula: &str) -> Option<Cow<'_, str>> {
let inner = formula
.strip_prefix('\n')
.or_else(|| formula.strip_prefix("\r\n"))?;
let inner = inner
.strip_suffix("\r\n")
.or_else(|| inner.strip_suffix('\n'))?;
Some(if inner.contains('\r') {
Cow::Owned(inner.replace("\r\n", "\n"))
} else {
Cow::Borrowed(inner)
})
}
fn range_lines<'s>(source: &'s str, range: &Range<usize>) -> Vec<&'s str> {
let line_start = source
.get(..range.start)
.and_then(|before| before.rfind('\n'))
.map_or(0, |i| i + 1);
source
.get(line_start..range.end)
.unwrap_or("")
.split('\n')
.collect()
}
pub(crate) fn with_formula_cols<'s>(
src: &SrcLines,
body: Option<&str>,
line_text: impl Fn(usize) -> &'s str,
) -> SrcLines {
let mut body_lines = body.map(|body| body.split('\n'));
let cols: Vec<Option<u32>> = src
.cols()
.enumerate()
.map(|(k, col)| {
if k == 0 {
return col;
}
let text = body_lines.as_mut()?.next()?;
let raw = line_text(k).trim_end_matches('\r');
raw.ends_with(text)
.then(|| to_u32(raw.chars().count() - text.chars().count()))
})
.collect();
SrcLines::new(src.first, &cols)
}
fn split_math_ranges(source: &str, para: &Range<usize>, count: usize) -> Option<Vec<Range<usize>>> {
let body = source.get(para.clone())?;
let mut ranges = Vec::with_capacity(count);
let mut search_from = 0usize;
for i in 0..count {
let open = body[search_from..].find("$$")? + search_from;
let close_rel = body[open + 2..].find("$$")?;
let close = open + 2 + close_rel;
let end = if i + 1 == count {
body.len()
} else {
close + 2
};
ranges.push(para.start + open..para.start + end);
search_from = close + 2;
}
Some(ranges)
}
fn extract_lone_image(inlines: &[Inline]) -> Option<(String, String)> {
let mut image: Option<(String, String)> = None;
for inline in inlines {
match inline {
Inline::Image { alt, url } => {
if image.is_some() {
return None;
}
image = Some((alt.clone(), url.clone()));
}
Inline::Text(t) if t.trim().is_empty() => {}
Inline::SoftBreak | Inline::HardBreak => {}
_ => return None,
}
}
image
}
pub(crate) fn is_html_comment_only(body: &str) -> bool {
let mut rest = body.trim();
if rest.is_empty() {
return false;
}
while !rest.is_empty() {
if !rest.starts_with("<!--") {
return false;
}
let Some(close) = rest[4..].find("-->") else {
return false;
};
rest = rest[4 + close + 3..].trim_start();
}
true
}
pub fn promote_html_comments(blocks: &mut [Block]) {
for block in blocks.iter_mut() {
match block {
Block::Html(body, src) if is_html_comment_only(body) => {
let body = std::mem::take(body);
let src = std::mem::take(src);
*block = Block::HtmlComment(body, src);
}
_ => for_each_child_list(block, |children| promote_html_comments(children)),
}
}
}
pub fn attach_trailing_tui_columns_comments(
blocks: &mut Vec<Block>,
mut ranges: Option<&mut Vec<Range<usize>>>,
) {
let old_blocks = std::mem::take(blocks);
let old_ranges = ranges.as_deref_mut().map(std::mem::take);
debug_assert!(old_ranges
.as_ref()
.is_none_or(|r| r.len() == old_blocks.len()));
let mut old_ranges = old_ranges.map(Vec::into_iter);
for block in old_blocks {
let range = old_ranges.as_mut().and_then(Iterator::next);
if let (
Some(Block::Table {
user_widths: slot @ None,
..
}),
Block::HtmlComment(body, _),
) = (blocks.last_mut(), &block)
{
if let Some(widths) = crate::markdown::table_layout::parse_column_widths_comment(body) {
*slot = Some(widths);
if let (Some(table), Some(comment)) =
(ranges.as_deref_mut().and_then(|r| r.last_mut()), range)
{
table.end = comment.end;
}
continue;
}
}
blocks.push(block);
if let (Some(out), Some(range)) = (ranges.as_deref_mut(), range) {
out.push(range);
}
}
}
pub fn attach_nested_tui_columns_comments(blocks: &mut [Block]) {
for block in blocks.iter_mut() {
for_each_child_list(block, |children| {
for i in 1..children.len() {
let Block::HtmlComment(body, _) = &children[i] else {
continue;
};
let Some(widths) = crate::markdown::table_layout::parse_column_widths_comment(body)
else {
continue;
};
if let Block::Table { user_widths, .. } = &mut children[i - 1] {
user_widths.get_or_insert(widths);
}
}
attach_nested_tui_columns_comments(children);
});
}
}
fn for_each_child_list(block: &mut Block, mut f: impl FnMut(&mut Vec<Block>)) {
match block {
Block::BlockQuote { blocks, .. } | Block::FootnoteDefinition { blocks, .. } => f(blocks),
Block::List { items, .. } => items.iter_mut().for_each(|item| f(&mut item.blocks)),
_ => {}
}
}