prolog-notebook 0.5.1 → 0.6.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/CHANGELOG.md +72 -0
- package/README.md +61 -16
- package/bin/prolog-notebook.mjs +84 -15
- package/package.json +1 -1
- package/src/build-info.json +2 -2
- package/src/build.js +77 -0
- package/src/export.js +23 -0
- package/src/notebook.js +176 -9
- package/src/serve.js +16 -3
- package/src/upgrade.js +1 -1
- package/src/version.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.1] — 2026-08-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`view` served the notebook as it was when the server started.** It built the page once and
|
|
8
|
+
handed the map to the server, which held it for the life of the process — so an author who
|
|
9
|
+
edited their chapter and reloaded was shown the version the server had started with. A reload
|
|
10
|
+
is the gesture for *show me what I just did*, and answering it with the old page teaches an
|
|
11
|
+
author to doubt their own edit rather than the tool.
|
|
12
|
+
|
|
13
|
+
The request is now what reads the file, so there is no window in which the page and the file
|
|
14
|
+
can disagree, and nothing that can have missed a change. Compared by bytes rather than mtime:
|
|
15
|
+
an editor writing through a rename, a `git checkout` and two saves inside one millisecond are
|
|
16
|
+
all changes an mtime reports unreliably, and reparsing a chapter is milliseconds on a file the
|
|
17
|
+
author has open anyway. An unchanged file is read and not rebuilt.
|
|
18
|
+
|
|
19
|
+
A chapter that stops parsing keeps the last version that did, with the parser's own message
|
|
20
|
+
across the top of the page and once on stderr — not once per asset. Silently serving the
|
|
21
|
+
previous version would be the same bug wearing the fix's clothes, and blanking the page would
|
|
22
|
+
throw a chapter away over a half-typed fence. Fixing the file restores it with no restart.
|
|
23
|
+
|
|
24
|
+
`build` is unchanged: it takes the first answer and writes it.
|
|
25
|
+
|
|
26
|
+
## [0.6.0] — 2026-08-30
|
|
27
|
+
|
|
28
|
+
Taking the answers back out — at the terminal, and on the page. Plus the first documentation
|
|
29
|
+
written for somebody who is not us.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **`prolog-notebook clear <file>`** empties every output block and leaves everything else
|
|
34
|
+
alone: prose, program cells, goals and attributes are the author's. A chapter with no answers
|
|
35
|
+
is a valid chapter — one that has not been executed yet — so `clear` then `execute` returns
|
|
36
|
+
the original bytes exactly. For a workbook edition, for a diff you can read, or for starting
|
|
37
|
+
again deliberately rather than trusting an overwrite.
|
|
38
|
+
|
|
39
|
+
- **The page can clear its own answers, and put them back.** A new row in the panel:
|
|
40
|
+
|
|
41
|
+
4 outputs in this chapter [Clear all outputs]
|
|
42
|
+
4 outputs cleared [Restore outputs]
|
|
43
|
+
|
|
44
|
+
It sits next to the hide row on purpose, because the two blank the same box for opposite
|
|
45
|
+
reasons. Hiding acts on the **screen** — the answers are still the chapter's and still go
|
|
46
|
+
into a download. Clearing acts on the **notebook**: a download taken after it carries no
|
|
47
|
+
output block at all, through the same erasure the CLI uses, so a chapter emptied on the page
|
|
48
|
+
and one emptied at the terminal are the same bytes.
|
|
49
|
+
|
|
50
|
+
That makes clearing a third way away from the published chapter, beside running and editing,
|
|
51
|
+
which is why nothing new was needed to get back. **One origin, one way back**: reset already
|
|
52
|
+
meant *as published* and now has one more thing it can undo — per cell, or all at once with
|
|
53
|
+
restore. An auto cell does not refill itself once cleared, and restore is the exact inverse
|
|
54
|
+
of clear, leaving an edited goal alone.
|
|
55
|
+
|
|
56
|
+
- **`docs/authoring.md` — the author's handbook.** The loop, a whole chapter from scratch,
|
|
57
|
+
what `hold` and `rerun` do to a reader, why you never hand-write an output block, staleness,
|
|
58
|
+
what a reader can do to your chapter, publishing, and nine things that otherwise cost an
|
|
59
|
+
afternoon. Every command line in it was run.
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **`run` is now `execute`.** It executes a chapter and writes its answers into the file, which
|
|
64
|
+
`run` did not say — and *run* was already the name of the button a reader presses on one
|
|
65
|
+
cell. `run` and `exec` still work; nothing has been taken away.
|
|
66
|
+
|
|
67
|
+
- **The README had not met the CLI.** Its banner still said the renderer was unwritten, Status
|
|
68
|
+
still counted 186 tests, and *Try it* pointed a fresh global install at a chapter that ships
|
|
69
|
+
in the repo and not in the package — so the first command in the front door failed for
|
|
70
|
+
everybody who had just installed it. It now writes a three-line chapter, executes it and
|
|
71
|
+
views it.
|
|
72
|
+
|
|
73
|
+
- `docs/modes.md` §3 gains *Hiding is not clearing*.
|
|
74
|
+
|
|
3
75
|
## [0.5.1] — 2026-08-30
|
|
4
76
|
|
|
5
77
|
Everything here came from one field report: a chapter written with the tool rather than a test
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Jupyter-style notebooks for Prolog. Runs in the browser, installs nothing.**
|
|
4
4
|
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
5
|
+
> **v0.5 — usable, and moving.** A chapter is a markdown file; the CLI runs it, serves it and
|
|
6
|
+
> publishes it. Writing one is [the author's handbook](docs/authoring.md). See
|
|
7
|
+
> [Status](#status) for what is not built yet.
|
|
8
8
|
|
|
9
9
|
## The idea
|
|
10
10
|
|
|
@@ -40,19 +40,35 @@ So a query cell gives you the first solution, and then you step.
|
|
|
40
40
|
|
|
41
41
|
## Try it
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
A whole chapter, from nothing, in four commands:
|
|
44
|
+
|
|
45
|
+
````sh
|
|
44
46
|
npm i -g prolog-notebook
|
|
45
|
-
|
|
47
|
+
|
|
48
|
+
cat > splitting.prolog.md <<'EOF'
|
|
49
|
+
# Splitting a list
|
|
50
|
+
|
|
51
|
+
`append/3` is usually introduced as the predicate that joins two lists. That is the
|
|
52
|
+
least interesting thing it does — run it backwards and it takes a list apart, every
|
|
53
|
+
way it can be split, one solution at a time.
|
|
54
|
+
|
|
55
|
+
```prolog query
|
|
56
|
+
append(Front, Back, [hello, there, world])
|
|
46
57
|
```
|
|
58
|
+
EOF
|
|
59
|
+
|
|
60
|
+
prolog-notebook execute splitting.prolog.md # SWI fills the answers in
|
|
61
|
+
prolog-notebook view splitting.prolog.md # read it, cells live
|
|
62
|
+
````
|
|
47
63
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
`execute` runs the chapter and writes the solutions back into the markdown — you never
|
|
65
|
+
hand-write an answer. `view` opens it in your browser: press Run, then `; next`, and watch the
|
|
66
|
+
four splits arrive one at a time. Nothing is installed but the command.
|
|
51
67
|
|
|
52
68
|
To send it to somebody, or host it:
|
|
53
69
|
|
|
54
70
|
```sh
|
|
55
|
-
prolog-notebook build
|
|
71
|
+
prolog-notebook build splitting.prolog.md --out site/
|
|
56
72
|
```
|
|
57
73
|
|
|
58
74
|
A plain directory: prerendered HTML with the saved answers in it, the runtime beside it, and
|
|
@@ -98,7 +114,7 @@ author's *guess* at what SWI prints, published as though it ran. So write the fi
|
|
|
98
114
|
output blocks and let the engine fill them in:
|
|
99
115
|
|
|
100
116
|
```sh
|
|
101
|
-
|
|
117
|
+
prolog-notebook execute chapter.prolog.md
|
|
102
118
|
```
|
|
103
119
|
|
|
104
120
|
It consults every program cell, runs every query below it, and writes the solution sequences
|
|
@@ -106,11 +122,24 @@ back into the file along with an `input-hash` for each — which is what makes t
|
|
|
106
122
|
complete, and render as *current*, before the engine arrives. Running it again on an unchanged
|
|
107
123
|
chapter changes nothing.
|
|
108
124
|
|
|
125
|
+
And back out again, for a workbook edition or a diff you can read:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
prolog-notebook clear chapter.prolog.md
|
|
129
|
+
chapter.prolog.md: 4 answers removed
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`clear` empties every output block and touches nothing else; `execute` fills them in again from
|
|
133
|
+
the engine. A chapter with no answers is a valid chapter — one that has not been executed yet.
|
|
134
|
+
|
|
109
135
|
| flag | |
|
|
110
136
|
|---|---|
|
|
111
137
|
| `--limit <n>` | solutions to take from one query before stopping. Default 100. |
|
|
112
138
|
| `--stdout` | print the result instead of writing the file |
|
|
113
139
|
| `--quiet` | report only failures |
|
|
140
|
+
| `--out <dir>` | where `build` writes. Default `<file>-site` |
|
|
141
|
+
| `--port <n>` | what `view` listens on. Default 8777, and it takes another if that one is busy |
|
|
142
|
+
| `--no-open` | `view` prints the URL instead of opening a browser |
|
|
114
143
|
| `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
|
|
115
144
|
| `--check-update` | ask npm whether a newer one exists, and say so either way |
|
|
116
145
|
|
|
@@ -135,7 +164,7 @@ doing the work — and if you say yes it upgrades, then runs your command on the
|
|
|
135
164
|
One command, no re-run:
|
|
136
165
|
|
|
137
166
|
```
|
|
138
|
-
$ prolog-notebook
|
|
167
|
+
$ prolog-notebook execute ch04.prolog.md
|
|
139
168
|
You have Prolog Notebook 0.4.0. The latest is 0.4.2.
|
|
140
169
|
Update and continue on the new version? [Y/n] y
|
|
141
170
|
Updating with npm i -g prolog-notebook@0.4.2
|
|
@@ -158,6 +187,20 @@ It has no defence against a non-terminating goal yet — the engine runs in this
|
|
|
158
187
|
not a solution count. Fixing it properly means a worker thread, and it is the prerequisite for
|
|
159
188
|
putting this in CI.
|
|
160
189
|
|
|
190
|
+
## Write one
|
|
191
|
+
|
|
192
|
+
**[The author's handbook](docs/authoring.md)** — the loop, a chapter from scratch, what `hold`
|
|
193
|
+
and `rerun` do to a reader, why you never hand-write an answer, publishing, and the things that
|
|
194
|
+
will otherwise cost you an afternoon.
|
|
195
|
+
|
|
196
|
+
| | |
|
|
197
|
+
|---|---|
|
|
198
|
+
| [docs/authoring.md](docs/authoring.md) | writing and publishing a chapter |
|
|
199
|
+
| [docs/format.md](docs/format.md) | the `.prolog.md` format, normatively |
|
|
200
|
+
| [docs/modes.md](docs/modes.md) | Read, Explore, Own — what a reader may change, and what may never be confused with what |
|
|
201
|
+
| [docs/binding.md](docs/binding.md) | chapters into books; why a notebook never states its own position |
|
|
202
|
+
| [docs/platform-seams.md](docs/platform-seams.md) | what is environment-specific, and where |
|
|
203
|
+
|
|
161
204
|
## Use it
|
|
162
205
|
|
|
163
206
|
Headless, in Node — this is how you test that every example in a document still works:
|
|
@@ -268,14 +311,16 @@ Working and tested:
|
|
|
268
311
|
- program cells and query cells with `Run` / `; next` / `all` / `stop`, per-cell reset, and a
|
|
269
312
|
page that says what the engine is holding
|
|
270
313
|
- `hold` and `rerun="auto"` — the author decides what a reader may see and when it refreshes
|
|
271
|
-
- **the CLI**: `
|
|
272
|
-
in a browser, `build` writes a page you can host or send
|
|
273
|
-
|
|
274
|
-
-
|
|
314
|
+
- **the CLI**: `execute` runs a chapter headlessly and writes its answers back, `clear` takes
|
|
315
|
+
them out again, `view` opens it in a browser, `build` writes a page you can host or send,
|
|
316
|
+
and it updates itself
|
|
317
|
+
- **page controls**: hide the saved answers to work a chapter cold, clear them out and restore
|
|
318
|
+
them, and download your own copy — yours or the chapter as published
|
|
319
|
+
- 253 passing tests
|
|
275
320
|
|
|
276
321
|
Not built yet:
|
|
277
322
|
|
|
278
|
-
-
|
|
323
|
+
- `check` — run a chapter in CI and fail the build when its answers have drifted. Needs a
|
|
279
324
|
timeout first: a test suite that can hang forever is not a test suite.
|
|
280
325
|
- custom elements (`<prolog-program>`, `<prolog-query>`) so notebooks drop into any static site
|
|
281
326
|
- a VS Code notebook controller — VS Code supplies the UI, this supplies the kernel, still no Python
|
package/bin/prolog-notebook.mjs
CHANGED
|
@@ -12,9 +12,9 @@ import { buildLine, currentBuild } from '../src/build-info.js';
|
|
|
12
12
|
import { banner, VERSION } from '../src/version.js';
|
|
13
13
|
import { updateNotice } from '../src/update.js';
|
|
14
14
|
import { confirm, describeInstall, globalRoot, install, relaunch, upgradePlan } from '../src/upgrade.js';
|
|
15
|
-
import { exportSource } from '../src/export.js';
|
|
15
|
+
import { clearedSource, exportSource } from '../src/export.js';
|
|
16
16
|
import { runNotebook, DEFAULT_LIMIT } from '../src/run.js';
|
|
17
|
-
import {
|
|
17
|
+
import { livePages } from '../src/build.js';
|
|
18
18
|
import { openInBrowser, serve } from '../src/serve.js';
|
|
19
19
|
|
|
20
20
|
// The engine is imported WHERE IT IS USED, never at the top. src/node.js pulls in
|
|
@@ -23,7 +23,7 @@ import { openInBrowser, serve } from '../src/serve.js';
|
|
|
23
23
|
// commands most likely to be typed at a broken install are --help and --version.
|
|
24
24
|
const engine = () => import('../src/node.js');
|
|
25
25
|
|
|
26
|
-
// `prolog-notebook
|
|
26
|
+
// `prolog-notebook execute --stdout file | head` closes the pipe while we are still
|
|
27
27
|
// writing to it. That is the reader using the shell correctly, not an error, and
|
|
28
28
|
// a command that answers it with an unhandled EPIPE and a stack trace is
|
|
29
29
|
// complaining about being used properly.
|
|
@@ -39,10 +39,11 @@ const require = createRequire(import.meta.url);
|
|
|
39
39
|
|
|
40
40
|
const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
|
|
41
41
|
|
|
42
|
-
prolog-notebook view <file.prolog.md>
|
|
43
|
-
prolog-notebook build <file.prolog.md>
|
|
44
|
-
prolog-notebook
|
|
45
|
-
prolog-notebook
|
|
42
|
+
prolog-notebook view <file.prolog.md> read it in a browser, cells and all
|
|
43
|
+
prolog-notebook build <file.prolog.md> write a page you can host or send
|
|
44
|
+
prolog-notebook execute <file.prolog.md>... run every query, write the answers in
|
|
45
|
+
prolog-notebook clear <file.prolog.md>... take the answers back out
|
|
46
|
+
prolog-notebook upgrade fetch the latest version
|
|
46
47
|
|
|
47
48
|
Options
|
|
48
49
|
--limit <n> solutions to take from one query before stopping (default ${DEFAULT_LIMIT})
|
|
@@ -146,7 +147,7 @@ function canAsk() {
|
|
|
146
147
|
* newer version has nothing left to do.
|
|
147
148
|
*
|
|
148
149
|
* Every command that does real work goes through here: `run`, `view` and
|
|
149
|
-
* `build`. It went in the
|
|
150
|
+
* `build`. It went in the execute path first and stayed there, so `view` — the
|
|
150
151
|
* command somebody is most likely to leave running — was the one that never
|
|
151
152
|
* looked.
|
|
152
153
|
*
|
|
@@ -213,12 +214,17 @@ async function main(argv) {
|
|
|
213
214
|
|
|
214
215
|
const command = args.shift();
|
|
215
216
|
if (command === 'view' || command === 'build') return page(command, args);
|
|
217
|
+
if (command === 'clear') return clear(args);
|
|
216
218
|
if (command === 'upgrade') {
|
|
217
219
|
const { message, newer } = await updateNotice({ version: VERSION, force: true });
|
|
218
220
|
if (message) process.stderr.write(`${message}\n`);
|
|
219
221
|
return newer ? upgrade(newer) : 0;
|
|
220
222
|
}
|
|
221
|
-
|
|
223
|
+
// `execute` and `exec` are aliases and stay undocumented: one name is the name.
|
|
224
|
+
// `run` because it is published in every release since 0.3.0 and breaking it
|
|
225
|
+
// silently would be rude; `exec` because seven characters is a lot to type in
|
|
226
|
+
// a loop (869erp0jd).
|
|
227
|
+
if (!['execute', 'exec', 'run'].includes(command)) {
|
|
222
228
|
process.stderr.write(`unknown command "${command}"\n\n${USAGE}`);
|
|
223
229
|
return 2;
|
|
224
230
|
}
|
|
@@ -246,7 +252,7 @@ async function main(argv) {
|
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
if (!files.length) {
|
|
249
|
-
process.stderr.write('
|
|
255
|
+
process.stderr.write('execute needs at least one file\n');
|
|
250
256
|
return 2;
|
|
251
257
|
}
|
|
252
258
|
if (!options.quiet) process.stderr.write(`${RUNAWAY_WARNING}\n`);
|
|
@@ -282,7 +288,7 @@ async function main(argv) {
|
|
|
282
288
|
status = Math.max(status, await runFile(file, session, options));
|
|
283
289
|
}
|
|
284
290
|
|
|
285
|
-
// stderr, always: `
|
|
291
|
+
// stderr, always: `execute --stdout` is a notebook going down a pipe, and a version
|
|
286
292
|
// notice in the middle of it would corrupt the file it is printing.
|
|
287
293
|
const { message, newer } = await update;
|
|
288
294
|
if (message) process.stderr.write(`${message}\n`);
|
|
@@ -292,6 +298,62 @@ async function main(argv) {
|
|
|
292
298
|
return status;
|
|
293
299
|
}
|
|
294
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Take the answers back out.
|
|
303
|
+
*
|
|
304
|
+
* The counterpart to `execute`, and it exists because the alternative was editing
|
|
305
|
+
* the file by hand: a workbook edition with the answers withheld, a diff without
|
|
306
|
+
* nineteen solution sequences in the way, or starting again deliberately rather
|
|
307
|
+
* than trusting an overwrite.
|
|
308
|
+
*
|
|
309
|
+
* No engine, no network and no update check: this is a text operation on a file
|
|
310
|
+
* the reader already has.
|
|
311
|
+
*/
|
|
312
|
+
async function clear(args) {
|
|
313
|
+
const options = { stdout: false, quiet: false };
|
|
314
|
+
const files = [];
|
|
315
|
+
for (const arg of args) {
|
|
316
|
+
if (arg === '--stdout') options.stdout = true;
|
|
317
|
+
else if (arg === '--quiet') options.quiet = true;
|
|
318
|
+
else if (arg.startsWith('-')) {
|
|
319
|
+
process.stderr.write(`unknown option "${arg}"\n`);
|
|
320
|
+
return 2;
|
|
321
|
+
} else files.push(arg);
|
|
322
|
+
}
|
|
323
|
+
if (!files.length) {
|
|
324
|
+
process.stderr.write('clear needs at least one file\n');
|
|
325
|
+
return 2;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let status = 0;
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
let source;
|
|
331
|
+
let emptied;
|
|
332
|
+
try {
|
|
333
|
+
source = readFileSync(file, 'utf8');
|
|
334
|
+
emptied = clearedSource(parse(source));
|
|
335
|
+
} catch (e) {
|
|
336
|
+
process.stderr.write(`${file}: ${e.message}\n`);
|
|
337
|
+
status = 1;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (options.stdout) {
|
|
341
|
+
process.stdout.write(emptied.text);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (emptied.text === source) {
|
|
345
|
+
if (!options.quiet) process.stderr.write(`${basename(file)}: nothing to remove\n`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
writeFileSync(file, emptied.text);
|
|
349
|
+
if (!options.quiet) {
|
|
350
|
+
const n = emptied.cleared;
|
|
351
|
+
process.stderr.write(`${basename(file)}: ${n} answer${n === 1 ? '' : 's'} removed\n`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return status;
|
|
355
|
+
}
|
|
356
|
+
|
|
295
357
|
/**
|
|
296
358
|
* `build` and `view`, which are the same page put in two different places.
|
|
297
359
|
*
|
|
@@ -322,17 +384,24 @@ async function page(command, args) {
|
|
|
322
384
|
return 2;
|
|
323
385
|
}
|
|
324
386
|
|
|
325
|
-
// The same offer the
|
|
387
|
+
// The same offer the execute path makes, and for the same reason: a server about to
|
|
326
388
|
// start, or a directory about to be written, is work that a newer version
|
|
327
389
|
// should be doing.
|
|
328
390
|
const jump = await upgradeFirst();
|
|
329
391
|
if (jump !== null) return jump;
|
|
330
392
|
|
|
331
393
|
const file = files[0];
|
|
394
|
+
// ASKED AGAIN ON EVERY REQUEST, and built again only when the bytes have moved
|
|
395
|
+
// (869erpuhk). `build` takes the first answer and writes it; `view` keeps the
|
|
396
|
+
// producer, so a reload shows the chapter as it is now rather than as it was
|
|
397
|
+
// when the server started.
|
|
398
|
+
const pages = livePages(() => readFileSync(file, 'utf8'), {
|
|
399
|
+
filename: basename(file),
|
|
400
|
+
onError: (e) => process.stderr.write(`${file}: ${e.message}\n`),
|
|
401
|
+
});
|
|
332
402
|
let built;
|
|
333
403
|
try {
|
|
334
|
-
|
|
335
|
-
built = buildFiles(parse(source), source, { filename: basename(file) });
|
|
404
|
+
built = pages();
|
|
336
405
|
} catch (e) {
|
|
337
406
|
process.stderr.write(`${file}: ${e.message}\n`);
|
|
338
407
|
return 1;
|
|
@@ -351,7 +420,7 @@ async function page(command, args) {
|
|
|
351
420
|
return 0;
|
|
352
421
|
}
|
|
353
422
|
|
|
354
|
-
const server = await serve(
|
|
423
|
+
const server = await serve(pages, { port: options.port });
|
|
355
424
|
// THE URL IS THIS COMMAND'S OUTPUT. `view` writes no notebook and no data to
|
|
356
425
|
// stdout, so there is nothing for it to corrupt — and a URL on stderr is a URL
|
|
357
426
|
// a wrapper does not see, which is how somebody came to type localhost by hand
|
package/package.json
CHANGED
package/src/build-info.json
CHANGED
package/src/build.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// contain — generated text, or a path to copy — so that `build` can write it,
|
|
16
16
|
// `view` can serve it, and a test can read it, without any of the three
|
|
17
17
|
// disagreeing about what a page is.
|
|
18
|
+
import { parse } from './format.js';
|
|
18
19
|
import { renderNotebook } from './render.js';
|
|
19
20
|
|
|
20
21
|
/** The runtime a page needs. Copied side by side, so their relative imports hold. */
|
|
@@ -67,6 +68,82 @@ export function buildFiles(notebook, source, options = {}) {
|
|
|
67
68
|
return files;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The page, rebuilt whenever the notebook has changed underneath it (869erpuhk).
|
|
73
|
+
*
|
|
74
|
+
* `view` used to build once and serve that forever, so an author who edited their
|
|
75
|
+
* chapter and reloaded was shown the version the server had started with. A
|
|
76
|
+
* reload is the gesture for "show me what I just did"; answering it with the old
|
|
77
|
+
* page teaches an author to doubt their own edit.
|
|
78
|
+
*
|
|
79
|
+
* COMPARED BY BYTES, not by mtime. The point of a rebuild here is to be right
|
|
80
|
+
* rather than quick — an editor that writes through a rename, a `git checkout`, a
|
|
81
|
+
* clock that went backwards, two saves inside one millisecond, all of them are
|
|
82
|
+
* changes and none of them reliably move an mtime the way one would hope. Reading
|
|
83
|
+
* a chapter is microseconds and reparsing one is milliseconds, on a file the
|
|
84
|
+
* author has open anyway.
|
|
85
|
+
*
|
|
86
|
+
* A BROKEN FILE KEEPS THE LAST GOOD PAGE AND SAYS SO. Silently serving the
|
|
87
|
+
* previous version would be the same bug this exists to fix, so the page carries
|
|
88
|
+
* the parser's own message. Blanking it instead would throw away a chapter over a
|
|
89
|
+
* half-typed fence — an author saves mid-thought, and the useful thing on screen
|
|
90
|
+
* is the last version that worked, labelled.
|
|
91
|
+
*
|
|
92
|
+
* @param {() => string} read the notebook's bytes, now
|
|
93
|
+
* @param {{onError?: (e: Error) => void}} [options] and everything buildFiles takes
|
|
94
|
+
* @returns {() => Map<string, {text: string}|{copy: URL}>}
|
|
95
|
+
*/
|
|
96
|
+
export function livePages(read, { onError = () => {}, ...options } = {}) {
|
|
97
|
+
let last = null;
|
|
98
|
+
let failing = null;
|
|
99
|
+
return () => {
|
|
100
|
+
let source = null;
|
|
101
|
+
try {
|
|
102
|
+
source = read();
|
|
103
|
+
if (last && last.source === source) return last.files;
|
|
104
|
+
const files = buildFiles(parse(source), source, options);
|
|
105
|
+
last = { source, files };
|
|
106
|
+
failing = null;
|
|
107
|
+
return files;
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Nothing good to fall back to: this is the first build, and the caller —
|
|
110
|
+
// the command — is the one that should report it and stop.
|
|
111
|
+
if (!last) throw e;
|
|
112
|
+
// Once per broken version, not once per request. A page fetches a dozen
|
|
113
|
+
// files, and a terminal repeating the same syntax error a dozen times is
|
|
114
|
+
// worse at communicating it than saying it once.
|
|
115
|
+
if (failing !== source) onError(e);
|
|
116
|
+
failing = source;
|
|
117
|
+
return withNotice(last.files, e.message);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The last good page, wearing the reason it is not the current one.
|
|
124
|
+
*
|
|
125
|
+
* A copy, so the good build is never mutated and recovering is simply serving it
|
|
126
|
+
* again.
|
|
127
|
+
*/
|
|
128
|
+
function withNotice(files, message) {
|
|
129
|
+
const index = files.get('index.html');
|
|
130
|
+
if (index?.text === undefined) return files;
|
|
131
|
+
const copy = new Map(files);
|
|
132
|
+
copy.set('index.html', { text: index.text.replace('<body>', `<body>\n${notice(message)}`) });
|
|
133
|
+
return copy;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Inline styles, deliberately: this belongs to no chapter and must never depend
|
|
138
|
+
* on a stylesheet the broken file might itself have been changing.
|
|
139
|
+
*/
|
|
140
|
+
function notice(message) {
|
|
141
|
+
return '<div role="alert" style="position:sticky;top:0;z-index:99;padding:.7rem 1rem;'
|
|
142
|
+
+ 'background:#7a2618;color:#fff;font:500 .8rem/1.5 ui-monospace,Menlo,monospace">'
|
|
143
|
+
+ '<strong>This notebook does not currently parse.</strong> Showing the last version that'
|
|
144
|
+
+ ` did.<br>${escapeHtml(message)}</div>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
70
147
|
/**
|
|
71
148
|
* The chapter's own title, from its first H1 (format §2).
|
|
72
149
|
*
|
package/src/export.js
CHANGED
|
@@ -94,6 +94,29 @@ export function exportSource(notebook, edits) {
|
|
|
94
94
|
return serialise(withEdits(notebook, edits));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The chapter with its answers taken back out.
|
|
99
|
+
*
|
|
100
|
+
* A query cell with no output block is valid — the result is a chapter that
|
|
101
|
+
* simply has not been executed yet, and `execute` will fill it in again. Nothing
|
|
102
|
+
* else moves: prose, program cells, goals and attributes are the author's, and
|
|
103
|
+
* this is not an excuse to reformat them.
|
|
104
|
+
*
|
|
105
|
+
* It goes through the SAME ERASURE PATH the reader's download already uses:
|
|
106
|
+
* `output: null` has always meant "there are no answers to write down here"
|
|
107
|
+
* (869ejgbxf), so nothing new decides what an emptied cell means.
|
|
108
|
+
*
|
|
109
|
+
* @param {{frontMatter: Map<string, string>, cells: object[]}} notebook
|
|
110
|
+
* @returns {{text: string, cleared: number}} the bytes, and how many were emptied
|
|
111
|
+
*/
|
|
112
|
+
export function clearedSource(notebook) {
|
|
113
|
+
const edits = new Map();
|
|
114
|
+
for (const cell of notebook.cells) {
|
|
115
|
+
if (cell.kind === 'query' && cell.output) edits.set(cell.id, { output: null });
|
|
116
|
+
}
|
|
117
|
+
return { text: exportSource(notebook, edits), cleared: edits.size };
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
/**
|
|
98
121
|
* A filename for the reader's copy.
|
|
99
122
|
*
|
package/src/notebook.js
CHANGED
|
@@ -198,6 +198,14 @@ const ICONS = {
|
|
|
198
198
|
download: '<path d="M12 3.6v10.6"/><path d="m7.6 10.2 4.4 4.4 4.4-4.4"/><path d="M4.6 19.4h14.8"/>',
|
|
199
199
|
hide: '<path d="M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.7"/><path d="M4 4l16 16"/>',
|
|
200
200
|
show: '<path d="M2.5 12S6 6 12 6s9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z"/><circle cx="12" cy="12" r="2.7"/>',
|
|
201
|
+
// An eraser rubbing something out, NOT a bin. What clear does is undone by the
|
|
202
|
+
// button beside it, and a lid-and-body icon promises a finality this control
|
|
203
|
+
// does not have.
|
|
204
|
+
erase: '<path d="m7 21-4.3-4.3a2.4 2.4 0 0 1 0-3.4l9.6-9.6a2.4 2.4 0 0 1 3.4 0l5.6 5.6a2.4 2.4 0 0 1 0 3.4L13 21"/><path d="M21.4 21H7"/><path d="m5 11 9 9"/>',
|
|
205
|
+
// The undo hook, deliberately not the engine's circular arrow two rows below:
|
|
206
|
+
// two glyphs that near-match in one small card is how a reader presses the
|
|
207
|
+
// wrong one.
|
|
208
|
+
restore: '<path d="M3.2 7.4v6.2h6.2"/><path d="M20.8 17.2a9 9 0 0 0-9-9 9 9 0 0 0-6.4 2.6L3.2 13.6"/>',
|
|
201
209
|
};
|
|
202
210
|
|
|
203
211
|
function icon(name) {
|
|
@@ -226,6 +234,13 @@ function mountPageBar(root, options, bus, programs, queries) {
|
|
|
226
234
|
+ `<span class="dot"></span><span class="count"></span><span class="chev">${icon('chevron')}</span></button>`
|
|
227
235
|
+ `<div class="panel" id="${panelId}">`
|
|
228
236
|
+ '<div class="unit answers"><span class="state answers-state"></span></div>'
|
|
237
|
+
// THE SAME ANSWERS, THE OTHER QUESTION. The row above is about the screen —
|
|
238
|
+
// put them out of sight, work the chapter cold, they are still the chapter's.
|
|
239
|
+
// This one is about the FILE: an output cleared here is gone from the page
|
|
240
|
+
// and from a download of it, which is why its verb is undone by restore and
|
|
241
|
+
// not by show. Two rows because they are two questions, adjacent because a
|
|
242
|
+
// reader comparing them is exactly what tells them apart.
|
|
243
|
+
+ '<div class="unit outputs"><span class="state outputs-state"></span></div>'
|
|
229
244
|
+ '<div class="unit"><span class="state engine-state"></span>'
|
|
230
245
|
+ `<button data-act="restart"><span class="icon">${icon('power')}</span>`
|
|
231
246
|
+ '<span class="label">Start engine</span></button></div>'
|
|
@@ -346,8 +361,14 @@ function mountPageBar(root, options, bus, programs, queries) {
|
|
|
346
361
|
let age = null;
|
|
347
362
|
|
|
348
363
|
bus.on((event) => {
|
|
349
|
-
// A cell's own hide control moved something this panel is reporting
|
|
350
|
-
|
|
364
|
+
// A cell's own hide or reset control moved something this panel is reporting
|
|
365
|
+
// on — both rows, since a cell restored on its own leaves one fewer cleared
|
|
366
|
+
// and one more showing.
|
|
367
|
+
if (event.kind === 'answers') {
|
|
368
|
+
refreshAnswers();
|
|
369
|
+
refreshOutputs();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
351
372
|
if (event.kind === 'booting') {
|
|
352
373
|
// Lit from wherever the engine was asked for — a Run halfway up the chapter
|
|
353
374
|
// starts it just as this button does, and the light should not care which.
|
|
@@ -440,6 +461,73 @@ function mountPageBar(root, options, bus, programs, queries) {
|
|
|
440
461
|
refreshAnswers();
|
|
441
462
|
}
|
|
442
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Take the answers out of the chapter, and put them back (869erp9ap).
|
|
466
|
+
*
|
|
467
|
+
* The page's half of `prolog-notebook clear`. An author emptying a chapter has
|
|
468
|
+
* the command; a reader who wants the file without the answers — to work
|
|
469
|
+
* through it later, to hand it to somebody, to keep a copy that gives nothing
|
|
470
|
+
* away — had to edit markdown by hand, which is the same "that's tedious" that
|
|
471
|
+
* put the command in the CLI.
|
|
472
|
+
*
|
|
473
|
+
* WHAT MAKES IT SAFE TO OFFER is that it is reversible in the page's existing
|
|
474
|
+
* vocabulary. Clear is a third way AWAY from the chapter, beside running and
|
|
475
|
+
* editing, and reset is still the one way back: per cell for one of them, and
|
|
476
|
+
* restore here for all at once.
|
|
477
|
+
*/
|
|
478
|
+
const outputsUnit = bar.querySelector('.unit.outputs');
|
|
479
|
+
const outputsState = bar.querySelector('.outputs-state');
|
|
480
|
+
let refreshOutputs = () => {};
|
|
481
|
+
// Nothing published to clear, and nothing a restore could give back. The row
|
|
482
|
+
// goes, exactly as the one above it does — an unrun chapter is the CLI's
|
|
483
|
+
// business, and a control whose only possible effect is on the reader's own
|
|
484
|
+
// run is a control offering to undo the thing they just asked for.
|
|
485
|
+
if (!spoilers.length) {
|
|
486
|
+
outputsUnit.remove();
|
|
487
|
+
} else {
|
|
488
|
+
const wipe = document.createElement('button');
|
|
489
|
+
wipe.dataset.act = 'clear-all';
|
|
490
|
+
wipe.innerHTML = '<span class="icon"></span><span class="label"></span>';
|
|
491
|
+
outputsUnit.appendChild(wipe);
|
|
492
|
+
|
|
493
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
494
|
+
|
|
495
|
+
refreshOutputs = () => {
|
|
496
|
+
const gone = queries.filter((q) => q.isCleared());
|
|
497
|
+
const left = queries.filter((q) => q.hasOutput());
|
|
498
|
+
// Restore only once there is nothing left to clear, which is the rule the
|
|
499
|
+
// hide/show button already follows: in a half-cleared page the useful move
|
|
500
|
+
// is to finish, and one vocabulary is cheaper to learn than two.
|
|
501
|
+
const back = left.length === 0 && gone.length > 0;
|
|
502
|
+
// A cleared cell that never had saved answers has nothing of the chapter's
|
|
503
|
+
// to give back, so a page of only those leaves restore with no work.
|
|
504
|
+
const restorable = gone.filter((q) => q.hasSaved).length;
|
|
505
|
+
wipe.disabled = back ? restorable === 0 : left.length === 0;
|
|
506
|
+
// COUNTED FROM THE FILE while nothing is cleared, because that is the fact
|
|
507
|
+
// the reader does not have: how much of this chapter is answers. After
|
|
508
|
+
// that it counts what they did, which is the fact they want confirmed.
|
|
509
|
+
outputsState.textContent = gone.length === 0
|
|
510
|
+
? `${plural(spoilers.length, 'output')} in this chapter`
|
|
511
|
+
: `${plural(gone.length, 'output')} cleared`;
|
|
512
|
+
label(wipe, back ? 'restore' : 'erase',
|
|
513
|
+
back ? 'Restore outputs' : 'Clear all outputs');
|
|
514
|
+
wipe.title = back
|
|
515
|
+
? 'put the chapter’s own answers back into every cell you cleared'
|
|
516
|
+
: 'empty every output on this page, your own runs included — a download'
|
|
517
|
+
+ ' taken then carries none of them, and restore puts the chapter’s back';
|
|
518
|
+
render();
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
wipe.addEventListener('click', () => {
|
|
522
|
+
const left = queries.filter((q) => q.hasOutput());
|
|
523
|
+
if (left.length) for (const q of left) q.clear();
|
|
524
|
+
else for (const q of queries) if (q.isCleared()) q.restore();
|
|
525
|
+
refreshOutputs();
|
|
526
|
+
refreshAnswers();
|
|
527
|
+
});
|
|
528
|
+
refreshOutputs();
|
|
529
|
+
}
|
|
530
|
+
|
|
443
531
|
restart.title = 'download SWI-Prolog and have it ready, so your first Run is not the slow one';
|
|
444
532
|
|
|
445
533
|
restart.addEventListener('click', async () => {
|
|
@@ -823,6 +911,14 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
823
911
|
// to the chapter.
|
|
824
912
|
let mine = false;
|
|
825
913
|
let hidden = false;
|
|
914
|
+
// Emptied on purpose, by the page's own control (869erp9ap). A THIRD state
|
|
915
|
+
// rather than a kind of hiding: hidden answers are still the chapter's and
|
|
916
|
+
// still go into a download, and cleared ones are gone from both. What the two
|
|
917
|
+
// have in common is only that the box looks empty.
|
|
918
|
+
//
|
|
919
|
+
// It is a way AWAY from the chapter, like a run and like an edit, so the way
|
|
920
|
+
// back is the one that has always existed — reset.
|
|
921
|
+
let cleared = false;
|
|
826
922
|
// The author's own spoiler mark (format §5). It is a starting state rather than
|
|
827
923
|
// a lock: the reader can always press show, because withholding the answer from
|
|
828
924
|
// someone who has decided they want it is theatre, not teaching.
|
|
@@ -878,7 +974,11 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
878
974
|
|
|
879
975
|
const refresh = () => {
|
|
880
976
|
if (resetBtn) {
|
|
881
|
-
|
|
977
|
+
// Cleared counts, and it is the reason this cell can be brought back one at
|
|
978
|
+
// a time out of a page-wide clear. A reset button left grey over an emptied
|
|
979
|
+
// cell would say the answers are gone for good, which is the opposite of
|
|
980
|
+
// what this control is for.
|
|
981
|
+
const changed = mine || cleared || input.value !== published.goal;
|
|
882
982
|
cell.dataset.edited = String(changed);
|
|
883
983
|
resetBtn.disabled = !changed;
|
|
884
984
|
resetBtn.title = changed
|
|
@@ -1088,9 +1188,13 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
1088
1188
|
// with the reader's own is fine; replacing them SILENTLY is not, so the run is
|
|
1089
1189
|
// labelled and the way back is stated (docs/modes.md §3) — and the way back is
|
|
1090
1190
|
// now a button on this cell rather than a page reload.
|
|
1091
|
-
|
|
1191
|
+
// ASKED OF THE CHAPTER, not of the screen. A cleared cell has nothing on
|
|
1192
|
+
// screen to have come from the chapter, but the chapter's answers are still
|
|
1193
|
+
// one press of reset away — so the note that says so is still owed.
|
|
1194
|
+
const hadSaved = !mine && published.out !== '';
|
|
1092
1195
|
out.innerHTML = '';
|
|
1093
1196
|
mine = true;
|
|
1197
|
+
cleared = false;
|
|
1094
1198
|
// This cell has just stopped showing the chapter's answers, which changes what
|
|
1095
1199
|
// the page's control is counting. setHidden only speaks up when the hidden
|
|
1096
1200
|
// flag itself moves, so a cell that was already visible would leave the count
|
|
@@ -1205,6 +1309,12 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
1205
1309
|
// that quizzes the reader and then answers itself is worse than one that
|
|
1206
1310
|
// never asked.
|
|
1207
1311
|
if (held) return;
|
|
1312
|
+
// AND A CLEARED CELL STAYS CLEARED. Auto exists so that answers do not go
|
|
1313
|
+
// stale under a reader who changed the program; a cell with no answers has
|
|
1314
|
+
// none that can. Refilling it would be the page overruling the reader who
|
|
1315
|
+
// emptied it, and doing so behind their back — they pressed Consult
|
|
1316
|
+
// somewhere else entirely.
|
|
1317
|
+
if (cleared) return;
|
|
1208
1318
|
if (running || query) return;
|
|
1209
1319
|
if (!outOfDate()) return;
|
|
1210
1320
|
bus.queue(() => {
|
|
@@ -1277,18 +1387,59 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
1277
1387
|
* disagreed in fact — the same argument that makes a program cell's reset
|
|
1278
1388
|
* un-consult.
|
|
1279
1389
|
*/
|
|
1280
|
-
|
|
1390
|
+
const restore = ({ goal = true } = {}) => {
|
|
1281
1391
|
query?.close();
|
|
1282
|
-
input.value = published.goal;
|
|
1392
|
+
if (goal) input.value = published.goal;
|
|
1283
1393
|
out.innerHTML = published.out;
|
|
1284
1394
|
mine = false;
|
|
1395
|
+
cleared = false;
|
|
1285
1396
|
ran = null;
|
|
1286
1397
|
engineChanged = false;
|
|
1287
1398
|
decorateSaved();
|
|
1288
1399
|
// Back in the set the page's control acts on, for the same reason.
|
|
1289
1400
|
bus.emit({ kind: 'answers' });
|
|
1290
1401
|
finish();
|
|
1291
|
-
}
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
resetBtn?.addEventListener('click', () => restore());
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Take the answers out — the chapter's included.
|
|
1408
|
+
*
|
|
1409
|
+
* The reader's half of `prolog-notebook clear`, and the same claim: there are
|
|
1410
|
+
* no answers here. Not a stronger kind of hiding — a download taken now
|
|
1411
|
+
* carries no output block for this cell, which is the whole difference and the
|
|
1412
|
+
* reason it is undone by reset rather than by show.
|
|
1413
|
+
*
|
|
1414
|
+
* The reader's own run goes with it, because "clear all output" that leaves
|
|
1415
|
+
* some output on the page has not done what it says. That is no worse than
|
|
1416
|
+
* what reset has always done to a run, and Run reproduces it.
|
|
1417
|
+
*/
|
|
1418
|
+
const clear = () => {
|
|
1419
|
+
if (!mine && published.out === '') return false;
|
|
1420
|
+
// An open sequence in a cell showing nothing is a frame held in the reader's
|
|
1421
|
+
// name against a cell they have emptied — the same argument that makes reset
|
|
1422
|
+
// close one.
|
|
1423
|
+
query?.close();
|
|
1424
|
+
out.innerHTML = '';
|
|
1425
|
+
mine = false;
|
|
1426
|
+
cleared = true;
|
|
1427
|
+
ran = null;
|
|
1428
|
+
engineChanged = false;
|
|
1429
|
+
produced = [];
|
|
1430
|
+
failed = null;
|
|
1431
|
+
exhausted = false;
|
|
1432
|
+
count = 0;
|
|
1433
|
+
// Nothing to hide, and nothing to be held back from: the box is empty, and a
|
|
1434
|
+
// cell that still said "held until you run it" would be describing a wait
|
|
1435
|
+
// that no longer has anything to wait for.
|
|
1436
|
+
hidden = false;
|
|
1437
|
+
held = false;
|
|
1438
|
+
out.classList.remove('answers-hidden');
|
|
1439
|
+
bus.emit({ kind: 'answers' });
|
|
1440
|
+
finish();
|
|
1441
|
+
return true;
|
|
1442
|
+
};
|
|
1292
1443
|
|
|
1293
1444
|
input.addEventListener('input', refresh);
|
|
1294
1445
|
|
|
@@ -1343,11 +1494,23 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
1343
1494
|
* control acts on what is on screen, and after a run the chapter's answers
|
|
1344
1495
|
* are behind reset rather than in front of the reader.
|
|
1345
1496
|
*/
|
|
1346
|
-
showsChapter: () => !mine && published.out !== '',
|
|
1497
|
+
showsChapter: () => !mine && !cleared && published.out !== '',
|
|
1347
1498
|
setHidden,
|
|
1348
1499
|
isHidden: () => hidden,
|
|
1349
|
-
isEdited: () => mine || input.value !== published.goal,
|
|
1500
|
+
isEdited: () => mine || cleared || input.value !== published.goal,
|
|
1350
1501
|
goal: () => input.value,
|
|
1502
|
+
/** Is there an output on screen at all — the chapter's or the reader's? */
|
|
1503
|
+
hasOutput: () => !cleared && (mine || published.out !== ''),
|
|
1504
|
+
isCleared: () => cleared,
|
|
1505
|
+
clear,
|
|
1506
|
+
/**
|
|
1507
|
+
* Put the chapter's answers back, and nothing else.
|
|
1508
|
+
*
|
|
1509
|
+
* The exact inverse of clear, which is what lets the page offer the pair as
|
|
1510
|
+
* one control: an edited goal is not an answer and was not what clear took
|
|
1511
|
+
* away, so restoring one must not quietly discard the other.
|
|
1512
|
+
*/
|
|
1513
|
+
restore: () => restore({ goal: false }),
|
|
1351
1514
|
/**
|
|
1352
1515
|
* This cell's answers for an export, in the format's own spelling (§6).
|
|
1353
1516
|
*
|
|
@@ -1358,6 +1521,10 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
|
|
|
1358
1521
|
* has actually produced.
|
|
1359
1522
|
*/
|
|
1360
1523
|
output: () => {
|
|
1524
|
+
// `null` is the one that ERASES, and it is the same null the CLI's clear
|
|
1525
|
+
// writes (src/export.js): a chapter emptied on the page and one emptied at
|
|
1526
|
+
// the terminal produce the same bytes, because they take the same path.
|
|
1527
|
+
if (cleared) return null;
|
|
1361
1528
|
if (!mine) return undefined;
|
|
1362
1529
|
// Stopping and finishing look the same from outside — both leave no open
|
|
1363
1530
|
// query — so `exhausted` is the only thing that distinguishes them.
|
package/src/serve.js
CHANGED
|
@@ -31,11 +31,24 @@ export function contentType(name) {
|
|
|
31
31
|
/**
|
|
32
32
|
* Serve a built page.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
34
|
+
* THE REQUEST IS WHAT READS THE FILE, when a producer is given rather than a map
|
|
35
|
+
* (869erpuhk). The first version of this held the map it was handed for the life
|
|
36
|
+
* of the process, so `view` served the notebook as it had been at start-up and a
|
|
37
|
+
* reload — the universal gesture for "show me what I just did" — confirmed the
|
|
38
|
+
* old version. An author doubts their edit before they doubt the tool.
|
|
39
|
+
*
|
|
40
|
+
* Asked per request rather than pushed by a watcher, because that is what makes
|
|
41
|
+
* the guarantee unconditional: there is no window in which the page and the file
|
|
42
|
+
* disagree, and nothing to have missed a change. A watcher (869edp5c8) can only
|
|
43
|
+
* ever save the reader a keystroke on top of this.
|
|
44
|
+
*
|
|
45
|
+
* @param {Map<string, {text: string}|{copy: URL}>|(() => Map)} pages what build
|
|
46
|
+
* produced, or something that produces it — called once per request
|
|
35
47
|
* @param {{port?: number, host?: string}} [options]
|
|
36
48
|
* @returns {Promise<{url: string, port: number, close: () => Promise<void>}>}
|
|
37
49
|
*/
|
|
38
|
-
export async function serve(
|
|
50
|
+
export async function serve(pages, { port = 8777, host = '127.0.0.1' } = {}) {
|
|
51
|
+
const files = typeof pages === 'function' ? pages : () => pages;
|
|
39
52
|
// ASK WHETHER ANYBODY IS THERE, on both stacks, before binding to one of them.
|
|
40
53
|
//
|
|
41
54
|
// An IPv6 wildcard listener — `python3 -m http.server --bind ::` — does not
|
|
@@ -49,7 +62,7 @@ export async function serve(files, { port = 8777, host = '127.0.0.1' } = {}) {
|
|
|
49
62
|
// Only GET, and only the names this process generated: the path never
|
|
50
63
|
// reaches the filesystem, so there is nothing for a `..` to escape into.
|
|
51
64
|
const name = decodeURIComponent(new URL(request.url, 'http://x').pathname).replace(/^\//, '');
|
|
52
|
-
const entry = files.get(name === '' ? 'index.html' : name);
|
|
65
|
+
const entry = files().get(name === '' ? 'index.html' : name);
|
|
53
66
|
if (request.method !== 'GET' || !entry) {
|
|
54
67
|
response.writeHead(404, { 'content-type': 'text/plain' }).end('not found\n');
|
|
55
68
|
return;
|
package/src/upgrade.js
CHANGED
|
@@ -67,7 +67,7 @@ export async function globalRoot(exec = run) {
|
|
|
67
67
|
/**
|
|
68
68
|
* Ask a yes/no question, defaulting to yes.
|
|
69
69
|
*
|
|
70
|
-
* ON STDERR, always: `
|
|
70
|
+
* ON STDERR, always: `execute --stdout` is a notebook going down a pipe, and a
|
|
71
71
|
* question in the middle of it would corrupt the file it is writing.
|
|
72
72
|
*
|
|
73
73
|
* @returns {Promise<boolean>}
|
package/src/version.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
export const NAME = 'Prolog Notebook';
|
|
11
11
|
|
|
12
12
|
/** Must equal package.json's `version` — test/run.test.mjs enforces it. */
|
|
13
|
-
export const VERSION = '0.
|
|
13
|
+
export const VERSION = '0.6.1';
|
|
14
14
|
|
|
15
15
|
/** The two facts a licence notice is actually made of. */
|
|
16
16
|
export const YEAR = '2026';
|