@lincyaw/kernel 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 +32 -2
- package/package.json +41 -3
- package/src/access.ts +18 -0
- package/src/anchors.ts +58 -0
- package/src/authoring.ts +56 -0
- package/src/blocks.ts +19 -0
- package/src/checks.ts +35 -0
- package/src/component-code.ts +6 -0
- package/src/contracts/agent.ts +76 -0
- 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 +118 -0
- package/src/contracts/locale.ts +3 -0
- package/src/contracts/offline.ts +2 -0
- package/src/contracts/operations.ts +8 -0
- package/src/contracts/plugin.ts +183 -0
- package/src/contracts/presenter-panels.ts +17 -0
- package/src/contracts/preview.ts +386 -0
- 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 +108 -0
- package/src/contracts/service.ts +8 -0
- package/src/contracts/speech-settings.ts +16 -0
- package/src/contracts/speech.ts +149 -0
- 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 +35 -0
- 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 +15 -0
- package/src/events.ts +32 -0
- package/src/execution.ts +25 -0
- package/src/fingerprint.ts +31 -0
- package/src/follow.ts +33 -0
- package/src/git.ts +12 -0
- package/src/html.ts +41 -0
- package/src/index.ts +82 -0
- package/src/ir.ts +367 -0
- package/src/jupyter-kernel.ts +9 -0
- package/src/kernel.ts +190 -0
- package/src/keyboard.ts +43 -0
- package/src/locale.ts +13 -0
- package/src/lock.ts +81 -0
- package/src/logging.ts +11 -0
- package/src/lower-myst.ts +312 -0
- 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 -0
- 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/strip-types.mjs +15 -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/published.ts +11 -0
- package/src/push-stream.ts +67 -0
- package/src/reading.ts +96 -0
- package/src/recording.ts +38 -0
- package/src/renderer-components.ts +30 -0
- package/src/requests.ts +28 -0
- package/src/responses.ts +49 -0
- package/src/run-outputs.ts +15 -0
- package/src/runner.py +454 -0
- package/src/services.ts +146 -0
- 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/ir.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnchorLockFile,
|
|
3
|
+
AnchorResolverRegistry,
|
|
4
|
+
AnchorString,
|
|
5
|
+
ExecuteConfig,
|
|
6
|
+
Executor,
|
|
7
|
+
Finding,
|
|
8
|
+
ResolutionStatus,
|
|
9
|
+
RunResult,
|
|
10
|
+
ServiceToken,
|
|
11
|
+
SourceReader,
|
|
12
|
+
} from './contracts.ts'
|
|
13
|
+
|
|
14
|
+
export const IR_VERSION = 1
|
|
15
|
+
|
|
16
|
+
export type RendererId = string
|
|
17
|
+
|
|
18
|
+
export interface MdastNode {
|
|
19
|
+
type: string
|
|
20
|
+
children?: MdastNode[]
|
|
21
|
+
value?: string
|
|
22
|
+
[key: string]: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SourcePos {
|
|
26
|
+
file: string
|
|
27
|
+
line: number
|
|
28
|
+
endLine?: number
|
|
29
|
+
format?: 'json'
|
|
30
|
+
pointer?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Compiler-owned identity of the exact canonical authoring entry behind a runtime snapshot. */
|
|
34
|
+
export interface CanonicalSource {
|
|
35
|
+
documentId: string
|
|
36
|
+
version: string
|
|
37
|
+
fingerprint: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IRDocument {
|
|
41
|
+
ir: typeof IR_VERSION
|
|
42
|
+
/** Read-only provenance, never authored metadata or editable document content. */
|
|
43
|
+
source?: CanonicalSource
|
|
44
|
+
meta: {
|
|
45
|
+
title: string
|
|
46
|
+
entry: string
|
|
47
|
+
lang?: string
|
|
48
|
+
translationOf?: { entry: string, digest: string }
|
|
49
|
+
[key: string]: unknown
|
|
50
|
+
}
|
|
51
|
+
config: {
|
|
52
|
+
plugins: string[]
|
|
53
|
+
presmium: Record<string, unknown>
|
|
54
|
+
renderers: Record<RendererId, Record<string, unknown>>
|
|
55
|
+
execute?: ExecuteConfig
|
|
56
|
+
translations?: Record<string, string>
|
|
57
|
+
}
|
|
58
|
+
sections: IRSection[]
|
|
59
|
+
diagnostics: Finding[]
|
|
60
|
+
production?: import('./contracts/document.ts').Production
|
|
61
|
+
/** Canonical media declarations available to production renderers without reopening authoring JSON. */
|
|
62
|
+
productionAssets?: Record<string, import('./contracts/document.ts').Asset>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface IRSection {
|
|
66
|
+
id: string
|
|
67
|
+
index: number
|
|
68
|
+
title: string
|
|
69
|
+
depth: number
|
|
70
|
+
source: SourcePos
|
|
71
|
+
steps: number
|
|
72
|
+
stepMap?: number[]
|
|
73
|
+
stepIds?: string[]
|
|
74
|
+
/** Display-only canonical narration by stable step, without an audio or timing claim. */
|
|
75
|
+
stepNarrations?: Record<string, { id: string, text: string }[]>
|
|
76
|
+
/** Exact canonical per-step caption basis, independent of overall source version and preceding steps. */
|
|
77
|
+
captionSources?: Record<string, string>
|
|
78
|
+
hints: Record<RendererId, Record<string, unknown>>
|
|
79
|
+
config?: Record<string, Record<string, unknown>>
|
|
80
|
+
blocks: IRBlock[]
|
|
81
|
+
audience?: string[]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type BlockRole = 'visual' | 'narration'
|
|
85
|
+
|
|
86
|
+
export interface IRBlock {
|
|
87
|
+
id: string
|
|
88
|
+
role: BlockRole
|
|
89
|
+
at: number
|
|
90
|
+
until?: number
|
|
91
|
+
span?: number
|
|
92
|
+
only?: RendererId[]
|
|
93
|
+
audience?: string[]
|
|
94
|
+
source: SourcePos
|
|
95
|
+
node: IRNode
|
|
96
|
+
/** Complete resolved state at every section step, derived from the authoring graph. */
|
|
97
|
+
stepStates?: { visible: boolean, state?: string, parameters?: Record<string, unknown>, assets?: Record<string, string>, localStep: number }[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type IRNode = MarkdownNode | CodeNode | ComponentNode
|
|
101
|
+
|
|
102
|
+
export interface MarkdownNode {
|
|
103
|
+
type: 'markdown'
|
|
104
|
+
mdast: MdastNode
|
|
105
|
+
reveal?: 'items'
|
|
106
|
+
/** An explicitly authored step's speech starts with, or after, its animation. */
|
|
107
|
+
narration?: { mode: 'during' | 'after' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface CodeOrigin {
|
|
111
|
+
anchor: AnchorString
|
|
112
|
+
file: string
|
|
113
|
+
startLine: number
|
|
114
|
+
endLine: number
|
|
115
|
+
pin?: string
|
|
116
|
+
permalink?: string
|
|
117
|
+
status: ResolutionStatus
|
|
118
|
+
units?: Record<string, { startLine: number; endLine: number }>
|
|
119
|
+
drift?: 'synced' | 'drifted'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface RunSpec {
|
|
123
|
+
lang: string
|
|
124
|
+
args?: Record<string, string>
|
|
125
|
+
stdin?: string
|
|
126
|
+
timeoutMs?: number
|
|
127
|
+
executor?: string
|
|
128
|
+
when: 'compile' | 'live' | 'both'
|
|
129
|
+
session?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface RecordedRun extends RunResult {
|
|
133
|
+
cacheKey: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface CodeVariant {
|
|
137
|
+
label: string
|
|
138
|
+
value: string
|
|
139
|
+
permalink?: string
|
|
140
|
+
startLine?: number
|
|
141
|
+
endLine?: number
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface CodeNode {
|
|
145
|
+
type: 'code'
|
|
146
|
+
lang: string
|
|
147
|
+
value: string
|
|
148
|
+
title?: string
|
|
149
|
+
origin?: CodeOrigin
|
|
150
|
+
highlight?: number[][]
|
|
151
|
+
run?: RunSpec
|
|
152
|
+
outputs?: RecordedRun[]
|
|
153
|
+
variants?: CodeVariant[]
|
|
154
|
+
echo?: false
|
|
155
|
+
/** Structured source references for the resolver pass, independent of authoring syntax. */
|
|
156
|
+
reference?: { anchor: AnchorString, pin?: string, repo?: string, highlightRegions?: string[], variants?: { anchor?: AnchorString, pin?: string, repo?: string, label: string, value?: string }[] }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ComponentNode {
|
|
160
|
+
type: 'component'
|
|
161
|
+
plugin: string
|
|
162
|
+
name: string
|
|
163
|
+
props: Record<string, unknown>
|
|
164
|
+
children?: IRBlock[]
|
|
165
|
+
ownsCode?: boolean
|
|
166
|
+
fallback?: MdastNode
|
|
167
|
+
assets?: Record<string, string>
|
|
168
|
+
layer?: 'overlay'
|
|
169
|
+
durations?: number[]
|
|
170
|
+
/** The authored code that defines this component instance, when supplied by its plugin. */
|
|
171
|
+
definition?: { file: string, language: string, exportName?: string }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type DirectiveOptionType = 'string' | 'number' | 'boolean' | 'list'
|
|
175
|
+
|
|
176
|
+
export interface DirectiveInput {
|
|
177
|
+
name: string
|
|
178
|
+
arg?: string
|
|
179
|
+
options: Record<string, unknown>
|
|
180
|
+
body: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface DirectiveContext {
|
|
184
|
+
file: string
|
|
185
|
+
deckDir: string
|
|
186
|
+
workspaceRoot: string
|
|
187
|
+
lang?: string
|
|
188
|
+
config: Record<string, unknown>
|
|
189
|
+
source: SourcePos
|
|
190
|
+
parse(markdown: string): MdastNode
|
|
191
|
+
report(finding: Finding): void
|
|
192
|
+
watch(...files: string[]): void
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface DirectiveResult {
|
|
196
|
+
node: IRNode
|
|
197
|
+
role?: BlockRole
|
|
198
|
+
span?: number
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface RoleInput {
|
|
202
|
+
name: string
|
|
203
|
+
value: string
|
|
204
|
+
options: Record<string, unknown>
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface RoleSpec {
|
|
208
|
+
name: string
|
|
209
|
+
options?: Record<string, DirectiveOptionType>
|
|
210
|
+
children?(input: RoleInput, ctx: DirectiveContext): MdastNode[]
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface PresmiumRoleNode extends MdastNode {
|
|
214
|
+
type: 'presmiumRole'
|
|
215
|
+
plugin: string
|
|
216
|
+
name: string
|
|
217
|
+
options: Record<string, unknown>
|
|
218
|
+
children: MdastNode[]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface DirectiveSpec {
|
|
222
|
+
name: string
|
|
223
|
+
arg?: boolean | 'anchor'
|
|
224
|
+
body?: boolean
|
|
225
|
+
options?: Record<string, DirectiveOptionType>
|
|
226
|
+
toIR(input: DirectiveInput, ctx: DirectiveContext): DirectiveResult | Promise<DirectiveResult>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface RunCache {
|
|
230
|
+
get(key: string): RecordedRun | undefined
|
|
231
|
+
set(key: string, run: RecordedRun): void
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface PassContext {
|
|
235
|
+
deckDir: string
|
|
236
|
+
workspaceRoot: string
|
|
237
|
+
lang?: string
|
|
238
|
+
src: SourceReader
|
|
239
|
+
resolvers: AnchorResolverRegistry
|
|
240
|
+
lock: AnchorLockFile
|
|
241
|
+
executors: Executor[]
|
|
242
|
+
cache: RunCache
|
|
243
|
+
config: Record<string, unknown>
|
|
244
|
+
configFor(section: IRSection): Record<string, unknown>
|
|
245
|
+
report(finding: Finding): void
|
|
246
|
+
watch(...files: string[]): void
|
|
247
|
+
service?<T>(token: ServiceToken<T>): T | undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface IRPass {
|
|
251
|
+
name: string
|
|
252
|
+
order?: number
|
|
253
|
+
after?: 'execute'
|
|
254
|
+
run(doc: IRDocument, ctx: PassContext): void | Promise<void>
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export type Support = 'full' | 'degraded' | 'none'
|
|
258
|
+
|
|
259
|
+
export interface RenderContext {
|
|
260
|
+
deckDir: string
|
|
261
|
+
workspaceRoot: string
|
|
262
|
+
outDir: string
|
|
263
|
+
workDir: string
|
|
264
|
+
config: Record<string, unknown>
|
|
265
|
+
extensions: BoundRendererExtension[]
|
|
266
|
+
plugins: string[]
|
|
267
|
+
pluginDirs: Record<string, string>
|
|
268
|
+
lang?: string
|
|
269
|
+
renderers: RendererId[]
|
|
270
|
+
base: string
|
|
271
|
+
nodeVitePlugins?: NodeVitePlugin[]
|
|
272
|
+
extensionFor(node: ComponentNode): ((node: ComponentNode, block: IRBlock) => unknown) | undefined
|
|
273
|
+
portable(value: unknown, from: string): unknown
|
|
274
|
+
localAsset(source: SourcePos, url: string): LocalAsset | undefined
|
|
275
|
+
documentLink?(source: SourcePos, url: string): DocumentLink | undefined
|
|
276
|
+
report(finding: Finding): void
|
|
277
|
+
/** A line of the build's output (a tool's), for the job that runs it; renderers that start tools give theirs. */
|
|
278
|
+
log?(line: string): void
|
|
279
|
+
service?<T>(token: ServiceToken<T>): T | undefined
|
|
280
|
+
translations?: { lang: string, doc: IRDocument }[]
|
|
281
|
+
server?: { url: string }
|
|
282
|
+
preview?: { origins: string[] }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface NodeVitePlugin {
|
|
286
|
+
id: string
|
|
287
|
+
entry: string
|
|
288
|
+
config: Record<string, unknown>
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface LocalAsset {
|
|
292
|
+
file: string
|
|
293
|
+
name: string
|
|
294
|
+
directory?: boolean
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface DocumentLink {
|
|
298
|
+
dir: string
|
|
299
|
+
fragment?: string
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface RenderOutput {
|
|
303
|
+
outDir: string
|
|
304
|
+
skipped: { section: string; block: string; reason: string }[]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface RenderDevServer {
|
|
308
|
+
url: string
|
|
309
|
+
update(doc: IRDocument): Promise<void>
|
|
310
|
+
close(): Promise<void>
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface Renderer {
|
|
314
|
+
id: RendererId
|
|
315
|
+
site?: boolean
|
|
316
|
+
support(node: IRNode, extensions: BoundRendererExtension[]): Support
|
|
317
|
+
build(doc: IRDocument, ctx: RenderContext): Promise<RenderOutput>
|
|
318
|
+
dev?(doc: IRDocument, ctx: RenderContext): Promise<RenderDevServer>
|
|
319
|
+
buildSite?(site: SiteSpec, ctx: RenderContext): Promise<RenderOutput>
|
|
320
|
+
devSite?(site: SiteSpec, ctx: RenderContext, compile: (dir: string) => Promise<IRDocument>): Promise<SiteDevServer>
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export interface SiteDocument {
|
|
324
|
+
dir: string
|
|
325
|
+
title: string
|
|
326
|
+
description?: string
|
|
327
|
+
lang?: string
|
|
328
|
+
tags?: string[]
|
|
329
|
+
doc?: IRDocument
|
|
330
|
+
translations?: { lang: string, doc: IRDocument }[]
|
|
331
|
+
nodeVitePlugins?: NodeVitePlugin[]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export interface SiteSpec {
|
|
335
|
+
title: string
|
|
336
|
+
documents: SiteDocument[]
|
|
337
|
+
home?: string
|
|
338
|
+
config: Record<string, unknown>
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export interface SiteDevServer {
|
|
342
|
+
url: string
|
|
343
|
+
update(dir: string, doc: IRDocument): Promise<void>
|
|
344
|
+
/** Starts the dev server of document `dir` ahead of its first request (the author is about to open it); may decline. */
|
|
345
|
+
warm?(dir: string): Promise<void>
|
|
346
|
+
/** The origin of the dev server that serves `path` (started on first use), so that a host forwards to it directly. */
|
|
347
|
+
target?(path: string): Promise<string>
|
|
348
|
+
/**
|
|
349
|
+
* The address (below the site's URL) of the workbench's viewer app showing document `dir`, once the app (prebuilt for
|
|
350
|
+
* the workspace's plugins) and the document's own bundle are built: they are started when missing, and waited for a
|
|
351
|
+
* while with `wait`. Undefined until then, and for a document whose plugins the app cannot run (its dev server shows it).
|
|
352
|
+
* With `wait`, a build still running after the wait rejects with `PreviewPreparing`; a failed build with its reason.
|
|
353
|
+
*/
|
|
354
|
+
app?(dir: string, wait?: boolean): Promise<string | undefined>
|
|
355
|
+
close(): Promise<void>
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export type ComponentRenderer = (node: ComponentNode, block: IRBlock, ctx: RenderContext) => unknown
|
|
359
|
+
|
|
360
|
+
export interface RendererContribution {
|
|
361
|
+
components?: Record<string, ComponentRenderer>
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export interface BoundRendererExtension {
|
|
365
|
+
plugin: string
|
|
366
|
+
components?: Record<string, (node: ComponentNode, block: IRBlock) => unknown>
|
|
367
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { isRecord } from './errors.ts'
|
|
2
|
+
import { languageId } from './execution.ts'
|
|
3
|
+
|
|
4
|
+
/** The language of the notebook's Jupyter kernel, as `renderers.notebook.kernel` names it (a kernel name or `{ name, language }`); default Python. */
|
|
5
|
+
export function jupyterKernelLanguage(kernel: unknown): string {
|
|
6
|
+
if (typeof kernel === 'string' && kernel) return languageId(kernel.replace(/[-\d.]+$/, '') || kernel)
|
|
7
|
+
if (isRecord(kernel) && typeof kernel.language === 'string') return languageId(kernel.language)
|
|
8
|
+
return 'python'
|
|
9
|
+
}
|
package/src/kernel.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { KERNEL_VERSION } from './contracts.ts'
|
|
2
|
+
import type {
|
|
3
|
+
EnvironmentSpec,
|
|
4
|
+
EventBus,
|
|
5
|
+
KernelEvent,
|
|
6
|
+
Logger,
|
|
7
|
+
PluginContext,
|
|
8
|
+
PluginDefinition,
|
|
9
|
+
PluginReport,
|
|
10
|
+
PresentationMode,
|
|
11
|
+
ServiceToken,
|
|
12
|
+
StartReport,
|
|
13
|
+
} from './contracts.ts'
|
|
14
|
+
import { errorMessage, isRecord } from './errors.ts'
|
|
15
|
+
import { createEventBus } from './events.ts'
|
|
16
|
+
import { prefixedLogger } from './logging.ts'
|
|
17
|
+
import { ServiceRegistry } from './services.ts'
|
|
18
|
+
|
|
19
|
+
export interface KernelOptions {
|
|
20
|
+
mode: PresentationMode
|
|
21
|
+
config?: Record<string, unknown>
|
|
22
|
+
log?: Logger
|
|
23
|
+
events?: EventBus
|
|
24
|
+
environments?: Record<string, EnvironmentSpec>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Kernel {
|
|
28
|
+
readonly version = KERNEL_VERSION
|
|
29
|
+
readonly mode: PresentationMode
|
|
30
|
+
readonly services = new ServiceRegistry()
|
|
31
|
+
readonly events: EventBus
|
|
32
|
+
#log: Logger
|
|
33
|
+
#config: Record<string, unknown>
|
|
34
|
+
#environments?: Readonly<Record<string, EnvironmentSpec>>
|
|
35
|
+
#plugins: PluginDefinition[] = []
|
|
36
|
+
#state: 'idle' | 'starting' | 'started' | 'disposed' = 'idle'
|
|
37
|
+
#starting?: Promise<StartReport>
|
|
38
|
+
#disposing?: Promise<void>
|
|
39
|
+
#report?: StartReport
|
|
40
|
+
|
|
41
|
+
constructor(options: KernelOptions) {
|
|
42
|
+
this.mode = options.mode
|
|
43
|
+
this.#config = options.config ?? {}
|
|
44
|
+
this.#environments = options.environments && Object.freeze({ ...options.environments })
|
|
45
|
+
this.#log = options.log ?? console
|
|
46
|
+
this.events = options.events ?? createEventBus((error, event) => {
|
|
47
|
+
this.#log.error(`[presmium] handler for "${event.type}" threw`, error)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
register(plugin: PluginDefinition): void {
|
|
52
|
+
if (this.#state !== 'idle') throw new Error(`cannot register ${plugin.manifest.id}: kernel already started`)
|
|
53
|
+
if (this.#plugins.some(p => p.manifest.id === plugin.manifest.id)) {
|
|
54
|
+
throw new Error(`plugin ${plugin.manifest.id} is already registered`)
|
|
55
|
+
}
|
|
56
|
+
this.#plugins.push(plugin)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
provide<T>(token: ServiceToken<T>, implementation: T): void {
|
|
60
|
+
this.services.provide(token, implementation)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getService<T>(token: ServiceToken<T>): T | undefined {
|
|
64
|
+
return this.services.tryGet(token)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
report(): StartReport | undefined {
|
|
68
|
+
return this.#report
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
start(): Promise<StartReport> {
|
|
72
|
+
if (this.#state !== 'idle') return Promise.reject(new Error('kernel already started'))
|
|
73
|
+
this.#state = 'starting'
|
|
74
|
+
return this.#starting = Promise.resolve().then(() => this.#start())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async #start(): Promise<StartReport> {
|
|
78
|
+
const plan = this.services.plan(this.#plugins.map(p => p.manifest), this.version)
|
|
79
|
+
const reports = new Map<string, PluginReport>()
|
|
80
|
+
for (const [id, reason] of plan.excluded) reports.set(id, this.#disable(id, reason))
|
|
81
|
+
|
|
82
|
+
const order: string[] = []
|
|
83
|
+
for (const id of plan.order) {
|
|
84
|
+
const plugin = this.#plugins.find(p => p.manifest.id === id)!
|
|
85
|
+
const unavailable = (plugin.manifest.requires ?? []).filter(r => !this.services.has(r.token))
|
|
86
|
+
const missing = unavailable.map(r => r.token.id)
|
|
87
|
+
if (unavailable.some(r => !r.optional)) {
|
|
88
|
+
const required = unavailable.filter(r => !r.optional).map(r => r.token.id)
|
|
89
|
+
reports.set(id, this.#disable(id, `missing required services: ${required.join(', ')}`, missing))
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
order.push(id)
|
|
93
|
+
reports.set(id, await this.#setup(plugin, missing))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.#report = { order, plugins: this.#plugins.map(p => ({ ...reports.get(p.manifest.id)!, ...p.manifest.name ? { name: p.manifest.name } : {} })), cycles: plan.cycles }
|
|
97
|
+
if (this.#state !== 'disposed') this.#state = 'started'
|
|
98
|
+
return this.#report
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dispose(): Promise<void> {
|
|
102
|
+
if (this.#disposing) return this.#disposing
|
|
103
|
+
this.#state = 'disposed'
|
|
104
|
+
return this.#disposing = this.#dispose()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async #dispose(): Promise<void> {
|
|
108
|
+
await this.#starting?.catch(() => undefined)
|
|
109
|
+
const running = new Set(
|
|
110
|
+
this.#report?.plugins.filter(r => r.status === 'started' || r.status === 'degraded').map(r => r.id),
|
|
111
|
+
)
|
|
112
|
+
for (const id of [...(this.#report?.order ?? [])].reverse()) {
|
|
113
|
+
if (!running.has(id)) continue
|
|
114
|
+
const plugin = this.#plugins.find(p => p.manifest.id === id)!
|
|
115
|
+
await this.#disposePlugin(plugin)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async #setup(plugin: PluginDefinition, missing: string[]): Promise<PluginReport> {
|
|
120
|
+
const { id, provides = [] } = plugin.manifest
|
|
121
|
+
try {
|
|
122
|
+
await plugin.setup(this.#context(plugin))
|
|
123
|
+
const unprovided = provides.filter(t => this.services.owner(t) !== id).map(t => t.id)
|
|
124
|
+
if (unprovided.length > 0) throw new Error(`declared but did not provide: ${unprovided.join(', ')}`)
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await this.#disposePlugin(plugin)
|
|
127
|
+
const reason = errorMessage(error)
|
|
128
|
+
this.#log.error(`[presmium] ${id} failed to start`, error)
|
|
129
|
+
this.#emit({ type: 'kernel.plugin.failed', payload: { id, error: reason } })
|
|
130
|
+
return { id, status: 'failed', reason }
|
|
131
|
+
}
|
|
132
|
+
if (missing.length > 0) {
|
|
133
|
+
this.#log.warn(`[presmium] ${id} started without optional services: ${missing.join(', ')}`)
|
|
134
|
+
this.#emit({ type: 'kernel.plugin.degraded', payload: { id, missing } })
|
|
135
|
+
return { id, status: 'degraded', reason: `optional services unavailable: ${missing.join(', ')}`, missing }
|
|
136
|
+
}
|
|
137
|
+
this.#emit({ type: 'kernel.plugin.started', payload: { id } })
|
|
138
|
+
return { id, status: 'started' }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async #disposePlugin(plugin: PluginDefinition): Promise<void> {
|
|
142
|
+
try {
|
|
143
|
+
await plugin.dispose?.()
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.#log.error(`[presmium] ${plugin.manifest.id} dispose failed`, error)
|
|
146
|
+
} finally {
|
|
147
|
+
this.services.revoke(plugin.manifest.id)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
#disable(id: string, reason: string, missing?: string[]): PluginReport {
|
|
152
|
+
this.#log.warn(`[presmium] ${id} disabled: ${reason}`)
|
|
153
|
+
this.#emit({ type: 'kernel.plugin.disabled', payload: { id, reason } })
|
|
154
|
+
return missing ? { id, status: 'disabled', reason, missing } : { id, status: 'disabled', reason }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#emit(event: KernelEvent): void {
|
|
158
|
+
this.events.emit(event)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#context(plugin: PluginDefinition): PluginContext {
|
|
162
|
+
const { id, provides = [], requires = [] } = plugin.manifest
|
|
163
|
+
const declared = new Set([...requires.map(r => r.token.id), ...provides.map(t => t.id)])
|
|
164
|
+
const assertDeclared = (token: ServiceToken<unknown>) => {
|
|
165
|
+
if (!declared.has(token.id)) throw new Error(`${id} did not declare service ${token.id} in its manifest`)
|
|
166
|
+
}
|
|
167
|
+
const config = this.#config[id]
|
|
168
|
+
return {
|
|
169
|
+
config: isRecord(config) ? { ...config } : {},
|
|
170
|
+
mode: this.mode,
|
|
171
|
+
provide: (token, implementation) => {
|
|
172
|
+
if (!provides.some(t => t.id === token.id)) {
|
|
173
|
+
throw new Error(`${id} provides ${token.id} without declaring it in manifest.provides`)
|
|
174
|
+
}
|
|
175
|
+
this.services.provide(token, implementation, id)
|
|
176
|
+
},
|
|
177
|
+
get: token => {
|
|
178
|
+
assertDeclared(token)
|
|
179
|
+
return this.services.get(token)
|
|
180
|
+
},
|
|
181
|
+
tryGet: token => {
|
|
182
|
+
assertDeclared(token)
|
|
183
|
+
return this.services.tryGet(token)
|
|
184
|
+
},
|
|
185
|
+
events: this.events,
|
|
186
|
+
log: prefixedLogger(this.#log, id),
|
|
187
|
+
...this.#environments ? { environments: this.#environments } : {},
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
package/src/keyboard.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Which key presses a page shortcut may take. Text entry, editors and open menus or dialogs keep every key. A control
|
|
2
|
+
// the keyboard moved to keeps the keys it uses itself: Space presses a button or toggles a checkbox, and the arrows move
|
|
3
|
+
// a slider, a select, a pane separator, the items of a toolbar or tab list, or scroll a focusable region. A click leaves the focus on the
|
|
4
|
+
// clicked control; Space and the arrows then mean what they mean everywhere else on the page (play or pause, the next
|
|
5
|
+
// step) instead of pressing that control again.
|
|
6
|
+
|
|
7
|
+
const TEXT = 'textarea, [contenteditable]:not([contenteditable="false"]), input:not([type="button"], [type="checkbox"], [type="radio"], [type="range"], [type="reset"], [type="submit"], [type="color"], [type="file"], [type="image"])'
|
|
8
|
+
const LAYER = '[role="dialog"], [role="alertdialog"], [role="menu"], [role="listbox"]'
|
|
9
|
+
const PRESSABLE = 'button, a[href], summary, select, input, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="option"], [role="treeitem"]'
|
|
10
|
+
const NAVIGABLE = 'select, input[type="range"], [role="slider"], [role="spinbutton"], [role="tablist"], [role="toolbar"], [role="radiogroup"], [role="grid"], [role="tree"], [role="menubar"]'
|
|
11
|
+
const NAVIGATION_KEYS = new Set(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'])
|
|
12
|
+
|
|
13
|
+
const sources = new WeakMap<Document, { pointer: boolean, byPointer: WeakSet<Element> }>()
|
|
14
|
+
|
|
15
|
+
/** Starts noting, in `document`, which elements took the focus from a pointer press. Call it before the first click. */
|
|
16
|
+
export function watchFocusSource(document: Document | undefined = globalThis.document): void {
|
|
17
|
+
if (!document || sources.has(document)) return
|
|
18
|
+
const source = { pointer: false, byPointer: new WeakSet<Element>() }
|
|
19
|
+
sources.set(document, source)
|
|
20
|
+
document.addEventListener('pointerdown', () => { source.pointer = true }, true)
|
|
21
|
+
document.addEventListener('keydown', () => { source.pointer = false }, true)
|
|
22
|
+
document.addEventListener('focusin', event => {
|
|
23
|
+
const target = event.composedPath()[0]
|
|
24
|
+
if (!(target instanceof Element)) return
|
|
25
|
+
if (source.pointer) source.byPointer.add(target)
|
|
26
|
+
else source.byPointer.delete(target)
|
|
27
|
+
}, true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a key press belongs to the element it was pressed on, so that a page shortcut leaves it alone. */
|
|
31
|
+
export function keyBelongsToTarget(event: KeyboardEvent): boolean {
|
|
32
|
+
if (event.isComposing || event.keyCode === 229) return true
|
|
33
|
+
const target = event.composedPath?.()[0] ?? event.target
|
|
34
|
+
if (!(target instanceof Element)) return false
|
|
35
|
+
if ((target as HTMLElement).isContentEditable || target.closest(TEXT) || target.closest(LAYER)) return true
|
|
36
|
+
if (event.key !== ' ' && !NAVIGATION_KEYS.has(event.key)) return false
|
|
37
|
+
// Resize handles keep their keys after pointer focus too; resizing must never
|
|
38
|
+
// navigate a teaching step or start its playback.
|
|
39
|
+
if (target.closest('[role="separator"]')) return true
|
|
40
|
+
if (sources.get(target.ownerDocument)?.byPointer.has(target)) return false
|
|
41
|
+
const region = (target as HTMLElement).tabIndex >= 0 && !target.matches(`${PRESSABLE}, ${NAVIGABLE}`)
|
|
42
|
+
return region || Boolean(target.closest(event.key === ' ' ? PRESSABLE : NAVIGABLE))
|
|
43
|
+
}
|
package/src/locale.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { DEFAULT_LANG } from './contracts/locale.ts'
|
|
2
|
+
import type { Locale } from './contracts/locale.ts'
|
|
3
|
+
|
|
4
|
+
export function localeOf(lang: string | null | undefined): Locale | undefined {
|
|
5
|
+
const tag = lang?.trim().toLowerCase() ?? ''
|
|
6
|
+
if (tag === 'zh' || tag.startsWith('zh-')) return 'zh-CN'
|
|
7
|
+
if (tag === 'en' || tag.startsWith('en-')) return 'en'
|
|
8
|
+
return undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deckLocale(lang?: string): Locale {
|
|
12
|
+
return localeOf(lang || DEFAULT_LANG) ?? 'en'
|
|
13
|
+
}
|