@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/lock.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { isRenderable } from './anchors.ts'
|
|
2
|
+
import type { AnchorLockFile, RecordedAnchor, Resolution } from './contracts.ts'
|
|
3
|
+
import { errorMessage, isRecord } from './errors.ts'
|
|
4
|
+
|
|
5
|
+
export const ANCHOR_LOCK_PATH = '.presmium/anchors.lock.json'
|
|
6
|
+
|
|
7
|
+
export class AnchorLock {
|
|
8
|
+
#anchors = new Map<string, RecordedAnchor>()
|
|
9
|
+
|
|
10
|
+
private constructor(anchors: Iterable<[string, RecordedAnchor]>) {
|
|
11
|
+
for (const [anchor, recorded] of anchors) this.set(anchor, recorded)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static empty(): AnchorLock {
|
|
15
|
+
return new AnchorLock([])
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
static from(file: AnchorLockFile): AnchorLock {
|
|
19
|
+
return new AnchorLock(Object.entries(file.anchors))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static parse(text: string | undefined): AnchorLock {
|
|
23
|
+
if (text === undefined || text.trim() === '') return AnchorLock.empty()
|
|
24
|
+
let data: unknown
|
|
25
|
+
try {
|
|
26
|
+
data = JSON.parse(text)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(`invalid anchor lock: ${errorMessage(error)}`)
|
|
29
|
+
}
|
|
30
|
+
return AnchorLock.from(validate(data))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get size(): number {
|
|
34
|
+
return this.#anchors.size
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(anchor: string): RecordedAnchor | undefined {
|
|
38
|
+
return this.#anchors.get(anchor)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
set(anchor: string, recorded: RecordedAnchor): void {
|
|
42
|
+
this.#anchors.set(anchor, { fingerprint: recorded.fingerprint, file: recorded.file, startLine: recorded.startLine })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
record(anchor: string, resolution: Resolution): boolean {
|
|
46
|
+
const { location, fingerprint } = resolution
|
|
47
|
+
if (!isRenderable(resolution) || !location || !fingerprint) return false
|
|
48
|
+
this.set(anchor, { fingerprint, file: location.file, startLine: location.startLine })
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
retain(anchors: Iterable<string>): void {
|
|
53
|
+
const keep = new Set(anchors)
|
|
54
|
+
for (const anchor of [...this.#anchors.keys()]) {
|
|
55
|
+
if (!keep.has(anchor)) this.#anchors.delete(anchor)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toJSON(): AnchorLockFile {
|
|
60
|
+
const keys = [...this.#anchors.keys()].sort()
|
|
61
|
+
return { version: 1, anchors: Object.fromEntries(keys.map(k => [k, this.#anchors.get(k)!])) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
serialize(): string {
|
|
65
|
+
return `${JSON.stringify(this.toJSON(), null, 2)}\n`
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validate(data: unknown): AnchorLockFile {
|
|
70
|
+
if (!isRecord(data) || data.version !== 1 || !isRecord(data.anchors)) {
|
|
71
|
+
throw new Error('invalid anchor lock: expected { "version": 1, "anchors": { ... } }')
|
|
72
|
+
}
|
|
73
|
+
for (const [anchor, record] of Object.entries(data.anchors)) {
|
|
74
|
+
const valid = isRecord(record)
|
|
75
|
+
&& typeof record.fingerprint === 'string' && record.fingerprint !== ''
|
|
76
|
+
&& typeof record.file === 'string'
|
|
77
|
+
&& Number.isInteger(record.startLine) && (record.startLine as number) >= 1
|
|
78
|
+
if (!valid) throw new Error(`invalid anchor lock: bad record for "${anchor}"`)
|
|
79
|
+
}
|
|
80
|
+
return data as unknown as AnchorLockFile
|
|
81
|
+
}
|
package/src/logging.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Logger } from './contracts.ts'
|
|
2
|
+
|
|
3
|
+
export function prefixedLogger(log: Logger, id: string): Logger {
|
|
4
|
+
const tag = `[${id}]`
|
|
5
|
+
return {
|
|
6
|
+
debug: (...args) => log.debug(tag, ...args),
|
|
7
|
+
info: (...args) => log.info(tag, ...args),
|
|
8
|
+
warn: (...args) => log.warn(tag, ...args),
|
|
9
|
+
error: (...args) => log.error(tag, ...args),
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { escapeHtml as escape, mapHtmlUrls } from './html.ts'
|
|
2
|
+
import type { MdastNode } from './ir.ts'
|
|
3
|
+
|
|
4
|
+
export type LinkedType = 'image' | 'link' | 'definition'
|
|
5
|
+
|
|
6
|
+
export interface LowerOptions {
|
|
7
|
+
links?: (label: string) => string | undefined
|
|
8
|
+
urls?: (url: string, type: LinkedType) => string | undefined
|
|
9
|
+
htmlUrls?: boolean
|
|
10
|
+
footnotes?: 'gfm' | 'html'
|
|
11
|
+
iframes?: 'element' | 'link'
|
|
12
|
+
roles?: 'children' | 'marks'
|
|
13
|
+
cards?: 'element' | 'headings'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LoweredMarkdown {
|
|
17
|
+
mdast: MdastNode
|
|
18
|
+
degraded: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PHRASING = new Set([
|
|
22
|
+
'text', 'emphasis', 'strong', 'delete', 'inlineCode', 'inlineMath', 'break', 'link', 'linkReference', 'image',
|
|
23
|
+
'imageReference', 'footnoteReference', 'abbreviation', 'subscript', 'superscript', 'crossReference', 'cite', 'citeGroup',
|
|
24
|
+
'underline', 'keyboard', 'smallcaps', 'presmiumRole',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
const STANDARD: Record<string, string[]> = {
|
|
28
|
+
root: [], paragraph: [], blockquote: [], thematicBreak: [], break: [], text: ['value'], html: ['value'],
|
|
29
|
+
heading: ['depth'], list: ['ordered', 'start', 'spread'], listItem: ['spread', 'checked'],
|
|
30
|
+
code: ['lang', 'value'], inlineCode: ['value'], emphasis: [], strong: [],
|
|
31
|
+
link: ['url', 'title'], image: ['url', 'alt', 'title'], definition: ['identifier', 'label', 'url', 'title'],
|
|
32
|
+
linkReference: ['identifier', 'label', 'referenceType'], imageReference: ['identifier', 'label', 'referenceType', 'alt'],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FOOTNOTES: Record<string, string[]> = { footnoteReference: ['identifier'], footnoteDefinition: ['identifier'] }
|
|
36
|
+
|
|
37
|
+
const LINKED = new Set<string>(['image', 'link', 'definition'] satisfies LinkedType[])
|
|
38
|
+
|
|
39
|
+
const TAGS: Record<string, string> = { subscript: 'sub', superscript: 'sup', delete: 'del', underline: 'u', keyboard: 'kbd' }
|
|
40
|
+
|
|
41
|
+
const CONTENT = new Set(['image', 'table', 'code', 'iframe', 'mermaid'])
|
|
42
|
+
|
|
43
|
+
const text = (value: string): MdastNode => ({ type: 'text', value })
|
|
44
|
+
const html = (value: string): MdastNode => ({ type: 'html', value })
|
|
45
|
+
const paragraph = (children: MdastNode[]): MdastNode => ({ type: 'paragraph', children })
|
|
46
|
+
const capitalized = (value: string) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
47
|
+
|
|
48
|
+
interface Lowering {
|
|
49
|
+
options: LowerOptions
|
|
50
|
+
degraded: Set<string>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const SMALL_HEADING = 4
|
|
54
|
+
|
|
55
|
+
export function lowerMyst(mdast: MdastNode, options: LowerOptions = {}): LoweredMarkdown {
|
|
56
|
+
const tree = JSON.parse(JSON.stringify(mdast)) as MdastNode
|
|
57
|
+
const ctx: Lowering = { options, degraded: new Set() }
|
|
58
|
+
const top = tree.type === 'root' ? paragraphs(tree.children ?? []) : [tree]
|
|
59
|
+
return { mdast: { type: 'root', children: top.flatMap(node => lower(node, ctx)) }, degraded: [...ctx.degraded] }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function plain(node: MdastNode): string {
|
|
63
|
+
return typeof node.value === 'string' ? node.value : (node.children ?? []).map(plain).join('')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function paragraphs(nodes: MdastNode[]): MdastNode[] {
|
|
67
|
+
const out: MdastNode[] = []
|
|
68
|
+
let run: MdastNode[] | undefined
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
if (!PHRASING.has(node.type)) {
|
|
71
|
+
out.push(node)
|
|
72
|
+
run = undefined
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (!run) out.push(paragraph(run = []))
|
|
76
|
+
run.push(node)
|
|
77
|
+
}
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function labelLink(labels: unknown[], children: MdastNode[], ctx: Lowering): MdastNode[] {
|
|
82
|
+
const url = labels.map(label => typeof label === 'string' && label ? ctx.options.links?.(label) : undefined).find(Boolean)
|
|
83
|
+
return url ? [{ type: 'link', url, children }] : children
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function dollars(value: string): MdastNode[] {
|
|
87
|
+
return value.split('$').flatMap((part, i) => [...i > 0 ? [html('\\$')] : [], ...part ? [text(part)] : []])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function lower(node: MdastNode, ctx: Lowering): MdastNode[] {
|
|
91
|
+
const { options, degraded } = ctx
|
|
92
|
+
const all = (nodes: MdastNode[] = []) => nodes.flatMap(child => lower(child, ctx))
|
|
93
|
+
const children = () => all(node.children)
|
|
94
|
+
if (node.type === 'link' && isButton(node) && cardsAs(options) === 'element') {
|
|
95
|
+
return [html('<span class="pm-button">'), ...lower({ ...node, class: undefined }, ctx), html('</span>')]
|
|
96
|
+
}
|
|
97
|
+
if (node.type === 'link' && String(node.url).startsWith('#')) {
|
|
98
|
+
const label = String(node.url).slice(1)
|
|
99
|
+
return labelLink([label], node.children?.length ? children() : [text(label)], ctx)
|
|
100
|
+
}
|
|
101
|
+
if (node.type === 'text' && typeof node.value === 'string' && node.value.includes('$')) return dollars(node.value)
|
|
102
|
+
const fields = STANDARD[node.type] ?? (options.footnotes === 'gfm' ? FOOTNOTES[node.type] : undefined)
|
|
103
|
+
if (fields) {
|
|
104
|
+
const own: Record<string, unknown> = Object.fromEntries(fields.filter(key => node[key] != null).map(key => [key, node[key]]))
|
|
105
|
+
if (LINKED.has(node.type) && typeof own.url === 'string') own.url = options.urls?.(own.url, node.type as LinkedType) ?? own.url
|
|
106
|
+
if (node.type === 'html' && typeof own.value === 'string') own.value = rawHtml(own.value, options)
|
|
107
|
+
return [{ type: node.type, ...own, ...node.children ? { children: children() } : {} }]
|
|
108
|
+
}
|
|
109
|
+
switch (node.type) {
|
|
110
|
+
case 'math':
|
|
111
|
+
return [html(`$$\n${node.value ?? ''}\n$$`)]
|
|
112
|
+
case 'inlineMath':
|
|
113
|
+
return [html(`$${node.value ?? ''}$`)]
|
|
114
|
+
case 'admonition':
|
|
115
|
+
case 'aside': {
|
|
116
|
+
const title = node.children?.find(child => child.type === 'admonitionTitle')
|
|
117
|
+
const heading = title ? all(title.children) : typeof node.kind === 'string' ? [text(capitalized(node.kind))] : []
|
|
118
|
+
const body = all(node.children?.filter(child => child !== title))
|
|
119
|
+
return [{ type: 'blockquote', children: [...heading.length ? [paragraph([{ type: 'strong', children: heading }])] : [], ...body] }]
|
|
120
|
+
}
|
|
121
|
+
case 'container':
|
|
122
|
+
return container(node, ctx)
|
|
123
|
+
case 'table':
|
|
124
|
+
return [table(node, ctx)]
|
|
125
|
+
case 'details': {
|
|
126
|
+
const summary = node.children?.find(child => child.type === 'summary')
|
|
127
|
+
const body = all(node.children?.filter(child => child !== summary))
|
|
128
|
+
return [html(`<details>\n<summary>${escape(summary ? plain(summary) : '')}</summary>`), ...body, html('</details>')]
|
|
129
|
+
}
|
|
130
|
+
case 'definitionList':
|
|
131
|
+
return [definitions(node, ctx)]
|
|
132
|
+
case 'footnoteDefinition': {
|
|
133
|
+
const [first, ...rest] = children()
|
|
134
|
+
const mark = html(`<sup>[${escape(footnoteNumber(node))}]</sup> `)
|
|
135
|
+
return first?.type === 'paragraph' ? [paragraph([mark, ...first.children ?? []]), ...rest] : [paragraph([mark]), ...first ? [first] : [], ...rest]
|
|
136
|
+
}
|
|
137
|
+
case 'footnoteReference':
|
|
138
|
+
return [html(`<sup>[${escape(footnoteNumber(node))}]</sup>`)]
|
|
139
|
+
case 'crossReference':
|
|
140
|
+
return labelLink([node.identifier, node.label], node.children?.length ? children() : [text(String(node.label ?? node.identifier ?? ''))], ctx)
|
|
141
|
+
case 'abbreviation':
|
|
142
|
+
return [html(`<abbr title="${escape(String(node.title ?? ''))}">`), ...children(), html('</abbr>')]
|
|
143
|
+
case 'subscript':
|
|
144
|
+
case 'superscript':
|
|
145
|
+
case 'delete':
|
|
146
|
+
case 'underline':
|
|
147
|
+
case 'keyboard':
|
|
148
|
+
return [html(`<${TAGS[node.type]}>`), ...children(), html(`</${TAGS[node.type]}>`)]
|
|
149
|
+
case 'cite':
|
|
150
|
+
return [text(`[${node.label}]`)]
|
|
151
|
+
case 'citeGroup':
|
|
152
|
+
return [text(`[${(node.children ?? []).map(child => child.label).join(', ')}]`)]
|
|
153
|
+
case 'mermaid':
|
|
154
|
+
return [{ type: 'code', lang: 'mermaid', value: node.value ?? '' }]
|
|
155
|
+
case 'raw':
|
|
156
|
+
if (node.lang === 'html') return [html(rawHtml(String(node.value ?? ''), options))]
|
|
157
|
+
break
|
|
158
|
+
case 'iframe': {
|
|
159
|
+
const src = String(node.src ?? '')
|
|
160
|
+
if (options.iframes === 'element') {
|
|
161
|
+
const attrs = (['width', 'height'] as const).filter(key => node[key] != null && node[key] !== '').map(key => ` ${key}="${escape(String(node[key]))}"`).join('')
|
|
162
|
+
return [html(`<iframe src="${escape(src)}"${attrs} loading="lazy"></iframe>`)]
|
|
163
|
+
}
|
|
164
|
+
degraded.add(node.type)
|
|
165
|
+
return [paragraph([{ type: 'link', url: src, children: [text(src)] }])]
|
|
166
|
+
}
|
|
167
|
+
case 'presmiumRole':
|
|
168
|
+
if (!node.children?.length && options.roles === 'marks') return [text(roleMark(node))]
|
|
169
|
+
return children()
|
|
170
|
+
case 'grid':
|
|
171
|
+
case 'grid-item':
|
|
172
|
+
case 'card':
|
|
173
|
+
case 'tabSet':
|
|
174
|
+
case 'tabItem':
|
|
175
|
+
return layout(node, ctx)
|
|
176
|
+
case 'comment':
|
|
177
|
+
case 'mystTarget':
|
|
178
|
+
return []
|
|
179
|
+
case 'caption':
|
|
180
|
+
case 'legend':
|
|
181
|
+
case 'glossary':
|
|
182
|
+
case 'smallcaps':
|
|
183
|
+
case 'block':
|
|
184
|
+
case 'div':
|
|
185
|
+
case 'span':
|
|
186
|
+
return children()
|
|
187
|
+
}
|
|
188
|
+
degraded.add(node.type)
|
|
189
|
+
return node.children ? children() : typeof node.value === 'string' && node.value ? [text(node.value)] : []
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function rawHtml(value: string, options: LowerOptions): string {
|
|
193
|
+
const { urls } = options
|
|
194
|
+
return options.htmlUrls && urls ? mapHtmlUrls(value, (url, attribute) => urls(url, attribute === 'href' ? 'link' : 'image')) : value
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function cardsAs(options: LowerOptions): 'element' | 'headings' {
|
|
198
|
+
return options.cards ?? (options.iframes === 'element' ? 'element' : 'headings')
|
|
199
|
+
}
|
|
200
|
+
|
|
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 {
|
|
203
|
+
return typeof node.class === 'string' && node.class.split(/\s+/).includes('button')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function layout(node: MdastNode, ctx: Lowering): MdastNode[] {
|
|
207
|
+
const all = (nodes: MdastNode[] = []) => nodes.flatMap(child => lower(child, ctx))
|
|
208
|
+
const element = cardsAs(ctx.options) === 'element'
|
|
209
|
+
const div = (className: string, content: MdastNode[], style = '') => [html(`<div class="${className}"${style}>`), ...content, html('</div>')]
|
|
210
|
+
const heading = (content: MdastNode[]): MdastNode => ({ type: 'heading', depth: SMALL_HEADING, children: content })
|
|
211
|
+
const items = node.children ?? []
|
|
212
|
+
switch (node.type) {
|
|
213
|
+
case 'grid': {
|
|
214
|
+
if (!element) {
|
|
215
|
+
ctx.degraded.add(node.type)
|
|
216
|
+
return all(items)
|
|
217
|
+
}
|
|
218
|
+
const columns = Array.isArray(node.columns) ? Number(node.columns.at(-1)) || 1 : 1
|
|
219
|
+
return div('pm-grid', all(items), ` style="--pm-grid-columns: ${columns}"`)
|
|
220
|
+
}
|
|
221
|
+
case 'grid-item':
|
|
222
|
+
return element ? div('pm-grid__item', all(items)) : all(items)
|
|
223
|
+
case 'card': {
|
|
224
|
+
const part = (type: string) => items.find(child => child.type === type)?.children ?? []
|
|
225
|
+
const title = all(part('cardTitle'))
|
|
226
|
+
const url = typeof node.url === 'string' && node.url ? node.url : undefined
|
|
227
|
+
const linked = url ? lower({ type: 'link', url, children: title.length ? title : [text(url)] }, ctx) : title
|
|
228
|
+
const body = all(items.filter(child => !['header', 'cardTitle', 'footer'].includes(child.type)))
|
|
229
|
+
if (!element) {
|
|
230
|
+
ctx.degraded.add(node.type)
|
|
231
|
+
const top = title.length ? [heading(linked)] : url ? [paragraph(linked)] : []
|
|
232
|
+
return [...all(part('header')), ...top, ...body, ...all(part('footer'))]
|
|
233
|
+
}
|
|
234
|
+
const header = part('header').length ? div('pm-card__header', all(part('header'))) : []
|
|
235
|
+
const footer = part('footer').length ? div('pm-card__footer', all(part('footer'))) : []
|
|
236
|
+
const top = title.length || url ? div('pm-card__title', [paragraph(linked)]) : []
|
|
237
|
+
return div(url ? 'pm-card pm-card--link' : 'pm-card', [...header, ...top, ...body, ...footer])
|
|
238
|
+
}
|
|
239
|
+
case 'tabSet':
|
|
240
|
+
if (!element) ctx.degraded.add(node.type)
|
|
241
|
+
return element ? div('pm-tabs', all(items)) : all(items)
|
|
242
|
+
case 'tabItem': {
|
|
243
|
+
const title = typeof node.title === 'string' ? node.title : ''
|
|
244
|
+
if (!element) return [...title ? [heading([text(title)])] : [], ...all(items)]
|
|
245
|
+
return div('pm-tabs__item', [...title ? [html(`<div class="pm-tabs__title">${escape(title)}</div>`)] : [], ...all(items)])
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return all(items)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function roleMark(node: MdastNode): string {
|
|
252
|
+
const options = typeof node.options === 'object' && node.options !== null ? node.options as Record<string, unknown> : {}
|
|
253
|
+
const value = typeof options.value === 'string' ? options.value.trim() : ''
|
|
254
|
+
return `〔${String(node.name ?? '')}${value ? ` ${value}` : ''}〕`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function footnoteNumber(node: MdastNode): string {
|
|
258
|
+
return String(node.enumerator ?? node.label ?? node.identifier)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function container(node: MdastNode, ctx: Lowering): MdastNode[] {
|
|
262
|
+
const all = (nodes: MdastNode[] = []) => nodes.flatMap(child => lower(child, ctx))
|
|
263
|
+
const items = node.children ?? []
|
|
264
|
+
const given = items.find(child => child.type === 'caption')
|
|
265
|
+
const derived = !given && node.kind !== 'quote'
|
|
266
|
+
const others = items.filter(child => !CONTENT.has(child.type))
|
|
267
|
+
const content = derived ? items.filter(child => CONTENT.has(child.type)) : items.filter(child => child.type !== 'caption' && child.type !== 'legend')
|
|
268
|
+
const caption = derived ? others.slice(0, 1) : given?.children ?? []
|
|
269
|
+
const legend = derived ? others.slice(1) : items.find(child => child.type === 'legend')?.children ?? []
|
|
270
|
+
const words = all(caption).flatMap(child => (child.type === 'paragraph' ? child.children ?? [] : [child]))
|
|
271
|
+
const title = words.length ? [paragraph([{ type: node.kind === 'table' ? 'strong' : 'emphasis', children: words }])] : []
|
|
272
|
+
const shown = all(content).map(child => (child.type === 'image' ? paragraph([child]) : child))
|
|
273
|
+
return node.kind === 'table' ? [...title, ...shown, ...all(legend)] : [...shown, ...title, ...all(legend)]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function table(node: MdastNode, ctx: Lowering): MdastNode {
|
|
277
|
+
const inline = (child: MdastNode) => PHRASING.has(child.type) || child.type === 'html'
|
|
278
|
+
const line = (nodes: MdastNode[]) => nodes.map(child => (child.type === 'break' ? html('<br>') : child))
|
|
279
|
+
const rows = (node.children ?? []).map(row => ({
|
|
280
|
+
type: 'tableRow',
|
|
281
|
+
children: (row.children ?? []).map((cell) => {
|
|
282
|
+
const parts: MdastNode[][] = []
|
|
283
|
+
let run: MdastNode[] | undefined
|
|
284
|
+
for (const child of (cell.children ?? []).flatMap(c => lower(c, ctx))) {
|
|
285
|
+
if (inline(child)) {
|
|
286
|
+
if (!run) parts.push(run = [])
|
|
287
|
+
run.push(...line([child]))
|
|
288
|
+
continue
|
|
289
|
+
}
|
|
290
|
+
run = undefined
|
|
291
|
+
parts.push(child.type === 'paragraph' ? line(child.children ?? []) : [text(plain(child))])
|
|
292
|
+
}
|
|
293
|
+
return { type: 'tableCell', children: parts.flatMap((part, i) => (i ? [html('<br>'), ...part] : part)) }
|
|
294
|
+
}),
|
|
295
|
+
}))
|
|
296
|
+
const align = Array.isArray(node.align) ? node.align : node.children?.[0]?.children?.map(cell => cell.align ?? null)
|
|
297
|
+
return { type: 'table', align, children: rows }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function definitions(node: MdastNode, ctx: Lowering): MdastNode {
|
|
301
|
+
const items: MdastNode[] = []
|
|
302
|
+
for (const child of node.children ?? []) {
|
|
303
|
+
if (child.type === 'definitionTerm') {
|
|
304
|
+
const term = (child.children ?? []).flatMap(c => lower(c, ctx))
|
|
305
|
+
items.push({ type: 'listItem', spread: true, children: [paragraph([{ type: 'strong', children: term }])] })
|
|
306
|
+
} else {
|
|
307
|
+
if (!items.length) items.push({ type: 'listItem', spread: true, children: [] })
|
|
308
|
+
items.at(-1)!.children!.push(...paragraphs(child.children ?? []).flatMap(c => lower(c, ctx)))
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { type: 'list', ordered: false, spread: true, children: items }
|
|
312
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_JSON_SCHEMA } from '../document-schema.ts'
|
|
4
|
+
import { DOCUMENT_PAGE_DUPLICATION_SCHEMA } from '../contracts/authoring.ts'
|
|
5
|
+
import { DOCUMENT_COMPONENT_IMPACT_COLLECTIONS, DOCUMENT_PARAMETER_MAX_CHANGES, DOCUMENT_PARAMETER_MAX_PATH_DEPTH } from '../contracts/authoring-actions.ts'
|
|
6
|
+
import type { DocumentAuthoringActionRequest, DocumentAuthoringActionReference, DocumentComponentImpactQuery } from '../contracts/authoring-actions.ts'
|
|
7
|
+
|
|
8
|
+
const id: Schema = { type: 'string', minLength: 1, maxLength: 200 }
|
|
9
|
+
const object = (properties: Record<string, Schema>, required = Object.keys(properties)): Schema => ({ type: 'object', additionalProperties: false, properties, required })
|
|
10
|
+
const action = (type: string, properties: Record<string, Schema>, required = Object.keys(properties)) => object({ type: { const: type }, ...properties }, ['type', ...required])
|
|
11
|
+
const content: Schema = { type: 'array', minItems: 1, maxItems: 100, items: { $ref: '#/$defs/RichTextNode' } }
|
|
12
|
+
const metadata: Schema = { $ref: '#/$defs/JsonObject' }
|
|
13
|
+
const scope: Schema = { oneOf: [object({ kind: { const: 'document' } }), object({ kind: { const: 'page' }, sectionId: id }), object({ kind: { const: 'step' }, stepId: id }), object({ kind: { const: 'steps' }, stepIds: { type: 'array', minItems: 1, maxItems: 1000, items: id } })] }
|
|
14
|
+
const voice: Schema = { oneOf: [{ type: 'null' }, { allOf: [{ $ref: '#/$defs/VoiceSource' }, { properties: { type: { enum: ['none', 'tts'] } } }] }] }
|
|
15
|
+
const parameterPath: Schema = { type: 'array', minItems: 1, maxItems: DOCUMENT_PARAMETER_MAX_PATH_DEPTH, items: { type: 'string', maxLength: 200 } }
|
|
16
|
+
const changes: Schema = { type: 'array', minItems: 1, maxItems: DOCUMENT_PARAMETER_MAX_CHANGES, items: { oneOf: [
|
|
17
|
+
object({ op: { const: 'set' }, path: parameterPath, value: { $ref: '#/$defs/JsonValue' } }),
|
|
18
|
+
object({ op: { const: 'remove' }, path: parameterPath }),
|
|
19
|
+
] } }
|
|
20
|
+
|
|
21
|
+
/** Node-only schema reuses the actual canonical rich-text definitions, not BlockNote data. */
|
|
22
|
+
export const DOCUMENT_AUTHORING_ACTION_SCHEMA: Schema = {
|
|
23
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
24
|
+
...object({ id, source: DOCUMENT_PAGE_DUPLICATION_SCHEMA.properties.source as unknown as Schema, action: { $ref: '#/$defs/AuthoringAction' } }),
|
|
25
|
+
$defs: { ...(structuredClone(DOCUMENT_JSON_SCHEMA) as Exclude<Schema, boolean>).$defs,
|
|
26
|
+
AuthoringAction: { oneOf: [
|
|
27
|
+
action('comment.create', { content, anchor: { oneOf: [{ type: 'null' }, object({ from: { $ref: '#/$defs/TextPoint' }, to: { $ref: '#/$defs/TextPoint' } })] }, metadata, commentMetadata: metadata }, ['content', 'anchor']),
|
|
28
|
+
action('comment.reply', { threadId: id, content, metadata }, ['threadId', 'content']),
|
|
29
|
+
action('comment.update', { threadId: id, commentId: id, content, metadata }, ['threadId', 'commentId', 'content']),
|
|
30
|
+
action('comment.react', { threadId: id, commentId: id, emoji: { type: 'string', minLength: 1, maxLength: 32 }, active: { type: 'boolean' } }),
|
|
31
|
+
action('comment.resolve', { threadId: id }), action('comment.reopen', { threadId: id }),
|
|
32
|
+
action('comment.delete', { threadId: id, commentId: id }, ['threadId']),
|
|
33
|
+
action('voice.set', { scope, source: voice, replaceOverrides: { type: 'boolean' } }, ['scope', 'source']),
|
|
34
|
+
action('caption.timing.set', { stepId: id, value: { $ref: '#/$defs/CaptionTiming' } }),
|
|
35
|
+
action('caption.timing.reset', { stepId: id }),
|
|
36
|
+
action('component.parameters.patch', { blockId: id, changes }),
|
|
37
|
+
action('component.step-parameters.patch', { blockId: id, stepId: id, changes }),
|
|
38
|
+
action('component.step-parameters.reset', { blockId: id, stepId: id }),
|
|
39
|
+
action('component.definition.use', { blockId: id, definitionId: id, changes: { ...changes, minItems: 0 } }, ['blockId', 'definitionId']),
|
|
40
|
+
] },
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
export const DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA: Schema = object({ planId: id, source: DOCUMENT_PAGE_DUPLICATION_SCHEMA.properties.source as unknown as Schema, digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, confirmDeletion: { const: true } }, ['planId', 'source', 'digest'])
|
|
44
|
+
export const DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA: Schema = object({ planId: id, digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, collection: { enum: [...DOCUMENT_COMPONENT_IMPACT_COLLECTIONS] }, offset: { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER } }, ['planId', 'digest', 'collection'])
|
|
45
|
+
const request = new Validator(structuredClone(DOCUMENT_AUTHORING_ACTION_SCHEMA), '2020-12', false)
|
|
46
|
+
const reference = new Validator(structuredClone(DOCUMENT_AUTHORING_ACTION_APPLY_SCHEMA), '2020-12', false)
|
|
47
|
+
const impact = new Validator(structuredClone(DOCUMENT_AUTHORING_ACTION_IMPACT_SCHEMA), '2020-12', false)
|
|
48
|
+
export function validateDocumentAuthoringAction(value: unknown): value is DocumentAuthoringActionRequest { return request.validate(value).valid }
|
|
49
|
+
export function validateDocumentAuthoringActionReference(value: unknown): value is DocumentAuthoringActionReference { return reference.validate(value).valid }
|
|
50
|
+
export function validateDocumentAuthoringActionImpact(value: unknown): value is DocumentComponentImpactQuery { return impact.validate(value).valid }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import { AUTHORING_CLIENT_ACK_SCHEMA, AUTHORING_CLIENT_CONNECTION_SCHEMA, AUTHORING_CLIENT_OPEN_SCHEMA, AUTHORING_CLIENT_QUERY_SCHEMA, AUTHORING_CLIENT_REPORT_SCHEMA, AUTHORING_CLIENT_REQUEST_QUERY_SCHEMA, AUTHORING_CLIENT_REQUEST_SCHEMA } from '../contracts/authoring-clients.ts'
|
|
3
|
+
import type { AuthoringClientAcknowledgement, AuthoringClientLease, AuthoringClientOpen, AuthoringClientReport, AuthoringClientRequest } from '../contracts/authoring-clients.ts'
|
|
4
|
+
|
|
5
|
+
const validator = (schema: typeof AUTHORING_CLIENT_REQUEST_SCHEMA) => new Validator(structuredClone(schema), '2020-12', false)
|
|
6
|
+
const request = validator(AUTHORING_CLIENT_REQUEST_SCHEMA), query = validator(AUTHORING_CLIENT_QUERY_SCHEMA), get = validator(AUTHORING_CLIENT_REQUEST_QUERY_SCHEMA)
|
|
7
|
+
const open = validator(AUTHORING_CLIENT_OPEN_SCHEMA), report = validator(AUTHORING_CLIENT_REPORT_SCHEMA), ack = validator(AUTHORING_CLIENT_ACK_SCHEMA), connection = validator(AUTHORING_CLIENT_CONNECTION_SCHEMA)
|
|
8
|
+
export const validateAuthoringClientRequest = (value: unknown): value is AuthoringClientRequest => request.validate(value).valid
|
|
9
|
+
export const validateAuthoringClientQuery = (value: unknown): value is { clientId?: string, after?: string } => query.validate(value).valid
|
|
10
|
+
export const validateAuthoringClientRequestQuery = (value: unknown): value is { clientId: string, epoch: string, requestId: string } => get.validate(value).valid
|
|
11
|
+
export const validateAuthoringClientOpen = (value: unknown): value is AuthoringClientOpen => open.validate(value).valid
|
|
12
|
+
export const validateAuthoringClientReport = (value: unknown): value is AuthoringClientReport => report.validate(value).valid
|
|
13
|
+
export const validateAuthoringClientAcknowledgement = (value: unknown): value is AuthoringClientAcknowledgement => ack.validate(value).valid
|
|
14
|
+
export const validateAuthoringClientLease = (value: unknown): value is AuthoringClientLease => connection.validate(value).valid
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_RECORDING_ADOPTION_SCHEMA, DOCUMENT_AUDIO_INSPECTION_SCHEMA, DOCUMENT_AUDIO_WORKSPACE_IMPORT_SCHEMA, DOCUMENT_WORKSPACE_AUDIO_INSPECTION_SCHEMA, DOCUMENT_MEDIA_CLIP_VALIDATION_SCHEMA, DOCUMENT_MEDIA_CLIP_CHANGE_SCHEMA, DOCUMENT_PRODUCTION_PLAN_SCHEMA } from '../contracts/document-audio.ts'
|
|
4
|
+
import type { DocumentRecordingAdoption, DocumentAudioInspect, DocumentWorkspaceAudioImport, DocumentWorkspaceAudioInspect, DocumentMediaClipValidation, DocumentMediaClipEdit, DocProductionPlanQuery } from '../contracts/document-audio.ts'
|
|
5
|
+
|
|
6
|
+
const inspectValidator = new Validator(structuredClone(DOCUMENT_AUDIO_INSPECTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const importValidator = new Validator(structuredClone(DOCUMENT_AUDIO_WORKSPACE_IMPORT_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
export const validateDocumentAudioInspect = (value: unknown): value is DocumentAudioInspect => inspectValidator.validate(value).valid
|
|
9
|
+
export const validateDocumentWorkspaceAudioImport = (value: unknown): value is DocumentWorkspaceAudioImport => importValidator.validate(value).valid
|
|
10
|
+
|
|
11
|
+
const workspaceInspectValidator = new Validator(structuredClone(DOCUMENT_WORKSPACE_AUDIO_INSPECTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
12
|
+
export const validateDocumentWorkspaceAudioInspect = (value: unknown): value is DocumentWorkspaceAudioInspect => workspaceInspectValidator.validate(value).valid
|
|
13
|
+
|
|
14
|
+
const clipValidationValidator = new Validator(structuredClone(DOCUMENT_MEDIA_CLIP_VALIDATION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
15
|
+
const clipChangeValidator = new Validator(structuredClone(DOCUMENT_MEDIA_CLIP_CHANGE_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
16
|
+
export const validateDocumentMediaClipValidation = (value: unknown): value is DocumentMediaClipValidation => clipValidationValidator.validate(value).valid
|
|
17
|
+
export const validateDocumentMediaClipEdit = (value: unknown): value is DocumentMediaClipEdit => clipChangeValidator.validate(value).valid
|
|
18
|
+
const productionPlanValidator = new Validator(structuredClone(DOCUMENT_PRODUCTION_PLAN_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
19
|
+
export const validateDocumentProductionPlanQuery = (value: unknown): value is DocProductionPlanQuery => productionPlanValidator.validate(value).valid
|
|
20
|
+
|
|
21
|
+
const recordingValidator = new Validator(structuredClone(DOCUMENT_RECORDING_ADOPTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
22
|
+
export const validateDocumentRecordingAdoption = (value: unknown): value is DocumentRecordingAdoption => recordingValidator.validate(value).valid
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_HISTORY_CHANGE_SCHEMA, DOCUMENT_HISTORY_QUERY_SCHEMA, DOCUMENT_HISTORY_READ_SCHEMA } from '../contracts/document-history.ts'
|
|
4
|
+
import type { DocumentHistoryChange, DocumentHistoryQuery, DocumentHistoryRead } from '../contracts/document-history.ts'
|
|
5
|
+
|
|
6
|
+
const query = new Validator(structuredClone(DOCUMENT_HISTORY_QUERY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const read = new Validator(structuredClone(DOCUMENT_HISTORY_READ_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
const change = new Validator(structuredClone(DOCUMENT_HISTORY_CHANGE_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
9
|
+
export const validateDocumentHistoryQuery = (value: unknown): value is DocumentHistoryQuery => query.validate(value).valid
|
|
10
|
+
export const validateDocumentHistoryRead = (value: unknown): value is DocumentHistoryRead => read.validate(value).valid
|
|
11
|
+
export const validateDocumentHistoryChange = (value: unknown): value is DocumentHistoryChange => change.validate(value).valid
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_WORKSPACE_IMAGE_INSPECTION_SCHEMA, DOCUMENT_WORKSPACE_IMAGE_IMPORT_SCHEMA } from '../contracts/document-image.ts'
|
|
4
|
+
import type { DocumentWorkspaceImageInspect, DocumentWorkspaceImageImport } from '../contracts/document-image.ts'
|
|
5
|
+
const inspect = new Validator(structuredClone(DOCUMENT_WORKSPACE_IMAGE_INSPECTION_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
6
|
+
const importImage = new Validator(structuredClone(DOCUMENT_WORKSPACE_IMAGE_IMPORT_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
export const validateDocumentWorkspaceImageInspect = (value: unknown): value is DocumentWorkspaceImageInspect => inspect.validate(value).valid
|
|
8
|
+
export const validateDocumentWorkspaceImageImport = (value: unknown): value is DocumentWorkspaceImageImport => importImage.validate(value).valid
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_OUTLINE_QUERY_SCHEMA, DOCUMENT_PART_QUERY_SCHEMA, DOCUMENT_READ_MAX_REQUEST_CHARS } from '../contracts/document-reads.ts'
|
|
4
|
+
import type { DocumentOutlineQuery, DocumentPartQuery } from '../contracts/document-reads.ts'
|
|
5
|
+
|
|
6
|
+
const outline = new Validator(structuredClone(DOCUMENT_OUTLINE_QUERY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
const part = new Validator(structuredClone(DOCUMENT_PART_QUERY_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
8
|
+
const bounded = (value: unknown) => { try { return JSON.stringify(value).length <= DOCUMENT_READ_MAX_REQUEST_CHARS } catch { return false } }
|
|
9
|
+
export function validateDocumentOutlineQuery(value: unknown): value is DocumentOutlineQuery { return bounded(value) && outline.validate(value).valid }
|
|
10
|
+
export function validateDocumentPartQuery(value: unknown): value is DocumentPartQuery { return bounded(value) && part.validate(value).valid }
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Validator } from '@cfworker/json-schema'
|
|
2
|
+
import type { Schema } from '@cfworker/json-schema'
|
|
3
|
+
import { DOCUMENT_VOICE_CATALOG_SCHEMA } from '../contracts/document-voice.ts'
|
|
4
|
+
import type { DocumentVoiceCatalogQuery } from '../contracts/document-voice.ts'
|
|
5
|
+
|
|
6
|
+
const validator = new Validator(structuredClone(DOCUMENT_VOICE_CATALOG_SCHEMA) as unknown as Schema, '2020-12', false)
|
|
7
|
+
export const validateDocumentVoiceCatalogQuery = (value: unknown): value is DocumentVoiceCatalogQuery => validator.validate(value).valid
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export interface Ffmpeg {
|
|
4
|
+
/** The program, as it is run. */
|
|
5
|
+
file: string
|
|
6
|
+
/** The names of its encoders (`libx264`, `libvpx-vp9`, `aac`, `libopus`…). */
|
|
7
|
+
encoders: Set<string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let found: Ffmpeg | null | undefined
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* FFmpeg as the author installed it: `PRESMIUM_FFMPEG`, else `ffmpeg` on the path; undefined when it does not run.
|
|
14
|
+
* Presmium does not ship it; it encodes the lecture video with it when it is there (`ffmpegInstallHint` says how).
|
|
15
|
+
*/
|
|
16
|
+
export function findFfmpeg(): Ffmpeg | undefined {
|
|
17
|
+
if (found !== undefined) return found ?? undefined
|
|
18
|
+
const file = process.env.PRESMIUM_FFMPEG || 'ffmpeg'
|
|
19
|
+
const run = spawnSync(file, ['-hide_banner', '-encoders'], { encoding: 'utf8', timeout: 10_000 })
|
|
20
|
+
found = run.status === 0 ? { file, encoders: new Set([...run.stdout.matchAll(/^ [A-Z.]{6} (\S+)/gm)].map(m => m[1]!)) } : null
|
|
21
|
+
return found ?? undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The one command that installs FFmpeg on this system. */
|
|
25
|
+
export function ffmpegInstallCommand(platform: NodeJS.Platform = process.platform): string {
|
|
26
|
+
if (platform === 'darwin') return 'brew install ffmpeg'
|
|
27
|
+
if (platform === 'win32') return 'winget install --id Gyan.FFmpeg'
|
|
28
|
+
return 'sudo apt install ffmpeg'
|
|
29
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BigIntStats } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
/** A local file's identity and observed contents, including replacements that preserve its modification time. */
|
|
4
|
+
export function fileVersion(stat: Pick<BigIntStats, 'dev' | 'ino' | 'size' | 'mtimeNs' | 'ctimeNs'>): string {
|
|
5
|
+
return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(':')
|
|
6
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
3
|
+
|
|
4
|
+
export const FONT_LICENSE = 'presmium/fonts/LICENSE'
|
|
5
|
+
|
|
6
|
+
const FONT_COPYRIGHTS: Record<string, string> = {
|
|
7
|
+
'@fontsource/ibm-plex-mono': 'IBM Plex Mono: Copyright 2017 IBM Corp. All rights reserved.',
|
|
8
|
+
'@fontsource/ibm-plex-sans': 'IBM Plex Sans: Copyright 2019 IBM Corp. All rights reserved.',
|
|
9
|
+
'@fontsource/source-serif-4': 'Source Serif 4: Copyright 2014 - 2021 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name \'Source\'.',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function fontLicense(from: string | URL): string {
|
|
13
|
+
const manifest = findPackageJSON('@lincyaw/host', from)
|
|
14
|
+
if (!manifest) throw new Error('@lincyaw/host is not installed')
|
|
15
|
+
const { dependencies = {} } = JSON.parse(readFileSync(manifest, 'utf8')) as { dependencies?: Record<string, string> }
|
|
16
|
+
const require = createRequire(manifest)
|
|
17
|
+
const files = Object.keys(dependencies).filter(name => name.startsWith('@fontsource/')).sort().map((name) => {
|
|
18
|
+
const copyright = FONT_COPYRIGHTS[name] ? `${FONT_COPYRIGHTS[name]}\n\n` : ''
|
|
19
|
+
return `${name}\n\n${copyright}${readFileSync(require.resolve(`${name}/LICENSE`), 'utf8').trim()}\n`
|
|
20
|
+
})
|
|
21
|
+
return [`The fonts of this site, from Fontsource, with their copyrights and licenses (SIL Open Font License 1.1).\n`, ...files].join(`\n${'='.repeat(78)}\n\n`)
|
|
22
|
+
}
|