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 fe800cafc..5808d63d8 100644 --- a/.github/workflows/bundle_2.0_schemas.yml +++ b/.github/workflows/bundle_2.0_schemas.yml @@ -19,12 +19,14 @@ 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: 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' @@ -40,6 +42,8 @@ jobs: ../../../../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" @@ -54,5 +58,5 @@ jobs: 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 schema [skip ci]" - git push + 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 4eb653264..8357f2ef5 100755 --- a/docgen/json/gen.sh +++ b/docgen/json/gen.sh @@ -2,6 +2,7 @@ set -eu declare -a CDX_VERSIONS=( + '2.0' '1.7' '1.6' '1.5' @@ -25,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" + # -- @@ -44,11 +48,15 @@ generate () { local title="CycloneDX v${version} JSON Reference" echo "Generating: $title" - local SCHEMA_FILE="$SCHEMA_PATH/bom-${version}.schema.json" - local STRICT_SCHEMA_FILE="$SCHEMA_PATH/bom-${version}-strict.schema.json" - if [ -f "$STRICT_SCHEMA_FILE" ] - then + local SCHEMA_FILE + if [[ "$version" == 1.* ]]; then + SCHEMA_FILE="$SCHEMA_PATH/bom-${version}.schema.json" + local STRICT_SCHEMA_FILE="$SCHEMA_PATH/bom-${version}-strict.schema.json" + if [ -f "$STRICT_SCHEMA_FILE" ]; then SCHEMA_FILE="$STRICT_SCHEMA_FILE" + fi + else + SCHEMA_FILE="$SCHEMA_PATH/${version}/cyclonedx-${version}-bundled.schema.json" fi echo "SCHEMA_FILE: $SCHEMA_FILE" @@ -58,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 \ @@ -71,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/requirements.txt b/docgen/json/requirements.txt index d5e43567e..dbb2b26d0 100644 --- a/docgen/json/requirements.txt +++ b/docgen/json/requirements.txt @@ -1 +1 @@ -json-schema-for-humans==1.3.4 +json-schema-for-humans==1.5.1 diff --git a/docgen/json/templates/cyclonedx/base.html b/docgen/json/templates/cyclonedx/base.html index fd480909b..00f172874 100644 --- a/docgen/json/templates/cyclonedx/base.html +++ b/docgen/json/templates/cyclonedx/base.html @@ -18,54 +18,15 @@ - - + + - - - + - + - +
@@ -75,9 +36,9 @@

${title}

{{ title }}

{%- endif -%} {%- if config.expand_buttons -%} -
- - +
+ +
{%- endif -%} @@ -95,5 +56,7 @@

{{ title }}

--> + + diff --git a/docgen/json/templates/cyclonedx/content.html b/docgen/json/templates/cyclonedx/content.html index 53bc8f7f1..ecfc23627 100644 --- a/docgen/json/templates/cyclonedx/content.html +++ b/docgen/json/templates/cyclonedx/content.html @@ -17,17 +17,17 @@ {# Display type #} {%- if not schema is combining -%} - Type: {{ type_name }} + Type: {{ type_name }} {%- endif -%} {%- if schema.format -%} - Format: {{ schema.format }} + Format: {{ schema.format }} {%- endif -%} {# Display default #} {%- set default_value = schema.default_value -%} {%- if default_value -%} - {{ " " }}Default: {{ default_value }} + {{ " " }}Default: {{ default_value }} {%- endif -%}
@@ -42,7 +42,7 @@ {{ content(schema.refers_to_merged, True) }} {%- else -%} {%- if schema.explicit_no_additional_properties -%} - {{ " " }}No Additional Properties + {{ " " }}No Additional Properties {%- endif -%} {# Combining: allOf, anyOf, oneOf, not #} @@ -64,7 +64,7 @@

Must be one of:

- + 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
, a description span, + * or whitespace text nodes between them. + */ + function isLeadingMeta(node) { + if (node.nodeType === 3) { + // Text node: skip if whitespace-only + return node.textContent.trim() === ''; + } + if (node.nodeType !== 1) return false; + var el = node; + // Type badge, e.g. + if (el.tagName === 'SPAN' && el.classList.contains('value-type')) return true; + //
element right after the type badge + if (el.tagName === 'BR') return true; + // Description span + if (el.tagName === 'SPAN' && el.classList.contains('description')) return true; + return false; + } + + /** + * Clone a source definition into the container that holds + * the ref-link. The ref-link itself is hidden via CSS. + * Leading type badge,
, and description are skipped + * because the container already shows them. + */ + function expandRefLink(link) { + // Skip if already expanded + if (link.getAttribute('data-ref-expanded') === 'true') return; + link.setAttribute('data-ref-expanded', 'true'); + + var targetId = link.getAttribute('href').substring(1); + var source = document.getElementById(targetId); + if (!source) return; + + expandCounter++; + var suffix = '__exp' + expandCounter; + + var content = document.createElement('div'); + content.className = 'ref-expand-content'; + + // Clone child nodes, skipping leading metadata that + // duplicates what the container already displays. + var nodes = source.childNodes; + var pastLeading = false; + for (var i = 0; i < nodes.length; i++) { + if (!pastLeading && isLeadingMeta(nodes[i])) continue; + pastLeading = true; + content.appendChild(nodes[i].cloneNode(true)); + } + + deduplicateIds(content, suffix); + + // Insert the cloned content after the ref-link + link.parentNode.insertBefore(content, link.nextSibling); + } + + /** + * Check whether a ref-link is directly visible within the + * panel that was just shown. Returns false if the link sits + * inside a nested collapse that is still hidden. + */ + function isVisibleInPanel(link, panel) { + var el = link.parentElement; + while (el && el !== panel) { + if (el.classList.contains('collapse') && !el.classList.contains('show')) { + return false; + } + el = el.parentElement; + } + return true; + } + + /** + * When a collapse panel is shown, expand only the ref-links + * that are directly visible (not buried in nested collapses). + */ + function onCollapseShown(e) { + var panel = e.target; + var refLinks = panel.querySelectorAll('.ref-link'); + refLinks.forEach(function(link) { + if (isVisibleInPanel(link, panel)) { + expandRefLink(link); + } + }); + } + + /** + * Initialize: hide ref-link text, listen for collapse events. + */ + function initRefLinks() { + var refLinks = document.querySelectorAll('.ref-link'); + refLinks.forEach(function(link) { + // Remove the original onclick + link.removeAttribute('onclick'); + + // Expand ref-links that are already visible on load + // (not inside any collapsed panel) + var parentCollapse = link.closest('.collapse'); + if (!parentCollapse || parentCollapse.classList.contains('show')) { + expandRefLink(link); + } + }); + + // Listen for Bootstrap collapse show events + document.addEventListener('shown.bs.collapse', onCollapseShown); + } + + // Run on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initRefLinks); + } else { + initRefLinks(); + } +})(); diff --git a/docgen/json/templates/cyclonedx/schema_doc.min.js b/docgen/json/templates/cyclonedx/schema_doc.min.js index 0c58a70ef..a3934ac7c 100644 --- a/docgen/json/templates/cyclonedx/schema_doc.min.js +++ b/docgen/json/templates/cyclonedx/schema_doc.min.js @@ -1 +1 @@ -function flashElement(t){(myElement=document.getElementById(t)).classList.add("jsfh-animated-property"),setTimeout(function(){myElement.classList.remove("jsfh-animated-property")},1e3)}function setAnchor(t){history.pushState({},"",t)}function anchorOnLoad(){let t=decodeURIComponent(window.location.hash.split("?")[0].split("&")[0]);"#"===t[0]&&(t=t.substr(1)),t.length>0&&anchorLink(t)}function anchorLink(t){let e=$("#"+t);e.parents().addBack().filter(".collapse:not(.show), .tab-pane, [role='tab']").each(function(t){if($(this).hasClass("collapse"))$(this).collapse("show");else if($(this).hasClass("tab-pane")){let e=$("a[href='#"+$(this).attr("id")+"']");e&&e.tab("show")}else"tab"===$(this).attr("role")&&$(this).tab("show")}),setTimeout(function(){let e=document.getElementById(t);e&&(e.scrollIntoView({block:"center",behavior:"smooth"}),setTimeout(function(){flashElement(t)},500))},1e3)}$(document).on("click",'a[href^="#"]',function(t){t.preventDefault(),history.pushState({},"",this.href)}); \ No newline at end of file +function flashElement(t){var e=document.getElementById(t);e&&(e.classList.add("jsfh-animated-property"),setTimeout(function(){e.classList.remove("jsfh-animated-property")},1e3))}function setAnchor(t){history.pushState({},"",t)}function anchorOnLoad(){var t=decodeURIComponent(window.location.hash.split("?")[0].split("&")[0]);"#"===t[0]&&(t=t.substr(1)),t.length>0&&anchorLink(t)}function anchorLink(t){var e=document.getElementById(t);if(e){for(var r=e;r;){if(r.classList.contains("collapse")&&!r.classList.contains("show"))new bootstrap.Collapse(r,{toggle:!0});if(r.classList.contains("tab-pane")){var n=document.querySelector('a[href="#'+r.id+'"]');if(n)new bootstrap.Tab(n).show()}if("tab"===r.getAttribute("role"))new bootstrap.Tab(r).show();r=r.parentElement}setTimeout(function(){var e=document.getElementById(t);e&&(e.scrollIntoView({block:"center",behavior:"smooth"}),setTimeout(function(){flashElement(t)},500))},1e3)}}document.addEventListener("click",function(t){var e=t.target.closest('a[href^="#"]');if(e){if(e.classList.contains("ref-link"))return void t.preventDefault();if(e.getAttribute("data-bs-toggle"))return;t.preventDefault(),history.pushState({},"",e.href)}}),function(){function t(){var t={},e=0;if(document.querySelectorAll("[id]").forEach(function(r){var n=r.id;n&&(t[n]?(e++,r.setAttribute("data-orig-id",n),r.id=n+"__d"+e):t[n]=!0)}),0!==e){var r={};document.querySelectorAll("[data-orig-id]").forEach(function(t){var e=t.getAttribute("data-orig-id");r[e]||(r[e]=[]),r[e].push(t)});var n={};Object.keys(r).forEach(function(t){var e=document.getElementById(t);n[t]=e?[e].concat(r[t]):r[t]}),document.querySelectorAll('a[href^="#"]').forEach(function(t){o(t,"href")}),document.querySelectorAll('[data-bs-target^="#"]').forEach(function(t){o(t,"data-bs-target")}),document.querySelectorAll('[data-bs-parent^="#"]').forEach(function(t){o(t,"data-bs-parent")}),document.querySelectorAll("[aria-controls]").forEach(function(t){i(t,"aria-controls")}),document.querySelectorAll("[aria-labelledby]").forEach(function(t){i(t,"aria-labelledby")}),document.querySelectorAll("[onclick]").forEach(function(t){var e=t.getAttribute("onclick");if(e){var n=!1,o=e.replace(/anchorLink\('([^']+)'\)/g,function(e,o){if(!r[o])return e;var i=a(t,o);return i!==o?(n=!0,"anchorLink('"+i+"')"):e}).replace(/setAnchor\('#([^']+)'\)/g,function(e,o){if(!r[o])return e;var i=a(t,o);return i!==o?(n=!0,"setAnchor('#"+i+"')"):e});n&&t.setAttribute("onclick",o)}})}function a(t,e){var r=n[e];if(!r)return e;for(var a=t.parentElement;a;){for(var o=0;oNo Additional Items
+ {{ " " }}No Additional Items {%- endif -%} {%- if schema.array_items_def -%}

Each item of this array must be:

@@ -24,7 +24,7 @@

Tuple Validation

Item at {{ loop.index }} must be:
- + {{ content(item) }}
diff --git a/docgen/json/templates/cyclonedx/section_conditional_subschema.html b/docgen/json/templates/cyclonedx/section_conditional_subschema.html index e543669d9..01ba04d58 100644 --- a/docgen/json/templates/cyclonedx/section_conditional_subschema.html +++ b/docgen/json/templates/cyclonedx/section_conditional_subschema.html @@ -7,7 +7,7 @@

{% set tab_id = schema.kw_if.html_id %} @@ -16,7 +16,7 @@

{% set tab_id = schema.kw_then.html_id %} @@ -26,7 +26,7 @@

{%- set tab_id = schema.kw_else.html_id -%} diff --git a/docgen/json/templates/cyclonedx/section_description.html b/docgen/json/templates/cyclonedx/section_description.html index e0cc3fab5..e5ecc5f34 100644 --- a/docgen/json/templates/cyclonedx/section_description.html +++ b/docgen/json/templates/cyclonedx/section_description.html @@ -8,7 +8,7 @@ {{ description }}
-
diff --git a/docgen/json/templates/cyclonedx/section_examples.html b/docgen/json/templates/cyclonedx/section_examples.html index 06f485e93..b21809102 100644 --- a/docgen/json/templates/cyclonedx/section_examples.html +++ b/docgen/json/templates/cyclonedx/section_examples.html @@ -6,7 +6,7 @@ {%- set example_id = schema.html_id ~ "_ex" ~ loop.index -%} {%- set example_is_long = example is not description_short -%} {%- if example_is_long -%} - + {%- endif -%}
{%- if not examples_as_yaml -%} diff --git a/docgen/json/templates/cyclonedx/section_properties.html b/docgen/json/templates/cyclonedx/section_properties.html index 222fd8b61..e07c9a445 100644 --- a/docgen/json/templates/cyclonedx/section_properties.html +++ b/docgen/json/templates/cyclonedx/section_properties.html @@ -3,10 +3,10 @@

-

@@ -29,13 +29,13 @@

-
+ data-bs-parent="#accordion{{ html_id }}"> +
{%- if sub_property.is_pattern_property -%}

-

All property whose name matches the following regular expression must respect the following conditions

+

All properties whose name matches the following regular expression must respect the following conditions

Property name regular expression: {{ sub_property.property_name | escape }}
{%- endif -%} @@ -52,4 +52,4 @@

-

+
\ No newline at end of file diff --git a/docgen/json/templates/cyclonedx/tabbed_section.html b/docgen/json/templates/cyclonedx/tabbed_section.html index 81ea390bf..ce3b2dfaa 100644 --- a/docgen/json/templates/cyclonedx/tabbed_section.html +++ b/docgen/json/templates/cyclonedx/tabbed_section.html @@ -9,7 +9,7 @@

{%- for node in current_node.array_items -%}

Name Description
- - - - - {{range .Fields}} - - - - - - - {{end}} - -
FieldTypeLabelDescription
{{.Name}}{{.LongType}}{{.Label}}

{{if (index .Options "deprecated"|default false)}}Deprecated. {{end}}{{.Description}} {{if .DefaultValue}}Default: {{.DefaultValue}}{{end}}

- - {{$message := .}} - {{- range .FieldOptions}} - {{$option := .}} - {{if eq . "validator.field" "validate.rules" }} -

Validated Fields

- - - - - - - - - {{range $message.FieldsWithOption .}} - - - - - {{end}} - -
FieldValidations
{{.Name}} -
    - {{range (.Option $option).Rules}} -
  • {{.Name}}: {{.Value}}
  • - {{end}} -
-
- {{else}} -

Fields with {{.}} option

- - - - - - - - - {{range $message.FieldsWithOption .}} - - - - - {{end}} - -
NameOption
{{.Name}}

{{ printf "%+v" (.Option $option)}}

- {{end}} - {{end -}} - {{end}} - - {{if .HasExtensions}} -
- - - - - - {{range .Extensions}} - - - - - - - - {{end}} - -
ExtensionTypeBaseNumberDescription
{{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

{{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

- {{end}} +
+
+
+

+ +

+
+
+
+ {{p .Description}} + + {{if .HasFields}} + + + + + + {{range .Fields}} + + + + + + + {{end}} + +
FieldTypeLabelDescription
{{.Name}}{{.LongType}}{{.Label}}

{{if (index .Options "deprecated"|default false)}}Deprecated. {{end}}{{.Description}} {{if .DefaultValue}}Default: {{.DefaultValue}}{{end}}

+ + {{$message := .}} + {{- range .FieldOptions}} + {{$option := .}} + {{if eq . "validator.field" "validate.rules" }} +

Validated Fields

+ + + + + + + + + {{range $message.FieldsWithOption .}} + + + + + {{end}} + +
FieldValidations
{{.Name}} +
    + {{range (.Option $option).Rules}} +
  • {{.Name}}: {{.Value}}
  • + {{end}} +
+
+ {{else}} +

Fields with {{.}} option

+ + + + + + + + + {{range $message.FieldsWithOption .}} + + + + + {{end}} + +
NameOption
{{.Name}}

{{ printf "%+v" (.Option $option)}}

+ {{end}} + {{end -}} + {{end}} + + {{if .HasExtensions}} +
+ + + + + + {{range .Extensions}} + + + + + + + + {{end}} + +
ExtensionTypeBaseNumberDescription
{{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

{{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

+ {{end}} +
+
+
+
{{end}} {{range .Enums}} -

{{.LongName}}

- {{p .Description}} - - - - - - {{range .Values}} - - - - - - {{end}} - -
NameNumberDescription
{{.Name}}{{.Number}}

{{.Description}}

+
+
+
+

+ +

+
+
+
+ {{p .Description}} + + + + + + {{range .Values}} + + + + + + {{end}} + +
NameNumberDescription
{{.Name}}{{.Number}}

{{.Description}}

+
+
+
+
{{end}} {{if .HasExtensions}} -

File-level Extensions

- - - - - - {{range .Extensions}} - - - - - - - - {{end}} - -
ExtensionTypeBaseNumberDescription
{{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

{{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

+
+
+
+

+ +

+
+
+
+ + + + + + {{range .Extensions}} + + + + + + + + {{end}} + +
ExtensionTypeBaseNumberDescription
{{.Name}}{{.LongType}}{{.ContainingLongType}}{{.Number}}

{{.Description}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}}

+
+
+
+
{{end}} {{range .Services}} -

{{.Name}}

- {{p .Description}} - - - - - - {{range .Methods}} - - - - - - - {{end}} - -
Method NameRequest TypeResponse TypeDescription
{{.Name}}{{.RequestLongType}}{{if .RequestStreaming}} stream{{end}}{{.ResponseLongType}}{{if .ResponseStreaming}} stream{{end}}

{{.Description}}

- - {{$service := .}} - {{- range .MethodOptions}} - {{$option := .}} - {{if eq . "google.api.http"}} -

Methods with HTTP bindings

- - - - - - - - - - - {{range $service.MethodsWithOption .}} - {{$name := .Name}} - {{range (.Option $option).Rules}} - - - - - - - {{end}} - {{end}} - -
Method NameMethodPatternBody
{{$name}}{{.Method}}{{.Pattern}}{{.Body}}
- {{else}} -

Methods with {{.}} option

- - - - - - - - - {{range $service.MethodsWithOption .}} - - - - - {{end}} - -
Method NameOption
{{.Name}}

{{ printf "%+v" (.Option $option)}}

- {{end}} - {{end -}} +
+
+
+

+ +

+
+
+
+ {{p .Description}} + + + + + + {{range .Methods}} + + + + + + + {{end}} + +
Method NameRequest TypeResponse TypeDescription
{{.Name}}{{.RequestLongType}}{{if .RequestStreaming}} stream{{end}}{{.ResponseLongType}}{{if .ResponseStreaming}} stream{{end}}

{{.Description}}

+ + {{$service := .}} + {{- range .MethodOptions}} + {{$option := .}} + {{if eq . "google.api.http"}} +

Methods with HTTP bindings

+ + + + + + + + + + + {{range $service.MethodsWithOption .}} + {{$name := .Name}} + {{range (.Option $option).Rules}} + + + + + + + {{end}} + {{end}} + +
Method NameMethodPatternBody
{{$name}}{{.Method}}{{.Pattern}}{{.Body}}
+ {{else}} +

Methods with {{.}} option

+ + + + + + + + + {{range $service.MethodsWithOption .}} + + + + + {{end}} + +
Method NameOption
{{.Name}}

{{ printf "%+v" (.Option $option)}}

+ {{end}} + {{end -}} +
+
+
+
{{end}} {{end}} -

Scalar Value Types

- - - - - - {{range .Scalars}} - - - - - - - - - - - - {{end}} - -
.proto TypeNotesC++JavaPythonGoC#PHPRuby
{{.ProtoType}}{{.Notes}}{{.CppType}}{{.JavaType}}{{.PythonType}}{{.GoType}}{{.CSharp}}{{.PhpType}}{{.RubyType}}
+
+
+
+

+ +

+
+
+
+ + + + + + {{range .Scalars}} + + + + + + + + + + + + {{end}} + +
.proto TypeNotesC++JavaPythonGoC#PHPRuby
{{.ProtoType}}{{.Notes}}{{.CppType}}{{.JavaType}}{{.PythonType}}{{.GoType}}{{.CSharp}}{{.PhpType}}{{.RubyType}}
+
+
+
+
+ + diff --git a/docgen/static/generate-menu.py b/docgen/static/generate-menu.py new file mode 100644 index 000000000..a8d2216f3 --- /dev/null +++ b/docgen/static/generate-menu.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +generate-menu.py — Generate mega menu HTML from releases.json + +Usage: + python3 generate-menu.py + +Reads the releases data and outputs the mega menu inner HTML +(sidebar + content panels) to stdout. + +Each release can have: + "featured": true — renders as a featured card (gradient, description) + "skip": true — excluded from output + +Groups can also have: + "skip": true — entire group excluded + "note": "..." — footnote shown below all releases in the panel +""" + +import json +import os +import sys +import html + +# This script's directory is the only allowed location for input files +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def validate_input_path(path): + """Ensure the input file resolves to within the script's directory.""" + # Resolve symlinks and remove any ".." components to get a canonical path + resolved = os.path.realpath(path) + try: + common = os.path.commonpath([resolved, SCRIPT_DIR]) + except ValueError: + common = None + # Require the resolved path to be strictly inside SCRIPT_DIR (not equal to it) + if common != SCRIPT_DIR or resolved == SCRIPT_DIR: + print(f"ERROR: Input file must reside in {SCRIPT_DIR}", file=sys.stderr) + sys.exit(1) + return resolved + + +def fmt_label(fmt): + """Format code to display label.""" + return {"json": "JSON", "xml": "XML", "proto": "Proto"}.get(fmt, fmt.upper()) + + +def render_sidebar(groups): + lines = ['
'] + for group in groups: + lines.append(f'') + lines.append(f'') + lines.append(f'{html.escape(group["label"])}') + lines.append(f'{group["desc"]}') + lines.append('') + lines.append('
') + return "\n".join(lines) + + +def render_featured_release(rel): + """Render a single release as a featured card.""" + lines = [] + formats = rel.get("formats", []) + lines.append('') + return "\n".join(lines) + + +def render_card_release(rel): + """Render a single release as a standard version card.""" + lines = [] + formats = rel.get("formats", []) + lines.append('
') + lines.append(f'
v{rel["version"]}
') + lines.append(f'
{html.escape(rel["date"])}
') + if "ecma" in rel: + lines.append(f'
{html.escape(rel["ecma"])}
') + else: + lines.append('
') + if formats: + lines.append('
') + for i, fmt in enumerate(formats): + fmt_href = f"/docs/{rel['version']}/{fmt}/" + cls = "vc-fmt vc-fmt-default" if i == 0 else "vc-fmt" + lines.append(f'{fmt_label(fmt)}') + lines.append('
') + lines.append('
') + return "\n".join(lines) + + +def render_panel(group): + """Render a panel with a mix of featured and standard cards.""" + lines = [f'
'] + + featured = [r for r in group["releases"] if r.get("featured")] + cards = [r for r in group["releases"] if not r.get("featured")] + + for rel in featured: + lines.append(render_featured_release(rel)) + + if cards: + lines.append('
') + for rel in cards: + lines.append(render_card_release(rel)) + lines.append('
') + + if "note" in group: + note = html.escape(group["note"]) + lines.append(f'

{note}

') + + lines.append('
') + return "\n".join(lines) + + +def main(): + if len(sys.argv) != 2: + print("Usage: generate-menu.py ", file=sys.stderr) + sys.exit(1) + + input_path = validate_input_path(sys.argv[1]) + with open(input_path, "r") as f: + data = json.load(f) + + groups = [g for g in data["groups"] if not g.get("skip")] + for group in groups: + group["releases"] = [r for r in group["releases"] if not r.get("skip")] + sidebar_html = render_sidebar(groups) + + panels = [] + for group in groups: + panels.append(render_panel(group)) + + content_html = "\n".join([ + '
', + "\n".join(panels), + '
' + ]) + + print(sidebar_html) + print(content_html) + + +if __name__ == "__main__": + main() diff --git a/docgen/static/header.html b/docgen/static/header.html new file mode 100644 index 000000000..1e7402de9 --- /dev/null +++ b/docgen/static/header.html @@ -0,0 +1,648 @@ + + + + + + + +
+ + diff --git a/docgen/static/inject-header.sh b/docgen/static/inject-header.sh new file mode 100755 index 000000000..608b0d865 --- /dev/null +++ b/docgen/static/inject-header.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# ═══════════════════════════════════════════════════════════ +# inject-header.sh — Inject centralized mega menu header +# ═══════════════════════════════════════════════════════════ +# +# Source this file from any gen.sh, then call: +# +# inject_header +# +# Arguments: +# output_file — Path to the generated HTML file +# version — Spec version, e.g. "2.0" or "1.7" +# format — Serialization format: "json", "xml", or "proto" +# +# The output file must contain the placeholder: +# +# +# This function: +# 1. Runs generate-menu.py to build mega menu HTML from releases.json +# 2. Inserts the generated content into header.html (at ${MEGA_MENU_PANELS}) +# 3. Replaces version/format tokens +# 4. Injects the result at the placeholder location in the output file +# ═══════════════════════════════════════════════════════════ + +inject_header() { + local output_file="$1" + local version="$2" + local format="$3" + + # Resolve path to the static directory (relative to this script) + local HEADER_DIR + HEADER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + if [ ! -f "$HEADER_DIR/header.html" ]; then + echo "ERROR: header.html not found in $HEADER_DIR" >&2 + return 1 + fi + if [ ! -f "$HEADER_DIR/releases.json" ]; then + echo "ERROR: releases.json not found in $HEADER_DIR" >&2 + return 1 + fi + if [ ! -f "$HEADER_DIR/generate-menu.py" ]; then + echo "ERROR: generate-menu.py not found in $HEADER_DIR" >&2 + return 1 + fi + + # ── Compute display labels ── + + local formatLabel + case "$format" in + json) formatLabel="JSON" ;; + xml) formatLabel="XML" ;; + proto) formatLabel="Protobuf" ;; + *) + echo "ERROR: Unknown format '$format' (expected json, xml, or proto)" >&2 + return 1 + ;; + esac + + # Nav label: "CycloneDX 2.0" for modern, "CycloneDX 1.7 (JSON)" for classic + local navLabel + if [[ "$version" == 1.* ]]; then + navLabel="CycloneDX ${version} (${formatLabel})" + else + navLabel="CycloneDX ${version}" + fi + + # ── Generate mega menu panels from releases.json ── + + local tmppanels + tmppanels=$(mktemp) + if ! python3 "$HEADER_DIR/generate-menu.py" "$HEADER_DIR/releases.json" > "$tmppanels"; then + echo "ERROR: generate-menu.py failed" >&2 + rm -f "$tmppanels" + return 1 + fi + + # ── Build the complete header: insert panels into header.html, then replace tokens ── + + local tmpheader + tmpheader=$(mktemp) + + # First: insert generated panels at ${MEGA_MENU_PANELS} placeholder + sed \ + -e '/\${MEGA_MENU_PANELS}/r '"$tmppanels" \ + -e '/\${MEGA_MENU_PANELS}/d' \ + "$HEADER_DIR/header.html" > "$tmpheader" + + rm -f "$tmppanels" + + # Second: replace version/format tokens + local tmpheader2 + tmpheader2=$(mktemp) + sed \ + -e 's|\${navLabel}|'"$navLabel"'|g' \ + -e 's|\${version}|'"$version"'|g' \ + -e 's|\${format}|'"$format"'|g' \ + -e 's|\${formatLabel}|'"$formatLabel"'|g' \ + "$tmpheader" > "$tmpheader2" + + rm -f "$tmpheader" + + # ── Inject into output file at placeholder ── + + if ! grep -q '' "$output_file"; then + echo "WARNING: Placeholder '' not found in $output_file" >&2 + rm -f "$tmpheader2" + return 1 + fi + + local tmpout + tmpout=$(mktemp) + sed \ + -e '//r '"$tmpheader2" \ + -e '//d' \ + "$output_file" > "$tmpout" && mv "$tmpout" "$output_file" + + rm -f "$tmpheader2" + echo " Injected header: v$version ($formatLabel)" +} diff --git a/docgen/static/releases.json b/docgen/static/releases.json new file mode 100644 index 000000000..878c8438f --- /dev/null +++ b/docgen/static/releases.json @@ -0,0 +1,72 @@ +{ + "groups": [ + { + "id": "panel-tel", + "section": "Future", + "label": "Transparency Exchange Language", + "desc": "v2.0", + "skip": false, + "note": "The CycloneDX Transparency Exchange Language is a superset of CycloneDX BOM. Everything you know and love, reimagined and expanded.", + "releases": [ + { + "version": "2.0", + "date": "Coming in 2026", + "featured": true, + "description": "A modular file format and API specification that unifies Bill of Materials with complete supply chain transparency including architectural blueprints, threat, behavioral, and risk modeling, AI and agentic capabilities, compliance attestations, and post-quantum cryptography readiness." + } + ] + }, + { + "id": "panel-bom", + "section": "Stable", + "label": "Bill of Materials", + "desc": "v1.0 – v1.7", + "releases": [ + { + "version": "1.7", + "date": "October 2025", + "featured": true, + "description": "The international standard for inventorying software, hardware, services, cryptographic assets, and AI models with dependency graphs, vulnerability disclosures, licensing, build formulation, and assembly completeness.", + "ecma": "ECMA-424, 2nd Edition", + "formats": ["json", "xml", "proto"] + }, + { + "version": "1.6", + "date": "April 2024", + "ecma": "ECMA-424, 1st Edition", + "formats": ["json", "xml", "proto"] + }, + { + "version": "1.5", + "date": "June 2023", + "formats": ["json", "xml", "proto"] + }, + { + "version": "1.4", + "date": "January 2022", + "formats": ["json", "xml", "proto"] + }, + { + "version": "1.3", + "date": "May 2021", + "formats": ["json", "xml", "proto"] + }, + { + "version": "1.2", + "date": "May 2020", + "formats": ["json", "xml"] + }, + { + "version": "1.1", + "date": "March 2019", + "formats": ["xml"] + }, + { + "version": "1.0", + "date": "March 2018", + "formats": ["xml"] + } + ] + } + ] +} diff --git a/docgen/xml/gen.sh b/docgen/xml/gen.sh index 1718a0f4a..3aa498b03 100755 --- a/docgen/xml/gen.sh +++ b/docgen/xml/gen.sh @@ -26,6 +26,9 @@ THIS_PATH="$(realpath "$(dirname "$0")")" SCHEMA_PATH="$(realpath "$THIS_PATH/../../schema")" DOCS_PATH="$THIS_PATH/docs" +# Centralized header injection +source "$THIS_PATH/../static/inject-header.sh" + SAXON_VERSION='10.9' @@ -59,6 +62,8 @@ generate () { -o:"$OUT_FILE" \ cycloneDxVersion="$version" \ title="$title" + + inject_header "$OUT_FILE" "$version" "xml" } diff --git a/docgen/xml/xs3p.xsl b/docgen/xml/xs3p.xsl index a85e3c4d6..7b246c0b4 100644 --- a/docgen/xml/xs3p.xsl +++ b/docgen/xml/xs3p.xsl @@ -158,13 +158,10 @@ specific CSS, not the Bootstrap CSS. --> - - https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js - - https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.1 + /js/bootstrap.bundle.min.js must exist.--> + https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist 0.0 @@ -299,7 +296,8 @@ - + +