use std::collections::HashMap;
use std::ops::Range;
use crate::diagram::DiagramSource;
use crate::markdown::ast::{Block, Inline};
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 } = block {
if let Some((alt, url)) = extract_lone_image(inlines) {
*block = Block::ImageBlock { alt, url };
}
}
}
}
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 Block::CodeBlock { content, .. } = std::mem::replace(block, Block::HorizontalRule)
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,
};
}
sources
}
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(super) 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() {
if let Block::Html(body) = block {
if is_html_comment_only(body) {
let body = std::mem::take(body);
*block = Block::HtmlComment(body);
}
}
}
}
pub fn attach_trailing_tui_columns_comments(blocks: &mut Vec<Block>) {
let mut i = 0;
while i + 1 < blocks.len() {
let is_pair = matches!(
(&blocks[i], &blocks[i + 1]),
(Block::Table { user_widths: None, .. }, Block::HtmlComment(body))
if crate::markdown::table_layout::parse_column_widths_comment(body).is_some()
);
if is_pair {
let body = match &blocks[i + 1] {
Block::HtmlComment(s) => s.clone(),
_ => unreachable!(),
};
let widths = crate::markdown::table_layout::parse_column_widths_comment(&body).unwrap();
if let Block::Table { user_widths, .. } = &mut blocks[i] {
*user_widths = Some(widths);
}
blocks.remove(i + 1);
continue;
}
i += 1;
}
}
pub fn annotate_list_blanks(blocks: &mut [Block], ranges: &[Range<usize>], source: &str) {
for (block, range) in blocks.iter_mut().zip(ranges.iter()) {
let Block::List { items, .. } = block else {
continue;
};
let list_src = &source[range.clone()];
let item_offsets = top_level_item_offsets(list_src);
if item_offsets.len() != items.len() {
continue;
}
for k in 1..item_offsets.len() {
let prev_line_end = line_end_in_str(list_src, item_offsets[k - 1]);
let between_start = (prev_line_end + 1).min(item_offsets[k]);
if let Some(gap_start) =
separator_blank_run_start(list_src, between_start, item_offsets[k])
{
items[k].blank_lines_before = list_src.as_bytes()[gap_start..item_offsets[k]]
.iter()
.filter(|&&b| b == b'\n')
.count();
}
}
}
}
fn top_level_item_offsets(list_src: &str) -> Vec<usize> {
let bytes = list_src.as_bytes();
let mut offsets = Vec::new();
let mut first_indent: Option<String> = None;
let mut pos = 0;
while pos < bytes.len() {
let line_end = line_end_in_str(list_src, pos);
let line = &list_src[pos..line_end];
if let Some((indent, _, _)) = parse_marker_line(line) {
if first_indent.is_none() {
first_indent = Some(indent.clone());
}
if first_indent.as_deref() == Some(indent.as_str()) {
offsets.push(pos);
}
}
pos = if line_end < bytes.len() {
line_end + 1
} else {
line_end
};
}
offsets
}
fn line_end_in_str(s: &str, start: usize) -> usize {
let bytes = s.as_bytes();
let mut p = start;
while p < bytes.len() && bytes[p] != b'\n' {
p += 1;
}
p
}
fn separator_blank_run_start(s: &str, start: usize, end: usize) -> Option<usize> {
let bytes = s.as_bytes();
let mut pos = start;
let mut fence: Option<(char, usize)> = None;
let mut run_start: Option<usize> = None;
while pos < end {
let mut le = pos;
while le < end && bytes[le] != b'\n' {
le += 1;
}
let line = &s[pos..le];
if let Some((fence_char, min_count)) = fence {
if is_closing_fence(line, fence_char, min_count) {
fence = None;
}
run_start = None;
} else if let Some((c, count)) = parse_opening_fence(line) {
fence = Some((c, count));
run_start = None;
} else if line.chars().all(char::is_whitespace) {
run_start.get_or_insert(pos);
} else {
run_start = None;
}
pos = if le < end { le + 1 } else { le };
}
run_start
}
pub fn parse_opening_fence(line: &str) -> Option<(char, usize)> {
let trimmed = line.trim_start();
let first = trimmed.chars().next()?;
if first != '`' && first != '~' {
return None;
}
let count = trimmed.chars().take_while(|&c| c == first).count();
if count < 3 {
return None;
}
if first == '`' && trimmed[count..].contains('`') {
return None;
}
Some((first, count))
}
pub fn is_closing_fence(line: &str, fence_char: char, min_count: usize) -> bool {
let trimmed = line.trim_start();
let count = trimmed.chars().take_while(|&c| c == fence_char).count();
if count < min_count {
return false;
}
trimmed[count..].chars().all(char::is_whitespace)
}
fn parse_marker_line(line: &str) -> Option<(String, char, Option<u64>)> {
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t') {
i += 1;
}
let indent = line[..i].to_owned();
let rest = &line[i..];
let rb = rest.as_bytes();
if let Some(&c) = rb.first() {
if matches!(c, b'-' | b'*' | b'+') && rb.get(1) == Some(&b' ') {
return Some((indent, c as char, None));
}
}
let digits_len = rb.iter().take_while(|b| b.is_ascii_digit()).count();
if digits_len > 0 {
let num: u64 = rest[..digits_len].parse().ok()?;
let delim = *rb.get(digits_len)?;
if matches!(delim, b'.' | b')') && rb.get(digits_len + 1) == Some(&b' ') {
return Some((indent, delim as char, Some(num)));
}
}
None
}