From 153e1382de5b472279401c678efe825d3a4e682d Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Sun, 11 Oct 2026 15:06:42 +0200 Subject: [PATCH 01/33] Build-tools documentation: editorial pass over docs/Documentation/ (about 13% shorter): long paragraphs split, rationale and history a contributor does not act on cut, plainer register; Tools-Toolchain-Tests says Tools.md owns the gate lists; a stray tab in Tools-IDE's %TEMP%\tb-ports --- docs/Documentation/Authoring.md | 849 +++++++++----------- docs/Documentation/Book-Configuration.md | 96 ++- docs/Documentation/Builder.md | 358 ++++----- docs/Documentation/Building.md | 516 ++++-------- docs/Documentation/Extending.md | 363 ++++----- docs/Documentation/Fixes-PDFLib.md | 102 +-- docs/Documentation/Fixes-PagedJS.md | 84 +- docs/Documentation/Fixes.md | 4 +- docs/Documentation/PDF-Generation.md | 135 ++-- docs/Documentation/Permanent-Links.md | 32 +- docs/Documentation/Pipeline-Stages.md | 384 ++++----- docs/Documentation/Tools-Build.md | 124 ++- docs/Documentation/Tools-Compiler.md | 300 +++---- docs/Documentation/Tools-Defects.md | 116 +-- docs/Documentation/Tools-IDE.md | 340 +++----- docs/Documentation/Tools-Samples.md | 179 ++--- docs/Documentation/Tools-Site-Checks.md | 119 ++- docs/Documentation/Tools-Toolchain-Tests.md | 258 ++++-- docs/Documentation/Tools.md | 112 +-- docs/Documentation/Wisdom.md | 130 +-- docs/Documentation/index.md | 16 +- 21 files changed, 2215 insertions(+), 2402 deletions(-) diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 8f675ffc6..9e39279a6 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -8,9 +8,9 @@ permalink: /Documentation/Development/Authoring # Authoring Pages {: .no_toc } -The starting point for writing or editing a documentation page. It covers the anatomy of a page --- frontmatter, headings, formatting, prose style, attribution, and links --- so a first contribution matches the rest of the site without a second editing pass. For the build-and-preview workflow that turns markdown into HTML, see [Building and Deployment](Building); for the stable URL contract a page's `permalink` must honour, see [Permanent Links](Permanent-Links). +The starting point for writing or editing a documentation page: frontmatter, headings, formatting, prose style, attribution and links. To build and preview, see [Building and Deployment](Building). For the URL contract a page's `permalink` must honour, see [Permanent Links](Permanent-Links). -The surest guide is an existing page of the same kind. Open the nearest neighbour to what you are writing --- a sibling in the same sidebar folder --- and match its shape. The rules below are the conventions those pages already follow. Two worked reference pages to imitate: [`Const.md`](https://github.com/twinbasic/documentation/blob/main/docs/Reference/Core/Const.md) and [`Dim.md`](https://github.com/twinbasic/documentation/blob/main/docs/Reference/Core/Dim.md). +The best guide is an existing page of the same kind. Open a sibling in the same sidebar folder and match its shape. Two worked reference pages to imitate: [`Const.md`](https://github.com/twinbasic/documentation/blob/main/docs/Reference/Core/Const.md) and [`Dim.md`](https://github.com/twinbasic/documentation/blob/main/docs/Reference/Core/Dim.md). * TOC goes here {:toc} @@ -24,20 +24,17 @@ The documentation is grouped by area, each a folder under `docs/`: - **Features** --- articles on individual twinBASIC language features. - **LLVM** --- compiling with the LLVM back end: turning it on, its options, and its current limitations. - **IDE** --- the IDE user guide. -- **Documentation** --- this section: the toolchain reference and the authoring guide you are reading now. +- **Documentation** --- this section: the toolchain reference and this authoring guide. -Where a page sits in the source tree matters less than its `permalink`, which fixes the page's URL independently of the folder the file lives in. Get the permalink right (next section) and the file can move without breaking a single link. +A page's `permalink` fixes its URL independently of the folder the file lives in. With the right permalink (next section) the file can move without breaking a link. ## What may live in docs/ Anything under `docs/` that is not a page is copied into the published site unchanged, at a URL matching its path. That is how images, fonts and stylesheets -get published, and it used to be how a stray file got published too --- a `.bak` -left beside a page, a `.twin` sample, a scratch `.md`, a `Thumbs.db` the shell -wrote without being asked. All of them went to a public URL on a green build. - -The build now refuses anything that is not a publishable type, names the file, -and stops before writing: +get published. The build refuses anything that is not a publishable type (a +`.bak` beside a page, a `.twin` sample, a scratch `.md`, a `Thumbs.db`), names +the file, and stops before writing: ``` 1 file would be published from docs but is not a publishable type: @@ -49,76 +46,56 @@ and stops before writing: The types a source file may use: `.html`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.woff2`, `.css`, `.js`, `.txt`, and `CNAME` at the site root. Pages -(`.md` with frontmatter) are rendered rather than copied, so they are not -affected. - -Three things follow. +(`.md` with frontmatter) are rendered rather than copied. **A file you do not want published does not belong under `docs/`,** unless a -pattern in `_config.yml`'s `exclude:` already covers it. Source that sits beside -the pages it produces is excluded that way, at any depth in the tree: `.scss` -stylesheets, `.dot` diagram sources, and the Affinity `.af` screenshot sources -under `_Images/`. So is everything underscore-prefixed at the root of `docs/` ---- `_config.yml` and `_book.yml`, the `_sass/` stylesheet tree, and the build's -own output trees (`_site/`, `_site-offline/`, `_serve/`, `_pdf/` and their -variants). Scratch notes, editor backups and working documents have no such -exemption --- keep them outside the tree. - -That root-level underscore rule is two patterns --- `_*` for the root's own -files, `_*/**` for the contents of its folders --- and where it stops matters to -anyone documenting a COM interface. Both are matched against a -file's path relative to `docs/` under fast-glob semantics, where `*` does not -cross a directory separator, so `_*/**` reaches into a folder only when the -underscore sits at the top level. A `docs/_Config/` is therefore dropped, and -every page in it disappears without a message: excluded files are not missing -files, and the build cannot report on a page it never saw. A -`docs/Reference/Built-In/SomePackage/_Config/` is untouched, and so is an -underscore-prefixed *page* at any depth, such as +pattern in `_config.yml`'s `exclude:` covers it. These are excluded at any depth: +`.scss` stylesheets, `.dot` diagram sources, and the Affinity `.af` screenshot +sources under `_Images/`. So is everything underscore-prefixed at the root of +`docs/` --- `_config.yml` and `_book.yml`, the `_sass/` tree, and the build's +output trees (`_site/`, `_site-offline/`, `_serve/`, `_pdf/` and their +variants). Keep scratch notes, editor backups and working documents outside the +tree. + +The root-level underscore rule is two patterns --- `_*` for the root's own +files, `_*/**` for the contents of its folders. `*` does not cross a directory +separator, so `_*/**` reaches into a folder only when the underscore sits at the +top level. A `docs/_Config/` is therefore dropped, and every page in it +disappears without a message. A `docs/Reference/Built-In/SomePackage/_Config/` +is untouched, and so is an underscore-prefixed *page* at any depth, such as `Reference/Built-In/CustomControls/Framework/_CustomControlContext.md`. -The rule is scoped that narrowly because a wider one did real damage. It was -once `**/_*/**`, which matches an underscore folder anywhere in the tree, and it -swallowed all 37 pages under -`Reference/Built-In/AppGlobalClassObject/_App/` --- the twinBASIC interface is -named `_App`, following the COM convention for a hidden interface, and the -folder is named after the interface. Nothing reported the loss. An interface or -class whose name begins with an underscore is documented exactly like any other, -wherever it sits in the tree; the one placement to avoid is an underscore-named -folder directly under `docs/`. - -**A `.md` the build reports as unpublishable is a file with no frontmatter block -at all.** Either it genuinely has none --- a scratch note, a README --- or -something precedes the opening `---`, which has to be the first line. A blank -line before it is enough. A file in that state is not a broken page: without the -refusal it would be copied out and served as raw markdown, frontmatter keys and -all, which is how one package index page was published for months. - -Two neighbouring faults are *not* this, and knowing which you have saves a -search: - -- **A UTF-8 BOM is handled.** Windows editors add one without asking, and it - used to be exactly this failure --- it sits in front of the `---` and the - parser then reports no frontmatter. The build strips it before parsing, so a - BOM'd page renders normally and never reaches this message. -- **Malformed YAML inside the block reports itself.** It does not fall through - to this check; it aborts with `Failed to parse frontmatter in ` and the - parser's own line and column. - -**If a file genuinely belongs on the published site,** there are two ways to -allow it, and picking the wrong one quietly undoes the allowlist. The question is -whether you are adding a *type* or a *file*. +The rule is narrow because a wider one, `**/_*/**`, once swallowed all 37 pages +under `Reference/Built-In/AppGlobalClassObject/_App/` (the interface is named +`_App`, following the COM convention for a hidden interface). Document an +interface or class whose name begins with an underscore like any other; avoid +only an underscore-named folder directly under `docs/`. + +**A `.md` the build reports as unpublishable has no frontmatter block.** Either +it has none --- a scratch note, a README --- or something precedes the opening +`---`, which must be the first line. A blank line before it is enough. Without +the refusal, the file would be served as raw markdown, frontmatter keys and all. + +Two neighbouring faults are *not* this: + +- **A UTF-8 BOM is handled.** Windows editors add one without asking. The build + strips it before parsing, so a BOM'd page renders normally. +- **Malformed YAML inside the block reports itself.** The build aborts with + `Failed to parse frontmatter in ` and the parser's own line and column. + +**If a file belongs on the published site,** there are two ways to allow it, and +the wrong one quietly undoes the allowlist. Decide whether you are adding a +*type* or a *file*. A **type** is an extension the site will go on using --- a new image format, a new -font format --- where the next file of that kind should publish without anyone -having to think about it. Add the extension to `SOURCE_EXTENSIONS` in +font format --- so the next file of that kind publishes without further thought. +Add the extension to `SOURCE_EXTENSIONS` in [`builder/publish-policy.mjs`](https://github.com/twinbasic/documentation/blob/main/builder/publish-policy.mjs) -in the same commit as the file. That is a deliberate one-line edit, and it is the -point of the rule: the decision gets made once, by the person who knows they are -making it. +in the same commit as the file. A **file** is one particular download or sample whose extension you would not -want blessed everywhere --- a `.py` script, a `.json` data file, a `.zip`. -Declare it in `_config.yml`'s `bundle_extra`, which spells out a source and a +want allowed everywhere --- a `.py` script, a `.json` data file, a `.zip`. +Declare it in `_config.yml`'s `bundle_extra`, which gives a source and a published path and exempts that exact path rather than the extension: ```yaml @@ -129,47 +106,39 @@ bundle_extra: `src` is resolved against `docs/`, so it may point outside the tree --- the two real entries publish the [Import/Export Tool](../../Features/Packages/Import-Export-Tool) -from `scripts/`, where it is maintained and run, instead of requiring a second -copy under `docs/` that would drift from it. `dest` is the path in the built -site, and it is what the page links to. A `src` that does not exist aborts the -build, so a typo there is not silent either. - -Between the two, **prefer `bundle_extra` unless you really are adding a type.** -Widening `SOURCE_EXTENSIONS` to `.json` so that one download can ship makes every -stray `.json` under `docs/` publishable again --- a `docs/secrets.json` would -reach a public URL, which is the whole class of accident the allowlist exists to -refuse, reintroduced by the person fixing a build failure. +from `scripts/`, where it is maintained, with no second copy under `docs/`. +`dest` is the path in the built site, and the page links to it. A `src` that +does not exist aborts the build. + +**Prefer `bundle_extra` unless you are adding a type.** Widening +`SOURCE_EXTENSIONS` to `.json` for one download makes every stray `.json` under +`docs/` publishable again --- a `docs/secrets.json` would reach a public URL. `scripts/check_publish_policy.mjs` asserts that a `bundle_extra` exemption stays pinned to its declared path and never leaks to the extension. -The other half of this --- how the refusal is enforced --- is on the build page. -[What the build refuses to publish](Building#what-the-build-refuses-to-publish) -covers the two inventories the policy sweeps (the source files on disk, before -anything is written, and what each output tree is about to receive), why a -finding there aborts the build outright where a broken link only sets an exit -code, and how that self-test proves the allowlist can still refuse something -rather than having been widened until it refuses nothing. +How the refusal is enforced is in +[What the build refuses to publish](Building#what-the-build-refuses-to-publish). ## Frontmatter and permalinks Every page opens with a YAML frontmatter block. The keys that matter: - **`title`** --- the page's name in the sidebar, breadcrumb, and search results. -- **`parent`** (and sometimes **`grand_parent`**) --- the title of the nav parent. The build aborts if this does not resolve to exactly one page, so a typo is caught at build time rather than silently dropping the page from the sidebar or attaching it under the wrong branch. -- **`permalink`** --- **required on every page.** The page's stable URL, and the contract the IDE help system relies on. The build aborts if it is missing rather than deriving one from the file path: this tree deliberately does not mirror its URLs (`Reference/Built-In/CEF/` publishes at `/tB/Packages/CEF/`), so a derived URL would be wrong, and wrong silently. Reference pages follow a fixed scheme by section: a core keyword is `/tB/Core/`, a library symbol is `/tB/Packages//...`, and so on. [Permanent Links](Permanent-Links) documents the full scheme. +- **`parent`** (and sometimes **`grand_parent`**) --- the title of the nav parent. The build aborts if this does not resolve to exactly one page, so a typo cannot drop the page from the sidebar or attach it to the wrong branch. +- **`permalink`** --- **required on every page.** The page's stable URL, and the contract the IDE help system relies on. The build aborts if it is missing: this tree does not mirror its URLs (`Reference/Built-In/CEF/` publishes at `/tB/Packages/CEF/`), so a URL derived from the file path would be wrong. A core keyword is `/tB/Core/`, a library symbol is `/tB/Packages//...`, and so on; [Permanent Links](Permanent-Links) documents the full scheme. - **`nav_order`** --- optional integer that orders the page among its siblings in the sidebar. - **`has_toc`** --- set to `false` to suppress the automatic list of child pages the template appends to a page that has any; see [Folder-style pages](#folder-style-pages-and-has_toc). Every page that sets it sets it to `false`. -- **`redirect_from`** --- optional. List any earlier URL the page has moved from, so existing links keep resolving. Each entry becomes a small stub page at that URL, so two rules apply and the build aborts naming both files if either is broken: a `redirect_from` entry may not point at a URL some page already publishes at, and no two pages may claim the same one. +- **`redirect_from`** --- optional. List any earlier URL the page has moved from, so existing links keep resolving. Each entry becomes a small stub page at that URL. The build aborts, naming both files, if an entry points at a URL some page already publishes at, or if two pages claim the same one. - **`vba_attribution`** --- set to `true` only on pages adapted from the VBA-Docs source; see [Attribution](#attribution). -- **`nav_exclude`**, **`sitemap: false`**, **`search_exclude: true`** --- optional opt-outs, each from exactly one thing: the sidebar, `sitemap.xml`, and the search index. They are independent; a page that should be unlisted everywhere sets all three. The build's own link check honours the last two, so a page that opts out is not then reported as missing from the index it opted out of. +- **`nav_exclude`**, **`sitemap: false`**, **`search_exclude: true`** --- optional opt-outs from the sidebar, `sitemap.xml` and the search index. They are independent; a page that should be unlisted everywhere sets all three. The link check honours the last two, so an opted-out page is not reported as missing from the index. - **`symbols`** --- optional, and rare. The names a page documents, when its title and first heading cannot say; see [Naming what a page documents](#symbols). - **`index`**, **`index_also`** --- optional. Terms a reader searches for that should find this page, as a book's index lists them; see [Index entries for the site search](#index-entries-for-the-site-search). -A package's `index.md` may also carry **`indexed_from`**, **`exclude_from_docs`** and **`exclude_kinds`**. Those are provenance for the authoring pass: they record which twinBASIC build the package download was indexed against, and what was deliberately left undocumented, so a later re-index can tell a genuine gap from a deliberate omission. The build reads `exclude_from_docs` for one thing only, to leave those names out of the [symbol index](Building#the-symbol-index)'s count of undocumented symbols; none of the three reaches the HTML. **Leave them in place**, and bump `indexed_from` in the same commit if you re-index a package against a newer build. +A package's `index.md` may also carry **`indexed_from`**, **`exclude_from_docs`** and **`exclude_kinds`**. They record which twinBASIC build the package download was indexed against and what was excluded from the docs, so a later re-index can tell a gap from an exclusion. The build uses `exclude_from_docs` only to leave those names out of the [symbol index](Building#the-symbol-index)'s count of undocumented symbols; none of the three reaches the HTML. **Leave them in place**, and bump `indexed_from` in the same commit if you re-index a package against a newer build. -Any key the build does not recognise is simply inert --- nothing iterates frontmatter generically, so an unknown key is never emitted into the page. A key whose *value* is not valid YAML is a different matter: the build aborts with `Failed to parse frontmatter in `, quoting the YAML parser's own line and column. +An unrecognised key is inert and never emitted into the page. A key whose *value* is not valid YAML aborts the build with `Failed to parse frontmatter in `, quoting the parser's own line and column. -A value that is valid YAML can still mean less than it shows. YAML reads a `#` after a space, and everything after it, as a comment, so `title: Input #` titles the page *Input*. The build warns about any value left unquoted that ends in `#`, naming the file and line, and goes on building. Quote the value --- `title: "Input #"` --- to keep the `#`. Quoting also silences the warning where YAML would have kept the `#` anyway, as in `C#`. +A value that is valid YAML can still mean less than it shows. YAML reads a `#` after a space, and everything after it, as a comment, so `title: Input #` titles the page *Input*. The build warns about any unquoted value that ends in `#`, naming the file and line. Quote the value --- `title: "Input #"` --- to keep the `#`. Quoting also silences the warning where YAML would have kept the `#` anyway, as in `C#`. A minimal reference-page header: @@ -191,46 +160,42 @@ holds `index.md` and `EnvironmentOptions.md`, and publishes at `/tB/Packages/CEF/CefBrowser/` and `/tB/Packages/CEF/CefBrowser/EnvironmentOptions`. {{tbdocs:folderStyleIndexes}} pages under `Reference/` are `index.md` files of this shape, {{tbdocs:folderStyleSlashPermalinks}} of them declaring a permalink -that ends in a slash, and the layout is written down nowhere but in them. +that ends in a slash. -**The filename carries no meaning; the permalink does all the work.** `index.md` -is not special to the build --- nothing in `discover` looks at basenames except to -sort by them --- and the source folder is not the URL folder either: that CEF page -lives at `Reference/Built-In/CEF/CefBrowser/` and publishes under -`/tB/Packages/CEF/`. What decides the shape of the output is one character: +**The filename carries no meaning; the permalink decides the output.** `index.md` +is not special to the build, and the source folder is not the URL folder: that +CEF page lives at `Reference/Built-In/CEF/CefBrowser/` and publishes under +`/tB/Packages/CEF/`. One character decides the shape of the output: permalink: /tB/Packages/CEF/CefBrowser/ -> tB/Packages/CEF/CefBrowser/index.html permalink: /tB/Packages/CEF/CefBrowser -> tB/Packages/CEF/CefBrowser.html A trailing slash makes the page an `index.html` inside a folder of that name; -without one it is a file beside its siblings. That is not cosmetic, because -relative links resolve against the rendered URL: the trailing-slash form has one -more URL segment than the other, so every `../` count on the page and every link -written *to* it changes with the slash. Pick the form first and do not change it -afterwards. +without one it is a file beside its siblings. Relative links resolve against the +rendered URL, and the trailing-slash form has one more URL segment, so every +`../` count on the page and every link written *to* it changes with the slash. +Pick the form first and do not change it afterwards. The frontmatter follows the same nesting. The index declares `parent: Package`; each sibling declares `parent: ` and `grand_parent: Package`, so the sidebar nests one level deeper. A sibling addresses its own index as `[CefBrowser](.)`, and a section of it as `[Create](.#create)`. -**Set `has_toc: false` on every page in the group.** It is a real key the template -reads, and it suppresses `renderChildrenNav` --- the automatic "Table of contents" -heading and list of child pages that the template otherwise appends to a parent -page, after its own content. On a reference index that list repeats the members -the page already introduces in prose, so every one of the 238 pages that sets the -key sets it to `false`; it is never `true` anywhere on the site. The whole -`AppGlobalClassObject` package is the precedent to copy --- its `index.md` and all -37 pages under `_App/` carry it. +**Set `has_toc: false` on every page in the group.** It suppresses the automatic +"Table of contents" heading and list of child pages that the template otherwise +appends to a parent page. On a reference index that list repeats the members the +page already introduces, so every one of the 238 pages that sets the key sets it +to `false`. The `AppGlobalClassObject` package is the example to copy --- its +`index.md` and all 37 pages under `_App/` carry it. -Note what it does not do. `has_toc` has nothing to do with the in-page table of -contents, which is the separate `* TOC goes here` / `{:toc}` marker, or with -`{: .no_toc }` on the page title, which keeps the title out of that list. A page -can use all three, and most reference index pages do. +`has_toc` is unrelated to the in-page table of contents, which is the +`* TOC goes here` / `{:toc}` marker, and to `{: .no_toc }` on the page title, +which keeps the title out of that list. A page can use all three, and most +reference index pages do. ## Page skeleton -A reference page follows a predictable shape: the title, a one-line summary, the syntax, the parameters, any remarks, then an example and the related links. The parameters use a definition list --- a term line, then a `:` line indented beneath it: +A reference page has the title, a one-line summary, the syntax, the parameters, any remarks, then an example and the related links. Parameters use a definition list --- a term line, then a `:` line beneath it: # MyFunction {: .no_toc } @@ -259,56 +224,56 @@ A reference page follows a predictable shape: the title, a one-line summary, the ## Heading levels -The page title is a single `#` (a *chapter* --- a page may legitimately have more than one). Top-level sections such as **Example** and **See Also** are `##`; subsections are `###`. +The page title is a single `#` (a *chapter*; a page may have more than one). Top-level sections such as **Example** and **See Also** are `##`; subsections are `###`. -Do **not** jump from `#` straight to `###`. That old "house style" --- an h1 followed directly by an h3 --- is a heading-order defect on the built site, where headings must not skip a level. Older pages that still do it are repaired at build time by the `headingLevelNormalizePlugin`, but new content must use the correct levels from the start. See [Extending the Builder](Extending) for where that plugin sits in the render stage. +Do **not** jump from `#` straight to `###`. An h1 followed directly by an h3 is a heading-order defect on the built site, where headings must not skip a level. The `headingLevelNormalizePlugin` repairs older pages that still do it at build time, but new content must use the correct levels. See [Extending the Builder](Extending) for where that plugin sits in the render stage. -The repair is a re-levelling of the whole page, not a patch to one heading. The plugin raises **every** heading of level 3 or deeper until each one sits exactly one level below the heading it belongs under, closing every gap in a single pass: `#` / `###` renders as h1 / h2, and `#` / `###` / `#####` renders as h1 / h2 / h3. Only the h1 chapters are left as they are, since a page may legitimately have several. +The repair re-levels the whole page. The plugin raises **every** heading of level 3 or deeper until each sits exactly one level below its parent: `#` / `###` renders as h1 / h2, and `#` / `###` / `#####` renders as h1 / h2 / h3. Only the h1 chapters are left as they are. -**Only `#`, `##` and `###` headings get an entry of their own in the site search.** The search index cuts each page at its h1, h2 and h3 headings and gives each piece one entry, titled with its heading. A `####` or deeper heading gets no entry: its text is folded into the entry of the nearest heading above it. So does a heading with a generic name, such as **Example** or **See Also**, whatever its level: it names nothing a reader would search for, so its text joins the section before it. The list is `search.fold_headings` in `docs/_config.yml`. The index is built from the rendered page, after the normalizer has run, so it sees the levels the page renders with. +**Only `#`, `##` and `###` headings get an entry of their own in the site search.** The search index cuts each page at its h1, h2 and h3 headings and gives each piece one entry, titled with its heading. A `####` or deeper heading gets no entry: its text joins the nearest heading above it. So does a heading with a generic name, such as **Example** or **See Also**, at any level. The list is `search.fold_headings` in `docs/_config.yml`. The index is built from the rendered page, so it sees the levels after the repair. ### Editing a page that still uses the old style -The repair is conditional, and the condition is easy to break without noticing. **The plugin runs only on a page that uses `#` and `###` and no `##` anywhere** --- a single `##` and it does not run at all. More than four hundred pages on this site are currently in that state, so on most of them, adding one `##` section disarms the normalizer for the whole page: every `###` that was already there stops being repaired and becomes a live heading-order defect, in the same edit that added a correctly-levelled section. +**The plugin runs only on a page that uses `#` and `###` and no `##` anywhere.** More than four hundred pages are in that state. Adding one `##` section to such a page disarms the normalizer: every `###` already there stops being repaired and becomes a heading-order defect, in the same edit that added a correctly-levelled section. -Nothing tells you. The build still succeeds --- heading order is not one of the things it checks --- and `check.bat`'s accessibility scan audits fourteen sample pages out of roughly 1,160, so unless you happened to edit one of those fourteen, the defect ships. +Nothing warns you. The build succeeds, because it does not check heading order, and `check.bat`'s accessibility scan audits fourteen sample pages out of roughly 1,160. -So when you add a section to a page whose headings start at `###`, pick one of these and finish it: +When you add a section to a page whose headings start at `###`, pick one of these and finish it: -- **Leave the page in the old style** and write your new section as `###` too. The normalizer goes on repairing the whole page, your addition included. This is the smaller and safer edit, and the right one when you are adding a section to a page you are otherwise not touching. -- **Convert the page, in the same commit.** Renumber every heading so it sits one level below its parent --- which is what the normalizer was computing for you --- then add your section at the level it belongs. Done correctly the rendered HTML does not change at all, which is what makes the conversion safe to make on its own and easy to review: the diff is only `#` characters. +- **Leave the page in the old style** and write your new section as `###` too. The normalizer keeps repairing the whole page. This is the smaller edit, and the right one when you are not otherwise touching the page. +- **Convert the page, in the same commit.** Renumber every heading so it sits one level below its parent, then add your section at the level it belongs. The rendered HTML does not change, so the diff is only `#` characters and easy to review. -What you must not do is mix the two and leave it there. +Do not mix the two. ## Renaming a heading without breaking its links -A heading's text is its anchor. Nothing in the source declares the `id` --- `headerIdPlugin` derives it from the words, by lowercasing them, dropping every character that is not a letter, digit, hyphen or space, and turning each remaining space into a hyphen. `## Heading levels` becomes `id="heading-levels"`, and `#heading-levels` is what every link into that section uses. Reword the heading and the id changes with it. The old link does not fail visibly: a fragment that matches no element leaves the reader at the top of the page --- the right page, no error --- with nothing to say the section they asked for was ever there. +A heading's text is its anchor. `headerIdPlugin` derives the `id` from the words: it lowercases them, drops every character that is not a letter, digit, hyphen or space, and turns each remaining space into a hyphen. `## Heading levels` becomes `id="heading-levels"`, and every link into that section uses `#heading-levels`. Reword the heading and the id changes with it. The old link fails silently: a fragment that matches no element leaves the reader at the top of the right page, with no error. -Two details of the slug surprise people. Runs of hyphens are not collapsed and the ends are not trimmed, so `## AppObject (optional Bool)` --- two spaces, a parenthesised type --- produces `appobject--optional-bool`, not `appobject-optional-bool`. And repeated heading text is disambiguated by position: the first keeps the bare slug, the second gets `-1` appended, the third `-2`. Adding a second `## Example` *above* an existing one renames the existing one's anchor without touching a character of its text. +Two details of the slug surprise people. Runs of hyphens are not collapsed and the ends are not trimmed, so `## AppObject (optional Bool)` --- two spaces, a parenthesised type --- produces `appobject--optional-bool`, not `appobject-optional-bool`. And repeated heading text is disambiguated by position: the first keeps the bare slug, the second gets `-1` appended, the third `-2`. Adding a second `## Example` *above* an existing one renames the existing one's anchor. **Pin the id when the wording might change, or when anything outside the page links to it.** Write the attribute on the line directly below the heading, as `Reference/Attributes.md` does for all 55 of its attribute headings: ## AppObject (optional Bool) {: #appobject } -`headerIdPlugin` skips a heading that already carries an `id`, so the pinned value wins and the heading can then be reworded freely. Two things about the syntax are load-bearing. The delimiters are `{:` and `}`, not the bare braces the upstream plugin defaults to. And the blank line decides the direction: an attribute list on the line *immediately* after a block attaches backward to that block, while one separated by a blank line attaches forward to the next one. Leave a blank line in and the id silently lands on the following paragraph. +`headerIdPlugin` skips a heading that already carries an `id`, so the heading can then be reworded freely. Two details of the syntax matter. The delimiters are `{:` and `}`, not bare braces. And a blank line changes the direction: an attribute list on the line *immediately* after a block attaches to that block, while one after a blank line attaches to the next block. Leave a blank line in and the id lands on the following paragraph. -The same attribute works inline, attached to a span instead of a block, and that is where most of its use is --- `| **vbPRCMColor**{: #vbPRCMColor } | 2 | ...` gives one enumeration member in a table its own anchor. Across `docs/` 1,342 attribute lists set an id, in 179 files. Only 159 stand alone on a line of their own, 146 of them pinning a heading; the other 1,183 are inline. +The same attribute works inline, attached to a span --- `| **vbPRCMColor**{: #vbPRCMColor } | 2 | ...` gives one enumeration member in a table its own anchor. Across `docs/`, 1,342 attribute lists set an id, in 179 files. Only 159 stand alone on a line of their own, 146 of them pinning a heading; the other 1,183 are inline. -**`redirect_from` cannot rescue a renamed anchor,** and it is the first thing most people reach for. It emits a whole-page stub --- one destination URL written four times, as the canonical link, a `location=` assignment, a meta refresh and a visible fallback link --- and nothing in it maps one fragment to another. It moves a page. It cannot move a section. +**`redirect_from` cannot rescue a renamed anchor.** It emits a whole-page stub (one destination URL written four times, as the canonical link, a `location=` assignment, a meta refresh and a visible fallback link) and nothing maps one fragment to another. It moves a page, not a section. -To find what a rename would break, start with `grep -rn "#old-anchor" docs`, then let the build settle it. `build.bat` resolves every fragment in the tree against the ids that actually reached the HTML and reports `fragment #old-anchor not found` for each link that misses. It is the oracle rather than grep because almost every id is generated: it exists in the built page and in no source file, so there is nothing for grep to match on the receiving end. +To find what a rename would break, run `grep -rn "#old-anchor" docs`, then let the build settle it. `build.bat` resolves every fragment in the tree against the ids that reached the HTML and reports `fragment #old-anchor not found` for each link that misses. Grep alone is not enough because almost every id is generated and exists in no source file. -That check sees only links made from inside this repository, and the `/tB/` anchors have a consumer outside it: the IDE help add-in, through the [symbol index](Building#the-symbol-index). Every member heading on a reference page, and every attribute heading on `/tB/Core/Attributes`, is an entry in that index, and **a build that loses one of its URLs fails**, naming it --- so a reworded member heading is caught whether or not anything in the site links to it. The fix is the one above: keep the new wording and pin the old id. +That check sees only links made from inside this repository. The `/tB/` anchors have a consumer outside it: the IDE help add-in, through the [symbol index](Building#the-symbol-index). Every member heading on a reference page, and every attribute heading on `/tB/Core/Attributes`, is an entry in that index, and **a build that loses one of its URLs fails**, naming it. A reworded member heading is caught even if nothing in the site links to it. The fix is the one above: keep the new wording and pin the old id. -Every heading on the attributes page also carries a pinned `{: #... }` id, and that is not decoration. Three of them once had none and resolved on the default slug alone --- which ties the published URL to the heading text, so appending a parenthesised type to one, to match its neighbours, would have broken it silently. Pin the id when you add the heading. +Every heading on the attributes page carries a pinned `{: #... }` id. Three once had none and resolved on the default slug alone, so appending a parenthesised type to one, to match its neighbours, would have broken it silently. Pin the id when you add the heading. ## Naming what a page documents {: #symbols } -The [symbol index](Building#the-symbol-index) learns what a reference page documents from the page itself: its title, less a trailing word such as `Module` or `class`, and the comma-separated names of its first heading --- `# Left, LeftB` documents both. For a Core page, the words of the title are the statement and its keywords: `Do...Loop` is the **Do** statement, and **Loop** is a keyword that leads to it. +The [symbol index](Building#the-symbol-index) reads what a reference page documents from the page itself: its title, less a trailing word such as `Module` or `class`, and the comma-separated names of its first heading --- `# Left, LeftB` documents both. On a Core page the words of the title are the statement and its keywords: `Do...Loop` is the **Do** statement, and **Loop** is a keyword that leads to it. -A page whose title cannot name its subject says so with `symbols:`, which replaces those names: +A page whose title cannot name its subject lists the names in `symbols:`, which replaces those names: ```yaml --- @@ -319,11 +284,11 @@ symbols: [_HiddenModule] --- ``` -On a statement page the first name is the statement and the rest are its keywords; on an operator page each is an operator of its own --- the comparison operators page lists `["=", "<>", "<", "<=", ">", ">="]`, quoted, because a bare `>` is YAML syntax. Only two pages need it today. The build tells you when a third does: a page in a package folder that gives the index no entry at all is named after the build's summary. +On a statement page the first name is the statement and the rest are its keywords; on an operator page each is an operator of its own --- the comparison operators page lists `["=", "<>", "<", "<=", ">", ">="]`, quoted, because a bare `>` is YAML syntax. Only two pages need it today. A page in a package folder that gives the index no entry at all is named after the build's summary. ## Index entries for the site search -The search finds a page by the words in it. That fails for a term the right page never uses: the `#If` / `#Const` page is where a reader searching for *conditional compilation* should land, and its text never says those words. An index entry names such a term, the way a book's index does, and a search for the whole term then finds that page first. +The search finds a page by the words in it. That fails for a term the right page never uses: a reader searching for *conditional compilation* should land on the `#If` / `#Const` page, whose text never says those words. An index entry names such a term, as a book's index does, and a search for the whole term then finds that page first. A page takes entries in its frontmatter, as one term or a list: @@ -341,24 +306,21 @@ A section takes them on its heading, with terms separated by `;`, on the line di ## Object {: index="late binding" } -**`index`** is the main entry: the one place a reader searching for the term wants first. **`index_also`** is a strong second answer, which ranks right behind it --- `/Reference/Compiler-Constants` takes `index_also: conditional compilation`. A term has one main entry across the whole site, and the build stops, naming every page that claims it, if two do. Any number of places may take it as `index_also`. +**`index`** is the main entry: the page a reader searching for the term wants first. **`index_also`** is a strong second answer that ranks right behind it --- `/Reference/Compiler-Constants` takes `index_also: conditional compilation`. A term has one main entry across the site; the build stops, naming every page that claims it, if two do. Any number of pages may take it as `index_also`. -A term matches only a search that names all of it, alone or among other words: *late binding* and *late binding in VBA* find the entry, *binding* on its own does not. Capitals, hyphens and word endings do not matter. The entries never appear on the page; the build takes them off the heading before it writes the HTML, and fails on `index` written on anything but a heading. +A term matches a search that names all of it, alone or among other words: *late binding* and *late binding in VBA* find the entry, *binding* on its own does not. Capitals, hyphens and word endings do not matter. The entries never appear on the page. The build fails on `index` written on anything but a heading. -Mark a term only where the page's own words cannot find it, and name the page a reader wants for that term, not a summary of the page. The entries in use are few on purpose: before adding one, search for the term on a built site and see where it lands. +Mark a term only where the page's own words cannot find it, and name the page a reader wants for that term. Few entries are in use; before adding one, search for the term on a built site and see where it lands. ## Counts the build fills in {: #counts } -A figure written by hand is correct the day it is written and attached to -nothing that would notice when it stops being correct. A wrong count breaks no -link, fails no gate, and reads exactly like a right one. So the numbers the -build already knows are written as names: +A count written by hand goes stale without any gate noticing. Write the numbers +the build already knows as names: {{tbdocs:pages}} pages under `Reference/` are `index.md` files of this shape -renders as the current figure. The available names, each derived on every build -from the same data the pages themselves come from: +renders as the current figure. The names are derived on every build: | name | is | |---|---| @@ -375,61 +337,50 @@ from the same data the pages themselves come from: | `enumerations` | entries in the alphabetical index of `Reference/Enumerations.md` | | `redirectStubs` | whole-page stubs emitted for `redirect_from:` entries | -The three package names are separate because the words for them collide. The +The package names are separate because the words for them collide. The [welcome page](../../) heads a section *Built-in packages* and lists all {{tbdocs:packages}} under it, while [Packages](../../tB/Packages/) reserves that -word for the {{tbdocs:builtInPackages}} a project references on demand. Neither -is wrong, and no reader can tell the two apart from one page alone, so a -sentence meaning one of them should not be written with a digit. Pick the name -for the set your sentence means and it cannot drift into the other set's number. +word for the {{tbdocs:builtInPackages}} a project references on demand. Both are +right, so a sentence meaning one of them should use the name for that set, not a +digit. -**A name is a derivation, never a constant.** A registry holding `pages: 908` -would not have removed the stale figure, only moved it from a page a -contributor reads into a module nobody opens. If a number cannot be derived it -does not get a name --- an assertion that needs a person to check it belongs in -prose, where a reader can see that it is a claim. +**A name is always derived from the build.** A number that cannot be derived +does not get a name; an assertion that needs a person to check it belongs in +prose. -Two consequences worth knowing before using one: +Two consequences: - **A misspelled name fails the build**, before any page renders, naming the - file, the line and the nearest match. It cannot be allowed to pass: an - unrecognised placeholder would otherwise be published to readers verbatim, - which is the failure the whole mechanism exists to prevent. -- **Code is immune, and needs no escaping.** A placeholder inside backticks or - a fence is left alone, because the substitution runs over markdown's inline - text and code is a different kind of token entirely. A page that needs to - *show* the syntax puts it in backticks, which is what such a page does - anyway. The one placement that does not work is inside a raw HTML block; the - build fails on that too rather than publishing it. + file, the line and the nearest match. +- **Code needs no escaping.** A placeholder inside backticks or a fence is left + alone. To *show* the syntax, put it in backticks. A placeholder inside a raw + HTML block fails the build. Numbers already written as words stay as words, because a substitution always yields digits. [Callouts](#callouts) below opens *Three severities, used -distinctly*, and should keep the word: it counts the three bullets directly -beneath it, which is a fact about that sentence rather than about the site and -could not be derived from build state at all. +distinctly* and keeps the word: it counts the three bullets beneath it, a fact +about that sentence, not about the site. ## Formatting conventions - **Bold** (`**...**`) for keywords and literal tokens the reader would type verbatim; *italic* (`*...*`) for placeholders and argument names. -- twinBASIC code goes in a ` ```tb ` fenced block --- Shiki highlights it with the vendored twinBASIC grammar, and `twinbasic`, `vb` and `vba` select the same grammar. The other highlighted fence languages are `js`, `yaml`, `json`, `c`, `html`, `xml`, `sql` and `batch`. Anything else renders as unhighlighted plain text: the build does not fail, but it prints `highlight: unknown fence language ""` naming the language and the list to add it to, so check the build output rather than the page. +- twinBASIC code goes in a ` ```tb ` fenced block. Shiki highlights it with the vendored twinBASIC grammar; `twinbasic`, `vb` and `vba` select the same grammar. The other highlighted fence languages are `js`, `yaml`, `json`, `c`, `html`, `xml`, `sql` and `batch`. Anything else renders as plain text, and the build prints `highlight: unknown fence language ""` without failing, so check the build output rather than the page. - Parameter lists use the definition-list pattern (a term line, then a `: definition` line beneath it), not a markdown table. -- **An inline code span cannot start or end with a single space.** CommonMark removes one space from each end of a span whose content is not all spaces, so `` ` 1 2 ` `` renders as `1 2`. That matters whenever the padding *is* the value being shown --- what `Debug.Print` emits into its 14-column print zones, a fixed-width return such as [Partition](../tB/Modules/Interaction/Partition)'s, anything a reader might count characters in. Write two spaces at each end to get one, and confirm against the built HTML rather than the preview: five claims on two pages were quietly de-padded this way, each of them a documented output value with its leading and trailing spaces missing. -- For dashes, write `--` in the source (it renders as an en-dash) or `---` (an em-dash). Never paste a literal `–` or `—`. Nothing in the build rejects one: the typographer converts the ASCII forms and passes a literal character straight through, so a stray dash ships silently and only the source becomes inconsistent. `scripts/convert_em_dash_separators.mjs` is the normaliser, and it is run by hand. +- **An inline code span cannot start or end with a single space.** CommonMark removes one space from each end of a span whose content is not all spaces, so `` ` 1 2 ` `` renders as `1 2`. That matters when the padding *is* the value shown --- what `Debug.Print` emits into its 14-column print zones, a fixed-width return such as [Partition](../tB/Modules/Interaction/Partition)'s. Write two spaces at each end to get one, and confirm against the built HTML rather than the preview. +- For dashes, write `--` in the source (it renders as an en-dash) or `---` (an em-dash). Never paste a literal `–` or `—`. The build does not reject one, so a stray dash ships silently. `scripts/convert_em_dash_separators.mjs` is the normaliser, and it is run by hand. ### A code sample that holds a fence marker {: #fence-in-fence } -A sample that builds a Markdown string has ` ``` ` inside it, and the fence -around the sample has to survive that. Which form to use depends on where the -marker sits. +A sample that builds a Markdown string has ` ``` ` inside it. The form of the +fence around the sample depends on where the marker sits. **A marker in the middle of a line needs nothing special.** A fence closes only on a line that is nothing but backticks, so an ordinary three-backtick fence holds a sample whose markers are inside string literals. The [`Description`](../../tB/Core/Attributes#description) entry on the attribute -reference is the shipped precedent: its example assembles a Markdown description -from twinBASIC literals, two of which are fence markers, inside a plain ` ```tb ` -fence. +reference is an example: it assembles a Markdown description from twinBASIC +literals, two of which are fence markers, inside a plain ` ```tb ` fence. ```tb [Description("### Example" & vbCrLf & _ @@ -448,57 +399,43 @@ with four backticks and close with four: ```` Nothing else changes. A fence closes only on a run at least as long as the one -that opened it, and both pre-render passes that have to find fences get them from -the site's own parser, through `lib/markdown.mjs` --- `maskCode`, which hides code -from the rewrites, and `rewriteAdmonitions`, which is the one that matters here -because it runs *outside* the mask by design. The language tag reaching the -highlighter is the same string either way --- a four-backtick `tb` fence is highlighted exactly as a -three-backtick one is. No page in `docs/` uses one yet, so there is no example to -copy; it is still the right form. - -**Prefer backticks to a tilde fence.** Both work: `rewriteAdmonitions` used to -recognise backtick fences only, so a tilde fence holding a ` ``` ` line was not -hidden from it and every `> [!NOTE]` after it rendered as literal text in a plain -blockquote. That is fixed, and `check_code_regions.mjs` carries a probe for it. -Backticks remain the house form because every fence in `docs/` is one, and a -longer backtick run is the one construct here with a shipped precedent. - -That failure is not hypothetical. The attribute reference once shipped all six of -its admonitions as the literal text `[!NOTE]`, because the same pairing closed -that page's opening fence on a marker in the middle of a line and every pairing -after it was off by one. The mid-line case is fixed --- the rewrite now asks the -site's parser where the fences are --- and [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), which -`test.bat` runs, is the only gate that sees this class of fault at all. +that opened it, and the language tag reaching the highlighter is the same string +either way. No page in `docs/` uses one yet. + +**Prefer backticks to a tilde fence.** Both work, but every fence in `docs/` is +a backtick one. A fault in how the admonition rewrite finds fences shows every +`> [!NOTE]` after it as literal text; the attribute reference once shipped all +six of its admonitions that way. +[`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), which +`test.bat` runs, is the only gate that sees this class of fault. ### Typography -The site ships its own fonts, so a page renders the same on every platform: **Inter** for text, **Cascadia Mono** for code, and **Source Serif 4** for body text in the PDF book. Nothing is needed from you to use them --- they apply automatically --- but two things follow from it. +The site ships its own fonts, so a page renders the same on every platform: **Inter** for text, **Cascadia Mono** for code, and **Source Serif 4** for body text in the PDF book. They apply automatically. Two things follow. -**Characters outside the shipped subsets fall back to a system font**, which is visible as a glyph in the wrong face. The subsets cover Latin (including Latin Extended-A and -B), Greek, punctuation, arrows, mathematical operators, currency, fractions, geometric shapes, dingbats, and --- for code --- box drawing. Emoji are deliberately excluded and come from the platform's own emoji font. Two specifics worth knowing: write the check mark as **U+2713** (✓), not **U+2714** (✔), which no text face in the stack carries; and prefer `:` over **U+22EE** (⋮) inside a monospaced ASCII diagram, where a fallback glyph of a different width shears the box borders. +**Characters outside the shipped subsets fall back to a system font**, visible as a glyph in the wrong face. The subsets cover Latin (including Latin Extended-A and -B), Greek, punctuation, arrows, mathematical operators, currency, fractions, geometric shapes, dingbats, and --- for code --- box drawing. Emoji are excluded and come from the platform's own emoji font. Write the check mark as **U+2713** (✓), not **U+2714** (✔), which no text face in the stack carries. Prefer `:` over **U+22EE** (⋮) inside a monospaced ASCII diagram, where a fallback glyph of a different width shears the box borders. -Those two example glyphs are, deliberately, the only characters on the whole site that fall back --- so the ✔ and ⋮ above are being drawn by your system's symbol font right now, next to text that is not. That difference in weight and shape is the effect this section is about. +Those two example glyphs are the only characters on the site that fall back, so the ✔ and ⋮ above are drawn by your system's symbol font, next to text that is not. That difference in weight and shape is the effect to avoid. -Diagram exports carry the font with them. The Download / Copy SVG and PNG buttons of each diagram embed the typeface into the exported file, because an exported SVG has no access to the site's stylesheet and would otherwise render in whatever the viewer has installed. All four buttons work on every diagram. +Diagram exports carry the font with them. The Download / Copy SVG and PNG buttons of each diagram embed the typeface into the exported file, because an exported SVG cannot use the site's stylesheet. All four buttons work on every diagram. -**Do not hand-edit a diagram's `.svg`.** It is a build artifact: the `.dot` beside it is the source, and the next build overwrites your edit. Changing the face is the edit that looks most harmless and is not --- Graphviz sizes each box to the text it measured, so a diagram whose labels are painted in a font the layout never saw has text hanging outside its boxes. `check.bat` fails on that; see [Diagrams](#diagrams) below. +**Do not hand-edit a diagram's `.svg`.** It is a build artifact: the `.dot` beside it is the source, and the next build overwrites your edit. Changing the font is especially harmful --- Graphviz sizes each box to the text it measured, so labels in a font the layout never saw hang outside their boxes. `check.bat` fails on that; see [Diagrams](#diagrams) below. ## Adding a CSS rule that works in both themes {: #css-rules } -A style rule for something new on the site goes in `docs/_sass/custom/custom.scss`. Do not put it in the vendored theme under `builder/vendor/just-the-docs/`, and do not start a stylesheet of your own, which no page would load: everything under `docs/_sass/` is compiled into `just-the-docs-combined.css`, which every page does load. `serve.bat` rebuilds it each time you save. +A style rule for something new goes in `docs/_sass/custom/custom.scss`. Do not put it in the vendored theme under `builder/vendor/just-the-docs/`, and do not start a stylesheet of your own, which no page would load. Everything under `docs/_sass/` is compiled into `just-the-docs-combined.css`, which every page loads. `serve.bat` rebuilds it each time you save. -**The dark theme is a second copy of the whole theme, not a set of CSS variables.** The build compiles the theme twice and emits the dark copy inside a theme selector such as `html[data-theme="dark"]`, so every theme rule is more specific in dark mode than it is in light. A rule you write with a single class can therefore beat the theme in light mode and lose to it in dark, with no error: `.reversefootnote` did exactly that. Prefix the selector with `.main-content` --- `.main-content .reversefootnote` --- and it wins in both. [The specificity trap](Builder#the-specificity-trap) covers the cases where that is not enough. +**The dark theme is a second copy of the whole theme, not a set of CSS variables.** The build compiles the theme twice and emits the dark copy inside a theme selector such as `html[data-theme="dark"]`, so every theme rule is more specific in dark mode. A rule with a single class can therefore beat the theme in light mode and lose to it in dark, with no error: `.reversefootnote` did exactly that. Prefix the selector with `.main-content` --- `.main-content .reversefootnote` --- and it wins in both. [The specificity trap](Builder#the-specificity-trap) covers the cases where that is not enough. -**Check it in both themes.** Run `serve.bat`, open a page that uses the rule, and switch themes with the theme button in the page header rather than with your operating system's setting, because the button is what exercises the `[data-theme]` rules. A rule that fails only in the dark theme is usually cosmetic, and no gate reports it. +**Check it in both themes.** Run `serve.bat`, open a page that uses the rule, and switch themes with the theme button in the page header, not with your operating system's setting: the button is what exercises the `[data-theme]` rules. No gate reports a rule that fails only in dark. -[Project styling](Builder#project-styling) is the full account: which file under `docs/_sass/` holds what, why the theme is compiled twice, and how to verify a style change. +[Project styling](Builder#project-styling) says which file under `docs/_sass/` holds what and how to verify a style change. ## Checking that a sample compiles -Nothing in the ordinary build looks inside a code fence. The link check, the accessibility -scan and the code-region gate all pass over a twinBASIC sample that the compiler would -refuse --- two such samples shipped, one of them a flagship example on a package page, and -every gate was green over both. +The ordinary build does not look inside a code fence. The link check, the accessibility +scan and the code-region gate all pass a twinBASIC sample that the compiler would refuse. A sample can ask to be compiled. Add `check_build` to its fence: @@ -515,9 +452,8 @@ Then run it, which needs a twinBASIC install and Windows: examples.bat --only "^Reference/Core" **Paste the summary line into the pull request description.** `examples.bat` runs in no -CI workflow, so a green pull request says nothing about a sample; the line a run ends with -is the evidence a reviewer has. Run it over the pages you changed, and paste the command -with the line: +CI workflow, so the line a run ends with is the reviewer's only evidence. Run it over the +pages you changed, and paste the command with the line: examples.bat --only "^Tutorials/Testing-with-Assert" check_examples: 7 sample(s), 7 compile, 0 finding(s), 19.6s -- clean @@ -526,48 +462,44 @@ with the line: sample that failed against its page and line, and exits 1. **The marker never reaches the page.** The renderer takes the first word of a fence's info -string as the language and discards the rest, so a marked fence produces byte-identical -HTML to an unmarked one. Nothing in the built site, the search index or the PDF can tell -the difference. +string as the language and discards the rest, so the built site, the search index and the +PDF are identical with or without it. **Mark every `tb` fence: `check_build` for a sample that is a program, `inert=` for one that is not.** A statement run with an elision in it, a signature with no body, a -syntax skeleton with `` --- those are good documentation, and there is nothing -for a compiler to say about them. They are marked all the same, as [Saying that a sample is -not a program](#saying-that-a-sample-is-not-a-program) describes, because an unmarked fence -is never compiled and `examples.bat --census` counts it as still unmarked. Checking is -opt-in for a historical reason: when the harness arrived, roughly a third of the fences -were not programs, and a gate that demanded every fence compile would have needed hundreds -of exceptions on the first day. +syntax skeleton with `` are good documentation, but a compiler has nothing to +say about them. Mark them anyway, as [Saying that a sample is +not a program](#saying-that-a-sample-is-not-a-program) describes: an unmarked fence +is never compiled, and `examples.bat --census` counts it as still unmarked. The tool works out what to build around a sample --- a whole `Class` goes in a file of its own, procedures and declarations go in a generated module, loose statements go in a -generated `Sub`. Three keys override it when it guesses wrong, and one flag asks for more: +generated `Sub`. These keys override it when it guesses wrong: | In the fence | Means | |---|---| | `check_build` | Compile this sample. | | `check_run` | Compile it, build it and run it, then compare what it prints with what the page says it prints, as described below. Only a statement sample (`slot=sub`) can be run. A sample that raises an error, does not return, or prints something else is a finding. A sample that calls `MsgBox` or `InputBox`, or contains an `End` statement, is refused without being built. | | `hidden` | Context for the page's other samples that the reader never sees. Implies `check_build`. See below. | -| `slot=file` / `slot=module` / `slot=sub` / `slot=class` / `slot=method` | What to generate around it, when the inference is wrong. `class` and `method` are the same two shapes inside a `Class` rather than a `Module`, for code-behind. The report always names the slot it used, so a wrong guess reads as a wrong guess. | +| `slot=file` / `slot=module` / `slot=sub` / `slot=class` / `slot=method` | What to generate around it, when the inference is wrong. `class` and `method` are the same two shapes inside a `Class` rather than a `Module`, for code-behind. The report names the slot it used. | | `inherits=` | The sample is code-behind *of* something --- `inherits=Form`, `inherits=MDIForm`. The wrapper becomes a `Class` that inherits it, so `Me.Caption` resolves against the real type. | | `project=` | Which template project to build into. The default follows the page: a page under `Reference/Built-In/` gets the one that references every package. `project=form` is for a `check_run` sample that needs a real form, to draw on or to read a property of: the project has an empty `Form1`, and the checker unloads every form a sample leaves loaded. Every fence of a `projname` group names the same project. | -| `projname=` | Build these samples **as one project**, for a page that presents one program in pieces --- a function in one fence and the tests for it in the next three. Every sample sharing the name is compiled together and nothing else is compiled with them. | +| `projname=` | Build these samples **as one project**, for a page that presents one program in pieces --- a function in one fence and the tests for it in the next three. Samples sharing the name are compiled together and with nothing else. | | `concat_group=` | Join these fences, in page order, into **one** piece of code before building it --- for a single construct shown in parts, such as a `Sub` or a `Class` introduced a section at a time. Implies `check_build`. See below. | -| `expect-error=` | This sample is *meant* not to compile --- it is showing what goes wrong --- and the run fails if it compiles. | +| `expect-error=` | This sample is *meant* not to compile --- it shows what goes wrong --- and the run fails if it compiles. | | `resource=` | **On a fence in any language**, not just ` ```tb `. The fence's contents are written into the generated project at that project-relative path, so the page's samples can be compiled against it. For the compile-time attributes that read a project file --- see below. | -| `inert=` | This fence is **not a program**, and saying so settles it: it is never compiled, never proposed, and counted under its reason instead of sitting in the backlog. See below. | - -A `check_run` sample states what it prints in one of two ways, and the checker reads -the sample's own text, so the page needs nothing extra. A trailing comment on a -`Debug.Print` line gives that line's output. A comment line that is exactly `' Output:` -gives the whole output, one line per comment line below it. A sample that states -nothing is still run, and must return without raising an error. An error the sample -handles itself is not raised, so a sample can show an error with `On Error Resume Next` -and print `Err.Number`. When a sample states -anything, it must print exactly as many lines as it states. Each line is compared with -the spaces at both ends removed, because twinBASIC prints a space before a positive -number. +| `inert=` | This fence is **not a program**: it is never compiled or proposed, and is counted under its reason instead of sitting in the backlog. See below. | + +A `check_run` sample states what it prints in one of two ways, read from the +sample's own text. A trailing comment on a `Debug.Print` line gives that line's +output. A comment line that is exactly `' Output:` gives the whole output, one +line per comment line below it. When a sample states anything, it must print +exactly as many lines as it states. Each line is compared with the spaces at both +ends removed, because twinBASIC prints a space before a positive number. + +A sample that states nothing is still run, and must return without raising an +error. An error the sample handles itself is not raised, so a sample can show an +error with `On Error Resume Next` and print `Err.Number`. ````markdown ```tb check_run @@ -591,41 +523,39 @@ Next ### Saying that a sample is not a program -Plenty of good fences are not programs. A syntax skeleton written with placeholder names -(`Interface name Extends base_interface`) teaches the shape better than any compilable -stand-in would; a fence that continues the previous one, or shows the invalid form beside -the valid one, is doing its job exactly as written. +Many good fences are not programs. A syntax skeleton written with placeholder names +(`Interface name Extends base_interface`) teaches the shape better than a compilable +stand-in; so does a fence that continues the previous one or shows the invalid form beside +the valid one. Mark those `inert=` rather than leaving them unmarked, so nobody triages them twice: | Reason | For | |---|---| | `skeleton` | placeholder identifiers --- `name`, `base_interface`, `` | -| `signature` | a procedure's signature shown on purpose without a body, where a body would mislead --- the members an interface declares, which a reader implements under other names | -| `excerpt` | deliberately continues another fence, or shows part of one | +| `signature` | a procedure's signature shown without a body, where a body would mislead --- the members an interface declares, which a reader implements under other names | +| `excerpt` | continues another fence, or shows part of one | | `pseudo` | prose, a table or a protocol listing set in a code fence | -| `contrast` | shows the invalid form on purpose, beside the valid one | +| `contrast` | shows the invalid form, beside the valid one | | `external` | needs a file or environment the checker cannot provide | | `designer` | needs a real form designer --- a `Handles` clause on designer-declared fields, or a control array | -| `blocked` | correct code that a **product defect** stops compiling *and for which no workaround exists*. Look hard for one first --- a package's private half can be reached with an asterisk [library symbol](../Features/Packages/Library-Symbols), and a page that shows the qualified form plus a note is better than a page that shows code the reader cannot run | +| `blocked` | correct code that a **product defect** stops compiling *and for which no workaround exists*. Look hard for one first --- a package's private half can be reached with an asterisk [library symbol](../Features/Packages/Library-Symbols), and a page that shows the qualified form plus a note beats one that shows code the reader cannot run | -An unrecognised reason is refused, the same as a bad `slot=`, and `inert` together with -`check_build` is refused as a contradiction. The census then reports three numbers rather -than two: how many samples are checked, how many are inert, and how many are **undecided**. -Only the last is a backlog. +An unrecognised reason is refused, as is `inert` together with `check_build`. The +census reports how many samples are checked, how many are inert, and how many are +**undecided**. Only the last is a backlog. **A fence that stops before its closing line is usually one line from compiling.** Before marking it `excerpt` or `signature`, put an elision comment (`' ...`) where the omitted code would go and add the construct's closing line --- `End Function`, `End Interface`. The -reader still sees that something was left out, and the compiler can now check what was -left in. +reader still sees that something was left out, and the compiler can check what was left in. ### A sample that reads a file Some twinBASIC features read a project file *while compiling*. -[`[PopulateFrom]`](../tB/Core/Attributes#populatefrom) is the one to know: it fills an empty -**Enum** with members taken from a JSON resource, so a page documenting it has an enum whose -members exist only if that file does. +[`[PopulateFrom]`](../tB/Core/Attributes#populatefrom) fills an empty **Enum** with +members taken from a JSON resource, so a page documenting it has an enum whose members +exist only if that file does. Mark the JSON block that the page already shows with `resource=`, and the file is staged beside the samples: @@ -636,23 +566,18 @@ beside the samples: ``` ```` -The file travels with its page, exactly as a `hidden` fence does, so every sample on that -page is compiled against it. The block is rendered normally --- the reader is meant to see -the file --- and the path may not climb out of the project, so no `..` and no drive letter. - -What this buys is more than a green tick: the enum members the page's *other* samples use -come from that JSON, so the compiler checks the file's shape and the names it produces -against the code that reads them. +Every sample on that page is compiled against the file. The block is rendered normally, +and the path may not climb out of the project: no `..` and no drive letter. The enum +members the page's *other* samples use come from that JSON, so the compiler checks the +file's shape and the names it produces against the code that reads them. A sample that assumes a control on a form is fine: the templates declare `Text1`, -`ListView1`, `CefBrowser1` and the others, exactly as a reader's own project would, so what -gets checked is the part the sample is actually claiming. +`ListView1`, `CefBrowser1` and the others, as a reader's own project would. -**`Me` and `WithEvents` are read as declarations of intent.** Neither is legal in a standard -module --- the compiler says so in as many words --- so a fence using either is class -code-behind, and the tool wraps it in a `Class` without being told. What it cannot guess is +**`Me` and `WithEvents` mark class code-behind.** Neither is legal in a standard module, +so the tool wraps a fence using either in a `Class` without being told. It cannot guess *which* class, so a sample reaching a member of the thing it is code-behind of --- -`Me.Caption`, `Me.Arrange` --- needs `inherits=` to say. +`Me.Caption`, `Me.Arrange` --- needs `inherits=`. ### A page can carry its own context @@ -661,8 +586,8 @@ nothing**: it is absent from the page, the search index, the offline mirror and It is for the declarations a sample assumes but no reader needs to read --- a class the page describes in prose but never lists, an API `Declare`, a control instance only this -page uses. Putting them in a hidden fence keeps them beside the samples that need them, -in one file, instead of in a template shared by every other page on the site. +page uses. They stay in one file beside the samples that need them, not in a template +shared by every other page. ````markdown ```tb hidden @@ -674,27 +599,26 @@ End Class ``` ```` -Use it for context, not for hiding a sample. A hidden fence is still compiled, so it is -checked like everything else --- but nobody can read it, and a fence nobody can read is -not documentation. +Use it for context, not for hiding a sample. A hidden fence is compiled like any other, +but nobody can read it, and a fence nobody can read is not documentation. -**Decide what to hide by asking what the reader needs.** Before hiding a line, ask whether -a reader would write it, or would need it to know where an object comes from. An event -handler's header is both --- `Request.Headers` means nothing until the reader sees which -event passes `Request` --- and so is the subject of a `With` that a leading-dot line depends -on, or the `WithEvents` field that an event handler's name is built from. Show those. Hide -what the reader brings from elsewhere: their own helper routines, the controls the form -designer declares, and stand-ins for declarations that an external library provides. +**Decide what to hide by asking what the reader needs.** Show a line the reader would +write, or needs to know where an object comes from: an event handler's header +(`Request.Headers` means nothing until the reader sees which event passes `Request`), the +subject of a `With` that a leading-dot line depends on, or the `WithEvents` field that an +event handler's name is built from. Hide what the reader brings from elsewhere: their own +helper routines, the controls the form designer declares, and stand-ins for declarations +that an external library provides. ### One construct across several fences `concat_group=` joins every fence on the page that carries the name, in page order, -into one piece of code before anything else happens. It is for a single construct shown in -parts --- a `Sub` built up a few lines at a time, a `Class` whose members are introduced one -section at a time --- which no fence can compile on its own, because each part is an -unclosed block. It is not `projname`: that compiles its samples as *separate* modules of one -project, while the parts of a `concat_group` become one file, so a `Private` field declared -in the first part is visible in the last. +into one piece of code. It is for a single construct shown in parts --- a `Sub` built up a +few lines at a time, a `Class` whose members are introduced one section at a time --- which +no fence can compile on its own, because each part is an unclosed block. It differs from +`projname`, which compiles its samples as *separate* modules of one project: the parts of a +`concat_group` become one file, so a `Private` field declared in the first part is visible +in the last. Parts can be `hidden`. The [Painting](../Tutorials/CustomControls/Painting) tutorial shows only the `OnPaint` method it is teaching. A hidden fence before it opens the class, @@ -705,84 +629,69 @@ the sample *inside* anything. The joined code is reported as its first visible p error in any part is reported against that part's own line. **A sample that needs another sample needs `projname`.** Samples are packed several to a -generated project, so one can sometimes see another's declarations by luck --- and luck -changes with what else is being checked, which makes a page pass one run and fail the next. -Naming the group says the dependency out loud, and the tool then refuses a group that is -only half marked rather than reporting a missing symbol in the sample that is fine. +generated project, so one can see another's declarations by luck, and a page passes one +run and fails the next. Naming the group states the dependency, and the tool refuses a +group that is only half marked. -**A mistyped marker is caught.** `check_bild` renders identically to no marker at all, so a -sample carrying one would simply never be compiled; the tool reports an unrecognised token -rather than skipping it in silence. +**A mistyped marker is caught.** `check_bild` renders like no marker at all, so the tool +reports an unrecognised token rather than skipping the fence silently. [Code Samples and Attributes](Tools-Samples#check-examples) covers running it --- the census and survey modes, the flags, and what the report means. ## Bullet lists, dashes, and parentheses -Most bullets on this site are a term, a dash, and a description, and which dash -depends on what precedes it. The practice is near-absolute and has never been -written down, so it survives only by imitation: +Most bullets on this site are a term, a dash, and a description. The dash depends +on what precedes it: -- A **bold-term** bullet takes `---`, the em-dash: `- **Reference** --- the language and library reference.` 50 of the 50 such bullets in `docs/Reference/` do this, with no exceptions. -- A **link** bullet takes `--`, the en-dash: `- [Permanent Links](Permanent-Links) -- the stable URL scheme.` 1,570 of 1,570 in `docs/Reference/`, again with none the other way. +- A **bold-term** bullet takes `---`, the em-dash: `- **Reference** --- the language and library reference.` All 50 such bullets in `docs/Reference/` do this. +- A **link** bullet takes `--`, the en-dash: `- [Permanent Links](Permanent-Links) -- the stable URL scheme.` All 1,570 in `docs/Reference/` do this. -The reason is visual rather than grammatical. A bold term is a label with a -definition after it, so the separator has to be strong enough to read as a break; -a link already ends in its own visual boundary, and an em-dash after it crowds -the line. The distinction matters only because it is consistent: a page that -mixes the two looks like two people wrote it. +A bold term is a label with a definition after it, so the separator has to read as +a break. A link already ends in its own visual boundary, and an em-dash after it +crowds the line. Do not mix the two on a page. **The description after the dash continues the sentence and is not capitalised.** Of the bold-term bullets in `docs/Reference/` that begin with a letter, 26 of 26 are lowercase, and in `docs/Documentation/` 16 of 16. Link bullets hold to the same rule where they can: 1,411 begin lowercase, and almost all of the 94 that do not begin with a word that carries its own capital anyway --- `Win32`, `OLE`, -`GDI`, `Variant`, `JavaScript`. The only genuine exceptions are 36 bullets on +`GDI`, `Variant`, `JavaScript`. The only exceptions are 36 bullets on `Reference/Built-In/AppGlobalClassObject/index.md`, which writes each annotation -as a full sentence starting `Returns` or `Gets`. That is one page's local habit, -not a second convention to choose between. - -**Parentheses are for a short label, `---` for an aside that is a clause.** The -split is real practice, not a preference: across the prose of `docs/Reference/`, -78% of parenthesised spans are 25 characters or shorter and the median is 12. What -fills them is a tag the reader takes in without stopping --- `(default)` 152 -times, `(VBRUN)` 86, `(twinBASIC)` 59, `(optional Bool)` 38, and the kind markers -`(Class)`, `(Property)`, `(Sub)`. Once the aside has a verb in it, it is part of -the argument of the sentence and belongs between em-dashes, where the reader -still reads it: +as a full sentence starting `Returns` or `Gets`. + +**Parentheses are for a short label, `---` for an aside that is a clause.** +Across the prose of `docs/Reference/`, 78% of parenthesised spans are 25 +characters or shorter and the median is 12. They hold a tag the reader takes in +without stopping --- `(default)` 152 times, `(VBRUN)` 86, `(twinBASIC)` 59, +`(optional Bool)` 38, and the kind markers `(Class)`, `(Property)`, `(Sub)`. An +aside with a verb in it is part of the argument of the sentence and belongs +between em-dashes: ... or **Any** (twinBASIC; the type is inferred from *expression*) ... equivalent to a separate assignment immediately after the **Dim** --- `Dim i As Long = 1` is the same as `Dim i As Long: i = 1`. -`Reference/Core/Dim.md` currently does both within four lines of each other, -which is what an unstated rule looks like. Apply the length test: a tag of two or -three words goes in parentheses, anything that could stand as its own sentence -takes `---`. +`Reference/Core/Dim.md` currently does both within four lines of each other. Apply +the length test: a tag of two or three words goes in parentheses, anything that +could stand as its own sentence takes `---`. ## Tables A table is plain GitHub-flavoured markdown --- a header row, a delimiter row, then -the body. Nothing else is needed, and in particular **the scroll wrapper is not -yours to write.** The renderer's `table_open` rule wraps every table in -`
` on its way out, and that `tabindex` is -an accessibility fix rather than decoration: the wrapper is `overflow-x: auto`, and -a scroll container a keyboard user cannot focus cannot be scrolled without a -mouse. It is unconditional because whether a given table overflows depends on the -reader's viewport, so there is no answer to give at render time. `th` gets its -`scope="col"` from the same place. If you are writing a markdown-it plugin, do not -add a second wrapper: the outer one would have no `tabindex`, which is the shipped -fix undone. +the body. **Do not write the scroll wrapper.** The renderer's `table_open` rule +wraps every table in `
`. The wrapper is +`overflow-x: auto`, and the `tabindex` lets a keyboard user focus and scroll it. +`th` gets its `scope="col"` from the same place. If you are writing a markdown-it +plugin, do not add a second wrapper: the outer one would have no `tabindex`. **A literal `|` inside a cell must be escaped as `\|`,** or the parser reads it as -the next column boundary and the row silently gains a cell. Backticks are no -protection: the row is split into cells before inline parsing runs, so an -unescaped pipe inside a code span breaks the row exactly as a bare one does. It -comes up in this documentation more than in most, because `|` is twinBASIC's -separator for alternatives in a syntax line and the builder's own reference writes -type unions with it --- `[ ( True \| False ) ]`, `string\|undefined`. It is escaped -in 38 files and explained in none of them. +the next column boundary and the row silently gains a cell. Backticks do not +help: the row is split into cells before inline parsing runs, so an unescaped pipe +inside a code span breaks the row too. It comes up often here, because `|` +separates alternatives in a twinBASIC syntax line and the builder's reference +writes type unions with it --- `[ ( True \| False ) ]`, `string\|undefined`. Per-column alignment goes in the delimiter row, with a colon on the side the content should sit: `:---` left, `---:` right, `:---:` centred, and a bare `---` @@ -794,28 +703,22 @@ the default against 73 explicit left, 24 centred and 4 right. three columns, 16 have four, and five have five. Exactly one has more: `Reference/Default/VBA/Interaction/Partition.md` at seven, and it is the model to copy if you need one that wide. It right-aligns the three numeric argument columns -and left-aligns the four result columns, and it spends a sentence of prose before -the table explaining what the two least obvious column headings mean, which is -what makes seven columns readable at all. +and left-aligns the four result columns, and a sentence of prose before the table +explains the two least obvious column headings. **Every total in this section is a hand count of `docs/`, taken on 2026-09-21.** -No [count name](#counts) covers tables, and a number the build cannot derive does -not get one --- so nothing keeps these current, and each of them is already a -little wrong by the time you read it. The proportions are the durable part, and -the rarities are more durable still: two or three columns stays the norm, four -stays rare, five rarer, and seven stays one page, however many tables get -written. Quote the shape, and re-count before quoting a total. - -Check a wide table in the PDF as well as on the site, because the two behave -differently and only one of them degrades gracefully. `book.mjs` strips the -`table-wrapper` before assembling the book, and `print.css` sets `table { width: -100% }` with no overflow rule anywhere --- so a table too wide for the 170mm text -column has no scroll affordance to fall back on and simply runs into the margin. -Nothing reports it. Run `book.bat` and look at the page. +No [count name](#counts) covers tables, so the totals drift. The proportions last: +two or three columns is the norm, four is rare, five rarer, and seven is one page. +Re-count before quoting a total. + +Check a wide table in the PDF as well as on the site. The PDF has no scroll +wrapper (`book.mjs` strips it) and `print.css` sets `table { width: 100% }`, so a +table too wide for the 170mm text column runs into the margin, and nothing reports +it. Run `book.bat` and look at the page. ## Diagrams -A diagram is a Graphviz `.dot` file. Put it where it belongs: `docs/assets/images/dot/` if more than one page uses it, or in the `Images/` folder beside the page if only one does. The build renders an `.svg` next to it, inlines that into the page, and gives it the zoom and export controls. Both files belong in git. +A diagram is a Graphviz `.dot` file. Put it in `docs/assets/images/dot/` if more than one page uses it, or in the `Images/` folder beside the page if only one does. The build renders an `.svg` next to it, inlines that into the page, and gives it the zoom and export controls. Commit both files. ``` docs/Tutorials/CEF/Images/MonacoArchitecture.dot <-- you write this @@ -823,8 +726,7 @@ docs/Tutorials/CEF/Images/MonacoArchitecture.svg <-- the build writes this ``` **Reference the `.svg`, never the `.dot`**, and use the markdown image form rather -than a raw `` tag --- the same rule, and the same reason, as [Images](#images) -below: +than a raw `` tag, as under [Images](#images) below: ```markdown ![A twinBASIC form holding two CefBrowser controls. The left one hosts the Monaco @@ -832,11 +734,11 @@ editor and posts the edited HTML to a handler in twinBASIC code, which passes it the right control to render as a preview.](Images/MonacoArchitecture.svg) ``` -Name the font as the existing diagrams do --- copy the `node`, `edge` and `graph` blocks from one of them --- and let the build measure it. Copy from a diagram of the same kind: `docs/Tutorials/CEF/Images/MonacoArchitecture.dot` for one belonging to a page, `docs/assets/images/dot/toolchain-overview.dot` for a shared one. They differ in node `margin` --- `0.16,0.09` against `0.12,0.06` --- so a figure quoted for one family is not the other's. You do not need to do anything special for Inter: `builder/dot-metrics.mjs` installs its real widths into Graphviz before layout, and `check.bat` asserts afterwards that nothing overflowed. +Copy the `node`, `edge` and `graph` blocks from an existing diagram of the same kind, which name the font, and let the build measure it: `docs/Tutorials/CEF/Images/MonacoArchitecture.dot` for one belonging to a page, `docs/assets/images/dot/toolchain-overview.dot` for a shared one. They differ in node `margin` --- `0.16,0.09` against `0.12,0.06`. Nothing special is needed for Inter: `builder/dot-metrics.mjs` installs its real widths into Graphviz before layout, and `check.bat` asserts that nothing overflowed. -Two things to leave alone. Node text is deliberately dark against each node's light fill in both themes; only the cluster and edge labels follow the page's text colour, because those are the ones sitting on the page background. And a diagram needs alt text like any other image --- the whole diagram gets one accessible name, so describe what it shows, not what shapes it contains, and never `![Diagram]`. Everything under [Images](#images) about what makes alt text worth having, and about nothing in the build being able to tell you, applies here unchanged. +Leave node text alone: it is dark against each node's light fill in both themes. Only the cluster and edge labels follow the page's text colour, because they sit on the page background. A diagram needs alt text like any other image. The whole diagram gets one accessible name, so describe what it shows, not what shapes it contains, and never `![Diagram]`. What [Images](#images) says about alt text applies here. -A box can link somewhere: give its node, or its cluster, a `URL`, relative to the page that shows the diagram (`URL="Tools-Build#tbdocs"`). The diagram is then a group of links rather than one image, and the build moves each cluster's label just before the first box inside it, so that a reader, and the Tab key, takes each label and then its boxes. Declare the nodes in reading order and set `outputorder=nodesfirst` on the graph, so that the boxes come in that order too. `docs/Documentation/Images/Tools-Groups.dot` does both. The build's link check reports a link that leads nowhere, as it does for any other link. +A box can link somewhere: give its node, or its cluster, a `URL`, relative to the page that shows the diagram (`URL="Tools-Build#tbdocs"`). The diagram is then a group of links rather than one image, and the build moves each cluster's label just before the first box inside it, so the Tab key takes each label and then its boxes. Declare the nodes in reading order and set `outputorder=nodesfirst` on the graph, so the boxes come in that order too. `docs/Documentation/Images/Tools-Groups.dot` does both. The link check reports a link that leads nowhere. ## Images @@ -846,48 +748,48 @@ Images live in an `Images/` folder beside the page that uses them, and are refer ![The Package Manager panel, with arrows pointing at the PUBLISH THIS PACKAGE button](Images/packPublishButton.png) ``` -**The text in the square brackets is the image's alt text, and every image needs one.** Write what a reader who cannot see the picture would otherwise miss --- what the screenshot shows and which part of it the surrounding prose is pointing at --- rather than a label for the file. A caption-length phrase is usually right; the file name, the word *screenshot*, and the name of the button you happened to be looking for are not. +**The text in the square brackets is the image's alt text, and every image needs one.** Write what a reader who cannot see the picture would otherwise miss --- what the screenshot shows and which part of it the surrounding prose is pointing at. A caption-length phrase is usually right; the file name, the word *screenshot*, and the name of the button you happened to be looking for are not. -Nothing in the build will correct you, and it is worth knowing exactly why. The accessibility scan's `image-alt` rule asks only whether an accessible name *exists*, never whether it is the right one. `![image](…)` passes it. So does `![img](…)`. So does an empty `![](…)`, because an empty alt is the explicit signal that an image is decorative and a screen reader should skip it --- true of a spacer or a bullet glyph, never true of a screenshot. A green `check.bat` therefore says only that something is present in the brackets. Whether it describes the picture is a judgement no gate here makes, and the scan samples fourteen pages in any case, so most images are never looked at by it at all. +No gate checks the alt text. The accessibility scan's `image-alt` rule asks only whether an accessible name *exists*. `![image](…)` passes it. So does `![img](…)`. So does an empty `![](…)`, because an empty alt marks an image as decorative, which is right for a spacer and wrong for a screenshot. The scan also samples only fourteen pages. Whether the text describes the picture is for you and your reviewer to judge. -One habit to avoid in particular: copy the *shape* of a neighbouring page's image markdown, never its alt text. The example above used to read `![Create Package]`, and that one string travelled from it onto ten further images across the site --- a references dialog, a toolbox, a properties window, an animated GIF --- describing none of them, on pages that passed every check the project has. They have since been rewritten, but nothing except a reader would ever have found them. +Copy the *shape* of a neighbouring page's image markdown, never its alt text. The example above used to read `![Create Package]`, and that string spread onto ten further images --- a references dialog, a toolbox, a properties window, an animated GIF --- describing none of them. -Use the markdown form. A raw `` tag with a page-relative `src` is **not** rewritten when the PDF book is assembled: the book flattens every page into a single document, so a path like `Images/x.png` that resolves correctly on the site resolves against the book root instead and the render aborts with `pdf: missing image`. The markdown form is rewritten to a section-qualified path and works in all three outputs. +Use the markdown form. A raw `` tag with a page-relative `src` is **not** rewritten when the PDF book is assembled. The book flattens every page into one document, so a path like `Images/x.png` resolves against the book root and the render aborts with `pdf: missing image`. The markdown form is rewritten to a section-qualified path and works in all three outputs. -**A finished page never *renders* an image from a remote URL.** Pasting a screenshot into a GitHub issue or pull request produces a `https://github.com/user-attachments/assets/...` link, and pasting that straight into a page is fine --- the build vendors it for you. The next local build downloads the file to `assets/attachments/gh-.` and substitutes that path while rendering, so the published HTML, the offline mirror and the PDF all point at the local copy. Your markdown keeps the remote URL, which is why the downloaded file has to be committed along with your edit: it is the only copy the site has. Any *other* remote host has no such handling --- download it yourself, commit it under the section's `Images/` folder, and reference that path in the page. +**A finished page never *renders* an image from a remote URL.** Pasting a screenshot into a GitHub issue or pull request produces a `https://github.com/user-attachments/assets/...` link, and pasting that into a page is fine. The next local build downloads the file to `assets/attachments/gh-.` and substitutes that path while rendering, so the published HTML, the offline mirror and the PDF all point at the local copy. Your markdown keeps the remote URL, so commit the downloaded file with your edit: it is the only copy the site has. Any *other* remote host has no such handling --- download the image, commit it under the section's `Images/` folder, and reference that path. -There are three reasons, in increasing order of severity: +A remote image does harm in three ways, in increasing order of severity: - **Every page view pays a network round trip.** A GitHub attachment URL answers with a redirect to S3, so a single image costs two requests --- up to about a second on a cold connection. -- **The offline mirror stops being self-contained.** `_site-offline/` is meant to be browsable from a `file://` URL with no network at all. A remote image renders as a broken placeholder there. -- **The PDF book fails to build.** The forked paged.js in `book/lib/` dropped support for loading images asynchronously, so an image that has not finished downloading by the time the page-breaking pass runs raises an error and aborts the whole render. A remote image makes `book.bat` depend on a reachable third-party host; when that host is slow, blocked, or has expired the asset, the build does not degrade to a missing picture --- it stops. +- **The offline mirror stops being self-contained.** `_site-offline/` is meant to be browsable from a `file://` URL with no network. A remote image renders as a broken placeholder there. +- **The PDF book fails to build.** The forked paged.js in `book/lib/` cannot load images asynchronously, so an image still downloading when the page-breaking pass runs aborts the whole render. When the host is slow, blocked, or has expired the asset, `book.bat` stops. [`build.bat`](Building#checking-link-integrity) enforces this. A remote `` --- `http://`, `https://`, or protocol-relative `//host/...` --- is reported as `remote-asset:` and fails the run. ### A figure's buttons -An image that stands alone in its paragraph is shown as a *figure*: the image with a bar of buttons to download it, copy it and zoom it. That holds for a diagram, a picture in both themes and any other PNG, JPEG, GIF or WebP, with no markup of yours. An image inside a sentence, or inside a link, is a plain image and gets no bar, so put an image you want to be zoomable on a line of its own. +An image that stands alone in its paragraph is shown as a *figure*: the image with a bar of buttons to download it, copy it and zoom it. That holds for a diagram, a picture in both themes and any other PNG, JPEG, GIF or WebP. An image inside a sentence, or inside a link, is a plain image with no bar, so put an image you want to be zoomable on a line of its own. -Give a picture its `{:width="..." height="..."}`. The build uses the size to decide where the bar goes: a figure at least as tall as the bar stood on end gets the bar beside it, in the margin of a wide window and in view while the figure scrolls past, and a shorter figure gets the bar above it or at its top right. A picture with no stated size is treated as short: its bar stays above it, however tall it is. The decision is made in the build, so nothing on the page moves after it loads. +Give a picture its `{:width="..." height="..."}`. A figure at least as tall as the bar stood on end gets the bar beside it, in the margin of a wide window and in view while the figure scrolls past. A shorter figure gets the bar above it or at its top right. A picture with no stated size is treated as short, however tall it is. ### A picture in both themes -A screenshot of the IDE shows one of its themes, and the site has two. To give a picture a version for the site's light theme, save it beside the first one under the same name with `.light` before the extension: +A screenshot of the IDE shows one of its themes, and the site has two. To give a picture a version for the light theme, save it beside the first one under the same name with `.light` before the extension: ```text docs/IDE/Images/Watches.png <-- the dark picture: the page names this one docs/IDE/Images/Watches.light.png <-- the light picture: the page never names it ``` -Keep the page as it is, naming `Watches.png` with its `{:width height}`. The build finds the light picture by its name and puts both into the page: a reader sees the light one in the light theme and the dark one in the dark theme, whether the theme was chosen with the toggle or follows the operating system, and the browser downloads only the one it shows. The PDF book has the light picture only. +The page keeps naming `Watches.png` with its `{:width height}`. The build finds the light picture by its name and puts both into the page: a reader sees the light one in the light theme and the dark one in the dark theme, whether the theme was chosen with the toggle or follows the operating system, and the browser downloads only the one it shows. The PDF book has the light picture only. -Three things to get right: +Three rules: -- **Never name the `.light.png` in a page**, in an image, a link or raw HTML. The build stops and says which page did it, because that picture would then show in both themes. -- **Take the two pictures at the same scale.** The width and height in the page are for the dark picture, and the build shows the light one at the same scale: a light picture 8 pixels narrower is shown 4 pixels narrower when the page halves the dark one. The IDE's light theme lays some things out a few pixels smaller, so the two need not be exactly the same size. -- **A `.light.png` needs its dark picture beside it.** One without stops the build, because it would never be shown. +- **Never name the `.light.png` in a page**, in an image, a link or raw HTML. The build stops and names the page, because that picture would then show in both themes. +- **Take the two pictures at the same scale.** The width and height in the page are for the dark picture, and the build shows the light one at the same scale: a light picture 8 pixels narrower is shown 4 pixels narrower when the page halves the dark one. The two need not be exactly the same size, because the IDE's light theme lays some things out a few pixels smaller. +- **A `.light.png` needs its dark picture beside it.** One without stops the build. -The IDE's own screenshots come from `scripts/shoot_docs.mjs`, which takes each one in both themes and writes both files; it keeps no light picture that looks exactly like the dark one. Describe a picture in words that are true in both themes: its alt text and the prose around it are the same for both. "The Run button" is true of both, while "the white Run button" is not. +The IDE's own screenshots come from `scripts/shoot_docs.mjs`, which takes each one in both themes and writes both files; it keeps no light picture that looks exactly like the dark one. The alt text and the prose around a picture serve both themes, so describe it in words true in both: "The Run button", not "the white Run button". ### A picture as SVG @@ -900,11 +802,11 @@ docs/IDE/Images/Watches.light.png docs/IDE/Images/Watches.light.svg <-- shown in the light theme ``` -`scripts/shoot_docs.mjs --svg` draws each SVG from the IDE's page at the moment it takes the PNG. Its text is real text, in the fonts the IDE drew it with, so the picture stays sharp at any zoom, and in the PDF book its text can be selected and searched. The page still names `Watches.png`: the build shows the SVG in its place wherever there is one, in each theme. +`scripts/shoot_docs.mjs --svg` draws each SVG from the IDE's page at the moment it takes the PNG. Its text is real text, in the fonts the IDE drew it with, so the picture stays sharp at any zoom, and in the PDF book its text can be selected and searched. The page still names `Watches.png`, and the build shows the SVG in its place in each theme. -- **Name the PNG, never the SVG.** The PNG stays in the repository. The SVG is checked against it when it is made, and an output that shows PNGs uses it. Which outputs show the SVGs is set by `picture_svg` in `docs/_config.yml`: the website and the PDF book do, and the offline copy and the IDE help add-in's archive show the PNGs. The SVGs would make that archive half as large again. +- **Name the PNG, never the SVG.** The PNG stays in the repository, and an output that shows PNGs uses it. The `picture_svg` setting in `docs/_config.yml` decides which outputs show the SVGs: the website and the PDF book do, and the offline copy and the IDE help add-in's archive show the PNGs (the SVGs would make that archive half as large again). - **To keep one picture's PNG everywhere**, put `png` in its attribute block: `![The Watches panel](Images/Watches.png){:width="190" height="337" png}`. The build takes the word out of the page. -- **Do not edit a picture's SVG.** Take the picture again. `shoot_docs` writes an SVG only when it matches its PNG closely. One that has more than 0.25% of its pixels far from the PNG's is not written, and an older one is removed, so the page shows the PNG instead. +- **Do not edit a picture's SVG.** Take the picture again. `shoot_docs` writes an SVG only when it matches its PNG closely: if more than 0.25% of its pixels are far from the PNG's, it is not written, an older one is removed, and the page shows the PNG. ### Videos @@ -916,23 +818,23 @@ Link the video and mark it `.video`: The build renders that as a poster frame with a play button, linking out to the video page. The link text becomes the image's accessible name, so write the real title. -The poster frame is downloaded once to `assets/thumbnails/yt-.jpg` and committed; the build never re-fetches a thumbnail it already has. Do **not** hotlink `img.youtube.com` --- that reintroduces the third-party request, and [`build.bat`](Building#checking-link-integrity) fails the run for it, exactly as it does for any other remote ``. +The poster frame is downloaded once to `assets/thumbnails/yt-.jpg` and committed; the build never re-fetches it. Do **not** hotlink `img.youtube.com`: [`build.bat`](Building#checking-link-integrity) fails the run, as it does for any remote ``. -This is why the pages do not embed YouTube players. An `