Skip to content

feat: add custom math renderer support#9

Open
tani wants to merge 2 commits into
unjs:mainfrom
tani:feat/latex-math
Open

feat: add custom math renderer support#9
tani wants to merge 2 commits into
unjs:mainfrom
tani:feat/latex-math

Conversation

@tani

@tani tani commented Mar 9, 2026

Copy link
Copy Markdown

Description

This PR introduces support for custom math rendering via a new math option in RenderOptions. 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

  • Backend:
    • Captured start/end offsets and the display flag for math spans.
    • Updated the metadata structure to include a dedicated field for math blocks.
  • Frontend:
    • Added math callback definition to the rendering options.
    • Updated the parsing logic to handle sequential replacement of both code and math blocks.
    • Integrated the new option across all build targets.
  • Tests:
    • Added tests verifying:
      • Inline math replacement.
      • Display math replacement (with whitespace handling).
      • Sequential replacement of multiple math spans.
      • Proper interleaving between math blocks and fenced code blocks.

Usage Example

const html = await renderToHtml("This is $E=mc^2$", {
  math: (code, display) => {
    return display 
      ? `<div class="math-display">${render(code)}</div>` 
      : `<span class="math-inline">${render(code)}</span>`;
  }
});

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

    • Added support for rendering mathematical expressions (inline and display) in both HTML and ANSI outputs.
    • Introduced an optional custom math renderer for tailored math rendering.
    • Math blocks are now detected, tracked, and processed alongside code blocks so highlighting and rendering preserve original order.
  • Tests

    • Added coverage for math rendering, fallbacks, and interleaving of math and code blocks.

@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds first-class math-block detection and rendering: a new optional math RenderOptions callback, extraction of math metadata alongside code blocks, plumbing through parsing/highlighting paths, C renderer metadata structures for HTML/ANSI, and tests covering math rendering and interleaving with code.

Changes

Cohort / File(s) Summary
Shared JS parsing & highlighting
packages/md4x/lib/_shared.mjs
parseCodeMeta/parseHtmlMeta/parseAnsiMeta now return mathBlocks alongside code blocks. parseHtmlWithHighlighting and parseAnsiWithHighlighting accept a math callback, merge/sort code+math blocks, and apply math rendering during highlighting. Early-return guards updated.
Node/WASM bindings
packages/md4x/lib/napi.mjs, packages/md4x/lib/wasm/common.mjs
renderToHtml/renderToAnsi now short-circuit only when neither highlighter nor math callback present; calls to parseXxxWithHighlighting pass the new math argument.
Type definitions
packages/md4x/lib/types.d.mts
Added `math?: (math: string, display: boolean) => string
Tests
packages/md4x/test/_suite.mjs
Added tests verifying inline/display math rendering, fallback when math returns undefined, multiple math blocks, and interleaving math with code for both HTML and ANSI outputs.
ANSI renderer (C)
src/renderers/md4x-ansi.c
Added MD_ANSI_MATH_META and math-tracking fields to MD_ANSI; capture math spans (start/end/display) during render, emit m array in code-meta JSON, and cleanup math buffers.
HTML renderer (C)
src/renderers/md4x-html.c, md4x-html.h
Added MD_HTML_MATH_META and math-tracking fields to MD_HTML; push/finalize math spans in span callbacks, include m array in emitted JSON, and free math metadata on cleanup.

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
Loading
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: [...] }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰✨ I nibble through LaTeX vines,

math and code in tidy lines,
Inline hops and displays grand,
callbacks render on demand,
Together we make output fine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add custom math renderer support' clearly and concisely describes the main feature addition introduced in the changeset.
Linked Issues check ✅ Passed All code requirements from issue #8 are met: a math callback option is added to RenderOptions [types.d.mts], integrated into rendering paths [napi.mjs, common.mjs], and inline/display math is captured and passed to the callback [md4x-html.c, md4x-ansi.c, _shared.mjs].
Out of Scope Changes check ✅ Passed All changes directly support the math renderer feature: metadata structures capture math spans, parsing functions extract math blocks, and tests cover the new math callback functionality with no unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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

📥 Commits

Reviewing files that changed from the base of the PR and between df601b7 and 263a51c.

📒 Files selected for processing (7)
  • packages/md4x/lib/_shared.mjs
  • packages/md4x/lib/napi.mjs
  • packages/md4x/lib/types.d.mts
  • packages/md4x/lib/wasm/common.mjs
  • packages/md4x/test/_suite.mjs
  • src/renderers/md4x-ansi.c
  • src/renderers/md4x-html.c

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

The new ANSI math path still drops showUrls and showFrontmatter.

This slow path only preserves heal by preprocessing s; the other ANSI flags never reach md4x_to_ansi_meta(). Enabling math therefore 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

📥 Commits

Reviewing files that changed from the base of the PR and between 263a51c and 19d6262.

📒 Files selected for processing (5)
  • packages/md4x/lib/types.d.mts
  • packages/md4x/lib/wasm/common.mjs
  • packages/md4x/test/_suite.mjs
  • src/renderers/md4x-ansi.c
  • src/renderers/md4x-html.c
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/md4x/test/_suite.mjs

Comment on lines 92 to +103
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/renderers/md4x-ansi.c
Comment on lines +383 to +397
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];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread src/renderers/md4x-ansi.c
Comment on lines +941 to +962
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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).

Comment thread src/renderers/md4x-html.c
Comment on lines +767 to +782
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];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment thread src/renderers/md4x-html.c
Comment on lines +1253 to +1274
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

math option in RenderOptions

1 participant