tosijs-ui 1.16.8 → 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 +68 -0
- package/README.md +1 -1
- 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/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 +3 -0
- package/dist/doc-system/site/index.js +4 -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/iife.js +92 -90
- package/dist/iife.js.map +9 -8
- package/dist/live-example/component.d.ts +26 -0
- package/dist/live-example/component.js +195 -6
- package/dist/live-example/dialects.d.ts +4 -0
- package/dist/live-example/styles.d.ts +17 -0
- package/dist/live-example/styles.js +28 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
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
|
+
|
|
3
71
|
## 1.16.8
|
|
4
72
|
|
|
5
73
|
A patch for honest results. The doc-test lane can no longer pass over pages that never
|
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
|
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
import { type BookPlacement } from '../book-target.js';
|
|
1
2
|
export interface Doc {
|
|
2
3
|
text: string;
|
|
3
4
|
title: string;
|
|
@@ -32,6 +33,18 @@ export interface Doc {
|
|
|
32
33
|
* individual chapter can still divert, join several volumes, or opt out.
|
|
33
34
|
*/
|
|
34
35
|
book?: string | string[];
|
|
36
|
+
/**
|
|
37
|
+
* Where this doc sits in a particular volume, when that differs from the site:
|
|
38
|
+
* `{ "language": { "parent": "for-programmers", "order": 20 } }`. Keyed by `book` name
|
|
39
|
+
* (`"default"` for the main volume); each entry may set `parent`, `order`, `pin` and
|
|
40
|
+
* `title`. Naming a volume here also binds the doc into it. The site nav is unaffected.
|
|
41
|
+
*/
|
|
42
|
+
placement?: Record<string, BookPlacement>;
|
|
43
|
+
/**
|
|
44
|
+
* The text a book binds, set by the build only where `{"only": …}` conditional text made
|
|
45
|
+
* it differ from `text` (which is always what the site shows). Never author this.
|
|
46
|
+
*/
|
|
47
|
+
bookText?: string;
|
|
35
48
|
/**
|
|
36
49
|
* Opt this page out of the reading column.
|
|
37
50
|
*
|
|
@@ -99,6 +99,7 @@ import { truncationWarnings, formatTruncationWarnings, } from './truncated-doc.j
|
|
|
99
99
|
import { pinnedSort } from '../nav-tree.js';
|
|
100
100
|
import { buildSlugMap } from '../routing.js';
|
|
101
101
|
import { withoutHidden } from '../book-target.js';
|
|
102
|
+
import { assembleCorpus, isSingleSourceDirective } from './single-source.js';
|
|
102
103
|
/**
|
|
103
104
|
* Directories a doc site should not publish, excluded by default (tosijs-ui#153).
|
|
104
105
|
*
|
|
@@ -234,7 +235,14 @@ function metadata(content, filePath) {
|
|
|
234
235
|
And it must START A LINE. A metadata block is a standalone directive, so an inline mention
|
|
235
236
|
in a sentence is prose — exactly the rule `/*#` doc blocks already follow.
|
|
236
237
|
*/
|
|
237
|
-
|
|
238
|
+
/*
|
|
239
|
+
The first such block that is not a single-sourcing directive. An inset or an `only` is
|
|
240
|
+
spelled the same way, and a doc that opens with one and has no metadata of its own was
|
|
241
|
+
otherwise published carrying `inset` as a metadata field.
|
|
242
|
+
*/
|
|
243
|
+
const source = [
|
|
244
|
+
...scannable.matchAll(/^[ \t]*<!--(\{.*\})-->|^[ \t]*\/\*(\{.*\})\*\//gm),
|
|
245
|
+
].find((m) => !isSingleSourceDirective(m[1] || m[2]));
|
|
238
246
|
let data = {};
|
|
239
247
|
if (source) {
|
|
240
248
|
try {
|
|
@@ -297,7 +305,9 @@ function hasDocBlock(filePath) {
|
|
|
297
305
|
return false; // unreadable is not our business here
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
|
-
function findMarkdownFiles(paths, ignore, defaultIgnores = new Set()
|
|
308
|
+
function findMarkdownFiles(paths, ignore, defaultIgnores = new Set(),
|
|
309
|
+
/** filled with the real path of every directory actually walked (see `assembleCorpus`) */
|
|
310
|
+
walked) {
|
|
301
311
|
const markdownFiles = [];
|
|
302
312
|
const truncationFound = [];
|
|
303
313
|
const withheld = [];
|
|
@@ -366,6 +376,12 @@ function findMarkdownFiles(paths, ignore, defaultIgnores = new Set()) {
|
|
|
366
376
|
}
|
|
367
377
|
return;
|
|
368
378
|
}
|
|
379
|
+
try {
|
|
380
|
+
walked?.add(fs.realpathSync(dir));
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// unreadable: nothing in it is scraped, so nothing in it is a fragment either
|
|
384
|
+
}
|
|
369
385
|
files.forEach((file) => {
|
|
370
386
|
// Skip scaffolding / working files (Jekyll-style `_`-prefix): a
|
|
371
387
|
// `_template.md` or `_drafting-log.md` shouldn't leak into the corpus or
|
|
@@ -498,7 +514,8 @@ function findMarkdownFiles(paths, ignore, defaultIgnores = new Set()) {
|
|
|
498
514
|
}
|
|
499
515
|
export function extractDocs(options) {
|
|
500
516
|
const { paths, ignore = ['node_modules', 'dist', 'build'], output } = options;
|
|
501
|
-
const
|
|
517
|
+
const walked = new Set();
|
|
518
|
+
const found = findMarkdownFiles(paths, ignore, new Set(DEFAULT_DOC_IGNORES), walked);
|
|
502
519
|
/*
|
|
503
520
|
Drop hidden docs HERE, before anything else sees them.
|
|
504
521
|
|
|
@@ -513,6 +530,21 @@ export function extractDocs(options) {
|
|
|
513
530
|
rest of the pipeline is concerned. Descendants of a hidden doc go with it.
|
|
514
531
|
*/
|
|
515
532
|
const docs = withoutHidden(found, buildSlugMap(found));
|
|
533
|
+
/*
|
|
534
|
+
Insets and conditional text are resolved HERE, for the same reason hidden docs are dropped
|
|
535
|
+
here: the corpus is the thing that ships, so anything assembled later is assembled by each
|
|
536
|
+
consumer separately. After the hidden filter, so a withheld doc cannot be published by
|
|
537
|
+
insetting it; a fragment that should not be a page is a `_`-prefixed file instead, and that
|
|
538
|
+
is the only kind of file an inset will read (see `fragmentPath`).
|
|
539
|
+
*/
|
|
540
|
+
/*
|
|
541
|
+
A fragment may be read from a directory this extraction WALKED, and nowhere else. Not a
|
|
542
|
+
list of roots with the ignore rule applied again: the re-review found that second copy
|
|
543
|
+
disagreeing with the first within the hour (a file doc path such as `README.md` made the
|
|
544
|
+
whole project a root, and a path-form ignore was not honoured). The walk already decided
|
|
545
|
+
which directories are published, so ask it.
|
|
546
|
+
*/
|
|
547
|
+
assembleCorpus(docs, { fragmentDirs: walked });
|
|
516
548
|
if (output) {
|
|
517
549
|
saveDocsJSON(docs, output);
|
|
518
550
|
}
|
|
@@ -18,9 +18,9 @@ import * as fs from 'fs';
|
|
|
18
18
|
import * as path from 'path';
|
|
19
19
|
import { renderDocMarkdown } from '../render.js';
|
|
20
20
|
import { highlightHtml } from '../highlight.js';
|
|
21
|
-
import { buildSlugMap, pathForSlug, slugForPath, withBase } from '../routing.js';
|
|
21
|
+
import { buildSlugMap, pathForSlug, resolveParent, slugForPath, withBase, } from '../routing.js';
|
|
22
22
|
import { buildNavTree } from '../nav-tree.js';
|
|
23
|
-
import { partitionByBook, DEFAULT_BOOK } from '../book-target.js';
|
|
23
|
+
import { partitionByBook, placeInBook, bookTextOf, DEFAULT_BOOK, } from '../book-target.js';
|
|
24
24
|
import { epubVolumeIdentity } from './epub-volumes.js';
|
|
25
25
|
import { isLiveFence, languageOfClass } from '../example-policy.js';
|
|
26
26
|
import { DEFAULT_BOOK_CSS, stripDocMeta, flatten, slugify, } from '../book-html.js';
|
|
@@ -638,8 +638,28 @@ export async function buildEpub(config, opts = {}) {
|
|
|
638
638
|
` A doc joins a volume with \`{"book": "<name>"}\` (inherited by its children).\n` +
|
|
639
639
|
` If this is a typo, note that book names are case- and space-sensitive.`);
|
|
640
640
|
}
|
|
641
|
+
/*
|
|
642
|
+
Arrange for THIS volume before curating: a doc's `placement` for this book replaces its
|
|
643
|
+
site parent and order (#217), and the manifest then orders what results.
|
|
644
|
+
|
|
645
|
+
A placement parent that is not in the volume is an error. The nav tree would otherwise
|
|
646
|
+
quietly promote the chapter to the top level, which is a book with the wrong outline and a
|
|
647
|
+
green build.
|
|
648
|
+
*/
|
|
649
|
+
const book = opts.bookTarget ?? DEFAULT_BOOK;
|
|
650
|
+
const placed = placeInBook(visible, book);
|
|
651
|
+
const placedSlugs = buildSlugMap(placed);
|
|
652
|
+
for (const [i, doc] of placed.entries()) {
|
|
653
|
+
if (doc === visible[i] || !doc.parent)
|
|
654
|
+
continue;
|
|
655
|
+
if (doc.parent === visible[i].parent)
|
|
656
|
+
continue;
|
|
657
|
+
if (!resolveParent(doc.parent, placed, placedSlugs)) {
|
|
658
|
+
throw new Error(`ePub volume ${JSON.stringify(book || '(default)')}: ${doc.filename} is placed under "${doc.parent}", which is not a document in that volume.`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
641
661
|
// Curate/reorder for the book (a no-op when config.book is absent).
|
|
642
|
-
const docs = selectBookDocs(
|
|
662
|
+
const docs = selectBookDocs(placed, config.book);
|
|
643
663
|
const slugMap = buildSlugMap(docs);
|
|
644
664
|
const roots = buildNavTree(docs, slugMap);
|
|
645
665
|
const fileFor = (d) => `${slugMap[d.filename] || 'index'}.xhtml`;
|
|
@@ -727,7 +747,7 @@ export async function buildEpub(config, opts = {}) {
|
|
|
727
747
|
`injectExampleLinks` matches on the `language-*` class, never on the code text, so
|
|
728
748
|
highlighting cannot break the "Run this example live" links.
|
|
729
749
|
*/
|
|
730
|
-
await highlightHtml(renderDocMarkdown(stripDocMeta(doc
|
|
750
|
+
await highlightHtml(renderDocMarkdown(stripDocMeta(bookTextOf(doc))), 'none'), bookFiles, slugMap, config.basePath);
|
|
731
751
|
// happy-dom occasionally throws on exotic content (e.g. an internal selector
|
|
732
752
|
// bug); fall back to the regex pass for that doc rather than aborting.
|
|
733
753
|
let bodyHtml;
|
|
@@ -18,6 +18,9 @@ export { buildEpub, DEFAULT_BOOK_CSS } from './epub.js';
|
|
|
18
18
|
export type { BuildEpubOptions, BookMeta } from './epub.js';
|
|
19
19
|
export { selectBookDocs } from '../book-manifest.js';
|
|
20
20
|
export type { BookManifest } from '../book-manifest.js';
|
|
21
|
+
export { placeInBook, bookTextOf } from '../book-target.js';
|
|
22
|
+
export type { BookPlacement } from '../book-target.js';
|
|
23
|
+
export { assembleCorpus, resolveInsets, splitConditions, SingleSourceError, } from './single-source.js';
|
|
21
24
|
export { listEpubVolumes, epubVolumeIdentity, renderEpubDownloads, type EpubVolume, } from './epub-volumes.js';
|
|
22
25
|
export { currentHolder, describeHolder, lockPathFor, lockDecision, isProcessAlive, } from './build-lock.js';
|
|
23
26
|
export type { LockHolder, LockDecision } from './build-lock.js';
|
|
@@ -18,6 +18,10 @@ export { extractDocs, saveDocsJSON } from './docs.js';
|
|
|
18
18
|
export { generateSite } from './generate-site.js';
|
|
19
19
|
export { buildEpub, DEFAULT_BOOK_CSS } from './epub.js';
|
|
20
20
|
export { selectBookDocs } from '../book-manifest.js';
|
|
21
|
+
export { placeInBook, bookTextOf } from '../book-target.js';
|
|
22
|
+
// Insets and conditional text (#217): `extractDocs` applies these; exported for a consumer
|
|
23
|
+
// that builds its corpus some other way.
|
|
24
|
+
export { assembleCorpus, resolveInsets, splitConditions, SingleSourceError, } from './single-source.js';
|
|
21
25
|
export { listEpubVolumes, epubVolumeIdentity, renderEpubDownloads, } from './epub-volumes.js';
|
|
22
26
|
/*
|
|
23
27
|
Reading the build lock is part of the PUBLIC surface, not an internal detail (#117).
|
|
@@ -643,6 +643,14 @@ export async function buildSite(config, opts = {}) {
|
|
|
643
643
|
doc.text = filled;
|
|
644
644
|
touched++;
|
|
645
645
|
}
|
|
646
|
+
// The book's variant is a second copy of the text (conditional text, #217).
|
|
647
|
+
if (typeof doc.bookText === 'string') {
|
|
648
|
+
const book = renderEpubDownloads(doc.bookText, volumes);
|
|
649
|
+
if (book !== doc.bookText) {
|
|
650
|
+
doc.bookText = book;
|
|
651
|
+
touched++;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
646
654
|
}
|
|
647
655
|
if (touched)
|
|
648
656
|
await Bun.write(DOCS_JSON, JSON.stringify(corpus, null, 2));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare class SingleSourceError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
interface SourceDoc {
|
|
5
|
+
text: string;
|
|
6
|
+
filename: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
/** the text a book binds, when it differs from `text` (the site's) */
|
|
10
|
+
bookText?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Is this JSON directive single-sourcing rather than doc metadata? */
|
|
13
|
+
export declare function isSingleSourceDirective(json: string): boolean;
|
|
14
|
+
export interface ResolveInsetsOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Real paths of the directories a fragment file may be read from: the ones extraction
|
|
17
|
+
* walked. `extractDocs` supplies this. Without it no file inset is read at all.
|
|
18
|
+
*/
|
|
19
|
+
fragmentDirs?: ReadonlySet<string>;
|
|
20
|
+
/** read a fragment file; injectable so the rule is testable without a filesystem */
|
|
21
|
+
readFile?: (absolutePath: string) => string | undefined;
|
|
22
|
+
/** resolve symlinks; injectable for the same reason */
|
|
23
|
+
realPath?: (absolutePath: string) => string;
|
|
24
|
+
}
|
|
25
|
+
export declare function resolveInsets(docs: SourceDoc[], options?: ResolveInsetsOptions): number;
|
|
26
|
+
export declare function splitConditions(text: string, where?: string): {
|
|
27
|
+
site: string;
|
|
28
|
+
book: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function assembleCorpus(docs: SourceDoc[], options?: ResolveInsetsOptions): void;
|
|
31
|
+
export {};
|