| index 4a921e7..5ef0b1f 100644 |
| --- a/fixtures/generate-fragment.mjs |
| +++ b/fixtures/generate-fragment.mjs |
| @@ -269,20 +269,41 @@ writeFileSync('vectors/fragment/sizes.bin', docWith(f => { |
| // Y.Text. |
| // |
| // plainTextOf mirrors core/src/fragment.rs's fragment_text: walk the |
| -// tree, concatenate each Y.XmlText's own rendering, and join top-level |
| +// tree, concatenate each Y.XmlText's characters, and join top-level |
| // block texts with a blank line. |
| // |
| -// Uses each Y.XmlText's own toString(), not toDelta() — verified |
| -// against the real Rust output, not assumed: fragment_text calls |
| -// yrs's XmlTextRef::get_string(txn), and yrs's own get_string embeds |
| -// formatting as inline pseudo-tags exactly the way Y.XmlText.toString() |
| -// does in JS ("plain <strong>bold</strong> plain", not "plain bold |
| -// plain") — this is XmlText's own "plain text" convention in both |
| -// implementations, not the HTML fragment_html builds via its own MARKS |
| -// table. A first version of this generator used toDelta() (mark-free) |
| -// and it disagreed with the real Rust output on the very first run — |
| -// core/tests/wasm_trap_containment.rs caught it immediately, which is |
| -// the whole reason this fixture set exists. |
| +// TODELTA, NOT toString, AND THIS LINE HAS BEEN BOTH — the history is |
| +// the point and is worth reading before changing it back. |
| +// |
| +// The first version of this generator used toDelta (mark-free). It |
| +// disagreed with the real Rust output immediately, and the disagreement |
| +// was resolved the wrong way round: the generator was changed to |
| +// toString to match Rust, under a comment explaining that inline |
| +// pseudo-tags are "XmlText's own plain text convention in both |
| +// implementations". Both halves of that sentence are true. The |
| +// conclusion drawn from it was not. |
| +// |
| +// fragment_text's own doc comment says what it is for: "the words |
| +// without the markup". It called yrs's XmlTextRef::get_string, which |
| +// serialises a formatted run's attributes as XML tags, so it returned |
| +// "plain <strong>bold</strong> plain" — markup, from the one renderer |
| +// whose entire job is to have none. Y.XmlText.toString does the same |
| +// thing in JS, so the two agreed. They agreed because they shared a |
| +// library's convention, not because either was right, and the |
| +// expected.json committed here recorded the wrong answer as the |
| +// contract. |
| +// |
| +// It was latent — Store.Text has no caller in a handler — and it |
| +// surfaced from a LINK, whose "<link href=…>" was long enough to notice |
| +// where "<strong>" had sat unnoticed since marks existed. fragment.rs's |
| +// node_text walks diff now; this walks toDelta, which is the same walk |
| +// in the other language; and the vectors were regenerated in the commit |
| +// that changed both. |
| +// |
| +// The lesson worth keeping: a generator that is "verified against the |
| +// real output" is verified against the code, not against what the code |
| +// should do. Two implementations agreeing is evidence about the |
| +// library they share. |
| function plainTextOf(doc) { |
| const f = doc.getXmlFragment('prosemirror') |
| const isBlock = (name) => |
| @@ -291,7 +312,9 @@ function plainTextOf(doc) { |
| let cur = '' |
| function walk(node) { |
| if (node instanceof Y.XmlText) { |
| - cur += node.toString() |
| + for (const op of node.toDelta()) { |
| + if (typeof op.insert === 'string') cur += op.insert |
| + } |
| } else if (node instanceof Y.XmlElement) { |
| for (const child of node.toArray()) walk(child) |
| if (isBlock(node.nodeName) && cur.length > 0) { |