dexin-content 0.2.0 → 0.3.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/README.md +121 -112
- package/cli/build.ts +4 -4
- package/cli/main.ts +197 -207
- package/core/artifact/store.ts +2 -2
- package/core/parser/markdown.ts +343 -327
- package/index.ts +57 -61
- package/package.json +2 -3
- package/core/validator/integrity.ts +0 -199
- package/domains/document/identity.ts +0 -27
- package/domains/document/index.ts +0 -5
- package/domains/document/parser.ts +0 -105
package/cli/main.ts
CHANGED
|
@@ -1,207 +1,197 @@
|
|
|
1
|
-
#!/usr/bin/env tsx
|
|
2
|
-
// ─────────────────────────────────────────────────────────────
|
|
3
|
-
// dexin-content CLI main entry
|
|
4
|
-
//
|
|
5
|
-
// Modes:
|
|
6
|
-
// Single-file: tsx cli/main.ts build <file.md> --domain lesson --out <path>
|
|
7
|
-
// Directory: tsx cli/main.ts build <dir> --out <dir> [--manifest <path>]
|
|
8
|
-
// ─────────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
11
|
-
import { existsSync } from 'node:fs'
|
|
12
|
-
import { resolve, isAbsolute, dirname, basename } from 'node:path'
|
|
13
|
-
|
|
14
|
-
import { DomainParserRegistry, compile } from '../core/compiler/compiler'
|
|
15
|
-
import type { CompileInput } from '../core/compiler/compiler'
|
|
16
|
-
import { lessonParser, buildLessonIdentity } from '../domains/lesson/index'
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
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
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
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
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
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
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
await runSingleFile(target, domain, args.out as string | undefined)
|
|
200
|
-
} else {
|
|
201
|
-
await runDirectory(target, args)
|
|
202
|
-
}
|
|
203
|
-
} else {
|
|
204
|
-
console.error(`Unknown command: ${command}`)
|
|
205
|
-
printHelp()
|
|
206
|
-
process.exit(1)
|
|
207
|
-
}
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
// ─────────────────────────────────────────────────────────────
|
|
3
|
+
// dexin-content CLI main entry
|
|
4
|
+
//
|
|
5
|
+
// Modes:
|
|
6
|
+
// Single-file: tsx cli/main.ts build <file.md> --domain lesson --out <path>
|
|
7
|
+
// Directory: tsx cli/main.ts build <dir> --out <dir> [--manifest <path>]
|
|
8
|
+
// ─────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
11
|
+
import { existsSync } from 'node:fs'
|
|
12
|
+
import { resolve, isAbsolute, dirname, basename } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { DomainParserRegistry, compile } from '../core/compiler/compiler'
|
|
15
|
+
import type { CompileInput } from '../core/compiler/compiler'
|
|
16
|
+
import { lessonParser, buildLessonIdentity } from '../domains/lesson/index'
|
|
17
|
+
import { flowBuild } from './build'
|
|
18
|
+
import { flowPackage } from './package'
|
|
19
|
+
|
|
20
|
+
type CmdArgs = Record<string, string | boolean>
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv: string[]): { command: string; args: CmdArgs; positionals: string[] } {
|
|
23
|
+
const command = argv[0] ?? ''
|
|
24
|
+
const positionals: string[] = []
|
|
25
|
+
const args: CmdArgs = {}
|
|
26
|
+
for (let i = 1; i < argv.length; i++) {
|
|
27
|
+
const a = argv[i]
|
|
28
|
+
if (a.startsWith('--')) {
|
|
29
|
+
const key = a.slice(2)
|
|
30
|
+
const next = argv[i + 1]
|
|
31
|
+
if (next && !next.startsWith('--')) {
|
|
32
|
+
args[key] = next; i++
|
|
33
|
+
} else {
|
|
34
|
+
args[key] = true
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
positionals.push(a)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { command, args, positionals }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`dexin-content CLI — Independent content compilation tool
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
dexin-content <command> [options] (npm bin; or locally: npx tsx cli/main.ts)
|
|
48
|
+
|
|
49
|
+
Commands:
|
|
50
|
+
build Compile markdown to artifacts
|
|
51
|
+
help Show this help
|
|
52
|
+
|
|
53
|
+
Build modes:
|
|
54
|
+
Single-file:
|
|
55
|
+
dexin-content build <file.md> --domain lesson --out <path>
|
|
56
|
+
|
|
57
|
+
Directory tree:
|
|
58
|
+
dexin-content build <dir> --out <dir> [--manifest <path>] [--lessons <dir>]
|
|
59
|
+
|
|
60
|
+
Options:
|
|
61
|
+
--domain <name> Domain parser (lesson). Required for single-file mode.
|
|
62
|
+
--out <path> Output path (file for single, directory for batch).
|
|
63
|
+
--manifest <path> Content manifest JSON for packaging.
|
|
64
|
+
--lessons <dir> Relative path to lessons directory (default: lessons).
|
|
65
|
+
`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runSingleFile(filePath: string, domain: string, outPath?: string) {
|
|
69
|
+
const absPath = isAbsolute(filePath) ? filePath : resolve(filePath)
|
|
70
|
+
if (!existsSync(absPath)) {
|
|
71
|
+
console.error(`Error: file not found: ${absPath}`)
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const source = await readFile(absPath, 'utf-8')
|
|
76
|
+
const relPath = basename(absPath)
|
|
77
|
+
|
|
78
|
+
const registry = new DomainParserRegistry()
|
|
79
|
+
registry.register(lessonParser)
|
|
80
|
+
|
|
81
|
+
let identity: any
|
|
82
|
+
let fixture: string
|
|
83
|
+
|
|
84
|
+
if (domain === 'lesson') {
|
|
85
|
+
// Try to parse lesson identity from parent directories
|
|
86
|
+
const parent = dirname(absPath)
|
|
87
|
+
const grandParent = dirname(parent)
|
|
88
|
+
const topicSlug = basename(grandParent)
|
|
89
|
+
const chapterSlug = basename(parent)
|
|
90
|
+
const lessonSlug = basename(absPath).replace(/\.md$/, '')
|
|
91
|
+
fixture = `${topicSlug}/${chapterSlug}/${lessonSlug}`
|
|
92
|
+
identity = buildLessonIdentity(lessonSlug, topicSlug, chapterSlug, relPath)
|
|
93
|
+
} else {
|
|
94
|
+
console.error(`Error: unknown domain '${domain}'`)
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const input: CompileInput = {
|
|
99
|
+
fixture,
|
|
100
|
+
domain,
|
|
101
|
+
identity,
|
|
102
|
+
source,
|
|
103
|
+
file: relPath
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = compile(input, registry)
|
|
107
|
+
|
|
108
|
+
if (result.kind === 'error') {
|
|
109
|
+
const err = result.error
|
|
110
|
+
console.error(`Compile error: ${err?.code ?? 'UNKNOWN'} — ${err?.message ?? err}`)
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const output = JSON.stringify(result.artifact, null, 2) + '\n'
|
|
115
|
+
|
|
116
|
+
if (outPath) {
|
|
117
|
+
const absOut = isAbsolute(outPath) ? outPath : resolve(outPath)
|
|
118
|
+
await mkdir(dirname(absOut), { recursive: true })
|
|
119
|
+
await writeFile(absOut, output, 'utf-8')
|
|
120
|
+
console.log(`Compiled ${fixture} → ${absOut}`)
|
|
121
|
+
} else {
|
|
122
|
+
process.stdout.write(output)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function runDirectory(rootDir: string, args: CmdArgs) {
|
|
127
|
+
const root = isAbsolute(rootDir) ? rootDir : resolve(rootDir)
|
|
128
|
+
if (!existsSync(root)) {
|
|
129
|
+
console.error(`Error: directory not found: ${root}`)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const outDirRaw = args.out as string | undefined
|
|
134
|
+
const outDir = outDirRaw
|
|
135
|
+
? (isAbsolute(outDirRaw) ? outDirRaw : resolve(outDirRaw))
|
|
136
|
+
: resolve(root, 'output', 'flow')
|
|
137
|
+
|
|
138
|
+
const registry = new DomainParserRegistry()
|
|
139
|
+
registry.register(lessonParser)
|
|
140
|
+
|
|
141
|
+
const lessonResult = await flowBuild({
|
|
142
|
+
root,
|
|
143
|
+
lessonsDir: args.lessons as string | undefined,
|
|
144
|
+
outDir,
|
|
145
|
+
domain: {
|
|
146
|
+
lesson: lessonParser,
|
|
147
|
+
buildLessonIdentity
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
console.log(`Build complete: ${lessonResult.compiled} artifacts in ${lessonResult.durationMs}ms`)
|
|
152
|
+
|
|
153
|
+
if (args.manifest) {
|
|
154
|
+
const manifestPath = isAbsolute(args.manifest as string)
|
|
155
|
+
? args.manifest as string
|
|
156
|
+
: resolve(args.manifest as string)
|
|
157
|
+
const pkg = await flowPackage({
|
|
158
|
+
storeDir: outDir,
|
|
159
|
+
manifestPath,
|
|
160
|
+
outFile: resolve(outDir, 'manifest.json')
|
|
161
|
+
})
|
|
162
|
+
console.log(`Packaged manifest.json: ${pkg.lessons.length} lessons, ${pkg.pages.length} pages`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Main ──────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
const { command, args, positionals } = parseArgs(process.argv.slice(2))
|
|
169
|
+
|
|
170
|
+
if (command === 'help' || !command) {
|
|
171
|
+
printHelp()
|
|
172
|
+
process.exit(0)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (command === 'build') {
|
|
176
|
+
const target = positionals[0]
|
|
177
|
+
if (!target) {
|
|
178
|
+
console.error('Error: missing target. Usage: build <file.md | directory>')
|
|
179
|
+
process.exit(1)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const isFile = target.endsWith('.md')
|
|
183
|
+
if (isFile) {
|
|
184
|
+
const domain = args.domain as string | undefined
|
|
185
|
+
if (!domain) {
|
|
186
|
+
console.error('Error: --domain required for single-file mode')
|
|
187
|
+
process.exit(1)
|
|
188
|
+
}
|
|
189
|
+
await runSingleFile(target, domain, args.out as string | undefined)
|
|
190
|
+
} else {
|
|
191
|
+
await runDirectory(target, args)
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.error(`Unknown command: ${command}`)
|
|
195
|
+
printHelp()
|
|
196
|
+
process.exit(1)
|
|
197
|
+
}
|
package/core/artifact/store.ts
CHANGED
|
@@ -73,7 +73,7 @@ const defaultFsOps: FsOps = {
|
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
75
|
* File-system Artifact Store.
|
|
76
|
-
* Layout: <baseDir>/index.json + <baseDir>/
|
|
76
|
+
* Layout: <baseDir>/index.json + <baseDir>/content/<id>.json
|
|
77
77
|
* Docs are serialised via toCanonicalJSON (2-space, key-lexicographic,
|
|
78
78
|
* LF, trailing newline) to match golden candidate form.
|
|
79
79
|
*/
|
|
@@ -82,7 +82,7 @@ export function createFsArtifactStore (
|
|
|
82
82
|
fsOps: FsOps = defaultFsOps
|
|
83
83
|
): ArtifactStore {
|
|
84
84
|
const idxPath = () => `${baseDir}/index.json`
|
|
85
|
-
const docPath = (id: string) => `${baseDir}/
|
|
85
|
+
const docPath = (id: string) => `${baseDir}/content/${id}.json`
|
|
86
86
|
|
|
87
87
|
return {
|
|
88
88
|
async writeIndex (index) {
|