Files

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)

  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.