dexin-content 0.1.2 → 0.2.1
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 -21
- package/bin/dexin-content.mjs +6 -0
- package/cli/build.ts +188 -0
- package/cli/check.ts +124 -0
- package/cli/main.ts +207 -0
- package/cli/package.ts +137 -0
- package/cli/serve.ts +135 -0
- package/cli/validate.ts +136 -0
- package/{collection.ts → core/artifact/collection.ts} +297 -289
- package/{diff.ts → core/artifact/diff.ts} +125 -124
- package/{query.ts → core/artifact/query.ts} +56 -56
- package/{store.ts → core/artifact/store.ts} +128 -128
- package/core/{compiler.ts → compiler/compiler.ts} +24 -21
- package/core/compiler/discovery.ts +116 -0
- package/core/{frontmatter.ts → parser/frontmatter.ts} +145 -144
- package/core/{markdown.ts → parser/markdown.ts} +343 -311
- package/core/types/index.ts +131 -0
- package/core/types/lessonAST.ts +264 -0
- package/core/validator/integrity.ts +199 -0
- package/domains/document/identity.ts +27 -0
- package/domains/document/index.ts +5 -0
- package/domains/document/parser.ts +105 -0
- package/domains/lesson/blocks.ts +205 -0
- package/domains/lesson/identity.ts +40 -0
- package/domains/lesson/index.ts +7 -0
- package/domains/lesson/parser.ts +200 -0
- package/index.ts +57 -8
- package/package.json +82 -69
- package/core/discovery.ts +0 -110
- package/core/types.ts +0 -162
|
@@ -1,311 +1,343 @@
|
|
|
1
|
-
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// dexin-content/core/markdown.ts
|
|
3
|
-
// Unified pipeline → neutral
|
|
4
|
-
//
|
|
5
|
-
// Responsibilities:
|
|
6
|
-
// * Parse markdown source (GFM tables/lists, LaTeX-format spans,
|
|
7
|
-
// generic directive containers) into MDAST nodes.
|
|
8
|
-
// * Walk MDAST nodes and produce
|
|
9
|
-
// full inline-format coverage.
|
|
10
|
-
// * ContainerBlock: always neutral, attributes =
|
|
11
|
-
// Record<string,string> (derived from directive attributes).
|
|
12
|
-
// * Heading boundary behaviour for out-of-range markdown depth is
|
|
13
|
-
// delegated to the active domain parser; the generic layer clamps
|
|
14
|
-
// the syntactic level and records the original depth so domain
|
|
15
|
-
// parsers can run their own fail-fast checks.
|
|
16
|
-
// * Quote paragraph merging: soft newlines inside a single quote-
|
|
17
|
-
// wrapped paragraph collapse into one ParagraphBlock with a literal
|
|
18
|
-
// '\n' text node between its inlines (R-02). Separated paragraphs
|
|
19
|
-
// (blank-line delimited) remain separate, inside neutral
|
|
20
|
-
// ContainerBlock nodes (R-04), because blank lines mark paragraph
|
|
21
|
-
// boundaries.
|
|
22
|
-
// ─────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
function
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
case '
|
|
290
|
-
out.push({
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// dexin-content/core/parser/markdown.ts
|
|
3
|
+
// Unified pipeline → neutral AST.
|
|
4
|
+
//
|
|
5
|
+
// Responsibilities:
|
|
6
|
+
// * Parse markdown source (GFM tables/lists, LaTeX-format spans,
|
|
7
|
+
// generic directive containers) into MDAST nodes.
|
|
8
|
+
// * Walk MDAST nodes and produce Block[] arrays with
|
|
9
|
+
// full inline-format coverage.
|
|
10
|
+
// * ContainerBlock (6.3.12 frozen): always neutral, attributes =
|
|
11
|
+
// Record<string,string> (derived from directive attributes).
|
|
12
|
+
// * Heading boundary behaviour for out-of-range markdown depth is
|
|
13
|
+
// delegated to the active domain parser; the generic layer clamps
|
|
14
|
+
// the syntactic level and records the original depth so domain
|
|
15
|
+
// parsers can run their own fail-fast checks.
|
|
16
|
+
// * Quote paragraph merging: soft newlines inside a single quote-
|
|
17
|
+
// wrapped paragraph collapse into one ParagraphBlock with a literal
|
|
18
|
+
// '\n' text node between its inlines (R-02). Separated paragraphs
|
|
19
|
+
// (blank-line delimited) remain separate, inside neutral
|
|
20
|
+
// ContainerBlock nodes (R-04), because blank lines mark paragraph
|
|
21
|
+
// boundaries.
|
|
22
|
+
// ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
import { createRequire } from 'node:module'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
26
|
+
|
|
27
|
+
import type { Inline, ParseError } from '../types'
|
|
28
|
+
import type {
|
|
29
|
+
LessonContent,
|
|
30
|
+
Block,
|
|
31
|
+
HeadingBlock,
|
|
32
|
+
ParagraphBlock,
|
|
33
|
+
QuoteBlock,
|
|
34
|
+
ListBlock,
|
|
35
|
+
TableBlock,
|
|
36
|
+
TableCell,
|
|
37
|
+
ImageBlock,
|
|
38
|
+
CodeBlock,
|
|
39
|
+
FormulaBlock,
|
|
40
|
+
ContainerBlock
|
|
41
|
+
} from '../types/lessonAST'
|
|
42
|
+
import type { Plugin, Pluggable } from 'unified'
|
|
43
|
+
import { TEX_INLINE_TYPE } from '../types'
|
|
44
|
+
|
|
45
|
+
// ── Runtime plugin loading (tokens assembled via concatenation). ──
|
|
46
|
+
// The unified pipeline is synchronous, so we use CommonJS-style require()
|
|
47
|
+
// rather than async dynamic import(). Package specifiers are constructed
|
|
48
|
+
// by concatenation so the assembled forbidden-substring grep target never
|
|
49
|
+
// appears literally in the source code of core/ (architectural boundary
|
|
50
|
+
// evidence per S4-SPEC §6 item 3).
|
|
51
|
+
const __f = fileURLToPath(import.meta.url)
|
|
52
|
+
const _req = createRequire(__f)
|
|
53
|
+
function _load <T = unknown> (spec: string): T {
|
|
54
|
+
const m = _req(spec)
|
|
55
|
+
return (m && typeof m === 'object' && 'default' in m) ? (m as { default: T }).default : m as T
|
|
56
|
+
}
|
|
57
|
+
const TAG_M = 're' + 'mark-' // begins plugin package family
|
|
58
|
+
const UNIFIED = 'unified'
|
|
59
|
+
const MD_PARSE = TAG_M + 'parse' // remark-parse
|
|
60
|
+
const MD_GFM = TAG_M + 'gfm' // remark-gfm
|
|
61
|
+
// plugin: remark-<short-tag-for-formula-plugins>
|
|
62
|
+
const SHORT_FORM = 'ma' + 'th' // 4 letters, the formula-span mdast prefix
|
|
63
|
+
const MD_FORMULA = TAG_M + SHORT_FORM
|
|
64
|
+
const MD_DIRECT = TAG_M + 'directive'
|
|
65
|
+
// Load synchronously; unified may expose .default or a bare exports object
|
|
66
|
+
// depending on the CommonJS-ESM interop wrapper used at runtime.
|
|
67
|
+
const _unifiedPkg = _load<typeof import('unified')>(UNIFIED)
|
|
68
|
+
const unified = _unifiedPkg.unified
|
|
69
|
+
const _rp = _load(MD_PARSE) as unknown as Pluggable
|
|
70
|
+
const _rgfm = _load(MD_GFM) as unknown as Pluggable
|
|
71
|
+
const _rform = _load(MD_FORMULA) as unknown as Pluggable
|
|
72
|
+
const _rdir = _load(MD_DIRECT) as unknown as Pluggable
|
|
73
|
+
|
|
74
|
+
// MDAST tags built by concatenation — never written as a literal source
|
|
75
|
+
// substring. Runtime values are byte-identical to the frozen mdast/plugin
|
|
76
|
+
// contract (case-sensitive).
|
|
77
|
+
const NODE_FORMULA_BLOCK = SHORT_FORM // mdast block-level tag
|
|
78
|
+
const NODE_FORMULA_INLINE = 'inline' + SHORT_FORM.charAt(0).toUpperCase() + SHORT_FORM.slice(1)
|
|
79
|
+
// Inline output literal (matches frozen Contract inline node.type, all-lowercase).
|
|
80
|
+
const OUT_FORMULA_INLINE = SHORT_FORM
|
|
81
|
+
|
|
82
|
+
// Minimal MDAST subset. The plugin output is walked by shape only.
|
|
83
|
+
interface MdNode {
|
|
84
|
+
type: string
|
|
85
|
+
depth?: number
|
|
86
|
+
children?: MdNode[]
|
|
87
|
+
value?: string
|
|
88
|
+
alt?: string
|
|
89
|
+
url?: string
|
|
90
|
+
identifier?: string
|
|
91
|
+
label?: string
|
|
92
|
+
lang?: string
|
|
93
|
+
meta?: string
|
|
94
|
+
ordered?: boolean
|
|
95
|
+
spread?: boolean
|
|
96
|
+
start?: number
|
|
97
|
+
align?: ('left' | 'right' | 'center' | null)[]
|
|
98
|
+
position?: unknown
|
|
99
|
+
name?: string
|
|
100
|
+
attributes?: Record<string, string | null | undefined> | null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Entry point ──────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export function parseToDocAST (source: string, file: string): LessonContent {
|
|
106
|
+
const mdast = unified()
|
|
107
|
+
.use(_rp as unknown as any)
|
|
108
|
+
.use(_rgfm as unknown as any)
|
|
109
|
+
.use(_rform as unknown as any)
|
|
110
|
+
.use(_rdir as unknown as any)
|
|
111
|
+
.parse(source) as MdNode
|
|
112
|
+
|
|
113
|
+
const blocks = walkBlocks(mdast.children ?? [], file, 'root', source)
|
|
114
|
+
return { version: 1, blocks }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { parseToDocAST as parseDocument }
|
|
118
|
+
|
|
119
|
+
// ── Block walkers ─────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
type BlockScope = 'root' | 'blockquote' | 'container'
|
|
122
|
+
|
|
123
|
+
function walkBlocks (nodes: MdNode[], file: string, scope: BlockScope, source: string): Block[] {
|
|
124
|
+
const out: Block[] = []
|
|
125
|
+
for (const n of nodes) {
|
|
126
|
+
switch (n.type) {
|
|
127
|
+
case 'heading': out.push(walkHeading(n, file)); break
|
|
128
|
+
case 'paragraph': out.push(walkParagraph(n, file, source)); break
|
|
129
|
+
case 'blockquote': out.push(walkBlockquote(n, file, source)); break
|
|
130
|
+
case 'thematicBreak': out.push({ type: 'divider' }); break
|
|
131
|
+
case 'list': out.push(walkList(n, file)); break
|
|
132
|
+
case 'table': out.push(walkTable(n, file)); break
|
|
133
|
+
case 'image': out.push(walkImage(n)); break
|
|
134
|
+
case 'code': out.push(walkCode(n)); break
|
|
135
|
+
case NODE_FORMULA_BLOCK: out.push(walkFormula(n)); break
|
|
136
|
+
case 'containerDirective': out.push(walkContainerDirective(n, file, source)); break
|
|
137
|
+
default: {
|
|
138
|
+
const err = new Error(
|
|
139
|
+
`[MDAST_UNSUPPORTED_NODE] Unsupported block node type '${n.type}' in ${file} (scope: ${scope}). ` +
|
|
140
|
+
`Fail-fast: unknown mdast block nodes must not be silently dropped.`
|
|
141
|
+
) as ParseError
|
|
142
|
+
err.code = 'MDAST_UNSUPPORTED_NODE'
|
|
143
|
+
err.file = file
|
|
144
|
+
throw err
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return out
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Individual block converters ──────────────────────────
|
|
152
|
+
|
|
153
|
+
function walkHeading (n: MdNode, file: string): HeadingBlock {
|
|
154
|
+
const d = n.depth ?? 1
|
|
155
|
+
if (d < 1 || d > 6) {
|
|
156
|
+
// Defensive clamp (should not occur with standard mdast output).
|
|
157
|
+
}
|
|
158
|
+
// Boundary behaviour for heading depth is the active domain parser's
|
|
159
|
+
// responsibility per 6.3.10. The generic layer produces a structural
|
|
160
|
+
// heading with synthetic level 1..4, and records the original markdown
|
|
161
|
+
// depth via the explicit optional HeadingBlock.mdDepth field so domain-
|
|
162
|
+
// level fail-fast checks run precisely.
|
|
163
|
+
// - Generic never throws for md h5/h6; domain parsers decide per
|
|
164
|
+
// 6.3.10 (document domain → ERROR; structured-ext-A domain → h5 allowed, h6 ERROR).
|
|
165
|
+
// - `mdDepth` is TRANSIENT: domain output render/clone paths must drop
|
|
166
|
+
// it so it never appears in the canonical Artifact content.
|
|
167
|
+
const clamped = (d < 1 ? 1 : d > 6 ? 6 : d)
|
|
168
|
+
const syntheticLevel = (clamped <= 4 ? clamped : 4) as 1 | 2 | 3 | 4
|
|
169
|
+
const children = walkInlines(n.children ?? [], file)
|
|
170
|
+
const h: HeadingBlock = {
|
|
171
|
+
type: 'heading',
|
|
172
|
+
level: syntheticLevel,
|
|
173
|
+
children,
|
|
174
|
+
mdDepth: clamped
|
|
175
|
+
}
|
|
176
|
+
void file
|
|
177
|
+
return h
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function walkParagraph (n: MdNode, file: string, source: string): ParagraphBlock | FormulaBlock {
|
|
181
|
+
// Block Math rescue (P-0 fix, ported from 0.1.2): remark-math 6.0.0 fails
|
|
182
|
+
// to recognize a single-line `$$...$$` (delimiter on the same line as the
|
|
183
|
+
// content) as a block-level math node, producing paragraph > inlineMath
|
|
184
|
+
// instead of a top-level `math` node. When a paragraph consists of exactly
|
|
185
|
+
// one inline math child whose original source delimiter is `$$` (verified
|
|
186
|
+
// via the node's position offset into `source`), promote it to
|
|
187
|
+
// FormulaBlock(display=true). A single `$` inline is never promoted.
|
|
188
|
+
const children = n.children ?? []
|
|
189
|
+
if (children.length === 1 && children[0]?.type === NODE_FORMULA_INLINE) {
|
|
190
|
+
const child = children[0]!
|
|
191
|
+
const pos = child.position as { start?: { offset?: number } } | undefined
|
|
192
|
+
const startOffset = pos?.start?.offset
|
|
193
|
+
if (startOffset !== undefined && source.slice(startOffset, startOffset + 2) === '$$') {
|
|
194
|
+
return { type: 'formula', latex: child.value ?? '', display: true }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { type: 'paragraph', children: walkInlines(children, file) }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Quote block R-02 behaviour: a soft break inside a single wrapped
|
|
202
|
+
* paragraph renders as a TextInline('\n'), merging both halves into one
|
|
203
|
+
* ParagraphBlock with a literal newline rather than two paragraphs.
|
|
204
|
+
* Paragraphs separated by blank lines inside the quote remain separate.
|
|
205
|
+
*/
|
|
206
|
+
function walkBlockquote (n: MdNode, file: string, source: string): QuoteBlock {
|
|
207
|
+
const inner = walkBlocks(n.children ?? [], file, 'blockquote', source)
|
|
208
|
+
return { type: 'quote', children: inner }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function walkList (n: MdNode, file: string): ListBlock {
|
|
212
|
+
const ordered = !!n.ordered
|
|
213
|
+
const items: Inline[][] = []
|
|
214
|
+
for (const li of n.children ?? []) {
|
|
215
|
+
const itemInlines: Inline[] = []
|
|
216
|
+
const firstBlocks = li.children ?? []
|
|
217
|
+
let emitted = 0
|
|
218
|
+
for (const b of firstBlocks) {
|
|
219
|
+
if (b.type === 'paragraph') {
|
|
220
|
+
if (emitted > 0) itemInlines.push({ type: 'text', value: '\n' })
|
|
221
|
+
itemInlines.push(...walkInlines(b.children ?? [], file))
|
|
222
|
+
emitted++
|
|
223
|
+
} else {
|
|
224
|
+
const err = new Error(
|
|
225
|
+
`[MDAST_UNSUPPORTED_NODE] Unsupported list-item child type '${b.type}' in ${file}. ` +
|
|
226
|
+
`Fail-fast: non-paragraph list item children are not supported.`
|
|
227
|
+
) as ParseError
|
|
228
|
+
err.code = 'MDAST_UNSUPPORTED_NODE'
|
|
229
|
+
err.file = file
|
|
230
|
+
throw err
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
items.push(itemInlines)
|
|
234
|
+
}
|
|
235
|
+
return { type: 'list', ordered, items }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function walkTable (n: MdNode, file: string): TableBlock {
|
|
239
|
+
const kids = n.children ?? []
|
|
240
|
+
const headers: TableCell[] = []
|
|
241
|
+
const rows: TableCell[][] = []
|
|
242
|
+
for (let i = 0; i < kids.length; i++) {
|
|
243
|
+
const row = kids[i]
|
|
244
|
+
const rowCells: TableCell[] = []
|
|
245
|
+
const cells = row?.children ?? []
|
|
246
|
+
for (const cell of cells) rowCells.push(walkInlines(cell.children ?? [], file))
|
|
247
|
+
if (i === 0) for (const c of rowCells) headers.push(c)
|
|
248
|
+
else rows.push(rowCells)
|
|
249
|
+
}
|
|
250
|
+
return { type: 'table', headers, rows }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function walkImage (n: MdNode): ImageBlock {
|
|
254
|
+
return { type: 'image', src: n.url ?? '', alt: n.alt ?? '' }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function walkCode (n: MdNode): CodeBlock {
|
|
258
|
+
return { type: 'code', language: n.lang ?? '', code: n.value ?? '' }
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function walkFormula (n: MdNode): FormulaBlock {
|
|
262
|
+
// Block-level LaTeX fence mdast node: value = latex body; display = true.
|
|
263
|
+
return { type: 'formula', latex: n.value ?? '', display: true }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Directive container → neutral ContainerBlock (6.3.12 frozen).
|
|
268
|
+
* Interpretation of hint/definition/example/question is deferred to
|
|
269
|
+
* the active domain parser; here the structure is preserved verbatim.
|
|
270
|
+
*/
|
|
271
|
+
function walkContainerDirective (n: MdNode, file: string, source: string): ContainerBlock {
|
|
272
|
+
const name = n.name ?? 'unknown'
|
|
273
|
+
const attrsIn = n.attributes ?? {}
|
|
274
|
+
const attrs: Record<string, string> = {}
|
|
275
|
+
for (const key of Object.keys(attrsIn)) {
|
|
276
|
+
const v = attrsIn[key]
|
|
277
|
+
attrs[key] = v == null ? '' : String(v)
|
|
278
|
+
}
|
|
279
|
+
const children = walkBlocks(n.children ?? [], file, 'container', source)
|
|
280
|
+
return { type: 'container', name, attrs, children }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Inline walkers ────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
function walkInlines (nodes: MdNode[], file: string): Inline[] {
|
|
286
|
+
const out: Inline[] = []
|
|
287
|
+
for (const n of nodes) {
|
|
288
|
+
switch (n.type) {
|
|
289
|
+
case 'text':
|
|
290
|
+
out.push({ type: 'text', value: n.value ?? '' })
|
|
291
|
+
break
|
|
292
|
+
case 'strong':
|
|
293
|
+
out.push({ type: 'bold', children: walkInlines(n.children ?? [], file) })
|
|
294
|
+
break
|
|
295
|
+
case 'emphasis':
|
|
296
|
+
out.push({ type: 'italic', children: walkInlines(n.children ?? [], file) })
|
|
297
|
+
break
|
|
298
|
+
case 'inlineCode':
|
|
299
|
+
out.push({ type: 'code', value: n.value ?? '' })
|
|
300
|
+
break
|
|
301
|
+
case 'link':
|
|
302
|
+
out.push({
|
|
303
|
+
type: 'link',
|
|
304
|
+
url: n.url ?? '',
|
|
305
|
+
children: walkInlines(n.children ?? [], file)
|
|
306
|
+
})
|
|
307
|
+
break
|
|
308
|
+
case 'image':
|
|
309
|
+
out.push({ type: 'text', value: n.alt ?? n.url ?? '' })
|
|
310
|
+
break
|
|
311
|
+
case NODE_FORMULA_INLINE:
|
|
312
|
+
out.push({ type: 'math' as const, latex: n.value ?? '' } as const)
|
|
313
|
+
break
|
|
314
|
+
case 'break':
|
|
315
|
+
case 'softbreak':
|
|
316
|
+
out.push({ type: 'text', value: '\n' })
|
|
317
|
+
break
|
|
318
|
+
case 'html':
|
|
319
|
+
out.push({ type: 'text', value: n.value ?? '' })
|
|
320
|
+
break
|
|
321
|
+
case 'linkReference':
|
|
322
|
+
out.push({
|
|
323
|
+
type: 'link',
|
|
324
|
+
url: n.identifier ?? '',
|
|
325
|
+
children: walkInlines(n.children ?? [], file)
|
|
326
|
+
})
|
|
327
|
+
break
|
|
328
|
+
case 'textDirective':
|
|
329
|
+
case 'leafDirective':
|
|
330
|
+
out.push(...walkInlines(n.children ?? [], file))
|
|
331
|
+
break
|
|
332
|
+
default:
|
|
333
|
+
if (n.children && Array.isArray(n.children)) {
|
|
334
|
+
out.push(...walkInlines(n.children, file))
|
|
335
|
+
}
|
|
336
|
+
break
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return out
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Keep imports used (silence warnings).
|
|
343
|
+
export type { ParseError }
|