regulex
Version:
JavaScript Regular Expression Parser and Visualizer.
818 lines (755 loc) • 22.7 kB
JavaScript
if (typeof define !== 'function') var define = require('amdefine')(module);
define(['./Kit','./parse'],function (K,parse) {
parse.exportConstants();
var FONT_SIZE=16,LABEL_FONT_SIZE=14,PATH_LEN=16,BG_COLOR='#EEE',
FONT_FAMILY='DejaVu Sans Mono,monospace';
var _multiLine=false; /* global flag quick work*/
var PAPER_MARGIN=10;
var _charSizeCache={},_tmpText;
function getCharSize(fontSize,fontBold) {
fontBold=fontBold || 'normal';
if (_charSizeCache[fontSize] && _charSizeCache[fontSize][fontBold])
return _charSizeCache[fontSize][fontBold];
_tmpText.attr({'font-size':fontSize,'font-weight':fontBold});
var box=_tmpText.getBBox();
_charSizeCache[fontSize]=_charSizeCache[fontSize] || {};
return _charSizeCache[fontSize][fontBold]={
width:box.width/((_tmpText.attr('text').length-1)/2),
height:box.height/2
};
}
function initTmpText(paper) {
_tmpText=paper.text(
-1000,-1000,"XgfTlM|.q\nXgfTlM|.q" // random multiline text
).attr({'font-family':FONT_FAMILY,'font-size':FONT_SIZE});
}
/**
@param {AST} re AST returned by `parse`
*/
function visualize(re,flags,paper) {
paper.clear();
paper.setSize(0,0);
var bg = paper.rect(0,0,0,0);
bg.attr("fill", BG_COLOR);
bg.attr("stroke", BG_COLOR);
initTmpText(paper);
_multiLine=!!~flags.indexOf('m');
var texts=highlight(re.tree);
texts.unshift(text('/',hlColorMap.delimiter));
texts.unshift(text("RegExp: "));
texts.push(text('/',hlColorMap.delimiter));
if (flags) texts.push(text(flags,hlColorMap.flags));
var charSize=getCharSize(FONT_SIZE,'bold'),
startX=PAPER_MARGIN,startY=charSize.height/2+PAPER_MARGIN,
width=0,height=0;
width=texts.reduce(function(x,t) {
t.x=x;
t.y=startY;
var w=t.text.length*charSize.width;
return x+w;
},startX);
width+=PAPER_MARGIN;
height=charSize.height+PAPER_MARGIN*2;
texts=paper.add(texts);
paper.setSize(width,charSize.height+PAPER_MARGIN*2);
var ret=plot(re.tree,0,0);
height=Math.max(ret.height+3*PAPER_MARGIN+charSize.height,height);
width=Math.max(ret.width+2*PAPER_MARGIN,width);
paper.setSize(width,height);
bg.attr('width',width);
bg.attr('height',height);
translate(ret.items,PAPER_MARGIN,PAPER_MARGIN*2+charSize.height-ret.y);
paper.add(ret.items);
}
function plot(tree,x,y) {
tree.unshift({type:'startPoint'});
tree.push({type:'endPoint'});
return plotTree(tree,x,y);
}
function translate(items,dx,dy) {
items.forEach(function (t) {
if (t._translate) t._translate(dx,dy);
else {t.x+=dx;t.y+=dy;}
});
}
// return NodePlot config
function plotTree(tree,x,y) {
var results=[],items=[],
width=0,height=0,
fromX=x,top=y,bottom=y;
if (!tree.length) return plotNode.empty(null,x,y);
tree.forEach(function (node) {
var ret;
if (node.repeat) {
ret=plotNode.repeat(node,fromX,y);
} else {
ret=plotNode[node.type](node,fromX,y);
}
results.push(ret);
fromX+=ret.width+PATH_LEN;
width+=ret.width;
top=Math.min(top,ret.y);
bottom=Math.max(bottom,ret.y+ret.height);
items=items.concat(ret.items);
});
height=bottom-top;
results.reduce(function (a,b) {
width+=PATH_LEN;
var p=hline(a.lineOutX,y,b.lineInX);
items.push(p);
return b;
});
var lineInX=results[0].lineInX,lineOutX=results[results.length-1].lineOutX;
return {
items:items,
width:width,height:height,x:x,y:top,
lineInX:lineInX,lineOutX:lineOutX
};
}
// return NodePlot config
function textRect(s,x,y,bgColor,textColor) {
s=K.toPrint(s);
var padding=6;
var charSize=getCharSize(FONT_SIZE);
var tw=s.length*charSize.width,h=charSize.height+padding*2,w=tw+padding*2;
var rect={
type:'rect',
x:x,y:y-(h/2),
width:w,height:h,
stroke:'none',
fill:bgColor || 'transparent'
};
var t={
type:'text',
x:x+w/2,y:y,
text:s,
'font-size':FONT_SIZE,
'font-family':FONT_FAMILY,
fill:textColor || 'black'
};
return {
text:t,rect:rect,
items:[rect,t],
width:w,height:h,
x:x,y:rect.y,
lineInX:x,lineOutX:x+w
};
}
// return LabelObject {lable:Element,x,y,width,height}
function textLabel(x,y,s,color) {// x is center x ,y is bottom y
var charSize=getCharSize(LABEL_FONT_SIZE);
var lines=s.split("\n");
var textHeight=lines.length*charSize.height;
var textWidth;
if (lines.length>1) {
textWidth=Math.max.apply(Math,lines.map(function (a) {return a.length}));
} else {
textWidth=s.length;
}
textWidth=textWidth*charSize.width;
var margin=4;
var txt={
type:'text',
x:x,y:y-textHeight/2-margin,
text:s,
'font-size':LABEL_FONT_SIZE,
'font-family':FONT_FAMILY,
fill:color || '#444'
};
return {
label:txt,
x:x-textWidth/2,y:y-textHeight-margin,
width:textWidth,height:textHeight+margin
};
}
//return element config
function hline(x,y,destX) {
return {
type:'path',
x:x,y:y,
path:["M",x,y,"H",destX],
'stroke-linecap':'butt',
'stroke-linejoin':'round',
'stroke':'#333',
'stroke-width':2,
_translate:function (x,y) {
var p=this.path;
p[1]+=x;p[2]+=y;p[4]+=x;
},
};
}
//return element config
function smoothLine(fromX,fromY,toX,toY) {
var radius=10,p,_translate;
var signX=fromX>toX?-1:1,signY=fromY>toY?-1:1;
if (Math.abs(fromY-toY)<radius*1.5 /*|| Math.abs(fromX-toX)<radius*2*/) {
p=['M',fromX,fromY,
'C',fromX+Math.min(Math.abs(toX-fromX)/2,radius)*signX,fromY,
toX-(toX-fromX)/2,toY,
toX,toY];
_translate=function (x,y) {
var p=this.path;
p[1]+=x;p[2]+=y;
p[4]+=x;p[5]+=y;
p[6]+=x;p[7]+=y;
p[8]+=x;p[9]+=y;
};
} else {
p=[
'M',fromX,fromY,
'Q',fromX+radius*signX,fromY,fromX+radius*signX,fromY+radius*signY,
'V',Math.abs(fromY-toY)<radius*2 ? fromY+radius*signY : (toY-radius*signY),
'Q',fromX+radius*signX,toY,fromX+radius*signX*2,toY,
'H',toX
];
_translate=function (x,y) {
var p=this.path;
p[1]+=x;p[2]+=y;
p[4]+=x;p[5]+=y;p[6]+=x;p[7]+=y;
p[9]+=y;
p[11]+=x;p[12]+=y;p[13]+=x;p[14]+=y;
p[16]+=x;
};
}
return {
type:'path',
path:p,
'stroke-linecap':'butt',
'stroke-linejoin':'round',
'stroke':'#333',
'stroke-width':2,
_translate:_translate
};
}
function point(x,y,fill) {
var r=10;
return {
items:[{
type:'circle',
fill:fill,
cx:x+r,cy:y,r:r,
stroke:"none",
_translate:function (x,y) {
this.cx+=x;
this.cy+=y;
}
}],
width:r*2,height:r*2,x:x,y:y,
lineInX:x,lineOutX:x+r*2
};
}
var plotNode={
startPoint:function (node,x,y) {
return point(x,y,"r(0.5,0.5)#EFE-green")
},
endPoint:function (node,x,y) {
return point(x,y,"r(0.5,0.5)#FFF-#000")
},
empty:function (node,x,y) {
var len=10;
var l=hline(x,y,x+len);
return {
items:[l],
width:len,height:2,
x:x,y:y,lineInX:x,lineOutX:x+len
};
},
exact:function (node,x,y) {
var color='skyblue';
return textRect(node.chars,x,y,color);
},
dot:function (node,x,y) {
var bgColor='DarkGreen',textColor='white';
var a=textRect('AnyCharExceptNewLine',x,y,bgColor,textColor);
a.rect.r=10;
a.rect.tip="AnyChar except CR LF"
return a;
},
backref:function (node,x,y) {
var bgColor='navy',textColor='white';
var a=textRect('Backref #'+node.num,x,y,bgColor,textColor);
a.rect.r=8;
return a;
},
repeat:function (node,x,y) {
if (elideOK(node)) return plotNode.empty(null,x,y);
var padding=10,LABEL_MARGIN=4;
var repeat=node.repeat,txt="",items=[];
var NonGreedySkipPathColor='darkgreen';
/*if (repeat.min===0 && !node._branched) {
node._branched=true;
return plotNode.choice({type:CHOICE_NODE,branches:[[{type:EMPTY_NODE}],[node]]},x,y);
}*/
if (repeat.min===repeat.max && repeat.min===0) {
return plotNode.empty(null,x,y); // so why allow /a{0}/ ?
}
var ret=plotNode[node.type](node,x,y);
var width=ret.width,height=ret.height;
if (repeat.min===repeat.max && repeat.min===1) {
return ret; // if someone write /a{1}/
} else if (repeat.min===repeat.max) {
txt+=_plural(repeat.min);
} else {
txt+=repeat.min;
if (isFinite(repeat.max)) {
txt+= (repeat.max-repeat.min > 1 ? " to " : " or ") +_plural(repeat.max);
} else {
txt+=" or more times";
}
}
var offsetX=padding,offsetY=0,r=padding,rectH=ret.y+ret.height-y,rectW=padding*2+ret.width;
width=rectW;
var p; // repeat rect box path
if (repeat.max!==1) {// draw repeat rect box
rectH+=padding;
height+=padding;
p={
type:'path',
path:['M',ret.x+padding,y,
'Q',x,y,x,y+r,
'V',y+rectH-r,
'Q',x,y+rectH,x+r,y+rectH,
'H',x+rectW-r,
'Q',x+rectW,y+rectH,x+rectW,y+rectH-r,
'V',y+r,
'Q',x+rectW,y,ret.x+ret.width+padding,y
],
_translate:_curveTranslate,
stroke:'maroon',
'stroke-width':2
};
if (repeat.nonGreedy) {
//txt+="(NonGreedy!)";
p.stroke="Brown";
p['stroke-dasharray']="-";
}
items.push(p);
} else { // so completely remove label when /a?/ but not /a??/
txt=false;
}
var skipPath;
if (repeat.min===0) {//draw a skip path
var skipRectH=y-ret.y+padding,skipRectW=rectW+padding*2;
offsetX+=padding;
offsetY=-padding-2; //tweak,stroke-width is 2
width=skipRectW; height+=padding;
skipPath={
type:'path',
path:['M',x,y,
'Q',x+r,y,x+r,y-r,
'V',y-skipRectH+r,
'Q',x+r,y-skipRectH,x+r*2,y-skipRectH,
'H',x+skipRectW-r*2,
'Q',x+skipRectW-r,y-skipRectH,x+skipRectW-r,y-skipRectH+r,
'V',y-r,
'Q',x+skipRectW-r,y,x+skipRectW,y
],
_translate:_curveTranslate,
stroke:repeat.nonGreedy? NonGreedySkipPathColor:'#333',
'stroke-width':2
};
if (p) translate([p],padding,0);
items.push(skipPath);
}
if (txt) {
var tl=textLabel(x+width/2,y,txt);
translate([tl.label],0,rectH+tl.height+LABEL_MARGIN); //bottom label
items.push(tl.label);
height+=LABEL_MARGIN+tl.height;
var labelOffsetX=(Math.max(tl.width,width)-width)/2;
if (labelOffsetX) translate(items,labelOffsetX,0);
width=Math.max(tl.width,width);
offsetX+=labelOffsetX;
}
translate(ret.items,offsetX,0);
items=items.concat(ret.items);
return {
items:items,
width:width,height:height,
x:x,y:ret.y+offsetY,
lineInX:ret.lineInX+offsetX,
lineOutX:ret.lineOutX+offsetX
};
function _plural(n) {
return n+ ((n<2)? " time":" times");
}
function _curveTranslate(x,y) {
var p=this.path;
p[1]+=x;p[2]+=y;
p[4]+=x;p[5]+=y;p[6]+=x;p[7]+=y;
p[9]+=y;
p[11]+=x;p[12]+=y;p[13]+=x;p[14]+=y;
p[16]+=x;
p[18]+=x;p[19]+=y;p[20]+=x;p[21]+=y;
p[23]+=y;
p[25]+=x;p[26]+=y;p[27]+=x;p[28]+=y;
}
},
choice:function (node,x,y) {
if (elideOK(node)) return plotNode.empty(null,x,y);
var marginX=20,spacing=6,paddingY=4,height=0,width=0;
var branches=node.branches.map(function (branch) {
var ret=plotTree(branch,x,y);
height+=ret.height;
width=Math.max(width,ret.width);
return ret;
});
height+=(branches.length-1)*spacing+paddingY*2;
width+=marginX*2;
var centerX=x+width/2,dy=y-height/2+paddingY,// destY
lineOutX=x+width,items=[];
branches.forEach(function (a) {
var dx=centerX-a.width/2; // destX
translate(a.items,dx-a.x,dy-a.y);
items=items.concat(a.items);
/*
var p1=smoothLine(x,y,dx-a.x+a.lineInX,y+dy-a.y);
var p2=smoothLine(lineOutX,y,a.lineOutX+dx-a.x,y+dy-a.y);
items=items.concat(a.items);
items.push(p1,p2);*/
// current a.y based on y(=0),its middle at y=0
var lineY=y+dy-a.y;
var p1=smoothLine(x,y,x+marginX,lineY);
var p2=smoothLine(lineOutX,y,x+width-marginX,lineY);
items.push(p1,p2);
if (x+marginX!==dx-a.x+a.lineInX) {
items.push(hline(x+marginX,lineY,dx-a.x+a.lineInX));
}
if (a.lineOutX+dx-a.x!==x+width-marginX) {
items.push(hline(a.lineOutX+dx-a.x,lineY,x+width-marginX));
}
a.x=dx;a.y=dy;
dy+=a.height+spacing;
});
return {
items:items,
width:width,height:height,
x:x,y:y-height/2,
lineInX:x,lineOutX:lineOutX
};
},
charset:function (node,x,y) {
var padding=6,spacing=4;
var clsDesc={d:'Digit',D:'NonDigit',w:'Word',W:'NonWord',s:'WhiteSpace',S:'NonWhiteSpace'};
var charBgColor='LightSkyBlue',charTextColor='black',
clsBgColor='Green',clsTextColor='white',
rangeBgColor='teal',rangeTextColor='white',
boxColor=node.exclude?'Pink':'Khaki',
labelColor=node.exclude?'#C00':'';
var simple=onlyCharClass(node);
if (simple) {
var a=textRect(clsDesc[node.classes[0]],x,y,clsBgColor,clsTextColor);
a.rect.r=5;
if (!node.exclude) {
return a;
} else {
var tl=textLabel(a.x+a.width/2,a.y,'None of:',labelColor);
var items=a.items;
items.push(tl.label);
var oldWidth=a.width;
var width=Math.max(tl.width,a.width);
var offsetX=(width-oldWidth)/2;//ajust label text
translate(items,offsetX,0);
return {
items:items,
width:width,height:a.height+tl.height,
x:Math.min(tl.x,a.x),y:tl.y,
lineInX:offsetX+a.x,lineOutX:offsetX+a.x+a.width
};
}
}
if (!node.chars && !node.ranges.length && !node.classes.length) {
// It must be exclude charset here
var a= textRect('AnyChar',x,y,'green','white');
a.rect.r=5;
return a;
}
var packs=[],ret,width=0,height=0,singleBoxHeight;
if (node.chars) {
ret=textRect(node.chars,x,y,charBgColor,charTextColor);
ret.rect.r=5;
packs.push(ret);
width=ret.width;
}
node.ranges.forEach(function (rg) {
rg=rg.split('').join('-');
var ret=textRect(rg,x,y,rangeBgColor,rangeTextColor);
ret.rect.r=5;
packs.push(ret);
width=Math.max(ret.width,width);
});
node.classes.forEach(function (cls) {
var ret=textRect(clsDesc[cls],x,y,clsBgColor,clsTextColor);
ret.rect.r=5;
packs.push(ret);
width=Math.max(ret.width,width);
});
singleBoxHeight=packs[0].height;
var pack1=[],pack2=[];
packs.sort(function (a,b) {return b.width-a.width});
packs.forEach(function (a) {
if (a.width*2+spacing>width) pack1.push(a);
else pack2.push(a); // can be inline
});
packs=pack1;
var a1,a2;
while (pack2.length) {
a1=pack2.pop(); a2=pack2.pop();
if (!a2) {packs.push(a1);break;}
if (a1.width-a2.width > 2) {
packs.push(a1);
pack2.push(a2);
continue;
}
translate(a2.items,a1.width+spacing,0);
packs.push({
items:a1.items.concat(a2.items),
width:a1.width+a2.width+spacing,
height:a1.height,
x:a1.x,y:a1.y
});
height-=a1.height;
}
width+=padding*2;
height=(packs.length-1)*spacing+packs.length*singleBoxHeight+padding*2;
var rect={
type:'rect',
x:x,y:y-height/2,r:4,
width:width,height:height,
stroke:'none',fill:boxColor
};
var startY=rect.y+padding;
var items=[rect];
packs.forEach(function (a) {
translate(a.items,x-a.x+(width-a.width)/2,startY-a.y);
items=items.concat(a.items);
startY+=a.height+spacing;
});
var tl=textLabel(rect.x+rect.width/2,rect.y,(node.exclude?'None':'One')+' of:',labelColor);
items.push(tl.label);
var oldWidth=width;
width=Math.max(tl.width,width);
var offsetX=(width-oldWidth)/2;//ajust label text
translate(items,offsetX,0);
return {
items:items,
width:width,height:height+tl.height,
x:Math.min(tl.x,x),y:tl.y,
lineInX:offsetX+x,lineOutX:offsetX+x+rect.width
};
},
group:function (node,x,y) {
if (elideOK(node)) return plotNode.empty(null,x,y);
var padding=10,lineColor='silver',strokeWidth=2;
var sub=plotTree(node.sub,x,y);
if (node.num) {
translate(sub.items,padding,0);
var rectW=sub.width+padding*2,rectH=sub.height+padding*2;
var rect={
type:'rect',
x:x,y:sub.y-padding,r:6,
width:rectW,height:rectH,
'stroke-dasharray':".",
stroke:lineColor,
'stroke-width':strokeWidth
};
var tl=textLabel(rect.x+rect.width/2,rect.y-strokeWidth,'Group #'+node.num);
var items=sub.items.concat([rect,tl.label]);
var width=Math.max(tl.width,rectW);
var offsetX=(width-rectW)/2;//ajust label text space
if (offsetX) translate(items,offsetX,0);
return {
items:items,
width:width,
height:rectH+tl.height+4, // 4 is margin
x:x,y:tl.y,
lineInX:offsetX+sub.lineInX+padding,lineOutX:offsetX+sub.lineOutX+padding
};
}
return sub;
},
assert:function (node,x,y) {
var simpleAssert={
AssertNonWordBoundary:{bg:"maroon",fg:"white"},
AssertWordBoundary:{bg:"purple",fg:"white"},
AssertEnd:{bg:"Indigo",fg:"white"},
AssertBegin:{bg:"Indigo",fg:"white"}
};
var conf,nat=node.assertionType,txt=nat.replace('Assert','')+'!';
if (conf=simpleAssert[nat]) {
if (_multiLine && (nat==='AssertBegin' || nat==='AssertEnd')) {
txt='Line'+txt;
}
return textRect(txt,x,y,conf.bg,conf.fg);
}
var lineColor,fg,padding=8;
if (nat===AssertLookahead) {
lineColor="CornflowerBlue";
fg="darkgreen";
txt="Followed by:";
} else if (nat===AssertNegativeLookahead) {
lineColor="#F63";
fg="Purple";
//txt="Negative\nLookahead!"; // break line
txt="Not followed by:";
}
var sub=plotNode.group(node,x,y);
var rectH=sub.height+padding*2,rectW=sub.width+padding*2;
var rect={
type:'rect',
x:x,y:sub.y-padding,r:6,
width:rectW,height:rectH,
'stroke-dasharray':"-",
stroke:lineColor,
'stroke-width':2
};
var tl=textLabel(rect.x+rectW/2,rect.y,txt,fg);
var width=Math.max(rectW,tl.width);
var offsetX=(width-rectW)/2;//ajust label text
translate(sub.items,offsetX+padding,0);
if (offsetX) translate([rect,tl.label],offsetX,0);
var items=sub.items.concat([rect,tl.label]);
return {
items:items,
width:width,
height:rect.height+tl.height,
x:x,y:tl.y,
lineInX:offsetX+sub.lineInX+padding,lineOutX:offsetX+sub.lineOutX+padding
};
}
};
function elideOK(a) {
if (Array.isArray(a)) {//stack
var stack=a;
for (var i=0;i<stack.length;i++) {
if (!elideOK(stack[i])) return false;
}
return true;
}
var node=a;
if (node.type===EMPTY_NODE) return true;
if (node.type===GROUP_NODE && node.num===undefined) {
return elideOK(node.sub);
}
if (node.type===CHOICE_NODE) {
return elideOK(node.branches);
}
}
var hlColorMap={
delimiter:'Indigo',
flags:'darkgreen',
exact:'#334',
dot:'darkblue',
backref:'teal',
'$':'purple',
'^':'purple',
'\\b':'#F30',
'\\B':'#F30',
'(':'blue',
')':'blue',
'?=':'darkgreen',
'?!':'red',
'?:':'grey',
'[':'navy',
']':'navy',
'|':'blue',
'{':'maroon',
',':'maroon',
'}':'maroon',
'*':'maroon',
'+':'maroon',
'?':'maroon',
repeatNonGreedy:'#F61',
defaults:'black',
charsetRange:'olive',
charsetClass:'navy',
charsetExclude:'red',
charsetChars:'#534'
};
/**
@param {AST.tree} re AST.tree return by `parse`
*/
function highlight(tree) {
var texts=[];
tree.forEach(function (node) {
if (node.sub) {
texts.push(text('('));
if (node.type===ASSERT_NODE) {
if (node.assertionType===AssertLookahead) {
texts.push(text('?='));
} else {
texts.push(text('?!'));
}
} else if (node.nonCapture) {
texts.push(text('?:'));
}
texts=texts.concat(highlight(node.sub));
texts.push(text(')'));
} else if (node.branches) {
node.branches.map(highlight).forEach(function (ts) {
texts=texts.concat(ts);
texts.push(text('|'));
});
texts.pop();
} else {
var color=hlColorMap[node.type] || hlColorMap.defaults;
switch (node.type) {
case CHARSET_NODE:
var simple=onlyCharClass(node);
(!simple || node.exclude) && texts.push(text('['));
if (node.exclude) texts.push(text('^',hlColorMap.charsetExclude));
node.ranges.forEach(function (rg) {
texts.push(text(_charsetEscape(rg[0]+'-'+rg[1]),hlColorMap.charsetRange));
});
node.classes.forEach(function (cls) {
texts.push(text("\\"+cls,hlColorMap.charsetClass));
});
texts.push(text(_charsetEscape(node.chars),hlColorMap.charsetChars));
(!simple || node.exclude) && texts.push(text(']'));
break;
default:
var s=node.raw || '';
if (node.repeat) s=s.slice(0,node.repeat.beginIndex);
s=K.toPrint(s,true);
texts.push(text(s,color));
}
}
if (node.repeat) {
var min=node.repeat.min,max=node.repeat.max;
if (min===0 && max===Infinity) texts.push(text('*'));
else if (min===1 && max===Infinity) texts.push(text('+'));
else if (min===0 && max===1) texts.push(text('?'));
else {
texts.push(text('{'));
texts.push(text(min));
if (min===max) texts.push(text('}'));
else {
texts.push(text(','));
if (isFinite(max)) texts.push(text(max));
texts.push(text('}'));
}
}
if (node.repeat.nonGreedy) {
texts.push(text('?',hlColorMap.repeatNonGreedy));
}
}
});
return texts;
}
function _charsetEscape(s) {
s=K.toPrint(s);
return s.replace(/\[/g,'\\[').replace(/\]/g,'\\]');
}
function text(s,color) {
color = color || hlColorMap[s] || hlColorMap.defaults;
return {
type:'text',
'font-size':FONT_SIZE,'font-family':FONT_FAMILY,
text:s+"",fill:color,'text-anchor':'start','font-weight':'bold'
};
}
function onlyCharClass(node) {
return !node.chars && !node.ranges.length && node.classes.length===1;
}
return visualize;
});