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('}')