legacy/js/docTool/schemaHelper.js (429 lines of code) (raw):

define(function (require) { /** * [optionPath reserved characters]: * a-z、A-Z、0-9、[、]、-、All of non-ascii */ /** * [Schema Format]: * { * type: 'Array', 'Object', 'string', 'Function', or ['Array', 'string'], * description: 'description text', * default: string, default value, * properties: If type is `'Object'`, * { * type: 'Object', * properties: { * objAttr1: {... objAttr1 definition ...}, * objAttr2: {... objAttr2 definition ...} * } * } * items: If type is `'Array'` * { * type: 'Array', * items: {... item definition ...} * } * or * { * type: 'Array', * items: { * anyOf: [ * { * type: 'Object', * properties: { * type: {... A property named `type` (that is, typeEnum) should exists ...}, * attr2: {... attr2 definition ...} * } * }, * ... * ] * } * } * } */ /** * Doc tree structure: * (1) * { * propertyName: 'option', * children: [{ * propertyName: 'xAxis' // Use data[i] instead of data level. * children: [...] * }, { * propertyName: 'color' * }, ...] * } * (2) * { * propertyName: 'legend', * children: [{ * propertyName: 'data', // Use `'data[i]'` instead of data level. * arrayDepth: 1, // `1` means `data[i]`, `2` means `data[i][i]`. * children: [{ * propertyName: 'id' * }, ...] * }, ...] * } * (3) * { * propertyName: 'option', * children: [{ * propertyName: 'series', * arrayDepth: 1, * isEnumParent: true, * children: [{ * // No `propertyName` in type enum node. * typeEnum: 'line', * children: [{ * propertyName: 'id' * }, ...] * }, { * typeEnum: 'bar', * children: [{ * propertyName: 'id' * }, ...] * }, ...] * }, ...] * } */ // References var $ = require('jquery'); var dtLib = require('dt/lib'); var docUtil = require('./docUtil'); var globalArgs = require('globalArgs'); var encodeHTML = dtLib.encodeHTML; // Inner constants var DEFAULT_VALUE_BRIEF_LENGTH = 20; var QUOTATION_REG_SINGLE = /^\s*'(.*)'\s*$/; // 中间也含有引号,因为贪婪,所以可以不管。 var QUOTATION_REG_DOUBLE = /^\s*"(.*)"\s*$/; // 中间也含有引号,因为贪婪,所以可以不管。 // var PATH_ITEM_REG = /^(\w+|i\])(\-([a-zA-Z_ \/,]*))?$/; // Only ascii. var PATH_ITEM_REG = /^([^\[\]\-]+|i\])(\-([0-9a-zA-Z_ \/,]*))?$/; // Also 中文. /** * @public */ var schemaHelper = {}; // Relation info: var IS_OBJECT_ITEM = 'isPropertyItem'; var IS_ARRAY_ITEM = 'isArrayItem'; // Enum info: var IS_ENUM_ITEM = 'isEnumItem'; var IS_ENUM_PARENT = 'isEnumParent'; /** * option path: * * `'tooltip.formatter'` * `'axis[i].symbol'` (the same as `'axis.symbol'` in query) * `'series[i]-line.symbol'` (the same as `'series-line.symbol'` in query) * where `'line'` is `typeEnum`, and `'[i]'` will be removed. * * @public * @param {string} optionPath * @param {Object=} options * @param {boolean=} [options.noTypeEnum=false] If false, in 'Responsive%20Mobile-End', * '-End' will be recognized as ctxVar. * @return {Array.<Object>} An array of: * { * // either arrayName (for array) or propertyName (for object) * propertyName: 'xxx', * // A string indicates the type enum. * typeEnum: 'line' * } */ schemaHelper.parseOptionPath = function (optionPath, options) { options = options || {}; var errorInfo = 'Path is illegal: \'' + optionPath + '\''; dtLib.assert( optionPath && (optionPath = $.trim(optionPath)), errorInfo ); var pathArr = optionPath.replace(/\[i\]/g, '').split(/\./); var retArr = []; for (var i = 0, len = pathArr.length; i < len; i++) { var itemStr = $.trim(pathArr[i]); // if (options.ignoreEmptyItem && itemStr === '') { if (itemStr === '') { continue; } // match: 'asdf-bb' 'asdf-' 'i]-bb' 'i]-' 'asdf' 'i]' var regResult = itemStr.match(PATH_ITEM_REG) || []; var propertyName = regResult[1]; var typeEnumSegment = regResult[2]; // '-line' var typeEnum = regResult[3]; // 'line' if (options.noTypeEnum) { propertyName += typeEnumSegment || ''; typeEnum = null; } retArr.push({ propertyName: propertyName, typeEnum: typeEnum ? typeEnum : null }); } return retArr; }; /** * @public * @param {Object} docTree * @param {Object} args * @param {string=} [args.fuzzyPath] Like 'bbb(line,pie).ccc', * using fuzzy mode, case insensitive. * i.e. The query string above matches the result 'mm.zbbbx.yyy.cccl'. * @param {string=} [args.optionPath] Like 'aaa(line,pie).bbb.cc', * must be matched accurately, case sensitive. * @param {string=} [args.anyText] Like 'somesomesome', * using fuzzy mode, case insensitive.. * full text query (include descriptoin) * @param {boolean} [args.noTypeEnum=false] If false, in 'Responsive%20Mobile-End', * '-End' will be recognized as ctxVar. * @return {Array.<Object>} result * @throws {Error} */ schemaHelper.queryDocTree = function (docTree, args) { args = args || {}; var context = { originalDocTree: docTree, result: [], optionPath: args.optionPath ? schemaHelper.parseOptionPath( args.optionPath, {noTypeEnum: args.noTypeEnum} ) : null, fuzzyPath: args.fuzzyPath ? schemaHelper.parseOptionPath( args.fuzzyPath, {noTypeEnum: args.noTypeEnum} ) : null, anyText: args.anyText && $.trim(args.anyText) || null }; dtLib.assert( (context.optionPath || context.fuzzyPath || context.anyText) && (!!context.optionPath && !!context.fuzzyPath) === false, 'invalid query string!' ); if (context.optionPath || context.fuzzyPath) { queryRecursivelyByPath(docTree, context, 0); } else { queryRecursivelyByContent(docTree, context); } return context.result; function queryRecursivelyByPath(docTree, context, pathIndex) { if (!dtLib.isObject(docTree)) { return; } var pathItem = (context.optionPath || context.fuzzyPath)[pathIndex]; var lastPathItem = (context.optionPath || context.fuzzyPath)[pathIndex - 1]; if (!pathItem) { // Consider: query 'series-line', whether match 'series'? if (!docTree.isEnumParent || context.fuzzyPath || !lastPathItem || !lastPathItem.typeEnum ) { context.result.push(docTree); } if (!docTree.isEnumParent) { // Enum children can be matched togather with their parent. return; } } for (var i = 0, len = (docTree.children || []).length; i < len; i++) { var child = docTree.children[i]; var nextPathIndex = null; if (docTree.isEnumParent) { if (!lastPathItem || !lastPathItem.typeEnum || child.typeEnum === lastPathItem.typeEnum ) { nextPathIndex = pathIndex; } // else do nothing. } else if (context.optionPath && pathAccurateMatch(child, pathItem.propertyName, pathItem.arrayName) ) { nextPathIndex = pathIndex + 1; } else if (context.fuzzyPath) { if (pathFuzzyMatch(child, pathItem.propertyName, pathItem.arrayName)) { nextPathIndex = pathIndex + 1; } else { nextPathIndex = pathIndex; } } if (nextPathIndex != null) { queryRecursivelyByPath(child, context, nextPathIndex); } } } function queryRecursivelyByContent(docTree, context) { if (!dtLib.isObject(docTree)) { return; } if (context.anyText && ( pathFuzzyMatch(docTree, context.anyText) || (docTree.description && docTree.description.indexOf(context.anyText) >= 0) )) { context.result.push(docTree); return; } for (var i = 0, len = (docTree.children || []).length; i < len; i++) { queryRecursivelyByContent(docTree.children[i], context); } } function pathAccurateMatch(child, propertyName, arrayName) { return child.propertyName != null && child.propertyName === propertyName; } function pathFuzzyMatch(child, propertyName, arrayName) { if (propertyName != null) { propertyName = propertyName.toLowerCase(); } if (arrayName != null) { arrayName = arrayName.replace(/\[i\]/g, '').toLowerCase(); } return child.propertyName != null && child.propertyName.toLowerCase().indexOf(propertyName) >= 0; } }; /** * Build doc by schema. * A doc json will be generated, which is different from schema json. * Some business rules will be applied when doc being built. * For example, the doc of 'series' will be organized by chart type. * * @public * @param {Object} schema * @param {Object} renderBase */ schemaHelper.buildDoc = function (schema, renderBase) { buildRecursively(renderBase, schema.option); return renderBase; // To reduce GC cost, pass context parameters directly. function buildRecursively(renderBase, schemaItem, optionPathItemName, relationInfo, enumInfo, arrayFrom) { if (!dtLib.isObject(schemaItem)) { return; } if (schemaItem.anyOf) { var subRenderBase = renderDocItem( 'isEnumParent', renderBase, schemaItem, optionPathItemName, relationInfo, IS_ENUM_PARENT, arrayFrom ); for (var j = 0; j < schemaItem.anyOf.length; j++) { buildRecursively( subRenderBase, schemaItem.anyOf[j], optionPathItemName, null, IS_ENUM_ITEM, arrayFrom ? arrayFrom.slice() : null ); } } else if (schemaItem.items) { var subRenderBase = renderDocItem( 'hasArrayItems', renderBase, schemaItem, optionPathItemName, relationInfo, enumInfo, arrayFrom ); buildRecursively( subRenderBase, schemaItem.items, optionPathItemName, // Actually this is array base item name. IS_ARRAY_ITEM, null, arrayFrom ? (arrayFrom.push(schemaItem), arrayFrom) : [schemaItem] ); } else if (schemaItem.properties) { var subRenderBase = renderDocItem( 'hasObjectProperties', renderBase, schemaItem, optionPathItemName, relationInfo, enumInfo, arrayFrom ); var properties = schemaItem.properties; for (var propertyName in schemaItem.properties) { if (properties.hasOwnProperty(propertyName)) { buildRecursively( subRenderBase, schemaItem.properties[propertyName], propertyName, IS_OBJECT_ITEM, null, null ); } } } // SchemaItem with type of neither 'object' or 'array', and schemaItem with type of 'object' // but do not has properties defined. else { renderDocItem( 'isAtom', renderBase, schemaItem, optionPathItemName, relationInfo, enumInfo, arrayFrom ); } } }; function renderDocItem( handlerName, renderBase, schemaItem, optionPathItemName, relationInfo, enumInfo, arrayFrom ) { var subRenderBase = renderBase; var typeEnum = enumInfo === IS_ENUM_ITEM ? getTypeEnum(schemaItem) : null; // makeSubRenderBase if (handlerName !== 'hasArrayItems') { var descInfo = retrieveDescFromSchemaItem(schemaItem, relationInfo, enumInfo, arrayFrom); subRenderBase = { value: 'id-' + dtLib.localUID(), parent: renderBase, hasObjectProperties: handlerName === 'hasObjectProperties', isEnumParent: enumInfo === IS_ENUM_PARENT, type: schemaItem.type, typeEnum: typeEnum, description: descInfo.description, defau: descInfo.defau, // optionPath: context.optionPath.slice(), defaultValueText: schemaHelper.getDefaultValueText(descInfo.defau), itemEncodeHTML: false, tooltipEncodeHTML: false }; if (enumInfo !== IS_ENUM_ITEM) { subRenderBase.propertyName = optionPathItemName; } if (relationInfo === IS_ARRAY_ITEM) { subRenderBase.arrayDepth = arrayFrom.length; } (renderBase.children = renderBase.children || []).push(subRenderBase); } // Make prefix, suffix and childrenBrief. var prefix = ''; var suffix = ''; var childrenBrief = '...'; if (enumInfo === IS_ENUM_ITEM) { childrenBrief = 'type: \'' + encodeHTML(typeEnum) + '\', ...'; } else { if (optionPathItemName) { prefix = '<span class="ecdoc-api-tree-text-prop">' + encodeHTML(optionPathItemName) + '</span>'; if (!docUtil.getGlobalArg('pureTitle')) { prefix += ': '; } } if (arrayFrom && arrayFrom.length) { // Simple optimize. if (arrayFrom.length === 1) { prefix += '['; suffix += ']'; } else { var tmpArr = new Array(arrayFrom.length + 1); prefix += tmpArr.join('['); suffix += tmpArr.join(']'); } } } // Make tree item text and children. if (handlerName === 'hasObjectProperties') { subRenderBase.childrenPre = prefix + '{'; subRenderBase.childrenPost = '}' + suffix + ','; subRenderBase.childrenBrief = childrenBrief; } else if (handlerName === 'isAtom') { var defaultValueText = schemaHelper.getDefaultValueText( subRenderBase.defau, {getBrief: true} ); subRenderBase.text = prefix; if (!docUtil.getGlobalArg('pureTitle')) { subRenderBase.text += '' + '<span class="ecdoc-api-tree-text-default">' + encodeHTML(defaultValueText) + '</span>' + suffix + ','; } } else if (handlerName === 'isEnumParent') { subRenderBase.childrenPre = prefix; subRenderBase.childrenPost = suffix + ','; subRenderBase.childrenBrief = childrenBrief; } return subRenderBase; } function retrieveDescFromSchemaItem(schemaItem, relationInfo, enumInfo, arrayFrom) { // Array parent has no renderBase, so consider these cases: // // Case 1: // { // name: 'visualMap', // type: 'Array', // description: 'visualMap introduce', // items: { // anyOf: [ // { // type: 'continuous', // description: 'visualMapContinuous introduce' // }, // { // type: 'piecewise', // description: 'visualMapPiecewise introduce' // } // ] // } // } // The real renderBase is on "items", // where we need show description of 'visualMap'. // // Case 2: // { // name: 'data', // type: 'Array', // description: 'description of data', // items: { // properties: { // ... // } // } // } // The real renderBase is on "items", // where we need show description of 'data'. var arrayFrom = (arrayFrom || []).slice(); var item = ( arrayFrom && arrayFrom.length && ( enumInfo === IS_ENUM_PARENT || (relationInfo === IS_ARRAY_ITEM && enumInfo !== IS_ENUM_ITEM ) ) ) ? arrayFrom[0] : schemaItem; var result = { description: globalArgs.schemaName === 'option3' // 兼容 option3.json ? item.descriptionCN : item.description, defau: {type: item.type} }; if (item.hasOwnProperty('default')) { result.defau['default'] = item['default']; } return result; } /** * 得到默认值的简写,在一行之内显示。 * defau中,设置了default但值是undefined,和没有设置default,是不一样的。 * * @public * @param {Object} defau * @param {Object} [defau.default] * @param {Object} options * @param {boolean} [options.getBrief] default false, otherwise return full text. * @param {Object} [options.briefMapping] * @return {strting} default value text */ schemaHelper.getDefaultValueText = function (defau, options) { options = options || {}; var briefMapping = $.extend( { 'object': '{...}', 'array': '[...]', 'regexp': '/.../', 'function': 'Function', '?': '...' }, options.briefMapping ); if (defau.hasOwnProperty('default')) { var defaultValue = defau['default']; var type = $.type(defaultValue); if ('null,undefined,number,boolean'.indexOf(type) >= 0) { return defaultValue + ''; } else if (type === 'string') { return ( options.getBrief ? cutString(defaultValue, DEFAULT_VALUE_BRIEF_LENGTH) : defaultValue ); } else { if (options.getBrief) { return briefMapping[type] || briefMapping['?']; } else { try { // FIXME // json2? return JSON.stringify(defaultValue, null, 4); } catch (e) { return defaultValue + ''; } } } } else { if (options.getBrief) { var type = docUtil.normalizeToArray(defau.type); return type.length === 1 // Only one type, can be sure what the brief looks like. && briefMapping[type[0].toLowerCase()] || briefMapping['?']; } else { return ''; } } }; // /** // * @param {Object} schema Where schema Will be filled. // * @param {Object} descSchema // */ // schemaHelper.fillSchemaWithDescription = function (schema, descSchema) { // buildRecursively(descSchema.option, schema.option); // function buildRecursively(descSchemaItem, schemaItem) { // if (!dtLib.isObject(schemaItem)) { // return; // } // if (schemaItem.anyOf) { // schemaItem.anyOf.forEach(function (item, j) { // buildRecursively(descSchemaItem.anyOf[j], item); // }); // } // else if (schemaItem.items) { // buildRecursively(descSchemaItem.items, schemaItem.items); // } // else if (schemaItem.properties) { // Object.keys(schemaItem.properties).forEach(function (propertyName) { // buildRecursively( // descSchemaItem.properties[propertyName], // schemaItem.properties[propertyName] // ); // }); // } // else { // schemaItem.description = descSchemaItem.description; // } // } // }; schemaHelper.getOptionPathForHash = function (treeItem) { return buildOptionPathFromTreeItem(treeItem); }; schemaHelper.getOptionPathForHTML = function (treeItem) { return buildOptionPathFromTreeItem(treeItem, true, true); }; function buildOptionPathFromTreeItem(treeItem, useSquareBrackets, html) { var optionPath = []; var notLeaf; // Exclude the root. while (treeItem && treeItem.parent && treeItem.parent.parent) { var typeEnum = treeItem.typeEnum; var itemStr = typeEnum ? getPropertyName(treeItem.parent, useSquareBrackets) + '-' + typeEnum : getPropertyName(treeItem, useSquareBrackets); if (html) { itemStr = dtLib.encodeHTML(itemStr || ''); if (!notLeaf) { itemStr = '<strong>' + itemStr + '</strong>'; } } optionPath.push(itemStr); treeItem = treeItem.parent; if (typeEnum) { treeItem = treeItem.parent; } notLeaf = true; } return optionPath.reverse().join('.'); } function getPropertyName(treeItem, useSquareBrackets) { var propertyName = treeItem.propertyName; var arrayDepth = treeItem.arrayDepth; if (useSquareBrackets && arrayDepth) { if (arrayDepth === 1) { propertyName += '[i]'; } else { propertyName += new Array(arrayDepth + 1).join('[i]'); } } return propertyName; } /** * @inner */ function getTypeEnum(schemaItem) { // 这里是硬编码:anyOf的子节点必须有properties,必须有type属性。 var typeEnum = schemaItem.properties.type['default']; return removeQuotation(typeEnum); } /** * @inner */ function removeQuotation(value, noQuotationReturnNull) { var matchResult = value.match(QUOTATION_REG_SINGLE) || value.match(QUOTATION_REG_DOUBLE); if (matchResult) { return matchResult[1]; } return noQuotationReturnNull ? null : value; } /** * @inner * @param {string} value 包含引号 * @param {number} length */ function cutString(value, length) { var removed = removeQuotation(value, true); return removed != null ? '\'' + cut(removed) + '\'' : cut(value); function cut(str) { return str.length > length ? (str.slice(0, length) + '...') : str; } } return schemaHelper; });