diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 55172b321..0c2cf9336 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,8 @@ updates: schedule: interval: 'weekly' day: 'saturday' + cooldown: + default-days: 7 labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 @@ -16,6 +18,8 @@ updates: schedule: interval: 'weekly' day: 'saturday' + cooldown: + default-days: 7 labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 @@ -26,6 +30,8 @@ updates: schedule: interval: 'weekly' day: 'saturday' + cooldown: + default-days: 7 labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 @@ -36,6 +42,8 @@ updates: schedule: interval: 'weekly' day: 'saturday' + cooldown: + default-days: 7 labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 @@ -46,6 +54,8 @@ updates: schedule: interval: 'weekly' day: 'saturday' + cooldown: + default-days: 7 labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 @@ -56,6 +66,8 @@ updates: schedule: interval: 'weekly' day: 'saturday' + cooldown: + default-days: 7 labels: [ 'dependencies' ] commit-message: prefix: 'chore' ## prefix maximum string length of 15 diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 2b862b92e..b919ba5b8 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -21,10 +21,12 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up JDK # see https://github.com/actions/setup-java - uses: actions/setup-java@v5 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '21' distribution: 'zulu' @@ -33,7 +35,7 @@ jobs: run: ./gen.sh - name: Archive Schema documentation # https://github.com/actions/upload-artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: XML-Schema-documentation path: docgen/xml/docs @@ -46,10 +48,12 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup Python Environment # see https://github.com/actions/setup-python - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: ${{ env.PYTHON_VERSION_DEFAULT }} architecture: 'x64' @@ -57,7 +61,7 @@ jobs: run: ./gen.sh - name: Archive Schema documentation # https://github.com/actions/upload-artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: JSON-Schema-documentation path: docgen/json/docs @@ -70,12 +74,14 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Generate Schema documentation run: ./gen.sh - name: Archive Schema documentation # https://github.com/actions/upload-artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: PROTO-Schema-documentation path: docgen/proto/docs diff --git a/.github/workflows/bundle_2.0_schemas.yml b/.github/workflows/bundle_2.0_schemas.yml index e232da3ed..5808d63d8 100644 --- a/.github/workflows/bundle_2.0_schemas.yml +++ b/.github/workflows/bundle_2.0_schemas.yml @@ -7,7 +7,7 @@ on: - 2.0-dev-threatmodeling paths: - 'schema/2.0/**/*.schema.json' - - 'tools/src/main/js/bundler/bundle-schemas.js' + - 'tools/src/main/js/bundle-schemas.js' workflow_dispatch: # Allows manual trigger jobs: @@ -19,39 +19,44 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + # see https://github.com/actions/checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v6 + # see https://github.com/actions/setup-node + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '20' - name: Install dependencies - working-directory: tools/src/main/js/bundler + working-directory: tools/src/main/js run: npm install - name: Bundle schemas - working-directory: tools/src/main/js/bundler + working-directory: tools/src/main/js run: | node bundle-schemas.js \ - ../../../../../schema/2.0/model \ - ../../../../../schema/2.0/cyclonedx-2.0.schema.json + ../../../../schema/2.0/model \ + ../../../../schema/2.0/cyclonedx-2.0.schema.json - name: Check for changes and commit + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | BUNDLED_FILE="schema/2.0/cyclonedx-2.0-bundled.schema.json" - MINIFIED_FILE="schema/2.0/cyclonedx-2.0-bundled.min.schema.json" - # Add both files (works for both new and modified files) - git add "$BUNDLED_FILE" "$MINIFIED_FILE" + # Add the file (works for both new and modified files) + git add "$BUNDLED_FILE" # Check if there are staged changes if git diff --staged --quiet; then - echo "No changes to bundled schemas" + echo "No changes to bundled schema" else echo "Committing bundled schema changes" git config --local user.email "github-actions[bot]@users.noreply.github.com" git config --local user.name "github-actions[bot]" - git commit -m "chore: update bundled schemas [skip ci]" - git push + git commit -m "chore: update bundled schema [skip ci]" + git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" fi diff --git a/.github/workflows/generate_algorithm_families.yml b/.github/workflows/generate_algorithm_families.yml index d349edcf9..ae5ee2c53 100644 --- a/.github/workflows/generate_algorithm_families.yml +++ b/.github/workflows/generate_algorithm_families.yml @@ -17,13 +17,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + # see https://github.com/actions/checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + # see https://github.com/actions/setup-python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.x' diff --git a/.github/workflows/test_java.yml b/.github/workflows/test_java.yml index 226e1d797..061300a84 100644 --- a/.github/workflows/test_java.yml +++ b/.github/workflows/test_java.yml @@ -23,10 +23,12 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Set up JDK # see https://github.com/actions/setup-java - uses: actions/setup-java@v5 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '8' distribution: 'zulu' diff --git a/.github/workflows/test_js.yml b/.github/workflows/test_js.yml index f2029ba40..02e5dcdf2 100644 --- a/.github/workflows/test_js.yml +++ b/.github/workflows/test_js.yml @@ -26,10 +26,12 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup Node.js # see https://github.com/actions/setup-node - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24.x' package-manager-cache: false diff --git a/.github/workflows/test_php.yml b/.github/workflows/test_php.yml index 3d044081b..b761bc7b3 100644 --- a/.github/workflows/test_php.yml +++ b/.github/workflows/test_php.yml @@ -26,10 +26,12 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup PHP # see https://github.com/shivammathur/setup-php - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: "8.4" tools: composer:v2 diff --git a/.github/workflows/test_proto.yml b/.github/workflows/test_proto.yml index 7758e146e..30b9acda1 100644 --- a/.github/workflows/test_proto.yml +++ b/.github/workflows/test_proto.yml @@ -26,6 +26,8 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Run test run: ./test.sh diff --git a/.github/workflows/update_spdx_licenses.yml b/.github/workflows/update_spdx_licenses.yml index d751798b0..fe95faa76 100644 --- a/.github/workflows/update_spdx_licenses.yml +++ b/.github/workflows/update_spdx_licenses.yml @@ -23,12 +23,13 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ github.ref_name }} + persist-credentials: false - name: Set up JDK # see https://github.com/actions/setup-java - uses: actions/setup-java@v5 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '21' distribution: 'zulu' @@ -54,7 +55,7 @@ jobs: - name: Artifact changes if: ${{ steps.diff.outputs.changed == 'true' }} # https://github.com/actions/upload-artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 1 name: schema-spdx @@ -74,9 +75,10 @@ jobs: steps: - name: Checkout # see https://github.com/actions/checkout - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 with: ref: ${{ github.ref_name }} + persist-credentials: false - name: Switch branch id: branch run: | @@ -93,11 +95,13 @@ jobs: fi - name: Fetch changes # https://github.com/actions/download-artifact - uses: actions/download-artifact@v5 + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 with: name: schema-spdx path: schema - name: Commit and push + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eux if git diff --quiet -- 'schema/spdx.*' @@ -109,7 +113,7 @@ jobs: git config user.email 'spdx-license-bumper@bot.local' git add -A schema git commit -s -m "feat: bump SPDX licenses $SB_VERSION" - git push origin "$SB_BRANCH" + git push "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$SB_BRANCH" - name: Pull request if: ${{ steps.branch.outputs.existed == 'false' }} run: > diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..03725cafc --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,42 @@ +name: Zizmor + +on: + push: + branches: ['master', 'main'] + pull_request: + branches: ['**'] + workflow_dispatch: + schedule: + - cron: '0 0 * * 6' + +permissions: {} + +concurrency: + group: '${{ github.workflow }}-${{ github.ref }}' + cancel-in-progress: true + +jobs: + zizmor: + name: Zizmor + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout + # see https://github.com/actions/checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Run zizmor 🌈 + # see https://github.com/zizmorcore/zizmor-action + uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + with: + # advanced-security: false => emit findings as workflow-command annotations (::error file=…) rather than + # uploading a SARIF report to GitHub's Security tab. + # Uploading SARIF requires `security-events: write` and GitHub Advanced Security (GHAS), + # both of which are unnecessary here and would violate the least-privilege policy. + # The two modes are mutually exclusive: advanced-security must be false for + # annotations to take effect. + advanced-security: false + annotations: true diff --git a/.gitignore b/.gitignore index 2628a5390..875ca5863 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +# Filesystem +**/.DS_Store + +# Tooling .idea/ .vscode/ tools/target/ +.bob + +# UML diagrams +**/.uml/ diff --git a/docgen/json/gen.sh b/docgen/json/gen.sh index 66db731f8..8357f2ef5 100755 --- a/docgen/json/gen.sh +++ b/docgen/json/gen.sh @@ -26,6 +26,9 @@ SCHEMA_PATH="$(realpath "$THIS_PATH/../../schema")" DOCS_PATH="$THIS_PATH/docs" TEMPLATES_PATH="$THIS_PATH/templates" +# Centralized header injection +source "$THIS_PATH/../static/inject-header.sh" + # -- @@ -63,7 +66,7 @@ generate () { mkdir -p "$OUT_DIR" generate-schema-doc \ - --config no_link_to_reused_ref \ + --config link_to_reused_ref \ --config no_show_breadcrumbs \ --config no_collapse_long_descriptions \ --deprecated-from-description \ @@ -76,6 +79,8 @@ generate () { sed -i -e "s/\${quotedTitle}/\"$title\"/g" "$OUT_FILE" sed -i -e "s/\${title}/$title/g" "$OUT_FILE" sed -i -e "s/\${version}/$version/g" "$OUT_FILE" + + inject_header "$OUT_FILE" "$version" "json" } diff --git a/docgen/json/templates/cyclonedx/base.html b/docgen/json/templates/cyclonedx/base.html index 48dc68ef5..00f172874 100644 --- a/docgen/json/templates/cyclonedx/base.html +++ b/docgen/json/templates/cyclonedx/base.html @@ -18,56 +18,15 @@ - - + + - - - + -
+ - +| Name | Description | diff --git a/docgen/json/templates/cyclonedx/schema_doc.css b/docgen/json/templates/cyclonedx/schema_doc.css index ecf2b5d29..1367c320a 100644 --- a/docgen/json/templates/cyclonedx/schema_doc.css +++ b/docgen/json/templates/cyclonedx/schema_doc.css @@ -5,19 +5,22 @@ body { padding: 0; } .navbar { - height: 90px; + min-height: 90px; padding: 0; } -.navbar-inverse .navbar-nav>.open>a, -.navbar-inverse .navbar-nav>.open>a:focus, -.navbar-inverse .navbar-nav>.open>a:hover, -.navbar-inverse { +.navbar-toggler-icon { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") !important; +} +.navbar-dark .navbar-nav>.open>a, +.navbar-dark .navbar-nav>.open>a:focus, +.navbar-dark .navbar-nav>.open>a:hover, +.navbar-dark { background-image: linear-gradient(269.12deg, rgba(232, 52, 82, 1) 0%, rgba(136, 38, 125, 1) 51.26%, rgba(52, 57, 175, 1) 100%); } -.navbar-brand, .navbar-fixed-top { +.navbar-brand, .fixed-top { padding: 0 30px 0 30px; } -.navbar-inverse .navbar-nav>li>a { +.navbar-dark .navbar-nav>li>a { color: #ffffff; } .site-header__logo img { @@ -26,8 +29,9 @@ body { .version-selector { font-size: 1.2rem } -.table .thead-dark th { +.table .table-dark th { background-color: #323550; + color: #ffffff; } .container { margin-right: auto; @@ -73,15 +77,24 @@ ul .dropdown-menu li { } .card { border-radius: 0; + --bs-card-border-color: rgba(0, 0, 0, 0.125); +} +.accordion + .accordion .card { + margin-top: -1px; } .card-header { padding: 0; } -.card-header .fa { +.card-header .btn .bi { + display: inline-block; transition: .3s transform ease-in-out; + -webkit-text-stroke: 2px; +} +.card-header .btn[aria-expanded="true"] .bi { transform: rotate(90deg); } -.card-header .collapsed .fa { +.card-header .btn[aria-expanded="false"] .bi, +.card-header .btn.collapsed .bi { transform: rotate(0deg); } .btn.btn-link { @@ -136,7 +149,7 @@ ul .dropdown-menu li { content: '- Read Less'; } .badge { - color: #222222; + color: #222222 !important; padding: .1em .4em .2em; margin-right: .2em; font-weight: normal; @@ -144,44 +157,47 @@ ul .dropdown-menu li { border-radius: 0; } .badge.required-property { - background-color: rgba(255,137,29,0.3); + background-color: rgba(255,137,29,0.3) !important; border: 1px solid #FF7F0B; } .badge.value-type { - background-color: rgba(174,206,229,0.3); + background-color: rgba(174,206,229,0.3) !important; border: 1px solid #5C9CCB; } .badge.default-value { - background-color: rgba(175,228,191,0.3); + background-color: rgba(175,228,191,0.3) !important; border: 1px solid #73D08F; } .badge.example { - background-color: rgba(235,202,255,0.3); + background-color: rgba(235,202,255,0.3) !important; border: 1px solid #DA9FFF; } .badge.deprecated-property { - background-color: rgba(255,95,95,0.3); + background-color: rgba(255,95,95,0.3) !important; border: 1px solid #FF3333; } .badge.no-additional { - background-color: rgba(255,82,174,0.3); + background-color: rgba(255,82,174,0.3) !important; border: 1px solid #FF33A0; } .badge.pattern-property { - background-color: rgba(235,229,168,0.3); + background-color: rgba(235,229,168,0.3) !important; border: 1px solid #FFEA1C; } .accordion div.card:only-child { border-bottom: 1px solid rgba(0, 0, 0, 0.125); } .examples { - padding: 1rem !important; + padding: 0 !important; + margin-top: 0.5rem; + margin-bottom: 0.5rem; } .examples pre { margin-bottom: 0; + padding: 1rem 1.5rem; } .highlight.jumbotron { - padding: 1rem !important; + padding: 1rem 1.5rem !important; } .generated-by-footer { margin-top: 1em; @@ -261,4 +277,19 @@ ul .dropdown-menu li { .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ -.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ \ No newline at end of file +.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ + +/* ═══════════════════════════════════════════════════════════ + Inline expansion for reused definitions (ref-links) + ═══════════════════════════════════════════════════════════ */ + +/* Hide the "Same definition as..." link text; content is + cloned inline automatically when the parent row expands. */ +.ref-link[data-ref-expanded="true"] { + display: none; +} + +/* Container for the cloned definition content */ +.ref-expand-content { + margin-top: 0.25rem; +} \ No newline at end of file diff --git a/docgen/json/templates/cyclonedx/schema_doc.js b/docgen/json/templates/cyclonedx/schema_doc.js index e03ce9464..fa9faf5be 100644 --- a/docgen/json/templates/cyclonedx/schema_doc.js +++ b/docgen/json/templates/cyclonedx/schema_doc.js @@ -1,15 +1,26 @@ -$(document).on('click', 'a[href^="#"]', function(event) { - event.preventDefault(); - history.pushState({}, '', this.href); +document.addEventListener('click', function(event) { + var anchor = event.target.closest('a[href^="#"]'); + if (anchor) { + // Skip ref-links; they are replaced by inline expansions + if (anchor.classList.contains('ref-link')) { + event.preventDefault(); + return; + } + // Don't interfere with Bootstrap tabs or collapse toggles + if (anchor.getAttribute('data-bs-toggle')) return; + event.preventDefault(); + history.pushState({}, '', anchor.href); + } }); function flashElement(elementId) { - // $( "#" + elementId ).fadeOut(100).fadeIn(200).fadeOut(100).fadeIn(500); - myElement = document.getElementById(elementId); - myElement.classList.add("jsfh-animated-property"); - setTimeout(function() { - myElement.classList.remove("jsfh-animated-property"); - }, 1000); + var myElement = document.getElementById(elementId); + if (myElement) { + myElement.classList.add("jsfh-animated-property"); + setTimeout(function() { + myElement.classList.remove("jsfh-animated-property"); + }, 1000); + } } function setAnchor(anchorLinkDestination) { @@ -19,7 +30,7 @@ function setAnchor(anchorLinkDestination) { function anchorOnLoad() { // Added to onload on body, checks if there is an anchor link and if so, expand - let linkTarget = decodeURIComponent(window.location.hash.split("?")[0].split("&")[0]); + var linkTarget = decodeURIComponent(window.location.hash.split("?")[0].split("&")[0]); if (linkTarget[0] === "#") { linkTarget = linkTarget.substr(1); } @@ -30,31 +41,35 @@ function anchorOnLoad() { } function anchorLink(linkTarget) { - const target = $( "#" + linkTarget ); - // Find the targeted element to expand and all its parents that can be expanded - target.parents().addBack().filter(".collapse:not(.show), .tab-pane, [role='tab']").each( - function(index) { - if($( this ).hasClass("collapse")) { - $( this ).collapse("show"); - } else if ($( this ).hasClass("tab-pane")) { - // We have the pane and not the tab itself, find the tab - const tabToShow = $( "a[href='#" + $( this ).attr("id") + "']" ); - if (tabToShow) { - tabToShow.tab("show"); - } - } else if ($( this ).attr("role") === "tab") { - // The tab is not a parent of underlying elements, the tab pane is - // However, it can still be linked directly - $( this ).tab("show"); + var target = document.getElementById(linkTarget); + if (!target) return; + + // Find the targeted element and all its parents that can be expanded + var element = target; + while (element) { + // Expand collapsed sections + if (element.classList.contains("collapse") && !element.classList.contains("show")) { + var bsCollapse = new bootstrap.Collapse(element, { toggle: true }); + } + // Activate tab panes + if (element.classList.contains("tab-pane")) { + var tabTrigger = document.querySelector('a[href="#' + element.id + '"]'); + if (tabTrigger) { + var bsTab = new bootstrap.Tab(tabTrigger); + bsTab.show(); } } - ); + // Handle direct tab links + if (element.getAttribute("role") === "tab") { + var bsTab = new bootstrap.Tab(element); + bsTab.show(); + } + element = element.parentElement; + } // Wait a little so the user has time to see the page scroll - // Or maybe it is to be sure everything is expanded before scrolling and I was not able to bind to the bootstrap - // events in a way that works all the time, we may never know setTimeout(function() { - let targetElement = document.getElementById(linkTarget); + var targetElement = document.getElementById(linkTarget); if (targetElement) { targetElement.scrollIntoView({ block: "center", behavior:"smooth" }); // Flash the element so that the user notices where the link points to @@ -63,4 +78,342 @@ function anchorLink(linkTarget) { }, 500); } }, 1000); -} \ No newline at end of file +} + + +// ═══════════════════════════════════════════════════════════ +// Fix duplicate IDs produced by link_to_reused_ref +// ═══════════════════════════════════════════════════════════ +// +// The schema doc generator reuses the same IDs when inlining +// a $ref definition at multiple schema paths. Duplicate IDs +// break Bootstrap tabs/collapses because getElementById +// always returns the first match. This pass finds duplicates +// and rewrites subsequent occurrences so every ID is unique. +// ═══════════════════════════════════════════════════════════ + +(function() { + function fixDuplicateIds() { + var seen = {}; // id -> true for first occurrence + var dupCount = 0; + + // Pass 1: rename duplicate IDs. First occurrence keeps + // its id; subsequent occurrences get a unique suffix. + var allWithId = document.querySelectorAll('[id]'); + allWithId.forEach(function(el) { + var id = el.id; + if (!id) return; + if (seen[id]) { + dupCount++; + el.setAttribute('data-orig-id', id); + el.id = id + '__d' + dupCount; + } else { + seen[id] = true; + } + }); + + if (dupCount === 0) return; + + // Build lookup: origId -> [el, el, ...] for fast scoping + var renamed = {}; + document.querySelectorAll('[data-orig-id]').forEach(function(el) { + var origId = el.getAttribute('data-orig-id'); + if (!renamed[origId]) renamed[origId] = []; + renamed[origId].push(el); + }); + + // Build full candidate list: origId -> [el, ...] including + // both the original (first-occurrence) element and all renamed + // duplicates so that scoping works for every occurrence. + var allTargets = {}; + Object.keys(renamed).forEach(function(origId) { + var orig = document.getElementById(origId); + allTargets[origId] = orig ? [orig].concat(renamed[origId]) : renamed[origId]; + }); + + // Find the target element (original or renamed) that shares + // the closest common ancestor with the referrer. + function findLocalTarget(referrer, origId) { + var candidates = allTargets[origId]; + if (!candidates) return origId; + var scope = referrer.parentElement; + while (scope) { + for (var i = 0; i < candidates.length; i++) { + if (scope.contains(candidates[i])) return candidates[i].id; + } + scope = scope.parentElement; + } + return origId; + } + + // Pass 2: fix references that point to renamed IDs. + function fixHashAttr(el, attr) { + var val = el.getAttribute(attr); + if (!val || val.charAt(0) !== '#') return; + var refId = val.substring(1); + if (!renamed[refId]) return; + var localId = findLocalTarget(el, refId); + if (localId !== refId) el.setAttribute(attr, '#' + localId); + } + + function fixPlainAttr(el, attr) { + var val = el.getAttribute(attr); + if (!val || !renamed[val]) return; + var localId = findLocalTarget(el, val); + if (localId !== val) el.setAttribute(attr, localId); + } + + document.querySelectorAll('a[href^="#"]').forEach(function(el) { + fixHashAttr(el, 'href'); + }); + document.querySelectorAll('[data-bs-target^="#"]').forEach(function(el) { + fixHashAttr(el, 'data-bs-target'); + }); + document.querySelectorAll('[data-bs-parent^="#"]').forEach(function(el) { + fixHashAttr(el, 'data-bs-parent'); + }); + document.querySelectorAll('[aria-controls]').forEach(function(el) { + fixPlainAttr(el, 'aria-controls'); + }); + document.querySelectorAll('[aria-labelledby]').forEach(function(el) { + fixPlainAttr(el, 'aria-labelledby'); + }); + document.querySelectorAll('[onclick]').forEach(function(el) { + var onclick = el.getAttribute('onclick'); + if (!onclick) return; + var changed = false; + var updated = onclick.replace( + /anchorLink\('([^']+)'\)/g, + function(match, id) { + if (!renamed[id]) return match; + var localId = findLocalTarget(el, id); + if (localId !== id) { changed = true; return "anchorLink('" + localId + "')"; } + return match; + } + ).replace( + /setAnchor\('#([^']+)'\)/g, + function(match, id) { + if (!renamed[id]) return match; + var localId = findLocalTarget(el, id); + if (localId !== id) { changed = true; return "setAnchor('#" + localId + "')"; } + return match; + } + ); + if (changed) el.setAttribute('onclick', updated); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', fixDuplicateIds); + } else { + fixDuplicateIds(); + } +})(); + + +// ═══════════════════════════════════════════════════════════ +// Automatic inline expansion for reused definitions +// ═══════════════════════════════════════════════════════════ +// +// When link_to_reused_ref is enabled, repeated definitions +// render as "Same definition as X" links pointing to the +// original. This enhancement hides those links and clones +// the original definition inline automatically when the +// parent property row is expanded. No user click required. +// The full HTML stays in the DOM for SEO crawlability. +// ═══════════════════════════════════════════════════════════ + +(function() { + var expandCounter = 0; + + /** + * Rewrite IDs inside a cloned subtree so they don't collide + * with the originals. Also updates internal href="#...", + * data-bs-target, data-bs-parent, and aria attributes. + */ + function deduplicateIds(container, suffix) { + var elements = container.querySelectorAll('[id]'); + var idMap = {}; + elements.forEach(function(el) { + var oldId = el.id; + var newId = oldId + suffix; + idMap[oldId] = newId; + el.id = newId; + }); + + container.querySelectorAll('[href]').forEach(function(el) { + var href = el.getAttribute('href'); + if (href && href.charAt(0) === '#') { + var refId = href.substring(1); + if (idMap[refId]) { + el.setAttribute('href', '#' + idMap[refId]); + } + } + }); + container.querySelectorAll('[data-bs-target]').forEach(function(el) { + var val = el.getAttribute('data-bs-target'); + if (val && val.charAt(0) === '#') { + var refId = val.substring(1); + if (idMap[refId]) { + el.setAttribute('data-bs-target', '#' + idMap[refId]); + } + } + }); + container.querySelectorAll('[data-bs-parent]').forEach(function(el) { + var val = el.getAttribute('data-bs-parent'); + if (val && val.charAt(0) === '#') { + var refId = val.substring(1); + if (idMap[refId]) { + el.setAttribute('data-bs-parent', '#' + idMap[refId]); + } + } + }); + container.querySelectorAll('[aria-controls]').forEach(function(el) { + var val = el.getAttribute('aria-controls'); + if (val && idMap[val]) { + el.setAttribute('aria-controls', idMap[val]); + } + }); + container.querySelectorAll('[aria-labelledby]').forEach(function(el) { + var val = el.getAttribute('aria-labelledby'); + if (val && idMap[val]) { + el.setAttribute('aria-labelledby', idMap[val]); + } + }); + + container.querySelectorAll('[onclick]').forEach(function(el) { + var onclick = el.getAttribute('onclick'); + if (onclick) { + var updated = onclick.replace( + /anchorLink\('([^']+)'\)/g, + function(match, id) { + return idMap[id] ? "anchorLink('" + idMap[id] + "')" : match; + } + ).replace( + /setAnchor\('#([^']+)'\)/g, + function(match, id) { + return idMap[id] ? "setAnchor('#" + idMap[id] + "')" : match; + } + ); + el.setAttribute('onclick', updated); + } + }); + } + + /** + * Check whether a node is "leading metadata" that already + * appears in the ref-link's container: the type badge + * (span.badge.value-type), a
|---|