Summary
An italic span that contains a bold word, like *a **b** c*, does not render as nested emphasis. Instead of one <em> wrapping a <strong>, the parser emits three separate <em> runs and drops the strong entirely. Underscore emphasis is worse: _italic __bold__ tail_ produces mangled, partially unparsed output.
This is plain prose (an italic sentence with one bold word), not a pathological delimiter case. The syntax profile doc lists "Emphasis and strong: Natural prose patterns" as supported, and the reverse nesting (**a *b* c**, bold containing italic) already works, so this looks like an oversight rather than an intentional limitation.
Environment
@tanstack/markdown 0.0.12, main
- Entry point:
@tanstack/markdown/html (renderHtml)
- Node v22.22.3
- Reproduces at the parser level, so it's renderer-independent (react/octane affected too).
Steps to reproduce
import { renderHtml } from '@tanstack/markdown/html'
console.log(renderHtml('*a **b** c*'))
console.log(renderHtml('*see **the docs** here*'))
console.log(renderHtml('_italic __bold__ tail_'))
console.log(renderHtml('*outer **inner***'))
Expected
<p><em>a <strong>b</strong> c</em></p>
<p><em>see <strong>the docs</strong> here</em></p>
<p><em>italic <strong>bold</strong> tail</em></p>
<p><em>outer <strong>inner</strong></em></p>
Actual
<p><em>a </em><em>b</em><em> c</em></p>
<p><em>see </em><em>the docs</em><em> here</em></p>
<p><em>italic __bold</em>_ tail_</p>
<p><em>outer </em><em>inner</em>**</p>
For comparison, the bold-outer form works today:
renderHtml('**a *b* c**') // <p><strong>a <em>b</em> c</strong></p> ✅
renderHtml('***important***') // <p><em><strong>important</strong></em></p> ✅
Root cause
In src/inline.ts, single-character emphasis scans for the next matching delimiter character:
// src/inline.ts (single * / _ branch)
if (char === '*' || char === '_') {
const close = char === '_' && !canUseUnderscore(...) ? -1 : findDelimiter(value, index + 1, char, budget)
...
}
findDelimiter returns the first unescaped occurrence of the delimiter string. For *a **b** c*, the opening * at index 0 calls findDelimiter(value, 1, '*'), which returns the first * of the inner **b**. Emphasis closes right before the bold marker, and the leftover *b** c* gets re-scanned into more stray <em> runs.
The single-* search doesn't skip a * that is part of a longer run opening a nested strong span. It matches the nearest character greedily. The **/__ branch avoids this because findDelimiter looks for the two-character string and skips lone *, which is exactly why bold-containing-italic works while italic-containing-bold does not. That asymmetry is the tell. The underscore case additionally trips the canUseUnderscore flanking checks, giving the extra-broken <em>italic __bold</em>_ tail_.
Suspected files / functions
src/inline.ts — parseInlineRaw single-emphasis branch (*/_) and findDelimiter
Suggested fix (optional)
When scanning for a single-* closer, don't accept a * that is the start of a longer run meant to open a nested span. In findDelimiter, for a single */_ delimiter, skip positions where the character is immediately adjacent to the same character (part of a **/*** run) unless that run is a valid closer for the current open delimiter. A fuller fix pairs delimiters by run length the way CommonMark's delimiter stack does. Regression fixtures like *a **b** c* and _italic __bold__ tail_ would lock it down.
Summary
An italic span that contains a bold word, like
*a **b** c*, does not render as nested emphasis. Instead of one<em>wrapping a<strong>, the parser emits three separate<em>runs and drops the strong entirely. Underscore emphasis is worse:_italic __bold__ tail_produces mangled, partially unparsed output.This is plain prose (an italic sentence with one bold word), not a pathological delimiter case. The syntax profile doc lists "Emphasis and strong: Natural prose patterns" as supported, and the reverse nesting (
**a *b* c**, bold containing italic) already works, so this looks like an oversight rather than an intentional limitation.Environment
@tanstack/markdown0.0.12,main@tanstack/markdown/html(renderHtml)Steps to reproduce
Expected
Actual
For comparison, the bold-outer form works today:
Root cause
In
src/inline.ts, single-character emphasis scans for the next matching delimiter character:findDelimiterreturns the first unescaped occurrence of the delimiter string. For*a **b** c*, the opening*at index 0 callsfindDelimiter(value, 1, '*'), which returns the first*of the inner**b**. Emphasis closes right before the bold marker, and the leftover*b** c*gets re-scanned into more stray<em>runs.The single-
*search doesn't skip a*that is part of a longer run opening a nested strong span. It matches the nearest character greedily. The**/__branch avoids this becausefindDelimiterlooks for the two-character string and skips lone*, which is exactly why bold-containing-italic works while italic-containing-bold does not. That asymmetry is the tell. The underscore case additionally trips thecanUseUnderscoreflanking checks, giving the extra-broken<em>italic __bold</em>_ tail_.Suspected files / functions
src/inline.ts—parseInlineRawsingle-emphasis branch (*/_) andfindDelimiterSuggested fix (optional)
When scanning for a single-
*closer, don't accept a*that is the start of a longer run meant to open a nested span. InfindDelimiter, for a single*/_delimiter, skip positions where the character is immediately adjacent to the same character (part of a**/***run) unless that run is a valid closer for the current open delimiter. A fuller fix pairs delimiters by run length the way CommonMark's delimiter stack does. Regression fixtures like*a **b** c*and_italic __bold__ tail_would lock it down.