diff --git a/.eslintignore b/.eslintignore index 5c9eafbbe..5f3b1d6e9 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,8 @@ # /node_modules/* and /bower_components/* ignored by default +# Exclude shared resources that can't (yet) follow our conventions +lib/processors/jsdoc/lib + # Exclude coverage folder coverage/ diff --git a/index.js b/index.js index 59f3eaf65..db8ee8ae9 100644 --- a/index.js +++ b/index.js @@ -13,6 +13,9 @@ module.exports = { flexChangesBundler: require("./lib/processors/bundlers/flexChangesBundler"), manifestBundler: require("./lib/processors/bundlers/manifestBundler"), moduleBundler: require("./lib/processors/bundlers/moduleBundler"), + apiIndexGenerator: require("./lib/processors/jsdoc/apiIndexGenerator"), + jsdocGenerator: require("./lib/processors/jsdoc/jsdocGenerator"), + sdkTransformer: require("./lib/processors/jsdoc/sdkTransformer"), bootstrapHtmlTransformer: require("./lib/processors/bootstrapHtmlTransformer"), debugFileCreator: require("./lib/processors/debugFileCreator"), resourceCopier: require("./lib/processors/resourceCopier"), @@ -35,6 +38,9 @@ module.exports = { generateBundle: require("./lib/tasks/bundlers/generateBundle"), buildThemes: require("./lib/tasks/buildThemes"), createDebugFiles: require("./lib/tasks/createDebugFiles"), + executeJsdocSdkTransformation: require("./lib/tasks/jsdoc/executeJsdocSdkTransformation"), + generateApiIndex: require("./lib/tasks/jsdoc/generateApiIndex"), + generateJsdoc: require("./lib/tasks/jsdoc/generateJsdoc"), generateVersionInfo: require("./lib/tasks/generateVersionInfo"), replaceCopyright: require("./lib/tasks/replaceCopyright"), replaceVersion: require("./lib/tasks/replaceVersion"), diff --git a/lib/builder/builder.js b/lib/builder/builder.js index 01307c6e0..5c34192d5 100644 --- a/lib/builder/builder.js +++ b/lib/builder/builder.js @@ -1,5 +1,6 @@ const log = require("@ui5/logger").getGroupLogger("builder:builder"); const resourceFactory = require("@ui5/fs").resourceFactory; +const MemAdapter = require("@ui5/fs").adapters.Memory; const typeRepository = require("../types/typeRepository"); const taskRepository = require("../tasks/taskRepository"); @@ -36,20 +37,24 @@ function getElapsedTime(startTime) { * @param {Object} parameters * @param {boolean} parameters.dev Sets development mode, which only runs essential tasks * @param {boolean} parameters.selfContained True if a the build should be self-contained or false for prelead build bundles + * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed * @param {Array} parameters.includedTasks Task list to be included from build * @param {Array} parameters.excludedTasks Task list to be excluded from build * @returns {Array} Return a task list for the builder */ -function composeTaskList({dev, selfContained, includedTasks, excludedTasks}) { +function composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTasks}) { let selectedTasks = Object.keys(definedTasks).reduce((list, key) => { list[key] = true; return list; }, {}); - // Exclude tasks: manifestBundler + // Exclude non default tasks selectedTasks.generateManifestBundle = false; selectedTasks.generateStandaloneAppBundle = false; selectedTasks.transformBootstrapHtml = false; + selectedTasks.generateJsdoc = false; + selectedTasks.executeJsdocSdkTransformation = false; + selectedTasks.generateApiIndex = false; if (selfContained) { // No preloads, bundle only @@ -59,6 +64,27 @@ function composeTaskList({dev, selfContained, includedTasks, excludedTasks}) { selectedTasks.generateLibraryPreload = false; } + if (jsdoc) { + // Include JSDoc tasks + selectedTasks.generateJsdoc = true; + selectedTasks.executeJsdocSdkTransformation = true; + selectedTasks.generateApiIndex = true; + + // Include theme build as required for SDK + selectedTasks.buildThemes = true; + + // Exclude all tasks not relevant to JSDoc generation + selectedTasks.replaceCopyright = false; + selectedTasks.replaceVersion = false; + selectedTasks.generateComponentPreload = false; + selectedTasks.generateLibraryPreload = false; + selectedTasks.generateLibraryManifest = false; + selectedTasks.createDebugFiles = false; + selectedTasks.uglify = false; + selectedTasks.generateFlexChangesBundle = false; + selectedTasks.generateManifestBundle = false; + } + // Only run essential tasks in development mode, it is not desired to run time consuming tasks during development. if (dev) { // Overwrite all other tasks with noop promise @@ -123,6 +149,7 @@ module.exports = { * @param {boolean} [parameters.buildDependencies=false] Decides whether project dependencies are built as well * @param {boolean} [parameters.dev=false] Decides whether a development build should be activated (skips non-essential and time-intensive tasks) * @param {boolean} [parameters.selfContained=false] Flag to activate self contained build + * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build * @param {Array} [parameters.includedTasks=[]] List of tasks to be included * @param {Array} [parameters.excludedTasks=[]] List of tasks to be excluded. If the wildcard '*' is provided, only the included tasks will be executed. * @param {Array} [parameters.devExcludeProject=[]] List of projects to be excluded from development build @@ -130,7 +157,7 @@ module.exports = { */ build({ tree, destPath, - buildDependencies = false, dev = false, selfContained = false, + buildDependencies = false, dev = false, selfContained = false, jsdoc = false, includedTasks = [], excludedTasks = [], devExcludeProject = [] }) { const startTime = process.hrtime(); @@ -138,7 +165,7 @@ module.exports = { " including dependencies..." + (dev ? " [dev mode]" : "")); log.verbose(`Building to ${destPath}...`); - const selectedTasks = composeTaskList({dev, selfContained, includedTasks, excludedTasks}); + const selectedTasks = composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTasks}); const fsTarget = resourceFactory.createAdapter({ fsBasePath: destPath, @@ -147,6 +174,7 @@ module.exports = { const projects = {}; // Unique project index to prevent building the same project multiple times + const projectWriters = {}; // Collection of memory adapters of already built libraries const projectCountMarker = {}; function projectCount(project, count = 0) { @@ -187,10 +215,21 @@ module.exports = { const projectType = typeRepository.getType(project.type); const resourceCollections = resourceFactory.createCollectionsForTree(project, { - useNamespaces: true + useNamespaces: true, + virtualReaders: projectWriters + }); + + const writer = new MemAdapter({ + virBasePath: "/" }); + // Store project writer as virtual reader for parent projects + // so they can access the build results of this project + projectWriters[project.metadata.name] = writer; + // TODO: Add getter for writer of DuplexColection const workspace = resourceFactory.createWorkspace({ + virBasePath: "/", + writer, reader: resourceCollections.source, name: project.metadata.name }); @@ -198,6 +237,7 @@ module.exports = { if (dev && devExcludeProject.indexOf(project.metadata.name) !== -1) { projectTasks = composeTaskList({dev: false, selfContained, includedTasks, excludedTasks}); } + return projectType.build({ resourceCollections: { workspace, diff --git a/lib/processors/jsdoc/apiIndexGenerator.js b/lib/processors/jsdoc/apiIndexGenerator.js new file mode 100644 index 000000000..c3026557d --- /dev/null +++ b/lib/processors/jsdoc/apiIndexGenerator.js @@ -0,0 +1,51 @@ +const resourceFactory = require("@ui5/fs").resourceFactory; +const createIndex = require("./lib/create-api-index"); + +/** + * Compiles API index resources from all api.json resources available in the given test resources directory + * as created by the [sdkTransformer]{@link module:@ui5/builder.processors.sdkTransformer} processor. + * The resulting index resources (e.g. api-index.json, api-index-deprecated.json, + * api-index-experimental.json and api-index-since.json) are mainly to be used in the SDK. + * + * @public + * @alias module:@ui5/builder.processors.apiIndexGenerator + * @param {Object} parameters Parameters + * @param {string} parameters.versionInfoPath Path to sap-ui-version.json resource + * @param {string} parameters.testResourcesRootPath Path to /test-resources root directory in the + * given fs + * @param {string} parameters.targetApiIndexPath Path to create the generated API index JSON resource for + * @param {string} parameters.targetApiIndexDeprecatedPath Path to create the generated API index "deprecated" JSON + * resource for + * @param {string} parameters.targetApiIndexExperimentalPath Path to create the generated API index "experimental" JSON + * resource for + * @param {string} parameters.targetApiIndexSincePath Path to create the generated API index "since" JSON resource for + * @param {fs|module:@ui5/fs.fsInterface} parameters.fs Node fs or + * custom [fs interface]{@link module:resources/module:@ui5/fs.fsInterface} to use + * @returns {Promise} Promise resolving with created resources api-index.json, + * api-index-deprecated.json, api-index-experimental.json and + * api-index-since.json (names depend on the supplied paths) + */ +const apiIndexGenerator = async function({ + versionInfoPath, testResourcesRootPath, targetApiIndexPath, targetApiIndexDeprecatedPath, + targetApiIndexExperimentalPath, targetApiIndexSincePath, fs +} = {}) { + if (!versionInfoPath || !testResourcesRootPath || !targetApiIndexPath || !targetApiIndexDeprecatedPath || + !targetApiIndexExperimentalPath || !targetApiIndexSincePath || !fs) { + throw new Error("[apiIndexGenerator]: One or more mandatory parameters not provided"); + } + + const resourceMap = await createIndex(versionInfoPath, testResourcesRootPath, targetApiIndexPath, + targetApiIndexDeprecatedPath, targetApiIndexExperimentalPath, targetApiIndexSincePath, { + fs, + returnOutputFiles: true + }); + + return Object.keys(resourceMap).map((resPath) => { + return resourceFactory.createResource({ + path: resPath, + string: resourceMap[resPath] + }); + }); +}; + +module.exports = apiIndexGenerator; diff --git a/lib/processors/jsdoc/jsdocGenerator.js b/lib/processors/jsdoc/jsdocGenerator.js new file mode 100644 index 000000000..4c1d766c5 --- /dev/null +++ b/lib/processors/jsdoc/jsdocGenerator.js @@ -0,0 +1,164 @@ +const spawn = require("child_process").spawn; +const fs = require("graceful-fs"); +const path = require("path"); +const {promisify} = require("util"); +const writeFile = promisify(fs.writeFile); +const {resourceFactory} = require("@ui5/fs"); + +/** + * JSDoc generator + * + * @public + * @alias module:@ui5/builder.processors.jsdocGenerator + * @param {Object} parameters Parameters + * @param {string} parameters.sourcePath Path of the source files to be processed + * @param {string} parameters.targetPath Path to write any output files + * @param {string} parameters.tmpPath Path to write temporary and debug files + * @param {Object} parameters.options Options + * @param {string} parameters.options.projectName Project name + * @param {string} parameters.options.namespace Namespace to build (e.g. some/project/name) + * @param {string} parameters.options.version Project version + * @param {Array} [parameters.options.variants=["apijson"]] JSDoc variants to be built + * @returns {Promise} Promise resolving with newly created resources + */ +const jsdocGenerator = async function({sourcePath, targetPath, tmpPath, options} = {}) { + if (!sourcePath || !targetPath || !tmpPath || !options.projectName || !options.namespace || !options.version) { + throw new Error("[jsdocGenerator]: One or more mandatory parameters not provided"); + } + + if (!options.variants || options.variants.length === 0) { + options.variants = ["apijson"]; + } + + const config = await jsdocGenerator._generateJsdocConfig({ + targetPath, + tmpPath, + namespace: options.namespace, + projectName: options.projectName, + version: options.version, + variants: options.variants + }); + + const configPath = await jsdocGenerator._writeJsdocConfig(tmpPath, config); + + await jsdocGenerator._buildJsdoc({ + sourcePath, + configPath + }); + + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: targetPath, + virBasePath: "/" + }); + + // create resources from the output files + return Promise.all([ + fsTarget.byPath(`/test-resources/${options.namespace}/designtime/api.json`) + // fsTarget.byPath(`/libraries/${options.projectName}.js`) + ]).then((res) => res.filter(($)=>$)); +}; + + +/** + * Generate jsdoc-config.json content + * + * @private + * @param {Object} parameters Parameters + * @param {string} parameters.targetPath Path to write any output files + * @param {string} parameters.tmpPath Path to write temporary and debug files + * @param {string} parameters.projectName Project name + * @param {string} parameters.version Project version + * @param {Array} parameters.variants JSDoc variants to be built + * @returns {string} jsdoc-config.json content string + */ +async function generateJsdocConfig({targetPath, tmpPath, namespace, projectName, version, variants}) { + // Backlash needs to be escaped as double-backslash + // This is not only relevant for win32 paths but also for + // Unix directory names that contain a backslash in their name + const backslashRegex = /\\/g; + + // Resolve path to this script to get the path to the JSDoc extensions folder + const jsdocPath = path.normalize(__dirname); + const pluginPath = path.join(jsdocPath, "lib", "ui5", "plugin.js").replace(backslashRegex, "\\\\"); + const templatePath = path.join(jsdocPath, "lib", "ui5", "template").replace(backslashRegex, "\\\\"); + const destinationPath = path.normalize(tmpPath).replace(backslashRegex, "\\\\"); + const jsapiFilePath = path.join(targetPath, "libraries", projectName + ".js").replace(backslashRegex, "\\\\"); + const apiJsonFolderPath = path.join(tmpPath, "dependency-apis").replace(backslashRegex, "\\\\"); + const apiJsonFilePath = + path.join(targetPath, "test-resources", path.normalize(namespace), "designtime", "api.json") + .replace(backslashRegex, "\\\\"); + + const config = `{ + "plugins": ["${pluginPath}"], + "opts": { + "recurse": true, + "lenient": true, + "template": "${templatePath}", + "ui5": { + "saveSymbols": true + }, + "destination": "${destinationPath}" + }, + "templates": { + "ui5": { + "variants": ${JSON.stringify(variants)}, + "version": "${version}", + "jsapiFile": "${jsapiFilePath}", + "apiJsonFolder": "${apiJsonFolderPath}", + "apiJsonFile": "${apiJsonFilePath}" + } + } + }`; + return config; +} + +/** + * Write jsdoc-config.json to file system + * + * @private + * @param {string} targetDirPath Directory Path to write the jsdoc-config.json file to + * @param {string} config jsdoc-config.json content + * @returns {string} Full path to the written jsdoc-config.json file + */ +async function writeJsdocConfig(targetDirPath, config) { + const configPath = path.join(targetDirPath, "jsdoc-config.json"); + await writeFile(configPath, config); + return configPath; +} + + +/** + * Execute JSDoc build by spawning JSDoc as an external process + * + * @private + * @param {Object} parameters Parameters + * @param {string} parameters.sourcePath Project resources (input for JSDoc generation) + * @param {string} parameters.configPath Full path to jsdoc-config.json file + * @returns {Promise} + */ +async function buildJsdoc({sourcePath, configPath}) { + const args = [ + require.resolve("jsdoc/jsdoc"), + "-c", + configPath, + "--verbose", + sourcePath + ]; + return new Promise((resolve, reject) => { + const child = spawn("node", args, { + stdio: ["ignore", "ignore", process.stderr] + }); + child.on("close", function(code) { + if (code === 0 || code === 1) { + resolve(); + } else { + reject(new Error(`JSDoc child process closed with code ${code}`)); + } + }); + }); +} + +module.exports = jsdocGenerator; +module.exports._generateJsdocConfig = generateJsdocConfig; +module.exports._writeJsdocConfig = writeJsdocConfig; +module.exports._buildJsdoc = buildJsdoc; diff --git a/lib/processors/jsdoc/lib/create-api-index.js b/lib/processors/jsdoc/lib/create-api-index.js new file mode 100644 index 000000000..a2186a8e5 --- /dev/null +++ b/lib/processors/jsdoc/lib/create-api-index.js @@ -0,0 +1,452 @@ +/* + * Node script to create cross-library API index files for use in the UI5 SDKs. + * + * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +"use strict"; +const path = require("path"); +const log = (function() { + try { + return require("@ui5/logger").getLogger("builder:processors:jsdoc:create-api-index"); + } catch (error) { + /* eslint-disable no-console */ + return { + info: function info(...msg) { + console.log("[INFO]", ...msg); + }, + error: function error(...msg) { + console.error(...msg); + } + }; + /* eslint-enable no-console */ + } +}()); + +function process(versionInfoFile, unpackedTestresourcesRoot, targetFile, targetFileDeprecated, targetFileExperimental, targetFileSince, options) { + const fs = options && options.fs || require("fs"); + const returnOutputFiles = options && !!options.returnOutputFiles; + + log.info("creating API index files"); + log.info(" sap-ui-version.json: " + versionInfoFile); + log.info(" unpacked test-resources: " + unpackedTestresourcesRoot); + log.info(" target file: " + targetFile); + log.info(" target file deprecated: " + targetFileDeprecated); + log.info(" target file experimental: " + targetFileExperimental); + log.info(" target file since: " + targetFileSince); + if (options && options.fs) { + log.info("Using custom fs"); + } + if (returnOutputFiles) { + log.info("Returning output files instead of writing to fs.") + } + log.info(""); + + // Deprecated, Experimental and Since collections + let oListCollection = { + deprecated: { + noVersion: { + apis: [] + } + }, + experimental: { + noVersion: { + apis: [] + } + }, + since: {} + }; + + function readJSONFile(file) { + return new Promise(function (resolve, reject) { + fs.readFile(file, 'utf8', function (err, data) { + if (err) { + reject(err); + } else { + // Handle empty files scenario + if (data.trim() === "") { + resolve({}); + } else { + resolve(JSON.parse(String(data))); + } + } + }); + }); + } + + function mkdirSync(dir) { + if (dir && !fs.existsSync(dir)) { + mkdirSync( path.dirname(dir) ); + fs.mkdirSync(dir); + } + } + + function writeJSON(file, content) { + return new Promise(function(resolve,reject) { + // Create dir if it does not exist + mkdirSync( path.dirname(file) ); + fs.writeFile(file, JSON.stringify(content), "utf-8", function(err) { + if ( err ) { + reject(err); + return; + } + resolve(true); + }); + }); + } + + /* + * Extracts main symbol information from a library api.json. + * Also collects deprecated, experimental and since api's. + * Returns a promise that resolves with an array of symbols. + */ + function createSymbolSummaryForLib(lib) { + let file = path.join(unpackedTestresourcesRoot, lib.replace(/\./g, "/"), "designtime/api.json"); + + return readJSONFile(file).then(function (apijson) { + if (!apijson.hasOwnProperty("symbols") || !Array.isArray(apijson.symbols)) { + // Ignore libraries with invalid api.json content like empty object or non-array "symbols" property. + return []; + } + return apijson.symbols.map(symbol => { + let oReturn = { + name: symbol.name, + kind: symbol.kind, + visibility: symbol.visibility, + extends: symbol.extends, + implements: symbol.implements, + lib: lib + }; + // We add deprecated member only when the control is deprecated to keep file size at check + if (symbol.deprecated) { + oReturn.deprecated = true; + } + collectLists(symbol); + return oReturn; + }); + }) + } + + /* + * Collects Deprecated, Experimental and Since data from passed symbol + * including symbol itself, methods and events. + */ + function collectLists(oSymbol) { + + function addData(oDataType, oEntityObject, sObjectType, sSymbolName) { + let sSince = oDataType !== "since" ? oEntityObject[oDataType].since : oEntityObject.since, + oData = { + control: sSymbolName, + text: oEntityObject[oDataType].text || oEntityObject.description, + type: sObjectType, + "static": !!oEntityObject.static, + visibility: oEntityObject.visibility + }; + + // For class we skip entityName + if (sObjectType !== "class") { + oData.entityName = oEntityObject.name; + } + + if (sSince && sSince !== "undefined" /* Sometimes sSince comes as string "undefined" */) { + // take only major and minor versions + let sVersion = sSince.split(".").slice(0, 2).join("."); + + oData.since = sSince; + + if (!oListCollection[oDataType][sVersion]) { + oListCollection[oDataType][sVersion] = { + name: sVersion, + apis: [] + }; + } + + oListCollection[oDataType][sVersion].apis.push(oData); + } else if (oDataType !== "since" /* noVersion does not make sense for since and will fail */) { + oListCollection[oDataType].noVersion.apis.push(oData); + } + } + + // Classes + if (oSymbol.deprecated) { + addData("deprecated", oSymbol, "class", oSymbol.name); + } + + if (oSymbol.experimental) { + addData("experimental", oSymbol, "class", oSymbol.name); + } + + if (oSymbol.since && oSymbol.since !== "undefined" /* Sometimes sSince comes as string "undefined" */) { + addData("since", oSymbol, "class", oSymbol.name); + } + + // Methods + oSymbol.methods && oSymbol.methods.forEach(oMethod => { + if (oMethod.deprecated) { + addData("deprecated", oMethod, "methods", oSymbol.name); + } + + if (oMethod.experimental) { + addData("experimental", oMethod, "methods", oSymbol.name); + } + + if (oMethod.since) { + addData("since", oMethod, "methods", oSymbol.name); + } + }); + + // Events + oSymbol.events && oSymbol.events.forEach(oEvent => { + if (oEvent.deprecated) { + addData("deprecated", oEvent, "events", oSymbol.name); + } + + if (oEvent.experimental) { + addData("experimental", oEvent, "events", oSymbol.name); + } + + if (oEvent.since) { + addData("since", oEvent, "events", oSymbol.name); + } + }); + + } + + function deepMerge(arrayOfArrays) { + return arrayOfArrays.reduce((array, items) => { + array.push.apply(array, items); + return array; + }, []); + } + + function expandHierarchyInfo(symbols) { + let byName = new Map(); + symbols.forEach(symbol => { + byName.set(symbol.name, symbol); + }); + symbols.forEach(symbol => { + let parent = symbol.extends && byName.get(symbol.extends); + if (parent) { + parent.extendedBy = parent.extendedBy ||  []; + parent.extendedBy.push(symbol.name); + } + if (symbol.implements) { + symbol.implements.forEach(intfName => { + let intf = byName.get(intfName); + if (intf) { + intf.implementedBy = intf.implementedBy ||  []; + intf.implementedBy.push(symbol.name); + } + }); + } + }); + return symbols; + } + + function convertListToTree(symbols) { + let aTree = []; + + // Filter out black list libraries + symbols = symbols.filter(({lib}) => ["sap.ui.demokit", "sap.ui.documentation"].indexOf(lib) === -1); + + // Create treeName and displayName + symbols.forEach(oSymbol => { + oSymbol.treeName = oSymbol.name.replace(/^module:/, "").replace(/\//g, "."); + oSymbol.displayName = oSymbol.treeName.split(".").pop(); + oSymbol.bIsDeprecated = !!oSymbol.deprecated; + }); + + // Create missing - virtual namespaces + symbols.forEach(oSymbol => { + oSymbol.treeName.split(".").forEach((sPart, i, a) => { + let sName = a.slice(0, (i + 1)).join("."); + + if (!symbols.find(o => o.treeName === sName)) { + symbols.push({ + name: sName, + treeName: sName, + displayName: sPart, + lib: oSymbol.lib, + kind: "namespace", + bIsDeprecated: false // Virtual namespace can't be deprecated + }); + } + }); + }); + + // Discover parents + symbols.forEach(oSymbol => { + let aParent = oSymbol.treeName.split("."), + sParent; + + // Extract parent name + aParent.pop(); + sParent = aParent.join("."); + + // Mark parent + if (symbols.find(({treeName}) => treeName === sParent)) { + oSymbol.parent = sParent; + } + }); + + // Sort the list before building the tree + symbols.sort((a, b) => { + let sA = a.treeName.toUpperCase(), + sB = b.treeName.toUpperCase(); + + if (sA < sB) return -1; + if (sA > sB) return 1; + return 0; + }); + + // Build tree + symbols.forEach(oSymbol => { + if (oSymbol.parent) { + let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent); + + if (!oParent.nodes) oParent.nodes = []; + oParent.nodes.push(oSymbol); + } else { + aTree.push(oSymbol); + } + }); + + // Custom sort first level tree items - "sap" namespace should be on top + aTree.sort((a, b) => { + let sA = a.displayName.toUpperCase(), + sB = b.displayName.toUpperCase(); + + if (sA === "SAP") return -1; + if (sB === "SAP") return 1; + if (sA < sB) return -1; + if (sA > sB) return 1; + + return 0; + }); + + // Clean tree - keep file size down + function cleanTree (oSymbol) { + delete oSymbol.treeName; + delete oSymbol.parent; + if (oSymbol.children) { + oSymbol.children.forEach(o => cleanTree(o)); + } + } + aTree.forEach(o => cleanTree(o)); + + return aTree; + } + + function createOverallIndex() { + let version = "0.0.0"; + const filesToReturn = {}; + + var p = readJSONFile(versionInfoFile) + .then(versionInfo => { + version = versionInfo.version; + return Promise.all( + versionInfo.libraries.map( + lib => createSymbolSummaryForLib(lib.name).catch(err => { + // ignore 'file not found' errors as some libs don't have an api.json (themes, server libs) + if (err.code === 'ENOENT') { + return []; + } + throw err; + }) + ) + ); + }) + .then(deepMerge) + .then(expandHierarchyInfo) + .then(convertListToTree) + .then(symbols => { + let result = { + "$schema-ref": "http://schemas.sap.com/sapui5/designtime/api-index.json/1.0", + version: version, + library: "*", + symbols: symbols + }; + if (returnOutputFiles) { + filesToReturn[targetFile] = JSON.stringify(result); + } else { + return writeJSON(targetFile, result); + } + }) + .then(() => { + /* Lists - modify and cleanup */ + let sortList = function (oList) { + /* Sorting since records */ + let aKeys = Object.keys(oList), + oSorted = {}; + + aKeys.sort((a, b) => { + let aA = a.split("."), + aB = b.split("."); + + if (a === "noVersion") { + return 1; /* No version always at end of list */ + } + + if (b === "noVersion") { + return -1; /* No version always at end of list */ + } + + // Normalize old versions 1.4 to 1.04 for proper sorting + a = [aA[0], ('0' + aA[1]).slice(-2)].join(""); + b = [aB[0], ('0' + aB[1]).slice(-2)].join(""); + + // Sort descending + return parseInt(b, 10) - parseInt(a, 10); + }); + + aKeys.forEach((sKey) => { + oSorted[sKey] = oList[sKey]; + }); + + return oSorted; + }; + + /* Since */ + oListCollection.since = sortList(oListCollection.since); + + /* Deprecated */ + oListCollection.deprecated = sortList(oListCollection.deprecated); + if (!oListCollection.deprecated.noVersion.apis.length) { + delete oListCollection.deprecated.noVersion; + } + + /* Experimental */ + oListCollection.experimental = sortList(oListCollection.experimental); + if (!oListCollection.experimental.noVersion.apis.length) { + delete oListCollection.experimental.noVersion; + } + }) + .then(() => { + if (returnOutputFiles) { + filesToReturn[targetFileDeprecated] = JSON.stringify(oListCollection.deprecated); + filesToReturn[targetFileExperimental] = JSON.stringify(oListCollection.experimental); + filesToReturn[targetFileSince] = JSON.stringify(oListCollection.since); + return filesToReturn; + } else { + return Promise.all([ + // write deprecated, experimental and since collections in the respective index files + writeJSON(targetFileDeprecated, oListCollection.deprecated), + writeJSON(targetFileExperimental, oListCollection.experimental), + writeJSON(targetFileSince, oListCollection.since) + ]); + } + }) + .catch(err => { + log.error("**** failed to create API index for libraries:", err) + throw err; + }); + + return p; + } + + return createOverallIndex(); + +} + +module.exports = process; diff --git a/lib/processors/jsdoc/lib/transform-apijson-for-sdk.js b/lib/processors/jsdoc/lib/transform-apijson-for-sdk.js new file mode 100644 index 000000000..c8e27a71a --- /dev/null +++ b/lib/processors/jsdoc/lib/transform-apijson-for-sdk.js @@ -0,0 +1,1994 @@ +/* + * Node script to preprocess api.json files for use in the UI5 SDKs. + * + * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +"use strict"; +const cheerio = require("cheerio"); +const path = require('path'); +const log = (function() { + try { + return require("@ui5/logger").getLogger("builder:processors:jsdoc:transform-apijson-for-sdk"); + } catch (error) { + /* eslint-disable no-console */ + return { + info: function info(...msg) { + console.log("[INFO]", ...msg); + }, + error: function error(...msg) { + console.error(...msg); + } + }; + /* eslint-enable no-console */ + } +}()); + +/* + * Transforms the api.json as created by the JSDoc build into a pre-processed api.json file suitable for the SDK. + * + * The pre-processing includes formatting of type references, rewriting of links and other time consuming calculations. + * + * @param {string} sInputFile Path of the original api.json file that should be transformed + * @param {string} sOutputFile Path that the transformed api.json file should should be written to + * @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 + * @returns {Promise} A Promise that resolves after the transformation has been completed + */ +function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, options) { + const fs = options && options.fs || require("fs"); + const returnOutputFiles = options && !!options.returnOutputFiles; + + log.info("Transform API index files for sap.ui.documentation"); + log.info(" original file: " + sInputFile); + log.info(" output file: " + sOutputFile); + log.info(" library file: " + sLibraryFile); + log.info(" dependency dir: " + vDependencyAPIFiles); + if (options && options.fs) { + log.info("Using custom fs."); + } + if (returnOutputFiles) { + log.info("Returning output files instead of writing to fs.") + } + log.info(""); + + /** + * Transforms api.json file + * @param {object} oChainObject chain object + */ + let transformApiJson = function (oChainObject) { + function isBuiltInType(type) { + return formatters._baseTypes.indexOf(type) >= 0; + } + + /** + * Heuristically determining if there is a possibility the given input string + * to be a UI5 symbol + * @param {string} sName + * @returns {boolean} + */ + function possibleUI5Symbol(sName) { + return /^[a-zA-Z][a-zA-Z.]*[a-zA-Z]$/.test(sName); + } + + // Function is a copy from: LibraryInfo.js => LibraryInfo.prototype._getActualComponent => "match" inline method + function matchComponent(sModuleName, sPattern) { + sModuleName = sModuleName.toLowerCase(); + sPattern = sPattern.toLowerCase(); + return ( + sModuleName === sPattern + || sPattern.match(/\*$/) && sModuleName.indexOf(sPattern.slice(0,-1)) === 0 // simple prefix match + || sPattern.match(/\.\*$/) && sModuleName === sPattern.slice(0,-2) // directory pattern also matches directory itself + ); + } + + /** + * Pre-processes the symbols list - creating virtual namespace records and attaching children list to namespace + * records. + * @param {object} symbols list + */ + function preProcessSymbols(symbols) { + // Create treeName and modify module names + symbols.forEach(oSymbol => { + let sModuleClearName = oSymbol.name.replace(/^module:/, ""); + oSymbol.displayName = sModuleClearName; + oSymbol.treeName = sModuleClearName.replace(/\//g, "."); + }); + + // Create missing - virtual namespaces + symbols.forEach(oSymbol => { + oSymbol.treeName.split(".").forEach((sPart, i, a) => { + let sName = a.slice(0, (i + 1)).join("."); + + if (!symbols.find(o => o.treeName === sName)) { + symbols.push({ + name: sName, + displayName: sName, + treeName: sName, + lib: oSymbol.lib, + kind: "namespace" + }); + } + }); + }); + + // Discover parents + symbols.forEach(oSymbol => { + let aParent = oSymbol.treeName.split("."), + sParent; + + // Extract parent name + aParent.pop(); + sParent = aParent.join("."); + + // Mark parent + if (symbols.find(({treeName}) => treeName === sParent)) { + oSymbol.parent = sParent; + } + }); + + // Attach children info + symbols.forEach(oSymbol => { + if (oSymbol.parent) { + let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent); + + if (!oParent.nodes) oParent.nodes = []; + oParent.nodes.push({ + name: oSymbol.displayName, + description: formatters._preProcessLinksInTextBlock(oSymbol.description), + href: "#/api/" + encodeURIComponent(oSymbol.name) + }); + } + }); + + // Clean list - keep file size down + symbols.forEach(o => { + delete o.treeName; + delete o.parent; + }); + } + + // Transform to object + let oData = oChainObject.fileData; + + // Attach default component for the library if available + if (oChainObject.defaultComponent) { + oData.defaultComponent = oChainObject.defaultComponent; + } + + formatters._oOwnLibrary = oData; + + // Pre process symbols + preProcessSymbols(oData.symbols); + + // Apply formatter's and modify data as needed + oData.symbols.forEach((oSymbol) => { + + // when the module name starts with the library name, then we apply the default component + if (oSymbol.name.indexOf(oData.library) === 0) { + oSymbol.component = oChainObject.defaultComponent; + } + + // Attach symbol specific component if available (special cases) + // Note: Last hit wins as there may be a more specific component pattern + if (oChainObject.customSymbolComponents) { + Object.keys(oChainObject.customSymbolComponents).forEach(sComponent => { + if (matchComponent(oSymbol.name, sComponent)) { + oSymbol.component = oChainObject.customSymbolComponents[sComponent]; + } + }); + } + + // Attach symbol sample flag if available + if (oChainObject.entitiesWithSamples) { + oSymbol.hasSample = oChainObject.entitiesWithSamples.indexOf(oSymbol.name) >= 0; + } + + // Apply settings to formatter object - needed until formatter's are rewritten + formatters._sTopicId = oSymbol.name; + formatters._oTopicData = oSymbol; + + // Format Page Title + oSymbol.title = (oSymbol.abstract ? "abstract " : "") + oSymbol.kind + " " + oSymbol.displayName; + oSymbol.subTitle = formatters.formatSubtitle(oSymbol.deprecated); + + // Constructor + if (oSymbol.constructor) { + let oConstructor = oSymbol.constructor; + + // Description + if (oConstructor.description) { + oConstructor.description = formatters.formatDescription(oConstructor.description); + } + + // References + formatters.modifyReferences(oSymbol, true); + + // Examples + if (oConstructor.examples) { + oConstructor.examples.forEach((oExample) => { + oExample.data = formatters.formatExample(oExample.caption, oExample.text); + + // Keep file size in check + if (oExample.caption) { + delete oExample.caption; + } + if (oExample.text) { + delete oExample.text; + } + }); + } + + // Code Example string + oConstructor.codeExample = formatters.formatConstructor(oSymbol.name, oConstructor.parameters); + + // Parameters + if (oConstructor.parameters) { + oConstructor.parameters = methods.buildConstructorParameters(oConstructor.parameters); + + let aParameters = oConstructor.parameters; + aParameters.forEach(oParameter => { + + // Types + oParameter.types = []; + if (oParameter.type) { + let aTypes = oParameter.type.split("|"); + + for (let i = 0; i < aTypes.length; i++) { + oParameter.types.push({ + name: aTypes[i], + linkEnabled: !isBuiltInType(aTypes[i]) + }); + } + + // Keep file size in check + delete oParameter.type; + } + + // Default value + oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue); + + // Description + if (oParameter.description) { + oParameter.description = formatters.formatDescription(oParameter.description); + } + + }) + } + + // Throws + if (oConstructor.throws) { + oConstructor.throws.forEach(oThrows => { + + // Description + if (oThrows.description) { + oThrows.description = formatters.formatDescription(oThrows.description); + } + + // Exception link enabled + if (oThrows.type) { + oThrows.linkEnabled = formatters.formatExceptionLink(oThrows.type); + } + + }); + } + } + + // Description + if (oSymbol.description) { + oSymbol.description = formatters.formatOverviewDescription(oSymbol.description, oSymbol.constructor.references); + } + + // Deprecated + if (oSymbol.deprecated) { + oSymbol.deprecatedText = formatters.formatDeprecated(oSymbol.deprecated.since, oSymbol.deprecated.text); + // Keep file size in check + delete oSymbol.deprecated; + } + + // Properties + if (oSymbol.properties) { + oSymbol.properties.forEach((oProperty) => { + + // Name + oProperty.name = formatters.formatEntityName(oProperty.name, oSymbol.name, oProperty.static); + + // Description + if (oProperty.deprecated) { + oProperty.description = formatters.formatDescription(oProperty.description, + oProperty.deprecated.text, oProperty.deprecated.since); + } else { + oProperty.description = formatters.formatDescription(oProperty.description); + } + + // Link Enabled + if (oSymbol.kind !== "enum" && !isBuiltInType(oProperty.type) && possibleUI5Symbol(oProperty.type)) { + oProperty.linkEnabled = true; + oProperty.href = "#/api/" + oProperty.type.replace("[]", ""); + } + + // Keep file size in check + if (oProperty.static) { + delete oProperty.static; + } + + if (oSymbol.kind === "enum" || oProperty.type === "undefined") { + delete oProperty.type; + } + + }); + } + + // UI5 Metadata + if (oSymbol["ui5-metadata"]) { + let oMeta = oSymbol["ui5-metadata"]; + + // Properties + if (oMeta.properties) { + // Sort + oMeta.properties.sort(function (a, b) { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + }); + + // Pre-process + oMeta.properties.forEach((oProperty) => { + // Name + oProperty.name = formatters.formatEntityName(oProperty.name, oSymbol.name, oProperty.static); + + // Description + oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since); + + // Link Enabled + if (!isBuiltInType(oProperty.type)) { + oProperty.linkEnabled = true; + } + + // Default value + oProperty.defaultValue = formatters.formatDefaultValue(oProperty.defaultValue); + + // Deprecated + if (oProperty.deprecated) { + oProperty.deprecatedText = formatters.formatDeprecated(oProperty.deprecated.since, + oProperty.deprecated.text); + + // Keep file size in check + delete oProperty.deprecated; + } + }); + } + + // Aggregations + if (oMeta.aggregations) { + // Sort + oMeta.aggregations.sort(function (a, b) { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + }); + + // Pre-process + oMeta.aggregations.forEach((oAggregation) => { + // Link Enabled + if (!isBuiltInType(oAggregation.type)) { + oAggregation.linkEnabled = true; + } + + // Description + if (oAggregation.deprecated) { + oAggregation.description = formatters.formatDescription(oAggregation.description, + oAggregation.deprecated.text, oAggregation.deprecated.since); + } else { + oAggregation.description = formatters.formatDescription(oAggregation.description); + } + + // Link enabled + oAggregation.linkEnabled = !isBuiltInType(oAggregation.type); + }); + } + + // Associations + + if (oMeta.associations) { + // Sort + oMeta.associations.sort(function (a, b) { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + }); + + // Pre-process + oMeta.associations.forEach((oAssociation) => { + // Link Enabled + if (!isBuiltInType(oAssociation.type)) { + oAssociation.linkEnabled = true; + } + + // Description + if (oAssociation.deprecated) { + oAssociation.description = formatters.formatDescription(oAssociation.description, + oAssociation.deprecated.text, oAssociation.deprecated.since); + } else { + oAssociation.description = formatters.formatDescription(oAssociation.description); + } + }); + } + + // Events + if (oMeta.events) { + // We don't need event's data from the UI5-metadata for now. Keep file size in check + delete oMeta.events; + } + + // Special Settings + if (oMeta.specialSettings) { + oMeta.specialSettings.forEach(oSetting => { + + // Link Enabled + if (!isBuiltInType(oSetting.type)) { + oSetting.linkEnabled = true; + } + + // Description + if (oSetting.deprecated) { + oSetting.description = formatters.formatDescription(oSetting.description, + oSetting.deprecated.text, oSetting.deprecated.since); + } else { + oSetting.description = formatters.formatDescription(oSetting.description); + } + + }); + } + + // Annotations + if (oMeta.annotations) { + oMeta.annotations.forEach(oAnnotation => { + + // Description + oAnnotation.description = formatters.formatAnnotationDescription(oAnnotation.description, + oAnnotation.since); + + // Namespace + oAnnotation.namespaceText = oAnnotation.namespace; + oAnnotation.namespace = formatters.formatAnnotationNamespace(oAnnotation.namespace); + + // Target + oAnnotation.target = formatters.formatAnnotationTarget(oAnnotation.target); + + // Applies to + oAnnotation.appliesTo = formatters.formatAnnotationTarget(oAnnotation.appliesTo); + + }); + } + + } + + if (oSymbol.events) { + + // Pre-process events + methods.buildEventsModel(oSymbol.events); + + oSymbol.events.forEach(oEvent => { + + // Description + if (oEvent.description) { + oEvent.description = formatters.formatDescriptionSince(oEvent.description, oEvent.since); + } + + // Deprecated + if (oEvent.deprecated) { + oEvent.deprecatedText = formatters.formatEventDeprecated(oEvent.deprecated.since, + oEvent.deprecated.text); + } + + // Parameters + if (oEvent.parameters && Array.isArray(oEvent.parameters)) { + oEvent.parameters.forEach(oParameter => { + + // Link Enabled + if (!isBuiltInType(oParameter.type)) { + oParameter.linkEnabled = true; + } + + // Description + if (oParameter.deprecated) { + oParameter.description = formatters.formatDescription(oParameter.description, + oParameter.deprecated.text, oParameter.deprecated.since); + } else { + oParameter.description = formatters.formatDescription(oParameter.description); + } + + }); + } + + }); + + } + + // Methods + if (oSymbol.methods) { + + // Pre-process methods + methods.buildMethodsModel(oSymbol.methods); + + oSymbol.methods.forEach(oMethod => { + + // Name and link + if (oMethod.name) { + oMethod.name = formatters.formatEntityName(oMethod.name, oSymbol.name, oMethod.static); + + // Link + oMethod.href = "#/api/" + encodeURIComponent(oSymbol.name) + + "/methods/" + encodeURIComponent(oMethod.name); + } + + formatters.formatReferencesInDescription(oMethod); + + // Description + if (oMethod.description) { + oMethod.description = formatters.formatDescription(oMethod.description); + } + + // Examples + oMethod.examples && oMethod.examples.forEach(oExample => { + oExample = formatters.formatExample(oExample.caption, oExample.text); + }); + + // Deprecated + if (oMethod.deprecated) { + oMethod.deprecatedText = formatters.formatEventDeprecated(oMethod.deprecated.since, + oMethod.deprecated.text); + } + + // Code example + oMethod.code = formatters.formatMethodCode(oMethod.name, oMethod.parameters, oMethod.returnValue); + + // Parameters + if (oMethod.parameters) { + oMethod.parameters.forEach(oParameter => { + + // Types + if (oParameter.types) { + oParameter.types.forEach(oType => { + + // Link Enabled + if (!isBuiltInType(oType.value) && possibleUI5Symbol(oType.value)) { + oType.linkEnabled = true; + oType.href = "#/api/" + oType.value.replace("[]", ""); + } + + }); + } + + // Default value + oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue); + + // Description + if (oParameter.deprecated) { + oParameter.description = formatters.formatDescription(oParameter.description, + oParameter.deprecated.text, oParameter.deprecated.since); + } else { + oParameter.description = formatters.formatDescription(oParameter.description); + } + + }); + } + + // Return value + if (oMethod.returnValue) { + + // Description + oMethod.returnValue.description = formatters.formatDescription(oMethod.returnValue.description); + + // Types + if (oMethod.returnValue.types) { + oMethod.returnValue.types.forEach(oType => { + + // Link Enabled + if (!isBuiltInType(oType.value)) { + oType.href = "#/api/" + encodeURIComponent(oType.value.replace("[]", "")); + oType.linkEnabled = true; + } + + }); + } + + } + + // Throws + if (oMethod.throws) { + oMethod.throws.forEach(oThrows => { + + // Description + if (oThrows.description) { + oThrows.description = formatters.formatDescription(oThrows.description); + } + + // Exception link enabled + if (oThrows.type) { + oThrows.linkEnabled = formatters.formatExceptionLink(oThrows.type); + } + + }); + } + + // Examples + if (oMethod.examples) { + oMethod.examples.forEach((oExample) => { + oExample.data = formatters.formatExample(oExample.caption, oExample.text); + + // Keep file size in check + if (oExample.caption) { + delete oExample.caption; + } + if (oExample.text) { + delete oExample.text; + } + }); + + } + + + }); + } + + // Formatting namespaces, functions and enums, which may contain examples + // or references with links to process them similar to methods and constructors of classes + + if (oSymbol.kind !== "class") { + + if (oSymbol.examples) { + oSymbol.examples.forEach((oExample) => { + oExample.data = formatters.formatExample(oExample.caption, oExample.text); + // Keep file size in check + if (oExample.caption) { + delete oExample.caption; + } + if (oExample.text) { + delete oExample.text; + } + }); + } + + if (oSymbol.references) { + formatters.modifyReferences(oSymbol); + formatters.formatReferencesInDescription(oSymbol); + } + + // Description + if (oSymbol.description) { + oSymbol.description = formatters.formatDescription(oSymbol.description); + } + } + }); + + oChainObject.parsedData = oData; + + return oChainObject; + }; + + function getDependencyLibraryFilesList(oChainObject) { + // if vDependencyAPIFiles is an array, it contains the file paths of api.json files + if ( Array.isArray(vDependencyAPIFiles) ) { + return oChainObject; + } + + // otherwise, it names a directory that has to be scanned for the files + return new Promise(oResolve => { + fs.readdir(vDependencyAPIFiles, function (oError, aItems) { + if (!oError && aItems && aItems.length) { + let aFiles = []; + aItems.forEach(sItem => { + aFiles.push(path.join(vDependencyAPIFiles, sItem)); + }) + oChainObject.aDependentLibraryFiles = aFiles; + } + oResolve(oChainObject); // We don't fail if no dependency library files are available + }); + }); + } + + function loadDependencyLibraryFiles (oChainObject) { + if (!oChainObject.aDependentLibraryFiles) { + return oChainObject; + } + let aPromises = []; + oChainObject.aDependentLibraryFiles.forEach(sFile => { + aPromises.push(new Promise(oResolve => { + fs.readFile(sFile, 'utf8', (oError, oData) => { + oResolve(oError ? false : oData); + }); + })); + }); + return Promise.all(aPromises).then(aValues => { + let oDependentAPIs = {}; + + aValues.forEach(sData => { + let oData; + + try { + oData = JSON.parse(sData); + } catch (e) { + // Silence possible dependency library invalid json errors as they are not critical + // and we should not fail BCP: 1870560058 + } + + // OpenUI5 build specific - own library can be listed as dependency library also. + // 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; + } + }); + + oChainObject.oDependentAPIs = oDependentAPIs; + return oChainObject; + }) + } + + /** + * Create api.json from parsed data + * @param oChainObject chain object + */ + function createApiRefApiJson(oChainObject) { + if (returnOutputFiles) { + // If requested, return data instead of writing to FS (required by UI5 Tooling/UI5 Builder) + return JSON.stringify(oChainObject.parsedData); + } + let sOutputDir = path.dirname(oChainObject.outputFile); + + // Create dir if it does not exist + if (!fs.existsSync(sOutputDir)) { + fs.mkdirSync(sOutputDir); + } + + // Write result to file + fs.writeFileSync(oChainObject.outputFile, JSON.stringify(oChainObject.parsedData) /* Transform back to string */, 'utf8'); + } + + /** + * Load .library file + * @param oChainObject chain return object + * @returns {Promise} library file promise + */ + function getLibraryPromise(oChainObject) { + return new Promise(function(oResolve) { + fs.readFile(oChainObject.libraryFile, 'utf8', (oError, oData) => { + oChainObject.libraryFileData = oData; + oResolve(oChainObject); + }); + }); + } + + /** + * Extracts components list and docuindex.json relative path from .library file data + * @param {object} oChainObject chain object + * @returns {object} chain object + */ + function extractComponentAndDocuindexUrl(oChainObject) { + oChainObject.modules = []; + + if (oChainObject.libraryFileData) { + let $ = cheerio.load(oChainObject.libraryFileData, { + ignoreWhitespace: true, + xmlMode: true, + lowerCaseTags: false + }); + + // Extract documentation URL + oChainObject.docuPath = $("appData documentation").attr("indexUrl"); + + // Extract components + $("ownership > component").each((i, oComponent) => { + + if (oComponent.children) { + if (oComponent.children.length === 1) { + oChainObject.defaultComponent = $(oComponent).text(); + } else { + let sCurrentComponentName = $(oComponent).find("name").text(); + let aCurrentModules = []; + $(oComponent).find("module").each((a, oC) => { + aCurrentModules.push($(oC).text().replace(/\//g, ".")); + }); + + oChainObject.modules.push({ + componentName: sCurrentComponentName, + modules: aCurrentModules + }); + } + } + + }); + + } + + return oChainObject; + } + + /** + * Adds to the passed object custom symbol component map generated from the extracted components list + * to be easily searchable later + * @param {object} oChainObject chain object + * @returns {object} chain object + */ + function flattenComponents(oChainObject) { + if (oChainObject.modules && oChainObject.modules.length > 0) { + oChainObject.customSymbolComponents = {}; + oChainObject.modules.forEach(oComponent => { + let sCurrentComponent = oComponent.componentName; + oComponent.modules.forEach(sModule => { + oChainObject.customSymbolComponents[sModule] = sCurrentComponent; + }); + }); + } + + return oChainObject; + } + + /** + * Adds to the passed object array with entities which have explored samples + * @param {object} oChainObject chain object + * @returns {object} chain object + */ + function extractSamplesFromDocuIndex(oChainObject) { + // If we have not extracted docuPath we return early + if (!oChainObject.docuPath) { + return oChainObject; + } + return new Promise(function(oResolve) { + // Join .library path with relative docuindex.json path + let sPath = path.join(path.dirname(oChainObject.libraryFile), oChainObject.docuPath); + // Normalize path to resolve relative path + sPath = path.normalize(sPath); + + fs.readFile(sPath, 'utf8', (oError, oFileData) => { + if (!oError) { + oFileData = JSON.parse(oFileData); + if (oFileData.explored && oFileData.explored.entities && oFileData.explored.entities.length > 0) { + oChainObject.entitiesWithSamples = []; + oFileData.explored.entities.forEach(oEntity => { + oChainObject.entitiesWithSamples.push(oEntity.id); + }); + } + } + // We aways resolve as this data is not mandatory + oResolve(oChainObject); + }); + + }); + } + + /** + * Load api.json file + * @param {object} oChainObject chain object + * @returns {object} chain object + */ + function getAPIJSONPromise(oChainObject) { + return new Promise(function(oResolve, oReject) { + fs.readFile(sInputFile, 'utf8', (oError, sFileData) => { + if (oError) { + oReject(oError); + } else { + oChainObject.fileData = JSON.parse(sFileData); + oResolve(oChainObject); + } + }); + }); + } + + /* + * ===================================================================================================================== + * IMPORTANT NOTE: Formatter code is a copy from APIDetail.controller.js with a very little modification and mocking and + * code can be significantly improved + * ===================================================================================================================== + */ + let formatters = { + + _sTopicId: "", + _oTopicData: {}, + _baseTypes: [ + "sap.ui.core.any", + "sap.ui.core.object", + "sap.ui.core.function", + "sap.ui.core.number", // TODO discuss with Thomas, type does not exist + "sap.ui.core.float", + "sap.ui.core.int", + "sap.ui.core.boolean", + "sap.ui.core.string", + "sap.ui.core.void", + "null", + "any", + "any[]", + "Error", + "Error[]", + "array", + "element", + "Element", + "DomRef", + "object", + "Object", + "object[]", + "object|object[]", + "[object Object][]", + "Array.<[object Object]>", + "Object.", + "function", + "float", + "int", + "boolean", + "string", + "string[]", + "number", + "map", + "promise", + "Promise", + "document", + "Document", + "Touch", + "TouchList", + "undefined" + ], + ANNOTATIONS_LINK: 'http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part3-csdl.html', + ANNOTATIONS_NAMESPACE_LINK: 'http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/vocabularies/', + + /** + * Adds "deprecated" information if such exists to the header area + * @param deprecated - object containing information about deprecation + * @returns {string} - the deprecated text to display + */ + formatSubtitle: function (deprecated) { + var result = ""; + + if (deprecated) { + result += "Deprecated in version: " + deprecated.since; + } + + return result; + }, + + /** + * Formats the target and applies to texts of annotations + * @param target - the array of texts to be formatted + * @returns string - the formatted text + */ + formatAnnotationTarget: function (target) { + var result = ""; + + if (target) { + target.forEach(function (element) { + result += element + '
'; + }); + } + + result = this._preProcessLinksInTextBlock(result); + return result; + }, + + /** + * Formats the namespace of annotations + * @param namespace - the namespace to be formatted + * @returns string - the formatted text + */ + formatAnnotationNamespace: function (namespace) { + var result, + aNamespaceParts = namespace.split("."); + + if (aNamespaceParts[0] === "Org" && aNamespaceParts[1] === "OData") { + result = '' + namespace + ''; + } else { + result = namespace; + } + + result = this._preProcessLinksInTextBlock(result); + return result; + }, + + /** + * Formats the description of annotations + * @param description - the description of the annotation + * @param since - the since version information of the annotation + * @returns string - the formatted description + */ + formatAnnotationDescription: function (description, since) { + var result = description || ""; + + result += '
For more information, see ' + this.handleExternalUrl(this.ANNOTATIONS_LINK, "OData v4 Annotations"); + + if (since) { + result += '

Since: ' + since + '.'; + } + + result = this._preProcessLinksInTextBlock(result); + return result; + }, + + formatExceptionLink: function (linkText) { + linkText = linkText || ''; + return linkText.indexOf('sap.ui.') !== -1; + }, + + formatMethodCode: function (sName, aParams, aReturnValue) { + var result = '
' + sName + '(';
+
+			if (aParams && aParams.length > 0) {
+				/* We consider only root level parameters so we get rid of all that are not on the root level */
+				aParams = aParams.filter(oElem => {
+					return oElem.depth === undefined;
+				});
+				aParams.forEach(function (element, index, array) {
+					result += element.name;
+
+					if (element.optional) {
+						result += '?';
+					}
+
+					if (index < array.length - 1) {
+						result += ', ';
+					}
+				});
+			}
+
+			result += ') : ';
+
+			if (aReturnValue) {
+				result += aReturnValue.type;
+			} else {
+				result += 'void';
+			}
+
+			result += "
"; + + return result; + }, + + /** + * Formats method deprecation message and pre-process jsDoc link and code blocks + * @param {string} sSince since text + * @param {string} sDescription deprecation description text + * @returns {string} formatted deprecation message + */ + formatMethodDeprecated: function (sSince, sDescription) { + return this.formatDeprecated(sSince, sDescription, "methods"); + }, + + /** + * Formats event deprecation message and pre-process jsDoc link and code blocks + * @param {string} sSince since text + * @param {string} sDescription deprecation description text + * @returns {string} formatted deprecation message + */ + formatEventDeprecated: function (sSince, sDescription) { + return this.formatDeprecated(sSince, sDescription, "events"); + }, + + /** + * Formats the description of control properties + * @param description - the description of the property + * @param since - the since version information of the property + * @returns string - the formatted description + */ + formatDescriptionSince: function (description, since) { + var result = description || ""; + + if (since) { + result += '

Since: ' + since + '.'; + } + + result = this._preProcessLinksInTextBlock(result); + return result; + }, + + /** + * Formats the default value of the property as a string. + * @param defaultValue - the default value of the property + * @returns string - The default value of the property formatted as a string. + */ + formatDefaultValue: function (defaultValue) { + var sReturn; + + switch (defaultValue) { + case null: + case undefined: + sReturn = ''; + break; + case '': + sReturn = 'empty string'; + break; + default: + sReturn = defaultValue; + } + + return Array.isArray(sReturn) ? sReturn.join(', ') : sReturn; + }, + + /** + * Formats the constructor of the class + * @param name + * @param params + * @returns string - The code needed to create an object of that class + */ + formatConstructor: function (name, params) { + var result = '
new ';
+
+			if (name) {
+				result += name + '(';
+			}
+
+			if (params) {
+				params.forEach(function (element, index, array) {
+					result += element.name;
+
+					if (element.optional) {
+						result += '?';
+					}
+
+					if (index < array.length - 1) {
+						result += ', ';
+					}
+				});
+			}
+
+			if (name) {
+				result += ')
'; + } + + return result; + }, + + formatExample: function (sCaption, sText) { + return this.formatDescription( + ["Example: ", + sCaption, + "
",
+					sText,
+					"
"].join("") + ); + }, + + /** + * Formats the name of a property or a method depending on if it's static or not + * @param sName {string} - Name + * @param sClassName {string} - Name of the class + * @param bStatic {boolean} - If it's static + * @returns {string} - Formatted name + */ + formatEntityName: function (sName, sClassName, bStatic) { + return (bStatic === true) ? sClassName + "." + sName : sName; + }, + + JSDocUtil: function () { + + var rEscapeRegExp = /[[\]{}()*+?.\\^$|]/g; + + // Local mocked methods + var escapeRegExp = function escapeRegExp(sString) { + return sString.replace(rEscapeRegExp, "\\$&"); + }; + + function defaultLinkFormatter(target, text) { + return "" + (text || target) + ""; + } + + function format(src, options) { + + options = options || {}; + var beforeParagraph = options.beforeParagraph === undefined ? '

' : options.beforeParagraph; + var afterParagraph = options.afterParagraph === undefined ? '

' : options.afterParagraph; + var beforeFirstParagraph = options.beforeFirstParagraph === undefined ? beforeParagraph : options.beforeFirstParagraph; + var afterLastParagraph = options.afterLastParagraph === undefined ? afterParagraph : options.afterLastParagraph; + var linkFormatter = typeof options.linkFormatter === 'function' ? options.linkFormatter : defaultLinkFormatter; + + /* + * regexp to recognize important places in the text + * + * Capturing groups of the RegExp: + * group 1: begin of a pre block + * group 2: end of a pre block + * group 3: begin of a header, implicitly ends a paragraph + * group 4: end of a header, implicitly starts a new paragraph + * group 5: target portion of an inline @link tag + * group 6: (optional) text portion of an inline link tag + * group 7: an empty line which implicitly starts a new paragraph + * + * [--
 block -] [---- some header ----] [---- an inline [@link ...} tag ----] [---------- an empty line ---------]  */
+				var r = /(
)|(<\/pre>)|()|(<\/h[\d+]>)|\{@link\s+([^}\s]+)(?:\s+([^\}]*))?\}|((?:\r\n|\r|\n)[ \t]*(?:\r\n|\r|\n))/gi;
+				var inpre = false;
+
+				src = src || '';
+				linkFormatter = linkFormatter || defaultLinkFormatter;
+
+				src = beforeFirstParagraph + src.replace(r, function(match, pre, endpre, header, endheader, linkTarget, linkText, emptyline) {
+					if ( pre ) {
+						inpre = true;
+					} else if ( endpre ) {
+						inpre = false;
+					} else if ( header ) {
+						if ( !inpre ) {
+							return afterParagraph + match;
+						}
+					} else if ( endheader ) {
+						if ( !inpre ) {
+							return match + beforeParagraph;
+						}
+					} else if ( emptyline ) {
+						if ( !inpre ) {
+							return afterParagraph + beforeParagraph;
+						}
+					} else if ( linkTarget ) {
+						if ( !inpre ) {
+							return linkFormatter(linkTarget, linkText);
+						}
+					}
+					return match;
+				}) + afterLastParagraph;
+
+				// remove empty paragraphs
+				if (beforeParagraph !== "" && afterParagraph !== "") {
+					src = src.replace(new RegExp(escapeRegExp(beforeParagraph) + "\\s*" + escapeRegExp(afterParagraph), "g"), "");
+				}
+
+				return src;
+			}
+
+			return {
+				formatTextBlock: format
+			};
+
+		},
+
+		handleExternalUrl: function (sTarget, sText) {
+			// Check if the external domain is SAP hosted
+			let bSAPHosted = /^https?:\/\/(?:www.)?[\w.]*(?:sap|hana\.ondemand|sapfioritrial)\.com/.test(sTarget);
+
+		return `${sText}
+`;
+		},
+
+	/**
+	 * Discover possible target type by looking at symbols from own and depending libraries
+	 * @param {string} target
+	 * @param {object} self
+	 * @param {object} ownLibrary
+	 * @param {object} dependencyLibs
+	 * @param {boolean} module
+	 * @returns {string}
+	 */
+	createLinkFromTargetType: function ({className, methodName, target, self, ownLibrary, dependencyLibs, text}) {
+		let sResult;
+
+		function searchInSymbol(oSymbol) {
+			function findProperty(oEntity, sName, sTarget) {
+				if (!oEntity || !oEntity.properties) {
+					return;
+				}
+				return oEntity.properties.find(({name}) => name === sName || name === sTarget);
+			}
+
+			function findMethod(oEntity, sName, sTarget) {
+				if (!oEntity || !oEntity.methods) {
+					return;
+				}
+				return oEntity.methods.find(({name}) => name === sName || name === sTarget);
+			}
+
+			function findEvent(oEntity, sName) {
+				if (!oEntity || !oEntity.events) {
+					return;
+				}
+				return oEntity.events.find(({name}) => name === sName);
+			}
+
+			if (oSymbol.name === target) {
+				sResult = this.createLink({
+					name: oSymbol.name,
+					text: text
+				});
+				return true;
+			}
+			if (oSymbol.name === className) {
+				let oProperty = findProperty(oSymbol, methodName, target);
+				if (oProperty) {
+					sResult = this.createLink({
+						name: oProperty.name,
+						text: text
+					});
+					return true;
+				}
+				let oMethod = findMethod(oSymbol, methodName, target);
+				if (oMethod) {
+					sResult = this.createLink({
+						name: oMethod.static ? target : oMethod.name,
+						type: "methods",
+						text: text,
+						className: className
+					});
+					return true;
+				}
+
+				let oEvent = findEvent(oSymbol, methodName);
+				if (oEvent) {
+					sResult = this.createLink({
+						name: oEvent.name,
+						type: "events",
+						text: text,
+						className: className
+					});
+					return true;
+				}
+			}
+
+			return false;
+		}
+
+		// Self link
+		if (self.name === target) {
+			return this.createLink({
+				name: target,
+				text: text
+			});
+		}
+
+		// Own methods search
+		if (self.name === className && self.methods) {
+			let oResult = self.methods.find(({name}) => name === methodName);
+			if (oResult) {
+				return this.createLink({
+					name: oResult.static ? [self.name, oResult.name].join(".") : oResult.name,
+					type: "methods",
+					className: className,
+					text: text,
+					local: true
+				});
+			}
+		}
+
+		// Local library symbols
+		ownLibrary.symbols.find(oSymbol => {
+			return searchInSymbol.call(this, oSymbol);
+		});
+
+		if (sResult) return sResult;
+
+		// Dependent library symbols
+		dependencyLibs && Object.keys(dependencyLibs).find(sLib => {
+			if (sLib === target) {
+				sResult = this.createLink({
+					name: sLib,
+					text: text
+				});
+				return true;
+			}
+			return dependencyLibs[sLib].find(oSymbol => {
+				return searchInSymbol.call(this, oSymbol);
+			});
+		});
+
+		return sResult;
+	},
+
+	/**
+	 * Creates a html link
+	 * @param {string} name
+	 * @param {string} type
+	 * @param {string} className
+	 * @param {string} [text=name] by default if no text is provided the name will be used
+	 * @param {boolean} [local=false]
+	 * @param {string} [hrefAppend=""]
+	 * @returns {string} link
+	 */
+	createLink: function ({name, type, className, text=name, local=false, hrefAppend=""}) {
+		let sLink;
+
+		// handling module's
+		if (className !== undefined && (/^module:/.test(name) || /^module:/.test(className))) {
+			name = name.replace(/^module:/, "");
+		}
+
+		name = encodeURIComponent(name);
+		className = encodeURIComponent(className);
+
+		// Build the link
+ 		sLink = type ? `${className}/${type}/${name}` : name;
+
+		if (hrefAppend) {
+			sLink += hrefAppend;
+		}
+
+		if (local) {
+			return `${text}`;
+		}
+
+		return `${text}`;
+	},
+
+		/**
+		 * Pre-process links in text block
+		 * @param {string} sText text block
+	 * @param {boolean} bSkipParagraphs skip paragraphs
+		 * @returns {string} processed text block
+		 * @private
+		 */
+		_preProcessLinksInTextBlock: function (sText, bSkipParagraphs) {
+		let oSelf = this._oTopicData,
+			oOwnLibrary = this._oOwnLibrary,
+			oDependencyLibs = oChainObject.oDependentAPIs,
+				oOptions = {
+				linkFormatter: function (sTarget, sText) {
+					let aMatch,
+						aTarget;
+
+					// keep the full target in the fallback text
+					sText = sText || sTarget;
+
+						// If the link has a protocol, do not modify, but open in a new window
+					if (/:\/\//.test(sTarget)) {
+						return this.handleExternalUrl(sTarget, sText);
+						}
+
+					// topic:xxx Topic
+					aMatch = sTarget.match(/^topic:(\w{32})$/);
+					if (aMatch) {
+						return '' + sText + '';
+					}
+
+					// sap.x.Xxx.prototype.xxx - In case of prototype we have a link to method
+					aMatch = sTarget.match(/([a-zA-Z0-9.$_]+?)\.prototype\.([a-zA-Z0-9.$_]+)$/);
+						if (aMatch) {
+						return this.createLink({
+							name: aMatch[2],
+							type: "methods",
+							className: aMatch[1],
+							text: sText
+						});
+					}
+
+					// Heuristics: Extend is always a static method
+					// sap.x.Xxx.extend
+					// module:sap/x/Xxx.extend
+					aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)\.extend$/);
+					if (aMatch) {
+						let [, sModule, sClass] = aMatch;
+						return this.createLink({
+							name: sTarget.replace(/^module:/, ""),
+							type: "methods",
+							className: (sModule ? sModule : "") + sClass,
+							text: sText
+						});
+					}
+
+					// Constructor links are handled in special manner by the SDK
+					// sap.x.Xxx.constructor
+					// sap.x.Xxx#constructor
+					// module:sap/x/Xxx.constructor
+					// #constructor
+					aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)?[\.#]constructor$/i);
+					if (aMatch) {
+						let [, sModule, sClass] = aMatch,
+							sName;
+
+						if (sClass) {
+							sName = (sModule ? sModule : "") + sClass;
+						} else {
+							sName = oSelf.name
+										}
+
+						return this.createLink({
+							name: sName,
+							hrefAppend: "/constructor",
+							text: sText
+						});
+										}
+
+					// #.setText - local static method
+					// #setText - local instance method
+					// #.setText.from - local nested method
+					aMatch = sTarget.match(/^#(\.)?([a-zA-Z0-9.$_]+)$/);
+					if (aMatch) {
+						return this.createLink({
+							name: aMatch[1] ? `${oSelf.name}.${aMatch[2]}` : aMatch[2],
+							type: "methods",
+							className: oSelf.name,
+							local: true,
+							text: sText
+						});
+					}
+
+					// #event:press - local event
+					aMatch = sTarget.match(/^#event:([a-zA-Z0-9$_]+)$/);
+					if (aMatch) {
+						return this.createLink({
+							name: aMatch[1],
+							type: "events",
+							className: oSelf.name,
+							local: true,
+							text: sText
+						});
+					}
+					// Event links
+					// sap.m.Button#event:press
+					// sap.m.Button.event:press
+					// module:sap/m/Button.event:press
+					// module:sap/m/Button#event:press
+					aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)[.#]event:([a-zA-Z0-9$_]+)$/);
+					if (aMatch) {
+						let [, sModule, sClass, sEvent] = aMatch;
+						return this.createLink({
+							name: sEvent,
+							type: "events",
+							className: (sModule ? sModule : "") + sClass,
+							text: sText
+						});
+					}
+
+					// sap.m.Button#setText - instance method
+					// module:sap/m/Button#setText
+					aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+)#([a-zA-Z0-9.$_]+)$/);
+					if (aMatch) {
+						let [, sModule, sClass, sMethod] = aMatch;
+						return this.createLink({
+							name: sMethod,
+							type: "methods",
+							className: (sModule ? sModule : "") + sClass,
+							text: sText
+						});
+					}
+
+					// Unresolved type - try to discover target type
+					// sap.x.Xxx.xxx
+					// module:sap/x/Xxx.xxx
+					if (/^(?:module:)?([a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/.test(sTarget)) {
+						let [,sClass, sName] = sTarget.match(/^((?:module:)?[a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/),
+							sResult = this.createLinkFromTargetType({
+								className: sClass,
+								methodName: sName,
+								target: sTarget,
+								self: oSelf,
+								ownLibrary: oOwnLibrary,
+								dependencyLibs: oDependencyLibs,
+								text: sText
+							});
+						if (sResult) {
+							return sResult;
+								}
+					}
+
+					// Possible nested functions discovery - currently we do this only for regular symbols
+					aTarget = sTarget.split(".");
+					if (aTarget.length >= 3) {
+						let sResult = this.createLinkFromTargetType({
+							methodName: aTarget.splice(-2).join("."),
+							className: aTarget.join("."),
+							target: sTarget,
+							self: oSelf,
+							ownLibrary: oOwnLibrary,
+							dependencyLibs: oDependencyLibs,
+							text: sText
+						});
+						if (sResult) {
+							return sResult;
+						}
+					}
+
+					// Possible forward reference - we will treat them as symbol link
+					return this.createLink({
+						name: sTarget,
+						text: sText
+					});
+
+					}.bind(this)
+				};
+
+			if (bSkipParagraphs) {
+				oOptions.beforeParagraph = "";
+				oOptions.afterParagraph = "";
+			}
+
+			return this.JSDocUtil().formatTextBlock(sText, oOptions);
+		},
+
+		/**
+		 * Formatter for Overview section
+		 * @param {string} sDescription - Class about description
+		 * @param {array} aReferences - References
+		 * @returns {string} - formatted text block
+		 */
+		formatOverviewDescription: function (sDescription, aReferences) {
+			var iLen,
+				i;
+
+			// format references
+			if (aReferences && aReferences.length > 0) {
+				sDescription += "

Documentation links:
    "; + + iLen = aReferences.length; + for (i = 0; i < iLen; i++) { + // We treat references as links but as they may not be defined as such we enforce it if needed + if (/{@link.*}/.test(aReferences[i])) { + sDescription += "
  • " + aReferences[i] + "
  • "; + } else { + sDescription += "
  • {@link " + aReferences[i] + "}
  • "; + + } + } + + sDescription += "
"; + } + + // Calling formatDescription so it could handle further formatting + return this.formatDescription(sDescription); + }, + + /** + * Formats the description of the property + * @param description - the description of the property + * @param deprecatedText - the text explaining this property is deprecated + * @param deprecatedSince - the version when this property was deprecated + * @returns string - the formatted description + */ + formatDescription: function (description, deprecatedText, deprecatedSince) { + if (!description && !deprecatedText && !deprecatedSince) { + return ""; + } + + var result = description || ""; + + if (deprecatedSince || deprecatedText) { + // Note: sapUiDocumentationDeprecated - transformed to sapUiDeprecated to keep json file size low + result += "
"; + + result += this.formatDeprecated(deprecatedSince, deprecatedText); + + result += "
"; + } + + result = this._preProcessLinksInTextBlock(result); + return result; + }, + + /** + * Formats the entity deprecation message and pre-process jsDoc link and code blocks + * @param {string} sSince since text + * @param {string} sDescription deprecation description text + * @param {string} sEntityType string representation of entity type + * @returns {string} formatted deprecation message + */ + formatDeprecated: function (sSince, sDescription, sEntityType) { + var aResult; + + // Build deprecation message + // Note: there may be no since or no description text available + aResult = ["Deprecated"]; + if (sSince) { + aResult.push(" as of version " + sSince); + } + if (sDescription) { + // Evaluate code blocks - Handle ... pattern + sDescription = sDescription.replace(/(\S+)<\/code>/gi, function (sMatch, sCodeEntity) { + return ['', sCodeEntity, ''].join(""); + } + ); + + // Evaluate links in the deprecation description + aResult.push(". " + this._preProcessLinksInTextBlock(sDescription, true)); + } + + return aResult.join(""); + }, + + /** + * Pre-process and modify references + * @param {object} oSymbol control data object which will be modified + * @private + */ + modifyReferences: function (oSymbol, bCalledOnConstructor) { + var bHeaderDocuLinkFound = false, + bUXGuidelinesLinkFound = false, + aReferences = [], + entity = bCalledOnConstructor? oSymbol.constructor.references : oSymbol.references; + + const UX_GUIDELINES_BASE_URL = "https://experience.sap.com/fiori-design-web/"; + + if (entity && entity.length > 0) { + entity.forEach(function (sReference) { + var aParts; + + // Docu link - For the header we take into account only the first link that matches one of the patterns + if (!bHeaderDocuLinkFound) { + + // Handled patterns: + // * topic:59a0e11712e84a648bb990a1dba76bc7 + // * {@link topic:59a0e11712e84a648bb990a1dba76bc7} + // * {@link topic:59a0e11712e84a648bb990a1dba76bc7 Link text} + aParts = sReference.match(/^{@link\s+topic:(\w{32})(\s.+)?}$|^topic:(\w{32})$/); + + if (aParts) { + if (aParts[3]) { + // Link is of type topic:GUID + oSymbol.docuLink = aParts[3]; + oSymbol.docuLinkText = oSymbol.basename; + } else if (aParts[1]) { + // Link of type {@link topic:GUID} or {@link topic:GUID Link text} + oSymbol.docuLink = aParts[1]; + oSymbol.docuLinkText = aParts[2] ? aParts[2] : oSymbol.basename; + } + bHeaderDocuLinkFound = true; + return; + } + } + + // Fiori link - Handled patterns: + // * fiori:flexible-column-layout + // * fiori:/flexible-column-layout/ + // * fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/ + // * {@link fiori:flexible-column-layout} + // * {@link fiori:/flexible-column-layout/} + // * {@link fiori:/flexible-column-layout/ Flexible Column Layout} + // * {@link fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/} + // * {@link fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/ Flexible Column Layout} + aParts = sReference.match(/^(?:{@link\s)?fiori:(?:https:\/\/experience\.sap\.com\/fiori-design-web\/)?\/?(\S+\b)\/?\s?(.*[^\s}])?}?$/); + + if (aParts) { + let [, sTarget, sTargetName] = aParts; + + if (bCalledOnConstructor && !bUXGuidelinesLinkFound) { + // Extract first found UX Guidelines link as primary + oSymbol.uxGuidelinesLink = UX_GUIDELINES_BASE_URL + sTarget; + oSymbol.uxGuidelinesLinkText = sTargetName ? sTargetName : oSymbol.basename; + bUXGuidelinesLinkFound = true; + return; + } else { + // BCP: 1870155880 - Every consecutive "fiori:" link should be handled as a normal link + sReference = "{@link " + UX_GUIDELINES_BASE_URL + sTarget + (sTargetName ? " " + sTargetName : "") + "}"; + } + } + + aReferences.push(sReference); + }); + bCalledOnConstructor? oSymbol.constructor.references = aReferences : oSymbol.references = aReferences; + } else { + bCalledOnConstructor? oSymbol.constructor.references = [] : oSymbol.references = []; + } + }, + + /** + * Manage References, to apply as an unordered list in the description + * @param {object} oEntity control data object which will be modified + * @private + */ + formatReferencesInDescription: function(oEntity) { + if (oEntity.references && Array.isArray(oEntity.references)) { + oEntity.references = oEntity.references.map(sReference => { + return `
  • ${sReference}
  • `; + }); + if (!oEntity.description) { + // If there is no method description - references should be the first line of it + oEntity.description = ''; + } else { + oEntity.description += '

    '; + } + oEntity.description += `References:
      ${oEntity.references.join("")}
    `; + } + } + }; + + /* Methods direct copy from API Detail */ + let methods = { + + /** + * 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) { + var fnCreateTypesArr = function (sTypes) { + return sTypes.split("|").map(function (sType) { + return {value: sType} + }); + }; + var fnExtractParameterProperties = function (oParameter, aParameters, iDepth, aPhoneName) { + if (oParameter.parameterProperties) { + Object.keys(oParameter.parameterProperties).forEach(function (sProperty) { + var oProperty = oParameter.parameterProperties[sProperty]; + + oProperty.depth = iDepth; + + // Handle types + if (oProperty.type) { + oProperty.types = fnCreateTypesArr(oProperty.type); + } + + // Phone name - available only for parameters + oProperty.phoneName = [aPhoneName.join("."), oProperty.name].join("."); + + // Add property to parameter array as we need a simple structure + aParameters.push(oProperty); + + // Handle child parameterProperties + fnExtractParameterProperties(oProperty, aParameters, (iDepth + 1), aPhoneName.concat([oProperty.name])); + + // Keep file size in check + delete oProperty.type; + }); + + // Keep file size in check + delete oParameter.parameterProperties; + } + }; + aMethods.forEach(function (oMethod) { + // New array to hold modified parameters + var aParameters = []; + + // Handle parameters + if (oMethod.parameters) { + oMethod.parameters.forEach(function (oParameter) { + if (oParameter.type) { + oParameter.types = fnCreateTypesArr(oParameter.type); + } + + // Keep file size in check + delete oParameter.type; + + // Add the parameter before the properties + aParameters.push(oParameter); + + // Handle Parameter Properties + // Note: We flatten the structure + fnExtractParameterProperties(oParameter, aParameters, 1, [oParameter.name]); + + }); + + // Override the old data + oMethod.parameters = aParameters; + } + + // Handle return values + if (oMethod.returnValue && oMethod.returnValue.type) { + // Handle types + oMethod.returnValue.types = fnCreateTypesArr(oMethod.returnValue.type); + } + + }); + }, + + /** + * Adjusts events info so that it can be easily displayed in a table + * @param {Array} aEvents - the events array initially coming from the server + */ + buildEventsModel: function (aEvents) { + var fnExtractParameterProperties = function (oParameter, aParameters, iDepth, aPhoneName) { + if (oParameter.parameterProperties) { + Object.keys(oParameter.parameterProperties).forEach(function (sProperty) { + var oProperty = oParameter.parameterProperties[sProperty], + sPhoneTypeSuffix; + + oProperty.depth = iDepth; + + // Phone name - available only for parameters + sPhoneTypeSuffix = oProperty.type === "array" ? "[]" : ""; + oProperty.phoneName = [aPhoneName.join("."), (oProperty.name + sPhoneTypeSuffix)].join("."); + + // Add property to parameter array as we need a simple structure + aParameters.push(oProperty); + + // Handle child parameterProperties + fnExtractParameterProperties(oProperty, aParameters, (iDepth + 1), + aPhoneName.concat([oProperty.name + sPhoneTypeSuffix])); + }); + + // Keep file size in check + delete oParameter.parameterProperties; + } + }; + aEvents.forEach(function (aEvents) { + // New array to hold modified parameters + var aParameters = []; + + // Handle parameters + if (aEvents.parameters) { + aEvents.parameters.forEach(function (oParameter) { + // Add the parameter before the properties + aParameters.push(oParameter); + + // Handle Parameter Properties + // Note: We flatten the structure + fnExtractParameterProperties(oParameter, aParameters, 1, [oParameter.name]); + }); + + // Override the old data + aEvents.parameters = aParameters; + } + }); + }, + + /** + * Adjusts constructor parameters info so that it can be easily displayed in a table + * @param {Array} aParameters - the events array initially coming from the server + */ + buildConstructorParameters: function (aParameters) { + // New array to hold modified parameters + var aNodes = [], + processNode = function (oNode, sPhoneName, iDepth, aNodes) { + // Handle phone name + oNode.phoneName = sPhoneName ? [sPhoneName, oNode.name].join(".") : oNode.name; + + // Depth + oNode.depth = iDepth; + + // Add to array + aNodes.push(oNode); + + // Handle nesting + if (oNode.parameterProperties) { + Object.keys(oNode.parameterProperties).forEach(function (sNode) { + processNode(oNode.parameterProperties[sNode], oNode.phoneName, (iDepth + 1), aNodes); + }); + } + + delete oNode.parameterProperties; + }; + + aParameters.forEach(function (oParameter) { + // Handle Parameter Properties + // Note: We flatten the structure + processNode(oParameter, undefined, 0, aNodes); + }); + + return aNodes; + }, + + oLibsData: {}, + + }; + + // Create the chain object + let oChainObject = { + inputFile: sInputFile, + outputFile: sOutputFile, + libraryFile: sLibraryFile, + aDependentLibraryFiles: Array.isArray(vDependencyAPIFiles) ? vDependencyAPIFiles : null + }; + + // Start the work here + let p = getLibraryPromise(oChainObject) + .then(extractComponentAndDocuindexUrl) + .then(flattenComponents) + .then(extractSamplesFromDocuIndex) + .then(getDependencyLibraryFilesList) + .then(getAPIJSONPromise) + .then(loadDependencyLibraryFiles) + .then(transformApiJson) + .then(createApiRefApiJson); + return p; + +}; + +module.exports = transformer; + +// auto execute the transformer, if this module is the executed script (not used in grunt tooling) +if ( process.argv.length > 1 && /transform-apijson-for-sdk.js$/.test(process.argv[1]) ) { + + let sInputFile = process.argv[2]; + let sOutputFile = process.argv[3]; + let sLibraryFile = process.argv[4]; + let sAPIJSonDependencyDir = process.argv[5]; + + transformer(sInputFile, sOutputFile, sLibraryFile, sAPIJSonDependencyDir) + .catch(oError => { + log.error(oError); + }); + +} diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js new file mode 100644 index 000000000..0cba05cf7 --- /dev/null +++ b/lib/processors/jsdoc/lib/ui5/plugin.js @@ -0,0 +1,2395 @@ +/* + * JSDoc3 plugin for UI5 documentation generation. + * + * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +/* global global, require, exports, env */ +/* eslint strict: [2, "global"]*/ + +'use strict'; + +/** + * UI5 plugin for JSDoc3 (3.3.0-alpha5) + * + * The plugin adds the following SAPUI5 specific tag definitions to JSDoc3 + * + * disclaimer + * + * experimental + * + * final + * + * interface + * + * implements + * + * + * + * It furthermore listens to the following JSDoc3 events to implement additional functionality + * + * parseBegin + * to create short names for all file that are to be parsed + * + * fileBegin + * to write some line to the log (kind of a progress indicator) + * + * jsdocCommentFound + * to pre-process comments, empty lines are used as paragraph markers + * a default visibility is added, legacy tag combinations used in JSdoc2 are converted to JSDoc3 conventions + * + * newDoclet + * + * parseComplete + * remove undocumented/ignored/private doclets or duplicate doclets + * + * + * Last but not least, it implements an astNodeVisitor to detect UI5 specific "extend" calls and to create + * documentation for the properties, aggregations etc. that are created with the "extend" call. + * + * @module plugins/sapui5-jsdoc + */ + +/* imports */ +var Syntax = require('jsdoc/src/syntax').Syntax; +var Doclet = require('jsdoc/doclet').Doclet; +var fs = require('jsdoc/fs'); +var path = require('jsdoc/path'); +var pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || {}; + +/* ---- global vars---- */ + +/** + * Potential path prefixes. + * + * Will be determined in the handler for the parseBegin event + */ +var pathPrefixes = []; + +/** + * Prefixes of the UI5 unified resource name for the source files is NOT part of the file name. + * (e.g. when a common root namespaces has been omitted from the folder structure). + * + * The prefix will be prepended to all resource names. + */ +var resourceNamePrefixes = []; + +/** + * A UI5 specific unique Id for all doclets. + */ +var docletUid = 0; + +var currentProgram; + +/** + * Information about the current module. + * + * The info object is created in the 'fileBegin' event handler and the 'resource' and 'module' properties + * are derived from the filename provided by the event. The derived information is only correct, when the + * resource name prefix is known for the directory from which a source is loaded (prefixes can be configured + * via sapui5.resourceNamePrefixes, for UI5 libraries it is empty by default). + * + * During AST visiting, the 'name' property and the 'localeNames' map will be filled. + * 'name' will be the name of the class defined by the module (assuming that there is only one). + * 'localNames' will contain information objects for each parameter of an AMD Factory function and for + * all shortcut variables that are defined top-level in the module factory function (e.g. something like + * var ButtonDesign = coreLibrary.ButtonDesign; ). + * An info object for a local name either can have a 'value' property (simple, constant value) or it can + * have a 'module' and optionally a 'path' value. In that case, the local name represents an AMD + * module import or a shortcut derived from such an import. + * + * See {@link getREsolvedObjectName} how the knowledge about locale names is used. + * + * @type {{name:string,resource:string,module:string,localName:Object}} + */ +var currentModule; + +var currentSource; + +/** + * Cached UI5 metadata for encountered UI5 classes. + * + * The metadata is collected from the 'metadata' property of 'extend' calls. It is stored + * in this map keyed by the name of the class (as defined in the first parameter of the extend call). + * Only after all files have been parsed, the collected information can be associated with the + * corresponding JSDoc doclet (e.g. with the class documentation). + */ +var classInfos = Object.create(null); + +/** + * + */ +var typeInfos = Object.create(null); + +/** + * Cached designtime info for encountered sources. + * + * The designtime information is collected only for files named '*.designtime.js'. + * It is stored in this map keyed by the corresponding module name (e.g. 'sap/m/designtime/Button.designtime'). + * Only after all files have been parsed, the collected information can be associated with runtime metadata + * that refers to that designtime module name. + */ +var designtimeInfos = Object.create(null); + +/* ---- private functions ---- */ + +function ui5data(doclet) { + return doclet.__ui5 || (doclet.__ui5 = { id: ++docletUid }); +} + +var pendingMessageHeader; + +function msgHeader(str) { + pendingMessageHeader = str; +} + +/* eslint-disable no-console */ +function debug() { + if ( env.opts.debug ) { + console.log.apply(console, arguments); + } +} + +function info() { + if ( env.opts.verbose || env.opts.debug ) { + if ( pendingMessageHeader ) { + console.log(""); + pendingMessageHeader = null; + } + console.log.apply(console, arguments); + } +} + +function warning(msg) { + if ( pendingMessageHeader ) { + if ( !env.opts.verbose && !env.opts.debug ) { + console.log(pendingMessageHeader); + } else { + console.log(""); + } + pendingMessageHeader = null; + } + var args = Array.prototype.slice.apply(arguments); + args[0] = "**** warning: " + args[0]; + console.log.apply(console, args); +} + +function error(msg) { + if ( pendingMessageHeader && !env.opts.verbose && !env.opts.debug ) { + if ( !env.opts.verbose && !env.opts.debug ) { + console.log(pendingMessageHeader); + } else { + console.log(""); + } + pendingMessageHeader = null; + } + var args = Array.prototype.slice.apply(arguments); + args[0] = "**** error: " + args[0]; + console.log.apply(console, args); +} +/* eslint-enable no-console */ + +//---- path handling --------------------------------------------------------- + +function ensureEndingSlash(path) { + path = path || ''; + return path && path.slice(-1) !== '/' ? path + '/' : path; +} + +function getRelativePath(filename) { + var relative = path.resolve(filename); + for ( var i = 0; i < pathPrefixes.length; i++ ) { + if ( relative.indexOf(pathPrefixes[i]) === 0 ) { + relative = relative.slice(pathPrefixes[i].length); + break; + } + } + return relative.replace(/\\/g, '/'); +} + +function getResourceName(filename) { + var resource = path.resolve(filename); + for ( var i = 0; i < pathPrefixes.length; i++ ) { + if ( resource.indexOf(pathPrefixes[i]) === 0 ) { + resource = resourceNamePrefixes[i] + resource.slice(pathPrefixes[i].length); + break; + } + } + return resource.replace(/\\/g, '/'); +} + +function getModuleName(resource) { + return resource.replace(/\.js$/,''); +} + +/* + * resolves relative AMD module identifiers relative to a given base name + */ +function resolveModuleName(base, name) { + var stack = base.split('/'); + stack.pop(); + name.split('/').forEach(function(segment, i) { + if ( segment == '..' ) { + stack.pop(); + } else if ( segment === '.' ) { + // ignore + } else { + if ( i === 0 ) { + stack = []; + } + stack.push(segment); + } + }); + return stack.join('/'); +} + +// ---- AMD handling + +function analyzeModuleDefinition(node) { + var args = node.arguments; + var arg = 0; + if ( arg < args.length + && args[arg].type === Syntax.Literal && typeof args[arg].value === 'string' ) { + warning("module explicitly defined a module name '" + args[arg].value + "'"); + currentModule.name = args[arg].value; + arg++; + } + if ( arg < args.length && args[arg].type === Syntax.ArrayExpression ) { + currentModule.dependencies = convertValue(args[arg], "string[]"); + arg++; + } + if ( arg < args.length && args[arg].type === Syntax.FunctionExpression ) { + currentModule.factory = args[arg]; + arg++; + } + if ( currentModule.dependencies && currentModule.factory ) { + for ( var i = 0; i < currentModule.dependencies.length && i < currentModule.factory.params.length; i++ ) { + var name = currentModule.factory.params[i].name; + var module = resolveModuleName(currentModule.module, currentModule.dependencies[i]); + debug(" import " + name + " from '" + module + "'"); + currentModule.localNames[name] = { + module: module + // no (or empty) path + }; + } + } + if ( currentModule.factory ) { + collectShortcuts(currentModule.factory.body); + } +} + +/** + * Searches the given body for variable declarations that can be evaluated statically, + * either because they refer to known AMD modukle imports (e.g. shortcut varialbes) + * or because they have a (design time) constant value. + * + * @param {ASTNode} body AST node of a function body that shall be searched for shortcuts + */ +function collectShortcuts(body) { + + function checkAssignment(name, valueNode) { + if ( valueNode.type === Syntax.Literal ) { + currentModule.localNames[name] = { + value: valueNode.value + }; + debug("compile time constant found ", name, valueNode.value); + } else if ( valueNode.type === Syntax.MemberExpression ) { + var _import = getLeftmostName(valueNode); + var local = _import && currentModule.localNames[_import]; + if ( typeof local === 'object' && local.module ) { + currentModule.localNames[name] = { + module: local.module, + path: getObjectName(valueNode).split('.').slice(1).join('.') // TODO chaining if local has path + }; + debug(" found local shortcut: ", name, currentModule.localNames[name]); + } + } else if ( isRequireSyncCall(valueNode) || isProbingRequireCall(valueNode) ) { + if ( valueNode.arguments[0] + && valueNode.arguments[0].type === Syntax.Literal + && typeof valueNode.arguments[0].value === 'string' ) { + currentModule.localNames[name] = { + module: valueNode.arguments[0].value + // no (or empty) path + }; + debug(" found local import: %s = %s('%s')", name, valueNode.callee.property.name, valueNode.arguments[0].value); + } + } else if ( isExtendCall(valueNode) ) { + currentModule.localNames[name] = { + "class": valueNode.arguments[0].value + // no (or empty) path + }; + debug(" found local class definition: %s = .extend('%s', ...)", name, valueNode.arguments[0].value); + } + } + + if ( body.type === Syntax.BlockStatement ) { + body.body.forEach(function ( stmt ) { + // console.log(stmt); + if ( stmt.type === Syntax.VariableDeclaration ) { + stmt.declarations.forEach(function(decl) { + if ( decl.init ) { + checkAssignment(decl.id.name, decl.init); + } + }); + } else if ( stmt.type === Syntax.ExpressionStatement + && stmt.expression.type === Syntax.AssignmentExpression + && stmt.expression.left.type === Syntax.Identifier ) { + checkAssignment(stmt.expression.left.name, stmt.expression.right); + } + }); + } +} + +// ---- text handling --------------------------------------------------------- + +var rPlural = /(children|ies|ves|oes|ses|ches|shes|xes|s)$/i; +var mSingular = {'children' : -3, 'ies' : 'y', 'ves' : 'f', 'oes' : -2, 'ses' : -2, 'ches' : -2, 'shes' : -2, 'xes' : -2, 's' : -1 }; + +function guessSingularName(sPluralName) { + return sPluralName.replace(rPlural, function($,sPlural) { + var vRepl = mSingular[sPlural.toLowerCase()]; + return typeof vRepl === "string" ? vRepl : sPlural.slice(0,vRepl); + }); +} + +function getPropertyKey(prop) { + if ( prop.key.type === Syntax.Identifier ) { + return prop.key.name; + } else if ( prop.key.type === Syntax.Literal ) { + return String(prop.key.value); + } else { + return prop.key.toSource(); + } +} + +/** + * Creates a map of property values from an AST 'object literal' node. + * + * The values in the map are again AST 'property' nodes (representing key/value pairs). + * It would be more convenient to just return the values, but the property node is needed + * to find the corresponding (preceding) documentation comment. + * + * If a defaultKey is given and if the node is not an object literal + * but another simple type literal, the value is treated as a shortcut for + *
    + *   {
    + *     [defaultKey]: node.value
    + *   }
    + * 
    + * This is used in ManagedObjectMetadata to allow a simpler declaration of properties by + * specifying a type name only. + * + * @param {ASTNode} node AST node for an object literal or simple literal + * @param {string} [defaultKey=undefined] A default key to use for simple values + * @returns {Map} Map of AST nodes of type 'Property', keyed by their property name + */ +function createPropertyMap(node, defaultKey) { + + var result; + + if ( node != null ) { + + // if, instead of an object literal only a literal is given and there is a defaultKey, then wrap the literal in a map + if ( node.type === Syntax.Literal && defaultKey != null ) { + result = {}; + result[defaultKey] = { type: Syntax.Property, value: node }; + return result; + } + + if ( node.type != Syntax.ObjectExpression ) { + // something went wrong, it's not an object literal + error("not an object literal:" + node.type + ":" + node.value); + // console.log(node.toSource()); + return undefined; + } + + // invariant: node.type == Syntax.ObjectExpression + result = {}; + for (var i = 0; i < node.properties.length; i++) { + var prop = node.properties[i]; + //console.log("objectproperty " + prop.type); + var name = getPropertyKey(prop); + //console.log("objectproperty " + prop.type + ":" + name); + result[name] = prop; + } + } + return result; +} + +function isExtendCall(node) { + + return ( + node + && node.type === Syntax.CallExpression + && node.callee.type === Syntax.MemberExpression + && node.callee.property.type === Syntax.Identifier + && node.callee.property.name === 'extend' + && node.arguments.length >= 2 + && node.arguments[0].type === Syntax.Literal + && typeof node.arguments[0].value === "string" + && node.arguments[1].type === Syntax.ObjectExpression + ); + +} + +function isSapUiDefineCall(node) { + + return ( + node + && node.type === Syntax.CallExpression + && node.callee.type === Syntax.MemberExpression + && node.callee.object.type === Syntax.MemberExpression + && node.callee.object.object.type === Syntax.Identifier + && node.callee.object.object.name === 'sap' + && node.callee.object.property.type === Syntax.Identifier + && node.callee.object.property.name === 'ui' + && node.callee.property.type === Syntax.Identifier + && node.callee.property.name === 'define' + ); + +} + +function isCreateDataTypeCall(node) { + return ( + node + && node.type === Syntax.CallExpression + && node.callee.type === Syntax.MemberExpression + && /^(sap\.ui\.base\.)?DataType$/.test(getObjectName(node.callee.object)) + && node.callee.property.type === Syntax.Identifier + && node.callee.property.name === 'createType' + ); +} + +function isRequireSyncCall(node) { + return ( + node + && node.type === Syntax.CallExpression + && node.callee.type === Syntax.MemberExpression + && node.callee.object.type === Syntax.MemberExpression + && node.callee.object.object.type === Syntax.Identifier + && node.callee.object.object.name === 'sap' + && node.callee.object.property.type === Syntax.Identifier + && node.callee.object.property.name === 'ui' + && node.callee.property.type === Syntax.Identifier + && node.callee.property.name === 'requireSync' + ); +} + +function isProbingRequireCall(node) { + return ( + node + && node.type === Syntax.CallExpression + && node.callee.type === Syntax.MemberExpression + && node.callee.object.type === Syntax.MemberExpression + && node.callee.object.object.type === Syntax.Identifier + && node.callee.object.object.name === 'sap' + && node.callee.object.property.type === Syntax.Identifier + && node.callee.object.property.name === 'ui' + && node.callee.property.type === Syntax.Identifier + && node.callee.property.name === 'require' + && node.arguments.length === 1 + && node.arguments[0].type === Syntax.Literal + && typeof node.arguments[0].value === 'string' // TODO generalize to statically analyzable constants + ); +} + +function getObjectName(node) { + if ( node.type === Syntax.MemberExpression && !node.computed && node.property.type === Syntax.Identifier ) { + var prefix = getObjectName(node.object); + return prefix ? prefix + "." + node.property.name : null; + } else if ( node.type === Syntax.Identifier ) { + return /* scope[node.name] ? scope[node.name] : */ node.name; + } else { + return null; + } +} + +/* + * Checks whether the node is a qualified name (a.b.c) and if so, + * returns the leftmost identifier a + */ +function getLeftmostName(node) { + while ( node.type === Syntax.MemberExpression ) { + node = node.object; + } + if ( node.type === Syntax.Identifier ) { + return node.name; + } + // return undefined; +} + +function getResolvedObjectName(node) { + var name = getObjectName(node); + var _import = getLeftmostName(node); + var local = _import && currentModule.localNames[_import]; + if ( local && (local.class || local.module) ) { + var resolvedName; + if ( local.class ) { + resolvedName = local.class; + } else { + resolvedName = local.module.replace(/\//g, ".").replace(/\.library$/, ""); + if ( local.path ) { + resolvedName = resolvedName + "." + local.path; + } + } + if ( name.indexOf('.') > 0 ) { + resolvedName = resolvedName + name.slice(name.indexOf('.')); + } + debug("resolved " + name + " to " + resolvedName); + return resolvedName; + } + return name; +} + +function convertValue(node, type, propertyName) { + + var value; + + if ( node.type === Syntax.Literal ) { + + // 'string' or number or true or false + return node.value; + + } else if ( node.type === Syntax.UnaryExpression + && node.prefix + && node.argument.type === Syntax.Literal + && typeof node.argument.value === 'number' + && ( node.operator === '-' || node.operator === '+' )) { + + // -n or +n + value = node.argument.value; + return node.operator === '-' ? -value : value; + + } else if ( node.type === Syntax.MemberExpression && type ) { + + // enum value (a.b.c) + value = getResolvedObjectName(node); + if ( value.indexOf(type + ".") === 0 ) { + // starts with fully qualified enum name -> cut off name + return value.slice(type.length + 1); +// } else if ( value.indexOf(type.split(".").slice(-1)[0] + ".") === 0 ) { +// // unqualified name might be a local name (just a guess - would need static code analysis for proper solution) +// return value.slice(type.split(".").slice(-1)[0].length + 1); + } else { + warning("did not understand default value '%s'%s, falling back to source", value, propertyName ? " of property '" + propertyName + "'" : ""); + return value; + } + + } else if ( node.type === Syntax.Identifier ) { + if ( node.name === 'undefined') { + // undefined + return undefined; + } + var local = currentModule.localNames[node.name]; + if ( typeof local === 'object' && 'value' in local ) { + // TODO check type + return local.value; + } + } else if ( node.type === Syntax.ArrayExpression ) { + + if ( node.elements.length === 0 ) { + // empty array literal + return "[]"; // TODO return this string or an empty array + } + + if ( type && type.slice(-2) === "[]" ) { + var componentType = type.slice(0,-2); + return node.elements.map( function(elem) { + return convertValue(elem, componentType, propertyName); + }); + } + + } else if ( node.type === Syntax.ObjectExpression ) { + + if ( node.properties.length === 0 && (type === 'object' || type === 'any') ) { + return {}; + } + + } + + value = '???'; + if ( currentSource && node.range ) { + value = currentSource.slice( node.range[0], node.range[1] ); + } + error("unexpected type of default value (type='%s', source='%s')%s, falling back to '%s'", node.type, node.toString(), propertyName ? " of property '" + propertyName + "'" : "", value); + return value; +} + +function convertStringArray(node) { + if ( node.type !== Syntax.ArrayExpression ) { + throw new Error("not an array"); + } + var result = []; + for ( var i = 0; i < node.elements.length; i++ ) { + if ( node.elements[i].type !== Syntax.Literal || typeof node.elements[i].value !== 'string' ) { + throw new Error("not a string literal"); + } + result.push(node.elements[i].value); + } + // console.log(result); + return result; +} + +function convertDragDropValue(node, cardinality) { + var mDragDropValue; + var mDefaults = { draggable : false, droppable: false }; + + if ( node.type === Syntax.ObjectExpression ) { + mDragDropValue = (node.properties || []).reduce(function(oObject, oProperty) { + var sKey = getPropertyKey(oProperty); + if (mDefaults.hasOwnProperty(sKey)) { + oObject[sKey] = convertValue(oProperty.value); + } + return oObject; + }, {}); + } else if ( node.type === Syntax.Literal ) { + mDragDropValue = { + draggable : node.value, + droppable : node.value + }; + } else { + throw new Error("not a valid dnd node"); + } + + return Object.assign(mDefaults, mDragDropValue); +} + +function collectClassInfo(extendCall, classDoclet) { + + var baseType; + if ( classDoclet && classDoclet.augments && classDoclet.augments.length === 1 ) { + baseType = classDoclet.augments[0]; + } + if ( extendCall.callee.type === Syntax.MemberExpression ) { + var baseCandidate = getResolvedObjectName(extendCall.callee.object); + if ( baseCandidate && baseType == null ) { + baseType = baseCandidate; + } else if ( baseCandidate !== baseType ) { + error("documented base type '" + baseType + "' doesn't match technical base type '" + baseCandidate + "'"); + } + } + + var oClassInfo = { + name : extendCall.arguments[0].value, + baseType : baseType, + interfaces : [], + doc : classDoclet && classDoclet.description, + deprecation : classDoclet && classDoclet.deprecated, + since : classDoclet && classDoclet.since, + experimental : classDoclet && classDoclet.experimental, + specialSettings : {}, + properties : {}, + aggregations : {}, + associations : {}, + events : {}, + methods : {}, + annotations : {}, + designtime: false + }; + + function upper(n) { + return n.slice(0,1).toUpperCase() + n.slice(1); + } + + function each(node, defaultKey, callback) { + var map,n,settings,doclet; + + map = node && createPropertyMap(node.value); + if ( map ) { + for (n in map ) { + if ( map.hasOwnProperty(n) ) { + doclet = getLeadingDoclet(map[n]); + settings = createPropertyMap(map[n].value, defaultKey); + if ( settings == null ) { + error("no valid metadata for " + n + " (AST type '" + map[n].value.type + "')"); + continue; + } + + callback(n, settings, doclet, map[n]); + } + } + } + } + + var classInfoNode = extendCall.arguments[1]; + var 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 + "'."); + return null; + } + + var metadata = classInfoMap && classInfoMap.metadata && createPropertyMap(classInfoMap.metadata.value); + if ( metadata ) { + + debug(" analyzing metadata for '" + oClassInfo.name + "'"); + + oClassInfo["abstract"] = !!(metadata["abstract"] && metadata["abstract"].value.value); + oClassInfo["final"] = !!(metadata["final"] && metadata["final"].value.value); + oClassInfo.dnd = metadata.dnd && convertDragDropValue(metadata.dnd.value); + + if ( metadata.interfaces ) { + oClassInfo.interfaces = convertStringArray(metadata.interfaces.value); + } + + each(metadata.specialSettings, "type", function(n, settings, doclet) { + oClassInfo.specialSettings[n] = { + name : n, + doc : doclet && doclet.description, + since : doclet && doclet.since, + deprecation : doclet && doclet.deprecated, + experimental : doclet && doclet.experimental, + visibility : (settings.visibility && settings.visibility.value.value) || "public", + type : settings.type ? settings.type.value.value : "any" + }; + }); + + oClassInfo.defaultProperty = (metadata.defaultProperty && metadata.defaultProperty.value.value) || undefined; + + each(metadata.properties, "type", function(n, settings, doclet) { + var type; + var N = upper(n); + var methods; + oClassInfo.properties[n] = { + name : n, + doc : doclet && doclet.description, + since : doclet && doclet.since, + deprecation : doclet && doclet.deprecated, + experimental : doclet && doclet.experimental, + visibility : (settings.visibility && settings.visibility.value.value) || "public", + type : (type = settings.type ? settings.type.value.value : "string"), + defaultValue : settings.defaultValue ? convertValue(settings.defaultValue.value, type, n) : null, + group : settings.group ? settings.group.value.value : 'Misc', + bindable : settings.bindable ? !!convertValue(settings.bindable.value) : false, + methods: (methods = { + "get": "get" + N, + "set": "set" + N + }) + }; + if ( oClassInfo.properties[n].bindable ) { + methods["bind"] = "bind" + N; + methods["unbind"] = "unbind" + N; + } + // if ( !settings.defaultValue ) { + // console.log("property without defaultValue: " + oClassInfo.name + "." + n); + //} + }); + + oClassInfo.defaultAggregation = (metadata.defaultAggregation && metadata.defaultAggregation.value.value) || undefined; + + each(metadata.aggregations, "type", function(n, settings, doclet) { + var N = upper(n); + var methods; + var aggr = oClassInfo.aggregations[n] = { + name: n, + doc : doclet && doclet.description, + deprecation : doclet && doclet.deprecated, + since : doclet && doclet.since, + experimental : doclet && doclet.experimental, + visibility : (settings.visibility && settings.visibility.value.value) || "public", + type : settings.type ? settings.type.value.value : "sap.ui.core.Control", + altTypes: settings.altTypes ? convertStringArray(settings.altTypes.value) : undefined, + singularName : settings.singularName ? settings.singularName.value.value : guessSingularName(n), + cardinality : (settings.multiple && !settings.multiple.value.value) ? "0..1" : "0..n", + bindable : settings.bindable ? !!convertValue(settings.bindable.value) : false, + methods: (methods = { + "get": "get" + N, + "destroy": "destroy" + N + }) + }; + + aggr.dnd = settings.dnd && convertDragDropValue(settings.dnd.value, aggr.cardinality); + + if ( aggr.cardinality === "0..1" ) { + methods["set"] = "set" + N; + } else { + var N1 = upper(aggr.singularName); + methods["insert"] = "insert" + N1; + methods["add"] = "add" + N1; + methods["remove"] = "remove" + N1; + methods["indexOf"] = "indexOf" + N1; + methods["removeAll"] = "removeAll" + N; + } + if ( aggr.bindable ) { + methods["bind"] = "bind" + N; + methods["unbind"] = "unbind" + N; + } + }); + + each(metadata.associations, "type", function(n, settings, doclet) { + var N = upper(n); + var methods; + oClassInfo.associations[n] = { + name: n, + doc : doclet && doclet.description, + deprecation : doclet && doclet.deprecated, + since : doclet && doclet.since, + experimental : doclet && doclet.experimental, + visibility : (settings.visibility && settings.visibility.value.value) || "public", + type : settings.type ? settings.type.value.value : "sap.ui.core.Control", + singularName : settings.singularName ? settings.singularName.value.value : guessSingularName(n), + cardinality : (settings.multiple && settings.multiple.value.value) ? "0..n" : "0..1", + methods: (methods = { + "get": "get" + N + }) + }; + if ( oClassInfo.associations[n].cardinality === "0..1" ) { + methods["set"] = "set" + N; + } else { + var N1 = upper(oClassInfo.associations[n].singularName); + methods["add"] = "add" + N1; + methods["remove"] = "remove" + N1; + methods["removeAll"] = "removeAll" + N; + } + }); + + each(metadata.events, null, function(n, settings, doclet) { + var N = upper(n); + var info = oClassInfo.events[n] = { + name: n, + doc : doclet && doclet.description, + deprecation : doclet && doclet.deprecated, + since : doclet && doclet.since, + experimental : doclet && doclet.experimental, + visibility : /* (settings.visibility && settings.visibility.value.value) || */ "public", + allowPreventDefault : !!(settings.allowPreventDefault && settings.allowPreventDefault.value.value), + parameters : {}, + methods: { + "attach": "attach" + N, + "detach": "detach" + N, + "fire": "fire" + N + } + }; + each(settings.parameters, "type", function(pName, pSettings, pDoclet) { + info.parameters[pName] = { + name : pName, + doc : pDoclet && pDoclet.description, + deprecation : pDoclet && pDoclet.deprecated, + since : pDoclet && pDoclet.since, + experimental : pDoclet && pDoclet.experimental, + type : pSettings && pSettings.type ? pSettings.type.value.value : "" + }; + }); + }); + + var designtime = (metadata.designtime && convertValue(metadata.designtime.value)) || (metadata.designTime && convertValue(metadata.designTime.value)); + if ( typeof designtime === 'string' || typeof designtime === 'boolean' ) { + oClassInfo.designtime = designtime; + } + // console.log(oClassInfo.name + ":" + JSON.stringify(oClassInfo, null, " ")); + } + + // remember class info by name + classInfos[oClassInfo.name] = oClassInfo; + + return oClassInfo; +} + +function collectDesigntimeInfo(dtNode) { + + function each(node, defaultKey, callback) { + var map,n,settings,doclet; + + map = node && createPropertyMap(node.value); + if ( map ) { + for (n in map ) { + if ( map.hasOwnProperty(n) ) { + doclet = getLeadingDoclet(map[n], true); + settings = createPropertyMap(map[n].value, defaultKey); + if ( settings == null ) { + error("no valid metadata for " + n + " (AST type '" + map[n].value.type + "')"); + continue; + } + + callback(n, settings, doclet, map[n]); + } + } + } + } + + var oDesigntimeInfo; + + var map = createPropertyMap(dtNode.argument); + + if (map.annotations) { + + oDesigntimeInfo = { + annotations: {} + }; + + each(map.annotations, null, function(n, settings, doclet) { + var appliesTo = [], + targets = [], + i, oAnno, iPos; + + if (settings.appliesTo) { + for (i = 0; i < settings.appliesTo.value.elements.length; i++) { + appliesTo.push(settings.appliesTo.value.elements[i].value); + } + } + + if (settings.target) { + for (i = 0; i < settings.target.value.elements.length; i++) { + targets.push(settings.target.value.elements[i].value); + } + } + + oDesigntimeInfo.annotations[n] = { + name: n, + doc : doclet && doclet.description, + deprecation : doclet && doclet.deprecated, + since : doclet && doclet.since || settings.since && settings.since.value.value, + namespace: settings.namespace && settings.namespace.value.value, + annotation: settings.annotation && settings.annotation.value.value, + appliesTo: appliesTo, + target: targets, + interpretation: settings.interpretation && settings.interpretation.value.value, + defaultValue: settings.defaultValue && settings.defaultValue.value.value + }; + + oAnno = oDesigntimeInfo.annotations[n].annotation; + iPos = oAnno && oAnno.lastIndexOf("."); + + if ( !oDesigntimeInfo.annotations[n].namespace && iPos > 0 ) { + oDesigntimeInfo.annotations[n].namespace = oAnno.slice(0, iPos); + oDesigntimeInfo.annotations[n].annotation = oAnno.slice(iPos + 1); + } + }); + } + + return oDesigntimeInfo; +} + +function determineValueRangeBorder(range, expression, varname, inverse) { + if ( expression.type === Syntax.BinaryExpression ) { + var value; + if ( expression.left.type === Syntax.Identifier && expression.left.name === varname && expression.right.type === Syntax.Literal ) { + value = expression.right.value; + } else if ( expression.left.type === Syntax.Literal && expression.right.type === Syntax.Identifier && expression.right.name === varname ) { + inverse = !inverse; + value = expression.left.value; + } else { + return false; + } + switch (expression.operator) { + case '<': + range[inverse ? 'minExclusive' : 'maxExclusive'] = value; + break; + case '<=': + range[inverse ? 'minInclusive' : 'maxInclusive'] = value; + break; + case '>=': + range[inverse ? 'maxInclusive' : 'minInclusive'] = value; + break; + case '>': + range[inverse ? 'maxExclusive' : 'minExclusive'] = value; + break; + default: + return false; + } + return true; + } + return false; +} + +function determineValueRange(expression, varname, inverse) { + var range = {}; + if ( expression.type === Syntax.LogicalExpression + && expression.operator === '&&' + && expression.left.type === Syntax.BinaryExpression + && expression.right.type === Syntax.BinaryExpression + && determineValueRangeBorder(range, expression.left, varname, inverse) + && determineValueRangeBorder(range, expression.right, varname, inverse) ) { + return range; + } else if ( expression.type === Syntax.BinaryExpression + && determineValueRangeBorder(range, expression, varname, inverse) ) { + return range; + } + return undefined; +} + +function collectDataTypeInfo(extendCall, classDoclet) { + var args = extendCall.arguments, + i = 0, + name, def, base, pattern, range; + + if ( i < args.length && args[i].type === Syntax.Literal && typeof args[i].value === 'string' ) { + name = args[i++].value; + } + if ( i < args.length && args[i].type === Syntax.ObjectExpression ) { + def = createPropertyMap(args[i++]); + } + if ( i < args.length ) { + if ( args[i].type === Syntax.Literal && typeof args[i].value === 'string' ) { + base = args[i++].value; + } else if ( args[i].type === Syntax.CallExpression + && args[i].callee.type === Syntax.MemberExpression + && /^(sap\.ui\.base\.)?DataType$/.test(getObjectName(args[i].callee.object)) + && args[i].callee.property.type === Syntax.Identifier + && args[i].callee.property.name === 'getType' + && args[i].arguments.length === 1 + && args[i].arguments[0].type === Syntax.Literal + && typeof args[i].arguments[0].value === 'string' ) { + base = args[i++].arguments[0].value; + } else { + error("could not identify base type of data type '" + name + "'"); + } + } else { + base = "any"; + } + + if ( def + && def.isValid + && def.isValid.value.type === Syntax.FunctionExpression + && def.isValid.value.params.length === 1 + && def.isValid.value.params[0].type === Syntax.Identifier + && def.isValid.value.body.body.length === 1 ) { + var varname = def.isValid.value.params[0].name; + var stmt = def.isValid.value.body.body[0]; + if ( stmt.type === Syntax.ReturnStatement && stmt.argument ) { + if ( stmt.argument.type === Syntax.CallExpression + && stmt.argument.callee.type === Syntax.MemberExpression + && stmt.argument.callee.object.type === Syntax.Literal + && stmt.argument.callee.object.regex + && stmt.argument.callee.property.type === Syntax.Identifier + && stmt.argument.callee.property.name === 'test' ) { + pattern = stmt.argument.callee.object.regex.pattern; + // console.log(pattern); + } else { + range = determineValueRange(stmt.argument, varname, false); + } + } else if ( stmt.type === Syntax.IfStatement + && stmt.consequent.type === Syntax.BlockStatement + && stmt.consequent.body.length === 1 + && stmt.consequent.body[0].type === Syntax.ReturnStatement + && stmt.consequent.body[0].argument + && stmt.consequent.body[0].argument.type === Syntax.Literal + && typeof stmt.consequent.body[0].argument.value === 'boolean' + && stmt.alternate.type === Syntax.BlockStatement + && stmt.alternate.body.length === 1 + && stmt.alternate.body[0].type === Syntax.ReturnStatement + && stmt.alternate.body[0].argument + && stmt.alternate.body[0].argument.type === Syntax.Literal + && typeof stmt.alternate.body[0].argument.value === 'boolean' + && stmt.consequent.body[0].argument.value !== typeof stmt.alternate.body[0].argument.value ) { + var inverse = stmt.alternate.body[0].argument.value; + range = determineValueRange(stmt.test, varname, inverse); + } else { + debug("unexpected implementation of a DataType's isValid() implementation: ", stmt); + } + } + + // remember type info by name + if ( name && def && base ) { + typeInfos[name] = { + name: name, + def: def, + pattern: pattern, + range: range, + base: base + }; + // console.log("found data type:", typeInfos[name]); + } +} + +var rEmptyLine = /^\s*$/; + +function createAutoDoc(oClassInfo, classComment, node, parser, filename, commentAlreadyProcessed) { + + var newStyle = !!pluginConfig.newStyle, + includeSettings = !!pluginConfig.includeSettingsInConstructor, + rawClassComment = getRawComment(classComment), + p,n,n1,pName,info,lines,link; + + function isEmpty(obj) { + if ( !obj ) { + return true; + } + for (var n in obj) { + if ( obj.hasOwnProperty(n) ) { + return false; + } + } + return true; + } + + function jsdocCommentFound(comment) { + parser.emit('jsdocCommentFound', { + event:'jsdocCommentFound', + comment : comment, + lineno : node.loc.start.line, + filename : filename, + range : [ node.range[0], node.range[0] ] + }, parser); + } + + function removeDuplicateEmptyLines(lines) { + var lastWasEmpty = false, + i,j,l,line; + + for (i = 0, j = 0, l = lines.length; i < l; i++) { + line = lines[i]; + if ( line == null || rEmptyLine.test(line) ) { + if ( !lastWasEmpty ) { + lines[j++] = line; + } + lastWasEmpty = true; + } else { + lines[j++] = line; + lastWasEmpty = false; + } + } + return j < i ? lines.slice(0,j) : lines; + } + + function newJSDoc(lines) { + //console.log("add completely new jsdoc comment to prog " + node.type + ":" + node.nodeId + ":" + Object.keys(node)); + + lines = removeDuplicateEmptyLines(lines); + lines.push("@synthetic"); + + var comment = " * " + lines.join("\r\n * "); + jsdocCommentFound("/**\r\n" + comment + "\r\n */"); + + var m = /@name\s+([^\r\n\t ]+)/.exec(comment); + debug(" creating synthetic comment '" + (m && m[1]) + "'"); + } + + function rname(prefix,n,_static) { + return (_static ? "." : "#") + prefix + n.slice(0,1).toUpperCase() + n.slice(1); + } + + function name(prefix,n,_static) { + return oClassInfo.name + rname(prefix,n,_static); + } + + /* + * creates a JSDoc type string from the given metadata info object. + * It takes into account the type, the altTypes and the cardinality + * (the latter only if componentTypeOnly is not set). + */ + function makeTypeString(aggr, componentTypeOnly) { + var s = aggr.type; + if ( aggr.altTypes ) { + s = s + "|" + aggr.altTypes.join("|"); + } + if ( !componentTypeOnly && aggr.cardinality === "0..n" ) { + // if multiple types are allowed, use Array.<> for proper grouping + if ( aggr.altTypes ) { + s = "Array.<" + s + ">"; + } else { + s = s + "[]"; + } + } + return s; + } + +// function shortname(s) { +// return s.slice(s.lastIndexOf('.') + 1); +// } + + var HUNGARIAN_PREFIXES = { + 'int' : 'i', + 'boolean' : 'b', + 'float' : 'f', + 'string' : 's', + 'function' : 'fn', + 'object' : 'o', + 'regexp' : 'r', + 'jQuery' : '$', + 'any' : 'o', + 'variant' : 'v', + 'map' : 'm' + }; + + function varname(n, type, property) { + var prefix = HUNGARIAN_PREFIXES[type] || (property ? "s" : "o"); + return prefix + n.slice(0,1).toUpperCase() + n.slice(1); + } + + // add a list of the possible settings if and only if + // - documentation for the constructor exists + // - no (generated) documentation for settings exists already + // - a suitable place for inserting the settings can be found + var m = /(?:^|\r\n|\n|\r)[ \t]*\**[ \t]*@[a-zA-Z]/.exec(rawClassComment); + p = m ? m.index : -1; + var hasSettingsDocs = rawClassComment.indexOf("The supported settings are:") >= 0; + + // heuristic to recognize a ManagedObject + var isManagedObject = ( + /@extends\s+sap\.ui\.(?:base\.ManagedObject|core\.(?:Element|Control|Component))(?:\s|$)/.test(rawClassComment) + || oClassInfo.library + || !isEmpty(oClassInfo.specialSettings) + || !isEmpty(oClassInfo.properties) + || !isEmpty(oClassInfo.aggregations) + || !isEmpty(oClassInfo.associations) + || !isEmpty(oClassInfo.events) + ); + + if ( p >= 0 && !hasSettingsDocs ) { + lines = [ + "" + ]; + + if ( isManagedObject ) { // only a ManagedObject has settings + + if ( oClassInfo.name !== "sap.ui.base.ManagedObject" ) { + // add the hint for the general description only when the current class is not ManagedObject itself + lines.push( + "", + "Accepts an object literal mSettings that defines initial", + "property values, aggregated and associated objects as well as event handlers.", + "See {@link sap.ui.base.ManagedObject#constructor} for a general description of the syntax of the settings object." + ); + } + + // add the settings section only if there are any settings + if ( !isEmpty(oClassInfo.properties) + || !isEmpty(oClassInfo.aggregations) + || !isEmpty(oClassInfo.associations) + || !isEmpty(oClassInfo.events) ) { + + lines.push( + "", + includeSettings ? "" : "@ui5-settings", + "The supported settings are:", + "
      " + ); + if ( !isEmpty(oClassInfo.properties) ) { + lines.push("
    • Properties"); + lines.push("
        "); + for (n in oClassInfo.properties) { + lines.push("
      • {@link " + rname("get", n) + " " + n + "} : " + oClassInfo.properties[n].type + (oClassInfo.properties[n].defaultValue !== null ? " (default: " + oClassInfo.properties[n].defaultValue + ")" : "") + (oClassInfo.defaultProperty == n ? " (default)" : "") + "
      • "); + } + lines.push("
      "); + lines.push("
    • "); + } + if ( !isEmpty(oClassInfo.aggregations) ) { + lines.push("
    • Aggregations"); + lines.push("
        "); + for (n in oClassInfo.aggregations) { + if ( oClassInfo.aggregations[n].visibility !== "hidden" ) { + lines.push("
      • {@link " + rname("get", n) + " " + n + "} : " + makeTypeString(oClassInfo.aggregations[n]) + (oClassInfo.defaultAggregation == n ? " (default)" : "") + "
      • "); + } + } + lines.push("
      "); + lines.push("
    • "); + } + if ( !isEmpty(oClassInfo.associations) ) { + lines.push("
    • Associations"); + lines.push("
        "); + for (n in oClassInfo.associations) { + lines.push("
      • {@link " + rname("get", n) + " " + n + "} : (sap.ui.core.ID | " + oClassInfo.associations[n].type + ")" + (oClassInfo.associations[n].cardinality === "0..n" ? "[]" : "") + "
      • "); + } + lines.push("
      "); + lines.push("
    • "); + } + if ( !isEmpty(oClassInfo.events) ) { + lines.push("
    • Events"); + lines.push("
        "); + for (n in oClassInfo.events) { + lines.push("
      • {@link " + "#event:" + n + " " + n + "} : fnListenerFunction or [fnListenerFunction, oListenerObject] or [oData, fnListenerFunction, oListenerObject]
      • "); + } + lines.push("
      "); + lines.push("
    • "); + } + lines.push("
    "); + + // add the reference to the base class only if this is not ManagedObject and if the base class is known + if ( oClassInfo.name !== "sap.ui.base.ManagedObject" && oClassInfo.baseType ) { + lines.push( + "", + "In addition, all settings applicable to the base type {@link " + oClassInfo.baseType + "#constructor " + oClassInfo.baseType + "}", + "can be used as well." + ); + } + lines.push(""); + + } else if ( oClassInfo.name !== "sap.ui.base.ManagedObject" && oClassInfo.baseType && oClassInfo.hasOwnProperty("abstract") ) { + + // if a class has no settings, but metadata, point at least to the base class - if it makes sense + lines.push( + "", + newStyle && !includeSettings ? "@ui5-settings" : "", + "This class does not have its own settings, but all settings applicable to the base type", + "{@link " + oClassInfo.baseType + "#constructor " + oClassInfo.baseType + "} can be used." + ); + + } + } + + debug(" enhancing constructor documentation with settings"); + var enhancedComment = + rawClassComment.slice(0,p) + + "\n * " + removeDuplicateEmptyLines(lines).join("\n * ") + + (commentAlreadyProcessed ? "@ui5-updated-doclet\n * " : "") + + rawClassComment.slice(p); + enhancedComment = preprocessComment({ comment : enhancedComment, lineno : classComment.lineno }); + + if ( commentAlreadyProcessed ) { + jsdocCommentFound(enhancedComment); + } else { + setRawComment(classComment, enhancedComment); + } + + } + + newJSDoc([ + "Returns a metadata object for class " + oClassInfo.name + ".", + "", + "@returns {sap.ui.base.Metadata} Metadata object describing this class", + "@public", + "@static", + "@name " + name("getMetadata", "", true), + "@function" + ]); + + if ( !oClassInfo["final"] ) { + newJSDoc([ + "Creates a new subclass of class " + oClassInfo.name + " 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") + "}.", + "", + "@param {string} sClassName Name of the class being created", + "@param {object} [oClassInfo] Object literal with information about the class", + "@param {function} [FNMetaImpl] Constructor function for the metadata object; if not given, it defaults to sap.ui.core.ElementMetadata", + "@returns {function} Created class / constructor function", + "@public", + "@static", + "@name " + name("extend", "", true), + "@function" + ]); + } + + for (n in oClassInfo.properties ) { + info = oClassInfo.properties[n]; + if ( info.visibility === 'hidden' ) { + continue; + } + // link = newStyle ? "{@link #setting:" + n + " " + n + "}" : "" + n + ""; + link = "{@link " + (newStyle ? "#setting:" + n : rname("get", n)) + " " + n + "}"; + newJSDoc([ + "Gets current value of property " + link + ".", + "", + !newStyle && info.doc ? info.doc : "", + "", + info.defaultValue !== null ? "Default value is " + (info.defaultValue === "" ? "empty string" : info.defaultValue) + "." : "", + "@returns {" + info.type + "} Value of property " + n + "", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("get",n), + "@function" + ]); + newJSDoc([ + "Sets a new value for property " + link + ".", + "", + !newStyle && info.doc ? info.doc : "", + "", + "When called with a value of null or undefined, the default value of the property will be restored.", + "", + info.defaultValue !== null ? "Default value is " + (info.defaultValue === "" ? "empty string" : info.defaultValue) + "." : "", + "@param {" + info.type + "} " + varname(n,info.type,true) + " New value for property " + n + "", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("set",n), + "@function" + ]); + if ( info.bindable ) { + newJSDoc([ + "Binds property " + link + " to model data.", + "", + "See {@link sap.ui.base.ManagedObject#bindProperty ManagedObject.bindProperty} for a ", + "detailed description of the possible properties of oBindingInfo", + "@param {object} oBindingInfo The binding information", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("bind",n), + "@function" + ]); + newJSDoc([ + "Unbinds property " + link + " from model data.", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("unbind",n), + "@function" + ]); + } + } + + for (n in oClassInfo.aggregations ) { + info = oClassInfo.aggregations[n]; + if ( info.visibility === 'hidden' ) { + continue; + } + // link = newStyle ? "{@link #setting:" + n + " " + n + "}" : "" + n + ""; + link = "{@link " + (newStyle ? "#setting:" + n : rname("get", n)) + " " + n + "}"; + newJSDoc([ + "Gets content of aggregation " + link + ".", + "", + !newStyle && info.doc ? info.doc : "", + "", + n === info.defaultAggregation ? "Note: this is the default aggregation for " + n + "." : "", + "@returns {" + makeTypeString(info) + "}", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("get",n), + "@function" + ]); + if ( info.cardinality == "0..n" ) { + n1 = info.singularName; + newJSDoc([ + "Inserts a " + n1 + " into the aggregation " + link + ".", + "", + "@param {" + makeTypeString(info, true) + "}", + " " + varname(n1,info.altTypes ? "variant" : info.type) + " The " + n1 + " to insert; if empty, nothing is inserted", + "@param {int}", + " iIndex The 0-based index the " + n1 + " should be inserted at; for", + " a negative value of iIndex, the " + n1 + " is inserted at position 0; for a value", + " greater than the current size of the aggregation, the " + n1 + " is inserted at", + " the last position", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("insert",n1), + "@function" + ]); + newJSDoc([ + "Adds some " + n1 + " to the aggregation " + link + ".", + + "@param {" + makeTypeString(info, true) + "}", + " " + varname(n1,info.altTypes ? "variant" : info.type) + " The " + n1 + " to add; if empty, nothing is inserted", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("add",n1), + "@function" + ]); + newJSDoc([ + "Removes a " + n1 + " from the aggregation " + link + ".", + "", + "@param {int | string | " + makeTypeString(info, true) + "} " + varname(n1,"variant") + " The " + n1 + " to remove or its index or id", + "@returns {" + makeTypeString(info, true) + "} The removed " + n1 + " or null", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("remove", n1), + "@function" + ]); + newJSDoc([ + "Removes all the controls from the aggregation " + link + ".", + "", + "Additionally, it unregisters them from the hosting UIArea.", + "@returns {" + makeTypeString(info) + "} An array of the removed elements (might be empty)", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("removeAll", n), + "@function" + ]); + newJSDoc([ + "Checks for the provided " + info.type + " in the aggregation " + link + ".", + "and returns its index if found or -1 otherwise.", + "@param {" + makeTypeString(info, true) + "}", + " " + varname(n1, info.altTypes ? "variant" : info.type) + " The " + n1 + " whose index is looked for", + "@returns {int} The index of the provided control in the aggregation if found, or -1 otherwise", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("indexOf", n1), + "@function" + ]); + } else { + newJSDoc([ + "Sets the aggregated " + link + ".", + "@param {" + makeTypeString(info) + "} " + varname(n, info.altTypes ? "variant" : info.type) + " The " + n + " to set", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("set", n), + "@function" + ]); + } + newJSDoc([ + "Destroys " + (info.cardinality === "0..n" ? "all " : "") + "the " + n + " in the aggregation " + link + ".", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("destroy", n), + "@function" + ]); + if ( info.bindable ) { + newJSDoc([ + "Binds aggregation " + link + " to model data.", + "", + "See {@link sap.ui.base.ManagedObject#bindAggregation ManagedObject.bindAggregation} for a ", + "detailed description of the possible properties of oBindingInfo.", + "@param {object} oBindingInfo The binding information", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("bind",n), + "@function" + ]); + newJSDoc([ + "Unbinds aggregation " + link + " from model data.", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("unbind",n), + "@function" + ]); + } + } + + for (n in oClassInfo.associations ) { + info = oClassInfo.associations[n]; + if ( info.visibility === 'hidden' ) { + continue; + } + // link = newStyle ? "{@link #setting:" + n + " " + n + "}" : "" + n + ""; + link = "{@link " + (newStyle ? "#setting:" + n : rname("get", n)) + " " + n + "}"; + newJSDoc([ + info.cardinality === "0..n" ? + "Returns array of IDs of the elements which are the current targets of the association " + link + "." : + "ID of the element which is the current target of the association " + link + ", or null.", + "", + newStyle && info.doc ? info.doc : "", + "", + "@returns {sap.ui.core.ID" + (info.cardinality === "0..n" ? "[]" : "") + "}", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("get",n), + "@function" + ]); + if ( info.cardinality === "0..n" ) { + n1 = info.singularName; + newJSDoc([ + "Adds some " + n1 + " into the association " + link + ".", + "", + "@param {sap.ui.core.ID | " + info.type + "} " + varname(n1, "variant") + " The " + n + " to add; if empty, nothing is inserted", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("add",n1), + "@function" + ]); + newJSDoc([ + "Removes an " + n1 + " from the association named " + link + ".", + "@param {int | sap.ui.core.ID | " + info.type + "} " + varname(n1,"variant") + " The " + n1 + " to be removed or its index or ID", + "@returns {sap.ui.core.ID} The removed " + n1 + " or null", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("remove", n1), + "@function" + ]); + newJSDoc([ + "Removes all the controls in the association named " + link + ".", + "@returns {sap.ui.core.ID[]} An array of the removed elements (might be empty)", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("removeAll", n), + "@function" + ]); + } else { + newJSDoc([ + "Sets the associated " + link + ".", + "@param {sap.ui.core.ID | " + info.type + "} " + varname(n, info.type) + " ID of an element which becomes the new target of this " + n + " association; alternatively, an element instance may be given", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("set", n), + "@function" + ]); + } + } + + for (n in oClassInfo.events ) { + info = oClassInfo.events[n]; + //link = newStyle ? "{@link #event:" + n + " " + n + "}" : "" + n + ""; + link = "{@link #event:" + n + " " + n + "}"; + + lines = [ + info.doc ? info.doc : "", + "", + "@name " + oClassInfo.name + "#" + n, + "@event", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@param {sap.ui.base.Event} oControlEvent", + "@param {sap.ui.base.EventProvider} oControlEvent.getSource", + "@param {object} oControlEvent.getParameters" + ]; + for (pName in info.parameters ) { + lines.push( + "@param {" + (info.parameters[pName].type || "") + "} oControlEvent.getParameters." + pName + " " + (info.parameters[pName].doc || "") + ); + } + lines.push("@public"); + newJSDoc(lines); + + newJSDoc([ + "Attaches event handler fnFunction to the " + link + " event of this " + oClassInfo.name + ".", + "", + "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.", + "", + !newStyle && info.doc ? info.doc : "", + "", + "@param {object}", + " [oData] An application-specific payload object that will be passed to the event handler along with the event object when firing the event", + "@param {function}", + " 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", + "", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + "@public", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@name " + name("attach", n), + "@function" + ]); + newJSDoc([ + "Detaches event handler fnFunction from the " + link + " event of this " + oClassInfo.name + ".", + "", + "The passed function and listener object must match the ones used for event registration.", + "", + "@param {function}", + " fnFunction The function to be called, when the event occurs", + "@param {object}", + " [oListener] Context object on which the given function had to be called", + "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@public", + "@name " + name("detach", n), + "@function" + ]); + + // build documentation for fireEvent. It contains conditional parts which makes it a bit more complicated + lines = [ + "Fires event " + link + " to attached listeners." + ]; + if ( info.allowPreventDefault ) { + lines.push( + "", + "Listeners may prevent the default action of this event by using the preventDefault-method on the event object.", + ""); + } + lines.push( + "", + "@param {object} [mParameters] Parameters to pass along with the event" + ); + if ( !isEmpty(info.parameters) ) { + for (pName in info.parameters) { + lines.push( + "@param {" + (info.parameters[pName].type || "any") + "} [mParameters." + pName + "] " + (info.parameters[pName].doc || "") + ); + } + lines.push(""); + } + if ( info.allowPreventDefault ) { + lines.push("@returns {boolean} Whether or not to prevent the default action"); + } else { + lines.push("@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining"); + } + lines.push( + "@protected", + info.since ? "@since " + info.since : "", + info.deprecation ? "@deprecated " + info.deprecation : "", + info.experimental ? "@experimental " + info.experimental : "", + "@name " + name("fire", n), + "@function" + ); + newJSDoc(lines); + } + +} + +function createDataTypeAutoDoc(oTypeInfo, classComment, node, parser, filename) { +} + +/** + * Creates a human readable location info for a given doclet. + * @param {Doclet} doclet Doclet to get a location info for + * @returns {string} A human readable location info + */ +function location(doclet) { + var filename = (doclet.meta && doclet.meta.filename) || "unknown"; + return " #" + ui5data(doclet).id + "@" + filename + (doclet.meta.lineno != null ? ":" + doclet.meta.lineno : "") + (doclet.synthetic ? "(synthetic)" : ""); +} + +// ---- Comment handling --------------------------------------------------------------------------- + +// --- comment related functions that depend on the JSdoc version (e.g. on the used parser) + +var isDocComment; +var getLeadingCommentNode; + +// JSDoc added the node type Syntax.File with the same change that activated Babylon +// See https://github.com/jsdoc3/jsdoc/commit/ffec4a42291de6d68e6240f304b68d6abb82a869 +if ( Syntax.File === 'File' ) { + + // JSDoc starting with version 3.5.0 + + isDocComment = function isDocCommentBabylon(comment) { + return comment && comment.type === 'CommentBlock' && comment.value && comment.value.charAt(0) === '*'; + }; + + getLeadingCommentNode = function getLeadingCommentNodeBabylon(node, longname) { + var leadingComments = node.leadingComments; + if ( Array.isArray(leadingComments) ) { + // in babylon, all comments are already attached to the node + // and the last one is the closest one and should win + // non-block comments have to be filtered out + leadingComments = leadingComments.filter(isDocComment); + if ( leadingComments.length > 0 ) { + return leadingComments[leadingComments.length - 1]; + } + } + }; + +} else { + + // JSDoc versions before 3.5.0 + + isDocComment = function isDoccommentEsprima(comment) { + return comment && comment.type === 'Block'; + }; + + getLeadingCommentNode = function getLeadingCommentNodeEsprima(node, longname) { + var comment, + leadingComments = node.leadingComments; + + // when espree is used, JSDOc attached the leading comment and the first one was picked + if (Array.isArray(leadingComments) && leadingComments.length && leadingComments[0].raw) { + comment = leadingComments[0]; + } + + // also check all comments attached to the Program node (if found) whether they refer to the same longname + // TODO check why any matches here override the direct leading comment from above + if ( longname && currentProgram && currentProgram.leadingComments && currentProgram.leadingComments.length ) { + leadingComments = currentProgram.leadingComments; + var rLongname = new RegExp("@(name|alias|class|namespace)\\s+" + longname.replace(/\./g, '\\.')); + for ( var i = 0; i < leadingComments.length; i++ ) { + var raw = getRawComment(leadingComments[i]); + if ( /^\/\*\*[\s\S]*\*\/$/.test(raw) && rLongname.test(raw) ) { + comment = leadingComments[i]; + // console.log("\n\n**** alternative comment found for " + longname + " on program level\n\n", comment); + break; + } + } + } + + return comment; + }; +} + +//--- comment related functions that are independent from the JSdoc version + +function getLeadingComment(node) { + var comment = getLeadingCommentNode(node); + return comment ? getRawComment(comment) : null; +} + +function getLeadingDoclet(node, preprocess) { + var comment = getLeadingComment(node); + if ( comment && preprocess ) { + comment = preprocessComment({comment:comment, lineno: node.loc.start.line }); + } + return comment ? new Doclet(comment, {}) : null; +} + +/** + * Determines the raw comment string (source code form, including leading and trailing comment markers / *...* /) from a comment node. + * Works for Esprima and Babylon based JSDoc versions. + * @param {ASTNode} commentNode Node that contains the comment. + * @returns {string} Comment string as written in the source + */ +function getRawComment(commentNode) { + // in esprima, there's a 'raw' property, in babylon, the 'raw' string has to be reconstructed from the 'value' by adding the markers + return commentNode ? commentNode.raw || '/*' + commentNode.value + '*/' : ''; +} + +function setRawComment(commentNode, newRawComment) { + if ( commentNode.raw ) { + commentNode.raw = newRawComment; + } + commentNode.value = newRawComment.slice(2, -2); +} + +/** + * Removes the mandatory comment markers and the optional but common asterisks at the beginning of each JSDoc comment line. + * + * The result is easier to parse/analyze. + * + * Implementation is a 1:1 copy from JSDoc's lib/jsdoc/doclet.js (closure function, not directly reusable) + * + * @param {string} docletSrc the source comment with or without block comment markers + * @returns {string} the unwrapped content of the JSDoc comment + * + */ +function unwrap(docletSrc) { + if (!docletSrc) { return ''; } + + // note: keep trailing whitespace for @examples + // extra opening/closing stars are ignored + // left margin is considered a star and a space + // use the /m flag on regex to avoid having to guess what this platform's newline is + docletSrc = + docletSrc.replace(/^\/\*\*+/, '') // remove opening slash+stars + .replace(/\**\*\/$/, "\\Z") // replace closing star slash with end-marker + .replace(/^\s*(\* ?|\\Z)/gm, '') // remove left margin like: spaces+star or spaces+end-marker + .replace(/\s*\\Z$/g, ''); // remove end-marker + + return docletSrc; +} + +/** + * Inverse operation of unwrap. + * + * The prefix for lines is fixed to be " * ", lines are separated with '\n', independent from the platform. + * @param {string|string[]} lines Multiline string or an array of lines + * @returns {string} Full comment string created from the line(s) + */ +function wrap(lines) { + if ( typeof lines === "string" ) { + lines = lines.split(/\r\n?|\n/); + } + return "/**\n * " + lines.join('\n * ') + "\n */"; +} + +/** + * Pre-processes a JSDoc comment string to ensure some UI5 standards. + * + * @param {event} e Event for the new comment + * @returns {event} Returns the modified event + */ +function preprocessComment(e) { + + var src = e.comment; + + // add a default visibility + if ( !/@private|@public|@protected|@sap-restricted|@ui5-restricted/.test(src) ) { + src = unwrap(src); + src = src + "\n@private"; + src = wrap(src); + // console.log("added default visibility to '" + src + "'"); + } + + if ( /@class/.test(src) && /@static/.test(src) ) { + warning("combination of @class and @static is no longer supported with jsdoc3, converting it to @namespace and @classdesc: (line " + e.lineno + ")"); + src = unwrap(src); + src = src.replace(/@class/, "@classdesc").replace(/@static/, "@namespace"); + src = wrap(src); + //console.log(src); + } + + return src; + +} + +// ---- other functionality --------------------------------------------------------------------------- + +// HACK: override cli.exit() to avoid that JSDoc3 exits the VM +if ( pluginConfig.noExit ) { + info("disabling exit() call"); + require( path.join(global.env.dirname, 'cli') ).exit = function(retval) { + info("cli.exit(): do nothing (ret val=" + retval + ")"); + }; +} + + +// ---- exports ---------------------------------------------------------------------------------------- + +exports.defineTags = function(dictionary) { + + /** + * a special value that is not 'falsy' but results in an empty string when output + * Used for the disclaimer and experimental tag + */ + var EMPTY = { + toString: function() { return ""; } + }; + + /** + * A sapui5 specific tag to add a disclaimer to a symbol + */ + dictionary.defineTag('disclaimer', { + // value is optional + onTagged: function(doclet, tag) { + doclet.disclaimer = tag.value || EMPTY; + } + }); + + /** + * A sapui5 specific tag to mark a symbol as experimental. + */ + dictionary.defineTag('experimental', { + // value is optional + onTagged: function(doclet, tag) { + doclet.experimental = tag.value || EMPTY; + } + }); + + /** + * Re-introduce the deprecated 'final tag. JSDoc used it as a synonym for readonly, but we use it to mark classes as final + */ + dictionary.defineTag('final', { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.final_ = true; + } + }); + + /** + * Introduce a new kind of symbol: 'interface' + * 'interface' is like 'class', but without a constructor. + * Support for 'interface' might not be complete (only standard UI5 use cases tested) + */ + dictionary.defineTag('interface', { + //mustNotHaveValue: true, + onTagged: function(doclet, tag) { + // debug("setting kind of " + doclet.name + " to 'interface'"); + doclet.kind = 'interface'; + if ( tag.value ) { + doclet.classdesc = tag.value; + } + } + }); + + /** + * Classes can declare that they implement a set of interfaces + */ + dictionary.defineTag('implements', { + mustHaveValue: true, + onTagged: function(doclet, tag) { + // console.log("setting implements of " + doclet.name + " to 'interface'"); + if ( tag.value ) { + doclet.implements = doclet.implements || []; + tag.value.split(/\s*,\s*/g).forEach(function($) { + if ( doclet.implements.indexOf($) < 0 ) { + doclet.implements.push($); + } + }); + } + } + }); + + /** + * Set the visibility of a doclet to 'restricted'. + */ + dictionary.defineTag('ui5-restricted', { + onTagged: function(doclet, tag) { + doclet.access = 'restricted'; + if ( tag.value ) { + ui5data(doclet).stakeholders = tag.value.trim().split(/(?:\s*,\s*|\s+)/); + } + } + }); + dictionary.defineSynonym('ui5-restricted', 'sap-restricted'); + + /** + * Mark a doclet as synthetic. + * + * Used for doclets that the autodoc generation creates. This helps the template + * later to recognize such doclets and maybe filter them out. + */ + dictionary.defineTag('synthetic', { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.synthetic = true; + } + }); + + /** + * Mark a doclet that intentionally updates a previous doclet + */ + dictionary.defineTag('ui5-updated-doclet', { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + ui5data(doclet).updatedDoclet = true; + } + }); + + /** + * The @hideconstructor tag tells JSDoc that the generated documentation should not display the constructor for a class. + * Note: this tag will be natively available in JSDoc >= 3.5.0 + */ + dictionary.defineTag('hideconstructor', { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.hideconstructor = true; + } + }); + +}; + +exports.handlers = { + + /** + * Before all files are parsed, determine the common path prefix of all filenames + * @param {object} e Event info object + */ + parseBegin : function(e) { + + pathPrefixes = env.opts._.reduce(function(result, fileOrDir) { + fileOrDir = path.resolve( path.normalize(fileOrDir) ); + if ( fs.statSync(fileOrDir).isDirectory() ) { + // ensure a trailing path separator + if ( fileOrDir.indexOf(path.sep, fileOrDir.length - path.sep.length) < 0 ) { + fileOrDir += path.sep; + } + result.push(fileOrDir); + } + return result; + }, []); + resourceNamePrefixes = pluginConfig.resourceNamePrefixes || []; + if ( !Array.isArray(resourceNamePrefixes) ) { + resourceNamePrefixes = [resourceNamePrefixes]; + } + resourceNamePrefixes.forEach(ensureEndingSlash); + while ( resourceNamePrefixes.length < pathPrefixes.length ) { + resourceNamePrefixes.push(''); + } + + debug("path prefixes " + JSON.stringify(pathPrefixes)); + debug("resource name prefixes " + JSON.stringify(resourceNamePrefixes)); + }, + + /** + * Log each file before it is parsed + * @param {object} e Event info object + */ + fileBegin: function (e) { + currentProgram = undefined; + currentModule = { + name: null, + resource: getResourceName(e.filename), + module: getModuleName(getResourceName(e.filename)), + localNames: Object.create(null) + }; + }, + + fileComplete: function (e) { + // debug("module info after parsing: ", currentModule); + currentSource = undefined; + currentProgram = undefined; + currentModule = undefined; + }, + + jsdocCommentFound: function(e) { + // console.log("jsdocCommentFound: " + e.comment); + e.comment = preprocessComment(e); + }, + + symbolFound: function(e) { + // console.log("symbolFound: " + e.comment); + }, + + newDoclet: function(e) { + + var _ui5data = ui5data(e.doclet); + + // remove code: this is a try to reduce the required heap size + if ( e.doclet.meta ) { + if ( e.doclet.meta.code ) { + e.doclet.meta.code = {}; + } + var filepath = (e.doclet.meta.path && e.doclet.meta.path !== 'null' ) ? path.join(e.doclet.meta.path, e.doclet.meta.filename) : e.doclet.meta.filename; + e.doclet.meta.__shortpath = getRelativePath(filepath); + _ui5data.resource = currentModule.resource; + _ui5data.module = currentModule.name || currentModule.module; + } + + + // JSDoc 3 has a bug when it encounters a property in an object literal with an empty string as name + // (e.g. { "" : something } will result in a doclet without longname + if ( !e.doclet.longname ) { + if ( e.doclet.memberof ) { + e.doclet.longname = e.doclet.memberof + "." + e.doclet.name; // TODO '.' depends on scope? + warning("found doclet without longname, derived longname: " + e.doclet.longname + " " + location(e.doclet)); + } else { + error("found doclet without longname, could not derive longname " + location(e.doclet)); + } + return; + } + + // try to detect misused memberof + if ( e.doclet.memberof && e.doclet.longname.indexOf(e.doclet.memberof) !== 0 ) { + warning("potentially unsupported use of @name and @memberof " + location(e.doclet)); + //console.log(e.doclet); + } + + if ( e.doclet.returns + && e.doclet.returns.length > 0 + && e.doclet.returns[0] + && e.doclet.returns[0].type + && e.doclet.returns[0].type.names + && e.doclet.returns[0].type.names[0] === 'this' + && e.doclet.memberof ) { + warning("fixing return type 'this' with " + e.doclet.memberof); + e.doclet.returns[0].type.names[0] = e.doclet.memberof; + } + }, + + beforeParse : function(e) { + msgHeader("parsing " + getRelativePath(e.filename)); + currentSource = e.source; + }, + + parseComplete : function(e) { + + var doclets = e.doclets; + var l = doclets.length,i,j,doclet; + //var noprivate = !env.opts.private; + var rAnonymous = /^(~|$)/; + + // remove undocumented symbols, ignored symbols, anonymous functions and their members, scope members + for (i = 0, j = 0; i < l; i++) { + + doclet = doclets[i]; + if ( !doclet.undocumented && + !doclet.ignore && + !(doclet.memberof && rAnonymous.test(doclet.memberof)) && + doclet.longname.indexOf("~") < 0 ) { + doclets[j++] = doclet; + } + } + if ( j < l ) { + doclets.splice(j, l - j); + info("removed " + (l - j) + " undocumented, ignored or anonymous symbols"); + l = j; + } + + // sort doclets by name, synthetic, lineno, uid + // 'ignore' is a combination of criteria, see function above + debug("sorting doclets by name"); + doclets.sort(function(a,b) { + if ( a.longname === b.longname ) { + if ( a.synthetic === b.synthetic ) { + if ( a.meta && b.meta && a.meta.filename == b.meta.filename ) { + if ( a.meta.lineno !== b.meta.lineno ) { + return a.meta.lineno < b.meta.lineno ? -1 : 1; + } + } + return a.__ui5.id - b.__ui5.id; + } + return a.synthetic && !b.synthetic ? -1 : 1; + } + return a.longname < b.longname ? -1 : 1; + }); + debug("sorting doclets by name done."); + + for (i = 0, j = 0; i < l; i++) { + + doclet = doclets[i]; + + // add metadata to symbol + if ( classInfos[doclet.longname] ) { + doclet.__ui5.metadata = classInfos[doclet.longname]; + + // add designtime infos, if configured + var designtimeModule = doclet.__ui5.metadata.designtime; + if ( designtimeModule && typeof designtimeModule !== 'string' ) { + designtimeModule = doclet.__ui5.module + ".designtime"; + } + if ( designtimeModule && designtimeInfos[designtimeModule] ) { + info("associating designtime data with class metadata: ", designtimeModule); + // TODO do a more generic merge or maybe add whole information as "designtime" information + doclet.__ui5.metadata.annotations = designtimeInfos[designtimeModule].annotations; + } + + // derive extends from UI5 APIs + if ( doclet.__ui5.metadata.baseType + && !(doclet.augments && doclet.augments.length > 0) ) { + doclet.augments = doclet.augments || []; + info(" @extends " + doclet.__ui5.metadata.baseType + " derived from UI5 APIs (" + doclet.longname + ")"); + doclet.augments.push(doclet.__ui5.metadata.baseType); + } + + // derive interface implementations from UI5 metadata + if ( doclet.__ui5.metadata.interfaces && doclet.__ui5.metadata.interfaces.length ) { + /* eslint-disable no-loop-func */ + doclet.__ui5.metadata.interfaces.forEach(function(intf) { + doclet.implements = doclet.implements || []; + if ( doclet.implements.indexOf(intf) < 0 ) { + info(" @implements " + intf + " derived from UI5 metadata (" + doclet.longname + ")"); + doclet.implements.push(intf); + } + }); + /* eslint-enable no-loop-func */ + } + } + + if ( typeInfos[doclet.longname] ) { + doclet.__ui5.stereotype = 'datatype'; + doclet.__ui5.metadata = { + basetype: typeInfos[doclet.longname].base, + pattern: typeInfos[doclet.longname].pattern, + range: typeInfos[doclet.longname].range + }; + } + + // check for duplicates: last one wins + if ( j > 0 && doclets[j - 1].longname === doclet.longname ) { + if ( !doclets[j - 1].synthetic && !doclet.__ui5.updatedDoclet ) { + // replacing synthetic comments or updating comments are trivial case. Just log non-trivial duplicates + debug("ignoring duplicate doclet for " + doclet.longname + ":" + location(doclet) + " overrides " + location(doclets[j - 1])); + } + doclets[j - 1] = doclet; + } else { + doclets[j++] = doclet; + } + } + + if ( j < l ) { + doclets.splice(j, l - j); + info("removed " + (l - j) + " duplicate symbols - " + doclets.length + " remaining"); + } + + if ( pluginConfig.saveSymbols ) { + + fs.mkPath(env.opts.destination); + fs.writeFileSync(path.join(env.opts.destination, "symbols-parseComplete.json"), JSON.stringify(e.doclets, null, "\t"), 'utf8'); + + } + + } +}; + +exports.astNodeVisitor = { + + visitNode: function(node, e, parser, currentSourceName) { + + var comment; + + if ( node.type === Syntax.Program ) { + currentProgram = node; + } + + function processExtendCall(extendCall, comment, commentAlreadyProcessed) { + var doclet = comment && new Doclet(getRawComment(comment), {}); + var classInfo = collectClassInfo(extendCall, doclet); + if ( classInfo ) { + createAutoDoc(classInfo, comment, extendCall, parser, currentSourceName, commentAlreadyProcessed); + } + } + + function processDataType(createCall, comment) { + var doclet = comment && new Doclet(getRawComment(comment), {}); + var typeInfo = collectDataTypeInfo(createCall, doclet); + if ( typeInfo ) { + createDataTypeAutoDoc(typeInfo, comment, createCall, parser, currentSourceName); + } + } + + if ( node.type === Syntax.ExpressionStatement ) { + if ( isSapUiDefineCall(node.expression) ) { + analyzeModuleDefinition(node.expression); + /* + } else if ( isJQuerySapDeclareCall(node.expression) + && node.expression.arguments.length > 0 + && node.expression.arguments[0].type === Syntax.Literal + && typeof node.expression.arguments[0].value === "string" ) { + warning("module has explicit module name " + node.expression.arguments[0].value); + */ + } + + } + + if (node.type === Syntax.ReturnStatement && node.argument && node.argument.type === Syntax.ObjectExpression && /\.designtime\.js$/.test(currentSourceName) ) { + + // assume this node to return designtime metadata. Collect it and remember it by its module name + var oDesigntimeInfo = collectDesigntimeInfo(node); + if ( oDesigntimeInfo ) { + designtimeInfos[currentModule.module] = oDesigntimeInfo; + info("collected designtime info " + currentModule.module); + } + + } else if ( node.type === Syntax.ExpressionStatement && isExtendCall(node.expression) ) { + + // Something.extend(...) -- return value (new class) is not used in an assignment + + // className = node.expression.arguments[0].value; + comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.expression); + // console.log("ast node with comment " + comment); + processExtendCall(node.expression, comment); + + } else if ( node.type === Syntax.VariableDeclaration ) { + node.declarations.forEach(function(decl, idx) { + if ( isExtendCall(decl.init) ) { + // var NewClass = Something.extend(...) + + // className = node.declarations[0].init.arguments[0].value; + comment = (idx === 0 ? getLeadingCommentNode(node) : undefined) || getLeadingCommentNode(decl); + // console.log("ast node with comment " + comment); + processExtendCall(decl.init, comment); + } + }); + + } else if ( node.type === Syntax.ReturnStatement && isExtendCall(node.argument) ) { + + // return Something.extend(...) + + var className = node.argument.arguments[0].value; + comment = getLeadingCommentNode(node, className) || getLeadingCommentNode(node.argument, className); + // console.log("ast node with comment " + comment); + processExtendCall(node.argument, comment, true); + } else if ( node.type === Syntax.ExpressionStatement && node.expression.type === Syntax.AssignmentExpression ) { + + if ( isCreateDataTypeCall(node.expression.right) ) { + + // thisLib.TypeName = DataType.createType( ... ) + comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.expression); + processDataType(node.expression.right); + // TODO remember knowledge about type and its name (left hand side of assignment) + + } + + } + } + +}; diff --git a/lib/processors/jsdoc/lib/ui5/template/publish.js b/lib/processors/jsdoc/lib/ui5/template/publish.js new file mode 100644 index 000000000..4ea98a10c --- /dev/null +++ b/lib/processors/jsdoc/lib/ui5/template/publish.js @@ -0,0 +1,4129 @@ +/* + * JSDoc3 template for UI5 documentation generation. + * + * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +/*global env: true, require, exports */ +/*eslint strict: [2, "global"]*/ + +"use strict"; + +/* imports */ +var template = require('jsdoc/template'), + helper = require('jsdoc/util/templateHelper'), + fs = require('jsdoc/fs'), + doclet = require('jsdoc/doclet'), + path = require('jsdoc/path'); + +/* globals, constants */ +var MY_TEMPLATE_NAME = "ui5", + ANONYMOUS_LONGNAME = doclet.ANONYMOUS_LONGNAME, + A_SECURITY_TAGS = [ + { + name : "SecSource", + caption : "Taint Source", + description : "APIs that might introduce tainted data into an application, e.g. due to user input or network access", + params : ["out","flags"] + }, + { + name : "SecEntryPoint", + caption : "Taint Entry Point", + description: "APIs that are called implicitly by a framework or server and trigger execution of application logic", + params : ["in","flags"] + }, + { + name : "SecSink", + caption : "Taint Sink", + description : "APIs that pose a security risk when they receive tainted data", + params : ["in","flags"] + }, + { + name : "SecPassthrough", + caption : "Taint Passthrough", + description : "APIs that might propagate tainted data when they receive it as input", + params : ["in","out","flags"] + }, + { + name : "SecValidate", + caption : "Validation", + description : "APIs that (partially) cleanse tainted data so that it no longer poses a security risk in the further data flow of an application", + params : ["in","out","flags"] + } + ]; + +var rSecurityTags = new RegExp(A_SECURITY_TAGS.map(function($) {return $.name.toLowerCase(); }).join('|'), "i"); + //debug(A_SECURITY_TAGS.map(function($) {return $.name; }).join('|')); + +var templateConf = (env.conf.templates || {})[MY_TEMPLATE_NAME] || {}, + pluginConf = templateConf, + conf = {}, + view; + +var __db; +var __longnames; +var __missingLongnames = {}; + +/** + * Maps the symbol 'longname's to the unique filename that contains the documentation of that symbol. + * This map is maintained to deal with names that only differ in case (e.g. the namespace sap.ui.model.type and the class sap.ui.model.Type). + */ +var __uniqueFilenames = {}; + +/* eslint-disable no-console */ +function info() { + if ( env.opts.verbose || env.opts.debug ) { + console.log.apply(console, arguments); + } +} + +function warning(msg) { + var args = Array.prototype.slice.apply(arguments); + args[0] = "**** warning: " + args[0]; + console.log.apply(console, args); +} + +function error(msg) { + var args = Array.prototype.slice.apply(arguments); + args[0] = "**** error: " + args[0]; + console.log.apply(console, args); +} + +function debug() { + if ( env.opts.debug ) { + console.log.apply(console, arguments); + } +} +/* eslint-disable no-console */ + +function compare(v1, v2) { + if ( v1 !== v2 ) { + return v1 < v2 ? -1 : 1; + } + return 0; +} + +function merge(target, source) { + if ( source != null ) { + // simple single source merge + Object.keys(source).forEach(function(prop) { + var value = source[prop]; + if ( value != null && value.constructor === Object ) { + merge(target[prop] || {}, value); + } else { + target[prop] = value; + } + }); + } + // if there are more sources, merge them, too + for (var i = 2; i < arguments.length; i++) { + merge(target, arguments[i]); + } + return target; +} + +function lookup(longname /*, variant*/) { + var key = longname; // variant ? longname + "|" + variant : longname; + if ( !Object.prototype.hasOwnProperty.call(__longnames, key) ) { + __missingLongnames[key] = (__missingLongnames[key] || 0) + 1; + var oResult = __db({longname: longname /*, variant: variant ? variant : {isUndefined: true}*/}); + __longnames[key] = oResult.first(); + } + return __longnames[key]; +} + +var externalSymbols = {}; + +function loadExternalSymbols(apiJsonFolder) { + + var files; + + try { + files = fs.readdirSync(templateConf.apiJsonFolder); + } catch (e) { + error("failed to list symbol files in folder '" + apiJsonFolder + "': " + (e.message || e)); + return; + } + + if ( files && files.length ) { + files.forEach(function(localFileName) { + var file = path.join(templateConf.apiJsonFolder, localFileName); + try { + var sJSON = fs.readFileSync(file, 'UTF-8'); + var data = JSON.parse(sJSON); + if ( !Array.isArray(data.symbols) ) { + throw new TypeError("api.json does not contain a 'symbols' array"); + } + data.symbols.forEach(function(symbol) { + debug(" adding external symbol " + symbol.name); + externalSymbols[symbol.name] = symbol; + }); + } catch (e) { + error("failed to load symbols from " + file + ": " + (e.message || e)); + } + }); + } +} + +function isModuleExport($) { + return $.longname.startsWith("module:") && $.longname.search(/[.#~]/) < 0; +} + +function isaClass($) { + return /^(namespace|interface|class|typedef)$/.test($.kind) || ($.kind === 'member' && $.isEnum )/* isNonEmptyNamespace($) */; +} + +function supportsInheritance($) { + return /^(interface|class|typedef)$/.test($.kind); +} + +/* + * Returns true for any symbol that should appear in the API reference index of the SDK. + * + * In a perfect world, such symbols would be + * - default exports of AMD modules (named 'module:some/module) + * - classes, interfaces, enums, typedefs and namespaces, all with global names whose parents are all namespaces + * In the less perfect documentation build, the criterion 'whose parents are all namespaces' is ignored + */ +function isFirstClassSymbol($) { + return /^(namespace|interface|class|typedef)$/.test($.kind) || ($.kind === 'member' && $.isEnum || isModuleExport($) )/* isNonEmptyNamespace($) */; +} + + +var REGEXP_ARRAY_TYPE = /^Array\.<(.*)>$/; + +// ---- Version class ----------------------------------------------------------------------------------------------------------------------------------------------------------- + +var Version = (function() { + + var rVersion = /^[0-9]+(?:\.([0-9]+)(?:\.([0-9]+))?)?(.*)$/; + + /** + * Creates a Version object from the given version string. + * + * @param {string} versionStr A dot-separated version string + * + * @classdesc Represents a version consisting of major, minor, patch version and suffix, + * e.g. '1.2.7-SNAPSHOT'. All parts after the major version are optional. + * @class + */ + function Version(versionStr) { + + var match = rVersion.exec(versionStr) || []; + + function norm(v) { + v = parseInt(v); + return isNaN(v) ? 0 : v; + } + + Object.defineProperty(this, "major", { + enumerable: true, + value: norm(match[0]) + }); + Object.defineProperty(this, "minor", { + enumerable: true, + value: norm(match[1]) + }); + Object.defineProperty(this, "patch", { + enumerable: true, + value: norm(match[2]) + }); + Object.defineProperty(this, "suffix", { + enumerable: true, + value: String(match[3] || "") + }); + + } + + Version.prototype.toMajorMinor = function() { + return new Version(this.major + "." + this.minor); + }; + + Version.prototype.toString = function() { + return this.major + "." + this.minor + "." + this.patch + this.suffix; + }; + + Version.prototype.compareTo = function(other) { + return this.major - other.major || + this.minor - other.minor || + this.patch - other.patch || + compare(this.suffix, other.suffix); + }; + + return Version; + +}()); + +// ---- Link class -------------------------------------------------------------------------------------------------------------------------------------------------------------- + +//TODO move to separate module + +var Link = (function() { + + var Link = function() { + }; + + Link.prototype.toSymbol = function(longname) { + if ( longname != null ) { + longname = String(longname); + if ( /#constructor$/.test(longname) ) { + if ( !this.innerName ) { + this.innerName = 'constructor'; + } + longname = longname.slice(0, -"#constructor".length); + } + this.longname = longname; + } + return this; + }; + + Link.prototype.withText = function(text) { + this.text = text; + return this; + }; + + Link.prototype.withTooltip = function(text) { + this.tooltip = text; + return this; + }; + + Link.prototype.toFile = function(file) { + if ( file != null ) { + this.file = file; + } + return this; + }; + + function _makeLink(href, target, tooltip, text) { + return '' + text + ''; + } + + Link.prototype.toString = function() { + var longname = this.longname, + linkString; + + if (longname) { + + if ( /^(?:(?:ftp|https?):\/\/|\.\.?\/)/.test(longname) ) { + // handle real hyperlinks (TODO should be handled with a different "to" method + linkString = _makeLink(longname, this.targetName, this.tooltip, this.text || longname); + } else if ( /^topic:/.test(longname) ) { + // handle documentation links + longname = conf.topicUrlPattern.replace("{{topic}}", longname.slice("topic:".length)); + linkString = _makeLink(longname, this.targetName, this.tooltip, this.text || longname); + } else { + linkString = this._makeSymbolLink(longname); + } + + } else if (this.file) { + linkString = _makeLink(Link.base + this.file, this.targetName, null, this.text || this.file); + } + + return linkString; + }; + + var missingTypes = {}; + Link.getMissingTypes = function() { + return Object.keys(missingTypes); + }; + + Link.prototype._makeSymbolLink = function(longname) { + + // normalize .prototype. and # + longname = longname.replace(/\.prototype\./g, '#'); + + // if it is an internal reference, then don't validate against symbols, just create a link + if ( longname.charAt(0) == "#" ) { + + return _makeLink(longname + (this.innerName ? "#" + this.innerName : ""), this.targetName, this.tooltip, this.text || longname.slice(1)); + + } + + var linkTo = lookup(longname); + // if there is no symbol by that name just return the name unaltered + if ( !linkTo ) { + + missingTypes[longname] = true; + + return this.text || longname; + + } + + // it's a full symbol reference (potentially to another file) + var mainSymbol, anchor; + if ( (linkTo.kind === 'member' && !linkTo.isEnum) || linkTo.kind === 'constant' || linkTo.kind === 'function' || linkTo.kind === 'event' ) { // it's a method or property + + mainSymbol = linkTo.memberof; + anchor = ( linkTo.kind === 'event' ? "event:" : "") + Link.symbolNameToLinkName(linkTo); + + } else { + + mainSymbol = linkTo.longname; + anchor = this.innerName; + + } + + return _makeLink(Link.baseSymbols + __uniqueFilenames[mainSymbol] + conf.ext + (anchor ? "#" + anchor : ""), this.targetName, this.tooltip, this.text || longname); + }; + + Link.symbolNameToLinkName = function(symbol) { + var linker = ""; + if ( symbol.scope === 'static' ) { + linker = "."; + } else if (symbol.isInner) { + linker = "-"; // TODO-migrate? + } + return linker + symbol.name; + }; + + return Link; + +}()); + + + +// ---- publish() - main entry point for JSDoc templates ------------------------------------------------------------------------------------------------------- + +/* Called automatically by JsDoc Toolkit. */ +function publish(symbolSet) { + + info("entering sapui5 template"); + + // create output dir + fs.mkPath(env.opts.destination); + +// if ( symbolSet().count() < 20000 ) { +// info("writing raw symbols to " + path.join(env.opts.destination, "symbols-unpruned-ui5.json")); +// fs.writeFileSync(path.join(env.opts.destination, "symbols-unpruned-ui5.json"), JSON.stringify(symbolSet().get(), filter, "\t"), 'utf8'); +// } + + info("before prune: " + symbolSet().count() + " symbols."); + symbolSet = helper.prune(symbolSet); + info("after prune: " + symbolSet().count() + " symbols."); + + __db = symbolSet; + __longnames = {}; + __db().each(function($) { + __longnames[$.longname] = $; + }); + + if ( templateConf.apiJsonFolder ) { + info("loading external apis from folder '" + templateConf.apiJsonFolder + "'"); + loadExternalSymbols(templateConf.apiJsonFolder); + } + + var templatePath = path.join(env.opts.template, 'tmpl/'); + info("using templates from '" + templatePath + "'"); + view = new template.Template(templatePath); + + function filter(key,value) { + if ( key === 'meta' ) { + //return; + } + if ( key === '__ui5' && value ) { + var v = { + resource: value.resource, + module: value.module, + stakeholders: value.stakeholders + }; + if ( value.derived ) { + v.derived = value.derived.map(function($) { return $.longname; }); + } + if ( value.base ) { + v.base = value.base.longname; + } + if ( value.implementations ) { + v.base = value.implementations.map(function($) { return $.longname; }); + } + if ( value.parent ) { + v.parent = value.parent.longname; + } + if ( value.children ) { + v.children = value.children.map(function($) { return $.longname; }); + } + return v; + } + return value; + } + + // now resolve relationships + var aRootNamespaces = createNamespaceTree(); + var hierarchyRoots = createInheritanceTree(); + collectMembers(); + mergeEventDocumentation(); + + if ( symbolSet().count() < 20000 ) { + info("writing raw symbols to " + path.join(env.opts.destination, "symbols-pruned-ui5.json")); + fs.writeFileSync(path.join(env.opts.destination, "symbols-pruned-ui5.json"), JSON.stringify(symbolSet().get(), filter, "\t"), 'utf8'); + } + + // used to allow Link to check the details of things being linked to + Link.symbolSet = symbolSet; + + // get an array version of the symbol set, useful for filtering + var symbols = symbolSet().get(); + + // ----- + + var PUBLISHING_VARIANTS = { + + "apixml" : { + defaults : { + apiXmlFile: path.join(env.opts.destination, "jsapi.xml") + }, + processor : function(conf) { + createAPIXML(symbols, conf.apiXmlFile, { + legacyContent: true + }); + } + }, + + "apijson" : { + defaults : { + apiJsonFile: path.join(env.opts.destination, "api.json") + }, + processor : function(conf) { + createAPIJSON(symbols, conf.apiJsonFile); + } + }, + + "fullapixml" : { + defaults : { + fullXmlFile: path.join(env.opts.destination, "fulljsapi.xml") + }, + processor : function(conf) { + createAPIXML(symbols, conf.fullXmlFile, { + roots: aRootNamespaces, + omitDefaults : conf.omitDefaultsInFullXml, + resolveInheritance: true + }); + } + }, + + "apijs" : { + defaults: { + jsapiFile: path.join(env.opts.destination, "api.js") + }, + processor: function(conf) { + createAPIJS(symbols, conf.jsapiFile); + } + }, + + "full" : { + defaults : { + outdir: path.join(env.opts.destination, "full/"), + contentOnly: false, + hierarchyIndex: true + }, + processor: function() { + publishClasses(symbolSet, aRootNamespaces, hierarchyRoots); + } + }, + + "public" : { + defaults: { + outdir: path.join(env.opts.destination, "public/"), + filter: function($) { return $.access === 'public' || $.access === 'protected' || $.access == null; }, + contentOnly: false, + hierarchyIndex: true + }, + processor: function(conf) { + publishClasses(symbolSet, aRootNamespaces, hierarchyRoots); + } + }, + + "demokit" : { + defaults: { + outdir: path.join(env.opts.destination, "demokit/"), + filter: function($) { return $.access === 'public' || $.access === 'protected' || $.access == null; }, + contentOnly: true, + modulePages: true, + hierarchyIndex: false, + securityIndex: true, + sinceIndex: true, + deprecationIndex: true, + experimentalIndex: true, + suppressAuthor: true, + suppressVersion: true + }, + processor: function(conf) { + publishClasses(symbolSet, aRootNamespaces, hierarchyRoots); + } + }, + + "demokit-internal" : { + defaults: { + outdir: path.join(env.opts.destination, "demokit-internal/"), + // filter: function($) { return $.access === 'public' || $.access === 'protected' || $.access === 'restricted' || $.access == null; }, + contentOnly: true, + modulePages: true, + hierarchyIndex: false, + securityIndex: true, + sinceIndex: true, + deprecationIndex: true, + experimentalIndex: true, + suppressAuthor: true, + suppressVersion: true + }, + processor: function(conf) { + publishClasses(symbolSet, aRootNamespaces, hierarchyRoots); + } + } + + }; + + var now = new Date(); + + info("start publishing"); + for (var i = 0; i < templateConf.variants.length; i++) { + + var vVariant = templateConf.variants[i]; + if ( typeof vVariant === "string" ) { + vVariant = { variant : vVariant }; + } + + info(""); + + if ( PUBLISHING_VARIANTS[vVariant.variant] ) { + + // Merge different sources of configuration (listed in increasing priority order - last one wins) + // and expose the result in the global 'conf' variable + // - global defaults + // - defaults for current variant + // - user configuration for sapui5 template + // - user configuration for current variant + // + // Note: trailing slash expected for dirs + conf = merge({ + ext: ".html", + filter: function($) { return true; }, + templatesDir: "/templates/sapui5/", + symbolsDir: "symbols/", + modulesDir: "modules/", + topicUrlPattern: "../../guide/{{topic}}.html", + srcDir: "symbols/src/", + creationDate : now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDay() + " " + now.getHours() + ":" + now.getMinutes(), + outdir: env.opts.destination + }, PUBLISHING_VARIANTS[vVariant.variant].defaults, templateConf, vVariant); + + info("publishing as variant '" + vVariant.variant + "'"); + debug("final configuration:"); + debug(conf); + + PUBLISHING_VARIANTS[vVariant.variant].processor(conf); + + info("done with variant " + vVariant.variant); + + } else { + + info("cannot publish unknown variant '" + vVariant.variant + "' (ignored)"); + + } + } + + var builtinSymbols = templateConf.builtinSymbols; + if ( builtinSymbols ) { + Link.getMissingTypes().filter(function($) { + return builtinSymbols.indexOf($) < 0; + }).sort().forEach(function($) { + // TODO instead of filtering topic: and fiori: links out here, they should be correctly linked in the template + if ( !/\{@link (?:topic:|fiori:)/.test($) ) { + error(" unresolved reference: " + $); + } + }); + } + info("publishing done."); + +} + +//---- namespace tree -------------------------------------------------------------------------------- + +/* + * Completes the tree of namespaces. Namespaces for which content is available + * but which have not been documented are created as dummy without documentation. + */ +function createNamespaceTree() { + + info("create namespace tree (" + __db().count() + " symbols)"); + + var aRootNamespaces = []; + var aTypes = __db(function() { return isFirstClassSymbol(this); }).get(); + + for (var i = 0; i < aTypes.length; i++) { // loop with a for-loop as it can handle concurrent modifications + + var symbol = aTypes[i]; + if ( symbol.memberof ) { + + var parent = lookup(symbol.memberof); + if ( !parent ) { + warning("create missing namespace '" + symbol.memberof + "' (referenced by " + symbol.longname + ")"); + parent = makeNamespace(symbol.memberof); + __longnames[symbol.memberof] = parent; + __db.insert(parent); + aTypes.push(parent); // concurrent modification: parent will be processed later in this loop + } + symbol.__ui5.parent = parent; + parent.__ui5.children = parent.__ui5.children || []; + parent.__ui5.children.push(symbol); + + } else if ( symbol.longname !== ANONYMOUS_LONGNAME ) { + + aRootNamespaces.push(symbol); + + } + } + + return aRootNamespaces; +} + +function makeNamespace(memberof) { + + info("adding synthetic namespace symbol " + memberof); + + var comment = [ + "@name " + memberof, + "@namespace", + "@synthetic", + "@public" + ]; + + var symbol = new doclet.Doclet("/**\n * " + comment.join("\n * ") + "\n */", {}); + symbol.__ui5 = {}; + + return symbol; +} + +//---- inheritance hierarchy ---------------------------------------------------------------------------- + +/* + * Calculates the inheritance hierarchy for all class/interface/namespace symbols. + * Each node in the tree has the content + * + * Node : { + * longname : {string} // name of the node (usually equals symbol.longname) + * symbol : {Symbol} // backlink to the original symbol + * base : {Node} // parent node or undefined for root nodes + * derived : {Node[]} // subclasses/-types + * } + * + */ +function createInheritanceTree() { + + function makeDoclet(longname, lines) { + lines.push("@name " + longname); + var newDoclet = new doclet.Doclet("/**\n * " + lines.join("\n * ") + "\n */", {}); + newDoclet.__ui5 = {}; + __longnames[longname] = newDoclet; + __db.insert(newDoclet); + return newDoclet; + } + + info("create inheritance tree (" + __db().count() + " symbols)"); + + var oTypes = __db(function() { return supportsInheritance(this); }); + var aRootTypes = []; + + var oObject = lookup("Object"); + if ( !oObject ) { + oObject = makeDoclet("Object", [ + "@class", + "@synthetic", + "@public" + ]); + aRootTypes.push(oObject); + } + + function getOrCreateClass(sClass, sExtendingClass) { + var oClass = lookup(sClass); + if ( !oClass ) { + warning("create missing class " + sClass + " (extended by " + sExtendingClass + ")"); + var sBaseClass = 'Object'; + if ( externalSymbols[sClass] ) { + sBaseClass = externalSymbols[sClass].extends || sBaseClass; + } + var oBaseClass = getOrCreateClass(sBaseClass, sClass); + oClass = makeDoclet(sClass, [ + "@extends " + sBaseClass, + "@class", + "@synthetic", + "@public" + ]); + oClass.__ui5.base = oBaseClass; + oBaseClass.__ui5.derived = oBaseClass.__ui5.derived || []; + oBaseClass.__ui5.derived.push(oClass); + } + return oClass; + } + + // link them according to the inheritance infos + oTypes.each(function(oClass) { + + if ( oClass.longname === 'Object') { + return; + } + + var sBaseClass = "Object"; + if ( oClass.augments && oClass.augments.length > 0 ) { + if ( oClass.augments.length > 1 ) { + warning("multiple inheritance detected in " + oClass.longname); + } + sBaseClass = oClass.augments[0]; + } else { + aRootTypes.push(oClass); + } + + var oBaseClass = getOrCreateClass(sBaseClass, oClass.longname); + oClass.__ui5.base = oBaseClass; + oBaseClass.__ui5.derived = oBaseClass.__ui5.derived || []; + oBaseClass.__ui5.derived.push(oClass); + + if ( oClass.implements ) { + for (var j = 0; j < oClass.implements.length; j++) { + var oInterface = lookup(oClass.implements[j]); + if ( !oInterface ) { + warning("create missing interface " + oClass.implements[j]); + oInterface = makeDoclet(oClass.implements[j], [ + "@extends Object", + "@interface", + "@synthetic", + "@public" + ]); + oInterface.__ui5.base = oObject; + oObject.__ui5.derived = oObject.__ui5.derived || []; + oObject.__ui5.derived.push(oInterface); + } + oInterface.__ui5.implementations = oInterface.__ui5.implementations || []; + oInterface.__ui5.implementations.push(oClass); + } + } + }); + + function setStereotype(oSymbol, sStereotype) { + if ( !oSymbol ) { + return; + } + oSymbol.__ui5.stereotype = sStereotype; + var derived = oSymbol.__ui5.derived; + if ( derived ) { + for (var i = 0; i < derived.length; i++ ) { + if ( !derived[i].__ui5.stereotype ) { + setStereotype(derived[i], sStereotype); + } + } + } + } + + setStereotype(lookup("sap.ui.core.Component"), "component"); + setStereotype(lookup("sap.ui.core.Control"), "control"); + setStereotype(lookup("sap.ui.core.Element"), "element"); + setStereotype(lookup("sap.ui.base.Object"), "object"); + + // check for cyclic inheritance (not supported) + // Note: the check needs to run bottom up, not top down as a typical cyclic dependency never will end at the root node + oTypes.each(function(oStartClass) { + var visited = {}; + function visit(oClass) { + if ( visited[oClass.longname] ) { + throw new Error("cyclic inheritance detected: " + JSON.stringify(Object.keys(visited))); + } + if ( oClass.__ui5.base ) { + visited[oClass.longname] = true; + visit(oClass.__ui5.base); + delete visited[oClass.longname]; + } + } + visit(oStartClass); + }); + + // collect root nodes (and ignore pure packages) + return aRootTypes; + /* + return __db(function() { + return R_KINDS.test(this.kind) && this.__ui5 && this.__ui5.base == null; + }).get(); + */ +} + +function collectMembers() { + __db().each(function($) { + if ( $.memberof ) { + var parent = lookup($.memberof); + if ( parent /* && supportsInheritance(parent) */ ) { + parent.__ui5.members = parent.__ui5.members || []; + parent.__ui5.members.push($); + } + } + }); +} + +function mergeEventDocumentation() { + + debug("merging JSDoc event documentation into UI5 metadata"); + + var oTypes = __db(function() { return isaClass(this); }); + + oTypes.each(function(symbol) { + + var metadata = symbol.__ui5.metadata; + var members = symbol.__ui5.members; + + if ( !metadata || !metadata.events || Object.keys(metadata.events).length <= 0 || !members ) { + return; + } + + // debug('merging events for ' + symbol.longname); + members.forEach(function($) { + if ( $.kind === 'event' && !$.inherited + && ($.access === 'public' || $.access === 'protected' || $.access == null) + && metadata.events[$.name] + && Array.isArray($.params) + && !$.synthetic ) { + + var event = metadata.events[$.name]; + var modified = false; + + $.params.forEach(function(param) { + var m = /^\w+\.getParameters\.(.*)$/.exec(param.name); + if ( m ) { + var pname = m[1]; + var ui5param = event.parameters[pname] || ( event.parameters[pname] = {}); + if ( ui5param.type == null ) { + ui5param.type = listTypes(param.type); + modified = true; + } + if ( ui5param.doc == null ) { + ui5param.doc = param.description; + modified = true; + } + } + }); + + if ( modified ) { + info(" merged documentation for managed event " + symbol.longname + "#" + $.name); + } + + } + }); + + }); + +} + +// ---- publishing ----------------------------------------------------------------------- + +function publishClasses(symbols, aRootNamespaces, hierarchyRoots) { + + // create output dir + fs.mkPath(path.join(conf.outdir, conf.symbolsDir)); + + // get a list of all the first class symbols in the symbolset + var firstClassSymbols = symbols(function() { + return supportsInheritance(this) && conf.filter(this); + }).order("longname"); + + // create unique file names + __uniqueFilenames = {}; + var filenames = {}; + firstClassSymbols.get().sort(sortByAlias).forEach(function(symbol) { + var filename = escape(symbol.longname.replace(/^module:/, "")).replace(/\//g, "%25"); + if ( filenames.hasOwnProperty(filename.toUpperCase()) && (filenames[filename.toUpperCase()].longname !== symbol.longname) ) { + // find an unused filename by appending "-n" where n is an integer > 0 + var j = 1; + while (filenames.hasOwnProperty(filename.toUpperCase() + "-" + j)) { + j++; + } + warning("duplicate symbol names " + filenames[filename.toUpperCase()].longname + " and " + symbol.longname + ", renaming the latter to " + filename + "-" + j); + filename = filename + "-" + j; + } + filenames[filename.toUpperCase()] = symbol; + __uniqueFilenames[symbol.longname] = filename; + }); + filenames = null; + + // create a class index, displayed in the left-hand column of every class page + var classTemplate; + if ( !conf.contentOnly ) { + info("create embedded class index"); + Link.base = "../"; + Link.baseSymbols = ""; + classTemplate = 'classWithIndex.html.tmpl'; + publish.header = processTemplate("_header.tmpl", firstClassSymbols); + publish.footer = processTemplate("_footer.tmpl", firstClassSymbols); + publish.classesIndex = processTemplate("_navIndex.tmpl", firstClassSymbols); // kept in memory + } else { + var newStyle = !!pluginConf.newStyle; + classTemplate = newStyle ? "class-new.html.tmpl" : "class.html.tmpl"; + publish.header = ''; + publish.footer = ''; + publish.classesIndex = ''; + + // instead create an index as XML + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + processTemplateAndSave("index.xml.tmpl", aRootNamespaces, "index.xml"); + } + + // create each of the class pages + info("create class/namespace pages"); + Link.base = "../"; + Link.baseSymbols = ""; + firstClassSymbols.each(function(symbol) { + var sOutName = path.join(conf.symbolsDir, __uniqueFilenames[symbol.longname]) + conf.ext; + processTemplateAndSave(classTemplate, symbol, sOutName); + }); + + if ( conf.modulePages ) { + info("create module pages"); + Link.base = "../"; + Link.baseSymbols = "../" + conf.symbolsDir; + fs.mkPath(path.join(conf.outdir, conf.modulesDir)); + groupByModule(firstClassSymbols.get()).forEach(function(module) { + var sOutName = path.join(conf.modulesDir, module.name.replace(/\//g, '_')) + conf.ext; + processTemplateAndSave("module.html.tmpl", module, sOutName); + }); + } + + // regenerate the index with a different link base, used in the overview pages + info("create global class/namespace index"); + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + publish.header = processTemplate("_header.tmpl", firstClassSymbols); + publish.footer = processTemplate("_footer.tmpl", firstClassSymbols); + publish.classesIndex = processTemplate("_navIndex.tmpl", firstClassSymbols); + + // create the all classes index + processTemplateAndSave("index.html.tmpl", firstClassSymbols, "index" + conf.ext); + + // create the class hierarchy page + if ( conf.hierarchyIndex ) { + info("create class hierarchy index"); + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + processTemplateAndSave("hierarchy.html.tmpl", hierarchyRoots.filter(conf.filter), "hierarchy" + conf.ext); + } + + if ( conf.sinceIndex ) { + info("create API by version index"); + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + var sinceSymbols = symbols(function() { + var r = !!this.since && !this.inherited && conf.filter(this); + if ( r && this.memberof ) { + var parent = lookup(this.memberof); + // filter out symbol when parent is filtered out + if ( !parent || !conf.filter(parent) ) { + debug("since index: filtering out " + this.longname + ", member of " + this.memberof); + r = false; + } + if ( parent && parent.since === this.since ) { + // r = false; + } + } + return r; + }).order("longname"); + processTemplateAndSave("since.html.tmpl", sinceSymbols, "since" + conf.ext); + } + + if ( conf.deprecationIndex ) { + info("create deprecated API index"); + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + var deprecatedSymbols = symbols(function() { + return !!this.deprecated && !this.inherited && conf.filter(this); + }).order("longname"); + processTemplateAndSave("deprecation.html.tmpl", deprecatedSymbols, "deprecation" + conf.ext); + } + + if ( conf.experimentalIndex ) { + info("create experimental API index"); + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + var experimentalSymbols = symbols(function() { + return !!this.experimental && !this.inherited && conf.filter(this); + }).order("longname"); + processTemplateAndSave("experimental.html.tmpl", experimentalSymbols, "experimental" + conf.ext); + } + + if ( conf.securityIndex ) { + info("create Security Relevant API index"); + + var securityRelevantSymbols = {}; + A_SECURITY_TAGS.forEach(function(oTagDef) { + securityRelevantSymbols[oTagDef.name.toLowerCase()] = { tag : oTagDef, symbols: [] }; + }); + symbols().each(function($) { + var tags = $.tags; + if ( !$.inherited && conf.filter($) && tags ) { + for (var i = 0; i < tags.length; i++) { + if ( rSecurityTags.test(tags[i].title) ) { + securityRelevantSymbols[tags[i].title.toLowerCase()].symbols.push({ symbol: $, tag : tags[i]}); + } + } + } + }); + + Link.base = ""; + Link.baseSymbols = conf.symbolsDir; + processTemplateAndSave("security.html.tmpl", securityRelevantSymbols, "security" + conf.ext); + } + + firstClassSymbols = null; + + // copy needed mimes + info("copy mimes"); + // copy the template's static files to outdir + var templatePath = env.opts.template; + var fromDir = path.join(templatePath, 'static'); + var staticFiles = fs.ls(fromDir, 3); + staticFiles.forEach(function(fileName) { + var toDir = fs.toDir( fileName.replace(fromDir, conf.outdir) ); + fs.mkPath(toDir); + fs.copyFileSync(fileName, toDir); + }); + + __uniqueFilenames = null; + + info("publishing done."); +} + +// ---- helper functions for the templates ---- + +var rSinceVersion = /^([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?([-.][0-9A-Z]+)?)(?:\s|$)/i; + +function extractVersion(value) { + + if ( !value ) { + return undefined; + } + + if ( value === true ) { + value = ''; + } else { + value = String(value); + } + + var m = rSinceVersion.exec(value); + return m ? m[1] : undefined; + +} + +var rSince = /^(?:as\s+of|since)(?:\s+version)?\s*([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?([-.][0-9A-Z]+)?)(?:\.$|\.\s+|[,:]\s*|\s-\s*|\s|$)/i; + +function extractSince(value) { + + if ( !value ) { + return undefined; + } + + if ( value === true ) { + value = ''; + } else { + value = String(value); + } + + var m = rSince.exec(value); + if ( m ) { + return { + since : m[1], + pos : m[0].length, + value : value.slice(m[0].length).trim() + }; + } + + return { + pos : 0, + value: value.trim() + }; + +} + +function sortByAlias(a, b) { + var partsA = a.longname.split(/[.#]/); + var partsB = b.longname.split(/[.#]/); + var i = 0; + while ( i < partsA.length && i < partsB.length ) { + if ( partsA[i].toLowerCase() < partsB[i].toLowerCase() ) { + return -1; + } + if ( partsA[i].toLowerCase() > partsB[i].toLowerCase() ) { + return 1; + } + i++; + } + if ( partsA.length < partsB.length ) { + return -1; + } + if ( partsA.length > partsB.length ) { + return 1; + } + // as a last resort, try to compare the aliases case sensitive in case we have aliases that only + // differ in case like with "sap.ui.model.type" and "sap.ui.model.Type" + if ( a.longname < b.longname ) { + return -1; + } + if ( a.longname > b.longname ) { + return 1; + } + return 0; +} + +/* +function isNonEmptyNamespace($) { + return $.isNamespace && ( + ($.properties && $.properties.length > 0) || + ($.methods && $.methods.length > 0) || + ($.augments && $.augments.length > 0) || + ($.children && $.children.length > 0)); +};*/ + +/* Just the first sentence (up to a full stop). Should not break on dotted variable names. */ +function summarize(desc) { + if ( desc != null ) { + desc = String(desc).replace(/\s+/g, ' '). + replace(/"'/g, '"'). + replace(/^(<\/?p>||\s)+/, ''); + + var match = /([\w\W]+?\.)[^a-z0-9_$]/i.exec(desc); + return match ? match[1] : desc; + } +} + +/* Make a symbol sorter by some attribute. */ +function makeSortby(/* fields ...*/) { + var aFields = Array.prototype.slice.apply(arguments), + aNorms = [], + aFuncs = []; + for (var i = 0; i < arguments.length; i++) { + aNorms[i] = 1; + if ( typeof aFields[i] === 'function' ) { + aFuncs[i] = aFields[i]; + continue; + } + aFuncs[i] = function($,n) { return $[n]; }; + if ( aFields[i].indexOf("!") === 0 ) { + aNorms[i] = -1; + aFields[i] = aFields[i].slice(1); + } + if ( aFields[i] === 'deprecated' ) { + aFuncs[i] = function($,n) { return !!$[n]; }; + } else if ( aFields[i] === 'static' ) { + aFields[i] = 'scope'; + aFuncs[i] = function($,n) { return $[n] === 'static'; }; + } else if ( aFields[i].indexOf("#") === 0 ) { + aFields[i] = aFields[i].slice(1); + aFuncs[i] = function($,n) { return $.comment.getTag(n).length > 0; }; + } + } + return function(a, b) { + // info("compare " + a.longname + " : " + b.longname); + var r = 0,i,va,vb; + for (i = 0; r === 0 && i < aFields.length; i++) { + va = aFuncs[i](a,aFields[i]); + vb = aFuncs[i](b,aFields[i]); + if ( va && !vb ) { + r = -aNorms[i]; + } else if ( !va && vb ) { + r = aNorms[i]; + } else if ( va && vb ) { + va = String(va).toLowerCase(); + vb = String(vb).toLowerCase(); + if (va < vb) { + r = -aNorms[i]; + } + if (va > vb) { + r = aNorms[i]; + } + } + // debug(" " + aFields[i] + ": " + va + " ? " + vb + " = " + r); + } + return r; + }; +} + +/** Pull in the contents of an external file at the given path. */ + +function processTemplateAndSave(sTemplateName, oData, sOutputName) { + var sResult = processTemplate(sTemplateName, oData); + if ( conf.normalizeWhitespace && /\.html$/.test(sOutputName) ) { + sResult = normalizeWhitespace(sResult); + } + var sOutpath = path.join(conf.outdir, sOutputName); + try { + fs.mkPath( path.dirname(sOutpath) ); + fs.writeFileSync(sOutpath, sResult, 'utf8'); + } catch (e) { + error("failed to write generated file '" + sOutpath + "':" + (e.message || String(e))); + } +} + +function processTemplate(sTemplateName, data) { + debug("processing template '" + sTemplateName + "' for " + data.longname); + + var result; + + try { + result = view.render(sTemplateName, { + asPlainSummary: asPlainSummary, + bySimpleName: bySimpleName, + childrenOfKind: childrenOfKind, + conf: conf, + data: data, + getConstructorDescription : getConstructorDescription, + getNSClass: getNSClass, + groupByVersion: groupByVersion, + extractSince: extractSince, + include: processTemplate, + Link: Link, + listTypes: listTypes, + linkTypes: linkTypes, + makeExample: makeExample, + makeLinkList: makeLinkList, + makeLinkToSymbolFile: makeLinkToSymbolFile, + makeSignature: makeSignature, + makeSortby: makeSortby, + publish : publish, + formatText: formatText, + simpleNameOf: simpleNameOf, + sortByAlias: sortByAlias, + summarize: summarize, + Version : Version + }); + } catch (e) { + if ( e.source ) { + var filename = path.join(env.opts.destination, sTemplateName + ".js"); + error("**** failed to process template, source written to " + filename); + fs.mkPath(path.dirname(filename)); + fs.writeFileSync(filename, e.source, 'utf8'); + } + error("error while processing " + sTemplateName); + throw e; + } + debug("processing template done."); + return result; +} + +function groupByVersion(symbols, extractVersion) { + + var map = {}; + + symbols.forEach(function(symbol) { + + var version = extractVersion(symbol), + key = String(version); + + if ( !map[key] ) { + map[key] = { version: version, symbols : [] }; + } + map[key].symbols.push(symbol); + + }); + + var groups = Object.keys(map).map(function(key) { return map[key]; }); + + return groups.sort(function(a,b) { + if ( !a.version && b.version ) { + return -1; + } else if ( a.version && !b.version ) { + return 1; + } else if ( a.version && b.version ) { + return -a.version.compareTo(b.version); + } + return 0; + }); +} + +function groupByModule(symbols) { + + var map = {}; + + function add(key, symbol) { + if ( !map[key] ) { + map[key] = { name: key, symbols : [] }; + } + if ( map[key].symbols.indexOf(symbol) < 0 ) { + map[key].symbols.push(symbol); + } + } + + symbols.forEach(function(symbol) { + + var key = symbol.__ui5.module; + + if ( key ) { + add(key, symbol); + if ( symbol.__ui5.members ) { + symbol.__ui5.members.forEach(function($) { + if ( !$.inherited && $.__ui5.module && $.__ui5.module !== key && conf.filter($) ) { + add($.__ui5.module, $); + } + }); + } + } + + }); + + var groups = Object.keys(map).map(function(key) { return map[key]; }); + + return groups; +} + + +var REGEXP_TAG = /<(\/?(?:[A-Z][A-Z0-9_-]*:)?[A-Z][A-Z0-9_-]*)(?:\s[^>]*)?>/gi; + +/** + * Removes unnecessary whitespace from an HTML document: + * - if the text between two adjacent HTML tags consists of whitespace only, the whole text is removed + * - otherwise, any sequence of whitespace in the text is reduced to a single blank + * - inside a
     tag, whitespace is preserved
    + *
    + * Whitespace inside an element tag is not touched (although it could be normalized as well)
    + * @param {string} content raw HTML file
    + * @returns {string} HTML file with normalized whitespace
    + */
    +function normalizeWhitespace(content) {
    +	var compressed = '',
    +		preformatted = 0,
    +		p = 0, m, text;
    +
    +	REGEXP_TAG.lastIndex = 0;
    +	while ( (m = REGEXP_TAG.exec(content)) ) {
    +		if ( m.index > p ) {
    +			text = content.slice(p, m.index);
    +			if ( preformatted ) {
    +				compressed += text;
    +				// debug('  "' + text + '" (preformatted)');
    +			} else {
    +				text = text.replace(/\s+/g,' ');
    +				if ( text.trim() ) {
    +					compressed += text;
    +				}
    +				// debug('  "' + text + '" (trimmed)');
    +			}
    +		}
    +
    +		compressed += m[0];
    +		// debug('  "' + m[0] + '" (tag)');
    +		p = m.index + m[0].length;
    +
    +		if ( /^pre$/i.test(m[1]) ) {
    +			preformatted++;
    +		} else if ( /^\/pre$/i.test(m[1]) && preformatted ) {
    +			preformatted--;
    +		}
    +
    +	}
    +
    +	if ( content.length > p ) {
    +		text = content.slice(p, content.length);
    +		if ( preformatted ) {
    +			compressed += text;
    +			// debug('  "' + text + '" (preformatted)');
    +		} else {
    +			text = text.replace(/\s+/g,' ');
    +			if ( text.trim() ) {
    +				compressed += text;
    +			}
    +			// debug('  "' + text + '" (trimmed)');
    +		}
    +	}
    +
    +	return compressed;
    +}
    +
    +function makeLinkToSymbolFile(longname) {
    +	return Link.baseSymbols + __uniqueFilenames[longname] + conf.ext;
    +}
    +
    +function simpleNameOf(longname) {
    +	longname = String(longname);
    +	var p = longname.lastIndexOf('.');
    +	return p < 0 ? longname : longname.slice(p + 1);
    +}
    +
    +function bySimpleName(a,b) {
    +	if ( a === b ) {
    +		return 0;
    +	}
    +	var simpleA = simpleNameOf(a);
    +	var simpleB = simpleNameOf(b);
    +	if ( simpleA === simpleB ) {
    +		return a < b ? -1 : 1;
    +	} else {
    +		return simpleA < simpleB ? -1 : 1;
    +	}
    +}
    +
    +/* Build output for displaying function parameters. */
    +function makeSignature(params) {
    +	var r = ['('], desc;
    +	if ( params ) {
    +		for (var i = 0, p; (p = params[i]); i++) {
    +			// ignore @param tags for 'virtual' params that are used to document members of config-like params
    +			// (e.g. like "@param param1.key ...")
    +			if (p.name && p.name.indexOf('.') == -1) {
    +				if (i > 0) {
    +					r.push(', ');
    +				}
    +
    +				r.push('');
    +				r.push(p.name);
    +				r.push('');
    +				if ( p.optional ) {
    +					r.push('?');
    +				}
    +			}
    +		}
    +	}
    +	r.push(')');
    +	return r.join('');
    +}
    +
    +
    +/*
    + * regexp to recognize important places in the text
    + *
    + * Capturing groups of the RegExp:
    + *   group 1: begin of a pre block
    + *   group 2: end of a pre block
    + *   group 3: begin of a header/ul/ol/table, implicitly ends a paragraph
    + *   group 4: end of a header/ul/ol/table, implicitly starts a new paragraph
    + *   group 5: target portion of an inline @link tag
    + *   group 6: (optional) text portion of an inline link tag
    + *   group 7: an empty line which implicitly starts a new paragraph
    + *
    + *                 [------- 
     block -------] [----------------------- some flow content -----------------------] [---- an inline {@link ...} tag ----] [---------- an empty line ---------]  */
    +var rFormatText = /(]*)?>)|(<\/pre>)|(<(?:h[\d+]|ul|ol|table)(?:\s[^>]*)?>)|(<\/(?:h[\d+]|ul|ol|table)>)|\{@link\s+([^}\s]+)(?:\s+([^\}]*))?\}|((?:\r\n|\r|\n)[ \t]*(?:\r\n|\r|\n))/gi;
    +
    +function formatText(text) {
    +
    +	if ( !text ) {
    +		return '';
    +	}
    +
    +	var inpre = false,
    +		paragraphs = 0;
    +
    +	text = String(text).replace(rFormatText, function(match, pre, endpre, flow, endflow, linkTarget, linkText, emptyline) {
    +		if ( pre ) {
    +			inpre = true;
    +			return pre.replace(/
    /gi, "
    ").replace(//gi, "
    ");
    +		} else if ( endpre ) {
    +			inpre = false;
    +		} else if ( flow ) {
    +			if ( !inpre ) {
    +				paragraphs++;
    +				return '

    ' + match; + } + } else if ( endflow ) { + if ( !inpre ) { + paragraphs++; + return match + '

    '; + } + } else if ( emptyline ) { + if ( !inpre ) { + paragraphs++; + return '

    '; + } + } else if ( linkTarget ) { + if ( !inpre ) { + // convert to a hyperlink + var link = new Link().toSymbol(linkTarget); + // if link tag contained a replacement text, use it + if ( linkText && linkText.trim()) { + link = link.withText(linkText.trim()); + } + return link.toString(); + } + } + return match; + }); + + if ( paragraphs > 0 ) { + text = '

    ' + text + '

    '; + } + + // remove empty paragraphs + text = text.replace(/

    \s*<\/p>/g, ''); + + return text; +} + + +function childrenOfKind(data, kind) { + /* old version based on TaffyDB (slow) + var oChildren = symbolSet({kind: kind, memberof: data.longname === GLOBAL_LONGNAME ? {isUndefined: true} : data.longname}).filter(function() { return conf.filter(this); }); + return { + own : oChildren.filter({inherited: {isUndefined:true}}).get().sort(makeSortby("!deprecated","static","name")), + borrowed : groupByContributors(data, oChildren.filter({inherited: true}).get().sort(makeSortby("name"))) + } */ + var oResult = { + own: [], + borrowed: [] + }; + //debug("calculating kind " + kind + " from " + data.longname); + //console.log(data); + var fnFilter; + switch (kind) { + case 'property': + fnFilter = function($) { + return $.kind === 'constant' || ($.kind === 'member' && !$.isEnum); + }; + break; + case 'event': + fnFilter = function($) { + return $.kind === 'event'; + }; + break; + case 'method': + fnFilter = function($) { + return $.kind === 'function'; + }; + break; + default: + // default: none + fnFilter = function($) { return false; }; + break; + } + + if ( data.__ui5.members ) { + data.__ui5.members.forEach(function($) { + if ( fnFilter($) && conf.filter($) ) { + oResult[$.inherited ? 'borrowed' : 'own'].push($); + } + }); + } + oResult.own.sort(makeSortby("!deprecated","static","name")); + oResult.borrowed = groupByContributors(data, oResult.borrowed); + + return oResult; +} + +/** + * Determines the set of contributors of the given borrowed members. + * The contributors are sorted according to the inheritance hierarchy: + * first the base class of symbol, then the base class of the base class etc. + * Any contributors that can not be found in the hierarchy are appended + * to the set. + * + * @param {Symbol} symbol of which these are the members + * @param {array} aBorrowedMembers set of borrowed members to determine the contributors for + * @return {array} sorted array of contributors + */ +function groupByContributors(symbol, aBorrowedMembers) { + + var MAX_ORDER = 1000, // a sufficiently large number + mContributors = {}, + aSortedContributors = [], + i,order; + + aBorrowedMembers.forEach(function($) { + $ = lookup($.inherits); + if ($ && mContributors[$.memberof] == null) { + mContributors[$.memberof] = { order : MAX_ORDER, items : [$] }; + } else { + mContributors[$.memberof].items.push($); + } + }); + + // order contributors according to their distance in the inheritance hierarchy + order = 0; + (function handleAugments(oSymbol) { + var i,oTarget,aParentsToVisit; + if ( oSymbol.augments ) { + aParentsToVisit = []; + // first assign an order + for (i = 0; i < oSymbol.augments.length; i++) { + if ( mContributors[oSymbol.augments[i]] != null && mContributors[oSymbol.augments[i]].order === MAX_ORDER ) { + mContributors[oSymbol.augments[i]].order = ++order; + aParentsToVisit.push(oSymbol.augments[i]); + } + } + // only then dive into parents (breadth first search) + for (i = 0; i < aParentsToVisit.length; i++) { + oTarget = lookup(aParentsToVisit); + if ( oTarget ) { + handleAugments(oTarget); + } + } + } + }(symbol)); + + // convert to an array and sort by order + for (i in mContributors) { + aSortedContributors.push(mContributors[i]); + } + aSortedContributors.sort(function (a,b) { return a.order - b.order; }); + + return aSortedContributors; + +} + +function makeLinkList(aSymbols) { + return aSymbols + .sort(makeSortby("name")) + .map(function($) { return new Link().toSymbol($.longname).withText($.name); }) + .join(", "); +} + +// ---- type parsing --------------------------------------------------------------------------------------------- + +function TypeParser(defaultBuilder) { + + /* TODO + * - function(this:) // type of this + * - function(new:) // constructor + */ + var rLexer = /\s*(Array\.?<|Object\.?<|Set\.?<|Promise\.?<|function\(|\{|:|\(|\||\}|>|\)|,|\[\]|\*|\?|!|\.\.\.)|\s*((?:module:)?\w+(?:[\/.#~]\w+)*)|./g; + + var input, + builder, + token, + tokenStr; + + function next(expected) { + if ( expected !== undefined && token !== expected ) { + throw new SyntaxError("TypeParser: expected '" + expected + "', but found '" + tokenStr + "' (pos: " + rLexer.lastIndex + ", input='" + input + "')"); + } + var match = rLexer.exec(input); + if ( match ) { + tokenStr = match[1] || match[2]; + token = match[1] || (match[2] && 'symbol'); + if ( !token ) { + throw new SyntaxError("TypeParser: unexpected '" + tokenStr + "' (pos: " + match.index + ", input='" + input + "')"); + } + } else { + tokenStr = token = null; + } + } + + function parseType() { + var nullable = false; + var mandatory = false; + if ( token === '?' ) { + next(); + nullable = true; + } else if ( token === '!' ) { + next(); + mandatory = true; + } + + var type; + + if ( token === 'Array.<' || token === 'Array<' ) { + next(); + var componentType = parseType(); + next('>'); + type = builder.array(componentType); + } else if ( token === 'Object.<' || token === 'Object<' ) { + next(); + var keyType; + var valueType = parseType(); + if ( token === ',' ) { + next(); + keyType = valueType; + valueType = parseType(); + } else { + keyType = builder.synthetic(builder.simpleType('string')); + } + next('>'); + type = builder.object(keyType, valueType); + } else if ( token === 'Set.<' || token === 'Set<' ) { + next(); + var elementType = parseType(); + next('>'); + type = builder.set(elementType); + } else if ( token === 'Promise.<' || token === 'Promise<' ) { + next(); + var resultType = parseType(); + next('>'); + type = builder.promise(resultType); + } else if ( token === 'function(' ) { + next(); + var thisType, constructorType, paramTypes = [], returnType; + if ( tokenStr === 'this' ) { + next(); + next(':'); + thisType = parseType(); + if ( token === ',' ) { + next(); + } + } else if ( tokenStr === 'new' ) { + next(); + next(':'); + constructorType = parseType(); + if ( token === ',' ) { + next(); + } + } + while ( token === 'symbol' || token === '...' ) { + var repeatable = token === '...'; + if ( repeatable) { + next(); + } + var paramType = parseType(); + if ( repeatable ) { + paramType = builder.repeatable(paramType); + } + paramTypes.push(paramType); + if ( token === ',' ) { + if ( repeatable ) { + throw new SyntaxError("TypeParser: only the last parameter of a function can be repeatable (pos: " + rLexer.lastIndex + ", input='" + input + "')"); + } + next(); + } + } + next(')'); + if ( token === ':' ) { + next(':'); + returnType = parseType(); + } + type = builder.function(paramTypes, returnType, thisType, constructorType); + } else if ( token === '{' ) { + var structure = Object.create(null); + var propName,propType; + next(); + do { + propName = tokenStr; + if ( !/^\w+$/.test(propName) ) { + throw new SyntaxError("TypeParser: structure field must have a simple name (pos: " + rLexer.lastIndex + ", input='" + input + "', field:'" + propName + "')"); + } + next('symbol'); + if ( token === ':' ) { + next(); + propType = parseType(); + } else { + propType = builder.synthetic(builder.simpleType('any')); + } + structure[propName] = propType; + if ( token === '}' ) { + break; + } + next(','); + } while (token); + next('}'); + type = builder.structure(structure); + } else if ( token === '(' ) { + next(); + type = parseTypes(); + next(')'); + } else if ( token === '*' ) { + next(); + type = builder.simpleType('*'); + } else { + type = builder.simpleType(tokenStr); + next('symbol'); + while ( token === '[]' ) { + next(); + type = builder.array(type); + } + } + if ( nullable ) { + type = builder.nullable(type); + } + if ( mandatory ) { + type = builder.mandatory(type); + } + return type; + } + + function parseTypes() { + var types = []; + do { + types.push(parseType()); + if ( token !== '|' ) { + break; + } + next(); + } while (token); + return types.length === 1 ? types[0] : builder.union(types); + } + + this.parse = function(typeStr, tempBuilder) { + builder = tempBuilder || defaultBuilder || TypeParser.ASTBuilder; + input = String(typeStr); + rLexer.lastIndex = 0; + next(); + var type = parseTypes(); + next(null); + return type; + }; + +} + +TypeParser.ASTBuilder = { + simpleType: function(type) { + return { + type: 'simpleType', + name: type + }; + }, + array: function(componentType) { + return { + type: 'array', + component: componentType + }; + }, + object: function(keyType, valueType) { + return { + type: 'object', + key: keyType, + value: valueType + }; + }, + set: function(elementType) { + return { + type: 'set', + element: elementType + }; + }, + promise: function(fulfillmentType) { + return { + type: 'promise', + fulfill: fulfillmentType + }; + }, + "function": function(paramTypes, returnType, thisType, constructorType) { + return { + type: 'function', + params: paramTypes, + "return": returnType, + "this": thisType, + constructor: constructorType + }; + }, + structure: function(structure) { + return { + type: 'structure', + fields: structure + }; + }, + union: function(types) { + return { + type: 'union', + types: types + }; + }, + synthetic: function(type) { + type.synthetic = true; + return type; + }, + nullable: function(type) { + type.nullable = true; + return type; + }, + mandatory: function(type) { + type.mandatory = true; + return type; + }, + repeatable: function(type) { + type.repeatable = true; + return type; + } +}; + +TypeParser.LinkBuilder = function(style, encoded) { + this.linkStyle = style; + this.lt = encoded ? "<" : "<"; + this.gt = encoded ? ">" : ">"; +}; +TypeParser.LinkBuilder.prototype = { + safe: function(type) { + return type.needsParenthesis ? "(" + type.str + ")" : type.str; + }, + simpleType: function(type) { + if ( this.linkStyle === 'text' ) { + return { + str: type + }; + } + var link = new Link().toSymbol(type); + if ( this.linkStyle === 'short' ) { + link.withText(simpleNameOf(type)).withTooltip(type); + } + return { + str: link.toString() + }; + }, + array: function(componentType) { + if ( componentType.needsParenthesis ) { + return { + str: "Array.<" + componentType.str + ">" + }; + } + return { + str: componentType.str + "[]" + }; + }, + object: function(keyType, valueType) { + if ( keyType.synthetic ) { + return { + str: "Object." + this.lt + valueType.str + this.gt + }; + } + return { + str: "Object." + this.lt + keyType.str + "," + valueType.str + this.gt + }; + }, + set: function(elementType) { + return { + str: 'Set.' + this.lt + elementType.str + this.gt + }; + }, + promise: function(fulfillmentType) { + return { + str: 'Promise.' + this.lt + fulfillmentType.str + this.gt + }; + }, + "function": function(paramTypes, returnType) { + return { + str: "function(" + paramTypes.map(function(type) { return type.str; }).join(',') + ")" + ( returnType ? " : " + this.safe(returnType) : "") + }; + }, + structure: function(structure) { + var r = []; + for ( var fieldName in structure ) { + if ( structure[fieldName].synthetic ) { + r.push(fieldName); + } else { + r.push(fieldName + ":" + structure[fieldName].str); + } + } + return { + str: "{" + r.join(",") + "}" + }; + }, + union: function(types) { + return { + needsParenthesis: true, + str: types.map( this.safe.bind(this) ).join('|') + }; + }, + synthetic: function(type) { + type.synthetic = true; + return type; + }, + nullable: function(type) { + type.str = "?" + type.str; + return type; + }, + mandatory: function(type) { + type.str = "!" + type.str; + return type; + }, + repeatable: function(type) { + type.str = "..." + type.str; + return type; + } +}; + +var typeParser = new TypeParser(); +var _SHORT_BUILDER = new TypeParser.LinkBuilder('short', true); +var _LONG_BUILDER = new TypeParser.LinkBuilder('long', true); +var _TEXT_BUILDER = new TypeParser.LinkBuilder('text', false); +var _TEXT_BUILDER_ENCODED = new TypeParser.LinkBuilder('text', true); + +/* +function testTypeParser(type) { + debug("Type: '" + type + "' gives AST"); + try { + console.log(typeParser.parse(type)); + } catch (e) { + error("**** throws: " + e); + } +} + +testTypeParser("Array."); +testTypeParser("Array"); +testTypeParser("Object."); +testTypeParser("Object"); +testTypeParser("function(...string):Set"); +testTypeParser("{a:int,b,c:float,d,e}"); +*/ + +function _processTypeString(type, builder) { + if ( type && Array.isArray(type.names) ) { + type = type.names.join('|'); + } + if ( type ) { + try { + return typeParser.parse( type, builder ).str; + } catch (e) { + error("failed to parse type string '" + type + "': " + e); + return type; + } + } +} + +function listTypes(type, encoded) { + return _processTypeString(type, encoded ? _TEXT_BUILDER_ENCODED : _TEXT_BUILDER); +} + +function linkTypes(type, short) { + return _processTypeString(type, short ? _SHORT_BUILDER : _LONG_BUILDER ); +} + +function isArrayType(type) { + if ( type && Array.isArray(type.names) ) { + type = type.names.join('|'); + } + if ( type ) { + try { + var ast = typeParser.parse( type, TypeParser.ASTBuilder ); + return ( ast.type === 'array' || (ast.type === 'union' && ast.types.some( function(subtype) { return subtype.type === 'array'; })) ); + } catch (e) { + error("failed to parse type string '" + type + "': " + e); + } + } + return false; +} + +/** + * Reduces the given text to a summary and removes all tags links etc. and escapes double quotes. + * The result therefore should be suitable as content for an HTML tag attribute (e.g. title). + * + * @param {string} sText Text to extract a summary from + * @returns {string} summarized, plain attribute + */ +function asPlainSummary(sText) { + return sText ? summarize(sText).replace(/<.*?>/g, '').replace(/\{\@link\s*(.*?)\}/g, '$1').replace(/"/g,""") : ''; +} + +function getNSClass(item) { + if (item.kind === 'interface') { + return " interface"; + } else if (item.kind === 'namespace') { + return " namespace"; + } else if (item.kind === 'typedef' ) { + return " typedef"; + } else if (item.kind === 'member' && item.isEnum ) { + return " enum"; + } else { + return ""; + } +} + +/* + * regexp to recognize important places in the text + * + * Capturing groups of the RegExp: + * group 1: begin of a pre block + * group 2: end of a pre block + * group 3: an empty line + surrounding whitespace (implicitly starts a new paragraph) + * group 4: an isolated line feed + surrounding whitespace + * + * [-------

     block -------] [---- an empty line and surrounding whitespace ----] [---- new line or whitespaces ----] */
    +var rNormalizeText = /(]*)?>)|(<\/pre>)|([ \t]*(?:\r\n|\r|\n)[ \t]*(?:\r\n|\r|\n)[ \t\r\n]*)|([ \t]*(?:\r\n|\r|\n)[ \t]*|[ \t]+)/gi;
    +
    +function normalizeWS(text) {
    +	if ( text == null ) {
    +		return text;
    +	}
    +
    +	var inpre = false;
    +	return String(text).replace(rNormalizeText, function(match, pre, endpre, emptyline, ws) {
    +		if ( pre ) {
    +			inpre = true;
    +			return pre;
    +		} else if ( endpre ) {
    +			inpre = false;
    +			return endpre;
    +		} else if ( emptyline ) {
    +			return inpre ? emptyline : '\n\n';
    +		} else if ( ws ) {
    +			return inpre ? ws : ' ';
    +		}
    +		return match;
    +	});
    +
    +}
    +
    +//---- add on: API JSON -----------------------------------------------------------------
    +
    +function createAPIJSON(symbols, filename) {
    +
    +	var api = {
    +		"$schema-ref": "http://schemas.sap.com/sapui5/designtime/api.json/1.0"
    +	};
    +
    +	if ( templateConf.version ) {
    +		api.version = templateConf.version.replace(/-SNAPSHOT$/,"");
    +	}
    +	if ( templateConf.uilib ) {
    +		api.library = templateConf.uilib;
    +	}
    +
    +	api.symbols = [];
    +	// sort only a copy(!) of the symbols, otherwise the SymbolSet lookup is broken
    +	symbols.slice(0).sort(sortByAlias).forEach(function(symbol) {
    +		if ( isFirstClassSymbol(symbol) && !symbol.synthetic ) { // dump a symbol if it as a class symbol and if it is not a synthetic symbol
    +			api.symbols.push(createAPIJSON4Symbol(symbol, false));
    +		}
    +	});
    +
    +	postProcessAPIJSON(api);
    +
    +	fs.mkPath(path.dirname(filename));
    +	fs.writeFileSync(filename, JSON.stringify(api), 'utf8');
    +	info("  apiJson saved as " + filename);
    +}
    +
    +function createAPIJSON4Symbol(symbol, omitDefaults) {
    +
    +	var obj = [];
    +	var curr = obj;
    +	var attribForKind = 'kind';
    +	var stack = [];
    +
    +	function isEmpty(obj) {
    +		if ( !obj ) {
    +			return true;
    +		}
    +		for (var n in obj) {
    +			if ( obj.hasOwnProperty(n) ) {
    +				return false;
    +			}
    +		}
    +		return true;
    +	}
    +
    +	function tag(name, value, omitEmpty) {
    +
    +		if ( omitEmpty && !value ) {
    +			return;
    +		}
    +		if ( arguments.length === 1 ) { // opening tag
    +			stack.push(curr);
    +			stack.push(attribForKind);
    +			var obj = {};
    +			if ( Array.isArray(curr) ) {
    +				if ( attribForKind != null ) {
    +					obj[attribForKind] = name;
    +				}
    +				curr.push(obj);
    +			} else {
    +				curr[name] = obj;
    +			}
    +			curr = obj;
    +			attribForKind = null;
    +			return;
    +		}
    +		if ( value == null ) {
    +			curr[name] = true;
    +		} else {
    +			curr[name] = String(value);
    +		}
    +	}
    +
    +	function attrib(name, value, defaultValue, raw) {
    +		if ( omitDefaults && arguments.length >= 3 && value === defaultValue ) {
    +			return;
    +		}
    +		if ( arguments.length === 1 /* empty tag */ ) {
    +			curr[name] = true;
    +		} else {
    +			curr[name] = raw ? value : String(value);
    +		}
    +	}
    +
    +	function closeTag(name, noIndent) {
    +		attribForKind = stack.pop();
    +		curr  = stack.pop();
    +	}
    +
    +	function collection(name, attribForKind) {
    +		stack.push(curr);
    +		stack.push(attribForKind);
    +		// TODO only supported if this.curr was an object check or fully implement
    +		curr = curr[name] = [];
    +		attribForKind = attribForKind || null;
    +	}
    +
    +	function endCollection(name) {
    +		attribForKind = stack.pop();
    +		curr  = stack.pop();
    +	}
    +
    +	function tagWithSince(name, value) {
    +
    +		if ( !value ) {
    +			return;
    +		}
    +
    +		var info = extractSince(value);
    +
    +		tag(name);
    +		if ( info.since ) {
    +			attrib("since", info.since);
    +		}
    +		if ( info.value ) {
    +			curr["text"] = normalizeWS(info.value);
    +		}
    +		closeTag(name, true);
    +
    +	}
    +
    +	function examples(symbol) {
    +		var j, example;
    +
    +		if ( symbol.examples && symbol.examples.length ) {
    +			collection("examples");
    +			for ( j = 0; j < symbol.examples.length; j++) {
    +				example = makeExample(symbol.examples[j]);
    +				tag("example");
    +				if ( example.caption ) {
    +					attrib("caption", example.caption);
    +				}
    +				attrib("text", example.example);
    +				closeTag("example");
    +			}
    +			endCollection("examples");
    +		}
    +	}
    +
    +	function referencesList(symbol) {
    +		if ( symbol.see && symbol.see.length ) {
    +			curr["references"] = symbol.see.slice();
    +		}
    +	}
    +
    +	function visibility($) {
    +		if ( $.access === 'protected' ) {
    +			return "protected";
    +		} else if ( $.access === 'restricted' ) {
    +			return "restricted";
    +		} else if ( $.access === 'private' ) {
    +			return "private";
    +		} else {
    +			return "public";
    +		}
    +	}
    +
    +	function exceptions(symbol) {
    +		var array = symbol.exceptions,
    +			j, exception;
    +
    +		if ( Array.isArray(array) ) {
    +			array = array.filter( function (ex) {
    +				return (ex.type && listTypes(ex.type)) || (ex.description && ex.description.trim());
    +			});
    +		}
    +		if ( array == null || array.length === 0 ) {
    +			return;
    +		}
    +
    +		collection("throws");
    +		for (j = 0; j < array.length; j++) {
    +			exception = array[j];
    +			tag("exception");
    +			if ( exception.type !== undefined ) {
    +				attrib("type", listTypes(exception.type));
    +			}
    +			tag("description", normalizeWS(exception.description), true);
    +			closeTag("exception");
    +		}
    +		endCollection("throws");
    +	}
    +
    +	function methodList(tagname, methods) {
    +		methods = methods && Object.keys(methods).map(function(key) { return methods[key]; });
    +		if ( methods != null && methods.length > 0 ) {
    +			curr[tagname] = methods;
    +		}
    +	}
    +
    +	function interfaceList(tagname, interfaces) {
    +		if ( interfaces != null && interfaces.length > 0 ) {
    +			curr[tagname] = interfaces.slice();
    +		}
    +	}
    +
    +	function hasSettings($, visited) {
    +
    +		visited = visited || {};
    +
    +		if ( $.augments && $.augments.length > 0 ) {
    +			var baseSymbol = $.augments[0];
    +			if ( visited.hasOwnProperty(baseSymbol) ) {
    +				error("detected cyclic inheritance when looking at " + $.longname + ": " + JSON.stringify(visited));
    +				return false;
    +			}
    +			visited[baseSymbol] = true;
    +			baseSymbol = lookup(baseSymbol);
    +			if ( hasSettings(baseSymbol, visited) ) {
    +				return true;
    +			}
    +		}
    +
    +		var metadata = $.__ui5.metadata;
    +		return metadata &&
    +			(
    +				!isEmpty(metadata.specialSettings)
    +				|| !isEmpty(metadata.properties)
    +				|| !isEmpty(metadata.aggregations)
    +				|| !isEmpty(metadata.associations)
    +				|| !isEmpty(metadata.annotations)
    +				|| !isEmpty(metadata.events)
    +			);
    +	}
    +
    +	function writeMetadata($) {
    +
    +		var metadata = $.__ui5.metadata;
    +		if ( !metadata ) {
    +			return;
    +		}
    +
    +		var n;
    +
    +		if ( metadata.specialSettings && Object.keys(metadata.specialSettings).length > 0 ) {
    +			collection("specialSettings");
    +			for ( n in metadata.specialSettings ) {
    +				var special = metadata.specialSettings[n];
    +				tag("specialSetting");
    +				attrib("name", special.name);
    +				attrib("type", special.type);
    +				attrib("visibility", special.visibility, 'public');
    +				if ( special.since ) {
    +					attrib("since", extractVersion(special.since));
    +				}
    +				tag("description", normalizeWS(special.doc), true);
    +				tagWithSince("experimental", special.experimental);
    +				tagWithSince("deprecated", special.deprecation);
    +				methodList("method", special.methods);
    +				closeTag("specialSetting");
    +			}
    +			endCollection("specialSettings");
    +		}
    +
    +		if ( metadata.properties && Object.keys(metadata.properties).length > 0 ) {
    +			collection("properties");
    +			for ( n in metadata.properties ) {
    +				var prop = metadata.properties[n];
    +				tag("property");
    +				attrib("name", prop.name);
    +				attrib("type", prop.type, 'string');
    +				attrib("defaultValue", prop.defaultValue, null, /* raw = */true);
    +				attrib("group", prop.group, 'Misc');
    +				attrib("visibility", prop.visibility, 'public');
    +				if ( prop.since ) {
    +					attrib("since", extractVersion(prop.since));
    +				}
    +				if ( prop.bindable ) {
    +					attrib("bindable", prop.bindable, false, /* raw = */true);
    +				}
    +				tag("description", normalizeWS(prop.doc), true);
    +				tagWithSince("experimental", prop.experimental);
    +				tagWithSince("deprecated", prop.deprecation);
    +				methodList("methods", prop.methods);
    +				closeTag("property");
    +			}
    +			endCollection("properties");
    +		}
    +
    +		if ( metadata.defaultProperty ) {
    +			tag("defaultProperty", metadata.defaultProperty);
    +		}
    +
    +		if ( metadata.dnd ) {
    +			curr.dnd = metadata.dnd;
    +		}
    +
    +		if ( metadata.aggregations && Object.keys(metadata.aggregations).length > 0 ) {
    +			collection("aggregations");
    +			for ( n in metadata.aggregations ) {
    +				var aggr = metadata.aggregations[n];
    +				tag("aggregation");
    +				attrib("name", aggr.name);
    +				attrib("singularName", aggr.singularName); // TODO omit default?
    +				attrib("type", aggr.type, 'sap.ui.core.Control');
    +				if ( aggr.altTypes ) {
    +					curr.altTypes = aggr.altTypes.slice();
    +				}
    +				attrib("cardinality", aggr.cardinality, '0..n');
    +				attrib("visibility", aggr.visibility, 'public');
    +				if ( aggr.since ) {
    +					attrib("since", extractVersion(aggr.since));
    +				}
    +				if ( aggr.bindable ) {
    +					attrib("bindable", aggr.bindable, false, /* raw = */true);
    +				}
    +				if ( aggr.dnd ) {
    +					curr.dnd = aggr.dnd;
    +				}
    +				tag("description", normalizeWS(aggr.doc), true);
    +				tagWithSince("experimental", aggr.experimental);
    +				tagWithSince("deprecated", aggr.deprecation);
    +				methodList("methods", aggr.methods);
    +				closeTag("aggregation");
    +			}
    +			endCollection("aggregations");
    +		}
    +
    +		if ( metadata.defaultAggregation ) {
    +			tag("defaultAggregation", metadata.defaultAggregation);
    +		}
    +
    +		if ( metadata.associations && Object.keys(metadata.associations).length > 0 ) {
    +			collection("associations");
    +			for ( n in metadata.associations ) {
    +				var assoc = metadata.associations[n];
    +				tag("association");
    +				attrib("name", assoc.name);
    +				attrib("singularName", assoc.singularName); // TODO omit default?
    +				attrib("type", assoc.type, 'sap.ui.core.Control');
    +				attrib("cardinality", assoc.cardinality, '0..1');
    +				attrib("visibility", assoc.visibility, 'public');
    +				if ( assoc.since ) {
    +					attrib("since", extractVersion(assoc.since));
    +				}
    +				tag("description", normalizeWS(assoc.doc), true);
    +				tagWithSince("experimental", assoc.experimental);
    +				tagWithSince("deprecated", assoc.deprecation);
    +				methodList("methods", assoc.methods);
    +				closeTag("association");
    +			}
    +			endCollection("associations");
    +		}
    +
    +		if ( metadata.events && Object.keys(metadata.events).length > 0 ) {
    +			collection("events");
    +			for ( n in metadata.events ) {
    +				var event = metadata.events[n];
    +				tag("event");
    +				attrib("name", event.name);
    +				attrib("visibility", event.visibility, 'public');
    +				if ( event.since ) {
    +					attrib("since", extractVersion(event.since));
    +				}
    +				tag("description", normalizeWS(event.doc), true);
    +				tagWithSince("experimental", event.experimental);
    +				tagWithSince("deprecated", event.deprecation);
    +				if ( event.parameters && Object.keys(event.parameters).length > 0 ) {
    +					tag("parameters");
    +					for ( var pn in event.parameters ) {
    +						if ( event.parameters.hasOwnProperty(pn) ) {
    +							var param = event.parameters[pn];
    +							tag(pn);
    +							attrib("name", pn);
    +							attrib("type", param.type);
    +							if ( param.since ) {
    +								attrib("since", extractVersion(param.since));
    +							}
    +							tag("description", normalizeWS(param.doc), true);
    +							tagWithSince("experimental", param.experimental);
    +							tagWithSince("deprecated", param.deprecation);
    +							closeTag(pn);
    +						}
    +					}
    +					closeTag("parameters");
    +				}
    +				methodList("methods", event.methods, true);
    +				closeTag("event");
    +			}
    +			endCollection("events");
    +		}
    +
    +		if ( metadata.annotations && Object.keys(metadata.annotations).length > 0 ) {
    +			collection("annotations");
    +			for ( n in metadata.annotations ) {
    +				var anno = metadata.annotations[n];
    +				tag("annotation");
    +				attrib("name", anno.name);
    +				attrib("namespace", anno.namespace);
    +				if ( anno.target && anno.target.length > 0 ) {
    +					curr.target = anno.target.slice();
    +				}
    +				attrib("annotation", anno.annotation);
    +				attrib("defaultValue", anno.defaultValue);
    +				if ( anno.appliesTo && anno.appliesTo.length > 0 ) {
    +					curr.appliesTo = anno.appliesTo.slice();
    +				}
    +				if ( anno.since ) {
    +					attrib("since", extractVersion(anno.since));
    +				}
    +				tag("description", normalizeWS(anno.doc), true);
    +				tagWithSince("deprecated", anno.deprecation);
    +				closeTag("annotation");
    +			}
    +			endCollection("annotations");
    +		}
    +
    +		if ( metadata.designtime ) { // don't write falsy values
    +			tag("designtime", metadata.designtime);
    +		}
    +
    +	}
    +
    +	function writeParameterProperties(param, params) {
    +		var prefix = param.name + '.',
    +			altPrefix = isArrayType(param.type) ? param.name + '[].' : null,
    +			count = 0,
    +			i;
    +
    +		for ( i = 0; i < params.length; i++ ) {
    +
    +			var name = params[i].name;
    +			if ( altPrefix && name.lastIndexOf(altPrefix, 0) === 0 ) { // startsWith
    +				name = name.slice(altPrefix.length);
    +			} else if ( name.lastIndexOf(prefix, 0) === 0 ) { // startsWith
    +				if ( altPrefix ) {
    +					warning("Nested @param tag in the context of an array type is used without []-suffix", name);
    +				}
    +				name = name.slice(prefix.length);
    +			} else {
    +				continue;
    +			}
    +
    +			if ( name.indexOf('.') >= 0 ) {
    +				continue;
    +			}
    +
    +			if ( count === 0 ) {
    +				tag("parameterProperties");
    +			}
    +
    +			count++;
    +
    +			tag(name);
    +			attrib("name", name);
    +			attrib("type", listTypes(params[i].type));
    +			attrib("optional", !!params[i].optional, false, /* raw = */true);
    +			if ( params[i].defaultvalue !== undefined ) {
    +				attrib("defaultValue", params[i].defaultvalue, undefined, /* raw = */true);
    +			}
    +			if ( params[i].since ) {
    +				attrib("since", extractVersion(params[i].since));
    +			}
    +
    +			writeParameterProperties(params[i], params);
    +
    +			tag("description", normalizeWS(params[i].description), true);
    +			tagWithSince("experimental", params[i].experimental);
    +			tagWithSince("deprecated", params[i].deprecated);
    +
    +			closeTag(name);
    +		}
    +
    +		if ( count > 0 ) {
    +			closeTag("parameterProperties");
    +		}
    +	}
    +
    +	function methodSignature(member, suppressReturnValue) {
    +
    +		if ( !suppressReturnValue ) {
    +			var returns = member.returns && member.returns.length && member.returns[0];
    +			var type = member.type || (returns && returns.type);
    +			type = listTypes(type);
    +			//if ( type && type !== 'void' ) {
    +			//	attrib("type", type, 'void');
    +			//}
    +			if ( type && type !== 'void' || returns && returns.description ) {
    +				tag("returnValue");
    +				if ( type && type !== 'void' ) {
    +					attrib("type", type);
    +				}
    +				if ( returns && returns.description ) {
    +					attrib("description", normalizeWS(returns.description));
    +				}
    +				closeTag("returnValue");
    +			}
    +		}
    +
    +		if ( member.params && member.params.length > 0 ) {
    +			collection("parameters");
    +			for ( var j = 0; j < member.params.length; j++) {
    +				var param = member.params[j];
    +				if ( param.name.indexOf('.') >= 0 ) {
    +					continue;
    +				}
    +				tag("parameter");
    +				attrib("name", param.name);
    +				attrib("type", listTypes(param.type));
    +				attrib("optional", !!param.optional, false, /* raw = */true);
    +				if ( param.defaultvalue !== undefined ) {
    +					attrib("defaultValue", param.defaultvalue, undefined, /* raw = */true);
    +				}
    +				if ( param.since ) {
    +					attrib("since", extractVersion(param.since));
    +				}
    +				writeParameterProperties(param, member.params);
    +				tag("description", normalizeWS(param.description), true);
    +				tagWithSince("experimental", param.experimental);
    +				tagWithSince("deprecated", param.deprecated);
    +				closeTag("parameter");
    +			}
    +			endCollection("parameters");
    +		}
    +
    +		exceptions(member);
    +
    +	}
    +
    +	function writeMethod(member, name) {
    +		tag("method");
    +		attrib("name", name || member.name);
    +		if ( member.__ui5.module && member.__ui5.module !== symbol.__ui5.module ) {
    +			attrib("module", member.__ui5.module);
    +			attrib("export", undefined, '', true);
    +		}
    +		attrib("visibility", visibility(member), 'public');
    +		if ( member.scope === 'static' ) {
    +			attrib("static", true, false, /* raw = */true);
    +		}
    +		if ( member.since ) {
    +			attrib("since", extractVersion(member.since));
    +		}
    +		if ( member.tags && member.tags.some(function(tag) { return tag.title === 'ui5-metamodel'; }) ) {
    +			attrib('ui5-metamodel', true, false, /* raw = */true);
    +		}
    +
    +		methodSignature(member);
    +
    +		tag("description", normalizeWS(member.description), true);
    +		tagWithSince("experimental", member.experimental);
    +		tagWithSince("deprecated", member.deprecated);
    +		examples(member);
    +		referencesList(member);
    +		//secTags(member);
    +		if ( member.__ui5.resource && member.__ui5.resource !== symbol.__ui5.resource ) {
    +			attrib("resource", member.__ui5.resource);
    +		}
    +		closeTag("method");
    +
    +	}
    +
    +	/*
    +	var rSplitSecTag = /^\s*\{([^\}]*)\}/;
    +
    +	function secTags($) {
    +		if ( true ) {
    +			return;
    +		}
    +		var aTags = $.tags;
    +		if ( !aTags ) {
    +			return;
    +		}
    +		for (var iTag = 0; iTag < A_SECURITY_TAGS.length; iTag++  ) {
    +			var oTagDef = A_SECURITY_TAGS[iTag];
    +			for (var j = 0; j < aTags.length; j++ ) {
    +				if ( aTags[j].title.toLowerCase() === oTagDef.name.toLowerCase() ) {
    +					tag(oTagDef.name);
    +					var m = rSplitSecTag.exec(aTags[j].text);
    +					if ( m && m[1].trim() ) {
    +						var aParams = m[1].trim().split(/\s*\|\s* /); <-- remember to remove the space!
    +						for (var iParam = 0; iParam < aParams.length; iParam++ ) {
    +							tag(oTagDef.params[iParam], aParams[iParam]);
    +						}
    +					}
    +					var sDesc = aTags[j].description;
    +					tag("description", sDesc, true);
    +					closeTag(oTagDef.name);
    +				}
    +			}
    +		}
    +	}
    +	*/
    +
    +	var kind = (symbol.kind === 'member' && symbol.isEnum) ? "enum" : symbol.kind; // handle pseudo-kind 'enum'
    +
    +	tag(kind);
    +
    +	attrib("name", symbol.longname);
    +	attrib("basename", symbol.name);
    +	if ( symbol.__ui5.resource ) {
    +		attrib("resource", symbol.__ui5.resource);
    +	}
    +	if ( symbol.__ui5.module ) {
    +		attrib("module", symbol.__ui5.module);
    +		attrib("export", undefined, '', true);
    +	}
    +	if ( /* TODO (kind === 'class') && */ symbol.virtual ) {
    +		// Note reg. the TODO: only one unexpected occurrence found in DragSession (DragAndDrop.js)
    +		attrib("abstract", true, false, /* raw = */true);
    +	}
    +	if ( /* TODO (kind === 'class' || kind === 'interface') && */ symbol.final_ ) {
    +		// Note reg. the TODO: enums are marked as final & namespace, they would loose the final with the addtl. check.
    +		attrib("final", true, false, /* raw = */true);
    +	}
    +	if ( symbol.scope === 'static' ) {
    +		attrib("static", true, false, /* raw = */true);
    +	}
    +	attrib("visibility", visibility(symbol), 'public');
    +	if ( symbol.since ) {
    +		attrib("since", extractVersion(symbol.since));
    +	}
    +	/* TODO if ( kind === 'class' || kind === 'interface' ) { */
    +		// Note reg. the TODO: some objects document that they extend other objects. JSDoc seems to support this use case
    +		// (properties show up as 'borrowed') and the borrowed entities also show up in the SDK (but not the 'extends' relationship itself)
    +		if ( symbol.augments && symbol.augments.length ) {
    +			tag("extends", symbol.augments.sort().join(",")); // TODO what about multiple inheritance?
    +		}
    +		interfaceList("implements", symbol.implements);
    +	/* } */
    +	tag("description", normalizeWS(symbol.classdesc || (symbol.kind === 'class' ? '' : symbol.description)), true);
    +	if ( kind !== 'class' ) {
    +		examples(symbol); // for a class, examples are added to the constructor
    +		referencesList(symbol); // for a class, references are added to the constructor
    +	}
    +	tagWithSince("experimental", symbol.experimental);
    +	tagWithSince("deprecated", symbol.deprecated);
    +	if ( symbol.tags && symbol.tags.some(function(tag) { return tag.title === 'ui5-metamodel'; }) ) {
    +		attrib('ui5-metamodel', true, false, /* raw = */true);
    +	}
    +
    +	var skipMembers = false;
    +	var i, j, member, param;
    +
    +	if ( kind === 'class' ) {
    +
    +		if ( symbol.__ui5.stereotype || hasSettings(symbol) ) {
    +
    +			tag("ui5-metadata");
    +
    +			if ( symbol.__ui5.stereotype ) {
    +				attrib("stereotype", symbol.__ui5.stereotype);
    +			}
    +
    +			writeMetadata(symbol);
    +
    +			closeTag("ui5-metadata");
    +		}
    +
    +
    +		// IF @hideconstructor tag is present we omit the whole constructor
    +		if ( !symbol.hideconstructor ) {
    +
    +			tag("constructor");
    +			attrib("visibility", visibility(symbol));
    +			methodSignature(symbol, /* suppressReturnValue = */ true);
    +
    +			tag("description", normalizeWS(symbol.description), true);
    +			// tagWithSince("experimental", symbol.experimental); // TODO repeat from class?
    +			// tagWithSince("deprecated", symbol.deprecated); // TODO repeat from class?
    +			examples(symbol); // TODO here or for class?
    +			referencesList(symbol); // TODO here or for class?
    +			// secTags(symbol); // TODO repeat from class?
    +			closeTag("constructor");
    +
    +		}
    +	} else if ( kind === 'namespace' ) {
    +		if ( symbol.__ui5.stereotype || symbol.__ui5.metadata ) {
    +			tag("ui5-metadata");
    +
    +			if ( symbol.__ui5.stereotype ) {
    +				attrib("stereotype", symbol.__ui5.stereotype);
    +			}
    +
    +			if ( symbol.__ui5.metadata && symbol.__ui5.metadata.basetype ) {
    +				attrib("basetype", symbol.__ui5.metadata.basetype);
    +			}
    +
    +			if ( symbol.__ui5.metadata && symbol.__ui5.metadata.pattern ) {
    +				attrib("pattern", symbol.__ui5.metadata.pattern);
    +			}
    +
    +			if ( symbol.__ui5.metadata && symbol.__ui5.metadata.range ) {
    +				attrib("range", symbol.__ui5.metadata.range, null, /* raw = */ true);
    +			}
    +
    +			closeTag("ui5-metadata");
    +		}
    +	} else if ( kind === 'typedef' ) {
    +		// typedefs have their own property structure
    +		skipMembers = true;
    +		if ( symbol.properties && symbol.properties.length > 0 ) {
    +			collection("properties");
    +			symbol.properties.forEach(function(prop) {
    +				tag("property");
    +				attrib("name", prop.name);
    +				attrib("type", listTypes(prop.type));
    +				attrib("visibility", visibility(symbol), 'public'); // properties inherit visibility of typedef
    +				tag("description", normalizeWS(prop.description), true);
    +				closeTag("property");
    +			});
    +			endCollection("properties");
    +		}
    +	} else if ( kind === 'function' ) {
    +		methodSignature(symbol);
    +	}
    +
    +	if ( !skipMembers ) {
    +		var ownProperties = childrenOfKind(symbol, "property").own.sort(sortByAlias);
    +		if ( ownProperties.length > 0 ) {
    +			collection("properties");
    +			for ( i = 0; i < ownProperties.length; i++ ) {
    +				member = ownProperties[i];
    +				tag("property");
    +				attrib("name", member.name);
    +				if ( member.__ui5.module && member.__ui5.module !== symbol.__ui5.module ) {
    +					attrib("module", member.__ui5.module);
    +					attrib("export", undefined, '', true);
    +				}
    +				attrib("visibility", visibility(member), 'public');
    +				if ( member.scope === 'static' ) {
    +					attrib("static", true, false, /* raw = */true);
    +				}
    +				if ( member.since ) {
    +					attrib("since", extractVersion(member.since));
    +				}
    +				attrib("type", listTypes(member.type));
    +				tag("description", normalizeWS(member.description), true);
    +				tagWithSince("experimental", member.experimental);
    +				tagWithSince("deprecated", member.deprecated);
    +				examples(member);
    +				referencesList(member);
    +				if ( member.__ui5.resource && member.__ui5.resource !== symbol.__ui5.resource ) {
    +					attrib("resource", member.__ui5.resource);
    +				}
    +				closeTag("property");
    +			}
    +			endCollection("properties");
    +		}
    +
    +		var ownEvents = childrenOfKind(symbol, 'event').own.sort(sortByAlias);
    +		if ( ownEvents.length > 0 ) {
    +			collection("events");
    +			for (i = 0; i < ownEvents.length; i++ ) {
    +				member = ownEvents[i];
    +				tag("event");
    +				attrib("name", member.name);
    +				if ( member.__ui5.module && member.__ui5.module !== symbol.__ui5.module ) {
    +					attrib("module", member.__ui5.module);
    +					attrib("export", undefined, '', true);
    +				}
    +				attrib("visibility", visibility(member), 'public');
    +				if ( member.scope === 'static' ) {
    +					attrib("static", true, false, /* raw = */true);
    +				}
    +				if ( member.since ) {
    +					attrib("since", extractVersion(member.since));
    +				}
    +
    +				if ( member.params && member.params.length > 0 ) {
    +					collection("parameters");
    +					for (j = 0; j < member.params.length; j++) {
    +						param = member.params[j];
    +						if ( param.name.indexOf('.') >= 0 ) {
    +							continue;
    +						}
    +
    +						tag("parameter");
    +						attrib("name", param.name);
    +						attrib("type", listTypes(param.type));
    +						if ( param.since ) {
    +							attrib("since", extractVersion(param.since));
    +						}
    +						writeParameterProperties(param, member.params);
    +						tag("description", normalizeWS(param.description), true);
    +						tagWithSince("experimental", param.experimental);
    +						tagWithSince("deprecated", param.deprecated);
    +						closeTag("parameter");
    +					}
    +					endCollection("parameters");
    +				}
    +				tag("description", normalizeWS(member.description), true);
    +				tagWithSince("deprecated", member.deprecated);
    +				tagWithSince("experimental", member.experimental);
    +				examples(member);
    +				referencesList(member);
    +				//secTags(member);
    +				if ( member.__ui5.resource && member.__ui5.resource !== symbol.__ui5.resource ) {
    +					attrib("resource", member.__ui5.resource);
    +				}
    +				closeTag("event");
    +			}
    +			endCollection("events");
    +		}
    +
    +		var ownMethods = childrenOfKind(symbol, 'method').own.sort(sortByAlias);
    +		if ( ownMethods.length > 0 ) {
    +			collection("methods");
    +			ownMethods.forEach(function(member) {
    +				writeMethod(member);
    +				if ( member.__ui5.members ) {
    +					// HACK: export nested static functions as siblings of the current function
    +					// A correct representation has to be discussed with the SDK / WebIDE
    +					member.__ui5.members.forEach(function($) {
    +						if ( $.kind === 'function' && $.scope === 'static'
    +							 && conf.filter($) && !$.inherited ) {
    +							error("exporting nested function '" + member.name + "." + $.name + "'");
    +							writeMethod($, member.name + "." + $.name);
    +						}
    +					});
    +				}
    +			});
    +			endCollection("methods");
    +		}
    +
    +	//	if ( roots && symbol.__ui5.children && symbol.__ui5.children.length ) {
    +	//		collection("children", "kind");
    +	//		symbol.__ui5.children.forEach(writeSymbol);
    +	//		endCollection("children");
    +	//	}
    +	}
    +
    +	closeTag(kind);
    +
    +	return obj[0];
    +}
    +
    +function postProcessAPIJSON(api) {
    +	var modules = {};
    +	var symbols = api.symbols;
    +	var i,j,n,symbol,defaultExport;
    +
    +	// collect modules and the symbols that refer to them
    +	for ( i = 0; i < symbols.length; i++) {
    +		symbol = symbols[i];
    +		if ( symbol.module ) {
    +			modules[symbol.module] = modules[symbol.module] || [];
    +			modules[symbol.module].push({
    +				name: symbol.name,
    +				symbol: symbol
    +			});
    +		}
    +		if ( symbol.properties ) {
    +			for ( j = 0; j < symbol.properties.length; j++ ) {
    +				if ( symbol.properties[j].static && symbol.properties[j].module ) {
    +					modules[symbol.properties[j].module] = modules[symbol.properties[j].module] || [];
    +					modules[symbol.properties[j].module].push({
    +						name: symbol.name + "." + symbol.properties[j].name,
    +						symbol: symbol.properties[j]
    +					});
    +				}
    +			}
    +		}
    +		if ( symbol.methods ) {
    +			for ( j = 0; j < symbol.methods.length; j++ ) {
    +				if ( symbol.methods[j].static && symbol.methods[j].module ) {
    +					modules[symbol.methods[j].module] = modules[symbol.methods[j].module] || [];
    +					modules[symbol.methods[j].module].push({
    +						name: symbol.name + "." + symbol.methods[j].name,
    +						symbol: symbol.methods[j]
    +					});
    +				}
    +			}
    +		}
    +	}
    +
    +	function guessExports(moduleName, symbols) {
    +
    +		symbols = symbols.sort(function(a,b) {
    +			if ( a.name === b.name ) {
    +				return 0;
    +			}
    +			return a.name < b.name ? -1 : 1;
    +		});
    +
    +		// info('resolving exports of ' + n + ": " + symbols.map(function(symbol) { return symbol.name; } ));
    +		var moduleNamePath = "module:" + moduleName;
    +		if ( /^jquery\.sap\./.test(moduleName) ) {
    +			// the jquery.sap.* modules all export 'jQuery'.
    +			// any API from those modules is reachable via 'jQuery.*'
    +			defaultExport = 'jQuery';
    +		} else {
    +			// library.js modules export the library namespace; for all other modules, the assumed default export
    +			// is identical to the name of the module (converted to a 'dot' name)
    +			defaultExport = moduleName.replace(/\/library$/, "").replace(/\//g, ".");
    +		}
    +
    +		symbols.forEach(function(symbol) {
    +			// debug("check ", symbol.name, "against", defaultExport, "and", moduleNamePath);
    +			if ( symbol.name === moduleNamePath ) {
    +				// symbol name is the same as the module namepath -> symbol is the default export
    +				symbol.symbol.export = "";
    +			} else if ( symbol.name.lastIndexOf(moduleNamePath + ".", 0) === 0 ) {
    +				// symbol name starts with the module namepath and a dot -> symbol is a named export (static)
    +				symbol.symbol.export = symbol.name.slice(moduleNamePath.length + 1);
    +			} else if ( symbol.name === defaultExport ) {
    +				// default export equals the symbol name
    +				symbol.symbol.export = "";
    +				//debug("    (default):" + defaultExport);
    +			} else if ( symbol.name.lastIndexOf(defaultExport + ".", 0) === 0 ) {
    +				// default export is a prefix of the symbol name
    +				symbol.symbol.export = symbol.name.slice(defaultExport.length + 1);
    +				//debug("    " + symbol.name.slice(defaultExport.length + 1) + ":" + symbol.name);
    +			} else {
    +				// default export is not a prefix of the symbol name -> no way to access it in AMD
    +				symbol.symbol.export = undefined;
    +				if ( symbol.symbol.kind !== "namespace"
    +					 || (symbol.symbol.properties && symbol.symbol.properties.length > 0)
    +					 || (symbol.symbol.methods && symbol.symbol.methods.length > 0) ) {
    +					error("could not identify export name of '" + symbol.name + "', contained in module '" + moduleName + "'");
    +				} else {
    +					debug("could not identify export name of namespace '" + symbol.name + "', contained in module '" + moduleName + "'");
    +				}
    +			}
    +		});
    +
    +	}
    +
    +	for ( n in modules ) {
    +		guessExports(n, modules[n]);
    +	}
    +}
    +
    +//---- add on: API XML -----------------------------------------------------------------
    +
    +function createAPIXML(symbols, filename, options) {
    +
    +	options = options || {};
    +	var roots = options.roots || null;
    +	var legacyContent = !!options.legacyContent;
    +	var omitDefaults = !!options.omitDefaults;
    +	var addRedundancy = !!options.resolveInheritance;
    +
    +	var indent = 0;
    +	var output = [];
    +	var sIndent = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
    +	var tags = [];
    +	var ENUM = legacyContent ? "namespace" : "enum";
    +	var BASETYPE = legacyContent ? "baseType" : "extends";
    +	var PROPERTY = legacyContent ? "parameter" : "property";
    +	var unclosedStartTag = false;
    +
    +	function getAPIJSON(name) {
    +
    +		var symbol = lookup(name);
    +		if ( symbol && !symbol.synthetic ) {
    +			return createAPIJSON4Symbol(symbol, false);
    +		}
    +		if ( addRedundancy && externalSymbols[name] ) {
    +			debug("  using " + name + " from external dependency");
    +			return externalSymbols[name];
    +		}
    +		return symbol;
    +	}
    +
    +	function encode(s) {
    +		return s ? s.replace(/&/g, "&").replace(/ 0 ) {
    +			output.push(sIndent.slice(0,indent));
    +		}
    +		if ( arguments.length ) {
    +			for (var i = 0; i < arguments.length; i++) {
    +				output.push(arguments[i]);
    +			}
    +		}
    +		output.push("\n");
    +	}
    +
    +	function rootTag(name) {
    +		tags = [];
    +		unclosedStartTag = false;
    +		tag(name);
    +	}
    +
    +	function closeRootTag(name) {
    +		closeTag(name);
    +	}
    +
    +	function namespace(alias, namespace) {
    +		attrib(alias, namespace);
    +	}
    +
    +	function tag(name, value, omitEmpty) {
    +
    +		if ( omitEmpty && !value ) {
    +			return;
    +		}
    +		if ( unclosedStartTag ) {
    +			unclosedStartTag = false;
    +			write('>\n');
    +		}
    +		if ( arguments.length === 1 ) { // opening tag
    +			if ( indent > 0 ) {
    +				output.push(sIndent.slice(0,indent));
    +			}
    +			write("<", name);
    +			unclosedStartTag = true;
    +			if ( legacyContent ) {
    +				unclosedStartTag = false;
    +				write(">\n");
    +			}
    +			tags.push(name);
    +			indent++;
    +			return;
    +		}
    +		if ( value == null ) {
    +			writeln("<", name, "/>");
    +		} else {
    +			writeln("<", name, ">", encode(String(value)), "");
    +		}
    +	}
    +
    +	function attrib(name, value, defaultValue) {
    +		var emptyTag = arguments.length === 1;
    +		if ( omitDefaults && arguments.length === 3 && value === defaultValue ) {
    +			return;
    +		}
    +
    +		if ( !legacyContent ) {
    +			write(" " + name + "=\"");
    +			write(emptyTag ? "true" : encode(String(value)).replace(/"/g, """));
    +			write("\"");
    +		} else if ( emptyTag ) {
    +			writeln("<", name, "/>");
    +		} else {
    +			writeln("<", name, ">", encode(String(value)), "");
    +		}
    +	}
    +
    +	function closeTag(name, noIndent) {
    +
    +		indent--;
    +		var top = tags.pop();
    +		if ( top != name ) {
    +			// ERROR?
    +		}
    +
    +		if ( unclosedStartTag ) {
    +			unclosedStartTag = false;
    +			write("/>\n");
    +		} else if ( noIndent ) {
    +			write("\n");
    +		} else {
    +			writeln("");
    +		}
    +	}
    +
    +	function textContent(text) {
    +		if ( unclosedStartTag ) {
    +			unclosedStartTag = false;
    +			write('>');
    +		}
    +		write(encode(text));
    +	}
    +
    +	function tagWithSince(tagName, prop) {
    +		if ( prop ) {
    +			tag(tagName);
    +			if ( prop.since ) {
    +				attrib("since", prop.since);
    +			}
    +			if ( prop.text && prop.text.trim() ) {
    +				textContent(prop.text);
    +			}
    +			closeTag(tagName, true);
    +		}
    +	}
    +
    +	function getAsString() {
    +		return output.join("");
    +	}
    +
    +	function writeMetadata(symbolAPI, inherited) {
    +
    +		var ui5Metadata = symbolAPI["ui5-metadata"];
    +		if ( !ui5Metadata ) {
    +			return;
    +		}
    +
    +		if ( addRedundancy && symbolAPI["extends"] ) {
    +			var baseSymbolAPI = getAPIJSON(symbolAPI["extends"]);
    +			if ( baseSymbolAPI ) {
    +				writeMetadata(baseSymbolAPI, true);
    +			}
    +		}
    +
    +		if ( ui5Metadata.specialSettings ) {
    +			ui5Metadata.specialSettings.forEach(function(special) {
    +				tag("specialSetting");
    +				attrib("name", special.name);
    +				attrib("type", special.type);
    +				attrib("visibility", special.visibility, 'public');
    +				if ( special.since ) {
    +					attrib("since", special.since);
    +				}
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", special.description, true);
    +				tagWithSince("experimental", special.experimental);
    +				tagWithSince("deprecated", special.deprecated);
    +				tag("methods", special.methods);
    +				closeTag("specialSetting");
    +			});
    +		}
    +
    +		if ( ui5Metadata.properties ) {
    +			ui5Metadata.properties.forEach(function(prop) {
    +				tag("property");
    +				attrib("name", prop.name);
    +				attrib("type", prop.type, 'string');
    +				if ( prop.defaultValue !== null ) {
    +					attrib("defaultValue", prop.defaultValue, null);
    +				}
    +				attrib("visibility", prop.visibility, 'public');
    +				if ( prop.since ) {
    +					attrib("since", prop.since);
    +				}
    +				if ( prop.bindable ) {
    +					attrib("bindable", prop.bindable);
    +				}
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", prop.description, true);
    +				tagWithSince("experimental", prop.experimental);
    +				tagWithSince("deprecated", prop.deprecated);
    +				tag("methods", prop.methods);
    +				closeTag("property");
    +			});
    +		}
    +
    +		if ( ui5Metadata.defaultProperty ) {
    +			tag("defaultProperty", ui5Metadata.defaultProperty);
    +		}
    +
    +		if ( ui5Metadata.aggregations ) {
    +			ui5Metadata.aggregations.forEach(function(aggr) {
    +				tag("aggregation");
    +				attrib("name", aggr.name);
    +				attrib("singularName", aggr.singularName); // TODO omit default?
    +				attrib("type", aggr.type, 'sap.ui.core.Control');
    +				if ( aggr.altTypes ) {
    +					attrib("altTypes", aggr.altTypes.join(","));
    +				}
    +				attrib("cardinality", aggr.cardinality, '0..n');
    +				attrib("visibility", aggr.visibility, 'public');
    +				if ( aggr.since ) {
    +					attrib("since", aggr.since);
    +				}
    +				if ( aggr.bindable ) {
    +					attrib("bindable", aggr.bindable);
    +				}
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", aggr.description, true);
    +				tagWithSince("experimental", aggr.experimental);
    +				tagWithSince("deprecated", aggr.deprecated);
    +				tag("methods", aggr.methods);
    +				closeTag("aggregation");
    +			});
    +		}
    +
    +		if ( ui5Metadata.defaultAggregation ) {
    +			tag("defaultAggregation", ui5Metadata.defaultAggregation);
    +		}
    +
    +		if ( ui5Metadata.associations ) {
    +			ui5Metadata.associations.forEach(function(assoc) {
    +				tag("association");
    +				attrib("name", assoc.name);
    +				attrib("singularName", assoc.singularName); // TODO omit default?
    +				attrib("type", assoc.type, 'sap.ui.core.Control');
    +				attrib("cardinality", assoc.cardinality, '0..1');
    +				attrib("visibility", assoc.visibility, 'public');
    +				if ( assoc.since ) {
    +					attrib("since", assoc.since);
    +				}
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", assoc.description, true);
    +				tagWithSince("experimental", assoc.experimental);
    +				tagWithSince("deprecated", assoc.deprecated);
    +				tag("methods", assoc.methods);
    +				closeTag("association");
    +			});
    +		}
    +
    +		if ( ui5Metadata.events ) {
    +			ui5Metadata.events.forEach(function(event) {
    +				tag("event");
    +				attrib("name", event.name);
    +				attrib("visibility", event.visibility, 'public');
    +				if ( event.since ) {
    +					attrib("since", event.since);
    +				}
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", event.description, true);
    +				tagWithSince("experimental", event.experimental);
    +				tagWithSince("deprecated", event.deprecated);
    +				if ( event.parameters ) {
    +					tag("parameters");
    +					for ( var pn in event.parameters ) {
    +						if ( event.parameters.hasOwnProperty(pn) ) {
    +							var param = event.parameters[pn];
    +
    +							tag("parameter");
    +							attrib("name", param.name);
    +							attrib("type", param.type);
    +							if ( param.since ) {
    +								attrib("since", param.since);
    +							}
    +							tag("description", param.description, true);
    +							tagWithSince("experimental", param.experimental);
    +							tagWithSince("deprecated", param.deprecated);
    +							closeTag("parameter");
    +						}
    +					}
    +					closeTag("parameters");
    +				}
    +				tag("methods", event.methods, true);
    +				closeTag("event");
    +			});
    +		}
    +
    +		if ( ui5Metadata.annotations ) {
    +			ui5Metadata.annotations.forEach(function(anno) {
    +				tag("annotation");
    +				attrib("name", anno.name);
    +				attrib("namespace", anno.namespace); // TODO omit default?
    +				attrib("target", anno.target);
    +				attrib("annotation", anno.annotation);
    +				attrib("appliesTo", anno.appliesTo);
    +				if ( anno.since ) {
    +					attrib("since", anno.since);
    +				}
    +				tag("description", anno.description, true);
    +				tagWithSince("deprecated", anno.deprecated);
    +				closeTag("annotation");
    +			});
    +		}
    +
    +	}
    +
    +	function writeParameterPropertiesForMSettings(symbolAPI, inherited) {
    +
    +		var ui5Metadata = symbolAPI["ui5-metadata"];
    +		if ( !ui5Metadata ) {
    +			return;
    +		}
    +
    +		if ( symbolAPI["extends"] ) {
    +			var baseSymbolAPI = getAPIJSON(symbolAPI["extends"]);
    +			writeParameterPropertiesForMSettings(baseSymbolAPI, true);
    +		}
    +
    +		if ( ui5Metadata.specialSettings ) {
    +			ui5Metadata.specialSettings.forEach(function(special) {
    +				if ( special.visibility !== 'hidden' ) {
    +					tag("property");
    +					attrib("name", special.name);
    +					attrib("type", special.type);
    +					attrib("optional");
    +					if ( inherited ) {
    +						attrib("origin", symbolAPI.name);
    +					}
    +					tag("description", special.description, true);
    +					closeTag("property");
    +				}
    +			});
    +		}
    +
    +		if ( ui5Metadata.properties ) {
    +			ui5Metadata.properties.forEach(function(prop) {
    +				tag("property");
    +				attrib("name", prop.name);
    +				attrib("type", prop.type);
    +				attrib("group", prop.group, 'Misc');
    +				if ( prop.defaultValue !== null ) {
    +					attrib("defaultValue", typeof prop.defaultValue === 'string' ? "\"" + prop.defaultValue + "\"" : prop.defaultValue);
    +				}
    +				attrib("optional");
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", prop.description, true);
    +				closeTag("property");
    +			});
    +		}
    +
    +		if ( ui5Metadata.aggregations ) {
    +			ui5Metadata.aggregations.forEach(function(aggr) {
    +				if ( aggr.visibility !== "hidden" ) {
    +					tag("property");
    +					attrib("name", aggr.name);
    +					attrib("type", aggr.type + (aggr.cardinality === '0..1' ? "" : "[]"));
    +					if ( aggr.altTypes ) {
    +						attrib("altTypes", aggr.altTypes.join(","));
    +					}
    +					attrib("optional");
    +					if ( inherited ) {
    +						attrib("origin", symbolAPI.name);
    +					}
    +					tag("description", aggr.description, true);
    +					closeTag("property");
    +				}
    +			});
    +		}
    +
    +		if ( ui5Metadata.associations ) {
    +			ui5Metadata.associations.forEach(function(assoc) {
    +				if ( assoc.visibility !== "hidden" ) {
    +					tag("property");
    +					attrib("name", assoc.name);
    +					attrib("type", "(" + assoc.type + "|" + "string)" + (assoc.cardinality === '0..1' ? "" : "[]"));
    +					attrib("optional");
    +					if ( inherited ) {
    +						attrib("origin", symbolAPI.name);
    +					}
    +					tag("description", assoc.description, true);
    +					closeTag("property");
    +				}
    +			});
    +		}
    +
    +		if ( ui5Metadata.events ) {
    +			ui5Metadata.events.forEach(function(event) {
    +				tag("property");
    +				attrib("name", event.name);
    +				attrib("type", "function|array");
    +				attrib("optional");
    +				if ( inherited ) {
    +					attrib("origin", symbolAPI.name);
    +				}
    +				tag("description", event.description, true);
    +				closeTag("property");
    +			});
    +		}
    +
    +	}
    +
    +	function writeParameterProperties(param, paramName) {
    +		var props = param.parameterProperties,
    +			prefix = paramName + '.',
    +			count = 0;
    +
    +		if ( props ) {
    +			for (var n in props ) {
    +				if ( props.hasOwnProperty(n) ) {
    +
    +					param = props[n];
    +
    +					if ( !legacyContent && count === 0 ) {
    +						tag("parameterProperties");
    +					}
    +
    +					count++;
    +
    +					tag(PROPERTY);
    +					attrib("name", legacyContent ? prefix + n : n);
    +					attrib("type", param.type);
    +					if ( param.since ) {
    +						attrib("since", param.since);
    +					}
    +					if ( param.optional ) {
    +						attrib("optional", param.optional);
    +					}
    +
    +					if ( !legacyContent ) {
    +						writeParameterProperties(param, prefix + n);
    +					}
    +
    +					tag("description", param.description, true);
    +					tagWithSince("experimental", param.experimental);
    +					tagWithSince("deprecated", param.deprecated);
    +
    +					closeTag(PROPERTY);
    +
    +					if ( legacyContent ) {
    +						writeParameterProperties(param, prefix + n);
    +					}
    +				}
    +			}
    +		}
    +
    +		if ( !legacyContent && count > 0 ) {
    +			closeTag("parameterProperties");
    +		}
    +	}
    +
    +	/*
    +	var rSplitSecTag = /^\s*\{([^\}]*)\}/;
    +
    +	function secTags($) {
    +		if ( !legacyContent ) {
    +			return;
    +		}
    +		var aTags = $.tags;
    +		if ( !aTags ) {
    +			return;
    +		}
    +		for (var iTag = 0; iTag < A_SECURITY_TAGS.length; iTag++  ) {
    +			var oTagDef = A_SECURITY_TAGS[iTag];
    +			for (var j = 0; j < aTags.length; j++ ) {
    +				if ( aTags[j].title.toLowerCase() === oTagDef.name.toLowerCase() ) {
    +					tag(oTagDef.name);
    +					var m = rSplitSecTag.exec(aTags[j].text);
    +					if ( m && m[1].trim() ) {
    +						var aParams = m[1].trim().split(/\s*\|\s* /); <-- remove the blank!
    +						for (var iParam = 0; iParam < aParams.length; iParam++ ) {
    +							tag(oTagDef.params[iParam], aParams[iParam]);
    +						}
    +					}
    +					var sDesc = aTags[j].description;
    +					tag("description", sDesc, true);
    +					closeTag(oTagDef.name);
    +				}
    +			}
    +		}
    +	}
    +	*/
    +
    +	function writeSymbol(symbol) {
    +
    +		var kind;
    +
    +		if ( isFirstClassSymbol(symbol) && (roots || !symbol.synthetic) ) { // dump a symbol if it as a class symbol and if either hierarchies are dumped or if it is not a synthetic symbol
    +
    +			// for the hierarchy we use only the local information
    +			var symbolAPI = createAPIJSON4Symbol(symbol);
    +
    +			kind = symbolAPI.kind === 'enum' ? ENUM : symbolAPI.kind;
    +
    +			tag(kind);
    +
    +			attrib("name", symbolAPI.name);
    +			attrib("basename", symbolAPI.basename);
    +//			if ( symbolAPI["resource"] ) {
    +//				attrib("resource");
    +//			}
    +			if ( symbolAPI["module"] ) {
    +				attrib("module", symbolAPI["module"]);
    +			}
    +			if ( symbolAPI["abstract"] ) {
    +				attrib("abstract");
    +			}
    +			if ( symbolAPI["final"] ) {
    +				attrib("final");
    +			}
    +			if ( symbolAPI["static"] ) {
    +				attrib("static");
    +			}
    +			attrib("visibility", symbolAPI.visibility, 'public');
    +			if ( symbolAPI.since ) {
    +				attrib("since", symbolAPI.since);
    +			}
    +			if ( symbolAPI["extends"] ) {
    +				tag(BASETYPE, symbolAPI["extends"]); // TODO what about multiple inheritance?
    +			}
    +			tag("description", symbolAPI.description, true);
    +			tagWithSince("experimental", symbolAPI.experimental);
    +			tagWithSince("deprecated", symbolAPI.deprecated);
    +
    +			if ( kind === 'class' ) {
    +
    +				var hasSettings = symbolAPI["ui5-metadata"];
    +
    +				if ( !legacyContent && symbolAPI["ui5-metadata"] ) {
    +
    +					tag("ui5-metadata");
    +
    +					if ( symbolAPI["ui5-metadata"].stereotype ) {
    +						attrib("stereotype", symbolAPI["ui5-metadata"].stereotype);
    +					}
    +
    +					writeMetadata(symbolAPI);
    +
    +					closeTag("ui5-metadata");
    +
    +				}
    +
    +				tag("constructor");
    +				if ( legacyContent ) {
    +					attrib("name", symbolAPI.basename);
    +				}
    +				attrib("visibility", symbolAPI.visibility, 'public');
    +				if ( symbolAPI.constructor.parameters ) {
    +					symbolAPI.constructor.parameters.forEach(function(param, j) {
    +
    +						tag("parameter");
    +						attrib("name", param.name);
    +						attrib("type", param.type);
    +						attrib("optional", param.optional, false);
    +						if ( param.defaultValue !== undefined ) {
    +							attrib("defaultValue", param.defaultValue);
    +						}
    +						if ( param.since ) {
    +							attrib("since", param.since);
    +						}
    +
    +						if ( !legacyContent ) {
    +							if ( hasSettings && j == 1 && /setting/i.test(param.name) && /object/i.test(param.type) ) {
    +								if ( addRedundancy ) {
    +									tag("parameterProperties");
    +									writeParameterPropertiesForMSettings(symbolAPI);
    +									closeTag("parameterProperties");
    +								}
    +							} else {
    +								writeParameterProperties(param, param.name);
    +							}
    +						}
    +						tag("description", param.description, true);
    +						tagWithSince("experimental", param.experimental);
    +						tagWithSince("deprecated", param.deprecated);
    +						closeTag("parameter");
    +						if ( legacyContent ) {
    +							writeParameterProperties(param, param.name);
    +						}
    +					});
    +				}
    +
    +				tag("description", getConstructorDescription(symbol), true);
    +				// tagWithSince("experimental", symbol.experimental); // TODO repeat from class?
    +				// tagWithSince("deprecated", symbol.deprecated); // TODO repeat from class?
    +				// secTags(symbol); // TODO repeat from class?
    +				closeTag("constructor");
    +			}
    +
    +			/* TODO MIGRATE or remove, if not needed
    +			var ownSubspaces = ( symbol.__ui5.children || [] ).filter(function($) { return $.kind === 'namespace' }).sort(sortByAlias);
    +			for (var i=0; i");
    +	rootTag("api");
    +	if ( !legacyContent ) {
    +		namespace("xmlns", "http://www.sap.com/sap.ui.library.api.xsd");
    +		attrib("_version", "1.0.0");
    +		if ( templateConf.version ) {
    +			attrib("version", templateConf.version.replace(/-SNAPSHOT$/,""));
    +		}
    +		if ( templateConf.uilib ) {
    +			attrib("library", templateConf.uilib);
    +		}
    +	}
    +
    +	if ( roots ) {
    +		roots.forEach(writeSymbol);
    +	} else {
    +		// sort only a copy(!) of the symbols, otherwise the SymbolSet lookup is broken
    +		symbols.slice(0).sort(sortByAlias).forEach(writeSymbol);
    +	}
    +
    +	closeRootTag("api");
    +
    +	fs.mkPath(path.dirname(filename));
    +	fs.writeFileSync(filename, getAsString(), 'utf8');
    +}
    +
    +//---- add on: API JS -----------------------------------------------------------------
    +
    +function createAPIJS(symbols, filename) {
    +
    +	var output = [];
    +
    +	var rkeywords = /^(?:abstract|as|boolean|break|byte|case|catch|char|class|continue|const|debugger|default|delete|do|double|else|enum|export|extends|false|final|finally|float|for|function|goto|if|implements|import|in|instanceof|int|interface|is|long|namespace|native|new|null|package|private|protected|public|return|short|static|super|switch|synchronized|this|throw|throws|transient|true|try|typeof|use|var|void|volatile|while|with)$/;
    +
    +	function isNoKeyword($) { return !rkeywords.test($.name); }
    +
    +	function isAPI($) { return $.access === 'public' || $.access === 'protected' || !$.access; }
    +
    +	function writeln(args) {
    +		if ( arguments.length ) {
    +			for (var i = 0; i < arguments.length; i++) {
    +				output.push(arguments[i]);
    +			}
    +		}
    +		output.push("\n");
    +	}
    +
    +	function unwrap(docletSrc) {
    +		if (!docletSrc) { return ''; }
    +
    +		// note: keep trailing whitespace for @examples
    +		// extra opening/closing stars are ignored
    +		// left margin is considered a star and a space
    +		// use the /m flag on regex to avoid having to guess what this platform's newline is
    +		docletSrc =
    +			docletSrc.replace(/^\/\*\*+/, '') // remove opening slash+stars
    +			.replace(/\**\*\/$/, "\\Z")       // replace closing star slash with end-marker
    +			.replace(/^\s*(\* ?|\\Z)/gm, '')  // remove left margin like: spaces+star or spaces+end-marker
    +			.replace(/\s*\\Z$/g, '');         // remove end-marker
    +
    +		return docletSrc;
    +	}
    +
    +	function comment($, sMetaType) {
    +
    +		var s = unwrap($.comment.toString());
    +
    +		// remove the @desc tag
    +		s = s.replace(/(\r\n|\r|\n)/gm, "\n");
    +		s = s.replace(/^\s*@desc\s*/gm, "");
    +		s = s.replace(/^\s*@alias[^\r\n]*(\r\n|\r|\n)?/gm, "");
    +		s = s.replace(/^\s*@name[^\r\n]*(\r\n|\r|\n)?/gm, "");
    +		s = s.replace(/^\s*@function[^\r\n]*(\r\n|\r|\n)?/gm, "");
    +		s = s.replace(/^\s*@author[^\r\n]*(\r\n|\r|\n)?/gm, "");
    +		s = s.replace(/^\s*@synthetic[^\r\n]*(\r\n|\r|\n)?/gm, "");
    +		s = s.replace(/^\s*<\/p>

    \s*(\r\n|\r|\n)?/gm, "\n"); + // skip empty documentation + if ( !s ) { + return; + } + + // for namespaces, enforce the @.memberof tag + if ( sMetaType === "namespace" && $.memberof && s.indexOf("@memberof") < 0 ) { + s = s + "\n@memberof " + $.memberof; + } + + writeln("/**\n * " + s.replace(/\n/g, "\n * ") + "\n */"); + + /* + writeln("/**"); + writeln(s.split(/\r\n|\r|\n/g).map(function($) { return " * " + $;}).join("\r\n")); + writeln(" * /"); + */ + + } + + function signature($) { + var p = $.params, + r = [], + i; + if ( p ) { + for (i = 0; i < p.length; i++) { + // ignore @param tags for 'virtual' params that are used to document members of config-like params + // (e.g. like "@param param1.key ...") + if (p[i].name && p[i].name.indexOf('.') < 0) { + r.push(p[i].name); + } + } + } + return r.join(','); + } + + function qname(member,parent) { + var r = member.memberof; + if ( member.scope !== 'static' ) { + r += ".prototype"; + } + return (r ? r + "." : "") + member.name; + } + + var mValues = { + "boolean" : "false", + "int" : "0", + "float" : "0.0", + "number" : "0.0", + "string" : "\"\"", + "object" : "new Object()", + "function" : "function() {}" + }; + + function valueForType(type) { + if ( type && type.names && type.names[0] ) { + type = type.names[0]; + if ( REGEXP_ARRAY_TYPE.test(type) || type.indexOf("[]") > 0 ) { + return "new Array()"; + } else if ( mValues[type] ) { + return mValues[type]; + } else if ( type.indexOf(".") > 0 ) { + return "new " + type + "()"; + } else { + // return "/* unsupported type: " + member.type + " */ null"; + return "null"; + } + } + } + + function value(member) { + return valueForType(member.type); + } + + function retvalue(member) { + //debug(member); + var r = valueForType(member.type || (member.returns && member.returns.length && member.returns[0] && member.returns[0].type && member.returns[0].type)); + if ( r ) { + return "return " + r + ";"; + } + return ""; + } + + var sortedSymbols = symbols.slice(0).filter(function($) { return isaClass($) && isAPI($) && !$.synthetic; }).sort(sortByAlias); // sort only a copy(!) of the symbols, otherwise the SymbolSet lookup is broken + sortedSymbols.forEach(function(symbol) { + + var sMetaType = (symbol.kind === 'member' && symbol.isEnum) ? 'enum' : symbol.kind; + if ( sMetaType ) { + + writeln(""); + writeln("// ---- " + symbol.longname + " --------------------------------------------------------------------------"); + writeln(""); + + var memberId, member; + + var ownProperties = childrenOfKind(symbol, 'property').own.filter(isNoKeyword).sort(sortByAlias); + if ( sMetaType === "class" ) { + comment(symbol, sMetaType); + writeln(symbol.longname + " = function(" + signature(symbol) + ") {};"); + for ( memberId in ownProperties ) { + member = ownProperties[memberId]; + comment(member, sMetaType); + writeln(qname(member, symbol) + " = " + value(member)); + writeln(""); + } + } else if ( sMetaType === 'namespace' || sMetaType === 'enum' ) { + //debug("found namespace " + symbol.longname); + //debug(ownProperties); + if ( ownProperties.length ) { + writeln("// dummy function to make Eclipse aware of namespace"); + writeln(symbol.longname + ".toString = function() { return \"\"; };"); + } + } + + var ownEvents = childrenOfKind(symbol, 'event').own.filter(isNoKeyword).sort(sortByAlias); + if ( ownEvents.length ) { + for ( memberId in ownEvents ) { + member = ownEvents[memberId]; + comment(member, sMetaType); + writeln(qname(member, symbol) + " = function(" + signature(member) + ") { " + retvalue(member) + " };"); + writeln(""); + } + } + + var ownMethods = childrenOfKind(symbol, 'method').own.filter(isNoKeyword).sort(sortByAlias); + if ( ownMethods.length ) { + for ( memberId in ownMethods ) { + member = ownMethods[memberId]; + comment(member, sMetaType); + writeln(qname(member, symbol) + " = function(" + signature(member) + ") { " + retvalue(member) + " };"); + writeln(""); + } + } + + } + }); + + writeln("// ---- static fields of namespaces ---------------------------------------------------------------------"); + + sortedSymbols.forEach(function(symbol) { + + var sMetaType = (symbol.kind === 'member' && symbol.isEnum) ? 'enum' : symbol.kind; + + if ( sMetaType === 'namespace' || sMetaType === 'enum' ) { + + var ownProperties = childrenOfKind(symbol, 'property').own.filter(isNoKeyword).sort(sortByAlias); + if ( ownProperties.length ) { + writeln(""); + writeln("// ---- " + symbol.longname + " --------------------------------------------------------------------------"); + writeln(""); + + for (var memberId in ownProperties ) { + var member = ownProperties[memberId]; + comment(member, sMetaType); + writeln(qname(member, symbol) + " = " + value(member) + ";"); + writeln(""); + } + } + } + + }); + + fs.mkPath(path.dirname(filename)); + fs.writeFileSync(filename, output.join(""), 'utf8'); + info(" saved as " + filename); +} + +// Description + Settings + +function getConstructorDescription(symbol) { + var description = symbol.description; + var tags = symbol.tags; + if ( tags ) { + for (var i = 0; i < tags.length; i++) { + if ( tags[i].title === "ui5-settings" && tags[i].text) { + description += "\n

    \n" + tags[i].text; + break; + } + } + } + return description; +} + + +// Example + +function makeExample(example) { + var result = { + caption: null, + example: example + }, + match = /^\s*([\s\S]+?)<\/caption>(?:[ \t]*[\n\r]*)([\s\S]+)$/i.exec(example); + + if ( match ) { + result.caption = match[1]; + result.example = match[2]; + } + + return result; +} + +/* ---- exports ---- */ + +exports.publish = publish; + diff --git a/lib/processors/jsdoc/sdkTransformer.js b/lib/processors/jsdoc/sdkTransformer.js new file mode 100644 index 000000000..0dbb98dd2 --- /dev/null +++ b/lib/processors/jsdoc/sdkTransformer.js @@ -0,0 +1,39 @@ +const resourceFactory = require("@ui5/fs").resourceFactory; +const transformer = require("./lib/transform-apijson-for-sdk"); + +/** + * Transform api.json as created by [jsdocGenerator]{@link module:@ui5/builder.processors.jsdocGenerator} + * for usage in a UI5 SDK + * + * @public + * @alias module:@ui5/builder.processors.sdkTransformer + * @param {Object} parameters Parameters + * @param {string} parameters.apiJsonPath Path to the projects api.json file as created by + * [jsdocGenerator]{@link module:@ui5/builder.processors.jsdoc.jsdocGenerator} + * @param {string} parameters.dotLibraryPath Path to the projects .library file + * @param {string[]} parameters.dependencyApiJsonPaths List of paths to the api.json files of all dependencies of + * the project as created by [jsdocGenerator]{@link module:@ui5/builder.processors.jsdoc.jsdocGenerator} + * @param {string} parameters.targetApiJsonPath Path to create the new, transformed api.json resource for + * @param {fs|module:@ui5/fs.fsInterface} parameters.fs Node fs or + * custom [fs interface]{@link module:resources/module:@ui5/fs.fsInterface} to use + * + * @returns {Promise} Promise resolving with created resources + */ +const sdkTransformer = async function({ + apiJsonPath, dotLibraryPath, dependencyApiJsonPaths, targetApiJsonPath, fs} = {} +) { + if (!apiJsonPath || !dotLibraryPath || !targetApiJsonPath || !dependencyApiJsonPaths || !fs) { + throw new Error("[sdkTransformer]: One or more mandatory parameters not provided"); + } + const fakeTargetPath = "/ignore/this/path/resource/will/be/returned"; + const apiJsonContent = await transformer(apiJsonPath, fakeTargetPath, dotLibraryPath, dependencyApiJsonPaths, { + fs, + returnOutputFiles: true + }); + return [resourceFactory.createResource({ + path: targetApiJsonPath, + string: apiJsonContent + })]; +}; + +module.exports = sdkTransformer; diff --git a/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/lib/tasks/jsdoc/executeJsdocSdkTransformation.js new file mode 100644 index 000000000..55226db7d --- /dev/null +++ b/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -0,0 +1,74 @@ +const log = require("@ui5/logger").getLogger("builder:tasks:jsdoc:executeJsdocSdkTransformation"); +const ReaderCollectionPrioritized = require("@ui5/fs").ReaderCollectionPrioritized; +const fsInterface = require("@ui5/fs").fsInterface; +const sdkTransformer = require("../../processors/jsdoc/sdkTransformer"); + +/** + * Task to transform the api.json file as created by the + * [generateJsdoc]{@link module:@ui5/builder.tasks.generateJsdoc} task into a pre-processed api.json + * file suitable for the SDK. + * + * @public + * @alias module:@ui5/builder.tasks.executeJsdocSdkTransformation + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files + * @param {Object} parameters.options Options + * @param {string|Array} parameters.options.dotLibraryPattern Pattern to locate the .library resource to be processed + * @param {string} parameters.options.projectName Project name + * @returns {Promise} Promise resolving with undefined once data has been written + */ +const executeJsdocSdkTransformation = async function({workspace, dependencies, options} = {}) { + if (!options || !options.projectName || !options.dotLibraryPattern) { + throw new Error("[executeJsdocSdkTransformation]: One or more mandatory options not provided"); + } + + const [apiJsons, dotLibraries, depApiJsons] = await Promise.all([ + workspace.byGlob("/test-resources/**/designtime/api.json"), + workspace.byGlob(options.dotLibraryPattern), + dependencies.byGlob("/test-resources/**/designtime/api.json") + ]); + if (!apiJsons.length) { + log.info(`Failed to locate api.json resource for project ${options.projectName}. ` + + `Skipping SDK Transformation...`); + return; + } else if (apiJsons.length > 1) { + throw new Error(`[executeJsdocSdkTransformation]: Found more than one api.json resources for project ` + + `${options.projectName}.`); + } + if (!dotLibraries.length) { + throw new Error(`[executeJsdocSdkTransformation]: Failed to locate .library resource for project ` + + `${options.projectName}.`); + } else if (dotLibraries.length > 1) { + throw new Error(`[executeJsdocSdkTransformation]: Found more than one .library resources for project ` + + `${options.projectName}.`); + } + + const combo = new ReaderCollectionPrioritized({ + name: `executeJsdocSdkTransformation - custom workspace + dependencies FS: ${options.projectName}`, + readers: [workspace, dependencies] + }); + + const apiJsonPath = apiJsons[0].getPath(); + const dotLibraryPath = dotLibraries[0].getPath(); + const dependencyApiJsonPaths = depApiJsons.map((res) => { + return res.getPath(); + }); + + // Target path is typically "/test-resources/${options.namespace}/designtime/apiref/api.json" + const targetApiJsonPath = apiJsonPath.replace(/\/api\.json$/i, "/apiref/api.json"); + + const createdResources = await sdkTransformer({ + apiJsonPath, + dotLibraryPath, + dependencyApiJsonPaths, + targetApiJsonPath, + fs: fsInterface(combo) + }); + + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); +}; + +module.exports = executeJsdocSdkTransformation; diff --git a/lib/tasks/jsdoc/generateApiIndex.js b/lib/tasks/jsdoc/generateApiIndex.js new file mode 100644 index 000000000..005068ec0 --- /dev/null +++ b/lib/tasks/jsdoc/generateApiIndex.js @@ -0,0 +1,49 @@ +const ui5Fs = require("@ui5/fs"); +const ReaderCollectionPrioritized = ui5Fs.ReaderCollectionPrioritized; +const fsInterface = ui5Fs.fsInterface; +const apiIndexGenerator = require("../../processors/jsdoc/apiIndexGenerator"); + +/** + * Compiles an api-index.json resource from all available api.json resources as created by the + * [executeJsdocSdkTransformation]{@link module:@ui5/builder.tasks.executeJsdocSdkTransformation} task. + * The resulting api-index.json resource is mainly to be used in the SDK. + * + * @public + * @alias module:@ui5/builder.tasks.generateApiIndex + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files + * @param {Object} parameters.options Options + * @param {string} parameters.options.projectName Project name + * @returns {Promise} Promise resolving with undefined once data has been written + */ +module.exports = async function({workspace, dependencies, options} = {}) { + if (!options || !options.projectName) { + throw new Error("[generateApiIndex]: One or more mandatory options not provided"); + } + const combo = new ReaderCollectionPrioritized({ + name: `generateApiIndex - workspace + dependencies: ${options.projectName}`, + readers: [workspace, dependencies] + }); + + const versionInfoPath = "/resources/sap-ui-version.json"; + const testResourcesRootPath = "/test-resources"; + const targetApiIndexPath = "/docs/api/api-index.json"; + const targetApiIndexDeprecatedPath = "/docs/api/api-index-deprecated.json"; + const targetApiIndexExperimentalPath = "/docs/api/api-index-experimental.json"; + const targetApiIndexSincePath = "/docs/api/api-index-since.json"; + + const createdResources = await apiIndexGenerator({ + versionInfoPath, + testResourcesRootPath, + targetApiIndexPath, + targetApiIndexDeprecatedPath, + targetApiIndexExperimentalPath, + targetApiIndexSincePath, + fs: fsInterface(combo), + }); + + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); +}; diff --git a/lib/tasks/jsdoc/generateJsdoc.js b/lib/tasks/jsdoc/generateJsdoc.js new file mode 100644 index 000000000..0c807fdee --- /dev/null +++ b/lib/tasks/jsdoc/generateJsdoc.js @@ -0,0 +1,181 @@ +const log = require("@ui5/logger").getLogger("builder:tasks:jsdoc:generateJsdoc"); +const path = require("path"); +const makeDir = require("make-dir"); +const fs = require("graceful-fs"); +const tmp = require("tmp"); +tmp.setGracefulCleanup(); +const jsdocGenerator = require("../../processors/jsdoc/jsdocGenerator"); +const {resourceFactory} = require("@ui5/fs"); + +/** + * Task to execute a JSDoc build for UI5 projects + * + * @public + * @alias module:@ui5/builder.tasks.generateJsdoc + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files + * @param {Object} parameters.options Options + * @param {string|Array} parameters.options.pattern Pattern to locate the files to be processed + * @param {string} parameters.options.projectName Project name + * @param {string} parameters.options.namespace Namespace to build (e.g. some/project/name) + * @param {string} parameters.options.version Project version + * @returns {Promise} Promise resolving with undefined once data has been written + */ +const generateJsdoc = async function({workspace, dependencies, options} = {}) { + if (!options || !options.projectName || !options.namespace || !options.version || !options.pattern) { + throw new Error("[generateJsdoc]: One or more mandatory options not provided"); + } + + const {sourcePath: resourcePath, targetPath, tmpPath} = await generateJsdoc._createTmpDirs(options.projectName); + + const [writtenResourcesCount] = await Promise.all([ + generateJsdoc._writeResourcesToDir({ + workspace, + pattern: options.pattern, + targetPath: resourcePath + }), + generateJsdoc._writeDependencyApisToDir({ + dependencies, + targetPath: path.posix.join(tmpPath, "dependency-apis") + }) + ]); + + if (writtenResourcesCount === 0) { + log.info(`Failed to find any input resources for project ${options.projectName} using pattern ` + + `${options.pattern}. Skipping JSDoc generation...`); + return; + } + + const createdResources = await jsdocGenerator({ + sourcePath: resourcePath, + targetPath, + tmpPath, + options: { + projectName: options.projectName, + namespace: options.namespace, + version: options.version, + variants: ["apijson"] + } + }); + + await Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); +}; + + +/** + * Create temporary directories for JSDoc generation processor + * + * @private + * @param {string} projectName Project name used for naming the temporary working directory + * @returns {Promise} Promise resolving with sourcePath, targetPath and tmpPath strings + */ +async function createTmpDirs(projectName) { + const {path: tmpDirPath} = await createTmpDir(projectName); + + const sourcePath = path.join(tmpDirPath, "src"); // dir will be created by writing project resources below + await makeDir(sourcePath, {fs}); + const targetPath = path.join(tmpDirPath, "target"); // dir will be created by jsdoc itself + await makeDir(targetPath, {fs}); + + const tmpPath = path.join(tmpDirPath, "tmp"); // dir needs to be created by us + await makeDir(tmpPath, {fs}); + + return { + sourcePath, + targetPath, + tmpPath + }; +} + +/** + * Create a temporary directory on the host system + * + * @private + * @param {string} projectName Project name used for naming the temporary directory + * @param {boolean} [keep=false] Whether to keep the temporary directory + * @returns {Promise} Promise resolving with path of the temporary directory + */ +function createTmpDir(projectName, keep = false) { + // Remove all non alpha-num characters from project name + const sanitizedProjectName = projectName.replace(/[^A-Za-z0-9]/g, ""); + return new Promise((resolve, reject) => { + tmp.dir({ + prefix: `ui5-tooling-tmp-jsdoc-${sanitizedProjectName}-`, + keep, + unsafeCleanup: true + }, (err, path) => { + if (err) { + reject(err); + return; + } + + resolve({ + path + }); + }); + }); +} + +/** + * Write resources from workspace matching the given pattern to the given fs destination + * + * @private + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {string} parameters.pattern Pattern to match resources in workspace against + * @param {string} parameters.targetPath Path to write the resources to + * @returns {Promise} Promise resolving with number of resources written to given directory + */ +async function writeResourcesToDir({workspace, pattern, targetPath}) { + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: targetPath, + virBasePath: "/resources/" + }); + + let allResources; + if (workspace.byGlobSource) { // API only available on duplex collections + allResources = await workspace.byGlobSource(pattern); + } else { + allResources = await workspace.byGlob(pattern); + } + + // write all resources to the tmp folder + await Promise.all(allResources.map((resource) => fsTarget.write(resource))); + return allResources.length; +} + +/** + * Write api.json files of dependencies to given target path in a flat structure + * + * @private + * @param {Object} parameters Parameters + * @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files + * @param {string} parameters.targetPath Path to write the resources to + * @returns {Promise} Promise resolving with number of resources written to given directory + */ +async function writeDependencyApisToDir({dependencies, targetPath}) { + const depApis = await dependencies.byGlob("/test-resources/**/designtime/api.json"); + + // Clone resources before changing their path + const apis = await Promise.all(depApis.map((resource) => resource.clone())); + + for (let i = 0; i < apis.length; i++) { + apis[i].setPath(`/api-${i}.json`); + } + + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: targetPath, + virBasePath: "/" + }); + await Promise.all(apis.map((resource) => fsTarget.write(resource))); + return apis.length; +} + +module.exports = generateJsdoc; +module.exports._createTmpDirs = createTmpDirs; +module.exports._createTmpDir = createTmpDir; +module.exports._writeResourcesToDir = writeResourcesToDir; +module.exports._writeDependencyApisToDir = writeDependencyApisToDir; diff --git a/lib/tasks/taskRepository.js b/lib/tasks/taskRepository.js index 2b1285c5b..d510e2459 100644 --- a/lib/tasks/taskRepository.js +++ b/lib/tasks/taskRepository.js @@ -2,6 +2,9 @@ const tasks = { replaceCopyright: require("./replaceCopyright"), replaceVersion: require("./replaceVersion"), createDebugFiles: require("./createDebugFiles"), + executeJsdocSdkTransformation: require("./jsdoc/executeJsdocSdkTransformation"), + generateApiIndex: require("./jsdoc/generateApiIndex"), + generateJsdoc: require("./jsdoc/generateJsdoc"), uglify: require("./uglify"), buildThemes: require("./buildThemes"), transformBootstrapHtml: require("./transformBootstrapHtml"), diff --git a/lib/types/application/ApplicationBuilder.js b/lib/types/application/ApplicationBuilder.js index b4dbdd764..c5bd612d1 100644 --- a/lib/types/application/ApplicationBuilder.js +++ b/lib/types/application/ApplicationBuilder.js @@ -13,7 +13,8 @@ const tasks = { // can't require index.js due to circular dependency replaceCopyright: require("../../tasks/replaceCopyright"), replaceVersion: require("../../tasks/replaceVersion"), transformBootstrapHtml: require("../../tasks/transformBootstrapHtml"), - uglify: require("../../tasks/uglify") + uglify: require("../../tasks/uglify"), + generateApiIndex: require("../../tasks/jsdoc/generateApiIndex") }; class ApplicationBuilder extends AbstractBuilder { @@ -159,6 +160,16 @@ class ApplicationBuilder extends AbstractBuilder { } }); }); + + this.addTask("generateApiIndex", () => { + return tasks.generateApiIndex({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name + } + }); + }); } } diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index 98fb709b4..94c253c30 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -8,6 +8,8 @@ const tasks = { // can't require index.js due to circular dependency generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"), buildThemes: require("../../tasks/buildThemes"), createDebugFiles: require("../../tasks/createDebugFiles"), + generateJsdoc: require("../../tasks/jsdoc/generateJsdoc"), + executeJsdocSdkTransformation: require("../../tasks/jsdoc/executeJsdocSdkTransformation"), generateLibraryManifest: require("../../tasks/generateLibraryManifest"), generateVersionInfo: require("../../tasks/generateVersionInfo"), replaceCopyright: require("../../tasks/replaceCopyright"), @@ -39,6 +41,45 @@ class LibraryBuilder extends AbstractBuilder { }); }); + this.addTask("generateJsdoc", () => { + const generateJsdoc = tasks.generateJsdoc; + + const patterns = ["/resources/**/*.js"]; + // Add excludes + if (project.builder && project.builder.jsdoc && project.builder.jsdoc.excludes) { + const excludes = project.builder.jsdoc.excludes.map((pattern) => { + return `!/resources/${pattern}`; + }); + + patterns.push(...excludes); + } + const namespace = project.metadata.name.replace(/\./g, "/"); + + return generateJsdoc({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name, + namespace, + version: project.version, + pattern: patterns + } + }); + }); + + this.addTask("executeJsdocSdkTransformation", () => { + const executeJsdocSdkTransformation = tasks.executeJsdocSdkTransformation; + + return executeJsdocSdkTransformation({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name, + dotLibraryPattern: "/resources/**/*.library", + } + }); + }); + const componentPreload = project.builder && project.builder.componentPreload; if (componentPreload) { const generateComponentPreload = tasks.generateComponentPreload; diff --git a/package-lock.json b/package-lock.json index bf9846c57..3cbc3945a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -331,6 +331,16 @@ "minimatch": "^3.0.3", "pretty-hrtime": "^1.0.3", "random-int": "^1.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + } + } } }, "@ui5/logger": { @@ -790,6 +800,23 @@ "pinkie-promise": "^2.0.0" } }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -1376,8 +1403,7 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, "boolbase": { "version": "1.0.0", @@ -1616,7 +1642,6 @@ "version": "0.8.9", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=", - "dev": true, "requires": { "underscore-contrib": "~0.3.0" } @@ -1979,6 +2004,17 @@ "unique-string": "^1.0.0", "write-file-atomic": "^2.0.0", "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + } } }, "console-control-strings": { @@ -2921,6 +2957,17 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, "extglob": { @@ -3100,6 +3147,17 @@ "commondir": "^1.0.1", "make-dir": "^1.0.0", "pkg-dir": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + } } }, "find-up": { @@ -4596,7 +4654,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", - "dev": true, "requires": { "xmlcreate": "^1.0.1" } @@ -4611,7 +4668,6 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", - "dev": true, "requires": { "babylon": "7.0.0-beta.19", "bluebird": "~3.5.0", @@ -4630,14 +4686,12 @@ "babylon": { "version": "7.0.0-beta.19", "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", - "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==", - "dev": true + "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -4715,7 +4769,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", - "dev": true, "requires": { "graceful-fs": "^4.1.9" } @@ -4967,11 +5020,19 @@ } }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.0.0.tgz", + "integrity": "sha512-DCZvJtCxpfY3a0Onp57Jm0PY9ggZENfVtBMsPdXFZDrMSHU5kYCMJkJesLr0/UrFdJKuDUYoGxCpc93n4F3Z8g==", "requires": { - "pify": "^3.0.0" + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" + } } }, "map-cache": { @@ -4996,8 +5057,7 @@ "marked": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", - "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", - "dev": true + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==" }, "matcher": { "version": "1.1.1", @@ -5205,8 +5265,7 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minimist-options": { "version": "3.0.2", @@ -6700,8 +6759,7 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "p-finally": { "version": "1.0.0", @@ -7334,7 +7392,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", "integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=", - "dev": true, "requires": { "underscore": "~1.6.0" }, @@ -7342,8 +7399,7 @@ "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" } } }, @@ -7878,8 +7934,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supertap": { "version": "1.0.0", @@ -8018,8 +8073,7 @@ "taffydb": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=" }, "tap-nyan": { "version": "1.1.0", @@ -8106,6 +8160,17 @@ "pify": "^3.0.0", "temp-dir": "^1.0.0", "uuid": "^3.0.1" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + } } }, "term-size": { @@ -8155,7 +8220,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "requires": { "os-tmpdir": "~1.0.2" } @@ -8300,14 +8364,12 @@ "underscore": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", - "dev": true + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, "underscore-contrib": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz", "integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=", - "dev": true, "requires": { "underscore": "1.6.0" }, @@ -8315,8 +8377,7 @@ "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" } } }, @@ -8640,6 +8701,15 @@ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=", "dev": true + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } } } }, @@ -8676,8 +8746,7 @@ "xmlcreate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", - "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", - "dev": true + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=" }, "xtend": { "version": "4.0.1", diff --git a/package.json b/package.json index 5fc6220ee..41f935e6d 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "docs/**", "jsdocs/**", "coverage/**", - "test/**" + "test/**", + "lib/processors/jsdoc/lib/**" ], "check-coverage": true, "statements": 85, @@ -101,11 +102,14 @@ "estraverse": "^4.2.0", "globby": "^7.1.1", "graceful-fs": "^4.1.15", + "jsdoc": "^3.5.5", "less-openui5": "^0.6.0", + "make-dir": "^2.0.0", "pretty-data": "^0.40.0", "pretty-hrtime": "^1.0.3", "replacestream": "^4.0.3", "semver": "^5.6.0", + "tmp": "0.0.33", "uglify-es": "^3.2.2", "xml2js": "^0.4.17", "yazl": "^2.5.1" @@ -121,7 +125,6 @@ "eslint-config-google": "^0.12.0", "eslint-plugin-jsdoc": "^4.1.1", "extract-zip": "^1.6.7", - "jsdoc": "^3.5.5", "mock-require": "^3.0.3", "nyc": "^13.3.0", "opn-cli": "^4.0.0", diff --git a/test/expected/build/library.j/dest/resources/library/j/.library b/test/expected/build/library.j/dest/resources/library/j/.library new file mode 100644 index 000000000..1919da37e --- /dev/null +++ b/test/expected/build/library.j/dest/resources/library/j/.library @@ -0,0 +1,11 @@ + + + + library.j + SAP SE + Some fancy copyright + ${version} + + Library J + + diff --git a/test/expected/build/library.j/dest/resources/library/j/some.js b/test/expected/build/library.j/dest/resources/library/j/some.js new file mode 100644 index 000000000..8fd81bb68 --- /dev/null +++ b/test/expected/build/library.j/dest/resources/library/j/some.js @@ -0,0 +1,16 @@ +/*! + * ${copyright} + */ + +sap.ui.define([], + function() { + "use strict"; + + /** + * @alias library.j + * @namespace + * @public + */ + var SomeFunction = function() {}; + +}, /* bExport= */ true); diff --git a/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json b/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json new file mode 100644 index 000000000..37b7d11d9 --- /dev/null +++ b/test/expected/build/library.j/dest/test-resources/library/j/designtime/api.json @@ -0,0 +1 @@ +{"$schema-ref":"http://schemas.sap.com/sapui5/designtime/api.json/1.0","version":"1.0.0","symbols":[{"kind":"namespace","name":"library.j","basename":"j","resource":"library/j/some.js","module":"library/j/some","static":true,"visibility":"public"}]} \ No newline at end of file diff --git a/test/fixtures/library.j/main/src/library/j/.library b/test/fixtures/library.j/main/src/library/j/.library new file mode 100644 index 000000000..1919da37e --- /dev/null +++ b/test/fixtures/library.j/main/src/library/j/.library @@ -0,0 +1,11 @@ + + + + library.j + SAP SE + Some fancy copyright + ${version} + + Library J + + diff --git a/test/fixtures/library.j/main/src/library/j/some.js b/test/fixtures/library.j/main/src/library/j/some.js new file mode 100644 index 000000000..8fd81bb68 --- /dev/null +++ b/test/fixtures/library.j/main/src/library/j/some.js @@ -0,0 +1,16 @@ +/*! + * ${copyright} + */ + +sap.ui.define([], + function() { + "use strict"; + + /** + * @alias library.j + * @namespace + * @public + */ + var SomeFunction = function() {}; + +}, /* bExport= */ true); diff --git a/test/fixtures/library.j/package.json b/test/fixtures/library.j/package.json new file mode 100644 index 000000000..5961b3664 --- /dev/null +++ b/test/fixtures/library.j/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.j", + "version": "1.0.0", + "description": "Simple SAPUI5 based library for testing JSDoc builds", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/test/fixtures/library.j/ui5.yaml b/test/fixtures/library.j/ui5.yaml new file mode 100644 index 000000000..eae34d176 --- /dev/null +++ b/test/fixtures/library.j/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "0.1" +type: library +metadata: + name: library.j +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/test/lib/builder/builder.js b/test/lib/builder/builder.js index d25a8a966..a5a6d8364 100644 --- a/test/lib/builder/builder.js +++ b/test/lib/builder/builder.js @@ -14,8 +14,9 @@ const applicationGPath = path.join(__dirname, "..", "..", "fixtures", "applicati const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h"); const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d"); const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e"); -const libraryIPath = path.join(__dirname, "..", "..", "fixtures", "library.i"); const libraryHPath = path.join(__dirname, "..", "..", "fixtures", "library.h"); +const libraryIPath = path.join(__dirname, "..", "..", "fixtures", "library.i"); +const libraryJPath = path.join(__dirname, "..", "..", "fixtures", "library.j"); const libraryCore = path.join(__dirname, "..", "..", "fixtures", "sap.ui.core-evo"); const themeJPath = path.join(__dirname, "..", "..", "fixtures", "theme.j"); @@ -248,6 +249,28 @@ test("Build library.i with manifest info taken from .library and library.js", (t }); }); +test("Build library.j with JSDoc build only", (t) => { + const destPath = path.join("test", "tmp", "build", "library.j", "dest"); + const expectedPath = path.join("test", "expected", "build", "library.j", "dest"); + + return builder.build({ + tree: libraryJTree, + destPath, + includedTasks: ["generateJsdoc"], + excludedTasks: ["*"] + }).then(() => { + return findFiles(expectedPath); + }).then((expectedFiles) => { + // Check for all directories and files + assert.directoryDeepEqual(destPath, expectedPath); + + // Check for all file contents + return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + }).then(() => { + t.pass(); + }); +}); + test("Build theme.j even without an library", (t) => { const destPath = "./test/tmp/build/theme.j/dest"; const expectedPath = "./test/expected/build/theme.j/dest"; @@ -753,6 +776,30 @@ const libraryITree = { } }; +const libraryJTree = { + "id": "library.j", + "version": "1.0.0", + "path": libraryJPath, + "dependencies": [], + "_level": 0, + "specVersion": "0.1", + "type": "library", + "metadata": { + "name": "library.j", + "copyright": "Some fancy copyright" + }, + "resources": { + "configuration": { + "paths": { + "src": "main/src" + } + }, + "pathMappings": { + "/resources/": "main/src" + } + } +}; + const themeJTree = { "id": "library.i", "version": "1.0.0", diff --git a/test/lib/processors/jsdoc/apiIndexGenerator.js b/test/lib/processors/jsdoc/apiIndexGenerator.js new file mode 100644 index 000000000..f35b49785 --- /dev/null +++ b/test/lib/processors/jsdoc/apiIndexGenerator.js @@ -0,0 +1,85 @@ +const {test} = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const apiIndexGenerator = require("../../../../lib/processors/jsdoc/apiIndexGenerator"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("apiIndexGenerator", async (t) => { + const createApiIndexStub = sinon.stub().resolves({ + "/some/path/api-index.json": "resource content A", + "/some/path/api-index-deprecated.json": "resource content B", + "/some/path/api-index-experimental.json": "resource content C", + "/some/path/api-index-since.json": "resource content D" + }); + mock("../../../../lib/processors/jsdoc/lib/create-api-index", createApiIndexStub); + const createResourceStub = sinon.stub(require("@ui5/fs").resourceFactory, "createResource") + .onCall(0).returns("result resource A") + .onCall(1).returns("result resource B") + .onCall(2).returns("result resource C") + .onCall(3).returns("result resource D"); + + const apiIndexGenerator = mock.reRequire("../../../../lib/processors/jsdoc/apiIndexGenerator"); + + const res = await apiIndexGenerator({ + versionInfoPath: "/some/path/sap-ui-version.json", + testResourcesRootPath: "/some/test-resources/path", + targetApiIndexPath: "/some/path/api-index.json", + targetApiIndexDeprecatedPath: "/some/path/api-index-deprecated.json", + targetApiIndexExperimentalPath: "/some/path/api-index-experimental.json", + targetApiIndexSincePath: "/some/path/api-index-since.json", + fs: "custom fs" + }); + + t.deepEqual(res.length, 4, "Returned one resource"); + t.deepEqual(res[0], "result resource A", "Returned correct resource"); + t.deepEqual(res[1], "result resource B", "Returned correct resource"); + t.deepEqual(res[2], "result resource C", "Returned correct resource"); + t.deepEqual(res[3], "result resource D", "Returned correct resource"); + + t.deepEqual(createApiIndexStub.callCount, 1, "create-api-index called once"); + t.deepEqual(createApiIndexStub.getCall(0).args[0], "/some/path/sap-ui-version.json", + "create-api-index called with correct argument #1"); + t.deepEqual(createApiIndexStub.getCall(0).args[1], "/some/test-resources/path", + "create-api-index called with correct argument #2"); + t.deepEqual(createApiIndexStub.getCall(0).args[2], "/some/path/api-index.json", + "create-api-index called with correct argument #3"); + t.deepEqual(createApiIndexStub.getCall(0).args[3], "/some/path/api-index-deprecated.json", + "create-api-index called with correct argument #4"); + t.deepEqual(createApiIndexStub.getCall(0).args[4], "/some/path/api-index-experimental.json", + "create-api-index called with correct argument #5"); + t.deepEqual(createApiIndexStub.getCall(0).args[5], "/some/path/api-index-since.json", + "create-api-index called with correct argument #6"); + t.deepEqual(createApiIndexStub.getCall(0).args[6], { + fs: "custom fs", + returnOutputFiles: true + }, "create-api-index called with correct argument #7"); + + t.deepEqual(createResourceStub.callCount, 4, "createResource called once"); + t.deepEqual(createResourceStub.getCall(0).args[0], { + path: "/some/path/api-index.json", + string: "resource content A" + }, "createResource called with correct arguments for resource 1"); + t.deepEqual(createResourceStub.getCall(1).args[0], { + path: "/some/path/api-index-deprecated.json", + string: "resource content B" + }, "createResource called with correct arguments for resource 2"); + t.deepEqual(createResourceStub.getCall(2).args[0], { + path: "/some/path/api-index-experimental.json", + string: "resource content C" + }, "createResource called with correct arguments for resource 3"); + t.deepEqual(createResourceStub.getCall(3).args[0], { + path: "/some/path/api-index-since.json", + string: "resource content D" + }, "createResource called with correct arguments for resource 4"); + + mock.stop("../../../../lib/processors/jsdoc/lib/create-api-index"); +}); + +test("apiIndexGenerator missing parameters", async (t) => { + const error = await t.throws(apiIndexGenerator()); + t.deepEqual(error.message, "[apiIndexGenerator]: One or more mandatory parameters not provided", + "Correct error message thrown"); +}); diff --git a/test/lib/processors/jsdoc/jsdocGenerator.js b/test/lib/processors/jsdoc/jsdocGenerator.js new file mode 100644 index 000000000..f1ef55ebe --- /dev/null +++ b/test/lib/processors/jsdoc/jsdocGenerator.js @@ -0,0 +1,218 @@ +const path = require("path"); +const {test} = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const jsdocGenerator = require("../../../../lib/processors/jsdoc/jsdocGenerator"); + +test("generateJsdocConfig", async (t) => { + const res = await jsdocGenerator._generateJsdocConfig({ + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tm\\p/path", + namespace: "some/namespace", + projectName: "some.namespace", + version: "1.0.0", + variants: ["apijson"] + }); + + const jsdocGeneratorPath = path.resolve(__dirname, "..", "..", "..", "..", "lib", "processors", + "jsdoc"); + + const backslashRegex = /\\/g; + + const pluginPath = path.join(jsdocGeneratorPath, "lib", "ui5", "plugin.js") + .replace(backslashRegex, "\\\\"); + const templatePath = path.join(jsdocGeneratorPath, "lib", "ui5", "template") + .replace(backslashRegex, "\\\\"); + const destinationPath = path.join("/", "some", "tm\\p", "path") + .replace(backslashRegex, "\\\\"); + const jsapiFilePath = path.join("/", "some", "target", "path", "libraries", "some.namespace.js") + .replace(backslashRegex, "\\\\"); + const apiJsonFolderPath = path.join("/", "some", "tm\\p", "path", "dependency-apis") + .replace(backslashRegex, "\\\\"); + const apiJsonFilePath = + path.join("/", "some", "target", "path", "test-resources", "some", "namespace", "designtime", "api.json") + .replace(backslashRegex, "\\\\"); + + + t.deepEqual(res, `{ + "plugins": ["${pluginPath}"], + "opts": { + "recurse": true, + "lenient": true, + "template": "${templatePath}", + "ui5": { + "saveSymbols": true + }, + "destination": "${destinationPath}" + }, + "templates": { + "ui5": { + "variants": ["apijson"], + "version": "1.0.0", + "jsapiFile": "${jsapiFilePath}", + "apiJsonFolder": "${apiJsonFolderPath}", + "apiJsonFile": "${apiJsonFilePath}" + } + } + }`, "Correct config generated"); +}); + +test.serial("writeJsdocConfig", async (t) => { + mock("graceful-fs", { + writeFile: (configPath, configContent, callback) => { + t.deepEqual(configPath, path.join("/", "some", "path", "jsdoc-config.json"), + "Correct config path supplied"); + t.deepEqual(configContent, "some config", "Correct config content supplied"); + callback(); + } + }); + mock.reRequire("graceful-fs"); + + // Re-require tested module + const jsdocGenerator = mock.reRequire("../../../../lib/processors/jsdoc/jsdocGenerator"); + const res = await jsdocGenerator._writeJsdocConfig("/some/path", "some config"); + + t.deepEqual(res, path.join("/", "some", "path", "jsdoc-config.json"), "Correct config path returned"); + + mock.stop("graceful-fs"); +}); + +test.serial("buildJsdoc", async (t) => { + const childProcess = require("child_process"); + let exitCode = 0; + const cpStub = sinon.stub(childProcess, "spawn").returns({ + on: (event, callback) => { + callback(exitCode); + } + }); + const jsdocGenerator = mock.reRequire("../../../../lib/processors/jsdoc/jsdocGenerator"); + + await jsdocGenerator._buildJsdoc({ + sourcePath: "/some/path", + configPath: "/some/config/path/jsdoc-config.json" + }); + t.deepEqual(cpStub.callCount, 1, "Spawn got called"); + + const firstCallArgs = cpStub.getCall(0).args; + t.deepEqual(firstCallArgs[0], "node", "Spawn got called with correct process argument"); + t.deepEqual(firstCallArgs[1], [ + path.resolve(__dirname, "..", "..", "..", "..", "node_modules", "jsdoc", "jsdoc.js"), + "-c", + "/some/config/path/jsdoc-config.json", + "--verbose", + "/some/path" + ], "Spawn got called with correct arguments"); + + + // Re-execute with exit code 1 + exitCode = 1; + await t.notThrows(jsdocGenerator._buildJsdoc({ + sourcePath: "/some/path", + configPath: "/some/config/path/jsdoc-config.json" + })); + + // Re-execute with exit code 2 + exitCode = 2; + const error = await t.throws(jsdocGenerator._buildJsdoc({ + sourcePath: "/some/path", + configPath: "/some/config/path/jsdoc-config.json" + })); + t.deepEqual(error.message, "JSDoc child process closed with code 2"); +}); + +test.serial("jsdocGenerator", async (t) => { + const generateJsdocConfigStub = sinon.stub(jsdocGenerator, "_generateJsdocConfig").resolves("some config"); + const writeJsdocConfigStub = sinon.stub(jsdocGenerator, "_writeJsdocConfig").resolves("/some/config/path"); + const buildJsdocStub = sinon.stub(jsdocGenerator, "_buildJsdoc").resolves(); + const byPathStub = sinon.stub().resolves("some resource"); + const createAdapterStub = sinon.stub(require("@ui5/fs").resourceFactory, "createAdapter").returns({ + byPath: byPathStub + }); + + const res = await jsdocGenerator({ + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + options: { + projectName: "some.project.name", + namespace: "some/project/name", + version: "1.0.0" + } + }); + + t.deepEqual(res.length, 1, "Returned 1 resource"); + t.deepEqual(res[0], "some resource", "Returned 1 resource"); + + t.deepEqual(generateJsdocConfigStub.callCount, 1, "generateJsdocConfig called once"); + t.deepEqual(generateJsdocConfigStub.getCall(0).args[0], { + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + namespace: "some/project/name", + projectName: "some.project.name", + version: "1.0.0", + variants: ["apijson"] + }, "generateJsdocConfig called with correct arguments"); + + t.deepEqual(writeJsdocConfigStub.callCount, 1, "writeJsdocConfig called once"); + t.deepEqual(writeJsdocConfigStub.getCall(0).args[0], "/some/tmp/path", + "writeJsdocConfig called with correct tmpPath argument"); + t.deepEqual(writeJsdocConfigStub.getCall(0).args[1], "some config", + "writeJsdocConfig called with correct config argument"); + + t.deepEqual(buildJsdocStub.callCount, 1, "buildJsdoc called once"); + t.deepEqual(buildJsdocStub.getCall(0).args[0], { + sourcePath: "/some/source/path", + configPath: "/some/config/path" + }, "buildJsdoc called with correct arguments"); + + t.deepEqual(createAdapterStub.getCall(0).args[0], { + fsBasePath: "/some/target/path", + virBasePath: "/" + }, "createAdapter called with correct arguments"); + t.deepEqual(byPathStub.getCall(0).args[0], "/test-resources/some/project/name/designtime/api.json", + "byPath called with correct path for api.json"); + + + /* Test branch: empty variants array*/ + await jsdocGenerator({ + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + options: { + projectName: "some.project.name", + namespace: "some/project/name", + version: "1.0.0", + variants: [] + } + }); + + t.deepEqual(generateJsdocConfigStub.getCall(1).args[0].variants, ["apijson"], + "generateJsdocConfig called with correct variants arguments"); + + + /* Test branch: variants array set + sdkBuild requested*/ + await jsdocGenerator({ + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + options: { + projectName: "some.project.name", + namespace: "some/project/name", + version: "1.0.0", + variants: ["pony"], + sdkBuild: true + } + }); + + t.deepEqual(generateJsdocConfigStub.getCall(2).args[0].variants, ["pony"], + "generateJsdocConfig called with correct variants arguments"); + + sinon.restore(); +}); + +test("jsdocGenerator missing parameters", async (t) => { + const error = await t.throws(jsdocGenerator()); + t.deepEqual(error.message, "[jsdocGenerator]: One or more mandatory parameters not provided", + "Correct error message thrown"); +}); diff --git a/test/lib/processors/jsdoc/sdkTransformer.js b/test/lib/processors/jsdoc/sdkTransformer.js new file mode 100644 index 000000000..215392988 --- /dev/null +++ b/test/lib/processors/jsdoc/sdkTransformer.js @@ -0,0 +1,61 @@ +const {test} = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const sdkTransformer = require("../../../../lib/processors/jsdoc/sdkTransformer"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("sdkTransformer", async (t) => { + const transformerStub = sinon.stub().resolves("api.json content"); + mock("../../../../lib/processors/jsdoc/lib/transform-apijson-for-sdk", transformerStub); + const createResourceStub = sinon.stub(require("@ui5/fs").resourceFactory, "createResource") + .returns("result resource"); + + const sdkTransformer = mock.reRequire("../../../../lib/processors/jsdoc/sdkTransformer"); + + const res = await sdkTransformer({ + apiJsonPath: "/some/path/api.json", + dotLibraryPath: "/some/path/.library", + targetApiJsonPath: "/some/other/path/api.json", + dependencyApiJsonPaths: [ + "/some/path/x/api.json", + "/some/path/y/api.json" + ], + fs: "custom fs" + }); + + t.deepEqual(res.length, 1, "Returned one resource"); + t.deepEqual(res[0], "result resource", "Returned one resource"); + + t.deepEqual(transformerStub.callCount, 1, "generateJsdocConfig called once"); + t.deepEqual(transformerStub.getCall(0).args[0], "/some/path/api.json", + "transform-apijson-for-sdk called with correct argument #1"); + t.deepEqual(transformerStub.getCall(0).args[1], "/ignore/this/path/resource/will/be/returned", + "transform-apijson-for-sdk called with correct argument #2"); + t.deepEqual(transformerStub.getCall(0).args[2], "/some/path/.library", + "transform-apijson-for-sdk called with correct argument #3"); + t.deepEqual(transformerStub.getCall(0).args[3], [ + "/some/path/x/api.json", + "/some/path/y/api.json" + ], "transform-apijson-for-sdk called with correct argument #4"); + t.deepEqual(transformerStub.getCall(0).args[4], { + fs: "custom fs", + returnOutputFiles: true + }, "transform-apijson-for-sdk called with correct argument #5"); + + t.deepEqual(createResourceStub.callCount, 1, "createResource called once"); + t.deepEqual(createResourceStub.getCall(0).args[0], { + path: "/some/other/path/api.json", + string: "api.json content" + }, "createResource called with correct arguments"); + + mock.stop("../../../../lib/processors/jsdoc/lib/transform-apijson-for-sdk"); +}); + +test("sdkTransformer missing parameters", async (t) => { + const error = await t.throws(sdkTransformer()); + t.deepEqual(error.message, "[sdkTransformer]: One or more mandatory parameters not provided", + "Correct error message thrown"); +}); diff --git a/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js b/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js new file mode 100644 index 000000000..410487c8d --- /dev/null +++ b/test/lib/tasks/jsdoc/executeJsdocSdkTransformation.js @@ -0,0 +1,250 @@ +const {test} = require("ava"); +const sinon = require("sinon"); +const ui5Fs = require("@ui5/fs"); + +const mock = require("mock-require"); + +const executeJsdocSdkTransformation = require("../../../../lib/tasks/jsdoc/executeJsdocSdkTransformation"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("executeJsdocSdkTransformation", async (t) => { + const sdkTransformerStub = sinon.stub().resolves(["resource A", "resource B"]); + const fsInterfaceStub = sinon.stub(ui5Fs, "fsInterface").returns("custom fs"); + mock("../../../../lib/processors/jsdoc/sdkTransformer", sdkTransformerStub); + + class ReaderCollectionPrioritizedStubClass { + constructor(parameters) { + t.deepEqual(parameters, { + name: "executeJsdocSdkTransformation - custom workspace + dependencies FS: some.project", + readers: [workspace, dependencies] + }, "ReaderCollectionPrioritized got called with the correct arguments"); + } + } + const readerCollectionPrioritizedStub = ReaderCollectionPrioritizedStubClass; + const readerCollectionPrioritizedOrig = ui5Fs.ReaderCollectionPrioritized; + ui5Fs.ReaderCollectionPrioritized = readerCollectionPrioritizedStub; + const executeJsdocSdkTransformation = mock.reRequire("../../../../lib/tasks/jsdoc/executeJsdocSdkTransformation"); + ui5Fs.ReaderCollectionPrioritized = readerCollectionPrioritizedOrig; + + const writeStub = sinon.stub().resolves(); + const byGlobWorkspaceStub = sinon.stub() + .onFirstCall().resolves([{ + getPath: () => "workspace/api.json" + }]) + .onSecondCall().resolves([{ + getPath: () => "workspace/.library" + }]); + const workspace = { + write: writeStub, + byGlob: byGlobWorkspaceStub + }; + const byGlobDependenciesStub = sinon.stub().resolves() + .resolves([{ + getPath: () => "depA/api.json" + }, { + getPath: () => "depB/api.json" + }]); + const dependencies = { + byGlob: byGlobDependenciesStub + }; + await executeJsdocSdkTransformation({ + workspace, + dependencies, + options: { + projectName: "some.project", + dotLibraryPattern: "some .library pattern" + } + }); + t.deepEqual(byGlobWorkspaceStub.callCount, 2, "workspace.byGlob got called twice"); + t.deepEqual(byGlobWorkspaceStub.getCall(0).args[0], "/test-resources/**/designtime/api.json", + "first workspace.byGlob call with correct arguments"); + t.deepEqual(byGlobWorkspaceStub.getCall(1).args[0], "some .library pattern", + "second workspace.byGlob call with correct arguments"); + + t.deepEqual(byGlobDependenciesStub.callCount, 1, "dependencies.byGlob got called once"); + t.deepEqual(byGlobDependenciesStub.getCall(0).args[0], "/test-resources/**/designtime/api.json", + "dependencies.byGlob got called with correct arguments"); + + t.deepEqual(fsInterfaceStub.callCount, 1, "fsInterface got called once"); + t.true(fsInterfaceStub.getCall(0).args[0] instanceof ReaderCollectionPrioritizedStubClass, + "fsInterface got called with an instance of ReaderCollectionPrioritizedStubClass"); + + t.deepEqual(sdkTransformerStub.callCount, 1, "sdkTransformer processor got called once"); + t.deepEqual(sdkTransformerStub.getCall(0).args[0], { + apiJsonPath: "workspace/api.json", + dotLibraryPath: "workspace/.library", + dependencyApiJsonPaths: [ + "depA/api.json", + "depB/api.json" + ], + targetApiJsonPath: "workspace/apiref/api.json", + fs: "custom fs" + }, "sdkTransformer got called with correct arguments"); + + t.deepEqual(writeStub.callCount, 2, "Write got called twice"); + t.deepEqual(writeStub.getCall(0).args[0], "resource A", "Write got called with correct arguments"); + t.deepEqual(writeStub.getCall(1).args[0], "resource B", "Write got called with correct arguments"); + + mock.stop("../../../../lib/processors/jsdoc/sdkTransformer"); + + sinon.restore(); + mock.reRequire("../../../../lib/tasks/jsdoc/executeJsdocSdkTransformation"); +}); + +test("executeJsdocSdkTransformation with missing parameters", async (t) => { + const error = await t.throws(executeJsdocSdkTransformation()); + t.deepEqual(error.message, "[executeJsdocSdkTransformation]: One or more mandatory options not provided", + "Correct error message thrown"); +}); + +test.serial("executeJsdocSdkTransformation with missing project api.json (skips processing)", async (t) => { + const logger = require("@ui5/logger"); + const infoLogStub = sinon.stub(); + const myLoggerInstance = { + info: infoLogStub + }; + sinon.stub(logger, "getLogger").returns(myLoggerInstance); + const executeJsdocSdkTransformation = mock.reRequire("../../../../lib/tasks/jsdoc/executeJsdocSdkTransformation"); + + const byGlobWorkspaceStub = sinon.stub() + .onFirstCall().resolves([]) + .onSecondCall().resolves([{ + getPath: () => "workspace/.library" + }]); + const workspace = { + byGlob: byGlobWorkspaceStub + }; + const byGlobDependenciesStub = sinon.stub().resolves() + .resolves([{ + getPath: () => "depA/api.json" + }, { + getPath: () => "depB/api.json" + }]); + const dependencies = { + byGlob: byGlobDependenciesStub + }; + + await executeJsdocSdkTransformation({ + workspace, + dependencies, + options: { + projectName: "some.project", + dotLibraryPattern: "some .library pattern" + } + }); + + t.deepEqual(infoLogStub.callCount, 1, "One message has been logged"); + t.deepEqual(infoLogStub.getCall(0).args[0], + "Failed to locate api.json resource for project some.project. Skipping SDK Transformation...", + "Correct message has been logged"); + + sinon.restore(); + mock.reRequire("../../../../lib/tasks/jsdoc/executeJsdocSdkTransformation"); +}); + +test("executeJsdocSdkTransformation too many project api.json resources", async (t) => { + const byGlobWorkspaceStub = sinon.stub() + .onFirstCall().resolves([{ + getPath: () => "workspace/a/api.json" + }, { + getPath: () => "workspace/b/api.json" + }]) + .onSecondCall().resolves([{ + getPath: () => "workspace/a/.library" + }]); + const workspace = { + byGlob: byGlobWorkspaceStub + }; + const byGlobDependenciesStub = sinon.stub().resolves() + .resolves([{ + getPath: () => "depA/api.json" + }, { + getPath: () => "depB/api.json" + }]); + const dependencies = { + byGlob: byGlobDependenciesStub + }; + + const error = await t.throws(executeJsdocSdkTransformation({ + workspace, + dependencies, + options: { + projectName: "some.project", + dotLibraryPattern: "some .library pattern" + } + })); + t.deepEqual(error.message, + "[executeJsdocSdkTransformation]: Found more than one api.json resources for project some.project.", + "Correct error message thrown"); +}); + +test("executeJsdocSdkTransformation missing project .library", async (t) => { + const byGlobWorkspaceStub = sinon.stub() + .onFirstCall().resolves([{ + getPath: () => "workspace/api.json" + }]) + .onSecondCall().resolves([]); + const workspace = { + byGlob: byGlobWorkspaceStub + }; + const byGlobDependenciesStub = sinon.stub().resolves() + .resolves([{ + getPath: () => "depA/api.json" + }, { + getPath: () => "depB/api.json" + }]); + const dependencies = { + byGlob: byGlobDependenciesStub + }; + + const error = await t.throws(executeJsdocSdkTransformation({ + workspace, + dependencies, + options: { + projectName: "some.project", + dotLibraryPattern: "some .library pattern" + } + })); + t.deepEqual(error.message, + "[executeJsdocSdkTransformation]: Failed to locate .library resource for project some.project.", + "Correct error message thrown"); +}); + +test("executeJsdocSdkTransformation too many project .library resources", async (t) => { + const byGlobWorkspaceStub = sinon.stub() + .onFirstCall().resolves([{ + getPath: () => "workspace/a/api.json" + }]) + .onSecondCall().resolves([{ + getPath: () => "workspace/a/.library" + }, { + getPath: () => "workspace/b/.library" + }]); + const workspace = { + byGlob: byGlobWorkspaceStub + }; + const byGlobDependenciesStub = sinon.stub().resolves() + .resolves([{ + getPath: () => "depA/api.json" + }, { + getPath: () => "depB/api.json" + }]); + const dependencies = { + byGlob: byGlobDependenciesStub + }; + + const error = await t.throws(executeJsdocSdkTransformation({ + workspace, + dependencies, + options: { + projectName: "some.project", + dotLibraryPattern: "some .library pattern" + } + })); + t.deepEqual(error.message, + "[executeJsdocSdkTransformation]: Found more than one .library resources for project some.project.", + "Correct error message thrown"); +}); diff --git a/test/lib/tasks/jsdoc/generateApiIndex.js b/test/lib/tasks/jsdoc/generateApiIndex.js new file mode 100644 index 000000000..305a79d3e --- /dev/null +++ b/test/lib/tasks/jsdoc/generateApiIndex.js @@ -0,0 +1,74 @@ +const {test} = require("ava"); +const sinon = require("sinon"); +const ui5Fs = require("@ui5/fs"); + +const mock = require("mock-require"); + +const generateApiIndex = require("../../../../lib/tasks/jsdoc/generateApiIndex"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("generateApiIndex", async (t) => { + const apiIndexGeneratorStub = sinon.stub().resolves(["resource A", "resource B"]); + const fsInterfaceStub = sinon.stub(ui5Fs, "fsInterface").returns("custom fs"); + mock("../../../../lib/processors/jsdoc/apiIndexGenerator", apiIndexGeneratorStub); + + class ReaderCollectionPrioritizedStubClass { + constructor(parameters) { + t.deepEqual(parameters, { + name: "generateApiIndex - workspace + dependencies: some.project", + readers: [workspace, dependencies] + }, "ReaderCollectionPrioritized got called with the correct arguments"); + } + } + const readerCollectionPrioritizedStub = ReaderCollectionPrioritizedStubClass; + const readerCollectionPrioritizedOrig = ui5Fs.ReaderCollectionPrioritized; + ui5Fs.ReaderCollectionPrioritized = readerCollectionPrioritizedStub; + const generateApiIndex = mock.reRequire("../../../../lib/tasks/jsdoc/generateApiIndex"); + ui5Fs.ReaderCollectionPrioritized = readerCollectionPrioritizedOrig; + + const writeStub = sinon.stub().resolves(); + const workspace = { + write: writeStub + }; + const dependencies = {}; + await generateApiIndex({ + workspace, + dependencies, + options: { + projectName: "some.project" + } + }); + + t.deepEqual(fsInterfaceStub.callCount, 1, "fsInterface got called once"); + t.true(fsInterfaceStub.getCall(0).args[0] instanceof ReaderCollectionPrioritizedStubClass, + "fsInterface got called with an instance of ReaderCollectionPrioritizedStubClass"); + + t.deepEqual(apiIndexGeneratorStub.callCount, 1, "apiIndexGenerator processor got called once"); + t.deepEqual(apiIndexGeneratorStub.getCall(0).args[0], { + versionInfoPath: "/resources/sap-ui-version.json", + testResourcesRootPath: "/test-resources", + targetApiIndexPath: "/docs/api/api-index.json", + targetApiIndexDeprecatedPath: "/docs/api/api-index-deprecated.json", + targetApiIndexExperimentalPath: "/docs/api/api-index-experimental.json", + targetApiIndexSincePath: "/docs/api/api-index-since.json", + fs: "custom fs" + }, "apiIndexGenerator got called with correct arguments"); + + t.deepEqual(writeStub.callCount, 2, "Write got called twice"); + t.deepEqual(writeStub.getCall(0).args[0], "resource A", "Write got called with correct arguments"); + t.deepEqual(writeStub.getCall(1).args[0], "resource B", "Write got called with correct arguments"); + + mock.stop("../../../../lib/processors/jsdoc/apiIndexGenerator"); + + sinon.restore(); + mock.reRequire("../../../../lib/tasks/jsdoc/generateApiIndex"); +}); + +test("generateApiIndex with missing parameters", async (t) => { + const error = await t.throws(generateApiIndex()); + t.deepEqual(error.message, "[generateApiIndex]: One or more mandatory options not provided", + "Correct error message thrown"); +}); diff --git a/test/lib/tasks/jsdoc/generateJsdoc.js b/test/lib/tasks/jsdoc/generateJsdoc.js new file mode 100644 index 000000000..9e7878e05 --- /dev/null +++ b/test/lib/tasks/jsdoc/generateJsdoc.js @@ -0,0 +1,289 @@ +const {test} = require("ava"); +const sinon = require("sinon"); +const tmp = require("tmp"); +const path = require("path"); + +const mock = require("mock-require"); + +const generateJsdoc = require("../../../../lib/tasks/jsdoc/generateJsdoc"); + +test.beforeEach((t) => { + t.context.tmpStub = sinon.stub(tmp, "dir"); +}); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("createTmpDir successful", async (t) => { + t.context.tmpStub.callsArgWithAsync(1, undefined, "some/path"); + + const res = await generateJsdoc._createTmpDir("som$e.nam3/space"); // non alphanum characters get removed + + t.deepEqual(t.context.tmpStub.callCount, 1, "Tmp dir is called once"); + t.deepEqual(t.context.tmpStub.getCall(0).args[0].prefix, "ui5-tooling-tmp-jsdoc-somenam3space-"); + t.deepEqual(res, {path: "some/path"}, "Correct path returned"); +}); + +test.serial("createTmpDir error", async (t) => { + t.context.tmpStub.callsArgWithAsync(1, {message: "Dir creation failed"}, "some/path"); + + const res = await t.throws(generateJsdoc._createTmpDir("some.namespace")); + + t.deepEqual(t.context.tmpStub.callCount, 1, "Tmp dir is called once"); + t.deepEqual(t.context.tmpStub.getCall(0).args[0].prefix, "ui5-tooling-tmp-jsdoc-somenamespace-"); + t.deepEqual(res, {message: "Dir creation failed"}, "Dir creation failed"); +}); + +test.serial("createTmpDirs", async (t) => { + const makeDirStub = sinon.stub().resolves(); + mock("make-dir", makeDirStub); + const generateJsdoc = mock.reRequire("../../../../lib/tasks/jsdoc/generateJsdoc"); + + t.context.tmpStub.callsArgWithAsync(1, undefined, "/some/path"); + + const res = await generateJsdoc._createTmpDirs("some.namespace"); + + t.deepEqual(res, { + sourcePath: path.join("/", "some", "path", "src"), + targetPath: path.join("/", "some", "path", "target"), + tmpPath: path.join("/", "some", "path", "tmp") + }, "Correct temporary directories returned"); + t.deepEqual(makeDirStub.callCount, 3, "One directory got created"); + t.deepEqual(makeDirStub.getCall(0).args[0], path.join("/", "some", "path", "src"), + "Correct srcdir path got created"); + t.deepEqual(makeDirStub.getCall(1).args[0], path.join("/", "some", "path", "target"), + "Correct target dir path got created"); + t.deepEqual(makeDirStub.getCall(2).args[0], path.join("/", "some", "path", "tmp"), + "Correct tmp dir path got created"); + + mock.stop("make-dir"); +}); + +test.serial("writeResourcesToDir with byGlobSource", async (t) => { + const writeStub = sinon.stub().resolves(); + const createAdapterStub = sinon.stub(require("@ui5/fs").resourceFactory, "createAdapter").returns({ + write: writeStub + }); + + await generateJsdoc._writeResourcesToDir({ + workspace: { + // stub byGlobSource + byGlobSource: (pattern) => { + t.deepEqual(pattern, "some pattern", "Glob with correct pattern"); + return Promise.resolve(["resource A", "resource B"]); + } + }, + pattern: "some pattern", + targetPath: "/some/target/path" + }); + + t.deepEqual(createAdapterStub.getCall(0).args[0], { + fsBasePath: "/some/target/path", + virBasePath: "/resources/" + }, "createAdapter called with correct arguments"); + + t.deepEqual(writeStub.callCount, 2, "Write got called four times"); + t.deepEqual(writeStub.getCall(0).args[0], "resource A", "Write got called for resource A"); + t.deepEqual(writeStub.getCall(1).args[0], "resource B", "Write got called for resource B"); +}); + +test.serial("writeResourcesToDir with byGlob", async (t) => { + const writeStub = sinon.stub().resolves(); + const createAdapterStub = sinon.stub(require("@ui5/fs").resourceFactory, "createAdapter").returns({ + write: writeStub + }); + + await generateJsdoc._writeResourcesToDir({ + workspace: { + byGlob: (pattern) => { + t.deepEqual(pattern, "some pattern", "Glob with correct pattern"); + return Promise.resolve(["resource A", "resource B"]); + } + }, + pattern: "some pattern", + targetPath: "/some/target/path" + }); + + t.deepEqual(createAdapterStub.getCall(0).args[0], { + fsBasePath: "/some/target/path", + virBasePath: "/resources/" + }, "createAdapter called with correct arguments"); + + t.deepEqual(writeStub.callCount, 2, "Write got called four times"); + t.deepEqual(writeStub.getCall(0).args[0], "resource A", "Write got called for resource A"); + t.deepEqual(writeStub.getCall(1).args[0], "resource B", "Write got called for resource B"); +}); + +test.serial("writeDependencyApisToDir with byGlob", async (t) => { + const writeStub = sinon.stub().resolves(); + const createAdapterStub = sinon.stub(require("@ui5/fs").resourceFactory, "createAdapter").returns({ + write: writeStub + }); + + const setPathStubA = sinon.stub(); + const setPathStubB = sinon.stub(); + + const cloneStubA = sinon.stub().resolves({ + // Cloned resource + id: "resource A", + setPath: setPathStubA + }); + const cloneStubB = sinon.stub().resolves({ + // Cloned resource + id: "resource B", + setPath: setPathStubB + }); + const initialResourceA = { + // Globbed resource + clone: cloneStubA + }; + const initialResourceB = { + // Globbed resource + clone: cloneStubB + }; + + await generateJsdoc._writeDependencyApisToDir({ + dependencies: { + byGlob: (pattern) => { + t.deepEqual(pattern, "/test-resources/**/designtime/api.json", + "Dependency api.json glob with correct pattern"); + return Promise.resolve([initialResourceA, initialResourceB]); + } + }, + targetPath: "/some/target/path" + }); + + t.deepEqual(cloneStubA.callCount, 1, "resource A got cloned once"); + t.deepEqual(cloneStubB.callCount, 1, "resource B got cloned once"); + + t.deepEqual(setPathStubA.callCount, 1, "Path of cloned resource A got changed"); + t.deepEqual(setPathStubA.getCall(0).args[0], "/api-0.json", "Path of cloned resource A got changed correctly"); + + t.deepEqual(setPathStubB.callCount, 1, "Path of cloned resource B got changed"); + t.deepEqual(setPathStubB.getCall(0).args[0], "/api-1.json", "Path of cloned resource B got changed correctly"); + + t.deepEqual(createAdapterStub.getCall(0).args[0], { + fsBasePath: "/some/target/path", + virBasePath: "/" + }, "createAdapter called with correct arguments"); + + t.deepEqual(writeStub.callCount, 2, "Write got called four times"); + t.deepEqual(writeStub.getCall(0).args[0].id, "resource A", "Write got called for resource A"); + t.deepEqual(writeStub.getCall(1).args[0].id, "resource B", "Write got called for resource B"); +}); + +test.serial("generateJsdoc", async (t) => { + const jsdocGeneratorStub = sinon.stub().resolves(["resource A", "resource B"]); + mock("../../../../lib/processors/jsdoc/jsdocGenerator", jsdocGeneratorStub); + const generateJsdoc = mock.reRequire("../../../../lib/tasks/jsdoc/generateJsdoc"); + + const createTmpDirsStub = sinon.stub(generateJsdoc, "_createTmpDirs").resolves({ + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + }); + const writeResourcesToDirStub = sinon.stub(generateJsdoc, "_writeResourcesToDir").resolves(1); + const writeDependencyApisToDirStub = sinon.stub(generateJsdoc, "_writeDependencyApisToDir").resolves(0); + + const writeStub = sinon.stub().resolves(); + const workspace = { + write: writeStub + }; + await generateJsdoc({ + workspace, + dependencies: "dependencies", + options: { + pattern: "some pattern", + projectName: "some.project", + namespace: "some/project", + version: "some version" + } + }); + + t.deepEqual(createTmpDirsStub.callCount, 1, "createTmpDirs got called once"); + t.deepEqual(createTmpDirsStub.getCall(0).args[0], "some.project", + "createTmpDirs got called with correct arguments"); + + t.deepEqual(writeResourcesToDirStub.callCount, 1, "writeResourcesToDir got called once"); + t.deepEqual(writeResourcesToDirStub.getCall(0).args[0], { + workspace, + pattern: "some pattern", + targetPath: "/some/source/path" // one's target is another one's source + }, "writeResourcesToDir got called with correct arguments"); + + t.deepEqual(writeDependencyApisToDirStub.callCount, 1, "writeDependencyApisToDir got called once"); + t.deepEqual(writeDependencyApisToDirStub.getCall(0).args[0], { + dependencies: "dependencies", + targetPath: "/some/tmp/path/dependency-apis" + }, "writeDependencyApisToDir got called with correct arguments"); + + t.deepEqual(jsdocGeneratorStub.callCount, 1, "jsdocGenerator processor got called once"); + t.deepEqual(jsdocGeneratorStub.getCall(0).args[0], { + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + options: { + projectName: "some.project", + namespace: "some/project", + version: "some version", + variants: ["apijson"] + } + }, "jsdocGenerator got called with correct arguments"); + + t.deepEqual(writeStub.callCount, 2, "Write got called twice"); + t.deepEqual(writeStub.getCall(0).args[0], "resource A", "Write got called with correct arguments"); + t.deepEqual(writeStub.getCall(1).args[0], "resource B", "Write got called with correct arguments"); + + mock.stop("../../../../lib/processors/jsdoc/jsdocGenerator"); +}); + +test.serial("generateJsdoc with missing resources", async (t) => { + const jsdocGeneratorStub = sinon.stub().resolves(); + mock("../../../../lib/processors/jsdoc/jsdocGenerator", jsdocGeneratorStub); + const logger = require("@ui5/logger"); + const infoLogStub = sinon.stub(); + const myLoggerInstance = { + info: infoLogStub + }; + sinon.stub(logger, "getLogger").returns(myLoggerInstance); + const generateJsdoc = mock.reRequire("../../../../lib/tasks/jsdoc/generateJsdoc"); + + sinon.stub(generateJsdoc, "_createTmpDirs").resolves({ + sourcePath: "/some/source/path", + targetPath: "/some/target/path", + tmpPath: "/some/tmp/path", + }); + sinon.stub(generateJsdoc, "_writeResourcesToDir").resolves(0); + sinon.stub(generateJsdoc, "_writeDependencyApisToDir").resolves(0); + + const writeStub = sinon.stub().resolves(); + const workspace = { + write: writeStub + }; + await generateJsdoc({ + workspace, + dependencies: "dependencies", + options: { + pattern: "some pattern", + projectName: "some.project", + namespace: "some/project", + version: "some version" + } + }); + + t.deepEqual(infoLogStub.callCount, 1, "One message has been logged"); + t.deepEqual(infoLogStub.getCall(0).args[0], "Failed to find any input resources for project some.project " + + "using pattern some pattern. Skipping JSDoc generation...", + "Correct message has been logged"); + + t.deepEqual(jsdocGeneratorStub.callCount, 0, "jsdocGenerator processor has *not* been called"); + + mock.stop("../../../../lib/processors/jsdoc/jsdocGenerator"); +}); + +test.serial("generateJsdoc missing parameters", async (t) => { + const error = await t.throws(generateJsdoc()); + t.deepEqual(error.message, "[generateJsdoc]: One or more mandatory options not provided", + "Correct error message thrown"); +}); diff --git a/test/lib/types/application/ApplicationBuilder.js b/test/lib/types/application/ApplicationBuilder.js index 359550558..a25eccfde 100644 --- a/test/lib/types/application/ApplicationBuilder.js +++ b/test/lib/types/application/ApplicationBuilder.js @@ -67,7 +67,8 @@ test("Instantiation", (t) => { "generateBundle", "createDebugFiles", "uglify", - "generateVersionInfo" + "generateVersionInfo", + "generateApiIndex" ], "ApplicationBuilder is instantiated with standard tasks"); }); @@ -87,7 +88,8 @@ test("Instantiation without component preload project configuration", (t) => { "generateBundle", "createDebugFiles", "uglify", - "generateVersionInfo" + "generateVersionInfo", + "generateApiIndex" ], "ApplicationBuilder is still instantiated with standard tasks"); }); @@ -107,7 +109,8 @@ test("Instantiation without project namespace", (t) => { "generateBundle", "createDebugFiles", "uglify", - "generateVersionInfo" + "generateVersionInfo", + "generateApiIndex" ], "All standard tasks but generateComponentPreload will be executed"); }); @@ -132,6 +135,7 @@ test("Instantiation with custom tasks", (t) => { "createDebugFiles", "uglify", "replaceVersion--1", - "generateVersionInfo" + "generateVersionInfo", + "generateApiIndex" ], "ApplicationBuilder is still instantiated with standard tasks"); });