diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d235220b..60ba2e02 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,22 +1,33 @@ name: Run Checks on: - workflow_dispatch: -# push: -# branches: [ main, dev ] -# paths: docs/** - pull_request: - branches: [ main ] - paths: docs/** + workflow_dispatch: + # `staging` is this repo's default branch and the one the deploy + # workflow publishes from, so that is what PRs target. Checking only + # PRs into `main` meant this never ran. + # + # Pushes are deliberately not listed: tbdocs-gh-pages.yml already + # builds and checks on every push to `staging` before it deploys, so a + # push trigger here would duplicate that work. This workflow's job is + # to catch the breakage BEFORE the merge. + pull_request: + branches: [ staging, main ] +# No `paths:` filter. It used to be `docs/**`, which skipped any PR that +# touched only builder/ or scripts/ -- exactly the code most able to break +# the build, the link checker, or asset vendoring. The whole run is a couple +# of minutes, so filtering buys little and silently drops real coverage. + +# Check-only job: it never deploys, so it needs no Pages or OIDC rights. permissions: contents: read - pages: write - id-token: write +# Must NOT share the deploy workflow's "pages" group -- that made PR checks +# queue against production deployments. Keyed per PR, and superseded runs are +# cancelled when new commits arrive. concurrency: - group: "pages" - cancel-in-progress: false + group: checks-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: # Build job @@ -33,38 +44,110 @@ jobs: cache-dependency-path: package-lock.json - name: Install dependencies run: npm ci - - name: Build with tbdocs - run: node builder/tbdocs.mjs --src docs - - name: Check links (check_links.mjs) - # Three passes run in parallel via /sep/: - # 1. Online (_site/): --fallback-extensions html mirrors GitHub Pages' - # extensionless-URL behaviour. No --base-path needed -- this - # workflow builds without --baseurl (contrast jekyll-gh-pages.yml). - # Integrity checks (--check-html/a11y/ids/sitemap/search) run here. - # 2. Offline (_site-offline/): --forbid catches any surviving - # https://docs.twinbasic.com/ link the offline rewrite missed - # (bare root URL is exempt). Integrity checks run here too (minus - # sitemap/search -- the offline tree has neither). - # 3. Book (_site-pdf/book.html): --no-fail makes failures informational - # (some links are not yet fully resolved). - run: >- - node scripts/check_links.mjs - --offline --include-fragments - --check-html --check-a11y --check-ids - --check-sitemap --check-search --check-canonical - --fallback-extensions html - --index-files 'index.html,.' - --root-dir docs/_site - docs/_site - /sep/ - --offline --include-fragments - --check-html --check-a11y --check-ids - --index-files 'index.html,.' - --fallback-extensions html - --forbid 'https://docs.twinbasic.com' - --root-dir docs/_site-offline - docs/_site-offline - /sep/ - --offline --no-fail --include-fragments - --root-dir docs/_site-pdf - docs/_site-pdf/book.html + # check_a11y.mjs drives axe-core inside headless Chromium. The + # build itself needs no browser -- only this scan does. + # fonts-liberation is installed explicitly, not incidentally. axe's + # target-size rule measures rendered boxes, and an inline element's + # measured height is its font's content area -- so the 24px floor is + # cleared or missed depending on what system-ui resolves to. At the + # mobile h3 size: Segoe UI 19px, Verdana and Tahoma 17px, Arial 16px, + # Liberation Sans / DejaVu Sans / Roboto 15px. The site's padding is + # calibrated against the smallest of those, so the runner has to have + # it. ubuntu-latest migrates to Ubuntu 26 on 19 October 2026; pinning + # the image would also work and expires differently, while this + # addresses the actual variable. + - name: Install Liberation fonts + run: sudo apt-get install -y --no-install-recommends fonts-liberation + - name: Install Chromium + # Same incantation as tbdocs-gh-pages.yml's install step, which + # is the proven-working one in this repo: --install-deps apt-installs + # the shared libraries Chromium needs, which requires root. + run: sudo npx puppeteer browsers install chrome --install-deps + - name: Build and check links with tbdocs + # --no-fetch-assets: CI must never download a referenced image. An + # author who wrote the markdown but forgot to commit the file would + # otherwise get a green build while the published site went on + # hotlinking a third party. A missing asset is a hard failure here, + # naming the file to commit. ($CI already implies this; the flag + # states it.) See builder/vendor-assets.mjs. + # + # --check runs the link and site-integrity check over the HTML the + # build already holds in worker memory, instead of writing three + # trees out and reading ~270 MB of them back through + # scripts/check_links.mjs. Same findings -- that equivalence is + # gated by scripts/check_links_diff.mjs, run by hand when either + # implementation changes. Do NOT also add a check_links.mjs pass + # over _site/: it would check the same bytes a second time. + # + # --check-audit-index adds the index audit on top: a diff of the + # tree index the build derives from its own records against what + # landed on disk. It is the one direction the findings comparison + # cannot see -- a spurious entry makes the oracle answer "exists" + # for a path that 404s in production, and on a clean site nothing + # links to a path that does not exist, so nothing else would + # notice. CI lost its previous coverage of this when the FsOracle + # pass was removed. + # + # It covers all three trees: _site/ (links, fragments, duplicate + # ids, well-formedness, remote , sitemap, search index, + # canonical URLs), _site-offline/ (the same minus sitemap and + # search, plus the forbidden-prefix rule that catches live-site + # links the offline rewrite missed), and _site-pdf/book.html + # (informational). A failing check never aborts the build -- a + # broken link still produces a site worth inspecting -- so the + # step fails on the exit code: 1 for link failures, 2 for + # integrity failures, 3 for both. + run: node builder/tbdocs.mjs --src docs --no-fetch-assets --check-audit-index + # The build above is the only pass over the site's HTML. This step is + # NOT a second one: it runs the differential harness against a + # nine-file synthetic tree that carries one fault of every kind, so + # scripts/check_links.mjs -- still the tool for a tree the build did + # not produce, and the oracle the fused path is defined against -- + # cannot rot unnoticed. ~0.3 s, no site files touched. + # + # The full script-vs-fused comparison over the real trees stays a + # manual gate (`check_links_diff.mjs --a script --b fused`): running + # it here would mean checking every page twice, which is exactly what + # folding the check into the build removed. + - name: Verify the standalone link checker (check_links_diff.mjs) + run: node scripts/check_links_diff.mjs --case fixture --a script --b index + # The step above compares two front ends over a hand-written tree. + # This one compares the script against the BUILD's own checker, over + # a three-page tree the build produces from test/fixtures/check-src. + # That is the pass the harness exists for and the one nothing + # exercised: `--b fused` skipped the synthetic `fixture` case every + # time, because the build cannot check a tree it did not write. A + # regression in builder/check.mjs that stopped REPORTING a category + # would have left every gate green. + # + # One extra three-page build, ~1 s. It is here and not in the deploy + # workflow because this is the PR gate: catching it before a merge is + # the point, and the deploy workflow has a site to ship. + - name: Verify the build's own link checker (check_links_diff.mjs) + run: node scripts/check_links_diff.mjs --case fixture-built --case fixture-built-offline --a script --b fused + # check_a11y.mjs injects a PATCHED axe bundle (plain-color-fields, + # -26 % on a realistic page set -- see builder/PLAN-axe-perf.md), so the + # patch has to be verified before its results are trusted. The patch + # asserts its substitution targets and so fails loudly if an axe-core + # bump moves the code; this catches the other case, where the text still + # matches but the colour maths has changed. The fingerprint gate cannot + # see that -- it compares `incomplete` as a rule-id set. + # + # Cheap (one page, two bundles) and Chromium is already installed. The + # PR that bumps axe-core is exactly when it earns its place. + - name: Verify axe source patch (check_axe_patch_equiv.mjs) + run: node scripts/check_axe_patch_equiv.mjs + # The scan is eleven pages of ~1,160, so its page list decides what it can + # report at all. This fails when the site grows a markup construct no + # sample page carries -- the drift that let the previous six-page sample + # report a clean pass while 54 pages had violations in constructs it never + # saw. No browser, ~1 s. + - name: Verify a11y sample coverage (pick_a11y_sample.mjs) + run: node scripts/pick_a11y_sample.mjs --check + # Matches check.bat: the build's own link check gates this, and a + # link failure short-circuits before the (slower) browser scan runs. + # Scans _site-offline/, whose relative asset paths actually resolve -- + # _site/ uses root-absolute URLs that render unstyled here, making + # every colour-contrast result meaningless. + - name: Accessibility check (check_a11y.mjs) + run: node scripts/check_a11y.mjs diff --git a/.github/workflows/tbdocs-gh-pages.yml b/.github/workflows/tbdocs-gh-pages.yml index 9ffbc139..fe7b9cf7 100644 --- a/.github/workflows/tbdocs-gh-pages.yml +++ b/.github/workflows/tbdocs-gh-pages.yml @@ -41,53 +41,79 @@ jobs: cache: 'npm' cache-dependency-path: package-lock.json - name: Install dependencies and Chromium + # fonts-liberation is installed explicitly, not incidentally. + # axe's target-size rule measures rendered boxes, and an inline + # element's measured height is its font's content area -- so the + # 24px floor is cleared or missed depending on what system-ui + # resolves to. At the mobile h3 size: Segoe UI 19px, Verdana and + # Tahoma 17px, Arial 16px, Liberation Sans / DejaVu Sans / Roboto + # 15px. The site's padding is calibrated against the smallest of + # those, so the runner has to have it. ubuntu-latest migrates to + # Ubuntu 26 on 19 October 2026; pinning the image would also work + # and expires differently, while this addresses the actual + # variable. run: | npm ci + sudo apt-get install -y --no-install-recommends fonts-liberation sudo npx puppeteer browsers install chrome --install-deps - name: Setup Pages id: pages uses: actions/configure-pages@v6 - - name: Build with tbdocs + - name: Build and check links with tbdocs # --url + --baseurl together make canonical / og:url / sitemap # entries point at the actual deployment (e.g. on a fork that # deploys to kubao.github.io/twinBASIC-docs/, both bits move # so the page advertises its own URL rather than the config'd # production host). - run: node builder/tbdocs.mjs --src docs --url '${{ steps.pages.outputs.origin }}' --baseurl '${{ steps.pages.outputs.base_path }}' - - name: Check links and site integrity - # Three passes run in parallel via /sep/: - # 1. Online (_site/): --fallback-extensions html mirrors GitHub Pages' - # extensionless-URL behaviour. --base-path strips the Pages baseurl - # (e.g. `/twinBASIC-docs`) from absolute URLs before resolving. - # Integrity checks (--check-html/a11y/ids/sitemap/search) run here. - # 2. Offline (_site-offline/): --forbid catches any surviving - # https://docs.twinbasic.com/ link the offline rewrite missed - # (bare root URL is exempt). Integrity checks run here too (minus - # sitemap/search -- the offline tree has neither). - # 3. Book (_site-pdf/book.html): --no-fail makes failures informational - # (some links are not yet fully resolved). - run: >- - node scripts/check_links.mjs - --offline --include-fragments - --check-html --check-a11y --check-ids - --check-sitemap --check-search --check-canonical - --fallback-extensions html - --index-files 'index.html,.' - --base-path '${{ steps.pages.outputs.base_path }}' - --root-dir docs/_site - docs/_site - /sep/ - --offline --include-fragments - --check-html --check-a11y --check-ids - --forbid 'https://docs.twinbasic.com' - --fallback-extensions html - --index-files 'index.html,.' - --root-dir docs/_site-offline - docs/_site-offline - /sep/ - --offline --no-fail --include-fragments - --root-dir docs/_site-pdf - docs/_site-pdf/book.html + # --no-fetch-assets: CI must never download a referenced image. An + # author who wrote the markdown but forgot to commit the file would + # otherwise get a green build while the published site went on + # hotlinking a third party. A missing asset is a hard failure here, + # naming the file to commit. ($CI already implies this; the flag + # states it.) See builder/vendor-assets.mjs. + # + # --check runs the link and site-integrity check over the HTML the + # build already holds in worker memory, across all three trees, + # instead of writing them out and reading ~270 MB back through + # scripts/check_links.mjs. Do NOT also add a check_links.mjs pass: + # it would check the same bytes a second time. + # + # --check-audit-index adds the index audit on top: a diff of the + # tree index the build derives from its own records against what + # landed on disk. It is the one direction the findings comparison + # cannot see -- a spurious entry makes the oracle answer "exists" + # for a path that 404s in production, and on a clean site nothing + # links to a path that does not exist, so nothing else would + # notice. CI lost its previous coverage of this when the FsOracle + # pass was removed. + # + # It also removes a synchronisation point that used to be easy to + # get wrong. The standalone pass needed --base-path to match this + # step's --baseurl, in a different step, by hand; the fused check + # reads the baseurl off the config it just built with. Only the + # online tree gets a base path -- the offline tree's links are all + # relative after the rewrite. + run: node builder/tbdocs.mjs --src docs --url '${{ steps.pages.outputs.origin }}' --baseurl '${{ steps.pages.outputs.base_path }}' --no-fetch-assets --check-audit-index + # Not a second pass over the site: a nine-file synthetic tree carrying + # one fault of every kind, so scripts/check_links.mjs -- still the + # tool for a tree the build did not produce -- cannot rot unnoticed. + # ~0.3 s. See the same step in checks.yml, which additionally runs the + # comparison against the build's own checker. + - name: Verify the standalone link checker (check_links_diff.mjs) + run: node scripts/check_links_diff.mjs --case fixture --a script --b index + # check_a11y.mjs injects a PATCHED axe bundle; verify the patch is still + # value-preserving before trusting what it reports. See the same step in + # checks.yml for why the fingerprint gate does not cover this. + - name: Verify axe source patch (check_axe_patch_equiv.mjs) + run: node scripts/check_axe_patch_equiv.mjs + # Fails when the site grows a construct no sample page covers; see the + # same step in checks.yml. No browser, ~1 s. + - name: Verify a11y sample coverage (pick_a11y_sample.mjs) + run: node scripts/pick_a11y_sample.mjs --check + # Same gate as check.bat and the PR workflow. Chromium is already + # installed above for the PDF render, so this costs only the scan. + - name: Accessibility check (check_a11y.mjs) + run: node scripts/check_a11y.mjs - name: Render book PDF run: | mkdir -p _pdf diff --git a/.gitignore b/.gitignore index 270d0bee..9d3ebfe4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,12 @@ node_modules/ /after-*/ /findoverflow-baseline/ indexer/.packages/ + +# Temporary session handoff notes. Root-anchored: only the repo-root file. +/HANDOFF.md + +# scripts/check_links_diff.mjs builds test/fixtures/check-src into these. +/test/fixtures/_out/ +/test/fixtures/_out-offline/ +/test/fixtures/_out-pdf/ +/test/fixtures/_out.json diff --git a/WIP.md b/WIP.md index 170dacf8..2b312eb9 100644 --- a/WIP.md +++ b/WIP.md @@ -41,6 +41,7 @@ The rest of this file is the maintenance guide for updating existing pages or ad - `docs/Reference/Statements.md` — alphabetical index of language statements. - `docs/Reference/Procedures and Functions.md` — alphabetical index of procedures/functions. - Footer rendering — [builder/template.mjs](builder/template.mjs)'s `renderFooterCustom()` renders the copyright line and, when `vba_attribution: true` is set in a page's frontmatter, an additional CC-BY-4.0 attribution line beneath it. +- Contributor authoring guide — [docs/Documentation/Authoring.md](docs/Documentation/Authoring.md) is the public "start here" page that distils this file's authoring conventions (page template, heading levels, formatting, plain-English prose, attribution policy, cross-section linking) for a new contributor. This file remains the exhaustive maintainer source of truth; keep the two in sync when a convention changes. ## Package API notes @@ -102,7 +103,7 @@ Syntax: **** [ *args* ] -### Example +## Example This example... @@ -110,13 +111,14 @@ This example... ' code ``` -### See Also +## See Also - [Other](OtherSymbol) ```` Formatting conventions: +- Heading levels: the page title is `#` (a *chapter* -- a page may legitimately have more than one). Top-level sections (Example, See Also, ...) are `##`; subsections `###`. Do **not** skip `##` with the old `# Title` -> `### Example` "house style". That pattern (h1 straight to h3) exists on many older pages only to keep GitHub's raw-markdown view at a modest heading size; it is a heading-order defect on the built site. `headingLevelNormalizePlugin` in [builder/render.mjs](builder/render.mjs) repairs those legacy pages at build time (it raises `h3`->`h2` on any page that uses h1 and h3 but no h2), so existing pages are not being churned all at once -- but new content must use correct levels, and pages that already mix `##` and `###` are left untouched. A mixed page that skips a level is a real defect the normalizer will not save you from: five were found and fixed this way, and axe's `heading-order` rule now guards the sample against more. - `**...**` for keywords/literal tokens; `*...*` for placeholders/arguments. - Code blocks use ` ```tb ` (highlighted via Shiki using the vendored `builder/twinbasic.tmLanguage.json` grammar). - Parameter lists use the deflist `term` + `: definition` indentation pattern (NOT the MS-style markdown table). @@ -351,6 +353,7 @@ The vocabulary tables further down cover word choice. The rules in this subsecti | `catches up` | resumes, processes the queue | | `comes up` (a connection) | is established, becomes ready | | `drive` / `driven` (figurative) | controlled by, determined by, powered by | +| `footgun` / `footguns` | easy mistake to make, hazard, pitfall | | `for free` (figurative) | as a side effect, without extra effort | | `hand off` / `hand over` / `hand back` | returns, passes, delivers | | `hand-rolled` | manually constructed, custom-built | @@ -462,13 +465,17 @@ The site builds via [builder/](builder/), a custom Node.js static site generator A task-graph scheduler / parallelisation pass is designed in [builder/PLAN-scheduler.md](builder/PLAN-scheduler.md) and has been implemented (Phases 0--4). +**Before adding a fan-out to the task graph, read [why a dep count of zero does not mean the submits have run](builder/PLAN-sab-pull-scheduler.md#a-dep-count-of-zero-does-not-mean-the-submits-have-run).** A worker posts its result and *then* decrements its successors' dependency counts in shared memory, so a barrier's count can reach zero while results are still queued and the `submit()` calls that merge them into build state have not run --- the shared counter orders the work, not the state. A dynamic barrier must therefore list every chunk task in its `expected`, even when its own `execute()` ignores the inputs; that list is the only thing the scheduler checks before it lets the barrier proceed. `renderJoin` went without it and silently dropped ~6 pages from `search-data.json` on about one build in three, because the index is built by flattening a `new Array(N)` and `Array.prototype.flat()` skips holes without reporting anything. Two silent failures combining into one invisible one. Both halves are fixed, and every skip on the chunk-merge path that used to tolerate a missing piece now refuses to continue --- see [where the completeness checks are](builder/PLAN-sab-pull-scheduler.md#where-the-completeness-checks-are). Keep it that way: on this path, "the piece is missing" is a bug, not a case to handle. + +Folding `check.bat`'s gates into that same graph is designed in [builder/PLAN-checks.md](builder/PLAN-checks.md). Phase A, the link checker, is **implemented**: extraction runs inside `flush`, where both trees' final HTML is already in worker memory, so the build no longer writes ~270 MB out only to read it back and re-parse it. The `pick_a11y_sample.mjs --check` census and the axe scan's orchestration are follow-ons, seeded with measurements and open questions but not yet designed. + Historical engineering notes from the Jekyll era --- the original build pipeline, the HTML-compress plugin, the per-phase optimisation passes that preceded the JS port, the migration notes, and the Phase 11 parity-update retrospective --- live in [WIP.OldJekyll.md](WIP.OldJekyll.md). ## Build / preview -- `build.bat` — runs `node builder\tbdocs.mjs --src docs` which 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. +- `build.bat` — runs `node builder\tbdocs.mjs --src docs --check` which 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. - `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` — link + integrity check (offline `scripts/check_links.mjs` against `_site/` and `_site-offline/`; the offline pass also runs `--forbid 'https://docs.twinbasic.com'` to catch surviving live-site links). +- `check.bat` — the gates that need a browser or a second pass over the built tree: a freshness check that refuses a stale tree (`scripts/check_tree_fresh.mjs`), the axe source-patch verification (`scripts/check_axe_patch_equiv.mjs`), the a11y sample-coverage check (`scripts/pick_a11y_sample.mjs --check`), then the accessibility check (`scripts/check_a11y.mjs`). The link + integrity check moved into `build.bat`. - `book.bat` — renders the PDF from `docs\_site-pdf\book.html` via `node book\render-book.mjs` into `docs\_pdf\book.pdf`. Run `build.bat` first to populate `_site-pdf/`. @@ -480,10 +487,123 @@ After a batch of changes, verify the site builds clean and all links resolve: build.bat && check.bat ``` -`check.bat` runs [scripts/check_links.mjs](scripts/check_links.mjs) in offline mode against both `_site/` and `_site-offline/` — it catches broken intra-site links, missing pages, malformed `redirect_from` entries (the most common breakage when adding new pages or moving content between sections), and (via `--forbid 'https://docs.twinbasic.com'` on the offline pass) any extracted link that still points at the live docs site after the offlinify rewrite. A clean run is the bar for "ready to commit". +On the dev box that is ~4 s of build against ~23 s of check, of which the axe scan is ~20 s. [builder/PLAN-checks.md](builder/PLAN-checks.md) records how the link checker got folded into the build's task graph, what it cost and what it saved; the axe follow-ons are designed there but not implemented. + +**The link and integrity check runs inside the build.** `build.bat` passes `--check-audit-index`, which implies `--check`, and the check walks the HTML on the worker lanes that produced it -- both trees' final strings are already decoded and in memory at `flush()`, so the ~270 MB the two trees weigh is never written out only to be read back. It also audits the tree index the build derives from its own records against what landed on disk -- the one direction the two-checker comparison structurally cannot see, since a spurious entry makes the oracle answer "exists" for a path that 404s in production. It catches broken intra-site links, missing pages, malformed `redirect_from` entries (the most common breakage when adding new pages or moving content between sections), duplicate ids, remote ``, badly nested tags, sitemap and search-index gaps, canonical mismatches, and (via a forbidden-prefix rule on the offline tree) any extracted link that still points at the live docs site after the offlinify rewrite. A clean `build.bat && check.bat` is the bar for "ready to commit". + +A failing check never aborts the build: a broken link still produces a site you want on disk to inspect. It sets the exit code instead, using the same scheme `check_links.mjs` has always used -- 1 for link failures, 2 for integrity failures, 3 for both -- so CI can tell them apart. + +The remote-asset rule fails the run on any `` resolving off-box (`http://`, `https://`, or protocol-relative `//host`). In the build it is unconditional -- `checkRemoteAssets: true` on both trees in `builder/check.mjs`'s `TREES` -- and is *not* reachable by a flag: `tbdocs` rejects `--check-remote-assets` as an unknown argument. That name belongs to the standalone `scripts/check_links.mjs`, where it is opt-in. The PDF pass over `book.html` is informational, so enforcement comes from the `_site/` pass -- every page in the book is also in `_site/`, making it a superset. The check is deliberately scoped to `` only; ` +[Access DevCon 2026 - Practical twinBASIC](https://www.youtube.com/watch?v=qUwTszCFQHA){: .video } For the slide deck and project files see: https://nolongerset.com/devcon-2026/ @@ -25,11 +21,7 @@ To learn more about the conference: [https://www.donkarl.com/devcon][2]. 26 Apr 2025 - +[Access DevCon 2025 - twinBASIC Update](https://www.youtube.com/watch?v=vLmy1ZY-IT4){: .video } Access MVP Mike Wolfe presents a twinBASIC project update. - For the slide deck and more information see: [https://nolongerset.com/devcon-2025][1]. @@ -45,11 +37,7 @@ Add-In creation with twinBASIC 29 Apr 2024 - +[Access DevCon 2024 - twinBASIC](https://www.youtube.com/watch?v=7IfAkOOWSao){: .video } Mike Wolfe presents a twinBASIC project update and how to create add-ins for Access with twinBASIC. - For more information see: [https://nolongerset.com/tag/twinbasic-weekly-update/][3]. @@ -62,11 +50,7 @@ Mike Wolfe presents a twinBASIC project update and how to create add-ins for Acc 8 May 2023 - +[Access DevCon 2023 - twinBASIC Update](https://www.youtube.com/watch?v=Vq4ZFkeZyiA){: .video } Mike Wolfe presents a session on twinBASIC covering a brief project overview, progress, roadmap, demos and Access integration plans. - For more information see [https://nolongerset.com/tag/twinbasic][5]. @@ -79,11 +63,7 @@ Mike Wolfe presents a session on twinBASIC covering a brief project overview, pr 12 May 2022 - +[Access DevCon 2022 - twinBASIC Update](https://www.youtube.com/watch?v=y4DRAcdH9tg){: .video } Mike Wolfe presents the current state of twinBASIC focussing on the practical use and usefulness for Access developers. - For more information, see [https://nolongerset.com/tag/twinbasic][5] @@ -94,10 +74,6 @@ Mike Wolfe presents the current state of twinBASIC focussing on the practical us 12 May 2021 - +[Access DevCon 2021 - twinBasic](https://www.youtube.com/watch?v=dBvMBAUSyT8){: .video } Mike Wolfe presents: The world premier of twinBasic, a new flavour of VB(A). diff --git a/docs/Videos/twinBASIC.md b/docs/Videos/twinBASIC.md index 198d9361..67914476 100644 --- a/docs/Videos/twinBASIC.md +++ b/docs/Videos/twinBASIC.md @@ -10,11 +10,7 @@ permalink: /Videos/tB 22 Jul 2024 - +[twinBASIC For Applications (Proof Of Concept)](https://www.youtube.com/watch?v=LCxfObwA7E8){: .video } Introducing "twinBASIC for applications" -- the game-changing drop-in replacement for VBA (and VBA SDK). See our proof of concept inside MS ACCESS in action! 🚀👨‍💻👩‍💻 @@ -30,11 +26,7 @@ For more info: [https://www.reddit.com/r/vba/comments/1dg0lse/the_next_evolution 3 Oct 2021 - +[twinBASIC: CustomControls and form designer sneek peek!](https://www.youtube.com/watch?v=nC7r4JdwB_A){: .video } A quick look at the twinBASIC form designer coming to twinBASIC very soon. We look at the start of a grid-based CustomControl (written in twinBASIC), and also look at many of the form designer features in action. @@ -44,11 +36,7 @@ A quick look at the twinBASIC form designer coming to twinBASIC very soon. We 29 Jun 2021 - +[twinBASIC Preview - Quick Setup Guide (UPDATED JUNE 2021)](https://www.youtube.com/watch?v=ZzpyZWiCnzo){: .video } twinBASIC Preview 1 is out now on the VS Code marketplace. This setup guide will show you how to very quickly install it and start using it. Enjoy! @@ -58,11 +46,7 @@ twinBASIC Preview 1 is out now on the VS Code marketplace. This setup guide wil 10 Apr 2021 - +[twinBASIC Preview - Quick Setup Guide (OLD VERSION)](https://www.youtube.com/watch?v=ztBWwI3Qhp4){: .video } > [!WARNING] > This video is out of date @@ -78,11 +62,7 @@ twinBASIC Preview 1 is out now on the VS Code marketplace. This setup guide wil 29 Jan 2021 - +[twinBASIC - New Compiler Features (part 3)](https://www.youtube.com/watch?v=nnXx9q35rIk){: .video } Today we're looking at some more compiler features; DeclareWide support (for bypassing ANSI DLL conversions), parameterized constructors and method overloading. @@ -92,11 +72,7 @@ Today we're looking at some more compiler features; DeclareWide support (for by 14 Jan 2021 - +[twinBASIC - New Compiler Features (part 2)](https://www.youtube.com/watch?v=vSSNYa_xYmo){: .video } Today we're looking at some more compiler features, in particular: CurrentComponentName, CurrentProcedureName, RETURN syntax, IsNot operator, plus the IDE feature of 'inline parameter hints'. What features are you most looking forward to? @@ -106,11 +82,7 @@ Today we're looking at some more compiler features, in particular: CurrentCompon 30 Dec 2020 - +[twinBASIC - Remote debugging applications (sneak peek)](https://www.youtube.com/watch?v=c6mxgl9d_SQ){: .video } A little teaser video that demonstrates how easy it is to edit and debug twinBASIC projects remotely... even from different platforms like Linux! @@ -120,11 +92,7 @@ A little teaser video that demonstrates how easy it is to edit and debug twinBAS 22 Dec 2020 - +[twinBASIC - New Compiler Features (part 1)](https://www.youtube.com/watch?v=-mBxtFQxLNs){: .video } Today we're looking at some of the new compiler features offered by twinBASIC. We look at unicode support, 64-bit support, new operators, new datatypes, assignment operators, short-circuiting operators. @@ -134,11 +102,7 @@ Today we're looking at some of the new compiler features offered by twinBASIC. W 20 Dec 2020 - +[twinBASIC - Debugging in action (32-bit and 64-bit)](https://www.youtube.com/watch?v=GnDq3z6GwCg){: .video } Today we show off some of the debugging features available in twinBASIC. We feature the debug console, breakpoints, error breaking, live call stack, variables info panel, and the watch window. We also touch on 64-bit support, showing how to switch seamlessly between them. @@ -148,10 +112,6 @@ Today we show off some of the debugging features available in twinBASIC. We feat 18 Dec 2020 - +[twinBASIC - Introduction](https://www.youtube.com/watch?v=havi3Dv4saY){: .video } An introduction to twinBASIC, a new BASIC compiler that expands and improves upon VB6 and VBA code whilst giving 100% backwards compatibility with existing code. diff --git a/docs/_sass/custom/_theme.scss b/docs/_sass/custom/_theme.scss new file mode 100644 index 00000000..340fd528 --- /dev/null +++ b/docs/_sass/custom/_theme.scss @@ -0,0 +1,27 @@ +// Dark-theme scoping mixin (Phase 5 theme toggle). +// +// Emits @content under both dark contexts so dark mode works with NO JS: +// 1. system preference (prefers-color-scheme: dark), unless the visitor has +// forced light via the toggle -- the :not([data-theme="light"]) escape hatch; +// 2. an explicit [data-theme="dark"] override, which wins even on a light OS. +// +// Both selectors are (0,1,1) specificity, so the explicit block is emitted last +// and wins on a dark OS when a choice is set. Selectors inside @content are +// relative to the root: `.foo {}` -> `html[data-theme="dark"] .foo`, +// `&.bar {}` -> the compound `html[data-theme="dark"].bar`. +// +// The dark declarations are duplicated across the media / attribute boundary +// (identical text, so gzip/brotli compress it away). The clean single-source +// alternative -- a CSS-custom-properties token layer -- is tracked in +// builder/FUTURE-WORK.md (B19). +@mixin dark-theme { + @media (prefers-color-scheme: dark) { + html:not([data-theme="light"]) { + @content; + } + } + + html[data-theme="dark"] { + @content; + } +} diff --git a/docs/_sass/custom/admonitions.scss b/docs/_sass/custom/admonitions.scss index 3fbfaae0..495a4266 100644 --- a/docs/_sass/custom/admonitions.scss +++ b/docs/_sass/custom/admonitions.scss @@ -3,6 +3,43 @@ // so that the rules are bundled once into the site stylesheet rather than // minified and inlined into every page's . The per-page injection is // suppressed by `_plugins/jekyll-gfm-admonitions-patch.rb`. +// +// WCAG 1.4.3: the gem ships GitHub's dark-theme alert colours, which sit at +// 2.3:1 to 3.3:1 on a white page. The title colours below are GitHub's light +// palette (the same one docs/assets/css/print.css already uses), 4.8:1 to +// 5.4:1 on $body-background-color; the dark block restores the gem's original +// colours, 4.8:1 to 6.0:1 on the dark background. Each variant's left border +// tracks its title colour. + +@use "theme" as *; + +$alert-colors-light: ( + note: #0969da, + important: #8250df, + warning: #9a6700, + tip: #1a7f37, + caution: #cf222e, +); + +$alert-colors-dark: ( + note: #4493f8, + important: #ab7df8, + warning: #d29922, + tip: #3fb950, + caution: #ff7b72, +); + +@mixin alert-palette($colors) { + @each $name, $color in $colors { + &.markdown-alert-#{$name} { + border-left-color: $color; + + .markdown-alert-title { + color: $color; + } + } + } +} .markdown-alert { padding: 0.5rem 1rem; @@ -33,43 +70,16 @@ fill: currentColor; } - &.markdown-alert-note { - border-left-color: #4493f8; - - .markdown-alert-title { - color: #4493f8; - } - } - - &.markdown-alert-important { - border-left-color: #ab7df8; - - .markdown-alert-title { - color: #ab7df8; - } - } - - &.markdown-alert-warning { - border-left-color: #9e6a03; - - .markdown-alert-title { - color: #d29922; - } - } - - &.markdown-alert-tip { - border-left-color: #238636; - - .markdown-alert-title { - color: #3fb950; - } - } - - &.markdown-alert-caution { - border-left-color: #da3633; + @include alert-palette($alert-colors-light); +} - .markdown-alert-title { - color: #f85149; - } +// custom.scss is emitted once at root level -- just-the-docs-dark.scss +// deliberately does not @use it -- so the dark variants are scoped here +// rather than inherited from a dark-theme compilation pass. The dark-theme +// mixin emits them under both the prefers-color-scheme media query (no-JS +// system default) and the explicit [data-theme="dark"] override. +@include dark-theme { + .markdown-alert { + @include alert-palette($alert-colors-dark); } } diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss index 984debdd..7cb65102 100644 --- a/docs/_sass/custom/custom.scss +++ b/docs/_sass/custom/custom.scss @@ -1,14 +1,39 @@ @use "admonitions"; +@use "theme" as *; -// Inline SVG diagrams (Phase 13). Controls are float-right links above -// the diagram; the SVG fills the column width. -.svg-controls { +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +// Inline SVG diagrams (Phase 13). Controls sit right-aligned above the +// diagram; the SVG fills the column width. +.svg-controls { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 0.25rem 0.5rem; font-size: 0.85em; - a { - float: right; - margin-left: 1em; + button { + display: inline-flex; + align-items: center; + cursor: pointer; + // WCAG 2.5.8 Target Size (Minimum): btn-reset strips all padding, leaving + // these ~21px-tall text buttons under the 24x24 CSS px floor -- and on a + // narrow column the old float layout wrapped them into rows spaced tighter + // than 24px. A 24px min-height clears the floor outright (no spacing + // exception needed); dropping the float also makes focus order follow the + // visual left-to-right order instead of reversing it. + min-height: 24px; + padding: 0 0.25rem; } } @@ -17,6 +42,166 @@ height: auto; } +// WCAG 2.1.1: builder/highlight.mjs marks the code-block scroll container +// tabindex="0", and render.mjs does the same for the table wrapper, so a +// keyboard user can scroll an overflowing block or a wide table. Give that +// focus a visible indicator. +div.highlight:focus-visible, +.table-wrapper:focus-visible { + outline: 2px solid; + outline-offset: 2px; +} + +// Section links: the keyboard and screen-reader path to the per-section URLs +// that the aria-hidden chain icons offer to the mouse. Emitted at the end of +//
by renderSectionLinks (builder/template.mjs). +// +// The page footer is one rule-divided block: the section-links disclosure, +// a row of actions, then the legal row. It used to be two dividers (the
+// plus a border-top on the disclosure, which sat above it at the end of +//
), a 16px "Back to top" among 12px siblings, and five stacked

-- +// 224px of page bottom for four short lines of content. +// +// The type size is set once, here, rather than by a `text-small` utility on +// each element. That is the whole reason the old block looked mismatched: +// "Back to top" was the one

that never got the class. +// The divider itself. JTD gives every


32px top and bottom, which is +// right in running prose and is half the reason this block sprawled. Scoped +// to the direct child of the main-content element, which is only ever the +// footer rule -- an
written in a page is inside
. +// +// The id selector is load-bearing, and `.main-content > hr` is not enough: +// the dark compilation re-emits JTD's bare `hr` rule as +// `html[data-theme=dark] hr`, which is (0,1,2) against that selector's +// (0,1,1). It applied in light and silently did not in dark -- 24/16px +// against 32/32px -- which is the same failure the footnote underlines hit. +// `#main-content > hr` is (1,0,1) and clears it in both themes. +#main-content > hr { + margin-top: 1.5rem; + margin-bottom: 1rem; +} + +.main-content > footer[role="contentinfo"] { + font-size: 0.75rem; + line-height: 1.5; + color: #54515a; // $grey-dk-100 -- 7.77:1 on #fff + + // The combined stylesheet ends with a bare `span { color: #4e26af }` -- + // Jekyll's callouts.scss.liquid output, emitted after this file, so it + // beats an inherited colour on every span in here and turns the copyright + // line link-purple. Opt the footer's spans back into inheritance. + span { + color: inherit; + } + + // Rows, not stacks. `gap` alone separates the items: a "\00b7" in a + // ::before would be read out by some screen readers on every page. + // + // `align-items: center` is not cosmetic padding-out of the rule -- the + // default `stretch` pulls every item to the row's 24px height and then + // leaves its text at the top of that box, while the min-height anchors + // below centre theirs. That put the "Download ... or PDF." baseline 4px + // above "Back to top". + .footer-actions, + .footer-legal { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.125rem 1.25rem; + } + + .footer-actions { margin-top: 0.5rem; } + .footer-legal { margin-top: 0.25rem; } + + // The actions row is all and the legal row is all (see + // renderFooterActions / renderFooterLegal), so one rule covers every item + // in the row and there is no second element type to fall out of step. The + // selector stays `> *` rather than `> a` so that a stray element added + // later is sized like its neighbours instead of quietly sitting 4px high, + // which is how the old in this row behaved. + // + // inline-block, not flex: an item may carry phrase content with a link in + // it, and a flex container turns each run of words into a flex item and + // collapses the spaces between them. + // + // 24px is WCAG 2.5.8 target-size, met on *size* so no font swap can flip + // it. Without it #back-to-top and #edit-this-page are 15.8px targets that + // scrape through on the spacing exception -- axe measures "a diameter of + // 24px which is at least 24px", zero margin, against a number that moves + // with whatever `system-ui` resolves to. That is how the heading-link hit + // box passed here and failed on CI's fonts. + // + // The legal row is deliberately left out: its links sit inside sentences + // ("License: CC-BY-4.0"), the criterion's inline exception, and axe does + // not evaluate them. + .footer-actions > *, + .section-links li > a { + display: inline-block; + min-height: 24px; + line-height: 24px; + } +} + +// Kept visually quiet -- one muted line closed, which is the whole point; +// anyone who does not need it should be able to ignore it. +// +// Two things this rule deliberately does NOT set. `min-height` for WCAG 2.5.8 +// target-size is already handled site-wide by `.main-content summary` further +// down this file, which is same-specificity and later -- a local `min-height` +// here silently loses to it. And `display` stays at the UA's `list-item`: +// giving a `display: flex` makes Chrome drop the disclosure +// triangle, which is the only thing marking this as something you can open. +.section-links { + > summary { + color: inherit; + } + + // `.main-content ol, .main-content ul, ...` sets `margin-top: 0.5em` + // at the same (0,1,1) and later in the file, so a bare + // `.section-links > ul` here never applied at all -- the list has been + // taking the body-prose 0.5em since it shipped. `.main-content` in + // front clears that in light mode; the dark compilation re-emits the + // same JTD rule as `html[data-theme=dark] .main-content ul` at + // (0,2,2), which out-ranks (0,2,1), so the dark half is not optional. + // This is the third instance of that trap in this file. + .main-content & > ul { + margin: 0.25rem 0 0; + } + + // The marker is placed against the
  • 's line box, so it only lines up + // with the link text if the two are the same height. Two things are + // needed: a line-height matching the 24px anchor box, and `vertical-align` + // to stop that anchor -- an inline-block, so it sits on the baseline and + // reserves descender space beneath itself -- from stretching the
  • to + // 31px and dropping the bullet 3.5px out of line with its own text. + li { + line-height: 24px; + + > a { + vertical-align: top; + } + } +} + +// A list of same-page links is worth nothing on paper. +@media print { + .section-links { + display: none; + } +} + +@include dark-theme { + .main-content > footer[role="contentinfo"] { + color: #b6b4b7; // 7.30:1 on #27262b ($body-background-color) + } + + // See the light-mode rule above: the dark compilation's copy of JTD's + // list margin is (0,2,2) and would otherwise win. + .main-content .section-links > ul { + margin-top: 0.25rem; + } +} + .site-logo { padding-right: 3rem; } @@ -25,12 +210,19 @@ // commit db4e3b0 "Make the code blocks more readable by growing their // font size.", which originally hand-edited the pre-compiled CSS. JTD's // base.scss sets `code { font-size: 0.75em }` and code.scss sets -// `:not(pre, figure) > code { padding: 0.2em 0.15em }`; we override both. +// `:not(pre, figure) > code { padding: 0.2em 0.15em }`; we override both +// at equal specificity, so source order decides the winner. +// +// This mixin is therefore @included by the callers *after* their +// meta.load-css() of modules -- never at this module's root. A root-level +// include here would be emitted before modules in the light compilation +// (custom/custom is @used at the top of just-the-docs-combined.scss), so +// base.scss / code.scss would come later and win -- the bug that made +// light-mode code render at 0.75em while dark rendered at 0.95em. // -// `@include tb-code-overrides` is included a second time inside -// `html.dark-mode { ... }` in just-the-docs-combined.scss so the dark- -// theme JTD rules (`html.dark-mode code { ... }` at specificity 0,1,1) -// don't out-rank our root-level overrides. +// Light theme: just-the-docs-combined.scss @includes it after load-css. +// Dark theme: just-the-docs-dark.scss inlines an identical copy inside +// html.dark-mode {} after its own load-css (0,1,1 there). @mixin tb-code-overrides { code { font-size: 0.95em; @@ -46,9 +238,7 @@ } } -@include tb-code-overrides; - -html.dark-mode { +@include dark-theme { /* * SCROLLBAR STYLING */ @@ -84,7 +274,10 @@ html.dark-mode { } // Dark-mode background for ```tb fenced code blocks, matching the - // twinBASIC IDE's CodePanelBackColor. Token colours come from Shiki via + // twinBASIC IDE's CodePanelBackColor. Now the same value as + // $code-background-color (see docs/_sass/modules-dark.scss); kept + // explicit so tb blocks stay pinned to the IDE colour if that moves. + // Token colours come from Shiki via // assets/css/tb-highlight.css (generated by builder/highlight-theme.mjs // from builder/themes/{Light,Dark}.theme). .language-tb.highlighter-rouge, @@ -94,3 +287,219 @@ html.dark-mode { background-color: rgb(33, 33, 33); } } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + +// Theme toggle (Phase 5): a 3-state cycle button (system / light / dark). +// It ships `hidden` and is revealed by theme-toggle.js, so with JS off there +// is no dead control and prefers-color-scheme governs. `.site-button` sets +// `display: flex`, which would override the UA `[hidden]` rule -- restore it +// explicitly so the button stays hidden until the script wires it. +#theme-toggle[hidden] { + display: none; +} + +// Show only the icon matching the current choice; theme-toggle.js sets +// data-theme-choice on the button. +#theme-toggle .theme-icon { + display: none; +} + +#theme-toggle[data-theme-choice="system"] [data-icon="system"], +#theme-toggle[data-theme-choice="light"] [data-icon="light"], +#theme-toggle[data-theme-choice="dark"] [data-icon="dark"] { + display: block; +} + +// `.aux-nav` is `overflow-x: auto` (navigation.scss:182), and the overflow +// spec forces the other axis from `visible` to `auto` when one axis is not +// `visible` -- so the nav is a scroll container and clips at its padding box +// on all four sides. The toggle is the nav's first item and `height: 100%`, +// which puts its left, top and bottom edges exactly on that clip edge, so +// `.btn-reset`'s outset ring (2px at 2px offset, buttons.scss:134) survives +// only on the right, where the aux-nav link leaves room. Draw it inside the +// border box instead; the button is 50x59 with 16px padding, so an inset ring +// still clears the 18px icon by 14px. +// +// Only the offset is overridden -- width and colour stay with `.btn-reset`, +// whose dark-mode copy re-emits the whole rule at (0,3,1). An id selector is +// what clears that bar in both themes; a `.aux-nav .site-button` descendant +// selector is (0,3,0) and would apply in light mode only. +#theme-toggle:focus-visible { + outline-offset: -2px; +} + +// The aux nav's "twinBASIC Home" link has the same problem and the same +// cause. Measured at 1280x900 in both themes: the nav's box is top 0 / +// bottom 59 and the link's is top 0 / bottom 59 -- identical -- so +// Chrome's UA `outline: auto` at `outline-offset: 1px` draws its top and +// bottom segments outside the scroll container's padding box and they +// are clipped away. Only the left and right bars survive, because the +// link starts 50px inboard of the nav's left edge. +// +// Give it an author ring drawn inside the border box, matching +// `.btn-reset:focus-visible` (buttons.scss:134) in width and colour so +// the two aux-nav controls look the same. The link is not a +// `.btn-reset`, and nothing else in either compilation sets an outline +// on `.site-button`, so (0,3,1) here is unopposed in light mode -- but +// the dark half still needs its own colour, and the dark-theme mixin +// puts it at (0,4,2). +.aux-nav a.site-button:focus-visible { + outline: 2px solid #4e26af; // $link-color -- as compiled for light + outline-offset: -2px; +} + +@include dark-theme { + .aux-nav a.site-button:focus-visible { + outline-color: #8cc2ff; // dark $link-color + } +} + +// Video thumbnails (Videos section). The YouTube