clone

a browser-first diagram editor with live svg previews, local drafts and encrypted, accountless sharing

main
diagramzip / renderers / python / blockdiag-family / src / rackdiag / parser.py
parser.py 7.9K
1# -*- coding: utf-8 -*-23# Copyright (c) 2008/2009 Andrey Vlasovskikh4#5# Permission is hereby granted, free of charge, to any person obtaining6# a copy of this software and associated documentation files (the7# "Software"), to deal in the Software without restriction, including8# without limitation the rights to use, copy, modify, merge, publish,9# distribute, sublicense, and/or sell copies of the Software, and to10# permit persons to whom the Software is furnished to do so, subject to11# the following conditions:12#13# The above copyright notice and this permission notice shall be included14# in all copies or substantial portions of the Software.15#16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.19# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY20# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,21# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE22# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.2324r'''A DOT language parser using funcparserlib.2526The parser is based on [the DOT grammar][1]. It is pretty complete with a few27not supported things:2829* Ports and compass points30* XML identifiers3132At the moment, the parser builds only a parse tree, not an abstract syntax tree33(AST) or an API for dealing with DOT.3435 [1]: http://www.graphviz.org/doc/info/lang.html36'''3738import io39from collections import namedtuple40from re import DOTALL, MULTILINE4142from blockdiag.parser import create_mapper, oneplus_to_list43from funcparserlib.lexer import LexerError, Token, make_tokenizer44from funcparserlib.parser import a, finished, many, maybe, skip, some4546Diagram = namedtuple('Diagram', 'id stmts')47RackItem = namedtuple('RackItem', 'number label attrs')48Attr = namedtuple('Attr', 'name value')49Rack = namedtuple('Rack', 'id stmts')50Extension = namedtuple('Extension', 'type name attrs')515253class ParseException(Exception):54 pass555657def tokenize(string):58 """str -> Sequence(Token)"""59 # flake8: NOQA60 specs = [ # NOQA61 ('Comment', (r'/\*(.|[\r\n])*?\*/', MULTILINE)), # NOQA62 ('Comment', (r'(//|#).*',)), # NOQA63 ('NL', (r'[\r\n]+',)), # NOQA64 ('QuotedRackItem', (r'(?<=[:*\-])\s*(?P<quote>"|\').*?(?<!\\)(?P=quote)', DOTALL)), # NOQA65 ('RackItem', (r'(?<=[:*\-])[^\r\n\[;}]+',)), # NOQA66 ('Space', (r'[ \t\r\n]+',)), # NOQA67 ('RackHeight', (r'[0-9]+U',)), # NOQA68 ('Units', (r'[0-9]+(?:\.[0-9]+)?(A|kg)',)), # NOQA69 ('Number', (r'[0-9]+',)), # NOQA70 ('Name', ('[A-Za-z_0-9\u0080-\uffff]' + # NOQA71 '[A-Za-z_\\-.0-9\u0080-\uffff]*',)), # NOQA72 ('Op', (r'[{}:;,*\-=\[\]]',)), # NOQA73 ('String', (r'(?P<quote>(""")|(\'\'\')|"|\').*?(?<!\\)(?P=quote)', DOTALL)), # NOQA74 ]75 useless = ['Comment', 'NL', 'Space']76 t = make_tokenizer(specs)77 return [x for x in t(string) if x.type not in useless]787980def parse(seq):81 """Sequence(Token) -> object"""82 id_tokens = ['Name', 'Number', 'String', 'RackHeight', 'Units']83 rackitem_tokens = ['QuotedRackItem', 'RackItem']8485 tokval = lambda x: x.value86 op = lambda s: a(Token('Op', s)) >> tokval87 op_ = lambda s: skip(op(s))88 _id = some(lambda t: t.type in id_tokens) >> tokval89 keyword = lambda s: a(Token('Name', s)) >> tokval90 rackheight = some(lambda t: t.type == 'RackHeight') >> tokval91 number = some(lambda t: t.type == 'Number').named('number') >> tokval92 rackitem = some(lambda t: t.type in rackitem_tokens) >> tokval9394 def make_num_rackitem(num, text, attr):95 return RackItem(num, text.strip(), attr)9697 def make_nonnum_rackitem(*args):98 if len(args) == 2:99 # funcparserlib-0.x100 text, attr = args101 else:102 # funcparserlib-1.0103 _, text, attr = args104105 return RackItem(None, text.strip(), attr)106107 def make_nonvalued_attr(rackheight):108 return Attr(rackheight, None)109110 #111 # parts of syntax112 #113 option_stmt = (114 _id +115 maybe(op_('=') + _id)116 >> create_mapper(Attr)117 )118 option_list = (119 maybe(op_('[') + option_stmt + many(op_(',') + option_stmt) + op_(']'))120 >> create_mapper(oneplus_to_list, default_value=[])121 )122123 # attributes statement::124 # default_shape = box;125 # default_fontsize = 16;126 # 12U;127 # ascending;128 #129 attribute_stmt = (130 _id + op_('=') + _id131 >> create_mapper(Attr)132 )133 rackheight_stmt = (134 rackheight135 >> make_nonvalued_attr136 )137 ascending_stmt = (138 keyword('ascending')139 >> make_nonvalued_attr140 )141142 # field statement::143 # 1: A144 # 2: B [attr = value, attr = value];145 # * C [attr = value, attr = value];146 # * D [attr = value, attr = value];147 #148 numbered_field_item_stmt = (149 number +150 op_(':') +151 rackitem +152 option_list153 >> create_mapper(make_num_rackitem)154 )155 nonnumbered_field_item_stmt = (156 (op_('-') | op_('*')) +157 rackitem +158 option_list159 >> create_mapper(make_nonnum_rackitem)160 )161 field_item_stmt = (162 numbered_field_item_stmt |163 nonnumbered_field_item_stmt164 )165166 # rack statement::167 # rack {168 # 1: A;169 # }170 #171172 # rack definition173 rack_inline_stmt = (174 attribute_stmt |175 rackheight_stmt |176 field_item_stmt177 )178 rack_inline_stmt_list = (179 many(rack_inline_stmt + skip(maybe(op(';'))))180 )181 rack_stmt = (182 skip(keyword('rack')) +183 maybe(_id) +184 op_('{') +185 rack_inline_stmt_list +186 op_('}')187 >> create_mapper(Rack)188 )189190 # extension statement (plugin)::191 # plugin attributes [name = Name];192 #193 extension_stmt = (194 keyword('plugin') +195 _id +196 option_list197 >> create_mapper(Extension)198 )199200 #201 # diagram statement::202 # rackdiag {203 # A;204 # }205 #206 diagram_id = (207 (keyword('diagram') | keyword('rackdiag')) +208 maybe(_id)209 >> list210 )211 diagram_inline_stmt = (212 ascending_stmt |213 extension_stmt |214 rack_stmt |215 field_item_stmt |216 attribute_stmt |217 rackheight_stmt218 )219 diagram_inline_stmt_list = (220 many(diagram_inline_stmt + skip(maybe(op(';'))))221 )222 diagram = (223 maybe(diagram_id) +224 op_('{') +225 diagram_inline_stmt_list +226 op_('}')227 >> create_mapper(Diagram)228 )229 dotfile = diagram + skip(finished)230231 return dotfile.parse(seq)232233234def sort_tree(tree):235 def weight(node):236 if isinstance(node, (Attr, Extension)):237 return 1238 else:239 return 2240241 if hasattr(tree, 'stmts'):242 tree.stmts.sort(key=lambda x: weight(x))243 for stmt in tree.stmts:244 sort_tree(stmt)245246 return tree247248249def parse_string(string):250 try:251 tree = parse(tokenize(string))252 return sort_tree(tree)253 except LexerError as e:254 message = "Got unexpected token at line %d column %d" % e.place255 raise ParseException(message)256 except Exception as e:257 raise ParseException(str(e))258259260def parse_file(path):261 code = io.open(path, 'r', encoding='utf-8-sig').read()262 return parse_string(code)