From dd846fb349c0dc22de888e49d92afb323d69ca50 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 16:59:49 +0100 Subject: [PATCH 1/7] [INTERNAL] lib/processors/jsdoc: improve support for "module:" syntax When the JSDoc name for a ManagedObject class differs from its runtime name (as specified in the extend call), then use the JSDoc name to calculate the fully qualified names of generated documentation. Otherwise, JSDoc can't associate the generated documentation with the class. Also enhances the check for a mismatch between documented base type and technical base type. It no longer reports a future error when module:* syntax is used for the documented base type. Cherry-picked from UI5/openui5@1bf4429d4. --- lib/processors/jsdoc/lib/ui5/plugin.js | 30 ++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js index 4d8622a5d..34a696db7 100644 --- a/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -1224,7 +1224,7 @@ function collectClassInfo(extendCall, classDoclet) { const baseCandidate = getResolvedObjectName(extendCall.callee.object); if ( baseCandidate && baseType == null ) { baseType = baseCandidate; - } else if ( baseCandidate !== baseType ) { + } else if ( baseCandidate !== toRuntimeMetadataName(baseType) ) { future(`documented base type '${baseType}' doesn't match technical base type '${baseCandidate}'`); } } @@ -1772,7 +1772,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, } function name(prefix, n, _static) { - return oClassInfo.name + rname(prefix, n, _static); + return jsdocClassName + rname(prefix, n, _static); } /* @@ -1884,6 +1884,18 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, const p = m ? m.index : -1; const hasSettingsDocs = rawClassComment.indexOf("The supported settings are:") >= 0; const classAccess = doclet?.access ?? "public"; + + // If doclet exists and has an alias or longname, use that when building the fully qualified + // names of methods or events. + // Do this only when the doclet is of kind "class". Many TypeScript classes unfortunately + // use a broken pattern with @namespace (needs to be deprecated in babel-plugin-transform-modules-ui5) + const jsdocClassName = + (doclet?.kind === "class" ? doclet.alias || doclet.longname : undefined) || oClassInfo.name; + if (jsdocClassName !== oClassInfo.name) { + info("jsdoc class name differs from runtime metadata name", jsdocClassName, oClassInfo.name); + } + + const displayClassName = oClassInfo.name; const visibility = createVisibilityTags(classAccess, doclet?.__ui5?.stakeholders); const thisClass = 'this'; // oClassInfo.name @@ -2007,7 +2019,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, } newJSDoc([ - "Returns a metadata object for class " + oClassInfo.name + ".", + "Returns a metadata object for class " + displayClassName + ".", "", "@returns {sap.ui.base.Metadata} Metadata object describing this class", visibility, @@ -2018,7 +2030,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, if ( !oClassInfo["final"] ) { newJSDoc([ - "Creates a new subclass of class " + oClassInfo.name + " with name sClassName", + "Creates a new subclass of class " + displayClassName + " with name sClassName", "and enriches it with the information contained in oClassInfo.", "", "oClassInfo might contain the same kind of information as described in {@link " + (oClassInfo.baseType ? oClassInfo.baseType + ".extend" : "sap.ui.base.Object.extend Object.extend") + "}.", @@ -2304,7 +2316,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "", info.enableEventBubbling ? "This event bubbles up the control hierarchy." : "", "", - "@name " + oClassInfo.name + "#" + n, + "@name " + jsdocClassName + "#" + n, "@event", devStateTags, "@param {sap.ui.base.Event} oControlEvent", @@ -2321,10 +2333,10 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, newJSDoc(lines); newJSDoc([ - "Attaches event handler fnFunction to the " + link + " event of this " + oClassInfo.name + ".", + "Attaches event handler fnFunction to the " + link + " event of this " + displayClassName + ".", "", "When called, the context of the event handler (its this) will be bound to oListener if specified, ", - "otherwise it will be bound to this " + oClassInfo.name + " itself.", + "otherwise it will be bound to this " + displayClassName + " itself.", "", !newStyle && info.doc ? info.doc : "", "", @@ -2333,7 +2345,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "@param {function(sap.ui.base.Event):void}", " fnFunction The function to be called when the event occurs", "@param {object}", - " [oListener] Context object to call the event handler with. Defaults to this " + oClassInfo.name + " itself", + " [oListener] Context object to call the event handler with. Defaults to this " + displayClassName + " itself", "", "@returns {" + thisClass + "} Reference to this in order to allow method chaining", devStateTags, @@ -2342,7 +2354,7 @@ function createAutoDoc(oClassInfo, classComment, doclet, node, parser, filename, "@function" ]); newJSDoc([ - "Detaches event handler fnFunction from the " + link + " event of this " + oClassInfo.name + ".", + "Detaches event handler fnFunction from the " + link + " event of this " + displayClassName + ".", "", "The passed function and listener object must match the ones used for event registration.", "", From 52de20dce770af8cf9fb7389a568b3af820ee75b Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 17:05:12 +0100 Subject: [PATCH 2/7] [INTERNAL] lib/processors/jsdoc: push the deprecation of namespaces down to the contained APIs Cherry-picked from UI5/openui5@b16b6bcaa. --- .../jsdoc/lib/ui5/template/publish.js | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/processors/jsdoc/lib/ui5/template/publish.js b/lib/processors/jsdoc/lib/ui5/template/publish.js index 4a24aebd1..98ad45f43 100644 --- a/lib/processors/jsdoc/lib/ui5/template/publish.js +++ b/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -231,6 +231,7 @@ function publish(symbolSet) { // now resolve relationships const aRootNamespaces = createNamespaceTree(allSymbols); + pushDownDeprecations(allSymbols); createInheritanceTree(allSymbols); collectMembers(allSymbols); mergeEventDocumentation(allSymbols); @@ -374,6 +375,33 @@ function makeNamespace(memberof) { ]); } +function pushDownDeprecations() { + + function pushDown(oSymbol, sInheritedDeprecation) { + if (oSymbol == null || !isFirstClassSymbol(oSymbol)) { + return; + } + const sOwnDeprecation = oSymbol.deprecated; + if (sOwnDeprecation == null && sInheritedDeprecation != null) { + oSymbol.deprecated = sInheritedDeprecation; + info(`mark ${oSymbol.longname} as deprecated (${sInheritedDeprecation})`); + } else if (sOwnDeprecation != null) { + const info = extractSince(sOwnDeprecation); + const sinceBecause = info.since ? `As of version ${info.since}, because`: `Because`; + sInheritedDeprecation = `${sinceBecause} it is part of the deprecated package ${oSymbol.longname}`; + } + oSymbol.__ui5.children?.forEach((oChild) => pushDown(oChild, sInheritedDeprecation)); + } + + // w/o a library name we can't start with the library namespace + if (!templateConf.uilib) { + return; + } + + // start the push down at the library namespace to ensure locality + pushDown(lookup(templateConf.uilib)); +} + //---- inheritance hierarchy ---------------------------------------------------------------------------- /* From 82751d51f9ef047ef1692f3e419256a350fe425c Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 17:08:55 +0100 Subject: [PATCH 3/7] [INTERNAL] lib/processors/jsdoc: automatically add @lends tag to extend calls When an extend call creates a class and when the name of the class is known and when the extend call's second argument is an object literal, then ensure that it has a @lends tag associating the object literal with the class prototype. Also logs a future error when the name of an existing @lends tags doesn't match the name of the described class. Cherry-picked from UI5/openui5@daab95d89. --- lib/processors/jsdoc/lib/ui5/plugin.js | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js index 34a696db7..d2ee1ee72 100644 --- a/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -1220,6 +1220,8 @@ function collectClassInfo(extendCall, classDoclet) { if ( classDoclet && classDoclet.augments && classDoclet.augments.length === 1 ) { baseType = classDoclet.augments[0]; } + const jsdocClassName = classDoclet?.kind === "class" ? classDoclet.alias || classDoclet.longname : undefined; + if ( isMemberExpression(extendCall.callee) ) { const baseCandidate = getResolvedObjectName(extendCall.callee.object); if ( baseCandidate && baseType == null ) { @@ -1283,6 +1285,32 @@ function collectClassInfo(extendCall, classDoclet) { } const classInfoNode = extendCall.arguments[1]; + if (classInfoNode?.type === Syntax.ObjectExpression && jsdocClassName && addLeadingCommentNode) { + // Take care of the @lends comment only + // - when the class info is an object literal (otherwise lends makes no sense) + // - when a class name is found (it might differ from the runtime metadata name) + // - when the entity is documented as a class (or else '.prototype' would be wrong) + const classInfoComment = getLeadingCommentNode(classInfoNode); + if (classInfoComment == null) { + // If the class info object doesn't have a @lends tag, add it + const raw = `/** @lends ${jsdocClassName}.prototype */`; + info(`lends comment injected for class '${jsdocClassName}'`); + addLeadingCommentNode(classInfoNode, { + type: "CommentBlock", + raw, + value: raw.slice(2,-2), + loc: classInfoNode.loc + }); + } else { + // Otherwise check whether the name matches the name of the described class + const doclet = new Doclet(getRawComment(classInfoComment), {}); + // The name after the @lends is surprisingly stored in the 'alias' property + if (doclet.alias !== `${jsdocClassName}.prototype`) { + future(`lends comment '${doclet.alias}' doesn't match documented class name '${jsdocClassName}'`); + } + } + } + const classInfoMap = createPropertyMap(classInfoNode); if ( classInfoMap && classInfoMap.metadata && classInfoMap.metadata.value.type !== Syntax.ObjectExpression ) { warning(`class metadata exists but can't be analyzed. It is not of type 'ObjectExpression', but a '${classInfoMap.metadata.value.type}'.`); @@ -2451,6 +2479,7 @@ function toRuntimeMetadataName(longname) { let isDocComment; let getLeadingCommentNode; +let addLeadingCommentNode; // JSDoc added the node type Syntax.File with the same change that activated Babylon // See https://github.com/jsdoc3/jsdoc/commit/ffec4a42291de6d68e6240f304b68d6abb82a869 @@ -2476,6 +2505,15 @@ if ( Syntax.File === 'File' ) { return undefined; }; + addLeadingCommentNode = function(node, comment) { + node.leadingComments ??= []; + let index = node.leadingComments.length; + while (index > 0 && node.leadingComments[index - 1].type !== 'CommentBlock') { + index--; + } + node.leadingComments.splice(index, 0, comment); + } + } else { // JSDoc versions before 3.5.0 @@ -2510,6 +2548,9 @@ if ( Syntax.File === 'File' ) { return comment; }; + + // not implemented for espree (no automatic injection of @lends comments) + addLeadingCommentNode = undefined; } //--- comment related functions that are independent from the JSdoc version From 9198453253f61b62b043323a048b66108b73b83d Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 17:11:32 +0100 Subject: [PATCH 4/7] [INTERNAL] lib/processors/jsdoc: resolve versionUtil after mvn build Cherry-picked from UI5/openui5@4b5513d6c. --- lib/processors/jsdoc/lib/ui5/plugin.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js index d2ee1ee72..e7e5f0790 100644 --- a/lib/processors/jsdoc/lib/ui5/plugin.js +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -73,7 +73,8 @@ const path = require('jsdoc/path'); const logger = require('jsdoc/util/logger'); const pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || env.opts.sapui5 || {}; const escope = require("escope"); -const {isSemVer} = require("./template/utils/versionUtil"); +const templatePath = path.getResourcePath(env.opts.template) || "./template"; +const {isSemVer} = require(`${templatePath}/utils/versionUtil`); /* ---- logging ---- */ From 99565709e15184857435f3c89f78340b409f36ae Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 17:15:13 +0100 Subject: [PATCH 5/7] [INTERNAL] lib/processors/jsdoc: Add dynamic section for API reference Enables automatic discovery and rendering of documentation sections (FAQ, guides, etc.) from directory structure. Sections are discovered at build time and loaded asynchronously at runtime. JIRA: BGSOFUIPIRIN-6925 Cherry-picked from UI5/openui5@c71a404cb. --- lib/processors/jsdoc/lib/transformApiJson.js | 44 +++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/lib/processors/jsdoc/lib/transformApiJson.js b/lib/processors/jsdoc/lib/transformApiJson.js index a2192ef6c..3b1fef1ca 100644 --- a/lib/processors/jsdoc/lib/transformApiJson.js +++ b/lib/processors/jsdoc/lib/transformApiJson.js @@ -47,10 +47,10 @@ const typeParser = new TypeParser(); * @param {string} sLibraryFile Path to the .library file of the library, used to extract further documentation information * @param {string|string[]} vDependencyAPIFiles Path of folder that contains api.json files of predecessor libs or * an array of paths of those files - * @param {string} sFAQDir Path to the directory containing the sources for the FAQ section in APiRef + * @param {string} sSectionsDir Path to the directory containing the sources for the sections (FAQ, documentation, etc.) in the API Reference * @returns {Promise} A Promise that resolves after the transformation has been completed */ -function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, sFAQDir, options) { +function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, sSectionsDir, options) { const fs = options && options.fs || require("fs"); const returnOutputFiles = options && !!options.returnOutputFiles; @@ -59,7 +59,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, log.info(" output file: " + sOutputFile); log.info(" library file: " + sLibraryFile); log.info(" dependency dir: " + vDependencyAPIFiles); - log.info(" FAQ src dir: " + sFAQDir); + log.info(" sections src dir: " + sSectionsDir); if (options && options.fs) { log.info("Using custom fs."); } @@ -987,22 +987,43 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } /** - * Check for existence of FAQ data - * (FAQ data must be defined as *.md files in the sFAQDir) + * Check for existence of sections data + * (Sections data must be defined as *.md files in the sSectionsDir) * and add a boolean flag in case it exists * * @param oChainObject chain object */ - function addFlagsForFAQData(oChainObject) { - if (!sFAQDir) { + function addFlagsForSectionsData(oChainObject) { + if (!sSectionsDir) { return oChainObject; } + const slibName = oChainObject.fileData.library; + oChainObject.fileData.symbols.forEach(function(symbol) { - const sfile = symbol.name.replace(slibName, "").replace(/[.]/g, "/") + ".md"; - if (fs.existsSync(path.join(sFAQDir, sfile))) { - symbol.hasFAQ = true; + const sComponentPath = symbol.name.replace(slibName, "").replace(/^[.]/, "").replace(/[.]/g, "/"); + if (!sComponentPath) { + return; } + + const componentDir = path.join(sSectionsDir, sComponentPath); + if (fs.existsSync(componentDir)) { + try { + + const customSections = fs.readdirSync(componentDir, { withFileTypes: true }) + .filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md')) + .map((dirent) => dirent.name.replace(/\.md$/, '')); + + // Store list of available sections for this symbol + if (customSections.length > 0) { + symbol.customSections = customSections; + } + + } catch (error) { + log.error('Error scanning component sections directory:', componentDir, error); + } + } + }); return oChainObject; } @@ -2375,6 +2396,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, inputFile: sInputFile, outputFile: sOutputFile, libraryFile: sLibraryFile, + sectionsDir: sSectionsDir, aDependentLibraryFiles: Array.isArray(vDependencyAPIFiles) ? vDependencyAPIFiles : null }; @@ -2387,7 +2409,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, .then(getAPIJSONPromise) .then(loadDependencyLibraryFiles) .then(transformApiJson) - .then(addFlagsForFAQData) + .then(addFlagsForSectionsData) .then(createApiRefApiJson); return p; From 939d0bd2989037d416efe119fcf316d7f16d2d79 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 17:18:38 +0100 Subject: [PATCH 6/7] [INTERNAL] lib/processors/jsdoc: visualisation of structured parameters Parameters of constructors/methods/events, which are of typeDefs are now visualized in DemoKit in expandable Rows of Table: when row is expanded the properties of the typedef are shown in additional rows of the same Table. JIRA: BGSOFUIPIRIN-6935 Cherry-picked from UI5/openui5@1a77603c3. --- lib/processors/jsdoc/lib/transformApiJson.js | 47 +++++++++++++++----- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/lib/processors/jsdoc/lib/transformApiJson.js b/lib/processors/jsdoc/lib/transformApiJson.js index 3b1fef1ca..73c06e3d5 100644 --- a/lib/processors/jsdoc/lib/transformApiJson.js +++ b/lib/processors/jsdoc/lib/transformApiJson.js @@ -156,9 +156,10 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, * UI5Types: ["sap.ui.core.Control"] // skip template if its value is "${0}" (default value) * } * @param {string} sComplexType + * @param {Object} oAllDependentAPIs Map of entity names to their symbol * @returns {{template=: string, UI5Types=: string[]}} */ - function parseUI5Types(sComplexType) { + function parseUI5Types(sComplexType, oAllDependentAPIs) { let oParsed; try { oParsed = typeParser.parseSimpleTypes(sComplexType, isUI5Type); @@ -176,6 +177,14 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (oParsed.simpleTypes?.length) { // it can be empty if none of the included simple types satisfied the filter function result.UI5Types = oParsed.simpleTypes; + + // check whether the type refers to a typedef and set a marker + if (result.template == null && result.UI5Types.length === 1) { + const symbol = oAllDependentAPIs[result.UI5Types[0]]; + if (symbol?.kind === "typedef" && Array.isArray(symbol.properties)) { + result.refersToTypedef = true; + } + } } return result; @@ -266,6 +275,8 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, * @param {object} oChainObject chain object */ const transformApiJson = function (oChainObject) { + const {oAllDependentAPIs} = oChainObject; + // Function is a copy from: LibraryInfo.js => LibraryInfo.prototype._getActualComponent => "match" inline method function matchComponent(sModuleName, sPattern) { sModuleName = sModuleName.toLowerCase(); @@ -435,7 +446,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Types oParameter.types = []; if (oParameter.type) { - oParameter.typeInfo = parseUI5Types(oParameter.type); + oParameter.typeInfo = parseUI5Types(oParameter.type, oAllDependentAPIs); // Keep file size in check delete oParameter.type; } @@ -520,7 +531,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Type if (oSymbol.kind !== "enum") { // enum properties don't have an own type if (oProperty.type) { - oProperty.typeInfo = parseUI5Types(oProperty.type); + oProperty.typeInfo = parseUI5Types(oProperty.type, oAllDependentAPIs); // Keep file size in check delete oProperty.type; } @@ -560,7 +571,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Type if (oProperty.type) { - oProperty.typeInfo = parseUI5Types(oProperty.type); + oProperty.typeInfo = parseUI5Types(oProperty.type, oAllDependentAPIs); // Keep file size in check delete oProperty.type; } @@ -687,7 +698,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Link Enabled if (oSetting.type) { - oSetting.typeInfo = parseUI5Types(oSetting.type); + oSetting.typeInfo = parseUI5Types(oSetting.type, oAllDependentAPIs); delete oSetting.type; // Keep file size in check } @@ -756,7 +767,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, oEvent.parameters.forEach((oParameter) => { if (oParameter.type) { - oParameter.typeInfo = parseUI5Types(oParameter.type); + oParameter.typeInfo = parseUI5Types(oParameter.type, oAllDependentAPIs); delete oParameter.type; // Keep file size in check } @@ -789,7 +800,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (oSymbol.methods) { // Pre-process methods - methods.buildMethodsModel(oSymbol.methods); + methods.buildMethodsModel(oSymbol.methods, oAllDependentAPIs); oSymbol.methods.forEach((oMethod) => { @@ -950,6 +961,14 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, } function loadDependencyLibraryFiles (oChainObject) { + const oAllDependentAPIs = oChainObject.oAllDependentAPIs = Object.create(null); + // add symbols from current library + if (oChainObject.fileData.symbols) { + for (const oSymbol of oChainObject.fileData.symbols) { + oAllDependentAPIs[oSymbol.name] = oSymbol; + } + } + if (!oChainObject.aDependentLibraryFiles) { return oChainObject; } @@ -978,6 +997,12 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // In this case we don't add it to the dependency list to skip double iteration. if (oData && oChainObject.fileData.library !== oData.library) { oDependentAPIs[oData.library] = oData.symbols; + + if (Array.isArray(oData.symbols)) { + for (const oSymbol of oData.symbols) { + oAllDependentAPIs[oSymbol.name] = oSymbol; + } + } } }); @@ -2196,7 +2221,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, * Adjusts methods info so that it can be easily displayed in a table * @param aMethods - the methods array initially coming from the server */ - buildMethodsModel: function (aMethods) { + buildMethodsModel: function (aMethods, oAllDependentAPIs) { var fnExtractParameterProperties = function (oParameter, aParameters, iDepth, aPhoneName) { if (oParameter.parameterProperties) { Object.keys(oParameter.parameterProperties).forEach(function (sProperty) { @@ -2206,7 +2231,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Handle types if (oProperty.type) { - oProperty.typeInfo = parseUI5Types(oProperty.type); + oProperty.typeInfo = parseUI5Types(oProperty.type, oAllDependentAPIs); // Keep file size in check delete oProperty.type; } @@ -2238,7 +2263,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, if (oMethod.parameters) { oMethod.parameters.forEach(function (oParameter) { if (oParameter.type) { - oParameter.typeInfo = parseUI5Types(oParameter.type); + oParameter.typeInfo = parseUI5Types(oParameter.type, oAllDependentAPIs); // Keep file size in check delete oParameter.type; } @@ -2259,7 +2284,7 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, // Handle return values if (oMethod.returnValue && oMethod.returnValue.type) { // Handle types - oMethod.returnValue.typeInfo = parseUI5Types(oMethod.returnValue.type); + oMethod.returnValue.typeInfo = parseUI5Types(oMethod.returnValue.type, oAllDependentAPIs); } }); From 77649de8bf7eadd9a05b41810d7a368c0fbbdebd Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Mon, 23 Mar 2026 17:21:13 +0100 Subject: [PATCH 7/7] [INTERNAL] lib/processors/jsdoc: add hierarchical subsection support for API reference sections Implement support for multi-level section structure in API documentation, allowing sections to contain subsections organized in folder hierarchies. - Sections can now have subsections (e.g., FAQ.md + FAQ/ folder) - Main section content displays as "Overview" subsection - Subsection files automatically loaded from matching directories - Dropdown navigation shows only subsection names, not parent section - The new dynamic sections/subsections are bookmark-able the page automatically scrolls down to the section Example structure: sections/Component/FAQ.md "Overview" subsection sections/Component/FAQ/Example1.md "Example1" subsection sections/Component/FAQ/Example2.md "Example2" subsection JIRA: BGSOFUIPIRIN-6925 Cherry-picked from UI5/openui5@05f6fef35. --- lib/processors/jsdoc/lib/transformApiJson.js | 40 ++++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/processors/jsdoc/lib/transformApiJson.js b/lib/processors/jsdoc/lib/transformApiJson.js index 73c06e3d5..6b9b92867 100644 --- a/lib/processors/jsdoc/lib/transformApiJson.js +++ b/lib/processors/jsdoc/lib/transformApiJson.js @@ -1034,15 +1034,41 @@ function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, const componentDir = path.join(sSectionsDir, sComponentPath); if (fs.existsSync(componentDir)) { try { + const dirContents = fs.readdirSync(componentDir, { withFileTypes: true }); + + // Separate files and directories in one pass + const mdFiles = []; + const dirNames = new Set(); + dirContents.forEach((dirent) => { + if (dirent.isFile() && dirent.name.endsWith('.md')) { + mdFiles.push(dirent.name.replace(/\.md$/, '')); + } else if (dirent.isDirectory()) { + dirNames.add(dirent.name); + } + }); - const customSections = fs.readdirSync(componentDir, { withFileTypes: true }) - .filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md')) - .map((dirent) => dirent.name.replace(/\.md$/, '')); - - // Store list of available sections for this symbol - if (customSections.length > 0) { - symbol.customSections = customSections; + // Build customSections array with subsection support + const customSections = mdFiles.map(function(sectionName) { + // Check if there's a matching directory for this section + if (dirNames.has(sectionName)) { + try { + const subsectionFiles = fs.readdirSync(path.join(componentDir, sectionName), { withFileTypes: true }) + .filter((dirent) => dirent.isFile() && dirent.name.endsWith('.md')) + .map((dirent) => dirent.name.replace(/\.md$/, '')); + + if (subsectionFiles.length > 0) { + return { name: sectionName, hasSubsections: true, subsections: subsectionFiles }; + } + } catch (error) { + log.error('Error scanning subsection directory:', path.join(componentDir, sectionName), error); + } } + return sectionName; + }); + + if (customSections.length > 0) { + symbol.customSections = customSections; + } } catch (error) { log.error('Error scanning component sections directory:', componentDir, error);