@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.
@@ -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
+ })
@@ -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
+ }
@@ -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
+ }