@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
package/src/markdown.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
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
|
+
function isButton(node: MdastNode): boolean {
|
|
202
|
+
return typeof node.class === 'string' && node.class.split(/\s+/).includes('button')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function layout(node: MdastNode, ctx: Lowering): MdastNode[] {
|
|
206
|
+
const all = (nodes: MdastNode[] = []) => nodes.flatMap(child => lower(child, ctx))
|
|
207
|
+
const element = cardsAs(ctx.options) === 'element'
|
|
208
|
+
const div = (className: string, content: MdastNode[], style = '') => [html(`<div class="${className}"${style}>`), ...content, html('</div>')]
|
|
209
|
+
const heading = (content: MdastNode[]): MdastNode => ({ type: 'heading', depth: SMALL_HEADING, children: content })
|
|
210
|
+
const items = node.children ?? []
|
|
211
|
+
switch (node.type) {
|
|
212
|
+
case 'grid': {
|
|
213
|
+
if (!element) {
|
|
214
|
+
ctx.degraded.add(node.type)
|
|
215
|
+
return all(items)
|
|
216
|
+
}
|
|
217
|
+
const columns = Array.isArray(node.columns) ? Number(node.columns.at(-1)) || 1 : 1
|
|
218
|
+
return div('pm-grid', all(items), ` style="--pm-grid-columns: ${columns}"`)
|
|
219
|
+
}
|
|
220
|
+
case 'grid-item':
|
|
221
|
+
return element ? div('pm-grid__item', all(items)) : all(items)
|
|
222
|
+
case 'card': {
|
|
223
|
+
const part = (type: string) => items.find(child => child.type === type)?.children ?? []
|
|
224
|
+
const title = all(part('cardTitle'))
|
|
225
|
+
const url = typeof node.url === 'string' && node.url ? node.url : undefined
|
|
226
|
+
const linked = url ? lower({ type: 'link', url, children: title.length ? title : [text(url)] }, ctx) : title
|
|
227
|
+
const body = all(items.filter(child => !['header', 'cardTitle', 'footer'].includes(child.type)))
|
|
228
|
+
if (!element) {
|
|
229
|
+
ctx.degraded.add(node.type)
|
|
230
|
+
const top = title.length ? [heading(linked)] : url ? [paragraph(linked)] : []
|
|
231
|
+
return [...all(part('header')), ...top, ...body, ...all(part('footer'))]
|
|
232
|
+
}
|
|
233
|
+
const header = part('header').length ? div('pm-card__header', all(part('header'))) : []
|
|
234
|
+
const footer = part('footer').length ? div('pm-card__footer', all(part('footer'))) : []
|
|
235
|
+
const top = title.length || url ? div('pm-card__title', [paragraph(linked)]) : []
|
|
236
|
+
return div(url ? 'pm-card pm-card--link' : 'pm-card', [...header, ...top, ...body, ...footer])
|
|
237
|
+
}
|
|
238
|
+
case 'tabSet':
|
|
239
|
+
if (!element) ctx.degraded.add(node.type)
|
|
240
|
+
return element ? div('pm-tabs', all(items)) : all(items)
|
|
241
|
+
case 'tabItem': {
|
|
242
|
+
const title = typeof node.title === 'string' ? node.title : ''
|
|
243
|
+
if (!element) return [...title ? [heading([text(title)])] : [], ...all(items)]
|
|
244
|
+
return div('pm-tabs__item', [...title ? [html(`<div class="pm-tabs__title">${escape(title)}</div>`)] : [], ...all(items)])
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return all(items)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function roleMark(node: MdastNode): string {
|
|
251
|
+
const options = typeof node.options === 'object' && node.options !== null ? node.options as Record<string, unknown> : {}
|
|
252
|
+
const value = typeof options.value === 'string' ? options.value.trim() : ''
|
|
253
|
+
return `〔${String(node.name ?? '')}${value ? ` ${value}` : ''}〕`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function footnoteNumber(node: MdastNode): string {
|
|
257
|
+
return String(node.enumerator ?? node.label ?? node.identifier)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function container(node: MdastNode, ctx: Lowering): MdastNode[] {
|
|
261
|
+
const all = (nodes: MdastNode[] = []) => nodes.flatMap(child => lower(child, ctx))
|
|
262
|
+
const items = node.children ?? []
|
|
263
|
+
const given = items.find(child => child.type === 'caption')
|
|
264
|
+
const derived = !given && node.kind !== 'quote'
|
|
265
|
+
const others = items.filter(child => !CONTENT.has(child.type))
|
|
266
|
+
const content = derived ? items.filter(child => CONTENT.has(child.type)) : items.filter(child => child.type !== 'caption' && child.type !== 'legend')
|
|
267
|
+
const caption = derived ? others.slice(0, 1) : given?.children ?? []
|
|
268
|
+
const legend = derived ? others.slice(1) : items.find(child => child.type === 'legend')?.children ?? []
|
|
269
|
+
const words = all(caption).flatMap(child => (child.type === 'paragraph' ? child.children ?? [] : [child]))
|
|
270
|
+
const title = words.length ? [paragraph([{ type: node.kind === 'table' ? 'strong' : 'emphasis', children: words }])] : []
|
|
271
|
+
const shown = all(content).map(child => (child.type === 'image' ? paragraph([child]) : child))
|
|
272
|
+
return node.kind === 'table' ? [...title, ...shown, ...all(legend)] : [...shown, ...title, ...all(legend)]
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function table(node: MdastNode, ctx: Lowering): MdastNode {
|
|
276
|
+
const inline = (child: MdastNode) => PHRASING.has(child.type) || child.type === 'html'
|
|
277
|
+
const line = (nodes: MdastNode[]) => nodes.map(child => (child.type === 'break' ? html('<br>') : child))
|
|
278
|
+
const rows = (node.children ?? []).map(row => ({
|
|
279
|
+
type: 'tableRow',
|
|
280
|
+
children: (row.children ?? []).map((cell) => {
|
|
281
|
+
const parts: MdastNode[][] = []
|
|
282
|
+
let run: MdastNode[] | undefined
|
|
283
|
+
for (const child of (cell.children ?? []).flatMap(c => lower(c, ctx))) {
|
|
284
|
+
if (inline(child)) {
|
|
285
|
+
if (!run) parts.push(run = [])
|
|
286
|
+
run.push(...line([child]))
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
run = undefined
|
|
290
|
+
parts.push(child.type === 'paragraph' ? line(child.children ?? []) : [text(plain(child))])
|
|
291
|
+
}
|
|
292
|
+
return { type: 'tableCell', children: parts.flatMap((part, i) => (i ? [html('<br>'), ...part] : part)) }
|
|
293
|
+
}),
|
|
294
|
+
}))
|
|
295
|
+
const align = Array.isArray(node.align) ? node.align : node.children?.[0]?.children?.map(cell => cell.align ?? null)
|
|
296
|
+
return { type: 'table', align, children: rows }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function definitions(node: MdastNode, ctx: Lowering): MdastNode {
|
|
300
|
+
const items: MdastNode[] = []
|
|
301
|
+
for (const child of node.children ?? []) {
|
|
302
|
+
if (child.type === 'definitionTerm') {
|
|
303
|
+
const term = (child.children ?? []).flatMap(c => lower(c, ctx))
|
|
304
|
+
items.push({ type: 'listItem', spread: true, children: [paragraph([{ type: 'strong', children: term }])] })
|
|
305
|
+
} else {
|
|
306
|
+
if (!items.length) items.push({ type: 'listItem', spread: true, children: [] })
|
|
307
|
+
items.at(-1)!.children!.push(...paragraphs(child.children ?? []).flatMap(c => lower(c, ctx)))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return { type: 'list', ordered: false, spread: true, children: items }
|
|
311
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
|
|
2
|
+
import { createRequire, findPackageJSON } from 'node:module'
|
|
3
|
+
import { dirname, join, sep } from 'node:path'
|
|
4
|
+
import { pathToFileURL } from 'node:url'
|
|
5
|
+
import type { LocalAsset } from '../ir.ts'
|
|
6
|
+
|
|
7
|
+
export function copyPublished(found: LocalAsset, dir: string): string {
|
|
8
|
+
const target = join(dir, found.name)
|
|
9
|
+
if (!existsSync(target)) {
|
|
10
|
+
mkdirSync(dir, { recursive: true })
|
|
11
|
+
cpSync(found.file, target, { recursive: true })
|
|
12
|
+
}
|
|
13
|
+
return target
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FONT_LICENSE = 'presmium/fonts/LICENSE'
|
|
17
|
+
|
|
18
|
+
const FONT_COPYRIGHTS: Record<string, string> = {
|
|
19
|
+
'@fontsource/ibm-plex-mono': 'IBM Plex Mono: Copyright 2017 IBM Corp. All rights reserved.',
|
|
20
|
+
'@fontsource/ibm-plex-sans': 'IBM Plex Sans: Copyright 2019 IBM Corp. All rights reserved.',
|
|
21
|
+
'@fontsource/source-serif-4': 'Source Serif 4: Copyright 2014 - 2021 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name \'Source\'.',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function fontLicense(from: string | URL): string {
|
|
25
|
+
const manifest = findPackageJSON('@lincyaw/host', from)
|
|
26
|
+
if (!manifest) throw new Error('@lincyaw/host is not installed')
|
|
27
|
+
const { dependencies = {} } = JSON.parse(readFileSync(manifest, 'utf8')) as { dependencies?: Record<string, string> }
|
|
28
|
+
const require = createRequire(manifest)
|
|
29
|
+
const files = Object.keys(dependencies).filter(name => name.startsWith('@fontsource/')).sort().map((name) => {
|
|
30
|
+
const copyright = FONT_COPYRIGHTS[name] ? `${FONT_COPYRIGHTS[name]}\n\n` : ''
|
|
31
|
+
return `${name}\n\n${copyright}${readFileSync(require.resolve(`${name}/LICENSE`), 'utf8').trim()}\n`
|
|
32
|
+
})
|
|
33
|
+
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`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const TYPESCRIPT_IMPORT: readonly string[] = ['--import', new URL('./strip-types.mjs', import.meta.url).href]
|
|
37
|
+
|
|
38
|
+
const PROVIDED = /^(?:vue|@vue\/.*|vitepress|@slidev\/.*)$/
|
|
39
|
+
const PRESMIUM = /^@lincyaw\//
|
|
40
|
+
const IMPORT = /(?:^|\n)\s*(?:import|export)(?!\s+type\b)[^'"`;]*?\bfrom\s*['"]([^'"./][^'"]*)['"]|(?:^|\n)\s*import\s*['"]([^'"./][^'"]*)['"]|\bimport\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g
|
|
41
|
+
const SOURCE = /\.(?:[cm]?[jt]s|vue)$/
|
|
42
|
+
const DECLARATION = /\.d\.[cm]?ts$/
|
|
43
|
+
const STYLE = /\.(?:css|scss|sass|less|styl)$/
|
|
44
|
+
const NOT_BROWSER = new Set(['node', 'bin', 'test', 'fixtures', 'node_modules'])
|
|
45
|
+
|
|
46
|
+
export function preBundle(root: string, packages: Iterable<string>): string[] {
|
|
47
|
+
return optimizeDeps(root, packages).include
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function optimizeDeps(root: string, packages: Iterable<string>): { include: string[], exclude: string[] } {
|
|
51
|
+
const include = new Set<string>()
|
|
52
|
+
const exclude = new Set<string>()
|
|
53
|
+
const seen = new Set<string>()
|
|
54
|
+
const visit = (dir: string): void => {
|
|
55
|
+
const real = realpathSync(dir)
|
|
56
|
+
if (seen.has(real) || !real.split(sep).includes('node_modules')) return
|
|
57
|
+
seen.add(real)
|
|
58
|
+
const { name, imports, presmium } = browserDependencies(real)
|
|
59
|
+
exclude.add(name)
|
|
60
|
+
if (imports.length > 0) linkPackage(root, name, real)
|
|
61
|
+
for (const specifier of imports) include.add(`${name} > ${specifier}`)
|
|
62
|
+
for (const dependency of presmium) {
|
|
63
|
+
const manifest = installedManifest(dependency, real)
|
|
64
|
+
if (manifest) visit(dirname(manifest))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (const dir of packages) if (existsSync(join(dir, 'package.json'))) visit(dir)
|
|
68
|
+
return { include: [...include].sort(), exclude: [...exclude].sort() }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function installedManifest(name: string, dir: string): string | undefined {
|
|
72
|
+
try {
|
|
73
|
+
return findPackageJSON(name, pathToFileURL(join(dir, 'package.json')))
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function packageOf(file: string): string | undefined {
|
|
80
|
+
const manifest = findPackageJSON(pathToFileURL(file))
|
|
81
|
+
return manifest && dirname(manifest)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function linkPackage(root: string, name: string, dir: string): void {
|
|
85
|
+
const link = join(root, 'node_modules', name)
|
|
86
|
+
const stat = lstatSync(link, { throwIfNoEntry: false })
|
|
87
|
+
if (stat?.isSymbolicLink() && readlinkSync(link) === dir) return
|
|
88
|
+
if (stat && !stat.isSymbolicLink()) return
|
|
89
|
+
rmSync(link, { force: true })
|
|
90
|
+
mkdirSync(dirname(link), { recursive: true })
|
|
91
|
+
symlinkSync(dir, link, 'junction')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface BrowserDependencies {
|
|
95
|
+
name: string
|
|
96
|
+
imports: string[]
|
|
97
|
+
presmium: string[]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const dependencies = new Map<string, BrowserDependencies>()
|
|
101
|
+
function browserDependencies(dir: string): BrowserDependencies {
|
|
102
|
+
let found = dependencies.get(dir)
|
|
103
|
+
if (!found) {
|
|
104
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { name: string, dependencies?: Record<string, string>, peerDependencies?: Record<string, string> }
|
|
105
|
+
const own = (specifier: string) => Object.hasOwn(pkg.dependencies ?? {}, packageName(specifier))
|
|
106
|
+
const specifiers = [...browserImports(dir)]
|
|
107
|
+
found = {
|
|
108
|
+
name: pkg.name,
|
|
109
|
+
imports: specifiers.filter(specifier => own(specifier) && !PROVIDED.test(packageName(specifier)) && !PRESMIUM.test(specifier) && !STYLE.test(specifier)),
|
|
110
|
+
presmium: [...new Set(specifiers.filter(specifier => PRESMIUM.test(specifier)).map(packageName))]
|
|
111
|
+
.filter(name => Object.hasOwn({ ...pkg.dependencies, ...pkg.peerDependencies }, name)),
|
|
112
|
+
}
|
|
113
|
+
dependencies.set(dir, found)
|
|
114
|
+
}
|
|
115
|
+
return found
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function browserImports(dir: string, found = new Set<string>(), top = true): Set<string> {
|
|
119
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
120
|
+
const path = join(dir, entry.name)
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
if (!NOT_BROWSER.has(entry.name)) browserImports(path, found, false)
|
|
123
|
+
} else if (SOURCE.test(entry.name) && !DECLARATION.test(entry.name) && !(top && entry.name === 'vite.config.ts')) {
|
|
124
|
+
for (const match of readFileSync(path, 'utf8').matchAll(IMPORT)) found.add((match[1] ?? match[2] ?? match[3])!)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return found
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function packageName(specifier: string): string {
|
|
131
|
+
const parts = specifier.split('/')
|
|
132
|
+
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]!
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { registerHooks, stripTypeScriptTypes } from 'node:module'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const emitWarning = process.emitWarning
|
|
6
|
+
process.emitWarning = (warning, ...args) => {
|
|
7
|
+
if (!String(warning).includes('stripTypeScriptTypes')) emitWarning.call(process, warning, ...args)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
registerHooks({
|
|
11
|
+
load(url, context, nextLoad) {
|
|
12
|
+
if (!url.startsWith('file:') || !/\.m?ts$/.test(url) || !url.includes('/node_modules/')) return nextLoad(url, context)
|
|
13
|
+
return { format: 'module', source: stripTypeScriptTypes(readFileSync(fileURLToPath(url), 'utf8')), shortCircuit: true }
|
|
14
|
+
},
|
|
15
|
+
})
|
package/src/published.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LocalAsset } from './ir.ts'
|
|
2
|
+
|
|
3
|
+
export interface PublishedUrlOptions {
|
|
4
|
+
prefix?: string
|
|
5
|
+
directoryIndex?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function publishedUrl(found: Pick<LocalAsset, 'name' | 'directory'>, url: string, options: PublishedUrlOptions = {}): string {
|
|
9
|
+
const index = found.directory ? options.directoryIndex ?? '/index.html' : ''
|
|
10
|
+
return `${options.prefix ?? ''}${encodeURIComponent(found.name)}${index}${/[?#].*$/s.exec(url)?.[0] ?? ''}`
|
|
11
|
+
}
|
package/src/reading.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { irBlocks } from './blocks.ts'
|
|
2
|
+
import type { IRBlock, IRDocument, IRSection, MdastNode } from './ir.ts'
|
|
3
|
+
|
|
4
|
+
export interface Chapter {
|
|
5
|
+
title: string
|
|
6
|
+
sections: IRSection[]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sectionDepth(doc: IRDocument): number {
|
|
10
|
+
return doc.sections.reduce((depth, section) => Math.max(depth, section.depth), 0)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function titles(depth: number, section: IRSection): boolean {
|
|
14
|
+
return section.depth === 0 || section.depth < depth
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isChapterTitle(doc: IRDocument, section: IRSection): boolean {
|
|
18
|
+
return titles(sectionDepth(doc), section)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function chapters(doc: IRDocument): Chapter[] {
|
|
22
|
+
const depth = sectionDepth(doc)
|
|
23
|
+
const result: Chapter[] = []
|
|
24
|
+
let previous: IRSection | undefined
|
|
25
|
+
for (const section of doc.sections) {
|
|
26
|
+
const current = result.at(-1)
|
|
27
|
+
if (!current || section.source.file !== previous?.source.file || titles(depth, section)) {
|
|
28
|
+
result.push({ title: section.title, sections: [section] })
|
|
29
|
+
} else {
|
|
30
|
+
current.sections.push(section)
|
|
31
|
+
}
|
|
32
|
+
previous = section
|
|
33
|
+
}
|
|
34
|
+
return result
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function blockVisible(block: IRBlock, step: number): boolean {
|
|
38
|
+
return step >= block.at && (block.until === undefined || step < block.until)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function blockState(block: IRBlock, step: number): number {
|
|
42
|
+
return Math.min(Math.max(step - block.at, 0), block.span ?? 0)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function narrationAt(section: IRSection, step: number): IRBlock[] {
|
|
46
|
+
return section.blocks.filter(block => block.role === 'narration' && block.at === step)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function sectionNarration(section: IRSection): IRBlock[] {
|
|
50
|
+
return [...irBlocks(section.blocks)].filter(block => block.role === 'narration')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const BREAKS = new Set(['paragraph', 'heading', 'listItem', 'tableRow', 'tableCell', 'code', 'math', 'blockquote', 'break'])
|
|
54
|
+
|
|
55
|
+
function htmlText(html: string): string {
|
|
56
|
+
return html.replace(/<(style|script)\b[\s\S]*?<\/\1\s*>/gi, ' ').replace(/<[^>]*>/g, ' ')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function mdastText(node: MdastNode): string {
|
|
60
|
+
if (node.type === 'html' && typeof node.value === 'string') return htmlText(node.value)
|
|
61
|
+
if (typeof node.value === 'string' && !node.children) return node.value
|
|
62
|
+
const text = (node.children ?? []).map(mdastText).join('')
|
|
63
|
+
return BREAKS.has(node.type) ? `${text}\n` : text
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function propText(value: unknown): string[] {
|
|
67
|
+
if (typeof value === 'string') return [value]
|
|
68
|
+
if (Array.isArray(value)) return value.flatMap(propText)
|
|
69
|
+
if (value && typeof value === 'object') return Object.values(value).flatMap(propText)
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function blockText(block: IRBlock): string {
|
|
74
|
+
const { node } = block
|
|
75
|
+
switch (node.type) {
|
|
76
|
+
case 'markdown': return mdastText(node.mdast).trim()
|
|
77
|
+
case 'code': return [node.title, node.value, ...(node.outputs ?? []).map(o => o.stdout)].filter(Boolean).join('\n')
|
|
78
|
+
case 'component': return [...propText(node.props), ...(node.children ?? []).map(blockText)].join('\n')
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function tokenize(text: string): string[] {
|
|
83
|
+
const segmenter = new Intl.Segmenter('zh', { granularity: 'word' })
|
|
84
|
+
const words: string[] = []
|
|
85
|
+
for (const piece of text.split(/[\n\r\p{Z}\p{P}]+/u)) {
|
|
86
|
+
for (const part of segmenter.segment(piece)) {
|
|
87
|
+
if (part.isWordLike) words.push(part.segment)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return words
|
|
91
|
+
}
|
package/src/requests.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface RequestHeaders {
|
|
2
|
+
host?: string | string[]
|
|
3
|
+
origin?: string | string[]
|
|
4
|
+
'sec-fetch-site'?: string | string[]
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
|
|
8
|
+
|
|
9
|
+
function first(value: string | string[] | undefined): string | undefined {
|
|
10
|
+
return Array.isArray(value) ? value[0] : value
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function hostName(value: string): string {
|
|
14
|
+
const host = value.includes('://') ? value.slice(value.indexOf('://') + 3) : value
|
|
15
|
+
if (host.startsWith('[')) return host.slice(0, host.indexOf(']') + 1).toLowerCase()
|
|
16
|
+
return host.split(':')[0]!.toLowerCase()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function localRequestRefusal(headers: RequestHeaders, allowedHosts?: true | readonly string[]): string | undefined {
|
|
20
|
+
if (first(headers['sec-fetch-site']) === 'cross-site') return 'cross-site request'
|
|
21
|
+
if (allowedHosts === true) return undefined
|
|
22
|
+
const allowed = (name: string) => LOOPBACK.has(name) || (allowedHosts ?? []).some(host => host.startsWith('.') ? name.endsWith(host) || name === host.slice(1) : name === host.toLowerCase())
|
|
23
|
+
const host = first(headers.host)
|
|
24
|
+
if (!host || !allowed(hostName(host))) return `Host ${host ?? '(none)'} is not this machine`
|
|
25
|
+
const origin = first(headers.origin)
|
|
26
|
+
if (origin && origin !== 'null' && !allowed(hostName(origin))) return `Origin ${origin} is not this machine`
|
|
27
|
+
return undefined
|
|
28
|
+
}
|