mls-parser 0.0.6__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- mls_parser-0.0.6/LICENSE +21 -0
- mls_parser-0.0.6/MANIFEST.in +2 -0
- mls_parser-0.0.6/PKG-INFO +94 -0
- mls_parser-0.0.6/README.md +49 -0
- mls_parser-0.0.6/pyproject.toml +30 -0
- mls_parser-0.0.6/setup.cfg +4 -0
- mls_parser-0.0.6/src/mls_parser/__init__.py +1 -0
- mls_parser-0.0.6/src/mls_parser/__main__.py +67 -0
- mls_parser-0.0.6/src/mls_parser/exceptions.py +100 -0
- mls_parser-0.0.6/src/mls_parser/layout.peg +109 -0
- mls_parser-0.0.6/src/mls_parser/layout_parser.py +118 -0
- mls_parser-0.0.6/src/mls_parser/layout_visitor.py +510 -0
- mls_parser-0.0.6/src/mls_parser/log.conf +42 -0
- mls_parser-0.0.6/src/mls_parser.egg-info/PKG-INFO +94 -0
- mls_parser-0.0.6/src/mls_parser.egg-info/SOURCES.txt +17 -0
- mls_parser-0.0.6/src/mls_parser.egg-info/dependency_links.txt +1 -0
- mls_parser-0.0.6/src/mls_parser.egg-info/entry_points.txt +2 -0
- mls_parser-0.0.6/src/mls_parser.egg-info/requires.txt +12 -0
- mls_parser-0.0.6/src/mls_parser.egg-info/top_level.txt +1 -0
mls_parser-0.0.6/LICENSE
ADDED
@@ -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.2
|
2
|
+
Name: mls-parser
|
3
|
+
Version: 0.0.6
|
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 Sheet Parser
|
47
|
+
|
48
|
+
Parses an *.mls file (Model Layout Sheet) 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.11+ 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 couple 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,49 @@
|
|
1
|
+
# Model Layout Sheet Parser
|
2
|
+
|
3
|
+
Parses an *.mls file (Model Layout Sheet) to yield an abstract syntax tree using python named tuples
|
4
|
+
|
5
|
+
### Why you need this
|
6
|
+
|
7
|
+
You need to process an *.mls file to layout a model diagram
|
8
|
+
|
9
|
+
### Installation
|
10
|
+
|
11
|
+
Create or use a python 3.11+ environment. Then
|
12
|
+
|
13
|
+
% pip install mls-parser
|
14
|
+
|
15
|
+
At this point you can invoke the parser via the command line or from your python script.
|
16
|
+
|
17
|
+
#### From your python script
|
18
|
+
|
19
|
+
You need this import statement at a minimum:
|
20
|
+
|
21
|
+
from mls_parser.parser import LayoutParser
|
22
|
+
|
23
|
+
You can then specify a path as shown:
|
24
|
+
|
25
|
+
result = LayoutParser.parse_file(file_input=path_to_file, debug=False)
|
26
|
+
|
27
|
+
In either case, `result` will be a list of parsed class model elements. You may find the header of the `visitor.py`
|
28
|
+
file helpful in interpreting these results.
|
29
|
+
|
30
|
+
#### From the command line
|
31
|
+
|
32
|
+
This is not the intended usage scenario, but may be helpful for testing or exploration. Since the parser
|
33
|
+
may generate some diagnostic info you may want to create a fresh working directory and cd into it
|
34
|
+
first. From there...
|
35
|
+
|
36
|
+
% mls elevator.mls
|
37
|
+
|
38
|
+
The .xcm extension is not necessary, but the file must contain xcm text. See this repository's wiki for
|
39
|
+
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,
|
40
|
+
just browse through the code looking for the class_model.peg file, and let me know so I can fix it)
|
41
|
+
|
42
|
+
You can also specify a debug option like this:
|
43
|
+
|
44
|
+
% mls elevator.mls -D
|
45
|
+
|
46
|
+
This will create a scrall-diagnostics folder in your current working directory and deposite a couple of PDFs defining
|
47
|
+
the parse of both the class model grammar: `class_model_tree.pdf` and your supplied text: `class_model.pdf`.
|
48
|
+
|
49
|
+
You should also see a file named `mls_parser.log` in a diagnostics directory within your working directory
|
@@ -0,0 +1,30 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["setuptools>=75.2.0", "wheel"]
|
3
|
+
build-backend = "setuptools.build_meta"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "mls-parser"
|
7
|
+
version = "0.0.6"
|
8
|
+
description = "Flatland Model Layout Sheet Parser"
|
9
|
+
readme = "README.md"
|
10
|
+
authors = [{ name = "Leon Starr", email = "leon_starr@modelint.com" }]
|
11
|
+
license = { file = "LICENSE" }
|
12
|
+
classifiers = [
|
13
|
+
"License :: OSI Approved :: MIT License",
|
14
|
+
"Programming Language :: Python",
|
15
|
+
"Programming Language :: Python :: 3",
|
16
|
+
]
|
17
|
+
keywords = ["action language", "executable uml", "class model", "mbse", "xuml", "xtuml", "platform independent", "sysml"]
|
18
|
+
dependencies = ["Arpeggio", 'tomli; python_version < "3.13"']
|
19
|
+
requires-python = ">=3.11"
|
20
|
+
|
21
|
+
[project.optional-dependencies]
|
22
|
+
build = ["build", "twine"]
|
23
|
+
dev = ["bump2version", "pytest"]
|
24
|
+
|
25
|
+
[project.scripts]
|
26
|
+
mls = "mls_parser.__main__:main"
|
27
|
+
|
28
|
+
[project.urls]
|
29
|
+
repository = "https://github.com/modelint/mls-parser"
|
30
|
+
documentation = "https://github.com/modelint/mls-parser/wiki"
|
@@ -0,0 +1 @@
|
|
1
|
+
version = "0.0.6"
|
@@ -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()
|
@@ -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}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}'
|
@@ -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 ternary_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
|
+
ternary_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': 'RIGHT', 'l': 'LEFT', 't': 'TOP', 'b': '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_ternary_node(cls, node, children):
|
285
|
+
"""Ternary 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
|
+
|
@@ -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,94 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: mls-parser
|
3
|
+
Version: 0.0.6
|
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 Sheet Parser
|
47
|
+
|
48
|
+
Parses an *.mls file (Model Layout Sheet) 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.11+ 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 couple 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,17 @@
|
|
1
|
+
LICENSE
|
2
|
+
MANIFEST.in
|
3
|
+
README.md
|
4
|
+
pyproject.toml
|
5
|
+
src/mls_parser/__init__.py
|
6
|
+
src/mls_parser/__main__.py
|
7
|
+
src/mls_parser/exceptions.py
|
8
|
+
src/mls_parser/layout.peg
|
9
|
+
src/mls_parser/layout_parser.py
|
10
|
+
src/mls_parser/layout_visitor.py
|
11
|
+
src/mls_parser/log.conf
|
12
|
+
src/mls_parser.egg-info/PKG-INFO
|
13
|
+
src/mls_parser.egg-info/SOURCES.txt
|
14
|
+
src/mls_parser.egg-info/dependency_links.txt
|
15
|
+
src/mls_parser.egg-info/entry_points.txt
|
16
|
+
src/mls_parser.egg-info/requires.txt
|
17
|
+
src/mls_parser.egg-info/top_level.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
mls_parser
|