diff --git a/lib/markdownBuilder.js b/lib/markdownBuilder.js index fcf381b4..21195f89 100644 --- a/lib/markdownBuilder.js +++ b/lib/markdownBuilder.js @@ -847,11 +847,13 @@ export default function build({ /** * Generates the properties section for a schema * @param {*} schema + * @param {*} slugger + * @param {number} level - Base level for this schema */ - function makeproperties(schema, slugger) { + function makeproperties(schema, slugger, level = 1) { if (schema[keyword`properties`] || schema[keyword`patternProperties`] || schema[keyword`additionalProperties`]) { return [ - heading(1, text(i18n`${simpletitle(schema)} Properties`)), + heading(level, text(i18n`${simpletitle(schema)} Properties`)), makeproptable( schema[keyword`properties`], schema[keyword`patternProperties`], @@ -864,25 +866,49 @@ export default function build({ schema[keyword`patternProperties`], schema[keyword`additionalProperties`], schema[keyword`required`], - 1, + level, ), ]; } return []; } + /** + * Calculates the appropriate base header level for a schema based on its position + * in the schema hierarchy. Schemas within composition constructs (allOf/anyOf/oneOf) + * that are nested within properties should use higher header levels to maintain + * proper document hierarchy. + * + * @param {string} pointer - JSON pointer path to the schema + * @returns {number} The base header level (1-6) + */ + function calculateBaseLevel(pointer) { + // Only adjust level for schemas within composition constructs + const isCompositionSchema = /\/(allOf|anyOf|oneOf)\//.test(pointer); + if (!isCompositionSchema) return 1; + + // Calculate property nesting depth by counting /properties/ segments + const propertyDepth = (pointer.match(/\/properties\//g) || []).length; + return propertyDepth > 0 ? propertyDepth : 1; + } + console.log('generating markdown'); return (schemas) => foldl(schemas, {}, (pv, schema) => { const slugger = new GhSlugger(); + + // Calculate the appropriate base level for this schema based on its position + // in the hierarchy (composition constructs within properties need higher levels) + const baseLevel = calculateBaseLevel(schema[s.pointer]); + // eslint-disable-next-line no-param-reassign pv[schema[s.slug]] = root([ // todo add more elements ...makeheader(schema), - ...maketypesection(schema, 1), - ...makeconstraintssection(schema, 1), - ...makedefault(schema, 1), - ...makeexamples(schema, 1), - ...makeproperties(schema, slugger), + ...maketypesection(schema, baseLevel), + ...makeconstraintssection(schema, baseLevel), + ...makedefault(schema, baseLevel), + ...makeexamples(schema, baseLevel), + ...makeproperties(schema, slugger, baseLevel), ...makedefinitions(schema, slugger), ]); return pv; diff --git a/package-lock.json b/package-lock.json index 660dbf48..e3114763 100644 --- a/package-lock.json +++ b/package-lock.json @@ -625,7 +625,6 @@ "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.1", @@ -1437,7 +1436,6 @@ "integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1476,7 +1474,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2722,7 +2719,6 @@ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, - "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -3534,7 +3530,6 @@ "integrity": "sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6495,7 +6490,6 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -10079,7 +10073,6 @@ "dev": true, "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11500,7 +11493,6 @@ "integrity": "sha512-JL2hY7b1hBli5FS98nAXH0OwfRsgdywuUVhKktyJOHu9tUO0wJGWNpQL69SDhJ5Uwp63rtIvGvy7ZDy4C3RsXg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -12761,7 +12753,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12988,7 +12979,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/test/allof-anyof-hierarchy.test.js b/test/allof-anyof-hierarchy.test.js new file mode 100644 index 00000000..ac838cd5 --- /dev/null +++ b/test/allof-anyof-hierarchy.test.js @@ -0,0 +1,63 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +/* eslint-env mocha */ +/* eslint-disable no-unused-expressions */ +import { assertMarkdown, traverseSchemas } from './testUtils.js'; +import build from '../lib/markdownBuilder.js'; + +describe('Testing Markdown Builder: allOf/anyOf header hierarchy', () => { + let results; + + before(async () => { + const schemas = await traverseSchemas('allof-anyof-hierarchy'); + const builder = build({ header: true }); + results = builder(schemas); + }); + + it('allOf item schemas should use higher base level for proper hierarchy', () => { + // The allOf[1] item is a composition schema within the docker property + // It should use baseLevel = 2 (property depth) instead of 1 + const allof1Slug = 'schema-properties-tests-configuration-properties-docker-tests-allof-1'; + assertMarkdown(results[allof1Slug]) + .matches(/^## 1 Properties$/m) // Properties heading at level 2 (baseLevel) + .matches(/^### preTestSteps/m) // Property detail at level 3 (baseLevel + 1) + .matches(/^#### preTestSteps Type$/m); // Type section at level 4 (baseLevel + 1 + 1) + }); + + it('anyOf item schemas should use higher base level for proper hierarchy', () => { + // The anyOf[1] item within allOf[0] is a composition schema + // It should also use baseLevel = 2 (property depth) instead of 1 + const anyof1Slug = 'schema-properties-tests-configuration-properties-docker-tests-allof-test-options-anyof-steplist'; + assertMarkdown(results[anyof1Slug]) + .matches(/^## 1 Properties$/m) // Properties heading at level 2 (baseLevel) + .matches(/^### testSteps/m) // Property detail at level 3 (baseLevel + 1) + .matches(/^#### testSteps Type$/m); // Type section at level 4 (baseLevel + 1 + 1) + }); + + it('oneOf item schemas should use higher base level for proper hierarchy', () => { + // The oneOf[0] item within kubernetes property is a composition schema + // It should use baseLevel = 2 (property depth) instead of 1 + const oneof0Slug = 'schema-properties-tests-configuration-properties-kubernetes-tests-oneof-helm'; + assertMarkdown(results[oneof0Slug]) + .matches(/^## 0 Properties$/m) // Properties heading at level 2 (baseLevel) + .matches(/^### helmChart/m) // Property detail at level 3 (baseLevel + 1) + .matches(/^#### helmChart Type$/m); // Type section at level 4 (baseLevel + 1 + 1) + }); + + it('regular property schemas should still use baseLevel 1', () => { + // Regular property schemas (not in composition constructs) should still use level 1 + const dockerSlug = 'schema-properties-tests-configuration-properties-docker-tests'; + assertMarkdown(results[dockerSlug]) + .matches(/^# Docker Tests Schema$/m) // Schema header at level 1 + .matches(/^## docker Type$/m); // Type section at level 2 (baseLevel 1 + 1) + }); +}); diff --git a/test/fixtures/allof-anyof-hierarchy/schema.schema.json b/test/fixtures/allof-anyof-hierarchy/schema.schema.json new file mode 100644 index 00000000..154acff8 --- /dev/null +++ b/test/fixtures/allof-anyof-hierarchy/schema.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Example Schema", + "type": "object", + "properties": { + "tests": { + "title": "Tests Configuration", + "description": "Configure test execution", + "type": "object", + "properties": { + "docker": { + "title": "Docker Tests", + "description": "Run Docker-based tests", + "type": "object", + "allOf": [ + { + "title": "test-options", + "description": "Test execution options", + "anyOf": [ + { + "title": "script", + "properties": { + "testScript": { + "title": "Test Script", + "description": "Script to execute for testing", + "type": "string" + } + }, + "required": ["testScript"] + }, + { + "title": "stepList", + "properties": { + "testSteps": { + "title": "Test Steps", + "description": "List of test steps to execute", + "type": "array", + "items": { + "type": "object" + } + } + }, + "required": ["testSteps"] + } + ] + }, + { + "properties": { + "preTestSteps": { + "title": "Pre-test Steps", + "description": "Steps to execute before running tests", + "type": "array", + "items": { + "type": "object" + } + } + } + } + ] + }, + "kubernetes": { + "title": "Kubernetes Tests", + "description": "Run Kubernetes-based tests", + "type": "object", + "oneOf": [ + { + "title": "helm", + "properties": { + "helmChart": { + "title": "Helm Chart", + "description": "Helm chart to deploy for testing", + "type": "string" + } + }, + "required": ["helmChart"] + }, + { + "title": "manifest", + "properties": { + "manifestFile": { + "title": "Manifest File", + "description": "Kubernetes manifest file to apply", + "type": "string" + } + }, + "required": ["manifestFile"] + } + ] + } + } + } + } +}