@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.
- package/README.md +6 -4
- package/package.json +20 -8
- package/src/client/block.ts +70 -0
- package/src/client/code/code-block.ts +54 -0
- package/src/client/code/code-editor.ts +73 -0
- package/src/client/code/code-view.ts +22 -0
- package/src/{highlight.ts → client/code/highlight.ts} +2 -2
- package/src/{outputs.ts → client/code/outputs.ts} +23 -36
- package/src/client/content-marks.ts +15 -0
- package/src/client/element.ts +27 -0
- package/src/client/index.ts +40 -0
- package/src/client/layout.ts +22 -0
- package/src/{markdown.ts → client/markdown/handlers.ts} +22 -116
- package/src/client/markdown/hast.ts +20 -0
- package/src/client/markdown/layout-handlers.ts +57 -0
- package/src/client/markdown/math.ts +10 -0
- package/src/client/markdown/mermaid.ts +75 -0
- package/src/client/markdown/render.ts +79 -0
- package/src/client/markdown/role-mark.ts +9 -0
- package/src/client/markdown/tables.ts +49 -0
- package/src/client/markdown/tabs.ts +52 -0
- package/src/client/markdown/tree-cache.ts +77 -0
- package/src/client/operations.ts +9 -0
- package/src/client/registry.ts +21 -0
- package/src/client/section-context.ts +5 -0
- package/src/client/section.ts +30 -0
- package/src/client/slidev-compat/click-directives.ts +16 -0
- package/src/client/slidev-compat/slide-globals.ts +3 -0
- package/src/{style.css → client/style.css} +306 -81
- package/src/client/viewer-messages.ts +7 -0
- package/src/{plugins.ts → client/viewer-plugins.ts} +3 -2
- package/src/client/viewer-state.ts +47 -0
- package/src/client/views/actions.ts +52 -0
- package/src/client/views/controls.css +44 -0
- package/src/client/views/document.ts +12 -0
- package/src/client/views/outlets.ts +37 -0
- package/src/client/views/overlay.ts +16 -0
- package/src/client/views/present.ts +132 -0
- package/src/client/views/presenter.ts +76 -0
- package/src/client/views/scrolly.ts +4 -0
- package/src/client/views/slide.css +8 -0
- package/src/client/views/slide.ts +57 -0
- package/src/domain/clicks.ts +1 -1
- package/src/domain/code-width.ts +11 -0
- package/src/domain/document-content.ts +6 -0
- package/src/domain/{text.ts → footnotes.ts} +0 -12
- package/src/domain/index.ts +5 -4
- package/src/domain/live.ts +11 -10
- package/src/domain/messages.ts +54 -30
- package/src/domain/origin.ts +17 -32
- package/src/domain/outputs.ts +27 -0
- package/src/domain/steps.ts +2 -1
- package/src/block.ts +0 -122
- package/src/cards.ts +0 -130
- package/src/clicks.ts +0 -124
- package/src/code.ts +0 -235
- package/src/context.ts +0 -16
- package/src/dark.ts +0 -5
- package/src/domain/follow.ts +0 -32
- package/src/globals.ts +0 -66
- package/src/i18n.ts +0 -7
- package/src/index.ts +0 -55
- package/src/operations.ts +0 -28
- package/src/present.ts +0 -327
- package/src/registry.ts +0 -81
- package/src/rich.ts +0 -155
- package/src/section.ts +0 -209
- package/src/viewer.ts +0 -116
- package/src/views.ts +0 -101
- /package/src/{env.d.ts → client/env.d.ts} +0 -0
package/src/domain/messages.ts
CHANGED
|
@@ -3,40 +3,52 @@ import type { Locale } from '@lincyaw/kernel/contracts'
|
|
|
3
3
|
const zhStep = (step: number, steps: number) => `第 ${step + 1} 步,共 ${steps + 1} 步`
|
|
4
4
|
|
|
5
5
|
const zh = {
|
|
6
|
-
|
|
6
|
+
contentRoles: { visual: '上屏内容', narration: '讲稿 · 字幕', reading: '仅供阅读' },
|
|
7
|
+
views: { document: '网页', scrolly: '滚动叙事', present: '放映', presenter: '演示者视图' },
|
|
7
8
|
present: {
|
|
8
9
|
region: '放映',
|
|
10
|
+
jumpStep: '跳转步骤',
|
|
11
|
+
captions: '字幕(按步骤)',
|
|
12
|
+
captionsHint: '字幕随步骤切换',
|
|
13
|
+
noCaptions: '本步没有讲稿',
|
|
14
|
+
step: (step: number) => `第 ${step} 步`,
|
|
9
15
|
counter: (index: number, total: number, step: number, steps: number) => `${index} / ${total}${steps > 0 ? ` · ${zhStep(step, steps)}` : ''}`,
|
|
10
16
|
prev: '上一步',
|
|
11
17
|
next: '下一步',
|
|
12
18
|
overview: '总览',
|
|
13
|
-
presenter: '
|
|
19
|
+
presenter: '演示者视图',
|
|
20
|
+
refresh: '更新',
|
|
14
21
|
fullscreen: '全屏',
|
|
15
|
-
|
|
22
|
+
exitFullscreen: '退出全屏',
|
|
23
|
+
exit: '结束放映',
|
|
16
24
|
},
|
|
17
25
|
presenter: {
|
|
18
26
|
step: (step: number, steps: number) => (steps > 0 ? zhStep(step, steps) : ''),
|
|
19
27
|
timer: '计时',
|
|
20
28
|
restart: '重新计时',
|
|
21
|
-
notes: '
|
|
22
|
-
noNotes: '
|
|
29
|
+
notes: '讲稿',
|
|
30
|
+
noNotes: '本页没有讲稿',
|
|
23
31
|
upcoming: '下一步',
|
|
24
32
|
end: '已到结尾',
|
|
25
|
-
nextSection: (title: string) =>
|
|
33
|
+
nextSection: (title: string) => `下一页:${title}`,
|
|
26
34
|
nextStep: (step: number) => `第 ${step + 1} 步`,
|
|
27
35
|
},
|
|
28
36
|
section: {
|
|
29
37
|
anchor: (title: string) => `链接到“${title}”`,
|
|
30
|
-
openSource: '
|
|
31
|
-
sourceTitle: (where: string) => `${where}
|
|
38
|
+
openSource: '在编辑器中打开',
|
|
39
|
+
sourceTitle: (where: string) => `${where}(按住 Alt 单击任一内容块,在编辑器中打开)`,
|
|
32
40
|
},
|
|
33
41
|
code: {
|
|
34
|
-
|
|
42
|
+
actions: '代码操作',
|
|
43
|
+
executorFailed: (_id: string, message: string) => `无法运行代码:${message}`,
|
|
35
44
|
run: '运行',
|
|
36
|
-
rerun: '
|
|
37
|
-
running: '
|
|
38
|
-
unavailable: '
|
|
39
|
-
|
|
45
|
+
rerun: '重新运行',
|
|
46
|
+
running: '正在运行…',
|
|
47
|
+
unavailable: '此处无法运行代码',
|
|
48
|
+
resetRunSession: '重置会话',
|
|
49
|
+
edit: '编辑代码',
|
|
50
|
+
doneEditing: '完成编辑',
|
|
51
|
+
revert: '还原代码',
|
|
40
52
|
},
|
|
41
53
|
markdown: {
|
|
42
54
|
admonitions: {
|
|
@@ -53,46 +65,58 @@ const zh = {
|
|
|
53
65
|
} as Record<string, string>,
|
|
54
66
|
footnote: (label: string) => `脚注 ${label}`,
|
|
55
67
|
},
|
|
56
|
-
missingComponent: (
|
|
68
|
+
missingComponent: (_name: string) => '无法显示此内容块',
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
export type Messages = typeof zh
|
|
60
72
|
|
|
61
73
|
const en: Messages = {
|
|
62
|
-
|
|
74
|
+
contentRoles: { visual: 'On screen', narration: 'Script · Captions', reading: 'Reading only' },
|
|
75
|
+
views: { document: 'Web', scrolly: 'Scrollytelling', present: 'Present', presenter: 'Presenter View' },
|
|
63
76
|
present: {
|
|
64
77
|
region: 'Presentation',
|
|
78
|
+
jumpStep: 'Jump to Step',
|
|
79
|
+
captions: 'Captions by step',
|
|
80
|
+
captionsHint: 'Captions follow the selected step',
|
|
81
|
+
noCaptions: 'No script for this step',
|
|
82
|
+
step: step => `Step ${step}`,
|
|
65
83
|
counter: (index, total, step, steps) => `${index} / ${total}${steps > 0 ? ` · step ${step + 1} of ${steps + 1}` : ''}`,
|
|
66
84
|
prev: 'Previous',
|
|
67
85
|
next: 'Next',
|
|
68
86
|
overview: 'Overview',
|
|
69
|
-
presenter: 'Presenter
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
presenter: 'Presenter View',
|
|
88
|
+
refresh: 'Update',
|
|
89
|
+
fullscreen: 'Full Screen',
|
|
90
|
+
exitFullscreen: 'Exit Full Screen',
|
|
91
|
+
exit: 'End Show',
|
|
72
92
|
},
|
|
73
93
|
presenter: {
|
|
74
94
|
step: (step, steps) => (steps > 0 ? `Step ${step + 1} of ${steps + 1}` : ''),
|
|
75
95
|
timer: 'Timer',
|
|
76
|
-
restart: 'Restart
|
|
77
|
-
notes: '
|
|
78
|
-
noNotes: 'No
|
|
96
|
+
restart: 'Restart Timer',
|
|
97
|
+
notes: 'Script',
|
|
98
|
+
noNotes: 'No script on this page',
|
|
79
99
|
upcoming: 'Next',
|
|
80
100
|
end: 'End of the document',
|
|
81
|
-
nextSection: title => `Next
|
|
101
|
+
nextSection: title => `Next page: ${title}`,
|
|
82
102
|
nextStep: step => `Step ${step + 1}`,
|
|
83
103
|
},
|
|
84
104
|
section: {
|
|
85
|
-
anchor: title => `Link to
|
|
86
|
-
openSource: 'Open
|
|
87
|
-
sourceTitle: where => `${where} (Alt+click
|
|
105
|
+
anchor: title => `Link to “${title}”`,
|
|
106
|
+
openSource: 'Open in Editor',
|
|
107
|
+
sourceTitle: where => `${where} (Alt+click any block to open it in the editor)`,
|
|
88
108
|
},
|
|
89
109
|
code: {
|
|
90
|
-
|
|
110
|
+
actions: 'Code actions',
|
|
111
|
+
executorFailed: (_id, message) => `Couldn’t run the code: ${message}`,
|
|
91
112
|
run: 'Run',
|
|
92
|
-
rerun: 'Run
|
|
113
|
+
rerun: 'Run Again',
|
|
93
114
|
running: 'Running…',
|
|
94
|
-
unavailable: '
|
|
95
|
-
|
|
115
|
+
unavailable: 'Code can’t run here',
|
|
116
|
+
resetRunSession: 'Reset Session',
|
|
117
|
+
edit: 'Edit Code',
|
|
118
|
+
doneEditing: 'Done Editing',
|
|
119
|
+
revert: 'Restore Code',
|
|
96
120
|
},
|
|
97
121
|
markdown: {
|
|
98
122
|
admonitions: {
|
|
@@ -109,7 +133,7 @@ const en: Messages = {
|
|
|
109
133
|
},
|
|
110
134
|
footnote: label => `Footnote ${label}`,
|
|
111
135
|
},
|
|
112
|
-
missingComponent:
|
|
136
|
+
missingComponent: () => 'This block can’t be shown',
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
export const VIEWER_MESSAGES: Record<Locale, Messages> = { 'zh-CN': zh, 'en': en }
|
package/src/domain/origin.ts
CHANGED
|
@@ -1,42 +1,36 @@
|
|
|
1
|
-
import type { CodeNode, ResolutionStatus
|
|
1
|
+
import type { CodeNode, ResolutionStatus } from '@lincyaw/kernel'
|
|
2
2
|
import type { Locale } from '@lincyaw/kernel/contracts'
|
|
3
3
|
|
|
4
4
|
const zh = {
|
|
5
|
-
workingTree: '
|
|
6
|
-
status: { moved: '已移动', fuzzy: '
|
|
5
|
+
workingTree: '当前代码',
|
|
6
|
+
status: { moved: '已移动', fuzzy: '待核对', lost: '已失效' } as Partial<Record<ResolutionStatus, string>>,
|
|
7
7
|
statusTitle: {
|
|
8
|
-
moved: '
|
|
9
|
-
fuzzy: '
|
|
10
|
-
lost: '
|
|
8
|
+
moved: '引用的代码在文件中移动了位置,显示的是移动后的代码。',
|
|
9
|
+
fuzzy: '引用的代码有改动,找到的位置有待确认;请运行 presmium check 查看。',
|
|
10
|
+
lost: '找不到引用的代码。请运行 presmium check 查看原因。',
|
|
11
11
|
} as Partial<Record<ResolutionStatus, string>>,
|
|
12
12
|
drifted: '已漂移',
|
|
13
|
-
driftedTitle: '
|
|
13
|
+
driftedTitle: '当前代码已改变。请运行 presmium check 查看差异。',
|
|
14
14
|
synced: '已锁定',
|
|
15
|
-
syncedTitle: '
|
|
15
|
+
syncedTitle: '锁定的代码与当前代码一致',
|
|
16
16
|
openFile: (file: string) => `在仓库中打开 ${file}`,
|
|
17
|
-
timedOut: '超时',
|
|
18
|
-
exitCode: (code: number) => `退出码 ${code}`,
|
|
19
|
-
recorded: '预录输出',
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
export type OriginMessages = typeof zh
|
|
23
20
|
|
|
24
21
|
const en: OriginMessages = {
|
|
25
|
-
workingTree: '
|
|
26
|
-
status: { moved: 'Moved', fuzzy: '
|
|
22
|
+
workingTree: 'Current Code',
|
|
23
|
+
status: { moved: 'Moved', fuzzy: 'Unverified', lost: 'Lost' },
|
|
27
24
|
statusTitle: {
|
|
28
|
-
moved: 'The referenced code moved in its file; this is the code where it is now',
|
|
29
|
-
fuzzy: 'The referenced code changed and the place found is uncertain; run presmium check to see it',
|
|
30
|
-
lost: 'The referenced code is no longer found; run presmium check to see why',
|
|
25
|
+
moved: 'The referenced code moved in its file; this is the code where it is now.',
|
|
26
|
+
fuzzy: 'The referenced code changed and the place found is uncertain; run presmium check to see it.',
|
|
27
|
+
lost: 'The referenced code is no longer found; run presmium check to see why.',
|
|
31
28
|
},
|
|
32
29
|
drifted: 'Drifted',
|
|
33
|
-
driftedTitle: 'The code changed
|
|
30
|
+
driftedTitle: 'The current code changed; run presmium check to see the difference.',
|
|
34
31
|
synced: 'Pinned',
|
|
35
|
-
syncedTitle: 'The pinned code matches the
|
|
36
|
-
openFile: file => `Open ${file} in the
|
|
37
|
-
timedOut: 'Timed out',
|
|
38
|
-
exitCode: code => `Exit code ${code}`,
|
|
39
|
-
recorded: 'Recorded output',
|
|
32
|
+
syncedTitle: 'The pinned code matches the current code',
|
|
33
|
+
openFile: file => `Open ${file} in the Repository`,
|
|
40
34
|
}
|
|
41
35
|
|
|
42
36
|
export const ORIGIN_MESSAGES: Record<Locale, OriginMessages> = { 'zh-CN': zh, 'en': en }
|
|
@@ -60,7 +54,7 @@ function lines(start: number, end = start): string {
|
|
|
60
54
|
return end > start ? `${start}-${end}` : `${start}`
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
export function
|
|
57
|
+
export function originBar(node: CodeNode, shown: number | undefined, locale: Locale): OriginBar | undefined {
|
|
64
58
|
const t = ORIGIN_MESSAGES[locale]
|
|
65
59
|
const { origin } = node
|
|
66
60
|
if (!origin) return node.title ? { parts: [{ kind: 'file', text: node.title }] } : undefined
|
|
@@ -82,12 +76,3 @@ export function originParts(node: CodeNode, shown: number | undefined, locale: L
|
|
|
82
76
|
}
|
|
83
77
|
return { anchor: origin.anchor, parts }
|
|
84
78
|
}
|
|
85
|
-
|
|
86
|
-
export function outputNotes(run: RunResult, locale: Locale): string[] {
|
|
87
|
-
const t = ORIGIN_MESSAGES[locale]
|
|
88
|
-
return [
|
|
89
|
-
run.timedOut ? t.timedOut : '',
|
|
90
|
-
run.exitCode !== null && run.exitCode !== 0 ? t.exitCode(run.exitCode) : '',
|
|
91
|
-
run.source === 'recorded' ? t.recorded : '',
|
|
92
|
-
].filter(Boolean)
|
|
93
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RunResult } from '@lincyaw/kernel'
|
|
2
|
+
import type { Locale } from '@lincyaw/kernel/contracts'
|
|
3
|
+
|
|
4
|
+
const zh = {
|
|
5
|
+
timedOut: '超时',
|
|
6
|
+
exitCode: (code: number) => `退出码 ${code}`,
|
|
7
|
+
recorded: '预录输出',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type OutputMessages = typeof zh
|
|
11
|
+
|
|
12
|
+
const en: OutputMessages = {
|
|
13
|
+
timedOut: 'Timed out',
|
|
14
|
+
exitCode: code => `Exit code ${code}`,
|
|
15
|
+
recorded: 'Recorded output',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const OUTPUT_MESSAGES: Record<Locale, OutputMessages> = { 'zh-CN': zh, 'en': en }
|
|
19
|
+
|
|
20
|
+
export function outputNotes(run: RunResult, locale: Locale): string[] {
|
|
21
|
+
const t = OUTPUT_MESSAGES[locale]
|
|
22
|
+
return [
|
|
23
|
+
run.timedOut ? t.timedOut : '',
|
|
24
|
+
run.exitCode !== null && run.exitCode !== 0 ? t.exitCode(run.exitCode) : '',
|
|
25
|
+
run.source === 'recorded' ? t.recorded : '',
|
|
26
|
+
].filter(Boolean)
|
|
27
|
+
}
|
package/src/domain/steps.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const FINAL_STEP = Number.POSITIVE_INFINITY
|
|
|
5
5
|
export interface Position {
|
|
6
6
|
section: string
|
|
7
7
|
step: number
|
|
8
|
+
boundary?: 'start' | 'end'
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function itemVisible(block: IRBlock, item: number, step: number): boolean {
|
|
@@ -19,7 +20,7 @@ export function clampPosition(doc: IRDocument, position: Position): Position {
|
|
|
19
20
|
const section = doc.sections.find(s => s.id === position.section) ?? doc.sections[0]
|
|
20
21
|
if (!section) return position
|
|
21
22
|
const step = Number.isFinite(position.step) ? Math.min(Math.max(Math.round(position.step), 0), section.steps) : section.steps
|
|
22
|
-
return { section: section.id, step }
|
|
23
|
+
return { section: section.id, step, ...position.boundary ? { boundary: position.boundary } : {} }
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function keepPosition(prev: IRDocument, next: IRDocument, position: Position): Position {
|
package/src/block.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { PmBoundary, provideStepScope } from '@lincyaw/host'
|
|
2
|
-
import { blockVisible, fnv1a64 } from '@lincyaw/kernel'
|
|
3
|
-
import type { ComponentNode, IRBlock, IRSection } from '@lincyaw/kernel'
|
|
4
|
-
import type { InjectionKey, PropType, VNode } from 'vue'
|
|
5
|
-
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, shallowRef } from 'vue'
|
|
6
|
-
import { BLOCK_CLASS, trackClicks } from './clicks.ts'
|
|
7
|
-
import { CodeBlockView } from './code.ts'
|
|
8
|
-
import { IN_COMPONENT } from './context.ts'
|
|
9
|
-
import { FINAL_STEP } from './domain/index.ts'
|
|
10
|
-
import { messages } from './i18n.ts'
|
|
11
|
-
import { hastToVNodes, markdownToHast, MarkdownView } from './markdown.ts'
|
|
12
|
-
import { getComponent, getNodeRenderer } from './registry.ts'
|
|
13
|
-
import { PmMermaid } from './rich.ts'
|
|
14
|
-
import { VIEWER } from './viewer.ts'
|
|
15
|
-
|
|
16
|
-
const blockProps = {
|
|
17
|
-
block: { type: Object as PropType<IRBlock>, required: true },
|
|
18
|
-
section: { type: Object as PropType<IRSection>, required: true },
|
|
19
|
-
step: { type: Number, required: true },
|
|
20
|
-
expanded: Boolean,
|
|
21
|
-
} as const
|
|
22
|
-
|
|
23
|
-
const COMPONENT_NODE: InjectionKey<{ node: ComponentNode, section: string }> = Symbol('presmium.viewer.componentNode')
|
|
24
|
-
|
|
25
|
-
function fallbackOf(node: ComponentNode, section: string): VNode[] {
|
|
26
|
-
return node.fallback ? hastToVNodes(markdownToHast(node.fallback, section).children, {}) as VNode[] : []
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const PmComponentFallback = defineComponent({
|
|
30
|
-
name: 'PmComponentFallback',
|
|
31
|
-
setup() {
|
|
32
|
-
const current = inject(COMPONENT_NODE, undefined)
|
|
33
|
-
return () => (current ? fallbackOf(current.node, current.section) : null)
|
|
34
|
-
},
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const ComponentView = defineComponent({
|
|
38
|
-
name: 'PmComponentNode',
|
|
39
|
-
props: blockProps,
|
|
40
|
-
setup(props) {
|
|
41
|
-
provide(IN_COMPONENT, true)
|
|
42
|
-
provide(COMPONENT_NODE, {
|
|
43
|
-
get node() {
|
|
44
|
-
return props.block.node as ComponentNode
|
|
45
|
-
},
|
|
46
|
-
get section() {
|
|
47
|
-
return props.section.id
|
|
48
|
-
},
|
|
49
|
-
})
|
|
50
|
-
const content = computed(() => {
|
|
51
|
-
const { props: values, children } = props.block.node as ComponentNode
|
|
52
|
-
return fnv1a64(JSON.stringify([values, children ?? []]))
|
|
53
|
-
})
|
|
54
|
-
return () => {
|
|
55
|
-
const node = props.block.node as ComponentNode
|
|
56
|
-
const component = getComponent(node.name)
|
|
57
|
-
if (!component) {
|
|
58
|
-
const fallback = fallbackOf(node, props.section.id)
|
|
59
|
-
return h('div', { 'class': 'pm-component-missing', 'data-component': node.name }, fallback.length ? fallback : messages().missingComponent(node.name))
|
|
60
|
-
}
|
|
61
|
-
const children = node.children?.length
|
|
62
|
-
? { default: () => node.children!.map(child => h(BlockView, { key: child.id, block: child, section: props.section, step: props.step, expanded: props.expanded })) }
|
|
63
|
-
: undefined
|
|
64
|
-
const resetKey = props.expanded ? content.value : `${content.value}@${props.step}`
|
|
65
|
-
return h(PmBoundary, { name: node.name, resetKey }, () => h(component, { ...node.props }, children))
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
function content(props: { block: IRBlock, section: IRSection, step: number, expanded: boolean }): VNode {
|
|
71
|
-
const { node } = props.block
|
|
72
|
-
const custom = (node.type === 'component' && getNodeRenderer(node.name))
|
|
73
|
-
|| (node.type === 'code' && getNodeRenderer(`code:${node.lang}`))
|
|
74
|
-
|| getNodeRenderer(node.type)
|
|
75
|
-
if (custom) return h(custom, props)
|
|
76
|
-
switch (node.type) {
|
|
77
|
-
case 'markdown': return h(MarkdownView, { block: props.block, step: props.step, expanded: props.expanded, prefix: props.section.id })
|
|
78
|
-
case 'code': return node.lang === 'mermaid'
|
|
79
|
-
? h(PmMermaid, { code: node.value })
|
|
80
|
-
: h(CodeBlockView, { block: props.block, step: props.step, expanded: props.expanded })
|
|
81
|
-
case 'component': return h(ComponentView, props)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function isOverlay(block: IRBlock): boolean {
|
|
86
|
-
return block.node.type === 'component' && block.node.layer === 'overlay'
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export const BlockView = defineComponent({
|
|
90
|
-
name: 'PmBlock',
|
|
91
|
-
props: blockProps,
|
|
92
|
-
setup(props) {
|
|
93
|
-
const viewer = inject(VIEWER, undefined)
|
|
94
|
-
const el = shallowRef<HTMLElement>()
|
|
95
|
-
provideStepScope(() => props.block.at)
|
|
96
|
-
const clicks = computed(() => (props.expanded ? FINAL_STEP : props.step))
|
|
97
|
-
let stop: (() => void) | undefined
|
|
98
|
-
onMounted(() => {
|
|
99
|
-
if (el.value) stop = trackClicks(el.value, clicks)
|
|
100
|
-
})
|
|
101
|
-
onBeforeUnmount(() => stop?.())
|
|
102
|
-
const openSource = (event: MouseEvent) => {
|
|
103
|
-
if (!event.altKey || !viewer?.options.openSource) return
|
|
104
|
-
event.preventDefault()
|
|
105
|
-
event.stopPropagation()
|
|
106
|
-
viewer.options.openSource(props.block.source)
|
|
107
|
-
}
|
|
108
|
-
return () => {
|
|
109
|
-
const { block } = props
|
|
110
|
-
const visible = props.expanded || blockVisible(block, props.step)
|
|
111
|
-
return h('div', {
|
|
112
|
-
'ref': el,
|
|
113
|
-
'class': [BLOCK_CLASS, `pm-block--${block.role}`, `pm-block--${block.node.type}`, { 'pm-step-range': block.until !== undefined, 'pm-block--overlay': isOverlay(block), 'pm-hidden': !visible }],
|
|
114
|
-
'data-block': block.id,
|
|
115
|
-
'data-at': block.at,
|
|
116
|
-
'data-component': block.node.type === 'component' ? block.node.name : undefined,
|
|
117
|
-
'aria-hidden': visible ? undefined : 'true',
|
|
118
|
-
'onClick': viewer?.options.openSource ? openSource : undefined,
|
|
119
|
-
}, [content({ block, section: props.section, step: props.step, expanded: props.expanded })])
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
})
|
package/src/cards.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'reka-ui'
|
|
2
|
-
import type { MdastNode } from '@lincyaw/kernel'
|
|
3
|
-
import type { Element, ElementContent, Properties } from 'hast'
|
|
4
|
-
import type { Handler, State } from 'mdast-util-to-hast'
|
|
5
|
-
import type { PropType, VNodeChild } from 'vue'
|
|
6
|
-
import { defineComponent, h, reactive, ref, watch } from 'vue'
|
|
7
|
-
|
|
8
|
-
const WIDTHS = ['xs', 'sm', 'md', 'lg'] as const
|
|
9
|
-
|
|
10
|
-
function element(tagName: string, properties: Properties, children: ElementContent[] = []): Element {
|
|
11
|
-
return { type: 'element', tagName, properties, children }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function classes(node: MdastNode): string[] {
|
|
15
|
-
return typeof node.class === 'string' ? node.class.split(/\s+/).filter(Boolean) : []
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function isButton(node: MdastNode): boolean {
|
|
19
|
-
return classes(node).includes('button')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function gridColumns(node: MdastNode): number[] {
|
|
23
|
-
const given = Array.isArray(node.columns) ? node.columns.map(Number).filter(n => Number.isInteger(n) && n >= 1) : []
|
|
24
|
-
return WIDTHS.map((_, i) => given[Math.min(i, given.length - 1)] ?? 1)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const style = (values: Record<string, number>) => Object.entries(values).map(([key, value]) => `${key}: ${value}`).join('; ')
|
|
28
|
-
|
|
29
|
-
const grid: Handler = (state, node: MdastNode) => {
|
|
30
|
-
const columns = gridColumns(node)
|
|
31
|
-
const children = state.all(node as never).map((child) => {
|
|
32
|
-
const twelfths = child.type === 'element' ? Number(child.properties.dataPmColumns) : Number.NaN
|
|
33
|
-
if (child.type !== 'element' || !(twelfths >= 1)) return child
|
|
34
|
-
const spans = Object.fromEntries(WIDTHS.map((width, i) => [`--pm-span-${width}`, Math.min(columns[i]!, Math.max(1, Math.round(twelfths * columns[i]! / 12)))]))
|
|
35
|
-
return { ...child, properties: { ...child.properties, style: style(spans) } }
|
|
36
|
-
})
|
|
37
|
-
const widths = Object.fromEntries(WIDTHS.map((width, i) => [`--pm-grid-${width}`, columns[i]!]))
|
|
38
|
-
return element('div', { className: ['pm-grid', ...classes(node)], style: style(widths) }, children)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const gridItem: Handler = (state, node: MdastNode) =>
|
|
42
|
-
element('div', { className: ['pm-grid__item', ...classes(node)], dataPmColumns: typeof node.columns === 'number' ? node.columns : undefined }, state.all(node as never))
|
|
43
|
-
|
|
44
|
-
const card: Handler = (state: State, node: MdastNode) => {
|
|
45
|
-
const items = node.children ?? []
|
|
46
|
-
const part = (type: string) => items.find(child => child.type === type)
|
|
47
|
-
const url = typeof node.url === 'string' && node.url ? node.url : undefined
|
|
48
|
-
const header = part('header')
|
|
49
|
-
const footer = part('footer')
|
|
50
|
-
const title = part('cardTitle')
|
|
51
|
-
const content: ElementContent[] = []
|
|
52
|
-
if (header) content.push(element('header', { className: ['pm-card__header'] }, state.all(header as never)))
|
|
53
|
-
if (title || url) {
|
|
54
|
-
const words: MdastNode[] = title?.children?.length ? title.children : [{ type: 'text', value: url ?? '' }]
|
|
55
|
-
const inner = url ? [state.one({ type: 'link', url, children: words } as never, node as never) as ElementContent] : state.all({ type: 'paragraph', children: words } as never)
|
|
56
|
-
content.push(element('p', { className: ['pm-card__title'] }, inner))
|
|
57
|
-
}
|
|
58
|
-
const body = items.filter(child => child !== header && child !== title && child !== footer)
|
|
59
|
-
content.push(element('div', { className: ['pm-card__body'] }, state.all({ type: 'root', children: body } as never)))
|
|
60
|
-
if (footer) content.push(element('footer', { className: ['pm-card__footer'] }, state.all(footer as never)))
|
|
61
|
-
return element('div', { className: ['pm-card', ...url ? ['pm-card--link'] : [], ...classes(node)] }, content)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const tabSet: Handler = (state, node: MdastNode) => {
|
|
65
|
-
const panels = (node.children ?? []).filter(child => child.type === 'tabItem').map(item => element('div', {
|
|
66
|
-
className: ['pm-tabs__panel'],
|
|
67
|
-
dataTitle: typeof item.title === 'string' ? item.title : '',
|
|
68
|
-
dataSync: typeof item.sync === 'string' && item.sync ? item.sync : undefined,
|
|
69
|
-
dataSelected: item.selected === true ? 'true' : undefined,
|
|
70
|
-
}, state.all(item as never)))
|
|
71
|
-
return element('div', { className: ['pm-tabs', ...classes(node)], dataPmTabs: '' }, panels)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function cardHandlers(): Record<string, Handler> {
|
|
75
|
-
return {
|
|
76
|
-
'grid': grid,
|
|
77
|
-
'grid-item': gridItem,
|
|
78
|
-
'card': card,
|
|
79
|
-
'tabSet': tabSet,
|
|
80
|
-
'tabItem': (state, node) => element('div', {}, state.all(node)),
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface TabEntry {
|
|
85
|
-
title: string
|
|
86
|
-
sync?: string
|
|
87
|
-
selected: boolean
|
|
88
|
-
content: () => VNodeChild[]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function tabEntries(set: Element, render: (nodes: ElementContent[]) => VNodeChild[]): TabEntry[] {
|
|
92
|
-
return set.children.flatMap((panel): TabEntry[] => panel.type !== 'element' ? [] : [{
|
|
93
|
-
title: String(panel.properties.dataTitle ?? ''),
|
|
94
|
-
sync: typeof panel.properties.dataSync === 'string' ? panel.properties.dataSync : undefined,
|
|
95
|
-
selected: panel.properties.dataSelected === 'true',
|
|
96
|
-
content: () => render(panel.children),
|
|
97
|
-
}])
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const synced = reactive({ key: undefined as string | undefined, choices: 0 })
|
|
101
|
-
|
|
102
|
-
export const TabSetView = defineComponent({
|
|
103
|
-
name: 'PmTabSet',
|
|
104
|
-
props: {
|
|
105
|
-
tabs: { type: Array as PropType<TabEntry[]>, required: true },
|
|
106
|
-
},
|
|
107
|
-
setup(props) {
|
|
108
|
-
const withKey = (key?: string) => (key ? props.tabs.findIndex(tab => tab.sync === key) : -1)
|
|
109
|
-
const active = ref([withKey(synced.key), props.tabs.findIndex(tab => tab.selected), 0].find(k => k >= 0)!)
|
|
110
|
-
watch(() => synced.choices, () => {
|
|
111
|
-
const k = withKey(synced.key)
|
|
112
|
-
if (k >= 0) active.value = k
|
|
113
|
-
})
|
|
114
|
-
const choose = (k: number) => {
|
|
115
|
-
active.value = k
|
|
116
|
-
const key = props.tabs[k]?.sync
|
|
117
|
-
if (key) {
|
|
118
|
-
synced.key = key
|
|
119
|
-
synced.choices++
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return () => h(TabsRoot, { modelValue: String(active.value), 'onUpdate:modelValue': value => choose(Number(value)) }, () => [
|
|
123
|
-
h(TabsList, { class: 'pm-tabs__list' }, () => props.tabs.map((tab, k) =>
|
|
124
|
-
h(TabsTrigger, { key: k, value: String(k), class: 'pm-tabs__tab' }, () => tab.title))),
|
|
125
|
-
...props.tabs.map((tab, k) => h(TabsContent, {
|
|
126
|
-
key: k, value: String(k), class: 'pm-tabs__panel', forceMount: true, hidden: k !== active.value, tabindex: 0,
|
|
127
|
-
}, tab.content)),
|
|
128
|
-
])
|
|
129
|
-
},
|
|
130
|
-
})
|
package/src/clicks.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import type { App, Directive, DirectiveBinding, Ref } from 'vue'
|
|
2
|
-
import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watchEffect, withDirectives } from 'vue'
|
|
3
|
-
import { clickVisible, resolveClicks } from './domain/index.ts'
|
|
4
|
-
import type { ClickRange, ClickSpec } from './domain/index.ts'
|
|
5
|
-
|
|
6
|
-
interface Controller {
|
|
7
|
-
add(el: HTMLElement, spec: ClickSpec): void
|
|
8
|
-
remove(el: HTMLElement): void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const BLOCK_CLASS = 'pm-block'
|
|
12
|
-
|
|
13
|
-
const controllers = new WeakMap<Element, Controller>()
|
|
14
|
-
const owners = new WeakMap<HTMLElement, Controller>()
|
|
15
|
-
const pending = new Map<HTMLElement, ClickSpec>()
|
|
16
|
-
|
|
17
|
-
function register(el: HTMLElement, spec: ClickSpec): void {
|
|
18
|
-
const block = el.closest(`.${BLOCK_CLASS}`)
|
|
19
|
-
const controller = block ? controllers.get(block) : undefined
|
|
20
|
-
if (!controller) {
|
|
21
|
-
pending.set(el, spec)
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
|
-
owners.set(el, controller)
|
|
25
|
-
controller.add(el, spec)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function unregister(el: HTMLElement): void {
|
|
29
|
-
pending.delete(el)
|
|
30
|
-
owners.get(el)?.remove(el)
|
|
31
|
-
owners.delete(el)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function trackClicks(block: HTMLElement, step: Readonly<Ref<number>>): () => void {
|
|
35
|
-
const specs = new Map<HTMLElement, ClickSpec>()
|
|
36
|
-
const ranges = shallowRef(new Map<HTMLElement, ClickRange>())
|
|
37
|
-
const resolve = () => {
|
|
38
|
-
const ordered = [...specs.keys()].sort((a, b) => (a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1))
|
|
39
|
-
const resolved = resolveClicks(ordered.map(el => specs.get(el)!))
|
|
40
|
-
ranges.value = new Map(ordered.map((el, i) => [el, resolved[i]!]))
|
|
41
|
-
}
|
|
42
|
-
const controller: Controller = {
|
|
43
|
-
add(el, spec) {
|
|
44
|
-
specs.set(el, spec)
|
|
45
|
-
resolve()
|
|
46
|
-
},
|
|
47
|
-
remove(el) {
|
|
48
|
-
if (specs.delete(el)) resolve()
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
controllers.set(block, controller)
|
|
52
|
-
for (const [el, spec] of pending) {
|
|
53
|
-
if (!block.contains(el)) continue
|
|
54
|
-
pending.delete(el)
|
|
55
|
-
owners.set(el, controller)
|
|
56
|
-
specs.set(el, spec)
|
|
57
|
-
}
|
|
58
|
-
resolve()
|
|
59
|
-
const stop = watchEffect(() => {
|
|
60
|
-
for (const [el, range] of ranges.value) el.dataset.pmClick = clickVisible(range, step.value) ? 'shown' : 'hidden'
|
|
61
|
-
})
|
|
62
|
-
return () => {
|
|
63
|
-
stop()
|
|
64
|
-
controllers.delete(block)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function clickDirective(kind: ClickSpec['kind'], hide = false): Directive<HTMLElement, unknown> {
|
|
69
|
-
const spec = (binding: DirectiveBinding<unknown>): ClickSpec => ({ kind, at: binding.value, hide: hide || Boolean(binding.modifiers.hide) })
|
|
70
|
-
return {
|
|
71
|
-
mounted(el, binding) {
|
|
72
|
-
register(el, spec(binding))
|
|
73
|
-
},
|
|
74
|
-
updated(el, binding) {
|
|
75
|
-
if (JSON.stringify(binding.value) === JSON.stringify(binding.oldValue)) return
|
|
76
|
-
unregister(el)
|
|
77
|
-
register(el, spec(binding))
|
|
78
|
-
},
|
|
79
|
-
unmounted(el) {
|
|
80
|
-
unregister(el)
|
|
81
|
-
},
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const vClick = clickDirective('click')
|
|
86
|
-
const vAfter = clickDirective('after')
|
|
87
|
-
|
|
88
|
-
const clickProps = { at: { type: [Number, String, Array], default: undefined }, hide: Boolean }
|
|
89
|
-
|
|
90
|
-
function clickComponent(name: string, directive: Directive) {
|
|
91
|
-
return defineComponent({
|
|
92
|
-
name,
|
|
93
|
-
props: clickProps,
|
|
94
|
-
setup(props, { slots }) {
|
|
95
|
-
return () => withDirectives(h('span', { class: 'pm-v-click' }, slots.default?.()), [[directive, props.at, '', { hide: props.hide }]])
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const VClicks = defineComponent({
|
|
101
|
-
name: 'VClicks',
|
|
102
|
-
props: { every: { type: [Number, String], default: 1 }, hide: Boolean },
|
|
103
|
-
setup(props, { slots }) {
|
|
104
|
-
const root = shallowRef<HTMLElement>()
|
|
105
|
-
let items: HTMLElement[] = []
|
|
106
|
-
onMounted(() => {
|
|
107
|
-
const children = [...root.value!.children] as HTMLElement[]
|
|
108
|
-
items = children.flatMap(child => (/^(UL|OL)$/.test(child.tagName) ? [...child.children] as HTMLElement[] : [child]))
|
|
109
|
-
const every = Math.max(1, Number(props.every) || 1)
|
|
110
|
-
items.forEach((item, i) => register(item, { kind: i % every === 0 ? 'click' : 'after', hide: props.hide }))
|
|
111
|
-
})
|
|
112
|
-
onBeforeUnmount(() => items.forEach(unregister))
|
|
113
|
-
return () => h('div', { ref: root, class: 'pm-v-clicks' }, slots.default?.())
|
|
114
|
-
},
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
export function installClicks(app: App): void {
|
|
118
|
-
app.directive('click', vClick)
|
|
119
|
-
app.directive('after', vAfter)
|
|
120
|
-
app.directive('click-hide', clickDirective('click', true))
|
|
121
|
-
app.component('VClick', clickComponent('VClick', vClick))
|
|
122
|
-
app.component('VAfter', clickComponent('VAfter', vAfter))
|
|
123
|
-
app.component('VClicks', VClicks)
|
|
124
|
-
}
|