Warning, /frameworks/syntax-highlighting/utils/generate-dot-file.lua is written in an unsupported language. File is not indexed.
0001 #!/usr/bin/env lua 0002 -- SPDX-FileCopyrightText: 2020 Jonathan Poelen <jonathan.poelen@gmail.com> 0003 -- SPDX-License-Identifier: MIT 0004 0005 -- load modules or show installation commands 0006 _, argparse = xpcall(require, function(err) trace1 = err end, 'argparse') 0007 _, xmlparser = xpcall(require, function(err) trace2 = err end, 'xmllpegparser') 0008 if not argparse or not xmlparser then 0009 local versparam = '--lua-version=' .. _VERSION:match('Lua (.*)') 0010 io.stderr:write('Please install ' 0011 .. (argparse and '' or 'argparse') 0012 .. (not argparse and not xmlparser and ' and ' or '') 0013 .. (xmlparser and '' or 'xmllpegparser') 0014 .. ':\n\n luarocks --local ' .. versparam .. ' install' 0015 .. (argparse and '' or ' argparse') 0016 .. (xmllpegparser and '' or ' xmllpegparser') 0017 .. '\n\n eval "$(luarocks ' .. versparam .. ' path)"\n\n' 0018 .. (trace1 or trace2) .. '\n') 0019 os.exit(1) 0020 end 0021 0022 parser = argparse(arg[0], 0023 "Dot file generator for xml syntax\n\nExample:\n " 0024 .. arg[0] .. " data/syntax/lua.xml | dot -T png -o image.png") 0025 0026 function parser:pattern(name, description) 0027 parser 0028 :option(name, description .. "\nCan be present several times.") 0029 :argname'<pattern>' 0030 :count'*' 0031 end 0032 0033 parser:flag('-c --context-only', "Generates contexts without rules") 0034 parser:flag('-r --replace-entities', "Evaluate html entities") 0035 parser:pattern('-i --include', "Include only contexts that respect a (lua) pattern.") 0036 parser:pattern('-e --exclude', "Exclude contexts that respect a (lua) pattern.") 0037 parser:argument('syntax.xml', "Syntax Definition Files") 0038 0039 do 0040 -- same message for -h, --help and usage 0041 local help = parser:get_help() 0042 parser:usage(help) 0043 parser:help(help) 0044 end 0045 0046 args = parser:parse() 0047 0048 excludes = args.exclude 0049 -- '' is a pattern always found 0050 includes = #args.include == 0 and {''} or args.include 0051 contextOnly = args.context_only 0052 replaceEntities = args.replace_entities and not contextOnly 0053 0054 0055 do 0056 -- load file 0057 local f, err = io.open(args['syntax.xml']) 0058 if not f then 0059 io.stderr:write(err .. '\n') 0060 os.exit(2) 0061 end 0062 0063 local content = f:read'*a' 0064 0065 -- remove BOM 0066 if content:sub(1,3) == '\xef\xbb\xbf' then 0067 content = content:sub(4) 0068 end 0069 0070 -- load document 0071 document, err = xmlparser.parse(content) 0072 if not document then 0073 io.stderr:write(err .. '\n') 0074 os.exit(3) 0075 end 0076 end 0077 0078 0079 local contexts 0080 -- search language/highlighting/contexts 0081 for k,nodes in pairs(document.children[1].children[1].children) do 0082 if (nodes.tag == 'contexts') then 0083 contexts = nodes.children 0084 break 0085 end 0086 end 0087 0088 if not contexts then 0089 io.stderr:write('<contexts> not found\n') 0090 os.exit(4) 0091 end 0092 0093 if replaceEntities then 0094 local entities = xmlparser.createEntityTable(document.entities) 0095 0096 for _,ctx in pairs(contexts) do 0097 for _,rule in pairs(ctx.children) do 0098 for _,k in pairs({'String', 'char', 'char1'}) do 0099 if rule.attrs[k] then 0100 rule.attrs[k] = xmlparser.replaceEntities(rule.attrs[k], entities) 0101 end 0102 end 0103 end 0104 end 0105 end 0106 0107 0108 local colorMap = { 0109 '"/set312/1"', 0110 '"lightgoldenrod1"', 0111 '"/set312/3"', 0112 '"/set312/4"', 0113 '"/set312/5"', 0114 '"/set312/6"', 0115 '"/set312/7"', 0116 '"/rdpu3/2"', 0117 '"/rdgy4/3"', 0118 '"/purd6/3"', 0119 '"/ylgn4/2"', 0120 '"/set26/6"', 0121 } 0122 0123 -- returns a color which depends on the first 2 characters 0124 function computeColor(name) 0125 local hash = name:byte(1) 0126 if #name ~= 1 then 0127 hash = hash + name:byte(2) * 25 0128 end 0129 return colorMap[hash % #colorMap + 1]; 0130 end 0131 0132 function matchPatterns(name, patterns) 0133 for _,v in pairs(patterns) do 0134 if name:find(v) then 0135 return true 0136 end 0137 end 0138 return false 0139 end 0140 0141 function matchContext(name) 0142 return not matchPatterns(name, excludes) and matchPatterns(name, includes) 0143 end 0144 0145 fmt = string.format 0146 0147 do 0148 local lpeg = require'lpeg' 0149 local P = lpeg.P 0150 local C = lpeg.C 0151 local S = lpeg.S 0152 local Cf = lpeg.Cf 0153 local Cc = lpeg.Cc 0154 local Cs = lpeg.Cs 0155 local Ct = lpeg.Ct 0156 0157 -- #pop counter 0158 _countpop = Cf((P'#pop' * Cc(1))^1, function(a,b) return a+b end) 0159 -- cut by slice of 40 characters 0160 _wordwap = Ct(Cc('') * C(P(40) + P(1)^1)^0) 0161 -- replace " by \" and \ by \\ 0162 _escape = Cs((S'"' / '\\"' + S'\\' / '\\\\' + 1)^0) 0163 -- extract context of #pop#pop#pop!Context 0164 _jumpctx = (1 - S'!')^1 * '!' * C(P(1)^1) 0165 end 0166 0167 function labelize(name) 0168 local n = _countpop:match(name) 0169 if n and n > 1 then 0170 return fmt('#pop(%d)%s', n, name:sub(n * 4 + 1)) 0171 end 0172 return name 0173 end 0174 0175 -- convert {[k]={data, index}, ...} to {{k, data, index}, ...} sorted by index 0176 -- for stable output 0177 function kIndexedTableToTable(kt) 0178 local t = {} 0179 for k,x in pairs(kt) do 0180 t[#t+1] = {k, x[1], x[2]} 0181 end 0182 table.sort(t, function(a,b) return a[3] < b[3] end) 0183 return t 0184 end 0185 0186 local sharp = string.byte('#',1) 0187 0188 0189 if contextOnly then 0190 print('digraph G {') 0191 print(' compound=true;ratio=auto') 0192 0193 for _,ctx in pairs(contexts) do 0194 local ctxName = ctx.attrs.name 0195 if matchContext(ctxName) then 0196 -- avoid multi arrow for ctx1 -> ctx2 0197 local kRuleContexts = {} 0198 for i,rule in ipairs(ctx.children) do 0199 kRuleContexts[rule.attrs.context or '#stay'] = {i,i} 0200 end 0201 0202 if kRuleContexts['#stay'] and kRuleContexts[ctxName] then 0203 kRuleContexts[ctxName] = nil 0204 end 0205 0206 local color = computeColor(ctxName) 0207 local escapedCtxName = _escape:match(ctxName) 0208 print(fmt(' "%s" [style=filled,color=%s]', escapedCtxName, color)) 0209 0210 for _,t in pairs(kIndexedTableToTable(kRuleContexts)) do 0211 local ruleContext, i = t[1], t[2] 0212 if ruleContext == '#stay' then 0213 print(fmt(' "%s" -> "%s" [color=%s]', 0214 escapedCtxName, escapedCtxName, color)) 0215 elseif ruleContext:byte(1) == sharp then 0216 local escapedRuleContext = _escape:match(ruleContext) 0217 local nextContext = _jumpctx:match(escapedRuleContext) 0218 print(fmt(' "%s" -> "%s!!%d" [color=%s];\n "%s!!%d" [label="%s"];', 0219 escapedCtxName, 0220 escapedCtxName, i, color, 0221 escapedCtxName, i, labelize(escapedRuleContext))) 0222 if nextContext then 0223 print(fmt(' "%s!!%d" -> "%s"', 0224 escapedCtxName, i, nextContext)) 0225 end 0226 else 0227 print(fmt(' "%s" -> "%s" [color=%s]', 0228 escapedCtxName, labelize(_escape:match(ruleContext)), color)) 0229 end 0230 end 0231 end 0232 end 0233 0234 print('}') 0235 0236 return 0 0237 end 0238 0239 function stringifyAttrs(t, attrs) 0240 local attr 0241 local s = '' 0242 for k,v in pairs(t) do 0243 if attrs[v] then 0244 attr = attrs[v] 0245 if #attr > 40 then 0246 attr = table.concat(_wordwap:match(attr),'\n') 0247 end 0248 s = s .. ' ' .. v .. ':' .. attr 0249 end 0250 end 0251 return s 0252 end 0253 0254 function xmlBool(s) 0255 return s == 'true' or s == '1' 0256 end 0257 0258 function printContextAttr(escapedOrigin, escapedCtxName, escapedNameAttr, style, color) 0259 if not escapedNameAttr then 0260 print(fmt(' "%s" -> "%s" [style=%s,color=%s];', 0261 escapedOrigin, escapedCtxName, style, color)) 0262 elseif escapedNameAttr:byte(1) == sharp then 0263 print(fmt(' "%s" -> "%s!!%s" [style=%s,color=%s];\n "%s!!%s" [label="%s",color=%s];', 0264 escapedOrigin, 0265 escapedCtxName, escapedNameAttr, style, color, 0266 escapedCtxName, escapedNameAttr, labelize(escapedNameAttr), color)) 0267 end 0268 end 0269 0270 function printLastTransition(escapedName, escapedCtxName, escapedNameAttr, color) 0271 if escapedNameAttr:byte(1) == sharp then 0272 local escapedLastCtx = _jumpctx:match(escapedNameAttr) 0273 if escapedLastCtx then 0274 print(fmt(' "%s!!%s" -> "%s" [style=dashed,color=%s];', 0275 escapedCtxName, escapedNameAttr, escapedLastCtx, color)) 0276 end 0277 else 0278 print(fmt(' "%s" -> "%s" [style=dashed,color=%s];', 0279 escapedName, escapedNameAttr, color)) 0280 end 0281 end 0282 0283 local firstLineAttrs = {'attribute','String','char','char1'} 0284 local secondLineAttrs = {'beginRegion','endRegion','lookAhead','firstNonSpace', 'column'} 0285 0286 print('digraph G {') 0287 print(' compound=true;ratio=auto') 0288 for ictx,ctx in pairs(contexts) do 0289 local ctxName = ctx.attrs.name 0290 if matchContext(ctxName) then 0291 local color = computeColor(ctxName) 0292 local escapedCtxName = _escape:match(ctxName) 0293 print(fmt(' subgraph cluster%d {', ictx)) 0294 print(fmt(' "%s" [shape=box,style=filled,color=%s];', escapedCtxName, color)) 0295 0296 local name = ctxName 0297 local escapedName = escapedCtxName 0298 local kDot = {} 0299 for irule,rule in pairs(ctx.children) do 0300 local nextName = ctxName .. '!!' .. irule .. '!!' .. rule.tag 0301 local escapedNextName = _escape:match(nextName) 0302 local ruleContext = rule.attrs.context 0303 print(fmt(' "%s" -> "%s" [style=dashed,color=%s];', 0304 escapedName, escapedNextName, color)) 0305 name = nextName 0306 escapedName = escapedNextName 0307 0308 local a = '' 0309 if rule.tag == 'IncludeRules' then 0310 a = ' ' .. ruleContext 0311 else 0312 if not rule.attrs.attribute then 0313 rule.attrs.attribute = ctx.attrs.attribute 0314 end 0315 a = a .. stringifyAttrs(firstLineAttrs, rule.attrs) 0316 local a2 = stringifyAttrs(secondLineAttrs, rule.attrs) 0317 if #a2 ~= 0 then 0318 a = a .. '\n' .. a2 0319 end 0320 end 0321 print(fmt(' "%s" [label="%s%s"];', escapedName, rule.tag, _escape:match(a))) 0322 0323 if xmlBool(rule.attrs.lookAhead) then 0324 print(fmt(' "%s" [style=dashed];', escapedName)) 0325 end 0326 0327 0328 if ruleContext == '#stay' then 0329 print(fmt(' "%s" -> "%s" [color=dodgerblue3];', 0330 escapedName, escapedCtxName)) 0331 elseif ruleContext then 0332 local escapedRuleContext = _escape:match(ruleContext) 0333 if ruleContext:byte(1) == sharp then 0334 local escapedBindCtxName = _jumpctx:match(escapedRuleContext) 0335 print(fmt(' "%s" -> "%s!!%s" [color=%s];\n "%s!!%s" [label="%s"];', 0336 escapedName, 0337 escapedCtxName, escapedRuleContext, color, 0338 escapedCtxName, escapedRuleContext, labelize(escapedRuleContext))) 0339 if escapedBindCtxName then 0340 kDot[escapedCtxName .. '!!' .. escapedRuleContext .. '!!' .. escapedBindCtxName] = { 0341 fmt(' "%s!!%s" -> "%s" [color=%s];\n "%s!!%s" [color=red];', 0342 escapedCtxName, escapedRuleContext, 0343 escapedBindCtxName, color, 0344 escapedCtxName, escapedRuleContext), 0345 irule, 0346 } 0347 end 0348 else 0349 kDot[irule] = { 0350 fmt(' "%s" -> "%s" [color=%s];', 0351 escapedName, escapedRuleContext, color), 0352 irule, 0353 } 0354 end 0355 end 0356 end 0357 0358 local fallthroughCtx = ctx.attrs.fallthroughContext 0359 local escapedFallthroughCtx = fallthroughCtx and _escape:match(fallthroughCtx) 0360 printContextAttr(escapedName, escapedCtxName, escapedFallthroughCtx, 'dashed', color) 0361 0362 local endCtx = ctx.attrs.lineEndContext 0363 local escapedEndCtx = endCtx and _escape:match(endCtx) 0364 printContextAttr(escapedCtxName, escapedCtxName, escapedEndCtx, 'dotted', 'blue') 0365 0366 print(' }') 0367 0368 if fallthroughCtx then 0369 printLastTransition(escapedName, escapedCtxName, escapedFallthroughCtx, color) 0370 end 0371 0372 if endCtx then 0373 printLastTransition(escapedName, escapedCtxName, escapedEndCtx, color) 0374 end 0375 0376 for _,t in pairs(kIndexedTableToTable(kDot)) do 0377 print(t[2]) 0378 end 0379 end 0380 end 0381 print('}')