clone

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

main
diagramzip / renderers / python / blockdiag-family / src / packetdiag / parser.py
parser.py 6.8K
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 io39import re40from collections import namedtuple41from re import DOTALL, MULTILINE4243from blockdiag.parser import create_mapper, oneplus_to_list44from funcparserlib.lexer import LexerError, Token, make_tokenizer45from funcparserlib.parser import a, finished, many, maybe, skip, some4647Diagram = namedtuple('Diagram', 'id stmts')48FieldItem = namedtuple('FieldItem', 'begin end label attrs')49Attr = namedtuple('Attr', 'name value')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 ('Number', (r'[0-9]+',)), # NOQA65 ('QuotedFieldItem', (r'(?<=[:*\-])\s*(?P<quote>"|\').*?(?<!\\)(?P=quote)',)), # NOQA66 ('FieldItem', (r'(?<=[:*\-])\s*[^\r\n\[;}]+',)), # NOQA67 ('Space', (r'[ \t\r\n]+',)), # NOQA68 ('Name', ('[A-Za-z_0-9\u0080-\uffff]' + # NOQA69 '[A-Za-z_\\-.0-9\u0080-\uffff]*',)), # NOQA70 ('Op', (r'[{}:;,*\-=\[\]]',)), # NOQA71 ('String', (r'(?P<quote>(""")|(\'\'\')|"|\').*?(?<!\\)(?P=quote)', DOTALL)), # NOQA72 ]73 useless = ['Comment', 'NL', 'Space']74 t = make_tokenizer(specs)75 return [x for x in t(string) if x.type not in useless]767778def parse(seq):79 """Sequence(Token) -> object"""80 fielditem_tokens = ['QuotedFieldItem', 'FieldItem']8182 tokval = lambda x: x.value83 op = lambda s: a(Token('Op', s)) >> tokval84 op_ = lambda s: skip(op(s))85 _id = some(lambda t: t.type in ['Name', 'Number', 'String']) >> tokval86 keyword = lambda s: a(Token('Name', s)) >> tokval87 number = some(lambda t: t.type == 'Number').named('number') >> tokval88 field_item = some(lambda t: t.type in fielditem_tokens) >> tokval8990 def make_num_field_item(_from, to, text, attr):91 return FieldItem(_from, to, text.strip(), attr)9293 def make_nonnum_field_item(*args):94 if len(args) == 2:95 # funcparserlib-0.x96 text, attr = args97 else:98 # funcparserlib-1.099 _, text, attr = args100101 return FieldItem(None, None, text.strip(), attr)102103 #104 # parts of syntax105 #106 option_stmt = (107 _id +108 maybe(op_('=') + _id)109 >> create_mapper(Attr)110 )111 option_list = (112 maybe(op_('[') + option_stmt + many(op_(',') + option_stmt) + op_(']'))113 >> create_mapper(oneplus_to_list, default_value=[])114 )115116 # field statement::117 # 1: A118 # 2-3: B [attr = value, attr = value];119 # * C [attr = value, attr = value];120 # * D [attr = value, attr = value];121 #122 numbered_field_item_stmt = (123 number +124 maybe(op_('-') + number) +125 op_(':') +126 field_item +127 option_list128 >> create_mapper(make_num_field_item)129 )130 nonnumbered_field_item_stmt = (131 (op_('-') | op_('*')) +132 field_item +133 option_list134 >> create_mapper(make_nonnum_field_item)135 )136 field_item_stmt = (137 numbered_field_item_stmt |138 nonnumbered_field_item_stmt139 )140141 # attributes statement::142 # default_shape = box;143 # default_fontsize = 16;144 #145 attribute_stmt = (146 _id + op_('=') + _id147 >> create_mapper(Attr)148 )149150 # extension statement (plugin)::151 # plugin attributes [name = Name];152 #153 extension_stmt = (154 keyword('plugin') +155 _id +156 option_list157 >> create_mapper(Extension)158 )159160 #161 # diagram statement::162 # packetdiag {163 # A;164 # }165 #166 diagram_id = (167 (keyword('diagram') | keyword('packetdiag')) +168 maybe(_id)169 >> list170 )171 diagram_inline_stmt = (172 extension_stmt |173 field_item_stmt |174 attribute_stmt175 )176 diagram_inline_stmt_list = (177 many(diagram_inline_stmt + skip(maybe(op(';'))))178 )179 diagram = (180 maybe(diagram_id) +181 op_('{') +182 diagram_inline_stmt_list +183 op_('}')184 >> create_mapper(Diagram)185 )186 dotfile = diagram + skip(finished)187188 return dotfile.parse(seq)189190191def sort_tree(tree):192 def weight(node):193 if isinstance(node, (Attr, Extension)):194 return 1195 else:196 return 2197198 if hasattr(tree, 'stmts'):199 tree.stmts.sort(key=lambda x: weight(x))200 for stmt in tree.stmts:201 sort_tree(stmt)202203 return tree204205206def parse_string(string):207 try:208 tree = parse(tokenize(string))209 return sort_tree(tree)210 except LexerError as e:211 message = "Got unexpected token at line %d column %d" % e.place212 raise ParseException(message)213 except Exception as e:214 raise ParseException(str(e))215216217def parse_file(path):218 code = io.open(path, 'r', encoding='utf-8-sig').read()219 return parse_string(code)