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