chalk-talk 0.14.0 → 0.14.1
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 +1 -1
- package/kit/browser.mjs +21 -4
- package/kit/format.check.ts +1 -0
- package/kit/format.ts +4 -2
- package/kit/tools/gallery.mjs +6 -5
- package/package.json +1 -1
- package/scripts/browser.mjs +21 -4
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Hand-drawn lessons that draw themselves, from a small JSON **recipe** — the pl
|
|
|
10
10
|
<meta charset="utf-8">
|
|
11
11
|
<title>My lesson</title>
|
|
12
12
|
<script type="application/json" id="chalk-recipe">{ "id": "my-lesson", "title": "…", "width": 900, "height": 560, "steps": [ … ] }</script>
|
|
13
|
-
<script src="https://cdn.jsdelivr.net/npm/chalk-talk@0.14.
|
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/chalk-talk@0.14.1/player.js"></script>
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The player builds the page around the recipe — chapters, Next / Back, values to move, the check, downloads — and needs
|
package/kit/browser.mjs
CHANGED
|
@@ -121,7 +121,7 @@ async function start(exe) {
|
|
|
121
121
|
|
|
122
122
|
/** Opens `file` at `#mode` (with `query`) in a tab of its own and returns what the page wrote, parsed — { error } when
|
|
123
123
|
* the page could not be opened or wrote nothing in `ms`. */
|
|
124
|
-
async function open(b, file, mode, query, ms) {
|
|
124
|
+
async function open(b, file, mode, query, ms, shot) {
|
|
125
125
|
const until = Date.now() + ms
|
|
126
126
|
let sessionId
|
|
127
127
|
// Every call waits at most until the page's time is up: a page busy forever does not hold the build.
|
|
@@ -133,8 +133,24 @@ async function open(b, file, mode, query, ms) {
|
|
|
133
133
|
const { targetId } = await ask('Target.createTarget', { url: 'about:blank' })
|
|
134
134
|
try {
|
|
135
135
|
;({ sessionId } = await ask('Target.attachToTarget', { targetId, flatten: true }))
|
|
136
|
-
|
|
136
|
+
if (shot) await ask('Emulation.setDeviceMetricsOverride', { width: shot.width, height: shot.height, deviceScaleFactor: 1, mobile: false })
|
|
137
|
+
if (shot?.scheme) await ask('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value: shot.scheme }] })
|
|
138
|
+
const nav = await ask('Page.navigate', { url: `${pathToFileURL(file).href}${query ? `?${query}` : ''}${mode ? `#${mode}` : ''}` })
|
|
137
139
|
if (nav.errorText) return { error: `the page did not open (${nav.errorText})` }
|
|
140
|
+
// A picture of the page as it is once loaded, its fonts in and (when it says so) data-ready on the body.
|
|
141
|
+
if (shot) {
|
|
142
|
+
const ready = "document.readyState === 'complete' && document.fonts.status === 'loaded' && (document.body?.dataset.ready ?? '1') === '1'"
|
|
143
|
+
while (Date.now() < until) {
|
|
144
|
+
const r = await ask('Runtime.evaluate', { expression: ready, returnByValue: true })
|
|
145
|
+
if (r.result?.value === true) {
|
|
146
|
+
await pause(shot.settle ?? 400)
|
|
147
|
+
const { data } = await ask('Page.captureScreenshot', { format: 'png' })
|
|
148
|
+
return { png: `data:image/png;base64,${data}` }
|
|
149
|
+
}
|
|
150
|
+
await pause(50)
|
|
151
|
+
}
|
|
152
|
+
throw new BrowserFailed(`the page was not ready in ${Math.round(ms / 1000)} s`)
|
|
153
|
+
}
|
|
138
154
|
while (Date.now() < until) {
|
|
139
155
|
const r = await ask('Runtime.evaluate', { expression: `document.getElementById('chalk-${mode}')?.textContent ?? null`, returnByValue: true })
|
|
140
156
|
const text = r.result?.value
|
|
@@ -149,7 +165,8 @@ async function open(b, file, mode, query, ms) {
|
|
|
149
165
|
}
|
|
150
166
|
}
|
|
151
167
|
|
|
152
|
-
/** Runs `jobs` ({ file, mode, query }) in one browser
|
|
168
|
+
/** Runs `jobs` ({ file, mode, query, shot }) in one browser — `shot: { width, height }` answers with { png } (a
|
|
169
|
+
* picture of the page once loaded) instead of what the page wrote — and returns their results in order, with the browser used —
|
|
153
170
|
* `results` null when there is no browser at all. A browser that fails (does not start, stops, or a page gets no answer
|
|
154
171
|
* in `timeout` seconds) gives way to the next one, which runs every job again; when none works, each result is
|
|
155
172
|
* { error } naming what was tried. */
|
|
@@ -162,7 +179,7 @@ export async function runPages(jobs, { timeout = 20 } = {}) {
|
|
|
162
179
|
try {
|
|
163
180
|
b = await start(exe)
|
|
164
181
|
const results = []
|
|
165
|
-
for (const { file, mode, query = '' } of jobs) results.push(await open(b, file, mode, query, timeout * 1000))
|
|
182
|
+
for (const { file, mode = '', query = '', shot } of jobs) results.push(await open(b, file, mode, query, timeout * 1000, shot))
|
|
166
183
|
return { results, used: exe, tried }
|
|
167
184
|
} catch (e) {
|
|
168
185
|
tried.push(`${exe}: ${e instanceof BrowserFailed ? e.message : e.message.split('\n')[0]}`)
|
package/kit/format.check.ts
CHANGED
|
@@ -12,6 +12,7 @@ if (r.ok) {
|
|
|
12
12
|
assert.equal(r.format.at(['items', 1]), 6)
|
|
13
13
|
assert.equal(r.format.at(['items', 1, 'price']), 6, 'a path not in the file says where its nearest part is')
|
|
14
14
|
assert.equal(r.format.at(['nowhere']), 1)
|
|
15
|
+
assert.equal(r.format.lineOf(text.indexOf('cake')), 6)
|
|
15
16
|
}
|
|
16
17
|
const syntax = readFormat('format: shop/v1\nname: x\n bad: indent\n', 'shop/v1')
|
|
17
18
|
assert.ok(!syntax.ok && syntax.problem.key === 'syntax' && syntax.problem.line === 2)
|
package/kit/format.ts
CHANGED
|
@@ -6,13 +6,15 @@ import { isMap, LineCounter, parseDocument, type Document } from 'yaml'
|
|
|
6
6
|
export type Path = (string | number)[]
|
|
7
7
|
|
|
8
8
|
/** A problem the reader found before the skill's own checks: `syntax` (with YAML's code) or `format`. */
|
|
9
|
-
export
|
|
9
|
+
export type FormatProblem = { line: number; key: 'syntax'; detail: string } | { line: number; key: 'format' }
|
|
10
10
|
|
|
11
11
|
export interface Format {
|
|
12
12
|
raw: Record<string, unknown>
|
|
13
13
|
doc: Document
|
|
14
14
|
/** The line where `path` (or the nearest part of it that is in the file) starts; 1 when none is. */
|
|
15
15
|
at: (path: Path) => number
|
|
16
|
+
/** The line of a position in the text (a node's `range[0]`). */
|
|
17
|
+
lineOf: (offset: number) => number
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type Read = { ok: true; format: Format } | { ok: false; problem: FormatProblem }
|
|
@@ -33,5 +35,5 @@ export function readFormat(text: string, format: string): Read {
|
|
|
33
35
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw) || (raw as Record<string, unknown>).format !== format || !isMap(doc.contents)) {
|
|
34
36
|
return { ok: false, problem: { line: at(['format']), key: 'format' } }
|
|
35
37
|
}
|
|
36
|
-
return { ok: true, format: { raw: raw as Record<string, unknown>, doc, at } }
|
|
38
|
+
return { ok: true, format: { raw: raw as Record<string, unknown>, doc, at, lineOf: (offset) => lines.linePos(offset).line } }
|
|
37
39
|
}
|
package/kit/tools/gallery.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// The pages and pictures of a skill's GitHub Pages gallery: each example's page in <docs>/<pages>/ and a PNG of it in
|
|
2
|
-
// <docs>/thumbs/,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// <docs>/thumbs/, in a headless browser (the kit's browser.mjs, over the DevTools pipe). Either the page draws its own
|
|
3
|
+
// picture (`mode`, the #mode that answers with it, and `pick`, which takes the PNG data: URL from the answer) or the
|
|
4
|
+
// browser takes one (`shot: { width, height }`, after `mode` if any). The gallery's index stays the skill's own. Part
|
|
5
|
+
// of the skills kit (chalk-talk/kit).
|
|
5
6
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
7
|
import { join } from 'node:path'
|
|
7
8
|
import { browser, runPages } from '../browser.mjs'
|
|
8
9
|
|
|
9
10
|
/** `items`: [{ name, html }]. Writes every page, then every picture in one opening of the browser. */
|
|
10
|
-
export async function gallery(docs, items, { pages = 'lessons', mode, pick, timeout = 60 }) {
|
|
11
|
+
export async function gallery(docs, items, { pages = 'lessons', mode, pick = (r) => r.png, shot, timeout = 60 }) {
|
|
11
12
|
if (!browser()) throw new Error('No Chrome, Chromium or Edge: the pictures need one (set CHROME_PATH).')
|
|
12
13
|
for (const dir of [pages, 'thumbs']) {
|
|
13
14
|
rmSync(join(docs, dir), { recursive: true, force: true })
|
|
@@ -18,7 +19,7 @@ export async function gallery(docs, items, { pages = 'lessons', mode, pick, time
|
|
|
18
19
|
writeFileSync(file, html)
|
|
19
20
|
return file
|
|
20
21
|
})
|
|
21
|
-
const { results } = await runPages(files.map((file) => ({ file, mode })), { timeout })
|
|
22
|
+
const { results } = await runPages(files.map((file) => ({ file, mode, shot })), { timeout })
|
|
22
23
|
items.forEach(({ name }, k) => {
|
|
23
24
|
const r = results[k]
|
|
24
25
|
const png = r && !r.error ? pick(r) : undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chalk-talk",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "Hand-drawn lessons that draw themselves: a player for chalk-talk recipes (one script for the browser or a CDN), a lesson you can mount inside any element, and a command that checks a recipe and builds one HTML file.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/scripts/browser.mjs
CHANGED
|
@@ -121,7 +121,7 @@ async function start(exe) {
|
|
|
121
121
|
|
|
122
122
|
/** Opens `file` at `#mode` (with `query`) in a tab of its own and returns what the page wrote, parsed — { error } when
|
|
123
123
|
* the page could not be opened or wrote nothing in `ms`. */
|
|
124
|
-
async function open(b, file, mode, query, ms) {
|
|
124
|
+
async function open(b, file, mode, query, ms, shot) {
|
|
125
125
|
const until = Date.now() + ms
|
|
126
126
|
let sessionId
|
|
127
127
|
// Every call waits at most until the page's time is up: a page busy forever does not hold the build.
|
|
@@ -133,8 +133,24 @@ async function open(b, file, mode, query, ms) {
|
|
|
133
133
|
const { targetId } = await ask('Target.createTarget', { url: 'about:blank' })
|
|
134
134
|
try {
|
|
135
135
|
;({ sessionId } = await ask('Target.attachToTarget', { targetId, flatten: true }))
|
|
136
|
-
|
|
136
|
+
if (shot) await ask('Emulation.setDeviceMetricsOverride', { width: shot.width, height: shot.height, deviceScaleFactor: 1, mobile: false })
|
|
137
|
+
if (shot?.scheme) await ask('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value: shot.scheme }] })
|
|
138
|
+
const nav = await ask('Page.navigate', { url: `${pathToFileURL(file).href}${query ? `?${query}` : ''}${mode ? `#${mode}` : ''}` })
|
|
137
139
|
if (nav.errorText) return { error: `the page did not open (${nav.errorText})` }
|
|
140
|
+
// A picture of the page as it is once loaded, its fonts in and (when it says so) data-ready on the body.
|
|
141
|
+
if (shot) {
|
|
142
|
+
const ready = "document.readyState === 'complete' && document.fonts.status === 'loaded' && (document.body?.dataset.ready ?? '1') === '1'"
|
|
143
|
+
while (Date.now() < until) {
|
|
144
|
+
const r = await ask('Runtime.evaluate', { expression: ready, returnByValue: true })
|
|
145
|
+
if (r.result?.value === true) {
|
|
146
|
+
await pause(shot.settle ?? 400)
|
|
147
|
+
const { data } = await ask('Page.captureScreenshot', { format: 'png' })
|
|
148
|
+
return { png: `data:image/png;base64,${data}` }
|
|
149
|
+
}
|
|
150
|
+
await pause(50)
|
|
151
|
+
}
|
|
152
|
+
throw new BrowserFailed(`the page was not ready in ${Math.round(ms / 1000)} s`)
|
|
153
|
+
}
|
|
138
154
|
while (Date.now() < until) {
|
|
139
155
|
const r = await ask('Runtime.evaluate', { expression: `document.getElementById('chalk-${mode}')?.textContent ?? null`, returnByValue: true })
|
|
140
156
|
const text = r.result?.value
|
|
@@ -149,7 +165,8 @@ async function open(b, file, mode, query, ms) {
|
|
|
149
165
|
}
|
|
150
166
|
}
|
|
151
167
|
|
|
152
|
-
/** Runs `jobs` ({ file, mode, query }) in one browser
|
|
168
|
+
/** Runs `jobs` ({ file, mode, query, shot }) in one browser — `shot: { width, height }` answers with { png } (a
|
|
169
|
+
* picture of the page once loaded) instead of what the page wrote — and returns their results in order, with the browser used —
|
|
153
170
|
* `results` null when there is no browser at all. A browser that fails (does not start, stops, or a page gets no answer
|
|
154
171
|
* in `timeout` seconds) gives way to the next one, which runs every job again; when none works, each result is
|
|
155
172
|
* { error } naming what was tried. */
|
|
@@ -162,7 +179,7 @@ export async function runPages(jobs, { timeout = 20 } = {}) {
|
|
|
162
179
|
try {
|
|
163
180
|
b = await start(exe)
|
|
164
181
|
const results = []
|
|
165
|
-
for (const { file, mode, query = '' } of jobs) results.push(await open(b, file, mode, query, timeout * 1000))
|
|
182
|
+
for (const { file, mode = '', query = '', shot } of jobs) results.push(await open(b, file, mode, query, timeout * 1000, shot))
|
|
166
183
|
return { results, used: exe, tried }
|
|
167
184
|
} catch (e) {
|
|
168
185
|
tried.push(`${exe}: ${e instanceof BrowserFailed ? e.message : e.message.split('\n')[0]}`)
|