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

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 (54) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +31 -2
  3. package/package.json +65 -3
  4. package/src/client/block.ts +70 -0
  5. package/src/client/code/code-block.ts +54 -0
  6. package/src/client/code/code-editor.ts +73 -0
  7. package/src/client/code/code-view.ts +22 -0
  8. package/src/client/code/highlight.ts +73 -0
  9. package/src/client/code/outputs.ts +84 -0
  10. package/src/client/content-marks.ts +15 -0
  11. package/src/client/element.ts +27 -0
  12. package/src/client/env.d.ts +3 -0
  13. package/src/client/index.ts +40 -0
  14. package/src/client/layout.ts +22 -0
  15. package/src/client/markdown/handlers.ts +161 -0
  16. package/src/client/markdown/hast.ts +20 -0
  17. package/src/client/markdown/layout-handlers.ts +57 -0
  18. package/src/client/markdown/math.ts +10 -0
  19. package/src/client/markdown/mermaid.ts +75 -0
  20. package/src/client/markdown/render.ts +79 -0
  21. package/src/client/markdown/role-mark.ts +9 -0
  22. package/src/client/markdown/tables.ts +49 -0
  23. package/src/client/markdown/tabs.ts +52 -0
  24. package/src/client/markdown/tree-cache.ts +77 -0
  25. package/src/client/operations.ts +9 -0
  26. package/src/client/registry.ts +21 -0
  27. package/src/client/section-context.ts +5 -0
  28. package/src/client/section.ts +30 -0
  29. package/src/client/slidev-compat/click-directives.ts +16 -0
  30. package/src/client/slidev-compat/slide-globals.ts +3 -0
  31. package/src/client/style.css +1058 -0
  32. package/src/client/viewer-messages.ts +7 -0
  33. package/src/client/viewer-plugins.ts +35 -0
  34. package/src/client/viewer-state.ts +47 -0
  35. package/src/client/views/actions.ts +52 -0
  36. package/src/client/views/controls.css +44 -0
  37. package/src/client/views/document.ts +12 -0
  38. package/src/client/views/outlets.ts +37 -0
  39. package/src/client/views/overlay.ts +16 -0
  40. package/src/client/views/present.ts +132 -0
  41. package/src/client/views/presenter.ts +76 -0
  42. package/src/client/views/scrolly.ts +4 -0
  43. package/src/client/views/slide.css +8 -0
  44. package/src/client/views/slide.ts +57 -0
  45. package/src/domain/clicks.ts +38 -0
  46. package/src/domain/code-width.ts +11 -0
  47. package/src/domain/document-content.ts +6 -0
  48. package/src/domain/footnotes.ts +15 -0
  49. package/src/domain/index.ts +22 -0
  50. package/src/domain/live.ts +44 -0
  51. package/src/domain/messages.ts +139 -0
  52. package/src/domain/origin.ts +78 -0
  53. package/src/domain/outputs.ts +27 -0
  54. package/src/domain/steps.ts +46 -0
@@ -0,0 +1,22 @@
1
+ export { clickVisible, resolveClicks } from './clicks.ts'
2
+ export type { ClickRange, ClickSpec } from './clicks.ts'
3
+ export { codeColumns } from './code-width.ts'
4
+ export { footnoteSections } from './footnotes.ts'
5
+ export { applyUpdate, diffDocuments, sectionDiagnostics } from './live.ts'
6
+ export type { LiveUpdate } from './live.ts'
7
+ export { VIEWER_MESSAGES } from './messages.ts'
8
+ export type { Messages } from './messages.ts'
9
+ export { ORIGIN_MESSAGES, originBar } from './origin.ts'
10
+ export type { OriginBar, OriginMessages, OriginPart } from './origin.ts'
11
+ export { OUTPUT_MESSAGES, outputNotes } from './outputs.ts'
12
+ export type { OutputMessages } from './outputs.ts'
13
+ export {
14
+ clampPosition,
15
+ FINAL_STEP,
16
+ itemVisible,
17
+ keepPosition,
18
+ nextPosition,
19
+ prevPosition,
20
+ stepAtLine,
21
+ } from './steps.ts'
22
+ export type { Position } from './steps.ts'
@@ -0,0 +1,44 @@
1
+ import type { Finding, IRDocument, IRSection } from '@lincyaw/kernel'
2
+
3
+ export interface LiveUpdate {
4
+ order: string[]
5
+ sections: IRSection[]
6
+ diagnostics: Finding[]
7
+ document?: Omit<IRDocument, 'sections' | 'diagnostics'>
8
+ }
9
+
10
+ function key(value: unknown): string {
11
+ return JSON.stringify(value)
12
+ }
13
+ const documentFields = ({ sections: _sections, diagnostics: _diagnostics, ...document }: IRDocument) => document
14
+
15
+ export function diffDocuments(prev: IRDocument, next: IRDocument): LiveUpdate | undefined {
16
+ const old = new Map(prev.sections.map(s => [s.id, s]))
17
+ const sections = next.sections.filter(s => {
18
+ const previous = old.get(s.id)
19
+ return previous !== s && (previous === undefined || key(previous) !== key(s))
20
+ })
21
+ const order = next.sections.map(s => s.id)
22
+ const update: LiveUpdate = { order, sections, diagnostics: next.diagnostics }
23
+ const document = documentFields(next)
24
+ if (key(documentFields(prev)) !== key(document)) update.document = document
25
+ const reordered = key(order) !== key(prev.sections.map(s => s.id))
26
+ const changed = sections.length > 0 || reordered || update.document || key(prev.diagnostics) !== key(next.diagnostics)
27
+ return changed ? update : undefined
28
+ }
29
+
30
+ export function applyUpdate(doc: IRDocument, update: LiveUpdate): IRDocument {
31
+ const known = new Map(doc.sections.map(s => [s.id, s]))
32
+ for (const section of update.sections) known.set(section.id, section)
33
+ return {
34
+ ...(update.document ?? doc),
35
+ sections: update.order.flatMap(id => known.get(id) ?? []),
36
+ diagnostics: update.diagnostics,
37
+ }
38
+ }
39
+
40
+ export function sectionDiagnostics(section: IRSection, diagnostics: readonly Finding[]): Finding[] {
41
+ const ranges = [section.source, ...section.blocks.map(b => b.source)]
42
+ return diagnostics.filter(({ source }) => source?.line !== undefined && ranges.some(range =>
43
+ range.file === source.file && source.line! >= range.line && source.line! <= (range.endLine ?? range.line)))
44
+ }
@@ -0,0 +1,139 @@
1
+ import type { Locale } from '@lincyaw/kernel/contracts'
2
+
3
+ const zhStep = (step: number, steps: number) => `第 ${step + 1} 步,共 ${steps + 1} 步`
4
+
5
+ const zh = {
6
+ contentRoles: { visual: '上屏内容', narration: '讲稿 · 字幕', reading: '仅供阅读' },
7
+ views: { document: '网页', scrolly: '滚动叙事', present: '放映', presenter: '演示者视图' },
8
+ present: {
9
+ region: '放映',
10
+ jumpStep: '跳转步骤',
11
+ captions: '字幕(按步骤)',
12
+ captionsHint: '字幕随步骤切换',
13
+ noCaptions: '本步没有讲稿',
14
+ step: (step: number) => `第 ${step} 步`,
15
+ counter: (index: number, total: number, step: number, steps: number) => `${index} / ${total}${steps > 0 ? ` · ${zhStep(step, steps)}` : ''}`,
16
+ prev: '上一步',
17
+ next: '下一步',
18
+ overview: '总览',
19
+ presenter: '演示者视图',
20
+ refresh: '更新',
21
+ fullscreen: '全屏',
22
+ exitFullscreen: '退出全屏',
23
+ exit: '结束放映',
24
+ },
25
+ presenter: {
26
+ step: (step: number, steps: number) => (steps > 0 ? zhStep(step, steps) : ''),
27
+ timer: '计时',
28
+ restart: '重新计时',
29
+ notes: '讲稿',
30
+ noNotes: '本页没有讲稿',
31
+ upcoming: '下一步',
32
+ end: '已到结尾',
33
+ nextSection: (title: string) => `下一页:${title}`,
34
+ nextStep: (step: number) => `第 ${step + 1} 步`,
35
+ },
36
+ section: {
37
+ anchor: (title: string) => `链接到“${title}”`,
38
+ openSource: '在编辑器中打开',
39
+ sourceTitle: (where: string) => `${where}(按住 Alt 单击任一内容块,在编辑器中打开)`,
40
+ },
41
+ code: {
42
+ actions: '代码操作',
43
+ executorFailed: (_id: string, message: string) => `无法运行代码:${message}`,
44
+ run: '运行',
45
+ rerun: '重新运行',
46
+ running: '正在运行…',
47
+ unavailable: '此处无法运行代码',
48
+ resetRunSession: '重置会话',
49
+ edit: '编辑代码',
50
+ doneEditing: '完成编辑',
51
+ revert: '还原代码',
52
+ },
53
+ markdown: {
54
+ admonitions: {
55
+ note: '注意',
56
+ tip: '提示',
57
+ hint: '提示',
58
+ important: '重要',
59
+ warning: '警告',
60
+ caution: '小心',
61
+ attention: '注意',
62
+ danger: '危险',
63
+ error: '错误',
64
+ seealso: '另请参阅',
65
+ } as Record<string, string>,
66
+ footnote: (label: string) => `脚注 ${label}`,
67
+ },
68
+ missingComponent: (_name: string) => '无法显示此内容块',
69
+ }
70
+
71
+ export type Messages = typeof zh
72
+
73
+ const en: Messages = {
74
+ contentRoles: { visual: 'On screen', narration: 'Script · Captions', reading: 'Reading only' },
75
+ views: { document: 'Web', scrolly: 'Scrollytelling', present: 'Present', presenter: 'Presenter View' },
76
+ present: {
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}`,
83
+ counter: (index, total, step, steps) => `${index} / ${total}${steps > 0 ? ` · step ${step + 1} of ${steps + 1}` : ''}`,
84
+ prev: 'Previous',
85
+ next: 'Next',
86
+ overview: 'Overview',
87
+ presenter: 'Presenter View',
88
+ refresh: 'Update',
89
+ fullscreen: 'Full Screen',
90
+ exitFullscreen: 'Exit Full Screen',
91
+ exit: 'End Show',
92
+ },
93
+ presenter: {
94
+ step: (step, steps) => (steps > 0 ? `Step ${step + 1} of ${steps + 1}` : ''),
95
+ timer: 'Timer',
96
+ restart: 'Restart Timer',
97
+ notes: 'Script',
98
+ noNotes: 'No script on this page',
99
+ upcoming: 'Next',
100
+ end: 'End of the document',
101
+ nextSection: title => `Next page: ${title}`,
102
+ nextStep: step => `Step ${step + 1}`,
103
+ },
104
+ section: {
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)`,
108
+ },
109
+ code: {
110
+ actions: 'Code actions',
111
+ executorFailed: (_id, message) => `Couldn’t run the code: ${message}`,
112
+ run: 'Run',
113
+ rerun: 'Run Again',
114
+ running: 'Running…',
115
+ unavailable: 'Code can’t run here',
116
+ resetRunSession: 'Reset Session',
117
+ edit: 'Edit Code',
118
+ doneEditing: 'Done Editing',
119
+ revert: 'Restore Code',
120
+ },
121
+ markdown: {
122
+ admonitions: {
123
+ note: 'Note',
124
+ tip: 'Tip',
125
+ hint: 'Hint',
126
+ important: 'Important',
127
+ warning: 'Warning',
128
+ caution: 'Caution',
129
+ attention: 'Attention',
130
+ danger: 'Danger',
131
+ error: 'Error',
132
+ seealso: 'See also',
133
+ },
134
+ footnote: label => `Footnote ${label}`,
135
+ },
136
+ missingComponent: () => 'This block can’t be shown',
137
+ }
138
+
139
+ export const VIEWER_MESSAGES: Record<Locale, Messages> = { 'zh-CN': zh, 'en': en }
@@ -0,0 +1,78 @@
1
+ import type { CodeNode, ResolutionStatus } from '@lincyaw/kernel'
2
+ import type { Locale } from '@lincyaw/kernel/contracts'
3
+
4
+ const zh = {
5
+ workingTree: '当前代码',
6
+ status: { moved: '已移动', fuzzy: '待核对', lost: '已失效' } as Partial<Record<ResolutionStatus, string>>,
7
+ statusTitle: {
8
+ moved: '引用的代码在文件中移动了位置,显示的是移动后的代码。',
9
+ fuzzy: '引用的代码有改动,找到的位置有待确认;请运行 presmium check 查看。',
10
+ lost: '找不到引用的代码。请运行 presmium check 查看原因。',
11
+ } as Partial<Record<ResolutionStatus, string>>,
12
+ drifted: '已漂移',
13
+ driftedTitle: '当前代码已改变。请运行 presmium check 查看差异。',
14
+ synced: '已锁定',
15
+ syncedTitle: '锁定的代码与当前代码一致',
16
+ openFile: (file: string) => `在仓库中打开 ${file}`,
17
+ }
18
+
19
+ export type OriginMessages = typeof zh
20
+
21
+ const en: OriginMessages = {
22
+ workingTree: 'Current Code',
23
+ status: { moved: 'Moved', fuzzy: 'Unverified', lost: 'Lost' },
24
+ statusTitle: {
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.',
28
+ },
29
+ drifted: 'Drifted',
30
+ driftedTitle: 'The current code changed; run presmium check to see the difference.',
31
+ synced: 'Pinned',
32
+ syncedTitle: 'The pinned code matches the current code',
33
+ openFile: file => `Open ${file} in the Repository`,
34
+ }
35
+
36
+ export const ORIGIN_MESSAGES: Record<Locale, OriginMessages> = { 'zh-CN': zh, 'en': en }
37
+
38
+ export interface OriginPart {
39
+ kind: 'file' | 'variant' | 'commit' | 'badge'
40
+ text: string
41
+ href?: string
42
+ title?: string
43
+ active?: boolean
44
+ status?: ResolutionStatus
45
+ drift?: 'synced' | 'drifted'
46
+ }
47
+
48
+ export interface OriginBar {
49
+ anchor?: string
50
+ parts: OriginPart[]
51
+ }
52
+
53
+ function lines(start: number, end = start): string {
54
+ return end > start ? `${start}-${end}` : `${start}`
55
+ }
56
+
57
+ export function originBar(node: CodeNode, shown: number | undefined, locale: Locale): OriginBar | undefined {
58
+ const t = ORIGIN_MESSAGES[locale]
59
+ const { origin } = node
60
+ if (!origin) return node.title ? { parts: [{ kind: 'file', text: node.title }] } : undefined
61
+ const variants = node.variants ?? []
62
+ const version = shown === undefined ? undefined : variants[shown]
63
+ const href = version ? version.permalink : origin.permalink
64
+ const [start, end] = version?.startLine !== undefined ? [version.startLine, version.endLine] : [origin.startLine, origin.endLine]
65
+ const parts: OriginPart[] = [{ kind: 'file', text: `${node.title ?? origin.file}:${lines(start, end)}`, href, title: href ? t.openFile(origin.file) : origin.anchor }]
66
+ if (variants.length) {
67
+ variants.forEach((variant, k) => parts.push({ kind: 'variant', text: variant.label, href: variant.permalink, active: k === shown }))
68
+ } else {
69
+ parts.push({ kind: 'commit', text: origin.pin ? `@ ${origin.pin.slice(0, 7)}` : t.workingTree, title: origin.pin })
70
+ }
71
+ const status = t.status[origin.status]
72
+ if (status) parts.push({ kind: 'badge', text: status, title: t.statusTitle[origin.status], status: origin.status })
73
+ else if (origin.pin && !variants.length && origin.drift) {
74
+ const drifted = origin.drift === 'drifted'
75
+ parts.push({ kind: 'badge', text: drifted ? t.drifted : t.synced, title: drifted ? t.driftedTitle : t.syncedTitle, drift: origin.drift })
76
+ }
77
+ return { anchor: origin.anchor, parts }
78
+ }
@@ -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
+ }
@@ -0,0 +1,46 @@
1
+ import type { IRBlock, IRDocument } from '@lincyaw/kernel'
2
+
3
+ export const FINAL_STEP = Number.POSITIVE_INFINITY
4
+
5
+ export interface Position {
6
+ section: string
7
+ step: number
8
+ boundary?: 'start' | 'end'
9
+ }
10
+
11
+ export function itemVisible(block: IRBlock, item: number, step: number): boolean {
12
+ return step >= block.at + item
13
+ }
14
+
15
+ export function stepAtLine(markers: readonly { step: number, top: number }[], line: number): number {
16
+ return markers.reduce((step, marker) => (marker.top <= line ? Math.max(step, marker.step) : step), 0)
17
+ }
18
+
19
+ export function clampPosition(doc: IRDocument, position: Position): Position {
20
+ const section = doc.sections.find(s => s.id === position.section) ?? doc.sections[0]
21
+ if (!section) return position
22
+ const step = Number.isFinite(position.step) ? Math.min(Math.max(Math.round(position.step), 0), section.steps) : section.steps
23
+ return { section: section.id, step, ...position.boundary ? { boundary: position.boundary } : {} }
24
+ }
25
+
26
+ export function keepPosition(prev: IRDocument, next: IRDocument, position: Position): Position {
27
+ if (next.sections.some(s => s.id === position.section) || next.sections.length === 0) return clampPosition(next, position)
28
+ const at = prev.sections.findIndex(s => s.id === position.section)
29
+ const section = next.sections[Math.min(Math.max(at, 0), next.sections.length - 1)]!
30
+ return clampPosition(next, { section: section.id, step: position.step })
31
+ }
32
+
33
+ export function nextPosition(doc: IRDocument, position: Position): Position | undefined {
34
+ const i = doc.sections.findIndex(s => s.id === position.section)
35
+ const section = doc.sections[i]
36
+ if (section && position.step < section.steps) return { section: section.id, step: position.step + 1 }
37
+ const next = doc.sections[i + 1]
38
+ return next ? { section: next.id, step: 0 } : undefined
39
+ }
40
+
41
+ export function prevPosition(doc: IRDocument, position: Position): Position | undefined {
42
+ const i = doc.sections.findIndex(s => s.id === position.section)
43
+ if (i >= 0 && position.step > 0) return { section: position.section, step: position.step - 1 }
44
+ const prev = doc.sections[i - 1]
45
+ return prev ? { section: prev.id, step: prev.steps } : undefined
46
+ }