@lincyaw/kernel 1.0.0-rc.0 → 1.0.0-rc.2

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 (119) hide show
  1. package/README.md +7 -7
  2. package/package.json +6 -2
  3. package/src/access.ts +18 -0
  4. package/src/anchors.ts +5 -1
  5. package/src/authoring.ts +56 -0
  6. package/src/blocks.ts +0 -8
  7. package/src/component-code.ts +6 -0
  8. package/src/contracts/agent.ts +5 -82
  9. package/src/contracts/anchors.ts +46 -0
  10. package/src/contracts/authoring-actions.ts +60 -0
  11. package/src/contracts/authoring-clients.ts +213 -0
  12. package/src/contracts/authoring.ts +180 -0
  13. package/src/contracts/checks.ts +29 -0
  14. package/src/contracts/classroom.ts +49 -0
  15. package/src/contracts/diagnostics.ts +37 -0
  16. package/src/contracts/document-audio.ts +150 -0
  17. package/src/contracts/document-comments.ts +31 -0
  18. package/src/contracts/document-history.ts +63 -0
  19. package/src/contracts/document-image.ts +32 -0
  20. package/src/contracts/document-reads.ts +57 -0
  21. package/src/contracts/document-voice.ts +44 -0
  22. package/src/contracts/document.ts +240 -0
  23. package/src/contracts/execution.ts +4 -5
  24. package/src/contracts/locale.ts +3 -0
  25. package/src/contracts/offline.ts +2 -0
  26. package/src/contracts/plugin.ts +183 -0
  27. package/src/contracts/presenter-panels.ts +17 -0
  28. package/src/contracts/preview.ts +305 -310
  29. package/src/contracts/production-assets.ts +61 -0
  30. package/src/contracts/production.ts +109 -0
  31. package/src/contracts/projects.ts +59 -0
  32. package/src/contracts/server.ts +1 -51
  33. package/src/contracts/speech-settings.ts +16 -0
  34. package/src/contracts/speech.ts +82 -70
  35. package/src/contracts/steps.ts +15 -0
  36. package/src/contracts/transcription-adoption.ts +79 -0
  37. package/src/contracts/transcription.ts +112 -0
  38. package/src/contracts/voice.ts +97 -0
  39. package/src/contracts/voiceover-execution.ts +68 -0
  40. package/src/contracts/workspace.ts +458 -0
  41. package/src/contracts.ts +28 -278
  42. package/src/diagnostics.ts +7 -0
  43. package/src/document-authoring-actions.ts +47 -0
  44. package/src/document-caption-timings.ts +64 -0
  45. package/src/document-colors.ts +25 -0
  46. package/src/document-comments.ts +118 -0
  47. package/src/document-components.ts +251 -0
  48. package/src/document-operations.ts +132 -0
  49. package/src/document-pages.ts +215 -0
  50. package/src/document-reads.ts +81 -0
  51. package/src/document-schema.ts +85 -0
  52. package/src/document-voices.ts +153 -0
  53. package/src/document.ts +252 -0
  54. package/src/durations.ts +7 -0
  55. package/src/errors.ts +8 -0
  56. package/src/execution.ts +5 -1
  57. package/src/fingerprint.ts +1 -1
  58. package/src/follow.ts +26 -30
  59. package/src/git.ts +1 -1
  60. package/src/index.ts +66 -13
  61. package/src/ir.ts +54 -5
  62. package/src/jupyter-kernel.ts +9 -0
  63. package/src/kernel.ts +38 -25
  64. package/src/keyboard.ts +43 -0
  65. package/src/locale.ts +13 -0
  66. package/src/lock.ts +4 -8
  67. package/src/logging.ts +1 -1
  68. package/src/{markdown.ts → lower-myst.ts} +2 -1
  69. package/src/node/assets.ts +23 -0
  70. package/src/node/authoring-actions.ts +50 -0
  71. package/src/node/authoring-clients.ts +14 -0
  72. package/src/node/document-audio.ts +22 -0
  73. package/src/node/document-history.ts +11 -0
  74. package/src/node/document-image.ts +8 -0
  75. package/src/node/document-reads.ts +10 -0
  76. package/src/node/document-voice.ts +7 -0
  77. package/src/node/ffmpeg.ts +29 -0
  78. package/src/node/file-version.ts +6 -0
  79. package/src/node/fonts.ts +22 -0
  80. package/src/node/index.ts +29 -133
  81. package/src/node/installed-packages.ts +11 -0
  82. package/src/node/lines.ts +24 -0
  83. package/src/node/locale.ts +7 -0
  84. package/src/node/network.ts +11 -0
  85. package/src/node/operator-speech.ts +91 -0
  86. package/src/node/packages.ts +74 -0
  87. package/src/node/paths.ts +7 -0
  88. package/src/node/projects.ts +7 -0
  89. package/src/node/resources.ts +86 -0
  90. package/src/node/revision.ts +6 -0
  91. package/src/node/run-directory.ts +70 -0
  92. package/src/node/transcription-adoption.ts +9 -0
  93. package/src/node/transcription.ts +10 -0
  94. package/src/node/voiceover-routes.ts +73 -0
  95. package/src/node/voiceover.ts +199 -0
  96. package/src/page-source.ts +359 -0
  97. package/src/parameters.ts +81 -0
  98. package/src/preview-edit.ts +18 -0
  99. package/src/preview-events.ts +57 -0
  100. package/src/preview-frame.ts +1220 -0
  101. package/src/props.ts +17 -0
  102. package/src/push-stream.ts +67 -0
  103. package/src/reading.ts +12 -7
  104. package/src/recording.ts +38 -0
  105. package/src/renderer-components.ts +30 -0
  106. package/src/responses.ts +49 -0
  107. package/src/run-outputs.ts +15 -0
  108. package/src/runner.py +3 -3
  109. package/src/services.ts +6 -6
  110. package/src/slides.ts +36 -0
  111. package/src/speech-words.ts +76 -0
  112. package/src/speech.ts +13 -0
  113. package/src/step-keys.ts +38 -0
  114. package/src/steps.ts +7 -0
  115. package/src/transcription-adoption.ts +105 -0
  116. package/src/uri-decoding.ts +8 -0
  117. package/src/urls.ts +13 -0
  118. package/src/views.ts +47 -0
  119. package/src/voice.ts +86 -0
@@ -1,4 +1,4 @@
1
- export function normalizeWhitespace(text: string): string {
1
+ function normalizeWhitespace(text: string): string {
2
2
  const lines = text
3
3
  .replace(/\r\n?/g, '\n')
4
4
  .split('\n')
package/src/follow.ts CHANGED
@@ -1,37 +1,33 @@
1
- import type { IRBlock, IRSection } from './ir.ts'
1
+ import type { IRSection } from './ir.ts'
2
+ import { stepTrail, trailStep } from './step-keys.ts'
3
+ import type { StepKeys } from './step-keys.ts'
2
4
 
3
- export type StepKeys = string[][]
4
-
5
- export function stepKeys(section: Pick<IRSection, 'blocks' | 'steps'>, renderer?: string): StepKeys {
6
- const keys: StepKeys = Array.from({ length: section.steps + 1 }, () => [])
7
- const add = (step: number, key: string) => step > 0 && keys[step]?.push(key)
8
- let left = false
9
- const visit = (blocks: readonly IRBlock[], prefix: string) => {
10
- for (const block of blocks) {
11
- if (renderer !== undefined && block.only && !block.only.includes(renderer)) {
12
- left = true
13
- continue
14
- }
15
- const id = prefix + block.id
16
- if (block.role === 'visual') {
17
- for (let k = 0; k <= (block.span ?? 0); k++) add(block.at + k, k === 0 ? id : `${id}+${k}`)
18
- if (block.until !== undefined) add(block.until, `${id}-`)
19
- }
20
- if (block.node.type === 'component' && block.node.children) visit(block.node.children, `${id}/`)
21
- }
22
- }
23
- visit(section.blocks, '')
24
- return left ? keys.filter((step, i) => i === 0 || step.length > 0) : keys
5
+ /** The presenter's position by ID, so learners with another version of the document reach the same place. */
6
+ export interface FollowAnchor {
7
+ section?: string
8
+ trail?: string[]
9
+ back?: string
25
10
  }
26
11
 
27
- export function stepTrail(keys: StepKeys, step: number): string[] {
28
- return keys.slice(1, step + 1).flat()
12
+ const LEARNERS = 'student'
13
+
14
+ /** For a section only some audiences have: the nearest earlier section that the other versions have too. */
15
+ export function backSection(sections: readonly Pick<IRSection, 'id' | 'audience'>[], id: string): string | undefined {
16
+ const at = sections.findIndex(section => section.id === id)
17
+ const audience = sections[at]?.audience
18
+ if (!audience?.length) return undefined
19
+ const learners = !audience.includes(LEARNERS)
20
+ return sections.slice(0, at).findLast(section => !section.audience?.length || (learners && section.audience.includes(LEARNERS)))?.id
29
21
  }
30
22
 
31
- export function trailStep(keys: StepKeys, trail: readonly string[]): number {
32
- const reached = new Set(trail)
33
- for (let step = keys.length - 1; step > 0; step--) {
34
- if (keys[step]!.some(key => reached.has(key))) return step
23
+ /** Where a learner goes in their own version of the document when following the presenter's position. */
24
+ export function followedPlace(position: FollowAnchor & { step: number }, keysOf: (id: string) => StepKeys | undefined): { section: string, step: number } | undefined {
25
+ const { section, trail, back, step } = position
26
+ const keys = section === undefined ? undefined : keysOf(section)
27
+ if (keys) {
28
+ const other = trail !== undefined && stepTrail(keys, step).join('\n') !== trail.join('\n')
29
+ return { section: section!, step: other ? trailStep(keys, trail) : step }
35
30
  }
36
- return 0
31
+ const before = back === undefined ? undefined : keysOf(back)
32
+ return before && { section: back!, step: before.length - 1 }
37
33
  }
package/src/git.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const GIT_SAFE_CONFIG: readonly string[] = ['core.hooksPath=/dev/null', 'core.fsmonitor=false', 'core.quotePath=false', 'commit.gpgSign=false', 'tag.gpgSign=false']
1
+ const GIT_SAFE_CONFIG: readonly string[] = ['core.hooksPath=/dev/null', 'core.fsmonitor=false', 'core.quotePath=false', 'commit.gpgSign=false', 'tag.gpgSign=false']
2
2
 
3
3
  export function gitConfigArgs(): string[] {
4
4
  return GIT_SAFE_CONFIG.flatMap(entry => ['-c', entry])
package/src/index.ts CHANGED
@@ -1,29 +1,82 @@
1
1
  export * from './contracts.ts'
2
+ export { applyDocumentOperations } from './document-operations.ts'
3
+ export { validateDocumentChange, DOCUMENT_CHANGE_JSON_SCHEMA } from './authoring.ts'
4
+ export { allocateDocumentPage, createDocumentPage, documentPageDefaults, DOCUMENT_PAGE_DEFAULT_TIMING, duplicateDocumentPage, moveDocumentPage, pageContents, pageDeletion, pageDeletionImpact, removeDocumentPage, renameDocumentPage } from './document-pages.ts'
5
+ export { documentOutline, documentPart, DocumentReadError } from './document-reads.ts'
6
+ export { changeDocumentVoice, documentVoiceStepIds, previewDocumentVoiceChange, resolveDocumentStepVoice } from './document-voices.ts'
7
+ export { DOCUMENT_COLOR_NAMES, documentColorToken, documentColorValue } from './document-colors.ts'
8
+ export type { DocumentColorKind, DocumentColorName } from './document-colors.ts'
9
+ export { applyDocumentCommentAction, isDocumentTextLink, documentCommentTargets, validateDocumentCommentAnchor, DocumentCommentError } from './document-comments.ts'
10
+ export { applyDocumentAuthoringAction } from './document-authoring-actions.ts'
11
+ export { applyDocumentComponentAction, effectiveComponentParameters, createDocumentParameterChanges, documentComponentSection, DocumentComponentError } from './document-components.ts'
12
+ export type { DocumentStepVoice, DocumentVoiceImpact, DocumentVoiceOptions, DocumentVoiceScope } from './document-voices.ts'
13
+ export { prepareTranscriptionAdoption } from './transcription-adoption.ts'
14
+ export { productionAssetIds, recordingStepIssue, recordingSourceRange } from './recording.ts'
2
15
  export * from './ir.ts'
3
- export { createAnchorResolverRegistry, parseAnchor } from './anchors.ts'
16
+ export { createAnchorResolverRegistry, isRenderable, parseAnchor } from './anchors.ts'
4
17
  export type { ParsedAnchor } from './anchors.ts'
5
- export { docBlocks, irBlocks, nodeMdast, withProp } from './blocks.ts'
18
+ export { diagnosticOf } from './diagnostics.ts'
19
+ export { DOCUMENT_JSON_SCHEMA, validateDocument } from './document.ts'
20
+ export { validateDocumentParameters, documentParameterAccess, documentParameterSchemaForAccess } from './parameters.ts'
21
+ export type { DocumentParameterAccess, DocumentParameterIssue, DocumentParameterSchemaReference, DocumentParameterValidation } from './parameters.ts'
22
+ export { docBlocks, irBlocks, nodeMdast } from './blocks.ts'
6
23
  export { CheckRegistry } from './checks.ts'
7
- export { errorMessage, isRecord } from './errors.ts'
24
+ export { errorMessage, isRecord, PreviewPreparing } from './errors.ts'
8
25
  export { createEventBus } from './events.ts'
9
- export { CREDENTIAL_VARIABLE, failedRun, languageId } from './execution.ts'
10
- export { GIT_SAFE_CONFIG, gitConfigArgs, gitEnvironment } from './git.ts'
26
+ export { CREDENTIAL_VARIABLE, environmentExecutor, failedRun, languageId } from './execution.ts'
27
+ export { gitConfigArgs, gitEnvironment } from './git.ts'
11
28
  export { escapeHtml, htmlAttributes, mapHtmlUrls, URL_ATTRIBUTES } from './html.ts'
12
29
  export type { HtmlAttribute } from './html.ts'
13
30
  export { hostName, localRequestRefusal } from './requests.ts'
14
31
  export type { RequestHeaders } from './requests.ts'
15
- export { fingerprint, fnv1a64, normalizeWhitespace } from './fingerprint.ts'
16
- export { stepKeys, stepTrail, trailStep } from './follow.ts'
17
- export type { StepKeys } from './follow.ts'
32
+ export { fingerprint, fnv1a64 } from './fingerprint.ts'
33
+ export { applyDocumentCaptionTiming, captionSourceFingerprint, captionTimingProblem, stepCaptionText, narrationCaptionText, CAPTION_TIMING_MAX_SEGMENTS, DocumentCaptionTimingError } from './document-caption-timings.ts'
34
+ export type { DocumentCaptionTimingAction } from './document-caption-timings.ts'
35
+ export { backSection, followedPlace } from './follow.ts'
36
+ export type { FollowAnchor } from './follow.ts'
18
37
  export { Kernel } from './kernel.ts'
19
38
  export type { KernelOptions } from './kernel.ts'
39
+ export { deckLocale, localeOf } from './locale.ts'
20
40
  export { ANCHOR_LOCK_PATH, AnchorLock } from './lock.ts'
21
- export { prefixed } from './logging.ts'
22
- export { lowerMyst, roleMark } from './markdown.ts'
23
- export type { LinkedType, LoweredMarkdown, LowerOptions } from './markdown.ts'
41
+ export { prefixedLogger } from './logging.ts'
42
+ export { lowerMyst, roleMark } from './lower-myst.ts'
43
+ export type { LinkedType, LoweredMarkdown, LowerOptions } from './lower-myst.ts'
44
+ export { connectPreviewFrame, isPreviewActive, isPreviewEmbedded, previewAppearance, previewFrameAncestors, reportPreview } from './preview-frame.ts'
45
+ export { keyBelongsToTarget, watchFocusSource } from './keyboard.ts'
46
+ export type { PreviewFrame, PreviewPage, PreviewProblem, PreviewTextEdit, PreviewTextHandle } from './preview-frame.ts'
47
+ export { previewEditBinding } from './preview-edit.ts'
48
+ export { pageEditCapability, pinPageSource, renamePageSource, movePageSource, removePageSource, pageRemovalTargets, pageIncomingReferences, writePage } from './page-source.ts'
49
+ export type { PageEditCapability, PageEditReason, PageRemoval } from './page-source.ts'
50
+ export { propText, withProp } from './props.ts'
24
51
  export { publishedUrl } from './published.ts'
25
52
  export type { PublishedUrlOptions } from './published.ts'
26
- export { blockState, blockText, blockVisible, chapters, isChapterTitle, mdastText, narrationAt, sectionNarration, tokenize } from './reading.ts'
53
+ export { blockState, blockText, blockVisible, blockStepState, chapters, isChapterTitle, mdastText, narrationAt, sectionNarration, tokenize } from './reading.ts'
27
54
  export type { Chapter } from './reading.ts'
28
- export { HOST_OWNER, kernelIncompatibility, ServiceRegistry } from './services.ts'
55
+ export { slideChapters, slideShape } from './slides.ts'
56
+ export { compactSteps, viewFor } from './views.ts'
57
+ export type { SlideShape } from './slides.ts'
58
+ export { jsonResponse, problemResponse, sseResponse } from './responses.ts'
59
+ export { kernelIncompatibility, ServiceRegistry } from './services.ts'
29
60
  export type { StartPlan } from './services.ts'
61
+ export { remapWords, splitWords } from './speech-words.ts'
62
+ export type { TextWord, WordSpan, WrittenWord } from './speech-words.ts'
63
+ export { stepKeys, stepTrail, trailStep } from './step-keys.ts'
64
+ export type { StepKeys } from './step-keys.ts'
65
+ export { parseDuration } from './durations.ts'
66
+ export { stepStateAt } from './steps.ts'
67
+ export { ABSOLUTE_URL, NON_FILE_URL, urlSlug } from './urls.ts'
68
+ export { isButton } from './lower-myst.ts'
69
+ export { componentProps, findComponentRenderer, hasRendererExtension, WEB_RENDERER_PACKAGE, withPublishedAssets } from './renderer-components.ts'
70
+ export { htmlAsText, isProseRun } from './run-outputs.ts'
71
+ export { safeDecodeURIComponent } from './uri-decoding.ts'
72
+ export { APPROVAL_KINDS, hasRole, isPresentingRole, LOCAL_AUTHOR } from './access.ts'
73
+ export { pushStream } from './push-stream.ts'
74
+ export type { PushStream, PushStreamOptions } from './push-stream.ts'
75
+ export { aigcLabel, aigcString, speechEngineId } from './speech.ts'
76
+ export { documentVoice, resolvedVoice, speechVoice, stepVoiceReason, voiceOverrides, voiceTarget } from './voice.ts'
77
+ export { isCjk } from './speech-words.ts'
78
+ export { componentCode } from './component-code.ts'
79
+ export { jupyterKernelLanguage } from './jupyter-kernel.ts'
80
+
81
+ export { createPreviewEvents } from './preview-events.ts'
82
+ export type { PreviewEvents } from './preview-events.ts'
package/src/ir.ts CHANGED
@@ -26,10 +26,21 @@ export interface SourcePos {
26
26
  file: string
27
27
  line: number
28
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
29
38
  }
30
39
 
31
40
  export interface IRDocument {
32
41
  ir: typeof IR_VERSION
42
+ /** Read-only provenance, never authored metadata or editable document content. */
43
+ source?: CanonicalSource
33
44
  meta: {
34
45
  title: string
35
46
  entry: string
@@ -46,6 +57,9 @@ export interface IRDocument {
46
57
  }
47
58
  sections: IRSection[]
48
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>
49
63
  }
50
64
 
51
65
  export interface IRSection {
@@ -56,6 +70,11 @@ export interface IRSection {
56
70
  source: SourcePos
57
71
  steps: number
58
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>
59
78
  hints: Record<RendererId, Record<string, unknown>>
60
79
  config?: Record<string, Record<string, unknown>>
61
80
  blocks: IRBlock[]
@@ -74,6 +93,8 @@ export interface IRBlock {
74
93
  audience?: string[]
75
94
  source: SourcePos
76
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 }[]
77
98
  }
78
99
 
79
100
  export type IRNode = MarkdownNode | CodeNode | ComponentNode
@@ -82,6 +103,8 @@ export interface MarkdownNode {
82
103
  type: 'markdown'
83
104
  mdast: MdastNode
84
105
  reveal?: 'items'
106
+ /** An explicitly authored step's speech starts with, or after, its animation. */
107
+ narration?: { mode: 'during' | 'after' }
85
108
  }
86
109
 
87
110
  export interface CodeOrigin {
@@ -129,6 +152,8 @@ export interface CodeNode {
129
152
  outputs?: RecordedRun[]
130
153
  variants?: CodeVariant[]
131
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 }[] }
132
157
  }
133
158
 
134
159
  export interface ComponentNode {
@@ -137,9 +162,13 @@ export interface ComponentNode {
137
162
  name: string
138
163
  props: Record<string, unknown>
139
164
  children?: IRBlock[]
165
+ ownsCode?: boolean
140
166
  fallback?: MdastNode
141
167
  assets?: Record<string, string>
142
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 }
143
172
  }
144
173
 
145
174
  export type DirectiveOptionType = 'string' | 'number' | 'boolean' | 'list'
@@ -233,24 +262,32 @@ export interface RenderContext {
233
262
  outDir: string
234
263
  workDir: string
235
264
  config: Record<string, unknown>
236
- extensions: RendererExtension[]
265
+ extensions: BoundRendererExtension[]
237
266
  plugins: string[]
238
267
  pluginDirs: Record<string, string>
239
268
  lang?: string
240
269
  renderers: RendererId[]
241
270
  base: string
242
- vitePlugins: unknown[]
271
+ nodeVitePlugins?: NodeVitePlugin[]
243
272
  extensionFor(node: ComponentNode): ((node: ComponentNode, block: IRBlock) => unknown) | undefined
244
273
  portable(value: unknown, from: string): unknown
245
274
  localAsset(source: SourcePos, url: string): LocalAsset | undefined
246
275
  documentLink?(source: SourcePos, url: string): DocumentLink | undefined
247
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
248
279
  service?<T>(token: ServiceToken<T>): T | undefined
249
280
  translations?: { lang: string, doc: IRDocument }[]
250
281
  server?: { url: string }
251
282
  preview?: { origins: string[] }
252
283
  }
253
284
 
285
+ export interface NodeVitePlugin {
286
+ id: string
287
+ entry: string
288
+ config: Record<string, unknown>
289
+ }
290
+
254
291
  export interface LocalAsset {
255
292
  file: string
256
293
  name: string
@@ -276,8 +313,7 @@ export interface RenderDevServer {
276
313
  export interface Renderer {
277
314
  id: RendererId
278
315
  site?: boolean
279
- support(node: IRNode, extensions: RendererExtension[]): Support
280
- prepare?(doc: IRDocument, ctx: RenderContext): Promise<void>
316
+ support(node: IRNode, extensions: BoundRendererExtension[]): Support
281
317
  build(doc: IRDocument, ctx: RenderContext): Promise<RenderOutput>
282
318
  dev?(doc: IRDocument, ctx: RenderContext): Promise<RenderDevServer>
283
319
  buildSite?(site: SiteSpec, ctx: RenderContext): Promise<RenderOutput>
@@ -287,10 +323,12 @@ export interface Renderer {
287
323
  export interface SiteDocument {
288
324
  dir: string
289
325
  title: string
326
+ description?: string
290
327
  lang?: string
291
328
  tags?: string[]
292
329
  doc?: IRDocument
293
330
  translations?: { lang: string, doc: IRDocument }[]
331
+ nodeVitePlugins?: NodeVitePlugin[]
294
332
  }
295
333
 
296
334
  export interface SiteSpec {
@@ -303,6 +341,17 @@ export interface SiteSpec {
303
341
  export interface SiteDevServer {
304
342
  url: string
305
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>
306
355
  close(): Promise<void>
307
356
  }
308
357
 
@@ -312,7 +361,7 @@ export interface RendererContribution {
312
361
  components?: Record<string, ComponentRenderer>
313
362
  }
314
363
 
315
- export interface RendererExtension {
364
+ export interface BoundRendererExtension {
316
365
  plugin: string
317
366
  components?: Record<string, (node: ComponentNode, block: IRBlock) => unknown>
318
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 CHANGED
@@ -11,9 +11,9 @@ import type {
11
11
  ServiceToken,
12
12
  StartReport,
13
13
  } from './contracts.ts'
14
- import { errorMessage } from './errors.ts'
14
+ import { errorMessage, isRecord } from './errors.ts'
15
15
  import { createEventBus } from './events.ts'
16
- import { prefixed } from './logging.ts'
16
+ import { prefixedLogger } from './logging.ts'
17
17
  import { ServiceRegistry } from './services.ts'
18
18
 
19
19
  export interface KernelOptions {
@@ -34,6 +34,8 @@ export class Kernel {
34
34
  #environments?: Readonly<Record<string, EnvironmentSpec>>
35
35
  #plugins: PluginDefinition[] = []
36
36
  #state: 'idle' | 'starting' | 'started' | 'disposed' = 'idle'
37
+ #starting?: Promise<StartReport>
38
+ #disposing?: Promise<void>
37
39
  #report?: StartReport
38
40
 
39
41
  constructor(options: KernelOptions) {
@@ -54,8 +56,8 @@ export class Kernel {
54
56
  this.#plugins.push(plugin)
55
57
  }
56
58
 
57
- provide<T>(token: ServiceToken<T>, impl: T): void {
58
- this.services.provide(token, impl)
59
+ provide<T>(token: ServiceToken<T>, implementation: T): void {
60
+ this.services.provide(token, implementation)
59
61
  }
60
62
 
61
63
  getService<T>(token: ServiceToken<T>): T | undefined {
@@ -66,9 +68,13 @@ export class Kernel {
66
68
  return this.#report
67
69
  }
68
70
 
69
- async start(): Promise<StartReport> {
70
- if (this.#state !== 'idle') throw new Error('kernel already started')
71
+ start(): Promise<StartReport> {
72
+ if (this.#state !== 'idle') return Promise.reject(new Error('kernel already started'))
71
73
  this.#state = 'starting'
74
+ return this.#starting = Promise.resolve().then(() => this.#start())
75
+ }
76
+
77
+ async #start(): Promise<StartReport> {
72
78
  const plan = this.services.plan(this.#plugins.map(p => p.manifest), this.version)
73
79
  const reports = new Map<string, PluginReport>()
74
80
  for (const [id, reason] of plan.excluded) reports.set(id, this.#disable(id, reason))
@@ -87,26 +93,26 @@ export class Kernel {
87
93
  reports.set(id, await this.#setup(plugin, missing))
88
94
  }
89
95
 
90
- this.#report = { order, plugins: this.#plugins.map(p => reports.get(p.manifest.id)!), cycles: plan.cycles }
91
- this.#state = 'started'
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'
92
98
  return this.#report
93
99
  }
94
100
 
95
- async dispose(): Promise<void> {
96
- if (this.#state === 'disposed') return
101
+ dispose(): Promise<void> {
102
+ if (this.#disposing) return this.#disposing
97
103
  this.#state = 'disposed'
104
+ return this.#disposing = this.#dispose()
105
+ }
106
+
107
+ async #dispose(): Promise<void> {
108
+ await this.#starting?.catch(() => undefined)
98
109
  const running = new Set(
99
110
  this.#report?.plugins.filter(r => r.status === 'started' || r.status === 'degraded').map(r => r.id),
100
111
  )
101
112
  for (const id of [...(this.#report?.order ?? [])].reverse()) {
102
113
  if (!running.has(id)) continue
103
114
  const plugin = this.#plugins.find(p => p.manifest.id === id)!
104
- try {
105
- await plugin.dispose?.()
106
- } catch (error) {
107
- this.#log.error(`[presmium] ${id} dispose failed`, error)
108
- }
109
- this.services.revoke(id)
115
+ await this.#disposePlugin(plugin)
110
116
  }
111
117
  }
112
118
 
@@ -117,7 +123,7 @@ export class Kernel {
117
123
  const unprovided = provides.filter(t => this.services.owner(t) !== id).map(t => t.id)
118
124
  if (unprovided.length > 0) throw new Error(`declared but did not provide: ${unprovided.join(', ')}`)
119
125
  } catch (error) {
120
- this.services.revoke(id)
126
+ await this.#disposePlugin(plugin)
121
127
  const reason = errorMessage(error)
122
128
  this.#log.error(`[presmium] ${id} failed to start`, error)
123
129
  this.#emit({ type: 'kernel.plugin.failed', payload: { id, error: reason } })
@@ -132,6 +138,16 @@ export class Kernel {
132
138
  return { id, status: 'started' }
133
139
  }
134
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
+
135
151
  #disable(id: string, reason: string, missing?: string[]): PluginReport {
136
152
  this.#log.warn(`[presmium] ${id} disabled: ${reason}`)
137
153
  this.#emit({ type: 'kernel.plugin.disabled', payload: { id, reason } })
@@ -148,14 +164,15 @@ export class Kernel {
148
164
  const assertDeclared = (token: ServiceToken<unknown>) => {
149
165
  if (!declared.has(token.id)) throw new Error(`${id} did not declare service ${token.id} in its manifest`)
150
166
  }
167
+ const config = this.#config[id]
151
168
  return {
152
- config: pluginConfig(this.#config[id]),
169
+ config: isRecord(config) ? { ...config } : {},
153
170
  mode: this.mode,
154
- provide: (token, impl) => {
171
+ provide: (token, implementation) => {
155
172
  if (!provides.some(t => t.id === token.id)) {
156
173
  throw new Error(`${id} provides ${token.id} without declaring it in manifest.provides`)
157
174
  }
158
- this.services.provide(token, impl, id)
175
+ this.services.provide(token, implementation, id)
159
176
  },
160
177
  get: token => {
161
178
  assertDeclared(token)
@@ -166,12 +183,8 @@ export class Kernel {
166
183
  return this.services.tryGet(token)
167
184
  },
168
185
  events: this.events,
169
- log: prefixed(this.#log, id),
186
+ log: prefixedLogger(this.#log, id),
170
187
  ...this.#environments ? { environments: this.#environments } : {},
171
188
  }
172
189
  }
173
190
  }
174
-
175
- function pluginConfig(value: unknown): Record<string, unknown> {
176
- return value !== null && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {}
177
- }
@@ -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
+ }
package/src/lock.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { isRenderable } from './contracts.ts'
1
+ import { isRenderable } from './anchors.ts'
2
2
  import type { AnchorLockFile, RecordedAnchor, Resolution } from './contracts.ts'
3
- import { errorMessage } from './errors.ts'
3
+ import { errorMessage, isRecord } from './errors.ts'
4
4
 
5
5
  export const ANCHOR_LOCK_PATH = '.presmium/anchors.lock.json'
6
6
 
@@ -67,11 +67,11 @@ export class AnchorLock {
67
67
  }
68
68
 
69
69
  function validate(data: unknown): AnchorLockFile {
70
- if (!isObject(data) || data.version !== 1 || !isObject(data.anchors)) {
70
+ if (!isRecord(data) || data.version !== 1 || !isRecord(data.anchors)) {
71
71
  throw new Error('invalid anchor lock: expected { "version": 1, "anchors": { ... } }')
72
72
  }
73
73
  for (const [anchor, record] of Object.entries(data.anchors)) {
74
- const valid = isObject(record)
74
+ const valid = isRecord(record)
75
75
  && typeof record.fingerprint === 'string' && record.fingerprint !== ''
76
76
  && typeof record.file === 'string'
77
77
  && Number.isInteger(record.startLine) && (record.startLine as number) >= 1
@@ -79,7 +79,3 @@ function validate(data: unknown): AnchorLockFile {
79
79
  }
80
80
  return data as unknown as AnchorLockFile
81
81
  }
82
-
83
- function isObject(value: unknown): value is Record<string, unknown> {
84
- return value !== null && typeof value === 'object' && !Array.isArray(value)
85
- }
package/src/logging.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from './contracts.ts'
2
2
 
3
- export function prefixed(log: Logger, id: string): Logger {
3
+ export function prefixedLogger(log: Logger, id: string): Logger {
4
4
  const tag = `[${id}]`
5
5
  return {
6
6
  debug: (...args) => log.debug(tag, ...args),
@@ -198,7 +198,8 @@ function cardsAs(options: LowerOptions): 'element' | 'headings' {
198
198
  return options.cards ?? (options.iframes === 'element' ? 'element' : 'headings')
199
199
  }
200
200
 
201
- function isButton(node: MdastNode): boolean {
201
+ /** Whether a MyST node has the class `button`, which renders a link or span as a button. */
202
+ export function isButton(node: MdastNode): boolean {
202
203
  return typeof node.class === 'string' && node.class.split(/\s+/).includes('button')
203
204
  }
204
205
 
@@ -0,0 +1,23 @@
1
+ import { cpSync, existsSync, mkdirSync } from 'node:fs'
2
+ import { join, resolve } from 'node:path'
3
+ import type { LocalAsset } from '../ir.ts'
4
+
5
+ /** Production assets are local to one document; hidden, remote and shared paths are not admitted. */
6
+ export function productionAssetFile(deckDir: string, location: string): string | undefined {
7
+ if (!location || /^(?:[a-z][a-z\d+.-]*:|\/)/i.test(location) || /[\\\0?#]/.test(location)) return undefined
8
+ let decoded: string
9
+ try { decoded = decodeURIComponent(location) } catch { return undefined }
10
+ if (/[\\\0?#]/.test(decoded)) return undefined
11
+ const parts = decoded.split('/')
12
+ if (parts.some(part => !part || part.startsWith('.') || part === 'node_modules' || part === 'dist' || part.includes(':'))) return undefined
13
+ return resolve(deckDir, ...parts)
14
+ }
15
+
16
+ export function copyPublished(found: LocalAsset, dir: string): string {
17
+ const target = join(dir, found.name)
18
+ if (!existsSync(target)) {
19
+ mkdirSync(dir, { recursive: true })
20
+ cpSync(found.file, target, { recursive: true })
21
+ }
22
+ return target
23
+ }