@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/LICENSE +202 -0
- package/README.md +29 -2
- package/package.json +53 -3
- package/src/block.ts +122 -0
- package/src/cards.ts +130 -0
- package/src/clicks.ts +124 -0
- package/src/code.ts +235 -0
- package/src/context.ts +16 -0
- package/src/dark.ts +5 -0
- package/src/domain/clicks.ts +38 -0
- package/src/domain/follow.ts +32 -0
- package/src/domain/index.ts +21 -0
- package/src/domain/live.ts +43 -0
- package/src/domain/messages.ts +115 -0
- package/src/domain/origin.ts +93 -0
- package/src/domain/steps.ts +45 -0
- package/src/domain/text.ts +27 -0
- package/src/env.d.ts +3 -0
- package/src/globals.ts +66 -0
- package/src/highlight.ts +73 -0
- package/src/i18n.ts +7 -0
- package/src/index.ts +55 -0
- package/src/markdown.ts +255 -0
- package/src/operations.ts +28 -0
- package/src/outputs.ts +97 -0
- package/src/plugins.ts +34 -0
- package/src/present.ts +327 -0
- package/src/registry.ts +81 -0
- package/src/rich.ts +155 -0
- package/src/section.ts +209 -0
- package/src/style.css +833 -0
- package/src/viewer.ts +116 -0
- package/src/views.ts +101 -0
package/src/viewer.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
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
|
+
})
|