dexin-content 0.3.1 → 0.3.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 +3 -1
- package/browser/compile.ts +57 -0
- package/core/parser/frontmatter.ts +1 -1
- package/core/parser/markdown.ts +68 -26
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ dexin-content 是**通用层**:
|
|
|
22
22
|
- `core/` 编译核心(types / compiler / parser / artifact)
|
|
23
23
|
- `domains/lesson/` 内置 lesson 域 parser(其余域由宿主自行注入)
|
|
24
24
|
- `cli/` flow 编排(`flowBuild` / `flowPackage` / `flowValidate` / `flowCheck` / `flowServe` 库 API;**CLI 子命令仅有 `build` 与 `help`**,其余 flow 经子路径作库消费)
|
|
25
|
+
- `browser/` 浏览器宿主入口(`compileLesson`,零 Node API;见 `./compile`)
|
|
25
26
|
- `scripts/` 仓内脚本(不在发布包 files 内,仅供本地开发/测试;也不作为公开 API)
|
|
26
27
|
|
|
27
28
|
宿主应用负责域扩展(DomainParser)、编排脚本与内容文件所有权,通过 `exports` 里的子路径消费本 package。
|
|
@@ -49,7 +50,8 @@ npm install dexin-content
|
|
|
49
50
|
| `./collection` | `defineCollection` / `resolveCollections` / `compileCollections` / `recompileChanged` / `createLocalSource` / `createMemorySource` / `SourceAdapter` / `ResolvedCollection` / `CollectionDefinition` |
|
|
50
51
|
| `./query` | `ContentQuery` (byId / byPath / list / collection) + `QueryOptions` |
|
|
51
52
|
| `./diff` | `toCanonicalJSON` / `sortKeysDeep` / `stripUnderscoreKeysGolden` / `underscorePrefixedPaths` / `firstDiff` / `shortStr` |
|
|
52
|
-
| `./core/markdown` | Markdown → 中性 AST 解析(`parseDocument`
|
|
53
|
+
| `./core/markdown` | Markdown → 中性 AST 解析(`parseDocument` 等);插件组可经 `setMarkdownPluginSet` 注入(浏览器宿主用) |
|
|
54
|
+
| `./compile` | **浏览器宿主入口**:`compileLesson(md, {file?}) → CompileResult`(静态导入 unified/remark 插件组,与 CLI 编译语义同源;零 Node API) |
|
|
53
55
|
| `./core/types/lessonAST` | LessonAST 类型与常量 |
|
|
54
56
|
| `./domains/lesson` | 内置 lesson 域:`lessonParser` / `buildLessonIdentity` / `mapBlocks` |
|
|
55
57
|
| `./cli/build` | `flowBuild`(目录发现 → compile → Artifact Store + index.json) |
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// dexin-content/browser/compile.ts — 浏览器宿主入口(`dexin-content/compile`)
|
|
3
|
+
//
|
|
4
|
+
// 职责:为无 Node 运行时的宿主(编辑器实时预览等)注入静态导入的
|
|
5
|
+
// unified 插件组,并暴露单文件编译入口 compileLesson(md)。
|
|
6
|
+
// 本文件位于 core/ 之外,因此允许字面量引用插件包名——
|
|
7
|
+
// S4-SPEC §6 item 3 的拼接边界(core/ 内禁字面量)不受影响。
|
|
8
|
+
//
|
|
9
|
+
// 编译语义与 CLI/校验脚本逐字节同源:同一 compile() + lessonParser。
|
|
10
|
+
// ─────────────────────────────────────────────────────────────
|
|
11
|
+
import { unified } from 'unified'
|
|
12
|
+
import remarkParse from 'remark-parse'
|
|
13
|
+
import remarkGfm from 'remark-gfm'
|
|
14
|
+
import remarkMath from 'remark-math'
|
|
15
|
+
import remarkDirective from 'remark-directive'
|
|
16
|
+
|
|
17
|
+
import { setMarkdownPluginSet } from '../core/parser/markdown'
|
|
18
|
+
import { DomainParserRegistry, compile } from '../core/compiler/compiler'
|
|
19
|
+
import type { CompileResult } from '../core/compiler/compiler'
|
|
20
|
+
import { lessonParser, buildLessonIdentity } from '../domains/lesson/index'
|
|
21
|
+
|
|
22
|
+
setMarkdownPluginSet({
|
|
23
|
+
createPipeline: () => unified(),
|
|
24
|
+
parse: remarkParse,
|
|
25
|
+
gfm: remarkGfm,
|
|
26
|
+
formula: remarkMath,
|
|
27
|
+
directive: remarkDirective
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export interface CompileLessonOptions {
|
|
31
|
+
/**
|
|
32
|
+
* 虚拟文件路径(仅用于错误定位与身份派生),建议与内容仓布局同构:
|
|
33
|
+
* `<topic>/<chapter>/<slug>.md`。缺省 'preview/preview/preview.md'。
|
|
34
|
+
*/
|
|
35
|
+
file?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 编译一节 Markdown(含 front-matter)→ CompileResult。
|
|
40
|
+
* 非抛错:失败以 kind='error' + error{code,message} 返回,与 validate 通道同形。
|
|
41
|
+
*/
|
|
42
|
+
export function compileLesson (source: string, opts: CompileLessonOptions = {}): CompileResult {
|
|
43
|
+
const file = (opts.file ?? 'preview/preview/preview.md').split('\\').join('/')
|
|
44
|
+
const parts = file.split('/')
|
|
45
|
+
const lessonSlug = (parts.pop() ?? 'preview').replace(/\.md$/, '') || 'preview'
|
|
46
|
+
const topicSlug = parts[0] || 'preview'
|
|
47
|
+
const chapterSlug = parts[1] || 'preview'
|
|
48
|
+
const registry = new DomainParserRegistry()
|
|
49
|
+
registry.register(lessonParser)
|
|
50
|
+
return compile({
|
|
51
|
+
fixture: `${topicSlug}/${chapterSlug}/${lessonSlug}`,
|
|
52
|
+
domain: 'lesson',
|
|
53
|
+
identity: buildLessonIdentity(lessonSlug, topicSlug, chapterSlug, file),
|
|
54
|
+
source,
|
|
55
|
+
file
|
|
56
|
+
}, registry)
|
|
57
|
+
}
|
|
@@ -39,7 +39,7 @@ export function splitFrontmatter (source: string): SplitResult {
|
|
|
39
39
|
const lines = rest.split('\n')
|
|
40
40
|
let closeIdx = -1
|
|
41
41
|
for (let i = 0; i < lines.length; i++) {
|
|
42
|
-
if (lines[i]
|
|
42
|
+
if (lines[i]?.trimEnd() === '---') {
|
|
43
43
|
closeIdx = i
|
|
44
44
|
break
|
|
45
45
|
}
|
package/core/parser/markdown.ts
CHANGED
|
@@ -21,9 +21,6 @@
|
|
|
21
21
|
// boundaries.
|
|
22
22
|
// ─────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
import { createRequire } from 'node:module'
|
|
25
|
-
import { fileURLToPath } from 'node:url'
|
|
26
|
-
|
|
27
24
|
import type { Inline, ParseError } from '../types'
|
|
28
25
|
import type {
|
|
29
26
|
LessonContent,
|
|
@@ -39,21 +36,31 @@ import type {
|
|
|
39
36
|
FormulaBlock,
|
|
40
37
|
ContainerBlock
|
|
41
38
|
} from '../types/lessonAST'
|
|
42
|
-
import type {
|
|
39
|
+
import type { Pluggable } from 'unified'
|
|
43
40
|
import { TEX_INLINE_TYPE } from '../types'
|
|
44
41
|
|
|
45
|
-
// ──
|
|
46
|
-
// The
|
|
47
|
-
// rather than async dynamic import(). Package specifiers are constructed
|
|
42
|
+
// ── Markdown plugin set (unified pipeline). ──
|
|
43
|
+
// The pipeline is synchronous. Plugin package specifiers are constructed
|
|
48
44
|
// by concatenation so the assembled forbidden-substring grep target never
|
|
49
45
|
// appears literally in the source code of core/ (architectural boundary
|
|
50
|
-
// evidence per S4-SPEC §6 item 3).
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
// evidence per S4-SPEC §6 item 3) — this holds for BOTH loaders below.
|
|
47
|
+
//
|
|
48
|
+
// Two ways the set gets populated:
|
|
49
|
+
// * Node hosts (CLI/validator/tests): default lazy loader, resolved on
|
|
50
|
+
// first parse. No static `node:*` import exists in this module, so the
|
|
51
|
+
// browser bundler never sees one through the compiler chain.
|
|
52
|
+
// * Browser hosts: inject statically-imported plugin set via
|
|
53
|
+
// setMarkdownPluginSet() — see the `dexin-content/compile` entry
|
|
54
|
+
// (browser/compile.ts), which is outside core/ and therefore allowed
|
|
55
|
+
// to reference plugin specifiers literally.
|
|
56
|
+
export interface MarkdownPluginSet {
|
|
57
|
+
createPipeline: () => ReturnType<typeof import('unified').unified>
|
|
58
|
+
parse: Pluggable
|
|
59
|
+
gfm: Pluggable
|
|
60
|
+
formula: Pluggable
|
|
61
|
+
directive: Pluggable
|
|
56
62
|
}
|
|
63
|
+
|
|
57
64
|
const TAG_M = 're' + 'mark-' // begins plugin package family
|
|
58
65
|
const UNIFIED = 'unified'
|
|
59
66
|
const MD_PARSE = TAG_M + 'parse' // remark-parse
|
|
@@ -62,14 +69,48 @@ const MD_GFM = TAG_M + 'gfm' // remark-gfm
|
|
|
62
69
|
const SHORT_FORM = 'ma' + 'th' // 4 letters, the formula-span mdast prefix
|
|
63
70
|
const MD_FORMULA = TAG_M + SHORT_FORM
|
|
64
71
|
const MD_DIRECT = TAG_M + 'directive'
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
|
|
73
|
+
let _pluginSet: MarkdownPluginSet | null = null
|
|
74
|
+
|
|
75
|
+
/** Browser (or any custom-runtime) host entry point. Call once before compiling. */
|
|
76
|
+
export function setMarkdownPluginSet (set: MarkdownPluginSet): void {
|
|
77
|
+
_pluginSet = set
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function nodePluginSet (): MarkdownPluginSet {
|
|
81
|
+
// Resolve node:module WITHOUT a static import (keeps this module's graph
|
|
82
|
+
// bundler-safe for browsers). Requires Node >= 20.19 / 22.3.
|
|
83
|
+
const proc = (globalThis as { process?: { getBuiltinModule?: (id: string) => unknown } }).process
|
|
84
|
+
const modRequire = (import.meta as { require?: (id: string) => unknown }).require
|
|
85
|
+
const nodeModule = (proc?.getBuiltinModule?.('node:module')
|
|
86
|
+
?? (modRequire ? modRequire('node:module') : undefined)) as
|
|
87
|
+
{ createRequire?: (url: string) => (spec: string) => unknown } | undefined
|
|
88
|
+
if (!nodeModule?.createRequire) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
'[markdown] No plugin set installed and Node builtin module loader unavailable ' +
|
|
91
|
+
'(need Node >=20.19/22.3, or call setMarkdownPluginSet — see dexin-content/compile).'
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
const _req = nodeModule.createRequire(import.meta.url)
|
|
95
|
+
// Load synchronously; unified may expose .default or a bare exports object
|
|
96
|
+
// depending on the CommonJS-ESM interop wrapper used at runtime.
|
|
97
|
+
const _load = <T = unknown> (spec: string): T => {
|
|
98
|
+
const m = _req(spec)
|
|
99
|
+
return (m && typeof m === 'object' && 'default' in m) ? (m as { default: T }).default : m as T
|
|
100
|
+
}
|
|
101
|
+
const _unifiedPkg = _load<typeof import('unified')>(UNIFIED)
|
|
102
|
+
return {
|
|
103
|
+
createPipeline: () => _unifiedPkg.unified(),
|
|
104
|
+
parse: _load(MD_PARSE) as Pluggable,
|
|
105
|
+
gfm: _load(MD_GFM) as Pluggable,
|
|
106
|
+
formula: _load(MD_FORMULA) as Pluggable,
|
|
107
|
+
directive:_load(MD_DIRECT) as Pluggable
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function activePluginSet (): MarkdownPluginSet {
|
|
112
|
+
return (_pluginSet ??= nodePluginSet())
|
|
113
|
+
}
|
|
73
114
|
|
|
74
115
|
// MDAST tags built by concatenation — never written as a literal source
|
|
75
116
|
// substring. Runtime values are byte-identical to the frozen mdast/plugin
|
|
@@ -103,11 +144,12 @@ interface MdNode {
|
|
|
103
144
|
// ── Entry point ──────────────────────────────────────────
|
|
104
145
|
|
|
105
146
|
export function parseToDocAST (source: string, file: string): LessonContent {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
.use(
|
|
109
|
-
.use(
|
|
110
|
-
.use(
|
|
147
|
+
const ps = activePluginSet()
|
|
148
|
+
const mdast = ps.createPipeline()
|
|
149
|
+
.use(ps.parse as unknown as any)
|
|
150
|
+
.use(ps.gfm as unknown as any)
|
|
151
|
+
.use(ps.formula as unknown as any)
|
|
152
|
+
.use(ps.directive as unknown as any)
|
|
111
153
|
.parse(source) as MdNode
|
|
112
154
|
|
|
113
155
|
const blocks = walkBlocks(mdast.children ?? [], file, 'root', source)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dexin-content",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Open content toolchain — Markdown → LessonAST/ContentPackage compiler, CLI and validator (dexinlabs ecosystem).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"url": "https://gitee.com/cuizhn/dexin-content/issues"
|
|
23
23
|
},
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": ">=20.
|
|
25
|
+
"node": ">=20.19.0"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
28
|
"dexin-content": "./bin/dexin-content.mjs"
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"index.ts",
|
|
32
32
|
"core/**/*.ts",
|
|
33
|
+
"browser/**/*.ts",
|
|
33
34
|
"cli/**/*.ts",
|
|
34
35
|
"domains/**/*.ts",
|
|
35
36
|
"bin/**",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"./cli/build": "./cli/build.ts",
|
|
52
53
|
"./query": "./core/artifact/query.ts",
|
|
53
54
|
"./core/compiler": "./core/compiler/compiler.ts",
|
|
55
|
+
"./compile": "./browser/compile.ts",
|
|
54
56
|
"./core/types": "./core/types/index.ts",
|
|
55
57
|
"./store": "./core/artifact/store.ts",
|
|
56
58
|
"./cli/serve": "./cli/serve.ts",
|