tosijs-ui 1.6.14 → 1.6.16
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 +94 -0
- package/dist/babylon-3d.d.ts +6 -0
- package/dist/babylon-3d.js +99 -5
- package/dist/doc-system/book-html.d.ts +1 -1
- package/dist/doc-system/book-html.js +10 -3
- package/dist/doc-system/book-manifest.d.ts +36 -0
- package/dist/doc-system/book-manifest.js +96 -0
- package/dist/doc-system/render.js +103 -0
- package/dist/doc-system/site/docs.d.ts +16 -0
- package/dist/doc-system/site/docs.js +70 -5
- package/dist/doc-system/site/epub.d.ts +9 -1
- package/dist/doc-system/site/epub.js +49 -20
- package/dist/doc-system/site/index.d.ts +2 -2
- package/dist/doc-system/site/index.js +1 -1
- package/dist/doc-system/site/orchestrator.js +41 -1
- package/dist/doc-system/site/site-config.d.ts +20 -1
- package/dist/iife.js +73 -54
- package/dist/iife.js.map +10 -10
- package/dist/live-example/code-transform.js +0 -0
- package/dist/live-example/component.js +43 -9
- package/dist/live-example/remote-sync.d.ts +4 -0
- package/dist/live-example/remote-sync.js +11 -2
- package/dist/live-example/save-to-source.d.ts +12 -1
- package/dist/live-example/save-to-source.js +43 -7
- package/dist/live-example/types.d.ts +6 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +3 -4
- package/dist/doc-system/site/pdf.d.ts +0 -17
- package/dist/doc-system/site/pdf.js +0 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.16
|
|
4
|
+
|
|
5
|
+
Doc-system: prose/book adoption. Component APIs are unchanged; everything here is
|
|
6
|
+
doc-site tooling (the `tosijs-ui/site` build system + live examples).
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Book manifest — curate/reorder the book without touching site nav.** A new
|
|
11
|
+
`book` field on the site config (`BookManifest`) selects and sequences the ePub
|
|
12
|
+
(and, later, print) as a subset of the corpus while the live site still shows
|
|
13
|
+
everything — one source, two outputs. `include`/`exclude` globs pick docs,
|
|
14
|
+
`order: [...]` names the lead sequence (front/back matter are just docs you
|
|
15
|
+
name), and `sort: 'filename'` gives a folder of chapters natural order with no
|
|
16
|
+
metadata. It adds no new ordering mechanism — it overlays each doc's `order` so
|
|
17
|
+
the existing nav sort sequences the book (pins/parents still apply). Zero-config
|
|
18
|
+
is unchanged: the book is the whole visible corpus. Identity (title/author/
|
|
19
|
+
cover) still comes from `epub`.
|
|
20
|
+
- **Prose Markdown (Batch A), activated only by its own syntax** so code docs are
|
|
21
|
+
byte-identical: **YAML frontmatter** (`title`/`order`/`author`/`date`/`draft`,
|
|
22
|
+
frontmatter wins over JSON-comment metadata), **wikilinks** `[[slug]]` /
|
|
23
|
+
`[[slug|label]]` → `/slug/`, and **footnotes** `[^id]` → numbered refs + an
|
|
24
|
+
endnotes section (ePub/web).
|
|
25
|
+
- **Book/prose quick wins:** auto-serve `/iife.js` when `bundleEntry` is omitted;
|
|
26
|
+
skip `_`-prefixed scaffolding files; empty metadata `title` falls back to the
|
|
27
|
+
H1; warn when no ePub cover is generated.
|
|
28
|
+
- **`epub.coverIcon`** — embed a custom flat SVG glyph into the generated ePub
|
|
29
|
+
cover (in place of the favicon); the source viewBox is preserved so any
|
|
30
|
+
square-ish icon scales correctly. tosijs-ui ships a `tosi-book` cover glyph.
|
|
31
|
+
- **`<tosi-3d>` declarative attributes** — assemble a scene purely in HTML (no
|
|
32
|
+
`sceneCreated` callback): `src` (a `.glb` URL, auto-loads like `<img>`),
|
|
33
|
+
`hero-light` (a directional key light over a soft hemispheric fill), `fov` (a
|
|
34
|
+
field-of-view multiplier — `1` unchanged, `<1` a longer lens), and `clear-color`
|
|
35
|
+
(a hex color, or `transparent` to composite the 3D over the page). Aimed at
|
|
36
|
+
declarative narrative pages.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **Multiple `<tosi-3d>` on one page.** Their scenes are created asynchronously and
|
|
41
|
+
interleave, so objects created without an explicit scene landed on Babylon's
|
|
42
|
+
`Engine.LastCreatedScene` (another instance's scene) — the built-in default
|
|
43
|
+
camera did this, leaving a scene with no camera and a blank canvas. The default
|
|
44
|
+
camera (and the doc examples' lights) now pass their scene explicitly.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **PDF is now the in-browser Print button, not a batch job.** The doc-browser's
|
|
49
|
+
Print action renders the shared book HTML (`book-html.ts`) and the browser
|
|
50
|
+
prints it to PDF. This is the single, supported PDF path going forward; a future
|
|
51
|
+
paginated/footnote-anchored PDF will build on it. (ePub is still generated at
|
|
52
|
+
build time and unaffected.)
|
|
53
|
+
- **Live examples pinned to tjs-lang 0.8.7** (upstream subtle-bug fix).
|
|
54
|
+
|
|
55
|
+
### Removed
|
|
56
|
+
|
|
57
|
+
- **The headless PDF batch builder** (`buildPdf` / `BuildPdfOptions`, exported
|
|
58
|
+
from `tosijs-ui/site`, and the `book:pdf` / `--pdf` script) — a secondary code
|
|
59
|
+
path superseded by the Print button above. If you generated PDFs in a build,
|
|
60
|
+
print the book page to PDF (via the doc-browser) instead.
|
|
61
|
+
|
|
62
|
+
## 1.6.15
|
|
63
|
+
|
|
64
|
+
Live-example fixes from tosijs-3d adoption. All fixes to the doc-system's
|
|
65
|
+
editing surface; no API changes.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **Save to source works when the doc comment is indented.** A `/*# … */` doc
|
|
70
|
+
comment is often indented in the source; the extractor dedents its fenced blocks
|
|
71
|
+
(so examples render with correct ordinals), but the raw scan required the closing
|
|
72
|
+
` ``` ` right after a newline, so an indented file matched **zero** blocks and
|
|
73
|
+
every save failed with "no matching block". The scan is now indentation-aware,
|
|
74
|
+
and edits compare dedented / write re-indented so a block keeps its place. The
|
|
75
|
+
failure alert is also split into a precise message (page↔source ordinal mismatch
|
|
76
|
+
vs. a genuine no-op).
|
|
77
|
+
- **The pop-out editor window ("view/edit code in a new window") now has the full
|
|
78
|
+
menu.** It previously opened as a bare instance, so View changes, Save changes
|
|
79
|
+
(local), and Save to source were hidden; the source↔doc key and the pristine
|
|
80
|
+
snapshot are now passed through, so the pop-out offers — and can execute — the
|
|
81
|
+
same actions as the main window.
|
|
82
|
+
- **Opening the editor in a new window closes the inline code view** in the main
|
|
83
|
+
window if it was open (the pop-out owns editing).
|
|
84
|
+
- **The doc extractor now only treats a `/*#` block as a doc when it starts a
|
|
85
|
+
line** (whitespace-only before the slash), so a `/*#` inside a `//` comment, a
|
|
86
|
+
string, or mid-line can't be scraped as a spurious doc page.
|
|
87
|
+
|
|
3
88
|
## 1.6.14
|
|
4
89
|
|
|
5
90
|
Lighter live-example transpilation. Additive; affects only how the live-example
|
|
@@ -14,11 +99,20 @@ runner loads its (optional, lazy) transpiler.
|
|
|
14
99
|
and the transpiler's own deps are never in a consumer's dependency graph. This
|
|
15
100
|
also fixes the `ts` example path, which previously tried to load the TypeScript
|
|
16
101
|
compiler through a CDN transform that timed out on its size.
|
|
102
|
+
- **Robust transpiler loading.** The doc-site build now ships the tjs-lang browser
|
|
103
|
+
bundles **same-origin** (copied next to the iife under `/tjs/`, with a global
|
|
104
|
+
pointing the loader at them), so live examples never depend on a third-party
|
|
105
|
+
CDN's propagation timing or uptime. The loader prefers the same-origin copy,
|
|
106
|
+
then falls back through a multi-CDN chain (jsdelivr → unpkg → esm.sh). Installed
|
|
107
|
+
ESM consumers resolve the peer locally as before.
|
|
17
108
|
- `tjs-lang` optional peer bumped to `^0.8.6`.
|
|
18
109
|
|
|
19
110
|
### Fixed
|
|
20
111
|
|
|
21
112
|
- A garbled character in the first `example` doc snippet that made it throw.
|
|
113
|
+
- Live examples could all break for the propagation window after a tjs-lang
|
|
114
|
+
release, when the single pinned CDN 404'd the just-published version (now
|
|
115
|
+
served same-origin + multi-CDN).
|
|
22
116
|
|
|
23
117
|
## 1.6.13
|
|
24
118
|
|
package/dist/babylon-3d.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ interface B3dUIOptions {
|
|
|
9
9
|
type MeshProcessCallback = (meshes: any[]) => void;
|
|
10
10
|
export declare class B3d extends WebComponent {
|
|
11
11
|
static preferredTagName: string;
|
|
12
|
+
static initAttributes: {
|
|
13
|
+
src: string;
|
|
14
|
+
clearColor: string;
|
|
15
|
+
fov: number;
|
|
16
|
+
heroLight: boolean;
|
|
17
|
+
};
|
|
12
18
|
babylonReady: Promise<any>;
|
|
13
19
|
BABYLON?: any;
|
|
14
20
|
static shadowStyleSpec: {
|
package/dist/babylon-3d.js
CHANGED
|
@@ -21,7 +21,7 @@ preview.append(b3d({
|
|
|
21
21
|
)
|
|
22
22
|
camera.attachControl(element.parts.canvas, true)
|
|
23
23
|
|
|
24
|
-
new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.25, 1, -0.5))
|
|
24
|
+
new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.25, 1, -0.5), element.scene)
|
|
25
25
|
|
|
26
26
|
this.loadScene('/', 'xin3d.glb')
|
|
27
27
|
|
|
@@ -129,7 +129,7 @@ preview.append(b3d({
|
|
|
129
129
|
const { scene } = element
|
|
130
130
|
const { createNoise2D } = await import('https://cdn.jsdelivr.net/npm/simplex-noise@4.0.1/+esm')
|
|
131
131
|
|
|
132
|
-
new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.25, 1, 2))
|
|
132
|
+
new BABYLON.HemisphericLight('light', new BABYLON.Vector3(0.25, 1, 2), scene)
|
|
133
133
|
|
|
134
134
|
const terrain = new BABYLON.Mesh('terrain', scene)
|
|
135
135
|
const vertexData = new BABYLON.VertexData()
|
|
@@ -168,10 +168,38 @@ preview.append(b3d({
|
|
|
168
168
|
}))
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
## Pure-HTML scenes (attributes)
|
|
172
|
+
|
|
173
|
+
`<tosi-3d>` can assemble a scene entirely from HTML attributes — no `sceneCreated`
|
|
174
|
+
callback, no JavaScript — so a 3D hero can be dropped into a declarative page. Set
|
|
175
|
+
`src` and it works like an `<img>`; a default camera and lighting are added when
|
|
176
|
+
the scene doesn't provide its own, so a model shows up out of the box.
|
|
177
|
+
|
|
178
|
+
- **`src`** — a `.glb` URL; loads on connect (calls `loadScene` for you).
|
|
179
|
+
- **`hero-light`** — adds a directional key light (front-upper-left) over the soft
|
|
180
|
+
hemispheric fill, for a "product photography" look.
|
|
181
|
+
- **`fov`** — field-of-view *multiplier* on the camera's natural lens: `1` (default)
|
|
182
|
+
leaves it alone, `<1` is a longer lens (less perspective distortion), `>1` wider.
|
|
183
|
+
- **`clear-color`** — scene background: a css hex color, or `transparent` to
|
|
184
|
+
composite the 3D over the page (great for themed narrative pages).
|
|
185
|
+
|
|
186
|
+
This loads the same scene as the example above, in plain HTML, with a hero key
|
|
187
|
+
light and a slightly longer lens:
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<tosi-3d src="/xin3d.glb" hero-light fov="0.6"></tosi-3d>
|
|
191
|
+
```
|
|
192
|
+
```css
|
|
193
|
+
.preview tosi-3d {
|
|
194
|
+
width: 100%;
|
|
195
|
+
height: 100%;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
171
199
|
## loadScene
|
|
172
200
|
|
|
173
201
|
`<tosi-3d>.loadScene(path: string, file: string, callBack(meshes: any[]): void)` can
|
|
174
|
-
be used to load `.glb` files.
|
|
202
|
+
be used to load `.glb` files. Setting `src` calls this for you on connect.
|
|
175
203
|
|
|
176
204
|
## loadUI
|
|
177
205
|
|
|
@@ -184,8 +212,42 @@ import { icons, svg2DataUrl } from './icons';
|
|
|
184
212
|
const noop = () => {
|
|
185
213
|
/* do not care */
|
|
186
214
|
};
|
|
215
|
+
/** Parse a css hex color (#rgb/#rrggbb/#rrggbbaa) or the keyword `transparent`
|
|
216
|
+
* into a BABYLON.Color4. */
|
|
217
|
+
function toColor4(BABYLON, css) {
|
|
218
|
+
if (!css || css.trim() === 'transparent')
|
|
219
|
+
return new BABYLON.Color4(0, 0, 0, 0);
|
|
220
|
+
let hex = css.trim().replace(/^#/, '');
|
|
221
|
+
if (hex.length === 3)
|
|
222
|
+
hex = hex
|
|
223
|
+
.split('')
|
|
224
|
+
.map((c) => c + c)
|
|
225
|
+
.join('');
|
|
226
|
+
if (hex.length === 6)
|
|
227
|
+
hex += 'ff';
|
|
228
|
+
const n = parseInt(hex, 16);
|
|
229
|
+
if (Number.isNaN(n) || hex.length !== 8)
|
|
230
|
+
return new BABYLON.Color4(0, 0, 0, 1);
|
|
231
|
+
return new BABYLON.Color4(((n >>> 24) & 255) / 255, ((n >>> 16) & 255) / 255, ((n >>> 8) & 255) / 255, (n & 255) / 255);
|
|
232
|
+
}
|
|
187
233
|
export class B3d extends WebComponent {
|
|
188
234
|
static preferredTagName = 'tosi-3d';
|
|
235
|
+
// Declarative config so a scene can be assembled purely in HTML (no JS):
|
|
236
|
+
// <tosi-3d src="/model.glb" hero-light fov="0.5" clear-color="transparent">
|
|
237
|
+
static initAttributes = {
|
|
238
|
+
// `src` (a .glb URL) makes <tosi-3d> work like <img>: the scene loads on connect.
|
|
239
|
+
src: '',
|
|
240
|
+
// scene background: a css hex color, or `transparent` to composite the 3D
|
|
241
|
+
// over the page (e.g. themed narrative pages). Empty = Babylon default.
|
|
242
|
+
clearColor: '',
|
|
243
|
+
// field-of-view MULTIPLIER on the camera's natural lens. 1 = unchanged,
|
|
244
|
+
// <1 = longer lens / less perspective distortion, >1 = wider. Author-friendly:
|
|
245
|
+
// no need to know Babylon's default fov in radians.
|
|
246
|
+
fov: 1,
|
|
247
|
+
// add a directional key light (front-upper-left) for a product-photography
|
|
248
|
+
// look, on top of the soft hemispheric fill.
|
|
249
|
+
heroLight: false,
|
|
250
|
+
};
|
|
189
251
|
babylonReady;
|
|
190
252
|
BABYLON;
|
|
191
253
|
static shadowStyleSpec = {
|
|
@@ -281,15 +343,47 @@ export class B3d extends WebComponent {
|
|
|
281
343
|
const { canvas } = this.parts;
|
|
282
344
|
this.babylonReady.then(async (BABYLON) => {
|
|
283
345
|
this.BABYLON = BABYLON;
|
|
284
|
-
|
|
346
|
+
// A `transparent` (alpha < 1) clear-color needs an alpha WebGL context so
|
|
347
|
+
// the canvas composites over the page.
|
|
348
|
+
const clearColor = this.clearColor ? toColor4(BABYLON, this.clearColor) : null;
|
|
349
|
+
this.engine = new BABYLON.Engine(canvas, true, clearColor && clearColor.a < 1
|
|
350
|
+
? { alpha: true, premultipliedAlpha: false }
|
|
351
|
+
: undefined);
|
|
285
352
|
this.scene = new BABYLON.Scene(this.engine);
|
|
353
|
+
if (clearColor)
|
|
354
|
+
this.scene.clearColor = clearColor;
|
|
286
355
|
if (this.sceneCreated) {
|
|
287
356
|
await this.sceneCreated(this, BABYLON);
|
|
288
357
|
}
|
|
358
|
+
// `src` (pure-HTML usage): auto-load the .glb. Split into the rootUrl +
|
|
359
|
+
// filename that BABYLON.SceneLoader.Append expects.
|
|
360
|
+
if (this.src) {
|
|
361
|
+
const slash = this.src.lastIndexOf('/');
|
|
362
|
+
await this.loadScene(this.src.slice(0, slash + 1) || '/', this.src.slice(slash + 1));
|
|
363
|
+
}
|
|
289
364
|
if (this.scene.activeCamera === undefined) {
|
|
290
|
-
|
|
365
|
+
// Pass `this.scene` explicitly: with multiple <tosi-3d> on a page their
|
|
366
|
+
// scenes interleave, so BABYLON.Engine.LastCreatedScene (the implicit
|
|
367
|
+
// default) may point at another instance's scene.
|
|
368
|
+
const camera = new BABYLON.ArcRotateCamera('default-camera', -Math.PI / 2, Math.PI / 2.5, 3, new BABYLON.Vector3(0, 0, 0), this.scene);
|
|
291
369
|
camera.attachControl(this.parts.canvas, true);
|
|
292
370
|
}
|
|
371
|
+
// `fov` multiplier: <1 gives a long "product photography" lens (less
|
|
372
|
+
// distortion), >1 a wider one. 1 (default) leaves the camera untouched.
|
|
373
|
+
if (this.fov && this.fov !== 1 && this.scene.activeCamera) {
|
|
374
|
+
this.scene.activeCamera.fov *= this.fov;
|
|
375
|
+
}
|
|
376
|
+
// A model loaded via `src` needs a light or it renders black; add a soft
|
|
377
|
+
// hemispheric fill from above only when the scene has none of its own.
|
|
378
|
+
if (this.scene.lights.length === 0) {
|
|
379
|
+
const hemi = new BABYLON.HemisphericLight('hemi', new BABYLON.Vector3(0, 1, 0), this.scene);
|
|
380
|
+
hemi.intensity = 0.6;
|
|
381
|
+
}
|
|
382
|
+
// `hero-light`: a directional key light (front-upper-left) over the fill.
|
|
383
|
+
if (this.heroLight) {
|
|
384
|
+
const dir = new BABYLON.DirectionalLight('hero', new BABYLON.Vector3(-1, -2, 1), this.scene);
|
|
385
|
+
dir.intensity = 0.8;
|
|
386
|
+
}
|
|
293
387
|
this.engine.runRenderLoop(this._update);
|
|
294
388
|
});
|
|
295
389
|
}
|
|
@@ -6,7 +6,7 @@ export interface BookDoc {
|
|
|
6
6
|
parent?: string;
|
|
7
7
|
hidden?: boolean;
|
|
8
8
|
}
|
|
9
|
-
export declare const DEFAULT_BOOK_CSS = "/* doc-system book stylesheet */\nhtml { font-size: 100%; }\nbody {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.5;\n margin: 0 1em;\n color: #1a1a1a;\n}\nh1, h2, h3, h4, h5, h6 {\n font-family: -apple-system, 'Helvetica Neue', Arial, sans-serif;\n line-height: 1.2;\n margin: 1.4em 0 0.5em;\n}\nh1 { font-size: 1.8em; }\nh2 { font-size: 1.4em; }\nh3 { font-size: 1.2em; }\np { margin: 0.6em 0; }\na { color: #08c; text-decoration: none; }\nul, ol { margin: 0.6em 0; padding-left: 1.4em; }\nblockquote {\n margin: 0.8em 0;\n padding: 0 0 0 1em;\n border-left: 3px solid #ccc;\n color: #555;\n}\nimg { max-width: 100%; height: auto; }\ntable { border-collapse: collapse; margin: 0.8em 0; font-size: 0.85em; }\nth, td { border: 1px solid #ccc; padding: 0.3em 0.6em; text-align: left; }\ncode, pre {\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 0.8em;\n}\n:not(pre) > code {\n background: #f3f3f3;\n padding: 0.1em 0.3em;\n border-radius: 3px;\n}\npre {\n background: #f6f8fa;\n border: 1px solid #e1e4e8;\n border-radius: 4px;\n padding: 0.7em 0.9em;\n margin: 0.8em 0;\n white-space: pre-wrap;\n word-break: break-word;\n overflow-wrap: anywhere;\n}\npre code { background: none; padding: 0; }\n/* the in-flow Contents page (ePub spine TOC) */\n.toc-title { text-align: center; }\nol.toc, ol.toc ol { list-style: none; padding-left: 0; }\nol.toc ol { padding-left: 1.2em; }\nol.toc li { margin: 0.3em 0; }\nol.toc > li > a { font-weight: bold; }\n/* per-example \"run this live\" deep link */\n.example-live-link { margin: 0.6em 0 0; font-size: 0.85em; }\n.example-live-link a { color: #08c; font-weight: bold; }\n";
|
|
9
|
+
export declare const DEFAULT_BOOK_CSS = "/* doc-system book stylesheet */\nhtml { font-size: 100%; }\nbody {\n font-family: Georgia, 'Times New Roman', serif;\n line-height: 1.5;\n margin: 0 1em;\n color: #1a1a1a;\n}\nh1, h2, h3, h4, h5, h6 {\n font-family: -apple-system, 'Helvetica Neue', Arial, sans-serif;\n line-height: 1.2;\n margin: 1.4em 0 0.5em;\n}\nh1 { font-size: 1.8em; }\nh2 { font-size: 1.4em; }\nh3 { font-size: 1.2em; }\np { margin: 0.6em 0; }\na { color: #08c; text-decoration: none; }\nul, ol { margin: 0.6em 0; padding-left: 1.4em; }\nblockquote {\n margin: 0.8em 0;\n padding: 0 0 0 1em;\n border-left: 3px solid #ccc;\n color: #555;\n}\nimg { max-width: 100%; height: auto; }\ntable { border-collapse: collapse; margin: 0.8em 0; font-size: 0.85em; }\nth, td { border: 1px solid #ccc; padding: 0.3em 0.6em; text-align: left; }\ncode, pre {\n font-family: 'SF Mono', Menlo, Consolas, monospace;\n font-size: 0.8em;\n}\n:not(pre) > code {\n background: #f3f3f3;\n padding: 0.1em 0.3em;\n border-radius: 3px;\n}\npre {\n background: #f6f8fa;\n border: 1px solid #e1e4e8;\n border-radius: 4px;\n padding: 0.7em 0.9em;\n margin: 0.8em 0;\n white-space: pre-wrap;\n word-break: break-word;\n overflow-wrap: anywhere;\n}\npre code { background: none; padding: 0; }\n/* the in-flow Contents page (ePub spine TOC) */\n.toc-title { text-align: center; }\nol.toc, ol.toc ol { list-style: none; padding-left: 0; }\nol.toc ol { padding-left: 1.2em; }\nol.toc li { margin: 0.3em 0; }\nol.toc > li > a { font-weight: bold; }\n/* per-example \"run this live\" deep link */\n.example-live-link { margin: 0.6em 0 0; font-size: 0.85em; }\n.example-live-link a { color: #08c; font-weight: bold; }\n/* footnotes / endnotes + wikilinks */\nsup.footnote-ref { font-size: 0.7em; line-height: 0; }\nsup.footnote-ref a { text-decoration: none; }\n.footnotes { margin-top: 2em; font-size: 0.9em; }\n.footnotes hr { border: none; border-top: 1px solid #ccc; margin: 1em 0; }\n.footnote-backref { text-decoration: none; }\na.wikilink { text-decoration: none; border-bottom: 1px dotted currentColor; }\n";
|
|
10
10
|
export declare const PRINT_CSS = "\n@page { margin: 18mm 16mm; }\n.book-title { text-align: center; margin: 30vh 0 1em; }\n.book-toc { page-break-after: always; }\n.book-toc h2 { page-break-before: avoid; }\n.book-toc ol { list-style: none; padding-left: 0; }\n.book-toc ol ol { padding-left: 1.2em; }\n.book-toc a { color: inherit; }\n.chapter { page-break-before: always; }\n";
|
|
11
11
|
export declare function escapeHtml(s: string): string;
|
|
12
12
|
export declare function slugify(s: string): string;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
Shared, browser-safe assembly of the whole doc corpus into one printable HTML
|
|
3
3
|
"book": a title page, a generated table of contents, then every doc as a chapter
|
|
4
4
|
that starts on a new page. Used by the client-side **Print** button in the
|
|
5
|
-
doc-browser (the user's browser prints it to PDF)
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
doc-browser (the user's browser prints it to PDF). Pure rendering — no fs, no
|
|
6
|
+
Node-only APIs — so it bundles into the iife. Live examples are pretty-printed,
|
|
7
|
+
force-wrapped code listings, not executed.
|
|
8
8
|
*/
|
|
9
9
|
import { renderDocMarkdown } from './render';
|
|
10
10
|
import { buildSlugMap } from './routing';
|
|
@@ -67,6 +67,13 @@ ol.toc > li > a { font-weight: bold; }
|
|
|
67
67
|
/* per-example "run this live" deep link */
|
|
68
68
|
.example-live-link { margin: 0.6em 0 0; font-size: 0.85em; }
|
|
69
69
|
.example-live-link a { color: #08c; font-weight: bold; }
|
|
70
|
+
/* footnotes / endnotes + wikilinks */
|
|
71
|
+
sup.footnote-ref { font-size: 0.7em; line-height: 0; }
|
|
72
|
+
sup.footnote-ref a { text-decoration: none; }
|
|
73
|
+
.footnotes { margin-top: 2em; font-size: 0.9em; }
|
|
74
|
+
.footnotes hr { border: none; border-top: 1px solid #ccc; margin: 1em 0; }
|
|
75
|
+
.footnote-backref { text-decoration: none; }
|
|
76
|
+
a.wikilink { text-decoration: none; border-bottom: 1px dotted currentColor; }
|
|
70
77
|
`;
|
|
71
78
|
// Print-only layout: page margins, a chapter per page, an avoid-break TOC.
|
|
72
79
|
export const PRINT_CSS = `
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface BookManifest {
|
|
2
|
+
/**
|
|
3
|
+
* Globs selecting which docs are in the book (matched against each doc's
|
|
4
|
+
* `path` AND `filename`, so `chapters/**` or `*.md` both work). Default: every
|
|
5
|
+
* visible doc.
|
|
6
|
+
*/
|
|
7
|
+
include?: string[];
|
|
8
|
+
/** Globs removed from the selection, applied after `include`. */
|
|
9
|
+
exclude?: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Explicit reading order, by filename (with or without extension), slug, or
|
|
12
|
+
* title. Listed docs lead in this exact sequence; everything else follows in
|
|
13
|
+
* normal order. Unmatched keys are ignored.
|
|
14
|
+
*/
|
|
15
|
+
order?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Baseline order for docs you neither pinned via metadata nor named in
|
|
18
|
+
* `order`: `'nav'` (default — today's pin / `order` / title sort) or
|
|
19
|
+
* `'filename'` (natural sort, so `01-*.md`, `02-*.md`, … sequence with zero
|
|
20
|
+
* metadata). A per-doc `order` still wins over `'filename'`.
|
|
21
|
+
*/
|
|
22
|
+
sort?: 'nav' | 'filename';
|
|
23
|
+
}
|
|
24
|
+
interface BookDocLike {
|
|
25
|
+
filename: string;
|
|
26
|
+
path?: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
order?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Apply a book manifest to the (already visible) corpus. Returns a NEW array of
|
|
32
|
+
* shallow-copied docs with `order` overlaid for book sequencing. Pure — never
|
|
33
|
+
* mutates the input, so the caller's site-nav copy is untouched.
|
|
34
|
+
*/
|
|
35
|
+
export declare function selectBookDocs<T extends BookDocLike>(docs: T[], manifest?: BookManifest): T[];
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Pure "book manifest" logic: curate a subset of the doc corpus into a book
|
|
3
|
+
(ePub / print) WITHOUT touching the live-site navigation. One source, two
|
|
4
|
+
outputs.
|
|
5
|
+
|
|
6
|
+
Everything here is an overlay on the defaults — omit the manifest and you get the
|
|
7
|
+
whole visible corpus in normal nav order (the doc system's zero-config behavior).
|
|
8
|
+
The manifest never introduces a new ordering mechanism: it selects docs, then
|
|
9
|
+
overlays each doc's `order` so the SHARED buildNavTree/flatten (used by the site,
|
|
10
|
+
print, and ePub alike) sequences the book. So "front/back matter" are just
|
|
11
|
+
regular docs you name in `order` (or position with a per-doc `order`), and pins /
|
|
12
|
+
parents keep working exactly as on the site.
|
|
13
|
+
|
|
14
|
+
No fs, no DOM — a pure function of the docs array, so it's shared + unit-testable.
|
|
15
|
+
*/
|
|
16
|
+
// glob → RegExp. `**` matches any run including `/`; `*` matches any run except
|
|
17
|
+
// `/`; `?` matches a single non-`/`. Everything else is matched literally.
|
|
18
|
+
function globToRegExp(glob) {
|
|
19
|
+
let re = '';
|
|
20
|
+
for (let i = 0; i < glob.length; i += 1) {
|
|
21
|
+
const c = glob[i];
|
|
22
|
+
if (c === '*') {
|
|
23
|
+
if (glob[i + 1] === '*') {
|
|
24
|
+
re += '.*';
|
|
25
|
+
i += 1;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
re += '[^/]*';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else if (c === '?') {
|
|
32
|
+
re += '[^/]';
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return new RegExp('^' + re + '$');
|
|
39
|
+
}
|
|
40
|
+
function matchesAny(doc, globs) {
|
|
41
|
+
return globs.some((re) => re.test(doc.path ?? '') || re.test(doc.filename));
|
|
42
|
+
}
|
|
43
|
+
const baseName = (filename) => filename.replace(/\.[^.]+$/, '');
|
|
44
|
+
/** Does `key` (from an `order` list) name this doc? filename / basename / title. */
|
|
45
|
+
function docMatchesKey(doc, key) {
|
|
46
|
+
const k = key.trim().toLowerCase();
|
|
47
|
+
return (doc.filename.toLowerCase() === k ||
|
|
48
|
+
baseName(doc.filename).toLowerCase() === k ||
|
|
49
|
+
(doc.title ?? '').toLowerCase() === k);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply a book manifest to the (already visible) corpus. Returns a NEW array of
|
|
53
|
+
* shallow-copied docs with `order` overlaid for book sequencing. Pure — never
|
|
54
|
+
* mutates the input, so the caller's site-nav copy is untouched.
|
|
55
|
+
*/
|
|
56
|
+
export function selectBookDocs(docs, manifest) {
|
|
57
|
+
if (!manifest)
|
|
58
|
+
return docs.slice();
|
|
59
|
+
// 1) select ────────────────────────────────────────────────────────────────
|
|
60
|
+
let selected = docs.slice();
|
|
61
|
+
if (manifest.include && manifest.include.length) {
|
|
62
|
+
const inc = manifest.include.map(globToRegExp);
|
|
63
|
+
selected = selected.filter((d) => matchesAny(d, inc));
|
|
64
|
+
}
|
|
65
|
+
if (manifest.exclude && manifest.exclude.length) {
|
|
66
|
+
const exc = manifest.exclude.map(globToRegExp);
|
|
67
|
+
selected = selected.filter((d) => !matchesAny(d, exc));
|
|
68
|
+
}
|
|
69
|
+
const copies = selected.map((d) => ({ ...d }));
|
|
70
|
+
// 2) baseline order overlay ─────────────────────────────────────────────────
|
|
71
|
+
// 'filename' fills only docs with no explicit `order`, so metadata still wins.
|
|
72
|
+
if (manifest.sort === 'filename') {
|
|
73
|
+
const natural = copies
|
|
74
|
+
.slice()
|
|
75
|
+
.sort((a, b) => (a.path ?? a.filename).localeCompare(b.path ?? b.filename, undefined, {
|
|
76
|
+
numeric: true,
|
|
77
|
+
sensitivity: 'base',
|
|
78
|
+
}));
|
|
79
|
+
natural.forEach((d, i) => {
|
|
80
|
+
if (d.order === undefined)
|
|
81
|
+
d.order = i * 10;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
// 3) explicit `order` list leads, in the given sequence ─────────────────────
|
|
85
|
+
// Large-negative orders keep listed docs ahead of everything else while
|
|
86
|
+
// preserving their relative order; pins/parents still apply (navSortKey).
|
|
87
|
+
if (manifest.order && manifest.order.length) {
|
|
88
|
+
manifest.order.forEach((key, i) => {
|
|
89
|
+
for (const d of copies) {
|
|
90
|
+
if (docMatchesKey(d, key))
|
|
91
|
+
d.order = -1_000_000 + i;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return copies;
|
|
96
|
+
}
|
|
@@ -32,6 +32,109 @@ docMarked.use({
|
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
|
+
// ── Prose Markdown: wikilinks + footnotes ────────────────────────────────────
|
|
36
|
+
// Enabled by default because each activates ONLY on its own syntax ([[…]] /
|
|
37
|
+
// [^id]) — a doc that doesn't use them renders byte-identically (tosijs-ui's own
|
|
38
|
+
// docs use neither). marked core supports neither; both are common in prose.
|
|
39
|
+
function slugify(s) {
|
|
40
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
41
|
+
}
|
|
42
|
+
function escapeHtml(s) {
|
|
43
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
44
|
+
}
|
|
45
|
+
// Per-parse footnote state. `parse()` is synchronous and Marked tokenizes fully
|
|
46
|
+
// before rendering, so a module-level object is safe: preprocess resets it, the
|
|
47
|
+
// tokenizers fill it (defs during the block pass, refs during inline — refs set
|
|
48
|
+
// the number by first-appearance order), postprocess renders the endnotes.
|
|
49
|
+
const plainMarked = new Marked(); // renders footnote-definition markdown (no extensions)
|
|
50
|
+
let footnoteDefs = new Map();
|
|
51
|
+
let footnoteOrder = [];
|
|
52
|
+
docMarked.use({
|
|
53
|
+
hooks: {
|
|
54
|
+
preprocess(markdown) {
|
|
55
|
+
footnoteDefs = new Map();
|
|
56
|
+
footnoteOrder = [];
|
|
57
|
+
return markdown;
|
|
58
|
+
},
|
|
59
|
+
postprocess(html) {
|
|
60
|
+
if (footnoteOrder.length === 0)
|
|
61
|
+
return html;
|
|
62
|
+
const items = footnoteOrder
|
|
63
|
+
.map((id) => {
|
|
64
|
+
const body = plainMarked.parseInline(footnoteDefs.get(id) ?? '');
|
|
65
|
+
const eid = slugify(id);
|
|
66
|
+
return `<li id="fn-${eid}"><p>${body} <a href="#fnref-${eid}" class="footnote-backref" aria-label="Back to reference">↩</a></p></li>`;
|
|
67
|
+
})
|
|
68
|
+
.join('\n');
|
|
69
|
+
return `${html}\n<section class="footnotes" role="doc-endnotes">\n<hr />\n<ol>\n${items}\n</ol>\n</section>\n`;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
extensions: [
|
|
73
|
+
{
|
|
74
|
+
// [[slug]] / [[slug|label]] → link to /slug/. Not matched inside code spans
|
|
75
|
+
// (Marked tokenizes those first). Resolves by slugifying the target.
|
|
76
|
+
name: 'wikilink',
|
|
77
|
+
level: 'inline',
|
|
78
|
+
start(src) {
|
|
79
|
+
return src.match(/\[\[/)?.index;
|
|
80
|
+
},
|
|
81
|
+
tokenizer(src) {
|
|
82
|
+
const m = /^\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]/.exec(src);
|
|
83
|
+
if (!m)
|
|
84
|
+
return undefined;
|
|
85
|
+
return {
|
|
86
|
+
type: 'wikilink',
|
|
87
|
+
raw: m[0],
|
|
88
|
+
target: m[1].trim(),
|
|
89
|
+
label: (m[2] ?? m[1]).trim(),
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
renderer(token) {
|
|
93
|
+
return `<a href="/${slugify(token.target)}/" class="wikilink">${escapeHtml(token.label)}</a>`;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
// [^id]: definition (single line) — collected, rendered as endnotes.
|
|
98
|
+
name: 'footnoteDef',
|
|
99
|
+
level: 'block',
|
|
100
|
+
start(src) {
|
|
101
|
+
return src.match(/^\[\^[^\]]+\]:/m)?.index;
|
|
102
|
+
},
|
|
103
|
+
tokenizer(src) {
|
|
104
|
+
const m = /^\[\^([^\]]+)\]:[ \t]*([^\n]*)(?:\n|$)/.exec(src);
|
|
105
|
+
if (!m)
|
|
106
|
+
return undefined;
|
|
107
|
+
footnoteDefs.set(m[1].trim(), m[2].trim());
|
|
108
|
+
return { type: 'footnoteDef', raw: m[0] }; // collected; renders nothing inline
|
|
109
|
+
},
|
|
110
|
+
renderer() {
|
|
111
|
+
return '';
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
// [^id] reference → superscript link to the endnote.
|
|
116
|
+
name: 'footnoteRef',
|
|
117
|
+
level: 'inline',
|
|
118
|
+
start(src) {
|
|
119
|
+
return src.match(/\[\^/)?.index;
|
|
120
|
+
},
|
|
121
|
+
tokenizer(src) {
|
|
122
|
+
const m = /^\[\^([^\]]+)\]/.exec(src);
|
|
123
|
+
if (!m)
|
|
124
|
+
return undefined;
|
|
125
|
+
const id = m[1].trim();
|
|
126
|
+
if (!footnoteOrder.includes(id))
|
|
127
|
+
footnoteOrder.push(id);
|
|
128
|
+
return { type: 'footnoteRef', raw: m[0], id };
|
|
129
|
+
},
|
|
130
|
+
renderer(token) {
|
|
131
|
+
const n = footnoteOrder.indexOf(token.id) + 1;
|
|
132
|
+
const eid = slugify(token.id);
|
|
133
|
+
return `<sup class="footnote-ref"><a href="#fn-${eid}" id="fnref-${eid}">${n}</a></sup>`;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
});
|
|
35
138
|
/** Render a doc's markdown text to HTML (synchronous, default marked options). */
|
|
36
139
|
export function renderDocMarkdown(text) {
|
|
37
140
|
return docMarked.parse(text);
|
|
@@ -14,11 +14,27 @@ export interface Doc {
|
|
|
14
14
|
keywords?: string | string[];
|
|
15
15
|
image?: string;
|
|
16
16
|
noindex?: boolean;
|
|
17
|
+
author?: string;
|
|
18
|
+
date?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface ExtractDocsOptions {
|
|
19
21
|
paths: string[];
|
|
20
22
|
ignore?: string[];
|
|
21
23
|
output?: string;
|
|
22
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse & strip a leading YAML frontmatter block (`---\n…\n---`). Every prose
|
|
27
|
+
* toolchain (Jekyll/Hugo/Astro/Obsidian/Pandoc) uses it, so authors paste it in;
|
|
28
|
+
* without this the `---` was rendered as content (and became the doc title).
|
|
29
|
+
*
|
|
30
|
+
* A minimal, dependency-free subset: `key: value` lines mapped onto doc metadata
|
|
31
|
+
* (`title`, `order`→number, `author`, `date`, `draft: true`→hidden). Only strips
|
|
32
|
+
* when the block actually parses as ≥1 key/value pair, so a genuine leading `---`
|
|
33
|
+
* horizontal rule is left alone. Frontmatter wins over the JSON-comment metadata.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseFrontmatter(content: string): {
|
|
36
|
+
data: Partial<Doc>;
|
|
37
|
+
body: string;
|
|
38
|
+
};
|
|
23
39
|
export declare function extractDocs(options: ExtractDocsOptions): Doc[];
|
|
24
40
|
export declare function saveDocsJSON(docs: Doc[], outputPath: string): void;
|