From d000409c33e734c1863aa02ae509cc01e5bfb485 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 27 Sep 2026 09:50:03 +0000 Subject: [PATCH 1/7] Adopt facto-stack Antora extensions and playbook wiring Merge DevCentr/facto-stack compose: indexify URLs, site-nav-tree with curated OSO sidebar roots, nav-typology, page-context, link tooling, mermaid-client, diagram-lightbox, and Kroki for non-Mermaid diagrams. Point grammaton docs at Desktop-Tooling/grammaton-ui-shell. Co-authored-by: Ryan Johnson --- antora-playbook-local.yml | 50 +++++++- antora-playbook.yml | 52 ++++++++- package.json | 15 ++- pnpm-lock.yaml | 238 +++++++++++++++++++++++++++++++++++++- 4 files changed, 348 insertions(+), 7 deletions(-) diff --git a/antora-playbook-local.yml b/antora-playbook-local.yml index 8f23700..bb4d494 100644 --- a/antora-playbook-local.yml +++ b/antora-playbook-local.yml @@ -1,3 +1,4 @@ +# Local sibling checkouts — same Facto extensions as antora-playbook.yml site: title: OpenShellOrg Docs url: https://docs.opensh.org @@ -7,12 +8,50 @@ site: site_home_url: https://opensh.org site_home_label: OpenShellOrg home header_doc_title: OpenShellOrg Docs + nav_typology_diataxis: 'true' + +urls: + html_extension_style: indexify antora: extensions: - '@antora/lunr-extension' - require: '@antora-supplemental/unversioned-component-urls' + - require: '@antora-supplemental/site-nav-tree' + include: + - open-shell-org + - shell-architecture + - about + - nu-require + - nu-emit + - project-map + order: + - open-shell-org + - shell-architecture + - about + - nu-require + - nu-emit + - project-map + - require: '@antora-supplemental/nav-typology' + - require: '@antora-supplemental/nav-typology-diataxis' - '@antora-supplemental/build-stack' + - '@antora-supplemental/page-edit' + - require: '@antora-supplemental/link-validator' + reportEmail: openshell@devcentr.org + softFail403: true + failLevel: none + - require: '@antora-supplemental/linkinator' + statusDefault: unchecked + - require: '@antora-supplemental/extension-lister' + layouts: [footer] + - require: '@antora-supplemental/orphan-finder/extension' + reportEmail: openshell@devcentr.org + failOnUnresolved: false + - require: '@antora-supplemental/page-context/antora' + component: open-shell-org + path: keywords + - require: '@antora-supplemental/mermaid-client/antora' + - require: '@antora-supplemental/diagram-lightbox' content: sources: @@ -43,7 +82,7 @@ content: - url: ../project-map branches: HEAD start_path: docs - - url: ../actor-shell + - url: ../grammaton-ui-shell branches: HEAD start_path: docs - url: ../terminal-gui-prompts @@ -57,12 +96,21 @@ ui: supplemental_files: ./supplemental-ui asciidoc: + extensions: + - asciidoctor-kroki + - '@antora-supplemental/page-context' attributes: stem: latexmath experimental: "" idprefix: "" idseparator: "-" page-pagination: "" + kroki-server-url: https://kroki.io + kroki-fetch-diagram: true + mermaid-client: '' + mermaid-client-mode: client + page-context-active: '' + page-context-keyword-base: '/open-shell-org/keywords' output: dir: build/site diff --git a/antora-playbook.yml b/antora-playbook.yml index 358c4a6..889c7e4 100644 --- a/antora-playbook.yml +++ b/antora-playbook.yml @@ -1,3 +1,4 @@ +# OpenShellOrg Docs — CI/production (Facto stack + OSO hub sources) site: title: OpenShellOrg Docs url: https://docs.opensh.org @@ -11,13 +12,50 @@ site: site_home_url: https://opensh.org site_home_label: OpenShellOrg home header_doc_title: OpenShellOrg Docs + nav_typology_diataxis: 'true' + +urls: + html_extension_style: indexify antora: extensions: - '@antora/lunr-extension' - require: '@antora-supplemental/unversioned-component-urls' - - '@antora-supplemental/page-edit' + - require: '@antora-supplemental/site-nav-tree' + include: + - open-shell-org + - shell-architecture + - about + - nu-require + - nu-emit + - project-map + order: + - open-shell-org + - shell-architecture + - about + - nu-require + - nu-emit + - project-map + - require: '@antora-supplemental/nav-typology' + - require: '@antora-supplemental/nav-typology-diataxis' - '@antora-supplemental/build-stack' + - '@antora-supplemental/page-edit' + - require: '@antora-supplemental/link-validator' + reportEmail: openshell@devcentr.org + softFail403: true + failLevel: none + - require: '@antora-supplemental/linkinator' + statusDefault: unchecked + - require: '@antora-supplemental/extension-lister' + layouts: [footer] + - require: '@antora-supplemental/orphan-finder/extension' + reportEmail: openshell@devcentr.org + failOnUnresolved: false + - require: '@antora-supplemental/page-context/antora' + component: open-shell-org + path: keywords + - require: '@antora-supplemental/mermaid-client/antora' + - require: '@antora-supplemental/diagram-lightbox' content: sources: @@ -48,7 +86,8 @@ content: - url: https://github.com/openshellorg/project-map.git branches: [main] start_path: docs - - url: https://github.com/openshellorg/actor-shell.git + # Grammaton UI Shell moved from openshellorg/actor-shell to Desktop-Tooling/grammaton-ui-shell + - url: https://github.com/Desktop-Tooling/grammaton-ui-shell.git branches: [main] start_path: docs - url: https://github.com/openshellorg/terminal-gui-prompts.git @@ -62,12 +101,21 @@ ui: supplemental_files: ./supplemental-ui asciidoc: + extensions: + - asciidoctor-kroki + - '@antora-supplemental/page-context' attributes: stem: latexmath experimental: "" idprefix: "" idseparator: "-" page-pagination: "" + kroki-server-url: https://kroki.io + kroki-fetch-diagram: true + mermaid-client: '' + mermaid-client-mode: client + page-context-active: '' + page-context-keyword-base: '/open-shell-org/keywords' output: dir: build/site diff --git a/package.json b/package.json index 862e8d3..b1be35b 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,25 @@ "format": "dprint fmt", "format:check": "dprint check" }, + "dependencies": { + "@antora-supplemental/page-context": "github:antora-supplemental/page-context#v0.6.7", + "@antora/lunr-extension": "^1.0.0-alpha.13", + "asciidoctor-kroki": "^0.18.1" + }, "devDependencies": { "@antora-supplemental/build-stack": "github:antora-supplemental/build-stack#main", + "@antora-supplemental/diagram-lightbox": "github:antora-supplemental/antora-diagram-engines#dbdca5920074ef1559bdd263f874270292df9558&path:packages/diagram-lightbox", + "@antora-supplemental/extension-lister": "github:antora-supplemental/extension-lister#v0.1.0", + "@antora-supplemental/link-validator": "github:antora-supplemental/link-validator#v0.2.0", + "@antora-supplemental/linkinator": "github:antora-supplemental/linkinator#v0.1.0", + "@antora-supplemental/mermaid-client": "github:antora-supplemental/antora-diagram-engines#dbdca5920074ef1559bdd263f874270292df9558&path:packages/mermaid-client", + "@antora-supplemental/nav-typology": "github:antora-supplemental/nav-typology#c3e4006", + "@antora-supplemental/nav-typology-diataxis": "github:antora-supplemental/nav-typology-diataxis#main", + "@antora-supplemental/orphan-finder": "github:antora-supplemental/orphan-finder#v0.1.0", "@antora-supplemental/page-edit": "github:antora-supplemental/page-edit#v0.2.4", + "@antora-supplemental/site-nav-tree": "github:antora-supplemental/site-nav-tree#54b6cbb", "@antora-supplemental/unversioned-component-urls": "github:antora-supplemental/antora-unversioned-component-urls#v2.1.0", "@antora/cli": "^3.1.14", - "@antora/lunr-extension": "1.0.0-alpha.13", "@antora/site-generator": "^3.1.14" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e2a809..751962e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,22 +7,56 @@ settings: importers: .: + dependencies: + '@antora-supplemental/page-context': + specifier: github:antora-supplemental/page-context#v0.6.7 + version: https://codeload.github.com/antora-supplemental/page-context/tar.gz/ee6fb4d68f5daf10e3b51f4a817e2ebfd5d09d6d(@antora/site-generator@3.1.15) + '@antora/lunr-extension': + specifier: ^1.0.0-alpha.13 + version: 1.0.0-alpha.13 + asciidoctor-kroki: + specifier: ^0.18.1 + version: 0.18.1(@asciidoctor/core@2.2.9) devDependencies: '@antora-supplemental/build-stack': specifier: github:antora-supplemental/build-stack#main version: https://codeload.github.com/antora-supplemental/build-stack/tar.gz/84875b509693847d123c066d815af3269dd3054c(@antora/site-generator@3.1.15) + '@antora-supplemental/diagram-lightbox': + specifier: github:antora-supplemental/antora-diagram-engines#dbdca5920074ef1559bdd263f874270292df9558&path:packages/diagram-lightbox + version: https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/diagram-lightbox(@antora/site-generator@3.1.15) + '@antora-supplemental/extension-lister': + specifier: github:antora-supplemental/extension-lister#v0.1.0 + version: https://codeload.github.com/antora-supplemental/extension-lister/tar.gz/19149b332ead9cf681a118aa0e730f8cda325900(@antora/site-generator@3.1.15) + '@antora-supplemental/link-validator': + specifier: github:antora-supplemental/link-validator#v0.2.0 + version: https://codeload.github.com/antora-supplemental/link-validator/tar.gz/67b5f6ef0c341d4c6613517b0b31910dd0dd00b1(@antora/site-generator@3.1.15) + '@antora-supplemental/linkinator': + specifier: github:antora-supplemental/linkinator#v0.1.0 + version: https://codeload.github.com/antora-supplemental/linkinator/tar.gz/68eda3930ad811adbc0bd1fafdbd0199217a94c3(@antora/site-generator@3.1.15) + '@antora-supplemental/mermaid-client': + specifier: github:antora-supplemental/antora-diagram-engines#dbdca5920074ef1559bdd263f874270292df9558&path:packages/mermaid-client + version: https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/mermaid-client(@antora/site-generator@3.1.15) + '@antora-supplemental/nav-typology': + specifier: github:antora-supplemental/nav-typology#c3e4006 + version: https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0(@antora/site-generator@3.1.15) + '@antora-supplemental/nav-typology-diataxis': + specifier: github:antora-supplemental/nav-typology-diataxis#main + version: https://codeload.github.com/antora-supplemental/nav-typology-diataxis/tar.gz/92e0eca1675bafedb19dffe3239b78a80de0d17d(@antora-supplemental/nav-typology@https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0(@antora/site-generator@3.1.15))(@antora/site-generator@3.1.15) + '@antora-supplemental/orphan-finder': + specifier: github:antora-supplemental/orphan-finder#v0.1.0 + version: https://codeload.github.com/antora-supplemental/orphan-finder/tar.gz/5f32d2d07a76adf4171c6da3015f3e74b4653198(@antora/site-generator@3.1.15) '@antora-supplemental/page-edit': specifier: github:antora-supplemental/page-edit#v0.2.4 version: https://codeload.github.com/antora-supplemental/page-edit/tar.gz/207bc008e73b24673a813bfb9b0e0245cb0a47e3(@antora/site-generator@3.1.15) + '@antora-supplemental/site-nav-tree': + specifier: github:antora-supplemental/site-nav-tree#54b6cbb + version: https://codeload.github.com/antora-supplemental/site-nav-tree/tar.gz/54b6cbb504f29563a5fa97ee832aae06d6703154(@antora/site-generator@3.1.15) '@antora-supplemental/unversioned-component-urls': specifier: github:antora-supplemental/antora-unversioned-component-urls#v2.1.0 version: https://codeload.github.com/antora-supplemental/antora-unversioned-component-urls/tar.gz/fab7292ee987bc1b479ad174f4a394fe8952998e(@antora/site-generator@3.1.15) '@antora/cli': specifier: ^3.1.14 version: 3.1.15 - '@antora/lunr-extension': - specifier: 1.0.0-alpha.13 - version: 1.0.0-alpha.13 '@antora/site-generator': specifier: ^3.1.14 version: 3.1.15 @@ -58,6 +92,12 @@ importers: packages: + '@antora-supplemental/asciidoc-link-validator@https://codeload.github.com/antora-supplemental/asciidoc-link-validator/tar.gz/e522c175fd7eb18a04773092a220ba6fce37e521': + resolution: {tarball: https://codeload.github.com/antora-supplemental/asciidoc-link-validator/tar.gz/e522c175fd7eb18a04773092a220ba6fce37e521} + version: 0.2.0 + engines: {node: '>=18'} + hasBin: true + '@antora-supplemental/build-stack@https://codeload.github.com/antora-supplemental/build-stack/tar.gz/84875b509693847d123c066d815af3269dd3054c': resolution: {tarball: https://codeload.github.com/antora-supplemental/build-stack/tar.gz/84875b509693847d123c066d815af3269dd3054c} version: 0.1.2 @@ -65,6 +105,97 @@ packages: peerDependencies: '@antora/site-generator': '>=3.1.0' + '@antora-supplemental/diagram-engines-shared@https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/shared': + resolution: {path: packages/shared, tarball: https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558} + version: 0.1.0 + engines: {node: '>=18'} + + '@antora-supplemental/diagram-lightbox@https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/diagram-lightbox': + resolution: {path: packages/diagram-lightbox, tarball: https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558} + version: 0.1.0 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + + '@antora-supplemental/extension-lister@https://codeload.github.com/antora-supplemental/extension-lister/tar.gz/19149b332ead9cf681a118aa0e730f8cda325900': + resolution: {tarball: https://codeload.github.com/antora-supplemental/extension-lister/tar.gz/19149b332ead9cf681a118aa0e730f8cda325900} + version: 0.1.0 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + + '@antora-supplemental/link-validator@https://codeload.github.com/antora-supplemental/link-validator/tar.gz/67b5f6ef0c341d4c6613517b0b31910dd0dd00b1': + resolution: {tarball: https://codeload.github.com/antora-supplemental/link-validator/tar.gz/67b5f6ef0c341d4c6613517b0b31910dd0dd00b1} + version: 0.2.0 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + + '@antora-supplemental/linkinator@https://codeload.github.com/antora-supplemental/linkinator/tar.gz/68eda3930ad811adbc0bd1fafdbd0199217a94c3': + resolution: {tarball: https://codeload.github.com/antora-supplemental/linkinator/tar.gz/68eda3930ad811adbc0bd1fafdbd0199217a94c3} + version: 0.1.0 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + + '@antora-supplemental/mermaid-client@https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/mermaid-client': + resolution: {path: packages/mermaid-client, tarball: https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558} + version: 0.1.0 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + + '@antora-supplemental/nav-typology-diataxis@https://codeload.github.com/antora-supplemental/nav-typology-diataxis/tar.gz/92e0eca1675bafedb19dffe3239b78a80de0d17d': + resolution: {tarball: https://codeload.github.com/antora-supplemental/nav-typology-diataxis/tar.gz/92e0eca1675bafedb19dffe3239b78a80de0d17d} + version: 0.1.0 + engines: {node: '>=18'} + peerDependencies: + '@antora-supplemental/nav-typology': '>=0.1.0' + '@antora/site-generator': '>=3.1.0' + + '@antora-supplemental/nav-typology@https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0': + resolution: {tarball: https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0} + version: 0.1.8 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + + '@antora-supplemental/orphan-finder@https://codeload.github.com/antora-supplemental/orphan-finder/tar.gz/5f32d2d07a76adf4171c6da3015f3e74b4653198': + resolution: {tarball: https://codeload.github.com/antora-supplemental/orphan-finder/tar.gz/5f32d2d07a76adf4171c6da3015f3e74b4653198} + version: 0.1.0 + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + + '@antora-supplemental/page-context@https://codeload.github.com/antora-supplemental/page-context/tar.gz/ee6fb4d68f5daf10e3b51f4a817e2ebfd5d09d6d': + resolution: {tarball: https://codeload.github.com/antora-supplemental/page-context/tar.gz/ee6fb4d68f5daf10e3b51f4a817e2ebfd5d09d6d} + version: 0.6.7 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + peerDependenciesMeta: + '@antora/site-generator': + optional: true + '@antora-supplemental/page-edit@https://codeload.github.com/antora-supplemental/page-edit/tar.gz/207bc008e73b24673a813bfb9b0e0245cb0a47e3': resolution: {tarball: https://codeload.github.com/antora-supplemental/page-edit/tar.gz/207bc008e73b24673a813bfb9b0e0245cb0a47e3} version: 0.2.4 @@ -72,6 +203,18 @@ packages: peerDependencies: '@antora/site-generator': '>=3.1.0' + '@antora-supplemental/site-nav-tree@https://codeload.github.com/antora-supplemental/site-nav-tree/tar.gz/54b6cbb504f29563a5fa97ee832aae06d6703154': + resolution: {tarball: https://codeload.github.com/antora-supplemental/site-nav-tree/tar.gz/54b6cbb504f29563a5fa97ee832aae06d6703154} + version: 0.5.3 + engines: {node: '>=18'} + peerDependencies: + '@antora/site-generator': '>=3.1.0' + + '@antora-supplemental/triage-ux-kit@https://codeload.github.com/antora-supplemental/triage-ux-kit/tar.gz/4b7888d2f18048e68e1176c9aca4abf7aa4bac40': + resolution: {tarball: https://codeload.github.com/antora-supplemental/triage-ux-kit/tar.gz/4b7888d2f18048e68e1176c9aca4abf7aa4bac40} + version: 0.1.0 + engines: {node: '>=18'} + '@antora-supplemental/unversioned-component-urls@https://codeload.github.com/antora-supplemental/antora-unversioned-component-urls/tar.gz/fab7292ee987bc1b479ad174f4a394fe8952998e': resolution: {tarball: https://codeload.github.com/antora-supplemental/antora-unversioned-component-urls/tar.gz/fab7292ee987bc1b479ad174f4a394fe8952998e} version: 2.1.0 @@ -1141,6 +1284,12 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asciidoctor-kroki@0.18.1: + resolution: {integrity: sha512-eQxbBCaPTbyNoJtk62Gp+6h4LlJp2147g7eS0QIVjqaLpFa8sseH0BlMiBoATrJUYv1w3nR+FTzvloBJ/MioYg==} + engines: {node: '>=10'} + peerDependencies: + '@asciidoctor/core': '>=2.2 <4.0' + asciidoctor-opal-runtime@0.3.4: resolution: {integrity: sha512-zqd6zn1LV+PZ69AP/kEbB00zuPHMIAJY3IX8+aZV+X1qOwatYvKGjsMmdMc5ApfhtkjZ4mYkqiTPJWnEnBiMJg==} engines: {node: '>=8.11'} @@ -2165,6 +2314,11 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp@2.1.3: + resolution: {integrity: sha512-sjAkg21peAG9HS+Dkx7hlG9Ztx7HLeKnvB3NQRcu/mltCVmvkF0pisbiTSfDVYTT86XEfZrTUosLdZLStquZUw==} + engines: {node: '>=10'} + hasBin: true + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -2278,6 +2432,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -2507,6 +2664,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rusha@0.8.14: + resolution: {integrity: sha512-cLgakCUf6PedEu15t8kbsjnwIFFR2D4RfL+W3iWFJ4iac7z4B0ZI8fxy4R3J956kAI68HclCFGL8MPoUVC3qVA==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -3105,14 +3265,71 @@ packages: snapshots: + '@antora-supplemental/asciidoc-link-validator@https://codeload.github.com/antora-supplemental/asciidoc-link-validator/tar.gz/e522c175fd7eb18a04773092a220ba6fce37e521': + dependencies: + js-yaml: 4.1.1 + '@antora-supplemental/build-stack@https://codeload.github.com/antora-supplemental/build-stack/tar.gz/84875b509693847d123c066d815af3269dd3054c(@antora/site-generator@3.1.15)': dependencies: '@antora/site-generator': 3.1.15 + '@antora-supplemental/diagram-engines-shared@https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/shared': {} + + '@antora-supplemental/diagram-lightbox@https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/diagram-lightbox(@antora/site-generator@3.1.15)': + dependencies: + '@antora-supplemental/diagram-engines-shared': https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/shared + optionalDependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/extension-lister@https://codeload.github.com/antora-supplemental/extension-lister/tar.gz/19149b332ead9cf681a118aa0e730f8cda325900(@antora/site-generator@3.1.15)': + optionalDependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/link-validator@https://codeload.github.com/antora-supplemental/link-validator/tar.gz/67b5f6ef0c341d4c6613517b0b31910dd0dd00b1(@antora/site-generator@3.1.15)': + dependencies: + '@antora-supplemental/asciidoc-link-validator': https://codeload.github.com/antora-supplemental/asciidoc-link-validator/tar.gz/e522c175fd7eb18a04773092a220ba6fce37e521 + optionalDependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/linkinator@https://codeload.github.com/antora-supplemental/linkinator/tar.gz/68eda3930ad811adbc0bd1fafdbd0199217a94c3(@antora/site-generator@3.1.15)': + optionalDependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/mermaid-client@https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/mermaid-client(@antora/site-generator@3.1.15)': + dependencies: + '@antora-supplemental/diagram-engines-shared': https://codeload.github.com/antora-supplemental/antora-diagram-engines/tar.gz/dbdca5920074ef1559bdd263f874270292df9558#path:packages/shared + optionalDependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/nav-typology-diataxis@https://codeload.github.com/antora-supplemental/nav-typology-diataxis/tar.gz/92e0eca1675bafedb19dffe3239b78a80de0d17d(@antora-supplemental/nav-typology@https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0(@antora/site-generator@3.1.15))(@antora/site-generator@3.1.15)': + dependencies: + '@antora-supplemental/nav-typology': https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0(@antora/site-generator@3.1.15) + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/nav-typology@https://codeload.github.com/antora-supplemental/nav-typology/tar.gz/c3e4006cd0bc3f5dbbee6e03f87e52bd93d61be0(@antora/site-generator@3.1.15)': + dependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/orphan-finder@https://codeload.github.com/antora-supplemental/orphan-finder/tar.gz/5f32d2d07a76adf4171c6da3015f3e74b4653198(@antora/site-generator@3.1.15)': + dependencies: + '@antora-supplemental/triage-ux-kit': https://codeload.github.com/antora-supplemental/triage-ux-kit/tar.gz/4b7888d2f18048e68e1176c9aca4abf7aa4bac40 + optionalDependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/page-context@https://codeload.github.com/antora-supplemental/page-context/tar.gz/ee6fb4d68f5daf10e3b51f4a817e2ebfd5d09d6d(@antora/site-generator@3.1.15)': + optionalDependencies: + '@antora/site-generator': 3.1.15 + '@antora-supplemental/page-edit@https://codeload.github.com/antora-supplemental/page-edit/tar.gz/207bc008e73b24673a813bfb9b0e0245cb0a47e3(@antora/site-generator@3.1.15)': dependencies: '@antora/site-generator': 3.1.15 + '@antora-supplemental/site-nav-tree@https://codeload.github.com/antora-supplemental/site-nav-tree/tar.gz/54b6cbb504f29563a5fa97ee832aae06d6703154(@antora/site-generator@3.1.15)': + dependencies: + '@antora/site-generator': 3.1.15 + + '@antora-supplemental/triage-ux-kit@https://codeload.github.com/antora-supplemental/triage-ux-kit/tar.gz/4b7888d2f18048e68e1176c9aca4abf7aa4bac40': {} + '@antora-supplemental/unversioned-component-urls@https://codeload.github.com/antora-supplemental/antora-unversioned-component-urls/tar.gz/fab7292ee987bc1b479ad174f4a394fe8952998e(@antora/site-generator@3.1.15)': dependencies: '@antora/site-generator': 3.1.15 @@ -4151,6 +4368,15 @@ snapshots: argparse@2.0.1: {} + asciidoctor-kroki@0.18.1(@asciidoctor/core@2.2.9): + dependencies: + '@asciidoctor/core': 2.2.9 + json5: 2.2.3 + mkdirp: 2.1.3 + pako: 2.1.0 + rusha: 0.8.14 + unxhr: 1.2.0 + asciidoctor-opal-runtime@0.3.4: dependencies: fast-glob: 3.3.3 @@ -5149,6 +5375,8 @@ snapshots: dependencies: minipass: 7.1.2 + mkdirp@2.1.3: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -5340,6 +5568,8 @@ snapshots: pako@1.0.11: {} + pako@2.1.0: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -5597,6 +5827,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rusha@0.8.14: {} + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} From 6302d386da5e9de5af7c1c622d5f1e6082a98791 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 27 Sep 2026 09:50:05 +0000 Subject: [PATCH 2/7] Add Facto supplemental UI for nav-tree and diagrams Copy nav-typology and site-nav-tree companion partials, helpers, and scripts from the DevCentr docs pattern. Wire mermaid-client and diagram-lightbox via head/footer partials; drop legacy site-mermaid assets. Co-authored-by: Ryan Johnson --- CHANGELOG.adoc | 6 + README.md | 2 +- docs/modules/ROOT/pages/ecosystem.adoc | 2 +- supplemental-ui/css/nav-typology.css | 117 +++++++ supplemental-ui/css/site-mermaid.css | 30 -- .../css/site-nav-column-scroll.css | 101 ++++++ .../helpers/adt-bc-trail-crumbs.js | 156 +++++++++ supplemental-ui/helpers/adt-version-label.js | 31 ++ supplemental-ui/helpers/in-site-nav-tree.js | 22 ++ supplemental-ui/helpers/nav-component-logo.js | 36 ++ supplemental-ui/helpers/nav-typology-icon.js | 109 ++++++ supplemental-ui/helpers/nav-typology-id.js | 60 ++++ .../helpers/strip-nav-typology-emoji.js | 17 + supplemental-ui/img/nav-typology.svg | 35 ++ supplemental-ui/js/site-mermaid.js | 97 ------ supplemental-ui/js/site-nav-tree-current.js | 321 ++++++++++++++++++ supplemental-ui/js/site-toc-main-article.js | 117 +++++++ supplemental-ui/partials/breadcrumbs.hbs | 81 +++++ supplemental-ui/partials/footer-content.hbs | 15 +- supplemental-ui/partials/head-meta.hbs | 113 ++++++ supplemental-ui/partials/nav-menu.hbs | 13 + supplemental-ui/partials/nav-tree.hbs | 27 ++ 22 files changed, 1371 insertions(+), 137 deletions(-) create mode 100644 supplemental-ui/css/nav-typology.css delete mode 100644 supplemental-ui/css/site-mermaid.css create mode 100644 supplemental-ui/css/site-nav-column-scroll.css create mode 100644 supplemental-ui/helpers/adt-bc-trail-crumbs.js create mode 100644 supplemental-ui/helpers/adt-version-label.js create mode 100644 supplemental-ui/helpers/in-site-nav-tree.js create mode 100644 supplemental-ui/helpers/nav-component-logo.js create mode 100644 supplemental-ui/helpers/nav-typology-icon.js create mode 100644 supplemental-ui/helpers/nav-typology-id.js create mode 100644 supplemental-ui/helpers/strip-nav-typology-emoji.js create mode 100644 supplemental-ui/img/nav-typology.svg delete mode 100644 supplemental-ui/js/site-mermaid.js create mode 100644 supplemental-ui/js/site-nav-tree-current.js create mode 100644 supplemental-ui/js/site-toc-main-article.js create mode 100644 supplemental-ui/partials/breadcrumbs.hbs create mode 100644 supplemental-ui/partials/head-meta.hbs create mode 100644 supplemental-ui/partials/nav-menu.hbs create mode 100644 supplemental-ui/partials/nav-tree.hbs diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b70b7c7..b41137f 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -1,5 +1,11 @@ = Changelog +== 2026-09-27 -- Facto stack for docs.opensh.org + +* Adopt `@antora-supplemental/facto-stack` wiring: `indexify` URLs, site-nav-tree curated sidebar roots, nav-typology (+ Diátaxis), page-context/edit, link-validator, linkinator, extension-lister, orphan-finder, build-stack footer, client Mermaid + diagram lightbox, Kroki bake for non-Mermaid diagrams. +* Playbook content source: `openshellorg/actor-shell` → `Desktop-Tooling/grammaton-ui-shell` (component `grammaton-ui-shell`). +* Supplemental UI: nav-tree / typology partials and helpers from the Facto compose pattern (DevCentr reference). + == 2026-09-26 -- Config Key Sanitation renamed to Config Lifecycle Management * The protocol page is now xref:standard-config-lifecycle-management.adoc[Config Lifecycle Management]; the old `standard-config-key-sanitation` URL redirects to it. diff --git a/README.md b/README.md index fb116af..e94cfbc 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ ## About The Project -Documentation hub for [OpenShellOrg](https://github.com/openshellorg) — Antora site aggregating org projects, plus SOS certification packages in this monorepo. +Documentation hub for [OpenShellOrg](https://github.com/openshellorg) — Antora site aggregating org projects, plus SOS certification packages in this monorepo. The published site follows the [facto-stack](https://github.com/antora-supplemental/facto-stack) Antora compose pack (Valentus + Lunr + page-context + site-nav-tree + maint extensions), aligned with [DevCentr docs](https://docs.devcentr.org/). ### Mission diff --git a/docs/modules/ROOT/pages/ecosystem.adoc b/docs/modules/ROOT/pages/ecosystem.adoc index 5cc9b85..ccb50bf 100644 --- a/docs/modules/ROOT/pages/ecosystem.adoc +++ b/docs/modules/ROOT/pages/ecosystem.adoc @@ -33,7 +33,7 @@ link:https://github.com/AMDphreak/connectome-fs[connectome-fs] owns node/edition Spec and ownership live in shell-architecture: * xref:shell-architecture::shell-host-and-env-refresh.adoc[env-refresh + terminal fork] — Nu-first `env-refresh` CLI (planned) and https://github.com/openshellorg/terminal[`openshellorg/terminal`] (fork of Windows Terminal) with per-tab env health + Fix/Insert desync bar; DevCentr prefers these when installed -* xref:actor-shell::index.adoc[actor-shell] — non-frustrating Windows/Linux UI shell on an Erlang-style actor runtime (D); Phase 1 `libbeam_d` landed +* xref:grammaton-ui-shell::index.adoc[Grammaton UI Shell] — non-frustrating Windows/Linux UI shell on an Erlang-style actor runtime (D); Phase 1 `libbeam_d` landed (formerly actor-shell; now `Desktop-Tooling/grammaton-ui-shell`) == Orientation and help diff --git a/supplemental-ui/css/nav-typology.css b/supplemental-ui/css/nav-typology.css new file mode 100644 index 0000000..3da59e9 --- /dev/null +++ b/supplemental-ui/css/nav-typology.css @@ -0,0 +1,117 @@ +:root { + --typology-component-root: #178f80; + --typology-spec-component: #2563eb; + --typology-spec-feature: #7c3aed; + --typology-diataxis-tutorial: #2563eb; + --typology-diataxis-howto: #178f80; + --typology-diataxis-reference: #7c3aed; + --typology-diataxis-explanation: #d97706; + --typology-changelog: #c2410c; + --typology-overview: #0f766e; + /* Larger solid glyphs; DeepWiki left-nav text-sm ≈ 0.875rem */ + --nav-typology-icon-size: 1.05em; + --nav-typology-font-size: 0.875rem; + --nav-typology-font-size-root: 0.9375rem; + --nav-typology-toggle-size: 0.625rem; +} + +html.dark-theme { + --typology-component-root: #2dd4bf; + --typology-spec-component: #60a5fa; + --typology-spec-feature: #a78bfa; + --typology-diataxis-tutorial: #60a5fa; + --typology-diataxis-howto: #2dd4bf; + --typology-diataxis-reference: #a78bfa; + --typology-diataxis-explanation: #fbbf24; + --typology-changelog: #fb923c; + --typology-overview: #2dd4bf; +} + +/* + * Avoid a nested horizontal scrollport inside the SoftNav rail (wheel / middle-button + * jank). Clip overflow; labels wrap or ellipsize instead of forcing overflow-x:auto. + */ +nav.nav-menu.nav-tree-only, +nav.nav-menu.adt-nav-tree-only { + overflow-x: hidden; +} + +nav.nav-menu.nav-tree-only .nav-item > .nav-link, +nav.nav-menu.adt-nav-tree-only .nav-item > .nav-link, +nav.nav-menu.nav-tree-only .nav-item > .nav-text, +nav.nav-menu.adt-nav-tree-only .nav-item > .nav-text { + display: flex; + align-items: flex-start; + gap: 0.4rem; + font-size: var(--nav-typology-font-size); + line-height: 1.35; +} + +/* Component forest roots: slightly larger section label (DeepWiki section feel). */ +nav.nav-menu.nav-tree-only .nav-item[data-depth="0"] > .nav-link, +nav.nav-menu.adt-nav-tree-only .nav-item[data-depth="0"] > .nav-link, +nav.nav-menu.nav-tree-only .nav-item[data-depth="0"] > .nav-text, +nav.nav-menu.adt-nav-tree-only .nav-item[data-depth="0"] > .nav-text { + font-size: var(--nav-typology-font-size-root); + font-weight: 600; +} + +/* Expander chevrons must stay smaller than label text. */ +nav.nav-menu.nav-tree-only .nav-item-toggle, +nav.nav-menu.adt-nav-tree-only .nav-item-toggle { + width: var(--nav-typology-toggle-size) !important; + height: var(--nav-typology-toggle-size) !important; + min-width: var(--nav-typology-toggle-size); + min-height: var(--nav-typology-toggle-size); +} + +nav.nav-menu.nav-tree-only .nav-typology-icon, +nav.nav-menu.adt-nav-tree-only .nav-typology-icon { + width: var(--nav-typology-icon-size); + height: var(--nav-typology-icon-size); + min-width: var(--nav-typology-icon-size); + max-width: var(--nav-typology-icon-size); + flex: 0 0 var(--nav-typology-icon-size); + margin-top: 0.15em; + fill: currentColor; + overflow: visible; +} + +nav.nav-menu.nav-tree-only .nav-component-logo, +nav.nav-menu.adt-nav-tree-only .nav-component-logo { + width: var(--nav-typology-icon-size); + height: var(--nav-typology-icon-size); + min-width: var(--nav-typology-icon-size); + max-width: var(--nav-typology-icon-size); + flex: 0 0 var(--nav-typology-icon-size); + margin-top: 0.15em; + object-fit: contain; +} + +nav.nav-menu.nav-tree-only .nav-item > .nav-link > .nav-text, +nav.nav-menu.adt-nav-tree-only .nav-item > .nav-link > .nav-text, +nav.nav-menu.nav-tree-only .nav-item > .nav-text > .nav-typology-label, +nav.nav-menu.adt-nav-tree-only .nav-item > .nav-text > .nav-typology-label { + min-width: 0; + flex: 1 1 auto; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; +} + +.nav-typology-icon--component-root { color: var(--typology-component-root); } +.nav-typology-icon--spec-component { color: var(--typology-spec-component); } +.nav-typology-icon--spec-feature { color: var(--typology-spec-feature); } +.nav-typology-icon--diataxis-tutorial { color: var(--typology-diataxis-tutorial); } +.nav-typology-icon--diataxis-howto { color: var(--typology-diataxis-howto); } +.nav-typology-icon--diataxis-reference { color: var(--typology-diataxis-reference); } +.nav-typology-icon--diataxis-explanation { color: var(--typology-diataxis-explanation); } +.nav-typology-icon--changelog { color: var(--typology-changelog); font-weight: 600; } +.nav-typology-icon--overview { color: var(--typology-overview); } + +nav.nav-menu.nav-tree-only .nav-item[data-nav-typology="changelog"] > .nav-link, +nav.nav-menu.adt-nav-tree-only .nav-item[data-nav-typology="changelog"] > .nav-link, +nav.nav-menu.nav-tree-only .nav-item[data-nav-typology="changelog"] > .nav-text, +nav.nav-menu.adt-nav-tree-only .nav-item[data-nav-typology="changelog"] > .nav-text { + font-weight: 600; +} \ No newline at end of file diff --git a/supplemental-ui/css/site-mermaid.css b/supplemental-ui/css/site-mermaid.css deleted file mode 100644 index a9966cb..0000000 --- a/supplemental-ui/css/site-mermaid.css +++ /dev/null @@ -1,30 +0,0 @@ -/* Mermaid diagrams in Antora content */ -.adt-mermaid, -div.mermaid { - margin: 1.25rem 0 1.75rem; - padding: 1rem 0.75rem; - overflow-x: auto; - border: 1px solid color-mix(in srgb, currentColor 14%, transparent); - border-radius: 0.5rem; - background: color-mix(in srgb, currentColor 4%, transparent); - - --mermaid-primaryColor: color-mix(in srgb, currentColor 8%, transparent); - --mermaid-primaryTextColor: currentColor; - --mermaid-primaryBorderColor: color-mix(in srgb, currentColor 35%, transparent); - --mermaid-lineColor: color-mix(in srgb, currentColor 45%, transparent); - --mermaid-textColor: currentColor; - --mermaid-mainBkg: color-mix(in srgb, currentColor 8%, transparent); - --mermaid-nodeBorder: color-mix(in srgb, currentColor 35%, transparent); - --mermaid-clusterBkg: color-mix(in srgb, currentColor 5%, transparent); - --mermaid-clusterBorder: color-mix(in srgb, currentColor 30%, transparent); - --mermaid-titleColor: currentColor; - --mermaid-edgeLabelBackground: color-mix(in srgb, currentColor 4%, transparent); -} - -.adt-mermaid svg, -div.mermaid svg { - display: block; - max-width: 100%; - height: auto; - margin-inline: auto; -} diff --git a/supplemental-ui/css/site-nav-column-scroll.css b/supplemental-ui/css/site-nav-column-scroll.css new file mode 100644 index 0000000..c927147 --- /dev/null +++ b/supplemental-ui/css/site-nav-column-scroll.css @@ -0,0 +1,101 @@ +/* + * Independent column scroll for Valentus 2.x on the DevCentr docs hub. + * + * Root cause (still after 11d35d6): site-doc-layout + * (1) align-self:stretch + height:auto on .nav so the rail grows with the article + * (2) overflow-y:visible on .nav-panel-menu and overflow-y:auto on nav.nav-menu + * so the tree (not the SoftNav scroll key) is the scrollport — or neither is + * (3) overscroll-behavior:auto !important chains wheel to the page + * A fixed height of (100vh - navbar) on a column that starts *below* the tool band + * also hangs past the fold, so page scroll was required to reach the tree bottom. + * + * Fix: sticky + max-height = remaining viewport under sticky top; panel + * [data-panel=menu] is the only vertical scrollport (SoftNav scroll-y key); + * contain overscroll; tree grows with content (horizontal edge-fade only). + */ +:root { + --adt-nav-sticky-top: var(--navbar-height, 3.5rem); + --adt-nav-sticky-max: calc(100vh - var(--adt-nav-sticky-top)); +} + +@supports (height: 100dvh) { + :root { + --adt-nav-sticky-max: calc(100dvh - var(--adt-nav-sticky-top)); + } +} + +@media screen and (min-width: 1024px) { + .body.adt-body { + align-items: flex-start !important; + } + + .body.adt-body > .nav-container, + .nav-container { + align-self: flex-start !important; + position: sticky !important; + top: var(--adt-nav-sticky-top) !important; + max-height: var(--adt-nav-sticky-max) !important; + height: var(--adt-nav-sticky-max) !important; + min-height: 0 !important; + overflow: hidden !important; + } + + .nav-container .nav, + .nav { + position: static !important; + top: auto !important; + height: 100% !important; + max-height: 100% !important; + min-height: 0 !important; + display: flex !important; + flex-direction: column !important; + flex: 1 1 auto !important; + } + + .nav .panels { + display: flex !important; + flex-direction: column !important; + flex: 1 1 auto !important; + min-height: 0 !important; + height: 100% !important; + max-height: 100% !important; + } + + /* + * Vertical scrollport = [data-panel=menu] (JS restore key). + * Defeat Valentus overflow-y:visible + overscroll chaining. + */ + body.adt-body .nav .nav-panel-menu, + .nav-container .nav-panel-menu, + .nav-panel-menu { + flex: 1 1 auto !important; + min-height: 0 !important; + min-width: 0 !important; + height: auto !important; + max-height: none !important; + overflow-x: hidden !important; + overflow-y: auto !important; + overscroll-behavior: contain !important; + -ms-scroll-chaining: none !important; + } + + body.adt-body .nav nav.nav-menu.nav-tree-only, + body.adt-body .nav nav.nav-menu.adt-nav-tree-only, + .nav-container nav.nav-menu.nav-tree-only, + .nav-container nav.nav-menu.adt-nav-tree-only, + nav.nav-menu.nav-tree-only, + nav.nav-menu.adt-nav-tree-only { + flex: 0 0 auto !important; + min-height: 0 !important; + height: auto !important; + max-height: none !important; + overflow-x: hidden !important; + overflow-y: visible !important; + } +} + +/* SoftNav seamlessness: do NOT blank the tree with visibility:hidden. + * Prior data-snt-nav-ready hide caused a flash on every soft navigation + * (worse than DeepWiki). site-nav-tree 0.5.3 + SoftNav 0.2.0 patch currents + * in place instead. + */ diff --git a/supplemental-ui/helpers/adt-bc-trail-crumbs.js b/supplemental-ui/helpers/adt-bc-trail-crumbs.js new file mode 100644 index 0000000..8a5f09e --- /dev/null +++ b/supplemental-ui/helpers/adt-bc-trail-crumbs.js @@ -0,0 +1,156 @@ +'use strict' + +/** + * Hub override of Valentus `adt-bc-trail-crumbs`. + * + * Stock Valentus (with site-nav-tree) only drops a leading crumb that matches the + * *current* component root. On this hub, `home` is the first forest root and + * home/nav.adoc xrefs into other components, so Antora's trail often starts with + * Home even when page.component is e.g. general-knowledge. The house icon already + * covers site home — the underlined trail should stay within the current component. + * + * While `site.keys.site_nav_tree` is on: + * 1. Strip leading crumbs that are foreign component roots (esp. Home / site home) + * 2. Then apply the usual current-component-root strip + */ + +function normalizeUrl (url) { + if (url == null || url === '') return '' + let u = String(url).split(/[?#]/)[0] + u = u.replace(/\/index\.html$/i, '/') + if (u.length > 1) u = u.replace(/\/+$/, '/') || '/' + return u +} + +function crumbContent (crumb) { + if (!crumb || crumb.content == null) return '' + return String(crumb.content).trim() +} + +function componentRootUrl (component) { + if (!component) return '' + const latest = component.latest + if (latest && latest.url) return latest.url + const versions = component.versions + if (Array.isArray(versions) && versions[0] && versions[0].url) return versions[0].url + return component.url || '' +} + +function componentRootTitle (component) { + if (!component) return '' + return ( + component.title || + (component.latest && (component.latest.title || component.latest.displayVersion)) || + '' + ) +} + +function crumbMatchesComponentRoot (crumb, component) { + if (!crumb || !component) return false + const rootUrl = componentRootUrl(component) + const rootTitle = componentRootTitle(component) + const urlMatch = + crumb.url && rootUrl && normalizeUrl(crumb.url) === normalizeUrl(rootUrl) + const titleMatch = + crumbContent(crumb) && + rootTitle && + crumbContent(crumb) === String(rootTitle).trim() + return !!(urlMatch || titleMatch) +} + +function isSiteHomeCrumb (crumb, site, homeComponent) { + if (!crumb) return false + const content = crumbContent(crumb) + if (content.toLowerCase() === 'home') return true + + const homeUrl = site && site.homeUrl + if (crumb.url && homeUrl && normalizeUrl(crumb.url) === normalizeUrl(homeUrl)) { + return true + } + + if (homeComponent && crumbMatchesComponentRoot(crumb, homeComponent)) return true + return false +} + +function findHomeComponent (site) { + const components = site && site.components + if (!components) return null + if (components.home) return components.home + // Antora may expose components as a map-like object or array + if (typeof components[Symbol.iterator] === 'function') { + for (const c of components) { + if (c && (c.name === 'home' || String(c.title || '').trim() === 'Home')) return c + } + } + for (const key of Object.keys(components)) { + const c = components[key] + if (c && (c.name === 'home' || key === 'home')) return c + } + return null +} + +function listSiteComponents (site) { + const components = site && site.components + if (!components) return [] + if (typeof components[Symbol.iterator] === 'function') return Array.from(components) + return Object.keys(components).map((k) => components[k]).filter(Boolean) +} + +function isForeignComponentRootCrumb (crumb, site, currentComponentName) { + if (!crumb) return false + for (const component of listSiteComponents(site)) { + const name = component && component.name + if (!name || name === currentComponentName) continue + if (crumbMatchesComponentRoot(crumb, component)) return true + } + return false +} + +module.exports = (breadcrumbs, options) => { + const list = Array.isArray(breadcrumbs) ? breadcrumbs.slice() : [] + if (!list.length) return list + + const root = options && options.data && options.data.root + const site = root && root.site + const keys = site && site.keys + if (!keys || String(keys.site_nav_tree) !== 'true') return list + + const page = root.page + const currentComponent = page && page.component + const currentName = currentComponent && currentComponent.name + const homeComponent = findHomeComponent(site) + + // 1. Drop leading foreign roots (Home first among them) / site-home crumbs + while (list.length) { + const first = list[0] + const foreign = + isSiteHomeCrumb(first, site, homeComponent) || + isForeignComponentRootCrumb(first, site, currentName) + // Never strip the current component's own root here — step 2 owns that + if (currentComponent && crumbMatchesComponentRoot(first, currentComponent)) break + if (!foreign) break + list.shift() + } + + // 2. Stock Valentus: drop leading crumb that duplicates the current component root + const cv = page && page.componentVersion + const componentUrl = cv && cv.url + const componentTitle = + (currentComponent && currentComponent.title) || + (cv && (cv.title || cv.displayVersion)) + + const first = list[0] + if (!first) return list + + const urlMatch = + first.url && + componentUrl && + normalizeUrl(first.url) === normalizeUrl(componentUrl) + const titleMatch = + first.content && + componentTitle && + String(first.content) === String(componentTitle) + + if (urlMatch || titleMatch) return list.slice(1) + return list +} diff --git a/supplemental-ui/helpers/adt-version-label.js b/supplemental-ui/helpers/adt-version-label.js new file mode 100644 index 0000000..2f29331 --- /dev/null +++ b/supplemental-ui/helpers/adt-version-label.js @@ -0,0 +1,31 @@ +'use strict' + +/** + * Version kicker / dropdown label: named versions as-is; implicit default → fallback. + * Default fallback is `~` (Antora’s unversioned marker) for dropdown rows. + * Kickers pass fallback="Version" so the button reads “Version”, not `~`. + * Keep in sync with valentus-theme (hub overrides breadcrumbs for site-nav-tree filter). + */ +function isNamedVersion (value) { + if (value == null) return false + const s = String(value).trim() + if (s === '' || s === '~') return false + const lower = s.toLowerCase() + return lower !== 'default' +} + +module.exports = (versionRef, { hash } = {}) => { + const fallback = (hash && hash.fallback) || '~' + let display + let version + if (versionRef && typeof versionRef === 'object') { + display = versionRef.displayVersion + version = versionRef.version + } else { + display = versionRef + version = hash && hash.version + } + if (isNamedVersion(display)) return String(display) + if (isNamedVersion(version)) return String(version) + return fallback +} diff --git a/supplemental-ui/helpers/in-site-nav-tree.js b/supplemental-ui/helpers/in-site-nav-tree.js new file mode 100644 index 0000000..62db717 --- /dev/null +++ b/supplemental-ui/helpers/in-site-nav-tree.js @@ -0,0 +1,22 @@ +'use strict' + +/** + * True when a component name belongs in the curated site-nav-tree forest. + * Reads site.keys.site_nav_tree_include / _exclude published by the extension. + * No include list → all components except exclude (legacy all-forest mode). + */ +module.exports = (name, { data } = {}) => { + if (name == null || name === '') return false + const keys = (data && data.root && data.root.site && data.root.site.keys) || {} + const split = (v) => + String(v || '') + .split(',') + .map((s) => s.trim()) + .filter(Boolean) + const include = split(keys.site_nav_tree_include) + const exclude = split(keys.site_nav_tree_exclude) + const n = String(name) + if (exclude.includes(n)) return false + if (include.length) return include.includes(n) + return true +} diff --git a/supplemental-ui/helpers/nav-component-logo.js b/supplemental-ui/helpers/nav-component-logo.js new file mode 100644 index 0000000..ebe4c2d --- /dev/null +++ b/supplemental-ui/helpers/nav-component-logo.js @@ -0,0 +1,36 @@ +'use strict' + +/** + * Optional per-component nav logos (site-nav-tree depth-0 roots and subtree items). + * Map Antora component name → supplemental-ui/img/nav-logos/. + */ +const LOGOS = {} + +function normalizeUrl (url) { + if (url == null || url === '') return '' + let u = String(url).split(/[?#]/)[0] + u = u.replace(/\/index\.html$/i, '/') + if (u.length > 1) u = u.replace(/\/+$/, '') || '/' + return u +} + +function componentNameFromUrl (url) { + const path = normalizeUrl(url) + if (!path || path === '/') return '' + const segment = path.replace(/^\/+/, '').split('/')[0] + return segment || '' +} + +module.exports = (item, options = {}) => { + if (!item || typeof item !== 'object') return '' + const name = componentNameFromUrl(item.url) + if (!name) return '' + const file = LOGOS[name] + if (!file) return '' + const uiRoot = options.data?.root?.uiRootPath || options.data?.root?.siteRootPath || '/_' + return ( + `` + ) +} + +module.exports.componentNameFromUrl = componentNameFromUrl diff --git a/supplemental-ui/helpers/nav-typology-icon.js b/supplemental-ui/helpers/nav-typology-icon.js new file mode 100644 index 0000000..bf735d9 --- /dev/null +++ b/supplemental-ui/helpers/nav-typology-icon.js @@ -0,0 +1,109 @@ +'use strict' + +const { resolveTypologyId: resolveTypologyIdCore } = require('@antora-supplemental/nav-typology/lib/resolve-typology') + +const TYPOLOGIES = { + 'component-root': { id: 'component-root', spriteId: 'icon-component-root', label: 'Component' }, + 'spec-component': { id: 'spec-component', spriteId: 'icon-spec-component', label: 'Component spec' }, + 'spec-feature': { id: 'spec-feature', spriteId: 'icon-spec-feature', label: 'Feature spec' }, + 'diataxis-tutorial': { id: 'diataxis-tutorial', spriteId: 'icon-diataxis-tutorial', label: 'Tutorial' }, + 'diataxis-howto': { id: 'diataxis-howto', spriteId: 'icon-diataxis-howto', label: 'How-to' }, + 'diataxis-reference': { id: 'diataxis-reference', spriteId: 'icon-diataxis-reference', label: 'Reference' }, + 'diataxis-explanation': { + id: 'diataxis-explanation', + spriteId: 'icon-diataxis-explanation', + label: 'Explanation', + }, + changelog: { + id: 'changelog', + spriteId: 'icon-changelog', + label: 'Changelog', + }, + overview: { + id: 'overview', + spriteId: 'icon-overview', + label: 'Overview', + }, +} + +/** Typologies that render an icon by default (component-root is opt-in). */ +const DEFAULT_ICON_IDS = new Set([ + 'spec-component', + 'spec-feature', + 'diataxis-tutorial', + 'diataxis-howto', + 'diataxis-reference', + 'diataxis-explanation', + 'changelog', + 'overview', +]) + +function siteKeys (options = {}) { + return (options.data && options.data.root && options.data.root.site && options.data.root.site.keys) || {} +} + +function diataxisEnabled (options = {}) { + const keys = siteKeys(options) + return keys.nav_typology_diataxis === 'true' || keys.nav_typology === 'true' +} + +/** Opt-in: site.keys.nav_typology_component_root_icons === 'true' */ +function componentRootIconsEnabled (options = {}) { + return siteKeys(options).nav_typology_component_root_icons === 'true' +} + +/** + * Icons on by default when Diataxis typology is enabled. + * Temporary A/B kill-switch: site.keys.nav_typology_icons === 'false' + */ +function typologyIconsEnabled (options = {}) { + const keys = siteKeys(options) + if (keys.nav_typology_icons === 'false') return false + return true +} + +function shouldRenderIcon (meta, options = {}) { + if (!meta) return false + if (!typologyIconsEnabled(options)) return false + if (meta.id === 'component-root') return componentRootIconsEnabled(options) + return DEFAULT_ICON_IDS.has(meta.id) +} + +function resolveTypology (item, options = {}) { + if (!item || typeof item !== 'object') return null + + const level = options.hash?.level ?? 0 + const depth = Number(level) || 0 + const parentTypologyId = options.hash?.parentTypologyId || '' + const diataxis = diataxisEnabled(options) + + let id = null + if (depth === 0 && item.url && Array.isArray(item.items) && item.items.length) { + id = 'component-root' + } else { + id = resolveTypologyIdCore(item, { + depth, + parentTypologyId, + diataxisEnabled: diataxis, + skipBuildFallback: false, + }) + } + + return id && TYPOLOGIES[id] ? TYPOLOGIES[id] : null +} + +module.exports = (item, options = {}) => { + const meta = resolveTypology(item, options) + if (!shouldRenderIcon(meta, options)) return '' + const uiRoot = options.data?.root?.uiRootPath || options.data?.root?.siteRootPath || '/_' + // Explicit size attributes match --nav-typology-icon-size (solid glyphs read larger). + return ( + `` + ) +} + +module.exports.resolveTypology = resolveTypology +module.exports.shouldRenderIcon = shouldRenderIcon +module.exports.componentRootIconsEnabled = componentRootIconsEnabled +module.exports.typologyIconsEnabled = typologyIconsEnabled diff --git a/supplemental-ui/helpers/nav-typology-id.js b/supplemental-ui/helpers/nav-typology-id.js new file mode 100644 index 0000000..df23f6e --- /dev/null +++ b/supplemental-ui/helpers/nav-typology-id.js @@ -0,0 +1,60 @@ +'use strict' + +const { resolveTypologyId: resolveTypologyIdCore } = require('@antora-supplemental/nav-typology/lib/resolve-typology') + +const TYPOLOGIES = { + 'component-root': { id: 'component-root', spriteId: 'icon-component-root', label: 'Component' }, + 'spec-component': { id: 'spec-component', spriteId: 'icon-spec-component', label: 'Component spec' }, + 'spec-feature': { id: 'spec-feature', spriteId: 'icon-spec-feature', label: 'Feature spec' }, + 'diataxis-tutorial': { id: 'diataxis-tutorial', spriteId: 'icon-diataxis-tutorial', label: 'Tutorial' }, + 'diataxis-howto': { id: 'diataxis-howto', spriteId: 'icon-diataxis-howto', label: 'How-to' }, + 'diataxis-reference': { id: 'diataxis-reference', spriteId: 'icon-diataxis-reference', label: 'Reference' }, + 'diataxis-explanation': { + id: 'diataxis-explanation', + spriteId: 'icon-diataxis-explanation', + label: 'Explanation', + }, + changelog: { + id: 'changelog', + spriteId: 'icon-changelog', + label: 'Changelog', + }, + overview: { + id: 'overview', + spriteId: 'icon-overview', + label: 'Overview', + }, +} + +function diataxisEnabled ({ data } = {}) { + const keys = (data && data.root && data.root.site && data.root.site.keys) || {} + return keys.nav_typology_diataxis === 'true' || keys.nav_typology === 'true' +} + +function resolveTypology (item, options = {}) { + if (!item || typeof item !== 'object') return null + + const level = options.hash?.level ?? 0 + const depth = Number(level) || 0 + const parentTypologyId = options.hash?.parentTypologyId || '' + const diataxis = diataxisEnabled(options) + + let id = null + if (depth === 0 && item.url && Array.isArray(item.items) && item.items.length) { + id = 'component-root' + } else { + id = resolveTypologyIdCore(item, { + depth, + parentTypologyId, + diataxisEnabled: diataxis, + skipBuildFallback: false, + }) + } + + return id && TYPOLOGIES[id] ? TYPOLOGIES[id] : null +} + +module.exports = (item, options = {}) => { + const meta = resolveTypology(item, options) + return meta ? meta.id : '' +} diff --git a/supplemental-ui/helpers/strip-nav-typology-emoji.js b/supplemental-ui/helpers/strip-nav-typology-emoji.js new file mode 100644 index 0000000..d697def --- /dev/null +++ b/supplemental-ui/helpers/strip-nav-typology-emoji.js @@ -0,0 +1,17 @@ +'use strict' + +const LEADING_EMOJI_RE = + /^[\s\uFE0F\u200D]*(?:🎓|🛠️?|📚|🧠|📋|🧩|✨|📦)[\s\uFE0F\u200D]*/u +const LEADING_NAV_DOT_RE = /^[\s]*\.(?=\S)/u + +module.exports = (text) => { + if (text == null || text === '') return text + let out = String(text) + let prev + do { + prev = out + out = out.replace(LEADING_EMOJI_RE, '') + out = out.replace(LEADING_NAV_DOT_RE, '') + } while (out !== prev) + return out.trimStart() +} diff --git a/supplemental-ui/img/nav-typology.svg b/supplemental-ui/img/nav-typology.svg new file mode 100644 index 0000000..a654ce1 --- /dev/null +++ b/supplemental-ui/img/nav-typology.svg @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/supplemental-ui/js/site-mermaid.js b/supplemental-ui/js/site-mermaid.js deleted file mode 100644 index 8a29e7b..0000000 --- a/supplemental-ui/js/site-mermaid.js +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Render AsciiDoc Mermaid blocks in Antora pages. - * Supports [source,mermaid] (code.language-mermaid) and bare [mermaid] listingblocks. - */ -;(function () { - const MERMAID_CDN = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js' - - function isDark() { - return document.documentElement.classList.contains('dark-theme') - } - - function looksLikeMermaid(text) { - const t = String(text || '').trim() - return /^(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|erDiagram|journey|gantt|pie|mindmap|timeline|gitGraph|C4Context|C4Container)\b/.test( - t, - ) - } - - function collectTargets() { - const nodes = [] - document.querySelectorAll('code.language-mermaid, code[data-lang="mermaid"]').forEach((code) => { - const block = code.closest('.listingblock') || code.parentElement - if (block) nodes.push({ block, text: code.textContent || '' }) - }) - document.querySelectorAll('.listingblock > .content > pre').forEach((pre) => { - if (pre.querySelector('code.language-mermaid, code[data-lang="mermaid"]')) return - const text = pre.textContent || '' - if (!looksLikeMermaid(text)) return - const block = pre.closest('.listingblock') - if (block) nodes.push({ block, text }) - }) - return nodes - } - - function mountDiagram(block, text, index) { - const host = document.createElement('div') - host.className = 'mermaid adt-mermaid' - host.setAttribute('data-mermaid-index', String(index)) - host.textContent = text.trim() - block.replaceWith(host) - return host - } - - function loadScript(src) { - return new Promise((resolve, reject) => { - if (window.mermaid) { - resolve(window.mermaid) - return - } - const existing = document.querySelector('script[data-adt-mermaid]') - if (existing) { - existing.addEventListener('load', () => resolve(window.mermaid)) - existing.addEventListener('error', reject) - return - } - const s = document.createElement('script') - s.src = src - s.async = true - s.dataset.adtMermaid = '1' - s.onload = () => resolve(window.mermaid) - s.onerror = reject - document.head.appendChild(s) - }) - } - - async function renderAll() { - const targets = collectTargets() - if (!targets.length) return - - const mermaid = await loadScript(MERMAID_CDN) - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'strict', - theme: isDark() ? 'dark' : 'default', - flowchart: { htmlLabels: true, curve: 'basis' }, - }) - - const hosts = targets.map((t, i) => mountDiagram(t.block, t.text, i)) - await mermaid.run({ nodes: hosts }) - } - - function boot() { - renderAll().catch((err) => console.warn('[adt-mermaid]', err)) - } - - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', boot) - } else { - boot() - } - - function onSoftNavLoaded (fn) { - if (window.SoftNav && typeof SoftNav.on === 'function') SoftNav.on('loaded', fn) - else document.addEventListener('soft-nav:loaded', function (e) { fn(e.detail || {}) }) - } - onSoftNavLoaded(function () { boot() }) -})() diff --git a/supplemental-ui/js/site-nav-tree-current.js b/supplemental-ui/js/site-nav-tree-current.js new file mode 100644 index 0000000..9c87a72 --- /dev/null +++ b/supplemental-ui/js/site-nav-tree-current.js @@ -0,0 +1,321 @@ +/** + * Prefer the deepest is-current-page when site-nav-tree inlines many components. + * Default UI expands only the first match; a duplicated start-page URL on the + * component root then leaves children behind an inactive anonymous wrapper. + * + * Also retains expand/collapse state across page navigations via sessionStorage + * so opening other component trees is not wiped when the current-path rewrite + * runs (which clears and re-applies is-active along the active page only). + * + * Expand keys use component-absolute pathnames (not relative hrefs) so SoftNav + * depth changes and absolutize() do not look like inject/remove of the tree. + * + * Nav panel scrollTop is persisted separately so left-rail position survives + * SoftNav swaps and full document loads independently of the content column. + * + * Does not blank the tree (no visibility:hidden / data-snt-nav-ready hide). + * Scroll saves are debounced; SoftNav before/unload flush immediately. + */ +;(function () { + 'use strict' + + // v2: pathname keys (stable across URL depth / SoftNav absolutize) + var STORAGE_KEY = 'site-nav-tree:expanded-v2' + var SCROLL_KEY = 'site-nav-tree:scroll-y' + var scrollSaveTimer = null + + function normalizeHref (href) { + if (!href) return '' + try { + var u = new URL(href, window.location.href) + var path = u.pathname || '/' + path = path.replace(/\/index\.html$/i, '/') + if (path.length > 1) path = path.replace(/\/+$/, '/') || '/' + return path + } catch (e) { + return String(href).split(/[?#]/)[0] + } + } + + function itemKey (el) { + var link = el.querySelector(':scope > .nav-link') + var href = link && link.getAttribute('href') + if (href) return 'h:' + normalizeHref(href) + var labelEl = el.querySelector(':scope > .nav-text, :scope > .nav-link') + var label = ((labelEl && labelEl.textContent) || '').trim().replace(/\s+/g, ' ') + var parts = [(el.getAttribute('data-depth') || '0') + ':' + label] + var parent = el.parentElement ? el.parentElement.closest('li.nav-item') : null + while (parent) { + var plink = parent.querySelector(':scope > .nav-link') + var phref = plink && plink.getAttribute('href') + if (phref) { + parts.unshift('h:' + normalizeHref(phref)) + } else { + var plabelEl = parent.querySelector(':scope > .nav-text, :scope > .nav-link') + var plabel = ((plabelEl && plabelEl.textContent) || '').trim().replace(/\s+/g, ' ') + parts.unshift((parent.getAttribute('data-depth') || '0') + ':' + plabel) + } + parent = parent.parentElement ? parent.parentElement.closest('li.nav-item') : null + } + return 'p:' + parts.join('/') + } + + function loadExpanded () { + try { + var raw = sessionStorage.getItem(STORAGE_KEY) + var parsed = raw ? JSON.parse(raw) : [] + return Array.isArray(parsed) ? parsed : [] + } catch (e) { + return [] + } + } + + function saveExpanded (keys) { + try { + var unique = [] + var seen = Object.create(null) + keys.forEach(function (k) { + if (!k || seen[k]) return + seen[k] = true + unique.push(k) + }) + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(unique)) + } catch (e) { + /* private mode / quota - ignore */ + } + } + + function collectExpanded (menu) { + return [].slice.call(menu.querySelectorAll('.nav-item.is-active')).map(itemKey) + } + + function applyExpanded (menu, keys) { + if (!keys || !keys.length) return + var want = Object.create(null) + keys.forEach(function (k) { + want[k] = true + }) + menu.querySelectorAll('.nav-item').forEach(function (el) { + if (want[itemKey(el)]) el.classList.add('is-active') + }) + } + + function persistMenu (menu) { + if (!menu) return + saveExpanded(collectExpanded(menu)) + } + + function navScrollEl () { + return document.querySelector('.nav-container [data-panel=menu]') + } + + function saveNavScrollNow () { + var el = navScrollEl() + if (!el) return + try { + sessionStorage.setItem(SCROLL_KEY, String(el.scrollTop || 0)) + } catch (e) { + /* ignore */ + } + } + + function saveNavScroll () { + if (scrollSaveTimer != null) return + scrollSaveTimer = window.setTimeout(function () { + scrollSaveTimer = null + saveNavScrollNow() + }, 100) + } + + function flushNavScroll () { + if (scrollSaveTimer != null) { + window.clearTimeout(scrollSaveTimer) + scrollSaveTimer = null + } + saveNavScrollNow() + } + + function restoreNavScroll () { + var el = navScrollEl() + if (!el) return + var raw + try { + raw = sessionStorage.getItem(SCROLL_KEY) + } catch (e) { + return + } + if (raw == null || raw === '') return + var y = parseInt(raw, 10) + if (isNaN(y)) return + el.scrollTop = y + requestAnimationFrame(function () { + var again = navScrollEl() + if (again) again.scrollTop = y + }) + } + + /** + * After SoftNav swaps (or marks current), restore remembered expansion *and* + * the current-page path without collapsing non-current siblings the reader + * left open under other (or the same) component roots. + */ + function siteNavTreeCurrent () { + var menu = document.querySelector('.nav-container [data-panel=menu]') + if (!menu) { + try { + document.documentElement.setAttribute('data-snt-nav-ready', '1') + } catch (e) { + /* ignore */ + } + return + } + + var remembered = loadExpanded() + + var currents = [].slice.call(menu.querySelectorAll('.nav-item.is-current-page')) + if (!currents.length) { + applyExpanded(menu, remembered) + persistMenu(menu) + restoreNavScroll() + try { + document.documentElement.setAttribute('data-snt-nav-ready', '1') + } catch (e) { + /* ignore */ + } + return + } + + currents.sort(function (a, b) { + return (parseInt(b.getAttribute('data-depth'), 10) || 0) - (parseInt(a.getAttribute('data-depth'), 10) || 0) + }) + var best = currents[0] + + currents.forEach(function (el) { + if (el !== best) el.classList.remove('is-current-page') + }) + + menu.querySelectorAll('.nav-item.is-current-path').forEach(function (el) { + el.classList.remove('is-current-path') + }) + menu.querySelectorAll('.nav-item.is-active').forEach(function (el) { + el.classList.remove('is-active') + }) + + var node = best + while (node && !(node.classList && node.classList.contains('nav-menu'))) { + if (node.tagName === 'LI' && node.classList.contains('nav-item')) { + node.classList.add('is-active', 'is-current-path') + } + node = node.parentNode + } + best.classList.add('is-active', 'is-current-page') + + applyExpanded(menu, remembered) + persistMenu(menu) + restoreNavScroll() + try { + document.documentElement.setAttribute('data-snt-nav-ready', '1') + } catch (e) { + /* ignore */ + } + } + + /** + * SoftNav keeps the live forest DOM (no panel wipe). Mark the deepest nav-link + * matching destPath as is-current-page, then expand via siteNavTreeCurrent. + */ + function markCurrentByPath (pathname) { + var menu = document.querySelector('.nav-container [data-panel=menu]') + if (!menu) return + var want = normalizeHref(pathname) + if (!want) return + var matches = [] + menu.querySelectorAll('a.nav-link').forEach(function (a) { + if (normalizeHref(a.getAttribute('href') || a.href) === want) { + var li = a.closest('li.nav-item') + if (li) matches.push(li) + } + }) + if (!matches.length) return + matches.sort(function (a, b) { + return (parseInt(b.getAttribute('data-depth'), 10) || 0) - (parseInt(a.getAttribute('data-depth'), 10) || 0) + }) + menu.querySelectorAll('.nav-item.is-current-page').forEach(function (el) { + el.classList.remove('is-current-page') + }) + matches[0].classList.add('is-current-page') + } + + function onToggleClick (e) { + var toggle = e.target && e.target.closest && e.target.closest('.nav-item-toggle') + if (!toggle) return + var menu = document.querySelector('.nav-container [data-panel=menu]') + if (!menu || !menu.contains(toggle)) return + setTimeout(function () { + persistMenu(menu) + }, 0) + } + + window.siteNavTreeCurrent = siteNavTreeCurrent + window.siteNavTreeMarkCurrentByPath = markCurrentByPath + window.siteNavTreeSaveScroll = flushNavScroll + window.siteNavTreeRestoreScroll = restoreNavScroll + + document.addEventListener('click', onToggleClick) + + // Debounced persist while the reader moves the left rail only. + document.addEventListener( + 'scroll', + function (e) { + var el = navScrollEl() + if (!el) return + if (e.target === el || (e.target && el.contains(e.target))) saveNavScroll() + }, + true + ) + function bindPanelScroll () { + var el = navScrollEl() + if (!el || el.getAttribute('data-snt-scroll-bound')) return + el.setAttribute('data-snt-scroll-bound', '1') + el.addEventListener('scroll', saveNavScroll, { passive: true }) + } + bindPanelScroll() + window.addEventListener('beforeunload', flushNavScroll) + document.addEventListener( + 'click', + function (e) { + var a = e.target && e.target.closest && e.target.closest('a.nav-link') + if (!a) return + var panel = navScrollEl() + if (panel && panel.contains(a)) flushNavScroll() + }, + true + ) + + siteNavTreeCurrent() + bindPanelScroll() + + function onSoftNavLoaded (fn) { + if (window.SoftNav && typeof SoftNav.on === 'function') SoftNav.on('loaded', fn) + else document.addEventListener('soft-nav:loaded', function (e) { fn(e.detail || {}) }) + } + function onSoftNavBefore (fn) { + if (window.SoftNav && typeof SoftNav.on === 'function') SoftNav.on('before', fn) + else document.addEventListener('soft-nav:before', function (e) { fn(e.detail || {}) }) + } + onSoftNavBefore(function () { + flushNavScroll() + }) + onSoftNavLoaded(function (detail) { + if (detail && detail.url && typeof markCurrentByPath === 'function') { + try { + markCurrentByPath(detail.url) + } catch (e) { + /* ignore */ + } + } + siteNavTreeCurrent() + bindPanelScroll() + restoreNavScroll() + }) +})() \ No newline at end of file diff --git a/supplemental-ui/js/site-toc-main-article.js b/supplemental-ui/js/site-toc-main-article.js new file mode 100644 index 0000000..67d6c8c --- /dev/null +++ b/supplemental-ui/js/site-toc-main-article.js @@ -0,0 +1,117 @@ +/** + * Keep the right-rail Contents chrome even when the article has no section + * headings. Default UI site.js removes aside.toc.sidebar when its heading + * query is empty; SoftNav also leaves a stale TOC because it only swaps + * mast/nav/article. + * + * Prefer a "Main article" placeholder that jumps to the page title (or article + * top) so long pages still have a Contents affordance. + */ +;(function () { + 'use strict' + + function ensureTitleId (article) { + var h1 = article.querySelector('h1.page, h1.adt-page-title, h1') + if (!h1) return null + if (!h1.id) h1.id = 'main-article' + return h1 + } + + function headingSelector (levels) { + var n = 'article.doc' + var parts = [] + var i + for (i = 0; i <= levels; i++) { + var r = [n] + if (i) { + var c + for (c = 1; c <= i; c++) r.push((c === 2 ? '.sectionbody>' : '') + '.sect' + c) + r.push('h' + (i + 1) + '[id]' + (i > 1 ? ':not(.discrete)' : '')) + } else { + r.push('h1[id].sect0') + } + parts.push(r.join('>')) + } + return parts.join(',') + } + + function ensureTocAside () { + var aside = document.querySelector('aside.toc.sidebar') + if (aside) return aside + var body = document.querySelector('.body.adt-body') || document.querySelector('.body') + if (!body) return null + aside = document.createElement('aside') + aside.className = 'toc sidebar' + aside.setAttribute('data-title', 'Contents') + aside.setAttribute('data-levels', '2') + var menu = document.createElement('div') + menu.className = 'toc-menu' + aside.appendChild(menu) + body.appendChild(aside) + return aside + } + + function rebuildToc () { + if (document.body && document.body.classList.contains('-toc')) return + var article = document.querySelector('article.doc') + if (!article) return + + var aside = ensureTocAside() + if (!aside) return + + var levels = parseInt(aside.getAttribute('data-levels') || '2', 10) + if (isNaN(levels) || levels < 0) levels = 2 + + var headings = [].slice.call(article.querySelectorAll(headingSelector(levels))) + var menu = aside.querySelector('.toc-menu') + if (!menu) { + menu = document.createElement('div') + menu.className = 'toc-menu' + aside.appendChild(menu) + } + menu.innerHTML = '' + + var title = document.createElement('h3') + title.textContent = aside.getAttribute('data-title') || 'Contents' + menu.appendChild(title) + + var ul = document.createElement('ul') + menu.appendChild(ul) + + if (!headings.length) { + var h1 = ensureTitleId(article) + var li = document.createElement('li') + li.dataset.level = '0' + var a = document.createElement('a') + a.textContent = 'Main article' + a.href = h1 ? '#' + h1.id : '#' + li.appendChild(a) + ul.appendChild(li) + return + } + + headings.forEach(function (h) { + var li = document.createElement('li') + li.dataset.level = String(parseInt(h.nodeName.slice(1), 10) - 1) + var a = document.createElement('a') + a.textContent = h.textContent + a.href = '#' + h.id + li.appendChild(a) + ul.appendChild(li) + }) + } + + function onSoftNav (fn) { + if (window.SoftNav && typeof SoftNav.on === 'function') SoftNav.on('loaded', fn) + else document.addEventListener('soft-nav:loaded', function (e) { fn(e.detail || {}) }) + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', rebuildToc) + } else { + rebuildToc() + } + onSoftNav(function () { + rebuildToc() + }) +})() diff --git a/supplemental-ui/partials/breadcrumbs.hbs b/supplemental-ui/partials/breadcrumbs.hbs new file mode 100644 index 0000000..ffe882b --- /dev/null +++ b/supplemental-ui/partials/breadcrumbs.hbs @@ -0,0 +1,81 @@ +{{! + Hub breadcrumbs: Valentus mast (component + version + copyable "/" trail seps). + Only hub delta vs stock Valentus: component dropdown filtered to site-nav-tree + curated roots via `in-site-nav-tree` (plus the current component). +}} + diff --git a/supplemental-ui/partials/footer-content.hbs b/supplemental-ui/partials/footer-content.hbs index f5725ef..0551c05 100644 --- a/supplemental-ui/partials/footer-content.hbs +++ b/supplemental-ui/partials/footer-content.hbs @@ -1,12 +1,11 @@ - - - - - - +{{> mermaid-client-scripts}} +{{> diagram-lightbox-scripts}} + + + diff --git a/supplemental-ui/partials/head-meta.hbs b/supplemental-ui/partials/head-meta.hbs new file mode 100644 index 0000000..c15ef84 --- /dev/null +++ b/supplemental-ui/partials/head-meta.hbs @@ -0,0 +1,113 @@ +{{!-- + HACK (interim paint-hold): hide until layered Valentus CSS + prefs + web fonts. + Vendored from valentus-theme 2.0.13+; keep brand/mermaid/math links with the + other stylesheets, before the fonts.ready release script. +--}} + + + + + + + + + + + +{{!-- Valentus bundle CSS (vendored head-meta pattern) --}} +{{> mermaid-client-styles}} +{{> diagram-lightbox-styles}} + + + + +{{!-- Independent nav vs article column scroll (after Valentus layout) --}} + + +{{!-- read-width FOUC (same as ui-modules/packages/read-width/ui/js/adt-read-width-fouc.js) --}} + +{{!-- font-size FOUC (same as ui-modules/packages/font-size/ui/js/adt-font-size-fouc.js) --}} + +{{!-- Webfont FOUT: kick loads, then release paint-hold (3s timeout if CDN stalls). --}} + +{{#if page.origin.webUrl}} + +{{/if}} diff --git a/supplemental-ui/partials/nav-menu.hbs b/supplemental-ui/partials/nav-menu.hbs new file mode 100644 index 0000000..5ab96a4 --- /dev/null +++ b/supplemental-ui/partials/nav-menu.hbs @@ -0,0 +1,13 @@ +{{! + Companion partial for Valentus when @antora-supplemental/site-nav-tree is on. + Component titles live in the tree roots - do not also print adt-nav-component-title. + Keeps stock nav-tree partial + Default UI / Valentus expand-collapse JS. +}} + diff --git a/supplemental-ui/partials/nav-tree.hbs b/supplemental-ui/partials/nav-tree.hbs new file mode 100644 index 0000000..bdd6c79 --- /dev/null +++ b/supplemental-ui/partials/nav-tree.hbs @@ -0,0 +1,27 @@ +{{#if navigation.length}} + +{{/if}} From d1422c00178c158c95d7285a1c5bf3751ae74b46 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 27 Sep 2026 09:53:16 +0000 Subject: [PATCH 3/7] Wire Facto themed-SVG audits and DevCentr runtime pattern Add verify-themed-svg-dark-mode Antora extension, vendored @dev-centr/themed-svg browser runtime, sync/check scripts, shell-architecture upstream diagram audits, and CI diagram-audit job. Document upstream theme manifest fix for label colors. Co-authored-by: Ryan Johnson --- .github/workflows/docs.yml | 36 + CHANGELOG.adoc | 1 + README.md | 3 + antora-playbook-local.yml | 1 + antora-playbook.yml | 1 + lib/verify-themed-svg-dark-mode.js | 208 ++ package.json | 14 +- pnpm-lock.yaml | 60 + scripts/audit-themed-svg-gaps.mjs | 70 + scripts/check-shell-architecture-diagrams.mjs | 27 + scripts/shell-architecture-themed-svg-fix.md | 18 + scripts/sync-themed-svg-runtime.mjs | 19 + supplemental-ui/js/site-themed-svg.js | 19 +- .../js/vendor/themed-svg-element.js | 2229 +++++++++++++++++ test/verify-themed-svg-dark-mode.test.js | 69 + 15 files changed, 2769 insertions(+), 6 deletions(-) create mode 100644 lib/verify-themed-svg-dark-mode.js create mode 100644 scripts/audit-themed-svg-gaps.mjs create mode 100644 scripts/check-shell-architecture-diagrams.mjs create mode 100644 scripts/shell-architecture-themed-svg-fix.md create mode 100644 scripts/sync-themed-svg-runtime.mjs create mode 100644 supplemental-ui/js/vendor/themed-svg-element.js create mode 100644 test/verify-themed-svg-dark-mode.test.js diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bb77bdf..d686e7d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,12 +5,17 @@ on: branches: [main, master] paths: - "docs/**" + - "lib/**" + - "scripts/**" + - "test/**" - "supplemental-ui/**" - "antora-playbook.yml" - "antora-playbook-local.yml" - "package.json" - "pnpm-lock.yaml" - ".github/workflows/docs.yml" + pull_request: + branches: [main, master] workflow_dispatch: repository_dispatch: types: [docs-rebuild] @@ -27,9 +32,36 @@ concurrency: cancel-in-progress: true jobs: + diagram-audit: + name: Themed SVG and diagram checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "pnpm" + cache-dependency-path: pnpm-lock.yaml + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Sync themed-svg browser runtime + run: pnpm run diagrams:sync + + - name: Run diagram and dark-mode audits + run: pnpm run diagrams:check:all + build: name: Build Antora docs runs-on: ubuntu-latest + needs: diagram-audit steps: - name: Checkout uses: actions/checkout@v4 @@ -47,6 +79,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Sync themed-svg browser runtime + run: pnpm run diagrams:sync + - name: Build Antora docs id: antora uses: antora-supplemental/antora-build-action@v2 @@ -69,6 +104,7 @@ jobs: name: Deploy to GitHub Pages runs-on: ubuntu-latest needs: build + if: github.event_name != 'pull_request' environment: name: github-pages url: ${{ steps.deploy.outputs.page_url }} diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index b41137f..5053af5 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -5,6 +5,7 @@ * Adopt `@antora-supplemental/facto-stack` wiring: `indexify` URLs, site-nav-tree curated sidebar roots, nav-typology (+ Diátaxis), page-context/edit, link-validator, linkinator, extension-lister, orphan-finder, build-stack footer, client Mermaid + diagram lightbox, Kroki bake for non-Mermaid diagrams. * Playbook content source: `openshellorg/actor-shell` → `Desktop-Tooling/grammaton-ui-shell` (component `grammaton-ui-shell`). * Supplemental UI: nav-tree / typology partials and helpers from the Facto compose pattern (DevCentr reference). +* Diagram engines: client Mermaid + diagram-lightbox + Kroki bake; `[.themed-svg]` host runtime vendored from `@dev-centr/themed-svg`; Antora `verify-themed-svg-dark-mode` extension; `pnpm diagrams:check:all` audits upstream shell-architecture diagram staleness and dark-mode text gaps in CI. == 2026-09-26 -- Config Key Sanitation renamed to Config Lifecycle Management diff --git a/README.md b/README.md index e94cfbc..b421c61 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,9 @@ pnpm build pnpm docs # or local sibling checkouts: pnpm docs:local + +# Themed SVG / diagram audits (Facto compose; fails on stale or dark-mode text gaps) +pnpm diagrams:check:all ``` Published docs: https://docs.opensh.org/ diff --git a/antora-playbook-local.yml b/antora-playbook-local.yml index bb4d494..d41a446 100644 --- a/antora-playbook-local.yml +++ b/antora-playbook-local.yml @@ -35,6 +35,7 @@ antora: - require: '@antora-supplemental/nav-typology' - require: '@antora-supplemental/nav-typology-diataxis' - '@antora-supplemental/build-stack' + - ./lib/verify-themed-svg-dark-mode.js - '@antora-supplemental/page-edit' - require: '@antora-supplemental/link-validator' reportEmail: openshell@devcentr.org diff --git a/antora-playbook.yml b/antora-playbook.yml index 889c7e4..ed3318a 100644 --- a/antora-playbook.yml +++ b/antora-playbook.yml @@ -39,6 +39,7 @@ antora: - require: '@antora-supplemental/nav-typology' - require: '@antora-supplemental/nav-typology-diataxis' - '@antora-supplemental/build-stack' + - ./lib/verify-themed-svg-dark-mode.js - '@antora-supplemental/page-edit' - require: '@antora-supplemental/link-validator' reportEmail: openshell@devcentr.org diff --git a/lib/verify-themed-svg-dark-mode.js b/lib/verify-themed-svg-dark-mode.js new file mode 100644 index 0000000..da092e1 --- /dev/null +++ b/lib/verify-themed-svg-dark-mode.js @@ -0,0 +1,208 @@ +'use strict' + +const fs = require('node:fs') +const path = require('node:path') + +/** + * Antora extension: WARN (once per finding) when Themed SVG adaptive images + * leave dark-mode text hard to read. + * + * Borrow-from-light: dark preset may reuse light values (optional borrowFromLight[]). + * Parent -> child: light-stable surfaces keep nested text stable; WARN when a changed + * darker surface/canvas still has light-mode text left behind. + */ + +function pictureName (fileLabel) { + if (!fileLabel) return 'diagram' + try { + return path.basename(String(fileLabel)) + } catch { + return String(fileLabel) + } +} + +function normalizeColor (value) { + if (typeof value !== 'string') return '' + return value.trim().toLowerCase().replace(/\s+/g, '') +} + +function sameColor (a, b) { + return normalizeColor(a) !== '' && normalizeColor(a) === normalizeColor(b) +} + +function borrowedTokenIds (manifest) { + const out = new Set() + if (!manifest?.presets?.light || !manifest?.presets?.dark) return out + const { light, dark } = manifest.presets + for (const id of Object.keys(light)) { + if (Object.prototype.hasOwnProperty.call(dark, id) && sameColor(light[id], dark[id])) out.add(id) + } + const explicit = manifest.borrowFromLight || manifest['x-devcentr-borrow-from-light'] || [] + if (Array.isArray(explicit)) for (const id of explicit) out.add(id) + return out +} + +function textTokensInside (surfaceToken, allTokenIds) { + const ids = allTokenIds instanceof Set ? allTokenIds : new Set(allTokenIds) + const related = [] + if (surfaceToken === 'color.canvas') { + for (const id of ids) { + if (id === 'color.text.primary' || id === 'color.text.muted') related.push(id) + } + return related + } + const m = surfaceToken.match(/^color\.surface\.(.+)$/) + if (!m) return related + const leaf = m[1] + for (const candidate of [`color.text.on-${leaf}`, `color.text.${leaf}`, `color.text.on.${leaf}`]) { + if (ids.has(candidate)) related.push(candidate) + } + return related +} + +function isDarkerBackground (lightHex, darkHex) { + const lum = (hex) => { + const h = normalizeColor(hex).replace('#', '') + if (!/^([0-9a-f]{3}|[0-9a-f]{6})$/.test(h)) return null + const full = h.length === 3 ? h.split('').map((c) => c + c).join('') : h + const r = parseInt(full.slice(0, 2), 16) / 255 + const g = parseInt(full.slice(2, 4), 16) / 255 + const b = parseInt(full.slice(4, 6), 16) / 255 + return 0.2126 * r + 0.7152 * g + 0.0722 * b + } + const a = lum(lightHex) + const b = lum(darkHex) + if (a == null || b == null) return darkHex !== lightHex + return b < a - 0.05 +} + +function findDarkModeTextGaps (svg, fileLabel, manifest) { + const gaps = [] + const name = pictureName(fileLabel) + if (!/prefers-color-scheme:\s*dark/.test(svg)) return gaps + if (!svg.includes('--themed-svg-')) { + gaps.push( + `Dark-mode picture ${name} is set up for dark mode but is missing theme colors, so text may not adapt.` + ) + return gaps + } + + const nsMatch = svg.match(/--themed-svg-([a-z0-9-]+)-color-text-primary/) + const ns = (manifest && manifest.namespace) || (nsMatch && nsMatch[1]) + const textVar = ns + ? new RegExp(`fill:var\\(--themed-svg-${ns}-color-text-primary`) + : /fill:var\(--themed-svg-[a-z0-9-]+-color-text-primary/ + + const borrowed = borrowedTokenIds(manifest) + const light = (manifest && manifest.presets && manifest.presets.light) || {} + const dark = (manifest && manifest.presets && manifest.presets.dark) || {} + const allIds = new Set([...Object.keys(light), ...Object.keys(dark)]) + + const canvasBorrowed = borrowed.has('color.canvas') + const textPrimaryBorrowed = borrowed.has('color.text.primary') + const lightStableFigure = canvasBorrowed && textPrimaryBorrowed + const canvasChangedToDark = + light['color.canvas'] && + dark['color.canvas'] && + !canvasBorrowed && + isDarkerBackground(light['color.canvas'], dark['color.canvas']) + + const rootRule = svg.match(/#my-svg\{[^}]*\}/) + if (rootRule) { + const hardcoded = /fill:\s*#([0-9a-fA-F]{3,8})|fill:\s*rgb\(/.test(rootRule[0]) && !textVar.test(rootRule[0]) + if (hardcoded && !lightStableFigure) { + gaps.push( + `Dark-mode picture ${name} still has dark text that will be hard to read. Try lighter text for dark backgrounds.` + ) + } + } + + const labelRule = svg.match(/#my-svg \.label\{[^}]*\}/) + if ( + labelRule && + /color:\s*#([0-9a-fA-F]{3,8})|color:\s*rgb\(0,\s*0,\s*0\)/.test(labelRule[0]) && + !/color:var\(--themed-svg-/.test(labelRule[0]) && + !lightStableFigure + ) { + gaps.push( + `Dark-mode picture ${name} still has dark labels that will be hard to read. Try lighter label text for dark backgrounds.` + ) + } + + if (canvasChangedToDark && textPrimaryBorrowed) { + gaps.push( + `Dark-mode picture ${name}: the background got darker, but the text still uses light-mode colors and will be hard to read. Use lighter text for dark backgrounds.` + ) + } + + for (const surfaceId of allIds) { + if (!surfaceId.startsWith('color.surface.')) continue + const childTextIds = textTokensInside(surfaceId, allIds) + for (const textId of childTextIds) { + if (!light[textId] || !dark[textId]) continue + const surfaceBorrowed = borrowed.has(surfaceId) + const textBorrowed = borrowed.has(textId) + const surfaceChanged = + light[surfaceId] && dark[surfaceId] && !surfaceBorrowed && !sameColor(light[surfaceId], dark[surfaceId]) + if (surfaceBorrowed && !textBorrowed && !sameColor(light[textId], dark[textId])) { + gaps.push( + `Dark-mode picture ${name}: a box stays light-colored in dark mode, but text inside it was recolored for dark mode. Keep that text matching the light look too.` + ) + } + if (surfaceChanged && isDarkerBackground(light[surfaceId], dark[surfaceId]) && textBorrowed) { + gaps.push( + `Dark-mode picture ${name}: a panel got darker in dark mode, but text on it still uses light-mode colors and will be hard to read. Use lighter text on that panel.` + ) + } + } + } + + return gaps +} + +function loadSiblingManifest (svgPath) { + if (!svgPath) return null + const base = path.basename(svgPath, '.svg') + const candidates = [ + svgPath.replace(/\.svg$/i, '.theme.json'), + path.join(path.dirname(svgPath), '..', 'partials', 'diagrams', `${base}.theme.json`), + ] + for (const themePath of candidates) { + try { + if (fs.existsSync(themePath)) return JSON.parse(fs.readFileSync(themePath, 'utf8')) + } catch { + /* ignore */ + } + } + return null +} + +module.exports.register = function () { + this.on('contentClassified', ({ contentCatalog }) => { + const logger = this.getLogger('themed-svg-dark-text') + for (const file of contentCatalog.getFiles()) { + const srcPath = file.src && file.src.path ? file.src.path : file.path + if (!srcPath || !srcPath.endsWith('.svg')) continue + if (/\.(host|fixed)\.svg$/.test(srcPath)) continue + + let svg + try { + svg = file.contents.toString('utf8') + } catch { + continue + } + if (!svg.includes('prefers-color-scheme')) continue + + const abspath = file.src && file.src.abspath ? file.src.abspath : null + const manifest = loadSiblingManifest(abspath) + const label = abspath || srcPath + for (const gap of findDarkModeTextGaps(svg, label, manifest)) { + logger.warn(gap) + } + } + }) +} + +module.exports.findDarkModeTextGaps = findDarkModeTextGaps +module.exports.borrowedTokenIds = borrowedTokenIds +module.exports.pictureName = pictureName \ No newline at end of file diff --git a/package.json b/package.json index b1be35b..301be2d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,12 @@ "build": "pnpm -r build", "docs": "antora antora-playbook.yml", "docs:local": "antora antora-playbook-local.yml", + "diagrams:sync": "node scripts/sync-themed-svg-runtime.mjs", + "diagrams:check": "node scripts/sync-themed-svg-runtime.mjs --check", + "diagrams:check:shell-architecture": "node scripts/check-shell-architecture-diagrams.mjs", + "diagrams:audit:dark-text": "node scripts/audit-themed-svg-gaps.mjs", + "diagrams:check:all": "pnpm diagrams:check && pnpm diagrams:check:shell-architecture && pnpm diagrams:audit:dark-text && pnpm test:dark-text", + "test:dark-text": "node --test test/verify-themed-svg-dark-mode.test.js", "format": "dprint fmt", "format:check": "dprint check" }, @@ -29,6 +35,12 @@ "@antora-supplemental/site-nav-tree": "github:antora-supplemental/site-nav-tree#54b6cbb", "@antora-supplemental/unversioned-component-urls": "github:antora-supplemental/antora-unversioned-component-urls#v2.1.0", "@antora/cli": "^3.1.14", - "@antora/site-generator": "^3.1.14" + "@antora/site-generator": "^3.1.14", + "@dev-centr/themed-svg": "0.2.3" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "puppeteer" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 751962e..7db41b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: '@antora/site-generator': specifier: ^3.1.14 version: 3.1.15 + '@dev-centr/themed-svg': + specifier: 0.2.3 + version: 0.2.3 apps/main: dependencies: @@ -400,6 +403,11 @@ packages: '@deno/shim-deno@0.19.2': resolution: {integrity: sha512-q3VTHl44ad8T2Tw2SpeAvghdGOjlnLPDNO2cpOxwMrBE/PVas6geWpbpIgrM+czOCH0yejp0yi8OaTuB+NU40Q==} + '@dev-centr/themed-svg@0.2.3': + resolution: {integrity: sha512-mEd90oVCPTj3Bj8wSQ9gKgJdHiM6JDbGYlVyM5sm0RZJYkwStDk8bM0RQ/fq0P4263nN96qHxlk0zPYrMs0E3Q==} + engines: {node: '>=20'} + hasBin: true + '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1184,6 +1192,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1210,6 +1221,10 @@ packages: peerDependencies: vinxi: ^0.5.5 + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} + engines: {node: '>=10.0.0'} + abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} engines: {node: ^18.17.0 || >=20.5.0} @@ -1483,6 +1498,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@2.1.1: + resolution: {integrity: sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==} + engines: {node: '>=12.20'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -1664,6 +1683,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.4.16: + resolution: {integrity: sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2515,6 +2537,9 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -2677,6 +2702,10 @@ packages: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -3217,6 +3246,9 @@ packages: resolution: {integrity: sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==} engines: {node: '>=4'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3623,6 +3655,15 @@ snapshots: '@deno/shim-deno-test': 0.5.0 which: 4.0.0 + '@dev-centr/themed-svg@0.2.3': + dependencies: + '@xmldom/xmldom': 0.8.15 + color-name: 2.1.1 + dompurify: 3.4.16 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + saxes: 6.0.0 + '@esbuild/aix-ppc64@0.25.12': optional: true @@ -4229,6 +4270,9 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@3.0.3': {} '@ungap/structured-clone@1.3.0': {} @@ -4296,6 +4340,8 @@ snapshots: recast: 0.23.11 vinxi: 0.5.11(db0@0.3.4)(ioredis@5.9.2)(jiti@2.6.1)(terser@5.46.0) + '@xmldom/xmldom@0.8.15': {} + abbrev@3.0.1: {} abort-controller@3.0.0: @@ -4567,6 +4613,8 @@ snapshots: color-name@1.1.4: {} + color-name@2.1.1: {} + colorette@2.0.20: {} comma-separated-tokens@2.0.3: {} @@ -4692,6 +4740,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.4.16: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -5662,6 +5714,8 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-value-parser@4.2.0: {} + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -5835,6 +5889,10 @@ snapshots: safe-stable-stringify@2.5.0: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scule@1.3.0: {} secure-json-parse@2.7.0: {} @@ -6473,6 +6531,8 @@ snapshots: xdg-basedir@3.0.0: {} + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@3.1.1: {} diff --git a/scripts/audit-themed-svg-gaps.mjs b/scripts/audit-themed-svg-gaps.mjs new file mode 100644 index 0000000..31e0e9f --- /dev/null +++ b/scripts/audit-themed-svg-gaps.mjs @@ -0,0 +1,70 @@ +/** + * Fail when adaptive Themed SVG figures have dark-mode text contrast gaps. + * Scans shell-architecture committed images (same layout as Antora aggregated content). + */ +import { execFileSync } from 'node:child_process' +import { existsSync, mkdtempSync, readFileSync, readdirSync, statSync, rmSync } from 'node:fs' +import { basename, dirname, join } from 'node:path' +import { tmpdir } from 'node:os' +import { findDarkModeTextGaps } from '../lib/verify-themed-svg-dark-mode.js' + +const repo = 'https://github.com/openshellorg/shell-architecture.git' +const branch = process.env.SHELL_ARCHITECTURE_REF || 'main' +const imageDirRel = 'docs/modules/ROOT/images' + +function cloneWorktree () { + const dir = mkdtempSync(join(tmpdir(), 'shell-architecture-audit-')) + execFileSync('git', ['clone', '--depth', '1', '--branch', branch, repo, dir], { stdio: 'inherit' }) + return dir +} + +function manifestForSvg (svgPath) { + const base = basename(svgPath, '.svg') + const candidates = [ + svgPath.replace(/\.svg$/i, '.theme.json'), + join(dirname(svgPath), '..', 'partials', 'diagrams', `${base}.theme.json`), + ] + for (const themePath of candidates) { + try { + if (existsSync(themePath)) return JSON.parse(readFileSync(themePath, 'utf8')) + } catch { + /* ignore */ + } + } + return null +} + +function listAdaptiveSvgs (dir) { + const out = [] + for (const name of readdirSync(dir)) { + const full = join(dir, name) + if (!statSync(full).isFile()) continue + if (!name.endsWith('.svg')) continue + if (/\.(host|fixed)\.svg$/.test(name)) continue + out.push(full) + } + return out +} + +const root = cloneWorktree() +try { + const imageDir = join(root, imageDirRel) + const failures = [] + for (const svgPath of listAdaptiveSvgs(imageDir)) { + const svg = readFileSync(svgPath, 'utf8') + if (!svg.includes('prefers-color-scheme')) continue + const manifest = manifestForSvg(svgPath) + if (!manifest) continue + const gaps = findDarkModeTextGaps(svg, svgPath, manifest) + for (const gap of gaps) failures.push(gap) + } + if (failures.length) { + console.error('Themed SVG dark-mode audit failed:\n') + for (const line of failures) console.error(`- ${line}`) + process.exitCode = 1 + } else { + console.log('Themed SVG dark-mode audit: no gaps in shell-architecture adaptive figures.') + } +} finally { + rmSync(root, { recursive: true, force: true }) +} diff --git a/scripts/check-shell-architecture-diagrams.mjs b/scripts/check-shell-architecture-diagrams.mjs new file mode 100644 index 0000000..aa5efdd --- /dev/null +++ b/scripts/check-shell-architecture-diagrams.mjs @@ -0,0 +1,27 @@ +/** + * Run openshellorg/shell-architecture diagram staleness checks (mermaid-svg-css-vars pipeline). + */ +import { execFileSync } from 'node:child_process' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const repo = 'https://github.com/openshellorg/shell-architecture.git' +const branch = process.env.SHELL_ARCHITECTURE_REF || 'main' + +const worktree = mkdtempSync(join(tmpdir(), 'shell-architecture-diagram-check-')) +try { + execFileSync('git', ['clone', '--depth', '1', '--branch', branch, repo, worktree], { stdio: 'inherit' }) + execFileSync('pnpm', ['install', '--frozen-lockfile'], { cwd: worktree, stdio: 'inherit' }) + execFileSync('pnpm', ['run', 'diagrams:check'], { cwd: worktree, stdio: 'inherit' }) +} catch (err) { + console.error( + '\nUpstream shell-architecture diagram check failed. Regenerate with `pnpm diagrams` in openshellorg/shell-architecture', + ) + console.error( + 'and ensure theme manifests include #my-svg .label color binding for dark-mode labels.\n', + ) + throw err +} finally { + rmSync(worktree, { recursive: true, force: true }) +} diff --git a/scripts/shell-architecture-themed-svg-fix.md b/scripts/shell-architecture-themed-svg-fix.md new file mode 100644 index 0000000..556520a --- /dev/null +++ b/scripts/shell-architecture-themed-svg-fix.md @@ -0,0 +1,18 @@ +Apply in openshellorg/shell-architecture so docs hub `pnpm diagrams:check:all` passes: + +1. In each `docs/modules/ROOT/partials/diagrams/*.theme.json`, add: + +```json +{ + "kind": "stylesheet", + "selector": "#my-svg .label", + "property": "color", + "token": "color.text.primary" +} +``` + +2. Run `pnpm install && pnpm diagrams && pnpm diagrams:check` and commit the regenerated + `docs/modules/ROOT/images/*.svg` and `*.host.svg` pairs. + +Without this, Antora `verify-themed-svg-dark-mode` warns on edge labels in dark mode, and hub CI +diagram audits fail against `main`. diff --git a/scripts/sync-themed-svg-runtime.mjs b/scripts/sync-themed-svg-runtime.mjs new file mode 100644 index 0000000..c468434 --- /dev/null +++ b/scripts/sync-themed-svg-runtime.mjs @@ -0,0 +1,19 @@ +import { copyFile, mkdir, readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' + +const source = resolve('node_modules/@dev-centr/themed-svg/browser/themed-svg-element.js') +const destination = resolve('supplemental-ui/js/vendor/themed-svg-element.js') +const check = process.argv.includes('--check') + +if (check) { + const [published, committed] = await Promise.all([ + readFile(source), + readFile(destination), + ]) + if (!published.equals(committed)) { + throw new Error('themed SVG browser runtime is stale; run pnpm diagrams:generate') + } +} else { + await mkdir(dirname(destination), { recursive: true }) + await copyFile(source, destination) +} diff --git a/supplemental-ui/js/site-themed-svg.js b/supplemental-ui/js/site-themed-svg.js index 5f9bc5e..de3e473 100644 --- a/supplemental-ui/js/site-themed-svg.js +++ b/supplemental-ui/js/site-themed-svg.js @@ -1,11 +1,20 @@ -import { upgradeThemedSvgImages } from "https://cdn.jsdelivr.net/npm/@dev-centr/themed-svg@0.1.1/browser/themed-svg-element.js" +import { upgradeThemedSvgImages } from './vendor/themed-svg-element.js' -upgradeThemedSvgImages(document, { - selector: ".imageblock.themed-svg img", -}) +function upgradeMarkedDiagrams () { + document.querySelectorAll('.imageblock.themed-svg img').forEach((image) => { + image.setAttribute('data-themed-svg', '') + }) + upgradeThemedSvgImages() +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', upgradeMarkedDiagrams, { once: true }) +} else { + upgradeMarkedDiagrams() +} function onSoftNavLoaded (fn) { if (window.SoftNav && typeof SoftNav.on === 'function') SoftNav.on('loaded', fn) else document.addEventListener('soft-nav:loaded', function (e) { fn(e.detail || {}) }) } -onSoftNavLoaded(function () { upgradeThemedSvgImages(document, { selector: ".imageblock.themed-svg img" }) }) +onSoftNavLoaded(function () { upgradeMarkedDiagrams() }) diff --git a/supplemental-ui/js/vendor/themed-svg-element.js b/supplemental-ui/js/vendor/themed-svg-element.js new file mode 100644 index 0000000..c222520 --- /dev/null +++ b/supplemental-ui/js/vendor/themed-svg-element.js @@ -0,0 +1,2229 @@ +var __defProp = Object.defineProperty; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __esm = (fn, res, err) => function __init() { + if (err) throw err[0]; + try { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; + } catch (e) { + throw err = [e], e; + } +}; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; + +// node_modules/.pnpm/dompurify@3.4.15/node_modules/dompurify/dist/purify.es.mjs +var purify_es_exports = {}; +__export(purify_es_exports, { + default: () => purify +}); +function _arrayLikeToArray(r, a) { + (null == a || a > r.length) && (a = r.length); + for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; + return n; +} +function _arrayWithHoles(r) { + if (Array.isArray(r)) return r; +} +function _iterableToArrayLimit(r, l) { + var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; + if (null != t) { + var e, n, i, u, a = [], f = true, o = false; + try { + if (i = (t = t.call(r)).next, 0 === l) ; + else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = true) ; + } catch (r2) { + o = true, n = r2; + } finally { + try { + if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; + } finally { + if (o) throw n; + } + } + return a; + } +} +function _nonIterableRest() { + throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); +} +function _slicedToArray(r, e) { + return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); +} +function _unsupportedIterableToArray(r, a) { + if (r) { + if ("string" == typeof r) return _arrayLikeToArray(r, a); + var t = {}.toString.call(r).slice(8, -1); + return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; + } +} +function unapply(func) { + return function(thisArg) { + if (thisArg instanceof RegExp) { + thisArg.lastIndex = 0; + } + for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { + args[_key3 - 1] = arguments[_key3]; + } + return apply(func, thisArg, args); + }; +} +function unconstruct(Func) { + return function() { + for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + return construct(Func, args); + }; +} +function addToSet(set, array) { + let transformCaseFunc = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : stringToLowerCase; + if (setPrototypeOf) { + setPrototypeOf(set, null); + } + if (!arrayIsArray(array)) { + return set; + } + let l = array.length; + while (l--) { + let element = array[l]; + if (typeof element === "string") { + const lcElement = transformCaseFunc(element); + if (lcElement !== element) { + if (!isFrozen(array)) { + array[l] = lcElement; + } + element = lcElement; + } + } + set[element] = true; + } + return set; +} +function cleanArray(array) { + for (let index = 0; index < array.length; index++) { + const isPropertyExist = objectHasOwnProperty(array, index); + if (!isPropertyExist) { + array[index] = null; + } + } + return array; +} +function clone(object) { + const newObject = create(null); + for (const _ref2 of entries(object)) { + var _ref3 = _slicedToArray(_ref2, 2); + const property = _ref3[0]; + const value = _ref3[1]; + const isPropertyExist = objectHasOwnProperty(object, property); + if (isPropertyExist) { + if (arrayIsArray(value)) { + newObject[property] = cleanArray(value); + } else if (value && typeof value === "object" && value.constructor === Object) { + newObject[property] = clone(value); + } else { + newObject[property] = value; + } + } + } + return newObject; +} +function stringifyValue(value) { + switch (typeof value) { + case "string": { + return value; + } + case "number": { + return numberToString(value); + } + case "boolean": { + return booleanToString(value); + } + case "bigint": { + return bigintToString ? bigintToString(value) : "0"; + } + case "symbol": { + return symbolToString ? symbolToString(value) : "Symbol()"; + } + case "undefined": { + return objectToString(value); + } + case "function": + case "object": { + if (value === null) { + return objectToString(value); + } + const valueAsRecord = value; + const valueToString = lookupGetter(valueAsRecord, "toString"); + if (typeof valueToString === "function") { + const stringified = valueToString(valueAsRecord); + return typeof stringified === "string" ? stringified : objectToString(stringified); + } + return objectToString(value); + } + default: { + return objectToString(value); + } + } +} +function lookupGetter(object, prop) { + while (object !== null) { + const desc = getOwnPropertyDescriptor(object, prop); + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + if (typeof desc.value === "function") { + return unapply(desc.value); + } + } + object = getPrototypeOf(object); + } + function fallbackValue() { + return null; + } + return fallbackValue; +} +function isRegex(value) { + try { + regExpTest(value, ""); + return true; + } catch (_unused) { + return false; + } +} +function createDOMPurify() { + let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal(); + const DOMPurify = (root) => createDOMPurify(root); + DOMPurify.version = "3.4.15"; + DOMPurify.removed = []; + if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document || !window2.Element) { + DOMPurify.isSupported = false; + return DOMPurify; + } + let document = window2.document; + const originalDocument = document; + const currentScript = originalDocument.currentScript; + window2.DocumentFragment; + const HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap; + _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap; + window2.HTMLFormElement; + const DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes; + const ElementPrototype = Element.prototype; + const cloneNode = lookupGetter(ElementPrototype, "cloneNode"); + const remove = lookupGetter(ElementPrototype, "remove"); + const removeAttributeNode = lookupGetter(ElementPrototype, "removeAttributeNode"); + const getNextSibling = lookupGetter(ElementPrototype, "nextSibling"); + const getChildNodes = lookupGetter(ElementPrototype, "childNodes"); + const getParentNode = lookupGetter(ElementPrototype, "parentNode"); + const getShadowRoot = lookupGetter(ElementPrototype, "shadowRoot"); + const getAttributes = lookupGetter(ElementPrototype, "attributes"); + const getNodeType = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeType") : null; + const getNodeName = Node && Node.prototype ? lookupGetter(Node.prototype, "nodeName") : null; + const getOwnerDocument = Node && Node.prototype ? lookupGetter(Node.prototype, "ownerDocument") : null; + const _readNodeType = function _readNodeType2(node) { + return getNodeType ? getNodeType(node) : node.nodeType; + }; + const _readNodeName = function _readNodeName2(node) { + return getNodeName ? getNodeName(node) : node.nodeName; + }; + if (typeof HTMLTemplateElement === "function") { + const template = document.createElement("template"); + if (template.content && template.content.ownerDocument) { + document = template.content.ownerDocument; + } + } + let trustedTypesPolicy; + let emptyHTML = ""; + let defaultTrustedTypesPolicy; + let defaultTrustedTypesPolicyResolved = false; + let IN_TRUSTED_TYPES_POLICY = 0; + const _assertNotInTrustedTypesPolicy = function _assertNotInTrustedTypesPolicy2() { + if (IN_TRUSTED_TYPES_POLICY > 0) { + throw typeErrorCreate('A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.'); + } + }; + const _createTrustedHTML = function _createTrustedHTML2(html2) { + _assertNotInTrustedTypesPolicy(); + IN_TRUSTED_TYPES_POLICY++; + try { + return trustedTypesPolicy.createHTML(html2); + } finally { + IN_TRUSTED_TYPES_POLICY--; + } + }; + const _createTrustedScriptURL = function _createTrustedScriptURL2(scriptUrl) { + _assertNotInTrustedTypesPolicy(); + IN_TRUSTED_TYPES_POLICY++; + try { + return trustedTypesPolicy.createScriptURL(scriptUrl); + } finally { + IN_TRUSTED_TYPES_POLICY--; + } + }; + const _getDefaultTrustedTypesPolicy = function _getDefaultTrustedTypesPolicy2() { + if (!defaultTrustedTypesPolicyResolved) { + defaultTrustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); + defaultTrustedTypesPolicyResolved = true; + } + return defaultTrustedTypesPolicy; + }; + const _document = document, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName; + const importNode = originalDocument.importNode; + let hooks = _createHooksMap(); + DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0; + const MUSTACHE_EXPR$1 = MUSTACHE_EXPR, ERB_EXPR$1 = ERB_EXPR, TMPLIT_EXPR$1 = TMPLIT_EXPR, DATA_ATTR$1 = DATA_ATTR, ARIA_ATTR$1 = ARIA_ATTR, IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, ATTR_WHITESPACE$1 = ATTR_WHITESPACE, CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT; + let IS_ALLOWED_URI$1 = IS_ALLOWED_URI; + let ALLOWED_TAGS = null; + const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); + let ALLOWED_ATTR = null; + const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); + let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { + tagNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeNameCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + allowCustomizedBuiltInElements: { + writable: true, + configurable: false, + enumerable: true, + value: false + } + })); + let FORBID_TAGS = null; + let FORBID_ATTR = null; + const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, { + tagCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + }, + attributeCheck: { + writable: true, + configurable: false, + enumerable: true, + value: null + } + })); + let ALLOW_ARIA_ATTR = true; + let ALLOW_DATA_ATTR = true; + let ALLOW_UNKNOWN_PROTOCOLS = false; + let ALLOW_SELF_CLOSE_IN_ATTR = true; + let SAFE_FOR_TEMPLATES = false; + let SAFE_FOR_XML = true; + let WHOLE_DOCUMENT = false; + let SET_CONFIG = false; + let SET_CONFIG_ALLOWED_TAGS = null; + let SET_CONFIG_ALLOWED_ATTR = null; + let FORCE_BODY = false; + let RETURN_DOM = false; + let RETURN_DOM_FRAGMENT = false; + let RETURN_TRUSTED_TYPE = false; + let SANITIZE_DOM = true; + let SANITIZE_NAMED_PROPS = false; + const SANITIZE_NAMED_PROPS_PREFIX = "user-content-"; + let KEEP_CONTENT = true; + let IN_PLACE = false; + let USE_PROFILES = {}; + let FORBID_CONTENTS = null; + const DEFAULT_FORBID_CONTENTS = addToSet({}, [ + "annotation-xml", + "audio", + "colgroup", + "desc", + "foreignobject", + "head", + "iframe", + "math", + "mi", + "mn", + "mo", + "ms", + "mtext", + "noembed", + "noframes", + "noscript", + "plaintext", + "script", + // mirrors the selected