' - // | remove '
' | strip. Inline render via the markdown-it - // instance Phase 3 stashed on site.markdown. - const md = site.markdown; - if (!md) { - throw new Error("Phase 8: site.markdown is unset; Phase 3 didn't run before assembleBook."); - } - const renderedSubtitle = md.render(String(part.subtitle)).replaceAll("", "").replaceAll("
", "").trim(); - out += `\n${renderedSubtitle}
`; + const { subtitleHtml, introHtml } = part._rendered ?? renderPartText(part, site.markdown); + if (subtitleHtml !== undefined) out += `\n${subtitleHtml}
`; + if (introHtml !== undefined) out += `\n' | remove '
' | strip; the intro is rendered +// whole. Through the markdown-it instance Phase 3 stashed on site.markdown. +function renderPartText(part, md) { + const out = {}; + if (!part.subtitle && !part.intro) return out; + if (!md) { + throw new Error("Phase 8: site.markdown is unset; Phase 3 didn't run before assembleBook."); } - if (part.intro) { - const md = site.markdown; - if (!md) { - throw new Error("Phase 8: site.markdown is unset; Phase 3 didn't run before assembleBook."); - } - out += `\n", "").replaceAll("
", "").trim(); } - out += `\n`; + if (part.intro) out.introHtml = md.render(String(part.intro)); return out; } +// Renders every part's subtitle and intro into `part._rendered`, for an +// assembly that has no markdown-it instance: the book is assembled on a +// worker, and markdown-it cannot cross the thread boundary. +export function prerenderPartText(bookData, md) { + for (const part of bookData?.parts ?? []) part._rendered = renderPartText(part, md); +} + // PLAN-8 §6.9: chapter divider matches book.html lines 218-227. function renderChapterDivider(chEntry) { const dividerId = chapterDividerId(chEntry); diff --git a/builder/counts.mjs b/builder/counts.mjs index 44863915e..68a1d11f9 100644 --- a/builder/counts.mjs +++ b/builder/counts.mjs @@ -62,11 +62,39 @@ // // A name with no uses is fine and is not reported. Names are cheap; the // registry is allowed to offer more than the prose currently asks for. +// +// ------------------------------------------------------ the download pages +// +// Two placeholders are not numbers from `deriveCounts` but text the Downloads +// page needs, and both sit where the inline rule cannot reach: in a raw HTML +// block, and as a line of a markdown table. +// +// {{tbdocs:size:... and ...atomically in the leading // alternation (code-guard.mjs), so a placeholder the documentation is // deliberately SHOWING does not trip the assertion. const SURVIVING_PLACEHOLDER_RE = new RegExp( - String.raw`${CODE_OR_PRE.source}|(\{\{tbdocs:[A-Za-z][A-Za-z0-9]*\}\})`, + String.raw`${CODE_OR_PRE.source}|(\{\{tbdocs:[A-Za-z][A-Za-z0-9]*(?::[^\s{}]+)?\}\})`, "g", ); diff --git a/builder/cpu-worker.mjs b/builder/cpu-worker.mjs index 4e794489d..47c623ea2 100644 --- a/builder/cpu-worker.mjs +++ b/builder/cpu-worker.mjs @@ -9,6 +9,10 @@ import { parentPort, workerData } from "node:worker_threads"; import { compileLightScss, compileDarkScss } from "./scss.mjs"; import { regenerateDot } from "./dot.mjs"; import { captureBuildInfo } from "./build-info.mjs"; +import { helpArchiveStep } from "./help-archive-step.mjs"; +import { writePdf } from "./pdf.mjs"; +import { unpackBookPayload } from "./book-payload.mjs"; +import { REPO_ROOT } from "../lib/repo-paths.mjs"; import { createMarkdownIt, renderPhase } from "./render.mjs"; import { renderFullNav, templatePhase, withPartialNav } from "./template.mjs"; @@ -47,6 +51,16 @@ let ctx = null; // { srcRoot, destRoot, opts, workerCount } let _payloadSAB = null; // SharedArrayBuffer with packed per-task payloads let _sharedSAB = null; // SharedArrayBuffer with packed shared payload +let _shared = null; // _sharedSAB decoded, once per lane + +// What dispatch broadcast. A task that reads it runs after dispatch, so the +// SAB is here by then; renderEnvInit, which can be claimed earlier, waits +// for it first. +function sharedData() { + if (!_sharedSAB) throw new Error("the shared payload was read before dispatch broadcast it"); + _shared ??= unpackShared(_sharedSAB); + return _shared; +} // ── Handler table ─────────────────────────────────────────────────────────── @@ -78,7 +92,8 @@ const handlers = { vendoredImagesObj, pictureSizes, counts, - } = unpackShared(_sharedSAB); + downloads, + } = sharedData(); const { initHighlighter } = await import("./highlight.mjs"); const highlighter = await initHighlighter(); @@ -97,6 +112,7 @@ const handlers = { vendoredVideos, vendoredImages, counts, + downloads, }); const site = { ...siteData, markdown, buildInfo }; @@ -199,6 +215,55 @@ const handlers = { return { buildInfo }; }, + // On a worker because it is the main thread's longest task after the + // pages are written, and it needs nothing from the main thread that + // dispatch has not already broadcast: it reads the offline tree from disk. + async helpArchive() { + const { skipOffline } = sharedData(); + return helpArchiveStep({ + src: path.relative(REPO_ROOT, ctx.srcRoot).replaceAll(path.sep, "/"), + offlineRoot: skipOffline ? null : `${ctx.destRoot}-offline`, + serve: !!ctx.opts.serve, + dryRun: !!ctx.opts.dryRun, + disabled: !!ctx.opts.skipHelpArchive, + }); + }, + + // The PDF tree, from what bookPayload packed on the main thread. The book + // itself stays on this lane for checkBook rather than crossing back. + async writePdf() { + const book = unpackBookPayload(await taskData("book")); + _bookCheck = null; + if (!book) return null; + const { checkBook, ...counters } = await writePdf(book.pages, book.staticFiles, book.site, ctx.destRoot, { + tolerateMissingImages: ctx.opts.tolerateMissingImages, + highlightCss: book.highlightCss, + check: book.check, + }); + _bookCheck = checkBook ?? null; + return counters; + }, + + // The book is one 6.5 MB document with almost entirely internal + // fragments, written by a different task from a different string. + // Keeping it its own pass is cheaper than special-casing the chunk + // path for a single file. + async checkBook() { + if (!_bookCheck) return null; + const { html, rels } = _bookCheck; + _bookCheck = null; + const { checkChunk, joinChunks, treeIndexFor, TREES } = await import("./check.mjs"); + const root = ctx.destRoot + TREES.pdf.suffix; + const env = { root, tree: TREES.pdf, basePath: "", index: treeIndexFor(root, rels) }; + let chunk; + try { + chunk = checkChunk([{ destPath: "book.html", html }], env); + } catch (err) { + chunk = { error: `book check failed: ${err.message}` }; + } + return joinChunks([chunk], { root, tree: TREES.pdf, relFiles: ["book.html"] }); + }, + async render(taskIdx) { const offset = Atomics.load(views.payloadOffset, taskIdx); const length = Atomics.load(views.payloadLength, taskIdx); @@ -278,6 +343,16 @@ for (const [name, id] of Object.entries(HANDLERS)) handlerById[id] = handlers[na let _renderEnv = null; let _pendingFlush = []; +let _taskData = new Map(); // key → SAB a main-thread task broadcast (broadcastTaskData) +let _bookCheck = null; // writePdf's book for checkBook, which is pinned to the same lane + +// The data a main-thread predecessor broadcast for this task. The message +// is queued before the predecessor is marked done, but this lane may still +// be inside the pull loop that claimed the task, so yield until it lands. +async function taskData(key) { + while (!_taskData.has(key)) await new Promise((resolve) => setImmediate(resolve)); + return _taskData.get(key); +} let _checkEnv = null; // set by renderEnvInit when --check is on let _checkChunk = null; // builder/check.mjs's checkChunk, imported with it @@ -315,7 +390,7 @@ async function runChunkCheck(items) { return out; } -// ── Message handler (init + dynamicData only) ─────────────────────────────── +// ── Message handler (init, dynamicData, taskData) ─────────────────────────── parentPort.on("message", (msg) => { if (msg.init) { @@ -323,15 +398,23 @@ parentPort.on("message", (msg) => { ctx = msg.ctx; _payloadSAB = null; _sharedSAB = null; + _shared = null; _renderEnv = null; _checkEnv = null; _pendingFlush = []; + _taskData = new Map(); + _bookCheck = null; pullLoop(); return; } if (msg.dynamicData) { _payloadSAB = msg.payloadSAB; _sharedSAB = msg.sharedSAB; + _shared = null; + return; + } + if (msg.taskData) { + _taskData.set(msg.taskData, msg.sab); return; } }); diff --git a/builder/gantt.mjs b/builder/gantt.mjs index 190071b84..31360fcfb 100644 --- a/builder/gantt.mjs +++ b/builder/gantt.mjs @@ -43,11 +43,11 @@ export const GANTT_SECTION = { highlighterInit: "Seeds", loadData: "Seeds", discover: "Spine", vendorAssets: "Spine", nav: "Spine", markdownInit: "Spine", buildInit: "Spine", resolveBookChapters: "Spine", - deriveRedirects: "Spine", deriveSitemap: "Spine", + deriveRedirects: "Spine", deriveSitemap: "Spine", packTests: "Spine", dispatch: "Render", prepDest: "Render", prepPageDirs: "Render", renderJoin: "Render", flushJoin: "Write", writeAssets: "Write", searchData: "Write", symbolIndex: "Write", helpArchive: "Write", addinProject: "Write", packageTests: "Write", writeAux: "Write", - writeOffline: "Write", writePdf: "Write", + writeOffline: "Write", bookPayload: "Write", writePdf: "Write", linkJoin: "Check", checkBook: "Check", checkReport: "Check", }; const GANTT_SECTION_ORDER = ["Seeds", "Spine", "Render", "Write", "Check"]; diff --git a/builder/help-archive-step.mjs b/builder/help-archive-step.mjs index a6bcc88f1..882b73281 100644 --- a/builder/help-archive-step.mjs +++ b/builder/help-archive-step.mjs @@ -4,13 +4,20 @@ // gitignored, so a local build is what produces it. The add-in's project file, // the download, carries the same bytes (addin-project.mjs). // +// The archive is the offline tree less HELP_ARCHIVE_SKIP: what nobody looks up +// from the IDE, and the files readers download, which stay on the website. The +// pages left out are still linked from every page's navigation, from the +// search and from prose; the add-in's server answers a request for a file the +// archive lacks with a redirect to the website. +// // `helpArchiveStep` is the build's `helpArchive` task. It waits for every task // that writes into the offline tree, and runs before the Gantt injection, which // rewrites BuildInfo.html in both trees after the task graph is done and -// writes assets/images/gantt.svg. So the archive holds BuildInfo.html without -// the chart, and gantt.svg as the placeholder the offline tree copied from the -// sources. Two builds of one tree write the same archive, and the same project -// file with it, which the chart (it holds the build's timings) would prevent. +// writes assets/images/gantt.svg. Both are left out of the archive; were they +// not, the archive would hold BuildInfo.html without the chart, and gantt.svg +// as the placeholder the offline tree copied from the sources. Two builds of +// one tree write the same archive, and the same project file with it, which +// the chart (it holds the build's timings) would prevent. // // `recheckHelpArchive` runs at the end of runBuild, after the Gantt injection, // and checks that the offline tree on disk still lists the archive's files and @@ -26,13 +33,75 @@ // download published from CI's build holds the archive. The step does not look // at $CI. +import { readFile } from "node:fs/promises"; import path from "node:path"; import { checkHelpArchive, writeHelpArchive } from "../lib/help-archive.mjs"; import { REPO_ROOT } from "../lib/repo-paths.mjs"; import { GUARDED_SRC } from "./baseline.mjs"; +import { offlineExcluded } from "./offline-rewrite.mjs"; export const HELP_ARCHIVE_REL = "add-in/Resources/HELP/site.zip"; +// What the archive leaves out of the offline tree: fnmatch patterns with +// PATHNAME semantics, as offline_exclude in _config.yml (`*` stays within one +// folder, `**` crosses them), against each file's path in the tree. +export const HELP_ARCHIVE_SKIP = [ + // The documentation's own development: its tools, build and authoring, and + // BuildInfo. Its pictures are in Documentation/Images. + "Documentation/**", + // BuildInfo's chart. + "assets/images/gantt.svg", + // Links to videos, which YouTube does not let the help pane show in its + // frame, and their poster frames, which only these pages and the + // documentation's own show. + "Videos.html", + "Videos/**", + "assets/thumbnails/**", + // Contests that are over. + "Challenges.html", + "Challenges/**", + // The files readers download: they stay on the website. Each is in a + // `downloads` folder beside the page that documents it, and the step + // refuses an archive that holds a file the Downloads page offers. + "**/downloads/**", +]; + +/** True for a file of the offline tree (relative, forward slashes) that the archive leaves out. */ +export const helpArchiveSkips = (name) => offlineExcluded(name, HELP_ARCHIVE_SKIP); + +// The page that offers every file the documentation publishes for readers. +const DOWNLOADS_PAGE = "Downloads.html"; + +/** + * The files of the offline tree that the Downloads page offers (a same-site + * link with the `download` attribute) and `skip` keeps in the archive. + * + * @param {string} offlineRoot the offline tree + * @param {(name: string) => boolean} skip + * @returns {Promise
` and ``, for a `{{tbdocs:...}}` placeholder that survived rendering; returns the first or `null`. `renderPhase` in `render.mjs` calls it after each page renders and throws on a survivor. |
@@ -895,7 +937,7 @@ For **renderer rules**, order inverts. Both image plugins capture the current `m
| Symbol | Signature | Description |
|---|---|---|
| `addinProjectOf` | `(config) → { src, dest } \| null` | The download `_config.yml`'s `addin_project` declares, `dest` without a leading slash; `null` unless both ends are given. |
-| `packAddinProject` | `({ dir, symbols }) → Promise<{ bytes, files, untracked }>` | Packs the files `git ls-files` lists in `dir`, as the working folder holds them, less `Resources/HELP/`, with CRLF made LF outside `Resources/`, and `symbols` in place of `Resources/SYMBOLS/symbols.json` when given. Stages them in a temporary folder and packs it with `importProject` from `scripts/impexp.mjs`. `untracked` lists the files git does not track that were left out. Throws when git fails or tracks no `Settings`. |
+| `packAddinProject` | `({ dir, symbols }) → Promise<{ bytes, files, untracked }>` | Packs the files `git ls-files` lists in `dir`, as the working folder holds them, less `Resources/HELP/`, with CRLF made LF outside `Resources/`, and `symbols` in place of `Resources/SYMBOLS/symbols.json` when given. Packs them in memory with `packFiles` from `scripts/impexp.mjs`, which gives the bytes `importProject` would give for the same files in a folder. `untracked` lists the files git does not track that were left out. Throws when git fails or tracks no `Settings`. |
| `writeAddinProject` | `({ dir, symbols, roots, rel }) → Promise<{ rel, bytes, files, untracked, roots, ms }>` | Packs once and writes the file at `rel` in each of `roots`. |
| `formatAddinProject` | `(result, dirLabel, indent) → string` | The line `runBuild` prints, and a second naming any untracked file left out. |
@@ -937,7 +979,7 @@ Pure-compute rewrite helpers, split from `offline.mjs` so `cpu-worker.mjs` can i
| Symbol | Signature | Description |
|---|---|---|
-| `writePdf` | `(pages, staticFiles, site, destRoot, { tolerateMissingImages?, highlightCss }) → Promise` | Writes the `_site-pdf/` source tree: `book.html` + `tb-highlight.css` + `print.css` + referenced images. |
+| `writePdf` | `(pages, staticFiles, site, destRoot, { tolerateMissingImages?, highlightCss, check? }) → Promise` | Writes the `_site-pdf/` source tree: `book.html` + `tb-highlight.css` + `print.css` + referenced images. Runs on a worker, on the arguments `unpackBookPayload` returns. With `check` the stats also hold the book HTML and the tree's file list, which the worker keeps for `checkBook`. |
### `scheduler.mjs`
@@ -950,7 +992,7 @@ Pure-compute rewrite helpers, split from `offline.mjs` so `cpu-worker.mjs` can i
| Symbol | Signature | Description |
|---|---|---|
-| `WorkerPool` | class | Constructor: `(size, workerUrl)`. Methods: `sendInit(sab, ctx)` (broadcasts the init message; increments `_buildCount`), `broadcastDynamicData(payloadSAB, sharedSAB)`, `destroy()`. Public fields: `bootTimings[]`, `_buildCount` (read by `runBuild` to detect serve-mode rebuilds). The caller sets the callbacks (`onWorkerDone`, `onWorkerError`, `onPerWorkerTiming`, `onMainTaskReady`). |
+| `WorkerPool` | class | Constructor: `(size, workerUrl)`. Methods: `sendInit(sab, ctx)` (broadcasts the init message; increments `_buildCount`), `broadcastDynamicData(payloadSAB, sharedSAB)`, `broadcastTaskData(key, sab)` (posts one main-thread task's output to every lane, for the worker task that follows it; the handler reads it with `taskData(key)`), `destroy()`. Public fields: `bootTimings[]`, `_buildCount` (read by `runBuild` to detect serve-mode rebuilds). The caller sets the callbacks (`onWorkerDone`, `onWorkerError`, `onPerWorkerTiming`, `onMainTaskReady`). |
### `cpu-worker.mjs`
@@ -967,6 +1009,9 @@ The handler table is built from the imported `HANDLERS` constant:
| `dot` | `dot.mjs` | `regenerateDot` over `srcRoot`. |
| `buildInfo` | `build-info.mjs` | `captureBuildInfo`. |
| `render` | inline | Five-stage chunk render: `renderPhase` → `computeChunkSeo` → `templatePhase` → offline → `deriveSearchEntries`. |
+| `helpArchive` | `help-archive-step.mjs` | `helpArchiveStep` over the offline tree; reads `skipOffline` from the shared payload through `sharedData()`. |
+| `writePdf` | `pdf.mjs` | Waits for the `"book"` task data, unpacks it with `unpackBookPayload`, writes the PDF tree and keeps the book for `checkBook`. |
+| `checkBook` | inline | `checkChunk` over the book `writePdf` kept on this lane; `null` without `--check`. |
### `sab-scheduler.mjs`
diff --git a/docs/Documentation/Tools-Build.md b/docs/Documentation/Tools-Build.md
index 157b45bd9..2e8849c6f 100644
--- a/docs/Documentation/Tools-Build.md
+++ b/docs/Documentation/Tools-Build.md
@@ -106,15 +106,17 @@ A `.twinproj` is not a publishable type. The [publish allowlist](Tools-Toolchain
### The help archive
{: #the-help-archive }
-Every build writes the zip the IDE help add-in serves the documentation from. The add-in embeds `add-in/Resources/HELP/site.zip` as a resource, and the build writes that file from the offline tree, before the build-time chart is added to `BuildInfo.html`. The archive therefore holds `BuildInfo.html` without the chart, and `assets/images/gantt.svg` as the placeholder the sources hold. The step prints one line:
+Every build writes the zip the IDE help add-in serves the documentation from. The add-in embeds `add-in/Resources/HELP/site.zip` as a resource, and the build writes that file from the offline tree, before the build-time chart is added to `BuildInfo.html`. The step prints one line:
- help archive: add-in/Resources/HELP/site.zip, 1475 entries, 26.0 MB (0.3 s)
+ help archive: add-in/Resources/HELP/site.zip, 2098 entries, 47.6 MB (0.7 s)
+
+The archive leaves out what nobody looks up from the IDE: this Documentation Development section (`BuildInfo.html` and its chart, `assets/images/gantt.svg`, among it), the Videos pages and their poster frames, and the Challenges. It also leaves out every file readers download, which stays on the website: each is in a `downloads` folder beside the page that documents it. `HELP_ARCHIVE_SKIP` in `builder/help-archive-step.mjs` lists them, as patterns in the form `offline_exclude` takes. Every page still links to them, from the navigation, the search and the text; the add-in's server answers a request for a file the archive lacks with a redirect to the same file on the website. A file the Downloads page offers that the archive would hold fails the build, which names it.
The zip is too large to commit; `add-in/Resources/HELP/` is in `.gitignore`.
Only a build of the documentation tree into `docs/_site-offline` writes the archive. Builds of another source tree or `--dest`, `--serve`, `--dry-run` and builds without an offline tree skip it, and so does `--no-help-archive`. CI must not pass `--no-help-archive`, or the project file it publishes would hold no archive; `check_ci_workflows.mjs` fails when one does.
-A file the build cannot write, or an archive that does not match the tree, fails the build with exit code 1. The build ends by checking the offline tree on disk against the archive again, now that the chart is in: the same files must be listed, and only `BuildInfo.html` and `assets/images/gantt.svg` may differ in content.
+A file the build cannot write, or an archive that does not match the tree, fails the build with exit code 1. The build ends by checking the offline tree on disk against the archive again, now that the chart is in: the same files must be listed, less the ones the archive leaves out, and only `BuildInfo.html` and `assets/images/gantt.svg` may differ in content.
The reader on the twinBASIC side does no inflating of its own, so the zip format is fixed:
diff --git a/docs/Documentation/Tools-Compiler.md b/docs/Documentation/Tools-Compiler.md
index 1558c5e85..fd0a32e05 100644
--- a/docs/Documentation/Tools-Compiler.md
+++ b/docs/Documentation/Tools-Compiler.md
@@ -300,6 +300,6 @@ No gate can build it, so `check_impexp_parity.mjs` compares it only when `IMPEXP
**None of them is run as a command by the site build.** They are published downloads: `_config.yml`'s `bundle_extra` copies the Node and Python scripts and the twinBASIC project file into `Features/Packages/downloads/` for [Import/Export Tool](../../Features/Packages/Import-Export-Tool). The `bundle_extra` exemption is by exact path, so moving any of the three files breaks the download; see [`check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy).
-The Node edition is also a library. `scripts/lib/tb-project.mjs` imports its `exportProject` and `importProject`, which every tool that unpacks or packs a project calls, among them [`tbbuild.mjs`](#tbbuild), [`tbrun.mjs`](#tbrun), [`check_examples.mjs`](Tools-Samples#check-examples), [`census_attributes.mjs`](Tools-Samples#census-attributes) and [`bug_repro.mjs`](Tools-Defects#bug-repro). The site build imports `importProject` too, to pack the [help add-in's project file](Tools-Build#the-help-add-ins-project-file). `impexp.mjs` is the one module under `scripts/` that `builder/` may import (`biome.jsonc` refuses any other), so [`check_tree_fresh.mjs`](Tools-Site-Checks#check-tree-fresh) watches it.
+The Node edition is also a library. `scripts/lib/tb-project.mjs` imports its `exportProject` and `importProject`, which every tool that unpacks or packs a project calls, among them [`tbbuild.mjs`](#tbbuild), [`tbrun.mjs`](#tbrun), [`check_examples.mjs`](Tools-Samples#check-examples), [`census_attributes.mjs`](Tools-Samples#census-attributes) and [`bug_repro.mjs`](Tools-Defects#bug-repro). The site build imports its `packFiles`, which packs a list of files held in memory as `importProject` packs the same files in a folder, to pack the [help add-in's project file](Tools-Build#the-help-add-ins-project-file) and the contributed packages' test projects. `impexp.mjs` is the one module under `scripts/` that `builder/` may import (`biome.jsonc` refuses any other), so [`check_tree_fresh.mjs`](Tools-Site-Checks#check-tree-fresh) watches it.
Exit codes: the table in [Import/Export Tool](../../Features/Packages/Import-Export-Tool#checking-the-result) gives every code. The three editions share these codes, which differ from those of the other tools here.
diff --git a/docs/Documentation/Tools-Site-Checks.md b/docs/Documentation/Tools-Site-Checks.md
index e5498f641..31aafbdbd 100644
--- a/docs/Documentation/Tools-Site-Checks.md
+++ b/docs/Documentation/Tools-Site-Checks.md
@@ -65,7 +65,7 @@ Exit codes: **0** every link is reachable and every anchor exists, **1** a link
node scripts/check_a11y.mjs [--root-dir ] [--theme light|dark|both] [--viewport desktop|mobile|both]
[--stock-axe] [--minified]
-Automated accessibility scan of the built site, and the second of `check.bat`'s four steps (right after [`check_tree_fresh.mjs`](#check-tree-fresh)). It loads `axe-core` into headless Chromium (via `puppeteer`) and runs it against fourteen sample pages in both themes at two viewports, plus two state audits that open a disclosure first --- 64 audits in all. The page list is derived, and [`scripts/pick_a11y_sample.mjs`](#pick-a11y-sample) keeps it representative.
+Automated accessibility scan of the built site, and the second of `check.bat`'s four steps (right after [`check_tree_fresh.mjs`](#check-tree-fresh)). It loads `axe-core` into headless Chromium (via `puppeteer`) and runs it against sixteen sample pages in both themes at two viewports, plus two state audits that open a disclosure first --- 72 audits in all. The page list is derived, and [`scripts/pick_a11y_sample.mjs`](#pick-a11y-sample) keeps it representative.
This script only reports. The scan itself --- page list, themes and viewports, blocked requests, axe run options, source patches and state audits --- lives in [`scripts/lib/axe-scan.mjs`](#axe-scan), shared with `sweep_a11y.mjs` and `check_a11y_fingerprint.mjs`. Change the scan there, not here.
@@ -94,7 +94,7 @@ Exit codes: **0** no page has a violation (incomplete checks are reported but do
node scripts/pick_a11y_sample.mjs [--check|--propose|--census] [--fresh]
-Derives the accessibility scan's page list, and checks that it still covers every construct the site uses. The scan reads fourteen pages out of ~1,160. If the list stops being representative, nothing fails: the rule for a construct no sample page carries never runs.
+Derives the accessibility scan's page list, and checks that it still covers every construct the site uses. The scan reads sixteen pages out of ~1,160. If the list stops being representative, nothing fails: the rule for a construct no sample page carries never runs.
The script holds a list of **construct families**: markup shapes some axe rule keys on, each recording that rule. The modes:
@@ -248,7 +248,7 @@ Each substitution inside a patch also asserts its target was found, so a bump th
[--limit N] [--resume] [--report] [--out FILE]
[--root-dir DIR] [--stock-axe] [--recycle-every N]
-The full-site accessibility sweep: every page, both themes, both viewports --- 3,476 audits, roughly 20 minutes. That is too slow for a commit gate, so the fourteen-page sample runs instead; but the sample reports only on constructs it carries.
+The full-site accessibility sweep: every page, both themes, both viewports --- 3,476 audits, roughly 20 minutes. That is too slow for a commit gate, so the sixteen-page sample runs instead; but the sample reports only on constructs it carries.
Run the sweep after any change that moves type metrics or page structure, and when adding a construct family to [`pick_a11y_sample.mjs`](#pick-a11y-sample). It audits disclosures **closed** only; open-state coverage is the sample scan's `STATE_AUDITS`.
diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md
index 0d2ca17cd..befb63ba6 100644
--- a/docs/Documentation/Tools-Toolchain-Tests.md
+++ b/docs/Documentation/Tools-Toolchain-Tests.md
@@ -81,6 +81,7 @@ The gate also covers `lib/markdown.mjs` and `lib/frontmatter.mjs`, the modules t
- the rewrite chain leaves alone a fence that only the site's parser finds, and an admonition written inside one;
- the build's check of `{{tbdocs:}}` count names skips names in code and gives an unknown one's line in its file;
+- the `{{tbdocs:size:}}` and `{{tbdocs:packageTestRows}}` placeholders are expanded outside code, in a raw HTML block and on a table line, and refused, at their line, for a path the build does not publish or cannot size;
- `discover` names the page and line of `title: Input #`, and says nothing about a quoted value;
- [`convert_em_dash_separators.mjs`](Tools-Build#convert-em-dash-separators), which rewrites page source by hand, converts prose and nothing else.
diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md
index 8c90eddcf..b7c208d2c 100644
--- a/docs/Documentation/Tools.md
+++ b/docs/Documentation/Tools.md
@@ -308,7 +308,7 @@ The build also reads these declarative files.
| File | Effect |
|---|---|
-| `docs/_config.yml` | Site config. `tbdocs` reads `url`, `baseurl`, `title`, `logo`, `also_build_offline`, `also_build_pdf`, `offline_exclude`, `exclude` (which filters the source walk but is **not** the publish safety net --- see [`check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy)), the footer / aux-link knobs, the GitHub edit-link knobs, and the download-link knobs (`gh_offline_link`, `gh_offline_link_url`, `gh_pdf_link_url`). Jekyll-only keys (`markdown`, `kramdown`, `theme`, `highlighter`, the `defaults` block, the `compress_html` block) are ignored. |
+| `docs/_config.yml` | Site config. `tbdocs` reads `url`, `baseurl`, `title`, `logo`, `also_build_offline`, `also_build_pdf`, `offline_exclude`, `exclude` (which filters the source walk but is **not** the publish safety net --- see [`check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy)), the footer / aux-link knobs, the GitHub edit-link knobs, and the footer's **Read Offline** link (`read_offline_link`). Jekyll-only keys (`markdown`, `kramdown`, `theme`, `highlighter`, the `defaults` block, the `compress_html` block) are ignored. |
| `docs/_book.yml` | The PDF book's chapter manifest. Entries are resolved to pages via the selector schema (`page` / `pages` / `nav_page` / `nav_pages` / `no_descent`) and control PDF outline behaviour via `landing_page:`, `landing_is_target:`, `no_outline_entry:`, `no_heading_shift:`, and `outline_closed:`. Its `left_out:` list names the pages not in the book, each with a `reason:`; the build warns about a page that is in neither. The file header documents the full schema. |
| `builder/themes/Light.theme`, `Dark.theme`, `Classic.theme` | twinBASIC IDE theme files, vendored from the BETA installer. `builder/highlight-theme.mjs` parses them into the palette behind the renderer's scope-to-class mapping and the generated `tb-highlight.css`. Refresh them from the installer when the IDE adds palette entries. |
| `builder/twinbasic.tmLanguage.json` | TextMate grammar for the twinBASIC language. Shiki uses it to tokenise every ` ```tb ` code block. |
diff --git a/docs/Downloads/index.md b/docs/Downloads/index.md
new file mode 100644
index 000000000..952050810
--- /dev/null
+++ b/docs/Downloads/index.md
@@ -0,0 +1,73 @@
+---
+title: Downloads
+nav_order: 4
+permalink: /Downloads
+---
+
+# Downloads
+
+Lists the tools, offline copies and test projects this documentation publishes, each with a link to its documentation.
+
+## Tools
+
+
+
+
+### Help add-in for the IDE
+
+Press F1 on a name in the code editor, and its reference page opens in a tool window beside the code, with or without an internet connection.
+
+
+
+
+
+
+
+
+### impexp
+
+Unpacks a `.twinproj` or `.twinpack` file into a folder of source files, and packs a folder back into a project file. The three editions take the same commands.
+
+
+
+impexp.mjs {{tbdocs:size:Features/Packages/downloads/impexp.mjs}} impexp.py {{tbdocs:size:Features/Packages/downloads/impexp.py}} impexp.twinproj {{tbdocs:size:Features/Packages/downloads/impexp.twinproj}} Documentation for impexp
+
+
+
+
+## Read offline
+
+Both are attached to the [documentation releases](https://github.com/twinbasic/documentation/releases) on GitHub.
+
+
+
+
+### The twinBASIC Book
+
+The whole site as one A4 PDF of about 2,400 pages, with bookmarks for every section. It suits a tablet or a printer better than a small e-reader.
+
+
+
+
+
+
+
+
+### Offline copy of the site
+
+Every page of this site, to read from disk in any browser with no network. Navigation, dark mode and search all work.
+
+
+
+
+
+
+
+
+## Package test projects
+
+Each contributed package that has test cases publishes them as a twinBASIC project. The project holds only the tests: it references the package, which goes in `%APPDATA%\twinBASIC\packages`. The package's landing page says what the cases check.
+
+| Package | Test project | Size |
+|---------|--------------|------|
+{{tbdocs:packageTestRows}}
diff --git a/docs/IDE/AddIns/Help.md b/docs/IDE/AddIns/Help.md
index d1062d3ef..df2576d43 100644
--- a/docs/IDE/AddIns/Help.md
+++ b/docs/IDE/AddIns/Help.md
@@ -113,7 +113,7 @@ The window is open again the next time the IDE starts, if it was open when the I
The add-in is a twinBASIC project, which the IDE builds into two DLLs, one for each bitness.
-1. Download the project file, tbDocsHelp.twinproj, about 28 MB. It is made from the add-in's source code each time this documentation is published. It holds the index of the symbols these pages document and a copy of the whole documentation.
+1. Download the project file, tbDocsHelp.twinproj, about 51 MB. It is made from the add-in's source code each time this documentation is published. It holds the index of the symbols these pages document and a copy of the whole documentation.
2. Open `tbDocsHelp.twinproj` in the twinBASIC IDE and build it. Then set the build target to **win64** and build it again. The two DLLs, `tbDocsHelp_win32.dll` and `tbDocsHelp_win64.dll`, are written to a `Build` folder next to the project file.
3. Copy each DLL to the matching `win32` or `win64` add-ins folder, as [Add Ins](../AddIns/) describes, and restart the IDE.
@@ -129,7 +129,7 @@ python scripts/impexp.py import tbDocsHelp.twinproj add-in
Then build `tbDocsHelp.twinproj` and copy the DLLs as in steps 2 and 3 above.
-**For help that works offline**, build the documentation before packing the folder, as [Building and Deployment](../../../Documentation/Development/Building) describes. The build writes a copy of the whole site into the add-in's source folder, `add-in\Resources\HELP\site.zip`, about 27 MB. A DLL built with that file in place serves the pages itself, without the internet. Without the file, the add-in shows the pages from the website.
+**For help that works offline**, build the documentation before packing the folder, as [Building and Deployment](../../../Documentation/Development/Building) describes. The build writes a copy of the site into the add-in's source folder, `add-in\Resources\HELP\site.zip`, about 50 MB. A DLL built with that file in place serves the pages itself, without the internet. The copy leaves out the pages about developing this documentation, the videos, the challenges and the files to download, which open from the website. Without the file, the add-in shows all the pages from the website.
## See Also
diff --git a/docs/IDE/Projects/Saving-and-Exporting.md b/docs/IDE/Projects/Saving-and-Exporting.md
index fa76fe65b..74b93d45c 100644
--- a/docs/IDE/Projects/Saving-and-Exporting.md
+++ b/docs/IDE/Projects/Saving-and-Exporting.md
@@ -86,7 +86,7 @@ The state is kept in the registry, in a value for each project path, under `Proj
## Open a project
- **File → New Project...** (Ctrl+N) and **File → Open Recent...** open the **New / Open Project** dialog, which has the tabs **New**, **Samples** and **Recent**. **Open Recent...** starts on the **Recent** tab. A **Recent (VB6)** tab shows too when the machine has a VB6 history. [New Project](../Project/New) describes the dialog.
-- **File → Open Project...** (Ctrl+O) opens the Windows **Open** dialog. A `.vbp` file is imported as a VB6 project; the result is unsaved. A project opened with **Import from folder...** is unsaved as well.
+- **File → Open Project...** (Ctrl+O) opens the Windows **Open** dialog. A `.vbp` file is imported as a VB6 project; the result is unsaved. The import keeps the `Attribute` lines of the VB6 files as they are, and the [FAQ](../../../FAQ#vb6-import) says what becomes of each file and each line. A project opened with **Import from folder...** is unsaved as well.
The project that is open is closed before the new one loads, and the question above comes at that point. In the **New / Open Project** dialog, it comes when you press **Open**, not before.
diff --git a/docs/Miscellaneous/FAQs.md b/docs/Miscellaneous/FAQs.md
index 105e52039..423692e97 100644
--- a/docs/Miscellaneous/FAQs.md
+++ b/docs/Miscellaneous/FAQs.md
@@ -248,6 +248,32 @@ You can import individual files, from VB projects or any type, through the Impor
{:width="468" height="397"}
Importing a .vbp also carries over its [startup object](tB/IDE/Project-Settings#startup-object) and its [icon form](tB/IDE/Project-Settings#icon-form). A VB6 project that was linked as a console program --- its `LinkSwitches` line contains `/SUBSYSTEM:CONSOLE`, written in capitals --- becomes a [console application](tB/IDE/Project-Settings#is-console-application). No other linker switch is carried over.
+
+**What happens to the `Attribute` lines.** The import keeps every `Attribute` line as VB6 wrote it, and turns none of them into a twinBASIC attribute such as `[Description]`. twinBASIC reads the lines itself when it compiles. **Import from VBP...** and **File → Open Project...** both import a project this way:
+
+- A `.bas` or `.cls` file is imported unchanged, because twinBASIC compiles these files as they are.
+- A `.frm`, `.ctl` or `.pag` file becomes two files. The designer file (`.tbform`, `.tbcontrol` or `.tbppage`) holds the properties from the file's `Begin` block. The `.twin` file holds a **Class** with the module's code, its `Attribute` lines included. The import puts the attributes `[FormDesignerId]`, `[ClassId]`, `[InterfaceId]` and `[EventInterfaceId]` in front of the class, with new GUIDs, and `[COMControl]` in front of a UserControl. None of them comes from an `Attribute` line: a form keeps its `Attribute VB_PredeclaredId = True` line and gets no `[PredeclaredId]`.
+- A UserControl's **PropertyPages** list, which VB6 keeps in the `.ctx` file, goes into the `PropertyPages` entry of the `.tbcontrol` file. The `VB_ProcData` lines that tie a property to a property page or a category stay in the class as they were.
+
+In a built ActiveX DLL, these lines have the same effect as the twinBASIC attribute beside them:
+
+| VB6 line | Effect in the type library | twinBASIC attribute |
+|---|---|---|
+| `VB_Description`, `VB_VarDescription` | The help string of the class, member, public field or event. | [`[Description]`](tB/Core/Attributes#description) |
+| `VB_UserMemId = 0` | The default member. | [`[DefaultMember]`](tB/Core/Attributes#defaultmember) |
+| `VB_UserMemId = -4` | The enumerator that **For Each** uses. | [`[Enumerator]`](tB/Core/Attributes#enumerator) |
+| `VB_UserMemId` with another value | The member's dispatch ID. | [`[DispId]`](tB/Core/Attributes#dispid) |
+| `VB_VarUserMemId` | The public field's dispatch ID. | None: `[DispId]` is not accepted on a field. |
+| `VB_MemberFlags = "40"` | The member is hidden. | [`[Hidden]`](tB/Core/Attributes#hidden) |
+| `VB_MemberFlags = "400"` | The member is nonbrowsable. | [`[NonBrowsable]`](tB/Core/Attributes#nonbrowsable) |
+| `VB_PredeclaredId = True` | The class has a predeclared instance. | [`[PredeclaredId]`](tB/Core/Attributes#predeclaredid) |
+
+`VB_Name` is the name of the module or class. A class with `VB_Exposed = False` is left out of the type library. A class with `VB_Creatable = False` is not marked creatable there, and a class with `VB_GlobalNameSpace = True` is marked as an application object (`appobject`).
+
+
+
+> [!WARNING] BETA 1005
+> Some `Attribute` lines have no effect, and nothing says so. `VB_HelpID` and `VB_VarHelpID` give no help context ID. `VB_VarMemberFlags` leaves a public field neither hidden nor nonbrowsable. `VB_MemberFlags = "200"` (the user-interface default) and `"14"` (the data-binding flags `bindable` and `displaybind`) set no flag. A component moved from VB6 loses these when it is built again. In a `.twin` file, `[Hidden]` on a public field does hide it.
diff --git a/docs/Reference/Attributes.md b/docs/Reference/Attributes.md
index 4d7f0cf1a..ef0541491 100644
--- a/docs/Reference/Attributes.md
+++ b/docs/Reference/Attributes.md
@@ -33,27 +33,46 @@ The available attributes are listed below in alphabetic order. Not every attribu
Syntax: **[AllowUnpopulatedVtableEntry]**
-Applicable to: [procedure](../Gloss#procedure) prototype in an [**Interface**](Interface)
+Applicable to: [procedure](../Gloss#procedure) prototype in an [**Interface**](Interface), [procedure](../Gloss#procedure) in a [**Class**](Class)
Marks a prototype that a class implementing the interface is not obliged to supply.
The twinBASIC packages use it to add members to an interface without breaking code written against an earlier revision of it. `ItbHostEventsV2` extends `ItbHostEventsV1` and adds one such prototype; `ItbHostEventsV3` extends that and adds another. An addin that implements only the **V1** members still satisfies **V3**.
+On a **Public** procedure of a **Class**, it does the same for a class that [**Implements**](Implements) that class: the member may be left out, where otherwise the build fails with TB65535, *Missing implementation of member ...*. On a property it covers only the accessor it is written on, so a **Property Get** and its **Property Let** each need it.
+
+ from that usage, not from a specification.
+ On a Class member (BETA 1005, tbbuild): a Class B that Implements Class A and
+ supplies only A's unmarked Sub compiles when A's other Sub carries the
+ attribute, and fails with TB65535 "Missing implementation of member Sub
+ OptionalOne()" without it; the same for a Function, an object-returning
+ Function and each property accessor separately (marking the Get alone still
+ reports the Let). A Friend Sub and an Event are never required, with or
+ without it. No trace in the type library. -->
## AppObject (optional Bool)
{: #appobject }
Syntax: **[AppObject** [ **( True** \| **False )** ] **]**
-Applicable to: [**CoClass**](CoClass)
+Applicable to: [**CoClass**](CoClass), [**Class**](Class)
Legacy VB attribute: *VB_GlobalNameSpace*
Indicates the class is part of the global namespace. You should not include this attribute without a full understanding of the meaning. The **Global** class is an AppObject.
+On a **Class**, the members of the class can be called by their bare names anywhere in the project. Those calls all reach one instance of the class, which is not the same as an instance created with [**New**](New). In an ActiveX build the class is marked `appobject` in the type library, so a client sees its members as global names too.
+
+
+
For more details, see [this VBA documentation page](https://learn.microsoft.com/en-us/openspecs/microsoft_general_purpose_programming_languages/ms-vbal/189fb41b-cc3a-4999-a6d2-ba89f72d2870).
## ArrayBoundsChecks (optional Bool)
@@ -70,12 +89,20 @@ Disables or enables array element access bounds checking within the scope of a c
Syntax: **[BindOnlyIfNoArguments** [ **( True** \| **False )** ] **]**
-Applicable to: [procedure](../Gloss#procedure)
+Applicable to: [**Declare** (API declaration)](Declare), variable in a [**Module**](Module), variable in a [**Class**](Class), [procedure](../Gloss#procedure)
Only binds this name to a callsite when no arguments are present. Normally false, but see below for an exception.
This attribute resolves the cases where compiler's special treatment of certain procedure names conflicts with a procedure of the same name that shouldn't be treated specially. This currently affects procedures named `Left`. Such procedures get an implicit `[BindOnlyIfNoArguments(True)]` assigned by the compiler. If the user wants to have a procedure of this name, it should include `[BindOnlyIfNoArguments(False)]`.
+A **Declare** takes the attribute as a procedure does, and a **Declare** named `Left` gets the implicit one too. On a variable named like a built-in procedure, a call with arguments binds to the built-in procedure, where it would otherwise be error TB5030, *Unexpected call arguments*.
+
+
+
## BindOnlyIfStringSuffix (optional Bool)
{: #bindonlyifstringsuffix }
@@ -151,7 +178,17 @@ For an overview of coclasses in tB, see [Defining coclasses](../../Features/Lang
Syntax: **[COMControl** [ **( True** \| **False )** ] **]**
-Applicable to: [**Interface**](Interface)
+Applicable to: [**Interface**](Interface), [**Class**](Class)
+
+On a **Class**, it marks the class as an ActiveX control (the `control` flag of its coclass in the type library). The compiler then warns, with TB0013, about a class that has no [InterfaceId](#interfaceid) or no [EventInterfaceId](#eventinterfaceid) attribute: a host may cache those identifiers, so they should be fixed rather than generated anew by each build.
+
+
## COMCreatable (optional Bool)
{: #comcreatable }
@@ -197,9 +234,13 @@ See also [DllExport](#dllexport).
Syntax: **[COMExtensible** [ **( True** \| **False )** ] **]**
-Applicable to: [**Interface**](Interface)
+Applicable to: [**Interface**](Interface), [**Class**](Class)
+
+Specifies whether new members added at runtime can be called by name through an interface implementing **IDispatch**. This attribute is set to **False** by default. On a **Class**, it applies to the interface the class exposes through COM.
-Specifies whether new members added at runtime can be called by name through an interface implementing **IDispatch**. This attribute is set to **False** by default.
+
## ComImport (optional Bool)
{: #comimport }
@@ -215,10 +256,17 @@ Specifies that an interface is an import from an external COM library, for insta
Syntax: **[CompileIf(** condition **)]**
-Applicable to: [procedure definitions](../Gloss#procedure)
+Applicable to: [**Declare** (API declaration)](Declare), [**Const**](Const), variable in a [**Module**](Module), variable in a [**Class**](Class), [**Event**](Event) declaration in a [**Class**](Class), [procedure definitions](../Gloss#procedure)
Controls the conditional compilation of a procedure definition. Has no default value.
+It controls a **Declare**, a **Const**, a variable or an **Event** in the same way: with a **False** condition the declaration is left out, so a use of its name is an error, such as TB5079, *Unrecognized symbol*.
+
+
+
## CompilerOptions (String)
{: #compileroptions }
@@ -238,20 +286,40 @@ Sets the LLVM compiler options for one procedure, in place of the **Compiler Opt
Syntax: **[ConstantFoldable** [ **( True** \| **False )** ] **]**
-Applicable to: [**Function**](Function) in a [**Module**](Module). The compiler rejects it on a method in a [**Class**](Class).
+Applicable to: [**Function**](Function) in a [**Module**](Module), [**Property Get**](Property) in a [**Module**](Module). The compiler rejects it on a method in a [**Class**](Class).
Specify this attribute for functions that, when called with non-variable input, can be computed at compile time rather than at runtime. For example, a function that converts a string literal to ANSI. The result never changes, so the resulting ANSI string is stored rather than recomputed on every run. Such functions are also called *pure functions*, because their output depends only on the arguments and not on the state of the program.
The restriction to modules is not an oversight. Folding a call to a method would require constant propagation through object state, and a notion of a constant object for the propagation to terminate on. twinBASIC's object model is dynamic enough to make both hard, so the compiler rejects the attribute there rather than folding a subset of cases that would be difficult to describe.
+Calls are folded only when **Constant Function Folding** is set to **Yes** in [Project Settings](../IDE/Project-Settings#constant-function-folding). A **Property Get** in a module folds as a function does. On a **Sub** the attribute is accepted and has no effect.
+
+> [!WARNING] BETA 1005
+> The compiler also accepts the attribute on a **Declare Function**. With **Constant Function Folding** on, a call to such a function whose arguments are all constants crashes the compiler during the build, though the project compiles without an error.
+
+
+
+
## ConstantFoldableNumericsOnly (optional Bool)
{: #constantfoldablenumericsonly }
Syntax: **[ConstantFoldableNumericsOnly** [ **( True** \| **False )** ] **]**
-Applicable to: [**Function**](Function) in a [**Module**](Module). The compiler rejects it on a method in a [**Class**](Class).
+Applicable to: [**Function**](Function) in a [**Module**](Module), [**Property Get**](Property) in a [**Module**](Module). The compiler rejects it on a method in a [**Class**](Class).
-A limited case of [constant foldable attribute](#constantfoldable), which applies only if the function was called with a numeric parameter. The restriction to modules is the same one [ConstantFoldable](#constantfoldable) carries, and for the same reason.
+A limited case of [constant foldable attribute](#constantfoldable), which applies only if the function was called with a numeric parameter. The restriction to modules is the same one [ConstantFoldable](#constantfoldable) carries, and for the same reason, and so are the Project Settings option it needs and the crash on a **Declare Function**.
+
+
## CustomControl (String)
{: #customcontrol }
@@ -297,18 +365,26 @@ The editor names the twinBASIC packages use, most-used first. The list is not ex
Syntax: **[Debuggable** [ **( True** \| **False )** ] **]**
-Applicable to: [**Module**](Module), [procedure in a **Class** or **Module**](../Gloss#procedure)
+Applicable to: [**Module**](Module), [**Class**](Class), [procedure in a **Class** or **Module**](../Gloss#procedure)
+
+When false, turns off breakpoints and stepping for the method, class or module. The default value is **True**. In a class or module marked `[Debuggable(False)]`, a [**Stop**](Stop) statement does not enter break mode.
-When false, turns of breakpoints and stepping for the method or module. The default value is **True**.
+
## DebugOnly (optional Bool)
{: #debugonly }
Syntax: **[DebugOnly** [ **( True** \| **False )** ] **]**
-Applicable to: [**Sub**](Sub)
+Applicable to: [**Sub**](Sub), [**Declare Sub**](Declare)
-Excludes calls to this procedure from the Build. They are only available when running from the IDE, i.e. debugging.
+Excludes calls to this procedure from the Build. They are only available when running from the IDE, i.e. debugging. On a **Declare Sub** the same holds for calls to the API procedure.
+
+
The attribute is accepted on a **Sub** in a **Module**, a **Class** or an **Interface**, and on a **Declare Sub**. On a **Function**, a **Property** or a **Declare Function** it is error **TB5162**, *[DebugOnly] is only allowed on Sub procedures*.
@@ -356,7 +432,7 @@ Marks the event a control nominates as its primary one. Each control of the VB p
Syntax: **[DefaultMember** [ **(** **True** \| **False** **)** ] **]**
-Applicable to: [procedure in a **Class**](../Gloss#procedure)
+Applicable to: [procedure in a **Class**](../Gloss#procedure), [procedure](../Gloss#procedure) prototype in an [**Interface**](Interface)
Default members are accessed under the instance of the object itself, without specifying their name. For example, a class that offers indexable elements may have an **Item** property that is the default member:
@@ -382,14 +458,29 @@ Module DefaultMemberDemo
End Module
```
+On a prototype in an **Interface**, the member gets the dispatch ID 0, so it is the default member of a variable declared as that interface, whichever class is behind it.
+
+
+
## Description (String)
{: #description }
Syntax: **[Description("** arbitrary text **")]**
-Applicable to: [**Class**](Class), [**CoClass**](CoClass), [**Const**](Const), [**Declare** (API declaration)](Declare), [**Interface**](Interface), [**Module**](Module), [procedure](../Gloss#procedure), [**Type** (UDT)](Type)
+Applicable to: [**Class**](Class), [**CoClass**](CoClass), [**Const**](Const), [**Declare** (API declaration)](Declare), [**Event**](Event) declaration in a [**Class**](Class), variable in a [**Class**](Class), [**Interface**](Interface), [**Module**](Module), [procedure](../Gloss#procedure), [**Type** (UDT)](Type)
+
+Provides a description in information popups in the IDE, and is exported as a `helpstring` attribute in the type library (if applicable). The type library holds one for each public procedure, property, event and variable of a class, and for each prototype of an interface; an **Enum**, a **Type**, a **Type** member and a parameter get none.
-Provides a description in information popups in the IDE, and is exported as a `helpstring` attribute in the type library (if applicable).
+
The value is a **String** whose content is Markdown. The IDE renders it when it displays the popup, so headings, code spans and fenced code blocks all work. The attribute takes a single string literal, so a description running to several lines is assembled with `& vbCrLf & _` continuations, one source line per line of Markdown.
@@ -428,9 +519,14 @@ Five details of that are easy to get wrong:
Syntax: **[DispId(** 123 **)]**
-Applicable to: [procedure in an Interface](../Gloss#procedure)
+Applicable to: [procedure in an Interface](../Gloss#procedure), [procedure in a **Class**](../Gloss#procedure), [**Event**](Event) declaration in a [**Class**](Class)
-Defines a dispatch ID associated with the procedure when exposed via **IDispatch**.
+Defines a dispatch ID associated with the procedure when exposed via **IDispatch**. In a **Class**, it sets the dispatch ID of the member in the interface the class exposes through COM, or, on an **Event**, in its event interface; the accessors of a property share one ID.
+
+
## DispInterface
{: #dispinterface }
@@ -476,9 +572,14 @@ The attribute takes no **Boolean**: `[DllExport(True)]` and `[DllExport(False)]`
Syntax: **[DLLStackCheck** [ **( True** \| **False)** ] **]**
-Applicable to: [**Declare** (API declaration)](Declare)
+Applicable to: [**Declare** (API declaration)](Declare), [**Delegate**](Delegate)
-Gives minor codegen size reduction on 32-bit API calls on the Intel platform. Has no effect on other platforms.
+Gives minor codegen size reduction on 32-bit API calls on the Intel platform. Has no effect on other platforms. On a **Delegate** it changes the code generated for calls through the delegate in the same way; the calls give the same results.
+
+
## DualInterface
{: #dualinterface }
@@ -500,18 +601,18 @@ See [**LibraryId**](#libraryid) for what a generated **Library** module looks li
Syntax: **[EnforceErrors** **(** **TBnnnn** [ **,** **TBmmmm** ]... **)** **]**
-Applicable to: [**Class**](Class), [**Module**](Module), [procedure](../Gloss#procedure)
+Applicable to: [**Class**](Class), [**Interface**](Interface), [**Module**](Module), [procedure](../Gloss#procedure)
-Reports the named warnings as errors within the class, module or procedure the attribute is applied to. The codes are written bare, as for [IgnoreWarnings](#ignorewarnings). With `[EnforceErrors(TB0009)]` on a procedure, a call to a procedure marked [Unimplemented](#unimplemented) is an error where it would otherwise be warning TB0009. Written bare it is accepted and has no effect. With **True** or **False** in place of a code it is error TB5169, *Expected an error code in the format of TBxxxx*.
+Reports the named warnings as errors within the class, interface, module or procedure the attribute is applied to. The codes are written bare, as for [IgnoreWarnings](#ignorewarnings). With `[EnforceErrors(TB0009)]` on a procedure, a call to a procedure marked [Unimplemented](#unimplemented) is an error where it would otherwise be warning TB0009. Written bare it is accepted and has no effect. With **True** or **False** in place of a code it is error TB5169, *Expected an error code in the format of TBxxxx*.
## EnforceWarnings (Warning code list)
{: #enforcewarnings }
Syntax: **[EnforceWarnings** **(** **TBnnnn** [ **,** **TBmmmm** ]... **)** **]**
-Applicable to: [**Class**](Class), [**Module**](Module), [procedure](../Gloss#procedure)
+Applicable to: [**Class**](Class), [**Interface**](Interface), [**Module**](Module), [procedure](../Gloss#procedure)
-Takes warning codes written bare, as [IgnoreWarnings](#ignorewarnings) does. A warning it names is still reported as a warning. With **True** or **False** in place of a code the attribute is TB5169, *Expected an error code in the format of TBxxxx*.
+Takes warning codes written bare, as [IgnoreWarnings](#ignorewarnings) does. A warning it names is reported as a warning, also one the compiler does not report by default: with `[EnforceWarnings(TB0018)]`, assigning a **Long** to an **Integer** gives warning TB0018, *implicit narrowing conversion*, which is otherwise not reported. With **True** or **False** in place of a code the attribute is TB5169, *Expected an error code in the format of TBxxxx*.
## Enumerator
{: #enumerator }
@@ -616,10 +717,12 @@ Applicable to: [**Class**](Class)
Syntax: **[Hidden** [ **(** **True** \| **False** **)** ] **]**
-Applicable to: [**Class**](Class), [**CoClass**](CoClass), [**Interface**](Interface), [**Module**](Module), a [procedure](../Gloss#procedure) in a Class or Module, a procedure in an Interface, and a variable in a Class
+Applicable to: [**Class**](Class), [**CoClass**](CoClass), [**Interface**](Interface), [**Module**](Module), a [procedure](../Gloss#procedure) in a Class or Module, a procedure in an Interface, variable in a Class, variable in a Module, [**Const**](Const), [**Event**](Event) declaration in a Class, [**Declare**](Declare)
Hides the declaration from certain IntelliSense and other lists. It applies to a whole type --- a **Class**, **CoClass**, **Interface** or **Module** --- and equally to a single member of one, so a member can be kept out of those lists without hiding the type that declares it. Within a **Class** that covers procedures, variables, constants and events; within an **Interface**, the member prototypes; within a **Module**, procedures, variables, constants and [**Declare**](Declare) statements.
+In the type library of an ActiveX build, it sets the `hidden` flag of each public member of a class and each prototype of an interface it is written on; on a property, the flag goes on every accessor, whichever one carries the attribute.
+
> [!NOTE]
> A **CoClass** can only be hidden whole. Its body holds nothing but **Interface** lines, and the attribute is refused there with TB5155 --- unlike [**Default**](#default) and [**Source**](#source), which are interface-line attributes. It is likewise refused on an **Enum** or [**Type**](Type) declaration, on a **Type** member, and on a procedure parameter. An **Enum** member cannot be hidden either: written before the member on the same line, the attribute is **TB5182**, and on a line of its own inside the **Enum**, `[Hidden]` declares a member named `Hidden` (see [**Enum**](Enum)).
@@ -640,7 +743,11 @@ Hides the declaration from certain IntelliSense and other lists. It applies to a
Type, a Delegate, Implements and Inherits. TB5182: a Type member, a
procedure parameter.
Note the census finds NO use on a whole Class, though the probe compiles
- -- absence of a use is not absence of applicability. -->
+ -- absence of a use is not absence of applicability.
+ Type library (BETA 1005, ActiveX DLL dumped with TLI): FUNCFLAG_FHIDDEN
+ (0x40) on class procedures, properties (both accessors), events, public
+ variables and interface prototypes; (False) is as without it. Module and
+ Private members never reach a type library. -->
## IdeButton (String)
@@ -655,9 +762,9 @@ Applicable to: [procedure](../Gloss#procedure) definition in a module.
Syntax: **[IgnoreWarnings** **(** **TBnnnn** [ **,** **TBmmmm** ]... **)** **]**
-Applicable to: [**Class**](Class), [**Module**](Module), [procedure](../Gloss#procedure)
+Applicable to: [**Class**](Class), [**Interface**](Interface), [**Module**](Module), [procedure](../Gloss#procedure)
-Suppresses the named warnings within the class, module or procedure the attribute is applied to. The codes are written bare, exactly as the compiler prints them, and are **not** quoted:
+Suppresses the named warnings within the class, interface, module or procedure the attribute is applied to. The codes are written bare, exactly as the compiler prints them, and are **not** quoted:
```tb check_build
[IgnoreWarnings(TB0001)]
@@ -734,7 +841,13 @@ Dividing the most negative **Integer**, **Long** or **LongLong** by -1 with [**\
Syntax: **[InterfaceId( "**00000000-0000-0000-0000-000000000000**" )]**
-Applicable to: [**Interface**](Interface)
+Applicable to: [**Interface**](Interface), [**Class**](Class)
+
+On a **Class**, it fixes the identifier of the interface the class exposes through COM, which is otherwise generated anew by each build. [EventInterfaceId](#eventinterfaceid) does the same for the class's event interface. A control class ([COMControl](#comcontrol)) should have both, and the compiler warns when it does not.
+
+
twinBASIC supports defining COM interfaces using BASIC syntax, rather than needing an type library with IDL and C++. These are only supported in .twin files, not in legacy .bas or .cls files. They must appear *before* the [**Class**](Class) or [**Module**](Module) statement, and will always have a project-wide scope. the The generic form for is as follows:
@@ -825,16 +938,21 @@ On a class the attribute is error **TB5155**, *This attribute is not supported i
Syntax: **[NonBrowsable** [ **( True** \| **False )** ] **]**
-Applicable to: variables and [procedures](../Gloss#procedure) in a [**Class**](Class)
+Applicable to: variable in a [**Class**](Class), [procedure in a **Class**](../Gloss#procedure), [**Event**](Event) declaration in a [**Class**](Class), [procedure](../Gloss#procedure) prototype in an [**Interface**](Interface)
Keeps a member out of the surfaces that list a class's members, while leaving it callable. The twinBASIC packages apply it to members that exist for the framework's own use, such as `InternalSectionId` and `hWndHeader`.
+In the type library of an ActiveX build, it sets the `nonbrowsable` flag of a public procedure, property or event of a class and of a prototype of an interface; on a property, the flag goes on every accessor. A public variable of a class gets no flag.
+
This is distinct from [Hidden](#hidden), which reaches the same member as well as the whole type, and from [Restricted](#restricted).
+ affects is not established.
+ Type library (BETA 1005, ActiveX DLL dumped with TLI): FUNCFLAG_FNONBROWSABLE
+ (0x400) on class procedures, properties (both accessors), events and
+ interface prototypes; a public class variable's Get/Put pair stays 0x0. -->
## OleAutomation (optional Bool)
{: #oleautomation }
@@ -845,6 +963,13 @@ Applicable to: [**Interface**](Interface)
Controls whether this attribute is applied in the typelibrary. This attribute is set to **True** by default.
+It changes only an interface that extends **IUnknown**. An interface that extends **IDispatch** is a dual interface, which is OLE Automation compatible whatever the attribute says.
+
+
+
## PackingAlignment (Integer)
{: #packingalignment index="packing alignment" }
@@ -930,6 +1055,16 @@ When set, a global instance of the class is created when the application starts.
This attribute is equivalent to the `VB_PredeclaredId` attribute in VBx .cls files.
+> [!NOTE] BETA 1005
+> In an ActiveX build, the class is marked `appobject` in the type library as well as `predeclid`, which is what [AppObject](#appobject) gives it, so a client that references the library treats the members of the class as global names.
+
+
+
## PreserveSig (optional Bool)
{: #preservesig }
@@ -954,6 +1089,52 @@ can be rewritten as
Public Declare PtrSafe Function SHGetDesktopFolder Lib "shell32" () As IShellFolder
```
+## PropertyPage (String)
+{: #propertypage }
+
+Syntax: **[PropertyPage("** page name **")]**
+
+Applicable to: variables and [procedures](../Gloss#procedure) in a [**Class**](Class)
+
+Legacy VB attribute: *VB_ProcData.VB_Invoke_Property*
+
+Ties a property of a [**UserControl**](../Packages/VB/UserControl/) to one of the project's property pages, so that a host that shows a property page for a single property (the **...** button of a property sheet) opens that one. The argument is the class name of the property page, as **Add Windows PropertyPage** created it, matched exactly, case included. The control reports the page through **IPerPropertyBrowsing.MapPropertyToPage**.
+
+```tb inert=designer
+[PropertyPage("CaptionPage")]
+Public Property Get Caption() As String
+ Return m_Caption
+End Property
+
+Public Property Let Caption(ByVal Value As String)
+ m_Caption = Value
+ PropertyChanged "Caption"
+End Property
+```
+
+A property takes the attribute on its first accessor in the source, normally the **Property Get**; written on a later accessor, it is ignored. The legacy line `Attribute Caption.VB_ProcData.VB_Invoke_Property = "CaptionPage"` inside the **Property Get** means the same. The argument must be a **String**: `[PropertyPage]` with no argument, or with a **Boolean** or a number, is TB5163, *Expected a single String argument*. An empty string ties the property to no page.
+
+> [!NOTE] BETA 1005
+> A name that matches no property page in the project is not reported: the build succeeds, and the property is tied to no page. The same holds for a name that differs only in case.
+
+> [!NOTE] BETA 1005
+> VB6 stores a property's page as `"PageName;Category"` in the legacy attribute, with the category the property sheet groups the property under. twinBASIC reads such a legacy line and drops what follows the semicolon. **[PropertyPage]** takes the whole string as the name, so `[PropertyPage("CaptionPage;Appearance")]` matches no page.
+
+The pages the control offers as a whole are listed in its [**PropertyPages**](../Packages/VB/UserControl/#propertypages) property.
+
+
+
## RedirectToStaticImplementation (String)
{: #redirecttostaticimplementation }
@@ -989,11 +1170,18 @@ End Interface
Syntax: **[Restricted** [ **( True** \| **False )** ] **]**
-Applicable to: [**Interface**](Interface)
+Applicable to: [**Interface**](Interface), [**Class**](Class), [procedure in a **Class**](../Gloss#procedure), variable in a [**Class**](Class), [**Event**](Event) declaration in a [**Class**](Class), [procedure](../Gloss#procedure) prototype in an [**Interface**](Interface)
Restricts the interface methods from being called in most contexts.
-This is attribute has the same function as the [**restricted** MIDL attribute][MIDL restricted].
+This is attribute has the same function as the [**restricted** MIDL attribute][MIDL restricted]. On a **Class**, it marks the class and the interface it exposes through COM as restricted in the type library. On a single member, it marks that member; on a property, only the accessor it is written on.
+
+
[MIDL restricted]: https://learn.microsoft.com/en-us/windows/win32/midl/restricted
@@ -1002,7 +1190,7 @@ This is attribute has the same function as the [**restricted** MIDL attribute][M
Syntax: **[RunAfterBuild** [ **( True** \| **False )** ] **]**
-Applicable to: [**Function**](Function), [**Sub**](Sub)
+Applicable to: [**Function**](Function), [**Sub**](Sub), [**Property Get**](Property) in a [**Module**](Module)
Specifies a function that runs after your exe is built. There's `App.LastBuildPath` to know where it is if you're e.g. signing the executable.
@@ -1013,15 +1201,35 @@ The procedure runs whatever the project's modules and procedures are called: a m
> [!NOTE]
> VB6 refuses a module that has the project's name, with *Name conflicts with existing module, project, or object library*. It allows a procedure named like its module.
+> [!WARNING] BETA 1005
+> The compiler also accepts the attribute on a generic **Sub**, such as `Sub Signed(Of T)()`, and counts it as the project's one **[RunAfterBuild]**. The build gives no error or warning, and the DEBUG CONSOLE reports the procedure as executed, but its body does not run.
+
+
+
## RunBeforeStartupObject
{: #runbeforestartupobject }
Syntax: **[RunBeforeStartupObject]**
-Applicable to: [**Function**](Function) in a [**Module**](Module), returning a **Boolean**
+Applicable to: [procedure](../Gloss#procedure) in a [**Module**](Module)
Runs the function before the project's startup object. Returning **True** suppresses the startup object entirely; returning **False** lets startup proceed as normal.
+A **Function** is the usual form, returning a **Boolean**. A **Function** of another type, or a **Property Get**, works the same way: a result of 0 or **Nothing** lets startup proceed, and any other result suppresses the startup object. A **Sub** runs too, and then the startup object never runs.
+
+> [!WARNING] BETA 1005
+> On a generic procedure, such as `Sub Pre(Of T)()`, the project compiles without an error, but building it crashes the compiler.
+
+
+
The CEF package uses it to intercept the sub-process launches Chromium makes of the host executable, which must not run the application's own `Sub Main`:
```tb check_build project=packages
@@ -1138,10 +1346,18 @@ Allows populating Intellisense with an enum for types other than **Long**.
Syntax: **[Unimplemented** [ **( True** \| **False )** ] **]**
-Applicable to: [procedure](../Gloss#procedure) definitions
+Applicable to: [**Declare** (API declaration)](Declare), variable in a [**Class**](Class), [procedure](../Gloss#procedure) definitions
Makes the compiler issue a warning about the procedure being unimplemented wherever it's called. You can upgrade it to an error too.
+On a **Declare** the warning, TB0009, comes at each call in the same way. On a **Public** variable of a class it comes at each use of the variable from outside the class; a use inside the class gives none.
+
+
+
## UseGetLastError (optional Bool)
{: #usegetlasterror }
diff --git a/docs/Reference/Built-In/WinNativeCommonCtls/ListView/index.md b/docs/Reference/Built-In/WinNativeCommonCtls/ListView/index.md
index c1120c082..e79438545 100644
--- a/docs/Reference/Built-In/WinNativeCommonCtls/ListView/index.md
+++ b/docs/Reference/Built-In/WinNativeCommonCtls/ListView/index.md
@@ -203,7 +203,10 @@ The [**ImageList**](../ImageList/) used for small icons in **lvwSmallIcon**, **l
### TextBackground
{: .no_toc }
-Whether item-label text has an opaque background. A member of [**ListTextBackgroundConstants**](#listtextbackgroundconstants). Default: **lvwTransparent**. Changing it repaints the list at once.
+Whether item-label text has an opaque background. A member of [**ListTextBackgroundConstants**](#listtextbackgroundconstants). Default: **lvwTransparent**. Changing it repaints the list at once. The opaque background is [**BackColor**](#backcolor), the color the list is already drawn in, so the two settings look the same.
+
+> [!NOTE] BETA 1005
+> **TextBackground** has no visible effect: the control has no **Picture** property, the background image an opaque text background would hide.
### View
{: .no_toc }
diff --git a/docs/Reference/Built-In/WinNativeCommonCtls/Slider.md b/docs/Reference/Built-In/WinNativeCommonCtls/Slider.md
index eae8197b6..0be1f8b35 100644
--- a/docs/Reference/Built-In/WinNativeCommonCtls/Slider.md
+++ b/docs/Reference/Built-In/WinNativeCommonCtls/Slider.md
@@ -51,7 +51,10 @@ Properties
### BackColor
{: .no_toc }
-The background color of the channel. **OLE_COLOR**. Default: **vbButtonFace**.
+The background color of the control. **OLE_COLOR**. Default: **vbButtonFace**.
+
+> [!NOTE] BETA 1005
+> A change to **BackColor** does not repaint the control, and neither does invalidating it: the new color shows only when the user drags the thumb, or when the control is hidden and shown again.
### BorderStyle
{: .no_toc }
diff --git a/docs/Reference/COM-Interfaces/IUnknown.md b/docs/Reference/COM-Interfaces/IUnknown.md
index 2736cda3f..ea7629aeb 100644
--- a/docs/Reference/COM-Interfaces/IUnknown.md
+++ b/docs/Reference/COM-Interfaces/IUnknown.md
@@ -76,14 +76,42 @@ twinBASIC issues these calls itself, so the language has no statement for them:
- **Set** *a* **= Nothing**, and a variable going out of scope, each release one reference.
- **Set** to a variable of an interface type asks the object for that interface with **QueryInterface**.
-The three methods themselves cannot be called by name:
+The three methods cannot be called by name through the types the language provides:
- On a variable declared **As stdole.IUnknown**, `u.AddRef`, `u.Release` and `u.QueryInterface` are compile errors, TB5027 *Unrecognized member*. The same holds for a variable of a project's own interface that extends **stdole.IUnknown**.
- On an **Object** variable, `o.AddRef` and `CallByName(o, "AddRef", VbMethod)` raise run-time error 438 (*Object doesn't support this property or method*).
-- A project cannot redeclare **IUnknown** under its own name and call it. An **Interface** declared with no **Extends** clause derives from **IDispatch**, not from **IUnknown**, so its first method is at slot 7 and the three declared methods are not the three of **IUnknown**. Declared with **Extends stdole.IUnknown**, the first method is at slot 3. Neither reaches slots 0 to 2.
+
+The first method of an **Interface** declared with no **Extends** clause is at slot 7, because the interface derives from **IDispatch**, and the first method of one declared **Extends stdole.IUnknown** is at slot 3. An interface declared **Extends Nothing** has no base, so its first method is at slot 0. Declared with the identifier of **IUnknown** and its three methods in their order, it calls them by name:
+
+```tb check_build projname=com-iunknown slot=file
+[InterfaceId("00000000-0000-0000-C000-000000000046")]
+[OleAutomation(False)]
+Private Interface IUnknownCallable Extends Nothing
+ [PreserveSig]
+ Function QueryInterface(ByVal riid As LongPtr, ByRef ppvObject As LongPtr) As Long
+ [PreserveSig]
+ Function AddRef() As Long
+ [PreserveSig]
+ Function Release() As Long
+End Interface
+```
+
+**Set** to a variable of this type asks the object for **IUnknown**, which every object supports. With any other identifier, the **Set** fails with `E_NOINTERFACE`, because the object does not know the interface. The counts include the reference that each variable holds:
+
+```tb check_run projname=com-iunknown
+Dim c As New Collection
+Dim u As IUnknownCallable = c
+Debug.Print u.AddRef()
+Debug.Print u.Release()
+' Output:
+' 3
+' 2
+```
+
+The [OLEGuids](../../tB/Packages/Contributed/OLEGuids/IUnknownUnrestricted) package declares this interface as **IUnknownUnrestricted**.
> [!NOTE]
-> Code that has to read or change a reference count goes through the object's method table. The example below does this with **DispCallFunc** from oleaut32.dll. [**vbaObjAddref**](../../tB/Modules/HiddenModule/vbaObjAddref) calls **AddRef** on a raw pointer without reading the result.
+> Without such an interface, code that has to read or change a reference count goes through the object's method table. The example below does this with **DispCallFunc** from oleaut32.dll. [**vbaObjAddref**](../../tB/Modules/HiddenModule/vbaObjAddref) calls **AddRef** on a raw pointer without reading the result.
Calling **IUnknown** methods by hand is a low-level operation. A wrong slot number, or a **Release** with no matching **AddRef**, ends in an access violation or in an object freed while a variable still refers to it.
@@ -133,10 +161,10 @@ The class's `Class_Terminate` procedure runs inside the **Release** that brings
A class never needs to implement **IUnknown**: the compiler gives every class an implementation, and an interface that extends **stdole.IUnknown** is satisfied without a body for those three methods.
-`Implements stdole.IUnknown` compiles, and the class works as before. A project's own **Interface** that carries the identifier of **IUnknown** also compiles, and a class can implement it.
+`Implements stdole.IUnknown` compiles, and the class works as before. A project's own **Interface** that carries the identifier of **IUnknown** and extends another interface also compiles, and a class can implement it.
> [!WARNING] BETA 1005
-> That interface is unusable. **Set** to a variable of that type gives the object's ordinary **IUnknown** pointer, which has none of the interface's methods, and calling one ends the program with an access violation, and any unsaved data is lost. The compiler gives no diagnostic. Do not declare an **Interface** with the identifier of **IUnknown**; use `stdole.IUnknown`.
+> That interface is unusable. **Set** to a variable of that type gives the object's ordinary **IUnknown** pointer, which has only the three methods of **IUnknown**. Calling a method the interface declares after them ends the program with an access violation, and any unsaved data is lost. The compiler gives no diagnostic. Give the identifier of **IUnknown** only to an interface declared **Extends Nothing** with the three methods of **IUnknown**, as in [Calling it from twinBASIC](#calling-it-from-twinbasic); otherwise use `stdole.IUnknown`.
## Example
diff --git a/docs/Reference/Contributed/Fmt/ExponentialSFI.md b/docs/Reference/Contributed/Fmt/ExponentialSFI.md
index ecc24fd67..ceb83b629 100644
--- a/docs/Reference/Contributed/Fmt/ExponentialSFI.md
+++ b/docs/Reference/Contributed/Fmt/ExponentialSFI.md
@@ -14,7 +14,7 @@ Formats a number in exponential notation for the **E** identifier, and reads the
## Output
-The value is passed to **Format$** with a format of the form `0.00000#E-#`. The letter in the format is the identifier as written, so `E` gives an upper-case `E` in the result and `e` a lower-case `e`. The exponent has no plus sign and no leading zeros.
+The value is passed to **Format$** with a format of the form `0.00000#E-#`. The letter in the format is the identifier as written, so `E` gives an upper-case `E` in the result and `e` a lower-case `e`. The exponent has no plus sign and no leading zeros. The decimal point follows the Windows regional settings.
- Without a precision the mantissa has six decimal places. The sixth is optional, so a trailing zero there is dropped.
- With a precision *n* greater than zero the mantissa has *n* decimal places, and the last of them is optional. A precision of `0` is the same as none.
diff --git a/docs/Reference/Contributed/Fmt/GeneralNumericSFI.md b/docs/Reference/Contributed/Fmt/GeneralNumericSFI.md
index 506bdfcb8..8a36e8aca 100644
--- a/docs/Reference/Contributed/Fmt/GeneralNumericSFI.md
+++ b/docs/Reference/Contributed/Fmt/GeneralNumericSFI.md
@@ -20,7 +20,7 @@ The result depends on the **TypeName** of the value:
- A **Double** is first formatted by [**ExponentialSFI**](ExponentialSFI) with the same precision. The exponent of that text is read back with [**ParseExponent**](ExponentialSFI#parseexponent). When the exponent is greater than -5 and its absolute value is less than the precision, the value is formatted again by [**FixedPointSFI**](FixedPointSFI), with the precision as the count of decimal places. Otherwise the exponential text is the result. Without a precision the second test cannot succeed, so a **Double** is always in exponential notation.
- A value of any other type, such as a **Single**, **Currency**, **Byte**, **Decimal** or a numeric **String**, gives an empty string.
-The precision is therefore a count of decimal places and a limit for the exponent. It is not a count of significant digits, as it is in .NET. The case of the identifier is kept: `g` gives a lower-case `e` in exponential notation.
+The precision is therefore a count of decimal places and a limit for the exponent. It is not a count of significant digits, as it is in .NET. The case of the identifier is kept: `g` gives a lower-case `e` in exponential notation. The decimal point follows the Windows regional settings.
```tb check_build check_run
Debug.Print StringFormat("{0:G}", 42) ' 42
diff --git a/docs/Reference/Default/VB/UserControl/index.md b/docs/Reference/Default/VB/UserControl/index.md
index 3f18ec68d..b66c66430 100644
--- a/docs/Reference/Default/VB/UserControl/index.md
+++ b/docs/Reference/Default/VB/UserControl/index.md
@@ -77,7 +77,7 @@ End Property
[**Ambient**](#ambient) exposes the host's ambient properties (the colours, font, locale, and design/run-mode flags the host wants child controls to honour). Changes to any of those raise the [**AmbientChanged**](#ambientchanged) event with the name of the affected property.
-[**Verbs**](#verbs) and [**VerbInvoked**](#verbinvoked) let the control register host-invokable commands (entries that appear on the host's context menu for the control). [**PropertyPages**](#propertypages) registers the **CLSID**s of additional [**PropertyPage**](../PropertyPage) classes that the host should offer through the property browser.
+[**Verbs**](#verbs) and [**VerbInvoked**](#verbinvoked) let the control register host-invokable commands (entries that appear on the host's context menu for the control). [**PropertyPages**](#propertypages) names the [**PropertyPage**](../PropertyPage) classes that the host should offer through the property browser.
## Drawing surface
@@ -464,16 +464,23 @@ Whether the [**PreKeyDown**](#prekeydown) and [**PreKeyUp**](#prekeyup) events a
### PropertyPages
{: .no_toc }
-An array of **String** **CLSID**s identifying the [**PropertyPage**](../PropertyPage) classes the host should offer through the **(Custom)** entry on the property browser. Read at the host's `ISpecifyPropertyPages::GetPages` call. Order matters --- the array order is the tab order in the property-sheet dialog.
+An array of **String**s naming the [**PropertyPage**](../PropertyPage) classes of the project that the host should offer through the **(Custom)** entry on the property browser. Each element is a class name, as **Add Windows PropertyPage** created it; a name that matches no property page is skipped. Read at the host's `ISpecifyPropertyPages::GetPages` call. Order matters --- the array order is the tab order in the property-sheet dialog.
```tb check_build
Private Sub UserControl_Initialize()
ReDim PropertyPages(0 To 1)
- PropertyPages(0) = "{}"
- PropertyPages(1) = "{}"
+ PropertyPages(0) = "GeneralPage"
+ PropertyPages(1) = "ColourPage"
End Sub
```
+To tie a single property to a page, use the [**PropertyPage**](../../../Core/Attributes#propertypage) attribute.
+
+
+
### Public
{: .no_toc }
diff --git a/docs/Tutorials/WebView2/Getting started.md b/docs/Tutorials/WebView2/Getting started.md
index cb8f9cfde..be2ddc1c6 100644
--- a/docs/Tutorials/WebView2/Getting started.md
+++ b/docs/Tutorials/WebView2/Getting started.md
@@ -26,7 +26,7 @@ Once you've added the package references, you should find that the WebView2 cont
We use the WebView2 control just like any ordinary control:
-{:width="1000" height="530"}
+{:width="1000" height="530"}
## WebView2 control properties
diff --git a/docs/Tutorials/WebView2/Images/tbWebView2InAForm.gif b/docs/Tutorials/WebView2/Images/tbWebView2InAForm.gif
deleted file mode 100644
index a9c264b43..000000000
Binary files a/docs/Tutorials/WebView2/Images/tbWebView2InAForm.gif and /dev/null differ
diff --git a/docs/Tutorials/WebView2/Images/tbWebView2InAForm.webp b/docs/Tutorials/WebView2/Images/tbWebView2InAForm.webp
new file mode 100644
index 000000000..a7fcc6ed2
Binary files /dev/null and b/docs/Tutorials/WebView2/Images/tbWebView2InAForm.webp differ
diff --git a/docs/_book.yml b/docs/_book.yml
index d6985b495..a489d7023 100644
--- a/docs/_book.yml
+++ b/docs/_book.yml
@@ -713,6 +713,9 @@ left_out:
- reason: Links to video recordings, with nothing to read in print
page: /Videos
+ - reason: Download links, with nothing to read in print
+ page: /Downloads
+
- reason: Time-limited community contests
page: /Challenges
diff --git a/docs/_config.yml b/docs/_config.yml
index f4e0d82a7..894765277 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -32,22 +32,13 @@ gh_edit_branch: "main"
gh_edit_source: docs
gh_edit_view_mode: "tree"
-# Footer "Download offline copy" link -- points at the offline-site
-# zip attached to the latest GitHub release (cut by the manual
-# `workflow_dispatch` path in .github/workflows/jekyll-gh-pages.yml).
-# Rendered inside the same flex row as the "Edit this page" link,
-# with id="download-offline" on the . Emitted in both the online
-# _site/ and the offline _site-offline/ trees -- online readers use
-# it to fetch the offline copy, offline readers use it to refresh
-# theirs against the latest release.
-gh_offline_link: true
-gh_offline_link_url: "https://github.com/twinbasic/documentation/releases/latest/download/twinbasic-docs-offline.zip"
-# The released asset is "twinBASIC Book.pdf", with a space. GitHub replaces spaces
-# with dots in release download URLs, so the dotted spelling here is correct and the
-# spaced one 404s. Verified against the live release. Do not "fix" this to match the
-# workflow's filename. It also depends on make_latest: 'true' in tbdocs-gh-pages.yml,
-# which is what makes /releases/latest/download/ resolve at all.
-gh_pdf_link_url: "https://github.com/twinbasic/documentation/releases/latest/download/twinBASIC.Book.pdf"
+# Footer "Read Offline" link -- a site path, to the Downloads page's
+# section on the offline copy and the PDF book, which link to the
+# latest GitHub release. Rendered inside the same flex row as the
+# "Edit this page" link, with id="read-offline-link" on the , in both
+# the online _site/ and the offline _site-offline/ trees -- offline
+# readers use it to refresh their copy against the latest release.
+read_offline_link: "/Downloads#read-offline"
# When true, tbdocs's Phase 7 writes a `file://`-browsable mirror of
# `_site/` to `_site-offline/` with every URL rewritten to a page-
diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss
index 50cff4a80..72ee101e3 100644
--- a/docs/_sass/custom/custom.scss
+++ b/docs/_sass/custom/custom.scss
@@ -905,3 +905,67 @@ div.highlight:focus-visible,
margin-left: auto;
}
}
+
+// ── Download cards (the Downloads page) ──────────────────────────────────
+//
+// `` holds ``s, each a
+// heading, a paragraph, a `.download-meta` line and a `.download-actions` row
+// of `.btn` links (the theme's, styled in both themes) and one
+// `.download-doc` link. The grid takes as many 18rem columns as fit, and one
+// at phone width. The actions row follows the text directly.
+//
+// The heading's top margin is the theme's `.main-content h3`, which the dark
+// re-emission raises to (0,2,2); `> :first-child` here is (0,3,0) and wins in
+// both themes (see "The specificity trap" in Builder.md).
+.download-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
+ gap: 1rem;
+ margin: 1rem 0 1.5rem;
+}
+
+.download-card {
+ padding: 1rem 1.25rem;
+ border: 1px solid #e6e1e8; // $grey-lt-300
+ border-radius: 6px;
+
+ > p {
+ margin: 0 0 0.75rem;
+ }
+}
+
+.main-content .download-card > :first-child {
+ margin-top: 0;
+}
+
+@include dark-theme {
+ .download-card {
+ border-color: #44434d; // dark $border-color
+ }
+}
+
+.download-card > .download-meta {
+ font-size: 0.875em;
+}
+
+.download-card > .download-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ align-items: center;
+ margin: 0;
+}
+
+.download-size {
+ font-weight: 400;
+ font-size: 0.875em;
+}
+
+// Pushed to the right end of the row, after the buttons.
+.download-doc {
+ margin-left: auto;
+
+ &::after {
+ content: " \2192" / "";
+ }
+}
diff --git a/docs/assets/images/dot/scheduler-dag.dot b/docs/assets/images/dot/scheduler-dag.dot
index eb7786284..dc79a0cf6 100644
--- a/docs/assets/images/dot/scheduler-dag.dot
+++ b/docs/assets/images/dot/scheduler-dag.dot
@@ -64,10 +64,12 @@ digraph scheduler {
AP [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
WX [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
WO [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
- WP [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
+ HA [label= [W]>, fillcolor="#e8f0fe", color="#4285f4"]
+ BP [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
+ WP [label= [W]>, fillcolor="#e8f0fe", color="#4285f4"]
LJ [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
- CB [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
+ CB [label= [W] pinned>, fillcolor="#e8f0fe", color="#4285f4"]
CR [label= [M]>, fillcolor="#fef7e0", color="#f9ab00"]
// ── Edges ───────────────────────────────────────────────────────────
@@ -124,6 +126,7 @@ digraph scheduler {
SYM -> AP
PD -> AP
+ HA -> AP
WA -> WX
SDt -> WX
@@ -134,11 +137,16 @@ digraph scheduler {
WX -> WO
WA -> WO
- FJ -> WP
- RJ -> WP
- DT -> WP
- RBC -> WP
- HI -> WP [style=dashed]
+ FJ -> HA
+ SCSS -> HA
+ WO -> HA
+
+ FJ -> BP
+ RJ -> BP
+ DT -> BP
+ RBC -> BP
+ HI -> BP [style=dashed]
+ BP -> WP
FJ -> LJ
WX -> LJ
diff --git a/docs/assets/images/dot/scheduler-dag.svg b/docs/assets/images/dot/scheduler-dag.svg
index a2f9d5978..4925cb8be 100644
--- a/docs/assets/images/dot/scheduler-dag.svg
+++ b/docs/assets/images/dot/scheduler-dag.svg
@@ -1,633 +1,677 @@
-