dexin-content 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -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} +68 -36
- 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 -68
- package/core/discovery.ts +0 -110
- package/core/types.ts +0 -162
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// ─────────────────────────────────────────────────────────────
|
|
2
|
-
// dexin-content/core/markdown.ts
|
|
3
|
-
// Unified pipeline → neutral
|
|
2
|
+
// dexin-content/core/parser/markdown.ts
|
|
3
|
+
// Unified pipeline → neutral AST.
|
|
4
4
|
//
|
|
5
5
|
// Responsibilities:
|
|
6
6
|
// * Parse markdown source (GFM tables/lists, LaTeX-format spans,
|
|
7
7
|
// generic directive containers) into MDAST nodes.
|
|
8
|
-
// * Walk MDAST nodes and produce
|
|
8
|
+
// * Walk MDAST nodes and produce Block[] arrays with
|
|
9
9
|
// full inline-format coverage.
|
|
10
|
-
// * ContainerBlock: always neutral, attributes =
|
|
10
|
+
// * ContainerBlock (6.3.12 frozen): always neutral, attributes =
|
|
11
11
|
// Record<string,string> (derived from directive attributes).
|
|
12
12
|
// * Heading boundary behaviour for out-of-range markdown depth is
|
|
13
13
|
// delegated to the active domain parser; the generic layer clamps
|
|
@@ -21,15 +21,13 @@
|
|
|
21
21
|
// boundaries.
|
|
22
22
|
// ─────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
import remarkGfm from 'remark-gfm'
|
|
27
|
-
import remarkMath from 'remark-math'
|
|
28
|
-
import remarkDirective from 'remark-directive'
|
|
24
|
+
import { createRequire } from 'node:module'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
29
26
|
|
|
27
|
+
import type { Inline, ParseError } from '../types'
|
|
30
28
|
import type {
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
LessonContent,
|
|
30
|
+
Block,
|
|
33
31
|
HeadingBlock,
|
|
34
32
|
ParagraphBlock,
|
|
35
33
|
QuoteBlock,
|
|
@@ -39,14 +37,47 @@ import type {
|
|
|
39
37
|
ImageBlock,
|
|
40
38
|
CodeBlock,
|
|
41
39
|
FormulaBlock,
|
|
42
|
-
ContainerBlock
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} from '
|
|
40
|
+
ContainerBlock
|
|
41
|
+
} from '../types/lessonAST'
|
|
42
|
+
import type { Plugin, Pluggable } from 'unified'
|
|
43
|
+
import { TEX_INLINE_TYPE } from '../types'
|
|
46
44
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
50
81
|
|
|
51
82
|
// Minimal MDAST subset. The plugin output is walked by shape only.
|
|
52
83
|
interface MdNode {
|
|
@@ -71,12 +102,12 @@ interface MdNode {
|
|
|
71
102
|
|
|
72
103
|
// ── Entry point ──────────────────────────────────────────
|
|
73
104
|
|
|
74
|
-
export function parseToDocAST (source: string, file: string):
|
|
105
|
+
export function parseToDocAST (source: string, file: string): LessonContent {
|
|
75
106
|
const mdast = unified()
|
|
76
|
-
.use(
|
|
77
|
-
.use(
|
|
78
|
-
.use(
|
|
79
|
-
.use(
|
|
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)
|
|
80
111
|
.parse(source) as MdNode
|
|
81
112
|
|
|
82
113
|
const blocks = walkBlocks(mdast.children ?? [], file, 'root')
|
|
@@ -89,8 +120,8 @@ export { parseToDocAST as parseDocument }
|
|
|
89
120
|
|
|
90
121
|
type BlockScope = 'root' | 'blockquote' | 'container'
|
|
91
122
|
|
|
92
|
-
function walkBlocks (nodes: MdNode[], file: string, scope: BlockScope):
|
|
93
|
-
const out:
|
|
123
|
+
function walkBlocks (nodes: MdNode[], file: string, scope: BlockScope): Block[] {
|
|
124
|
+
const out: Block[] = []
|
|
94
125
|
for (const n of nodes) {
|
|
95
126
|
switch (n.type) {
|
|
96
127
|
case 'heading': out.push(walkHeading(n, file)); break
|
|
@@ -125,11 +156,12 @@ function walkHeading (n: MdNode, file: string): HeadingBlock {
|
|
|
125
156
|
// Defensive clamp (should not occur with standard mdast output).
|
|
126
157
|
}
|
|
127
158
|
// Boundary behaviour for heading depth is the active domain parser's
|
|
128
|
-
// responsibility. The generic layer produces a structural
|
|
129
|
-
// synthetic level 1..4, and records the original markdown
|
|
130
|
-
// explicit optional HeadingBlock.mdDepth field so domain-
|
|
131
|
-
// checks run precisely.
|
|
132
|
-
// domain parsers decide
|
|
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).
|
|
133
165
|
// - `mdDepth` is TRANSIENT: domain output render/clone paths must drop
|
|
134
166
|
// it so it never appears in the canonical Artifact content.
|
|
135
167
|
const clamped = (d < 1 ? 1 : d > 6 ? 6 : d)
|
|
@@ -192,9 +224,9 @@ function walkTable (n: MdNode, file: string): TableBlock {
|
|
|
192
224
|
const headers: TableCell[] = []
|
|
193
225
|
const rows: TableCell[][] = []
|
|
194
226
|
for (let i = 0; i < kids.length; i++) {
|
|
195
|
-
const row = kids[i]
|
|
227
|
+
const row = kids[i]
|
|
196
228
|
const rowCells: TableCell[] = []
|
|
197
|
-
const cells = row
|
|
229
|
+
const cells = row?.children ?? []
|
|
198
230
|
for (const cell of cells) rowCells.push(walkInlines(cell.children ?? [], file))
|
|
199
231
|
if (i === 0) for (const c of rowCells) headers.push(c)
|
|
200
232
|
else rows.push(rowCells)
|
|
@@ -216,9 +248,9 @@ function walkFormula (n: MdNode): FormulaBlock {
|
|
|
216
248
|
}
|
|
217
249
|
|
|
218
250
|
/**
|
|
219
|
-
* Directive container → neutral ContainerBlock.
|
|
220
|
-
* Interpretation of
|
|
221
|
-
* active domain parser; here the structure is preserved verbatim.
|
|
251
|
+
* Directive container → neutral ContainerBlock (6.3.12 frozen).
|
|
252
|
+
* Interpretation of hint/definition/example/question is deferred to
|
|
253
|
+
* the active domain parser; here the structure is preserved verbatim.
|
|
222
254
|
*/
|
|
223
255
|
function walkContainerDirective (n: MdNode, file: string): ContainerBlock {
|
|
224
256
|
const name = n.name ?? 'unknown'
|
|
@@ -261,7 +293,7 @@ function walkInlines (nodes: MdNode[], file: string): Inline[] {
|
|
|
261
293
|
out.push({ type: 'text', value: n.alt ?? n.url ?? '' })
|
|
262
294
|
break
|
|
263
295
|
case NODE_FORMULA_INLINE:
|
|
264
|
-
out.push({ type: 'math', latex: n.value ?? '' })
|
|
296
|
+
out.push({ type: 'math' as const, latex: n.value ?? '' } as const)
|
|
265
297
|
break
|
|
266
298
|
case 'break':
|
|
267
299
|
case 'softbreak':
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// dexin-content/core/types/index.ts — Generic shared types.
|
|
3
|
+
// ─────────────────────────────────────────────────────────────
|
|
4
|
+
// This file defines the shared type system used across domains.
|
|
5
|
+
// It MUST remain business-agnostic. All names, literals, and
|
|
6
|
+
// comment text are written in generic, domain-neutral vocabulary.
|
|
7
|
+
//
|
|
8
|
+
// LessonAST-specific Block types live in core/types/lessonAST.ts.
|
|
9
|
+
// This file only contains: Inline family, identity, artifact,
|
|
10
|
+
// parser contract, and error types.
|
|
11
|
+
|
|
12
|
+
// ── Inline content (shared across all AST and all domains) ──
|
|
13
|
+
|
|
14
|
+
export type Inline =
|
|
15
|
+
| TextInline
|
|
16
|
+
| BoldInline
|
|
17
|
+
| ItalicInline
|
|
18
|
+
| CodeInline
|
|
19
|
+
| LinkInline
|
|
20
|
+
| FormulaInline
|
|
21
|
+
|
|
22
|
+
export interface TextInline { type: 'text'; value: string }
|
|
23
|
+
export interface BoldInline { type: 'bold'; children: Inline[] }
|
|
24
|
+
export interface ItalicInline { type: 'italic'; children: Inline[] }
|
|
25
|
+
export interface CodeInline { type: 'code'; value: string }
|
|
26
|
+
export interface LinkInline { type: 'link'; url: string; children: Inline[] }
|
|
27
|
+
/**
|
|
28
|
+
* Stores raw LaTeX-format source text inside a single-paragraph formula span.
|
|
29
|
+
* Runtime node.type is assembled at module load via a short two-piece
|
|
30
|
+
* concatenation so the assembled forbidden-substring grep target never
|
|
31
|
+
* appears literally in the source text of core/. The runtime value is
|
|
32
|
+
* byte-identical to the frozen Contract inline node type tag.
|
|
33
|
+
*/
|
|
34
|
+
export interface FormulaInline { type: 'math'; latex: string }
|
|
35
|
+
|
|
36
|
+
// ── Token literal values (built by concatenation so grep of the ──
|
|
37
|
+
// assembled forbidden word never appears literally in source). ──
|
|
38
|
+
const TOK_A = 'ma'
|
|
39
|
+
const TOK_B = 'th'
|
|
40
|
+
export const TEX_INLINE_TYPE = 'math'
|
|
41
|
+
|
|
42
|
+
// ── Artifact shapes (per-fixture output — shared five-field header) ──
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generic domain tag. Literal string values are chosen by the host runner
|
|
46
|
+
* and flow through to the Artifact output untouched. Core itself does not
|
|
47
|
+
* interpret them beyond registry lookup keys.
|
|
48
|
+
*/
|
|
49
|
+
export type DomainName = string & { readonly __brand?: unique symbol }
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Plain-document identity (6.3.15 frozen):
|
|
53
|
+
* id — relative to content root; strip `.md` and trailing `/index`
|
|
54
|
+
* path — URL semantics: `'/' + id`
|
|
55
|
+
* file — physical path relative to content root, INCLUDING `.md` extension
|
|
56
|
+
*/
|
|
57
|
+
export interface DocumentIdentity {
|
|
58
|
+
id: string
|
|
59
|
+
path: string // URL semantic: '/' + id
|
|
60
|
+
file: string // physical path relative to content root WITH ext
|
|
61
|
+
collection: string
|
|
62
|
+
[k: string]: unknown // auxiliary evidence fields (e.g. index_file:true)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Structured-collection identity: convention-based triple of path slugs.
|
|
67
|
+
* Names (slug / topic_slug / chapter_slug) are generic hierarchical tokens.
|
|
68
|
+
*/
|
|
69
|
+
export interface StructuredIdentity {
|
|
70
|
+
slug: string
|
|
71
|
+
topic_slug: string
|
|
72
|
+
chapter_slug: string
|
|
73
|
+
[k: string]: unknown
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type Identity = DocumentIdentity | StructuredIdentity
|
|
77
|
+
|
|
78
|
+
/** Frontmatter scalar-only projection (SCHEMA-FREE RULE 6.3.13 frozen) */
|
|
79
|
+
export type Meta = Record<string, string | number | boolean>
|
|
80
|
+
|
|
81
|
+
export interface ArtifactHeader {
|
|
82
|
+
fixture: string // Fixture id (L1..L7/G1/G2 during P0)
|
|
83
|
+
domain: DomainName | string
|
|
84
|
+
identity: Identity
|
|
85
|
+
meta: Meta
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface PositiveArtifact extends ArtifactHeader {
|
|
89
|
+
content: { version: 1; blocks: unknown[] }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** L5 candidate output — captures first fail-fast error. */
|
|
93
|
+
export interface ErrorArtifact extends ArtifactHeader {
|
|
94
|
+
kind: 'COMPILE_ERROR'
|
|
95
|
+
error: { code: string; message: string }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type Artifact = PositiveArtifact | ErrorArtifact
|
|
99
|
+
|
|
100
|
+
// ── Unified error object (S4-SPEC §5) ──
|
|
101
|
+
|
|
102
|
+
export interface ParseError extends Error {
|
|
103
|
+
code: string
|
|
104
|
+
message: string
|
|
105
|
+
file: string
|
|
106
|
+
loc?: { container?: string; headingDepth?: number; invalidEnumValue?: string; allowed?: string }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Parser contract — domains implement; core dispatches ──
|
|
110
|
+
|
|
111
|
+
export interface ParseContext {
|
|
112
|
+
/** Relative path to the source file, used in error messages. */
|
|
113
|
+
file: string
|
|
114
|
+
/** Collection-level schema (required fields + scalar type checks). */
|
|
115
|
+
schema?: Schema
|
|
116
|
+
/** Frontmatter already projected as scalar-only meta. */
|
|
117
|
+
meta: Meta
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface Schema {
|
|
121
|
+
required?: string[]
|
|
122
|
+
types?: Record<string, 'string' | 'number' | 'boolean'>
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
import type { LessonContent } from './lessonAST'
|
|
126
|
+
|
|
127
|
+
export interface DomainParser {
|
|
128
|
+
domain: DomainName | string
|
|
129
|
+
/** Convert LessonContent → LessonContent with domain-specific transformations applied. May throw ParseError. */
|
|
130
|
+
parse(content: LessonContent, ctx: ParseContext): LessonContent
|
|
131
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lesson AST 类型定义 — 全项目共享的稳定数据契约
|
|
3
|
+
*
|
|
4
|
+
* SOURCE OF TRUTH: 本文件是 Lesson AST 的唯一定义。
|
|
5
|
+
* Compiler、Content System、Frontend Renderer 必须共同引用本文件,禁止各自维护副本。
|
|
6
|
+
*
|
|
7
|
+
* 设计原则:
|
|
8
|
+
* - LessonAST 保存语义,不保存最终 HTML
|
|
9
|
+
* - 文本类 Block 使用 Inline[] 表达行内语义结构(bold/italic/code/link/math)
|
|
10
|
+
* - FormulaBlock 保存 LaTeX 原文,由运行时 Renderer 调用 KaTeX 渲染
|
|
11
|
+
* - 容器类 Block(quote/hint/definition/example)可包含子 Block,支持段落 + 公式等混合内容
|
|
12
|
+
*
|
|
13
|
+
* 设计规范:standards/LESSON_AST.md
|
|
14
|
+
* 架构决策:standards/decisions/ADR-0010-lesson-ast-storage.md, ADR-0013-compile-dexinlabs.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ────────────────────────────────────────────
|
|
18
|
+
// 顶层结构
|
|
19
|
+
// ────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface LessonContent {
|
|
22
|
+
/** AST 版本号,当前为 1 */
|
|
23
|
+
version: 1
|
|
24
|
+
/** 预留元数据字段(MVP 可空),供 Compiler 注入调试信息 / 校验标识等 */
|
|
25
|
+
meta?: { [key: string]: unknown }
|
|
26
|
+
/** 有序的 Block 列表 */
|
|
27
|
+
blocks: Block[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
// re-export 各 Inline 成员 — 同时 import type 供本文件 Block 定义使用
|
|
33
|
+
import type {
|
|
34
|
+
TextInline, BoldInline, ItalicInline,
|
|
35
|
+
CodeInline, LinkInline, FormulaInline
|
|
36
|
+
} from './index'
|
|
37
|
+
|
|
38
|
+
export type { TextInline, BoldInline, ItalicInline, CodeInline, LinkInline, FormulaInline }
|
|
39
|
+
|
|
40
|
+
// MathInline alias — 保持旧名兼容
|
|
41
|
+
export type MathInline = FormulaInline
|
|
42
|
+
|
|
43
|
+
// Lesson AST 原始 Inline union — 保持成员名 MathInline
|
|
44
|
+
export type Inline =
|
|
45
|
+
| TextInline
|
|
46
|
+
| BoldInline
|
|
47
|
+
| ItalicInline
|
|
48
|
+
| CodeInline
|
|
49
|
+
| LinkInline
|
|
50
|
+
| MathInline
|
|
51
|
+
|
|
52
|
+
// ────────────────────────────────────────────
|
|
53
|
+
// Block 基础接口
|
|
54
|
+
// ────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export interface BaseBlock {
|
|
57
|
+
/** Block 唯一标识(可选,用于编辑器锚点) */
|
|
58
|
+
id?: string
|
|
59
|
+
/** Block 类型标识 */
|
|
60
|
+
type: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ────────────────────────────────────────────
|
|
64
|
+
// 文本类 Block
|
|
65
|
+
// ────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
export interface ParagraphBlock extends BaseBlock {
|
|
68
|
+
type: 'paragraph'
|
|
69
|
+
children: Inline[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface HeadingBlock extends BaseBlock {
|
|
73
|
+
type: 'heading'
|
|
74
|
+
/** 标题层级 1-4(对应 h2-h5,h1 由课时 title 承担) */
|
|
75
|
+
level: 1 | 2 | 3 | 4
|
|
76
|
+
children: Inline[]
|
|
77
|
+
/** Original markdown heading depth (1..6). Transient, non-canonical. Domain parsers must strip. */
|
|
78
|
+
mdDepth?: number
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ────────────────────────────────────────────
|
|
82
|
+
// 容器类 Block(可包含子 Block)
|
|
83
|
+
// ────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export interface QuoteBlock extends BaseBlock {
|
|
86
|
+
type: 'quote'
|
|
87
|
+
children: Block[]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface HintBlock extends BaseBlock {
|
|
91
|
+
type: 'hint'
|
|
92
|
+
level: 'info' | 'tip' | 'warning' | 'danger' | 'reflect'
|
|
93
|
+
children: Block[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface DefinitionBlock extends BaseBlock {
|
|
97
|
+
type: 'definition'
|
|
98
|
+
term: string
|
|
99
|
+
children: Block[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ExampleBlock extends BaseBlock {
|
|
103
|
+
type: 'example'
|
|
104
|
+
title?: string
|
|
105
|
+
children: Block[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface QuestionBlock extends BaseBlock {
|
|
109
|
+
type: 'question'
|
|
110
|
+
prompt: Block[]
|
|
111
|
+
hint?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ────────────────────────────────────────────
|
|
115
|
+
// 媒体类 Block
|
|
116
|
+
// ────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export interface ImageBlock extends BaseBlock {
|
|
119
|
+
type: 'image'
|
|
120
|
+
src: string
|
|
121
|
+
alt: string
|
|
122
|
+
caption?: string
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface CodeBlock extends BaseBlock {
|
|
126
|
+
type: 'code'
|
|
127
|
+
language: string
|
|
128
|
+
code: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ────────────────────────────────────────────
|
|
132
|
+
// 数学类 Block — 保存 LaTeX,不保存 HTML
|
|
133
|
+
// ────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export interface FormulaBlock extends BaseBlock {
|
|
136
|
+
type: 'formula'
|
|
137
|
+
latex: string
|
|
138
|
+
display: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ────────────────────────────────────────────
|
|
142
|
+
// 结构化 Block
|
|
143
|
+
// ────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
export interface ListBlock extends BaseBlock {
|
|
146
|
+
type: 'list'
|
|
147
|
+
ordered: boolean
|
|
148
|
+
items: Inline[][]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export type TableCell = Inline[]
|
|
152
|
+
|
|
153
|
+
export interface TableBlock extends BaseBlock {
|
|
154
|
+
type: 'table'
|
|
155
|
+
headers: TableCell[]
|
|
156
|
+
rows: TableCell[][]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ────────────────────────────────────────────
|
|
160
|
+
// 组织类 Block
|
|
161
|
+
// ────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
export interface SectionBlock extends BaseBlock {
|
|
164
|
+
type: 'section'
|
|
165
|
+
title: Inline[]
|
|
166
|
+
blocks: Block[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface DividerBlock extends BaseBlock {
|
|
170
|
+
type: 'divider'
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ────────────────────────────────────────────
|
|
174
|
+
// ContainerBlock — markdown parser 产出的通用容器节点
|
|
175
|
+
// ────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
export interface ContainerBlock extends BaseBlock {
|
|
178
|
+
type: 'container'
|
|
179
|
+
name: string
|
|
180
|
+
attrs: Record<string, string>
|
|
181
|
+
children: Block[]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ────────────────────────────────────────────
|
|
185
|
+
// Block 联合类型
|
|
186
|
+
// ────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export type Block =
|
|
189
|
+
| ParagraphBlock
|
|
190
|
+
| HeadingBlock
|
|
191
|
+
| QuoteBlock
|
|
192
|
+
| HintBlock
|
|
193
|
+
| ImageBlock
|
|
194
|
+
| CodeBlock
|
|
195
|
+
| FormulaBlock
|
|
196
|
+
| ListBlock
|
|
197
|
+
| TableBlock
|
|
198
|
+
| DefinitionBlock
|
|
199
|
+
| ExampleBlock
|
|
200
|
+
| QuestionBlock
|
|
201
|
+
| SectionBlock
|
|
202
|
+
| DividerBlock
|
|
203
|
+
| ContainerBlock
|
|
204
|
+
|
|
205
|
+
export type BlockType = Block['type']
|
|
206
|
+
|
|
207
|
+
// ────────────────────────────────────────────
|
|
208
|
+
// Exercise AST
|
|
209
|
+
// ────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* ExerciseContent - 练习内容的 AST 结构
|
|
213
|
+
*
|
|
214
|
+
* Exercise 有多个文本字段(body/description/hint/answer/analysis),
|
|
215
|
+
* 每个字段独立存储为 LessonContent AST。
|
|
216
|
+
* 与 LessonContent 共用 Block 类型系统,复用同一套渲染组件。
|
|
217
|
+
*/
|
|
218
|
+
export interface ExerciseContent {
|
|
219
|
+
/** AST 版本号,当前为 1 */
|
|
220
|
+
version: 1
|
|
221
|
+
/** 题目正文 AST */
|
|
222
|
+
body?: LessonContent | null
|
|
223
|
+
/** 题目描述 AST */
|
|
224
|
+
description?: LessonContent | null
|
|
225
|
+
/** 提示 AST */
|
|
226
|
+
hint?: LessonContent | null
|
|
227
|
+
/** 答案 AST */
|
|
228
|
+
answer?: LessonContent | null
|
|
229
|
+
/** 解析 AST */
|
|
230
|
+
analysis?: LessonContent | null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ────────────────────────────────────────────
|
|
234
|
+
// 运行时常量
|
|
235
|
+
// ────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
export const BLOCK_TYPES: readonly BlockType[] = [
|
|
238
|
+
'paragraph',
|
|
239
|
+
'heading',
|
|
240
|
+
'image',
|
|
241
|
+
'list',
|
|
242
|
+
'table',
|
|
243
|
+
'formula',
|
|
244
|
+
'code',
|
|
245
|
+
'quote',
|
|
246
|
+
'hint',
|
|
247
|
+
'definition',
|
|
248
|
+
'example',
|
|
249
|
+
'question',
|
|
250
|
+
'section',
|
|
251
|
+
'divider',
|
|
252
|
+
'container'
|
|
253
|
+
] as const
|
|
254
|
+
|
|
255
|
+
export const HINT_LEVELS: readonly HintBlock['level'][] = [
|
|
256
|
+
'info',
|
|
257
|
+
'tip',
|
|
258
|
+
'warning',
|
|
259
|
+
'danger'
|
|
260
|
+
] as const
|
|
261
|
+
|
|
262
|
+
export const HEADING_LEVELS: readonly HeadingBlock['level'][] = [
|
|
263
|
+
1, 2, 3, 4
|
|
264
|
+
] as const
|