@lincyaw/viewer 0.0.0-stage → 1.0.0-rc.0

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/src/present.ts ADDED
@@ -0,0 +1,327 @@
1
+ import { FocusScope } from 'reka-ui'
2
+ import { PmApprovals } from '@lincyaw/host'
3
+ import { blockText, sectionNarration } from '@lincyaw/kernel'
4
+ import type { IRSection } from '@lincyaw/kernel'
5
+ import type { Ref } from 'vue'
6
+ import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, provide, ref, shallowRef, watch } from 'vue'
7
+ import { BlockView } from './block.ts'
8
+ import { SECTION_CONTEXT } from './context.ts'
9
+ import { nextPosition } from './domain/index.ts'
10
+ import { messages } from './i18n.ts'
11
+ import { listActions, listLayers } from './registry.ts'
12
+ import type { ViewerAction } from './registry.ts'
13
+ import { useViewer } from './viewer.ts'
14
+ import type { Viewer } from './viewer.ts'
15
+
16
+ const overview = ref(false)
17
+
18
+ export const BUILTIN_ACTIONS: readonly ViewerAction[] = [
19
+ { id: 'next', keys: ['ArrowRight', 'ArrowDown', 'PageDown', ' '], run: (viewer) => { viewer.next() } },
20
+ { id: 'prev', keys: ['ArrowLeft', 'ArrowUp', 'PageUp', 'Backspace'], run: (viewer) => { viewer.prev() } },
21
+ { id: 'first', keys: ['Home'], run: viewer => viewer.go({ section: viewer.doc.value.sections[0]?.id ?? '', step: 0 }) },
22
+ { id: 'last', keys: ['End'], run: viewer => viewer.go({ section: viewer.doc.value.sections.at(-1)?.id ?? '', step: Number.POSITIVE_INFINITY }) },
23
+ { id: 'exit', keys: ['Escape'], views: ['present'], run: viewer => viewer.setView(viewer.pageView.value) },
24
+ {
25
+ id: 'presenter',
26
+ keys: ['p'],
27
+ views: ['present'],
28
+ run: viewer => viewer.options.openPresenter ? viewer.options.openPresenter(viewer.position.value) : false,
29
+ },
30
+ { id: 'overview', keys: ['o'], views: ['present'], run: () => { overview.value = !overview.value } },
31
+ ]
32
+
33
+ function typing(target: EventTarget | null): boolean {
34
+ return Boolean((target as HTMLElement | null)?.closest?.('input, textarea, select, [contenteditable], [role="dialog"], [role="menu"], [role="tablist"], [role="slider"]'))
35
+ }
36
+
37
+ function useActions(viewer: Viewer, view: string): void {
38
+ const onKey = (event: KeyboardEvent) => {
39
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || typing(event.target)) return
40
+ for (const action of listActions(view)) {
41
+ if (!action.keys.includes(event.key) || action.run(viewer) === false) continue
42
+ event.preventDefault()
43
+ return
44
+ }
45
+ }
46
+ onMounted(() => {
47
+ window.addEventListener('keydown', onKey)
48
+ document.documentElement.classList.add('pm-presenting')
49
+ })
50
+ onBeforeUnmount(() => {
51
+ window.removeEventListener('keydown', onKey)
52
+ document.documentElement.classList.remove('pm-presenting')
53
+ })
54
+ }
55
+
56
+ // The page stays mounted to preserve its scroll position; only the overlay is interactive.
57
+ function useModal(root: Readonly<Ref<HTMLElement | undefined>>): void {
58
+ const inert: Element[] = []
59
+ onMounted(() => {
60
+ const el = root.value?.closest('[data-pm-overlay-scope]')
61
+ if (!el) return
62
+ for (let node: Element = el; node.parentElement && node !== document.body; node = node.parentElement) {
63
+ for (const sibling of node.parentElement.children) {
64
+ if (sibling === node || sibling.hasAttribute('inert') || ['SCRIPT', 'STYLE', 'LINK'].includes(sibling.tagName)) continue
65
+ sibling.setAttribute('inert', '')
66
+ inert.push(sibling)
67
+ }
68
+ }
69
+ })
70
+ onBeforeUnmount(() => inert.splice(0).forEach(el => el.removeAttribute('inert')))
71
+ }
72
+
73
+ function focusScope(root: Readonly<Ref<HTMLElement | undefined>>) {
74
+ return { 'data-pm-overlay-scope': '', style: { display: 'contents' }, loop: true, trapped: true, onMountAutoFocus: (event: Event) => {
75
+ event.preventDefault()
76
+ root.value?.focus({ preventScroll: true })
77
+ } }
78
+ }
79
+
80
+ interface Fitted {
81
+ width?: number
82
+ scale: number
83
+ height: number
84
+ shift: number
85
+ }
86
+
87
+ function useFit(frame: Readonly<Ref<HTMLElement | undefined>>, content: Readonly<Ref<HTMLElement | undefined>>) {
88
+ const fitted = ref<Fitted>()
89
+ const fit = () => {
90
+ const [outer, inner] = [frame.value, content.value]
91
+ if (!outer || !inner) return
92
+ const style = getComputedStyle(outer)
93
+ const available = {
94
+ height: outer.clientHeight - Number.parseFloat(style.paddingTop) - Number.parseFloat(style.paddingBottom) - 1,
95
+ width: outer.clientWidth - Number.parseFloat(style.paddingLeft) - Number.parseFloat(style.paddingRight),
96
+ }
97
+ if (available.height <= 0 || available.width <= 0) return
98
+ inner.style.width = ''
99
+ const wide = Math.max(0, ...[...inner.querySelectorAll<HTMLElement>('pre, table, .pm-block, .pm-mermaid, .pm-math--display')].map(el => el.scrollWidth - el.clientWidth))
100
+ const width = available.width + wide
101
+ if (wide > 0) inner.style.width = `${width}px`
102
+ const natural = inner.offsetHeight
103
+ const scale = Math.min(1, available.width / width, available.height / natural)
104
+ const next = scale < 1 ? { width: wide > 0 ? width : undefined, scale, height: natural * scale, shift: (available.width - width * scale) / 2 } : undefined
105
+ if (JSON.stringify(next) !== JSON.stringify(fitted.value)) fitted.value = next
106
+ else if (next?.width) inner.style.width = `${next.width}px`
107
+ }
108
+ let observer: ResizeObserver | undefined
109
+ onMounted(() => {
110
+ observer = new ResizeObserver(fit)
111
+ if (frame.value) observer.observe(frame.value)
112
+ })
113
+ watch(content, (el, old) => {
114
+ if (old) observer?.unobserve(old)
115
+ if (el) observer?.observe(el)
116
+ }, { flush: 'post' })
117
+ onBeforeUnmount(() => observer?.disconnect())
118
+ return fitted
119
+ }
120
+
121
+ function useOverlayBar(bar: Readonly<Ref<HTMLElement | undefined>>): void {
122
+ const root = () => document.documentElement.style
123
+ let observer: ResizeObserver | undefined
124
+ onMounted(() => {
125
+ const el = bar.value
126
+ if (!el) return
127
+ const update = () => root().setProperty('--pm-overlay-bar', `${el.offsetHeight}px`)
128
+ update()
129
+ observer = new ResizeObserver(update)
130
+ observer.observe(el)
131
+ })
132
+ onBeforeUnmount(() => {
133
+ observer?.disconnect()
134
+ root().removeProperty('--pm-overlay-bar')
135
+ })
136
+ }
137
+
138
+ function useCurrent(viewer: Viewer, view: string) {
139
+ const section = computed(() => viewer.section(viewer.position.value.section))
140
+ const step = computed(() => viewer.position.value.step)
141
+ provide(SECTION_CONTEXT, { section: section as Readonly<Ref<IRSection>>, step, view: computed(() => view) })
142
+ return { section, step }
143
+ }
144
+
145
+ function visuals(section: IRSection, step: number) {
146
+ return section.blocks.filter(b => b.role === 'visual').map(block => h(BlockView, { key: `${section.id}/${block.id}`, block, section, step }))
147
+ }
148
+
149
+ function button(label: string, onClick: () => void) {
150
+ return h('button', { type: 'button', class: 'pm-present__button', onClick }, label)
151
+ }
152
+
153
+ function layers(view: string) {
154
+ return listLayers(view).map(layer => h(layer.component, { key: layer.id, view }))
155
+ }
156
+
157
+ function moveInOverview(root: HTMLElement | undefined, key: string): boolean {
158
+ const items = [...root?.querySelectorAll<HTMLElement>('.pm-overview__item') ?? []]
159
+ if (!items.length) return false
160
+ const at = Math.max(items.indexOf(document.activeElement as HTMLElement), 0)
161
+ const columns = items.filter(item => item.offsetTop === items[0]!.offsetTop).length
162
+ const moves: Record<string, number> = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: columns, ArrowUp: -columns, Home: -at, End: items.length }
163
+ if (!(key in moves)) return false
164
+ items[Math.min(Math.max(at + moves[key]!, 0), items.length - 1)]!.focus()
165
+ return true
166
+ }
167
+
168
+ export const PresentView = defineComponent({
169
+ name: 'PmPresentView',
170
+ setup() {
171
+ const viewer = useViewer()
172
+ const { section, step } = useCurrent(viewer, 'present')
173
+ const root = shallowRef<HTMLElement>()
174
+ const frame = shallowRef<HTMLElement>()
175
+ const content = shallowRef<HTMLElement>()
176
+ const bar = shallowRef<HTMLElement>()
177
+ const fitted = useFit(frame, content)
178
+ useOverlayBar(bar)
179
+ overview.value = false
180
+ const closeOverview = () => {
181
+ overview.value = false
182
+ root.value?.focus()
183
+ }
184
+ useActions(viewer, 'present')
185
+ const onOverviewKey = (event: KeyboardEvent) => {
186
+ if (!overview.value || event.key === 'Tab' || event.altKey || event.ctrlKey || event.metaKey) return
187
+ event.stopImmediatePropagation()
188
+ if (event.key === 'Escape' || event.key === 'o') closeOverview()
189
+ else if (!moveInOverview(root.value, event.key)) return
190
+ event.preventDefault()
191
+ }
192
+ useModal(root)
193
+ onMounted(() => {
194
+ window.addEventListener('keydown', onOverviewKey, { capture: true })
195
+ })
196
+ onBeforeUnmount(() => {
197
+ window.removeEventListener('keydown', onOverviewKey, { capture: true })
198
+ overview.value = false
199
+ })
200
+ watch(overview, async (open) => {
201
+ if (!open) return
202
+ await nextTick()
203
+ root.value?.querySelector<HTMLElement>('.pm-overview__item[aria-current="true"]')?.focus()
204
+ })
205
+ const jump = (id: string) => {
206
+ viewer.go({ section: id, step: 0 })
207
+ closeOverview()
208
+ }
209
+
210
+ return () => {
211
+ const current = section.value
212
+ if (!current) return null
213
+ const t = messages()
214
+ const { sections } = viewer.doc.value
215
+ return h(FocusScope, focusScope(root), () => [h('div', {
216
+ 'ref': root,
217
+ 'class': 'pm-present',
218
+ 'role': 'region',
219
+ 'aria-label': t.present.region,
220
+ 'tabindex': -1,
221
+ 'data-section': current.id,
222
+ 'data-step': step.value,
223
+ }, [
224
+ h('div', { ref: frame, class: 'pm-present__slide' }, [
225
+ h('div', { class: 'pm-present__fit', style: fitted.value ? { height: `${fitted.value.height}px`, overflow: 'clip' } : undefined }, h('div', {
226
+ key: current.id,
227
+ ref: content,
228
+ class: 'pm-present__content',
229
+ style: fitted.value
230
+ ? { width: fitted.value.width ? `${fitted.value.width}px` : undefined, transform: `translateX(${fitted.value.shift}px) scale(${fitted.value.scale})` }
231
+ : undefined,
232
+ }, [
233
+ h('h1', { class: 'pm-present__title' }, current.title),
234
+ ...visuals(current, step.value),
235
+ ])),
236
+ overview.value
237
+ ? h('div', { 'class': 'pm-overview', 'role': 'dialog', 'aria-label': t.present.overview }, h('ol', { class: 'pm-overview__grid' }, sections.map(s => h('li', { key: s.id }, h('button', {
238
+ 'type': 'button',
239
+ 'class': 'pm-overview__item',
240
+ 'data-section': s.id,
241
+ 'aria-current': s.id === current.id ? 'true' : undefined,
242
+ 'onClick': () => jump(s.id),
243
+ }, [h('span', { class: 'pm-overview__index' }, String(s.index)), h('span', { class: 'pm-overview__title' }, s.title)])))))
244
+ : null,
245
+ ]),
246
+ h('div', { ref: bar, class: 'pm-present__bar' }, [
247
+ h('span', { class: 'pm-present__counter' }, t.present.counter(current.index, sections.length, step.value, current.steps)),
248
+ button(t.present.prev, () => viewer.prev()),
249
+ button(t.present.next, () => viewer.next()),
250
+ button(t.present.overview, () => overview.value = !overview.value),
251
+ viewer.options.openPresenter ? button(t.present.presenter, () => viewer.options.openPresenter!(viewer.position.value)) : null,
252
+ button(t.present.fullscreen, () => void root.value?.requestFullscreen?.().catch(() => {})),
253
+ button(t.present.exit, () => viewer.setView(viewer.pageView.value)),
254
+ h(PmApprovals, { placement: 'above' }),
255
+ ]),
256
+ ]), ...layers('present')])
257
+ }
258
+ },
259
+ })
260
+
261
+ function clock(ms: number): string {
262
+ const s = Math.floor(ms / 1000)
263
+ return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
264
+ }
265
+
266
+ export const PresenterView = defineComponent({
267
+ name: 'PmPresenterView',
268
+ setup() {
269
+ const viewer = useViewer()
270
+ const { section, step } = useCurrent(viewer, 'presenter')
271
+ const root = shallowRef<HTMLElement>()
272
+ const bar = shallowRef<HTMLElement>()
273
+ useActions(viewer, 'presenter')
274
+ useOverlayBar(bar)
275
+ useModal(root)
276
+ const started = ref(Date.now())
277
+ const now = ref(Date.now())
278
+ let timer: ReturnType<typeof setInterval> | undefined
279
+ onMounted(() => {
280
+ timer = setInterval(() => now.value = Date.now(), 1000)
281
+ })
282
+ onBeforeUnmount(() => clearInterval(timer))
283
+
284
+ const upcoming = computed(() => {
285
+ const t = messages()
286
+ const next = nextPosition(viewer.doc.value, viewer.position.value)
287
+ if (!next) return t.presenter.end
288
+ const target = viewer.section(next.section)!
289
+ if (next.section !== section.value?.id) return t.presenter.nextSection(target.title)
290
+ return sectionNarration(target).filter(block => block.at === next.step).map(blockText).join('\n') || t.presenter.nextStep(next.step)
291
+ })
292
+
293
+ return () => {
294
+ const current = section.value
295
+ if (!current) return null
296
+ const t = messages()
297
+ const notes = sectionNarration(current)
298
+ return h(FocusScope, focusScope(root), () => [h('div', { 'ref': root, 'tabindex': -1, 'class': 'pm-presenter', 'data-section': current.id, 'data-step': step.value }, [
299
+ h('div', { ref: bar, class: 'pm-presenter__bar' }, [
300
+ h('strong', { class: 'pm-presenter__title' }, `${current.index}. ${current.title}`),
301
+ current.steps > 0 ? h('span', { class: 'pm-presenter__counter' }, t.presenter.step(step.value, current.steps)) : null,
302
+ h('span', { 'class': 'pm-presenter__timer', 'aria-label': t.presenter.timer }, clock(now.value - started.value)),
303
+ button(t.presenter.restart, () => started.value = now.value = Date.now()),
304
+ button(t.present.prev, () => viewer.prev()),
305
+ button(t.present.next, () => viewer.next()),
306
+ h(PmApprovals),
307
+ ]),
308
+ h('div', { class: 'pm-presenter__main' }, [
309
+ h('div', { key: current.id, class: 'pm-presenter__slide' }, [h('h1', { class: 'pm-present__title' }, current.title), ...visuals(current, step.value)]),
310
+ h('div', { class: 'pm-presenter__side' }, [
311
+ h('p', { class: 'pm-presenter__label' }, t.presenter.notes),
312
+ h('div', { class: 'pm-presenter__notes' }, notes.length
313
+ ? notes.map(block => h('div', {
314
+ 'key': block.id,
315
+ 'class': ['pm-presenter__note', { 'pm-current': block.at === step.value }],
316
+ 'data-step': block.at,
317
+ 'aria-current': block.at === step.value ? 'step' : undefined,
318
+ }, [h(BlockView, { block, section: current, step: step.value, expanded: true })]))
319
+ : [h('p', { class: 'pm-presenter__empty' }, t.presenter.noNotes)]),
320
+ h('p', { class: 'pm-presenter__label' }, t.presenter.upcoming),
321
+ h('p', { class: 'pm-presenter__next' }, upcoming.value),
322
+ ]),
323
+ ]),
324
+ ]), ...layers('presenter')])
325
+ }
326
+ },
327
+ })
@@ -0,0 +1,81 @@
1
+ import type { Component } from 'vue'
2
+ import { markRaw, shallowReactive } from 'vue'
3
+ import type { Viewer } from './viewer.ts'
4
+
5
+ export interface ViewDefinition {
6
+ id: string
7
+ label: string
8
+ kind: 'page' | 'overlay'
9
+ component: Component
10
+ hidden?: boolean
11
+ }
12
+
13
+ export type NodeRenderer = Component
14
+
15
+ export interface ViewerAction {
16
+ id: string
17
+ keys: readonly string[]
18
+ run(viewer: Viewer): boolean | void
19
+ views?: readonly string[]
20
+ }
21
+
22
+ export interface ViewerLayer {
23
+ id: string
24
+ component: Component
25
+ views?: readonly string[]
26
+ }
27
+
28
+ export const OVERLAY_VIEWS: readonly string[] = ['present', 'presenter']
29
+
30
+ const views = shallowReactive(new Map<string, ViewDefinition>())
31
+ const nodeRenderers = shallowReactive(new Map<string, NodeRenderer>())
32
+ const components = shallowReactive(new Map<string, Component>())
33
+ const actions = shallowReactive(new Map<string, ViewerAction>())
34
+ const layers = shallowReactive(new Map<string, ViewerLayer>())
35
+
36
+ export function registerView(view: ViewDefinition): void {
37
+ const definition = Object.defineProperties({}, Object.getOwnPropertyDescriptors(view)) as ViewDefinition
38
+ definition.component = markRaw(view.component)
39
+ views.set(view.id, definition)
40
+ }
41
+
42
+ export function registerNodeRenderer(key: string, renderer: NodeRenderer): void {
43
+ nodeRenderers.set(key, markRaw(renderer))
44
+ }
45
+
46
+ export function registerComponent(name: string, component: Component): void {
47
+ components.set(name, markRaw(component))
48
+ }
49
+
50
+ export function registerAction(action: ViewerAction): void {
51
+ actions.delete(action.id)
52
+ actions.set(action.id, action)
53
+ }
54
+
55
+ export function registerLayer(layer: ViewerLayer): void {
56
+ layers.set(layer.id, { ...layer, component: markRaw(layer.component) })
57
+ }
58
+
59
+ export function getView(id: string): ViewDefinition | undefined {
60
+ return views.get(id)
61
+ }
62
+
63
+ export function listViews(): ViewDefinition[] {
64
+ return [...views.values()]
65
+ }
66
+
67
+ export function getNodeRenderer(key: string): NodeRenderer | undefined {
68
+ return nodeRenderers.get(key)
69
+ }
70
+
71
+ export function getComponent(name: string): Component | undefined {
72
+ return components.get(name)
73
+ }
74
+
75
+ export function listActions(view: string): ViewerAction[] {
76
+ return [...actions.values()].reverse().filter(action => (action.views ?? OVERLAY_VIEWS).includes(view))
77
+ }
78
+
79
+ export function listLayers(view: string): ViewerLayer[] {
80
+ return [...layers.values()].filter(layer => (layer.views ?? OVERLAY_VIEWS).includes(view))
81
+ }
package/src/rich.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { AUTHOR_MARKS_CLASS, errorMessage, escapeHtml } from '@lincyaw/kernel'
2
+ import 'katex/dist/katex.min.css'
3
+ import { computed, defineComponent, h, onMounted, ref, shallowRef, watch } from 'vue'
4
+ import { useSectionContext } from './context.ts'
5
+ import { useDark } from './dark.ts'
6
+
7
+ const never = <T>() => new Promise<T>(() => {})
8
+
9
+ type Katex = typeof import('katex').default
10
+
11
+ let katex: Promise<Katex> | undefined
12
+
13
+ function loadKatex(): Promise<Katex> {
14
+ if (import.meta.env.SSR) return never()
15
+ katex ??= import('katex').then(m => m.default)
16
+ return katex
17
+ }
18
+
19
+ export const PmMath = defineComponent({
20
+ name: 'PmMath',
21
+ props: {
22
+ tex: { type: String, required: true },
23
+ display: Boolean,
24
+ html: { type: String, default: undefined },
25
+ },
26
+ setup(props) {
27
+ const rendered = ref<string>()
28
+ const render = async () => {
29
+ if (props.html) return
30
+ const { renderToString } = await loadKatex()
31
+ rendered.value = renderToString(props.tex, { displayMode: props.display, throwOnError: false })
32
+ }
33
+ onMounted(render)
34
+ watch(() => [props.tex, props.display, props.html], render)
35
+ return () => {
36
+ const html = props.html ?? rendered.value
37
+ return h(props.display ? 'div' : 'span', {
38
+ 'class': ['pm-math', props.display ? 'pm-math--display' : 'pm-math--inline'],
39
+ 'data-rendered': html ? '' : undefined,
40
+ 'innerHTML': html ?? escapeHtml(props.tex),
41
+ })
42
+ }
43
+ },
44
+ })
45
+
46
+ type Mermaid = typeof import('mermaid').default
47
+
48
+ let mermaid: Promise<Mermaid> | undefined
49
+ let diagrams = 0
50
+
51
+ function loadMermaid(): Promise<Mermaid> {
52
+ if (import.meta.env.SSR) return never()
53
+ mermaid ??= import('mermaid').then(m => m.default)
54
+ return mermaid
55
+ }
56
+
57
+ function tokenTheme(dark: boolean): Record<string, string | boolean> | undefined {
58
+ const style = getComputedStyle(document.documentElement)
59
+ const token = (name: string) => style.getPropertyValue(`--pm-color-${name}`).trim()
60
+ const [ink, muted, surface, sunk, line, accent, accentSoft, warn, warnSoft] = ['ink', 'muted', 'surface', 'sunk', 'line', 'accent', 'accent-soft', 'warn', 'warn-soft'].map(token)
61
+ if (!ink || !surface || !accent) return undefined
62
+ return {
63
+ darkMode: dark,
64
+ background: surface,
65
+ fontFamily: style.getPropertyValue('--pm-font-sans').trim() || 'sans-serif',
66
+ textColor: ink,
67
+ lineColor: muted!,
68
+ primaryColor: accentSoft!,
69
+ primaryTextColor: ink,
70
+ primaryBorderColor: accent,
71
+ secondaryColor: sunk!,
72
+ secondaryTextColor: ink,
73
+ secondaryBorderColor: line!,
74
+ tertiaryColor: surface,
75
+ tertiaryTextColor: ink,
76
+ tertiaryBorderColor: line!,
77
+ edgeLabelBackground: surface,
78
+ clusterBkg: sunk!,
79
+ clusterBorder: line!,
80
+ titleColor: ink,
81
+ noteBkgColor: warnSoft!,
82
+ noteTextColor: ink,
83
+ noteBorderColor: warn!,
84
+ actorBkg: accentSoft!,
85
+ actorBorder: accent,
86
+ actorTextColor: ink,
87
+ signalColor: ink,
88
+ signalTextColor: ink,
89
+ labelBoxBkgColor: sunk!,
90
+ labelTextColor: ink,
91
+ loopTextColor: ink,
92
+ }
93
+ }
94
+
95
+ export const PmMermaid = defineComponent({
96
+ name: 'PmMermaid',
97
+ props: { code: { type: String, required: true } },
98
+ setup(props) {
99
+ const drawn = ref<{ svg: string, theme: string }>()
100
+ const failed = ref<string>()
101
+ const dark = useDark()
102
+ let latest = 0
103
+ const render = async () => {
104
+ const id = ++latest
105
+ try {
106
+ const library = await loadMermaid()
107
+ const variables = tokenTheme(dark.value)
108
+ const appearance = dark.value ? 'dark' : 'default'
109
+ const theme = variables ? 'base' : appearance
110
+ library.initialize({ startOnLoad: false, securityLevel: 'strict', theme, ...variables ? { themeVariables: variables } : {}, flowchart: { useMaxWidth: false } })
111
+ const { svg } = await library.render(`pm-mermaid-${++diagrams}`, props.code)
112
+ if (id !== latest) return
113
+ drawn.value = { svg, theme: appearance }
114
+ failed.value = undefined
115
+ } catch (error) {
116
+ if (id === latest) failed.value = errorMessage(error)
117
+ }
118
+ }
119
+ onMounted(render)
120
+ watch(() => [props.code, dark.value], render)
121
+ return () => h('div', {
122
+ 'class': ['pm-mermaid', { 'pm-mermaid--error': failed.value }],
123
+ 'data-rendered': drawn.value ? '' : undefined,
124
+ 'data-theme': drawn.value?.theme,
125
+ 'title': failed.value,
126
+ 'innerHTML': drawn.value?.svg ?? `<pre>${escapeHtml(props.code)}</pre>`,
127
+ })
128
+ },
129
+ })
130
+
131
+ const authorMarks = shallowRef(false)
132
+ let observed = false
133
+
134
+ function useAuthorMarks() {
135
+ if (!observed && typeof document !== 'undefined') {
136
+ observed = true
137
+ const root = document.documentElement
138
+ authorMarks.value = root.classList.contains(AUTHOR_MARKS_CLASS)
139
+ new MutationObserver(() => {
140
+ authorMarks.value = root.classList.contains(AUTHOR_MARKS_CLASS)
141
+ }).observe(root, { attributes: true, attributeFilter: ['class'] })
142
+ }
143
+ return authorMarks
144
+ }
145
+
146
+ export const PmRoleMark = defineComponent({
147
+ name: 'PmRoleMark',
148
+ props: { mark: { type: String, required: true } },
149
+ setup(props) {
150
+ const section = useSectionContext()
151
+ const marks = useAuthorMarks()
152
+ const shown = computed(() => section?.view.value === 'presenter' || marks.value)
153
+ return () => (shown.value ? h('span', { class: 'pm-role-mark' }, props.mark) : null)
154
+ },
155
+ })