From 58441e3e93fdb1e87ab31e50c8addcc19d19815e Mon Sep 17 00:00:00 2001 From: Steve Springett Date: Mon, 9 Mar 2026 20:03:31 -0500 Subject: [PATCH 1/5] Updated documentation generation Signed-off-by: Steve Springett --- docgen/json/gen.sh | 5 + docgen/json/templates/cyclonedx/base.html | 58 +- docgen/json/templates/cyclonedx/content.html | 10 +- .../json/templates/cyclonedx/schema_doc.css | 56 +- docgen/json/templates/cyclonedx/schema_doc.js | 68 +- .../templates/cyclonedx/schema_doc.min.js | 10 +- .../templates/cyclonedx/section_array.html | 4 +- .../section_conditional_subschema.html | 6 +- .../cyclonedx/section_description.html | 2 +- .../templates/cyclonedx/section_examples.html | 2 +- .../cyclonedx/section_properties.html | 20 +- .../templates/cyclonedx/tabbed_section.html | 2 +- docgen/proto/gen.sh | 5 + docgen/proto/templates/html.tmpl | 847 ++++++++++-------- docgen/xml/gen.sh | 5 + docgen/xml/xs3p.xsl | 267 +++--- 16 files changed, 744 insertions(+), 623 deletions(-) diff --git a/docgen/json/gen.sh b/docgen/json/gen.sh index 4eb65326..88169379 100755 --- a/docgen/json/gen.sh +++ b/docgen/json/gen.sh @@ -25,6 +25,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" + # -- @@ -71,6 +74,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 8b7dfb68..00f17287 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,6 +56,7 @@

{{ title }}

--> - + + diff --git a/docgen/json/templates/cyclonedx/content.html b/docgen/json/templates/cyclonedx/content.html index 53bc8f7f..ecfc2362 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 ecf2b5d2..cb73114f 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; diff --git a/docgen/json/templates/cyclonedx/schema_doc.js b/docgen/json/templates/cyclonedx/schema_doc.js index e03ce946..93f1669e 100644 --- a/docgen/json/templates/cyclonedx/schema_doc.js +++ b/docgen/json/templates/cyclonedx/schema_doc.js @@ -1,15 +1,19 @@ -$(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) { + 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 +23,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 +34,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 diff --git a/docgen/json/templates/cyclonedx/schema_doc.min.js b/docgen/json/templates/cyclonedx/schema_doc.min.js index 0c58a70e..a8756dcb 100644 --- a/docgen/json/templates/cyclonedx/schema_doc.min.js +++ b/docgen/json/templates/cyclonedx/schema_doc.min.js @@ -1 +1,9 @@ -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 +document.addEventListener('click',function(event){var anchor=event.target.closest('a[href^="#"]');if(anchor){event.preventDefault();history.pushState({},'',anchor.href);}});function flashElement(elementId){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){history.pushState({},'',anchorLinkDestination);} +function anchorOnLoad(){var linkTarget=decodeURIComponent(window.location.hash.split("?")[0].split("&")[0]);if(linkTarget[0]==="#"){linkTarget=linkTarget.substr(1);} +if(linkTarget.length>0){anchorLink(linkTarget);}} +function anchorLink(linkTarget){var target=document.getElementById(linkTarget);if(!target)return;var element=target;while(element){if(element.classList.contains("collapse")&&!element.classList.contains("show")){var bsCollapse=new bootstrap.Collapse(element,{toggle:true});} +if(element.classList.contains("tab-pane")){var tabTrigger=document.querySelector('a[href="#'+element.id+'"]');if(tabTrigger){var bsTab=new bootstrap.Tab(tabTrigger);bsTab.show();}} +if(element.getAttribute("role")==="tab"){var bsTab=new bootstrap.Tab(element);bsTab.show();} +element=element.parentElement;} +setTimeout(function(){var targetElement=document.getElementById(linkTarget);if(targetElement){targetElement.scrollIntoView({block:"center",behavior:"smooth"});setTimeout(function(){flashElement(linkTarget);},500);}},1000);} \ No newline at end of file diff --git a/docgen/json/templates/cyclonedx/section_array.html b/docgen/json/templates/cyclonedx/section_array.html index 15489a4b..14a7fb4a 100644 --- a/docgen/json/templates/cyclonedx/section_array.html +++ b/docgen/json/templates/cyclonedx/section_array.html @@ -8,7 +8,7 @@ {{ restriction("All items must be unique", "unique-items", schema.kw_unique_items.html_id) }} {%- endif -%} {%- if not schema.array_additional_items -%} - {{ " " }}No 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 e543669d..01ba04d5 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 e0cc3fab..e5ecc5f3 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 06f485e9..b2180910 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 222fd8b6..e07c9a44 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 81ea390b..ce3b2dfa 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/xml/gen.sh b/docgen/xml/gen.sh index 1718a0f4..3aa498b0 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 626ddd11..aec625c5 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 @@ - + + + + + + + +
+ + diff --git a/docgen/static/inject-header.sh b/docgen/static/inject-header.sh new file mode 100755 index 00000000..608b0d86 --- /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 00000000..878c8438 --- /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"] + } + ] + } + ] +} From 18c651a1e940bbb31864f4668a4219dda7e66941 Mon Sep 17 00:00:00 2001 From: Steve Springett Date: Mon, 9 Mar 2026 20:29:21 -0500 Subject: [PATCH 3/5] Fixed potential security issue Signed-off-by: Steve Springett --- docgen/static/generate-menu.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docgen/static/generate-menu.py b/docgen/static/generate-menu.py index ed07296a..82058d22 100644 --- a/docgen/static/generate-menu.py +++ b/docgen/static/generate-menu.py @@ -18,9 +18,22 @@ """ 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.""" + resolved = os.path.realpath(path) + if not resolved.startswith(SCRIPT_DIR + os.sep) and 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.""" @@ -115,7 +128,8 @@ def main(): print("Usage: generate-menu.py ", file=sys.stderr) sys.exit(1) - with open(sys.argv[1], "r") as f: + 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")] From 1badf6bb513a1cdb882737ec4e84a390ba606dc2 Mon Sep 17 00:00:00 2001 From: Steve Springett Date: Mon, 9 Mar 2026 20:33:37 -0500 Subject: [PATCH 4/5] Fixed potential security issue Signed-off-by: Steve Springett --- docgen/static/generate-menu.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docgen/static/generate-menu.py b/docgen/static/generate-menu.py index 82058d22..bf063c93 100644 --- a/docgen/static/generate-menu.py +++ b/docgen/static/generate-menu.py @@ -29,7 +29,11 @@ def validate_input_path(path): """Ensure the input file resolves to within the script's directory.""" resolved = os.path.realpath(path) - if not resolved.startswith(SCRIPT_DIR + os.sep) and resolved != SCRIPT_DIR: + try: + common = os.path.commonpath([resolved, SCRIPT_DIR]) + except ValueError: + common = None + if common != SCRIPT_DIR: print(f"ERROR: Input file must reside in {SCRIPT_DIR}", file=sys.stderr) sys.exit(1) return resolved From 5aef9ae2de0be190aade6c4e5c0d3208fd41d52d Mon Sep 17 00:00:00 2001 From: Steve Springett Date: Mon, 9 Mar 2026 20:38:11 -0500 Subject: [PATCH 5/5] Potential fix for code scanning alert no. 39: Uncontrolled data used in path expression Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Signed-off-by: Steve Springett --- docgen/static/generate-menu.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docgen/static/generate-menu.py b/docgen/static/generate-menu.py index bf063c93..a8d2216f 100644 --- a/docgen/static/generate-menu.py +++ b/docgen/static/generate-menu.py @@ -28,12 +28,14 @@ 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 - if common != SCRIPT_DIR: + # 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