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 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].trimEnd() === '---') {
42
+ if (lines[i]?.trimEnd() === '---') {
43
43
  closeIdx = i
44
44
  break
45
45
  }
@@ -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 { Plugin, Pluggable } from 'unified'
39
+ import type { Pluggable } from 'unified'
43
40
  import { TEX_INLINE_TYPE } from '../types'
44
41
 
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
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
- 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
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
- // 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
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 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)
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.1",
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.0.0"
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",