# 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/` (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; **``, `` and `` 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.