项目文件夹

文件
wehub-resource-sync a789495a98
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:34 +08:00

1078 行
45 KiB
Rust

use super::render_support::highlight_code;
use super::*;
thread_local! {
/// Renders performed by THIS thread. `MarkdownDebugStats::total_renders`
/// is process-global, so tests that assert "no extra render happened
/// between two calls" race concurrent renders on other test threads;
/// they should diff this counter instead (see `thread_render_count`).
static THREAD_RENDER_COUNT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// Number of full markdown renders performed by the current thread. Unlike
/// `debug_stats().total_renders`, this is immune to concurrent renders on
/// other threads, making it suitable for cache-behavior assertions in
/// parallel test runs.
pub fn thread_render_count() -> u64 {
THREAD_RENDER_COUNT.with(|c| c.get())
}
pub fn render_markdown_with_width(text: &str, max_width: Option<usize>) -> Vec<Line<'static>> {
let render_start = Instant::now();
let text = jcode_render_core::normalize_latex_math(text);
let text = escape_currency_dollars(&text);
let text = preserve_line_oriented_softbreaks(&text);
let text = text.as_str();
let mut lines: Vec<Line<'static>> = Vec::new();
let mut current_spans: Vec<Span<'static>> = Vec::new();
let streaming_mode = streaming_render_context_enabled();
let deferred_mermaid_mode = deferred_mermaid_render_context_enabled();
let spacing_mode = effective_markdown_spacing_mode();
let configured_latex_mode = config_snapshot().latex_rendering;
// Image rendering invokes an external TeX toolchain synchronously. Doing
// that for every partial token batch can block the UI for hundreds of
// milliseconds and leave the last incomplete delimiter frame visible even
// after the response has completed. Stream with the deterministic Unicode
// renderer, then let the normal completed-message render upgrade to images.
let latex_mode = if streaming_mode && configured_latex_mode == LatexRenderingMode::Image {
LatexRenderingMode::Unicode
} else {
configured_latex_mode
};
// Style stack for nested formatting
let mut bold = false;
let mut italic = false;
// True while inside an emphasis run that opened with the reasoning sentinel.
// Smart-punctuation (e.g. apostrophes) splits a single reasoning line into
// multiple text events; only the first carries the sentinel, so we latch the
// dim/italic styling for the whole emphasis span.
let mut reasoning_emphasis = false;
let mut strike = false;
let mut in_code_block = false;
let mut code_block_lang: Option<String> = None;
let mut code_block_content = String::new();
let mut heading_level: Option<u8> = None;
let mut blockquote_depth = 0usize;
let mut list_stack: Vec<ListRenderState> = Vec::new();
let mut link_targets: Vec<String> = Vec::new();
let mut in_image = false;
let mut image_url: Option<String> = None;
let mut image_alt = String::new();
let mut in_definition_list = false;
let mut in_definition_item = false;
let mut in_footnote_definition = false;
let mut centered_blocks = CenteredStructuredBlockState::default();
// Table state
let mut in_table = false;
let mut table_row: Vec<String> = Vec::new();
let mut table_rows: Vec<Vec<String>> = Vec::new();
let mut current_cell = String::new();
let mut _is_header_row = false;
// Enable table parsing
let mut options = Options::empty();
options.insert(Options::ENABLE_TABLES);
options.insert(Options::ENABLE_MATH);
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TASKLISTS);
options.insert(Options::ENABLE_FOOTNOTES);
options.insert(Options::ENABLE_GFM);
options.insert(Options::ENABLE_DEFINITION_LIST);
options.insert(Options::ENABLE_SMART_PUNCTUATION);
let parser = Parser::new_ext(text, options);
// Debug counters
let mut dbg_headings = 0usize;
let mut dbg_code_blocks = 0usize;
let mut dbg_mermaid_blocks = 0usize;
let mut dbg_tables = 0usize;
let mut dbg_list_items = 0usize;
let mut dbg_blockquotes = 0usize;
for event in parser {
match event {
Event::Start(Tag::Heading { level, .. }) => {
dbg_headings += 1;
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
heading_level = Some(level as u8);
}
Event::End(TagEnd::Heading(_)) => {
if !current_spans.is_empty() {
// Choose color based on heading level
let color = match heading_level {
Some(1) => heading_h1_color(),
Some(2) => heading_h2_color(),
Some(3) => heading_h3_color(),
_ => heading_color(),
};
let heading_spans: Vec<Span<'static>> = current_spans
.drain(..)
.map(|s| {
Span::styled(s.content.to_string(), Style::default().fg(color).bold())
})
.collect();
lines.push(Line::from(heading_spans));
push_block_separator(&mut lines, MarkdownBlockKind::Heading, spacing_mode);
}
heading_level = None;
}
Event::Start(Tag::Strong) => bold = true,
Event::End(TagEnd::Strong) => bold = false,
Event::Start(Tag::Emphasis) => italic = true,
Event::End(TagEnd::Emphasis) => {
italic = false;
reasoning_emphasis = false;
}
Event::Start(Tag::Strikethrough) => strike = true,
Event::End(TagEnd::Strikethrough) => strike = false,
Event::Start(Tag::BlockQuote(_)) => {
dbg_blockquotes += 1;
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
enter_centered_structured_block(&mut centered_blocks, lines.len());
blockquote_depth += 1;
}
Event::End(TagEnd::BlockQuote(_)) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
blockquote_depth = blockquote_depth.saturating_sub(1);
exit_centered_structured_block(&mut centered_blocks, lines.len());
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(&mut lines, MarkdownBlockKind::BlockQuote, spacing_mode);
}
}
Event::Start(Tag::List(start)) => {
enter_centered_structured_block(&mut centered_blocks, lines.len());
let start_index = start.unwrap_or(1);
let state = ListRenderState {
ordered: start.is_some(),
next_index: start_index,
item_line_starts: Vec::new(),
max_marker_digits: start_index.to_string().len(),
};
list_stack.push(state);
}
Event::End(TagEnd::List(_)) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
if let Some(state) = list_stack.pop()
&& center_code_blocks()
&& state.ordered
{
align_ordered_list_markers(
&mut lines,
&state.item_line_starts,
state.max_marker_digits,
);
}
exit_centered_structured_block(&mut centered_blocks, lines.len());
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(&mut lines, MarkdownBlockKind::List, spacing_mode);
}
}
Event::Start(Tag::Link { dest_url, .. }) => {
link_targets.push(dest_url.to_string());
}
Event::End(TagEnd::Link) => {
if let Some(url) = link_targets.pop()
&& !url.is_empty()
{
current_spans.push(Span::styled(
format!(" ({})", url),
Style::default().fg(md_dim_color()),
));
}
}
Event::Start(Tag::Image { dest_url, .. }) => {
in_image = true;
image_url = Some(dest_url.to_string());
image_alt.clear();
}
Event::End(TagEnd::Image) => {
let alt = if image_alt.trim().is_empty() {
"image".to_string()
} else {
image_alt.trim().to_string()
};
let label = if let Some(url) = image_url.take() {
format!("[image: {}] ({})", alt, url)
} else {
format!("[image: {}]", alt)
};
if in_table {
current_cell.push_str(&label);
} else {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(label, Style::default().fg(md_dim_color())));
}
in_image = false;
image_alt.clear();
}
Event::Start(Tag::FootnoteDefinition(label)) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
enter_centered_structured_block(&mut centered_blocks, lines.len());
in_footnote_definition = true;
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(
format!("[^{}]: ", label),
Style::default().fg(md_dim_color()),
));
}
Event::End(TagEnd::FootnoteDefinition) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
exit_centered_structured_block(&mut centered_blocks, lines.len());
in_footnote_definition = false;
}
Event::Start(Tag::DefinitionList) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
enter_centered_structured_block(&mut centered_blocks, lines.len());
in_definition_list = true;
}
Event::End(TagEnd::DefinitionList) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
exit_centered_structured_block(&mut centered_blocks, lines.len());
in_definition_list = false;
if blockquote_depth == 0 && list_stack.is_empty() && !in_footnote_definition {
push_block_separator(
&mut lines,
MarkdownBlockKind::DefinitionList,
spacing_mode,
);
}
}
Event::Start(Tag::DefinitionListTitle) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled("• ", Style::default().fg(md_dim_color())));
}
Event::End(TagEnd::DefinitionListTitle) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
}
Event::Start(Tag::DefinitionListDefinition) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(" -> ", Style::default().fg(md_dim_color())));
in_definition_item = true;
}
Event::End(TagEnd::DefinitionListDefinition) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
in_definition_item = false;
}
Event::Start(Tag::CodeBlock(kind)) => {
dbg_code_blocks += 1;
// Flush current line before code block
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
enter_centered_structured_block(&mut centered_blocks, lines.len());
in_code_block = true;
code_block_lang = match kind {
CodeBlockKind::Fenced(lang) if !lang.is_empty() => Some(lang.to_string()),
_ => None,
};
// Don't add header here - we'll add it at the end when we know the block width
code_block_content.clear();
}
Event::End(TagEnd::CodeBlock) => {
// Check if this is a mermaid diagram
let is_mermaid = mermaid_rendering_enabled()
&& code_block_lang
.as_ref()
.map(|l| mermaid::is_mermaid_lang(l))
.unwrap_or(false);
if is_mermaid {
dbg_mermaid_blocks += 1;
// Render mermaid diagram.
// In streaming mode this updates only the ephemeral preview entry.
let terminal_width = max_width.and_then(|w| u16::try_from(w).ok());
if !streaming_mode
&& !mermaid_should_register_active()
&& !mermaid::image_protocol_available()
{
lines.push(mermaid_sidebar_placeholder(
"↗ mermaid diagram (image protocols unavailable)",
));
continue;
}
let result = if streaming_mode {
mermaid::render_mermaid_deferred_with_stream_scope(
&code_block_content,
terminal_width,
dbg_mermaid_blocks as u64,
)
} else if deferred_mermaid_mode {
mermaid::render_mermaid_deferred_with_registration(
&code_block_content,
terminal_width,
mermaid_should_register_active(),
)
} else if !mermaid_should_register_active() {
Some(mermaid::render_mermaid_untracked(
&code_block_content,
terminal_width,
))
} else {
Some(mermaid::render_mermaid_sized(
&code_block_content,
terminal_width,
))
};
match result {
Some(result) => {
if streaming_mode
&& let mermaid::RenderResult::Image {
hash,
width,
height,
..
} = &result
{
mermaid::set_streaming_preview_diagram(
*hash, *width, *height, None,
);
}
let mermaid_lines = mermaid::result_to_lines(result, max_width);
lines.extend(mermaid_lines);
}
None => {
lines.push(mermaid_sidebar_placeholder(
MERMAID_PENDING_PLACEHOLDER_TEXT,
));
}
}
} else {
// Render code block with syntax highlighting (cached)
let highlighted =
highlight_code_cached(&code_block_content, code_block_lang.as_deref());
let lang_label = code_block_lang.as_deref().unwrap_or("");
// Add header
lines.push(
Line::from(Span::styled(
format!("┌─ {} ", lang_label),
Style::default().fg(md_dim_color()),
))
.left_aligned(),
);
// Add code lines
for hl_line in highlighted {
let mut spans =
vec![Span::styled("│ ", Style::default().fg(md_dim_color()))];
spans.extend(hl_line.spans);
lines.push(Line::from(spans).left_aligned());
}
// Add footer
lines.push(
Line::from(Span::styled("└─", Style::default().fg(md_dim_color())))
.left_aligned(),
);
}
exit_centered_structured_block(&mut centered_blocks, lines.len());
in_code_block = false;
code_block_lang = None;
code_block_content.clear();
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(&mut lines, MarkdownBlockKind::CodeBlock, spacing_mode);
}
}
Event::Code(code) => {
if in_image {
image_alt.push_str(&code);
continue;
}
// Inline code - handle differently in tables vs regular text
if in_table {
current_cell.push_str(&code);
} else {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(
code.to_string(),
apply_inline_decorations(
Style::default().fg(code_fg()).bg(code_bg()),
strike,
!link_targets.is_empty(),
),
));
}
}
Event::InlineMath(math) => {
if in_image {
image_alt.push('$');
image_alt.push_str(&math);
image_alt.push('$');
continue;
}
if in_table {
match latex_mode {
LatexRenderingMode::None => current_cell.push_str(&format!("${math}$")),
LatexRenderingMode::Unicode | LatexRenderingMode::Image => {
current_cell.push_str(&jcode_render_core::render_inline_latex(&math));
}
}
} else {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
match latex_mode {
LatexRenderingMode::None => current_spans.push(raw_math_inline_span(&math)),
LatexRenderingMode::Unicode => current_spans.push(math_inline_span(&math)),
LatexRenderingMode::Image
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition =>
{
if let Some(image_lines) = latex_image_lines(&math, false, max_width) {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
None,
);
lines.extend(image_lines);
} else {
current_spans.push(math_inline_span(&math));
}
}
LatexRenderingMode::Image => current_spans.push(math_inline_span(&math)),
}
}
}
Event::DisplayMath(math) => {
if in_image {
image_alt.push_str("$$");
image_alt.push_str(&math);
image_alt.push_str("$$");
continue;
}
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
if in_table {
match latex_mode {
LatexRenderingMode::None => current_cell.push_str(&format!("$${math}$$")),
LatexRenderingMode::Unicode | LatexRenderingMode::Image => {
current_cell.push_str(&jcode_render_core::render_inline_latex(&math));
}
}
} else {
let block_start = lines.len();
let rendered = match latex_mode {
LatexRenderingMode::None => raw_math_display_lines(&math),
LatexRenderingMode::Unicode => math_display_lines(&math),
LatexRenderingMode::Image
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition =>
{
latex_image_lines(&math, true, max_width)
.unwrap_or_else(|| math_display_lines(&math))
}
LatexRenderingMode::Image => math_display_lines(&math),
};
for line in rendered {
lines.push(with_blockquote_prefix(line, blockquote_depth));
}
record_centered_independent_block(
&mut centered_blocks,
block_start,
lines.len(),
);
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(
&mut lines,
MarkdownBlockKind::DisplayMath,
spacing_mode,
);
}
}
}
Event::Text(text) => {
if in_code_block {
code_block_content.push_str(&text);
} else if in_image {
image_alt.push_str(&text);
} else if in_table {
current_cell.push_str(&text);
} else {
// "Thought for X.Xs" footers and streamed reasoning lines
// (italic, sentinel-wrapped) render dim with no gutter.
let is_thinking_duration =
text.starts_with("Thought for ") && text.ends_with('s');
// The sentinel can appear at the start and/or end of the line
// (and smart-punctuation may split it across events), so latch
// on its presence anywhere and strip every occurrence.
let has_sentinel = text.contains(crate::REASONING_SENTINEL);
if has_sentinel {
// Latch for the rest of this emphasis span so smart-
// punctuation splits keep the dim/italic styling.
reasoning_emphasis = true;
}
let is_reasoning = reasoning_emphasis;
let stripped;
let text: &str = if has_sentinel {
stripped = text.replace(crate::REASONING_SENTINEL, "");
&stripped
} else {
&text
};
let mut style = if is_thinking_duration || is_reasoning {
Style::default().fg(md_dim_color()).italic()
} else {
match (bold, italic) {
(true, true) => Style::default().fg(bold_color()).bold().italic(),
(true, false) => Style::default().fg(bold_color()).bold(),
(false, true) => Style::default().fg(text_color()).italic(),
(false, false) => Style::default().fg(text_color()),
}
};
style = apply_inline_decorations(style, strike, !link_targets.is_empty());
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(text.to_string(), style));
}
}
Event::SoftBreak => {
if in_image {
image_alt.push(' ');
} else if !in_code_block {
if blockquote_depth > 0 {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
} else {
current_spans.push(Span::raw(" "));
}
}
}
Event::HardBreak => {
if in_image {
image_alt.push(' ');
} else if !in_code_block {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
}
}
Event::Rule => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
let block_start = lines.len();
let width = rendered_rule_width(max_width);
let rule = Span::styled("─".repeat(width), Style::default().fg(md_dim_color()));
lines.push(with_blockquote_prefix(
Line::from(rule).left_aligned(),
blockquote_depth,
));
record_centered_independent_block(&mut centered_blocks, block_start, lines.len());
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(&mut lines, MarkdownBlockKind::Rule, spacing_mode);
}
}
Event::Html(html) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
let block_start = lines.len();
for raw in html.lines() {
let span =
Span::styled(raw.to_string(), Style::default().fg(html_fg()).italic());
lines.push(with_blockquote_prefix(
Line::from(span).left_aligned(),
blockquote_depth,
));
}
record_centered_independent_block(&mut centered_blocks, block_start, lines.len());
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(&mut lines, MarkdownBlockKind::HtmlBlock, spacing_mode);
}
}
Event::InlineHtml(html) => {
if in_image {
image_alt.push_str(&html);
} else if in_table {
current_cell.push_str(&html);
} else {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(
html.to_string(),
Style::default().fg(html_fg()).italic(),
));
}
}
Event::FootnoteReference(label) => {
if in_image {
image_alt.push_str(&format!("[^{}]", label));
} else if in_table {
current_cell.push_str(&format!("[^{}]", label));
} else {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(
format!("[^{}]", label),
Style::default().fg(md_dim_color()),
));
}
}
Event::TaskListMarker(checked) => {
if in_table {
current_cell.push_str(if checked { "[x] " } else { "[ ] " });
} else {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
current_spans.push(Span::styled(
if checked { "[x] " } else { "[ ] " },
Style::default().fg(md_dim_color()),
));
}
}
Event::Start(Tag::Paragraph) => {
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
if in_definition_item && current_spans.is_empty() {
current_spans.push(Span::styled(" ", Style::default().fg(md_dim_color())));
}
}
Event::End(TagEnd::Paragraph) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
push_block_separator(&mut lines, MarkdownBlockKind::Paragraph, spacing_mode);
}
Event::Start(Tag::Item) => {
dbg_list_items += 1;
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
ensure_blockquote_prefix(&mut current_spans, blockquote_depth);
let item_line_start = lines.len();
let depth = list_stack.len().saturating_sub(1);
let indent = " ".repeat(depth);
let marker = if let Some(state) = list_stack.last_mut() {
if state.ordered {
let idx = state.next_index;
state.next_index = state.next_index.saturating_add(1);
state.max_marker_digits =
state.max_marker_digits.max(idx.to_string().len());
state.item_line_starts.push(item_line_start);
format!("{}{}. ", indent, idx)
} else {
format!("{}• ", indent)
}
} else {
"• ".to_string()
};
current_spans.push(Span::styled(marker, Style::default().fg(md_dim_color())));
}
Event::End(TagEnd::Item) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
}
// Table handling
Event::Start(Tag::Table(_)) => {
dbg_tables += 1;
// Flush any pending content
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
enter_centered_structured_block(&mut centered_blocks, lines.len());
in_table = true;
table_rows.clear();
}
Event::End(TagEnd::Table) => {
// Render the collected table
if !table_rows.is_empty() {
let rendered = render_table(&table_rows, max_width);
lines.extend(rendered);
exit_centered_structured_block(&mut centered_blocks, lines.len());
if blockquote_depth == 0
&& list_stack.is_empty()
&& !in_definition_list
&& !in_footnote_definition
{
push_block_separator(&mut lines, MarkdownBlockKind::Table, spacing_mode);
}
} else {
exit_centered_structured_block(&mut centered_blocks, lines.len());
}
in_table = false;
table_rows.clear();
}
Event::Start(Tag::TableHead) => {
_is_header_row = true;
table_row.clear();
}
Event::End(TagEnd::TableHead) => {
if !table_row.is_empty() {
table_rows.push(table_row.clone());
}
table_row.clear();
_is_header_row = false;
}
Event::Start(Tag::TableRow) => {
table_row.clear();
}
Event::End(TagEnd::TableRow) => {
if !table_row.is_empty() {
table_rows.push(table_row.clone());
}
table_row.clear();
}
Event::Start(Tag::TableCell) => {
current_cell.clear();
}
Event::End(TagEnd::TableCell) => {
table_row.push(current_cell.trim().to_string());
current_cell.clear();
}
Event::Start(Tag::MetadataBlock(_)) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
}
Event::End(TagEnd::MetadataBlock(_)) => {
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
}
_ => {}
}
}
// Handle incomplete code block (streaming case)
// If we're still inside a code block, render what we have so far
if in_code_block && !code_block_content.is_empty() {
let is_mermaid = code_block_lang
.as_ref()
.map(|l| mermaid::is_mermaid_lang(l))
.unwrap_or(false);
if is_mermaid {
// For mermaid, show "rendering..." placeholder while streaming
let dim = Style::default().fg(md_dim_color());
lines.push(Line::from(Span::styled("┌─ mermaid (streaming...) ", dim)));
// Show first few lines of the diagram source
for source_line in code_block_content.lines().take(5) {
lines.push(Line::from(vec![
Span::styled("│ ", dim),
Span::styled(source_line.to_string(), Style::default().fg(code_fg())),
]));
}
if code_block_content.lines().count() > 5 {
lines.push(Line::from(Span::styled("│ ...", dim)));
}
lines.push(Line::from(Span::styled("└─", dim)));
} else {
// Regular code block - render what we have
let lang_str = code_block_lang.as_deref().unwrap_or("");
let header = format!(
"┌─ {} (streaming...)",
if lang_str.is_empty() {
"code"
} else {
lang_str
}
);
lines.push(Line::from(Span::styled(
header,
Style::default().fg(md_dim_color()),
)));
// Render code with syntax highlighting
let highlighted = highlight_code(&code_block_content, code_block_lang.as_deref());
for line in highlighted {
let mut prefixed = vec![Span::styled("│ ", Style::default().fg(md_dim_color()))];
prefixed.extend(line.spans);
lines.push(Line::from(prefixed));
}
// Show cursor to indicate more content coming
lines.push(Line::from(Span::styled(
"│ ▌",
Style::default().fg(md_dim_color()),
)));
lines.push(Line::from(Span::styled(
"└─",
Style::default().fg(md_dim_color()),
)));
}
}
// Flush remaining spans
flush_current_line_with_alignment(
&mut lines,
&mut current_spans,
structured_markdown_alignment(
blockquote_depth,
&list_stack,
in_definition_list,
in_footnote_definition,
),
);
finalize_centered_structured_blocks(&mut centered_blocks, lines.len());
normalize_block_separators(&mut lines);
if center_code_blocks()
&& let Some(width) = max_width
{
center_structured_block_ranges(&mut lines, width, &centered_blocks.ranges);
}
THREAD_RENDER_COUNT.with(|c| c.set(c.get() + 1));
if let Ok(mut state) = MARKDOWN_DEBUG.lock() {
state.stats.total_renders += 1;
state.stats.last_render_ms = Some(render_start.elapsed().as_secs_f32() * 1000.0);
state.stats.last_text_len = Some(text.len());
state.stats.last_lines = Some(lines.len());
state.stats.last_headings = dbg_headings;
state.stats.last_code_blocks = dbg_code_blocks;
state.stats.last_mermaid_blocks = dbg_mermaid_blocks;
state.stats.last_tables = dbg_tables;
state.stats.last_list_items = dbg_list_items;
state.stats.last_blockquotes = dbg_blockquotes;
}
lines
}