@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.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +32 -2
  3. package/package.json +41 -3
  4. package/src/access.ts +18 -0
  5. package/src/anchors.ts +58 -0
  6. package/src/authoring.ts +56 -0
  7. package/src/blocks.ts +19 -0
  8. package/src/checks.ts +35 -0
  9. package/src/component-code.ts +6 -0
  10. package/src/contracts/agent.ts +76 -0
  11. package/src/contracts/anchors.ts +46 -0
  12. package/src/contracts/authoring-actions.ts +60 -0
  13. package/src/contracts/authoring-clients.ts +213 -0
  14. package/src/contracts/authoring.ts +180 -0
  15. package/src/contracts/checks.ts +29 -0
  16. package/src/contracts/classroom.ts +49 -0
  17. package/src/contracts/diagnostics.ts +37 -0
  18. package/src/contracts/document-audio.ts +150 -0
  19. package/src/contracts/document-comments.ts +31 -0
  20. package/src/contracts/document-history.ts +63 -0
  21. package/src/contracts/document-image.ts +32 -0
  22. package/src/contracts/document-reads.ts +57 -0
  23. package/src/contracts/document-voice.ts +44 -0
  24. package/src/contracts/document.ts +240 -0
  25. package/src/contracts/execution.ts +118 -0
  26. package/src/contracts/locale.ts +3 -0
  27. package/src/contracts/offline.ts +2 -0
  28. package/src/contracts/operations.ts +8 -0
  29. package/src/contracts/plugin.ts +183 -0
  30. package/src/contracts/presenter-panels.ts +17 -0
  31. package/src/contracts/preview.ts +386 -0
  32. package/src/contracts/production-assets.ts +61 -0
  33. package/src/contracts/production.ts +109 -0
  34. package/src/contracts/projects.ts +59 -0
  35. package/src/contracts/server.ts +108 -0
  36. package/src/contracts/service.ts +8 -0
  37. package/src/contracts/speech-settings.ts +16 -0
  38. package/src/contracts/speech.ts +149 -0
  39. package/src/contracts/steps.ts +15 -0
  40. package/src/contracts/transcription-adoption.ts +79 -0
  41. package/src/contracts/transcription.ts +112 -0
  42. package/src/contracts/voice.ts +97 -0
  43. package/src/contracts/voiceover-execution.ts +68 -0
  44. package/src/contracts/workspace.ts +458 -0
  45. package/src/contracts.ts +35 -0
  46. package/src/diagnostics.ts +7 -0
  47. package/src/document-authoring-actions.ts +47 -0
  48. package/src/document-caption-timings.ts +64 -0
  49. package/src/document-colors.ts +25 -0
  50. package/src/document-comments.ts +118 -0
  51. package/src/document-components.ts +251 -0
  52. package/src/document-operations.ts +132 -0
  53. package/src/document-pages.ts +215 -0
  54. package/src/document-reads.ts +81 -0
  55. package/src/document-schema.ts +85 -0
  56. package/src/document-voices.ts +153 -0
  57. package/src/document.ts +252 -0
  58. package/src/durations.ts +7 -0
  59. package/src/errors.ts +15 -0
  60. package/src/events.ts +32 -0
  61. package/src/execution.ts +25 -0
  62. package/src/fingerprint.ts +31 -0
  63. package/src/follow.ts +33 -0
  64. package/src/git.ts +12 -0
  65. package/src/html.ts +41 -0
  66. package/src/index.ts +82 -0
  67. package/src/ir.ts +367 -0
  68. package/src/jupyter-kernel.ts +9 -0
  69. package/src/kernel.ts +190 -0
  70. package/src/keyboard.ts +43 -0
  71. package/src/locale.ts +13 -0
  72. package/src/lock.ts +81 -0
  73. package/src/logging.ts +11 -0
  74. package/src/lower-myst.ts +312 -0
  75. package/src/node/assets.ts +23 -0
  76. package/src/node/authoring-actions.ts +50 -0
  77. package/src/node/authoring-clients.ts +14 -0
  78. package/src/node/document-audio.ts +22 -0
  79. package/src/node/document-history.ts +11 -0
  80. package/src/node/document-image.ts +8 -0
  81. package/src/node/document-reads.ts +10 -0
  82. package/src/node/document-voice.ts +7 -0
  83. package/src/node/ffmpeg.ts +29 -0
  84. package/src/node/file-version.ts +6 -0
  85. package/src/node/fonts.ts +22 -0
  86. package/src/node/index.ts +29 -0
  87. package/src/node/installed-packages.ts +11 -0
  88. package/src/node/lines.ts +24 -0
  89. package/src/node/locale.ts +7 -0
  90. package/src/node/network.ts +11 -0
  91. package/src/node/operator-speech.ts +91 -0
  92. package/src/node/packages.ts +74 -0
  93. package/src/node/paths.ts +7 -0
  94. package/src/node/projects.ts +7 -0
  95. package/src/node/resources.ts +86 -0
  96. package/src/node/revision.ts +6 -0
  97. package/src/node/run-directory.ts +70 -0
  98. package/src/node/strip-types.mjs +15 -0
  99. package/src/node/transcription-adoption.ts +9 -0
  100. package/src/node/transcription.ts +10 -0
  101. package/src/node/voiceover-routes.ts +73 -0
  102. package/src/node/voiceover.ts +199 -0
  103. package/src/page-source.ts +359 -0
  104. package/src/parameters.ts +81 -0
  105. package/src/preview-edit.ts +18 -0
  106. package/src/preview-events.ts +57 -0
  107. package/src/preview-frame.ts +1220 -0
  108. package/src/props.ts +17 -0
  109. package/src/published.ts +11 -0
  110. package/src/push-stream.ts +67 -0
  111. package/src/reading.ts +96 -0
  112. package/src/recording.ts +38 -0
  113. package/src/renderer-components.ts +30 -0
  114. package/src/requests.ts +28 -0
  115. package/src/responses.ts +49 -0
  116. package/src/run-outputs.ts +15 -0
  117. package/src/runner.py +454 -0
  118. package/src/services.ts +146 -0
  119. package/src/slides.ts +36 -0
  120. package/src/speech-words.ts +76 -0
  121. package/src/speech.ts +13 -0
  122. package/src/step-keys.ts +38 -0
  123. package/src/steps.ts +7 -0
  124. package/src/transcription-adoption.ts +105 -0
  125. package/src/uri-decoding.ts +8 -0
  126. package/src/urls.ts +13 -0
  127. package/src/views.ts +47 -0
  128. 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
+ }
@@ -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
+ }