@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/README.md +8 -4
- package/package.json +12 -13
- package/src/assets.ts +163 -0
- package/src/audience.ts +21 -216
- package/src/build.ts +101 -0
- package/src/{host.ts → compilation-actions.ts} +14 -7
- package/src/compile-queue.ts +28 -0
- package/src/compiler.ts +269 -195
- package/src/dev.ts +222 -0
- package/src/directives.ts +1 -0
- package/src/document-lowering.ts +176 -0
- package/src/environments.ts +6 -4
- package/src/execute.ts +50 -36
- package/src/export.ts +49 -0
- package/src/forbid.ts +6 -12
- package/src/frontmatter.ts +16 -0
- package/src/index.ts +21 -11
- package/src/{dependencies.ts → inputs.ts} +22 -13
- package/src/links.ts +21 -45
- package/src/logging.ts +16 -0
- package/src/node-services.ts +33 -0
- package/src/paths.ts +12 -26
- package/src/plugins.ts +53 -10
- package/src/references.ts +31 -20
- package/src/renderers.ts +61 -401
- package/src/sections.ts +6 -80
- package/src/serve.ts +58 -56
- package/src/source-lines.ts +47 -0
- package/src/translations.ts +51 -57
- package/src/validate.ts +3 -1
- package/src/views.ts +2 -0
- package/src/workspace-settings.ts +59 -0
- package/src/workspace.ts +17 -7
- package/src/blocks.ts +0 -557
- package/src/published.ts +0 -105
- package/src/settings.ts +0 -105
- package/src/source.ts +0 -300
- package/src/steps.ts +0 -66
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
|
-
}
|