mls-parser 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mls_parser/__init__.py +1 -0
- mls_parser/__main__.py +67 -0
- mls_parser/exceptions.py +100 -0
- mls_parser/layout.peg +109 -0
- mls_parser/layout_parser.py +118 -0
- mls_parser/layout_visitor.py +510 -0
- mls_parser/log.conf +42 -0
- mls_parser-0.0.2.dist-info/LICENSE +21 -0
- mls_parser-0.0.2.dist-info/METADATA +94 -0
- mls_parser-0.0.2.dist-info/RECORD +13 -0
- mls_parser-0.0.2.dist-info/WHEEL +5 -0
- mls_parser-0.0.2.dist-info/entry_points.txt +2 -0
- mls_parser-0.0.2.dist-info/top_level.txt +1 -0
mls_parser/__init__.py
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
version = "0.0.2"
|
mls_parser/__main__.py
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
"""
|
2
|
+
__main__.py
|
3
|
+
|
4
|
+
Layout Model Sheet Parser
|
5
|
+
"""
|
6
|
+
|
7
|
+
# System
|
8
|
+
import logging
|
9
|
+
import logging.config
|
10
|
+
import sys
|
11
|
+
import argparse
|
12
|
+
from pathlib import Path
|
13
|
+
|
14
|
+
# Parser
|
15
|
+
from mls_parser import version
|
16
|
+
from mls_parser.layout_parser import LayoutParser
|
17
|
+
|
18
|
+
_logpath = Path("mls_parser.log")
|
19
|
+
|
20
|
+
def get_logger():
|
21
|
+
"""Initiate the logger"""
|
22
|
+
log_conf_path = Path(__file__).parent / 'log.conf' # Logging configuration is in this file
|
23
|
+
logging.config.fileConfig(fname=log_conf_path, disable_existing_loggers=False)
|
24
|
+
return logging.getLogger(__name__) # Create a logger for this module
|
25
|
+
|
26
|
+
# Configure the expected parameters and actions for the argparse module
|
27
|
+
def parse(cl_input):
|
28
|
+
"""
|
29
|
+
The command line interface is for diagnostic purposes
|
30
|
+
|
31
|
+
:param cl_input:
|
32
|
+
:return:
|
33
|
+
"""
|
34
|
+
parser = argparse.ArgumentParser(description='Model layout parser')
|
35
|
+
parser.add_argument('cmfile', nargs='?', action='store',
|
36
|
+
help='Model layout file name with .mls extension')
|
37
|
+
parser.add_argument('-D', '--debug', action='store_true',
|
38
|
+
help='Debug mode'),
|
39
|
+
parser.add_argument('-V', '--version', action='store_true',
|
40
|
+
help='Print the current version of parser')
|
41
|
+
return parser.parse_args(cl_input)
|
42
|
+
|
43
|
+
|
44
|
+
def main():
|
45
|
+
# Start logging
|
46
|
+
logger = get_logger()
|
47
|
+
logger.info(f'Model layout parser version: {version}')
|
48
|
+
|
49
|
+
# Parse the command line args
|
50
|
+
args = parse(sys.argv[1:])
|
51
|
+
|
52
|
+
if args.version:
|
53
|
+
# Just print the version and quit
|
54
|
+
print(f'Model layout parser version: {version}')
|
55
|
+
sys.exit(0)
|
56
|
+
|
57
|
+
if args.cmfile:
|
58
|
+
fpath = Path(args.cmfile)
|
59
|
+
d = args.debug
|
60
|
+
result = LayoutParser.parse_file(file_input=fpath, debug=d)
|
61
|
+
|
62
|
+
logger.info("No problemo") # We didn't die on an exception, basically
|
63
|
+
print("\nNo problemo")
|
64
|
+
|
65
|
+
|
66
|
+
if __name__ == "__main__":
|
67
|
+
main()
|
mls_parser/exceptions.py
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
"""
|
2
|
+
exceptions.py – Model layout parser specific exceptions
|
3
|
+
"""
|
4
|
+
|
5
|
+
# Every error should have the same format
|
6
|
+
# with a standard prefix and postfix defined here
|
7
|
+
pre = "\nModel layout parser: ["
|
8
|
+
post = "]"
|
9
|
+
|
10
|
+
|
11
|
+
class MLSException(Exception):
|
12
|
+
pass
|
13
|
+
|
14
|
+
class MLSUserInputException(MLSException):
|
15
|
+
pass
|
16
|
+
|
17
|
+
class MLSIOException(MLSException):
|
18
|
+
pass
|
19
|
+
|
20
|
+
class LayoutParseError(MLSUserInputException):
|
21
|
+
def __init__(self, model_file, e):
|
22
|
+
self.model_file = model_file
|
23
|
+
self.e = e
|
24
|
+
|
25
|
+
def __str__(self):
|
26
|
+
return f'{pre}Parse error in layout "{self.model_file}"\n\t{self.e}"{post}'
|
27
|
+
|
28
|
+
class LayoutInputFileOpen(MLSIOException):
|
29
|
+
def __init__(self, path):
|
30
|
+
self.path = path
|
31
|
+
|
32
|
+
def __str__(self):
|
33
|
+
return f'{pre}Parser cannot open this layout file: "{self.path}"{post}'
|
34
|
+
|
35
|
+
class LayoutInputFileEmpty(MLSIOException):
|
36
|
+
def __init__(self, path):
|
37
|
+
self.path = path
|
38
|
+
|
39
|
+
def __str__(self):
|
40
|
+
return f'{pre}For some reason, nothing was read from the layout file: "{self.path}"{post}'
|
41
|
+
|
42
|
+
class LayoutGrammarFileOpen(MLSIOException):
|
43
|
+
def __init__(self, path):
|
44
|
+
self.path = path
|
45
|
+
|
46
|
+
def __str__(self):
|
47
|
+
return f'{pre}Parser cannot open this layout grammar file: "{self.path}"{post}'
|
48
|
+
|
49
|
+
class MultipleFloatsInSameStraightConnector(MLSException):
|
50
|
+
def __init__(self, name):
|
51
|
+
self.name = name
|
52
|
+
|
53
|
+
def __str__(self):
|
54
|
+
return f'{pre}Straight connector "{self.name}" has two floating anchors (*). Specify one.{post}'
|
55
|
+
|
56
|
+
class MultipleFloatsInSameBranch(MLSException):
|
57
|
+
def __init__(self, branch):
|
58
|
+
self.branch = branch
|
59
|
+
|
60
|
+
def __str__(self):
|
61
|
+
return f'{pre}There may be at most one floating anchor (*) per branch: "{self.branch}"{post}'
|
62
|
+
|
63
|
+
class ConflictingGraftFloat(MLSException):
|
64
|
+
def __init__(self, stem):
|
65
|
+
self.stem = stem
|
66
|
+
|
67
|
+
def __str__(self):
|
68
|
+
return f'{pre}A floating anchor(*) may not graft (>, >>): "{self.stem}"{post}'
|
69
|
+
|
70
|
+
class MultipleGraftsInSameBranch(MLSException):
|
71
|
+
def __init__(self, branch):
|
72
|
+
self.branch = branch
|
73
|
+
|
74
|
+
def __str__(self):
|
75
|
+
return f'{pre}There may be at most one graft (>, >>) per branch: "{self.branch}"{post}'
|
76
|
+
|
77
|
+
class TrunkLeafGraftConflict(MLSException):
|
78
|
+
def __str__(self):
|
79
|
+
return f'{pre}Leaf may not graft locally (>) if Trunk is grafting (>) {post}'
|
80
|
+
|
81
|
+
class ExternalLocalGraftConflict(MLSException):
|
82
|
+
def __init__(self, branch):
|
83
|
+
self.branch = branch
|
84
|
+
|
85
|
+
def __str__(self):
|
86
|
+
return f'{pre}Branch has local (>) graft with conflicting external graft (>>) in preceding branch: "{self.branch}"{post}'
|
87
|
+
|
88
|
+
class ExternalGraftOnLastBranch(MLSException):
|
89
|
+
def __init__(self, branch):
|
90
|
+
self.branch = branch
|
91
|
+
|
92
|
+
def __str__(self):
|
93
|
+
return f'{pre}Last branch in tree layout has a superfluous external (>>) graft: "{self.branch}"{post}'
|
94
|
+
|
95
|
+
class GraftRutBranchConflict(MLSException):
|
96
|
+
def __init__(self, branch):
|
97
|
+
self.branch = branch
|
98
|
+
|
99
|
+
def __str__(self):
|
100
|
+
return f'{pre}A rut branch, with (: Ln[R+/-n]), may not include a local graft(>): "{self.branch}"{post}'
|
mls_parser/layout.peg
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
// Flatland Model Layout Arpeggio Clean Peg Grammar
|
2
|
+
|
3
|
+
// This grammar expresses the geometric layout of nodes and connectors and where
|
4
|
+
// connectors attach to node faces on a diagram. It does not specify any meaning
|
5
|
+
// associated with the nodes and connectors. Node and connector names are ascribed
|
6
|
+
// in their model files, but no other information in the model files is referenced
|
7
|
+
// in this grammar.
|
8
|
+
|
9
|
+
// You can draw unconnected nodes, connected nodes or a blank sheet
|
10
|
+
diagram_layout = EOL* layout_spec ((node_block connector_block) / (node_block))? EOF
|
11
|
+
|
12
|
+
// Layout spec
|
13
|
+
layout_spec = (diagram notation color? presentation sheet padding? orientation frame? frame_presentation?)# // Each of these in any order
|
14
|
+
|
15
|
+
// Diagram
|
16
|
+
diagram = "diagram" SP+ name EOL* // The type of diagram to draw
|
17
|
+
notation = "notation" SP+ name EOL* // The notation to use in the diagram
|
18
|
+
color = "color" SP+ name EOL* // The type of diagram to draw
|
19
|
+
presentation = "presentation" SP+ name EOL* // The presentation style for drawing
|
20
|
+
sheet = "sheet" SP+ name EOL* // The sheet size name
|
21
|
+
padding = "padding" (tpad? bpad? lpad? rpad?)# EOL*
|
22
|
+
tpad = SP+ 't'number
|
23
|
+
bpad = SP+ 'b'number
|
24
|
+
lpad = SP+ 'l'number
|
25
|
+
rpad = SP+ 'r'number
|
26
|
+
orientation = "orientation" SP+ ("portrait" / "landscape") EOL* // The sheet orientation
|
27
|
+
frame = "frame" SP+ name EOL* // The frame name
|
28
|
+
frame_presentation = "frame_presentation" SP+ name EOL* // The presentation style for the frame
|
29
|
+
|
30
|
+
// Node
|
31
|
+
node_block = nodes_header node_spec+
|
32
|
+
nodes_header = "nodes" EOL*
|
33
|
+
node_spec = INDENT node_name wrap? (SP+ node_width_expansion)? (SP+ node_height_expansion)? SP+ node_placement (SP+ color_tag)? EOL*
|
34
|
+
node_name = name
|
35
|
+
node_width_expansion = number '%' // Increase the width of the node
|
36
|
+
node_height_expansion = comp_height_expansion (SP+ comp_height_expansion)* // A node's height is the sum of its expanded compartments
|
37
|
+
comp_height_expansion = '[C' number ']' number '%' // Increase the height of a single compartment
|
38
|
+
node_placement = grid_place (SP+ ':' SP+ grid_place)* // One or more such locations
|
39
|
+
grid_place = node_loc (SP+ node_align)? // Location in grid where a node will appear
|
40
|
+
node_loc = span ',' span // Row and column
|
41
|
+
span = number '-' number / number
|
42
|
+
color_tag = '<' name '>'
|
43
|
+
|
44
|
+
// Connector
|
45
|
+
connector_block = connectors_header connector_layout+ // All
|
46
|
+
connectors_header = "connectors" EOL* // starts section where connector layout info is specified
|
47
|
+
connector_layout = INDENT cname_place? ( binary_layout / tree_layout / unary_layout ) EOL*
|
48
|
+
cname_place = dir? name wrap? bend? notch? csep // Side of connector axis and name of connector (since it is probably short)
|
49
|
+
bend = '.' number // Bend in connector where 1 is at the tstem side increasing to the pstem side
|
50
|
+
|
51
|
+
// Binary connector
|
52
|
+
binary_layout = tstem csep pstem tertiary_node? (csep paths)? // All layout info for a binary connector
|
53
|
+
tstem = stem_side // one stem in a binary connector (t and p are arbitary names)
|
54
|
+
pstem = stem_side // the opposite stem in a binary connector
|
55
|
+
tertiary_node = ',' SP+ node_face
|
56
|
+
paths = path (SP+ path)*
|
57
|
+
sname_place = dir wrap // Side of stem axis and number of text lines after wrapping in stem name text block
|
58
|
+
stem_side = (sname_place SP+)? node_face // Either the tstem or pstem layout
|
59
|
+
|
60
|
+
// Tree layout
|
61
|
+
tree_layout = trunk_face (SP+ branch)+ // All layout info for a tree connector
|
62
|
+
trunk_face = node_face ('>')? // May or may not graft the trunk branch with > symbol
|
63
|
+
branch = '{ ' leaf_faces (csep path)? ' }' // A branch is attached to one or more leaf faces
|
64
|
+
leaf_faces = leaf_face (', ' leaf_face)*
|
65
|
+
leaf_face = node_face ('>>' / '>' )? // graft its own branch or the next one
|
66
|
+
// Within a branch, one leaf may either graft that branch > or graft the subsequent branch >>
|
67
|
+
// See patterns 3 and 2 respectively in tech note tn.2 in the documentation folder for examples
|
68
|
+
// There are three ways to specify the path of a branch:
|
69
|
+
// 1) Interpolated - Compute position between opposing node faces (no specification)
|
70
|
+
// 2) Grafted - Line up branch with some face placement (a > on the trunk or some leaf face or a >> on a leaf face)
|
71
|
+
// 3) Rut - Run branch down a rut in a lane as specified by a path (path)
|
72
|
+
// A branch that is neither interpolated nor grafted has a path specified where it runs
|
73
|
+
|
74
|
+
// Unary connector
|
75
|
+
unary_layout = stem_side
|
76
|
+
|
77
|
+
// Face attachment
|
78
|
+
node_ref = name ('.' number)? // Optional additional placement of the same node
|
79
|
+
face = "t" / "b" / "l" / "r" // top, bottom, left or right node face
|
80
|
+
dir = "+" / "-" // direction of increasing coord values, up and to the right is positive
|
81
|
+
csep = SP+ ':' SP+ // argument separator
|
82
|
+
anchor = notch / '*'
|
83
|
+
node_face = face anchor? "|" node_ref // Where a stem attaches to a node face
|
84
|
+
|
85
|
+
// Alignment
|
86
|
+
valign = ">" ("top" / "bottom")
|
87
|
+
halign = ">" ("right" / "left")
|
88
|
+
node_align = valign SP? halign / halign SP? valign / valign / halign
|
89
|
+
// A notch is based on a system where 0 means 'centered' and deviations from the center are proportional
|
90
|
+
// increments, each the same size, distant from the center in the positive or negative direction
|
91
|
+
// Positive is always in the increasing coordinate direction, up or to the right
|
92
|
+
notch = '0' / ('+' / '-') number // A unit of alignment, center, or a positive or negative integer
|
93
|
+
path = 'L' number ('R' notch)? // Lane and rut, assume rut 0 if R not specified
|
94
|
+
|
95
|
+
// Elements
|
96
|
+
wrap = '/' number // Number of lines to wrap an associated string
|
97
|
+
number = r'[1-9][0-9]*' // Always a positive integer
|
98
|
+
|
99
|
+
// Model element names
|
100
|
+
// A name is one or more words separated by a delimiter
|
101
|
+
delim = r'[ _]' // Delimiter to separate words in a name
|
102
|
+
word = r'[A-Za-z][A-Za-z0-9]*' // String of alpahnumeric text with no whitespace starting with alpha char
|
103
|
+
name = word (delim word)* // Sequence of delimited words forming a name
|
104
|
+
|
105
|
+
// Whitespace and comments
|
106
|
+
INDENT = " " // For clarity
|
107
|
+
EOL = SP* COMMENT? '\n' // end of line: Comments, blank lines, whitespace we can omit from the parser result
|
108
|
+
COMMENT = '//' r'.*' // Comment slashes don't work if included in the regular expression for some reason
|
109
|
+
SP = " "
|
@@ -0,0 +1,118 @@
|
|
1
|
+
""" layout_parser.py – Parse layout file """
|
2
|
+
|
3
|
+
from mls_parser.exceptions import LayoutGrammarFileOpen, LayoutInputFileOpen, LayoutInputFileEmpty, LayoutParseError
|
4
|
+
from mls_parser.layout_visitor import LayoutVisitor
|
5
|
+
from arpeggio import visit_parse_tree, NoMatch
|
6
|
+
from arpeggio.cleanpeg import ParserPEG
|
7
|
+
import os
|
8
|
+
from pathlib import Path
|
9
|
+
|
10
|
+
|
11
|
+
class LayoutParser:
|
12
|
+
"""
|
13
|
+
Parses a model layout file using the arpeggio parser generator
|
14
|
+
|
15
|
+
Attributes
|
16
|
+
|
17
|
+
- grammar_file -- (class based) Name of the system file defining the Executable UML grammar
|
18
|
+
- root_rule_name -- (class based) Name of the top level grammar element found in grammar file
|
19
|
+
- debug -- debug flag (used to set arpeggio parser mode)
|
20
|
+
- model_grammar -- The model grammar text read from the system grammar file
|
21
|
+
- model_text -- The input model text read from the user supplied text file
|
22
|
+
"""
|
23
|
+
|
24
|
+
debug = False # by default
|
25
|
+
mls_grammar = None # We haven't read it in yet
|
26
|
+
model_text = None # User will provide this in a file
|
27
|
+
model_file = None # The user supplied xcm file path
|
28
|
+
|
29
|
+
root_rule_name = 'diagram_layout' # The required name of the highest level parse element
|
30
|
+
|
31
|
+
# Useful paths within the project
|
32
|
+
src_path = Path(__file__).parent.parent # Path to src folder
|
33
|
+
module_path = src_path / 'mls_parser'
|
34
|
+
grammar_path = module_path # The grammar files are all here
|
35
|
+
cwd = Path.cwd()
|
36
|
+
diagnostics_path = cwd / 'diagnostics' # All parser diagnostic output goes here
|
37
|
+
|
38
|
+
# Files
|
39
|
+
grammar_file = grammar_path / "layout.peg" # We parse using this peg grammar
|
40
|
+
grammar_model_pdf = diagnostics_path / "layout.pdf"
|
41
|
+
parse_tree_pdf = diagnostics_path / "layout_parse_tree.pdf"
|
42
|
+
parse_tree_dot = cwd / f"{root_rule_name}_parse_tree.dot"
|
43
|
+
parser_model_dot = cwd / f"{root_rule_name}_peg_parser_model.dot"
|
44
|
+
|
45
|
+
pg_tree_dot = cwd / "peggrammar_parse_tree.dot"
|
46
|
+
pg_model_dot = cwd / "peggrammar_parser_model.dot"
|
47
|
+
pg_tree_pdf = diagnostics_path / "peggrammar_parse_tree.pdf"
|
48
|
+
pg_model_pdf = diagnostics_path / "peggrammar_parser_model.pdf"
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def parse_file(cls, file_input: Path, debug=False):
|
52
|
+
"""
|
53
|
+
|
54
|
+
:param file_input: class model file to read
|
55
|
+
:param debug: Run parser in debug mode
|
56
|
+
"""
|
57
|
+
cls.model_file = file_input
|
58
|
+
cls.debug = debug
|
59
|
+
if debug:
|
60
|
+
# If there is no diagnostics directory, create one in the current working directory
|
61
|
+
cls.diagnostics_path.mkdir(parents=False, exist_ok=True)
|
62
|
+
|
63
|
+
# Read the class model file
|
64
|
+
try:
|
65
|
+
cls.model_text = open(file_input, 'r').read() + '\n'
|
66
|
+
# At least one newline at end simplifies grammar rules
|
67
|
+
except OSError as e:
|
68
|
+
raise LayoutInputFileOpen(file_input)
|
69
|
+
|
70
|
+
if not cls.model_text:
|
71
|
+
raise LayoutInputFileEmpty(file_input)
|
72
|
+
|
73
|
+
return cls.parse()
|
74
|
+
|
75
|
+
@classmethod
|
76
|
+
# def parse(cls) -> Subsystem_a:
|
77
|
+
def parse(cls):
|
78
|
+
"""
|
79
|
+
Parse the model file and return the content
|
80
|
+
:return: The abstract syntax tree content of interest
|
81
|
+
"""
|
82
|
+
# Read the grammar file
|
83
|
+
try:
|
84
|
+
cls.mls_grammar = open(LayoutParser.grammar_file, 'r').read()
|
85
|
+
except OSError as e:
|
86
|
+
raise LayoutGrammarFileOpen(LayoutParser.grammar_file)
|
87
|
+
|
88
|
+
# Create an arpeggio parser for our model grammar that does not eliminate whitespace
|
89
|
+
# We interpret newlines and indents in our grammar, so whitespace must be preserved
|
90
|
+
parser = ParserPEG(cls.mls_grammar, LayoutParser.root_rule_name, skipws=False, debug=cls.debug)
|
91
|
+
if cls.debug:
|
92
|
+
# Transform dot files into pdfs
|
93
|
+
# os.system(f'dot -Tpdf {cls.pg_tree_dot} -o {cls.pg_tree_pdf}')
|
94
|
+
# os.system(f'dot -Tpdf {cls.pg_model_dot} -o {cls.pg_model_pdf}')
|
95
|
+
os.system(f'dot -Tpdf {cls.parser_model_dot} -o {cls.grammar_model_pdf}')
|
96
|
+
cls.parser_model_dot.unlink(True)
|
97
|
+
cls.pg_tree_dot.unlink(True)
|
98
|
+
cls.pg_model_dot.unlink(True)
|
99
|
+
|
100
|
+
# Now create an abstract syntax tree from our model text
|
101
|
+
try:
|
102
|
+
parse_tree = parser.parse(cls.model_text)
|
103
|
+
except NoMatch as e:
|
104
|
+
raise LayoutParseError(cls.model_file.name, e) from None
|
105
|
+
|
106
|
+
# Transform that into a result that is better organized with grammar artifacts filtered out
|
107
|
+
result = visit_parse_tree(parse_tree, LayoutVisitor(debug=cls.debug))
|
108
|
+
|
109
|
+
if cls.debug:
|
110
|
+
# Transform dot files into pdfs
|
111
|
+
os.system(f'dot -Tpdf {cls.parse_tree_dot} -o {cls.parse_tree_pdf}')
|
112
|
+
# Delete dot files since we are only interested in the generated PDFs
|
113
|
+
# Comment this part out if you want to retain the dot files
|
114
|
+
cls.parse_tree_dot.unlink(True)
|
115
|
+
|
116
|
+
return result
|
117
|
+
|
118
|
+
|
@@ -0,0 +1,510 @@
|
|
1
|
+
""" layout_visitor.py """
|
2
|
+
from collections import namedtuple
|
3
|
+
from enum import Enum
|
4
|
+
from arpeggio import PTNodeVisitor
|
5
|
+
from mls_parser.exceptions import (ConflictingGraftFloat, MultipleGraftsInSameBranch, ExternalLocalGraftConflict,
|
6
|
+
MultipleFloatsInSameBranch, TrunkLeafGraftConflict, GraftRutBranchConflict,
|
7
|
+
ExternalGraftOnLastBranch)
|
8
|
+
|
9
|
+
|
10
|
+
DiagramLayout = namedtuple('DiagramLayout', 'layout_spec node_placement connector_placement')
|
11
|
+
LayoutSpec = namedtuple('LayoutSpec', 'dtype pres notation color sheet orientation frame frame_presentation padding')
|
12
|
+
|
13
|
+
class NodeFace(Enum):
|
14
|
+
"""
|
15
|
+
Values are multiplied by absolute distance to get an x or y coordinate.
|
16
|
+
"""
|
17
|
+
TOP = 0
|
18
|
+
BOTTOM = 1
|
19
|
+
RIGHT = 2
|
20
|
+
LEFT = 3
|
21
|
+
|
22
|
+
|
23
|
+
face_map = {'r': NodeFace.RIGHT, 'l': NodeFace.LEFT, 't': NodeFace.TOP, 'b': NodeFace.BOTTOM}
|
24
|
+
|
25
|
+
|
26
|
+
class LayoutVisitor(PTNodeVisitor):
|
27
|
+
"""
|
28
|
+
Organized in the same categories commented in the clean peg grammar file.
|
29
|
+
|
30
|
+
Some conventions:
|
31
|
+
|
32
|
+
- Comment each visit with parsing semantics
|
33
|
+
- Descriptive named variables if processing is required
|
34
|
+
- Use *node.rule_name* in case the rule name changes
|
35
|
+
- Combine values into dictionaries for stability, ease of interpretation and to avoid mistakes
|
36
|
+
- Assigining result to a variable that is returned for ease of debugging
|
37
|
+
"""
|
38
|
+
|
39
|
+
# Root
|
40
|
+
@classmethod
|
41
|
+
def visit_diagram_layout(cls, node, children) -> DiagramLayout:
|
42
|
+
"""
|
43
|
+
EOL* layout_spec ((node_block connector_block) / (node_block))? EOF
|
44
|
+
|
45
|
+
Root Rule
|
46
|
+
"""
|
47
|
+
# Organize the input into a layout spec, a node dictionary, and an optional connector block
|
48
|
+
lspec = children.results['layout_spec'][0]
|
49
|
+
node_pdict = {}
|
50
|
+
for n in children.results['node_block'][0]:
|
51
|
+
dup_num = n.get('duplicate')
|
52
|
+
key = n['node_name'] if not dup_num else f"{n['node_name']}_{dup_num}"
|
53
|
+
node_pdict[key] = n
|
54
|
+
|
55
|
+
if 'connector_block' in children.results:
|
56
|
+
rc = children.results['connector_block'][0]
|
57
|
+
else:
|
58
|
+
rc = None
|
59
|
+
return DiagramLayout(layout_spec=lspec, node_placement=node_pdict, connector_placement=rc)
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def visit_layout_spec(cls, node, children) -> LayoutSpec:
|
63
|
+
"""
|
64
|
+
(diagram notation color? presentation sheet padding? orientation frame? frame_presentation?)#
|
65
|
+
|
66
|
+
Layout specification
|
67
|
+
"""
|
68
|
+
ld = children.results
|
69
|
+
frame = ld.get('frame')
|
70
|
+
color = ld.get('color', ['white'])
|
71
|
+
frame_presentation = ld.get('frame_presentation')
|
72
|
+
padding = ld.get('padding')
|
73
|
+
lspec = LayoutSpec(dtype=ld['diagram'][0], notation=ld['notation'][0], pres=ld['presentation'][0],
|
74
|
+
orientation=ld['orientation'][0], sheet=ld['sheet'][0],
|
75
|
+
color=color[0],
|
76
|
+
frame=None if not frame else frame[0],
|
77
|
+
# frame_presentation not relevant if no frame
|
78
|
+
frame_presentation=None if not frame else frame_presentation[0],
|
79
|
+
padding=None if not padding else padding[0])
|
80
|
+
return lspec
|
81
|
+
|
82
|
+
# Diagram
|
83
|
+
@classmethod
|
84
|
+
def visit_diagram(cls, node, children):
|
85
|
+
"""Keyword argument"""
|
86
|
+
return children[0]
|
87
|
+
|
88
|
+
@classmethod
|
89
|
+
def visit_notation(cls, node, children):
|
90
|
+
"""Keyword argument"""
|
91
|
+
return children[0]
|
92
|
+
|
93
|
+
@classmethod
|
94
|
+
def visit_color(cls, node, children):
|
95
|
+
"""Keyword argument"""
|
96
|
+
return children[0]
|
97
|
+
|
98
|
+
@classmethod
|
99
|
+
def visit_presentation(cls, node, children):
|
100
|
+
"""Keyword argument"""
|
101
|
+
return children[0]
|
102
|
+
|
103
|
+
@classmethod
|
104
|
+
def visit_sheet(cls, node, children):
|
105
|
+
"""Keyword argument"""
|
106
|
+
return children[0]
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def visit_padding(cls, node, children):
|
110
|
+
"""Keyword argument"""
|
111
|
+
d = {k: v for c in children for k, v in c.items()}
|
112
|
+
return d
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def visit_tpad(cls, node, children):
|
116
|
+
return {'top': children[0]}
|
117
|
+
|
118
|
+
@classmethod
|
119
|
+
def visit_bpad(cls, node, children):
|
120
|
+
return {'bottom': children[0]}
|
121
|
+
|
122
|
+
@classmethod
|
123
|
+
def visit_lpad(cls, node, children):
|
124
|
+
return {'left': children[0]}
|
125
|
+
|
126
|
+
@classmethod
|
127
|
+
def visit_rpad(cls, node, children):
|
128
|
+
return {'right': children[0]}
|
129
|
+
|
130
|
+
@classmethod
|
131
|
+
def visit_orientation(cls, node, children):
|
132
|
+
"""Keyword argument"""
|
133
|
+
return children[0]
|
134
|
+
|
135
|
+
@classmethod
|
136
|
+
def visit_frame(cls, node, children):
|
137
|
+
"""Keyword argument"""
|
138
|
+
return children[0]
|
139
|
+
|
140
|
+
@classmethod
|
141
|
+
def visit_frame_presentation(cls, node, children):
|
142
|
+
"""Keyword argument"""
|
143
|
+
return children[0]
|
144
|
+
|
145
|
+
|
146
|
+
# Node
|
147
|
+
@classmethod
|
148
|
+
def visit_node_block(cls, node, children):
|
149
|
+
""" nodes_header node_spec+ """
|
150
|
+
return children
|
151
|
+
|
152
|
+
@classmethod
|
153
|
+
def visit_node_spec(cls, node, children):
|
154
|
+
"""
|
155
|
+
INDENT node_name wrap? (SP+ node_width_expansion)? (SP+ node_height_expansion)?
|
156
|
+
SP+ node_placement (SP+ color_tag)? EOL*
|
157
|
+
"""
|
158
|
+
ditems = {k: v for c in children for k, v in c.items()}
|
159
|
+
return ditems
|
160
|
+
|
161
|
+
@classmethod
|
162
|
+
def visit_node_name(cls, node, children):
|
163
|
+
""" name """
|
164
|
+
return {node.rule_name: ''.join(children)}
|
165
|
+
|
166
|
+
@classmethod
|
167
|
+
def visit_node_width_expansion(cls, node, children):
|
168
|
+
""" number '%' """
|
169
|
+
# Convert percentage to ratio ensuring ratio is positive
|
170
|
+
user_percent = children[0]
|
171
|
+
ratio = 0 if user_percent < 0 else round(user_percent / 100, 2)
|
172
|
+
return {node.rule_name: ratio}
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
def visit_node_height_expansion(cls, node, children):
|
176
|
+
""" number '%' """
|
177
|
+
d = {k: v for k, v in children}
|
178
|
+
return {node.rule_name: d}
|
179
|
+
|
180
|
+
@classmethod
|
181
|
+
def visit_comp_height_expansion(cls, node, children):
|
182
|
+
""" '[C' number ']' number '%' """
|
183
|
+
# Convert percentage to ratio ensuring ratio is positive
|
184
|
+
user_percent = children[1]
|
185
|
+
ratio = 0 if user_percent < 0 else round(user_percent / 100, 2)
|
186
|
+
return [children[0], ratio]
|
187
|
+
|
188
|
+
@classmethod
|
189
|
+
def visit_node_placement(cls, node, children):
|
190
|
+
""" grid_place (SP+ ':' SP+ grid_place)* """
|
191
|
+
return {'placements': children}
|
192
|
+
|
193
|
+
@classmethod
|
194
|
+
def visit_grid_place(cls, node, children):
|
195
|
+
""" node_loc (SP+ node_align)? """
|
196
|
+
d = {k: v for c in children for k, v in c.items()}
|
197
|
+
return d
|
198
|
+
|
199
|
+
@classmethod
|
200
|
+
def visit_node_loc(cls, node, children):
|
201
|
+
"""
|
202
|
+
span ',' span
|
203
|
+
|
204
|
+
row and column
|
205
|
+
"""
|
206
|
+
return {node.rule_name: children}
|
207
|
+
|
208
|
+
@classmethod
|
209
|
+
def visit_span(cls, node, children):
|
210
|
+
"""
|
211
|
+
number '-' number / number
|
212
|
+
|
213
|
+
for example:
|
214
|
+
3-5 is returned as span [3,5]
|
215
|
+
3 is returned as span [3]
|
216
|
+
"""
|
217
|
+
return children
|
218
|
+
|
219
|
+
@classmethod
|
220
|
+
def visit_color_tag(cls, node, children):
|
221
|
+
""" '<' name '>' """
|
222
|
+
return {node.rule_name: children[0]}
|
223
|
+
|
224
|
+
# Connector
|
225
|
+
@classmethod
|
226
|
+
def visit_connector_block(cls, node, children):
|
227
|
+
return children
|
228
|
+
|
229
|
+
@classmethod
|
230
|
+
def visit_connector_layout(cls, node, children):
|
231
|
+
"""All layout info for the connector"""
|
232
|
+
# Combine all child dictionaries
|
233
|
+
items = {k: v for d in children for k, v in d.items()}
|
234
|
+
items['bend'] = items.get('bend', 1) # No bend supplied, assume 1
|
235
|
+
return items
|
236
|
+
|
237
|
+
@classmethod
|
238
|
+
def visit_cname_place(cls, node, children):
|
239
|
+
"""Name of connector and the side of the connector axis where it is placed"""
|
240
|
+
# If a value is supplied it will be a single time list, so extract with [0]
|
241
|
+
# If no value is supplied for an optional item, default must also be a single item list [default_value]
|
242
|
+
w = children.results.get('wrap')
|
243
|
+
wrap_value = 1 if not w else w[0]['wrap']
|
244
|
+
cplace = {'cname': children.results['name'][0], # Required component
|
245
|
+
'dir': children.results.get('dir', [1])[0], # many optional components with default values
|
246
|
+
'bend': children.results.get('bend', [1])[0],
|
247
|
+
'notch': children.results.get('notch', [0])[0],
|
248
|
+
'wrap': wrap_value,
|
249
|
+
}
|
250
|
+
return cplace
|
251
|
+
|
252
|
+
@classmethod
|
253
|
+
def visit_bend(cls, node, children):
|
254
|
+
"""Number of bend where cname appears"""
|
255
|
+
# return {node.rule_name: int(children[0])}
|
256
|
+
bend = int(children[0])
|
257
|
+
return bend
|
258
|
+
|
259
|
+
# Binary connector
|
260
|
+
@classmethod
|
261
|
+
def visit_binary_layout(cls, node, children):
|
262
|
+
"""All layout info for the binary connector"""
|
263
|
+
# Combine all child dictionaries
|
264
|
+
items = {k: v for d in children for k, v in d.items()}
|
265
|
+
return items
|
266
|
+
|
267
|
+
@classmethod
|
268
|
+
def visit_tstem(cls, node, children):
|
269
|
+
"""T stem layout info"""
|
270
|
+
items = {k: v for d in children for k, v in d.items()}
|
271
|
+
items['anchor'] = items.get('anchor', 0)
|
272
|
+
tstem = {node.rule_name: items}
|
273
|
+
return tstem
|
274
|
+
|
275
|
+
@classmethod
|
276
|
+
def visit_pstem(cls, node, children):
|
277
|
+
"""P stem layout info"""
|
278
|
+
items = {k: v for d in children for k, v in d.items()}
|
279
|
+
items['anchor'] = items.get('anchor', 0)
|
280
|
+
pstem = {node.rule_name: items}
|
281
|
+
return pstem
|
282
|
+
|
283
|
+
@classmethod
|
284
|
+
def visit_tertiary_node(cls, node, children):
|
285
|
+
"""Tertiary node face and anchor"""
|
286
|
+
return {node.rule_name: children[0]}
|
287
|
+
|
288
|
+
@classmethod
|
289
|
+
def visit_paths(cls, node, children):
|
290
|
+
"""A sequence of one or more paths since a binary connector may bend multiple times"""
|
291
|
+
paths = {node.rule_name: [p['path'] for p in children]}
|
292
|
+
return paths
|
293
|
+
|
294
|
+
@classmethod
|
295
|
+
def visit_sname_place(cls, node, children):
|
296
|
+
"""Side of stem axis and number of lines in text block"""
|
297
|
+
d = {'stem_dir': children[0]} # initialize d
|
298
|
+
d.update(children[1]) # Add wrap key
|
299
|
+
return d
|
300
|
+
|
301
|
+
# Tree connector
|
302
|
+
@classmethod
|
303
|
+
def visit_tree_layout(cls, node, children):
|
304
|
+
"""All layout info for the tree connector"""
|
305
|
+
tlayout = children[0]
|
306
|
+
# If the trunk is grafting (>), there can be no other leaf stem grafting locally (>)
|
307
|
+
tlayout['branches'] = [c['branch'] for c in children[1:]]
|
308
|
+
tgraft = tlayout['trunk_face']['graft']
|
309
|
+
tleaves = tlayout['branches'][0]['leaf_faces']
|
310
|
+
if tgraft and [tleaves[n]['graft'] for n in tleaves if tleaves[n]['graft'] == 'local']:
|
311
|
+
raise TrunkLeafGraftConflict() # In the first branch (trunk branch) both trunk and some leaf are grafting
|
312
|
+
# For all offshoot (non-trunk) branches, there can be no local graft (>) if the preceding branch
|
313
|
+
# is grafting externally (>>). In other words, no more than one graft per branch.
|
314
|
+
for b, next_b in zip(tlayout['branches'], tlayout['branches'][1:]):
|
315
|
+
lf = b['leaf_faces']
|
316
|
+
external_graft = [lf[n]['graft'] for n in lf if lf[n]['graft'] == 'next']
|
317
|
+
if external_graft:
|
318
|
+
next_lf = next_b['leaf_faces']
|
319
|
+
if [next_lf[n]['graft'] for n in next_lf if next_lf[n]['graft'] == 'local']:
|
320
|
+
# External graft conflicts with local branch
|
321
|
+
raise ExternalLocalGraftConflict(set(lf.keys()))
|
322
|
+
# Check for dangling external graft in last branch
|
323
|
+
last_lf = tlayout['branches'][-1]['leaf_faces']
|
324
|
+
external_graft = [last_lf[n]['graft'] for n in last_lf if last_lf[n]['graft'] == 'next']
|
325
|
+
if external_graft:
|
326
|
+
raise ExternalGraftOnLastBranch(branch=set(last_lf.keys()))
|
327
|
+
return tlayout
|
328
|
+
|
329
|
+
@classmethod
|
330
|
+
def visit_trunk_face(cls, node, children):
|
331
|
+
"""A single trunk node at the top of the tree layout. It may or may not graft its branch."""
|
332
|
+
face = children[0] # Face, node and optional notch
|
333
|
+
graft = False if len(children) == 1 else True
|
334
|
+
if 'anchor' not in face.keys():
|
335
|
+
face['anchor'] = 0 # A Trunk face is never grafted, so an unspecified anchor is 0
|
336
|
+
tface = {'trunk_face': {'node_ref': face.pop('node_ref'), **face, 'graft': graft}}
|
337
|
+
return tface
|
338
|
+
|
339
|
+
@classmethod
|
340
|
+
def visit_branch(cls, node, children):
|
341
|
+
"""A tree connector branch"""
|
342
|
+
branch = {k: v for d in children for k, v in d.items()}
|
343
|
+
# Verify that this is either an interpolated, rut or graft branch and not an illegal mix
|
344
|
+
# If a path is specified it is a rut branch or if there is a local graft it is a grafted branch
|
345
|
+
# If both path and local graft are present in the same branch it is illegal
|
346
|
+
if branch.get('path', None): # Path specified, so there should be no local grafts in this branch
|
347
|
+
lf = branch['leaf_faces']
|
348
|
+
local_graft = [lf[n]['graft'] for n in lf if lf[n]['graft'] == 'local']
|
349
|
+
if local_graft:
|
350
|
+
raise GraftRutBranchConflict(branch=set(lf.keys()))
|
351
|
+
# Return dictionary of leaf faces and an optional path keyed to the local rule
|
352
|
+
return {node.rule_name: branch}
|
353
|
+
|
354
|
+
@classmethod
|
355
|
+
def visit_leaf_faces(cls, node, children):
|
356
|
+
"""Combine into dictionary of each leaf face indexed by node name"""
|
357
|
+
lfaces = {k: v for d in children for k, v in d.items()}
|
358
|
+
if len([lfaces[n]['graft'] for n in lfaces if lfaces[n]['graft']]) > 1:
|
359
|
+
raise MultipleGraftsInSameBranch(branch=set(lfaces.keys()))
|
360
|
+
if len([lfaces[n]['anchor'] for n in lfaces if lfaces[n]['anchor'] == 'float']) > 1:
|
361
|
+
raise MultipleFloatsInSameBranch(branch=set(lfaces.keys()))
|
362
|
+
return {node.rule_name: lfaces}
|
363
|
+
|
364
|
+
@classmethod
|
365
|
+
def visit_leaf_face(cls, node, children):
|
366
|
+
"""Branch face that may be a graft to its branch (local) or the (next) branch"""
|
367
|
+
lface = children[0]
|
368
|
+
graft = None
|
369
|
+
if 'anchor' not in lface.keys():
|
370
|
+
lface['anchor'] = 0 # If not float or a number, it must be zero in a tree layout
|
371
|
+
if len(children) == 2:
|
372
|
+
graft = 'local' if children[1] == '>' else 'next'
|
373
|
+
if lface['anchor'] == 'float' and graft:
|
374
|
+
raise ConflictingGraftFloat(stem=lface['name'])
|
375
|
+
lface['graft'] = graft
|
376
|
+
node_ref = lface.pop('node_ref')
|
377
|
+
# name = node_ref[0] if len(node_ref) == 1 else f"{node_ref[0]}_{node_ref[1]}"
|
378
|
+
return {node_ref: lface} # Single element dictionary indexed by the node name
|
379
|
+
|
380
|
+
# Unary connector
|
381
|
+
@classmethod
|
382
|
+
def visit_unary_layout(cls, node, children):
|
383
|
+
"""Unary layout which is just a single stem"""
|
384
|
+
return {'ustem': children[0]}
|
385
|
+
|
386
|
+
# Face attachment
|
387
|
+
@classmethod
|
388
|
+
def visit_node_ref(cls, node, children):
|
389
|
+
"""name number?"""
|
390
|
+
return children[0] if len(children) < 2 else f"{children[0]}_{children[1]}"
|
391
|
+
|
392
|
+
@classmethod
|
393
|
+
def visit_face(cls, node, children):
|
394
|
+
"""Face character"""
|
395
|
+
return face_map[node.value]
|
396
|
+
|
397
|
+
@classmethod
|
398
|
+
def visit_dir(cls, node, children):
|
399
|
+
"""Pos-neg direction"""
|
400
|
+
return 1 if node.value == '+' else -1
|
401
|
+
|
402
|
+
@classmethod
|
403
|
+
def visit_anchor(cls, node, children):
|
404
|
+
"""Anchor position"""
|
405
|
+
anchor = 'float' if children[0] == '*' else children[0]
|
406
|
+
return anchor
|
407
|
+
|
408
|
+
@classmethod
|
409
|
+
def visit_node_face(cls, node, children):
|
410
|
+
"""Where connector attaches to node face"""
|
411
|
+
nface = {k: v[0] for k, v in children.results.items()}
|
412
|
+
return nface
|
413
|
+
|
414
|
+
# Alignment
|
415
|
+
@classmethod
|
416
|
+
def visit_valign(cls, node, children):
|
417
|
+
"""Vertical alignment of noce in its cell"""
|
418
|
+
return {node.rule_name: children[0].upper()}
|
419
|
+
|
420
|
+
@classmethod
|
421
|
+
def visit_halign(cls, node, children):
|
422
|
+
"""Horizontal alignment of noce in its cell"""
|
423
|
+
return {node.rule_name: children[0].upper()}
|
424
|
+
|
425
|
+
@classmethod
|
426
|
+
def visit_node_align(cls, node, children):
|
427
|
+
"""Vertical and/or horizontal alignment of node in its cell"""
|
428
|
+
if len(children) == 2:
|
429
|
+
# Merge the two dictionaries
|
430
|
+
return {**children[0], **children[1]}
|
431
|
+
else:
|
432
|
+
return children[0]
|
433
|
+
|
434
|
+
@classmethod
|
435
|
+
def visit_notch(cls, node, children):
|
436
|
+
"""The digit 0 or a positive or negative number of notches"""
|
437
|
+
if children[0] == '0':
|
438
|
+
return 0
|
439
|
+
else:
|
440
|
+
scale = -1 if children[0] == '-' else 1
|
441
|
+
return int(children[1]) * scale
|
442
|
+
|
443
|
+
@classmethod
|
444
|
+
def visit_path(cls, node, children):
|
445
|
+
"""
|
446
|
+
'L' number ('R' notch)?
|
447
|
+
|
448
|
+
Lane and rut, assume rut 0 if R not specified
|
449
|
+
"""
|
450
|
+
# Rut is zero by default
|
451
|
+
path = {node.rule_name: {'lane': children[0], 'rut': children.results.get('notch', [0])[0]}}
|
452
|
+
return path # { path: { lane: <lane_num>, rut: <rut_displacement> }
|
453
|
+
|
454
|
+
# Elements
|
455
|
+
@classmethod
|
456
|
+
def visit_wrap(cls, node, children):
|
457
|
+
"""
|
458
|
+
'/' number
|
459
|
+
|
460
|
+
Number of lines to wrap an associated string
|
461
|
+
"""
|
462
|
+
return {node.rule_name: int(children[0])}
|
463
|
+
|
464
|
+
@classmethod
|
465
|
+
def visit_number(cls, node, children):
|
466
|
+
"""
|
467
|
+
r'[1-9][0-9]*'
|
468
|
+
|
469
|
+
Natural nummber
|
470
|
+
"""
|
471
|
+
return int(node.value)
|
472
|
+
|
473
|
+
@classmethod
|
474
|
+
def visit_name(cls, node, children):
|
475
|
+
"""
|
476
|
+
word (delim word)*
|
477
|
+
|
478
|
+
Sequence of delimited words forming a name
|
479
|
+
"""
|
480
|
+
name = ''.join(children)
|
481
|
+
return name
|
482
|
+
|
483
|
+
# Discarded whitespace and comments
|
484
|
+
@classmethod
|
485
|
+
def visit_LINEWRAP(cls, node, children):
|
486
|
+
"""
|
487
|
+
EOL SP*
|
488
|
+
|
489
|
+
end of line followed by optional INDENT on next line
|
490
|
+
"""
|
491
|
+
return None
|
492
|
+
|
493
|
+
@classmethod
|
494
|
+
def visit_EOL(cls, node, children):
|
495
|
+
"""
|
496
|
+
SP* COMMENT? '\n'
|
497
|
+
|
498
|
+
end of line: Spaces, Comments, blank lines, whitespace we can omit from the parser result
|
499
|
+
"""
|
500
|
+
return None
|
501
|
+
|
502
|
+
@classmethod
|
503
|
+
def visit_SP(cls, node, children):
|
504
|
+
"""
|
505
|
+
' '
|
506
|
+
|
507
|
+
Single space character (SP)
|
508
|
+
"""
|
509
|
+
return None
|
510
|
+
|
mls_parser/log.conf
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
[loggers]
|
2
|
+
keys=root,mlsParserLogger
|
3
|
+
|
4
|
+
[handlers]
|
5
|
+
keys=fileHandler, consoleHandler, consoleHandlerUser
|
6
|
+
|
7
|
+
[formatters]
|
8
|
+
keys=mlsParserFormatter, mlsParserFormatterUser
|
9
|
+
|
10
|
+
[logger_root]
|
11
|
+
level=DEBUG
|
12
|
+
handlers=fileHandler, consoleHandlerUser
|
13
|
+
|
14
|
+
[logger_mlsParserLogger]
|
15
|
+
level=DEBUG
|
16
|
+
handlers=fileHandler, consoleHandlerUser
|
17
|
+
qualname=mlsParserLogger
|
18
|
+
propagate=0
|
19
|
+
|
20
|
+
[handler_fileHandler]
|
21
|
+
class=FileHandler
|
22
|
+
level=DEBUG
|
23
|
+
formatter=mlsParserFormatter
|
24
|
+
args=('mls_parser.log', 'w')
|
25
|
+
|
26
|
+
[handler_consoleHandlerUser]
|
27
|
+
class=StreamHandler
|
28
|
+
level=WARNING
|
29
|
+
formatter=mlsParserFormatterUser
|
30
|
+
args=(sys.stderr,)
|
31
|
+
|
32
|
+
[handler_consoleHandler]
|
33
|
+
class=StreamHandler
|
34
|
+
level=WARNING
|
35
|
+
formatter=mlsParserFormatter
|
36
|
+
args=(sys.stderr,)
|
37
|
+
|
38
|
+
[formatter_mlsParserFormatter]
|
39
|
+
format=mlsParser parser: %(name)s - %(levelname)s - %(message)s
|
40
|
+
|
41
|
+
[formatter_mlsParserFormatterUser]
|
42
|
+
format=mlsParser parser: %(levelname)s - %(message)s
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019-2023 Leon Starr
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1,94 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: mls-parser
|
3
|
+
Version: 0.0.2
|
4
|
+
Summary: Flatland Model Layout Sheet Parser
|
5
|
+
Author-email: Leon Starr <leon_starr@modelint.com>
|
6
|
+
License: MIT License
|
7
|
+
|
8
|
+
Copyright (c) 2019-2023 Leon Starr
|
9
|
+
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
12
|
+
in the Software without restriction, including without limitation the rights
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
15
|
+
furnished to do so, subject to the following conditions:
|
16
|
+
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
18
|
+
copies or substantial portions of the Software.
|
19
|
+
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
26
|
+
SOFTWARE.
|
27
|
+
|
28
|
+
Project-URL: repository, https://github.com/modelint/mls-parser
|
29
|
+
Project-URL: documentation, https://github.com/modelint/mls-parser/wiki
|
30
|
+
Keywords: action language,executable uml,class model,mbse,xuml,xtuml,platform independent,sysml
|
31
|
+
Classifier: License :: OSI Approved :: MIT License
|
32
|
+
Classifier: Programming Language :: Python
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
34
|
+
Requires-Python: >=3.11
|
35
|
+
Description-Content-Type: text/markdown
|
36
|
+
License-File: LICENSE
|
37
|
+
Requires-Dist: Arpeggio
|
38
|
+
Requires-Dist: tomli; python_version < "3.13"
|
39
|
+
Provides-Extra: build
|
40
|
+
Requires-Dist: build; extra == "build"
|
41
|
+
Requires-Dist: twine; extra == "build"
|
42
|
+
Provides-Extra: dev
|
43
|
+
Requires-Dist: bump2version; extra == "dev"
|
44
|
+
Requires-Dist: pytest; extra == "dev"
|
45
|
+
|
46
|
+
# Model Layout Parser
|
47
|
+
|
48
|
+
Parses an *.mls file (Model Layout Style) to yield an abstract syntax tree using python named tuples
|
49
|
+
|
50
|
+
### Why you need this
|
51
|
+
|
52
|
+
You need to process an *.mls file to layout a model diagram
|
53
|
+
|
54
|
+
### Installation
|
55
|
+
|
56
|
+
Create or use a python 3.13+ environment. Then
|
57
|
+
|
58
|
+
% pip install mls-parser
|
59
|
+
|
60
|
+
At this point you can invoke the parser via the command line or from your python script.
|
61
|
+
|
62
|
+
#### From your python script
|
63
|
+
|
64
|
+
You need this import statement at a minimum:
|
65
|
+
|
66
|
+
from mls-parser.parser import LayoutParser
|
67
|
+
|
68
|
+
You can then specify a path as shown:
|
69
|
+
|
70
|
+
result = LayoutParser.parse_file(file_input=path_to_file, debug=False)
|
71
|
+
|
72
|
+
In either case, `result` will be a list of parsed class model elements. You may find the header of the `visitor.py`
|
73
|
+
file helpful in interpreting these results.
|
74
|
+
|
75
|
+
#### From the command line
|
76
|
+
|
77
|
+
This is not the intended usage scenario, but may be helpful for testing or exploration. Since the parser
|
78
|
+
may generate some diagnostic info you may want to create a fresh working directory and cd into it
|
79
|
+
first. From there...
|
80
|
+
|
81
|
+
% mls elevator.mls
|
82
|
+
|
83
|
+
The .xcm extension is not necessary, but the file must contain xcm text. See this repository's wiki for
|
84
|
+
more about the mls language. The grammar is defined in the [layout.peg](https://github.com/modelint/mls-parser/blob/main/src/mls_parser/layout.peg) file. (if the link breaks after I do some update to the code,
|
85
|
+
just browse through the code looking for the class_model.peg file, and let me know so I can fix it)
|
86
|
+
|
87
|
+
You can also specify a debug option like this:
|
88
|
+
|
89
|
+
% mls elevator.mls -D
|
90
|
+
|
91
|
+
This will create a scrall-diagnostics folder in your current working directory and deposite a coupel of PDFs defining
|
92
|
+
the parse of both the class model grammar: `class_model_tree.pdf` and your supplied text: `class_model.pdf`.
|
93
|
+
|
94
|
+
You should also see a file named `mls-parser.log` in a diagnostics directory within your working directory
|
@@ -0,0 +1,13 @@
|
|
1
|
+
mls_parser/__init__.py,sha256=zWVHf3DqIS6sYe780DU38lmTH_EXQUcBK32nOsBO7ao,18
|
2
|
+
mls_parser/__main__.py,sha256=NkowelWxc7cc6HRmcRRcWMdMShy0nKC4uO4Rm9EuB-g,1877
|
3
|
+
mls_parser/exceptions.py,sha256=hJx2OZPTEFOS4RqZJfH0meqV4ZzH7iBP7O9fXLV_McI,3059
|
4
|
+
mls_parser/layout.peg,sha256=BBAeHYR5hBgThUtCej5FmiphI3DlV_1kGJh4NgvnFLo,6019
|
5
|
+
mls_parser/layout_parser.py,sha256=fahdd8YeyJybJXZzz-lDoMi202Sv5UFJa0oGGmMiYcA,4854
|
6
|
+
mls_parser/layout_visitor.py,sha256=krXYZHIs0UkwJ446fYEmyLs448Iq5nPmOa2QRIh0rHI,18008
|
7
|
+
mls_parser/log.conf,sha256=EL_8Pn8A7gz1IV_AODmgtQdBsOREtmhaoomIuAxMSLk,867
|
8
|
+
mls_parser-0.0.2.dist-info/LICENSE,sha256=kL0xVrwl2i3Pk9mQXAVAPANCTaLGGOsoXgvqW7TBs20,1072
|
9
|
+
mls_parser-0.0.2.dist-info/METADATA,sha256=pEBz1x1dFI3geJSkm09J8nojcG0QykMgsJxsVf7B3Dk,4026
|
10
|
+
mls_parser-0.0.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
11
|
+
mls_parser-0.0.2.dist-info/entry_points.txt,sha256=HsIgu1_OJw84-8oMi7zGKznq7oQgpviMC_HL46vsjVo,49
|
12
|
+
mls_parser-0.0.2.dist-info/top_level.txt,sha256=s_pWxPSrMvumqwFDqPVWrajhZO2giOrZg9acHgn6h8Q,11
|
13
|
+
mls_parser-0.0.2.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
mls_parser
|