tosijs-ui 1.16.5 → 1.16.7
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 +103 -0
- package/dist/doc-browser.js +13 -1
- package/dist/doc-system/doc-system.d.ts +2 -1
- package/dist/doc-system/doc-system.js +13 -1
- package/dist/doc-system/site/build-warnings.d.ts +23 -0
- package/dist/doc-system/site/build-warnings.js +45 -0
- package/dist/doc-system/site/check-examples.js +2 -32
- package/dist/doc-system/site/code-fences.d.ts +5 -0
- package/dist/doc-system/site/code-fences.js +40 -0
- package/dist/doc-system/site/generate-site.d.ts +7 -0
- package/dist/doc-system/site/generate-site.js +34 -16
- package/dist/doc-system/site/index.d.ts +1 -0
- package/dist/doc-system/site/index.js +3 -0
- package/dist/doc-system/site/make-llms-txt.d.ts +9 -0
- package/dist/doc-system/site/make-llms-txt.js +14 -2
- package/dist/doc-system/site/orchestrator.js +84 -16
- package/dist/doc-system/site/site-config.d.ts +19 -0
- package/dist/iife.js +92 -92
- package/dist/iife.js.map +10 -10
- package/dist/live-example/code-transform.d.ts +19 -0
- package/dist/live-example/code-transform.js +40 -1
- package/dist/live-example/component.js +6 -2
- package/dist/live-example/execution.js +3 -1
- package/dist/live-example/test-harness.js +2 -2
- package/dist/router.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.16.7
|
|
4
|
+
|
|
5
|
+
A patch for readers that are not browsers, and for builds that were too polite. Every doc page
|
|
6
|
+
now also ships as markdown, for LLM fetchers that fail on ordinary HTML. Two misconfigurations
|
|
7
|
+
tjs-lang found the hard way now say so, and `strict: true` turns the build's warnings into
|
|
8
|
+
failures. Nothing to change on upgrade: `markdownPages` is on by default and adds files only;
|
|
9
|
+
`strict` is off by default. `dist/iife.js` grows by 0.3 kB gzip (483.7 → 484.0 kB).
|
|
10
|
+
|
|
11
|
+
### Every doc page also ships as markdown
|
|
12
|
+
|
|
13
|
+
The site build now writes each page's markdown source beside its HTML, at
|
|
14
|
+
`/{slug}/index.md`, links it from the page with
|
|
15
|
+
`<link rel="alternate" type="text/markdown" href="index.md">`, and says so in `llms.txt`.
|
|
16
|
+
It is for readers that want the text and not the page: an LLM's URL fetcher, a script, `curl`.
|
|
17
|
+
Prompted by one such fetcher reporting "no `<body>`" for a page whose whole article was in the
|
|
18
|
+
static HTML.
|
|
19
|
+
|
|
20
|
+
- On by default; `markdownPages: false` in the site config ships HTML only. It adds one file
|
|
21
|
+
per page: on this repo's site, 70 files and 0.5 MB, about 3% of the output.
|
|
22
|
+
- The file is the page's source as written, so HTML comments and metadata blocks in a doc
|
|
23
|
+
are served as text. If a page's source holds something its rendered page does not show,
|
|
24
|
+
mark it `noindex` or turn this off.
|
|
25
|
+
- A `noindex` page gets neither the file nor the link.
|
|
26
|
+
- The build now writes `.nojekyll` whenever markdown pages are on, not only under
|
|
27
|
+
`host: 'github-pages'`. Without it, GitHub Pages' Jekyll would render `index.md` over the
|
|
28
|
+
real page. The file is empty and harmless on other hosts.
|
|
29
|
+
|
|
30
|
+
### Misconfigurations the build and the page used to accept in silence (#210)
|
|
31
|
+
|
|
32
|
+
Two reported by tjs-lang, whose site ran every example against the wrong tjs-lang for weeks
|
|
33
|
+
without being told.
|
|
34
|
+
|
|
35
|
+
- **The same-origin tjs-lang copy was skipped without a word.** When `tjs-lang/browser`
|
|
36
|
+
cannot be resolved, nothing is written to `/tjs/`, and `tjs`/`ts` examples load tjs-lang
|
|
37
|
+
from a CDN at the version tosijs-ui pins. The build now warns when the corpus has live
|
|
38
|
+
`tjs` or `ts` examples and that happens. A corpus with none stays quiet.
|
|
39
|
+
- **A custom `bundleEntry` silently dropped what examples import.** It replaces the bundle
|
|
40
|
+
that set the `xinjs`/`xinjsui` globals, so `import { elements } from 'tosijs'` in an example
|
|
41
|
+
failed with "Cannot destructure property 'elements' of 'tosijs'". Examples now get `tosijs`
|
|
42
|
+
from the doc system's own copy. `tosijs-ui` still has to be supplied by the entry (importing
|
|
43
|
+
it here would put every component in every doc site's bundle), and an example that imports
|
|
44
|
+
it without one now fails with a message naming the module and the two lines that fix it,
|
|
45
|
+
whether it is met in the example or its tests. The cost of the
|
|
46
|
+
`tosijs` fallback is 0.3 kB gzip on this site's own bundle.
|
|
47
|
+
|
|
48
|
+
### `strict: true` makes the build fail on what it otherwise warns about
|
|
49
|
+
|
|
50
|
+
A new site-config option, default `false`. With it, `buildSite` throws
|
|
51
|
+
`SiteMisconfiguredError` when `bundleEntry` does not register `<tosi-doc-system>`, when the
|
|
52
|
+
corpus has executable fences and `bundleEntry` does not register `<tosi-example>`, or when
|
|
53
|
+
the tjs-lang copy above is skipped on a corpus that needs it. Warning stays the default
|
|
54
|
+
because each is a legitimate thing to do on purpose; this is for a project that never does.
|
|
55
|
+
Asked for by tosijs-product, whose inert-bundle blocker reached a release candidate under a
|
|
56
|
+
green build. `SiteMisconfiguredError` is exported from `tosijs-ui/site`, so a caller can tell a
|
|
57
|
+
refused misconfiguration from a crash.
|
|
58
|
+
|
|
59
|
+
Whether a corpus "has executable fences" is now decided by the same rule the page uses, under
|
|
60
|
+
the site's `liveExamples` policy. The build's warning used a narrower pattern of its own: it
|
|
61
|
+
counted a bare `js` fence on an opt-in site, and missed `js:iframe`, `html`, `css`, `~~~`
|
|
62
|
+
fences and registered dialects. `llms.txt`'s note about live examples follows the same rule.
|
|
63
|
+
|
|
64
|
+
### Found by the pre-tag review, fixed before release
|
|
65
|
+
|
|
66
|
+
None of these ever shipped; they are listed so the notes account for every fix in the release.
|
|
67
|
+
|
|
68
|
+
- `liveFenceLanguages` found no fences in a CRLF corpus and disagreed with the renderer on
|
|
69
|
+
blockquoted, indented and commented-out fences, because it scanned lines. It now reads
|
|
70
|
+
marked's tokens through `collectCodeTokens`, shared with check-examples.
|
|
71
|
+
- The missing-module error covered the example only; tests and inline tjs tests still got the
|
|
72
|
+
destructuring error. They now go through `rewriteContextImports`, and the check matches a
|
|
73
|
+
real import statement, not a commented-out one.
|
|
74
|
+
- The Console REPL keeps plain `rewriteImports`: the checking variant threw outside the `try`
|
|
75
|
+
that prints REPL errors, so a line importing an unsupplied module printed nothing.
|
|
76
|
+
- A failed copy of the tjs-lang bundles shared a `catch` with resolution and would have been
|
|
77
|
+
reported as "could not be resolved"; only resolution is caught now.
|
|
78
|
+
|
|
79
|
+
### `<meta charset>` is back in the first 1024 bytes of every page
|
|
80
|
+
|
|
81
|
+
Two explanatory HTML comments at the top of each generated page's `<head>` had pushed the
|
|
82
|
+
charset declaration to about byte 1550; the HTML spec wants it within the first 1024. The
|
|
83
|
+
comments now live in the generator's source, and charset and viewport lead the head. Every
|
|
84
|
+
page is about 1.4 kB smaller.
|
|
85
|
+
|
|
86
|
+
## 1.16.6
|
|
87
|
+
|
|
88
|
+
A one-fix patch: a link to the page you are already on no longer adds a history entry that Back
|
|
89
|
+
has to step through (#174). No API change.
|
|
90
|
+
|
|
91
|
+
### Clicking a link to the page you are on no longer costs an extra Back (#174)
|
|
92
|
+
|
|
93
|
+
A doc link pointing at the document already showing pushed a duplicate history entry, so the
|
|
94
|
+
next Back press left the page where it was. It now replaces the entry, as a native same-URL
|
|
95
|
+
navigation does. "The same page" means the same document: `/x`, `/x/` and `/x/index.html`, or
|
|
96
|
+
`?x` with extra query parameters, all count. A URL with a hash still pushes, so Back from
|
|
97
|
+
`/x/` returns to `/x/#example`. The `router`'s `navigate()` had the same defect and gets the
|
|
98
|
+
same rule: navigating to the current path (with no hash) replaces.
|
|
99
|
+
|
|
100
|
+
The double push #174 measured on every link (ensemble on 1.14.1) does not reproduce on 1.16.4
|
|
101
|
+
with the same direct import: one click, one entry, and Back and Forward both work, in Chromium
|
|
102
|
+
and WebKit. The documented consumer entry point is `import 'tosijs-ui/doc-browser'`, which
|
|
103
|
+
registers `<tosi-doc-system>` since 1.14.2. The deep `tosijs-ui/doc-system/doc-system.js`
|
|
104
|
+
import loads the same module.
|
|
105
|
+
|
|
3
106
|
## 1.16.5
|
|
4
107
|
|
|
5
108
|
A patch, mostly found by using the doc site on an iPhone: menus that stay on screen as Safari's
|
package/dist/doc-browser.js
CHANGED
|
@@ -1027,7 +1027,19 @@ export function createDocBrowser(options) {
|
|
|
1027
1027
|
}
|
|
1028
1028
|
else {
|
|
1029
1029
|
const href = hrefFor(filename);
|
|
1030
|
-
|
|
1030
|
+
/*
|
|
1031
|
+
A link to the document already showing REPLACES the entry rather than pushing one, as a
|
|
1032
|
+
native same-URL navigation does. Pushing left a duplicate the reader had to Back
|
|
1033
|
+
through, and Back then visibly did nothing — the symptom #174 reported.
|
|
1034
|
+
|
|
1035
|
+
"Same" is the DOCUMENT, the identity the popstate guard uses, not the raw path: `/x`,
|
|
1036
|
+
`/x/` and `/x/index.html`, or `?x` and `?x&_testMode=1`, are one page. A URL carrying a
|
|
1037
|
+
hash still pushes, because leaving `/x/#example` for `/x/` is a navigation the reader
|
|
1038
|
+
may want to come Back from.
|
|
1039
|
+
*/
|
|
1040
|
+
const same = filename === String(app.currentDoc.filename) &&
|
|
1041
|
+
document.location.hash === '';
|
|
1042
|
+
window.history[same ? 'replaceState' : 'pushState']({ href }, '', href);
|
|
1031
1043
|
}
|
|
1032
1044
|
navigateTo(filename);
|
|
1033
1045
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { ElementCreator } from 'tosijs';
|
|
2
|
+
import * as tosijsModule from 'tosijs';
|
|
2
3
|
import { Doc } from '../doc-browser.js';
|
|
3
4
|
import './css-var-editor.js';
|
|
4
5
|
export declare const CORPUS_ATTEMPTS = 3;
|
|
5
6
|
export declare function fetchCorpus(url: string): Promise<Doc[]>;
|
|
6
|
-
declare const TosiDocSystem_base:
|
|
7
|
+
declare const TosiDocSystem_base: tosijsModule.WithAttributes<{
|
|
7
8
|
docs: string;
|
|
8
9
|
config: string;
|
|
9
10
|
localized: string;
|
|
@@ -57,6 +57,7 @@ A self-contained, controllable embed (e.g. docs in a floating panel):
|
|
|
57
57
|
*/
|
|
58
58
|
/*{ "parent": "Appendices" }*/
|
|
59
59
|
import { StyleSheet, elements, tosi, vars, withAttributes, } from 'tosijs';
|
|
60
|
+
import * as tosijsModule from 'tosijs';
|
|
60
61
|
import { createDocBrowser, } from '../doc-browser.js';
|
|
61
62
|
import { buildSlugMap, legacyQueryPath } from './routing.js';
|
|
62
63
|
import { buildBookHtml, slugify } from './book-html.js';
|
|
@@ -527,8 +528,19 @@ export class TosiDocSystem extends withAttributes({
|
|
|
527
528
|
this.route = slug;
|
|
528
529
|
}
|
|
529
530
|
: undefined,
|
|
531
|
+
/*
|
|
532
|
+
The iife sets `xinjs` / `xinjsui`; a site with its own `bundleEntry` has neither, and
|
|
533
|
+
examples there used to get `undefined` for both with no word said (#210 item 4).
|
|
534
|
+
|
|
535
|
+
`tosijs` falls back to the copy this module already imports — the same instance the
|
|
536
|
+
page's components use, which is the one an example should share state with.
|
|
537
|
+
`tosijs-ui` has no such fallback on purpose: importing the barrel here would put every
|
|
538
|
+
component in every doc site's bundle (#133). A site that wants it in examples supplies
|
|
539
|
+
it, and an example that imports it without one now fails with a sentence saying so
|
|
540
|
+
(`assertContextProvided`).
|
|
541
|
+
*/
|
|
530
542
|
context: this.context || {
|
|
531
|
-
tosijs: globalThis.xinjs,
|
|
543
|
+
tosijs: globalThis.xinjs ?? tosijsModule,
|
|
532
544
|
'tosijs-ui': globalThis.xinjsui,
|
|
533
545
|
},
|
|
534
546
|
projectName: config.projectName,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ExamplePolicy } from '../example-policy.js';
|
|
2
|
+
/** Thrown by `misconfigured` under `strict`, so a caller can tell it from a crash. */
|
|
3
|
+
export declare class SiteMisconfiguredError extends Error {
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Report a misconfiguration: a warning by default, a thrown `SiteMisconfiguredError` (which
|
|
8
|
+
* fails `buildSite`) when the site config sets `strict: true`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function misconfigured(strict: boolean | undefined, message: string): void;
|
|
11
|
+
/**
|
|
12
|
+
* The languages of the corpus's LIVE fences, under the site's example policy.
|
|
13
|
+
*
|
|
14
|
+
* Code blocks come from marked (`collectCodeTokens`), the parser the pages are rendered with,
|
|
15
|
+
* and liveness from `isLiveFence`, THE predicate. Neither is re-derived here: a second copy of
|
|
16
|
+
* the liveness rule is what disagreed when `:static` shipped, a regex of its own in the bundle
|
|
17
|
+
* guard is what the 1.16.7 review caught deciding a `strict` build, and the line scanner that
|
|
18
|
+
* first replaced it missed CRLF files and blockquoted fences. Every build-side "does this
|
|
19
|
+
* corpus have live examples?" asks here.
|
|
20
|
+
*/
|
|
21
|
+
export declare function liveFenceLanguages(docs: Array<{
|
|
22
|
+
text?: string;
|
|
23
|
+
}>, policy?: ExamplePolicy): Set<string>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Things a site build can see are wrong and used to mention quietly, or not at all.
|
|
3
|
+
|
|
4
|
+
A build that exits 0 with a warning in scrollback is how tosijs-product's 0.8.0 blocker reached
|
|
5
|
+
a release candidate (#145, board #2543), and how tjs.tosijs.net ran every example against the
|
|
6
|
+
wrong tjs-lang for weeks without anyone being told (#210). Warning stays the default, because
|
|
7
|
+
each of these is a legitimate thing to do on purpose. `strict: true` in the site config is how a
|
|
8
|
+
project that never does them on purpose says so, and then the build fails.
|
|
9
|
+
*/
|
|
10
|
+
import { isLiveFence } from '../example-policy.js';
|
|
11
|
+
import { collectCodeTokens } from './code-fences.js';
|
|
12
|
+
/** Thrown by `misconfigured` under `strict`, so a caller can tell it from a crash. */
|
|
13
|
+
export class SiteMisconfiguredError extends Error {
|
|
14
|
+
name = 'SiteMisconfiguredError';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Report a misconfiguration: a warning by default, a thrown `SiteMisconfiguredError` (which
|
|
18
|
+
* fails `buildSite`) when the site config sets `strict: true`.
|
|
19
|
+
*/
|
|
20
|
+
export function misconfigured(strict, message) {
|
|
21
|
+
if (strict) {
|
|
22
|
+
throw new SiteMisconfiguredError(`${message}\n This fails the build because the site config sets \`strict: true\`.\n`);
|
|
23
|
+
}
|
|
24
|
+
console.warn(message);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The languages of the corpus's LIVE fences, under the site's example policy.
|
|
28
|
+
*
|
|
29
|
+
* Code blocks come from marked (`collectCodeTokens`), the parser the pages are rendered with,
|
|
30
|
+
* and liveness from `isLiveFence`, THE predicate. Neither is re-derived here: a second copy of
|
|
31
|
+
* the liveness rule is what disagreed when `:static` shipped, a regex of its own in the bundle
|
|
32
|
+
* guard is what the 1.16.7 review caught deciding a `strict` build, and the line scanner that
|
|
33
|
+
* first replaced it missed CRLF files and blockquoted fences. Every build-side "does this
|
|
34
|
+
* corpus have live examples?" asks here.
|
|
35
|
+
*/
|
|
36
|
+
export function liveFenceLanguages(docs, policy = 'auto') {
|
|
37
|
+
const langs = new Set();
|
|
38
|
+
for (const doc of docs) {
|
|
39
|
+
for (const { lang, mode } of collectCodeTokens(doc.text ?? '')) {
|
|
40
|
+
if (lang && isLiveFence(lang, mode, policy))
|
|
41
|
+
langs.add(lang);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return langs;
|
|
45
|
+
}
|
|
@@ -21,9 +21,9 @@ executed, so they're skipped.
|
|
|
21
21
|
|
|
22
22
|
Build-time only (bun). Never import from browser code.
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import { collectCodeTokens } from './code-fences.js';
|
|
25
25
|
import { rewriteImports, AsyncFunction, loadTransform, transformAvailable, UnsupportedImportError, } from '../../live-example/code-transform.js';
|
|
26
|
-
import { isLiveFence
|
|
26
|
+
import { isLiveFence } from '../example-policy.js';
|
|
27
27
|
// The default live-example context (matches the IIFE globals the pages provide).
|
|
28
28
|
// A project that sets a custom `context` on its <tosi-doc-system> can pass its
|
|
29
29
|
// own keys; these are the tosijs-ui defaults.
|
|
@@ -54,36 +54,6 @@ const EXECUTABLE = new Set(['js', 'tjs', 'ts', 'test']);
|
|
|
54
54
|
// scope is the difference between "once" and "once per rebuild". Lazily created, so a
|
|
55
55
|
// corpus with no `ts` examples never makes one at all.
|
|
56
56
|
let tsTranspiler;
|
|
57
|
-
/*
|
|
58
|
-
Fence info is parsed by `parseFenceInfo`, the one copy of that grammar. This file had its own
|
|
59
|
-
`dialectOf`/`modeOf` pair; once fences could carry JSON options (#184) `modeOf` would have read
|
|
60
|
-
`{"debug":true}` as the mode `true`.
|
|
61
|
-
*/
|
|
62
|
-
function collectCodeTokens(text) {
|
|
63
|
-
const out = [];
|
|
64
|
-
const walk = (tokens) => {
|
|
65
|
-
for (const t of tokens) {
|
|
66
|
-
/*
|
|
67
|
-
Keep the MODE. `dialectOf` reduced `js:static` to `js`, so the checker could not tell a
|
|
68
|
-
block that will never run from one that will — and hard-failed a build over the syntax
|
|
69
|
-
of illustrative code, advising the author to retag it as `typescript`, which is exactly
|
|
70
|
-
the mislabelling this release exists to abolish (review major M2).
|
|
71
|
-
*/
|
|
72
|
-
if (t.type === 'code')
|
|
73
|
-
out.push({
|
|
74
|
-
lang: parseFenceInfo(t.lang ?? '').lang,
|
|
75
|
-
text: t.text,
|
|
76
|
-
mode: parseFenceInfo(t.lang ?? '').mode,
|
|
77
|
-
});
|
|
78
|
-
if (Array.isArray(t.tokens))
|
|
79
|
-
walk(t.tokens);
|
|
80
|
-
if (Array.isArray(t.items))
|
|
81
|
-
walk(t.items); // list items
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
walk(marked.lexer(text));
|
|
85
|
-
return out;
|
|
86
|
-
}
|
|
87
57
|
/**
|
|
88
58
|
* Transpile-check every executable block in the corpus. Returns the problems and
|
|
89
59
|
* the `tjs` bakes (which it computes anyway while checking — no double transpile).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The build's ONE way to enumerate the code blocks in a doc: marked's own lexer.
|
|
3
|
+
|
|
4
|
+
`check-examples` had this function; `build-warnings` then grew a hand-written line scanner for
|
|
5
|
+
the same job, and the 1.16.7 re-review found the scanner disagreeing with marked on CRLF files,
|
|
6
|
+
blockquoted fences and fences inside HTML blocks. A scanner can only ever approximate the
|
|
7
|
+
parser the pages are rendered with, so nothing here scans: both callers ask marked.
|
|
8
|
+
*/
|
|
9
|
+
import { marked } from 'marked';
|
|
10
|
+
import { parseFenceInfo } from '../example-policy.js';
|
|
11
|
+
/*
|
|
12
|
+
Fence info is parsed by `parseFenceInfo`, the one copy of that grammar. This file had its own
|
|
13
|
+
`dialectOf`/`modeOf` pair; once fences could carry JSON options (#184) `modeOf` would have read
|
|
14
|
+
`{"debug":true}` as the mode `true`.
|
|
15
|
+
*/
|
|
16
|
+
export function collectCodeTokens(text) {
|
|
17
|
+
const out = [];
|
|
18
|
+
const walk = (tokens) => {
|
|
19
|
+
for (const t of tokens) {
|
|
20
|
+
/*
|
|
21
|
+
Keep the MODE. `dialectOf` reduced `js:static` to `js`, so the checker could not tell a
|
|
22
|
+
block that will never run from one that will — and hard-failed a build over the syntax
|
|
23
|
+
of illustrative code, advising the author to retag it as `typescript`, which is exactly
|
|
24
|
+
the mislabelling this release exists to abolish (review major M2).
|
|
25
|
+
*/
|
|
26
|
+
if (t.type === 'code')
|
|
27
|
+
out.push({
|
|
28
|
+
lang: parseFenceInfo(t.lang ?? '').lang,
|
|
29
|
+
text: t.text,
|
|
30
|
+
mode: parseFenceInfo(t.lang ?? '').mode,
|
|
31
|
+
});
|
|
32
|
+
if (Array.isArray(t.tokens))
|
|
33
|
+
walk(t.tokens);
|
|
34
|
+
if (Array.isArray(t.items))
|
|
35
|
+
walk(t.items); // list items
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
walk(marked.lexer(text));
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
@@ -75,6 +75,13 @@ export interface GenerateSiteConfig {
|
|
|
75
75
|
* self-contained-examples-plan.md.
|
|
76
76
|
*/
|
|
77
77
|
bakes?: Map<string, ExampleBakes>;
|
|
78
|
+
/**
|
|
79
|
+
* Write each page's markdown beside its HTML (`/slug/index.md`) and point at it with
|
|
80
|
+
* `<link rel="alternate" type="text/markdown">`. Default true. For readers that want the
|
|
81
|
+
* text and not the page: an LLM's URL fetcher, a script, `curl`. `noindex` pages get
|
|
82
|
+
* neither, the same rule the sitemap applies.
|
|
83
|
+
*/
|
|
84
|
+
markdownPages?: boolean;
|
|
78
85
|
/** URL of the burned-in theme stylesheet (written by ./generate-css.ts) */
|
|
79
86
|
stylesUrl?: string;
|
|
80
87
|
/** extra lines injected into every <head> (favicon, analytics, etc.) */
|
|
@@ -163,8 +163,6 @@ async function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
163
163
|
})
|
|
164
164
|
: '';
|
|
165
165
|
const head = [
|
|
166
|
-
' <meta charset="utf-8" />',
|
|
167
|
-
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
168
166
|
// Render-blocking @import of web fonts lives in the stylesheet; warm the connection.
|
|
169
167
|
' <link rel="preconnect" href="https://fonts.googleapis.com" />',
|
|
170
168
|
' <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />',
|
|
@@ -179,6 +177,10 @@ async function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
179
177
|
: '',
|
|
180
178
|
doc.noindex ? ' <meta name="robots" content="noindex, follow" />' : '',
|
|
181
179
|
baseUrl ? ` <link rel="canonical" href="${escapeAttr(canonical)}" />` : '',
|
|
180
|
+
// Relative to the page's own directory, so it holds at any mount (see relativeUrl).
|
|
181
|
+
hasMarkdownPage(doc, config)
|
|
182
|
+
? ` <link rel="alternate" type="text/markdown" href="index.md" title="${escapeAttr(title)} (markdown)" />`
|
|
183
|
+
: '',
|
|
182
184
|
' <meta property="og:type" content="article" />',
|
|
183
185
|
projectName
|
|
184
186
|
? ` <meta property="og:site_name" content="${escapeAttr(projectName)}" />`
|
|
@@ -203,23 +205,31 @@ async function pageHtml(doc, config, slugMap, configAttr) {
|
|
|
203
205
|
]
|
|
204
206
|
.filter(Boolean)
|
|
205
207
|
.join('\n');
|
|
208
|
+
/*
|
|
209
|
+
Two notes that used to ship as HTML comments at the top of every page's <head>. They are for
|
|
210
|
+
whoever edits this template, not for readers, and they pushed `<meta charset>` to byte ~1550
|
|
211
|
+
— the HTML spec wants it within the first 1024, and a byte-limited or strict fetcher may give
|
|
212
|
+
up before it gets there. So charset and viewport now lead, and the notes live here.
|
|
213
|
+
|
|
214
|
+
1. The body is NOT hidden until hydration. It used to be (body opacity 0 + a 4s safety-net
|
|
215
|
+
timeout), because an undefined custom element is display:inline, so the pre-rendered page
|
|
216
|
+
stacked as bare text and hydration reflowed the whole thing. The cost was a blank screen
|
|
217
|
+
for as long as the bundle took — ~4.5s on a cheap phone, for content already in the HTML.
|
|
218
|
+
The tosi-doc-system:not(:defined) rules in the stylesheet now lay the static page out as
|
|
219
|
+
though the chrome were there, so hydration only ADDS the chrome and nothing moves. Paint
|
|
220
|
+
immediately; don't hide readable content.
|
|
221
|
+
|
|
222
|
+
2. The inline script applies the theme BEFORE first paint. Now that we paint the static page
|
|
223
|
+
rather than hiding it, a dark-mode reader would otherwise get a flash of the light theme
|
|
224
|
+
until the bundle lands and sets body.darkmode. CSS alone can't do this: an explicit theme
|
|
225
|
+
choice lives in localStorage. Mirrors applyThemePrefs() (doc-system.ts) — keep the two in
|
|
226
|
+
step. Fails safe: any error leaves the light default.
|
|
227
|
+
*/
|
|
206
228
|
return `<!DOCTYPE html>
|
|
207
229
|
<html lang="${escapeAttr(lang)}">
|
|
208
230
|
<head>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
display:inline, so the pre-rendered page stacked as bare text and hydration
|
|
212
|
-
reflowed the whole thing. The cost was a blank screen for as long as the
|
|
213
|
-
bundle took — ~4.5s on a cheap phone, for content already in the HTML. The
|
|
214
|
-
tosi-doc-system:not(:defined) rules in the stylesheet now lay the static page
|
|
215
|
-
out as though the chrome were there, so hydration only ADDS the chrome and
|
|
216
|
-
nothing moves. Paint immediately; don't hide readable content. -->
|
|
217
|
-
<!-- Theme, applied BEFORE first paint. Now that we paint the static page rather
|
|
218
|
-
than hiding it, a dark-mode reader would otherwise get a flash of the light
|
|
219
|
-
theme until the bundle lands and sets body.darkmode. CSS alone can't do this:
|
|
220
|
-
an explicit theme choice lives in localStorage. Mirrors applyThemePrefs()
|
|
221
|
-
(doc-system.ts) — keep the two in step. Fails safe: any error leaves the
|
|
222
|
-
light default, exactly as before. -->
|
|
231
|
+
<meta charset="utf-8" />
|
|
232
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
223
233
|
<script>
|
|
224
234
|
try {
|
|
225
235
|
var p = JSON.parse(localStorage.getItem('tosi-doc-system-prefs') || '{}')
|
|
@@ -263,6 +273,10 @@ Only same-origin: `scriptUrl` may legitimately point at a CDN, and appending a q
|
|
|
263
273
|
someone else's URL can miss their cache key or, worse, be rejected. A URL that already carries
|
|
264
274
|
a query is left alone — the caller has said something deliberate about it.
|
|
265
275
|
*/
|
|
276
|
+
/** A page gets a markdown copy unless the site opted out or the page is `noindex`. */
|
|
277
|
+
function hasMarkdownPage(doc, config) {
|
|
278
|
+
return config.markdownPages !== false && !doc.noindex;
|
|
279
|
+
}
|
|
266
280
|
export function withStamp(url, stamp) {
|
|
267
281
|
if (!stamp)
|
|
268
282
|
return url;
|
|
@@ -297,6 +311,10 @@ export async function generateSite(config) {
|
|
|
297
311
|
const slug = slugMap[doc.filename];
|
|
298
312
|
const dir = slug === '' ? outputDir : `${outputDir}/${slug}`;
|
|
299
313
|
await Bun.write(`${dir}/index.html`, await pageHtml(doc, config, slugMap, configAttr));
|
|
314
|
+
// The page's own source, for readers that want text rather than a page.
|
|
315
|
+
if (hasMarkdownPage(doc, config)) {
|
|
316
|
+
await Bun.write(`${dir}/index.md`, doc.text.trimEnd() + '\n');
|
|
317
|
+
}
|
|
300
318
|
count += 1;
|
|
301
319
|
}
|
|
302
320
|
// The corpus the component fetches for nav + client-side rendering of other pages.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { defineSiteConfig } from './site-config.js';
|
|
2
2
|
export type { SiteConfig, SiteHost, LibraryBuildContext, } from './site-config.js';
|
|
3
3
|
export { buildSite } from './orchestrator.js';
|
|
4
|
+
export { SiteMisconfiguredError } from './build-warnings.js';
|
|
4
5
|
export { devServer } from './dev-server.js';
|
|
5
6
|
export { auditDependencies, reportAudit, resolveAuditMode, } from './audit-guard.js';
|
|
6
7
|
export type { AuditConfig, AuditGate, AuditResult, AuditSeverity, AuditMode, AuditAdvisory, } from './audit-guard.js';
|
|
@@ -5,6 +5,9 @@ browser code. See ./README is at ../doc-site-system.md and the repo docs.
|
|
|
5
5
|
*/
|
|
6
6
|
export { defineSiteConfig } from './site-config.js';
|
|
7
7
|
export { buildSite } from './orchestrator.js';
|
|
8
|
+
// Thrown by `buildSite` under `strict: true` — exported so a caller can tell a refused
|
|
9
|
+
// misconfiguration from a crash (`catch (e) { if (e instanceof SiteMisconfiguredError) … }`).
|
|
10
|
+
export { SiteMisconfiguredError } from './build-warnings.js';
|
|
8
11
|
export { devServer } from './dev-server.js';
|
|
9
12
|
export { auditDependencies, reportAudit, resolveAuditMode, } from './audit-guard.js';
|
|
10
13
|
export { openDevBrowser, buildOpenPlan } from './open-browser.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExamplePolicy } from '../example-policy.js';
|
|
1
2
|
/** A doc as it appears in the extracted corpus (docs.json). */
|
|
2
3
|
interface CorpusDoc {
|
|
3
4
|
title?: string;
|
|
@@ -39,6 +40,14 @@ export interface LlmsTxtMeta {
|
|
|
39
40
|
* for exactly the projects that enabled the MORE capable mode.
|
|
40
41
|
*/
|
|
41
42
|
haltijaDev?: boolean | 'tunnel';
|
|
43
|
+
/**
|
|
44
|
+
* Mirrors `SiteConfig.markdownPages` (default true): the build writes `index.md` beside
|
|
45
|
+
* every page, so the index says so. Pass false when the site ships HTML only — pointing a
|
|
46
|
+
* reader at a file that is not there costs more than not mentioning it.
|
|
47
|
+
*/
|
|
48
|
+
markdownPages?: boolean;
|
|
49
|
+
/** Mirrors `SiteConfig.liveExamples`: which fences are live decides the examples note. */
|
|
50
|
+
liveExamples?: ExamplePolicy;
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Build entries from the extracted corpus — every doc that was actually
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { buildSlugMap, pathForSlug, withBase } from '../routing.js';
|
|
4
|
+
import { liveFenceLanguages } from './build-warnings.js';
|
|
4
5
|
const SRC = 'src';
|
|
5
6
|
const DIST = 'dist';
|
|
6
7
|
function extractTitle(text) {
|
|
@@ -113,8 +114,9 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
|
|
|
113
114
|
: entriesFromSrcScan();
|
|
114
115
|
// Only orient agents about the live-example execution model when the corpus
|
|
115
116
|
// actually has live examples (a book / pure-docs site has none).
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
// THE shared rule (build-warnings.ts), under the site's policy — this was a regex of its
|
|
118
|
+
// own, which told an opt-in prose site about live examples it does not have.
|
|
119
|
+
const hasLiveExamples = !!corpus && liveFenceLanguages(corpus, meta.liveExamples).size > 0;
|
|
118
120
|
const liveExampleNote = hasLiveExamples
|
|
119
121
|
? [
|
|
120
122
|
'Live examples run inline on the shared page by default (not sandboxed):',
|
|
@@ -142,6 +144,15 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
|
|
|
142
144
|
'',
|
|
143
145
|
]
|
|
144
146
|
: [];
|
|
147
|
+
// Only for a corpus-driven index: the legacy scan links to dist/ files, not pages.
|
|
148
|
+
const markdownNote = corpus && meta.markdownPages !== false
|
|
149
|
+
? [
|
|
150
|
+
'Pages below are also available as plain markdown: append `index.md` to the URL',
|
|
151
|
+
'(for example `/carousel/index.md`). Prefer that if you only need the text. A page',
|
|
152
|
+
'marked noindex has no markdown copy.',
|
|
153
|
+
'',
|
|
154
|
+
]
|
|
155
|
+
: [];
|
|
145
156
|
const name = meta.name ?? pkg.name ?? '';
|
|
146
157
|
const description = meta.description ?? pkg.description ?? '';
|
|
147
158
|
const links = [];
|
|
@@ -165,6 +176,7 @@ export function generateLlmsTxt(outputPath, meta = {}, corpus) {
|
|
|
165
176
|
'',
|
|
166
177
|
'Full documentation, with live code examples, is at the links below.',
|
|
167
178
|
'',
|
|
179
|
+
...markdownNote,
|
|
168
180
|
...liveExampleNote,
|
|
169
181
|
...haltijaNote,
|
|
170
182
|
'## Pages',
|