@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/section.ts DELETED
@@ -1,209 +0,0 @@
1
- import { isChapterTitle, narrationAt } from '@lincyaw/kernel'
2
- import type { IRBlock, IRSection } from '@lincyaw/kernel'
3
- import type { InjectionKey, PropType, Ref, VNodeChild } from 'vue'
4
- import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, shallowRef, toRef } from 'vue'
5
- import { BlockView } from './block.ts'
6
- import { SECTION_CONTEXT } from './context.ts'
7
- import { FINAL_STEP, sectionDiagnostics, stepAtLine } from './domain/index.ts'
8
- import type { Position } from './domain/index.ts'
9
- import { messages } from './i18n.ts'
10
- import { useViewer } from './viewer.ts'
11
-
12
- export type SectionLayout = 'document' | 'scrolly'
13
-
14
- export const STEP_LINE = 0.6
15
-
16
- const READ_LINE = 0.25
17
-
18
- interface Tracked {
19
- el: HTMLElement
20
- id(): string
21
- layout(): SectionLayout
22
- step: Ref<number>
23
- }
24
-
25
- export interface PageTracker {
26
- add(section: Tracked): () => void
27
- scrollTo(position: Position): boolean
28
- hold(): () => void
29
- dispose(): void
30
- }
31
-
32
- export const PAGE_TRACKER: InjectionKey<PageTracker> = Symbol('presmium.viewer.page')
33
-
34
- function markers(el: HTMLElement): HTMLElement[] {
35
- return [...el.querySelectorAll<HTMLElement>('.pm-step')]
36
- }
37
-
38
- export function createPageTracker(report: (section: string, step?: number) => void, active: () => boolean): PageTracker {
39
- const tracked = new Set<Tracked>()
40
- let frame = 0
41
- let restoredY: number | undefined
42
- let pinned: Position | undefined
43
- const resized = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(() => schedule())
44
- const scrolled = () => restoredY !== undefined && Math.abs(window.scrollY - restoredY) >= 2
45
-
46
- const update = () => {
47
- frame = 0
48
- const steps = window.innerHeight * STEP_LINE
49
- const line = window.innerHeight * READ_LINE
50
- let reading: { section: Tracked, top: number } | undefined
51
- let first: { section: Tracked, top: number } | undefined
52
- for (const section of tracked) {
53
- if (section.layout() === 'scrolly') {
54
- section.step.value = stepAtLine(markers(section.el).map(m => ({ step: Number(m.dataset.step), top: m.getBoundingClientRect().top })), steps)
55
- }
56
- const top = section.el.getBoundingClientRect().top
57
- if (top <= line && (!reading || top > reading.top)) reading = { section, top }
58
- if (!first || top < first.top) first = { section, top }
59
- }
60
- if (restoredY !== undefined && !scrolled()) {
61
- if (pinned) place(pinned)
62
- return
63
- }
64
- restoredY = undefined
65
- pinned = undefined
66
- if (!active()) return
67
- const current = (reading ?? first)?.section
68
- if (current) report(current.id(), current.layout() === 'scrolly' ? current.step.value : undefined)
69
- }
70
- const schedule = () => {
71
- frame ||= requestAnimationFrame(update)
72
- }
73
- const place = (position: Position): boolean => {
74
- const section = [...tracked].find(s => s.id() === position.section)
75
- if (!section) return false
76
- const marker = section.layout() === 'scrolly'
77
- ? markers(section.el).filter(m => Number(m.dataset.step) <= position.step).at(-1)
78
- : undefined
79
- const top = marker
80
- ? window.scrollY + marker.getBoundingClientRect().top - window.innerHeight * STEP_LINE + 1
81
- : window.scrollY + section.el.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(section.el).scrollMarginTop || '0')
82
- if (Math.abs(window.scrollY - Math.max(top, 0)) >= 1) window.scrollTo({ top, behavior: 'instant' })
83
- restoredY = window.scrollY
84
- return true
85
- }
86
- const dispose = () => {
87
- cancelAnimationFrame(frame)
88
- frame = 0
89
- resized?.disconnect()
90
- window.removeEventListener('scroll', schedule)
91
- window.removeEventListener('resize', schedule)
92
- }
93
-
94
- return {
95
- add(section) {
96
- if (tracked.size === 0) {
97
- window.addEventListener('scroll', schedule, { passive: true })
98
- window.addEventListener('resize', schedule)
99
- }
100
- tracked.add(section)
101
- resized?.observe(section.el)
102
- schedule()
103
- return () => {
104
- tracked.delete(section)
105
- resized?.unobserve(section.el)
106
- if (tracked.size === 0) dispose()
107
- }
108
- },
109
- hold() {
110
- const line = window.innerHeight * STEP_LINE
111
- let reading: { section: Tracked, top: number } | undefined
112
- for (const section of tracked) {
113
- const top = section.el.getBoundingClientRect().top
114
- if (top <= line && (!reading || top > reading.top)) reading = { section, top }
115
- }
116
- if (!reading) return () => {}
117
- const { el } = reading.section
118
- const above = (candidates: HTMLElement[]) => candidates.filter(c => c.getBoundingClientRect().top <= line).at(-1)
119
- const anchor = reading.section.layout() === 'scrolly'
120
- ? above(markers(el)) ?? el
121
- : [...el.querySelectorAll<HTMLElement>('.pm-block')].find(b => b.getBoundingClientRect().top <= line && b.getBoundingClientRect().bottom > line) ?? el
122
- const top = anchor.getBoundingClientRect().top
123
- return () => {
124
- const moved = anchor.isConnected ? anchor.getBoundingClientRect().top - top : 0
125
- if (Math.abs(moved) < 1) return
126
- window.scrollBy({ top: moved, behavior: 'instant' })
127
- restoredY = window.scrollY
128
- schedule()
129
- }
130
- },
131
- scrollTo(position) {
132
- if (!place(position)) return false
133
- pinned = position
134
- schedule()
135
- return true
136
- },
137
- dispose,
138
- }
139
- }
140
-
141
- function blockViews(blocks: IRBlock[], section: IRSection, step: number, expanded: boolean): VNodeChild[] {
142
- return blocks.map(block => h(BlockView, { key: block.id, block, section, step, expanded }))
143
- }
144
-
145
-
146
- export const SectionView = defineComponent({
147
- name: 'PmSectionView',
148
- props: {
149
- section: { type: Object as PropType<IRSection>, required: true },
150
- layout: { type: String as PropType<SectionLayout>, default: 'document' },
151
- },
152
- setup(props) {
153
- const viewer = useViewer()
154
- const tracker = inject(PAGE_TRACKER, undefined)
155
- const el = shallowRef<HTMLElement>()
156
- const scrolled = ref(0)
157
- const step = computed(() => (props.layout === 'scrolly' ? scrolled.value : FINAL_STEP))
158
- provide(SECTION_CONTEXT, { section: toRef(props, 'section'), step, view: viewer.pageView })
159
-
160
- let untrack: (() => void) | undefined
161
- onMounted(() => {
162
- if (!el.value || !tracker) return
163
- untrack = tracker.add({ el: el.value, id: () => props.section.id, layout: () => props.layout, step: scrolled })
164
- })
165
- onBeforeUnmount(() => untrack?.())
166
-
167
- const heading = computed(() => (isChapterTitle(viewer.doc.value, props.section) ? 'h1' : 'h2'))
168
-
169
- return () => {
170
- const { section, layout } = props
171
- const t = messages().section
172
- const current = step.value
173
- const problems = viewer.options.diagnostics ? sectionDiagnostics(section, viewer.doc.value.diagnostics) : []
174
- const body = layout === 'scrolly' && section.blocks.some(b => b.role === 'visual')
175
- ? [
176
- h('div', { class: 'pm-stage pm-board' }, blockViews(section.blocks.filter(b => b.role === 'visual'), section, current, false)),
177
- h('div', { class: 'pm-track' }, Array.from({ length: section.steps + 1 }, (_, k) => {
178
- const narration = narrationAt(section, k)
179
- return h('div', { 'key': k, 'class': ['pm-step', { 'pm-step--current': k === current, 'pm-step--bare': narration.length === 0 }], 'data-step': k }, blockViews(narration, section, current, false))
180
- })),
181
- ]
182
- : [h('div', { class: 'pm-section__body' }, blockViews(section.blocks, section, current, true))]
183
- return h('section', {
184
- 'ref': el,
185
- 'class': ['pm-section', `pm-section--${layout}`, { 'pm-section--error': problems.some(p => p.severity === 'error') }],
186
- 'data-section': section.id,
187
- 'data-index': section.index,
188
- 'data-step': layout === 'scrolly' ? current : undefined,
189
- }, [
190
- h(heading.value, { id: section.id, class: 'pm-section__title', tabindex: -1 }, [
191
- section.title,
192
- viewer.options.sectionHeading?.(section, current) ?? null,
193
- h('a', { 'class': 'header-anchor', 'href': `#${section.id}`, 'aria-label': t.anchor(section.title) }, '​'),
194
- ]),
195
- viewer.options.openSource
196
- ? h('p', { class: 'pm-section__source' }, h('button', {
197
- type: 'button',
198
- title: t.sourceTitle(`${section.source.file}:${section.source.line}`),
199
- onClick: () => viewer.options.openSource!(section.source),
200
- }, t.openSource))
201
- : null,
202
- problems.length
203
- ? h('ul', { class: 'pm-diagnostics', role: 'alert' }, problems.map((p, i) => h('li', { 'key': i, 'data-severity': p.severity }, `${p.severity}: ${p.message}`)))
204
- : null,
205
- ...body,
206
- ])
207
- }
208
- },
209
- })
package/src/viewer.ts DELETED
@@ -1,116 +0,0 @@
1
- import { joinWindows } from '@lincyaw/host'
2
- import type { IRDocument, IRSection, SourcePos } from '@lincyaw/kernel'
3
- import type { App, InjectionKey, Ref, ShallowRef, VNodeChild } from 'vue'
4
- import { computed, inject, ref, shallowRef } from 'vue'
5
- import { applyUpdate, clampPosition, footnoteSections, keepPosition, nextPosition, prevPosition } from './domain/index.ts'
6
- import type { LiveUpdate, Position } from './domain/index.ts'
7
- import { getView } from './registry.ts'
8
-
9
- export interface ViewerOptions {
10
- renderer?: string
11
- sectionHref?(id: string): string
12
- sectionHeading?(section: IRSection, step: number): VNodeChild
13
- diagnostics?: boolean
14
- navigate?(position: Position): void
15
- openPresenter?(position: Position): void
16
- openSource?(source: SourcePos): void
17
- }
18
-
19
- export interface Viewer {
20
- readonly doc: Readonly<ShallowRef<IRDocument>>
21
- readonly view: Readonly<Ref<string>>
22
- readonly pageView: Readonly<Ref<string>>
23
- readonly position: Readonly<ShallowRef<Position>>
24
- readonly restoring: Ref<boolean>
25
- readonly options: ViewerOptions & { renderer: string }
26
- section(id: string): IRSection | undefined
27
- footnoteHref(identifier: string): string
28
- go(position: Position, remote?: boolean): void
29
- show(position: Position): void
30
- next(): boolean
31
- prev(): boolean
32
- setView(id: string): void
33
- update(update: LiveUpdate): void
34
- replace(doc: IRDocument): void
35
- }
36
-
37
- export const VIEWER: InjectionKey<Viewer> = Symbol('presmium.viewer')
38
-
39
- export function footnoteId(identifier: string): string {
40
- return `fn-${identifier}`
41
- }
42
-
43
- const CHANNEL = 'presmium.viewer.position'
44
-
45
- export function createViewer(initial: IRDocument, options: ViewerOptions = {}): Viewer {
46
- const doc = shallowRef(initial)
47
- const view = ref('document')
48
- const pageView = ref('document')
49
- const position = shallowRef<Position>(clampPosition(initial, { section: initial.sections[0]?.id ?? '', step: 0 }))
50
- const restoring = ref(false)
51
- const presenting = () => getView(view.value)?.kind === 'overlay'
52
- const footnotes = computed(() => footnoteSections(doc.value))
53
- const windows = joinWindows(`${CHANNEL}:${initial.meta.title}`, (value) => {
54
- if (presenting()) viewer.go(value as Position, true)
55
- }, () => undefined)
56
- const replace = (next: IRDocument) => {
57
- const kept = keepPosition(doc.value, next, position.value)
58
- doc.value = next
59
- if (kept.section !== position.value.section || kept.step !== position.value.step) position.value = kept
60
- }
61
-
62
- const viewer: Viewer = {
63
- doc,
64
- view,
65
- pageView,
66
- position,
67
- restoring,
68
- options: { ...options, renderer: options.renderer ?? 'web' },
69
- section: id => doc.value.sections.find(s => s.id === id),
70
- footnoteHref(identifier) {
71
- const anchor = `#${footnoteId(identifier)}`
72
- const section = footnotes.value.get(identifier)
73
- return section && options.sectionHref ? `${options.sectionHref(section).split('#')[0]}${anchor}` : anchor
74
- },
75
- go(target, remote = false) {
76
- const next = clampPosition(doc.value, target)
77
- if (next.section === position.value.section && next.step === position.value.step) return
78
- position.value = next
79
- if (!remote && presenting()) windows?.send(next)
80
- },
81
- show(target) {
82
- viewer.go(target)
83
- if (!presenting()) restoring.value = true
84
- },
85
- next() {
86
- const next = nextPosition(doc.value, position.value)
87
- if (next) viewer.go(next)
88
- return Boolean(next)
89
- },
90
- prev() {
91
- const prev = prevPosition(doc.value, position.value)
92
- if (prev) viewer.go(prev)
93
- return Boolean(prev)
94
- },
95
- setView(id) {
96
- const target = getView(id)
97
- if (!target || id === view.value) return
98
- view.value = id
99
- if (target.kind === 'page') pageView.value = id
100
- restoring.value = true
101
- },
102
- update: update => replace(applyUpdate(doc.value, update)),
103
- replace,
104
- }
105
- return viewer
106
- }
107
-
108
- export function provideViewer(app: App, viewer: Viewer): void {
109
- app.provide(VIEWER, viewer)
110
- }
111
-
112
- export function useViewer(): Viewer {
113
- const viewer = inject(VIEWER, undefined)
114
- if (!viewer) throw new Error('[presmium] no viewer: call installViewer(app, viewer) first')
115
- return viewer
116
- }
package/src/views.ts DELETED
@@ -1,101 +0,0 @@
1
- import type { IRSection } from '@lincyaw/kernel'
2
- import type { PropType } from 'vue'
3
- import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, provide, watch } from 'vue'
4
- import { decoded } from './markdown.ts'
5
- import { getView } from './registry.ts'
6
- import { createPageTracker, PAGE_TRACKER, SectionView } from './section.ts'
7
- import type { SectionLayout } from './section.ts'
8
- import { useViewer } from './viewer.ts'
9
-
10
- const sectionsProp = { sections: { type: Array as PropType<IRSection[]>, required: true } } as const
11
-
12
- export const DocumentView = defineComponent({
13
- name: 'PmDocumentView',
14
- props: sectionsProp,
15
- setup(props) {
16
- const viewer = useViewer()
17
- const layout = (section: IRSection): SectionLayout => (section.hints[viewer.options.renderer]?.layout === 'scrolly' ? 'scrolly' : 'document')
18
- return () => h('div', { class: 'pm-view pm-view--document' }, props.sections.map(section => h(SectionView, { key: section.id, section, layout: layout(section) })))
19
- },
20
- })
21
-
22
- export const ScrollyView = defineComponent({
23
- name: 'PmScrollyView',
24
- props: sectionsProp,
25
- setup(props) {
26
- return () => h('div', { class: 'pm-view pm-view--scrolly' }, props.sections.map(section => h(SectionView, { key: section.id, section, layout: 'scrolly' })))
27
- },
28
- })
29
-
30
- const frame = () => new Promise(resolve => requestAnimationFrame(resolve))
31
-
32
- export const ViewHost = defineComponent({
33
- name: 'PmViewHost',
34
- props: {
35
- sections: { type: Array as PropType<string[]>, required: true },
36
- },
37
- setup(props) {
38
- const viewer = useViewer()
39
- const pageView = computed(() => getView(viewer.pageView.value) ?? getView('document'))
40
- const isPage = () => getView(viewer.view.value)?.kind === 'page'
41
- const tracker = createPageTracker((section, step) => {
42
- const { position } = viewer
43
- viewer.go({ section, step: step ?? (section === position.value.section ? position.value.step : 0) })
44
- }, () => isPage() && !viewer.restoring.value)
45
- provide(PAGE_TRACKER, tracker)
46
- onBeforeUnmount(() => tracker.dispose())
47
-
48
- const linked = () => {
49
- const hash = decoded(location.hash.slice(1))
50
- return props.sections.includes(hash) ? hash : undefined
51
- }
52
- const onHash = () => {
53
- const section = linked()
54
- if (section && isPage()) viewer.show({ section, step: 0 })
55
- }
56
- onMounted(() => {
57
- window.addEventListener('hashchange', onHash)
58
- if (viewer.restoring.value || props.sections.length === 0) return
59
- const section = linked()
60
- if (section) viewer.show({ section, step: 0 })
61
- else if (!props.sections.includes(viewer.position.value.section)) viewer.go({ section: props.sections[0]!, step: 0 })
62
- })
63
- onBeforeUnmount(() => window.removeEventListener('hashchange', onHash))
64
-
65
- watch(viewer.doc, async () => {
66
- if (!isPage() || viewer.restoring.value) return
67
- const restore = tracker.hold()
68
- await nextTick()
69
- restore()
70
- await frame()
71
- restore()
72
- }, { flush: 'pre' })
73
-
74
- watch([viewer.restoring, viewer.view], async () => {
75
- if (!viewer.restoring.value || !isPage()) return
76
- await nextTick()
77
- await frame()
78
- await frame()
79
- const { position } = viewer
80
- if (tracker.scrollTo(position.value)) viewer.restoring.value = false
81
- else if (!props.sections.includes(position.value.section) && viewer.options.navigate) viewer.options.navigate(position.value)
82
- else viewer.restoring.value = false
83
- }, { immediate: true })
84
-
85
- const sections = computed(() => props.sections.flatMap(id => viewer.section(id) ?? []))
86
- return () => h('div', { 'class': 'pm-view-host', 'aria-busy': viewer.restoring.value ? 'true' : undefined }, [
87
- pageView.value ? h(pageView.value.component, { sections: sections.value }) : null,
88
- ])
89
- },
90
- })
91
-
92
- export const OverlayHost = defineComponent({
93
- name: 'PmOverlayHost',
94
- setup() {
95
- const viewer = useViewer()
96
- return () => {
97
- const view = getView(viewer.view.value)
98
- return view?.kind === 'overlay' ? h(view.component, { key: view.id }) : null
99
- }
100
- },
101
- })
File without changes