@lincyaw/viewer 1.0.0-rc.13 → 1.0.0-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/client/block.ts +4 -1
- package/src/client/code/code-view.ts +8 -4
- package/src/client/code/highlight.ts +15 -9
- package/src/client/code/output-parsers.ts +19 -0
- package/src/client/code/outputs.ts +7 -9
- package/src/client/deferred.ts +65 -0
- package/src/client/index.ts +1 -0
- package/src/client/markdown/handlers.ts +28 -2
- package/src/client/markdown/tree-cache.ts +2 -2
- package/src/client/views/present.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lincyaw/viewer",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.14",
|
|
4
4
|
"description": "Presmium's IR runtime: renders the IR in the browser, with runtime views and live updates",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
"react-dom": "18.3.1",
|
|
57
57
|
"shiki": "4.5.0",
|
|
58
58
|
"style-to-js": "2.0.2",
|
|
59
|
-
"@lincyaw/host": "1.0.0-rc.
|
|
60
|
-
"@lincyaw/kernel": "1.0.0-rc.
|
|
59
|
+
"@lincyaw/host": "1.0.0-rc.14",
|
|
60
|
+
"@lincyaw/kernel": "1.0.0-rc.14"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/hast": "3.0.5",
|
package/src/client/block.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ComponentNode, IRBlock, IRSection } from '@lincyaw/kernel'
|
|
|
6
6
|
import { FINAL_STEP } from '../domain/index.ts'
|
|
7
7
|
import { CODE_OWNED, CodeBlockView } from './code/code-block.ts'
|
|
8
8
|
import { contentMarks } from './content-marks.ts'
|
|
9
|
+
import { loadedParts } from './deferred.ts'
|
|
9
10
|
import { markdownToHast } from './markdown/handlers.ts'
|
|
10
11
|
import { PmMermaid } from './markdown/mermaid.ts'
|
|
11
12
|
import { hastToVNodes, MarkdownView } from './markdown/render.ts'
|
|
@@ -18,7 +19,7 @@ import { SECTION_CONTEXT } from './section-context.ts'
|
|
|
18
19
|
export interface BlockProps { block: IRBlock, section: IRSection, step: number, expanded?: boolean, localPreview?: boolean }
|
|
19
20
|
const COMPONENT_NODE = createContext<{ node: ComponentNode, section: string } | undefined>(undefined)
|
|
20
21
|
function fallbackOf(node: ComponentNode, section: string) { return node.fallback ? hastToVNodes(markdownToHast(node.fallback, section).children, {}) : [] }
|
|
21
|
-
export function PmComponentFallback() { const current = useContext(COMPONENT_NODE); return current ? h('div', { className: 'pm-component-fallback' }, fallbackOf(current.node, current.section)) : null }
|
|
22
|
+
export function PmComponentFallback() { const current = useContext(COMPONENT_NODE); useStore(loadedParts); return current ? h('div', { className: 'pm-component-fallback' }, fallbackOf(current.node, current.section)) : null }
|
|
22
23
|
function ComponentView(props: BlockProps) {
|
|
23
24
|
const node = props.block.node as ComponentNode
|
|
24
25
|
const printed = useStaticRender()
|
|
@@ -44,6 +45,8 @@ function content(props: BlockProps): ReactNode {
|
|
|
44
45
|
function BlockViewContent(props: BlockProps) {
|
|
45
46
|
const viewer = useContext(VIEWER); const el = useRef<HTMLDivElement>(null)
|
|
46
47
|
useStore(registryRevision)
|
|
48
|
+
// A part of the viewer the block needed and did not have yet (`deferred.ts`) has arrived.
|
|
49
|
+
useStore(loadedParts)
|
|
47
50
|
useEffect(() => el.current ? trackClicks(el.current, props.expanded ? FINAL_STEP : props.step) : undefined, [props.step, props.expanded, props.block])
|
|
48
51
|
const { block } = props
|
|
49
52
|
const visible = props.expanded || blockVisible(block, props.step)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useAppearance, usePrefersReducedMotion } from '@lincyaw/host'
|
|
2
|
-
import { ShikiMagicMove } from '@shikijs/magic-move/react'
|
|
3
2
|
import '@shikijs/magic-move/style.css'
|
|
4
3
|
import { useEffect, useState } from 'react'
|
|
4
|
+
import type { ComponentProps, ComponentType } from 'react'
|
|
5
|
+
import type { ShikiMagicMove } from '@shikijs/magic-move/react'
|
|
5
6
|
import type { Element, ElementContent, Root } from 'hast'
|
|
6
|
-
import { toHtml } from 'hast-util-to-html'
|
|
7
7
|
import { codeColumns } from '../../domain/index.ts'
|
|
8
8
|
import { highlighterFor, SHIKI_THEMES } from './highlight.ts'
|
|
9
9
|
import { h } from '../element.ts'
|
|
@@ -13,10 +13,14 @@ function lineElements(tree: Root): Element[] { const pre = tree.children.find((n
|
|
|
13
13
|
export function CodeView({ lang = '', value, highlight }: { lang?: string, value: string, highlight?: number[] }) {
|
|
14
14
|
const shiki = useHighlighter(lang); const code = value.replace(/\n$/, ''); const lines = new Set(highlight ?? [])
|
|
15
15
|
let html: string | undefined
|
|
16
|
-
if (shiki) { const tree = shiki.highlighter.codeToHast(code, { lang: shiki.lang, themes: SHIKI_THEMES, defaultColor: false }); lineElements(tree).forEach((line, i) => line.properties.class = lines.has(i + 1) ? 'line highlighted' : 'line'); html = toHtml(tree) }
|
|
16
|
+
if (shiki) { const tree = shiki.highlighter.codeToHast(code, { lang: shiki.lang, themes: SHIKI_THEMES, defaultColor: false }); lineElements(tree).forEach((line, i) => line.properties.class = lines.has(i + 1) ? 'line highlighted' : 'line'); html = shiki.toHtml(tree) }
|
|
17
17
|
const attrs = { className: `pm-code${lines.size ? ' pm-code--highlight' : ''}`, style: { '--pm-code-columns': codeColumns(code) }, 'data-lang': lang || undefined }
|
|
18
18
|
if (html) return h('div', { ...attrs, innerHTML: html })
|
|
19
19
|
const rows = code.split('\n').flatMap((text, i) => [h('span', { key: i, className: `line${lines.has(i + 1) ? ' highlighted' : ''}` }, text), '\n'])
|
|
20
20
|
return h('div', attrs, h('pre', { className: 'shiki' }, h('code', {}, rows.slice(0, -1))))
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
type MagicMove = ComponentType<ComponentProps<typeof ShikiMagicMove>>
|
|
23
|
+
let magicMove: Promise<MagicMove> | undefined
|
|
24
|
+
/** The animation between variants, loaded once a page shows one (until then, and on the server, the listing alone). */
|
|
25
|
+
function useMagicMove(wanted: boolean) { const [move, setMove] = useState<MagicMove>(); useEffect(() => { if (!wanted || move) return; let active = true; magicMove ??= import('@shikijs/magic-move/react').then(m => m.ShikiMagicMove as MagicMove); void magicMove.then(value => { if (active) setMove(() => value) }).catch(() => {}); return () => { active = false } }, [wanted]); return move }
|
|
26
|
+
export function VariantView({ lang = '', value }: { lang?: string, value: string }) { const loaded = useHighlighter(lang); const reduced = usePrefersReducedMotion(); const { dark } = useAppearance(); const ShikiMagicMove = useMagicMove(!reduced); if (!loaded || reduced || !ShikiMagicMove) return h(CodeView, { lang, value }); const code = value.replace(/\n$/, ''); return h('div', { className: 'pm-code pm-code--magic-move', style: { '--pm-code-columns': codeColumns(code) }, 'data-lang': lang || undefined }, h(ShikiMagicMove, { highlighter: loaded.highlighter, lang: loaded.lang, theme: dark ? SHIKI_THEMES.dark : SHIKI_THEMES.light, code })) }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { languageId } from '@lincyaw/kernel'
|
|
2
2
|
import type { HighlighterCore } from 'shiki/core'
|
|
3
|
+
import type { toHtml } from 'hast-util-to-html'
|
|
3
4
|
|
|
4
5
|
type Loader = () => Promise<unknown>
|
|
5
6
|
|
|
@@ -43,17 +44,22 @@ const ALIASES: Record<string, string> = {
|
|
|
43
44
|
|
|
44
45
|
export const SHIKI_THEMES = { light: 'github-light', dark: 'github-dark' } as const
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
/** Shiki, with what writes its trees as HTML. */
|
|
48
|
+
interface Loaded { highlighter: HighlighterCore, toHtml: typeof toHtml }
|
|
49
|
+
|
|
50
|
+
let highlighter: Promise<Loaded> | undefined
|
|
47
51
|
const loading = new Map<string, Promise<void>>()
|
|
48
52
|
|
|
49
|
-
function load(): Promise<
|
|
53
|
+
function load(): Promise<Loaded> {
|
|
50
54
|
if (import.meta.env.SSR) return new Promise(() => {})
|
|
51
|
-
highlighter ??= Promise.all([import('shiki/core'), import('shiki/engine/javascript')]).then(([core, engine]) =>
|
|
52
|
-
core.createHighlighterCore({
|
|
55
|
+
highlighter ??= Promise.all([import('shiki/core'), import('shiki/engine/javascript'), import('hast-util-to-html')]).then(async ([core, engine, html]) => ({
|
|
56
|
+
highlighter: await core.createHighlighterCore({
|
|
53
57
|
themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark.mjs')],
|
|
54
58
|
langs: [],
|
|
55
59
|
engine: engine.createJavaScriptRegexEngine({ forgiving: true }),
|
|
56
|
-
})
|
|
60
|
+
}),
|
|
61
|
+
toHtml: html.toHtml,
|
|
62
|
+
}))
|
|
57
63
|
return highlighter
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -63,11 +69,11 @@ function grammarOf(lang: string): string | undefined {
|
|
|
63
69
|
return name in GRAMMARS ? name : undefined
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
export async function highlighterFor(lang: string): Promise<
|
|
72
|
+
export async function highlighterFor(lang: string): Promise<Loaded & { lang: string } | undefined> {
|
|
67
73
|
const name = grammarOf(lang)
|
|
68
74
|
if (!name) return undefined
|
|
69
|
-
const
|
|
70
|
-
if (!loading.has(name)) loading.set(name,
|
|
75
|
+
const loaded = await load()
|
|
76
|
+
if (!loading.has(name)) loading.set(name, loaded.highlighter.loadLanguage(GRAMMARS[name]!() as never))
|
|
71
77
|
await loading.get(name)
|
|
72
|
-
return {
|
|
78
|
+
return { ...loaded, lang: name }
|
|
73
79
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { htmlAsText } from '@lincyaw/kernel'
|
|
2
|
+
import type { MdastNode } from '@lincyaw/kernel'
|
|
3
|
+
import { AnsiUp } from 'ansi_up'
|
|
4
|
+
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
5
|
+
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
|
6
|
+
import { gfm } from 'micromark-extension-gfm'
|
|
7
|
+
|
|
8
|
+
/** What the outputs of code runs are read with, loaded once a page shows one (`deferred.ts`). */
|
|
9
|
+
export interface OutputParsers {
|
|
10
|
+
/** Terminal text with its ANSI colours, as HTML. */
|
|
11
|
+
ansi(text: string): string
|
|
12
|
+
/** A Markdown output as MDAST, its raw HTML as text. */
|
|
13
|
+
markdown(text: string): MdastNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const OUTPUT_PARSERS: OutputParsers = {
|
|
17
|
+
ansi: text => new AnsiUp().ansi_to_html(text),
|
|
18
|
+
markdown: text => htmlAsText(fromMarkdown(text, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] }) as unknown as MdastNode),
|
|
19
|
+
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { currentLocale } from '@lincyaw/host'
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
import { AnsiUp } from 'ansi_up'
|
|
5
|
-
import { fromMarkdown } from 'mdast-util-from-markdown'
|
|
6
|
-
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
|
7
|
-
import { gfm } from 'micromark-extension-gfm'
|
|
2
|
+
import { escapeHtml, isProseRun } from '@lincyaw/kernel'
|
|
3
|
+
import type { RunOutput, RunResult } from '@lincyaw/kernel'
|
|
8
4
|
import type { ReactElement, ReactNode } from 'react'
|
|
9
5
|
import { useEffect, useState } from 'react'
|
|
10
6
|
import { h } from '../element.ts'
|
|
11
7
|
import { outputNotes } from '../../domain/index.ts'
|
|
8
|
+
import { codeOutputParsers } from '../deferred.ts'
|
|
12
9
|
import { markdownToHast } from '../markdown/handlers.ts'
|
|
13
10
|
import { PmMath } from '../markdown/math.ts'
|
|
14
11
|
import { hastToVNodes } from '../markdown/render.ts'
|
|
@@ -20,13 +17,14 @@ function text(value: unknown): string {
|
|
|
20
17
|
return Array.isArray(value) ? value.join('') : typeof value === 'string' ? value : JSON.stringify(value, null, 2)
|
|
21
18
|
}
|
|
22
19
|
|
|
20
|
+
// Until the parsers are loaded (`deferred.ts`), terminal text shows without its colours and Markdown as written.
|
|
23
21
|
function ansi(value: string): string {
|
|
24
|
-
return
|
|
22
|
+
return codeOutputParsers()?.ansi(value) ?? escapeHtml(value)
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
function markdownOutput(markdown: string, prefix: string): ReactNode[] {
|
|
28
|
-
const
|
|
29
|
-
return hastToVNodes(markdownToHast(
|
|
26
|
+
const parsers = codeOutputParsers()
|
|
27
|
+
return parsers ? hastToVNodes(markdownToHast(parsers.markdown(markdown), prefix).children, {}) : [h('pre', { key: 0, className: 'pm-output__text' }, markdown)]
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
let purify: Promise<(html: string) => string> | undefined
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createStore } from '@lincyaw/host'
|
|
2
|
+
import { docBlocks } from '@lincyaw/kernel'
|
|
3
|
+
import type { IRDocument, MdastNode } from '@lincyaw/kernel'
|
|
4
|
+
import type { raw } from 'hast-util-raw'
|
|
5
|
+
import type { OutputParsers } from './code/output-parsers.ts'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The parts of the viewer that only some documents use, loaded apart from the app: the HTML parser for HTML written in
|
|
9
|
+
* Markdown, and the parsers of code outputs (ANSI colours, Markdown). A page loads the parts its document uses before it
|
|
10
|
+
* first renders (`loadDocumentParts`), so that the server's render and the browser's first one agree; a part a later
|
|
11
|
+
* change of the document needs loads then, and the blocks render again once it is there.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type RawHtml = typeof raw
|
|
15
|
+
|
|
16
|
+
/** Counts the parts loaded, so that a block shown without one renders again once it is there. */
|
|
17
|
+
export const loadedParts = createStore(0)
|
|
18
|
+
let rawHtml: RawHtml | undefined
|
|
19
|
+
let outputParsers: OutputParsers | undefined
|
|
20
|
+
const loading = new Map<string, Promise<void>>()
|
|
21
|
+
|
|
22
|
+
function load(name: string, start: () => Promise<void>): Promise<void> {
|
|
23
|
+
let known = loading.get(name)
|
|
24
|
+
if (!known) {
|
|
25
|
+
known = start().then(
|
|
26
|
+
() => loadedParts.set(loadedParts.getSnapshot() + 1),
|
|
27
|
+
(error: unknown) => {
|
|
28
|
+
loading.delete(name)
|
|
29
|
+
console.error(`[presmium] the viewer's ${name} did not load`, error)
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
loading.set(name, known)
|
|
33
|
+
}
|
|
34
|
+
return known
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const loadRawHtml = () => load('HTML parser', async () => { rawHtml = (await import('hast-util-raw')).raw })
|
|
38
|
+
const loadOutputParsers = () => load('output parsers', async () => { outputParsers = (await import('./code/output-parsers.ts')).OUTPUT_PARSERS })
|
|
39
|
+
|
|
40
|
+
/** The HTML parser if it is loaded; else it starts loading and the caller shows the HTML as missing meanwhile. */
|
|
41
|
+
export function rawHtmlParser(): RawHtml | undefined {
|
|
42
|
+
if (!rawHtml) void loadRawHtml()
|
|
43
|
+
return rawHtml
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The parsers of code outputs if they are loaded; else they start loading and the caller shows plain text meanwhile. */
|
|
47
|
+
export function codeOutputParsers(): OutputParsers | undefined {
|
|
48
|
+
if (!outputParsers) void loadOutputParsers()
|
|
49
|
+
return outputParsers
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasHtml(node: MdastNode | undefined): boolean {
|
|
53
|
+
return node !== undefined && (node.type === 'html' || (node.children?.some(hasHtml) ?? false))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Loads the parts `doc` uses (`import.meta.env.SSR` included: the server renders the document with them too). */
|
|
57
|
+
export async function loadDocumentParts(doc: IRDocument): Promise<void> {
|
|
58
|
+
const parts: Promise<void>[] = []
|
|
59
|
+
for (const { block } of docBlocks(doc)) {
|
|
60
|
+
const node = block.node
|
|
61
|
+
if (!rawHtml && ((node.type === 'markdown' && hasHtml(node.mdast)) || (node.type === 'component' && hasHtml(node.fallback)))) parts.push(loadRawHtml())
|
|
62
|
+
if (!outputParsers && node.type === 'code' && node.outputs?.length) parts.push(loadOutputParsers())
|
|
63
|
+
}
|
|
64
|
+
await Promise.all(parts)
|
|
65
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ Object.entries(DOCUMENT_LAYOUT_COMPONENTS).forEach(([name, component]) => regist
|
|
|
22
22
|
export function ViewerProvider({ viewer, children }: { viewer: Viewer, children?: ReactNode }) { return createElement(VIEWER.Provider, { value: viewer }, children) }
|
|
23
23
|
|
|
24
24
|
export { BlockView, BlockPreview, PmComponentFallback } from './block.ts'
|
|
25
|
+
export { loadDocumentParts } from './deferred.ts'
|
|
25
26
|
export { syncViewerWithOperations } from './operations.ts'
|
|
26
27
|
export { findView, findComponent, listViews, registerComponent, registryRevision } from './registry.ts'
|
|
27
28
|
export type { NodeRenderer, ViewDefinition, ViewerAction, ViewerLayer } from './registry.ts'
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isButton, roleMark, safeDecodeURIComponent, urlSlug } from '@lincyaw/kernel'
|
|
2
2
|
import type { MdastNode } from '@lincyaw/kernel'
|
|
3
3
|
import type { Element, ElementContent, Nodes, Root } from 'hast'
|
|
4
|
-
import { raw } from 'hast-util-raw'
|
|
5
4
|
import type { Handler, Handlers, State } from 'mdast-util-to-hast'
|
|
6
5
|
import { defaultHandlers, toHast } from 'mdast-util-to-hast'
|
|
6
|
+
import { rawHtmlParser } from '../deferred.ts'
|
|
7
7
|
import { viewerMessages } from '../viewer-messages.ts'
|
|
8
8
|
import { footnoteId } from '../viewer-state.ts'
|
|
9
9
|
import type { Viewer } from '../viewer-state.ts'
|
|
@@ -145,9 +145,35 @@ function textOf(node: ElementContent | Root): string {
|
|
|
145
145
|
return 'children' in node ? node.children.map(c => textOf(c as ElementContent)).join('') : ''
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function hasRaw(node: Nodes): boolean {
|
|
149
|
+
return node.type === 'raw' || ('children' in node && node.children.some(child => hasRaw(child as Nodes)))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Joins the neighbouring texts of a tree, as parsing it as HTML does (`hast-util-raw`), so that it renders as one text. */
|
|
153
|
+
function joinTexts(node: Nodes): void {
|
|
154
|
+
if (!('children' in node)) return
|
|
155
|
+
const children: Nodes[] = []
|
|
156
|
+
for (const child of node.children as Nodes[]) {
|
|
157
|
+
const last = children.at(-1)
|
|
158
|
+
if (child.type === 'text' && last?.type === 'text') last.value += child.value
|
|
159
|
+
else children.push(child)
|
|
160
|
+
joinTexts(child)
|
|
161
|
+
}
|
|
162
|
+
node.children = children as never
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** The trees made while the HTML parser was loading, without their HTML; they are made again once it is there. */
|
|
166
|
+
const partial = new WeakSet<Root>()
|
|
167
|
+
export const isPartialTree = (tree: Root): boolean => partial.has(tree)
|
|
168
|
+
|
|
148
169
|
export function markdownToHast(mdast: MdastNode, prefix: string, links: MarkdownLinks = {}): Root {
|
|
149
170
|
const html = toHast(mdast as never, { allowDangerousHtml: true, handlers: handlers(links), unknownHandler: unknown as never }) as Nodes
|
|
150
|
-
|
|
171
|
+
// HTML written in the Markdown is parsed with the rest (`hast-util-raw`); without any, the tree needs no HTML parser.
|
|
172
|
+
const written = hasRaw(html)
|
|
173
|
+
const raw = written ? rawHtmlParser() : undefined
|
|
174
|
+
if (!raw) joinTexts(html)
|
|
175
|
+
const tree = (raw ? raw(html, { passThrough: VIEWER_NODES }) : html) as Root
|
|
176
|
+
if (written && !raw) partial.add(tree)
|
|
151
177
|
let item = 0
|
|
152
178
|
for (const child of tree.children) {
|
|
153
179
|
if (child.type !== 'element') continue
|
|
@@ -2,7 +2,7 @@ import type { IRDocument, MdastNode } from '@lincyaw/kernel'
|
|
|
2
2
|
import type { Root } from 'hast'
|
|
3
3
|
import type { Viewer } from '../viewer-state.ts'
|
|
4
4
|
import { footnoteSections } from '../../domain/index.ts'
|
|
5
|
-
import { markdownToHast } from './handlers.ts'
|
|
5
|
+
import { isPartialTree, markdownToHast } from './handlers.ts'
|
|
6
6
|
import type { MarkdownLinks } from './handlers.ts'
|
|
7
7
|
|
|
8
8
|
interface CachedTree {
|
|
@@ -58,7 +58,7 @@ export function cachedTree(viewer: Viewer, doc: IRDocument, node: MdastNode, pre
|
|
|
58
58
|
const tree = markdownToHast(node, prefix, links)
|
|
59
59
|
// Only immutable HAST survives component unmounts; no VNodes or mounted resources are retained.
|
|
60
60
|
const bytes = JSON.stringify(tree).length * 2 // UTF-16 JSON size is a soft heap estimate.
|
|
61
|
-
if (bytes > TREE_BYTES) return tree
|
|
61
|
+
if (bytes > TREE_BYTES || isPartialTree(tree)) return tree
|
|
62
62
|
const entry: CachedTree = { node, key, tree, bytes }
|
|
63
63
|
const variants = cache.nodes.get(node) ?? new Map<string, CachedTree>()
|
|
64
64
|
variants.set(key, entry)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FocusScope } from '@radix-ui/react-focus-scope'
|
|
2
|
-
import { PmApprovals, PmCaption, PmCaptionProvider, PmIcon, TIMED_CAPTIONS, useService, PLAYBACK_CONTROL, presmiumService } from '@lincyaw/host'
|
|
2
|
+
import { PAGE_READINESS, PmApprovals, PmCaption, PmCaptionProvider, PmIcon, TIMED_CAPTIONS, useService, PLAYBACK_CONTROL, presmiumService } from '@lincyaw/host'
|
|
3
|
+
import { notifyEmbedder } from '@lincyaw/kernel'
|
|
3
4
|
import { useEffect, useRef, useState } from 'react'
|
|
4
5
|
import { useStore } from '@lincyaw/host'
|
|
5
6
|
import { h } from '../element.ts'
|
|
@@ -39,6 +40,17 @@ export function PresentView() {
|
|
|
39
40
|
const [fullscreen, setFullscreen] = useState(false)
|
|
40
41
|
const [captions, setCaptions] = useState(true)
|
|
41
42
|
const timedCaptions = useStore(useService(TIMED_CAPTIONS)?.active, false)
|
|
43
|
+
// The page that embeds the presentation learns when the current slide is drawn: no component still loading,
|
|
44
|
+
// checked after the slide's components have mounted and taken their holds.
|
|
45
|
+
const loading = useStore(useService(PAGE_READINESS)?.pending, 0)
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (loading > 0) {
|
|
48
|
+
notifyEmbedder({ drawn: false, section: section?.id, step })
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
const frame = requestAnimationFrame(() => notifyEmbedder({ drawn: true, section: section?.id, step }))
|
|
52
|
+
return () => cancelAnimationFrame(frame)
|
|
53
|
+
}, [loading, section?.id, step])
|
|
42
54
|
const overview = useStore(overviewOpen)
|
|
43
55
|
const doc = useStore(viewer.doc)
|
|
44
56
|
useStore(viewer.options.confirmedRevision)
|