clone

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

main
diagramzip / renderers / python / blockdiag-family / src / seqdiag / parser.py
parser.py 8.0K
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, flatten, oneplus_to_list43from funcparserlib.lexer import LexerError, Token, make_tokenizer44from funcparserlib.parser import (a, finished, forward_decl, many, maybe, skip,45 some)4647Diagram = namedtuple('Diagram', 'id stmts')48Group = namedtuple('Group', 'stmts')49Node = namedtuple('Node', 'id attrs')50Attr = namedtuple('Attr', 'name value')51Edge = namedtuple('Edge', ('from_node edge_type to_node '52 'followers attrs edge_block'))53Statements = namedtuple('Statements', 'stmts')54Separator = namedtuple('Separator', 'type value')55Extension = namedtuple('Extension', 'type name attrs')56Fragment = namedtuple('Fragment', 'type id stmts')575859class ParseException(Exception):60 pass616263def tokenize(string):64 """str -> Sequence(Token)"""65 # flake8: NOQA66 specs = [ # NOQA67 ('Comment', (r'/\*(.|[\r\n])*?\*/', MULTILINE)), # NOQA68 ('Comment', (r'(//|#).*',)), # NOQA69 ('NL', (r'[\r\n]+',)), # NOQA70 ('Space', (r'[ \t\r\n]+',)), # NOQA71 ('Separator', (r'(?P<sep>===|\.\.\.)[^\r\n]+(?P=sep)',)), # NOQA72 ('Name', ('[A-Za-z_0-9\u0080-\uffff]' + # NOQA73 '[A-Za-z_\\-.0-9\u0080-\uffff]*',)), # NOQA74 ('Op', (r'(=>)|[{};,=\[\]]|(<<?--?)|(--?>>?)',)), # NOQA75 ('Number', (r'-?(\.[0-9]+)|([0-9]+(\.[0-9]*)?)',)), # NOQA76 ('String', (r'(?P<quote>(""")|(\'\'\')|"|\').*?(?<!\\)(?P=quote)', DOTALL)), # NOQA77 ]78 useless = ['Comment', 'NL', 'Space']79 t = make_tokenizer(specs)80 return [x for x in t(string) if x.type not in useless]818283def parse(seq):84 """Sequence(Token) -> object"""85 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 ['Name', 'Number', 'String']) >> tokval89 keyword = lambda s: a(Token('Name', s)) >> tokval90 separator = some(lambda t: t.type == 'Separator') >> tokval9192 def make_separator(sep):93 return Separator(sep[0:3], sep[3:-3].strip())9495 #96 # parts of syntax97 #98 option_stmt = (99 _id +100 maybe(op_('=') + _id)101 >> create_mapper(Attr)102 )103 option_list = (104 maybe(op_('[') + option_stmt + many(op_(',') + option_stmt) + op_(']'))105 >> create_mapper(oneplus_to_list, default_value=[])106 )107108 # attributes statement::109 # default_shape = box;110 # default_fontsize = 16;111 #112 attribute_stmt = (113 _id + op_('=') + _id114 >> create_mapper(Attr)115 )116117 # node statement::118 # A;119 # B [attr = value, attr = value];120 #121 node_stmt = (122 _id + option_list123 >> create_mapper(Node)124 )125126 # separator statement::127 # === message ===128 # ... message ...129 #130 separator_stmt = (131 separator132 >> make_separator133 )134135 # edge statement::136 # A -> B;137 # C -> D {138 # D -> E;139 # }140 #141 edge_block = forward_decl()142 edge_relation = (143 op('<<--') | op('<--') | op('<<-') | op('<-') |144 op('->') | op('->>') | op('-->') | op('-->>') |145 op('=>')146 )147 edge_stmt = (148 _id +149 edge_relation +150 _id +151 many(edge_relation + _id) +152 option_list +153 maybe(edge_block)154 >> create_mapper(Edge)155 )156 edge_block_inline_stmt_list = (157 many(edge_stmt + skip(maybe(op(';'))) | separator_stmt)158 )159 edge_block.define(160 op_('{') +161 edge_block_inline_stmt_list +162 op_('}')163 >> Statements164 )165166 # group statement::167 # group {168 # A;169 # }170 #171 group_inline_stmt_list = (172 many((attribute_stmt | node_stmt) + skip(maybe(op(';'))))173 )174 group_stmt = (175 skip(keyword('group')) +176 skip(maybe(_id)) +177 op_('{') +178 group_inline_stmt_list +179 op_('}')180 >> Group181 )182183 # combined fragment (alt, loop) statement::184 # loop {185 # A -> B;186 # }187 # alt {188 # D -> E;189 # }190 #191 fragment_stmt = forward_decl()192 fragment_inline_stmt = (193 attribute_stmt |194 fragment_stmt |195 edge_stmt |196 node_stmt197 )198 fragment_inline_stmt_list = (199 many(fragment_inline_stmt + skip(maybe(op(';'))))200 )201 fragment_types = (202 keyword('alt') | keyword('loop')203 )204 fragment_stmt.define(205 fragment_types +206 maybe(_id) +207 op_('{') +208 fragment_inline_stmt_list +209 op_('}')210 >> create_mapper(Fragment)211 )212213 # extension statement (class, plugin)::214 # class red [color = red];215 # plugin attributes [name = Name];216 #217 extension_stmt = (218 (keyword('class') | keyword('plugin')) +219 _id +220 option_list221 >> create_mapper(Extension)222 )223224 # diagram statement::225 # seqdiag {226 # A -> B;227 # }228 #229 diagram_id = (230 (keyword('diagram') | keyword('seqdiag')) +231 maybe(_id)232 >> list233 )234 diagram_inline_stmt = (235 extension_stmt |236 attribute_stmt |237 fragment_stmt |238 group_stmt |239 edge_stmt |240 separator_stmt |241 node_stmt242 )243 diagram_inline_stmt_list = (244 many(diagram_inline_stmt + skip(maybe(op(';'))))245 )246 diagram = (247 maybe(diagram_id) +248 op_('{') +249 diagram_inline_stmt_list +250 op_('}')251 >> create_mapper(Diagram)252 )253 dotfile = diagram + skip(finished)254255 return dotfile.parse(seq)256257258def sort_tree(tree):259 def weight(node):260 if isinstance(node, (Attr, Extension)):261 return 1262 else:263 return 2264265 if hasattr(tree, 'stmts'):266 tree.stmts.sort(key=lambda x: weight(x))267 for stmt in tree.stmts:268 sort_tree(stmt)269270 return tree271272273def parse_string(string):274 try:275 tree = parse(tokenize(string))276 return sort_tree(tree)277 except LexerError as e:278 message = "Got unexpected token at line %d column %d" % e.place279 raise ParseException(message)280 except Exception as e:281 raise ParseException(str(e))282283284def parse_file(path):285 code = io.open(path, 'r', encoding='utf-8-sig').read()286 return parse_string(code)