| author | |
| committer | |
| log | b97903baac224e5529613b593fe793f6b8699d0f |
| tree | 432ff98d6fcf695365dbc677f33e1183d5b6f0bb |
| parent | 8dfe1d43f189dcc9d18b44480ea8fe2d3b021136 |
| signature |
3 files changed, 62 insertions(+), 5 deletions(-)
src/hast.ts+18-2| ... | ... | @@ -61,6 +61,15 @@ function recursiveMemoRender( |
| 61 | 61 | const nextChildren: RenderState[] = []; |
| 62 | 62 | const children: ReactNode[] = []; |
| 63 | 63 | const countsByName = new Map<string, number>(); |
| 64 | const usedKeys = new Set(previousChildren.map((child) => child.key).filter((key) => key !== undefined)); | |
| 65 | let prefixLength = 0; | |
| 66 | for (; prefixLength < node.children.length && prefixLength < previousChildren.length; prefixLength += 1) { | |
| 67 | if (!nodeDeepEquals(node.children[prefixLength], previousChildren[prefixLength]?.node)) break; | |
| 68 | } | |
| 69 | let suffixLength = 0; | |
| 70 | for (; suffixLength < node.children.length - prefixLength && suffixLength < previousChildren.length - prefixLength; suffixLength += 1) { | |
| 71 | if (!nodeDeepEquals(node.children[node.children.length - 1 - suffixLength], previousChildren[previousChildren.length - 1 - suffixLength]?.node)) break; | |
| 72 | } | |
| 64 | 73 | |
| 65 | 74 | for (let i = 0; i < node.children.length; i += 1) { |
| 66 | 75 | const child = node.children[i]!; |
| ... | ... | @@ -70,8 +79,15 @@ function recursiveMemoRender( |
| 70 | 79 | countsByName.set(child.tagName, count + 1); |
| 71 | 80 | childKey = `${child.tagName}-${count}`; |
| 72 | 81 | } |
| 73 | ||
| 74 | const childState = recursiveMemoRender(child, previousChildren[i] ?? null, options, childKey); | |
| 82 | let previousIndex = -1; | |
| 83 | if (i < prefixLength || node.children.length === previousChildren.length) previousIndex = i; | |
| 84 | if (i >= node.children.length - suffixLength) { | |
| 85 | previousIndex = previousChildren.length - (node.children.length - i); | |
| 86 | } | |
| 87 | if (previousIndex >= 0) childKey = previousChildren[previousIndex]?.key ?? childKey; | |
| 88 | while (childKey && usedKeys.has(childKey) && previousChildren[previousIndex]?.key !== childKey) childKey = `${childKey}+`; | |
| 89 | if (childKey) usedKeys.add(childKey); | |
| 90 | const childState = recursiveMemoRender(child, previousChildren[previousIndex] ?? null, options, childKey); | |
| 75 | 91 | nextChildren.push(childState); |
| 76 | 92 | if (childState.react !== undefined) children.push(childState.react); |
| 77 | 93 | } |
tests/Markdown.memoization.test.tsx+20| ... | ... | @@ -341,6 +341,26 @@ it("keeps a stable block tied to its own DOM node when blocks are inserted or re |
| 341 | 341 | expect(screen.getByRole("link")).toBe(stableLink); |
| 342 | 342 | }); |
| 343 | 343 | |
| 344 | it("keeps an unchanged list item tied to its DOM node when a sibling item is inserted above it", () => { | |
| 345 | function ListItem(props: MarkdownProps<"li">) { | |
| 346 | const instanceRef = useRef(Symbol("instance")); | |
| 347 | return <li data-instance={String(instanceRef.current)} {...omitNode(props)} />; | |
| 348 | } | |
| 349 | ||
| 350 | const components = { li: ListItem } satisfies Partial<Components>; | |
| 351 | const { rerender } = render( | |
| 352 | <Markdown content={"- stable\n- tail"} components={components} />, | |
| 353 | renderOptions, | |
| 354 | ); | |
| 355 | ||
| 356 | const stableItem = screen.getByText("stable").closest("li"); | |
| 357 | expect(stableItem).not.toBeNull(); | |
| 358 | ||
| 359 | rerender(<Markdown content={"- inserted\n- stable\n- tail"} components={components} />); | |
| 360 | ||
| 361 | expect(screen.getByText("stable").closest("li")).toBe(stableItem); | |
| 362 | }); | |
| 363 | ||
| 344 | 364 | it("keeps demo README strong renderers asleep when editing a different block", () => { |
| 345 | 365 | const Strong = vi.fn(function Strong(props: MarkdownProps<"strong">) { |
| 346 | 366 | return <strong data-testid="readme-strong" {...omitNode(props)} />; |
tests/memoizedHastToReact.test.tsx+24-3| ... | ... | @@ -165,7 +165,7 @@ it("reuses deep descendants through rebuilt ancestors", () => { |
| 165 | 165 | expect(secondStrong).toBe(firstStrong); |
| 166 | 166 | }); |
| 167 | 167 | |
| 168 | it("does not reuse an equal subtree when its generated key changes", () => { | |
| 168 | it("reuses an equal subtree when a sibling of another tag is inserted above it", () => { | |
| 169 | 169 | const first = renderTree( |
| 170 | 170 | root(element("span", [text("first")]), element("span", [text("stable")])), |
| 171 | 171 | ); |
| ... | ... | @@ -180,8 +180,29 @@ it("does not reuse an equal subtree when its generated key changes", () => { |
| 180 | 180 | |
| 181 | 181 | const secondStable = childAt(second.react, 1); |
| 182 | 182 | |
| 183 | expect(asElement(secondStable).key).toBe("span-0"); | |
| 184 | expect(secondStable).not.toBe(firstStable); | |
| 183 | expect(asElement(secondStable).key).toBe("span-1"); | |
| 184 | expect(secondStable).toBe(firstStable); | |
| 185 | }); | |
| 186 | ||
| 187 | it("preserves an unchanged list item when a sibling item is inserted above it", () => { | |
| 188 | const first = renderTree(root(element("ul", [element("li", [text("stable")]), element("li", [text("tail")])]))); | |
| 189 | const firstList = childAt(first.react, 0); | |
| 190 | const firstStable = childAt(firstList, 0); | |
| 191 | ||
| 192 | const second = renderTree( | |
| 193 | root( | |
| 194 | element("ul", [ | |
| 195 | element("li", [text("inserted")]), | |
| 196 | element("li", [text("stable")]), | |
| 197 | element("li", [text("tail")]), | |
| 198 | ]), | |
| 199 | ), | |
| 200 | first.state, | |
| 201 | ); | |
| 202 | const secondList = childAt(second.react, 0); | |
| 203 | const secondStable = childAt(secondList, 1); | |
| 204 | ||
| 205 | expect(secondStable).toBe(firstStable); | |
| 185 | 206 | }); |
| 186 | 207 | |
| 187 | 208 | it("uses configured components for matching tags", () => { |