Files

120 lines
9.0 KiB
Markdown

# Row 154 (#1648) phase 1: library search
Researcher, 2026-10-18. Brief: `docs/plans/2026-10-16_q168-wysiwyg-editor.md` (blob 9d01d989c187). Starts from row 148's
search (`agents/researcher/work/queue-148/LIBRARY.md`). Nothing was installed in the repository; every install was a
scratch directory with its own `package.json` under `/mnt/storage/scratch/researcher/q154/`, `--ignore-scripts`, and
the Node runs were under `bwrap --unshare-net`. The advisory queries used the OSV batch API
(`https://api.osv.dev/v1/querybatch`) on each candidate's lockfile, run 2026-10-18; the positive control (dompurify
2.3.3, markdown-it 12.2.0) returned 20 and 3 advisories the same day, so an empty answer means no advisory.
## Bar (row 148's, from the brief)
1. Permissive license.
2. `script-src 'self'`: no eval, no `new Function` that can run; nothing loaded at runtime; no CSP change.
3. A current sanitizer, or a design with no HTML sink that needs one.
4. A bundle that rebuilds from its tagged source (Q167 D3 bans vendoring prebuilt or minified bundles).
The Console CSP is `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self'; font-src
'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'` (`packages/webui/src/public`
is served from a static route table in `serve.mjs`, line 24-25).
## Candidates
"Normalisation" is measured over the 414 `.md` files in this checkout (`results/corpus.txt`; no `spec/north-star.md`
exists in this checkout, the nearest file is `packages/project/templates/north-star.md`, 3 lines). The test is
parse, serialise, compare bytes, with no edit.
| # | Candidate | Version | License | Closure (locked pkgs) | Advisories (OSV) | Verdict |
|---|---|---|---|---|---|---|
| A | ProseMirror (11 pkgs) + prosemirror-markdown + markdown-it | pm-markdown 1.13.8, markdown-it 14.3.3, see REPORT.md for all pins | MIT (entities BSD-2-Clause) | 29 | 0 | **Passes. The candidate.** |
| B | Milkdown (core, kit, preset-commonmark, transformer) | 7.22.3 | MIT; one dual `MPL-2.0 OR Apache-2.0` (dompurify) | 156 | 0 | Meets the bar; not chosen. |
| C | Tiptap (core, pm, starter-kit, markdown) | 3.32.0 | MIT | 44 | 0 | Meets the license and CSP bars; fails on content: drops tables. |
| D | Lexical (+ markdown, rich-text, list, link, code, history) | 0.52.0 | MIT | 19 | 0 | Meets the bar on paper; round trip not measured. |
| E | BlockNote core | 0.56.0 | MPL-2.0 (core), MIT (rest) | 36 | 0 | Weaker license fit; block model; not measured. |
| F | Toast UI Editor | 3.2.2 | MIT (+ dual MPL/Apache dompurify) | 13 | dompurify 2.5.9: 17 | **Fails** (row 148's finding, rechecked). |
| G | CodeMirror 6 live-preview hybrid | view 6.43.14, state 6.7.6, language 6.13.1, commands 6.11.1, lang-markdown 6.5.2 | MIT | 22 | 0 | Passes; not WYSIWYG, see note. |
| H | Muya (MarkText) | not examined | not checked | - | - | Not measured. |
### A. ProseMirror + prosemirror-markdown + markdown-it
- **Origin.** Source repositories are `code.haverbeke.berlin/prosemirror/<pkg>` (tags `1.25.12` and so on; the GitHub
mirrors lag and lack the current tags), `github.com/markdown-it/*`, `github.com/marijnh/{orderedmap,rope-sequence,w3c-keyname}`,
`github.com/mathiasbynens/punycode.js`, `github.com/fb55/entities`. Pinned commits are in REPORT.md.
- **Source readable without a prebuilt.** Yes. The ProseMirror packages ship their TypeScript in `src/`; the tagged
trees were compared file by file with the npm packages (`results/pins.txt`): every file present in both is
identical (one metadata difference: punycode.js's `package.json` name). `dist/` is the prebuilt output; the plan
imports `src/` and never `dist/`. Two small packages (orderedmap, rope-sequence) keep a root `index.js` source; their
tags track only type declarations under `dist/`, and the build aliases the root `index.js`.
- **Build.** One unminified IIFE from `esbuild` 0.28.2 pinned in `package.json` with lockfile integrity; two builds
from the pinned trees gave identical sha256 (`9587a170...`). See REPORT.md.
- **Sanitizer.** There is no HTML to sanitize on the load path: `markdown-it` runs with `html:false` and the editor
schema is the whitelist. The one HTML sink is paste, which `prosemirror-view` parses into a detached
`createHTMLDocument` body through the schema's `parseDOM` rules. Measured with hostile HTML: scripts, handlers,
`svg`, `iframe`, `style`, `form`, `object` and `math` are dropped; **`<img src>`, `<a href="javascript:...">` and
`<a href="data:...">` survive the default schema** (`results/hostile-html.txt`). Phase 2 must therefore replace the link and image
node specs with ones that enforce `linkTarget` and render images as text, as row 148's preview does.
- **CSP/eval.** No `eval`, `new Function`, `document.write`, `fetch(`, `XMLHttpRequest` or dynamic `import(` in the
bundle (0 hits; the bundle holds all 132 inputs). It needs only `script-src 'self'`; `style-src` already allows
inline styles. I ran it in jsdom, not in a browser under the real CSP; the brief's browser test settles that in
phase 2. One CSS file (`prosemirror-view/style/prosemirror.css`, 54 lines) must be served as a route, plus the
editor's own CSS.
- **Normalisation.** Stock serialiser: 1 of 414 byte-identical. Splice design (below) makes it 414 of 414 by
construction for an unedited file. Details in REPORT.md.
- **Why it passes.** Smallest closure that is a real WYSIWYG, all MIT/BSD-2, tagged source, no sanitizer needed because
the schema is the filter, and it is the engine the larger options (B, C, E) sit on.
### B. Milkdown 7.22.3
Built on ProseMirror (same `prosemirror-model` 1.25.12 / `prosemirror-view` 1.42.6) with remark/unified for markdown.
156 locked packages, 152 MIT, one dual-licensed (dompurify 3.4.17, `MPL-2.0 OR Apache-2.0`, pulled in by
`@milkdown/components`), BSD-2-Clause, BSD-3-Clause, ISC one each. 0 advisories. `src/` ships in the npm package
(`@milkdown/core` 19 files, `preset-commonmark` 60, `transformer` 17, `crepe` 120). No `eval(` or `new Function(`
text in the four tarballs I grepped. Normalisation with `commonmark` + `gfm`: 59 of 414 byte-identical; 142 of 414 after setting the stringify
bullet to `-`. Not chosen: a closure 5 times the size of A to read for a review, remark's stringifier still rewrites
most files, and the extra packages (unified, mdast, micromark, vfile and so on) are all code the reviewers must accept.
Not an outright fail; if A is rejected it is the next one to build.
### C. Tiptap 3.32.0 + `@tiptap/markdown`
44 locked packages, all MIT, 0 advisories, no sanitizer in the closure (it sits on ProseMirror). `src/` ships for
`@tiptap/core` (323 files) and `@tiptap/markdown` (9). Round trip: 1 of 414 identical, and **tables are dropped
entirely** (346 table blocks in the corpus), which is silent content loss, so it fails the brief's "same bytes or
stated normalisation" rule. `@tiptap/pm` re-exports the ProseMirror packages, so the schema-filter caveat in A applies
here too.
### D. Lexical 0.52.0
19 locked packages, all MIT, 0 advisories, no ProseMirror. `src/` ships. I did not run the 414-file round trip
through `@lexical/markdown`, so its normalisation is **not measured**. It is a possible alternative that I did not
examine further, because A already meets the brief.
### E. BlockNote core 0.56.0
`@blocknote/core` is **MPL-2.0** (file-level copyleft; the other 35 packages are MIT), 0 advisories, TipTap/ProseMirror
underneath. Its model is Notion-style blocks, which does not match a Markdown file's own structure. Not measured; the
MPL-2.0 core would need Mos's say-so, which is not worth asking for a candidate that does not beat A.
### F. Toast UI Editor 3.2.2 (row 148's rejection, rechecked)
A fresh lock now resolves `dompurify` 2.5.9, which has **17** OSV advisories (the control confirms the query works).
The npm package has no `src/`; the standalone bundle is not rebuildable from the tag (row 148's `build.md`), and 2 files in the tarball contain the text
`eval(` or `new Function(` (I did not trace whether either can run). Fails the bar for the same reasons as row 148.
### G. CodeMirror 6 (live-preview hybrid)
MIT, 22 locked packages, 0 advisories, no ProseMirror, no sanitizer. Source is on `code.haverbeke.berlin/codemirror/*`
(the npm tarballs hold no `src/`, so the tag is the only readable source, built with the same method as A). It edits the
**source text** with decorations that hide marks around the cursor, so an open-and-save with no edit is byte-exact with no
extra mechanism. It is not a WYSIWYG editor in the sense of the brief (the user sees and edits markup on the active
line). Kept as the fallback if Mos prefers byte-exactness over a rendered editing surface.
### H. Muya
Not examined. It is MarkText's editor core; I did not check its license text, closure, advisories or build. Listed so
the gap is on record.
### Carried from row 148
`marked`, `markdown-it`, `micromark`/`mdast-util-from-markdown` are renderers and parsers, not editors. They do not
satisfy a WYSIWYG requirement; `markdown-it` appears as the parser inside A.