blob: b5a0175a4c3045077168824eb7a5ec65c5202ff3 [file] [log] [blame]
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;
});