@lincyaw/compiler 1.0.0-rc.0 → 1.0.0-rc.2

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/src/settings.ts DELETED
@@ -1,105 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
- import { existsSync, readFileSync, realpathSync } from 'node:fs'
3
- import { homedir } from 'node:os'
4
- import { isAbsolute, join, relative, resolve, sep } from 'node:path'
5
- import { errorMessage, isRecord } from '@lincyaw/kernel'
6
- import type { Logger, ServiceToken } from '@lincyaw/kernel'
7
- import { parse as parseYaml } from 'yaml'
8
-
9
- export const WORKSPACE_CONFIG = 'presmium.workspace.yaml'
10
-
11
- export type OperatorSettings = Record<string, Record<string, unknown>>
12
-
13
- const read = new Map<string, { settings: OperatorSettings, problem?: string }>()
14
-
15
- export function workspaceSettings(root: string): { settings: OperatorSettings, problem?: string } {
16
- const file = join(root, WORKSPACE_CONFIG)
17
- const known = read.get(file)
18
- if (known) return known
19
- let result: { settings: OperatorSettings, problem?: string } = { settings: {} }
20
- if (existsSync(file)) {
21
- try {
22
- const data: unknown = parseYaml(readFileSync(file, 'utf8'))
23
- const settings = isRecord(data) && isRecord(data.settings) ? data.settings : {}
24
- result = { settings: Object.fromEntries(Object.entries(settings).map(([id, value]) => [id, isRecord(value) ? value : {}])) }
25
- } catch (error) {
26
- result = { settings: {}, problem: `${file}: ${errorMessage(error)}` }
27
- }
28
- }
29
- read.set(file, result)
30
- return result
31
- }
32
-
33
- export function operatorPlugins(settings: OperatorSettings): string[] {
34
- const list = settings['presmium.workspace']?.plugins
35
- return Array.isArray(list) ? list.filter((name): name is string => typeof name === 'string' && name.length > 0) : []
36
- }
37
-
38
- export function inside(path: string, root: string): boolean {
39
- const rel = relative(root, path)
40
- return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
41
- }
42
-
43
- function real(path: string): string {
44
- try {
45
- return realpathSync(path)
46
- } catch {
47
- return resolve(path)
48
- }
49
- }
50
-
51
- export function operatorDir(workspaceRoot: string, given = process.env.PRESMIUM_OPERATOR_DIR): string {
52
- const root = real(workspaceRoot)
53
- if (given) {
54
- const dir = resolve(given)
55
- if (inside(real(dir), root)) throw new Error(`the operator directory ${dir} lies inside the workspace ${root}; it must be outside every workspace`)
56
- return dir
57
- }
58
- const state = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
59
- return join(state, 'presmium', 'operator', createHash('sha256').update(root).digest('hex').slice(0, 16))
60
- }
61
-
62
- export class NodeServices {
63
- readonly #impls = new Map<string, { impl: unknown, owner: string }>()
64
-
65
- constructor(host: [ServiceToken<unknown>, unknown][] = []) {
66
- for (const [token, impl] of host) this.#impls.set(token.id, { impl, owner: 'host' })
67
- }
68
-
69
- add(owner: string, entries: [ServiceToken<unknown>, unknown][]): string[] {
70
- const problems: string[] = []
71
- for (const [token, impl] of entries) {
72
- if (!token || typeof token.id !== 'string') {
73
- problems.push(`plugin ${owner}: services() gave an entry without a service token`)
74
- continue
75
- }
76
- const known = this.#impls.get(token.id)
77
- if (known) {
78
- problems.push(`plugin ${owner}: ${token.id} is already provided by ${known.owner}; this implementation is ignored`)
79
- continue
80
- }
81
- this.#impls.set(token.id, { impl, owner })
82
- }
83
- return problems
84
- }
85
-
86
- get<T>(token: ServiceToken<T>): T | undefined {
87
- return this.#impls.get(token.id)?.impl as T | undefined
88
- }
89
-
90
- owner(token: ServiceToken<unknown>): string | undefined {
91
- return this.#impls.get(token.id)?.owner
92
- }
93
- }
94
-
95
- export function pluginLogger(id: string): Logger {
96
- const tag = `[${id}]`
97
- return {
98
- debug: (...args) => {
99
- if (process.env.PRESMIUM_DEBUG) console.error(tag, ...args)
100
- },
101
- info: (...args) => console.error(tag, ...args),
102
- warn: (...args) => console.error(tag, ...args),
103
- error: (...args) => console.error(tag, ...args),
104
- }
105
- }
package/src/source.ts DELETED
@@ -1,300 +0,0 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { dirname, resolve } from 'node:path'
3
- import { errorMessage, isRecord } from '@lincyaw/kernel'
4
- import type { DirectiveOptionType, Finding, MdastNode, SourcePos } from '@lincyaw/kernel'
5
- import { buttonRole } from 'myst-ext-button'
6
- import { cardDirective } from 'myst-ext-card'
7
- import { gridDirectives } from 'myst-ext-grid'
8
- import { tabDirectives } from 'myst-ext-tabs'
9
- import { mystParse } from 'myst-parser'
10
- import { includeDirectiveTransform } from 'myst-transforms'
11
- import { parse as parseYaml } from 'yaml'
12
-
13
- export const BODY = 'presmiumDirectiveBody'
14
-
15
- export const ROLE = 'presmiumRoleOptions'
16
-
17
- const MYST_EXTENSIONS = { directives: [cardDirective, ...gridDirectives, ...tabDirectives], roles: [buttonRole] }
18
-
19
- const OPTION_TYPES = { string: String, number: Number, boolean: Boolean, list: String } as const
20
-
21
- export interface ParsedFile {
22
- file: string
23
- text: string
24
- frontmatter: Record<string, unknown>
25
- findings: Finding[]
26
- children: MdastNode[]
27
- }
28
-
29
- export interface ExpandedSource {
30
- children: MdastNode[]
31
- files: string[]
32
- missing: string[]
33
- findings: Finding[]
34
- }
35
-
36
- const origin = new WeakMap<object, string>()
37
-
38
- export function fileOf(node: MdastNode, fallback: string): string {
39
- return origin.get(node) ?? fallback
40
- }
41
-
42
- export function lineOf(node: MdastNode): { line: number, endLine: number } | undefined {
43
- const position = node.position as { start?: { line: number }, end?: { line: number } } | undefined
44
- if (!position?.start) return undefined
45
- return { line: position.start.line, endLine: Math.max(position.start.line, position.end?.line ?? 0) }
46
- }
47
-
48
- export function sourceOf(node: MdastNode, entry: string): SourcePos {
49
- const lines = lineOf(node)
50
- return { file: fileOf(node, entry), line: lines?.line ?? 1, endLine: lines?.endLine }
51
- }
52
-
53
- export function withoutBom(text: string): string {
54
- return text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text
55
- }
56
-
57
- export class Parser {
58
- readonly #specs: object[]
59
- readonly #roles: object[]
60
- readonly #cache = new Map<string, ParsedFile>()
61
-
62
- constructor(directives: Map<string, boolean>, roles: Map<string, Record<string, DirectiveOptionType> | undefined> = new Map()) {
63
- this.#roles = [...MYST_EXTENSIONS.roles, ...[...roles].map(([name, options]) => ({
64
- name,
65
- options: Object.fromEntries(Object.entries(options ?? {}).map(([key, type]) => [key, { type: OPTION_TYPES[type] ?? String }])),
66
- body: { type: String },
67
- run(data: { options?: Record<string, unknown> }) {
68
- return [{ type: ROLE, options: data.options ?? {} }]
69
- },
70
- }))]
71
- this.#specs = [...MYST_EXTENSIONS.directives, ...[...directives].map(([name, mystBody]) => ({
72
- name,
73
- arg: { type: String },
74
- body: { type: mystBody ? 'myst' : String },
75
- run(data: { node: MdastNode, body?: unknown }) {
76
- const body = data.node.children?.find(c => c.type === 'mystDirectiveBody')
77
- const children = mystBody && Array.isArray(data.body) ? data.body as MdastNode[] : []
78
- return [{ type: BODY, line: body && lineOf(body)?.line, children }]
79
- },
80
- }))]
81
- }
82
-
83
- parse(markdown: string, line = 1): MdastNode {
84
- const tree = mystParse(markdown, { directives: this.#specs as never, roles: this.#roles as never }) as MdastNode
85
- if (line !== 1) shiftLines(tree, line - 1)
86
- return tree
87
- }
88
-
89
- async read(file: string): Promise<ParsedFile | undefined> {
90
- const read = await readFile(file, 'utf8').catch(() => undefined)
91
- if (read === undefined) return undefined
92
- const text = withoutBom(read)
93
- const cached = this.#cache.get(file)
94
- if (cached?.text === text) return cached
95
- const parsed = this.#parseFile(file, text)
96
- this.#cache.set(file, parsed)
97
- return parsed
98
- }
99
-
100
- async expand(entry: string): Promise<ExpandedSource> {
101
- const files: string[] = []
102
- const findings: Finding[] = []
103
- const missing: string[] = []
104
- const parsed = await this.read(entry)
105
- if (!parsed) throw new Error(`cannot read ${entry}`)
106
- files.push(entry)
107
- findings.push(...parsed.findings)
108
- const children = await this.#expandAll(parsed.children, entry, [entry], { files, missing, findings })
109
- for (const file of this.#cache.keys()) {
110
- if (!files.includes(file)) this.#cache.delete(file)
111
- }
112
- return { children, files, missing, findings }
113
- }
114
-
115
- #parseFile(file: string, text: string): ParsedFile {
116
- const findings: Finding[] = []
117
- const { yaml, body } = splitFrontmatter(text)
118
- let frontmatter: Record<string, unknown> = {}
119
- if (yaml !== undefined) {
120
- try {
121
- const data: unknown = parseYaml(yaml) ?? {}
122
- if (isRecord(data)) frontmatter = data
123
- else findings.push({ checkId: 'presmium.parse', severity: 'error', message: 'frontmatter is not a mapping', source: { file, line: 1 } })
124
- } catch (error) {
125
- findings.push({ checkId: 'presmium.parse', severity: 'error', message: `invalid frontmatter: ${errorMessage(error)}`, source: { file, line: 1 } })
126
- }
127
- }
128
- const tree = this.parse(body)
129
- tagFile(tree, file)
130
- const children = placeFootnotes((tree.children ?? []).flatMap(c => c.type === 'block' ? c.children ?? [] : [c]))
131
- return { file, text, frontmatter, findings, children }
132
- }
133
-
134
- async #expandAll(nodes: MdastNode[], file: string, stack: string[], out: Expansion): Promise<MdastNode[]> {
135
- const result: MdastNode[] = []
136
- for (const node of nodes) {
137
- if (node.type === 'mystDirective' && node.children?.some(child => child.type === 'include' && child.literal === true)) {
138
- result.push(...await this.#literal(node, file, out))
139
- continue
140
- }
141
- if (node.type === 'mystDirective' && node.name === 'include') {
142
- result.push(...await this.#include(node, file, stack, out))
143
- continue
144
- }
145
- if (!node.children?.length) {
146
- result.push(node)
147
- continue
148
- }
149
- const children = await this.#expandAll(node.children, file, stack, out)
150
- const same = children.length === node.children.length && children.every((c, i) => c === node.children![i])
151
- const copy = same ? node : { ...node, children }
152
- origin.set(copy, file)
153
- result.push(copy)
154
- }
155
- return result
156
- }
157
-
158
- async #include(node: MdastNode, file: string, stack: string[], out: Expansion): Promise<MdastNode[]> {
159
- const source = { file, line: lineOf(node)?.line ?? 1 }
160
- const fail = (message: string) => {
161
- out.findings.push({ checkId: 'presmium.parse', severity: 'error', message, source })
162
- return []
163
- }
164
- const arg = typeof node.args === 'string' ? node.args.trim() : ''
165
- if (!arg) return fail('{include} needs a file argument')
166
- const target = resolve(dirname(file), arg)
167
- if (stack.includes(target)) return fail(`{include} ${arg} includes itself`)
168
- const parsed = await this.read(target)
169
- if (!parsed) {
170
- if (!out.missing.includes(target)) out.missing.push(target)
171
- return fail(`{include} ${arg}: file not found`)
172
- }
173
- if (!out.files.includes(target)) out.files.push(target)
174
- out.findings.push(...parsed.findings)
175
- if (Object.keys(parsed.frontmatter).length > 0) {
176
- out.findings.push({ checkId: 'presmium.parse', severity: 'warning', message: 'the frontmatter of an included file is ignored', source: { file: target, line: 1 } })
177
- }
178
- return this.#expandAll(parsed.children, target, [...stack, target], out)
179
- }
180
-
181
- async #literal(node: MdastNode, file: string, out: Expansion): Promise<MdastNode[]> {
182
- const source = { file, line: lineOf(node)?.line ?? 1 }
183
- const name = `{${String(node.name)}}`
184
- const fail = (message: string) => {
185
- out.findings.push({ checkId: 'presmium.parse', severity: 'error', message, source })
186
- return []
187
- }
188
- const include = structuredClone(node.children!.find(child => child.type === 'include')!)
189
- const arg = typeof include.file === 'string' ? include.file.trim() : ''
190
- if (!arg) return fail(`${name} needs a file argument`)
191
- const target = resolve(dirname(file), arg)
192
- const text = await readFile(target, 'utf8').catch(() => undefined)
193
- if (text === undefined) {
194
- if (!out.missing.includes(target)) out.missing.push(target)
195
- return fail(`${name} ${arg}: file not found`)
196
- }
197
- if (!out.files.includes(target)) out.files.push(target)
198
- const vfile = {
199
- message(reason: string) {
200
- out.findings.push({ checkId: 'presmium.directive', severity: 'warning', message: `${name} ${arg}: ${reason}`, source })
201
- return {}
202
- },
203
- }
204
- await includeDirectiveTransform({ type: 'root', children: [include] } as never, {}, vfile as never, {
205
- loadFile: () => text,
206
- resolveFile: () => target,
207
- parseContent: () => ({ mdast: { type: 'root', children: [] } }),
208
- sourceFile: file,
209
- sourcePath: dirname(file),
210
- })
211
- const children = include.children ?? []
212
- for (const code of [...children, ...children.flatMap(child => child.children ?? [])]) {
213
- if (code.type === 'code' && typeof code.value === 'string') code.value = code.value.replace(/\r?\n$/, '')
214
- }
215
- const copy: MdastNode = { ...node, children }
216
- origin.set(copy, file)
217
- return [copy]
218
- }
219
- }
220
-
221
- interface Expansion {
222
- files: string[]
223
- missing: string[]
224
- findings: Finding[]
225
- }
226
-
227
- export function readFrontmatter(text: string): Record<string, unknown> {
228
- const { yaml } = splitFrontmatter(withoutBom(text))
229
- try {
230
- const data: unknown = yaml === undefined ? {} : parseYaml(yaml)
231
- return isRecord(data) ? data : {}
232
- } catch {
233
- return {}
234
- }
235
- }
236
-
237
- function splitFrontmatter(text: string): { yaml?: string, body: string } {
238
- const match = /^---[ \t]*\r?\n(?:([\s\S]*?)\r?\n)?---[ \t]*(?:\r?\n|$)/.exec(text)
239
- if (!match) return { body: text }
240
- const lines = match[0].split('\n').length - 1
241
- return { yaml: match[1] ?? '', body: '\n'.repeat(lines) + text.slice(match[0].length) }
242
- }
243
-
244
- function placeFootnotes(nodes: MdastNode[]): MdastNode[] {
245
- const placed = nodes.filter(node => node.type !== 'footnoteDefinition')
246
- for (const definition of nodes.filter(node => node.type === 'footnoteDefinition')) {
247
- const lines = contentLines(definition)
248
- if (!lines) {
249
- placed.push(definition)
250
- continue
251
- }
252
- definition.position = { start: { line: lines.line, column: 1 }, end: { line: lines.endLine, column: 1 } }
253
- insertAt(placed, definition, lines.line)
254
- }
255
- return placed
256
- }
257
-
258
- const INLINE = new Set(['paragraph', 'heading', 'tableCell', 'caption', 'admonitionTitle', 'cardTitle', 'summary', 'definitionTerm'])
259
-
260
- function insertAt(nodes: MdastNode[], node: MdastNode, line: number): void {
261
- const holder = nodes.find((n) => {
262
- const lines = lineOf(n)
263
- return lines && lines.line < line && line <= lines.endLine && !INLINE.has(n.type) && n.children?.length
264
- })
265
- const inner = holder && contentOf(holder)
266
- if (inner) return insertAt(inner, node, line)
267
- const after = nodes.findIndex(n => (lineOf(n)?.line ?? 0) > line)
268
- nodes.splice(after < 0 ? nodes.length : after, 0, node)
269
- }
270
-
271
- function contentOf(holder: MdastNode): MdastNode[] | undefined {
272
- const children = holder.children!
273
- if (holder.type !== 'mystDirective') return children
274
- const body = children.find(c => c.type === BODY)
275
- if (body) return body.children
276
- const [made] = children
277
- return children.length === 1 && made && !lineOf(made) && made.children ? made.children : children
278
- }
279
-
280
- function contentLines(node: MdastNode): { line: number, endLine: number } | undefined {
281
- let found: { line: number, endLine: number } | undefined
282
- for (const child of node.children ?? []) {
283
- const lines = lineOf(child) ?? contentLines(child)
284
- if (lines) found = { line: Math.min(found?.line ?? lines.line, lines.line), endLine: Math.max(found?.endLine ?? 0, lines.endLine) }
285
- }
286
- return found
287
- }
288
-
289
- function tagFile(node: MdastNode, file: string): void {
290
- origin.set(node, file)
291
- for (const child of node.children ?? []) tagFile(child, file)
292
- }
293
-
294
- function shiftLines(node: MdastNode, offset: number): void {
295
- const position = node.position as { start?: { line: number }, end?: { line: number } } | undefined
296
- if (position?.start) position.start.line += offset
297
- if (position?.end) position.end.line += offset
298
- if (typeof node.line === 'number') node.line += offset
299
- for (const child of node.children ?? []) shiftLines(child, offset)
300
- }
package/src/steps.ts DELETED
@@ -1,66 +0,0 @@
1
- import type { Finding, IRBlock } from '@lincyaw/kernel'
2
- import type { BlockDraft } from './blocks.ts'
3
- import { unique } from './sections.ts'
4
-
5
- export function scheduleSteps(drafts: BlockDraft[], report: (finding: Finding) => void): { blocks: IRBlock[], steps: number } {
6
- const used = new Set<string>()
7
- for (const { label, source } of drafts) {
8
- if (!label) continue
9
- if (used.has(label)) report({ checkId: 'presmium.sections', severity: 'error', message: `duplicate block label (${label})= in this section`, source: { file: source.file, line: source.line } })
10
- used.add(label)
11
- }
12
- const counts = new Map<string, number>()
13
- const ids = drafts.map(({ label, kind }) => {
14
- if (label) return label
15
- let id: string
16
- do {
17
- const n = (counts.get(kind) ?? 0) + 1
18
- counts.set(kind, n)
19
- id = `${kind}-${n}`
20
- } while (used.has(id))
21
- return unique(id, used)
22
- })
23
-
24
- let last = 0
25
- let current = 0
26
- let currentSpan = 0
27
- let narrated = 0
28
- const groups = new Map<number, number>()
29
- const blocks = drafts.map((draft, i): IRBlock => {
30
- const { group } = draft
31
- if (group !== undefined && !groups.has(group)) {
32
- if (drafts.some(d => d.group === group && d.role === 'visual' && !d.reveal)) {
33
- current = ++last
34
- currentSpan = 0
35
- narrated = 0
36
- }
37
- groups.set(group, current)
38
- }
39
- const at = draft.at ?? (draft.reveal ? last + 1
40
- : draft.role === 'narration' ? current + Math.min(narrated, currentSpan)
41
- : draft.span > 0 ? last
42
- : group !== undefined ? groups.get(group)!
43
- : 0)
44
- if (draft.until !== undefined && draft.until <= at) report({ checkId: 'presmium.steps', severity: 'error', message: ':until: must be greater than the block’s starting step', source: draft.source })
45
- if (draft.role === 'visual') {
46
- current = at
47
- currentSpan = draft.span
48
- narrated = 0
49
- }
50
- else if (draft.role === 'narration') {
51
- narrated++
52
- }
53
- last = Math.max(last, at + draft.span, draft.until ?? 0)
54
- return {
55
- id: ids[i]!,
56
- role: draft.role,
57
- at,
58
- ...draft.until !== undefined ? { until: draft.until } : {},
59
- ...draft.span > 0 ? { span: draft.span } : {},
60
- ...draft.only ? { only: draft.only } : {},
61
- source: draft.source,
62
- node: draft.node,
63
- }
64
- })
65
- return { blocks, steps: last }
66
- }