From a87fdc2ff784d6f53039e894065c91223fbdcf96 Mon Sep 17 00:00:00 2001 From: Vladislav Yakimchik <31860804+vyakimchik@users.noreply.github.com> Date: Sun, 9 Aug 2026 18:14:05 +0300 Subject: [PATCH 1/7] Added enums to menu --- README.md | 2 +- plugins/enum-tables.js | 151 +++++++++++++++++++++++++++++++++++++++++ redocly.docs.yml | 5 ++ scripts/build-docs.sh | 30 ++++++++ templates/redoc.hbs | 37 ++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 plugins/enum-tables.js create mode 100644 redocly.docs.yml create mode 100755 scripts/build-docs.sh create mode 100644 templates/redoc.hbs diff --git a/README.md b/README.md index af50ffdc..6b8926a5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli validate - ## Building Redoc single page html documentation ```bash -npx @redocly/cli build-docs index.yml -o=document-reader-static-doc.html +./scripts/build-docs.sh ``` ## Bundle scheme to single .json file diff --git a/plugins/enum-tables.js b/plugins/enum-tables.js new file mode 100644 index 00000000..80020288 --- /dev/null +++ b/plugins/enum-tables.js @@ -0,0 +1,151 @@ +const ENUM_TABLE_MARKER = '| Value | Name | Description |'; +const ENUM_TAG = 'Enums'; + +function escapeTableCell(value) { + return String(value) + .replaceAll('\\', '\\\\') + .replaceAll('|', '\\|') + .replaceAll(/\r?\n/g, '
'); +} + +function enumReference(name, schema) { + const reference = { + type: schema.type, + description: `See [${name}](#schema/${encodeURIComponent(name)}) enumeration values.`, + }; + + if (schema.format) { + reference.format = schema.format; + } + + if (schema.nullable !== undefined) { + reference.nullable = schema.nullable; + } + + return reference; +} + +function addEnumTable(schema) { + const values = schema.enum; + + if (!Array.isArray(values) || schema.description?.includes(ENUM_TABLE_MARKER)) { + return; + } + + const descriptions = schema['x-enum-descriptions']; + const variableNames = schema['x-enum-varnames']; + const rows = values.map((value, index) => { + const variableName = variableNames?.[index] ?? ''; + const description = descriptions?.[index] ?? ''; + + return `| ${escapeTableCell(value)} | ${escapeTableCell(variableName)} | ${escapeTableCell(description)} |`; + }); + const table = [ + ENUM_TABLE_MARKER, + '| --- | --- | --- |', + ...rows, + ].join('\n'); + + schema.description = schema.description + ? `${schema.description}\n\n${table}` + : table; + + // The table replaces Redoc's compact, single-line enum value list. + delete schema.enum; + schema['x-tags'] = [...new Set([...(schema['x-tags'] ?? []), ENUM_TAG])]; +} + +function replaceEnumReferences(node, enumSchemas, visited = new WeakSet()) { + if (!node || typeof node !== 'object' || visited.has(node)) { + return; + } + + visited.add(node); + + if (typeof node.$ref === 'string') { + const prefix = '#/components/schemas/'; + + if (node.$ref.startsWith(prefix)) { + const name = decodeURIComponent(node.$ref.slice(prefix.length)); + const enumSchema = enumSchemas.get(name); + + if (enumSchema) { + const existingDescription = node.description; + + for (const key of Object.keys(node)) { + delete node[key]; + } + + Object.assign(node, enumReference(name, enumSchema)); + + if (existingDescription) { + node.description = `${existingDescription}\n\n${node.description}`; + } + + return; + } + } + } + + for (const value of Object.values(node)) { + replaceEnumReferences(value, enumSchemas, visited); + } +} + +function addEnumNavigation(root) { + const schemas = root.components?.schemas ?? {}; + const schemaEntries = Object.entries(schemas); + const enumEntries = schemaEntries + .filter(([, schema]) => Array.isArray(schema.enum)) + .sort(([left], [right]) => { + const normalizedOrder = left + .toLocaleLowerCase('en') + .localeCompare(right.toLocaleLowerCase('en')); + + return normalizedOrder || left.localeCompare(right); + }); + const enumSchemas = new Map(enumEntries); + + root.components.schemas = Object.fromEntries([ + ...schemaEntries.filter(([, schema]) => !Array.isArray(schema.enum)), + ...enumEntries, + ]); + + for (const schema of enumSchemas.values()) { + addEnumTable(schema); + } + + replaceEnumReferences(root.paths, enumSchemas); + replaceEnumReferences(root.components, enumSchemas); + + root.tags ??= []; + if (!root.tags.some((tag) => tag.name === ENUM_TAG)) { + root.tags.push({ + name: ENUM_TAG, + description: 'Enumeration definitions.', + }); + } + + root['x-tagGroups'] ??= []; + if (!root['x-tagGroups'].some((group) => group.tags?.includes(ENUM_TAG))) { + root['x-tagGroups'].push({ + name: 'Definitions', + tags: [ENUM_TAG], + }); + } +} + +export default function enumTablesPlugin() { + return { + id: 'enum-tables', + decorators: { + oas3: { + 'for-redoc': () => ({ + Root: { + enter: addEnumNavigation, + }, + }), + }, + }, + }; +} diff --git a/redocly.docs.yml b/redocly.docs.yml new file mode 100644 index 00000000..7a502d69 --- /dev/null +++ b/redocly.docs.yml @@ -0,0 +1,5 @@ +plugins: + - ./plugins/enum-tables.js + +decorators: + enum-tables/for-redoc: on diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh new file mode 100755 index 00000000..1d2dd0cd --- /dev/null +++ b/scripts/build-docs.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh + +set -eu + +output=${1:-document-reader-static-doc.html} +bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/document-reader-docs.XXXXXX") +source_bundle="$bundle_dir/source.yml" +docs_bundle="$bundle_dir/docs.yml" + +cleanup() { + rm -f "$source_bundle" "$docs_bundle" + rmdir "$bundle_dir" +} + +trap cleanup EXIT HUP INT TERM + +run_redocly() { + if command -v redocly >/dev/null 2>&1; then + redocly "$@" + else + npx @redocly/cli "$@" + fi +} + +run_redocly bundle index.yml --config redocly.yml --output "$source_bundle" +run_redocly bundle "$source_bundle" --config redocly.docs.yml --output "$docs_bundle" +run_redocly build-docs "$docs_bundle" \ + --config redocly.yml \ + --template templates/redoc.hbs \ + --output "$output" diff --git a/templates/redoc.hbs b/templates/redoc.hbs new file mode 100644 index 00000000..6a25ff67 --- /dev/null +++ b/templates/redoc.hbs @@ -0,0 +1,37 @@ + + + + + + {{title}} + + + {{{redocHead}}} + {{#unless disableGoogleFont}}{{/unless}} + + + + {{{redocHTML}}} + + + From 158fdeefec3109035849f25d70f7993f49ff964b Mon Sep 17 00:00:00 2001 From: Vladislav Yakimchik <31860804+vyakimchik@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:00:55 +0300 Subject: [PATCH 2/7] Updated workflow --- .github/workflows/validate-spec.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate-spec.yml b/.github/workflows/validate-spec.yml index 82ba18c5..9a2799b4 100644 --- a/.github/workflows/validate-spec.yml +++ b/.github/workflows/validate-spec.yml @@ -17,13 +17,13 @@ jobs: - name: Setup Node uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 22 registry-url: https://registry.npmjs.org/ - name: Install redoc run: | npm install -g @redocly/cli - redocly build-docs index.yml + ./scripts/build-docs.sh /tmp/document-reader-static-doc.html - name: Revert changes if: ${{ false }} From 752200736540341cdbbe38b25aa30f0001f9f842 Mon Sep 17 00:00:00 2001 From: Artsiom Tsybulko Date: Fri, 21 Aug 2026 12:20:35 +0300 Subject: [PATCH 3/7] Add normalize_yaml --- README.md | 3 +++ scripts/build-docs.sh | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b8926a5..26daa849 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli validate - ./scripts/build-docs.sh ``` +The build script normalizes multiline quoted YAML strings in a temporary directory. +Source YAML files are not modified during the build. + ## Bundle scheme to single .json file ```bash diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index 1d2dd0cd..c1756f6b 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -4,10 +4,12 @@ set -eu output=${1:-document-reader-static-doc.html} bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/document-reader-docs.XXXXXX") +normalized_dir="$bundle_dir/normalized" source_bundle="$bundle_dir/source.yml" docs_bundle="$bundle_dir/docs.yml" cleanup() { + rm -rf "$normalized_dir" rm -f "$source_bundle" "$docs_bundle" rmdir "$bundle_dir" } @@ -22,7 +24,42 @@ run_redocly() { fi } -run_redocly bundle index.yml --config redocly.yml --output "$source_bundle" +normalize_yaml() { + awk ' + BEGIN { quote = "" } + quote != "" { + print " " $0 + if ((quote == "\"" && $0 ~ /"[[:space:]]*$/) || + (quote == "\047" && $0 ~ /\047[[:space:]]*$/)) { + quote = "" + } + next + } + { + print + if ($0 ~ /^[[:space:]]*(description|example|title|summary):[[:space:]]*"/ && + $0 !~ /"[[:space:]]*$/) { + quote = "\"" + } else if ($0 ~ /^[[:space:]]*(description|example|title|summary):[[:space:]]*\047/ && + $0 !~ /\047[[:space:]]*$/) { + quote = "\047" + } + } + ' "$1" > "$2" +} + +mkdir "$normalized_dir" +find . \ + -path './.git' -prune -o \ + -path './node_modules' -prune -o \ + -type f \( -name '*.yml' -o -name '*.yaml' \) -print | +while IFS= read -r yaml_file; do + normalized_file="$normalized_dir/${yaml_file#./}" + mkdir -p "$(dirname "$normalized_file")" + normalize_yaml "$yaml_file" "$normalized_file" +done + +run_redocly bundle "$normalized_dir/index.yml" --config redocly.yml --output "$source_bundle" run_redocly bundle "$source_bundle" --config redocly.docs.yml --output "$docs_bundle" run_redocly build-docs "$docs_bundle" \ --config redocly.yml \ From a9d56de0fbbc88dcd92e157ea54e51f5193add53 Mon Sep 17 00:00:00 2001 From: Vladislav Yakimchik Date: Fri, 21 Aug 2026 15:17:49 +0300 Subject: [PATCH 4/7] update readme --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 26daa849..392b6bfa 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,7 @@ docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli validate - ./scripts/build-docs.sh ``` -The build script normalizes multiline quoted YAML strings in a temporary directory. -Source YAML files are not modified during the build. +The build script normalizes multiline quoted YAML strings in a temporary directory. Source YAML files are not modified during the build. ## Bundle scheme to single .json file From 0431d51027131cb3eeef98569c6d672a9e3f8627 Mon Sep 17 00:00:00 2001 From: Artsiom Tsybulko Date: Fri, 21 Aug 2026 15:22:35 +0300 Subject: [PATCH 5/7] Add SAST exclude xss rule --- .github/workflows/sast.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sast.yaml b/.github/workflows/sast.yaml index ffe3beaa..871cbdb0 100644 --- a/.github/workflows/sast.yaml +++ b/.github/workflows/sast.yaml @@ -28,7 +28,7 @@ jobs: # Exclude existing .github/workflows/*.yml — they use mutable action tags. # Pinning those files is tracked separately. EXCLUDE_PATHS: '.github/workflows' - EXCLUDE_RULES: '' + EXCLUDE_RULES: 'javascript.express.security.audit.xss.mustache.explicit-unescape.template-explicit-unescape' run: | EXCLUDED_PATHS=() if [[ -n "$EXCLUDE_PATHS" ]]; then From b4080e8489adf25309f3277d709b2ecacf37b24e Mon Sep 17 00:00:00 2001 From: Artsiom Tsybulko Date: Fri, 21 Aug 2026 16:46:27 +0300 Subject: [PATCH 6/7] Add enum links --- plugins/enum-tables.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/plugins/enum-tables.js b/plugins/enum-tables.js index 80020288..accf70b5 100644 --- a/plugins/enum-tables.js +++ b/plugins/enum-tables.js @@ -1,5 +1,6 @@ const ENUM_TABLE_MARKER = '| Value | Name | Description |'; const ENUM_TAG = 'Enums'; +const SCHEMA_REF_PREFIX = '#/components/schemas/'; function escapeTableCell(value) { return String(value) @@ -11,7 +12,8 @@ function escapeTableCell(value) { function enumReference(name, schema) { const reference = { type: schema.type, - description: `See [${name}](#schema/${encodeURIComponent(name)}) enumeration values.`, + title: name, + description: enumLink(name), }; if (schema.format) { @@ -25,6 +27,10 @@ function enumReference(name, schema) { return reference; } +function enumLink(name) { + return `See [${name}](#schema/${encodeURIComponent(name)}) enumeration values.`; +} + function addEnumTable(schema) { const values = schema.enum; @@ -62,11 +68,25 @@ function replaceEnumReferences(node, enumSchemas, visited = new WeakSet()) { visited.add(node); - if (typeof node.$ref === 'string') { - const prefix = '#/components/schemas/'; + if (node.type === 'array' && typeof node.items?.$ref === 'string') { + const name = node.items.$ref.startsWith(SCHEMA_REF_PREFIX) + ? decodeURIComponent(node.items.$ref.slice(SCHEMA_REF_PREFIX.length)) + : null; - if (node.$ref.startsWith(prefix)) { - const name = decodeURIComponent(node.$ref.slice(prefix.length)); + if (name && enumSchemas.has(name)) { + const link = enumLink(name); + + if (!node.description?.includes(link)) { + node.description = node.description + ? `${node.description}\n\n${link}` + : link; + } + } + } + + if (typeof node.$ref === 'string') { + if (node.$ref.startsWith(SCHEMA_REF_PREFIX)) { + const name = decodeURIComponent(node.$ref.slice(SCHEMA_REF_PREFIX.length)); const enumSchema = enumSchemas.get(name); if (enumSchema) { From 9cf469324e9dea8093b73f4480ab7c19c2adb94a Mon Sep 17 00:00:00 2001 From: Artsiom Tsybulko Date: Fri, 21 Aug 2026 17:55:09 +0300 Subject: [PATCH 7/7] Change .html output name --- scripts/build-docs.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-docs.sh b/scripts/build-docs.sh index c1756f6b..19f5cf28 100755 --- a/scripts/build-docs.sh +++ b/scripts/build-docs.sh @@ -2,7 +2,7 @@ set -eu -output=${1:-document-reader-static-doc.html} +output=${1:-doc.html} bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/document-reader-docs.XXXXXX") normalized_dir="$bundle_dir/normalized" source_bundle="$bundle_dir/source.yml"