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 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.0/player.js"></script>
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
- const nav = await ask('Page.navigate', { url: `${pathToFileURL(file).href}${query ? `?${query}` : ''}#${mode}` })
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 and returns their results in order, with the browser used —
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]}`)
@@ -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 interface FormatProblem { line: number; key: 'syntax' | 'format'; detail?: string }
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
  }
@@ -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/, drawn by the page itself in a headless browser (the kit's browser.mjs, over the DevTools pipe). The
3
- // page says what its picture is: `mode` is the #mode that answers with it and `pick` takes the PNG data: URL from the
4
- // answer. The gallery's index stays the skill's own. Part of the skills kit (chalk-talk/kit).
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.0",
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",
@@ -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
- const nav = await ask('Page.navigate', { url: `${pathToFileURL(file).href}${query ? `?${query}` : ''}#${mode}` })
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 and returns their results in order, with the browser used —
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]}`)