authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 01:17:08-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 14:11:08-07:00
log8dfe1d43f189dcc9d18b44480ea8fe2d3b021136
treee86fc72828986dd6f71d1fccc6e011bbff391861
parent5b921620c94fade38cd72a34fe2e1d195ef19cb8
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

fix: footnote hell


2 files changed, 18 insertions(+), 1 deletions(-)

src/Memoizer.ts+4-1
......@@ -100,6 +100,9 @@ export class Memoizer {
100100 // much effort), but any extra re-renders get eliminated with AST diffing.
101101 const processor = UNWRAP(this.#astProcessor);
102102 const parsed = this.#parsed;
103 const replacedTailAffectedDocument = parsed
104 .slice(blockStart)
105 .some((block) => block.some(nodeAffectsDocument));
103106 let parsedTree = processor.parse(content.slice(parseOffset));
104107 let newPositions = parsedTree.children.map(
105108 (child) => UNWRAP(UNWRAP(child.position).start.offset) + parseOffset,
......@@ -118,7 +121,7 @@ export class Memoizer {
118121 // If a reflink is involved, fall back to a full reparse. Rehydrating a
119122 // synthetic root from cached blocks can lose source offsets on transformed
120123 // top-level nodes, which would scramble block bucketing and key identity.
121 if (nodeAffectsDocument(parsedTree)) {
124 if (replacedTailAffectedDocument || nodeAffectsDocument(parsedTree)) {
122125 blockStart = 0;
123126 parseOffset = 0;
124127 parsedTree = processor.parse(content);
tests/Memoizer.test.tsx+14
......@@ -140,6 +140,20 @@ describe("Memoizer incremental rendering", () => {
140140 expect(after).toContain('href="/two"');
141141 });
142142
143 it("keeps earlier gfm footnotes rendered when a later footnote definition is removed", () => {
144 const incremental = createMemoizer(gfmProcessor);
145
146 incremental.update("a[^b]\n\n[^b]: bee\n\nc");
147 incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]\n\n[^d]: dee");
148 const after = renderNodes(incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]"));
149
150 const fresh = createMemoizer(gfmProcessor);
151 const expected = renderNodes(fresh.update("a[^b]\n\n[^b]: bee\n\nc[^d]"));
152
153 expect(after).toBe(expected);
154 expect(after).toContain('id="user-content-fn-b"');
155 });
156
143157 it.each([
144158 {
145159 content: ["alpha\n\nbeta", "alpha\nbeta", "alpha\n\nbeta\n\ngamma", "alpha\n\nbeta"],