9.0 KiB
Row 154 (#1648) phase 1: library search
Researcher, 2026-10-18. Brief: docs/plans/2026-10-16_q168-wysiwyg-editor.md (blob 9d01d989c1). 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)
- Permissive license.
script-src 'self': no eval, nonew Functionthat can run; nothing loaded at runtime; no CSP change.- A current sanitizer, or a design with no HTML sink that needs one.
- 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>(tags1.25.12and 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'spackage.jsonname).dist/is the prebuilt output; the plan importssrc/and neverdist/. Two small packages (orderedmap, rope-sequence) keep a rootindex.jssource; their tags track only type declarations underdist/, and the build aliases the rootindex.js. - Build. One unminified IIFE from
esbuild0.28.2 pinned inpackage.jsonwith 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-itruns withhtml:falseand the editor schema is the whitelist. The one HTML sink is paste, whichprosemirror-viewparses into a detachedcreateHTMLDocumentbody through the schema'sparseDOMrules. Measured with hostile HTML: scripts, handlers,svg,iframe,style,form,objectandmathare 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 enforcelinkTargetand render images as text, as row 148's preview does. - CSP/eval. No
eval,new Function,document.write,fetch(,XMLHttpRequestor dynamicimport(in the bundle (0 hits; the bundle holds all 132 inputs). It needs onlyscript-src 'self';style-srcalready 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.