authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 02:04:07-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-03-20 14:11:08-07:00
log91dbe3faf3a1deed7e649f98eb06b8ae72eaa456
tree2f56e23ac7fcf682cf546f714d26acd1bfb87a1b
parentb97903baac224e5529613b593fe793f6b8699d0f
signaturebadge-check Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

chore: document a failure case


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

tests/Memoizer.test.tsx+47-2
...@@ -17,6 +17,30 @@ import {...@@ -17,6 +17,30 @@ import {
17} from "../src/Memoizer.ts";17} from "../src/Memoizer.ts";
1818
19const gfmProcessor = unified().use(remarkParse).use(remarkGfm);19const gfmProcessor = unified().use(remarkParse).use(remarkGfm);
20const documentStateProcessor = unified()
21 .use(remarkParse)
22 .use(function documentStatePlugin() {
23 return (tree: Parent) => {
24 const paragraphs = tree.children.filter(
25 (node): node is Parent =>
26 node.type === "paragraph" && "children" in node && Array.isArray(node.children),
27 );
28 const suffix = paragraphs
29 .map((paragraph) => paragraphText(paragraph))
30 .findLast((text) => text.startsWith("suffix: "))
31 ?.slice("suffix: ".length);
32 if (!suffix) return tree;
33
34 const target = paragraphs.find((paragraph) => !paragraphText(paragraph).startsWith("suffix: "));
35 const firstChild = target?.children[0];
36 if (firstChild?.type === "text") {
37 const textNode = firstChild as Literal;
38 const value = String(textNode.value);
39 textNode.value = `${value} (${suffix})`;
40 }
41 return tree;
42 };
43 });
2044
21describe("Memoizer utilities", () => {45describe("Memoizer utilities", () => {
22 it("finds the first differing index for edits in the middle, start, and end", () => {46 it("finds the first differing index for edits in the middle, start, and end", () => {
...@@ -143,8 +167,8 @@ describe("Memoizer incremental rendering", () => {...@@ -143,8 +167,8 @@ describe("Memoizer incremental rendering", () => {
143 it("keeps earlier gfm footnotes rendered when a later footnote definition is removed", () => {167 it("keeps earlier gfm footnotes rendered when a later footnote definition is removed", () => {
144 const incremental = createMemoizer(gfmProcessor);168 const incremental = createMemoizer(gfmProcessor);
145169
146 incremental.update("a[^b]\n\n[^b]: bee\n\nc");170 void 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");171 void 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]"));172 const after = renderNodes(incremental.update("a[^b]\n\n[^b]: bee\n\nc[^d]"));
149173
150 const fresh = createMemoizer(gfmProcessor);174 const fresh = createMemoizer(gfmProcessor);
...@@ -222,6 +246,20 @@ describe("Memoizer incremental rendering", () => {...@@ -222,6 +246,20 @@ describe("Memoizer incremental rendering", () => {
222246
223 expect(second[2]).toBe(first[2]);247 expect(second[2]).toBe(first[2]);
224 });248 });
249
250 // TODO: this is a tough problem to cleanly solve without giving up too much memoization.
251 it.fails("re-renders earlier blocks when a later custom-plugin block changes document-wide state", () => {
252 const incremental = createMemoizer(documentStateProcessor);
253 void incremental.update("hello\n\nsuffix: one");
254
255 const after = renderNodes(incremental.update("hello\n\nsuffix: two"));
256
257 const fresh = createMemoizer(documentStateProcessor);
258 const expected = renderNodes(fresh.update("hello\n\nsuffix: two"));
259
260 expect(after).toBe(expected);
261 expect(expected).toContain("hello (two)");
262 });
225});263});
226264
227function createMemoizer(processor = defaultProcessor, predict = false) {265function createMemoizer(processor = defaultProcessor, predict = false) {
...@@ -233,3 +271,10 @@ function createMemoizer(processor = defaultProcessor, predict = false) {...@@ -233,3 +271,10 @@ function createMemoizer(processor = defaultProcessor, predict = false) {
233function renderNodes(nodes: readonly ReactNode[]) {271function renderNodes(nodes: readonly ReactNode[]) {
234 return renderToStaticMarkup(<Fragment>{nodes}</Fragment>);272 return renderToStaticMarkup(<Fragment>{nodes}</Fragment>);
235}273}
274
275function paragraphText(node: Parent) {
276 return node.children
277 .filter((child): child is Literal => child.type === "text")
278 .map((child) => String(child.value))
279 .join("");
280}