Skip to content

Commit

Permalink
Changed .bsb parser to a state machine (breaking!)
Browse files Browse the repository at this point in the history
Implemented a state machine for parsing bsb files instead of the fudged
yaml syntax abuse. The pipes '|' after tags are now not required
(and are actually invalid syntax).
  • Loading branch information
mp4096 committed Sep 20, 2016
1 parent 33db60d commit b051de0
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 44 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,13 @@ kann man den mitgelieferten Boilerplate-Code-Generator verwenden.
Dafür legt man eine Textdatei mit Erweiterung `.bsb` an und spezifiziert
das Blockschaltbild wie folgt:

```yaml
Skizze: |
```
Skizze:
C1 S1 S2 I1 I2 C2
P1
P2
Verbindungen: |
Verbindungen:
C1 - S1
S1 - S2
S2 - I1
Expand All @@ -173,7 +173,7 @@ Verbindungen: |
P1 - S2
P2 - S1
Namen: |
Namen:
C1: eingang
C2: ausgang
S1: sum 1
Expand Down
196 changes: 157 additions & 39 deletions blockschaltbilder/boilerplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import fnmatch
import os
import re
import yaml


"""Frontend file parser for Blockschaltbilder."""
Expand All @@ -12,16 +11,161 @@
# Specify exports
__all__ = ["convert_to_tikz"]

# Specify German-English equivalents for keys
_GERMAN_KEYS_TO_ENGLISH = {
"skizze": "sketch",
"verbindungen": "connections",
"namen": "names",
}
# Define regexen for the detection of file sections
_PATTERN_SKETCH = r"(?:sketch|skizze)\:$"
_RE_SKETCH = re.compile(_PATTERN_SKETCH, re.IGNORECASE)
_PATTERN_CONNECTIONS = r"(?:connections|verbindungen)\:$"
_RE_CONNECTIONS = re.compile(_PATTERN_CONNECTIONS, re.IGNORECASE)
_PATTERN_NAMES = r"(?:names|namen)\:$"
_RE_NAMES = re.compile(_PATTERN_NAMES, re.IGNORECASE)


# We use a state machine to parse the *.bsb files
class Reader:
"""State machine for reading *.bsb files."""

def __init__(self):
"""Creates a new reader state machine."""

# Initialise with inactive state
self.transit_to(Inactive)

# Initialise accumulator lists
self.sketch = []
self.connections = []
self.names = []

def transit_to(self, new_state):
"""Transit to new state."""
self._state = new_state

def read_line(self, line):
"""Read one line.
This method reads one line and decides what to do:
Depending on the line contents, it either performs a transition to
new state (file section) or stores the line in the appropriate
state-dependent accumulator list.
Parameters
----------
line : str
One line of a *.bsb file.
"""

# We strip the line only for tag matching and early exit;
# the original line is stored in accumulator lists in order to
# preserve indentation.
stripped_line = line.strip()

# Early return on empty lines
if not stripped_line:
return

# Try to match section tags and transit to the corresponding state;
# otherwise just chomp this line.
if _RE_SKETCH.match(stripped_line) is not None:
self.transit_to(Sketch)
elif _RE_CONNECTIONS.match(stripped_line) is not None:
self.transit_to(Connections)
elif _RE_NAMES.match(stripped_line) is not None:
self.transit_to(Names)
else:
self._store(line)

def _store(self, line):
"""Stores a line in an appropriate accumulator list.
Delegates this to the state's static method.
Parameters
----------
line : str
One line of a *.bsb file.
"""

return self._state.store_line(self, line)


# Classes representing reader's states; self-explanatory
class ReaderState:
"""Parent class for reader state."""

@staticmethod
def store_line(reader, line):
raise NotImplementedError()


class Inactive(ReaderState):
@staticmethod
def store_line(reader, line):
pass


class Sketch(ReaderState):
@staticmethod
def store_line(reader, line):
reader.sketch.append(line)


class Connections(ReaderState):
@staticmethod
def store_line(reader, line):
reader.connections.append(line)


class Names(ReaderState):
@staticmethod
def store_line(reader, line):
reader.names.append(line)


def _convert_text(lines):
"""Create a Blockschaltbild from text.
Parameters
----------
lines : list of str
Text lines with the Blockschaltbild specification.
Returns
-------
Blockschaltbild
A block diagram created from text.
"""

# Set current status to inactive
reader = Reader()

# Read the text line by line
for l in lines:
reader.read_line(l)

# Create an empty block diagram
bsb = Blockschaltbild()
# Import sketch; note that it is mandatory since it defines the blocks
if reader.sketch:
bsb.import_sketch(reader.sketch)
else:
raise ValueError("The input file must contain a sketch")
# If the connections are specified, import them
if reader.connections:
bsb.import_connections(reader.connections)
# If new names are specified, rename blocks
if reader.names:
bsb.import_names(reader.names)

# Add auto joints instead of blocks with multiple outgoing connections
bsb.add_auto_joints()

return bsb


def _convert_single_file(filename):
"""Convert a single file to a *.tex file.
"""Convert a single .bsb file into a boilerplate .tex file.
Parameters
----------
Expand All @@ -34,39 +178,13 @@ def _convert_single_file(filename):
if not fnmatch.fnmatch(filename, '*.bsb'):
raise ValueError("The input file must have a 'bsb' extension")

# Create an empty list for the modified contents of the file
modified_lines = []
# Open the file and save its modified contents line by line
# Open this file and read all its contents into a list
with codecs.open(filename, 'r', encoding="utf-8") as f:
for line in f:
# Replace hard tabs with soft tabs
line = line.replace("\t", " "*4).rstrip()
# Specify chomping indent of 1 for all multiline string literals
# This is required for parsing sketches correctly
modified_lines.append(re.sub(r":\s*\|$", r": |1", line))
# Create an empty block diagram and a dictionary for the contents of the file
bsb = Blockschaltbild()
contents = {}
# Iterate through the modified lines
for key, value in yaml.load("\n".join(modified_lines)).items():
# Transform German keys into English analogues
if key.lower() in _GERMAN_KEYS_TO_ENGLISH:
normalised_key = _GERMAN_KEYS_TO_ENGLISH[key.lower()]
else:
normalised_key = key.lower()
# Add the contents to the dict
contents[normalised_key] = value
lines = f.readlines()

# Convert them into a Blockschaltbild with automatically placed joints
bsb = _convert_text(lines)

# Import sketch; note that it is mandatory since it defines the blocks
bsb.import_sketch(contents["sketch"].splitlines())
# If the connections are specified, import them
if "connections" in contents:
bsb.import_connections(contents["connections"].splitlines())
# If new names are specified, rename blocks
if "names" in contents:
bsb.import_names(contents["names"].splitlines())
# Add auto joints instead of blocks with multiple outgoing connections
bsb.add_auto_joints()
# Export to a *.tex file
bsb.export_to_file(re.sub(r"\.bsb$", ".tex", filename))

Expand Down
2 changes: 2 additions & 0 deletions blockschaltbilder/bsb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import numpy as np
import re


"""This module contains the Blockschaltbild class."""


# Export only the Blockschaltbild class
__all__ = ["Blockschaltbild"]

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
author_email="mikhail.pak@tum.de",
url="https://github.com/mp4096/blockschaltbilder",
packages=["blockschaltbilder"],
install_requires=["numpy", "PyYAML"],
install_requires=["numpy"],
license="MIT",
test_suite="blockschaltbilder.tests",
)

0 comments on commit b051de0

Please sign in to comment.