Two live-reply bugs visible in the Zalo client after 2.3.0:
### 1. Inline span offsets drifted across passes
Live test of `**Chữ đậm**. *Chữ nghiêng*. __Chữ gạch chân__.` rendered the
Underline span over "ữ gạch chân." instead of "Chữ gạch chân" — two
characters off. The `<small>Chữ nhỏ fine print</small>` span was even
worse: it pointed into the next paragraph's "rint\n\nBullet item".
Root cause: applyInlineStyles ran each regex as a separate pass. A pass
emitted span offsets computed from `result.length` at substitution time,
i.e. offsets in the text as it stood AFTER that pass. Later passes strip
more characters (every `**` `__` `~~` removed shifts positions) but never
re-aligned the already-emitted spans.
Fix: tokenise-then-render. Each pass now replaces matches with a
sentinel `\u0001<N>\u0001` and stashes content+style in a side array.
Later passes see the tokens as opaque characters and can't shift them.
A single final walk expands tokens to content and emits spans at the
correct final offset. Nested styles still collapse to outer-only (Zalo
limitation), but every single-level span is now byte-accurate.
### 2. Inline-content admonition markers were not detected
LLMs emit `> [!TIP] content on same line` but the pre-scan regex
(`/^>\s*\[!TYPE\]\s*$/`) required the marker alone on its line.
Consequence: the marker leaked through as literal text with a `│ `
quote prefix (`│ [!TIP] 💡 Mỗi ngày...`) and no colour applied.
Fix: pre-normalise inline variants to the canonical two-line form
before the block pre-scan:
`> [!TIP] content` → `> [!TIP]\n> content`
Existing multi-line handling then colours the content line correctly
and drops the marker line.
Also removed the dangling blank line the skipped marker used to leave.
### Verified via full palette
Ran live bot reply through markdownToZaloStyles — 26 style spans emitted,
every one snaps to exactly the intended content substring. Heading /
bold / italic / underline / strike / small / ul (5 levels) / ol / indent
lvl 1+2 / green / yellow / orange / red all aligned.