@lincyaw/viewer 1.0.0-rc.0 → 1.0.0-rc.2

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.
Files changed (70) hide show
  1. package/README.md +6 -4
  2. package/package.json +20 -8
  3. package/src/client/block.ts +70 -0
  4. package/src/client/code/code-block.ts +54 -0
  5. package/src/client/code/code-editor.ts +73 -0
  6. package/src/client/code/code-view.ts +22 -0
  7. package/src/{highlight.ts → client/code/highlight.ts} +2 -2
  8. package/src/{outputs.ts → client/code/outputs.ts} +23 -36
  9. package/src/client/content-marks.ts +15 -0
  10. package/src/client/element.ts +27 -0
  11. package/src/client/index.ts +40 -0
  12. package/src/client/layout.ts +22 -0
  13. package/src/{markdown.ts → client/markdown/handlers.ts} +22 -116
  14. package/src/client/markdown/hast.ts +20 -0
  15. package/src/client/markdown/layout-handlers.ts +57 -0
  16. package/src/client/markdown/math.ts +10 -0
  17. package/src/client/markdown/mermaid.ts +75 -0
  18. package/src/client/markdown/render.ts +79 -0
  19. package/src/client/markdown/role-mark.ts +9 -0
  20. package/src/client/markdown/tables.ts +49 -0
  21. package/src/client/markdown/tabs.ts +52 -0
  22. package/src/client/markdown/tree-cache.ts +77 -0
  23. package/src/client/operations.ts +9 -0
  24. package/src/client/registry.ts +21 -0
  25. package/src/client/section-context.ts +5 -0
  26. package/src/client/section.ts +30 -0
  27. package/src/client/slidev-compat/click-directives.ts +16 -0
  28. package/src/client/slidev-compat/slide-globals.ts +3 -0
  29. package/src/{style.css → client/style.css} +306 -81
  30. package/src/client/viewer-messages.ts +7 -0
  31. package/src/{plugins.ts → client/viewer-plugins.ts} +3 -2
  32. package/src/client/viewer-state.ts +47 -0
  33. package/src/client/views/actions.ts +52 -0
  34. package/src/client/views/controls.css +44 -0
  35. package/src/client/views/document.ts +12 -0
  36. package/src/client/views/outlets.ts +37 -0
  37. package/src/client/views/overlay.ts +16 -0
  38. package/src/client/views/present.ts +132 -0
  39. package/src/client/views/presenter.ts +76 -0
  40. package/src/client/views/scrolly.ts +4 -0
  41. package/src/client/views/slide.css +8 -0
  42. package/src/client/views/slide.ts +57 -0
  43. package/src/domain/clicks.ts +1 -1
  44. package/src/domain/code-width.ts +11 -0
  45. package/src/domain/document-content.ts +6 -0
  46. package/src/domain/{text.ts → footnotes.ts} +0 -12
  47. package/src/domain/index.ts +5 -4
  48. package/src/domain/live.ts +11 -10
  49. package/src/domain/messages.ts +54 -30
  50. package/src/domain/origin.ts +17 -32
  51. package/src/domain/outputs.ts +27 -0
  52. package/src/domain/steps.ts +2 -1
  53. package/src/block.ts +0 -122
  54. package/src/cards.ts +0 -130
  55. package/src/clicks.ts +0 -124
  56. package/src/code.ts +0 -235
  57. package/src/context.ts +0 -16
  58. package/src/dark.ts +0 -5
  59. package/src/domain/follow.ts +0 -32
  60. package/src/globals.ts +0 -66
  61. package/src/i18n.ts +0 -7
  62. package/src/index.ts +0 -55
  63. package/src/operations.ts +0 -28
  64. package/src/present.ts +0 -327
  65. package/src/registry.ts +0 -81
  66. package/src/rich.ts +0 -155
  67. package/src/section.ts +0 -209
  68. package/src/viewer.ts +0 -116
  69. package/src/views.ts +0 -101
  70. /package/src/{env.d.ts → client/env.d.ts} +0 -0
package/src/code.ts DELETED
@@ -1,235 +0,0 @@
1
- import { currentLocale, OPERATIONS, usePrefersReducedMotion, useService, useStartReport } from '@lincyaw/host'
2
- import type { Operations } from '@lincyaw/host'
3
- import { blockState, EXECUTOR, errorMessage, failedRun } from '@lincyaw/kernel'
4
- import type { CodeNode, IRBlock, RecordedRun, RunRequest, RunResult } from '@lincyaw/kernel'
5
- import { ShikiMagicMove } from '@shikijs/magic-move/vue'
6
- import '@shikijs/magic-move/style.css'
7
- import type { Element, ElementContent, Root } from 'hast'
8
- import { toHtml } from 'hast-util-to-html'
9
- import type { PropType, VNode, VNodeChild } from 'vue'
10
- import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
11
- import { IN_COMPONENT, useSectionContext } from './context.ts'
12
- import { useDark } from './dark.ts'
13
- import { codeColumns, originParts } from './domain/index.ts'
14
- import type { OriginPart } from './domain/index.ts'
15
- import { highlighterFor, THEMES } from './highlight.ts'
16
- import { messages } from './i18n.ts'
17
- import { proseOnly, runOutput } from './outputs.ts'
18
-
19
- type Loaded = Awaited<ReturnType<typeof highlighterFor>>
20
-
21
- function useHighlighter(lang: () => string) {
22
- const shiki = shallowRef<Loaded>()
23
- const load = async () => {
24
- const wanted = lang()
25
- const loaded = await highlighterFor(wanted).catch(() => undefined)
26
- if (wanted === lang()) shiki.value = loaded
27
- }
28
- onMounted(load)
29
- watch(lang, load)
30
- return shiki
31
- }
32
-
33
- function lineElements(tree: Root): Element[] {
34
- const pre = tree.children.find((node): node is Element => node.type === 'element')
35
- const code = pre?.children.find((node): node is Element => node.type === 'element' && node.tagName === 'code')
36
- return (code?.children ?? []).filter((node: ElementContent): node is Element => node.type === 'element')
37
- }
38
-
39
- export const CodeView = defineComponent({
40
- name: 'PmCodeView',
41
- props: {
42
- lang: { type: String, default: '' },
43
- value: { type: String, required: true },
44
- highlight: { type: Array as PropType<number[]>, default: undefined },
45
- },
46
- setup(props) {
47
- const shiki = useHighlighter(() => props.lang)
48
- const code = computed(() => props.value.replace(/\n$/, ''))
49
- const lines = computed(() => new Set(props.highlight ?? []))
50
- const tree = computed(() => shiki.value && shiki.value.highlighter.codeToHast(code.value, { lang: shiki.value.lang, themes: THEMES, defaultColor: false }))
51
- const html = computed(() => {
52
- if (!tree.value) return undefined
53
- const marked = lines.value
54
- lineElements(tree.value).forEach((line, i) => {
55
- line.properties.class = marked.has(i + 1) ? 'line highlighted' : 'line'
56
- })
57
- return toHtml(tree.value)
58
- })
59
- const columns = computed(() => codeColumns(code.value))
60
- return () => {
61
- const attrs = {
62
- 'class': ['pm-code', { 'pm-code--highlight': lines.value.size > 0 }],
63
- 'style': { '--pm-code-columns': columns.value },
64
- 'data-lang': props.lang || undefined,
65
- }
66
- if (html.value) return h('div', { ...attrs, key: 'shiki', innerHTML: html.value })
67
- const rows = code.value.split('\n').flatMap((text, i) => [h('span', { class: ['line', { highlighted: lines.value.has(i + 1) }] }, text), '\n'])
68
- return h('div', { ...attrs, key: 'plain' }, [h('pre', { class: 'shiki' }, [h('code', rows.slice(0, -1))])])
69
- }
70
- },
71
- })
72
-
73
- const VariantView = defineComponent({
74
- name: 'PmCodeVariant',
75
- props: {
76
- lang: { type: String, default: '' },
77
- value: { type: String, required: true },
78
- },
79
- setup(props) {
80
- const shiki = useHighlighter(() => props.lang)
81
- const reduced = usePrefersReducedMotion()
82
- const dark = useDark()
83
- const code = computed(() => props.value.replace(/\n$/, ''))
84
- return () => {
85
- const loaded = shiki.value
86
- if (!loaded || reduced.value) return h(CodeView, { lang: props.lang, value: props.value })
87
- return h('div', { 'class': 'pm-code pm-code--magic-move', 'style': { '--pm-code-columns': codeColumns(code.value) }, 'data-lang': props.lang || undefined }, [
88
- h(ShikiMagicMove, { highlighter: loaded.highlighter, lang: loaded.lang, theme: dark.value ? THEMES.dark : THEMES.light, code: code.value, options: { duration: 500 } }),
89
- ])
90
- }
91
- },
92
- })
93
-
94
- function link(className: string, href: string | undefined, text: string, attrs: Record<string, unknown> = {}): VNode {
95
- return href
96
- ? h('a', { ...attrs, class: className, href, target: '_blank', rel: 'noopener' }, text)
97
- : h('span', { ...attrs, class: className }, text)
98
- }
99
-
100
- function originPart(part: OriginPart, k: number): VNode {
101
- return link(`pm-code-origin__${part.kind}`, part.href, part.text, {
102
- 'key': `${part.kind}:${k}`,
103
- 'title': part.title,
104
- 'data-active': part.active ? '' : undefined,
105
- 'data-status': part.status,
106
- 'data-drift': part.drift,
107
- })
108
- }
109
-
110
- function originBar(node: CodeNode, shown: number | undefined): VNodeChild {
111
- const bar = originParts(node, shown, currentLocale())
112
- return bar ? h('div', { 'class': 'pm-code-origin', 'data-anchor': bar.anchor }, bar.parts.map(originPart)) : null
113
- }
114
-
115
- const replays = new Set<(data: Record<string, unknown>) => void>()
116
- const followed = new WeakSet<Operations>()
117
-
118
- function followRuns(operations: Operations): void {
119
- if (followed.has(operations)) return
120
- followed.add(operations)
121
- operations.onReplay('run', { apply: data => replays.forEach(replay => replay(data)) })
122
- }
123
-
124
- export const CodeBlockView = defineComponent({
125
- name: 'PmCodeBlock',
126
- props: {
127
- block: { type: Object as PropType<IRBlock>, required: true },
128
- step: { type: Number, required: true },
129
- expanded: Boolean,
130
- },
131
- setup(props) {
132
- const owned = inject(IN_COMPONENT, false)
133
- const executor = useService(EXECUTOR)
134
- const operations = useService(OPERATIONS)
135
- const section = useSectionContext()
136
- const report = useStartReport()
137
- const live = shallowRef<RunResult>()
138
- const fallback = ref<string>()
139
- const running = ref(false)
140
- const mounted = ref(false)
141
- let current: AbortController | undefined
142
- const stop = () => {
143
- current?.abort()
144
- current = undefined
145
- running.value = false
146
- }
147
- onMounted(() => {
148
- mounted.value = true
149
- })
150
- onBeforeUnmount(stop)
151
- const show = (result: RunResult | undefined) => {
152
- const node = props.block.node as CodeNode
153
- const recorded = result && !result.ok && result.exitCode === null && node.outputs?.length
154
- fallback.value = recorded ? result.stderr.trim() || messages().code.unavailable : undefined
155
- live.value = recorded ? undefined : result
156
- }
157
- const run = async () => {
158
- const node = props.block.node as CodeNode
159
- const router = executor.value
160
- if (!node.run || !router || running.value) return
161
- stop()
162
- const controller = new AbortController()
163
- current = controller
164
- running.value = true
165
- const { lang, args, stdin, timeoutMs, executor: named, session } = node.run
166
- const request: RunRequest = { lang, code: node.value, args, stdin, timeoutMs, executor: named, session }
167
- const started = Date.now()
168
- let result: RunResult
169
- try {
170
- result = await router.run(request, controller.signal)
171
- } catch (error) {
172
- result = failedRun(router.id, messages().code.executorFailed(router.id, errorMessage(error)), Date.now() - started)
173
- }
174
- if (controller.signal.aborted) return
175
- show(result)
176
- current = undefined
177
- running.value = false
178
- const recording = node.outputs?.[0] as RecordedRun | undefined
179
- operations.value?.report({ type: 'run', data: { section: section?.section.value.id, block: props.block.id, result: fallback.value && recording ? `freeze:${recording.cacheKey}` : 'live' } })
180
- }
181
- const reset = async () => {
182
- const node = props.block.node as CodeNode
183
- if (!node.run?.session) return
184
- stop()
185
- await executor.value?.endSession?.(node.run.session)
186
- show(undefined)
187
- }
188
- const replay = (data: Record<string, unknown>) => {
189
- if (data.block !== props.block.id || (data.section !== undefined && data.section !== section?.section.value.id)) return
190
- stop()
191
- show(undefined)
192
- }
193
- watch(operations, (ops) => {
194
- if (ops) followRuns(ops)
195
- }, { immediate: true })
196
- onMounted(() => replays.add(replay))
197
- onBeforeUnmount(() => replays.delete(replay))
198
- watch(() => (props.block.node as CodeNode).value, () => {
199
- stop()
200
- show(undefined)
201
- })
202
-
203
- return () => {
204
- const node = props.block.node as CodeNode
205
- const state = blockState(props.block, props.step)
206
- const variants = node.variants ?? []
207
- const shown = variants.length && !props.expanded ? Math.min(state, variants.length - 1) : undefined
208
- const listings = variants.length === 0
209
- ? [h(CodeView, { lang: node.lang, value: node.value, highlight: props.expanded ? undefined : node.highlight?.[state] })]
210
- : shown === undefined
211
- ? variants.map(v => h('div', { key: v.label, class: 'pm-code-block__variant' }, [h('p', { class: 'pm-code-block__label' }, v.label), h(CodeView, { lang: node.lang, value: v.value })]))
212
- : [h(VariantView, { key: 'variant', lang: node.lang, value: variants[shown]!.value })]
213
- const echo = node.echo !== false
214
- const runnable = !owned && echo && node.run && node.run.when !== 'compile'
215
- const t = messages().code
216
- const available = mounted.value && executor.value
217
- const controls = runnable
218
- ? h('div', { class: 'pm-code-block__run' }, [
219
- h('button', { type: 'button', disabled: running.value || !available, onClick: run }, running.value ? t.running : live.value ? t.rerun : t.run),
220
- node.run!.session ? h('button', { 'type': 'button', 'class': 'pm-code-block__reset', 'disabled': !available, 'data-session': node.run!.session, 'onClick': reset }, t.resetSession) : null,
221
- mounted.value && !executor.value && report.value ? h('span', { class: 'pm-code-block__hint' }, t.unavailable) : null,
222
- ])
223
- : null
224
- const runs = owned ? [] : live.value ? [live.value] : node.outputs ?? []
225
- const prose = !echo && !live.value && runs.length > 0 && runs.every(proseOnly)
226
- const prefix = section?.section.value.id ?? props.block.id
227
- const outputs = runs.length
228
- ? h('div', { 'class': ['pm-outputs', { 'pm-outputs--prose': prose }], 'data-source': live.value ? 'live' : 'recorded' }, runs.map((r, i) => runOutput(r, (r as RecordedRun).cacheKey || i, prefix, { prose, note: fallback.value })))
229
- : null
230
- return h('div', { 'class': ['pm-code-block', { 'pm-code-block--output-only': !echo }], 'data-state': props.expanded ? undefined : state, 'data-run': runnable ? node.run!.when : undefined, 'data-session': node.run?.session }, echo
231
- ? [originBar(node, shown), ...listings, controls, outputs]
232
- : [outputs])
233
- }
234
- },
235
- })
package/src/context.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { IRSection } from '@lincyaw/kernel'
2
- import type { InjectionKey, Ref } from 'vue'
3
- import { inject } from 'vue'
4
-
5
- export interface SectionContext {
6
- section: Readonly<Ref<IRSection>>
7
- step: Readonly<Ref<number>>
8
- view: Readonly<Ref<string>>
9
- }
10
-
11
- export const SECTION_CONTEXT: InjectionKey<SectionContext> = Symbol('presmium.viewer.section')
12
- export const IN_COMPONENT: InjectionKey<boolean> = Symbol('presmium.viewer.inComponent')
13
-
14
- export function useSectionContext(): SectionContext | undefined {
15
- return inject(SECTION_CONTEXT, undefined)
16
- }
package/src/dark.ts DELETED
@@ -1,5 +0,0 @@
1
- import { useTheme } from '@lincyaw/host'
2
-
3
- export function useDark() {
4
- return useTheme().dark
5
- }
@@ -1,32 +0,0 @@
1
- import { stepTrail, trailStep } from '@lincyaw/kernel'
2
- import type { IRSection, StepKeys } from '@lincyaw/kernel'
3
-
4
- export { stepKeys, stepTrail, trailStep } from '@lincyaw/kernel'
5
- export type { StepKeys } from '@lincyaw/kernel'
6
-
7
- export interface FollowAnchor {
8
- section?: string
9
- trail?: string[]
10
- back?: string
11
- }
12
-
13
- const LEARNERS = 'student'
14
-
15
- export function backSection(sections: readonly Pick<IRSection, 'id' | 'audience'>[], id: string): string | undefined {
16
- const at = sections.findIndex(section => section.id === id)
17
- const audience = sections[at]?.audience
18
- if (!audience?.length) return undefined
19
- const learners = !audience.includes(LEARNERS)
20
- return sections.slice(0, at).findLast(section => !section.audience?.length || (learners && section.audience.includes(LEARNERS)))?.id
21
- }
22
-
23
- export function followedPlace(position: FollowAnchor & { step: number }, keysOf: (id: string) => StepKeys | undefined): { section: string, step: number } | undefined {
24
- const { section, trail, back, step } = position
25
- const keys = section === undefined ? undefined : keysOf(section)
26
- if (keys) {
27
- const other = trail !== undefined && stepTrail(keys, step).join('\n') !== trail.join('\n')
28
- return { section: section!, step: other ? trailStep(keys, trail) : step }
29
- }
30
- const before = back === undefined ? undefined : keysOf(back)
31
- return before && { section: back!, step: before.length - 1 }
32
- }
package/src/globals.ts DELETED
@@ -1,66 +0,0 @@
1
- import type { IRSection } from '@lincyaw/kernel'
2
- import type { App, Component, ComponentOptions } from 'vue'
3
- import { computed, getCurrentInstance, inject, reactive } from 'vue'
4
- import { SECTION_CONTEXT } from './context.ts'
5
- import type { SectionContext } from './context.ts'
6
- import { VIEWER } from './viewer.ts'
7
- import type { Viewer } from './viewer.ts'
8
-
9
- function section() {
10
- return getCurrentInstance() ? inject(SECTION_CONTEXT, undefined) : undefined
11
- }
12
-
13
- export function installSlideGlobals(app: App, viewer: Viewer): void {
14
- const props = app.config.globalProperties as Record<string, unknown>
15
- const index = computed(() => viewer.section(viewer.position.value.section)?.index ?? 1)
16
- const go = (n: number) => {
17
- const target = viewer.doc.value.sections[n - 1]
18
- if (target) viewer.show({ section: target.id, step: 0 })
19
- }
20
- const nav = reactive({
21
- currentPage: index,
22
- currentSlideNo: index,
23
- clicks: computed(() => viewer.position.value.step),
24
- total: computed(() => viewer.doc.value.sections.length),
25
- go,
26
- next: () => viewer.next(),
27
- prev: () => viewer.prev(),
28
- nextSlide: () => go(index.value + 1),
29
- prevSlide: () => go(index.value - 1),
30
- })
31
- const configs = computed(() => ({ ...viewer.doc.value.meta, presmium: viewer.doc.value.config.presmium }))
32
- const define = (name: string, get: () => unknown) => {
33
- const existing = Object.getOwnPropertyDescriptor(props, name)
34
- if (existing && !existing.configurable) return
35
- Object.defineProperty(props, name, { configurable: true, enumerable: true, get })
36
- }
37
- define('$page', () => section()?.section.value.index ?? index.value)
38
- define('$clicks', () => section()?.step.value ?? viewer.position.value.step)
39
- define('$nav', () => nav)
40
- define('$slidev', () => ({ configs: configs.value, themeConfigs: {}, nav }))
41
- }
42
-
43
- export function sectionFrontmatter(section: IRSection, renderer: string): Record<string, unknown> {
44
- return { title: section.title, ...section.hints[renderer] }
45
- }
46
-
47
- export function withSlideGlobals(component: Component): Component {
48
- if (typeof component === 'function') return component
49
- const options = component as ComponentOptions
50
- const inject = Array.isArray(options.inject) ? Object.fromEntries(options.inject.map(key => [key, key])) : options.inject
51
- return {
52
- ...options,
53
- inject: {
54
- ...inject,
55
- pmSection: { from: SECTION_CONTEXT, default: undefined },
56
- pmViewer: { from: VIEWER, default: undefined },
57
- },
58
- computed: {
59
- ...options.computed,
60
- $frontmatter(): Record<string, unknown> | undefined {
61
- const { pmSection, pmViewer } = this as unknown as { pmSection?: SectionContext, pmViewer?: Viewer }
62
- return pmSection && sectionFrontmatter(pmSection.section.value, pmViewer?.options.renderer ?? 'web')
63
- },
64
- },
65
- } as ComponentOptions
66
- }
package/src/i18n.ts DELETED
@@ -1,7 +0,0 @@
1
- import { currentLocale } from '@lincyaw/host'
2
- import { VIEWER_MESSAGES } from './domain/index.ts'
3
- import type { Messages } from './domain/index.ts'
4
-
5
- export function messages(): Messages {
6
- return VIEWER_MESSAGES[currentLocale()]
7
- }
package/src/index.ts DELETED
@@ -1,55 +0,0 @@
1
- import type { App } from 'vue'
2
- import { installClicks } from './clicks.ts'
3
- import { installSlideGlobals } from './globals.ts'
4
- import { messages } from './i18n.ts'
5
- import { BUILTIN_ACTIONS, PresenterView, PresentView } from './present.ts'
6
- import { registerAction, registerView } from './registry.ts'
7
- import { provideViewer } from './viewer.ts'
8
- import type { Viewer } from './viewer.ts'
9
- import { DocumentView, ScrollyView } from './views.ts'
10
- import './style.css'
11
-
12
- registerView({ id: 'document', get label() { return messages().views.document }, kind: 'page', component: DocumentView })
13
- registerView({ id: 'scrolly', get label() { return messages().views.scrolly }, kind: 'page', component: ScrollyView })
14
- registerView({ id: 'present', get label() { return messages().views.present }, kind: 'overlay', component: PresentView })
15
- registerView({ id: 'presenter', get label() { return messages().views.presenter }, kind: 'overlay', component: PresenterView, hidden: true })
16
- BUILTIN_ACTIONS.forEach(registerAction)
17
-
18
- export function installViewer(app: App, viewer: Viewer): void {
19
- provideViewer(app, viewer)
20
- installClicks(app)
21
- installSlideGlobals(app, viewer)
22
- }
23
-
24
- export { BlockView, PmComponentFallback } from './block.ts'
25
- export { CodeView } from './code.ts'
26
- export { SECTION_CONTEXT, useSectionContext } from './context.ts'
27
- export type { SectionContext } from './context.ts'
28
- export * from './domain/index.ts'
29
- export { sectionFrontmatter, withSlideGlobals } from './globals.ts'
30
- export { messages } from './i18n.ts'
31
- export { MarkdownView } from './markdown.ts'
32
- export { followOperations } from './operations.ts'
33
- export { installViewerPlugin } from './plugins.ts'
34
- export type { ViewerPlugin, ViewerPluginContext } from './plugins.ts'
35
- export { BUILTIN_ACTIONS, PresenterView, PresentView } from './present.ts'
36
- export {
37
- getComponent,
38
- getNodeRenderer,
39
- getView,
40
- listActions,
41
- listLayers,
42
- listViews,
43
- registerAction,
44
- registerComponent,
45
- registerLayer,
46
- registerNodeRenderer,
47
- registerView,
48
- } from './registry.ts'
49
- export type { NodeRenderer, ViewDefinition, ViewerAction, ViewerLayer } from './registry.ts'
50
- export { PmMath, PmMermaid, PmRoleMark } from './rich.ts'
51
- export { SectionView, STEP_LINE } from './section.ts'
52
- export type { SectionLayout } from './section.ts'
53
- export { createViewer, useViewer, VIEWER } from './viewer.ts'
54
- export type { Viewer, ViewerOptions } from './viewer.ts'
55
- export { DocumentView, OverlayHost, ScrollyView, ViewHost } from './views.ts'
package/src/operations.ts DELETED
@@ -1,28 +0,0 @@
1
- import type { Operations } from '@lincyaw/host'
2
- import { watch } from 'vue'
3
- import { getView } from './registry.ts'
4
- import type { Viewer } from './viewer.ts'
5
-
6
- export function followOperations(viewer: Viewer, operations: Operations): () => void {
7
- const stop = watch(
8
- () => ({ section: viewer.position.value.section, step: viewer.position.value.step, view: viewer.view.value }),
9
- (now, before) => {
10
- if (before && now.section === before.section && now.step === before.step && now.view === before.view) return
11
- operations.report({ type: 'go', data: { ...now } })
12
- },
13
- { flush: 'sync' },
14
- )
15
- const off = operations.onReplay('go', {
16
- apply(data) {
17
- const view = typeof data.view === 'string' ? getView(data.view) : undefined
18
- if (view && !view.hidden) viewer.setView(view.id)
19
- if (typeof data.section !== 'string') return
20
- const step = Number(data.step)
21
- viewer.show({ section: data.section, step: Number.isFinite(step) && step >= 0 ? step : 0 })
22
- },
23
- })
24
- return () => {
25
- stop()
26
- off()
27
- }
28
- }