@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/document.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import { DOCUMENT_JSON_SCHEMA } from './document-schema.ts'
|
|
3
|
+
import { isRecord } from './errors.ts'
|
|
4
|
+
import { captionSourceFingerprint, captionTimingProblem, stepCaptionText } from './document-caption-timings.ts'
|
|
5
|
+
import { validateDocumentParameters } from './parameters.ts'
|
|
6
|
+
import type { DocumentIssue, DocumentValidation, RichTextNode, TeachingDocument } from './contracts/document.ts'
|
|
7
|
+
|
|
8
|
+
export { DOCUMENT_JSON_SCHEMA } from './document-schema.ts'
|
|
9
|
+
export { DOCUMENT_ENTRY, DOCUMENT_SCHEMA_VERSION } from './contracts/document.ts'
|
|
10
|
+
|
|
11
|
+
const structural = new Validator(DOCUMENT_JSON_SCHEMA, '2020-12', false)
|
|
12
|
+
const escape = (key: string) => key.replaceAll('~', '~0').replaceAll('/', '~1')
|
|
13
|
+
const at = (path: string, key: string | number) => `${path}/${escape(String(key))}`
|
|
14
|
+
const own = (table: object, key: string) => Object.hasOwn(table, key)
|
|
15
|
+
|
|
16
|
+
/** The same structural and semantic checks serve agent changes, native editing and compilation. */
|
|
17
|
+
export function validateDocument(value: unknown): DocumentValidation {
|
|
18
|
+
const issues: DocumentIssue[] = []
|
|
19
|
+
const issue = (path: string, code: string, message: string, severity: DocumentIssue['severity'] = 'error') => { issues.push({ path, code, message, severity }) }
|
|
20
|
+
const active = new WeakSet<object>()
|
|
21
|
+
let nodes = 0
|
|
22
|
+
const json = (item: unknown, path: string, depth = 0): boolean => {
|
|
23
|
+
if (++nodes > 1_000_000 || depth > 128) { issue(path, 'json.limit', 'The document exceeds the supported JSON size or nesting limit.'); return false }
|
|
24
|
+
if (item === null || typeof item === 'string' || typeof item === 'boolean') return true
|
|
25
|
+
if (typeof item === 'number') { if (Number.isFinite(item)) return true; issue(path, 'json.number', 'JSON numbers must be finite.'); return false }
|
|
26
|
+
if (typeof item !== 'object') { issue(path, 'json.value', 'A document contains only JSON values.'); return false }
|
|
27
|
+
if (active.has(item)) { issue(path, 'json.cycle', 'A JSON value cannot contain an object cycle.'); return false }
|
|
28
|
+
if (!Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item))) { issue(path, 'json.object', 'A JSON object cannot have a custom object prototype.'); return false }
|
|
29
|
+
active.add(item)
|
|
30
|
+
const valid = Object.entries(item).every(([key, child]) => json(child, at(path, key), depth + 1))
|
|
31
|
+
active.delete(item)
|
|
32
|
+
return valid
|
|
33
|
+
}
|
|
34
|
+
if (!json(value, '')) return { issues }
|
|
35
|
+
let checked
|
|
36
|
+
try { checked = structural.validate(value) }
|
|
37
|
+
catch (error) { issue('', 'schema.invalid', error instanceof Error ? error.message : 'Document schema validation failed.'); return { issues } }
|
|
38
|
+
if (!checked.valid) {
|
|
39
|
+
for (const error of checked.errors.slice(0, 100)) issue(error.instanceLocation.replace(/^#/, ''), `schema.${error.keyword}`, error.error)
|
|
40
|
+
return { issues }
|
|
41
|
+
}
|
|
42
|
+
const doc = value as TeachingDocument
|
|
43
|
+
const lectureRenderer = doc.metadata.renderers?.lecture
|
|
44
|
+
if (isRecord(lectureRenderer) && own(lectureRenderer, 'script')) issue('/metadata/renderers/lecture/script', 'narration.external-source', 'External lecture scripts are not supported. Author narration in lecture.json narrations and bind it through each step narrationIds.')
|
|
45
|
+
const titleTargets = new Map(Object.keys(doc.sections).map(id => [`page:${id}`, id]))
|
|
46
|
+
const ids = new Map<string, string>()
|
|
47
|
+
const identify = (id: string, path: string) => {
|
|
48
|
+
const section = titleTargets.get(id)
|
|
49
|
+
if (section !== undefined) issue(path, 'id.reserved', `ID ${id} is reserved for the title of section ${section}.`)
|
|
50
|
+
const prior = ids.get(id)
|
|
51
|
+
if (prior !== undefined) issue(path, 'id.duplicate', `ID ${id} is already used at ${prior}.`)
|
|
52
|
+
else ids.set(id, path)
|
|
53
|
+
}
|
|
54
|
+
identify(doc.id, '/id')
|
|
55
|
+
for (const name of ['sections', 'blocks', 'steps', 'narrations', 'assets', 'definitions'] as const) {
|
|
56
|
+
for (const [key, entity] of Object.entries(doc[name])) {
|
|
57
|
+
const path = at(`/${name}`, key)
|
|
58
|
+
if (entity.id !== key) issue(`${path}/id`, 'id.key', 'An entity ID must match its map key.')
|
|
59
|
+
identify(entity.id, `${path}/id`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const reference = (table: object, id: string, path: string, kind: string): boolean => {
|
|
63
|
+
if (own(table, id)) return true
|
|
64
|
+
issue(path, 'reference.missing', `Unknown ${kind} ${id}.`)
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
for (const [index, id] of doc.sectionOrder.entries()) reference(doc.sections, id, `/sectionOrder/${index}`, 'section')
|
|
68
|
+
for (const id of Object.keys(doc.sections)) if (!doc.sectionOrder.includes(id)) issue(at('/sections', id), 'section.order', 'Every section must appear once in sectionOrder.')
|
|
69
|
+
const parents = new Map<string, string>()
|
|
70
|
+
const sectionOf = new Map<string, string>()
|
|
71
|
+
const visiting = new Set<string>()
|
|
72
|
+
const visit = (id: string, path: string, section: string) => {
|
|
73
|
+
if (!reference(doc.blocks, id, path, 'block')) return
|
|
74
|
+
if (visiting.has(id)) { issue(path, 'block.cycle', 'Block ownership cannot contain a cycle.'); return }
|
|
75
|
+
if (parents.has(id)) { issue(path, 'block.parent', `Block ${id} already belongs to ${parents.get(id)}.`); return }
|
|
76
|
+
parents.set(id, path); sectionOf.set(id, section); visiting.add(id)
|
|
77
|
+
const block = doc.blocks[id]!
|
|
78
|
+
if (block.type === 'container' || block.type === 'component') for (const [index, child] of (block.children ?? []).entries()) visit(child, at(at('/blocks', id), `children`) + `/${index}`, section)
|
|
79
|
+
visiting.delete(id)
|
|
80
|
+
}
|
|
81
|
+
for (const [id, section] of Object.entries(doc.sections)) for (const [index, child] of section.children.entries()) visit(child, at('/sections', id) + `/children/${index}`, id)
|
|
82
|
+
for (const id of Object.keys(doc.blocks)) if (!parents.has(id)) issue(at('/blocks', id), 'block.orphan', 'Every content block must have one section or container parent.')
|
|
83
|
+
const richIds = new Map<string, Set<string>>()
|
|
84
|
+
const rich = (node: RichTextNode | { id: string, type: string, children?: unknown[] }, path: string, collected: Set<string>) => {
|
|
85
|
+
identify(node.id, `${path}/id`); collected.add(node.id)
|
|
86
|
+
if (node.type === 'table') {
|
|
87
|
+
const table = node as Extract<RichTextNode, { type: 'table' }>
|
|
88
|
+
if ((table.headerRows ?? 0) > table.children.length) issue(`${path}/headerRows`, 'table.header', 'Header rows cannot exceed table rows.')
|
|
89
|
+
let spans: { from: number, to: number, until: number }[] = []
|
|
90
|
+
const widths = table.children.map((row, rowIndex) => {
|
|
91
|
+
spans = spans.filter(span => span.until > rowIndex).sort((a, b) => a.from - b.from)
|
|
92
|
+
let column = 0, index = 0
|
|
93
|
+
const added: typeof spans = []
|
|
94
|
+
for (const [cellIndex, cell] of row.children.entries()) {
|
|
95
|
+
const cellPath = `${path}/children/${rowIndex}/children/${cellIndex}`
|
|
96
|
+
const colspan = cell.colspan ?? 1, rowspan = cell.rowspan ?? 1
|
|
97
|
+
if (!Number.isSafeInteger(colspan) || !Number.isSafeInteger(rowspan)) {
|
|
98
|
+
issue(cellPath, 'table.span', 'Cell spans must be safe positive integers.')
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
while (spans[index] && spans[index]!.from <= column) { column = Math.max(column, spans[index]!.to); index++ }
|
|
102
|
+
const end = column + colspan
|
|
103
|
+
const overlap = spans[index] && spans[index]!.from < end
|
|
104
|
+
if (overlap) issue(cellPath, 'table.overlap', 'A cell cannot overlap a cell spanning from an earlier row.')
|
|
105
|
+
if (rowIndex + rowspan > table.children.length) issue(`${cellPath}/rowspan`, 'table.span', 'A row span cannot extend beyond the table rows.')
|
|
106
|
+
if (!Number.isSafeInteger(end) || table.columnWidths && end > table.columnWidths.length) issue(`${cellPath}/colspan`, 'table.span', 'A cell span cannot extend beyond the declared table columns.')
|
|
107
|
+
if (rowspan > 1 && !overlap) added.push({ from: column, to: end, until: rowIndex + rowspan })
|
|
108
|
+
column = end
|
|
109
|
+
}
|
|
110
|
+
const width = spans.reduce((width, span) => Math.max(width, span.to), column)
|
|
111
|
+
spans.push(...added)
|
|
112
|
+
return width
|
|
113
|
+
})
|
|
114
|
+
if (table.headerColumns !== undefined && widths.some(width => table.headerColumns! > width)) issue(`${path}/headerColumns`, 'table.header', 'Header columns cannot exceed table columns.')
|
|
115
|
+
if (table.columnWidths && widths.some(width => width !== table.columnWidths!.length)) issue(`${path}/columnWidths`, 'table.widths', 'Column widths must match the logical table columns.')
|
|
116
|
+
}
|
|
117
|
+
if ('children' in node && Array.isArray(node.children)) for (const [index, child] of node.children.entries()) if (isRecord(child) && typeof child.id === 'string') rich(child as unknown as RichTextNode, `${path}/children/${index}`, collected)
|
|
118
|
+
if (node.type === 'extension' && 'fallback' in node && Array.isArray(node.fallback)) node.fallback.forEach((child, index) => rich(child as RichTextNode, `${path}/fallback/${index}`, collected))
|
|
119
|
+
}
|
|
120
|
+
const schema = (value: JsonSchemaLike, parameters: unknown, path: string) => {
|
|
121
|
+
const result = validateDocumentParameters(value, parameters)
|
|
122
|
+
for (const error of result.issues) issue(path + error.path, result.available ? 'parameters.schema' : 'parameters.schema-unavailable', error.message)
|
|
123
|
+
}
|
|
124
|
+
for (const [id, block] of Object.entries(doc.blocks)) {
|
|
125
|
+
const path = at('/blocks', id)
|
|
126
|
+
if (block.type === 'text') {
|
|
127
|
+
const collected = new Set<string>(); richIds.set(id, collected)
|
|
128
|
+
block.content.forEach((node, index) => rich(node, `${path}/content/${index}`, collected))
|
|
129
|
+
for (const [index, item] of (block.reveal?.order ?? []).entries()) if (!collected.has(item)) issue(`${path}/reveal/order/${index}`, 'reveal.target', 'Reveal items must name rich-text nodes of this block.')
|
|
130
|
+
} else if (block.type === 'image') {
|
|
131
|
+
if (block.assetId !== undefined && reference(doc.assets, block.assetId, `${path}/assetId`, 'asset') && doc.assets[block.assetId]!.kind !== 'image') issue(`${path}/assetId`, 'asset.kind', 'An image block requires an image asset.')
|
|
132
|
+
} else if (block.type === 'component') {
|
|
133
|
+
if (reference(doc.definitions, block.definitionId, `${path}/definitionId`, 'definition')) schema(doc.definitions[block.definitionId]!.parameterSchema, block.parameters, `${path}/parameters`)
|
|
134
|
+
} else if (block.type === 'code') {
|
|
135
|
+
if (block.highlight && block.origin?.highlightRegions) issue(`${path}/highlight`, 'code.highlight-owner', 'Choose authored line numbers or tracked source regions, not both.')
|
|
136
|
+
for (const [field, values] of [['highlight', block.highlight], ['origin/highlightRegions', block.origin?.highlightRegions]] as const) for (const step of Object.keys(values ?? {})) {
|
|
137
|
+
if (reference(doc.steps, step, `${path}/${field}/${escape(step)}`, 'step') && doc.steps[step]!.sectionId !== sectionOf.get(id)) issue(`${path}/${field}/${escape(step)}`, 'code.highlight-section', 'A code highlight must belong to a step of its own section.')
|
|
138
|
+
}
|
|
139
|
+
for (const [index, variant] of (block.variants ?? []).entries()) identify(variant.id, `${path}/variants/${index}/id`)
|
|
140
|
+
} else if (block.type === 'container') {
|
|
141
|
+
if (block.properties.columnWidths && block.properties.columnWidths.length !== block.children.length) issue(`${path}/properties/columnWidths`, 'container.columns', 'Column widths must match the child count.')
|
|
142
|
+
if (block.layout === 'tabs' && block.properties.tabTitles && block.properties.tabTitles.length !== block.children.length) issue(`${path}/properties/tabTitles`, 'container.tabs', 'Tab titles must match the child count.')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const [id, definition] of Object.entries(doc.definitions)) {
|
|
146
|
+
const states = new Set<string>()
|
|
147
|
+
for (const [index, state] of definition.states.entries()) {
|
|
148
|
+
if (states.has(state.id)) issue(at('/definitions', id) + `/states/${index}/id`, 'state.duplicate', 'Definition state IDs must be unique within the definition.')
|
|
149
|
+
states.add(state.id)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const effectiveVisuals = (id: string, visiting = new Set<string>()): Map<string, TeachingDocument['steps'][string]['visuals'][number]> => {
|
|
153
|
+
if (visiting.has(id) || !own(doc.steps, id)) return new Map()
|
|
154
|
+
visiting.add(id)
|
|
155
|
+
const step = doc.steps[id]!
|
|
156
|
+
const result = step.inherits ? effectiveVisuals(step.inherits, visiting) : new Map<string, TeachingDocument['steps'][string]['visuals'][number]>()
|
|
157
|
+
for (const visual of step.visuals) result.set(visual.blockId, { ...result.get(visual.blockId), ...visual, parameters: { ...result.get(visual.blockId)?.parameters, ...visual.parameters } })
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
160
|
+
const orderedSteps = new Set<string>()
|
|
161
|
+
for (const [id, section] of Object.entries(doc.sections)) for (const [index, stepId] of section.stepOrder.entries()) {
|
|
162
|
+
const path = at('/sections', id) + `/stepOrder/${index}`
|
|
163
|
+
if (orderedSteps.has(stepId)) issue(path, 'step.owner', 'A step belongs to one section order.')
|
|
164
|
+
orderedSteps.add(stepId)
|
|
165
|
+
if (reference(doc.steps, stepId, path, 'step') && doc.steps[stepId]!.sectionId !== id) issue(path, 'step.section', 'A step must belong to the section that orders it.')
|
|
166
|
+
}
|
|
167
|
+
for (const [id, step] of Object.entries(doc.steps)) {
|
|
168
|
+
const path = at('/steps', id)
|
|
169
|
+
if (!orderedSteps.has(id)) issue(path, 'step.orphan', 'Every step must appear in its section stepOrder.')
|
|
170
|
+
reference(doc.sections, step.sectionId, `${path}/sectionId`, 'section')
|
|
171
|
+
const visuals = new Set<string>()
|
|
172
|
+
for (const [index, visual] of step.visuals.entries()) {
|
|
173
|
+
const location = `${path}/visuals/${index}`
|
|
174
|
+
if (visuals.has(visual.blockId)) issue(`${location}/blockId`, 'visual.duplicate', 'A step has one override per block.')
|
|
175
|
+
visuals.add(visual.blockId)
|
|
176
|
+
if (!reference(doc.blocks, visual.blockId, `${location}/blockId`, 'block')) continue
|
|
177
|
+
if (sectionOf.get(visual.blockId) !== step.sectionId) issue(`${location}/blockId`, 'visual.section', 'Visual state must target a block owned by the same section.')
|
|
178
|
+
const actual = effectiveVisuals(id).get(visual.blockId)!
|
|
179
|
+
const block = doc.blocks[visual.blockId]!
|
|
180
|
+
if (block.type === 'component') {
|
|
181
|
+
const definition = doc.definitions[block.definitionId]
|
|
182
|
+
if (definition) {
|
|
183
|
+
if (actual.parameters) schema(definition.parameterSchema, { ...block.parameters, ...actual.parameters }, `${location}/parameters`)
|
|
184
|
+
const state = actual.state === undefined ? undefined : definition.states.find(state => state.id === actual.state)
|
|
185
|
+
if (actual.state !== undefined && !state) issue(`${location}/state`, 'state.missing', 'The component definition does not declare this state.')
|
|
186
|
+
if (state?.parameterSchema) schema(state.parameterSchema, { ...block.parameters, ...actual.parameters }, `${location}/parameters`)
|
|
187
|
+
}
|
|
188
|
+
} else if (visual.state !== undefined) issue(`${location}/state`, 'state.unsupported', 'Named animation states require a component definition.')
|
|
189
|
+
}
|
|
190
|
+
for (const [index, narration] of step.narrationIds.entries()) reference(doc.narrations, narration, `${path}/narrationIds/${index}`, 'narration')
|
|
191
|
+
const seen = new Set<string>([id]); let inherited = step.inherits
|
|
192
|
+
while (inherited !== undefined) {
|
|
193
|
+
if (seen.has(inherited)) { issue(`${path}/inherits`, 'step.cycle', 'Step inheritance cannot contain a cycle.'); break }
|
|
194
|
+
seen.add(inherited)
|
|
195
|
+
if (!reference(doc.steps, inherited, `${path}/inherits`, 'step')) break
|
|
196
|
+
if (doc.steps[inherited]!.sectionId !== step.sectionId) { issue(`${path}/inherits`, 'step.section', 'Inherited steps must belong to the same section.'); break }
|
|
197
|
+
inherited = doc.steps[inherited]!.inherits
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const cues = new Set<string>()
|
|
201
|
+
for (const [id, narration] of Object.entries(doc.narrations)) if (narration.provenance) {
|
|
202
|
+
const path = at('/narrations', id) + '/provenance/assetId'
|
|
203
|
+
if (reference(doc.assets, narration.provenance.assetId, path, 'asset') && doc.assets[narration.provenance.assetId]!.kind !== 'audio') issue(path, 'asset.kind', 'Transcription provenance requires the original audio asset.')
|
|
204
|
+
}
|
|
205
|
+
for (const [id, narration] of Object.entries(doc.narrations)) for (const [index, node] of narration.content.entries()) if (node.type === 'cue') {
|
|
206
|
+
const path = at('/narrations', id) + `/content/${index}`
|
|
207
|
+
identify(node.id, `${path}/id`); cues.add(node.id)
|
|
208
|
+
if (node.target) reference(doc.blocks, node.target.blockId, `${path}/target/blockId`, 'block')
|
|
209
|
+
if (node.parameters && ['pauseMs', 'durationMs', 'delayMs'].some(key => own(node.parameters!, key))) issue(`${path}/parameters`, 'production.owner', 'Cue timing belongs to production.cueTimings.')
|
|
210
|
+
}
|
|
211
|
+
const voice = (production: TeachingDocument['production']['defaults'], path: string) => {
|
|
212
|
+
const source = production.voiceSource
|
|
213
|
+
if (source?.type === 'recording' && reference(doc.assets, source.assetId, `${path}/voiceSource/assetId`, 'asset') && !['audio', 'video'].includes(doc.assets[source.assetId]!.kind)) issue(`${path}/voiceSource/assetId`, 'asset.kind', 'Recorded speech requires audio or video.')
|
|
214
|
+
if (source?.type === 'recording' && source.sourceRange && source.sourceRange.endMs <= source.sourceRange.startMs) issue(`${path}/voiceSource/sourceRange`, 'media.range', 'A recording range must end after it starts.')
|
|
215
|
+
}
|
|
216
|
+
voice(doc.production.defaults, '/production/defaults')
|
|
217
|
+
for (const [id, override] of Object.entries(doc.production.sectionOverrides ?? {})) { reference(doc.sections, id, at('/production/sectionOverrides', id), 'section'); voice(override, at('/production/sectionOverrides', id)) }
|
|
218
|
+
for (const [id, override] of Object.entries(doc.production.stepOverrides)) { reference(doc.steps, id, at('/production/stepOverrides', id), 'step'); voice(override, at('/production/stepOverrides', id)) }
|
|
219
|
+
for (const id of Object.keys(doc.production.narrationOverrides ?? {})) reference(doc.narrations, id, at('/production/narrationOverrides', id), 'narration')
|
|
220
|
+
for (const id of Object.keys(doc.production.cueTimings ?? {})) if (!cues.has(id)) issue(at('/production/cueTimings', id), 'cue.missing', 'Cue timing must reference an authored narration cue.')
|
|
221
|
+
for (const [stepId, timing] of Object.entries(doc.production.captionTimings ?? {})) {
|
|
222
|
+
const step = doc.steps[stepId]
|
|
223
|
+
const stale = !step || JSON.stringify(step.narrationIds) !== JSON.stringify(timing.narrationIds) || captionSourceFingerprint(doc, stepId) !== timing.sourceFingerprint
|
|
224
|
+
const problem = stale ? 'caption-source-changed' : captionTimingProblem(timing, stepCaptionText(doc, stepId))
|
|
225
|
+
if (problem) issue(at('/production/captionTimings', stepId), 'caption.timing-stale', 'Manual caption timing requires review: ' + problem, 'warning')
|
|
226
|
+
}
|
|
227
|
+
for (const [index, clip] of doc.production.mediaClips.entries()) {
|
|
228
|
+
const path = `/production/mediaClips/${index}`
|
|
229
|
+
identify(clip.id, `${path}/id`)
|
|
230
|
+
if (reference(doc.assets, clip.assetId, `${path}/assetId`, 'asset') && !['audio', 'video'].includes(doc.assets[clip.assetId]!.kind)) issue(`${path}/assetId`, 'asset.kind', 'A media clip requires audio or video.')
|
|
231
|
+
reference(doc.steps, clip.attachment.stepId, `${path}/attachment/stepId`, 'step')
|
|
232
|
+
if (clip.sourceRange.endMs <= clip.sourceRange.startMs) issue(`${path}/sourceRange`, 'media.range', 'A media range must end after it starts.')
|
|
233
|
+
}
|
|
234
|
+
const lecture = doc.metadata.renderers?.lecture
|
|
235
|
+
if (lecture && ['voice', 'gap', 'script'].some(key => own(lecture, key))) issue('/metadata/renderers/lecture', 'production.owner', 'Voice, narration sources and playback timing belong only to production and narration entities.')
|
|
236
|
+
const textTargets = new Set([...richIds.values()].flatMap(values => [...values]))
|
|
237
|
+
for (const id of doc.sectionOrder) textTargets.add(`page:${id}`)
|
|
238
|
+
for (const [id, block] of Object.entries(doc.blocks)) if (block.type === 'code' && block.value !== undefined) textTargets.add(id)
|
|
239
|
+
for (const [id, thread] of Object.entries(doc.comments ?? {})) {
|
|
240
|
+
const path = at('/comments', id)
|
|
241
|
+
if (id !== thread.id) issue(`${path}/id`, 'id.key', 'A thread ID must match its map key.')
|
|
242
|
+
identify(thread.id, `${path}/id`)
|
|
243
|
+
for (const edge of ['from', 'to'] as const) if (thread.anchor && !textTargets.has(thread.anchor[edge].blockId)) issue(`${path}/anchor/${edge}/blockId`, 'comment.target', 'A comment anchor must name a stable rich-text node or section title.')
|
|
244
|
+
for (const [index, comment] of thread.comments.entries()) {
|
|
245
|
+
identify(comment.id, `${path}/comments/${index}/id`)
|
|
246
|
+
comment.content?.forEach((node, child) => rich(node, `${path}/comments/${index}/content/${child}`, new Set()))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return issues.some(issue => issue.severity === 'error') ? { issues } : { document: doc, issues }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
type JsonSchemaLike = TeachingDocument['definitions'][string]['parameterSchema']
|
package/src/durations.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** A duration as authors write one (`{cue}` `pause`, `:durations:`): `1.2` and `1.2s` are seconds, `800ms` milliseconds; in milliseconds. */
|
|
2
|
+
export function parseDuration(value: string): number | undefined {
|
|
3
|
+
const match = /^(\d+(?:\.\d+)?)\s*(ms|s)?$/.exec(value.trim())
|
|
4
|
+
if (!match) return undefined
|
|
5
|
+
const n = Number(match[1])
|
|
6
|
+
return Math.round(match[2] === 'ms' ? n : n * 1000)
|
|
7
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function errorMessage(error: unknown): string {
|
|
2
|
+
return error instanceof Error ? error.message : String(error)
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
6
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** What a preview needs (the viewer app, the document's code) is still being built: ask again later; not a failure. */
|
|
10
|
+
export class PreviewPreparing extends Error {
|
|
11
|
+
constructor(message = 'preview-preparing') {
|
|
12
|
+
super(message)
|
|
13
|
+
this.name = 'PreviewPreparing'
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DomainEvent, EventBus } from './contracts.ts'
|
|
2
|
+
|
|
3
|
+
type Handler = (event: DomainEvent) => void
|
|
4
|
+
type ErrorHandler = (error: unknown, event: DomainEvent) => void
|
|
5
|
+
|
|
6
|
+
const reportToConsole: ErrorHandler = (error, event) => {
|
|
7
|
+
console.error(`[presmium] handler for "${event.type}" threw`, error)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createEventBus(onError: ErrorHandler = reportToConsole): EventBus {
|
|
11
|
+
const handlers = new Map<string, Set<Handler>>()
|
|
12
|
+
return {
|
|
13
|
+
emit(event) {
|
|
14
|
+
for (const handler of [...(handlers.get(event.type) ?? [])]) {
|
|
15
|
+
try {
|
|
16
|
+
handler(event)
|
|
17
|
+
} catch (error) {
|
|
18
|
+
onError(error, event)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
on(type, handler) {
|
|
23
|
+
const set = handlers.get(type) ?? new Set()
|
|
24
|
+
handlers.set(type, set)
|
|
25
|
+
const h = handler as Handler
|
|
26
|
+
set.add(h)
|
|
27
|
+
return () => {
|
|
28
|
+
set.delete(h)
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/execution.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RunResult } from './contracts/execution.ts'
|
|
2
|
+
|
|
3
|
+
const ALIASES: Record<string, string> = {
|
|
4
|
+
py: 'python',
|
|
5
|
+
python3: 'python',
|
|
6
|
+
js: 'javascript',
|
|
7
|
+
mjs: 'javascript',
|
|
8
|
+
ts: 'typescript',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function languageId(lang: string): string {
|
|
12
|
+
const id = lang.trim().toLowerCase()
|
|
13
|
+
if (id.startsWith('ipython')) return 'python'
|
|
14
|
+
return ALIASES[id] ?? id
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function failedRun(executorId: string, stderr: string, durationMs = 0): RunResult {
|
|
18
|
+
return { ok: false, stdout: '', stderr, exitCode: null, durationMs, timedOut: false, source: 'live', executorId }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const CREDENTIAL_VARIABLE = /_(?:KEY|TOKEN|SECRET|PASSWORD|APPID)$/i
|
|
22
|
+
|
|
23
|
+
export function environmentExecutor(name: string): string {
|
|
24
|
+
return `presmium.env.${name}`
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function normalizeWhitespace(text: string): string {
|
|
2
|
+
const lines = text
|
|
3
|
+
.replace(/\r\n?/g, '\n')
|
|
4
|
+
.split('\n')
|
|
5
|
+
.map(line => line.replace(/\s+$/u, '').replace(/^[ \t]+/, lead => ' '.repeat(lead.replace(/\t/g, ' ').length)))
|
|
6
|
+
.filter(line => line.length > 0)
|
|
7
|
+
if (lines.length === 0) return ''
|
|
8
|
+
const indent = Math.min(...lines.map(line => line.length - line.trimStart().length))
|
|
9
|
+
return lines
|
|
10
|
+
.map((line) => {
|
|
11
|
+
const depth = line.length - line.trimStart().length - indent
|
|
12
|
+
const body = line.trimStart().replace(/\s+/gu, ' ').replace(/ ?([^\p{L}\p{N}_ ]) ?/gu, '$1')
|
|
13
|
+
return ' '.repeat(depth) + body
|
|
14
|
+
})
|
|
15
|
+
.join('\n')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const FNV_OFFSET = 0xcbf29ce484222325n
|
|
19
|
+
const FNV_PRIME = 0x100000001b3n
|
|
20
|
+
|
|
21
|
+
export function fnv1a64(text: string): string {
|
|
22
|
+
let hash = FNV_OFFSET
|
|
23
|
+
for (const byte of new TextEncoder().encode(text)) {
|
|
24
|
+
hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * FNV_PRIME)
|
|
25
|
+
}
|
|
26
|
+
return hash.toString(16).padStart(16, '0')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function fingerprint(text: string): string {
|
|
30
|
+
return fnv1a64(normalizeWhitespace(text))
|
|
31
|
+
}
|
package/src/follow.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { IRSection } from './ir.ts'
|
|
2
|
+
import { stepTrail, trailStep } from './step-keys.ts'
|
|
3
|
+
import type { StepKeys } from './step-keys.ts'
|
|
4
|
+
|
|
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
|
|
10
|
+
}
|
|
11
|
+
|
|
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
|
|
21
|
+
}
|
|
22
|
+
|
|
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 }
|
|
30
|
+
}
|
|
31
|
+
const before = back === undefined ? undefined : keysOf(back)
|
|
32
|
+
return before && { section: back!, step: before.length - 1 }
|
|
33
|
+
}
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const GIT_SAFE_CONFIG: readonly string[] = ['core.hooksPath=/dev/null', 'core.fsmonitor=false', 'core.quotePath=false', 'commit.gpgSign=false', 'tag.gpgSign=false']
|
|
2
|
+
|
|
3
|
+
export function gitConfigArgs(): string[] {
|
|
4
|
+
return GIT_SAFE_CONFIG.flatMap(entry => ['-c', entry])
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function gitEnvironment(env: Record<string, string | undefined>): Record<string, string> {
|
|
8
|
+
const own: Record<string, string> = {}
|
|
9
|
+
for (const [name, value] of Object.entries(env)) if (value !== undefined && !/^GIT_/i.test(name)) own[name] = value
|
|
10
|
+
own.GIT_TERMINAL_PROMPT = '0'
|
|
11
|
+
return own
|
|
12
|
+
}
|
package/src/html.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseFragment } from 'parse5'
|
|
2
|
+
import type { DefaultTreeAdapterMap } from 'parse5'
|
|
3
|
+
|
|
4
|
+
export function escapeHtml(text: string): string {
|
|
5
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface HtmlAttribute {
|
|
9
|
+
name: string
|
|
10
|
+
value: string
|
|
11
|
+
start: number
|
|
12
|
+
end: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const URL_ATTRIBUTES: ReadonlySet<string> = new Set(['src', 'poster', 'href'])
|
|
16
|
+
|
|
17
|
+
export function htmlAttributes(html: string, names: ReadonlySet<string>): HtmlAttribute[] {
|
|
18
|
+
const found: HtmlAttribute[] = []
|
|
19
|
+
const visit = (node: DefaultTreeAdapterMap['node']) => {
|
|
20
|
+
if ('attrs' in node) {
|
|
21
|
+
for (const { name, value } of node.attrs) {
|
|
22
|
+
const at = node.sourceCodeLocation?.attrs?.[name]
|
|
23
|
+
if (names.has(name) && at) found.push({ name, value, start: at.startOffset, end: at.endOffset })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if ('childNodes' in node) node.childNodes.forEach(visit)
|
|
27
|
+
if ('content' in node) visit(node.content)
|
|
28
|
+
}
|
|
29
|
+
visit(parseFragment(html, { sourceCodeLocationInfo: true }))
|
|
30
|
+
return found.sort((a, b) => a.start - b.start)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function mapHtmlUrls(html: string, map: (url: string, attribute: string) => string | undefined): string {
|
|
34
|
+
let out = html
|
|
35
|
+
for (const { name, value, start, end } of htmlAttributes(html, URL_ATTRIBUTES).reverse()) {
|
|
36
|
+
const mapped = map(value, name)
|
|
37
|
+
if (mapped === undefined) continue
|
|
38
|
+
out = `${out.slice(0, start)}${html.slice(start, start + name.length)}="${escapeHtml(mapped)}"${out.slice(end)}`
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
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'
|
|
15
|
+
export * from './ir.ts'
|
|
16
|
+
export { createAnchorResolverRegistry, isRenderable, parseAnchor } from './anchors.ts'
|
|
17
|
+
export type { ParsedAnchor } from './anchors.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'
|
|
23
|
+
export { CheckRegistry } from './checks.ts'
|
|
24
|
+
export { errorMessage, isRecord, PreviewPreparing } from './errors.ts'
|
|
25
|
+
export { createEventBus } from './events.ts'
|
|
26
|
+
export { CREDENTIAL_VARIABLE, environmentExecutor, failedRun, languageId } from './execution.ts'
|
|
27
|
+
export { gitConfigArgs, gitEnvironment } from './git.ts'
|
|
28
|
+
export { escapeHtml, htmlAttributes, mapHtmlUrls, URL_ATTRIBUTES } from './html.ts'
|
|
29
|
+
export type { HtmlAttribute } from './html.ts'
|
|
30
|
+
export { hostName, localRequestRefusal } from './requests.ts'
|
|
31
|
+
export type { RequestHeaders } from './requests.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'
|
|
37
|
+
export { Kernel } from './kernel.ts'
|
|
38
|
+
export type { KernelOptions } from './kernel.ts'
|
|
39
|
+
export { deckLocale, localeOf } from './locale.ts'
|
|
40
|
+
export { ANCHOR_LOCK_PATH, AnchorLock } from './lock.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'
|
|
51
|
+
export { publishedUrl } from './published.ts'
|
|
52
|
+
export type { PublishedUrlOptions } from './published.ts'
|
|
53
|
+
export { blockState, blockText, blockVisible, blockStepState, chapters, isChapterTitle, mdastText, narrationAt, sectionNarration, tokenize } from './reading.ts'
|
|
54
|
+
export type { Chapter } from './reading.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'
|
|
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'
|