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"