From 996c711885c8134d3c15ac67b398fdad1da3f4c3 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Tue, 26 Jun 2018 22:08:49 +0200 Subject: [PATCH] [WIP] Task to create JSDoc for a library, using the UI5 template/plugin Note: the code is full of console.log just to track down an unexpected exit of the Node.js process. --- lib/processors/jsdoc/create-api-index.js | 270 ++ lib/processors/jsdoc/dummy-child.js | 5 + .../jsdoc/jsdoc-config-template.json | 23 + lib/processors/jsdoc/jsdoc.js | 128 + .../jsdoc/transform-apijson-for-sdk.js | 1732 +++++++ lib/processors/jsdoc/ui5/plugin.js | 2321 ++++++++++ lib/processors/jsdoc/ui5/template/publish.js | 4056 +++++++++++++++++ lib/tasks/createJSDoc.js | 29 + lib/tasks/taskRepository.js | 1 + lib/types/library/LibraryBuilder.js | 18 + package-lock.json | 161 +- package.json | 3 +- 12 files changed, 8605 insertions(+), 142 deletions(-) create mode 100644 lib/processors/jsdoc/create-api-index.js create mode 100644 lib/processors/jsdoc/dummy-child.js create mode 100644 lib/processors/jsdoc/jsdoc-config-template.json create mode 100644 lib/processors/jsdoc/jsdoc.js create mode 100644 lib/processors/jsdoc/transform-apijson-for-sdk.js create mode 100644 lib/processors/jsdoc/ui5/plugin.js create mode 100644 lib/processors/jsdoc/ui5/template/publish.js create mode 100644 lib/tasks/createJSDoc.js diff --git a/lib/processors/jsdoc/create-api-index.js b/lib/processors/jsdoc/create-api-index.js new file mode 100644 index 000000000..b68357dde --- /dev/null +++ b/lib/processors/jsdoc/create-api-index.js @@ -0,0 +1,270 @@ +/* + * Node script to create cross-library API index files for use in the UI5 SDKs. + * + * (c) Copyright 2009-2018 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +"use strict"; +const fs = require("fs"); +const path = require("path"); + +function process(versionInfoFile, unpackedTestresourcesRoot, targetFile, targetFileDeprecated, targetFileExperimental, targetFileSince) { + + console.log("[INFO] creating API index files"); + console.log("[INFO] sap-ui-version.json: " + versionInfoFile); + console.log("[INFO] unpacked test-resources: " + unpackedTestresourcesRoot); + console.log("[INFO] target file: " + targetFile); + console.log("[INFO] target file deprecated: " + targetFileDeprecated); + console.log("[INFO] target file experimental: " + targetFileExperimental); + console.log("[INFO] target file since: " + targetFileSince); + console.log("[INFO]"); + + // Deprecated, Experimental and Since collections + let oListCollection = { + deprecated: { + noVersion: { + apis: [] + } + }, + experimental: { + noVersion: { + apis: [] + } + }, + since: { + noVersion: { + apis: [] + } + } + }; + + 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 => { + collectLists(symbol); + return { + name: symbol.name, + kind: symbol.kind, + visibility: symbol.visibility, + extends: symbol.extends, + implements: symbol.implements, + lib: lib + }; + }); + }) + } + + /* + * 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) { + // 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 { + 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) { + 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 createOverallIndex() { + let version = "0.0.0"; + + 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(symbols => { + let result = { + "$schema-ref": "http://schemas.sap.com/sapui5/designtime/api.json/1.0", + version: version, + library: "*", + symbols: symbols + }; + return writeJSON(targetFile, result); + }) + .then(() => 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 => { + console.error("**** failed to create API index for libraries:", err) + throw err; + }); + + return p; + } + + return createOverallIndex(); + +} + +module.exports = process; diff --git a/lib/processors/jsdoc/dummy-child.js b/lib/processors/jsdoc/dummy-child.js new file mode 100644 index 000000000..64a4a6727 --- /dev/null +++ b/lib/processors/jsdoc/dummy-child.js @@ -0,0 +1,5 @@ +console.log('child executing'); +setTimeout(function() { + console.log("child done"); + //process.exit(0); +},20000); diff --git a/lib/processors/jsdoc/jsdoc-config-template.json b/lib/processors/jsdoc/jsdoc-config-template.json new file mode 100644 index 000000000..52569ef4c --- /dev/null +++ b/lib/processors/jsdoc/jsdoc-config-template.json @@ -0,0 +1,23 @@ +{ + "source": { + "excludePattern": "(/|\\\\)library-all\\.js|(/|\\\\).*-preload\\.js|^jquery-.*\\.js|^sap-.*\\.js" + }, + "opts" : { + "recurse": true, + "template" : "lib/jsdoc/ui5/template" + }, + "plugins": [ + "lib/jsdoc/ui5/plugin.js" + ], + "templates" : { + "ui5" : { + "variants": [ + "apijson" + ], + "version": "${version}", + "apiJsonFolder": "${apiJsonFolder}", + "apiJsonFile": "${apiJsonFile}", + "includeSettingsInConstructor": false + } + } +} diff --git a/lib/processors/jsdoc/jsdoc.js b/lib/processors/jsdoc/jsdoc.js new file mode 100644 index 000000000..8cc13afd7 --- /dev/null +++ b/lib/processors/jsdoc/jsdoc.js @@ -0,0 +1,128 @@ +const spawn = require('cross-spawn').spawn; +const fs = require('fs'); +const path = require('path'); +const tmp = require('tmp'); +const resourceFactory = require("@ui5/fs").resourceFactory; + +function createJSDocConfig({source, target, namespace, libraryName, version}) { + // resolve path to the package.json to get the path to the jsdocext folder + const jsdocext = path.normalize(__dirname); + + const config = `{ + "plugins": ["${jsdocext}/ui5/plugin.js"], + "opts": { + "recurse": true, + "lenient": true, + "template": "${jsdocext}/ui5/template", + "ui5": { + "saveSymbols": true + } + }, + "templates": { + "ui5": { + "variants": [ "apijson", "fullapixml", "apijs", "api.xml"], + "version": "${version}", + "jsapiFile": "${target}/libraries/${libraryName}.js", + "apiJsonFolder": "${target}/dependency-apis", + "apiJsonFile": "${target}/test-resources/${namespace}/designtime/api.json" + } + } + }`; + console.log(config); + return config; +} + +function jsdoc({sources, target, namespace, libraryName, version}) { + + const tmpobj = tmp.fileSync(); + fs.writeFileSync(tmpobj.name, createJSDocConfig({target, namespace, libraryName, version}), 'utf8'); // TODO async + promise + + console.log("jsdoc called for ", sources); + var args = [ + require.resolve("jsdoc/jsdoc"), + '-c', + tmpobj.name, + '--verbose' + ]; + args = args.concat(sources); + + return new Promise((resolve, reject) => { + const child = spawn('node', args); + child.stdout.on('data', function(data) { + console.log(String(data)); + }); + child.stderr.on('data', function(data) { + console.error(String(data)); + }); + child.on('exit', function(code) { + var resolvedDest; + console.log("jsdoc exited with code ", code); + if (code === 0 || code === 1) { + resolve(code); + } else { + reject(code) + } + }); + }); +} + +/** + * Creates *-dbg.js files for all JavaScript-resources supplied and writes them to target locator. + * + * @module build/processors/dbg + * + * @param {Object} parameters Parameters + * @param {Array} parameters.resources List of resources to be processed + * @param {ResourceLocatorCollection} parameters.sourceLocator Source locator + * @param {ResourceLocator} parameters.targetLocator Target locator + * @param {Object} [parameters.config] Configuration + * @return {Promise} Promise resolving with undefined once data has been written to the target locator + */ +module.exports = function({resources, options}) { + if ( !options.libraryName ) { + throw new TypeError("Cannot execute JSDoc build without a library name"); + } + const namespace = options.libraryName.replace(/\./g, "/"); + const tmpDirObj = tmp.dirSync(); + const tmpSourceDir = path.join(tmpDirObj.name, 'src'); + const tmpTargetDir = path.join(tmpDirObj.name, 'target'); + + const fsSources = resourceFactory.createAdapter({ + fsBasePath: tmpSourceDir, + virBasePath: "/resources/" + }); + const fsTarget = resourceFactory.createAdapter({ + fsBasePath: tmpTargetDir, + virBasePath: "/" + }); + + //return Promise.resolve([]); + + return Promise.all( + // write all resources to the tmp folder + resources.map((resource) => fsSources.write(resource)) + // after this step, a follow-up step aborts silenty for an unknown reasons + // cloning the resources before writing them avoids the problem: + // resources.map((resource) => resource.clone().then((resource) => fsSources.write(resource))) + ).then(() => [], (err) => { + console.log(err); + return []; + }).then((files) => { + return jsdoc({ + sources: [tmpSourceDir], + target: tmpTargetDir, + namespace, + libraryName: options.libraryName, + version: options.version + }); + }).then(() => { + // create resources from the output files + return Promise.all([ + fsTarget.byPath(`/test-resources/${namespace}/designtime/api.json`) + //,fsTarget.byPath(`/libraries/${options.libraryName}.js`) + ]).then((res) => res.filter($=>$)); + }).then((result) => { + // TODO cleanup tmp dir + return result; + }); +}; diff --git a/lib/processors/jsdoc/transform-apijson-for-sdk.js b/lib/processors/jsdoc/transform-apijson-for-sdk.js new file mode 100644 index 000000000..f4f3a8544 --- /dev/null +++ b/lib/processors/jsdoc/transform-apijson-for-sdk.js @@ -0,0 +1,1732 @@ +/* + * Node script to preprocess api.json files for use in the UI5 SDKs. + * + * (c) Copyright 2009-2018 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +"use strict"; +const fs = require("fs"); +const cheerio = require("cheerio"); +const path = require('path'); + +module.exports = function transformer(sInputFile, sOutputFile, sLibraryFile) { + + console.log("[INFO] Transform API index files for sap.ui.documentation"); + console.log("[INFO] original file: " + sInputFile); + console.log("[INFO] output file: " + sOutputFile); + console.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 + ); + } + + // Transform to object + let oData = JSON.parse(oChainObject.fileData); + + // Attach default component for the library if available + if (oChainObject.defaultComponent) { + oData.defaultComponent = oChainObject.defaultComponent; + } + + // Populate methods.aTreeContent for later use for symbol children if applicable + // NOTE: This will inject missing root sub_namespace entries in oData.symbols array!!! + methods._parseLibraryElements(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.name; + oSymbol.subTitle = formatters.formatSubtitle(oSymbol.deprecated); + + // Symbol children + let aControlChildren = methods._getControlChildren(oSymbol.name); + if (aControlChildren) { + oSymbol.nodes = aControlChildren; + methods._addChildrenDescription(oData.symbols, oSymbol.nodes); + } + + // Constructor + if (oSymbol.constructor) { + let oConstructor = oSymbol.constructor; + + // Description + if (oConstructor.description) { + oConstructor.description = formatters.formatDescription(oConstructor.description); + } + + // References + methods.modifyReferences(oSymbol); + + // 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 (!isBuiltInType(oProperty.type)) { + oProperty.linkEnabled = true; + } + + // Keep file size in check + if (oProperty.static) { + delete oProperty.static; + } + if (oProperty.type) { + 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 + if (oMethod.name) { + oMethod.name = formatters.formatEntityName(oMethod.name, oSymbol.name, oMethod.static); + } + + // 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.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; + } + }); + + } + + + }); + } + + }); + + oChainObject.parsedData = oData; + + return oChainObject; + }; + + /** + * Create api.json from parsed data + * @param oChainObject chain object + */ + function createApiRefApiJson(oChainObject) { + 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 = 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 ' + '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
+			};
+
+		},
+
+		/**
+		 * Pre-process links in text block
+		 * @param {string} sText text block
+		 * @returns {string} processed text block
+		 * @private
+		 */
+		_preProcessLinksInTextBlock: function (sText, bSkipParagraphs) {
+			var topicsData = this._oTopicData, //this.getModel('topics').oData,
+				topicName = topicsData.name || "",
+				topicMethods = topicsData.methods || [],
+				oOptions = {
+					linkFormatter: function (target, text) {
+						var iHashIndex, // indexOf('#')
+							iHashDotIndex, // indexOf('#.')
+							iHashEventIndex, // indexOf('#event:')
+							aMatched,
+							sRoute = "api",
+							sTargetBase,
+							sScrollHandlerClass = "scrollToMethod",
+							sEntityName,
+							aMatch,
+							sLink;
+
+						text = text || target; // keep the full target in the fallback text
+
+						// If the link has a protocol, do not modify, but open in a new window
+						if (target.match("://")) {
+							return '' + text + '';
+						}
+
+						target = target.trim().replace(/\.prototype\./g, "#");
+
+						// Link matches the pattern of an static extend method sap.ui.core.Control.extend
+						// BCP: 1780477951
+						aMatch = target.match(/^([a-zA-Z0-9\.]*)\.extend$/);
+						if (aMatch) {
+							// In this case the link should be a link to a static method of the control like for example
+							// #/api/sap.ui.core.Control/methods/sap.ui.core.Control.extend
+							target = aMatch[1] + "/methods/" + aMatch[0];
+							sEntityName = aMatch[1];
+							sScrollHandlerClass = false; // No scroll handler needed
+						} else {
+
+							iHashIndex = target.indexOf('#');
+							iHashDotIndex = target.indexOf('#.');
+							iHashEventIndex = target.indexOf('#event:');
+
+							if (iHashIndex === -1) {
+								var lastDotIndex = target.lastIndexOf('.'),
+									entityName = sEntityName = target.substring(lastDotIndex + 1),
+									targetMethod = topicMethods.filter(function (method) {
+										if (method.name === entityName) {
+											return method;
+										}
+									})[0];
+
+								if (targetMethod) {
+									if (targetMethod.static === true) {
+										sEntityName = target;
+										// We need to handle links to static methods in a different way if static method is
+										// a child of the current or a different entity
+										sTargetBase = target.replace("." + entityName, "");
+										if (sTargetBase.length > 0 && sTargetBase !== topicName) {
+											// Different entity
+											target = sTargetBase + "/methods/" + target;
+											// We will navigate to a different entity so no scroll is needed
+											sScrollHandlerClass = false;
+										} else {
+											// Current entity
+											target = topicName + '/methods/' + target;
+										}
+									} else {
+										target = topicName + '/methods/' + entityName;
+									}
+								} else {
+									// Handle links to documentation
+									aMatched = target.match(/^topic:(\w{32})$/);
+									if (aMatched) {
+										target = sEntityName = aMatched[1];
+										sRoute = "topic";
+									}
+								}
+							}
+
+							if (iHashDotIndex === 0) {
+								// clear '#.' from target string
+								target = target.slice(2);
+
+								target = topicName + '/methods/' + topicName + '.' + target;
+							} else if (iHashEventIndex >= 0) {
+								//format is 'className#event:eventName'  or  '#event:eventName'
+								var sClassName = target.substring(0, iHashIndex);
+								target = target.substring(iHashIndex);
+
+								// clear '#event:' from target string
+								target = target.slice('#event:'.length);
+
+								if (!sClassName) {
+									sClassName = topicName; // if no className => event is relative to current topicName
+									sScrollHandlerClass = "scrollToEvent"; // mark the element as relative link to the events section
+								}
+
+								target = sClassName + '/events/' + target;
+								sEntityName = target;
+
+							} else if (iHashIndex === 0) {
+								// clear '#' from target string
+								target = target.slice(1);
+								sEntityName = target;
+
+								target = topicName + '/methods/' + target;
+							} else if (iHashIndex > 0) {
+								target = target.replace('#', '/methods/');
+								sEntityName = target;
+							}
+
+						}
+
+						sLink = '' + text + '';
+
+						return sLink;
+
+					}
+				};
+
+			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(""); + }, + + _formatChildDescription: function (description) { + if (description) { + return this._extractFirstSentence(description); + } + }, + + /** Just the first sentence (up to a full stop). Should not break on dotted variable names. */ + _extractFirstSentence: function(desc) { + if ( desc ) { + desc = String(desc).replace(/\s+/g, ' '). + replace(/^(<\/?p>||\w+<\/h\d>|\s)+/, ''); + + var match = /([\w\W]+?\.)[^a-z0-9_$]/i.exec(desc); + return match ? match[1] : desc; + } + return ""; + }, + + _sliceSpecialTags: function (descriptionCopy, startSymbol, endSymbol) { + var startIndex, endIndex; + while (descriptionCopy.indexOf(startSymbol) !== -1 && descriptionCopy.indexOf(startSymbol) < descriptionCopy.indexOf(".")) { + startIndex = descriptionCopy.indexOf(startSymbol); + endIndex = descriptionCopy.indexOf(endSymbol); + descriptionCopy = descriptionCopy.slice(0, startIndex) + descriptionCopy.slice(endIndex + endSymbol.length, descriptionCopy.length); + } + return descriptionCopy; + } + + }; + + /* Methods direct copy from API Detail */ + let methods = { + + /** + * Pre-process and modify references + * @param {object} oSymbol control data object which will be modified + * @private + */ + modifyReferences: function (oSymbol) { + var bHeaderDocuLinkFound = false, + bUXGuidelinesLinkFound = false, + aReferences = []; + + if (oSymbol.constructor.references && oSymbol.constructor.references.length > 0) { + oSymbol.constructor.references.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:https://experience.sap.com/fiori-design-web/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:(\S+)(\s.+)?}$|^fiori:(\S+)$/); + + if (aParts) { + + if (!bUXGuidelinesLinkFound) { + // Extract first found UX Guidelines link as primary + if (aParts) { + if (aParts[3]) { + // String of type: "fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/" + oSymbol.uxGuidelinesLink = aParts[3]; + oSymbol.uxGuidelinesLinkText = oSymbol.basename; + } else if (aParts[1]) { + // String of type: "{@link fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/}" + // or + // String of type: "{@link fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/ Flexible Column Layout}" + oSymbol.uxGuidelinesLink = aParts[1]; + oSymbol.uxGuidelinesLinkText = aParts[2] ? aParts[2] : oSymbol.basename; + } + bUXGuidelinesLinkFound = true; + return; + } + } else { + // BCP: 1870155880 - Every consecutive "fiori:" link should be handled as a normal link + sReference = sReference.replace("fiori:", ""); + } + + } + + aReferences.push(sReference); + }); + oSymbol.constructor.references = aReferences; + } else { + oSymbol.constructor.references = []; + } + }, + + /** + * 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: {}, + aTreeContent: [], + + _getControlChildren: function (sTopicId) { + // Find tree node + var findTreeNode = function (aNodes, sTopicId) { + var iLen, + oNode, + i; + + for (i = 0, iLen = aNodes.length; i < iLen; i++) { + oNode = aNodes[i]; + if (oNode.name === sTopicId) { + return oNode; + } + if (oNode.nodes) { + oNode = findTreeNode(aNodes[i].nodes, sTopicId); + if (oNode) { + return oNode; + } + } + } + }, + oNode = findTreeNode(this.aTreeContent, sTopicId); + + return oNode.nodes ? oNode.nodes : false; + }, + + _parseLibraryElements : function (aLibraryElements) { + var oLibraryElement, + aNodes, + i; + + for (i = 0; i < aLibraryElements.length; i++) { + oLibraryElement = aLibraryElements[i]; + aNodes = oLibraryElement.nodes; + + if (!aNodes) { + this.oLibsData[oLibraryElement.name] = oLibraryElement; + } + + this._addElementToTreeData(oLibraryElement, aLibraryElements); + + if (aNodes) { + this._parseLibraryElements(aNodes, true); + } + } + + return this.aTreeContent; + }, + + _addElementToTreeData : function (oJSONElement, aLibraryElements) { + var oNewNodeNamespace; + + if (oJSONElement.kind !== "namespace") { + var aNameParts = oJSONElement.name.split("."), + sBaseName = aNameParts.pop(), + sNodeNamespace = aNameParts.join("."), // Note: Array.pop() on the previous line modifies the array itself + oTreeNode = this._createTreeNode(sBaseName, oJSONElement.name), + oExistingNodeNamespace = this._findNodeNamespaceInTreeStructure(sNodeNamespace); + + if (oExistingNodeNamespace) { + if (!oExistingNodeNamespace.nodes) { + oExistingNodeNamespace.nodes = []; + } + oExistingNodeNamespace.nodes.push(oTreeNode); + } else if (sNodeNamespace) { + oNewNodeNamespace = this._createTreeNode(sNodeNamespace, sNodeNamespace); + oNewNodeNamespace.nodes = []; + oNewNodeNamespace.nodes.push(oTreeNode); + this.aTreeContent.push(oNewNodeNamespace); + + this._removeDuplicatedNodeFromTree(sNodeNamespace); + + // Inject missing new root namespace in main collection + aLibraryElements.push({ + kind: "namespace", // Note: we show this elements as namespaces + name: sNodeNamespace, + ref: "#/api/" + sNodeNamespace + }); + + } else { + // Entities for which we can't resolve namespace we are shown in the root level + oNewNodeNamespace = this._createTreeNode(oJSONElement.name, oJSONElement.name); + this.aTreeContent.push(oNewNodeNamespace); + } + } else { + oNewNodeNamespace = this._createTreeNode(oJSONElement.name, oJSONElement.name); + this.aTreeContent.push(oNewNodeNamespace); + } + }, + + _createTreeNode : function (text, name, sLib) { + var oTreeNode = {}; + oTreeNode.text = text; + oTreeNode.name = name; + oTreeNode.ref = "#/api/" + name; + return oTreeNode; + }, + + _findNodeNamespaceInTreeStructure : function (sNodeNamespace, aTreeStructure) { + aTreeStructure = aTreeStructure || this.aTreeContent; + for (var i = 0; i < aTreeStructure.length; i++) { + var oTreeNode = aTreeStructure[i]; + if (oTreeNode.name === sNodeNamespace) { + return oTreeNode; + } + if (oTreeNode.nodes) { + var oChildNode = this._findNodeNamespaceInTreeStructure(sNodeNamespace, oTreeNode.nodes); + if (oChildNode) { + return oChildNode; + } + } + } + }, + + _removeNodeFromNamespace : function (sNode, oNamespace) { + for (var i = 0; i < oNamespace.nodes.length; i++) { + if (oNamespace.nodes[i].text === sNode) { + oNamespace.nodes.splice(i, 1); + return; + } + } + }, + + _removeDuplicatedNodeFromTree : function (sNodeFullName) { + if (this.oLibsData[sNodeFullName]) { + var sNodeNamespace = sNodeFullName.substring(0, sNodeFullName.lastIndexOf(".")); + var oNamespace = this._findNodeNamespaceInTreeStructure(sNodeNamespace); + var sNode = sNodeFullName.substring(sNodeFullName.lastIndexOf(".") + 1, sNodeFullName.lenght); + this._removeNodeFromNamespace(sNode, oNamespace); + } + }, + _addChildrenDescription: function (aLibsData, aControlChildren) { + function getDataByName (sName) { + var iLen, + i; + + for (i = 0, iLen = aLibsData.length; i < iLen; i++) { + if (aLibsData[i].name === sName) { + return aLibsData[i]; + } + } + return false; + } + for (var i = 0; i < aControlChildren.length; i++) { + aControlChildren[i].description = formatters._formatChildDescription(getDataByName(aControlChildren[i].name).description); + aControlChildren[i].description = formatters._preProcessLinksInTextBlock(aControlChildren[i].description, true); + + // Handle nesting + if (aControlChildren[i].nodes) { + this._addChildrenDescription(aLibsData, aControlChildren[i].nodes); + } + } + } + }; + + // Create the chain object + let oChainObject = { + inputFile: sInputFile, + outputFile: sOutputFile, + libraryFile: sLibraryFile + }; + + // Start the work here + var p = getLibraryPromise(oChainObject) + .then(extractComponentAndDocuindexUrl) + .then(flattenComponents) + .then(extractSamplesFromDocuIndex) + .then(getAPIJSONPromise) + .then(transformApiJson) + .then(createApiRefApiJson); + return p; + +} diff --git a/lib/processors/jsdoc/ui5/plugin.js b/lib/processors/jsdoc/ui5/plugin.js new file mode 100644 index 000000000..9a323ea06 --- /dev/null +++ b/lib/processors/jsdoc/ui5/plugin.js @@ -0,0 +1,2321 @@ +/* + * JSDoc3 plugin for UI5 documentation generation. + * + * (c) Copyright 2009-2018 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +/* 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; +} + +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); +} + +//---- 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 + */ +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]); + } + } + } + + 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); + }); +} + +/** + * 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. + * + * @param node + * @param defaultKey + * @returns {Map} + */ +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]; + var name; + //console.log("objectproperty " + prop.type); + if ( prop.key.type === Syntax.Identifier ) { + name = prop.key.name; + } else if ( prop.key.type === Syntax.Literal ) { + name = String(prop.key.value); + } else { + name = prop.key.toSource(); + } + //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 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.module ) { + var 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 mPossibleKeys = {draggable: 1, droppable: 1}; + var mDefaults = { + "self" : { draggable : true, droppable: true }, + "0..1" : { draggable : true, droppable: true }, + "0..n" : { draggable : false, droppable: false } + }; + + if ( node.type === Syntax.ObjectExpression ) { + mDragDropValue = (node.properties || []).reduce(function(oObject, oProperty) { + var sKey = convertValue(oProperty.key); + if (mPossibleKeys[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[cardinality || "self"], 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); + //} + if ( oClassInfo.properties[n].visibility !== 'public' ) { + error("Property '" + n + "' uses visibility '" + oClassInfo.properties[n].visibility + "' which is not supported by the runtime"); + } + }); + + 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; + } + if ( oClassInfo.associations[n].visibility !== 'public' ) { + error("Association '" + n + "' uses visibility '" + oClassInfo.associations[n].visibility + "' which is not supported by the runtime"); + } + }); + + 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 { + console.log(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 + * @returns {String} + */ +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 commentNode + * @returns + */ +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. + */ +function wrap(lines) { + if ( typeof lines === "string" ) { + lines = lines.split(/\r\n?|\n/); + } + return "/**\n * " + lines.join('\n * ') + "\n */"; +} + +/** + * Preprocesses a JSDoc comment string to ensure some UI5 standards. + * + * @param {event} e Event for the new comment + * @returns {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 + */ + parseBegin : function(e) { + + pathPrefixes = env.opts._.reduce(function(result, fileOrDir) { + fileOrDir = path.resolve( path.normalize(fileOrDir) ); + if ( fs.statSync(fileOrDir).isDirectory() ) { + if ( !fileOrDir.endsWith(path.sep) ) { + 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 + */ + fileBegin: function (e) { + currentProgram = undefined; + currentModule = { + name: null, + resource: getResourceName(e.filename), + module: getModuleName(getResourceName(e.filename)), + localNames: Object.create(null) + }; + debug(currentModule); + }, + + fileComplete: function (e) { + 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 ) { + 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); + } + }) + } + } + + 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.length == 1 && isExtendCall(node.declarations[0].init) ) { + + // var NewClass = Something.extend(...) + + // className = node.declarations[0].init.arguments[0].value; + comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.declarations[0]); + // console.log("ast node with comment " + comment); + processExtendCall(node.declarations[0].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 && isCreateDataTypeCall(node.expression.right) ) { + + // thisLib.TypeName = DataType.createType( ... ) + comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.expression); + processDataType(node.expression.right); + } + } + +}; diff --git a/lib/processors/jsdoc/ui5/template/publish.js b/lib/processors/jsdoc/ui5/template/publish.js new file mode 100644 index 000000000..5a766771a --- /dev/null +++ b/lib/processors/jsdoc/ui5/template/publish.js @@ -0,0 +1,4056 @@ +/* + * JSDoc3 template for UI5 documentation generation. + * + * (c) Copyright 2009-2018 SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. + */ + +/*global env: true */ +/*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 = {}; + +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); + } +} + +function merge(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i]; + Object.keys(source).forEach(function(p) { + var v = source[p]; + target[p] = ( v.constructor === Object ) ? merge(target[p] || {}, v) : v; + }); + } + 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) { + try { + var file = path.join(templateConf.apiJsonFolder, localFileName); + 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 isaClass($) { + return /^(namespace|interface|class|typedef)$/.test($.kind) || ($.kind === 'member' && $.isEnum) /* isNonEmptyNamespace($) */; +} + +var REGEXP_ARRAY_TYPE = /^Array\.<(.*)>$/; + +// ---- Version class ----------------------------------------------------------------------------------------------------------------------------------------------------------- + +var Version = (function() { + + var rVersion = /^[0-9]+(?:\.([0-9]+)(?:\.([0-9]+))?)?(.*)$/; + + /** + * Returns a Version instance created from the given parameters. + * + * This function can either be called as a constructor (using new) or as a normal function. + * It always returns an immutable Version instance. + * + * The parts of the version number (major, minor, patch, suffix) can be provided in several ways: + *
    + *
  • Version("1.2.3-SNAPSHOT") - as a dot-separated string. Any non-numerical char or a dot followed by a non-numerical char starts the suffix portion. + * Any missing major, minor or patch versions will be set to 0.
  • + *
  • Version(1,2,3,"-SNAPSHOT") - as individual parameters. Major, minor and patch must be integer numbers or empty, suffix must be a string not starting with digits.
  • + *
  • Version([1,2,3,"-SNAPSHOT"]) - as an array with the individual parts. The same type restrictions apply as before.
  • + *
  • Version(otherVersion) - as a Version instance (cast operation). Returns the given instance instead of creating a new one.
  • + *
+ * + * To keep the code size small, this implementation mainly validates the single string variant. + * All other variants are only validated to some degree. It is the responsibility of the caller to + * provide proper parts. + * + * @param {int|string|any[]|jQuery.sap.Version} vMajor the major part of the version (int) or any of the single parameter variants explained above. + * @param {int} iMinor the minor part of the version number + * @param {int} iPatch the patch part of the version number + * @param {string} sSuffix the suffix part of the version number + * @return {jQuery.sap.Version} the version object as determined from the parameters + * + * @class Represents a version consisting of major, minor, patch version and suffix, e.g. '1.2.7-SNAPSHOT'. + * + * @author SAP SE + * @version ${version} + * @constructor + * @public + * @since 1.15.0 + * @name jQuery.sap.Version + */ + function Version(versionStr) { + + var match = rVersion.exec(versionStr) || []; + + function norm(v) { + v = parseInt(v,10); + 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 || + ((this.suffix < other.suffix) ? -1 : (this.suffix === other.suffix) ? 0 : 1); + }; + + 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($) { + 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 isaClass(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 isaClass(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 && isaClass(parent) ) { + parent.__ui5.members = parent.__ui5.members || []; + parent.__ui5.members.push($); + } + } + }); +} + +function mergeEventDocumentation() { + + console.log("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; + } + + // console.log('mergeing 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; + //console.log("<<<<<<<"); + //console.log(event); + //console.log("======="); + //console.log($); + + $.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 ) { + console.log(" merged documentation for managed event " + symbol.longname + "#" + $.name); + } + + //console.log("======="); + //console.log(JSON.stringify(event, null, '\t')); + //console.log(">>>>>>>"); + } + }); + + }); + +} + +// ---- publishing ----------------------------------------------------------------------- + +function publishClasses(symbols, aRootNamespaces, hierarchyRoots) { + + // create output dir + fs.mkPath(path.join(conf.outdir, conf.symbolsDir)); + + // get a list of all the classes in the symbolset + var classes = symbols(function() { + return isaClass(this) && conf.filter(this); + }).order("longname"); + + // create unique file names + __uniqueFilenames = {}; + var filenames = {}; + classes.get().sort(sortByAlias).forEach(function(symbol) { + var filename = escape(symbol.longname); + if ( filenames.hasOwnProperty(filename.toUpperCase()) && (filenames[filename.toUpperCase()].longname !== symbol.longname) ) { + // find an unused filename by appending "-n" where n is an integer > 0 + for (var j = 1; 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", classes); + publish.footer = processTemplate("_footer.tmpl", classes); + publish.classesIndex = processTemplate("_navIndex.tmpl", classes); // 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 = ""; + classes.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(classes.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", classes); + publish.footer = processTemplate("_footer.tmpl", classes); + publish.classesIndex = processTemplate("_navIndex.tmpl", classes); + + // create the all classes index + processTemplateAndSave("index.html.tmpl", classes, "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); + } + + classes = 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; + } + + 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; + } + + 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.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); + + try { + var 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"); + console.log("**** failed to process template, source written to " + filename); + fs.mkPath(path.dirname(filename)); + fs.writeFileSync(filename, e.source, 'utf8'); + } + console.log("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;
+				// console.log('  "' + text + '" (preformatted)');
+			} else {
+				text = text.replace(/\s+/g,' ');
+				if ( text.trim() ) {
+					compressed += text;
+				}
+				// console.log('  "' + text + '" (trimmed)');
+			}
+		}
+
+		compressed += m[0];
+		// console.log('  "' + 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;
+			// console.log('  "' + text + '" (preformatted)');
+		} else {
+			text = text.replace(/\s+/g,' ');
+			if ( text.trim() ) {
+				compressed += text;
+			}
+			// console.log('  "' + 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; +} + + +//console.log("#### samples"); +//console.log(formatText(summarize("This is a first\n\nparagraph with empty \n \n \nlines in it. This is the remainder."))); + +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: [] + }; + //console.log("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 of which these are the members + * @param borrowedMembers set of borrowed members to determine the contributors for + * @return 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*(\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 elementType = parseType(); + next('>'); + type = builder.promise(elementType); + } 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) { + console.log("Type: '" + type + "' gives AST"); + try { + console.log(typeParser.parse(type)); + } catch (e) { + console.log("**** 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 ); +} + +/** + * 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 sText + * @return 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 ( isaClass(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) {
+		var emptyTag = arguments.length === 1;
+		if ( omitDefaults && arguments.length >= 3 && value === defaultValue ) {
+			return;
+		}
+		curr[name] = emptyTag ? true : (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(paramName, params) {
+		var prefix = paramName + '.',
+			count = 0,
+			i;
+
+		for ( i = 0; i < params.length; i++ ) {
+
+			var name = params[i].name;
+			if ( name.lastIndexOf(prefix, 0) !== 0 ) { // startsWith
+				continue;
+			}
+			name = name.slice(prefix.length);
+			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].name, 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");
+		}
+	}
+
+	/*
+	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 ( symbol.virtual ) {
+		attrib("abstract", true, false, /* raw = */true);
+	}
+	if ( symbol.final_ ) {
+		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));
+	}
+	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);
+	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 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));
+			if (symbol.params && symbol.params.length > 0) {
+				collection("parameters");
+				for (j = 0; j < symbol.params.length; j++) {
+					param = symbol.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.name, symbol.params);
+					tag("description", normalizeWS(param.description), true);
+					tagWithSince("experimental", param.experimental);
+					tagWithSince("deprecated", param.deprecated);
+					closeTag("parameter");
+				}
+				endCollection("parameters");
+			}
+			exceptions(symbol);
+			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");
+		}
+	}
+
+	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.name, 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");
+		for ( i = 0; i < ownMethods.length; i++ ) {
+			member = ownMethods[i];
+			tag("method");
+			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.tags && member.tags.some(function(tag) { return tag.title === 'ui5-metamodel'; }) ) {
+				attrib('ui5-metamodel', true, false, /* raw = */true);
+			}
+
+			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.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));
+					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.name, member.params);
+					tag("description", normalizeWS(param.description), true);
+					tagWithSince("experimental", param.experimental);
+					tagWithSince("deprecated", param.deprecated);
+					closeTag("parameter");
+				}
+				endCollection("parameters");
+			}
+			exceptions(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");
+		}
+		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 guessExport(defaultExport, symbol) {
+		if ( symbol.name === defaultExport ) {
+			// default export equals the symbol name
+			symbol.symbol.export = ""; 
+			//console.log("    (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); 
+			//console.log("    " + 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;
+			console.log("    **** could not identify module export for API " + symbol.name);
+		}
+	}
+	
+	for ( n in modules ) {
+		
+		symbols = modules[n].sort(function(a,b) {
+			if ( a.name === b.name ) {
+				return 0;
+			}
+			return a.name < b.name ? -1 : 1;
+		});
+		
+		// console.log('  resolved exports of ' + n + ": " + symbols.map(function(symbol) { return symbol.name; } ));
+		if ( /^jquery\.sap\./.test(n) ) {
+			// the jquery.sap.* modules all export 'jQuery'.
+			// any API from those modules is reachable via 'jQuery.*'
+			defaultExport = 'jQuery';
+			symbols.forEach(
+				guessExport.bind(this, defaultExport)
+			);
+		} else if ( /\/library$/.test(n) ) {
+			// library.js modules export the library namespace
+			defaultExport = n.replace(/\/library$/, "").replace(/\//g, ".");
+			if ( symbols.some(function(symbol) { return symbol.name === defaultExport; }) ) {
+				// if there is a symbol for the namespace, then all other symbols from the module should be sub-exports of that symbol
+				symbols.forEach(
+					guessExport.bind(this, defaultExport)
+				);
+			} else {
+				// otherwise, we don't know how to map it to an export
+				symbols.forEach(function(symbol) {
+					symbol.symbol.export = symbol.name;
+					console.log("    **** unresolved " + symbol.name + " in library.js (no export that matches module name)");
+				});
+			}
+		} else {
+			// for all other modules, the assumed default export is identical to the name of the module (converted to a 'dot' name)
+			defaultExport = n.replace(/\//g, ".");
+			if ( symbols.some(function(symbol) { return symbol.name === defaultExport; }) ) {
+				symbols.forEach(
+					guessExport.bind(this, defaultExport)
+				);
+			//} else if ( symbols.length === 1 && (symbols[0].symbol.kind === 'class' || symbols[0].symbol.kind === 'namespace') ) {
+				// if there is only one symbol and if that symbol is of type class or namespace, assume it is the default export
+				// TODO is that assumption safe? Was only done because of IBarPageEnabler (which maybe better should be fixed in the JSDoc)
+				//symbols[0].symbol.export = '';
+			} else {
+				symbols.forEach(function(symbol) {
+					symbol.symbol.export = undefined;
+					console.log("    **** unresolved " + symbol.name + " (no export that matches module name)");
+				});
+			}
+		}
+	}
+}
+
+//---- 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 ( isaClass(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) { + //console.log(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' ) { + //console.log("found namespace " + symbol.longname); + //console.log(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/tasks/createJSDoc.js b/lib/tasks/createJSDoc.js new file mode 100644 index 000000000..f7e4d9c69 --- /dev/null +++ b/lib/tasks/createJSDoc.js @@ -0,0 +1,29 @@ +const runJSDoc = require("../processors/jsdoc/jsdoc"); + +/** + * Task to create dbg files. + * + * @module builder/tasks/createDebugFiles + * @param {Object} parameters Parameters + * @param {DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {Object} [parameters.options] Options + * @param {string} [parameters.options.pattern] Pattern to locate the files to be processed + * @returns {Promise} Promise resolving with undefined once data has been written + */ +module.exports = async function({workspace, options}) { + let allResources; + if (workspace.byGlobSource) { // API only available on duplex collections + allResources = await workspace.byGlobSource(options.pattern); + } else { + allResources = await workspace.byGlob(options.pattern); + } + return runJSDoc({ + resources: allResources, + options + }).then((createdResources) => { + console.log(createdResources); + return Promise.all(createdResources.map((resource) => { + return workspace.write(resource); + })); + }); +}; diff --git a/lib/tasks/taskRepository.js b/lib/tasks/taskRepository.js index 2b1285c5b..3afffd6e0 100644 --- a/lib/tasks/taskRepository.js +++ b/lib/tasks/taskRepository.js @@ -2,6 +2,7 @@ const tasks = { replaceCopyright: require("./replaceCopyright"), replaceVersion: require("./replaceVersion"), createDebugFiles: require("./createDebugFiles"), + createJSDoc: require("./createJSDoc"), uglify: require("./uglify"), buildThemes: require("./buildThemes"), transformBootstrapHtml: require("./transformBootstrapHtml"), diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index 98fb709b4..f38f6117a 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -8,6 +8,7 @@ const tasks = { // can't require index.js due to circular dependency generateStandaloneAppBundle: require("../../tasks/bundlers/generateStandaloneAppBundle"), buildThemes: require("../../tasks/buildThemes"), createDebugFiles: require("../../tasks/createDebugFiles"), + createJSDoc: require("../../tasks/createJSDoc"), generateLibraryManifest: require("../../tasks/generateLibraryManifest"), generateVersionInfo: require("../../tasks/generateVersionInfo"), replaceCopyright: require("../../tasks/replaceCopyright"), @@ -39,6 +40,20 @@ class LibraryBuilder extends AbstractBuilder { }); }); + this.addTask("createJSDoc", () => { + const createJSDoc = tasks.createJSDoc; + return createJSDoc({ + workspace: resourceCollections.workspace, + options: { + libraryName: project.metadata.name, + version: project.version, + pattern: "/resources/**/*.js" + } + }).then(() => { + console.log("createJSDOC done"); + }); + }); + const componentPreload = project.builder && project.builder.componentPreload; if (componentPreload) { const generateComponentPreload = tasks.generateComponentPreload; @@ -75,6 +90,9 @@ class LibraryBuilder extends AbstractBuilder { options: { projectName: project.metadata.name } + }).catch((err) => { + console.log("generateLibraryPreload failed:", err); + throw err; }); }); diff --git a/package-lock.json b/package-lock.json index 182864a19..1f05464e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1605,15 +1605,6 @@ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, - "catharsis": { - "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" - } - }, "chai": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", @@ -2083,7 +2074,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", - "dev": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -2914,6 +2904,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": { @@ -4517,8 +4518,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -4581,58 +4581,12 @@ } } }, - "js2xmlparser": { - "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" - } - }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, - "jsdoc": { - "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", - "catharsis": "~0.8.9", - "escape-string-regexp": "~1.0.5", - "js2xmlparser": "~3.0.0", - "klaw": "~2.0.0", - "marked": "~0.3.6", - "mkdirp": "~0.5.1", - "requizzle": "~0.2.1", - "strip-json-comments": "~2.0.1", - "taffydb": "2.6.2", - "underscore": "~1.8.3" - }, - "dependencies": { - "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 - }, - "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" - } - } - } - }, "jsdoctypeparser": { "version": "2.0.0-alpha-8", "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-2.0.0-alpha-8.tgz", @@ -4700,15 +4654,6 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" }, - "klaw": { - "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" - } - }, "last-line-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/last-line-stream/-/last-line-stream-1.0.0.tgz", @@ -4955,7 +4900,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "dev": true, "requires": { "pseudomap": "^1.0.2", "yallist": "^2.1.2" @@ -4988,12 +4932,6 @@ "object-visit": "^1.0.0" } }, - "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 - }, "matcher": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz", @@ -6690,8 +6628,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", @@ -6994,8 +6931,7 @@ "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "punycode": { "version": "1.4.1", @@ -7320,23 +7256,6 @@ "integrity": "sha1-WhtS63Dr7UPrmC6XTIWrWVceVvo=", "dev": true }, - "requizzle": { - "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" - }, - "dependencies": { - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true - } - } - }, "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", @@ -7494,7 +7413,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -7502,8 +7420,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "sift": { "version": "5.1.0", @@ -8004,12 +7921,6 @@ } } }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true - }, "tap-nyan": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/tap-nyan/-/tap-nyan-1.1.0.tgz", @@ -8147,12 +8058,11 @@ "dev": true }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.31.tgz", + "integrity": "sha1-jzirlDjhcxXl29izZX6L+yd65Kc=", "requires": { - "os-tmpdir": "~1.0.2" + "os-tmpdir": "~1.0.1" } }, "to-fast-properties": { @@ -8292,29 +8202,6 @@ "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", "dev": true }, - "underscore": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", - "dev": true - }, - "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" - }, - "dependencies": { - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true - } - } - }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", @@ -8514,7 +8401,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, "requires": { "isexe": "^2.0.0" } @@ -8668,12 +8554,6 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, - "xmlcreate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", - "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", - "dev": true - }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", @@ -8683,8 +8563,7 @@ "yallist": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs-parser": { "version": "10.1.0", diff --git a/package.json b/package.json index b631b5a89..4af010f9c 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "@ui5/fs": "^1.0.1", "@ui5/logger": "^1.0.0", "cheerio": "^0.22.0", + "cross-spawn": "^5.1.0", "escodegen": "^1.11.0", "escope": "^3.6.0", "esprima": "^4.0.1", @@ -106,6 +107,7 @@ "pretty-hrtime": "^1.0.3", "replacestream": "^4.0.3", "semver": "^5.6.0", + "tmp": "0.0.31", "uglify-es": "^3.2.2", "xml2js": "^0.4.17", "yazl": "^2.5.1" @@ -121,7 +123,6 @@ "eslint-config-google": "^0.11.0", "eslint-plugin-jsdoc": "^4.0.1", "extract-zip": "^1.6.7", - "jsdoc": "^3.5.5", "mock-require": "^3.0.3", "nyc": "^13.2.0", "opn-cli": "^4.0.0",