@lincyaw/kernel 1.0.0-rc.0 → 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/README.md +7 -7
- package/package.json +6 -2
- package/src/access.ts +18 -0
- package/src/anchors.ts +5 -1
- package/src/authoring.ts +56 -0
- package/src/blocks.ts +0 -8
- package/src/component-code.ts +6 -0
- package/src/contracts/agent.ts +5 -82
- package/src/contracts/anchors.ts +46 -0
- package/src/contracts/authoring-actions.ts +60 -0
- package/src/contracts/authoring-clients.ts +213 -0
- package/src/contracts/authoring.ts +180 -0
- package/src/contracts/checks.ts +29 -0
- package/src/contracts/classroom.ts +49 -0
- package/src/contracts/diagnostics.ts +37 -0
- package/src/contracts/document-audio.ts +150 -0
- package/src/contracts/document-comments.ts +31 -0
- package/src/contracts/document-history.ts +63 -0
- package/src/contracts/document-image.ts +32 -0
- package/src/contracts/document-reads.ts +57 -0
- package/src/contracts/document-voice.ts +44 -0
- package/src/contracts/document.ts +240 -0
- package/src/contracts/execution.ts +4 -5
- package/src/contracts/locale.ts +3 -0
- package/src/contracts/offline.ts +2 -0
- package/src/contracts/plugin.ts +183 -0
- package/src/contracts/presenter-panels.ts +17 -0
- package/src/contracts/preview.ts +305 -310
- package/src/contracts/production-assets.ts +61 -0
- package/src/contracts/production.ts +109 -0
- package/src/contracts/projects.ts +59 -0
- package/src/contracts/server.ts +1 -51
- package/src/contracts/speech-settings.ts +16 -0
- package/src/contracts/speech.ts +82 -70
- package/src/contracts/steps.ts +15 -0
- package/src/contracts/transcription-adoption.ts +79 -0
- package/src/contracts/transcription.ts +112 -0
- package/src/contracts/voice.ts +97 -0
- package/src/contracts/voiceover-execution.ts +68 -0
- package/src/contracts/workspace.ts +458 -0
- package/src/contracts.ts +28 -278
- package/src/diagnostics.ts +7 -0
- package/src/document-authoring-actions.ts +47 -0
- package/src/document-caption-timings.ts +64 -0
- package/src/document-colors.ts +25 -0
- package/src/document-comments.ts +118 -0
- package/src/document-components.ts +251 -0
- package/src/document-operations.ts +132 -0
- package/src/document-pages.ts +215 -0
- package/src/document-reads.ts +81 -0
- package/src/document-schema.ts +85 -0
- package/src/document-voices.ts +153 -0
- package/src/document.ts +252 -0
- package/src/durations.ts +7 -0
- package/src/errors.ts +8 -0
- package/src/execution.ts +5 -1
- package/src/fingerprint.ts +1 -1
- package/src/follow.ts +26 -30
- package/src/git.ts +1 -1
- package/src/index.ts +66 -13
- package/src/ir.ts +54 -5
- package/src/jupyter-kernel.ts +9 -0
- package/src/kernel.ts +38 -25
- package/src/keyboard.ts +43 -0
- package/src/locale.ts +13 -0
- package/src/lock.ts +4 -8
- package/src/logging.ts +1 -1
- package/src/{markdown.ts → lower-myst.ts} +2 -1
- package/src/node/assets.ts +23 -0
- package/src/node/authoring-actions.ts +50 -0
- package/src/node/authoring-clients.ts +14 -0
- package/src/node/document-audio.ts +22 -0
- package/src/node/document-history.ts +11 -0
- package/src/node/document-image.ts +8 -0
- package/src/node/document-reads.ts +10 -0
- package/src/node/document-voice.ts +7 -0
- package/src/node/ffmpeg.ts +29 -0
- package/src/node/file-version.ts +6 -0
- package/src/node/fonts.ts +22 -0
- package/src/node/index.ts +29 -133
- package/src/node/installed-packages.ts +11 -0
- package/src/node/lines.ts +24 -0
- package/src/node/locale.ts +7 -0
- package/src/node/network.ts +11 -0
- package/src/node/operator-speech.ts +91 -0
- package/src/node/packages.ts +74 -0
- package/src/node/paths.ts +7 -0
- package/src/node/projects.ts +7 -0
- package/src/node/resources.ts +86 -0
- package/src/node/revision.ts +6 -0
- package/src/node/run-directory.ts +70 -0
- package/src/node/transcription-adoption.ts +9 -0
- package/src/node/transcription.ts +10 -0
- package/src/node/voiceover-routes.ts +73 -0
- package/src/node/voiceover.ts +199 -0
- package/src/page-source.ts +359 -0
- package/src/parameters.ts +81 -0
- package/src/preview-edit.ts +18 -0
- package/src/preview-events.ts +57 -0
- package/src/preview-frame.ts +1220 -0
- package/src/props.ts +17 -0
- package/src/push-stream.ts +67 -0
- package/src/reading.ts +12 -7
- package/src/recording.ts +38 -0
- package/src/renderer-components.ts +30 -0
- package/src/responses.ts +49 -0
- package/src/run-outputs.ts +15 -0
- package/src/runner.py +3 -3
- package/src/services.ts +6 -6
- package/src/slides.ts +36 -0
- package/src/speech-words.ts +76 -0
- package/src/speech.ts +13 -0
- package/src/step-keys.ts +38 -0
- package/src/steps.ts +7 -0
- package/src/transcription-adoption.ts +105 -0
- package/src/uri-decoding.ts +8 -0
- package/src/urls.ts +13 -0
- package/src/views.ts +47 -0
- package/src/voice.ts +86 -0
package/src/contracts/preview.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRecord } from '../errors.ts'
|
|
2
|
+
import type { ProductionBinding, ProductionPlaybackCommand, ProductionPlaybackState } from './production.ts'
|
|
2
3
|
|
|
3
4
|
export const PREVIEW_PROTOCOL = 1
|
|
4
5
|
|
|
@@ -6,12 +7,60 @@ export const PREVIEW_NAME = 'presmium-preview'
|
|
|
6
7
|
|
|
7
8
|
export const PREVIEW_PARAM = 'presmium-preview'
|
|
8
9
|
|
|
10
|
+
/** The workbench's appearance when it loads a preview (`dark` or `light`), so that the page's first paint matches it. */
|
|
11
|
+
export const PREVIEW_APPEARANCE_PARAM = 'presmium-appearance'
|
|
12
|
+
|
|
9
13
|
export const PREVIEW_REVISION = 'presmiumRevision'
|
|
10
14
|
|
|
11
15
|
export const AUTHOR_MARKS_CLASS = 'pm-author-marks'
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
/** Read-only, version-bound control of an individual document widget. */
|
|
18
|
+
export interface BlockPreviewCommand {
|
|
19
|
+
type: 'presmium:block-preview-command'
|
|
20
|
+
document: string
|
|
21
|
+
blockId: string
|
|
22
|
+
revision: string
|
|
23
|
+
sectionId: string
|
|
24
|
+
stepId: string
|
|
25
|
+
action: 'seek' | 'pause' | 'sync'
|
|
26
|
+
/** The parent's current navigation intent, including repeat visits to the same step. */
|
|
27
|
+
navigation?: string
|
|
28
|
+
/** Omitted and document initial/reload seeks show the completed step; start is an explicit playback boundary. */
|
|
29
|
+
at?: 'start' | 'end'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface BlockPreviewPosition {
|
|
33
|
+
type: 'presmium:block-preview-position'
|
|
34
|
+
document: string
|
|
35
|
+
blockId: string
|
|
36
|
+
revision: string
|
|
37
|
+
sectionId: string
|
|
38
|
+
stepId: string
|
|
39
|
+
navigation: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BlockPreviewPlaybackIdentity {
|
|
43
|
+
document: string
|
|
44
|
+
blockId: string
|
|
45
|
+
revision: string
|
|
46
|
+
request: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BlockPreviewPlaybackRequest extends BlockPreviewPlaybackIdentity {
|
|
50
|
+
type: 'presmium:block-preview-playback-request'
|
|
51
|
+
sectionId: string
|
|
52
|
+
stepId: string
|
|
53
|
+
navigation: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BlockPreviewPlaybackGranted extends BlockPreviewPlaybackIdentity {
|
|
57
|
+
type: 'presmium:block-preview-playback-granted'
|
|
58
|
+
error?: string
|
|
59
|
+
navigation: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BlockPreviewPlaybackCancelled extends BlockPreviewPlaybackIdentity {
|
|
63
|
+
type: 'presmium:block-preview-playback-cancelled'
|
|
15
64
|
}
|
|
16
65
|
|
|
17
66
|
export interface PreviewAnchor {
|
|
@@ -19,6 +68,20 @@ export interface PreviewAnchor {
|
|
|
19
68
|
block?: string
|
|
20
69
|
}
|
|
21
70
|
|
|
71
|
+
/** A box in the preview page's viewport, in CSS pixels. */
|
|
72
|
+
export interface PreviewRect {
|
|
73
|
+
x: number
|
|
74
|
+
y: number
|
|
75
|
+
width: number
|
|
76
|
+
height: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A point in the preview page's viewport, in CSS pixels. */
|
|
80
|
+
export interface PreviewPoint {
|
|
81
|
+
x: number
|
|
82
|
+
y: number
|
|
83
|
+
}
|
|
84
|
+
|
|
22
85
|
export interface PreviewPin {
|
|
23
86
|
id: string
|
|
24
87
|
section: string
|
|
@@ -33,359 +96,291 @@ export interface PreviewPosition {
|
|
|
33
96
|
view?: string
|
|
34
97
|
}
|
|
35
98
|
|
|
99
|
+
/** Source-oriented edits from the block editor. Opaque blocks retain their source verbatim. */
|
|
100
|
+
export interface PreviewEditedBlock {
|
|
101
|
+
id: string
|
|
102
|
+
source?: string
|
|
103
|
+
at?: number
|
|
104
|
+
markdown?: string
|
|
105
|
+
scene?: string
|
|
106
|
+
durations?: number[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The source of one anchored block, read by the workbench from its current IR binding. */
|
|
110
|
+
export interface PreviewSource {
|
|
111
|
+
part: 'definition' | 'block'
|
|
112
|
+
path: string
|
|
113
|
+
language: string
|
|
114
|
+
content: string
|
|
115
|
+
version: string
|
|
116
|
+
shared: number
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Fixed live-viewer channels; no arbitrary upstream URL crosses the preview boundary. */
|
|
120
|
+
export const PREVIEW_PRESENTATION_PARAM = 'presmium-presentation'
|
|
121
|
+
/** `playback`: the workbench plays this page, so Space and the left and right arrows pressed in it go to the workbench. */
|
|
122
|
+
export const PREVIEW_KEYS_PARAM = 'presmium-keys'
|
|
123
|
+
export const PREVIEW_EVENTS_PARAM = 'presmium-events'
|
|
124
|
+
export type PreviewEventTarget =
|
|
125
|
+
| { topic: 'viewer-site', document: string, app: string, lang: string, form: 'web' | 'slides' }
|
|
126
|
+
| { topic: 'viewer-control', document: string, window: string }
|
|
127
|
+
export type PreviewEventMessage = { type: 'open' | 'error' } | { type: 'event', event: string, data: string }
|
|
128
|
+
export type PreviewEventSubscription = (target: PreviewEventTarget, received: (message: PreviewEventMessage) => void) => () => void
|
|
129
|
+
|
|
130
|
+
export function isPreviewEventTarget(value: unknown): value is PreviewEventTarget {
|
|
131
|
+
if (!isRecord(value) || !(typeof value.document === 'string' && value.document.length <= 1024)) return false
|
|
132
|
+
if (value.topic === 'viewer-site') return Object.keys(value).every(key => ['topic', 'document', 'app', 'lang', 'form'].includes(key))
|
|
133
|
+
&& bounded(value.app, 200) && (typeof value.lang === 'string' && value.lang.length <= 80) && (value.form === 'web' || value.form === 'slides')
|
|
134
|
+
return value.topic === 'viewer-control' && Object.keys(value).every(key => ['topic', 'document', 'window'].includes(key))
|
|
135
|
+
&& typeof value.window === 'string' && /^[a-zA-Z0-9-]{1,80}$/.test(value.window)
|
|
136
|
+
}
|
|
137
|
+
const eventMessage = (value: unknown): value is PreviewEventMessage => isRecord(value) && (value.type === 'open' || value.type === 'error'
|
|
138
|
+
|| value.type === 'event' && bounded(value.event, 40) && (typeof value.data === 'string' && value.data.length <= (2 << 20)))
|
|
139
|
+
|
|
36
140
|
export type PreviewCommand =
|
|
141
|
+
| { type: 'presentation-confirmed', revision: string }
|
|
142
|
+
| { type: 'events-ready', connection: string }
|
|
143
|
+
| { type: 'events-message', connection: string, id: string, message: PreviewEventMessage }
|
|
144
|
+
| { type: 'production-playback', request: number, control: ProductionPlaybackCommand }
|
|
145
|
+
| { type: 'document-history', action: 'undo' | 'redo', request: number }
|
|
146
|
+
| { type: 'document-history-reset' }
|
|
147
|
+
| { type: 'pause-playback', request: number }
|
|
148
|
+
| { type: 'playback-granted', request: number }
|
|
149
|
+
| { type: 'flush', request: number }
|
|
150
|
+
| { type: 'edit-result', target: PreviewAnchor, request: number, revision?: string, binding?: string, error?: string }
|
|
151
|
+
| { type: 'blocks-result', section: string, request: number, revision?: string, error?: string }
|
|
152
|
+
| { type: 'source-result', section: string, block: string, request: number, source?: PreviewSource, error?: string }
|
|
153
|
+
| { type: 'source-saved', section: string, block: string, request: number, sourceVersion?: string, error?: string }
|
|
154
|
+
| { type: 'source-created', section: string, request: number, block?: string, error?: string }
|
|
155
|
+
| { type: 'page-insert', section: string, id: string, request: number }
|
|
156
|
+
| { type: 'page-created', section: string, request: number, page?: string, error?: string }
|
|
37
157
|
| { type: 'go', section: string, step?: number, trail?: string[], view?: string }
|
|
38
158
|
| { type: 'highlight', target: PreviewAnchor | null }
|
|
39
159
|
| { type: 'pick', on: boolean }
|
|
40
160
|
| { type: 'pins', targets: PreviewPin[] }
|
|
41
161
|
| { type: 'marks', on: boolean }
|
|
42
162
|
| { type: 'appearance', dark: boolean }
|
|
163
|
+
| { type: 'activity', active: boolean, request: number }
|
|
164
|
+
/** Edits the text of a block (a section's heading when there is no block) in place, the caret at `at`; null ends editing and puts the text back. */
|
|
165
|
+
| { type: 'edit', target: PreviewAnchor | null, at?: PreviewPoint, revision?: string, binding?: string }
|
|
166
|
+
/** Makes the blocks (or sections) blink a few times, the first brought into view: what the AI just changed. */
|
|
167
|
+
| { type: 'flash', targets: PreviewAnchor[] }
|
|
43
168
|
|
|
44
169
|
export type PreviewCommandType = PreviewCommand['type']
|
|
45
170
|
|
|
171
|
+
/** A workbench command the page's keyboard asks for while it has the focus. */
|
|
172
|
+
export type PreviewShortcut = 'undo' | 'redo' | 'dismiss' | 'save' | 'play-pause' | 'previous' | 'next'
|
|
173
|
+
const PREVIEW_SHORTCUTS: readonly string[] = ['undo', 'redo', 'dismiss', 'save', 'play-pause', 'previous', 'next'] satisfies readonly PreviewShortcut[]
|
|
174
|
+
|
|
46
175
|
export type PreviewReport =
|
|
47
|
-
| { type: '
|
|
176
|
+
| { type: 'events-subscribe', connection: string, id: string, target: PreviewEventTarget }
|
|
177
|
+
| { type: 'events-unsubscribe', connection: string, id: string }
|
|
178
|
+
| { type: 'presentation-action', action: 'refresh' | 'exit' }
|
|
179
|
+
| { type: 'production-playback-state', state: ProductionPlaybackState }
|
|
180
|
+
| { type: 'production-playback-result', request: number, binding: ProductionBinding, state?: ProductionPlaybackState, error?: string }
|
|
181
|
+
| { type: 'document-history-state', section?: string, undo: boolean, redo: boolean }
|
|
182
|
+
| { type: 'document-history-result', request: number, handled: boolean, error?: string }
|
|
183
|
+
| { type: 'playback-paused', request: number, error?: string }
|
|
184
|
+
| { type: 'playback-request', request: number }
|
|
185
|
+
| { type: 'flushed', request: number, error?: string }
|
|
186
|
+
| { type: 'blocks-edited', section: string, request: number, revision: string, blocks: PreviewEditedBlock[] }
|
|
187
|
+
| { type: 'source-read', section: string, block: string, request: number, part?: 'definition' | 'block' }
|
|
188
|
+
| { type: 'source-save', section: string, block: string, request: number, path: string, base: string, content: string, part?: 'definition' | 'block' }
|
|
189
|
+
| { type: 'source-create', section: string, id: string, after?: string, request: number, revision: string }
|
|
190
|
+
| { type: 'source-draft', dirty: boolean }
|
|
191
|
+
/** Aggregate unsaved body blocks, independent of generated-source and narration drafts. */
|
|
192
|
+
| { type: 'body-draft', dirty: boolean }
|
|
193
|
+
/** Aggregate unacknowledged native title and complex-text drafts. */
|
|
194
|
+
| { type: 'native-draft', dirty: boolean }
|
|
195
|
+
| { type: 'page-create', section: string, id: string, request: number, revision: string }
|
|
196
|
+
| { type: 'page-create-error', request: number, error: string }
|
|
197
|
+
| { type: 'narrate', section: string, step: number, navigate?: boolean }
|
|
198
|
+
| { type: 'ready', renderer: 'web' | 'slides', document: string, revision?: string, capabilities: PreviewCommandType[], eventConnection?: string }
|
|
48
199
|
| ({ type: 'position' } & PreviewPosition)
|
|
49
|
-
| { type: 'picked', section: string, block?: string, step?: number, trail?: string[] }
|
|
50
|
-
| { type: 'selection', selection: (PreviewAnchor & { text: string }) | null }
|
|
200
|
+
| { type: 'picked', section: string, block?: string, step?: number, trail?: string[], rect?: PreviewRect, point?: PreviewPoint, clicks?: number }
|
|
201
|
+
| { type: 'selection', selection: (PreviewAnchor & { text: string, rect?: PreviewRect, obstacles?: PreviewRect[] }) | null }
|
|
202
|
+
/** Where the picked block or the selection is after the page scrolled or resized; null when it is out of sight. */
|
|
203
|
+
| { type: 'rect', rect: PreviewRect | null, obstacles?: PreviewRect[] }
|
|
204
|
+
/** Where the blocks the AI changed are in the preview's viewport while they blink, and again as the page scrolls (at most 16). */
|
|
205
|
+
| { type: 'flashed', rects: PreviewRect[] }
|
|
206
|
+
| { type: 'editing', target: PreviewAnchor, active: boolean }
|
|
207
|
+
/** The text of a block edited in place, before and after (its `textContent`); sent only when it changed. */
|
|
208
|
+
| { type: 'edited', target: PreviewAnchor, before: string, after: string, request?: number, revision?: string, binding?: string }
|
|
51
209
|
| { type: 'updated', revision?: string }
|
|
52
|
-
| { type: '
|
|
210
|
+
| { type: 'activity', active: boolean, request: number, revision?: string, error?: string }
|
|
211
|
+
| { type: 'report', level: 'error' | 'warning', plugin?: string, message: string, resolved?: boolean }
|
|
212
|
+
| { type: 'shortcut', command: PreviewShortcut }
|
|
53
213
|
|
|
54
214
|
export type PreviewEnvelope<M> = M & { protocol: typeof PREVIEW_NAME, version: number, nonce: string }
|
|
55
215
|
|
|
56
216
|
export function previewEnvelope<M extends PreviewCommand | PreviewReport>(nonce: string, message: M): PreviewEnvelope<M> {
|
|
57
|
-
return { protocol: PREVIEW_NAME, version: PREVIEW_PROTOCOL, nonce
|
|
217
|
+
return { ...message, protocol: PREVIEW_NAME, version: PREVIEW_PROTOCOL, nonce }
|
|
58
218
|
}
|
|
59
219
|
|
|
60
|
-
const record = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
61
220
|
const text = (value: unknown): value is string => typeof value === 'string'
|
|
62
221
|
const optional = <T>(value: unknown, is: (v: unknown) => v is T) => value === undefined || is(value)
|
|
222
|
+
const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
|
|
223
|
+
const positive = (value: unknown): value is number => finite(value) && value > 0
|
|
224
|
+
const nonnegative = (value: unknown): value is number => finite(value) && value >= 0
|
|
225
|
+
const productionId = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= 200
|
|
226
|
+
const productionBinding = (value: unknown): value is ProductionBinding => isRecord(value) && isRecord(value.source)
|
|
227
|
+
&& typeof value.source.documentId === 'string' && value.source.documentId.length > 0 && value.source.documentId.length <= 200
|
|
228
|
+
&& typeof value.source.version === 'string' && /^[\da-f]{20}$/.test(value.source.version)
|
|
229
|
+
&& typeof value.source.fingerprint === 'string' && /^[\da-f]{16}$/.test(value.source.fingerprint)
|
|
230
|
+
&& typeof value.irRevision === 'string' && value.irRevision.length > 0 && value.irRevision.length <= 200
|
|
231
|
+
&& typeof value.planId === 'string' && value.planId.length > 0 && value.planId.length <= 200
|
|
232
|
+
const productionState = (value: unknown): value is ProductionPlaybackState => {
|
|
233
|
+
if (!isRecord(value) || !productionBinding(value.binding) || !['loading', 'ready', 'estimated', 'unavailable', 'failed'].includes(String(value.status)) || !optional(value.error, text)) return false
|
|
234
|
+
if (value.position === undefined) return true
|
|
235
|
+
const position = value.position
|
|
236
|
+
return isRecord(position) && nonnegative(position.timeMs) && nonnegative(position.durationMs) && position.timeMs <= position.durationMs
|
|
237
|
+
&& optional(position.stepTimeMs, nonnegative) && optional(position.sectionId, text) && optional(position.stepId, text) && optional(position.waitingFor, text)
|
|
238
|
+
&& typeof position.playing === 'boolean' && positive(position.rate) && typeof position.trying === 'boolean'
|
|
239
|
+
}
|
|
240
|
+
const productionCommand = (value: unknown): value is ProductionPlaybackCommand => isRecord(value) && productionBinding(value.binding)
|
|
241
|
+
&& (value.action === 'load' || value.action === 'pause' || value.action === 'play' || value.action === 'seek' && nonnegative(value.timeMs)
|
|
242
|
+
|| value.action === 'step' && productionId(value.sectionId) && productionId(value.stepId) && optional(value.utteranceId, productionId) || value.action === 'rate' && positive(value.rate))
|
|
63
243
|
const step = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 0
|
|
244
|
+
const request = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0
|
|
64
245
|
const strings = (value: unknown): value is string[] => Array.isArray(value) && value.length <= 10_000 && value.every(text)
|
|
65
|
-
const anchor = (value: unknown): value is PreviewAnchor =>
|
|
246
|
+
const anchor = (value: unknown): value is PreviewAnchor => isRecord(value) && text(value.section) && optional(value.block, text)
|
|
247
|
+
const length = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
|
|
248
|
+
const rect = (value: unknown): value is PreviewRect => isRecord(value) && length(value.x) && length(value.y) && length(value.width) && length(value.height) && value.width >= 0 && value.height >= 0
|
|
249
|
+
const obstacles = (value: unknown): value is PreviewRect[] => Array.isArray(value) && value.length <= 16 && value.every(rect)
|
|
250
|
+
const point = (value: unknown): value is PreviewPoint => isRecord(value) && length(value.x) && length(value.y)
|
|
251
|
+
const clicks = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 1 && (value as number) <= 3
|
|
252
|
+
const toAnchor = (value: PreviewAnchor): PreviewAnchor => ({ section: value.section, ...value.block ? { block: value.block } : {} })
|
|
253
|
+
const toRect = (value: PreviewRect): PreviewRect => ({ x: value.x, y: value.y, width: value.width, height: value.height })
|
|
254
|
+
const toPoint = (value: PreviewPoint): PreviewPoint => ({ x: value.x, y: value.y })
|
|
255
|
+
const MAX_EDITED = 200_000
|
|
256
|
+
const bounded = (value: unknown, max: number): value is string => text(value) && value.length > 0 && value.length <= max
|
|
257
|
+
const editBaseline = (m: Record<string, unknown>) => m.revision === undefined && m.binding === undefined || bounded(m.revision, 200) && bounded(m.binding, 200)
|
|
258
|
+
const editedRequest = (m: Record<string, unknown>) => m.request === undefined && m.revision === undefined && m.binding === undefined || request(m.request) && bounded(m.revision, 200) && bounded(m.binding, 200)
|
|
259
|
+
const sourceTarget = (m: Record<string, unknown>) => bounded(m.section, 200) && bounded(m.block, 200) && request(m.request)
|
|
260
|
+
const sourcePart = (value: unknown): value is 'definition' | 'block' => value === 'definition' || value === 'block'
|
|
261
|
+
const pageId = (value: unknown): value is string => text(value) && /^page-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)
|
|
262
|
+
const sourceValue = (value: unknown): value is PreviewSource => isRecord(value) && sourcePart(value.part) && bounded(value.path, 4096) && bounded(value.language, 100) && text(value.content) && value.content.length <= MAX_EDITED && bounded(value.version, 200) && request(value.shared) && value.shared > 0
|
|
263
|
+
|
|
264
|
+
export function toBlockPreviewCommand(value: unknown): BlockPreviewCommand | undefined {
|
|
265
|
+
if (!isRecord(value) || value.type !== 'presmium:block-preview-command' || !text(value.document)
|
|
266
|
+
|| !text(value.blockId) || !value.blockId || !text(value.revision) || !value.revision
|
|
267
|
+
|| !text(value.sectionId) || !value.sectionId || !text(value.stepId) || !value.stepId
|
|
268
|
+
|| value.action !== 'seek' && value.action !== 'pause' && value.action !== 'sync'
|
|
269
|
+
|| (value.action === 'pause' ? !optional(value.navigation, value => bounded(value, 200)) : !bounded(value.navigation, 200))
|
|
270
|
+
|| value.at !== undefined && value.at !== 'start' && value.at !== 'end') return undefined
|
|
271
|
+
return { type: value.type, document: value.document, blockId: value.blockId, revision: value.revision, sectionId: value.sectionId, stepId: value.stepId, action: value.action, ...value.navigation !== undefined ? { navigation: value.navigation as string } : {}, ...value.at !== undefined ? { at: value.at } : {} }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function toBlockPreviewPosition(value: unknown): BlockPreviewPosition | undefined {
|
|
275
|
+
if (!isRecord(value) || value.type !== 'presmium:block-preview-position' || !text(value.document)
|
|
276
|
+
|| !bounded(value.blockId, 4096) || !bounded(value.revision, 200) || !bounded(value.sectionId, 4096)
|
|
277
|
+
|| !bounded(value.stepId, 4096) || !bounded(value.navigation, 200)) return undefined
|
|
278
|
+
return { type: value.type, document: value.document, blockId: value.blockId, revision: value.revision, sectionId: value.sectionId, stepId: value.stepId, navigation: value.navigation }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const playbackIdentity = (value: unknown): value is Record<string, unknown> & BlockPreviewPlaybackIdentity => isRecord(value) && text(value.document) && bounded(value.blockId, 4096) && bounded(value.revision, 200) && request(value.request)
|
|
282
|
+
const toPlaybackIdentity = (value: BlockPreviewPlaybackIdentity): BlockPreviewPlaybackIdentity => ({ document: value.document, blockId: value.blockId, revision: value.revision, request: value.request })
|
|
283
|
+
|
|
284
|
+
export function toBlockPreviewPlaybackRequest(value: unknown): BlockPreviewPlaybackRequest | undefined {
|
|
285
|
+
return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-request' && bounded(value.sectionId, 4096) && bounded(value.stepId, 4096) && bounded(value.navigation, 200)
|
|
286
|
+
? { ...toPlaybackIdentity(value), type: value.type, sectionId: value.sectionId, stepId: value.stepId, navigation: value.navigation } : undefined
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function toBlockPreviewPlaybackGranted(value: unknown): BlockPreviewPlaybackGranted | undefined {
|
|
290
|
+
return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-granted' && optional(value.error, text) && bounded(value.navigation, 200)
|
|
291
|
+
? { ...toPlaybackIdentity(value), type: value.type, navigation: value.navigation, ...value.error !== undefined ? { error: value.error as string } : {} } : undefined
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function toBlockPreviewPlaybackCancelled(value: unknown): BlockPreviewPlaybackCancelled | undefined {
|
|
295
|
+
return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-cancelled' ? { ...toPlaybackIdentity(value), type: value.type } : undefined
|
|
296
|
+
}
|
|
66
297
|
|
|
67
298
|
export function isPreviewEnvelope(data: unknown, nonce: string): data is PreviewEnvelope<{ type: string }> {
|
|
68
|
-
return
|
|
299
|
+
return isRecord(data) && data.protocol === PREVIEW_NAME && data.nonce === nonce && typeof data.version === 'number' && text(data.type)
|
|
69
300
|
}
|
|
70
301
|
|
|
71
|
-
export function
|
|
302
|
+
export function toPreviewCommand(data: unknown, nonce: string): PreviewCommand | undefined {
|
|
72
303
|
if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
|
|
73
304
|
const m = data as Record<string, unknown>
|
|
74
305
|
switch (m.type) {
|
|
306
|
+
case 'presentation-confirmed': return productionId(m.revision) ? { type: 'presentation-confirmed', revision: m.revision } : undefined
|
|
307
|
+
case 'events-ready': return productionId(m.connection) ? { type: 'events-ready', connection: m.connection } : undefined
|
|
308
|
+
case 'events-message': return productionId(m.connection) && productionId(m.id) && eventMessage(m.message) ? { type: 'events-message', connection: m.connection, id: m.id, message: m.message } : undefined
|
|
309
|
+
case 'production-playback': return request(m.request) && productionCommand(m.control) ? { type: 'production-playback', request: m.request, control: m.control } : undefined
|
|
310
|
+
case 'document-history': return request(m.request) && (m.action === 'undo' || m.action === 'redo') ? { type: 'document-history', request: m.request, action: m.action } : undefined
|
|
311
|
+
case 'document-history-reset': return { type: 'document-history-reset' }
|
|
312
|
+
case 'pause-playback':
|
|
313
|
+
case 'playback-granted': return request(m.request) ? { type: m.type, request: m.request } : undefined
|
|
314
|
+
case 'flush': return request(m.request) ? { type: 'flush', request: m.request } : undefined
|
|
315
|
+
case 'edit-result': return anchor(m.target) && request(m.request) && editBaseline(m) && optional(m.error, text) ? { type: 'edit-result', target: toAnchor(m.target), request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string, binding: m.binding as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
316
|
+
case 'blocks-result': return text(m.section) && request(m.request) && optional(m.revision, text) && optional(m.error, text) ? { type: 'blocks-result', section: m.section, request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
317
|
+
case 'source-result': return sourceTarget(m) && optional(m.source, sourceValue) && optional(m.error, text) ? { type: 'source-result', section: m.section as string, block: m.block as string, request: m.request as number, ...m.source === undefined ? {} : { source: m.source as PreviewSource }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
318
|
+
case 'source-saved': return sourceTarget(m) && optional(m.sourceVersion, (value): value is string => bounded(value, 200)) && optional(m.error, text) ? { type: 'source-saved', section: m.section as string, block: m.block as string, request: m.request as number, ...m.sourceVersion === undefined ? {} : { sourceVersion: m.sourceVersion as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
319
|
+
case 'source-created': return bounded(m.section, 200) && request(m.request) && optional(m.block, (value): value is string => bounded(value, 200)) && optional(m.error, text) ? { type: 'source-created', section: m.section, request: m.request, ...m.block === undefined ? {} : { block: m.block as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
320
|
+
case 'page-insert': return bounded(m.section, 200) && pageId(m.id) && request(m.request) ? { type: 'page-insert', section: m.section, id: m.id, request: m.request } : undefined
|
|
321
|
+
case 'page-created': return bounded(m.section, 200) && request(m.request) && optional(m.page, pageId) && optional(m.error, text) ? { type: 'page-created', section: m.section, request: m.request, ...m.page === undefined ? {} : { page: m.page as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
75
322
|
case 'go': return text(m.section) && optional(m.step, step) && optional(m.trail, strings) && optional(m.view, text) ? { type: 'go', section: m.section, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.view ? { view: m.view as string } : {} } : undefined
|
|
76
323
|
case 'highlight': return m.target === null || anchor(m.target) ? { type: 'highlight', target: m.target === null ? null : { section: m.target.section, ...m.target.block ? { block: m.target.block } : {} } } : undefined
|
|
77
324
|
case 'pick':
|
|
78
325
|
case 'marks': return typeof m.on === 'boolean' ? { type: m.type, on: m.on } : undefined
|
|
79
326
|
case 'appearance': return typeof m.dark === 'boolean' ? { type: 'appearance', dark: m.dark } : undefined
|
|
80
|
-
case '
|
|
327
|
+
case 'activity': return typeof m.active === 'boolean' && request(m.request) ? { type: 'activity', active: m.active, request: m.request } : undefined
|
|
328
|
+
case 'edit': return (m.target === null || anchor(m.target)) && optional(m.at, point) && editBaseline(m) ? { type: 'edit', target: m.target === null ? null : toAnchor(m.target), ...m.at ? { at: toPoint(m.at as PreviewPoint) } : {}, ...m.revision === undefined ? {} : { revision: m.revision as string, binding: m.binding as string } } : undefined
|
|
329
|
+
case 'pins': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(t => isRecord(t) && text(t.id) && text(t.section) && text(t.block) && text(t.label))
|
|
81
330
|
? { type: 'pins', targets: (m.targets as PreviewPin[]).map(({ id, section, block, label }) => ({ id, section, block, label })) }
|
|
82
331
|
: undefined
|
|
332
|
+
case 'flash': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(anchor) ? { type: 'flash', targets: (m.targets as PreviewAnchor[]).map(toAnchor) } : undefined
|
|
83
333
|
default: return undefined
|
|
84
334
|
}
|
|
85
335
|
}
|
|
86
336
|
|
|
87
|
-
export function
|
|
337
|
+
export function toPreviewReport(data: unknown, nonce: string): PreviewReport | undefined {
|
|
88
338
|
if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
|
|
89
339
|
const m = data as Record<string, unknown>
|
|
90
340
|
switch (m.type) {
|
|
91
|
-
case '
|
|
341
|
+
case 'events-subscribe': return productionId(m.connection) && productionId(m.id) && isPreviewEventTarget(m.target) ? { type: 'events-subscribe', connection: m.connection, id: m.id, target: m.target } : undefined
|
|
342
|
+
case 'events-unsubscribe': return productionId(m.connection) && productionId(m.id) ? { type: 'events-unsubscribe', connection: m.connection, id: m.id } : undefined
|
|
343
|
+
case 'presentation-action': return m.action === 'refresh' || m.action === 'exit' ? { type: 'presentation-action', action: m.action } : undefined
|
|
344
|
+
case 'production-playback-state': return productionState(m.state) ? { type: 'production-playback-state', state: m.state } : undefined
|
|
345
|
+
case 'production-playback-result': return request(m.request) && productionBinding(m.binding) && optional(m.state, productionState) && optional(m.error, text)
|
|
346
|
+
? { type: 'production-playback-result', request: m.request, binding: m.binding, ...m.state ? { state: m.state as ProductionPlaybackState } : {}, ...m.error !== undefined ? { error: m.error as string } : {} } : undefined
|
|
347
|
+
case 'document-history-state': return optional(m.section, text) && typeof m.undo === 'boolean' && typeof m.redo === 'boolean' ? { type: 'document-history-state', ...m.section === undefined ? {} : { section: m.section as string }, undo: m.undo, redo: m.redo } : undefined
|
|
348
|
+
case 'document-history-result': return request(m.request) && typeof m.handled === 'boolean' && optional(m.error, text) ? { type: 'document-history-result', request: m.request, handled: m.handled, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
349
|
+
case 'playback-request': return request(m.request) ? { type: 'playback-request', request: m.request } : undefined
|
|
350
|
+
case 'playback-paused': return request(m.request) && optional(m.error, text) ? { type: 'playback-paused', request: m.request, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
351
|
+
case 'flushed': return request(m.request) && optional(m.error, text) ? { type: 'flushed', request: m.request, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
|
|
352
|
+
case 'blocks-edited': return text(m.section) && request(m.request) && text(m.revision) && Array.isArray(m.blocks) && m.blocks.length <= 2000 && m.blocks.every(b => isRecord(b) && text(b.id) && b.id.length <= 200 && optional(b.source, text) && optional(b.at, step) && optional(b.markdown, (v): v is string => text(v) && v.length <= MAX_EDITED) && optional(b.scene, text) && optional(b.durations, (v): v is number[] => Array.isArray(v) && v.length <= 1000 && v.every(n => typeof n === 'number' && Number.isFinite(n) && n >= 0))) ? { type: 'blocks-edited', section: m.section, request: m.request, revision: m.revision, blocks: m.blocks as unknown as PreviewEditedBlock[] } : undefined
|
|
353
|
+
case 'source-read': return sourceTarget(m) && optional(m.part, sourcePart) ? { type: 'source-read', section: m.section as string, block: m.block as string, request: m.request as number, ...m.part === undefined ? {} : { part: m.part as 'definition' | 'block' } } : undefined
|
|
354
|
+
case 'source-save': return sourceTarget(m) && optional(m.part, sourcePart) && bounded(m.path, 4096) && bounded(m.base, 200) && text(m.content) && m.content.length <= MAX_EDITED ? { type: 'source-save', section: m.section as string, block: m.block as string, request: m.request as number, path: m.path as string, base: m.base as string, content: m.content, ...m.part === undefined ? {} : { part: m.part as 'definition' | 'block' } } : undefined
|
|
355
|
+
case 'source-create': return bounded(m.section, 200) && text(m.id) && /^scene-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(m.id) && request(m.request) && bounded(m.revision, 200) && optional(m.after, (value): value is string => bounded(value, 200)) ? { type: 'source-create', section: m.section, id: m.id, request: m.request, revision: m.revision, ...m.after === undefined ? {} : { after: m.after as string } } : undefined
|
|
356
|
+
case 'source-draft': return typeof m.dirty === 'boolean' ? { type: 'source-draft', dirty: m.dirty } : undefined
|
|
357
|
+
case 'body-draft': return typeof m.dirty === 'boolean' ? { type: 'body-draft', dirty: m.dirty } : undefined
|
|
358
|
+
case 'native-draft': return typeof m.dirty === 'boolean' ? { type: 'native-draft', dirty: m.dirty } : undefined
|
|
359
|
+
case 'page-create': return bounded(m.section, 200) && pageId(m.id) && request(m.request) && bounded(m.revision, 200) ? { type: 'page-create', section: m.section, id: m.id, request: m.request, revision: m.revision } : undefined
|
|
360
|
+
case 'page-create-error': return request(m.request) && bounded(m.error, 4000) ? { type: 'page-create-error', request: m.request, error: m.error } : undefined
|
|
361
|
+
case 'narrate': return text(m.section) && step(m.step) && optional(m.navigate, (value): value is boolean => typeof value === 'boolean') ? { type: 'narrate', section: m.section, step: m.step, ...m.navigate === undefined ? {} : { navigate: m.navigate as boolean } } : undefined
|
|
362
|
+
case 'ready': return (m.renderer === 'web' || m.renderer === 'slides') && text(m.document) && optional(m.revision, text) && strings(m.capabilities) && optional(m.eventConnection, productionId) ? { type: 'ready', renderer: m.renderer, document: m.document, ...m.revision ? { revision: m.revision as string } : {}, capabilities: m.capabilities as PreviewCommandType[], ...m.eventConnection ? { eventConnection: m.eventConnection as string } : {} } : undefined
|
|
92
363
|
case 'position': return text(m.section) && step(m.step) && strings(m.trail) && optional(m.view, text) ? { type: 'position', section: m.section, step: m.step, trail: m.trail, ...m.view ? { view: m.view as string } : {} } : undefined
|
|
93
|
-
case 'picked': return text(m.section) && optional(m.block, text) && optional(m.step, step) && optional(m.trail, strings)
|
|
94
|
-
|
|
364
|
+
case 'picked': return text(m.section) && optional(m.block, text) && optional(m.step, step) && optional(m.trail, strings) && optional(m.rect, rect) && optional(m.point, point) && optional(m.clicks, clicks)
|
|
365
|
+
? { type: 'picked', section: m.section, ...m.block ? { block: m.block as string } : {}, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.rect ? { rect: toRect(m.rect as PreviewRect) } : {}, ...m.point ? { point: toPoint(m.point as PreviewPoint) } : {}, ...m.clicks === undefined ? {} : { clicks: m.clicks as number } }
|
|
366
|
+
: undefined
|
|
367
|
+
case 'selection': {
|
|
368
|
+
if (m.selection === null) return { type: 'selection', selection: null }
|
|
369
|
+
const selected = m.selection
|
|
370
|
+
return isRecord(selected) && text(selected.text) && anchor(selected) && optional(selected.rect, rect) && optional(selected.obstacles, obstacles)
|
|
371
|
+
? { type: 'selection', selection: { ...toAnchor(selected), text: selected.text, ...selected.rect ? { rect: toRect(selected.rect as PreviewRect) } : {}, ...selected.obstacles !== undefined ? { obstacles: (selected.obstacles as PreviewRect[]).map(toRect) } : {} } }
|
|
372
|
+
: undefined
|
|
373
|
+
}
|
|
374
|
+
case 'flashed': return Array.isArray(m.rects) && m.rects.length <= 16 && m.rects.every(rect) ? { type: 'flashed', rects: (m.rects as PreviewRect[]).map(toRect) } : undefined
|
|
375
|
+
case 'rect': return (m.rect === null || rect(m.rect)) && optional(m.obstacles, obstacles) ? { type: 'rect', rect: m.rect === null ? null : toRect(m.rect), ...m.obstacles !== undefined ? { obstacles: (m.obstacles as PreviewRect[]).map(toRect) } : {} } : undefined
|
|
376
|
+
case 'editing': return anchor(m.target) && typeof m.active === 'boolean' ? { type: 'editing', target: toAnchor(m.target), active: m.active } : undefined
|
|
377
|
+
case 'edited': return anchor(m.target) && text(m.before) && text(m.after) && m.before.length <= MAX_EDITED && m.after.length <= MAX_EDITED && editedRequest(m) ? { type: 'edited', target: toAnchor(m.target), before: m.before, after: m.after, ...m.request === undefined ? {} : { request: m.request as number, revision: m.revision as string, binding: m.binding as string } } : undefined
|
|
95
378
|
case 'updated': return optional(m.revision, text) ? { type: 'updated', ...m.revision ? { revision: m.revision as string } : {} } : undefined
|
|
96
|
-
case '
|
|
379
|
+
case 'activity': return typeof m.active === 'boolean' && request(m.request) && optional(m.revision, text) && optional(m.error, text)
|
|
380
|
+
? { type: 'activity', active: m.active, request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string }, ...m.error === undefined ? {} : { error: m.error as string } }
|
|
381
|
+
: undefined
|
|
382
|
+
case 'report': return (m.level === 'error' || m.level === 'warning') && text(m.message) && optional(m.plugin, text) && (m.resolved === undefined || typeof m.resolved === 'boolean') ? { type: 'report', level: m.level, message: m.message, ...m.plugin ? { plugin: m.plugin as string } : {}, ...m.resolved === undefined ? {} : { resolved: m.resolved as boolean } } : undefined
|
|
383
|
+
case 'shortcut': return typeof m.command === 'string' && PREVIEW_SHORTCUTS.includes(m.command) ? { type: 'shortcut', command: m.command as PreviewShortcut } : undefined
|
|
97
384
|
default: return undefined
|
|
98
385
|
}
|
|
99
386
|
}
|
|
100
|
-
|
|
101
|
-
export interface PreviewPage {
|
|
102
|
-
renderer: 'web' | 'slides'
|
|
103
|
-
document: string
|
|
104
|
-
origins: readonly string[]
|
|
105
|
-
revision(): string | undefined
|
|
106
|
-
position(): PreviewPosition | undefined
|
|
107
|
-
go(command: Extract<PreviewCommand, { type: 'go' }>): void
|
|
108
|
-
reveal(target: PreviewAnchor): void
|
|
109
|
-
setDark(dark: boolean): void
|
|
110
|
-
sectionOf?(element: Element): string | undefined
|
|
111
|
-
sectionElement?(section: string): HTMLElement | undefined
|
|
112
|
-
seen?(element: Element): boolean
|
|
113
|
-
stepOf?(anchor: Required<PreviewAnchor>): { step: number, trail: string[] } | undefined
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export interface PreviewAgent {
|
|
117
|
-
moved(): void
|
|
118
|
-
updated(): void
|
|
119
|
-
redraw(): void
|
|
120
|
-
report(problem: { level: 'error' | 'warning', plugin?: string, message: string }): void
|
|
121
|
-
stop(): void
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const FRAME_NAME = `${PREVIEW_NAME}:`
|
|
125
|
-
const STYLE_ID = 'presmium-preview-style'
|
|
126
|
-
const HIGHLIGHT = 'data-preview-highlight'
|
|
127
|
-
const HOVER = 'data-preview-hover'
|
|
128
|
-
const POSITIONED = 'data-preview-positioned'
|
|
129
|
-
const PIN_CLASS = 'pm-preview-pin'
|
|
130
|
-
const CONTROLS = 'a, button, input, select, textarea, label, summary, [contenteditable], [role="dialog"]'
|
|
131
|
-
const COMMANDS: PreviewCommandType[] = ['go', 'highlight', 'pick', 'pins', 'marks', 'appearance']
|
|
132
|
-
|
|
133
|
-
const STYLE = `
|
|
134
|
-
[${HIGHLIGHT}] { outline: 2px solid var(--pm-color-accent, #1d6b7a) !important; outline-offset: 4px; border-radius: 2px; }
|
|
135
|
-
[${HOVER}] { outline: 2px dashed var(--pm-color-accent, #1d6b7a) !important; outline-offset: 2px; cursor: crosshair; }
|
|
136
|
-
[${POSITIONED}] { position: relative; }
|
|
137
|
-
.${PIN_CLASS} { position: absolute; top: -10px; left: -10px; z-index: 50; min-width: 20px; height: 20px; padding: 0 5px; box-sizing: border-box; border-radius: 10px; background: var(--pm-color-accent, #1d6b7a); color: #fff; font: 600 12px/20px system-ui, sans-serif; text-align: center; pointer-events: none; }
|
|
138
|
-
`
|
|
139
|
-
|
|
140
|
-
function previewNonce(): string | undefined {
|
|
141
|
-
if (typeof window === 'undefined') return undefined
|
|
142
|
-
const given = new URL(window.location.href).searchParams.get(PREVIEW_PARAM)
|
|
143
|
-
if (given) return given
|
|
144
|
-
return window.name.startsWith(FRAME_NAME) ? window.name.slice(FRAME_NAME.length) || undefined : undefined
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function startPreviewAgent(page: PreviewPage): PreviewAgent | undefined {
|
|
148
|
-
if (typeof window === 'undefined' || window.parent === window || page.origins.length === 0) return undefined
|
|
149
|
-
const nonce = previewNonce()
|
|
150
|
-
if (!nonce) return undefined
|
|
151
|
-
window.name = `${FRAME_NAME}${nonce}`
|
|
152
|
-
const root = document.documentElement
|
|
153
|
-
root.dataset.presmiumPreview = 'on'
|
|
154
|
-
if (!document.getElementById(STYLE_ID)) {
|
|
155
|
-
const style = document.createElement('style')
|
|
156
|
-
style.id = STYLE_ID
|
|
157
|
-
style.textContent = STYLE
|
|
158
|
-
document.head.append(style)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const post = (message: PreviewReport) => {
|
|
162
|
-
for (const origin of page.origins) window.parent.postMessage(previewEnvelope(nonce, message), origin)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
let highlight: PreviewAnchor | null = null
|
|
166
|
-
let pins: PreviewPin[] = []
|
|
167
|
-
let picking = false
|
|
168
|
-
let ready = false
|
|
169
|
-
const waiting: Extract<PreviewReport, { type: 'report' }>[] = []
|
|
170
|
-
let sent: string | undefined
|
|
171
|
-
let revision: string | undefined
|
|
172
|
-
|
|
173
|
-
const seen = (element: Element): boolean =>
|
|
174
|
-
!element.closest('[inert], .pm-hidden') && element.getClientRects().length > 0 && (page.seen?.(element) ?? true)
|
|
175
|
-
const seenBlock = (element: Element): HTMLElement | undefined => {
|
|
176
|
-
for (let block = element.closest<HTMLElement>('.pm-block[data-block]'); block; block = block.parentElement?.closest<HTMLElement>('.pm-block[data-block]') ?? null) {
|
|
177
|
-
if (seen(block)) return block
|
|
178
|
-
}
|
|
179
|
-
return undefined
|
|
180
|
-
}
|
|
181
|
-
const sectionOf = (element: Element): string | undefined =>
|
|
182
|
-
element.closest<HTMLElement>('[data-section]')?.dataset.section ?? page.sectionOf?.(element)
|
|
183
|
-
const blockElement = (section: string, block: string): HTMLElement | undefined => {
|
|
184
|
-
for (const element of document.querySelectorAll<HTMLElement>(`.pm-block[data-block="${CSS.escape(block)}"]`)) {
|
|
185
|
-
if (sectionOf(element) === section && seen(element)) return element
|
|
186
|
-
}
|
|
187
|
-
return undefined
|
|
188
|
-
}
|
|
189
|
-
const sectionElement = (section: string): HTMLElement | undefined => {
|
|
190
|
-
const own = page.sectionElement?.(section)
|
|
191
|
-
if (own) return own
|
|
192
|
-
const all = [...document.querySelectorAll<HTMLElement>(`[data-section="${CSS.escape(section)}"]`)]
|
|
193
|
-
return all.find(el => el.classList.contains('pm-section') && seen(el)) ?? all.find(seen)
|
|
194
|
-
}
|
|
195
|
-
const elementOf = (target: PreviewAnchor) => (target.block ? blockElement(target.section, target.block) : sectionElement(target.section))
|
|
196
|
-
const anchorOf = (element: Element): PreviewAnchor | undefined => {
|
|
197
|
-
const block = seenBlock(element)
|
|
198
|
-
const section = sectionOf(block ?? element)
|
|
199
|
-
if (!section) return undefined
|
|
200
|
-
return block ? { section, block: block.dataset.block! } : { section }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const drawHighlight = (scroll: boolean) => {
|
|
204
|
-
const element = highlight && elementOf(highlight)
|
|
205
|
-
for (const other of document.querySelectorAll(`[${HIGHLIGHT}]`)) if (other !== element) other.removeAttribute(HIGHLIGHT)
|
|
206
|
-
if (!element) return false
|
|
207
|
-
element.setAttribute(HIGHLIGHT, '')
|
|
208
|
-
if (scroll) {
|
|
209
|
-
const box = element.getBoundingClientRect()
|
|
210
|
-
if (box.bottom < 0 || box.top > window.innerHeight) element.scrollIntoView({ block: 'center' })
|
|
211
|
-
}
|
|
212
|
-
return true
|
|
213
|
-
}
|
|
214
|
-
const drawPins = () => {
|
|
215
|
-
const wanted = new Map<HTMLElement, PreviewPin[]>()
|
|
216
|
-
for (const pin of pins) {
|
|
217
|
-
const element = blockElement(pin.section, pin.block)
|
|
218
|
-
if (element) wanted.set(element, [...wanted.get(element) ?? [], pin])
|
|
219
|
-
}
|
|
220
|
-
for (const old of document.querySelectorAll<HTMLElement>(`.${PIN_CLASS}`)) {
|
|
221
|
-
const host = old.parentElement
|
|
222
|
-
if (!host || !wanted.get(host)?.some(pin => pin.id === old.dataset.pin && pin.label === old.textContent)) old.remove()
|
|
223
|
-
}
|
|
224
|
-
for (const host of document.querySelectorAll<HTMLElement>(`[${POSITIONED}]`)) {
|
|
225
|
-
if (!wanted.has(host)) host.removeAttribute(POSITIONED)
|
|
226
|
-
}
|
|
227
|
-
for (const [element, own] of wanted) {
|
|
228
|
-
if (getComputedStyle(element).position === 'static') element.setAttribute(POSITIONED, '')
|
|
229
|
-
own.forEach((pin, i) => {
|
|
230
|
-
if ([...element.children].some(child => child.classList.contains(PIN_CLASS) && (child as HTMLElement).dataset.pin === pin.id)) return
|
|
231
|
-
const mark = document.createElement('span')
|
|
232
|
-
mark.className = PIN_CLASS
|
|
233
|
-
mark.dataset.pin = pin.id
|
|
234
|
-
mark.textContent = pin.label
|
|
235
|
-
mark.setAttribute('aria-hidden', 'true')
|
|
236
|
-
if (i > 0) mark.style.left = `${-10 + 24 * i}px`
|
|
237
|
-
element.append(mark)
|
|
238
|
-
})
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
let frame = 0
|
|
242
|
-
const redraw = () => {
|
|
243
|
-
cancelAnimationFrame(frame)
|
|
244
|
-
frame = requestAnimationFrame(() => {
|
|
245
|
-
drawHighlight(false)
|
|
246
|
-
drawPins()
|
|
247
|
-
})
|
|
248
|
-
}
|
|
249
|
-
const showHighlight = (target: PreviewAnchor | null) => {
|
|
250
|
-
highlight = target
|
|
251
|
-
if (!target) return void drawHighlight(false)
|
|
252
|
-
if (drawHighlight(true)) return
|
|
253
|
-
page.reveal(target)
|
|
254
|
-
const until = Date.now() + 5000
|
|
255
|
-
const retry = () => {
|
|
256
|
-
if (highlight !== target || drawHighlight(true) || Date.now() > until) return
|
|
257
|
-
setTimeout(retry, 100)
|
|
258
|
-
}
|
|
259
|
-
setTimeout(retry, 50)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
let hovered: Element | undefined
|
|
263
|
-
const hover = (event: MouseEvent) => {
|
|
264
|
-
const target = event.target instanceof Element ? event.target : undefined
|
|
265
|
-
const block = target && !target.closest(CONTROLS) ? seenBlock(target) : undefined
|
|
266
|
-
if (block === hovered) return
|
|
267
|
-
hovered?.removeAttribute(HOVER)
|
|
268
|
-
hovered = block
|
|
269
|
-
hovered?.setAttribute(HOVER, '')
|
|
270
|
-
}
|
|
271
|
-
const click = (event: MouseEvent) => {
|
|
272
|
-
const target = event.target instanceof Element ? event.target : undefined
|
|
273
|
-
if (!target || event.button !== 0 || target.closest(CONTROLS)) return
|
|
274
|
-
if (document.getSelection()?.isCollapsed === false) return
|
|
275
|
-
const anchor = anchorOf(target)
|
|
276
|
-
if (!anchor) return
|
|
277
|
-
event.preventDefault()
|
|
278
|
-
event.stopImmediatePropagation()
|
|
279
|
-
const at = anchor.block ? page.stepOf?.({ section: anchor.section, block: anchor.block }) : undefined
|
|
280
|
-
post({ type: 'picked', ...anchor, ...at ? { step: at.step, trail: at.trail } : {} })
|
|
281
|
-
}
|
|
282
|
-
const setPicking = (on: boolean) => {
|
|
283
|
-
if (on === picking) return
|
|
284
|
-
picking = on
|
|
285
|
-
if (on) {
|
|
286
|
-
document.addEventListener('mouseover', hover, true)
|
|
287
|
-
document.addEventListener('click', click, true)
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
document.removeEventListener('mouseover', hover, true)
|
|
291
|
-
document.removeEventListener('click', click, true)
|
|
292
|
-
hovered?.removeAttribute(HOVER)
|
|
293
|
-
hovered = undefined
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let selected = false
|
|
297
|
-
let selecting: ReturnType<typeof setTimeout> | undefined
|
|
298
|
-
const selection = () => {
|
|
299
|
-
clearTimeout(selecting)
|
|
300
|
-
selecting = setTimeout(() => {
|
|
301
|
-
const range = document.getSelection()
|
|
302
|
-
const value = range && !range.isCollapsed ? range.toString().trim() : ''
|
|
303
|
-
const node = range?.rangeCount ? range.getRangeAt(0).commonAncestorContainer : undefined
|
|
304
|
-
const anchor = value && node ? anchorOf(node instanceof Element ? node : node.parentElement ?? document.body) : undefined
|
|
305
|
-
if (anchor) {
|
|
306
|
-
selected = true
|
|
307
|
-
post({ type: 'selection', selection: { ...anchor, text: value.slice(0, 4000) } })
|
|
308
|
-
} else if (selected) {
|
|
309
|
-
selected = false
|
|
310
|
-
post({ type: 'selection', selection: null })
|
|
311
|
-
}
|
|
312
|
-
}, 200)
|
|
313
|
-
}
|
|
314
|
-
document.addEventListener('selectionchange', selection)
|
|
315
|
-
|
|
316
|
-
const observer = new MutationObserver(() => {
|
|
317
|
-
if (highlight || pins.length > 0) redraw()
|
|
318
|
-
})
|
|
319
|
-
observer.observe(document.body, { childList: true, subtree: true })
|
|
320
|
-
|
|
321
|
-
const receive = (event: MessageEvent) => {
|
|
322
|
-
if (event.source !== window.parent || !page.origins.includes(event.origin)) return
|
|
323
|
-
const command = readPreviewCommand(event.data, nonce)
|
|
324
|
-
if (!command) return
|
|
325
|
-
try {
|
|
326
|
-
switch (command.type) {
|
|
327
|
-
case 'go': return page.go(command)
|
|
328
|
-
case 'highlight': return showHighlight(command.target)
|
|
329
|
-
case 'pick': return setPicking(command.on)
|
|
330
|
-
case 'pins':
|
|
331
|
-
pins = command.targets
|
|
332
|
-
return drawPins()
|
|
333
|
-
case 'marks': return void root.classList.toggle(AUTHOR_MARKS_CLASS, command.on)
|
|
334
|
-
case 'appearance': return page.setDark(command.dark)
|
|
335
|
-
}
|
|
336
|
-
} catch (error) {
|
|
337
|
-
console.error(`[presmium] the preview could not take ${command.type}: ${errorMessage(error)}`)
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
window.addEventListener('message', receive)
|
|
341
|
-
|
|
342
|
-
let timer: ReturnType<typeof setTimeout> | undefined
|
|
343
|
-
let last = 0
|
|
344
|
-
const sendPosition = () => {
|
|
345
|
-
timer = undefined
|
|
346
|
-
const position = page.position()
|
|
347
|
-
if (!position) return
|
|
348
|
-
if (!ready) {
|
|
349
|
-
ready = true
|
|
350
|
-
revision = page.revision()
|
|
351
|
-
post({ type: 'ready', renderer: page.renderer, document: page.document, ...revision ? { revision } : {}, capabilities: COMMANDS })
|
|
352
|
-
for (const problem of waiting.splice(0)) post(problem)
|
|
353
|
-
}
|
|
354
|
-
const key = JSON.stringify(position)
|
|
355
|
-
if (key === sent) return
|
|
356
|
-
sent = key
|
|
357
|
-
last = Date.now()
|
|
358
|
-
post({ type: 'position', ...position })
|
|
359
|
-
}
|
|
360
|
-
const moved = () => {
|
|
361
|
-
if (timer) return
|
|
362
|
-
timer = setTimeout(sendPosition, Math.max(0, 150 - (Date.now() - last)))
|
|
363
|
-
}
|
|
364
|
-
moved()
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
moved,
|
|
368
|
-
updated() {
|
|
369
|
-
const next = page.revision()
|
|
370
|
-
if (!ready || next === revision) return
|
|
371
|
-
revision = next
|
|
372
|
-
post({ type: 'updated', ...next ? { revision: next } : {} })
|
|
373
|
-
redraw()
|
|
374
|
-
},
|
|
375
|
-
redraw,
|
|
376
|
-
report(problem) {
|
|
377
|
-
const message = { type: 'report' as const, ...problem }
|
|
378
|
-
if (ready) post(message)
|
|
379
|
-
else waiting.push(message)
|
|
380
|
-
},
|
|
381
|
-
stop() {
|
|
382
|
-
window.removeEventListener('message', receive)
|
|
383
|
-
document.removeEventListener('selectionchange', selection)
|
|
384
|
-
observer.disconnect()
|
|
385
|
-
setPicking(false)
|
|
386
|
-
clearTimeout(timer)
|
|
387
|
-
clearTimeout(selecting)
|
|
388
|
-
delete root.dataset.presmiumPreview
|
|
389
|
-
},
|
|
390
|
-
}
|
|
391
|
-
}
|