The editor shows what the file says - #182
Conversation
A scoped package defaults to restricted, so its visibility depended on whoever published remembering `--access public`. Forget it once and the package goes private — which fails loudly on a free account and, on a paid one, succeeds quietly and leaves consumers with a 404. `publishConfig` moves that decision into the file, where it is reviewed like anything else. Verified against a bumped version rather than assumed: with no flag, the dry run reports "public access". It sits beside `files`, which is the other field governing what a publish sends.
Lezer emits a `Link` node for every bracketed span it sees. CommonMark
makes `[label]` a link only when the document also carries a matching
`[label]: url` definition, and the parser leaves that lookup to its
consumer — so "there is a Link node here" was never the same claim as
"this is a link", and the registry read it as one.
Every `[…]` therefore lost its brackets on screen:
the value arr[0] is wrong → the value arr0 is wrong
see footnote [3] → see footnote 3
matrix [1,2;3,4] rows → matrix 1,2;3,4 rows
and a pasted LaTeX block lost every optional argument it had:
\begin{tikzpicture}[scale=0.42] → \begin{tikzpicture}scale=0.42
\draw (0,0)++(0:0.7) arc[radius=0.7] → \draw (0,0)++(0:0.7) arcradius=0.7
The bytes were fine; only the rendering lied, which is the worse
failure — nothing looked broken enough to report, and a reader copying
what they saw got LaTeX that no longer compiles.
`linkResolution.ts` does the lookup the parser skipped: an inline
`[text](url)` resolves on its URL, a reference link on a definition
found in the document (case-folded and whitespace-collapsed, per
CommonMark). Unresolved, the brackets are content — visible, unstyled,
and no longer atomic to the caret.
This is also an export-parity fix. Verified against the export's own
parser rather than assumed:
comrak "the value arr[0] is wrong" → <p>the value arr[0] is wrong</p>
comrak "see [ref] now" → <p>see [ref] now</p>
comrak + a [ref]: definition → <p>see <a href="…">ref</a> now</p>
The editor was the only renderer in the app getting this wrong.
The definition scan is cached per `EditorState` and descends only into
block containers — a definition is a block construct, so a paragraph's
inline children are never walked.
Reported as "there is valid LaTeX here — should it not render?". It is
valid, it renders in body text, and the cell showed the source:
body | 440 = 2 × 22/7 × r
cell | $440 = 2 \times \frac{22}{7} \times r$
Math was not the only one. Cells walk the Lezer tree, and FOUR of the
editor's constructs have no node in it — wikilink, `==highlight==`,
footnote reference and `$math$` are not CommonMark, so the editor finds
each by scanning text. A tree walk is blind to all four, and two came
out worse than untouched, because the parser's own reading of their
brackets is wrong:
[[Some Note]] → [Some Note] inner brackets eaten, styled a link
text[^1] → text^1 caret marker styled a link
`inlineScan.ts` is the seam the string renderers were missing: a feature
contributes its pattern and its markup, and the cell renderer consults
the facet knowing nothing about what the constructs are — the same shape
as `nodeRulesFacet`, which the tree-parsed constructs already use. The
decoration plugins keep their own viewport-scanning path; the two share
the pattern, not the machinery, since one produces decorations over a
live document and the other an HTML string.
Cells now carve scanned spans out of the text before walking the tree,
so a scanned construct wins over the markup beneath it — the order the
body already renders in, where a replace decoration covers the marks it
spans. Code stays literal by the same `inCode` guard the body uses, and
a span that would cut a parsed node in half is left alone rather than
rendering that node twice.
Math is the one rule that hydrates. KaTeX builds DOM, and its markup
carries inline styles the cell's sanitiser allow-list drops on purpose,
so the render pass emits the TeX as inert text and KaTeX replaces it
after sanitisation, reading the element's own textContent. The
allow-list stays exactly as tight as it is for prose.
Verified in real WebKit, not just jsdom: the `.katex` span is inline, so
its own rect reads the same for a fraction as for a single letter — the
CELL's height is what moves (26.8px with a fraction, 25.4px flat math,
20.2px prose). Unstyled KaTeX renders SHORTER than prose, so measuring
the cell is what would catch a stripped stylesheet.
Two documentation defects found on the way, both shipped in
@latentic/live-markdown@0.0.1: `styles.css` still called the package
`ai-editor`, and both it and the README promised math needs no CSS
import. KaTeX renders through its own stylesheet, which no CodeMirror
theme can supply — Compose imports it, a reader following the README
would not have.
Cost, measured on a 91k document with a bracket, math, a wikilink and a
table on EVERY line: p50 keystroke 9.87ms on main → 12.19ms with this
and the bracket fix. The definition index is 0.51ms of that (1602 block
nodes); forcing the whole-document parse is 0.03ms, because parseToEnd
has already covered it.
|
Superseded by #183 — not abandoned. Both commits are released and reach Compose through the registry instead. The editor left this repository for getlatentic/live-markdown while this was open, and these two commits touched nothing outside Everything reviewed here still stands:
Verified in #183 against the published tarball rather than a workspace symlink, which is a stronger check than this PR could make: under the old arrangement Compose resolved the package to its TypeScript source and never exercised what npm ships. One correction worth recording. Merging this was going to be gated on a live pass in the packaged app, per the standing rule that headless green is a test bar and not a merge bar. That verification is still owed — it moves to #183, where the app is built against the real package. |
The editor now lives in getlatentic/live-markdown and publishes itself, so Compose depends on `@latentic/live-markdown@^0.1.1` like any other consumer. 20,462 lines leave this repository; the history came with them via `git subtree split`, all 115 commits. Consuming it the way everyone else does is the point. The workspace arrangement resolved the package to its TypeScript SOURCE through a Vite alias and a matching tsconfig path, so Compose never once exercised the thing npm ships — which is how a broken declaration build sat unnoticed while every check here was green, and how the package's own README came to promise that mathematics needs no CSS import. `pnpm-workspace.yaml` stays, with no members, because deleting it was worse than keeping it: pnpm then searches UPWARD for a workspace to join, and silently captured a git worktree nested under the primary checkout — resolving the dependency back to that checkout's `packages/rich-editor` and reporting success. The file now exists to stop the search. Supersedes #182. Those two commits touched nothing outside the package and are already released in 0.1.0, so the fixes arrive here through the registry instead: * a `[…]` keeps its brackets until it resolves to a link * a table cell renders what the paragraph above it renders Verified against the published tarball rather than a link: typecheck clean, 548 tests passing, `pnpm build` succeeds, and the shipped bundle really does contain the bracket fix. The editor-package build step leaves CI with it. That gate has moved to the editor's own three-platform CI, where it belongs — and where, given a second platform for the first time, it immediately found two bugs the macOS-only browser tier had been hiding.
Two rendering defects, found from one screenshot of a table cell.
The LaTeX was valid — the cell never looked
$440 = 2 \times \frac{22}{7} \times r$typesets in body text and showed as source in a table cell.Cells render by walking the Lezer tree. Four of the editor's constructs have no node in that tree — wikilink,
==highlight==, footnote reference and$math$are not CommonMark, so the editor finds each by scanning text. The walk was blind to all four, and two came out worse than untouched, because the parser's own reading of their brackets is wrong:$x^2$$x^2$[[Some Note]][Some Note]— inner brackets eaten, styled a link==marked====marked==text[^1]text^1— styled a linkinlineScan.tsis the seam that was missing: a feature contributes its pattern and its markup, and the cell renderer consults the facet knowing nothing about the constructs — the same shape asnodeRulesFacet, which the tree-parsed constructs already use. The decoration plugins keep their own viewport path; the two share the pattern, not the machinery.Math is the only rule that hydrates, and that is deliberate. KaTeX builds DOM, and its markup carries inline styles the cell's sanitiser allow-list drops on purpose — so the render pass emits the TeX as inert text, and KaTeX replaces it after sanitisation from the element's own
textContent. The allow-list stays exactly as tight as it is for prose.[…]was losing its brackets everywhereChasing the same screenshot's TikZ block turned up something wider. Lezer emits a
Linknode for every bracketed span; CommonMark makes[label]a link only when the document also carries a[label]: urldefinition, and the parser leaves that lookup to its consumer. The registry read "there is a Link node" as "this is a link":The bytes were fine. Only the rendering lied — the worse failure, because nothing looks broken enough to report, and a reader copying what they see gets LaTeX that no longer compiles.
linkResolution.tsdoes the lookup the parser skipped. Unresolved, the brackets are content: visible, unstyled, and no longer atomic to the caret.This is an export-parity fix, verified against the export's own parser rather than assumed:
The editor was the only renderer in the app getting this wrong.
Verification
709 jsdom tests and 174 real-WebKit tests green.
The KaTeX assertion is browser-tier on purpose, and the first version of it was wrong in an instructive way:
.katexis an inline span, so its own rect reads identically for a fraction and for a single letter. The cell's height is what moves — 26.8px with a fraction, 25.4px flat math, 20.2px prose. Unstyled KaTeX renders shorter than prose, so measuring the cell is what catches a stripped stylesheet; measuring.katexcatches nothing.Cost
Measured, on a 91k document with a bracket, math, a wikilink and a table on every line:
The definition index is 0.51 ms of that (1602 block nodes — it descends only into block containers, since a definition is a block construct). Forcing the whole-document parse costs 0.03 ms, because
parseToEndhas already covered it.Baseline was already over #70's 8ms budget on this document, which is adversarial by construction; the budget is advisory today.
Also fixed: two doc defects shipped in
@latentic/live-markdown@0.0.1styles.cssstill called the packageai-editor, and both it and the README promised math needs no CSS import. It does — KaTeX renders through its own stylesheet, which no CodeMirror theme can supply. Compose imports it inApp.tsx; a reader following the README would not have, and would have got math that silently stops stacking.Not fixed: TikZ
Still unsupported, and this PR does not change that — KaTeX has no TikZ, and nothing else in the pipeline draws one. What changes is that a pasted TikZ block now reads back exactly as written instead of quietly losing every
[…]. A fenced block was always safe and still is.