docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
This commit is contained in:
@@ -1,7 +1,4 @@
|
||||
//! Reusable buffers and internal data types for markdown parsing and rendering.
|
||||
//!
|
||||
//! This module contains all the intermediate data structures used by
|
||||
//! MarkdownHighlighter during parsing and rendering.
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
@@ -9,7 +6,6 @@ use anstyle::Style as AnsiStyle;
|
||||
use ratatui::text::{Line, Span};
|
||||
use syntect::highlighting::Style as SyntectStyle;
|
||||
|
||||
/// A range of text with optional styling.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Highlight {
|
||||
pub style: Option<AnsiStyle>,
|
||||
@@ -18,12 +14,11 @@ pub struct Highlight {
|
||||
|
||||
/// Syntax-highlighted code block replacement.
|
||||
///
|
||||
/// Stores the raw highlighted spans per line (intermediate representation).
|
||||
/// This allows rendering to either ANSI strings or ratatui Lines on demand.
|
||||
/// Spans are kept in their intermediate form so the block can be rendered to
|
||||
/// either ANSI strings or ratatui Lines on demand.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Replace {
|
||||
/// Raw highlighted spans per line: Vec<(style, text)>.
|
||||
/// Each inner Vec represents one line of the code block.
|
||||
/// One inner Vec per line of the code block.
|
||||
pub highlighted: Vec<Vec<(SyntectStyle, String)>>,
|
||||
/// Source byte range this replaces.
|
||||
pub range: Range<usize>,
|
||||
@@ -37,7 +32,6 @@ pub struct Replace {
|
||||
pub struct LinkTarget {
|
||||
/// Source byte range of the *link text* (not the full `[text](url)` span).
|
||||
pub source_range: Range<usize>,
|
||||
/// Destination URL.
|
||||
pub url: String,
|
||||
/// Monotonically increasing identifier assigned during parsing.
|
||||
pub id: u32,
|
||||
@@ -118,12 +112,10 @@ impl StyledCell {
|
||||
Self { spans: Vec::new() }
|
||||
}
|
||||
|
||||
/// Get plain text content (for width calculation).
|
||||
pub fn plain_text(&self) -> String {
|
||||
self.spans.iter().map(|s| s.text.as_str()).collect()
|
||||
}
|
||||
|
||||
/// Clear the cell content.
|
||||
pub fn clear(&mut self) {
|
||||
self.spans.clear();
|
||||
}
|
||||
@@ -134,15 +126,10 @@ impl StyledCell {
|
||||
pub struct TableState {
|
||||
/// Column alignments from the table header.
|
||||
pub alignments: Vec<pulldown_cmark::Alignment>,
|
||||
/// Header row cells.
|
||||
pub header: Vec<StyledCell>,
|
||||
/// Body rows (each row is a Vec of styled cells).
|
||||
pub rows: Vec<Vec<StyledCell>>,
|
||||
/// Current row being built.
|
||||
pub current_row: Vec<StyledCell>,
|
||||
/// Current cell content being accumulated.
|
||||
pub current_cell: StyledCell,
|
||||
/// Current style state for the cell.
|
||||
pub cell_bold: bool,
|
||||
pub cell_italic: bool,
|
||||
pub cell_code: bool,
|
||||
@@ -151,7 +138,6 @@ pub struct TableState {
|
||||
/// is set produce link-tagged `CellSpan`s so the table renderer can
|
||||
/// apply link styling and emit `HyperlinkTarget`s.
|
||||
pub cell_link: Option<(String, u32)>,
|
||||
/// Whether we're in the header section.
|
||||
pub in_header: bool,
|
||||
/// Source byte range of the entire table.
|
||||
pub range: Range<usize>,
|
||||
@@ -194,11 +180,9 @@ impl TableState {
|
||||
/// `HyperlinkTarget`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TableHyperlink {
|
||||
/// Index within `TableReplace::styled_lines`.
|
||||
pub line_offset: usize,
|
||||
/// Column range (display cells) on that line.
|
||||
/// Column range in display cells, not bytes.
|
||||
pub column_range: Range<usize>,
|
||||
/// Destination URL.
|
||||
pub url: String,
|
||||
/// Stable identifier shared with the paragraph link path.
|
||||
pub id: u32,
|
||||
@@ -245,7 +229,6 @@ pub struct MermaidReplace {
|
||||
pub range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Calculate the display width of a string (accounting for Unicode).
|
||||
pub fn unicode_display_width(s: &str) -> usize {
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
s.width()
|
||||
@@ -287,8 +270,10 @@ pub enum RenderEventKind {
|
||||
Mermaid = 3,
|
||||
}
|
||||
|
||||
/// Render event: marks where a highlight/replace/table starts or ends.
|
||||
/// Derives Ord for sorting by (pos, kind, index, is_end).
|
||||
/// Marks where a highlight/replace/table starts or ends.
|
||||
///
|
||||
/// Field order is load-bearing: the derived `Ord` sorts the event queue by
|
||||
/// (pos, kind, index, is_end).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct RenderEvent {
|
||||
pub pos: usize,
|
||||
@@ -301,22 +286,8 @@ pub struct RenderEvent {
|
||||
///
|
||||
/// All vectors are cleared (keeping capacity) between renders, eliminating
|
||||
/// allocation overhead in the streaming hot path.
|
||||
///
|
||||
/// # Buffer Categories
|
||||
///
|
||||
/// **Parse output buffers** - populated during `run()`, read-only during `render()`:
|
||||
/// - `highlights`: Style ranges for inline formatting
|
||||
/// - `replaces`: Syntax-highlighted code blocks
|
||||
/// - `transforms`: Character substitutions (e.g., bullets)
|
||||
/// - `untagged_code_ranges`: Code blocks without language tags
|
||||
/// - `table_replaces`: Formatted table replacements
|
||||
///
|
||||
/// **Render scratch buffers** - temporary storage during `render()`:
|
||||
/// - `render_events`: Sorted event queue for the render loop
|
||||
/// - `current_spans`: Building current line's spans
|
||||
/// - `active_highlights`: Stack of active highlight indices
|
||||
pub struct MarkdownBuffers {
|
||||
// Parse output buffers (written by run(), read by render())
|
||||
// Parse output buffers: written by run(), read-only during render().
|
||||
pub highlights: Vec<Highlight>,
|
||||
pub replaces: Vec<Replace>,
|
||||
pub transforms: Vec<Transform>,
|
||||
@@ -327,7 +298,7 @@ pub struct MarkdownBuffers {
|
||||
/// Closed fenced code blocks, in document order (see [`CodeBlockMeta`]).
|
||||
pub code_blocks: Vec<CodeBlockMeta>,
|
||||
|
||||
// Render scratch buffers (used only during render())
|
||||
// Render scratch buffers: used only during render().
|
||||
pub render_events: Vec<RenderEvent>,
|
||||
pub current_spans: Vec<Span<'static>>,
|
||||
pub active_highlights: Vec<usize>,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Checkpoint types for incremental markdown rendering.
|
||||
//!
|
||||
//! This module defines types for identifying stable boundaries in markdown text
|
||||
//! where rendered output can be "frozen" and cached. Content before a checkpoint
|
||||
//! will not change regardless of what text is appended after it.
|
||||
//! A checkpoint marks a stable boundary in markdown text where rendered output
|
||||
//! can be "frozen" and cached: content before it will not change regardless of
|
||||
//! what text is appended after it.
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
|
||||
@@ -165,7 +165,7 @@ pub fn set_color_level(level: ColorLevel) -> Result<(), ColorLevel> {
|
||||
/// Convert an `anstyle::Color` to the appropriate level based on terminal support.
|
||||
///
|
||||
/// This will downgrade colors as needed:
|
||||
/// - TrueColor terminals: pass through unchanged
|
||||
/// - TrueColor terminals: pass through `unchanged`
|
||||
/// - 256-color terminals: RGB colors are converted to closest ANSI 256 color
|
||||
/// - Basic terminals: colors are converted to closest ANSI 16 color
|
||||
/// - No color: returns None
|
||||
@@ -234,22 +234,26 @@ mod tests {
|
||||
|
||||
// Medium gray
|
||||
let result = rgb_to_ansi256(RgbColor(128, 128, 128));
|
||||
assert!(result.index() >= 232); // Should be in grayscale range
|
||||
// Should be in grayscale range
|
||||
assert!(result.index() >= 232);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_ansi256_colors() {
|
||||
// Pure red
|
||||
let result = rgb_to_ansi256(RgbColor(255, 0, 0));
|
||||
assert_eq!(result.index(), 196); // Bright red in the cube
|
||||
// Bright red in the cube
|
||||
assert_eq!(result.index(), 196);
|
||||
|
||||
// Pure green
|
||||
let result = rgb_to_ansi256(RgbColor(0, 255, 0));
|
||||
assert_eq!(result.index(), 46); // Bright green in the cube
|
||||
// Bright green in the cube
|
||||
assert_eq!(result.index(), 46);
|
||||
|
||||
// Pure blue
|
||||
let result = rgb_to_ansi256(RgbColor(0, 0, 255));
|
||||
assert_eq!(result.index(), 21); // Bright blue in the cube
|
||||
// Bright blue in the cube
|
||||
assert_eq!(result.index(), 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -60,7 +60,7 @@ pub(crate) struct ChunkLinkRange {
|
||||
/// transform's replacement string. Both endpoints (start/end) clamp the
|
||||
/// same direction, so a link whose endpoint straddles a transform produces
|
||||
/// a column range that excludes the straddling bytes. This is intentional
|
||||
/// rather than precise — a future transform that intentionally rewrites
|
||||
/// rather than precise — a future transform that deliberately rewrites
|
||||
/// link text should add a typed mapping instead of relying on this clamp.
|
||||
pub(crate) fn source_to_chunk_offset(
|
||||
src_pos: usize,
|
||||
@@ -231,7 +231,7 @@ mod hyperlink_tests {
|
||||
/// one whose `column_range` slices to `expected_slice` in the
|
||||
/// rendered output. Since `render_markdown_ratatui_full` now also
|
||||
/// emits a url_scan target for the pretty-mode `(url)` suffix, tests
|
||||
/// that previously checked `hyperlinks.len() == 1` must explicitly
|
||||
/// that earlier checked `hyperlinks.len() == 1` must explicitly
|
||||
/// pick the parser-produced entry.
|
||||
fn parser_link_text<'a>(
|
||||
out: &'a crate::output::MarkdownRenderOutput,
|
||||
@@ -448,7 +448,7 @@ mod hyperlink_tests {
|
||||
let view = renderer.view();
|
||||
|
||||
// Compare on `(url, line_index, column_range)` — ids are
|
||||
// intentionally independent between the two code paths (full
|
||||
// Deliberately independent between the two code paths (full
|
||||
// re-render restarts id counters; streaming preserves continuity).
|
||||
let extract = |hs: &[HyperlinkTarget]| -> Vec<(String, usize, std::ops::Range<usize>)> {
|
||||
let mut v: Vec<_> = hs
|
||||
@@ -623,7 +623,7 @@ mod hyperlink_tests {
|
||||
}
|
||||
|
||||
/// Paragraph links must keep the `link_text` foreground color even when
|
||||
/// the `text` style sets its own foreground. Previously the parser
|
||||
/// the `text` style sets its own foreground. earlier the parser
|
||||
/// pushed `ms.text` as a highlight after the link_text highlight whenever
|
||||
/// no `Heading`/`Emphasis`/`Strong`/`Strikethrough` ancestor was present
|
||||
/// — and `merge_styles` lets the later fg color win, so `ms.text`'s color
|
||||
|
||||
@@ -205,7 +205,7 @@ fn script_atom_is_wordlike(atom: &str, rendered: &str) -> bool {
|
||||
fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode: Mode) {
|
||||
let name = cursor.read_command_name();
|
||||
match name {
|
||||
// ── Structure ────────────────────────────────────────────────────
|
||||
// Structure
|
||||
"" => out.push('\\'),
|
||||
"\\" => out.push('\n'),
|
||||
"begin" => render_environment(cursor, out, depth, mode),
|
||||
@@ -232,7 +232,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fractions / binomials / roots ────────────────────────────────
|
||||
// Fractions / binomials / roots
|
||||
"frac" | "dfrac" | "tfrac" | "cfrac" => {
|
||||
let num = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
|
||||
let den = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
|
||||
@@ -261,7 +261,8 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
cursor.bump();
|
||||
}
|
||||
let idx = &cursor.src[start..cursor.pos];
|
||||
cursor.bump(); // consume `]`
|
||||
// consume `]`
|
||||
cursor.bump();
|
||||
Some(render_atom(idx, depth, mode))
|
||||
} else {
|
||||
None
|
||||
@@ -290,7 +291,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Boxes (frame dropped; content preserved) ─────────────────────
|
||||
// Boxes (frame dropped; content preserved)
|
||||
"boxed" => {
|
||||
if let Some(arg) = take_brace_arg(cursor) {
|
||||
out.push_str(&render_atom(arg, depth, mode));
|
||||
@@ -302,7 +303,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text / alphabets ─────────────────────────────────────────────
|
||||
// Text / alphabets
|
||||
"text" | "textrm" | "textit" | "textbf" | "textsf" | "texttt" | "textnormal" | "mbox"
|
||||
| "hbox" => {
|
||||
if let Some(arg) = take_brace_arg(cursor) {
|
||||
@@ -321,7 +322,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
render_mapped_alphabet(cursor, out, depth, mode, map_mathbf)
|
||||
}
|
||||
|
||||
// ── Accents (combining marks) ────────────────────────────────────
|
||||
// Accents (combining marks)
|
||||
"hat" | "widehat" => render_accent(cursor, out, depth, mode, '\u{0302}'),
|
||||
"bar" | "overline" => render_accent(cursor, out, depth, mode, '\u{0304}'),
|
||||
"tilde" | "widetilde" => render_accent(cursor, out, depth, mode, '\u{0303}'),
|
||||
@@ -335,7 +336,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
"mathring" => render_accent(cursor, out, depth, mode, '\u{030A}'),
|
||||
"underline" => render_accent(cursor, out, depth, mode, '\u{0332}'),
|
||||
|
||||
// ── Negation ─────────────────────────────────────────────────────
|
||||
// Negation
|
||||
"not" => {
|
||||
if let Some(atom) = cursor.read_atom() {
|
||||
let rendered = render_atom(atom, depth, mode);
|
||||
@@ -359,7 +360,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Decorations rendered as base + script ────────────────────────
|
||||
// Decorations rendered as base + script
|
||||
"overset" | "stackrel" => {
|
||||
let over = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
|
||||
let base = take_brace_arg(cursor).map(|a| render_atom(a, depth, mode));
|
||||
@@ -385,7 +386,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Modular arithmetic ───────────────────────────────────────────
|
||||
// Modular arithmetic
|
||||
"pmod" => {
|
||||
if let Some(arg) = take_brace_arg(cursor) {
|
||||
if !out.at_line_start() && !out.ends_with_space() {
|
||||
@@ -401,7 +402,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
out.push_str("mod ");
|
||||
}
|
||||
|
||||
// ── Spacing ──────────────────────────────────────────────────────
|
||||
// Spacing
|
||||
"," | ";" | ":" | ">" | " " | "space" | "thinspace" | "medspace" | "thickspace"
|
||||
| "enspace" => {
|
||||
if !out.at_line_start() && !out.ends_with_space() {
|
||||
@@ -412,7 +413,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
"qquad" => out.push_str(" "),
|
||||
"!" | "negthinspace" | "negmedspace" | "negthickspace" => {}
|
||||
|
||||
// ── No-ops (sizing/styling/structure hints) ──────────────────────
|
||||
// No-ops (sizing/styling/structure hints)
|
||||
"limits" | "nolimits" | "displaystyle" | "textstyle" | "scriptstyle"
|
||||
| "scriptscriptstyle" | "big" | "Big" | "bigg" | "Bigg" | "bigl" | "Bigl" | "biggl"
|
||||
| "Biggl" | "bigr" | "Bigr" | "biggr" | "Biggr" | "bigm" | "Bigm" | "biggm" | "Biggm"
|
||||
@@ -425,7 +426,7 @@ fn render_command(cursor: &mut Cursor<'_>, out: &mut MathBox, depth: usize, mode
|
||||
}
|
||||
}
|
||||
|
||||
// ── Symbol table ─────────────────────────────────────────────────
|
||||
// Symbol table
|
||||
_ => {
|
||||
if let Some(sym) = symbol(name) {
|
||||
out.push_str(sym);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//! Byte cursor over TeX source.
|
||||
|
||||
/// Byte cursor over the TeX source.
|
||||
pub(super) struct Cursor<'a> {
|
||||
pub(super) src: &'a str,
|
||||
pub(super) pos: usize,
|
||||
@@ -24,7 +23,7 @@ impl<'a> Cursor<'a> {
|
||||
/// Consume `\command` (alphabetic name) or `\<single char>`; the leading
|
||||
/// backslash must already be consumed. Returns the command name.
|
||||
///
|
||||
/// Unlike TeX we do NOT consume trailing whitespace: the caller's
|
||||
/// Unlike TeX, trailing whitespace is NOT consumed: the caller's
|
||||
/// whitespace collapsing keeps `\to 0` rendering as `→ 0`.
|
||||
pub(super) fn read_command_name(&mut self) -> &'a str {
|
||||
let start = self.pos;
|
||||
@@ -43,7 +42,6 @@ impl<'a> Cursor<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip whitespace (TeX collapses it; meaning comes from commands).
|
||||
pub(super) fn skip_ws(&mut self) {
|
||||
while matches!(self.peek(), Some(c) if c.is_whitespace()) {
|
||||
self.bump();
|
||||
@@ -59,7 +57,7 @@ impl<'a> Cursor<'a> {
|
||||
while let Some(ch) = self.bump() {
|
||||
match ch {
|
||||
'\\' => {
|
||||
// Skip escaped char so `\{`/`\}` don't affect depth.
|
||||
// Skip the escaped char so `\{`/`\}` don't affect depth.
|
||||
self.bump();
|
||||
}
|
||||
'{' => depth += 1,
|
||||
|
||||
@@ -19,8 +19,8 @@ pub(super) fn render_environment(
|
||||
};
|
||||
let env_name = env_name.trim().trim_end_matches('*');
|
||||
|
||||
// Capture body source until the matching `\end{name}`, tracking nesting
|
||||
// of same-named environments. Scans raw source from the cursor.
|
||||
// Scan raw source for the matching `\end{name}`, tracking nesting of
|
||||
// same-named environments.
|
||||
let body_start = cursor.pos;
|
||||
let mut body_end = cursor.src.len();
|
||||
let mut resume = cursor.src.len();
|
||||
@@ -38,8 +38,8 @@ pub(super) fn render_environment(
|
||||
} else if command_at(after_bs, "end") {
|
||||
"end".len()
|
||||
} else {
|
||||
// Not begin/end: skip the backslash and the char after it (so
|
||||
// `\\` and `\{` never confuse the scan).
|
||||
// Skip the backslash and the char after it so that `\\` and `\{`
|
||||
// never confuse the scan.
|
||||
let skip = after_bs.chars().next().map_or(0, char::len_utf8);
|
||||
search = bs_pos + 1 + skip.max(1);
|
||||
continue;
|
||||
@@ -66,7 +66,7 @@ pub(super) fn render_environment(
|
||||
cursor.pos = resume;
|
||||
let mut body = &cursor.src[body_start..body_end.min(cursor.src.len())];
|
||||
|
||||
// Optional column spec for array environments: `\begin{array}{ll}`.
|
||||
// Discard the optional column spec: `\begin{array}{ll}`.
|
||||
if env_name == "array" || env_name == "alignat" {
|
||||
let mut probe = Cursor::new(body);
|
||||
probe.skip_ws();
|
||||
@@ -92,9 +92,9 @@ fn command_at(rest: &str, word: &str) -> bool {
|
||||
|
||||
/// Split an environment body into rows (`\\`) and cells (`&`) at brace and
|
||||
/// environment depth 0, render each cell, then lay the rows out according to
|
||||
/// the environment. Returns one string per visual row; the caller attaches
|
||||
/// them as a box. In `flat` mode, matrix/cases environments render as a
|
||||
/// single row with `; ` between matrix rows.
|
||||
/// the environment. Returns one string per visual row. In `flat` mode,
|
||||
/// matrix/cases environments collapse to a single row with `; ` between
|
||||
/// matrix rows.
|
||||
fn env_rows_to_strings(
|
||||
body: &str,
|
||||
env_name: &str,
|
||||
@@ -130,7 +130,7 @@ fn env_rows_to_strings(
|
||||
env_depth = env_depth.saturating_sub(1);
|
||||
}
|
||||
// Skip the backslash plus the char after it so escaped
|
||||
// delimiters (`\&`, `\{`, `\}`) never affect depth/splits.
|
||||
// delimiters (`\&`, `\{`, `\}`) never affect depth or splits.
|
||||
let skip = rest.chars().next().map_or(0, char::len_utf8);
|
||||
i += 1 + skip.max(1);
|
||||
continue;
|
||||
@@ -148,7 +148,6 @@ fn env_rows_to_strings(
|
||||
row.push(body[cell_start.min(bytes.len())..].to_string());
|
||||
rows.push(row);
|
||||
|
||||
// Render each cell, drop fully-empty rows.
|
||||
let mut rendered_rows: Vec<Vec<String>> = rows
|
||||
.into_iter()
|
||||
.map(|cells| {
|
||||
@@ -177,15 +176,14 @@ fn env_rows_to_strings(
|
||||
let n_rows = rendered_rows.len();
|
||||
|
||||
if is_matrix {
|
||||
// Flat (inline) mode: one row, single delimiter pair, rows joined
|
||||
// with `; ` — `(1 2; 3 4)`.
|
||||
if flat {
|
||||
let inner = rendered_rows
|
||||
.iter()
|
||||
.map(|cells| cells.join(" "))
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ");
|
||||
// Single-row delimiter pair; plain `matrix` has none (' ').
|
||||
// Ask for the single-row form; plain `matrix` has no delimiter
|
||||
// and reports ' '.
|
||||
let (l, r) = matrix_delims(env_name, 0, 1);
|
||||
let mut s = String::new();
|
||||
if l != ' ' {
|
||||
@@ -197,7 +195,6 @@ fn env_rows_to_strings(
|
||||
}
|
||||
return vec![s];
|
||||
}
|
||||
// Pad columns to equal width so rows align.
|
||||
let n_cols = rendered_rows.iter().map(Vec::len).max().unwrap_or(0);
|
||||
let mut widths = vec![0usize; n_cols];
|
||||
for cells in &rendered_rows {
|
||||
@@ -243,9 +240,7 @@ fn env_rows_to_strings(
|
||||
.collect()
|
||||
} else {
|
||||
// aligned/align/gather/split/equation/…: `&` is an invisible
|
||||
// alignment marker; rejoin cells with a single space. One string per
|
||||
// row; the caller's box attachment (or flat `; ` join) handles the
|
||||
// rest.
|
||||
// alignment marker, so cells rejoin with a single space.
|
||||
rendered_rows
|
||||
.iter()
|
||||
.map(|cells| {
|
||||
@@ -255,7 +250,7 @@ fn env_rows_to_strings(
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
// Collapse any double spaces introduced around markers.
|
||||
// Empty alignment cells leave runs of spaces behind.
|
||||
while s.contains(" ") {
|
||||
s = s.replace(" ", " ");
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ impl MathBox {
|
||||
self.lines.push(String::new());
|
||||
}
|
||||
}
|
||||
// Place the box rows, left-padded to the attach column.
|
||||
// Place the box rows, left-`padded` to the attach column.
|
||||
for (i, row) in rows.iter().enumerate() {
|
||||
let target = self.anchor - box_anchor + i;
|
||||
let line = &mut self.lines[target];
|
||||
|
||||
@@ -79,7 +79,6 @@ pub(crate) fn latex_to_unicode_display(src: &str) -> Option<Vec<String>> {
|
||||
Some(lines)
|
||||
}
|
||||
|
||||
/// Run the converter and return the output lines.
|
||||
fn convert(src: &str, flat: bool) -> Vec<String> {
|
||||
let mut cursor = Cursor::new(src);
|
||||
let mut out = MathBox::new(flat);
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
//! Character and symbol mapping tables.
|
||||
//!
|
||||
//! In the `map_math*` alphabets the explicit letter arms come first on
|
||||
//! purpose: those letters were encoded as Letterlike Symbols before the
|
||||
//! contiguous Mathematical Alphanumeric blocks existed, so their block slots
|
||||
//! are unassigned and the arithmetic arms below would yield a reserved
|
||||
//! codepoint.
|
||||
|
||||
pub(super) fn to_superscript(c: char) -> Option<char> {
|
||||
Some(match c {
|
||||
|
||||
@@ -29,10 +29,10 @@ fn subscripts_map_to_unicode() {
|
||||
|
||||
#[test]
|
||||
fn script_fallback_uses_parens() {
|
||||
// φ has no superscript form → fall back to ^(...)
|
||||
// Greek letters have no superscript forms; a multi-char run falls back to
|
||||
// ^(...) while a lone char keeps the bare marker.
|
||||
assert_eq!(inline("x^{\\alpha\\beta}"), "x^(αβ)");
|
||||
assert_eq!(inline("x^\\alpha"), "x^α");
|
||||
// Single unmappable subscript char.
|
||||
assert_eq!(inline("a_q"), "a_q");
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
//! located and the ASCII whitespace immediately inside the delimiters is
|
||||
//! trimmed, so the emitted `$…$` has no space right after the opening `$` or
|
||||
//! before the closing `$`. pulldown-cmark's dollar-math flanking rule rejects
|
||||
//! `$ … $` (whitespace next to a delimiter) and would otherwise leave a padded
|
||||
//! `$ … $` (whitespace next to a delimiter) and would otherwise leave a `padded`
|
||||
//! span as raw `$ … $` text. Interior newlines join to spaces (TeX treats them
|
||||
//! as spaces) so a span wrapped across source lines cannot be re-parsed as
|
||||
//! block structure.
|
||||
@@ -199,7 +199,8 @@ impl LatexDelimiterNormalizer {
|
||||
b'`' => {
|
||||
let run = count_run(bytes, i, b'`');
|
||||
if i + run == n && !final_flush {
|
||||
break; // run may extend; hold it back
|
||||
// run may extend; hold it back
|
||||
break;
|
||||
}
|
||||
out.push_str(&buf[i..i + run]);
|
||||
i += run;
|
||||
@@ -277,7 +278,8 @@ impl LatexDelimiterNormalizer {
|
||||
b'$' => {
|
||||
let run = count_run(bytes, i, b'$');
|
||||
if run == 1 && i + 1 == n && !final_flush {
|
||||
break; // may become `$$`; hold it back
|
||||
// may become `$$`; hold it back
|
||||
break;
|
||||
}
|
||||
if run >= 2 {
|
||||
// A display opener is exactly two `$`; any further
|
||||
@@ -340,7 +342,8 @@ impl LatexDelimiterNormalizer {
|
||||
let r = count_run(bytes, i, b'`');
|
||||
if i + r == n && !final_flush {
|
||||
out.push_str(&buf[start..i]);
|
||||
return (out, i); // hold back the trailing run
|
||||
// hold back the trailing run
|
||||
return (out, i);
|
||||
}
|
||||
if r == run {
|
||||
i += r;
|
||||
@@ -350,13 +353,15 @@ impl LatexDelimiterNormalizer {
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
i += r; // non-matching run is literal content
|
||||
// non-matching run is literal content
|
||||
i += r;
|
||||
}
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
if !handled {
|
||||
out.push_str(&buf[start..i]); // EOF inside code
|
||||
// EOF inside code
|
||||
out.push_str(&buf[start..i]);
|
||||
}
|
||||
}
|
||||
State::Fenced { ch, len } => {
|
||||
@@ -379,7 +384,8 @@ impl LatexDelimiterNormalizer {
|
||||
i += 1;
|
||||
}
|
||||
if i < n {
|
||||
i += 1; // include the newline
|
||||
// include the newline
|
||||
i += 1;
|
||||
self.at_line_start = true;
|
||||
} else {
|
||||
self.at_line_start = false;
|
||||
@@ -430,13 +436,15 @@ fn scan_fence_open(bytes: &[u8], i: usize, final_flush: bool) -> FenceScan {
|
||||
j += 1;
|
||||
}
|
||||
if spaces >= 4 {
|
||||
return FenceScan::No; // indented; not treated as a fence opener
|
||||
// indented; not treated as a fence opener
|
||||
return FenceScan::No;
|
||||
}
|
||||
if j == n {
|
||||
return if final_flush {
|
||||
FenceScan::No
|
||||
} else {
|
||||
FenceScan::NeedMore // ≤3 spaces then EOF: a fence may still start
|
||||
// ≤3 spaces then EOF: a fence may still start
|
||||
FenceScan::NeedMore
|
||||
};
|
||||
}
|
||||
let ch = bytes[j];
|
||||
@@ -445,10 +453,12 @@ fn scan_fence_open(bytes: &[u8], i: usize, final_flush: bool) -> FenceScan {
|
||||
}
|
||||
let run = count_run(bytes, j, ch);
|
||||
if j + run == n && !final_flush {
|
||||
return FenceScan::NeedMore; // run may extend
|
||||
// run may extend
|
||||
return FenceScan::NeedMore;
|
||||
}
|
||||
if run < 3 {
|
||||
return FenceScan::No; // inline code / stray tildes, not a fence
|
||||
// inline code / stray tildes, not a fence
|
||||
return FenceScan::No;
|
||||
}
|
||||
FenceScan::Match {
|
||||
ch,
|
||||
@@ -482,7 +492,8 @@ fn scan_fence_close(bytes: &[u8], i: usize, ch: u8, len: usize, final_flush: boo
|
||||
}
|
||||
let run = count_run(bytes, j, ch);
|
||||
if j + run == n && !final_flush {
|
||||
return FenceScan::NeedMore; // run may still grow to >= len
|
||||
// run may still grow to >= len
|
||||
return FenceScan::NeedMore;
|
||||
}
|
||||
if run < len {
|
||||
return FenceScan::No;
|
||||
@@ -510,7 +521,8 @@ fn scan_fence_close(bytes: &[u8], i: usize, ch: u8, len: usize, final_flush: boo
|
||||
end: j + run,
|
||||
}
|
||||
} else {
|
||||
FenceScan::No // non-whitespace after the run → info string → content
|
||||
// non-whitespace after the run → info string → content
|
||||
FenceScan::No
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,9 +629,11 @@ fn find_inline_close(bytes: &[u8], open: usize, final_flush: bool) -> InlineClos
|
||||
}
|
||||
if bytes[k] == b'\\' {
|
||||
match bytes.get(k + 1) {
|
||||
None => break, // trailing `\`: need the next byte to classify
|
||||
// trailing `\`: need the next byte to classify
|
||||
None => break,
|
||||
Some(b')') => return InlineClose::Found { close: k },
|
||||
Some(_) => k += 2, // `\\` pair or `\x` escape: skip both bytes
|
||||
// `\\` pair or `\x` escape: skip both bytes
|
||||
Some(_) => k += 2,
|
||||
}
|
||||
} else {
|
||||
k += 1;
|
||||
@@ -678,7 +692,8 @@ fn find_display_close(buf: &str, content_start: usize, final_flush: bool) -> Dis
|
||||
}
|
||||
match bytes[k] {
|
||||
b'\\' => match bytes.get(k + 1) {
|
||||
None => break, // trailing `\`: need the next byte to classify
|
||||
// trailing `\`: need the next byte to classify
|
||||
None => break,
|
||||
Some(b']') => {
|
||||
return DisplayClose::Found {
|
||||
close: k,
|
||||
@@ -709,9 +724,11 @@ fn find_display_close(buf: &str, content_start: usize, final_flush: bool) -> Dis
|
||||
if could_extend && !final_flush {
|
||||
return DisplayClose::NeedMore;
|
||||
}
|
||||
k += 2; // `\e…` of something else: span content
|
||||
// `\e…` of something else: span content
|
||||
k += 2;
|
||||
}
|
||||
Some(_) => k += 2, // `\\` pair or `\x` escape: span content
|
||||
// `\\` pair or `\x` escape: span content
|
||||
Some(_) => k += 2,
|
||||
},
|
||||
b'$' => {
|
||||
let run = count_run(bytes, k, b'$');
|
||||
@@ -722,7 +739,8 @@ fn find_display_close(buf: &str, content_start: usize, final_flush: bool) -> Dis
|
||||
};
|
||||
}
|
||||
if k + run == n && !final_flush {
|
||||
return DisplayClose::NeedMore; // lone `$` at EOB may extend
|
||||
// lone `$` at EOB may extend
|
||||
return DisplayClose::NeedMore;
|
||||
}
|
||||
k += run;
|
||||
}
|
||||
@@ -734,7 +752,8 @@ fn find_display_close(buf: &str, content_start: usize, final_flush: bool) -> Dis
|
||||
j += 1;
|
||||
}
|
||||
if j == n {
|
||||
break; // need the next line's first byte to decide
|
||||
// need the next line's first byte to decide
|
||||
break;
|
||||
}
|
||||
if matches!(bytes[j], b'\n' | b'>') {
|
||||
return DisplayClose::Unmatched;
|
||||
@@ -757,7 +776,7 @@ fn find_display_close(buf: &str, content_start: usize, final_flush: bool) -> Dis
|
||||
/// through byte-for-byte, keeping the pass idempotent). Multi-line interiors
|
||||
/// have each line trimmed and joined with a single space so CommonMark block
|
||||
/// parsing (setext underlines, list items, headings) cannot split the span;
|
||||
/// TeX treats the newlines as spaces, so rendering is unchanged.
|
||||
/// TeX treats the newlines as spaces, so rendering is `unchanged`.
|
||||
fn emit_display_span(out: &mut String, interior: &str) {
|
||||
out.push_str("$$");
|
||||
push_joined_lines(out, interior);
|
||||
@@ -825,7 +844,7 @@ mod tests {
|
||||
normalize_latex_delimiters(s)
|
||||
}
|
||||
|
||||
// ── Basic conversions ────────────────────────────────────────────────
|
||||
// Basic conversions
|
||||
|
||||
#[test]
|
||||
fn inline_paren_converts() {
|
||||
@@ -833,7 +852,7 @@ mod tests {
|
||||
assert_eq!(norm("a \\(x\\) b"), "a $x$ b");
|
||||
}
|
||||
|
||||
// ── Inline `\( … \)` boundary-whitespace trimming (the regression) ────
|
||||
// Inline `\( … \)` boundary-whitespace trimming (the regression)
|
||||
|
||||
#[test]
|
||||
fn normalize_inline_paren_trims_boundary_ws() {
|
||||
@@ -858,7 +877,7 @@ mod tests {
|
||||
fn normalize_inline_paren_trim_leaves_escapes_and_dollars_alone() {
|
||||
// Escaped `\\(`/`\\)` is a literal backslash + paren, not a math span.
|
||||
assert_eq!(norm("\\\\( x \\\\)"), "\\\\( x \\\\)");
|
||||
// Only the backslash forms are ours: a space-padded bare `$ x $` is NOT
|
||||
// Only the backslash forms are ours: a space-`padded` bare `$ x $` is NOT
|
||||
// trimmed (currency untouched-ness is covered by `currency_not_misconverted`).
|
||||
assert_eq!(norm("$ x $"), "$ x $");
|
||||
}
|
||||
@@ -880,7 +899,7 @@ mod tests {
|
||||
assert_eq!(norm("a\n\\[x\\]\nb"), "a\n$$x$$\nb");
|
||||
}
|
||||
|
||||
// ── Multi-line display spans join onto one line ──────────────────────
|
||||
// Multi-line display spans join onto one line
|
||||
|
||||
#[test]
|
||||
fn multiline_display_with_setext_hazard_joins() {
|
||||
@@ -971,7 +990,7 @@ mod tests {
|
||||
assert_eq!(norm("$$\nprice \\$5\n=\nz\n$$"), "$$price \\$5 = z$$");
|
||||
}
|
||||
|
||||
// ── Inline `\(…\)` spans join interior newlines ──────────────────────
|
||||
// Inline `\(…\)` spans join interior newlines
|
||||
|
||||
#[test]
|
||||
fn multiline_inline_paren_joins() {
|
||||
@@ -1016,7 +1035,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Escapes & currency ───────────────────────────────────────────────
|
||||
// Escapes & currency
|
||||
|
||||
#[test]
|
||||
fn escaped_backslash_paren_stays_literal() {
|
||||
@@ -1037,7 +1056,7 @@ mod tests {
|
||||
assert_eq!(norm("\\(a\\) costs $5"), "$a$ costs $5");
|
||||
}
|
||||
|
||||
// ── Code is left verbatim ────────────────────────────────────────────
|
||||
// Code is left verbatim
|
||||
|
||||
#[test]
|
||||
fn inline_code_latex_untouched() {
|
||||
@@ -1072,7 +1091,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Math inside tables (the bug) ─────────────────────────────────────
|
||||
// Math inside tables (the bug)
|
||||
|
||||
#[test]
|
||||
fn table_cell_backslash_math_converts() {
|
||||
@@ -1081,7 +1100,7 @@ mod tests {
|
||||
assert_eq!(norm(input), expected);
|
||||
}
|
||||
|
||||
// ── Streaming equivalence (the key invariant) ────────────────────────
|
||||
// Streaming equivalence (the key invariant)
|
||||
|
||||
const RICH_DOC: &str = concat!(
|
||||
"Inline \\(a+b\\), dollar $c+d$, display \\[e=mc^2\\].\n\n",
|
||||
@@ -1146,13 +1165,13 @@ mod tests {
|
||||
" ",
|
||||
"\\\\(escaped\\\\)",
|
||||
"`unterminated \\(x\\)\nafter \\(y\\)",
|
||||
// Padded inline spans exercise the look-ahead + trim hold-back.
|
||||
// `Padded` inline spans exercise the look-ahead + trim hold-back.
|
||||
"\\( x \\)",
|
||||
"a \\( x+y \\) b",
|
||||
"\\( \\alpha + \\beta \\)",
|
||||
"\\( \\{x\\} \\)",
|
||||
"\\( \\) empty",
|
||||
// Unclosed padded open: held back until finish() flushes a lone `$`.
|
||||
// Unclosed `padded` open: held back until finish() flushes a lone `$`.
|
||||
"unclosed padded \\( x + y",
|
||||
// Display spans exercise the close-scan hold-back and its aborts.
|
||||
"$$\nx\n=\ny\n$$",
|
||||
@@ -1172,7 +1191,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── finish() flushes held-back partials literally ────────────────────
|
||||
// finish() flushes held-back partials literally
|
||||
|
||||
#[test]
|
||||
fn finish_flushes_partial_backslash() {
|
||||
|
||||
@@ -129,7 +129,7 @@ pub fn render_markdown_ratatui_with_buffers_width(
|
||||
/// still-open fenced code block: only the streaming tail re-render passes
|
||||
/// `Some(cache)`; `finish()` and non-streaming callers pass `None`. Everything
|
||||
/// other than that one open block (closed code blocks, HTML, math, tables,
|
||||
/// inline) always goes through the unchanged batch highlighter, so output is
|
||||
/// inline) always goes through the `unchanged` batch highlighter, so output is
|
||||
/// byte-for-byte identical to the cache-less path. See [`open_code_highlighter`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn render_markdown_ratatui_with_link_id(
|
||||
|
||||
@@ -366,7 +366,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── highlight_closed (closed-fence memo) ─────────────────────────
|
||||
// highlight_closed (closed-fence memo)
|
||||
|
||||
#[test]
|
||||
fn closed_memo_matches_batch_and_is_idempotent() {
|
||||
|
||||
@@ -262,8 +262,10 @@ pub(crate) fn cell_word_separator<'a>(
|
||||
{
|
||||
let mut in_whitespace = false;
|
||||
let mut after_break_char = false;
|
||||
let mut prev_is_digit = false; // was the *previous* char a digit?
|
||||
let mut digit_before_break = false; // was the char before the break char a digit?
|
||||
// was the *previous* char a digit?
|
||||
let mut prev_is_digit = false;
|
||||
// was the char before the break char a digit?
|
||||
let mut digit_before_break = false;
|
||||
let mut last_break_ch: char = '\0';
|
||||
let mut break_char_start: usize = 0;
|
||||
for (idx, ch) in line.char_indices() {
|
||||
@@ -740,7 +742,7 @@ impl<'a, 'b, 'syn, 'oc> MarkdownParser<'a, 'b, 'syn, 'oc> {
|
||||
Event::Html(_) => {
|
||||
// Render HTML block content as regular text (not code).
|
||||
// pulldown-cmark treats XML-like tags (e.g. <example>) as HTML
|
||||
// blocks, which previously got code-block styling via Replace.
|
||||
// blocks, which would otherwise get code-block styling via Replace.
|
||||
self.push_highlight(Some(self.ms.text), &range);
|
||||
}
|
||||
Event::InlineHtml(html) => {
|
||||
@@ -1115,7 +1117,7 @@ impl<'a, 'b, 'syn, 'oc> MarkdownParser<'a, 'b, 'syn, 'oc> {
|
||||
}
|
||||
}
|
||||
|
||||
// We intentionally use allow_outside=true here (instead of the previous
|
||||
// Use allow_outside=true here (instead of the alternate
|
||||
// pointer-based allow_outside=false) and then do an rfind on the prefix
|
||||
// before the (last) dest_url occurrence. This is required because dest_url
|
||||
// may be a CowStr::Owned (after percent-decoding or HTML entity expansion)
|
||||
@@ -1224,7 +1226,8 @@ impl<'a, 'b, 'syn, 'oc> MarkdownParser<'a, 'b, 'syn, 'oc> {
|
||||
}
|
||||
None
|
||||
}
|
||||
TagEnd::Strikethrough => None, // No highlight pushed
|
||||
// No highlight pushed
|
||||
TagEnd::Strikethrough => None,
|
||||
TagEnd::CodeBlock => {
|
||||
// pulldown synthesizes a block end at end-of-input even for an
|
||||
// unterminated fence, so the end event alone does not prove
|
||||
@@ -1591,7 +1594,8 @@ impl<'a, 'b, 'syn, 'oc> MarkdownParser<'a, 'b, 'syn, 'oc> {
|
||||
// = 1 + sum(col_width) + num_cols * 2 * padding + (num_cols - 1) + 1
|
||||
// = num_cols * (2 * padding + 1) + sum(col_width) + 2 - 1
|
||||
if let Some(max_width) = self.max_table_width {
|
||||
let overhead = num_cols * (2 * padding + 1) + 1; // borders + padding
|
||||
// borders + padding
|
||||
let overhead = num_cols * (2 * padding + 1) + 1;
|
||||
let content_budget = max_width.saturating_sub(overhead);
|
||||
let total_content: usize = col_widths.iter().sum();
|
||||
|
||||
@@ -1740,7 +1744,8 @@ impl<'a, 'b, 'syn, 'oc> MarkdownParser<'a, 'b, 'syn, 'oc> {
|
||||
|
||||
// Body rows
|
||||
for (i, row) in state.rows.iter().enumerate() {
|
||||
let row_offset = separator_offset + 1 + i; // offset 2, 3, ...
|
||||
// offset 2, 3, ...
|
||||
let row_offset = separator_offset + 1 + i;
|
||||
|
||||
let (row_plains, row_styleds, row_links) = self.format_styled_content_lines(
|
||||
row,
|
||||
|
||||
@@ -1546,7 +1546,8 @@ mod tests {
|
||||
true,
|
||||
&mut buffers,
|
||||
None,
|
||||
Some(30), // narrow enough to force wrapping in column B
|
||||
// narrow enough to force wrapping in column B
|
||||
Some(30),
|
||||
);
|
||||
|
||||
// Find the lines that contain "abc" — they should have a styled span
|
||||
@@ -1587,7 +1588,8 @@ mod tests {
|
||||
true,
|
||||
&mut buffers,
|
||||
None,
|
||||
Some(20), // narrow enough to force wrapping around the em-dash
|
||||
// narrow enough to force wrapping around the em-dash
|
||||
Some(20),
|
||||
);
|
||||
let text = lines_to_text(&output.lines);
|
||||
let all_text: String = text.join("");
|
||||
@@ -1628,7 +1630,8 @@ mod tests {
|
||||
let md = "| A | B |\n|---|---|\n| x | y |\n| w | z |\n\n";
|
||||
|
||||
let table_start_line = 0usize;
|
||||
let table_source_lines = 4usize; // header + separator + 2 rows
|
||||
// header + separator + 2 rows
|
||||
let table_source_lines = 4usize;
|
||||
|
||||
let (output, _) = render_markdown_ratatui_full(md, test_style::STYLE, true, None);
|
||||
|
||||
@@ -1694,7 +1697,8 @@ mod tests {
|
||||
Some(30),
|
||||
);
|
||||
|
||||
let table_source_lines = 3; // header + separator + 1 row
|
||||
// header + separator + 1 row
|
||||
let table_source_lines = 3;
|
||||
for (i, &src_line) in output.line_source_map.iter().enumerate() {
|
||||
assert!(
|
||||
src_line < table_source_lines,
|
||||
@@ -2428,7 +2432,7 @@ mod math_tests {
|
||||
|
||||
#[test]
|
||||
fn paren_inline_math_in_table_cell_renders_unicode() {
|
||||
// `\(…\)` inside a table cell must convert. Previously the
|
||||
// `\(…\)` inside a table cell must convert. Historically the
|
||||
// backslash-form scanner was disabled inside tables, leaving raw TeX.
|
||||
// Normalization rewrites `\(…\)` → `$…$` before parsing, so the existing
|
||||
// in-cell `$` path converts it.
|
||||
|
||||
@@ -105,37 +105,6 @@ impl SourceMap {
|
||||
}
|
||||
}
|
||||
|
||||
// ## Restoring Byte-Level Source Maps (if ever needed)
|
||||
// Ratatui path tracks line-level mapping only (`line_source_map`) for
|
||||
// copy/selection. Byte-level `SourceMap` is unused here (~6% faster).
|
||||
//
|
||||
// The ratatui rendering path currently only tracks line-level source mapping
|
||||
// (`line_source_map`), which is sufficient for copy/selection operations.
|
||||
// Byte-level `SourceMap` was removed for simplicity and ~6% speedup.
|
||||
//
|
||||
// To restore byte-level source maps:
|
||||
//
|
||||
// 1. Add field to MarkdownRenderOutput and MarkdownRenderView:
|
||||
// ```
|
||||
// pub source_map: SourceMap,
|
||||
// ```
|
||||
//
|
||||
// 2. In render_ratatui(), add tracking variables:
|
||||
// ```
|
||||
// let mut source_map = SourceMap::new();
|
||||
// let mut rendered_offset: usize = 0;
|
||||
// ```
|
||||
//
|
||||
// 3. For each text segment emitted, record the mapping:
|
||||
// ```
|
||||
// source_map.add(rendered_offset, source_start..source_end);
|
||||
// rendered_offset += emitted_text.len();
|
||||
// ```
|
||||
//
|
||||
// 4. In streaming.rs, update FrozenState to track:
|
||||
// ```
|
||||
// source_map_len: usize,
|
||||
// rendered_bytes: usize,
|
||||
// ```
|
||||
//
|
||||
// 5. Use SourceMap::extend_with_offsets() to merge tail source maps.
|
||||
//
|
||||
// See git history for the removed implementation.
|
||||
|
||||
@@ -1899,7 +1899,8 @@ The frozen lines are **never re-rendered**, making streaming O(N) instead of O(N
|
||||
let first_splits: Vec<usize> = if first_half.len() > 1 {
|
||||
vec![first_half.len() / 2]
|
||||
} else {
|
||||
vec![first_half.len()] // No split, use whole thing
|
||||
// No split, use whole thing
|
||||
vec![first_half.len()]
|
||||
};
|
||||
|
||||
// Split second half (if possible)
|
||||
@@ -1927,7 +1928,8 @@ The frozen lines are **never re-rendered**, making streaming O(N) instead of O(N
|
||||
.collect();
|
||||
|
||||
if chunks.len() < 2 {
|
||||
continue; // Need at least 2 chunks
|
||||
// Need at least 2 chunks
|
||||
continue;
|
||||
}
|
||||
|
||||
tested += 1;
|
||||
@@ -2482,9 +2484,7 @@ The frozen lines are **never re-rendered**, making streaming O(N) instead of O(N
|
||||
assert_streaming_matches_full_both(text);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Syntect-enabled streaming equivalence (incremental open-code highlighter)
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/// Build a nested YAML body of at least `num_lines` lines (no fences).
|
||||
fn yaml_body(num_lines: usize) -> String {
|
||||
@@ -2866,7 +2866,8 @@ The frozen lines are **never re-rendered**, making streaming O(N) instead of O(N
|
||||
#[test]
|
||||
fn clone_preserves_held_back_pending() {
|
||||
let mut r = StreamingMarkdownRenderer::new(test_style::STYLE, true);
|
||||
r.push_and_render("ab \\(\\alpha\\) cd \\", None); // trailing `\` held back
|
||||
// trailing `\` held back
|
||||
r.push_and_render("ab \\(\\alpha\\) cd \\", None);
|
||||
let mut cloned = r.clone();
|
||||
r.push_and_render("(\\beta\\) ef\n\n", None);
|
||||
cloned.push_and_render("(\\beta\\) ef\n\n", None);
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
//! Markdown styling types.
|
||||
//!
|
||||
//! This module provides the `MarkdownStyle` struct which defines colors and
|
||||
//! effects for all markdown elements.
|
||||
|
||||
use anstyle::{Effects, Style};
|
||||
|
||||
use crate::colors::adapt_style;
|
||||
|
||||
/// Table border characters for rendering tables in pretty mode.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TableBorders {
|
||||
chars: [char; 11],
|
||||
@@ -42,7 +38,8 @@ impl TableBorders {
|
||||
Self { chars }
|
||||
}
|
||||
|
||||
// Short names (used in table formatting)
|
||||
// Every character is reachable under two names: a terse one for dense
|
||||
// table-formatting expressions and a spelled-out one for everything else.
|
||||
pub const fn h(&self) -> char {
|
||||
self.chars[Self::H]
|
||||
}
|
||||
@@ -77,7 +74,6 @@ impl TableBorders {
|
||||
self.chars[Self::X]
|
||||
}
|
||||
|
||||
// Long names (for readability)
|
||||
pub const fn horizontal(&self) -> char {
|
||||
self.chars[Self::H]
|
||||
}
|
||||
@@ -121,9 +117,8 @@ impl Default for TableBorders {
|
||||
|
||||
/// Style configuration for markdown rendering.
|
||||
///
|
||||
/// Each field controls the styling for a specific markdown element.
|
||||
/// The `_inner` variants are applied to the content, while `_outer` variants
|
||||
/// are applied to the syntax markers (which are hidden in pretty mode).
|
||||
/// `_inner` variants style an element's content; `_outer` variants style its
|
||||
/// syntax markers, which pretty mode hides.
|
||||
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct MarkdownStyle {
|
||||
pub heading_inner: [Style; 6],
|
||||
@@ -161,9 +156,8 @@ pub struct MarkdownStyle {
|
||||
}
|
||||
|
||||
impl MarkdownStyle {
|
||||
/// Adapt all styles for the terminal's color capabilities.
|
||||
///
|
||||
/// This downgrades RGB colors to 256-color or 16-color as needed.
|
||||
/// Downgrade every style's RGB colors to 256-color or 16-color to match
|
||||
/// the terminal's capabilities.
|
||||
pub fn adapt(self) -> Self {
|
||||
Self {
|
||||
heading_inner: [
|
||||
@@ -210,8 +204,8 @@ impl MarkdownStyle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if ALL active styles have HIDDEN effect.
|
||||
/// Used in pretty mode to determine if text should be skipped.
|
||||
/// Pretty mode skips a span when every style covering it is HIDDEN. An empty
|
||||
/// style set is not hidden.
|
||||
pub(crate) fn all_hidden(styles: impl IntoIterator<Item = Option<Style>>) -> bool {
|
||||
let mut has_any = false;
|
||||
let mut all_are_hidden = true;
|
||||
@@ -230,11 +224,15 @@ pub(crate) fn all_hidden(styles: impl IntoIterator<Item = Option<Style>>) -> boo
|
||||
}
|
||||
|
||||
/// Merge multiple styles into one for rendering.
|
||||
/// Strips HIDDEN from final output - it's a semantic marker, not a visual style.
|
||||
///
|
||||
/// HIDDEN is a semantic marker rather than a visual effect, so it never
|
||||
/// reaches the output: a style that raised it contributes nothing at all, and
|
||||
/// a trailing HIDDEN is dropped from the result.
|
||||
pub(crate) fn merge_styles(styles: impl IntoIterator<Item = Option<Style>>) -> Style {
|
||||
let mut out = Style::new();
|
||||
let mut prev = Style::new();
|
||||
for style in styles {
|
||||
// Rewind past a style that raised HIDDEN before folding in the next one.
|
||||
if out.get_effects().contains(Effects::HIDDEN) {
|
||||
out = prev;
|
||||
} else {
|
||||
@@ -265,13 +263,12 @@ pub(crate) fn merge_styles(styles: impl IntoIterator<Item = Option<Style>>) -> S
|
||||
out.effects(out.get_effects().remove(Effects::HIDDEN))
|
||||
}
|
||||
|
||||
// Simple default style for testing (no colors, just effects)
|
||||
#[cfg(any(test, fuzzing))]
|
||||
pub mod test_style {
|
||||
use super::MarkdownStyle;
|
||||
use anstyle::Style;
|
||||
|
||||
/// A minimal style for testing with no colors.
|
||||
/// Effects only, no colors, so assertions compare stable escape sequences.
|
||||
pub const STYLE: MarkdownStyle = MarkdownStyle {
|
||||
heading_inner: [Style::new().bold(); 6],
|
||||
heading_outer: [Style::new().dimmed().hidden(); 6],
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
//! Syntax highlighting support using syntect.
|
||||
//!
|
||||
//! This module provides the `Syntect` struct which holds the syntax definitions
|
||||
//! and theme for code block highlighting.
|
||||
|
||||
use std::io::Cursor;
|
||||
use std::path::Path;
|
||||
@@ -14,20 +11,16 @@ use syntect::{
|
||||
|
||||
/// Syntax highlighting configuration.
|
||||
///
|
||||
/// Holds the theme and syntax definitions for code highlighting.
|
||||
/// Create one instance and pass it to the markdown renderer.
|
||||
/// Loading the syntax set is expensive; create one instance and pass it to
|
||||
/// the markdown renderer.
|
||||
pub struct Syntect {
|
||||
/// The color theme for syntax highlighting.
|
||||
pub theme: SyntectTheme,
|
||||
/// The syntax definitions (supports 250+ languages via two-face).
|
||||
pub syntax_set: SyntaxSet,
|
||||
}
|
||||
|
||||
impl Syntect {
|
||||
/// Create a new Syntect instance from theme bytes.
|
||||
///
|
||||
/// The theme bytes should be a TextMate `.tmTheme` file.
|
||||
/// Uses two-face's extended syntax set with 250+ languages.
|
||||
/// `theme_bytes` must be a TextMate `.tmTheme` file. The syntax set is
|
||||
/// two-face's extended one, covering the 250+ languages bat ships.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
@@ -37,12 +30,11 @@ impl Syntect {
|
||||
pub fn new(theme_bytes: &[u8]) -> Self {
|
||||
let mut cursor = Cursor::new(theme_bytes);
|
||||
let theme = ThemeSet::load_from_reader(&mut cursor).expect("Failed to load theme");
|
||||
// Use two-face's extended syntax set which includes 250+ languages from bat
|
||||
let syntax_set = two_face::syntax::extra_newlines();
|
||||
Self { theme, syntax_set }
|
||||
}
|
||||
|
||||
/// Find a syntax definition by file path extension.
|
||||
/// Resolves on the extension alone; the rest of the file name is ignored.
|
||||
pub fn find_syntax_by_file_path(&self, file_path: &Path) -> Option<&SyntaxReference> {
|
||||
let ext = file_path.extension()?.to_str()?;
|
||||
self.syntax_set.find_syntax_by_extension(ext)
|
||||
@@ -53,7 +45,6 @@ impl Syntect {
|
||||
self.syntax_set.find_syntax_by_token(token)
|
||||
}
|
||||
|
||||
/// Create a highlighter for the given file path.
|
||||
pub fn highlight_lines_by_file_path(&self, file_path: &Path) -> Option<HighlightLines<'_>> {
|
||||
Some(HighlightLines::new(
|
||||
self.find_syntax_by_file_path(file_path)?,
|
||||
@@ -61,7 +52,6 @@ impl Syntect {
|
||||
))
|
||||
}
|
||||
|
||||
/// Create a highlighter for the given language token.
|
||||
pub fn highlight_lines_for_token(&self, token: &str) -> Option<HighlightLines<'_>> {
|
||||
Some(HighlightLines::new(
|
||||
self.find_syntax_by_token(token)?,
|
||||
@@ -78,7 +68,7 @@ impl Syntect {
|
||||
/// If the string matches the citation form but no syntax is found for the
|
||||
/// path, this falls back to [`Syntect::find_syntax_by_token`] with the full
|
||||
/// `fence_info` string, so plain ` ```lang` blocks keep working and odd
|
||||
/// citations degrade like the pre-citation code path.
|
||||
/// citations degrade to a plain token lookup.
|
||||
pub fn highlight_lines_for_fence_info(&self, fence_info: &str) -> Option<HighlightLines<'_>> {
|
||||
Some(HighlightLines::new(
|
||||
self.find_syntax_for_fence_info(fence_info)?,
|
||||
@@ -159,10 +149,8 @@ pub(crate) fn syntax_highlight_raw(
|
||||
Some(lines)
|
||||
}
|
||||
|
||||
/// Get a shared Syntect instance for tests.
|
||||
///
|
||||
/// This loads the tokyo-night theme bundled with the crate.
|
||||
/// Uses a static OnceLock for efficiency in test runs.
|
||||
/// Shared Syntect instance for tests, using the crate's bundled tokyo-night
|
||||
/// theme.
|
||||
#[cfg(any(test, fuzzing))]
|
||||
#[allow(dead_code)]
|
||||
pub fn test_syntect() -> &'static Syntect {
|
||||
|
||||
Reference in New Issue
Block a user