json-treeify
Version:
json-treeify: Get tree string(├└│─) via json, support browser|node, browser none dependencies!
269 lines (246 loc) • 9.51 kB
JavaScript
let { isString, isNill, isNaN, isObject, isSpreadable } = require('./type-of');
let travelJson = require('./json-travel');
/*
* escape string
* https://github.com/douglascrockford/JSON-js/blob/e39db4b7e6249f04a195e7dd0840e610cc9e941e/json2.js#L195
*/
// eslint-disable-next-line
let escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"': '\\"',
'\\': '\\\\'
};
function escapeString(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;//复原
return escapable.test(string) ? `"${string.replace(escapable, (a) => {
let c = meta[a];
return typeof c === 'string' ?
c :
`\\u${(`0000${a.charCodeAt(0).toString(16)}`).slice(-4)}`;
})}"` : `"${string}"`;
}
let _TreeChar_ = {
I: '│',
T: '├',
L: '└',
_: '─',
SPLIT: ':',
1: ' ',
2: ' ',
3: ' ',
4: ' ',
5: ' ',
6: ' ',
7: ' ',
8: ' ',
9: ' ',
10: ' '
};
function _isStartWith(chars){
let c = chars.join('|');
let reg = new RegExp(`^(${c})+`);
return function(str){
return isString(str) && !!str.match(reg);
};
}
let isNodeStr = _isStartWith([_TreeChar_.T, _TreeChar_.L, 'ROOT']);
let _RegTreeLinkChars = new RegExp(`^(${[_TreeChar_.I, _TreeChar_.T, _TreeChar_._, _TreeChar_.L].join('|')})`);
function replaceTreeLinkChar(str){
let check = isString(str) && str.match(_RegTreeLinkChars);
return check && check.length ? `'${check[1]}'${str.substr(1)}` : str;
}
function checkNextSibling(w, h, arr){
let i, hasNextSibling = false;
for(i=(h+1); i<arr.length; i++){
let ele = arr[i][w];
if(undefined===ele){
break;
}else if(isNodeStr(`${ele}`)){
hasNextSibling = true;
break;
}
}
return {
wPos: w,
hPos: i,
isLast: !hasNextSibling
};
}
//修复简单粗暴生成的二位数字,以便生成正确的节点连接符
function fixArr(arr){
let regNode = new RegExp(`^${_TreeChar_.T}`, 'g'),
regVert = new RegExp(`^${_TreeChar_.I}`, 'g'),
S = ' ';
let i, iLen = arr.length;
for(i=0; i<iLen; i++){
let row = arr[i];
for(let j=0; j<row.length; j++){
let o = row[j];
if(isNodeStr(o)){
let checkNext = checkNextSibling(j, i, arr);
if(checkNext.isLast){
// "├" => "└", 其实不做也行,因为 line236 中,已经判断并处理过
arr[i][j] = o.replace(regNode, _TreeChar_.L);
// 批量消除多余的"│"字符
for(let c=i+1; c<checkNext.hPos; c++){
if(arr[c]){
if(!!arr[c][j] && !!arr[c][j].match(regVert)){
// "│" => " "
arr[c][j] = arr[c][j].replace(regVert, S);
}
}
}
}
}
}
}
// fix root:
// "└" => "",,消除"└"
// "─" => " ",转化多余的"─"
let regSimRoot1 = new RegExp(_TreeChar_._, 'gi'),
regSimRoot2 = new RegExp(`${_TreeChar_.T}|${_TreeChar_.L}`, 'g');
arr[0][0] = arr[0][0].replace(regSimRoot1, ' ').replace(regSimRoot2, '');
return arr;
}
// '*' ---repeat-5---> '*****'
function repeatChar(char, n){
let res = '';
char = char.charAt(0);
n = parseInt(n) || 0;
n = n > 0 ? n : 0;
while(n--) {
res += char;
}
return res;
}
// format input config
function formatOption(options = {}){
// config
let {jsonName, space, vSpace, needValueOut, msReturnChar} = options || {};//不做空值保护,后面挨个做
// root name
jsonName = (isString(jsonName) ? jsonName : 0) || 'ROOT';
// horizon space
if ('\t'===space) {
space = 1;
} else {
space = parseInt(space);
space = isNaN(space) ? 3 : space;
space = space <= 0 ? 1 : space > 8 ? 8 : space;//1 <= space <= 8
}
// vertical space
vSpace = parseInt(vSpace);
vSpace = isNaN(vSpace) ? (space > 5 ? 2 : 1) : vSpace;
vSpace = vSpace< 0 ? 0 : vSpace > 2 ? 2 : vSpace;//0 <= vSpace <= 2
// whether value should print out
needValueOut = isNill(needValueOut) ? true : !!needValueOut;
return {jsonName, space, vSpace, needValueOut, msReturnChar};
}
/**
* treeify
* view process with 'example/img/flow.png'
* @param {Object} json
* @param {Object} options
* {String} options.rootName
* {char|Number} options.space [1,8]
* {Number} options.vSpace [0,2]
* {Boolean} options.valueOut
* {Boolean} options.msReturnChar '\r'
* @returns {string} a tree-like string
*/
function treeify(json, options){
// config
let {jsonName, space, vSpace, needValueOut, msReturnChar} = formatOption(options);
// check json content
if(isNill(json)){
return `${jsonName}'s content is ${String(json)}`;
}else if(isObject.isEmptyOwn(json)){
return `${jsonName}'s content is empty!`;
}else if(!isSpreadable(json)){
return `${jsonName}'s content is ${isString(json) ? escapeString(json) : String(json)}`;
}
// 间距
let _I_ = _TreeChar_.I + _TreeChar_[space],
// 为了美观一点,控制节点间距 1->0 2->0 3->1 4->1 5->2 6->2 7->3 8->3
_T_ = `${_TreeChar_.T + repeatChar(_TreeChar_._, parseInt((space - 1) / 2))} `,
_L_ = `${_TreeChar_.L + repeatChar(_TreeChar_._, parseInt((space - 1) / 2))} `,
SPLIT = `${_TreeChar_.SPLIT} `;
let ft = (Math.floor(jsonName.length / 2)) % 10; // 左间距 max 10
let _rT_ = `${_TreeChar_.T} `; // 求取根节点的符号
let _I1_ = _TreeChar_.I + repeatChar(' ', ft-1); // 最左边的符号和间距
//res:用于存储节点信息的二维数组,末尾显式加上值undefined来区分是否为父节点
let res = [
[_rT_ + jsonName, undefined]
];
for(let q=0; q < vSpace; q++){ // vertical-space
res.push([_I1_, _I_]);
}
// travel
// travelJson(json, (key, value, curKeyPath, typeStr, isSpreadable, curDepth) => {
// let depth = curDepth;
travelJson(json, (key, value, option = {}) => {
let { type, isSpreadable, depth, isLast } = option || {};
let typeStr = type;
let v;
if (isSpreadable) {
v = undefined;// 显式的赋值undefined,仅仅是为了标识区别出‘可拓展节点’(含有子属性的父节点)
} else {
if(needValueOut){
if(typeStr==='string'){
// 如果v值中就包含了特殊连接符号,需要转换一下
v = replaceTreeLinkChar(value);//v = value; //其实不转也行的
v = escapeString(v);
}else if(typeStr==='array'){
v = '[]';
}else if(typeStr==='object'){
v = '{}';
}else if(typeStr==='function'){
v = '[function code]';
}else{
v = String(value);
}
// 只有叶子节点才放值,父节点放undefined,采用显式赋值undefined作为父节点的标识(显式加入undefined元素可以占一个位置,会增加数组的length)
v = SPLIT + v;
} else {
v = '';// 不输出值,(此时应该是空字符串,而非undefined!,不然跟父节点混淆了)
}
}
let lineArr = [], i;// res[n]
for(i=1; i<depth; i++){// 左边缩进用的元素
lineArr.push(i===1?_I1_:_I_);//首元素加入的是_I1_
}
lineArr.push((isLast ? _L_ : _T_) + escapeString(key).slice(1, -1));// 节点名
lineArr.push(v);// 节点值
res.push(lineArr);
// vertical-space
for(let vs=0; vs<vSpace; vs++){
res.push(lineArr.map((item, index) => {
let isFirst = index===0, //首元素加入的是_I1_
isLast = index===(lineArr.length-1),
isSpreadableNodeEndFlag = isLast && item===undefined;
return index<(lineArr.length-1) || isSpreadableNodeEndFlag ? (isFirst?_I1_:_I_) : undefined;
}));
}
}, 'obj');
fixArr(res);
// 打印输出
// let color = require("./cli/colorful");
// res.forEach(function(item){
// let strOut = "[";
// item.forEach(function(sub,subIndex){
// strOut += ((sub===undefined?color.magenta("undefined"):color.yellow("'"+sub+"'")) + (subIndex===(item.length-1) ? "" : ", "));
// });
// strOut += "]";
// console.log(strOut);
// });
return res.map(item => item.join('').replace(/(\s|\u00A0)+$/, '')).join(msReturnChar ? '\r\n' : '\n');// '\r' just for windows
}
module.exports = treeify;