feat: add custom math renderer support#9
Conversation
📝 WalkthroughWalkthroughAdds first-class math-block detection and rendering: a new optional Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant API as renderToHtml/Ansi
participant C as C Renderer
participant Meta as parseXxxMeta
participant High as parseXxxWithHighlighting
User->>API: markdown + {highlighter?, math?}
API->>C: renderMetaBytes(bytes)
C-->>API: raw bytes + metadata
alt highlighter or math provided
API->>Meta: parseXxxMeta(bytes)
Meta-->>API: {output, codeBlocks, mathBlocks}
API->>High: parseXxxWithHighlighting(bytes, highlighter, math)
High->>High: merge & sort codeBlocks + mathBlocks by start
loop per block (by position)
alt code block
High->>High: apply highlighter -> replacement
else math block
High->>High: call math callback -> replacement
end
end
High-->>API: highlighted output
end
API-->>User: final rendered output
sequenceDiagram
participant C as C Renderer
participant Span as Span Callbacks
participant Meta as math_blocks array
C->>Span: enterSpan(LATEXMATH or DISPLAY)
Span->>Meta: math_meta_push(start, display)
Meta->>Meta: allocate/resize if needed
Span->>C: set in_math_span = true
C->>Span: ...emit math content...
C->>Span: leaveSpan(LATEXMATH/DISPLAY)
Span->>Meta: record end offset, set display flag
Span->>C: set in_math_span = false
C->>C: emit JSON meta: { c: [...], m: [...] }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
packages/md4x/lib/_shared.mjs (1)
107-175: ANSI math rendering handles escape sequences correctly.The implementation accounts for the different offset semantics between code blocks (which include DIM escapes in their range) and math blocks (which exclude YELLOW escapes from their range). The wrapper stripping logic at lines 127-128 is a safe no-op for math blocks since their recorded offsets already exclude the color escapes.
One minor observation: Lines 127-128 perform escape stripping that's only applicable to code blocks. For math blocks, these checks will always be false since the C renderer records math offsets after the YELLOW escape and before the DEFAULT escape. This is correct behavior but could benefit from a brief comment.
📝 Consider adding a clarifying comment
let escapeCode = block.type === 'code' ? DIM : "\x1b[33m"; // ANSI_COLOR_YELLOW let escapeOffCode = block.type === 'code' ? DIM_OFF : "\x1b[39m"; // ANSI_COLOR_DEFAULT - // Sometimes the prefix might be mixed, we gently remove them if they match what we expect. - // md4x-ansi puts ANSI_COLOR_YELLOW before the span contents. + // Code blocks include DIM/DIM_OFF in their recorded offsets, so we strip them. + // Math blocks exclude YELLOW/DEFAULT from their offsets, so these are no-ops for math. if (inner.startsWith(escapeCode)) inner = inner.slice(escapeCode.length);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/md4x/lib/_shared.mjs` around lines 107 - 175, The strip-of-ANSI logic in parseAnsiWithHighlighting currently checks and removes escapeCode/escapeOffCode (variables escapeCode and escapeOffCode applied to inner) even though that only applies to code blocks; add a short clarifying comment above the two if-checks that explains codeBlocks include DIM escapes in their recorded ranges while mathBlocks have offsets recorded after the YELLOW escape (so the starts/ends won't match and the checks are no-ops for math), referencing the function parseAnsiWithHighlighting and the variables inner, escapeCode, escapeOffCode, codeBlocks and mathBlocks to make intent clear for future readers.src/renderers/md4x-ansi.c (1)
495-509: Minor: trailing whitespace at Line 503.The JSON emission logic for math blocks is correct. However, there's trailing whitespace at line 503 that should be removed.
🧹 Remove trailing whitespace
n = snprintf(buf, sizeof(buf), "{\"s\":%u,\"e\":%u", (unsigned)m->start, (unsigned)m->end); out(buf, (MD_SIZE)n, ud); - + if(m->display) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderers/md4x-ansi.c` around lines 495 - 509, Remove the trailing whitespace at the end of the line that emits the display flag for math blocks (the out call that writes ",\"d\":1" in the loop over r->math_blocks / MD_ANSI_MATH_META). Edit the source so there is no extra space or invisible character after the string literal (out(",\"d\":1", 6, ud);) while keeping the call and arguments identical otherwise so JSON output and behavior are unchanged.src/renderers/md4x-html.c (1)
842-857: Minor: trailing whitespace at Line 850.The JSON emission for math blocks is correct. Minor trailing whitespace at line 850.
🧹 Remove trailing whitespace
n = snprintf(buf, sizeof(buf), "{\"s\":%u,\"e\":%u", (unsigned)m->start, (unsigned)m->end); out(buf, (MD_SIZE)n, ud); - + if(m->display) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderers/md4x-html.c` around lines 842 - 857, There is a stray trailing space emitted in the math-block JSON output; locate the math-block emission loop (references: r->n_math_blocks, r->math_blocks, MD_HTML_MATH_META, and the out(...) calls) and remove the accidental trailing whitespace from the string literal being written (the out call emitting the display flag or surrounding punctuation) so the JSON tokens have no extra spaces.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@packages/md4x/lib/_shared.mjs`:
- Around line 107-175: The strip-of-ANSI logic in parseAnsiWithHighlighting
currently checks and removes escapeCode/escapeOffCode (variables escapeCode and
escapeOffCode applied to inner) even though that only applies to code blocks;
add a short clarifying comment above the two if-checks that explains codeBlocks
include DIM escapes in their recorded ranges while mathBlocks have offsets
recorded after the YELLOW escape (so the starts/ends won't match and the checks
are no-ops for math), referencing the function parseAnsiWithHighlighting and the
variables inner, escapeCode, escapeOffCode, codeBlocks and mathBlocks to make
intent clear for future readers.
In `@src/renderers/md4x-ansi.c`:
- Around line 495-509: Remove the trailing whitespace at the end of the line
that emits the display flag for math blocks (the out call that writes ",\"d\":1"
in the loop over r->math_blocks / MD_ANSI_MATH_META). Edit the source so there
is no extra space or invisible character after the string literal
(out(",\"d\":1", 6, ud);) while keeping the call and arguments identical
otherwise so JSON output and behavior are unchanged.
In `@src/renderers/md4x-html.c`:
- Around line 842-857: There is a stray trailing space emitted in the math-block
JSON output; locate the math-block emission loop (references: r->n_math_blocks,
r->math_blocks, MD_HTML_MATH_META, and the out(...) calls) and remove the
accidental trailing whitespace from the string literal being written (the out
call emitting the display flag or surrounding punctuation) so the JSON tokens
have no extra spaces.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1c9c8914-cde4-4ee1-85aa-5cf871f72d15
📒 Files selected for processing (7)
packages/md4x/lib/_shared.mjspackages/md4x/lib/napi.mjspackages/md4x/lib/types.d.mtspackages/md4x/lib/wasm/common.mjspackages/md4x/test/_suite.mjssrc/renderers/md4x-ansi.csrc/renderers/md4x-html.c
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/md4x/lib/wasm/common.mjs (1)
119-132:⚠️ Potential issue | 🟠 MajorThe new ANSI math path still drops
showUrlsandshowFrontmatter.This slow path only preserves
healby preprocessings; the other ANSI flags never reachmd4x_to_ansi_meta(). Enablingmaththerefore flips both options back to their default behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/md4x/lib/wasm/common.mjs` around lines 119 - 132, The slow ANSI path forgets to forward the bitflags (showUrls/showFrontmatter) to the metadata renderer; compute/keep the existing flags variable and pass it into the metadata rendering call so md4x_to_ansi_meta sees the same flags as the fast path (i.e., update the renderMetaBytes invocation used with exports.md4x_to_ansi_meta to accept the flags argument and forward flags through to parse/rendering code handling md4x_to_ansi_meta).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/md4x/lib/wasm/common.mjs`:
- Around line 92-103: The flags (built from opts.full and opts.heal) are ignored
when opts.math or opts.highlighter take the md4x_to_html_meta path; update the
md4x_to_html_meta flow to carry the same flags: pass the flags into
renderMetaBytes (or into the md4x_to_html_meta invocation) so the wasm call
receives them, and ensure parseHtmlWithHighlighting still receives the resulting
bytes; i.e., keep the flags variable created from opts.full/opts.heal and thread
it into the renderMetaBytes call that currently invokes
exports.md4x_to_html_meta so output honors full/heal like the md4x_to_html path.
In `@src/renderers/md4x-ansi.c`:
- Around line 383-397: The function ansi_math_meta_push currently returns NULL
on malloc/realloc failure but does not mark the renderer error state, so callers
continue as if allocation succeeded; modify ansi_math_meta_push to set a
persistent allocation-failure flag on the MD_ANSI struct (e.g., r->alloc_failed
= 1 or a similarly named field you add) before returning NULL whenever malloc or
realloc fails (checks around r->math_blocks and the realloc p == NULL branch),
and ensure callers that build output check this flag and propagate a hard
failure instead of silently omitting the math span.
- Around line 941-962: The math-span enter handlers (MD_SPAN_LATEXMATH and
MD_SPAN_LATEXMATH_DISPLAY) currently unconditionally call ansi_math_meta_push
and set r->in_math_span, allowing duplicate enters to overwrite math_blocks;
modify these branches to first check whether a math span is already active
(r->in_math_span) and reject a second enter (skip push) or track the active span
type (e.g., add r->in_math_span_type) so only matching leaves clear state; only
call ansi_math_meta_push and set meta->start/meta->display and r->in_math_span
when no span is active, and mirror the same defensive checks in the
corresponding leave handlers (also apply the same fix to the similar code around
the MD_SPAN_LATEXMATH/MD_SPAN_LATEXMATH_DISPLAY duplication at the other
location referenced).
In `@src/renderers/md4x-html.c`:
- Around line 1253-1274: The math span enter handlers (cases MD_SPAN_LATEXMATH
and MD_SPAN_LATEXMATH_DISPLAY) currently set r->in_math_span and reuse the same
meta slot on nested or mismatched callbacks; change them to guard transitions by
checking and recording the active math span type instead of a bare boolean: only
push a new MD_HTML_MATH_META via math_meta_push and set
meta->start/meta->display and r->in_math_span (or a new
r->active_math_span_type) when no active math span of any type exists, and
ensure the corresponding leave handlers check that the active span type matches
(use the same type enum used by MD_SPAN_LATEXMATH / MD_SPAN_LATEXMATH_DISPLAY)
before finalizing the meta; apply the same fix pattern to the related blocks
around the other occurrence (lines ~1301-1309).
- Around line 767-782: math_meta_push currently returns NULL on malloc/realloc
failure but callers (e.g., md_html_ex) don't detect it; modify math_meta_push to
set an error flag on the MD_HTML struct (e.g., r->alloc_failed or r->error_alloc
= 1—create the field if missing) before returning NULL so allocation failures
are recorded and can be propagated by md_html_ex, and ensure the function still
returns NULL when allocation fails; also update any struct initialization (where
MD_HTML is created) to initialize this flag to 0 and ensure callers check the
flag or NULL return to abort processing.
---
Outside diff comments:
In `@packages/md4x/lib/wasm/common.mjs`:
- Around line 119-132: The slow ANSI path forgets to forward the bitflags
(showUrls/showFrontmatter) to the metadata renderer; compute/keep the existing
flags variable and pass it into the metadata rendering call so md4x_to_ansi_meta
sees the same flags as the fast path (i.e., update the renderMetaBytes
invocation used with exports.md4x_to_ansi_meta to accept the flags argument and
forward flags through to parse/rendering code handling md4x_to_ansi_meta).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d2867bfc-96c8-4aff-a438-196cc6ede5fe
📒 Files selected for processing (5)
packages/md4x/lib/types.d.mtspackages/md4x/lib/wasm/common.mjspackages/md4x/test/_suite.mjssrc/renderers/md4x-ansi.csrc/renderers/md4x-html.c
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/md4x/test/_suite.mjs
| let flags = opts?.full ? 0x0008 : 0; | ||
| if (opts?.heal) flags |= HEAL_FLAG; | ||
| const exports = _getExports(); | ||
| if (!opts?.highlighter) { | ||
| if (!opts?.highlighter && !opts?.math) { | ||
| return render(exports, exports.md4x_to_html, input, flags); | ||
| } | ||
| const { bytes, outPtr } = renderMetaBytes( | ||
| exports, | ||
| exports.md4x_to_html_meta, | ||
| input, | ||
| ); | ||
| const result = parseHtmlWithHighlighting(bytes, opts.highlighter); | ||
| const result = parseHtmlWithHighlighting(bytes, opts?.highlighter, opts?.math); |
There was a problem hiding this comment.
The new HTML math path still drops full and heal.
Once opts.math routes through md4x_to_html_meta(), the flags built above are no longer used. renderToHtml(..., { full: true, math }) will return fragment output, and heal is skipped too.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/md4x/lib/wasm/common.mjs` around lines 92 - 103, The flags (built
from opts.full and opts.heal) are ignored when opts.math or opts.highlighter
take the md4x_to_html_meta path; update the md4x_to_html_meta flow to carry the
same flags: pass the flags into renderMetaBytes (or into the md4x_to_html_meta
invocation) so the wasm call receives them, and ensure parseHtmlWithHighlighting
still receives the resulting bytes; i.e., keep the flags variable created from
opts.full/opts.heal and thread it into the renderMetaBytes call that currently
invokes exports.md4x_to_html_meta so output honors full/heal like the
md4x_to_html path.
| ansi_math_meta_push(MD_ANSI* r) | ||
| { | ||
| if(r->math_blocks == NULL) { | ||
| r->math_blocks = (MD_ANSI_MATH_META*) malloc(8 * sizeof(MD_ANSI_MATH_META)); | ||
| if(r->math_blocks == NULL) return NULL; | ||
| r->math_blocks_cap = 8; | ||
| } else if(r->n_math_blocks >= r->math_blocks_cap) { | ||
| int new_cap = r->math_blocks_cap * 2; | ||
| MD_ANSI_MATH_META* p = (MD_ANSI_MATH_META*) realloc(r->math_blocks, new_cap * sizeof(MD_ANSI_MATH_META)); | ||
| if(p == NULL) return NULL; | ||
| r->math_blocks = p; | ||
| r->math_blocks_cap = new_cap; | ||
| } | ||
| memset(&r->math_blocks[r->n_math_blocks], 0, sizeof(MD_ANSI_MATH_META)); | ||
| return &r->math_blocks[r->n_math_blocks]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Propagate math-meta allocation failures.
If malloc/realloc fails here, the renderer still returns success and just omits the current math span from m. That leaves the JS replacement path with partial metadata instead of a hard failure.
As per coding guidelines, "Every malloc and realloc call must be checked for NULL; use error flags on growable buffers and propagate failures up".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderers/md4x-ansi.c` around lines 383 - 397, The function
ansi_math_meta_push currently returns NULL on malloc/realloc failure but does
not mark the renderer error state, so callers continue as if allocation
succeeded; modify ansi_math_meta_push to set a persistent allocation-failure
flag on the MD_ANSI struct (e.g., r->alloc_failed = 1 or a similarly named field
you add) before returning NULL whenever malloc or realloc fails (checks around
r->math_blocks and the realloc p == NULL branch), and ensure callers that build
output check this flag and propagate a hard failure instead of silently omitting
the math span.
| case MD_SPAN_LATEXMATH: | ||
| render_ansi(r, ANSI_COLOR_YELLOW); | ||
| if(r->flags & MD_ANSI_FLAG_CODE_META) { | ||
| MD_ANSI_MATH_META* meta = ansi_math_meta_push(r); | ||
| if(meta != NULL) { | ||
| meta->start = r->output_offset; | ||
| meta->display = 0; | ||
| r->in_math_span = 1; | ||
| } | ||
| } | ||
| break; | ||
| case MD_SPAN_LATEXMATH_DISPLAY: | ||
| render_ansi(r, ANSI_COLOR_YELLOW); | ||
| if(r->flags & MD_ANSI_FLAG_CODE_META) { | ||
| MD_ANSI_MATH_META* meta = ansi_math_meta_push(r); | ||
| if(meta != NULL) { | ||
| meta->start = r->output_offset; | ||
| meta->display = 1; | ||
| r->in_math_span = 1; | ||
| } | ||
| } | ||
| break; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Guard math-span state against malformed callback sequences.
in_math_span is only a boolean, so a duplicate math enter rewrites math_blocks[r->n_math_blocks], and any later math leave will still commit that slot. Track the active span type, or at least reject a second enter while one is open.
As per coding guidelines, "Renderers must be defensive against unbalanced enter/leave callbacks from the parser; always guard state transitions with correct type checks".
Also applies to: 1006-1014
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderers/md4x-ansi.c` around lines 941 - 962, The math-span enter
handlers (MD_SPAN_LATEXMATH and MD_SPAN_LATEXMATH_DISPLAY) currently
unconditionally call ansi_math_meta_push and set r->in_math_span, allowing
duplicate enters to overwrite math_blocks; modify these branches to first check
whether a math span is already active (r->in_math_span) and reject a second
enter (skip push) or track the active span type (e.g., add r->in_math_span_type)
so only matching leaves clear state; only call ansi_math_meta_push and set
meta->start/meta->display and r->in_math_span when no span is active, and mirror
the same defensive checks in the corresponding leave handlers (also apply the
same fix to the similar code around the
MD_SPAN_LATEXMATH/MD_SPAN_LATEXMATH_DISPLAY duplication at the other location
referenced).
| static MD_HTML_MATH_META* | ||
| math_meta_push(MD_HTML* r) | ||
| { | ||
| if(r->math_blocks == NULL) { | ||
| r->math_blocks = (MD_HTML_MATH_META*) malloc(8 * sizeof(MD_HTML_MATH_META)); | ||
| if(r->math_blocks == NULL) return NULL; | ||
| r->math_blocks_cap = 8; | ||
| } else if(r->n_math_blocks >= r->math_blocks_cap) { | ||
| int new_cap = r->math_blocks_cap * 2; | ||
| MD_HTML_MATH_META* p = (MD_HTML_MATH_META*) realloc(r->math_blocks, new_cap * sizeof(MD_HTML_MATH_META)); | ||
| if(p == NULL) return NULL; | ||
| r->math_blocks = p; | ||
| r->math_blocks_cap = new_cap; | ||
| } | ||
| memset(&r->math_blocks[r->n_math_blocks], 0, sizeof(MD_HTML_MATH_META)); | ||
| return &r->math_blocks[r->n_math_blocks]; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Propagate math-meta allocation failures here as well.
A failed malloc/realloc currently just drops the span from m while md_html_ex() still succeeds. That makes the HTML replacement path silently incomplete under memory pressure.
As per coding guidelines, "Every malloc and realloc call must be checked for NULL; use error flags on growable buffers and propagate failures up".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderers/md4x-html.c` around lines 767 - 782, math_meta_push currently
returns NULL on malloc/realloc failure but callers (e.g., md_html_ex) don't
detect it; modify math_meta_push to set an error flag on the MD_HTML struct
(e.g., r->alloc_failed or r->error_alloc = 1—create the field if missing) before
returning NULL so allocation failures are recorded and can be propagated by
md_html_ex, and ensure the function still returns NULL when allocation fails;
also update any struct initialization (where MD_HTML is created) to initialize
this flag to 0 and ensure callers check the flag or NULL return to abort
processing.
| case MD_SPAN_LATEXMATH: | ||
| RENDER_VERBATIM(r, "<x-equation>"); | ||
| if(r->flags & MD_HTML_FLAG_CODE_META) { | ||
| MD_HTML_MATH_META* meta = math_meta_push(r); | ||
| if(meta != NULL) { | ||
| meta->start = r->output_offset; | ||
| meta->display = 0; | ||
| r->in_math_span = 1; | ||
| } | ||
| } | ||
| break; | ||
| case MD_SPAN_LATEXMATH_DISPLAY: | ||
| RENDER_VERBATIM(r, "<x-equation type=\"display\">"); | ||
| if(r->flags & MD_HTML_FLAG_CODE_META) { | ||
| MD_HTML_MATH_META* meta = math_meta_push(r); | ||
| if(meta != NULL) { | ||
| meta->start = r->output_offset; | ||
| meta->display = 1; | ||
| r->in_math_span = 1; | ||
| } | ||
| } | ||
| break; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use a stricter math-span state machine.
This has the same overwrite problem as the ANSI renderer: a second math enter before the matching leave reuses the in-progress slot, and a mismatched leave still finalizes it. Guard the transition with the active span type instead of a bare boolean.
As per coding guidelines, "Renderers must be defensive against unbalanced enter/leave callbacks from the parser; always guard state transitions with correct type checks".
Also applies to: 1301-1309
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderers/md4x-html.c` around lines 1253 - 1274, The math span enter
handlers (cases MD_SPAN_LATEXMATH and MD_SPAN_LATEXMATH_DISPLAY) currently set
r->in_math_span and reuse the same meta slot on nested or mismatched callbacks;
change them to guard transitions by checking and recording the active math span
type instead of a bare boolean: only push a new MD_HTML_MATH_META via
math_meta_push and set meta->start/meta->display and r->in_math_span (or a new
r->active_math_span_type) when no active math span of any type exists, and
ensure the corresponding leave handlers check that the active span type matches
(use the same type enum used by MD_SPAN_LATEXMATH / MD_SPAN_LATEXMATH_DISPLAY)
before finalizing the meta; apply the same fix pattern to the related blocks
around the other occurrence (lines ~1301-1309).
Description
This PR introduces support for custom math rendering via a new
mathoption inRenderOptions. This allows users to intercept and replace LaTeX math spans (both inline and display) with any custom string (e.g., KaTeX/MathJax rendered HTML or specific ANSI sequences for terminal).Key Changes
mathcallback definition to the rendering options.Usage Example
Caution
I use AI generated code to create PR.
If you have any concerns, please feel free to ask me.
Resolves #8
Summary by CodeRabbit
New Features
Tests