tosijs-ui 1.17.2 → 1.17.4
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 +64 -0
- package/dist/doc-browser.d.ts +3 -1
- package/dist/doc-browser.js +16 -1
- package/dist/doc-system/example-policy.d.ts +9 -0
- package/dist/doc-system/example-policy.js +10 -0
- package/dist/doc-system/site/dev-server.js +8 -3
- package/dist/doc-system/site/docs.d.ts +5 -0
- package/dist/doc-system/site/docs.js +7 -0
- package/dist/doc-system/site/generate-site.d.ts +4 -0
- package/dist/doc-system/site/generate-site.js +119 -5
- package/dist/doc-system/site/orchestrator.js +1 -0
- package/dist/doc-system/site/page-globals.d.ts +3 -1
- package/dist/doc-system/site/page-globals.js +7 -2
- package/dist/doc-system/site/site-config.d.ts +9 -1
- package/dist/iife.js +100 -100
- package/dist/iife.js.map +11 -11
- package/dist/live-example/component.d.ts +8 -0
- package/dist/live-example/component.js +53 -9
- package/dist/live-example/index.d.ts +2 -2
- package/dist/live-example/index.js +1 -1
- package/dist/live-example/insert-examples.d.ts +4 -1
- package/dist/live-example/insert-examples.js +20 -1
- package/dist/live-example/styles.d.ts +6 -0
- package/dist/live-example/styles.js +11 -0
- package/dist/live-theme.js +0 -3
- package/dist/palette.js +3 -4
- package/dist/tab-selector.d.ts +10 -0
- package/dist/tab-selector.js +63 -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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.17.4
|
|
4
|
+
|
|
5
|
+
> Live examples' inline views keep their controls in the code panel's tab bar, and a site or
|
|
6
|
+
> page can set the view its examples open in. Nothing to change on upgrade. Asked by tjs-lang
|
|
7
|
+
> for its learn-to-program book (#252). `dist/iife.js` grows 0.6 kB (508.7 kB gzip).
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `SiteConfig.exampleView` (`'preview'` | `'code'` | `'console'`) sets the view every live
|
|
12
|
+
example opens in, and `exampleView` in a page's metadata or frontmatter sets it for one page.
|
|
13
|
+
The nearest setting wins (fence, then page, then site), and `"preview"` opts out of a default
|
|
14
|
+
above it, so a big example on a code-first page takes `{"view": "preview"}`. A misspelt value
|
|
15
|
+
fails the build in the site config and is reported in page metadata. `setExampleView` (from
|
|
16
|
+
`tosijs-ui/live-example`) sets it from code.
|
|
17
|
+
- A refresh button in the code panel's tab bar, beside ⋮, in every view. Editing does not re-run
|
|
18
|
+
an example, and ⌘S / ⌘R were undiscoverable.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- In the inline views (`{"view": "code"}`, `{"view": "console"}`) the toolbar column beside the
|
|
23
|
+
preview is gone: it stood between the preview and its code. Maximize and the new-window editor
|
|
24
|
+
are in the code panel's tab bar. "view/edit code" (the code is already open) and the tests
|
|
25
|
+
check (inline tests always run) are not shown there. Maximized and plain examples are
|
|
26
|
+
unchanged.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- `<tosi-tabs>` with more tabs than fit pushed its `after-tabs` controls past its clipped edge:
|
|
31
|
+
at phone width a live example's close button was pushed past the panel's clipped edge. The tab names now
|
|
32
|
+
scroll (swipe, trackpad, or the mouse wheel over them) and the controls stay where they are.
|
|
33
|
+
Selecting a tab scrolls it into view, and the underline follows the scroll, clipped to the
|
|
34
|
+
strip.
|
|
35
|
+
|
|
36
|
+
## 1.17.3
|
|
37
|
+
|
|
38
|
+
> A doc site answers `/slug` as well as `/slug/`, and a missing page gets a real 404 page.
|
|
39
|
+
> Nothing to change on upgrade. On Firebase, add `"cleanUrls": true` to `firebase.json` for
|
|
40
|
+
> `/slug` to serve the page; GitHub Pages, Netlify and Cloudflare Pages need nothing.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- Every page (and alias) is also written as `/slug.html`, with its references written for
|
|
45
|
+
`/slug`, so a host that resolves extensionless paths (GitHub Pages, Netlify, Cloudflare
|
|
46
|
+
Pages, Firebase with `cleanUrls`) answers `/slug` with the page instead of a redirect. An
|
|
47
|
+
agent's fetch tool that does not follow redirects got a "moved" stub at
|
|
48
|
+
virta.tosijs.net/start. The canonical link stays `/slug/`; a file the site already has at
|
|
49
|
+
`/slug.html` is left alone and named in a warning.
|
|
50
|
+
- The build writes `404.html` (unless the site ships its own): it names the path asked for,
|
|
51
|
+
suggests the closest page, lists every page and links `llms.txt`. Every link in it is
|
|
52
|
+
absolute, since hosts serve it at whatever URL was asked for.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- After Cmd-K, the doc site's search field sat on top of a maximized live example. Opening the
|
|
57
|
+
palette raised the whole `<tosi-palette>` (its trigger included) to the page's highest
|
|
58
|
+
z-index and never put it back, and any overlay on the page (haltija's widget on a dev
|
|
59
|
+
server, a toast in an app) made that higher than the example's. The dialog never needed it:
|
|
60
|
+
`showModal()` already puts it in the top layer.
|
|
61
|
+
- The dev server served `/slug` from `slug/index.html`, whose references are written for
|
|
62
|
+
`/slug/`; it now serves `slug.html`. (Its `.html` fallback looked for `slug/.html` and never
|
|
63
|
+
matched.)
|
|
64
|
+
- The live-theme page showed two theme editors: its example had an `html` block and a `js`
|
|
65
|
+
block that each made one.
|
|
66
|
+
|
|
3
67
|
## 1.17.2
|
|
4
68
|
|
|
5
69
|
> Components carry only the icons they draw, and `<tosi-affix>` centres a caption on its
|
package/dist/doc-browser.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AccessibilityMode } from './doc-system/render.js';
|
|
2
2
|
import { TestResults } from './live-example/test-harness.js';
|
|
3
|
-
import { type ExamplePolicy } from './doc-system/example-policy.js';
|
|
3
|
+
import { type ExamplePolicy, type ExampleView } from './doc-system/example-policy.js';
|
|
4
4
|
export declare const hasTestBlock: (text: string | undefined, policy?: ExamplePolicy) => boolean;
|
|
5
5
|
export interface PageTestResults {
|
|
6
6
|
passed: boolean;
|
|
@@ -45,6 +45,8 @@ export interface Doc {
|
|
|
45
45
|
header?: boolean;
|
|
46
46
|
/** `'full-width'` drops the reading measure; `'full-screen'` also puts the nav away. */
|
|
47
47
|
layout?: 'full-width' | 'full-screen';
|
|
48
|
+
/** the view this page's live examples open in (see the site `Doc.exampleView`) */
|
|
49
|
+
exampleView?: ExampleView;
|
|
48
50
|
testStatus?: 'passed' | 'failed' | 'pending';
|
|
49
51
|
bakes?: Array<[string, {
|
|
50
52
|
dialect: string;
|
package/dist/doc-browser.js
CHANGED
|
@@ -110,7 +110,7 @@ import { LiveExample, testManager, pageTestCount, enableTests, disableTests, } f
|
|
|
110
110
|
import { highlightBlocks } from './doc-system/highlight.js';
|
|
111
111
|
import { examplePolicy } from './live-example/insert-examples.js';
|
|
112
112
|
import { collectCodeTokens } from './doc-system/code-fences.js';
|
|
113
|
-
import { isLiveFence } from './doc-system/example-policy.js';
|
|
113
|
+
import { isExampleView, isLiveFence, } from './doc-system/example-policy.js';
|
|
114
114
|
import { tosiSidenav, TosiSidenav } from './side-nav.js';
|
|
115
115
|
import { icons } from './icons.js';
|
|
116
116
|
import { tosiPalette } from './palette.js';
|
|
@@ -957,6 +957,19 @@ export function createDocBrowser(options) {
|
|
|
957
957
|
// render from doc text. Every navigation funnels through navigateTo() -> showDoc().
|
|
958
958
|
const docContent = contentElement || div();
|
|
959
959
|
docContent.classList.add('doc-content');
|
|
960
|
+
/*
|
|
961
|
+
A page's `exampleView` (#252), where its examples look for it: on the content they sit in. A
|
|
962
|
+
value the metadata block got wrong is reported, not ignored, as frontmatter's is.
|
|
963
|
+
*/
|
|
964
|
+
const stampExampleView = (doc) => {
|
|
965
|
+
const view = doc.exampleView;
|
|
966
|
+
if (view !== undefined && !isExampleView(view))
|
|
967
|
+
console.error(`${doc.path || doc.filename}: unknown exampleView ${JSON.stringify(view)}`);
|
|
968
|
+
if (isExampleView(view))
|
|
969
|
+
docContent.setAttribute('data-example-view', view);
|
|
970
|
+
else
|
|
971
|
+
docContent.removeAttribute('data-example-view');
|
|
972
|
+
};
|
|
960
973
|
Object.assign(docContent.style, {
|
|
961
974
|
display: 'block',
|
|
962
975
|
// Same var the pre-hydration `:not(:defined)` layout uses (doc-system-styles.ts).
|
|
@@ -1132,6 +1145,7 @@ export function createDocBrowser(options) {
|
|
|
1132
1145
|
rewriteContentLinks();
|
|
1133
1146
|
// Stamp each example with its source file (for the source↔doc map). doc.path
|
|
1134
1147
|
// is the extracted file (.md, or a source file with doc comments).
|
|
1148
|
+
stampExampleView(doc);
|
|
1135
1149
|
LiveExample.insertExamples(docContent, context, doc.path || undefined);
|
|
1136
1150
|
focusableCodeBlocks(docContent);
|
|
1137
1151
|
/*
|
|
@@ -1370,6 +1384,7 @@ export function createDocBrowser(options) {
|
|
|
1370
1384
|
if (view === 'preview') {
|
|
1371
1385
|
docContent.innerHTML = renderDocMarkdown(docMarkdownFromSource(editUI.editor.value, editUI.doc.path), { wikilinks, accessible: !!accessibility, title: editUI.doc.title });
|
|
1372
1386
|
rewriteContentLinks();
|
|
1387
|
+
stampExampleView(editUI.doc);
|
|
1373
1388
|
LiveExample.insertExamples(docContent, context, editUI.doc.path || undefined);
|
|
1374
1389
|
focusableCodeBlocks(docContent);
|
|
1375
1390
|
}
|
|
@@ -71,6 +71,15 @@ export declare const EXAMPLE_TABS: readonly ["js", "html", "css", "test", "conso
|
|
|
71
71
|
export type ExampleTab = (typeof EXAMPLE_TABS)[number];
|
|
72
72
|
/** Why `value` is not a list of example tabs, or undefined when it is one. */
|
|
73
73
|
export declare function exampleTabsProblem(value: unknown): string | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The view a live example opens in (#252): `preview` (the default), `code` (the preview beside
|
|
76
|
+
* its code) or `console` (the console beside the code). Set per fence (`{"view": "code"}`), per
|
|
77
|
+
* page (`exampleView` in its metadata) or per site (`SiteConfig.exampleView`); the nearest wins,
|
|
78
|
+
* so `preview` opts one example or page out of a default.
|
|
79
|
+
*/
|
|
80
|
+
export declare const EXAMPLE_VIEWS: readonly ["preview", "code", "console"];
|
|
81
|
+
export type ExampleView = (typeof EXAMPLE_VIEWS)[number];
|
|
82
|
+
export declare function isExampleView(value: unknown): value is ExampleView;
|
|
74
83
|
/**
|
|
75
84
|
* Will this fence become a live example?
|
|
76
85
|
*
|
|
@@ -118,6 +118,16 @@ export function exampleTabsProblem(value) {
|
|
|
118
118
|
.join(', ')} (the tabs are ${EXAMPLE_TABS.join(', ')})`
|
|
119
119
|
: undefined;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* The view a live example opens in (#252): `preview` (the default), `code` (the preview beside
|
|
123
|
+
* its code) or `console` (the console beside the code). Set per fence (`{"view": "code"}`), per
|
|
124
|
+
* page (`exampleView` in its metadata) or per site (`SiteConfig.exampleView`); the nearest wins,
|
|
125
|
+
* so `preview` opts one example or page out of a default.
|
|
126
|
+
*/
|
|
127
|
+
export const EXAMPLE_VIEWS = ['preview', 'code', 'console'];
|
|
128
|
+
export function isExampleView(value) {
|
|
129
|
+
return EXAMPLE_VIEWS.includes(value);
|
|
130
|
+
}
|
|
121
131
|
/**
|
|
122
132
|
* Will this fence become a live example?
|
|
123
133
|
*
|
|
@@ -1228,10 +1228,15 @@ export async function devServer(config, opts = {}) {
|
|
|
1228
1228
|
*/
|
|
1229
1229
|
if (!isUnderRoot(directory, basePath))
|
|
1230
1230
|
return null;
|
|
1231
|
-
|
|
1232
|
-
|
|
1231
|
+
// `/slug` is the build's `slug.html` (written for that URL) before `slug/index.html`;
|
|
1232
|
+
// `/slug/` is only ever the folder's index
|
|
1233
|
+
const candidates = [
|
|
1234
|
+
basePath,
|
|
1235
|
+
...(reqPath.endsWith('/') ? [] : [basePath + '.html']),
|
|
1236
|
+
path.join(basePath, 'index.html'),
|
|
1237
|
+
];
|
|
1238
|
+
for (const pathWithSuffix of candidates) {
|
|
1233
1239
|
try {
|
|
1234
|
-
const pathWithSuffix = path.join(basePath, suffix);
|
|
1235
1240
|
const stat = statSync(pathWithSuffix);
|
|
1236
1241
|
if (stat && stat.isFile()) {
|
|
1237
1242
|
return pathWithSuffix;
|
|
@@ -76,6 +76,11 @@ export interface Doc {
|
|
|
76
76
|
* reach them from out here.
|
|
77
77
|
*/
|
|
78
78
|
layout?: 'full-width' | 'full-screen';
|
|
79
|
+
/**
|
|
80
|
+
* The view this page's live examples open in (1.17.4, #252): `'code'`, `'console'`, or
|
|
81
|
+
* `'preview'` to opt out of the site's `exampleView`. A fence's `{"view": …}` still wins.
|
|
82
|
+
*/
|
|
83
|
+
exampleView?: 'preview' | 'code' | 'console';
|
|
79
84
|
headTitle?: string;
|
|
80
85
|
description?: string;
|
|
81
86
|
keywords?: string | string[];
|
|
@@ -160,6 +160,13 @@ export function parseFrontmatter(content) {
|
|
|
160
160
|
else if (val)
|
|
161
161
|
console.error(`unknown layout ${JSON.stringify(val)} in frontmatter`);
|
|
162
162
|
}
|
|
163
|
+
else if (key === 'exampleview') {
|
|
164
|
+
// Same reasoning as layout: a misspelt view would silently do nothing.
|
|
165
|
+
if (val === 'preview' || val === 'code' || val === 'console')
|
|
166
|
+
data.exampleView = val;
|
|
167
|
+
else if (val)
|
|
168
|
+
console.error(`unknown exampleView ${JSON.stringify(val)} in frontmatter`);
|
|
169
|
+
}
|
|
163
170
|
else if (key === 'header') {
|
|
164
171
|
if (/^(false|no|0)$/i.test(val))
|
|
165
172
|
data.header = false;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { Doc } from './docs.js';
|
|
2
2
|
import type { ProjectLinks, LinkItem } from '../../doc-browser.js';
|
|
3
|
+
import { NavNode } from '../nav-tree.js';
|
|
3
4
|
import { type AccessibilityMode, type ExampleBakes } from '../render.js';
|
|
4
5
|
declare global {
|
|
5
6
|
var Bun: any;
|
|
6
7
|
}
|
|
7
8
|
export interface GenerateSiteConfig {
|
|
8
9
|
docs: Doc[];
|
|
10
|
+
/** whether the site serves `/llms.txt`, which the 404 page then points to */
|
|
11
|
+
llmsTxt?: boolean;
|
|
9
12
|
/** `SiteConfig.aliases`: { alias path -> slug } (board #3513) */
|
|
10
13
|
aliases?: Record<string, string>;
|
|
11
14
|
/** `SiteConfig.searchPlaceholder` */
|
|
@@ -160,3 +163,4 @@ export declare function withStamp(url: string, stamp?: string): string;
|
|
|
160
163
|
*/
|
|
161
164
|
export declare function rebaseRelativeRefs(html: string, fromPath: string, depth: number): string;
|
|
162
165
|
export declare function generateSite(config: GenerateSiteConfig): Promise<number>;
|
|
166
|
+
export declare function notFoundHtml(pages: NavNode<Doc>[], slugMap: Record<string, string>, config: Pick<GenerateSiteConfig, 'projectName' | 'basePath' | 'lang' | 'llmsTxt'>): string;
|
|
@@ -163,11 +163,12 @@ export function relativeUrl(depth, p) {
|
|
|
163
163
|
}
|
|
164
164
|
async function pageHtml(doc, config, slugMap, configAttr, wikilinks,
|
|
165
165
|
/** the slug this copy is written at: the doc's own, or an alias of it */
|
|
166
|
-
at = slugMap[doc.filename] ?? ''
|
|
166
|
+
at = slugMap[doc.filename] ?? '',
|
|
167
|
+
/** the directory depth its references resolve from: 0 for the `/slug` copy (`slug.html`) */
|
|
168
|
+
depth = pageDepth(at)) {
|
|
167
169
|
const { projectName = '', baseUrl = '', lang = 'en', favicon = '/favicon.svg', docsUrl = '/docs.json', scriptUrl = '/iife.js', hydrateUrl, stylesUrl = '/doc-system.css', assetStamp, docsStamp, localizedUrl = '/localized-strings.txt', basePath, headExtra = '', bakes, } = config;
|
|
168
170
|
// Functional URLs are emitted relative to THIS page's depth so the build is
|
|
169
171
|
// mount-agnostic (issue #25); metadata URLs below stay absolute via withBase.
|
|
170
|
-
const depth = pageDepth(at);
|
|
171
172
|
const own = slugMap[doc.filename] ?? '';
|
|
172
173
|
const localizedAttr = config.localizedStrings
|
|
173
174
|
? ` localized="${escapeAttr(relativeUrl(depth, localizedUrl))}"`
|
|
@@ -210,9 +211,10 @@ at = slugMap[doc.filename] ?? '') {
|
|
|
210
211
|
config.liveExamples ?? 'auto'), (filename) => slugMap[filename] !== undefined
|
|
211
212
|
? relativeUrl(depth, pathForSlug(slugMap[filename]))
|
|
212
213
|
: null);
|
|
213
|
-
// A copy
|
|
214
|
-
// the same file (a README's
|
|
215
|
-
|
|
214
|
+
// A copy anywhere but the page's own path (an alias, or `/slug` with no slash): what the
|
|
215
|
+
// author wrote relative to that path must still reach the same file (a README's
|
|
216
|
+
// `img/logo.png` is /img/logo.png, not /readme/img/logo.png).
|
|
217
|
+
const body = at === own && depth === pageDepth(own)
|
|
216
218
|
? rendered
|
|
217
219
|
: rebaseRelativeRefs(rendered, pathForSlug(own), depth);
|
|
218
220
|
const nav = navHtml(config.docs, slugMap, doc.filename, depth);
|
|
@@ -562,6 +564,35 @@ export async function generateSite(config) {
|
|
|
562
564
|
await Bun.write(`${dir}/index.md`, doc.text.trimEnd() + '\n');
|
|
563
565
|
}
|
|
564
566
|
}
|
|
567
|
+
/*
|
|
568
|
+
Every page again as `slug.html`, with its references written for `/slug`, so the trailing
|
|
569
|
+
slash is optional. Hosts serve `/slug` from `slug/index.html` with a redirect, and a client
|
|
570
|
+
that does not follow it (an agent's fetch tool, onboarding from a typed URL) got a "moved"
|
|
571
|
+
stub instead of the page. With `slug.html` beside it, a host that resolves extensionless
|
|
572
|
+
paths to `.html` (GitHub Pages, Netlify, Cloudflare Pages, Firebase with `cleanUrls`)
|
|
573
|
+
answers `/slug` with the page itself. The canonical link stays `/slug/`.
|
|
574
|
+
*/
|
|
575
|
+
const slashless = [
|
|
576
|
+
...docs.map((doc) => [slugMap[doc.filename], doc]),
|
|
577
|
+
...Object.entries(aliases).map(([alias, target]) => [alias, docs.find((d) => slugMap[d.filename] === target)]),
|
|
578
|
+
];
|
|
579
|
+
const occupied = [];
|
|
580
|
+
for (const [at, doc] of slashless) {
|
|
581
|
+
if (at === '')
|
|
582
|
+
continue;
|
|
583
|
+
const file = `${outputDir}/${at}.html`;
|
|
584
|
+
// the output folder starts empty, so a file here is one the site ships: leave it alone
|
|
585
|
+
if (existsSync(file)) {
|
|
586
|
+
occupied.push(` /${at}.html`);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
await Bun.write(file, await pageHtml(doc, config, slugMap, configAttr, wikilinks, at, 0));
|
|
590
|
+
}
|
|
591
|
+
if (occupied.length) {
|
|
592
|
+
console.warn(`\n⚠️ These pages are served only at /slug/, because the site already has a file where\n` +
|
|
593
|
+
` the /slug copy goes (rename the file to make the trailing slash optional):\n` +
|
|
594
|
+
`${occupied.join('\n')}\n`);
|
|
595
|
+
}
|
|
565
596
|
if (danglingWikilinks.length) {
|
|
566
597
|
misconfigured(config.strict, `\n⚠️ Wikilinks that reach no doc page. A target matches a page's slug (its file name)\n` +
|
|
567
598
|
` or its title; write [[slug|label]] to link one by file name, and an ordinary\n` +
|
|
@@ -631,5 +662,88 @@ export async function generateSite(config) {
|
|
|
631
662
|
await Bun.write(robotsPath, `${base}Sitemap: ${config.baseUrl}/sitemap.xml\n`);
|
|
632
663
|
}
|
|
633
664
|
}
|
|
665
|
+
// A site's own 404.html (copied from its static files) wins.
|
|
666
|
+
if (!existsSync(`${outputDir}/404.html`)) {
|
|
667
|
+
await Bun.write(`${outputDir}/404.html`, notFoundHtml(flattenNav(buildNavTree(docs, slugMap)), slugMap, config));
|
|
668
|
+
}
|
|
634
669
|
return count;
|
|
635
670
|
}
|
|
671
|
+
/*
|
|
672
|
+
The page a host serves for a path the site does not have: GitHub Pages and Firebase serve
|
|
673
|
+
`404.html` from the site root, at whatever URL was asked for, so nothing in it may be
|
|
674
|
+
relative. It lists every page, which is what a reader without JavaScript (an agent's fetch
|
|
675
|
+
tool, most often) needs to find its way, and with JavaScript it names the closest page to the
|
|
676
|
+
path asked for. The script is a nicety: under a policy that blocks inline scripts, the list
|
|
677
|
+
is still there.
|
|
678
|
+
*/
|
|
679
|
+
export function notFoundHtml(pages, slugMap, config) {
|
|
680
|
+
const { projectName = '', basePath, lang = 'en', llmsTxt } = config;
|
|
681
|
+
const href = (slug) => withBase(basePath, pathForSlug(slug));
|
|
682
|
+
const listed = pages.filter((node) => !node.doc.hidden);
|
|
683
|
+
const items = listed
|
|
684
|
+
.map((node) => ` <li><a href="${escapeAttr(href(node.slug))}">${escapeText(node.doc.title || node.slug || 'Home')}</a></li>`)
|
|
685
|
+
.join('\n');
|
|
686
|
+
const slugs = JSON.stringify(Object.values(slugMap).filter((slug) => slug !== '')).replace(/</g, '\\u003c');
|
|
687
|
+
const title = projectName
|
|
688
|
+
? `Page not found — ${projectName}`
|
|
689
|
+
: 'Page not found';
|
|
690
|
+
return `<!DOCTYPE html>
|
|
691
|
+
<html lang="${escapeAttr(lang)}">
|
|
692
|
+
<head>
|
|
693
|
+
<meta charset="utf-8" />
|
|
694
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
695
|
+
<meta name="robots" content="noindex" />
|
|
696
|
+
<title>${escapeText(title)}</title>
|
|
697
|
+
<style>
|
|
698
|
+
body { font: 16px/1.5 system-ui, sans-serif; max-width: 40em; margin: 2em auto; padding: 0 1em; }
|
|
699
|
+
@media (prefers-color-scheme: dark) { body { background: #111; color: #eee; } a { color: #8bf; } }
|
|
700
|
+
</style>
|
|
701
|
+
</head>
|
|
702
|
+
<body>
|
|
703
|
+
<h1>Page not found</h1>
|
|
704
|
+
<p>There is no page at <code id="asked"></code> on this site.<span id="closest"></span></p>
|
|
705
|
+
<p><a href="${escapeAttr(withBase(basePath, '/'))}">Home</a>${llmsTxt
|
|
706
|
+
? ` · <a href="${escapeAttr(withBase(basePath, '/llms.txt'))}">llms.txt</a> (every page, for agents)`
|
|
707
|
+
: ''}</p>
|
|
708
|
+
<h2>Pages</h2>
|
|
709
|
+
<ul>
|
|
710
|
+
${items}
|
|
711
|
+
</ul>
|
|
712
|
+
<script>
|
|
713
|
+
;(() => {
|
|
714
|
+
const slugs = ${slugs}
|
|
715
|
+
const base = ${JSON.stringify(withBase(basePath, '/'))}
|
|
716
|
+
const asked = decodeURIComponent(location.pathname)
|
|
717
|
+
document.getElementById('asked').textContent = asked
|
|
718
|
+
const want = asked.slice(base.length - 1).replace(/^\\/+|\\/+$/g, '').toLowerCase()
|
|
719
|
+
if (!want) return
|
|
720
|
+
const distance = (a, b) => {
|
|
721
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i)
|
|
722
|
+
for (let i = 1; i <= a.length; i++) {
|
|
723
|
+
let prev = row[0]++
|
|
724
|
+
for (let j = 1; j <= b.length; j++) {
|
|
725
|
+
const next = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1))
|
|
726
|
+
prev = row[j]
|
|
727
|
+
row[j] = next
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return row[b.length]
|
|
731
|
+
}
|
|
732
|
+
let best = '', score = Infinity
|
|
733
|
+
for (const slug of slugs) {
|
|
734
|
+
const prefix = Math.min(want.length, slug.length) >= 3 && (slug.startsWith(want) || want.startsWith(slug))
|
|
735
|
+
const d = prefix ? 0.5 : distance(want, slug)
|
|
736
|
+
if (d < score) [best, score] = [slug, d]
|
|
737
|
+
}
|
|
738
|
+
if (score > Math.max(2, want.length / 3)) return
|
|
739
|
+
const link = document.createElement('a')
|
|
740
|
+
link.href = base + best + '/'
|
|
741
|
+
link.textContent = base + best + '/'
|
|
742
|
+
const closest = document.getElementById('closest')
|
|
743
|
+
closest.append(' Did you mean ', link, '?')
|
|
744
|
+
})()
|
|
745
|
+
</script>
|
|
746
|
+
</body>
|
|
747
|
+
</html>
|
|
748
|
+
`;
|
|
749
|
+
}
|
|
@@ -1653,6 +1653,7 @@ export async function buildSite(config, opts = {}) {
|
|
|
1653
1653
|
const pageCount = await generateSite({
|
|
1654
1654
|
liveExamples: config.liveExamples,
|
|
1655
1655
|
docs,
|
|
1656
|
+
llmsTxt: config.llmsTxt !== false,
|
|
1656
1657
|
aliases: config.aliases,
|
|
1657
1658
|
searchPlaceholder: config.searchPlaceholder,
|
|
1658
1659
|
accessibility: config.accessibility,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ExampleTab } from '../example-policy.js';
|
|
1
|
+
import type { ExampleTab, ExampleView } from '../example-policy.js';
|
|
2
2
|
export interface PageGlobalsConfig {
|
|
3
3
|
liveExamples?: 'auto' | 'opt-in' | 'no-code';
|
|
4
4
|
exampleConsole?: boolean;
|
|
5
5
|
exampleTabs?: ExampleTab[];
|
|
6
|
+
exampleView?: ExampleView;
|
|
6
7
|
dialects?: string[];
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
@@ -13,5 +14,6 @@ export interface PageGlobalsConfig {
|
|
|
13
14
|
export declare function siteSwitchProblem(config: {
|
|
14
15
|
liveExamples?: string;
|
|
15
16
|
kind?: string;
|
|
17
|
+
exampleView?: string;
|
|
16
18
|
}): string | null;
|
|
17
19
|
export declare function pageGlobalsHead(config: PageGlobalsConfig): string;
|
|
@@ -6,7 +6,7 @@ bundle reads when it runs (so they must come BEFORE the bundle script — genera
|
|
|
6
6
|
Each is emitted only when it differs from the default, so a default site's HTML is unchanged
|
|
7
7
|
byte-for-byte when a new switch is added.
|
|
8
8
|
*/
|
|
9
|
-
import { exampleTabsProblem, normalizeDialectNames } from '../example-policy.js';
|
|
9
|
+
import { EXAMPLE_VIEWS, exampleTabsProblem, normalizeDialectNames, } from '../example-policy.js';
|
|
10
10
|
const LIVE_EXAMPLE_POLICIES = ['auto', 'opt-in', 'no-code'];
|
|
11
11
|
const SITE_KINDS = ['documentation', 'book'];
|
|
12
12
|
/**
|
|
@@ -19,7 +19,8 @@ export function siteSwitchProblem(config) {
|
|
|
19
19
|
? `SiteConfig.${name} is ${JSON.stringify(value)}; it must be one of ${allowed.map((a) => `'${a}'`).join(', ')}`
|
|
20
20
|
: null;
|
|
21
21
|
return (wrong('liveExamples', config.liveExamples, LIVE_EXAMPLE_POLICIES) ??
|
|
22
|
-
wrong('kind', config.kind, SITE_KINDS)
|
|
22
|
+
wrong('kind', config.kind, SITE_KINDS) ??
|
|
23
|
+
wrong('exampleView', config.exampleView, [...EXAMPLE_VIEWS]));
|
|
23
24
|
}
|
|
24
25
|
export function pageGlobalsHead(config) {
|
|
25
26
|
// A misspelt tab would hide a tab on every example with nothing to say why.
|
|
@@ -35,6 +36,10 @@ export function pageGlobalsHead(config) {
|
|
|
35
36
|
config.exampleTabs
|
|
36
37
|
? `<script>globalThis.__TOSI_EXAMPLE_TABS=${JSON.stringify(config.exampleTabs)}</script>`
|
|
37
38
|
: '',
|
|
39
|
+
// #252: the view examples open in
|
|
40
|
+
config.exampleView && config.exampleView !== 'preview'
|
|
41
|
+
? `<script>globalThis.__TOSI_EXAMPLE_VIEW="${config.exampleView}"</script>`
|
|
42
|
+
: '',
|
|
38
43
|
// 1.16.2: the example console
|
|
39
44
|
config.exampleConsole === false
|
|
40
45
|
? `<script>globalThis.__TOSI_EXAMPLE_CONSOLE=false</script>`
|
|
@@ -8,7 +8,7 @@ import type { EditableDataOptions } from './data-store.js';
|
|
|
8
8
|
import type { BookManifest } from '../book-manifest.js';
|
|
9
9
|
import type { AccessibilityMode } from '../render.js';
|
|
10
10
|
import type { AltTextHook } from './alt-text.js';
|
|
11
|
-
import type { ExampleTab } from '../example-policy.js';
|
|
11
|
+
import type { ExampleTab, ExampleView } from '../example-policy.js';
|
|
12
12
|
export type SiteHost = 'github-pages' | 'firebase' | 'static';
|
|
13
13
|
/** Resolved paths handed to a `libraryBuild` override (see SiteConfig). */
|
|
14
14
|
export interface LibraryBuildContext {
|
|
@@ -200,6 +200,14 @@ export interface SiteConfig {
|
|
|
200
200
|
* `{"tabs": ["html"]}`. A hidden tab's block still runs; it just can't be edited.
|
|
201
201
|
*/
|
|
202
202
|
exampleTabs?: ExampleTab[];
|
|
203
|
+
/**
|
|
204
|
+
* The view every live example opens in (1.17.4, #252): `'code'` puts the preview beside its
|
|
205
|
+
* code, `'console'` the console beside the code. Default `'preview'`, the example alone. For
|
|
206
|
+
* a book whose small examples should show their code: set it once here instead of
|
|
207
|
+
* `{"view": "code"}` on every fence. A page overrides it with `exampleView` in its metadata,
|
|
208
|
+
* and one example with the fence option `{"view": "preview"}`.
|
|
209
|
+
*/
|
|
210
|
+
exampleView?: ExampleView;
|
|
203
211
|
/**
|
|
204
212
|
* Run script examples from same-origin modules (1.17.0, #245), for a site served under a
|
|
205
213
|
* Content Security Policy without `'unsafe-eval'` (`script-src 'self'`).
|