tosijs-ui 1.16.7 → 1.16.9
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 +129 -0
- package/README.md +1 -1
- package/dist/doc-browser.d.ts +2 -1
- package/dist/doc-browser.js +138 -52
- package/dist/doc-system/book-html.d.ts +2 -0
- package/dist/doc-system/book-html.js +2 -1
- package/dist/doc-system/book-target.d.ts +24 -0
- package/dist/doc-system/book-target.js +62 -3
- package/dist/doc-system/{site/code-fences.js → code-fences.js} +4 -3
- package/dist/doc-system/site/build-warnings.js +1 -1
- package/dist/doc-system/site/check-examples.js +1 -1
- package/dist/doc-system/site/dev-server.js +12 -2
- package/dist/doc-system/site/docs.d.ts +13 -0
- package/dist/doc-system/site/docs.js +35 -3
- package/dist/doc-system/site/epub.js +24 -4
- package/dist/doc-system/site/index.d.ts +5 -0
- package/dist/doc-system/site/index.js +7 -0
- package/dist/doc-system/site/orchestrator.js +8 -0
- package/dist/doc-system/site/single-source.d.ts +31 -0
- package/dist/doc-system/site/single-source.js +377 -0
- package/dist/doc-system/site/test-report.d.ts +13 -0
- package/dist/doc-system/site/test-report.js +33 -0
- package/dist/doc-system/test-completion.d.ts +43 -0
- package/dist/doc-system/test-completion.js +54 -0
- package/dist/iife.js +105 -101
- package/dist/iife.js.map +13 -11
- package/dist/live-example/code-transform.d.ts +4 -2
- package/dist/live-example/code-transform.js +21 -6
- package/dist/live-example/component.d.ts +63 -0
- package/dist/live-example/component.js +321 -9
- package/dist/live-example/dialects.d.ts +4 -0
- package/dist/live-example/styles.d.ts +46 -0
- package/dist/live-example/styles.js +62 -0
- package/dist/live-example/types.d.ts +11 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- /package/dist/doc-system/{site/code-fences.d.ts → code-fences.d.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,134 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.9
|
|
4
|
+
|
|
5
|
+
Two sets of asks from tjs-lang for *The TJS Programming Language*, a book that teaches
|
|
6
|
+
programming in the page. Single-sourcing for books (#217): one corpus, several outputs,
|
|
7
|
+
nothing kept in sync by hand. And live examples that open with their code showing and a
|
|
8
|
+
console that speaks the example's language (#216). Additive, with one behaviour change to
|
|
9
|
+
know about: in a `tjs` or `ts` example, or one in a registered dialect, a line typed at the
|
|
10
|
+
console is now evaluated in that language and not as JavaScript. `dist/iife.js` grows by
|
|
11
|
+
0.7 kB gzip.
|
|
12
|
+
|
|
13
|
+
### The book opens on the project, not on "Adopting the doc system"
|
|
14
|
+
|
|
15
|
+
Both pages are pinned to the top, and the adopting page had an `order` where the README had
|
|
16
|
+
none, so it sorted first: the ePub's opening chapter, and the first entry in the site nav, was
|
|
17
|
+
a page about adopting the doc system. The README now leads both.
|
|
18
|
+
|
|
19
|
+
### One source, several outputs (#217)
|
|
20
|
+
|
|
21
|
+
- **Per-volume placement.** A page can sit in a different place in each book:
|
|
22
|
+
`{"placement": {"language": {"parent": "for-programmers", "order": 20}}}` in its metadata.
|
|
23
|
+
Each entry may set `parent`, `order`, `pin` and `title`, and naming a volume also binds the
|
|
24
|
+
page into it. The site nav is unaffected.
|
|
25
|
+
- **Text insets.** `<!--{ "inset": "install.md#with-bun" }-->` includes another document, or
|
|
26
|
+
one heading's section of it, in the page. A fragment (a markdown file whose name starts
|
|
27
|
+
with `_`, in a directory the docs are extracted from) can be inset without being a page, and is the only kind of
|
|
28
|
+
file an inset reads. Resolved at extraction, so the site, the ePubs, `llms.txt` and the markdown copies
|
|
29
|
+
all get the assembled text, and an inset passage's live examples and tests run where it is
|
|
30
|
+
inset.
|
|
31
|
+
- **Conditional text.** `<!--{ "only": "book" | "site" }--> … <!--{ "end": "only" }-->`
|
|
32
|
+
conditional text, for wording that belongs to one output, such as "next chapter" against
|
|
33
|
+
"next page". Books read `bookText`, everything else reads `text`; both are in the published
|
|
34
|
+
`docs.json`, so this chooses wording and does not hide anything.
|
|
35
|
+
- **Each is guarded.** An inset that resolves nowhere, an include cycle, a missing heading, a
|
|
36
|
+
file that is not a fragment, an unbalanced `only`, a directive inside a list, quote or
|
|
37
|
+
paragraph, and a placement under a parent that is not in the volume all fail the build.
|
|
38
|
+
- **Print is not a volume.** The in-app Print output takes the book wording but stays the
|
|
39
|
+
whole site in site order; `placement` arranges ePub volumes.
|
|
40
|
+
- New from `tosijs-ui/site`, for a consumer that builds its corpus some other way:
|
|
41
|
+
`assembleCorpus`, `resolveInsets`, `splitConditions`, `SingleSourceError`, `placeInBook`,
|
|
42
|
+
`bookTextOf`, and the `BookPlacement` type.
|
|
43
|
+
- Saving an edited live example back to source is refused on a page whose source uses an
|
|
44
|
+
inset or conditional text, where the example's position on the page no longer identifies a
|
|
45
|
+
block in that file. No existing page is affected: it applies only to a page that uses one
|
|
46
|
+
of the new directives.
|
|
47
|
+
|
|
48
|
+
### Examples that open with their code showing, and a console in their language (#216)
|
|
49
|
+
|
|
50
|
+
- **A fence picks the view an example opens in.** `{"view": "code"}` in the source block's
|
|
51
|
+
fence options opens the editor beside the preview, in the page and not full-screen;
|
|
52
|
+
`{"view": "console"}` puts the console where the preview would be, beside the code, for
|
|
53
|
+
examples whose point is a value. The editor is built as the example nears the viewport. In
|
|
54
|
+
a narrow column the panes stack. `example.showInline('code' | 'console')` does the same
|
|
55
|
+
from code.
|
|
56
|
+
- **The console evaluates in the example's dialect.** A line goes through the same transform
|
|
57
|
+
as the example's code, with the same fence options, so `x == '5'` in a `tjs` example is
|
|
58
|
+
TJS's `==`. A line the dialect rejects shows the dialect's error and is not retried as
|
|
59
|
+
JavaScript. Plain `js` examples and `run` dialects are unchanged. A registered dialect's
|
|
60
|
+
`transform` now also receives console lines, marked with `options.repl === true`.
|
|
61
|
+
- **Known limit of the above with tjs.** tjs puts setup statements in front of the line it
|
|
62
|
+
translates, so an expression shows its value, but a line that is only a declaration shows a
|
|
63
|
+
leftover value where a browser console shows `undefined`, and a line needing both that
|
|
64
|
+
setup and `await` shows `undefined`. Fixing it needs the setup to be separable, which is
|
|
65
|
+
asked of tjs-lang.
|
|
66
|
+
- The generated JavaScript tjs-lang asked to see already exists as the read-only **JS** tab of
|
|
67
|
+
a `tjs` or `ts` example, and the inline views make it reachable in the page. Showing the
|
|
68
|
+
JavaScript for a single console line is not in this release: a transform's output for one
|
|
69
|
+
line is mostly its runtime prelude, and there is no general way to separate the two.
|
|
70
|
+
|
|
71
|
+
## 1.16.8
|
|
72
|
+
|
|
73
|
+
A patch for honest results. The doc-test lane can no longer pass over pages that never
|
|
74
|
+
reported, three more of tjs-lang's findings are fixed, and an example that only logs now
|
|
75
|
+
shows what it logged. Two things to know on upgrade: `ts` examples lose tjs's runtime
|
|
76
|
+
parameter guards (below), and one thing may newly go red: a doc-test
|
|
77
|
+
run that was silently losing pages will now fail and name them. `dist/iife.js` grows by
|
|
78
|
+
1.0 kB gzip (484.0 → 485.0 kB).
|
|
79
|
+
|
|
80
|
+
### Three more from tjs-lang's sweep of its own site (#210)
|
|
81
|
+
|
|
82
|
+
- **Inline tjs tests ran with plain-JavaScript semantics**, so passing tests were shown
|
|
83
|
+
failing: `"hello world".capitalize is not a function` for a local `extend`, `Expected false
|
|
84
|
+
but got true` for a boxed boolean. This release prepares the fix and tjs-lang delivers it:
|
|
85
|
+
an example's inline tests now use the runner `tjs()` itself returns, and from the tjs-lang
|
|
86
|
+
release after 0.14.0-rc.2 that runner is built from the rewritten test bodies. On 0.13.13,
|
|
87
|
+
which this release still pins, nothing changes.
|
|
88
|
+
- **The `ts` dialect overrode converted TypeScript's semantics.** It passed `dialect: 'tjs'`
|
|
89
|
+
to `tjs()`, which outranks the annotation `fromTS` writes, so a TS example's
|
|
90
|
+
`new Calculator(…)` was refused as native TJS. It now passes no dialect. **This changes
|
|
91
|
+
what every `ts` example does at run time:** converted TypeScript now has JavaScript's
|
|
92
|
+
semantics, so the runtime parameter guards are gone too. `add('a', 2)` with
|
|
93
|
+
`(a: number, b: number)` returned a type error before and returns `'a2'` now, as it would
|
|
94
|
+
in TypeScript. `tjs` examples are unaffected.
|
|
95
|
+
- **An example whose output is only `console.log` was an empty box.** When a run finishes with
|
|
96
|
+
nothing rendered into its preview, the lines it logged (and its inline-test summary, when
|
|
97
|
+
one was run) are shown where the preview would be. It steps aside when the example renders
|
|
98
|
+
anything, never writes into `preview`, and follows the existing console switches. Inline
|
|
99
|
+
examples only: an `:iframe` example's output stays in its Console tab.
|
|
100
|
+
|
|
101
|
+
Not in this release: examples written as modules (`export const x = …`) still fail. Stripping
|
|
102
|
+
`export` safely needs a JavaScript parser, which tosijs-ui does not ship; tjs-lang already
|
|
103
|
+
bundles one, so the ask is with them to export their stripper.
|
|
104
|
+
|
|
105
|
+
### The doc-test lane can no longer pass over pages that reported nothing (#2874)
|
|
106
|
+
|
|
107
|
+
The background test runner counted a page as tested when it finished or timed out, whether or
|
|
108
|
+
not any results arrived, and the lane passed on "0 failed". A run once passed with results for
|
|
109
|
+
7 of 19 pages.
|
|
110
|
+
|
|
111
|
+
- A page with test blocks that reports no tests, or does not finish within 45 seconds, is now
|
|
112
|
+
recorded as a failed test naming the page and the reason. A page that timed out after some
|
|
113
|
+
of its examples had reported was still recorded as a pass in the first cut of this fix; a
|
|
114
|
+
timeout now fails the page whatever had reported.
|
|
115
|
+
- The parent waited 30 seconds per page, the same as the frame's own stall timeout, so a
|
|
116
|
+
stall report could arrive just after the parent moved on. The page budget is now 45s.
|
|
117
|
+
- Which pages have tests is decided by parsing the markdown the way the page is rendered. A
|
|
118
|
+
` ```test ` line shown inside a longer or `:static` block no longer makes a page count as
|
|
119
|
+
one with tests.
|
|
120
|
+
- `bun run test-browser` now exits non-zero unless every page with tests has recorded results,
|
|
121
|
+
and prints how many pages its count covers.
|
|
122
|
+
- The page a run starts on is tested in place, by its own code path. That path replaced the
|
|
123
|
+
page's results each time one example finished, so the last example was the whole page and
|
|
124
|
+
a failing earlier one could vanish; and it marked the page tested on a timer. It now adds
|
|
125
|
+
its examples up and waits for all of them, by the same rule the iframes use. A deployed
|
|
126
|
+
(non-localhost) page with tests enabled follows the same rule.
|
|
127
|
+
- The runner accepted `tosi-test-results` messages from any window on the page; it now
|
|
128
|
+
requires its own test frame as the source.
|
|
129
|
+
- `testReportVerdict` is exported from `tosijs-ui/site` for adopters' own gates. A gate that
|
|
130
|
+
checks only `failed === 0` should also require results for every page with tests.
|
|
131
|
+
|
|
3
132
|
## 1.16.7
|
|
4
133
|
|
|
5
134
|
A patch for readers that are not browsers, and for builds that were too polite. Every doc page
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tosijs-ui
|
|
2
2
|
|
|
3
|
-
<!--{ "pin": "top", "layout": "full-width", "headTitle": "tosijs-ui — robust, standards-based web components", "description": "A small, fast library of standards-based web components (data tables, dialogs, forms, rich text, carousels, and more) built on tosijs — smaller and faster than a React app, with no virtual DOM.", "keywords": "web components, custom elements, tosijs, data table, dialog, form, rich text, no virtual dom" }-->
|
|
3
|
+
<!--{ "pin": "top", "order": 0, "layout": "full-width", "headTitle": "tosijs-ui — robust, standards-based web components", "description": "A small, fast library of standards-based web components (data tables, dialogs, forms, rich text, carousels, and more) built on tosijs — smaller and faster than a React app, with no virtual DOM.", "keywords": "web components, custom elements, tosijs, data table, dialog, form, rich text, no virtual dom" }-->
|
|
4
4
|
|
|
5
5
|
[ui.tosijs.net live demo](https://ui.tosijs.net) | [tosijs](https://tosijs.net) | [discord](https://discord.gg/ramJ9rgky5) | [github](https://github.com/tonioloewald/tosijs-ui#readme) | [npm](https://www.npmjs.com/package/tosijs-ui)
|
|
6
6
|
|
package/dist/doc-browser.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TestResults } from './live-example/test-harness.js';
|
|
2
|
-
|
|
2
|
+
import { type ExamplePolicy } from './doc-system/example-policy.js';
|
|
3
|
+
export declare const hasTestBlock: (text: string | undefined, policy?: ExamplePolicy) => boolean;
|
|
3
4
|
export interface PageTestResults {
|
|
4
5
|
passed: boolean;
|
|
5
6
|
tests: TestResults['tests'];
|
package/dist/doc-browser.js
CHANGED
|
@@ -156,10 +156,12 @@ import { buildSlugMap, pathForSlug, filenameForPath, } from './doc-system/routin
|
|
|
156
156
|
import { buildNavTree } from './doc-system/nav-tree.js';
|
|
157
157
|
import { renderDocMarkdown } from './doc-system/render.js';
|
|
158
158
|
import { pageTitle } from './doc-system/doc-title.js';
|
|
159
|
-
import { unsettledExamples } from './doc-system/test-completion.js';
|
|
159
|
+
import { unsettledExamples, tallyExamples, closePage, } from './doc-system/test-completion.js';
|
|
160
160
|
import { LiveExample, testManager, pageTestCount, enableTests, disableTests, } from './live-example.js';
|
|
161
161
|
import { highlightBlocks } from './doc-system/highlight.js';
|
|
162
162
|
import { examplePolicy } from './live-example/insert-examples.js';
|
|
163
|
+
import { collectCodeTokens } from './doc-system/code-fences.js';
|
|
164
|
+
import { isLiveFence } from './doc-system/example-policy.js';
|
|
163
165
|
import { tosiSidenav, TosiSidenav } from './side-nav.js';
|
|
164
166
|
import { icons } from './icons.js';
|
|
165
167
|
import { tosiLocalized } from './localize.js';
|
|
@@ -183,8 +185,17 @@ standalone run reporting "62 passed" against a build the full suite failed on.
|
|
|
183
185
|
A fence is only a fence at the start of a line, which is the rule the doc extractor already
|
|
184
186
|
applies to `/*#`. `[ \t]*` allows the indented fences that appear inside list items.
|
|
185
187
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
/*
|
|
189
|
+
…and a line-anchored regex was not the rule either (#2874). A ` ```test ` line SHOWN inside a
|
|
190
|
+
longer or `:static` block matched it, so a page that documents the test tier was counted as a
|
|
191
|
+
page with tests, ran in a background iframe, reported nothing, and was counted as tested. The
|
|
192
|
+
lane said "19 of 19 pages" over results for 18. Code blocks now come from marked, the parser
|
|
193
|
+
the page is rendered with, and liveness from `isLiveFence`, under the site's policy.
|
|
194
|
+
*/
|
|
195
|
+
export const hasTestBlock = (text, policy = 'auto') => typeof text === 'string' &&
|
|
196
|
+
// Cheap reject first: lexing every doc in a large corpus is not free.
|
|
197
|
+
text.includes('test') &&
|
|
198
|
+
collectCodeTokens(text).some((t) => t.lang === 'test' && isLiveFence('test', t.mode, policy));
|
|
188
199
|
const { div, span, a, header, button, template, input, h2, img, details, summary, ul, li, } = elements;
|
|
189
200
|
/*
|
|
190
201
|
Test colors, and the text color that goes ON each — because contrast is a PAIR.
|
|
@@ -498,10 +509,15 @@ export function createDocBrowser(options) {
|
|
|
498
509
|
let refreshNav = () => { };
|
|
499
510
|
// Test result tracking
|
|
500
511
|
const pageTestResults = {};
|
|
512
|
+
// The per-example results behind the entry for the page the reader is on.
|
|
513
|
+
let liveResults = new Map();
|
|
514
|
+
let liveResultsFile = '';
|
|
501
515
|
let testResultsResolve;
|
|
502
516
|
let backgroundTestsStarted = false;
|
|
503
517
|
let pagesWithTests = 0;
|
|
504
518
|
let pagesTested = 0;
|
|
519
|
+
// Per-page budget for the background runner; see the wait loop for why it exceeds 30s.
|
|
520
|
+
const PAGE_DEADLINE_MS = 45_000;
|
|
505
521
|
// Set up global promise for scriptable browser integration. A memory-routed
|
|
506
522
|
// (embedded) browser must not clobber the host page's global.
|
|
507
523
|
if (!memoryRouting) {
|
|
@@ -554,15 +570,93 @@ export function createDocBrowser(options) {
|
|
|
554
570
|
const handleTestComplete = (event) => {
|
|
555
571
|
const { results } = event.detail;
|
|
556
572
|
const filename = String(app.currentDoc.filename);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
573
|
+
/*
|
|
574
|
+
One entry per EXAMPLE, added up — `testcomplete` fires once for each. This used to
|
|
575
|
+
replace the page's entry on every event, so the last example to finish was the whole
|
|
576
|
+
page: a failing first example followed by a passing second one reported green.
|
|
577
|
+
|
|
578
|
+
Keyed by element, so an example that re-runs (an edit, a refresh) replaces its own
|
|
579
|
+
result and nothing else's. A different page starts a fresh tally, and elements no longer
|
|
580
|
+
in the document are dropped first.
|
|
581
|
+
*/
|
|
582
|
+
if (liveResultsFile !== filename) {
|
|
583
|
+
liveResultsFile = filename;
|
|
584
|
+
liveResults = new Map();
|
|
585
|
+
}
|
|
586
|
+
// Navigating away and back re-creates the page's examples; the old elements are gone
|
|
587
|
+
// from the document and their results must go with them, or the page counts double.
|
|
588
|
+
for (const element of liveResults.keys()) {
|
|
589
|
+
if (!element.isConnected)
|
|
590
|
+
liveResults.delete(element);
|
|
591
|
+
}
|
|
592
|
+
liveResults.set(event.detail.element, results);
|
|
593
|
+
pageTestResults[filename] = tallyExamples(liveResults.values());
|
|
564
594
|
updateDocTestStatus(filename);
|
|
565
595
|
};
|
|
596
|
+
/*
|
|
597
|
+
When has the page now rendered in THIS document finished its tests? Resolves with the
|
|
598
|
+
examples that never settled (empty when all did).
|
|
599
|
+
|
|
600
|
+
One definition, used by a test iframe to signal its parent, by the page the reader is on,
|
|
601
|
+
and by a deployed page — the latter two used to be a flat timer, which marked a page tested
|
|
602
|
+
whether or not anything had reported.
|
|
603
|
+
|
|
604
|
+
The census is only as good as the DOM it reads. Examples are inserted into the page as it
|
|
605
|
+
renders, so a census taken while insertion is still in progress sees FEWER examples than the
|
|
606
|
+
page has — and an example that appears afterwards was never waited on, never reported, and
|
|
607
|
+
never missed. So the count must be observed to STOP GROWING before "done" means anything:
|
|
608
|
+
two consecutive equal censuses, and a third example arriving later restarts the requirement.
|
|
609
|
+
*/
|
|
610
|
+
const whenPageSettled = (deadlineMs) => new Promise((resolve) => {
|
|
611
|
+
const startedAt = Date.now();
|
|
612
|
+
let previousCount = -1;
|
|
613
|
+
let stableCensuses = 0;
|
|
614
|
+
const census = () => {
|
|
615
|
+
const examples = [...container.querySelectorAll('tosi-example')];
|
|
616
|
+
if (examples.length === previousCount)
|
|
617
|
+
stableCensuses++;
|
|
618
|
+
else
|
|
619
|
+
stableCensuses = 0;
|
|
620
|
+
previousCount = examples.length;
|
|
621
|
+
const stalled = unsettledExamples(examples.map((ex) => ({
|
|
622
|
+
element: ex,
|
|
623
|
+
test: ex.test,
|
|
624
|
+
hasTests: ex.classList.contains('-has-tests'),
|
|
625
|
+
testRunning: ex.classList.contains('-test-running'),
|
|
626
|
+
})));
|
|
627
|
+
const settling = stalled.length > 0 || stableCensuses < 2;
|
|
628
|
+
if (settling && Date.now() - startedAt < deadlineMs) {
|
|
629
|
+
setTimeout(census, 100);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
resolve(stalled.map(({ element }) => element));
|
|
633
|
+
};
|
|
634
|
+
// Give the page a moment to insert its examples before the first census.
|
|
635
|
+
setTimeout(census, 500);
|
|
636
|
+
});
|
|
637
|
+
/*
|
|
638
|
+
The page the reader is on runs its tests in place. Wait for them the way a test iframe is
|
|
639
|
+
waited for, then close the entry by the same rule (`closePage`): examples that never
|
|
640
|
+
settled, or no results at all, are failures with a name.
|
|
641
|
+
*/
|
|
642
|
+
const settleCurrentPage = async (filename) => {
|
|
643
|
+
const stalled = await whenPageSettled(PAGE_DEADLINE_MS);
|
|
644
|
+
/*
|
|
645
|
+
If the reader navigated away, the census was of ANOTHER page and says nothing about this
|
|
646
|
+
one. Whatever this page had reported stands, and if it reported nothing no failure is
|
|
647
|
+
invented for it: the report then has one page fewer than it should, which is the honest
|
|
648
|
+
statement ("we did not look") and is what `testReportVerdict` fails on.
|
|
649
|
+
*/
|
|
650
|
+
if (String(app.currentDoc.filename) === filename) {
|
|
651
|
+
pageTestResults[filename] = closePage(pageTestResults[filename], {
|
|
652
|
+
timedOut: false,
|
|
653
|
+
deadlineMs: PAGE_DEADLINE_MS,
|
|
654
|
+
stalled: stalled.length,
|
|
655
|
+
});
|
|
656
|
+
updateDocTestStatus(filename);
|
|
657
|
+
}
|
|
658
|
+
markPageTested(filename);
|
|
659
|
+
};
|
|
566
660
|
// Track when a page finishes loading all its tests
|
|
567
661
|
const markPageTested = (_filename) => {
|
|
568
662
|
pagesTested++;
|
|
@@ -1769,36 +1863,7 @@ export function createDocBrowser(options) {
|
|
|
1769
1863
|
if (isTestFrame && testFrameFilename) {
|
|
1770
1864
|
// Generous: this bounds a hung example, it does not pace a slow one.
|
|
1771
1865
|
const STALL_TIMEOUT_MS = 30000;
|
|
1772
|
-
|
|
1773
|
-
/*
|
|
1774
|
-
The census is only as good as the DOM it reads. Examples are inserted into the page as
|
|
1775
|
-
it renders, so a census taken while insertion is still in progress sees FEWER examples
|
|
1776
|
-
than the page has — and an example that appears afterwards was never waited on, never
|
|
1777
|
-
reported, and never missed. That is the same silent-loss failure as the bug above, one
|
|
1778
|
-
step earlier, so the count must be observed to STOP GROWING before "done" means
|
|
1779
|
-
anything. Two consecutive equal censuses is enough; a third example arriving later
|
|
1780
|
-
restarts the requirement.
|
|
1781
|
-
*/
|
|
1782
|
-
let previousCount = -1;
|
|
1783
|
-
let stableCensuses = 0;
|
|
1784
|
-
const signalDone = () => {
|
|
1785
|
-
const examples = [...container.querySelectorAll('tosi-example')];
|
|
1786
|
-
if (examples.length === previousCount)
|
|
1787
|
-
stableCensuses++;
|
|
1788
|
-
else
|
|
1789
|
-
stableCensuses = 0;
|
|
1790
|
-
previousCount = examples.length;
|
|
1791
|
-
const stalled = unsettledExamples(examples.map((ex) => ({
|
|
1792
|
-
element: ex,
|
|
1793
|
-
test: ex.test,
|
|
1794
|
-
hasTests: ex.classList.contains('-has-tests'),
|
|
1795
|
-
testRunning: ex.classList.contains('-test-running'),
|
|
1796
|
-
})));
|
|
1797
|
-
const settling = stalled.length > 0 || stableCensuses < 2;
|
|
1798
|
-
if (settling && Date.now() - startedAt < STALL_TIMEOUT_MS) {
|
|
1799
|
-
setTimeout(signalDone, 100);
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1866
|
+
void whenPageSettled(STALL_TIMEOUT_MS).then((stalled) => {
|
|
1802
1867
|
/*
|
|
1803
1868
|
Past the deadline with examples still unsettled: report them as failures rather than
|
|
1804
1869
|
signalling a done that omits them. An example that never finishes is a real defect —
|
|
@@ -1812,7 +1877,7 @@ export function createDocBrowser(options) {
|
|
|
1812
1877
|
results: {
|
|
1813
1878
|
passed: 0,
|
|
1814
1879
|
failed: stalled.length,
|
|
1815
|
-
tests: stalled.map((
|
|
1880
|
+
tests: stalled.map((element) => ({
|
|
1816
1881
|
name: `example never finished running (${STALL_TIMEOUT_MS}ms)`,
|
|
1817
1882
|
passed: false,
|
|
1818
1883
|
error: `The example's code did not complete, so its test block never ran. Its source begins: ${String(element.js ?? '').slice(0, 120)}`,
|
|
@@ -1821,9 +1886,7 @@ export function createDocBrowser(options) {
|
|
|
1821
1886
|
}, '*');
|
|
1822
1887
|
}
|
|
1823
1888
|
window.parent.postMessage({ type: 'tosi-tests-done', filename: testFrameFilename }, '*');
|
|
1824
|
-
};
|
|
1825
|
-
// Give the page a moment to insert its examples before taking the census.
|
|
1826
|
-
setTimeout(signalDone, 500);
|
|
1889
|
+
});
|
|
1827
1890
|
}
|
|
1828
1891
|
// Background test runner for all doc pages
|
|
1829
1892
|
const runBackgroundTests = async () => {
|
|
@@ -1847,7 +1910,7 @@ export function createDocBrowser(options) {
|
|
|
1847
1910
|
exits 0. So a runtime error in an example on a page with no ` ```test ` block is caught by
|
|
1848
1911
|
nothing. Tracked as its own issue rather than papered over by making this predicate loose.
|
|
1849
1912
|
*/
|
|
1850
|
-
const docsWithTests = docs.filter((doc) => hasTestBlock(doc.text));
|
|
1913
|
+
const docsWithTests = docs.filter((doc) => hasTestBlock(doc.text, examplePolicy()));
|
|
1851
1914
|
pagesWithTests = docsWithTests.length;
|
|
1852
1915
|
if (pagesWithTests > 0) {
|
|
1853
1916
|
setTestWidgetRunning();
|
|
@@ -1895,6 +1958,9 @@ export function createDocBrowser(options) {
|
|
|
1895
1958
|
const messageHandler = (event) => {
|
|
1896
1959
|
if (event.data?.type !== 'tosi-test-results')
|
|
1897
1960
|
return;
|
|
1961
|
+
// Only from OUR test frame: any other window on the page could otherwise post results.
|
|
1962
|
+
if (event.source !== testFrame.contentWindow)
|
|
1963
|
+
return;
|
|
1898
1964
|
const { filename, results } = event.data;
|
|
1899
1965
|
if (!pageTestResults[filename]) {
|
|
1900
1966
|
pageTestResults[filename] = {
|
|
@@ -1922,11 +1988,15 @@ export function createDocBrowser(options) {
|
|
|
1922
1988
|
routing === 'path'
|
|
1923
1989
|
? `${window.location.origin}${hrefFor(doc.filename)}?_testMode=1`
|
|
1924
1990
|
: `${window.location.origin}${window.location.pathname}?${doc.filename}&_testMode=1`;
|
|
1925
|
-
// Wait for the iframe to signal it's done
|
|
1991
|
+
// Wait for the iframe to signal it's done. LONGER than the frame's own 30s stall
|
|
1992
|
+
// timeout, so a frame that gives up on a hung example gets to say so: at the same 30s
|
|
1993
|
+
// the two raced, and the parent could move on a moment before the stall report landed.
|
|
1994
|
+
let timedOut = false;
|
|
1926
1995
|
await new Promise((resolve) => {
|
|
1927
|
-
const deadline = Date.now() +
|
|
1996
|
+
const deadline = Date.now() + PAGE_DEADLINE_MS;
|
|
1928
1997
|
const onDone = (event) => {
|
|
1929
|
-
if (event.
|
|
1998
|
+
if (event.source === testFrame.contentWindow &&
|
|
1999
|
+
event.data?.type === 'tosi-tests-done' &&
|
|
1930
2000
|
event.data.filename === doc.filename) {
|
|
1931
2001
|
window.removeEventListener('message', onDone);
|
|
1932
2002
|
resolve();
|
|
@@ -1935,17 +2005,24 @@ export function createDocBrowser(options) {
|
|
|
1935
2005
|
window.addEventListener('message', onDone);
|
|
1936
2006
|
setTimeout(() => {
|
|
1937
2007
|
window.removeEventListener('message', onDone);
|
|
2008
|
+
timedOut = true;
|
|
1938
2009
|
resolve();
|
|
1939
2010
|
}, deadline - Date.now());
|
|
1940
2011
|
});
|
|
2012
|
+
// A page that timed out fails even if some of its examples had reported (closePage).
|
|
2013
|
+
pageTestResults[doc.filename] = closePage(pageTestResults[doc.filename], {
|
|
2014
|
+
timedOut,
|
|
2015
|
+
deadlineMs: PAGE_DEADLINE_MS,
|
|
2016
|
+
});
|
|
2017
|
+
updateDocTestStatus(doc.filename);
|
|
1941
2018
|
markPageTested(doc.filename);
|
|
1942
2019
|
}
|
|
1943
2020
|
// Clean up
|
|
1944
2021
|
window.removeEventListener('message', messageHandler);
|
|
1945
2022
|
testFrame.remove();
|
|
1946
|
-
//
|
|
2023
|
+
// The page the reader is on ran its tests in place; wait for them and close its entry.
|
|
1947
2024
|
if (docsWithTests.some((d) => d.filename === currentFilename)) {
|
|
1948
|
-
|
|
2025
|
+
void settleCurrentPage(currentFilename);
|
|
1949
2026
|
}
|
|
1950
2027
|
};
|
|
1951
2028
|
// Run background tests when enabled (initially or when toggled on)
|
|
@@ -1956,11 +2033,20 @@ export function createDocBrowser(options) {
|
|
|
1956
2033
|
setTimeout(runBackgroundTests, 1000);
|
|
1957
2034
|
}
|
|
1958
2035
|
else {
|
|
1959
|
-
|
|
2036
|
+
/*
|
|
2037
|
+
The page showing NOW, not the one this browser was constructed on. Tests are toggled
|
|
2038
|
+
from the menu, often after navigating, and the construction-time doc then named a page
|
|
2039
|
+
that is no longer on screen: it was judged by a census of some other page and recorded
|
|
2040
|
+
as "reported no tests" without ever having run (1.16.8 re-review, F1).
|
|
2041
|
+
*/
|
|
2042
|
+
const showing = String(app.currentDoc.filename);
|
|
2043
|
+
const showingDoc = docs.find((d) => d.filename === showing);
|
|
2044
|
+
const currentHasTests = hasTestBlock(showingDoc?.text, examplePolicy());
|
|
1960
2045
|
if (currentHasTests) {
|
|
1961
2046
|
pagesWithTests = 1;
|
|
1962
2047
|
setTestWidgetRunning();
|
|
1963
|
-
|
|
2048
|
+
// Not a flat timer: the same settle-and-close rule as on localhost.
|
|
2049
|
+
void settleCurrentPage(showing);
|
|
1964
2050
|
}
|
|
1965
2051
|
else if (testResultsResolve) {
|
|
1966
2052
|
testResultsResolve({
|
|
@@ -9,6 +9,7 @@ force-wrapped code listings, not executed.
|
|
|
9
9
|
import { renderDocMarkdown } from './render.js';
|
|
10
10
|
import { buildSlugMap } from './routing.js';
|
|
11
11
|
import { buildNavTree } from './nav-tree.js';
|
|
12
|
+
import { bookTextOf } from './book-target.js';
|
|
12
13
|
// Clean book typography; force-wraps code so listings never overflow a page.
|
|
13
14
|
export const DEFAULT_BOOK_CSS = `/* doc-system book stylesheet */
|
|
14
15
|
html { font-size: 100%; }
|
|
@@ -149,7 +150,7 @@ export function buildBookHtml(docs, opts) {
|
|
|
149
150
|
const roots = buildNavTree(visible, slugMap);
|
|
150
151
|
const css = opts.css ?? `${DEFAULT_BOOK_CSS}\n${PRINT_CSS}`;
|
|
151
152
|
const chapters = flatten(roots)
|
|
152
|
-
.map((node) => `<section class="chapter" id="${slugify(node.doc.filename)}">\n${renderDocMarkdown(stripDocMeta(node.doc
|
|
153
|
+
.map((node) => `<section class="chapter" id="${slugify(node.doc.filename)}">\n${renderDocMarkdown(stripDocMeta(bookTextOf(node.doc)))}\n</section>`)
|
|
153
154
|
.join('\n');
|
|
154
155
|
return `<!DOCTYPE html>
|
|
155
156
|
<html lang="${opts.lang ?? 'en'}">
|
|
@@ -14,6 +14,16 @@ export interface BookTargetDoc {
|
|
|
14
14
|
/** one book, several books, or `'none'` */
|
|
15
15
|
book?: string | string[];
|
|
16
16
|
hidden?: boolean;
|
|
17
|
+
/** where this doc sits in a particular volume, keyed by `book` name */
|
|
18
|
+
placement?: Record<string, BookPlacement>;
|
|
19
|
+
}
|
|
20
|
+
export interface BookPlacement {
|
|
21
|
+
/** parent within this volume; `''` puts the doc at the top level */
|
|
22
|
+
parent?: string;
|
|
23
|
+
order?: number;
|
|
24
|
+
pin?: 'top' | 'bottom';
|
|
25
|
+
/** chapter title in this volume */
|
|
26
|
+
title?: string;
|
|
17
27
|
}
|
|
18
28
|
type SlugMap = Record<string, string>;
|
|
19
29
|
/**
|
|
@@ -29,6 +39,15 @@ export declare function chain<T extends BookTargetDoc>(doc: T, docs: T[], slugMa
|
|
|
29
39
|
* another, or bind into several (`["default", "appendices"]`).
|
|
30
40
|
*/
|
|
31
41
|
export declare function resolveBooks<T extends BookTargetDoc>(doc: T, docs: T[], slugMap?: SlugMap): string[];
|
|
42
|
+
/**
|
|
43
|
+
* The docs of one volume, arranged for it: each doc's `placement` for this book overlaid on
|
|
44
|
+
* its site `parent` / `order` / `pin` / `title`. Returns shallow copies; the site's
|
|
45
|
+
* arrangement is untouched.
|
|
46
|
+
*/
|
|
47
|
+
export declare function placeInBook<T extends BookTargetDoc & {
|
|
48
|
+
order?: number;
|
|
49
|
+
pin?: 'top' | 'bottom';
|
|
50
|
+
}>(docs: T[], book: string): T[];
|
|
32
51
|
/**
|
|
33
52
|
* Hidden here or anywhere above.
|
|
34
53
|
*
|
|
@@ -48,4 +67,9 @@ export declare function withoutHidden<T extends BookTargetDoc>(docs: T[], slugMa
|
|
|
48
67
|
export declare function partitionByBook<T extends BookTargetDoc>(docs: T[], slugMap?: SlugMap): Map<string, T[]>;
|
|
49
68
|
/** Named books only, in stable order — the extra volumes beside the default one. */
|
|
50
69
|
export declare function namedBooks<T extends BookTargetDoc>(docs: T[], slugMap?: SlugMap): string[];
|
|
70
|
+
export declare const bookTextOf: (doc: {
|
|
71
|
+
text: string;
|
|
72
|
+
bookText?: string;
|
|
73
|
+
}) => string;
|
|
74
|
+
export declare const mayHaveSingleSourceDirective: (text: string) => boolean;
|
|
51
75
|
export {};
|
|
@@ -29,6 +29,11 @@ export const NO_BOOK = 'none';
|
|
|
29
29
|
* `book: ["default", "field-guide"]` binds the doc into both.
|
|
30
30
|
*/
|
|
31
31
|
export const DEFAULT_BOOK_NAME = 'default';
|
|
32
|
+
const placementKey = (book) => book === DEFAULT_BOOK ? DEFAULT_BOOK_NAME : book;
|
|
33
|
+
/** Volumes a doc's `placement` names, as book keys. */
|
|
34
|
+
function placedBooks(doc) {
|
|
35
|
+
return Object.keys(doc.placement ?? {}).map((k) => k.trim().toLowerCase() === DEFAULT_BOOK_NAME ? DEFAULT_BOOK : k.trim());
|
|
36
|
+
}
|
|
32
37
|
/**
|
|
33
38
|
* The doc itself, then each ancestor nearest-first. Stops at an unresolvable parent and
|
|
34
39
|
* at a cycle, so a self-parented doc yields just itself rather than hanging.
|
|
@@ -86,14 +91,51 @@ function normalizeDeclaration(value) {
|
|
|
86
91
|
* another, or bind into several (`["default", "appendices"]`).
|
|
87
92
|
*/
|
|
88
93
|
export function resolveBooks(doc, docs, slugMap = {}) {
|
|
94
|
+
const placed = placedBooks(doc);
|
|
95
|
+
const withPlaced = (books) => [
|
|
96
|
+
...books,
|
|
97
|
+
...placed.filter((b) => !books.includes(b)),
|
|
98
|
+
];
|
|
89
99
|
for (const d of chain(doc, docs, slugMap)) {
|
|
90
100
|
if (d.book === undefined)
|
|
91
101
|
continue;
|
|
92
102
|
const declared = normalizeDeclaration(d.book);
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
/*
|
|
104
|
+
An explicit `none` on the doc ITSELF still wins over its own placement: withholding is
|
|
105
|
+
the conservative reading of a contradiction, as it is for `["default", "none"]`. An
|
|
106
|
+
inherited `none` does not, or a chapter could never be placed into a book from a section
|
|
107
|
+
that is otherwise site-only.
|
|
108
|
+
*/
|
|
109
|
+
if (declared !== null) {
|
|
110
|
+
if (!declared.length && d === doc)
|
|
111
|
+
return [];
|
|
112
|
+
return withPlaced(declared);
|
|
113
|
+
}
|
|
95
114
|
}
|
|
96
|
-
return [DEFAULT_BOOK];
|
|
115
|
+
return withPlaced([DEFAULT_BOOK]);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The docs of one volume, arranged for it: each doc's `placement` for this book overlaid on
|
|
119
|
+
* its site `parent` / `order` / `pin` / `title`. Returns shallow copies; the site's
|
|
120
|
+
* arrangement is untouched.
|
|
121
|
+
*/
|
|
122
|
+
export function placeInBook(docs, book) {
|
|
123
|
+
const key = placementKey(book);
|
|
124
|
+
return docs.map((doc) => {
|
|
125
|
+
const entry = Object.entries(doc.placement ?? {}).find(([k]) => k.trim() === key ||
|
|
126
|
+
(book === DEFAULT_BOOK && k.trim().toLowerCase() === DEFAULT_BOOK_NAME));
|
|
127
|
+
if (!entry)
|
|
128
|
+
return doc;
|
|
129
|
+
// Only what a placement is documented to set: an entry is author-written JSON, and
|
|
130
|
+
// copying whatever it holds would let `"hidden": false` or `"text"` through.
|
|
131
|
+
const out = { ...doc };
|
|
132
|
+
for (const field of ['parent', 'order', 'pin', 'title']) {
|
|
133
|
+
const value = entry[1][field];
|
|
134
|
+
if (value !== undefined)
|
|
135
|
+
out[field] = value;
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
});
|
|
97
139
|
}
|
|
98
140
|
/**
|
|
99
141
|
* Hidden here or anywhere above.
|
|
@@ -134,3 +176,20 @@ export function namedBooks(docs, slugMap = {}) {
|
|
|
134
176
|
.filter((k) => k !== DEFAULT_BOOK)
|
|
135
177
|
.sort();
|
|
136
178
|
}
|
|
179
|
+
/*
|
|
180
|
+
The text a book binds for this doc.
|
|
181
|
+
|
|
182
|
+
`bookText` exists only where conditional text made the book differ from the site
|
|
183
|
+
(`site/single-source.ts`). Every book output asks here, so there are exactly two readers to
|
|
184
|
+
get right (the ePub and print) and every other consumer keeps reading `text`.
|
|
185
|
+
*/
|
|
186
|
+
export const bookTextOf = (doc) => doc.bookText ?? doc.text;
|
|
187
|
+
/*
|
|
188
|
+
Might this source use an inset or conditional text?
|
|
189
|
+
|
|
190
|
+
Deliberately loose: it also matches a directive that is only being SHOWN in a code fence.
|
|
191
|
+
Its two callers want exactly that bias. The build uses it to skip docs that certainly have
|
|
192
|
+
none before asking the parser, and the example editor uses it to refuse a save, where a
|
|
193
|
+
false positive costs a refusal and a false negative writes an edit into the wrong block.
|
|
194
|
+
*/
|
|
195
|
+
export const mayHaveSingleSourceDirective = (text) => /<!--\s*\{[^\n]*"(inset|only|end)"\s*:/.test(text);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
|
-
|
|
2
|
+
THE way to enumerate the code blocks in a doc, build and client alike: marked's own lexer.
|
|
3
3
|
|
|
4
4
|
`check-examples` had this function; `build-warnings` then grew a hand-written line scanner for
|
|
5
5
|
the same job, and the 1.16.7 re-review found the scanner disagreeing with marked on CRLF files,
|
|
6
6
|
blockquoted fences and fences inside HTML blocks. A scanner can only ever approximate the
|
|
7
|
-
parser the pages are rendered with, so nothing here scans:
|
|
7
|
+
parser the pages are rendered with, so nothing here scans: every caller asks marked. The
|
|
8
|
+
doc-browser's test runner asks here too (`hasTestBlock`), for the same reason.
|
|
8
9
|
*/
|
|
9
10
|
import { marked } from 'marked';
|
|
10
|
-
import { parseFenceInfo } from '
|
|
11
|
+
import { parseFenceInfo } from './example-policy.js';
|
|
11
12
|
/*
|
|
12
13
|
Fence info is parsed by `parseFenceInfo`, the one copy of that grammar. This file had its own
|
|
13
14
|
`dialectOf`/`modeOf` pair; once fences could carry JSON options (#184) `modeOf` would have read
|
|
@@ -8,7 +8,7 @@ each of these is a legitimate thing to do on purpose. `strict: true` in the site
|
|
|
8
8
|
project that never does them on purpose says so, and then the build fails.
|
|
9
9
|
*/
|
|
10
10
|
import { isLiveFence } from '../example-policy.js';
|
|
11
|
-
import { collectCodeTokens } from '
|
|
11
|
+
import { collectCodeTokens } from '../code-fences.js';
|
|
12
12
|
/** Thrown by `misconfigured` under `strict`, so a caller can tell it from a crash. */
|
|
13
13
|
export class SiteMisconfiguredError extends Error {
|
|
14
14
|
name = 'SiteMisconfiguredError';
|
|
@@ -21,7 +21,7 @@ executed, so they're skipped.
|
|
|
21
21
|
|
|
22
22
|
Build-time only (bun). Never import from browser code.
|
|
23
23
|
*/
|
|
24
|
-
import { collectCodeTokens } from '
|
|
24
|
+
import { collectCodeTokens } from '../code-fences.js';
|
|
25
25
|
import { rewriteImports, AsyncFunction, loadTransform, transformAvailable, UnsupportedImportError, } from '../../live-example/code-transform.js';
|
|
26
26
|
import { isLiveFence } from '../example-policy.js';
|
|
27
27
|
// The default live-example context (matches the IIFE globals the pages provide).
|