mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-24 14:43:37 -04:00
fix(app): avoid replaying streamed markdown fade (#44396)
This commit is contained in:
@@ -40,3 +40,23 @@ test("keeps completed block text compact", () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("marks words for animation only when requested", () => {
|
||||
expect(
|
||||
parseMarkdownNodes("<p>Hello</p>", true).flatMap((node) => (node.type === "element" ? node.children : [node]))[0],
|
||||
).toEqual({
|
||||
key: "0.0:0",
|
||||
type: "word",
|
||||
text: "Hello",
|
||||
})
|
||||
expect(
|
||||
parseMarkdownNodes("<p>Hello</p>", true, true).flatMap((node) =>
|
||||
node.type === "element" ? node.children : [node],
|
||||
)[0],
|
||||
).toEqual({
|
||||
key: "0.0:0",
|
||||
type: "word",
|
||||
text: "Hello",
|
||||
animate: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dynamic, render } from "solid-js/web"
|
||||
type MarkdownNode =
|
||||
| { key: string; type: "element"; tag: string; attributes: Record<string, string>; children: MarkdownNode[] }
|
||||
| { key: string; type: "text"; text: string }
|
||||
| { key: string; type: "word"; text: string }
|
||||
| { key: string; type: "word"; text: string; animate?: true }
|
||||
|
||||
export function createMarkdownRenderer(root: HTMLDivElement, html: string, words: boolean) {
|
||||
const [nodes, setNodes] = createStore(parseMarkdownNodes(html, words))
|
||||
@@ -17,8 +17,8 @@ export function createMarkdownRenderer(root: HTMLDivElement, html: string, words
|
||||
ready = true
|
||||
|
||||
return {
|
||||
update(next: string, nextWords: boolean) {
|
||||
setNodes(reconcile(parseMarkdownNodes(next, nextWords), { key: "key" }))
|
||||
update(next: string, nextWords: boolean, animate = true) {
|
||||
setNodes(reconcile(parseMarkdownNodes(next, nextWords, animate), { key: "key" }))
|
||||
},
|
||||
dispose,
|
||||
}
|
||||
@@ -30,7 +30,7 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean })
|
||||
if (node.type === "word") {
|
||||
let ref: HTMLSpanElement | undefined
|
||||
onMount(() => {
|
||||
if (props.animate()) ref?.setAttribute("data-markdown-enter", "")
|
||||
if (props.animate() && node.animate) ref?.setAttribute("data-markdown-enter", "")
|
||||
})
|
||||
return (
|
||||
<span ref={ref} data-markdown-word="">
|
||||
@@ -45,19 +45,19 @@ function MarkdownDomNode(props: { node: MarkdownNode; animate: () => boolean })
|
||||
)
|
||||
}
|
||||
|
||||
export function parseMarkdownNodes(html: string, words: boolean) {
|
||||
export function parseMarkdownNodes(html: string, words: boolean, animate = false) {
|
||||
const template = document.createElement("template")
|
||||
template.innerHTML = html
|
||||
return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words))
|
||||
return Array.from(template.content.childNodes).flatMap((node, index) => parseNode(node, `${index}`, words, animate))
|
||||
}
|
||||
|
||||
function parseNode(node: Node, key: string, words: boolean): MarkdownNode[] {
|
||||
function parseNode(node: Node, key: string, words: boolean, animate: boolean): MarkdownNode[] {
|
||||
if (node instanceof Text) {
|
||||
if (!words) return [{ key, type: "text", text: node.data }]
|
||||
return node.data.split(/(\s+)/).flatMap((text, index): MarkdownNode[] => {
|
||||
if (!text) return []
|
||||
if (/^\s+$/.test(text)) return [{ key: `${key}:${index}`, type: "text", text }]
|
||||
return [{ key: `${key}:${index}`, type: "word", text }]
|
||||
return [{ key: `${key}:${index}`, type: "word", text, ...(animate ? { animate: true as const } : {}) }]
|
||||
})
|
||||
}
|
||||
if (!(node instanceof Element)) return []
|
||||
@@ -67,7 +67,7 @@ function parseNode(node: Node, key: string, words: boolean): MarkdownNode[] {
|
||||
type: "element",
|
||||
tag: node.tagName.toLowerCase(),
|
||||
attributes: Object.fromEntries(Array.from(node.attributes).map((attribute) => [attribute.name, attribute.value])),
|
||||
children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words)),
|
||||
children: Array.from(node.childNodes).flatMap((child, index) => parseNode(child, `${key}.${index}`, words, animate)),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -54,7 +54,10 @@ type RenderResult = {
|
||||
}
|
||||
|
||||
const renderedCodeTokens = new WeakMap<HTMLDivElement, RenderedCodeState>()
|
||||
const renderedMarkdown = new WeakMap<HTMLDivElement, ReturnType<typeof createMarkdownRenderer>>()
|
||||
const renderedMarkdown = new WeakMap<
|
||||
HTMLDivElement,
|
||||
{ renderer: ReturnType<typeof createMarkdownRenderer>; raw: string }
|
||||
>()
|
||||
|
||||
function escape(text: string) {
|
||||
return text
|
||||
@@ -184,7 +187,7 @@ function disposeRenderedMarkdown(root: Element) {
|
||||
...Array.from(root.querySelectorAll<HTMLDivElement>("[data-markdown-block]")),
|
||||
]
|
||||
blocks.forEach((block) => {
|
||||
renderedMarkdown.get(block)?.dispose()
|
||||
renderedMarkdown.get(block)?.renderer.dispose()
|
||||
renderedMarkdown.delete(block)
|
||||
})
|
||||
}
|
||||
@@ -629,17 +632,21 @@ function updateBlock(container: HTMLDivElement, index: number, block: RenderedBl
|
||||
const html = source.innerHTML
|
||||
|
||||
if (existing) {
|
||||
const renderer = renderedMarkdown.get(existing)
|
||||
if (renderer) {
|
||||
renderer.update(html, block.mode === "live")
|
||||
const rendered = renderedMarkdown.get(existing)
|
||||
if (rendered) {
|
||||
rendered.renderer.update(html, block.mode === "live", rendered.raw !== block.raw)
|
||||
rendered.raw = block.raw
|
||||
return
|
||||
}
|
||||
existing.innerHTML = ""
|
||||
renderedMarkdown.set(existing, createMarkdownRenderer(existing, html, block.mode === "live"))
|
||||
renderedMarkdown.set(existing, {
|
||||
renderer: createMarkdownRenderer(existing, html, block.mode === "live"),
|
||||
raw: block.raw,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
renderedMarkdown.set(next, createMarkdownRenderer(next, html, block.mode === "live"))
|
||||
renderedMarkdown.set(next, { renderer: createMarkdownRenderer(next, html, block.mode === "live"), raw: block.raw })
|
||||
if (!current) {
|
||||
container.appendChild(next)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user