chalk-talk 0.6.1 → 0.8.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 +2 -1
- package/assets/player.html +2 -2
- package/package.json +1 -1
- package/player.js +3 -3
- package/scripts/build.mjs +70 -18
- package/scripts/contract.mjs +751 -192
- package/scripts/mermaid.mjs +199 -0
package/scripts/build.mjs
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Checks chalk-talk recipes and turns them into one HTML file that plays the lesson — or, given several, a course: one
|
|
3
|
-
// file with every lesson and a list to go from one to another.
|
|
3
|
+
// file with every lesson and a list to go from one to another. A recipe can be JSON (.json), text (.chalk), a Mermaid
|
|
4
|
+
// flowchart (.mmd, with `%% # chapter` and `%% > narration`), or Markdown (.md): each ```chalk-talk or ```mermaid block
|
|
5
|
+
// in it is a lesson.
|
|
4
6
|
//
|
|
5
|
-
// node build.mjs <recipe.json> [out.html] [--no-review]
|
|
6
|
-
// node build.mjs <one
|
|
7
|
+
// node build.mjs <recipe.json|.chalk|.mmd|.md> [out.html] [--no-review]
|
|
8
|
+
// node build.mjs <one> <two> … --out course.html [--title "The course"] [--no-review]
|
|
9
|
+
// node build.mjs <recipe> [out.svg] --svg an SVG that draws itself, with no script: it plays inside an <img>,
|
|
10
|
+
// in a README on GitHub too (one SVG per lesson, named by its id)
|
|
7
11
|
//
|
|
8
|
-
// Only
|
|
9
|
-
// breaks the contract (nothing written); 2 bad arguments; 3 drawn, but the reviewer found something to fix (off
|
|
10
|
-
// paper, text over text, a line across a text). Needs Node 18 or later and
|
|
11
|
-
//
|
|
12
|
-
import { readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
12
|
+
// Only paths ending in .html or .svg are ever written. Exit codes: 0 drawn and checked (or no browser to check with); 1 a
|
|
13
|
+
// recipe breaks the contract (nothing written); 2 bad arguments; 3 drawn, but the reviewer found something to fix (off
|
|
14
|
+
// the paper, text over text, a line across a text); 4 --svg with no browser to draw it in. Needs Node 18 or later and
|
|
15
|
+
// nothing else; the review and --svg use Chrome, Chromium or Edge.
|
|
16
|
+
import { mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
13
18
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
14
19
|
import { fileURLToPath } from 'node:url'
|
|
15
20
|
import { runPage } from './browser.mjs'
|
|
16
|
-
import { chaptersFor, validateScene } from './contract.mjs'
|
|
21
|
+
import { ChalkTextError, chaptersFor, parseChalk, PROP_ALIASES, PROP_NAMES, validateScene } from './contract.mjs'
|
|
17
22
|
|
|
18
23
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
19
24
|
const argv = process.argv.slice(2)
|
|
@@ -24,27 +29,51 @@ const option = (name) => {
|
|
|
24
29
|
const valued = new Set(['--out', '--title'])
|
|
25
30
|
const args = argv.filter((a, i) => !a.startsWith('--') && !valued.has(argv[i - 1]))
|
|
26
31
|
const noReview = argv.includes('--no-review')
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
32
|
+
const asSvg = argv.includes('--svg')
|
|
33
|
+
const ext = asSvg ? '.svg' : '.html'
|
|
34
|
+
const KINDS = ['.json', '.chalk', '.mmd', '.md']
|
|
35
|
+
const sources = args.filter((a) => KINDS.some((k) => a.endsWith(k)))
|
|
36
|
+
const outArg = option('--out') ?? args.find((a) => a.endsWith(ext))
|
|
37
|
+
const usage = 'usage: node build.mjs <recipe.json> [out.html] [--no-review]\n node build.mjs <one.json> <two.json> … --out course.html [--title "…"] [--no-review]\n node build.mjs <recipe.json> [out.svg] --svg'
|
|
38
|
+
if (!sources.length || args.some((a) => !KINDS.some((k) => a.endsWith(k)) && !a.endsWith(ext)) || (outArg && !outArg.endsWith(ext))) {
|
|
31
39
|
console.error(usage)
|
|
32
40
|
process.exit(2)
|
|
33
41
|
}
|
|
34
42
|
|
|
43
|
+
const PROPS = [...PROP_NAMES, ...Object.keys(PROP_ALIASES)]
|
|
44
|
+
// The Mermaid converter is large: loaded only for a Mermaid block.
|
|
45
|
+
const mermaid = async (text) => (await import('./mermaid.mjs')).fromMermaid(text)
|
|
46
|
+
|
|
47
|
+
/** The recipes a file holds: one for .json, .chalk and .mmd; one per ```chalk-talk / ```mermaid block in a .md. */
|
|
48
|
+
async function recipesOf(file) {
|
|
49
|
+
const text = readFileSync(resolve(file), 'utf8')
|
|
50
|
+
if (file.endsWith('.json')) return [JSON.parse(text)]
|
|
51
|
+
if (file.endsWith('.chalk')) return [parseChalk(text, PROPS)]
|
|
52
|
+
if (file.endsWith('.mmd')) return [await mermaid(text)]
|
|
53
|
+
const blocks = [...text.matchAll(/^```(chalk-talk|mermaid)[^\n]*\n([\s\S]*?)^```/gm)]
|
|
54
|
+
if (!blocks.length) throw new Error('no ```chalk-talk or ```mermaid block in this file.')
|
|
55
|
+
const out = []
|
|
56
|
+
for (const [, kind, body] of blocks) out.push(kind === 'mermaid' ? await mermaid(body) : parseChalk(body, PROPS))
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
const scenes = []
|
|
36
61
|
for (const file of sources) {
|
|
37
62
|
try {
|
|
38
|
-
scenes.push(validateScene(
|
|
63
|
+
for (const recipe of await recipesOf(file)) scenes.push(validateScene(recipe))
|
|
39
64
|
} catch (e) {
|
|
40
|
-
|
|
65
|
+
const where = sources.length > 1 || file.endsWith('.md') ? `${basename(file)}: ` : ''
|
|
66
|
+
console.error(`✗ ${e instanceof ChalkTextError ? 'text' : 'contract'}: ${where}${e.message}`)
|
|
41
67
|
process.exit(1)
|
|
42
68
|
}
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
const course = scenes.length > 1
|
|
46
|
-
const
|
|
47
|
-
const
|
|
72
|
+
const slugOf = (scene, file) => String(scene.id || basename(file).replace(/\.[^.]+$/, '')).replace(/[^a-z0-9-]+/gi, '-').slice(0, 60) || 'drawing'
|
|
73
|
+
const slug = slugOf(scenes[0], sources[0])
|
|
74
|
+
// With --svg the page is only the browser's workbench: it goes to a folder of its own and away at the end.
|
|
75
|
+
const work = asSvg ? mkdtempSync(join(tmpdir(), 'chalk-talk-svg-')) : undefined
|
|
76
|
+
const out = work ? join(work, 'page.html') : resolve(outArg ?? join(dirname(resolve(sources[0])), `${slug}.html`))
|
|
48
77
|
const content = course ? { course: option('--title') ?? scenes[0].title, lessons: scenes } : scenes[0]
|
|
49
78
|
// JSON inside a <script> tag: "<" escaped so no text in a recipe can end the tag.
|
|
50
79
|
const recipe = JSON.stringify(content).replace(/</g, '\\u003c')
|
|
@@ -75,5 +104,28 @@ else {
|
|
|
75
104
|
}
|
|
76
105
|
}
|
|
77
106
|
}
|
|
78
|
-
|
|
107
|
+
if (!asSvg) {
|
|
108
|
+
console.log(`→ ${out} (${Math.round(statSync(out).size / 1024)} KB, opens offline${course ? `, ${scenes.length} lessons` : ''})`)
|
|
109
|
+
process.exit(code)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --svg: each lesson drawn by the browser with its timing as CSS; one file, or one per lesson beside the first source.
|
|
113
|
+
const where = outArg && !course ? undefined : dirname(resolve(outArg ?? sources[0]))
|
|
114
|
+
for (let k = 0; k < scenes.length; k++) {
|
|
115
|
+
const r = runPage(out, 'svg', course ? `lesson=${k + 1}` : '')
|
|
116
|
+
if (!r) {
|
|
117
|
+
console.error('✗ svg no Chrome, Chromium or Edge here to draw it (set CHROME_PATH)')
|
|
118
|
+
code = 4
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
if (r.error) {
|
|
122
|
+
console.error(`✗ svg ${r.error}`)
|
|
123
|
+
code = 4
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
const file = where ? join(where, `${slugOf(scenes[k], sources[0])}.svg`) : resolve(outArg)
|
|
127
|
+
writeFileSync(file, r.svg)
|
|
128
|
+
console.log(`→ ${file} (${Math.round(statSync(file).size / 1024)} KB, draws itself in ${Math.round(r.ms / 1000)} s)`)
|
|
129
|
+
}
|
|
130
|
+
rmSync(work, { recursive: true, force: true })
|
|
79
131
|
process.exit(code)
|