From 133952ce365ac6448507f1520e606b9ea0aa8efa Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Tue, 13 Oct 2026 22:05:01 +0200 Subject: [PATCH 01/13] tbbuild and every IDE the harness starts: remove the IDE's temp folder and WebView2 profile once it ends, and tbbuild's work folder after each run (a successful --build keeps out\) --- .github/actions/run-gates/action.yml | 5 +- WIP.md | 4 +- docs/Documentation/Tools-Compiler.md | 6 +- docs/Documentation/Tools-Toolchain-Tests.md | 2 + docs/Documentation/Tools.md | 2 +- scripts/lib/tb-ide-copy.mjs | 24 ++++++- scripts/lib/tb-ide.mjs | 61 ++++++++++++++---- scripts/tbbuild.mjs | 26 +++++++- test.bat | 3 +- test/ports.test.mjs | 71 ++++++++++++++++++++- 10 files changed, 177 insertions(+), 27 deletions(-) diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index 290e63a41..405d89fd6 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -135,8 +135,9 @@ runs: run: node --test test/example-batches.test.mjs # Unit tests for scripts/lib/tb-ports.mjs, which claims the DevTools ports # the harness's IDEs use: two claims at once are disjoint, a port in use - # and a live lock are passed over, a stale lock is taken over. Starts no - # IDE. No browser, no built tree, about 2 s. + # and a live lock are passed over, a stale lock is taken over; and for what + # ending an IDE removes of its temp folder and profile. Starts no IDE. No + # browser, no built tree, about 2 s. - name: Unit-test the harness's port claims (test/ports.test.mjs) shell: bash run: node --test test/ports.test.mjs diff --git a/WIP.md b/WIP.md index 661cef1d1..745c9e7ee 100644 --- a/WIP.md +++ b/WIP.md @@ -126,7 +126,7 @@ node scripts/tbrun.mjs # what does it print - **Pack and unpack projects with `impexp`, never with the compiler executable's `import` or `export` verbs.** The tooling does both in process, through `packTree` and `unpackProject` in `scripts/lib/tb-project.mjs`; by hand it is `node scripts/impexp.mjs`. The compiler's `import` refuses a project that embeds a package (exit 999, twinbasic/twinbasic#841), and both verbs exit 0 after the failures they report. The reasons, and the path and stdin rules for anything that does run the executable (`bug_repro`'s `cli` mode), are in [WIP.Harness.md](WIP.Harness.md#getting-at-the-twin-sources). - **`tbbuild` exit codes:** 0 clean, 1 the project has errors, 2 the harness failed or the project file cannot be read (empty, cut short, not a project: it is read before an IDE starts), 3 the compile never settled, 4 the project crashes the compiler, 5 the build failed after a clean compile. `--json` returns one object, `--keep` leaves the IDE running. **`--build` and `--llvm` (which implies it) build the project after a clean compile**, because LLVM code generation runs in a build and a compile never asks for it. They first stage a copy with an explicit build path (`%TEMP%\tbbuild\\`), so the given `.twinproj` is untouched. A plain `--build` is the control for an `--llvm` run. `--llvm` refuses a Community or Personal licence. - **It runs the IDE on a private Windows desktop**, so it cannot seize focus. Set `TBBUILD_SHOW=1` while working interactively and leave it unset for unattended runs: a wedged IDE nobody can see is expensive to diagnose. -- **One project per IDE**, 6--8 seconds each and flat in project size. Reusing a live IDE for a second project wedges it, so the cold start is the unit of work, not overhead to optimise away. Run concurrently to go faster: distinct `--port` values give distinct DevTools ports, user-data folders, temp folders (`%TEMP%\tbbuild-tmp-`; IDEs building in one temp folder fail now and then to write the type library) and desktops. +- **One project per IDE**, 6--8 seconds each and flat in project size. Reusing a live IDE for a second project wedges it, so the cold start is the unit of work, not overhead to optimise away. Run concurrently to go faster: distinct `--port` values give distinct DevTools ports, user-data folders, temp folders (`%TEMP%\tbbuild-tmp-`; IDEs building in one temp folder fail now and then to write the type library) and desktops. `shutdownIde` removes the temp folder and the user-data folder (`%TEMP%\tbbuild-wv2-`, 28 MB) once the IDE has ended, and `launchIde` empties both again at the next launch on the port, for what a killed run left; a kept IDE's stay until the next run. - **Keep a probe that might crash the compiler in a project of its own.** twinBASIC runs the compiler in the same process as user code, so one bad probe can take the run down and cost the other probes their answer. - **`tbrun` takes an exported tree, not a `.twinproj`**, because it has to pin `project.buildPath` in its own staged copy. A project still on the default template opens a native Save dialog that is invisible on the private desktop: the build never happens, while every health check says the IDE is fine. The probe is a module with a `[RunAfterBuild]` Sub, and must start with `Debug.Cls`. Its exit codes: 0 the probe ran and its output was captured, 1 the project has compile errors, 2 the harness failed or the build did after a clean compile, or, on a build before 1005, the probe's module holds a procedure named like the module, which stops the `[RunAfterBuild]` Sub from running there (`--allow-name-clash` runs it anyway), 3 no output, 4 the compiler crashed, as `tbbuild` reports it, 5 the probe ended before it returned (`End`, or an error raised with no handler in LLVM-compiled code, which ends the run silently), 6 with `--exe`, the exe exited with a code other than 0, the event log records that it faulted, it opened a box (an unhandled error opens one, and `tbrun` closes it), or it was still running at `--timeout`, 7 with `--tests`, a case failed. - **A project's own unit tests: `tbrun --tests`.** It builds nothing: it runs each `[TestCase]` Sub of each `[TestFixture]` Module in the compiler's test mode, the debugger's `evaluate` request with `isTest`, which the IDE itself never sends (BETA 1005 has no test runner a person can use), and prints `PASS` or `FAIL` for each. A failed assertion, an unhandled error and an access violation each end only their case. A defect that is a diagnostic is a compile case instead, a line ending `' CASE : `. A follow-up issue's regression tests are written this way: [WIP.Harness.md](WIP.Harness.md#running-a-projects-test-cases). @@ -595,7 +595,7 @@ wrapper: | `test.bat` | `test/compact-image.test.mjs` | what an embedded image keeps (`compactImage` and `stripFile` in `scripts/lib/compact-image.mjs`): an editor's metadata goes, a notice of whose the picture is and on what terms stays (the IDE's lock icon carries a CC BY-NC-SA attribution), an sRGB profile goes and any other stays, a JPEG or a GIF is labelled by its bytes, and an image inside an SVG inside a CSS file is reached and keeps its encoding. Its second half runs `scripts/imagestrip/` over a CSS file and SVG files of every image the test builds and over the IDE's own stylesheets, and compares output, summary and reports with `stripFile` byte for byte; it is skipped unless `IMAGESTRIP_EXE` names the built exe. Run by `node --test` | | `test.bat` | `test/svgshot.test.mjs` | an SVG screenshot is written on one line (`oneLine` in `scripts/svgshot/snapshot-svg.mjs`), a break in a tag as a space and one in text as a character reference, so that a CRLF checkout changes only its last line ending; an underline is placed and sized by the figures fitted in Edge (`underlineBand`) under the boxes that set one (`decoratingBoxes`); a background image is sized by its bytes, a JPEG the IDE calls `image/png` included (`naturalSize`); an iframe's document is decoded and placed at the iframe's content box, less its scroll, scaled when zoomed, over its canvas colour (`frameGeometry`, `frameView`, `unzoom`, `viewportScroller`, `canvasColor`); a run's web font is the `@font-face` rule CSS matching picks (`webFontFiles`), and a variable WOFF2 is read and cut per weight; a family split by `unicode-range` gives each character to the rule whose range holds it (`parseUnicodeRange`, `webRulesFor`); an inline `` is found and its ids scoped (`inlineSvgsIn`, `scopeIds`); a filter of colour functions is baked into the colours under it, in order and clamped, its pictures wrapped in a real filter and its opacity kept apart (`colour-filter.mjs`); a transformed element's boxes and text are put back from the snapshot's after-transform boxes (`elementTransform`, `untransformBox`); a closed select shows its option's whole text (`selectLabel`); a bundle draws the same SVG from its file as in memory, and counts each question it cannot answer (`renderBundle`); its web font rules and files draw each character from the file whose range holds it, at the weight its rule allows, and its inline `` markup is drawn in its box, from the file and when trimmed (`drawBundle`); a bundle of version 1 is refused; one trimmed to its view draws the same (`trimSnapshot`); a font's cut comes from the subset cache with no Python run (`subsetKey`, `useSubsetCache`). What it keeps of an image it embeds is `compactImage`'s, which the row above tests. Run by `node --test` | | `test.bat` | `test/example-batches.test.mjs` | `check_examples.mjs`'s probes, from `scripts/lib/example-batches.mjs`: batching, crash isolation through a fake lane, the canaries, the fence classifier and `check_run`'s refusals, expected output, dispatcher and marker reading, with no IDE. Run by `node --test` | -| `test.bat` | `test/ports.test.mjs` | `scripts/lib/tb-ports.mjs` claims ports as the lane runners need: two claims at once are disjoint, a port in use and a live lock are passed over, a stale lock is taken over, a lock goes when its process exits. Run by `node --test` | +| `test.bat` | `test/ports.test.mjs` | `scripts/lib/tb-ports.mjs` claims ports as the lane runners need: two claims at once are disjoint, a port in use and a live lock are passed over, a stale lock is taken over, a lock goes when its process exits; `shutdownIde` and `shutdownIdeAsync` remove the IDE's temp folder and profile once it is gone, and no other folder. Run by `node --test` | | `test.bat` | `test/addin-project.test.mjs` | `builder/addin-project.mjs` packs the help add-in's project file from exactly the files git tracks in `add-in/`, as the working folder holds them, never `Resources/HELP/`, with the index it is given, the same bytes from a CRLF and an LF checkout, into every tree it is given. Needs git. Run by `node --test` | | `test.bat` | `test/indexer.test.mjs` | the package indexer (`indexer/lib/`) keeps its promise that a snapshot changes only when the API does: VB6 files named by `VB_Name`, else by their file; a package's files in code-unit order with the `Packages` folder left out; `api.json` types sorted by path with `in` for nested ones, members by name, Enum values and Type fields in declaration order, a type character stripped from an unbracketed name only, the same bytes from the same files in any order; `diffApi` and `auditCoverage` on small hand-made inputs. Reads `indexer/sample.twinpack`; no network, git or install. Run by `node --test` | | `test.bat` | `check_pdf_shims_equiv` | the book's pdf-lib shims write what stock pdf-lib writes: one document written by the gate and one built with `PDFDocument.create` are each saved both ways in child processes, and each pair of files is compared object by object with streams inflated and their cross-reference entries checked; every shim must run, and the members of pdf-lib the shims patch must be those its `PATCHES` lists, each run in one document or the other unless marked there as not reached; each of them must also be in the target table its shim passes to `checkTargets`, which `book/lib/shim-targets.mjs` records for the side to ask | diff --git a/docs/Documentation/Tools-Compiler.md b/docs/Documentation/Tools-Compiler.md index ccaeb2457..0acc5b6dc 100644 --- a/docs/Documentation/Tools-Compiler.md +++ b/docs/Documentation/Tools-Compiler.md @@ -28,18 +28,18 @@ twinBASIC has no command-line build. None of the compiler executable's verbs --- | Flag | Effect | |---|---| | `--ide ` | Path to `twinBASIC.exe`. Default: `$TB_IDE`, else the newest `twinBASIC_IDE_BETA_` folder on `%USERPROFILE%\Desktop`. **No install path is hardcoded in this tooling**, because one contains a username, so an install kept elsewhere needs one of those two. | -| `--port ` | DevTools port. Default 9333. It also names the WebView2 user-data folder, the IDE's temp folder (`%TEMP%\tbbuild-tmp-`, its `TEMP` and `TMP`) and the private desktop, so runs on distinct ports can go at once; IDEs sharing one temp folder now and then fail to write the type library. A port another IDE already holds is refused after ten seconds. | +| `--port ` | DevTools port. Default 9333. It also names the WebView2 user-data folder, the IDE's temp folder (`%TEMP%\tbbuild-tmp-`, its `TEMP` and `TMP`) and the private desktop, so runs on distinct ports can go at once; IDEs sharing one temp folder now and then fail to write the type library. Both folders (`%TEMP%\tbbuild-tmp-` and the user-data folder, `%TEMP%\tbbuild-wv2-`, about 28 MB) are emptied when the IDE starts and removed once it has ended. A port another IDE already holds is refused after ten seconds. | | `--arch ` | `win32` or `win64`. Default `win32`. Diagnostics can differ, because `#If Win64` and the size of `LongPtr` change what compiles. The target is set on every run, whatever the IDE last used for the project; a switch restarts the compiler and adds a few seconds. When the target is not `win32`, or the IDE remembered another, the report starts with a `target:` line. | | `--timeout ` | Give up waiting for the compile to settle. Default 180. | | `--build` | After a compile with no errors, build the project as the toolbar's Build button does, and print `built: ` after the summary line. The given file is never changed (see below). | | `--llvm` | `--build` with the project's compiler options set to `+llvm`. Refused, with exit 2, on a Community or Personal licence, which would silently build with the default compiler. A plain `--build` is the control for an `--llvm` run. | | `--json` | Emit one JSON object instead of lines of text: the target, counts, diagnostic rows, the text of any alert the IDE opened (dismissed so the compile can go on), `built` (the file a build wrote, or null) and `buildLog`. | -| `--keep` | Leave the IDE running and print its pid as `ide-pid: `, then the `taskkill` command that ends it and every process it started, for cmd and PowerShell and for Git Bash (`//PID`). The IDE's registry entries for the project are left as they are, since the IDE is still writing them. | +| `--keep` | Leave the IDE running and print its pid as `ide-pid: `, then the `taskkill` command that ends it and every process it started, for cmd and PowerShell and for Git Bash (`//PID`). The IDE's registry entries for the project and its folders under `%TEMP%` are left as they are, since the IDE is still using them; ending it does not remove them, and the next run on the same port empties them. | | `--show` / `--hide` | Run the IDE on your own desktop, where you can watch it, or on a private one, where it cannot take focus. Hidden is the default unless `TBBUILD_SHOW` is set to something other than `0`, `false` or `no`. | **A compile does not generate code, and a build does.** LLVM code generation can refuse a construct the compiler's front end accepts ("a feature used in your code is not yet supported with the LLVM compiler"), so a clean compile says nothing about an LLVM build. `--build` and `--llvm` press Build after the compile and read the build log. -A packed project's default build path opens a *Save* dialog that the private desktop hides. So either flag first exports the project into `%TEMP%\tbbuild\\src`, packs a copy with an explicit build path under `%TEMP%\tbbuild\\out` and a project id of its own, and opens that copy. +A packed project's default build path opens a *Save* dialog that the private desktop hides. So either flag first exports the project into `%TEMP%\tbbuild\\src`, packs a copy with an explicit build path under `%TEMP%\tbbuild\\out` and a project id of its own, and opens that copy. Once the run is over the copy and its sources are removed, and so is the whole folder when nothing was built. After a build that succeeded, `%TEMP%\tbbuild\\out` stays, because it holds the file `built:` names; the next run on that port replaces it. - A project with compile errors is not built: the report is the compile's, with exit 1. - A build that fails, or that ends the compiler with a native exception, prints the build log on stdout and the failing line on stderr, and exits 5. diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index 718ed09cb..a9ccb4b77 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -288,6 +288,8 @@ Unit tests for `scripts/lib/tb-ports.mjs`, which claims the DevTools ports that Four child processes claim three ports each at the same time. The range holds one port in use, one with the lock of a process that has ended and one with the lock of a live process, and each child holds its ports until all have claimed. The test checks that the claims are disjoint, that they are exactly the free ports with the stale lock taken over, and that each lock is gone once its process exits. Two more tests ask for more free ports than the range holds, which is refused with the port found unlocked again, and claim past the unfinished claim of a run that ended. +The same file tests what an IDE leaves under the system temp directory. `launchIde` gives each IDE a temp folder and a WebView2 profile named for its port, and `shutdownIde` and `shutdownIdeAsync` remove both once the IDE's process has gone. The tests give them a handle for a process that cannot exist, and check that its two folders are removed and a third is not, and that a folder already gone, a handle with no folders and no handle are no error. + Starts no IDE. No browser, no built tree, about 2 s. Exit codes: **0** every test passed, **1** a test failed. diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index 741034ed9..d571bcc3f 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -123,7 +123,7 @@ The tests the toolchain has to pass. Twenty-five steps, each stopping the run if 9. [`test/compact-image.test.mjs`](Tools-Toolchain-Tests#compact-image-test) --- unit tests for what an embedded image keeps: an editor's metadata goes, a notice of ownership and terms stays. The half that runs [`imagestrip`](Tools-IDE#imagestrip) is skipped unless its exe is named. 10. [`test/svgshot.test.mjs`](Tools-Toolchain-Tests#svgshot-test) --- unit tests that an SVG screenshot is written on one line, that its underlines are placed as Chromium places them, that a background image is sized by its bytes, that a frame's page is placed inside its frame, that a web font is found and cut as the page uses it, that the page's own `` icons are found and kept apart, and that a colour filter is baked into the colours it paints. 11. [`test/example-batches.test.mjs`](Tools-Toolchain-Tests#example-batches-test) --- runs `check_examples.mjs`'s probes, which test how samples are batched and how a crashed batch is cut down, without an IDE. -12. [`test/ports.test.mjs`](Tools-Toolchain-Tests#ports-test) --- unit tests for how the harness claims the DevTools ports its IDEs use, without an IDE. +12. [`test/ports.test.mjs`](Tools-Toolchain-Tests#ports-test) --- unit tests for how the harness claims the DevTools ports its IDEs use, and for the temp folder and profile an IDE on a port leaves behind, without an IDE. 13. [`test/addin-project.test.mjs`](Tools-Toolchain-Tests#addin-project-test) --- unit tests for how the build packs the help add-in's project file: exactly the files git tracks in `add-in/`, never `Resources/HELP/`, the same bytes from any checkout. 14. [`test/indexer.test.mjs`](Tools-Toolchain-Tests#indexer-test) --- unit tests for the package indexer: the VB6 preprocessing, the files of a package, the `api.json` built from them (sorted by code unit, nested types by their path, values and fields in declaration order, the same bytes from the same files) and the differ that compares two of them. 15. [`scripts/check_regex_safety.mjs`](Tools-Toolchain-Tests#check-regex-safety) --- refuses a regex that can backtrack exponentially, written as a literal or built from constants. diff --git a/scripts/lib/tb-ide-copy.mjs b/scripts/lib/tb-ide-copy.mjs index 38cdcbc7d..22347cba0 100644 --- a/scripts/lib/tb-ide-copy.mjs +++ b/scripts/lib/tb-ide-copy.mjs @@ -27,6 +27,7 @@ // hold a startTidy/finishTidy around them, which tbbuild and tbrun already do. import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -161,8 +162,29 @@ export function removeTree(dir, { timeout = 5000 } = {}) { rmSync(dir, { recursive: true, force: true }); return; } catch (e) { - if (!["EPERM", "EBUSY", "ENOTEMPTY", "EACCES"].includes(e.code) || Date.now() >= until) throw e; + if (!HELD.includes(e.code) || Date.now() >= until) throw e; Atomics.wait(cell, 0, 0, 100); } } } + +/** + * removeTree without blocking the event loop, for a caller that removes a folder + * while other work is going on in the same process (shutdownIdeAsync, with + * several lanes at once). + */ +export async function removeTreeAsync(dir, { timeout = 5000 } = {}) { + const until = Date.now() + timeout; + for (;;) { + try { + await rm(dir, { recursive: true, force: true }); + return; + } catch (e) { + if (!HELD.includes(e.code) || Date.now() >= until) throw e; + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } +} + +// The codes Windows gives for a file another process still has open. +const HELD = ["EPERM", "EBUSY", "ENOTEMPTY", "EACCES"]; diff --git a/scripts/lib/tb-ide.mjs b/scripts/lib/tb-ide.mjs index e3a69e6ed..087da6dae 100644 --- a/scripts/lib/tb-ide.mjs +++ b/scripts/lib/tb-ide.mjs @@ -15,6 +15,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { attach } from "./tb-cdp.mjs"; import { click, pagePicture } from "./tb-click.mjs"; +import { removeTree, removeTreeAsync } from "./tb-ide-copy.mjs"; import { consoleMark, keepClears, keptClears, linesSince, readConsole } from "./tb-ide-console.mjs"; import { watchCompiles } from "./tb-wire.mjs"; @@ -120,9 +121,10 @@ export function killTree(pid) { * DevTools ones, e.g. "--force-device-scale-factor=1" * to lay the page out at 100% whatever the display's * scaling - * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess | null}>} - * `pid` is the IDE's own. Throws when the port is taken, or when the launch - * fails. + * @returns {Promise<{pid: number, launcher: import("node:child_process").ChildProcess | null, folders: string[]}>} + * `pid` is the IDE's own. `folders` are its temp folder and WebView2 profile, + * which shutdownIde removes. Throws when the port is taken, or when the + * launch fails. */ export async function launchIde({ exe, project, port, show = false, keep = false, env = {}, browserArgs = "" }) { const waitFrom = Date.now(); @@ -149,12 +151,21 @@ export async function launchIde({ exe, project, port, show = false, keep = false // typelibrary. Disk error?`: 0 in 192 with a folder per IDE, 8 in 192 with // one for all eight at once (BETA 995). const ideTemp = path.join(process.env.TEMP, `tbbuild-tmp-${port}`); - try { - rmSync(ideTemp, { recursive: true, force: true }); - } catch { - // a file still held open: the IDE gets the folder as it is + // The WebView2 profile, 28 MB once the IDE has run. Emptied here as well, for + // what a run that was killed left behind, and removed when the IDE ends + // (shutdownIde), so that a run leaves neither folder. + const ideProfile = path.join(process.env.TEMP, `tbbuild-wv2-${port}`); + for (const dir of [ideTemp, ideProfile]) { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + // a file still held open: the IDE gets the folder as it is + } } mkdirSync(ideTemp, { recursive: true }); + // What shutdownIde removes once the IDE is gone: only these two, which this + // function has just made or emptied, never a folder a caller names. + const folders = [ideTemp, ideProfile]; // Node leaves out of a child's environment any variable whose value is // undefined, so `env` can remove one as well as set it. const fullEnv = { @@ -164,7 +175,7 @@ export async function launchIde({ exe, project, port, show = false, keep = false WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS: [`--remote-debugging-port=${port}`, "--remote-allow-origins=*", browserArgs] .filter(Boolean) .join(" "), - WEBVIEW2_USER_DATA_FOLDER: `${process.env.TEMP}/tbbuild-wv2-${port}`, + WEBVIEW2_USER_DATA_FOLDER: ideProfile, [ADDIN_TEST_ENV]: "1", ...env, }; @@ -179,7 +190,7 @@ export async function launchIde({ exe, project, port, show = false, keep = false child.once("error", (e) => reject(new Error(`could not start the IDE: ${e.message}`))); }); child.unref(); - return { pid: child.pid, launcher: null }; + return { pid: child.pid, launcher: null, folders }; } // lib/tb-launch.ps1 does the Win32 calls Node cannot -- CreateDesktop, @@ -211,8 +222,10 @@ export async function launchIde({ exe, project, port, show = false, keep = false job: !keep, env: fullEnv, }); - return { pid, launcher }; + return { pid, launcher, folders }; } catch (e) { + // No IDE started, so nothing holds the folders and nothing else would remove them. + for (const dir of folders) rmSync(dir, { recursive: true, force: true, maxRetries: 3 }); throw new Error( `could not start the IDE on a private desktop:\n${e.message}\n(--show runs it on your own desktop instead)`, ); @@ -442,6 +455,12 @@ export function keptIdeLines(pid) { * end of a process: tidying the registry before the process has actually gone * could lose to one last write. It is normally over in well under a second, * and it gives up after five. + * + * Once the process is gone its temp folder and WebView2 profile are removed + * (`ide.folders`, which launchIde made): without that every run left both under + * %TEMP%, 28 MB of profile for each port it used. A process still alive keeps + * its folders, since they are in use. A folder that cannot be removed is + * reported on stderr and left; launchIde empties it when the port is next used. */ export function shutdownIde(ide) { if (!ide) return; @@ -454,7 +473,14 @@ export function shutdownIde(ide) { } catch { /* already gone */ } - waitForExit(ide.pid, 5000); + if (!waitForExit(ide.pid, 5000)) return; + for (const dir of ide.folders ?? []) { + try { + removeTree(dir); + } catch (e) { + console.error(`could not remove ${dir}: ${e.code ?? e.message}`); + } + } } /** @@ -476,14 +502,23 @@ export async function shutdownIdeAsync(ide) { /* already gone */ } const until = Date.now() + 5000; - while (Date.now() < until) { + for (;;) { try { process.kill(ide.pid, 0); } catch { - return; + break; } + if (Date.now() >= until) return; await sleep(100); } + // The folders go as shutdownIde's do, with its own reasons. + await Promise.all( + (ide.folders ?? []).map((dir) => + removeTreeAsync(dir).catch((e) => { + console.error(`could not remove ${dir}: ${e.code ?? e.message}`); + }), + ), + ); } /** diff --git a/scripts/tbbuild.mjs b/scripts/tbbuild.mjs index 8cc66f7b5..e943553ae 100644 --- a/scripts/tbbuild.mjs +++ b/scripts/tbbuild.mjs @@ -64,7 +64,7 @@ // scripts/gen_attribute_probes.mjs -- but it does not know anything about // them. See WIP.Harness.md, "Compiling a twinBASIC project without the IDE in // front of you". -import { existsSync, mkdirSync, rmSync, statSync } from "node:fs"; +import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { @@ -80,6 +80,7 @@ import { import { findIde } from "./lib/tb-install.mjs"; import { compileProject } from "./lib/tb-build.mjs"; import { COMPILE_TIMEOUT, keptIdeLines, TARGETS, summaryLine, wantShow } from "./lib/tb-ide.mjs"; +import { removeTree } from "./lib/tb-ide-copy.mjs"; import { checkProject, laneProjectId, stageProject, unpackProject } from "./lib/tb-project.mjs"; import { finishTidy, startTidy } from "./lib/tb-registry.mjs"; @@ -254,6 +255,7 @@ if (build) { try { unpackProject(path.resolve(proj), src); } catch (e) { + removeWork(work, null); die(2, `exporting ${proj} failed:\n${e.message}`); } // Under --llvm the options are the run's and the exe's alike, as tbrun's are. @@ -269,11 +271,30 @@ if (build) { }, }); } catch (e) { + removeWork(work, null); die(2, e.message); } } if (!keep) tidy = startTidy(work ? { prefixes: [work] } : { paths: [target] }); +// The staged copy is of no use once the run is over, and neither is anything in +// the work folder after a run that built nothing. A build that succeeded keeps +// its out\ folder, which holds the exe that "built:" names; the next run on this +// port replaces it. One that cannot be removed is reported and left. +function removeWork(dir, built) { + try { + if (!built) { + removeTree(dir); + return; + } + for (const name of readdirSync(dir)) { + if (name !== "out") removeTree(path.join(dir, name)); + } + } catch (e) { + console.error(`could not remove ${dir}: ${e.code ?? e.message}`); + } +} + // compileProject ends its IDE before it returns, unless --keep, so the tidy comes // after the IDE has gone. Tidies whether or not an IDE was started, as tbrun // does: a failed launch has written nothing to the registry so far, since @@ -284,6 +305,9 @@ try { r = await compileProject({ project: work ? target : proj, ide: IDE, port, arch, timeout, show, keep, build, llvm }); } finally { finishTidy(tidy); + // After the tidy, which names paths under the work folder, and with the IDE + // gone. Not under --keep: that IDE still has the staged project open. + if (work && !keep) removeWork(work, r?.built); } if (r.code >= 2) { diff --git a/test.bat b/test.bat index 60750960d..cbc29a5c6 100644 --- a/test.bat +++ b/test.bat @@ -98,7 +98,8 @@ node --test test/example-batches.test.mjs @if errorlevel 1 goto :fail @rem Unit tests for scripts/lib/tb-ports.mjs, which claims the DevTools @rem ports the harness's IDEs use: two claims at once are disjoint, a port -@rem in use and a live lock are passed over, a stale lock is taken over. +@rem in use and a live lock are passed over, a stale lock is taken over; +@rem and for what ending an IDE removes of its temp folder and profile. @rem Starts no IDE. No tree, no browser, about 2 s. node --test test/ports.test.mjs @if errorlevel 1 goto :fail diff --git a/test/ports.test.mjs b/test/ports.test.mjs index 8ce460082..ffcc1b17d 100644 --- a/test/ports.test.mjs +++ b/test/ports.test.mjs @@ -1,21 +1,29 @@ -// Unit tests for scripts/lib/tb-ports.mjs, which claims the DevTools ports a -// run's IDEs use so that two runs never share one. +// Unit tests for what the harness keeps for each DevTools port: the claim +// (scripts/lib/tb-ports.mjs), which stops two runs sharing a port, and the +// folders an IDE started on it leaves under %TEMP% (shutdownIde in +// scripts/lib/tb-ide.mjs). // // The harness runs that claim ports need a twinBASIC install, so they run only // by hand; claiming a port needs none. Four child processes claim at once, from // a range where one port is listened on, one holds a stale lock and one a live // lock, and their claims are checked against the ports that were free. // +// The folders are tested with no IDE: the handle names a process id that cannot +// be a process, so the IDE is already gone when shutdownIde looks at its +// folders, as it is after waitForExit. +// // Runs with a bare `node --test test/ports.test.mjs`: no tree, no IDE. import assert from "node:assert/strict"; import { spawn, spawnSync } from "node:child_process"; -import { existsSync, mkdirSync, rmSync, utimesSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs"; import net from "node:net"; import { tmpdir } from "node:os"; import path from "node:path"; import { after, before, describe, test } from "node:test"; import { pathToFileURL } from "node:url"; +import { shutdownIde, shutdownIdeAsync } from "../scripts/lib/tb-ide.mjs"; +import { removeTreeAsync } from "../scripts/lib/tb-ide-copy.mjs"; import { claimPorts } from "../scripts/lib/tb-ports.mjs"; const MODULE = pathToFileURL(path.join(import.meta.dirname, "../scripts/lib/tb-ports.mjs")).href; @@ -110,3 +118,60 @@ describe("claimPorts", () => { assert.ok(!existsSync(claiming), "the claiming folder is removed after the claim"); }); }); + +describe("the folders an IDE leaves", () => { + // Windows process ids are multiples of four, so this is never a process, and + // a taskkill of it ends nothing. + const NO_PROCESS = 2147483647; + let root; + + before(() => { + root = mkdtempSync(path.join(tmpdir(), "tb-ide-folders-")); + }); + + after(() => { + rmSync(root, { recursive: true, force: true }); + }); + + // A folder the way an IDE leaves it: nested, with files. + function made(name) { + const dir = path.join(root, name); + mkdirSync(path.join(dir, "EBWebView", "Default"), { recursive: true }); + writeFileSync(path.join(dir, "EBWebView", "Default", "History"), "x"); + writeFileSync(path.join(dir, "Local State"), "x"); + return dir; + } + + test("shutdownIde removes the IDE's folders once it is gone, and no other", () => { + const [temp, profile, other] = [made("a-tmp"), made("a-wv2"), made("a-other")]; + shutdownIde({ pid: NO_PROCESS, launcher: null, folders: [temp, profile] }); + assert.equal(existsSync(temp), false); + assert.equal(existsSync(profile), false); + assert.equal(existsSync(other), true); + }); + + test("shutdownIdeAsync does the same", async () => { + const [temp, profile, other] = [made("b-tmp"), made("b-wv2"), made("b-other")]; + await shutdownIdeAsync({ pid: NO_PROCESS, launcher: null, folders: [temp, profile] }); + assert.equal(existsSync(temp), false); + assert.equal(existsSync(profile), false); + assert.equal(existsSync(other), true); + }); + + test("a folder that is not there, a handle without folders and no handle are accepted", async () => { + const gone = [path.join(root, "never-made")]; + shutdownIde({ pid: NO_PROCESS, launcher: null, folders: gone }); + await shutdownIdeAsync({ pid: NO_PROCESS, launcher: null, folders: gone }); + shutdownIde({ pid: NO_PROCESS, launcher: null }); + await shutdownIdeAsync({ pid: NO_PROCESS, launcher: null }); + shutdownIde(null); + await shutdownIdeAsync(null); + }); + + test("removeTreeAsync removes a tree, and a folder that is not there", async () => { + const dir = made("c"); + await removeTreeAsync(dir); + assert.equal(existsSync(dir), false); + await removeTreeAsync(dir); + }); +}); From c4c0595f8487b9cbf4fb2e864f137b657e134139 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 14 Oct 2026 10:25:58 +0200 Subject: [PATCH 02/13] WIP notes: history removed, so they say only what to do and why; decision dates dropped --- WIP.A11y.md | 2 +- WIP.Build.md | 16 ++-- WIP.ExamplesBuild.md | 39 ++++----- WIP.Fonts.md | 8 +- WIP.Harness.md | 21 +++-- WIP.HelpAddin.md | 60 +++++++------- WIP.IDE.md | 6 +- WIP.Indexer.md | 75 ++++++++--------- WIP.Screenshots.md | 114 ++++++++++++------------- WIP.Search.md | 192 +++++++++++++++++++------------------------ WIP.Typography.md | 12 +-- WIP.md | 18 ++-- WIP.tbIDE.md | 4 +- 13 files changed, 262 insertions(+), 305 deletions(-) diff --git a/WIP.A11y.md b/WIP.A11y.md index b8aae7312..0e8241028 100644 --- a/WIP.A11y.md +++ b/WIP.A11y.md @@ -98,7 +98,7 @@ It has one blind spot: it compares a candidate against a baseline produced by th It also compares *which* findings axe produces, never their shape. The `no-html` scheme passes the gate on every audit and would still crash the reporter, because `noHtml: true` makes `node.html` null and `check_a11y.mjs` calls `.slice()` on it. Treat the gate as necessary, not sufficient. -Cost attribution for the scan lives in [perf/ab-axe.mjs](perf/ab-axe.mjs), with [perf/probe-axe-dom.mjs](perf/probe-axe-dom.mjs) for DOM operation counts and [perf/probe-axe-scaling.mjs](perf/probe-axe-scaling.mjs) for the size curve. The investigation, [builder/PLAN-axe-perf.md](builder/PLAN-axe-perf.md), is complete. Its conclusion: one vendored source patch, `SOURCE_PATCHES['plain-color-fields']` in `axe-scan.mjs` (see above), is worth **26 %** across a realistic page set and **30 %** on large pages. It is gated audit-by-audit by [check_a11y_fingerprint.mjs](scripts/check_a11y_fingerprint.mjs) and verified value-by-value by [check_axe_patch_equiv.mjs](scripts/check_axe_patch_equiv.mjs) -- both are needed, because the fingerprint gate compares `incomplete` as a rule-id set and would not notice a colour error that shifted ratios without flipping a pass/fail. The patch asserts an exact occurrence count at each substitution point, so an axe-core bump fails loudly rather than silently reverting. Before touching the scan: +Cost attribution for the scan lives in [perf/ab-axe.mjs](perf/ab-axe.mjs), with [perf/probe-axe-dom.mjs](perf/probe-axe-dom.mjs) for DOM operation counts and [perf/probe-axe-scaling.mjs](perf/probe-axe-scaling.mjs) for the size curve. The investigation, [builder/PLAN-axe-perf.md](builder/PLAN-axe-perf.md), concludes that one vendored source patch, `SOURCE_PATCHES['plain-color-fields']` in `axe-scan.mjs` (see above), is worth **26 %** across a realistic page set and **30 %** on large pages. It is gated audit-by-audit by [check_a11y_fingerprint.mjs](scripts/check_a11y_fingerprint.mjs) and verified value-by-value by [check_axe_patch_equiv.mjs](scripts/check_axe_patch_equiv.mjs) -- both are needed, because the fingerprint gate compares `incomplete` as a rule-id set and would not notice a colour error that shifted ratios without flipping a pass/fail. The patch asserts an exact occurrence count at each substitution point, so an axe-core bump fails loudly rather than silently reverting. Before touching the scan: - **Audit cost is super-linear in page size** -- `k = 2.73` on real pages, almost entirely inside `color-contrast`. The four largest pages in the site cost ~5.9x an average sample page each. - **Every axe-core upgrade needs the fingerprint gate run across it** -- axe ships new and revised WCAG rules between minors, and the source patches are pinned to the bundle's current text. diff --git a/WIP.Build.md b/WIP.Build.md index e91dc2bb5..5168ff1be 100644 --- a/WIP.Build.md +++ b/WIP.Build.md @@ -112,9 +112,9 @@ Three `.py` files stay, and none is an oversight: account of the blocked JavaScript port. - **`scripts/subset_font.py`** cuts each font an SVG screenshot embeds down to the characters it draws (`shoot_docs --svg`, [WIP.Screenshots.md](WIP.Screenshots.md)). It is - fontTools, the subsetter `build_fonts.py` already relies on. harfbuzzjs was tried first and - removed (owner: "don't mess with harfbuzz"); it is the library whose CFF2 defect keeps - `build_fonts.py` in Python. It loads a font without recalculating glyph bounds, because the + fontTools, the subsetter `build_fonts.py` already relies on. It does not use harfbuzzjs + (owner: "don't mess with harfbuzz"), the library whose CFF2 defect keeps `build_fonts.py` in + Python. It loads a font without recalculating glyph bounds, because the IDE's icon font stores loose glyph boxes and recalculating them moved icons a pixel, and it keeps the hinting (owner's decision) and the font's timestamp, so a picture's SVG is the same bytes from one run to the next. @@ -141,7 +141,7 @@ a change to `src/` is packed again by hand. See [Tools and Scripts](docs/Documentation/Tools-IDE.md#imagestrip). **Its own unit tests are twinBASIC `[TestFixture]` modules in `src/Sources/Tests/`**, with -the images they build in `TestImages`, run by `tbrun --tests` (owner, 2026-10-10), which reads +the images they build in `TestImages`, run by `tbrun --tests` (owner), which reads every `.twin` under `Sources/`. `TestImages.Cat` copies each `ParamArray` element through a `Variant` before assigning it to an array, around a twinBASIC defect queued in BUGS-TO-REPORT.md. @@ -153,7 +153,7 @@ build without one rule) fails it. The two are kept in step by hand: a change to strips is a change to both. `compactImage` in the same module is what the SVG screenshots (`scripts/svgshot/snapshot-svg.mjs`, [Pictures as SVG](#pictures-as-svg)) embed their images through, so a change to what it strips reaches their SVGs on the next `shoot_docs --svg` run. -What both strip was decided by the owner, 2026-10-10: +What both strip was decided by the owner: - **An embedded image keeps its pixels and any notice of whose it is, and nothing else.** The IDE's icons carry GIMP's EXIF, IPTC and XMP profiles (12 KB around a 28-pixel icon), @@ -723,8 +723,8 @@ build. CI workflows: over a hundred probes on fixtures, no tree, no install. The scanner's probes are the traps the packages' `.twin` sources contain, the community packages' included, and what a declaration's record says (signature, attributes with their arguments, description, return -type, accessor, the project name). A block of them holds the forms an audit of `parseTwin` -found it losing without a word (lists after `Inherits` and `Implements`, a qualified CoClass +type, accessor, the project name). A block of them holds the forms `parseTwin` could lose +without a word (lists after `Inherits` and `Implements`, a qualified CoClass interface, `[Hidden(False)]`, modifier-named and initialised fields, brackets in a Type, a result type before `Overrides`, an unmodified `Declare`, and the `#If` branches as alternatives), each with the compiler's answer in its comment, checked with `tbrun --tests` @@ -1148,7 +1148,7 @@ SVGs: online and book do, offline (and so the help archive) does not. Why: and the index audit agree without knowing about pictures. That adds a few hundred literal patterns, so `offlineExcluded` (and its copy in `check-tree.mjs`) compares a pattern with no wildcard as a string instead of compiling it. The archive is a stored zip, 37 MB without - the SVGs, and the 310 of them (2026-10-10) would add 18 MB. + the SVGs, and the 310 of them would add 18 MB. - **A picture's SVG is never inlined.** `figurePlugin` inlines a diagram's SVG in its figure; a picture's (`isPictureSvg`: an `.svg` with its `.png` beside it) stays an `` in the figure's container, and `tbdocs.mjs` does not put it in the `svgContentsMap` the workers diff --git a/WIP.ExamplesBuild.md b/WIP.ExamplesBuild.md index d8534142b..50a3a4172 100644 --- a/WIP.ExamplesBuild.md +++ b/WIP.ExamplesBuild.md @@ -96,7 +96,7 @@ const lang = tok.info ? tok.info.trim().split(/\s+/)[0] : ""; ``` The first whitespace-separated token is the language and the rest is discarded, so anything -after `tb` is invisible. This was verified against the real pipeline (`createMarkdownIt` +after `tb` is invisible. This holds in the real pipeline (`createMarkdownIt` plus `initHighlighter`, not a bare markdown-it; see WIP.md's [Source dashes](WIP.md#source-dashes)). Four properties are probes that ride along on every run: @@ -158,7 +158,7 @@ judgement rather than with a silence. An unknown reason is refused the way a bad and `inert` beside `check_build` is refused as a contradiction. The census prints three numbers: marked, inert by reason, and **undecided**. Only the last -is a backlog, and it is the one to drive to zero. The inert count should sit still. +is a backlog, and it is the one to keep at zero. The inert count should sit still. **`blocked` is the one reason that is not about the fence**, and **nothing carries it today**. It marks correct code that a *product* defect stops compiling. It exists so that @@ -330,9 +330,8 @@ the repository's to commit: four of the nine ship no licence file, and WinReg em one project, VBA_JSON's `Dictionary` class hid the `Scripting.Dictionary` that WinReg's samples name. - **A project does not see the names a package's own references declare.** WinReg embeds - WinDevLib, and its samples' `REGTYPES`, `BOOL` and `RegOpenKeyEx` (and, before 1.0.1.1, - `HKEY_CURRENT_USER`, which WinReg's own `RegKeyspace` now supplies) were unresolved - until `contributed-WinReg` referenced WinDevLib (at the version WinReg embeds, 8.8.0.507) + WinDevLib, and its samples' `REGTYPES`, `BOOL` and `RegOpenKeyEx` are unresolved unless + `contributed-WinReg` references WinDevLib (at the version WinReg embeds, 8.8.0.507) and the Scripting Runtime itself. The WinReg landing page tells the reader the same. - **An error inside an embedded package is the package's, and fails nothing** (`splitPackageFaults`). WinReg 1.0.1.1 and ArrayListLib 1.6.151.1 do not compile on BETA @@ -412,18 +411,15 @@ form with a `TextBox` on it, and impossible to compile alone, because the design than the code declares `Text1`. Declaring those instances lets the compiler check what the sample asserts: that the member exists, that it takes those arguments, that the types line up. -Against a bare template 99% of the 263 `Reference/Built-In` fences fail; with every -package referenced 78%; with the stage set 67%. - **The list is written out rather than inferred, and that is the point.** A rule over "an identifier ending in a digit" would also declare `Var1`, `Arg1`, `Line2`, `SQLITE3`, `VBA7`, `MySub1` and `IShellView2`, none of which is a control. The stage set is the tool's one deliberate fiction, so it stays explicit and greppable. **Stage entries are the biggest lever.** A flat tail of undeclared *names* is not a flat -tail of *work*: 45 undeclared VB control instances (`lblName`, `mnuFileSaveAs`, `optPlain`, -`dlgOptions`, `fraLeft`), each on one or two pages, cost one file edit and fixed 22 samples -in `Default/VB`. +tail of *work*: a VB control instance (`lblName`, `mnuFileSaveAs`, `optPlain`, +`dlgOptions`, `fraLeft`) used on one or two pages costs one file edit and fixes every +sample that names it. ## Batching, which is the whole cost question @@ -431,8 +427,8 @@ One project per fence is unaffordable. What makes it tractable is that **IDE cos project size**: what is paid for is startup, not compilation. **Fill the lanes, not the batches.** Filling each batch to `--batch` before opening another -put 120, 55, 4 and 3 samples on four lanes, and a run takes as long as its biggest batch. The -batcher sizes to `ceil(total / jobs)` instead. +would put 120, 55, 4 and 3 samples on four lanes, and a run takes as long as its biggest +batch. The batcher sizes to `ceil(total / jobs)` instead. ### A sample can pass on its neighbour's declarations @@ -440,9 +436,9 @@ Several samples share a generated project, and a `module`-slot sample's declarat **visible to every other sample in it**. So a sample can compile because an unrelated one defined what it referenced. Which samples share a project depends on the selection, so the same page can pass one run and fail the next. `Tutorials/Testing-with-Assert.md` defines -`PadLeft` in one fence and tests it in three others: a `--propose` survey compiled all 30 of -its fences together and passed, while the gate (marked fences only) put the definition in a -different project and the tests failed on `Unrecognized symbol 'PadLeft'`. +`PadLeft` in one fence and tests it in three others: a `--propose` survey compiles all 30 of +its fences together and passes, while the gate (marked fences only) can put the definition in a +different project, where the tests fail on `Unrecognized symbol 'PadLeft'`. `projname=` is the answer. It is markup rather than a heuristic because the grouping is a fact about the page that no scan recovers: @@ -619,8 +615,8 @@ the Form page's `True`). both. No template changes `project.warnings` today (the defaults leave every warning on), so the attribute is insurance. An error canary puts an error in every batch, so `tbbuild` never exits 0. An undefined-symbol canary is only the warning TB0002 under **Option - Explicit off**, where the name is declared implicitly, and stopped a full run on - `Core/Deftype.md:56`, a sample that compiles, in the `[implicit]` template. A batch that + Explicit off**, where the name is declared implicitly (a sample that compiles, such as + `Core/Deftype.md:56` in the `[implicit]` template, would fail it). A batch that crashes the compiler returns no rows at all, canary included; `runBatch` hands a crash to `isolateCrash` before it reads the canary, so that case never reaches it. @@ -824,8 +820,7 @@ that fails only the second is one LLVM cannot generate code for. **Count diagnostics, then try the fix.** A guess from counting diagnostics is not a result. "`[Me]` wants a fourth slot" (27 samples, TB5025): a bare `Class` wrapper fixes **5**, because `Me` becoming legal does not make `Me.Caption` resolve: the -wrapper has no `Caption` (`TB5027 [tbx_…] does not contain 'Caption'`). The largest win was -the stage set. +wrapper has no `Caption` (`TB5027 [tbx_…] does not contain 'Caption'`). ### The Class row, and why it needs a base @@ -933,7 +928,7 @@ Language rules: ### What cannot be marked -The 52 `inert` fences are not programs: syntax skeletons with +The `inert` fences are not programs: syntax skeletons with `` names (`Interfaces-CoClasses.md` writes `Inherits base_interface`), property-assignment lines shown outside their `With` (`WinServicesLib/ServiceManager.md` documents each property as `.Name = "..."`), continuation fences that deliberately reuse the @@ -958,6 +953,6 @@ a reader writes. one line each, because its purpose is "the control instances the samples assume". The invented *classes* go in `hidden` fences, because a class is not a control instance and a page is where it belongs. The line between the two is a judgement, not a rule. -- Which pages should carry `check_run` (33 samples do)? A sample states its output in its own +- Which pages should carry `check_run` (88 samples do)? A sample states its output in its own text, as a trailing comment on a `Debug.Print` line or as the comment lines under `' Output:`; its comparison is only as useful as the pages that state a printed value. diff --git a/WIP.Fonts.md b/WIP.Fonts.md index be97efef4..bb5059c4a 100644 --- a/WIP.Fonts.md +++ b/WIP.Fonts.md @@ -131,12 +131,12 @@ There is a secondary design point for upstream HarfBuzz --- a subsetter that can CFF2 instance metrics should refuse rather than emit a font with mismatched outlines and an invalid `head` --- but the actionable report is harfbuzzjs's. -**Not yet reported upstream (last checked 2026-09-21).** A full sweep of both trackers +**Not yet reported upstream.** A full sweep of both trackers (every open issue and PR, closed-issue term searches, Discussions, and the `subset-font` / -`subfont` / `cn-font-split` wrappers) found nothing. Two look-alikes are different bugs: #6068 +`subfont` / `cn-font-split` wrappers) finds nothing. Two look-alikes are different bugs: #6068 (HVAR advances frozen on partial instancing; fixed) and the open #4029 (glyf `hhea` bounds not recomputed when the metrics map is empty --- the same shape, a different table). Both -repos' `main` still carried the defect when checked. +repos' `main` carried the defect when last checked. **Status.** Decided: wait for the harfbuzzjs fix rather than take the Pyodide route or leave the script Python for good. harfbuzzjs 1.6.2 is a pure HarfBuzz 14.5.0 bump @@ -170,7 +170,7 @@ built by fontTools and are correct. When the port happens: The only thing that meets all six is fontTools itself under Pyodide (`@web-alchemy/ fonttools`, ~15 MB, pinned to a stale 4.56.0) --- CPython in WASM, which satisfies "no Python" as a technicality and nothing more. -- One workaround was proposed and **declined**: stop pinning `opsz` on Source Serif, keep +- One workaround is **declined**: stop pinning `opsz` on Source Serif, keep the axis, and have `print.css` ask for `font-variation-settings: "opsz" 11`. It would sidestep the bug and cost ~70 KB in a PDF-only face, but it changes what the generator produces to suit a tool defect. Do not re-propose it; the pin stays. Switching Source diff --git a/WIP.Harness.md b/WIP.Harness.md index 973c148a5..08608065c 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -144,8 +144,8 @@ comment in the file: `E.[Hidden]` and `E.Hidden` are 0, `[Description("x")]` is a member of that name, and `[NoSuchAttribute]` too. Code after the group (`[Hidden] Red = 5`) is refused, TB5182 at `Red`. So the census records no attribute for such a group (`scripts/lib/census-scan.mjs`, - probes in `check_twin_parsers.mjs`); it once did for the 35 `[_]` members VBCCR's - `LongPtr` Enum declares in the samples. + probes in `check_twin_parsers.mjs`), the 35 `[_]` members VBCCR's `LongPtr` Enum declares + in the samples included. - **A comment can sit anywhere**: inline `/* voffset &H00A8*/ Property Get X()` before a declaration (DAO.twin), a trailing `' NOTE: ...` after a `]`, a whole `'` line *between* two attribute groups (VBA/Strings.twin; 142 sites), or a `#If` between an attribute and @@ -352,7 +352,7 @@ is written to stdout or stderr, ever**, it exits 0 with the `.exe` written when in code nothing calls, and for an error the build reaches --- an unknown procedure or type, an undeclared variable, no `Sub Main` --- it does not exit at all: it sits on a "Please wait…" dialog at 100% forever. It is silent, falsely green, and hangs on the one case worth -catching. Measured all three ways, on BETA 983 and 995 (the BUGS-TO-REPORT entry on +catching. All three hold on BETA 983 and 995 (the BUGS-TO-REPORT entry on `--buildAndExit32`). **It runs the IDE on a private Windows desktop, and that is not decoration.** A build tool @@ -388,7 +388,7 @@ reports. Two rules keep the cause readable. With its streams redirected, PowerSh progress records and errors to stderr as CLIXML (`#< CLIXML`, as `tb-registry.mjs` also found, below), so the script silences progress and writes a failure itself, as plain UTF-8 text. And a Win32 error read from PowerShell is not the call's: PowerShell makes calls of its -own before the next statement runs, and a `CreateProcess` that had set 3 was reported as 203, +own before the next statement runs, so a `CreateProcess` that set 3 reads as 203, "The system could not find the environment option that was entered". Each call is made, and its error read, in C#. @@ -449,8 +449,8 @@ Seven rules about the harness, each a comment in the file: helper the IDE is not a descendant of anything the harness spawned, so the helper reports the pid on stdout and the harness kills that. -**A crashing probe is a real risk.** Four `Debug.ExecuteHostCommand` argument-shape probes in -one project took the compiler down repeatedly. Keep a question that might crash the compiler +**A crashing probe is a real risk**: `Debug.ExecuteHostCommand` argument-shape probes take the +compiler down repeatedly. Keep a question that might crash the compiler in a project of its own, so the answer is attributable and one bad probe cannot cost the other thirty their run. @@ -548,8 +548,7 @@ Rules it follows: failed due to licence error" and builds nothing. `#appOverlay` is a transparent element over the whole page, shown while `lockUICount` is above zero: by every modal dialog, the "Compiling..." and project-loading progress dialogs among them, and by an open menu. A press - then fails with "its centre is covered by #appOverlay" (both were seen once each in a full - `bug_repro verify`). A licence still unset after the wait throws; an overlay still up is + then fails with "its centre is covered by #appOverlay". A licence still unset after the wait throws; an overlay still up is left to `click`, which names it. - **A click that fails saves a picture of the page** (`pagePicture` in `tb-click.mjs`, CDP `Page.captureScreenshot`, which works on the private desktop) under `%TEMP%\tb-click\`, and @@ -911,7 +910,7 @@ copy. Put back as found, it would point `.twinproj` files at a folder that has b and other sessions run `examples.bat` while add-in tests run copies, so the overlap is ordinary. `startTidy` notes whether the association it recorded names the temp folder, and if it did, `finishTidy` puts it back with every path to a copy's `twinBASIC.exe` replaced by -`findIde()`'s, the newest install on the Desktop (owner, 2026-10-10): what that install's IDE +`findIde()`'s, the newest install on the Desktop (owner): what that install's IDE writes when it is next started. With no install outside the temp folder it leaves the association as the IDEs set it and says so. @@ -1196,7 +1195,7 @@ Eight rules: gone. So a script that edits and then sets a breakpoint calls `waitForEditAnswer` first, as `shoot_docs.mjs`'s `breakpointOn` and the `debugging` lane do. It waits until the file is no longer `pendingChangeConfirmation`. A person rarely presses F9 that fast, and the defect is - not queued (owner, 2026-10-12). + not queued (owner). - **A click checks where its press lands** (the `assert` lane of `ide-test.bat` shows it). The page can change between the call that aims and the press, a few milliseconds to a hundred later on a busy page. In an editor the debugger has just @@ -1382,7 +1381,7 @@ probe lanes: list --- a check of a live site has to attach to it separately. The scenario turns on `Runtime` to record page exceptions, which is how P12's `TypeError` is seen. Its last test is not a numbered probe: a third button opens two tool windows given no id, - which turn out to be one window. + which are one window. - [test/addin/symbols.test.mjs](test/addin/symbols.test.mjs), P5: no add-in. It opens the project in [test/addin/probes/symbols](test/addin/probes/symbols), which references tbIDE and is never built, and asks the compiler about names in it the way the IDE's own code diff --git a/WIP.HelpAddin.md b/WIP.HelpAddin.md index 962722cee..f196abb0b 100644 --- a/WIP.HelpAddin.md +++ b/WIP.HelpAddin.md @@ -599,7 +599,7 @@ has the harness side of each item. under the runner leaves its registry entries to the runner, which sweeps only the lanes' folders. -Stage 1's acceptance: both scenarios pass under `addin-test.bat`, and the whole registry, +Stage 1 holds when both scenarios pass under `addin-test.bat` and the whole registry, `IDESettings` included through hashes, is identical around the run. - **Sample 10:** toolbar button, then its tool window, then a message box (the three-button @@ -854,8 +854,8 @@ not: a restart saves a harness about a second against opening a new IDE. rounds, and rows of 22.75 px overflowed a list of seven by a pixel each, which showed its scrollbar. The vertical scrollbar is left on the list view's default, shown only when there is something to scroll. - - **The line between the list and the page** (Wayne, 2026-10-09: nothing ended the list - and began the page) is the top border of `#helpPage` and `#helpSummary` while + - **The line between the list and the page** (Wayne: it ends the list and begins the + page) is the top border of `#helpPage` and `#helpSummary` while `#helpResults` shows (a sibling rule on the list's inline `display`): `2px solid color-mix(in srgb, var(--themeToolWindowBodyForeColor) 40%, transparent)`, the theme's text colour at 40%, so it follows every theme with no rule of its own. The @@ -931,7 +931,7 @@ not: a restart saves a harness about a second against opening a new IDE. `Page.getFrameTree` reads its URL. The offline tree cannot be used: a page served over http cannot frame a `file://` URL. So the lane needs `build.bat` first. 3. **Context: which `Add`?** Tested by the same lane on BETA 995. **The route is the - page's socket** (the owner, 2026-10-04): the add-in's own parser would have to be close + page's socket** (owner): the add-in's own parser would have to be close to a whole twinBASIC parser, so it waits, and the socket stands until upstream gives the API a call for it. For the name under the cursor, F1 asks the compiler's hover about the character the name starts from (`Words.NameColumn`); a selection is looked up as it @@ -967,16 +967,16 @@ not: a restart saves a harness about a second against opening a new IDE. the project's `Beep`. - The socket code is in files of its own, `CompilerHover.twin` and `hover.js`, without comments, so that it can be replaced whole once the API has a call. -4. **A name with no page**, tested by the same lane on BETA 995, at the owner's - request (2026-10-04): a user who documents their own code with `[Description]` sees it, - and is not sent to a page for another name. When the hover names a package the index +4. **A name with no page**, tested by the same lane on BETA 995 (owner): a user who + documents their own code with `[Description]` sees it, and is not sent to a page for + another name. When the hover names a package the index does not document --- the project, another package, a type library --- F1 shows, in place of the frame, the declaration, the description and where the name is declared, all from the hover (P15), read by `Declared.ReadSummary`; a name with none says `No description.`. *Open in browser* is disabled meanwhile, and showing a page puts the frame back. The saved page is unchanged, so a restart shows the last page. - `ReadHover` also reads `in library `, a class's or module's: a class of the - project named as a documented one no longer finds that one's page. + project named as a documented one does not find that one's page. - On a procedure's name where it is declared, the hover is the code-generation block, so F1 says `No help for 'Beep': it is declared here`. - **The lane's own race.** F1 on the name whose page shows already sets the same `src` @@ -984,12 +984,11 @@ not: a restart saves a harness about a second against opening a new IDE. `f1Shows` marks the page before pressing F1 and waits for a page without the mark. A page showing again after a summary is waited for too, since its `src` can already be right. -5. **Offline help**, tested by the lane `help-offline` on BETA 995, at the owner's - choice (2026-10-05): the add-in's own server, written in twinBASIC, serving one archive - embedded in the DLL. The owner's other choices: the archive is a resource; the - documentation build makes it (owner, 2026-10-06); the pane uses - it when the DLL has it, and the live site otherwise; the symbol index stays a resource of - its own. +5. **Offline help**, tested by the lane `help-offline` on BETA 995 (owner's choice): the + add-in's own server, written in twinBASIC, serving one archive embedded in the DLL. The + owner's other choices: the archive is a resource; the documentation build makes it; the + pane uses it when the DLL has it, and the live site otherwise; the symbol index stays a + resource of its own. - **The archive** is `add-in/Resources/HELP/site.zip`, gitignored, written from `docs/_site-offline` by the `helpArchive` task of a build of the documentation, once the offline tree is complete and before the Gantt chart goes into `BuildInfo.html` @@ -997,7 +996,7 @@ not: a restart saves a harness about a second against opening a new IDE. skips it, and CI never passes that, since the download carries the archive) (about 26 MB, 1,475 files; the DLL builds with it for win32 and win64). An add-in built without it serves nothing - and uses the live site, as before. The offline tree is the one served: its links are + and uses the live site. The offline tree is the one served: its links are relative, so they work on any origin, and its pages hold only their part of the nav tree. - **No inflating in twinBASIC.** A deflated entry is sent as it is, between a 10-byte @@ -1023,15 +1022,15 @@ not: a restart saves a harness about a second against opening a new IDE. offline-only case asks the server from Node: a page's gzip body and a stored image byte for byte, a folder's 301, a 404 and `HEAD`. 6. **Hover help**, tested by the `help` and `help-offline` lanes on BETA 997, with - the owner's choices (2026-10-06): links in the IDE's hover under the mouse, behind one + the owner's choices: links in the IDE's hover under the mouse, behind one *Hover help* checkbox in the settings panel (increment 8), off by default and kept with `SaveSetting` (`Hover`, `On`). It shows a link per page the name has, at most five, labelled with the name, its kind and where it is, as in *Help: MsgBox function (VBA.Interaction)* (the - owner's wording, 2026-10-07; `SymbolIndex.Place`); a click shows that page in the pane, + owner's wording; `SymbolIndex.Place`); a click shows that page in the pane, as F1 does. A name with no page shows nothing, since the IDE's hover already shows its declaration and description. `SymbolIndex.Pages` is the lookup F1 and hover help share. `HoverHelp` raises `Pick` with the page's path. - - **At the cursor there is nothing** (owner, 2026-10-07). A widget above the line, added + - **At the cursor there is nothing** (owner). A widget above the line, added through the public API once the cursor had rested on a name (P16), is not used: it covers the line above whenever typing pauses on a name, does not look like the IDE's own hover, and gives nothing the mouse hover and F1 do not. P16 says what it costs, should @@ -1041,7 +1040,7 @@ not: a restart saves a harness about a second against opening a new IDE. an earlier instance of the add-in left in `window.tbDocsHoverHelp`, and registers new ones while the box is ticked. The provider is for `"twinbasic"`, so the links are the hover's first lines, next to the name, where a long hover shows them without being - scrolled (the owner's choice, 2026-10-06; a provider for `"*"` puts them below the + scrolled (the owner's choice; a provider for `"*"` puts them below the compiler's text). The provider asks the compiler's hover itself, passes the line, the column and the hover to the add-in, and waits up to 4 s for the markdown it answers with (`window.tbDocsHoverHelp.answer`). The links point at the live site; a @@ -1059,9 +1058,8 @@ not: a restart saves a harness about a second against opening a new IDE. move it against the window's right edge before the mouse rests anywhere; and a hover left open covers the toolbar, so each mouse case moves the mouse away at its end. `test/addin/hover.mjs` holds the helpers both lanes use. -7. **A detached window**, tested by the `help` and `help-offline` lanes on BETA 997, - at the owner's request (2026-10-07): the pane can be moved to another monitor. The - owner's choices: the add-in shows a window of its own, a twinBASIC Form holding the +7. **A detached window**, tested by the `help` and `help-offline` lanes on BETA 997 + (owner's request): the pane can be moved to another monitor. The owner's choices: the add-in shows a window of its own, a twinBASIC Form holding the WebView2 package's control (P19), rather than a `window.open` from the page, which would be page internals and which the host most likely sends to the browser (the host DLL names `NewWindowRequested` and imports `ShellExecuteA`; not tried, since it might open @@ -1070,7 +1068,7 @@ not: a restart saves a harness about a second against opening a new IDE. **Attach** in the window's title bar hides the window and shows the pane. The window is made once and then hidden and shown, so its page is kept. It has no owner window and shows in the taskbar. - - **The title bar** (owner, 2026-10-07) is one row drawn by the page, in place of the + - **The title bar** (owner) is one row drawn by the page, in place of the Windows caption, which is light even in the dark theme. It is the IDE's tool-window header: `.sectionHeader`, with `--themePanelHeaderBackColor` and `...TextColor`, the theme's `...Border*`, `...Padding` and `...FontSize`, weight 600, the 6-dot grip at the @@ -1175,8 +1173,8 @@ not: a restart saves a harness about a second against opening a new IDE. caption (no read-back of the DWM attribute), Alt+F4, the registry read, maximizing on a monitor other than the primary one (the lab machine has one), and the move itself, whose loop needs the real mouse, which the harness's desktop has none of. -8. **Settings**, tested by the `help` and `help-offline` lanes on BETA 997, at the - owner's choice (2026-10-07): a gear at the right end of the pane's bar, and on the detached +8. **Settings**, tested by the `help` and `help-offline` lanes on BETA 997 (owner's + choice): a gear at the right end of the pane's bar, and on the detached window's title bar (in its bar under the Windows caption), opens a drop-down panel of settings, one label row each. A click outside it, Esc, or the gear closes it; changing a setting leaves it open. Each view has its own panel; the settings themselves go through @@ -1186,7 +1184,7 @@ not: a restart saves a harness about a second against opening a new IDE. `blur` closes the panel as well. The lanes' pane frame is on the IDE page's site, so one case points it at `127.0.0.1` to check the shade with a cross-site frame. 9. **Only the packages the project references**, tested by the `help` and - `help-offline` lanes on BETA 997, at the owner's choice (2026-10-07): F1, the hover links + `help-offline` lanes on BETA 997 (owner's choice): F1, the hover links and the search offer the language's own entries and the packages the project references; an *All packages* row in the settings panel, off by default and kept with `SaveSetting` (`Packages`, `All`), offers every package. `LoadPackages` reads the references at @@ -1198,7 +1196,7 @@ not: a restart saves a harness about a second against opening a new IDE. F1 on a name only an unreferenced package documents says *No help for 'X' in the packages this project references*. 10. **How a user gets an add-in with the archive.** The project file is a download, and it - is the offline add-in (owner, 2026-10-09). Every docs build, CI's included, packs + is the offline add-in (owner). Every docs build, CI's included, packs `add-in/` into `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the online tree (`builder/addin-project.mjs`, the `addinProject` task), and *Getting the add-in* links to it first, the source route second. It holds the files git tracks, the symbol index @@ -1207,7 +1205,7 @@ not: a restart saves a harness about a second against opening a new IDE. serves the pages itself with no internet; CI passes no `--no-help-archive` for that reason. A build that wrote no archive packs none. The offline tree holds no copy, and its links to the file go to the website. The reader still builds the two DLLs in the - IDE. **Not done:** the DLLs themselves, built in CI --- the owner's plan (2026-10-07) is + IDE. **Not done:** the DLLs themselves, built in CI --- the owner's plan is to build them there with everything else, once the compiler runs on Linux there. Why the pack is made the way it is: [WIP.Build.md](WIP.Build.md#the-help-add-ins-project-file). @@ -1283,9 +1281,9 @@ the route once the socket is gone, if the API never gains a call for it: Recommended, and not yet confirmed: - **Page internals are for probes only, with two exceptions the owner made**: increment 3 - asks the compiler's hover through the page's socket, until the API has a call for it - (2026-10-04), and increment 6's mouse route registers a Monaco hover provider through the - page (2026-10-06, P17). Otherwise the shipped add-in uses the public API and + asks the compiler's hover through the page's socket, until the API has a call for it, + and increment 6's mouse route registers a Monaco hover provider through the + page (P17). Otherwise the shipped add-in uses the public API and `ShellExecuteW`, and whatever the API lacks is requested upstream. P4 showed the route exists: any inline handler can reach the page's globals, `openEditors`, the socket to the compiler and `hostAppObject` among them. `raiseEvent` in a list view's items is diff --git a/WIP.IDE.md b/WIP.IDE.md index fbb4d9e6f..2f88f2977 100644 --- a/WIP.IDE.md +++ b/WIP.IDE.md @@ -3,14 +3,14 @@ The IDE section (`docs/IDE/`, `/tB/IDE`) is being rewritten so that every page is fit for the book. The goal is that `docs/_book.yml` holds no IDE entry under `left_out:`. This file covers the structure, the conventions, where each fact comes from, and how a chapter is -written. Owner decisions, 2026-10-11: +written. Owner decisions: - task chapters plus a reference part; - permalinks flattened, with redirects; - the how-to material in `Features/Compiler-IDE` moved here; - `docs/IDE/_Images/*.af` deleted. -The section is not bound to the structure it had. The existing pages are raw material. +The section is not bound to its current structure. The existing pages are raw material. ## What "book-ready" means for a page @@ -45,7 +45,7 @@ children. Second person ("you") is allowed here, as in tutorials. and the keyboard shortcuts, under `/tB/IDE/Reference/…`. Each entry gives the item, its shortcut, one or two sentences, and a link to the chapter that explains it. -- The keymap is shown as a table, not the JSON dump `Menu/Window.md` holds now. The table can be +- The keymap is shown as a table, not the JSON dump `Menu/Window.md` holds. The table can be generated from `main.js` if keeping it by hand proves error-prone. Decide that when the page is written. - Webpage and the other panes that are not in a chapter get a reference entry. diff --git a/WIP.Indexer.md b/WIP.Indexer.md index 766cc0f5e..e871cd4d4 100644 --- a/WIP.Indexer.md +++ b/WIP.Indexer.md @@ -36,14 +36,14 @@ opens from a deeply nested clone otherwise. from the working tree. A `sync` against a newer build rewrites the working-tree snapshots, and the baseline has to stay what the pages say until the pages and the new snapshots are committed together. It reads with a buffer of 1 GB because VB's snapshot is larger than the -1 MB `execFile` default, which once made VB fall back to an audit. +1 MB `execFile` default, and VB falls back to an audit with the smaller buffer. -**The manifests live in the store, not in the repository.** They were committed once, and two -things went wrong. After a pull, the manifest said the new build had been read while the store, -which is not committed, held old sources or none, so `sync` printed "Up to date" over stale -data. And a `sync` limited to some packages (`--package`) moved the whole manifest to the new -build, so the packages it skipped looked current and were not. Now the manifest says only what -was last fetched, a clone has none, and its first `sync` reads every package. Whether a package +**The manifests live in the store, not in the repository.** A committed manifest goes wrong +two ways. After a pull it says the new build has been read while the store, which is not +committed, holds old sources or none, so `sync` prints "Up to date" over stale data. And a +`sync` limited to some packages (`--package`) moves the whole manifest to the new build, so +the packages it skipped look current and are not. So the manifest says only what was last +fetched, a clone has none, and its first `sync` reads every package. Whether a package is current is judged from what the store holds: its sources, an `api.json` in the format this indexer writes, of the version the source has now, written by the scanner this checkout has. @@ -51,8 +51,8 @@ indexer writes, of the version the source has now, written by the scanner this c them*. The store records, beside each `api.json`, a SHA-256 over `scripts/lib/twin-api.mjs`, `scripts/lib/twin-declarations.mjs`, `indexer/lib/api-model.mjs` and `indexer/lib/vb6-preprocess.mjs` (line ends made alike, so a CRLF and an LF checkout agree). -Without it a scanner fix left every store's `api.json` "current" and `analyze` compared output -of the old scanner. With it, `sync` indexes the package again from the stored sources, with no +Without it a scanner fix would leave every store's `api.json` "current" and `analyze` would +compare output of the old scanner. With it, `sync` indexes the package again from the stored sources, with no download and under the version it had. That is why the store keeps each package's `Settings` file (the project name). The list is every module `api-model.mjs` imports, and the unit test fails when an import is added and the list is not; `package-files.mjs` is not on it, because @@ -65,11 +65,10 @@ paths are relative to `Sources/`. ## One scanner **`parseTwin` in `scripts/lib/twin-api.mjs` is the only scanner**; the indexer has no lexer or -extractor of its own. It had them once, and they misread what the shared scanner reads right: -a field named for a keyword opened a container nothing closed, a `Public CoClass` was missed, -so were Delegates and a `/*Private*/ Module`, and the values of a nested Enum and the fields of -a nested Type were dropped. On the same input `parseTwin` found a superset. Two scanners also -drift: the symbol index (`builder/package-api.json`) and the indexer would disagree about +extractor of its own, which would misread what the shared scanner reads right: a field named +for a keyword opening a container nothing closes, a `Public CoClass`, Delegates and a +`/*Private*/ Module` missed, and the values of a nested Enum and the fields of a nested Type +dropped. Two scanners also drift: the symbol index (`builder/package-api.json`) and the indexer would disagree about one package, and each fix would be made twice. The scanner is pinned by `scripts/check_symbol_index.mjs`, `scripts/check_twin_parsers.mjs` and `test/indexer.test.mjs`; its header comment lists the ways to misread this corpus. @@ -114,8 +113,8 @@ Branches of an `#If` are read as alternatives that each start from the block str `format` changes whenever the shape does, and `sync` indexes a package again when the store holds another number. Format 3 merges the declarations in the branches of an `#If` (`#If VBA7 Then ... As LongPtr #Else ... As Long`) into **one member with a `signatures` list**, -a condition on each. Before, the branches were separate members with no condition, so an -`#If` pair looked like overloads and a changed condition changed nothing the differ could see. +a condition on each. Separate members with no condition would make an `#If` pair look like +overloads and hide a changed condition from the differ. The differ reads the merged form, with the condition in front of each signature, so that a changed condition, a new branch or an edited signature each modifies the one symbol of that name and kind. The rules for what counts as an alternative (same block, name, kind, accessor @@ -125,14 +124,13 @@ overloads) are in the README. **A change to the fingerprinted code or to `format` means regenerating the committed snapshots from the build they record (`indexed_from`), in the same commit as the change.** If it does not, `analyze` compares the new scanner's output with the old scanner's and reports the -scanner's changes as changes to the API. The BETA 997 snapshots were regenerated this way when -the scanner changed. It needs that build's install, `sync --ide `: +scanner's changes as changes to the API. It needs that build's install, `sync --ide `: `--release` downloads only the newest release, and a plain `sync` reads the newest install, which would write the snapshots of another build. ## Decisions of the owner -- **Snapshots are committed only for the documented packages, to start with**: 14 files, 3.2 MB +- **Snapshots are committed only for the documented packages**: 14 files, 3.2 MB (about 350 KB compressed). The contributed packages and the extra CEF builds are indexed into the store and left out of the repository. A package joins when its section does; `sync` copies a snapshot for a documented package it indexed in that run, and gives a documented @@ -174,12 +172,10 @@ which would write the snapshots of another build. `builder/package-api.json` is the input of the help add-in's symbol index ([WIP.HelpAddin.md](WIP.HelpAddin.md#stage-3-the-symbol-index-generated-by-the-docs-build)). -`twin-index derive` writes it (`lib/package-api.mjs`), and `scripts/build_package_api.mjs`, -which wrote it before, is gone. That tool exported every package through the compiler -harness's impexp into a cache and read the exported folders; `derive` reads the `.twinproj` +`twin-index derive` writes it (`lib/package-api.mjs`). It reads the `.twinproj` files with the reader `sync` uses and parses them with `parsePackageFiles`, the parse -`buildApi` makes, so the two outputs come from one reading. Its output was byte-identical to -the old tool's at BETA 997, and it takes half a second with no cache. +`buildApi` makes, so the two outputs come from one reading. It takes half a second with no +cache. - **It reads the packages, not the snapshots or the store.** An `api.json` sorts types and members by name and drops line numbers, and `apiSnapshot` needs declaration order: the @@ -193,9 +189,8 @@ the old tool's at BETA 997, and it takes half a second with no cache. - **It stays committed.** It is a build input `check_tree_fresh.mjs` watches, and CI has no packages to derive it from. `derive --check` is outside every gate for the same reason. - **A scanner change can change it**, as it can the snapshots. The `[ ]` Enum member (a - bracket group alone on a line is a member) gave VB's `ReportBaseCtl.ReportSizeConstants` - its 17 values at BETA 997, the first named `" "`, which the committed file lacked until it - was derived again. + bracket group alone on a line is a member) gives VB's `ReportBaseCtl.ReportSizeConstants` + its 17 values at BETA 997, the first named `" "`. ## `draft` and the doc-draft workflow @@ -205,8 +200,8 @@ the old tool's at BETA 997, and it takes half a second with no cache. - **Placement is read from each section, not from a convention**, because the sections do not share one: the built-in packages keep enumerations in `Enumerations/`, the contributed ones beside the classes; a class is `X/index.md` with member pages, `X.md` with its members - inline, or `_App/_App.md`. The draft workflow this replaced derived every path from one rule - (`/.md`, `.md`), which matched no section exactly. Every rule in + inline, or `_App/_App.md`. One rule for every path (`/.md`, `.md`) matches no + section exactly. Every rule in `draft-plan.mjs` is a majority over the section's own pages. - **How well it places, checked by leaving each symbol out** of every section with a store package (the thirteen built-in and default ones at BETA 997, and the nine contributed ones of @@ -221,12 +216,12 @@ the old tool's at BETA 997, and it takes half a second with no cache. folder the check had itself taken away. The four members that miss are documented unlike the others of their kind in their type, or not at all. The rule that places a type in the folder of the type returning it is limited to folders that already keep a type, because - without that limit it moved Printer under Printers, Dictionary under JsonConverter and + without that limit it would move Printer under Printers, Dictionary under JsonConverter and eight others that the sections keep at their roots. - **A new section borrows its model pages from its group.** Its pages have no sibling of - their kind to follow, so the plan of the first package drafted into a new section - (tbRegExp) gave every task `model: null`, and the agents would have invented a page - layout. A page of a kind its section lacks now takes a model from the group's largest + their kind to follow, so the plan of the first package drafted into a new section would + give every task `model: null`, and the agents would invent a page + layout. A page of a kind its section lacks takes a model from the group's largest section that has one, of the same shape: a folder's `index.md` for a class that gets a folder, never a one-page class. - **A permalink is computed only where the rule holds for every existing page** of the @@ -257,12 +252,12 @@ the old tool's at BETA 997, and it takes half a second with no cache. - **The agents are Sonnet**, as the content agents of the contributed packages were; the drafts are read against the sources before they are committed, which no model choice replaces. -- **The first run, tbRegExp 1.0.5.0 (28 pages, 5 batches), took 7 agents, 7 minutes and - 0.96 M subagent tokens**, and its 30 samples compiled and ran on the first Check. The - review still found what no compile finds: the drafts state only what the code says, so +- **A batch of 28 pages in 5 batches takes about 7 agents, 7 minutes and 0.96 M subagent + tokens** (tbRegExp 1.0.5.0), and its samples compile and run on the first Check. The + review still finds what no compile finds: the drafts state only what the code says, so where the package differs from the object it replaces (VBScript's **RegExp**, run under - `cscript`) nothing said so, and claims read from the code but never run (`$0`, a lone `$`) - needed a probe page. Running the doubts is now the Check agent's (above); the review + `cscript`) nothing says so, and claims read from the code but never run (`$0`, a lone `$`) + need a probe page. Running the doubts is the Check agent's (above); the review still reads the pages against the sources. - **It is not in the published docs.** `draft` is (Tools-Build), because its plans serve a person writing pages by hand; the workflow is described here and in `WIP.md`, because @@ -287,6 +282,6 @@ the old tool's at BETA 997, and it takes half a second with no cache. describes a third party's HTTP API, which is neither a reason nor a decision, and folding 239 lines of it into this note would bury both. Its one reader is `lib/twinserv-client.mjs`, which calls only `query` and `download`; the rest (login, publish, version check, the IDE-to-compiler - commands) is there for whoever writes a publisher tool. It was reverse-engineered from - BETA 983 (June 2026) and keeps no copy of the IDE files it came from. Check it against the + commands) is there for whoever writes a publisher tool. It is reverse-engineered from + BETA 983 and keeps no copy of the IDE files it came from. Check it against the current IDE before relying on anything but those two endpoints. diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md index 920c05857..5dd1c5079 100644 --- a/WIP.Screenshots.md +++ b/WIP.Screenshots.md @@ -2,8 +2,7 @@ The documentation's IDE screenshots go stale with every beta. `scripts/shoot_docs.mjs` retakes them from a live IDE: every picture of the IDE the documentation holds, annotations included, -and the program windows and web pages that sit beside them. The owner's decisions behind it are -dated 2026-10-08 onward. +and the program windows and web pages that sit beside them. The owner's decisions behind it are marked (owner). ## What there is to retake @@ -18,7 +17,7 @@ Ten images under `docs/`, each for a stated reason: - **The six GitHub walkthrough pictures** of `Documentation/Building.md` (compare-changes, create-pull-request, merge-pull-request, confirm-merge, choose-workflow, run-workflow): the - owner's to retake by hand, with a signed-in account (owner, 2026-10-09); no agent touches them + owner's to retake by hand, with a signed-in account (owner); no agent touches them or their prose. They show base `main` and the old workflow name, and confirm-merge an e-mail address, until then. - **`packPublishPackage1`**: `packagePublisherPublish` opens the sign-in form when no publisher @@ -27,7 +26,7 @@ Ten images under `docs/`, each for a stated reason: - **`Tutorials/WebView2/Images/tbWebView2InAForm.gif`** is skipped (owner). - **`favicon.png`** is never changed by this work (owner). -## Decisions (owner, 2026-10-08) +## Decisions (owner) - **One tool.** `shoot_docs.mjs`; the help add-in's pictures are one setup among the others. - **2x pictures, shown at half size**: every retaken picture gets `{:width="W" height="H"}` (half @@ -48,15 +47,14 @@ Measured on BETA 997: `body.scale150`, 22.67 px menu rows). `--force-device-scale-factor=1` in the WebView2 browser arguments (`Lane`'s `browserArgs`) gives `scale100` and integer sizes; `Emulation.setDeviceMetricsOverride` at `deviceScaleFactor: 2` then gives a clean 2x. Menus and - dialogs come out at the CSS sizes of the pictures taken before the fix to the pixel (File - 189x336, Manage Keyboard Shortcuts 1016x736). + dialogs come out at their 1x CSS sizes (File 189x336, Manage Keyboard Shortcuts 1016x736). - **No-project start.** `Lane.open` always opens a project. `Lane.openNoProject()` (`launchOnDesktop` with no argument, then `attachIde`) starts the IDE as it shows its splash and then New / Open Project, with every menu in its no-project state. The lane's copy of the install leaves out `projects\` (29 MB): New Project then shows 2 templates and no samples, so a setup that shows them copies `projects\` into the lane's copy. -- **Theme** (owner, 2026-10-09). Every picture is taken in the dark theme - (`tbTheme_SwitchToDarkMode`) as `X.png`, byte for byte the picture it always was, and in the +- **Theme** (owner). Every picture is taken in the dark theme + (`tbTheme_SwitchToDarkMode`) as `X.png`, and in the **Light** theme (`tbTheme_SwitchToLightMode`, not Classic) as `X.light.png` beside it. The site shows the light one in its light theme and the dark one in its dark theme, following the toggle and, with no choice stored, the OS; pages are not edited, the build pairs the files by @@ -82,7 +80,7 @@ Measured on BETA 997: finished) — the same section; - the Windows user name: the visible-text check (text boxes included) covers every picture, run before the first capture and again when a capture is chosen, with the SVG under - `--svg`: the page can draw again in between, as Project Settings did, putting the real + `--svg`: the page can draw again in between, as Project Settings does, putting the real build path back over the template a shot had set in the page only (a shot that sets a value types it, `input` and `change`, so the page keeps it). @@ -94,10 +92,10 @@ Measured on BETA 997: - **Cut-out**: a style sheet sets `html, body, #bodyInner` transparent and `body *` hidden except the kept elements (`#rootMenu`, `#contextMenu`, `#contextMenuSUB`, or an annotation layer), plus `Emulation.setDefaultBackgroundColorOverride` to alpha 0; both undone - after. Verified: corners alpha 0, interior 255, the rounded bottom corners partial. -- **Quiet page**: the existing style sheet against carets and animations goes into every shot, - opaque ones included (the probe's IDE Options sample showed a caret without it). -- **Write only on change**: kept from the current tool. The tool prints, for each picture, the + after. Corners are alpha 0, the interior 255, the rounded bottom corners partial. +- **Quiet page**: the style sheet against carets and animations goes into every shot, + opaque ones included (IDE Options shows a caret without it). +- **Write only on change.** The tool prints, for each picture, the `{:width height}` its page should have and warns where the page disagrees; it does not edit markdown. - **A capture is not byte-stable.** `Page.captureScreenshot` of an unchanged screen alternates @@ -124,8 +122,8 @@ Measured on BETA 997: the element allows: the WebView2 download page's Version arrow (`#version .px-dropdown__icon`, 8 by 9) changes a pixel by more than the noise between any two captures, animations and transitions off. A shot without it compares byte for byte. -- **Settle by condition, not by sleep.** Fixed sleeps were most of a run (a closed menu leaves a - 2x8 px empty `#contextMenu`, so a wait for it to vanish always ran out its 2 s; ~3,000 calls). +- **Settle by condition, not by sleep.** Fixed sleeps would be most of a run (a closed menu leaves a + 2x8 px empty `#contextMenu`, so a wait for it to vanish always runs out its 2 s). A menu is closed when it holds no items; a dialog is still when a MutationObserver has seen no change and its images and fonts are loaded; a floated panel when the IDE's `.flashElement` class is gone; a help page when its fonts, images and body are settled. The page size is set only once @@ -224,7 +222,7 @@ Exit codes: 0 every picture written or unchanged, 1 a picture failed, 2 the tool light picture is narrower again (1498). Each comes out within the 32-pixel tolerance, so the run passes, but the file can be rewritten for the width alone. -## Standing rules for what a picture shows (owner, 2026-10-09) +## Standing rules for what a picture shows (owner) - **A state is reproduced as the IDE reaches it, never by editing what it draws.** The tool inspects the IDE's page first to see how a state can be reached. @@ -254,7 +252,7 @@ the page once real ones are shown, as History's times are. Setup `code-chapter` (three parts): FeatureTour, the feature map's Inventory project, with `test/shots/codechapter/` staged onto it (`Helpers`, `Ev`/`Ev2`, `Watcher`) and the project's warnings set so that TB0016 is a hint and TB0012 information. Its 20 pictures are under -`docs/IDE/Code/Images/`, plus `Menu_Edit` and `Menu_View`, now taken with the code focused. +`docs/IDE/Code/Images/`, plus `Menu_Edit` and `Menu_View`, taken with the code focused. - **Each picture starts from the default layout and its own tabs** (`chapterTabs`), and closes Monaco's find widget first: the editor is one for every file, so a widget left open shows over @@ -302,7 +300,7 @@ global object and WinNativeCommonCtls (for the Toolbox's COMMON CONTROLS group). Command1, Check1, Frame1 holding Text2, and the control array Command2(0) and (1); `MyReport` has a Label in Report Header and a Line and a Label in Report Detail. Its pictures are under `docs/IDE/Forms/Images/` and `docs/IDE/Reports/Images/`, plus `Menu_Format_HorizontalSpacing`, -now taken with Command1, Check1 and Command2(0) selected (the page's own example of **Left** 10, +taken with Command1, Check1 and Command2(0) selected (the page's own example of **Left** 10, 140 and 250), so that Make Equal is live. `Designer_PackageNeeded` is taken in setup `settings`, whose project does not refer to the VB package. @@ -317,7 +315,7 @@ whose project does not refer to the VB package. annotated picture, each label and arrow of the overlay from `layerBoxes`; the overlay's own box is too big for it). The form, its menu bar and its readouts are compared only 2 pixels in from their edges, a readout only between its rounded ends: the ground shows through their - edges and shadows, and a 2-pixel band there failed Designer_Locked now and then. A cut-out + edges and shadows, and a 2-pixel band there fails Designer_Locked now and then. A cut-out (`keep`) avoids the ground altogether. - **The container's red outline is an animation** (`borderBlink`, the outline red half the time), which the quiet style sheet stops where the outline is transparent. Controls_Container @@ -383,7 +381,7 @@ whose project does not refer to the VB package. `nameUserAsUserIn(c, ".debugConsoleOuter")` replaces the name there in the page; the console is cleared with its button once the compiler is back. - **The tooltips** (`Services_Unavailable_Tooltip`, `Services_Operational_Tooltip`) are - **replicas** (owner, 2026-10-10). A native + **replicas** (owner). A native `title` tooltip never appears on a private desktop: neither a CDP mouse move over the badge nor a `WM_MOUSEMOVE` posted or sent to any of the page's three windows (`Chrome_WidgetWin_0`, `Chrome_WidgetWin_1`, `Chrome_RenderWidgetHostHWND`) made a new @@ -392,16 +390,16 @@ whose project does not refer to the VB package. page, holding the badge's own `title` read live, styled by `TOOLTIP_CSS`, placed as Windows places a tooltip by the cursor (cursor taken at the badge's middle; the badge is at the window's bottom, so it goes above), captured alone and removed, in the badge's own setups - (`project` for OPERATIONAL, `no-project` for UNAVAILABLE). Calibrated against the January - pictures (154x73 and 158x73 px) by rendering the same text in puppeteer's Chrome at 1x: Segoe - UI 12 px (9 pt), 16 px lines, `#242424` for the 1 px border **and** the text (the January - pictures' darkest text pixel is 36 grey levels, not black), 7 px side padding, 3 above and 4 - below, the width rounded up to whole pixels. Result in a 1x render with the January text: the - same size to the pixel and a mean luminance difference of 0.4 grey levels (maximum 9). In the - IDE at 2x, shown at half size, OPERATIONAL is 158x73 like the January picture (its text a + (`project` for OPERATIONAL, `no-project` for UNAVAILABLE). The style is fitted to a real + Windows tooltip (154x73 and 158x73 px) by rendering the same text in puppeteer's Chrome at 1x: Segoe + UI 12 px (9 pt), 16 px lines, `#242424` for the 1 px border **and** the text (a real + tooltip's darkest text pixel is 36 grey levels, not black), 7 px side padding, 3 above and 4 + below, the width rounded up to whole pixels. That gives the same size to the pixel and a mean + luminance difference of 0.4 grey levels (maximum 9). In the + IDE at 2x, shown at half size, OPERATIONAL is 158x73 (its text a little softer than a 1x ClearType picture: mean difference 8 after a box-filter to 1x). **The text is the IDE's live one**: with no project the badge's title reads each service - `Not initialized` (the January picture says `Disconnected`, the state for 0.1 s between + `Not initialized` (`Disconnected` is the state for 0.1 s between compilers), so the UNAVAILABLE picture is 155x73 and its alt text says Not initialized. The replica does not follow the theme, so the Light pass takes the dark pixels and keeps no `.light.png` file. @@ -506,11 +504,10 @@ Taken on BETA 1005, in both themes where the theme shows. on load says 1.0.0.10 is available. OLEGuids replaces WinDevLib in the page (WinDevLib publishes weekly, so its line would change every run, and it is 23 MB). - **`a1331a0e`** is the Enabled Libraries tab with the embedded package's tick ringed: BETA 1005 - no longer asks "Also remove the imported package ...", and `Updating a package.md` says the copy - is deleted at once. -- **`packPublishPackage1`** kept: `packagePublisherPublish` opens the sign-in form when no + deletes the copy at once and asks nothing, and `Updating a package.md` says so. +- **`packPublishPackage1`** is not retaken: `packagePublisherPublish` opens the sign-in form when no publisher is signed in, before the confirmation box. -- **`e749e10f`**: the gear is the Project Explorer's (the old picture's toolbar gear is gone); +- **`e749e10f`**: the gear is the Project Explorer's; the dialog is shown whole with the gear beside it (2324 px wide; a smaller page does not make the dialog narrower, it covers the gear). The full dialog fills Available COM References (hidden, its text in the page), whose paths name the user: the shot waits for that list and @@ -567,7 +564,7 @@ Taken on BETA 1005. - `test/addin/help.test.mjs` fetches `/favicon.png` as its sample PNG, so `favicon.png` must stay a PNG. -## Machine state in the pictures (owner, 2026-10-08) +## Machine state in the pictures (owner) - **The IDE's settings** (View's ticks, IDE Options' values, the debugger options, user panel layouts and keyboard groups) are set to their defaults **in the page only** — `liveIDEOptions` @@ -576,10 +573,9 @@ Taken on BETA 1005. - **About's licence line**: the tool waits for `licenceIsSet` before opening About, so it shows the edition, never NOT READY. - **The language tick** stays on whatever language the IDE runs in. -- **`llvmdoc2`** shows the default thread count, 1 (the picture's earlier 10 was its author's - setting). +- **`llvmdoc2`** shows the default thread count, 1. -## Project-setup pictures (owner, 2026-10-08) +## Project-setup pictures (owner) Project Settings, the panels, Project Explorer, Sample 6, Sample 15 and Global Search. In BETA 997 Sample 6's control images are in the CustomControls package's Miscellaneous folder, not @@ -593,7 +589,7 @@ the project's, and the tutorial says so. value is changed to match. - **An old-design picture is retaken in today's look**, with alt text and prose rechecked. -## Designers, code and composites (owner, 2026-10-09) +## Designers, code and composites (owner) - **Composites** lay their parts out on the IDE's background in the picture's theme, with a gap, code on the left and the panel on the right, one red arrow across. @@ -650,13 +646,13 @@ the project's, and the tutorial says so. the PNG was just captured from, and the build shows it in place of the PNG ([WIP.Build.md, Pictures as SVG](WIP.Build.md#pictures-as-svg)). -**Owner's decisions (2026-10-09 and 10):** +**Owner's decisions:** - Text is real text, in per-picture subsets of the fonts the IDE drew it with, cut by fontTools - (`scripts/subset_font.py`), hinting kept. Glyph outlines were rejected (they render poorly); - harfbuzzjs was tried and removed ("don't mess with harfbuzz"). -- The PNG and the SVG are both committed. One SVG per theme: a single SVG switching on CSS - variables and a `#light` fragment was proposed and dropped. + (`scripts/subset_font.py`), hinting kept. Glyph outlines render poorly and are not used; + nor is harfbuzzjs ("don't mess with harfbuzz"). +- The PNG and the SVG are both committed. One SVG per theme, not a single SVG switching on CSS + variables. - Pages name `X.png` and the build substitutes; `png` in the attribute block keeps the PNG. - `picture_svg` in `_config.yml` per output: online and book on, offline (and so the help archive) off. @@ -675,12 +671,12 @@ the PNG was just captured from, and the build shows it in place of the PNG drawn as replicas of what Chromium paints: a closed `<select>`, a checkbox and a radio button, an input's and a textarea's text (scrolled), and scrollbars styled with `::-webkit-scrollbar`. - **An embedded image keeps its pixels and any notice of whose it is, and nothing else** - (owner, 2026-10-10). `renderSvg` passes each through `compactImage` from + (owner). `renderSvg` passes each through `compactImage` from `scripts/lib/compact-image.mjs`, which `scripts/imagestrip/` matches; what it keeps and why is in [WIP.Build.md](WIP.Build.md) beside `imagestrip`, and `test/compact-image.test.mjs` tests it. The icons whose sRGB profile it drops decode to the same pixels in Edge 155 and Chromium 148; a `gAMA` 1.0 control does not. The 318 by 346 pixels of a 16-pixel icon such - as tB-Red are the IDE's own asset at its own size, and stay so (owner, 2026-10-10). + as tB-Red are the IDE's own asset at its own size, and stay so (owner). - `fonts.mjs` gives each run's characters to the font files Chromium used for them, and `subset_font.py` cuts each file to the characters drawn, as WOFF2. - `diff.mjs` renders the SVG as an `<img>` at 2x and compares it with the PNG by luminance and @@ -691,7 +687,7 @@ the PNG was just captured from, and the build shows it in place of the PNG clip shows the half of the edge the PNG cuts off. That column alone put a small picture past the bar (a 16-pixel icon at 11%, a menu at 0.5%), so `capture` gives `svgOfPage` the clip with the PNG's width and height. -- **A converter change is tried with `replay.mjs`, not a retake** (owner, 2026-10-10). +- **A converter change is tried with `replay.mjs`, not a retake** (owner). `capture.mjs`'s `collectBundle` reads everything the converter needs from the page into a *bundle*: the snapshot, its style names (positional, so a bundle carries its own list), and per document the measuring layer's answers, the runs' platform fonts, the web fonts, the @@ -707,15 +703,14 @@ the PNG was just captured from, and the build shows it in place of the PNG `.svgshot-bundles/` (gitignored, never under `docs/`: a bundle holds the user name), **one file per picture holding both themes** (`X.bundle.json.br`, `{dark, light}`, the dark one's text first), **Brotli** at quality 9 with a 16 MB window: the light bundle is mostly the dark - one, which a long window finds and gzip's 32 KB does not, so the 370 bundles went from 57.7 - MB gzipped to 21.8 MB in 189 files, about 115 KB a picture. A light picture not kept (the + one, which a long window finds and gzip's 32 KB does not, so the 370 bundles are 21.8 MB in + 189 files (gzip: 57.7 MB), about 115 KB a picture. A light picture not kept (the same as the dark) has its slot dropped (`dropBundle`). `trimSnapshot` cuts the snapshot to the clip (the layout objects near it, every ancestor's, a frame's document only when its `<iframe>` is kept, `<style>` and `<script>` text emptied, unused strings dropped; every DOM node kept), 97 MB to 58 MB for the 370, and all 370 draw the same SVG trimmed as whole. A trimmed layout object keeps its old index in `layout.originalIndex`, which its key is made of: the scrollbar styles - measured at the capture are found by it. **A replay takes about 20 s for the 370** (it - took 3.4 min at first): an SVG that comes out byte-identical to its file beside the PNG its + measured at the capture are found by it. **A replay takes about 20 s for the 370**: an SVG that comes out byte-identical to its file beside the PNG its bundle names is not compared again (`--recheck` compares all); the fonts' cuts, about two thirds of an SVG's time, are kept in `.svgshot-bundles/subsets/` under a hash of the font (file, face, size, time, or a web font's bytes), its weight, the characters, the hinting and @@ -728,16 +723,16 @@ the PNG was just captured from, and the build shows it in place of the PNG misses are new since its capture, whose PNG hash changed, or whose bundle lacks a style now read, with the `--only` command that retakes them. **A change to what is read from the page that no miss shows raises `BUNDLE_VERSION`**: version 2 (the web font rules, `fontFiles`, - `svgs`) does not read a version 1 bundle, and replay lists every one as needing a retake - (the 370 of this cache did). Reading version 1 would replay a split font with the one file it + `svgs`) does not read a version 1 bundle, and replay lists every one as needing a retake. + Reading version 1 would replay a split font with the one file it kept, and a picture with an inline `<svg>` without it, and nothing in a version 1 bundle says which pictures have either. Answering misses offline in headless Chromium, and drawing bundles in `tbdocs`, are later steps, not done: the second needs the bundles committed, so cleaned of the user name, and complete, since CI has no Windows fonts to measure with. -- **A filter of colour functions is baked, not drawn** (`colour-filter.mjs`; owner, 2026-10-10). +- **A filter of colour functions is baked, not drawn** (`colour-filter.mjs`; owner). Chromium's PDF backend cannot express an SVG `<filter>` and draws the whole filtered group as a - 300 dpi bitmap, text included: the book lost the text of 44 of its 110 SVGs that way. Every CSS + 300 dpi bitmap, text included: the book would lose the text of 44 of its 110 SVGs that way. Every CSS filter function but `blur()` and `drop-shadow()` is an affine colour map, which commutes with antialiasing, so mapping each fill, stroke, gradient stop and text class under the group paints the same picture on screen, to a channel's rounding; `opacity()` becomes the group's opacity. @@ -745,8 +740,8 @@ the PNG was just captured from, and the build shows it in place of the PNG group that blends or already holds a real filter group. A `drop-shadow()` last in the list (the IDE's toolbar codicons glow with one) is split the same way: the shadow alone, from a copy of the group under a filter that blurs its alpha, then the group over it unfiltered, - which is what the filter's own merge paints. One SVG for screen and book; two - variants were considered and dropped, since the bake is exact. Blurred shadows stay bitmaps in + which is what the filter's own merge paints. One SVG serves screen and book, since the + bake is exact. Blurred shadows stay bitmaps in the PDF, which loses nothing. - **The bar** (`SVG_FAITHFUL` in `shoot_docs.mjs`): an SVG with more than 0.25% of its pixels 96 grey levels or more off is not written, and an older one is removed. Anything not drawn falls @@ -801,7 +796,7 @@ the IDE calls a JPEG and a BMP `image/png`. The About dialog's EXCALIBUR logo is drawn with `background-size: contain`; read as a PNG, its JFIF header gave 4,718,592 by about 4.3 billion pixels, and the logo would come out 0.054 px wide. -**Frames** (owner, 2026-10-10: cross-origin frames in general). A frame of the same site (the +**Frames** (owner: cross-origin frames in general). A frame of the same site (the docked help pane: the documentation on another `localhost` port) is in the IDE's process, and `DOMSnapshot.captureSnapshot` returns its document beside the page's: the `<iframe>` node's `contentDocumentIndex` names it, and its boxes are in its own coordinates. `decodeSnapshot` @@ -822,7 +817,7 @@ an `<iframe>` on `127.0.0.1` while the page is on `localhost`: another site, so **Web fonts** are found by CSS font matching on the run's `font-family` list, style and weight (`webFontFiles`, in the page), not by the face's own name: the site's Inter calls itself "Inter -Variable", and the last rule of a family was its italic file. A family split by +Variable", and the last rule of a family can be its italic file. A family split by `unicode-range` (Microsoft's Segoe UI Variable: five files per weight, Latin, Latin Extended, Cyrillic, Greek, symbols) gives a run every rule whose range holds one of its characters (`webRulesFor`, the last such rule per character, as CSS does), only those files are read, and @@ -871,8 +866,7 @@ and compares them with the browser's own picture: in Edge, whose Chromium is the (Puppeteer's trails it, and draws the select's arrow otherwise), laid out at 1x and drawn at 2x as the IDE is, text without ClearType. Each page is held to the figures recorded in `bench/baseline.json` (`--update` records them): no page comes out exact, and a fixed limit -missed a fault fixed while the bench was written (arrows a quarter too tall moved a page from -0.025% to 0.064%). `node scripts/svgshot/pixels.mjs <png> x y w h --beside <png>` prints a +would miss a small fault (arrows a quarter too tall move a page from 0.025% to 0.064%). `node scripts/svgshot/pixels.mjs <png> x y w h --beside <png>` prints a rectangle of two pictures as letters, one per colour: how an edge, a stroke or a blend came out. A new replica gets a page there. @@ -892,7 +886,7 @@ the baseline to a whole CSS pixel (a device pixel off at most; an underline uses one). A canvas is a raster (the editor's minimap is drawn with `putImageData`). The run reports each under "not drawn". -**Open, the owner's** (2026-10-11): +**Open, the owner's:** - The SVGs are to be checked in browsers other than Chromium's, with an online tool; leave them as they are until then. - fontTools' `gvar` subsetting bug, worked round by `patch_gvar_subset`, is to be reported diff --git a/WIP.Search.md b/WIP.Search.md index 4dad4cf8c..aa9b88a8f 100644 --- a/WIP.Search.md +++ b/WIP.Search.md @@ -1,7 +1,7 @@ # twinBASIC Documentation — Site search design notes -Why searching for a member such as `PaintPicture` did not find it, what was -measured, and the design that fixes it. Companion to +Why a search for a member such as `PaintPicture` needs the design below, what +was measured, and the design. Companion to [builder/PLAN-6.md](builder/PLAN-6.md) §5.3, which describes the search-data generator as ported from Jekyll. @@ -11,10 +11,10 @@ Like WIP.md, this file is not rendered through tbdocs, so literal dashes are fin Everything needed to continue is in this file and in `eval/`. -**Where it stands.** The design below is shipped. No decision is open. The +**Where it stands.** The design below is in place. No decision is open. The candidates for further work are under "Next"; the user picks. -**The eval now** (`node eval/search_quality.mjs`, ground truth `intent-5`, +**The eval** (`node eval/search_quality.mjs`, ground truth `intent-5`, 10,327 queries): 98.2% at rank 1, 99.1% in the top 10. | category | hit@1 | n | what it types | @@ -28,7 +28,7 @@ candidates for further work are under "Next"; the user picks. No bare name is out of tier order, and every prose query's `behind` page is within the top 3 (22 of 22). Compare categories, not totals, across ground -truths: adding the name-and-kind set lowered the overall hit@1 from 99.7%. +truths: the name-and-kind set lowers the overall hit@1 (99.7% without it). **Known misses (185 at rank 1)**, each recorded where it was diagnosed: - 158 name-and-kind queries, mostly properties; not diagnosed yet @@ -239,30 +239,28 @@ page for words it doesn't contain. So authors mark index entries by hand "lunr patches". - lunr 2.3.9's `Set#union` copies both sets, and `Index#query` unions once per expanded term and field of a REQUIRED clause, so a short wildcard word - was quadratic. Patched (`accumulateSetUnions()`); see "lunr patches". + is quadratic. Patched (`accumulateSetUnions()`); see "lunr patches". Profile before guessing: `node --cpu-prof`. ## The problem `PaintPicture` is documented on six classes (Form, PictureBox, Printer, PropertyPage, Report, UserControl), each under `### PaintPicture` inside -`## Methods`. With the original index, a search returned 24 results, but the -definitions came 13th to 19th, as entries titled just "Methods" that link to -`#methods`. The same happened to every member documented at `###`: `Line`, -`Print`, `Cls`, the Assert methods, and about 2,700 others. - -Three things combined: +`## Methods`. Every member documented at `###` (`Line`, `Print`, `Cls`, the +Assert methods, and about 2,700 others) shares the situation. Three things +make such a member hard to find, and each shapes the design: 1. **Granularity.** [builder/search.mjs](builder/search.mjs) splits a page into - one entry per heading up to `search.heading_level` (2 by default). A `###` - member became part of the text of its parent `##` entry, often 5 to 16 KB - long. + one entry per heading up to `search.heading_level`. At level 2 a `###` + member is part of the text of its parent `##` entry, often 5 to 16 KB + long, and a search shows it as an entry titled just "Methods" that links + to `#methods`. 2. **Length normalisation.** lunr scores with BM25 (`b` = 0.75). A term found in a 10 KB "Methods" entry scores far lower than the same term in a short entry that only mentions it, such as the glossary's "graphics method". -3. **Tokenisation.** The client split on `/[\s\-/]+/`, not on `.`. So - `Form.PaintPicture` was the single token `form.paintpicture`, which occurs - nowhere. Qualified names found their target 0.4% of the time. +3. **Tokenisation.** The lunr separator is `/[\s\-/]+/`, not `.`. So + `Form.PaintPicture` is the single token `form.paintpicture`, which occurs + nowhere. Splitting on `.` is therefore part of query construction (Design §3). The render step `headingLevelNormalizePlugin` ([builder/render.mjs](builder/render.mjs)) matters here. On a page with `#` and `###` but no `##` (405 of 912 pages, @@ -287,7 +285,7 @@ repeat a heading, by adding `-1`, `-2` and so on. A harness replays the client's search in Node: the same lunr build, boosts, query construction and capped fuzzy fallback, against a built -`search-data.json`. The first ground truth is `tB/symbols.json`, which gives +`search-data.json`. The base ground truth is `tB/symbols.json`, which gives 8,012 queries: - 2,884 bare names (`PaintPicture`). A result counts as correct if it is any @@ -296,7 +294,7 @@ query construction and capped fuzzy fallback, against a built - 5,108 qualified names (`Form.PaintPicture`). Only that class's URL counts. - 20 hand-picked prose queries ("late binding", "Option Explicit" and so on). -Later sections add ground truths: reader-intent tiers, page titles, page plus +Further ground truths: reader-intent tiers, page titles, page plus section, name and kind, and a larger prose set. The harness reports hit@1/5/10, MRR (capped at rank 20), results by symbol kind, cost (size raw and gzipped, index build time in the browser, heap) and the queries that changed rank @@ -305,48 +303,32 @@ its baseline is [eval/search_baseline.json](eval/search_baseline.json). Run `node eval/search_quality.mjs --compare eval/search_baseline.json` to measure a change against it. -### Results - -The table compares the configurations tried against the first ground truth. -"today (h2)" is the original index. - -| configuration | entries | gzip | hit@1 | hit@10 | MRR | bare hit@10 | qualified hit@10 | prose hit@10 / MRR | -|---|---|---|---|---|---|---|---|---| -| today (h2) | 3,781 | 1,039 KB | 16.7% | 20.5% | .182 | 55.8% | 0.4% | 80% / .690 | -| h3 | 7,606 | 1,131 KB | 27.7% | 35.3% | .307 | 84.6% | 7.3% | 80% / .611 | -| h4 | 7,637 | 1,132 KB | same as h3 | | | | | | -| V2: h3, generic sections folded | 6,713 | 1,094 KB | 28.3% | 35.7% | .312 | 84.6% | 7.9% | 80% / .621 | -| V3: h3, generic sections retitled | 7,606 | 1,112 KB | 27.7% | 35.3% | .307 | 84.6% | 7.3% | 80% / .610 | -| V2 + query split on every `.` | 6,713 | 1,094 KB | 67.9% | 90.5% | .761 | 84.6% | 93.9% | 80% / .621 | -| V2 + symbols field (boost 50) + split on every `.` | 6,713 | 1,143 KB | 77.9% | 97.5% | .862 | 98.1% | 97.2% | 80% | -| V2 + symbols (boost 200) + split | | | | 97.7% | .867 | | | **75%** | -| BM25 `b` 0.5 / 0.25 on V2 + split | | | | 90.4 / 90.3% | .757 / .751 | | | | -| **recommended**: V2 + symbols (50) + smart dot split | 6,713 | 1,143 KB | **88.7%** | **97.9%** | **.929** | 98.1% | 97.9% | 80% | +### Why the design is what it is -### What the numbers showed +Measured against the base ground truth: - **h4 adds nothing.** From h3 to h4, no query changes rank. - **Splitting creates short competitors.** At h3, `vbSolid` falls from rank 1 - to about 12. Its page, `DrawStyleConstants`, is 483 characters. The new + to about 12. Its page, `DrawStyleConstants`, is 483 characters. The per-property entries such as `DrawStyle` (171 characters) mention the - constant once and now outscore it. Titles play no part; every hit is on + constant once and outscore it. Titles play no part; every hit is on content. Tuning `b` down to 0.25 leaves `vbSolid` at 12, and only `b` = 0 fixes it, so BM25 tuning is not the answer. -- **Most remaining misses are enum values that live in table rows.** Of the - 444 bare names still outside the top 10 at h3: 410 have a page whose title +- **Most misses at h3 are enum values that live in table rows.** Of the + 444 bare names outside the top 10 at h3: 410 have a page whose title is not the symbol's name, mostly enum tables (`KeyCodeConstants` 100, `MenuAccelConstants` 75, `ShortcutConstants` 49); 24 are operators; 10 are common words (`And`, `Is`, `Not`, `With`). A symbols field fixes the first group. - **The symbols field generalises.** Built from a random 50% of symbols, it - still raises the other 50% from 82.4% to 87.3% in the top 10. At boost 200 - it starts to beat real prose answers (prose drops to 75%); at 50 prose is - unchanged. + raises the other 50% from 82.4% to 87.3% in the top 10. At boost 200 + it starts to beat real prose answers (prose drops from 80% to 75%); at 50 + prose is unchanged. - **Splitting on every `.` has two costs.** `Debug.Print` falls from rank 1 to 61, because `debug` and `print` are both common words and the exact token no longer counts. And `1.0`, `e.g.` and `i.e.` turn into single-character - tokens that match letter-index pages. The smart split below fixes both and - keeps every qualified-name gain. + tokens that match letter-index pages. The smart split (Design §3) avoids + both and keeps every qualified-name gain. - **Generic sections:** folding them into the entry before them is a small, free gain. Retitling them ("PaintPicture — Example") gains nothing. @@ -372,10 +354,10 @@ The table compares the configurations tried against the first ground truth. ### 2. Two fields built from the symbol index -A single `symbols` field at boost 50 was measured first. BM25 discounts a -match inside a long field, and `Container.Name` forms are longer than bare -names, so the two compete inside one field for no reason. Two fields, each -boosted on its own, rank better than one field at any single boost: +BM25 discounts a match inside a long field, and `Container.Name` forms are +longer than bare names, so the two would compete inside one field for no +reason. Two fields, each boosted on its own, rank better than one field at any +single boost: - `names`: the bare name of every symbol in `tB/symbols.json` whose URL is the entry's `relUrl` (ignoring a trailing slash or `index`), space-separated @@ -389,10 +371,9 @@ With the current corpus, 4,084 of 6,713 entries get either field. - In the build, `searchData` also waits for `symbolIndex`. Both run on the main thread after `renderJoin`. `symbolIndex` returns its symbols, and `searchData` joins them by URL before it writes (`joinSymbolsToEntries` in - `search.mjs`). Workers keep deriving sections as before; the join is a map - lookup per entry. + `search.mjs`). Workers derive sections; the join is a map lookup per entry. - `renderEntryString` adds `names` / `qualified` only when non-empty, so a - page with no symbols produces the same bytes as before. + page with no symbols produces the same bytes as without the join. - In the client, `this.field('names', { boost: 100 })` and `this.field('qualified', { boost: 500 })`. The title keeps 200 and content keeps 2. @@ -408,12 +389,12 @@ In the client's `update()`, for each token the tokenizer produces: and trailing wildcard as ordinary tokens. Drop parts of one character. So `1.0`, `3.9`, `e.g.` and `i.e.` add nothing, and `Debug.Print` still matches its exact entry first. Making the parts required (lunr's - `presence.REQUIRED`) was measured and rejected: qualified hit@10 falls from - 97.9% to 88.6%. + `presence.REQUIRED`) is rejected: qualified hit@10 falls from 97.9% to + 88.6%. - **Asterisk guard.** Drop tokens made only of `*`. If nothing is left, show no results. A search for `*` or `**` otherwise throws inside lunr's query - engine and search stops working. (Trimming query tokens, see "What - shipped", subsumes this.) + engine and search stops working. (Trimming query tokens, see "Query + construction", subsumes this.) ### 4. Both copies of the client @@ -447,7 +428,7 @@ drift apart silently. - **Stop words kept, so short common words crowd.** `VB` is a prefix of many `vbXxx` constant names in `names`; `Lock`, `Time$`, `Column`, `Index` crowd the same way. Everything is judged by reader intent, not by the - rank an earlier index gave; see [Reader intent](#reader-intent-after-the-rollout). + rank an earlier index gave; see [Reader intent](#reader-intent). - **Operators (24 symbols)** are unsearchable by their text under this design; exact names find them. See [Future work](#future-work). - **Ground-truth bias.** Most queries are symbol names, so the numbers favour @@ -457,20 +438,20 @@ drift apart silently. ## Design §5: stop words, dot runs, and a lazy build -Three independent, measured changes to both client copies and the eval -replica. +Three independent changes, in both client copies and the eval replica. ### A. Keep lunr's stop words in the index lunr's **index** pipeline runs `lunr.stopWordFilter` by default, dropping words like `do`, `for`, `if`, `is`, `on`, `with`, `each` from every field. Its -**search** pipeline never ran that filter, so a query for `Do` still carried -the token `do`, which existed nowhere in the index. Many twinBASIC keywords -are English stop words, so every keyword query missed its own entry (`Do` -ranked 18th, `For` 17th, `With` 55th, `Is` 177th). The fix is one line inside -the `lunr(function(){...})` builder: `this.pipeline.remove(lunr.stopWordFilter);`. -Every keyword query goes to rank 1, and the fix isn't specific to -twinBASIC's keyword list. The cost: a few short, extremely common tokens +**search** pipeline never runs that filter, so a query for `Do` still carries +the token `do`, which would exist nowhere in the index. Many twinBASIC keywords +are English stop words, so with the filter every keyword query misses its own +entry (`Do` ranked 18th, `For` 17th, `With` 55th, `Is` 177th). The remedy is +one line inside the `lunr(function(){...})` builder: +`this.pipeline.remove(lunr.stopWordFilter);`. Every keyword query is then at +rank 1, and it isn't specific to twinBASIC's keyword list. The cost: a few +short, extremely common tokens (mostly other `vbXxx` constant name prefixes and `Index`/`Lock`/`Column`/`Time$`) crowd into more results; see "Known regressions" above. @@ -487,7 +468,7 @@ match highlighting stay valid), then the original tokenizer runs. Non-string input (arrays, `null`) passes through unchanged, since lunr also calls its tokenizer with those. -The wrapper is installed once, where the separator used to be set, and has to +The wrapper is installed once, where the separator is set, and has to carry `separator` itself: the original tokenizer reads `lunr.tokenizer.separator` at call time, so once `lunr.tokenizer` points at the wrapper, that lookup resolves to the *wrapper's* own `.separator`. The @@ -497,7 +478,7 @@ it applies to the index build and every query alike. ### C. Build the index lazily, on the first keystroke, with visible feedback -Building the lunr index on every page load costs about 1.3 s and 240 MB of +Building the lunr index on every page load would cost about 1.3 s and 240 MB of heap on a desktop, paid by every reader whether or not they open search. So `initSearch()` only defines *how* to build the index (`loadIndex(onSuccess, onError)`, which fetches via XHR and installs the stop-word and dot-run @@ -530,8 +511,8 @@ leaves the index `null`, so the next keystroke retries from scratch. `update()` keeps its prefix (the dedup check, the `search-active` class, the iOS scroll workaround) and its keyboard/focus/blur listeners. The query part -was split out into `doSearch(input)`, gated behind `if (index === null) { -loadIndexNow(); return; }`. `searchLoaded()` takes `loadIndex` instead of +is `doSearch(input)`, gated behind `if (index === null) { +loadIndexNow(); return; }`. `searchLoaded()` takes `loadIndex`, not `(index, docs)`. `builder/offline.mjs`'s `JTD_INITSEARCH_FN_REPLACEMENT` works the same way: @@ -541,25 +522,20 @@ still waits for the first `loadIndexNow()`, with the same feedback. `eval/site_search.mjs` mirrors parts A and B exactly; there is no lazy build to mirror, since the CLI always wants an index. -**Cost per page view**, median of 15 interleaved runs on a desktop: - -| | today (h2) | steps 3-4 | + stop words kept | -|---|---|---|---| -| index build | 964 ms | 1,184 ms | 1,294 ms | -| heap | 157 MB | 204 MB | 243 MB | - -With the lazy build these costs are paid once per page, and only by readers -who type into the search box; a page never searched from pays nothing. +**Cost per page view**, median of 15 interleaved runs on a desktop: index +build 1,294 ms, heap 243 MB. With the lazy build these costs are paid once per +page, and only by readers who type into the search box; a page never searched +from pays nothing. -## Rollout +## Step numbers in code comments -Step numbers cited in code comments: 1 bug fixes (asterisk guard, tokenizer -separator in the replica); 2 the harness; 3 h3 entries and folding; 4 the -symbols fields and smart dot split; 5 stop words, dot runs and lazy build -(5A, 5B, 5C are "Design §5" A, B, C). Each step is its own change recording -its numbers from the harness. +Code comments cite steps: 1 bug fixes (asterisk guard, tokenizer separator in +the replica); 2 the harness; 3 h3 entries and folding; 4 the symbols fields +and smart dot split; 5 stop words, dot runs and lazy build (5A, 5B, 5C are +"Design §5" A, B, C). A change ships as its own commit recording its numbers +from the harness. -## Reader intent (after the rollout) +## Reader intent ### The criterion @@ -1029,9 +1005,9 @@ behaves here". - **`buildIndexInSlices()`** builds the index in slices (Design §5 C). **Entities.** The search data keeps the page's HTML entities (`&`, -`>`, `<`, `-`), so the index held `amp;h80004005` while a query -trims to `h80004005`, and `&H80004005` found none of the five pages that -mention it. The data can't decode them: the client inserts `doc.title`, +`>`, `<`, `-`), so undecoded the index would hold `amp;h80004005` +while a query trims to `h80004005`, and `&H80004005` would find none of the +five pages that mention it. The data can't decode them: the client inserts `doc.title`, `doc.doc` and slices of `doc.content` with `innerHTML` and highlights by lunr's character positions in that text, so a decoded `<Object>` would become a tag in the results panel and every highlight after an entity would @@ -1163,11 +1139,11 @@ decisions behind them, and what the measurements ruled out: - **Placing a section can move words between entries.** A section placed before a page's folded `Example` takes the Example into its own entry instead of the page's top entry, which loses its words (a *Named arguments* - section before Call's *Example* took `statement` away: `Call statement` fell - 1 to 5). New sections go after the Example. + section before Call's *Example* takes `statement` away: `Call statement` + falls 1 to 5). New sections go after the Example. - **A survey's "already fine" list is rechecked** before it is used as - guards: one such list was wrong at least once (`optional parameters` landed - on Compiler-Options). + guards: such a list can be wrong (`optional parameters` lands on + Compiler-Options). - **A term that is not a symbol reorders no bare name.** `ByRef` and `ByVal` are not symbols, so their entries are safe. - **Terms with no entry, because the right page is already first or in the @@ -1197,19 +1173,19 @@ decisions behind them, and what the measurements ruled out: ### Rejected: tier-specific exact fields (X1t) Splitting `exact` into `exact1`/`exact2`/`exact3` by symbol kind, with -descending boosts, measured no better than flat `exact` (hit@1 90.0% against -90.1%) and ordered tiers worse (48 violating queries against 42). It would -also need the build's join to emit each symbol's kind. **This is superseded:** -it was measured while a lone clause's boost cancelled out, so it never had a -fair test. Done with field boosts, as the `primary` field, it took tier-order -violations to zero. Don't rebuild X1t without a new idea. - -The explanation that CheckBox's `names` field lists too few members was false: -both entries' `names` are the single word `CheckBox`. The difference is the -content: the CheckBox page's top entry (`/tB/Packages/VB/CheckBox/`) has 1 -character, since a class's introduction sits under its own heading -(`#checkbox-class`), while `DTPicker › CheckBox` has 467 characters that -mention "checkbox" several times. +descending boosts, is not needed: the `primary` field, which uses field +boosts, brings tier-order violations to zero. X1t as first measured (no better +than flat `exact`, 90.0% against 90.1% hit@1, and 48 violating queries against +42) was not a fair test, because a lone clause's boost cancels out; the tiers +need field boosts. X1t would also need the build's join to emit each symbol's +kind. Don't rebuild it without a new idea. + +CheckBox against `DTPicker › CheckBox` is a content difference, not a `names` +difference: both entries' `names` are the single word `CheckBox`. The CheckBox +page's top entry (`/tB/Packages/VB/CheckBox/`) has 1 character, since a +class's introduction sits under its own heading (`#checkbox-class`), while +`DTPicker › CheckBox` has 467 characters that mention "checkbox" several +times. ### Known misses and limits diff --git a/WIP.Typography.md b/WIP.Typography.md index 1d7d95b7e..bb0edd421 100644 --- a/WIP.Typography.md +++ b/WIP.Typography.md @@ -137,7 +137,7 @@ advances after `Graphviz.load()`. Three facts make that work: moves the laid-out box for `AAAA` from 43.00 pt to 80.00 pt. - **Inter already routes there.** The name lookup is permissive and falls back to Times, so patching Times is what makes `fontname="Inter"` correct. Nothing - in the docs asks for Times; a diagram that did would now get Inter's metrics. + in the docs asks for Times; a diagram that did would get Inter's metrics. Widths are in the family's **own em units** (2048 for this table), not AFM thousandths, so a search of the WASM for the AFM values finds nothing. @@ -146,8 +146,9 @@ thousandths, so a search of the WASM for the AFM values finds nothing. browser rather than read out of the font binary: the browser's shaped advance is the number to match. -Result across the site's diagram labels: **-11.4% mean / -18.0% worst becomes -+0.5% mean / +2.8% worst**. The residual is kerning, which a per-character +Across the site's diagram labels the patched table measures **+0.5% mean / ++2.8% worst** against the browser's widths (unpatched Times: -11.4% mean / +-18.0% worst). The residual is kerning, which a per-character table cannot express, and it errs *wide* --- boxes come out slightly generous rather than slightly tight, which is the safe direction. @@ -243,7 +244,7 @@ on the web that *is* the 16px body size, but the book sets body text at 10.5pt, so that diagram comes out at **1.14x the prose**. The other four are already scaled down to fit and land below body size on their own. -`print.css` closes it with `zoom: 0.875`, which is 10.5/12 exactly: +`print.css` sets `zoom: 0.875`, which is 10.5/12 exactly: | diagram | no `zoom` | with `zoom` | with `max-width: 87.5%` | |---------|----------:|------------:|------------------------:| @@ -327,8 +328,7 @@ PNG* cannot work on such a diagram at all, and the throw lands inside an nothing. Graphviz emits plain `<text>`, which is why all four buttons work on all five diagrams. This is a property of the *format*, not of the export code. -Mermaid is the tool this was tested on, and **the obvious workaround does not -work**: `flowchart: { htmlLabels: false }` moves only the edge labels to `<text>` +Mermaid emits it, and **the obvious workaround does not work**: `flowchart: { htmlLabels: false }` moves only the edge labels to `<text>` and keeps node labels in `foreignObject` regardless (Mermaid 11). Mermaid is not wired up --- no dependency, no script emitted, only `code.language-mermaid` rules left in the vendored theme CSS --- so a `mermaid` fence renders as a code diff --git a/WIP.md b/WIP.md index 745c9e7ee..dc05504c4 100644 --- a/WIP.md +++ b/WIP.md @@ -34,7 +34,7 @@ sibling. History goes nowhere: git keeps it. Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. -Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON and xCalendar complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. The two console packages carry a NOTE (owner, 2026-10-12) that the samples harness cannot yet capture a console, so their console claims follow the source. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner, 2026-10-11 and 10-12), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-probes/xCalendar/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-probes/TBMANLIB_Dialog/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.7.0.0 is Contributed although a match is at most about 3,300 characters long: its engine gives up a search from one start position after 10,000 steps, a limit fixed inside the package (`a*` on 5,000 `a` finds 3,331 characters from position 1669; confirmed by a second agent, and the same limit is in upstream vba-regex, where a caller can raise it), stated in a WARNING at the top of its landing page and in its line in `Contributed.md` (owner, 2026-10-13). Each one has a chapter in the book's Packages part. **ArrayListLib, BigNumberLib, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner 2026-10-12): ArrayListLib and WinReg do not compile on BETA 1005 (each documented from its source and a copy of it that builds, its samples compile-only; ArrayListLib 1.6.151.1 was run in a copy with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, where a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; WinReg's methods were also run in a copy of its source with its four failing calls passed `ByVal`, and ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results there), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner 2026-10-13), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. See *Where things live* for how a package moves between the two lists. +Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON and xCalendar complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. The two console packages carry a NOTE (owner) that the samples harness cannot yet capture a console, so their console claims follow the source. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-probes/xCalendar/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-probes/TBMANLIB_Dialog/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.7.0.0 is Contributed although a match is at most about 3,300 characters long: its engine gives up a search from one start position after 10,000 steps, a limit fixed inside the package (`a*` on 5,000 `a` finds 3,331 characters from position 1669; confirmed by a second agent, and the same limit is in upstream vba-regex, where a caller can raise it), stated in a WARNING at the top of its landing page and in its line in `Contributed.md` (owner). Each one has a chapter in the book's Packages part. **ArrayListLib, BigNumberLib, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1005 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. See *Where things live* for how a package moves between the two lists. The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. @@ -79,7 +79,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad > would file the page as a *static file* and serve the raw markdown. Editors on > Windows add a BOM unasked. `lib/frontmatter.mjs` strips a leading BOM before parsing. - `docs/Reference/Built-In/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus 23 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing. -- `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner, 2026-10-11): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner, 2026-10-12). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `<Class>/index.md` at `/tB/Packages/Contributed/<Package>/<Class>/` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `<Class>.md`. Each package is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. Its samples compile in the `contributed-<Package>` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner, 2026-10-11): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1005") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner, 2026-10-12); a minor defect that the section documents does not make it Experimental (owner, 2026-10-12: VBComDlg, whose Replace All raises no event, stays Contributed; owner, 2026-10-13: a fixed limit that the section states at the top of its landing page does not either, as tbRegExp's match length). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner, 2026-10-12): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and its book chapter follows the Experimental Packages chapter. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three, and its pages keep their addresses. +- `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `<Class>/index.md` at `/tB/Packages/Contributed/<Package>/<Class>/` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `<Class>.md`. Each package is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. Its samples compile in the `contributed-<Package>` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1005") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner); a minor defect that the section documents does not make it Experimental (owner: VBComDlg, whose Replace All raises no event, stays Contributed; owner: nor does a fixed limit that the section states at the top of its landing page). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and its book chapter follows the Experimental Packages chapter. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three, and its pages keep their addresses. - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features is 5). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**. Its language claims are measured on BETA 995 with `tbrun --llvm` and `--exe`: the comma, space and comma-space spellings of `[CompilerOptions]` all turn LLVM and `+optimize` on, `[CompilerOptions("")]` turns it off under project-wide LLVM, the long CPU-flag sample runs, and an error is lost between procedures when either the callee or the caller is LLVM-compiled. Its samples are marked `check_build`, which compiles them; `examples.bat --llvm` also builds them with LLVM. @@ -167,7 +167,7 @@ scenario files import `scenario` from `test/addin/scenario.mjs`. one IDE set up as a lane is (`Lane` from `scripts/lib/tb-lane.mjs`), on the user's desktop, DevTools port 9590, opened on `test/addin/helphost` with `docs/_site` served to the pane. It waits for the IDE to close, then puts the registry and `tbDocsHelp` back. The lanes stay -on the private desktop by default (owner, 2026-10-04); only this tool shows its IDE, and only +on the private desktop by default (owner); only this tool shows its IDE, and only the one it opens the project in: the IDE that builds the add-in stays on a private desktop. ## Authoring a page @@ -224,9 +224,9 @@ The vocabulary tables further down cover word choice. The rules in this subsecti One callout per concern; don't stack a NOTE and an IMPORTANT for the same point. - **Where twinBASIC and VB6 behave differently, say what VB6 does** (owner's rule, 2026-10-06), checked with `scripts/vb6run.mjs` or a reproducer's `vb6/` side, never from memory. Where twinBASIC's behaviour is the nominal one, the VB6 behaviour goes in a `> [!NOTE]`; inside a defect's WARNING it is one sentence, such as "VB6 raises error 6, *Overflow*." + **Where twinBASIC and VB6 behave differently, say what VB6 does** (owner's rule), checked with `scripts/vb6run.mjs` or a reproducer's `vb6/` side, never from memory. Where twinBASIC's behaviour is the nominal one, the VB6 behaviour goes in a `> [!NOTE]`; inside a defect's WARNING it is one sentence, such as "VB6 raises error 6, *Overflow*." - **A known twinBASIC defect that silently gives a wrong result is a `> [!WARNING]` until a fixed build is released** (owner's rule): a wrong value with no error raised is data corruption the reader cannot see. A crash is a WARNING too (unsaved data is lost), and so is an error that hides a silent part (a procedure run twice before the error). A defect the reader cannot miss and that loses nothing --- an error raised, a statement refused --- is a `> [!NOTE]`. Name the build in either. **Once a fixed build is released, the callout goes** (owner's rule, 2026-10-05): the documentation is not a record of past defects, so the page then states the correct behaviour with no mention of the defect. The defect's BUGS-TO-REPORT entry says in its HTML comment which page carries the callout, so the callout is removed when the entry is retired. + **A known twinBASIC defect that silently gives a wrong result is a `> [!WARNING]` until a fixed build is released** (owner's rule): a wrong value with no error raised is data corruption the reader cannot see. A crash is a WARNING too (unsaved data is lost), and so is an error that hides a silent part (a procedure run twice before the error). A defect the reader cannot miss and that loses nothing --- an error raised, a statement refused --- is a `> [!NOTE]`. Name the build in either. **Once a fixed build is released, the callout goes** (owner's rule): the documentation is not a record of past defects, so the page then states the correct behaviour with no mention of the defect. The defect's BUGS-TO-REPORT entry says in its HTML comment which page carries the callout, so the callout is removed when the entry is retired. 7. **See Also.** Last section on the page, after `Example`. Format: `- [Symbol](Symbol) <noun>` where `<noun>` is the kind: statement, function, property, method, class, module, package. Pages with annotations use `- [Symbol](Symbol) -- short description` — the `--` source renders as a typographic dash via markdown-it's typographer (en-dash for `--`, em-dash for `---`). Don't write literal `—` in source; keep `--` for consistency across the docs. Order by conceptual proximity, not strict alphabetical. @@ -519,10 +519,10 @@ Why the report separates the wedged task from the merely blocked ones, and why - `addin-test.bat` — tests IDE add-ins by operating an IDE: every lane in `test/addin/lanes.mjs` builds the add-ins it tests into a private copy of the install, opens a project and checks what the add-in does. Outside every gate and outside CI for the same reasons as `examples.bat`; about two minutes for the seventeen lanes today (BETA 997): Samples 10 and 15, the thirteen probe lanes behind Stage 2's answers in [WIP.HelpAddin.md](WIP.HelpAddin.md), and the help add-in in `add-in/` twice, built with its committed copy of the symbol index, `add-in/Resources/SYMBOLS/symbols.json`, which a local build rewrites whenever the index changes. The `help` lane serves the built `docs/_site` on localhost to the add-in's pane; `help-offline` builds the add-in with a help archive of `docs/_site-offline` and the pane's pages come from the add-in's own server. So run `build.bat` first. Exit 0 every lane passed and the registry is as it was found, 1 a lane failed, 2 the harness failed, 3 the registry or a work folder was not put back. See [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) for its rules. - `ide-test.bat` --- the same runner for scenarios that operate the IDE itself rather than an add-in: every lane in `test/ide/lanes.mjs`, base port 9660. Same exit codes, same standing outside every gate and outside CI, same rules. - `try-help-addin.bat` --- opens one IDE on the user's desktop with the help add-in built and loaded, to try it by hand, and puts the registry back once it is closed (see [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler)). Needs `build.bat` first. Exit 0 closed and put back, 1 the add-in or the project did not compile, 2 the tool could not run, 3 not put back. -- `node scripts/shoot_docs.mjs` --- writes the documentation's IDE screenshots (plan and owner's decisions: [WIP.Screenshots.md](WIP.Screenshots.md)). Each picture is a shot in a table, `out` its path under `docs/` (`--only` matches it), in a setup (the main three below; the full list is in WIP.Screenshots.md), each one IDE on a **private** desktop started only when a shot in it is selected: `help` (builds the add-in as `try-help-addin.bat` does, opens `test/addin/helpdemo`, `TB_DOCS_HELP_REAL_TITLEBAR=0`: the eight `docs/IDE/AddIns/Images/Help_<Name>.png`), `no-project` (`Lane.openNoProject` with `includeProjects`: the menus, cut out with alpha, the dialogs that need no project, and the empty panels, each floated on its own, after the dialogs) and `project` (the demo without the add-in: About, which needs the compiler's licence answer, and IDE Options, whose font lists need a project). One setup is not an IDE: `web` starts puppeteer's Chrome through `launchOnDesktop` on a private desktop of its own (never headless, never on the user's desktop; native-window occlusion and occluded-window backgrounding switched off, or a capture waits forever or comes back blank) for the two public pages the FAQ shows, taken once; it needs the network, as do `webpage` (the Webpage pane on `https://docs.twinbasic.com/`, its page captured from the pane's own DevTools target and pasted into the pane's place) and the package-server setups, which never publish. Every IDE gets `--force-device-scale-factor=1` (`Lane`'s `browserArgs`), else the page follows the display's scaling; the page is overridden to 1280x880 at 2x, and `PAGE_DEFAULTS` puts the IDE's options, panel layouts and keyboard groups to their defaults and the recent lists to the tool's own **in the page only**, its save calls replaced first so nothing is written. Each picture is a `Page.captureScreenshot` with a `clip`; a page showing it gives it `{:width height}` of half its pixel size. **Every picture is taken in two themes by one IDE**: dark as `X.png`, then, after `backToPrepared` and `tbTheme_SwitchToLightMode`, Light (not Classic) as `X.light.png` beside it, which the build pairs by name ([WIP.Build.md](WIP.Build.md#pictures-in-two-themes)); a light picture with the dark one's pixels is not kept, and one more than 32 device pixels off the dark one's size fails (a wrong state, not the theme). A label's pill follows the theme unless the shot names a `tone`. A shot's `annotate` list (arrows, rings, badges...) is drawn by `scripts/lib/shot-annotate.mjs` as one SVG layer over the page before the capture and removed after it, the clip growing to hold it. A picture is rewritten only when its bytes differ, and a second run reports every one `unchanged`; a capture is not byte-stable, so `capture` takes up to eight and keeps one equal to the file on disk, and a picture that differs from the file only by a capture's noise (16 grey levels, 0.5% of its pixels) is left as it is. `--jobs N` (default 6) runs the setups at once on their own ports and desktops, a full run in about 200 s (`--jobs 1`: one at a time); `--diffs <dir>` writes the old picture, the new one and an amplified difference map for each that changed, named with its theme. Settle by a condition, never a fixed sleep, and a picture that shows some state sets it itself rather than relying on the shots before it. The page's animations and carets are switched off through a style sheet put into the document and every shadow root; a picture that holds DEBUG CONSOLE lines has their times (and a CodeLens run's time taken) set in the page once the real ones show. **A running program's window** (setup `programs`, fixture `test/shots/programs`) is taken **once** (`once: true`, the dark pass only) with `captureWindow`, cropped to its visible frame, at the display's DPI, shown at its size at 96 DPI. **The Windows user name must never be in a published picture**: the visible text of the page, text boxes included, is searched for it before the first capture and again when a capture is chosen, with the picture's SVG under `--svg` (a page can draw again in between: Project Settings once put a real path back over the one the shot set), and for a program's window the texts of the window and its children, and the run fails when it is there. The tool finds each name it presses F1 on by searching the demo's source, not by line. The add-in is built on a port of its own (`Lane.addAddin`'s `port`): the building IDE's WebView2 process can hold its port after the IDE ends for longer than `launchIde`'s ten seconds. **`--svg`** also draws each picture as SVG from the page as it stands at the capture (`scripts/svgshot/`: a DOM snapshot converted, its text set in per-picture subsets of the fonts the IDE drew it with, cut by `scripts/subset_font.py`, which needs Python with fontTools), compares it with the PNG, and writes `X.svg` or `X.light.svg` beside it only when no more than 0.25% of its pixels are far off; otherwise an older SVG is removed and the page shows the PNG ([WIP.Build.md](WIP.Build.md#pictures-as-svg)). With `--svg` each picture's *bundle*, what its SVG is drawn from, is saved in the gitignored `.svgshot-bundles/` (`--bundles`; never under `docs/`, a bundle holds the user name), and **after a change to the converter, `node scripts/svgshot/replay.mjs` draws every SVG again from the bundles, with no IDE**, in about 20 s for the 370 (an SVG the same as its file is not compared again, `--recheck` compares all; the fonts' cuts are kept in `.svgshot-bundles/subsets/`; one browser per process, GPU off); it lists the pictures whose bundle cannot answer what the converter now asks, with the `--only` command that retakes them ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). `node scripts/svgshot/bench.mjs` tests the converter's replicas of what Chromium draws outside the DOM (scrollbars, drop-down lists) in Edge, with no IDE ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). No wrapper; outside every gate and outside CI, as `try-help-addin.bat` is. Exit 0 every picture written or unchanged, 1 a picture failed, 2 the tool could not run, 3 not put back. Run `build.bat` first. +- `node scripts/shoot_docs.mjs` --- writes the documentation's IDE screenshots (plan and owner's decisions: [WIP.Screenshots.md](WIP.Screenshots.md)). Each picture is a shot in a table, `out` its path under `docs/` (`--only` matches it), in a setup (the main three below; the full list is in WIP.Screenshots.md), each one IDE on a **private** desktop started only when a shot in it is selected: `help` (builds the add-in as `try-help-addin.bat` does, opens `test/addin/helpdemo`, `TB_DOCS_HELP_REAL_TITLEBAR=0`: the eight `docs/IDE/AddIns/Images/Help_<Name>.png`), `no-project` (`Lane.openNoProject` with `includeProjects`: the menus, cut out with alpha, the dialogs that need no project, and the empty panels, each floated on its own, after the dialogs) and `project` (the demo without the add-in: About, which needs the compiler's licence answer, and IDE Options, whose font lists need a project). One setup is not an IDE: `web` starts puppeteer's Chrome through `launchOnDesktop` on a private desktop of its own (never headless, never on the user's desktop; native-window occlusion and occluded-window backgrounding switched off, or a capture waits forever or comes back blank) for the two public pages the FAQ shows, taken once; it needs the network, as do `webpage` (the Webpage pane on `https://docs.twinbasic.com/`, its page captured from the pane's own DevTools target and pasted into the pane's place) and the package-server setups, which never publish. Every IDE gets `--force-device-scale-factor=1` (`Lane`'s `browserArgs`), else the page follows the display's scaling; the page is overridden to 1280x880 at 2x, and `PAGE_DEFAULTS` puts the IDE's options, panel layouts and keyboard groups to their defaults and the recent lists to the tool's own **in the page only**, its save calls replaced first so nothing is written. Each picture is a `Page.captureScreenshot` with a `clip`; a page showing it gives it `{:width height}` of half its pixel size. **Every picture is taken in two themes by one IDE**: dark as `X.png`, then, after `backToPrepared` and `tbTheme_SwitchToLightMode`, Light (not Classic) as `X.light.png` beside it, which the build pairs by name ([WIP.Build.md](WIP.Build.md#pictures-in-two-themes)); a light picture with the dark one's pixels is not kept, and one more than 32 device pixels off the dark one's size fails (a wrong state, not the theme). A label's pill follows the theme unless the shot names a `tone`. A shot's `annotate` list (arrows, rings, badges...) is drawn by `scripts/lib/shot-annotate.mjs` as one SVG layer over the page before the capture and removed after it, the clip growing to hold it. A picture is rewritten only when its bytes differ, and a second run reports every one `unchanged`; a capture is not byte-stable, so `capture` takes up to eight and keeps one equal to the file on disk, and a picture that differs from the file only by a capture's noise (16 grey levels, 0.5% of its pixels) is left as it is. `--jobs N` (default 6) runs the setups at once on their own ports and desktops, a full run in about 200 s (`--jobs 1`: one at a time); `--diffs <dir>` writes the old picture, the new one and an amplified difference map for each that changed, named with its theme. Settle by a condition, never a fixed sleep, and a picture that shows some state sets it itself rather than relying on the shots before it. The page's animations and carets are switched off through a style sheet put into the document and every shadow root; a picture that holds DEBUG CONSOLE lines has their times (and a CodeLens run's time taken) set in the page once the real ones show. **A running program's window** (setup `programs`, fixture `test/shots/programs`) is taken **once** (`once: true`, the dark pass only) with `captureWindow`, cropped to its visible frame, at the display's DPI, shown at its size at 96 DPI. **The Windows user name must never be in a published picture**: the visible text of the page, text boxes included, is searched for it before the first capture and again when a capture is chosen, with the picture's SVG under `--svg` (a page can draw again in between: Project Settings puts a real path back over the one the shot set), and for a program's window the texts of the window and its children, and the run fails when it is there. The tool finds each name it presses F1 on by searching the demo's source, not by line. The add-in is built on a port of its own (`Lane.addAddin`'s `port`): the building IDE's WebView2 process can hold its port after the IDE ends for longer than `launchIde`'s ten seconds. **`--svg`** also draws each picture as SVG from the page as it stands at the capture (`scripts/svgshot/`: a DOM snapshot converted, its text set in per-picture subsets of the fonts the IDE drew it with, cut by `scripts/subset_font.py`, which needs Python with fontTools), compares it with the PNG, and writes `X.svg` or `X.light.svg` beside it only when no more than 0.25% of its pixels are far off; otherwise an older SVG is removed and the page shows the PNG ([WIP.Build.md](WIP.Build.md#pictures-as-svg)). With `--svg` each picture's *bundle*, what its SVG is drawn from, is saved in the gitignored `.svgshot-bundles/` (`--bundles`; never under `docs/`, a bundle holds the user name), and **after a change to the converter, `node scripts/svgshot/replay.mjs` draws every SVG again from the bundles, with no IDE**, in about 20 s for the 370 (an SVG the same as its file is not compared again, `--recheck` compares all; the fonts' cuts are kept in `.svgshot-bundles/subsets/`; one browser per process, GPU off); it lists the pictures whose bundle cannot answer what the converter now asks, with the `--only` command that retakes them ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). `node scripts/svgshot/bench.mjs` tests the converter's replicas of what Chromium draws outside the DOM (scrollbars, drop-down lists) in Edge, with no IDE ([WIP.Screenshots.md](WIP.Screenshots.md#pictures-as-svg)). No wrapper; outside every gate and outside CI, as `try-help-addin.bat` is. Exit 0 every picture written or unchanged, 1 a picture failed, 2 the tool could not run, 3 not put back. Run `build.bat` first. - **The help add-in's project file is written by every build, CI's included.** The `addinProject` task (`builder/addin-project.mjs`) packs `add-in/` into `tB/IDE/AddIns/downloads/tbDocsHelp.twinproj` in the **online tree only**, which the Help Add-In page offers as a download; `_config.yml`'s `addin_project` declares both ends, so a fixture config gets none. It waits for the `helpArchive` task. It packs the files `git ls-files` lists there, read from the working folder, never a `Resources/HELP/` file from the disk, with CRLF made LF outside `Resources/` (so a Windows and a Linux build write the same bytes), the `tB/symbols.json` the same build wrote in place of the committed copy, and **the zip that build's archive task wrote** as `Resources/HELP/site.zip`, passed in memory (a build that wrote none --- `--no-help-archive`, `--serve`, another `--dest`, a fixture --- packs without it, and the summary line says `without the offline archive`). The download is about 28 MB with the archive, 1 MB without. The offline tree holds no copy (the project carries that tree's archive, so a copy there would be circular), and its links to the file are rewritten to the website by the general rule in `offline-rewrite.mjs`: a URL the offline tree lacks but the online tree has becomes `site.url` + path, and is no miss (`computeWebsiteUrl`; the offline link check's `forbidExempt` excuses exactly those links, `isWebsiteOnlyLink`). The pack is `importProject` from `scripts/impexp.mjs`, the one module under `scripts/` that `builder/` may import (`biome.jsonc`); `check_tree_fresh.mjs` watches it and `add-in/` for that reason. The publish allowlist exempts the one path in the trees only (`publishPolicyFor`'s `generated`); a `.twinproj` anywhere else, or in `docs/`, is refused. `--serve` writes it into its one tree; `--dry-run` writes nothing. A failure (no git) fails the build with exit 1 and the index audit names the missing file. **Don't build the DLLs in CI from it**: that needs a twinBASIC install; the download is the project, and the reader builds it. Why each of these: [WIP.Build.md](WIP.Build.md#the-help-add-ins-project-file); `test/addin-project.test.mjs` is the gate. - **The help archive is the `helpArchive` task of the build's graph.** `builder/help-archive-step.mjs` writes `add-in/Resources/HELP/site.zip` from `docs/_site-offline` through `lib/help-archive.mjs`, once every task that writes into the offline tree has finished (`flushJoin`, `scss`, `writeOffline`; the archive task lists exactly those) and **before** the Gantt injection, so the zip holds `BuildInfo.html` without the chart and `assets/images/gantt.svg` as the 105-byte placeholder the sources hold (it is a tracked static file, so the file list already has it). It reads the file back to verify it. A mismatch fails the build (exit 1). After the Gantt injection `recheckHelpArchive` checks that the offline tree on disk lists the same files as the archive and differs from it only in `BuildInfo.html` and `gantt.svg`; a task that wrote into the offline tree late fails the build there. The help add-in embeds the zip as a resource and serves the pages from it with its own server; a build of the add-in without it uses the live site. The file is gitignored (about 26 MB), so a build of the add-in from a clean clone is the online one until `build.bat` has run. The download carries the same zip (previous bullet). **`--no-help-archive` turns the step off, and neither CI workflow may pass it** (`check_ci_workflows.mjs` refuses it, in `ALLOWED.forbiddenBuildFlags`), because CI's published download would then hold no archive. It runs only for the guarded source tree (`docs`) built into the default `docs/_site-offline` (not another `--dest`, so `compare_trees` and `check_links_diff` leave it alone), never under `--serve` or `--dry-run`, never without an offline tree, and not keyed on `$CI`. There is no command-line tool for the archive: the `help-offline` lane makes its own with `writeHelpArchive`. The zip's format is fixed by the add-in's reader, which does no inflating ([WIP.HelpAddin.md](WIP.HelpAddin.md), Stage 4, increment 5). -- `node scripts/bug_repro.mjs` --- the reproducer projects under `bugs/<slug>/` for the entries of [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): `new`, `pack` (impexp, then the zip, written in Node), `compile`, `build` and `run` through `tbbuild` and `tbrun`, `vb6` (builds the optional `vb6/` VB6 project beside the twinBASIC one in a temp copy, runs its exe on a private desktop, and prints its `out.txt`; a project with a form or a user control is built without Unattended Execution, which VB6 refuses for it; `pack` zips its sources into `<slug>-vb6.zip`; VB6 is started only by `scripts/lib/vb6.mjs`, never from a shell; a graphical defect's `images` key makes `run` and `vb6` keep what the `PngDump` modules of `test/repro-templates/png/` draw as `images/<name>-tb.png` and `-vb6.png`, with a `-compare.png` of both, tested by `test/png.test.mjs`), `file` (moves a filed entry and its reproducer to `bugs/filed/<slug>/`; `file --marked` does every marked entry), and `verify`, which reads each `bugs/*/repro.json` and `bugs/filed/*/repro.json` and says whether the bug still reproduces on the newest beta; a `lane` reproducer names the `ide-test` or `addin-test` lane and tests that assert its bug, which `verify` runs last through that runner, a `probe` one a script under `scripts/`, and a `test` one its own `[TestCase]` Subs and compile cases through `tbrun --tests`, judged by the `PASS` lines of what is fixed and the `FAIL` lines of the bug (`new --template tests` starts one). **No reproducer should need a person at the IDE** (owner, 2026-10-05): a bug the IDE has to be operated for gets a lane test, and `manual` is left for what cannot be automated at all. It has no wrapper. **`verify` is run by a person only**, never by a gate or CI: it needs a twinBASIC install, like `examples.bat`. Default port 9440, and `--jobs N` takes N ports from there. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. +- `node scripts/bug_repro.mjs` --- the reproducer projects under `bugs/<slug>/` for the entries of [BUGS-TO-REPORT.md](BUGS-TO-REPORT.md): `new`, `pack` (impexp, then the zip, written in Node), `compile`, `build` and `run` through `tbbuild` and `tbrun`, `vb6` (builds the optional `vb6/` VB6 project beside the twinBASIC one in a temp copy, runs its exe on a private desktop, and prints its `out.txt`; a project with a form or a user control is built without Unattended Execution, which VB6 refuses for it; `pack` zips its sources into `<slug>-vb6.zip`; VB6 is started only by `scripts/lib/vb6.mjs`, never from a shell; a graphical defect's `images` key makes `run` and `vb6` keep what the `PngDump` modules of `test/repro-templates/png/` draw as `images/<name>-tb.png` and `-vb6.png`, with a `-compare.png` of both, tested by `test/png.test.mjs`), `file` (moves a filed entry and its reproducer to `bugs/filed/<slug>/`; `file --marked` does every marked entry), and `verify`, which reads each `bugs/*/repro.json` and `bugs/filed/*/repro.json` and says whether the bug still reproduces on the newest beta; a `lane` reproducer names the `ide-test` or `addin-test` lane and tests that assert its bug, which `verify` runs last through that runner, a `probe` one a script under `scripts/`, and a `test` one its own `[TestCase]` Subs and compile cases through `tbrun --tests`, judged by the `PASS` lines of what is fixed and the `FAIL` lines of the bug (`new --template tests` starts one). **No reproducer should need a person at the IDE** (owner): a bug the IDE has to be operated for gets a lane test, and `manual` is left for what cannot be automated at all. It has no wrapper. **`verify` is run by a person only**, never by a gate or CI: it needs a twinBASIC install, like `examples.bat`. Default port 9440, and `--jobs N` takes N ports from there. Its command-line cases are in `scripts/lib/cli-cases.mjs`, and need no IDE. - `node scripts/vb6run.mjs <file | ->` and `node scripts/vb6run.mjs --docs [--only <regex>]` --- build and run VB6 code, so that what a sample prints in twinBASIC can be compared with VB6: a stand-alone file of statements or a `.bas` module, or `--docs`, which builds the documentation's `check_run` fences in VB6 and says for each whether it prints what its page says (`same`, `differs`, `not VB6`, `error`, `refused`; a `projname=` group is built as a project of its own, its `slot=file` fences translated into VB6 `.cls` and `.bas` components). It has no wrapper and is outside every gate and outside CI: it needs VB6 (`--vb6`, `VB6_EXE`, or the standard install folder). **Never start `VB6.EXE` from a shell**: in Git Bash `/make` is rewritten as a path and VB6 answers with a modal box on the desktop; the tool spawns it from Node with an argument array, and a sample that calls `MsgBox` or `InputBox` or contains `End` is refused. Its `Debug.Print` rewrite and its file translation have probes in `test/example-batches.test.mjs`, and its command-line cases are in `scripts/lib/cli-cases.mjs`; both need no VB6. What it relies on is in [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). Three generators sit outside that loop and produce committed artifacts rather than build output — none runs during a build, and none is needed for one. `python scripts/build_fonts.py` rebuilds the subset webfaces under `docs/assets/fonts/` and needs a network connection; `node scripts/build_dot_metrics.mjs` regenerates `builder/inter-metrics.json` from those webfaces and needs only a browser. See [Typography](#typography). `node indexer/twin-index.mjs derive` regenerates `builder/package-api.json`, the packages' declared API that the build's symbol index (`tB/symbols.json`, for the IDE help add-in) is annotated from; it needs a twinBASIC install (or `--release`), so **run it when the reference is re-indexed against a newer build** and commit it with the pages and the snapshots. See [WIP.HelpAddin.md, Stage 3](WIP.HelpAddin.md#stage-3-the-symbol-index-generated-by-the-docs-build). @@ -627,8 +627,8 @@ whitespace inside inline `<code>` is content. Favor concise one-line git commit messages. -**A piece of work goes in a short-lived worktree unless the owner says not to** (owner, -2026-10-11): ask when starting, and without a no, proceed. `git worktree add -b <name> +**A piece of work goes in a short-lived worktree unless the owner says not to** (owner): +ask when starting, and without a no, proceed. `git worktree add -b <name> .claude/worktrees/<name> staging` branches from the local `staging`, which holds commits `origin` may not have yet; give it a `node_modules` junction to the main checkout's (`New-Item -ItemType Junction`) and run `build.bat` once. When the work is done: rebase on diff --git a/WIP.tbIDE.md b/WIP.tbIDE.md index 6b580d70a..a1715c6ae 100644 --- a/WIP.tbIDE.md +++ b/WIP.tbIDE.md @@ -73,7 +73,7 @@ Almost every `.twin` declares one or two `Public Interface I<X>V1 Extends stdole **Versioning is conveyed by interface chains.** Two cases visible in the source: - `IFileV1` → `IFileV2 Extends IFileV1` (V2 adds `ReadText(ReadTextFlags)`). The `File` CoClass declares `[Default] Interface IFileV2`. Document the V2 surface as the canonical `File` page; do not split V1 vs V2. (Mention in passing that `ReadText` is V2-only and consequently won't bind against very early IDE builds — though in practice every shipping IDE is V2+.) -- `IHostV1` → `ItbHostEventsV1` → `ItbHostEventsV2 Extends V1` → `ItbHostEventsV3 Extends V2`. The `Host` CoClass declares `[Default, Source] Interface ItbHostEventsV3`. The new members on V2 / V3 (`OnChangedActiveEditor`, `OnChangedTheme`) are each tagged **`[AllowUnpopulatedVtableEntry]`**, which [Attributes](docs/Reference/Attributes.md#allowunpopulatedvtableentry) describes as marking a prototype that a class implementing the interface need not supply. Do not write that the attribute lets a newer addin "still load against an older IDE". Nothing measured it, and P14 found the opposite mechanism in the loader: the linker stamps every addin's entry point `_v3`, and an IDE whose loader does not know a DLL's stamp refuses it. Every install from BETA 947 on knows `_v3`, so the claim cannot be tested either way. +- `IHostV1` → `ItbHostEventsV1` → `ItbHostEventsV2 Extends V1` → `ItbHostEventsV3 Extends V2`. The `Host` CoClass declares `[Default, Source] Interface ItbHostEventsV3`. The new members on V2 / V3 (`OnChangedActiveEditor`, `OnChangedTheme`) are each tagged **`[AllowUnpopulatedVtableEntry]`**, which [Attributes](docs/Reference/Attributes.md#allowunpopulatedvtableentry) describes as marking a prototype that a class implementing the interface need not supply. Do not write that the attribute lets a newer addin "still load against an older IDE". Nothing measured it, and the loader works the other way (P14): the linker stamps every addin's entry point `_v3`, and an IDE whose loader does not know a DLL's stamp refuses it. Every install from BETA 947 on knows `_v3`, so the claim cannot be tested either way. Document all `Host` events together on the `Host.md` page (the per-version split is a compatibility detail, not a user-facing concept). @@ -211,7 +211,7 @@ Surface as the canonical safe-cast pattern on `CodeEditor.md`. The `keyString` argument is a literal key with optional `{CTRL}` / `{SHIFT}` / `{ALT}` prefixes, e.g. `"{CTRL}{SHIFT}d"` (the source-side `[Description]`'s example). The `Callback` is `AddressOf` an addin function; the function takes no arguments and returns nothing --- a `Private Sub` with no parameters, in the add-in's class, works (measured, BETA 983 and 995). -**The source's own example does not fire.** No sample uses `KeyboardShortcuts.Add`, so it was measured: P1 and P2 in [WIP.HelpAddin.md](WIP.HelpAddin.md), with the lane `test/addin/keys.test.mjs`. In BETA 983 and 995 a shortcut with `{CTRL}` or `{ALT}` never fires when pressed. The prefixes must come in the order `{ctrl}{shift}{alt}`. A shortcut fires on key-up wherever the focus is in the IDE's window, and a key that types fires each time it is typed. The published page says all of that, with an example on Shift+F12; keep it in step with the lane. +**The source's own example does not fire.** No sample uses `KeyboardShortcuts.Add`, so the behaviour comes from P1 and P2 in [WIP.HelpAddin.md](WIP.HelpAddin.md), with the lane `test/addin/keys.test.mjs`. In BETA 983 and 995 a shortcut with `{CTRL}` or `{ALT}` never fires when pressed. The prefixes must come in the order `{ctrl}{shift}{alt}`. A shortcut fires on key-up wherever the focus is in the IDE's window, and a key that types fires each time it is typed. The published page says all of that, with an example on Shift+F12; keep it in step with the lane. **Do not describe callbacks as running "on the IDE's UI thread".** It is unmeasured and probably misleading: an add-in runs inside the compiler's process (P10), not in the IDE's page, which is a WebView2 process of its own. The KeyboardShortcuts page does not say it. **The AddinTimer page still does**, three times, with "long-running work ... will block the UI thread" --- unverified. Which thread runs an add-in's callbacks, and what a slow one holds up, is not known. Fix the AddinTimer page when something measures it. From 7ac79610be5d5e29bae6d933a92a25519561d51e Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober <kuba@mareimbrium.org> Date: Wed, 14 Oct 2026 10:25:33 +0200 Subject: [PATCH 03/13] tbRegExp: moved to 1.8.0.0, whose step budget is per match and scales with the string, so long matches work (the 3,300-character limit is gone); SearchAborted reports a search that gave up --- WIP.md | 2 +- builder/page-baseline.json | 2 +- docs/Reference/Contributed.md | 2 +- .../Contributed/tbRegExp/RegExp/Execute.md | 3 +- .../Contributed/tbRegExp/RegExp/IsValid.md | 2 +- .../Contributed/tbRegExp/RegExp/Replace.md | 3 +- .../tbRegExp/RegExp/SearchAborted.md | 78 +++++++++++++++++++ .../Contributed/tbRegExp/RegExp/Split.md | 3 +- .../Contributed/tbRegExp/RegExp/Test.md | 3 +- .../Contributed/tbRegExp/RegExp/index.md | 1 + docs/Reference/Contributed/tbRegExp/index.md | 35 +++++---- .../snapshots/contributed/tbRegExp/api.json | 23 ++++-- .../contributed-tbRegExp/Settings | 2 +- 13 files changed, 124 insertions(+), 35 deletions(-) create mode 100644 docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md diff --git a/WIP.md b/WIP.md index dc05504c4..29c5e3367 100644 --- a/WIP.md +++ b/WIP.md @@ -34,7 +34,7 @@ sibling. History goes nowhere: git keeps it. Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. -Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON and xCalendar complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. The two console packages carry a NOTE (owner) that the samples harness cannot yet capture a console, so their console claims follow the source. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-probes/xCalendar/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-probes/TBMANLIB_Dialog/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.7.0.0 is Contributed although a match is at most about 3,300 characters long: its engine gives up a search from one start position after 10,000 steps, a limit fixed inside the package (`a*` on 5,000 `a` finds 3,331 characters from position 1669; confirmed by a second agent, and the same limit is in upstream vba-regex, where a caller can raise it), stated in a WARNING at the top of its landing page and in its line in `Contributed.md` (owner). Each one has a chapter in the book's Packages part. **ArrayListLib, BigNumberLib, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1005 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. See *Where things live* for how a package moves between the two lists. +Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON and xCalendar complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. The two console packages carry a NOTE (owner) that the samples harness cannot yet capture a console, so their console claims follow the source. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-probes/xCalendar/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-probes/TBMANLIB_Dialog/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.8.0.0 gives up a search that uses up its step budget (5,000 steps for each character of the string, at least 1,000,000 and at most 50,000,000, for the search of one match; fixed inside the package) and reports it in the read-only `RegExp.SearchAborted`, documented on its own page and in a section of the landing page; `a*` and `^.*$` match 1,000,000 characters whole in about half a second. Each one has a chapter in the book's Packages part. **ArrayListLib, BigNumberLib, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1005 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. See *Where things live* for how a package moves between the two lists. The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. diff --git a/builder/page-baseline.json b/builder/page-baseline.json index c73ead275..958e2350a 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1282, + "pages": 1283, "staticFiles": 1009 } diff --git a/docs/Reference/Contributed.md b/docs/Reference/Contributed.md index 94eb22c19..a68773f33 100644 --- a/docs/Reference/Contributed.md +++ b/docs/Reference/Contributed.md @@ -21,7 +21,7 @@ Each package is versioned apart from twinBASIC, and its section says which versi - [**TBMANLIB_ListBoxHScroll**](TBMANLIB_ListBoxHScroll/) -- a horizontal scroll bar for a **ListBox** - [**TBMANLIB_RES**](TBMANLIB_RES/) -- resources embedded in an executable, as bytes, strings, hex, Base64 or pictures - [**tbProjPacker**](tbProjPacker/) -- a `.twinproj` or `.twinpack` file unpacked into a folder of source files, and packed again -- [**tbRegExp**](tbRegExp/) -- regular expressions, in place of VBScript's **RegExp** object; a match is at most about 3,300 characters long +- [**tbRegExp**](tbRegExp/) -- regular expressions, in place of VBScript's **RegExp** object - [**tsJson**](tsJson/) -- a JSON model with parsing, JSONPath queries, JSON Pointer, JSON Patch and JSON Schema validation - [**tsPwGen**](tsPwGen/) -- passwords, PINs and passphrases, with their entropy known before they are generated - [**TwinTimerLib**](TwinTimerLib/) -- a Win32 timer class that reports the elapsed time diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md b/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md index 9508c60d5..b95a8d789 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md @@ -36,8 +36,7 @@ Debug.Print re.Execute("fooBarBaz").Count ' 2 Debug.Print re.Execute("fooBarBaz")(1).FirstIndex ' 6 ``` -> [!WARNING] -> A match of more than a few thousand characters is cut short, and nothing is raised: `a*` on a string of 5,000 `a` returns one match that starts at position 1669 and is 3,331 characters long, not the whole string. The [overview of the package](../) describes the limit. +A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging. The collection then holds the matches found before it gave up, and [**SearchAborted**](SearchAborted) returns **True**. Each **Match** gives the matched text ([**Value**](../Match/Value)), the position where it starts ([**FirstIndex**](../Match/FirstIndex), counted from 0), its length, and the text of each group of the pattern ([**SubMatches**](../Match/SubMatches)). A group written `(?<name>...)` can be read by its name. diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md index 8f2d28c48..ff9076812 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md @@ -26,7 +26,7 @@ Check **IsValid** before [**Execute**](Execute), [**Test**](Test), [**Replace**] Although nothing is raised, the compile error of an invalid pattern stays in the **Err** object after the call that compiled the pattern returns. A program that tests **Err** for other errors calls **Err.Clear** first. > [!NOTE] -> A quantifier whose lower count is above its upper count, such as `a{2,1}`, is not found when the pattern is compiled, so **IsValid** returns **True** for it. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise error 3000 when a search reaches the quantifier, with the description that Windows gives that number, *The specified print monitor is unknown.* This is how version 1.7.0.0 behaves. +> A quantifier whose lower count is above its upper count, such as `a{2,1}`, is not found when the pattern is compiled, so **IsValid** returns **True** for it. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise error 3000 when a search reaches the quantifier, with the description that Windows gives that number, *The specified print monitor is unknown.* This is how version 1.8.0.0 behaves. ## Example diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md index 386cf3adf..b574aa4ec 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md @@ -56,8 +56,7 @@ re.Pattern = "^" Debug.Print Replace(re.Replace("a" & vbLf & "b" & vbLf & "c", "> "), vbLf, "/") ' > a/> b/> c ``` -> [!WARNING] -> A match of more than a few thousand characters is cut short, and nothing is raised, so **Replace** replaces a shorter part of the text than the pattern describes. The [overview of the package](../) describes the limit. +A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging, and **Replace** then replaces only the matches found before it gave up. [**SearchAborted**](SearchAborted) returns **True** in that case. An invalid pattern raises the compile error, with the number in [**LastErrorNumber**](LastErrorNumber) and the description in [**LastError**](LastError). Check [**IsValid**](IsValid) first to avoid it. diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md b/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md new file mode 100644 index 000000000..b6303528b --- /dev/null +++ b/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md @@ -0,0 +1,78 @@ +--- +title: SearchAborted +parent: RegExp +permalink: /tB/Packages/Contributed/tbRegExp/RegExp/SearchAborted +has_toc: false +--- + +# SearchAborted +{: .no_toc } + +Returns whether the last search of this object gave up before it finished. + +Syntax: *object*.**SearchAborted** + +*object* +: *required* An object expression that evaluates to a **RegExp**. + +Returns a **Boolean**: **True** when the most recent call of [**Test**](Test), [**Execute**](Execute), [**Replace**](Replace) or [**Split**](Split) on *object* stopped because its search ran out of steps, **False** otherwise. The property is read-only. It is **False** for an object that has not searched yet. + +## Remarks + +A search is given a budget of steps, where a step is one operation of the compiled pattern. A pattern that backtracks without end, such as `(a+)+b` on a string of thirty `a`, needs twice as many steps for each `a` it is given, and the budget is what stops it. VBScript's **RegExp** has no such limit and for that string does not return in any reasonable time. + +When the budget runs out, the search stops and nothing is raised. Whatever was found before is returned, and the result can be short or empty: [**Test**](Test) returns **False**, [**Execute**](Execute) returns the matches found before the search stopped, [**Replace**](Replace) returns *sourceString* with only those matches replaced, and [**Split**](Split) splits only at them. A program that cannot tell this from a string without a match reads **SearchAborted** after the call. + +The budget is 5,000 steps for each character of the string, at least 1,000,000 and at most 50,000,000. It is not a property of the object, and a program cannot change it. It applies to the search for one match, so with [**Global**](Global) set to **True** each match has a budget of its own, and the first search that runs out ends the call. A simple pattern needs about three steps for each character it matches, so a long match is not affected: `a*` on a string of 1,000,000 `a` returns all of it in about half a second. + +Each call of **Test**, **Execute**, **Replace** or **Split** sets the value again: a call that finishes its search sets it to **False**, and so does a call that raises an error because the pattern is not valid, because no search was made. The value belongs to the object. A search on another **RegExp** does not change it, and a new object returns **False**. + +> [!NOTE] +> A pattern that is not catastrophic can also run out of steps, if it tries again from each start position and the string is long. `\d+x` on a string of 5,000 digits and no `x` takes about 5 seconds and then gives up, with the same result as for a string that has no match, and **SearchAborted** returns **True**. A string of 2,000 digits finishes in about 2 seconds and returns **False**. + +## Example + +This example searches a string of thirty `a` for `(a+)+b`, which cannot match and takes a very long time to prove it. The search gives up after about a fifth of a second, and **SearchAborted** says that the **False** from **Test** is not an answer. A search of a string that has no `b`, where the pattern is not catastrophic, finishes. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(a+)+b" +Debug.Print re.SearchAborted ' False + +Debug.Print re.Test(String(30, "a")) ' False +Debug.Print re.SearchAborted ' True + +Debug.Print re.Test("aaab") ' True +Debug.Print re.SearchAborted ' False + +re.Pattern = "a+b" +Debug.Print re.Test(String(30, "a")) ' False +Debug.Print re.SearchAborted ' False +``` + +This example shows that the value is per object, and that a search which gave up still returns the matches found before it. + +```tb check_build check_run +Dim re As New RegExp +Dim other As New RegExp +re.Pattern = "x|(a+)+b" +re.Global = True +other.Pattern = "a" + +Dim matches As MatchCollection +Set matches = re.Execute("xx" & String(30, "a")) +Debug.Print matches.Count ' 2 +Debug.Print re.SearchAborted ' True + +Debug.Print other.Test("a") ' True +Debug.Print other.SearchAborted ' False +Debug.Print re.SearchAborted ' True +``` + +## See Also + +- [Test](Test) method -- returns whether a string contains a match +- [Execute](Execute) method -- returns the matches +- [Replace](Replace) method -- replaces the matches +- [Split](Split) method -- splits a string at the matches +- [RegExp](./) class -- overview diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Split.md b/docs/Reference/Contributed/tbRegExp/RegExp/Split.md index 7631e1d3e..8009f2546 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/Split.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Split.md @@ -43,8 +43,7 @@ Debug.Print parts.Count ' 3 Debug.Print parts(1) & "|" & parts(2) & "|" & parts(3) ' foo|Bar|Baz ``` -> [!WARNING] -> A match of more than a few thousand characters is cut short, and nothing is raised, so **Split** splits at a shorter part of the text than the pattern describes. The [overview of the package](../) describes the limit. +A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging, and **Split** then splits only at the matches found before it gave up. [**SearchAborted**](SearchAborted) returns **True** in that case. When **Pattern** is not a valid regular expression, **Split** raises the compile error. Its number is [**LastErrorNumber**](LastErrorNumber), its description is [**LastError**](LastError), and its source is `RegExp`. Read [**IsValid**](IsValid) first to avoid the error. diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Test.md b/docs/Reference/Contributed/tbRegExp/RegExp/Test.md index c770c205e..b7116d0ff 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/Test.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Test.md @@ -26,8 +26,7 @@ Returns **True** if [**Pattern**](Pattern) finds a match in *sourceString*, othe [**IgnoreCase**](IgnoreCase), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) apply to the matching. [**Global**](Global) makes no difference, because one match is enough. -> [!WARNING] -> A match of more than a few thousand characters is not found, and nothing is raised: `^.*$` on a string of 5,000 characters returns **False**. The [overview of the package](../) describes the limit. +A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging, and **Test** then returns **False**. [**SearchAborted**](SearchAborted) tells that **False** from a string that has no match. When **Pattern** is not a valid regular expression, **Test** raises the compile error. Its number is [**LastErrorNumber**](LastErrorNumber), its description is [**LastError**](LastError), and its source is `RegExp`. Read [**IsValid**](IsValid) first to avoid the error. diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/index.md b/docs/Reference/Contributed/tbRegExp/RegExp/index.md index a98ad2734..413728d09 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/index.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/index.md @@ -60,6 +60,7 @@ A pattern that does not compile does not raise an error at once. [**IsValid**](I - [LastErrorNumber](LastErrorNumber) -- returns the number of the compile error - [MultiLine](MultiLine) -- returns or sets whether `^` and `$` match at line boundaries - [Pattern](Pattern) -- returns or sets the regular expression +- [SearchAborted](SearchAborted) -- returns whether the last search gave up before it finished ## Methods diff --git a/docs/Reference/Contributed/tbRegExp/index.md b/docs/Reference/Contributed/tbRegExp/index.md index 4531bbf6d..9abf8fcbb 100644 --- a/docs/Reference/Contributed/tbRegExp/index.md +++ b/docs/Reference/Contributed/tbRegExp/index.md @@ -4,33 +4,20 @@ parent: Contributed Packages has_toc: false exclude_from_docs: - StaticRegex -indexed_from: 1.7.0.0 +indexed_from: 1.8.0.0 permalink: /tB/Packages/Contributed/tbRegExp/ --- # tbRegExp Package {: .no_toc } -**Author:** GCuser99. **Version:** 1.7.0.0. **On GitHub:** [GCuser99/tbRegExp](https://github.com/GCuser99/tbRegExp), in the folder *Package*. +**Author:** GCuser99. **Version:** 1.8.0.0. **On GitHub:** [GCuser99/tbRegExp](https://github.com/GCuser99/tbRegExp), in the folder *Package*. The **tbRegExp** contributed package provides regular expressions for twinBASIC projects, as a replacement for the **RegExp** object of VBScript. It follows the same object model. A [**RegExp**](RegExp/) is given a pattern and options, its [**Execute**](RegExp/Execute) method returns a [**MatchCollection**](MatchCollection/) of [**Match**](Match/) objects, and each **Match** holds the text captured by the groups of the pattern as a [**SubMatches**](SubMatches/) object. Positions are zero-based, as in VBScript. -> [!WARNING] -> A match can be at most about 3,300 characters long. A longer one is reported cut short, later in the string, or not at all, and no error is raised. The engine gives up a search from one starting position after 10,000 steps, where a step is one operation of the compiled pattern, and then starts again one character later. The limit is fixed inside the package, and a program cannot change it. A pattern that repeats one character, such as `a*`, `.*` or `[^,]*`, takes three steps for each character it matches, so its longest match is 3,331 characters, and a pattern with more to do for each character has a shorter longest match. On a string of 5,000 `a`, `a*` finds a match that starts at position 1669 and is 3,331 characters long, where VBScript's **RegExp** finds the whole string from position 0, and `^.*$` finds no match at all, so [**Test**](RegExp/Test) returns **False**. Each starting position that fails costs up to 10,000 steps, so a search over a long run is slow as well: `a*` over a run of 9,500 characters takes several seconds, and returns a match of 3,331 characters from position 6169. This is how version 1.7.0.0 behaves. - -```tb check_build check_run -Dim re As New RegExp -Dim m As Match -re.Pattern = "a*" -Set m = re.Execute(String(5000, "a"))(0) -Debug.Print m.FirstIndex & ", " & m.Length ' 1669, 3331 -re.Pattern = "^.*$" -Debug.Print re.Test(String(5000, "a")) ' False -``` - The matching is done by the [vba-regex](https://github.com/sihlfall/vba-regex) engine of sihlfall, which the package includes as a private module, **StaticRegex**. -Beyond VBScript's object, **RegExp** has the [**DotAll**](RegExp/DotAll) and [**Flags**](RegExp/Flags) options, the [**Split**](RegExp/Split) method, and [**IsValid**](RegExp/IsValid), [**LastError**](RegExp/LastError) and [**LastErrorNumber**](RegExp/LastErrorNumber), which report an invalid pattern before it is used. [**Replace**](RegExp/Replace) reads a `$` in the replacement string as VBScript does, and adds `$<name>` for a named group. +Beyond VBScript's object, **RegExp** has the [**DotAll**](RegExp/DotAll) and [**Flags**](RegExp/Flags) options, the [**Split**](RegExp/Split) method, [**IsValid**](RegExp/IsValid), [**LastError**](RegExp/LastError) and [**LastErrorNumber**](RegExp/LastErrorNumber), which report an invalid pattern before it is used, and [**SearchAborted**](RegExp/SearchAborted), which reports a search that gave up. [**Replace**](RegExp/Replace) reads a `$` in the replacement string as VBScript does, and adds `$<name>` for a named group. ## Pattern syntax @@ -93,6 +80,21 @@ re.Pattern = "a\{x}" Debug.Print re.Execute("a{x}")(0).Value ' a{x} ``` +## Searches that give up + +A pattern that backtracks without end, such as `(a+)+b` on a string of thirty `a`, does not hang a search as it does in VBScript's **RegExp**. Every search has a budget of steps, which a program cannot change, and when it runs out the search stops and raises no error. Its result is then short or empty, and a program that has to tell it from a true result reads [**SearchAborted**](RegExp/SearchAborted) after the call. A long match is no problem: `a*` and `^.*$` match a string of 1,000,000 `a` whole. + +```tb check_build check_run +Dim re As New RegExp +re.Pattern = "(a+)+b" +Debug.Print re.Test(String(30, "a")) ' False +Debug.Print re.SearchAborted ' True + +re.Pattern = "^.*$" +Debug.Print re.Test(String(100000, "a")) ' True +Debug.Print re.SearchAborted ' False +``` + ## Classes - [Match](Match/) -- holds one match that a RegExp found in a string: the matched text, where it starts, how many characters it has, and the text captured by its groups @@ -116,6 +118,7 @@ Debug.Print re.Execute("a{x}")(0).Value ' a{x} - [MultiLine](RegExp/MultiLine) -- returns or sets whether `^` and `$` match at line boundaries - [Pattern](RegExp/Pattern) -- returns or sets the regular expression - [Replace](RegExp/Replace) -- replaces the matches in a string + - [SearchAborted](RegExp/SearchAborted) -- returns whether the last search gave up before it finished - [Split](RegExp/Split) -- splits a string at the matches - [Test](RegExp/Test) -- returns whether a string contains a match - [SubMatches](SubMatches/) -- holds the strings that the capture groups of a pattern captured in one Match diff --git a/indexer/snapshots/contributed/tbRegExp/api.json b/indexer/snapshots/contributed/tbRegExp/api.json index 4fd98c44d..3c6b3c1c7 100644 --- a/indexer/snapshots/contributed/tbRegExp/api.json +++ b/indexer/snapshots/contributed/tbRegExp/api.json @@ -2,7 +2,7 @@ "format": 3, "package": "tbRegExp", "project": "tbRegExp", - "version": "1.7.0.0", + "version": "1.8.0.0", "types": [ { "name": "IEnumVARIANT", @@ -115,6 +115,7 @@ {"name":"Pattern","kind":"property","vis":"public","signature":"Public Property Let Pattern(ByVal pPattern As String)","accessor":"Let","description":"The regular expression pattern."}, {"name":"Pattern","kind":"property","vis":"public","signature":"Public Property Get Pattern() As String","returnType":"String","accessor":"Get"}, {"name":"Replace","kind":"function","vis":"public","signature":"Public Function Replace(ByVal sourceString As String, ByVal replaceString As String) As String","returnType":"String","description":"Replaces matches with replaceString and returns the String with replacements made."}, + {"name":"SearchAborted","kind":"property","vis":"public","signature":"Public Property Get SearchAborted() As Boolean","returnType":"Boolean","accessor":"Get","description":"True if the most recent Test, Execute, Replace or Split on this object gave up before finishing its search. Results may be truncated or missing, and nothing is raised. In practice only a catastrophically backtracking pattern - nested quantifiers such as '(a+)+b' - can reach this."}, {"name":"Split","kind":"function","vis":"public","signature":"Public Function Split(ByVal sourceString As String) As Collection","returnType":"Collection","description":"Splits an input string into an array of substrings at the positions defined by a regular expression match. Needs Global Property set to 'True'."}, {"name":"Test","kind":"function","vis":"public","signature":"Public Function Test(ByVal sourceString As String) As Boolean","returnType":"Boolean","description":"Returns 'True' if Pattern finds a match in sourceString, otherwise returns 'False'."}, {"name":"mCapEntryPoints","kind":"field","vis":"private","signature":"Private mCapEntryPoints() As Long"}, @@ -130,6 +131,7 @@ {"name":"mNamedCount","kind":"field","vis":"private","signature":"Private mNamedCount As Long"}, {"name":"mNumCaptures","kind":"field","vis":"private","signature":"Private mNumCaptures As Long"}, {"name":"mPattern","kind":"field","vis":"private","signature":"Private mPattern As String"}, + {"name":"mSearchAborted","kind":"field","vis":"private","signature":"Private mSearchAborted As Boolean"}, {"name":"regex","kind":"field","vis":"private","signature":"Private regex As StaticRegex.RegexTy"} ] }, @@ -157,6 +159,7 @@ {"name":"AstTableInitialize","kind":"sub","vis":"private","signature":"Private Sub AstTableInitialize()"}, {"name":"AstToBytecode","kind":"sub","vis":"private","signature":"Private Sub AstToBytecode(ByRef ast() As Long, ByRef identifierTree As IdentifierTreeTy, ByVal caseInsensitive As Boolean, ByRef bytecode() As Long)"}, {"name":"Clear","kind":"sub","vis":"private","signature":"Private Sub Clear(ByRef sb As StaticStringBuilder)"}, + {"name":"ClearStepsLimitHit","kind":"sub","vis":"public","signature":"Public Sub ClearStepsLimitHit()"}, {"name":"CloseGroups","kind":"function","vis":"private","signature":"Private Function CloseGroups( ByRef ast As ArrayBuffer, ByRef parseStack As ArrayBuffer, ByRef pendingDisjunction As Long, ByRef currentDisjunction As Long, ByRef potentialConcat2 As Long, ByRef potentialConcat1 As Long, ByRef modifierMask As Long, ByRef spilloverWriteMask As Long, ByVal whatToHandle As Long ) As Long","returnType":"Long"}, {"name":"Compile","kind":"sub","vis":"private","signature":"Private Sub Compile(ByRef outBytecode() As Long, ByRef s As String, Optional ByVal caseInsensitive As Boolean = False)"}, {"name":"ComputeFirstChar","kind":"function","vis":"private","signature":"Private Function ComputeFirstChar( ByRef bytecode() As Long, ByRef outChar As Long, ByRef outRangesPc As Long, ByRef outIFlag As Long ) As Long","returnType":"Long"}, @@ -164,7 +167,7 @@ {"name":"CopyMemory","kind":"sub","vis":"private","declare":true,"signature":"Private Declare PtrSafe Sub CopyMemory Lib \"kernel32\" Alias \"RtlMoveMemory\" (Destination As Any, Source As Any, ByVal Length As LongPtr)"}, {"name":"DfsMatch","kind":"function","vis":"private","signature":"Private Function DfsMatch( ByRef outCaptures As CapturesTy, ByRef bytecode() As Long, ByRef inputStr As String, Optional ByVal stepsLimit As Long = DEFAULT_STEPS_LIMIT, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Long","returnType":"Long"}, {"name":"DfsMatchFrom","kind":"function","vis":"private","signature":"Private Function DfsMatchFrom( ByRef context As DfsMatcherContext, ByRef outCaptures As CapturesTy, ByRef bytecode() As Long, ByRef inputStr As String, ByVal sp As Long, Optional ByVal stepsLimit As Long = DEFAULT_STEPS_LIMIT, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Long","returnType":"Long"}, - {"name":"DfsRunThreads","kind":"function","vis":"private","signature":"Private Function DfsRunThreads( ByRef outCaptures As CapturesTy, ByRef context As DfsMatcherContext, ByRef bytecode() As Long, ByRef inputStr As String, ByVal sp As Long, ByVal stepsLimit As Long, ByVal multiline As Boolean, ByVal dotAll As Boolean ) As Long","returnType":"Long"}, + {"name":"DfsRunThreads","kind":"function","vis":"private","signature":"Private Function DfsRunThreads( ByRef outCaptures As CapturesTy, ByRef context As DfsMatcherContext, ByRef bytecode() As Long, ByRef inputStr As String, ByVal sp As Long, ByVal multiline As Boolean, ByVal dotAll As Boolean ) As Long","returnType":"Long"}, {"name":"EmitPredefinedRange","kind":"sub","vis":"private","signature":"Private Sub EmitPredefinedRange( ByRef outBuffer As ArrayBuffer, ByRef source() As Long, ByVal sourceStart As Long, ByVal sourceLength As Long )"}, {"name":"GetBc","kind":"function","vis":"private","signature":"Private Function GetBc(ByRef bytecode() As Long, ByRef pc As Long) As Long","returnType":"Long"}, {"name":"GetCapture","kind":"function","vis":"public","signature":"Public Function GetCapture(ByRef matcherState As MatcherStateTy, ByRef haystack As String, Optional ByVal num As Long = 0) As String","returnType":"String"}, @@ -226,6 +229,7 @@ {"name":"SetMinimumCapacity","kind":"sub","vis":"private","signature":"Private Sub SetMinimumCapacity(ByRef sb As StaticStringBuilder, ByVal MinimumCapacity As Long)"}, {"name":"SplitByRegex","kind":"function","vis":"public","signature":"Public Function SplitByRegex( ByRef regex As RegexTy, ByRef haystack As String, Optional ByVal localMatch As Boolean = False, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Collection","returnType":"Collection"}, {"name":"StaticData","kind":"field","vis":"private","signature":"Private StaticData(0 To STATIC_DATA_LENGTH - 1) As Long"}, + {"name":"StepsLimitHit","kind":"function","vis":"public","signature":"Public Function StepsLimitHit() As Boolean","returnType":"Boolean"}, {"name":"SwitchToLargerBuffer","kind":"sub","vis":"private","signature":"Private Sub SwitchToLargerBuffer(ByRef sb As StaticStringBuilder, ByVal nRequired As Long)"}, {"name":"Test","kind":"function","vis":"public","signature":"Public Function Test( ByRef regex As RegexTy, ByRef str As String, Optional ByVal multiline As Boolean = False, Optional ByVal dotAll As Boolean = False ) As Boolean","returnType":"Boolean"}, {"name":"TryInitializeRegex","kind":"function","vis":"public","signature":"Public Function TryInitializeRegex( ByRef regex As RegexTy, ByRef pattern As String, Optional ByVal caseInsensitive As Boolean = False ) As Boolean","returnType":"Boolean"}, @@ -238,7 +242,10 @@ {"name":"astTableInitialized","kind":"field","vis":"private","signature":"Private astTableInitialized As Boolean"}, {"name":"isCaseInsensitive","kind":"function","vis":"private","signature":"Private Function isCaseInsensitive(ByRef bytecode() As Long) As Boolean","returnType":"Boolean"}, {"name":"mInputChars","kind":"field","vis":"private","signature":"Private mInputChars() As Integer"}, - {"name":"mInputLen","kind":"field","vis":"private","signature":"Private mInputLen As Long"} + {"name":"mInputLen","kind":"field","vis":"private","signature":"Private mInputLen As Long"}, + {"name":"mStepsBudget","kind":"field","vis":"private","signature":"Private mStepsBudget As Long"}, + {"name":"mStepsLimitHit","kind":"field","vis":"private","signature":"Private mStepsLimitHit As Boolean"}, + {"name":"mStepsUsed","kind":"field","vis":"private","signature":"Private mStepsUsed As Long"} ] }, { @@ -372,7 +379,9 @@ {"name":"nNamedCaptures","vis":"public","signature":"nNamedCaptures As Long"}, {"name":"entireMatch","vis":"public","signature":"entireMatch As StartLengthPair"}, {"name":"numberedCaptures","vis":"public","signature":"numberedCaptures() As StartLengthPair"}, - {"name":"namedCaptures","vis":"public","signature":"namedCaptures() As Long"} + {"name":"namedCaptures","vis":"public","signature":"namedCaptures() As Long"}, + {"name":"allocNumbered","vis":"public","signature":"allocNumbered As Long"}, + {"name":"allocNamed","vis":"public","signature":"allocNamed As Long"} ], "members": [] }, @@ -420,7 +429,10 @@ "file": "StaticRegex.twin", "signature": "Private Enum DfsMatcherSharedConstant", "values": [ - {"name":"DEFAULT_STEPS_LIMIT","value":"10000"} + {"name":"DEFAULT_STEPS_LIMIT","value":"0"}, + {"name":"STEPS_BUDGET_PER_CHAR","value":"5000"}, + {"name":"STEPS_BUDGET_FLOOR","value":"1000000"}, + {"name":"STEPS_BUDGET_CAP","value":"50000000"} ] }, { @@ -531,7 +543,6 @@ {"name":"UNICODE_0","value":"48"}, {"name":"UNICODE_1","value":"49"}, {"name":"UNICODE_7","value":"55"}, - {"name":"UNICODE_8","value":"56"}, {"name":"UNICODE_9","value":"57"}, {"name":"UNICODE_COLON","value":"58"}, {"name":"UNICODE_LT","value":"60"}, diff --git a/test/example-projects/contributed-tbRegExp/Settings b/test/example-projects/contributed-tbRegExp/Settings index e4cd86dbc..14c87e3b7 100644 --- a/test/example-projects/contributed-tbRegExp/Settings +++ b/test/example-projects/contributed-tbRegExp/Settings @@ -57,7 +57,7 @@ "symbolId": "tbRegExp", "versionBuild": "0", "versionMajor": "1", - "versionMinor": "7", + "versionMinor": "8", "versionRevision": "0" } ], From 91b596d73d69582a516cb9be9552a9d51ac833d3 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober <kuba@mareimbrium.org> Date: Wed, 14 Oct 2026 10:25:33 +0200 Subject: [PATCH 04/13] tbFileSysTools: moved to 1.9.0.0: GetSpecialFolderPath, Folders.Add, GetSpecialFolder returns a Folder; Item raises 53, 76 and 68, GetFileVersion 53 for a missing file, writes to a read-only file raise 70 (its WARNING gone), DeleteFolder raises 70 as FSO does --- .../Contributed/tbFileSysTools/Drive.md | 4 +- .../Contributed/tbFileSysTools/Drives.md | 8 +- .../Contributed/tbFileSysTools/File.md | 2 +- .../tbFileSysTools/FileSystemObject.md | 17 +-- .../tbFileSysTools/FileSystemTools.md | 109 ++++++++++++------ .../Contributed/tbFileSysTools/Files.md | 8 +- .../Contributed/tbFileSysTools/Folder.md | 6 +- .../Contributed/tbFileSysTools/Folders.md | 50 +++++++- .../Contributed/tbFileSysTools/TextStream.md | 2 +- .../Contributed/tbFileSysTools/index.md | 36 +++--- .../contributed/tbFileSysTools/api.json | 23 ++-- .../contributed-tbFileSysTools/Settings | 2 +- 12 files changed, 179 insertions(+), 88 deletions(-) diff --git a/docs/Reference/Contributed/tbFileSysTools/Drive.md b/docs/Reference/Contributed/tbFileSysTools/Drive.md index 966d47ddd..ecc823219 100644 --- a/docs/Reference/Contributed/tbFileSysTools/Drive.md +++ b/docs/Reference/Contributed/tbFileSysTools/Drive.md @@ -14,8 +14,8 @@ A **Drive** is not created with **New**. [**GetDrive**](FileSystemTools#getdrive ```tb check_build check_run Dim d As Drive -Set d = GetDrive(Left$(GetSpecialFolder(sfWindowsFolder), 2)) -Debug.Print d.Path = Left$(GetSpecialFolder(sfWindowsFolder), 2) ' True +Set d = GetDrive(Left$(GetSpecialFolderPath(sfWindowsFolder), 2)) +Debug.Print d.Path = Left$(GetSpecialFolderPath(sfWindowsFolder), 2) ' True Debug.Print d.IsReady ' True Debug.Print d.DriveType = DriveTypeConst.Fixed ' True Debug.Print d.TotalSize > d.FreeSpace ' True diff --git a/docs/Reference/Contributed/tbFileSysTools/Drives.md b/docs/Reference/Contributed/tbFileSysTools/Drives.md index 96682097f..d89ef10f9 100644 --- a/docs/Reference/Contributed/tbFileSysTools/Drives.md +++ b/docs/Reference/Contributed/tbFileSysTools/Drives.md @@ -37,10 +37,10 @@ Debug.Print ds("C:\").Path ' C: On Error Resume Next Set d = ds("1") -Debug.Print Err.Number ' 5 +Debug.Print Err.Number ' 68 Err.Clear Set d = ds(0) -Debug.Print Err.Number ' 5 +Debug.Print Err.Number ' 68 Err.Clear On Error GoTo 0 ``` @@ -64,9 +64,9 @@ Syntax: *object*.**Item** ( *letterOrIndex* ) **As Drive** *letterOrIndex* : *required* A **Variant**: a drive letter such as `"C"`, or the position of the drive, counted from 1. A string is read by its first character, so `"C"`, `"c"`, `"C:"` and `"C:\"` all name the drive C. -The drives are in the order of their letters, and the letter is not case-sensitive. A letter that is not in the collection, and a position below 1 or above [**Count**](#count), raise error 5 (*Invalid procedure call or argument*), with the message *No drive 'x' in the collection*. +The drives are in the order of their letters, and the letter is not case-sensitive. A letter that is not in the collection, and a position below 1 or above [**Count**](#count), raise error 68 (*Device unavailable*), with the message *No drive 'x' in the collection*. It is the number that [**GetDrive**](FileSystemTools#getdrive) raises for a drive that does not exist. -The Scripting Runtime raises error 68 (*Device unavailable*) for a letter that is not in use, and error 5 for a position. +The Scripting Runtime raises error 68 for a letter that is not in use, and error 5 for a position. ### NewEnum {: .no_toc } diff --git a/docs/Reference/Contributed/tbFileSysTools/File.md b/docs/Reference/Contributed/tbFileSysTools/File.md index a0580ef33..af2cb391d 100644 --- a/docs/Reference/Contributed/tbFileSysTools/File.md +++ b/docs/Reference/Contributed/tbFileSysTools/File.md @@ -361,7 +361,7 @@ Syntax: *object*.**SetAttribute** ( *attr*, *state* ) : *required* A **Boolean**. **True** sets the attribute and **False** clears it. The other attributes stay as they are. > [!WARNING] -> In version 1.8.0.0, **SetAttribute** sets nothing, and raises no error, on a file that has no attribute at all, because [**Attributes**](#attributes) then reports **faNormal**, and **faNormal** wins over the attribute being set. A new file has the archive attribute, so it is a file from which the program cleared every attribute that this applies to. To give such a file an attribute, assign [**Attributes**](#attributes). The same method of a [**Folder**](Folder#setattribute) is not affected. +> In version 1.9.0.0, **SetAttribute** sets nothing, and raises no error, on a file that has no attribute at all, because [**Attributes**](#attributes) then reports **faNormal**, and **faNormal** wins over the attribute being set. A new file has the archive attribute, so it is a file from which the program cleared every attribute that this applies to. To give such a file an attribute, assign [**Attributes**](#attributes). The same method of a [**Folder**](Folder#setattribute) is not affected. ```tb check_build check_run Dim work As String, f As File diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemObject.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemObject.md index f7148baeb..ee254fe1f 100644 --- a/docs/Reference/Contributed/tbFileSysTools/FileSystemObject.md +++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemObject.md @@ -41,16 +41,10 @@ fso.DeleteFolder work Replace `CreateObject("Scripting.FileSystemObject")` with `New FileSystemObject`, and check these calls. The package differs from the Scripting Runtime in them, and a program compiles with the old code but does not do the same: -- [**GetSpecialFolder**](#getspecialfolder) returns the path as a **String**, not a **Folder**. Code that reads `.Path` from the result does not compile (the sample after this list). - The third argument of [**CreateTextFile**](#createtextfile) and of [**Folder.CreateTextFile**](Folder#createtextfile) is an encoding. The Scripting Runtime's *Unicode* **Boolean** **True** is -1 here, which writes UTF-8, and not UTF-16. The default is UTF-8 without a BOM, where the Scripting Runtime writes ANSI. - The fourth argument of [**OpenTextFile**](#opentextfile) and the second of [**File.OpenAsTextStream**](File#openastextstream) are an encoding, not a **Tristate**. **TristateTrue** is -1, which is **encAutoDetect**. - The other differences, such as the added members and the changed defaults, are listed under [Differences from the Scripting Runtime](./#differences-from-the-scripting-runtime). -```tb check_build expect-error=TB5027 -Dim fso As New FileSystemObject -Debug.Print fso.GetSpecialFolder(sfTemporaryFolder).Path -``` - ## Files and folders ### CopyFile @@ -223,9 +217,16 @@ See [**GetCurrentDir**](FileSystemTools#getcurrentdir). ### GetSpecialFolder {: .no_toc } -Syntax: *object*.**GetSpecialFolder** ( *specialFolder* ) **As String** +Syntax: *object*.**GetSpecialFolder** ( *specialFolder* ) **As Folder** + +Returns a [**Folder**](Folder) object, as the Scripting Runtime does. See [**GetSpecialFolder**](FileSystemTools#getspecialfolder). + +### GetSpecialFolderPath +{: .no_toc } + +Syntax: *object*.**GetSpecialFolderPath** ( *specialFolder* ) **As String** -Returns a path, not a **Folder** object as the Scripting Runtime does. See [**GetSpecialFolder**](FileSystemTools#getspecialfolder). +Returns the path as a **String**, for a folder that may not exist. The Scripting Runtime has no such member. See [**GetSpecialFolderPath**](FileSystemTools#getspecialfolderpath). ### SetCurrentDir {: .no_toc } diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md index af7100a24..e713cbc06 100644 --- a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md +++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md @@ -32,7 +32,7 @@ The procedures, by purpose: - Paths: [BuildPath](#buildpath), [CleanFileName](#cleanfilename), [GetAbsolutePathName](#getabsolutepathname), [GetBaseName](#getbasename), [GetDriveName](#getdrivename), [GetExtensionName](#getextensionname), [GetFileName](#getfilename), [GetParentFolderName](#getparentfoldername), [GetRelativePath](#getrelativepath), [GetTempName](#gettempname) - Existence: [DriveExists](#driveexists), [FileExists](#fileexists), [FolderExists](#folderexists) - Files and folders: [CopyFile](#copyfile), [CopyFolder](#copyfolder), [CreateFolder](#createfolder), [DeleteFile](#deletefile), [DeleteFolder](#deletefolder), [GetFilePaths](#getfilepaths), [MoveFile](#movefile), [MoveFolder](#movefolder), [Rename](#rename) -- Current directory and special folders: [GetCurrentDir](#getcurrentdir), [GetSpecialFolder](#getspecialfolder), [SetCurrentDir](#setcurrentdir) +- Current directory and special folders: [GetCurrentDir](#getcurrentdir), [GetSpecialFolder](#getspecialfolder), [GetSpecialFolderPath](#getspecialfolderpath), [SetCurrentDir](#setcurrentdir) - File information: [GetFileEncoding](#getfileencoding), [GetFileLineEnding](#getfilelineending), [GetFileType](#getfiletype), [GetFileVersion](#getfileversion), [IsFileLocked](#isfilelocked) - Objects: [GetDrive](#getdrive), [GetDrives](#getdrives), [GetFile](#getfile), [GetFolder](#getfolder) - Whole files: [ArrayToTextFile](#arraytotextfile), [MergeTextFiles](#mergetextfiles), [NormalizeTextFile](#normalizetextfile), [ReadStream](#readstream), [StringToTextFile](#stringtotextfile), [TextFileToArray](#textfiletoarray), [TextFileToString](#textfiletostring), [WriteStream](#writestream) @@ -346,7 +346,7 @@ Both paths are first made absolute with [**GetAbsolutePathName**](#getabsolutepa Returns the absolute *targetPath* when there is no relative path, for example when the two paths are on different drives, and when only *targetPath* is longer than 260 characters. > [!WARNING] -> In version 1.8.0.0, when *basePath* is 260 characters or longer, the result can be a relative path that leads to the wrong place, and no error is raised. For a target `x\y.txt` below such a base, the result starts with `..\` and the last name of the base, instead of being `x\y.txt`. A base path of 258 characters or fewer gives the right result. +> In version 1.9.0.0, when *basePath* is 260 characters or longer, the result can be a relative path that leads to the wrong place, and no error is raised. For a target `x\y.txt` below such a base, the result starts with `..\` and the last name of the base, instead of being `x\y.txt`. A base path of 258 characters or fewer gives the right result. ```tb check_build check_run Debug.Print GetRelativePath("C:\Projects\App", "C:\Projects\Lib\core.txt") ' ..\Lib\core.txt @@ -599,7 +599,7 @@ Syntax: **DeleteFolder** ( *folderPath* [, *force* [, *ignoreMissing* ] ] ) The contents are deleted first, and an item that cannot be deleted is skipped without an error. The folder itself is removed last; when it is not empty by then, the failure is reported. -Errors: 76 when no folder matches or a file is named; 75 when the folder is not empty because something in it could not be deleted, such as a read-only file when *force* is **False** (the message ends with the Windows code 145). The folder and the file stay. The Scripting Runtime raises error 70 in that case. +Errors: 76 when no folder matches or a file is named; 70 when the folder is not empty because something in it could not be deleted, such as a read-only file when *force* is **False**. The folder and that file stay. The Scripting Runtime raises error 70 in that case too. > [!NOTE] > A directory junction or symbolic link inside the folder is removed, and the folder it stands for keeps its files. The Scripting Runtime deletes the files in the target as well, so a tree that holds a link to a folder outside it loses the contents of that folder there. See [Junctions and symbolic links](#junctions-and-symbolic-links). @@ -742,22 +742,43 @@ Returns the path without a trailing backslash, except for a drive root (`C:\`). ### GetSpecialFolder {: .no_toc } -Returns the path of a well-known Windows folder. +Returns a [**Folder**](Folder) object for a well-known Windows folder. -Syntax: **GetSpecialFolder** ( *specialFolder* ) **As String** +Syntax: **GetSpecialFolder** ( *specialFolder* ) **As Folder** *specialFolder* : *required* A [**SpecialFolderConst**](#specialfolderconst) member. -Returns the path without a trailing backslash. **sfTemporaryFolder** (and its FSO name **TemporaryFolder**) returns the value of the `TEMP` environment variable; every other member is asked from Windows by its known-folder identifier. On Windows XP, which has no such identifiers, the older CSIDL numbers are used, and **sfDownloads** and **sfPublicDownloads**, which have none, raise error 5. Raises error 5 for a value that is not a member. A program built for 32 bits gets the `Program Files (x86)` folder for both **sfProgramFiles** and **sfProgramFilesX86**, and the `System32` path for **sfSystemFolder**; **sfSystemX86Folder** is `SysWOW64`. The letter case of the result can differ from that of the Scripting Runtime: `C:\WINDOWS` against `C:\Windows`. +Returns the object for the folder that [**GetSpecialFolderPath**](#getspecialfolderpath) names, as the Scripting Runtime does. The default member of a **Folder** is [**Path**](Folder#path), so the result can stand where a path is wanted: `s = GetSpecialFolder(sfDesktop)` and `BuildPath(GetSpecialFolder(sfDesktop), "list.txt")` both use the path. To get the path as a **String** explicitly, or the path of a folder that does not exist, call [**GetSpecialFolderPath**](#getspecialfolderpath). The **Path** of the object has the letter case that the file system stores for the last name of the folder, so it can differ from the result of **GetSpecialFolderPath**: `C:\Windows` against `C:\WINDOWS`. -> [!NOTE] -> Scripting.FileSystemObject returns a **Folder** object from **GetSpecialFolder**. This procedure returns the path as a **String**. Use [**GetFolder**](#getfolder) on the result to get the object. +Errors: 76 when the folder does not exist on this system, and for **sfTemporaryFolder** when the `TEMP` environment variable is empty or not set; 5 for a value that is not a member, and the others of [**GetSpecialFolderPath**](#getspecialfolderpath). + +```tb check_build check_run +Dim f As Folder +Set f = GetSpecialFolder(sfLocalAppData) +Debug.Print f.Name ' Local +Debug.Print f.ParentFolder.Name ' AppData +Debug.Print StrComp(GetSpecialFolder(sfWindowsFolder), Environ$("windir"), vbTextCompare) ' 0 +Debug.Print GetSpecialFolder(TemporaryFolder).Name = GetFolder(Environ$("TEMP")).Name ' True +``` + +### GetSpecialFolderPath +{: .no_toc } + +Returns the path of a well-known Windows folder as a **String**. + +Syntax: **GetSpecialFolderPath** ( *specialFolder* ) **As String** + +*specialFolder* +: *required* A [**SpecialFolderConst**](#specialfolderconst) member. + +Returns the path without a trailing backslash. The folder does not have to exist: the procedure does not look at the disk. **sfTemporaryFolder** (and its FSO name **TemporaryFolder**) returns the value of the `TEMP` environment variable; every other member is asked from Windows by its known-folder identifier. On Windows XP, which has no such identifiers, the older CSIDL numbers are used, and **sfDownloads** and **sfPublicDownloads**, which have none, raise error 5. Raises error 5 for a value that is not a member. A program built for 32 bits gets the `Program Files (x86)` folder for both **sfProgramFiles** and **sfProgramFilesX86**, and the `System32` path for **sfSystemFolder**; **sfSystemX86Folder** is `SysWOW64`. The letter case of the result is that of Windows, and can differ from that of the Scripting Runtime: `C:\WINDOWS` against `C:\Windows`. ```tb check_build check_run -Debug.Print StrComp(GetSpecialFolder(sfWindowsFolder), Environ$("windir"), vbTextCompare) ' 0 -Debug.Print GetSpecialFolder(TemporaryFolder) = Environ$("TEMP") ' True -Debug.Print FolderExists(GetSpecialFolder(sfLocalAppData)) ' True +Debug.Print StrComp(GetSpecialFolderPath(sfWindowsFolder), Environ$("windir"), vbTextCompare) ' 0 +Debug.Print GetSpecialFolderPath(TemporaryFolder) = Environ$("TEMP") ' True +Debug.Print FolderExists(GetSpecialFolderPath(sfLocalAppData)) ' True +Debug.Print Right$(GetSpecialFolderPath(sfLocalAppData), 1) <> "\" ' True ``` ### SetCurrentDir @@ -876,13 +897,24 @@ Syntax: **GetFileVersion** ( *filePath* [, *source* ] ) **As String** *source* : *optional* A [**FileVersionSource**](#fileversionsource) member. Default: **fvFixed**. -Returns the binary version as four numbers separated by dots, such as `10.0.19041.7725`, with **fvFixed**, the same string as the Scripting Runtime. **fvFileString** returns the `FileVersion` text that the author of the file wrote into it, and **fvProductString** the `ProductVersion` text; both return the binary version when the file has no such text. Returns an empty string when the file has no version resource, and when the file does not exist. The Scripting Runtime raises an error for a file that does not exist (the Windows code `0x80070002`, as the number -2147024894). +Returns the binary version as four numbers separated by dots, such as `10.0.19041.7725`, with **fvFixed**, the same string as the Scripting Runtime. **fvFileString** returns the `FileVersion` text that the author of the file wrote into it, and **fvProductString** the `ProductVersion` text; both return the binary version when the file has no such text. Returns an empty string when the file has no version resource, such as a `.txt` file. Raises error 53 when the file does not exist, and for a folder or an empty path. The Scripting Runtime raises an error in that case too, with the Windows code `0x80070002` as its number (-2147024894) and not 53. ```tb check_build check_run Dim kernel As String -kernel = BuildPath(GetSpecialFolder(sfSystemFolder), "kernel32.dll") +kernel = BuildPath(GetSpecialFolderPath(sfSystemFolder), "kernel32.dll") Debug.Print Len(GetFileVersion(kernel)) > 0 ' True Debug.Print Len(GetFileVersion(kernel, fvFileString)) > 0 ' True + +Dim work As String, v As String +work = BuildPath(Environ$("TEMP"), GetTempName("fst", "")) +CreateFolder work +StringToTextFile "text", BuildPath(work, "a.txt") +Debug.Print GetFileVersion(BuildPath(work, "a.txt")) = "" ' True +On Error Resume Next +v = GetFileVersion(BuildPath(work, "missing.exe")) +Debug.Print Err.Number ' 53 +On Error GoTo 0 +DeleteFolder work ``` ### IsFileLocked @@ -993,7 +1025,7 @@ Syntax: **ArrayToTextFile** ( *lines*(), *filePath* [, *encoding* [, *lineEnding *lineEnding* : *optional* A [**NewlineType**](#newlinetype) member. Default: **nlWindows**. **nlNoChange** is read as **nlWindows**, because a line ending is needed to join the lines. -The file is written as [**WriteStream**](#writestream) writes it, read-only files included; see the warning there. [**TextFileToArray**](#textfiletoarray) reads the result back into the same array. An array that is not dimensioned writes an empty file. +The file is written as [**WriteStream**](#writestream) writes it, so an existing read-only file raises error 70. [**TextFileToArray**](#textfiletoarray) reads the result back into the same array. An array that is not dimensioned writes an empty file. Errors: 52 when the last component of *filePath* holds `<`, `>`, `:`, `"`, `|`, `?`, `*` or a control character, or is a reserved device name such as `con` (see [**CleanFileName**](#cleanfilename)); the errors of [**WriteStream**](#writestream). @@ -1072,7 +1104,7 @@ Syntax: **NormalizeTextFile** ( *filePath* [, *toEncoding* [, *toNewLine* [, *fr The file is left alone when the text would be written back with exactly the bytes it has. Returns **True** when the file was changed, a change of attributes alone included, and **False** when it was left as it was. A second call with the same arguments returns **False**. -A binary file is refused with error 5; this includes a file that *fromEncoding* names when it is not UTF-16 or UTF-32 and holds binary data. An empty file stays empty, or becomes a BOM alone when *toEncoding* has one. The new file is written beside the old one and swapped in, as [**WriteStream**](#writestream) does. Raises error 53 when the file does not exist. +A binary file is refused with error 5; this includes a file that *fromEncoding* names when it is not UTF-16 or UTF-32 and holds binary data. An empty file stays empty, or becomes a BOM alone when *toEncoding* has one. The new file is written beside the old one and swapped in, as [**WriteStream**](#writestream) does. Unlike **WriteStream**, it rewrites a read-only file, because the call names the file and asks for the change; the file is read-only again afterwards, unless *toAttributes* says otherwise, and also when the rewrite fails. Raises error 53 when the file does not exist. ```tb check_build check_run Dim work As String @@ -1093,6 +1125,10 @@ Debug.Print NormalizeTextFile(path, , nlUnix) ' True Debug.Print GetAttr(path) ' 2 Debug.Print NormalizeTextFile(path, , nlUnix, , faReadOnly) ' True Debug.Print GetAttr(path) ' 1 + +' A read-only file is rewritten and stays read-only. +Debug.Print NormalizeTextFile(path, , nlWindows) ' True +Debug.Print GetAttr(path) ' 1 SetAttr path, vbNormal ' A binary file is refused. @@ -1147,7 +1183,7 @@ Syntax: **StringToTextFile** ( *text*, *filePath* [, *encoding* [, *outLineEndin *outLineEnding* : *optional* A [**NewlineType**](#newlinetype) member. A style other than **nlNoChange** converts every line ending in *text*, CR LF, LF or CR, to it. Default: **nlNoChange**, which writes *text* as it is. -An empty *text* creates an empty file, or one that holds only the BOM. The file is written as [**WriteStream**](#writestream) writes it, and an existing read-only file is replaced without an error; see the warning there. +An empty *text* creates an empty file, or one that holds only the BOM. The file is written as [**WriteStream**](#writestream) writes it, so an existing read-only file raises error 70. For a code page with a limited character set, a character that the code page does not have is replaced by the substitute character of that code page, without an error: the Greek letter pi written with **encLatin1** becomes the byte 63, a question mark. An unpaired surrogate in *text* raises error 5 when the encoding is UTF-8. @@ -1241,14 +1277,13 @@ Syntax: **WriteStream** ( *stream*(), *filePath* ) : *required* A **Byte** array. An empty array, or one that is not dimensioned, creates an empty file. *filePath* -: *required* A **String** naming the file. An existing file is replaced. +: *required* A **String** naming the file. An existing file is replaced, unless it is read-only. The bytes are first written to a new file in the same folder, `<name>.~XXXXXXXX.tmp`, which is then swapped in for the target. A failure part-way therefore leaves the old file whole. The folder must allow a new file to be created in it. -> [!WARNING] -> In version 1.8.0.0, **WriteStream**, [**StringToTextFile**](#stringtotextfile) and [**ArrayToTextFile**](#arraytotextfile) replace a read-only file without an error, and the new file is not read-only. The attributes of the old file are lost. [**CreateTextFile**](#createtextfile) and the Scripting Runtime's **CreateTextFile** raise error 70 for the same file. +A read-only file is not replaced, and nothing is written. [**StringToTextFile**](#stringtotextfile) and [**ArrayToTextFile**](#arraytotextfile) write through **WriteStream** and refuse such a file in the same way, as [**CreateTextFile**](#createtextfile) and the Scripting Runtime's **CreateTextFile** do. To replace such a file on purpose, clear its attribute first with [**File.SetAttribute**](File#setattribute), or delete it with *force* **True**. [**NormalizeTextFile**](#normalizetextfile) is the exception: it rewrites a read-only file and keeps it read-only. -Errors: 76 when the folder does not exist, and 70 when Windows refuses, such as for a file that is in use. A failure to write the new file raises the error of the runtime, such as 61 for a full disk; see [Errors](./#errors) for the other Windows failures. +Errors: 76 when the folder does not exist, and 70 when the file is read-only or when Windows refuses, such as for a file that is in use. A failure to write the new file raises the error of the runtime, such as 61 for a full disk; see [Errors](./#errors) for the other Windows failures. ```tb check_build check_run Dim work As String, path As String, ts As TextStream, b() As Byte, lines() As String @@ -1261,23 +1296,31 @@ lines = Split("a,b", ",") StringToTextFile "old", path SetAttr path, vbReadOnly -StringToTextFile "new", path ' no error -Debug.Print TextFileToString(path) ' new -Debug.Print GetAttr(path) And vbReadOnly ' 0 -SetAttr path, vbReadOnly +' A read-only file is refused by all four, and keeps its text and its attribute. +On Error Resume Next +StringToTextFile "new", path +Debug.Print Err.Number ' 70 +Err.Clear ArrayToTextFile lines, path -Debug.Print GetAttr(path) And vbReadOnly ' 0 - -SetAttr path, vbReadOnly +Debug.Print Err.Number ' 70 +Err.Clear WriteStream b, path -Debug.Print GetAttr(path) And vbReadOnly ' 0 - -SetAttr path, vbReadOnly -On Error Resume Next +Debug.Print Err.Number ' 70 +Err.Clear Set ts = CreateTextFile(path) Debug.Print Err.Number ' 70 Err.Clear +On Error GoTo 0 +Debug.Print TextFileToString(path) ' old +Debug.Print GetAttr(path) And vbReadOnly ' 1 + +' With the attribute cleared, the file is replaced. +GetFile(path).SetAttribute faReadOnly, False +WriteStream b, path +Debug.Print TextFileToString(path) ' A + +On Error Resume Next WriteStream b, BuildPath(work, "missing\x.bin") Debug.Print Err.Number ' 76 Err.Clear @@ -1455,7 +1498,7 @@ Syntax: **OpenTextFile** ( *filePath* [, *ioMode* [, *create* [, *encoding* [, * What the mode does: - **ForReading** requires the file. The encoding is found from the start of the file: a BOM first, then *encoding* when it names one, then the same guess as [**GetFileEncoding**](#getfileencoding). A file that looks binary is refused with error 5 when the encoding is detected. An empty file is reported as UTF-8. -- **ForWriting** empties the file when it is opened, for an encoding that is written in pieces; a stream that holds the whole file (UTF-7) replaces the file when it is closed or flushed. *encoding* gives the encoding, and **encAutoDetect** means UTF-8. A BOM is written for a member with **Bom** in its name. While the stream is open, other streams can read the file, and cannot open it for writing or appending (error 70). A stream open for reading does not stop another from opening the file for writing. +- **ForWriting** empties the file when it is opened, for an encoding that is written in pieces; a stream that holds the whole file (UTF-7) replaces the file when it is closed or flushed. An existing read-only file raises error 70 when the stream is opened, except for UTF-7, where the error comes at the first flush or close and the file keeps its text. *encoding* gives the encoding, and **encAutoDetect** means UTF-8. A BOM is written for a member with **Bom** in its name. While the stream is open, other streams can read the file, and cannot open it for writing or appending (error 70). A stream open for reading does not stop another from opening the file for writing. - **ForAppending** adds to the end. A file that exists keeps its encoding and BOM, found as for reading: *encoding* is used only when the file has no BOM, and is not checked against the text in the file, so a wrong one mixes encodings in the file. A BOM is not written again, and none is written to an existing file that is empty. The line ending for **WriteLine** is the one the first 64 KB of the file use, and *writeModeLineEnding* is used only for an empty file, a file without a line ending and a file with mixed ones. A file that does not exist is created like a new file under **ForWriting**, with the BOM of the encoding. Errors: 53 when the file does not exist for **ForReading**, or for the other modes with *create* **False**; 76 when the folder does not exist; 52 for a name that is not valid when *create* is **True**; 5 for an unknown *ioMode*, and for a binary file as above; 70 when another stream has the file open for writing. The error has an empty **Err.Source** and the standard text of its number, `File not found` for 53, without the path. @@ -1562,7 +1605,7 @@ How [**OpenTextFile**](#opentextfile) opens a file. The values are those of the ### SpecialFolderConst {: .no_toc } -A well-known folder, for [**GetSpecialFolder**](#getspecialfolder). The first three members have the values of the Scripting Runtime, and **WindowsFolder**, **SystemFolder** and **TemporaryFolder** are names for them that match it. The rest are known folders of Windows. +A well-known folder, for [**GetSpecialFolder**](#getspecialfolder) and [**GetSpecialFolderPath**](#getspecialfolderpath). The first three members have the values of the Scripting Runtime, and **WindowsFolder**, **SystemFolder** and **TemporaryFolder** are names for them that match it. The rest are known folders of Windows. | Member | Value | Folder | |---|---|---| diff --git a/docs/Reference/Contributed/tbFileSysTools/Files.md b/docs/Reference/Contributed/tbFileSysTools/Files.md index b326171fa..72073113c 100644 --- a/docs/Reference/Contributed/tbFileSysTools/Files.md +++ b/docs/Reference/Contributed/tbFileSysTools/Files.md @@ -47,10 +47,10 @@ Set fs = GetFolder(work).Files On Error Resume Next Set f = fs("missing.txt") -Debug.Print Err.Number ' 5 +Debug.Print Err.Number ' 53 Err.Clear Set f = fs(2) -Debug.Print Err.Number ' 5 +Debug.Print Err.Number ' 53 Err.Clear On Error GoTo 0 @@ -76,9 +76,9 @@ Syntax: *object*.**Item** ( *nameOrIndex* ) **As File** *nameOrIndex* : *required* A **Variant**: the name of the file with its extension, such as `"readme.txt"`, in any letter case, or the position of the file, counted from 1. -The files are in the order in which Windows lists the folder. A name that is not in the collection, and a position below 1 or above [**Count**](#count), raise error 5 (*Invalid procedure call or argument*), with the message *No item 'x' in the collection*. +The files are in the order in which Windows lists the folder. A name that is not in the collection, and a position below 1 or above [**Count**](#count), raise error 53 (*File not found*), with the message *No file 'x' in the collection*. -The Scripting Runtime takes a name only: it raises error 53 (*File not found*) for a name that is not in the collection, and error 5 for a position. +The Scripting Runtime takes a name only: it raises error 53 for a name that is not in the collection, and error 5 for a position. ### NewEnum {: .no_toc } diff --git a/docs/Reference/Contributed/tbFileSysTools/Folder.md b/docs/Reference/Contributed/tbFileSysTools/Folder.md index a05c0eaa2..036de88bc 100644 --- a/docs/Reference/Contributed/tbFileSysTools/Folder.md +++ b/docs/Reference/Contributed/tbFileSysTools/Folder.md @@ -43,7 +43,7 @@ A **Folder** keeps only the path of the folder. Each member that reads a propert The price is that every read costs a call to Windows. Read a value once and keep it. [**Size**](#size) costs the most: it goes through the whole tree on every read. -[**Files**](#files) and [**SubFolders**](#subfolders) return the members of the folder at the moment of the call. The collection does not follow later changes, so the property is read again to see them. The objects in it are live, as every **File** and **Folder** is. +[**Files**](#files) and [**SubFolders**](#subfolders) return the members of the folder at the moment of the call. The collection does not follow later changes, so the property is read again to see them. The one change a collection follows is a folder that [**Folders.Add**](Folders#add) creates through it. The objects in it are live, as every **File** and **Folder** is. The members that change the folder ([**Move**](#move), [**Rename**](#rename), [**Name**](#name) and [**Delete**](#delete)) are those of [**FileSystemTools**](FileSystemTools), with the object's path as the source: their errors are described there. **Move** and **Rename** point the object at the new path, so it stays usable. [**Copy**](#copy) leaves the object as it is and does not return the copy. After **Delete**, every member that reads raises error 76. @@ -106,7 +106,7 @@ Syntax: *object*.**Files** **As Files** Returns a [**Files**](Files) collection of the files directly in the folder. Hidden and system files are in it, which differs from [**GetFilePaths**](FileSystemTools#getfilepaths). The folder is read when the property is read, and the collection does not change afterwards. Raises error 70 when the folder cannot be read, as the Scripting Runtime does. > [!NOTE] -> In version 1.8.0.0, **Files**, [**SubFolders**](#subfolders) and [**Size**](#size) raise error 76 (*Path not found*) for a folder whose path has 258 characters or more, although the folder exists and its [**Name**](#name) can be read. The same members work for a folder with 256 characters. +> In version 1.9.0.0, **Files**, [**SubFolders**](#subfolders) and [**Size**](#size) raise error 76 (*Path not found*) for a folder whose path has 258 characters or more, although the folder exists and its [**Name**](#name) can be read. The same members work for a folder with 256 characters. ```tb check_build check_run Dim work As String, deep As String, n As Long @@ -211,7 +211,7 @@ The folders directly in the folder. Syntax: *object*.**SubFolders** **As Folders** -Returns a [**Folders**](Folders) collection of the folders directly below this one. Hidden and system folders are in it. The folder is read when the property is read, and the collection does not change afterwards. Raises error 70 when the folder cannot be read. +Returns a [**Folders**](Folders) collection of the folders directly below this one. Hidden and system folders are in it. The folder is read when the property is read, and the collection does not change afterwards, except that [**Folders.Add**](Folders#add) puts the folder it creates into it. Raises error 70 when the folder cannot be read. ### Type {: .no_toc } diff --git a/docs/Reference/Contributed/tbFileSysTools/Folders.md b/docs/Reference/Contributed/tbFileSysTools/Folders.md index b641e3dbd..40782588a 100644 --- a/docs/Reference/Contributed/tbFileSysTools/Folders.md +++ b/docs/Reference/Contributed/tbFileSysTools/Folders.md @@ -8,9 +8,9 @@ has_toc: false # Folders class {: .no_toc } -A **Folders** is a read-only collection of [**Folder**](Folder) objects: the folders directly below one folder. It counts them, returns one by name or by position, and is enumerated with **For Each**. +A **Folders** is a collection of [**Folder**](Folder) objects: the folders directly below one folder. It counts them, returns one by name or by position, creates a new one, and is enumerated with **For Each**. -A **Folders** is not created with **New**. [**Folder.SubFolders**](Folder#subfolders) returns one. It holds the folders that existed when the property was read, and does not follow later changes. Each **Folder** in it reads the disk when it is used, so a folder that was deleted after the collection was made raises error 76 when it is touched. Hidden and system folders are in the collection. Only the first level is in it: to go deeper, read **SubFolders** of each item in turn. +A **Folders** is not created with **New**. [**Folder.SubFolders**](Folder#subfolders) returns one. It holds the folders that existed when the property was read, and does not follow later changes, except for the folders that [**Add**](#add) creates through it. Each **Folder** in it reads the disk when it is used, so a folder that was deleted after the collection was made raises error 76 when it is touched. Hidden and system folders are in the collection. Only the first level is in it: to go deeper, read **SubFolders** of each item in turn. ```tb check_build check_run Dim work As String, fs As Folders, fd As Folder @@ -52,9 +52,9 @@ Syntax: *object*.**Item** ( *nameOrIndex* ) **As Folder** *nameOrIndex* : *required* A **Variant**: the name of the folder, such as `"bin"`, in any letter case, or the position of the folder, counted from 1. -The folders are in the order in which Windows lists the folder that holds them. A name that is not in the collection, and a position below 1 or above [**Count**](#count), raise error 5 (*Invalid procedure call or argument*), with the message *No item 'x' in the collection*. +The folders are in the order in which Windows lists the folder that holds them, and a folder made by [**Add**](#add) comes last. A name that is not in the collection, and a position below 1 or above [**Count**](#count), raise error 76 (*Path not found*), with the message *No folder 'x' in the collection*. -The Scripting Runtime takes a name only: it raises error 76 (*Path not found*) for a name that is not in the collection, and error 5 for a position. It also has an **Add** method, which creates a folder; this collection has none, and [**CreateFolder**](FileSystemTools#createfolder) does that work. +The Scripting Runtime takes a name only: it raises error 76 for a name that is not in the collection, and error 5 for a position. ### NewEnum {: .no_toc } @@ -63,6 +63,48 @@ The enumerator that **For Each** uses. It is hidden, and is not called from code Syntax: *object*.**NewEnum** **As IUnknown** +## Methods + +### Add +{: .no_toc } + +Creates a subfolder and adds it to the collection. + +Syntax: *object*.**Add** ( *name* ) **As Folder** + +*name* +: *required* A **String**: the name of the new folder. It is a name and not a path: a backslash or a forward slash in it is error 5. + +Creates the folder below the folder that the collection was read from, appends it to the collection, and returns it. [**Count**](#count) and [**Item**](#item) show it at once, and **Item** finds it by *name*. Any other **Folders** that was read from the same folder is not changed; reading [**SubFolders**](Folder#subfolders) again includes the new folder. + +Errors: 58 when the folder already exists, and for an empty *name*, which names the folder that holds the collection; 5 when *name* holds a slash; 52 for a name with `<`, `>`, `"`, `|`, `?`, `*` or a control character; 75 for a reserved device name such as `con`. The numbers are those of [**CreateFolder**](FileSystemTools#createfolder), which does the work. The Scripting Runtime has this method too. It raises error 58 for an existing folder and error 5 for a path, as here, and also error 5 for an empty *name*. + +```tb check_build check_run +Dim work As String, fs As Folders, nf As Folder +work = BuildPath(Environ$("TEMP"), GetTempName("fst", "")) +CreateFolder work +Set fs = GetFolder(work).SubFolders +Debug.Print fs.Count ' 0 + +Set nf = fs.Add("reports") +Debug.Print nf.Name ' reports +Debug.Print FolderExists(BuildPath(work, "reports")) ' True +Debug.Print fs.Count ' 1 +Debug.Print fs("REPORTS").Path = nf.Path ' True +Debug.Print GetFolder(work).SubFolders.Count ' 1 + +On Error Resume Next +fs.Add "reports" +Debug.Print Err.Number ' 58 +Err.Clear +fs.Add "a\b" +Debug.Print Err.Number ' 5 +Err.Clear +On Error GoTo 0 + +DeleteFolder work +``` + ## See Also - [Folder](Folder#subfolders) class -- **SubFolders** returns this collection diff --git a/docs/Reference/Contributed/tbFileSysTools/TextStream.md b/docs/Reference/Contributed/tbFileSysTools/TextStream.md index deb8c5127..3ec0abec8 100644 --- a/docs/Reference/Contributed/tbFileSysTools/TextStream.md +++ b/docs/Reference/Contributed/tbFileSysTools/TextStream.md @@ -77,7 +77,7 @@ Debug.Print UBound(b) + 1 ' 10 DeleteFolder work ``` -**Two modes.** Most encodings, UTF-8 among them, are read and written in pieces of 64 KB, and [**IsStreaming**](#isstreaming) is **True**. UTF-7 cannot be cut into pieces. A stream in UTF-7 holds the whole file in memory and [**IsStreaming**](#isstreaming) is **False**. A stream of this kind that is open for writing does not empty the file when it is opened: the file is replaced when the stream is closed or flushed. Every member works in both modes. +**Two modes.** Most encodings, UTF-8 among them, are read and written in pieces of 64 KB, and [**IsStreaming**](#isstreaming) is **True**. UTF-7 cannot be cut into pieces. A stream in UTF-7 holds the whole file in memory and [**IsStreaming**](#isstreaming) is **False**. A stream of this kind that is open for writing does not empty the file when it is opened: the file is replaced when the stream is closed or flushed, and when the file is read-only that fails with error 70. Every member works in both modes. **Position.** [**Line**](#line) and [**Column**](#column) follow the characters that are read or written. Reading a line moves past its line ending. diff --git a/docs/Reference/Contributed/tbFileSysTools/index.md b/docs/Reference/Contributed/tbFileSysTools/index.md index 32fc6c79e..8014e7012 100644 --- a/docs/Reference/Contributed/tbFileSysTools/index.md +++ b/docs/Reference/Contributed/tbFileSysTools/index.md @@ -6,14 +6,14 @@ exclude_from_docs: - FSTShared - TextCodec - WinAPI -indexed_from: 1.8.0.0 +indexed_from: 1.9.0.0 permalink: /tB/Packages/Contributed/tbFileSysTools/ --- # tbFileSysTools Package {: .no_toc } -**Author:** GCuser99. **Version:** 1.8.0.0. **On GitHub:** [GCuser99/tbFileSysTools](https://github.com/GCuser99/tbFileSysTools), in the folder *Package*. +**Author:** GCuser99. **Version:** 1.9.0.0. **On GitHub:** [GCuser99/tbFileSysTools](https://github.com/GCuser99/tbFileSysTools), in the folder *Package*. The **tbFileSysTools** contributed package, listed in the Available Packages dialog as *File System Tools - Modern Replacement for FSO*, reads and changes files, folders and drives, as the **FileSystemObject** of the Scripting Runtime does, and is written in twinBASIC. It reads and writes text files in any Windows code page, with detection of the encoding and handling of byte-order marks (BOMs), detects and converts line endings, streams files of any size, and works with paths longer than 260 characters. @@ -61,35 +61,33 @@ A call that works with the Scripting Runtime's **FileSystemObject** keeps its na - Whole files: [**StringToTextFile**](FileSystemTools#stringtotextfile), [**TextFileToString**](FileSystemTools#textfiletostring), [**ArrayToTextFile**](FileSystemTools#arraytotextfile), [**TextFileToArray**](FileSystemTools#textfiletoarray), [**ReadStream**](FileSystemTools#readstream), [**WriteStream**](FileSystemTools#writestream), [**NormalizeTextFile**](FileSystemTools#normalizetextfile) and [**MergeTextFiles**](FileSystemTools#mergetextfiles). - Detection: [**GetFileEncoding**](FileSystemTools#getfileencoding) and [**GetFileLineEnding**](FileSystemTools#getfilelineending). - Searching and locks: [**FindInTextFiles**](FileSystemTools#findintextfiles), [**FileContainsText**](FileSystemTools#filecontainstext) and [**IsFileLocked**](FileSystemTools#isfilelocked). -- Paths and directories: [**CleanFileName**](FileSystemTools#cleanfilename), [**GetRelativePath**](FileSystemTools#getrelativepath), [**GetFilePaths**](FileSystemTools#getfilepaths), [**GetFileType**](FileSystemTools#getfiletype), [**GetCurrentDir**](FileSystemTools#getcurrentdir), [**SetCurrentDir**](FileSystemTools#setcurrentdir) and [**Rename**](FileSystemTools#rename). +- Paths and directories: [**CleanFileName**](FileSystemTools#cleanfilename), [**GetRelativePath**](FileSystemTools#getrelativepath), [**GetFilePaths**](FileSystemTools#getfilepaths), [**GetFileType**](FileSystemTools#getfiletype), [**GetSpecialFolderPath**](FileSystemTools#getspecialfolderpath), [**GetCurrentDir**](FileSystemTools#getcurrentdir), [**SetCurrentDir**](FileSystemTools#setcurrentdir) and [**Rename**](FileSystemTools#rename). - On [**File**](File): **ContainsText**, **Encoding**, **ExtensionName**, **HasAttribute**, **IsLocked**, **LineEnding**, **Normalize**, **Refresh**, **Rename**, **SetAttribute**, **ToStream**, **ToString** and **Version**. On [**Folder**](Folder): **HasAttribute**, **Refresh**, **Rename** and **SetAttribute**. On [**Drive**](Drive): **SerialNumberHex** and **UnsignedSerialNumber**. On [**TextStream**](TextStream): **Encoding**, **Flush** and **IsStreaming**. - [**Files**](Files), [**Folders**](Folders) and [**Drives**](Drives) return an item by position as well as by name. - Enumerations for the encodings, line endings and 25 special folders. **Members with more arguments.** [**CreateFolder**](FileSystemTools#createfolder) can create the folders above the new one. [**DeleteFile**](FileSystemTools#deletefile) and [**DeleteFolder**](FileSystemTools#deletefolder) can ignore a missing target. [**GetAbsolutePathName**](FileSystemTools#getabsolutepathname) takes a base directory. [**GetTempName**](FileSystemTools#gettempname) takes a prefix, an extension and a folder to check. -**Missing members.** One: the **Add** method of the **Folders** collection, which creates a folder. [**CreateFolder**](FileSystemTools#createfolder) does that. Every other member of the Scripting Runtime's **FileSystemObject**, **File**, **Folder**, **Drive** and **TextStream** is here, and so are **Count**, **Item** and the enumerator of the three collections. **Drives** of the class [**FileSystemObject**](FileSystemObject#drives) is a function, called with or without parentheses. +**Missing members.** None. Every member of the Scripting Runtime's **FileSystemObject**, **File**, **Folder**, **Drive** and **TextStream** is here, and so are **Count**, **Item** and the enumerator of the three collections, and the **Add** method of **Folders**. **Drives** of the class [**FileSystemObject**](FileSystemObject#drives) is a function, called with or without parentheses. **Arguments that mean something else.** The Scripting Runtime selects the encoding of a text file with a **Boolean** (*Unicode*, in **CreateTextFile**) or a **Tristate** (*Format*, in **OpenTextFile** and **OpenAsTextStream**). Here the argument is a [**TextEncoding**](FileSystemTools#textencoding). Code that passes **True** or **TristateTrue**, which is -1, gets **encAutoDetect**, and for writing that is UTF-8, not UTF-16. The defaults also differ: a text file is written in UTF-8 without a BOM, where the Scripting Runtime writes ANSI, and a file opened for reading has its encoding detected, where the Scripting Runtime reads ANSI. **Results that differ.** -- [**GetSpecialFolder**](FileSystemTools#getspecialfolder) returns the path as a **String**, where the Scripting Runtime returns a **Folder**. It accepts 25 folders, and not only the Windows, System and Temporary folders. +- [**GetSpecialFolder**](FileSystemTools#getspecialfolder) accepts 25 folders, and not only the Windows, System and Temporary folders. Like the Scripting Runtime's it returns a **Folder**; [**GetSpecialFolderPath**](FileSystemTools#getspecialfolderpath) returns the path as a **String**, for a folder that may not exist. - [**FileAttribute**](FileSystemTools#fileattribute) has no *Volume* (8) and no *Alias* (1024). The label of a volume is [**Drive.VolumeName**](Drive#volumename), and an alias is **faReparsePoint**. [**File**](File#setattribute) and [**Folder**](Folder#setattribute) add **SetAttribute** and **HasAttribute**, which change and test one attribute. - [**Folder.Size**](Folder#size), [**CopyFolder**](FileSystemTools#copyfolder), [**DeleteFolder**](FileSystemTools#deletefolder) and [**GetFilePaths**](FileSystemTools#getfilepaths) with *recursive* do not enter directory junctions and symbolic links. The Scripting Runtime does, and counts the target of a link twice when it is in the same tree: 2,500 bytes there for a folder that holds 1,500. Its **CopyFolder** copies the files of the target into the copy, and its **DeleteFolder** deletes them. - [**BuildPath**](FileSystemTools#buildpath) and [**GetParentFolderName**](FileSystemTools#getparentfoldername) turn `/` into `\`. `BuildPath("C:\a", "b/c")` returns `C:\a\b\c` and the Scripting Runtime `C:\a\b/c`; `GetParentFolderName("C:/a/b.txt")` returns `C:\a` and the Scripting Runtime `C:/a`. - [**GetAbsolutePathName**](FileSystemTools#getabsolutepathname) raises error 52 for an empty string, where the Scripting Runtime returns the current directory. - [**CopyFile**](FileSystemTools#copyfile), [**CopyFolder**](FileSystemTools#copyfolder), [**MoveFile**](FileSystemTools#movefile) and [**MoveFolder**](FileSystemTools#movefolder) put the source inside an existing destination folder, whether or not the destination ends in a backslash. The Scripting Runtime does so only with a backslash. Without one it raises error 70 for **CopyFile**, 58 for **MoveFile** and **MoveFolder**, and **CopyFolder** copies the contents of the source straight into the folder. - [**MoveFile**](FileSystemTools#movefile) moves a folder when *source* names one. The Scripting Runtime raises error 53. -- [**DeleteFolder**](FileSystemTools#deletefolder) of a folder that holds a read-only file raises error 75 without *force*; the Scripting Runtime raises 70. -- [**GetFileVersion**](FileSystemTools#getfileversion) returns an empty string for a file that does not exist. The Scripting Runtime raises an error. +- [**GetFileVersion**](FileSystemTools#getfileversion) raises error 53 for a file that does not exist. The Scripting Runtime raises the Windows code `0x80070002` as the error number. - At the end of a stream, [**ReadAll**](TextStream#readall) returns an empty string and [**Skip**](TextStream#skip) does nothing, where the Scripting Runtime raises error 62. A closed stream raises 52 here and 91 there. -- [**Files.Item**](Files#item), [**Folders.Item**](Folders#item) and [**Drives.Item**](Drives#item) take a position as well as a name. A name or a position that is not in the collection raises error 5; the Scripting Runtime raises 53, 76 and 68 for a name that is not in the three. +- [**Files.Item**](Files#item), [**Folders.Item**](Folders#item) and [**Drives.Item**](Drives#item) take a position as well as a name. A name or a position that is not in the collection raises 53, 76 and 68, the numbers that the Scripting Runtime raises for a name that is not in the three; for a position it raises error 5. - [**GetTempName**](FileSystemTools#gettempname) returns eight random letters and digits and `.tmp`. The Scripting Runtime returns `rad` and five hexadecimal digits. - A path longer than 260 characters works in most procedures. The Scripting Runtime does not accept it: **FileExists** and **FolderExists** return **False** for it, **GetFile** raises error 53 and **OpenTextFile** raises 76. -- [**StringToTextFile**](FileSystemTools#stringtotextfile), [**ArrayToTextFile**](FileSystemTools#arraytotextfile) and [**WriteStream**](FileSystemTools#writestream) replace a read-only file without an error. [**CreateTextFile**](FileSystemTools#createtextfile) raises error 70 for it, as the Scripting Runtime's does. -**The same.** For these failures the two raise the same error numbers: a missing file (53) or folder (76) from **GetFile**, **GetFolder**, **CopyFile**, **DeleteFile**, **DeleteFolder** and **OpenTextFile**; an existing folder or destination (58) from **CreateFolder**, **CopyFile**, **MoveFile** and **CreateTextFile**; a missing parent folder (76) from **CreateFolder**; a drive that does not exist (68) and a path that is longer than a drive (5) from **GetDrive**; a read-only destination or file (70) from **CopyFile**, **DeleteFile** and **CreateTextFile**; an unreadable folder (70) from **Files**, **SubFolders** and **Size**; the wrong direction of a stream (54) and a read at the end (62) from **TextStream**. Both match a pattern against the 8.3 name of a file in **CopyFile**, **DeleteFile** and **MoveFile**, and both accept `C:` as the item of **Drives**. +**The same.** For these failures the two raise the same error numbers: a missing file (53) or folder (76) from **GetFile**, **GetFolder**, **CopyFile**, **DeleteFile**, **DeleteFolder** and **OpenTextFile**; an existing folder or destination (58) from **CreateFolder**, **CopyFile**, **MoveFile** and **CreateTextFile**; a missing parent folder (76) from **CreateFolder**; a drive that does not exist (68) and a path that is longer than a drive (5) from **GetDrive**; a read-only destination or file (70) from **CopyFile**, **DeleteFile** and **CreateTextFile**, and a folder with a read-only file in it (70) from **DeleteFolder**; an unreadable folder (70) from **Files**, **SubFolders** and **Size**; the wrong direction of a stream (54) and a read at the end (62) from **TextStream**. Both match a pattern against the 8.3 name of a file in **CopyFile**, **DeleteFile** and **MoveFile**, and both accept `C:` as the item of **Drives**. ## Using the package with the Scripting Runtime @@ -100,7 +98,7 @@ Write the name of the library before the type to be sure which one is meant. Bot ```tb check_build Dim fso As New tbFileSysTools.FileSystemObject Dim f As tbFileSysTools.File -Set f = fso.GetFile(fso.BuildPath(fso.GetSpecialFolder(sfSystemFolder), "kernel32.dll")) +Set f = fso.GetFile(fso.BuildPath(fso.GetSpecialFolderPath(sfSystemFolder), "kernel32.dll")) Debug.Print f.Size ``` @@ -110,18 +108,18 @@ The other ways out are to remove the reference to the Scripting Runtime, or to l The procedures raise the error numbers that VB6 uses for the same failures. **Err.Source** names the operation, such as `CopyFile`, and **Err.Description** names the path. The errors of [**OpenTextFile**](FileSystemTools#opentextfile), [**CreateTextFile**](FileSystemTools#createtextfile) and the other procedures that open a stream have an empty **Err.Source** and only the standard text of their number, without the path. Each entry says which numbers it raises. -- 5 -- *Invalid procedure call or argument*: a wildcard in a destination, a folder copied into itself, a text procedure given a binary file, a value that is not a member of an enumeration, a name or a position that is not in a collection, a string that is not a drive, and an empty search text. +- 5 -- *Invalid procedure call or argument*: a wildcard in a destination, a folder copied into itself, a text procedure given a binary file, a value that is not a member of an enumeration, a path given to [**Folders.Add**](Folders#add) as a name, a string that is not a drive, and an empty search text. - 7 -- *Out of memory*: a whole file that is too large for one array or **String** (more than 2 GB). - 52 -- *Bad file name or number*: a name with a character that Windows does not allow, or a reserved device name such as `con`, and a closed [**TextStream**](TextStream). -- 53 -- *File not found*. +- 53 -- *File not found*, and a name or a position that is not in a [**Files**](Files). - 54 -- *Bad file mode*: a read on a stream open for writing, or the reverse. - 58 -- *File already exists*. The text says *File* also when a folder is in the way. - 62 -- *Input past end of file*. -- 68 -- *Device unavailable*: a drive letter that is not in use. -- 70 -- *Permission denied*: access is refused, or the file is in use. +- 68 -- *Device unavailable*: a drive letter that is not in use, and a name or a position that is not in a [**Drives**](Drives). +- 70 -- *Permission denied*: access is refused, or the file is in use, or a read-only file is to be replaced or a folder holds one that stops it from being deleted. - 71 -- *Disk not ready*: a drive with no media. -- 75 -- *Path/File access error*: any other Windows failure. The text ends with the Windows code, such as 145 for a folder that is not empty and 206 for a path that is too long. -- 76 -- *Path not found*. +- 75 -- *Path/File access error*: any other Windows failure. The text ends with the Windows code, such as 206 for a path that is too long. +- 76 -- *Path not found*, and a name or a position that is not in a [**Folders**](Folders). Windows reports a failure with its own code, and the package maps it to one of those numbers: *file not found* (2) to 53, *path not found* (3) and *bad directory name* (267) to 76, *file exists* (80 and 183) to 58, *access denied* and the sharing violations (5, 32 and 33) to 70, and *invalid name* (123) to 52. @@ -200,7 +198,8 @@ In the procedures that read or write the text or the bytes of a file, a file tha - [GetFolder](FileSystemTools#getfolder) -- returns a Folder object - [GetParentFolderName](FileSystemTools#getparentfoldername) -- returns the path of the folder that holds the last component of a path - [GetRelativePath](FileSystemTools#getrelativepath) -- returns the path that leads from one path to another - - [GetSpecialFolder](FileSystemTools#getspecialfolder) -- returns the path of a well-known Windows folder + - [GetSpecialFolder](FileSystemTools#getspecialfolder) -- returns a Folder object for a well-known Windows folder + - [GetSpecialFolderPath](FileSystemTools#getspecialfolderpath) -- returns the path of a well-known Windows folder - [GetStandardStream](FileSystemTools#getstandardstream) -- returns a TextStream for standard input, output or error - [GetTempName](FileSystemTools#gettempname) -- returns a random name for a temporary file or folder - [IsFileLocked](FileSystemTools#isfilelocked) -- returns whether a file is in use @@ -242,6 +241,7 @@ In the procedures that read or write the text or the bytes of a file, a file tha - [Attributes](Folder#attributes), [DateCreated](Folder#datecreated), [DateLastAccessed](Folder#datelastaccessed), [DateLastModified](Folder#datelastmodified), [Drive](Folder#drive), [Files](Folder#files), [HasAttribute](Folder#hasattribute), [IsRootFolder](Folder#isrootfolder), [Name](Folder#name), [ParentFolder](Folder#parentfolder), [Path](Folder#path), [ShortName](Folder#shortname), [ShortPath](Folder#shortpath), [Size](Folder#size), [SubFolders](Folder#subfolders), [Type](Folder#type) -- the properties - [Copy](Folder#copy), [CreateTextFile](Folder#createtextfile), [Delete](Folder#delete), [Move](Folder#move), [Refresh](Folder#refresh), [Rename](Folder#rename), [SetAttribute](Folder#setattribute) -- the methods - [Folders](Folders) -- the collection of the folders of a folder + - [Add](Folders#add) -- creates a subfolder and adds it to the collection - [TextStream](TextStream) -- reads or writes a text file a piece at a time - [AtEndOfLine](TextStream#atendofline), [AtEndOfStream](TextStream#atendofstream), [Column](TextStream#column), [Encoding](TextStream#encoding), [IsStreaming](TextStream#isstreaming), [Line](TextStream#line) -- the properties - [Close](TextStream#close), [Flush](TextStream#flush), [Read](TextStream#read), [ReadAll](TextStream#readall), [ReadLine](TextStream#readline), [Skip](TextStream#skip), [SkipLine](TextStream#skipline), [Write](TextStream#write), [WriteBlankLines](TextStream#writeblanklines), [WriteLine](TextStream#writeline) -- the methods diff --git a/indexer/snapshots/contributed/tbFileSysTools/api.json b/indexer/snapshots/contributed/tbFileSysTools/api.json index d470845fe..151b6a4de 100644 --- a/indexer/snapshots/contributed/tbFileSysTools/api.json +++ b/indexer/snapshots/contributed/tbFileSysTools/api.json @@ -2,7 +2,7 @@ "format": 3, "package": "tbFileSysTools", "project": "tbFileSysTools", - "version": "1.8.0.0", + "version": "1.9.0.0", "types": [ { "name": "Drive", @@ -204,7 +204,8 @@ {"name":"GetFolder","kind":"function","vis":"public","signature":"Public Function GetFolder(ByVal folderPath As String) As Folder","returnType":"Folder","description":"Returns a Folder object for an existing folder."}, {"name":"GetParentFolderName","kind":"function","vis":"public","signature":"Public Function GetParentFolderName(ByVal path As String) As String","returnType":"String","description":"Returns a string containing the name of the parent folder of the last component in a specified path."}, {"name":"GetRelativePath","kind":"function","vis":"public","signature":"Public Function GetRelativePath(ByVal basePath As String, ByVal targetPath As String, Optional ByVal basePathIsFolder As Boolean = True, Optional ByVal trimLeadingDotSlash As Boolean = True) As String","returnType":"String","description":"Returns a relative path from a base path to a target path. The base path is treated as a folder by default. Relative inputs are resolved against the current directory. Returns the absolute target if no relative path exists (e.g. different drives)."}, - {"name":"GetSpecialFolder","kind":"function","vis":"public","signature":"Public Function GetSpecialFolder(ByVal specialFolder As SpecialFolderConst) As String","returnType":"String","description":"Returns the absolute path to the requested known folder. The returned path does NOT include a trailing backslash."}, + {"name":"GetSpecialFolder","kind":"function","vis":"public","signature":"Public Function GetSpecialFolder(ByVal specialFolder As SpecialFolderConst) As Folder","returnType":"Folder","description":"Returns a Folder object for the requested known folder. Raises 76 if the folder does not exist on this system."}, + {"name":"GetSpecialFolderPath","kind":"function","vis":"public","signature":"Public Function GetSpecialFolderPath(ByVal specialFolder As SpecialFolderConst) As String","returnType":"String","description":"Returns the absolute path to the requested known folder as a String. The returned path does NOT include a trailing backslash, and the folder is not required to exist."}, {"name":"GetStandardStream","kind":"function","vis":"public","signature":"Public Function GetStandardStream(ByVal standardStreamType As StandardStreamTypes, Optional ByVal unicode As Boolean = False) As TextStream","returnType":"TextStream","description":"Returns a TextStream over one of the process's standard streams. unicode:=True gives UTF-16LE (no BOM); default is the system ANSI code page. Raises if no handle is available (e.g. no console)."}, {"name":"GetTempName","kind":"function","vis":"public","signature":"Public Function GetTempName(Optional ByVal prefix As String, Optional ByVal extension As String = \".tmp\", Optional ByVal inFolder As String = \"\") As String","returnType":"String","description":"Returns a randomly generated temporary file or folder name. If inFolder is supplied, the returned name is one that does not already exist in that folder at the time of the call."}, {"name":"IsFileLocked","kind":"function","vis":"public","signature":"Public Function IsFileLocked(ByVal filePath As String) As Boolean","returnType":"Boolean","description":"Returns True if the file is currently locked by another process - that is, if it cannot be opened for exclusive read/write access right now. The answer is only valid for the instant of the call. Raises if the file does not exist."}, @@ -273,13 +274,14 @@ {"name":"GetFileName","kind":"function","vis":"public","signature":"Public Function GetFileName(ByVal path As String) As String","returnType":"String","description":"Returns the last component of a specified path that is not part of the drive specification."}, {"name":"GetFilePaths","kind":"function","vis":"public","signature":"Public Function GetFilePaths(ByVal folderPath As String, Optional ByVal pattern As String = \"*\", Optional ByVal includeHidden As Boolean = False, Optional ByVal includeSystem As Boolean = False, Optional ByVal recursive As Boolean = False) As Collection","returnType":"Collection","description":"Returns a Collection of full file paths in the specified folder. Supports a wildcard pattern, optional recursion into subfolders, inclusion of hidden/system files."}, {"name":"GetFileType","kind":"function","vis":"public","signature":"Public Function GetFileType(ByVal filePath As String) As String","returnType":"String","description":"Returns the shell's type description for a file or folder (e.g. \"Text Document\", \"Application\", \"File Folder\"), the same string Explorer and Scripting.FileSystemObject's File.Type show. Falls back to \"<EXT> File\" or \"File\" when the shell is unavailable, and for paths longer than 260 characters, which the shell APIs do not support."}, - {"name":"GetFileVersion","kind":"function","vis":"public","signature":"Public Function GetFileVersion(ByVal filePath As String, Optional ByVal source As FileVersionSource = fvFixed) As String","returnType":"String","description":"Returns the file's version. By default (fvFixed) returns the binary version as major.minor.build.revision, fvFileString returns the authored StringFileInfo FileVersion string and fvProductString returns the ProductVersion string; both fall back to the fixed version when the file has no string block. Returns an empty string when the file carries no version resource, and also for paths longer than 260 characters, which the version-resource APIs do not support."}, + {"name":"GetFileVersion","kind":"function","vis":"public","signature":"Public Function GetFileVersion(ByVal filePath As String, Optional ByVal source As FileVersionSource = fvFixed) As String","returnType":"String","description":"Returns the file's version. By default (fvFixed) returns the binary version as major.minor.build.revision, fvFileString returns the authored StringFileInfo FileVersion string and fvProductString returns the ProductVersion string; both fall back to the fixed version when the file has no string block. Raises 53 if the file does not exist. Returns an empty string when the file carries no version resource, and also for paths longer than 260 characters, which the version-resource APIs do not support."}, {"name":"GetFolder","kind":"function","vis":"public","signature":"Public Function GetFolder(ByVal folderPath As String) As Folder","returnType":"Folder","description":"Returns a Folder object for an existing folder."}, {"name":"GetParentFolderName","kind":"function","vis":"public","signature":"Public Function GetParentFolderName(ByVal path As String) As String","returnType":"String","description":"Returns a string containing the name of the parent folder of the last component in a specified path."}, {"name":"GetRelativePath","kind":"function","vis":"public","signature":"Public Function GetRelativePath(ByVal basePath As String, ByVal targetPath As String, Optional ByVal basePathIsFolder As Boolean = True, Optional ByVal trimLeadingDotSlash As Boolean = True) As String","returnType":"String","description":"Returns a relative path from a base path to a target path. The base path is treated as a folder by default. Relative inputs are resolved against the current directory. Returns the absolute target if no relative path exists (e.g. different drives)."}, - {"name":"GetSpecialFolder","kind":"function","vis":"public","signature":"Public Function GetSpecialFolder(ByVal specialFolder As SpecialFolderConst) As String","returnType":"String","description":"Returns the absolute path to the requested known folder. The returned path does NOT include a trailing backslash."}, + {"name":"GetSpecialFolder","kind":"function","vis":"public","signature":"Public Function GetSpecialFolder(ByVal specialFolder As SpecialFolderConst) As Folder","returnType":"Folder","description":"Returns a Folder object for the requested known folder. Raises 76 if the folder does not exist on this system."}, {"name":"GetSpecialFolderCSIDL","kind":"function","vis":"private","signature":"Private Function GetSpecialFolderCSIDL(ByVal specialFolder As SpecialFolderConst) As String","returnType":"String"}, {"name":"GetSpecialFolderKnown","kind":"function","vis":"private","signature":"Private Function GetSpecialFolderKnown(ByVal specialFolder As SpecialFolderConst) As String","returnType":"String"}, + {"name":"GetSpecialFolderPath","kind":"function","vis":"public","signature":"Public Function GetSpecialFolderPath(ByVal specialFolder As SpecialFolderConst) As String","returnType":"String","description":"Returns the absolute path to the requested known folder as a String. The returned path does NOT include a trailing backslash, and the folder is not required to exist."}, {"name":"GetStandardStream","kind":"function","vis":"public","signature":"Public Function GetStandardStream(ByVal standardStreamType As StandardStreamTypes, Optional ByVal unicode As Boolean = False) As TextStream","returnType":"TextStream","description":"Returns a TextStream over one of the process's standard streams. unicode:=True gives UTF-16LE (no BOM); default is the system ANSI code page. Raises if no handle is available (e.g. no console)."}, {"name":"GetTempName","kind":"function","vis":"public","signature":"Public Function GetTempName(Optional ByVal prefix As String, Optional ByVal extension As String = \".tmp\", Optional ByVal inFolder As String = \"\") As String","returnType":"String","description":"Returns a randomly generated temporary file or folder name. If inFolder is supplied, the returned name is one that does not already exist in that folder at the time of the call."}, {"name":"GuidFromString","kind":"function","vis":"private","signature":"Private Function GuidFromString(ByVal s As String) As UUID","returnType":"UUID"}, @@ -319,7 +321,7 @@ {"name":"MoveOneFile","kind":"function","vis":"private","signature":"Private Function MoveOneFile(ByVal source As String, ByVal destination As String) As String","returnType":"String"}, {"name":"MoveOneFolder","kind":"function","vis":"private","signature":"Private Function MoveOneFolder(ByVal source As String, ByVal destination As String) As String","returnType":"String"}, {"name":"MoveWildcard","kind":"function","vis":"private","signature":"Private Function MoveWildcard(ByVal sourcePattern As String, ByVal destination As String, ByVal wantFolders As Boolean) As String","returnType":"String"}, - {"name":"NormalizeTextFile","kind":"function","vis":"public","signature":"Public Function NormalizeTextFile(ByVal filePath As String, Optional ByVal toEncoding As TextEncoding = encUtf8, Optional ByVal toNewLine As NewlineType = nlWindows, Optional ByVal fromEncoding As TextEncoding = encAutoDetect, Optional ByVal toAttributes As FileAttribute = faNoChange) As Boolean","returnType":"Boolean","description":"Rewrites a text file in place in the target encoding and newline style (source auto-detected). toAttributes enforces a target attribute set (settable bits only); NoChange (the default) preserves the file's current attributes. Skips the write when content is already in target form. Refuses binary files. Returns True if the file was modified."}, + {"name":"NormalizeTextFile","kind":"function","vis":"public","signature":"Public Function NormalizeTextFile(ByVal filePath As String, Optional ByVal toEncoding As TextEncoding = encUtf8, Optional ByVal toNewLine As NewlineType = nlWindows, Optional ByVal fromEncoding As TextEncoding = encAutoDetect, Optional ByVal toAttributes As FileAttribute = faNoChange) As Boolean","returnType":"Boolean","description":"Rewrites a text file in place in the target encoding and newline style (source auto-detected). toAttributes enforces a target attribute set (settable bits only); NoChange (the default) preserves the file's current attributes, including ReadOnly. Skips the write when content is already in target form. Refuses binary files. Returns True if the file was modified."}, {"name":"OpenTextFile","kind":"function","vis":"public","signature":"Public Function OpenTextFile(ByVal filePath As String, Optional ByVal ioMode As IOMode = ForReading, Optional ByVal create As Boolean = False, Optional ByVal encoding As TextEncoding = encAutoDetect, Optional ByVal writeModeLineEnding As NewlineType = nlWindows) As TextStream","returnType":"TextStream","description":"Opens a text file and returns a streaming TextStream. For reading, omit encoding (encAutoDetect) to detect it from a head sample. For writing/appending, encoding selects the output encoding (default UTF-8, no BOM). Encodings whose chunk boundaries cannot be managed (UTF-7, unclassifiable code pages) transparently fall back to a buffered path."}, {"name":"PathExists","kind":"function","vis":"private","signature":"Private Function PathExists(ByVal path As String) As Boolean","returnType":"Boolean"}, {"name":"PeekWord","kind":"function","vis":"private","signature":"Private Function PeekWord(ByVal p As LongPtr) As Integer","returnType":"Integer"}, @@ -333,7 +335,7 @@ {"name":"StringToTextFile","kind":"sub","vis":"public","signature":"Public Sub StringToTextFile(ByVal text As String, ByVal filePath As String, Optional ByVal encoding As TextEncoding = encUtf8, Optional ByVal outLineEnding As NewlineType = nlNoChange)","description":"Writes a string to a file in the specified encoding (a BOM is emitted only for BOM-bearing members such as encUtf8Bom). Overwrites any existing file."}, {"name":"TextFileToArray","kind":"function","vis":"public","signature":"Public Function TextFileToArray(ByVal filePath As String, Optional ByVal encoding As TextEncoding = encAutoDetect) As String()","returnType":"String()","description":"Reads a text file into an array of lines (mixed CR/LF/CRLF handled). Encoding auto-detected by default. A trailing newline yields a final empty element. An empty file returns an unallocated array."}, {"name":"TextFileToString","kind":"function","vis":"public","signature":"Public Function TextFileToString(ByVal filePath As String, Optional ByVal encoding As TextEncoding = encAutoDetect, Optional ByVal outLineEnding As NewlineType = nlNoChange) As String","returnType":"String","description":"Reads an entire text file to a string. With encAutoDetect (default) the encoding is detected, and a binary file raises; with an explicit encoding that encoding is used as-is (a byte-order mark still overrides it). A leading BOM is stripped. An empty file returns an empty string. Optionally normalizes line endings."}, - {"name":"WriteStream","kind":"sub","vis":"public","signature":"Public Sub WriteStream(ByRef stream() As Byte, ByVal filePath As String)","description":"Writes a byte array to the file specified."} + {"name":"WriteStream","kind":"sub","vis":"public","signature":"Public Sub WriteStream(ByRef stream() As Byte, ByVal filePath As String)","description":"Writes a byte array to the file specified. Raises 70 if the target exists and is read-only - clear the attribute first if replacement is intended."} ] }, { @@ -571,13 +573,15 @@ "file": "Folders.twin", "signature": "Public Class Folders", "attrs": [{"name":"Hidden","args":"False"}], - "description": "A read-only collection of Folder objects, enumerable with For Each.", + "description": "A collection of Folder objects, enumerable with For Each. Add creates a new subfolder.", "members": [ + {"name":"Add","kind":"function","vis":"public","signature":"Public Function Add(ByVal name As String) As Folder","returnType":"Folder","description":"Creates a new subfolder of the folder these were enumerated from, adds it to this collection, and returns it. name must be a bare name, not a path. Raises 58 if it already exists."}, {"name":"Count","kind":"property","vis":"public","signature":"Public Property Get Count() As Long","returnType":"Long","accessor":"Get","description":"Returns the number of Folder objects available."}, - {"name":"Internal_Init","kind":"sub","vis":"friend","signature":"Friend Sub Internal_Init(ByVal items As Collection)"}, + {"name":"Internal_Init","kind":"sub","vis":"friend","signature":"Friend Sub Internal_Init(ByVal items As Collection, ByVal parentPath As String)"}, {"name":"Item","kind":"property","vis":"public","signature":"Public Property Get Item(ByVal nameOrIndex As Variant) As Folder","returnType":"Folder","accessor":"Get","attrs":[{"name":"DefaultMember"}],"description":"Returns a specific Folder object by name or index."}, {"name":"NewEnum","kind":"function","vis":"public","signature":"Public Function NewEnum() As IUnknown","returnType":"IUnknown","hidden":true,"attrs":[{"name":"Enumerator"},{"name":"Hidden","args":"True"}]}, - {"name":"mItems","kind":"field","vis":"private","signature":"Private mItems As Collection"} + {"name":"mItems","kind":"field","vis":"private","signature":"Private mItems As Collection"}, + {"name":"mParentPath","kind":"field","vis":"private","signature":"Private mParentPath As String"} ] }, { @@ -774,6 +778,7 @@ {"name":"ERROR_BAD_NETPATH","kind":"const","vis":"public","signature":"Public Const ERROR_BAD_NETPATH As Long = 53"}, {"name":"ERROR_CONNECTION_UNAVAIL","kind":"const","vis":"public","signature":"Public Const ERROR_CONNECTION_UNAVAIL As Long = 1201"}, {"name":"ERROR_DIRECTORY","kind":"const","vis":"public","signature":"Public Const ERROR_DIRECTORY As Long = 267"}, + {"name":"ERROR_DIR_NOT_EMPTY","kind":"const","vis":"public","signature":"Public Const ERROR_DIR_NOT_EMPTY As Long = 145"}, {"name":"ERROR_DISK_FULL","kind":"const","vis":"public","signature":"Public Const ERROR_DISK_FULL As Long = 112"}, {"name":"ERROR_FILE_EXISTS","kind":"const","vis":"public","signature":"Public Const ERROR_FILE_EXISTS As Long = 80"}, {"name":"ERROR_FILE_NOT_FOUND","kind":"const","vis":"public","signature":"Public Const ERROR_FILE_NOT_FOUND As Long = 2"}, diff --git a/test/example-projects/contributed-tbFileSysTools/Settings b/test/example-projects/contributed-tbFileSysTools/Settings index 67f6e45e3..de27c99cf 100644 --- a/test/example-projects/contributed-tbFileSysTools/Settings +++ b/test/example-projects/contributed-tbFileSysTools/Settings @@ -57,7 +57,7 @@ "symbolId": "tbFileSysTools", "versionBuild": "0", "versionMajor": "1", - "versionMinor": "8", + "versionMinor": "9", "versionRevision": "0" } ], From 8bace4c0586c1ad1c55c45936231e1644c24446c Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober <kuba@mareimbrium.org> Date: Wed, 14 Oct 2026 11:04:57 +0200 Subject: [PATCH 05/13] Package test projects: test/package-tests/<Package>/ backs each contributed section's NOTEs and WARNINGs with [TestCase]s that check the correct behaviour (Expect.twin, generated RunAll, expect.json); package-tests.bat runs them (fixed / changed / documented), check_package_tests.mjs in test.bat ties each callout to its cases, the build publishes each project as <Package>-tests.twinproj with the package linked; tbrun --only; the probe harnesses move under it --- .github/actions/run-gates/action.yml | 8 + WIP.PackageTests.md | 102 +++++ WIP.md | 12 +- builder/gantt.mjs | 2 +- builder/package-tests.mjs | 96 +++++ builder/publish-policy.mjs | 7 +- builder/tbdocs.mjs | 37 +- docs/Documentation/Authoring.md | 11 + docs/Documentation/Building.md | 1 + docs/Documentation/Images/Tools-Groups.dot | 23 +- docs/Documentation/Images/Tools-Groups.svg | 364 +++++++++-------- docs/Documentation/Tools-Samples.md | 21 + docs/Documentation/Tools-Toolchain-Tests.md | 18 + docs/Documentation/Tools.md | 41 +- docs/_config.yml | 10 + lib/package-tests.mjs | 131 ++++++ package-tests.bat | 28 ++ scripts/check_gate_lists.mjs | 3 +- scripts/check_package_tests.mjs | 373 ++++++++++++++++++ scripts/check_tree_fresh.mjs | 13 +- scripts/lib/cli-cases.mjs | 1 + scripts/lib/package-test-cases.mjs | 207 ++++++++++ scripts/package_tests.mjs | 294 ++++++++++++++ scripts/tbrun.mjs | 19 +- test.bat | 8 + test/README.md | 10 +- test/package-tests/Expect.twin | 155 ++++++++ test/package-tests/README.md | 132 +++++++ .../TBMANLIB_Dialog/probe}/README.md | 12 +- .../TBMANLIB_Dialog/probe}/probe.mjs | 8 +- .../TBMANLIB_Dialog/probe}/run-exe.mjs | 10 +- .../TBMANLIB_Dialog/probe}/src/Auto.twin | 0 .../TBMANLIB_Dialog/probe}/src/AutoD.twin | 0 .../TBMANLIB_Dialog/probe}/src/DrvB1.twin | 0 .../TBMANLIB_Dialog/probe}/src/Probe.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbeB1.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbeD.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbeDT.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbeEx.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbeIB.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbePG.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProbePW.twin | 0 .../TBMANLIB_Dialog/probe}/src/ProgSink.twin | 0 .../xCalendar/probe}/README.md | 16 +- .../xCalendar/probe}/designer.mjs | 18 +- .../xCalendar/probe}/patch-stream.mjs | 2 +- .../xCalendar/probe}/probe.mjs | 8 +- .../xCalendar/probe}/run-exe.mjs | 10 +- .../xCalendar/probe}/src/Form1.tbform | 0 .../xCalendar/probe}/src/Form1.twin | 0 .../xCalendar/probe}/src/Probe.twin | 0 .../xCalendar/probe}/src/Win.twin | 0 .../xCalendar/probe}/stage.mjs | 8 +- 53 files changed, 1966 insertions(+), 253 deletions(-) create mode 100644 WIP.PackageTests.md create mode 100644 builder/package-tests.mjs create mode 100644 lib/package-tests.mjs create mode 100644 package-tests.bat create mode 100644 scripts/check_package_tests.mjs create mode 100644 scripts/lib/package-test-cases.mjs create mode 100644 scripts/package_tests.mjs create mode 100644 test/package-tests/Expect.twin create mode 100644 test/package-tests/README.md rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/README.md (91%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/probe.mjs (94%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/run-exe.mjs (93%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/Auto.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/AutoD.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/DrvB1.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/Probe.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbeB1.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbeD.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbeDT.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbeEx.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbeIB.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbePG.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProbePW.twin (100%) rename test/{package-probes/TBMANLIB_Dialog => package-tests/TBMANLIB_Dialog/probe}/src/ProgSink.twin (100%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/README.md (90%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/designer.mjs (91%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/patch-stream.mjs (95%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/probe.mjs (91%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/run-exe.mjs (93%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/src/Form1.tbform (100%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/src/Form1.twin (100%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/src/Probe.twin (100%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/src/Win.twin (100%) rename test/{package-probes/xCalendar => package-tests/xCalendar/probe}/stage.mjs (91%) diff --git a/.github/actions/run-gates/action.yml b/.github/actions/run-gates/action.yml index 405d89fd6..a00c889b1 100644 --- a/.github/actions/run-gates/action.yml +++ b/.github/actions/run-gates/action.yml @@ -216,6 +216,14 @@ runs: - name: Verify the book-coverage warnings (check_book_coverage.mjs) shell: bash run: node scripts/check_book_coverage.mjs + # Every NOTE and WARNING of a contributed package's section names the test + # cases behind it, and each package test project under test/package-tests/ + # is in order: its cases named, well formed, in expect.json for the + # documented version and in its generated RunAll module. Sources only: no + # browser, no built tree, no install. + - name: Verify the package test markers and projects (check_package_tests.mjs) + shell: bash + run: node scripts/check_package_tests.mjs # The symbol index (tB/symbols.json) is read by the IDE help add-in. A # build that indexes the reference cleanly says nothing about the rules # that did not fire on it, so these probes assert each against the case diff --git a/WIP.PackageTests.md b/WIP.PackageTests.md new file mode 100644 index 000000000..f3b301cc0 --- /dev/null +++ b/WIP.PackageTests.md @@ -0,0 +1,102 @@ +# Package test cases --- why they are built this way + +What to do is in [WIP.md](WIP.md) (*Where things live*, the contributed packages) and in +[test/package-tests/README.md](test/package-tests/README.md). This file says why. + +## What the test cases are for + +A contributed section's NOTEs and WARNINGs are claims about someone else's code: that a member +raises 53, that a match is cut short, that a dialog ignores a property. Each was true of one version +when an agent checked it, and nothing kept that evidence: the probe pages were deleted after use. +So the claims are written down as cases that persist, run again whenever the package's version +moves, and say which claims no longer hold. The owner wants the same cases to be usable by the +package's author, which decides most of what follows. + +## A case checks what is correct (owner) + +A case asserts the behaviour the package should have, not the behaviour it has. To the author a +red case is a defect to fix and a green one is done, with a message that reads as a bug report. +The documentation needs the opposite question answered too (does the page still describe the +package?), and `expect.json` answers it: it records, per case and architecture, the exact failure +report or that the case never returns, on the documented version. The runner's three verdicts +come from comparing the two: *documented*, *fixed* (the callout goes, by the rule that the +documentation is not a record of past defects), *changed*. + +The failure report is compared exactly, after the line number and the assertion prefix are +stripped, so a defect that changes shape (a different wrong value) is *changed*, not +*documented*. + +## Expect, not Assert (owner) + +twinBASIC's own form is a `[TestFixture]` module of `[TestCase]` Subs checked with the Assert +package, and `tbrun --tests` runs those in the compiler's test mode. But in BETA 1005 the IDE has +no way for a person to run a test case, and outside test mode a failed `Assert` stops the run at +the error panel, uncatchable by `On Error` (WIP.Harness.md, *Running a project's test cases*). An +author who opens the project would see the first failure only. So the cases check through +`Expect.twin`: + +- **Under `RunAll`** (the project's `Sub Main`, which an author runs with F5), `Expect` collects: + `Expect.Done` records the case's report, and `RunAll` calls every case under + `On Error Resume Next` and prints `PASS`, `FAIL` or `SKIP` for each. +- **Under `tbrun --tests`**, `Expect.Done` ends a failed case with `Assert.Strict.Fail` and the + whole report as its message. An `Assert` failure is the one thing a case's own + `On Error Resume Next` cannot hide; an `Err.Raise` from `Done` would be swallowed by it and + the case would pass. Checked on BETA 1005: the message comes back whole in the response's + `body.error`. +- Every check of a case runs, and `Done` reports all failed ones, so one run of a case shows + every defect it covers. + +The `[TestFixture]` and `[TestCase]` attributes stay, so the cases are in twinBASIC's own form +for whatever runner the IDE gains. `RunAll.twin` is generated rather than written, because a +hand-kept list of calls drifts from the cases; the gate refuses a stale one. + +## A case that never returns + +Some defects are a loop that never ends (BigNumberLib's modular inverse of zero, TBMANLIB_Xml on +some malformed text). In test mode such a case holds the compiler: `tbrun` gives up on it after +its timeout, and every case after it would time out too. So the runner, on a case that does not +return, records it and runs the cases after it again in a new IDE; once `expect.json` says +`hangs`, the case is run alone with a short timeout, and `RunAll` skips it, because an author's +IDE would hang. + +## The download carries no package (owner) + +Embedding the package in the published file would mean either the build fetching from TWINSERV +(the deploy then depends on that server) or committing every documented version's `.twinpack` +(about 35 MB, some of it WinDevLib, and four packages declare no licence that allows passing them +on). The owner chose neither. + +The published project keeps the template's reference to the package and has no `Packages/` +folder, which makes it a **linked** reference. Checked on BETA 1005 (private APPDATA, private +desktop): + +- With the package's `.twinpack` in `%APPDATA%\twinBASIC\packages`, the project compiles. The IDE + finds the file by the package's project id, whatever its name, and takes the version from the + file: a 1.7.0.0 file under a reference that says 1.8.0.0 compiles against 1.7.0.0. +- Without it, every name of the package is TB5079, and Enabled Libraries shows + `[LINKED-PACKAGE] ... ERROR unable to load this reference`; its FIX link is not implemented. +- A file put in the folder while the IDE runs is used after Restart Compiler. +- Nothing downloads a missing package by itself. The IDE's only download path is the Available + Packages tab (`importPackage` in `main.js`); a project load calls only `/packages/checkVersions`, + and only for a reference with a `publisher` field. `path32`/`path64` set to a file path or a + TWINSERV URL, or a `.twinpack` beside the project, are not used. + +So the reader puts the documented version's file in the packages folder (or adds the package from +Available Packages with Embedded cleared) and restarts the compiler; an author points the folder at +their own build. The runner embeds the package from `%TEMP%\tb-twinserv` instead, as +`check_examples` does, so it never touches a packages folder. + +## Markers, and a gate that only tightens + +A callout names its cases in an HTML comment on the line above it. The marker lives with the +callout, so moving or rewording the callout keeps its tie, and it is invisible on the page. The +cases name nothing back: a case file referring to a page and a heading would break on every edit +of the page. The gate checks both directions (an unmarked callout, an unnamed case) from the +sources alone, so it runs in `test.bat` and CI, where the cases themselves cannot run. + +The sections were written before any project existed, so the gate starts with every section in +`PENDING` and fails on a section that has a project and is still listed, or has none and is not: +the list can only shrink, one package at a time. + +`none -- <reason>` exists for a callout no program can check (a fact about the IDE, a licence); it +keeps the decision explicit and visible in review, where a missing marker would be ambiguous. diff --git a/WIP.md b/WIP.md index 29c5e3367..4b97b4963 100644 --- a/WIP.md +++ b/WIP.md @@ -19,6 +19,7 @@ one covering what you are about to change. | touch fonts, diagrams, or the PDF's type | [WIP.Typography.md](WIP.Typography.md), then [WIP.Fonts.md](WIP.Fonts.md) for the generator | | change the accessibility scan | [WIP.A11y.md](WIP.A11y.md) --- the axe scan, the sample, the fingerprint gate | | work on the sample-compiling harness | [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md) | +| write, run or change a contributed package's test cases, or the tools behind them | [test/package-tests/README.md](test/package-tests/README.md) --- how a case is written, marked and run; [WIP.PackageTests.md](WIP.PackageTests.md) --- why the design is what it is | | run or change Wisdom, the Discord harvester | [WIP.Wisdom.md](WIP.Wisdom.md) --- the three-phase pipeline and the Phase 3 extract flow | | retake the documentation's IDE screenshots, or change the tool that takes them | [WIP.Screenshots.md](WIP.Screenshots.md) --- `shoot_docs.mjs`: the setups, cut-outs, the annotation overlay and the owner's decisions | | work on the IDE help add-in, or on testing IDE add-ins by machine | [WIP.HelpAddin.md](WIP.HelpAddin.md) --- the plan, the IDE facts it rests on, and the probes still open | @@ -34,7 +35,7 @@ sibling. History goes nowhere: git keeps it. Reference documentation is **complete** for all thirteen packages: VBA, VBRUN, VB, WebView2, Assert, CustomControls, CEF, WinEventLogLib, WinNamedPipesLib, WinServicesLib, tbIDE, WinNativeCommonCtls and AppGlobalClassObject. It is adapted from primary sources: Microsoft VBA-Docs (CC-BY-4.0) for the runtime library, `.twin` source for the twinBASIC-specific packages. The CEF and WebView2 packages also carry a tutorial set. -Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON and xCalendar complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. The two console packages carry a NOTE (owner) that the samples harness cannot yet capture a console, so their console claims follow the source. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-probes/xCalendar/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-probes/TBMANLIB_Dialog/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.8.0.0 gives up a search that uses up its step budget (5,000 steps for each character of the string, at least 1,000,000 and at most 50,000,000, for the search of one match; fixed inside the package) and reports it in the read-only `RegExp.SearchAborted`, documented on its own page and in a section of the landing page; `a*` and `^.*$` match 1,000,000 characters whole in about half a second. Each one has a chapter in the book's Packages part. **ArrayListLib, BigNumberLib, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1005 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. See *Where things live* for how a package moves between the two lists. +Twenty-six **contributed** packages from TWINSERV have sections under `docs/Reference/Contributed/`: ArrayListLib, FileProps, Fmt, Generics, OLEGuids, WinReg, TwinTimerLib, tbFileSysTools, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_ListBoxHScroll, TBMANLIB_RES, TBMANLIB_Xml, tbProjPacker, tbRegExp, tsJson, tsPwGen, VBA_JSON and xCalendar complete; AutoCompleteLib, BigNumberLib and VBComDlg their main class only. The two console packages' console claims follow the source, because the samples harness cannot capture a console. Generics (seven tuple classes) and xCalendar (one control) are each one page (owner), so `analyze` in audit mode lists their classes and members as undocumented. What xCalendar's page says it does at run time comes from `test/package-tests/xCalendar/probe/`, which runs the control on a form in a built exe. Fmt documents its test code (`HelloWorldClass`, `HelloWorldModule`, `Pootle`) in one paragraph of its landing page, which `exclude_from_docs` names; Generics' `exclude_from_docs` names its test code, `HelloWorldModule`, which the page does not mention. VBComDlg's `exclude_from_docs` names `CommonDialogBase`, a module whose public procedures only its class calls. tbRegExp's `exclude_from_docs` names its private engine module, `StaticRegex`, and tbProjPacker's its helper modules, `modImpExp` and `modWin32`: the indexer counts a Public Enum as public even inside a Private Module, because a built-in package's are reached through its library symbol written with an asterisk (`*WinNativeCommonCtls`, as `ImlDrawConstants` says). By its bare name no such enum is reachable, in a built-in or a contributed package (TB5079 on BETA 1005). OLEGuids documents each interface's methods on the interface's page. What TBMANLIB_Dialog's pages say its dialogs do comes from `test/package-tests/TBMANLIB_Dialog/probe/`, which operates them in a built exe on a private desktop; run it again when the package's version changes. TBMANLIB_Dialog 1.1.0.0 has a page of its own for each class except cDialog, which keeps a page for each member, so `analyze` in audit mode lists the members of those one-page classes as undocumented; its `exclude_from_docs` names its test module, `TestModule`, and the seven COM interface declarations that cDialogEx calls inside (`IOleWindow`, `IModalWindow`, `IShellItem`, `IShellItemArray`, `IFileDialog`, `IFileOpenDialog`, `IFileSaveDialog`), which its landing page describes in a paragraph. cDialogEx 1.1.0.0 returns the name of the chosen file, not its path, ignores `InitialDir` and ignores a `Filter` that has no closing pipe, each documented as a WARNING. tbRegExp is the first section drafted by the doc-draft workflow ([WIP.Indexer.md](WIP.Indexer.md#draft-and-the-doc-draft-workflow)), then reviewed against the sources, the package's README and VBScript's own **RegExp** under `cscript`. tbRegExp 1.8.0.0 gives up a search that uses up its step budget (5,000 steps for each character of the string, at least 1,000,000 and at most 50,000,000, for the search of one match; fixed inside the package) and reports it in the read-only `RegExp.SearchAborted`, documented on its own page and in a section of the landing page; `a*` and `^.*$` match 1,000,000 characters whole in about half a second. Each one has a chapter in the book's Packages part. **ArrayListLib, BigNumberLib, TBMANLIB, TBMANLIB_Aes, TBMANLIB_ConsoleComponents, TBMANLIB_ConsoleCore, TBMANLIB_Diagnostic, TBMANLIB_Dialog, TBMANLIB_Xml, WinReg and xCalendar are listed under Experimental Packages** (`docs/Reference/Experimental.md`, owner): ArrayListLib and WinReg do not compile on BETA 1005 (each documented from its source and a copy of it that builds, its samples compile-only; in a copy of ArrayListLib 1.6.151.1 with the arguments of its three `VSwap` calls passed `ByVal` and `LSetByVal` taking a `LongPtr`, a For Each over a list with no items fails, ToString of such a list never returns, Reverse keeps the middle pair of an even run in place and a second GetEnumerator over a list ignores its arguments; in a copy of WinReg's source with its four failing calls passed `ByVal`, ReadString, GetRegistryValueType and the first ConvertValueToString give wrong results), xCalendar has a grid that keeps the old month's dates when Value is set to another month, and text colour properties that have no effect, BigNumberLib 1.0.2.0 compiles, but loses the minus sign after `IsNegative = True` and in a negative operand of the greatest common divisor and the least common multiple, makes a zero from `AssignFromLong` negative, and does not return from the modular inverse of zero (all four confirmed by a second agent), TBMANLIB 1.0.0.29 does not compile in a 64-bit build (TB5001 in cSubClass, where the window procedure is passed as a Long) and has many members that fail or give wrong results, TBMANLIB_ConsoleComponents cannot take a cConsole of the published TBMANLIB_ConsoleCore (it embeds a copy of it, the same package at 1.0.0.1, whose types the compiler treats as different; checked in a project made by the IDE's own import, in either order), TBMANLIB_ConsoleCore reads keys wrongly, TBMANLIB_Diagnostic cannot create its Dictionary and misplaces List items, TBMANLIB_Dialog 1.1.0.0 has classes that fail at their main task (cDialogFindReplace reports nothing, cDialogPageSetup ignores paper and orientation, cDialogEx returns names without their folder, cDialogPassword shows the password in its confirmation boxes; every defect confirmed by a second agent, owner), TBMANLIB_Aes runs other cipher modes than the ones named (its default CBC is ECB) and fails the NIST test vectors, and TBMANLIB_Xml's parser misreads self-closing elements and entities and does not return for some malformed text (and its XPath engine reads the step `..` and `and` with `or` wrongly). TBMANLIB_Xml's `exclude_from_docs` names its test module, `mXmlTests`, which its landing page describes in a paragraph, and the five types of its XPath engine (`XPathTokenType`, `XPathAxisType`, `XPathOperator`, `XPathStep`, `XPathPredicate`), which no public member takes or returns and which the mXPath page mentions in a paragraph; tsPwGen's names its five test modules and its six internal modules, and tsJson's its test and internal modules likewise, which their landing pages describe in a paragraph each. See *Where things live* for how a package moves between the two lists. The rest of this file is the maintenance guide for updating existing pages or adding new ones: package surface notes, page conventions, and the integrity check. @@ -79,7 +80,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad > would file the page as a *static file* and serve the raw markdown. Editors on > Windows add a BOM unasked. `lib/frontmatter.mjs` strips a leading BOM before parsing. - `docs/Reference/Built-In/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus 23 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing. -- `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `<Class>/index.md` at `/tB/Packages/Contributed/<Package>/<Class>/` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `<Class>.md`. Each package is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. Its samples compile in the `contributed-<Package>` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1005") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner); a minor defect that the section documents does not make it Experimental (owner: VBComDlg, whose Replace All raises no event, stays Contributed; owner: nor does a fixed limit that the section states at the top of its landing page). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and its book chapter follows the Experimental Packages chapter. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three, and its pages keep their addresses. +- `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `<Class>/index.md` at `/tB/Packages/Contributed/<Package>/<Class>/` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `<Class>.md`. Each package is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. **Every NOTE and WARNING of a section is backed by test cases** in the package's test project, `test/package-tests/<Package>/` (owner; [its README](test/package-tests/README.md)), and names them in a `<!-- test: ... -->` marker on the line above it, which `check_package_tests.mjs` in `test.bat` enforces. A case checks what is correct, not what the package does (owner): a defect is a failing case, recorded in the project's `expect.json`, so the project tells the package's author what to fix. A section whose project is not written yet is in that gate's `PENDING` set. `package-tests.bat` runs the cases; a case it reports *fixed* means the callout goes. **The landing page ends, after See Also, with `## Test cases`**: one or two sentences linking to `<Package>-tests.twinproj` (owner), which the build publishes beside the section without the package's code in it (owner): it references the package as a linked package, which the reader puts in `%APPDATA%\twinBASIC\packages` ([WIP.PackageTests.md](WIP.PackageTests.md)). Its samples compile in the `contributed-<Package>` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1005") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner); a minor defect that the section documents does not make it Experimental (owner: VBComDlg, whose Replace All raises no event, stays Contributed; owner: nor does a fixed limit that the section states at the top of its landing page). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and its book chapter follows the Experimental Packages chapter. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three, and its pages keep their addresses. - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features is 5). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**. Its language claims are measured on BETA 995 with `tbrun --llvm` and `--exe`: the comma, space and comma-space spellings of `[CompilerOptions]` all turn LLVM and `+optimize` on, `[CompilerOptions("")]` turns it off under project-wide LLVM, the long CPU-flag sample runs, and an error is lost between procedures when either the callee or the caller is LLVM-compiled. Its samples are marked `check_build`, which compiles them; `examples.bat --llvm` also builds them with LLVM. @@ -190,6 +191,7 @@ is in the right place at all: - **Placement.** A pure language keyword, parsed by the compiler with no runtime call, goes in `docs/Reference/Core/`. A runtime function or property goes under its package and module, with `redirect_from: /tB/Core/<name>` so legacy links still work. Packages are nested one level deeper than a bare `docs/Reference/<Package>/` --- see [Where things live](#where-things-live). - **Attribution is per page, decided by content provenance, not by package membership.** Set `vba_attribution: true` only on a page actually derived from a specific VBA-Docs source page. A symbol merely existing in VBA is not sufficient. - **Link to the canonical location**, meaning the page's own `permalink:`, never to one of its `redirect_from` aliases. +- **No public statement about the documentation's own testing** (owner). A page never says what this repository's tools could or could not check ("the samples harness cannot capture a console", "this was not probed"): that is a fact about the tooling, not the product. A claim nobody could check is written as the source supports it, or left out, and the reason goes in a WIP note. Any agent working on a section removes such a statement it finds there. The public "start here" page for a new contributor, [docs/Documentation/Authoring.md](docs/Documentation/Authoring.md), distils the same conventions (page template, heading levels, formatting, @@ -512,10 +514,11 @@ Why the report separates the wedged task from the merely blocked ones, and why - `build.bat` — runs `node builder\tbdocs.mjs --src docs --check-audit-index` (which implies `--check`) and produces three trees in one pass: the online copy at `_site/`, a `file://`-browsable copy at `_site-offline/`, and the sparse pagedjs source at `_site-pdf/`. The offline pass adds ~700 ms and the PDF pass adds ~150 ms on top of the ~2 s online build. Toggle `also_build_offline` / `also_build_pdf` in `_config.yml` (or pass `--no-offline` / `--no-pdf`) to skip a sibling output. `--check` adds ~1.7 s and runs the link + integrity check over the HTML while it is still in worker memory; `build.bat --no-check` gets a plain build. A build also writes the help add-in's archive, `add-in/Resources/HELP/site.zip` (see the help-archive bullet below); `--no-help-archive` skips it, and CI never passes it. - `serve.bat` — runs `tbdocs --serve`: initial build, then a long-lived process with watcher, debounced rebuilds, and SSE-driven browser auto-reload. Writes to `docs/_serve/` (disjoint from `build.bat`'s `_site*/`) and skips the offline + PDF passes — so a one-off `build.bat` for the PDF or offline mirror doesn't disturb the live preview. Ctrl+C to stop. - `check.bat` — the gates that read the built site: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the DOT diagram fit check (`scripts/check_dot_fit.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check runs in `build.bat`. ~37 s. -- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.mjs`), the markdown-plugin unit tests (`node --test test/render.test.mjs`), the date-formatter unit tests (`node --test test/strftime.test.mjs`), the bug-reproducer picture tests (`node --test test/png.test.mjs`), the embedded-image tests (`node --test test/compact-image.test.mjs`), the SVG screenshots' one-line test (`node --test test/svgshot.test.mjs`), `check_examples.mjs`'s probes (`node --test test/example-batches.test.mjs`), the port-claim unit tests (`node --test test/ports.test.mjs`), the add-in project-file tests (`node --test test/addin-project.test.mjs`), the package-indexer tests (`node --test test/indexer.test.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the attribute-sweep probes (`scripts/check_attribute_sweep.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~10 s and the impexp check ~4 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). +- `test.bat` — the tests the *toolchain* has to pass: the publish-allowlist self-test (`scripts/check_publish_policy.mjs`), the gate-list check (`scripts/check_gate_lists.mjs`), the CI-workflow roster check (`scripts/check_ci_workflows.mjs`), the lint gate (`scripts/check_lint.mjs`), the site-search unit tests (`node --test test/search.test.mjs`), the markdown-plugin unit tests (`node --test test/render.test.mjs`), the date-formatter unit tests (`node --test test/strftime.test.mjs`), the bug-reproducer picture tests (`node --test test/png.test.mjs`), the embedded-image tests (`node --test test/compact-image.test.mjs`), the SVG screenshots' one-line test (`node --test test/svgshot.test.mjs`), `check_examples.mjs`'s probes (`node --test test/example-batches.test.mjs`), the port-claim unit tests (`node --test test/ports.test.mjs`), the add-in project-file tests (`node --test test/addin-project.test.mjs`), the package-indexer tests (`node --test test/indexer.test.mjs`), the regex-safety gate (`scripts/check_regex_safety.mjs`), the code-region gate (`scripts/check_code_regions.mjs`), the page-count drift-guard probes (`scripts/check_page_baseline.mjs`), the book-coverage probes (`scripts/check_book_coverage.mjs`), the package-test markers and projects (`scripts/check_package_tests.mjs`), the symbol-index probes (`scripts/check_symbol_index.mjs`), the twinBASIC-scanner probes (`scripts/check_twin_parsers.mjs`), the attribute-sweep probes (`scripts/check_attribute_sweep.mjs`), the command-line probes and cases (`scripts/check_cli.mjs`), the pdf-lib shim comparison (`scripts/check_pdf_shims_equiv.mjs`), the impexp parity check (`scripts/check_impexp_parity.mjs`), and the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`). ~23 s, of which the regex-safety gate is ~10 s and the impexp check ~4 s. See [What belongs in test.bat rather than check.bat](WIP.Build.md#what-belongs-in-testbat-rather-than-checkbat). - `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\twinBASIC Book.pdf`. Run `build.bat` first to populate `_site-pdf/`; `book.bat` refuses a tree older than its sources rather than rendering the previous book (see [The book refuses a stale source tree](WIP.Build.md#the-book-refuses-a-stale-source-tree)). - `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from; a statement sample also marked `check_run` is built and run, and what it prints is checked against the page. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~110 s over the 1,370 samples marked (88 of them `check_run`). `--build` also builds each project that compiles clean, and `--llvm` builds it with LLVM (see [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md)). Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report <survey.json>` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md). +- `package-tests.bat` --- runs the contributed packages' test projects (`scripts/package_tests.mjs`): each project under `test/package-tests/` is staged with its package embedded and run with `tbrun --tests` as 32-bit and 64-bit, and each case is judged against `expect.json` as *documented*, *fixed* (the callout goes) or *changed* (a claim of the page is no longer true). `--update` writes `expect.json` from the run; read every entry it writes, since each is a claim that the package is defective. A case that never returns is caught and run alone. It also rewrites the generated `RunAll.twin`. Outside every gate and CI, as `examples.bat` is. The build publishes each project, referencing the package as a linked package and carrying none of its code, as `tB/Packages/Contributed/<Package>/<Package>-tests.twinproj` in the online tree (the `packageTests` task, `builder/package-tests.mjs`, `_config.yml`'s `package_tests`; the publish allowlist exempts those paths, and the offline tree's links go to the website). - `addin-test.bat` — tests IDE add-ins by operating an IDE: every lane in `test/addin/lanes.mjs` builds the add-ins it tests into a private copy of the install, opens a project and checks what the add-in does. Outside every gate and outside CI for the same reasons as `examples.bat`; about two minutes for the seventeen lanes today (BETA 997): Samples 10 and 15, the thirteen probe lanes behind Stage 2's answers in [WIP.HelpAddin.md](WIP.HelpAddin.md), and the help add-in in `add-in/` twice, built with its committed copy of the symbol index, `add-in/Resources/SYMBOLS/symbols.json`, which a local build rewrites whenever the index changes. The `help` lane serves the built `docs/_site` on localhost to the add-in's pane; `help-offline` builds the add-in with a help archive of `docs/_site-offline` and the pane's pages come from the add-in's own server. So run `build.bat` first. Exit 0 every lane passed and the registry is as it was found, 1 a lane failed, 2 the harness failed, 3 the registry or a work folder was not put back. See [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) for its rules. - `ide-test.bat` --- the same runner for scenarios that operate the IDE itself rather than an add-in: every lane in `test/ide/lanes.mjs`, base port 9660. Same exit codes, same standing outside every gate and outside CI, same rules. - `try-help-addin.bat` --- opens one IDE on the user's desktop with the help add-in built and loaded, to try it by hand, and puts the registry back once it is closed (see [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler)). Needs `build.bat` first. Exit 0 closed and put back, 1 the add-in or the project did not compile, 2 the tool could not run, 3 not put back. @@ -537,7 +540,7 @@ build.bat && check.bat On the dev box that is ~4 s of build against ~37 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker sits in the build's task graph and its cost; the axe follow-ons are designed there but not implemented. -**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/`, `test/`, `add-in/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Twenty-two of its twenty-five gates cannot be affected by an edit under `docs/` at all. **Three can.** `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `<repo>/docs`, and tokenises every markdown file in it, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep cannot see that fault, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: +**If the change touched `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/`, `test/`, `add-in/`, the site's scripts in `docs/assets/js/`, a wrapper or a workflow, run `test.bat` as well** --- another ~23 s. Twenty-two of its twenty-six gates cannot be affected by an edit under `docs/` at all. **Four can.** `check_package_tests.mjs` reads the callouts of the contributed packages' sections. `check_lint.mjs` lints the site's two scripts in `docs/assets/js/` along with the tooling. `check_gate_lists.mjs` reads `README.md` and every page under `docs/Documentation/`, so an edit to any developer page that states a gate count can fail it. **`check_code_regions.mjs` has two halves, and a content edit reaches only one.** Its corpus sweep reads `DOCS_DIR`, which is `<repo>/docs`, and tokenises every markdown file in it, so a page that provokes a rewrite into *altering* a code region fails it --- that half is content-dependent. Its fixed probes are not: they run against their own sources whatever the tree holds, and they cover the **mirror** fault, where a rewrite silently stops firing. The sweep cannot see that fault, because text the rewrite skipped is masked and restored unchanged and every region still matches. So run `test.bat` after adding an unusual code construct --- a fence whose contents include a fence marker, a 4-space indented block, an admonition wrapping a fence --- and read the built page as well, because for the mirror fault the gate is asserting that the mask and the admonition scan still work rather than checking your page: ```sh build.bat && check.bat && test.bat @@ -583,6 +586,7 @@ wrapper: | `check.bat` | `pick_a11y_sample --check`, `check_a11y` | see [WIP.A11y.md](WIP.A11y.md) | | `test.bat` | `check_code_regions` | no source or HTML rewrite altered a code region, and the rewrites over rendered HTML leave a raw `<pre>` or `<code>` as written; a rewrite still fires on prose beside a fence (the mirror fault); `lib/markdown.mjs` and `lib/frontmatter.mjs` pass their probes, and the block parse finds what the full parse finds; the count-name check skips code and names the file's line; `discover` warns about an unquoted frontmatter value that ends in `#`; the dash normaliser converts only prose | | `test.bat` | `check_regex_safety` | no regex in the tree can backtrack exponentially | +| `test.bat` | `check_package_tests` | every NOTE and WARNING of a contributed section with a test project names its cases in a marker, every case is named, starts with `Expect.Begin` and ends with `Expect.Done`, `expect.json` is for the documented version, `RunAll.twin` is current; a section without a project is in `PENDING`, which only shrinks | | `test.bat` | `check_symbol_index` | the symbol index still places each kind of symbol, from fixtures | | `test.bat` | `check_twin_parsers` | every word of the shared modifier list reaches all three scanners of twinBASIC source; the census's declaration kinds and `parseTargets`' targets hold for the shapes each is known to misread | | `test.bat` | `check_attribute_sweep` | the sweep's site skeletons render and are each in a family of `Applicable to:` targets or listed as in none; its reading of a probe's diagnostics puts the refusals, the control's fold and a skeleton's own errors in the right states; every real `Applicable to:` line reads to its pinned targets; probes batch once each and never two of a once-per-project attribute; a cell missing a form is not reported refused; and the isolating runner, against a scripted fake in place of the IDE, finds a crash, a hang, a disturbed canary or a stray row and stops at each cap | diff --git a/builder/gantt.mjs b/builder/gantt.mjs index edfef537e..190071b84 100644 --- a/builder/gantt.mjs +++ b/builder/gantt.mjs @@ -46,7 +46,7 @@ export const GANTT_SECTION = { deriveRedirects: "Spine", deriveSitemap: "Spine", dispatch: "Render", prepDest: "Render", prepPageDirs: "Render", renderJoin: "Render", flushJoin: "Write", - writeAssets: "Write", searchData: "Write", symbolIndex: "Write", helpArchive: "Write", addinProject: "Write", writeAux: "Write", + writeAssets: "Write", searchData: "Write", symbolIndex: "Write", helpArchive: "Write", addinProject: "Write", packageTests: "Write", writeAux: "Write", writeOffline: "Write", writePdf: "Write", linkJoin: "Check", checkBook: "Check", checkReport: "Check", }; diff --git a/builder/package-tests.mjs b/builder/package-tests.mjs new file mode 100644 index 000000000..c8ac0842d --- /dev/null +++ b/builder/package-tests.mjs @@ -0,0 +1,96 @@ +// The contributed packages' test projects, published as downloads. Each package with a test +// project under test/package-tests/<Package>/ (test/package-tests/README.md) gets +// <Package>-tests.twinproj beside its section, in the online tree, at +// <package_tests.dest>/<Package>/<Package>-tests.twinproj, which the section's landing page +// links to. The offline tree holds no copy: its links to the file go to the website, as a link +// to the help add-in's project file does (offline-rewrite.mjs, computeWebsiteUrl). +// +// The project is put together by lib/package-tests.mjs, as scripts/package_tests.mjs puts the +// one it runs together, except that the package itself is not in it: the project keeps its +// reference to the package and has no Packages/ folder, so the IDE resolves the package as a +// linked one, from the reader's %APPDATA%\twinBASIC\packages (the project's description and the +// landing page say how). So the download holds only the documentation's own code, the shared +// Expect.twin and the package's cases and RunAll module, under the repository's licence. +// +// The pack is impexp's own importProject, in process, as for the add-in's project +// (addin-project.mjs). Line ends are LF in the staged files, which impexp packs with CRLF, so a +// Windows and a Linux checkout write the same bytes. + +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { downloadName, packageTestNames, templateSettings, testSettings, testSources } from "../lib/package-tests.mjs"; +import { importProject } from "../scripts/impexp.mjs"; +import { posix } from "./paths.mjs"; + +/** + * What _config.yml's package_tests declares, resolved against the source tree, or null when it + * declares none (the test fixtures' configs do not). + * + * @returns {{src: string, templates: string, dest: string} | null} + */ +export function packageTestsOf(config, srcRoot) { + const e = config?.package_tests; + if (!e?.src || !e?.templates || !e?.dest) return null; + return { + src: path.resolve(srcRoot, String(e.src)), + templates: path.resolve(srcRoot, String(e.templates)), + dest: posix(String(e.dest)).replace(/^\/+|\/+$/g, ""), + }; +} + +/** The path of each package's download in the online tree. */ +export function packageTestRels(cfg) { + if (!cfg) return []; + return packageTestNames(cfg.src).map((pkg) => `${cfg.dest}/${pkg}/${downloadName(pkg)}`); +} + +/** One package's project file, in memory. */ +export async function packPackageTests(cfg, pkg) { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "package-tests-")); + try { + const stage = path.join(tmp, `${pkg}Tests`); + await fs.mkdir(path.join(stage, "Sources"), { recursive: true }); + const settings = testSettings(templateSettings(cfg.templates, pkg), pkg); + await fs.writeFile(path.join(stage, "Settings"), `${JSON.stringify(settings, null, "\t")}\n`, "utf8"); + for (const f of testSources(cfg.src, pkg)) { + const out = path.join(stage, "Sources", ...f.name.split("/")); + await fs.mkdir(path.dirname(out), { recursive: true }); + await fs.writeFile(out, f.text, "utf8"); + } + const out = path.join(tmp, "project.twinproj"); + importProject(out, stage); + return await fs.readFile(out); + } finally { + await fs.rm(tmp, { recursive: true, force: true }); + } +} + +/** + * Pack every package's test project and write it into the online tree. + * + * @returns {Promise<{written: {rel: string, bytes: number}[], ms: number}>} + */ +export async function writePackageTests(cfg, root) { + const t0 = Date.now(); + const written = []; + for (const pkg of packageTestNames(cfg.src)) { + const rel = `${cfg.dest}/${pkg}/${downloadName(pkg)}`; + const bytes = await packPackageTests(cfg, pkg); + const out = path.join(root, ...rel.split("/")); + await fs.mkdir(path.dirname(out), { recursive: true }); + await fs.writeFile(out, bytes); + written.push({ rel, bytes: bytes.length }); + } + return { written, ms: Date.now() - t0 }; +} + +/** The build's line for the task's result. */ +export function formatPackageTests(r, indent = "") { + if (!r) return ""; + if (r.failed) return `${indent}package tests: not written: ${r.error}\n`; + if (!r.written.length) return ""; + const kb = Math.round(r.written.reduce((n, w) => n + w.bytes, 0) / 1024); + const n = r.written.length; + return `${indent}package tests: ${n} project${n === 1 ? "" : "s"}, ${kb} KB, in the online tree (${r.ms} ms)\n`; +} diff --git a/builder/publish-policy.mjs b/builder/publish-policy.mjs index bde37d4bf..4057a5b60 100644 --- a/builder/publish-policy.mjs +++ b/builder/publish-policy.mjs @@ -79,7 +79,11 @@ export const EXTENSIONLESS_FILENAMES = new Set(["CNAME"]); // the tree surface only: a .twinproj is not a publishable type, and one found // in docs/ is refused even at that path, because the build writes the file // there and a copy in the source tree could only be a stale one. -export function publishPolicyFor(config) { +// +// `generated` adds the other files the build writes itself: the contributed +// packages' test projects (package-tests.mjs), whose paths the build works out +// from test/package-tests/, so a config alone cannot name them. +export function publishPolicyFor(config, { generated: more = [] } = {}) { const declared = new Set(); for (const entry of config?.bundle_extra ?? []) { if (entry?.dest) declared.add(posix(String(entry.dest))); @@ -87,6 +91,7 @@ export function publishPolicyFor(config) { const generated = new Set(); const addin = config?.addin_project; if (addin?.dest) generated.add(posix(String(addin.dest)).replace(/^\/+/, "")); + for (const rel of more) generated.add(posix(String(rel)).replace(/^\/+/, "")); return { declared, generated }; } diff --git a/builder/tbdocs.mjs b/builder/tbdocs.mjs index 705d646f1..a142f252a 100644 --- a/builder/tbdocs.mjs +++ b/builder/tbdocs.mjs @@ -97,6 +97,7 @@ import { writePdf } from "./pdf.mjs"; import { deriveTreeRels } from "./check-tree.mjs"; import { syncAddinIndex } from "./addin-index.mjs"; import { addinProjectOf, formatAddinProject, writeAddinProject } from "./addin-project.mjs"; +import { formatPackageTests, packageTestRels, packageTestsOf, writePackageTests } from "./package-tests.mjs"; import { helpArchiveStep, recheckHelpArchive } from "./help-archive-step.mjs"; import { baselineWrites } from "./baseline.mjs"; import { checkPageBaseline } from "./page-baseline.mjs"; @@ -681,6 +682,8 @@ const TASKS = { // tree holds no copy -- the project carries the offline tree's archive -- // so a link to the file there goes to the website (computeWebsiteUrl). const addin = addinProjectOf(state.site.config); + // The package test projects, which the packageTests task writes, likewise. + const packageTestFiles = packageTestRels(packageTestsOf(state.site.config, ctx.srcRoot)); const themeAssetRels = [ ...enumerateVendoredThemeAssets(), "assets/css/tb-highlight.css", @@ -704,7 +707,7 @@ const TASKS = { stubs, themeAssetRels, excludePatterns, - onlineOnlyRels: addin ? [addin.dest] : [], + onlineOnlyRels: [...(addin ? [addin.dest] : []), ...packageTestFiles], }; const treeNames = skipOffline ? ["online"] : ["online", "offline"]; const treeRels = new Map(treeNames.map((w) => [w, deriveTreeRels(w, common)])); @@ -720,7 +723,7 @@ const TASKS = { // search-data.json) are all minted by the build, not found in docs/. // Runs unconditionally, not under --check: a build with checks off // is exactly when nothing else is watching. - const policy = publishPolicyFor(state.site.config); + const policy = publishPolicyFor(state.site.config, { generated: packageTestFiles }); for (const [which, rels] of treeRels) { const strays = unpublishableTreePaths(rels, policy); if (strays.length) { @@ -1063,6 +1066,26 @@ const TASKS = { submit() {}, }, + // Write the contributed packages' test projects, packed from test/package-tests/, into the + // online tree beside each package's section -- see builder/package-tests.mjs. dispatch put + // their paths in the online tree's index, so checkReport waits for this task. --dry-run + // writes nothing, and a tree whose config declares no package_tests (the test fixtures) + // packs nothing. A failure is reported and fails the build. + packageTests: { + expected: ["prepDest"], + runOnMain: true, + async execute(_, ctx, state) { + const cfg = packageTestsOf(state.site.config, ctx.srcRoot); + if (!cfg || ctx.opts.dryRun) return null; + try { + return await writePackageTests(cfg, ctx.destRoot); + } catch (err) { + return { failed: true, error: err.message }; + } + }, + submit() {}, + }, + // Write redirect stubs + sitemap/robots. Waits for writeAssets (theme on // disk), searchData, deriveRedirects, and deriveSitemap. // Passes searchStats through to writeOffline (for search-data.js). @@ -1281,8 +1304,9 @@ const TASKS = { // the audit failed the build over nothing. // symbolIndex for the same reason: tB/symbols.json is in the online tree's // index, and the audit must not look for it before it is written. And - // addinProject: the add-in's project file is in both trees' indexes. - expected: ["linkJoin", "checkBook", "scss", "symbolIndex", "addinProject"], + // addinProject: the add-in's project file is in both trees' indexes. And + // packageTests: the package test projects are in the online tree's. + expected: ["linkJoin", "checkBook", "scss", "symbolIndex", "addinProject", "packageTests"], runOnMain: true, async execute({ linkJoin: trees, checkBook: book }, ctx, state) { if (!state.checkTrees) return null; @@ -1591,6 +1615,11 @@ export async function runBuild(opts) { process.stdout.write(formatAddinProject(addinResult, label, " ")); if (addinResult.failed) failBuild(); } + const packageTestsResult = results.get("packageTests"); + if (packageTestsResult) { + process.stdout.write(formatPackageTests(packageTestsResult, " ")); + if (packageTestsResult.failed) failBuild(); + } if (offlineResult) { console.log(` ${pc.bold("offline:")} -> ${pc.cyan(`${destRoot}-offline`)}`); console.log( diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 0a1527d77..06642d579 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -875,6 +875,17 @@ Where twinBASIC and VB6 behave differently, say what VB6 does. Check it in VB6 - Use one callout per concern, and only for genuine notes: plain "why this is useful" prose stays a plain paragraph. +A page states what the product does, never what the documentation's own checking could or could not do. "The samples harness cannot capture a console" is a fact about this repository's tools, not about the package, and has no place in a published page. A claim nobody has been able to check is written as the source supports it, or left out. + +### Callouts in a contributed package's section + +Every `> [!NOTE]` and `> [!WARNING]` in a contributed package's section is backed by test cases in the package's test project, `test/package-tests/<Package>/`, and names them in an HTML comment on the line above it: + + <!-- test: FilesTests.Item_MissingName, FilesTests.Item_MissingIndex --> + > [!NOTE] + +An item is a `Fixture.Sub` test case, `compile <Name>` for a compile case, `probe <id>` for a scenario of the package's `probe/` harness, or `none -- <reason>` for a callout no program can check. A case checks what is correct, so a defect of the package is a failing case. The folder's README says how to write one, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), in `test.bat`, fails a callout with no marker and a case no callout names. [`package-tests.bat`](Tools#package-testsbat) runs the cases. The section's landing page ends with a short **Test cases** section that links to the project the build publishes, `<Package>-tests.twinproj`. + ## Document the product as it is twinBASIC is in a long beta, and names still move. **When one does, document the current diff --git a/docs/Documentation/Building.md b/docs/Documentation/Building.md index 34f8514a0..c4d1cb93a 100644 --- a/docs/Documentation/Building.md +++ b/docs/Documentation/Building.md @@ -86,6 +86,7 @@ Do not drop `--check-audit-index` when typing the underlying build command: with && node scripts/check_code_regions.mjs \ && node scripts/check_page_baseline.mjs \ && node scripts/check_book_coverage.mjs \ + && node scripts/check_package_tests.mjs \ && node scripts/check_symbol_index.mjs \ && node scripts/check_twin_parsers.mjs \ && node scripts/check_attribute_sweep.mjs \ diff --git a/docs/Documentation/Images/Tools-Groups.dot b/docs/Documentation/Images/Tools-Groups.dot index 1ef5b79c0..bad47d4ff 100644 --- a/docs/Documentation/Images/Tools-Groups.dot +++ b/docs/Documentation/Images/Tools-Groups.dot @@ -12,7 +12,7 @@ // The clusters side by side in one row must have the same number of node rows. // dot spaces its ranks for the whole graph, so where a shorter cluster ends, // the gap its border needs opens inside its taller neighbour too. The -// invisible nodes (wp, cp1, cp2, ep, dp) pad the shorter ones. +// invisible nodes (cp1, cp2, dp) pad the shorter ones. // // Every node and cluster label is a link, set in the block at the end so the // layout above stays readable: a node to its tool's entry, by the id its @@ -67,8 +67,8 @@ digraph tools_groups { w1 [label="build.bat"] w2 [label="serve.bat"] w3 [label="check.bat"] w4 [label="test.bat"] w5 [label="book.bat"] w6 [label="examples.bat"] w7 [label="addin-test.bat"] w8 [label="ide-test.bat"] w9 [label="try-help-addin.bat"] - wp [style=invis label=""] - w1 -> w4 -> w7 -> wp + w10 [label="package-tests.bat"] + w1 -> w4 -> w7 -> w10 w2 -> w5 -> w8 w3 -> w6 -> w9 { rank=same; w4 w5 w6 } @@ -133,9 +133,9 @@ digraph tools_groups { t19 [label="check_twin_parsers"] t20 [label="check_attribute_sweep"] t21 [label="check_cli"] t22 [label="check_pdf_shims_equiv"] t23 [label="check_impexp_parity"] t24 [label="check_axe_patch_equiv"] - t25 [label="indexer.test"] + t25 [label="indexer.test"] t26 [label="check_package_tests"] t1 -> t5 -> t9 -> t13 -> t17 -> t21 -> t25 - t2 -> t6 -> t10 -> t14 -> t18 -> t22 + t2 -> t6 -> t10 -> t14 -> t18 -> t22 -> t26 t3 -> t7 -> t11 -> t15 -> t19 -> t23 t4 -> t8 -> t12 -> t16 -> t20 -> t24 { rank=same; t5 t6 t7 t8 } @@ -143,6 +143,7 @@ digraph tools_groups { { rank=same; t13 t14 t15 t16 } { rank=same; t17 t18 t19 t20 } { rank=same; t21 t22 t23 t24 } + { rank=same; t25 t26 } } // ---- row 4 ---- @@ -151,8 +152,8 @@ digraph tools_groups { label="Code Samples and Attributes" e1 [label="check_examples"] e2 [label="gen_attribute_probes"] e3 [label="census_attributes"] e4 [label="sweep_attributes"] - ep [style=invis label=""] - e1 -> e3 -> ep + e5 [label="package_tests"] + e1 -> e3 -> e5 e2 -> e4 { rank=same; e3 e4 } } @@ -184,7 +185,7 @@ digraph tools_groups { { rank=same; t1 t2 t3 t4 } { rank=same; e1 e2 i1 i2 d1 } edge [weight=1] - wp -> s1 b7 -> c1 + w10 -> s1 b7 -> c1 s9 -> t1 s10 -> t2 cp2 -> t3 c6 -> t4 t25 -> e1 t22 -> i1 t24 -> d1 @@ -242,6 +243,8 @@ digraph tools_groups { tooltip="Tests the twinBASIC IDE itself by machine: opens a project, operates the IDE and checks what the IDE did."] w9 [URL="#try-help-addinbat" tooltip="Opens an IDE with the help add-in built and loaded, to try by hand, and restores the registry when the IDE is closed. It is not a gate."] + w10 [URL="#package-testsbat" + tooltip="Runs the test projects of the contributed packages and reports each case that no longer does what its expect.json records. It is not a gate."] b1 [URL="Tools-Build#tbdocs" tooltip="The static site generator. Builds the online, offline and PDF trees from docs/ and runs the link and integrity checks."] @@ -344,6 +347,8 @@ digraph tools_groups { tooltip="Checks that the patched axe source computes the same colour values as the stock source, for the color-contrast rule."] t25 [URL="Tools-Toolchain-Tests#indexer-test" tooltip="Runs unit tests for the package indexer: VB6 preprocessing, package files, the api.json model and the differ."] + t26 [URL="Tools-Toolchain-Tests#check-package-tests" + tooltip="Verifies that every NOTE and WARNING of a contributed package's section names the test cases behind it, and that each package test project is in order."] e1 [URL="Tools-Samples#check-examples" tooltip="Compiles the code samples marked check_build and reports each diagnostic against the page line it came from. Samples marked check_run are also run."] @@ -353,6 +358,8 @@ digraph tools_groups { tooltip="Reports which enclosing construct and which kind of declaration each attribute decorates in the .twin sources of the shipped packages."] e4 [URL="Tools-Samples#sweep-attributes" tooltip="Writes each attribute at each declaration site, asks the compiler where it is legal, and compares the answers with the Applicable to lines."] + e5 [URL="Tools-Samples#package-tests" + tooltip="Runs the test projects of the contributed packages with tbrun and judges each case against the package's expect.json: documented, fixed or changed."] i1 [URL="Tools-IDE#addin-test" tooltip="Runs the add-in scenarios under test/addin/, each lane in its own process with a private copy of the twinBASIC install."] diff --git a/docs/Documentation/Images/Tools-Groups.svg b/docs/Documentation/Images/Tools-Groups.svg index 82c5cece7..bf202f84e 100644 --- a/docs/Documentation/Images/Tools-Groups.svg +++ b/docs/Documentation/Images/Tools-Groups.svg @@ -8,62 +8,62 @@ <g id="clust1" class="cluster"> <title>cluster_wrappers - -Batch wrappers + +Batch wrappers cluster_build - -Build and Generation + +Build and Generation cluster_site - -Site Checks + +Site Checks cluster_compiler - -Compiler Harness + +Compiler Harness cluster_tests - -Toolchain Tests + +Toolchain Tests - + cluster_samples - + Code Samples and Attributes - + cluster_ide - + IDE Automation - + cluster_defects - + Defect Reproducers @@ -73,8 +73,8 @@ w1 - -build.bat + +build.bat @@ -82,8 +82,8 @@ w2 - -serve.bat + +serve.bat @@ -91,8 +91,8 @@ w3 - -check.bat + +check.bat @@ -100,8 +100,8 @@ w4 - -test.bat + +test.bat @@ -109,8 +109,8 @@ w5 - -book.bat + +book.bat @@ -118,8 +118,8 @@ w6 - -examples.bat + +examples.bat @@ -127,8 +127,8 @@ w7 - -addin-test.bat + +addin-test.bat @@ -136,8 +136,8 @@ w8 - -ide-test.bat + +ide-test.bat @@ -145,18 +145,26 @@ w9 - -try-help-addin.bat + +try-help-addin.bat + + + + + +w10 + + +package-tests.bat - b1 - -tbdocs + +tbdocs @@ -164,8 +172,8 @@ b2 - -render-book + +render-book @@ -173,8 +181,8 @@ b3 - -compare_trees + +compare_trees @@ -182,8 +190,8 @@ b4 - -survey_tooling + +survey_tooling @@ -191,8 +199,8 @@ b5 - -convert_em_dash_separators + +convert_em_dash_separators @@ -200,8 +208,8 @@ b6 - -build_fonts.py + +build_fonts.py @@ -209,8 +217,8 @@ b7 - -build_dot_metrics + +build_dot_metrics @@ -218,8 +226,8 @@ b8 - -twin-index + +twin-index @@ -227,8 +235,8 @@ s1 - -check_links + +check_links @@ -236,8 +244,8 @@ s2 - -crawl_check + +crawl_check @@ -245,8 +253,8 @@ s3 - -check_links_diff + +check_links_diff @@ -254,8 +262,8 @@ s4 - -check_tree_fresh + +check_tree_fresh @@ -263,8 +271,8 @@ s5 - -check_dot_fit + +check_dot_fit @@ -272,8 +280,8 @@ s6 - -check_a11y + +check_a11y @@ -281,8 +289,8 @@ s7 - -pick_a11y_sample + +pick_a11y_sample @@ -290,8 +298,8 @@ s8 - -axe-scan + +axe-scan @@ -299,8 +307,8 @@ s9 - -check_a11y_fingerprint + +check_a11y_fingerprint @@ -308,8 +316,8 @@ s10 - -sweep_a11y + +sweep_a11y @@ -317,8 +325,8 @@ c1 - -tbbuild + +tbbuild @@ -326,8 +334,8 @@ c2 - -tbrun + +tbrun @@ -335,8 +343,8 @@ c3 - -impexp.mjs, impexp.py + +impexp.mjs, impexp.py @@ -344,8 +352,8 @@ c4 - -check_tb_registry + +check_tb_registry @@ -353,8 +361,8 @@ c5 - -probe_shared_temp + +probe_shared_temp @@ -362,8 +370,8 @@ c6 - -probe_build_twice + +probe_build_twice @@ -373,8 +381,8 @@ t1 - -check_publish_policy + +check_publish_policy @@ -382,8 +390,8 @@ t2 - -check_regex_safety + +check_regex_safety @@ -391,8 +399,8 @@ t3 - -check_code_regions + +check_code_regions @@ -400,8 +408,8 @@ t4 - -check_gate_lists + +check_gate_lists @@ -409,8 +417,8 @@ t5 - -check_ci_workflows + +check_ci_workflows @@ -418,8 +426,8 @@ t6 - -check_lint + +check_lint @@ -427,8 +435,8 @@ t7 - -search.test + +search.test @@ -436,8 +444,8 @@ t8 - -render.test + +render.test @@ -445,8 +453,8 @@ t9 - -strftime.test + +strftime.test @@ -454,8 +462,8 @@ t10 - -png.test + +png.test @@ -463,8 +471,8 @@ t11 - -compact-image.test + +compact-image.test @@ -472,8 +480,8 @@ t12 - -svgshot.test + +svgshot.test @@ -481,8 +489,8 @@ t13 - -example-batches.test + +example-batches.test @@ -490,8 +498,8 @@ t14 - -ports.test + +ports.test @@ -499,8 +507,8 @@ t15 - -addin-project.test + +addin-project.test @@ -508,8 +516,8 @@ t16 - -check_page_baseline + +check_page_baseline @@ -517,8 +525,8 @@ t17 - -check_book_coverage + +check_book_coverage @@ -526,8 +534,8 @@ t18 - -check_symbol_index + +check_symbol_index @@ -535,8 +543,8 @@ t19 - -check_twin_parsers + +check_twin_parsers @@ -544,8 +552,8 @@ t20 - -check_attribute_sweep + +check_attribute_sweep @@ -553,8 +561,8 @@ t21 - -check_cli + +check_cli @@ -562,8 +570,8 @@ t22 - -check_pdf_shims_equiv + +check_pdf_shims_equiv @@ -571,8 +579,8 @@ t23 - -check_impexp_parity + +check_impexp_parity @@ -580,8 +588,8 @@ t24 - -check_axe_patch_equiv + +check_axe_patch_equiv @@ -589,135 +597,152 @@ t25 - -indexer.test + +indexer.test - + +t26 + + +check_package_tests + + + + + e1 - + check_examples - + e2 - + gen_attribute_probes - + e3 - + census_attributes - + e4 - + sweep_attributes - - + +e5 + + +package_tests + + + + + i1 - + addin_test - + i2 - + ide_test - + i3 - + try_help_addin - + i4 - + shoot_docs - + i5 - + imagestrip - + i6 - + probe_ide_image_labels - + i7 - + svgshot/replay - + i8 - + probe_theme_semicolon - + d1 - - -bug_repro + + +bug_repro - + d2 - - -vb6run + + +vb6run @@ -728,8 +753,8 @@ - - + + @@ -776,12 +801,13 @@ + - + diff --git a/docs/Documentation/Tools-Samples.md b/docs/Documentation/Tools-Samples.md index 69f2b0572..373510bbc 100644 --- a/docs/Documentation/Tools-Samples.md +++ b/docs/Documentation/Tools-Samples.md @@ -103,6 +103,27 @@ Four files under `scripts/lib/` belong to the tool: Exit codes: **0** every marked sample compiles, or none is marked (`--report` always, and `--propose` when it found only unmarked samples that fail, which is advisory); **1** a marked sample does not compile, a marker is misused, a template does not compile, the compiler crashed on a project, `--build` or `--llvm` found a sample that fails the build, or a `check_run` sample raised an error, did not return or printed something other than the page says (the report names each); **2** the harness could not run: a refused command line, a failed self-test probe, no IDE, an unreadable `--report` file, a work folder it could not clear, an `--llvm` run on a Community or Personal licence, or a crash. +## package_tests.mjs +{: #package-tests } + + node scripts/package_tests.mjs [ ...] [--arch win32|win64|both] [--port N] [--timeout S] [--hang-timeout S] [--update] [--ide ] [--json] + +Runs the contributed packages' test projects. Each package the reference documents under `docs/Reference/Contributed/` has one in `test/package-tests//`: `[TestCase]` Subs of `[TestFixture]` modules that check every claim the NOTEs and WARNINGs of its section make, a generated `RunAll` module, and `expect.json`. The README in that folder says how a case is written. Each case checks what is correct, so a defect of the package is a failing case, and `expect.json` records how each case fails, or that it never returns, on the version the section documents. + +For each package and architecture (both by default), the tool stages the project with the package embedded, downloading it from TWINSERV into `%TEMP%\tb-twinserv` the first time, and runs it with [`tbrun --tests`](Tools-Compiler#tbrun). Each case is then judged: + +- **documented** --- as `expect.json` says; +- **fixed** --- `expect.json` says it fails or never returns, and it passes: the page's callout and the entry go; +- **changed** --- anything else: a claim of the page is no longer true, or the case is wrong. + +A case that never returns stops the compiler. The tool catches it, runs the cases after it again in a new IDE, and from then on, once `expect.json` says so, runs it alone with `--hang-timeout`. `--update` writes `expect.json` from the run, for the architectures run. The run also writes `RunAll.twin` again whenever the cases or `expect.json` change; [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests) fails while it is out of date. + +The build publishes each project as `-tests.twinproj` beside the package's section, and the section's landing page links to it. It carries none of the package's code: it references the package as a [linked package](../../Features/Packages/Linked), which the IDE loads from `%APPDATA%\twinBASIC\packages`. With the package's file there, the project's `Sub Main` runs every case and prints `PASS`, `FAIL` or `SKIP` for each in the DEBUG CONSOLE. + +It is outside every gate and CI: it needs Windows and a twinBASIC install. [`package-tests.bat`](Tools#package-testsbat) runs it. + +Exit codes: **0** every case did what `expect.json` says; **1** a case was fixed or changed; **2** a refused command line, a package with no test project, or the harness failed. + ## gen_attribute_probes.mjs {: #gen-attribute-probes } diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md index a9ccb4b77..917504449 100644 --- a/docs/Documentation/Tools-Toolchain-Tests.md +++ b/docs/Documentation/Tools-Toolchain-Tests.md @@ -355,6 +355,24 @@ Eight probes give each of the five findings a fault to report: a page with no en Exit codes: **0** every probe passed, **1** a probe failed, **2** the gate could not run: a refused command line, or a crash. +## check_package_tests.mjs +{: #check-package-tests } + + node scripts/check_package_tests.mjs + +Verifies that every NOTE and WARNING of a contributed package's section names the test cases behind it, and that each [package test project](Tools-Samples#package-tests) is in order. It reads only the sources --- the section's pages, the project's files and the sample template that names the package's version --- so it needs no install, no browser and no built tree, and takes well under a second. + +A callout names its cases in an HTML comment on the line above it: + + + > [!WARNING] + +Each item is a `Fixture.Sub` for a `[TestCase]`, `compile ` for a compile case, `probe ` for a scenario of the package's `probe/` harness, or `none -- ` for a callout no program can check. For a package with a test project, the gate fails when a callout has no marker, when an item names nothing that exists, when a case is named by no callout, when a `[TestCase]` does not start with `Expect.Begin` and end with `Expect.Done`, when `expect.json` is for another version than the section's `indexed_from` or names a case that does not exist, and when the generated `RunAll.twin` is out of date. A section with no test project yet is listed in the script's `PENDING` set; the gate fails on a name there with a project, and on a section neither there nor with one, so the list only shrinks. + +Eleven probes run first, on pages, sources and an `expect.json` built in memory: how a marker and its items are read, the RunAll module, how `expect.json` is read and written, and how a case's result is judged. + +Exit codes: **0** every probe passed and every test project is in order, **1** a probe failed, or a section or test project is not in order, **2** the gate could not run: a refused command line, or a crash. + ## check_symbol_index.mjs {: #check-symbol-index } diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md index d571bcc3f..c14a261ba 100644 --- a/docs/Documentation/Tools.md +++ b/docs/Documentation/Tools.md @@ -46,9 +46,9 @@ A tool that reads its command line through `lib/cli.mjs` and takes search terms, ## Batch wrappers at the repository root {: #batch-wrappers } -All nine sit at the repository root, beside `package.json`. Each uses `@pushd "%~dp0"` to run from that root wherever it is invoked, and each entry below gives the POSIX equivalent of what it runs. The equivalents have no `pushd`, so **run them from the repository root**: `tbdocs`'s `--src docs`, [`check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy)'s default source root and every path given to [`render-book.mjs`](Tools-Build#bookrender-bookmjs) resolve against the working directory. +All ten sit at the repository root, beside `package.json`. Each uses `@pushd "%~dp0"` to run from that root wherever it is invoked, and each entry below gives the POSIX equivalent of what it runs. The equivalents have no `pushd`, so **run them from the repository root**: `tbdocs`'s `--src docs`, [`check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy)'s default source root and every path given to [`render-book.mjs`](Tools-Build#bookrender-bookmjs) resolve against the working directory. -`examples.bat`, `addin-test.bat`, `ide-test.bat` and `try-help-addin.bat` have no POSIX equivalent. They need a twinBASIC install and drive the IDE, so they run only on Windows and are not part of the site build. Four other tools are Windows-specific for the same reason and are not part of the build either: +`examples.bat`, `package-tests.bat`, `addin-test.bat`, `ide-test.bat` and `try-help-addin.bat` have no POSIX equivalent. They need a twinBASIC install and drive the IDE, so they run only on Windows and are not part of the site build. Four other tools are Windows-specific for the same reason and are not part of the build either: - [`scripts/tbbuild.mjs`](Tools-Compiler#tbbuild) and [`scripts/tbrun.mjs`](Tools-Compiler#tbrun) drive the twinBASIC IDE. - [`census_attributes.mjs`](Tools-Samples#census-attributes) unpacks the packages of a twinBASIC install. It is cross-platform when given an already-exported tree with `--exported`. @@ -110,7 +110,7 @@ A local pass of [`check_a11y.mjs`](Tools-Site-Checks#check-a11y) does not predic test.bat -The tests the toolchain has to pass. Twenty-five steps, each stopping the run if it fails: +The tests the toolchain has to pass. Twenty-six steps, each stopping the run if it fails: 1. [`scripts/check_publish_policy.mjs`](Tools-Toolchain-Tests#check-publish-policy) --- verifies the publish allowlist still refuses the types it should. Needs neither a browser nor a built tree. 2. [`scripts/check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists) --- verifies the two gate lists on this page still match the wrappers that run them. @@ -130,13 +130,14 @@ The tests the toolchain has to pass. Twenty-five steps, each stopping the run if 16. [`scripts/check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions) --- verifies no pre-render rewrite alters the contents of a code fence or code span, that the rewrites over rendered HTML leave a raw `
` or `` alone, and that `lib/markdown.mjs` and `lib/frontmatter.mjs` pass their probes.
 17. [`scripts/check_page_baseline.mjs`](Tools-Toolchain-Tests#check-page-baseline) --- verifies the page-count drift guard still refuses a fall.
 18. [`scripts/check_book_coverage.mjs`](Tools-Toolchain-Tests#check-book-coverage) --- verifies the build still warns about a page `docs/_book.yml` does not mention.
-19. [`scripts/check_symbol_index.mjs`](Tools-Toolchain-Tests#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL.
-20. [`scripts/check_twin_parsers.mjs`](Tools-Toolchain-Tests#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread.
-21. [`scripts/check_attribute_sweep.mjs`](Tools-Toolchain-Tests#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`.
-22. [`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors.
-23. [`scripts/check_pdf_shims_equiv.mjs`](Tools-Toolchain-Tests#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, have each of them in their shim's table of targets, and run.
-24. [`scripts/check_impexp_parity.mjs`](Tools-Toolchain-Tests#check-impexp-parity) --- verifies the two editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI.
-25. [`scripts/check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values.
+19. [`scripts/check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests) --- verifies every NOTE and WARNING of a contributed package's section names the test cases behind it, and that each package test project is in order.
+20. [`scripts/check_symbol_index.mjs`](Tools-Toolchain-Tests#check-symbol-index) --- verifies the symbol index still places each kind of symbol, and its drift guard still refuses a lost URL.
+21. [`scripts/check_twin_parsers.mjs`](Tools-Toolchain-Tests#check-twin-parsers) --- verifies the scanners of twinBASIC source and of the attribute reference still read the shapes each once misread.
+22. [`scripts/check_attribute_sweep.mjs`](Tools-Toolchain-Tests#check-attribute-sweep) --- verifies the logic of the attribute sweep: its site skeletons, how it reads a probe's diagnostics, how it batches probes, and how it compares the answers with `Attributes.md`.
+23. [`scripts/check_cli.mjs`](Tools-Toolchain-Tests#check-cli) --- verifies `lib/cli.mjs`, the command-line parser, and each tool's recorded command-line errors.
+24. [`scripts/check_pdf_shims_equiv.mjs`](Tools-Toolchain-Tests#check-pdf-shims-equiv) --- verifies the book's pdf-lib shims write what stock pdf-lib writes, patch the members of pdf-lib it lists, have each of them in their shim's table of targets, and run.
+25. [`scripts/check_impexp_parity.mjs`](Tools-Toolchain-Tests#check-impexp-parity) --- verifies the two editions of the impexp tool pass the same built-in tests, and exit, print and write the same for one sequence of commands. Without Python it reports itself skipped and passes, except in CI.
+26. [`scripts/check_axe_patch_equiv.mjs`](Tools-Toolchain-Tests#check-axe-patch-equiv) --- verifies the vendored axe source patch still produces identical colour values.
 
 POSIX:
 
@@ -158,6 +159,7 @@ POSIX:
       && node scripts/check_code_regions.mjs \
       && node scripts/check_page_baseline.mjs \
       && node scripts/check_book_coverage.mjs \
+      && node scripts/check_package_tests.mjs \
       && node scripts/check_symbol_index.mjs \
       && node scripts/check_twin_parsers.mjs \
       && node scripts/check_attribute_sweep.mjs \
@@ -168,9 +170,9 @@ POSIX:
 
 Exit codes: **0** every step passed; otherwise the code of the step that stopped the run, as that step's entry gives it.
 
-**Twenty-two of the twenty-five cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a wrapper, or a workflow. Both CI workflows run all twenty-five unconditionally.
+**Twenty-two of the twenty-six cannot be affected by an edit confined to `docs/`**, which is why they are separate from `check.bat`. Run this one when the change touches `builder/`, `scripts/`, `lib/`, `book/`, `eval/`, `wisdom/`, `indexer/` or `test/`, the site's scripts in `docs/assets/js/`, the help add-in's source in `add-in/`, a contributed package's section, a wrapper, or a workflow. Both CI workflows run all twenty-six unconditionally.
 
-The three exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, and [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`.
+The four exceptions are [`check_code_regions.mjs`](Tools-Toolchain-Tests#check-code-regions), [`check_gate_lists.mjs`](Tools-Toolchain-Tests#check-gate-lists), which reads this page, [`check_lint.mjs`](Tools-Toolchain-Tests#check-lint), which lints the site's scripts in `docs/assets/js/`, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), which reads the callouts of the contributed packages' sections.
 
 `check_code_regions.mjs` has two halves. Its corpus sweep tokenises every markdown file under `docs/`, so a page that provokes a rewrite into altering a code region fails it. Its fixed probes run against their own sources whatever the tree holds, and cover the *mirror* fault, where a rewrite silently stops firing; the sweep cannot see that one. After adding a page with an unusual code construct, run `test.bat` and read the built page too.
 
@@ -239,6 +241,21 @@ Compiles the documentation's own twinBASIC code samples --- every ` ```tb ` fenc
 
 Exit codes, those of [`check_examples.mjs`](Tools-Samples#check-examples): **0** clean, **1** a sample does not compile, or does not run as its page says, **2** the harness failed.
 
+### package-tests.bat
+{: #package-testsbat }
+
+    package-tests.bat [ ...] [flags]
+
+One invocation of [`package_tests.mjs`](Tools-Samples#package-tests), with every argument passed straight through:
+
+    node scripts/package_tests.mjs [ ...] [flags]
+
+Runs the contributed packages' test projects under `test/package-tests/`, which back the NOTEs and WARNINGs of each package's section with test cases, and says which cases no longer do what the package's `expect.json` records: fixed, so the page's callout goes, or changed, so a claim of the page is no longer true.
+
+**It is not one of the gates**, for the reasons `examples.bat` is not, and is absent from `build.bat`, `check.bat`, `test.bat` and both CI workflows. The gate that ties each callout to its cases, [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), needs no install and is in `test.bat`.
+
+Exit codes, those of [`package_tests.mjs`](Tools-Samples#package-tests): **0** every case as documented, **1** a case fixed or changed, **2** the harness failed.
+
 ### addin-test.bat
 {: #addin-testbat }
 
diff --git a/docs/_config.yml b/docs/_config.yml
index 81c50e863..0b86eb263 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -125,6 +125,16 @@ addin_project:
   src: ../add-in
   dest: tB/IDE/AddIns/downloads/tbDocsHelp.twinproj
 
+# The contributed packages' test projects, which the build packs from src (one folder per
+# package, relative to docs/) with the Settings of the package's sample template in templates,
+# and writes into the online tree as //-tests.twinproj, beside the
+# package's section. The publish allowlist exempts those paths, as it does addin_project's. See
+# builder/package-tests.mjs and test/package-tests/README.md.
+package_tests:
+  src: ../test/package-tests
+  templates: ../test/example-projects
+  dest: tB/Packages/Contributed
+
 # Search index granularity and generic-section folding. See WIP.Search.md's
 # "Design" §1 for why: splitting at h3 instead of h2 finds API members
 # (`### PaintPicture`) as their own entries, and folding boilerplate
diff --git a/lib/package-tests.mjs b/lib/package-tests.mjs
new file mode 100644
index 000000000..579e50c9d
--- /dev/null
+++ b/lib/package-tests.mjs
@@ -0,0 +1,131 @@
+// The documentation's package test projects, test/package-tests//: what goes into each
+// one. Two users: scripts/package_tests.mjs, which runs them with the package embedded, and the
+// build's packageTests task (builder/package-tests.mjs), which publishes each one without the
+// package's code as -tests.twinproj beside the package's section, for a reader or the
+// package's author to open with the package linked, and run. test/package-tests/README.md says
+// how a project is laid out.
+//
+// A project is three things put together: the Settings of the package's sample template
+// (test/example-projects/contributed-/Settings), which names the version the section
+// documents; the shared Expect.twin; and the package folder's own Sources/, its [TestFixture]
+// modules and the RunAll module generated from them.
+
+import { createHash } from "node:crypto";
+import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
+import path from "node:path";
+
+export const EXPECT_FILE = "Expect.twin";
+export const RUNALL_FILE = "RunAll.twin";
+export const EXPECT_JSON = "expect.json";
+
+/** The file name of a package's published test project. */
+export const downloadName = (pkg) => `${pkg}-tests.twinproj`;
+
+// The Assert package is a compiler package, part of every install. Expect.Done ends a failed
+// case with Assert.Strict.Fail when a runner runs one case at a time.
+const ASSERT_REFERENCE = {
+  id: "{9847A10D-8987-44B2-8B87-EA77DEF7F1D9}",
+  isCompilerPackage: true,
+  licence: "MIT",
+  name: "[COMPILER PACKAGE] Assert",
+  path32: "",
+  path64: "",
+  publisher: "TWINBASIC-COMPILER",
+  symbolId: "Assert",
+  versionBuild: 0,
+  versionMajor: 0,
+  versionMinor: 0,
+  versionRevision: 24,
+};
+
+const isEmbedded = (r) => /^\/Packages\/[^/\\]+$/.test(typeof r.path32 === "string" ? r.path32 : "");
+
+/** The packages that have a test project under `root`, sorted. */
+export function packageTestNames(root) {
+  if (!existsSync(root)) return [];
+  return readdirSync(root, { withFileTypes: true })
+    .filter((d) => d.isDirectory() && existsSync(path.join(root, d.name, "Sources")))
+    .map((d) => d.name)
+    .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
+}
+
+/** The template's Settings, parsed. */
+export function templateSettings(templatesDir, pkg) {
+  const file = path.join(templatesDir, `contributed-${pkg}`, "Settings");
+  if (!existsSync(file)) throw new Error(`no sample template for ${pkg}: ${file} is missing`);
+  return JSON.parse(readFileSync(file, "utf8"));
+}
+
+/** The version of the package that the template references, as a.b.c.d. */
+export function packageVersion(settings, pkg) {
+  const r = (settings["project.references"] ?? []).find((x) => isEmbedded(x) && x.symbolId === pkg);
+  if (!r) throw new Error(`the ${pkg} template references no embedded package named ${pkg}`);
+  return [r.versionMajor, r.versionMinor, r.versionRevision, r.versionBuild].map(Number).join(".");
+}
+
+/**
+ * The test project's Settings. The reference to the package is the template's, which names the
+ * package by its project id. With the package's tree in the project's Packages/ folder it is
+ * embedded (the runner puts it there); without it the IDE resolves it as a linked package, from
+ * the .twinpack in %APPDATA%\twinBASIC\packages with that project id, once the compiler has
+ * (re)started. The published project has no Packages/ folder, so it carries none of the
+ * package's code. The IDE reads the version from that file, not from the reference.
+ *
+ * @param {object} template  the template's Settings
+ * @param {string} pkg
+ */
+export function testSettings(template, pkg) {
+  const version = packageVersion(template, pkg);
+  const s = structuredClone(template);
+  const refs = [...(s["project.references"] ?? [])];
+  if (!refs.some((r) => r.symbolId === "Assert")) refs.push({ ...ASSERT_REFERENCE });
+  s["project.references"] = refs;
+  const name = `${pkg.replace(/\W/g, "")}Tests`;
+  s["project.name"] = name;
+  s["project.appTitle"] = name;
+  s["project.id"] = guidFor(pkg);
+  s["project.description"] =
+    `The test cases of the twinBASIC documentation for the ${pkg} package, version ${version}, ` +
+    `which the project references as a linked package: put ${pkg}'s .twinpack of that version in ` +
+    `%APPDATA%\\twinBASIC\\packages and restart the compiler, then run Sub Main.`;
+  s["project.buildPath"] = "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}";
+  s["project.startupObject"] = "Sub Main";
+  return s;
+}
+
+/**
+ * The project's source files: Expect.twin, then the package folder's Sources/ in name order,
+ * each with LF line ends.
+ *
+ * @returns {{name: string, text: string}[]}  `name` is the path under Sources/
+ */
+export function testSources(root, pkg) {
+  const lf = (t) => t.replace(/\r\n?/g, "\n");
+  const files = [{ name: EXPECT_FILE, text: lf(readFileSync(path.join(root, EXPECT_FILE), "utf8")) }];
+  const dir = path.join(root, pkg, "Sources");
+  const walk = (rel) => {
+    for (const e of readdirSync(path.join(dir, rel), { withFileTypes: true }).sort((a, b) =>
+      a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
+    )) {
+      const r = rel ? `${rel}/${e.name}` : e.name;
+      if (e.isDirectory()) walk(r);
+      else if (statSync(path.join(dir, r)).isFile())
+        files.push({ name: r, text: lf(readFileSync(path.join(dir, r), "utf8")) });
+    }
+  };
+  walk("");
+  return files;
+}
+
+/** The package's expect.json, parsed, or an empty one. */
+export function readExpect(root, pkg) {
+  const file = path.join(root, pkg, EXPECT_JSON);
+  if (!existsSync(file)) return { cases: {} };
+  return JSON.parse(readFileSync(file, "utf8"));
+}
+
+// A project id of its own for each package, the same on every machine.
+function guidFor(pkg) {
+  const h = createHash("sha256").update(`twinBASIC documentation package tests: ${pkg}`).digest("hex");
+  return `{${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-9${h.slice(17, 20)}-${h.slice(20, 32)}}`.toUpperCase();
+}
diff --git a/package-tests.bat b/package-tests.bat
new file mode 100644
index 000000000..14241786b
--- /dev/null
+++ b/package-tests.bat
@@ -0,0 +1,28 @@
+@echo off
+@pushd "%~dp0"
+rem Run the contributed packages' test projects (test\package-tests\\).
+rem
+rem Each NOTE and WARNING of a contributed package's section is backed by test
+rem cases in that package's project. A case checks what is correct, so a
+rem defect of the package is a failing case, and expect.json records how each
+rem case fails on the version the section documents. This runs every case,
+rem with the package embedded, as 32-bit and 64-bit, and says which ones no
+rem longer do what expect.json says: fixed (the page's callout goes) or
+rem changed (a claim of the page is no longer true).
+rem
+rem THIS IS NOT ONE OF THE GATES, for the reasons examples.bat is not: it
+rem needs a twinBASIC install, Windows and a private desktop. The gate that
+rem ties the pages' callouts to the cases, check_package_tests.mjs, needs
+rem none of that and is in test.bat. Arguments are passed straight through:
+rem
+rem   package-tests.bat                       every project
+rem   package-tests.bat tbRegExp --arch win32 one package, one architecture
+rem   package-tests.bat tbRegExp --update     write its expect.json from the run
+rem
+rem Exit: 0 every case as documented, 1 a case fixed or changed, 2 the harness
+rem failed.
+node scripts/package_tests.mjs %*
+@rem popd resets ERRORLEVEL, so capture it first.
+@set "PACKAGE_TESTS_ERR=%ERRORLEVEL%"
+@popd
+@exit /b %PACKAGE_TESTS_ERR%
diff --git a/scripts/check_gate_lists.mjs b/scripts/check_gate_lists.mjs
index 4ede5e0c6..2bdfaca16 100644
--- a/scripts/check_gate_lists.mjs
+++ b/scripts/check_gate_lists.mjs
@@ -93,7 +93,8 @@ const NUMBER_WORDS = [
   "zero", "one", "two", "three", "four", "five",
   "six", "seven", "eight", "nine", "ten", "eleven", "twelve",
   "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty",
-  "twenty-one", "twenty-two", "twenty-three", "twenty-four", "twenty-five",
+  "twenty-one", "twenty-two", "twenty-three", "twenty-four", "twenty-five", "twenty-six",
+  "twenty-seven", "twenty-eight", "twenty-nine", "thirty",
 ];
 // Longest first, so that "twenty-one" is not read as "twenty" where a word
 // boundary follows the number, as in "of the twenty-one".
diff --git a/scripts/check_package_tests.mjs b/scripts/check_package_tests.mjs
new file mode 100644
index 000000000..4791fc6b1
--- /dev/null
+++ b/scripts/check_package_tests.mjs
@@ -0,0 +1,373 @@
+// Gate: every NOTE and WARNING of a contributed package's section is tied to the test cases
+// behind it, and every package test project is in order.
+//
+// A contributed package's test project (test/package-tests//, README there) holds a
+// case for each claim the section's callouts make. A callout names its cases in an HTML comment
+// on the line above it:
+//
+//     
+//     > [!WARNING]
+//
+// with `Fixture.Sub` for a [TestCase], `compile ` for a compile case, `probe ` for a
+// scenario of the package's probe/ harness, or `none -- ` for a callout no program can
+// check. This gate reads only the sources: the section's pages, the project's files and the
+// template that names the package's version. It needs no twinBASIC install; running the cases is
+// scripts/package_tests.mjs.
+//
+// For a package with a test project it checks that:
+//   * every callout of the section's pages has a marker, and every item of it names something
+//     that exists;
+//   * every case is named by some marker;
+//   * every [TestCase] Sub starts with Expect.Begin and ends with Expect.Done, and every
+//     [TestCase] can be called;
+//   * expect.json is for the version the section documents (its indexed_from, which the sample
+//     template references) and names only cases that exist;
+//   * RunAll.twin is what scripts/package_tests.mjs would write;
+//   * the project does not carry its own copy of Expect.twin.
+// A section listed in PENDING has no test project yet and is not checked; one listed there that
+// has one, or one not listed that has none, fails the gate, so the list only shrinks.
+//
+// Probes of the gate's own reading (markers, RunAll, expect.json, verdicts) run first, on inputs
+// built here, so the gate means the same against a tree with no test project at all.
+//
+//     node scripts/check_package_tests.mjs
+
+import { existsSync, readFileSync } from "node:fs";
+import path from "node:path";
+import { exitOnCrash, parseCli, printHelpAndExit, withUsageError } from "../lib/cli.mjs";
+import { parseFrontmatter } from "../lib/frontmatter.mjs";
+import { markdownFiles } from "../lib/markdown-files.mjs";
+import {
+  EXPECT_FILE,
+  EXPECT_JSON,
+  packageTestNames,
+  packageVersion,
+  RUNALL_FILE,
+  templateSettings,
+  testSources,
+} from "../lib/package-tests.mjs";
+import { REPO_ROOT } from "../lib/repo-paths.mjs";
+import { createProbes } from "./lib/gate-probes.mjs";
+import {
+  calloutsOf,
+  expectationFor,
+  expectFrom,
+  judge,
+  malformedCases,
+  outcomeOf,
+  parseMarker,
+  projectCases,
+  runAllText,
+} from "./lib/package-test-cases.mjs";
+
+exitOnCrash();
+
+const USAGE = `usage: node scripts/check_package_tests.mjs [-h, --help]
+
+Checks that every NOTE and WARNING of a contributed package's section names the
+test cases behind it, and that each package test project under
+test/package-tests/ is in order: its cases named, well formed, in expect.json
+for the documented version, and in its generated RunAll module.
+
+  -h, --help  print this text and exit
+
+Exit codes:
+  0  every probe passed and every test project is in order
+  1  a probe failed, or a section or test project is not in order
+  2  the gate could not run: a refused command line, or a crash`;
+
+if (
+  withUsageError(() =>
+    parseCli(process.argv.slice(2), {
+      options: { help: { type: "boolean", short: "h" } },
+      stopAt: ["help"],
+    }),
+  ).values.help
+)
+  printHelpAndExit(USAGE);
+
+// The contributed sections that have no test project yet. Each package's project removes it
+// from here; the gate fails on a name here with a project, and on a section neither here nor
+// with one.
+const PENDING = new Set([
+  "ArrayListLib",
+  "AutoCompleteLib",
+  "BigNumberLib",
+  "FileProps",
+  "Fmt",
+  "Generics",
+  "OLEGuids",
+  "TBMANLIB",
+  "TBMANLIB_Aes",
+  "TBMANLIB_ConsoleComponents",
+  "TBMANLIB_ConsoleCore",
+  "TBMANLIB_Diagnostic",
+  "TBMANLIB_Dialog",
+  "TBMANLIB_ListBoxHScroll",
+  "TBMANLIB_RES",
+  "TBMANLIB_Xml",
+  "TwinTimerLib",
+  "VBA_JSON",
+  "VBComDlg",
+  "WinReg",
+  "tbFileSysTools",
+  "tbProjPacker",
+  "tbRegExp",
+  "tsJson",
+  "tsPwGen",
+  "xCalendar",
+]);
+
+const DOCS = path.join(REPO_ROOT, "docs");
+const SECTIONS = path.join(DOCS, "Reference", "Contributed");
+const ROOT = path.join(REPO_ROOT, "test", "package-tests");
+const TEMPLATES = path.join(REPO_ROOT, "test", "example-projects");
+
+const { check, report } = createProbes("check_package_tests", "see test/package-tests/README.md");
+
+// ---------------------------------------------------------------- probes
+
+{
+  const page = [
+    "Text.",
+    "",
+    "> [!WARNING]",
+    "> Wrong.",
+    "",
+    "> [!NOTE]",
+    "> Unmarked.",
+    "",
+    "```md",
+    "> [!NOTE]",
+    "```",
+    "  ",
+    "  > [!NOTE]",
+  ].join("\n");
+  const c = calloutsOf(page);
+  check("a callout in a fence is not a callout", c.length === 3, JSON.stringify(c));
+  check(
+    "a marker on the line above is read, item by item",
+    c[0]?.kind === "WARNING" &&
+      JSON.stringify(c[0].items) === JSON.stringify([{ case: "A.One" }, { compile: "NoWin64" }]),
+    JSON.stringify(c[0]),
+  );
+  check("a callout with no marker above has none", c[1]?.marker === null, JSON.stringify(c[1]));
+  check(
+    "an indented callout and its marker are read, and none keeps its reason",
+    c[2]?.items?.[0]?.none === "needs a person at the dialog",
+    JSON.stringify(c[2]),
+  );
+  check(
+    "an item the marker grammar does not know is bad, and none without a reason is bad",
+    parseMarker("A.B, whatever")[1].bad === "whatever" &&
+      parseMarker("none").length === 1 &&
+      !!parseMarker("none")[0].bad,
+    JSON.stringify([parseMarker("A.B, whatever"), parseMarker("none")]),
+  );
+}
+
+{
+  const expect = {
+    cases: {
+      "F.Hangs": { hangs: true },
+      "F.Fails": { fails: "1 of 1 checks failed: x: expected 1, got 2" },
+      "F.Only64": { win32: "pass", win64: { fails: "boom" } },
+    },
+  };
+  const text = runAllText({
+    pkg: "Pkg",
+    version: "1.2.3.4",
+    tests: [
+      { fixture: "F", name: "Passes" },
+      { fixture: "F", name: "Hangs" },
+    ],
+    expect,
+  });
+  check(
+    "RunAll calls a case and judges what it left, and skips one that never returns",
+    text.includes('        F.Passes\n        Expect.EndCase "F.Passes", Err.Number, Err.Description') &&
+      text.includes('Expect.Skip "F.Hangs", "it does not return in version 1.2.3.4"') &&
+      !text.includes("        F.Hangs\n"),
+    text,
+  );
+  check(
+    "expect.json: an unnamed case passes, an entry may differ by architecture",
+    expectationFor(expect, "F.Passes", "win32").kind === "pass" &&
+      expectationFor(expect, "F.Only64", "win32").kind === "pass" &&
+      expectationFor(expect, "F.Only64", "win64").detail === "boom",
+  );
+  const assertion = outcomeOf({
+    pass: false,
+    detail: "ASSERTION FAILED: 1 of 1 checks failed: x: expected 1, got 2 (line 12)",
+  });
+  check(
+    "a failure is judged without its line and the assertion prefix",
+    judge(expectationFor(expect, "F.Fails", "win32"), assertion) === "documented",
+    JSON.stringify(assertion),
+  );
+  check(
+    "a case expected to fail that passes is fixed; one that fails otherwise is changed",
+    judge(expectationFor(expect, "F.Fails", "win32"), { kind: "pass", detail: "" }) === "fixed" &&
+      judge(expectationFor(expect, "F.Fails", "win32"), { kind: "fail", detail: "other" }) === "changed" &&
+      judge(
+        { kind: "pass", detail: "" },
+        outcomeOf({ pass: false, detail: "no answer from the compiler after 20 s" }),
+      ) === "changed",
+  );
+  const next = expectFrom({
+    version: "1.2.3.4",
+    names: ["F.Passes", "F.Only64"],
+    outcomes: new Map([
+      ["F.Passes", { win32: { kind: "pass" }, win64: { kind: "pass" } }],
+      ["F.Only64", { win32: { kind: "pass" } }],
+    ]),
+    previous: expect,
+  });
+  check(
+    "expect.json from a run of one architecture keeps the other's entry, and leaves out a case that passes",
+    JSON.stringify(next) ===
+      JSON.stringify({ version: "1.2.3.4", cases: { "F.Only64": { win32: "pass", win64: { fails: "boom" } } } }),
+    JSON.stringify(next),
+  );
+  const files = [
+    {
+      name: "F.twin",
+      text: "[TestFixture]\nModule F\n[TestCase]\nSub Good()\n  Expect.Begin\n  x = 1\n  Expect.Done ' end\nEnd Sub\n[TestCase]\nSub Bad()\n  x = 1\nEnd Sub\nEnd Module\n",
+    },
+  ];
+  const bad = malformedCases(files, projectCases(files).tests);
+  check(
+    "a case that does not start with Expect.Begin and end with Expect.Done is reported, a comment after Done is not",
+    bad.length === 2 && bad.every((b) => b.includes("F.Bad")),
+    bad.join("\n"),
+  );
+}
+
+// ---------------------------------------------------------------- the corpus
+
+const problems = [];
+const allPages = await markdownFiles(SECTIONS);
+const sections = allPages.filter((rel) => /^[^/]+\/index\.md$/.test(rel)).map((rel) => rel.split("/")[0]);
+const projects = packageTestNames(ROOT);
+
+for (const name of projects) {
+  if (!sections.includes(name))
+    problems.push(`test/package-tests/${name}: no section docs/Reference/Contributed/${name}/`);
+}
+if (existsSync(path.join(ROOT, EXPECT_FILE)) === false) problems.push(`test/package-tests/${EXPECT_FILE} is missing`);
+
+let covered = 0;
+for (const pkg of sections) {
+  const has = projects.includes(pkg);
+  if (PENDING.has(pkg)) {
+    if (has) problems.push(`${pkg} has a test project: remove it from PENDING in scripts/check_package_tests.mjs`);
+    continue;
+  }
+  if (!has) {
+    problems.push(`${pkg}: no test project under test/package-tests/${pkg}/, and not in PENDING`);
+    continue;
+  }
+  covered++;
+  problems.push(...checkPackage(pkg).map((p) => `${pkg}: ${p}`));
+}
+for (const name of PENDING) {
+  if (!sections.includes(name)) problems.push(`PENDING names ${name}, which has no section`);
+}
+
+check(
+  `every contributed section's callouts name their cases (${covered} with a test project, ${PENDING.size} pending)`,
+  problems.length === 0,
+  problems.join("\n"),
+);
+
+process.exit(report());
+
+// ---------------------------------------------------------------- one package
+
+function checkPackage(pkg) {
+  const out = [];
+  const dir = path.join(ROOT, pkg);
+  const landing = readFileSync(path.join(SECTIONS, pkg, "index.md"), "utf8");
+  const indexedFrom = String(parseFrontmatter(landing).data?.indexed_from ?? "");
+  let version;
+  try {
+    version = packageVersion(templateSettings(TEMPLATES, pkg), pkg);
+  } catch (e) {
+    return [e.message];
+  }
+  if (version !== indexedFrom)
+    out.push(`the sample template references ${version}, the section documents ${indexedFrom}`);
+
+  if (existsSync(path.join(dir, "Sources", EXPECT_FILE)))
+    out.push(`Sources/${EXPECT_FILE} is a copy: the shared one is used`);
+  const files = testSources(ROOT, pkg);
+  const { tests, compiles, problems: callable } = projectCases(files);
+  out.push(...callable, ...malformedCases(files, tests));
+
+  const expectFile = path.join(dir, EXPECT_JSON);
+  let expect = { cases: {} };
+  if (!existsSync(expectFile)) out.push(`${EXPECT_JSON} is missing`);
+  else {
+    try {
+      expect = JSON.parse(readFileSync(expectFile, "utf8"));
+    } catch (e) {
+      out.push(`${EXPECT_JSON}: ${e.message}`);
+    }
+    if (expect.version !== version)
+      out.push(`${EXPECT_JSON} is for version ${expect.version}, the section documents ${version}`);
+  }
+  const caseNames = new Set([...tests.map((t) => `${t.fixture}.${t.name}`), ...compiles.map((c) => c.name)]);
+  for (const name of Object.keys(expect.cases ?? {})) {
+    if (!caseNames.has(name)) out.push(`${EXPECT_JSON} names ${name}, which is not a case`);
+    try {
+      expectationFor(expect, name, "win32");
+      expectationFor(expect, name, "win64");
+    } catch (e) {
+      out.push(e.message);
+    }
+  }
+
+  const runAll = files.find((f) => f.name === RUNALL_FILE)?.text ?? null;
+  if (runAll !== runAllText({ pkg, version, tests, expect })) {
+    out.push(
+      `Sources/${RUNALL_FILE} is ${runAll === null ? "missing" : "out of date"}: run node scripts/package_tests.mjs ${pkg}`,
+    );
+  }
+
+  const named = new Set();
+  const hasProbe = existsSync(path.join(dir, "probe"));
+  for (const rel of sectionPages(pkg)) {
+    const text = readFileSync(path.join(SECTIONS, pkg, rel), "utf8");
+    for (const c of calloutsOf(text)) {
+      const where = `${rel} line ${c.line}`;
+      if (c.marker === null) {
+        out.push(`${where}: the ${c.kind} has no  marker on the line above`);
+        continue;
+      }
+      if (!c.items.length) out.push(`${where}: the marker names nothing`);
+      for (const item of c.items) {
+        if (item.bad)
+          out.push(
+            `${where}: the marker's item "${item.bad}" is not Fixture.Sub, compile , probe  or none -- `,
+          );
+        else if (item.case && !tests.some((t) => `${t.fixture}.${t.name}` === item.case))
+          out.push(`${where}: no [TestCase] ${item.case}`);
+        else if (item.compile && !compiles.some((x) => x.name === item.compile))
+          out.push(`${where}: no compile case ${item.compile}`);
+        else if (item.probe && !hasProbe)
+          out.push(`${where}: names probe ${item.probe}, and the project has no probe/ folder`);
+        if (item.case) named.add(item.case);
+        if (item.compile) named.add(item.compile);
+      }
+    }
+  }
+  for (const name of caseNames)
+    if (!named.has(name)) out.push(`the case ${name} is named by no callout of the section`);
+  return out;
+}
+
+// The section's pages, relative to its folder.
+function sectionPages(pkg) {
+  const prefix = `${pkg}/`;
+  return allPages.filter((rel) => rel.startsWith(prefix)).map((rel) => rel.slice(prefix.length));
+}
diff --git a/scripts/check_tree_fresh.mjs b/scripts/check_tree_fresh.mjs
index 347c9ef25..708604ba4 100644
--- a/scripts/check_tree_fresh.mjs
+++ b/scripts/check_tree_fresh.mjs
@@ -70,8 +70,17 @@ const IGNORED_PATHS = new Set(["add-in/Resources/HELP", "add-in/Resources/SYMBOL
 // the other tools, so an edit there can change the build too. add-in/ is
 // packed into the help add-in's project file, which the build publishes, and
 // scripts/impexp.mjs is both the module that packs it and a download the
-// build copies.
-const DEFAULT_SOURCES = ["docs", "builder", "lib", "add-in", "scripts/impexp.mjs"];
+// build copies. test/package-tests/ and the sample templates in
+// test/example-projects/ make the package test projects the build publishes.
+const DEFAULT_SOURCES = [
+  "docs",
+  "builder",
+  "lib",
+  "add-in",
+  "scripts/impexp.mjs",
+  "test/package-tests",
+  "test/example-projects",
+];
 const DEFAULT_TREE = "docs/_site-offline";
 const DEFAULT_MARKER = "index.html";
 
diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs
index 5390c7479..b6adf54b8 100644
--- a/scripts/lib/cli-cases.mjs
+++ b/scripts/lib/cli-cases.mjs
@@ -103,6 +103,7 @@ const CASES = [
   { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--help"], exit: 0, stdout: /^usage: node scripts\/tbrun\.mjs / },
   { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--arch", "win99"], exit: 2, stderr: /^--arch expects win32 or win64, got: win99\nusage: node scripts\/tbrun\.mjs / },
   { tool: "scripts/tbrun.mjs", args: ["--port", "no-such-dir"], exit: 2, stderr: /^--port expects a whole number from 1 to 65535, got: no-such-dir\nusage: node scripts\/tbrun\.mjs / },
+  { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--only", "A.B"], exit: 2, stderr: /^--only needs --tests\nusage: node scripts\/tbrun\.mjs / },
   { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--arch"], exit: 2, stderr: /^--arch needs a value\nusage: node scripts\/tbrun\.mjs / },
   { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--arch", ""], exit: 2, stderr: /^--arch needs a non-empty value\nusage: node scripts\/tbrun\.mjs / },
   { tool: "scripts/tbrun.mjs", args: ["--bogus", "no-such-dir"], exit: 2, stderr: /^unknown option: --bogus\nusage: node scripts\/tbrun\.mjs / },
diff --git a/scripts/lib/package-test-cases.mjs b/scripts/lib/package-test-cases.mjs
new file mode 100644
index 000000000..56fa8f012
--- /dev/null
+++ b/scripts/lib/package-test-cases.mjs
@@ -0,0 +1,207 @@
+// The package test projects' cases (test/package-tests//), as scripts/package_tests.mjs
+// runs them and scripts/check_package_tests.mjs checks them: the RunAll module generated from a
+// project's [TestCase] Subs, what expect.json says a case does, how a result is judged against
+// it, and the markers that tie each callout of a section's pages to the cases behind it.
+// lib/package-tests.mjs puts a project together; test/package-tests/README.md describes it all.
+
+import { EXPECT_FILE, RUNALL_FILE } from "../../lib/package-tests.mjs";
+import { findCases } from "./tb-tests.mjs";
+
+export const ARCHES = ["win32", "win64"];
+
+/**
+ * The cases of a package's project: its [TestCase] Subs and its compile cases, from the package
+ * folder's own sources (Expect.twin and RunAll.twin are not its cases).
+ *
+ * @param {{name: string, text: string}[]} files  testSources' list
+ */
+export function projectCases(files) {
+  const own = files.filter((f) => f.name !== EXPECT_FILE && f.name !== RUNALL_FILE);
+  return findCases(own);
+}
+
+const caseName = (t) => `${t.fixture}.${t.name}`;
+
+/**
+ * The text of RunAll.twin: Sub Main runs every [TestCase] Sub under Expect's collecting mode and
+ * prints PASS, FAIL or SKIP for each. A case expect.json says never returns is skipped.
+ *
+ * @param {object} o
+ * @param {string} o.pkg
+ * @param {string} o.version  the package's version, as a.b.c.d
+ * @param {{fixture: string, name: string}[]} o.tests
+ * @param {object} o.expect   expect.json
+ */
+export function runAllText({ pkg, version, tests, expect }) {
+  const lines = [
+    "' Generated by scripts/package_tests.mjs from this project's [TestCase] Subs and expect.json:",
+    "' do not edit. Run Sub Main: each case prints PASS, FAIL or SKIP in the DEBUG CONSOLE.",
+    `' The cases are written for the package ${pkg}, version ${version}. Each one checks what is`,
+    "' correct, so a case that fails names a defect of the package.",
+    "Module RunAll",
+    "",
+    "    Public Sub Main()",
+    `        Expect.StartRun "${pkg} ${version}: ${tests.length} test case${tests.length === 1 ? "" : "s"}"`,
+    "        On Error Resume Next",
+  ];
+  for (const t of tests) {
+    const name = caseName(t);
+    const never = ARCHES.every((a) => expectationFor(expect, name, a).kind === "hang");
+    if (never) {
+      lines.push(`        Expect.Skip "${name}", "it does not return in version ${version}"`);
+      continue;
+    }
+    lines.push("        Expect.StartCase", "        Err.Clear", `        ${name}`);
+    lines.push(`        Expect.EndCase "${name}", Err.Number, Err.Description`);
+  }
+  lines.push("        On Error GoTo 0", "        Expect.EndRun", "    End Sub", "", "End Module", "");
+  return lines.join("\n");
+}
+
+/**
+ * What expect.json says a case does on one architecture. A case it does not name passes. An
+ * entry is { fails: "" } or { hangs: true }, for both architectures, or
+ * { win32: , win64:  }, where  is "pass" or one of those two.
+ *
+ * @returns {{kind: "pass" | "fail" | "hang", detail: string}}
+ */
+export function expectationFor(expect, name, arch) {
+  let e = expect?.cases?.[name];
+  if (e && (e.win32 !== undefined || e.win64 !== undefined)) e = e[arch];
+  if (!e || e === "pass") return { kind: "pass", detail: "" };
+  if (e.hangs) return { kind: "hang", detail: "" };
+  if (typeof e.fails === "string") return { kind: "fail", detail: e.fails };
+  throw new Error(`expect.json: ${name} has an entry that is not pass, fails or hangs`);
+}
+
+/** A failure as tbrun reports it, without what changes from run to run: its line and the assertion prefix. */
+export function normaliseDetail(detail) {
+  return String(detail)
+    .replace(/ \(line -?\d+\)$/, "")
+    .replace(/^ASSERTION FAILED:\s*/, "")
+    .replace(/\s+/g, " ")
+    .trim();
+}
+
+const NO_ANSWER_RE = /^no answer from the compiler after /;
+
+/** A tbrun case result as one of pass, fail and hang. */
+export function outcomeOf(result) {
+  if (result.pass) return { kind: "pass", detail: "" };
+  if (NO_ANSWER_RE.test(result.detail)) return { kind: "hang", detail: "" };
+  return { kind: "fail", detail: normaliseDetail(result.detail) };
+}
+
+/**
+ * Judges a case's outcome against its expectation.
+ *
+ * @returns {"documented" | "fixed" | "changed"}  documented: as expect.json says; fixed: it was
+ *   expected to fail or hang and passes; changed: anything else
+ */
+export function judge(expected, outcome) {
+  if (expected.kind === outcome.kind) {
+    if (expected.kind !== "fail" || normaliseDetail(expected.detail) === outcome.detail) return "documented";
+    return "changed";
+  }
+  return outcome.kind === "pass" ? "fixed" : "changed";
+}
+
+/**
+ * expect.json as a run's outcomes give it. `outcomes` maps a case name to { win32, win64 }
+ * outcomes; an architecture not run keeps what `previous` says.
+ */
+export function expectFrom({ version, names, outcomes, previous }) {
+  const one = (o) => (o.kind === "pass" ? "pass" : o.kind === "hang" ? { hangs: true } : { fails: o.detail });
+  const cases = {};
+  for (const name of names) {
+    const per = {};
+    for (const a of ARCHES) {
+      const o = outcomes.get(name)?.[a] ?? expectationFor(previous, name, a);
+      per[a] = one(o);
+    }
+    const same = JSON.stringify(per.win32) === JSON.stringify(per.win64);
+    if (same && per.win32 === "pass") continue;
+    cases[name] = same ? per.win32 : per;
+  }
+  return { version, cases };
+}
+
+// ---------------------------------------------------------------- markers
+
+// A callout: a blockquote line `> [!NOTE]` or `> [!WARNING]`, indented or not.
+const CALLOUT_RE = /^[ \t]*>[ \t]*\[!(NOTE|WARNING)\]/;
+// Its marker: an HTML comment on the line above it, ``.
+const MARKER_RE = /^[ \t]*[ \t]*$/;
+const FENCE_RE = /^[ \t]*(`{3,}|~{3,})/;
+
+/**
+ * The callouts of a page and the marker above each.
+ *
+ * @returns {{line: number, kind: string, marker: string | null, items: object[]}[]}  `items`
+ *   parses the marker: { case: "Fixture.Sub" }, { compile: "Name" }, { probe: "id" } or
+ *   { none: "reason" }, and { bad: "text" } for one it cannot read
+ */
+export function calloutsOf(text) {
+  const lines = text.split(/\r?\n/);
+  const out = [];
+  let fence = null;
+  for (let i = 0; i < lines.length; i++) {
+    const f = FENCE_RE.exec(lines[i]);
+    if (f) {
+      if (!fence) fence = f[1][0];
+      else if (f[1][0] === fence) fence = null;
+      continue;
+    }
+    if (fence) continue;
+    const c = CALLOUT_RE.exec(lines[i]);
+    if (!c) continue;
+    const m = i > 0 ? MARKER_RE.exec(lines[i - 1]) : null;
+    out.push({ line: i + 1, kind: c[1], marker: m ? m[1] : null, items: m ? parseMarker(m[1]) : [] });
+  }
+  return out;
+}
+
+/** The items of a marker: Fixture.Sub, compile , probe , none -- . */
+export function parseMarker(text) {
+  const none = /^none[ \t]*(?:--|:)?[ \t]*(.*)$/.exec(text);
+  if (none) return [none[1].trim() ? { none: none[1].trim() } : { bad: "none needs a reason" }];
+  return text
+    .split(",")
+    .map((s) => s.trim())
+    .filter(Boolean)
+    .map((s) => {
+      let m = /^(\w+)\.(\w+)$/.exec(s);
+      if (m) return { case: s };
+      m = /^compile[ \t]+(\w+)$/.exec(s);
+      if (m) return { compile: m[1] };
+      m = /^probe[ \t]+([\w,.-]+)$/.exec(s);
+      if (m) return { probe: m[1] };
+      return { bad: s };
+    });
+}
+
+/**
+ * The [TestCase] Subs whose first statement is not Expect.Begin or whose last is not
+ * Expect.Done.
+ *
+ * @param {{name: string, text: string}[]} files
+ * @returns {string[]}  one line for each
+ */
+export function malformedCases(files, tests) {
+  const out = [];
+  for (const t of tests) {
+    const file = files.find((f) => f.name === t.file);
+    const lines = file.text.split(/\r?\n/);
+    const body = [];
+    for (let i = t.line; i < lines.length; i++) {
+      if (/^[ \t]*End[ \t]+Sub\b/i.test(lines[i])) break;
+      const s = lines[i].replace(/'.*$/, "").trim();
+      if (s) body.push(s);
+    }
+    if (!/^Expect\.Begin$/i.test(body[0] ?? ""))
+      out.push(`${t.file} line ${t.line}: ${caseName(t)} does not start with Expect.Begin`);
+    if (!/^Expect\.Done$/i.test(body.at(-1) ?? ""))
+      out.push(`${t.file} line ${t.line}: ${caseName(t)} does not end with Expect.Done`);
+  }
+  return out;
+}
diff --git a/scripts/package_tests.mjs b/scripts/package_tests.mjs
new file mode 100644
index 000000000..54f008fc2
--- /dev/null
+++ b/scripts/package_tests.mjs
@@ -0,0 +1,294 @@
+// Runs the package test projects, test/package-tests//, and judges each case against
+// the package's expect.json.
+//
+//     node scripts/package_tests.mjs [ ...] [options]
+//
+//       --arch         win32, win64 or both (default both)
+//       --port         the first DevTools port to try (default 9620); the run claims the first
+//                         free one from there (scripts/lib/tb-ports.mjs)
+//       --timeout   how long one case may take (default 60)
+//       --hang-timeout   how long a case expect.json says never returns is given (default 20)
+//       --update          write expect.json from this run, for the architectures run
+//       --ide       as tbrun's
+//       --json            one JSON object instead of text
+//
+// Each case is written to check what is correct, so a defect of the package is a failing case,
+// and expect.json records how each case fails, or that it never returns, on the version the
+// section documents. A case is then one of: documented (as expect.json says), fixed (expected
+// to fail or never return, and passes) or changed (anything else). Fixed means the page's
+// callout goes; changed means a claim of the page is no longer true.
+//
+// For each package and architecture the project is staged with the package embedded (from
+// %TEMP%\tb-twinserv, the cache check_examples keeps, downloaded from TWINSERV when it is not
+// there) and run with `tbrun --tests`. A case that does not return stops the IDE's compiler,
+// so a case expect.json marks as never returning is run alone, and a case that unexpectedly
+// does not return is judged, and the cases after it are run again in a new IDE.
+//
+// RunAll.twin, which the project's Sub Main runs, is generated from the cases and expect.json:
+// the run writes it again when it is out of date, and says so.
+//
+// Needs Windows and a twinBASIC install, as tbrun does; outside every gate and outside CI.
+//
+// Exit: 0 every case documented, 1 a case fixed or changed, 2 the harness failed or the command
+// line was refused.
+
+import { spawnSync } from "node:child_process";
+import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { downloadPackage } from "../indexer/lib/twinserv-client.mjs";
+import {
+  choiceOption,
+  die,
+  exitOnCrash,
+  numberOption,
+  parseCli,
+  printHelpAndExit,
+  withUsageError,
+} from "../lib/cli.mjs";
+import {
+  EXPECT_JSON,
+  packageTestNames,
+  packageVersion,
+  RUNALL_FILE,
+  readExpect,
+  templateSettings,
+  testSettings,
+  testSources,
+} from "../lib/package-tests.mjs";
+import { REPO_ROOT } from "../lib/repo-paths.mjs";
+import { embeddedPackages } from "./lib/example-batches.mjs";
+import {
+  ARCHES,
+  expectationFor,
+  expectFrom,
+  judge,
+  outcomeOf,
+  projectCases,
+  runAllText,
+} from "./lib/package-test-cases.mjs";
+import { claimPorts } from "./lib/tb-ports.mjs";
+import { unpackProject } from "./lib/tb-project.mjs";
+
+const ROOT = path.join(REPO_ROOT, "test", "package-tests");
+const TEMPLATES = path.join(REPO_ROOT, "test", "example-projects");
+const TBRUN = path.join(REPO_ROOT, "scripts", "tbrun.mjs");
+const CACHE = path.join(tmpdir(), "tb-twinserv");
+const WORK = path.join(tmpdir(), "tb-package-tests");
+
+const USAGE = `usage: node scripts/package_tests.mjs [ ...] [--arch win32|win64|both] [--port N] [--timeout S] [--hang-timeout S] [--update] [--ide ] [--json] [-h, --help]
+
+Runs the package test projects under test/package-tests/ with tbrun --tests, each
+with its package embedded, and judges every case against the package's
+expect.json: documented (as it says), fixed (expected to fail or never return,
+and passes) or changed. With no package named, runs every project.
+
+  --arch               win32, win64 or both (default both)
+  --port               the first DevTools port to try (default 9620)
+  --timeout         how long one case may take (default 60)
+  --hang-timeout    how long a case that never returns is given (default 20)
+  --update                write expect.json from this run, for the architectures run
+  --ide             as tbrun's
+  --json                  emit one JSON object instead of text
+  -h, --help              print this text and exit
+
+Exit codes:
+  0  every case did what expect.json says
+  1  a case was fixed or changed
+  2  a refused command line, a package with no test project, or the harness failed`;
+
+const { values, positionals } = withUsageError(
+  () =>
+    parseCli(process.argv.slice(2), {
+      options: {
+        arch: { type: "string" },
+        port: { type: "string" },
+        timeout: { type: "string" },
+        "hang-timeout": { type: "string" },
+        update: { type: "boolean", default: false },
+        ide: { type: "string" },
+        json: { type: "boolean", default: false },
+        help: { type: "boolean", short: "h", default: false },
+      },
+      positionals: { min: 0, max: Infinity },
+      stopAt: ["help"],
+    }),
+  { format: (err) => `${err.message}\n${USAGE}` },
+);
+if (values.help) printHelpAndExit(USAGE);
+
+const opts = withUsageError(
+  () => ({
+    arches:
+      choiceOption(values.arch ?? "both", { option: "--arch", choices: [...ARCHES, "both"] }) === "both"
+        ? ARCHES
+        : [values.arch],
+    port: numberOption(values.port ?? "9620", { option: "--port", integer: true, min: 1, max: 65535 }),
+    timeout: numberOption(values.timeout ?? "60", { option: "--timeout", above: 0 }),
+    hangTimeout: numberOption(values["hang-timeout"] ?? "20", { option: "--hang-timeout", above: 0 }),
+  }),
+  { format: (err) => `${err.message}\n${USAGE}` },
+);
+
+exitOnCrash();
+
+const known = packageTestNames(ROOT);
+const packages = positionals.length ? positionals : known;
+const missing = packages.filter((p) => !known.includes(p));
+if (missing.length) die(2, `no test project for ${missing.join(", ")} under test/package-tests/\n${USAGE}`);
+if (!packages.length) die(2, "test/package-tests/ holds no test project");
+
+const [port] = await claimPorts(1, { from: opts.port });
+const say = (s) => {
+  if (!values.json) console.log(s);
+};
+
+for (const pkg of packages) await ensurePackage(templateSettings(TEMPLATES, pkg));
+
+const report = [];
+let failed = false;
+for (const pkg of packages) {
+  const r = await runPackage(pkg);
+  report.push(r);
+  if (r.cases.some((c) => c.verdict !== "documented")) failed = true;
+}
+if (values.json) console.log(JSON.stringify({ port, packages: report }, null, 2));
+process.exit(failed ? 1 : 0);
+
+// ---------------------------------------------------------------- one package
+
+async function runPackage(pkg) {
+  const template = templateSettings(TEMPLATES, pkg);
+  const version = packageVersion(template, pkg);
+  const expect = readExpect(ROOT, pkg);
+  if (expect.version && expect.version !== version) {
+    say(`${pkg}: expect.json is for version ${expect.version}, the template for ${version}`);
+  }
+  let files = testSources(ROOT, pkg);
+  const { tests, compiles, problems } = projectCases(files);
+  if (problems.length) die(2, `${pkg}:\n${problems.map((p) => `  ${p}`).join("\n")}`);
+  refreshRunAll(pkg, version, tests, expect);
+  files = testSources(ROOT, pkg);
+
+  const names = [...compiles.map((c) => c.name), ...tests.map((t) => `${t.fixture}.${t.name}`)];
+  say(`${pkg} ${version}: ${names.length} case${names.length === 1 ? "" : "s"}`);
+  const outcomes = new Map(names.map((n) => [n, {}]));
+  for (const arch of opts.arches) {
+    const tree = stageTree(pkg, arch, template, files);
+    const hangs = names.filter((n) => expectationFor(expect, n, arch).kind === "hang");
+    let rest = names.filter((n) => !hangs.includes(n));
+    // A case that does not return stops the compiler, so the cases after it are run again.
+    while (rest.length) {
+      const results = tbrunCases(tree, arch, rest, opts.timeout);
+      let next = [];
+      for (const r of results) {
+        const o = outcomeOf(r);
+        outcomes.get(r.name)[arch] = o;
+        if (o.kind === "hang") {
+          next = rest.slice(rest.indexOf(r.name) + 1);
+          if (next.length) say(`  ${arch}: ${r.name} did not return; running the ${next.length} after it again`);
+          break;
+        }
+      }
+      rest = next;
+    }
+    for (const name of hangs) {
+      const [r] = tbrunCases(tree, arch, [name], opts.hangTimeout);
+      outcomes.get(name)[arch] = outcomeOf(r);
+    }
+  }
+
+  const cases = [];
+  for (const name of names) {
+    for (const arch of opts.arches) {
+      const expected = expectationFor(expect, name, arch);
+      const got = outcomes.get(name)[arch] ?? { kind: "fail", detail: "no result" };
+      const verdict = judge(expected, got);
+      cases.push({ name, arch, verdict, expected, got });
+      if (verdict !== "documented") {
+        say(`  ${verdict.toUpperCase()} ${arch} ${name}`);
+        say(`    expected: ${describe(expected)}`);
+        say(`    got:      ${describe(got)}`);
+      }
+    }
+  }
+  const documented = cases.filter((c) => c.verdict === "documented").length;
+  say(`  ${documented} of ${cases.length} as documented (${opts.arches.join(", ")})`);
+
+  if (values.update) {
+    const next = expectFrom({ version, names, outcomes, previous: expect });
+    const file = path.join(ROOT, pkg, EXPECT_JSON);
+    writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`, "utf8");
+    say(`  wrote ${path.relative(REPO_ROOT, file).replaceAll(path.sep, "/")}`);
+    refreshRunAll(pkg, version, tests, next);
+  }
+  return { package: pkg, version, cases };
+}
+
+function describe(o) {
+  return o.kind === "pass" ? "pass" : o.kind === "hang" ? "does not return" : `fails: ${o.detail}`;
+}
+
+// RunAll.twin follows the cases and expect.json; it is written only when it changes.
+function refreshRunAll(pkg, version, tests, expect) {
+  const file = path.join(ROOT, pkg, "Sources", RUNALL_FILE);
+  const text = runAllText({ pkg, version, tests, expect });
+  const old = existsSync(file) ? readFileSync(file, "utf8").replace(/\r\n/g, "\n") : null;
+  if (old === text) return;
+  writeFileSync(file, text, "utf8");
+  say(`  wrote ${path.relative(REPO_ROOT, file).replaceAll(path.sep, "/")}`);
+}
+
+// ---------------------------------------------------------------- staging and running
+
+async function ensurePackage(settings) {
+  for (const p of embeddedPackages(settings)) {
+    const tree = path.join(CACHE, p.key);
+    if (existsSync(path.join(tree, "Settings"))) continue;
+    const file = `${tree}.twinpack`;
+    if (!existsSync(file)) {
+      say(`  downloading ${p.folder} (${p.key}) from TWINSERV`);
+      mkdirSync(CACHE, { recursive: true });
+      writeFileSync(file, await downloadPackage(p.id, p.version));
+    }
+    unpackProject(file, tree);
+  }
+}
+
+function stageTree(pkg, arch, template, files) {
+  const settings = testSettings(template, pkg);
+  const tree = path.join(WORK, `${pkg}-${arch}-${port}`);
+  rmSync(tree, { recursive: true, force: true });
+  mkdirSync(path.join(tree, "Sources"), { recursive: true });
+  writeFileSync(path.join(tree, "Settings"), `${JSON.stringify(settings, null, "\t")}\n`, "utf8");
+  for (const p of embeddedPackages(settings)) {
+    const from = path.join(CACHE, p.key);
+    if (!existsSync(path.join(from, "Settings"))) throw new Error(`${p.folder} is not in ${CACHE}`);
+    cpSync(from, path.join(tree, "Packages", p.folder), { recursive: true });
+  }
+  for (const f of files) {
+    const out = path.join(tree, "Sources", ...f.name.split("/"));
+    mkdirSync(path.dirname(out), { recursive: true });
+    writeFileSync(out, f.text.replace(/\n/g, "\r\n"), "utf8");
+  }
+  return tree;
+}
+
+// tbrun --tests --json on some of the cases; its cases in the order asked.
+function tbrunCases(tree, arch, names, timeoutSecs) {
+  const args = [TBRUN, tree, "--tests", "--json", "--arch", arch, "--port", String(port)];
+  args.push("--timeout", String(timeoutSecs), "--only", names.join(","));
+  if (values.ide) args.push("--ide", values.ide);
+  const r = spawnSync(process.execPath, args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
+  let json = null;
+  try {
+    json = JSON.parse(r.stdout);
+  } catch {
+    // below
+  }
+  if (!json || (r.status !== 0 && r.status !== 7)) {
+    throw new Error(`tbrun exited ${r.status} on ${tree} (${arch}):\n${r.stderr || r.stdout}`);
+  }
+  const byName = new Map(json.cases.map((c) => [c.name, c]));
+  return names.map((n) => ({ name: n, ...(byName.get(n) ?? { pass: false, detail: "tbrun gave no result" }) }));
+}
diff --git a/scripts/tbrun.mjs b/scripts/tbrun.mjs
index c55b301b1..e696992f1 100644
--- a/scripts/tbrun.mjs
+++ b/scripts/tbrun.mjs
@@ -34,6 +34,10 @@
 //       --tests           do not build: judge the project's compile cases and
 //                         run its [TestCase] Subs, printing PASS or FAIL for
 //                         each (lib/tb-tests.mjs)
+//       --only     with --tests, judge and run only these cases: a
+//                         comma-separated list of Fixture.Sub names and compile
+//                         case names; a case that never returns is run alone
+//                         this way (scripts/package_tests.mjs)
 //
 // Exit: 0 captured output, 1 the project has compile errors, 2 the harness
 // could not run (a refused command line included), a compile never settled, or
@@ -135,6 +139,7 @@ import { existsSync, readFileSync, mkdirSync, statSync, readdirSync, rmSync, wri
 import { tmpdir } from "node:os";
 import path from "node:path";
 import {
+  CliError,
   choiceOption,
   die,
   exitOnCrash,
@@ -171,7 +176,7 @@ import { faultText, recordedFault } from "./lib/win-fault.mjs";
 
 exitOnCrash();
 
-const USAGE = `usage: node scripts/tbrun.mjs  [--ide ] [--port N] [--arch win32|win64] [--timeout S] [--quiet MS] [--json] [--raw] [--keep] [--no-reap] [--reap-images a,b] [--show|--hide] [--llvm | --compiler-options S] [--exe | --tests] [--allow-name-clash] [-h, --help]
+const USAGE = `usage: node scripts/tbrun.mjs  [--ide ] [--port N] [--arch win32|win64] [--timeout S] [--quiet MS] [--json] [--raw] [--keep] [--no-reap] [--reap-images a,b] [--show|--hide] [--llvm | --compiler-options S] [--exe | --tests [--only ]] [--allow-name-clash] [-h, --help]
 
 Builds an exported twinBASIC source tree in the IDE, runs it, and prints what it
 writes to the DEBUG CONSOLE. With --tests it builds nothing, and runs the
@@ -210,6 +215,8 @@ project's own test cases instead.
                       those diagnostic codes on that line, any error, or
                       nothing on it) and for each [TestCase] Sub of each
                       [TestFixture] Module, run in the compiler's test mode
+  --only       with --tests, only these cases: a comma-separated list of
+                      Fixture.Sub names and compile case names
   -h, --help          print this text and exit
 
 Exit codes:
@@ -249,6 +256,7 @@ const { values, positionals } = withUsageError(
         exe: { type: "boolean", default: false },
         "allow-name-clash": { type: "boolean", default: false },
         tests: { type: "boolean", default: false },
+        only: { type: "string" },
         json: { type: "boolean", default: false },
         raw: { type: "boolean", default: false },
         keep: { type: "boolean", default: false },
@@ -270,6 +278,7 @@ const { port, arch, timeoutMs, quietMs } = withUsageError(
     refuseTogether(values, ["show", "hide"]);
     refuseTogether(values, ["llvm", "compiler-options"]);
     refuseTogether(values, ["exe", "tests"]);
+    if (values.only !== undefined && !values.tests) throw new CliError("needs-option", "--only needs --tests");
     return {
       port: numberOption(values.port ?? "9346", { option: "--port", integer: true, min: 1, max: 65535 }),
       arch: choiceOption(values.arch ?? TARGETS[0], { option: "--arch", choices: TARGETS }),
@@ -354,6 +363,14 @@ const hasCls = /Debug\s*\.\s*Cls/i.test(sourceText);
 const cases = values.tests ? findCases(sourceFiles) : null;
 if (cases) {
   if (cases.problems.length) die(2, `tbrun --tests:\n${cases.problems.map((p) => `  ${p}`).join("\n")}`);
+  if (values.only !== undefined) {
+    const wanted = new Set(values.only.split(",").map((n) => n.trim()));
+    const known = new Set([...cases.tests.map((t) => `${t.fixture}.${t.name}`), ...cases.compiles.map((c) => c.name)]);
+    const unknown = [...wanted].filter((n) => !known.has(n));
+    if (unknown.length) die(2, `tbrun --tests --only: no such case: ${unknown.join(", ")}`);
+    cases.tests = cases.tests.filter((t) => wanted.has(`${t.fixture}.${t.name}`));
+    cases.compiles = cases.compiles.filter((c) => wanted.has(c.name));
+  }
   if (!cases.tests.length && !cases.compiles.length) {
     die(
       2,
diff --git a/test.bat b/test.bat
index cbc29a5c6..5d81a32ca 100644
--- a/test.bat
+++ b/test.bat
@@ -157,6 +157,14 @@ node scripts/check_page_baseline.mjs
 @rem empty docs\. No tree, no browser, well under a second.
 node scripts/check_book_coverage.mjs
 @if errorlevel 1 goto :fail
+@rem Every NOTE and WARNING of a contributed package's section names the
+@rem test cases behind it in a marker, and each package test project under
+@rem test\package-tests is in order: its cases named, well formed, in
+@rem expect.json for the documented version and in its generated RunAll
+@rem module. Its probes run on inputs built in memory first. Sources only:
+@rem no tree, no install, well under a second.
+node scripts/check_package_tests.mjs
+@if errorlevel 1 goto :fail
 @rem The symbol index (tB/symbols.json) is read by the IDE help add-in,
 @rem and a build that indexes the reference cleanly says nothing about the
 @rem rules that did not fire on it. These probes assert each one against
diff --git a/test/README.md b/test/README.md
index 1c8ebf541..97b4bb254 100644
--- a/test/README.md
+++ b/test/README.md
@@ -14,10 +14,12 @@ is input for three tools:
 - `ide/` for [`scripts/ide_test.mjs`](../scripts/ide_test.mjs), which `ide-test.bat` runs:
   the scenarios that operate the IDE itself. It is the add-in runner over its own
   `lanes.mjs`, and its scenario files use `scenario` from `addin/scenario.mjs`.
-- `package-probes/` for a person documenting a contributed package: a probe of what a
-  package does at run time that only a built program can show, run by hand and outside every
-  gate, with a README of its own. `TBMANLIB_Dialog/` operates the package's dialogs, and
-  `xCalendar/` the package's calendar control on a form.
+- `package-tests/` holds the contributed packages' test projects, one folder per package,
+  each backing the NOTEs and WARNINGs of the package's section with test cases; its own
+  README says how they are written and run. A package whose claims only a built program can
+  show has a `probe/` harness in its folder, run by hand and outside every gate, with a
+  README of its own: `TBMANLIB_Dialog/probe/` operates the package's dialogs, and
+  `xCalendar/probe/` the package's calendar control on a form.
 
 If a fixture case has just gone red, start at [The invariant, and how it
 breaks](#the-invariant-and-how-it-breaks) --- the commit that broke it need not have touched
diff --git a/test/package-tests/Expect.twin b/test/package-tests/Expect.twin
new file mode 100644
index 000000000..59f5c2f4c
--- /dev/null
+++ b/test/package-tests/Expect.twin
@@ -0,0 +1,155 @@
+' Expect: the checks every test case of the twinBASIC documentation's package tests makes.
+' Part of each test project; https://docs.twinbasic.com. MIT licence.
+'
+' A case is a [TestCase] Sub of a [TestFixture] Module. It starts with Expect.Begin, checks with
+' Expect.AreEqual, Expect.IsTrue, Expect.IsFalse and Expect.Fail, and ends with Expect.Done. Each
+' check states what is correct, so a case that fails names a defect of the package. A failed check
+' does not stop the case: Expect.Done reports every failed check of the case at once.
+'
+' Run the project (Sub Main in the module RunAll) to run every case: each one prints PASS, FAIL or
+' SKIP in the DEBUG CONSOLE. A test runner that runs one case at a time sees a failed case as a
+' failed assertion, which the case's own On Error statement cannot hide.
+Module Expect
+
+    Private mChecks As Long
+    Private mFailures As Long
+    Private mText As String
+    Private mCollecting As Boolean
+    Private mReachedDone As Boolean
+    Private mPassed As Long
+    Private mFailed As Long
+    Private mSkipped As Long
+
+    ' Starts a case: forgets the checks of the case before it.
+    Public Sub Begin()
+        mChecks = 0
+        mFailures = 0
+        mText = ""
+    End Sub
+
+    ' Checks that actual is expected. A string equals only a string, Empty only Empty, Null only
+    ' Null and an object only the same object; numbers, dates and Booleans compare by value.
+    Public Sub AreEqual(ByVal Expected As Variant, ByVal Actual As Variant, ByVal What As String)
+        mChecks = mChecks + 1
+        If Not Same(Expected, Actual) Then Record What & ": expected " & Show(Expected) & ", got " & Show(Actual)
+    End Sub
+
+    Public Sub IsTrue(ByVal Condition As Boolean, ByVal What As String)
+        mChecks = mChecks + 1
+        If Not Condition Then Record What & ": expected True, got False"
+    End Sub
+
+    Public Sub IsFalse(ByVal Condition As Boolean, ByVal What As String)
+        mChecks = mChecks + 1
+        If Condition Then Record What & ": expected False, got True"
+    End Sub
+
+    ' A failed check with no values, for a place the case must not reach.
+    Public Sub Fail(ByVal What As String)
+        mChecks = mChecks + 1
+        Record What
+    End Sub
+
+    ' Ends a case: reports the failed checks, if any.
+    Public Sub Done()
+        Dim text As String
+        If mFailures > 0 Then text = mFailures & " of " & mChecks & " checks failed: " & mText
+        Begin
+        If mCollecting Then
+            mReachedDone = True
+            mText = text
+        ElseIf Len(text) > 0 Then
+            Assert.Strict.Fail text
+        End If
+    End Sub
+
+    ' ---- used by RunAll
+
+    Public Sub StartRun(ByVal Title As String)
+        mCollecting = True
+        mPassed = 0
+        mFailed = 0
+        mSkipped = 0
+        Debug.Print Title
+    End Sub
+
+    Public Sub StartCase()
+        Begin
+        mReachedDone = False
+    End Sub
+
+    ' What the case's call left: Done's report, or the error that ended it before Done.
+    Public Sub EndCase(ByVal Name As String, ByVal ErrNumber As Long, ByVal ErrDescription As String)
+        Dim outcome As String
+        If mReachedDone Then
+            outcome = mText
+        ElseIf ErrNumber <> 0 Then
+            outcome = "run-time error " & ErrNumber & ": " & ErrDescription
+        Else
+            outcome = "the case ended before Expect.Done"
+        End If
+        If Len(outcome) = 0 Then
+            mPassed = mPassed + 1
+            Debug.Print "PASS " & Name
+        Else
+            mFailed = mFailed + 1
+            Debug.Print "FAIL " & Name & ": " & outcome
+        End If
+        Begin
+    End Sub
+
+    Public Sub Skip(ByVal Name As String, ByVal Reason As String)
+        mSkipped = mSkipped + 1
+        Debug.Print "SKIP " & Name & ": " & Reason
+    End Sub
+
+    Public Sub EndRun()
+        Debug.Print mPassed & " passed, " & mFailed & " failed, " & mSkipped & " skipped"
+        mCollecting = False
+    End Sub
+
+    ' ---- inside
+
+    Private Sub Record(ByVal Text As String)
+        mFailures = mFailures + 1
+        If Len(mText) > 0 Then mText = mText & "; "
+        mText = mText & Text
+    End Sub
+
+    Private Function Same(ByVal A As Variant, ByVal B As Variant) As Boolean
+        If IsObject(A) Or IsObject(B) Then
+            If IsObject(A) And IsObject(B) Then Same = (A Is B)
+        ElseIf IsArray(A) Or IsArray(B) Then
+            Same = False
+        ElseIf IsNull(A) Or IsNull(B) Then
+            Same = IsNull(A) And IsNull(B)
+        ElseIf IsEmpty(A) Or IsEmpty(B) Then
+            Same = IsEmpty(A) And IsEmpty(B)
+        ElseIf VarType(A) = vbString Or VarType(B) = vbString Then
+            If VarType(A) = vbString And VarType(B) = vbString Then Same = (StrComp(A, B, vbBinaryCompare) = 0)
+        Else
+            Same = (A = B)
+        End If
+    End Function
+
+    Private Function Show(ByVal V As Variant) As String
+        If IsObject(V) Then
+            If V Is Nothing Then Show = "Nothing" Else Show = "an object of type " & TypeName(V)
+        ElseIf IsArray(V) Then
+            Show = "an array of type " & TypeName(V)
+        ElseIf IsNull(V) Then
+            Show = "Null"
+        ElseIf IsEmpty(V) Then
+            Show = "Empty"
+        ElseIf VarType(V) = vbString Then
+            If Len(V) > 120 Then
+                Show = """" & Left$(V, 100) & """... (" & Len(V) & " characters)"
+            Else
+                Show = """" & V & """"
+            End If
+        Else
+            Show = CStr(V)
+        End If
+    End Function
+
+End Module
diff --git a/test/package-tests/README.md b/test/package-tests/README.md
new file mode 100644
index 000000000..38046221a
--- /dev/null
+++ b/test/package-tests/README.md
@@ -0,0 +1,132 @@
+# Package test projects
+
+Each contributed package the reference documents (`docs/Reference/Contributed//`) has a
+test project here: twinBASIC test cases for every claim its NOTEs and WARNINGs make. The build
+publishes each project as `-tests.twinproj` beside the package's section, for a reader or
+the package's author to open, add the package to, and run.
+
+```
+test/package-tests/
+  Expect.twin              the checks every case makes; part of every project
+  /
+    Sources/
+      .twin       [TestFixture] modules of [TestCase] Subs
+      RunAll.twin          generated: Sub Main runs every case
+    expect.json            how each case fails on the documented version
+    probe/                 optional: a harness for what only a built program shows
+```
+
+A project has no `Settings` of its own: it takes the package's sample template's,
+`test/example-projects/contributed-/Settings`, which names the version the section
+documents (`lib/package-tests.mjs`). So a section moved to a new version moves its test project
+with it.
+
+## Writing a case
+
+A case is a parameterless `[TestCase]` Sub of a `[TestFixture]` Module. It starts with
+`Expect.Begin`, ends with `Expect.Done`, and checks with `Expect.AreEqual expected, actual, what`,
+`Expect.IsTrue`, `Expect.IsFalse` and `Expect.Fail what`. A failed check does not stop the case;
+`Expect.Done` reports every failed check at once.
+
+```vb
+[TestFixture]
+Module FilesTests
+
+    [TestCase]
+    Public Sub Item_MissingName()
+        Expect.Begin
+        Dim fso As New FileSystemObject
+        On Error Resume Next
+        Dim f As File
+        Set f = fso.GetFolder(Environ$("TEMP")).Files("no such file.txt")
+        Expect.AreEqual 53, Err.Number, "Files.Item of a missing name raises"
+        Expect.Done
+    End Sub
+
+End Module
+```
+
+- **A case checks what is correct**, not what the package does: a defect is a failing case, so the
+  project tells the package's author what to fix. What *correct* is comes from the package's own
+  description, the object it stands in for (VBScript's **RegExp**, the Scripting Runtime's
+  **FileSystemObject**) or the standard it implements.
+- **Name a case for the behaviour**, `_`, one behaviour per case, so a failure names
+  one defect. A callout that makes three claims gets three cases.
+- **A case is self-contained**: it creates what it needs and removes it. Files go only under
+  `%TEMP%\tbdocs-package-tests\\`; nothing in the registry, a special folder or the
+  user's profile changes. No `MsgBox`, `InputBox`, `End`, no window left open, and nothing that
+  starts a browser or explorer (`ShellExecute` or `OpenFile` with an empty path does).
+- **The checks' `what` is read by the author**: say what was done, so that
+  `Files.Item of a missing name raises: expected 53, got 5` is a bug report.
+- A claim about what does not compile is a **compile case**: a line ending in
+  `' CASE : TB5001` (or `: error`, `: none`), as `scripts/lib/tb-tests.mjs` describes.
+- A claim that needs a dialog operated or a form shown goes in the package's `probe/` harness, as
+  TBMANLIB_Dialog's and xCalendar's do.
+
+## Tying a callout to its cases
+
+Every `> [!NOTE]` and `> [!WARNING]` of the section's pages has a marker on the line above it:
+
+```md
+
+> [!NOTE]
+```
+
+An item is `Fixture.Sub`, `compile `, `probe ` (a scenario of `probe/`), or
+`none -- ` for a callout no program can check (a statement about the IDE, a licence).
+`node scripts/check_package_tests.mjs`, in `test.bat`, fails when a callout has no marker, an item
+names nothing, a case is named by no callout, a case does not start with `Expect.Begin` and end
+with `Expect.Done`, `expect.json` is for another version, or `RunAll.twin` is out of date. A
+section whose project is not written yet is in the script's `PENDING` set; the package's project
+takes it out.
+
+## Running
+
+```
+package-tests.bat tbRegExp                  both architectures, judged against expect.json
+package-tests.bat tbRegExp --arch win32
+package-tests.bat tbRegExp --update         write expect.json from the run
+package-tests.bat                           every project
+```
+
+`scripts/package_tests.mjs` stages the project with the package embedded (downloaded from TWINSERV
+into `%TEMP%\tb-twinserv` once) and runs it with `tbrun --tests`, which runs each case in the
+compiler's test mode, so a crash or an access violation ends only its case. Each case is then:
+
+- **documented**: as `expect.json` says: it passes, fails with the recorded report, or never
+  returns;
+- **fixed**: `expect.json` says it fails or never returns, and it passes. The page's callout goes
+  (the documentation is not a record of past defects), and so does the entry;
+- **changed**: anything else. A claim of the page is no longer true, or the case is wrong.
+
+A case that never returns stops the compiler, so it is caught, recorded as `{ "hangs": true }` by
+`--update`, run alone from then on, and skipped by `RunAll`. The run writes `RunAll.twin` again
+whenever the cases or `expect.json` change.
+
+`expect.json`:
+
+```json
+{
+  "version": "1.9.0.0",
+  "cases": {
+    "FilesTests.Item_MissingName": { "fails": "1 of 1 checks failed: Files.Item of a missing name raises: expected 53, got 5" },
+    "ListTests.ToString_Empty": { "hangs": true },
+    "SubClassTests.Hook": { "win32": "pass", "win64": { "fails": "..." } }
+  }
+}
+```
+
+A case not named passes on both architectures. Write it with `--update`, then read every entry: an
+entry is a claim that the package is defective, and each one must match a callout.
+
+## In the IDE
+
+`RunAll`'s `Sub Main` is the project's startup object: run the project and each case prints
+`PASS`, `FAIL` or `SKIP` in the DEBUG CONSOLE, then a count. That is how a reader or the package's
+author runs the published project. The download carries none of the package's code: it references
+the package as a *linked* package, which the IDE loads from the package's `.twinpack` in
+`%APPDATA%\twinBASIC\packages` (found by its project id) once the compiler has started. So the
+reader puts the package's file there, of the version the section documents, or adds the package
+from Available Packages with Embedded cleared, and restarts the compiler. The IDE takes the
+version from that file, not from the reference, and nothing in the IDE downloads a missing package
+by itself (BETA 1005).
diff --git a/test/package-probes/TBMANLIB_Dialog/README.md b/test/package-tests/TBMANLIB_Dialog/probe/README.md
similarity index 91%
rename from test/package-probes/TBMANLIB_Dialog/README.md
rename to test/package-tests/TBMANLIB_Dialog/probe/README.md
index 003df310f..0e90d2aa3 100644
--- a/test/package-probes/TBMANLIB_Dialog/README.md
+++ b/test/package-tests/TBMANLIB_Dialog/probe/README.md
@@ -15,12 +15,12 @@ store (`node indexer/twin-index.mjs sync`).
 ## Running it
 
 ```sh
-node test/package-probes/TBMANLIB_Dialog/probe.mjs 13,1,2,3 4,5 6 6d,7 8,9,9e 11,12,12b,12c 14,15 16,17 18
-node test/package-probes/TBMANLIB_Dialog/probe.mjs --timeout 35 x0,y1 x1,x2 x3,x4 x5 x6 x7 x8 x9 x10 x11,x12 x13,x14 x15,x16 y2,y3
-node test/package-probes/TBMANLIB_Dialog/probe.mjs --timeout 35 y4
-node test/package-probes/TBMANLIB_Dialog/probe.mjs --arch win64 --port 9722 13,1,2 3 4 5 6,6d 7,8 9 9e 11,12,12b,12c 14 15 16,17 18
-node test/package-probes/TBMANLIB_Dialog/probe.mjs c1,c11,c21,c31,c41 c2 c3 c4
-node test/package-probes/TBMANLIB_Dialog/probe.mjs d0,d1 d2 d3,d4 d5 d10 d11 d12 d20 d21 d22,d23 d30 d31 d32 d33 d34 d40
+node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs 13,1,2,3 4,5 6 6d,7 8,9,9e 11,12,12b,12c 14,15 16,17 18
+node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs --timeout 35 x0,y1 x1,x2 x3,x4 x5 x6 x7 x8 x9 x10 x11,x12 x13,x14 x15,x16 y2,y3
+node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs --timeout 35 y4
+node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs --arch win64 --port 9722 13,1,2 3 4 5 6,6d 7,8 9 9e 11,12,12b,12c 14 15 16,17 18
+node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs c1,c11,c21,c31,c41 c2 c3 c4
+node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs d0,d1 d2 d3,d4 d5 d10 d11 d12 d20 d21 d22,d23 d30 d31 d32 d33 d34 d40
 ```
 
 The first run of 1.1.0.0 ran every scenario in both builds without a crash, apart from `y4`.
diff --git a/test/package-probes/TBMANLIB_Dialog/probe.mjs b/test/package-tests/TBMANLIB_Dialog/probe/probe.mjs
similarity index 94%
rename from test/package-probes/TBMANLIB_Dialog/probe.mjs
rename to test/package-tests/TBMANLIB_Dialog/probe/probe.mjs
index 0836c48d9..929cfa04d 100644
--- a/test/package-probes/TBMANLIB_Dialog/probe.mjs
+++ b/test/package-tests/TBMANLIB_Dialog/probe/probe.mjs
@@ -2,7 +2,7 @@
 // ids, on a private desktop. What the package's dialogs do at run time, for the pages under
 // docs/Reference/Contributed/TBMANLIB_Dialog/: see README.md beside this file.
 //
-//   node test/package-probes/TBMANLIB_Dialog/probe.mjs [--arch win32|win64] [--port N]  [ ...]
+//   node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs [--arch win32|win64] [--port N]  [ ...]
 //
 // 1. The source tree is written to %TEMP%\dialog-probe\tree-: the contributed template's
 //    Settings (test/example-projects/contributed-TBMANLIB_Dialog), the package embedded under
@@ -22,9 +22,9 @@ import {
   parseCli,
   printHelpAndExit,
   withUsageError,
-} from "../../../lib/cli.mjs";
+} from "../../../../lib/cli.mjs";
 
-const USAGE = `usage: node test/package-probes/TBMANLIB_Dialog/probe.mjs [--arch win32|win64] [--port N] [--timeout S] [--overlay D]  [ ...] [-h, --help]
+const USAGE = `usage: node test/package-tests/TBMANLIB_Dialog/probe/probe.mjs [--arch win32|win64] [--port N] [--timeout S] [--overlay D]  [ ...] [-h, --help]
 
 Builds the TBMANLIB_Dialog probe (src/) with tbrun, then runs the exe on a private desktop once
 for each , a comma-separated list of the scenario ids listed at the top of src/Probe.twin.
@@ -72,7 +72,7 @@ const { arch, port, timeout } = withUsageError(
 );
 
 const here = import.meta.dirname;
-const repo = path.resolve(here, "..", "..", "..");
+const repo = path.resolve(here, "..", "..", "..", "..");
 const pkg = path.join(repo, "indexer", ".packages", "contributed", "TBMANLIB_Dialog");
 if (!existsSync(path.join(pkg, "sources"))) {
   console.error(`probe: no package sources in ${pkg}; run \`node indexer/twin-index.mjs sync\` first`);
diff --git a/test/package-probes/TBMANLIB_Dialog/run-exe.mjs b/test/package-tests/TBMANLIB_Dialog/probe/run-exe.mjs
similarity index 93%
rename from test/package-probes/TBMANLIB_Dialog/run-exe.mjs
rename to test/package-tests/TBMANLIB_Dialog/probe/run-exe.mjs
index f8ea280ec..15b9f01fd 100644
--- a/test/package-probes/TBMANLIB_Dialog/run-exe.mjs
+++ b/test/package-tests/TBMANLIB_Dialog/probe/run-exe.mjs
@@ -1,6 +1,6 @@
 // Runs a built probe exe on a private desktop, WITHOUT tbrun --exe's dialog closer.
 //
-//   node test/package-probes/TBMANLIB_Dialog/run-exe.mjs   [--timeout S] [--desktop NAME]
+//   node test/package-tests/TBMANLIB_Dialog/probe/run-exe.mjs   [--timeout S] [--desktop NAME]
 //
 // tbrun --exe closes every dialog the exe opens within 250 ms by pressing its OK button, which
 // would race the probe's own automation (src/Auto.twin) and answer the Yes/No boxes itself. This
@@ -20,11 +20,11 @@
 import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
 import { tmpdir } from "node:os";
 import path from "node:path";
-import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../lib/cli.mjs";
-import { captureWindow, desktopWindows, killTree, launchOnDesktop } from "../../../scripts/lib/tb-ide.mjs";
-import { faultText, recordedFault } from "../../../scripts/lib/win-fault.mjs";
+import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../../lib/cli.mjs";
+import { captureWindow, desktopWindows, killTree, launchOnDesktop } from "../../../../scripts/lib/tb-ide.mjs";
+import { faultText, recordedFault } from "../../../../scripts/lib/win-fault.mjs";
 
-const USAGE = `usage: node test/package-probes/TBMANLIB_Dialog/run-exe.mjs   [--timeout S] [--desktop NAME] [-h, --help]
+const USAGE = `usage: node test/package-tests/TBMANLIB_Dialog/probe/run-exe.mjs   [--timeout S] [--desktop NAME] [-h, --help]
 
 Runs the TBMANLIB_Dialog probe exe on a private desktop, with the scenarios  names
 (comma-separated, as listed at the top of src/Probe.twin), and prints what they report.
diff --git a/test/package-probes/TBMANLIB_Dialog/src/Auto.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/Auto.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/Auto.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/Auto.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/AutoD.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/AutoD.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/AutoD.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/AutoD.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/DrvB1.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/DrvB1.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/DrvB1.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/DrvB1.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/Probe.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/Probe.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/Probe.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/Probe.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbeB1.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbeB1.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbeB1.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbeB1.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbeD.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbeD.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbeD.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbeD.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbeDT.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbeDT.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbeDT.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbeDT.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbeEx.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbeEx.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbeEx.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbeEx.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbeIB.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbeIB.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbeIB.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbeIB.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbePG.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbePG.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbePG.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbePG.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProbePW.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProbePW.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProbePW.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProbePW.twin
diff --git a/test/package-probes/TBMANLIB_Dialog/src/ProgSink.twin b/test/package-tests/TBMANLIB_Dialog/probe/src/ProgSink.twin
similarity index 100%
rename from test/package-probes/TBMANLIB_Dialog/src/ProgSink.twin
rename to test/package-tests/TBMANLIB_Dialog/probe/src/ProgSink.twin
diff --git a/test/package-probes/xCalendar/README.md b/test/package-tests/xCalendar/probe/README.md
similarity index 90%
rename from test/package-probes/xCalendar/README.md
rename to test/package-tests/xCalendar/probe/README.md
index cc3d26efc..c409cba34 100644
--- a/test/package-probes/xCalendar/README.md
+++ b/test/package-tests/xCalendar/probe/README.md
@@ -20,14 +20,14 @@ declared in the package's `xCalendar.tbcontrol`.
 ## Running it
 
 ```sh
-node test/package-probes/xCalendar/probe.mjs 1 2,3 4,5 6,7,8 9 10 11,12 13 14
-node test/package-probes/xCalendar/probe.mjs --arch win64 --port 9911 1 2,3 4,5 6,7,8 9 10 11,12 13 14
-node test/package-probes/xCalendar/probe.mjs --shots  15 15b
-node test/package-probes/xCalendar/probe.mjs p2,p3 p5,p7 p9 p10 p11 p12 p13 p14 p18 p19 p20,p21 p24,p25 p29 p33 p22x
-node test/package-probes/xCalendar/probe.mjs 16 17
-node test/package-probes/xCalendar/probe.mjs 12x 12y 12z 12w   # each opens the run-time error box, except 12w: see below
-node test/package-probes/xCalendar/designer.mjs --edit Enabled=False --out .tbform
-node test/package-probes/xCalendar/probe.mjs --form .tbform 1     # a control that loads disabled
+node test/package-tests/xCalendar/probe/probe.mjs 1 2,3 4,5 6,7,8 9 10 11,12 13 14
+node test/package-tests/xCalendar/probe/probe.mjs --arch win64 --port 9911 1 2,3 4,5 6,7,8 9 10 11,12 13 14
+node test/package-tests/xCalendar/probe/probe.mjs --shots  15 15b
+node test/package-tests/xCalendar/probe/probe.mjs p2,p3 p5,p7 p9 p10 p11 p12 p13 p14 p18 p19 p20,p21 p24,p25 p29 p33 p22x
+node test/package-tests/xCalendar/probe/probe.mjs 16 17
+node test/package-tests/xCalendar/probe/probe.mjs 12x 12y 12z 12w   # each opens the run-time error box, except 12w: see below
+node test/package-tests/xCalendar/probe/designer.mjs --edit Enabled=False --out .tbform
+node test/package-tests/xCalendar/probe/probe.mjs --form .tbform 1     # a control that loads disabled
 ```
 
 Each argument is one run of the exe; the scenario ids are listed at the top of
diff --git a/test/package-probes/xCalendar/designer.mjs b/test/package-tests/xCalendar/probe/designer.mjs
similarity index 91%
rename from test/package-probes/xCalendar/designer.mjs
rename to test/package-tests/xCalendar/probe/designer.mjs
index efe8b4f86..4f97e5958 100644
--- a/test/package-probes/xCalendar/designer.mjs
+++ b/test/package-tests/xCalendar/probe/designer.mjs
@@ -2,7 +2,7 @@
 // properties in the form designer's Properties panel as a person does, saves, and prints what the
 // IDE wrote for the control into Form1.tbform.
 //
-//   node test/package-probes/xCalendar/designer.mjs [--port N] [--edit Name=text ...] [--out FILE]
+//   node test/package-tests/xCalendar/probe/designer.mjs [--port N] [--edit Name=text ...] [--out FILE]
 //
 // Why: the control keeps its design-time values in a property bag (its ReadProperties and
 // WriteProperties), and what the IDE stores in the form's designer file for it cannot be written by
@@ -16,16 +16,16 @@
 import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
 import { tmpdir } from "node:os";
 import path from "node:path";
-import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../lib/cli.mjs";
-import { sleep } from "../../../scripts/lib/tb-ide.mjs";
-import { findIde } from "../../../scripts/lib/tb-install.mjs";
-import { Lane } from "../../../scripts/lib/tb-lane.mjs";
-import { openFile, pressKey, typeText, waitFor } from "../../../scripts/lib/tb-operate.mjs";
-import { wrapProbe } from "../../../scripts/lib/tb-probe.mjs";
-import { unpackProject } from "../../../scripts/lib/tb-project.mjs";
+import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../../lib/cli.mjs";
+import { sleep } from "../../../../scripts/lib/tb-ide.mjs";
+import { findIde } from "../../../../scripts/lib/tb-install.mjs";
+import { Lane } from "../../../../scripts/lib/tb-lane.mjs";
+import { openFile, pressKey, typeText, waitFor } from "../../../../scripts/lib/tb-operate.mjs";
+import { wrapProbe } from "../../../../scripts/lib/tb-probe.mjs";
+import { unpackProject } from "../../../../scripts/lib/tb-project.mjs";
 import { stageTree } from "./stage.mjs";
 
-const USAGE = `usage: node test/package-probes/xCalendar/designer.mjs [--port N] [--edit Name=text ...] [--out FILE] [-h, --help]
+const USAGE = `usage: node test/package-tests/xCalendar/probe/designer.mjs [--port N] [--edit Name=text ...] [--out FILE] [-h, --help]
 
 Opens the probe's project in an IDE, selects xCalendar1 in Form1's designer, types each --edit
 value into the Properties panel's cell for that property (Name=text), saves the project and prints
diff --git a/test/package-probes/xCalendar/patch-stream.mjs b/test/package-tests/xCalendar/probe/patch-stream.mjs
similarity index 95%
rename from test/package-probes/xCalendar/patch-stream.mjs
rename to test/package-tests/xCalendar/probe/patch-stream.mjs
index 12729ee26..a564df1bc 100644
--- a/test/package-probes/xCalendar/patch-stream.mjs
+++ b/test/package-tests/xCalendar/probe/patch-stream.mjs
@@ -1,6 +1,6 @@
 // Makes the designer data of a control that was saved on another day.
 //
-//   node test/package-probes/xCalendar/patch-stream.mjs   Year=2020 Month=5 Day=17
+//   node test/package-tests/xCalendar/probe/patch-stream.mjs   Year=2020 Month=5 Day=17
 //
 // designer.mjs gets the IDE to write xCalendar1's entry in a Form1.tbform: the properties as plain
 // keys, and `_IPersistStreamData`, the property bag the control's ReadProperties reads at run time
diff --git a/test/package-probes/xCalendar/probe.mjs b/test/package-tests/xCalendar/probe/probe.mjs
similarity index 91%
rename from test/package-probes/xCalendar/probe.mjs
rename to test/package-tests/xCalendar/probe/probe.mjs
index 0f0d659c8..c7191885d 100644
--- a/test/package-probes/xCalendar/probe.mjs
+++ b/test/package-tests/xCalendar/probe/probe.mjs
@@ -2,7 +2,7 @@
 // on a private desktop. What the package's xCalendar control does at run time, for the pages under
 // docs/Reference/Contributed/xCalendar/: see README.md beside this file.
 //
-//   node test/package-probes/xCalendar/probe.mjs [--arch win32|win64] [--port N] [--shots DIR] [--form FILE] [--set K=JSON]  [ ...]
+//   node test/package-tests/xCalendar/probe/probe.mjs [--arch win32|win64] [--port N] [--shots DIR] [--form FILE] [--set K=JSON]  [ ...]
 //
 // 1. stage.mjs writes the source tree to %TEMP%\xcal-probe\tree-.
 // 2. tbrun builds it (its [RunAfterBuild] Sub only prints; no form opens in the IDE), with
@@ -19,10 +19,10 @@ import {
   parseCli,
   printHelpAndExit,
   withUsageError,
-} from "../../../lib/cli.mjs";
+} from "../../../../lib/cli.mjs";
 import { stageTree } from "./stage.mjs";
 
-const USAGE = `usage: node test/package-probes/xCalendar/probe.mjs [--arch win32|win64] [--port N] [--shots DIR] [--form FILE] [--set K=JSON]  [ ...] [-h, --help]
+const USAGE = `usage: node test/package-tests/xCalendar/probe/probe.mjs [--arch win32|win64] [--port N] [--shots DIR] [--form FILE] [--set K=JSON]  [ ...] [-h, --help]
 
 Builds the xCalendar probe (src/) with tbrun, then runs the exe on a private desktop once for each
 , a comma-separated list of the scenario ids listed at the top of src/Probe.twin.
@@ -69,7 +69,7 @@ const { arch, port } = withUsageError(
 );
 
 const here = import.meta.dirname;
-const repo = path.resolve(here, "..", "..", "..");
+const repo = path.resolve(here, "..", "..", "..", "..");
 
 // 1. The source tree.
 let tree;
diff --git a/test/package-probes/xCalendar/run-exe.mjs b/test/package-tests/xCalendar/probe/run-exe.mjs
similarity index 93%
rename from test/package-probes/xCalendar/run-exe.mjs
rename to test/package-tests/xCalendar/probe/run-exe.mjs
index 9c8801509..1a9241f80 100644
--- a/test/package-probes/xCalendar/run-exe.mjs
+++ b/test/package-tests/xCalendar/probe/run-exe.mjs
@@ -1,6 +1,6 @@
 // Runs a built xCalendar probe exe on a private desktop.
 //
-//   node test/package-probes/xCalendar/run-exe.mjs   [--timeout S] [--desktop NAME] [--shots DIR]
+//   node test/package-tests/xCalendar/probe/run-exe.mjs   [--timeout S] [--desktop NAME] [--shots DIR]
 //
 // The exe is started as tbrun --exe starts it (launchOnDesktop: a private desktop, in a
 // kill-on-close job, so nothing appears on the user's desktop), and what it wrote with TbRun.Out
@@ -20,11 +20,11 @@ import { spawnSync } from "node:child_process";
 import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
 import os, { tmpdir } from "node:os";
 import path from "node:path";
-import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../lib/cli.mjs";
-import { captureWindow, desktopWindows, killTree, launchOnDesktop } from "../../../scripts/lib/tb-ide.mjs";
-import { faultText, recordedFault } from "../../../scripts/lib/win-fault.mjs";
+import { exitOnCrash, numberOption, parseCli, printHelpAndExit, withUsageError } from "../../../../lib/cli.mjs";
+import { captureWindow, desktopWindows, killTree, launchOnDesktop } from "../../../../scripts/lib/tb-ide.mjs";
+import { faultText, recordedFault } from "../../../../scripts/lib/win-fault.mjs";
 
-const USAGE = `usage: node test/package-probes/xCalendar/run-exe.mjs   [--timeout S] [--desktop NAME] [--shots DIR] [-h, --help]
+const USAGE = `usage: node test/package-tests/xCalendar/probe/run-exe.mjs   [--timeout S] [--desktop NAME] [--shots DIR] [-h, --help]
 
 Runs the xCalendar probe exe on a private desktop, with the scenarios  names
 (comma-separated, as listed at the top of src/Probe.twin), and prints what they report.
diff --git a/test/package-probes/xCalendar/src/Form1.tbform b/test/package-tests/xCalendar/probe/src/Form1.tbform
similarity index 100%
rename from test/package-probes/xCalendar/src/Form1.tbform
rename to test/package-tests/xCalendar/probe/src/Form1.tbform
diff --git a/test/package-probes/xCalendar/src/Form1.twin b/test/package-tests/xCalendar/probe/src/Form1.twin
similarity index 100%
rename from test/package-probes/xCalendar/src/Form1.twin
rename to test/package-tests/xCalendar/probe/src/Form1.twin
diff --git a/test/package-probes/xCalendar/src/Probe.twin b/test/package-tests/xCalendar/probe/src/Probe.twin
similarity index 100%
rename from test/package-probes/xCalendar/src/Probe.twin
rename to test/package-tests/xCalendar/probe/src/Probe.twin
diff --git a/test/package-probes/xCalendar/src/Win.twin b/test/package-tests/xCalendar/probe/src/Win.twin
similarity index 100%
rename from test/package-probes/xCalendar/src/Win.twin
rename to test/package-tests/xCalendar/probe/src/Win.twin
diff --git a/test/package-probes/xCalendar/stage.mjs b/test/package-tests/xCalendar/probe/stage.mjs
similarity index 91%
rename from test/package-probes/xCalendar/stage.mjs
rename to test/package-tests/xCalendar/probe/stage.mjs
index dc39d86ef..465e0c6c7 100644
--- a/test/package-probes/xCalendar/stage.mjs
+++ b/test/package-tests/xCalendar/probe/stage.mjs
@@ -10,12 +10,12 @@
 import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
 import { tmpdir } from "node:os";
 import path from "node:path";
-import { downloadPackage } from "../../../indexer/lib/twinserv-client.mjs";
-import { embeddedPackages } from "../../../scripts/lib/example-batches.mjs";
-import { unpackProject } from "../../../scripts/lib/tb-project.mjs";
+import { downloadPackage } from "../../../../indexer/lib/twinserv-client.mjs";
+import { embeddedPackages } from "../../../../scripts/lib/example-batches.mjs";
+import { unpackProject } from "../../../../scripts/lib/tb-project.mjs";
 
 const here = import.meta.dirname;
-const template = path.join(here, "..", "..", "example-projects", "contributed-xCalendar");
+const template = path.join(here, "..", "..", "..", "example-projects", "contributed-xCalendar");
 
 /** The text with CRLF line ends, as the IDE writes a source file. */
 export const crlf = (text) => text.replace(/\r?\n/g, "\r\n");

From c94b3be7f9a041ce8eeee9b9e8cbcd9126b202c0 Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Wed, 14 Oct 2026 11:12:45 +0200
Subject: [PATCH 06/13] package-tests: a package is named (in any case) or
 --all-packages is given, never all by default; --hang-timeout and
 --all-packages read under their camelCase keys; check_cli cases for both
 tools; WIP.md: Test cases after See Also on a contributed landing page

---
 WIP.md                              |  4 ++--
 docs/Documentation/Tools-Samples.md |  4 +++-
 docs/Documentation/Tools.md         |  5 ++--
 package-tests.bat                   |  6 ++++-
 scripts/lib/cli-cases.mjs           | 10 ++++++++
 scripts/package_tests.mjs           | 37 ++++++++++++++++++++++-------
 test/package-tests/README.md        |  7 ++++--
 7 files changed, 56 insertions(+), 17 deletions(-)

diff --git a/WIP.md b/WIP.md
index 4b97b4963..3e35e9056 100644
--- a/WIP.md
+++ b/WIP.md
@@ -230,7 +230,7 @@ The vocabulary tables further down cover word choice. The rules in this subsecti
 
    **A known twinBASIC defect that silently gives a wrong result is a `> [!WARNING]` until a fixed build is released** (owner's rule): a wrong value with no error raised is data corruption the reader cannot see. A crash is a WARNING too (unsaved data is lost), and so is an error that hides a silent part (a procedure run twice before the error). A defect the reader cannot miss and that loses nothing --- an error raised, a statement refused --- is a `> [!NOTE]`. Name the build in either. **Once a fixed build is released, the callout goes** (owner's rule): the documentation is not a record of past defects, so the page then states the correct behaviour with no mention of the defect. The defect's BUGS-TO-REPORT entry says in its HTML comment which page carries the callout, so the callout is removed when the entry is retired.
 
-7. **See Also.** Last section on the page, after `Example`. Format: `- [Symbol](Symbol) ` where `` is the kind: statement, function, property, method, class, module, package. Pages with annotations use `- [Symbol](Symbol) -- short description` — the `--` source renders as a typographic dash via markdown-it's typographer (en-dash for `--`, em-dash for `---`). Don't write literal `—` in source; keep `--` for consistency across the docs. Order by conceptual proximity, not strict alphabetical.
+7. **See Also.** Last section on the page, after `Example`; only a contributed package's landing page has one after it, `## Test cases`. Format: `- [Symbol](Symbol) ` where `` is the kind: statement, function, property, method, class, module, package. Pages with annotations use `- [Symbol](Symbol) -- short description` — the `--` source renders as a typographic dash via markdown-it's typographer (en-dash for `--`, em-dash for `---`). Don't write literal `—` in source; keep `--` for consistency across the docs. Order by conceptual proximity, not strict alphabetical.
 
 8. **Name the fault directly; never build up to it.** Setting up a contrast and then withholding the point is coy, and it makes the reader parse the sentence twice to extract one fact. *Avoid:* "double-clicking it is the obvious shortcut, and it is the one that misleads"; "the specificity trap --- which is the one thing that reliably catches people out". *Use:* "double-clicking it is obvious, and wrong"; "the specificity trap: a rule that loses it applies in light mode and silently does not in dark". Say what the thing is and what it does, in that order, in one clause. The same applies to "and that is the one that…", "which is precisely the…" and "therein lies the…".
 
@@ -518,7 +518,7 @@ Why the report separates the wedged task from the merely blocked ones, and why
 - `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\twinBASIC Book.pdf`. Run `build.bat` first to populate `_site-pdf/`; `book.bat` refuses a tree older than its sources rather than rendering the previous book (see [The book refuses a stale source tree](WIP.Build.md#the-book-refuses-a-stale-source-tree)).
 
 - `examples.bat` — compiles the documentation's own twinBASIC code samples, every `tb` fence marked `check_build`, and reports the ones the compiler refuses against the line in the page they came from; a statement sample also marked `check_run` is built and run, and what it prints is checked against the page. Needs a twinBASIC install and Windows, so it is outside every gate and outside CI; ~110 s over the 1,370 samples marked (88 of them `check_run`). `--build` also builds each project that compiles clean, and `--llvm` builds it with LLVM (see [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md)). Two modes need no compiler at all: `--census` classifies every fence and says how many classifiable ones are still unmarked, and `--report ` groups a saved `--propose --json` survey by diagnostic, section and unresolved name. `--propose` itself does compile. See [Compiling the reference's own code samples](#compiling-the-references-own-code-samples) and [WIP.ExamplesBuild.md](WIP.ExamplesBuild.md).
-- `package-tests.bat` --- runs the contributed packages' test projects (`scripts/package_tests.mjs`): each project under `test/package-tests/` is staged with its package embedded and run with `tbrun --tests` as 32-bit and 64-bit, and each case is judged against `expect.json` as *documented*, *fixed* (the callout goes) or *changed* (a claim of the page is no longer true). `--update` writes `expect.json` from the run; read every entry it writes, since each is a claim that the package is defective. A case that never returns is caught and run alone. It also rewrites the generated `RunAll.twin`. Outside every gate and CI, as `examples.bat` is. The build publishes each project, referencing the package as a linked package and carrying none of its code, as `tB/Packages/Contributed//-tests.twinproj` in the online tree (the `packageTests` task, `builder/package-tests.mjs`, `_config.yml`'s `package_tests`; the publish allowlist exempts those paths, and the offline tree's links go to the website).
+- `package-tests.bat` --- runs the test projects of the contributed packages named (in any case: `tbregexp`), or of every one with `--all-packages`, which is never the default (`scripts/package_tests.mjs`): each project under `test/package-tests/` is staged with its package embedded and run with `tbrun --tests` as 32-bit and 64-bit, and each case is judged against `expect.json` as *documented*, *fixed* (the callout goes) or *changed* (a claim of the page is no longer true). `--update` writes `expect.json` from the run; read every entry it writes, since each is a claim that the package is defective. A case that never returns is caught and run alone. It also rewrites the generated `RunAll.twin`. Outside every gate and CI, as `examples.bat` is. The build publishes each project, referencing the package as a linked package and carrying none of its code, as `tB/Packages/Contributed//-tests.twinproj` in the online tree (the `packageTests` task, `builder/package-tests.mjs`, `_config.yml`'s `package_tests`; the publish allowlist exempts those paths, and the offline tree's links go to the website).
 - `addin-test.bat` — tests IDE add-ins by operating an IDE: every lane in `test/addin/lanes.mjs` builds the add-ins it tests into a private copy of the install, opens a project and checks what the add-in does. Outside every gate and outside CI for the same reasons as `examples.bat`; about two minutes for the seventeen lanes today (BETA 997): Samples 10 and 15, the thirteen probe lanes behind Stage 2's answers in [WIP.HelpAddin.md](WIP.HelpAddin.md), and the help add-in in `add-in/` twice, built with its committed copy of the symbol index, `add-in/Resources/SYMBOLS/symbols.json`, which a local build rewrites whenever the index changes. The `help` lane serves the built `docs/_site` on localhost to the add-in's pane; `help-offline` builds the add-in with a help archive of `docs/_site-offline` and the pane's pages come from the add-in's own server. So run `build.bat` first. Exit 0 every lane passed and the registry is as it was found, 1 a lane failed, 2 the harness failed, 3 the registry or a work folder was not put back. See [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler) for its rules.
 - `ide-test.bat` --- the same runner for scenarios that operate the IDE itself rather than an add-in: every lane in `test/ide/lanes.mjs`, base port 9660. Same exit codes, same standing outside every gate and outside CI, same rules.
 - `try-help-addin.bat` --- opens one IDE on the user's desktop with the help add-in built and loaded, to try it by hand, and puts the registry back once it is closed (see [Driving the twinBASIC compiler](#driving-the-twinbasic-compiler)). Needs `build.bat` first. Exit 0 closed and put back, 1 the add-in or the project did not compile, 2 the tool could not run, 3 not put back.
diff --git a/docs/Documentation/Tools-Samples.md b/docs/Documentation/Tools-Samples.md
index 373510bbc..1593f5bdd 100644
--- a/docs/Documentation/Tools-Samples.md
+++ b/docs/Documentation/Tools-Samples.md
@@ -106,7 +106,9 @@ Exit codes: **0** every marked sample compiles, or none is marked (`--report` al
 ## package_tests.mjs
 {: #package-tests }
 
-    node scripts/package_tests.mjs [ ...] [--arch win32|win64|both] [--port N] [--timeout S] [--hang-timeout S] [--update] [--ide ] [--json]
+    node scripts/package_tests.mjs ( ... | --all-packages) [--arch win32|win64|both] [--port N] [--timeout S] [--hang-timeout S] [--update] [--ide ] [--json]
+
+It runs the packages named, each matched to its folder in any case (`tbregexp` is `tbRegExp`), or with `--all-packages` every package that has a test project; a whole run takes several minutes a package, so it is never the default. With neither, or with both, it refuses and lists the packages that have a test project.
 
 Runs the contributed packages' test projects. Each package the reference documents under `docs/Reference/Contributed/` has one in `test/package-tests//`: `[TestCase]` Subs of `[TestFixture]` modules that check every claim the NOTEs and WARNINGs of its section make, a generated `RunAll` module, and `expect.json`. The README in that folder says how a case is written. Each case checks what is correct, so a defect of the package is a failing case, and `expect.json` records how each case fails, or that it never returns, on the version the section documents.
 
diff --git a/docs/Documentation/Tools.md b/docs/Documentation/Tools.md
index c14a261ba..9ee29b56d 100644
--- a/docs/Documentation/Tools.md
+++ b/docs/Documentation/Tools.md
@@ -244,11 +244,12 @@ Exit codes, those of [`check_examples.mjs`](Tools-Samples#check-examples): **0**
 ### package-tests.bat
 {: #package-testsbat }
 
-    package-tests.bat [ ...] [flags]
+    package-tests.bat  [ ...] [flags]
+    package-tests.bat --all-packages [flags]
 
 One invocation of [`package_tests.mjs`](Tools-Samples#package-tests), with every argument passed straight through:
 
-    node scripts/package_tests.mjs [ ...] [flags]
+    node scripts/package_tests.mjs  [ ...] [flags]
 
 Runs the contributed packages' test projects under `test/package-tests/`, which back the NOTEs and WARNINGs of each package's section with test cases, and says which cases no longer do what the package's `expect.json` records: fixed, so the page's callout goes, or changed, so a claim of the page is no longer true.
 
diff --git a/package-tests.bat b/package-tests.bat
index 14241786b..73bfe8a89 100644
--- a/package-tests.bat
+++ b/package-tests.bat
@@ -15,9 +15,13 @@ rem needs a twinBASIC install, Windows and a private desktop. The gate that
 rem ties the pages' callouts to the cases, check_package_tests.mjs, needs
 rem none of that and is in test.bat. Arguments are passed straight through:
 rem
-rem   package-tests.bat                       every project
+rem   package-tests.bat tbregexp              one package, named in any case
 rem   package-tests.bat tbRegExp --arch win32 one package, one architecture
 rem   package-tests.bat tbRegExp --update     write its expect.json from the run
+rem   package-tests.bat --all-packages        every project, several minutes each
+rem
+rem With no package named and no --all-packages it refuses, and lists the
+rem packages that have a test project.
 rem
 rem Exit: 0 every case as documented, 1 a case fixed or changed, 2 the harness
 rem failed.
diff --git a/scripts/lib/cli-cases.mjs b/scripts/lib/cli-cases.mjs
index b6adf54b8..53303b739 100644
--- a/scripts/lib/cli-cases.mjs
+++ b/scripts/lib/cli-cases.mjs
@@ -110,6 +110,12 @@ const CASES = [
   { tool: "scripts/tbrun.mjs", args: ["no-such-dir", "--arch", "win64"], exit: 2, stderr: /^not a directory: .*no-such-dir\ntbrun takes an exported source tree / },
   { tool: "scripts/bug_repro.mjs", args: [], exit: 2, stderr: /^usage: node scripts\/bug_repro\.mjs / },
   { tool: "scripts/bug_repro.mjs", args: ["compile", "--help", "--bogus"], exit: 0, stdout: /^usage: node scripts\/bug_repro\.mjs / },
+  // package_tests runs a package only when named, or every one with --all-packages: neither, or
+  // both, is refused, and so is a name with no test project, before any IDE starts.
+  { tool: "scripts/package_tests.mjs", args: [], exit: 2, stderr: /^name a package, or pass --all-packages; packages with a test project: .*\nusage: node scripts\/package_tests\.mjs / },
+  { tool: "scripts/package_tests.mjs", args: ["tbregexp", "--all-packages"], exit: 2, stderr: /^name packages or pass --all-packages, not both\nusage: node scripts\/package_tests\.mjs / },
+  { tool: "scripts/package_tests.mjs", args: ["no-such-package"], exit: 2, stderr: /^no test project for no-such-package; packages with a test project: .*\nusage: node scripts\/package_tests\.mjs / },
+  { tool: "scripts/package_tests.mjs", args: ["--hang-timeout", "0"], exit: 2, stderr: /^--hang-timeout expects a number greater than 0, got: 0\nusage: node scripts\/package_tests\.mjs / },
   { tool: "scripts/vb6run.mjs", args: [], exit: 2, stderr: /^give a file to run, or --docs\nusage: node scripts\/vb6run\.mjs / },
   { tool: "scripts/vb6run.mjs", args: ["x.bas", "--help"], exit: 0, stdout: /^usage: node scripts\/vb6run\.mjs / },
   { tool: "scripts/vb6run.mjs", args: ["--docs", "x.bas"], exit: 2, stderr: /^--docs takes no file\nusage: node scripts\/vb6run\.mjs / },
@@ -387,6 +393,7 @@ const HELP_TOOLS = {
   "scripts/check_links.mjs": "Usage: node check_links.mjs [options] \n",
   "scripts/check_links_diff.mjs": "Usage: node scripts/check_links_diff.mjs [options]\n",
   "scripts/check_lint.mjs": null,
+  "scripts/check_package_tests.mjs": null,
   "scripts/check_page_baseline.mjs": null,
   "scripts/check_pdf_shims_equiv.mjs": null,
   "scripts/check_publish_policy.mjs": null,
@@ -400,6 +407,7 @@ const HELP_TOOLS = {
   "scripts/crawl_check.mjs": null,
   "scripts/gen_attribute_probes.mjs": "Generate a twinBASIC probe project for Reference/Attributes.md applicability.\n",
   "scripts/impexp.mjs": "Usage:\n",
+  "scripts/package_tests.mjs": null,
   "scripts/pick_a11y_sample.mjs": null,
   "scripts/probe_build_twice.mjs": null,
   "scripts/probe_ide_image_labels.mjs": null,
@@ -469,6 +477,7 @@ const REFUSALS = {
   "scripts/check_links.mjs": ["root-dir", { prefix: "error: " }],
   "scripts/check_links_diff.mjs": ["a"],
   "scripts/check_lint.mjs": [null, { prefix: "check_lint: " }],
+  "scripts/check_package_tests.mjs": [null],
   "scripts/check_page_baseline.mjs": [null],
   "scripts/check_pdf_shims_equiv.mjs": [null],
   "scripts/check_publish_policy.mjs": ["src"],
@@ -482,6 +491,7 @@ const REFUSALS = {
   "scripts/crawl_check.mjs": ["timeout"],
   "scripts/gen_attribute_probes.mjs": [null],
   "scripts/impexp.mjs": [null, { prefix: "ERROR: " }],
+  "scripts/package_tests.mjs": ["ide"],
   "scripts/pick_a11y_sample.mjs": ["sweep"],
   "scripts/probe_build_twice.mjs": ["ide"],
   "scripts/probe_ide_image_labels.mjs": ["ide"],
diff --git a/scripts/package_tests.mjs b/scripts/package_tests.mjs
index 54f008fc2..4e86380e0 100644
--- a/scripts/package_tests.mjs
+++ b/scripts/package_tests.mjs
@@ -1,8 +1,13 @@
 // Runs the package test projects, test/package-tests//, and judges each case against
 // the package's expect.json.
 //
-//     node scripts/package_tests.mjs [ ...] [options]
+//     node scripts/package_tests.mjs  [ ...] [options]
+//     node scripts/package_tests.mjs --all-packages [options]
 //
+//                a package with a test project, matched to its folder without regard
+//                         to case (tbregexp is tbRegExp)
+//       --all-packages    every package with a test project; a run of all of them takes several
+//                         minutes a package, so it is never the default
 //       --arch         win32, win64 or both (default both)
 //       --port         the first DevTools port to try (default 9620); the run claims the first
 //                         free one from there (scripts/lib/tb-ports.mjs)
@@ -76,13 +81,15 @@ const TBRUN = path.join(REPO_ROOT, "scripts", "tbrun.mjs");
 const CACHE = path.join(tmpdir(), "tb-twinserv");
 const WORK = path.join(tmpdir(), "tb-package-tests");
 
-const USAGE = `usage: node scripts/package_tests.mjs [ ...] [--arch win32|win64|both] [--port N] [--timeout S] [--hang-timeout S] [--update] [--ide ] [--json] [-h, --help]
+const USAGE = `usage: node scripts/package_tests.mjs ( ... | --all-packages) [--arch win32|win64|both] [--port N] [--timeout S] [--hang-timeout S] [--update] [--ide ] [--json] [-h, --help]
 
-Runs the package test projects under test/package-tests/ with tbrun --tests, each
+Runs package test projects under test/package-tests/ with tbrun --tests, each
 with its package embedded, and judges every case against the package's
 expect.json: documented (as it says), fixed (expected to fail or never return,
-and passes) or changed. With no package named, runs every project.
+and passes) or changed. Name one or more packages (in any case), or pass
+--all-packages to run every project.
 
+  --all-packages          run every package's test project
   --arch               win32, win64 or both (default both)
   --port               the first DevTools port to try (default 9620)
   --timeout         how long one case may take (default 60)
@@ -95,7 +102,8 @@ and passes) or changed. With no package named, runs every project.
 Exit codes:
   0  every case did what expect.json says
   1  a case was fixed or changed
-  2  a refused command line, a package with no test project, or the harness failed`;
+  2  a refused command line (no package named and no --all-packages, a package
+     with no test project), or the harness failed`;
 
 const { values, positionals } = withUsageError(
   () =>
@@ -106,6 +114,7 @@ const { values, positionals } = withUsageError(
         timeout: { type: "string" },
         "hang-timeout": { type: "string" },
         update: { type: "boolean", default: false },
+        "all-packages": { type: "boolean", default: false },
         ide: { type: "string" },
         json: { type: "boolean", default: false },
         help: { type: "boolean", short: "h", default: false },
@@ -125,17 +134,27 @@ const opts = withUsageError(
         : [values.arch],
     port: numberOption(values.port ?? "9620", { option: "--port", integer: true, min: 1, max: 65535 }),
     timeout: numberOption(values.timeout ?? "60", { option: "--timeout", above: 0 }),
-    hangTimeout: numberOption(values["hang-timeout"] ?? "20", { option: "--hang-timeout", above: 0 }),
+    hangTimeout: numberOption(values.hangTimeout ?? "20", { option: "--hang-timeout", above: 0 }),
   }),
   { format: (err) => `${err.message}\n${USAGE}` },
 );
 
 exitOnCrash();
 
+// The packages to run: those named, each matched to its folder without regard to case, or all
+// of them with --all-packages. Neither is refused, and so are both, before anything starts.
 const known = packageTestNames(ROOT);
-const packages = positionals.length ? positionals : known;
-const missing = packages.filter((p) => !known.includes(p));
-if (missing.length) die(2, `no test project for ${missing.join(", ")} under test/package-tests/\n${USAGE}`);
+const knownList = known.length ? known.join(", ") : "none";
+if (values.allPackages && positionals.length) die(2, `name packages or pass --all-packages, not both\n${USAGE}`);
+if (!values.allPackages && !positionals.length) {
+  die(2, `name a package, or pass --all-packages; packages with a test project: ${knownList}\n${USAGE}`);
+}
+const byLower = new Map(known.map((p) => [p.toLowerCase(), p]));
+const missing = positionals.filter((p) => !byLower.has(p.toLowerCase()));
+if (missing.length) {
+  die(2, `no test project for ${missing.join(", ")}; packages with a test project: ${knownList}\n${USAGE}`);
+}
+const packages = values.allPackages ? known : [...new Set(positionals.map((p) => byLower.get(p.toLowerCase())))];
 if (!packages.length) die(2, "test/package-tests/ holds no test project");
 
 const [port] = await claimPorts(1, { from: opts.port });
diff --git a/test/package-tests/README.md b/test/package-tests/README.md
index 38046221a..ea64f5069 100644
--- a/test/package-tests/README.md
+++ b/test/package-tests/README.md
@@ -83,12 +83,15 @@ takes it out.
 ## Running
 
 ```
-package-tests.bat tbRegExp                  both architectures, judged against expect.json
+package-tests.bat tbregexp                  both architectures, judged against expect.json
 package-tests.bat tbRegExp --arch win32
 package-tests.bat tbRegExp --update         write expect.json from the run
-package-tests.bat                           every project
+package-tests.bat --all-packages            every project, several minutes a package
 ```
 
+A package is named in any case. With no package named and no `--all-packages`, the run is
+refused and lists the packages that have a test project.
+
 `scripts/package_tests.mjs` stages the project with the package embedded (downloaded from TWINSERV
 into `%TEMP%\tb-twinserv` once) and runs it with `tbrun --tests`, which runs each case in the
 compiler's test mode, so a crash or an access violation ends only its case. Each case is then:

From dd95d8acc23be5881e71de9c2dcc5516ec31c469 Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Wed, 14 Oct 2026 11:34:23 +0200
Subject: [PATCH 07/13] Package tests: a region ( above
 prose,  after it) ties a statement that is not a callout to its
 cases; a NOTE or WARNING is only for a real difference or limit, and a silent
 difference from the replaced object is a NOTE (owner)

---
 WIP.md                                      |  2 +-
 docs/Documentation/Authoring.md             | 12 +++-
 docs/Documentation/Tools-Toolchain-Tests.md |  6 +-
 scripts/check_package_tests.mjs             | 78 ++++++++++++++++-----
 scripts/lib/package-test-cases.mjs          | 44 +++++++++---
 test/package-tests/README.md                | 25 +++++--
 6 files changed, 127 insertions(+), 40 deletions(-)

diff --git a/WIP.md b/WIP.md
index 3e35e9056..2f383f5c9 100644
--- a/WIP.md
+++ b/WIP.md
@@ -80,7 +80,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad
   > would file the page as a *static file* and serve the raw markdown. Editors on
   > Windows add a BOM unasked. `lib/frontmatter.mjs` strips a leading BOM before parsing.
 - `docs/Reference/Built-In/tbIDE/` — IDE Extensibility package (this is the **addin SDK**). The package is type-only — it ships **public interfaces + CoClasses** that an addin DLL binds to; every implementation behind them lives in the twinBASIC IDE itself. The user-facing surface is one entry-point factory (`tbCreateCompilerAddin`) plus 23 CoClasses grouped by role: the addin contract (`AddIn`), the root API (`Host`), the loaded `Project`, the editors collection (`Editor` / `CodeEditor` / `Editors`), the virtual file system (`FileSystem` / `FileSystemItem` / `Folder` / `File`), the in-IDE UI surface (`Toolbar` / `Toolbars` / `Button` / `ToolWindow` / `ToolWindows`), the HTML DOM inside a tool window (`HtmlElement` / `HtmlElements` / `HtmlElementProperty` / `HtmlElementProperties` / `HtmlEventProperty` / `HtmlEventProperties`), the `DebugConsole`, `KeyboardShortcuts`, `Themes`, and the single concrete user-instantiable helper class `AddinTimer`. Flat layout — one page per CoClass / Class plus the index landing.
-- `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `/index.md` at `/tB/Packages/Contributed///` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `.md`. Each package is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. **Every NOTE and WARNING of a section is backed by test cases** in the package's test project, `test/package-tests//` (owner; [its README](test/package-tests/README.md)), and names them in a `` marker on the line above it, which `check_package_tests.mjs` in `test.bat` enforces. A case checks what is correct, not what the package does (owner): a defect is a failing case, recorded in the project's `expect.json`, so the project tells the package's author what to fix. A section whose project is not written yet is in that gate's `PENDING` set. `package-tests.bat` runs the cases; a case it reports *fixed* means the callout goes. **The landing page ends, after See Also, with `## Test cases`**: one or two sentences linking to `-tests.twinproj` (owner), which the build publishes beside the section without the package's code in it (owner): it references the package as a linked package, which the reader puts in `%APPDATA%\twinBASIC\packages` ([WIP.PackageTests.md](WIP.PackageTests.md)). Its samples compile in the `contributed-` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1005") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner); a minor defect that the section documents does not make it Experimental (owner: VBComDlg, whose Replace All raises no event, stays Contributed; owner: nor does a fixed limit that the section states at the top of its landing page). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and its book chapter follows the Experimental Packages chapter. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three, and its pages keep their addresses.
+- `docs/Reference/Contributed/` — the contributed packages, one folder each, landing page `Contributed.md` beside it. **A package's landing page opens, under its H1, with `**Author:**`** (owner): the publisher TWINSERV lists (`publisher` in `indexer/.packages/manifests/contributed.json`), or the author the package's own sources name when that is someone else (VBA_JSON: Tim Hall, published by WaynePhillipsEA). Next to the author, on the same line, `**Version:**`: the version the section documents, which is its `indexed_from` and changes with it (owner). Then `**On GitHub:**` with a link, when a repository holds the package, or the library the package says it is made from; search GitHub for the package's name and the author's account, and check that the repository holds the package's code (its history too: `TBMANLIB_*` were never in woeoio's `tbman`). A landing page with one or two sections has no `* TOC` list. A class with member pages is a folder, `/index.md` at `/tB/Packages/Contributed///` with its members beside it, so a member links to its class as `(.)`; a class documented on one page is `.md`. Each package is a chapter of the book's Packages part in `_book.yml`. A section's `indexed_from` is the four-part TWINSERV version, and its snapshot is under `indexer/snapshots/contributed/`. **Every NOTE and WARNING of a section is backed by test cases** in the package's test project, `test/package-tests//` (owner; [its README](test/package-tests/README.md)), and names them in a `` marker on the line above it, which `check_package_tests.mjs` in `test.bat` enforces. **A NOTE or WARNING is only for a real difference or limit** (owner): a statement that describes how the package works stays prose, tied to its cases by a region, the marker on a line of its own above it and `` after it (owner). A difference from the object the package replaces that gives a different result silently (tbRegExp's `\1(a)`, `{,m}`, `$~` against VBScript) is a NOTE, not a WARNING (owner). A case checks what is correct, not what the package does (owner): a defect is a failing case, recorded in the project's `expect.json`, so the project tells the package's author what to fix. A section whose project is not written yet is in that gate's `PENDING` set. `package-tests.bat` runs the cases; a case it reports *fixed* means the callout goes. **The landing page ends, after See Also, with `## Test cases`**: one or two sentences linking to `-tests.twinproj` (owner), which the build publishes beside the section without the package's code in it (owner): it references the package as a linked package, which the reader puts in `%APPDATA%\twinBASIC\packages` ([WIP.PackageTests.md](WIP.PackageTests.md)). Its samples compile in the `contributed-` sample template, whose `Settings` names the package's version: **change that version with `indexed_from`** ([WIP.ExamplesBuild.md](WIP.ExamplesBuild.md#template-projects)). **A contributed package is documented and its samples compiled against the newest twinBASIC BETA installed, never an older one** (owner): the version that matters is the package's, and a statement about the compiler (such as "does not compile on BETA 1005") names that newest build. **A contributed package that does not compile on that build, or has defects that a program would meet, is listed under Experimental Packages** (owner); a minor defect that the section documents does not make it Experimental (owner: VBComDlg, whose Replace All raises no event, stays Contributed; owner: nor does a fixed limit that the section states at the top of its landing page). **A package moves from Contributed to Experimental for its defects only after a separate agent has reproduced each defect independently** and ruled out misuse, a wrong expectation, a twinBASIC defect and the harness (owner): `docs/Reference/Experimental.md`, at `/tB/Packages/Experimental/`, after Contributed Packages. Its landing page's `parent:` is `Experimental Packages`, its line is in `Experimental.md` and not in `Contributed.md`, and its book chapter follows the Experimental Packages chapter. Its folder and URLs stay under `Contributed/`, so a package that is fixed moves back by changing only those three, and its pages keep their addresses.
 - `docs/Reference/Statements.md` — alphabetical index of language statements.
 - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions.
 - `docs/LLVM/` — the LLVM section: compiling with the LLVM back end. A top-level section between Features and Reference Section in the nav, at `nav_order: 6` (Features is 5). `index.md` is the landing page and `Getting-Started.md` the only page so far, with its screenshots under `Images/`. Everything it describes arrived in **BETA 984**. Its language claims are measured on BETA 995 with `tbrun --llvm` and `--exe`: the comma, space and comma-space spellings of `[CompilerOptions]` all turn LLVM and `+optimize` on, `[CompilerOptions("")]` turns it off under project-wide LLVM, the long CPU-flag sample runs, and an error is lost between procedures when either the callee or the caller is LLVM-compiled. Its samples are marked `check_build`, which compiles them; `examples.bat --llvm` also builds them with LLVM.
diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md
index 06642d579..4066acfcb 100644
--- a/docs/Documentation/Authoring.md
+++ b/docs/Documentation/Authoring.md
@@ -884,7 +884,17 @@ Every `> [!NOTE]` and `> [!WARNING]` in a contributed package's section is backe
     
     > [!NOTE]
 
-An item is a `Fixture.Sub` test case, `compile ` for a compile case, `probe ` for a scenario of the package's `probe/` harness, or `none -- ` for a callout no program can check. A case checks what is correct, so a defect of the package is a failing case. The folder's README says how to write one, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), in `test.bat`, fails a callout with no marker and a case no callout names. [`package-tests.bat`](Tools#package-testsbat) runs the cases. The section's landing page ends with a short **Test cases** section that links to the project the build publishes, `-tests.twinproj`.
+An item is a `Fixture.Sub` test case, `compile ` for a compile case, `probe ` for a scenario of the package's `probe/` harness, or `none -- ` for a callout no program can check. A case checks what is correct, so a defect of the package is a failing case.
+
+A callout is for a real difference or limit. A statement that only describes how the package works stays prose, and its cases are tied to it by a region: the same marker on a line of its own above the prose, and `` after it.
+
+```md
+
+A search that backtracks without end gives up instead of hanging.
+
+```
+
+A region may hold paragraphs, lists, tables and samples, and regions do not nest. The folder's README says how to write a case, and [`check_package_tests.mjs`](Tools-Toolchain-Tests#check-package-tests), in `test.bat`, fails a callout with neither a marker nor a region, a region left open, and a case no marker names. [`package-tests.bat`](Tools#package-testsbat) runs the cases. The section's landing page ends with a short **Test cases** section that links to the project the build publishes, `-tests.twinproj`.
 
 ## Document the product as it is
 
diff --git a/docs/Documentation/Tools-Toolchain-Tests.md b/docs/Documentation/Tools-Toolchain-Tests.md
index 917504449..ba8db5cf8 100644
--- a/docs/Documentation/Tools-Toolchain-Tests.md
+++ b/docs/Documentation/Tools-Toolchain-Tests.md
@@ -367,9 +367,11 @@ A callout names its cases in an HTML comment on the line above it:
     
     > [!WARNING]
 
-Each item is a `Fixture.Sub` for a `[TestCase]`, `compile ` for a compile case, `probe ` for a scenario of the package's `probe/` harness, or `none -- ` for a callout no program can check. For a package with a test project, the gate fails when a callout has no marker, when an item names nothing that exists, when a case is named by no callout, when a `[TestCase]` does not start with `Expect.Begin` and end with `Expect.Done`, when `expect.json` is for another version than the section's `indexed_from` or names a case that does not exist, and when the generated `RunAll.twin` is out of date. A section with no test project yet is listed in the script's `PENDING` set; the gate fails on a name there with a project, and on a section neither there nor with one, so the list only shrinks.
+The same comment anywhere else opens a region, which `` closes: it ties the paragraphs, lists, tables and samples between them, which describe how the package works, to the cases that check them. Regions do not nest, and a callout inside one is marked by it.
 
-Eleven probes run first, on pages, sources and an `expect.json` built in memory: how a marker and its items are read, the RunAll module, how `expect.json` is read and written, and how a case's result is judged.
+Each item is a `Fixture.Sub` for a `[TestCase]`, `compile ` for a compile case, `probe ` for a scenario of the package's `probe/` harness, or `none -- ` for a callout no program can check. For a package with a test project, the gate fails when a callout has neither a marker nor a region around it, when a region is not closed or opens inside another, when an item names nothing that exists, when a case is named by no marker, when a `[TestCase]` does not start with `Expect.Begin` and end with `Expect.Done`, when `expect.json` is for another version than the section's `indexed_from` or names a case that does not exist, and when the generated `RunAll.twin` is out of date. A section with no test project yet is listed in the script's `PENDING` set; the gate fails on a name there with a project, and on a section neither there nor with one, so the list only shrinks.
+
+Thirteen probes run first, on pages, sources and an `expect.json` built in memory: how a marker, a region and their items are read, the RunAll module, how `expect.json` is read and written, and how a case's result is judged.
 
 Exit codes: **0** every probe passed and every test project is in order, **1** a probe failed, or a section or test project is not in order, **2** the gate could not run: a refused command line, or a crash.
 
diff --git a/scripts/check_package_tests.mjs b/scripts/check_package_tests.mjs
index 4791fc6b1..183bbbc9b 100644
--- a/scripts/check_package_tests.mjs
+++ b/scripts/check_package_tests.mjs
@@ -49,11 +49,11 @@ import {
 import { REPO_ROOT } from "../lib/repo-paths.mjs";
 import { createProbes } from "./lib/gate-probes.mjs";
 import {
-  calloutsOf,
   expectationFor,
   expectFrom,
   judge,
   malformedCases,
+  markersOf,
   outcomeOf,
   parseMarker,
   projectCases,
@@ -142,20 +142,57 @@ const { check, report } = createProbes("check_package_tests", "see test/package-
     "```",
     "  ",
     "  > [!NOTE]",
+    "",
+    "",
+    "A paragraph the case checks.",
+    "",
+    "> [!NOTE]",
+    "> Inside the region.",
+    "",
   ].join("\n");
-  const c = calloutsOf(page);
-  check("a callout in a fence is not a callout", c.length === 3, JSON.stringify(c));
+  const { callouts: c, markers: mk, problems } = markersOf(page);
+  check("a callout in a fence is not a callout", c.length === 4, JSON.stringify(c));
   check(
-    "a marker on the line above is read, item by item",
+    "a marker on the line above a callout is read, item by item, and marks it",
     c[0]?.kind === "WARNING" &&
-      JSON.stringify(c[0].items) === JSON.stringify([{ case: "A.One" }, { compile: "NoWin64" }]),
-    JSON.stringify(c[0]),
+      c[0].marked &&
+      JSON.stringify(mk[0]?.items) === JSON.stringify([{ case: "A.One" }, { compile: "NoWin64" }]),
+    JSON.stringify([c[0], mk[0]]),
+  );
+  check(
+    "a callout with no marker above and no region around it is not marked",
+    c[1]?.marked === false,
+    JSON.stringify(c[1]),
   );
-  check("a callout with no marker above has none", c[1]?.marker === null, JSON.stringify(c[1]));
   check(
     "an indented callout and its marker are read, and none keeps its reason",
-    c[2]?.items?.[0]?.none === "needs a person at the dialog",
-    JSON.stringify(c[2]),
+    c[2]?.marked && mk[1]?.items?.[0]?.none === "needs a person at the dialog",
+    JSON.stringify([c[2], mk[1]]),
+  );
+  check(
+    "a marker above a paragraph opens a region, which marks a callout inside it and closes cleanly",
+    c[3]?.marked && JSON.stringify(mk[2]?.items) === JSON.stringify([{ case: "B.Two" }]) && problems.length === 0,
+    JSON.stringify({ c3: c[3], mk2: mk[2], problems }),
+  );
+  const bad = markersOf(
+    [
+      "",
+      "Text.",
+      "",
+      "More.",
+      "",
+      "",
+      "",
+      "Open.",
+    ].join("\n"),
+  ).problems;
+  check(
+    "a region inside a region, a close with no region and a region left open are each reported",
+    bad.length === 3 &&
+      bad.some((p) => p.includes("inside")) &&
+      bad.some((p) => p.includes("closes no")) &&
+      bad.some((p) => p.includes("not closed")),
+    bad.join("\n"),
   );
   check(
     "an item the marker grammar does not know is bad, and none without a reason is bad",
@@ -338,14 +375,18 @@ function checkPackage(pkg) {
   const hasProbe = existsSync(path.join(dir, "probe"));
   for (const rel of sectionPages(pkg)) {
     const text = readFileSync(path.join(SECTIONS, pkg, rel), "utf8");
-    for (const c of calloutsOf(text)) {
-      const where = `${rel} line ${c.line}`;
-      if (c.marker === null) {
-        out.push(`${where}: the ${c.kind} has no  marker on the line above`);
-        continue;
-      }
-      if (!c.items.length) out.push(`${where}: the marker names nothing`);
-      for (const item of c.items) {
+    const { callouts, markers, problems: regionProblems } = markersOf(text);
+    out.push(...regionProblems.map((p) => `${rel} ${p}`));
+    for (const c of callouts) {
+      if (!c.marked)
+        out.push(
+          `${rel} line ${c.line}: the ${c.kind} has no  marker on the line above, nor a region around it`,
+        );
+    }
+    for (const mk of markers) {
+      const where = `${rel} line ${mk.line}`;
+      if (!mk.items.length) out.push(`${where}: the marker names nothing`);
+      for (const item of mk.items) {
         if (item.bad)
           out.push(
             `${where}: the marker's item "${item.bad}" is not Fixture.Sub, compile , probe  or none -- `,
@@ -361,8 +402,7 @@ function checkPackage(pkg) {
       }
     }
   }
-  for (const name of caseNames)
-    if (!named.has(name)) out.push(`the case ${name} is named by no callout of the section`);
+  for (const name of caseNames) if (!named.has(name)) out.push(`the case ${name} is named by no marker of the section`);
   return out;
 }
 
diff --git a/scripts/lib/package-test-cases.mjs b/scripts/lib/package-test-cases.mjs
index 56fa8f012..332c771e1 100644
--- a/scripts/lib/package-test-cases.mjs
+++ b/scripts/lib/package-test-cases.mjs
@@ -130,21 +130,31 @@ export function expectFrom({ version, names, outcomes, previous }) {
 
 // A callout: a blockquote line `> [!NOTE]` or `> [!WARNING]`, indented or not.
 const CALLOUT_RE = /^[ \t]*>[ \t]*\[!(NOTE|WARNING)\]/;
-// Its marker: an HTML comment on the line above it, ``.
+// A marker, ``, on a line of its own; and the end of a region.
 const MARKER_RE = /^[ \t]*[ \t]*$/;
+const END_RE = /^[ \t]*[ \t]*$/;
 const FENCE_RE = /^[ \t]*(`{3,}|~{3,})/;
 
 /**
- * The callouts of a page and the marker above each.
+ * The markers of a page, outside code fences. A marker on the line directly above a callout
+ * marks that callout. Any other marker opens a region, which `` closes: the
+ * paragraphs, lists, tables and samples between them state what the region's cases check.
+ * Regions do not nest, and a callout inside one is marked by it.
  *
- * @returns {{line: number, kind: string, marker: string | null, items: object[]}[]}  `items`
- *   parses the marker: { case: "Fixture.Sub" }, { compile: "Name" }, { probe: "id" } or
- *   { none: "reason" }, and { bad: "text" } for one it cannot read
+ * @returns {{callouts: {line: number, kind: string, marked: boolean}[],
+ *            markers: {line: number, items: object[]}[], problems: string[]}}  `markers` holds
+ *   every marker, a callout's and a region's alike; `items` parses one: { case: "Fixture.Sub" },
+ *   { compile: "Name" }, { probe: "id" } or { none: "reason" }, and { bad: "text" } for one it
+ *   cannot read. `problems` names a region not closed, a region opened inside another and a
+ *   close with no region.
  */
-export function calloutsOf(text) {
+export function markersOf(text) {
   const lines = text.split(/\r?\n/);
-  const out = [];
+  const callouts = [];
+  const markers = [];
+  const problems = [];
   let fence = null;
+  let open = null; // the line of the region's marker
   for (let i = 0; i < lines.length; i++) {
     const f = FENCE_RE.exec(lines[i]);
     if (f) {
@@ -153,12 +163,24 @@ export function calloutsOf(text) {
       continue;
     }
     if (fence) continue;
+    const m = MARKER_RE.exec(lines[i]);
+    if (m) {
+      markers.push({ line: i + 1, items: parseMarker(m[1]) });
+      if (CALLOUT_RE.test(lines[i + 1] ?? "")) continue; // a callout's own marker
+      if (open !== null) problems.push(`line ${i + 1}: a region opens inside the region of line ${open}`);
+      open = i + 1;
+      continue;
+    }
+    if (END_RE.test(lines[i])) {
+      if (open === null) problems.push(`line ${i + 1}:  closes no region`);
+      open = null;
+      continue;
+    }
     const c = CALLOUT_RE.exec(lines[i]);
-    if (!c) continue;
-    const m = i > 0 ? MARKER_RE.exec(lines[i - 1]) : null;
-    out.push({ line: i + 1, kind: c[1], marker: m ? m[1] : null, items: m ? parseMarker(m[1]) : [] });
+    if (c) callouts.push({ line: i + 1, kind: c[1], marked: open !== null || MARKER_RE.test(lines[i - 1] ?? "") });
   }
-  return out;
+  if (open !== null) problems.push(`line ${open}: the region is not closed with `);
+  return { callouts, markers, problems };
 }
 
 /** The items of a marker: Fixture.Sub, compile , probe , none -- . */
diff --git a/test/package-tests/README.md b/test/package-tests/README.md
index ea64f5069..dc06c05e2 100644
--- a/test/package-tests/README.md
+++ b/test/package-tests/README.md
@@ -63,7 +63,7 @@ End Module
 - A claim that needs a dialog operated or a form shown goes in the package's `probe/` harness, as
   TBMANLIB_Dialog's and xCalendar's do.
 
-## Tying a callout to its cases
+## Tying the pages to the cases
 
 Every `> [!NOTE]` and `> [!WARNING]` of the section's pages has a marker on the line above it:
 
@@ -72,13 +72,26 @@ Every `> [!NOTE]` and `> [!WARNING]` of the section's pages has a marker on the
 > [!NOTE]
 ```
 
+A NOTE or WARNING is for a real difference or limit: a defect, a difference from the object the
+package replaces, a limit a program can meet. A statement that only describes how the package
+works stays prose, and the cases that check it are tied to it by a **region**: the same marker on
+a line of its own above the prose, and `` after it. A region can hold paragraphs,
+lists, tables and samples; regions do not nest, and a callout inside one needs no marker of its
+own. Both are HTML comments, invisible on the page.
+
+```md
+
+A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging.
+
+```
+
 An item is `Fixture.Sub`, `compile `, `probe ` (a scenario of `probe/`), or
 `none -- ` for a callout no program can check (a statement about the IDE, a licence).
-`node scripts/check_package_tests.mjs`, in `test.bat`, fails when a callout has no marker, an item
-names nothing, a case is named by no callout, a case does not start with `Expect.Begin` and end
-with `Expect.Done`, `expect.json` is for another version, or `RunAll.twin` is out of date. A
-section whose project is not written yet is in the script's `PENDING` set; the package's project
-takes it out.
+`node scripts/check_package_tests.mjs`, in `test.bat`, fails when a callout has neither a marker
+nor a region around it, a region is not closed or opens inside another, an item names nothing, a
+case is named by no marker, a case does not start with `Expect.Begin` and end with `Expect.Done`,
+`expect.json` is for another version, or `RunAll.twin` is out of date. A section whose project is
+not written yet is in the script's `PENDING` set; the package's project takes it out.
 
 ## Running
 

From c8e6db3074f6cb190f3da4d1312d325d5c363a81 Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Wed, 14 Oct 2026 11:34:37 +0200
Subject: [PATCH 08/13] tbRegExp: test project (31 cases, 15 documented
 failures, the same on win32 and win64), markers and regions on its pages,
 Test cases section; IsValid's note adds LastErrorNumber 0 for a{2,1};
 SearchAborted's timings are a few seconds and a second or two

---
 .../Contributed/tbRegExp/RegExp/Execute.md    |   2 +
 .../Contributed/tbRegExp/RegExp/IsValid.md    |   7 +-
 .../Contributed/tbRegExp/RegExp/LastError.md  |   2 +
 .../tbRegExp/RegExp/LastErrorNumber.md        |   2 +
 .../Contributed/tbRegExp/RegExp/Replace.md    |   3 +
 .../tbRegExp/RegExp/SearchAborted.md          |   3 +-
 .../Contributed/tbRegExp/RegExp/Split.md      |   2 +
 .../Contributed/tbRegExp/RegExp/Test.md       |   2 +
 docs/Reference/Contributed/tbRegExp/index.md  |  22 ++-
 scripts/check_package_tests.mjs               |   1 -
 .../tbRegExp/Sources/IsValidTests.twin        | 127 ++++++++++++++++
 .../tbRegExp/Sources/PatternSyntaxTests.twin  | 109 ++++++++++++++
 .../tbRegExp/Sources/ReplaceTests.twin        |  71 +++++++++
 .../tbRegExp/Sources/RunAll.twin              | 138 ++++++++++++++++++
 .../tbRegExp/Sources/SearchAbortedTests.twin  |  27 ++++
 .../tbRegExp/Sources/SearchGivesUpTests.twin  |  84 +++++++++++
 test/package-tests/tbRegExp/expect.json       |  50 +++++++
 17 files changed, 645 insertions(+), 7 deletions(-)
 create mode 100644 test/package-tests/tbRegExp/Sources/IsValidTests.twin
 create mode 100644 test/package-tests/tbRegExp/Sources/PatternSyntaxTests.twin
 create mode 100644 test/package-tests/tbRegExp/Sources/ReplaceTests.twin
 create mode 100644 test/package-tests/tbRegExp/Sources/RunAll.twin
 create mode 100644 test/package-tests/tbRegExp/Sources/SearchAbortedTests.twin
 create mode 100644 test/package-tests/tbRegExp/Sources/SearchGivesUpTests.twin
 create mode 100644 test/package-tests/tbRegExp/expect.json

diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md b/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md
index b95a8d789..30cfea8d0 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/Execute.md
@@ -36,7 +36,9 @@ Debug.Print re.Execute("fooBarBaz").Count         ' 2
 Debug.Print re.Execute("fooBarBaz")(1).FirstIndex  ' 6
 ```
 
+
 A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging. The collection then holds the matches found before it gave up, and [**SearchAborted**](SearchAborted) returns **True**.
+
 
 Each **Match** gives the matched text ([**Value**](../Match/Value)), the position where it starts ([**FirstIndex**](../Match/FirstIndex), counted from 0), its length, and the text of each group of the pattern ([**SubMatches**](../Match/SubMatches)). A group written `(?...)` can be read by its name.
 
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md
index ff9076812..784bd1be7 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md
@@ -23,10 +23,13 @@ The property compiles the pattern if it has not been compiled since [**Pattern**
 
 Check **IsValid** before [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) or [**Split**](Split) when the pattern comes from outside the program, because each of those raises the compile error.
 
-Although nothing is raised, the compile error of an invalid pattern stays in the **Err** object after the call that compiled the pattern returns. A program that tests **Err** for other errors calls **Err.Clear** first.
+
+> [!NOTE]
+> Although nothing is raised, the compile error of an invalid pattern stays in the **Err** object after the call that compiled the pattern returns. A program that tests **Err** for other errors calls **Err.Clear** first.
 
+
 > [!NOTE]
-> A quantifier whose lower count is above its upper count, such as `a{2,1}`, is not found when the pattern is compiled, so **IsValid** returns **True** for it. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise error 3000 when a search reaches the quantifier, with the description that Windows gives that number, *The specified print monitor is unknown.* This is how version 1.8.0.0 behaves.
+> A quantifier whose lower count is above its upper count, such as `a{2,1}`, is not found when the pattern is compiled, so **IsValid** returns **True** for it and [**LastErrorNumber**](LastErrorNumber) returns 0. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise error 3000 when a search reaches the quantifier, with the description that Windows gives that number, *The specified print monitor is unknown.* This is how version 1.8.0.0 behaves.
 
 ## Example
 
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md b/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md
index c0f5dbbf2..5935dc1a7 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/LastError.md
@@ -19,7 +19,9 @@ Returns a **String**: the description of the compile error, or an empty string w
 
 ## Remarks
 
+
 The property compiles the pattern if it has not been compiled since [**Pattern**](Pattern) or [**IgnoreCase**](IgnoreCase) last changed. The compilation traps its error, so **LastError** raises nothing, but the error stays in the **Err** object (see [**IsValid**](IsValid)). The text is a sentence that names the fault, such as *Unterminated character class: a '[' without a matching ']'.* for the pattern `[abc`. [**LastErrorNumber**](LastErrorNumber) is the number of the error and lists the faults.
+
 
 [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) raise an error with this description when the pattern is invalid.
 
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md b/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md
index 8bc6b8c2d..142048169 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/LastErrorNumber.md
@@ -21,7 +21,9 @@ Returns a **Long**: the number of the compile error, or 0 when the pattern is va
 
 The property compiles the pattern if it has not been compiled since [**Pattern**](Pattern) or [**IgnoreCase**](IgnoreCase) last changed. The compilation traps its error, so **LastErrorNumber** raises nothing. The value is the `Number` of the error that the compilation raised; [**LastError**](LastError) is its description.
 
+
 The engine raises its pattern errors with numbers of the form `vbObjectError + 3000 + n`, where *n* identifies the fault, as in the example. The compilation traps the error, so **LastErrorNumber** raises nothing, but the error stays in the **Err** object (see [**IsValid**](IsValid)).
+
 
 | *n* | Fault | Example |
 |-----|-------|---------|
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md
index b574aa4ec..d0a6a4cc2 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md
@@ -39,6 +39,7 @@ A `$` in *replaceString* starts a substitution item. Any other text is copied as
 
 A `$` that starts no item raises no error, so a replacement string can hold a literal `$` as it is, at the end or before any other character. Before a character that would make an item, write `$$`: `$$$&` inserts a `$` and then the matched text, as the last example does.
 
+
 > [!NOTE]
 > Items are read as in VBScript's **RegExp**, with two differences: `$<`*name*`>` is an addition of this package, and `$~` is dropped, where VBScript copies `$~` into the result.
 
@@ -56,7 +57,9 @@ re.Pattern = "^"
 Debug.Print Replace(re.Replace("a" & vbLf & "b" & vbLf & "c", "> "), vbLf, "/")  ' > a/> b/> c
 ```
 
+
 A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging, and **Replace** then replaces only the matches found before it gave up. [**SearchAborted**](SearchAborted) returns **True** in that case.
+
 
 An invalid pattern raises the compile error, with the number in [**LastErrorNumber**](LastErrorNumber) and the description in [**LastError**](LastError). Check [**IsValid**](IsValid) first to avoid it.
 
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md b/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md
index b6303528b..a1b9c0d6a 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md
@@ -27,8 +27,9 @@ The budget is 5,000 steps for each character of the string, at least 1,000,000 a
 
 Each call of **Test**, **Execute**, **Replace** or **Split** sets the value again: a call that finishes its search sets it to **False**, and so does a call that raises an error because the pattern is not valid, because no search was made. The value belongs to the object. A search on another **RegExp** does not change it, and a new object returns **False**.
 
+
 > [!NOTE]
-> A pattern that is not catastrophic can also run out of steps, if it tries again from each start position and the string is long. `\d+x` on a string of 5,000 digits and no `x` takes about 5 seconds and then gives up, with the same result as for a string that has no match, and **SearchAborted** returns **True**. A string of 2,000 digits finishes in about 2 seconds and returns **False**.
+> A pattern that is not catastrophic can also run out of steps, if it tries again from each start position and the string is long. `\d+x` on a string of 5,000 digits and no `x` takes a few seconds and then gives up, with the same result as for a string that has no match, and **SearchAborted** returns **True**. A string of 2,000 digits finishes in a second or two and returns **False**.
 
 ## Example
 
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Split.md b/docs/Reference/Contributed/tbRegExp/RegExp/Split.md
index 8009f2546..b1563cfa9 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/Split.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/Split.md
@@ -43,7 +43,9 @@ Debug.Print parts.Count                                          ' 3
 Debug.Print parts(1) & "|" & parts(2) & "|" & parts(3)           ' foo|Bar|Baz
 ```
 
+
 A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging, and **Split** then splits only at the matches found before it gave up. [**SearchAborted**](SearchAborted) returns **True** in that case.
+
 
 When **Pattern** is not a valid regular expression, **Split** raises the compile error. Its number is [**LastErrorNumber**](LastErrorNumber), its description is [**LastError**](LastError), and its source is `RegExp`. Read [**IsValid**](IsValid) first to avoid the error.
 
diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Test.md b/docs/Reference/Contributed/tbRegExp/RegExp/Test.md
index b7116d0ff..49358f946 100644
--- a/docs/Reference/Contributed/tbRegExp/RegExp/Test.md
+++ b/docs/Reference/Contributed/tbRegExp/RegExp/Test.md
@@ -26,7 +26,9 @@ Returns **True** if [**Pattern**](Pattern) finds a match in *sourceString*, othe
 
 [**IgnoreCase**](IgnoreCase), [**MultiLine**](MultiLine) and [**DotAll**](DotAll) apply to the matching. [**Global**](Global) makes no difference, because one match is enough.
 
+
 A search that backtracks without end, such as `(a+)+b` on thirty `a`, gives up instead of hanging, and **Test** then returns **False**. [**SearchAborted**](SearchAborted) tells that **False** from a string that has no match.
+
 
 When **Pattern** is not a valid regular expression, **Test** raises the compile error. Its number is [**LastErrorNumber**](LastErrorNumber), its description is [**LastError**](LastError), and its source is `RegExp`. Read [**IsValid**](IsValid) first to avoid the error.
 
diff --git a/docs/Reference/Contributed/tbRegExp/index.md b/docs/Reference/Contributed/tbRegExp/index.md
index 9abf8fcbb..a6441988b 100644
--- a/docs/Reference/Contributed/tbRegExp/index.md
+++ b/docs/Reference/Contributed/tbRegExp/index.md
@@ -41,13 +41,15 @@ Debug.Print re.Test("ABCDEF")                            ' True
 Debug.Print re.Test("ABCdef")                            ' False
 ```
 
-A pattern that holds an atomic group `(?>...)` or a conditional is not valid: [**IsValid**](RegExp/IsValid) returns **False**. A named backreference `\k`, `\A`, `\Z` and `\p{...}` are not supported either, but they do not make the pattern invalid, because a backslash before a letter that names no escape stands for the letter itself, as in VBScript: `\k` matches the text `k`, and `\A` matches `A`. The pattern `\p{L}` is invalid for another reason: `{L}` does not start a quantifier (see below).
+
+> [!NOTE]
+> A pattern that holds an atomic group `(?>...)` or a conditional is not valid: [**IsValid**](RegExp/IsValid) returns **False**. A named backreference `\k`, `\A`, `\Z` and `\p{...}` are not supported either, but they do not make the pattern invalid, because a backslash before a letter that names no escape stands for the letter itself, as in VBScript: `\k` matches the text `k`, and `\A` matches `A`. The pattern `\p{L}` is invalid for another reason: `{L}` does not start a quantifier (see below).
 
 The escapes that name a control character or a character code are read as in VBScript:
 
 - `\c` followed by a letter, in either case, is a control character: `\cJ` and `\CJ` are the line feed. A `\c` followed by anything else makes the pattern invalid;
 - `\0` is the character with code 0, `\0` followed by octal digits, such as `\012`, is that code, and `\8` and `\9` are the digits themselves;
-- `\1` to `\9` and longer numbers are backreferences when the pattern has that many groups. Otherwise the digits are read as an octal character code, so `\101` is `A` and `(a)\2` is `a` followed by the character with code 2; [**IsValid**](RegExp/IsValid) returns **True** for both. A backreference to a group that has not yet taken part in the match, such as `\1(a)`, matches no characters; VBScript's **RegExp** finds no match for such a pattern;
+- `\1` to `\9` and longer numbers are backreferences when the pattern has that many groups. Otherwise the digits are read as an octal character code, so `\101` is `A` and `(a)\2` is `a` followed by the character with code 2; [**IsValid**](RegExp/IsValid) returns **True** for both;
 - `\f`, `\n`, `\r` and `\t`, inside a character class as well, are the form feed, line feed, carriage return and tab.
 
 ```tb check_build check_run
@@ -63,9 +65,17 @@ re.Pattern = "(a)\2"
 Debug.Print re.Test("a" & ChrW(2))      ' True
 ```
 
+
+> [!NOTE]
+> A backreference to a group that has not yet taken part in the match, such as `\1(a)`, matches no characters, so the pattern finds `a` in the string `a`. VBScript's **RegExp** finds no match for such a pattern.
+
 `\w` matches only the ASCII letters and digits and `_`, as in VBScript, so `\w+` finds `caf` in `café`.
 
-A `{` has to start a quantifier: `{n}`, `{n,}`, `{n,m}` or `{,m}`, which means `{0,m}`. A pattern in which a `{` starts none, such as `a{` or `a{x}`, is not valid; write `\{` for the character. VBScript reads a `{` that starts no quantifier, and `{,m}`, as ordinary text. A `]` or `}` that closes nothing is an ordinary character, as in VBScript.
+A `{` has to start a quantifier: `{n}`, `{n,}`, `{n,m}` or `{,m}`, which means `{0,m}`. A pattern in which a `{` starts none, such as `a{` or `a{x}`, is not valid; write `\{` for the character. A `]` or `}` that closes nothing is an ordinary character, as in VBScript.
+
+
+> [!NOTE]
+> VBScript's **RegExp** reads a `{` that starts no quantifier, and `{,m}`, as ordinary text: `a{x}` is a valid pattern there that finds the text `a{x}`, and `a{,2}` finds the text `a{,2}` and not `aa`. In **tbRegExp** the first is not valid, so a search with it raises the compile error, and `a{,2}` finds up to two `a`.
 
 ```tb check_build check_run
 Dim re As New RegExp
@@ -82,7 +92,9 @@ Debug.Print re.Execute("a{x}")(0).Value  ' a{x}
 
 ## Searches that give up
 
+
 A pattern that backtracks without end, such as `(a+)+b` on a string of thirty `a`, does not hang a search as it does in VBScript's **RegExp**. Every search has a budget of steps, which a program cannot change, and when it runs out the search stops and raises no error. Its result is then short or empty, and a program that has to tell it from a true result reads [**SearchAborted**](RegExp/SearchAborted) after the call. A long match is no problem: `a*` and `^.*$` match a string of 1,000,000 `a` whole.
+
 
 ```tb check_build check_run
 Dim re As New RegExp
@@ -125,3 +137,7 @@ Debug.Print re.SearchAborted               ' False
   - [Count](SubMatches/Count) -- returns the number of strings
   - [Item](SubMatches/Item) -- returns one string by position or by group name (the default member)
   - [NewEnum](SubMatches/NewEnum) -- returns the enumerator that For Each uses (hidden)
+
+## Test cases
+
+The notes and warnings of this section are checked by the test cases in [tbRegExp-tests.twinproj](tbRegExp-tests.twinproj), a twinBASIC project. It uses tbRegExp as a [linked package](../../../../Features/Packages/Linked#manual-management): put the package's `.twinpack` in `%APPDATA%\twinBASIC\packages`, then run the project, and each case prints whether it passes.
diff --git a/scripts/check_package_tests.mjs b/scripts/check_package_tests.mjs
index 183bbbc9b..b6068c3a5 100644
--- a/scripts/check_package_tests.mjs
+++ b/scripts/check_package_tests.mjs
@@ -112,7 +112,6 @@ const PENDING = new Set([
   "WinReg",
   "tbFileSysTools",
   "tbProjPacker",
-  "tbRegExp",
   "tsJson",
   "tsPwGen",
   "xCalendar",
diff --git a/test/package-tests/tbRegExp/Sources/IsValidTests.twin b/test/package-tests/tbRegExp/Sources/IsValidTests.twin
new file mode 100644
index 000000000..c33000119
--- /dev/null
+++ b/test/package-tests/tbRegExp/Sources/IsValidTests.twin
@@ -0,0 +1,127 @@
+' tbRegExp: RegExp.IsValid, LastError and LastErrorNumber, and what a pattern that does not compile does.
+' The pattern a{2,1} has a lower count above its upper count. VBScript's RegExp refuses it with
+' error 5017; tbRegExp names the fault "Invalid quantifier", error vbObjectError + 3008.
+[TestFixture]
+Module IsValidTests
+
+    [TestCase]
+    Public Sub IsValid_LowCountAboveHigh()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{2,1}"
+        Expect.IsFalse re.IsValid, "IsValid of the pattern a{2,1}, whose lower count is above its upper count"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub LastErrorNumber_LowCountAboveHigh()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{2,1}"
+        Expect.AreEqual vbObjectError + 3008, re.LastErrorNumber, "LastErrorNumber of the pattern a{2,1} (invalid quantifier)"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Test_LowCountAboveHighRaisesCompileError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{2,1}"
+        Dim ok As Boolean
+        Err.Clear
+        On Error Resume Next
+        ok = re.Test("aa")
+        Dim n As Long, d As String
+        n = Err.Number
+        d = Err.Description
+        On Error GoTo 0
+        Expect.AreEqual vbObjectError + 3008, n, "Err.Number of Test(""aa"") with the pattern a{2,1}"
+        Expect.AreEqual "Invalid quantifier: a malformed '{n,m}' count (e.g. '{2,1}', or a number too large).", d, "Err.Description of Test(""aa"") with the pattern a{2,1}"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Execute_LowCountAboveHighRaisesCompileError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{2,1}"
+        Dim m As MatchCollection
+        Err.Clear
+        On Error Resume Next
+        Set m = re.Execute("aa")
+        Dim n As Long
+        n = Err.Number
+        On Error GoTo 0
+        Expect.AreEqual vbObjectError + 3008, n, "Err.Number of Execute(""aa"") with the pattern a{2,1}"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_LowCountAboveHighRaisesCompileError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{2,1}"
+        Dim s As String
+        Err.Clear
+        On Error Resume Next
+        s = re.Replace("aa", "x")
+        Dim n As Long
+        n = Err.Number
+        On Error GoTo 0
+        Expect.AreEqual vbObjectError + 3008, n, "Err.Number of Replace(""aa"", ""x"") with the pattern a{2,1}"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Split_LowCountAboveHighRaisesCompileError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{2,1}"
+        Dim c As Collection
+        Err.Clear
+        On Error Resume Next
+        Set c = re.Split("aa")
+        Dim n As Long
+        n = Err.Number
+        On Error GoTo 0
+        Expect.AreEqual vbObjectError + 3008, n, "Err.Number of Split(""aa"") with the pattern a{2,1}"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub IsValid_InvalidPatternLeavesNoError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(\d+"
+        Err.Clear
+        Expect.IsFalse re.IsValid, "IsValid of the pattern (\d+"
+        Expect.AreEqual 0, Err.Number, "Err.Number after IsValid compiled the pattern (\d+"
+        Err.Clear
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub LastError_InvalidPatternLeavesNoError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(\d+"
+        Err.Clear
+        Expect.AreEqual "Unexpected end of pattern", Left$(re.LastError, 25), "first 25 characters of LastError for the pattern (\d+"
+        Expect.AreEqual 0, Err.Number, "Err.Number after LastError compiled the pattern (\d+"
+        Err.Clear
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub LastErrorNumber_InvalidPatternLeavesNoError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(\d+"
+        Err.Clear
+        Expect.AreEqual vbObjectError + 3009, re.LastErrorNumber, "LastErrorNumber of the pattern (\d+"
+        Expect.AreEqual 0, Err.Number, "Err.Number after LastErrorNumber compiled the pattern (\d+"
+        Err.Clear
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbRegExp/Sources/PatternSyntaxTests.twin b/test/package-tests/tbRegExp/Sources/PatternSyntaxTests.twin
new file mode 100644
index 000000000..dd7c19911
--- /dev/null
+++ b/test/package-tests/tbRegExp/Sources/PatternSyntaxTests.twin
@@ -0,0 +1,109 @@
+' tbRegExp: where the pattern syntax differs from VBScript's RegExp, which the package replaces.
+' The expected results are what VBScript's RegExp gives for the same pattern and text.
+[TestFixture]
+Module PatternSyntaxTests
+
+    [TestCase]
+    Public Sub IsValid_AtomicGroup()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(?>a)"
+        Expect.IsFalse re.IsValid, "IsValid of the atomic group (?>a)"
+        Err.Clear
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub IsValid_Conditional()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(?(1)a|b)"
+        Expect.IsFalse re.IsValid, "IsValid of the conditional (?(1)a|b)"
+        Err.Clear
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Test_NamedBackreferenceIsText()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "\k"
+        Expect.IsTrue re.IsValid, "IsValid of \k"
+        Expect.IsTrue re.Test("k"), "Test of \k on k"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Test_CapitalAAndZAreLetters()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "\A"
+        Expect.IsTrue re.Test("A"), "Test of \A on A"
+        Expect.IsFalse re.Test("b"), "Test of \A on b"
+        re.Pattern = "\Z"
+        Expect.IsTrue re.Test("Z"), "Test of \Z on Z"
+        Expect.IsFalse re.Test("b"), "Test of \Z on b"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Test_BackreferenceToLaterGroupMatchesNothing()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "\1(a)"
+        Expect.IsFalse re.Test("a"), "Test of \1(a) on a (VBScript finds no match)"
+        Expect.IsFalse re.Test("aa"), "Test of \1(a) on aa (VBScript finds no match)"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub IsValid_BraceThatStartsNoQuantifier()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a{"
+        Expect.IsTrue re.IsValid, "IsValid of a{ (VBScript reads the brace as text)"
+        re.Pattern = "a{x}"
+        Expect.IsTrue re.IsValid, "IsValid of a{x} (VBScript reads the braces as text)"
+        re.Pattern = "\p{L}"
+        Expect.IsTrue re.IsValid, "IsValid of \p{L} (VBScript reads it as the text p{L})"
+        Err.Clear
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Test_BraceThatStartsNoQuantifierIsText()
+        Expect.Begin
+        Dim re As New RegExp
+        Dim ok As Boolean
+        re.Pattern = "a{x}"
+        Err.Clear
+        On Error Resume Next
+        ok = re.Test("a{x}")
+        Dim n As Long
+        n = Err.Number
+        On Error GoTo 0
+        Expect.AreEqual 0, n, "Err.Number of Test of a{x} on a{x}"
+        Expect.IsTrue ok, "Test of a{x} on a{x} (VBScript reads the braces as text)"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Test_CommaAndMaximumOnlyIsText()
+        Expect.Begin
+        Dim re As New RegExp
+        Dim found As String, miss As Boolean
+        re.Pattern = "a{,2}"
+        Err.Clear
+        On Error Resume Next
+        found = re.Execute("a{,2}")(0).Value
+        miss = re.Test("aa")
+        Dim n As Long
+        n = Err.Number
+        On Error GoTo 0
+        Expect.AreEqual 0, n, "Err.Number of Execute and Test with the pattern a{,2}"
+        Expect.AreEqual "a{,2}", found, "first match of a{,2} in a{,2} (VBScript reads {,2} as text)"
+        Expect.IsFalse miss, "Test of a{,2} on aa (VBScript reads {,2} as text)"
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbRegExp/Sources/ReplaceTests.twin b/test/package-tests/tbRegExp/Sources/ReplaceTests.twin
new file mode 100644
index 000000000..6234a2b58
--- /dev/null
+++ b/test/package-tests/tbRegExp/Sources/ReplaceTests.twin
@@ -0,0 +1,71 @@
+' tbRegExp: RegExp.Replace and the substitution items of its replacement string. The expected
+' texts are what VBScript's RegExp.Replace returns, except for $, which only tbRegExp has.
+[TestFixture]
+Module ReplaceTests
+
+    [TestCase]
+    Public Sub Replace_MatchAndContextItems()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "b"
+        Expect.AreEqual "a[b|a|c|$]c", re.Replace("abc", "[$&|$`|$'|$$]"), "Replace(""abc"", ""[$&|$`|$'|$$]"") for the pattern b"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_NumberedGroupItems()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "(a)(b)"
+        Expect.AreEqual "[a|b|$3|$0|$00]c", re.Replace("abc", "[$1|$2|$3|$0|$00]"), "Replace(""abc"", ""[$1|$2|$3|$0|$00]"") for the pattern (a)(b)"
+        Expect.AreEqual "[a|b|a0|a1]c", re.Replace("abc", "[$01|$02|$10|$11]"), "Replace(""abc"", ""[$01|$02|$10|$11]"") for the pattern (a)(b)"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_DollarThatStartsNoItemIsCopied()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "(a)(b)"
+        Expect.AreEqual "a$c", re.Replace("abc", "$1$"), "Replace(""abc"", ""$1$"") for the pattern (a)(b)"
+        Expect.AreEqual "$xc", re.Replace("abc", "$x"), "Replace(""abc"", ""$x"") for the pattern (a)(b)"
+        Expect.AreEqual "$$c", re.Replace("abc", "$$$"), "Replace(""abc"", ""$$$"") for the pattern (a)(b)"
+        Expect.AreEqual "$c", re.Replace("abc", "$"), "Replace(""abc"", ""$"") for the pattern (a)(b), which has no named group"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_GroupThatTookNoPartIsEmpty()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "(a)|(z)"
+        Expect.AreEqual "[]bc", re.Replace("abc", "[$2]"), "Replace(""abc"", ""[$2]"") for the pattern (a)|(z)"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_NamedGroupItem()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(?\w+) (?\w+)"
+        Expect.AreEqual "Lovelace, Ada", re.Replace("Ada Lovelace", "$, $"), "Replace(""Ada Lovelace"", ""$, $"")"
+        Expect.AreEqual "[]", re.Replace("Ada Lovelace", "[$]"), "Replace(""Ada Lovelace"", ""[$]"") with no group of that name"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_TildeItemIsCopied()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "a"
+        Expect.AreEqual "x$~ybc", re.Replace("abc", "x$~y"), "Replace(""abc"", ""x$~y"") for the pattern a (VBScript copies $~)"
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbRegExp/Sources/RunAll.twin b/test/package-tests/tbRegExp/Sources/RunAll.twin
new file mode 100644
index 000000000..8a724f125
--- /dev/null
+++ b/test/package-tests/tbRegExp/Sources/RunAll.twin
@@ -0,0 +1,138 @@
+' Generated by scripts/package_tests.mjs from this project's [TestCase] Subs and expect.json:
+' do not edit. Run Sub Main: each case prints PASS, FAIL or SKIP in the DEBUG CONSOLE.
+' The cases are written for the package tbRegExp, version 1.8.0.0. Each one checks what is
+' correct, so a case that fails names a defect of the package.
+Module RunAll
+
+    Public Sub Main()
+        Expect.StartRun "tbRegExp 1.8.0.0: 31 test cases"
+        On Error Resume Next
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.IsValid_LowCountAboveHigh
+        Expect.EndCase "IsValidTests.IsValid_LowCountAboveHigh", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.LastErrorNumber_LowCountAboveHigh
+        Expect.EndCase "IsValidTests.LastErrorNumber_LowCountAboveHigh", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.Test_LowCountAboveHighRaisesCompileError
+        Expect.EndCase "IsValidTests.Test_LowCountAboveHighRaisesCompileError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.Execute_LowCountAboveHighRaisesCompileError
+        Expect.EndCase "IsValidTests.Execute_LowCountAboveHighRaisesCompileError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.Replace_LowCountAboveHighRaisesCompileError
+        Expect.EndCase "IsValidTests.Replace_LowCountAboveHighRaisesCompileError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.Split_LowCountAboveHighRaisesCompileError
+        Expect.EndCase "IsValidTests.Split_LowCountAboveHighRaisesCompileError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.IsValid_InvalidPatternLeavesNoError
+        Expect.EndCase "IsValidTests.IsValid_InvalidPatternLeavesNoError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.LastError_InvalidPatternLeavesNoError
+        Expect.EndCase "IsValidTests.LastError_InvalidPatternLeavesNoError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        IsValidTests.LastErrorNumber_InvalidPatternLeavesNoError
+        Expect.EndCase "IsValidTests.LastErrorNumber_InvalidPatternLeavesNoError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.IsValid_AtomicGroup
+        Expect.EndCase "PatternSyntaxTests.IsValid_AtomicGroup", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.IsValid_Conditional
+        Expect.EndCase "PatternSyntaxTests.IsValid_Conditional", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.Test_NamedBackreferenceIsText
+        Expect.EndCase "PatternSyntaxTests.Test_NamedBackreferenceIsText", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.Test_CapitalAAndZAreLetters
+        Expect.EndCase "PatternSyntaxTests.Test_CapitalAAndZAreLetters", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.Test_BackreferenceToLaterGroupMatchesNothing
+        Expect.EndCase "PatternSyntaxTests.Test_BackreferenceToLaterGroupMatchesNothing", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.IsValid_BraceThatStartsNoQuantifier
+        Expect.EndCase "PatternSyntaxTests.IsValid_BraceThatStartsNoQuantifier", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.Test_BraceThatStartsNoQuantifierIsText
+        Expect.EndCase "PatternSyntaxTests.Test_BraceThatStartsNoQuantifierIsText", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PatternSyntaxTests.Test_CommaAndMaximumOnlyIsText
+        Expect.EndCase "PatternSyntaxTests.Test_CommaAndMaximumOnlyIsText", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ReplaceTests.Replace_MatchAndContextItems
+        Expect.EndCase "ReplaceTests.Replace_MatchAndContextItems", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ReplaceTests.Replace_NumberedGroupItems
+        Expect.EndCase "ReplaceTests.Replace_NumberedGroupItems", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ReplaceTests.Replace_DollarThatStartsNoItemIsCopied
+        Expect.EndCase "ReplaceTests.Replace_DollarThatStartsNoItemIsCopied", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ReplaceTests.Replace_GroupThatTookNoPartIsEmpty
+        Expect.EndCase "ReplaceTests.Replace_GroupThatTookNoPartIsEmpty", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ReplaceTests.Replace_NamedGroupItem
+        Expect.EndCase "ReplaceTests.Replace_NamedGroupItem", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ReplaceTests.Replace_TildeItemIsCopied
+        Expect.EndCase "ReplaceTests.Replace_TildeItemIsCopied", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchAbortedTests.SearchAborted_DigitsWithoutXFinishAt2000
+        Expect.EndCase "SearchAbortedTests.SearchAborted_DigitsWithoutXFinishAt2000", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchAbortedTests.SearchAborted_DigitsWithoutXFinishAt5000
+        Expect.EndCase "SearchAbortedTests.SearchAborted_DigitsWithoutXFinishAt5000", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchGivesUpTests.Test_CatastrophicPatternGivesUpWithoutError
+        Expect.EndCase "SearchGivesUpTests.Test_CatastrophicPatternGivesUpWithoutError", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchGivesUpTests.Execute_CatastrophicPatternKeepsMatchesFound
+        Expect.EndCase "SearchGivesUpTests.Execute_CatastrophicPatternKeepsMatchesFound", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchGivesUpTests.Replace_CatastrophicPatternReplacesMatchesFound
+        Expect.EndCase "SearchGivesUpTests.Replace_CatastrophicPatternReplacesMatchesFound", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchGivesUpTests.Split_CatastrophicPatternSplitsAtMatchesFound
+        Expect.EndCase "SearchGivesUpTests.Split_CatastrophicPatternSplitsAtMatchesFound", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchGivesUpTests.Search_LongMatchOfAStarIsNotCut
+        Expect.EndCase "SearchGivesUpTests.Search_LongMatchOfAStarIsNotCut", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        SearchGivesUpTests.Search_LongMatchOfDotStarIsNotCut
+        Expect.EndCase "SearchGivesUpTests.Search_LongMatchOfDotStarIsNotCut", Err.Number, Err.Description
+        On Error GoTo 0
+        Expect.EndRun
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbRegExp/Sources/SearchAbortedTests.twin b/test/package-tests/tbRegExp/Sources/SearchAbortedTests.twin
new file mode 100644
index 000000000..b81b661a5
--- /dev/null
+++ b/test/package-tests/tbRegExp/Sources/SearchAbortedTests.twin
@@ -0,0 +1,27 @@
+' tbRegExp: RegExp.SearchAborted for a pattern that is not catastrophic. A search that only has to
+' try each start position finishes, however long the string; SearchAborted is for a search that
+' backtracks without end (see SearchGivesUpTests).
+[TestFixture]
+Module SearchAbortedTests
+
+    [TestCase]
+    Public Sub SearchAborted_DigitsWithoutXFinishAt2000()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "\d+x"
+        Expect.IsFalse re.Test(String$(2000, "1")), "Test of \d+x on 2,000 digits and no x"
+        Expect.IsFalse re.SearchAborted, "SearchAborted after Test of \d+x on 2,000 digits and no x"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub SearchAborted_DigitsWithoutXFinishAt5000()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "\d+x"
+        Expect.IsFalse re.Test(String$(5000, "1")), "Test of \d+x on 5,000 digits and no x"
+        Expect.IsFalse re.SearchAborted, "SearchAborted after Test of \d+x on 5,000 digits and no x"
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbRegExp/Sources/SearchGivesUpTests.twin b/test/package-tests/tbRegExp/Sources/SearchGivesUpTests.twin
new file mode 100644
index 000000000..a255ceac1
--- /dev/null
+++ b/test/package-tests/tbRegExp/Sources/SearchGivesUpTests.twin
@@ -0,0 +1,84 @@
+' tbRegExp: a search that backtracks without end, such as (a+)+b on thirty a, gives up when its
+' step budget runs out instead of hanging, as VBScript's RegExp does. It raises nothing, returns
+' what was found before, and RegExp.SearchAborted says so. A long match is not cut short.
+[TestFixture]
+Module SearchGivesUpTests
+
+    [TestCase]
+    Public Sub Test_CatastrophicPatternGivesUpWithoutError()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "(a+)+b"
+        Err.Clear
+        Expect.IsFalse re.SearchAborted, "SearchAborted of an object that has not searched"
+        Expect.IsFalse re.Test(String$(30, "a")), "Test of (a+)+b on thirty a"
+        Expect.AreEqual 0, Err.Number, "Err.Number after the search gave up"
+        Expect.IsTrue re.SearchAborted, "SearchAborted after Test of (a+)+b on thirty a"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Execute_CatastrophicPatternKeepsMatchesFound()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "x|(a+)+b"
+        Dim m As MatchCollection
+        Set m = re.Execute("xx" & String$(30, "a"))
+        Expect.AreEqual 2, m.Count, "matches of x|(a+)+b in xx and thirty a"
+        Expect.IsTrue re.SearchAborted, "SearchAborted after that Execute"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Replace_CatastrophicPatternReplacesMatchesFound()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "x|(a+)+b"
+        Expect.AreEqual "--" & String$(30, "a"), re.Replace("xx" & String$(30, "a"), "-"), "Replace of x|(a+)+b in xx and thirty a"
+        Expect.IsTrue re.SearchAborted, "SearchAborted after that Replace"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Split_CatastrophicPatternSplitsAtMatchesFound()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Global = True
+        re.Pattern = "x|(?:a+)+b"
+        Dim c As Collection
+        Set c = re.Split("xx" & String$(30, "a"))
+        Expect.AreEqual 3, c.Count, "pieces of xx and thirty a split at x|(?:a+)+b"
+        Expect.AreEqual String$(30, "a"), c(3), "last piece"
+        Expect.IsTrue re.SearchAborted, "SearchAborted after that Split"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Search_LongMatchOfAStarIsNotCut()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "a*"
+        Dim m As MatchCollection
+        Set m = re.Execute(String$(1000000, "a"))
+        Expect.AreEqual 1, m.Count, "matches of a* in 1,000,000 a"
+        Expect.AreEqual 1000000, m(0).Length, "length of the match of a* in 1,000,000 a"
+        Expect.IsFalse re.SearchAborted, "SearchAborted after that Execute"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Search_LongMatchOfDotStarIsNotCut()
+        Expect.Begin
+        Dim re As New RegExp
+        re.Pattern = "^.*$"
+        Dim m As MatchCollection
+        Set m = re.Execute(String$(1000000, "a"))
+        Expect.AreEqual 1, m.Count, "matches of ^.*$ in 1,000,000 a"
+        Expect.AreEqual 1000000, m(0).Length, "length of the match of ^.*$ in 1,000,000 a"
+        Expect.IsFalse re.SearchAborted, "SearchAborted after that Execute"
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbRegExp/expect.json b/test/package-tests/tbRegExp/expect.json
new file mode 100644
index 000000000..d796abc12
--- /dev/null
+++ b/test/package-tests/tbRegExp/expect.json
@@ -0,0 +1,50 @@
+{
+  "version": "1.8.0.0",
+  "cases": {
+    "IsValidTests.IsValid_LowCountAboveHigh": {
+      "fails": "1 of 1 checks failed: IsValid of the pattern a{2,1}, whose lower count is above its upper count: expected False, got True"
+    },
+    "IsValidTests.LastErrorNumber_LowCountAboveHigh": {
+      "fails": "1 of 1 checks failed: LastErrorNumber of the pattern a{2,1} (invalid quantifier): expected -2147218496, got 0"
+    },
+    "IsValidTests.Test_LowCountAboveHighRaisesCompileError": {
+      "fails": "2 of 2 checks failed: Err.Number of Test(\"aa\") with the pattern a{2,1}: expected -2147218496, got 3000; Err.Description of Test(\"aa\") with the pattern a{2,1}: expected \"Invalid quantifier: a malformed '{n,m}' count (e.g. '{2,1}', or a number too large).\", got \"The specified print monitor is unknown.\""
+    },
+    "IsValidTests.Execute_LowCountAboveHighRaisesCompileError": {
+      "fails": "1 of 1 checks failed: Err.Number of Execute(\"aa\") with the pattern a{2,1}: expected -2147218496, got 3000"
+    },
+    "IsValidTests.Replace_LowCountAboveHighRaisesCompileError": {
+      "fails": "1 of 1 checks failed: Err.Number of Replace(\"aa\", \"x\") with the pattern a{2,1}: expected -2147218496, got 3000"
+    },
+    "IsValidTests.Split_LowCountAboveHighRaisesCompileError": {
+      "fails": "1 of 1 checks failed: Err.Number of Split(\"aa\") with the pattern a{2,1}: expected -2147218496, got 3000"
+    },
+    "IsValidTests.IsValid_InvalidPatternLeavesNoError": {
+      "fails": "1 of 2 checks failed: Err.Number after IsValid compiled the pattern (\\d+: expected 0, got -2147218495"
+    },
+    "IsValidTests.LastError_InvalidPatternLeavesNoError": {
+      "fails": "1 of 2 checks failed: Err.Number after LastError compiled the pattern (\\d+: expected 0, got -2147218495"
+    },
+    "IsValidTests.LastErrorNumber_InvalidPatternLeavesNoError": {
+      "fails": "1 of 2 checks failed: Err.Number after LastErrorNumber compiled the pattern (\\d+: expected 0, got -2147218495"
+    },
+    "PatternSyntaxTests.Test_BackreferenceToLaterGroupMatchesNothing": {
+      "fails": "2 of 2 checks failed: Test of \\1(a) on a (VBScript finds no match): expected False, got True; Test of \\1(a) on aa (VBScript finds no match): expected False, got True"
+    },
+    "PatternSyntaxTests.IsValid_BraceThatStartsNoQuantifier": {
+      "fails": "3 of 3 checks failed: IsValid of a{ (VBScript reads the brace as text): expected True, got False; IsValid of a{x} (VBScript reads the braces as text): expected True, got False; IsValid of \\p{L} (VBScript reads it as the text p{L}): expected True, got False"
+    },
+    "PatternSyntaxTests.Test_BraceThatStartsNoQuantifierIsText": {
+      "fails": "2 of 2 checks failed: Err.Number of Test of a{x} on a{x}: expected 0, got -2147218496; Test of a{x} on a{x} (VBScript reads the braces as text): expected True, got False"
+    },
+    "PatternSyntaxTests.Test_CommaAndMaximumOnlyIsText": {
+      "fails": "2 of 3 checks failed: first match of a{,2} in a{,2} (VBScript reads {,2} as text): expected \"a{,2}\", got \"a\"; Test of a{,2} on aa (VBScript reads {,2} as text): expected False, got True"
+    },
+    "ReplaceTests.Replace_TildeItemIsCopied": {
+      "fails": "1 of 1 checks failed: Replace(\"abc\", \"x$~y\") for the pattern a (VBScript copies $~): expected \"x$~ybc\", got \"xybc\""
+    },
+    "SearchAbortedTests.SearchAborted_DigitsWithoutXFinishAt5000": {
+      "fails": "1 of 2 checks failed: SearchAborted after Test of \\d+x on 5,000 digits and no x: expected False, got True"
+    }
+  }
+}

From 7d07f3c7832ece608b72333b763b28933ddfe052 Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Wed, 14 Oct 2026 11:34:43 +0200
Subject: [PATCH 09/13] tbFileSysTools: test project (72 cases, 24 documented
 failures, the same on win32 and win64), markers on its pages, Test cases
 section; GetRelativePath fails from a 259-character base, CreateFolder nul
 raises 76, MoveFolder's reserved-name errors, GetFileEncoding's hiragana and
 kanji; WARNING: MoveFile to nul discards the file

---
 .../Contributed/tbFileSysTools/File.md        |   2 +
 .../tbFileSysTools/FileSystemTools.md         |  50 +-
 .../Contributed/tbFileSysTools/Folder.md      |   1 +
 .../Contributed/tbFileSysTools/TextStream.md  |   6 +-
 .../Contributed/tbFileSysTools/index.md       |   7 +-
 scripts/check_package_tests.mjs               |   1 -
 .../Sources/AttributeTests.twin               | 172 +++++++
 .../Sources/DetectionTests.twin               | 117 +++++
 .../Sources/ErrorNumberTests.twin             | 445 ++++++++++++++++++
 .../tbFileSysTools/Sources/JunctionTests.twin | 142 ++++++
 .../tbFileSysTools/Sources/LongPathTests.twin | 395 ++++++++++++++++
 .../tbFileSysTools/Sources/PathTests.twin     |  48 ++
 .../tbFileSysTools/Sources/RunAll.twin        | 302 ++++++++++++
 .../tbFileSysTools/Sources/Sandbox.twin       |  87 ++++
 .../Sources/ShortNameTests.twin               | 168 +++++++
 .../Sources/TextStreamTests.twin              | 149 ++++++
 test/package-tests/tbFileSysTools/expect.json |  77 +++
 17 files changed, 2153 insertions(+), 16 deletions(-)
 create mode 100644 test/package-tests/tbFileSysTools/Sources/AttributeTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/DetectionTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/ErrorNumberTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/JunctionTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/LongPathTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/PathTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/RunAll.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/Sandbox.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/ShortNameTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/Sources/TextStreamTests.twin
 create mode 100644 test/package-tests/tbFileSysTools/expect.json

diff --git a/docs/Reference/Contributed/tbFileSysTools/File.md b/docs/Reference/Contributed/tbFileSysTools/File.md
index af2cb391d..d89871333 100644
--- a/docs/Reference/Contributed/tbFileSysTools/File.md
+++ b/docs/Reference/Contributed/tbFileSysTools/File.md
@@ -323,6 +323,7 @@ Syntax: *object*.**OpenAsTextStream** ( [ *ioMode* [, *encoding* [, *writeModeLi
 
 Returns a [**TextStream**](TextStream); the rules are those of [**OpenTextFile**](FileSystemTools#opentextfile) with *create* **False**.
 
+
 > [!WARNING]
 > **ForWriting** empties the file when the stream is opened. Use **ForAppending** to add to it.
 
@@ -360,6 +361,7 @@ Syntax: *object*.**SetAttribute** ( *attr*, *state* )
 *state*
 : *required* A **Boolean**. **True** sets the attribute and **False** clears it. The other attributes stay as they are.
 
+
 > [!WARNING]
 > In version 1.9.0.0, **SetAttribute** sets nothing, and raises no error, on a file that has no attribute at all, because [**Attributes**](#attributes) then reports **faNormal**, and **faNormal** wins over the attribute being set. A new file has the archive attribute, so it is a file from which the program cleared every attribute that this applies to. To give such a file an attribute, assign [**Attributes**](#attributes). The same method of a [**Folder**](Folder#setattribute) is not affected.
 
diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md
index e713cbc06..bb1a4ca15 100644
--- a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md
+++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md
@@ -50,7 +50,10 @@ These rules apply to every procedure on the page. Each entry states only what de
 - A forward slash in a path is read as a backslash.
 - A relative path is resolved against the current directory of the process, which [**GetCurrentDir**](#getcurrentdir) returns. A procedure that compares two paths, or uses one path more than once, resolves it first, so `a.txt` and `.\a.txt` name the same file to it.
 - A path longer than 260 characters works in most procedures, among them [**CreateFolder**](#createfolder), [**FileExists**](#fileexists), [**StringToTextFile**](#stringtotextfile), [**TextFileToString**](#textfiletostring), [**CopyFile**](#copyfile), [**MoveFile**](#movefile), [**Rename**](#rename), [**DeleteFile**](#deletefile), [**DeleteFolder**](#deletefolder), [**GetFile**](#getfile) and [**GetFilePaths**](#getfilepaths). The Scripting Runtime does not accept such a path: its **FileExists** and **FolderExists** return **False** for it, **GetFile** raises error 53 and **OpenTextFile** raises error 76.
-- These stop at the limit. [**GetFileVersion**](#getfileversion) returns an empty string, and [**File.ShortPath**](File#shortpath) returns the path as it is. [**SetCurrentDir**](#setcurrentdir) raises error 75. A path with a wildcard in it is not found: [**DeleteFile**](#deletefile) and [**CopyFile**](#copyfile) raise error 53 for `deep\*.dll`, where `deep` is a folder with a long path. [**Folder.Files**](Folder#files), [**Folder.SubFolders**](Folder#subfolders) and [**Folder.Size**](Folder#size) raise error 76 for a folder whose own path has 258 characters or more. [**GetRelativePath**](#getrelativepath) can return a wrong path.
+
+
+> [!NOTE]
+> These stop at the limit. [**GetFileVersion**](#getfileversion) returns an empty string, and [**File.ShortPath**](File#shortpath) returns the path as it is. [**SetCurrentDir**](#setcurrentdir) raises error 75. A path with a wildcard in it is not found: [**DeleteFile**](#deletefile) and [**CopyFile**](#copyfile) raise error 53 for `deep\*.dll`, where `deep` is a folder with a long path. [**Folder.Files**](Folder#files), [**Folder.SubFolders**](Folder#subfolders) and [**Folder.Size**](Folder#size) raise error 76 for a folder whose own path has 258 characters or more (see the note under [**Folder.Files**](Folder#files)), and [**GetRelativePath**](#getrelativepath) can return a wrong path (see its warning).
 
 ```tb check_build check_run
 Dim work As String
@@ -77,6 +80,7 @@ DeleteFolder work
 
 **Wildcards.** [**CopyFile**](#copyfile), [**CopyFolder**](#copyfolder), [**DeleteFile**](#deletefile), [**DeleteFolder**](#deletefolder), [**MoveFile**](#movefile) and [**MoveFolder**](#movefolder) accept `*` and `?` in the last component of the path that names what to act on. A wildcard in a destination is error 5. A pattern that matches nothing is an error, as described for each procedure. A failure part-way through a wildcard operation leaves the earlier matches changed; the sample under [**DeleteFile**](#deletefile) shows it.
 
+
 > [!WARNING]
 > Windows matches a pattern against the 8.3 short name of a file as well as against its name. On a volume that creates short names, `*.txt` also matches `a.txtx`, whose short name ends in `.TXT`. [**CopyFile**](#copyfile), [**DeleteFile**](#deletefile) and [**MoveFile**](#movefile) act on such a file, and the procedures for folders match the short names of folders in the same way. The Scripting Runtime does the same. [**GetFilePaths**](#getfilepaths) does not list such a file: it tests each name against the pattern again.
 
@@ -103,14 +107,18 @@ DeleteFolder work
 ### Junctions and symbolic links
 {: .no_toc }
 
-A directory junction or symbolic link is a folder that stands for another folder. [**CopyFolder**](#copyfolder), [**DeleteFolder**](#deletefolder), [**GetFilePaths**](#getfilepaths) with *recursive*, [**FindInTextFiles**](#findintextfiles) with *recursive* and [**Folder.Size**](Folder#size) do not enter one:
-
-- **CopyFolder** leaves the link out of the copy.
-- **DeleteFolder** removes the link and leaves the folder it stands for, with its files, as it was.
-- **GetFilePaths** lists no file below the link.
-- **Size** counts each file once, where it is stored.
+A directory junction or symbolic link is a folder that stands for another folder.
 
-The link is still a member of the [**SubFolders**](Folder#subfolders) of its folder. The Scripting Runtime enters a link in all of these. It counts a target twice when the target is in the same tree, copies the files of the target into the copy under the name of the link, and deletes the files of the target.
+
+> [!NOTE]
+> [**CopyFolder**](#copyfolder), [**DeleteFolder**](#deletefolder), [**GetFilePaths**](#getfilepaths) with *recursive*, [**FindInTextFiles**](#findintextfiles) with *recursive* and [**Folder.Size**](Folder#size) do not enter a link:
+>
+> - **CopyFolder** leaves the link out of the copy.
+> - **DeleteFolder** removes the link and leaves the folder it stands for, with its files, as it was.
+> - **GetFilePaths** lists no file below the link.
+> - **Size** counts each file once, where it is stored.
+>
+> The link is still a member of the [**SubFolders**](Folder#subfolders) of its folder. The Scripting Runtime enters a link in all of these. It counts a target twice when the target is in the same tree, copies the files of the target into the copy under the name of the link, and deletes the files of the target.
 
 The sample builds a tree with a link to one of its own folders, and a second tree with a link to a folder outside it. **DeleteFolder** of the second tree leaves the file in the outside folder.
 
@@ -345,8 +353,9 @@ Both paths are first made absolute with [**GetAbsolutePathName**](#getabsolutepa
 
 Returns the absolute *targetPath* when there is no relative path, for example when the two paths are on different drives, and when only *targetPath* is longer than 260 characters.
 
+
 > [!WARNING]
-> In version 1.9.0.0, when *basePath* is 260 characters or longer, the result can be a relative path that leads to the wrong place, and no error is raised. For a target `x\y.txt` below such a base, the result starts with `..\` and the last name of the base, instead of being `x\y.txt`. A base path of 258 characters or fewer gives the right result.
+> In version 1.9.0.0, when *basePath* is 259 characters or longer, the result can be a relative path that leads to the wrong place, and no error is raised. For a target `x\y.txt` below such a base, the result starts with `..\` and the last name of the base, instead of being `x\y.txt`. A base path of 258 characters or fewer gives the right result.
 
 ```tb check_build check_run
 Debug.Print GetRelativePath("C:\Projects\App", "C:\Projects\Lib\core.txt")   ' ..\Lib\core.txt
@@ -494,6 +503,7 @@ Errors:
 - 5 when *destination* is the source or lies inside it, which would copy the folder into itself.
 - 58 when a file exists and *overwrite* is **False**.
 
+
 > [!NOTE]
 > A directory junction or symbolic link inside the source is left out of the copy: the copy has no folder of that name, and nothing from the folder the link stands for. The Scripting Runtime copies the files of the target into the copy under the name of the link. See [Junctions and symbolic links](#junctions-and-symbolic-links).
 
@@ -517,7 +527,7 @@ Errors:
 - 58 when a folder, or a file, with that path exists.
 - 76 when the parent does not exist and *createIntermediates* is **False**.
 - 52 when the last component holds a character that is not allowed in a name, the wildcards `*` and `?` among them.
-- 75 when the last component is a reserved device name such as `con` or `nul` (the message ends with the Windows code 87).
+- 75 when the last component is the reserved device name `con` (the message ends with the Windows code 87), and 76 when it is `nul`.
 
 ```tb check_build check_run
 Dim work As String
@@ -601,6 +611,7 @@ The contents are deleted first, and an item that cannot be deleted is skipped wi
 
 Errors: 76 when no folder matches or a file is named; 70 when the folder is not empty because something in it could not be deleted, such as a read-only file when *force* is **False**. The folder and that file stay. The Scripting Runtime raises error 70 in that case too.
 
+
 > [!NOTE]
 > A directory junction or symbolic link inside the folder is removed, and the folder it stands for keeps its files. The Scripting Runtime deletes the files in the target as well, so a tree that holds a link to a folder outside it loses the contents of that folder there. See [Junctions and symbolic links](#junctions-and-symbolic-links).
 
@@ -671,11 +682,16 @@ A file is never replaced: when the destination file exists, error 58 is raised.
 
 Moving a file onto itself does nothing and returns its path, except that error 70 is raised when the file is in use. A change of letter case alone is also a move onto itself, so it changes nothing; use [**Rename**](#rename) for it.
 
-Errors: 53 when a wildcard matches no file, or matches only folders; 76 when the destination folder of a wildcard move does not exist; 58 when the destination file exists; 52 when the name of the destination holds a character that Windows does not allow; 75 when it is a reserved device name such as `con` (the message ends with the Windows code 87).
+Errors: 53 when a wildcard matches no file, or matches only folders; 76 when the destination folder of a wildcard move does not exist; 58 when the destination file exists; 52 when the name of the destination holds a character that Windows does not allow; 75 when it is the reserved device name `con` (the message ends with the Windows code 87).
 
+
 > [!NOTE]
 > A *source* without a wildcard that names a folder is moved too, with no error: `MoveFile "C:\old", "C:\new"` renames the folder. The Scripting Runtime raises error 53.
 
+
+> [!WARNING]
+> In version 1.9.0.0, a *destination* that is the reserved device name `nul`, in any letter case and with or without an extension (`nul`, `NUL`, `nul.txt`), is not refused. **MoveFile** raises no error, and the file is gone: its contents were moved to the NUL device and cannot be recovered. The Scripting Runtime raises error 58 and leaves the file. [**File.Move**](File#move) and the **MoveFile** of [**FileSystemObject**](FileSystemObject#movefile) do the same; **File.Move** then raises error 53.
+
 ### MoveFolder
 {: .no_toc }
 
@@ -819,7 +835,11 @@ Returns a [**TextEncoding**](#textencoding) member. A negative value means that
 
 A sample looks like binary data when it holds a zero byte, or more than 2% control characters other than tab, line feed, carriage return and form feed. The second test is made only for a sample of 64 bytes or more, and a zero byte that is the last byte of the file does not count.
 
-The result is an estimate, as every guess about the encoding of a text file is. A file in UTF-16, UTF-32 or UTF-8, with or without a BOM, is found right; so is a Latin-1 file when the system code page is Latin-1. A short file gives little evidence: UTF-16 text with no zero byte, such as eight Japanese characters with no line ending, is reported as **encSystemAnsi**, and a few bytes of binary data can be reported as UTF-16. Text in another code page without a BOM is not found: GB18030 text with an accented letter is reported as **encSystemAnsi**, and UTF-7 or Big5 text that is pure ASCII as **encUtf8**. A sample that ends in the first byte of a multi-byte sequence is judged without that byte, so a Latin-1 file whose only accented letter is its last byte is reported as **encUtf8**, and so is a Latin-1 file whose accented letter comes after the first 32,768 bytes. Pass the encoding to the procedure that reads the file when it is known.
+The result is an estimate, as every guess about the encoding of a text file is. A file in UTF-16, UTF-32 or UTF-8, with or without a BOM, is found right, except in the cases of the note below; so is a Latin-1 file when the system code page is Latin-1. Pass the encoding to the procedure that reads the file when it is known.
+
+
+> [!NOTE]
+> A short file gives little evidence. UTF-16 text with no zero byte, such as eight kanji with no line ending, is reported as **encSystemAnsi**, or as **encUtf8** when its bytes are also valid UTF-8, as for eight hiragana. A few bytes of binary data can be reported as UTF-16. Text in another code page without a BOM is not found: GB18030 text with an accented letter is reported as **encSystemAnsi**, and UTF-7 or Big5 text that is pure ASCII as **encUtf8**. A sample that ends in the first byte of a multi-byte sequence is judged without that byte, so a Latin-1 file whose only accented letter is its last byte is reported as **encUtf8**, and so is a Latin-1 file whose accented letter comes after the first 32,768 bytes.
 
 Raises error 53 when the file does not exist; see [Errors](./#errors).
 
@@ -1498,11 +1518,15 @@ Syntax: **OpenTextFile** ( *filePath* [, *ioMode* [, *create* [, *encoding* [, *
 What the mode does:
 
 - **ForReading** requires the file. The encoding is found from the start of the file: a BOM first, then *encoding* when it names one, then the same guess as [**GetFileEncoding**](#getfileencoding). A file that looks binary is refused with error 5 when the encoding is detected. An empty file is reported as UTF-8.
-- **ForWriting** empties the file when it is opened, for an encoding that is written in pieces; a stream that holds the whole file (UTF-7) replaces the file when it is closed or flushed. An existing read-only file raises error 70 when the stream is opened, except for UTF-7, where the error comes at the first flush or close and the file keeps its text. *encoding* gives the encoding, and **encAutoDetect** means UTF-8. A BOM is written for a member with **Bom** in its name. While the stream is open, other streams can read the file, and cannot open it for writing or appending (error 70). A stream open for reading does not stop another from opening the file for writing.
+- **ForWriting** empties the file when it is opened, for an encoding that is written in pieces; a stream that holds the whole file (UTF-7) replaces the file when it is closed or flushed. An existing read-only file raises error 70 when the stream is opened, except for UTF-7 (see the note below). *encoding* gives the encoding, and **encAutoDetect** means UTF-8. A BOM is written for a member with **Bom** in its name. While the stream is open, other streams can read the file, and cannot open it for writing or appending (error 70). A stream open for reading does not stop another from opening the file for writing.
 - **ForAppending** adds to the end. A file that exists keeps its encoding and BOM, found as for reading: *encoding* is used only when the file has no BOM, and is not checked against the text in the file, so a wrong one mixes encodings in the file. A BOM is not written again, and none is written to an existing file that is empty. The line ending for **WriteLine** is the one the first 64 KB of the file use, and *writeModeLineEnding* is used only for an empty file, a file without a line ending and a file with mixed ones. A file that does not exist is created like a new file under **ForWriting**, with the BOM of the encoding.
 
 Errors: 53 when the file does not exist for **ForReading**, or for the other modes with *create* **False**; 76 when the folder does not exist; 52 for a name that is not valid when *create* is **True**; 5 for an unknown *ioMode*, and for a binary file as above; 70 when another stream has the file open for writing. The error has an empty **Err.Source** and the standard text of its number, `File not found` for 53, without the path.
 
+
+> [!NOTE]
+> A stream in UTF-7 holds the whole file in memory, as [**TextStream**](TextStream#reading-and-writing) describes. For **ForWriting** on a read-only file, the stream opens without an error and the file keeps its text; error 70 comes at the first [**Flush**](TextStream#flush) or [**Close**](TextStream#close). Every other encoding raises error 70 when the stream is opened.
+
 > [!IMPORTANT]
 > The fourth argument of **Scripting.FileSystemObject.OpenTextFile** is a **Tristate** *Format*, and this procedure's is an *encoding*. **TristateTrue** is -1, which is **encAutoDetect**: on writing that means UTF-8, not UTF-16. The default also differs: a file opened for writing here is UTF-8 without a BOM, where the Scripting Runtime writes ANSI.
 
diff --git a/docs/Reference/Contributed/tbFileSysTools/Folder.md b/docs/Reference/Contributed/tbFileSysTools/Folder.md
index 036de88bc..7c418acd6 100644
--- a/docs/Reference/Contributed/tbFileSysTools/Folder.md
+++ b/docs/Reference/Contributed/tbFileSysTools/Folder.md
@@ -105,6 +105,7 @@ Syntax: *object*.**Files** **As Files**
 
 Returns a [**Files**](Files) collection of the files directly in the folder. Hidden and system files are in it, which differs from [**GetFilePaths**](FileSystemTools#getfilepaths). The folder is read when the property is read, and the collection does not change afterwards. Raises error 70 when the folder cannot be read, as the Scripting Runtime does.
 
+
 > [!NOTE]
 > In version 1.9.0.0, **Files**, [**SubFolders**](#subfolders) and [**Size**](#size) raise error 76 (*Path not found*) for a folder whose path has 258 characters or more, although the folder exists and its [**Name**](#name) can be read. The same members work for a folder with 256 characters.
 
diff --git a/docs/Reference/Contributed/tbFileSysTools/TextStream.md b/docs/Reference/Contributed/tbFileSysTools/TextStream.md
index 3ec0abec8..0fb3ad39c 100644
--- a/docs/Reference/Contributed/tbFileSysTools/TextStream.md
+++ b/docs/Reference/Contributed/tbFileSysTools/TextStream.md
@@ -77,7 +77,11 @@ Debug.Print UBound(b) + 1                     ' 10
 DeleteFolder work
 ```
 
-**Two modes.** Most encodings, UTF-8 among them, are read and written in pieces of 64 KB, and [**IsStreaming**](#isstreaming) is **True**. UTF-7 cannot be cut into pieces. A stream in UTF-7 holds the whole file in memory and [**IsStreaming**](#isstreaming) is **False**. A stream of this kind that is open for writing does not empty the file when it is opened: the file is replaced when the stream is closed or flushed, and when the file is read-only that fails with error 70. Every member works in both modes.
+**Two modes.** Most encodings, UTF-8 among them, are read and written in pieces of 64 KB, and [**IsStreaming**](#isstreaming) is **True**. UTF-7 cannot be cut into pieces. A stream in UTF-7 holds the whole file in memory and [**IsStreaming**](#isstreaming) is **False**. Every member works in both modes.
+
+
+> [!NOTE]
+> A stream of this kind that is open for writing does not empty the file when it is opened: the file is replaced when the stream is closed or flushed. When the file is read-only, the stream opens without an error, the file keeps its text, and the first [**Flush**](#flush) or [**Close**](#close) fails with error 70.
 
 **Position.** [**Line**](#line) and [**Column**](#column) follow the characters that are read or written. Reading a line moves past its line ending.
 
diff --git a/docs/Reference/Contributed/tbFileSysTools/index.md b/docs/Reference/Contributed/tbFileSysTools/index.md
index 8014e7012..1e1b98793 100644
--- a/docs/Reference/Contributed/tbFileSysTools/index.md
+++ b/docs/Reference/Contributed/tbFileSysTools/index.md
@@ -123,11 +123,12 @@ The procedures raise the error numbers that VB6 uses for the same failures. **Er
 
 Windows reports a failure with its own code, and the package maps it to one of those numbers: *file not found* (2) to 53, *path not found* (3) and *bad directory name* (267) to 76, *file exists* (80 and 183) to 58, *access denied* and the sharing violations (5, 32 and 33) to 70, and *invalid name* (123) to 52.
 
+
 > [!NOTE]
 > A few failures raise a number that is not the one the same failure raises elsewhere in the package. The sample shows the first two.
 >
 > - A file that is held open with exclusive access raises the unspecified error -2147467259 when it is read, by [**TextFileToString**](FileSystemTools#textfiletostring), [**GetFileEncoding**](FileSystemTools#getfileencoding), [**ReadStream**](FileSystemTools#readstream) and the other procedures that read a whole file. Writing to it raises 70.
-> - A reserved device name, such as `con` or `nul`, raises 52 in the procedures that write a file ([**StringToTextFile**](FileSystemTools#stringtotextfile), [**ArrayToTextFile**](FileSystemTools#arraytotextfile), [**CreateTextFile**](FileSystemTools#createtextfile), [**OpenTextFile**](FileSystemTools#opentextfile) with *create* **True**, [**Folder.CreateTextFile**](Folder#createtextfile)) and in [**CopyFile**](FileSystemTools#copyfile). It raises 75 in [**CreateFolder**](FileSystemTools#createfolder) and [**MoveFile**](FileSystemTools#movefile), and 5 in [**Rename**](FileSystemTools#rename).
+> - A reserved device name, such as `con` or `nul`, raises 52 in the procedures that write a file ([**StringToTextFile**](FileSystemTools#stringtotextfile), [**ArrayToTextFile**](FileSystemTools#arraytotextfile), [**CreateTextFile**](FileSystemTools#createtextfile), [**OpenTextFile**](FileSystemTools#opentextfile) with *create* **True**, [**Folder.CreateTextFile**](Folder#createtextfile)) and in [**CopyFile**](FileSystemTools#copyfile). It raises other numbers elsewhere. [**CreateFolder**](FileSystemTools#createfolder) raises 75 for `con` and 76 for `nul`; [**MoveFile**](FileSystemTools#movefile) raises 75 for `con`, and for `nul` it raises nothing, which has a warning of its own; [**MoveFolder**](FileSystemTools#movefolder) raises 75 for `con` and 70 for `nul`; [**Rename**](FileSystemTools#rename) raises 5.
 > - [**Folder.Files**](Folder#files), [**SubFolders**](Folder#subfolders) and [**Size**](Folder#size) raise error 76 for a folder whose path has 258 characters or more.
 
 ```tb check_build check_run
@@ -253,3 +254,7 @@ In the procedures that read or write the text or the bytes of a file, a file tha
 - [FileProps](../FileProps/) package -- the Windows shell properties of a file
 - [TBMANLIB_Dialog](../TBMANLIB_Dialog/) package -- the Open, Save and folder dialogs, with path helpers
 - [tbRegExp](../tbRegExp/) package -- regular expressions, by the same author
+
+## Test cases
+
+The notes and warnings of this section are checked by the test cases in [tbFileSysTools-tests.twinproj](tbFileSysTools-tests.twinproj), a twinBASIC project. It uses tbFileSysTools as a [linked package](../../../../Features/Packages/Linked#manual-management): put the package's `.twinpack` in `%APPDATA%\twinBASIC\packages`, then run the project, and each case prints whether it passes.
diff --git a/scripts/check_package_tests.mjs b/scripts/check_package_tests.mjs
index b6068c3a5..25c0315d4 100644
--- a/scripts/check_package_tests.mjs
+++ b/scripts/check_package_tests.mjs
@@ -110,7 +110,6 @@ const PENDING = new Set([
   "VBA_JSON",
   "VBComDlg",
   "WinReg",
-  "tbFileSysTools",
   "tbProjPacker",
   "tsJson",
   "tsPwGen",
diff --git a/test/package-tests/tbFileSysTools/Sources/AttributeTests.twin b/test/package-tests/tbFileSysTools/Sources/AttributeTests.twin
new file mode 100644
index 000000000..adb64c945
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/AttributeTests.twin
@@ -0,0 +1,172 @@
+' File.SetAttribute and Folder.SetAttribute, File.OpenAsTextStream and MoveFile of a folder.
+[TestFixture]
+Module AttributeTests
+
+    [TestCase]
+    Public Sub FileSetAttribute_NoAttributes_SetsHidden()
+        Expect.Begin
+        Dim w As String, f As File
+        w = Sandbox.Fresh("FileSetAttribute_NoAttributes_SetsHidden")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        Set f = GetFile(w & "\a.txt")
+        f.Attributes = faNormal
+        Expect.AreEqual 128, CLng(f.Attributes), "a file whose attributes were cleared reports faNormal"
+        f.SetAttribute faHidden, True
+        Expect.IsTrue f.HasAttribute(faHidden), "File.SetAttribute faHidden, True on a file with no attributes sets Hidden"
+        Expect.AreEqual 2, CLng(f.Attributes), "Attributes of that file after SetAttribute faHidden, True"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileSetAttribute_NoAttributes_SetsReadOnly()
+        Expect.Begin
+        Dim w As String, f As File
+        w = Sandbox.Fresh("FileSetAttribute_NoAttributes_SetsReadOnly")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        Set f = GetFile(w & "\a.txt")
+        f.Attributes = faNormal
+        f.SetAttribute faReadOnly, True
+        Expect.IsTrue f.HasAttribute(faReadOnly), "File.SetAttribute faReadOnly, True on a file with no attributes sets ReadOnly"
+        Expect.AreEqual 1, CLng(f.Attributes), "Attributes of that file after SetAttribute faReadOnly, True"
+        f.Attributes = faNormal
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileSetAttribute_ArchiveFile_SetsHidden()
+        Expect.Begin
+        Dim w As String, f As File
+        w = Sandbox.Fresh("FileSetAttribute_ArchiveFile_SetsHidden")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        Set f = GetFile(w & "\a.txt")
+        Expect.AreEqual 32, CLng(f.Attributes), "a new file has the archive attribute"
+        f.SetAttribute faHidden, True
+        Expect.AreEqual 34, CLng(f.Attributes), "Attributes of a new file after SetAttribute faHidden, True"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileAttributes_Assigned_SetsHidden()
+        Expect.Begin
+        Dim w As String, f As File
+        w = Sandbox.Fresh("FileAttributes_Assigned_SetsHidden")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        Set f = GetFile(w & "\a.txt")
+        f.Attributes = faNormal
+        f.Attributes = faHidden
+        Expect.AreEqual 2, CLng(f.Attributes), "Attributes assigned faHidden on a file with no attributes"
+        f.Attributes = faNormal
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSetAttribute_SetsHidden()
+        Expect.Begin
+        Dim w As String, d As Folder
+        w = Sandbox.Fresh("FolderSetAttribute_SetsHidden")
+        On Error GoTo Oops
+        CreateFolder w & "\d"
+        Set d = GetFolder(w & "\d")
+        d.SetAttribute faHidden, True
+        Expect.IsTrue d.HasAttribute(faHidden), "Folder.SetAttribute faHidden, True sets Hidden"
+        d.SetAttribute faHidden, False
+        Expect.IsFalse d.HasAttribute(faHidden), "Folder.SetAttribute faHidden, False clears Hidden"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub OpenAsTextStream_ForWriting_EmptiesFile()
+        Expect.Begin
+        Dim w As String, ts As TextStream
+        w = Sandbox.Fresh("OpenAsTextStream_ForWriting_EmptiesFile")
+        On Error GoTo Oops
+        StringToTextFile "keep this text", w & "\a.txt"
+        Set ts = GetFile(w & "\a.txt").OpenAsTextStream(ForWriting)
+        ts.Close
+        Expect.AreEqual 0, CLng(GetFile(w & "\a.txt").Size), "File.OpenAsTextStream(ForWriting) leaves a file of this many bytes"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub OpenAsTextStream_ForAppending_KeepsText()
+        Expect.Begin
+        Dim w As String, ts As TextStream
+        w = Sandbox.Fresh("OpenAsTextStream_ForAppending_KeepsText")
+        On Error GoTo Oops
+        StringToTextFile "old", w & "\a.txt"
+        Set ts = GetFile(w & "\a.txt").OpenAsTextStream(ForAppending)
+        ts.Write "new"
+        ts.Close
+        Expect.AreEqual "oldnew", TextFileToString(w & "\a.txt"), "text of a file after File.OpenAsTextStream(ForAppending) and Write"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub MoveFile_FolderSource_MovesFolder()
+        Expect.Begin
+        Dim w As String, result As String
+        w = Sandbox.Fresh("MoveFile_FolderSource_MovesFolder")
+        On Error GoTo Oops
+        CreateFolder w & "\old"
+        StringToTextFile "x", w & "\old\f.txt"
+        result = MoveFile(w & "\old", w & "\new")
+        Expect.AreEqual w & "\new", result, "MoveFile of a folder returns the path of the moved folder"
+        Expect.IsTrue FolderExists(w & "\new"), "the folder exists under its new name"
+        Expect.IsFalse FolderExists(w & "\old"), "the folder is gone from its old name"
+        Expect.IsTrue FileExists(w & "\new\f.txt"), "the folder's file moved with it"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/DetectionTests.twin b/test/package-tests/tbFileSysTools/Sources/DetectionTests.twin
new file mode 100644
index 000000000..0dd09fa9f
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/DetectionTests.twin
@@ -0,0 +1,117 @@
+' GetFileEncoding is an estimate: the cases where it is documented to give the wrong answer. Each
+' case checks the answer that the section states; the text of each file is in the encoding named
+' in the case, which GetFileEncoding does not report.
+[TestFixture]
+Module DetectionTests
+
+    [TestCase]
+    Public Sub GetFileEncoding_ShortUtf16WithoutZeroByte_IsSystemAnsi()
+        Expect.Begin
+        Dim w As String, s As String, c As Variant
+        w = Sandbox.Fresh("GetFileEncoding_ShortUtf16WithoutZeroByte_IsSystemAnsi")
+        On Error GoTo Oops
+        ' Eight kanji, 16 bytes with no zero byte and no control character, and not valid UTF-8.
+        For Each c In Array(&H65E5&, &H672C&, &H8A9E&, &H5165&, &H6587&, &H5B57&, &H4EBA&, &H8272&)
+            s = s & ChrW$(CLng(c))
+        Next
+        StringToTextFile s, w & "\a.txt", encUtf16
+        Expect.AreEqual CLng(encSystemAnsi), CLng(GetFileEncoding(w & "\a.txt")), "GetFileEncoding of eight Japanese characters in UTF-16 with no BOM and no zero byte"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileEncoding_ShortUtf16Hiragana_IsUtf8()
+        Expect.Begin
+        Dim w As String, s As String, i As Long
+        w = Sandbox.Fresh("GetFileEncoding_ShortUtf16Hiragana_IsUtf8")
+        On Error GoTo Oops
+        ' Eight hiragana: every byte is below 128, so the bytes are valid UTF-8.
+        For i = 0 To 7
+            s = s & ChrW$(&H3042& + i)
+        Next
+        StringToTextFile s, w & "\a.txt", encUtf16
+        Expect.AreEqual CLng(encUtf8), CLng(GetFileEncoding(w & "\a.txt")), "GetFileEncoding of eight hiragana in UTF-16 with no BOM"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileEncoding_Gb18030WithAccent_IsSystemAnsi()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFileEncoding_Gb18030WithAccent_IsSystemAnsi")
+        On Error GoTo Oops
+        StringToTextFile "caf" & ChrW$(233) & " au lait", w & "\a.txt", encGB18030
+        Expect.AreEqual CLng(encSystemAnsi), CLng(GetFileEncoding(w & "\a.txt")), "GetFileEncoding of GB18030 text with an accented letter and no BOM"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileEncoding_Utf7PureAscii_IsUtf8()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFileEncoding_Utf7PureAscii_IsUtf8")
+        On Error GoTo Oops
+        StringToTextFile "plain text", w & "\a.txt", encUtf7
+        Expect.AreEqual CLng(encUtf8), CLng(GetFileEncoding(w & "\a.txt")), "GetFileEncoding of UTF-7 text that is pure ASCII, with no signature"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileEncoding_Latin1AccentInLastByte_IsUtf8()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFileEncoding_Latin1AccentInLastByte_IsUtf8")
+        On Error GoTo Oops
+        StringToTextFile "caf" & ChrW$(233), w & "\a.txt", encLatin1
+        Expect.AreEqual CLng(encUtf8), CLng(GetFileEncoding(w & "\a.txt")), "GetFileEncoding of Latin-1 text whose only accented letter is its last byte"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileEncoding_Latin1AccentPastSample_IsUtf8()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFileEncoding_Latin1AccentPastSample_IsUtf8")
+        On Error GoTo Oops
+        StringToTextFile String$(40000, "a") & ChrW$(233) & "t" & ChrW$(233), w & "\a.txt", encLatin1
+        Expect.AreEqual CLng(encUtf8), CLng(GetFileEncoding(w & "\a.txt")), "GetFileEncoding of Latin-1 text whose accented letters come after the first 32,768 bytes"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/ErrorNumberTests.twin b/test/package-tests/tbFileSysTools/Sources/ErrorNumberTests.twin
new file mode 100644
index 000000000..30c7a16c0
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/ErrorNumberTests.twin
@@ -0,0 +1,445 @@
+' The error numbers that differ from the one the same failure raises elsewhere in the package:
+' a file held open with exclusive access (access refused is error 70) and a reserved device name
+' (an invalid name is error 52).
+[TestFixture]
+Module ErrorNumberTests
+
+    ' The error that the named procedure raises for a file that this program holds open with
+    ' exclusive access. -1 when the file could not be made.
+    Private Function ReadHeld(ByVal Folder As String, ByVal Which As String) As Long
+        Dim p As String, fn As Long, e As Long, s As String, b() As Byte, a() As String
+        p = Folder & "\held.txt"
+        StringToTextFile "some text", p
+        fn = FreeFile()
+        Open p For Binary Access Read Write Lock Read Write As #fn
+        On Error Resume Next
+        Select Case Which
+            Case "TextFileToString": s = TextFileToString(p)
+            Case "GetFileEncoding": s = CStr(GetFileEncoding(p))
+            Case "GetFileLineEnding": s = CStr(GetFileLineEnding(p))
+            Case "ReadStream": ReadStream p, b
+            Case "TextFileToArray": a = TextFileToArray(p)
+            Case "FileContainsText": s = CStr(FileContainsText(p, "text"))
+            Case "StringToTextFile": StringToTextFile "new", p
+            Case "OpenTextFile": s = CStr(OpenTextFile(p).AtEndOfStream)
+        End Select
+        e = Err.Number
+        Err.Clear
+        On Error GoTo 0
+        Close #fn
+        ReadHeld = e
+    End Function
+
+    [TestCase]
+    Public Sub TextFileToString_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("TextFileToString_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "TextFileToString"), "TextFileToString of a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileEncoding_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFileEncoding_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "GetFileEncoding"), "GetFileEncoding of a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFileLineEnding_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFileLineEnding_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "GetFileLineEnding"), "GetFileLineEnding of a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub ReadStream_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("ReadStream_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "ReadStream"), "ReadStream of a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub TextFileToArray_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("TextFileToArray_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "TextFileToArray"), "TextFileToArray of a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileContainsText_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("FileContainsText_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "FileContainsText"), "FileContainsText of a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub StringToTextFile_ExclusiveFile_Raises70()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("StringToTextFile_ExclusiveFile_Raises70")
+        On Error GoTo Oops
+        Expect.AreEqual 70, ReadHeld(w, "StringToTextFile"), "StringToTextFile to a file held open with exclusive access raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    ' ---- reserved device names
+
+    [TestCase]
+    Public Sub StringToTextFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("StringToTextFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            StringToTextFile "x", w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "StringToTextFile to the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub ArrayToTextFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long, lines() As String
+        w = Sandbox.Fresh("ArrayToTextFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        lines = Split("a,b", ",")
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            ArrayToTextFile lines, w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "ArrayToTextFile to the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateTextFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long, ts As TextStream
+        w = Sandbox.Fresh("CreateTextFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            Set ts = CreateTextFile(w & "\" & name)
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "CreateTextFile of the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub OpenTextFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long, ts As TextStream
+        w = Sandbox.Fresh("OpenTextFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            Set ts = OpenTextFile(w & "\" & name, ForWriting, True)
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "OpenTextFile(ForWriting, create) of the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderCreateTextFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long, ts As TextStream
+        w = Sandbox.Fresh("FolderCreateTextFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            Set ts = GetFolder(w).CreateTextFile(CStr(name))
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "Folder.CreateTextFile of the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CopyFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("CopyFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            CopyFile w & "\a.txt", w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "CopyFile to the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CreateFolder_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("CreateFolder_ReservedName_Raises52")
+        On Error GoTo Oops
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            CreateFolder w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "CreateFolder of the folder name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub MoveFile_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("MoveFile_ReservedName_Raises52")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            MoveFile w & "\a.txt", w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "MoveFile to the file name " & name & " raises"
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub MoveFolder_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("MoveFolder_ReservedName_Raises52")
+        On Error GoTo Oops
+        CreateFolder w & "\f"
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            MoveFolder w & "\f", w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "MoveFolder to the folder name " & name & " raises"
+        Next
+        Expect.IsTrue FolderExists(w & "\f"), "the source folder is still there"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    ' ---- the NUL device as the destination of a move: the file must not be lost
+
+    [TestCase]
+    Public Sub MoveFile_NulDevice_KeepsSourceFile()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("MoveFile_NulDevice_KeepsSourceFile")
+        On Error GoTo Oops
+        For Each name In Array("nul", "NUL", "nul.txt")
+            StringToTextFile "payload", w & "\a.txt"
+            On Error Resume Next
+            MoveFile w & "\a.txt", w & "\" & name
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "MoveFile to the file name " & name & " raises"
+            Expect.IsTrue FileExists(w & "\a.txt"), "the source file is still there after MoveFile to the file name " & name
+            DeleteFile w & "\a.txt", True, True
+        Next
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileMove_NulDevice_KeepsSourceFile()
+        Expect.Begin
+        Dim w As String, e As Long
+        w = Sandbox.Fresh("FileMove_NulDevice_KeepsSourceFile")
+        On Error GoTo Oops
+        StringToTextFile "payload", w & "\a.txt"
+        On Error Resume Next
+        GetFile(w & "\a.txt").Move w & "\nul"
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 52, e, "File.Move to the file name nul raises"
+        Expect.IsTrue FileExists(w & "\a.txt"), "the source file is still there after File.Move to the file name nul"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileSystemObjectMoveFile_NulDevice_KeepsSourceFile()
+        Expect.Begin
+        Dim w As String, e As Long, fso As New FileSystemObject
+        w = Sandbox.Fresh("FileSystemObjectMoveFile_NulDevice_KeepsSourceFile")
+        On Error GoTo Oops
+        StringToTextFile "payload", w & "\a.txt"
+        On Error Resume Next
+        fso.MoveFile w & "\a.txt", w & "\nul"
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 52, e, "FileSystemObject.MoveFile to the file name nul raises"
+        Expect.IsTrue FileExists(w & "\a.txt"), "the source file is still there after FileSystemObject.MoveFile to the file name nul"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub Rename_ReservedName_Raises52()
+        Expect.Begin
+        Dim w As String, name As Variant, e As Long
+        w = Sandbox.Fresh("Rename_ReservedName_Raises52")
+        On Error GoTo Oops
+        StringToTextFile "x", w & "\a.txt"
+        For Each name In Array("con", "nul")
+            On Error Resume Next
+            Rename w & "\a.txt", CStr(name)
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 52, e, "Rename of a file to the name " & name & " raises"
+        Next
+        Expect.IsTrue FileExists(w & "\a.txt"), "the file is still there under its name"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/JunctionTests.twin b/test/package-tests/tbFileSysTools/Sources/JunctionTests.twin
new file mode 100644
index 000000000..1c802f0b2
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/JunctionTests.twin
@@ -0,0 +1,142 @@
+' Directory junctions: CopyFolder, DeleteFolder, GetFilePaths, FindInTextFiles and Folder.Size do not
+' enter one. Every junction leads to a folder inside the sandbox, and is removed with RmDir.
+[TestFixture]
+Module JunctionTests
+
+    ' tree\file1.txt (500 bytes), tree\sub\file2.txt (1000 bytes, with the text "needle") and the
+    ' junction tree\link that leads to tree\sub. False, with a failed check, when the volume cannot
+    ' make a junction.
+    Private Function MakeTree(ByVal Folder As String) As Boolean
+        CreateFolder Folder & "\tree\sub", True
+        StringToTextFile String$(500, "x"), Folder & "\tree\file1.txt"
+        StringToTextFile "needle" & String$(994, "y"), Folder & "\tree\sub\file2.txt"
+        MakeTree = Sandbox.MakeJunction(Folder & "\tree\link", Folder & "\tree\sub")
+        If Not MakeTree Then Expect.Fail "the volume could not make the directory junction"
+    End Function
+
+    [TestCase]
+    Public Sub CopyFolder_Junction_LeftOutOfCopy()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("CopyFolder_Junction_LeftOutOfCopy")
+        On Error GoTo Oops
+        If MakeTree(w) Then
+            CopyFolder w & "\tree", w & "\copy"
+            Expect.IsFalse FolderExists(w & "\copy\link"), "CopyFolder copies no folder for the junction"
+            Expect.IsTrue FileExists(w & "\copy\sub\file2.txt"), "CopyFolder copies the folder the junction leads to under its own name"
+            Expect.IsTrue FileExists(w & "\copy\file1.txt"), "CopyFolder copies the other files"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.RemoveJunction w & "\tree\link"
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub DeleteFolder_Junction_KeepsTarget()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("DeleteFolder_Junction_KeepsTarget")
+        On Error GoTo Oops
+        CreateFolder w & "\target"
+        CreateFolder w & "\tree2"
+        StringToTextFile "keep me", w & "\target\keep.txt"
+        If Sandbox.MakeJunction(w & "\tree2\link", w & "\target") Then
+            DeleteFolder w & "\tree2", True
+            Expect.IsFalse FolderExists(w & "\tree2"), "DeleteFolder removes the folder that holds a junction"
+            Expect.IsTrue FolderExists(w & "\target"), "the folder the junction leads to is still there"
+            Expect.IsTrue FileExists(w & "\target\keep.txt"), "the file in the folder the junction leads to is still there"
+        Else
+            Expect.Fail "the volume could not make the directory junction"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.RemoveJunction w & "\tree2\link"
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFilePaths_Recursive_DoesNotEnterJunction()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFilePaths_Recursive_DoesNotEnterJunction")
+        On Error GoTo Oops
+        If MakeTree(w) Then
+            Expect.AreEqual 2, GetFilePaths(w & "\tree", , , , True).Count, "files that a recursive GetFilePaths lists in a tree with a junction"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.RemoveJunction w & "\tree\link"
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FindInTextFiles_Recursive_DoesNotEnterJunction()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("FindInTextFiles_Recursive_DoesNotEnterJunction")
+        On Error GoTo Oops
+        If MakeTree(w) Then
+            Expect.AreEqual 1, FindInTextFiles(w & "\tree", "needle", , True).Count, "files that a recursive FindInTextFiles finds in a tree with a junction"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.RemoveJunction w & "\tree\link"
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSize_Junction_CountsEachFileOnce()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("FolderSize_Junction_CountsEachFileOnce")
+        On Error GoTo Oops
+        If MakeTree(w) Then
+            Expect.AreEqual 1500, CLng(GetFolder(w & "\tree").Size), "Folder.Size of a tree of 1,500 bytes with a junction to one of its folders"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.RemoveJunction w & "\tree\link"
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSubFolders_Junction_StillListed()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("FolderSubFolders_Junction_StillListed")
+        On Error GoTo Oops
+        If MakeTree(w) Then
+            Expect.AreEqual 2, GetFolder(w & "\tree").SubFolders.Count, "folders in Folder.SubFolders of a tree with a junction"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.RemoveJunction w & "\tree\link"
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/LongPathTests.twin b/test/package-tests/tbFileSysTools/Sources/LongPathTests.twin
new file mode 100644
index 000000000..af6a19ad2
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/LongPathTests.twin
@@ -0,0 +1,395 @@
+' Paths of more than 260 characters: what works, what stops at the limit, and the folders of 258
+' characters or more whose Files, SubFolders and Size raise error 76.
+[TestFixture]
+Module LongPathTests
+
+    ' ---- Folder.Files, SubFolders and Size
+
+    [TestCase]
+    Public Sub FolderFiles_Path258_ListsFiles()
+        Expect.Begin
+        Dim w As String, deep As String, n As Long, e As Long
+        w = Sandbox.Fresh("FolderFiles_Path258_ListsFiles")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 258)
+        CreateFolder deep, True
+        StringToTextFile "hello", deep & "\f.txt"
+        On Error Resume Next
+        n = GetFolder(deep).Files.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Files of a folder whose path has 258 characters raises error"
+        Expect.AreEqual 1, n, "files in Folder.Files of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderFiles_Path270_ListsFiles()
+        Expect.Begin
+        Dim w As String, deep As String, n As Long, e As Long
+        w = Sandbox.Fresh("FolderFiles_Path270_ListsFiles")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 270)
+        CreateFolder deep, True
+        StringToTextFile "hello", deep & "\f.txt"
+        On Error Resume Next
+        n = GetFolder(deep).Files.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Files of a folder whose path has 270 characters raises error"
+        Expect.AreEqual 1, n, "files in Folder.Files of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSubFolders_Path258_ListsFolders()
+        Expect.Begin
+        Dim w As String, deep As String, n As Long, e As Long
+        w = Sandbox.Fresh("FolderSubFolders_Path258_ListsFolders")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 258)
+        CreateFolder deep & "\s", True
+        On Error Resume Next
+        n = GetFolder(deep).SubFolders.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.SubFolders of a folder whose path has 258 characters raises error"
+        Expect.AreEqual 1, n, "folders in Folder.SubFolders of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSize_Path258_AddsFiles()
+        Expect.Begin
+        Dim w As String, deep As String, n As LongLong, e As Long
+        w = Sandbox.Fresh("FolderSize_Path258_AddsFiles")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 258)
+        CreateFolder deep, True
+        StringToTextFile "hello", deep & "\f.txt"
+        On Error Resume Next
+        n = GetFolder(deep).Size
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Size of a folder whose path has 258 characters raises error"
+        Expect.AreEqual 5, CLng(n), "Folder.Size of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSize_SubfolderPath258_AddsFiles()
+        Expect.Begin
+        Dim w As String, deep As String, n As LongLong, e As Long
+        w = Sandbox.Fresh("FolderSize_SubfolderPath258_AddsFiles")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w & "\top", 258)
+        CreateFolder deep, True
+        StringToTextFile "hello", deep & "\f.txt"
+        On Error Resume Next
+        n = GetFolder(w & "\top").Size
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Size of a folder with a subfolder whose path has 258 characters raises error"
+        Expect.AreEqual 5, CLng(n), "Folder.Size of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    ' A folder of this path length with a file, and a subfolder when Sub is True.
+    Private Function MakeFolderOfLength(ByVal W As String, ByVal Length As Long, ByVal WithSubfolder As Boolean) As String
+        Dim deep As String
+        deep = Sandbox.PathOfLength(W, Length)
+        CreateFolder deep, True
+        StringToTextFile "hello", deep & "\f.txt"
+        If WithSubfolder Then CreateFolder deep & "\s"
+        MakeFolderOfLength = deep
+    End Function
+
+    [TestCase]
+    Public Sub FolderFilesAndSubFolders_Path256_Work()
+        Expect.Begin
+        Dim w As String, deep As String, n As Long, e As Long
+        w = Sandbox.Fresh("FolderFilesAndSubFolders_Path256_Work")
+        On Error GoTo Oops
+        deep = MakeFolderOfLength(w, 256, True)
+        On Error Resume Next
+        n = GetFolder(deep).Files.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Files of a folder whose path has 256 characters raises error"
+        Expect.AreEqual 1, n, "files in Folder.Files of that folder"
+        On Error Resume Next
+        n = GetFolder(deep).SubFolders.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.SubFolders of that folder raises error"
+        Expect.AreEqual 1, n, "folders in Folder.SubFolders of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSize_Path256_Works()
+        Expect.Begin
+        Dim w As String, deep As String, n As LongLong, e As Long
+        w = Sandbox.Fresh("FolderSize_Path256_Works")
+        On Error GoTo Oops
+        deep = MakeFolderOfLength(w, 256, False)
+        On Error Resume Next
+        n = GetFolder(deep).Size
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Size of a folder whose path has 256 characters raises error"
+        Expect.AreEqual 5, CLng(n), "Folder.Size of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderFilesAndSubFolders_Path257_Work()
+        Expect.Begin
+        Dim w As String, deep As String, n As Long, e As Long
+        w = Sandbox.Fresh("FolderFilesAndSubFolders_Path257_Work")
+        On Error GoTo Oops
+        deep = MakeFolderOfLength(w, 257, True)
+        On Error Resume Next
+        n = GetFolder(deep).Files.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Files of a folder whose path has 257 characters raises error"
+        Expect.AreEqual 1, n, "files in Folder.Files of that folder"
+        On Error Resume Next
+        n = GetFolder(deep).SubFolders.Count
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.SubFolders of that folder raises error"
+        Expect.AreEqual 1, n, "folders in Folder.SubFolders of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FolderSize_Path257_Works()
+        Expect.Begin
+        Dim w As String, deep As String, n As LongLong, e As Long
+        w = Sandbox.Fresh("FolderSize_Path257_Works")
+        On Error GoTo Oops
+        deep = MakeFolderOfLength(w, 257, False)
+        On Error Resume Next
+        n = GetFolder(deep).Size
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "Folder.Size of a folder whose path has 257 characters raises error"
+        Expect.AreEqual 5, CLng(n), "Folder.Size of that folder"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    ' ---- what works in a path of more than 260 characters
+
+    [TestCase]
+    Public Sub LongPath_Procedures_Work()
+        Expect.Begin
+        Dim w As String, deep As String, top As String, p As String, q As String, c As Collection
+        w = Sandbox.Fresh("LongPath_Procedures_Work")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 300)
+        CreateFolder deep, True
+        Expect.IsTrue FolderExists(deep), "FolderExists of a folder whose path has 300 characters"
+        p = deep & "\note.txt"
+        StringToTextFile "hello", p
+        Expect.IsTrue FileExists(p), "FileExists of a file in it"
+        Expect.AreEqual "hello", TextFileToString(p), "TextFileToString of that file"
+        Expect.AreEqual p, GetFile(p).Path, "File.Path of GetFile of that file"
+        Set c = GetFilePaths(deep)
+        Expect.AreEqual 1, c.Count, "files that GetFilePaths lists in it"
+        q = deep & "\copy.txt"
+        CopyFile p, q
+        Expect.IsTrue FileExists(q), "CopyFile to a path of more than 260 characters"
+        MoveFile q, deep & "\moved.txt"
+        Expect.IsTrue FileExists(deep & "\moved.txt"), "MoveFile to a path of more than 260 characters"
+        Rename deep & "\moved.txt", "renamed.txt"
+        Expect.IsTrue FileExists(deep & "\renamed.txt"), "Rename of a file with a path of more than 260 characters"
+        DeleteFile deep & "\renamed.txt"
+        Expect.IsFalse FileExists(deep & "\renamed.txt"), "DeleteFile of a file with a path of more than 260 characters"
+        top = w & "\" & String$(100, "d")
+        DeleteFolder top
+        Expect.IsFalse FolderExists(top), "DeleteFolder of a tree whose paths have more than 260 characters"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    ' ---- what stops at the limit
+
+    [TestCase]
+    Public Sub GetFileVersion_PathOver260_ReturnsEmpty()
+        Expect.Begin
+        Dim w As String, deep As String, kernel As String
+        w = Sandbox.Fresh("GetFileVersion_PathOver260_ReturnsEmpty")
+        On Error GoTo Oops
+        kernel = GetSpecialFolderPath(sfSystemFolder) & "\kernel32.dll"
+        CopyFile kernel, w & "\k.dll"
+        Expect.IsTrue Len(GetFileVersion(w & "\k.dll")) > 0, "GetFileVersion of a copy of kernel32.dll under a short path is not empty"
+        deep = Sandbox.PathOfLength(w, 300)
+        CreateFolder deep, True
+        CopyFile kernel, deep & "\k.dll"
+        Expect.IsTrue FileExists(deep & "\k.dll"), "the copy of kernel32.dll under a path of 300 characters exists"
+        Expect.AreEqual "", GetFileVersion(deep & "\k.dll"), "GetFileVersion of a file whose path has more than 260 characters"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub FileShortPath_PathOver260_ReturnsPath()
+        Expect.Begin
+        Dim w As String, deep As String, p As String
+        w = Sandbox.Fresh("FileShortPath_PathOver260_ReturnsPath")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 300)
+        CreateFolder deep, True
+        p = deep & "\note.txt"
+        StringToTextFile "hello", p
+        Expect.AreEqual p, GetFile(p).ShortPath, "File.ShortPath of a file whose path has more than 260 characters"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub SetCurrentDir_PathOver260_Raises75()
+        Expect.Begin
+        Dim w As String, deep As String, here As String, e As Long
+        here = GetCurrentDir()
+        w = Sandbox.Fresh("SetCurrentDir_PathOver260_Raises75")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 300)
+        CreateFolder deep, True
+        On Error Resume Next
+        SetCurrentDir deep
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 75, e, "SetCurrentDir of a folder whose path has 300 characters raises"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        On Error Resume Next
+        SetCurrentDir here
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub DeleteFile_WildcardInLongFolder_Raises53()
+        Expect.Begin
+        Dim w As String, deep As String, e As Long
+        w = Sandbox.Fresh("DeleteFile_WildcardInLongFolder_Raises53")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 300)
+        CreateFolder deep, True
+        StringToTextFile "x", deep & "\a.dll"
+        On Error Resume Next
+        DeleteFile deep & "\*.dll"
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 53, e, "DeleteFile of a pattern in a folder whose path has 300 characters raises"
+        Expect.IsTrue FileExists(deep & "\a.dll"), "the file is still there"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CopyFile_WildcardInLongFolder_Raises53()
+        Expect.Begin
+        Dim w As String, deep As String, e As Long
+        w = Sandbox.Fresh("CopyFile_WildcardInLongFolder_Raises53")
+        On Error GoTo Oops
+        deep = Sandbox.PathOfLength(w, 300)
+        CreateFolder deep, True
+        CreateFolder w & "\dst"
+        StringToTextFile "x", deep & "\a.dll"
+        On Error Resume Next
+        CopyFile deep & "\*.dll", w & "\dst"
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 53, e, "CopyFile of a pattern in a folder whose path has 300 characters raises"
+        Expect.IsFalse FileExists(w & "\dst\a.dll"), "no file was copied"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/PathTests.twin b/test/package-tests/tbFileSysTools/Sources/PathTests.twin
new file mode 100644
index 000000000..b1a4d79f9
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/PathTests.twin
@@ -0,0 +1,48 @@
+' GetRelativePath of a base path of 260 characters or more, and the paths just below it.
+[TestFixture]
+Module PathTests
+
+    ' GetRelativePath(base, base & "\x\y.txt") is the relative path "x\y.txt" whatever the length
+    ' of base is. Nothing is created: the paths name nothing that exists.
+    Private Function RelativeBelow(ByVal Length As Long) As String
+        Dim base As String
+        base = Sandbox.PathOfLength(Sandbox.Root() & "\GetRelativePath", Length)
+        RelativeBelow = GetRelativePath(base, base & "\x\y.txt")
+    End Function
+
+    [TestCase]
+    Public Sub GetRelativePath_Base258()
+        Expect.Begin
+        Expect.AreEqual "x\y.txt", RelativeBelow(258), "GetRelativePath of a target below a base of 258 characters"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetRelativePath_Base259()
+        Expect.Begin
+        Expect.AreEqual "x\y.txt", RelativeBelow(259), "GetRelativePath of a target below a base of 259 characters"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetRelativePath_Base260()
+        Expect.Begin
+        Expect.AreEqual "x\y.txt", RelativeBelow(260), "GetRelativePath of a target below a base of 260 characters"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetRelativePath_Base264()
+        Expect.Begin
+        Expect.AreEqual "x\y.txt", RelativeBelow(264), "GetRelativePath of a target below a base of 264 characters"
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetRelativePath_Base300()
+        Expect.Begin
+        Expect.AreEqual "x\y.txt", RelativeBelow(300), "GetRelativePath of a target below a base of 300 characters"
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/RunAll.twin b/test/package-tests/tbFileSysTools/Sources/RunAll.twin
new file mode 100644
index 000000000..51ce63690
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/RunAll.twin
@@ -0,0 +1,302 @@
+' Generated by scripts/package_tests.mjs from this project's [TestCase] Subs and expect.json:
+' do not edit. Run Sub Main: each case prints PASS, FAIL or SKIP in the DEBUG CONSOLE.
+' The cases are written for the package tbFileSysTools, version 1.9.0.0. Each one checks what is
+' correct, so a case that fails names a defect of the package.
+Module RunAll
+
+    Public Sub Main()
+        Expect.StartRun "tbFileSysTools 1.9.0.0: 72 test cases"
+        On Error Resume Next
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.FileSetAttribute_NoAttributes_SetsHidden
+        Expect.EndCase "AttributeTests.FileSetAttribute_NoAttributes_SetsHidden", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.FileSetAttribute_NoAttributes_SetsReadOnly
+        Expect.EndCase "AttributeTests.FileSetAttribute_NoAttributes_SetsReadOnly", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.FileSetAttribute_ArchiveFile_SetsHidden
+        Expect.EndCase "AttributeTests.FileSetAttribute_ArchiveFile_SetsHidden", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.FileAttributes_Assigned_SetsHidden
+        Expect.EndCase "AttributeTests.FileAttributes_Assigned_SetsHidden", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.FolderSetAttribute_SetsHidden
+        Expect.EndCase "AttributeTests.FolderSetAttribute_SetsHidden", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.OpenAsTextStream_ForWriting_EmptiesFile
+        Expect.EndCase "AttributeTests.OpenAsTextStream_ForWriting_EmptiesFile", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.OpenAsTextStream_ForAppending_KeepsText
+        Expect.EndCase "AttributeTests.OpenAsTextStream_ForAppending_KeepsText", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        AttributeTests.MoveFile_FolderSource_MovesFolder
+        Expect.EndCase "AttributeTests.MoveFile_FolderSource_MovesFolder", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        DetectionTests.GetFileEncoding_ShortUtf16WithoutZeroByte_IsSystemAnsi
+        Expect.EndCase "DetectionTests.GetFileEncoding_ShortUtf16WithoutZeroByte_IsSystemAnsi", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        DetectionTests.GetFileEncoding_ShortUtf16Hiragana_IsUtf8
+        Expect.EndCase "DetectionTests.GetFileEncoding_ShortUtf16Hiragana_IsUtf8", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        DetectionTests.GetFileEncoding_Gb18030WithAccent_IsSystemAnsi
+        Expect.EndCase "DetectionTests.GetFileEncoding_Gb18030WithAccent_IsSystemAnsi", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        DetectionTests.GetFileEncoding_Utf7PureAscii_IsUtf8
+        Expect.EndCase "DetectionTests.GetFileEncoding_Utf7PureAscii_IsUtf8", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        DetectionTests.GetFileEncoding_Latin1AccentInLastByte_IsUtf8
+        Expect.EndCase "DetectionTests.GetFileEncoding_Latin1AccentInLastByte_IsUtf8", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        DetectionTests.GetFileEncoding_Latin1AccentPastSample_IsUtf8
+        Expect.EndCase "DetectionTests.GetFileEncoding_Latin1AccentPastSample_IsUtf8", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.TextFileToString_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.TextFileToString_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.GetFileEncoding_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.GetFileEncoding_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.GetFileLineEnding_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.GetFileLineEnding_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.ReadStream_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.ReadStream_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.TextFileToArray_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.TextFileToArray_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.FileContainsText_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.FileContainsText_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.StringToTextFile_ExclusiveFile_Raises70
+        Expect.EndCase "ErrorNumberTests.StringToTextFile_ExclusiveFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.StringToTextFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.StringToTextFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.ArrayToTextFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.ArrayToTextFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.CreateTextFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.CreateTextFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.OpenTextFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.OpenTextFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.FolderCreateTextFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.FolderCreateTextFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.CopyFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.CopyFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.CreateFolder_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.CreateFolder_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.MoveFile_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.MoveFile_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.MoveFolder_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.MoveFolder_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.MoveFile_NulDevice_KeepsSourceFile
+        Expect.EndCase "ErrorNumberTests.MoveFile_NulDevice_KeepsSourceFile", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.FileMove_NulDevice_KeepsSourceFile
+        Expect.EndCase "ErrorNumberTests.FileMove_NulDevice_KeepsSourceFile", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.FileSystemObjectMoveFile_NulDevice_KeepsSourceFile
+        Expect.EndCase "ErrorNumberTests.FileSystemObjectMoveFile_NulDevice_KeepsSourceFile", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ErrorNumberTests.Rename_ReservedName_Raises52
+        Expect.EndCase "ErrorNumberTests.Rename_ReservedName_Raises52", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        JunctionTests.CopyFolder_Junction_LeftOutOfCopy
+        Expect.EndCase "JunctionTests.CopyFolder_Junction_LeftOutOfCopy", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        JunctionTests.DeleteFolder_Junction_KeepsTarget
+        Expect.EndCase "JunctionTests.DeleteFolder_Junction_KeepsTarget", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        JunctionTests.GetFilePaths_Recursive_DoesNotEnterJunction
+        Expect.EndCase "JunctionTests.GetFilePaths_Recursive_DoesNotEnterJunction", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        JunctionTests.FindInTextFiles_Recursive_DoesNotEnterJunction
+        Expect.EndCase "JunctionTests.FindInTextFiles_Recursive_DoesNotEnterJunction", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        JunctionTests.FolderSize_Junction_CountsEachFileOnce
+        Expect.EndCase "JunctionTests.FolderSize_Junction_CountsEachFileOnce", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        JunctionTests.FolderSubFolders_Junction_StillListed
+        Expect.EndCase "JunctionTests.FolderSubFolders_Junction_StillListed", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderFiles_Path258_ListsFiles
+        Expect.EndCase "LongPathTests.FolderFiles_Path258_ListsFiles", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderFiles_Path270_ListsFiles
+        Expect.EndCase "LongPathTests.FolderFiles_Path270_ListsFiles", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderSubFolders_Path258_ListsFolders
+        Expect.EndCase "LongPathTests.FolderSubFolders_Path258_ListsFolders", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderSize_Path258_AddsFiles
+        Expect.EndCase "LongPathTests.FolderSize_Path258_AddsFiles", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderSize_SubfolderPath258_AddsFiles
+        Expect.EndCase "LongPathTests.FolderSize_SubfolderPath258_AddsFiles", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderFilesAndSubFolders_Path256_Work
+        Expect.EndCase "LongPathTests.FolderFilesAndSubFolders_Path256_Work", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderSize_Path256_Works
+        Expect.EndCase "LongPathTests.FolderSize_Path256_Works", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderFilesAndSubFolders_Path257_Work
+        Expect.EndCase "LongPathTests.FolderFilesAndSubFolders_Path257_Work", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FolderSize_Path257_Works
+        Expect.EndCase "LongPathTests.FolderSize_Path257_Works", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.LongPath_Procedures_Work
+        Expect.EndCase "LongPathTests.LongPath_Procedures_Work", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.GetFileVersion_PathOver260_ReturnsEmpty
+        Expect.EndCase "LongPathTests.GetFileVersion_PathOver260_ReturnsEmpty", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.FileShortPath_PathOver260_ReturnsPath
+        Expect.EndCase "LongPathTests.FileShortPath_PathOver260_ReturnsPath", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.SetCurrentDir_PathOver260_Raises75
+        Expect.EndCase "LongPathTests.SetCurrentDir_PathOver260_Raises75", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.DeleteFile_WildcardInLongFolder_Raises53
+        Expect.EndCase "LongPathTests.DeleteFile_WildcardInLongFolder_Raises53", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        LongPathTests.CopyFile_WildcardInLongFolder_Raises53
+        Expect.EndCase "LongPathTests.CopyFile_WildcardInLongFolder_Raises53", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PathTests.GetRelativePath_Base258
+        Expect.EndCase "PathTests.GetRelativePath_Base258", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PathTests.GetRelativePath_Base259
+        Expect.EndCase "PathTests.GetRelativePath_Base259", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PathTests.GetRelativePath_Base260
+        Expect.EndCase "PathTests.GetRelativePath_Base260", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PathTests.GetRelativePath_Base264
+        Expect.EndCase "PathTests.GetRelativePath_Base264", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        PathTests.GetRelativePath_Base300
+        Expect.EndCase "PathTests.GetRelativePath_Base300", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.DeleteFile_Pattern_ActsOnShortNameMatch
+        Expect.EndCase "ShortNameTests.DeleteFile_Pattern_ActsOnShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.CopyFile_Pattern_ActsOnShortNameMatch
+        Expect.EndCase "ShortNameTests.CopyFile_Pattern_ActsOnShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.MoveFile_Pattern_ActsOnShortNameMatch
+        Expect.EndCase "ShortNameTests.MoveFile_Pattern_ActsOnShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.CopyFolder_Pattern_ActsOnShortNameMatch
+        Expect.EndCase "ShortNameTests.CopyFolder_Pattern_ActsOnShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.MoveFolder_Pattern_ActsOnShortNameMatch
+        Expect.EndCase "ShortNameTests.MoveFolder_Pattern_ActsOnShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.DeleteFolder_Pattern_ActsOnShortNameMatch
+        Expect.EndCase "ShortNameTests.DeleteFolder_Pattern_ActsOnShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        ShortNameTests.GetFilePaths_Pattern_LeavesOutShortNameMatch
+        Expect.EndCase "ShortNameTests.GetFilePaths_Pattern_LeavesOutShortNameMatch", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        TextStreamTests.OpenTextFile_ReadOnlyFileForWriting_Raises70
+        Expect.EndCase "TextStreamTests.OpenTextFile_ReadOnlyFileForWriting_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        TextStreamTests.OpenTextFile_Utf7ReadOnlyFileForWriting_Opens
+        Expect.EndCase "TextStreamTests.OpenTextFile_Utf7ReadOnlyFileForWriting_Opens", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        TextStreamTests.TextStreamClose_Utf7ReadOnlyFile_Raises70
+        Expect.EndCase "TextStreamTests.TextStreamClose_Utf7ReadOnlyFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        TextStreamTests.TextStreamFlush_Utf7ReadOnlyFile_Raises70
+        Expect.EndCase "TextStreamTests.TextStreamFlush_Utf7ReadOnlyFile_Raises70", Err.Number, Err.Description
+        Expect.StartCase
+        Err.Clear
+        TextStreamTests.OpenTextFile_Utf7ForWriting_KeepsTextUntilClose
+        Expect.EndCase "TextStreamTests.OpenTextFile_Utf7ForWriting_KeepsTextUntilClose", Err.Number, Err.Description
+        On Error GoTo 0
+        Expect.EndRun
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/Sandbox.twin b/test/package-tests/tbFileSysTools/Sources/Sandbox.twin
new file mode 100644
index 000000000..764fe34e0
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/Sandbox.twin
@@ -0,0 +1,87 @@
+' Sandbox: the folders the test cases of tbFileSysTools work in, and a few helpers they share.
+'
+' Every file, folder and junction a case makes is under %TEMP%\tbdocs-package-tests\tbFileSysTools\.
+' A case asks for a fresh folder of its own (Fresh), which empties it first in case an earlier run
+' left something, and removes it at its end (Remove). Remove goes through cmd's rmdir, which
+' removes a junction itself and never enters it, and takes a path of more than 260 characters in
+' its \\?\ form. Nothing is linked to a folder outside the sandbox.
+Module Sandbox
+
+    Public Function Root() As String
+        Root = Environ$("TEMP") & "\tbdocs-package-tests\tbFileSysTools"
+    End Function
+
+    ' A new, empty folder for one case.
+    Public Function Fresh(ByVal Tag As String) As String
+        Dim p As String
+        p = Root() & "\" & Tag
+        Remove p
+        CreateFolder p, True
+        Fresh = p
+    End Function
+
+    ' Removes a folder with everything in it, junctions included (not what they lead to), and then
+    ' the sandbox itself when nothing else is in it. Raises no error.
+    Public Sub Remove(ByVal Path As String)
+        On Error Resume Next
+        Shell_ "rmdir /s /q ""\\?\" & Path & """"
+        If FolderExists(Path) Then DeleteFolder Path, True, True
+        RmDir Root()
+        RmDir Environ$("TEMP") & "\tbdocs-package-tests"
+        Err.Clear
+    End Sub
+
+    ' Makes the directory junction LinkPath that leads to Target. False when the volume cannot.
+    Public Function MakeJunction(ByVal LinkPath As String, ByVal Target As String) As Boolean
+        On Error Resume Next
+        Shell_ "mklink /J """ & LinkPath & """ """ & Target & """"
+        MakeJunction = GetFolder(LinkPath).HasAttribute(faReparsePoint)
+        If Err.Number <> 0 Then MakeJunction = False
+        Err.Clear
+    End Function
+
+    ' Removes a junction and nothing it leads to.
+    Public Sub RemoveJunction(ByVal LinkPath As String)
+        On Error Resume Next
+        RmDir LinkPath
+        Err.Clear
+    End Sub
+
+    ' The number of the error that the last statement raised under On Error Resume Next, after
+    ' clearing it.
+    Public Function Caught() As Long
+        Caught = Err.Number
+        Err.Clear
+    End Function
+
+    ' A path that starts with Base and is exactly Length characters long, by adding folder names of
+    ' 100 characters or fewer. Nothing is created.
+    Public Function PathOfLength(ByVal Base As String, ByVal Length As Long) As String
+        Dim p As String, remaining As Long, k As Long
+        p = Base
+        Do While Len(p) < Length
+            remaining = Length - Len(p)
+            If remaining <= 101 Then
+                k = remaining - 1
+            ElseIf remaining - 101 = 1 Then
+                k = 99
+            Else
+                k = 100
+            End If
+            p = p & "\" & String$(k, "d")
+        Loop
+        PathOfLength = p
+    End Function
+
+    ' The 8.3 short name of the file or folder, or its name when Windows made none.
+    Public Function ShortNameOf(ByVal Path As String, ByVal IsFolder As Boolean) As String
+        On Error Resume Next
+        If IsFolder Then ShortNameOf = GetFolder(Path).ShortName Else ShortNameOf = GetFile(Path).ShortName
+        Err.Clear
+    End Function
+
+    Private Sub Shell_(ByVal CommandLine As String)
+        CreateObject("WScript.Shell").Run "cmd /c " & CommandLine & " >nul 2>&1", 0, True
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/ShortNameTests.twin b/test/package-tests/tbFileSysTools/Sources/ShortNameTests.twin
new file mode 100644
index 000000000..6d78dbf49
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/ShortNameTests.twin
@@ -0,0 +1,168 @@
+' A pattern is also matched against the 8.3 short name: a.txtx has a short name that ends in .TXT,
+' so *.txt acts on it. GetFilePaths tests the name again and does not list it.
+[TestFixture]
+Module ShortNameTests
+
+    ' A file whose short name differs from its name. False, with a failed check, when the volume
+    ' creates no 8.3 names, so that a case cannot pass without having tested anything.
+    Private Function MakeShortNamedFile(ByVal Folder As String, ByVal Name As String) As Boolean
+        StringToTextFile "x", Folder & "\" & Name
+        MakeShortNamedFile = (Sandbox.ShortNameOf(Folder & "\" & Name, False) <> Name)
+        If Not MakeShortNamedFile Then Expect.Fail "the volume creates no 8.3 short name for the file " & Name
+    End Function
+
+    ' The same for a folder.
+    Private Function MakeShortNamedFolder(ByVal Folder As String, ByVal Name As String) As Boolean
+        CreateFolder Folder & "\" & Name
+        MakeShortNamedFolder = (Sandbox.ShortNameOf(Folder & "\" & Name, True) <> Name)
+        If Not MakeShortNamedFolder Then Expect.Fail "the volume creates no 8.3 short name for the folder " & Name
+    End Function
+
+    [TestCase]
+    Public Sub DeleteFile_Pattern_ActsOnShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("DeleteFile_Pattern_ActsOnShortNameMatch")
+        On Error GoTo Oops
+        If MakeShortNamedFile(w, "a.txtx") Then
+            StringToTextFile "x", w & "\keep.log"
+            DeleteFile w & "\*.txt"
+            Expect.IsFalse FileExists(w & "\a.txtx"), "DeleteFile of *.txt deletes a.txtx, whose short name ends in .TXT"
+            Expect.IsTrue FileExists(w & "\keep.log"), "DeleteFile of *.txt leaves keep.log"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CopyFile_Pattern_ActsOnShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("CopyFile_Pattern_ActsOnShortNameMatch")
+        On Error GoTo Oops
+        CreateFolder w & "\dst"
+        If MakeShortNamedFile(w, "a.txtx") Then
+            CopyFile w & "\*.txt", w & "\dst"
+            Expect.IsTrue FileExists(w & "\dst\a.txtx"), "CopyFile of *.txt copies a.txtx, whose short name ends in .TXT"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub MoveFile_Pattern_ActsOnShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("MoveFile_Pattern_ActsOnShortNameMatch")
+        On Error GoTo Oops
+        CreateFolder w & "\dst"
+        If MakeShortNamedFile(w, "a.txtx") Then
+            MoveFile w & "\*.txt", w & "\dst"
+            Expect.IsTrue FileExists(w & "\dst\a.txtx"), "MoveFile of *.txt moves a.txtx, whose short name ends in .TXT"
+            Expect.IsFalse FileExists(w & "\a.txtx"), "a.txtx is gone from its folder"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub CopyFolder_Pattern_ActsOnShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("CopyFolder_Pattern_ActsOnShortNameMatch")
+        On Error GoTo Oops
+        CreateFolder w & "\src"
+        CreateFolder w & "\dst"
+        If MakeShortNamedFolder(w & "\src", "d.txtx") Then
+            CopyFolder w & "\src\*.txt", w & "\dst"
+            Expect.IsTrue FolderExists(w & "\dst\d.txtx"), "CopyFolder of *.txt copies the folder d.txtx, whose short name ends in .TXT"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub MoveFolder_Pattern_ActsOnShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("MoveFolder_Pattern_ActsOnShortNameMatch")
+        On Error GoTo Oops
+        CreateFolder w & "\src"
+        CreateFolder w & "\dst"
+        If MakeShortNamedFolder(w & "\src", "d.txtx") Then
+            MoveFolder w & "\src\*.txt", w & "\dst"
+            Expect.IsTrue FolderExists(w & "\dst\d.txtx"), "MoveFolder of *.txt moves the folder d.txtx, whose short name ends in .TXT"
+            Expect.IsFalse FolderExists(w & "\src\d.txtx"), "d.txtx is gone from its folder"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub DeleteFolder_Pattern_ActsOnShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("DeleteFolder_Pattern_ActsOnShortNameMatch")
+        On Error GoTo Oops
+        CreateFolder w & "\src"
+        If MakeShortNamedFolder(w & "\src", "d.txtx") Then
+            CreateFolder w & "\src\keep"
+            DeleteFolder w & "\src\*.txt"
+            Expect.IsFalse FolderExists(w & "\src\d.txtx"), "DeleteFolder of *.txt deletes the folder d.txtx, whose short name ends in .TXT"
+            Expect.IsTrue FolderExists(w & "\src\keep"), "DeleteFolder of *.txt leaves the folder keep"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub GetFilePaths_Pattern_LeavesOutShortNameMatch()
+        Expect.Begin
+        Dim w As String
+        w = Sandbox.Fresh("GetFilePaths_Pattern_LeavesOutShortNameMatch")
+        On Error GoTo Oops
+        If MakeShortNamedFile(w, "a.txtx") Then
+            StringToTextFile "x", w & "\b.txt"
+            Expect.AreEqual 1, GetFilePaths(w, "*.txt").Count, "files that GetFilePaths lists for *.txt when a.txtx and b.txt exist"
+            Expect.AreEqual w & "\b.txt", GetFilePaths(w, "*.txt")(1), "the file that GetFilePaths lists for *.txt"
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/Sources/TextStreamTests.twin b/test/package-tests/tbFileSysTools/Sources/TextStreamTests.twin
new file mode 100644
index 000000000..e0ad1bf53
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/Sources/TextStreamTests.twin
@@ -0,0 +1,149 @@
+' A stream open for writing on a read-only file: the error comes when the stream is opened, except
+' for UTF-7, which holds the whole file in memory and replaces it when it is closed or flushed.
+[TestFixture]
+Module TextStreamTests
+
+    [TestCase]
+    Public Sub OpenTextFile_ReadOnlyFileForWriting_Raises70()
+        Expect.Begin
+        Dim w As String, p As String, ts As TextStream, e As Long, enc As Variant
+        w = Sandbox.Fresh("OpenTextFile_ReadOnlyFileForWriting_Raises70")
+        On Error GoTo Oops
+        p = w & "\ro.txt"
+        StringToTextFile "keep", p
+        SetAttr p, vbReadOnly
+        For Each enc In Array(encUtf8, encUtf16Bom, encLatin1, encSystemAnsi)
+            On Error Resume Next
+            Set ts = OpenTextFile(p, ForWriting, , CLng(enc))
+            e = Sandbox.Caught()
+            On Error GoTo Oops
+            Expect.AreEqual 70, e, "OpenTextFile(ForWriting) of a read-only file raises, for the encoding " & enc
+        Next
+        Expect.AreEqual "keep", TextFileToString(p), "text of the read-only file after the failed opens"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        On Error Resume Next
+        SetAttr p, vbNormal
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub OpenTextFile_Utf7ReadOnlyFileForWriting_Opens()
+        Expect.Begin
+        Dim w As String, p As String, ts As TextStream, e As Long
+        w = Sandbox.Fresh("OpenTextFile_Utf7ReadOnlyFileForWriting_Opens")
+        On Error GoTo Oops
+        p = w & "\ro.txt"
+        StringToTextFile "keep", p
+        SetAttr p, vbReadOnly
+        On Error Resume Next
+        Set ts = OpenTextFile(p, ForWriting, , encUtf7)
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 0, e, "OpenTextFile(ForWriting, UTF-7) of a read-only file raises"
+        Expect.AreEqual "keep", TextFileToString(p), "text of the read-only file while the UTF-7 stream is open"
+        If Not ts Is Nothing Then
+            Expect.IsFalse ts.IsStreaming, "IsStreaming of the UTF-7 stream"
+            On Error Resume Next
+            ts.Close
+            Err.Clear
+            On Error GoTo Oops
+        End If
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        On Error Resume Next
+        SetAttr p, vbNormal
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub TextStreamClose_Utf7ReadOnlyFile_Raises70()
+        Expect.Begin
+        Dim w As String, p As String, ts As TextStream, e As Long
+        w = Sandbox.Fresh("TextStreamClose_Utf7ReadOnlyFile_Raises70")
+        On Error GoTo Oops
+        p = w & "\ro.txt"
+        StringToTextFile "keep", p
+        SetAttr p, vbReadOnly
+        Set ts = OpenTextFile(p, ForWriting, , encUtf7)
+        ts.Write "new"
+        On Error Resume Next
+        ts.Close
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 70, e, "Close of a UTF-7 stream open for writing on a read-only file raises"
+        Expect.AreEqual "keep", TextFileToString(p), "text of the read-only file after Close"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        On Error Resume Next
+        SetAttr p, vbNormal
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub TextStreamFlush_Utf7ReadOnlyFile_Raises70()
+        Expect.Begin
+        Dim w As String, p As String, ts As TextStream, e As Long
+        w = Sandbox.Fresh("TextStreamFlush_Utf7ReadOnlyFile_Raises70")
+        On Error GoTo Oops
+        p = w & "\ro.txt"
+        StringToTextFile "keep", p
+        SetAttr p, vbReadOnly
+        Set ts = OpenTextFile(p, ForWriting, , encUtf7)
+        ts.Write "new"
+        On Error Resume Next
+        ts.Flush
+        e = Sandbox.Caught()
+        On Error GoTo Oops
+        Expect.AreEqual 70, e, "Flush of a UTF-7 stream open for writing on a read-only file raises"
+        Expect.AreEqual "keep", TextFileToString(p), "text of the read-only file after Flush"
+        On Error Resume Next
+        ts.Close
+        Err.Clear
+        On Error GoTo Oops
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        On Error Resume Next
+        SetAttr p, vbNormal
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+    [TestCase]
+    Public Sub OpenTextFile_Utf7ForWriting_KeepsTextUntilClose()
+        Expect.Begin
+        Dim w As String, p As String, ts As TextStream
+        w = Sandbox.Fresh("OpenTextFile_Utf7ForWriting_KeepsTextUntilClose")
+        On Error GoTo Oops
+        p = w & "\a.txt"
+        StringToTextFile "keep", p
+        Set ts = OpenTextFile(p, ForWriting, , encUtf7)
+        Expect.AreEqual "keep", TextFileToString(p), "text of the file while a UTF-7 stream open for writing is open and nothing is written"
+        ts.Write "new"
+        ts.Close
+        Expect.AreEqual "new", TextFileToString(p, encUtf7), "text of the file after the UTF-7 stream is closed"
+        GoTo Finish
+Oops:
+        Expect.Fail "unexpected run-time error " & Err.Number & ": " & Err.Description
+        Resume Finish
+Finish:
+        Sandbox.Remove w
+        Expect.Done
+    End Sub
+
+End Module
diff --git a/test/package-tests/tbFileSysTools/expect.json b/test/package-tests/tbFileSysTools/expect.json
new file mode 100644
index 000000000..ddbd428ab
--- /dev/null
+++ b/test/package-tests/tbFileSysTools/expect.json
@@ -0,0 +1,77 @@
+{
+  "version": "1.9.0.0",
+  "cases": {
+    "AttributeTests.FileSetAttribute_NoAttributes_SetsHidden": {
+      "fails": "2 of 3 checks failed: File.SetAttribute faHidden, True on a file with no attributes sets Hidden: expected True, got False; Attributes of that file after SetAttribute faHidden, True: expected 2, got 128"
+    },
+    "AttributeTests.FileSetAttribute_NoAttributes_SetsReadOnly": {
+      "fails": "2 of 2 checks failed: File.SetAttribute faReadOnly, True on a file with no attributes sets ReadOnly: expected True, got False; Attributes of that file after SetAttribute faReadOnly, True: expected 1, got 128"
+    },
+    "ErrorNumberTests.TextFileToString_ExclusiveFile_Raises70": {
+      "fails": "1 of 1 checks failed: TextFileToString of a file held open with exclusive access raises: expected 70, got -2147467259"
+    },
+    "ErrorNumberTests.GetFileEncoding_ExclusiveFile_Raises70": {
+      "fails": "1 of 1 checks failed: GetFileEncoding of a file held open with exclusive access raises: expected 70, got -2147467259"
+    },
+    "ErrorNumberTests.GetFileLineEnding_ExclusiveFile_Raises70": {
+      "fails": "1 of 1 checks failed: GetFileLineEnding of a file held open with exclusive access raises: expected 70, got -2147467259"
+    },
+    "ErrorNumberTests.ReadStream_ExclusiveFile_Raises70": {
+      "fails": "1 of 1 checks failed: ReadStream of a file held open with exclusive access raises: expected 70, got -2147467259"
+    },
+    "ErrorNumberTests.TextFileToArray_ExclusiveFile_Raises70": {
+      "fails": "1 of 1 checks failed: TextFileToArray of a file held open with exclusive access raises: expected 70, got -2147467259"
+    },
+    "ErrorNumberTests.FileContainsText_ExclusiveFile_Raises70": {
+      "fails": "1 of 1 checks failed: FileContainsText of a file held open with exclusive access raises: expected 70, got -2147467259"
+    },
+    "ErrorNumberTests.CreateFolder_ReservedName_Raises52": {
+      "fails": "2 of 2 checks failed: CreateFolder of the folder name con raises: expected 52, got 75; CreateFolder of the folder name nul raises: expected 52, got 76"
+    },
+    "ErrorNumberTests.MoveFile_ReservedName_Raises52": {
+      "fails": "2 of 2 checks failed: MoveFile to the file name con raises: expected 52, got 75; MoveFile to the file name nul raises: expected 52, got 0"
+    },
+    "ErrorNumberTests.MoveFolder_ReservedName_Raises52": {
+      "fails": "2 of 3 checks failed: MoveFolder to the folder name con raises: expected 52, got 75; MoveFolder to the folder name nul raises: expected 52, got 70"
+    },
+    "ErrorNumberTests.MoveFile_NulDevice_KeepsSourceFile": {
+      "fails": "6 of 6 checks failed: MoveFile to the file name nul raises: expected 52, got 0; the source file is still there after MoveFile to the file name nul: expected True, got False; MoveFile to the file name NUL raises: expected 52, got 0; the source file is still there after MoveFile to the file name NUL: expected True, got False; MoveFile to the file name nul.txt raises: expected 52, got 0; the source file is still there after MoveFile to the file name nul.txt: expected True, got False"
+    },
+    "ErrorNumberTests.FileMove_NulDevice_KeepsSourceFile": {
+      "fails": "2 of 2 checks failed: File.Move to the file name nul raises: expected 52, got 53; the source file is still there after File.Move to the file name nul: expected True, got False"
+    },
+    "ErrorNumberTests.FileSystemObjectMoveFile_NulDevice_KeepsSourceFile": {
+      "fails": "2 of 2 checks failed: FileSystemObject.MoveFile to the file name nul raises: expected 52, got 0; the source file is still there after FileSystemObject.MoveFile to the file name nul: expected True, got False"
+    },
+    "ErrorNumberTests.Rename_ReservedName_Raises52": {
+      "fails": "2 of 3 checks failed: Rename of a file to the name con raises: expected 52, got 5; Rename of a file to the name nul raises: expected 52, got 5"
+    },
+    "LongPathTests.FolderFiles_Path258_ListsFiles": {
+      "fails": "2 of 2 checks failed: Folder.Files of a folder whose path has 258 characters raises error: expected 0, got 76; files in Folder.Files of that folder: expected 1, got 0"
+    },
+    "LongPathTests.FolderFiles_Path270_ListsFiles": {
+      "fails": "2 of 2 checks failed: Folder.Files of a folder whose path has 270 characters raises error: expected 0, got 76; files in Folder.Files of that folder: expected 1, got 0"
+    },
+    "LongPathTests.FolderSubFolders_Path258_ListsFolders": {
+      "fails": "2 of 2 checks failed: Folder.SubFolders of a folder whose path has 258 characters raises error: expected 0, got 76; folders in Folder.SubFolders of that folder: expected 1, got 0"
+    },
+    "LongPathTests.FolderSize_Path258_AddsFiles": {
+      "fails": "2 of 2 checks failed: Folder.Size of a folder whose path has 258 characters raises error: expected 0, got 76; Folder.Size of that folder: expected 5, got 0"
+    },
+    "LongPathTests.FolderSize_SubfolderPath258_AddsFiles": {
+      "fails": "2 of 2 checks failed: Folder.Size of a folder with a subfolder whose path has 258 characters raises error: expected 0, got 76; Folder.Size of that folder: expected 5, got 0"
+    },
+    "PathTests.GetRelativePath_Base259": {
+      "fails": "1 of 1 checks failed: GetRelativePath of a target below a base of 259 characters: expected \"x\\y.txt\", got \"..\\dddddddddddddddddddddddddddddddddddddddddddddddddddddddd\\x\\y.txt\""
+    },
+    "PathTests.GetRelativePath_Base260": {
+      "fails": "1 of 1 checks failed: GetRelativePath of a target below a base of 260 characters: expected \"x\\y.txt\", got \"..\\ddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\\x\\y.txt\""
+    },
+    "PathTests.GetRelativePath_Base264": {
+      "fails": "1 of 1 checks failed: GetRelativePath of a target below a base of 264 characters: expected \"x\\y.txt\", got \"..\\ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\\x\\y.txt\""
+    },
+    "PathTests.GetRelativePath_Base300": {
+      "fails": "1 of 1 checks failed: GetRelativePath of a target below a base of 300 characters: expected \"x\\y.txt\", got \"..\\ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\\x\\y.txt\""
+    }
+  }
+}

From 27f35418bc40ec959d8728a25e3c7252b6f2651c Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Wed, 14 Oct 2026 12:53:18 +0200
Subject: [PATCH 10/13] Callout labels: a callout names the build or package
 version it applies to after [!TYPE] (> [!WARNING] BETA 1005, 1.0.0.4, since
 BETA 984), drawn as tags; the build refuses any other label and a version
 other than the section's indexed_from; analyze lists the callouts a new build
 or version leaves to check again; 215 callouts moved to labels

---
 WIP.Build.md                                  |   9 ++
 WIP.IDE.md                                    |   3 +-
 WIP.md                                        |  13 +-
 builder/render.mjs                            |  42 +++++-
 builder/tbdocs.mjs                            |   4 +
 docs/Documentation/Authoring.md               |  18 ++-
 docs/Documentation/Tools-Build.md             |   5 +-
 docs/Features/Advanced/API-Declarations.md    |   8 +-
 docs/Features/Advanced/Classes-and-Modules.md |   4 +-
 docs/IDE/Code/Diagnostics.md                  |   4 +-
 docs/IDE/Code/Editor.md                       |  20 +--
 docs/IDE/Code/Finding.md                      |  20 +--
 docs/IDE/Code/IntelliSense.md                 |   4 +-
 docs/IDE/Code/Navigating.md                   |   8 +-
 docs/IDE/Customising/Keyboard-Shortcuts.md    |   4 +-
 docs/IDE/Customising/Themes.md                |   8 +-
 .../IDE/Debugging/Breakpoints-and-Stepping.md |  20 +--
 docs/IDE/Debugging/Debug-Console.md           |  12 +-
 docs/IDE/Debugging/Inspecting.md              |  12 +-
 docs/IDE/Debugging/Memory.md                  |  16 +--
 docs/IDE/Debugging/Running.md                 |  16 +--
 docs/IDE/Forms/Arranging.md                   |  20 +--
 docs/IDE/Forms/Controls.md                    |   4 +-
 docs/IDE/Forms/Designer.md                    |  36 ++---
 docs/IDE/Forms/Properties.md                  |   8 +-
 docs/IDE/Forms/Toolbox.md                     |   4 +-
 docs/IDE/Menu/Format.md                       |   4 +-
 docs/IDE/Projects/Adding-Files.md             |  16 +--
 docs/IDE/Projects/Files-and-Folders.md        |  36 ++---
 docs/IDE/Projects/Project-Explorer.md         |   8 +-
 docs/IDE/Projects/References-and-Packages.md  |  24 ++--
 docs/IDE/Projects/Saving-and-Exporting.md     |   8 +-
 docs/IDE/Reports/index.md                     |   4 +-
 docs/LLVM/Getting-Started.md                  |  20 +--
 .../WebView2/WebView2HeadersCollection.md     |   4 +-
 .../Built-In/tbIDE/FileSystemItem.md          |   4 +-
 docs/Reference/Built-In/tbIDE/HtmlElement.md  |   4 +-
 .../Built-In/tbIDE/HtmlElementProperties.md   |   4 +-
 .../Built-In/tbIDE/KeyboardShortcuts.md       |   4 +-
 docs/Reference/COM-Interfaces/IDispatch.md    |   4 +-
 docs/Reference/COM-Interfaces/IErrorInfo.md   |   8 +-
 docs/Reference/COM-Interfaces/IUnknown.md     |   8 +-
 .../Contributed/ArrayListLib/index.md         |   4 +-
 .../Contributed/BigNumberLib/BigNumber.md     |   5 +-
 .../Contributed/Fmt/EscapeSequence/index.md   |   4 +-
 .../Contributed/Fmt/StringValidation/index.md |   4 +-
 docs/Reference/Contributed/Generics/index.md  |  12 +-
 .../OLEGuids/IEnumVARIANTUnrestricted.md      |   4 +-
 .../OLEGuids/IUnknownUnrestricted.md          |   4 +-
 .../Reference/Contributed/TBMANLIB/ToolsCN.md |   4 +-
 .../Contributed/TBMANLIB/cHttpClient.md       |   8 +-
 .../Contributed/TBMANLIB/cToolsStream.md      |   4 +-
 docs/Reference/Contributed/TBMANLIB/index.md  |   4 +-
 .../Contributed/TBMANLIB_Aes/cAes.md          |  28 ++--
 .../Contributed/TBMANLIB_Aes/index.md         |   4 +-
 .../Contributed/TBMANLIB_Aes/mAesConstants.md |   8 +-
 .../TBMANLIB_ConsoleComponents/index.md       |   4 +-
 .../TBMANLIB_ConsoleCore/cConsole.md          |   4 +-
 .../TBMANLIB_ConsoleCore/cConsoleBuffer.md    |   4 +-
 .../Contributed/TBMANLIB_ConsoleCore/index.md |   4 +-
 .../Contributed/TBMANLIB_Diagnostic/index.md  |   8 +-
 .../TBMANLIB_Dialog/cDialogCountdownMsgBox.md |   4 +-
 .../TBMANLIB_Dialog/cDialogDateTime.md        |  12 +-
 .../Contributed/TBMANLIB_Dialog/cDialogEx.md  |  12 +-
 .../TBMANLIB_Dialog/cDialogFindReplace.md     |   4 +-
 .../TBMANLIB_Dialog/cDialogInputBox.md        |   4 +-
 .../TBMANLIB_Dialog/cDialogPageSetup.md       |   4 +-
 .../TBMANLIB_Dialog/cDialogPassword.md        |   4 +-
 .../Contributed/TBMANLIB_Dialog/index.md      |   5 +-
 .../Contributed/TBMANLIB_Xml/cXml.md          |   4 +-
 .../Contributed/TBMANLIB_Xml/index.md         |   4 +-
 .../Contributed/VBComDlg/CommonDialog.md      |   4 +-
 docs/Reference/Contributed/WinReg/index.md    |   4 +-
 .../Contributed/tbFileSysTools/File.md        |   4 +-
 .../tbFileSysTools/FileSystemTools.md         |   8 +-
 .../Contributed/tbFileSysTools/Folder.md      |   4 +-
 .../Contributed/tbRegExp/RegExp/IsValid.md    |   4 +-
 .../Contributed/tsJson/JsonOptions.md         |   4 +-
 docs/Reference/Contributed/tsJson/index.md    |   4 +-
 docs/Reference/Contributed/tsPwGen/PwGen.md   |   4 +-
 .../Contributed/tsPwGen/PwGenCryptoSource.md  |   4 +-
 .../Contributed/tsPwGen/PwGenOptions.md       |   8 +-
 docs/Reference/Contributed/xCalendar/index.md |  20 +--
 docs/Reference/Core/Const.md                  |   8 +-
 docs/Reference/Core/Divide.md                 |   4 +-
 docs/Reference/Core/Error.md                  |   4 +-
 docs/Reference/Core/For-Each-Next.md          |   5 +-
 docs/Reference/Core/Get.md                    |   4 +-
 docs/Reference/Core/Graphics-Methods.md       |   4 +-
 docs/Reference/Core/LSet.md                   |   4 +-
 docs/Reference/Core/LeftShift.md              |   4 +-
 docs/Reference/Core/Line-Input.md             |   4 +-
 docs/Reference/Core/Minus.md                  |   4 +-
 docs/Reference/Core/Multiply.md               |   4 +-
 docs/Reference/Core/Name.md                   |   4 +-
 docs/Reference/Core/On-Error.md               |   4 +-
 docs/Reference/Core/Open.md                   |   4 +-
 docs/Reference/Core/ParamArray.md             |   4 +-
 docs/Reference/Core/Plus.md                   |   4 +-
 docs/Reference/Core/Print.md                  |   4 +-
 docs/Reference/Core/Put.md                    |   4 +-
 docs/Reference/Core/RSet.md                   |   4 +-
 docs/Reference/Core/ReDim.md                  |   4 +-
 docs/Reference/Core/RightShift.md             |   4 +-
 docs/Reference/Default/VB/Data/index.md       |   8 +-
 docs/Reference/Default/VB/MultiFrame/index.md |   4 +-
 docs/Reference/Default/VB/OLE/index.md        |   4 +-
 docs/Reference/Default/VB/TextBox/index.md    |   4 +-
 .../Reference/Default/VBA/DateTime/DateAdd.md |   4 +-
 docs/Reference/Default/VBA/DateTime/index.md  |   4 +-
 .../Default/VBA/ErrObject/Description.md      |   4 +-
 docs/Reference/Default/VBA/ErrObject/Raise.md |   8 +-
 .../Reference/Default/VBA/ErrObject/Source.md |   4 +-
 .../Reference/Default/VBA/FileSystem/ChDir.md |   4 +-
 docs/Reference/Default/VBA/FileSystem/EOF.md  |   4 +-
 .../Default/VBA/FileSystem/FileAttr.md        |   4 +-
 .../Default/VBA/FileSystem/FileDateTime.md    |   4 +-
 .../Default/VBA/FileSystem/FileLen.md         |   4 +-
 .../Default/VBA/FileSystem/GetAttr.md         |   4 +-
 docs/Reference/Default/VBA/FileSystem/Kill.md |   4 +-
 docs/Reference/Default/VBA/FileSystem/LOF.md  |   4 +-
 docs/Reference/Default/VBA/FileSystem/Loc.md  |   4 +-
 .../Reference/Default/VBA/FileSystem/MkDir.md |   4 +-
 .../Reference/Default/VBA/FileSystem/RmDir.md |   4 +-
 docs/Reference/Default/VBA/FileSystem/Seek.md |   4 +-
 .../Default/VBA/FileSystem/SetAttr.md         |   8 +-
 .../Default/VBA/Interaction/CallByName.md     |   4 +-
 .../Default/VBA/Math/ShiftRotateLeft.md       |   4 +-
 .../Default/VBA/Math/ShiftRotateRight.md      |   4 +-
 .../Default/VBA/Math/ShiftUnsignedRight.md    |   4 +-
 .../Default/VBRUN/ErrorCallstack/index.md     |   4 +-
 .../Default/VBRUN/ErrorContext/index.md       |   4 +-
 .../Default/VBRUN/ErrorStackFrame/index.md    |   4 +-
 docs/Reference/Operators.md                   |   4 +-
 docs/_sass/custom/admonitions.scss            |  16 +++
 docs/assets/css/print.css                     |  11 ++
 indexer/README.md                             |  11 ++
 indexer/draft/workflow.mjs                    |   5 +
 indexer/twin-index.mjs                        |  72 ++++++++++
 lib/callout-labels.mjs                        | 131 ++++++++++++++++++
 scripts/lib/axe-scan.mjs                      |   5 +
 scripts/pick_a11y_sample.mjs                  |   3 +
 test/render.test.mjs                          | 114 ++++++++++++++-
 143 files changed, 878 insertions(+), 443 deletions(-)
 create mode 100644 lib/callout-labels.mjs

diff --git a/WIP.Build.md b/WIP.Build.md
index 5168ff1be..4d0d7ebc4 100644
--- a/WIP.Build.md
+++ b/WIP.Build.md
@@ -413,6 +413,15 @@ admonition's lines by their `>` markers and strips them, and a masked fence
 inside an admonition takes its markers with it into the stash. It asks
 `blockRegions`, with the same parser, which lines are code instead.
 
+It also reads the label, the rest of the `[!TYPE]` line (`lib/callout-labels.mjs`),
+and throws on one it cannot parse: it used to drop that text silently, which is
+how a typo in a label would vanish. A package version in a label is checked against
+`page.packageVersion`, the section's `indexed_from`, which the main thread stamps
+on every page before the render chunks go to the workers, since a chunk need not
+hold the section's landing page. The code-region gate calls the chain with no
+`packageVersion` at all, which skips that check rather than failing every
+labelled page: "" means a page in no section.
+
 ### Whitespace inside inline code is content
 
 A documented value can be a padded string: [Partition](docs/Reference/Default/VBA/Interaction/Partition.md)
diff --git a/WIP.IDE.md b/WIP.IDE.md
index 2f88f2977..ed7be4b79 100644
--- a/WIP.IDE.md
+++ b/WIP.IDE.md
@@ -98,7 +98,8 @@ for contributors.
   repeats the picture. A reference entry says what the item does. An item with nothing to say
   beyond its label is left to the picture.
 - **Headings** in sentence case, with no icon and no shortcut inside them.
-- **The section names no build**, except in a defect callout (owner's standing rule). Lines such
+- **The section names no build**, except in a defect callout's label, `> [!NOTE] BETA 1005`
+  (owner's standing rule; WIP.md rule 6). Lines such
   as "Components list coming soon" go, unless the IDE itself shows that text and the page says
   so.
 - **Defects the twinBASIC developers already know of** (owner) are not queued, but a page that
diff --git a/WIP.md b/WIP.md
index 2f383f5c9..a444d7b94 100644
--- a/WIP.md
+++ b/WIP.md
@@ -228,7 +228,9 @@ The vocabulary tables further down cover word choice. The rules in this subsecti
 
    **Where twinBASIC and VB6 behave differently, say what VB6 does** (owner's rule), checked with `scripts/vb6run.mjs` or a reproducer's `vb6/` side, never from memory. Where twinBASIC's behaviour is the nominal one, the VB6 behaviour goes in a `> [!NOTE]`; inside a defect's WARNING it is one sentence, such as "VB6 raises error 6, *Overflow*."
 
-   **A known twinBASIC defect that silently gives a wrong result is a `> [!WARNING]` until a fixed build is released** (owner's rule): a wrong value with no error raised is data corruption the reader cannot see. A crash is a WARNING too (unsaved data is lost), and so is an error that hides a silent part (a procedure run twice before the error). A defect the reader cannot miss and that loses nothing --- an error raised, a statement refused --- is a `> [!NOTE]`. Name the build in either. **Once a fixed build is released, the callout goes** (owner's rule): the documentation is not a record of past defects, so the page then states the correct behaviour with no mention of the defect. The defect's BUGS-TO-REPORT entry says in its HTML comment which page carries the callout, so the callout is removed when the entry is retired.
+   **A known twinBASIC defect that silently gives a wrong result is a `> [!WARNING]` until a fixed build is released** (owner's rule): a wrong value with no error raised is data corruption the reader cannot see. A crash is a WARNING too (unsaved data is lost), and so is an error that hides a silent part (a procedure run twice before the error). A defect the reader cannot miss and that loses nothing --- an error raised, a statement refused --- is a `> [!NOTE]`. Name the build in either, in its label (next paragraph). **Once a fixed build is released, the callout goes** (owner's rule): the documentation is not a record of past defects, so the page then states the correct behaviour with no mention of the defect. The defect's BUGS-TO-REPORT entry says in its HTML comment which page carries the callout, so the callout is removed when the entry is retired.
+
+   **A callout says what it applies to in its label, the rest of its `[!TYPE]` line, and never in its text** (owner's rule): `> [!WARNING] BETA 1005` for a fact of that twinBASIC build, `> [!NOTE] since BETA 984` for one true from that build on, `> [!WARNING] 1.0.0.4` for one of that version of the page's own package, and `> [!NOTE] 1.6.151.1, BETA 1005` for both. `BETA` is upper case and a package version is bare, all four parts. The text then starts with the fact: never "In twinBASIC BETA 1005, ...", "BETA 1005 has a defect: ..." or "In version 1.0.0.4, ...". The build draws each item as a tag after the title and fails on anything else after `[!TYPE]`, and on a package version other than the section's `indexed_from` (`lib/callout-labels.mjs`). A label belongs on what is true of one build or one version, a defect above all; a callout that holds as well as that something true of every build is split in two.
 
 7. **See Also.** Last section on the page, after `Example`; only a contributed package's landing page has one after it, `## Test cases`. Format: `- [Symbol](Symbol) ` where `` is the kind: statement, function, property, method, class, module, package. Pages with annotations use `- [Symbol](Symbol) -- short description` — the `--` source renders as a typographic dash via markdown-it's typographer (en-dash for `--`, em-dash for `---`). Don't write literal `—` in source; keep `--` for consistency across the docs. Order by conceptual proximity, not strict alphabetical.
 
@@ -435,7 +437,14 @@ as it is. The rules:
   `analyze` compares with the snapshot at `HEAD` and only when its `version` is the
   `indexed_from` tag; a `sync` against a newer build rewrites the working-tree snapshots, and
   committing them on their own, or the bump without them, drops the next `analyze` into audit
-  mode.
+  mode. The bump fails the build until each callout labelled with the old version is checked
+  against the new one and its label moved, or the callout removed: `analyze` lists them as the
+  package's `recheck_callout` tasks.
+- **After a new twinBASIC build, check again every callout labelled with an older one.** An
+  `analyze` of the whole reference (no `--package`) lists the callouts labelled `BETA ` below
+  the build the store's built-in packages come from, and writes them to
+  `indexer/.packages/callouts.json`. Each is checked on the new build: its label moves to it, or
+  the callout goes (rule 6 under *Plain-English prose*).
 - **A change to `scripts/lib/twin-api.mjs`, `scripts/lib/twin-declarations.mjs`,
   `indexer/lib/api-model.mjs` or `indexer/lib/vb6-preprocess.mjs`, or to `format` in
   `api.json`, regenerates the committed snapshots in the same commit**, with `sync --ide`
diff --git a/builder/render.mjs b/builder/render.mjs
index e9ba8cb52..8e719d510 100644
--- a/builder/render.mjs
+++ b/builder/render.mjs
@@ -22,6 +22,7 @@ import { replaceOutsideCode } from "./code-guard.mjs";
 import { splitFragment } from "./url.mjs";
 import { escapeMarkup, escapeMarkupAndQuotes, escapeRegExp } from "./escape.mjs";
 import { inlineDiagram } from "./inline-diagram.mjs";
+import { checkCalloutLabel, parseCalloutLabel } from "../lib/callout-labels.mjs";
 import {
   isLightPicture,
   isPictureSvg,
@@ -74,7 +75,12 @@ function renderPage(page, md) {
     // consumed by Phase 8. Either way, no markdown rendering happens here.
     return page.rawContent;
   }
-  const source = applyPreRenderRewrites(page.rawContent, md);
+  let source;
+  try {
+    source = applyPreRenderRewrites(page.rawContent, md, { packageVersion: page.packageVersion ?? "" });
+  } catch (e) {
+    throw new Error(`${page.srcRel}: ${e.message}`);
+  }
   const env = { page };
   const html = md.render(source, env);
   // Lifted off the headings by searchIndexMarksPlugin; search.mjs reads
@@ -110,7 +116,10 @@ function padEmptyCells(html) {
 // the definition-list plugin, for one, makes a fence after `: ` that a bare
 // parser reads as a paragraph. Without it the mask would silently differ from
 // the build's, so a call without one throws.
-export function applyPreRenderRewrites(rawContent, md) {
+//
+// `options.packageVersion` is the version the page's package section
+// documents, which a callout label's package version must be.
+export function applyPreRenderRewrites(rawContent, md, options = {}) {
   if (!md) throw new TypeError("applyPreRenderRewrites: pass the site's markdown-it instance");
   // CommonMark normalises CRLF/CR to LF before block parsing. The rewrites
   // below all rely on LF-only input -- do the normalisation up front so they
@@ -141,7 +150,7 @@ export function applyPreRenderRewrites(rawContent, md) {
   // admonition's lines by their `> ` markers and strips them, and a masked
   // fence inside an admonition has taken its markers with it into the stash.
   // It asks lib/markdown where the code is itself, with the same parser.
-  return rewriteAdmonitions(code.restore(work), md);
+  return rewriteAdmonitions(code.restore(work), md, options);
 }
 
 // kramdown accepts unescaped spaces inside `![alt](url)` and `[text](url)`
@@ -1767,8 +1776,9 @@ const ADMONITION_TYPES = {
 // admonitions inside a list item or blockquote share the parent's
 // indentation; the gem's regex captures that into \1 and uses it as a
 // per-line anchor on the body lines.
+// The rest of the `[!TYPE]` line is the callout's label (lib/callout-labels.mjs).
 const ADMONITION_RE =
-  /(^|\n)([ \t]*)>[ \t]*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\][^\n]*\n((?:\2[ \t]*>[ \t]*[^\n]*(?:\n|$))(?:(?![ \t]*>[ \t]*\[!)\2[ \t]*>[ \t]*[^\n]*(?:\n|$))*)?/g;
+  /(^|\n)([ \t]*)>[ \t]*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]([^\n]*)\n((?:\2[ \t]*>[ \t]*[^\n]*(?:\n|$))(?:(?![ \t]*>[ \t]*\[!)\2[ \t]*>[ \t]*[^\n]*(?:\n|$))*)?/g;
 // An admonition written inside a code region is a sample of the syntax, and is
 // left as it is. This rewrite runs outside the mask, so it asks lib/markdown
 // where the regions are, with the site's parser for the reason
@@ -1782,7 +1792,12 @@ const ADMONITION_RE =
 // leaves admonitions as the literal text "[!NOTE]". WIP.Build.md tells both.
 // A private scan can also disagree with the parser without anyone noticing;
 // check_code_regions.mjs probes each shape through the whole chain.
-export function rewriteAdmonitions(src, md) {
+//
+// A label that does not parse, or names a package version other than the one
+// the page's section documents (`packageVersion`), throws: the build stops and
+// names the callout. `packageVersion` is "" for a page in no package section;
+// left out, as the code-region gate's sweep leaves it, versions go unchecked.
+export function rewriteAdmonitions(src, md, { packageVersion } = {}) {
   if (!md) throw new TypeError("rewriteAdmonitions: pass the site's markdown-it instance");
   // CommonMark's normalisation pass converts CRLF/CR to LF before block
   // parsing. We do it up-front so our regexes operate on LF-only input.
@@ -1794,13 +1809,26 @@ export function rewriteAdmonitions(src, md) {
   let line = 0;
   let counted = 0;
 
-  return src.replace(ADMONITION_RE, (match, leading, indent, typeRaw, bodyRaw, offset) => {
+  return src.replace(ADMONITION_RE, (match, leading, indent, typeRaw, labelRaw, bodyRaw, offset) => {
     const at = offset + leading.length;
     for (; counted < at; counted++) if (src.charCodeAt(counted) === 10) line++;
     if (inCode[line]) return match;
 
     const type = typeRaw.toLowerCase();
     const meta = ADMONITION_TYPES[type];
+    const label = parseCalloutLabel(labelRaw);
+    const problem =
+      label.error ?? (packageVersion === undefined ? undefined : checkCalloutLabel(label.items, packageVersion));
+    if (problem) {
+      const opening = (bodyRaw ?? "")
+        .split("\n")[0]
+        .replace(/^[ \t]*>[ \t]?/, "")
+        .slice(0, 60);
+      throw new Error(`callout "[!${typeRaw}]${labelRaw}" (${opening}...): ${problem}`);
+    }
+    // One tag per item, a space between them so that the title reads as words
+    // to a screen reader; the title is a flex box, which does not draw it.
+    const tags = label.items.map((i) => ` ${escapeMarkup(i.text)}`).join("");
     // The body lines all share the same leading indent; strip it plus the
     // `>` marker and AT MOST ONE following space, which is what blockquote
     // unwrapping means.
@@ -1822,7 +1850,7 @@ export function rewriteAdmonitions(src, md) {
     // blockHtmlRecursionPlugin) parses it as an independent block.
     // The trailing blank line ensures any following text is parsed as
     // a separate markdown block rather than absorbed into the html_block.
-    return `${leading}
\n

${meta.icon} ${meta.title}

\n\n${body}\n
\n\n`; + return `${leading}
\n

${meta.icon} ${meta.title}${tags}

\n\n${body}\n
\n\n`; }); } diff --git a/builder/tbdocs.mjs b/builder/tbdocs.mjs index a142f252a..8848543b5 100644 --- a/builder/tbdocs.mjs +++ b/builder/tbdocs.mjs @@ -71,6 +71,7 @@ import { computeSiteSeo } from "./seo.mjs"; import { resolveBookChapters } from "./book.mjs"; import { loadData } from "./data.mjs"; import { createMarkdownIt, buildLinkTables, diagramIsTall, serializeLinkTables } from "./render.mjs"; +import { assignPackageVersions } from "../lib/callout-labels.mjs"; import { isPictureSvg, pairSizes, pictureSvgOutputs, unpairedLightPictures } from "./theme-pictures.mjs"; import { loadHighlightTheme } from "./highlight-theme.mjs"; import { NAV_SCRIPT_REL, buildInitConfig, renderSidebar } from "./template.mjs"; @@ -660,6 +661,9 @@ const TASKS = { ) { void _dotSignal; // dependency signal only -- static files already appended in dot.submit void _markdownInitSignal; // dependency signal only -- markdown + linkTablesSerialized + seoSiteTitle/seoLogoUrl already on state.site + // A callout label's package version is checked against its section's + // indexed_from, which a worker's chunk may not hold: stamp it here. + assignPackageVersions(state.pages); const chunks = chunkPages(state.pages, ctx.workerCount); // An offline tree that shows the pictures' PNGs (picture_svg.offline false) holds no // picture SVG either: each is added to offline_exclude, on the config every consumer of diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 4066acfcb..7d2060dc7 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -869,12 +869,28 @@ Three severities, used distinctly: - `> [!IMPORTANT]` --- requirements that affect correctness, such as admin rights, threading constraints, or ordering. - `> [!WARNING]` --- operations that can corrupt state or lose data. -A known defect in twinBASIC follows the same scale. A defect that silently gives a wrong result --- a function that returns the wrong value and raises no error --- is a `> [!WARNING]` until a fixed build is released, because nothing tells the reader their data is wrong. A defect that crashes the program is a `> [!WARNING]` too, because unsaved work is lost. So is a defect whose error hides a silent part, such as a procedure that runs twice before the error is raised. A defect the reader cannot miss and that loses nothing, such as an error raised or a statement the compiler refuses, is a `> [!NOTE]`. Either callout names the build it applies to. Once a fixed build is out, remove the callout and describe the correct behaviour with no mention of the defect: the documentation is not a record of past defects. +A known defect in twinBASIC follows the same scale. A defect that silently gives a wrong result --- a function that returns the wrong value and raises no error --- is a `> [!WARNING]` until a fixed build is released, because nothing tells the reader their data is wrong. A defect that crashes the program is a `> [!WARNING]` too, because unsaved work is lost. So is a defect whose error hides a silent part, such as a procedure that runs twice before the error is raised. A defect the reader cannot miss and that loses nothing, such as an error raised or a statement the compiler refuses, is a `> [!NOTE]`. Either callout names the build it applies to, in its label. Once a fixed build is out, remove the callout and describe the correct behaviour with no mention of the defect: the documentation is not a record of past defects. Where twinBASIC and VB6 behave differently, say what VB6 does. Check it in VB6 --- [`vb6run.mjs`](Tools-Defects#vb6run) builds and runs a sample there --- rather than writing it from memory. Where twinBASIC's behaviour is the intended one, the VB6 behaviour goes in a `> [!NOTE]`. Inside a defect's `> [!WARNING]` it is one sentence, such as "VB6 raises error 6, *Overflow*." Use one callout per concern, and only for genuine notes: plain "why this is useful" prose stays a plain paragraph. +### What a callout applies to + +A callout that is true of one twinBASIC build, or of one version of a package, says so in its label: the rest of the `[!TYPE]` line. The page shows each item of the label as a tag after the callout's title. + +```md +> [!WARNING] BETA 1005 +> **DateAdd** with the "m", "q" or "yyyy" interval does not stop at the end of a shorter month. +``` + +- `BETA 1005` --- true of that build, the usual label of a defect. +- `since BETA 984` --- true from that build on. +- `1.0.0.4` --- true of that version of the package whose section the page is in, written with all four parts. +- `1.6.151.1, BETA 1005` --- both, separated by a comma. + +The text then starts with the fact itself, not with "In twinBASIC BETA 1005, ..." or "In version 1.0.0.4, ...". If only part of a callout depends on the build, split it into two callouts. The build fails on any other label, and on a package version other than the one the section documents, its landing page's `indexed_from`. So when a section moves to a new version of its package, each callout labelled with the old one is checked again before the build passes. [`twin-index analyze`](Tools-Build#twin-index) lists those callouts, and after a new twinBASIC build, the callouts labelled with an older one. + A page states what the product does, never what the documentation's own checking could or could not do. "The samples harness cannot capture a console" is a fact about this repository's tools, not about the package, and has no place in a published page. A claim nobody has been able to check is written as the source supports it, or left out. ### Callouts in a contributed package's section diff --git a/docs/Documentation/Tools-Build.md b/docs/Documentation/Tools-Build.md index f74b58795..c35535af2 100644 --- a/docs/Documentation/Tools-Build.md +++ b/docs/Documentation/Tools-Build.md @@ -183,6 +183,7 @@ A name that matches no package is reported, the other packages are done, and the | `indexer/.packages/` | no | The store, a git repository of its own that `sync` creates. Each package has its `sources/`, its `Settings` file, its `api.json` and an `indexer.json` that records which version of the scanner wrote the `api.json`. A new store starts from the committed snapshots, so its history shows what changed in each package's API. | | `indexer/.packages/manifests/` | no | What was last read: `built-in.json` (the source, `install` or `release`, and the build) and `contributed.json` (the TWINSERV versions). They never hold the path of an install, which contains a user name. | | `indexer/.packages/report.json` | no | The report of the last `analyze`. | +| `indexer/.packages/callouts.json` | no | The callouts labelled with a build older than the store's, from the last `analyze` of the whole reference. | | `indexer/.packages/draft/` | no | What the last `draft` placed: a plan for each package, its pages in batches, and `manifest.json`, which lists them. | | `indexer/snapshots///api.json` | yes | A copy of the `api.json` of each package the reference documents, for `analyze` to compare with. `` is `default`, `built-in` or `contributed`. | @@ -195,6 +196,8 @@ A name that matches no package is reported, the other packages are done, and the - **Update mode**, for a section whose landing page has `indexed_from`. The current `api.json` is compared with the snapshot committed at `HEAD`, provided that snapshot's `version` is the `indexed_from` tag. The report lists the symbols added (to create), changed (to update) and removed (to flag). - **Audit mode**, for a section with no `indexed_from`, and when update mode has nothing to compare with: no committed snapshot, one in an older format, or one of another version, each with a warning. Every public symbol is matched by name with the `title:` of the pages in the section, and those with no page are to create. +**The callouts to check again.** A callout's label says which build of twinBASIC or which version of its package it is true of (see [What a callout applies to](Authoring#what-a-callout-applies-to)). When the store holds a version of a package other than its section's `indexed_from`, the report lists each callout of the section labelled with another version, as a `recheck_callout` task: the build refuses the section's new `indexed_from` until each is checked and its label moved, or the callout removed. An `analyze` with no `--package`, `--packages` or `--all` also lists the callouts of the whole documentation labelled `BETA ` below the build the store's built-in packages were read from, and writes them to `indexer/.packages/callouts.json`. A `since BETA ` label is never listed: it stays true. + `analyze` exits 0 whenever it writes its report, whatever the report lists: the list is the product, not a failure. **What `draft` places.** `draft` reads the last report and, for each package in it (or each one `--package` or `--packages` names), writes the plan `indexer/.packages/draft/.json`. Each entry names the page file to write or change, the title of its parent, and its permalink, all taken from what most of the section's pages do: a new type goes in the folder the section keeps that kind of type in (an `Enumerations/` folder, or beside the classes), as a folder with an `index.md` or as one page as those types are; a type that another type returns, such as a collection's item, goes in that type's folder when the folder already holds a type; and a new member gets a page of its own or goes on its type's page, as that type's other members do. A permalink is filled in only when every page of the section has the permalink its folders give it, which is so for every section except VBA's; otherwise it is left to follow the sibling pages. Each entry also names a page of the same kind in the section to follow, and the source file that declares the symbol. A package with no section gets one in its group's folder, with a landing page titled and placed like those of the group's other sections. A symbol no page can be found for, such as a member of a type that has no page, is listed as unplaced. A symbol the build no longer declares is listed with the page that documents it. The pages are also split into batches, one file each, for whoever writes them in parallel. @@ -202,7 +205,7 @@ A name that matches no package is reported, the other packages are done, and the **Moving the reference to a new build:** 1. Run `sync` and `derive` with the new install. `sync` writes the new snapshots into the working tree, and `analyze` still compares with the ones at `HEAD`; `derive` writes the new `builder/package-api.json`. -2. Run `analyze`, then `draft`, and write the pages the plans list, where they say. +2. Run `analyze`, then `draft`, and write the pages the plans list, where they say. Check each callout `analyze` lists as labelled with an older build on the new one, and move its label or remove it. 3. Set `indexed_from` on each section to the new build's tag (`beta-x-0997` is BETA 997), and commit the pages, the new `indexed_from`, the new snapshots and `builder/package-api.json` together. A snapshot committed on its own, or an `indexed_from` that is not the snapshot's `version`, sends the next `analyze` into audit mode. **After a change to the scanner or the model.** The code that writes `api.json` is `scripts/lib/twin-api.mjs`, `scripts/lib/twin-declarations.mjs`, `indexer/lib/api-model.mjs` and `indexer/lib/vb6-preprocess.mjs`. A change to any of them, the `format` number of `api.json` included, can change what the same sources index to. The next `sync` indexes every package again, and the committed snapshots have to be written again from the build they record, with `sync --ide` naming that build's install, in the same commit as the change. Otherwise `analyze` compares the output of the new code with that of the old and reports the difference as changes to the API. `--release` cannot do this: it downloads only the latest release. diff --git a/docs/Features/Advanced/API-Declarations.md b/docs/Features/Advanced/API-Declarations.md index 60006cfc9..a3eaa3806 100644 --- a/docs/Features/Advanced/API-Declarations.md +++ b/docs/Features/Advanced/API-Declarations.md @@ -97,8 +97,8 @@ End Class On a 32-bit build, a **ThisCall** implementation takes `this` in the ECX register and removes its own arguments from the stack, so native code calls it through the interface's vtable as it calls a C++ virtual method. An error raised in the implementation reaches the native caller as a failure **HRESULT**, as from any member without **[PreserveSig]**. Code compiled with LLVM does not pass the error on yet; see [Language support](../../LLVM/Getting-Started#language-support). -> [!NOTE] -> In BETA 1005, a class cannot implement a member marked [**[PreserveSig]**](../../tB/Core/Attributes#preservesig): the compiler reports the implementation as unmatched. Interfaces of this kind often return a value directly rather than an **HRESULT**, and are declared with **[PreserveSig]** for that reason. For a member that returns a 4-byte value, such as a **Long** or a C `BOOL`, declare it in the interface the class implements as a **Sub** without **[PreserveSig]**, and return the value with [**Err.ReturnHResult**](../../tB/Modules/ErrObject/ReturnHResult). Native code that calls this member as `long __thiscall Count()` gets 3 as its return value: +> [!NOTE] BETA 1005 +> A class cannot implement a member marked [**[PreserveSig]**](../../tB/Core/Attributes#preservesig): the compiler reports the implementation as unmatched. Interfaces of this kind often return a value directly rather than an **HRESULT**, and are declared with **[PreserveSig]** for that reason. For a member that returns a 4-byte value, such as a **Long** or a C `BOOL`, declare it in the interface the class implements as a **Sub** without **[PreserveSig]**, and return the value with [**Err.ReturnHResult**](../../tB/Modules/ErrObject/ReturnHResult). Native code that calls this member as `long __thiscall Count()` gets 3 as its return value: > > ```tb check_build projname=calling-conventions-thiscall-return > [InterfaceId("2C4E8A61-5B3F-4D7E-9C1A-6E0B2F4D8A13")] @@ -122,8 +122,8 @@ An ActiveX DLL's type library records a **ThisCall** or **FastCall** member as s A callback passed to an API must use the convention the API calls it with. Declare a [**Delegate**](../../tB/Core/Delegate) with that convention, use it as the parameter type, and give the target procedure the same keyword. [**AddressOf**](../../tB/Core/AddressOf) of a procedure with a different convention is accepted with a warning only (TB0026). -> [!NOTE] -> In BETA 1005, on a 32-bit build, a call made from twinBASIC code *through* a **ThisCall** or **FastCall** delegate passes its arguments as stdcall does, and fails with *Bad DLL definition*. Calling the procedure directly works. **CDecl** delegates are not affected, and neither is a 64-bit build. +> [!NOTE] BETA 1005 +> On a 32-bit build, a call made from twinBASIC code *through* a **ThisCall** or **FastCall** delegate passes its arguments as stdcall does, and fails with *Bad DLL definition*. Calling the procedure directly works. **CDecl** delegates are not affected, and neither is a 64-bit build. The following example performs a quicksort using the C runtime's [`qsort` function](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/qsort), which calls its comparator with the cdecl convention: diff --git a/docs/Features/Advanced/Classes-and-Modules.md b/docs/Features/Advanced/Classes-and-Modules.md index 0df1da080..f64c9720a 100644 --- a/docs/Features/Advanced/Classes-and-Modules.md +++ b/docs/Features/Advanced/Classes-and-Modules.md @@ -18,8 +18,8 @@ A class can have a `Sub New` constructor, and the constructor can take arguments - A class that has both a `Class_Initialize` and a `Sub New` with no parameters cannot be created with a plain `New`. The call matches both, and fails with TB5073. - A class whose only `Sub New` has required arguments, with no `Class_Initialize`, cannot be declared [`As New`](../../tB/Core/New), which creates the object without arguments. The declaration fails with TB5121. If every argument of the `Sub New` is `Optional`, the class can be declared `As New`: the object is created by calling `Sub New` with the default values. -> [!NOTE] -> A `Sub New` declared `Private` is meant to make construction private, so that only code inside the class creates objects with it. In BETA 1005 this is not enforced: `New MyClass` in another module of the project, or in a project that references a package with the class, compiles and runs the private constructor. +> [!NOTE] BETA 1005 +> A `Sub New` declared `Private` is meant to make construction private, so that only code inside the class creates objects with it. This is not enforced: `New MyClass` in another module of the project, or in a project that references a package with the class, compiles and runs the private constructor. ### Example diff --git a/docs/IDE/Code/Diagnostics.md b/docs/IDE/Code/Diagnostics.md index e286bb99a..30f9add05 100644 --- a/docs/IDE/Code/Diagnostics.md +++ b/docs/IDE/Code/Diagnostics.md @@ -72,8 +72,8 @@ Right-click in the list for a menu of five items. The menu is in English whateve A copied row reads `{ERROR}` or `{WARNING}`, the path of the file, the position, and the code and message. **Copy All** and **Copy All Errors** begin with a line that names the IDE: `twinBASIC (` and the version, then `) compilation error report:`. -> [!NOTE] -> In twinBASIC BETA 1005, each copied message contains the HTML tag `TBnnnn` around the code, and **Copy All** labels hints and information messages `{WARNING}`. Remove the tags from the pasted text by hand. +> [!NOTE] BETA 1005 +> Each copied message contains the HTML tag `TBnnnn` around the code, and **Copy All** labels hints and information messages `{WARNING}`. Remove the tags from the pasted text by hand. ## In the code diff --git a/docs/IDE/Code/Editor.md b/docs/IDE/Code/Editor.md index abf941210..39512cde0 100644 --- a/docs/IDE/Code/Editor.md +++ b/docs/IDE/Code/Editor.md @@ -26,8 +26,8 @@ Each open file has a tab. The tab shows the file's icon, its name and a close bu - Drag a tab to move it. A mark on the left or the right half of the tab under the pointer shows where it will land. - Click the middle mouse button on a tab to close it. -> [!NOTE] -> In twinBASIC BETA 1005, a tab is tinted only when the compiler reports its file while the tab is open. A tab opened after that report stays untinted until the compiler checks the file again, for example after the next edit to it. The Project Explorer row is tinted either way. +> [!NOTE] BETA 1005 +> A tab is tinted only when the compiler reports its file while the tab is open. A tab opened after that report stays untinted until the compiler checks the file again, for example after the next edit to it. The Project Explorer row is tinted either way. A tab in italics is a *preview tab*. When **File Explorer: Single-Click Preview** is on, a single click on a file in the Project Explorer opens it in a preview tab, and the next single click replaces it. A double-click, or any edit, makes the tab permanent. The **Find / Replace** panel also opens the files it visits in a preview tab. [IDE Options](../Customising/IDE-Options) describes the option. @@ -188,8 +188,8 @@ Typing, deleting and most commands act at every cursor. One Ctrl+Ctrl+D and Ctrl+Shift+L match whole words in the current file, including words inside strings and comments. Ctrl+Shift+L and **Select All Matches** (see [Finding](Finding#select-all-matches)) both give a cursor to each match. -> [!NOTE] -> In twinBASIC BETA 1005, Esc does not remove the extra cursors. Click in the text, or run any command that places the caret, to go back to one cursor. +> [!NOTE] BETA 1005 +> Esc does not remove the extra cursors. Click in the text, or run any command that places the caret, to go back to one cursor. ### Change case @@ -259,8 +259,8 @@ Paste on a blank line, or with the caret at the end of the indent. Pasting at th **Rename Symbol** is on the right-click menu and has the key F2. It opens a box over the word with its name selected. -> [!NOTE] -> In twinBASIC BETA 1005, Rename Symbol renames nothing. Typing a new name changes the box, but Enter and Esc do nothing, and the box stays open until you click in the code. To rename a name that is used in one file, use **Change All Occurrences**. For a name used in several files, use the [Find / Replace](Finding) panel with **Whole Word Only** and **Current Project**, and replace one match at a time, because **Replace All** does not work in project scope. +> [!NOTE] BETA 1005 +> Rename Symbol renames nothing. Typing a new name changes the box, but Enter and Esc do nothing, and the box stays open until you click in the code. To rename a name that is used in one file, use **Change All Occurrences**. For a name used in several files, use the [Find / Replace](Finding) panel with **Whole Word Only** and **Current Project**, and replace one match at a time, because **Replace All** does not work in project scope. **Change All Occurrences**, on the right-click menu only, gives a cursor to every occurrence of the word under the caret in the file. Type the new name to replace them all. The match is on text, not on meaning: the word is also changed inside strings and comments. A longer name that contains the word, such as `totalPrice` for `total`, is not changed. One Ctrl+Z restores all of them. Other files are not touched. @@ -270,8 +270,8 @@ Paste on a blank line, or with the caret at the end of the indent. Pasting at th The formatter changes only the indent of each line. It trims the line and sets the indent to four spaces for each level, whatever **Tab Size** is. It does not change the words, their case or the spacing inside the line. It adds a level after `Class`, `Module`, `Type`, `Enum`, `For`, `While`, `Do`, `With`, a block `If`, `#If`, `Sub`, `Function` and `Property`, and takes it away at the matching End. `ElseIf` and `Else` lines sit at the level of their `If`. A `Select Case` indents its `Case` lines one level and their bodies two. A continuation line is indented two levels deeper than its statement. The members of an interface get one level and no End. -> [!WARNING] -> In twinBASIC BETA 1005, **Format Selection** fails when the selection starts on a continuation line, which is a line after one that ends in ` _`. Nothing is formatted, and a box opens with the text *IDE startup failure*, followed by *JS Promise Rejection(1): Assignment to constant variable.* If the IDE was started by opening the project's `.twinproj` file, it then closes. With unsaved changes, it first asks whether to save them: choose **Save**, or **Cancel** to keep the IDE open. Save before you format, start the selection on the first line of the statement, or use **Format Document**. +> [!WARNING] BETA 1005 +> **Format Selection** fails when the selection starts on a continuation line, which is a line after one that ends in ` _`. Nothing is formatted, and a box opens with the text *IDE startup failure*, followed by *JS Promise Rejection(1): Assignment to constant variable.* If the IDE was started by opening the project's `.twinproj` file, it then closes. With unsaved changes, it first asks whether to save them: choose **Save**, or **Cancel** to keep the IDE open. Save before you format, start the selection on the first line of the statement, or use **Format Document**. ## Auto Prettify @@ -306,8 +306,8 @@ A right-click in the editor moves the caret to the point, unless you click insid Shift+F10 opens the same menu from the keyboard. -> [!NOTE] -> In twinBASIC BETA 1005, Shift+F10 opens the menu at the top-left corner of the IDE window, not at the caret. Right-click in the editor to open it at the pointer. +> [!NOTE] BETA 1005 +> Shift+F10 opens the menu at the top-left corner of the IDE window, not at the caret. Right-click in the editor to open it at the pointer. ## The command palette diff --git a/docs/IDE/Code/Finding.md b/docs/IDE/Code/Finding.md index 04ecf3695..6d90efa34 100644 --- a/docs/IDE/Code/Finding.md +++ b/docs/IDE/Code/Finding.md @@ -120,15 +120,15 @@ A search that reaches the end of its scope with **Down** asks **End of search sc A search with **All**, a project search, and **Replace All** end with **The specified region has been searched.** After a **Replace All**, a second box says how many replacements were made: **N replacements were made.**, or **1 replacement was made.** With no scope ticked, the panel says **Scope not selected**. -> [!NOTE] -> In twinBASIC BETA 1005, after a search of **Selected Text**, moving the caret leaves **Selected Text** ticked but greyed out, and **Current File** is no longer ticked. **Find Next** then says at once that the end of the search scope has been reached. Tick **Current File** again to search on. +> [!NOTE] BETA 1005 +> After a search of **Selected Text**, moving the caret leaves **Selected Text** ticked but greyed out, and **Current File** is no longer ticked. **Find Next** then says at once that the end of the search scope has been reached. Tick **Current File** again to search on. ## Match text A plain search ignores case unless **Match Case** is ticked. -> [!WARNING] -> In twinBASIC BETA 1005, **Whole Word Only** never finds a match that starts at the very first character of the file. For example, searching for `Module` with **Whole Word Only** skips a `Module` on line 1, column 1. A **Replace All** with **Whole Word Only** leaves that word unchanged. Search without **Whole Word Only**, and check the first line yourself. +> [!WARNING] BETA 1005 +> **Whole Word Only** never finds a match that starts at the very first character of the file. For example, searching for `Module` with **Whole Word Only** skips a `Module` on line 1, column 1. A **Replace All** with **Whole Word Only** leaves that word unchanged. Search without **Whole Word Only**, and check the first line yourself. ### Pattern matching @@ -150,14 +150,14 @@ With **Match Regular Expressions**, **Find What** is a JavaScript regular expres If **Find What** is not in the `/pattern/flags` form, a box says **Invalid format. Expecting /pattern/flags**. If the pattern is not a valid expression, a box says **Invalid RegExp:**. -> [!WARNING] -> In twinBASIC BETA 1005, a regular expression that has a capital letter finds nothing unless it has the `i` flag. The search runs on the text of the file turned into small letters, and the pattern is left as written. `/End Sub/` finds nothing and reports **The specified region has been searched.**, while `/end sub/` and `/End Sub/i` find every `End Sub`. Write the pattern in small letters, or add the `i` flag. +> [!WARNING] BETA 1005 +> A regular expression that has a capital letter finds nothing unless it has the `i` flag. The search runs on the text of the file turned into small letters, and the pattern is left as written. `/End Sub/` finds nothing and reports **The specified region has been searched.**, while `/end sub/` and `/End Sub/i` find every `End Sub`. Write the pattern in small letters, or add the `i` flag. -> [!NOTE] -> In twinBASIC BETA 1005, a regular expression whose match spans a line break, such as `/^\s+end sub$/`, selects nothing. The selection is an empty one at the end of the line before the match, and **Find Next** stays there. Search for one line at a time. +> [!NOTE] BETA 1005 +> A regular expression whose match spans a line break, such as `/^\s+end sub$/`, selects nothing. The selection is an empty one at the end of the line before the match, and **Find Next** stays there. Search for one line at a time. -> [!NOTE] -> In twinBASIC BETA 1005, the **Invalid RegExp:** box leaves out the reason. It shows the words and nothing after them. +> [!NOTE] BETA 1005 +> The **Invalid RegExp:** box leaves out the reason. It shows the words and nothing after them. ## Search the whole project diff --git a/docs/IDE/Code/IntelliSense.md b/docs/IDE/Code/IntelliSense.md index 4c13e8800..4a0c74660 100644 --- a/docs/IDE/Code/IntelliSense.md +++ b/docs/IDE/Code/IntelliSense.md @@ -53,8 +53,8 @@ What an accepted entry adds: Esc, Left, Right and Backspace close the list. Left also moves the caret, and Backspace also deletes. Scrolling the editor with the mouse wheel closes the list too. -> [!NOTE] -> In twinBASIC BETA 1005, typing `(` to accept a function from the list doubles the bracket: `Area` becomes `Area(()`. Accept the function with Tab or Enter instead. It gives `Area()` with the caret between the brackets, so type the arguments without the opening bracket. +> [!NOTE] BETA 1005 +> Typing `(` to accept a function from the list doubles the bracket: `Area` becomes `Area(()`. Accept the function with Tab or Enter instead. It gives `Area()` with the caret between the brackets, so type the arguments without the opening bracket. The Debug Console has the same list in its entry box. See [Debug Console](../Debugging/Debug-Console). diff --git a/docs/IDE/Code/Navigating.md b/docs/IDE/Code/Navigating.md index cd0e5a176..85a319ef7 100644 --- a/docs/IDE/Code/Navigating.md +++ b/docs/IDE/Code/Navigating.md @@ -38,8 +38,8 @@ Where the caret lands: | A member of a package, or of the VBA library | A new tab with the package's source. The tab is read-only. | | A keyword, or text in a string | Nowhere. | -> [!NOTE] -> In twinBASIC BETA 1005, Ctrl+Up and Ctrl+Down, and Go To Definition for most names, land on the line after the declaration, not on the declaration itself. Press Up once to reach it. +> [!NOTE] BETA 1005 +> Ctrl+Up and Ctrl+Down, and Go To Definition for most names, land on the line after the declaration, not on the declaration itself. Press Up once to reach it. Ctrl+click does not follow a name, and the editor has no Peek Definition. @@ -99,8 +99,8 @@ The **OUTLINE** panel lists the symbols of the file you are editing. It is empty ![The Outline pane listing the two top level items of the open file, the module MainModule and the class GlobalSearchAddIn.](../Images/Outline_1.png){:width="300" height="96"} -> [!NOTE] -> In twinBASIC BETA 1005, the Outline lists only the top-level types of a file, such as the module or the classes. The procedures, fields and properties inside them are missing, rows have no expand arrow, and the filters of the **...** menu below change nothing you can see. To go to a procedure, use the [combo boxes](Editor#the-navigation-combo-boxes). +> [!NOTE] BETA 1005 +> The Outline lists only the top-level types of a file, such as the module or the classes. The procedures, fields and properties inside them are missing, rows have no expand arrow, and the filters of the **...** menu below change nothing you can see. To go to a procedure, use the [combo boxes](Editor#the-navigation-combo-boxes). The **...** button in the header of the panel opens a menu of eleven items that you tick. The IDE keeps them for each project. diff --git a/docs/IDE/Customising/Keyboard-Shortcuts.md b/docs/IDE/Customising/Keyboard-Shortcuts.md index c097fb8d0..f7b9cc948 100644 --- a/docs/IDE/Customising/Keyboard-Shortcuts.md +++ b/docs/IDE/Customising/Keyboard-Shortcuts.md @@ -57,8 +57,8 @@ The **Edit as JSON** box is ticked. If you clear it, the pane shows only the tex Text that is not valid JSON turns red. The IDE ignores a group whose text is not valid when it merges the groups. -> [!WARNING] -> In twinBASIC BETA 1005, a group whose name contains a vertical bar (`|`) can break the list of groups that are on. The IDE saves the list as the names, each between two bars, such as `|Mine||Other|`. A name with a bar inside it can match part of another name. For example, with groups named `A|B` and `B` both on, clicking `B` in the menu leaves it ticked, and clicking it again switches off both groups, and leaves a broken entry in the saved list. Nothing warns you. Do not use `|` in a group name. +> [!WARNING] BETA 1005 +> A group whose name contains a vertical bar (`|`) can break the list of groups that are on. The IDE saves the list as the names, each between two bars, such as `|Mine||Other|`. A name with a bar inside it can match part of another name. For example, with groups named `A|B` and `B` both on, clicking `B` in the menu leaves it ticked, and clicking it again switches off both groups, and leaves a broken entry in the saved list. Nothing warns you. Do not use `|` in a group name. ## The text of a group diff --git a/docs/IDE/Customising/Themes.md b/docs/IDE/Customising/Themes.md index 45745259d..99498b63d 100644 --- a/docs/IDE/Customising/Themes.md +++ b/docs/IDE/Customising/Themes.md @@ -64,11 +64,11 @@ MenuBackColor: red; SymbolClassTextDecoration: underline; ``` -> [!WARNING] -> In twinBASIC BETA 1005, a file named `Dark.theme` or `Light.theme` in `%APPDATA%\twinBASIC\themes` breaks the inheritance of every custom theme. The IDE ignores the file's contents, but still counts it, and so it never links the themes to their parents. Every custom theme, and **Classic (Light)** with them, is listed as `(undefined)` in place of **Light** or **Dark**. Choosing one gives the dark base with only that theme's own properties changed, and **Reload from disk** no longer applies the edits you make. No error is shown. To change one of the shipped themes, give your file another name and inherit from the shipped theme: `inherits: dark;`. +> [!WARNING] BETA 1005 +> A file named `Dark.theme` or `Light.theme` in `%APPDATA%\twinBASIC\themes` breaks the inheritance of every custom theme. The IDE ignores the file's contents, but still counts it, and so it never links the themes to their parents. Every custom theme, and **Classic (Light)** with them, is listed as `(undefined)` in place of **Light** or **Dark**. Choosing one gives the dark base with only that theme's own properties changed, and **Reload from disk** no longer applies the edits you make. No error is shown. To change one of the shipped themes, give your file another name and inherit from the shipped theme: `inherits: dark;`. -> [!WARNING] -> In twinBASIC BETA 1005, two themes that inherit from each other freeze the IDE. For example, `LoopA` has the line `inherits: LoopB;`, and `LoopB` has `inherits: LoopA;`. When the IDE starts, or when you choose **Reload from disk**, it stops responding for several seconds. Choosing one of the two themes afterwards changes nothing on screen, because the IDE fails before it applies the theme. Never let a chain of `inherits:` lines return to a theme in it. To recover, remove the loop from the files and restart the IDE. +> [!WARNING] BETA 1005 +> Two themes that inherit from each other freeze the IDE. For example, `LoopA` has the line `inherits: LoopB;`, and `LoopB` has `inherits: LoopA;`. When the IDE starts, or when you choose **Reload from disk**, it stops responding for several seconds. Choosing one of the two themes afterwards changes nothing on screen, because the IDE fails before it applies the theme. Never let a chain of `inherits:` lines return to a theme in it. To recover, remove the loop from the files and restart the IDE. ### The format of a theme file diff --git a/docs/IDE/Debugging/Breakpoints-and-Stepping.md b/docs/IDE/Debugging/Breakpoints-and-Stepping.md index 0e711cada..0ddaecefc 100644 --- a/docs/IDE/Debugging/Breakpoints-and-Stepping.md +++ b/docs/IDE/Debugging/Breakpoints-and-Stepping.md @@ -87,8 +87,8 @@ A procedure in another file opens in the editor when you step into it, as a prev At the `End Sub` of a procedure whose local variables hold objects, a step goes into a frame named `{class_final_destruction}` and then into the `Class_Terminate` procedures of those objects. A step off the `End Sub` of the `Click` procedure of a form lets the program run on, and the form waits for input again. -> [!NOTE] -> In twinBASIC BETA 1005, once the program has stopped at a breakpoint, no step stops in a caller of the procedure it stopped in. Step Out, F8 at the `End Sub` and Step Over all run the caller on until the next breakpoint or the end of the program. Steps inside the procedure, and into the procedures it calls and back out to it, work. To stop in the caller, put a breakpoint there, or stop on the line of the call and step into the procedure. +> [!NOTE] BETA 1005 +> Once the program has stopped at a breakpoint, no step stops in a caller of the procedure it stopped in. Step Out, F8 at the `End Sub` and Step Over all run the caller on until the next breakpoint or the end of the program. Steps inside the procedure, and into the procedures it calls and back out to it, work. To stop in the caller, put a breakpoint there, or stop on the line of the call and step into the procedure. ## Set Next Statement @@ -168,8 +168,8 @@ Running any line in the console removes the error panel. The failing line stays - **Stop** does not end the program at an error. See [Traps](#traps). - **Search Online** opens a Google search in the default browser. The search text is `twinBASIC VB6`, the header and the description of the error. -> [!NOTE] -> In twinBASIC BETA 1005, **Search Online** puts the error text into the address as it is, without encoding it. An `&` in the description ends the search text at that point, so the search loses the rest, and a `"` ends the address. Copy the description from the panel and search for it in your browser instead. +> [!NOTE] BETA 1005 +> **Search Online** puts the error text into the address as it is, without encoding it. An `&` in the description ends the search text at that point, so the search loses the rest, and a `"` ends the address. Copy the description from the panel and search for it in your browser instead. ### Stepping on through the rest of the loop @@ -184,18 +184,18 @@ To run on to the end instead, correct the value and press F5, or pres Edit and Continue is not supported at the error panel either: when you change the code, the console writes `[EDIT & CONTINUE] not yet supported. Aborting.`. After you change the failing line, F5 starts a new run that runs the changed line. -> [!WARNING] -> In twinBASIC BETA 1005, an edit at the error panel does not abort the program, although the console says `Aborting`. The rest of the failing procedure is skipped, and the code after its call runs: its output appears in the console, and the run then ends. At a breakpoint the same edit does abort the program. Do not edit the code while the error panel is showing. End the run first, as the **Stop** warning under [Traps](#traps) describes, and then edit. +> [!WARNING] BETA 1005 +> An edit at the error panel does not abort the program, although the console says `Aborting`. The rest of the failing procedure is skipped, and the code after its call runs: its output appears in the console, and the run then ends. At a breakpoint the same edit does abort the program. Do not edit the code while the error panel is showing. End the run first, as the **Stop** warning under [Traps](#traps) describes, and then edit. ### Traps - **The panel disappears, and Ignore with it.** Running a line in the Debug Console, or clicking another procedure in the Call Stack, removes the panel. F5 brings it back only by running the failing line again. Set Next Statement onto the next line, then F5, does what **Ignore (Resume Next)** would have done. -> [!NOTE] -> In twinBASIC BETA 1005, a step key pressed on the failing line leaves a step waiting. After F8 on the failing line, **Ignore (Resume Next)** stops again on the next line instead of going on, and the next F5 is used up as well. Press F5 to go on, or use Set Next Statement as above instead of a step key. +> [!NOTE] BETA 1005 +> A step key pressed on the failing line leaves a step waiting. After F8 on the failing line, **Ignore (Resume Next)** stops again on the next line instead of going on, and the next F5 is used up as well. Press F5 to go on, or use Set Next Statement as above instead of a step key. -> [!WARNING] -> In twinBASIC BETA 1005, **Stop** at an error ends only the procedure that failed. The procedure that called it goes on from its next line: if `Sub Main` called `FillTable`, the rest of `Sub Main` still runs. The panel's **Stop**, the toolbar's **Stop** and **Run → End** all do this. At a failed `Debug.Assert`, **Stop** ends only the check, and the test goes on past it as if the check had passed. To end the whole run, first move the arrow to a later line with Ctrl+F9, then press **Stop**. Away from an error --- at a breakpoint, after a step, or after Set Next Statement --- **Stop** ends the run. +> [!WARNING] BETA 1005 +> **Stop** at an error ends only the procedure that failed. The procedure that called it goes on from its next line: if `Sub Main` called `FillTable`, the rest of `Sub Main` still runs. The panel's **Stop**, the toolbar's **Stop** and **Run → End** all do this. At a failed `Debug.Assert`, **Stop** ends only the check, and the test goes on past it as if the check had passed. To end the whole run, first move the arrow to a later line with Ctrl+F9, then press **Stop**. Away from an error --- at a breakpoint, after a step, or after Set Next Statement --- **Stop** ends the run. ## See Also diff --git a/docs/IDE/Debugging/Debug-Console.md b/docs/IDE/Debugging/Debug-Console.md index 75f41511a..712c383d6 100644 --- a/docs/IDE/Debugging/Debug-Console.md +++ b/docs/IDE/Debugging/Debug-Console.md @@ -95,14 +95,14 @@ The input row is one line. There is no way to enter a multi-line construct. - The row has the IntelliSense of the editor, in the mode that [IDE Options](../Customising/IDE-Options#intellisense-modes) sets, and Ctrl+Space opens the list. It has a line for signature help of its own. - A [keyboard shortcut](../Customising/Keyboard-Shortcuts#the-text-of-a-group) can run a line of code in the console: `tbDebugConsole_ExecuteCode('? 6*7')` is the one command that takes an argument. -> [!NOTE] -> In twinBASIC BETA 1005, the input row offers no completion for a name typed after `?` and a space, and Ctrl+Space there opens no list. Type the name without the space, as `?total`, or start the line with the name. +> [!NOTE] BETA 1005 +> The input row offers no completion for a name typed after `?` and a space, and Ctrl+Space there opens no list. Type the name without the space, as `?total`, or start the line with the name. -> [!NOTE] -> In twinBASIC BETA 1005, a space typed while the completion list is open accepts the list's first entry, and the space is lost. Typing `Print total` gives `Printertotal`, because the list offers `Printer` but not the keyword `Print`; `Call Touch(total)` and `Set obj = items` are changed in the same way. The console refuses the changed line. Press Escape to close the list before you type the space: the text you typed stays. +> [!NOTE] BETA 1005 +> A space typed while the completion list is open accepts the list's first entry, and the space is lost. Typing `Print total` gives `Printertotal`, because the list offers `Printer` but not the keyword `Print`; `Call Touch(total)` and `Set obj = items` are changed in the same way. The console refuses the changed line. Press Escape to close the list before you type the space: the text you typed stays. -> [!NOTE] -> In twinBASIC BETA 1005, `? p`, where `p` is a variable of a user-defined type, fails with `(compile error: codegen error; check for compilation errors)`, and the console prints a `[LINKER] compilation (codegen) error` line. The [Variables](Inspecting#variables) panel and a watch show the same variable, so read it there. +> [!NOTE] BETA 1005 +> `? p`, where `p` is a variable of a user-defined type, fails with `(compile error: codegen error; check for compilation errors)`, and the console prints a `[LINKER] compilation (codegen) error` line. The [Variables](Inspecting#variables) panel and a watch show the same variable, so read it there. ## Copy the output diff --git a/docs/IDE/Debugging/Inspecting.md b/docs/IDE/Debugging/Inspecting.md index 402e25705..13d248571 100644 --- a/docs/IDE/Debugging/Inspecting.md +++ b/docs/IDE/Debugging/Inspecting.md @@ -63,11 +63,11 @@ Double-click a value to edit it. A text box replaces the value. Enter - A row that is a folder, an object, a user-defined type or an array, has no text box. Expand it and edit its parts. - A line typed in the Debug Console, such as `s = "it's"`, changes a variable too, and the panel shows the change. -> [!WARNING] -> In twinBASIC BETA 1005, the edit box cuts a string at an apostrophe, and leaving the box changes the variable. With `s` holding `it's`, double-click `s`, and the box holds only `"it`. A click on another row, or Enter, with nothing typed, then makes `s` the empty string. Escape leaves it as it was. Do not edit a string that contains an apostrophe in the panel. Assign it in the [Debug Console](Debug-Console) instead. +> [!WARNING] BETA 1005 +> The edit box cuts a string at an apostrophe, and leaving the box changes the variable. With `s` holding `it's`, double-click `s`, and the box holds only `"it`. A click on another row, or Enter, with nothing typed, then makes `s` the empty string. Escape leaves it as it was. Do not edit a string that contains an apostrophe in the panel. Assign it in the [Debug Console](Debug-Console) instead. -> [!WARNING] -> In twinBASIC BETA 1005, a **Date** or **Double** row shows a wrong value for a while after you edit it. A **Date** edited to `#1/2/2020#` shows `#12:00:00 AM#`, and a **Double** edited to `7.25` shows `0`. The variable holds the value you typed. The row shows it correctly after the next line you run in the console, or at the next stop. To check the value now, run `? d` in the console, where `d` is the variable. +> [!WARNING] BETA 1005 +> A **Date** or **Double** row shows a wrong value for a while after you edit it. A **Date** edited to `#1/2/2020#` shows `#12:00:00 AM#`, and a **Double** edited to `7.25` shows `0`. The variable holds the value you typed. The row shows it correctly after the next line you run in the console, or at the next stop. To check the value now, run `? d` in the console, where `d` is the variable. ### What the tooltip shows @@ -145,8 +145,8 @@ A user-defined type shows `{user defined type, 8 bytes}` and one line for each f Pointing at a function call or at a property runs it, so a function with side effects changes the state of the program. -> [!WARNING] -> In twinBASIC BETA 1005, pointing at a call to a **Sub** runs the Sub, and the popup shows `t.Bump: 0`, a value that means nothing. The Sub, and any event handlers it raises, runs once for each hover. Do not point at the name of a procedure that changes the program's state while the program is paused. To read a variable that such a procedure uses, point at the variable. +> [!WARNING] BETA 1005 +> Pointing at a call to a **Sub** runs the Sub, and the popup shows `t.Bump: 0`, a value that means nothing. The Sub, and any event handlers it raises, runs once for each hover. Do not point at the name of a procedure that changes the program's state while the program is paused. To read a variable that such a procedure uses, point at the variable. When the program is not paused, the same pointing shows the compiler's usual hover text, which [IntelliSense](../Code/IntelliSense#hover) describes. diff --git a/docs/IDE/Debugging/Memory.md b/docs/IDE/Debugging/Memory.md index 667277ac8..8a1dd7042 100644 --- a/docs/IDE/Debugging/Memory.md +++ b/docs/IDE/Debugging/Memory.md @@ -38,13 +38,13 @@ The debugger evaluates the expression in the box as it does a watch, so the box - An **array**: the text is replaced with `VarPtr(`*expression*`)`, and the pane shows the array variable. - Any other result is a number, and **the number itself is the address**. The name of a **Long** variable therefore shows the memory at the address the variable *holds*, not the memory of the variable. That is what a **LongPtr** holding an address needs. For a variable's own bytes, enter `VarPtr(`*name*`)`. -> [!NOTE] -> In twinBASIC BETA 1005, a result that is not a whole number --- the name of a **Double**, a **Boolean** or a variable of a user-defined type --- gives no address and does not turn the box red. The rows stay as they were, and the DEBUG CONSOLE prints an *IDE internal error* such as `Uncaught SyntaxError: Cannot convert 1.5 to a BigInt`. Enter `VarPtr(`*name*`)` for such a variable. +> [!NOTE] BETA 1005 +> A result that is not a whole number --- the name of a **Double**, a **Boolean** or a variable of a user-defined type --- gives no address and does not turn the box red. The rows stay as they were, and the DEBUG CONSOLE prints an *IDE internal error* such as `Uncaught SyntaxError: Cannot convert 1.5 to a BigInt`. Enter `VarPtr(`*name*`)` for such a variable. The pane starts its first row at that address exactly, and the cell at the address takes the keyboard focus. An expression that cannot be evaluated --- an unknown name, an empty box, anything entered while the program is not paused --- turns the box red and leaves the rows as they were. -> [!NOTE] -> In twinBASIC BETA 1005, a negative number is taken as an address too. With -8, the first row is labelled `&HFFFFFFF8` and the rows after it `&H00000008`, `&H00000018` and so on. None of them can be read. +> [!NOTE] BETA 1005 +> A negative number is taken as an address too. With -8, the first row is labelled `&HFFFFFFF8` and the rows after it `&H00000008`, `&H00000018` and so on. None of them can be read. The table shows what each kind of expression gives in a 32-bit program paused after these lines: @@ -86,8 +86,8 @@ Below, `s` holds "Hello twinBASIC". After `s` is entered, the box reads `StrPtr( `??` in the value cells, and `·` in the text column, mark memory the program cannot read. -> [!NOTE] -> In twinBASIC BETA 1005, `??` and `·` are also what a cell shows while its bytes are still being read, so unreadable memory looks the same as memory not read yet. Memory that keeps showing `??` cannot be read. +> [!NOTE] BETA 1005 +> `??` and `·` are also what a cell shows while its bytes are still being read, so unreadable memory looks the same as memory not read yet. Memory that keeps showing `??` cannot be read. ## Display formats and columns @@ -106,8 +106,8 @@ The formats of two and four bytes show each value as a number, so the hexadecima The second list, **Cols 8**, **Cols 12**, **Cols 16**, **Cols 24** or **Cols 32**, sets the number of values in a row. A row therefore covers more bytes in the larger formats: 16 in **Byte (Hex)** at **Cols 16**, 32 in an **Integer** format and 64 in a **Long** format. The address column steps by the same amount, and is written as `&H` and eight hexadecimal digits. -> [!WARNING] -> In twinBASIC BETA 1005, the address column of a 64-bit program shows only the low 32 bits of each address. The bytes shown are those at the whole address, but an address of `&H4232CFF438` is labelled `&H32CFF438`, which is another address. Do not copy an address from the column in a 64-bit program; print it with `Hex(VarPtr(`*name*`))` instead. +> [!WARNING] BETA 1005 +> The address column of a 64-bit program shows only the low 32 bits of each address. The bytes shown are those at the whole address, but an address of `&H4232CFF438` is labelled `&H32CFF438`, which is another address. Do not copy an address from the column in a 64-bit program; print it with `Hex(VarPtr(`*name*`))` instead. The text column on the right always shows every byte of the row, one character each. A byte from 32 to 126 shows as its ASCII character, and any other byte as a dot. diff --git a/docs/IDE/Debugging/Running.md b/docs/IDE/Debugging/Running.md index 1eaafd606..d8cceb197 100644 --- a/docs/IDE/Debugging/Running.md +++ b/docs/IDE/Debugging/Running.md @@ -45,8 +45,8 @@ At a run-time error, **End** ends only the procedure that failed; see [Traps](Br In a form that is waiting for input, and in a loop that holds only `DoEvents`, there is no line of yours to stop at. The IDE opens a file of the VB package, `IdleMessageLoopBreak.twin`, and stops in its procedure `IdleMessageLoopBreakpoint`. With a form waiting for input, a line typed in the console works there. Press F5 and the program stops again in the same file. Press it once more to run on. -> [!WARNING] -> In twinBASIC BETA 1005, a program that is stopped by **Break** in a loop that holds only `DoEvents` ends with a native access violation as soon as the IDE evaluates anything. A line typed in the console does it, even `? 1`, and so does any watch, because the IDE evaluates every watch at every stop. The DEBUG CONSOLE prints `NATIVE EXCEPTION: ACCESS_VIOLATION`, and the IDE stays in the paused state, with the arrow on the VB package's line and **Stop** and the step commands enabled, although the program has ended. Do not use **Break** on a loop that holds only `DoEvents`. Put a statement in the loop and a [breakpoint](Breakpoints-and-Stepping#breakpoints) on it instead. If the IDE is already in that state, **Restart the compiler** in the toolbar puts it right. +> [!WARNING] BETA 1005 +> A program that is stopped by **Break** in a loop that holds only `DoEvents` ends with a native access violation as soon as the IDE evaluates anything. A line typed in the console does it, even `? 1`, and so does any watch, because the IDE evaluates every watch at every stop. The DEBUG CONSOLE prints `NATIVE EXCEPTION: ACCESS_VIOLATION`, and the IDE stays in the paused state, with the arrow on the VB package's line and **Stop** and the step commands enabled, although the program has ended. Do not use **Break** on a loop that holds only `DoEvents`. Put a statement in the loop and a [breakpoint](Breakpoints-and-Stepping#breakpoints) on it instead. If the IDE is already in that state, **Restart the compiler** in the toolbar puts it right. ## What Start runs @@ -69,8 +69,8 @@ Start shows no dialog for a compile error, and it opens no panel. The [DIAGNOSTI - **In a procedure that `Main` calls.** The console shows `[LINKER] compilation (codegen) error detected in 'Startup.Main' at line #42`, where 42 is the line of the call. The editor opens `Main`'s module and stops at its `Sub` line with the error panel, titled **ERROR OCCURRED**. Nothing in `Main` has run. - **In `Main` itself.** No panel opens. The console shows `[LINKER] compilation (codegen) error detected in '{temp_component}.{temp_procedure}' at line #1`, then `(compile error: codegen error; check for compilation errors)`, and the program is idle again. -> [!NOTE] -> In twinBASIC BETA 1005, the panel for a compile error gives the description `COMPILATION (CODEGEN) ERROR AT LINE #42. PLEASE REPORT THIS ERROR TO https://git.io/JMh6S`, and the line it names is the line of the call, not the line of the error. A compile error in your code is not a defect to report. Read the real error in the DIAGNOSTICS panel and fix it. +> [!NOTE] BETA 1005 +> The panel for a compile error gives the description `COMPILATION (CODEGEN) ERROR AT LINE #42. PLEASE REPORT THIS ERROR TO https://git.io/JMh6S`, and the line it names is the line of the call, not the line of the error. A compile error in your code is not a defect to report. Read the real error in the DIAGNOSTICS panel and fix it. ## Run Or Preview @@ -89,8 +89,8 @@ A preview is also a debug session. A breakpoint in `Form_Load` stops it, and a c - A caret at module level, outside a procedure: `RunCodeUnderCursor: failed to identify the procedure at the cursor position.` - With no editor active: `runCodeUnderCursor failed: no active text editor.` -> [!NOTE] -> In twinBASIC BETA 1005, F6 also gives `failed to identify the procedure at the cursor position.` when the caret is on the procedure's `End Sub` or `End Function` line, or on another of its lines at or past the column where that `End` line ends. A comment at the end of the `End` line moves that limit to where the comment starts. Nothing runs. Put the caret nearer the start of a line of the procedure. +> [!NOTE] BETA 1005 +> F6 also gives `failed to identify the procedure at the cursor position.` when the caret is on the procedure's `End Sub` or `End Function` line, or on another of its lines at or past the column where that `End` line ends. A comment at the end of the `End` line moves that limit to where the comment starts. Nothing runs. Put the caret nearer the start of a line of the procedure. The CodeLens **▶ run** link above a procedure runs it by another route: [CodeLens](../Code/IntelliSense#codelens) describes it. [Testing with Assert](../../../Tutorials/Testing-with-Assert) uses both. @@ -119,8 +119,8 @@ A `Shell` that runs from a line you type in the console makes a box too, with no - **Opening a form designer.** The IDE tells the compiler to end the program if it is busy. A form program that is waiting for input ends, with `form terminate` and no `Form_Unload`. A loop with no `DoEvents` keeps running, and so does a program that is paused. - **Editing the code.** See the next section. -> [!NOTE] -> In twinBASIC BETA 1005, an `End` statement in a procedure that LLVM compiles ends the run, and then the console shows `restarting from MEMORY []`: the IDE restarts the compiler and compiles the project again. The IDE recovers by itself. Remove the `End` statement, or compile the procedure without LLVM, to avoid the restart. +> [!NOTE] BETA 1005 +> An `End` statement in a procedure that LLVM compiles ends the run, and then the console shows `restarting from MEMORY []`: the IDE restarts the compiler and compiles the project again. The IDE recovers by itself. Remove the `End` statement, or compile the procedure without LLVM, to avoid the restart. ## Editing while the program runs diff --git a/docs/IDE/Forms/Arranging.md b/docs/IDE/Forms/Arranging.md index fc77468d2..e218431a7 100644 --- a/docs/IDE/Forms/Arranging.md +++ b/docs/IDE/Forms/Arranging.md @@ -50,11 +50,11 @@ None of the controls is the reference. The first control you clicked has no spec **Center (Horizontal)** and **Center (Vertical)** have no keys. **To Grid** has none either. It rounds the **Left** and **Top** of each selected control down to the grid, and rounds the size down too: a control 24 pixels high becomes 20 with a 10-pixel grid. It acts at once, even when **Align Controls To Grid** is off. -> [!NOTE] -> In twinBASIC BETA 1005, **Center (Horizontal)** and **Center (Vertical)** do not centre the controls on the middle of the selection. They centre each control on the left edge, or the top edge, of the selection. Three controls 100 pixels wide at **Left** 10, 140 and 250 all end at **Left** -40, their middles on 10, and may end outside their container. To line up the middles of a group, align the edges and then type the **Left** or **Top** values in [PROPERTIES](Properties). **Center In Container** centres a group in its container correctly at a zoom of 100 %. +> [!NOTE] BETA 1005 +> **Center (Horizontal)** and **Center (Vertical)** do not centre the controls on the middle of the selection. They centre each control on the left edge, or the top edge, of the selection. Three controls 100 pixels wide at **Left** 10, 140 and 250 all end at **Left** -40, their middles on 10, and may end outside their container. To line up the middles of a group, align the edges and then type the **Left** or **Top** values in [PROPERTIES](Properties). **Center In Container** centres a group in its container correctly at a zoom of 100 %. -> [!NOTE] -> In twinBASIC BETA 1005, each Alt+arrow also opens the IDE's menu bar when you release the keys. The controls are aligned all the same, and nothing is lost. If the menu bar is in the way, align with the **Format → Align** commands or the toolbar buttons instead. +> [!NOTE] BETA 1005 +> Each Alt+arrow also opens the IDE's menu bar when you release the keys. The controls are aligned all the same, and nothing is lost. If the menu bar is in the way, align with the **Format → Align** commands or the toolbar buttons instead. ## Make the controls the same size @@ -71,8 +71,8 @@ None of the controls is the reference. The first control you clicked has no spec A command changes nothing when the controls already have the same size. -> [!WARNING] -> In twinBASIC BETA 1005, **Make Same Size** changes the controls on the surface and marks the form changed, but it does not reach the saved form. If you press Ctrl+S, close the tab and open the form again, the controls have their old sizes. A later change, such as moving a control with an arrow key, saves the new sizes with it. After you use **Make Same Size**, make such a change before you save, or type the **Width** and **Height** values in [PROPERTIES](Properties) instead. +> [!WARNING] BETA 1005 +> **Make Same Size** changes the controls on the surface and marks the form changed, but it does not reach the saved form. If you press Ctrl+S, close the tab and open the form again, the controls have their old sizes. A later change, such as moving a control with an arrow key, saves the new sizes with it. After you use **Make Same Size**, make such a change before you save, or type the **Width** and **Height** values in [PROPERTIES](Properties) instead. ## Space the controls evenly @@ -100,15 +100,15 @@ For example, take three controls 100 pixels wide with **Left** 10, 140 and 250, The spacing commands compare positions that count from each control's own parent. Controls in different containers therefore give odd results. For example, **Remove** on a text box in a frame and a button on the form places the button at the text box's right edge as counted from the frame's corner, not from the form's. -> [!WARNING] -> In twinBASIC BETA 1005, **Horizontal Spacing** and **Vertical Spacing** move the controls on the surface, but do not mark the form changed and do not make an undo step. Ctrl+Z does not undo them. If you close the tab, the IDE asks no question, and the form opens again with the old positions. The new positions are saved only when a later real change follows, for example moving a control with an arrow key, because that change saves the whole surface. After you use these commands, make such a change before you save, and check the positions in [PROPERTIES](Properties). To go back, type the old **Left** or **Top** values there. +> [!WARNING] BETA 1005 +> **Horizontal Spacing** and **Vertical Spacing** move the controls on the surface, but do not mark the form changed and do not make an undo step. Ctrl+Z does not undo them. If you close the tab, the IDE asks no question, and the form opens again with the old positions. The new positions are saved only when a later real change follows, for example moving a control with an arrow key, because that change saves the whole surface. After you use these commands, make such a change before you save, and check the positions in [PROPERTIES](Properties). To go back, type the old **Left** or **Top** values there. ## Center in the container **Center In Container (Horizontally)** and **Center In Container (Vertically)** move the selection as a group, so that the group's bounding box is centred in the container the controls share. The controls keep their positions relative to one another. They work for one control or for many, and need all of them to have the same parent. If they do not, nothing moves and a notification says so. -> [!NOTE] -> In twinBASIC BETA 1005, **Center In Container** reads the width of the container as it is drawn, so at a zoom other than 100 % it moves the controls to the wrong place. At 50 %, a text box in a frame ended at **Left** -9.98 instead of 40, and the group on the form moved by -80 instead of 20. Set the zoom to 100 % before you use the command. +> [!NOTE] BETA 1005 +> **Center In Container** reads the width of the container as it is drawn, so at a zoom other than 100 % it moves the controls to the wrong place. At 50 %, a text box in a frame ended at **Left** -9.98 instead of 40, and the group on the form moved by -80 instead of 20. Set the zoom to 100 % before you use the command. ## Bring to front and send to back diff --git a/docs/IDE/Forms/Controls.md b/docs/IDE/Forms/Controls.md index f3a4aac50..af5025120 100644 --- a/docs/IDE/Forms/Controls.md +++ b/docs/IDE/Forms/Controls.md @@ -124,8 +124,8 @@ There are three ways to make an array: The last two give the new controls the highest **Index** plus one, and turn a lone original with **Index** -1 into index 0. Choosing an event of a member in the [EVENTS tab](Properties#events) inserts a handler with an `Index As Integer` argument, such as `Private Sub Command2_GotFocus(Index As Integer)`. -> [!NOTE] -> In twinBASIC BETA 1005, the box that asks about the control array comes twice when you give a control the name of a single control. The first **YES** sets the other control's **Index** to 0 and leaves the renamed control's name unchanged, and the same box comes again. The second **YES** renames the control to `X(1)`. Answer **YES** both times. **NO** puts the old name back. +> [!NOTE] BETA 1005 +> The box that asks about the control array comes twice when you give a control the name of a single control. The first **YES** sets the other control's **Index** to 0 and leaves the renamed control's name unchanged, and the same box comes again. The second **YES** renames the control to `X(1)`. Answer **YES** both times. **NO** puts the old name back. ## Names and code diff --git a/docs/IDE/Forms/Designer.md b/docs/IDE/Forms/Designer.md index f8dc2bce0..bd72011fe 100644 --- a/docs/IDE/Forms/Designer.md +++ b/docs/IDE/Forms/Designer.md @@ -40,8 +40,8 @@ The new files go into the folder that the Project Explorer has selected. The Pro Enter creates both files at once and opens the designer in a normal tab, not a preview tab. Esc, or an empty name, creates nothing. A name with a space is written in square brackets, such as `Class [My Form]`, both in the class and in the form's **Name** property. A Windows form, an MDI form and a report are bracketed for a punctuation mark too, such as the dot in `a.b`. A user control, a property page and a CustomControls form are bracketed for a space only. -> [!NOTE] -> In twinBASIC BETA 1005, the name box accepts a name that is not an identifier, such as `1bad`, and creates the files without a message. The class is then `Class 1bad`. +> [!NOTE] BETA 1005 +> The name box accepts a name that is not an identifier, such as `1bad`, and creates the files without a message. The class is then `Class 1bad`. What the new files hold depends on the kind: @@ -82,11 +82,11 @@ A file that the designer cannot use opens in one of three ways: - A file with a JSON error shows the notification *ERROR: bad JSON encountered when parsing tbform*. - A file without a `FormDesignerId` opens normally. The designer makes a new id for the session and does not write it to the file. -> [!NOTE] -> In twinBASIC BETA 1005, a read-only designer file still gets a tab, which is empty and selected. Close it. +> [!NOTE] BETA 1005 +> A read-only designer file still gets a tab, which is empty and selected. Close it. -> [!NOTE] -> In twinBASIC BETA 1005, a form file with a JSON error opens an empty designer with no controls, and PROPERTIES is empty. An edit there is refused, and Ctrl+S leaves the file as it was, so nothing is lost. Correct the JSON in a text editor outside the IDE, and open the form again. +> [!NOTE] BETA 1005 +> A form file with a JSON error opens an empty designer with no controls, and PROPERTIES is empty. An edit there is refused, and Ctrl+S leaves the file as it was, so nothing is lost. Correct the JSON in a text editor outside the IDE, and open the form again. ### The two files of a form @@ -110,8 +110,8 @@ From the code file, **View → Object Designer** and Shift+F7 [!NOTE] -> In twinBASIC BETA 1005, a folder whose name contains `.twin`, such as `v1.twin.forms`, breaks the switch from the code to the designer, and the notification above appears. The switch from the designer to the code still works. Open the designer from the Project Explorer, or rename the folder. +> [!NOTE] BETA 1005 +> A folder whose name contains `.twin`, such as `v1.twin.forms`, breaks the switch from the code to the designer, and the notification above appears. The switch from the designer to the code still works. Open the designer from the Project Explorer, or rename the folder. ## The parts of the window @@ -177,11 +177,11 @@ With a designer focused, the **View** menu shows **Zoom In** and **Zoom Out** in -> [!NOTE] -> In twinBASIC BETA 1005, **Zoom Out** stops at 50 %, although the box offers 25 %. Choose 25 % in the box. +> [!NOTE] BETA 1005 +> **Zoom Out** stops at 50 %, although the box offers 25 %. Choose 25 % in the box. -> [!WARNING] -> In twinBASIC BETA 1005, the arrow keys also move the selected controls while the zoom box has the focus. With a control selected, Up pressed twice there changes the zoom by two steps and moves the control up by two steps. The form is marked changed, each press is an undo step, and a save keeps the new position. Nothing on the screen says the control moved. Check the positions of the controls before you save. +> [!WARNING] BETA 1005 +> The arrow keys also move the selected controls while the zoom box has the focus. With a control selected, Up pressed twice there changes the zoom by two steps and moves the control up by two steps. The form is marked changed, each press is an undo step, and a save keeps the new position. Nothing on the screen says the control moved. Check the positions of the controls before you save. ## Lock controls @@ -193,8 +193,8 @@ The lock is stored under the control's name, so it follows a rename, and it cove ![A form with Show Outlines on, so that a dashed outline surrounds every control. The command button Command1 is selected and locked: a grey lock icon shows on it and its grabbers are grey. The other buttons are unlocked.](Images/Designer_Locked.png){:width="386" height="333"} -> [!WARNING] -> In twinBASIC BETA 1005, locking or unlocking controls does not mark the form changed. If you lock a control and close the tab, the IDE asks no question, and the lock is gone when you open the form again. The lock is saved only with the next real change. After you lock a control, make another change to the form, such as moving a control, before you save. +> [!WARNING] BETA 1005 +> Locking or unlocking controls does not mark the form changed. If you lock a control and close the tab, the IDE asks no question, and the lock is gone when you open the form again. The lock is saved only with the next real change. After you lock a control, make another change to the form, such as moving a control, before you save. ## Tab order @@ -216,11 +216,11 @@ A designer tab shows the dot for unsaved changes as soon as the form changes. [!NOTE] -> In twinBASIC BETA 1005, the tooltip of a form's entry in **HISTORY** ends with `[line: undefined]`, because a form change has no line. Nothing is lost. +> [!NOTE] BETA 1005 +> The tooltip of a form's entry in **HISTORY** ends with `[line: undefined]`, because a form change has no line. Nothing is lost. -> [!NOTE] -> In twinBASIC BETA 1005, a designer can be marked changed with no edit. Click a control, press Esc to select the form, and then click another control or only move the pointer over the form: the tab gets the dot, and closing asks to save. It does not happen every time. Every control is where it was, so saving or discarding gives the same form. Ctrl+Z then steps through two empty steps before it reaches your last real change. +> [!NOTE] BETA 1005 +> A designer can be marked changed with no edit. Click a control, press Esc to select the form, and then click another control or only move the pointer over the form: the tab gets the dot, and closing asks to save. It does not happen every time. Every control is where it was, so saving or discarding gives the same form. Ctrl+Z then steps through two empty steps before it reaches your last real change. ## See Also diff --git a/docs/IDE/Forms/Properties.md b/docs/IDE/Forms/Properties.md index 0f4e0eb59..ea771bc70 100644 --- a/docs/IDE/Forms/Properties.md +++ b/docs/IDE/Forms/Properties.md @@ -70,8 +70,8 @@ A property has the editor that suits its type. A value is committed as the table If the compiler is busy when you open a property page, the IDE says *Backend compiler seems to be busy. Check there isn't a modal dialog box open behind the IDE window.* -> [!NOTE] -> In twinBASIC BETA 1005, Ctrl+A does nothing in a property box. It does not select the text, and the selection of the controls on the form is unchanged. Text that you type is added to the text already in the box. +> [!NOTE] BETA 1005 +> Ctrl+A does nothing in a property box. It does not select the text, and the selection of the controls on the form is unchanged. Text that you type is added to the text already in the box. ### Colours @@ -125,8 +125,8 @@ For example, a button at **Left** 10 and **Top** 50 pixels, 100 pixels wide and A value you type is taken in the same unit. Typing **Left** 450 on that form moves the button to 30 pixels. A font size, **Opacity**, **DrawWidth** and other properties that are not positions show as they are. -> [!NOTE] -> In twinBASIC BETA 1005, the pane shows **Left**, **Top**, **Width** and **Height** of a control on a CustomControls form multiplied by 15, as if the form were in twips, but takes the value you type as pixels. A button at **Left** 20 pixels shows 300. Typing 40 moves it to 40 pixels, and the box then shows 600. Do not type the value the pane shows. Type pixels, as the readouts on the surface show them. +> [!NOTE] BETA 1005 +> The pane shows **Left**, **Top**, **Width** and **Height** of a control on a CustomControls form multiplied by 15, as if the form were in twips, but takes the value you type as pixels. A button at **Left** 20 pixels shows 300. Typing 40 moves it to 40 pixels, and the box then shows 600. Do not type the value the pane shows. Type pixels, as the readouts on the surface show them. ## See Also diff --git a/docs/IDE/Forms/Toolbox.md b/docs/IDE/Forms/Toolbox.md index 590096ed4..b963aedf8 100644 --- a/docs/IDE/Forms/Toolbox.md +++ b/docs/IDE/Forms/Toolbox.md @@ -52,8 +52,8 @@ The magnifier in the title bar opens a search box under it. Typing a printable c ![The Toolbox with its search box holding Command, Text, beside three tool icons, the pointer, a text box and a command button, and More components below them. The menu of the ... button is open at the right, holding Show Control Names.](Images/Toolbox_Search.png){:width="377" height="129"} -> [!NOTE] -> In twinBASIC BETA 1005, a **floating** Toolbox lists every tool when a report is open, 24 controls and **More components**, where the docked Toolbox lists seven. A tool that a report does not allow is refused when you use it, so nothing is lost. Dock the Toolbox, or choose only the seven tools that a report allows. +> [!NOTE] BETA 1005 +> A **floating** Toolbox lists every tool when a report is open, 24 controls and **More components**, where the docked Toolbox lists seven. A tool that a report does not allow is refused when you use it, so nothing is lost. Dock the Toolbox, or choose only the seven tools that a report allows. ## Add ActiveX controls and components diff --git a/docs/IDE/Menu/Format.md b/docs/IDE/Menu/Format.md index 32e41d16d..9b0240be4 100644 --- a/docs/IDE/Menu/Format.md +++ b/docs/IDE/Menu/Format.md @@ -16,8 +16,8 @@ The Format menu holds the commands that line up and order the controls selected Every command acts on the selection in a form designer, so every command is greyed out until a designer is open with a selection. The commands need different numbers of controls: **Align**, **Make Same Size** and the two spacing submenus need two or more, **Make Equal** three or more, and the other commands one or more. The same commands are in the designer's [right-click menu](../../Forms/Controls#the-right-click-menu). -> [!WARNING] -> In twinBASIC BETA 1005, the spacing commands and **Make Same Size** can leave the new positions or sizes out of the saved form. [Arranging controls](../../Forms/Arranging) says what to do instead. +> [!WARNING] BETA 1005 +> The spacing commands and **Make Same Size** can leave the new positions or sizes out of the saved form. [Arranging controls](../../Forms/Arranging) says what to do instead. ![The Format menu with no control selected, every command greyed out.](Images/Menu_Format.png){:width="232" height="281"} diff --git a/docs/IDE/Projects/Adding-Files.md b/docs/IDE/Projects/Adding-Files.md index 9b637ede7..17565e573 100644 --- a/docs/IDE/Projects/Adding-Files.md +++ b/docs/IDE/Projects/Adding-Files.md @@ -67,8 +67,8 @@ The new file goes into the selected folder, or into the folder of the selected f **Add Folder** and **Add Other File** are not moved to `Sources`: a file made with **Add Other File** while a file in `Resources/CUSTOM` is selected is made in `Resources/CUSTOM`. -> [!NOTE] -> In twinBASIC BETA 1005, **Add Other File**, and **Paste**, in the `Packages` folder create nothing. The notification says *cannot create files inside the root resources folder*, which names the wrong folder. A file that was cut and then pasted there is deleted: see [Cut, copy and paste](Files-and-Folders#cut-copy-and-paste). +> [!NOTE] BETA 1005 +> **Add Other File**, and **Paste**, in the `Packages` folder create nothing. The notification says *cannot create files inside the root resources folder*, which names the wrong folder. A file that was cut and then pasted there is deleted: see [Cut, copy and paste](Files-and-Folders#cut-copy-and-paste). ## What the new files hold @@ -102,8 +102,8 @@ The `.twin` files are written as UTF-8 with Windows line endings. The `.bas` and A file with any extension but the three below is copied into the project unchanged. It goes into the selected folder, or the folder of the selected file, or `Sources` if nothing is selected. The IDE does not ask when a file of the same name is there already: the new file replaces it. -> [!WARNING] -> In twinBASIC BETA 1005, importing a file with the name of a file already in the project crashes the compiler, whether or not the file is open. The Debug Console prints `NATIVE EXCEPTION: ACCESS_VIOLATION`, and the IDE restarts the compiler. When the file is open in a tab, the project then holds the imported text, but the tab keeps the old text. If you edit the tab and press Ctrl+S, the old text with your edit is saved over the import. Close the tab of a file before you import a file with its name. +> [!WARNING] BETA 1005 +> Importing a file with the name of a file already in the project crashes the compiler, whether or not the file is open. The Debug Console prints `NATIVE EXCEPTION: ACCESS_VIOLATION`, and the IDE restarts the compiler. When the file is open in a tab, the project then holds the imported text, but the tab keeps the old text. If you edit the tab and press Ctrl+S, the old text with your edit is saved over the import. Close the tab of a file before you import a file with its name. ### Projects and packages @@ -115,8 +115,8 @@ The tree shows the project's folders and files but not its `Packages`, `Settings **Import** writes each ticked file at the same path in the current project, and makes the folders for it. A file already at that path is replaced with no question. If the import fails, a box says **Import failed. Please check the DEBUG CONSOLE for details of the errors that occurred.** -> [!WARNING] -> In twinBASIC BETA 1005, importing a project's files replaces a file of the same path without a question, and closes the tab open on it first. Unsaved changes in that tab are lost, and so is the saved copy of the file, because the import also saves the project. Do not tick a file whose path is already in your project. +> [!WARNING] BETA 1005 +> Importing a project's files replaces a file of the same path without a question, and closes the tab open on it first. Unsaved changes in that tab are lost, and so is the saved copy of the file, because the import also saves the project. Do not tick a file whose path is already in your project. This is for taking files from another project. To open a whole VB6 project as a new twinBASIC project, use **File → Open Project...** on its `.vbp`, or the **Import from VBP...** and **Import from folder...** tiles of the [New Project](../Project/New) dialog. The [FAQ](../../../FAQ#vb6-import) describes the VB6 import. A package is added as a package in [Project Settings](References-and-Packages#available-packages), not here. @@ -124,8 +124,8 @@ This is for taking files from another project. To open a whole VB6 project as a When every file of the command is done, the IDE saves the project, as if you had pressed Ctrl+S. For a project with no file yet, the **Save As** dialog opens instead. Cancelling it leaves the file imported and the project unsaved. -> [!NOTE] -> In twinBASIC BETA 1005, an import saves the project, with the unsaved changes of every open editor, although you did not ask for a save. **Cancel** in the **Import from twinproj...** dialog does the same, and then the IDE restarts its compiler. +> [!NOTE] BETA 1005 +> An import saves the project, with the unsaved changes of every open editor, although you did not ask for a save. **Cancel** in the **Import from twinproj...** dialog does the same, and then the IDE restarts its compiler. ## Resources A resource is a file that the build embeds in the program, such as an icon, a bitmap, a manifest or a table of strings. The resources are in the `Resources` folder, in a subfolder named for the resource's type. A new Standard EXE project has `Resources/ICON/twinBASIC.ico`. A project that uses Visual Styles also has a `MANIFEST` folder. The `STRING` and `MESSAGETABLE` folders appear when you add those resources. diff --git a/docs/IDE/Projects/Files-and-Folders.md b/docs/IDE/Projects/Files-and-Folders.md index fe42cacfb..062d32e2a 100644 --- a/docs/IDE/Projects/Files-and-Folders.md +++ b/docs/IDE/Projects/Files-and-Folders.md @@ -64,8 +64,8 @@ A name is refused, with a notification in the corner of the window, when it: The notification reads **Cannot rename: the new name entered is invalid.** The seven special folders, `Settings`, `Sources`, `Resources`, `References`, `Packages`, `Miscellaneous` and `ImportedTypeLibraries`, cannot be renamed at all: the notification is **Cannot rename: permission denied.** -> [!NOTE] -> In twinBASIC BETA 1005, a rename that changes only the case of a name, or nothing, is refused with the same message as an invalid name, so `legacy.bas` cannot be changed to `Legacy.bas` in the tree. +> [!NOTE] BETA 1005 +> A rename that changes only the case of a name, or nothing, is refused with the same message as an invalid name, so `legacy.bas` cannot be changed to `Legacy.bas` in the tree. If the file is open in a tab with changes you have not saved, the IDE refuses the rename and shows a box, **Rename/Move failed. Please close the following editors that are preventing the operation:**, with the paths of the files. The same box appears for a folder that holds such a file. Save the file, or close its tab, and rename again. A tab on a file with no unsaved changes does not stop the rename. The tab closes, and the IDE does not open it again under the new name. Bookmarks move with the file. @@ -79,8 +79,8 @@ A form's designer file and its `.twin` file belong together. After you rename on The top row of the tree is the project. Renaming it changes the **Project Name** setting in the `Settings` file. The row, the title bar and the `.twinproj` file keep their names. A name that is not a valid project name gives **Cannot rename the project: the new name entered is invalid.** -> [!NOTE] -> In twinBASIC BETA 1005, renaming the project also saves it, with the unsaved edits of the open editors, and the title bar's changed mark goes. +> [!NOTE] BETA 1005 +> Renaming the project also saves it, with the unsaved edits of the open editors, and the title bar's changed mark goes. ## Move a file or folder by dragging @@ -91,11 +91,11 @@ Drag a row, or the selected rows, onto a folder. The folder under the pointer is - The `Resources` folder accepts a dropped file, although **Paste** refuses to put one there. - A file dragged in from Windows is not accepted. -> [!WARNING] -> In twinBASIC BETA 1005, dragging a folder onto one of its own files, or onto one of its own subfolders, is accepted with no message. The tree then nests the folder inside itself, about fifty levels deep. The next Ctrl+S prints the green `[DONE]` line in the Debug Console, and then an error, and it writes a project file that the IDE cannot open: opening it stops at *Please wait... 25.0%*. Never drop a folder onto anything inside it. If you did, do not save. Close the project with **Discard Changes** in the question that follows, and open it again. +> [!WARNING] BETA 1005 +> Dragging a folder onto one of its own files, or onto one of its own subfolders, is accepted with no message. The tree then nests the folder inside itself, about fifty levels deep. The next Ctrl+S prints the green `[DONE]` line in the Debug Console, and then an error, and it writes a project file that the IDE cannot open: opening it stops at *Please wait... 25.0%*. Never drop a folder onto anything inside it. If you did, do not save. Close the project with **Discard Changes** in the question that follows, and open it again. -> [!NOTE] -> In twinBASIC BETA 1005, nothing stops a drag of a special folder. Dragging `Sources` onto `Miscellaneous` moves it there with no message, and the next save keeps it there. Do not drag the special folders. A drop of a file on `Packages` does nothing. +> [!NOTE] BETA 1005 +> Nothing stops a drag of a special folder. Dragging `Sources` onto `Miscellaneous` moves it there with no message, and the next save keeps it there. Do not drag the special folders. A drop of a file on `Packages` does nothing. ## Cut, copy and paste @@ -107,11 +107,11 @@ Drag a row, or the selected rows, onto a folder. The folder under the pointer is - The menu greys out **Cut** for a read-only row, such as a file of a package. Ctrl+X on a `References` entry still puts a cut on the clipboard. - The menu greys out **Cut** and **Copy** for a folder. Ctrl+C on a folder still puts its path on the clipboard. -> [!WARNING] -> In twinBASIC BETA 1005, a file that you cut and paste onto the `Resources` folder itself, or onto `Packages`, is deleted. The IDE shows the notification *cannot create files inside the root resources folder*, which does not say so. The file is gone from the whole tree, and the next save writes the project without it. Do not cut a file into either folder. If a file went, close the project with **Discard Changes** before you save, and the saved project still has it. +> [!WARNING] BETA 1005 +> A file that you cut and paste onto the `Resources` folder itself, or onto `Packages`, is deleted. The IDE shows the notification *cannot create files inside the root resources folder*, which does not say so. The file is gone from the whole tree, and the next save writes the project without it. Do not cut a file into either folder. If a file went, close the project with **Discard Changes** before you save, and the saved project still has it. -> [!NOTE] -> In twinBASIC BETA 1005, pasting a file with no extension into a folder whose own name contains a period, such as `v1.2`, creates nothing and gives no message, when a file of that name is in the folder already. The IDE takes the file's extension from the last period of the whole path. +> [!NOTE] BETA 1005 +> Pasting a file with no extension into a folder whose own name contains a period, such as `v1.2`, creates nothing and gives no message, when a file of that name is in the folder already. The IDE takes the file's extension from the last period of the whole path. ## Export a file @@ -153,14 +153,14 @@ With the option off, the rows are deleted at once. Some folders cannot be deleted. For `Sources`, `References`, `Miscellaneous`, `ImportedTypeLibraries` and the compiler packages under `Packages`, the box is shown, but nothing is deleted and no message follows. -> [!WARNING] -> In twinBASIC BETA 1005, a file with unsaved changes in an open tab is deleted without a word about the changes. The box is the plain one, the tab closes, and the changes go with the file. A folder that holds such a file is the same. Look for the dot on a tab before you delete its file. The saved copy stays in the `.twinproj` file until the next save, so closing the project with **Discard Changes** brings the file back. +> [!WARNING] BETA 1005 +> A file with unsaved changes in an open tab is deleted without a word about the changes. The box is the plain one, the tab closes, and the changes go with the file. A folder that holds such a file is the same. Look for the dot on a tab before you delete its file. The saved copy stays in the `.twinproj` file until the next save, so closing the project with **Discard Changes** brings the file back. -> [!WARNING] -> In twinBASIC BETA 1005, deleting the project's own row, the top row of the tree, deletes every file in the project. The box reads like a file's: *Are you sure you want to delete* the project's path. After you confirm, the tree is empty, and the only message is an *IDE internal error* line in the Debug Console. The next Ctrl+S prints the green `[DONE]` line and writes a project with no files over the project file. Never delete the top row. If you did, do not save: close the project with **Discard Changes** and open it again. +> [!WARNING] BETA 1005 +> Deleting the project's own row, the top row of the tree, deletes every file in the project. The box reads like a file's: *Are you sure you want to delete* the project's path. After you confirm, the tree is empty, and the only message is an *IDE internal error* line in the Debug Console. The next Ctrl+S prints the green `[DONE]` line and writes a project with no files over the project file. Never delete the top row. If you did, do not save: close the project with **Discard Changes** and open it again. -> [!NOTE] -> In twinBASIC BETA 1005, Delete with nothing selected shows the box **Are you sure you want to delete the selected items?**, and deletes nothing. Choose **Cancel**. Deleting an entry under `References` removes its row from the tree, but not the reference: it stays in the project's settings. Remove a reference in [Project Settings](References-and-Packages#the-enabled-libraries-list). +> [!NOTE] BETA 1005 +> Delete with nothing selected shows the box **Are you sure you want to delete the selected items?**, and deletes nothing. Choose **Cancel**. Deleting an entry under `References` removes its row from the tree, but not the reference: it stays in the project's settings. Remove a reference in [Project Settings](References-and-Packages#the-enabled-libraries-list). ## See Also diff --git a/docs/IDE/Projects/Project-Explorer.md b/docs/IDE/Projects/Project-Explorer.md index 318ea3fa6..f5fc6a49e 100644 --- a/docs/IDE/Projects/Project-Explorer.md +++ b/docs/IDE/Projects/Project-Explorer.md @@ -100,8 +100,8 @@ These keys work while the tree has the focus. The tree scrolls to keep the selected row in view. Page Up, Page Down, Home and End do not move the selection. -> [!NOTE] -> In twinBASIC BETA 1005, several keys of the tree do not work as they should. +> [!NOTE] BETA 1005 +> Several keys of the tree do not work as they should. > > - Space does not open or close a folder. Click the folder's plus or minus box. > - Typing the start of a name does not filter the tree, although the IDE has code for it. To find a file, open the folders, or switch to the [object view](#the-object-view) and search for the object by name. To find text, use [Find In Project](../Code/Finding#search-the-whole-project). @@ -147,8 +147,8 @@ The **Search** button, or the first letter you type with a row selected, opens a -> [!NOTE] -> In twinBASIC BETA 1005, the object view has some defects. +> [!NOTE] BETA 1005 +> The object view has some defects. > > - A right click on a row opens no menu, so the menu's **View Object**, **View Code**, **Add**, **Rename** and **Delete** cannot be reached. To rename or delete an object's file, press Ctrl+R to return to the file view. > - Shift+double-click on a form opens its designer, like a plain double-click, and not its code. To open the code, switch to the file view and open the form's `.twin` file. diff --git a/docs/IDE/Projects/References-and-Packages.md b/docs/IDE/Projects/References-and-Packages.md index b3c4bf155..86c4d49bf 100644 --- a/docs/IDE/Projects/References-and-Packages.md +++ b/docs/IDE/Projects/References-and-Packages.md @@ -105,14 +105,14 @@ The **Import from file...** button under the lists opens the Windows **Open** di - On **Available COM References**, and on **Enabled Libraries**, it asks for a type library. The IDE copies the file into the project as `ImportedTypeLibraries/`, and adds it to the references at once, ticked. - On **Available Packages**, it asks for a `.twinpack` file. The package is added to the **Available Packages** list. [Importing a Package from a TWINPACK File](../../../Features/Packages/Importing-TWINPACK) gives the steps. -> [!NOTE] -> In twinBASIC BETA 1005, a package imported with **Import from file...** is listed but not ticked, so the project does not use it. Tick it yourself after the import. +> [!NOTE] BETA 1005 +> A package imported with **Import from file...** is listed but not ticked, so the project does not use it. Tick it yourself after the import. -> [!NOTE] -> In twinBASIC BETA 1005, **Import from file...** puts a copy of the package under the project's `Packages` folder. Closing the dialog with **Cancel** leaves the copy there, and the project is not marked as changed. +> [!NOTE] BETA 1005 +> **Import from file...** puts a copy of the package under the project's `Packages` folder. Closing the dialog with **Cancel** leaves the copy there, and the project is not marked as changed. -> [!NOTE] -> In twinBASIC BETA 1005, a type library added with **Import from file...** is drawn on the **Enabled Libraries** list as `[LINKED-PACKAGE]`, and on **Available COM References** it is listed twice, once from the project and once from the registered library, both ticked. Clearing the tick of either row removes the reference. +> [!NOTE] BETA 1005 +> A type library added with **Import from file...** is drawn on the **Enabled Libraries** list as `[LINKED-PACKAGE]`, and on **Available COM References** it is listed twice, once from the project and once from the registered library, both ticked. Clearing the tick of either row removes the reference. ## Package needed @@ -120,8 +120,8 @@ Choosing an **Add** item for a Windows form without the **WinNativeForms** packa The reference is listed under the package's own name, `WinNativeForms` or `CustomControlsPackage`, until the compiler has answered, and then under the name of the library, such as `twinBASIC - VB Compatibility Package (Forms)`. -> [!NOTE] -> In twinBASIC BETA 1005, in a project that has a file, the button also saves the project, although the box speaks only of restarting the compiler. A project with no file yet is not saved, and restarts from memory. +> [!NOTE] BETA 1005 +> In a project that has a file, the button also saves the project, although the box speaks only of restarting the compiler. A project with no file yet is not saved, and restarts from memory. ## Apply Changes and closing the dialog @@ -131,8 +131,8 @@ The dialog has a filter box, **Apply Changes** and **Cancel**. **Apply Changes** -> [!WARNING] -> In twinBASIC BETA 1005, the close button of the title bar does not ask when the only change is to the references. The dialog closes, the change is dropped, and nothing says so. The code that compares the settings leaves out the compiler warnings as well. Press **Apply Changes** before you close the dialog. +> [!WARNING] BETA 1005 +> The close button of the title bar does not ask when the only change is to the references. The dialog closes, the change is dropped, and nothing says so. The code that compares the settings leaves out the compiler warnings as well. Press **Apply Changes** before you close the dialog. ## The References folder @@ -140,8 +140,8 @@ The `References` folder of the Project Explorer has one entry for each library t **Cut**, **Rename** and **Delete Permanently** are greyed out on the right-click menu for these entries. -> [!NOTE] -> In twinBASIC BETA 1005, the Delete key removes a `References` entry from the tree, although its menu item is greyed out. The reference itself stays in the project. To remove a reference, untick it in the **Enabled Libraries** list. +> [!NOTE] BETA 1005 +> The Delete key removes a `References` entry from the tree, although its menu item is greyed out. The reference itself stays in the project. To remove a reference, untick it in the **Enabled Libraries** list. ## Where the project keeps them diff --git a/docs/IDE/Projects/Saving-and-Exporting.md b/docs/IDE/Projects/Saving-and-Exporting.md index 505821136..fa76fe65b 100644 --- a/docs/IDE/Projects/Saving-and-Exporting.md +++ b/docs/IDE/Projects/Saving-and-Exporting.md @@ -52,8 +52,8 @@ The compiler can also ask the IDE for another path: a box, **Failed to save the Some commands save the project besides **Save Project**: **Save Project As** saves after it chooses the path, and **Apply Changes** in [Project Settings](References-and-Packages#apply-changes-and-closing-the-dialog) saves when the compiler restarts. With the setting [Export After Save](../Project-Settings#export-after-save), a save runs **Export Project** first. -> [!NOTE] -> In twinBASIC BETA 1005, several actions save the project although you did not ask. They are [**Import...**](Adding-Files#the-project-is-saved) of ordinary files; the **Import** and **Cancel** buttons of the **Import from twinproj...** dialog; [renaming the project](Files-and-Folders#the-project); and the button of a [Package needed](References-and-Packages#package-needed) box in a project that has a file. Importing ordinary files, **Cancel** in the import dialog and renaming the project write the unsaved changes of the open editors into the file as well. +> [!NOTE] BETA 1005 +> Several actions save the project although you did not ask. They are [**Import...**](Adding-Files#the-project-is-saved) of ordinary files; the **Import** and **Cancel** buttons of the **Import from twinproj...** dialog; [renaming the project](Files-and-Folders#the-project); and the button of a [Package needed](References-and-Packages#package-needed) box in a project that has a file. Importing ordinary files, **Cancel** in the import dialog and renaming the project write the unsaved changes of the open editors into the file as well. ## Save Project As @@ -120,8 +120,8 @@ To write only some of the files, use [**Export...**](Files-and-Folders#export-a- With [*Export After Save*](../Project-Settings#export-after-save) set, every save runs the export again before it writes the project, into *Export Path*, and empties that folder again. -> [!NOTE] -> In twinBASIC BETA 1005, when the `Settings` file has an *Export Path* but no `project.exportPathIsV2` entry, the project's name is joined to the path with no separator: `${SourcePath}\exp2` exports into `exp2ProjTry\` for a project named `ProjTry`. +> [!NOTE] BETA 1005 +> When the `Settings` file has an *Export Path* but no `project.exportPathIsV2` entry, the project's name is joined to the path with no separator: `${SourcePath}\exp2` exports into `exp2ProjTry\` for a project named `ProjTry`. ### When a file cannot be deleted diff --git a/docs/IDE/Reports/index.md b/docs/IDE/Reports/index.md index 9108d89ab..238639d65 100644 --- a/docs/IDE/Reports/index.md +++ b/docs/IDE/Reports/index.md @@ -65,8 +65,8 @@ A report holds windowless controls only. Its Toolbox lists seven tools: **Pointe A control belongs to the section where you draw it. A double-click on a tool places the control in the current section, and a control that you paste goes into the section that is shown. Drawing or pasting any other control gives the notification *Can't add* the control's type *to a report. Only windowless controls allowed.*, and adds nothing. -> [!NOTE] -> In twinBASIC BETA 1005, a floating Toolbox lists all 24 controls and **More components** when a report is open. A control that a report does not allow is refused when you use it, so nothing is lost. Dock the Toolbox, or use only the seven tools above. +> [!NOTE] BETA 1005 +> A floating Toolbox lists all 24 controls and **More components** when a report is open. A control that a report does not allow is refused when you use it, so nothing is lost. Dock the Toolbox, or use only the seven tools above. A report has no **Form Menu** bar and no control arrays, and its controls have no **Index** property. The other designer features work: selection, moving and sizing, the grid, copy and paste, locks, undo, zoom, the right-click menu and the [Format](../Forms/Arranging) commands. diff --git a/docs/LLVM/Getting-Started.md b/docs/LLVM/Getting-Started.md index d7fd1600b..84e49dcef 100644 --- a/docs/LLVM/Getting-Started.md +++ b/docs/LLVM/Getting-Started.md @@ -9,8 +9,8 @@ permalink: /LLVM/Getting-Started Covers turning on LLVM compilation for a project or for a single procedure, the options that control it, and its current limitations. -> [!NOTE] -> This page describes twinBASIC BETA 984 and later. Earlier builds have no LLVM settings in Project Settings or IDE Options. +> [!NOTE] since BETA 984 +> Everything on this page is in twinBASIC since this build. Earlier builds have no LLVM settings in Project Settings or IDE Options. ## What is LLVM? @@ -57,17 +57,17 @@ Building with LLVM does not currently work on Windows 7; Windows 10 or 11 is rec The main feature not yet supported is passing an error up to the calling procedure. A fix is planned. -> [!WARNING] -> In twinBASIC BETA 1005, an error that occurs in a procedure with no error handler is not passed to its caller when either of the two procedures is compiled with LLVM: the caller's own `On Error` statement does not catch it. A run in the IDE ends without a message. A built program stops as it does for an unhandled error when only the caller is compiled with LLVM. When the procedure that has no handler is compiled with LLVM, the program shows a box that names the procedure but no error, and then ends with an access violation. Without LLVM, and in VB6, the caller's handler catches the error. +> [!WARNING] BETA 1005 +> An error that occurs in a procedure with no error handler is not passed to its caller when either of the two procedures is compiled with LLVM: the caller's own `On Error` statement does not catch it. A run in the IDE ends without a message. A built program stops as it does for an unhandled error when only the caller is compiled with LLVM. When the procedure that has no handler is compiled with LLVM, the program shows a box that names the procedure but no error, and then ends with an access violation. Without LLVM, and in VB6, the caller's handler catches the error. -> [!NOTE] -> In twinBASIC BETA 1005, an error handler in a procedure compiled with LLVM cannot read an error raised with [**Err.Raise**](../tB/Modules/ErrObject/Raise) in that procedure: **Err.Number** returns -353703423 (`&HEAEAEA01`), **Err.Source** is empty, and **Err.Description** is the generic "Application-defined or object-defined error". An error that a statement causes, such as a division by zero, is read correctly. +> [!NOTE] BETA 1005 +> An error handler in a procedure compiled with LLVM cannot read an error raised with [**Err.Raise**](../tB/Modules/ErrObject/Raise) in that procedure: **Err.Number** returns -353703423 (`&HEAEAEA01`), **Err.Source** is empty, and **Err.Description** is the generic "Application-defined or object-defined error". An error that a statement causes, such as a division by zero, is read correctly. -> [!WARNING] -> In twinBASIC BETA 1005, a **Double** addition, subtraction, multiplication or exponentiation (`^`) that overflows raises no error in code compiled with LLVM: the result is infinity (`1.#INF`), where the same code compiled without LLVM raises error 6, *Overflow*, as VB6 does. [**FloatingPointErrorChecks(True)**](../tB/Core/Attributes#floatingpointerrorchecks) does not change this. A **Double** division that overflows, and a division by zero, still raise their errors. After either, under `On Error Resume Next`, the variable that was to receive the result keeps its old value; without LLVM it holds infinity. +> [!WARNING] BETA 1005 +> A **Double** addition, subtraction, multiplication or exponentiation (`^`) that overflows raises no error in code compiled with LLVM: the result is infinity (`1.#INF`), where the same code compiled without LLVM raises error 6, *Overflow*, as VB6 does. [**FloatingPointErrorChecks(True)**](../tB/Core/Attributes#floatingpointerrorchecks) does not change this. A **Double** division that overflows, and a division by zero, still raise their errors. After either, under `On Error Resume Next`, the variable that was to receive the result keeps its old value; without LLVM it holds infinity. -> [!WARNING] -> In twinBASIC BETA 1005, code compiled with LLVM rounds a **Single** or **Double** that is exactly halfway between two integers away from zero when it converts the value to an integer type: in [**CLng**](../tB/Modules/Conversion/CLng), **CInt** and **CLngLng** of a variable, in an assignment to an **Integer** or **Long**, and in the operands of [**\\**](../tB/Core/IntegerDivide). With a **Double** variable holding 2.5, `CLng(x)` is 3, and with -0.5 it is -1. Without LLVM the value is rounded to the even integer, 2 and 0, as VB6 does. A constant expression such as `CLng(2.5)`, a **Variant** argument, and [**Round**](../tB/Modules/Math/Round), **Int** and **Fix** are not affected. +> [!WARNING] BETA 1005 +> Code compiled with LLVM rounds a **Single** or **Double** that is exactly halfway between two integers away from zero when it converts the value to an integer type: in [**CLng**](../tB/Modules/Conversion/CLng), **CInt** and **CLngLng** of a variable, in an assignment to an **Integer** or **Long**, and in the operands of [**\\**](../tB/Core/IntegerDivide). With a **Double** variable holding 2.5, `CLng(x)` is 3, and with -0.5 it is -1. Without LLVM the value is rounded to the even integer, 2 and 0, as VB6 does. A constant expression such as `CLng(2.5)`, a **Variant** argument, and [**Round**](../tB/Modules/Math/Round), **Int** and **Fix** are not affected. Code compiled with LLVM ignores [**Debug.Assert**](../tB/Modules/Debug#assert): the statement does not stop, and its condition is not evaluated. diff --git a/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md b/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md index 2e694db73..87b05762a 100644 --- a/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md +++ b/docs/Reference/Built-In/WebView2/WebView2HeadersCollection.md @@ -14,8 +14,8 @@ The collection implements the COM enumerator operations **Next**, **Skip**, **Re **Clone** returns an enumerator at the position of the enumerator it copies. -> [!WARNING] -> In BETA 1005, **Clone** on an enumerator of a [**WebView2ResponseHeaders**](WebView2ResponseHeaders) collection that has been **Reset** starts as many headers in as the enumerator had read before the **Reset**: after two **Next** calls and a **Reset**, the enumerator's **Next** returns the first header and the clone's **Next** the third. An enumerator of [**WebView2RequestHeaders**](WebView2RequestHeaders) is not affected. `For Each` never calls **Clone**, so only a COM client that does is affected. +> [!WARNING] BETA 1005 +> **Clone** on an enumerator of a [**WebView2ResponseHeaders**](WebView2ResponseHeaders) collection that has been **Reset** starts as many headers in as the enumerator had read before the **Reset**: after two **Next** calls and a **Reset**, the enumerator's **Next** returns the first header and the clone's **Next** the third. An enumerator of [**WebView2RequestHeaders**](WebView2RequestHeaders) is not affected. `For Each` never calls **Clone**, so only a COM client that does is affected. A **WebView2HeadersCollection** is never created by application code. The package returns one of the four derived classes listed under [Derived classes](#derived-classes), typed as **WebView2HeadersCollection**. diff --git a/docs/Reference/Built-In/tbIDE/FileSystemItem.md b/docs/Reference/Built-In/tbIDE/FileSystemItem.md index 4ed76e197..3dc21d042 100644 --- a/docs/Reference/Built-In/tbIDE/FileSystemItem.md +++ b/docs/Reference/Built-In/tbIDE/FileSystemItem.md @@ -37,8 +37,8 @@ The item's name (the last segment of its [**Path**](#path)). **String**, read-on The folder that contains this item. **As** [**Folder**](Folder). Read-only. The **Parent** of the project's folder, [**Project.RootFolder**](Project#rootfolder), is the file system's root, [**FileSystem.RootFolder**](FileSystem#rootfolder), whose **Path** is `twinbasic:/`. -> [!WARNING] -> In BETA 1005, reading **Parent** of the file system's root crashes the compiler, and the add-in with it. The read itself crashes, before anything is done with the value, so neither `Is Nothing` nor **TypeName** can test it, and each compiler the IDE restarts crashes again when the add-in reaches the read. Tell the root by its **Path**, `twinbasic:/`, rather than by its **Parent**. +> [!WARNING] BETA 1005 +> Reading **Parent** of the file system's root crashes the compiler, and the add-in with it. The read itself crashes, before anything is done with the value, so neither `Is Nothing` nor **TypeName** can test it, and each compiler the IDE restarts crashes again when the add-in reaches the read. Tell the root by its **Path**, `twinbasic:/`, rather than by its **Parent**. ### Path {: .no_toc } diff --git a/docs/Reference/Built-In/tbIDE/HtmlElement.md b/docs/Reference/Built-In/tbIDE/HtmlElement.md index 4ad364877..bdc7cdd40 100644 --- a/docs/Reference/Built-In/tbIDE/HtmlElement.md +++ b/docs/Reference/Built-In/tbIDE/HtmlElement.md @@ -99,8 +99,8 @@ raiseEvent(eventName, event, stopPropagation, ...customData) --- pass an event name (any string), the DOM `event` object, a boolean controlling propagation, and any number of trailing custom-data values. The addin then registers a listener with the same event name through **AddEventListener**, and the custom-data values arrive on the [**HtmlEventProperties**](HtmlEventProperties) as `eventInfo.customData0`, `eventInfo.customData1`, … (numerically indexed from zero). This pattern is used heavily in sample 13 (listview) and sample 15 (Global Search) to attach handlers to per-item buttons rendered inside a listview's HTML. -> [!NOTE] -> **raiseEvent** works only inside a `"listview"` or `"virtuallistview"` widget, or inside a widget added with [**CodeEditor.AddMonacoWidget**](CodeEditor#addmonacowidget). It passes the event to the nearest enclosing widget: in a list view, register the listener on the list view object, `.Properties.listview.AddEventListener("name", ...)`, as sample 15 does; in a code-editor widget, on the widget's own element. In plain tool-window HTML there is no such widget, and in BETA 1005 **raiseEvent** throws a `TypeError` in the IDE's page and the listener is not called. +> [!NOTE] BETA 1005 +> **raiseEvent** works only inside a `"listview"` or `"virtuallistview"` widget, or inside a widget added with [**CodeEditor.AddMonacoWidget**](CodeEditor#addmonacowidget). It passes the event to the nearest enclosing widget: in a list view, register the listener on the list view object, `.Properties.listview.AddEventListener("name", ...)`, as sample 15 does; in a code-editor widget, on the widget's own element. In plain tool-window HTML there is no such widget, and **raiseEvent** throws a `TypeError` in the IDE's page and the listener is not called. > > From plain HTML, an inline handler can call the listener itself. **AddEventListener** stores a listener for a name that is not one of the element's DOM events on the element, under that name, so an inline `onclick='this.parentNode.myEvent(event)'` calls the listener its parent registered as `"myEvent"`, and `eventInfo.target.id` names the element clicked. diff --git a/docs/Reference/Built-In/tbIDE/HtmlElementProperties.md b/docs/Reference/Built-In/tbIDE/HtmlElementProperties.md index 1880172aa..575466f0f 100644 --- a/docs/Reference/Built-In/tbIDE/HtmlElementProperties.md +++ b/docs/Reference/Built-In/tbIDE/HtmlElementProperties.md @@ -24,8 +24,8 @@ The shorthand reads at run time as a chain of `Item("name")` lookups against the > [!IMPORTANT] > This interface is **`[COMExtensible(True)]`**. Property names are resolved against the live DOM element at run time, not declared statically on the interface. The compiler does not validate names --- a typo (`.innerTxt = "..."` instead of `.innerText = "..."`) fails silently or throws at run time. The accepted set is **every DOM property of the underlying tag**, plus any custom-widget extensions; the reference does not enumerate it. -> [!NOTE] -> A property whose name starts with `on` is ignored. In BETA 1005, `.onclick = "..."` sets nothing, and the call returns without an error. Register a listener with [**HtmlElement.AddEventListener**](HtmlElement#addeventlistener) instead. +> [!NOTE] BETA 1005 +> A property whose name starts with `on` is ignored. `.onclick = "..."` sets nothing, and the call returns without an error. Register a listener with [**HtmlElement.AddEventListener**](HtmlElement#addeventlistener) instead. > [!IMPORTANT] > Text set as `innerHTML` is parsed as HTML, and an inline event handler in it runs as script of the IDE's own page, with access to everything that page's script can reach. An `` whose image fails to load runs its `onerror` handler as soon as it is set, with nothing clicked. Escape any text that comes from a file, a project or the user before it goes into `innerHTML`, or set `innerText` instead. diff --git a/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md b/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md index 6bef3995a..1193674f7 100644 --- a/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md +++ b/docs/Reference/Built-In/tbIDE/KeyboardShortcuts.md @@ -29,8 +29,8 @@ End Sub The shortcut fires when the key is released, wherever the focus is in the IDE's window --- in the code editor, in the DEBUG CONSOLE, or on nothing at all --- except while a dialog or the rename box is open. If the IDE has a command of its own on the same key, that command runs as well: F1, for one, also expands or collapses the signature help while it is showing. Shift+F12 has no command in the IDE's default keymap. -> [!NOTE] -> In BETA 1005, a shortcut that includes `{CTRL}` or `{ALT}` does not fire when the user presses it. The IDE matches an add-in's shortcut when the key is released, against a press of the same key less than half a second earlier, and it does not record a key pressed while Ctrl or Alt is held down. Such a shortcut fires only if the same key was also pressed on its own within that half second. Use a function key, alone or with `{SHIFT}`. +> [!NOTE] BETA 1005 +> A shortcut that includes `{CTRL}` or `{ALT}` does not fire when the user presses it. The IDE matches an add-in's shortcut when the key is released, against a press of the same key less than half a second earlier, and it does not record a key pressed while Ctrl or Alt is held down. Such a shortcut fires only if the same key was also pressed on its own within that half second. Use a function key, alone or with `{SHIFT}`. ## Methods diff --git a/docs/Reference/COM-Interfaces/IDispatch.md b/docs/Reference/COM-Interfaces/IDispatch.md index d67e813e5..36f52e513 100644 --- a/docs/Reference/COM-Interfaces/IDispatch.md +++ b/docs/Reference/COM-Interfaces/IDispatch.md @@ -231,8 +231,8 @@ The four [**VbCallType**](../../tB/Modules/Constants/VbCallType) values are the | The object is **Nothing** | 91, *Object variable or With block variable not set* | | **CallByName** on a value that is not an object | 424, *Object required* | -> [!WARNING] -> BETA 1005 has a defect: a call that fails inside **Invoke** can be made twice, so its side effects happen twice and nothing says so. VB6 makes the call once. A failed property assignment is followed by a second **Invoke** with *wFlags* 3. A call with more arguments than a **Sub** takes runs the **Sub**, then fails with error 13, and a late-bound statement does this twice. Code that implements **Invoke** must not change state before it can still fail. +> [!WARNING] BETA 1005 +> A call that fails inside **Invoke** can be made twice, so its side effects happen twice and nothing says so. VB6 makes the call once. A failed property assignment is followed by a second **Invoke** with *wFlags* 3. A call with more arguments than a **Sub** takes runs the **Sub**, then fails with error 13, and a late-bound statement does this twice. Code that implements **Invoke** must not change state before it can still fail. > [!NOTE] > **CallByName** repeats a call that returned `DISP_E_MEMBERNOTFOUND`: a second **Invoke** with the same identifier and *wFlags* and a null *pVarResult*, for **VbMethod**, **VbGet** and **VbLet** alike. VB6 does the same. A late-bound statement makes the call once. diff --git a/docs/Reference/COM-Interfaces/IErrorInfo.md b/docs/Reference/COM-Interfaces/IErrorInfo.md index 47855dd63..12c21c54b 100644 --- a/docs/Reference/COM-Interfaces/IErrorInfo.md +++ b/docs/Reference/COM-Interfaces/IErrorInfo.md @@ -183,8 +183,8 @@ The object that **GetErrorInfo** returns then holds what the method gave **Raise **GetErrorInfo** empties the slot as COM requires, so a second read returns `Nothing`. -> [!WARNING] -> BETA 1005 has a defect: the object does not keep the values it was given, and no error tells the caller. It reads them from the **Err** object each time a method is called. [**Err.Clear**](../../tB/Modules/ErrObject/Clear) empties **Err**, so an object read after it returns empty strings and 0, and the next error changes the values to its own. COM requires an **IErrorInfo** to keep what was stored in it. Read the five properties before calling anything else that clears **Err** or raises an error. +> [!WARNING] BETA 1005 +> The object does not keep the values it was given, and no error tells the caller. It reads them from the **Err** object each time a method is called. [**Err.Clear**](../../tB/Modules/ErrObject/Clear) empties **Err**, so an object read after it returns empty strings and 0, and the next error changes the values to its own. COM requires an **IErrorInfo** to keep what was stored in it. Read the five properties before calling anything else that clears **Err** or raises an error. A method that handles an error itself, with [**On Error**](../../tB/Core/On-Error), still leaves the values in **Err** and a returned **IErrorInfo** that shows them, although it returns `S_OK`. A caller reads the slot only after a failure code. @@ -228,8 +228,8 @@ A method can return a failure **HRESULT** without calling **SetErrorInfo**, and | `&H80070005` (`E_ACCESSDENIED`) | -2147024891 | `Automation error` | | `&H8FFF0001` (no meaning) | -1879113727 | `Automation error` | -> [!WARNING] -> BETA 1005 has a defect: the slot is not always empty, and no error tells the caller. A handled error leaves in it an object that reads from **Err**, even when the error was handled in twinBASIC code that never read the slot. VB6 leaves the slot empty. When **Err** has been cleared in between, that object has an empty description, so **Description** of a failure that arrived without information is the standard text of the code: `Unspecified error` for `E_FAIL`, `Invalid procedure call or argument` for `&H800A0005` and `Access is denied.`, in the language of the system, for `E_ACCESSDENIED`. A program that must tell failures apart reads **Number** and does not rely on **Description** for a code that arrived without information. +> [!WARNING] BETA 1005 +> The slot is not always empty, and no error tells the caller. A handled error leaves in it an object that reads from **Err**, even when the error was handled in twinBASIC code that never read the slot. VB6 leaves the slot empty. When **Err** has been cleared in between, that object has an empty description, so **Description** of a failure that arrived without information is the standard text of the code: `Unspecified error` for `E_FAIL`, `Invalid procedure call or argument` for `&H800A0005` and `Access is denied.`, in the language of the system, for `E_ACCESSDENIED`. A program that must tell failures apart reads **Number** and does not rely on **Description** for a code that arrived without information. ### A method that sets error information itself diff --git a/docs/Reference/COM-Interfaces/IUnknown.md b/docs/Reference/COM-Interfaces/IUnknown.md index 1dc8fa703..2736cda3f 100644 --- a/docs/Reference/COM-Interfaces/IUnknown.md +++ b/docs/Reference/COM-Interfaces/IUnknown.md @@ -41,8 +41,8 @@ Syntax: *object*.**QueryInterface** *riid*, *ppvObject* Returns `S_OK` and a pointer with one reference added for the caller, or `E_NOINTERFACE` (`&H80004002`) and a null pointer. -> [!NOTE] -> In BETA 1005, an object of a class that the project compiles --- a class, a form, a control --- returns `E_NOINTERFACE` for an interface it does not support but leaves *ppvObject* as it was, instead of setting it to a null pointer. The runtime's own objects, such as a **Collection**, set it to a null pointer, as every object of VB6 does. A caller tests the result, not the pointer. +> [!NOTE] BETA 1005 +> An object of a class that the project compiles --- a class, a form, a control --- returns `E_NOINTERFACE` for an interface it does not support but leaves *ppvObject* as it was, instead of setting it to a null pointer. The runtime's own objects, such as a **Collection**, set it to a null pointer, as every object of VB6 does. A caller tests the result, not the pointer. Three rules govern the answer: @@ -135,8 +135,8 @@ A class never needs to implement **IUnknown**: the compiler gives every class an `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. -> [!WARNING] -> BETA 1005 has a defect: 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`. +> [!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`. ## Example diff --git a/docs/Reference/Contributed/ArrayListLib/index.md b/docs/Reference/Contributed/ArrayListLib/index.md index e2a75afaa..45694edf6 100644 --- a/docs/Reference/Contributed/ArrayListLib/index.md +++ b/docs/Reference/Contributed/ArrayListLib/index.md @@ -19,8 +19,8 @@ The **ArrayListLib** contributed package is a list class for twinBASIC, in place This section documents version 1.6.151.1, which is the version on TWINSERV, published under the Unlicense. -> [!NOTE] -> Version 1.6.151.1 does not compile in twinBASIC BETA 1005, so a project that references it does not build (error TB5001, in a 32-bit and in a 64-bit build). **ArrayList** passes the addresses of two items, as **Long** values, to its private procedure **VSwap**, which declares both parameters as `ByRef` structures, in **Reverse** (`ArrayList.twin`, line 297) and in the two quicksort procedures that **Sort** calls (lines 585 and 602). **MemMoveBSTR**, a private procedure of the module **LibMemoryEx**, passes an address to **LSetByVal** in the same way (`Modules/LibMemoryEx.twin`, line 104), and **LSetByVal** declares its parameter as a `ByRef` structure that holds a string pointer. A pointer passed for a structure has to be passed `ByVal`. The behaviour described on these pages is that of the package with the two arguments of each **VSwap** call passed `ByVal`, and with **LSetByVal** taking the address as a **LongPtr** and putting it in a structure of its own. That copy builds in 32-bit and in 64-bit programs, and both give the same results. +> [!NOTE] 1.6.151.1, BETA 1005 +> The package does not compile, so a project that references it does not build (error TB5001, in a 32-bit and in a 64-bit build). **ArrayList** passes the addresses of two items, as **Long** values, to its private procedure **VSwap**, which declares both parameters as `ByRef` structures, in **Reverse** (`ArrayList.twin`, line 297) and in the two quicksort procedures that **Sort** calls (lines 585 and 602). **MemMoveBSTR**, a private procedure of the module **LibMemoryEx**, passes an address to **LSetByVal** in the same way (`Modules/LibMemoryEx.twin`, line 104), and **LSetByVal** declares its parameter as a `ByRef` structure that holds a string pointer. A pointer passed for a structure has to be passed `ByVal`. The behaviour described on these pages is that of the package with the two arguments of each **VSwap** call passed `ByVal`, and with **LSetByVal** taking the address as a **LongPtr** and putting it in a structure of its own. That copy builds in 32-bit and in 64-bit programs, and both give the same results. > [!WARNING] > Several members give wrong results, damage memory or never return. A **For Each** over a list with no items raises an error, and one that runs under `On Error Resume Next` never ends. A call to **GetEnumerator** after a **For Each** over the same list ran to the end ignores its arguments. **Reverse** keeps the two middle items of a run with an even number of items in place, and overwrites memory when the run has no items. **ToString** never returns for a list with no items. **Insert** at the first position fails unless the list has room for 31 more items. **Remove** with a negative **BaseIndex** removes the wrong item. A range reads released memory after a change made through it makes the list grow. See [Known problems](#known-problems). diff --git a/docs/Reference/Contributed/BigNumberLib/BigNumber.md b/docs/Reference/Contributed/BigNumberLib/BigNumber.md index 16a99e31c..2d5a005f8 100644 --- a/docs/Reference/Contributed/BigNumberLib/BigNumber.md +++ b/docs/Reference/Contributed/BigNumberLib/BigNumber.md @@ -21,8 +21,11 @@ result.AssignAddition a, b Debug.Print result.ValueString ' 1111111110111111111011111111100 ``` +> [!NOTE] since BETA 434 +> The class needs this build of twinBASIC or a later one. + > [!NOTE] -> This class requires twinBASIC BETA 434 or newer. String-to-value conversions silently stop at the first invalid character --- for example, setting `ValueHex = "ABC123he"` stores `&HABC123` without raising an error. +> String-to-value conversions silently stop at the first invalid character --- for example, setting `ValueHex = "ABC123he"` stores `&HABC123` without raising an error. > [!WARNING] > Overflow is not trapped. A result that needs more bits than the library holds loses its high bits, and a string with more digits than fit gives a wrong value. No error is raised in either case. The library holds a little more than the 4096 bits the class supports, 4352 bits in a 32-bit program and 4608 in a 64-bit one, so a result a few bits over 4096 may still be right. diff --git a/docs/Reference/Contributed/Fmt/EscapeSequence/index.md b/docs/Reference/Contributed/Fmt/EscapeSequence/index.md index b60a0c5b2..944fac154 100644 --- a/docs/Reference/Contributed/Fmt/EscapeSequence/index.md +++ b/docs/Reference/Contributed/Fmt/EscapeSequence/index.md @@ -27,8 +27,8 @@ esc.Execute text Debug.Print text ' aZZb ``` -> [!NOTE] -> [**Self**](Self) and the four property setters ([**AsciiBase**](AsciiBase), [**EscapeString**](EscapeString), [**IsAsciiCharacter**](IsAsciiCharacter) and [**ReplacementString**](ReplacementString) with **Let**) are declared **Friend**, for [**Create**](Create) to use. A project that references the package can call them too (checked on BETA 1005), so the properties of an instance can be changed after it is created. +> [!NOTE] BETA 1005 +> [**Self**](Self) and the four property setters ([**AsciiBase**](AsciiBase), [**EscapeString**](EscapeString), [**IsAsciiCharacter**](IsAsciiCharacter) and [**ReplacementString**](ReplacementString) with **Let**) are declared **Friend**, for [**Create**](Create) to use. A project that references the package can call them too, so the properties of an instance can be changed after it is created. ## Properties diff --git a/docs/Reference/Contributed/Fmt/StringValidation/index.md b/docs/Reference/Contributed/Fmt/StringValidation/index.md index 963af532e..75f93acd4 100644 --- a/docs/Reference/Contributed/Fmt/StringValidation/index.md +++ b/docs/Reference/Contributed/Fmt/StringValidation/index.md @@ -12,8 +12,8 @@ Holds the small string tests and the case helper that the package's formatter is **StringValidation** is a standard module of five public functions. [**StringContains**](StringContains), [**StringContainsAny**](StringContainsAny), [**StringMatchesAny**](StringMatchesAny) and [**StringStartsWith**](StringStartsWith) return a **Boolean** about the text of a string, and [**CopyCapitalisation**](CopyCapitalisation) returns a string in the letter case of another. The formatter calls them to look for backslashes and escape sequences, to test whether a format string begins with `@` and to follow the case of an identifier. A project that references the package can call them too, by name or as `StringValidation.StringContains`. -> [!NOTE] -> The package declares the module with `Option Private Module`, which marks it as internal to the package. A project that references the package can still call its functions (checked on BETA 1005). +> [!NOTE] BETA 1005 +> The package declares the module with `Option Private Module`, which marks it as internal to the package. A project that references the package can still call its functions. ```tb check_build check_run Debug.Print StringContains("Hello", "ELL") ' True diff --git a/docs/Reference/Contributed/Generics/index.md b/docs/Reference/Contributed/Generics/index.md index 65ba29ff2..cf04e2db6 100644 --- a/docs/Reference/Contributed/Generics/index.md +++ b/docs/Reference/Contributed/Generics/index.md @@ -96,8 +96,8 @@ Module GenericsMinMaxDemo End Module ``` -> [!NOTE] -> In BETA 1005, **New** with a generic class from a package compiles only when some declaration in the project or the package names the same class with the same type arguments. A variable declared with the tuple's type is such a declaration. Without one, `Dim t As Any = New TupleOf2(Of String, Long)("a", 2)` fails with TB5074, *Could not bind to parameterized constructor of class 'TupleOf2'*, and so does **New** assigned to an **Object** or **Variant** variable or passed as an argument. A generic class declared in the project's own code is not affected. Assign the tuple to a variable of its own type first; that variable can then be assigned to an **Object** or **Variant** variable, through which the items are read late-bound: +> [!NOTE] BETA 1005 +> **New** with a generic class from a package compiles only when some declaration in the project or the package names the same class with the same type arguments. A variable declared with the tuple's type is such a declaration. Without one, `Dim t As Any = New TupleOf2(Of String, Long)("a", 2)` fails with TB5074, *Could not bind to parameterized constructor of class 'TupleOf2'*, and so does **New** assigned to an **Object** or **Variant** variable or passed as an argument. A generic class declared in the project's own code is not affected. Assign the tuple to a variable of its own type first; that variable can then be assigned to an **Object** or **Variant** variable, through which the items are read late-bound: > > ```tb check_build check_run > Dim t As TupleOf2(Of String, Long) = New TupleOf2(Of String, Long)("width", 640) @@ -137,13 +137,13 @@ An item can have any numeric type, **String**, **Boolean**, **Date**, **Currency - **Object types.** The constructor stores each item with `=`, not with **Set**. A class whose default member takes an argument, such as **Collection**, fails to compile, with TB5092 *Missing argument 'Index'* reported in the package's source. **Object** compiles, but creating the tuple raises run-time error 91, *Object variable or With block variable not set*, even when the item passed is **Nothing**. So a tuple cannot hold another tuple either. - **Arrays**, such as **Long()**. -> [!WARNING] -> In BETA 1005, a tuple type with an array as a type argument, such as `TupleOf1(Of Long())`, crashes the compiler wherever it is written: in a declaration, a parameter, a function's result type or after **New**. The compiler restarts and crashes again, so nothing in the project compiles until the line is removed. The same happens with a generic class declared in a project's own code. +> [!WARNING] BETA 1005 +> A tuple type with an array as a type argument, such as `TupleOf1(Of Long())`, crashes the compiler wherever it is written: in a declaration, a parameter, a function's result type or after **New**. The compiler restarts and crashes again, so nothing in the project compiles until the line is removed. The same happens with a generic class declared in a project's own code. ## TupleOf4 -> [!NOTE] -> **TupleOf4** cannot be used in version 1.0.0.0. The package declares it with three type parameters, `TupleOf4(Of T1, T2, T3)`, but its fourth item uses a fourth, *T4*, which is not declared. With four type arguments, a declaration fails with TB5124, *Too many generic specifiers*. With three, the package's source fails with TB5079, *Unrecognized datatype symbol 'T4'*. A project that does not use **TupleOf4** is not affected. For four values, use **TupleOf5** and leave its last item unused. +> [!NOTE] 1.0.0.0 +> **TupleOf4** cannot be used. The package declares it with three type parameters, `TupleOf4(Of T1, T2, T3)`, but its fourth item uses a fourth, *T4*, which is not declared. With four type arguments, a declaration fails with TB5124, *Too many generic specifiers*. With three, the package's source fails with TB5079, *Unrecognized datatype symbol 'T4'*. A project that does not use **TupleOf4** is not affected. For four values, use **TupleOf5** and leave its last item unused. ## See Also diff --git a/docs/Reference/Contributed/OLEGuids/IEnumVARIANTUnrestricted.md b/docs/Reference/Contributed/OLEGuids/IEnumVARIANTUnrestricted.md index 09556c32f..0dde0e4eb 100644 --- a/docs/Reference/Contributed/OLEGuids/IEnumVARIANTUnrestricted.md +++ b/docs/Reference/Contributed/OLEGuids/IEnumVARIANTUnrestricted.md @@ -18,8 +18,8 @@ The enumerator declared in **stdole** cannot be implemented from twinBASIC, beca The declaration extends **stdole.IDispatch**. The Windows **IEnumVARIANT** extends **IUnknown**. -> [!WARNING] -> Because the declaration extends **stdole.IDispatch**, its four methods come after the seven of **IDispatch** in the method table, where the table of an **IEnumVARIANT** has them straight after the three of **IUnknown**. So in version 1.0.10.0 every method called through this interface on a real enumerator, such as the one a **Collection** returns, calls a slot past the end of its table: **Next**, **Skip** and **Clone** end the program with an access violation, and **Reset** raises a run-time error or does nothing. **For Each** over an object whose enumerator is a class that implements this interface ends the program the same way, without calling any of the class's methods. A program that calls or writes an enumerator declares its own copy of the interface, which extends **stdole.IUnknown**, as the [IEnumVARIANT](../../../../Reference/COM-Interfaces/IEnumVARIANT) page in the COM Interfaces section shows. +> [!WARNING] 1.0.10.0 +> Because the declaration extends **stdole.IDispatch**, its four methods come after the seven of **IDispatch** in the method table, where the table of an **IEnumVARIANT** has them straight after the three of **IUnknown**. So every method called through this interface on a real enumerator, such as the one a **Collection** returns, calls a slot past the end of its table: **Next**, **Skip** and **Clone** end the program with an access violation, and **Reset** raises a run-time error or does nothing. **For Each** over an object whose enumerator is a class that implements this interface ends the program the same way, without calling any of the class's methods. A program that calls or writes an enumerator declares its own copy of the interface, which extends **stdole.IUnknown**, as the [IEnumVARIANT](../../../../Reference/COM-Interfaces/IEnumVARIANT) page in the COM Interfaces section shows. The declaration is complete: it has all four methods of the Windows interface. Each is a **Sub**, so the HRESULT is hidden: a failure code raises a run-time error, and a success code such as `S_FALSE` is read with [**Err.LastHresult**](../../../Modules/ErrObject/LastHresult). The [IEnumVARIANT](../../../../Reference/COM-Interfaces/IEnumVARIANT) page in the COM Interfaces section gives the contract of the four methods, how **For Each** uses them, and a project's own declaration of the interface. diff --git a/docs/Reference/Contributed/OLEGuids/IUnknownUnrestricted.md b/docs/Reference/Contributed/OLEGuids/IUnknownUnrestricted.md index 836354a74..14adb82b0 100644 --- a/docs/Reference/Contributed/OLEGuids/IUnknownUnrestricted.md +++ b/docs/Reference/Contributed/OLEGuids/IUnknownUnrestricted.md @@ -29,8 +29,8 @@ Asks the object for another interface. *priid* is the address of the interface i A pointer that **QueryInterface** returns comes with a reference, which the caller gives back with **Release**. -> [!NOTE] -> When the interface is not supported, the COM contract sets *ppvObject* to 0. In BETA 1005, an object of a class that the project compiles --- a class, a form, a control --- returns `E_NOINTERFACE` and leaves the variable as it was. The runtime's own objects, such as a **Collection**, set it to 0, as every object of VB6 does. Set the variable to 0 before the call, and test the result rather than the pointer. +> [!NOTE] BETA 1005 +> When the interface is not supported, the COM contract sets *ppvObject* to 0. An object of a class that the project compiles --- a class, a form, a control --- returns `E_NOINTERFACE` and leaves the variable as it was. The runtime's own objects, such as a **Collection**, set it to 0, as every object of VB6 does. Set the variable to 0 before the call, and test the result rather than the pointer. ```tb check_run Dim u As IUnknownUnrestricted = New Collection diff --git a/docs/Reference/Contributed/TBMANLIB/ToolsCN.md b/docs/Reference/Contributed/TBMANLIB/ToolsCN.md index abd7a9584..bf848704a 100644 --- a/docs/Reference/Contributed/TBMANLIB/ToolsCN.md +++ b/docs/Reference/Contributed/TBMANLIB/ToolsCN.md @@ -29,8 +29,8 @@ For each character, the code takes the number that **Asc** returns for it. A num An empty string returns an empty string. -> [!NOTE] -> In version 1.0.0.29 the function raises error -2147467259, *Unspecified error*, for any text that holds a character with a code from 1 to 159, such as an English letter, a digit or a space. The line that copies such a character is written `G = Chr(Num)()`, with a pair of parentheses after the call to **Chr**, which returns a **Variant**, and that raises the error when it runs. On a computer whose code page is not a Chinese one, a Chinese character becomes `?`, code 63, so it raises the error too. Any other character is dropped with no error: `GetPinYin(ChrW(233))` returns an empty string. The same pair of parentheses after the call to **Asc** on the line before has no effect. The loop over the characters also uses the variable `i` for its own search through the table, so on a Chinese code page it would skip characters after a Chinese character. +> [!NOTE] 1.0.0.29 +> The function raises error -2147467259, *Unspecified error*, for any text that holds a character with a code from 1 to 159, such as an English letter, a digit or a space. The line that copies such a character is written `G = Chr(Num)()`, with a pair of parentheses after the call to **Chr**, which returns a **Variant**, and that raises the error when it runs. On a computer whose code page is not a Chinese one, a Chinese character becomes `?`, code 63, so it raises the error too. Any other character is dropped with no error: `GetPinYin(ChrW(233))` returns an empty string. The same pair of parentheses after the call to **Asc** on the line before has no effect. The loop over the characters also uses the variable `i` for its own search through the table, so on a Chinese code page it would skip characters after a Chinese character. ## See Also diff --git a/docs/Reference/Contributed/TBMANLIB/cHttpClient.md b/docs/Reference/Contributed/TBMANLIB/cHttpClient.md index 30cb719e4..20032b932 100644 --- a/docs/Reference/Contributed/TBMANLIB/cHttpClient.md +++ b/docs/Reference/Contributed/TBMANLIB/cHttpClient.md @@ -57,8 +57,8 @@ Several error texts of the class are written in Chinese. The ones that are not a > [!WARNING] > The class ignores every certificate error: an expired certificate, one from an unknown authority and one issued for another name are all accepted. An HTTPS request therefore encrypts the traffic, but does not prove who the server is, and a program that sends a password or a key with it can send it to an impostor. -> [!NOTE] -> In BETA 1005 the compiler refuses an assignment written through a public dictionary field of an object, such as `Http.RequestHeaders("X-Key") = "1"`, with TB5090. Write the default member out: `Http.RequestHeaders.Item("X-Key") = "1"`. Reading `Http.RequestHeaders("X-Key")` is accepted. +> [!NOTE] BETA 1005 +> The compiler refuses an assignment written through a public dictionary field of an object, such as `Http.RequestHeaders("X-Key") = "1"`, with TB5090. Write the default member out: `Http.RequestHeaders.Item("X-Key") = "1"`. Reading `Http.RequestHeaders("X-Key")` is accepted. ## Request settings @@ -284,8 +284,8 @@ Syntax: *object*.**FollowRedirect** ( [ *RedirectMethod* ] ) **As cHttpClient** Reads the address with [**GetRedirectUrl**](#getredirecturl) and sends a GET request to it (for 307 and 308, the given method). Each call counts, and error 310 is raised when the count exceeds [**MaxRedirects**](#maxredirects); [**ResetRedirectCount**](#resetredirectcount) sets the count to 0. The method needs [**EnableRedirects**](#enableredirects) set to **False** with [**AllowRedirects**](#allowredirects), or no redirect answer is left to follow. It returns the object itself, and does nothing if the last answer had no `Location` header. -> [!NOTE] -> The method cannot send a request in BETA 1005. The address comes from [**ResponseHeaders**](#responseheaders), which cuts a value at its first colon, so `http://host/path` is read as `http`, and WinHTTP raises error -2147012890, *the URL does not use a recognized protocol*. A `Location` header with a relative address, such as `/path`, has no colon and is read whole, but WinHTTP refuses it as well. A program that has to follow a redirect leaves **EnableRedirects** at **True**, so that WinHTTP follows it. +> [!NOTE] 1.0.0.29 +> The method cannot send a request. The address comes from [**ResponseHeaders**](#responseheaders), which cuts a value at its first colon, so `http://host/path` is read as `http`, and WinHTTP raises error -2147012890, *the URL does not use a recognized protocol*. A `Location` header with a relative address, such as `/path`, has no colon and is read whole, but WinHTTP refuses it as well. A program that has to follow a redirect leaves **EnableRedirects** at **True**, so that WinHTTP follows it. ### GetRedirectUrl {: .no_toc } diff --git a/docs/Reference/Contributed/TBMANLIB/cToolsStream.md b/docs/Reference/Contributed/TBMANLIB/cToolsStream.md index 1ce79ac0c..f8dfdb860 100644 --- a/docs/Reference/Contributed/TBMANLIB/cToolsStream.md +++ b/docs/Reference/Contributed/TBMANLIB/cToolsStream.md @@ -27,8 +27,8 @@ Kill Path ' True ``` -> [!WARNING] -> In version 1.0.0.29, [**LoadFileAsText**](#loadfileastext) and [**SaveFileAsText**](#savefileastext) leave the stream open, and neither closes it first. That breaks the next call of either on the same object, and the shared **ToolsStream** is such an object for the whole run of the program: +> [!WARNING] 1.0.0.29 +> [**LoadFileAsText**](#loadfileastext) and [**SaveFileAsText**](#savefileastext) leave the stream open, and neither closes it first. That breaks the next call of either on the same object, and the shared **ToolsStream** is such an object for the whole run of the program: > > - A **LoadFileAsText** that finds the stream open raises error 3705, *Operation is not allowed when the object is open*, before it reads anything. It finds it open after any earlier text call, a failed one included. > - A **SaveFileAsText** that finds the stream open writes its text over the text the stream still holds, from the start, and saves the whole stream. When the new text is shorter than the old, the end of the old text stays in the file: after saving `0123456789` and then `abc`, the file holds `abc3456789`. No error is raised. diff --git a/docs/Reference/Contributed/TBMANLIB/index.md b/docs/Reference/Contributed/TBMANLIB/index.md index a2551f960..3ee2ab1d2 100644 --- a/docs/Reference/Contributed/TBMANLIB/index.md +++ b/docs/Reference/Contributed/TBMANLIB/index.md @@ -79,8 +79,8 @@ The references of a package are not visible to the project that uses it. A progr The package is built for 32-bit programs. The window procedure that **cSubClass** installs is passed to **SetWindowLong** as a **Long**, which a 64-bit program cannot hold. -> [!WARNING] -> In BETA 1005, a project that embeds TBMANLIB and is built for 64-bit programs (**Win64**) does not compile. The compiler reports TB5001 in `cSubClass.twin`, *Validation of call to 'SetWindowLong' failed. Argument for 'dwNewLong': cannot coerce type 'Callback' to 'Long'*, even when the program uses nothing of the package. The package can be used in 32-bit programs only. +> [!WARNING] BETA 1005 +> A project that embeds TBMANLIB and is built for 64-bit programs (**Win64**) does not compile. The compiler reports TB5001 in `cSubClass.twin`, *Validation of call to 'SetWindowLong' failed. Argument for 'dwNewLong': cannot coerce type 'Callback' to 'Long'*, even when the program uses nothing of the package. The package can be used in 32-bit programs only. The other components are available to 32-bit programs. diff --git a/docs/Reference/Contributed/TBMANLIB_Aes/cAes.md b/docs/Reference/Contributed/TBMANLIB_Aes/cAes.md index da9a50eb5..ed9508ba4 100644 --- a/docs/Reference/Contributed/TBMANLIB_Aes/cAes.md +++ b/docs/Reference/Contributed/TBMANLIB_Aes/cAes.md @@ -61,8 +61,8 @@ Every call of an encrypt or a decrypt method starts again from **Key** and **IV* ## Which mode runs -> [!WARNING] -> In version 1.0.0.4 the name of a mode is not always the mode that runs. **ECB** runs CBC with an IV of zero bytes, **CBC** runs ECB, and **OFB** and **CTR** run CFB. Neither **ECB** nor **CBC** uses the IV that the program sets, so the same key and plaintext always give the same ciphertext, and in the default mode **CBC** equal plaintext blocks give equal ciphertext blocks. A round trip succeeds in every case, because encryption and decryption use the same mode, so a test that only decrypts what it has encrypted shows nothing. +> [!WARNING] 1.0.0.4 +> The name of a mode is not always the mode that runs. **ECB** runs CBC with an IV of zero bytes, **CBC** runs ECB, and **OFB** and **CTR** run CFB. Neither **ECB** nor **CBC** uses the IV that the program sets, so the same key and plaintext always give the same ciphertext, and in the default mode **CBC** equal plaintext blocks give equal ciphertext blocks. A round trip succeeds in every case, because encryption and decryption use the same mode, so a test that only decrypts what it has encrypted shows nothing. | Mode | Windows interface | What runs | The IV | |---|---|---|---| @@ -352,8 +352,8 @@ Debug.Print Err.Number A decryption with no tag set fails in the same way, after the line `DoDecryptCNG_GCM failed: GCM tag not set for decryption`. A tag of 16 bytes is the length that the encryption makes. The class passes the length of **GcmTag** to Windows as it is: a tag cut to its first 12 bytes is accepted and the decryption returns the plaintext, and one cut to 4 bytes makes Windows fail with an invalid parameter. -> [!WARNING] -> In version 1.0.0.4, a GCM encryption with no IV set makes a random 12-byte nonce, uses it, and keeps it nowhere. [**IV**](#iv) stays empty. A decryption with no IV uses a nonce of 12 zero bytes, so its tag check fails and the data cannot be decrypted. The encryption raises no error. Set **IV** before every GCM encryption, and keep it with the ciphertext. +> [!WARNING] 1.0.0.4 +> A GCM encryption with no IV set makes a random 12-byte nonce, uses it, and keeps it nowhere. [**IV**](#iv) stays empty. A decryption with no IV uses a nonce of 12 zero bytes, so its tag check fails and the data cannot be decrypted. The encryption raises no error. Set **IV** before every GCM encryption, and keep it with the ciphertext. ```tb check_build check_run projname=aes-class Dim aes As cAes = New cAes(AesKeySize.KeySize_128, AesCipherMode.GCM, AesPaddingMode.NoPadding) @@ -388,8 +388,8 @@ A failed call does not tell why, and what the caller gets depends on the member. > [!NOTE] > A program that calls **EncryptBytes** or **DecryptBytes** needs an `On Error` statement, and cannot tell a missing key from a wrong tag or a damaged ciphertext by the error. The Debug Console of the IDE shows more: the package prints lines with `Debug.Print`, such as `DecryptBytes error: Key not set` and `BCryptEncrypt failed: 0xC0000206` followed by a message, and a built program writes none of them. -> [!WARNING] -> In version 1.0.0.4 the string members hide a failure in an empty string. **DecryptString** of a GCM message whose tag does not match, of a message with a wrong key in a mode with padding, and of Base64 text that is not valid, all give the same empty string as an empty plaintext would, and **EncryptString** gives an empty string for a missing key. A program must test the result with `Len`. +> [!WARNING] 1.0.0.4 +> The string members hide a failure in an empty string. **DecryptString** of a GCM message whose tag does not match, of a message with a wrong key in a mode with padding, and of Base64 text that is not valid, all give the same empty string as an empty plaintext would, and **EncryptString** gives an empty string for a missing key. A program must test the result with `Len`. These are the causes of a failure in the source: @@ -644,8 +644,8 @@ With **NoPadding** in the modes **CFB**, **OFB**, **CTR** and **GCM**, the outpu **ISO10126** gives the output of **NoPadding**, and no padding is added. With **ZeroPadding** the decryption removes every zero byte at the end of the data, and with the data `41 42 00 00` it returns the two bytes `41 42`. -> [!WARNING] -> With **ZeroPadding**, version 1.0.0.4 gives back less than it was given when the data ends in zero bytes. The decryption removes every zero byte at the end of the data, and not only the ones that the encryption added, and raises no error. Data that is all zero bytes fails the decryption. Use **PKCS7** for data that can end in zero bytes. +> [!WARNING] 1.0.0.4 +> With **ZeroPadding**, the class gives back less than it was given when the data ends in zero bytes. The decryption removes every zero byte at the end of the data, and not only the ones that the encryption added, and raises no error. Data that is all zero bytes fails the decryption. Use **PKCS7** for data that can end in zero bytes. ## Methods @@ -768,8 +768,8 @@ Syntax: *object*.**GenerateRandomIV** ( ) **As Byte()** Returns a **Byte** array of 16 bytes, which is also the new value of **IV**. The method calls `Randomize`, which mixes the system timer into the state of `Rnd` for the whole program, and then takes 16 values of `Rnd`. -> [!WARNING] -> In version 1.0.0.4 the bytes come from `Rnd`, a pseudo-random generator, and not from a cryptographic one; the comment in the source says that the Crypto API would give better randomness. The state of `Rnd` has 24 bits: its sequence repeats after 16,777,216 values, so the method can return at most 16,777,216 different IVs, and two calls that start from the same state of `Rnd` return the same IV. A program that needs an IV that cannot be predicted can set [**IV**](#iv) from a source of its own. +> [!WARNING] 1.0.0.4 +> The bytes come from `Rnd`, a pseudo-random generator, and not from a cryptographic one; the comment in the source says that the Crypto API would give better randomness. The state of `Rnd` has 24 bits: its sequence repeats after 16,777,216 values, so the method can return at most 16,777,216 different IVs, and two calls that start from the same state of `Rnd` return the same IV. A program that needs an IV that cannot be predicted can set [**IV**](#iv) from a source of its own. The first sample counts the calls of `Rnd` between a value and its next appearance. The second puts `Rnd` into the same state before each of two calls, with `Rnd -1` and `Randomize 7`, and finds two calls that return the same IV. @@ -838,8 +838,8 @@ Syntax: *object*.**SetGcmAADFromString** ( *aadString* [, *encoding*] ) The method does not raise an error. When the conversion fails, it prints `SetGcmAADFromString failed: ` and the description with `Debug.Print`, which a built program does not show, and leaves **GcmAAD** as it was. The program that decrypts has to convert the string in the same way, since the bytes with another encoding are other bytes. [**EncryptString**](#encryptstring) uses UTF-8 for its text, and this method does not. -> [!WARNING] -> In version 1.0.0.4 an empty string does not remove the additional data. The method fails for an empty string and keeps the data that was set before, and only the Debug Console shows the message `SetGcmAADFromString failed: Unspecified error`. The next GCM encryption then authenticates the old data. To remove the data, assign an array that was never sized to [**GcmAAD**](#gcmaad). +> [!WARNING] 1.0.0.4 +> An empty string does not remove the additional data. The method fails for an empty string and keeps the data that was set before, and only the Debug Console shows the message `SetGcmAADFromString failed: Unspecified error`. The next GCM encryption then authenticates the old data. To remove the data, assign an array that was never sized to [**GcmAAD**](#gcmaad). ```tb check_build check_run projname=aes-class Dim aes As New cAes @@ -872,8 +872,8 @@ Syntax: *object*.**SetKeyFromString** ( *keyString* [, *encoding*] ) The method converts *keyString* to bytes with **StrConv** and makes an array of the length that [**KeySize**](#keysize) names, 16, 24 or 32 bytes: the first bytes of the conversion when it is longer, and the bytes followed by zero bytes when it is shorter. The array is the new value of **Key**. The method does not use the string in any other way. -> [!WARNING] -> In version 1.0.0.4 there is no key derivation. The key is the bytes of the text, cut or extended with zero bytes: a string longer than the key is cut without a message, so that two strings that begin with the same 32 bytes give the same key (with **KeySize_256**), and a short string is a short key followed by zeros. The modes **ECB** and **CBC** then hash the bytes once with SHA-256, without a salt; the modes **CFB**, **OFB**, **CTR** and **GCM** use them as the key. With the default encoding, a character that is not in the ANSI code page of the system is converted to another character, and the same text gives different bytes on systems with another code page. +> [!WARNING] 1.0.0.4 +> There is no key derivation. The key is the bytes of the text, cut or extended with zero bytes: a string longer than the key is cut without a message, so that two strings that begin with the same 32 bytes give the same key (with **KeySize_256**), and a short string is a short key followed by zeros. The modes **ECB** and **CBC** then hash the bytes once with SHA-256, without a salt; the modes **CFB**, **OFB**, **CTR** and **GCM** use them as the key. With the default encoding, a character that is not in the ANSI code page of the system is converted to another character, and the same text gives different bytes on systems with another code page. The method raises an error when *keyString* is empty: **Err.Number** is `vbObjectError + 2`, **Err.Source** is `cAes.SetKeyFromString` and **Err.Description** is `Key cannot be empty`. **Key** is not changed, and [**IsInitialized**](#isinitialized) is set to **False**. When **KeySize** has a value that is not a member of the enumeration, the key is left as it was and no error is raised. diff --git a/docs/Reference/Contributed/TBMANLIB_Aes/index.md b/docs/Reference/Contributed/TBMANLIB_Aes/index.md index 71636d23a..0ae66013f 100644 --- a/docs/Reference/Contributed/TBMANLIB_Aes/index.md +++ b/docs/Reference/Contributed/TBMANLIB_Aes/index.md @@ -41,8 +41,8 @@ Debug.Print aes.DecryptString(cipherText) ' Hello, AES! The text that **EncryptString** returns is Base64 of the ciphertext alone. The program keeps the IV, and in GCM mode the tag, and sets them in the object that decrypts. -> [!WARNING] -> In version 1.0.0.4 the name of a cipher mode is not always the mode that runs: **ECB** runs CBC, **CBC** runs ECB, and **OFB** and **CTR** run CFB. Neither **ECB** nor **CBC** uses the IV, so the same key and plaintext always give the same ciphertext, and the default mode, **CBC**, gives equal ciphertext blocks for equal plaintext blocks. A test that decrypts what it has encrypted does not show this. [Known problems in version 1.0.0.4](#known-problems-in-version-1004) lists this and the other defects. +> [!WARNING] 1.0.0.4 +> The name of a cipher mode is not always the mode that runs: **ECB** runs CBC, **CBC** runs ECB, and **OFB** and **CTR** run CFB. Neither **ECB** nor **CBC** uses the IV, so the same key and plaintext always give the same ciphertext, and the default mode, **CBC**, gives equal ciphertext blocks for equal plaintext blocks. A test that decrypts what it has encrypted does not show this. [Known problems in version 1.0.0.4](#known-problems-in-version-1004) lists this and the other defects. * TOC {:toc} diff --git a/docs/Reference/Contributed/TBMANLIB_Aes/mAesConstants.md b/docs/Reference/Contributed/TBMANLIB_Aes/mAesConstants.md index 846558342..555e5f13e 100644 --- a/docs/Reference/Contributed/TBMANLIB_Aes/mAesConstants.md +++ b/docs/Reference/Contributed/TBMANLIB_Aes/mAesConstants.md @@ -40,8 +40,8 @@ The cipher mode that a [**cAes**](cAes) uses. Default: **CBC**. | **CTR** | 5 | CNG | CFB with an 8-bit feedback | | **GCM** | 6 | CNG | GCM, with a 12-byte nonce and a 16-byte tag | -> [!WARNING] -> In version 1.0.0.4 the name of a mode is not always the mode that runs. **ECB** runs CBC with an IV of zero bytes, **CBC** runs ECB, and **OFB** and **CTR** run CFB. Neither **ECB** nor **CBC** uses the IV that the program sets, so the same key and plaintext always give the same ciphertext, and the default mode, **CBC**, gives equal ciphertext blocks for equal plaintext blocks. Encryption and decryption agree with each other, so a round trip succeeds and shows nothing. [**cAes**](cAes#which-mode-runs) describes each mode in detail. +> [!WARNING] 1.0.0.4 +> The name of a mode is not always the mode that runs. **ECB** runs CBC with an IV of zero bytes, **CBC** runs ECB, and **OFB** and **CTR** run CFB. Neither **ECB** nor **CBC** uses the IV that the program sets, so the same key and plaintext always give the same ciphertext, and the default mode, **CBC**, gives equal ciphertext blocks for equal plaintext blocks. Encryption and decryption agree with each other, so a round trip succeeds and shows nothing. [**cAes**](cAes#which-mode-runs) describes each mode in detail. The Crypto API is the older interface of Windows (`advapi32.dll`), and CNG, Cryptography API: Next Generation, the newer one (`bcrypt.dll`). **cAes** uses the Crypto API for **ECB** and **CBC**, and CNG for the other four modes. @@ -70,8 +70,8 @@ How [**cAes**](cAes) fills the last block of the data before it encrypts, and ho | **NoPadding** | 3 | Adds nothing. | Takes nothing off. | | **ISO10126** | 4 | Adds nothing, as **NoPadding**. | Takes nothing off, as **NoPadding**. | -> [!WARNING] -> **ISO10126** is not implemented in version 1.0.0.4. It behaves as **NoPadding**, and no error says so. The comment in the source calls the member "ISO/IEC 7816-4 padding", a scheme that differs from ISO 10126, and neither is applied. +> [!WARNING] 1.0.0.4 +> **ISO10126** is not implemented. It behaves as **NoPadding**, and no error says so. The comment in the source calls the member "ISO/IEC 7816-4 padding", a scheme that differs from ISO 10126, and neither is applied. The functions that add and take off **PKCS7** and **ZeroPadding** padding are public members of [**cAesHelper**](cAesHelper). The padding of the data is only part of what the Crypto API modes do: Windows adds its own padding to the output of **ECB** and **CBC**, whatever the padding mode is. See [**PaddingMode**](cAes#paddingmode). diff --git a/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md b/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md index 5dfc3f486..2d4d019aa 100644 --- a/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md +++ b/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md @@ -35,8 +35,8 @@ con.ReadKey True con.Restore ``` -> [!WARNING] -> In version 1.0.0.1 the components that take a **cConsole** cannot be used with the published package [TBMANLIB_ConsoleCore](../TBMANLIB_ConsoleCore/). The package compiles, but the constructors of [**cProgressBar**](cProgressBar), [**cSparkline**](cSparkline), [**cVBar**](cVBar), [**cDataGrid**](cDataGrid), [**cConsoleLayout**](cConsoleLayout) and [**cLayoutHelper**](Helpers), and the members of [**cConsolePanel**](cConsolePanel) that take a **cConsoleBuffer**, raise error -2147467262, *No such interface supported*, when they are given an object of the published ConsoleCore, and a program cannot read the **ConsoleRect** and **ConsolePosition** values that the components return. The package carries a copy of ConsoleCore of its own, and the compiler (BETA 1005) treats the types of that copy as different from the types of the published package. [The two packages](#the-two-packages) has the details, and the one arrangement in which the components work. Only [**cConsoleTheme**](cConsoleTheme), the module **mConsoleThemeFactory**, [**cColorHelper**](Helpers#ccolorhelper-class) and the geometry of **cConsolePanel** work with the package as it is. The outputs that the samples on these pages show are those of the source files of the components compiled into a project that references ConsoleCore, and not of a project that references the two packages. +> [!WARNING] 1.0.0.1, BETA 1005 +> The components that take a **cConsole** cannot be used with the published package [TBMANLIB_ConsoleCore](../TBMANLIB_ConsoleCore/). The package compiles, but the constructors of [**cProgressBar**](cProgressBar), [**cSparkline**](cSparkline), [**cVBar**](cVBar), [**cDataGrid**](cDataGrid), [**cConsoleLayout**](cConsoleLayout) and [**cLayoutHelper**](Helpers), and the members of [**cConsolePanel**](cConsolePanel) that take a **cConsoleBuffer**, raise error -2147467262, *No such interface supported*, when they are given an object of the published ConsoleCore, and a program cannot read the **ConsoleRect** and **ConsolePosition** values that the components return. The package carries a copy of ConsoleCore of its own, and the compiler treats the types of that copy as different from the types of the published package. [The two packages](#the-two-packages) has the details, and the one arrangement in which the components work. Only [**cConsoleTheme**](cConsoleTheme), the module **mConsoleThemeFactory**, [**cColorHelper**](Helpers#ccolorhelper-class) and the geometry of **cConsolePanel** work with the package as it is. The outputs that the samples on these pages show are those of the source files of the components compiled into a project that references ConsoleCore, and not of a project that references the two packages. * TOC {:toc} diff --git a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsole.md b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsole.md index 72d1f4c8a..40828f3d7 100644 --- a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsole.md +++ b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsole.md @@ -82,8 +82,8 @@ With the input read from the null device or from a file, [**ReadLine**](#readlin ## Known problems in version 1.0.0.1 -> [!WARNING] -> In version 1.0.0.1, [**ReadKey**](#readkey) and [**ReadPassword**](#readpassword) never return when the program has no console input: `ReadKey` loops until `ReadConsoleInputW` delivers a key-down event, and the call fails at once when the program has no console or its input is not a console. A comparison of a key with a member of the [**ConsoleKey**](Types#consolekey) enumeration is true for the wrong key and raises no error: [**KeyCode**](Types#consolekeyinfo) holds Windows virtual-key codes, but the arrow, navigation and function keys of **ConsoleKey** have other values, so `keyInfo.KeyCode = ConsoleKey.KeyUp` is false for the Up Arrow key (38) and true for the H key (72). +> [!WARNING] 1.0.0.1 +> [**ReadKey**](#readkey) and [**ReadPassword**](#readpassword) never return when the program has no console input: `ReadKey` loops until `ReadConsoleInputW` delivers a key-down event, and the call fails at once when the program has no console or its input is not a console. A comparison of a key with a member of the [**ConsoleKey**](Types#consolekey) enumeration is true for the wrong key and raises no error: [**KeyCode**](Types#consolekeyinfo) holds Windows virtual-key codes, but the arrow, navigation and function keys of **ConsoleKey** have other values, so `keyInfo.KeyCode = ConsoleKey.KeyUp` is false for the Up Arrow key (38) and true for the H key (72). - **KeyAvailable and the key-up events.** [**KeyAvailable**](#keyavailable) is true while the input queue holds any event, including the event for a key that was released, so a loop that tests it and then calls [**ReadKey**](#readkey) can block in **ReadKey** until the next key press. - **The colours of the Write members.** [**WriteColored**](#writecolored), [**WriteLineColored**](#writelinecolored) and [**WriteAtColored**](#writeatcolored) use a black background unless one is given, and put back the colours that the object tracks, which are Gray on Black until a property sets others, and not the colours that the console had. diff --git a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsoleBuffer.md b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsoleBuffer.md index 40a697d24..bde8ed146 100644 --- a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsoleBuffer.md +++ b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/cConsoleBuffer.md @@ -89,8 +89,8 @@ Debug.Print r.Left & " " & r.Top & " " & r.Right & " " & r.Bottom ## Known problems in version 1.0.0.1 -> [!WARNING] -> In version 1.0.0.1, [**ScrollRegion**](#scrollregion) moves a region by less than the distance asked for when the region starts closer to the top or left edge than that distance, and clears the full distance, so cells are lost without an error. A [**WriteAt**](#writeat), [**FillRect**](#fillrect) or [**CopyRegion**](#copyregion) with a negative column or row makes the next [**Flush**](#flush) draw nothing, and raises no error; see [The dirty rectangle](#the-dirty-rectangle). +> [!WARNING] 1.0.0.1 +> [**ScrollRegion**](#scrollregion) moves a region by less than the distance asked for when the region starts closer to the top or left edge than that distance, and clears the full distance, so cells are lost without an error. A [**WriteAt**](#writeat), [**FillRect**](#fillrect) or [**CopyRegion**](#copyregion) with a negative column or row makes the next [**Flush**](#flush) draw nothing, and raises no error; see [The dirty rectangle](#the-dirty-rectangle). ## Constructor diff --git a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md index 6fa3fc91c..e8da09ca8 100644 --- a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md +++ b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md @@ -32,8 +32,8 @@ con.WriteLine con.Restore ``` -> [!WARNING] -> Several members of version 1.0.0.1 do not do what their names suggest. [**ReadKey**](cConsole#readkey) never returns in a program that has no console input, and a comparison of a key with a member of [**ConsoleKey**](Types#consolekey) is true for the wrong key. [Defects in version 1.0.0.1](#defects-in-version-1001) lists them with the pages that describe them. +> [!WARNING] 1.0.0.1 +> Several members do not do what their names suggest. [**ReadKey**](cConsole#readkey) never returns in a program that has no console input, and a comparison of a key with a member of [**ConsoleKey**](Types#consolekey) is true for the wrong key. [Defects in version 1.0.0.1](#defects-in-version-1001) lists them with the pages that describe them. * TOC {:toc} diff --git a/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md b/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md index 738bb7f9f..d7bc0967c 100644 --- a/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md +++ b/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md @@ -126,8 +126,8 @@ The package has four modules whose contents a program has no need for: ## Known problems in version 1.0.0.1 -> [!WARNING] -> Version 1.0.0.1 of the package gives a wrong result without an error, or loses data, in these cases. +> [!WARNING] 1.0.0.1 +> The package gives a wrong result without an error, or loses data, in these cases. > > - **[List.Insert](List#insert) puts the new item one place too late**, and **[List.SetItem](List#setitem) with the index 0 puts it in the second place.** `SetItem` with the index 0 on a list of one item removes the item and then raises error 5, so the list is empty afterwards. > - **[FileHelper.WriteFile](FileHelper#writefile) does not shorten an existing file.** The bytes after the new content stay: writing `ab` over a file of 13 bytes leaves a file of 13 bytes. @@ -139,8 +139,8 @@ The package has four modules whose contents a program has no need for: > - **[LocaleCollector](Collectors#localecollector) reports the offset of standard time as `Timezone` all year.** In summer time the item is behind the clock by the daylight saving offset: `UTC+1` where the clock is at UTC+2, as in Central European Summer Time. > - **[NetworkInfoCollector](Collectors#networkinfocollector) lists the first adapter only in a 64-bit program.** It reads the adapters in steps of 640 bytes, and the records are 704 bytes apart in a 64-bit program. The summary may then have no `Primary IP`. A 32-bit program lists every adapter. -> [!NOTE] -> Version 1.0.0.1 has these other problems. Each raises an error. +> [!NOTE] 1.0.0.1 +> The package has these other problems. Each raises an error. > > - **A [Dictionary](Dictionary) cannot be created.** `New Dictionary(Of TKey, TValue)` raises run-time error 91, *Object variable or With block variable not set*. > - **[List.Contains](List#contains), [IndexOf](List#indexof) and [Remove](List#remove) raise error 438**, *Object doesn't support this property or method*, for a list of objects of a class that has no default member. diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogCountdownMsgBox.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogCountdownMsgBox.md index 574a4385a..32a1676ac 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogCountdownMsgBox.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogCountdownMsgBox.md @@ -26,8 +26,8 @@ Select Case answer End Select ``` -> [!WARNING] -> In version 1.1.0.0 a box with only an OK button that closes because its time ran out reports [**drOK**](#dialogresult), not [**drTimeout**](#dialogresult), and [**IsTimedOut**](#istimedout) is **False**. This affects the default box and [**ShowInfo**](#showinfo), [**ShowWarning**](#showwarning) and [**ShowError**](#showerror). A box with any other set of buttons reports **drTimeout** when it times out. No error is raised, so a program cannot tell a press of OK from the end of the time with these members alone. +> [!WARNING] 1.1.0.0 +> A box with only an OK button that closes because its time ran out reports [**drOK**](#dialogresult), not [**drTimeout**](#dialogresult), and [**IsTimedOut**](#istimedout) is **False**. This affects the default box and [**ShowInfo**](#showinfo), [**ShowWarning**](#showwarning) and [**ShowError**](#showerror). A box with any other set of buttons reports **drTimeout** when it times out. No error is raised, so a program cannot tell a press of OK from the end of the time with these members alone. ```tb check_build Dim box As New cDialogCountdownMsgBox diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogDateTime.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogDateTime.md index 1981fa3e2..3ec5ac0b3 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogDateTime.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogDateTime.md @@ -42,11 +42,11 @@ The class reads the text like this: The result of [**ShowDialog**](#showdialog) is **False** when the user cancelled or typed a value that fails these rules, which also shows a second box, `Invalid date/time format. Please use:` followed by the text that the edit box offered. The result is **True** when the user pressed OK, and that includes an empty entry and text without a recognisable part (`hello` or `1/2`): such text changes nothing. -> [!WARNING] -> In version 1.1.0.0, when the text fails the check, the parts that the class has already read stay in the object. After `13/45/2026`, [**Month**](#month) is 13 and [**Day**](#day) is 45; after `02/30/2026`, **Day** is 30 in February; after `25:61`, [**Hour**](#hour) is 25 and [**Minute**](#minute) is 61; after `04/05/2027 25:00` the date is 5 April 2027 and the hour is 25. [**ShowDialog**](#showdialog) returns **False** and [**Cancelled**](#cancelled) is **True**, yet the object does not hold the value it held before. The setters of the parts, which refuse such values, are bypassed. A program that carries on with the object after a **False** result reads wrong values. Copy [**DateTimeValue**](#datetimevalue) before the call and restore it when the call returns **False**. +> [!WARNING] 1.1.0.0 +> When the text fails the check, the parts that the class has already read stay in the object. After `13/45/2026`, [**Month**](#month) is 13 and [**Day**](#day) is 45; after `02/30/2026`, **Day** is 30 in February; after `25:61`, [**Hour**](#hour) is 25 and [**Minute**](#minute) is 61; after `04/05/2027 25:00` the date is 5 April 2027 and the hour is 25. [**ShowDialog**](#showdialog) returns **False** and [**Cancelled**](#cancelled) is **True**, yet the object does not hold the value it held before. The setters of the parts, which refuse such values, are bypassed. A program that carries on with the object after a **False** result reads wrong values. Copy [**DateTimeValue**](#datetimevalue) before the call and restore it when the call returns **False**. -> [!WARNING] -> In version 1.1.0.0 a time with `AM` or `PM` and no date in front of it is not read correctly. `9:30 PM` is cut at its space into the date `9:30` and the time `PM` and rejected, so the offered text of the time mode with [**Is24HourFormat**](#is24hourformat) **False** (`02:05 PM`) is rejected when the user presses OK without changing it. `9:30pm`, with no space, is accepted and read as 21:03, because the class removes three characters from the end of the text and the third is a digit of the minutes. With a date in front, `03/15/2026 9:30 PM` is read correctly. Typing the time with a 24-hour clock (`21:30`) works in every mode. +> [!WARNING] 1.1.0.0 +> A time with `AM` or `PM` and no date in front of it is not read correctly. `9:30 PM` is cut at its space into the date `9:30` and the time `PM` and rejected, so the offered text of the time mode with [**Is24HourFormat**](#is24hourformat) **False** (`02:05 PM`) is rejected when the user presses OK without changing it. `9:30pm`, with no space, is accepted and read as 21:03, because the class removes three characters from the end of the text and the third is a digit of the minutes. With a date in front, `03/15/2026 9:30 PM` is read correctly. Typing the time with a 24-hour clock (`21:30`) works in every mode. ```tb check_build Dim picker As New cDialogDateTime @@ -495,8 +495,8 @@ Syntax: *object*.**FormatDate** ( [ *formatType* ] ) **As String** **ShortDate** gives `03/15/2026`, with month, day and year in that order. **LongDate** gives `Sunday, March 15, 2026`, in English whatever the language of Windows is. **Custom** gives the text of [**CustomFormat**](#customformat) in which these patterns are replaced, in this order: `YYYY` (four digits), `YY` (two digits), `MMMM` (the month's name), `MMM` (its first three letters), `MM` (two digits), `M`, `DD` (two digits) and `D`. With an empty [**CustomFormat**](#customformat), **Custom** gives the short date. -> [!WARNING] -> In version 1.1.0.0 the patterns are replaced one after the other in the text that is being built, so the letters `M` and `D` of a month's name, which `MMMM` and `MMM` have just put in, are replaced as well. **March** and **May** lose their first letter to the number of the month (`3arch`, `5ay`) and **December** its first letter to the number of the day (`15ecember`). No error is raised. The same happens to any other `Y`, `M` or `D` in the format, which is not kept as a literal character. Formats with `MMMM` or `MMM` are right only for the other months. +> [!WARNING] 1.1.0.0 +> The patterns are replaced one after the other in the text that is being built, so the letters `M` and `D` of a month's name, which `MMMM` and `MMM` have just put in, are replaced as well. **March** and **May** lose their first letter to the number of the month (`3arch`, `5ay`) and **December** its first letter to the number of the day (`15ecember`). No error is raised. The same happens to any other `Y`, `M` or `D` in the format, which is not kept as a literal character. Formats with `MMMM` or `MMM` are right only for the other months. ```tb check_build check_run Dim picker As New cDialogDateTime diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogEx.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogEx.md index c72ee856c..122f76732 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogEx.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogEx.md @@ -29,14 +29,14 @@ result = dlg.ShowOpen() If Len(result) > 0 Then Debug.Print "Chosen: " & result ``` -> [!WARNING] -> In version 1.1.0.0 the three dialogs return the **name** of the chosen file or folder, not its full path: [**ShowOpen**](#showopen), [**ShowSave**](#showsave), [**ShowBrowseForFolder**](#showbrowseforfolder) and [**SelectFiles**](#selectfiles) give `a.txt` for `C:\Reports\a.txt`, and `other` for the folder `C:\Reports\other`. A program that opens or saves the result opens or saves a file in its own current folder, and no error is raised. [**FileName**](#filename) holds the same short name afterwards. The class asks Windows for the name that is relative to the parent folder, because the constant `SIGDN_FILESYSPATH` of the source has the value `&H80058001` and the file-system path is `&H80058000`. The folder in which the user chose is not reported, so the result cannot be used to open or save the file. Use [**cDialog**](cDialog/), which returns full paths. +> [!WARNING] 1.1.0.0 +> The three dialogs return the **name** of the chosen file or folder, not its full path: [**ShowOpen**](#showopen), [**ShowSave**](#showsave), [**ShowBrowseForFolder**](#showbrowseforfolder) and [**SelectFiles**](#selectfiles) give `a.txt` for `C:\Reports\a.txt`, and `other` for the folder `C:\Reports\other`. A program that opens or saves the result opens or saves a file in its own current folder, and no error is raised. [**FileName**](#filename) holds the same short name afterwards. The class asks Windows for the name that is relative to the parent folder, because the constant `SIGDN_FILESYSPATH` of the source has the value `&H80058001` and the file-system path is `&H80058000`. The folder in which the user chose is not reported, so the result cannot be used to open or save the file. Use [**cDialog**](cDialog/), which returns full paths. -> [!WARNING] -> In version 1.1.0.0 [**InitialDir**](#initialdir) has no effect: the dialogs always open in the *Documents* folder, and no error is raised. The class makes the folder item with `SHCreateItemFromParsingName` and a GUID for `IShellItem` that has a wrong byte (`&H37` where the interface identifier has `&H7B`), so Windows answers that it has no such interface and the class skips `SetDefaultFolder`. +> [!WARNING] 1.1.0.0 +> [**InitialDir**](#initialdir) has no effect: the dialogs always open in the *Documents* folder, and no error is raised. The class makes the folder item with `SHCreateItemFromParsingName` and a GUID for `IShellItem` that has a wrong byte (`&H37` where the interface identifier has `&H7B`), so Windows answers that it has no such interface and the class skips `SetDefaultFolder`. -> [!WARNING] -> In version 1.1.0.0 a [**Filter**](#filter) is ignored unless it holds an even number of pipe characters (`|`). A filter of N pairs written `Name|Pattern|Name|Pattern` holds 2N minus 1, an odd number, so the dialog shows no file-type list and accepts every file, and no error is raised. A pipe after the last pattern makes the count even: `Text (*.txt)|*.txt|All (*.*)|*.*|` shows two entries. [**AddFilter**](#addfilter) and the four **Set...Filters** methods build a filter without the closing pipe, and so does the text of [**GetPresetFilter**](#getpresetfilter), so none of them shows a list until the text gets its pipe: add `dlg.Filter = dlg.Filter & "|"` after them. +> [!WARNING] 1.1.0.0 +> A [**Filter**](#filter) is ignored unless it holds an even number of pipe characters (`|`). A filter of N pairs written `Name|Pattern|Name|Pattern` holds 2N minus 1, an odd number, so the dialog shows no file-type list and accepts every file, and no error is raised. A pipe after the last pattern makes the count even: `Text (*.txt)|*.txt|All (*.*)|*.*|` shows two entries. [**AddFilter**](#addfilter) and the four **Set...Filters** methods build a filter without the closing pipe, and so does the text of [**GetPresetFilter**](#getpresetfilter), so none of them shows a list until the text gets its pipe: add `dlg.Filter = dlg.Filter & "|"` after them. ## Which dialog uses which property diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogFindReplace.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogFindReplace.md index 3e1925f35..9cde06229 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogFindReplace.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogFindReplace.md @@ -10,8 +10,8 @@ has_toc: false A **cDialogFindReplace** shows the Windows Find dialog or the Find and Replace dialog. The dialog is modeless: [**ShowFind**](#showfind) and [**ShowReplace**](#showreplace) return at once, and the dialog stays open beside the program's own window. The class holds the text, the options and the last action of the user as properties, and builds the line of text that describes the search. -> [!WARNING] -> In version 1.1.0.0 the program never learns what the user did in the dialog. [**ShowFind**](#showfind) and [**ShowReplace**](#showreplace) keep the structure that Windows works on in a local variable, which no longer exists when the method returns, although Windows keeps its address for as long as the dialog is open. The owner window then receives no message for **Find Next**, **Replace**, **Replace All** or **Cancel**, [**LastAction**](#lastaction) stays **None**, and [**hWnd**](#hwnd) and [**IsVisible**](#isvisible) still show a dialog after it was closed. In a 32-bit program, **ShowReplace** can also end the program with an access violation while the dialog is open, before anything is pressed, because Windows reads memory that has been reused. See [How the dialog reports](#how-the-dialog-reports). A program that needs the dialog calls `FindTextW` or `ReplaceTextW` itself and keeps the `FINDREPLACE` structure in a module-level variable. +> [!WARNING] 1.1.0.0 +> The program never learns what the user did in the dialog. [**ShowFind**](#showfind) and [**ShowReplace**](#showreplace) keep the structure that Windows works on in a local variable, which no longer exists when the method returns, although Windows keeps its address for as long as the dialog is open. The owner window then receives no message for **Find Next**, **Replace**, **Replace All** or **Cancel**, [**LastAction**](#lastaction) stays **None**, and [**hWnd**](#hwnd) and [**IsVisible**](#isvisible) still show a dialog after it was closed. In a 32-bit program, **ShowReplace** can also end the program with an access violation while the dialog is open, before anything is pressed, because Windows reads memory that has been reused. See [How the dialog reports](#how-the-dialog-reports). A program that needs the dialog calls `FindTextW` or `ReplaceTextW` itself and keeps the `FINDREPLACE` structure in a module-level variable. **Properties:** [**AllowMatchCase**](#allowmatchcase), [**AllowUpDown**](#allowupdown), [**AllowWholeWord**](#allowwholeword), [**Direction**](#direction), [**FindWhat**](#findwhat), [**hWnd**](#hwnd), [**LastAction**](#lastaction), [**LastError**](#lasterror), [**MatchCase**](#matchcase), [**MessageId**](#messageid), [**Mode**](#mode), [**OwnerHwnd**](#ownerhwnd), [**ReplaceWith**](#replacewith), [**WholeWord**](#wholeword). **Methods:** [**IsVisible**](#isvisible), [**Reset**](#reset), [**ShowFind**](#showfind), [**ShowReplace**](#showreplace), [**ToDescription**](#todescription), [**UpdateFromFlags**](#updatefromflags). diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogInputBox.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogInputBox.md index 4ecca0f0b..0392d76cc 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogInputBox.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogInputBox.md @@ -43,8 +43,8 @@ The text of the box is [**Prompt**](#prompt), followed by a blank line and `(Hin An empty string passes when [**AllowEmpty**](#allowempty) is **True** (the default) and no [**MinLength**](#minlength) is set. The result of the call does not tell the user's cancelling apart from a refusal; both give **False** and [**Cancelled**](#cancelled) **True**. The two differ only in [**Value**](#value). -> [!WARNING] -> In version 1.1.0.0 the properties [**Multiline**](#multiline), [**PasswordChar**](#passwordchar), [**OwnerHwnd**](#ownerhwnd) and [**CustomPattern**](#custompattern) are stored but not used. The box is always a single line, it shows what is typed without masking it, it has no owner window and the owner stays usable while it is open, and the value **Custom** of [**Validation**](#validation) accepts any text that is not empty. A program that sets [**PasswordChar**](#passwordchar) to hide a secret shows the secret on the screen. [**cDialogPassword**](cDialogPassword) masks its entry. +> [!WARNING] 1.1.0.0 +> The properties [**Multiline**](#multiline), [**PasswordChar**](#passwordchar), [**OwnerHwnd**](#ownerhwnd) and [**CustomPattern**](#custompattern) are stored but not used. The box is always a single line, it shows what is typed without masking it, it has no owner window and the owner stays usable while it is open, and the value **Custom** of [**Validation**](#validation) accepts any text that is not empty. A program that sets [**PasswordChar**](#passwordchar) to hide a secret shows the secret on the screen. [**cDialogPassword**](cDialogPassword) masks its entry. ## Properties diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPageSetup.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPageSetup.md index 4b5e04243..ab926f7f4 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPageSetup.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPageSetup.md @@ -10,8 +10,8 @@ has_toc: false A **cDialogPageSetup** shows the Windows Page Setup dialog, in which the user picks the paper, its orientation and the four margins of a page. It holds the page as properties, in inches, with ready-made sizes and margins, and works out the printable area. -> [!WARNING] -> Three things in version 1.1.0.0 give a wrong result without an error. [**PaperWidth**](#paperwidth) and [**PaperHeight**](#paperheight) are not passed to the dialog, so it opens with the paper of the default printer whatever they hold. [**Orientation**](#orientation) is neither passed to the dialog nor read from it: a user who chooses *Landscape* leaves **Orientation** at **Portrait** and swaps the paper width and height instead. With [**Unit**](#unit) set to **Millimeters**, [**ToDescription**](#todescription) writes the values in inches and labels them `mm`. +> [!WARNING] 1.1.0.0 +> Three things give a wrong result without an error. [**PaperWidth**](#paperwidth) and [**PaperHeight**](#paperheight) are not passed to the dialog, so it opens with the paper of the default printer whatever they hold. [**Orientation**](#orientation) is neither passed to the dialog nor read from it: a user who chooses *Landscape* leaves **Orientation** at **Portrait** and swaps the paper width and height instead. With [**Unit**](#unit) set to **Millimeters**, [**ToDescription**](#todescription) writes the values in inches and labels them `mm`. **Properties:** [**AllowMargins**](#allowmargins), [**AllowOrientation**](#alloworientation), [**AllowPaper**](#allowpaper), [**DefaultMinMargins**](#defaultminmargins), [**MarginBottom**](#marginbottom), [**MarginLeft**](#marginleft), [**MarginRight**](#marginright), [**MarginTop**](#margintop), [**Orientation**](#orientation), [**OwnerHwnd**](#ownerhwnd), [**PaperHeight**](#paperheight), [**PaperWidth**](#paperwidth), [**Unit**](#unit). **Methods:** [**GetPrintableHeight**](#getprintableheight), [**GetPrintableWidth**](#getprintablewidth), [**Reset**](#reset), [**SetA4Size**](#seta4size), [**SetDefaultMargins**](#setdefaultmargins), [**SetLegalSize**](#setlegalsize), [**SetLetterSize**](#setlettersize), [**SetNarrowMargins**](#setnarrowmargins), [**ShowDialog**](#showdialog), [**ToDescription**](#todescription). diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPassword.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPassword.md index 69b1e2365..1df4b29ed 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPassword.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogPassword.md @@ -38,8 +38,8 @@ The class masks the edit box with the character of [**PasswordChar**](#passwordc [**ShowDialog**](#showdialog) repeats the question until it has a password that passes or [**MaxAttempts**](#maxattempts) attempts have failed. A failure shows a message box with the reason and `Attempts remaining: N`, and the last failure shows the reason and `Maximum attempts reached.`. If [**RequireConfirm**](#requireconfirm) is **True**, a password that passes is asked for again in a second box, `Please confirm your password:` under the title `Title - Confirm`, and a difference counts as a failed attempt (`Passwords do not match!`). Cancelling any box ends the call. -> [!WARNING] -> In version 1.1.0.0 only the first box of [**ShowDialog**](#showdialog) is masked. The confirmation box of [**RequireConfirm**](#requireconfirm) and the three boxes of [**ShowChangeDialog**](#showchangedialog) (`Title - Current Password`, `Title - New Password` and `Title - Confirm New Password`) show what the user types in clear text, because the timer looks for a window with exactly the title [**Title**](#title). A program that asks for a confirmation or a change of password shows the passwords on the screen. To keep the entry masked, ask with [**ShowDialog**](#showdialog) alone and compare the two entries in the program, or use another control. +> [!WARNING] 1.1.0.0 +> Only the first box of [**ShowDialog**](#showdialog) is masked. The confirmation box of [**RequireConfirm**](#requireconfirm) and the three boxes of [**ShowChangeDialog**](#showchangedialog) (`Title - Current Password`, `Title - New Password` and `Title - Confirm New Password`) show what the user types in clear text, because the timer looks for a window with exactly the title [**Title**](#title). A program that asks for a confirmation or a change of password shows the passwords on the screen. To keep the entry masked, ask with [**ShowDialog**](#showdialog) alone and compare the two entries in the program, or use another control. > [!NOTE] > The properties [**ShowStrength**](#showstrength), [**AllowShowPassword**](#allowshowpassword) and [**OwnerHwnd**](#ownerhwnd) are stored but not used: the box has no strength indicator, no control that shows the password and no owner window. The password stays in [**Password**](#password), an ordinary string, until [**Reset**](#reset) empties it or the object ends. diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/index.md b/docs/Reference/Contributed/TBMANLIB_Dialog/index.md index 348baf21c..0cd42760c 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/index.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/index.md @@ -21,9 +21,8 @@ exclude_from_docs: A twinBASIC package of dialogs. Its first class, **cDialog**, shows the Windows Open, Save As and Browse For Folder dialogs, builds the file-type filters of the first two, splits a file path into folder, name and extension, and opens a file in the program that Windows associates with it; its property names follow VB6's **CommonDialog** control. **cDialogEx** shows the Open, Save As and folder dialogs of Windows Vista and later. Five more classes show the Windows Color, Font, Find and Replace, Page Setup and Print dialogs, and five classes ask for a value or report progress (a message box with a time limit, a date and time, an input box, a password and a progress state); the last five use the run-time's own **InputBox** and **MsgBox** or draw nothing. Every class except **cDialog** is marked as not creatable with **CreateObject**, and is made with **New**. The module **mDialog** holds the types and conversion functions that they share. This section documents version 1.1.0.0, which added every class except **cDialog**. The **CommonDialog** class of the [VBComDlg](../VBComDlg/CommonDialog) package is a fuller alternative for the Windows common dialogs, with flags, a filter index and cancel errors. -> [!WARNING] -> -> Several classes of version 1.1.0.0 fail at their main task, and no error is raised. [**cDialogFindReplace**](cDialogFindReplace) reports nothing to the program, and its **ShowReplace** can end a 32-bit program. [**cDialogPageSetup**](cDialogPageSetup) ignores the paper size and the orientation the program sets, and does not read back the orientation the user chose. [**cDialogEx**](cDialogEx) returns file and folder names without their folder, ignores **InitialDir**, and shows no file types for a filter written in the usual form. [**cDialogPassword**](cDialogPassword) shows the password as it is typed in its confirmation boxes. [**cDialogDateTime**](cDialogDateTime) keeps the parts of an entry it refused, refuses a time with AM or PM and no date, and changes the letters of a month name in a custom format. Four properties of [**cDialogInputBox**](cDialogInputBox), among them **PasswordChar**, have no effect. Each class's page describes its defects. [**cDialog**](cDialog/), the class for the Open, Save As and Browse For Folder dialogs, works. +> [!WARNING] 1.1.0.0 +> Several classes fail at their main task, and no error is raised. [**cDialogFindReplace**](cDialogFindReplace) reports nothing to the program, and its **ShowReplace** can end a 32-bit program. [**cDialogPageSetup**](cDialogPageSetup) ignores the paper size and the orientation the program sets, and does not read back the orientation the user chose. [**cDialogEx**](cDialogEx) returns file and folder names without their folder, ignores **InitialDir**, and shows no file types for a filter written in the usual form. [**cDialogPassword**](cDialogPassword) shows the password as it is typed in its confirmation boxes. [**cDialogDateTime**](cDialogDateTime) keeps the parts of an entry it refused, refuses a time with AM or PM and no date, and changes the letters of a month name in a custom format. Four properties of [**cDialogInputBox**](cDialogInputBox), among them **PasswordChar**, have no effect. Each class's page describes its defects. [**cDialog**](cDialog/), the class for the Open, Save As and Browse For Folder dialogs, works. ## Classes diff --git a/docs/Reference/Contributed/TBMANLIB_Xml/cXml.md b/docs/Reference/Contributed/TBMANLIB_Xml/cXml.md index 895cf3bee..395321710 100644 --- a/docs/Reference/Contributed/TBMANLIB_Xml/cXml.md +++ b/docs/Reference/Contributed/TBMANLIB_Xml/cXml.md @@ -30,8 +30,8 @@ Debug.Print x.ToXmlString(False) ' DuneEmmachecked ``` -> [!WARNING] -> In version 1.0.0.1, the parser reads some valid XML wrongly and raises no error: a self-closing element, `&` and other entities in text, a tag that uses both kinds of quote or has a line break between its attributes, and a processing instruction. A comment, a CDATA section or a DOCTYPE declaration in the wrong place makes **Parse** loop forever. [Defects in version 1.0.0.1](#defects) lists each case. +> [!WARNING] 1.0.0.1 +> The parser reads some valid XML wrongly and raises no error: a self-closing element, `&` and other entities in text, a tag that uses both kinds of quote or has a line break between its attributes, and a processing instruction. A comment, a CDATA section or a DOCTYPE declaration in the wrong place makes **Parse** loop forever. [Defects in version 1.0.0.1](#defects) lists each case. * TOC {:toc} diff --git a/docs/Reference/Contributed/TBMANLIB_Xml/index.md b/docs/Reference/Contributed/TBMANLIB_Xml/index.md index ab567f548..c0b4862af 100644 --- a/docs/Reference/Contributed/TBMANLIB_Xml/index.md +++ b/docs/Reference/Contributed/TBMANLIB_Xml/index.md @@ -41,8 +41,8 @@ Debug.Print y.ToXmlString(False) ' Dune ``` -> [!WARNING] -> The parser of version 1.0.0.1 reads some valid XML wrongly and raises no error, and for some text it never returns. [Defects in version 1.0.0.1](cXml#defects) lists the cases: self-closing elements, entities in text, tags with both kinds of quote or with line breaks, processing instructions, and comments, CDATA sections and DOCTYPE declarations in some positions. +> [!WARNING] 1.0.0.1 +> The parser reads some valid XML wrongly and raises no error, and for some text it never returns. [Defects in version 1.0.0.1](cXml#defects) lists the cases: self-closing elements, entities in text, tags with both kinds of quote or with line breaks, processing instructions, and comments, CDATA sections and DOCTYPE declarations in some positions. > [!WARNING] > The XPath engine reads some valid expressions wrongly and raises no error: the step `..`, axes such as `ancestor::`, positions after `//`, and the operators `and` and `or`. [Defects in version 1.0.0.1](mXPath#defects) lists the cases. diff --git a/docs/Reference/Contributed/VBComDlg/CommonDialog.md b/docs/Reference/Contributed/VBComDlg/CommonDialog.md index 666b452d2..b560a7a4e 100644 --- a/docs/Reference/Contributed/VBComDlg/CommonDialog.md +++ b/docs/Reference/Contributed/VBComDlg/CommonDialog.md @@ -756,8 +756,8 @@ Declared for the **Replace All** button of the Replace dialog. Syntax: *object*\_**ReplaceAll**( ) -> [!NOTE] -> In version 1.0.13.0 of the package the class never raises this event. Its test for the **Replace All** button compares the masked flags with **CdlFRFindNext** instead of **CdlFRReplaceAll**, so the comparison is never true. Clicking **Replace All** raises none of the events. +> [!NOTE] 1.0.13.0 +> Of the package the class never raises this event. Its test for the **Replace All** button compares the masked flags with **CdlFRFindNext** instead of **CdlFRReplaceAll**, so the comparison is never true. Clicking **Replace All** raises none of the events. ## Example diff --git a/docs/Reference/Contributed/WinReg/index.md b/docs/Reference/Contributed/WinReg/index.md index c07326ef1..0d3abe678 100644 --- a/docs/Reference/Contributed/WinReg/index.md +++ b/docs/Reference/Contributed/WinReg/index.md @@ -15,8 +15,8 @@ The **WinReg** contributed package provides a Windows Registry helper class that The root keys, such as `HKEY_CURRENT_USER`, are members of **RegKeyspace**, so a project names them with **WinReg** alone. **WinReg** references the **WinDevLib** package, but a project does not see the names a package's own references declare. A project that names `BOOL` or `REGTYPES`, or calls `RegOpenKeyEx` or `RegCloseKey` and uses the `KEY_*` constants, as some examples here do, references **WinDevLib** as well. One that declares the **Dictionary** that [**EnumValues**](RegCls/EnumValues) returns references the Microsoft Scripting Runtime. -> [!NOTE] -> Version 1.0.1.1 does not compile in twinBASIC BETA 1005, so a project that references it does not build. **RegCls** declares `RegCreateKeyEx` and `RegEnumKeyEx` with a `ByRef` structure parameter, *lpSecurityAttributes* (`SECURITY_ATTRIBUTES`) and *lpftLastWriteTime* (`FILETIME`), and passes `VarPtr(0&)` for it in [**WriteString**](RegCls/WriteString), [**WriteDWORD**](RegCls/WriteDWORD), [**CreateKey**](RegCls/CreateKey) and [**DeleteKeyEx**](RegCls/DeleteKeyEx). A pointer passed for a structure has to be passed `ByVal` (error TB5001). `VarPtr(0&)` is also the address of a temporary **Long**, not a null pointer; `ByVal vbNullPtr` passes a null pointer. The behaviour described on these pages is that of the package with those four calls passed `ByVal`. +> [!NOTE] 1.0.1.1, BETA 1005 +> The package does not compile, so a project that references it does not build. **RegCls** declares `RegCreateKeyEx` and `RegEnumKeyEx` with a `ByRef` structure parameter, *lpSecurityAttributes* (`SECURITY_ATTRIBUTES`) and *lpftLastWriteTime* (`FILETIME`), and passes `VarPtr(0&)` for it in [**WriteString**](RegCls/WriteString), [**WriteDWORD**](RegCls/WriteDWORD), [**CreateKey**](RegCls/CreateKey) and [**DeleteKeyEx**](RegCls/DeleteKeyEx). A pointer passed for a structure has to be passed `ByVal` (error TB5001). `VarPtr(0&)` is also the address of a temporary **Long**, not a null pointer; `ByVal vbNullPtr` passes a null pointer. The behaviour described on these pages is that of the package with those four calls passed `ByVal`. > [!WARNING] > Three methods give wrong results. [**ReadString**](RegCls/ReadString) returns the text followed by NUL characters. [**GetRegistryValueType**](RegCls/GetRegistryValueType) returns **REG_NONE** for every value that holds data. The first overload of [**ConvertValueToString**](RegCls/ConvertValueToString) returns an empty string for the types it supports. diff --git a/docs/Reference/Contributed/tbFileSysTools/File.md b/docs/Reference/Contributed/tbFileSysTools/File.md index d89871333..bfa041d66 100644 --- a/docs/Reference/Contributed/tbFileSysTools/File.md +++ b/docs/Reference/Contributed/tbFileSysTools/File.md @@ -362,8 +362,8 @@ Syntax: *object*.**SetAttribute** ( *attr*, *state* ) : *required* A **Boolean**. **True** sets the attribute and **False** clears it. The other attributes stay as they are. -> [!WARNING] -> In version 1.9.0.0, **SetAttribute** sets nothing, and raises no error, on a file that has no attribute at all, because [**Attributes**](#attributes) then reports **faNormal**, and **faNormal** wins over the attribute being set. A new file has the archive attribute, so it is a file from which the program cleared every attribute that this applies to. To give such a file an attribute, assign [**Attributes**](#attributes). The same method of a [**Folder**](Folder#setattribute) is not affected. +> [!WARNING] 1.9.0.0 +> **SetAttribute** sets nothing, and raises no error, on a file that has no attribute at all, because [**Attributes**](#attributes) then reports **faNormal**, and **faNormal** wins over the attribute being set. A new file has the archive attribute, so it is a file from which the program cleared every attribute that this applies to. To give such a file an attribute, assign [**Attributes**](#attributes). The same method of a [**Folder**](Folder#setattribute) is not affected. ```tb check_build check_run Dim work As String, f As File diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md index bb1a4ca15..3f22abc5d 100644 --- a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md +++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md @@ -354,8 +354,8 @@ Both paths are first made absolute with [**GetAbsolutePathName**](#getabsolutepa Returns the absolute *targetPath* when there is no relative path, for example when the two paths are on different drives, and when only *targetPath* is longer than 260 characters. -> [!WARNING] -> In version 1.9.0.0, when *basePath* is 259 characters or longer, the result can be a relative path that leads to the wrong place, and no error is raised. For a target `x\y.txt` below such a base, the result starts with `..\` and the last name of the base, instead of being `x\y.txt`. A base path of 258 characters or fewer gives the right result. +> [!WARNING] 1.9.0.0 +> When *basePath* is 259 characters or longer, the result can be a relative path that leads to the wrong place, and no error is raised. For a target `x\y.txt` below such a base, the result starts with `..\` and the last name of the base, instead of being `x\y.txt`. A base path of 258 characters or fewer gives the right result. ```tb check_build check_run Debug.Print GetRelativePath("C:\Projects\App", "C:\Projects\Lib\core.txt") ' ..\Lib\core.txt @@ -689,8 +689,8 @@ Errors: 53 when a wildcard matches no file, or matches only folders; 76 when the > A *source* without a wildcard that names a folder is moved too, with no error: `MoveFile "C:\old", "C:\new"` renames the folder. The Scripting Runtime raises error 53. -> [!WARNING] -> In version 1.9.0.0, a *destination* that is the reserved device name `nul`, in any letter case and with or without an extension (`nul`, `NUL`, `nul.txt`), is not refused. **MoveFile** raises no error, and the file is gone: its contents were moved to the NUL device and cannot be recovered. The Scripting Runtime raises error 58 and leaves the file. [**File.Move**](File#move) and the **MoveFile** of [**FileSystemObject**](FileSystemObject#movefile) do the same; **File.Move** then raises error 53. +> [!WARNING] 1.9.0.0 +> A *destination* that is the reserved device name `nul`, in any letter case and with or without an extension (`nul`, `NUL`, `nul.txt`), is not refused. **MoveFile** raises no error, and the file is gone: its contents were moved to the NUL device and cannot be recovered. The Scripting Runtime raises error 58 and leaves the file. [**File.Move**](File#move) and the **MoveFile** of [**FileSystemObject**](FileSystemObject#movefile) do the same; **File.Move** then raises error 53. ### MoveFolder {: .no_toc } diff --git a/docs/Reference/Contributed/tbFileSysTools/Folder.md b/docs/Reference/Contributed/tbFileSysTools/Folder.md index 7c418acd6..598e2208e 100644 --- a/docs/Reference/Contributed/tbFileSysTools/Folder.md +++ b/docs/Reference/Contributed/tbFileSysTools/Folder.md @@ -106,8 +106,8 @@ Syntax: *object*.**Files** **As Files** Returns a [**Files**](Files) collection of the files directly in the folder. Hidden and system files are in it, which differs from [**GetFilePaths**](FileSystemTools#getfilepaths). The folder is read when the property is read, and the collection does not change afterwards. Raises error 70 when the folder cannot be read, as the Scripting Runtime does. -> [!NOTE] -> In version 1.9.0.0, **Files**, [**SubFolders**](#subfolders) and [**Size**](#size) raise error 76 (*Path not found*) for a folder whose path has 258 characters or more, although the folder exists and its [**Name**](#name) can be read. The same members work for a folder with 256 characters. +> [!NOTE] 1.9.0.0 +> **Files**, [**SubFolders**](#subfolders) and [**Size**](#size) raise error 76 (*Path not found*) for a folder whose path has 258 characters or more, although the folder exists and its [**Name**](#name) can be read. The same members work for a folder with 256 characters. ```tb check_build check_run Dim work As String, deep As String, n As Long diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md index 784bd1be7..753533395 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md @@ -28,8 +28,8 @@ Check **IsValid** before [**Execute**](Execute), [**Test**](Test), [**Replace**] > Although nothing is raised, the compile error of an invalid pattern stays in the **Err** object after the call that compiled the pattern returns. A program that tests **Err** for other errors calls **Err.Clear** first. -> [!NOTE] -> A quantifier whose lower count is above its upper count, such as `a{2,1}`, is not found when the pattern is compiled, so **IsValid** returns **True** for it and [**LastErrorNumber**](LastErrorNumber) returns 0. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise error 3000 when a search reaches the quantifier, with the description that Windows gives that number, *The specified print monitor is unknown.* This is how version 1.8.0.0 behaves. +> [!NOTE] 1.8.0.0 +> A quantifier whose lower count is above its upper count, such as `a{2,1}`, is not found when the pattern is compiled, so **IsValid** returns **True** for it and [**LastErrorNumber**](LastErrorNumber) returns 0. [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) and [**Split**](Split) then raise error 3000 when a search reaches the quantifier, with the description that Windows gives that number, *The specified print monitor is unknown.* ## Example diff --git a/docs/Reference/Contributed/tsJson/JsonOptions.md b/docs/Reference/Contributed/tsJson/JsonOptions.md index 88b43b723..1f66ae549 100644 --- a/docs/Reference/Contributed/tsJson/JsonOptions.md +++ b/docs/Reference/Contributed/tsJson/JsonOptions.md @@ -41,8 +41,8 @@ A property of an options object checks nothing when it is set. The value is used > ' 0|0|False||0|0 > ``` -> [!NOTE] -> The class declares a **Private** constructor and the attribute `[COMCreatable(False)]`, so that only its presets create options objects. In BETA 1005 twinBASIC does not enforce a **Private** constructor, and a project that references the package can still create an object with **New**. Every property of such an object has the empty value of its type: **Indent** 0, an empty **Newline**, **Syntax** **jsRfc8259**, and a limit of 0, which means no limit. The text it writes has no line breaks and no indentation, but still a space after each colon. The presets are the way to get the defaults. +> [!NOTE] BETA 1005 +> The class declares a **Private** constructor and the attribute `[COMCreatable(False)]`, so that only its presets create options objects. twinBASIC does not enforce a **Private** constructor, and a project that references the package can still create an object with **New**. Every property of such an object has the empty value of its type: **Indent** 0, an empty **Newline**, **Syntax** **jsRfc8259**, and a limit of 0, which means no limit. The text it writes has no line breaks and no indentation, but still a space after each colon. The presets are the way to get the defaults. > > ```tb check_build check_run > Dim jo As JsonOptions = New JsonOptions diff --git a/docs/Reference/Contributed/tsJson/index.md b/docs/Reference/Contributed/tsJson/index.md index 02e51bcd4..90cda2fcb 100644 --- a/docs/Reference/Contributed/tsJson/index.md +++ b/docs/Reference/Contributed/tsJson/index.md @@ -200,8 +200,8 @@ Debug.Print Tests.Run() ' mine ## Known problems in version 1.0.15.0 -> [!WARNING] -> In version 1.0.15.0 these problems give a wrong result with no error. +> [!WARNING] 1.0.15.0 +> These problems give a wrong result with no error. > > - **A choice in a regular expression is not tried again.** When one alternative matches and the rest of the pattern then fails, the matcher does not try the other alternatives, so `a|ab` does not match `ab`, and `(Mr|Mrs) Smith` does not match `Mrs Smith`, although `(Mrs|Mr) Smith` matches both. RFC 9485 defines the match of the whole pattern. A choice whose alternatives start the same way works when the longer alternative comes first. The matcher serves [**IsMatchPattern**](Json#ismatchpattern), [**IsSearchPattern**](Json#issearchpattern), the JSONPath functions `match` and `search`, and the schema keywords `pattern` and `patternProperties`. > - **`\n`, `\r` and `\t` are not understood.** RFC 9485 has these escapes. Outside a character set the pattern is invalid, and inside a set `[\n]` means the letter `n`. Write the control character itself in the pattern string, for example `ChrW(9)`, which is an ordinary character in the dialect. The escapes `\-` and `\^`, which the RFC also has, are invalid outside a set, and `[a-]` is invalid, although the RFC allows a last hyphen; put the hyphen first, as in `[-a]`. diff --git a/docs/Reference/Contributed/tsPwGen/PwGen.md b/docs/Reference/Contributed/tsPwGen/PwGen.md index 8a1ea28c3..5b8b0f518 100644 --- a/docs/Reference/Contributed/tsPwGen/PwGen.md +++ b/docs/Reference/Contributed/tsPwGen/PwGen.md @@ -21,8 +21,8 @@ Debug.Print token Like "*[!0-9a-f]*" ' False The module has no settings. Its only state is the last error, which [**PwGenError**](PwGenError) reports. A recipe that cannot be used does not raise a run-time error: the function returns an empty string, or 0, and sets the last error. Every function of the module clears the last error when it starts, so after a call the last error belongs to that call. -> [!NOTE] -> Write the module name in every call, as in `PwGen.Generate`. In version 1.0.8.0 the package's internal module **PwGenEngine** also declares public functions named **Generate** and **Entropy**, so a call written without the module name fails with TB5073, *'Entropy' is ambiguous*, or *'Generate' is ambiguous*. +> [!NOTE] 1.0.8.0 +> Write the module name in every call, as in `PwGen.Generate`. The package's internal module **PwGenEngine** also declares public functions named **Generate** and **Entropy**, so a call written without the module name fails with TB5073, *'Entropy' is ambiguous*, or *'Generate' is ambiguous*. > > ```tb check_build expect-error=TB5073 > Debug.Print CLng(Entropy()) diff --git a/docs/Reference/Contributed/tsPwGen/PwGenCryptoSource.md b/docs/Reference/Contributed/tsPwGen/PwGenCryptoSource.md index 5ccdb662d..c65e86527 100644 --- a/docs/Reference/Contributed/tsPwGen/PwGenCryptoSource.md +++ b/docs/Reference/Contributed/tsPwGen/PwGenCryptoSource.md @@ -55,8 +55,8 @@ Debug.Print PwGenError.Code ' InvalidRange Debug.Print PwGenError.Origin = "" ' True ``` -> [!WARNING] -> In version 1.0.8.0, **NextInRange** does not clear the last error before it starts, and it returns 0 whenever the last error is already set. It raises no error and sets no error of its own, so a direct call made after any earlier failure of the package returns 0 even when the bounds do not include 0, and the last error still belongs to the earlier failure. Call [**PwGenError.Clear**](PwGenError#clear) first. The functions of [**PwGen**](PwGen) do this themselves, so a recipe is not affected. +> [!WARNING] 1.0.8.0 +> **NextInRange** does not clear the last error before it starts, and it returns 0 whenever the last error is already set. It raises no error and sets no error of its own, so a direct call made after any earlier failure of the package returns 0 even when the bounds do not include 0, and the last error still belongs to the earlier failure. Call [**PwGenError.Clear**](PwGenError#clear) first. The functions of [**PwGen**](PwGen) do this themselves, so a recipe is not affected. > > ```tb check_build check_run > Dim src As PwGenRandom = New PwGenCryptoSource diff --git a/docs/Reference/Contributed/tsPwGen/PwGenOptions.md b/docs/Reference/Contributed/tsPwGen/PwGenOptions.md index ff854fa2c..92fc2bed0 100644 --- a/docs/Reference/Contributed/tsPwGen/PwGenOptions.md +++ b/docs/Reference/Contributed/tsPwGen/PwGenOptions.md @@ -42,8 +42,8 @@ A property of a recipe checks nothing when it is set. A value that cannot be use > Debug.Print "[" & PwGenOptions.WordSeparator & "]" ' [] > ``` -> [!NOTE] -> The class declares a **Private** constructor and the attribute `[COMCreatable(False)]`, so that only its presets create recipes. In BETA 1005 twinBASIC does not enforce a **Private** constructor, and a project that references the package can still create an object with **New**. It is a recipe in which every property has the empty value of its type: **Length** 0, no class enabled, empty strings, **MinPerClass** 0, **Mode** **pgmRandom** and **RandomSource** **Nothing**. **Generate** returns an empty string for it, with the error `InvalidLength`, until the properties are set. The presets are the way to get the defaults. +> [!NOTE] BETA 1005 +> The class declares a **Private** constructor and the attribute `[COMCreatable(False)]`, so that only its presets create recipes. twinBASIC does not enforce a **Private** constructor, and a project that references the package can still create an object with **New**. It is a recipe in which every property has the empty value of its type: **Length** 0, no class enabled, empty strings, **MinPerClass** 0, **Mode** **pgmRandom** and **RandomSource** **Nothing**. **Generate** returns an empty string for it, with the error `InvalidLength`, until the properties are set. The presets are the way to get the defaults. > > ```tb check_build check_run > Dim o As PwGenOptions = New PwGenOptions @@ -250,8 +250,8 @@ Debug.Print PwGen.Generate(o) = "" ' True Debug.Print PwGenError.Code ' CannotAvoidRepeats ``` -> [!NOTE] -> In version 1.0.8.0 the shuffle only rearranges the characters that were drawn, and the property never draws them again. When the drawn characters cannot be arranged without a repeat, **Generate** returns an empty string and sets `CannotAvoidRepeats`, although other passwords of the same pool would be valid. With a small pool this happens often. From the pool `AB` and a **Length** of 3, the draws `AAA` and `BBB` cannot be arranged, and together they are one draw in four. With a **Length** of 4 the failures are about three calls in five. With [**StartsWithLetter**](#startswithletter) the first character is not moved either, which makes the failures more frequent. A call that fails can be repeated, because the next call draws again. +> [!NOTE] 1.0.8.0 +> The shuffle only rearranges the characters that were drawn, and the property never draws them again. When the drawn characters cannot be arranged without a repeat, **Generate** returns an empty string and sets `CannotAvoidRepeats`, although other passwords of the same pool would be valid. With a small pool this happens often. From the pool `AB` and a **Length** of 3, the draws `AAA` and `BBB` cannot be arranged, and together they are one draw in four. With a **Length** of 4 the failures are about three calls in five. With [**StartsWithLetter**](#startswithletter) the first character is not moved either, which makes the failures more frequent. A call that fails can be repeated, because the next call draws again. > > ```tb check_build check_run > Dim o As PwGenOptions = PwGenOptions.Alphanumeric diff --git a/docs/Reference/Contributed/xCalendar/index.md b/docs/Reference/Contributed/xCalendar/index.md index 879855dc0..03eb46d7c 100644 --- a/docs/Reference/Contributed/xCalendar/index.md +++ b/docs/Reference/Contributed/xCalendar/index.md @@ -48,8 +48,8 @@ Set cal = xCalendar1 Debug.Print cal.Year ``` -> [!NOTE] -> `Dim cal As xCalendar` fails in BETA 1005 with TB5139, *[xCalendar] is a project or library, not a datatype*. +> [!NOTE] BETA 1005 +> `Dim cal As xCalendar` fails with TB5139, *[xCalendar] is a project or library, not a datatype*. > > ```tb check_build expect-error=TB5139 > Dim cal As xCalendar @@ -63,8 +63,8 @@ Dim m As MonthLengthEnum = MonthLengthEnum.LongLength Debug.Print d, m ' 2 1 ``` -> [!NOTE] -> In BETA 1005, `ShortLength` and `LongLength` written alone fail with TB5073, *'ShortLength' is ambiguous*. +> [!NOTE] BETA 1005 +> `ShortLength` and `LongLength` written alone fail with TB5073, *'ShortLength' is ambiguous*. > > ```tb check_build expect-error=TB5073 > Dim d As DayLengthEnum = ShortLength @@ -155,8 +155,8 @@ Setting **Value**, **Day**, **Month** or **Year**, and calling the methods that ## Known problems in version 1.0.17.0 -> [!WARNING] -> Version 1.0.17.0 of the package gives a wrong result without an error in these cases. +> [!WARNING] 1.0.17.0 +> The package gives a wrong result without an error in these cases. > > - **The colour properties of the text have no effect.** **DayFontColor**, **GridFontColor** and **TitleFontColor** are stored and returned, but the day names, the day numbers and the title are always drawn in the same near-black (`&H4`), and so are the days of the month before and the month after, which the control means to draw in grey. The disabled state has no effect on the text colour either. > - **Setting Value to a date in another month, or setting FirstDay, does not place the dates of the grid again.** After **Value** is set, the title and the two lists show the new month, but the grid still shows the dates of the month before, with no cell selected. After **FirstDay** is set, the day names move to the new order and the dates do not, so the names and the dates disagree. **Refresh** does not correct either. The dates are placed again when the selection is next moved, by a click on a cell, by **Today** or by one of the **Next** and **Previous** methods. @@ -165,8 +165,8 @@ Setting **Value**, **Day**, **Month** or **Year**, and calling the methods that > - **Year, Month and Day can disagree with Value.** See [Adding the control to a form](#adding-the-control-to-a-form). > - **A year outside 1900 to 2100 is not shown in the year list.** Setting **Year** or **Value** to 1899 or earlier leaves the year list blank, and to 2101 or later leaves it as it was. No error is raised, and **Value** and the title show the year. -> [!NOTE] -> Version 1.0.17.0 of the package has these other problems. Each raises an error, or concerns how the control looks or is reached. +> [!NOTE] 1.0.17.0 +> The package has these other problems. Each raises an error, or concerns how the control looks or is reached. > > - **A month or a year chosen from a list can open the run-time error box.** Choosing a month in which the selected day does not exist, such as February when the selected day is the 31st, or the year 2025 when the selected date is February 29, 2024, raises run-time error 13, *Type mismatch*, in the control's own code, where the program cannot handle it: the program shows its error box, and goes on when the box is closed. The list then shows the new month or year, and **Month** or **Year** returns it, but **Value** and **Day** keep the old date, the grid is not drawn again, and **NewMonth** or **NewYear** is not raised. The same dates set from code raise run-time error 13 to the caller. > - **Setting ValueIsNull raises run-time error 438**, *Object doesn't support this property or method*, to **True** or to **False**. The value is stored all the same, and nothing else in the control uses it. @@ -345,8 +345,8 @@ Setting **Year** selects the same day and month in that year, shows the year, an A method is called as *object*.**Name**. None takes an argument or returns a value. The methods that move the selection show the month of the new date, update the two lists, and raise no event. They do not check that the new date is in the range of the year list, 1900 to 2100: past either end, **Value** and the title follow, and the year list is blank before 1900 and unchanged after 2100. -> [!WARNING] -> In BETA 1005, [**DateAdd**](../../../Modules/DateTime/DateAdd) with the interval `"m"`, `"q"` or `"yyyy"` does not stop at the end of a shorter month: the date overflows into the next month. **NextMonth**, **PreviousMonth**, **NextYear** and **PreviousYear** call **DateAdd**, so in BETA 1005 **NextMonth** from January 31, 2026 selects March 3, 2026, **PreviousMonth** from March 31, 2026 selects March 3, 2026, and **NextYear** from February 29, 2024 selects March 1, 2025. VB6 gives the last day of the shorter month in every such case: February 28, 2026 for the first one. +> [!WARNING] BETA 1005 +> [**DateAdd**](../../../Modules/DateTime/DateAdd) with the interval `"m"`, `"q"` or `"yyyy"` does not stop at the end of a shorter month: the date overflows into the next month. **NextMonth**, **PreviousMonth**, **NextYear** and **PreviousYear** call **DateAdd**, so **NextMonth** from January 31, 2026 selects March 3, 2026, **PreviousMonth** from March 31, 2026 selects March 3, 2026, and **NextYear** from February 29, 2024 selects March 1, 2025. VB6 gives the last day of the shorter month in every such case: February 28, 2026 for the first one. ### NextDay {: .no_toc } diff --git a/docs/Reference/Core/Const.md b/docs/Reference/Core/Const.md index 41a7406a7..88b213dce 100644 --- a/docs/Reference/Core/Const.md +++ b/docs/Reference/Core/Const.md @@ -78,11 +78,11 @@ Debug.Print UBound(Primes) ' 4 Debug.Print Total ' 28 ``` -> [!WARNING] -> In twinBASIC BETA 1005, a module-level **Single** array constant whose element is too large for a **Single**, such as `Const Big() As Single = Array(1E+39)`, compiles without an error, and the element holds infinity. An array constant declared after it whose element calls **Sqr**, **Timer** or **Rnd** is then compile error **TB5001**, although it is correct. +> [!WARNING] BETA 1005 +> A module-level **Single** array constant whose element is too large for a **Single**, such as `Const Big() As Single = Array(1E+39)`, compiles without an error, and the element holds infinity. An array constant declared after it whose element calls **Sqr**, **Timer** or **Rnd** is then compile error **TB5001**, although it is correct. -> [!NOTE] -> In twinBASIC BETA 1005, in a module, an array constant declared after a procedure is compile error **TB5079**, *Unrecognized symbol*, in the procedures that follow it. The procedures before it see an empty array: reading an element raises error 9. Declare array constants above the module's first procedure. +> [!NOTE] BETA 1005 +> In a module, an array constant declared after a procedure is compile error **TB5079**, *Unrecognized symbol*, in the procedures that follow it. The procedures before it see an empty array: reading an element raises error 9. Declare array constants above the module's first procedure. > [!NOTE] > Array constants are a twinBASIC extension. In VBA and VB6, `Const Primes() As Long = Array(2, 3)` is a syntax error. diff --git a/docs/Reference/Core/Divide.md b/docs/Reference/Core/Divide.md index 88aa85ad6..def559d04 100644 --- a/docs/Reference/Core/Divide.md +++ b/docs/Reference/Core/Divide.md @@ -36,8 +36,8 @@ A **Single** divided by a **Long**, **LongLong**, **Double**, **Currency** or ** Dividing by zero raises error 11, *Division by zero*, and zero divided by zero raises error 6, *Overflow*. Use [**\\**](IntegerDivide) for truncating-integer division and [**Mod**](Mod) for remainder. -> [!WARNING] -> In twinBASIC BETA 1005, a division whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. Compiled with LLVM, the division raises error 6 as it should. Declare the variable **Double** where an overflow must be caught. +> [!WARNING] BETA 1005 +> A division whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. Compiled with LLVM, the division raises error 6 as it should. Declare the variable **Double** where an overflow must be caught. > [!NOTE] > **Decimal** can be a declared type in twinBASIC, so dividing by or into a declared **Decimal** gives a declared **Decimal**. In VBA, **Decimal** exists only inside a **Variant**. diff --git a/docs/Reference/Core/Error.md b/docs/Reference/Core/Error.md index e8bd8cad6..2e1fac66b 100644 --- a/docs/Reference/Core/Error.md +++ b/docs/Reference/Core/Error.md @@ -30,8 +30,8 @@ If *errornumber* is defined, the **Error** statement calls the error handler aft If no error handler exists or if none is enabled, an error message is created and displayed from the **Err** object properties. -> [!NOTE] -> In BETA 1005, an error that a class member raises with the **Error** statement reaches a caller that called the member late-bound --- through an **Object** variable or [**CallByName**](../Modules/Interaction/CallByName) --- as error -2147352567 (`&H80020009`), *Exception occurred.*, not as *errornumber*. VB6 raises *errornumber*. An early-bound caller receives *errornumber* unchanged. +> [!NOTE] BETA 1005 +> An error that a class member raises with the **Error** statement reaches a caller that called the member late-bound --- through an **Object** variable or [**CallByName**](../Modules/Interaction/CallByName) --- as error -2147352567 (`&H80020009`), *Exception occurred.*, not as *errornumber*. VB6 raises *errornumber*. An early-bound caller receives *errornumber* unchanged. ### Example diff --git a/docs/Reference/Core/For-Each-Next.md b/docs/Reference/Core/For-Each-Next.md index ee567c077..7406b731f 100644 --- a/docs/Reference/Core/For-Each-Next.md +++ b/docs/Reference/Core/For-Each-Next.md @@ -50,9 +50,8 @@ The **For...Each...Next** statement cannot be used with an array of user-defined An object of a class can be the *group* when the class has a member marked [**[Enumerator]**](Attributes#enumerator) that supplies an enumerator. See [Custom Enumerators](../../Features/Language/Custom-Enumerators) for how to write one. -> [!WARNING] -> -> In BETA 1005, a **For Each** that cannot get an enumerator from *group* never ends when it runs under `On Error Resume Next`, or under an error handler that ends with **Resume Next**. That happens when *group* is **Nothing**, an object of a class with no **[Enumerator]** member, or an object whose **[Enumerator]** member returns **Nothing** or raises an error. The error resumes into the loop body, and **Next** raises error 91 and goes back to the body, again and again. Test *group* before the loop. VB6 runs the body once, then raises error 92 at **Next**, and the loop ends. +> [!WARNING] BETA 1005 +> A **For Each** that cannot get an enumerator from *group* never ends when it runs under `On Error Resume Next`, or under an error handler that ends with **Resume Next**. That happens when *group* is **Nothing**, an object of a class with no **[Enumerator]** member, or an object whose **[Enumerator]** member returns **Nothing** or raises an error. The error resumes into the loop body, and **Next** raises error 91 and goes back to the body, again and again. Test *group* before the loop. VB6 runs the body once, then raises error 92 at **Next**, and the loop ends. ### Example diff --git a/docs/Reference/Core/Get.md b/docs/Reference/Core/Get.md index a6b126ea0..96e19b3b6 100644 --- a/docs/Reference/Core/Get.md +++ b/docs/Reference/Core/Get.md @@ -24,8 +24,8 @@ Syntax: *varname* : Valid variable name into which data is read. -> [!NOTE] -> In BETA 1005, **Get** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). +> [!NOTE] BETA 1005 +> **Get** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). Data read with **Get** is usually written to a file with [**Put**](Put). The first record or byte in a file is at position 1, the second record or byte is at position 2, and so on. When *recnumber* is omitted, the next record or byte following the last **Get** or **Put** statement (or pointed to by the last [**Seek**](../Modules/FileSystem/Seek) function) is read. The delimiting commas must be included: diff --git a/docs/Reference/Core/Graphics-Methods.md b/docs/Reference/Core/Graphics-Methods.md index 37aabbbc5..c58254f55 100644 --- a/docs/Reference/Core/Graphics-Methods.md +++ b/docs/Reference/Core/Graphics-Methods.md @@ -147,8 +147,8 @@ The built-in surfaces read *Color* as a **Long**. When *Color* is left out they The line of a **Line** and the fill of **BF** use the same rule, as does the circle of **Circle** and the point of **PSet**. -> [!WARNING] -> BETA 1005 has a defect in system colours. VB6 takes the system colour index from the low 16 bits of the value, so that `&H80010003` draws system colour 3 and `&H80FF000F` draws system colour 15. BETA 1005 draws both black, without raising an error, because it reads the value as a system colour only when it lies between `&H80000000` and `&H8000001E`. Pass a system colour as one of the `vb` constants, such as `vbButtonFace`, or as `&H800000nn` with nothing in the second and third bytes. +> [!WARNING] BETA 1005 +> System colours are read wrongly. VB6 takes the system colour index from the low 16 bits of the value, so that `&H80010003` draws system colour 3 and `&H80FF000F` draws system colour 15. twinBASIC draws both black, without raising an error, because it reads the value as a system colour only when it lies between `&H80000000` and `&H8000001E`. Pass a system colour as one of the `vb` constants, such as `vbButtonFace`, or as `&H800000nn` with nothing in the second and third bytes. > [!NOTE] > VB6 draws `&H80000019` to `&H8000001E` black, where twinBASIC draws the system colour of that index. The rest of the rule, including the truncation of every other value to 24 bits, is the same. diff --git a/docs/Reference/Core/LSet.md b/docs/Reference/Core/LSet.md index c049e3160..6c1418941 100644 --- a/docs/Reference/Core/LSet.md +++ b/docs/Reference/Core/LSet.md @@ -31,8 +31,8 @@ If *string* is longer than *stringvar*, **LSet** places only the leftmost charac **LSet** never changes the length of *stringvar*, so a *stringvar* of zero length stays empty. *stringvar* can be a variable-length string, a fixed-length string, or a **Variant** that holds a string. A *string* that is **Null** raises error 94. -> [!WARNING] -> BETA 1005 has a defect: **LSet** on an element of a **Variant** that holds an array, such as `LSet v(0) = "zz"` with `v = Array("0123456789", "abcde")` or a **Variant** that was given its bounds with **ReDim**, changes nothing and raises no error. VB6 changes the element. An element of an array declared `Dim w(0 To 1) As Variant` works. To change an element of a **Variant** array, copy it to a **String** variable, use **LSet** on the variable and assign the variable back. +> [!WARNING] BETA 1005 +> **LSet** on an element of a **Variant** that holds an array, such as `LSet v(0) = "zz"` with `v = Array("0123456789", "abcde")` or a **Variant** that was given its bounds with **ReDim**, changes nothing and raises no error. VB6 changes the element. An element of an array declared `Dim w(0 To 1) As Variant` works. To change an element of a **Variant** array, copy it to a **String** variable, use **LSet** on the variable and assign the variable back. When copying between user-defined types, **LSet** copies as many bytes as the smaller of the two types holds. If *varname1* is larger than *varname2*, its bytes beyond the end of the source keep their old values. Both types must be free of variable-length **String**, **Variant**, **Object** and dynamic-array fields; the compiler refuses the statement otherwise (error TB5249). Fixed-length **String** fields are allowed. diff --git a/docs/Reference/Core/LeftShift.md b/docs/Reference/Core/LeftShift.md index e79f6dd48..2f2daa236 100644 --- a/docs/Reference/Core/LeftShift.md +++ b/docs/Reference/Core/LeftShift.md @@ -41,8 +41,8 @@ An integer literal with no type suffix is an **Integer**, so `1 << 20` shifts a > > Convert such a value with [**CLng**](../Modules/Conversion/CLng) or [**CLngLng**](../Modules/Conversion/CLngLng) before shifting it. -> [!WARNING] -> In twinBASIC BETA 1005, code compiled with LLVM cuts the shift count to the size of the shifted **Byte** or **Integer** before shifting: for a **Byte**, a count of 256 becomes 0 and 257 becomes 1. So a **Byte** shifted by 256 is unchanged, where the result should be 0, as it is without LLVM. An **Integer** shifted by 65,536 or more behaves the same way. A **Long** or **LongLong** is not affected. +> [!WARNING] BETA 1005 +> Code compiled with LLVM cuts the shift count to the size of the shifted **Byte** or **Integer** before shifting: for a **Byte**, a count of 256 becomes 0 and 257 becomes 1. So a **Byte** shifted by 256 is unchanged, where the result should be 0, as it is without LLVM. An **Integer** shifted by 65,536 or more behaves the same way. A **Long** or **LongLong** is not affected. ### Compound assignment diff --git a/docs/Reference/Core/Line-Input.md b/docs/Reference/Core/Line-Input.md index dd5ce3a2c..61acf2a96 100644 --- a/docs/Reference/Core/Line-Input.md +++ b/docs/Reference/Core/Line-Input.md @@ -24,8 +24,8 @@ The **Line Input #** statement reads from a file one character at a time until i Reading past the last line of the file raises run-time error 62 (*Input past end of file*), and leaves *varname* unchanged. Test for the end of the file with [**EOF**](../Modules/FileSystem/EOF) before each read. -> [!NOTE] -> In BETA 1005, **Line Input #** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). +> [!NOTE] BETA 1005 +> **Line Input #** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). ### Example diff --git a/docs/Reference/Core/Minus.md b/docs/Reference/Core/Minus.md index 6de21d9f2..34fb7ee2a 100644 --- a/docs/Reference/Core/Minus.md +++ b/docs/Reference/Core/Minus.md @@ -39,8 +39,8 @@ The data type of *result* is usually the same as that of the most precise expres A declared (non-**Variant**) result that overflows raises error 6, *Overflow*. If one or both expressions are **Null** expressions, *result* is **Null**. If an expression is **Empty**, it is treated as 0. -> [!WARNING] -> In twinBASIC BETA 1005, a subtraction whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. The compound form **-=** does the same. Declare the variable **Double** where an overflow must be caught. +> [!WARNING] BETA 1005 +> A subtraction whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. The compound form **-=** does the same. Declare the variable **Double** where an overflow must be caught. > [!NOTE] > The order of precision used by addition and subtraction is not the same as the order of precision used by multiplication. diff --git a/docs/Reference/Core/Multiply.md b/docs/Reference/Core/Multiply.md index 994bdf900..e7b5d199c 100644 --- a/docs/Reference/Core/Multiply.md +++ b/docs/Reference/Core/Multiply.md @@ -35,8 +35,8 @@ The data type of *result* is usually the same as that of the most precise expres A declared (non-**Variant**) result that overflows raises error 6, *Overflow*. If one or both expressions are **Null** expressions, *result* is **Null**. If an expression is **Empty**, it is treated as 0. -> [!WARNING] -> In twinBASIC BETA 1005, a multiplication whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. The compound form ***=** does the same. Declare the variable **Double** where an overflow must be caught. +> [!WARNING] BETA 1005 +> A multiplication whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. The compound form ***=** does the same. Declare the variable **Double** where an overflow must be caught. > [!NOTE] > The order of precision used by multiplication is not the same as the order of precision used by addition and subtraction. diff --git a/docs/Reference/Core/Name.md b/docs/Reference/Core/Name.md index 74e66b5d2..ab3929df8 100644 --- a/docs/Reference/Core/Name.md +++ b/docs/Reference/Core/Name.md @@ -22,8 +22,8 @@ The **Name** statement renames a file and moves it to a different directory or f Using **Name** on an open file produces an error. An open file must be closed before renaming it. **Name** arguments cannot include multiple-character (`*`) and single-character (`?`) wildcards. -> [!NOTE] -> In BETA 1005, each of these failures of **Name** raises run-time error -2147467259 (`&H80004005`), where VB6 raises the numbered error shown: +> [!NOTE] BETA 1005 +> Each of these failures of **Name** raises run-time error -2147467259 (`&H80004005`), where VB6 raises the numbered error shown: > > - 53 (*File not found*) when *oldpathname* does not exist; > - 58 (*File already exists*) when *newpathname* already exists; diff --git a/docs/Reference/Core/On-Error.md b/docs/Reference/Core/On-Error.md index 002c63c6c..fb0b4b358 100644 --- a/docs/Reference/Core/On-Error.md +++ b/docs/Reference/Core/On-Error.md @@ -25,8 +25,8 @@ Syntax: Without an **On Error** statement, any run-time error that occurs is fatal; that is, an error message is displayed and execution stops. -> [!NOTE] -> In a built program, BETA 1005 does not end the program after an unhandled error in an event procedure of a form, such as **Form_Load** or **Form_Activate**. It displays the error message, and once the message is closed the program goes on running: the form is shown and its later events run. VB6 ends the program. Until this is fixed, handle errors in a form's event procedures, and unload the form or end the program there when it cannot continue. +> [!NOTE] BETA 1005 +> In a built program, twinBASIC does not end the program after an unhandled error in an event procedure of a form, such as **Form_Load** or **Form_Activate**. It displays the error message, and once the message is closed the program goes on running: the form is shown and its later events run. VB6 ends the program. Until this is fixed, handle errors in a form's event procedures, and unload the form or end the program there when it cannot continue. An "enabled" error handler is one that is turned on by an **On Error** statement; an "active" error handler is an enabled handler that is in the process of handling an error. If an error occurs while an error handler is active (between the occurrence of the error and a [**Resume**](Resume), [**Exit Sub**](Exit), **Exit Function**, or **Exit Property** statement), the current procedure's error handler can't handle the error. Control returns to the calling procedure. diff --git a/docs/Reference/Core/Open.md b/docs/Reference/Core/Open.md index 05c999624..ced2bcb56 100644 --- a/docs/Reference/Core/Open.md +++ b/docs/Reference/Core/Open.md @@ -41,8 +41,8 @@ If the file specified by *pathname* doesn't exist, it is created when a file is If the file is already opened by another process, and the specified type of access is not allowed, the **Open** operation fails and an error occurs. -> [!NOTE] -> In BETA 1005, each of these failures of **Open** raises run-time error -2147467259 (`&H80004005`), where VB6 raises the numbered error shown: +> [!NOTE] BETA 1005 +> Each of these failures of **Open** raises run-time error -2147467259 (`&H80004005`), where VB6 raises the numbered error shown: > > - 53 (*File not found*) for a file that does not exist, opened for **Input**; > - 76 (*Path not found*) for a folder that does not exist; diff --git a/docs/Reference/Core/ParamArray.md b/docs/Reference/Core/ParamArray.md index fcb1d0358..4e70a23eb 100644 --- a/docs/Reference/Core/ParamArray.md +++ b/docs/Reference/Core/ParamArray.md @@ -24,8 +24,8 @@ Syntax: When the procedure is called, each argument supplied in the call becomes a corresponding element of the **Variant** array. If no arguments are supplied for the **ParamArray** position, the array is empty. -> [!NOTE] -> In BETA 1005, when an argument is a **Variant** variable that holds an array, its element cannot be used as an array: `Args(0)(1)`, `Args(0)(1) = x`, and `b = Args(0)` where *b* is a dynamic array of any type raise run-time error -2147467259, *Unspecified error*, and so does passing `Args(0)` on to another **ParamArray** that indexes it; **Join** raises error 13. **LBound**, **UBound**, **IsArray**, **VarType** and **For Each** work on the element, and an argument that is an array variable or an expression is not affected. VB6 raises no error. Assign the element to a **Variant** first (`v = Args(0)`), or have the caller put the argument in parentheses, which passes a copy. +> [!NOTE] BETA 1005 +> When an argument is a **Variant** variable that holds an array, its element cannot be used as an array: `Args(0)(1)`, `Args(0)(1) = x`, and `b = Args(0)` where *b* is a dynamic array of any type raise run-time error -2147467259, *Unspecified error*, and so does passing `Args(0)` on to another **ParamArray** that indexes it; **Join** raises error 13. **LBound**, **UBound**, **IsArray**, **VarType** and **For Each** work on the element, and an argument that is an array variable or an expression is not affected. VB6 raises no error. Assign the element to a **Variant** first (`v = Args(0)`), or have the caller put the argument in parentheses, which passes a copy. > [!NOTE] > A procedure that defines a **ParamArray** parameter cannot be called using named-argument syntax. All arguments to such a procedure must be positional. To omit individual elements within the **ParamArray** position at a call site, leave the position blank between commas. diff --git a/docs/Reference/Core/Plus.md b/docs/Reference/Core/Plus.md index 0236c0093..3e02203a4 100644 --- a/docs/Reference/Core/Plus.md +++ b/docs/Reference/Core/Plus.md @@ -58,8 +58,8 @@ For simple arithmetic addition involving only numeric expressions, the data type A declared (non-**Variant**) result that overflows raises error 6, *Overflow*. If one or both expressions are **Null** expressions, *result* is **Null**. If both expressions are **Empty**, *result* is an **Integer**. However, if only one expression is **Empty**, the other expression is returned unchanged as *result*. -> [!WARNING] -> In twinBASIC BETA 1005, an addition whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. The compound form **+=** does the same. Declare the variable **Double** where an overflow must be caught. +> [!WARNING] BETA 1005 +> An addition whose result is a declared **Single** raises no error when the result overflows: the **Single** holds infinity (`1.#INF`) instead. VB6 raises error 6, *Overflow*. The compound form **+=** does the same. Declare the variable **Double** where an overflow must be caught. > [!NOTE] > The order of precision used by addition and subtraction is not the same as the order of precision used by multiplication. diff --git a/docs/Reference/Core/Print.md b/docs/Reference/Core/Print.md index e68861a5d..62d8fe614 100644 --- a/docs/Reference/Core/Print.md +++ b/docs/Reference/Core/Print.md @@ -55,8 +55,8 @@ An expression that reaches the end of its zone pushes the next one into the zone *filenumber* : Any valid file number. -> [!NOTE] -> In BETA 1005, **Print #** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). +> [!NOTE] BETA 1005 +> **Print #** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). When *outputlist* is omitted and only a list separator follows *filenumber*, a blank line is written. diff --git a/docs/Reference/Core/Put.md b/docs/Reference/Core/Put.md index 968c14bc7..349a1bf15 100644 --- a/docs/Reference/Core/Put.md +++ b/docs/Reference/Core/Put.md @@ -21,8 +21,8 @@ Syntax: *varname* : Name of the variable containing data to be written to disk. -> [!NOTE] -> In BETA 1005, **Put** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). It raises the same error for data longer than the record length of a **Random** file, where VB6 raises 59 (*Bad record length*). +> [!NOTE] BETA 1005 +> **Put** raises run-time error -2147467259 (`&H80004005`) for a *filenumber* that is not open, where VB6 raises 52 (*Bad file name or number*). It raises the same error for data longer than the record length of a **Random** file, where VB6 raises 59 (*Bad record length*). Data written with **Put** is usually read from a file with [**Get**](Get). diff --git a/docs/Reference/Core/RSet.md b/docs/Reference/Core/RSet.md index 4c100ba98..c84a392dd 100644 --- a/docs/Reference/Core/RSet.md +++ b/docs/Reference/Core/RSet.md @@ -24,8 +24,8 @@ If *string* is longer than *stringvar*, **RSet** places only the leftmost charac **RSet** never changes the length of *stringvar*, so a *stringvar* of zero length stays empty. *stringvar* can be a variable-length string, a fixed-length string, or a **Variant** that holds a string. A *string* that is **Null** raises error 94. -> [!WARNING] -> BETA 1005 has a defect: **RSet** on an element of a **Variant** that holds an array, such as `RSet v(1) = "zz"` with `v = Array("0123456789", "abcde")` or a **Variant** that was given its bounds with **ReDim**, changes nothing and raises no error. VB6 changes the element. An element of an array declared `Dim w(0 To 1) As Variant` works. To change an element of a **Variant** array, copy it to a **String** variable, use **RSet** on the variable and assign the variable back. +> [!WARNING] BETA 1005 +> **RSet** on an element of a **Variant** that holds an array, such as `RSet v(1) = "zz"` with `v = Array("0123456789", "abcde")` or a **Variant** that was given its bounds with **ReDim**, changes nothing and raises no error. VB6 changes the element. An element of an array declared `Dim w(0 To 1) As Variant` works. To change an element of a **Variant** array, copy it to a **String** variable, use **RSet** on the variable and assign the variable back. > [!NOTE] > **RSet** can't be used with user-defined types. The compiler refuses the statement. diff --git a/docs/Reference/Core/ReDim.md b/docs/Reference/Core/ReDim.md index 540fbcc1e..3f19fe279 100644 --- a/docs/Reference/Core/ReDim.md +++ b/docs/Reference/Core/ReDim.md @@ -46,8 +46,8 @@ Similarly, with **Preserve**, the size of the array can only change by changing When an array becomes smaller than it was, data in the eliminated elements will be lost. -> [!NOTE] -> An upper bound below the lower bound, as in `ReDim X(0 To -1)`, raises an error. In BETA 1005 the error is -2147467259, *Unspecified error*; VB6 raises error 9, *Subscript out of range*. +> [!NOTE] BETA 1005 +> An upper bound below the lower bound, as in `ReDim X(0 To -1)`, raises error -2147467259, *Unspecified error*. VB6 raises error 9, *Subscript out of range*. When variables are initialized, a numeric variable is initialized to 0, a variable-length string is initialized to a zero-length string (""), and a fixed-length string is filled with zeros. **Variant** variables are initialized to **Empty**. Each element of a user-defined type variable is initialized as if it were a separate variable. diff --git a/docs/Reference/Core/RightShift.md b/docs/Reference/Core/RightShift.md index bb5cde644..36b68e3ea 100644 --- a/docs/Reference/Core/RightShift.md +++ b/docs/Reference/Core/RightShift.md @@ -39,8 +39,8 @@ The data type of *result* matches the type of *number*, whatever the type of *co > > Convert such a value with [**CLng**](../Modules/Conversion/CLng) or [**CLngLng**](../Modules/Conversion/CLngLng) before shifting it. -> [!WARNING] -> In twinBASIC BETA 1005, code compiled with LLVM cuts the shift count to the size of the shifted **Byte** or **Integer** before shifting: for a **Byte**, a count of 256 becomes 0 and 257 becomes 1. So a **Byte** shifted by 256 is unchanged, where the result should be 0, as it is without LLVM. An **Integer** shifted by 65,536 or more behaves the same way. A **Long** or **LongLong** is not affected. +> [!WARNING] BETA 1005 +> Code compiled with LLVM cuts the shift count to the size of the shifted **Byte** or **Integer** before shifting: for a **Byte**, a count of 256 becomes 0 and 257 becomes 1. So a **Byte** shifted by 256 is unchanged, where the result should be 0, as it is without LLVM. An **Integer** shifted by 65,536 or more behaves the same way. A **Long** or **LongLong** is not affected. ### Compound assignment diff --git a/docs/Reference/Default/VB/Data/index.md b/docs/Reference/Default/VB/Data/index.md index faa26167d..3fee21034 100644 --- a/docs/Reference/Default/VB/Data/index.md +++ b/docs/Reference/Default/VB/Data/index.md @@ -80,8 +80,8 @@ Determines how the control's border is drawn by the OS. A member of [**Appearanc The fill colour of the band behind the [**Caption**](#caption), as an **OLE_COLOR**. Defaults to the system window-background colour. -> [!WARNING] -> BETA 1005 has a defect: assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. Check a colour that comes from user input before assigning it. +> [!WARNING] BETA 1005 +> Assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. Check a colour that comes from user input before assigning it. ### BOFAction {: .no_toc } @@ -165,8 +165,8 @@ The **StdFont** used to render [**Caption**](#caption). The convenience properti The text colour for the caption, as an **OLE_COLOR**. Defaults to the system window-text colour. A disabled control draws the caption in the system grey-text colour instead. -> [!WARNING] -> BETA 1005 has a defect: assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. Check a colour that comes from user input before assigning it. +> [!WARNING] BETA 1005 +> Assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. Check a colour that comes from user input before assigning it. ### Height {: .no_toc } diff --git a/docs/Reference/Default/VB/MultiFrame/index.md b/docs/Reference/Default/VB/MultiFrame/index.md index d62a65e77..3f4e9d195 100644 --- a/docs/Reference/Default/VB/MultiFrame/index.md +++ b/docs/Reference/Default/VB/MultiFrame/index.md @@ -78,8 +78,8 @@ The set of edges of the parent that the **MultiFrame**'s corresponding edges fol The background colour of the **MultiFrame**'s drawing surface, as an **OLE_COLOR**. Defaults to the system window-background colour. Visible only where the contained frames do not fully cover the extent --- e.g. when their **MultiFrameSize**s sum to less than 100%. -> [!WARNING] -> BETA 1005 has a defect: assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. Check a colour that comes from user input before assigning it. +> [!WARNING] BETA 1005 +> Assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. Check a colour that comes from user input before assigning it. ### Container {: .no_toc } diff --git a/docs/Reference/Default/VB/OLE/index.md b/docs/Reference/Default/VB/OLE/index.md index 087431a06..4af52bc00 100644 --- a/docs/Reference/Default/VB/OLE/index.md +++ b/docs/Reference/Default/VB/OLE/index.md @@ -107,8 +107,8 @@ When **True** (default), right-clicking the container automatically pops up a me The background colour, as an **OLE_COLOR**. Defaults to the system window-background colour. -> [!WARNING] -> BETA 1005 has a defect: assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. VB6 raises error 380, *Invalid property value*, and keeps the old colour. Check a colour that comes from user input before assigning it. +> [!WARNING] BETA 1005 +> Assigning a value that is not a colour, such as `-1`, raises no error and stores the value, so a later read returns it. VB6 raises error 380, *Invalid property value*, and keeps the old colour. Check a colour that comes from user input before assigning it. ### BackStyle {: .no_toc } diff --git a/docs/Reference/Default/VB/TextBox/index.md b/docs/Reference/Default/VB/TextBox/index.md index 48b8edec2..f8b678e13 100644 --- a/docs/Reference/Default/VB/TextBox/index.md +++ b/docs/Reference/Default/VB/TextBox/index.md @@ -274,8 +274,8 @@ When **True**, the user can scroll, select, and copy text but cannot modify it. The maximum number of characters the user can type into the control. **Long**, default `0` --- when zero, the OS imposes its own limit (typically 32 767 characters for single-line, much larger for multi-line). Setting **MaxLength** below the current text length does not truncate what is already there, but blocks further typing until the user deletes enough characters. -> [!WARNING] -> BETA 1005 has a defect: assigning [**Text**](#text) in code is not limited by **MaxLength**. The whole string is stored, and no error is raised. VB6 truncates the string to **MaxLength** characters. Truncate the string in code before assigning it, with **Left$**. +> [!WARNING] BETA 1005 +> Assigning [**Text**](#text) in code is not limited by **MaxLength**. The whole string is stored, and no error is raised. VB6 truncates the string to **MaxLength** characters. Truncate the string in code before assigning it, with **Left$**. ### MouseIcon {: .no_toc } diff --git a/docs/Reference/Default/VBA/DateTime/DateAdd.md b/docs/Reference/Default/VBA/DateTime/DateAdd.md index 846200eb3..1f3445903 100644 --- a/docs/Reference/Default/VBA/DateTime/DateAdd.md +++ b/docs/Reference/Default/VBA/DateTime/DateAdd.md @@ -50,8 +50,8 @@ DateAdd("m", 1, "31-Jan-95") In this case, **DateAdd** returns 28-Feb-95, not 31-Feb-95. If *date* is 31-Jan-96, it returns 29-Feb-96 because 1996 is a leap year. -> [!WARNING] -> In BETA 1005, **DateAdd** with the "m", "q" or "yyyy" interval does not stop at the last day of a shorter month: it counts the extra days into the next month and raises no error. `DateAdd("m", 1, "31-Jan-95")` returns 3-Mar-95, `DateAdd("m", -1, #3/31/2026#)` returns 3-Mar-2026, and `DateAdd("yyyy", 1, #2/29/2024#)` returns 1-Mar-2025. VB6 returns 28-Feb-95, 28-Feb-2026 and 28-Feb-2025. +> [!WARNING] BETA 1005 +> **DateAdd** with the "m", "q" or "yyyy" interval does not stop at the last day of a shorter month: it counts the extra days into the next month and raises no error. `DateAdd("m", 1, "31-Jan-95")` returns 3-Mar-95, `DateAdd("m", -1, #3/31/2026#)` returns 3-Mar-2026, and `DateAdd("yyyy", 1, #2/29/2024#)` returns 1-Mar-2025. VB6 returns 28-Feb-95, 28-Feb-2026 and 28-Feb-2025. If the calculated date would precede the year 100, an error occurs. diff --git a/docs/Reference/Default/VBA/DateTime/index.md b/docs/Reference/Default/VBA/DateTime/index.md index c44ed4de3..18c3a944b 100644 --- a/docs/Reference/Default/VBA/DateTime/index.md +++ b/docs/Reference/Default/VBA/DateTime/index.md @@ -58,8 +58,8 @@ Debug.Print DateAdd("m", 1, #1/31/2026#) ' 2/28/2026 — clamped to las Debug.Print DateDiff("d", #1/1/2026#, #5/9/2026#) ' 128 ``` -> [!WARNING] -> In BETA 1005, **DateAdd** does not clamp to the last day of the month, and the first line of the sample prints `3/3/2026`. VB6 prints `2/28/2026`. [**DateAdd**](DateAdd) describes the cases. +> [!WARNING] BETA 1005 +> **DateAdd** does not clamp to the last day of the month, and the first line of the sample prints `3/3/2026`. VB6 prints `2/28/2026`. [**DateAdd**](DateAdd) describes the cases. ## Calendar selection diff --git a/docs/Reference/Default/VBA/ErrObject/Description.md b/docs/Reference/Default/VBA/ErrObject/Description.md index d67e401c6..f64a295c4 100644 --- a/docs/Reference/Default/VBA/ErrObject/Description.md +++ b/docs/Reference/Default/VBA/ErrObject/Description.md @@ -20,8 +20,8 @@ The **Description** setting consists of a short description of the error. Use th When generating a user-defined error, assign a short description of the error to the **Description** property. If **Description** isn't filled in and the value of [**Number**](Number) corresponds to a built-in run-time error, the string returned by the [**Error**](../Conversion/Error) function is placed in **Description** when the error is generated. -> [!WARNING] -> BETA 1005 has a defect: for a number that has no built-in message, **Raise** without a *description* leaves **Description** as an empty string, a Windows system message or `Automation error`, depending on the number, and raises no error. VBA and VB6 place `Application-defined or object-defined error` there. [**Raise**](Raise) has the table. +> [!WARNING] BETA 1005 +> For a number that has no built-in message, **Raise** without a *description* leaves **Description** as an empty string, a Windows system message or `Automation error`, depending on the number, and raises no error. VBA and VB6 place `Application-defined or object-defined error` there. [**Raise**](Raise) has the table. ### Example diff --git a/docs/Reference/Default/VBA/ErrObject/Raise.md b/docs/Reference/Default/VBA/ErrObject/Raise.md index f2d168514..820e198f6 100644 --- a/docs/Reference/Default/VBA/ErrObject/Raise.md +++ b/docs/Reference/Default/VBA/ErrObject/Raise.md @@ -41,11 +41,11 @@ A *description* of an empty string gives an empty **Description** for every numb When the error is raised in a method of a class and the caller handles it, an empty **Description** reaches the caller as `Application-defined or object-defined error`. The other texts arrive unchanged. -> [!WARNING] -> BETA 1005 has a defect: **Raise** differs from VBA and VB6 in three ways, and none of them raises an error. Without *source*, VBA sets **Source** to the name of the project, and twinBASIC leaves it empty. Without *description*, VBA gives every number that has no built-in message the text `Application-defined or object-defined error`, from 1 up to 65535, and twinBASIC gives an empty string or one of the texts above. And an omitted argument keeps the value left by an earlier error in VBA, and is reset in twinBASIC. A program that relies on **Err.Source** being the project name, or on **Err.Description** being non-empty, has to pass those arguments. +> [!WARNING] BETA 1005 +> **Raise** differs from VBA and VB6 in three ways, and none of them raises an error. Without *source*, VBA sets **Source** to the name of the project, and twinBASIC leaves it empty. Without *description*, VBA gives every number that has no built-in message the text `Application-defined or object-defined error`, from 1 up to 65535, and twinBASIC gives an empty string or one of the texts above. And an omitted argument keeps the value left by an earlier error in VBA, and is reset in twinBASIC. A program that relies on **Err.Source** being the project name, or on **Err.Description** being non-empty, has to pass those arguments. -> [!NOTE] -> In BETA 1005, an error that a class member raises with a constant *number*, such as `Err.Raise 380`, reaches a caller that called the member late-bound --- through an **Object** variable or [**CallByName**](../Interaction/CallByName) --- as error -2147352567 (`&H80020009`), *Exception occurred.*, not as 380. VB6 raises 380. The number arrives unchanged when the member passes it in a variable, as in `Err.Raise Number`, and in every early-bound call. +> [!NOTE] BETA 1005 +> An error that a class member raises with a constant *number*, such as `Err.Raise 380`, reaches a caller that called the member late-bound --- through an **Object** variable or [**CallByName**](../Interaction/CallByName) --- as error -2147352567 (`&H80020009`), *Exception occurred.*, not as 380. VB6 raises 380. The number arrives unchanged when the member passes it in a variable, as in `Err.Raise Number`, and in every early-bound call. **Raise** is preferred over the [**Error**](../../Core/Error) statement when generating run-time errors, particularly inside class modules: the **Err** object holds richer information than the **Error** statement can supply. With **Raise** the source that generated the error can be specified in the [**Source**](Source) property, online Help for the error can be referenced through [**HelpFile**](HelpFile) and [**HelpContext**](HelpContext), and so on. diff --git a/docs/Reference/Default/VBA/ErrObject/Source.md b/docs/Reference/Default/VBA/ErrObject/Source.md index 98cb6061a..4f2031221 100644 --- a/docs/Reference/Default/VBA/ErrObject/Source.md +++ b/docs/Reference/Default/VBA/ErrObject/Source.md @@ -24,8 +24,8 @@ When generating an error from user code, pass the source to [**Raise**](Raise). When an error occurs that twinBASIC raises itself, such as a division by zero, **Source** is an empty string too, in a standard module and in a class module alike. -> [!WARNING] -> BETA 1005 has a defect: it does not fill in **Source**, and raises no error. VBA and VB6 fill it in automatically with the name of the project, for an error that the runtime raises and for **Raise** without a source argument. A program that reads **Err.Source** to find where an error came from gets an empty string. +> [!WARNING] BETA 1005 +> It does not fill in **Source**, and raises no error. VBA and VB6 fill it in automatically with the name of the project, for an error that the runtime raises and for **Raise** without a source argument. A program that reads **Err.Source** to find where an error came from gets an empty string. ### Example diff --git a/docs/Reference/Default/VBA/FileSystem/ChDir.md b/docs/Reference/Default/VBA/FileSystem/ChDir.md index db97eb78a..b4461dff9 100644 --- a/docs/Reference/Default/VBA/FileSystem/ChDir.md +++ b/docs/Reference/Default/VBA/FileSystem/ChDir.md @@ -24,8 +24,8 @@ ChDir "D:\TMP" ' Make "D:\TMP" the current folder. ChDrive "D" ' Make "D" the current drive. ``` -> [!NOTE] -> In BETA 1005, **ChDir** raises run-time error -2147467259 (`&H80004005`) for a *path* that does not exist, where VB6 raises 76 (*Path not found*). +> [!NOTE] BETA 1005 +> **ChDir** raises run-time error -2147467259 (`&H80004005`) for a *path* that does not exist, where VB6 raises 76 (*Path not found*). `, a CDATA section with no `]]>`, or a `` anywhere later in the text, which discards everything between, or, when there is none, up to the first `>` of the whole text, which is the end of the item itself only when the item is the first thing in the text. In every other position the search goes back to an earlier `>`, and the parser reads the item again, without end. A processing instruction `` is skipped, but the `>` that ends it stays behind as a text node. These calls return, and show how a processing instruction and a DOCTYPE declaration are read: @@ -1216,7 +1216,7 @@ x.Parse "
1" ### Malformed text and failed parses {: #defect-malformed .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > A **Parse** that returns **False** has already thrown away the previous document. The document is replaced before the text is read, so it holds what was read before the failure, which is nothing when the failure is at the start. [**Initialized**](#initialized) is not changed, so after a failed **Parse** into an object that held a document, the path members work on the part that was read. ```tb check_build check_run @@ -1236,7 +1236,7 @@ Debug.Print "[" & x.GetElementText("a") & "]" ### White space {: #defect-whitespace .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > **Parse** leaves out a text node that holds only spaces, and keeps one that has a line break or a tab in it. A space between two elements is lost: `

a b

` is read as `

ab

`, and its text is `ab`. An element that holds only spaces is read as an empty element. The white space of an indented document is kept, because it holds line breaks. ```tb check_build check_run @@ -1266,7 +1266,7 @@ Text set with [**SetElementText**](#setelementtext) is kept whatever it holds, s ### Pretty printing {: #defect-pretty .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > A document written with *prettyPrint* **True** does not read back as the document that was written. The line breaks and the indentation around the text of an element are text to the parser, which keeps white space that has a line break in it. [**GetElementText**](#getelementtext) of the element returns them, and writing the document again with *prettyPrint* puts them on lines of their own, so the text changes each time the document is written and read. ```tb check_build check_run @@ -1294,7 +1294,7 @@ Debug.Print Replace(Replace(s, " ", "."), vbCrLf, "|") ### Clone, Merge and the root element {: #defect-clone .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > The nodes that **Clone** copies and **Merge** adds have no **Parent**, so [**RemoveElement**](#removeelement) returns **False** for them and removes nothing. [**RootElement**](#rootelement) is set only when a document is read, created or cloned, so it keeps naming the old root after **RemoveElement** removes the root element and after **Parse** reads text that has no element. A **Merge** into an object that holds no document sets [**Initialized**](#initialized) and leaves **RootElement** **Nothing**, so later merges into it do nothing. ```tb check_build check_run @@ -1327,7 +1327,7 @@ Debug.Print c.Initialized & " " & (c.RootElement Is Nothing) ### A byte order mark in a string {: #defect-bom .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > [**Parse**](#parse) and the [**XmlString**](#xmlstring) property cut three characters from the start of a string that begins with the character U+FEFF, the character that a byte order mark decodes to. Only the first of the three is the mark: the next two are the start of the document. [**LoadFromFile**](#loadfromfile) removes the mark from the bytes of the file first, so a file is not affected. ```tb check_build check_run @@ -1341,7 +1341,7 @@ Debug.Print x.RootElement.Name ### Files {: #defect-files .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > **LoadFromFile** creates an empty file when the path names a file that does not exist, before it returns **False**. It returns **True** after the text of an existing file fails to parse, since the result of **Parse** is not tested. **SaveToFile** writes UTF-8 whatever [**Encoding**](#encoding) says, so a document labelled with another encoding is mislabelled in the file. ```tb check_build check_run diff --git a/docs/Reference/Contributed/TBMANLIB_Xml/cXmlNode.md b/docs/Reference/Contributed/TBMANLIB_Xml/cXmlNode.md index 7ac3df077..094303b71 100644 --- a/docs/Reference/Contributed/TBMANLIB_Xml/cXmlNode.md +++ b/docs/Reference/Contributed/TBMANLIB_Xml/cXmlNode.md @@ -65,7 +65,7 @@ Next The other members of [**XmlNodeType**](mXml#xmlnodetype), **Attribute**, **DocumentFragment**, **Notation** and **None**, are never given to a parsed node. A node made with `New` has the **NodeType** **None**. Attributes are not nodes: they are held in the **Attributes** field of their element. -> [!NOTE] +> [!NOTE] 1.0.0.1 > Names are compared without regard to letter case, in the attribute methods here and in the path members of [**cXml**](cXml#paths). XML itself distinguishes `Item` from `item`, and this package does not. ## Fields @@ -498,7 +498,7 @@ Syntax: *node*.**XPathText** ( *xpath* ) **As String** Returns the [**Text**](#text) of the first node that [**XPath**](#xpath) selects: the text directly inside it, and not the text inside its child elements. A text node and a CDATA section have no children, so when the expression selects one, the result is an empty string; its **Value** holds the text. Returns an empty string too when the expression selects nothing. The expression is evaluated by [**XPathSingle**](#xpathsingle). -> [!NOTE] +> [!NOTE] 1.0.0.1 > The [**XPathText**](cXml#xpathtext) of a **cXml** returns the text at any depth, and returns the value of a text node. For `xyz`, the **cXml** gives `xyz` for `/a`, and the root element gives `xz` for `.`. ```tb check_build check_run diff --git a/docs/Reference/Contributed/TBMANLIB_Xml/index.md b/docs/Reference/Contributed/TBMANLIB_Xml/index.md index c0b4862af..cf77923f9 100644 --- a/docs/Reference/Contributed/TBMANLIB_Xml/index.md +++ b/docs/Reference/Contributed/TBMANLIB_Xml/index.md @@ -44,7 +44,7 @@ Debug.Print y.ToXmlString(False) > [!WARNING] 1.0.0.1 > The parser reads some valid XML wrongly and raises no error, and for some text it never returns. [Defects in version 1.0.0.1](cXml#defects) lists the cases: self-closing elements, entities in text, tags with both kinds of quote or with line breaks, processing instructions, and comments, CDATA sections and DOCTYPE declarations in some positions. -> [!WARNING] +> [!WARNING] 1.0.0.1 > The XPath engine reads some valid expressions wrongly and raises no error: the step `..`, axes such as `ancestor::`, positions after `//`, and the operators `and` and `or`. [Defects in version 1.0.0.1](mXPath#defects) lists the cases. ## What the package reads and writes diff --git a/docs/Reference/Contributed/TBMANLIB_Xml/mXPath.md b/docs/Reference/Contributed/TBMANLIB_Xml/mXPath.md index 7e7299a31..b56d46ae4 100644 --- a/docs/Reference/Contributed/TBMANLIB_Xml/mXPath.md +++ b/docs/Reference/Contributed/TBMANLIB_Xml/mXPath.md @@ -32,7 +32,7 @@ Debug.Print x.XPathAttribute("//book[@id='2']", "cat") ' fiction ``` -> [!WARNING] +> [!WARNING] 1.0.0.1 > Several expressions that are valid XPath are read wrongly and raise no error: the parent step `..` selects nothing, `ancestor::a` is read as `child::a`, `//book[1]` selects one node where XPath selects one for each parent, and `and`, `or` and the comparison operators are found by plain searches of the text. [Defects in version 1.0.0.1](#defects) has each case. * TOC @@ -255,7 +255,7 @@ Returns a new **Collection** of **cXmlNode** objects, numbered from 1, in the or The function handles no error. A program that calls it directly meets the errors that the evaluation raises: error 6 (*Overflow*) for a position too large for a **Long**, such as `[99999999999]`. The **XPath** methods of [**cXml**](cXml#xpath) and [**cXmlNode**](cXmlNode#xpath) catch it, write a line that begins `XPath query failed:` to the Debug Console, and return an empty collection. -> [!NOTE] +> [!NOTE] 1.0.0.1 > The time grows with the square of the number of nodes the query visits, because each node found is compared with all the ones found before. Finding 2,000 elements with `//item` takes about a second, and each doubling of the count multiplies the time by about four. [**SelectElements**](cXml#selectelements) has no such cost. ```tb check_build check_run @@ -320,7 +320,7 @@ Each of these sections describes an expression that the engine reads differently ### The parent step {: #defect-parent .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > The step `..` selects nothing. The `parent::` axis works, so `parent::*` or `parent::book` takes its place. ```tb check_build check_run @@ -339,7 +339,7 @@ Debug.Print x.XPath("/catalog/book/title/parent::book").Count ### Axes {: #defect-axes .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > An axis other than **child**, **descendant**, **parent**, **self** and **attribute** is not refused. The engine drops the axis and reads the rest of the step as a child step, so `ancestor::a` selects a child named `a`, where XPath selects an ancestor, and `following-sibling::b` selects a child named `b`. `descendant-or-self::` is read as `child::`. ```tb check_build check_run @@ -359,7 +359,7 @@ Debug.Print x.XPathAttribute("/r/a/following-sibling::b", "id") ### Positions {: #defect-positions .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > A position counts the nodes that the step selected from one node of the step before. After `//`, those are all the descendants at any depth, so `//book[1]` is the first book in the whole document, where XPath gives the first book of each parent. A step written with `/` counts the children of each parent, as XPath does. Positions in any form other than `[n]`, `[last()]` and `[position()=n]` select nothing: `[last()-1]`, `[position()>1]`, `[position() = 2]` with spaces, and `[position()=last()]`. A position that is not a whole number is rounded to one, so `[1.5]` is the second node. ```tb check_build check_run @@ -389,7 +389,7 @@ Next ### Logical operators {: #defect-logic .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > `and` and `or` are found by a search for the first ` and ` and the first ` or ` in the text of the predicate, and the search does not skip quoted strings or parentheses. The predicate is cut at the first ` and ` if there is one, and the two halves are read separately, so `or` binds tighter than `and`: `a and b or c` is read as `a and (b or c)`, and `a or b and c` as `(a or b) and c`. XPath binds `and` tighter. A string that holds ` and ` or ` or `, such as `@t='salt and pepper'`, cuts the string in two, and the predicate selects nothing. Parentheses are not read as grouping: `not(@x and @y)` and `(@x='1') and (@y='2')` select nothing. ```tb check_build check_run @@ -426,7 +426,7 @@ XPath 1.0 selects `1 2` for the third and fourth queries, `2 3` for the fifth an ### Comparing attributes {: #defect-attributes .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > An attribute that a node does not have compares as an empty string, so `@p<10`, `@p!='x'` and `@p=''` keep the nodes that lack `p`; in XPath 1.0 a comparison with a missing attribute is false. A comparison operator is found by a search of the whole predicate, quoted strings included. A quoted string that holds `>`, `<` or `!=` therefore moves the split, and the attribute is looked up under a name such as `t='a`, which no node has, so it compares as an empty string: `@t='ab`. ```tb check_build check_run @@ -462,7 +462,7 @@ Debug.Print x.XPath("//i[@t='a [!WARNING] +> [!WARNING] 1.0.0.1 > A predicate that starts with `text()` is read as a test of the node's own text. Its operator is taken to be the first `=` in it, whatever comes before, so `text()!='Dune'` selects the nodes whose text equals `Dune`, and an operator without `=` (`text()>'Z'`) is ignored, so the predicate keeps every node that has text. A comparison with a child (`book[title='Dune']`) or with the node (`title[.='Dune']`) selects nothing. In `contains` and `starts-with`, a first argument that is the name of a child is taken as the text of the name: `contains(title, 'i')` asks whether the word `title` has an `i` in it, and is true for every node. ```tb check_build check_run @@ -491,7 +491,7 @@ Next ### A bracket that is not closed {: #defect-brackets .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > A `[` with no `]` after it is dropped together with what follows it, and the step is read without the predicate. `//b[@id='1'` selects every `b`. ```tb check_build check_run @@ -509,7 +509,7 @@ Debug.Print x.XPath("//b[").Count ### Slashes {: #defect-slashes .no_toc } -> [!WARNING] +> [!WARNING] 1.0.0.1 > An empty step, as in `///`, ends the path, and the steps after it are not read. `/catalog///book` is read as `/catalog` and selects the root element. ```tb check_build check_run diff --git a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md index 7189368ec..a671ae48a 100644 --- a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md +++ b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md @@ -33,7 +33,7 @@ Syntax: *object*.**Interval** **=** *milliseconds* Changing **Interval** at run time takes effect immediately: the underlying Win32 timer (`SetTimer` / `KillTimer`) is stopped and conditionally restarted on every assignment. The timer starts only when both **Interval** is greater than `0` and [**Enabled**](Enabled) is **True**. -> [!NOTE] +> [!NOTE] 1.0.7.0 > The Win32 `SetTimer` function raises an interval below 10 ms to 10 ms, and delivers its timer messages at the resolution of the system clock, typically 15--16 ms on standard Windows configurations. A short interval therefore does not give sub-15 ms accuracy. ### Example diff --git a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md index f92f65e50..fbd51249d 100644 --- a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md +++ b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md @@ -22,7 +22,7 @@ The **Timer** event fires on every tick of the underlying Win32 timer (`SetTimer The argument is derived from the `dwTime` parameter of the Win32 timer callback (a **DWORD** tick count, wrapping at approximately 49.7 days). **TwinTimer** handles the 32-bit wrap-around internally and promotes the result to a **LongLong**, so *ElapsedTime* does not wrap during a session regardless of how long the timer has been running. -> [!NOTE] +> [!NOTE] 1.0.7.0 > The event is delivered through the Win32 message pump of the thread that created the timer. If that thread's message pump is blocked when a tick is due, Windows delivers a single timer message when the pump resumes rather than one for each missed period. Long-running work inside the handler therefore lengthens the effective interval. ### Example diff --git a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md index 92bc7ba69..233bb0b5c 100644 --- a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md +++ b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md @@ -46,7 +46,7 @@ Syntax: *object*.**Enabled** [ = *value* ] *value* : A **Boolean**. **False** suppresses events; **True** allows them. -> [!NOTE] +> [!NOTE] 1.0.7.0 > **Enabled** defaults to **True** at construction. A freshly constructed **TwinTimer** with **Interval** = `0` (the initial default) does not fire until **Interval** is set to a positive value. ### Interval @@ -61,7 +61,7 @@ Syntax: *object*.**Interval** [ = *milliseconds* ] *milliseconds* : A non-negative **Long**. A value of `0` stops the timer. A negative value raises run-time error 380 (Invalid property value). -> [!NOTE] +> [!NOTE] 1.0.7.0 > The Win32 `SetTimer` function raises an interval below 10 ms to 10 ms, and delivers its timer messages at the resolution of the system clock, typically 15--16 ms on standard Windows configurations. A short interval therefore does not give sub-15 ms accuracy. ## Events diff --git a/docs/Reference/Contributed/VBComDlg/CommonDialog.md b/docs/Reference/Contributed/VBComDlg/CommonDialog.md index b560a7a4e..e8591ab6e 100644 --- a/docs/Reference/Contributed/VBComDlg/CommonDialog.md +++ b/docs/Reference/Contributed/VBComDlg/CommonDialog.md @@ -27,7 +27,7 @@ Setting [**CancelError**](#cancelerror) to **True** causes the class to raise ru Assigning a value outside the documented range or set of constants to a property raises run-time error 380 (*Invalid property value*). A failure that the sections below do not list raises an error whose number is the code the Windows common-dialog library reports and whose description is "Unexpected error." -> [!NOTE] +> [!NOTE] 1.0.13.0 > Events such as [**InitDialog**](#initdialog), [**FileValidate**](#filevalidate), and [**ColorValidate**](#colorvalidate) require setting [**HookEvents**](#hookevents) to **True** before calling the `Show*` method. Without the hook, these events are not raised. The Folder Browser dialog always raises its events, and the Help, Find and Replace events do not use the hook. * TOC @@ -319,7 +319,7 @@ Syntax: *object*.**HookEvents** [ = *value* ] *value* : **Boolean**. When **True**, the dialog's hook procedure is installed, enabling the [**InitDialog**](#initdialog), [**FileValidate**](#filevalidate), [**FileShareViolation**](#fileshareviolation), [**ColorValidate**](#colorvalidate), and [**FontApply**](#fontapply) events. Default **False**. The Folder Browser dialog raises [**InitDialog**](#initdialog) and [**FolderBrowserValidateFailed**](#folderbrowservalidatefailed) whatever the value. -> [!IMPORTANT] +> [!IMPORTANT] 1.0.13.0 > **HookEvents** must be set to **True** before calling the corresponding `Show*` method. Setting it after the call has no effect on the current invocation. ### InitDir @@ -562,7 +562,7 @@ Syntax: *object*.**ShowHelp** If [**HelpCommand**](#helpcommand) is 0, the method returns immediately without doing anything. On failure, error `CdlHelp` (32751) is raised. -> [!NOTE] +> [!NOTE] 1.0.13.0 > WinHelp (`.hlp`) is a deprecated help format. This method calls the `WinHelpW` API, which fails on a system that has no WinHelp viewer. ### ShowOpen diff --git a/docs/Reference/Contributed/WinReg/RegCls/ConvertValueToString.md b/docs/Reference/Contributed/WinReg/RegCls/ConvertValueToString.md index 47d55551a..faf7bba97 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/ConvertValueToString.md +++ b/docs/Reference/Contributed/WinReg/RegCls/ConvertValueToString.md @@ -28,7 +28,7 @@ Syntax: *object*.**ConvertValueToString**( *hKey*, *subkey*, *lType* ) Returns a **String**. For string types the null terminator is stripped. For DWORD types the value is returned as an eight-digit hexadecimal string prefixed with `0x`. For binary data the return value is formatted as `Binary (N bytes): XX XX ...` where each byte is a two-digit uppercase hex pair separated by spaces. For any other type code the return value is `Unsupported type: N`. -> [!WARNING] +> [!WARNING] 1.0.1.1 > This overload does not find the value. It passes `StrPtr(subkey)` to a parameter that `RegQueryValueEx` declares `ByVal` **String**, so the name that reaches the function is not *subkey*, the query fails with error 2 (file not found), and the overload returns an empty string for `REG_SZ`, `REG_EXPAND_SZ`, the DWORD types and `REG_BINARY`. Only the `Unsupported type: N` result for any other type code is as described. Overload 2 and [**EnumValues**](EnumValues) are not affected. ## Overload 2 -- convert from byte array @@ -51,7 +51,7 @@ The two overloads cover different calling patterns. Overload 1 is meant for the Overload 1 neither closes nor otherwise modifies *hKey*. When using Overload 1, open the key with at least `KEY_QUERY_VALUE` access and close it after all reads are complete. -> [!NOTE] +> [!NOTE] 1.0.1.1 > Both overloads include `On Error Resume Next`, so a run-time error inside them is not raised to the caller. Overload 1 returns an empty string when the query fails. The source comments mark Overload 1 as not yet tested. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/CreateKey.md b/docs/Reference/Contributed/WinReg/RegCls/CreateKey.md index 2163082f0..74ef553aa 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/CreateKey.md +++ b/docs/Reference/Contributed/WinReg/RegCls/CreateKey.md @@ -21,7 +21,7 @@ Returns **True** if the key was created or already existed, **False** if the ope **CreateKey** calls the Win32 `RegCreateKeyEx` function with `KEY_CREATE_SUB_KEY` access and does not write any values. If the key already exists, the function succeeds and the existing key and its values are left unchanged. -> [!NOTE] +> [!NOTE] 1.0.1.1 > Writing values to the new key requires a subsequent call to [**WriteString**](WriteString) or [**WriteDWORD**](WriteDWORD), which open the key with write access internally. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md b/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md index 0270e9c78..361fba85a 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md +++ b/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md @@ -19,7 +19,7 @@ Syntax: *object*.**DeleteKey** ( *RootKey*, *KeyPath* ) Returns **True** if the key was deleted successfully, or **False** if the call failed. -> [!IMPORTANT] +> [!IMPORTANT] 1.0.1.1 > The underlying Win32 `RegDeleteKey` call succeeds only when the target key has no subkeys. If the key contains subkeys, the call returns **False**. Use [DeleteKeyEx](DeleteKeyEx) to remove a key together with all its subkeys and values. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md b/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md index 82bb470e7..6f6b3e0ed 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md +++ b/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md @@ -27,7 +27,7 @@ This function is necessary because the Win32 `RegDeleteKey` API fails if the tar Both enumeration loops always request index 0 and rely on each deletion to expose the next entry. A subkey or value that cannot be deleted (for example, for lack of permission) therefore leaves the loop repeating on the same entry. -> [!IMPORTANT] +> [!IMPORTANT] 1.0.1.1 > Deleting a registry key is irreversible. Verify that *RootKey* and *KeyPath* are correct before calling this function, as all subkeys and values under the specified path will be permanently removed. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md b/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md index a99d11533..da3bf608d 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md +++ b/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md @@ -28,7 +28,7 @@ The function returns **True** when both the open and the delete operations retur Deleting a value entry is permanent. The value cannot be recovered through this API once removed. -> [!IMPORTANT] +> [!IMPORTANT] 1.0.1.1 > The calling process must have write access to the target key. Standard user processes typically have write access under **HKEY_CURRENT_USER** but may require elevation for keys under **HKEY_LOCAL_MACHINE**. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/EnumValues.md b/docs/Reference/Contributed/WinReg/RegCls/EnumValues.md index 25785230b..54fc89288 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/EnumValues.md +++ b/docs/Reference/Contributed/WinReg/RegCls/EnumValues.md @@ -32,7 +32,7 @@ Each value's data is converted to a **String** as follows: - **REG_BINARY** --- the bytes written in order as two-digit hexadecimal pairs with no separators. - Any other type, such as **REG_MULTI_SZ** --- the string `"Unsupported type: "` followed by the numeric type code. -> [!NOTE] +> [!NOTE] 1.0.1.1 > The enumeration stops at the first value that cannot be enumerated or read (for example, a value name longer than 259 characters, which does not fit the 260-character name buffer), and the function returns the entries collected up to that point. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/GetRegistryValueType.md b/docs/Reference/Contributed/WinReg/RegCls/GetRegistryValueType.md index 07493f9b6..dcdf52e7b 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/GetRegistryValueType.md +++ b/docs/Reference/Contributed/WinReg/RegCls/GetRegistryValueType.md @@ -24,10 +24,10 @@ Syntax: *object*.**GetRegistryValueType**( *hKey*, *subKey*, *valueName* ) **As If the key cannot be opened or the value query fails, the function returns `REGTYPES.REG_NONE`. -> [!WARNING] +> [!WARNING] 1.0.1.1 > The function returns `REG_NONE` for every value that holds data. It asks `RegQueryValueEx` for the type with a data pointer and a size of zero, and the call fails with error 234 (*more data*) when the value has any data. Only a value of zero length, such as an empty `REG_BINARY`, gets its type. A program that needs the type of a value calls `RegQueryValueEx` itself. -> [!NOTE] +> [!NOTE] 1.0.1.1 > *hKey* is declared as **Long**, not as **RegKeyspace**. The two types are assignment-compatible in twinBASIC, so passing a `RegKeyspace` constant works without an explicit cast. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/ReadString.md b/docs/Reference/Contributed/WinReg/RegCls/ReadString.md index cf00da4eb..23d93304b 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/ReadString.md +++ b/docs/Reference/Contributed/WinReg/RegCls/ReadString.md @@ -25,7 +25,7 @@ Syntax: *object*.**ReadString** ( *RootKey*, *KeyPath*, *ValueName* [, *DefaultV Returns a **String** containing the data stored in the registry value, or *DefaultValue* if the value cannot be read. -> [!WARNING] +> [!WARNING] 1.0.1.1 > The string that **ReadString** returns is longer than the stored text. For a text of *n* characters the result has 2*n* + 1: the text, then *n* + 1 NUL characters (`Chr(0)`). The method cuts its buffer to the size that `RegQueryValueEx` reports, less one, and that size counts bytes while a character of the buffer is two bytes. `ReadString(...) = "Alice"` is therefore **False** for a stored `Alice`, and a stored empty string comes back as one NUL character. A *DefaultValue* is returned as it is, with no NUL characters. To get the text, cut the result at the first NUL, as the second example does. ### Remarks diff --git a/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md b/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md index c67884444..49f0dc192 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md +++ b/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md @@ -33,7 +33,7 @@ If `RegCreateKeyEx` fails (for example, due to insufficient permissions), the fu The value is stored as a little-endian 32-bit integer, which is the standard **REG_DWORD** format on Windows. To read the value back, use [**ReadDWORD**](ReadDWORD). -> [!IMPORTANT] +> [!IMPORTANT] 1.0.1.1 > Writing to keys under `HKEY_LOCAL_MACHINE` typically requires the process to be running with elevated (administrator) privileges. Attempting to write without the necessary rights causes `RegCreateKeyEx` to fail and **WriteDWORD** to return **False**. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/WriteString.md b/docs/Reference/Contributed/WinReg/RegCls/WriteString.md index b517d605d..3b02f5cea 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/WriteString.md +++ b/docs/Reference/Contributed/WinReg/RegCls/WriteString.md @@ -29,7 +29,7 @@ Syntax: *object*.**WriteString**( *RootKey*, *KeyPath*, *ValueName*, *ValueData* The return value is a **BOOL**: **True** if the value was written successfully, **False** if any step failed. If the key was created but the value could not be written, the new key remains. -> [!NOTE] +> [!NOTE] 1.0.1.1 > The data size passed to `RegSetValueEx` is `LenB(ValueData) + 2`, which accounts for the string's byte count plus the two-byte null terminator. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/index.md b/docs/Reference/Contributed/WinReg/RegCls/index.md index 13d26ef7f..f521c5d2e 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/index.md +++ b/docs/Reference/Contributed/WinReg/RegCls/index.md @@ -52,10 +52,10 @@ Syntax: *object*.**ConvertValueToString** ( *hKey*, *subkey*, *lType* ) **As Str Returns an empty string when the call fails. For `REG_SZ` / `REG_EXPAND_SZ` values, returns the raw string. For `REG_DWORD` / `REG_DWORD_LITTLE_ENDIAN` / `REG_DWORD_BIG_ENDIAN` values, returns an 8-character zero-padded hexadecimal string in the form `0x00000000`. For `REG_BINARY` values, returns a space-separated hex dump prefixed with `"Binary (N bytes): "`. Unsupported types return `"Unsupported type: "` followed by the numeric type code. -> [!NOTE] +> [!NOTE] 1.0.1.1 > This overload queries the value through the provided key handle. The caller must keep the handle open for the duration of the call and close it with `RegCloseKey` afterwards. The source comments mark this overload as not yet tested. -> [!WARNING] +> [!WARNING] 1.0.1.1 > This overload does not find the value. It passes `StrPtr(subkey)` to a parameter that `RegQueryValueEx` declares `ByVal` **String**, so the name that reaches the function is not *subkey*, the query fails with error 2 (file not found), and the overload returns an empty string for `REG_SZ`, `REG_EXPAND_SZ`, the DWORD types and `REG_BINARY`. Any other type code returns `"Unsupported type: "` followed by the code. Overload 2 and [**EnumValues**](#enumvalues) are not affected. ### ConvertValueToString (overload 2) @@ -88,7 +88,7 @@ Syntax: *object*.**CreateKey** ( *RootKey*, *KeyPath* ) **As BOOL** Returns **True** on success, **False** if the key could not be created (typically due to insufficient privileges). -> [!IMPORTANT] +> [!IMPORTANT] 1.0.1.1 > Creating keys under `HKEY_LOCAL_MACHINE` typically requires the application to run with administrator rights. ### DeleteKey @@ -121,7 +121,7 @@ Syntax: *object*.**DeleteKeyEx** ( *RootKey*, *KeyPath* ) **As BOOL** Returns **True** on success, **False** if the key could not be deleted. The method first enumerates and recursively deletes all sub-keys (calling itself for each one), then deletes all values in the key, closes the handle, and finally removes the key itself with `RegDeleteKey`. Both enumeration loops always request index 0, so each deletion must succeed for the loop to advance; a sub-key or value that cannot be deleted (for example, for lack of permission) leaves the loop repeating on the same entry. -> [!WARNING] +> [!WARNING] 1.0.1.1 > This operation cannot be undone. The entire key tree is permanently removed from the registry. Double-check the *KeyPath* argument before calling. ### DeleteValue @@ -177,7 +177,7 @@ Syntax: *object*.**GetRegistryValueType** ( *hKey*, *subKey*, *valueName* ) **As Returns a member of the `REGTYPES` enumeration (from **WinDevLib**), such as `REG_SZ`, `REG_DWORD`, or `REG_BINARY`. Returns `REG_NONE` if the sub-key cannot be opened or the value does not exist. -> [!WARNING] +> [!WARNING] 1.0.1.1 > The method returns `REG_NONE` for every value that holds data. It asks `RegQueryValueEx` for the type with a data pointer and a size of zero, and the call fails with error 234 (*more data*) when the value has any data. Only a value of zero length, such as an empty `REG_BINARY`, gets its type. ### KeyExists @@ -233,7 +233,7 @@ Syntax: *object*.**ReadString** ( *RootKey*, *KeyPath*, *ValueName* [, *DefaultV *DefaultValue* : *optional* A **String** returned when the key or value does not exist, or when the stored type is not `REG_SZ`. Default: `""` (empty string). -> [!WARNING] +> [!WARNING] 1.0.1.1 > The string that **ReadString** returns is longer than the stored text. For a text of *n* characters the result has 2*n* + 1: the text, then *n* + 1 NUL characters (`Chr(0)`). The method cuts its buffer to the size that `RegQueryValueEx` reports, less one, and that size counts bytes while a character of the buffer is two bytes. `ReadString(...) = "Alice"` is therefore **False** for a stored `Alice`. A *DefaultValue* is returned as it is, with no NUL characters. To get the text, cut the result at the first NUL: `Split(ReadString(...), vbNullChar)(0)`. ### ValueExists @@ -296,7 +296,7 @@ Syntax: *object*.**WriteString** ( *RootKey*, *KeyPath*, *ValueName*, *ValueData Returns **True** on success, **False** if the key could not be created or the value could not be written. -> [!NOTE] +> [!NOTE] 1.0.1.1 > The byte count passed to `RegSetValueEx` is `LenB(ValueData) + 2`: the byte length of the string plus the two-byte null terminator. ## Remarks diff --git a/docs/Reference/Contributed/WinReg/index.md b/docs/Reference/Contributed/WinReg/index.md index 0d3abe678..98d89d2cc 100644 --- a/docs/Reference/Contributed/WinReg/index.md +++ b/docs/Reference/Contributed/WinReg/index.md @@ -18,7 +18,7 @@ The root keys, such as `HKEY_CURRENT_USER`, are members of **RegKeyspace**, so a > [!NOTE] 1.0.1.1, BETA 1005 > The package does not compile, so a project that references it does not build. **RegCls** declares `RegCreateKeyEx` and `RegEnumKeyEx` with a `ByRef` structure parameter, *lpSecurityAttributes* (`SECURITY_ATTRIBUTES`) and *lpftLastWriteTime* (`FILETIME`), and passes `VarPtr(0&)` for it in [**WriteString**](RegCls/WriteString), [**WriteDWORD**](RegCls/WriteDWORD), [**CreateKey**](RegCls/CreateKey) and [**DeleteKeyEx**](RegCls/DeleteKeyEx). A pointer passed for a structure has to be passed `ByVal` (error TB5001). `VarPtr(0&)` is also the address of a temporary **Long**, not a null pointer; `ByVal vbNullPtr` passes a null pointer. The behaviour described on these pages is that of the package with those four calls passed `ByVal`. -> [!WARNING] +> [!WARNING] 1.0.1.1 > Three methods give wrong results. [**ReadString**](RegCls/ReadString) returns the text followed by NUL characters. [**GetRegistryValueType**](RegCls/GetRegistryValueType) returns **REG_NONE** for every value that holds data. The first overload of [**ConvertValueToString**](RegCls/ConvertValueToString) returns an empty string for the types it supports. ## Classes diff --git a/docs/Reference/Contributed/tbFileSysTools/File.md b/docs/Reference/Contributed/tbFileSysTools/File.md index bfa041d66..61d5a98ae 100644 --- a/docs/Reference/Contributed/tbFileSysTools/File.md +++ b/docs/Reference/Contributed/tbFileSysTools/File.md @@ -324,7 +324,7 @@ Syntax: *object*.**OpenAsTextStream** ( [ *ioMode* [, *encoding* [, *writeModeLi Returns a [**TextStream**](TextStream); the rules are those of [**OpenTextFile**](FileSystemTools#opentextfile) with *create* **False**. -> [!WARNING] +> [!WARNING] 1.9.0.0 > **ForWriting** empties the file when the stream is opened. Use **ForAppending** to add to it. ### Refresh diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md index 3f22abc5d..e58ad4148 100644 --- a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md +++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md @@ -52,7 +52,7 @@ These rules apply to every procedure on the page. Each entry states only what de - A path longer than 260 characters works in most procedures, among them [**CreateFolder**](#createfolder), [**FileExists**](#fileexists), [**StringToTextFile**](#stringtotextfile), [**TextFileToString**](#textfiletostring), [**CopyFile**](#copyfile), [**MoveFile**](#movefile), [**Rename**](#rename), [**DeleteFile**](#deletefile), [**DeleteFolder**](#deletefolder), [**GetFile**](#getfile) and [**GetFilePaths**](#getfilepaths). The Scripting Runtime does not accept such a path: its **FileExists** and **FolderExists** return **False** for it, **GetFile** raises error 53 and **OpenTextFile** raises error 76. -> [!NOTE] +> [!NOTE] 1.9.0.0 > These stop at the limit. [**GetFileVersion**](#getfileversion) returns an empty string, and [**File.ShortPath**](File#shortpath) returns the path as it is. [**SetCurrentDir**](#setcurrentdir) raises error 75. A path with a wildcard in it is not found: [**DeleteFile**](#deletefile) and [**CopyFile**](#copyfile) raise error 53 for `deep\*.dll`, where `deep` is a folder with a long path. [**Folder.Files**](Folder#files), [**Folder.SubFolders**](Folder#subfolders) and [**Folder.Size**](Folder#size) raise error 76 for a folder whose own path has 258 characters or more (see the note under [**Folder.Files**](Folder#files)), and [**GetRelativePath**](#getrelativepath) can return a wrong path (see its warning). ```tb check_build check_run @@ -81,7 +81,7 @@ DeleteFolder work **Wildcards.** [**CopyFile**](#copyfile), [**CopyFolder**](#copyfolder), [**DeleteFile**](#deletefile), [**DeleteFolder**](#deletefolder), [**MoveFile**](#movefile) and [**MoveFolder**](#movefolder) accept `*` and `?` in the last component of the path that names what to act on. A wildcard in a destination is error 5. A pattern that matches nothing is an error, as described for each procedure. A failure part-way through a wildcard operation leaves the earlier matches changed; the sample under [**DeleteFile**](#deletefile) shows it. -> [!WARNING] +> [!WARNING] 1.9.0.0 > Windows matches a pattern against the 8.3 short name of a file as well as against its name. On a volume that creates short names, `*.txt` also matches `a.txtx`, whose short name ends in `.TXT`. [**CopyFile**](#copyfile), [**DeleteFile**](#deletefile) and [**MoveFile**](#movefile) act on such a file, and the procedures for folders match the short names of folders in the same way. The Scripting Runtime does the same. [**GetFilePaths**](#getfilepaths) does not list such a file: it tests each name against the pattern again. ```tb check_build check_run @@ -110,7 +110,7 @@ DeleteFolder work A directory junction or symbolic link is a folder that stands for another folder. -> [!NOTE] +> [!NOTE] 1.9.0.0 > [**CopyFolder**](#copyfolder), [**DeleteFolder**](#deletefolder), [**GetFilePaths**](#getfilepaths) with *recursive*, [**FindInTextFiles**](#findintextfiles) with *recursive* and [**Folder.Size**](Folder#size) do not enter a link: > > - **CopyFolder** leaves the link out of the copy. @@ -504,7 +504,7 @@ Errors: - 58 when a file exists and *overwrite* is **False**. -> [!NOTE] +> [!NOTE] 1.9.0.0 > A directory junction or symbolic link inside the source is left out of the copy: the copy has no folder of that name, and nothing from the folder the link stands for. The Scripting Runtime copies the files of the target into the copy under the name of the link. See [Junctions and symbolic links](#junctions-and-symbolic-links). ### CreateFolder @@ -612,7 +612,7 @@ The contents are deleted first, and an item that cannot be deleted is skipped wi Errors: 76 when no folder matches or a file is named; 70 when the folder is not empty because something in it could not be deleted, such as a read-only file when *force* is **False**. The folder and that file stay. The Scripting Runtime raises error 70 in that case too. -> [!NOTE] +> [!NOTE] 1.9.0.0 > A directory junction or symbolic link inside the folder is removed, and the folder it stands for keeps its files. The Scripting Runtime deletes the files in the target as well, so a tree that holds a link to a folder outside it loses the contents of that folder there. See [Junctions and symbolic links](#junctions-and-symbolic-links). ### GetFilePaths @@ -685,7 +685,7 @@ Moving a file onto itself does nothing and returns its path, except that error 7 Errors: 53 when a wildcard matches no file, or matches only folders; 76 when the destination folder of a wildcard move does not exist; 58 when the destination file exists; 52 when the name of the destination holds a character that Windows does not allow; 75 when it is the reserved device name `con` (the message ends with the Windows code 87). -> [!NOTE] +> [!NOTE] 1.9.0.0 > A *source* without a wildcard that names a folder is moved too, with no error: `MoveFile "C:\old", "C:\new"` renames the folder. The Scripting Runtime raises error 53. @@ -838,7 +838,7 @@ A sample looks like binary data when it holds a zero byte, or more than 2% contr The result is an estimate, as every guess about the encoding of a text file is. A file in UTF-16, UTF-32 or UTF-8, with or without a BOM, is found right, except in the cases of the note below; so is a Latin-1 file when the system code page is Latin-1. Pass the encoding to the procedure that reads the file when it is known. -> [!NOTE] +> [!NOTE] 1.9.0.0 > A short file gives little evidence. UTF-16 text with no zero byte, such as eight kanji with no line ending, is reported as **encSystemAnsi**, or as **encUtf8** when its bytes are also valid UTF-8, as for eight hiragana. A few bytes of binary data can be reported as UTF-16. Text in another code page without a BOM is not found: GB18030 text with an accented letter is reported as **encSystemAnsi**, and UTF-7 or Big5 text that is pure ASCII as **encUtf8**. A sample that ends in the first byte of a multi-byte sequence is judged without that byte, so a Latin-1 file whose only accented letter is its last byte is reported as **encUtf8**, and so is a Latin-1 file whose accented letter comes after the first 32,768 bytes. Raises error 53 when the file does not exist; see [Errors](./#errors). @@ -1458,7 +1458,7 @@ Syntax: **CreateTextFile** ( *filePath* [, *overwrite* [, *encoding* [, *lineEnd The file is written in place, with no swap file, and an existing read-only file raises error 70. The errors are those of [**OpenTextFile**](#opentextfile) for **ForWriting**, and error 52 for a name that has `<`, `>`, `"`, `|`, `?`, `*` or a control character, or is a reserved device name such as `con` (see [**CleanFileName**](#cleanfilename)). A colon in the name is allowed: it addresses an alternate data stream of an NTFS file. -> [!IMPORTANT] +> [!IMPORTANT] 1.9.0.0 > The third argument of **Scripting.FileSystemObject.CreateTextFile** is a **Boolean** *Unicode*, and this procedure's third argument is an *encoding*. A **Boolean** converts to a number: **True** is -1, which is **encAutoDetect**, and on writing that means UTF-8, not UTF-16. **False** is 0, **encSystemAnsi**. Code ported from the Scripting Runtime that passes **True** needs **encUtf16** or **encUtf16Bom** here. ```tb check_build check_run @@ -1524,10 +1524,10 @@ What the mode does: Errors: 53 when the file does not exist for **ForReading**, or for the other modes with *create* **False**; 76 when the folder does not exist; 52 for a name that is not valid when *create* is **True**; 5 for an unknown *ioMode*, and for a binary file as above; 70 when another stream has the file open for writing. The error has an empty **Err.Source** and the standard text of its number, `File not found` for 53, without the path. -> [!NOTE] +> [!NOTE] 1.9.0.0 > A stream in UTF-7 holds the whole file in memory, as [**TextStream**](TextStream#reading-and-writing) describes. For **ForWriting** on a read-only file, the stream opens without an error and the file keeps its text; error 70 comes at the first [**Flush**](TextStream#flush) or [**Close**](TextStream#close). Every other encoding raises error 70 when the stream is opened. -> [!IMPORTANT] +> [!IMPORTANT] 1.9.0.0 > The fourth argument of **Scripting.FileSystemObject.OpenTextFile** is a **Tristate** *Format*, and this procedure's is an *encoding*. **TristateTrue** is -1, which is **encAutoDetect**: on writing that means UTF-8, not UTF-16. The default also differs: a file opened for writing here is UTF-8 without a BOM, where the Scripting Runtime writes ANSI. This sample appends to a file that has a BOM and LF line endings. The new line gets the line ending of the file, and no second BOM is written: diff --git a/docs/Reference/Contributed/tbFileSysTools/TextStream.md b/docs/Reference/Contributed/tbFileSysTools/TextStream.md index 0fb3ad39c..e3d4108fe 100644 --- a/docs/Reference/Contributed/tbFileSysTools/TextStream.md +++ b/docs/Reference/Contributed/tbFileSysTools/TextStream.md @@ -80,7 +80,7 @@ DeleteFolder work **Two modes.** Most encodings, UTF-8 among them, are read and written in pieces of 64 KB, and [**IsStreaming**](#isstreaming) is **True**. UTF-7 cannot be cut into pieces. A stream in UTF-7 holds the whole file in memory and [**IsStreaming**](#isstreaming) is **False**. Every member works in both modes. -> [!NOTE] +> [!NOTE] 1.9.0.0 > A stream of this kind that is open for writing does not empty the file when it is opened: the file is replaced when the stream is closed or flushed. When the file is read-only, the stream opens without an error, the file keeps its text, and the first [**Flush**](#flush) or [**Close**](#close) fails with error 70. **Position.** [**Line**](#line) and [**Column**](#column) follow the characters that are read or written. Reading a line moves past its line ending. diff --git a/docs/Reference/Contributed/tbFileSysTools/index.md b/docs/Reference/Contributed/tbFileSysTools/index.md index 1e1b98793..4cab97d15 100644 --- a/docs/Reference/Contributed/tbFileSysTools/index.md +++ b/docs/Reference/Contributed/tbFileSysTools/index.md @@ -124,7 +124,7 @@ The procedures raise the error numbers that VB6 uses for the same failures. **Er Windows reports a failure with its own code, and the package maps it to one of those numbers: *file not found* (2) to 53, *path not found* (3) and *bad directory name* (267) to 76, *file exists* (80 and 183) to 58, *access denied* and the sharing violations (5, 32 and 33) to 70, and *invalid name* (123) to 52. -> [!NOTE] +> [!NOTE] 1.9.0.0 > A few failures raise a number that is not the one the same failure raises elsewhere in the package. The sample shows the first two. > > - A file that is held open with exclusive access raises the unspecified error -2147467259 when it is read, by [**TextFileToString**](FileSystemTools#textfiletostring), [**GetFileEncoding**](FileSystemTools#getfileencoding), [**ReadStream**](FileSystemTools#readstream) and the other procedures that read a whole file. Writing to it raises 70. diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md index 1b8e4da37..c0f5b278a 100644 --- a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Pack.md @@ -53,7 +53,7 @@ Every other file is stored as it is on disk, a text file in `Sources` and a code The conversion removes every CR and then turns every LF into CR LF. A file that already has CR LF line endings comes out unchanged. The files on disk are never changed; only the copy in the project file is. -> [!NOTE] +> [!NOTE] 1.1.0.0 > A CR that is not followed by LF is removed, and no line break replaces it. A code file whose lines end in a CR alone, as old Macintosh text files do, is packed as one line. The Import/export tool leaves such a CR in place. After the summary line, **Pack** prints the number of files that the conversion changed. A file that already had CR LF line endings is not counted, and when no file changed, the line is not printed: @@ -88,7 +88,7 @@ Exported "MyApp-src" -> C:\Temp\MyApp.twinproj (170594 bytes, 18 files, 6 direc The name is the root's name, the last folder of *inputDir*. The byte count is the size of the new file, and the file and folder counts do not include the root. -> [!NOTE] +> [!NOTE] 1.1.0.0 > The summary line of **Pack** begins `Exported`, and that of [**Unpack**](Unpack) begins `Imported`. The Import/export tool names the two directions the other way round: its `import` command packs a folder and its `export` command unpacks a project. ## Example diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md index 67c4bb2b3..663b9610c 100644 --- a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Test.md @@ -68,7 +68,7 @@ Without a sample, the four sample tests print `[SKIP]` and the summary ends with A *samplePath* that does not exist prints `Sample not found: ` and the path. No test runs. -> [!NOTE] +> [!NOTE] 1.1.0.0 > An empty *samplePath* does not make the method fail. With 20 of the 20 tests that run passed and 4 skipped, it returns **True**. Pass a project file to run all 24. ## Example diff --git a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md index 34f321005..6a0cb8e12 100644 --- a/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md +++ b/docs/Reference/Contributed/tbProjPacker/ProjPacker/Unpack.md @@ -48,7 +48,7 @@ The project file is read and checked before anything is deleted or written. If t A refusal returns **False** and leaves the folder as it was. -> [!WARNING] +> [!WARNING] 1.1.0.0 > A folder that passes the checks is deleted with everything in it, including files that are not part of any project. Point *cleanFirst* only at a folder that holds nothing but an earlier unpack. ## Output @@ -74,7 +74,7 @@ When the call fails, it prints `Unpack failed: ` and one of these reasons: | `Refusing to clean ` and the path in quotes, then `it is not empty and has no top-level 'Settings' file` | *cleanFirst* is **True** and *outputDir* fails the fourth check. | | `Cannot create directory: ` and the path | *outputDir* or a folder in it could not be created. | -> [!NOTE] +> [!NOTE] 1.1.0.0 > The summary line of **Unpack** begins `Imported`, and that of [**Pack**](Pack) begins `Exported`. The Import/export tool names the two directions the other way round: its `export` command unpacks a project and its `import` command packs a folder. ## Example diff --git a/docs/Reference/Contributed/tbProjPacker/index.md b/docs/Reference/Contributed/tbProjPacker/index.md index bf99520c1..a01757caa 100644 --- a/docs/Reference/Contributed/tbProjPacker/index.md +++ b/docs/Reference/Contributed/tbProjPacker/index.md @@ -36,7 +36,7 @@ Else End If ``` -> [!NOTE] +> [!NOTE] 1.1.0.0 > The package reads and writes the binary [file format](../../../../Features/Packages/File-Format) itself, as the Import/export tool's script does, and the format is an internal detail of the IDE that can change. [**Test**](ProjPacker/Test) with a project file as its argument parses that file, writes it again and compares the result, so it shows whether the package still understands the files of a given twinBASIC build. ## Output and errors diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md index 753533395..8929b93a3 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/IsValid.md @@ -24,7 +24,7 @@ The property compiles the pattern if it has not been compiled since [**Pattern** Check **IsValid** before [**Execute**](Execute), [**Test**](Test), [**Replace**](Replace) or [**Split**](Split) when the pattern comes from outside the program, because each of those raises the compile error. -> [!NOTE] +> [!NOTE] 1.8.0.0 > Although nothing is raised, the compile error of an invalid pattern stays in the **Err** object after the call that compiled the pattern returns. A program that tests **Err** for other errors calls **Err.Clear** first. diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md index d0a6a4cc2..0d68f4921 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/Replace.md @@ -40,7 +40,7 @@ A `$` in *replaceString* starts a substitution item. Any other text is copied as A `$` that starts no item raises no error, so a replacement string can hold a literal `$` as it is, at the end or before any other character. Before a character that would make an item, write `$$`: `$$$&` inserts a `$` and then the matched text, as the last example does. -> [!NOTE] +> [!NOTE] 1.8.0.0 > Items are read as in VBScript's **RegExp**, with two differences: `$<`*name*`>` is an addition of this package, and `$~` is dropped, where VBScript copies `$~` into the result. ## Remarks diff --git a/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md b/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md index a1b9c0d6a..b66672fec 100644 --- a/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md +++ b/docs/Reference/Contributed/tbRegExp/RegExp/SearchAborted.md @@ -28,7 +28,7 @@ The budget is 5,000 steps for each character of the string, at least 1,000,000 a Each call of **Test**, **Execute**, **Replace** or **Split** sets the value again: a call that finishes its search sets it to **False**, and so does a call that raises an error because the pattern is not valid, because no search was made. The value belongs to the object. A search on another **RegExp** does not change it, and a new object returns **False**. -> [!NOTE] +> [!NOTE] 1.8.0.0 > A pattern that is not catastrophic can also run out of steps, if it tries again from each start position and the string is long. `\d+x` on a string of 5,000 digits and no `x` takes a few seconds and then gives up, with the same result as for a string that has no match, and **SearchAborted** returns **True**. A string of 2,000 digits finishes in a second or two and returns **False**. ## Example diff --git a/docs/Reference/Contributed/tbRegExp/index.md b/docs/Reference/Contributed/tbRegExp/index.md index a6441988b..231c26230 100644 --- a/docs/Reference/Contributed/tbRegExp/index.md +++ b/docs/Reference/Contributed/tbRegExp/index.md @@ -42,7 +42,7 @@ Debug.Print re.Test("ABCdef") ' False ``` -> [!NOTE] +> [!NOTE] 1.8.0.0 > A pattern that holds an atomic group `(?>...)` or a conditional is not valid: [**IsValid**](RegExp/IsValid) returns **False**. A named backreference `\k`, `\A`, `\Z` and `\p{...}` are not supported either, but they do not make the pattern invalid, because a backslash before a letter that names no escape stands for the letter itself, as in VBScript: `\k` matches the text `k`, and `\A` matches `A`. The pattern `\p{L}` is invalid for another reason: `{L}` does not start a quantifier (see below). The escapes that name a control character or a character code are read as in VBScript: @@ -66,7 +66,7 @@ Debug.Print re.Test("a" & ChrW(2)) ' True ``` -> [!NOTE] +> [!NOTE] 1.8.0.0 > A backreference to a group that has not yet taken part in the match, such as `\1(a)`, matches no characters, so the pattern finds `a` in the string `a`. VBScript's **RegExp** finds no match for such a pattern. `\w` matches only the ASCII letters and digits and `_`, as in VBScript, so `\w+` finds `caf` in `café`. @@ -74,7 +74,7 @@ Debug.Print re.Test("a" & ChrW(2)) ' True A `{` has to start a quantifier: `{n}`, `{n,}`, `{n,m}` or `{,m}`, which means `{0,m}`. A pattern in which a `{` starts none, such as `a{` or `a{x}`, is not valid; write `\{` for the character. A `]` or `}` that closes nothing is an ordinary character, as in VBScript. -> [!NOTE] +> [!NOTE] 1.8.0.0 > VBScript's **RegExp** reads a `{` that starts no quantifier, and `{,m}`, as ordinary text: `a{x}` is a valid pattern there that finds the text `a{x}`, and `a{,2}` finds the text `a{,2}` and not `aa`. In **tbRegExp** the first is not valid, so a search with it raises the compile error, and `a{,2}` finds up to two `a`. ```tb check_build check_run diff --git a/docs/Reference/Contributed/tsJson/Json.md b/docs/Reference/Contributed/tsJson/Json.md index 79a8fe317..e7f75519f 100644 --- a/docs/Reference/Contributed/tsJson/Json.md +++ b/docs/Reference/Contributed/tsJson/Json.md @@ -24,7 +24,7 @@ The module has no state and no settings. Options for one call are a [**JsonOptio **Errors.** The functions of the module do not raise run-time errors for bad data or bad arguments. A function that fails returns an empty result (**Empty**, **Nothing**, an empty string, **False** or an unbound array) and sets the last error, which the predeclared object [**JsonError**](JsonError) reports. A function that can fail clears the last error when it starts, so the last error always belongs to the latest such call. The members that cannot fail neither clear nor set it: [**TypeOfValue**](#typeofvalue), [**IsObject**](#isobject), [**IsArray**](#isarray), [**Equals**](#equals), [**PointerEscape**](#pointerescape), [**PointerUnescape**](#pointerunescape), [**IsValidPattern**](#isvalidpattern), [**IsMatchPattern**](#ismatchpattern), [**IsSearchPattern**](#issearchpattern) and [**FormatIso8601**](#formatiso8601). The origin of an error is the name of the function that was called, such as `Json.Parse`. The functions [**TryParse**](#tryparse), [**TryParseFile**](#tryparsefile), [**TryParseLines**](#tryparselines) and [**TryParseLinesFile**](#tryparselinesfile) report the origin of the function they call: an error of **TryParse** has the origin `Json.Parse`. -> [!NOTE] +> [!NOTE] 1.0.15.0 > A function that finds nothing is not an error when its name says so (**PointerExists**, **PointerTryGet**, the `Try` functions and the JSONPath queries). The `Try` functions return **False**, and a JSONPath query that matches nothing returns an empty result with no error. Check [**JsonError.HasError**](JsonError#haserror), not **IsEmpty**, after a function that can return a JSON null: [**Parse**](#parse) returns **Null** for the text `null`, and **Empty** only for a failure. * TOC diff --git a/docs/Reference/Contributed/tsJson/JsonOptions.md b/docs/Reference/Contributed/tsJson/JsonOptions.md index 1f66ae549..fd2cf0cc0 100644 --- a/docs/Reference/Contributed/tsJson/JsonOptions.md +++ b/docs/Reference/Contributed/tsJson/JsonOptions.md @@ -22,7 +22,7 @@ Debug.Print JsonOptions.Rfc.Indent ' 4 A property of an options object checks nothing when it is set. The value is used by the call that receives the object, and a call does not change the object. -> [!IMPORTANT] +> [!IMPORTANT] 1.0.15.0 > Assigning a property on the predeclared object itself raises run-time error 5, *Invalid procedure call or argument*, with the source `JsonOptions` and the message *JsonOptions (default instance) is a factory. Use Rfc, Jsonc, Minified, or Pretty.* Create an options object first and assign the properties of that. > > ```tb check_build check_run diff --git a/docs/Reference/Contributed/tsJson/index.md b/docs/Reference/Contributed/tsJson/index.md index 90cda2fcb..dbc0b031b 100644 --- a/docs/Reference/Contributed/tsJson/index.md +++ b/docs/Reference/Contributed/tsJson/index.md @@ -284,7 +284,7 @@ Debug.Print Tests.Run() ' mine > Debug.Print JsonError.HasError ' False > ``` -> [!NOTE] +> [!NOTE] 1.0.15.0 > The package has three more problems, each of which is an error that the reader cannot miss. > > - **Numbers must fit a Decimal.** [**Parse**](Json#parse) rejects a valid JSON number with the error `InvalidNumber` when its digits make a whole number above 79228162514264337593543950335, or its scale is outside -28 to 28, so `1e29`, `1e-29`, a number of 36 digits such as pi, and the largest **Double**, `1.7976931348623157e308`, are errors. [**Serialize**](Json#serialize) fails the same way for a **Double** above that range. RFC 8259 allows an implementation to limit the range of numbers. diff --git a/docs/Reference/Contributed/tsPwGen/PwGenError.md b/docs/Reference/Contributed/tsPwGen/PwGenError.md index 82840c093..0c7bd012e 100644 --- a/docs/Reference/Contributed/tsPwGen/PwGenError.md +++ b/docs/Reference/Contributed/tsPwGen/PwGenError.md @@ -58,7 +58,7 @@ The code is a **String** that names the error. It is case-sensitive. These are t A recipe with several faults sets one error, the first fault that the package finds. In random mode the properties of wordlist mode are not checked, and the other way round. -> [!NOTE] +> [!NOTE] 1.0.8.0 > The package declares two more codes, `EmptyCharset` and `EmptyWordlist`. No recipe sets them: the package's own tests list them as having no trigger, together with `InvalidRange`, which only a direct call of the default random source can set. ```tb check_build check_run diff --git a/docs/Reference/Contributed/tsPwGen/PwGenOptions.md b/docs/Reference/Contributed/tsPwGen/PwGenOptions.md index 92fc2bed0..6c33c3692 100644 --- a/docs/Reference/Contributed/tsPwGen/PwGenOptions.md +++ b/docs/Reference/Contributed/tsPwGen/PwGenOptions.md @@ -24,7 +24,7 @@ Debug.Print Left$(pw, 1) Like "[A-Za-z]" ' True A property of a recipe checks nothing when it is set. A value that cannot be used, such as a **Length** of 0, is found when the recipe is passed to **Generate** or **Entropy**, and is reported there as an error code. -> [!IMPORTANT] +> [!IMPORTANT] 1.0.8.0 > Assigning a property on the predeclared object itself raises run-time error 5, *Invalid procedure call or argument*, with the source `PwGenOptions` and the message *PwGenOptions (default instance) is a factory. Use Alphanumeric, Complex, Passphrase, Pin, or Hex.* Create a recipe first and assign the properties of that. > > ```tb check_build check_run diff --git a/docs/Reference/Contributed/tsPwGen/index.md b/docs/Reference/Contributed/tsPwGen/index.md index cb42fc62a..5de254274 100644 --- a/docs/Reference/Contributed/tsPwGen/index.md +++ b/docs/Reference/Contributed/tsPwGen/index.md @@ -129,10 +129,10 @@ The names are visible to the whole project: the **Generate** and **Entropy** fun ## Known problems in version 1.0.8.0 -> [!WARNING] +> [!WARNING] 1.0.8.0 > Called directly, the **NextInRange** method of [**PwGenCryptoSource**](PwGenCryptoSource#errors) returns 0, with no error of its own, while an earlier failure of the package is still the last error. Clear the last error first. The functions of [**PwGen**](PwGen) are not affected. -> [!NOTE] +> [!NOTE] 1.0.8.0 > The package has four more problems. None of them returns a wrong password. > > - **`HasNoConsecutiveRepeats` fails for small pools.** A recipe with [**HasNoConsecutiveRepeats**](PwGenOptions#hasnoconsecutiverepeats) returns an empty string with the error `CannotAvoidRepeats` in about one call of four for the pool `AB` and a **Length** of 3, although valid passwords exist. diff --git a/indexer/draft/workflow.mjs b/indexer/draft/workflow.mjs index 7c12f7f54..19606ba91 100644 --- a/indexer/draft/workflow.mjs +++ b/indexer/draft/workflow.mjs @@ -165,7 +165,7 @@ Then settle each doubt below that a run can settle: - Where the sources say the package replaces another object, run the same input on that object when this machine can: VBScript's under \`cscript //nologo .vbs\`, printing with \`WScript.Echo\`; VB6 code with \`node scripts/vb6run.mjs\`. Write those files outside the repository, and never call \`MsgBox\` or \`InputBox\`. Where the two differ, the page says what the replaced object does, in a \`> [!NOTE]\`. - ${ /^\d+\.\d+\.\d+\.\d+$/.test(pkg.version) - ? `A callout about what this version of the package does, a defect above all, names the version in its label, the rest of its \`[!TYPE]\` line: \`> [!WARNING] ${pkg.version}\`. One` + ? `Every callout in the section has a label, the rest of its \`[!TYPE]\` line, and the build refuses one without. One about what the package does names this version: \`> [!WARNING] ${pkg.version}\`. One` : 'A callout' } about what this build of twinBASIC does names the build in its label, the rest of its \`[!TYPE]\` line: \`> [!NOTE] BETA \`. Its text then starts with the fact, never with "In version ..." or "In BETA ...". - Correct the pages to what the runs printed, delete ${probe}, and run the section once more. diff --git a/lib/callout-labels.mjs b/lib/callout-labels.mjs index ab27934fa..338db1951 100644 --- a/lib/callout-labels.mjs +++ b/lib/callout-labels.mjs @@ -9,7 +9,8 @@ // callout's text, so that a tool, and not a reader, can find what a new build or a new // version of a package leaves to re-check. A package version must be the one the section // documents, its landing page's `indexed_from`, so moving a section to a new version fails the -// build until each of its labelled callouts has been checked again and its label moved. +// build until each of its labelled callouts has been checked again and its label moved. In such +// a section every callout has a label. // `twin-index analyze` lists both kinds of callout to check again. // // The grammar is closed: anything else after `[!TYPE]` is an error, since the renderer used to @@ -62,7 +63,9 @@ export function parseCalloutLabel(text) { } /** - * Checks a parsed label against the page it is on. + * Checks a parsed label against the page it is on. In a package section every callout has a + * label: what it says is true of the version documented, or of a twinBASIC build, and a new + * version or build is a reason to check it again. * * @param {{kind: string, value: string}[]} items * @param {string | undefined} packageVersion the `indexed_from` of the page's package @@ -70,6 +73,8 @@ export function parseCalloutLabel(text) { * @returns {string | undefined} what is wrong, if anything */ export function checkCalloutLabel(items, packageVersion) { + if (packageVersion && items.length === 0) + return `a callout in a package section has a label: ${packageVersion}, the version it is true of, or BETA for a fact of twinBASIC alone`; const version = items.find((i) => i.kind === "version"); if (!version) return undefined; if (!packageVersion) diff --git a/test/render.test.mjs b/test/render.test.mjs index da00a85c4..0e50269ab 100644 --- a/test/render.test.mjs +++ b/test/render.test.mjs @@ -433,6 +433,10 @@ describe("callout labels", () => { assert.equal(title(src, { packageVersion: "1.0.0.4" }), `Warning ${tag("1.0.0.4")}`); assert.throws(() => title(src, { packageVersion: "1.0.0.5" }), /section documents 1\.0\.0\.5/); assert.throws(() => title(src, { packageVersion: "" }), /no section/); + // In a package section a callout has a label, and a build alone is one. + assert.throws(() => title("> [!NOTE]\n> body\n", { packageVersion: "1.0.0.4" }), /has a label: 1\.0\.0\.4/); + assert.equal(title("> [!NOTE] BETA 1005\n> body\n", { packageVersion: "1.0.0.4" }), `Note ${tag("BETA 1005")}`); + assert.equal(title("> [!NOTE]\n> body\n", { packageVersion: "" }), "Note"); // Left out, as the code-region gate's sweep leaves it, it is not checked. assert.equal(title(src), `Warning ${tag("1.0.0.4")}`); }); From 60c3e620e6489d9b36cdb3f5bddceeeb5bcc37cd Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 14 Oct 2026 13:20:58 +0200 Subject: [PATCH 12/13] Callout labels: timeless marks a callout no build or version can change (drawn as no tag, never listed to check again); tbFileSysTools' short-name WARNING is timeless and says which procedures the Scripting Runtime shares --- WIP.md | 2 +- builder/render.mjs | 6 +++++- docs/Documentation/Authoring.md | 3 ++- docs/Documentation/Tools-Build.md | 2 +- .../tbFileSysTools/FileSystemTools.md | 4 ++-- indexer/draft/workflow.mjs | 2 +- lib/callout-labels.mjs | 21 +++++++++++++++---- test/render.test.mjs | 9 ++++++++ 8 files changed, 38 insertions(+), 11 deletions(-) diff --git a/WIP.md b/WIP.md index 852bdf3c6..89d4706c3 100644 --- a/WIP.md +++ b/WIP.md @@ -230,7 +230,7 @@ The vocabulary tables further down cover word choice. The rules in this subsecti **A known twinBASIC defect that silently gives a wrong result is a `> [!WARNING]` until a fixed build is released** (owner's rule): a wrong value with no error raised is data corruption the reader cannot see. A crash is a WARNING too (unsaved data is lost), and so is an error that hides a silent part (a procedure run twice before the error). A defect the reader cannot miss and that loses nothing --- an error raised, a statement refused --- is a `> [!NOTE]`. Name the build in either, in its label (next paragraph). **Once a fixed build is released, the callout goes** (owner's rule): the documentation is not a record of past defects, so the page then states the correct behaviour with no mention of the defect. The defect's BUGS-TO-REPORT entry says in its HTML comment which page carries the callout, so the callout is removed when the entry is retired. - **A callout says what it applies to in its label, the rest of its `[!TYPE]` line, and never in its text** (owner's rule): `> [!WARNING] BETA 1005` for a fact of that twinBASIC build, `> [!NOTE] since BETA 984` for one true from that build on, `> [!WARNING] 1.0.0.4` for one of that version of the page's own package, and `> [!NOTE] 1.6.151.1, BETA 1005` for both. `BETA` is upper case and a package version is bare, all four parts. The text then starts with the fact: never "In twinBASIC BETA 1005, ...", "BETA 1005 has a defect: ..." or "In version 1.0.0.4, ...". The build draws each item as a tag after the title and fails on anything else after `[!TYPE]`, and on a package version other than the section's `indexed_from` (`lib/callout-labels.mjs`). A label belongs on what is true of one build or one version, a defect above all; a callout that holds as well as that something true of every build is split in two. **In a contributed package's section every callout has a label** (owner's rule), the section's version for what the package does, so that moving the section to a new version sends each one back to be checked; the build refuses one without. + **A callout says what it applies to in its label, the rest of its `[!TYPE]` line, and never in its text** (owner's rule): `> [!WARNING] BETA 1005` for a fact of that twinBASIC build, `> [!NOTE] since BETA 984` for one true from that build on, `> [!WARNING] 1.0.0.4` for one of that version of the page's own package, and `> [!NOTE] 1.6.151.1, BETA 1005` for both. `BETA` is upper case and a package version is bare, all four parts. The text then starts with the fact: never "In twinBASIC BETA 1005, ...", "BETA 1005 has a defect: ..." or "In version 1.0.0.4, ...". The build draws each item as a tag after the title and fails on anything else after `[!TYPE]`, and on a package version other than the section's `indexed_from` (`lib/callout-labels.mjs`). A label belongs on what is true of one build or one version, a defect above all; a callout that holds as well as that something true of every build is split in two. **In a contributed package's section every callout has a label** (owner's rule), the section's version for what the package does, so that moving the section to a new version sends each one back to be checked; the build refuses one without. **`> [!WARNING] timeless`** (owner's rule) marks a callout no new build or version can change: a fact of Windows or COM, behaviour the package exists to reproduce (the Scripting Runtime's, for an FSO replacement), or a design decision the sources state. It stands alone, draws no tag, and is never listed to check again; the package's test cases still cover what the package itself does. 7. **See Also.** Last section on the page, after `Example`; only a contributed package's landing page has one after it, `## Test cases`. Format: `- [Symbol](Symbol) ` where `` is the kind: statement, function, property, method, class, module, package. Pages with annotations use `- [Symbol](Symbol) -- short description` — the `--` source renders as a typographic dash via markdown-it's typographer (en-dash for `--`, em-dash for `---`). Don't write literal `—` in source; keep `--` for consistency across the docs. Order by conceptual proximity, not strict alphabetical. diff --git a/builder/render.mjs b/builder/render.mjs index f55fef2c9..782c946e0 100644 --- a/builder/render.mjs +++ b/builder/render.mjs @@ -1829,7 +1829,11 @@ export function rewriteAdmonitions(src, md, { packageVersion } = {}) { } // One tag per item, a space between them so that the title reads as words // to a screen reader; the title is a flex box, which does not draw it. - const tags = label.items.map((i) => ` ${escapeMarkup(i.text)}`).join(""); + // `timeless` is for the tooling only and draws nothing. + const tags = label.items + .filter((i) => i.kind !== "timeless") + .map((i) => ` ${escapeMarkup(i.text)}`) + .join(""); // The body lines all share the same leading indent; strip it plus the // `>` marker and AT MOST ONE following space, which is what blockquote // unwrapping means. diff --git a/docs/Documentation/Authoring.md b/docs/Documentation/Authoring.md index 15a62b108..11cd2b951 100644 --- a/docs/Documentation/Authoring.md +++ b/docs/Documentation/Authoring.md @@ -888,8 +888,9 @@ A callout that is true of one twinBASIC build, or of one version of a package, s - `since BETA 984` --- true from that build on. - `1.0.0.4` --- true of that version of the package whose section the page is in, written with all four parts. - `1.6.151.1, BETA 1005` --- both, separated by a comma. +- `timeless` --- true of every build and every version: a fact of Windows or COM, behaviour the package exists to reproduce, such as the Scripting Runtime's for a replacement of it, or a design decision the package's sources state. It stands alone, and the page shows no tag for it. -The text then starts with the fact itself, not with "In twinBASIC BETA 1005, ..." or "In version 1.0.0.4, ...". If only part of a callout depends on the build, split it into two callouts. In a contributed package's section every callout has a label: the section's version for what the package does, or `BETA ` for a fact of twinBASIC alone. The build fails on a callout there without one, on any other label, and on a package version other than the one the section documents, its landing page's `indexed_from`. So when a section moves to a new version of its package, each callout labelled with the old one is checked again before the build passes. [`twin-index analyze`](Tools-Build#twin-index) lists those callouts, and after a new twinBASIC build, the callouts labelled with an older one. +The text then starts with the fact itself, not with "In twinBASIC BETA 1005, ..." or "In version 1.0.0.4, ...". If only part of a callout depends on the build, split it into two callouts. In a contributed package's section every callout has a label: the section's version for what the package does, `BETA ` for a fact of twinBASIC alone, or `timeless`. The build fails on a callout there without one, on any other label, and on a package version other than the one the section documents, its landing page's `indexed_from`. So when a section moves to a new version of its package, each callout labelled with the old one is checked again before the build passes. [`twin-index analyze`](Tools-Build#twin-index) lists those callouts, and after a new twinBASIC build, the callouts labelled with an older one. A page states what the product does, never what the documentation's own checking could or could not do. "The samples harness cannot capture a console" is a fact about this repository's tools, not about the package, and has no place in a published page. A claim nobody has been able to check is written as the source supports it, or left out. diff --git a/docs/Documentation/Tools-Build.md b/docs/Documentation/Tools-Build.md index c35535af2..c30bd2c44 100644 --- a/docs/Documentation/Tools-Build.md +++ b/docs/Documentation/Tools-Build.md @@ -196,7 +196,7 @@ A name that matches no package is reported, the other packages are done, and the - **Update mode**, for a section whose landing page has `indexed_from`. The current `api.json` is compared with the snapshot committed at `HEAD`, provided that snapshot's `version` is the `indexed_from` tag. The report lists the symbols added (to create), changed (to update) and removed (to flag). - **Audit mode**, for a section with no `indexed_from`, and when update mode has nothing to compare with: no committed snapshot, one in an older format, or one of another version, each with a warning. Every public symbol is matched by name with the `title:` of the pages in the section, and those with no page are to create. -**The callouts to check again.** A callout's label says which build of twinBASIC or which version of its package it is true of (see [What a callout applies to](Authoring#what-a-callout-applies-to)). When the store holds a version of a package other than its section's `indexed_from`, the report lists each callout of the section labelled with another version, as a `recheck_callout` task: the build refuses the section's new `indexed_from` until each is checked and its label moved, or the callout removed. An `analyze` with no `--package`, `--packages` or `--all` also lists the callouts of the whole documentation labelled `BETA ` below the build the store's built-in packages were read from, and writes them to `indexer/.packages/callouts.json`. A `since BETA ` label is never listed: it stays true. +**The callouts to check again.** A callout's label says which build of twinBASIC or which version of its package it is true of (see [What a callout applies to](Authoring#what-a-callout-applies-to)). When the store holds a version of a package other than its section's `indexed_from`, the report lists each callout of the section labelled with another version, as a `recheck_callout` task: the build refuses the section's new `indexed_from` until each is checked and its label moved, or the callout removed. An `analyze` with no `--package`, `--packages` or `--all` also lists the callouts of the whole documentation labelled `BETA ` below the build the store's built-in packages were read from, and writes them to `indexer/.packages/callouts.json`. A `since BETA ` label is never listed, and neither is `timeless`: both stay true. `analyze` exits 0 whenever it writes its report, whatever the report lists: the list is the product, not a failure. diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md index e58ad4148..8b0e3e6e5 100644 --- a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md +++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md @@ -81,8 +81,8 @@ DeleteFolder work **Wildcards.** [**CopyFile**](#copyfile), [**CopyFolder**](#copyfolder), [**DeleteFile**](#deletefile), [**DeleteFolder**](#deletefolder), [**MoveFile**](#movefile) and [**MoveFolder**](#movefolder) accept `*` and `?` in the last component of the path that names what to act on. A wildcard in a destination is error 5. A pattern that matches nothing is an error, as described for each procedure. A failure part-way through a wildcard operation leaves the earlier matches changed; the sample under [**DeleteFile**](#deletefile) shows it. -> [!WARNING] 1.9.0.0 -> Windows matches a pattern against the 8.3 short name of a file as well as against its name. On a volume that creates short names, `*.txt` also matches `a.txtx`, whose short name ends in `.TXT`. [**CopyFile**](#copyfile), [**DeleteFile**](#deletefile) and [**MoveFile**](#movefile) act on such a file, and the procedures for folders match the short names of folders in the same way. The Scripting Runtime does the same. [**GetFilePaths**](#getfilepaths) does not list such a file: it tests each name against the pattern again. +> [!WARNING] timeless +> Windows matches a pattern against the 8.3 short name of a file as well as against its name. On a volume that creates short names, `*.txt` also matches `a.txtx`, whose short name ends in `.TXT`. [**CopyFile**](#copyfile), [**DeleteFile**](#deletefile) and [**MoveFile**](#movefile) act on such a file, as those of the Scripting Runtime do, and the procedures for folders match the short names of folders in the same way. [**GetFilePaths**](#getfilepaths), which the Scripting Runtime does not have, does not list such a file: it tests each name against the pattern again. ```tb check_build check_run Dim work As String diff --git a/indexer/draft/workflow.mjs b/indexer/draft/workflow.mjs index 19606ba91..26ec8a157 100644 --- a/indexer/draft/workflow.mjs +++ b/indexer/draft/workflow.mjs @@ -165,7 +165,7 @@ Then settle each doubt below that a run can settle: - Where the sources say the package replaces another object, run the same input on that object when this machine can: VBScript's under \`cscript //nologo .vbs\`, printing with \`WScript.Echo\`; VB6 code with \`node scripts/vb6run.mjs\`. Write those files outside the repository, and never call \`MsgBox\` or \`InputBox\`. Where the two differ, the page says what the replaced object does, in a \`> [!NOTE]\`. - ${ /^\d+\.\d+\.\d+\.\d+$/.test(pkg.version) - ? `Every callout in the section has a label, the rest of its \`[!TYPE]\` line, and the build refuses one without. One about what the package does names this version: \`> [!WARNING] ${pkg.version}\`. One` + ? `Every callout in the section has a label, the rest of its \`[!TYPE]\` line, and the build refuses one without. One about what the package does names this version: \`> [!WARNING] ${pkg.version}\`. The label \`timeless\` is for a callout no version can change, and the owner decides it: do not write it, but name a candidate in your report. One` : 'A callout' } about what this build of twinBASIC does names the build in its label, the rest of its \`[!TYPE]\` line: \`> [!NOTE] BETA \`. Its text then starts with the fact, never with "In version ..." or "In BETA ...". - Correct the pages to what the runs printed, delete ${probe}, and run the section once more. diff --git a/lib/callout-labels.mjs b/lib/callout-labels.mjs index 338db1951..ffdcb9b4f 100644 --- a/lib/callout-labels.mjs +++ b/lib/callout-labels.mjs @@ -4,6 +4,12 @@ // > [!NOTE] since BETA 984 true from this build on // > [!WARNING] 1.0.0.4 true of this version of the page's package // > [!WARNING] 1.6.151.1, BETA 1005 both +// > [!WARNING] timeless true of every build and version: drawn as no tag +// +// `timeless` is for a fact no new build or version can change: of Windows or COM, of what the +// package exists to reproduce (the Scripting Runtime's behaviour, for an FSO replacement), or +// a stated design decision. It stands alone, satisfies a package section's need for a label, +// and is never listed to check again. Only the tooling sees it. // // The label replaces "In twinBASIC BETA 1005, ..." and "In version 1.0.0.4, ..." in the // callout's text, so that a tool, and not a reader, can find what a new build or a new @@ -27,16 +33,23 @@ const CALLOUT_RE = /^[ \t]*>[ \t]*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]([^\n * Parses a label, the text after `[!TYPE]` on its line. * * @param {string} text - * @returns {{items: {kind: "beta" | "since" | "version", value: string, text: string}[], + * @returns {{items: {kind: "beta" | "since" | "version" | "timeless", value: string, text: string}[], * error?: string}} `items` is empty for an empty label; `error` says what is wrong * with one that does not parse, and then `items` is empty too. */ export function parseCalloutLabel(text) { const label = text.trim(); if (label === "") return { items: [] }; + const parts = label.split(","); const items = []; - for (const part of label.split(",")) { + for (const part of parts) { const item = part.trim().replace(/[ \t]+/g, " "); + if (item === "timeless") { + if (parts.length > 1) + return { items: [], error: `"${label}": timeless stands alone, since it is true of every build and version` }; + items.push({ kind: "timeless", value: "", text: item }); + continue; + } const beta = BETA_RE.exec(item); if (beta) { if (items.some((i) => i.kind !== "version")) @@ -54,7 +67,7 @@ export function parseCalloutLabel(text) { items: [], error: `"${item}" is not a callout label: write BETA , since BETA , or a package's ` + - "four-part version, separated by commas", + "four-part version, separated by commas, or timeless alone", }; } // The package version first, then the build, whatever order the source has. @@ -74,7 +87,7 @@ export function parseCalloutLabel(text) { */ export function checkCalloutLabel(items, packageVersion) { if (packageVersion && items.length === 0) - return `a callout in a package section has a label: ${packageVersion}, the version it is true of, or BETA for a fact of twinBASIC alone`; + return `a callout in a package section has a label: ${packageVersion}, the version it is true of, BETA for a fact of twinBASIC alone, or timeless for one no version can change`; const version = items.find((i) => i.kind === "version"); if (!version) return undefined; if (!packageVersion) diff --git a/test/render.test.mjs b/test/render.test.mjs index 0e50269ab..4cb976188 100644 --- a/test/render.test.mjs +++ b/test/render.test.mjs @@ -424,6 +424,9 @@ describe("callout labels", () => { ["two builds", "BETA 1005, since BETA 984", /twice/], ["two versions", "1.0.0.1, 1.0.0.2", /twice/], ["an empty item", "BETA 1005,", /not a callout label/], + ["timeless with a build", "timeless, BETA 1005", /stands alone/], + ["timeless with a version", "1.0.0.4, timeless", /stands alone/], + ["timeless in capitals", "Timeless", /not a callout label/], ]) { test(`refused: ${name}`, () => assert.throws(() => title(`> [!NOTE] ${label}\n> body\n`), why)); } @@ -437,6 +440,12 @@ describe("callout labels", () => { assert.throws(() => title("> [!NOTE]\n> body\n", { packageVersion: "1.0.0.4" }), /has a label: 1\.0\.0\.4/); assert.equal(title("> [!NOTE] BETA 1005\n> body\n", { packageVersion: "1.0.0.4" }), `Note ${tag("BETA 1005")}`); assert.equal(title("> [!NOTE]\n> body\n", { packageVersion: "" }), "Note"); + // timeless is a label, drawn as nothing, in a package section or out of one. + for (const packageVersion of ["1.0.0.4", "", undefined]) + assert.equal( + applyPreRenderRewrites("> [!WARNING] timeless\n> body\n", md, { packageVersion }), + applyPreRenderRewrites("> [!WARNING]\n> body\n", md), + ); // Left out, as the code-region gate's sweep leaves it, it is not checked. assert.equal(title(src), `Warning ${tag("1.0.0.4")}`); }); From 62bfd6d95e16cd6fc2843c719276261cfd1d4778 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 14 Oct 2026 13:29:20 +0200 Subject: [PATCH 13/13] Callout labels: 45 callouts that no version can change are timeless (Windows and registry facts, stated design, API contracts; owner); the console packages' NOTEs about the documentation's own tools are gone --- .../Contributed/FileProps/FileProperties/VCOMObject.md | 2 +- .../Contributed/FileProps/FileProperties/index.md | 2 +- .../Contributed/FileProps/FileProperty/VCOMObject.md | 2 +- .../Contributed/FileProps/FileProperty/Value.md | 2 +- .../Contributed/FileProps/FileProperty/index.md | 4 ++-- .../FileProps/FilePropertyExplorer/index.md | 2 +- .../Contributed/Fmt/PublicEntryPoints/StringFormat.md | 2 +- .../Fmt/StringValidation/StringStartsWith.md | 2 +- docs/Reference/Contributed/Fmt/index.md | 2 +- .../Contributed/TBMANLIB_ConsoleComponents/index.md | 3 --- .../Contributed/TBMANLIB_ConsoleCore/index.md | 3 --- .../Reference/Contributed/TBMANLIB_Diagnostic/index.md | 2 +- .../Contributed/TBMANLIB_Dialog/cDialogColor.md | 2 +- .../TBMANLIB_ListBoxHScroll/cListboxHScroll/Show.md | 4 ++-- .../TBMANLIB_ListBoxHScroll/cListboxHScroll/UnShow.md | 4 ++-- .../TBMANLIB_ListBoxHScroll/cListboxHScroll/index.md | 4 ++-- docs/Reference/Contributed/TBMANLIB_RES/GLOBAL_RES.md | 2 +- docs/Reference/Contributed/TBMANLIB_RES/cRes/Read.md | 2 +- .../Contributed/TBMANLIB_RES/cRes/ReturnBase64.md | 2 +- .../Contributed/TBMANLIB_RES/cRes/ReturnHex.md | 2 +- .../Contributed/TBMANLIB_RES/cRes/ReturnPicture.md | 2 +- .../Contributed/TBMANLIB_RES/cRes/ReturnString.md | 2 +- .../Contributed/TwinTimerLib/TwinTimer/Interval.md | 2 +- .../Contributed/TwinTimerLib/TwinTimer/Timer.md | 2 +- .../Contributed/TwinTimerLib/TwinTimer/index.md | 2 +- docs/Reference/Contributed/VBComDlg/CommonDialog.md | 6 +++--- docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md | 2 +- .../Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md | 2 +- .../Reference/Contributed/WinReg/RegCls/DeleteValue.md | 2 +- docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md | 2 +- docs/Reference/Contributed/WinReg/RegCls/index.md | 4 ++-- .../Contributed/tbFileSysTools/FileSystemTools.md | 10 +++++----- docs/Reference/Contributed/tbProjPacker/index.md | 2 +- docs/Reference/Contributed/tsJson/Json.md | 2 +- docs/Reference/Contributed/tsJson/JsonOptions.md | 2 +- docs/Reference/Contributed/tsPwGen/PwGenOptions.md | 2 +- 36 files changed, 45 insertions(+), 51 deletions(-) diff --git a/docs/Reference/Contributed/FileProps/FileProperties/VCOMObject.md b/docs/Reference/Contributed/FileProps/FileProperties/VCOMObject.md index e07ff2044..a57b0b3ad 100644 --- a/docs/Reference/Contributed/FileProps/FileProperties/VCOMObject.md +++ b/docs/Reference/Contributed/FileProps/FileProperties/VCOMObject.md @@ -9,7 +9,7 @@ permalink: /tB/Packages/Contributed/FileProps/FileProperties/VCOMObject Holds the native-code COM object created internally by the Virtual-COM library. For internal use only. -> [!WARNING] 1.7.1.0 +> [!WARNING] timeless > > **VCOMObject** is an internal implementation detail of the FileProps Virtual-COM loader, and the package marks it as for internal use only. Do not use it in application code. diff --git a/docs/Reference/Contributed/FileProps/FileProperties/index.md b/docs/Reference/Contributed/FileProps/FileProperties/index.md index b54f60efd..a0229a8a8 100644 --- a/docs/Reference/Contributed/FileProps/FileProperties/index.md +++ b/docs/Reference/Contributed/FileProps/FileProperties/index.md @@ -79,7 +79,7 @@ Set Prop = Props.Item("System.Media.Duration") Holds the native-code COM object created internally by the Virtual-COM library. **Object**. For internal use only. -> [!WARNING] 1.7.1.0 +> [!WARNING] timeless > Do not read, assign, or pass **VCOMObject** to any function. The package marks it as for internal use only, and **Count**, **FilePath** and **Item** forward their calls to it. ## Example diff --git a/docs/Reference/Contributed/FileProps/FileProperty/VCOMObject.md b/docs/Reference/Contributed/FileProps/FileProperty/VCOMObject.md index 8aa70280b..93323f726 100644 --- a/docs/Reference/Contributed/FileProps/FileProperty/VCOMObject.md +++ b/docs/Reference/Contributed/FileProps/FileProperty/VCOMObject.md @@ -9,7 +9,7 @@ permalink: /tB/Packages/Contributed/FileProps/FileProperty/VCOMObject Holds the native-code COM object created internally by the Virtual-COM library. For internal use only. -> [!WARNING] 1.7.1.0 +> [!WARNING] timeless > > **VCOMObject** is an internal implementation detail of the FileProps Virtual-COM loader, and the package marks it as for internal use only. Do not use it in application code. diff --git a/docs/Reference/Contributed/FileProps/FileProperty/Value.md b/docs/Reference/Contributed/FileProps/FileProperty/Value.md index eb646290c..55dbf828d 100644 --- a/docs/Reference/Contributed/FileProps/FileProperty/Value.md +++ b/docs/Reference/Contributed/FileProps/FileProperty/Value.md @@ -31,7 +31,7 @@ Syntax: *object*.**Value** **=** *value* *value* : A **Variant** containing the new value for the property. -> [!IMPORTANT] 1.7.1.0 +> [!IMPORTANT] timeless > > Write support requires Windows Vista or newer. Assigning a property that is not writable raises a *Permission denied* error. diff --git a/docs/Reference/Contributed/FileProps/FileProperty/index.md b/docs/Reference/Contributed/FileProps/FileProperty/index.md index adba344b4..f7c2c164b 100644 --- a/docs/Reference/Contributed/FileProps/FileProperty/index.md +++ b/docs/Reference/Contributed/FileProps/FileProperty/index.md @@ -66,7 +66,7 @@ Reading this property returns the value of the property as exposed by the operat Assigning a value writes the property. Write support requires Windows Vista or newer. Assigning a property that is not writable raises a *Permission denied* error. -> [!NOTE] 1.7.1.0 +> [!NOTE] timeless > Multi-value properties can return a **Variant** array. Check `IsArray(Prop.Value)` before using the result as a scalar. For a string representation of the value in localized form, use [**ValueDesc**](#valuedesc). @@ -89,7 +89,7 @@ Holds the native-code COM object created internally by the Virtual-COM library. Syntax: *object*.**VCOMObject** **As Object** -> [!WARNING] 1.7.1.0 +> [!WARNING] timeless > This member is for internal use only. Do not read or assign **VCOMObject** from application code. All public members of **FileProperty** forward their calls to it. ## Example diff --git a/docs/Reference/Contributed/FileProps/FilePropertyExplorer/index.md b/docs/Reference/Contributed/FileProps/FilePropertyExplorer/index.md index 846ef63dd..b83e99638 100644 --- a/docs/Reference/Contributed/FileProps/FilePropertyExplorer/index.md +++ b/docs/Reference/Contributed/FileProps/FilePropertyExplorer/index.md @@ -84,7 +84,7 @@ End If The shell property handler infrastructure requires Windows 2000 or later. If the memory for the native code cannot be allocated when the class is created, the class raises error 7 (*Out of memory*). -> [!NOTE] 1.7.1.0 +> [!NOTE] timeless > Write support for [**FileProperty.Value**](../FileProperty/Value) requires Windows Vista or newer. ## Example diff --git a/docs/Reference/Contributed/Fmt/PublicEntryPoints/StringFormat.md b/docs/Reference/Contributed/Fmt/PublicEntryPoints/StringFormat.md index 6005e9adf..0f079a9cb 100644 --- a/docs/Reference/Contributed/Fmt/PublicEntryPoints/StringFormat.md +++ b/docs/Reference/Contributed/Fmt/PublicEntryPoints/StringFormat.md @@ -47,7 +47,7 @@ An item is found again in the text by the text that [**ToString**](../StringForm > [!NOTE] 1.0.0.0 > The function is not .NET's **String.Format**, and it departs from it in several ways. Doubled braces do not escape a brace: `{{0}}` with the value `5` gives `{5}`. An index must be a number, and an item such as `{name}` raises error 13, *Type mismatch*. A value that no item uses is an error. Custom .NET format strings such as `{0:yyyy-MM-dd}` or `{0:#,##0.0}` are not understood, and give *Invalid format string*; the identifier `C` for a date takes a **Format$** string instead. The identifier `N` does not exist. A precision is a number of decimal places, not of significant digits. See the [package overview](../#differences-from-net) for the full list. -> [!NOTE] 1.0.0.0 +> [!NOTE] timeless > The results come from **Format$**, so the decimal separator, the thousands separator and the layout of dates and times follow the Windows regional settings. The output shown on this page is for an English (United States) system. ## Example diff --git a/docs/Reference/Contributed/Fmt/StringValidation/StringStartsWith.md b/docs/Reference/Contributed/Fmt/StringValidation/StringStartsWith.md index 05a6d006c..a968d12ab 100644 --- a/docs/Reference/Contributed/Fmt/StringValidation/StringStartsWith.md +++ b/docs/Reference/Contributed/Fmt/StringValidation/StringStartsWith.md @@ -22,7 +22,7 @@ Returns **True** when the first characters of *source* are exactly *startingSequ ## Remarks -> [!IMPORTANT] 1.0.0.0 +> [!IMPORTANT] timeless > The arguments are in the order *startingSequence*, *source*. This is the reverse of [**StringContains**](StringContains), which takes the string to search first. `StringStartsWith("Hello", "He")` asks whether `"He"` starts with `"Hello"` and returns **False**. The comparison is binary, so `"he"` does not start `"Hello"`. An empty *startingSequence* returns **True**. A *source* shorter than *startingSequence* returns **False**. diff --git a/docs/Reference/Contributed/Fmt/index.md b/docs/Reference/Contributed/Fmt/index.md index b1eba382b..fd48cb7b5 100644 --- a/docs/Reference/Contributed/Fmt/index.md +++ b/docs/Reference/Contributed/Fmt/index.md @@ -127,7 +127,7 @@ Debug.Print StringFormat("{0:f}|{0:g}", d) ' Sunday, October 11, 2026 2:05 PM Debug.Print StringFormat("{0:Cyyyy-MM-dd}", d) ' 2026-10-11 ``` -> [!NOTE] 1.0.0.0 +> [!NOTE] timeless > The results come from **Format$**, so the decimal separator, the thousands separator and the layout of dates and times follow the Windows regional settings. The output shown for the currency and date identifiers is for an English (United States) system. ## Escape sequences diff --git a/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md b/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md index 27c312ea8..20391d42a 100644 --- a/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md +++ b/docs/Reference/Contributed/TBMANLIB_ConsoleComponents/index.md @@ -13,9 +13,6 @@ permalink: /tB/Packages/Contributed/TBMANLIB_ConsoleComponents/ The **TBMANLIB_ConsoleComponents** contributed package is a set of text-interface components for console programs. It has progress bars ([**cProgressBar**](cProgressBar)), small line charts ([**cSparkline**](cSparkline)), bar charts ([**cVBar**](cVBar)), a table ([**cDataGrid**](cDataGrid)), bordered panels ([**cConsolePanel**](cConsolePanel)), a manager that places panels in a grid, a sidebar or a header and footer ([**cConsoleLayout**](cConsoleLayout)), colour themes ([**cConsoleTheme**](cConsoleTheme)), and helpers for colours and positions ([**cColorHelper**, **cLayoutHelper**](Helpers)). The components draw into the screen buffer of the full-screen mode of [**cConsole**](../TBMANLIB_ConsoleCore/cConsole), in the package [TBMANLIB_ConsoleCore](../TBMANLIB_ConsoleCore/); the program then writes the buffer to the console with [**Refresh**](../TBMANLIB_ConsoleCore/cConsole#refresh). This section documents version 1.0.0.1, published on TWINSERV on 3 February 2026 under the MIT licence. The package describes itself as "TBMAN: Text UI (TUI) component library base on ConsoleCore". -> [!NOTE] 1.0.0.1 -> The tools that check this documentation's samples run a sample and compare what it prints with **Debug.Print**. They cannot yet read what a program writes to a console. So the samples on these pages are compiled, and what the components draw on the console has not been checked by running them. Statements about what a component draws follow the package's source, and will be checked once the tools can capture a console. - ```tb check_build Dim con As New cConsole Dim layout As cConsoleLayout diff --git a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md index 133e99a69..dd8d744d7 100644 --- a/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md +++ b/docs/Reference/Contributed/TBMANLIB_ConsoleCore/index.md @@ -17,9 +17,6 @@ The **TBMANLIB_ConsoleCore** contributed package is a library for the console of The library is built on the Windows console functions of `kernel32.dll`. The package [TBMANLIB_ConsoleComponents](../TBMANLIB_ConsoleComponents/), by the same author, is a set of text-interface components that draw into the buffer of this package. -> [!NOTE] 1.0.0.1 -> The tools that check this documentation's samples run a sample and compare what it prints with **Debug.Print**. They cannot yet read what a program writes to a console, or send it keys and mouse events. So the samples on these pages are compiled, and what they show on the console has not been checked by running them. Statements about console output, keys and the mouse follow the package's source, and will be checked once the tools can capture a console. - ```tb check_build Dim con As New cConsole diff --git a/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md b/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md index 9dae36f4b..ba316a8b7 100644 --- a/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md +++ b/docs/Reference/Contributed/TBMANLIB_Diagnostic/index.md @@ -69,7 +69,7 @@ A result can also be read directly, without a formatter. Each [item](KeyValuePai ## What a report contains {: #what-a-report-contains } -> [!NOTE] 1.0.0.1 +> [!NOTE] timeless > A full report names the user and the computer, the folders in the environment variables such as `USERPROFILE` and `PATH`, every running program, the drives, and the names and IPv4 addresses of the network adapters. Read a report before it goes to someone else. ## Failures diff --git a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogColor.md b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogColor.md index b092f12c5..7b5130d39 100644 --- a/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogColor.md +++ b/docs/Reference/Contributed/TBMANLIB_Dialog/cDialogColor.md @@ -40,7 +40,7 @@ The color is a plain number. A system color constant such as `vbButtonFace` (`&H The object keeps 16 custom colors, numbered 0 to 15, for the 16 custom boxes of the dialog. They start as the colors of **QBColor**(0) to **QBColor**(15) and last as long as the object does, so a second dialog of the same object shows the custom colors that the first one left. Nothing is saved to the registry or to a file. -> [!NOTE] 1.1.0.0 +> [!NOTE] timeless > The **Add to Custom Colors** button of the expanded dialog writes into the custom colors of the object at once. Choosing **Cancel** afterwards leaves [**Color**](#color) unchanged but keeps the added custom color. [**Reset**](#reset) restores the 16 starting colors. ## Properties diff --git a/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/Show.md b/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/Show.md index d74d4a09e..472e2878b 100644 --- a/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/Show.md +++ b/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/Show.md @@ -22,10 +22,10 @@ Syntax: *object*.**Show** *ListBoxInst* After **Show** returns, the **ListBox** scrolls horizontally when any item is wider than the visible area. -> [!IMPORTANT] 1.0.0.1 +> [!IMPORTANT] timeless > **Show** must be called after the **ListBox** has been populated. If items are added or removed after the call, the horizontal extent is not updated automatically. Call **Show** again on the same control to recalculate the extent for the updated item list. -> [!NOTE] 1.0.0.1 +> [!NOTE] timeless > The subclass installed by **Show** is removed automatically when the class instance is destroyed (`Class_Terminate`). It can also be removed explicitly by calling [**UnShow**](UnShow). ### Example diff --git a/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/UnShow.md b/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/UnShow.md index b4faa5823..d401f68c3 100644 --- a/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/UnShow.md +++ b/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/UnShow.md @@ -16,10 +16,10 @@ Syntax: *object*.**UnShow** **UnShow** calls `RemoveWindowSubclass` (comctl32) to detach the scroll message handler that was installed by **Show**. After this call, the **ListBox** control associated with the instance no longer receives the subclassed `WM_HSCROLL` processing added by the class. -> [!NOTE] 1.0.0.1 +> [!NOTE] timeless > **UnShow** does not hide the horizontal scrollbar itself, and does not reset the horizontal extent or release the **ListBox** reference---it removes only the subclass procedure. Call `ShowScrollBar` directly with `SB_HORZ` and `0` if the scrollbar must also be hidden. -> [!IMPORTANT] 1.0.0.1 +> [!IMPORTANT] timeless > The class destructor (`Class_Terminate`) calls the internal `RemoveListBoxSubclass` helper automatically, so **UnShow** need only be called explicitly when the subclass must be removed before the **cListboxHScroll** instance goes out of scope. ### Example diff --git a/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/index.md b/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/index.md index 2f5f8dc14..4a4367608 100644 --- a/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/index.md +++ b/docs/Reference/Contributed/TBMANLIB_ListBoxHScroll/cListboxHScroll/index.md @@ -49,10 +49,10 @@ Syntax: *object*.**Show** *ListBoxInst* The scroll range is calculated from the widest item's pixel width measured with the font that the control reports through `WM_GETFONT`, so a **ListBox** that uses a non-default font is measured with that font. -> [!NOTE] 1.0.0.1 +> [!NOTE] timeless > **Show** measures item widths at the moment it is called. If items are added or removed after **Show**, the scroll range is not updated automatically. Call **Show** again after bulk changes to refresh the extent. -> [!IMPORTANT] 1.0.0.1 +> [!IMPORTANT] timeless > The control's **hWnd** must be valid (the form must be loaded and the control fully created) before calling **Show**. Calling **Show** before the control's window exists returns silently without installing any scrolling behavior. ### UnShow diff --git a/docs/Reference/Contributed/TBMANLIB_RES/GLOBAL_RES.md b/docs/Reference/Contributed/TBMANLIB_RES/GLOBAL_RES.md index 548ac9e47..47f5a126f 100644 --- a/docs/Reference/Contributed/TBMANLIB_RES/GLOBAL_RES.md +++ b/docs/Reference/Contributed/TBMANLIB_RES/GLOBAL_RES.md @@ -22,7 +22,7 @@ Public Res As New cRes Because of **As New**, the instance is created automatically the first time **Res** is referenced. It provides the same interface as a manually constructed **cRes** instance: [**Read**](cRes/Read), [**ReturnBytes**](cRes/ReturnBytes), [**ReturnString**](cRes/ReturnString), [**ReturnHex**](cRes/ReturnHex), [**ReturnBase64**](cRes/ReturnBase64), and [**ReturnPicture**](cRes/ReturnPicture). -> [!NOTE] 1.0.0.4 +> [!NOTE] timeless > **GLOBAL_RES.Res** is a single shared instance for the entire project. Calling **Res.Read** replaces its internal byte buffer. If multiple callers read different resources in the same execution path, use separate **cRes** instances rather than sharing **GLOBAL_RES.Res**. ## Example diff --git a/docs/Reference/Contributed/TBMANLIB_RES/cRes/Read.md b/docs/Reference/Contributed/TBMANLIB_RES/cRes/Read.md index 8e9268a49..dc1fe8631 100644 --- a/docs/Reference/Contributed/TBMANLIB_RES/cRes/Read.md +++ b/docs/Reference/Contributed/TBMANLIB_RES/cRes/Read.md @@ -19,7 +19,7 @@ Syntax: *object*.**Read**( *id*, *Type* ) **As cRes** **Read** calls the **LoadResData** function with the supplied *id* and *Type* arguments and stores the resulting byte array internally. The method returns the **cRes** instance itself, so calls to the retrieval methods ([**ReturnBytes**](ReturnBytes), [**ReturnString**](ReturnString), [**ReturnHex**](ReturnHex), [**ReturnBase64**](ReturnBase64), [**ReturnPicture**](ReturnPicture)) can be chained directly onto the **Read** call. -> [!IMPORTANT] 1.0.0.4 +> [!IMPORTANT] timeless > **Read** must be called before any retrieval method. The resource data is not populated at construction time; a retrieval method called without a prior **Read** will operate on an empty byte array and may raise a runtime error. > [!NOTE] 1.0.0.4 diff --git a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnBase64.md b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnBase64.md index 602e05e72..e9a1f62d4 100644 --- a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnBase64.md +++ b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnBase64.md @@ -37,7 +37,7 @@ The following **Base64Prefix** values are defined: > [!IMPORTANT] 1.0.0.4 > **ReturnBase64** requires that MSXML2 is registered on the machine. It creates an `MSXML2.DOMDocument` COM object internally. This call will fail at run time on systems where MSXML2 is not available. -> [!NOTE] 1.0.0.4 +> [!NOTE] timeless > Call [**Read**](Read) before **ReturnBase64**. The method encodes whatever bytes are currently stored in the instance, and an instance on which **Read** has not been called holds none. ### Example diff --git a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnHex.md b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnHex.md index 5a5d2b428..def5362ab 100644 --- a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnHex.md +++ b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnHex.md @@ -21,7 +21,7 @@ Each byte is formatted as exactly two uppercase hex digits, left-padded with `0` When *LineBreak* is greater than `0`, it takes precedence over *Separator*: the *Separator* string is not inserted between bytes that fall on a line-break boundary. If both *Separator* is non-empty and *LineBreak* is greater than `0`, *Separator* is inserted between bytes within each line while `vbCrLf` is inserted at the end of each complete *LineBreak*-sized group. -> [!NOTE] 1.0.0.4 +> [!NOTE] timeless > **ReturnHex** raises a run-time error if no resource has been loaded. Call [**Read**](Read) before calling **ReturnHex**. ### Example diff --git a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnPicture.md b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnPicture.md index 9ee303b98..0298497ee 100644 --- a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnPicture.md +++ b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnPicture.md @@ -13,7 +13,7 @@ Syntax: *object*.**ReturnPicture**() **As IPictureDisp** **ReturnPicture** passes the raw bytes stored by the preceding [**Read**](Read) call to **Global.LoadPicture**, the twinBASIC form of **LoadPicture** that accepts a byte array. **LoadPicture** interprets the bytes as an image and returns an **IPictureDisp** COM object. The returned picture can be assigned directly to the **Picture** property of any standard VB control that accepts one (such as **PictureBox** or **Image**). -> [!IMPORTANT] 1.0.0.4 +> [!IMPORTANT] timeless > [**Read**](Read) must be called before **ReturnPicture**. An instance on which **Read** has not been called holds no image data for **LoadPicture** to read. ### Example diff --git a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnString.md b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnString.md index 5fd23f14a..850534db9 100644 --- a/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnString.md +++ b/docs/Reference/Contributed/TBMANLIB_RES/cRes/ReturnString.md @@ -16,7 +16,7 @@ Syntax: *object*.**ReturnString** ( [ *Conversion* ] ) **As String** **ReturnString** passes the internal byte array held by the **cRes** instance to the **StrConv** function and returns the result. The byte array is the raw data loaded by a prior call to [**Read**](Read). -> [!NOTE] 1.0.0.4 +> [!NOTE] timeless > The right value for *Conversion* depends on the encoding of the resource data. For ANSI text, use the default **vbUnicode**. For UTF-8 text, pass **vbFromUTF8**. **vbFromUnicode** converts in the opposite direction (a **String** to the ANSI code page), so it does not suit a byte array. **ReturnString** has no option for UTF-16 LE text: assign the array from [**ReturnBytes**](ReturnBytes) to a **String** variable instead. A constant that does not match the encoding of the resource gives the wrong characters. ### Example diff --git a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md index a671ae48a..a9109429c 100644 --- a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md +++ b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Interval.md @@ -33,7 +33,7 @@ Syntax: *object*.**Interval** **=** *milliseconds* Changing **Interval** at run time takes effect immediately: the underlying Win32 timer (`SetTimer` / `KillTimer`) is stopped and conditionally restarted on every assignment. The timer starts only when both **Interval** is greater than `0` and [**Enabled**](Enabled) is **True**. -> [!NOTE] 1.0.7.0 +> [!NOTE] timeless > The Win32 `SetTimer` function raises an interval below 10 ms to 10 ms, and delivers its timer messages at the resolution of the system clock, typically 15--16 ms on standard Windows configurations. A short interval therefore does not give sub-15 ms accuracy. ### Example diff --git a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md index fbd51249d..88f0d7401 100644 --- a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md +++ b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/Timer.md @@ -22,7 +22,7 @@ The **Timer** event fires on every tick of the underlying Win32 timer (`SetTimer The argument is derived from the `dwTime` parameter of the Win32 timer callback (a **DWORD** tick count, wrapping at approximately 49.7 days). **TwinTimer** handles the 32-bit wrap-around internally and promotes the result to a **LongLong**, so *ElapsedTime* does not wrap during a session regardless of how long the timer has been running. -> [!NOTE] 1.0.7.0 +> [!NOTE] timeless > The event is delivered through the Win32 message pump of the thread that created the timer. If that thread's message pump is blocked when a tick is due, Windows delivers a single timer message when the pump resumes rather than one for each missed period. Long-running work inside the handler therefore lengthens the effective interval. ### Example diff --git a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md index 233bb0b5c..d830407cb 100644 --- a/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md +++ b/docs/Reference/Contributed/TwinTimerLib/TwinTimer/index.md @@ -61,7 +61,7 @@ Syntax: *object*.**Interval** [ = *milliseconds* ] *milliseconds* : A non-negative **Long**. A value of `0` stops the timer. A negative value raises run-time error 380 (Invalid property value). -> [!NOTE] 1.0.7.0 +> [!NOTE] timeless > The Win32 `SetTimer` function raises an interval below 10 ms to 10 ms, and delivers its timer messages at the resolution of the system clock, typically 15--16 ms on standard Windows configurations. A short interval therefore does not give sub-15 ms accuracy. ## Events diff --git a/docs/Reference/Contributed/VBComDlg/CommonDialog.md b/docs/Reference/Contributed/VBComDlg/CommonDialog.md index e8591ab6e..6510c1536 100644 --- a/docs/Reference/Contributed/VBComDlg/CommonDialog.md +++ b/docs/Reference/Contributed/VBComDlg/CommonDialog.md @@ -27,7 +27,7 @@ Setting [**CancelError**](#cancelerror) to **True** causes the class to raise ru Assigning a value outside the documented range or set of constants to a property raises run-time error 380 (*Invalid property value*). A failure that the sections below do not list raises an error whose number is the code the Windows common-dialog library reports and whose description is "Unexpected error." -> [!NOTE] 1.0.13.0 +> [!NOTE] timeless > Events such as [**InitDialog**](#initdialog), [**FileValidate**](#filevalidate), and [**ColorValidate**](#colorvalidate) require setting [**HookEvents**](#hookevents) to **True** before calling the `Show*` method. Without the hook, these events are not raised. The Folder Browser dialog always raises its events, and the Help, Find and Replace events do not use the hook. * TOC @@ -319,7 +319,7 @@ Syntax: *object*.**HookEvents** [ = *value* ] *value* : **Boolean**. When **True**, the dialog's hook procedure is installed, enabling the [**InitDialog**](#initdialog), [**FileValidate**](#filevalidate), [**FileShareViolation**](#fileshareviolation), [**ColorValidate**](#colorvalidate), and [**FontApply**](#fontapply) events. Default **False**. The Folder Browser dialog raises [**InitDialog**](#initdialog) and [**FolderBrowserValidateFailed**](#folderbrowservalidatefailed) whatever the value. -> [!IMPORTANT] 1.0.13.0 +> [!IMPORTANT] timeless > **HookEvents** must be set to **True** before calling the corresponding `Show*` method. Setting it after the call has no effect on the current invocation. ### InitDir @@ -562,7 +562,7 @@ Syntax: *object*.**ShowHelp** If [**HelpCommand**](#helpcommand) is 0, the method returns immediately without doing anything. On failure, error `CdlHelp` (32751) is raised. -> [!NOTE] 1.0.13.0 +> [!NOTE] timeless > WinHelp (`.hlp`) is a deprecated help format. This method calls the `WinHelpW` API, which fails on a system that has no WinHelp viewer. ### ShowOpen diff --git a/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md b/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md index 361fba85a..74af624cd 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md +++ b/docs/Reference/Contributed/WinReg/RegCls/DeleteKey.md @@ -19,7 +19,7 @@ Syntax: *object*.**DeleteKey** ( *RootKey*, *KeyPath* ) Returns **True** if the key was deleted successfully, or **False** if the call failed. -> [!IMPORTANT] 1.0.1.1 +> [!IMPORTANT] timeless > The underlying Win32 `RegDeleteKey` call succeeds only when the target key has no subkeys. If the key contains subkeys, the call returns **False**. Use [DeleteKeyEx](DeleteKeyEx) to remove a key together with all its subkeys and values. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md b/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md index 6f6b3e0ed..6a0225c41 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md +++ b/docs/Reference/Contributed/WinReg/RegCls/DeleteKeyEx.md @@ -27,7 +27,7 @@ This function is necessary because the Win32 `RegDeleteKey` API fails if the tar Both enumeration loops always request index 0 and rely on each deletion to expose the next entry. A subkey or value that cannot be deleted (for example, for lack of permission) therefore leaves the loop repeating on the same entry. -> [!IMPORTANT] 1.0.1.1 +> [!IMPORTANT] timeless > Deleting a registry key is irreversible. Verify that *RootKey* and *KeyPath* are correct before calling this function, as all subkeys and values under the specified path will be permanently removed. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md b/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md index da3bf608d..be167f787 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md +++ b/docs/Reference/Contributed/WinReg/RegCls/DeleteValue.md @@ -28,7 +28,7 @@ The function returns **True** when both the open and the delete operations retur Deleting a value entry is permanent. The value cannot be recovered through this API once removed. -> [!IMPORTANT] 1.0.1.1 +> [!IMPORTANT] timeless > The calling process must have write access to the target key. Standard user processes typically have write access under **HKEY_CURRENT_USER** but may require elevation for keys under **HKEY_LOCAL_MACHINE**. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md b/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md index 49f0dc192..bf8848fe4 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md +++ b/docs/Reference/Contributed/WinReg/RegCls/WriteDWORD.md @@ -33,7 +33,7 @@ If `RegCreateKeyEx` fails (for example, due to insufficient permissions), the fu The value is stored as a little-endian 32-bit integer, which is the standard **REG_DWORD** format on Windows. To read the value back, use [**ReadDWORD**](ReadDWORD). -> [!IMPORTANT] 1.0.1.1 +> [!IMPORTANT] timeless > Writing to keys under `HKEY_LOCAL_MACHINE` typically requires the process to be running with elevated (administrator) privileges. Attempting to write without the necessary rights causes `RegCreateKeyEx` to fail and **WriteDWORD** to return **False**. ### Example diff --git a/docs/Reference/Contributed/WinReg/RegCls/index.md b/docs/Reference/Contributed/WinReg/RegCls/index.md index f521c5d2e..20012cd1b 100644 --- a/docs/Reference/Contributed/WinReg/RegCls/index.md +++ b/docs/Reference/Contributed/WinReg/RegCls/index.md @@ -88,7 +88,7 @@ Syntax: *object*.**CreateKey** ( *RootKey*, *KeyPath* ) **As BOOL** Returns **True** on success, **False** if the key could not be created (typically due to insufficient privileges). -> [!IMPORTANT] 1.0.1.1 +> [!IMPORTANT] timeless > Creating keys under `HKEY_LOCAL_MACHINE` typically requires the application to run with administrator rights. ### DeleteKey @@ -121,7 +121,7 @@ Syntax: *object*.**DeleteKeyEx** ( *RootKey*, *KeyPath* ) **As BOOL** Returns **True** on success, **False** if the key could not be deleted. The method first enumerates and recursively deletes all sub-keys (calling itself for each one), then deletes all values in the key, closes the handle, and finally removes the key itself with `RegDeleteKey`. Both enumeration loops always request index 0, so each deletion must succeed for the loop to advance; a sub-key or value that cannot be deleted (for example, for lack of permission) leaves the loop repeating on the same entry. -> [!WARNING] 1.0.1.1 +> [!WARNING] timeless > This operation cannot be undone. The entire key tree is permanently removed from the registry. Double-check the *KeyPath* argument before calling. ### DeleteValue diff --git a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md index 8b0e3e6e5..5b41748de 100644 --- a/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md +++ b/docs/Reference/Contributed/tbFileSysTools/FileSystemTools.md @@ -110,7 +110,7 @@ DeleteFolder work A directory junction or symbolic link is a folder that stands for another folder. -> [!NOTE] 1.9.0.0 +> [!NOTE] timeless > [**CopyFolder**](#copyfolder), [**DeleteFolder**](#deletefolder), [**GetFilePaths**](#getfilepaths) with *recursive*, [**FindInTextFiles**](#findintextfiles) with *recursive* and [**Folder.Size**](Folder#size) do not enter a link: > > - **CopyFolder** leaves the link out of the copy. @@ -504,7 +504,7 @@ Errors: - 58 when a file exists and *overwrite* is **False**. -> [!NOTE] 1.9.0.0 +> [!NOTE] timeless > A directory junction or symbolic link inside the source is left out of the copy: the copy has no folder of that name, and nothing from the folder the link stands for. The Scripting Runtime copies the files of the target into the copy under the name of the link. See [Junctions and symbolic links](#junctions-and-symbolic-links). ### CreateFolder @@ -612,7 +612,7 @@ The contents are deleted first, and an item that cannot be deleted is skipped wi Errors: 76 when no folder matches or a file is named; 70 when the folder is not empty because something in it could not be deleted, such as a read-only file when *force* is **False**. The folder and that file stay. The Scripting Runtime raises error 70 in that case too. -> [!NOTE] 1.9.0.0 +> [!NOTE] timeless > A directory junction or symbolic link inside the folder is removed, and the folder it stands for keeps its files. The Scripting Runtime deletes the files in the target as well, so a tree that holds a link to a folder outside it loses the contents of that folder there. See [Junctions and symbolic links](#junctions-and-symbolic-links). ### GetFilePaths @@ -1458,7 +1458,7 @@ Syntax: **CreateTextFile** ( *filePath* [, *overwrite* [, *encoding* [, *lineEnd The file is written in place, with no swap file, and an existing read-only file raises error 70. The errors are those of [**OpenTextFile**](#opentextfile) for **ForWriting**, and error 52 for a name that has `<`, `>`, `"`, `|`, `?`, `*` or a control character, or is a reserved device name such as `con` (see [**CleanFileName**](#cleanfilename)). A colon in the name is allowed: it addresses an alternate data stream of an NTFS file. -> [!IMPORTANT] 1.9.0.0 +> [!IMPORTANT] timeless > The third argument of **Scripting.FileSystemObject.CreateTextFile** is a **Boolean** *Unicode*, and this procedure's third argument is an *encoding*. A **Boolean** converts to a number: **True** is -1, which is **encAutoDetect**, and on writing that means UTF-8, not UTF-16. **False** is 0, **encSystemAnsi**. Code ported from the Scripting Runtime that passes **True** needs **encUtf16** or **encUtf16Bom** here. ```tb check_build check_run @@ -1527,7 +1527,7 @@ Errors: 53 when the file does not exist for **ForReading**, or for the other mod > [!NOTE] 1.9.0.0 > A stream in UTF-7 holds the whole file in memory, as [**TextStream**](TextStream#reading-and-writing) describes. For **ForWriting** on a read-only file, the stream opens without an error and the file keeps its text; error 70 comes at the first [**Flush**](TextStream#flush) or [**Close**](TextStream#close). Every other encoding raises error 70 when the stream is opened. -> [!IMPORTANT] 1.9.0.0 +> [!IMPORTANT] timeless > The fourth argument of **Scripting.FileSystemObject.OpenTextFile** is a **Tristate** *Format*, and this procedure's is an *encoding*. **TristateTrue** is -1, which is **encAutoDetect**: on writing that means UTF-8, not UTF-16. The default also differs: a file opened for writing here is UTF-8 without a BOM, where the Scripting Runtime writes ANSI. This sample appends to a file that has a BOM and LF line endings. The new line gets the line ending of the file, and no second BOM is written: diff --git a/docs/Reference/Contributed/tbProjPacker/index.md b/docs/Reference/Contributed/tbProjPacker/index.md index a01757caa..7f7c72454 100644 --- a/docs/Reference/Contributed/tbProjPacker/index.md +++ b/docs/Reference/Contributed/tbProjPacker/index.md @@ -36,7 +36,7 @@ Else End If ``` -> [!NOTE] 1.1.0.0 +> [!NOTE] timeless > The package reads and writes the binary [file format](../../../../Features/Packages/File-Format) itself, as the Import/export tool's script does, and the format is an internal detail of the IDE that can change. [**Test**](ProjPacker/Test) with a project file as its argument parses that file, writes it again and compares the result, so it shows whether the package still understands the files of a given twinBASIC build. ## Output and errors diff --git a/docs/Reference/Contributed/tsJson/Json.md b/docs/Reference/Contributed/tsJson/Json.md index e7f75519f..8e53a2f75 100644 --- a/docs/Reference/Contributed/tsJson/Json.md +++ b/docs/Reference/Contributed/tsJson/Json.md @@ -24,7 +24,7 @@ The module has no state and no settings. Options for one call are a [**JsonOptio **Errors.** The functions of the module do not raise run-time errors for bad data or bad arguments. A function that fails returns an empty result (**Empty**, **Nothing**, an empty string, **False** or an unbound array) and sets the last error, which the predeclared object [**JsonError**](JsonError) reports. A function that can fail clears the last error when it starts, so the last error always belongs to the latest such call. The members that cannot fail neither clear nor set it: [**TypeOfValue**](#typeofvalue), [**IsObject**](#isobject), [**IsArray**](#isarray), [**Equals**](#equals), [**PointerEscape**](#pointerescape), [**PointerUnescape**](#pointerunescape), [**IsValidPattern**](#isvalidpattern), [**IsMatchPattern**](#ismatchpattern), [**IsSearchPattern**](#issearchpattern) and [**FormatIso8601**](#formatiso8601). The origin of an error is the name of the function that was called, such as `Json.Parse`. The functions [**TryParse**](#tryparse), [**TryParseFile**](#tryparsefile), [**TryParseLines**](#tryparselines) and [**TryParseLinesFile**](#tryparselinesfile) report the origin of the function they call: an error of **TryParse** has the origin `Json.Parse`. -> [!NOTE] 1.0.15.0 +> [!NOTE] timeless > A function that finds nothing is not an error when its name says so (**PointerExists**, **PointerTryGet**, the `Try` functions and the JSONPath queries). The `Try` functions return **False**, and a JSONPath query that matches nothing returns an empty result with no error. Check [**JsonError.HasError**](JsonError#haserror), not **IsEmpty**, after a function that can return a JSON null: [**Parse**](#parse) returns **Null** for the text `null`, and **Empty** only for a failure. * TOC diff --git a/docs/Reference/Contributed/tsJson/JsonOptions.md b/docs/Reference/Contributed/tsJson/JsonOptions.md index fd2cf0cc0..6eb859cb1 100644 --- a/docs/Reference/Contributed/tsJson/JsonOptions.md +++ b/docs/Reference/Contributed/tsJson/JsonOptions.md @@ -22,7 +22,7 @@ Debug.Print JsonOptions.Rfc.Indent ' 4 A property of an options object checks nothing when it is set. The value is used by the call that receives the object, and a call does not change the object. -> [!IMPORTANT] 1.0.15.0 +> [!IMPORTANT] timeless > Assigning a property on the predeclared object itself raises run-time error 5, *Invalid procedure call or argument*, with the source `JsonOptions` and the message *JsonOptions (default instance) is a factory. Use Rfc, Jsonc, Minified, or Pretty.* Create an options object first and assign the properties of that. > > ```tb check_build check_run diff --git a/docs/Reference/Contributed/tsPwGen/PwGenOptions.md b/docs/Reference/Contributed/tsPwGen/PwGenOptions.md index 6c33c3692..81b118945 100644 --- a/docs/Reference/Contributed/tsPwGen/PwGenOptions.md +++ b/docs/Reference/Contributed/tsPwGen/PwGenOptions.md @@ -24,7 +24,7 @@ Debug.Print Left$(pw, 1) Like "[A-Za-z]" ' True A property of a recipe checks nothing when it is set. A value that cannot be used, such as a **Length** of 0, is found when the recipe is passed to **Generate** or **Entropy**, and is reported there as an error code. -> [!IMPORTANT] 1.0.8.0 +> [!IMPORTANT] timeless > Assigning a property on the predeclared object itself raises run-time error 5, *Invalid procedure call or argument*, with the source `PwGenOptions` and the message *PwGenOptions (default instance) is a factory. Use Alphanumeric, Complex, Passphrase, Pin, or Hex.* Create a recipe first and assign the properties of that. > > ```tb check_build check_run