From 8dfe1d43f189dcc9d18b44480ea8fe2d3b021136 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 20 Mar 2026 01:17:08 -0700 Subject: [PATCH] fix: footnote hell --- src/Memoizer.ts | 5 ++++- tests/Memoizer.test.tsx | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Memoizer.ts b/src/Memoizer.ts index 0ace2982418b420bd56fdc07195cbf9b1f5a687e..81c2adfc023760a02149c449b4080a0273da37e0 100644 --- a/src/Memoizer.ts +++ b/src/Memoizer.ts @@ -100,6 +100,9 @@ export class Memoizer { // much effort), but any extra re-renders get eliminated with AST diffing. const processor = UNWRAP(this.#astProcessor); const parsed = this.#parsed; + const replacedTailAffectedDocument = parsed + .slice(blockStart) + .some((block) => block.some(nodeAffectsDocument)); let parsedTree = processor.parse(content.slice(parseOffset)); let newPositions = parsedTree.children.map( (child) => UNWRAP(UNWRAP(child.position).start.offset) + parseOffset, @@ -118,7 +121,7 @@ export class Memoizer { // If a reflink is involved, fall back to a full reparse. Rehydrating a // synthetic root from cached blocks can lose source offsets on transformed // top-level nodes, which would scramble block bucketing and key identity. - if (nodeAffectsDocument(parsedTree)) { + if (replacedTailAffectedDocument || nodeAffectsDocument(parsedTree)) { blockStart = 0; parseOffset = 0; parsedTree = processor.parse(content); diff --git a/tests/Memoizer.test.tsx b/tests/Memoizer.test.tsx index 0e3df355c15afa3d828131e6aa77ff685d56a532..f34696681e9f4e1329d2a4240ffa9d63bf2bd721 100644 --- a/tests/Memoizer.test.tsx +++ b/tests/Memoizer.test.tsx @@ -140,6 +140,20 @@ describe("Memoizer incremental rendering", () => { expect(after).toContain('href="/two"'); }); + it("keeps earlier gfm footnotes rendered when a later footnote definition is removed", () => { + const incremental = createMemoizer(gfmProcessor); + + incremental.update("a[^b]\n\n[^b]: bee\n\nc"); + incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]\n\n[^d]: dee"); + const after = renderNodes(incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]")); + + const fresh = createMemoizer(gfmProcessor); + const expected = renderNodes(fresh.update("a[^b]\n\n[^b]: bee\n\nc[^d]")); + + expect(after).toBe(expected); + expect(after).toContain('id="user-content-fn-b"'); + }); + it.each([ { content: ["alpha\n\nbeta", "alpha\nbeta", "alpha\n\nbeta\n\ngamma", "alpha\n\nbeta"], -- 2.54.0