diff --git a/.eslintignore b/.eslintignore
index 5c9eafbbe..5f3b1d6e9 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,5 +1,8 @@
# /node_modules/* and /bower_components/* ignored by default
+# Exclude shared resources that can't (yet) follow our conventions
+lib/processors/jsdoc/lib
+
# Exclude coverage folder
coverage/
diff --git a/index.js b/index.js
index 59f3eaf65..db8ee8ae9 100644
--- a/index.js
+++ b/index.js
@@ -13,6 +13,9 @@ module.exports = {
flexChangesBundler: require("./lib/processors/bundlers/flexChangesBundler"),
manifestBundler: require("./lib/processors/bundlers/manifestBundler"),
moduleBundler: require("./lib/processors/bundlers/moduleBundler"),
+ apiIndexGenerator: require("./lib/processors/jsdoc/apiIndexGenerator"),
+ jsdocGenerator: require("./lib/processors/jsdoc/jsdocGenerator"),
+ sdkTransformer: require("./lib/processors/jsdoc/sdkTransformer"),
bootstrapHtmlTransformer: require("./lib/processors/bootstrapHtmlTransformer"),
debugFileCreator: require("./lib/processors/debugFileCreator"),
resourceCopier: require("./lib/processors/resourceCopier"),
@@ -35,6 +38,9 @@ module.exports = {
generateBundle: require("./lib/tasks/bundlers/generateBundle"),
buildThemes: require("./lib/tasks/buildThemes"),
createDebugFiles: require("./lib/tasks/createDebugFiles"),
+ executeJsdocSdkTransformation: require("./lib/tasks/jsdoc/executeJsdocSdkTransformation"),
+ generateApiIndex: require("./lib/tasks/jsdoc/generateApiIndex"),
+ generateJsdoc: require("./lib/tasks/jsdoc/generateJsdoc"),
generateVersionInfo: require("./lib/tasks/generateVersionInfo"),
replaceCopyright: require("./lib/tasks/replaceCopyright"),
replaceVersion: require("./lib/tasks/replaceVersion"),
diff --git a/lib/builder/builder.js b/lib/builder/builder.js
index 01307c6e0..5c34192d5 100644
--- a/lib/builder/builder.js
+++ b/lib/builder/builder.js
@@ -1,5 +1,6 @@
const log = require("@ui5/logger").getGroupLogger("builder:builder");
const resourceFactory = require("@ui5/fs").resourceFactory;
+const MemAdapter = require("@ui5/fs").adapters.Memory;
const typeRepository = require("../types/typeRepository");
const taskRepository = require("../tasks/taskRepository");
@@ -36,20 +37,24 @@ function getElapsedTime(startTime) {
* @param {Object} parameters
* @param {boolean} parameters.dev Sets development mode, which only runs essential tasks
* @param {boolean} parameters.selfContained True if a the build should be self-contained or false for prelead build bundles
+ * @param {boolean} parameters.jsdoc True if a JSDoc build should be executed
* @param {Array} parameters.includedTasks Task list to be included from build
* @param {Array} parameters.excludedTasks Task list to be excluded from build
* @returns {Array} Return a task list for the builder
*/
-function composeTaskList({dev, selfContained, includedTasks, excludedTasks}) {
+function composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTasks}) {
let selectedTasks = Object.keys(definedTasks).reduce((list, key) => {
list[key] = true;
return list;
}, {});
- // Exclude tasks: manifestBundler
+ // Exclude non default tasks
selectedTasks.generateManifestBundle = false;
selectedTasks.generateStandaloneAppBundle = false;
selectedTasks.transformBootstrapHtml = false;
+ selectedTasks.generateJsdoc = false;
+ selectedTasks.executeJsdocSdkTransformation = false;
+ selectedTasks.generateApiIndex = false;
if (selfContained) {
// No preloads, bundle only
@@ -59,6 +64,27 @@ function composeTaskList({dev, selfContained, includedTasks, excludedTasks}) {
selectedTasks.generateLibraryPreload = false;
}
+ if (jsdoc) {
+ // Include JSDoc tasks
+ selectedTasks.generateJsdoc = true;
+ selectedTasks.executeJsdocSdkTransformation = true;
+ selectedTasks.generateApiIndex = true;
+
+ // Include theme build as required for SDK
+ selectedTasks.buildThemes = true;
+
+ // Exclude all tasks not relevant to JSDoc generation
+ selectedTasks.replaceCopyright = false;
+ selectedTasks.replaceVersion = false;
+ selectedTasks.generateComponentPreload = false;
+ selectedTasks.generateLibraryPreload = false;
+ selectedTasks.generateLibraryManifest = false;
+ selectedTasks.createDebugFiles = false;
+ selectedTasks.uglify = false;
+ selectedTasks.generateFlexChangesBundle = false;
+ selectedTasks.generateManifestBundle = false;
+ }
+
// Only run essential tasks in development mode, it is not desired to run time consuming tasks during development.
if (dev) {
// Overwrite all other tasks with noop promise
@@ -123,6 +149,7 @@ module.exports = {
* @param {boolean} [parameters.buildDependencies=false] Decides whether project dependencies are built as well
* @param {boolean} [parameters.dev=false] Decides whether a development build should be activated (skips non-essential and time-intensive tasks)
* @param {boolean} [parameters.selfContained=false] Flag to activate self contained build
+ * @param {boolean} [parameters.jsdoc=false] Flag to activate JSDoc build
* @param {Array} [parameters.includedTasks=[]] List of tasks to be included
* @param {Array} [parameters.excludedTasks=[]] List of tasks to be excluded. If the wildcard '*' is provided, only the included tasks will be executed.
* @param {Array} [parameters.devExcludeProject=[]] List of projects to be excluded from development build
@@ -130,7 +157,7 @@ module.exports = {
*/
build({
tree, destPath,
- buildDependencies = false, dev = false, selfContained = false,
+ buildDependencies = false, dev = false, selfContained = false, jsdoc = false,
includedTasks = [], excludedTasks = [], devExcludeProject = []
}) {
const startTime = process.hrtime();
@@ -138,7 +165,7 @@ module.exports = {
" including dependencies..." + (dev ? " [dev mode]" : ""));
log.verbose(`Building to ${destPath}...`);
- const selectedTasks = composeTaskList({dev, selfContained, includedTasks, excludedTasks});
+ const selectedTasks = composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTasks});
const fsTarget = resourceFactory.createAdapter({
fsBasePath: destPath,
@@ -147,6 +174,7 @@ module.exports = {
const projects = {}; // Unique project index to prevent building the same project multiple times
+ const projectWriters = {}; // Collection of memory adapters of already built libraries
const projectCountMarker = {};
function projectCount(project, count = 0) {
@@ -187,10 +215,21 @@ module.exports = {
const projectType = typeRepository.getType(project.type);
const resourceCollections = resourceFactory.createCollectionsForTree(project, {
- useNamespaces: true
+ useNamespaces: true,
+ virtualReaders: projectWriters
+ });
+
+ const writer = new MemAdapter({
+ virBasePath: "/"
});
+ // Store project writer as virtual reader for parent projects
+ // so they can access the build results of this project
+ projectWriters[project.metadata.name] = writer;
+ // TODO: Add getter for writer of DuplexColection
const workspace = resourceFactory.createWorkspace({
+ virBasePath: "/",
+ writer,
reader: resourceCollections.source,
name: project.metadata.name
});
@@ -198,6 +237,7 @@ module.exports = {
if (dev && devExcludeProject.indexOf(project.metadata.name) !== -1) {
projectTasks = composeTaskList({dev: false, selfContained, includedTasks, excludedTasks});
}
+
return projectType.build({
resourceCollections: {
workspace,
diff --git a/lib/processors/jsdoc/apiIndexGenerator.js b/lib/processors/jsdoc/apiIndexGenerator.js
new file mode 100644
index 000000000..c3026557d
--- /dev/null
+++ b/lib/processors/jsdoc/apiIndexGenerator.js
@@ -0,0 +1,51 @@
+const resourceFactory = require("@ui5/fs").resourceFactory;
+const createIndex = require("./lib/create-api-index");
+
+/**
+ * Compiles API index resources from all api.json resources available in the given test resources directory
+ * as created by the [sdkTransformer]{@link module:@ui5/builder.processors.sdkTransformer} processor.
+ * The resulting index resources (e.g. api-index.json, api-index-deprecated.json,
+ * api-index-experimental.json and api-index-since.json) are mainly to be used in the SDK.
+ *
+ * @public
+ * @alias module:@ui5/builder.processors.apiIndexGenerator
+ * @param {Object} parameters Parameters
+ * @param {string} parameters.versionInfoPath Path to sap-ui-version.json resource
+ * @param {string} parameters.testResourcesRootPath Path to /test-resources root directory in the
+ * given fs
+ * @param {string} parameters.targetApiIndexPath Path to create the generated API index JSON resource for
+ * @param {string} parameters.targetApiIndexDeprecatedPath Path to create the generated API index "deprecated" JSON
+ * resource for
+ * @param {string} parameters.targetApiIndexExperimentalPath Path to create the generated API index "experimental" JSON
+ * resource for
+ * @param {string} parameters.targetApiIndexSincePath Path to create the generated API index "since" JSON resource for
+ * @param {fs|module:@ui5/fs.fsInterface} parameters.fs Node fs or
+ * custom [fs interface]{@link module:resources/module:@ui5/fs.fsInterface} to use
+ * @returns {Promise} Promise resolving with created resources api-index.json,
+ * api-index-deprecated.json, api-index-experimental.json and
+ * api-index-since.json (names depend on the supplied paths)
+ */
+const apiIndexGenerator = async function({
+ versionInfoPath, testResourcesRootPath, targetApiIndexPath, targetApiIndexDeprecatedPath,
+ targetApiIndexExperimentalPath, targetApiIndexSincePath, fs
+} = {}) {
+ if (!versionInfoPath || !testResourcesRootPath || !targetApiIndexPath || !targetApiIndexDeprecatedPath ||
+ !targetApiIndexExperimentalPath || !targetApiIndexSincePath || !fs) {
+ throw new Error("[apiIndexGenerator]: One or more mandatory parameters not provided");
+ }
+
+ const resourceMap = await createIndex(versionInfoPath, testResourcesRootPath, targetApiIndexPath,
+ targetApiIndexDeprecatedPath, targetApiIndexExperimentalPath, targetApiIndexSincePath, {
+ fs,
+ returnOutputFiles: true
+ });
+
+ return Object.keys(resourceMap).map((resPath) => {
+ return resourceFactory.createResource({
+ path: resPath,
+ string: resourceMap[resPath]
+ });
+ });
+};
+
+module.exports = apiIndexGenerator;
diff --git a/lib/processors/jsdoc/jsdocGenerator.js b/lib/processors/jsdoc/jsdocGenerator.js
new file mode 100644
index 000000000..4c1d766c5
--- /dev/null
+++ b/lib/processors/jsdoc/jsdocGenerator.js
@@ -0,0 +1,164 @@
+const spawn = require("child_process").spawn;
+const fs = require("graceful-fs");
+const path = require("path");
+const {promisify} = require("util");
+const writeFile = promisify(fs.writeFile);
+const {resourceFactory} = require("@ui5/fs");
+
+/**
+ * JSDoc generator
+ *
+ * @public
+ * @alias module:@ui5/builder.processors.jsdocGenerator
+ * @param {Object} parameters Parameters
+ * @param {string} parameters.sourcePath Path of the source files to be processed
+ * @param {string} parameters.targetPath Path to write any output files
+ * @param {string} parameters.tmpPath Path to write temporary and debug files
+ * @param {Object} parameters.options Options
+ * @param {string} parameters.options.projectName Project name
+ * @param {string} parameters.options.namespace Namespace to build (e.g. some/project/name)
+ * @param {string} parameters.options.version Project version
+ * @param {Array} [parameters.options.variants=["apijson"]] JSDoc variants to be built
+ * @returns {Promise} Promise resolving with newly created resources
+ */
+const jsdocGenerator = async function({sourcePath, targetPath, tmpPath, options} = {}) {
+ if (!sourcePath || !targetPath || !tmpPath || !options.projectName || !options.namespace || !options.version) {
+ throw new Error("[jsdocGenerator]: One or more mandatory parameters not provided");
+ }
+
+ if (!options.variants || options.variants.length === 0) {
+ options.variants = ["apijson"];
+ }
+
+ const config = await jsdocGenerator._generateJsdocConfig({
+ targetPath,
+ tmpPath,
+ namespace: options.namespace,
+ projectName: options.projectName,
+ version: options.version,
+ variants: options.variants
+ });
+
+ const configPath = await jsdocGenerator._writeJsdocConfig(tmpPath, config);
+
+ await jsdocGenerator._buildJsdoc({
+ sourcePath,
+ configPath
+ });
+
+ const fsTarget = resourceFactory.createAdapter({
+ fsBasePath: targetPath,
+ virBasePath: "/"
+ });
+
+ // create resources from the output files
+ return Promise.all([
+ fsTarget.byPath(`/test-resources/${options.namespace}/designtime/api.json`)
+ // fsTarget.byPath(`/libraries/${options.projectName}.js`)
+ ]).then((res) => res.filter(($)=>$));
+};
+
+
+/**
+ * Generate jsdoc-config.json content
+ *
+ * @private
+ * @param {Object} parameters Parameters
+ * @param {string} parameters.targetPath Path to write any output files
+ * @param {string} parameters.tmpPath Path to write temporary and debug files
+ * @param {string} parameters.projectName Project name
+ * @param {string} parameters.version Project version
+ * @param {Array} parameters.variants JSDoc variants to be built
+ * @returns {string} jsdoc-config.json content string
+ */
+async function generateJsdocConfig({targetPath, tmpPath, namespace, projectName, version, variants}) {
+ // Backlash needs to be escaped as double-backslash
+ // This is not only relevant for win32 paths but also for
+ // Unix directory names that contain a backslash in their name
+ const backslashRegex = /\\/g;
+
+ // Resolve path to this script to get the path to the JSDoc extensions folder
+ const jsdocPath = path.normalize(__dirname);
+ const pluginPath = path.join(jsdocPath, "lib", "ui5", "plugin.js").replace(backslashRegex, "\\\\");
+ const templatePath = path.join(jsdocPath, "lib", "ui5", "template").replace(backslashRegex, "\\\\");
+ const destinationPath = path.normalize(tmpPath).replace(backslashRegex, "\\\\");
+ const jsapiFilePath = path.join(targetPath, "libraries", projectName + ".js").replace(backslashRegex, "\\\\");
+ const apiJsonFolderPath = path.join(tmpPath, "dependency-apis").replace(backslashRegex, "\\\\");
+ const apiJsonFilePath =
+ path.join(targetPath, "test-resources", path.normalize(namespace), "designtime", "api.json")
+ .replace(backslashRegex, "\\\\");
+
+ const config = `{
+ "plugins": ["${pluginPath}"],
+ "opts": {
+ "recurse": true,
+ "lenient": true,
+ "template": "${templatePath}",
+ "ui5": {
+ "saveSymbols": true
+ },
+ "destination": "${destinationPath}"
+ },
+ "templates": {
+ "ui5": {
+ "variants": ${JSON.stringify(variants)},
+ "version": "${version}",
+ "jsapiFile": "${jsapiFilePath}",
+ "apiJsonFolder": "${apiJsonFolderPath}",
+ "apiJsonFile": "${apiJsonFilePath}"
+ }
+ }
+ }`;
+ return config;
+}
+
+/**
+ * Write jsdoc-config.json to file system
+ *
+ * @private
+ * @param {string} targetDirPath Directory Path to write the jsdoc-config.json file to
+ * @param {string} config jsdoc-config.json content
+ * @returns {string} Full path to the written jsdoc-config.json file
+ */
+async function writeJsdocConfig(targetDirPath, config) {
+ const configPath = path.join(targetDirPath, "jsdoc-config.json");
+ await writeFile(configPath, config);
+ return configPath;
+}
+
+
+/**
+ * Execute JSDoc build by spawning JSDoc as an external process
+ *
+ * @private
+ * @param {Object} parameters Parameters
+ * @param {string} parameters.sourcePath Project resources (input for JSDoc generation)
+ * @param {string} parameters.configPath Full path to jsdoc-config.json file
+ * @returns {Promise}
+ */
+async function buildJsdoc({sourcePath, configPath}) {
+ const args = [
+ require.resolve("jsdoc/jsdoc"),
+ "-c",
+ configPath,
+ "--verbose",
+ sourcePath
+ ];
+ return new Promise((resolve, reject) => {
+ const child = spawn("node", args, {
+ stdio: ["ignore", "ignore", process.stderr]
+ });
+ child.on("close", function(code) {
+ if (code === 0 || code === 1) {
+ resolve();
+ } else {
+ reject(new Error(`JSDoc child process closed with code ${code}`));
+ }
+ });
+ });
+}
+
+module.exports = jsdocGenerator;
+module.exports._generateJsdocConfig = generateJsdocConfig;
+module.exports._writeJsdocConfig = writeJsdocConfig;
+module.exports._buildJsdoc = buildJsdoc;
diff --git a/lib/processors/jsdoc/lib/create-api-index.js b/lib/processors/jsdoc/lib/create-api-index.js
new file mode 100644
index 000000000..a2186a8e5
--- /dev/null
+++ b/lib/processors/jsdoc/lib/create-api-index.js
@@ -0,0 +1,452 @@
+/*
+ * Node script to create cross-library API index files for use in the UI5 SDKs.
+ *
+ * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
+ */
+
+"use strict";
+const path = require("path");
+const log = (function() {
+ try {
+ return require("@ui5/logger").getLogger("builder:processors:jsdoc:create-api-index");
+ } catch (error) {
+ /* eslint-disable no-console */
+ return {
+ info: function info(...msg) {
+ console.log("[INFO]", ...msg);
+ },
+ error: function error(...msg) {
+ console.error(...msg);
+ }
+ };
+ /* eslint-enable no-console */
+ }
+}());
+
+function process(versionInfoFile, unpackedTestresourcesRoot, targetFile, targetFileDeprecated, targetFileExperimental, targetFileSince, options) {
+ const fs = options && options.fs || require("fs");
+ const returnOutputFiles = options && !!options.returnOutputFiles;
+
+ log.info("creating API index files");
+ log.info(" sap-ui-version.json: " + versionInfoFile);
+ log.info(" unpacked test-resources: " + unpackedTestresourcesRoot);
+ log.info(" target file: " + targetFile);
+ log.info(" target file deprecated: " + targetFileDeprecated);
+ log.info(" target file experimental: " + targetFileExperimental);
+ log.info(" target file since: " + targetFileSince);
+ if (options && options.fs) {
+ log.info("Using custom fs");
+ }
+ if (returnOutputFiles) {
+ log.info("Returning output files instead of writing to fs.")
+ }
+ log.info("");
+
+ // Deprecated, Experimental and Since collections
+ let oListCollection = {
+ deprecated: {
+ noVersion: {
+ apis: []
+ }
+ },
+ experimental: {
+ noVersion: {
+ apis: []
+ }
+ },
+ since: {}
+ };
+
+ function readJSONFile(file) {
+ return new Promise(function (resolve, reject) {
+ fs.readFile(file, 'utf8', function (err, data) {
+ if (err) {
+ reject(err);
+ } else {
+ // Handle empty files scenario
+ if (data.trim() === "") {
+ resolve({});
+ } else {
+ resolve(JSON.parse(String(data)));
+ }
+ }
+ });
+ });
+ }
+
+ function mkdirSync(dir) {
+ if (dir && !fs.existsSync(dir)) {
+ mkdirSync( path.dirname(dir) );
+ fs.mkdirSync(dir);
+ }
+ }
+
+ function writeJSON(file, content) {
+ return new Promise(function(resolve,reject) {
+ // Create dir if it does not exist
+ mkdirSync( path.dirname(file) );
+ fs.writeFile(file, JSON.stringify(content), "utf-8", function(err) {
+ if ( err ) {
+ reject(err);
+ return;
+ }
+ resolve(true);
+ });
+ });
+ }
+
+ /*
+ * Extracts main symbol information from a library api.json.
+ * Also collects deprecated, experimental and since api's.
+ * Returns a promise that resolves with an array of symbols.
+ */
+ function createSymbolSummaryForLib(lib) {
+ let file = path.join(unpackedTestresourcesRoot, lib.replace(/\./g, "/"), "designtime/api.json");
+
+ return readJSONFile(file).then(function (apijson) {
+ if (!apijson.hasOwnProperty("symbols") || !Array.isArray(apijson.symbols)) {
+ // Ignore libraries with invalid api.json content like empty object or non-array "symbols" property.
+ return [];
+ }
+ return apijson.symbols.map(symbol => {
+ let oReturn = {
+ name: symbol.name,
+ kind: symbol.kind,
+ visibility: symbol.visibility,
+ extends: symbol.extends,
+ implements: symbol.implements,
+ lib: lib
+ };
+ // We add deprecated member only when the control is deprecated to keep file size at check
+ if (symbol.deprecated) {
+ oReturn.deprecated = true;
+ }
+ collectLists(symbol);
+ return oReturn;
+ });
+ })
+ }
+
+ /*
+ * Collects Deprecated, Experimental and Since data from passed symbol
+ * including symbol itself, methods and events.
+ */
+ function collectLists(oSymbol) {
+
+ function addData(oDataType, oEntityObject, sObjectType, sSymbolName) {
+ let sSince = oDataType !== "since" ? oEntityObject[oDataType].since : oEntityObject.since,
+ oData = {
+ control: sSymbolName,
+ text: oEntityObject[oDataType].text || oEntityObject.description,
+ type: sObjectType,
+ "static": !!oEntityObject.static,
+ visibility: oEntityObject.visibility
+ };
+
+ // For class we skip entityName
+ if (sObjectType !== "class") {
+ oData.entityName = oEntityObject.name;
+ }
+
+ if (sSince && sSince !== "undefined" /* Sometimes sSince comes as string "undefined" */) {
+ // take only major and minor versions
+ let sVersion = sSince.split(".").slice(0, 2).join(".");
+
+ oData.since = sSince;
+
+ if (!oListCollection[oDataType][sVersion]) {
+ oListCollection[oDataType][sVersion] = {
+ name: sVersion,
+ apis: []
+ };
+ }
+
+ oListCollection[oDataType][sVersion].apis.push(oData);
+ } else if (oDataType !== "since" /* noVersion does not make sense for since and will fail */) {
+ oListCollection[oDataType].noVersion.apis.push(oData);
+ }
+ }
+
+ // Classes
+ if (oSymbol.deprecated) {
+ addData("deprecated", oSymbol, "class", oSymbol.name);
+ }
+
+ if (oSymbol.experimental) {
+ addData("experimental", oSymbol, "class", oSymbol.name);
+ }
+
+ if (oSymbol.since && oSymbol.since !== "undefined" /* Sometimes sSince comes as string "undefined" */) {
+ addData("since", oSymbol, "class", oSymbol.name);
+ }
+
+ // Methods
+ oSymbol.methods && oSymbol.methods.forEach(oMethod => {
+ if (oMethod.deprecated) {
+ addData("deprecated", oMethod, "methods", oSymbol.name);
+ }
+
+ if (oMethod.experimental) {
+ addData("experimental", oMethod, "methods", oSymbol.name);
+ }
+
+ if (oMethod.since) {
+ addData("since", oMethod, "methods", oSymbol.name);
+ }
+ });
+
+ // Events
+ oSymbol.events && oSymbol.events.forEach(oEvent => {
+ if (oEvent.deprecated) {
+ addData("deprecated", oEvent, "events", oSymbol.name);
+ }
+
+ if (oEvent.experimental) {
+ addData("experimental", oEvent, "events", oSymbol.name);
+ }
+
+ if (oEvent.since) {
+ addData("since", oEvent, "events", oSymbol.name);
+ }
+ });
+
+ }
+
+ function deepMerge(arrayOfArrays) {
+ return arrayOfArrays.reduce((array, items) => {
+ array.push.apply(array, items);
+ return array;
+ }, []);
+ }
+
+ function expandHierarchyInfo(symbols) {
+ let byName = new Map();
+ symbols.forEach(symbol => {
+ byName.set(symbol.name, symbol);
+ });
+ symbols.forEach(symbol => {
+ let parent = symbol.extends && byName.get(symbol.extends);
+ if (parent) {
+ parent.extendedBy = parent.extendedBy || [];
+ parent.extendedBy.push(symbol.name);
+ }
+ if (symbol.implements) {
+ symbol.implements.forEach(intfName => {
+ let intf = byName.get(intfName);
+ if (intf) {
+ intf.implementedBy = intf.implementedBy || [];
+ intf.implementedBy.push(symbol.name);
+ }
+ });
+ }
+ });
+ return symbols;
+ }
+
+ function convertListToTree(symbols) {
+ let aTree = [];
+
+ // Filter out black list libraries
+ symbols = symbols.filter(({lib}) => ["sap.ui.demokit", "sap.ui.documentation"].indexOf(lib) === -1);
+
+ // Create treeName and displayName
+ symbols.forEach(oSymbol => {
+ oSymbol.treeName = oSymbol.name.replace(/^module:/, "").replace(/\//g, ".");
+ oSymbol.displayName = oSymbol.treeName.split(".").pop();
+ oSymbol.bIsDeprecated = !!oSymbol.deprecated;
+ });
+
+ // Create missing - virtual namespaces
+ symbols.forEach(oSymbol => {
+ oSymbol.treeName.split(".").forEach((sPart, i, a) => {
+ let sName = a.slice(0, (i + 1)).join(".");
+
+ if (!symbols.find(o => o.treeName === sName)) {
+ symbols.push({
+ name: sName,
+ treeName: sName,
+ displayName: sPart,
+ lib: oSymbol.lib,
+ kind: "namespace",
+ bIsDeprecated: false // Virtual namespace can't be deprecated
+ });
+ }
+ });
+ });
+
+ // Discover parents
+ symbols.forEach(oSymbol => {
+ let aParent = oSymbol.treeName.split("."),
+ sParent;
+
+ // Extract parent name
+ aParent.pop();
+ sParent = aParent.join(".");
+
+ // Mark parent
+ if (symbols.find(({treeName}) => treeName === sParent)) {
+ oSymbol.parent = sParent;
+ }
+ });
+
+ // Sort the list before building the tree
+ symbols.sort((a, b) => {
+ let sA = a.treeName.toUpperCase(),
+ sB = b.treeName.toUpperCase();
+
+ if (sA < sB) return -1;
+ if (sA > sB) return 1;
+ return 0;
+ });
+
+ // Build tree
+ symbols.forEach(oSymbol => {
+ if (oSymbol.parent) {
+ let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent);
+
+ if (!oParent.nodes) oParent.nodes = [];
+ oParent.nodes.push(oSymbol);
+ } else {
+ aTree.push(oSymbol);
+ }
+ });
+
+ // Custom sort first level tree items - "sap" namespace should be on top
+ aTree.sort((a, b) => {
+ let sA = a.displayName.toUpperCase(),
+ sB = b.displayName.toUpperCase();
+
+ if (sA === "SAP") return -1;
+ if (sB === "SAP") return 1;
+ if (sA < sB) return -1;
+ if (sA > sB) return 1;
+
+ return 0;
+ });
+
+ // Clean tree - keep file size down
+ function cleanTree (oSymbol) {
+ delete oSymbol.treeName;
+ delete oSymbol.parent;
+ if (oSymbol.children) {
+ oSymbol.children.forEach(o => cleanTree(o));
+ }
+ }
+ aTree.forEach(o => cleanTree(o));
+
+ return aTree;
+ }
+
+ function createOverallIndex() {
+ let version = "0.0.0";
+ const filesToReturn = {};
+
+ var p = readJSONFile(versionInfoFile)
+ .then(versionInfo => {
+ version = versionInfo.version;
+ return Promise.all(
+ versionInfo.libraries.map(
+ lib => createSymbolSummaryForLib(lib.name).catch(err => {
+ // ignore 'file not found' errors as some libs don't have an api.json (themes, server libs)
+ if (err.code === 'ENOENT') {
+ return [];
+ }
+ throw err;
+ })
+ )
+ );
+ })
+ .then(deepMerge)
+ .then(expandHierarchyInfo)
+ .then(convertListToTree)
+ .then(symbols => {
+ let result = {
+ "$schema-ref": "http://schemas.sap.com/sapui5/designtime/api-index.json/1.0",
+ version: version,
+ library: "*",
+ symbols: symbols
+ };
+ if (returnOutputFiles) {
+ filesToReturn[targetFile] = JSON.stringify(result);
+ } else {
+ return writeJSON(targetFile, result);
+ }
+ })
+ .then(() => {
+ /* Lists - modify and cleanup */
+ let sortList = function (oList) {
+ /* Sorting since records */
+ let aKeys = Object.keys(oList),
+ oSorted = {};
+
+ aKeys.sort((a, b) => {
+ let aA = a.split("."),
+ aB = b.split(".");
+
+ if (a === "noVersion") {
+ return 1; /* No version always at end of list */
+ }
+
+ if (b === "noVersion") {
+ return -1; /* No version always at end of list */
+ }
+
+ // Normalize old versions 1.4 to 1.04 for proper sorting
+ a = [aA[0], ('0' + aA[1]).slice(-2)].join("");
+ b = [aB[0], ('0' + aB[1]).slice(-2)].join("");
+
+ // Sort descending
+ return parseInt(b, 10) - parseInt(a, 10);
+ });
+
+ aKeys.forEach((sKey) => {
+ oSorted[sKey] = oList[sKey];
+ });
+
+ return oSorted;
+ };
+
+ /* Since */
+ oListCollection.since = sortList(oListCollection.since);
+
+ /* Deprecated */
+ oListCollection.deprecated = sortList(oListCollection.deprecated);
+ if (!oListCollection.deprecated.noVersion.apis.length) {
+ delete oListCollection.deprecated.noVersion;
+ }
+
+ /* Experimental */
+ oListCollection.experimental = sortList(oListCollection.experimental);
+ if (!oListCollection.experimental.noVersion.apis.length) {
+ delete oListCollection.experimental.noVersion;
+ }
+ })
+ .then(() => {
+ if (returnOutputFiles) {
+ filesToReturn[targetFileDeprecated] = JSON.stringify(oListCollection.deprecated);
+ filesToReturn[targetFileExperimental] = JSON.stringify(oListCollection.experimental);
+ filesToReturn[targetFileSince] = JSON.stringify(oListCollection.since);
+ return filesToReturn;
+ } else {
+ return Promise.all([
+ // write deprecated, experimental and since collections in the respective index files
+ writeJSON(targetFileDeprecated, oListCollection.deprecated),
+ writeJSON(targetFileExperimental, oListCollection.experimental),
+ writeJSON(targetFileSince, oListCollection.since)
+ ]);
+ }
+ })
+ .catch(err => {
+ log.error("**** failed to create API index for libraries:", err)
+ throw err;
+ });
+
+ return p;
+ }
+
+ return createOverallIndex();
+
+}
+
+module.exports = process;
diff --git a/lib/processors/jsdoc/lib/transform-apijson-for-sdk.js b/lib/processors/jsdoc/lib/transform-apijson-for-sdk.js
new file mode 100644
index 000000000..c8e27a71a
--- /dev/null
+++ b/lib/processors/jsdoc/lib/transform-apijson-for-sdk.js
@@ -0,0 +1,1994 @@
+/*
+ * Node script to preprocess api.json files for use in the UI5 SDKs.
+ *
+ * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
+ */
+
+"use strict";
+const cheerio = require("cheerio");
+const path = require('path');
+const log = (function() {
+ try {
+ return require("@ui5/logger").getLogger("builder:processors:jsdoc:transform-apijson-for-sdk");
+ } catch (error) {
+ /* eslint-disable no-console */
+ return {
+ info: function info(...msg) {
+ console.log("[INFO]", ...msg);
+ },
+ error: function error(...msg) {
+ console.error(...msg);
+ }
+ };
+ /* eslint-enable no-console */
+ }
+}());
+
+/*
+ * Transforms the api.json as created by the JSDoc build into a pre-processed api.json file suitable for the SDK.
+ *
+ * The pre-processing includes formatting of type references, rewriting of links and other time consuming calculations.
+ *
+ * @param {string} sInputFile Path of the original api.json file that should be transformed
+ * @param {string} sOutputFile Path that the transformed api.json file should should be written to
+ * @param {string} sLibraryFile Path to the .library file of the library, used to extract further documentation information
+ * @param {string|string[]} vDependencyAPIFiles Path of folder that contains api.json files of predecessor libs or
+ * an array of paths of those files
+ * @returns {Promise} A Promise that resolves after the transformation has been completed
+ */
+function transformer(sInputFile, sOutputFile, sLibraryFile, vDependencyAPIFiles, options) {
+ const fs = options && options.fs || require("fs");
+ const returnOutputFiles = options && !!options.returnOutputFiles;
+
+ log.info("Transform API index files for sap.ui.documentation");
+ log.info(" original file: " + sInputFile);
+ log.info(" output file: " + sOutputFile);
+ log.info(" library file: " + sLibraryFile);
+ log.info(" dependency dir: " + vDependencyAPIFiles);
+ if (options && options.fs) {
+ log.info("Using custom fs.");
+ }
+ if (returnOutputFiles) {
+ log.info("Returning output files instead of writing to fs.")
+ }
+ log.info("");
+
+ /**
+ * Transforms api.json file
+ * @param {object} oChainObject chain object
+ */
+ let transformApiJson = function (oChainObject) {
+ function isBuiltInType(type) {
+ return formatters._baseTypes.indexOf(type) >= 0;
+ }
+
+ /**
+ * Heuristically determining if there is a possibility the given input string
+ * to be a UI5 symbol
+ * @param {string} sName
+ * @returns {boolean}
+ */
+ function possibleUI5Symbol(sName) {
+ return /^[a-zA-Z][a-zA-Z.]*[a-zA-Z]$/.test(sName);
+ }
+
+ // Function is a copy from: LibraryInfo.js => LibraryInfo.prototype._getActualComponent => "match" inline method
+ function matchComponent(sModuleName, sPattern) {
+ sModuleName = sModuleName.toLowerCase();
+ sPattern = sPattern.toLowerCase();
+ return (
+ sModuleName === sPattern
+ || sPattern.match(/\*$/) && sModuleName.indexOf(sPattern.slice(0,-1)) === 0 // simple prefix match
+ || sPattern.match(/\.\*$/) && sModuleName === sPattern.slice(0,-2) // directory pattern also matches directory itself
+ );
+ }
+
+ /**
+ * Pre-processes the symbols list - creating virtual namespace records and attaching children list to namespace
+ * records.
+ * @param {object} symbols list
+ */
+ function preProcessSymbols(symbols) {
+ // Create treeName and modify module names
+ symbols.forEach(oSymbol => {
+ let sModuleClearName = oSymbol.name.replace(/^module:/, "");
+ oSymbol.displayName = sModuleClearName;
+ oSymbol.treeName = sModuleClearName.replace(/\//g, ".");
+ });
+
+ // Create missing - virtual namespaces
+ symbols.forEach(oSymbol => {
+ oSymbol.treeName.split(".").forEach((sPart, i, a) => {
+ let sName = a.slice(0, (i + 1)).join(".");
+
+ if (!symbols.find(o => o.treeName === sName)) {
+ symbols.push({
+ name: sName,
+ displayName: sName,
+ treeName: sName,
+ lib: oSymbol.lib,
+ kind: "namespace"
+ });
+ }
+ });
+ });
+
+ // Discover parents
+ symbols.forEach(oSymbol => {
+ let aParent = oSymbol.treeName.split("."),
+ sParent;
+
+ // Extract parent name
+ aParent.pop();
+ sParent = aParent.join(".");
+
+ // Mark parent
+ if (symbols.find(({treeName}) => treeName === sParent)) {
+ oSymbol.parent = sParent;
+ }
+ });
+
+ // Attach children info
+ symbols.forEach(oSymbol => {
+ if (oSymbol.parent) {
+ let oParent = symbols.find(({treeName}) => treeName === oSymbol.parent);
+
+ if (!oParent.nodes) oParent.nodes = [];
+ oParent.nodes.push({
+ name: oSymbol.displayName,
+ description: formatters._preProcessLinksInTextBlock(oSymbol.description),
+ href: "#/api/" + encodeURIComponent(oSymbol.name)
+ });
+ }
+ });
+
+ // Clean list - keep file size down
+ symbols.forEach(o => {
+ delete o.treeName;
+ delete o.parent;
+ });
+ }
+
+ // Transform to object
+ let oData = oChainObject.fileData;
+
+ // Attach default component for the library if available
+ if (oChainObject.defaultComponent) {
+ oData.defaultComponent = oChainObject.defaultComponent;
+ }
+
+ formatters._oOwnLibrary = oData;
+
+ // Pre process symbols
+ preProcessSymbols(oData.symbols);
+
+ // Apply formatter's and modify data as needed
+ oData.symbols.forEach((oSymbol) => {
+
+ // when the module name starts with the library name, then we apply the default component
+ if (oSymbol.name.indexOf(oData.library) === 0) {
+ oSymbol.component = oChainObject.defaultComponent;
+ }
+
+ // Attach symbol specific component if available (special cases)
+ // Note: Last hit wins as there may be a more specific component pattern
+ if (oChainObject.customSymbolComponents) {
+ Object.keys(oChainObject.customSymbolComponents).forEach(sComponent => {
+ if (matchComponent(oSymbol.name, sComponent)) {
+ oSymbol.component = oChainObject.customSymbolComponents[sComponent];
+ }
+ });
+ }
+
+ // Attach symbol sample flag if available
+ if (oChainObject.entitiesWithSamples) {
+ oSymbol.hasSample = oChainObject.entitiesWithSamples.indexOf(oSymbol.name) >= 0;
+ }
+
+ // Apply settings to formatter object - needed until formatter's are rewritten
+ formatters._sTopicId = oSymbol.name;
+ formatters._oTopicData = oSymbol;
+
+ // Format Page Title
+ oSymbol.title = (oSymbol.abstract ? "abstract " : "") + oSymbol.kind + " " + oSymbol.displayName;
+ oSymbol.subTitle = formatters.formatSubtitle(oSymbol.deprecated);
+
+ // Constructor
+ if (oSymbol.constructor) {
+ let oConstructor = oSymbol.constructor;
+
+ // Description
+ if (oConstructor.description) {
+ oConstructor.description = formatters.formatDescription(oConstructor.description);
+ }
+
+ // References
+ formatters.modifyReferences(oSymbol, true);
+
+ // Examples
+ if (oConstructor.examples) {
+ oConstructor.examples.forEach((oExample) => {
+ oExample.data = formatters.formatExample(oExample.caption, oExample.text);
+
+ // Keep file size in check
+ if (oExample.caption) {
+ delete oExample.caption;
+ }
+ if (oExample.text) {
+ delete oExample.text;
+ }
+ });
+ }
+
+ // Code Example string
+ oConstructor.codeExample = formatters.formatConstructor(oSymbol.name, oConstructor.parameters);
+
+ // Parameters
+ if (oConstructor.parameters) {
+ oConstructor.parameters = methods.buildConstructorParameters(oConstructor.parameters);
+
+ let aParameters = oConstructor.parameters;
+ aParameters.forEach(oParameter => {
+
+ // Types
+ oParameter.types = [];
+ if (oParameter.type) {
+ let aTypes = oParameter.type.split("|");
+
+ for (let i = 0; i < aTypes.length; i++) {
+ oParameter.types.push({
+ name: aTypes[i],
+ linkEnabled: !isBuiltInType(aTypes[i])
+ });
+ }
+
+ // Keep file size in check
+ delete oParameter.type;
+ }
+
+ // Default value
+ oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue);
+
+ // Description
+ if (oParameter.description) {
+ oParameter.description = formatters.formatDescription(oParameter.description);
+ }
+
+ })
+ }
+
+ // Throws
+ if (oConstructor.throws) {
+ oConstructor.throws.forEach(oThrows => {
+
+ // Description
+ if (oThrows.description) {
+ oThrows.description = formatters.formatDescription(oThrows.description);
+ }
+
+ // Exception link enabled
+ if (oThrows.type) {
+ oThrows.linkEnabled = formatters.formatExceptionLink(oThrows.type);
+ }
+
+ });
+ }
+ }
+
+ // Description
+ if (oSymbol.description) {
+ oSymbol.description = formatters.formatOverviewDescription(oSymbol.description, oSymbol.constructor.references);
+ }
+
+ // Deprecated
+ if (oSymbol.deprecated) {
+ oSymbol.deprecatedText = formatters.formatDeprecated(oSymbol.deprecated.since, oSymbol.deprecated.text);
+ // Keep file size in check
+ delete oSymbol.deprecated;
+ }
+
+ // Properties
+ if (oSymbol.properties) {
+ oSymbol.properties.forEach((oProperty) => {
+
+ // Name
+ oProperty.name = formatters.formatEntityName(oProperty.name, oSymbol.name, oProperty.static);
+
+ // Description
+ if (oProperty.deprecated) {
+ oProperty.description = formatters.formatDescription(oProperty.description,
+ oProperty.deprecated.text, oProperty.deprecated.since);
+ } else {
+ oProperty.description = formatters.formatDescription(oProperty.description);
+ }
+
+ // Link Enabled
+ if (oSymbol.kind !== "enum" && !isBuiltInType(oProperty.type) && possibleUI5Symbol(oProperty.type)) {
+ oProperty.linkEnabled = true;
+ oProperty.href = "#/api/" + oProperty.type.replace("[]", "");
+ }
+
+ // Keep file size in check
+ if (oProperty.static) {
+ delete oProperty.static;
+ }
+
+ if (oSymbol.kind === "enum" || oProperty.type === "undefined") {
+ delete oProperty.type;
+ }
+
+ });
+ }
+
+ // UI5 Metadata
+ if (oSymbol["ui5-metadata"]) {
+ let oMeta = oSymbol["ui5-metadata"];
+
+ // Properties
+ if (oMeta.properties) {
+ // Sort
+ oMeta.properties.sort(function (a, b) {
+ if (a.name < b.name) {
+ return -1;
+ } else if (a.name > b.name) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ // Pre-process
+ oMeta.properties.forEach((oProperty) => {
+ // Name
+ oProperty.name = formatters.formatEntityName(oProperty.name, oSymbol.name, oProperty.static);
+
+ // Description
+ oProperty.description = formatters.formatDescriptionSince(oProperty.description, oProperty.since);
+
+ // Link Enabled
+ if (!isBuiltInType(oProperty.type)) {
+ oProperty.linkEnabled = true;
+ }
+
+ // Default value
+ oProperty.defaultValue = formatters.formatDefaultValue(oProperty.defaultValue);
+
+ // Deprecated
+ if (oProperty.deprecated) {
+ oProperty.deprecatedText = formatters.formatDeprecated(oProperty.deprecated.since,
+ oProperty.deprecated.text);
+
+ // Keep file size in check
+ delete oProperty.deprecated;
+ }
+ });
+ }
+
+ // Aggregations
+ if (oMeta.aggregations) {
+ // Sort
+ oMeta.aggregations.sort(function (a, b) {
+ if (a.name < b.name) {
+ return -1;
+ } else if (a.name > b.name) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ // Pre-process
+ oMeta.aggregations.forEach((oAggregation) => {
+ // Link Enabled
+ if (!isBuiltInType(oAggregation.type)) {
+ oAggregation.linkEnabled = true;
+ }
+
+ // Description
+ if (oAggregation.deprecated) {
+ oAggregation.description = formatters.formatDescription(oAggregation.description,
+ oAggregation.deprecated.text, oAggregation.deprecated.since);
+ } else {
+ oAggregation.description = formatters.formatDescription(oAggregation.description);
+ }
+
+ // Link enabled
+ oAggregation.linkEnabled = !isBuiltInType(oAggregation.type);
+ });
+ }
+
+ // Associations
+
+ if (oMeta.associations) {
+ // Sort
+ oMeta.associations.sort(function (a, b) {
+ if (a.name < b.name) {
+ return -1;
+ } else if (a.name > b.name) {
+ return 1;
+ } else {
+ return 0;
+ }
+ });
+
+ // Pre-process
+ oMeta.associations.forEach((oAssociation) => {
+ // Link Enabled
+ if (!isBuiltInType(oAssociation.type)) {
+ oAssociation.linkEnabled = true;
+ }
+
+ // Description
+ if (oAssociation.deprecated) {
+ oAssociation.description = formatters.formatDescription(oAssociation.description,
+ oAssociation.deprecated.text, oAssociation.deprecated.since);
+ } else {
+ oAssociation.description = formatters.formatDescription(oAssociation.description);
+ }
+ });
+ }
+
+ // Events
+ if (oMeta.events) {
+ // We don't need event's data from the UI5-metadata for now. Keep file size in check
+ delete oMeta.events;
+ }
+
+ // Special Settings
+ if (oMeta.specialSettings) {
+ oMeta.specialSettings.forEach(oSetting => {
+
+ // Link Enabled
+ if (!isBuiltInType(oSetting.type)) {
+ oSetting.linkEnabled = true;
+ }
+
+ // Description
+ if (oSetting.deprecated) {
+ oSetting.description = formatters.formatDescription(oSetting.description,
+ oSetting.deprecated.text, oSetting.deprecated.since);
+ } else {
+ oSetting.description = formatters.formatDescription(oSetting.description);
+ }
+
+ });
+ }
+
+ // Annotations
+ if (oMeta.annotations) {
+ oMeta.annotations.forEach(oAnnotation => {
+
+ // Description
+ oAnnotation.description = formatters.formatAnnotationDescription(oAnnotation.description,
+ oAnnotation.since);
+
+ // Namespace
+ oAnnotation.namespaceText = oAnnotation.namespace;
+ oAnnotation.namespace = formatters.formatAnnotationNamespace(oAnnotation.namespace);
+
+ // Target
+ oAnnotation.target = formatters.formatAnnotationTarget(oAnnotation.target);
+
+ // Applies to
+ oAnnotation.appliesTo = formatters.formatAnnotationTarget(oAnnotation.appliesTo);
+
+ });
+ }
+
+ }
+
+ if (oSymbol.events) {
+
+ // Pre-process events
+ methods.buildEventsModel(oSymbol.events);
+
+ oSymbol.events.forEach(oEvent => {
+
+ // Description
+ if (oEvent.description) {
+ oEvent.description = formatters.formatDescriptionSince(oEvent.description, oEvent.since);
+ }
+
+ // Deprecated
+ if (oEvent.deprecated) {
+ oEvent.deprecatedText = formatters.formatEventDeprecated(oEvent.deprecated.since,
+ oEvent.deprecated.text);
+ }
+
+ // Parameters
+ if (oEvent.parameters && Array.isArray(oEvent.parameters)) {
+ oEvent.parameters.forEach(oParameter => {
+
+ // Link Enabled
+ if (!isBuiltInType(oParameter.type)) {
+ oParameter.linkEnabled = true;
+ }
+
+ // Description
+ if (oParameter.deprecated) {
+ oParameter.description = formatters.formatDescription(oParameter.description,
+ oParameter.deprecated.text, oParameter.deprecated.since);
+ } else {
+ oParameter.description = formatters.formatDescription(oParameter.description);
+ }
+
+ });
+ }
+
+ });
+
+ }
+
+ // Methods
+ if (oSymbol.methods) {
+
+ // Pre-process methods
+ methods.buildMethodsModel(oSymbol.methods);
+
+ oSymbol.methods.forEach(oMethod => {
+
+ // Name and link
+ if (oMethod.name) {
+ oMethod.name = formatters.formatEntityName(oMethod.name, oSymbol.name, oMethod.static);
+
+ // Link
+ oMethod.href = "#/api/" + encodeURIComponent(oSymbol.name) +
+ "/methods/" + encodeURIComponent(oMethod.name);
+ }
+
+ formatters.formatReferencesInDescription(oMethod);
+
+ // Description
+ if (oMethod.description) {
+ oMethod.description = formatters.formatDescription(oMethod.description);
+ }
+
+ // Examples
+ oMethod.examples && oMethod.examples.forEach(oExample => {
+ oExample = formatters.formatExample(oExample.caption, oExample.text);
+ });
+
+ // Deprecated
+ if (oMethod.deprecated) {
+ oMethod.deprecatedText = formatters.formatEventDeprecated(oMethod.deprecated.since,
+ oMethod.deprecated.text);
+ }
+
+ // Code example
+ oMethod.code = formatters.formatMethodCode(oMethod.name, oMethod.parameters, oMethod.returnValue);
+
+ // Parameters
+ if (oMethod.parameters) {
+ oMethod.parameters.forEach(oParameter => {
+
+ // Types
+ if (oParameter.types) {
+ oParameter.types.forEach(oType => {
+
+ // Link Enabled
+ if (!isBuiltInType(oType.value) && possibleUI5Symbol(oType.value)) {
+ oType.linkEnabled = true;
+ oType.href = "#/api/" + oType.value.replace("[]", "");
+ }
+
+ });
+ }
+
+ // Default value
+ oParameter.defaultValue = formatters.formatDefaultValue(oParameter.defaultValue);
+
+ // Description
+ if (oParameter.deprecated) {
+ oParameter.description = formatters.formatDescription(oParameter.description,
+ oParameter.deprecated.text, oParameter.deprecated.since);
+ } else {
+ oParameter.description = formatters.formatDescription(oParameter.description);
+ }
+
+ });
+ }
+
+ // Return value
+ if (oMethod.returnValue) {
+
+ // Description
+ oMethod.returnValue.description = formatters.formatDescription(oMethod.returnValue.description);
+
+ // Types
+ if (oMethod.returnValue.types) {
+ oMethod.returnValue.types.forEach(oType => {
+
+ // Link Enabled
+ if (!isBuiltInType(oType.value)) {
+ oType.href = "#/api/" + encodeURIComponent(oType.value.replace("[]", ""));
+ oType.linkEnabled = true;
+ }
+
+ });
+ }
+
+ }
+
+ // Throws
+ if (oMethod.throws) {
+ oMethod.throws.forEach(oThrows => {
+
+ // Description
+ if (oThrows.description) {
+ oThrows.description = formatters.formatDescription(oThrows.description);
+ }
+
+ // Exception link enabled
+ if (oThrows.type) {
+ oThrows.linkEnabled = formatters.formatExceptionLink(oThrows.type);
+ }
+
+ });
+ }
+
+ // Examples
+ if (oMethod.examples) {
+ oMethod.examples.forEach((oExample) => {
+ oExample.data = formatters.formatExample(oExample.caption, oExample.text);
+
+ // Keep file size in check
+ if (oExample.caption) {
+ delete oExample.caption;
+ }
+ if (oExample.text) {
+ delete oExample.text;
+ }
+ });
+
+ }
+
+
+ });
+ }
+
+ // Formatting namespaces, functions and enums, which may contain examples
+ // or references with links to process them similar to methods and constructors of classes
+
+ if (oSymbol.kind !== "class") {
+
+ if (oSymbol.examples) {
+ oSymbol.examples.forEach((oExample) => {
+ oExample.data = formatters.formatExample(oExample.caption, oExample.text);
+ // Keep file size in check
+ if (oExample.caption) {
+ delete oExample.caption;
+ }
+ if (oExample.text) {
+ delete oExample.text;
+ }
+ });
+ }
+
+ if (oSymbol.references) {
+ formatters.modifyReferences(oSymbol);
+ formatters.formatReferencesInDescription(oSymbol);
+ }
+
+ // Description
+ if (oSymbol.description) {
+ oSymbol.description = formatters.formatDescription(oSymbol.description);
+ }
+ }
+ });
+
+ oChainObject.parsedData = oData;
+
+ return oChainObject;
+ };
+
+ function getDependencyLibraryFilesList(oChainObject) {
+ // if vDependencyAPIFiles is an array, it contains the file paths of api.json files
+ if ( Array.isArray(vDependencyAPIFiles) ) {
+ return oChainObject;
+ }
+
+ // otherwise, it names a directory that has to be scanned for the files
+ return new Promise(oResolve => {
+ fs.readdir(vDependencyAPIFiles, function (oError, aItems) {
+ if (!oError && aItems && aItems.length) {
+ let aFiles = [];
+ aItems.forEach(sItem => {
+ aFiles.push(path.join(vDependencyAPIFiles, sItem));
+ })
+ oChainObject.aDependentLibraryFiles = aFiles;
+ }
+ oResolve(oChainObject); // We don't fail if no dependency library files are available
+ });
+ });
+ }
+
+ function loadDependencyLibraryFiles (oChainObject) {
+ if (!oChainObject.aDependentLibraryFiles) {
+ return oChainObject;
+ }
+ let aPromises = [];
+ oChainObject.aDependentLibraryFiles.forEach(sFile => {
+ aPromises.push(new Promise(oResolve => {
+ fs.readFile(sFile, 'utf8', (oError, oData) => {
+ oResolve(oError ? false : oData);
+ });
+ }));
+ });
+ return Promise.all(aPromises).then(aValues => {
+ let oDependentAPIs = {};
+
+ aValues.forEach(sData => {
+ let oData;
+
+ try {
+ oData = JSON.parse(sData);
+ } catch (e) {
+ // Silence possible dependency library invalid json errors as they are not critical
+ // and we should not fail BCP: 1870560058
+ }
+
+ // OpenUI5 build specific - own library can be listed as dependency library also.
+ // In this case we don't add it to the dependency list to skip double iteration.
+ if (oData && oChainObject.fileData.library !== oData.library) {
+ oDependentAPIs[oData.library] = oData.symbols;
+ }
+ });
+
+ oChainObject.oDependentAPIs = oDependentAPIs;
+ return oChainObject;
+ })
+ }
+
+ /**
+ * Create api.json from parsed data
+ * @param oChainObject chain object
+ */
+ function createApiRefApiJson(oChainObject) {
+ if (returnOutputFiles) {
+ // If requested, return data instead of writing to FS (required by UI5 Tooling/UI5 Builder)
+ return JSON.stringify(oChainObject.parsedData);
+ }
+ let sOutputDir = path.dirname(oChainObject.outputFile);
+
+ // Create dir if it does not exist
+ if (!fs.existsSync(sOutputDir)) {
+ fs.mkdirSync(sOutputDir);
+ }
+
+ // Write result to file
+ fs.writeFileSync(oChainObject.outputFile, JSON.stringify(oChainObject.parsedData) /* Transform back to string */, 'utf8');
+ }
+
+ /**
+ * Load .library file
+ * @param oChainObject chain return object
+ * @returns {Promise} library file promise
+ */
+ function getLibraryPromise(oChainObject) {
+ return new Promise(function(oResolve) {
+ fs.readFile(oChainObject.libraryFile, 'utf8', (oError, oData) => {
+ oChainObject.libraryFileData = oData;
+ oResolve(oChainObject);
+ });
+ });
+ }
+
+ /**
+ * Extracts components list and docuindex.json relative path from .library file data
+ * @param {object} oChainObject chain object
+ * @returns {object} chain object
+ */
+ function extractComponentAndDocuindexUrl(oChainObject) {
+ oChainObject.modules = [];
+
+ if (oChainObject.libraryFileData) {
+ let $ = cheerio.load(oChainObject.libraryFileData, {
+ ignoreWhitespace: true,
+ xmlMode: true,
+ lowerCaseTags: false
+ });
+
+ // Extract documentation URL
+ oChainObject.docuPath = $("appData documentation").attr("indexUrl");
+
+ // Extract components
+ $("ownership > component").each((i, oComponent) => {
+
+ if (oComponent.children) {
+ if (oComponent.children.length === 1) {
+ oChainObject.defaultComponent = $(oComponent).text();
+ } else {
+ let sCurrentComponentName = $(oComponent).find("name").text();
+ let aCurrentModules = [];
+ $(oComponent).find("module").each((a, oC) => {
+ aCurrentModules.push($(oC).text().replace(/\//g, "."));
+ });
+
+ oChainObject.modules.push({
+ componentName: sCurrentComponentName,
+ modules: aCurrentModules
+ });
+ }
+ }
+
+ });
+
+ }
+
+ return oChainObject;
+ }
+
+ /**
+ * Adds to the passed object custom symbol component map generated from the extracted components list
+ * to be easily searchable later
+ * @param {object} oChainObject chain object
+ * @returns {object} chain object
+ */
+ function flattenComponents(oChainObject) {
+ if (oChainObject.modules && oChainObject.modules.length > 0) {
+ oChainObject.customSymbolComponents = {};
+ oChainObject.modules.forEach(oComponent => {
+ let sCurrentComponent = oComponent.componentName;
+ oComponent.modules.forEach(sModule => {
+ oChainObject.customSymbolComponents[sModule] = sCurrentComponent;
+ });
+ });
+ }
+
+ return oChainObject;
+ }
+
+ /**
+ * Adds to the passed object array with entities which have explored samples
+ * @param {object} oChainObject chain object
+ * @returns {object} chain object
+ */
+ function extractSamplesFromDocuIndex(oChainObject) {
+ // If we have not extracted docuPath we return early
+ if (!oChainObject.docuPath) {
+ return oChainObject;
+ }
+ return new Promise(function(oResolve) {
+ // Join .library path with relative docuindex.json path
+ let sPath = path.join(path.dirname(oChainObject.libraryFile), oChainObject.docuPath);
+ // Normalize path to resolve relative path
+ sPath = path.normalize(sPath);
+
+ fs.readFile(sPath, 'utf8', (oError, oFileData) => {
+ if (!oError) {
+ oFileData = JSON.parse(oFileData);
+ if (oFileData.explored && oFileData.explored.entities && oFileData.explored.entities.length > 0) {
+ oChainObject.entitiesWithSamples = [];
+ oFileData.explored.entities.forEach(oEntity => {
+ oChainObject.entitiesWithSamples.push(oEntity.id);
+ });
+ }
+ }
+ // We aways resolve as this data is not mandatory
+ oResolve(oChainObject);
+ });
+
+ });
+ }
+
+ /**
+ * Load api.json file
+ * @param {object} oChainObject chain object
+ * @returns {object} chain object
+ */
+ function getAPIJSONPromise(oChainObject) {
+ return new Promise(function(oResolve, oReject) {
+ fs.readFile(sInputFile, 'utf8', (oError, sFileData) => {
+ if (oError) {
+ oReject(oError);
+ } else {
+ oChainObject.fileData = JSON.parse(sFileData);
+ oResolve(oChainObject);
+ }
+ });
+ });
+ }
+
+ /*
+ * =====================================================================================================================
+ * IMPORTANT NOTE: Formatter code is a copy from APIDetail.controller.js with a very little modification and mocking and
+ * code can be significantly improved
+ * =====================================================================================================================
+ */
+ let formatters = {
+
+ _sTopicId: "",
+ _oTopicData: {},
+ _baseTypes: [
+ "sap.ui.core.any",
+ "sap.ui.core.object",
+ "sap.ui.core.function",
+ "sap.ui.core.number", // TODO discuss with Thomas, type does not exist
+ "sap.ui.core.float",
+ "sap.ui.core.int",
+ "sap.ui.core.boolean",
+ "sap.ui.core.string",
+ "sap.ui.core.void",
+ "null",
+ "any",
+ "any[]",
+ "Error",
+ "Error[]",
+ "array",
+ "element",
+ "Element",
+ "DomRef",
+ "object",
+ "Object",
+ "object[]",
+ "object|object[]",
+ "[object Object][]",
+ "Array.<[object Object]>",
+ "Object.",
+ "function",
+ "float",
+ "int",
+ "boolean",
+ "string",
+ "string[]",
+ "number",
+ "map",
+ "promise",
+ "Promise",
+ "document",
+ "Document",
+ "Touch",
+ "TouchList",
+ "undefined"
+ ],
+ ANNOTATIONS_LINK: 'http://docs.oasis-open.org/odata/odata/v4.0/odata-v4.0-part3-csdl.html',
+ ANNOTATIONS_NAMESPACE_LINK: 'http://docs.oasis-open.org/odata/odata/v4.0/errata02/os/complete/vocabularies/',
+
+ /**
+ * Adds "deprecated" information if such exists to the header area
+ * @param deprecated - object containing information about deprecation
+ * @returns {string} - the deprecated text to display
+ */
+ formatSubtitle: function (deprecated) {
+ var result = "";
+
+ if (deprecated) {
+ result += "Deprecated in version: " + deprecated.since;
+ }
+
+ return result;
+ },
+
+ /**
+ * Formats the target and applies to texts of annotations
+ * @param target - the array of texts to be formatted
+ * @returns string - the formatted text
+ */
+ formatAnnotationTarget: function (target) {
+ var result = "";
+
+ if (target) {
+ target.forEach(function (element) {
+ result += element + ' ';
+ });
+ }
+
+ result = this._preProcessLinksInTextBlock(result);
+ return result;
+ },
+
+ /**
+ * Formats the namespace of annotations
+ * @param namespace - the namespace to be formatted
+ * @returns string - the formatted text
+ */
+ formatAnnotationNamespace: function (namespace) {
+ var result,
+ aNamespaceParts = namespace.split(".");
+
+ if (aNamespaceParts[0] === "Org" && aNamespaceParts[1] === "OData") {
+ result = '' + namespace + '';
+ } else {
+ result = namespace;
+ }
+
+ result = this._preProcessLinksInTextBlock(result);
+ return result;
+ },
+
+ /**
+ * Formats the description of annotations
+ * @param description - the description of the annotation
+ * @param since - the since version information of the annotation
+ * @returns string - the formatted description
+ */
+ formatAnnotationDescription: function (description, since) {
+ var result = description || "";
+
+ result += ' For more information, see ' + this.handleExternalUrl(this.ANNOTATIONS_LINK, "OData v4 Annotations");
+
+ if (since) {
+ result += '
Since: ' + since + '.';
+ }
+
+ result = this._preProcessLinksInTextBlock(result);
+ return result;
+ },
+
+ formatExceptionLink: function (linkText) {
+ linkText = linkText || '';
+ return linkText.indexOf('sap.ui.') !== -1;
+ },
+
+ formatMethodCode: function (sName, aParams, aReturnValue) {
+ var result = '
' + sName + '(';
+
+ if (aParams && aParams.length > 0) {
+ /* We consider only root level parameters so we get rid of all that are not on the root level */
+ aParams = aParams.filter(oElem => {
+ return oElem.depth === undefined;
+ });
+ aParams.forEach(function (element, index, array) {
+ result += element.name;
+
+ if (element.optional) {
+ result += '?';
+ }
+
+ if (index < array.length - 1) {
+ result += ', ';
+ }
+ });
+ }
+
+ result += ') : ';
+
+ if (aReturnValue) {
+ result += aReturnValue.type;
+ } else {
+ result += 'void';
+ }
+
+ result += "
";
+
+ return result;
+ },
+
+ /**
+ * Formats method deprecation message and pre-process jsDoc link and code blocks
+ * @param {string} sSince since text
+ * @param {string} sDescription deprecation description text
+ * @returns {string} formatted deprecation message
+ */
+ formatMethodDeprecated: function (sSince, sDescription) {
+ return this.formatDeprecated(sSince, sDescription, "methods");
+ },
+
+ /**
+ * Formats event deprecation message and pre-process jsDoc link and code blocks
+ * @param {string} sSince since text
+ * @param {string} sDescription deprecation description text
+ * @returns {string} formatted deprecation message
+ */
+ formatEventDeprecated: function (sSince, sDescription) {
+ return this.formatDeprecated(sSince, sDescription, "events");
+ },
+
+ /**
+ * Formats the description of control properties
+ * @param description - the description of the property
+ * @param since - the since version information of the property
+ * @returns string - the formatted description
+ */
+ formatDescriptionSince: function (description, since) {
+ var result = description || "";
+
+ if (since) {
+ result += '
Since: ' + since + '.';
+ }
+
+ result = this._preProcessLinksInTextBlock(result);
+ return result;
+ },
+
+ /**
+ * Formats the default value of the property as a string.
+ * @param defaultValue - the default value of the property
+ * @returns string - The default value of the property formatted as a string.
+ */
+ formatDefaultValue: function (defaultValue) {
+ var sReturn;
+
+ switch (defaultValue) {
+ case null:
+ case undefined:
+ sReturn = '';
+ break;
+ case '':
+ sReturn = 'empty string';
+ break;
+ default:
+ sReturn = defaultValue;
+ }
+
+ return Array.isArray(sReturn) ? sReturn.join(', ') : sReturn;
+ },
+
+ /**
+ * Formats the constructor of the class
+ * @param name
+ * @param params
+ * @returns string - The code needed to create an object of that class
+ */
+ formatConstructor: function (name, params) {
+ var result = '
new ';
+
+ if (name) {
+ result += name + '(';
+ }
+
+ if (params) {
+ params.forEach(function (element, index, array) {
+ result += element.name;
+
+ if (element.optional) {
+ result += '?';
+ }
+
+ if (index < array.length - 1) {
+ result += ', ';
+ }
+ });
+ }
+
+ if (name) {
+ result += ')
' : options.afterParagraph;
+ var beforeFirstParagraph = options.beforeFirstParagraph === undefined ? beforeParagraph : options.beforeFirstParagraph;
+ var afterLastParagraph = options.afterLastParagraph === undefined ? afterParagraph : options.afterLastParagraph;
+ var linkFormatter = typeof options.linkFormatter === 'function' ? options.linkFormatter : defaultLinkFormatter;
+
+ /*
+ * regexp to recognize important places in the text
+ *
+ * Capturing groups of the RegExp:
+ * group 1: begin of a pre block
+ * group 2: end of a pre block
+ * group 3: begin of a header, implicitly ends a paragraph
+ * group 4: end of a header, implicitly starts a new paragraph
+ * group 5: target portion of an inline @link tag
+ * group 6: (optional) text portion of an inline link tag
+ * group 7: an empty line which implicitly starts a new paragraph
+ *
+ * [--
block -] [---- some header ----] [---- an inline [@link ...} tag ----] [---------- an empty line ---------] */
+ var r = /(
)|(<\/pre>)|()|(<\/h[\d+]>)|\{@link\s+([^}\s]+)(?:\s+([^\}]*))?\}|((?:\r\n|\r|\n)[ \t]*(?:\r\n|\r|\n))/gi;
+ var inpre = false;
+
+ src = src || '';
+ linkFormatter = linkFormatter || defaultLinkFormatter;
+
+ src = beforeFirstParagraph + src.replace(r, function(match, pre, endpre, header, endheader, linkTarget, linkText, emptyline) {
+ if ( pre ) {
+ inpre = true;
+ } else if ( endpre ) {
+ inpre = false;
+ } else if ( header ) {
+ if ( !inpre ) {
+ return afterParagraph + match;
+ }
+ } else if ( endheader ) {
+ if ( !inpre ) {
+ return match + beforeParagraph;
+ }
+ } else if ( emptyline ) {
+ if ( !inpre ) {
+ return afterParagraph + beforeParagraph;
+ }
+ } else if ( linkTarget ) {
+ if ( !inpre ) {
+ return linkFormatter(linkTarget, linkText);
+ }
+ }
+ return match;
+ }) + afterLastParagraph;
+
+ // remove empty paragraphs
+ if (beforeParagraph !== "" && afterParagraph !== "") {
+ src = src.replace(new RegExp(escapeRegExp(beforeParagraph) + "\\s*" + escapeRegExp(afterParagraph), "g"), "");
+ }
+
+ return src;
+ }
+
+ return {
+ formatTextBlock: format
+ };
+
+ },
+
+ handleExternalUrl: function (sTarget, sText) {
+ // Check if the external domain is SAP hosted
+ let bSAPHosted = /^https?:\/\/(?:www.)?[\w.]*(?:sap|hana\.ondemand|sapfioritrial)\.com/.test(sTarget);
+
+ return `${sText}
+`;
+ },
+
+ /**
+ * Discover possible target type by looking at symbols from own and depending libraries
+ * @param {string} target
+ * @param {object} self
+ * @param {object} ownLibrary
+ * @param {object} dependencyLibs
+ * @param {boolean} module
+ * @returns {string}
+ */
+ createLinkFromTargetType: function ({className, methodName, target, self, ownLibrary, dependencyLibs, text}) {
+ let sResult;
+
+ function searchInSymbol(oSymbol) {
+ function findProperty(oEntity, sName, sTarget) {
+ if (!oEntity || !oEntity.properties) {
+ return;
+ }
+ return oEntity.properties.find(({name}) => name === sName || name === sTarget);
+ }
+
+ function findMethod(oEntity, sName, sTarget) {
+ if (!oEntity || !oEntity.methods) {
+ return;
+ }
+ return oEntity.methods.find(({name}) => name === sName || name === sTarget);
+ }
+
+ function findEvent(oEntity, sName) {
+ if (!oEntity || !oEntity.events) {
+ return;
+ }
+ return oEntity.events.find(({name}) => name === sName);
+ }
+
+ if (oSymbol.name === target) {
+ sResult = this.createLink({
+ name: oSymbol.name,
+ text: text
+ });
+ return true;
+ }
+ if (oSymbol.name === className) {
+ let oProperty = findProperty(oSymbol, methodName, target);
+ if (oProperty) {
+ sResult = this.createLink({
+ name: oProperty.name,
+ text: text
+ });
+ return true;
+ }
+ let oMethod = findMethod(oSymbol, methodName, target);
+ if (oMethod) {
+ sResult = this.createLink({
+ name: oMethod.static ? target : oMethod.name,
+ type: "methods",
+ text: text,
+ className: className
+ });
+ return true;
+ }
+
+ let oEvent = findEvent(oSymbol, methodName);
+ if (oEvent) {
+ sResult = this.createLink({
+ name: oEvent.name,
+ type: "events",
+ text: text,
+ className: className
+ });
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Self link
+ if (self.name === target) {
+ return this.createLink({
+ name: target,
+ text: text
+ });
+ }
+
+ // Own methods search
+ if (self.name === className && self.methods) {
+ let oResult = self.methods.find(({name}) => name === methodName);
+ if (oResult) {
+ return this.createLink({
+ name: oResult.static ? [self.name, oResult.name].join(".") : oResult.name,
+ type: "methods",
+ className: className,
+ text: text,
+ local: true
+ });
+ }
+ }
+
+ // Local library symbols
+ ownLibrary.symbols.find(oSymbol => {
+ return searchInSymbol.call(this, oSymbol);
+ });
+
+ if (sResult) return sResult;
+
+ // Dependent library symbols
+ dependencyLibs && Object.keys(dependencyLibs).find(sLib => {
+ if (sLib === target) {
+ sResult = this.createLink({
+ name: sLib,
+ text: text
+ });
+ return true;
+ }
+ return dependencyLibs[sLib].find(oSymbol => {
+ return searchInSymbol.call(this, oSymbol);
+ });
+ });
+
+ return sResult;
+ },
+
+ /**
+ * Creates a html link
+ * @param {string} name
+ * @param {string} type
+ * @param {string} className
+ * @param {string} [text=name] by default if no text is provided the name will be used
+ * @param {boolean} [local=false]
+ * @param {string} [hrefAppend=""]
+ * @returns {string} link
+ */
+ createLink: function ({name, type, className, text=name, local=false, hrefAppend=""}) {
+ let sLink;
+
+ // handling module's
+ if (className !== undefined && (/^module:/.test(name) || /^module:/.test(className))) {
+ name = name.replace(/^module:/, "");
+ }
+
+ name = encodeURIComponent(name);
+ className = encodeURIComponent(className);
+
+ // Build the link
+ sLink = type ? `${className}/${type}/${name}` : name;
+
+ if (hrefAppend) {
+ sLink += hrefAppend;
+ }
+
+ if (local) {
+ return `${text}`;
+ }
+
+ return `${text}`;
+ },
+
+ /**
+ * Pre-process links in text block
+ * @param {string} sText text block
+ * @param {boolean} bSkipParagraphs skip paragraphs
+ * @returns {string} processed text block
+ * @private
+ */
+ _preProcessLinksInTextBlock: function (sText, bSkipParagraphs) {
+ let oSelf = this._oTopicData,
+ oOwnLibrary = this._oOwnLibrary,
+ oDependencyLibs = oChainObject.oDependentAPIs,
+ oOptions = {
+ linkFormatter: function (sTarget, sText) {
+ let aMatch,
+ aTarget;
+
+ // keep the full target in the fallback text
+ sText = sText || sTarget;
+
+ // If the link has a protocol, do not modify, but open in a new window
+ if (/:\/\//.test(sTarget)) {
+ return this.handleExternalUrl(sTarget, sText);
+ }
+
+ // topic:xxx Topic
+ aMatch = sTarget.match(/^topic:(\w{32})$/);
+ if (aMatch) {
+ return '' + sText + '';
+ }
+
+ // sap.x.Xxx.prototype.xxx - In case of prototype we have a link to method
+ aMatch = sTarget.match(/([a-zA-Z0-9.$_]+?)\.prototype\.([a-zA-Z0-9.$_]+)$/);
+ if (aMatch) {
+ return this.createLink({
+ name: aMatch[2],
+ type: "methods",
+ className: aMatch[1],
+ text: sText
+ });
+ }
+
+ // Heuristics: Extend is always a static method
+ // sap.x.Xxx.extend
+ // module:sap/x/Xxx.extend
+ aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)\.extend$/);
+ if (aMatch) {
+ let [, sModule, sClass] = aMatch;
+ return this.createLink({
+ name: sTarget.replace(/^module:/, ""),
+ type: "methods",
+ className: (sModule ? sModule : "") + sClass,
+ text: sText
+ });
+ }
+
+ // Constructor links are handled in special manner by the SDK
+ // sap.x.Xxx.constructor
+ // sap.x.Xxx#constructor
+ // module:sap/x/Xxx.constructor
+ // #constructor
+ aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)?[\.#]constructor$/i);
+ if (aMatch) {
+ let [, sModule, sClass] = aMatch,
+ sName;
+
+ if (sClass) {
+ sName = (sModule ? sModule : "") + sClass;
+ } else {
+ sName = oSelf.name
+ }
+
+ return this.createLink({
+ name: sName,
+ hrefAppend: "/constructor",
+ text: sText
+ });
+ }
+
+ // #.setText - local static method
+ // #setText - local instance method
+ // #.setText.from - local nested method
+ aMatch = sTarget.match(/^#(\.)?([a-zA-Z0-9.$_]+)$/);
+ if (aMatch) {
+ return this.createLink({
+ name: aMatch[1] ? `${oSelf.name}.${aMatch[2]}` : aMatch[2],
+ type: "methods",
+ className: oSelf.name,
+ local: true,
+ text: sText
+ });
+ }
+
+ // #event:press - local event
+ aMatch = sTarget.match(/^#event:([a-zA-Z0-9$_]+)$/);
+ if (aMatch) {
+ return this.createLink({
+ name: aMatch[1],
+ type: "events",
+ className: oSelf.name,
+ local: true,
+ text: sText
+ });
+ }
+ // Event links
+ // sap.m.Button#event:press
+ // sap.m.Button.event:press
+ // module:sap/m/Button.event:press
+ // module:sap/m/Button#event:press
+ aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+?)[.#]event:([a-zA-Z0-9$_]+)$/);
+ if (aMatch) {
+ let [, sModule, sClass, sEvent] = aMatch;
+ return this.createLink({
+ name: sEvent,
+ type: "events",
+ className: (sModule ? sModule : "") + sClass,
+ text: sText
+ });
+ }
+
+ // sap.m.Button#setText - instance method
+ // module:sap/m/Button#setText
+ aMatch = sTarget.match(/^(module:)?([a-zA-Z0-9.$_\/]+)#([a-zA-Z0-9.$_]+)$/);
+ if (aMatch) {
+ let [, sModule, sClass, sMethod] = aMatch;
+ return this.createLink({
+ name: sMethod,
+ type: "methods",
+ className: (sModule ? sModule : "") + sClass,
+ text: sText
+ });
+ }
+
+ // Unresolved type - try to discover target type
+ // sap.x.Xxx.xxx
+ // module:sap/x/Xxx.xxx
+ if (/^(?:module:)?([a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/.test(sTarget)) {
+ let [,sClass, sName] = sTarget.match(/^((?:module:)?[a-zA-Z0-9.$_\/]+?)\.([a-zA-Z0-9$_]+)$/),
+ sResult = this.createLinkFromTargetType({
+ className: sClass,
+ methodName: sName,
+ target: sTarget,
+ self: oSelf,
+ ownLibrary: oOwnLibrary,
+ dependencyLibs: oDependencyLibs,
+ text: sText
+ });
+ if (sResult) {
+ return sResult;
+ }
+ }
+
+ // Possible nested functions discovery - currently we do this only for regular symbols
+ aTarget = sTarget.split(".");
+ if (aTarget.length >= 3) {
+ let sResult = this.createLinkFromTargetType({
+ methodName: aTarget.splice(-2).join("."),
+ className: aTarget.join("."),
+ target: sTarget,
+ self: oSelf,
+ ownLibrary: oOwnLibrary,
+ dependencyLibs: oDependencyLibs,
+ text: sText
+ });
+ if (sResult) {
+ return sResult;
+ }
+ }
+
+ // Possible forward reference - we will treat them as symbol link
+ return this.createLink({
+ name: sTarget,
+ text: sText
+ });
+
+ }.bind(this)
+ };
+
+ if (bSkipParagraphs) {
+ oOptions.beforeParagraph = "";
+ oOptions.afterParagraph = "";
+ }
+
+ return this.JSDocUtil().formatTextBlock(sText, oOptions);
+ },
+
+ /**
+ * Formatter for Overview section
+ * @param {string} sDescription - Class about description
+ * @param {array} aReferences - References
+ * @returns {string} - formatted text block
+ */
+ formatOverviewDescription: function (sDescription, aReferences) {
+ var iLen,
+ i;
+
+ // format references
+ if (aReferences && aReferences.length > 0) {
+ sDescription += "
Documentation links:
";
+
+ iLen = aReferences.length;
+ for (i = 0; i < iLen; i++) {
+ // We treat references as links but as they may not be defined as such we enforce it if needed
+ if (/{@link.*}/.test(aReferences[i])) {
+ sDescription += "
" + aReferences[i] + "
";
+ } else {
+ sDescription += "
{@link " + aReferences[i] + "}
";
+
+ }
+ }
+
+ sDescription += "
";
+ }
+
+ // Calling formatDescription so it could handle further formatting
+ return this.formatDescription(sDescription);
+ },
+
+ /**
+ * Formats the description of the property
+ * @param description - the description of the property
+ * @param deprecatedText - the text explaining this property is deprecated
+ * @param deprecatedSince - the version when this property was deprecated
+ * @returns string - the formatted description
+ */
+ formatDescription: function (description, deprecatedText, deprecatedSince) {
+ if (!description && !deprecatedText && !deprecatedSince) {
+ return "";
+ }
+
+ var result = description || "";
+
+ if (deprecatedSince || deprecatedText) {
+ // Note: sapUiDocumentationDeprecated - transformed to sapUiDeprecated to keep json file size low
+ result += " ";
+
+ result += this.formatDeprecated(deprecatedSince, deprecatedText);
+
+ result += "";
+ }
+
+ result = this._preProcessLinksInTextBlock(result);
+ return result;
+ },
+
+ /**
+ * Formats the entity deprecation message and pre-process jsDoc link and code blocks
+ * @param {string} sSince since text
+ * @param {string} sDescription deprecation description text
+ * @param {string} sEntityType string representation of entity type
+ * @returns {string} formatted deprecation message
+ */
+ formatDeprecated: function (sSince, sDescription, sEntityType) {
+ var aResult;
+
+ // Build deprecation message
+ // Note: there may be no since or no description text available
+ aResult = ["Deprecated"];
+ if (sSince) {
+ aResult.push(" as of version " + sSince);
+ }
+ if (sDescription) {
+ // Evaluate code blocks - Handle ... pattern
+ sDescription = sDescription.replace(/(\S+)<\/code>/gi, function (sMatch, sCodeEntity) {
+ return ['', sCodeEntity, ''].join("");
+ }
+ );
+
+ // Evaluate links in the deprecation description
+ aResult.push(". " + this._preProcessLinksInTextBlock(sDescription, true));
+ }
+
+ return aResult.join("");
+ },
+
+ /**
+ * Pre-process and modify references
+ * @param {object} oSymbol control data object which will be modified
+ * @private
+ */
+ modifyReferences: function (oSymbol, bCalledOnConstructor) {
+ var bHeaderDocuLinkFound = false,
+ bUXGuidelinesLinkFound = false,
+ aReferences = [],
+ entity = bCalledOnConstructor? oSymbol.constructor.references : oSymbol.references;
+
+ const UX_GUIDELINES_BASE_URL = "https://experience.sap.com/fiori-design-web/";
+
+ if (entity && entity.length > 0) {
+ entity.forEach(function (sReference) {
+ var aParts;
+
+ // Docu link - For the header we take into account only the first link that matches one of the patterns
+ if (!bHeaderDocuLinkFound) {
+
+ // Handled patterns:
+ // * topic:59a0e11712e84a648bb990a1dba76bc7
+ // * {@link topic:59a0e11712e84a648bb990a1dba76bc7}
+ // * {@link topic:59a0e11712e84a648bb990a1dba76bc7 Link text}
+ aParts = sReference.match(/^{@link\s+topic:(\w{32})(\s.+)?}$|^topic:(\w{32})$/);
+
+ if (aParts) {
+ if (aParts[3]) {
+ // Link is of type topic:GUID
+ oSymbol.docuLink = aParts[3];
+ oSymbol.docuLinkText = oSymbol.basename;
+ } else if (aParts[1]) {
+ // Link of type {@link topic:GUID} or {@link topic:GUID Link text}
+ oSymbol.docuLink = aParts[1];
+ oSymbol.docuLinkText = aParts[2] ? aParts[2] : oSymbol.basename;
+ }
+ bHeaderDocuLinkFound = true;
+ return;
+ }
+ }
+
+ // Fiori link - Handled patterns:
+ // * fiori:flexible-column-layout
+ // * fiori:/flexible-column-layout/
+ // * fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/
+ // * {@link fiori:flexible-column-layout}
+ // * {@link fiori:/flexible-column-layout/}
+ // * {@link fiori:/flexible-column-layout/ Flexible Column Layout}
+ // * {@link fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/}
+ // * {@link fiori:https://experience.sap.com/fiori-design-web/flexible-column-layout/ Flexible Column Layout}
+ aParts = sReference.match(/^(?:{@link\s)?fiori:(?:https:\/\/experience\.sap\.com\/fiori-design-web\/)?\/?(\S+\b)\/?\s?(.*[^\s}])?}?$/);
+
+ if (aParts) {
+ let [, sTarget, sTargetName] = aParts;
+
+ if (bCalledOnConstructor && !bUXGuidelinesLinkFound) {
+ // Extract first found UX Guidelines link as primary
+ oSymbol.uxGuidelinesLink = UX_GUIDELINES_BASE_URL + sTarget;
+ oSymbol.uxGuidelinesLinkText = sTargetName ? sTargetName : oSymbol.basename;
+ bUXGuidelinesLinkFound = true;
+ return;
+ } else {
+ // BCP: 1870155880 - Every consecutive "fiori:" link should be handled as a normal link
+ sReference = "{@link " + UX_GUIDELINES_BASE_URL + sTarget + (sTargetName ? " " + sTargetName : "") + "}";
+ }
+ }
+
+ aReferences.push(sReference);
+ });
+ bCalledOnConstructor? oSymbol.constructor.references = aReferences : oSymbol.references = aReferences;
+ } else {
+ bCalledOnConstructor? oSymbol.constructor.references = [] : oSymbol.references = [];
+ }
+ },
+
+ /**
+ * Manage References, to apply as an unordered list in the description
+ * @param {object} oEntity control data object which will be modified
+ * @private
+ */
+ formatReferencesInDescription: function(oEntity) {
+ if (oEntity.references && Array.isArray(oEntity.references)) {
+ oEntity.references = oEntity.references.map(sReference => {
+ return `
${sReference}
`;
+ });
+ if (!oEntity.description) {
+ // If there is no method description - references should be the first line of it
+ oEntity.description = '';
+ } else {
+ oEntity.description += '
';
+ }
+ oEntity.description += `References:
${oEntity.references.join("")}
`;
+ }
+ }
+ };
+
+ /* Methods direct copy from API Detail */
+ let methods = {
+
+ /**
+ * Adjusts methods info so that it can be easily displayed in a table
+ * @param aMethods - the methods array initially coming from the server
+ */
+ buildMethodsModel: function (aMethods) {
+ var fnCreateTypesArr = function (sTypes) {
+ return sTypes.split("|").map(function (sType) {
+ return {value: sType}
+ });
+ };
+ var fnExtractParameterProperties = function (oParameter, aParameters, iDepth, aPhoneName) {
+ if (oParameter.parameterProperties) {
+ Object.keys(oParameter.parameterProperties).forEach(function (sProperty) {
+ var oProperty = oParameter.parameterProperties[sProperty];
+
+ oProperty.depth = iDepth;
+
+ // Handle types
+ if (oProperty.type) {
+ oProperty.types = fnCreateTypesArr(oProperty.type);
+ }
+
+ // Phone name - available only for parameters
+ oProperty.phoneName = [aPhoneName.join("."), oProperty.name].join(".");
+
+ // Add property to parameter array as we need a simple structure
+ aParameters.push(oProperty);
+
+ // Handle child parameterProperties
+ fnExtractParameterProperties(oProperty, aParameters, (iDepth + 1), aPhoneName.concat([oProperty.name]));
+
+ // Keep file size in check
+ delete oProperty.type;
+ });
+
+ // Keep file size in check
+ delete oParameter.parameterProperties;
+ }
+ };
+ aMethods.forEach(function (oMethod) {
+ // New array to hold modified parameters
+ var aParameters = [];
+
+ // Handle parameters
+ if (oMethod.parameters) {
+ oMethod.parameters.forEach(function (oParameter) {
+ if (oParameter.type) {
+ oParameter.types = fnCreateTypesArr(oParameter.type);
+ }
+
+ // Keep file size in check
+ delete oParameter.type;
+
+ // Add the parameter before the properties
+ aParameters.push(oParameter);
+
+ // Handle Parameter Properties
+ // Note: We flatten the structure
+ fnExtractParameterProperties(oParameter, aParameters, 1, [oParameter.name]);
+
+ });
+
+ // Override the old data
+ oMethod.parameters = aParameters;
+ }
+
+ // Handle return values
+ if (oMethod.returnValue && oMethod.returnValue.type) {
+ // Handle types
+ oMethod.returnValue.types = fnCreateTypesArr(oMethod.returnValue.type);
+ }
+
+ });
+ },
+
+ /**
+ * Adjusts events info so that it can be easily displayed in a table
+ * @param {Array} aEvents - the events array initially coming from the server
+ */
+ buildEventsModel: function (aEvents) {
+ var fnExtractParameterProperties = function (oParameter, aParameters, iDepth, aPhoneName) {
+ if (oParameter.parameterProperties) {
+ Object.keys(oParameter.parameterProperties).forEach(function (sProperty) {
+ var oProperty = oParameter.parameterProperties[sProperty],
+ sPhoneTypeSuffix;
+
+ oProperty.depth = iDepth;
+
+ // Phone name - available only for parameters
+ sPhoneTypeSuffix = oProperty.type === "array" ? "[]" : "";
+ oProperty.phoneName = [aPhoneName.join("."), (oProperty.name + sPhoneTypeSuffix)].join(".");
+
+ // Add property to parameter array as we need a simple structure
+ aParameters.push(oProperty);
+
+ // Handle child parameterProperties
+ fnExtractParameterProperties(oProperty, aParameters, (iDepth + 1),
+ aPhoneName.concat([oProperty.name + sPhoneTypeSuffix]));
+ });
+
+ // Keep file size in check
+ delete oParameter.parameterProperties;
+ }
+ };
+ aEvents.forEach(function (aEvents) {
+ // New array to hold modified parameters
+ var aParameters = [];
+
+ // Handle parameters
+ if (aEvents.parameters) {
+ aEvents.parameters.forEach(function (oParameter) {
+ // Add the parameter before the properties
+ aParameters.push(oParameter);
+
+ // Handle Parameter Properties
+ // Note: We flatten the structure
+ fnExtractParameterProperties(oParameter, aParameters, 1, [oParameter.name]);
+ });
+
+ // Override the old data
+ aEvents.parameters = aParameters;
+ }
+ });
+ },
+
+ /**
+ * Adjusts constructor parameters info so that it can be easily displayed in a table
+ * @param {Array} aParameters - the events array initially coming from the server
+ */
+ buildConstructorParameters: function (aParameters) {
+ // New array to hold modified parameters
+ var aNodes = [],
+ processNode = function (oNode, sPhoneName, iDepth, aNodes) {
+ // Handle phone name
+ oNode.phoneName = sPhoneName ? [sPhoneName, oNode.name].join(".") : oNode.name;
+
+ // Depth
+ oNode.depth = iDepth;
+
+ // Add to array
+ aNodes.push(oNode);
+
+ // Handle nesting
+ if (oNode.parameterProperties) {
+ Object.keys(oNode.parameterProperties).forEach(function (sNode) {
+ processNode(oNode.parameterProperties[sNode], oNode.phoneName, (iDepth + 1), aNodes);
+ });
+ }
+
+ delete oNode.parameterProperties;
+ };
+
+ aParameters.forEach(function (oParameter) {
+ // Handle Parameter Properties
+ // Note: We flatten the structure
+ processNode(oParameter, undefined, 0, aNodes);
+ });
+
+ return aNodes;
+ },
+
+ oLibsData: {},
+
+ };
+
+ // Create the chain object
+ let oChainObject = {
+ inputFile: sInputFile,
+ outputFile: sOutputFile,
+ libraryFile: sLibraryFile,
+ aDependentLibraryFiles: Array.isArray(vDependencyAPIFiles) ? vDependencyAPIFiles : null
+ };
+
+ // Start the work here
+ let p = getLibraryPromise(oChainObject)
+ .then(extractComponentAndDocuindexUrl)
+ .then(flattenComponents)
+ .then(extractSamplesFromDocuIndex)
+ .then(getDependencyLibraryFilesList)
+ .then(getAPIJSONPromise)
+ .then(loadDependencyLibraryFiles)
+ .then(transformApiJson)
+ .then(createApiRefApiJson);
+ return p;
+
+};
+
+module.exports = transformer;
+
+// auto execute the transformer, if this module is the executed script (not used in grunt tooling)
+if ( process.argv.length > 1 && /transform-apijson-for-sdk.js$/.test(process.argv[1]) ) {
+
+ let sInputFile = process.argv[2];
+ let sOutputFile = process.argv[3];
+ let sLibraryFile = process.argv[4];
+ let sAPIJSonDependencyDir = process.argv[5];
+
+ transformer(sInputFile, sOutputFile, sLibraryFile, sAPIJSonDependencyDir)
+ .catch(oError => {
+ log.error(oError);
+ });
+
+}
diff --git a/lib/processors/jsdoc/lib/ui5/plugin.js b/lib/processors/jsdoc/lib/ui5/plugin.js
new file mode 100644
index 000000000..0cba05cf7
--- /dev/null
+++ b/lib/processors/jsdoc/lib/ui5/plugin.js
@@ -0,0 +1,2395 @@
+/*
+ * JSDoc3 plugin for UI5 documentation generation.
+ *
+ * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
+ */
+
+/* global global, require, exports, env */
+/* eslint strict: [2, "global"]*/
+
+'use strict';
+
+/**
+ * UI5 plugin for JSDoc3 (3.3.0-alpha5)
+ *
+ * The plugin adds the following SAPUI5 specific tag definitions to JSDoc3
+ *
+ * disclaimer
+ *
+ * experimental
+ *
+ * final
+ *
+ * interface
+ *
+ * implements
+ *
+ *
+ *
+ * It furthermore listens to the following JSDoc3 events to implement additional functionality
+ *
+ * parseBegin
+ * to create short names for all file that are to be parsed
+ *
+ * fileBegin
+ * to write some line to the log (kind of a progress indicator)
+ *
+ * jsdocCommentFound
+ * to pre-process comments, empty lines are used as paragraph markers
+ * a default visibility is added, legacy tag combinations used in JSdoc2 are converted to JSDoc3 conventions
+ *
+ * newDoclet
+ *
+ * parseComplete
+ * remove undocumented/ignored/private doclets or duplicate doclets
+ *
+ *
+ * Last but not least, it implements an astNodeVisitor to detect UI5 specific "extend" calls and to create
+ * documentation for the properties, aggregations etc. that are created with the "extend" call.
+ *
+ * @module plugins/sapui5-jsdoc
+ */
+
+/* imports */
+var Syntax = require('jsdoc/src/syntax').Syntax;
+var Doclet = require('jsdoc/doclet').Doclet;
+var fs = require('jsdoc/fs');
+var path = require('jsdoc/path');
+var pluginConfig = (env.conf && env.conf.templates && env.conf.templates.ui5) || {};
+
+/* ---- global vars---- */
+
+/**
+ * Potential path prefixes.
+ *
+ * Will be determined in the handler for the parseBegin event
+ */
+var pathPrefixes = [];
+
+/**
+ * Prefixes of the UI5 unified resource name for the source files is NOT part of the file name.
+ * (e.g. when a common root namespaces has been omitted from the folder structure).
+ *
+ * The prefix will be prepended to all resource names.
+ */
+var resourceNamePrefixes = [];
+
+/**
+ * A UI5 specific unique Id for all doclets.
+ */
+var docletUid = 0;
+
+var currentProgram;
+
+/**
+ * Information about the current module.
+ *
+ * The info object is created in the 'fileBegin' event handler and the 'resource' and 'module' properties
+ * are derived from the filename provided by the event. The derived information is only correct, when the
+ * resource name prefix is known for the directory from which a source is loaded (prefixes can be configured
+ * via sapui5.resourceNamePrefixes, for UI5 libraries it is empty by default).
+ *
+ * During AST visiting, the 'name' property and the 'localeNames' map will be filled.
+ * 'name' will be the name of the class defined by the module (assuming that there is only one).
+ * 'localNames' will contain information objects for each parameter of an AMD Factory function and for
+ * all shortcut variables that are defined top-level in the module factory function (e.g. something like
+ * var ButtonDesign = coreLibrary.ButtonDesign; ).
+ * An info object for a local name either can have a 'value' property (simple, constant value) or it can
+ * have a 'module' and optionally a 'path' value. In that case, the local name represents an AMD
+ * module import or a shortcut derived from such an import.
+ *
+ * See {@link getREsolvedObjectName} how the knowledge about locale names is used.
+ *
+ * @type {{name:string,resource:string,module:string,localName:Object}}
+ */
+var currentModule;
+
+var currentSource;
+
+/**
+ * Cached UI5 metadata for encountered UI5 classes.
+ *
+ * The metadata is collected from the 'metadata' property of 'extend' calls. It is stored
+ * in this map keyed by the name of the class (as defined in the first parameter of the extend call).
+ * Only after all files have been parsed, the collected information can be associated with the
+ * corresponding JSDoc doclet (e.g. with the class documentation).
+ */
+var classInfos = Object.create(null);
+
+/**
+ *
+ */
+var typeInfos = Object.create(null);
+
+/**
+ * Cached designtime info for encountered sources.
+ *
+ * The designtime information is collected only for files named '*.designtime.js'.
+ * It is stored in this map keyed by the corresponding module name (e.g. 'sap/m/designtime/Button.designtime').
+ * Only after all files have been parsed, the collected information can be associated with runtime metadata
+ * that refers to that designtime module name.
+ */
+var designtimeInfos = Object.create(null);
+
+/* ---- private functions ---- */
+
+function ui5data(doclet) {
+ return doclet.__ui5 || (doclet.__ui5 = { id: ++docletUid });
+}
+
+var pendingMessageHeader;
+
+function msgHeader(str) {
+ pendingMessageHeader = str;
+}
+
+/* eslint-disable no-console */
+function debug() {
+ if ( env.opts.debug ) {
+ console.log.apply(console, arguments);
+ }
+}
+
+function info() {
+ if ( env.opts.verbose || env.opts.debug ) {
+ if ( pendingMessageHeader ) {
+ console.log("");
+ pendingMessageHeader = null;
+ }
+ console.log.apply(console, arguments);
+ }
+}
+
+function warning(msg) {
+ if ( pendingMessageHeader ) {
+ if ( !env.opts.verbose && !env.opts.debug ) {
+ console.log(pendingMessageHeader);
+ } else {
+ console.log("");
+ }
+ pendingMessageHeader = null;
+ }
+ var args = Array.prototype.slice.apply(arguments);
+ args[0] = "**** warning: " + args[0];
+ console.log.apply(console, args);
+}
+
+function error(msg) {
+ if ( pendingMessageHeader && !env.opts.verbose && !env.opts.debug ) {
+ if ( !env.opts.verbose && !env.opts.debug ) {
+ console.log(pendingMessageHeader);
+ } else {
+ console.log("");
+ }
+ pendingMessageHeader = null;
+ }
+ var args = Array.prototype.slice.apply(arguments);
+ args[0] = "**** error: " + args[0];
+ console.log.apply(console, args);
+}
+/* eslint-enable no-console */
+
+//---- path handling ---------------------------------------------------------
+
+function ensureEndingSlash(path) {
+ path = path || '';
+ return path && path.slice(-1) !== '/' ? path + '/' : path;
+}
+
+function getRelativePath(filename) {
+ var relative = path.resolve(filename);
+ for ( var i = 0; i < pathPrefixes.length; i++ ) {
+ if ( relative.indexOf(pathPrefixes[i]) === 0 ) {
+ relative = relative.slice(pathPrefixes[i].length);
+ break;
+ }
+ }
+ return relative.replace(/\\/g, '/');
+}
+
+function getResourceName(filename) {
+ var resource = path.resolve(filename);
+ for ( var i = 0; i < pathPrefixes.length; i++ ) {
+ if ( resource.indexOf(pathPrefixes[i]) === 0 ) {
+ resource = resourceNamePrefixes[i] + resource.slice(pathPrefixes[i].length);
+ break;
+ }
+ }
+ return resource.replace(/\\/g, '/');
+}
+
+function getModuleName(resource) {
+ return resource.replace(/\.js$/,'');
+}
+
+/*
+ * resolves relative AMD module identifiers relative to a given base name
+ */
+function resolveModuleName(base, name) {
+ var stack = base.split('/');
+ stack.pop();
+ name.split('/').forEach(function(segment, i) {
+ if ( segment == '..' ) {
+ stack.pop();
+ } else if ( segment === '.' ) {
+ // ignore
+ } else {
+ if ( i === 0 ) {
+ stack = [];
+ }
+ stack.push(segment);
+ }
+ });
+ return stack.join('/');
+}
+
+// ---- AMD handling
+
+function analyzeModuleDefinition(node) {
+ var args = node.arguments;
+ var arg = 0;
+ if ( arg < args.length
+ && args[arg].type === Syntax.Literal && typeof args[arg].value === 'string' ) {
+ warning("module explicitly defined a module name '" + args[arg].value + "'");
+ currentModule.name = args[arg].value;
+ arg++;
+ }
+ if ( arg < args.length && args[arg].type === Syntax.ArrayExpression ) {
+ currentModule.dependencies = convertValue(args[arg], "string[]");
+ arg++;
+ }
+ if ( arg < args.length && args[arg].type === Syntax.FunctionExpression ) {
+ currentModule.factory = args[arg];
+ arg++;
+ }
+ if ( currentModule.dependencies && currentModule.factory ) {
+ for ( var i = 0; i < currentModule.dependencies.length && i < currentModule.factory.params.length; i++ ) {
+ var name = currentModule.factory.params[i].name;
+ var module = resolveModuleName(currentModule.module, currentModule.dependencies[i]);
+ debug(" import " + name + " from '" + module + "'");
+ currentModule.localNames[name] = {
+ module: module
+ // no (or empty) path
+ };
+ }
+ }
+ if ( currentModule.factory ) {
+ collectShortcuts(currentModule.factory.body);
+ }
+}
+
+/**
+ * Searches the given body for variable declarations that can be evaluated statically,
+ * either because they refer to known AMD modukle imports (e.g. shortcut varialbes)
+ * or because they have a (design time) constant value.
+ *
+ * @param {ASTNode} body AST node of a function body that shall be searched for shortcuts
+ */
+function collectShortcuts(body) {
+
+ function checkAssignment(name, valueNode) {
+ if ( valueNode.type === Syntax.Literal ) {
+ currentModule.localNames[name] = {
+ value: valueNode.value
+ };
+ debug("compile time constant found ", name, valueNode.value);
+ } else if ( valueNode.type === Syntax.MemberExpression ) {
+ var _import = getLeftmostName(valueNode);
+ var local = _import && currentModule.localNames[_import];
+ if ( typeof local === 'object' && local.module ) {
+ currentModule.localNames[name] = {
+ module: local.module,
+ path: getObjectName(valueNode).split('.').slice(1).join('.') // TODO chaining if local has path
+ };
+ debug(" found local shortcut: ", name, currentModule.localNames[name]);
+ }
+ } else if ( isRequireSyncCall(valueNode) || isProbingRequireCall(valueNode) ) {
+ if ( valueNode.arguments[0]
+ && valueNode.arguments[0].type === Syntax.Literal
+ && typeof valueNode.arguments[0].value === 'string' ) {
+ currentModule.localNames[name] = {
+ module: valueNode.arguments[0].value
+ // no (or empty) path
+ };
+ debug(" found local import: %s = %s('%s')", name, valueNode.callee.property.name, valueNode.arguments[0].value);
+ }
+ } else if ( isExtendCall(valueNode) ) {
+ currentModule.localNames[name] = {
+ "class": valueNode.arguments[0].value
+ // no (or empty) path
+ };
+ debug(" found local class definition: %s = .extend('%s', ...)", name, valueNode.arguments[0].value);
+ }
+ }
+
+ if ( body.type === Syntax.BlockStatement ) {
+ body.body.forEach(function ( stmt ) {
+ // console.log(stmt);
+ if ( stmt.type === Syntax.VariableDeclaration ) {
+ stmt.declarations.forEach(function(decl) {
+ if ( decl.init ) {
+ checkAssignment(decl.id.name, decl.init);
+ }
+ });
+ } else if ( stmt.type === Syntax.ExpressionStatement
+ && stmt.expression.type === Syntax.AssignmentExpression
+ && stmt.expression.left.type === Syntax.Identifier ) {
+ checkAssignment(stmt.expression.left.name, stmt.expression.right);
+ }
+ });
+ }
+}
+
+// ---- text handling ---------------------------------------------------------
+
+var rPlural = /(children|ies|ves|oes|ses|ches|shes|xes|s)$/i;
+var mSingular = {'children' : -3, 'ies' : 'y', 'ves' : 'f', 'oes' : -2, 'ses' : -2, 'ches' : -2, 'shes' : -2, 'xes' : -2, 's' : -1 };
+
+function guessSingularName(sPluralName) {
+ return sPluralName.replace(rPlural, function($,sPlural) {
+ var vRepl = mSingular[sPlural.toLowerCase()];
+ return typeof vRepl === "string" ? vRepl : sPlural.slice(0,vRepl);
+ });
+}
+
+function getPropertyKey(prop) {
+ if ( prop.key.type === Syntax.Identifier ) {
+ return prop.key.name;
+ } else if ( prop.key.type === Syntax.Literal ) {
+ return String(prop.key.value);
+ } else {
+ return prop.key.toSource();
+ }
+}
+
+/**
+ * Creates a map of property values from an AST 'object literal' node.
+ *
+ * The values in the map are again AST 'property' nodes (representing key/value pairs).
+ * It would be more convenient to just return the values, but the property node is needed
+ * to find the corresponding (preceding) documentation comment.
+ *
+ * If a defaultKey is given and if the node is not an object literal
+ * but another simple type literal, the value is treated as a shortcut for
+ *
+ * {
+ * [defaultKey]: node.value
+ * }
+ *
+ * This is used in ManagedObjectMetadata to allow a simpler declaration of properties by
+ * specifying a type name only.
+ *
+ * @param {ASTNode} node AST node for an object literal or simple literal
+ * @param {string} [defaultKey=undefined] A default key to use for simple values
+ * @returns {Map} Map of AST nodes of type 'Property', keyed by their property name
+ */
+function createPropertyMap(node, defaultKey) {
+
+ var result;
+
+ if ( node != null ) {
+
+ // if, instead of an object literal only a literal is given and there is a defaultKey, then wrap the literal in a map
+ if ( node.type === Syntax.Literal && defaultKey != null ) {
+ result = {};
+ result[defaultKey] = { type: Syntax.Property, value: node };
+ return result;
+ }
+
+ if ( node.type != Syntax.ObjectExpression ) {
+ // something went wrong, it's not an object literal
+ error("not an object literal:" + node.type + ":" + node.value);
+ // console.log(node.toSource());
+ return undefined;
+ }
+
+ // invariant: node.type == Syntax.ObjectExpression
+ result = {};
+ for (var i = 0; i < node.properties.length; i++) {
+ var prop = node.properties[i];
+ //console.log("objectproperty " + prop.type);
+ var name = getPropertyKey(prop);
+ //console.log("objectproperty " + prop.type + ":" + name);
+ result[name] = prop;
+ }
+ }
+ return result;
+}
+
+function isExtendCall(node) {
+
+ return (
+ node
+ && node.type === Syntax.CallExpression
+ && node.callee.type === Syntax.MemberExpression
+ && node.callee.property.type === Syntax.Identifier
+ && node.callee.property.name === 'extend'
+ && node.arguments.length >= 2
+ && node.arguments[0].type === Syntax.Literal
+ && typeof node.arguments[0].value === "string"
+ && node.arguments[1].type === Syntax.ObjectExpression
+ );
+
+}
+
+function isSapUiDefineCall(node) {
+
+ return (
+ node
+ && node.type === Syntax.CallExpression
+ && node.callee.type === Syntax.MemberExpression
+ && node.callee.object.type === Syntax.MemberExpression
+ && node.callee.object.object.type === Syntax.Identifier
+ && node.callee.object.object.name === 'sap'
+ && node.callee.object.property.type === Syntax.Identifier
+ && node.callee.object.property.name === 'ui'
+ && node.callee.property.type === Syntax.Identifier
+ && node.callee.property.name === 'define'
+ );
+
+}
+
+function isCreateDataTypeCall(node) {
+ return (
+ node
+ && node.type === Syntax.CallExpression
+ && node.callee.type === Syntax.MemberExpression
+ && /^(sap\.ui\.base\.)?DataType$/.test(getObjectName(node.callee.object))
+ && node.callee.property.type === Syntax.Identifier
+ && node.callee.property.name === 'createType'
+ );
+}
+
+function isRequireSyncCall(node) {
+ return (
+ node
+ && node.type === Syntax.CallExpression
+ && node.callee.type === Syntax.MemberExpression
+ && node.callee.object.type === Syntax.MemberExpression
+ && node.callee.object.object.type === Syntax.Identifier
+ && node.callee.object.object.name === 'sap'
+ && node.callee.object.property.type === Syntax.Identifier
+ && node.callee.object.property.name === 'ui'
+ && node.callee.property.type === Syntax.Identifier
+ && node.callee.property.name === 'requireSync'
+ );
+}
+
+function isProbingRequireCall(node) {
+ return (
+ node
+ && node.type === Syntax.CallExpression
+ && node.callee.type === Syntax.MemberExpression
+ && node.callee.object.type === Syntax.MemberExpression
+ && node.callee.object.object.type === Syntax.Identifier
+ && node.callee.object.object.name === 'sap'
+ && node.callee.object.property.type === Syntax.Identifier
+ && node.callee.object.property.name === 'ui'
+ && node.callee.property.type === Syntax.Identifier
+ && node.callee.property.name === 'require'
+ && node.arguments.length === 1
+ && node.arguments[0].type === Syntax.Literal
+ && typeof node.arguments[0].value === 'string' // TODO generalize to statically analyzable constants
+ );
+}
+
+function getObjectName(node) {
+ if ( node.type === Syntax.MemberExpression && !node.computed && node.property.type === Syntax.Identifier ) {
+ var prefix = getObjectName(node.object);
+ return prefix ? prefix + "." + node.property.name : null;
+ } else if ( node.type === Syntax.Identifier ) {
+ return /* scope[node.name] ? scope[node.name] : */ node.name;
+ } else {
+ return null;
+ }
+}
+
+/*
+ * Checks whether the node is a qualified name (a.b.c) and if so,
+ * returns the leftmost identifier a
+ */
+function getLeftmostName(node) {
+ while ( node.type === Syntax.MemberExpression ) {
+ node = node.object;
+ }
+ if ( node.type === Syntax.Identifier ) {
+ return node.name;
+ }
+ // return undefined;
+}
+
+function getResolvedObjectName(node) {
+ var name = getObjectName(node);
+ var _import = getLeftmostName(node);
+ var local = _import && currentModule.localNames[_import];
+ if ( local && (local.class || local.module) ) {
+ var resolvedName;
+ if ( local.class ) {
+ resolvedName = local.class;
+ } else {
+ resolvedName = local.module.replace(/\//g, ".").replace(/\.library$/, "");
+ if ( local.path ) {
+ resolvedName = resolvedName + "." + local.path;
+ }
+ }
+ if ( name.indexOf('.') > 0 ) {
+ resolvedName = resolvedName + name.slice(name.indexOf('.'));
+ }
+ debug("resolved " + name + " to " + resolvedName);
+ return resolvedName;
+ }
+ return name;
+}
+
+function convertValue(node, type, propertyName) {
+
+ var value;
+
+ if ( node.type === Syntax.Literal ) {
+
+ // 'string' or number or true or false
+ return node.value;
+
+ } else if ( node.type === Syntax.UnaryExpression
+ && node.prefix
+ && node.argument.type === Syntax.Literal
+ && typeof node.argument.value === 'number'
+ && ( node.operator === '-' || node.operator === '+' )) {
+
+ // -n or +n
+ value = node.argument.value;
+ return node.operator === '-' ? -value : value;
+
+ } else if ( node.type === Syntax.MemberExpression && type ) {
+
+ // enum value (a.b.c)
+ value = getResolvedObjectName(node);
+ if ( value.indexOf(type + ".") === 0 ) {
+ // starts with fully qualified enum name -> cut off name
+ return value.slice(type.length + 1);
+// } else if ( value.indexOf(type.split(".").slice(-1)[0] + ".") === 0 ) {
+// // unqualified name might be a local name (just a guess - would need static code analysis for proper solution)
+// return value.slice(type.split(".").slice(-1)[0].length + 1);
+ } else {
+ warning("did not understand default value '%s'%s, falling back to source", value, propertyName ? " of property '" + propertyName + "'" : "");
+ return value;
+ }
+
+ } else if ( node.type === Syntax.Identifier ) {
+ if ( node.name === 'undefined') {
+ // undefined
+ return undefined;
+ }
+ var local = currentModule.localNames[node.name];
+ if ( typeof local === 'object' && 'value' in local ) {
+ // TODO check type
+ return local.value;
+ }
+ } else if ( node.type === Syntax.ArrayExpression ) {
+
+ if ( node.elements.length === 0 ) {
+ // empty array literal
+ return "[]"; // TODO return this string or an empty array
+ }
+
+ if ( type && type.slice(-2) === "[]" ) {
+ var componentType = type.slice(0,-2);
+ return node.elements.map( function(elem) {
+ return convertValue(elem, componentType, propertyName);
+ });
+ }
+
+ } else if ( node.type === Syntax.ObjectExpression ) {
+
+ if ( node.properties.length === 0 && (type === 'object' || type === 'any') ) {
+ return {};
+ }
+
+ }
+
+ value = '???';
+ if ( currentSource && node.range ) {
+ value = currentSource.slice( node.range[0], node.range[1] );
+ }
+ error("unexpected type of default value (type='%s', source='%s')%s, falling back to '%s'", node.type, node.toString(), propertyName ? " of property '" + propertyName + "'" : "", value);
+ return value;
+}
+
+function convertStringArray(node) {
+ if ( node.type !== Syntax.ArrayExpression ) {
+ throw new Error("not an array");
+ }
+ var result = [];
+ for ( var i = 0; i < node.elements.length; i++ ) {
+ if ( node.elements[i].type !== Syntax.Literal || typeof node.elements[i].value !== 'string' ) {
+ throw new Error("not a string literal");
+ }
+ result.push(node.elements[i].value);
+ }
+ // console.log(result);
+ return result;
+}
+
+function convertDragDropValue(node, cardinality) {
+ var mDragDropValue;
+ var mDefaults = { draggable : false, droppable: false };
+
+ if ( node.type === Syntax.ObjectExpression ) {
+ mDragDropValue = (node.properties || []).reduce(function(oObject, oProperty) {
+ var sKey = getPropertyKey(oProperty);
+ if (mDefaults.hasOwnProperty(sKey)) {
+ oObject[sKey] = convertValue(oProperty.value);
+ }
+ return oObject;
+ }, {});
+ } else if ( node.type === Syntax.Literal ) {
+ mDragDropValue = {
+ draggable : node.value,
+ droppable : node.value
+ };
+ } else {
+ throw new Error("not a valid dnd node");
+ }
+
+ return Object.assign(mDefaults, mDragDropValue);
+}
+
+function collectClassInfo(extendCall, classDoclet) {
+
+ var baseType;
+ if ( classDoclet && classDoclet.augments && classDoclet.augments.length === 1 ) {
+ baseType = classDoclet.augments[0];
+ }
+ if ( extendCall.callee.type === Syntax.MemberExpression ) {
+ var baseCandidate = getResolvedObjectName(extendCall.callee.object);
+ if ( baseCandidate && baseType == null ) {
+ baseType = baseCandidate;
+ } else if ( baseCandidate !== baseType ) {
+ error("documented base type '" + baseType + "' doesn't match technical base type '" + baseCandidate + "'");
+ }
+ }
+
+ var oClassInfo = {
+ name : extendCall.arguments[0].value,
+ baseType : baseType,
+ interfaces : [],
+ doc : classDoclet && classDoclet.description,
+ deprecation : classDoclet && classDoclet.deprecated,
+ since : classDoclet && classDoclet.since,
+ experimental : classDoclet && classDoclet.experimental,
+ specialSettings : {},
+ properties : {},
+ aggregations : {},
+ associations : {},
+ events : {},
+ methods : {},
+ annotations : {},
+ designtime: false
+ };
+
+ function upper(n) {
+ return n.slice(0,1).toUpperCase() + n.slice(1);
+ }
+
+ function each(node, defaultKey, callback) {
+ var map,n,settings,doclet;
+
+ map = node && createPropertyMap(node.value);
+ if ( map ) {
+ for (n in map ) {
+ if ( map.hasOwnProperty(n) ) {
+ doclet = getLeadingDoclet(map[n]);
+ settings = createPropertyMap(map[n].value, defaultKey);
+ if ( settings == null ) {
+ error("no valid metadata for " + n + " (AST type '" + map[n].value.type + "')");
+ continue;
+ }
+
+ callback(n, settings, doclet, map[n]);
+ }
+ }
+ }
+ }
+
+ var classInfoNode = extendCall.arguments[1];
+ var classInfoMap = createPropertyMap(classInfoNode);
+ if ( classInfoMap && classInfoMap.metadata && classInfoMap.metadata.value.type !== Syntax.ObjectExpression ) {
+ warning("class metadata exists but can't be analyzed. It is not of type 'ObjectExpression', but a '" + classInfoMap.metadata.value.type + "'.");
+ return null;
+ }
+
+ var metadata = classInfoMap && classInfoMap.metadata && createPropertyMap(classInfoMap.metadata.value);
+ if ( metadata ) {
+
+ debug(" analyzing metadata for '" + oClassInfo.name + "'");
+
+ oClassInfo["abstract"] = !!(metadata["abstract"] && metadata["abstract"].value.value);
+ oClassInfo["final"] = !!(metadata["final"] && metadata["final"].value.value);
+ oClassInfo.dnd = metadata.dnd && convertDragDropValue(metadata.dnd.value);
+
+ if ( metadata.interfaces ) {
+ oClassInfo.interfaces = convertStringArray(metadata.interfaces.value);
+ }
+
+ each(metadata.specialSettings, "type", function(n, settings, doclet) {
+ oClassInfo.specialSettings[n] = {
+ name : n,
+ doc : doclet && doclet.description,
+ since : doclet && doclet.since,
+ deprecation : doclet && doclet.deprecated,
+ experimental : doclet && doclet.experimental,
+ visibility : (settings.visibility && settings.visibility.value.value) || "public",
+ type : settings.type ? settings.type.value.value : "any"
+ };
+ });
+
+ oClassInfo.defaultProperty = (metadata.defaultProperty && metadata.defaultProperty.value.value) || undefined;
+
+ each(metadata.properties, "type", function(n, settings, doclet) {
+ var type;
+ var N = upper(n);
+ var methods;
+ oClassInfo.properties[n] = {
+ name : n,
+ doc : doclet && doclet.description,
+ since : doclet && doclet.since,
+ deprecation : doclet && doclet.deprecated,
+ experimental : doclet && doclet.experimental,
+ visibility : (settings.visibility && settings.visibility.value.value) || "public",
+ type : (type = settings.type ? settings.type.value.value : "string"),
+ defaultValue : settings.defaultValue ? convertValue(settings.defaultValue.value, type, n) : null,
+ group : settings.group ? settings.group.value.value : 'Misc',
+ bindable : settings.bindable ? !!convertValue(settings.bindable.value) : false,
+ methods: (methods = {
+ "get": "get" + N,
+ "set": "set" + N
+ })
+ };
+ if ( oClassInfo.properties[n].bindable ) {
+ methods["bind"] = "bind" + N;
+ methods["unbind"] = "unbind" + N;
+ }
+ // if ( !settings.defaultValue ) {
+ // console.log("property without defaultValue: " + oClassInfo.name + "." + n);
+ //}
+ });
+
+ oClassInfo.defaultAggregation = (metadata.defaultAggregation && metadata.defaultAggregation.value.value) || undefined;
+
+ each(metadata.aggregations, "type", function(n, settings, doclet) {
+ var N = upper(n);
+ var methods;
+ var aggr = oClassInfo.aggregations[n] = {
+ name: n,
+ doc : doclet && doclet.description,
+ deprecation : doclet && doclet.deprecated,
+ since : doclet && doclet.since,
+ experimental : doclet && doclet.experimental,
+ visibility : (settings.visibility && settings.visibility.value.value) || "public",
+ type : settings.type ? settings.type.value.value : "sap.ui.core.Control",
+ altTypes: settings.altTypes ? convertStringArray(settings.altTypes.value) : undefined,
+ singularName : settings.singularName ? settings.singularName.value.value : guessSingularName(n),
+ cardinality : (settings.multiple && !settings.multiple.value.value) ? "0..1" : "0..n",
+ bindable : settings.bindable ? !!convertValue(settings.bindable.value) : false,
+ methods: (methods = {
+ "get": "get" + N,
+ "destroy": "destroy" + N
+ })
+ };
+
+ aggr.dnd = settings.dnd && convertDragDropValue(settings.dnd.value, aggr.cardinality);
+
+ if ( aggr.cardinality === "0..1" ) {
+ methods["set"] = "set" + N;
+ } else {
+ var N1 = upper(aggr.singularName);
+ methods["insert"] = "insert" + N1;
+ methods["add"] = "add" + N1;
+ methods["remove"] = "remove" + N1;
+ methods["indexOf"] = "indexOf" + N1;
+ methods["removeAll"] = "removeAll" + N;
+ }
+ if ( aggr.bindable ) {
+ methods["bind"] = "bind" + N;
+ methods["unbind"] = "unbind" + N;
+ }
+ });
+
+ each(metadata.associations, "type", function(n, settings, doclet) {
+ var N = upper(n);
+ var methods;
+ oClassInfo.associations[n] = {
+ name: n,
+ doc : doclet && doclet.description,
+ deprecation : doclet && doclet.deprecated,
+ since : doclet && doclet.since,
+ experimental : doclet && doclet.experimental,
+ visibility : (settings.visibility && settings.visibility.value.value) || "public",
+ type : settings.type ? settings.type.value.value : "sap.ui.core.Control",
+ singularName : settings.singularName ? settings.singularName.value.value : guessSingularName(n),
+ cardinality : (settings.multiple && settings.multiple.value.value) ? "0..n" : "0..1",
+ methods: (methods = {
+ "get": "get" + N
+ })
+ };
+ if ( oClassInfo.associations[n].cardinality === "0..1" ) {
+ methods["set"] = "set" + N;
+ } else {
+ var N1 = upper(oClassInfo.associations[n].singularName);
+ methods["add"] = "add" + N1;
+ methods["remove"] = "remove" + N1;
+ methods["removeAll"] = "removeAll" + N;
+ }
+ });
+
+ each(metadata.events, null, function(n, settings, doclet) {
+ var N = upper(n);
+ var info = oClassInfo.events[n] = {
+ name: n,
+ doc : doclet && doclet.description,
+ deprecation : doclet && doclet.deprecated,
+ since : doclet && doclet.since,
+ experimental : doclet && doclet.experimental,
+ visibility : /* (settings.visibility && settings.visibility.value.value) || */ "public",
+ allowPreventDefault : !!(settings.allowPreventDefault && settings.allowPreventDefault.value.value),
+ parameters : {},
+ methods: {
+ "attach": "attach" + N,
+ "detach": "detach" + N,
+ "fire": "fire" + N
+ }
+ };
+ each(settings.parameters, "type", function(pName, pSettings, pDoclet) {
+ info.parameters[pName] = {
+ name : pName,
+ doc : pDoclet && pDoclet.description,
+ deprecation : pDoclet && pDoclet.deprecated,
+ since : pDoclet && pDoclet.since,
+ experimental : pDoclet && pDoclet.experimental,
+ type : pSettings && pSettings.type ? pSettings.type.value.value : ""
+ };
+ });
+ });
+
+ var designtime = (metadata.designtime && convertValue(metadata.designtime.value)) || (metadata.designTime && convertValue(metadata.designTime.value));
+ if ( typeof designtime === 'string' || typeof designtime === 'boolean' ) {
+ oClassInfo.designtime = designtime;
+ }
+ // console.log(oClassInfo.name + ":" + JSON.stringify(oClassInfo, null, " "));
+ }
+
+ // remember class info by name
+ classInfos[oClassInfo.name] = oClassInfo;
+
+ return oClassInfo;
+}
+
+function collectDesigntimeInfo(dtNode) {
+
+ function each(node, defaultKey, callback) {
+ var map,n,settings,doclet;
+
+ map = node && createPropertyMap(node.value);
+ if ( map ) {
+ for (n in map ) {
+ if ( map.hasOwnProperty(n) ) {
+ doclet = getLeadingDoclet(map[n], true);
+ settings = createPropertyMap(map[n].value, defaultKey);
+ if ( settings == null ) {
+ error("no valid metadata for " + n + " (AST type '" + map[n].value.type + "')");
+ continue;
+ }
+
+ callback(n, settings, doclet, map[n]);
+ }
+ }
+ }
+ }
+
+ var oDesigntimeInfo;
+
+ var map = createPropertyMap(dtNode.argument);
+
+ if (map.annotations) {
+
+ oDesigntimeInfo = {
+ annotations: {}
+ };
+
+ each(map.annotations, null, function(n, settings, doclet) {
+ var appliesTo = [],
+ targets = [],
+ i, oAnno, iPos;
+
+ if (settings.appliesTo) {
+ for (i = 0; i < settings.appliesTo.value.elements.length; i++) {
+ appliesTo.push(settings.appliesTo.value.elements[i].value);
+ }
+ }
+
+ if (settings.target) {
+ for (i = 0; i < settings.target.value.elements.length; i++) {
+ targets.push(settings.target.value.elements[i].value);
+ }
+ }
+
+ oDesigntimeInfo.annotations[n] = {
+ name: n,
+ doc : doclet && doclet.description,
+ deprecation : doclet && doclet.deprecated,
+ since : doclet && doclet.since || settings.since && settings.since.value.value,
+ namespace: settings.namespace && settings.namespace.value.value,
+ annotation: settings.annotation && settings.annotation.value.value,
+ appliesTo: appliesTo,
+ target: targets,
+ interpretation: settings.interpretation && settings.interpretation.value.value,
+ defaultValue: settings.defaultValue && settings.defaultValue.value.value
+ };
+
+ oAnno = oDesigntimeInfo.annotations[n].annotation;
+ iPos = oAnno && oAnno.lastIndexOf(".");
+
+ if ( !oDesigntimeInfo.annotations[n].namespace && iPos > 0 ) {
+ oDesigntimeInfo.annotations[n].namespace = oAnno.slice(0, iPos);
+ oDesigntimeInfo.annotations[n].annotation = oAnno.slice(iPos + 1);
+ }
+ });
+ }
+
+ return oDesigntimeInfo;
+}
+
+function determineValueRangeBorder(range, expression, varname, inverse) {
+ if ( expression.type === Syntax.BinaryExpression ) {
+ var value;
+ if ( expression.left.type === Syntax.Identifier && expression.left.name === varname && expression.right.type === Syntax.Literal ) {
+ value = expression.right.value;
+ } else if ( expression.left.type === Syntax.Literal && expression.right.type === Syntax.Identifier && expression.right.name === varname ) {
+ inverse = !inverse;
+ value = expression.left.value;
+ } else {
+ return false;
+ }
+ switch (expression.operator) {
+ case '<':
+ range[inverse ? 'minExclusive' : 'maxExclusive'] = value;
+ break;
+ case '<=':
+ range[inverse ? 'minInclusive' : 'maxInclusive'] = value;
+ break;
+ case '>=':
+ range[inverse ? 'maxInclusive' : 'minInclusive'] = value;
+ break;
+ case '>':
+ range[inverse ? 'maxExclusive' : 'minExclusive'] = value;
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ return false;
+}
+
+function determineValueRange(expression, varname, inverse) {
+ var range = {};
+ if ( expression.type === Syntax.LogicalExpression
+ && expression.operator === '&&'
+ && expression.left.type === Syntax.BinaryExpression
+ && expression.right.type === Syntax.BinaryExpression
+ && determineValueRangeBorder(range, expression.left, varname, inverse)
+ && determineValueRangeBorder(range, expression.right, varname, inverse) ) {
+ return range;
+ } else if ( expression.type === Syntax.BinaryExpression
+ && determineValueRangeBorder(range, expression, varname, inverse) ) {
+ return range;
+ }
+ return undefined;
+}
+
+function collectDataTypeInfo(extendCall, classDoclet) {
+ var args = extendCall.arguments,
+ i = 0,
+ name, def, base, pattern, range;
+
+ if ( i < args.length && args[i].type === Syntax.Literal && typeof args[i].value === 'string' ) {
+ name = args[i++].value;
+ }
+ if ( i < args.length && args[i].type === Syntax.ObjectExpression ) {
+ def = createPropertyMap(args[i++]);
+ }
+ if ( i < args.length ) {
+ if ( args[i].type === Syntax.Literal && typeof args[i].value === 'string' ) {
+ base = args[i++].value;
+ } else if ( args[i].type === Syntax.CallExpression
+ && args[i].callee.type === Syntax.MemberExpression
+ && /^(sap\.ui\.base\.)?DataType$/.test(getObjectName(args[i].callee.object))
+ && args[i].callee.property.type === Syntax.Identifier
+ && args[i].callee.property.name === 'getType'
+ && args[i].arguments.length === 1
+ && args[i].arguments[0].type === Syntax.Literal
+ && typeof args[i].arguments[0].value === 'string' ) {
+ base = args[i++].arguments[0].value;
+ } else {
+ error("could not identify base type of data type '" + name + "'");
+ }
+ } else {
+ base = "any";
+ }
+
+ if ( def
+ && def.isValid
+ && def.isValid.value.type === Syntax.FunctionExpression
+ && def.isValid.value.params.length === 1
+ && def.isValid.value.params[0].type === Syntax.Identifier
+ && def.isValid.value.body.body.length === 1 ) {
+ var varname = def.isValid.value.params[0].name;
+ var stmt = def.isValid.value.body.body[0];
+ if ( stmt.type === Syntax.ReturnStatement && stmt.argument ) {
+ if ( stmt.argument.type === Syntax.CallExpression
+ && stmt.argument.callee.type === Syntax.MemberExpression
+ && stmt.argument.callee.object.type === Syntax.Literal
+ && stmt.argument.callee.object.regex
+ && stmt.argument.callee.property.type === Syntax.Identifier
+ && stmt.argument.callee.property.name === 'test' ) {
+ pattern = stmt.argument.callee.object.regex.pattern;
+ // console.log(pattern);
+ } else {
+ range = determineValueRange(stmt.argument, varname, false);
+ }
+ } else if ( stmt.type === Syntax.IfStatement
+ && stmt.consequent.type === Syntax.BlockStatement
+ && stmt.consequent.body.length === 1
+ && stmt.consequent.body[0].type === Syntax.ReturnStatement
+ && stmt.consequent.body[0].argument
+ && stmt.consequent.body[0].argument.type === Syntax.Literal
+ && typeof stmt.consequent.body[0].argument.value === 'boolean'
+ && stmt.alternate.type === Syntax.BlockStatement
+ && stmt.alternate.body.length === 1
+ && stmt.alternate.body[0].type === Syntax.ReturnStatement
+ && stmt.alternate.body[0].argument
+ && stmt.alternate.body[0].argument.type === Syntax.Literal
+ && typeof stmt.alternate.body[0].argument.value === 'boolean'
+ && stmt.consequent.body[0].argument.value !== typeof stmt.alternate.body[0].argument.value ) {
+ var inverse = stmt.alternate.body[0].argument.value;
+ range = determineValueRange(stmt.test, varname, inverse);
+ } else {
+ debug("unexpected implementation of a DataType's isValid() implementation: ", stmt);
+ }
+ }
+
+ // remember type info by name
+ if ( name && def && base ) {
+ typeInfos[name] = {
+ name: name,
+ def: def,
+ pattern: pattern,
+ range: range,
+ base: base
+ };
+ // console.log("found data type:", typeInfos[name]);
+ }
+}
+
+var rEmptyLine = /^\s*$/;
+
+function createAutoDoc(oClassInfo, classComment, node, parser, filename, commentAlreadyProcessed) {
+
+ var newStyle = !!pluginConfig.newStyle,
+ includeSettings = !!pluginConfig.includeSettingsInConstructor,
+ rawClassComment = getRawComment(classComment),
+ p,n,n1,pName,info,lines,link;
+
+ function isEmpty(obj) {
+ if ( !obj ) {
+ return true;
+ }
+ for (var n in obj) {
+ if ( obj.hasOwnProperty(n) ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ function jsdocCommentFound(comment) {
+ parser.emit('jsdocCommentFound', {
+ event:'jsdocCommentFound',
+ comment : comment,
+ lineno : node.loc.start.line,
+ filename : filename,
+ range : [ node.range[0], node.range[0] ]
+ }, parser);
+ }
+
+ function removeDuplicateEmptyLines(lines) {
+ var lastWasEmpty = false,
+ i,j,l,line;
+
+ for (i = 0, j = 0, l = lines.length; i < l; i++) {
+ line = lines[i];
+ if ( line == null || rEmptyLine.test(line) ) {
+ if ( !lastWasEmpty ) {
+ lines[j++] = line;
+ }
+ lastWasEmpty = true;
+ } else {
+ lines[j++] = line;
+ lastWasEmpty = false;
+ }
+ }
+ return j < i ? lines.slice(0,j) : lines;
+ }
+
+ function newJSDoc(lines) {
+ //console.log("add completely new jsdoc comment to prog " + node.type + ":" + node.nodeId + ":" + Object.keys(node));
+
+ lines = removeDuplicateEmptyLines(lines);
+ lines.push("@synthetic");
+
+ var comment = " * " + lines.join("\r\n * ");
+ jsdocCommentFound("/**\r\n" + comment + "\r\n */");
+
+ var m = /@name\s+([^\r\n\t ]+)/.exec(comment);
+ debug(" creating synthetic comment '" + (m && m[1]) + "'");
+ }
+
+ function rname(prefix,n,_static) {
+ return (_static ? "." : "#") + prefix + n.slice(0,1).toUpperCase() + n.slice(1);
+ }
+
+ function name(prefix,n,_static) {
+ return oClassInfo.name + rname(prefix,n,_static);
+ }
+
+ /*
+ * creates a JSDoc type string from the given metadata info object.
+ * It takes into account the type, the altTypes and the cardinality
+ * (the latter only if componentTypeOnly is not set).
+ */
+ function makeTypeString(aggr, componentTypeOnly) {
+ var s = aggr.type;
+ if ( aggr.altTypes ) {
+ s = s + "|" + aggr.altTypes.join("|");
+ }
+ if ( !componentTypeOnly && aggr.cardinality === "0..n" ) {
+ // if multiple types are allowed, use Array.<> for proper grouping
+ if ( aggr.altTypes ) {
+ s = "Array.<" + s + ">";
+ } else {
+ s = s + "[]";
+ }
+ }
+ return s;
+ }
+
+// function shortname(s) {
+// return s.slice(s.lastIndexOf('.') + 1);
+// }
+
+ var HUNGARIAN_PREFIXES = {
+ 'int' : 'i',
+ 'boolean' : 'b',
+ 'float' : 'f',
+ 'string' : 's',
+ 'function' : 'fn',
+ 'object' : 'o',
+ 'regexp' : 'r',
+ 'jQuery' : '$',
+ 'any' : 'o',
+ 'variant' : 'v',
+ 'map' : 'm'
+ };
+
+ function varname(n, type, property) {
+ var prefix = HUNGARIAN_PREFIXES[type] || (property ? "s" : "o");
+ return prefix + n.slice(0,1).toUpperCase() + n.slice(1);
+ }
+
+ // add a list of the possible settings if and only if
+ // - documentation for the constructor exists
+ // - no (generated) documentation for settings exists already
+ // - a suitable place for inserting the settings can be found
+ var m = /(?:^|\r\n|\n|\r)[ \t]*\**[ \t]*@[a-zA-Z]/.exec(rawClassComment);
+ p = m ? m.index : -1;
+ var hasSettingsDocs = rawClassComment.indexOf("The supported settings are:") >= 0;
+
+ // heuristic to recognize a ManagedObject
+ var isManagedObject = (
+ /@extends\s+sap\.ui\.(?:base\.ManagedObject|core\.(?:Element|Control|Component))(?:\s|$)/.test(rawClassComment)
+ || oClassInfo.library
+ || !isEmpty(oClassInfo.specialSettings)
+ || !isEmpty(oClassInfo.properties)
+ || !isEmpty(oClassInfo.aggregations)
+ || !isEmpty(oClassInfo.associations)
+ || !isEmpty(oClassInfo.events)
+ );
+
+ if ( p >= 0 && !hasSettingsDocs ) {
+ lines = [
+ ""
+ ];
+
+ if ( isManagedObject ) { // only a ManagedObject has settings
+
+ if ( oClassInfo.name !== "sap.ui.base.ManagedObject" ) {
+ // add the hint for the general description only when the current class is not ManagedObject itself
+ lines.push(
+ "",
+ "Accepts an object literal mSettings that defines initial",
+ "property values, aggregated and associated objects as well as event handlers.",
+ "See {@link sap.ui.base.ManagedObject#constructor} for a general description of the syntax of the settings object."
+ );
+ }
+
+ // add the settings section only if there are any settings
+ if ( !isEmpty(oClassInfo.properties)
+ || !isEmpty(oClassInfo.aggregations)
+ || !isEmpty(oClassInfo.associations)
+ || !isEmpty(oClassInfo.events) ) {
+
+ lines.push(
+ "",
+ includeSettings ? "" : "@ui5-settings",
+ "The supported settings are:",
+ "
");
+ for (n in oClassInfo.events) {
+ lines.push("
{@link " + "#event:" + n + " " + n + "} : fnListenerFunction or [fnListenerFunction, oListenerObject] or [oData, fnListenerFunction, oListenerObject]
");
+ }
+ lines.push("
");
+ lines.push("
");
+ }
+ lines.push("
");
+
+ // add the reference to the base class only if this is not ManagedObject and if the base class is known
+ if ( oClassInfo.name !== "sap.ui.base.ManagedObject" && oClassInfo.baseType ) {
+ lines.push(
+ "",
+ "In addition, all settings applicable to the base type {@link " + oClassInfo.baseType + "#constructor " + oClassInfo.baseType + "}",
+ "can be used as well."
+ );
+ }
+ lines.push("");
+
+ } else if ( oClassInfo.name !== "sap.ui.base.ManagedObject" && oClassInfo.baseType && oClassInfo.hasOwnProperty("abstract") ) {
+
+ // if a class has no settings, but metadata, point at least to the base class - if it makes sense
+ lines.push(
+ "",
+ newStyle && !includeSettings ? "@ui5-settings" : "",
+ "This class does not have its own settings, but all settings applicable to the base type",
+ "{@link " + oClassInfo.baseType + "#constructor " + oClassInfo.baseType + "} can be used."
+ );
+
+ }
+ }
+
+ debug(" enhancing constructor documentation with settings");
+ var enhancedComment =
+ rawClassComment.slice(0,p) +
+ "\n * " + removeDuplicateEmptyLines(lines).join("\n * ") +
+ (commentAlreadyProcessed ? "@ui5-updated-doclet\n * " : "") +
+ rawClassComment.slice(p);
+ enhancedComment = preprocessComment({ comment : enhancedComment, lineno : classComment.lineno });
+
+ if ( commentAlreadyProcessed ) {
+ jsdocCommentFound(enhancedComment);
+ } else {
+ setRawComment(classComment, enhancedComment);
+ }
+
+ }
+
+ newJSDoc([
+ "Returns a metadata object for class " + oClassInfo.name + ".",
+ "",
+ "@returns {sap.ui.base.Metadata} Metadata object describing this class",
+ "@public",
+ "@static",
+ "@name " + name("getMetadata", "", true),
+ "@function"
+ ]);
+
+ if ( !oClassInfo["final"] ) {
+ newJSDoc([
+ "Creates a new subclass of class " + oClassInfo.name + " with name sClassName",
+ "and enriches it with the information contained in oClassInfo.",
+ "",
+ "oClassInfo might contain the same kind of information as described in {@link " + (oClassInfo.baseType ? oClassInfo.baseType + ".extend" : "sap.ui.base.Object.extend Object.extend") + "}.",
+ "",
+ "@param {string} sClassName Name of the class being created",
+ "@param {object} [oClassInfo] Object literal with information about the class",
+ "@param {function} [FNMetaImpl] Constructor function for the metadata object; if not given, it defaults to sap.ui.core.ElementMetadata",
+ "@returns {function} Created class / constructor function",
+ "@public",
+ "@static",
+ "@name " + name("extend", "", true),
+ "@function"
+ ]);
+ }
+
+ for (n in oClassInfo.properties ) {
+ info = oClassInfo.properties[n];
+ if ( info.visibility === 'hidden' ) {
+ continue;
+ }
+ // link = newStyle ? "{@link #setting:" + n + " " + n + "}" : "" + n + "";
+ link = "{@link " + (newStyle ? "#setting:" + n : rname("get", n)) + " " + n + "}";
+ newJSDoc([
+ "Gets current value of property " + link + ".",
+ "",
+ !newStyle && info.doc ? info.doc : "",
+ "",
+ info.defaultValue !== null ? "Default value is " + (info.defaultValue === "" ? "empty string" : info.defaultValue) + "." : "",
+ "@returns {" + info.type + "} Value of property " + n + "",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("get",n),
+ "@function"
+ ]);
+ newJSDoc([
+ "Sets a new value for property " + link + ".",
+ "",
+ !newStyle && info.doc ? info.doc : "",
+ "",
+ "When called with a value of null or undefined, the default value of the property will be restored.",
+ "",
+ info.defaultValue !== null ? "Default value is " + (info.defaultValue === "" ? "empty string" : info.defaultValue) + "." : "",
+ "@param {" + info.type + "} " + varname(n,info.type,true) + " New value for property " + n + "",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("set",n),
+ "@function"
+ ]);
+ if ( info.bindable ) {
+ newJSDoc([
+ "Binds property " + link + " to model data.",
+ "",
+ "See {@link sap.ui.base.ManagedObject#bindProperty ManagedObject.bindProperty} for a ",
+ "detailed description of the possible properties of oBindingInfo",
+ "@param {object} oBindingInfo The binding information",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("bind",n),
+ "@function"
+ ]);
+ newJSDoc([
+ "Unbinds property " + link + " from model data.",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("unbind",n),
+ "@function"
+ ]);
+ }
+ }
+
+ for (n in oClassInfo.aggregations ) {
+ info = oClassInfo.aggregations[n];
+ if ( info.visibility === 'hidden' ) {
+ continue;
+ }
+ // link = newStyle ? "{@link #setting:" + n + " " + n + "}" : "" + n + "";
+ link = "{@link " + (newStyle ? "#setting:" + n : rname("get", n)) + " " + n + "}";
+ newJSDoc([
+ "Gets content of aggregation " + link + ".",
+ "",
+ !newStyle && info.doc ? info.doc : "",
+ "",
+ n === info.defaultAggregation ? "Note: this is the default aggregation for " + n + "." : "",
+ "@returns {" + makeTypeString(info) + "}",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("get",n),
+ "@function"
+ ]);
+ if ( info.cardinality == "0..n" ) {
+ n1 = info.singularName;
+ newJSDoc([
+ "Inserts a " + n1 + " into the aggregation " + link + ".",
+ "",
+ "@param {" + makeTypeString(info, true) + "}",
+ " " + varname(n1,info.altTypes ? "variant" : info.type) + " The " + n1 + " to insert; if empty, nothing is inserted",
+ "@param {int}",
+ " iIndex The 0-based index the " + n1 + " should be inserted at; for",
+ " a negative value of iIndex, the " + n1 + " is inserted at position 0; for a value",
+ " greater than the current size of the aggregation, the " + n1 + " is inserted at",
+ " the last position",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("insert",n1),
+ "@function"
+ ]);
+ newJSDoc([
+ "Adds some " + n1 + " to the aggregation " + link + ".",
+
+ "@param {" + makeTypeString(info, true) + "}",
+ " " + varname(n1,info.altTypes ? "variant" : info.type) + " The " + n1 + " to add; if empty, nothing is inserted",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("add",n1),
+ "@function"
+ ]);
+ newJSDoc([
+ "Removes a " + n1 + " from the aggregation " + link + ".",
+ "",
+ "@param {int | string | " + makeTypeString(info, true) + "} " + varname(n1,"variant") + " The " + n1 + " to remove or its index or id",
+ "@returns {" + makeTypeString(info, true) + "} The removed " + n1 + " or null",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("remove", n1),
+ "@function"
+ ]);
+ newJSDoc([
+ "Removes all the controls from the aggregation " + link + ".",
+ "",
+ "Additionally, it unregisters them from the hosting UIArea.",
+ "@returns {" + makeTypeString(info) + "} An array of the removed elements (might be empty)",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("removeAll", n),
+ "@function"
+ ]);
+ newJSDoc([
+ "Checks for the provided " + info.type + " in the aggregation " + link + ".",
+ "and returns its index if found or -1 otherwise.",
+ "@param {" + makeTypeString(info, true) + "}",
+ " " + varname(n1, info.altTypes ? "variant" : info.type) + " The " + n1 + " whose index is looked for",
+ "@returns {int} The index of the provided control in the aggregation if found, or -1 otherwise",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("indexOf", n1),
+ "@function"
+ ]);
+ } else {
+ newJSDoc([
+ "Sets the aggregated " + link + ".",
+ "@param {" + makeTypeString(info) + "} " + varname(n, info.altTypes ? "variant" : info.type) + " The " + n + " to set",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("set", n),
+ "@function"
+ ]);
+ }
+ newJSDoc([
+ "Destroys " + (info.cardinality === "0..n" ? "all " : "") + "the " + n + " in the aggregation " + link + ".",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("destroy", n),
+ "@function"
+ ]);
+ if ( info.bindable ) {
+ newJSDoc([
+ "Binds aggregation " + link + " to model data.",
+ "",
+ "See {@link sap.ui.base.ManagedObject#bindAggregation ManagedObject.bindAggregation} for a ",
+ "detailed description of the possible properties of oBindingInfo.",
+ "@param {object} oBindingInfo The binding information",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("bind",n),
+ "@function"
+ ]);
+ newJSDoc([
+ "Unbinds aggregation " + link + " from model data.",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("unbind",n),
+ "@function"
+ ]);
+ }
+ }
+
+ for (n in oClassInfo.associations ) {
+ info = oClassInfo.associations[n];
+ if ( info.visibility === 'hidden' ) {
+ continue;
+ }
+ // link = newStyle ? "{@link #setting:" + n + " " + n + "}" : "" + n + "";
+ link = "{@link " + (newStyle ? "#setting:" + n : rname("get", n)) + " " + n + "}";
+ newJSDoc([
+ info.cardinality === "0..n" ?
+ "Returns array of IDs of the elements which are the current targets of the association " + link + "." :
+ "ID of the element which is the current target of the association " + link + ", or null.",
+ "",
+ newStyle && info.doc ? info.doc : "",
+ "",
+ "@returns {sap.ui.core.ID" + (info.cardinality === "0..n" ? "[]" : "") + "}",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("get",n),
+ "@function"
+ ]);
+ if ( info.cardinality === "0..n" ) {
+ n1 = info.singularName;
+ newJSDoc([
+ "Adds some " + n1 + " into the association " + link + ".",
+ "",
+ "@param {sap.ui.core.ID | " + info.type + "} " + varname(n1, "variant") + " The " + n + " to add; if empty, nothing is inserted",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("add",n1),
+ "@function"
+ ]);
+ newJSDoc([
+ "Removes an " + n1 + " from the association named " + link + ".",
+ "@param {int | sap.ui.core.ID | " + info.type + "} " + varname(n1,"variant") + " The " + n1 + " to be removed or its index or ID",
+ "@returns {sap.ui.core.ID} The removed " + n1 + " or null",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("remove", n1),
+ "@function"
+ ]);
+ newJSDoc([
+ "Removes all the controls in the association named " + link + ".",
+ "@returns {sap.ui.core.ID[]} An array of the removed elements (might be empty)",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("removeAll", n),
+ "@function"
+ ]);
+ } else {
+ newJSDoc([
+ "Sets the associated " + link + ".",
+ "@param {sap.ui.core.ID | " + info.type + "} " + varname(n, info.type) + " ID of an element which becomes the new target of this " + n + " association; alternatively, an element instance may be given",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("set", n),
+ "@function"
+ ]);
+ }
+ }
+
+ for (n in oClassInfo.events ) {
+ info = oClassInfo.events[n];
+ //link = newStyle ? "{@link #event:" + n + " " + n + "}" : "" + n + "";
+ link = "{@link #event:" + n + " " + n + "}";
+
+ lines = [
+ info.doc ? info.doc : "",
+ "",
+ "@name " + oClassInfo.name + "#" + n,
+ "@event",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@param {sap.ui.base.Event} oControlEvent",
+ "@param {sap.ui.base.EventProvider} oControlEvent.getSource",
+ "@param {object} oControlEvent.getParameters"
+ ];
+ for (pName in info.parameters ) {
+ lines.push(
+ "@param {" + (info.parameters[pName].type || "") + "} oControlEvent.getParameters." + pName + " " + (info.parameters[pName].doc || "")
+ );
+ }
+ lines.push("@public");
+ newJSDoc(lines);
+
+ newJSDoc([
+ "Attaches event handler fnFunction to the " + link + " event of this " + oClassInfo.name + ".",
+ "",
+ "When called, the context of the event handler (its this) will be bound to oListener if specified, ",
+ "otherwise it will be bound to this " + oClassInfo.name + " itself.",
+ "",
+ !newStyle && info.doc ? info.doc : "",
+ "",
+ "@param {object}",
+ " [oData] An application-specific payload object that will be passed to the event handler along with the event object when firing the event",
+ "@param {function}",
+ " fnFunction The function to be called when the event occurs",
+ "@param {object}",
+ " [oListener] Context object to call the event handler with. Defaults to this " + oClassInfo.name + " itself",
+ "",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ "@public",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@name " + name("attach", n),
+ "@function"
+ ]);
+ newJSDoc([
+ "Detaches event handler fnFunction from the " + link + " event of this " + oClassInfo.name + ".",
+ "",
+ "The passed function and listener object must match the ones used for event registration.",
+ "",
+ "@param {function}",
+ " fnFunction The function to be called, when the event occurs",
+ "@param {object}",
+ " [oListener] Context object on which the given function had to be called",
+ "@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@public",
+ "@name " + name("detach", n),
+ "@function"
+ ]);
+
+ // build documentation for fireEvent. It contains conditional parts which makes it a bit more complicated
+ lines = [
+ "Fires event " + link + " to attached listeners."
+ ];
+ if ( info.allowPreventDefault ) {
+ lines.push(
+ "",
+ "Listeners may prevent the default action of this event by using the preventDefault-method on the event object.",
+ "");
+ }
+ lines.push(
+ "",
+ "@param {object} [mParameters] Parameters to pass along with the event"
+ );
+ if ( !isEmpty(info.parameters) ) {
+ for (pName in info.parameters) {
+ lines.push(
+ "@param {" + (info.parameters[pName].type || "any") + "} [mParameters." + pName + "] " + (info.parameters[pName].doc || "")
+ );
+ }
+ lines.push("");
+ }
+ if ( info.allowPreventDefault ) {
+ lines.push("@returns {boolean} Whether or not to prevent the default action");
+ } else {
+ lines.push("@returns {" + oClassInfo.name + "} Reference to this in order to allow method chaining");
+ }
+ lines.push(
+ "@protected",
+ info.since ? "@since " + info.since : "",
+ info.deprecation ? "@deprecated " + info.deprecation : "",
+ info.experimental ? "@experimental " + info.experimental : "",
+ "@name " + name("fire", n),
+ "@function"
+ );
+ newJSDoc(lines);
+ }
+
+}
+
+function createDataTypeAutoDoc(oTypeInfo, classComment, node, parser, filename) {
+}
+
+/**
+ * Creates a human readable location info for a given doclet.
+ * @param {Doclet} doclet Doclet to get a location info for
+ * @returns {string} A human readable location info
+ */
+function location(doclet) {
+ var filename = (doclet.meta && doclet.meta.filename) || "unknown";
+ return " #" + ui5data(doclet).id + "@" + filename + (doclet.meta.lineno != null ? ":" + doclet.meta.lineno : "") + (doclet.synthetic ? "(synthetic)" : "");
+}
+
+// ---- Comment handling ---------------------------------------------------------------------------
+
+// --- comment related functions that depend on the JSdoc version (e.g. on the used parser)
+
+var isDocComment;
+var getLeadingCommentNode;
+
+// JSDoc added the node type Syntax.File with the same change that activated Babylon
+// See https://github.com/jsdoc3/jsdoc/commit/ffec4a42291de6d68e6240f304b68d6abb82a869
+if ( Syntax.File === 'File' ) {
+
+ // JSDoc starting with version 3.5.0
+
+ isDocComment = function isDocCommentBabylon(comment) {
+ return comment && comment.type === 'CommentBlock' && comment.value && comment.value.charAt(0) === '*';
+ };
+
+ getLeadingCommentNode = function getLeadingCommentNodeBabylon(node, longname) {
+ var leadingComments = node.leadingComments;
+ if ( Array.isArray(leadingComments) ) {
+ // in babylon, all comments are already attached to the node
+ // and the last one is the closest one and should win
+ // non-block comments have to be filtered out
+ leadingComments = leadingComments.filter(isDocComment);
+ if ( leadingComments.length > 0 ) {
+ return leadingComments[leadingComments.length - 1];
+ }
+ }
+ };
+
+} else {
+
+ // JSDoc versions before 3.5.0
+
+ isDocComment = function isDoccommentEsprima(comment) {
+ return comment && comment.type === 'Block';
+ };
+
+ getLeadingCommentNode = function getLeadingCommentNodeEsprima(node, longname) {
+ var comment,
+ leadingComments = node.leadingComments;
+
+ // when espree is used, JSDOc attached the leading comment and the first one was picked
+ if (Array.isArray(leadingComments) && leadingComments.length && leadingComments[0].raw) {
+ comment = leadingComments[0];
+ }
+
+ // also check all comments attached to the Program node (if found) whether they refer to the same longname
+ // TODO check why any matches here override the direct leading comment from above
+ if ( longname && currentProgram && currentProgram.leadingComments && currentProgram.leadingComments.length ) {
+ leadingComments = currentProgram.leadingComments;
+ var rLongname = new RegExp("@(name|alias|class|namespace)\\s+" + longname.replace(/\./g, '\\.'));
+ for ( var i = 0; i < leadingComments.length; i++ ) {
+ var raw = getRawComment(leadingComments[i]);
+ if ( /^\/\*\*[\s\S]*\*\/$/.test(raw) && rLongname.test(raw) ) {
+ comment = leadingComments[i];
+ // console.log("\n\n**** alternative comment found for " + longname + " on program level\n\n", comment);
+ break;
+ }
+ }
+ }
+
+ return comment;
+ };
+}
+
+//--- comment related functions that are independent from the JSdoc version
+
+function getLeadingComment(node) {
+ var comment = getLeadingCommentNode(node);
+ return comment ? getRawComment(comment) : null;
+}
+
+function getLeadingDoclet(node, preprocess) {
+ var comment = getLeadingComment(node);
+ if ( comment && preprocess ) {
+ comment = preprocessComment({comment:comment, lineno: node.loc.start.line });
+ }
+ return comment ? new Doclet(comment, {}) : null;
+}
+
+/**
+ * Determines the raw comment string (source code form, including leading and trailing comment markers / *...* /) from a comment node.
+ * Works for Esprima and Babylon based JSDoc versions.
+ * @param {ASTNode} commentNode Node that contains the comment.
+ * @returns {string} Comment string as written in the source
+ */
+function getRawComment(commentNode) {
+ // in esprima, there's a 'raw' property, in babylon, the 'raw' string has to be reconstructed from the 'value' by adding the markers
+ return commentNode ? commentNode.raw || '/*' + commentNode.value + '*/' : '';
+}
+
+function setRawComment(commentNode, newRawComment) {
+ if ( commentNode.raw ) {
+ commentNode.raw = newRawComment;
+ }
+ commentNode.value = newRawComment.slice(2, -2);
+}
+
+/**
+ * Removes the mandatory comment markers and the optional but common asterisks at the beginning of each JSDoc comment line.
+ *
+ * The result is easier to parse/analyze.
+ *
+ * Implementation is a 1:1 copy from JSDoc's lib/jsdoc/doclet.js (closure function, not directly reusable)
+ *
+ * @param {string} docletSrc the source comment with or without block comment markers
+ * @returns {string} the unwrapped content of the JSDoc comment
+ *
+ */
+function unwrap(docletSrc) {
+ if (!docletSrc) { return ''; }
+
+ // note: keep trailing whitespace for @examples
+ // extra opening/closing stars are ignored
+ // left margin is considered a star and a space
+ // use the /m flag on regex to avoid having to guess what this platform's newline is
+ docletSrc =
+ docletSrc.replace(/^\/\*\*+/, '') // remove opening slash+stars
+ .replace(/\**\*\/$/, "\\Z") // replace closing star slash with end-marker
+ .replace(/^\s*(\* ?|\\Z)/gm, '') // remove left margin like: spaces+star or spaces+end-marker
+ .replace(/\s*\\Z$/g, ''); // remove end-marker
+
+ return docletSrc;
+}
+
+/**
+ * Inverse operation of unwrap.
+ *
+ * The prefix for lines is fixed to be " * ", lines are separated with '\n', independent from the platform.
+ * @param {string|string[]} lines Multiline string or an array of lines
+ * @returns {string} Full comment string created from the line(s)
+ */
+function wrap(lines) {
+ if ( typeof lines === "string" ) {
+ lines = lines.split(/\r\n?|\n/);
+ }
+ return "/**\n * " + lines.join('\n * ') + "\n */";
+}
+
+/**
+ * Pre-processes a JSDoc comment string to ensure some UI5 standards.
+ *
+ * @param {event} e Event for the new comment
+ * @returns {event} Returns the modified event
+ */
+function preprocessComment(e) {
+
+ var src = e.comment;
+
+ // add a default visibility
+ if ( !/@private|@public|@protected|@sap-restricted|@ui5-restricted/.test(src) ) {
+ src = unwrap(src);
+ src = src + "\n@private";
+ src = wrap(src);
+ // console.log("added default visibility to '" + src + "'");
+ }
+
+ if ( /@class/.test(src) && /@static/.test(src) ) {
+ warning("combination of @class and @static is no longer supported with jsdoc3, converting it to @namespace and @classdesc: (line " + e.lineno + ")");
+ src = unwrap(src);
+ src = src.replace(/@class/, "@classdesc").replace(/@static/, "@namespace");
+ src = wrap(src);
+ //console.log(src);
+ }
+
+ return src;
+
+}
+
+// ---- other functionality ---------------------------------------------------------------------------
+
+// HACK: override cli.exit() to avoid that JSDoc3 exits the VM
+if ( pluginConfig.noExit ) {
+ info("disabling exit() call");
+ require( path.join(global.env.dirname, 'cli') ).exit = function(retval) {
+ info("cli.exit(): do nothing (ret val=" + retval + ")");
+ };
+}
+
+
+// ---- exports ----------------------------------------------------------------------------------------
+
+exports.defineTags = function(dictionary) {
+
+ /**
+ * a special value that is not 'falsy' but results in an empty string when output
+ * Used for the disclaimer and experimental tag
+ */
+ var EMPTY = {
+ toString: function() { return ""; }
+ };
+
+ /**
+ * A sapui5 specific tag to add a disclaimer to a symbol
+ */
+ dictionary.defineTag('disclaimer', {
+ // value is optional
+ onTagged: function(doclet, tag) {
+ doclet.disclaimer = tag.value || EMPTY;
+ }
+ });
+
+ /**
+ * A sapui5 specific tag to mark a symbol as experimental.
+ */
+ dictionary.defineTag('experimental', {
+ // value is optional
+ onTagged: function(doclet, tag) {
+ doclet.experimental = tag.value || EMPTY;
+ }
+ });
+
+ /**
+ * Re-introduce the deprecated 'final tag. JSDoc used it as a synonym for readonly, but we use it to mark classes as final
+ */
+ dictionary.defineTag('final', {
+ mustNotHaveValue: true,
+ onTagged: function(doclet, tag) {
+ doclet.final_ = true;
+ }
+ });
+
+ /**
+ * Introduce a new kind of symbol: 'interface'
+ * 'interface' is like 'class', but without a constructor.
+ * Support for 'interface' might not be complete (only standard UI5 use cases tested)
+ */
+ dictionary.defineTag('interface', {
+ //mustNotHaveValue: true,
+ onTagged: function(doclet, tag) {
+ // debug("setting kind of " + doclet.name + " to 'interface'");
+ doclet.kind = 'interface';
+ if ( tag.value ) {
+ doclet.classdesc = tag.value;
+ }
+ }
+ });
+
+ /**
+ * Classes can declare that they implement a set of interfaces
+ */
+ dictionary.defineTag('implements', {
+ mustHaveValue: true,
+ onTagged: function(doclet, tag) {
+ // console.log("setting implements of " + doclet.name + " to 'interface'");
+ if ( tag.value ) {
+ doclet.implements = doclet.implements || [];
+ tag.value.split(/\s*,\s*/g).forEach(function($) {
+ if ( doclet.implements.indexOf($) < 0 ) {
+ doclet.implements.push($);
+ }
+ });
+ }
+ }
+ });
+
+ /**
+ * Set the visibility of a doclet to 'restricted'.
+ */
+ dictionary.defineTag('ui5-restricted', {
+ onTagged: function(doclet, tag) {
+ doclet.access = 'restricted';
+ if ( tag.value ) {
+ ui5data(doclet).stakeholders = tag.value.trim().split(/(?:\s*,\s*|\s+)/);
+ }
+ }
+ });
+ dictionary.defineSynonym('ui5-restricted', 'sap-restricted');
+
+ /**
+ * Mark a doclet as synthetic.
+ *
+ * Used for doclets that the autodoc generation creates. This helps the template
+ * later to recognize such doclets and maybe filter them out.
+ */
+ dictionary.defineTag('synthetic', {
+ mustNotHaveValue: true,
+ onTagged: function(doclet, tag) {
+ doclet.synthetic = true;
+ }
+ });
+
+ /**
+ * Mark a doclet that intentionally updates a previous doclet
+ */
+ dictionary.defineTag('ui5-updated-doclet', {
+ mustNotHaveValue: true,
+ onTagged: function(doclet, tag) {
+ ui5data(doclet).updatedDoclet = true;
+ }
+ });
+
+ /**
+ * The @hideconstructor tag tells JSDoc that the generated documentation should not display the constructor for a class.
+ * Note: this tag will be natively available in JSDoc >= 3.5.0
+ */
+ dictionary.defineTag('hideconstructor', {
+ mustNotHaveValue: true,
+ onTagged: function(doclet, tag) {
+ doclet.hideconstructor = true;
+ }
+ });
+
+};
+
+exports.handlers = {
+
+ /**
+ * Before all files are parsed, determine the common path prefix of all filenames
+ * @param {object} e Event info object
+ */
+ parseBegin : function(e) {
+
+ pathPrefixes = env.opts._.reduce(function(result, fileOrDir) {
+ fileOrDir = path.resolve( path.normalize(fileOrDir) );
+ if ( fs.statSync(fileOrDir).isDirectory() ) {
+ // ensure a trailing path separator
+ if ( fileOrDir.indexOf(path.sep, fileOrDir.length - path.sep.length) < 0 ) {
+ fileOrDir += path.sep;
+ }
+ result.push(fileOrDir);
+ }
+ return result;
+ }, []);
+ resourceNamePrefixes = pluginConfig.resourceNamePrefixes || [];
+ if ( !Array.isArray(resourceNamePrefixes) ) {
+ resourceNamePrefixes = [resourceNamePrefixes];
+ }
+ resourceNamePrefixes.forEach(ensureEndingSlash);
+ while ( resourceNamePrefixes.length < pathPrefixes.length ) {
+ resourceNamePrefixes.push('');
+ }
+
+ debug("path prefixes " + JSON.stringify(pathPrefixes));
+ debug("resource name prefixes " + JSON.stringify(resourceNamePrefixes));
+ },
+
+ /**
+ * Log each file before it is parsed
+ * @param {object} e Event info object
+ */
+ fileBegin: function (e) {
+ currentProgram = undefined;
+ currentModule = {
+ name: null,
+ resource: getResourceName(e.filename),
+ module: getModuleName(getResourceName(e.filename)),
+ localNames: Object.create(null)
+ };
+ },
+
+ fileComplete: function (e) {
+ // debug("module info after parsing: ", currentModule);
+ currentSource = undefined;
+ currentProgram = undefined;
+ currentModule = undefined;
+ },
+
+ jsdocCommentFound: function(e) {
+ // console.log("jsdocCommentFound: " + e.comment);
+ e.comment = preprocessComment(e);
+ },
+
+ symbolFound: function(e) {
+ // console.log("symbolFound: " + e.comment);
+ },
+
+ newDoclet: function(e) {
+
+ var _ui5data = ui5data(e.doclet);
+
+ // remove code: this is a try to reduce the required heap size
+ if ( e.doclet.meta ) {
+ if ( e.doclet.meta.code ) {
+ e.doclet.meta.code = {};
+ }
+ var filepath = (e.doclet.meta.path && e.doclet.meta.path !== 'null' ) ? path.join(e.doclet.meta.path, e.doclet.meta.filename) : e.doclet.meta.filename;
+ e.doclet.meta.__shortpath = getRelativePath(filepath);
+ _ui5data.resource = currentModule.resource;
+ _ui5data.module = currentModule.name || currentModule.module;
+ }
+
+
+ // JSDoc 3 has a bug when it encounters a property in an object literal with an empty string as name
+ // (e.g. { "" : something } will result in a doclet without longname
+ if ( !e.doclet.longname ) {
+ if ( e.doclet.memberof ) {
+ e.doclet.longname = e.doclet.memberof + "." + e.doclet.name; // TODO '.' depends on scope?
+ warning("found doclet without longname, derived longname: " + e.doclet.longname + " " + location(e.doclet));
+ } else {
+ error("found doclet without longname, could not derive longname " + location(e.doclet));
+ }
+ return;
+ }
+
+ // try to detect misused memberof
+ if ( e.doclet.memberof && e.doclet.longname.indexOf(e.doclet.memberof) !== 0 ) {
+ warning("potentially unsupported use of @name and @memberof " + location(e.doclet));
+ //console.log(e.doclet);
+ }
+
+ if ( e.doclet.returns
+ && e.doclet.returns.length > 0
+ && e.doclet.returns[0]
+ && e.doclet.returns[0].type
+ && e.doclet.returns[0].type.names
+ && e.doclet.returns[0].type.names[0] === 'this'
+ && e.doclet.memberof ) {
+ warning("fixing return type 'this' with " + e.doclet.memberof);
+ e.doclet.returns[0].type.names[0] = e.doclet.memberof;
+ }
+ },
+
+ beforeParse : function(e) {
+ msgHeader("parsing " + getRelativePath(e.filename));
+ currentSource = e.source;
+ },
+
+ parseComplete : function(e) {
+
+ var doclets = e.doclets;
+ var l = doclets.length,i,j,doclet;
+ //var noprivate = !env.opts.private;
+ var rAnonymous = /^(~|$)/;
+
+ // remove undocumented symbols, ignored symbols, anonymous functions and their members, scope members
+ for (i = 0, j = 0; i < l; i++) {
+
+ doclet = doclets[i];
+ if ( !doclet.undocumented &&
+ !doclet.ignore &&
+ !(doclet.memberof && rAnonymous.test(doclet.memberof)) &&
+ doclet.longname.indexOf("~") < 0 ) {
+ doclets[j++] = doclet;
+ }
+ }
+ if ( j < l ) {
+ doclets.splice(j, l - j);
+ info("removed " + (l - j) + " undocumented, ignored or anonymous symbols");
+ l = j;
+ }
+
+ // sort doclets by name, synthetic, lineno, uid
+ // 'ignore' is a combination of criteria, see function above
+ debug("sorting doclets by name");
+ doclets.sort(function(a,b) {
+ if ( a.longname === b.longname ) {
+ if ( a.synthetic === b.synthetic ) {
+ if ( a.meta && b.meta && a.meta.filename == b.meta.filename ) {
+ if ( a.meta.lineno !== b.meta.lineno ) {
+ return a.meta.lineno < b.meta.lineno ? -1 : 1;
+ }
+ }
+ return a.__ui5.id - b.__ui5.id;
+ }
+ return a.synthetic && !b.synthetic ? -1 : 1;
+ }
+ return a.longname < b.longname ? -1 : 1;
+ });
+ debug("sorting doclets by name done.");
+
+ for (i = 0, j = 0; i < l; i++) {
+
+ doclet = doclets[i];
+
+ // add metadata to symbol
+ if ( classInfos[doclet.longname] ) {
+ doclet.__ui5.metadata = classInfos[doclet.longname];
+
+ // add designtime infos, if configured
+ var designtimeModule = doclet.__ui5.metadata.designtime;
+ if ( designtimeModule && typeof designtimeModule !== 'string' ) {
+ designtimeModule = doclet.__ui5.module + ".designtime";
+ }
+ if ( designtimeModule && designtimeInfos[designtimeModule] ) {
+ info("associating designtime data with class metadata: ", designtimeModule);
+ // TODO do a more generic merge or maybe add whole information as "designtime" information
+ doclet.__ui5.metadata.annotations = designtimeInfos[designtimeModule].annotations;
+ }
+
+ // derive extends from UI5 APIs
+ if ( doclet.__ui5.metadata.baseType
+ && !(doclet.augments && doclet.augments.length > 0) ) {
+ doclet.augments = doclet.augments || [];
+ info(" @extends " + doclet.__ui5.metadata.baseType + " derived from UI5 APIs (" + doclet.longname + ")");
+ doclet.augments.push(doclet.__ui5.metadata.baseType);
+ }
+
+ // derive interface implementations from UI5 metadata
+ if ( doclet.__ui5.metadata.interfaces && doclet.__ui5.metadata.interfaces.length ) {
+ /* eslint-disable no-loop-func */
+ doclet.__ui5.metadata.interfaces.forEach(function(intf) {
+ doclet.implements = doclet.implements || [];
+ if ( doclet.implements.indexOf(intf) < 0 ) {
+ info(" @implements " + intf + " derived from UI5 metadata (" + doclet.longname + ")");
+ doclet.implements.push(intf);
+ }
+ });
+ /* eslint-enable no-loop-func */
+ }
+ }
+
+ if ( typeInfos[doclet.longname] ) {
+ doclet.__ui5.stereotype = 'datatype';
+ doclet.__ui5.metadata = {
+ basetype: typeInfos[doclet.longname].base,
+ pattern: typeInfos[doclet.longname].pattern,
+ range: typeInfos[doclet.longname].range
+ };
+ }
+
+ // check for duplicates: last one wins
+ if ( j > 0 && doclets[j - 1].longname === doclet.longname ) {
+ if ( !doclets[j - 1].synthetic && !doclet.__ui5.updatedDoclet ) {
+ // replacing synthetic comments or updating comments are trivial case. Just log non-trivial duplicates
+ debug("ignoring duplicate doclet for " + doclet.longname + ":" + location(doclet) + " overrides " + location(doclets[j - 1]));
+ }
+ doclets[j - 1] = doclet;
+ } else {
+ doclets[j++] = doclet;
+ }
+ }
+
+ if ( j < l ) {
+ doclets.splice(j, l - j);
+ info("removed " + (l - j) + " duplicate symbols - " + doclets.length + " remaining");
+ }
+
+ if ( pluginConfig.saveSymbols ) {
+
+ fs.mkPath(env.opts.destination);
+ fs.writeFileSync(path.join(env.opts.destination, "symbols-parseComplete.json"), JSON.stringify(e.doclets, null, "\t"), 'utf8');
+
+ }
+
+ }
+};
+
+exports.astNodeVisitor = {
+
+ visitNode: function(node, e, parser, currentSourceName) {
+
+ var comment;
+
+ if ( node.type === Syntax.Program ) {
+ currentProgram = node;
+ }
+
+ function processExtendCall(extendCall, comment, commentAlreadyProcessed) {
+ var doclet = comment && new Doclet(getRawComment(comment), {});
+ var classInfo = collectClassInfo(extendCall, doclet);
+ if ( classInfo ) {
+ createAutoDoc(classInfo, comment, extendCall, parser, currentSourceName, commentAlreadyProcessed);
+ }
+ }
+
+ function processDataType(createCall, comment) {
+ var doclet = comment && new Doclet(getRawComment(comment), {});
+ var typeInfo = collectDataTypeInfo(createCall, doclet);
+ if ( typeInfo ) {
+ createDataTypeAutoDoc(typeInfo, comment, createCall, parser, currentSourceName);
+ }
+ }
+
+ if ( node.type === Syntax.ExpressionStatement ) {
+ if ( isSapUiDefineCall(node.expression) ) {
+ analyzeModuleDefinition(node.expression);
+ /*
+ } else if ( isJQuerySapDeclareCall(node.expression)
+ && node.expression.arguments.length > 0
+ && node.expression.arguments[0].type === Syntax.Literal
+ && typeof node.expression.arguments[0].value === "string" ) {
+ warning("module has explicit module name " + node.expression.arguments[0].value);
+ */
+ }
+
+ }
+
+ if (node.type === Syntax.ReturnStatement && node.argument && node.argument.type === Syntax.ObjectExpression && /\.designtime\.js$/.test(currentSourceName) ) {
+
+ // assume this node to return designtime metadata. Collect it and remember it by its module name
+ var oDesigntimeInfo = collectDesigntimeInfo(node);
+ if ( oDesigntimeInfo ) {
+ designtimeInfos[currentModule.module] = oDesigntimeInfo;
+ info("collected designtime info " + currentModule.module);
+ }
+
+ } else if ( node.type === Syntax.ExpressionStatement && isExtendCall(node.expression) ) {
+
+ // Something.extend(...) -- return value (new class) is not used in an assignment
+
+ // className = node.expression.arguments[0].value;
+ comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.expression);
+ // console.log("ast node with comment " + comment);
+ processExtendCall(node.expression, comment);
+
+ } else if ( node.type === Syntax.VariableDeclaration ) {
+ node.declarations.forEach(function(decl, idx) {
+ if ( isExtendCall(decl.init) ) {
+ // var NewClass = Something.extend(...)
+
+ // className = node.declarations[0].init.arguments[0].value;
+ comment = (idx === 0 ? getLeadingCommentNode(node) : undefined) || getLeadingCommentNode(decl);
+ // console.log("ast node with comment " + comment);
+ processExtendCall(decl.init, comment);
+ }
+ });
+
+ } else if ( node.type === Syntax.ReturnStatement && isExtendCall(node.argument) ) {
+
+ // return Something.extend(...)
+
+ var className = node.argument.arguments[0].value;
+ comment = getLeadingCommentNode(node, className) || getLeadingCommentNode(node.argument, className);
+ // console.log("ast node with comment " + comment);
+ processExtendCall(node.argument, comment, true);
+ } else if ( node.type === Syntax.ExpressionStatement && node.expression.type === Syntax.AssignmentExpression ) {
+
+ if ( isCreateDataTypeCall(node.expression.right) ) {
+
+ // thisLib.TypeName = DataType.createType( ... )
+ comment = getLeadingCommentNode(node) || getLeadingCommentNode(node.expression);
+ processDataType(node.expression.right);
+ // TODO remember knowledge about type and its name (left hand side of assignment)
+
+ }
+
+ }
+ }
+
+};
diff --git a/lib/processors/jsdoc/lib/ui5/template/publish.js b/lib/processors/jsdoc/lib/ui5/template/publish.js
new file mode 100644
index 000000000..4ea98a10c
--- /dev/null
+++ b/lib/processors/jsdoc/lib/ui5/template/publish.js
@@ -0,0 +1,4129 @@
+/*
+ * JSDoc3 template for UI5 documentation generation.
+ *
+ * (c) Copyright 2009-2019 SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
+ */
+
+/*global env: true, require, exports */
+/*eslint strict: [2, "global"]*/
+
+"use strict";
+
+/* imports */
+var template = require('jsdoc/template'),
+ helper = require('jsdoc/util/templateHelper'),
+ fs = require('jsdoc/fs'),
+ doclet = require('jsdoc/doclet'),
+ path = require('jsdoc/path');
+
+/* globals, constants */
+var MY_TEMPLATE_NAME = "ui5",
+ ANONYMOUS_LONGNAME = doclet.ANONYMOUS_LONGNAME,
+ A_SECURITY_TAGS = [
+ {
+ name : "SecSource",
+ caption : "Taint Source",
+ description : "APIs that might introduce tainted data into an application, e.g. due to user input or network access",
+ params : ["out","flags"]
+ },
+ {
+ name : "SecEntryPoint",
+ caption : "Taint Entry Point",
+ description: "APIs that are called implicitly by a framework or server and trigger execution of application logic",
+ params : ["in","flags"]
+ },
+ {
+ name : "SecSink",
+ caption : "Taint Sink",
+ description : "APIs that pose a security risk when they receive tainted data",
+ params : ["in","flags"]
+ },
+ {
+ name : "SecPassthrough",
+ caption : "Taint Passthrough",
+ description : "APIs that might propagate tainted data when they receive it as input",
+ params : ["in","out","flags"]
+ },
+ {
+ name : "SecValidate",
+ caption : "Validation",
+ description : "APIs that (partially) cleanse tainted data so that it no longer poses a security risk in the further data flow of an application",
+ params : ["in","out","flags"]
+ }
+ ];
+
+var rSecurityTags = new RegExp(A_SECURITY_TAGS.map(function($) {return $.name.toLowerCase(); }).join('|'), "i");
+ //debug(A_SECURITY_TAGS.map(function($) {return $.name; }).join('|'));
+
+var templateConf = (env.conf.templates || {})[MY_TEMPLATE_NAME] || {},
+ pluginConf = templateConf,
+ conf = {},
+ view;
+
+var __db;
+var __longnames;
+var __missingLongnames = {};
+
+/**
+ * Maps the symbol 'longname's to the unique filename that contains the documentation of that symbol.
+ * This map is maintained to deal with names that only differ in case (e.g. the namespace sap.ui.model.type and the class sap.ui.model.Type).
+ */
+var __uniqueFilenames = {};
+
+/* eslint-disable no-console */
+function info() {
+ if ( env.opts.verbose || env.opts.debug ) {
+ console.log.apply(console, arguments);
+ }
+}
+
+function warning(msg) {
+ var args = Array.prototype.slice.apply(arguments);
+ args[0] = "**** warning: " + args[0];
+ console.log.apply(console, args);
+}
+
+function error(msg) {
+ var args = Array.prototype.slice.apply(arguments);
+ args[0] = "**** error: " + args[0];
+ console.log.apply(console, args);
+}
+
+function debug() {
+ if ( env.opts.debug ) {
+ console.log.apply(console, arguments);
+ }
+}
+/* eslint-disable no-console */
+
+function compare(v1, v2) {
+ if ( v1 !== v2 ) {
+ return v1 < v2 ? -1 : 1;
+ }
+ return 0;
+}
+
+function merge(target, source) {
+ if ( source != null ) {
+ // simple single source merge
+ Object.keys(source).forEach(function(prop) {
+ var value = source[prop];
+ if ( value != null && value.constructor === Object ) {
+ merge(target[prop] || {}, value);
+ } else {
+ target[prop] = value;
+ }
+ });
+ }
+ // if there are more sources, merge them, too
+ for (var i = 2; i < arguments.length; i++) {
+ merge(target, arguments[i]);
+ }
+ return target;
+}
+
+function lookup(longname /*, variant*/) {
+ var key = longname; // variant ? longname + "|" + variant : longname;
+ if ( !Object.prototype.hasOwnProperty.call(__longnames, key) ) {
+ __missingLongnames[key] = (__missingLongnames[key] || 0) + 1;
+ var oResult = __db({longname: longname /*, variant: variant ? variant : {isUndefined: true}*/});
+ __longnames[key] = oResult.first();
+ }
+ return __longnames[key];
+}
+
+var externalSymbols = {};
+
+function loadExternalSymbols(apiJsonFolder) {
+
+ var files;
+
+ try {
+ files = fs.readdirSync(templateConf.apiJsonFolder);
+ } catch (e) {
+ error("failed to list symbol files in folder '" + apiJsonFolder + "': " + (e.message || e));
+ return;
+ }
+
+ if ( files && files.length ) {
+ files.forEach(function(localFileName) {
+ var file = path.join(templateConf.apiJsonFolder, localFileName);
+ try {
+ var sJSON = fs.readFileSync(file, 'UTF-8');
+ var data = JSON.parse(sJSON);
+ if ( !Array.isArray(data.symbols) ) {
+ throw new TypeError("api.json does not contain a 'symbols' array");
+ }
+ data.symbols.forEach(function(symbol) {
+ debug(" adding external symbol " + symbol.name);
+ externalSymbols[symbol.name] = symbol;
+ });
+ } catch (e) {
+ error("failed to load symbols from " + file + ": " + (e.message || e));
+ }
+ });
+ }
+}
+
+function isModuleExport($) {
+ return $.longname.startsWith("module:") && $.longname.search(/[.#~]/) < 0;
+}
+
+function isaClass($) {
+ return /^(namespace|interface|class|typedef)$/.test($.kind) || ($.kind === 'member' && $.isEnum )/* isNonEmptyNamespace($) */;
+}
+
+function supportsInheritance($) {
+ return /^(interface|class|typedef)$/.test($.kind);
+}
+
+/*
+ * Returns true for any symbol that should appear in the API reference index of the SDK.
+ *
+ * In a perfect world, such symbols would be
+ * - default exports of AMD modules (named 'module:some/module)
+ * - classes, interfaces, enums, typedefs and namespaces, all with global names whose parents are all namespaces
+ * In the less perfect documentation build, the criterion 'whose parents are all namespaces' is ignored
+ */
+function isFirstClassSymbol($) {
+ return /^(namespace|interface|class|typedef)$/.test($.kind) || ($.kind === 'member' && $.isEnum || isModuleExport($) )/* isNonEmptyNamespace($) */;
+}
+
+
+var REGEXP_ARRAY_TYPE = /^Array\.<(.*)>$/;
+
+// ---- Version class -----------------------------------------------------------------------------------------------------------------------------------------------------------
+
+var Version = (function() {
+
+ var rVersion = /^[0-9]+(?:\.([0-9]+)(?:\.([0-9]+))?)?(.*)$/;
+
+ /**
+ * Creates a Version object from the given version string.
+ *
+ * @param {string} versionStr A dot-separated version string
+ *
+ * @classdesc Represents a version consisting of major, minor, patch version and suffix,
+ * e.g. '1.2.7-SNAPSHOT'. All parts after the major version are optional.
+ * @class
+ */
+ function Version(versionStr) {
+
+ var match = rVersion.exec(versionStr) || [];
+
+ function norm(v) {
+ v = parseInt(v);
+ return isNaN(v) ? 0 : v;
+ }
+
+ Object.defineProperty(this, "major", {
+ enumerable: true,
+ value: norm(match[0])
+ });
+ Object.defineProperty(this, "minor", {
+ enumerable: true,
+ value: norm(match[1])
+ });
+ Object.defineProperty(this, "patch", {
+ enumerable: true,
+ value: norm(match[2])
+ });
+ Object.defineProperty(this, "suffix", {
+ enumerable: true,
+ value: String(match[3] || "")
+ });
+
+ }
+
+ Version.prototype.toMajorMinor = function() {
+ return new Version(this.major + "." + this.minor);
+ };
+
+ Version.prototype.toString = function() {
+ return this.major + "." + this.minor + "." + this.patch + this.suffix;
+ };
+
+ Version.prototype.compareTo = function(other) {
+ return this.major - other.major ||
+ this.minor - other.minor ||
+ this.patch - other.patch ||
+ compare(this.suffix, other.suffix);
+ };
+
+ return Version;
+
+}());
+
+// ---- Link class --------------------------------------------------------------------------------------------------------------------------------------------------------------
+
+//TODO move to separate module
+
+var Link = (function() {
+
+ var Link = function() {
+ };
+
+ Link.prototype.toSymbol = function(longname) {
+ if ( longname != null ) {
+ longname = String(longname);
+ if ( /#constructor$/.test(longname) ) {
+ if ( !this.innerName ) {
+ this.innerName = 'constructor';
+ }
+ longname = longname.slice(0, -"#constructor".length);
+ }
+ this.longname = longname;
+ }
+ return this;
+ };
+
+ Link.prototype.withText = function(text) {
+ this.text = text;
+ return this;
+ };
+
+ Link.prototype.withTooltip = function(text) {
+ this.tooltip = text;
+ return this;
+ };
+
+ Link.prototype.toFile = function(file) {
+ if ( file != null ) {
+ this.file = file;
+ }
+ return this;
+ };
+
+ function _makeLink(href, target, tooltip, text) {
+ return '' + text + '';
+ }
+
+ Link.prototype.toString = function() {
+ var longname = this.longname,
+ linkString;
+
+ if (longname) {
+
+ if ( /^(?:(?:ftp|https?):\/\/|\.\.?\/)/.test(longname) ) {
+ // handle real hyperlinks (TODO should be handled with a different "to" method
+ linkString = _makeLink(longname, this.targetName, this.tooltip, this.text || longname);
+ } else if ( /^topic:/.test(longname) ) {
+ // handle documentation links
+ longname = conf.topicUrlPattern.replace("{{topic}}", longname.slice("topic:".length));
+ linkString = _makeLink(longname, this.targetName, this.tooltip, this.text || longname);
+ } else {
+ linkString = this._makeSymbolLink(longname);
+ }
+
+ } else if (this.file) {
+ linkString = _makeLink(Link.base + this.file, this.targetName, null, this.text || this.file);
+ }
+
+ return linkString;
+ };
+
+ var missingTypes = {};
+ Link.getMissingTypes = function() {
+ return Object.keys(missingTypes);
+ };
+
+ Link.prototype._makeSymbolLink = function(longname) {
+
+ // normalize .prototype. and #
+ longname = longname.replace(/\.prototype\./g, '#');
+
+ // if it is an internal reference, then don't validate against symbols, just create a link
+ if ( longname.charAt(0) == "#" ) {
+
+ return _makeLink(longname + (this.innerName ? "#" + this.innerName : ""), this.targetName, this.tooltip, this.text || longname.slice(1));
+
+ }
+
+ var linkTo = lookup(longname);
+ // if there is no symbol by that name just return the name unaltered
+ if ( !linkTo ) {
+
+ missingTypes[longname] = true;
+
+ return this.text || longname;
+
+ }
+
+ // it's a full symbol reference (potentially to another file)
+ var mainSymbol, anchor;
+ if ( (linkTo.kind === 'member' && !linkTo.isEnum) || linkTo.kind === 'constant' || linkTo.kind === 'function' || linkTo.kind === 'event' ) { // it's a method or property
+
+ mainSymbol = linkTo.memberof;
+ anchor = ( linkTo.kind === 'event' ? "event:" : "") + Link.symbolNameToLinkName(linkTo);
+
+ } else {
+
+ mainSymbol = linkTo.longname;
+ anchor = this.innerName;
+
+ }
+
+ return _makeLink(Link.baseSymbols + __uniqueFilenames[mainSymbol] + conf.ext + (anchor ? "#" + anchor : ""), this.targetName, this.tooltip, this.text || longname);
+ };
+
+ Link.symbolNameToLinkName = function(symbol) {
+ var linker = "";
+ if ( symbol.scope === 'static' ) {
+ linker = ".";
+ } else if (symbol.isInner) {
+ linker = "-"; // TODO-migrate?
+ }
+ return linker + symbol.name;
+ };
+
+ return Link;
+
+}());
+
+
+
+// ---- publish() - main entry point for JSDoc templates -------------------------------------------------------------------------------------------------------
+
+/* Called automatically by JsDoc Toolkit. */
+function publish(symbolSet) {
+
+ info("entering sapui5 template");
+
+ // create output dir
+ fs.mkPath(env.opts.destination);
+
+// if ( symbolSet().count() < 20000 ) {
+// info("writing raw symbols to " + path.join(env.opts.destination, "symbols-unpruned-ui5.json"));
+// fs.writeFileSync(path.join(env.opts.destination, "symbols-unpruned-ui5.json"), JSON.stringify(symbolSet().get(), filter, "\t"), 'utf8');
+// }
+
+ info("before prune: " + symbolSet().count() + " symbols.");
+ symbolSet = helper.prune(symbolSet);
+ info("after prune: " + symbolSet().count() + " symbols.");
+
+ __db = symbolSet;
+ __longnames = {};
+ __db().each(function($) {
+ __longnames[$.longname] = $;
+ });
+
+ if ( templateConf.apiJsonFolder ) {
+ info("loading external apis from folder '" + templateConf.apiJsonFolder + "'");
+ loadExternalSymbols(templateConf.apiJsonFolder);
+ }
+
+ var templatePath = path.join(env.opts.template, 'tmpl/');
+ info("using templates from '" + templatePath + "'");
+ view = new template.Template(templatePath);
+
+ function filter(key,value) {
+ if ( key === 'meta' ) {
+ //return;
+ }
+ if ( key === '__ui5' && value ) {
+ var v = {
+ resource: value.resource,
+ module: value.module,
+ stakeholders: value.stakeholders
+ };
+ if ( value.derived ) {
+ v.derived = value.derived.map(function($) { return $.longname; });
+ }
+ if ( value.base ) {
+ v.base = value.base.longname;
+ }
+ if ( value.implementations ) {
+ v.base = value.implementations.map(function($) { return $.longname; });
+ }
+ if ( value.parent ) {
+ v.parent = value.parent.longname;
+ }
+ if ( value.children ) {
+ v.children = value.children.map(function($) { return $.longname; });
+ }
+ return v;
+ }
+ return value;
+ }
+
+ // now resolve relationships
+ var aRootNamespaces = createNamespaceTree();
+ var hierarchyRoots = createInheritanceTree();
+ collectMembers();
+ mergeEventDocumentation();
+
+ if ( symbolSet().count() < 20000 ) {
+ info("writing raw symbols to " + path.join(env.opts.destination, "symbols-pruned-ui5.json"));
+ fs.writeFileSync(path.join(env.opts.destination, "symbols-pruned-ui5.json"), JSON.stringify(symbolSet().get(), filter, "\t"), 'utf8');
+ }
+
+ // used to allow Link to check the details of things being linked to
+ Link.symbolSet = symbolSet;
+
+ // get an array version of the symbol set, useful for filtering
+ var symbols = symbolSet().get();
+
+ // -----
+
+ var PUBLISHING_VARIANTS = {
+
+ "apixml" : {
+ defaults : {
+ apiXmlFile: path.join(env.opts.destination, "jsapi.xml")
+ },
+ processor : function(conf) {
+ createAPIXML(symbols, conf.apiXmlFile, {
+ legacyContent: true
+ });
+ }
+ },
+
+ "apijson" : {
+ defaults : {
+ apiJsonFile: path.join(env.opts.destination, "api.json")
+ },
+ processor : function(conf) {
+ createAPIJSON(symbols, conf.apiJsonFile);
+ }
+ },
+
+ "fullapixml" : {
+ defaults : {
+ fullXmlFile: path.join(env.opts.destination, "fulljsapi.xml")
+ },
+ processor : function(conf) {
+ createAPIXML(symbols, conf.fullXmlFile, {
+ roots: aRootNamespaces,
+ omitDefaults : conf.omitDefaultsInFullXml,
+ resolveInheritance: true
+ });
+ }
+ },
+
+ "apijs" : {
+ defaults: {
+ jsapiFile: path.join(env.opts.destination, "api.js")
+ },
+ processor: function(conf) {
+ createAPIJS(symbols, conf.jsapiFile);
+ }
+ },
+
+ "full" : {
+ defaults : {
+ outdir: path.join(env.opts.destination, "full/"),
+ contentOnly: false,
+ hierarchyIndex: true
+ },
+ processor: function() {
+ publishClasses(symbolSet, aRootNamespaces, hierarchyRoots);
+ }
+ },
+
+ "public" : {
+ defaults: {
+ outdir: path.join(env.opts.destination, "public/"),
+ filter: function($) { return $.access === 'public' || $.access === 'protected' || $.access == null; },
+ contentOnly: false,
+ hierarchyIndex: true
+ },
+ processor: function(conf) {
+ publishClasses(symbolSet, aRootNamespaces, hierarchyRoots);
+ }
+ },
+
+ "demokit" : {
+ defaults: {
+ outdir: path.join(env.opts.destination, "demokit/"),
+ filter: function($) { return $.access === 'public' || $.access === 'protected' || $.access == null; },
+ contentOnly: true,
+ modulePages: true,
+ hierarchyIndex: false,
+ securityIndex: true,
+ sinceIndex: true,
+ deprecationIndex: true,
+ experimentalIndex: true,
+ suppressAuthor: true,
+ suppressVersion: true
+ },
+ processor: function(conf) {
+ publishClasses(symbolSet, aRootNamespaces, hierarchyRoots);
+ }
+ },
+
+ "demokit-internal" : {
+ defaults: {
+ outdir: path.join(env.opts.destination, "demokit-internal/"),
+ // filter: function($) { return $.access === 'public' || $.access === 'protected' || $.access === 'restricted' || $.access == null; },
+ contentOnly: true,
+ modulePages: true,
+ hierarchyIndex: false,
+ securityIndex: true,
+ sinceIndex: true,
+ deprecationIndex: true,
+ experimentalIndex: true,
+ suppressAuthor: true,
+ suppressVersion: true
+ },
+ processor: function(conf) {
+ publishClasses(symbolSet, aRootNamespaces, hierarchyRoots);
+ }
+ }
+
+ };
+
+ var now = new Date();
+
+ info("start publishing");
+ for (var i = 0; i < templateConf.variants.length; i++) {
+
+ var vVariant = templateConf.variants[i];
+ if ( typeof vVariant === "string" ) {
+ vVariant = { variant : vVariant };
+ }
+
+ info("");
+
+ if ( PUBLISHING_VARIANTS[vVariant.variant] ) {
+
+ // Merge different sources of configuration (listed in increasing priority order - last one wins)
+ // and expose the result in the global 'conf' variable
+ // - global defaults
+ // - defaults for current variant
+ // - user configuration for sapui5 template
+ // - user configuration for current variant
+ //
+ // Note: trailing slash expected for dirs
+ conf = merge({
+ ext: ".html",
+ filter: function($) { return true; },
+ templatesDir: "/templates/sapui5/",
+ symbolsDir: "symbols/",
+ modulesDir: "modules/",
+ topicUrlPattern: "../../guide/{{topic}}.html",
+ srcDir: "symbols/src/",
+ creationDate : now.getFullYear() + "-" + (now.getMonth() + 1) + "-" + now.getDay() + " " + now.getHours() + ":" + now.getMinutes(),
+ outdir: env.opts.destination
+ }, PUBLISHING_VARIANTS[vVariant.variant].defaults, templateConf, vVariant);
+
+ info("publishing as variant '" + vVariant.variant + "'");
+ debug("final configuration:");
+ debug(conf);
+
+ PUBLISHING_VARIANTS[vVariant.variant].processor(conf);
+
+ info("done with variant " + vVariant.variant);
+
+ } else {
+
+ info("cannot publish unknown variant '" + vVariant.variant + "' (ignored)");
+
+ }
+ }
+
+ var builtinSymbols = templateConf.builtinSymbols;
+ if ( builtinSymbols ) {
+ Link.getMissingTypes().filter(function($) {
+ return builtinSymbols.indexOf($) < 0;
+ }).sort().forEach(function($) {
+ // TODO instead of filtering topic: and fiori: links out here, they should be correctly linked in the template
+ if ( !/\{@link (?:topic:|fiori:)/.test($) ) {
+ error(" unresolved reference: " + $);
+ }
+ });
+ }
+ info("publishing done.");
+
+}
+
+//---- namespace tree --------------------------------------------------------------------------------
+
+/*
+ * Completes the tree of namespaces. Namespaces for which content is available
+ * but which have not been documented are created as dummy without documentation.
+ */
+function createNamespaceTree() {
+
+ info("create namespace tree (" + __db().count() + " symbols)");
+
+ var aRootNamespaces = [];
+ var aTypes = __db(function() { return isFirstClassSymbol(this); }).get();
+
+ for (var i = 0; i < aTypes.length; i++) { // loop with a for-loop as it can handle concurrent modifications
+
+ var symbol = aTypes[i];
+ if ( symbol.memberof ) {
+
+ var parent = lookup(symbol.memberof);
+ if ( !parent ) {
+ warning("create missing namespace '" + symbol.memberof + "' (referenced by " + symbol.longname + ")");
+ parent = makeNamespace(symbol.memberof);
+ __longnames[symbol.memberof] = parent;
+ __db.insert(parent);
+ aTypes.push(parent); // concurrent modification: parent will be processed later in this loop
+ }
+ symbol.__ui5.parent = parent;
+ parent.__ui5.children = parent.__ui5.children || [];
+ parent.__ui5.children.push(symbol);
+
+ } else if ( symbol.longname !== ANONYMOUS_LONGNAME ) {
+
+ aRootNamespaces.push(symbol);
+
+ }
+ }
+
+ return aRootNamespaces;
+}
+
+function makeNamespace(memberof) {
+
+ info("adding synthetic namespace symbol " + memberof);
+
+ var comment = [
+ "@name " + memberof,
+ "@namespace",
+ "@synthetic",
+ "@public"
+ ];
+
+ var symbol = new doclet.Doclet("/**\n * " + comment.join("\n * ") + "\n */", {});
+ symbol.__ui5 = {};
+
+ return symbol;
+}
+
+//---- inheritance hierarchy ----------------------------------------------------------------------------
+
+/*
+ * Calculates the inheritance hierarchy for all class/interface/namespace symbols.
+ * Each node in the tree has the content
+ *
+ * Node : {
+ * longname : {string} // name of the node (usually equals symbol.longname)
+ * symbol : {Symbol} // backlink to the original symbol
+ * base : {Node} // parent node or undefined for root nodes
+ * derived : {Node[]} // subclasses/-types
+ * }
+ *
+ */
+function createInheritanceTree() {
+
+ function makeDoclet(longname, lines) {
+ lines.push("@name " + longname);
+ var newDoclet = new doclet.Doclet("/**\n * " + lines.join("\n * ") + "\n */", {});
+ newDoclet.__ui5 = {};
+ __longnames[longname] = newDoclet;
+ __db.insert(newDoclet);
+ return newDoclet;
+ }
+
+ info("create inheritance tree (" + __db().count() + " symbols)");
+
+ var oTypes = __db(function() { return supportsInheritance(this); });
+ var aRootTypes = [];
+
+ var oObject = lookup("Object");
+ if ( !oObject ) {
+ oObject = makeDoclet("Object", [
+ "@class",
+ "@synthetic",
+ "@public"
+ ]);
+ aRootTypes.push(oObject);
+ }
+
+ function getOrCreateClass(sClass, sExtendingClass) {
+ var oClass = lookup(sClass);
+ if ( !oClass ) {
+ warning("create missing class " + sClass + " (extended by " + sExtendingClass + ")");
+ var sBaseClass = 'Object';
+ if ( externalSymbols[sClass] ) {
+ sBaseClass = externalSymbols[sClass].extends || sBaseClass;
+ }
+ var oBaseClass = getOrCreateClass(sBaseClass, sClass);
+ oClass = makeDoclet(sClass, [
+ "@extends " + sBaseClass,
+ "@class",
+ "@synthetic",
+ "@public"
+ ]);
+ oClass.__ui5.base = oBaseClass;
+ oBaseClass.__ui5.derived = oBaseClass.__ui5.derived || [];
+ oBaseClass.__ui5.derived.push(oClass);
+ }
+ return oClass;
+ }
+
+ // link them according to the inheritance infos
+ oTypes.each(function(oClass) {
+
+ if ( oClass.longname === 'Object') {
+ return;
+ }
+
+ var sBaseClass = "Object";
+ if ( oClass.augments && oClass.augments.length > 0 ) {
+ if ( oClass.augments.length > 1 ) {
+ warning("multiple inheritance detected in " + oClass.longname);
+ }
+ sBaseClass = oClass.augments[0];
+ } else {
+ aRootTypes.push(oClass);
+ }
+
+ var oBaseClass = getOrCreateClass(sBaseClass, oClass.longname);
+ oClass.__ui5.base = oBaseClass;
+ oBaseClass.__ui5.derived = oBaseClass.__ui5.derived || [];
+ oBaseClass.__ui5.derived.push(oClass);
+
+ if ( oClass.implements ) {
+ for (var j = 0; j < oClass.implements.length; j++) {
+ var oInterface = lookup(oClass.implements[j]);
+ if ( !oInterface ) {
+ warning("create missing interface " + oClass.implements[j]);
+ oInterface = makeDoclet(oClass.implements[j], [
+ "@extends Object",
+ "@interface",
+ "@synthetic",
+ "@public"
+ ]);
+ oInterface.__ui5.base = oObject;
+ oObject.__ui5.derived = oObject.__ui5.derived || [];
+ oObject.__ui5.derived.push(oInterface);
+ }
+ oInterface.__ui5.implementations = oInterface.__ui5.implementations || [];
+ oInterface.__ui5.implementations.push(oClass);
+ }
+ }
+ });
+
+ function setStereotype(oSymbol, sStereotype) {
+ if ( !oSymbol ) {
+ return;
+ }
+ oSymbol.__ui5.stereotype = sStereotype;
+ var derived = oSymbol.__ui5.derived;
+ if ( derived ) {
+ for (var i = 0; i < derived.length; i++ ) {
+ if ( !derived[i].__ui5.stereotype ) {
+ setStereotype(derived[i], sStereotype);
+ }
+ }
+ }
+ }
+
+ setStereotype(lookup("sap.ui.core.Component"), "component");
+ setStereotype(lookup("sap.ui.core.Control"), "control");
+ setStereotype(lookup("sap.ui.core.Element"), "element");
+ setStereotype(lookup("sap.ui.base.Object"), "object");
+
+ // check for cyclic inheritance (not supported)
+ // Note: the check needs to run bottom up, not top down as a typical cyclic dependency never will end at the root node
+ oTypes.each(function(oStartClass) {
+ var visited = {};
+ function visit(oClass) {
+ if ( visited[oClass.longname] ) {
+ throw new Error("cyclic inheritance detected: " + JSON.stringify(Object.keys(visited)));
+ }
+ if ( oClass.__ui5.base ) {
+ visited[oClass.longname] = true;
+ visit(oClass.__ui5.base);
+ delete visited[oClass.longname];
+ }
+ }
+ visit(oStartClass);
+ });
+
+ // collect root nodes (and ignore pure packages)
+ return aRootTypes;
+ /*
+ return __db(function() {
+ return R_KINDS.test(this.kind) && this.__ui5 && this.__ui5.base == null;
+ }).get();
+ */
+}
+
+function collectMembers() {
+ __db().each(function($) {
+ if ( $.memberof ) {
+ var parent = lookup($.memberof);
+ if ( parent /* && supportsInheritance(parent) */ ) {
+ parent.__ui5.members = parent.__ui5.members || [];
+ parent.__ui5.members.push($);
+ }
+ }
+ });
+}
+
+function mergeEventDocumentation() {
+
+ debug("merging JSDoc event documentation into UI5 metadata");
+
+ var oTypes = __db(function() { return isaClass(this); });
+
+ oTypes.each(function(symbol) {
+
+ var metadata = symbol.__ui5.metadata;
+ var members = symbol.__ui5.members;
+
+ if ( !metadata || !metadata.events || Object.keys(metadata.events).length <= 0 || !members ) {
+ return;
+ }
+
+ // debug('merging events for ' + symbol.longname);
+ members.forEach(function($) {
+ if ( $.kind === 'event' && !$.inherited
+ && ($.access === 'public' || $.access === 'protected' || $.access == null)
+ && metadata.events[$.name]
+ && Array.isArray($.params)
+ && !$.synthetic ) {
+
+ var event = metadata.events[$.name];
+ var modified = false;
+
+ $.params.forEach(function(param) {
+ var m = /^\w+\.getParameters\.(.*)$/.exec(param.name);
+ if ( m ) {
+ var pname = m[1];
+ var ui5param = event.parameters[pname] || ( event.parameters[pname] = {});
+ if ( ui5param.type == null ) {
+ ui5param.type = listTypes(param.type);
+ modified = true;
+ }
+ if ( ui5param.doc == null ) {
+ ui5param.doc = param.description;
+ modified = true;
+ }
+ }
+ });
+
+ if ( modified ) {
+ info(" merged documentation for managed event " + symbol.longname + "#" + $.name);
+ }
+
+ }
+ });
+
+ });
+
+}
+
+// ---- publishing -----------------------------------------------------------------------
+
+function publishClasses(symbols, aRootNamespaces, hierarchyRoots) {
+
+ // create output dir
+ fs.mkPath(path.join(conf.outdir, conf.symbolsDir));
+
+ // get a list of all the first class symbols in the symbolset
+ var firstClassSymbols = symbols(function() {
+ return supportsInheritance(this) && conf.filter(this);
+ }).order("longname");
+
+ // create unique file names
+ __uniqueFilenames = {};
+ var filenames = {};
+ firstClassSymbols.get().sort(sortByAlias).forEach(function(symbol) {
+ var filename = escape(symbol.longname.replace(/^module:/, "")).replace(/\//g, "%25");
+ if ( filenames.hasOwnProperty(filename.toUpperCase()) && (filenames[filename.toUpperCase()].longname !== symbol.longname) ) {
+ // find an unused filename by appending "-n" where n is an integer > 0
+ var j = 1;
+ while (filenames.hasOwnProperty(filename.toUpperCase() + "-" + j)) {
+ j++;
+ }
+ warning("duplicate symbol names " + filenames[filename.toUpperCase()].longname + " and " + symbol.longname + ", renaming the latter to " + filename + "-" + j);
+ filename = filename + "-" + j;
+ }
+ filenames[filename.toUpperCase()] = symbol;
+ __uniqueFilenames[symbol.longname] = filename;
+ });
+ filenames = null;
+
+ // create a class index, displayed in the left-hand column of every class page
+ var classTemplate;
+ if ( !conf.contentOnly ) {
+ info("create embedded class index");
+ Link.base = "../";
+ Link.baseSymbols = "";
+ classTemplate = 'classWithIndex.html.tmpl';
+ publish.header = processTemplate("_header.tmpl", firstClassSymbols);
+ publish.footer = processTemplate("_footer.tmpl", firstClassSymbols);
+ publish.classesIndex = processTemplate("_navIndex.tmpl", firstClassSymbols); // kept in memory
+ } else {
+ var newStyle = !!pluginConf.newStyle;
+ classTemplate = newStyle ? "class-new.html.tmpl" : "class.html.tmpl";
+ publish.header = '';
+ publish.footer = '';
+ publish.classesIndex = '';
+
+ // instead create an index as XML
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ processTemplateAndSave("index.xml.tmpl", aRootNamespaces, "index.xml");
+ }
+
+ // create each of the class pages
+ info("create class/namespace pages");
+ Link.base = "../";
+ Link.baseSymbols = "";
+ firstClassSymbols.each(function(symbol) {
+ var sOutName = path.join(conf.symbolsDir, __uniqueFilenames[symbol.longname]) + conf.ext;
+ processTemplateAndSave(classTemplate, symbol, sOutName);
+ });
+
+ if ( conf.modulePages ) {
+ info("create module pages");
+ Link.base = "../";
+ Link.baseSymbols = "../" + conf.symbolsDir;
+ fs.mkPath(path.join(conf.outdir, conf.modulesDir));
+ groupByModule(firstClassSymbols.get()).forEach(function(module) {
+ var sOutName = path.join(conf.modulesDir, module.name.replace(/\//g, '_')) + conf.ext;
+ processTemplateAndSave("module.html.tmpl", module, sOutName);
+ });
+ }
+
+ // regenerate the index with a different link base, used in the overview pages
+ info("create global class/namespace index");
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ publish.header = processTemplate("_header.tmpl", firstClassSymbols);
+ publish.footer = processTemplate("_footer.tmpl", firstClassSymbols);
+ publish.classesIndex = processTemplate("_navIndex.tmpl", firstClassSymbols);
+
+ // create the all classes index
+ processTemplateAndSave("index.html.tmpl", firstClassSymbols, "index" + conf.ext);
+
+ // create the class hierarchy page
+ if ( conf.hierarchyIndex ) {
+ info("create class hierarchy index");
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ processTemplateAndSave("hierarchy.html.tmpl", hierarchyRoots.filter(conf.filter), "hierarchy" + conf.ext);
+ }
+
+ if ( conf.sinceIndex ) {
+ info("create API by version index");
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ var sinceSymbols = symbols(function() {
+ var r = !!this.since && !this.inherited && conf.filter(this);
+ if ( r && this.memberof ) {
+ var parent = lookup(this.memberof);
+ // filter out symbol when parent is filtered out
+ if ( !parent || !conf.filter(parent) ) {
+ debug("since index: filtering out " + this.longname + ", member of " + this.memberof);
+ r = false;
+ }
+ if ( parent && parent.since === this.since ) {
+ // r = false;
+ }
+ }
+ return r;
+ }).order("longname");
+ processTemplateAndSave("since.html.tmpl", sinceSymbols, "since" + conf.ext);
+ }
+
+ if ( conf.deprecationIndex ) {
+ info("create deprecated API index");
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ var deprecatedSymbols = symbols(function() {
+ return !!this.deprecated && !this.inherited && conf.filter(this);
+ }).order("longname");
+ processTemplateAndSave("deprecation.html.tmpl", deprecatedSymbols, "deprecation" + conf.ext);
+ }
+
+ if ( conf.experimentalIndex ) {
+ info("create experimental API index");
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ var experimentalSymbols = symbols(function() {
+ return !!this.experimental && !this.inherited && conf.filter(this);
+ }).order("longname");
+ processTemplateAndSave("experimental.html.tmpl", experimentalSymbols, "experimental" + conf.ext);
+ }
+
+ if ( conf.securityIndex ) {
+ info("create Security Relevant API index");
+
+ var securityRelevantSymbols = {};
+ A_SECURITY_TAGS.forEach(function(oTagDef) {
+ securityRelevantSymbols[oTagDef.name.toLowerCase()] = { tag : oTagDef, symbols: [] };
+ });
+ symbols().each(function($) {
+ var tags = $.tags;
+ if ( !$.inherited && conf.filter($) && tags ) {
+ for (var i = 0; i < tags.length; i++) {
+ if ( rSecurityTags.test(tags[i].title) ) {
+ securityRelevantSymbols[tags[i].title.toLowerCase()].symbols.push({ symbol: $, tag : tags[i]});
+ }
+ }
+ }
+ });
+
+ Link.base = "";
+ Link.baseSymbols = conf.symbolsDir;
+ processTemplateAndSave("security.html.tmpl", securityRelevantSymbols, "security" + conf.ext);
+ }
+
+ firstClassSymbols = null;
+
+ // copy needed mimes
+ info("copy mimes");
+ // copy the template's static files to outdir
+ var templatePath = env.opts.template;
+ var fromDir = path.join(templatePath, 'static');
+ var staticFiles = fs.ls(fromDir, 3);
+ staticFiles.forEach(function(fileName) {
+ var toDir = fs.toDir( fileName.replace(fromDir, conf.outdir) );
+ fs.mkPath(toDir);
+ fs.copyFileSync(fileName, toDir);
+ });
+
+ __uniqueFilenames = null;
+
+ info("publishing done.");
+}
+
+// ---- helper functions for the templates ----
+
+var rSinceVersion = /^([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?([-.][0-9A-Z]+)?)(?:\s|$)/i;
+
+function extractVersion(value) {
+
+ if ( !value ) {
+ return undefined;
+ }
+
+ if ( value === true ) {
+ value = '';
+ } else {
+ value = String(value);
+ }
+
+ var m = rSinceVersion.exec(value);
+ return m ? m[1] : undefined;
+
+}
+
+var rSince = /^(?:as\s+of|since)(?:\s+version)?\s*([0-9]+(?:\.[0-9]+(?:\.[0-9]+)?)?([-.][0-9A-Z]+)?)(?:\.$|\.\s+|[,:]\s*|\s-\s*|\s|$)/i;
+
+function extractSince(value) {
+
+ if ( !value ) {
+ return undefined;
+ }
+
+ if ( value === true ) {
+ value = '';
+ } else {
+ value = String(value);
+ }
+
+ var m = rSince.exec(value);
+ if ( m ) {
+ return {
+ since : m[1],
+ pos : m[0].length,
+ value : value.slice(m[0].length).trim()
+ };
+ }
+
+ return {
+ pos : 0,
+ value: value.trim()
+ };
+
+}
+
+function sortByAlias(a, b) {
+ var partsA = a.longname.split(/[.#]/);
+ var partsB = b.longname.split(/[.#]/);
+ var i = 0;
+ while ( i < partsA.length && i < partsB.length ) {
+ if ( partsA[i].toLowerCase() < partsB[i].toLowerCase() ) {
+ return -1;
+ }
+ if ( partsA[i].toLowerCase() > partsB[i].toLowerCase() ) {
+ return 1;
+ }
+ i++;
+ }
+ if ( partsA.length < partsB.length ) {
+ return -1;
+ }
+ if ( partsA.length > partsB.length ) {
+ return 1;
+ }
+ // as a last resort, try to compare the aliases case sensitive in case we have aliases that only
+ // differ in case like with "sap.ui.model.type" and "sap.ui.model.Type"
+ if ( a.longname < b.longname ) {
+ return -1;
+ }
+ if ( a.longname > b.longname ) {
+ return 1;
+ }
+ return 0;
+}
+
+/*
+function isNonEmptyNamespace($) {
+ return $.isNamespace && (
+ ($.properties && $.properties.length > 0) ||
+ ($.methods && $.methods.length > 0) ||
+ ($.augments && $.augments.length > 0) ||
+ ($.children && $.children.length > 0));
+};*/
+
+/* Just the first sentence (up to a full stop). Should not break on dotted variable names. */
+function summarize(desc) {
+ if ( desc != null ) {
+ desc = String(desc).replace(/\s+/g, ' ').
+ replace(/"'/g, '"').
+ replace(/^(<\/?p>| |\s)+/, '');
+
+ var match = /([\w\W]+?\.)[^a-z0-9_$]/i.exec(desc);
+ return match ? match[1] : desc;
+ }
+}
+
+/* Make a symbol sorter by some attribute. */
+function makeSortby(/* fields ...*/) {
+ var aFields = Array.prototype.slice.apply(arguments),
+ aNorms = [],
+ aFuncs = [];
+ for (var i = 0; i < arguments.length; i++) {
+ aNorms[i] = 1;
+ if ( typeof aFields[i] === 'function' ) {
+ aFuncs[i] = aFields[i];
+ continue;
+ }
+ aFuncs[i] = function($,n) { return $[n]; };
+ if ( aFields[i].indexOf("!") === 0 ) {
+ aNorms[i] = -1;
+ aFields[i] = aFields[i].slice(1);
+ }
+ if ( aFields[i] === 'deprecated' ) {
+ aFuncs[i] = function($,n) { return !!$[n]; };
+ } else if ( aFields[i] === 'static' ) {
+ aFields[i] = 'scope';
+ aFuncs[i] = function($,n) { return $[n] === 'static'; };
+ } else if ( aFields[i].indexOf("#") === 0 ) {
+ aFields[i] = aFields[i].slice(1);
+ aFuncs[i] = function($,n) { return $.comment.getTag(n).length > 0; };
+ }
+ }
+ return function(a, b) {
+ // info("compare " + a.longname + " : " + b.longname);
+ var r = 0,i,va,vb;
+ for (i = 0; r === 0 && i < aFields.length; i++) {
+ va = aFuncs[i](a,aFields[i]);
+ vb = aFuncs[i](b,aFields[i]);
+ if ( va && !vb ) {
+ r = -aNorms[i];
+ } else if ( !va && vb ) {
+ r = aNorms[i];
+ } else if ( va && vb ) {
+ va = String(va).toLowerCase();
+ vb = String(vb).toLowerCase();
+ if (va < vb) {
+ r = -aNorms[i];
+ }
+ if (va > vb) {
+ r = aNorms[i];
+ }
+ }
+ // debug(" " + aFields[i] + ": " + va + " ? " + vb + " = " + r);
+ }
+ return r;
+ };
+}
+
+/** Pull in the contents of an external file at the given path. */
+
+function processTemplateAndSave(sTemplateName, oData, sOutputName) {
+ var sResult = processTemplate(sTemplateName, oData);
+ if ( conf.normalizeWhitespace && /\.html$/.test(sOutputName) ) {
+ sResult = normalizeWhitespace(sResult);
+ }
+ var sOutpath = path.join(conf.outdir, sOutputName);
+ try {
+ fs.mkPath( path.dirname(sOutpath) );
+ fs.writeFileSync(sOutpath, sResult, 'utf8');
+ } catch (e) {
+ error("failed to write generated file '" + sOutpath + "':" + (e.message || String(e)));
+ }
+}
+
+function processTemplate(sTemplateName, data) {
+ debug("processing template '" + sTemplateName + "' for " + data.longname);
+
+ var result;
+
+ try {
+ result = view.render(sTemplateName, {
+ asPlainSummary: asPlainSummary,
+ bySimpleName: bySimpleName,
+ childrenOfKind: childrenOfKind,
+ conf: conf,
+ data: data,
+ getConstructorDescription : getConstructorDescription,
+ getNSClass: getNSClass,
+ groupByVersion: groupByVersion,
+ extractSince: extractSince,
+ include: processTemplate,
+ Link: Link,
+ listTypes: listTypes,
+ linkTypes: linkTypes,
+ makeExample: makeExample,
+ makeLinkList: makeLinkList,
+ makeLinkToSymbolFile: makeLinkToSymbolFile,
+ makeSignature: makeSignature,
+ makeSortby: makeSortby,
+ publish : publish,
+ formatText: formatText,
+ simpleNameOf: simpleNameOf,
+ sortByAlias: sortByAlias,
+ summarize: summarize,
+ Version : Version
+ });
+ } catch (e) {
+ if ( e.source ) {
+ var filename = path.join(env.opts.destination, sTemplateName + ".js");
+ error("**** failed to process template, source written to " + filename);
+ fs.mkPath(path.dirname(filename));
+ fs.writeFileSync(filename, e.source, 'utf8');
+ }
+ error("error while processing " + sTemplateName);
+ throw e;
+ }
+ debug("processing template done.");
+ return result;
+}
+
+function groupByVersion(symbols, extractVersion) {
+
+ var map = {};
+
+ symbols.forEach(function(symbol) {
+
+ var version = extractVersion(symbol),
+ key = String(version);
+
+ if ( !map[key] ) {
+ map[key] = { version: version, symbols : [] };
+ }
+ map[key].symbols.push(symbol);
+
+ });
+
+ var groups = Object.keys(map).map(function(key) { return map[key]; });
+
+ return groups.sort(function(a,b) {
+ if ( !a.version && b.version ) {
+ return -1;
+ } else if ( a.version && !b.version ) {
+ return 1;
+ } else if ( a.version && b.version ) {
+ return -a.version.compareTo(b.version);
+ }
+ return 0;
+ });
+}
+
+function groupByModule(symbols) {
+
+ var map = {};
+
+ function add(key, symbol) {
+ if ( !map[key] ) {
+ map[key] = { name: key, symbols : [] };
+ }
+ if ( map[key].symbols.indexOf(symbol) < 0 ) {
+ map[key].symbols.push(symbol);
+ }
+ }
+
+ symbols.forEach(function(symbol) {
+
+ var key = symbol.__ui5.module;
+
+ if ( key ) {
+ add(key, symbol);
+ if ( symbol.__ui5.members ) {
+ symbol.__ui5.members.forEach(function($) {
+ if ( !$.inherited && $.__ui5.module && $.__ui5.module !== key && conf.filter($) ) {
+ add($.__ui5.module, $);
+ }
+ });
+ }
+ }
+
+ });
+
+ var groups = Object.keys(map).map(function(key) { return map[key]; });
+
+ return groups;
+}
+
+
+var REGEXP_TAG = /<(\/?(?:[A-Z][A-Z0-9_-]*:)?[A-Z][A-Z0-9_-]*)(?:\s[^>]*)?>/gi;
+
+/**
+ * Removes unnecessary whitespace from an HTML document:
+ * - if the text between two adjacent HTML tags consists of whitespace only, the whole text is removed
+ * - otherwise, any sequence of whitespace in the text is reduced to a single blank
+ * - inside a
tag, whitespace is preserved
+ *
+ * Whitespace inside an element tag is not touched (although it could be normalized as well)
+ * @param {string} content raw HTML file
+ * @returns {string} HTML file with normalized whitespace
+ */
+function normalizeWhitespace(content) {
+ var compressed = '',
+ preformatted = 0,
+ p = 0, m, text;
+
+ REGEXP_TAG.lastIndex = 0;
+ while ( (m = REGEXP_TAG.exec(content)) ) {
+ if ( m.index > p ) {
+ text = content.slice(p, m.index);
+ if ( preformatted ) {
+ compressed += text;
+ // debug(' "' + text + '" (preformatted)');
+ } else {
+ text = text.replace(/\s+/g,' ');
+ if ( text.trim() ) {
+ compressed += text;
+ }
+ // debug(' "' + text + '" (trimmed)');
+ }
+ }
+
+ compressed += m[0];
+ // debug(' "' + m[0] + '" (tag)');
+ p = m.index + m[0].length;
+
+ if ( /^pre$/i.test(m[1]) ) {
+ preformatted++;
+ } else if ( /^\/pre$/i.test(m[1]) && preformatted ) {
+ preformatted--;
+ }
+
+ }
+
+ if ( content.length > p ) {
+ text = content.slice(p, content.length);
+ if ( preformatted ) {
+ compressed += text;
+ // debug(' "' + text + '" (preformatted)');
+ } else {
+ text = text.replace(/\s+/g,' ');
+ if ( text.trim() ) {
+ compressed += text;
+ }
+ // debug(' "' + text + '" (trimmed)');
+ }
+ }
+
+ return compressed;
+}
+
+function makeLinkToSymbolFile(longname) {
+ return Link.baseSymbols + __uniqueFilenames[longname] + conf.ext;
+}
+
+function simpleNameOf(longname) {
+ longname = String(longname);
+ var p = longname.lastIndexOf('.');
+ return p < 0 ? longname : longname.slice(p + 1);
+}
+
+function bySimpleName(a,b) {
+ if ( a === b ) {
+ return 0;
+ }
+ var simpleA = simpleNameOf(a);
+ var simpleB = simpleNameOf(b);
+ if ( simpleA === simpleB ) {
+ return a < b ? -1 : 1;
+ } else {
+ return simpleA < simpleB ? -1 : 1;
+ }
+}
+
+/* Build output for displaying function parameters. */
+function makeSignature(params) {
+ var r = ['('], desc;
+ if ( params ) {
+ for (var i = 0, p; (p = params[i]); i++) {
+ // ignore @param tags for 'virtual' params that are used to document members of config-like params
+ // (e.g. like "@param param1.key ...")
+ if (p.name && p.name.indexOf('.') == -1) {
+ if (i > 0) {
+ r.push(', ');
+ }
+
+ r.push('');
+ r.push(p.name);
+ r.push('');
+ if ( p.optional ) {
+ r.push('?');
+ }
+ }
+ }
+ }
+ r.push(')');
+ return r.join('');
+}
+
+
+/*
+ * regexp to recognize important places in the text
+ *
+ * Capturing groups of the RegExp:
+ * group 1: begin of a pre block
+ * group 2: end of a pre block
+ * group 3: begin of a header/ul/ol/table, implicitly ends a paragraph
+ * group 4: end of a header/ul/ol/table, implicitly starts a new paragraph
+ * group 5: target portion of an inline @link tag
+ * group 6: (optional) text portion of an inline link tag
+ * group 7: an empty line which implicitly starts a new paragraph
+ *
+ * [-------
block -------] [----------------------- some flow content -----------------------] [---- an inline {@link ...} tag ----] [---------- an empty line ---------] */
+var rFormatText = /(
';
+ }
+ } 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 = '