mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-26 06:19:49 +00:00
main
7 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
08a5f1c7d2 |
fix(editor): a block marker after a hard break stays text (BUG-3584) (#2060)
* fix(editor): a block marker after a hard break stays text (BUG-3584) prosemirror-markdown escapes a leading -, +, *, >, # or 1. only at a block start, but a hard break starts a new line too, so 'First line' + hard break + '- not a list item' was stored as 'First line\\\n- not a list item' and read back as a paragraph plus a bullet list. PadText treats a line start as a block start for that escape, restoring the flag after. Editor and materializer share PadText (htmlProseExtensions), so parity holds. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * test(materialize): corpus covers block markers after a hard break (BUG-3584) The BUG-3583 corpus block gains lines starting '- ', '# ' and '> ' after hard breaks (one block for both, so adding it did not reshuffle the seeded cases); a vantage probe pins that the corpus reaches the escaped form. corpus.json regenerated; go test ./internal/materialize passes on the rebuilt bundle. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * fix(editor): every block marker after a hard break stays text, and nothing else changes (BUG-3584) The first fix (atBlockStart for the line) missed `1)`, indented bullets, thematic breaks and setext underlines, and escaped markers that open no block. PadText now writes, at any line start, the escaped form of exactly the markers that would open a block (lineStartMarker): a bullet or ordered marker followed by a space/tab/end, `>`, `#{1,6}` followed by a space/tab/ end, or a line of only -, =, _ or *. `#tag`, `-5`, `1.5` keep their form. At a true block start prosemirror-markdown's escape stays in charge except for what it missed (`1)` and an indented bullet/quote, both lists before). Measured against main's serializer over 28 strings at a block start and after a hard break: every difference is one of those bug fixes. Corpus regenerated (the first version's `\-item` escape is gone); go test ./internal/materialize passes on the rebuilt bundle; 22 unit tests. Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ |
||
|
|
8d910dc20a |
fix(editor): bold, italic and strike always serialize to markdown that reads back (BUG-3583) (#2057)
* fix(editor): bold, italic and strike always serialize to markdown that reads back (BUG-3583) CommonMark lets a `**` / `*` / `~~` run open only where it is left-flanking and close only where it is right-flanking. A mark whose text starts with punctuation after a letter, or ends with punctuation before a letter, has no delimiter that can do both. Measured on the headless editor with the editor's own parser: every such case lost its mark on reload. tiptap-markdown slides an unflankable delimiter inward (the punctuation silently loses the mark), and skips that for a mark at output offset 0, so a bold run starting a block went out as `**Hello.**World` and read back as literal asterisks. flankingMarks.ts replaces bold, italic and strike (and re-adds code and underline, keeping StarterKit's mark order) in the body editor, the materializer (htmlProseExtensions, so parity holds) and the comment editor: - each run is written with its delimiter and, when it closes, tested against the characters actually around it with markdown-it's scanDelims; a run that cannot flank is rewritten to <strong>/<em>/<s>, which parse back to the same mark (html: true) and render through renderMarkdown's allow-list; - tiptap-markdown's sliding is switched off for these marks only (a guarded patch on prosemirror-markdown's base markString); - a textblock whose output could still mis-pair (a run ending in punctuation before a letter, or merged same-character runs) is parsed back and compared per character; on a mismatch it is re-rendered with the HTML form. Code needs nothing (measured). Fuzz (6000 paragraphs of letters, punctuation and spaces under overlapping marks, links and hard breaks, in paragraphs, quotes and list items): marks lost 0 (old serializer 211-228 per 1500); text altered 0-1 (old 163-172, the rest a pre-existing line-start escaping gap); output changed where the old one already round-tripped: 3 per 1500, all one whitespace placement. Serialization time on 500 paragraphs: equal to main for ordinary prose; the extra parse runs only on the textblocks that can fail. Corpus: a block with the failing shapes is added to the materializer parity generator; corpus.json regenerated; Go materialize tests pass on the new bundle. New e2e: typed bold "Hello." + "World" is stored as <strong>Hello.</strong>World; on main it is stored as **Hello.**World. (A reload of an open tab does not show the defect on main: it re-seeds from the op-log. The stored body is what readers and a pruned editor see.) Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ * fix(web): backlink snippets drop inline tags; every reader of an HTML mark pinned (BUG-3583) The lead's check before GO: the fix stores runs markdown delimiters cannot express as inline HTML, so every other reader of items.content must show it as formatting or plain text, never literal tags. - Share pages and their collection views: renderMarkdown's allow-list renders <strong>/<em>/<s> (e2e). - The editor seeding from the body (after an op-log prune): bold (e2e). - Search palette snippets: already stripped tags (e2e pins it). - Backlink snippets showed the raw slice, so `<strong>` appeared literally, and the slice could cut a tag in two (`…g>Hello.`). Tags and cut fragments are now dropped. Red on main in the new e2e, green here. - Email/notification templates carry no item content; the workspace and artifact exports hand out the raw body, where inline HTML is valid CommonMark/GFM; nothing syncs bodies to GitHub (read from the code). Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ |
||
|
|
92305fba4d |
fix(web): marks never sit on inline atoms, so a tab's editor equals its Y.Doc (BUG-3568) (#2038)
ProseMirror lets a mark sit on an inline atom (a hard break, an inline attachment image or chip). Bolding, striking or linking a range that held one marked it, and markdown or HTML parsing marked it when the source wrapped it (**a\ b**). y-tiptap never writes marks on non-text inline nodes to the Y.Doc, so that tab's editor held a document its own Y.Doc did not. It flushed one body while every other tab, a reload and the materializer derived another. This is the ~0.7% tab divergence materializerParity found. Measured: both Y.Docs identical, and only the marking tab's ProseMirror doc differs; a one-tab repro with no relay reproduces it 4 of 4. InlineAtomNoMarks (extensions/inlineAtomNoMarks.ts) is an appendTransaction plugin that strips marks from inline non-text nodes in the same update. It fires only on AddMarkStep, AddNodeMarkStep, or a replace whose slice holds a marked atom, so typing pays nothing. It is registered in Editor.svelte and the headless materializer. No node spec or persisted shape changes, so no SCHEMA_VERSION question; NodeSpec.marks cannot do this, since it governs a node's content (measured: it left the marks in place). One-time normalisation: a body holding a mark across a hard break (**a\ b**) flushes as **a**\ **b**, the form every Y-derived door already produced, so this touches only bodies that already flip-flopped. It is stable from then on. materializerParity: MAX_TAB_DISAGREEMENTS 2 -> 0. The corpus is regenerated; seed-300922 is now a case. The generator is not deterministic run to run (Yjs client ids), so a regen also rewrites a couple of other cases' expectations; each corpus is self-consistent. Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS |
||
|
|
1eecf710c8 |
fix(web): angle-bracketed prose survives the editor: <Button>, Promise<T>, comments and unknown tags are text, not dropped markup (BUG-3557) (#2035)
The editor parsed markdown with html enabled, so `<Name>` in prose was a tag the schema could not hold: dropped on the next flush (one keystroke, or an agent's write applied by an open tab, which answered 200 while the text vanished). `<Table>` became an empty table that was then removed. htmlProse.ts derives the allowlist from the schema's own parse rules, so the HTML the editor itself writes (tables with spans, HTML-only marks) still reads back: - an inline tag passes only if written lower case, matching an inline node's or a mark's rule, and closing one that passed; anything else is literal text; - an HTML block passes only if every tag in it matches a rule; otherwise it is kept whole as an HtmlBlock (stored as a ```html fence); - comments are visible text. PadText escapes `<` only where the parse would read a tag, so `Promise<T>` round-trips byte for byte. Both the editor and the headless materializer take the same extensions, right after StarterKit. materializerParity now skips and counts cases where the two live tabs converge to different documents (BUG-3568, pre-existing: 2 of 300 seeds on main), at most 2 of 50, and still requires the materializer to match one of them; such cases stay out of the Go corpus (lead ruling). Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ |
||
|
|
f0c65fe08c |
chore(web): adopt Tiptap 3.31.4 and regenerate the materializer corpus (TASK-3421) (#1811)
* chore(web): adopt Tiptap 3.31.4 and regenerate the materializer corpus (TASK-3421) 3.31.4's TableCell/TableHeader renderHTML stop rendering a default colspan="1" / rowspan="1". parseHTML is unchanged, so a stored body carrying them parses to the same document. Replaying all 50 old corpus op-logs through the 3.31.4 bundle reproduces the old output exactly once those attributes are removed (and fails on 28 cases without removing them). Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS * test(web): pin that a pre-3.31.4 table body still dedupes on open (TASK-3421) Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS |
||
|
|
1e16d2228b |
fix(web): link text with markdown/HTML-significant characters survives a save (BUG-3315 U1) (#1686)
A wiki link's display text was mangled on its way through the editor, and the first save of any item linking such a title pinned the mangled text as a display override, with no rename involved (a census found 76 stored overrides of this shape among 107 that differ from their target's title). Load: escapeMarkdownLinkText escaped only brackets and backslashes, so the html-enabled parser read the title as markdown. `*real*` became emphasis, `<topic>` was dropped as a tag, `>` was decoded. It now backslash-escapes \ [ ] * _ ~ ` < > &, all ASCII punctuation CommonMark lets a backslash escape. Save: unescapeMarkdownLinkText undid only bracket and backslash escapes, while the serializer also backslash-escapes other punctuation and writes < > & as entities. It now undoes both, in one pass. One residue is intrinsic and pinned by a test: the serializer writes a literal `>` and a literal `>` as the same bytes, so a title containing entity-shaped text still becomes an override (0 of 9,620 live titles do). The materializer corpus: two expectations change (a backtick and `<b>` in link text, both this fix), taken from the goja bundle's output. A full regeneration was not used because the generator is nondeterministic: with unchanged code it rewrites all 50 cases' rows and 1 expected value. Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC |
||
|
|
fa2755e183 |
feat(materialize): headless op-log materializer bundle, goja runner and worker (TASK-2198 U2) (#1678)
* feat(materialize): headless op-log materializer bundle, goja runner and worker (TASK-2198 U2)
Builds the editor's own JS (Yjs -> y-tiptap -> Tiptap schema -> tiptap-markdown
-> the tab's flush pipeline) into one IIFE, embeds it, and runs it in goja. Not
wired into the server yet (U3 supervisor, U4 write path).
- web/src/lib/collab/materializer/: bundle entry (extension list mirrors
Editor.svelte's collab branch via the U1 shared modules), op-log replay
mirroring wsProvider's MESSAGE_SYNC arm, linkedom DOM shim and polyfills
from the spike, and a vendored patch of prosemirror-markdown
MarkdownSerializerState.atBlank (quadratic under goja; equivalent).
- web/scripts/build-materializer.mjs (esbuild) -> web/build-materializer/,
run by `npm run build`; embedded as pad.MaterializerJS.
- internal/materialize: Runner (Go-native atob/btoa/TextEncoder/TextDecoder/
getRandomValues, schema-version check, ctx interrupt with a clean VM after),
and RunWorker's length-prefixed JSON protocol behind the hidden
`pad __materialize-worker` command.
- Parity: vitest drives two real Editor.svelte mounts and compares
materialize() with the tab's flush on 50 seeded cases; the same cases are
committed as testdata/corpus.json and replayed in goja (TestCorpusParity).
- CI Go jobs, release tests, Docker and Nix produce the bundle; linkedom and
esbuild are new devDependencies.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
* test(materialize): pin the atBlank patch's equivalence under V8 too (TASK-2198 U2, codex r1)
Codex r1 claimed upstream's /(^|\n)$/ is true for a trailing CR in JS. It is
not (checked in V8: false for "\r" and "a\u2028", as the patch returns),
but the only equivalence test ran under goja and the patch header cited a
V8-side test that did not exist. This adds it, with the line-terminator edge
cases, and it fails when the patch checks CR instead of LF.
Claude-Session: https://claude.ai/code/session_01H63zaWhkb5pAafx2WGjrVC
* build(nix): vendorHash for goja (TASK-2198 U2)
Taken from the Nix job's recompute on
|