@lincyaw/kernel 0.0.0-stage → 1.0.0-rc.0
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 +37 -3
- package/src/anchors.ts +54 -0
- package/src/blocks.ts +27 -0
- package/src/checks.ts +35 -0
- package/src/contracts/agent.ts +153 -0
- package/src/contracts/execution.ts +119 -0
- package/src/contracts/operations.ts +8 -0
- package/src/contracts/preview.ts +391 -0
- package/src/contracts/server.ts +158 -0
- package/src/contracts/service.ts +8 -0
- package/src/contracts/speech.ts +137 -0
- package/src/contracts.ts +285 -0
- package/src/errors.ts +7 -0
- package/src/events.ts +32 -0
- package/src/execution.ts +21 -0
- package/src/fingerprint.ts +31 -0
- package/src/follow.ts +37 -0
- package/src/git.ts +12 -0
- package/src/html.ts +41 -0
- package/src/index.ts +29 -0
- package/src/ir.ts +318 -0
- package/src/kernel.ts +177 -0
- package/src/lock.ts +85 -0
- package/src/logging.ts +11 -0
- package/src/markdown.ts +311 -0
- package/src/node/index.ts +133 -0
- package/src/node/strip-types.mjs +15 -0
- package/src/published.ts +11 -0
- package/src/reading.ts +91 -0
- package/src/requests.ts +28 -0
- package/src/runner.py +454 -0
- package/src/services.ts +146 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { errorMessage } from '../errors.ts'
|
|
2
|
+
|
|
3
|
+
export const PREVIEW_PROTOCOL = 1
|
|
4
|
+
|
|
5
|
+
export const PREVIEW_NAME = 'presmium-preview'
|
|
6
|
+
|
|
7
|
+
export const PREVIEW_PARAM = 'presmium-preview'
|
|
8
|
+
|
|
9
|
+
export const PREVIEW_REVISION = 'presmiumRevision'
|
|
10
|
+
|
|
11
|
+
export const AUTHOR_MARKS_CLASS = 'pm-author-marks'
|
|
12
|
+
|
|
13
|
+
export function previewFrameAncestors(origins: readonly string[]): string {
|
|
14
|
+
return `frame-ancestors ${origins.length > 0 ? origins.join(' ') : "'none'"}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PreviewAnchor {
|
|
18
|
+
section: string
|
|
19
|
+
block?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PreviewPin {
|
|
23
|
+
id: string
|
|
24
|
+
section: string
|
|
25
|
+
block: string
|
|
26
|
+
label: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PreviewPosition {
|
|
30
|
+
section: string
|
|
31
|
+
step: number
|
|
32
|
+
trail: string[]
|
|
33
|
+
view?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type PreviewCommand =
|
|
37
|
+
| { type: 'go', section: string, step?: number, trail?: string[], view?: string }
|
|
38
|
+
| { type: 'highlight', target: PreviewAnchor | null }
|
|
39
|
+
| { type: 'pick', on: boolean }
|
|
40
|
+
| { type: 'pins', targets: PreviewPin[] }
|
|
41
|
+
| { type: 'marks', on: boolean }
|
|
42
|
+
| { type: 'appearance', dark: boolean }
|
|
43
|
+
|
|
44
|
+
export type PreviewCommandType = PreviewCommand['type']
|
|
45
|
+
|
|
46
|
+
export type PreviewReport =
|
|
47
|
+
| { type: 'ready', renderer: 'web' | 'slides', document: string, revision?: string, capabilities: PreviewCommandType[] }
|
|
48
|
+
| ({ type: 'position' } & PreviewPosition)
|
|
49
|
+
| { type: 'picked', section: string, block?: string, step?: number, trail?: string[] }
|
|
50
|
+
| { type: 'selection', selection: (PreviewAnchor & { text: string }) | null }
|
|
51
|
+
| { type: 'updated', revision?: string }
|
|
52
|
+
| { type: 'report', level: 'error' | 'warning', plugin?: string, message: string }
|
|
53
|
+
|
|
54
|
+
export type PreviewEnvelope<M> = M & { protocol: typeof PREVIEW_NAME, version: number, nonce: string }
|
|
55
|
+
|
|
56
|
+
export function previewEnvelope<M extends PreviewCommand | PreviewReport>(nonce: string, message: M): PreviewEnvelope<M> {
|
|
57
|
+
return { protocol: PREVIEW_NAME, version: PREVIEW_PROTOCOL, nonce, ...message }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const record = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
61
|
+
const text = (value: unknown): value is string => typeof value === 'string'
|
|
62
|
+
const optional = <T>(value: unknown, is: (v: unknown) => v is T) => value === undefined || is(value)
|
|
63
|
+
const step = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 0
|
|
64
|
+
const strings = (value: unknown): value is string[] => Array.isArray(value) && value.length <= 10_000 && value.every(text)
|
|
65
|
+
const anchor = (value: unknown): value is PreviewAnchor => record(value) && text(value.section) && optional(value.block, text)
|
|
66
|
+
|
|
67
|
+
export function isPreviewEnvelope(data: unknown, nonce: string): data is PreviewEnvelope<{ type: string }> {
|
|
68
|
+
return record(data) && data.protocol === PREVIEW_NAME && data.nonce === nonce && typeof data.version === 'number' && text(data.type)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readPreviewCommand(data: unknown, nonce: string): PreviewCommand | undefined {
|
|
72
|
+
if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
|
|
73
|
+
const m = data as Record<string, unknown>
|
|
74
|
+
switch (m.type) {
|
|
75
|
+
case 'go': return text(m.section) && optional(m.step, step) && optional(m.trail, strings) && optional(m.view, text) ? { type: 'go', section: m.section, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.view ? { view: m.view as string } : {} } : undefined
|
|
76
|
+
case 'highlight': return m.target === null || anchor(m.target) ? { type: 'highlight', target: m.target === null ? null : { section: m.target.section, ...m.target.block ? { block: m.target.block } : {} } } : undefined
|
|
77
|
+
case 'pick':
|
|
78
|
+
case 'marks': return typeof m.on === 'boolean' ? { type: m.type, on: m.on } : undefined
|
|
79
|
+
case 'appearance': return typeof m.dark === 'boolean' ? { type: 'appearance', dark: m.dark } : undefined
|
|
80
|
+
case 'pins': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(t => record(t) && text(t.id) && text(t.section) && text(t.block) && text(t.label))
|
|
81
|
+
? { type: 'pins', targets: (m.targets as PreviewPin[]).map(({ id, section, block, label }) => ({ id, section, block, label })) }
|
|
82
|
+
: undefined
|
|
83
|
+
default: return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function readPreviewReport(data: unknown, nonce: string): PreviewReport | undefined {
|
|
88
|
+
if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
|
|
89
|
+
const m = data as Record<string, unknown>
|
|
90
|
+
switch (m.type) {
|
|
91
|
+
case 'ready': return (m.renderer === 'web' || m.renderer === 'slides') && text(m.document) && optional(m.revision, text) && strings(m.capabilities) ? { type: 'ready', renderer: m.renderer, document: m.document, ...m.revision ? { revision: m.revision as string } : {}, capabilities: m.capabilities as PreviewCommandType[] } : undefined
|
|
92
|
+
case 'position': return text(m.section) && step(m.step) && strings(m.trail) && optional(m.view, text) ? { type: 'position', section: m.section, step: m.step, trail: m.trail, ...m.view ? { view: m.view as string } : {} } : undefined
|
|
93
|
+
case 'picked': return text(m.section) && optional(m.block, text) && optional(m.step, step) && optional(m.trail, strings) ? { type: 'picked', section: m.section, ...m.block ? { block: m.block as string } : {}, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {} } : undefined
|
|
94
|
+
case 'selection': return m.selection === null || (record(m.selection) && text(m.selection.text) && anchor(m.selection)) ? { type: 'selection', selection: m.selection as (PreviewAnchor & { text: string }) | null } : undefined
|
|
95
|
+
case 'updated': return optional(m.revision, text) ? { type: 'updated', ...m.revision ? { revision: m.revision as string } : {} } : undefined
|
|
96
|
+
case 'report': return (m.level === 'error' || m.level === 'warning') && text(m.message) && optional(m.plugin, text) ? { type: 'report', level: m.level, message: m.message, ...m.plugin ? { plugin: m.plugin as string } : {} } : undefined
|
|
97
|
+
default: return undefined
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PreviewPage {
|
|
102
|
+
renderer: 'web' | 'slides'
|
|
103
|
+
document: string
|
|
104
|
+
origins: readonly string[]
|
|
105
|
+
revision(): string | undefined
|
|
106
|
+
position(): PreviewPosition | undefined
|
|
107
|
+
go(command: Extract<PreviewCommand, { type: 'go' }>): void
|
|
108
|
+
reveal(target: PreviewAnchor): void
|
|
109
|
+
setDark(dark: boolean): void
|
|
110
|
+
sectionOf?(element: Element): string | undefined
|
|
111
|
+
sectionElement?(section: string): HTMLElement | undefined
|
|
112
|
+
seen?(element: Element): boolean
|
|
113
|
+
stepOf?(anchor: Required<PreviewAnchor>): { step: number, trail: string[] } | undefined
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface PreviewAgent {
|
|
117
|
+
moved(): void
|
|
118
|
+
updated(): void
|
|
119
|
+
redraw(): void
|
|
120
|
+
report(problem: { level: 'error' | 'warning', plugin?: string, message: string }): void
|
|
121
|
+
stop(): void
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const FRAME_NAME = `${PREVIEW_NAME}:`
|
|
125
|
+
const STYLE_ID = 'presmium-preview-style'
|
|
126
|
+
const HIGHLIGHT = 'data-preview-highlight'
|
|
127
|
+
const HOVER = 'data-preview-hover'
|
|
128
|
+
const POSITIONED = 'data-preview-positioned'
|
|
129
|
+
const PIN_CLASS = 'pm-preview-pin'
|
|
130
|
+
const CONTROLS = 'a, button, input, select, textarea, label, summary, [contenteditable], [role="dialog"]'
|
|
131
|
+
const COMMANDS: PreviewCommandType[] = ['go', 'highlight', 'pick', 'pins', 'marks', 'appearance']
|
|
132
|
+
|
|
133
|
+
const STYLE = `
|
|
134
|
+
[${HIGHLIGHT}] { outline: 2px solid var(--pm-color-accent, #1d6b7a) !important; outline-offset: 4px; border-radius: 2px; }
|
|
135
|
+
[${HOVER}] { outline: 2px dashed var(--pm-color-accent, #1d6b7a) !important; outline-offset: 2px; cursor: crosshair; }
|
|
136
|
+
[${POSITIONED}] { position: relative; }
|
|
137
|
+
.${PIN_CLASS} { position: absolute; top: -10px; left: -10px; z-index: 50; min-width: 20px; height: 20px; padding: 0 5px; box-sizing: border-box; border-radius: 10px; background: var(--pm-color-accent, #1d6b7a); color: #fff; font: 600 12px/20px system-ui, sans-serif; text-align: center; pointer-events: none; }
|
|
138
|
+
`
|
|
139
|
+
|
|
140
|
+
function previewNonce(): string | undefined {
|
|
141
|
+
if (typeof window === 'undefined') return undefined
|
|
142
|
+
const given = new URL(window.location.href).searchParams.get(PREVIEW_PARAM)
|
|
143
|
+
if (given) return given
|
|
144
|
+
return window.name.startsWith(FRAME_NAME) ? window.name.slice(FRAME_NAME.length) || undefined : undefined
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function startPreviewAgent(page: PreviewPage): PreviewAgent | undefined {
|
|
148
|
+
if (typeof window === 'undefined' || window.parent === window || page.origins.length === 0) return undefined
|
|
149
|
+
const nonce = previewNonce()
|
|
150
|
+
if (!nonce) return undefined
|
|
151
|
+
window.name = `${FRAME_NAME}${nonce}`
|
|
152
|
+
const root = document.documentElement
|
|
153
|
+
root.dataset.presmiumPreview = 'on'
|
|
154
|
+
if (!document.getElementById(STYLE_ID)) {
|
|
155
|
+
const style = document.createElement('style')
|
|
156
|
+
style.id = STYLE_ID
|
|
157
|
+
style.textContent = STYLE
|
|
158
|
+
document.head.append(style)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const post = (message: PreviewReport) => {
|
|
162
|
+
for (const origin of page.origins) window.parent.postMessage(previewEnvelope(nonce, message), origin)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let highlight: PreviewAnchor | null = null
|
|
166
|
+
let pins: PreviewPin[] = []
|
|
167
|
+
let picking = false
|
|
168
|
+
let ready = false
|
|
169
|
+
const waiting: Extract<PreviewReport, { type: 'report' }>[] = []
|
|
170
|
+
let sent: string | undefined
|
|
171
|
+
let revision: string | undefined
|
|
172
|
+
|
|
173
|
+
const seen = (element: Element): boolean =>
|
|
174
|
+
!element.closest('[inert], .pm-hidden') && element.getClientRects().length > 0 && (page.seen?.(element) ?? true)
|
|
175
|
+
const seenBlock = (element: Element): HTMLElement | undefined => {
|
|
176
|
+
for (let block = element.closest<HTMLElement>('.pm-block[data-block]'); block; block = block.parentElement?.closest<HTMLElement>('.pm-block[data-block]') ?? null) {
|
|
177
|
+
if (seen(block)) return block
|
|
178
|
+
}
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
const sectionOf = (element: Element): string | undefined =>
|
|
182
|
+
element.closest<HTMLElement>('[data-section]')?.dataset.section ?? page.sectionOf?.(element)
|
|
183
|
+
const blockElement = (section: string, block: string): HTMLElement | undefined => {
|
|
184
|
+
for (const element of document.querySelectorAll<HTMLElement>(`.pm-block[data-block="${CSS.escape(block)}"]`)) {
|
|
185
|
+
if (sectionOf(element) === section && seen(element)) return element
|
|
186
|
+
}
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
const sectionElement = (section: string): HTMLElement | undefined => {
|
|
190
|
+
const own = page.sectionElement?.(section)
|
|
191
|
+
if (own) return own
|
|
192
|
+
const all = [...document.querySelectorAll<HTMLElement>(`[data-section="${CSS.escape(section)}"]`)]
|
|
193
|
+
return all.find(el => el.classList.contains('pm-section') && seen(el)) ?? all.find(seen)
|
|
194
|
+
}
|
|
195
|
+
const elementOf = (target: PreviewAnchor) => (target.block ? blockElement(target.section, target.block) : sectionElement(target.section))
|
|
196
|
+
const anchorOf = (element: Element): PreviewAnchor | undefined => {
|
|
197
|
+
const block = seenBlock(element)
|
|
198
|
+
const section = sectionOf(block ?? element)
|
|
199
|
+
if (!section) return undefined
|
|
200
|
+
return block ? { section, block: block.dataset.block! } : { section }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const drawHighlight = (scroll: boolean) => {
|
|
204
|
+
const element = highlight && elementOf(highlight)
|
|
205
|
+
for (const other of document.querySelectorAll(`[${HIGHLIGHT}]`)) if (other !== element) other.removeAttribute(HIGHLIGHT)
|
|
206
|
+
if (!element) return false
|
|
207
|
+
element.setAttribute(HIGHLIGHT, '')
|
|
208
|
+
if (scroll) {
|
|
209
|
+
const box = element.getBoundingClientRect()
|
|
210
|
+
if (box.bottom < 0 || box.top > window.innerHeight) element.scrollIntoView({ block: 'center' })
|
|
211
|
+
}
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
const drawPins = () => {
|
|
215
|
+
const wanted = new Map<HTMLElement, PreviewPin[]>()
|
|
216
|
+
for (const pin of pins) {
|
|
217
|
+
const element = blockElement(pin.section, pin.block)
|
|
218
|
+
if (element) wanted.set(element, [...wanted.get(element) ?? [], pin])
|
|
219
|
+
}
|
|
220
|
+
for (const old of document.querySelectorAll<HTMLElement>(`.${PIN_CLASS}`)) {
|
|
221
|
+
const host = old.parentElement
|
|
222
|
+
if (!host || !wanted.get(host)?.some(pin => pin.id === old.dataset.pin && pin.label === old.textContent)) old.remove()
|
|
223
|
+
}
|
|
224
|
+
for (const host of document.querySelectorAll<HTMLElement>(`[${POSITIONED}]`)) {
|
|
225
|
+
if (!wanted.has(host)) host.removeAttribute(POSITIONED)
|
|
226
|
+
}
|
|
227
|
+
for (const [element, own] of wanted) {
|
|
228
|
+
if (getComputedStyle(element).position === 'static') element.setAttribute(POSITIONED, '')
|
|
229
|
+
own.forEach((pin, i) => {
|
|
230
|
+
if ([...element.children].some(child => child.classList.contains(PIN_CLASS) && (child as HTMLElement).dataset.pin === pin.id)) return
|
|
231
|
+
const mark = document.createElement('span')
|
|
232
|
+
mark.className = PIN_CLASS
|
|
233
|
+
mark.dataset.pin = pin.id
|
|
234
|
+
mark.textContent = pin.label
|
|
235
|
+
mark.setAttribute('aria-hidden', 'true')
|
|
236
|
+
if (i > 0) mark.style.left = `${-10 + 24 * i}px`
|
|
237
|
+
element.append(mark)
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let frame = 0
|
|
242
|
+
const redraw = () => {
|
|
243
|
+
cancelAnimationFrame(frame)
|
|
244
|
+
frame = requestAnimationFrame(() => {
|
|
245
|
+
drawHighlight(false)
|
|
246
|
+
drawPins()
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
const showHighlight = (target: PreviewAnchor | null) => {
|
|
250
|
+
highlight = target
|
|
251
|
+
if (!target) return void drawHighlight(false)
|
|
252
|
+
if (drawHighlight(true)) return
|
|
253
|
+
page.reveal(target)
|
|
254
|
+
const until = Date.now() + 5000
|
|
255
|
+
const retry = () => {
|
|
256
|
+
if (highlight !== target || drawHighlight(true) || Date.now() > until) return
|
|
257
|
+
setTimeout(retry, 100)
|
|
258
|
+
}
|
|
259
|
+
setTimeout(retry, 50)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let hovered: Element | undefined
|
|
263
|
+
const hover = (event: MouseEvent) => {
|
|
264
|
+
const target = event.target instanceof Element ? event.target : undefined
|
|
265
|
+
const block = target && !target.closest(CONTROLS) ? seenBlock(target) : undefined
|
|
266
|
+
if (block === hovered) return
|
|
267
|
+
hovered?.removeAttribute(HOVER)
|
|
268
|
+
hovered = block
|
|
269
|
+
hovered?.setAttribute(HOVER, '')
|
|
270
|
+
}
|
|
271
|
+
const click = (event: MouseEvent) => {
|
|
272
|
+
const target = event.target instanceof Element ? event.target : undefined
|
|
273
|
+
if (!target || event.button !== 0 || target.closest(CONTROLS)) return
|
|
274
|
+
if (document.getSelection()?.isCollapsed === false) return
|
|
275
|
+
const anchor = anchorOf(target)
|
|
276
|
+
if (!anchor) return
|
|
277
|
+
event.preventDefault()
|
|
278
|
+
event.stopImmediatePropagation()
|
|
279
|
+
const at = anchor.block ? page.stepOf?.({ section: anchor.section, block: anchor.block }) : undefined
|
|
280
|
+
post({ type: 'picked', ...anchor, ...at ? { step: at.step, trail: at.trail } : {} })
|
|
281
|
+
}
|
|
282
|
+
const setPicking = (on: boolean) => {
|
|
283
|
+
if (on === picking) return
|
|
284
|
+
picking = on
|
|
285
|
+
if (on) {
|
|
286
|
+
document.addEventListener('mouseover', hover, true)
|
|
287
|
+
document.addEventListener('click', click, true)
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
document.removeEventListener('mouseover', hover, true)
|
|
291
|
+
document.removeEventListener('click', click, true)
|
|
292
|
+
hovered?.removeAttribute(HOVER)
|
|
293
|
+
hovered = undefined
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
let selected = false
|
|
297
|
+
let selecting: ReturnType<typeof setTimeout> | undefined
|
|
298
|
+
const selection = () => {
|
|
299
|
+
clearTimeout(selecting)
|
|
300
|
+
selecting = setTimeout(() => {
|
|
301
|
+
const range = document.getSelection()
|
|
302
|
+
const value = range && !range.isCollapsed ? range.toString().trim() : ''
|
|
303
|
+
const node = range?.rangeCount ? range.getRangeAt(0).commonAncestorContainer : undefined
|
|
304
|
+
const anchor = value && node ? anchorOf(node instanceof Element ? node : node.parentElement ?? document.body) : undefined
|
|
305
|
+
if (anchor) {
|
|
306
|
+
selected = true
|
|
307
|
+
post({ type: 'selection', selection: { ...anchor, text: value.slice(0, 4000) } })
|
|
308
|
+
} else if (selected) {
|
|
309
|
+
selected = false
|
|
310
|
+
post({ type: 'selection', selection: null })
|
|
311
|
+
}
|
|
312
|
+
}, 200)
|
|
313
|
+
}
|
|
314
|
+
document.addEventListener('selectionchange', selection)
|
|
315
|
+
|
|
316
|
+
const observer = new MutationObserver(() => {
|
|
317
|
+
if (highlight || pins.length > 0) redraw()
|
|
318
|
+
})
|
|
319
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
320
|
+
|
|
321
|
+
const receive = (event: MessageEvent) => {
|
|
322
|
+
if (event.source !== window.parent || !page.origins.includes(event.origin)) return
|
|
323
|
+
const command = readPreviewCommand(event.data, nonce)
|
|
324
|
+
if (!command) return
|
|
325
|
+
try {
|
|
326
|
+
switch (command.type) {
|
|
327
|
+
case 'go': return page.go(command)
|
|
328
|
+
case 'highlight': return showHighlight(command.target)
|
|
329
|
+
case 'pick': return setPicking(command.on)
|
|
330
|
+
case 'pins':
|
|
331
|
+
pins = command.targets
|
|
332
|
+
return drawPins()
|
|
333
|
+
case 'marks': return void root.classList.toggle(AUTHOR_MARKS_CLASS, command.on)
|
|
334
|
+
case 'appearance': return page.setDark(command.dark)
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.error(`[presmium] the preview could not take ${command.type}: ${errorMessage(error)}`)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
window.addEventListener('message', receive)
|
|
341
|
+
|
|
342
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
343
|
+
let last = 0
|
|
344
|
+
const sendPosition = () => {
|
|
345
|
+
timer = undefined
|
|
346
|
+
const position = page.position()
|
|
347
|
+
if (!position) return
|
|
348
|
+
if (!ready) {
|
|
349
|
+
ready = true
|
|
350
|
+
revision = page.revision()
|
|
351
|
+
post({ type: 'ready', renderer: page.renderer, document: page.document, ...revision ? { revision } : {}, capabilities: COMMANDS })
|
|
352
|
+
for (const problem of waiting.splice(0)) post(problem)
|
|
353
|
+
}
|
|
354
|
+
const key = JSON.stringify(position)
|
|
355
|
+
if (key === sent) return
|
|
356
|
+
sent = key
|
|
357
|
+
last = Date.now()
|
|
358
|
+
post({ type: 'position', ...position })
|
|
359
|
+
}
|
|
360
|
+
const moved = () => {
|
|
361
|
+
if (timer) return
|
|
362
|
+
timer = setTimeout(sendPosition, Math.max(0, 150 - (Date.now() - last)))
|
|
363
|
+
}
|
|
364
|
+
moved()
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
moved,
|
|
368
|
+
updated() {
|
|
369
|
+
const next = page.revision()
|
|
370
|
+
if (!ready || next === revision) return
|
|
371
|
+
revision = next
|
|
372
|
+
post({ type: 'updated', ...next ? { revision: next } : {} })
|
|
373
|
+
redraw()
|
|
374
|
+
},
|
|
375
|
+
redraw,
|
|
376
|
+
report(problem) {
|
|
377
|
+
const message = { type: 'report' as const, ...problem }
|
|
378
|
+
if (ready) post(message)
|
|
379
|
+
else waiting.push(message)
|
|
380
|
+
},
|
|
381
|
+
stop() {
|
|
382
|
+
window.removeEventListener('message', receive)
|
|
383
|
+
document.removeEventListener('selectionchange', selection)
|
|
384
|
+
observer.disconnect()
|
|
385
|
+
setPicking(false)
|
|
386
|
+
clearTimeout(timer)
|
|
387
|
+
clearTimeout(selecting)
|
|
388
|
+
delete root.dataset.presmiumPreview
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { Logger } from '../contracts.ts'
|
|
2
|
+
import { defineService } from './service.ts'
|
|
3
|
+
import type { ServiceToken } from './service.ts'
|
|
4
|
+
|
|
5
|
+
export type MemberRole = 'reader' | 'editor' | 'owner'
|
|
6
|
+
|
|
7
|
+
export interface Member {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
email?: string
|
|
11
|
+
roles: MemberRole[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RequestContext {
|
|
15
|
+
member: Member
|
|
16
|
+
document: string
|
|
17
|
+
rest: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RouteHandler = (request: Request, ctx: RequestContext) => Response | Promise<Response>
|
|
21
|
+
|
|
22
|
+
export interface Route {
|
|
23
|
+
role?: MemberRole
|
|
24
|
+
maxBody?: number
|
|
25
|
+
handle: RouteHandler
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const MAX_BODY = 1 << 20
|
|
29
|
+
|
|
30
|
+
export function json(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
|
|
31
|
+
return new Response(JSON.stringify(data), { status, headers: { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', ...headers } })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function problem(status: number, message: string): Response {
|
|
35
|
+
return json({ error: message }, status)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ProxySpec {
|
|
39
|
+
path: string
|
|
40
|
+
target: string
|
|
41
|
+
headers?: Record<string, string>
|
|
42
|
+
role?: MemberRole
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ServerContribution {
|
|
46
|
+
routes?: Record<string, Route>
|
|
47
|
+
proxies?: ProxySpec[]
|
|
48
|
+
close?(): void | Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ServerContext {
|
|
52
|
+
deckDir: string
|
|
53
|
+
workspaceRoot: string
|
|
54
|
+
config: Record<string, unknown>
|
|
55
|
+
settings: Record<string, unknown>
|
|
56
|
+
lang?: string
|
|
57
|
+
service<T>(token: ServiceToken<T>): T | undefined
|
|
58
|
+
secret(name: string): string | undefined
|
|
59
|
+
operatorDir?: string
|
|
60
|
+
home?: boolean
|
|
61
|
+
log: Logger
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface NodeServiceContext {
|
|
65
|
+
deckDir: string
|
|
66
|
+
workspaceRoot: string
|
|
67
|
+
config: Record<string, unknown>
|
|
68
|
+
settings: Record<string, unknown>
|
|
69
|
+
lang?: string
|
|
70
|
+
secret(name: string): string | undefined
|
|
71
|
+
operatorDir?: string
|
|
72
|
+
service?<T>(token: ServiceToken<T>): T | undefined
|
|
73
|
+
log: Logger
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ApprovalKind = 'operate' | 'spend' | 'write' | 'agent'
|
|
77
|
+
|
|
78
|
+
export type ApprovalScope = 'content' | 'workspace'
|
|
79
|
+
|
|
80
|
+
export interface ApprovalRequest {
|
|
81
|
+
kind: ApprovalKind
|
|
82
|
+
scope?: ApprovalScope
|
|
83
|
+
title: string
|
|
84
|
+
detail?: string
|
|
85
|
+
from: string
|
|
86
|
+
member?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Approvals {
|
|
90
|
+
request(request: ApprovalRequest, signal?: AbortSignal): Promise<boolean>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const APPROVALS = defineService<Approvals>('presmium.approvals')
|
|
94
|
+
|
|
95
|
+
export function eventStream(events: AsyncIterable<{ event?: string, data: unknown }>, signal?: AbortSignal): Response {
|
|
96
|
+
const encoder = new TextEncoder()
|
|
97
|
+
const iterator = events[Symbol.asyncIterator]()
|
|
98
|
+
let closed = false
|
|
99
|
+
const stop = (controller?: ReadableStreamDefaultController<Uint8Array>) => {
|
|
100
|
+
if (closed) return
|
|
101
|
+
closed = true
|
|
102
|
+
void iterator.return?.()
|
|
103
|
+
try {
|
|
104
|
+
controller?.close()
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
109
|
+
start(controller) {
|
|
110
|
+
if (signal?.aborted) stop(controller)
|
|
111
|
+
else signal?.addEventListener('abort', () => stop(controller), { once: true })
|
|
112
|
+
},
|
|
113
|
+
async pull(controller) {
|
|
114
|
+
if (closed) return
|
|
115
|
+
try {
|
|
116
|
+
const next = await iterator.next()
|
|
117
|
+
if (closed) return
|
|
118
|
+
if (next.done) {
|
|
119
|
+
stop(controller)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
const { event, data } = next.value
|
|
123
|
+
const lines = `${event ? `event: ${event}\n` : ''}${JSON.stringify(data ?? null).split('\n').map(line => `data: ${line}`).join('\n')}\n\n`
|
|
124
|
+
controller.enqueue(encoder.encode(lines))
|
|
125
|
+
} catch (error) {
|
|
126
|
+
closed = true
|
|
127
|
+
controller.error(error)
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
cancel() {
|
|
131
|
+
stop()
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
return new Response(stream, { headers: { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-store', 'X-Accel-Buffering': 'no' } })
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type AgentMode = 'author'
|
|
138
|
+
|
|
139
|
+
export interface McpAccess {
|
|
140
|
+
url: string
|
|
141
|
+
token: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface McpGrant extends McpAccess {
|
|
145
|
+
revoke(): void
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @experimental */
|
|
149
|
+
export interface McpIssueOptions {
|
|
150
|
+
confirmed?: boolean
|
|
151
|
+
document?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface McpEndpoint {
|
|
155
|
+
issue(member: Member, mode: AgentMode, options?: McpIssueOptions): McpGrant
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const MCP_ENDPOINT = defineService<McpEndpoint>('presmium.mcpEndpoint')
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { defineService } from './service.ts'
|
|
2
|
+
|
|
3
|
+
export interface SpeechWord { text: string, start: number, end: number }
|
|
4
|
+
|
|
5
|
+
const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u
|
|
6
|
+
const LETTER = /[\p{L}\p{N}'’]/u
|
|
7
|
+
|
|
8
|
+
export interface TextWord { text: string, start: number, end: number }
|
|
9
|
+
|
|
10
|
+
export function splitWords(text: string): TextWord[] {
|
|
11
|
+
const words: TextWord[] = []
|
|
12
|
+
let run: TextWord | undefined
|
|
13
|
+
let at = 0
|
|
14
|
+
for (const char of text) {
|
|
15
|
+
if (CJK.test(char)) {
|
|
16
|
+
if (run) words.push(run)
|
|
17
|
+
run = undefined
|
|
18
|
+
words.push({ text: char, start: at, end: at + char.length })
|
|
19
|
+
} else if (LETTER.test(char)) {
|
|
20
|
+
run ??= { text: '', start: at, end: at }
|
|
21
|
+
run.text += char
|
|
22
|
+
run.end = at + char.length
|
|
23
|
+
} else if (run) {
|
|
24
|
+
words.push(run)
|
|
25
|
+
run = undefined
|
|
26
|
+
}
|
|
27
|
+
at += char.length
|
|
28
|
+
}
|
|
29
|
+
if (run) words.push(run)
|
|
30
|
+
return words
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WordSpan { from: number, to: number, start: number, end: number }
|
|
34
|
+
|
|
35
|
+
export interface WrittenWord extends SpeechWord { offset?: number }
|
|
36
|
+
|
|
37
|
+
export function remapWords(words: readonly SpeechWord[], written: string, spoken: string, spans: readonly WordSpan[]): WrittenWord[] {
|
|
38
|
+
const places: { start: number, end: number }[] = []
|
|
39
|
+
let at = 0
|
|
40
|
+
for (const word of words) {
|
|
41
|
+
const index = word.text ? spoken.indexOf(word.text, at) : -1
|
|
42
|
+
if (index < 0) return words.map(w => ({ ...w }))
|
|
43
|
+
places.push({ start: index, end: at = index + word.text.length })
|
|
44
|
+
}
|
|
45
|
+
const writtenAt = (offset: number) => {
|
|
46
|
+
let shift = 0
|
|
47
|
+
for (const span of spans) {
|
|
48
|
+
if (offset < span.from) break
|
|
49
|
+
if (offset < span.to) return span.start
|
|
50
|
+
shift += (span.end - span.start) - (span.to - span.from)
|
|
51
|
+
}
|
|
52
|
+
return offset + shift
|
|
53
|
+
}
|
|
54
|
+
const result: WrittenWord[] = []
|
|
55
|
+
let group: { span: WordSpan, word: WrittenWord } | undefined
|
|
56
|
+
words.forEach((word, i) => {
|
|
57
|
+
const { start, end } = places[i]!
|
|
58
|
+
const span = spans.find(s => start < s.to && end > s.from)
|
|
59
|
+
if (span && group?.span === span) {
|
|
60
|
+
group.word.end = Math.max(group.word.end, word.end)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
group = undefined
|
|
64
|
+
if (span) {
|
|
65
|
+
const placed = { text: written.slice(span.start, span.end), start: word.start, end: word.end, offset: span.start }
|
|
66
|
+
group = { span, word: placed }
|
|
67
|
+
result.push(placed)
|
|
68
|
+
} else {
|
|
69
|
+
result.push({ ...word, offset: writtenAt(start) })
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
return result
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface Synthesis {
|
|
76
|
+
audio: Blob | string
|
|
77
|
+
mime: string
|
|
78
|
+
duration: number
|
|
79
|
+
words?: SpeechWord[]
|
|
80
|
+
key: string
|
|
81
|
+
engine: string
|
|
82
|
+
model: string
|
|
83
|
+
voice: string
|
|
84
|
+
synthetic: true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TranscriptSegment { start: number, end: number, text: string, words: SpeechWord[] }
|
|
88
|
+
|
|
89
|
+
export interface Transcript {
|
|
90
|
+
lang: string
|
|
91
|
+
duration: number
|
|
92
|
+
text: string
|
|
93
|
+
segments: TranscriptSegment[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SpeechEngineInfo {
|
|
97
|
+
id: string
|
|
98
|
+
provider: string
|
|
99
|
+
stt: boolean
|
|
100
|
+
tts: boolean
|
|
101
|
+
wordTimes: boolean
|
|
102
|
+
local: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface VoiceInfo {
|
|
106
|
+
id: string
|
|
107
|
+
label: string
|
|
108
|
+
lang: string[]
|
|
109
|
+
cloned?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface SynthesisRequest {
|
|
113
|
+
engine?: string
|
|
114
|
+
voice: string
|
|
115
|
+
text: string
|
|
116
|
+
lang: string
|
|
117
|
+
rate?: number
|
|
118
|
+
style?: string
|
|
119
|
+
lexicon?: Record<string, string>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface TranscriptionRequest {
|
|
123
|
+
engine?: string
|
|
124
|
+
audio: Blob
|
|
125
|
+
lang?: string
|
|
126
|
+
hints?: string[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface SpeechService {
|
|
130
|
+
engines(): Promise<SpeechEngineInfo[]>
|
|
131
|
+
voices(engine: string, lang?: string): Promise<VoiceInfo[]>
|
|
132
|
+
synthesize(request: SynthesisRequest, signal?: AbortSignal): Promise<Synthesis>
|
|
133
|
+
cached?(key: string, signal?: AbortSignal): Promise<Synthesis | undefined>
|
|
134
|
+
transcribe(request: TranscriptionRequest, signal?: AbortSignal): Promise<Transcript>
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const SPEECH = defineService<SpeechService>('presmium.speech')
|