@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.
- package/LICENSE +202 -0
- package/README.md +31 -2
- package/package.json +65 -3
- 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/client/code/highlight.ts +73 -0
- package/src/client/code/outputs.ts +84 -0
- package/src/client/content-marks.ts +15 -0
- package/src/client/element.ts +27 -0
- package/src/client/env.d.ts +3 -0
- package/src/client/index.ts +40 -0
- package/src/client/layout.ts +22 -0
- package/src/client/markdown/handlers.ts +161 -0
- 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/client/style.css +1058 -0
- package/src/client/viewer-messages.ts +7 -0
- package/src/client/viewer-plugins.ts +35 -0
- 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 +38 -0
- package/src/domain/code-width.ts +11 -0
- package/src/domain/document-content.ts +6 -0
- package/src/domain/footnotes.ts +15 -0
- package/src/domain/index.ts +22 -0
- package/src/domain/live.ts +44 -0
- package/src/domain/messages.ts +139 -0
- package/src/domain/origin.ts +78 -0
- package/src/domain/outputs.ts +27 -0
- 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
|
+
}
|