2024-01-31 18:13:52 +01:00
|
|
|
from typing import Iterable, Union, Mapping, TypeVar, List, TextIO
|
2024-01-31 17:30:20 +01:00
|
|
|
|
|
|
|
import re
|
|
|
|
|
2024-01-31 18:13:52 +01:00
|
|
|
from .parser import parser
|
|
|
|
from .exceptions import HeckParseException
|
2024-01-31 17:30:20 +01:00
|
|
|
|
|
|
|
HeckValue = TypeVar("HeckElement") | str | int | float
|
|
|
|
|
|
|
|
class HeckElement:
|
2024-01-31 18:13:52 +01:00
|
|
|
"""
|
|
|
|
Container for a tree of HECKformat elements.
|
|
|
|
"""
|
2024-01-31 17:30:20 +01:00
|
|
|
name: str
|
2024-01-31 18:13:52 +01:00
|
|
|
"""The name of the element, either __ROOT__ for top level or whatever is specified in file."""
|
2024-01-31 17:30:20 +01:00
|
|
|
children: Iterable[TypeVar]
|
2024-01-31 18:13:52 +01:00
|
|
|
"""The children of the element."""
|
2024-01-31 17:30:20 +01:00
|
|
|
values: Iterable[HeckValue]
|
2024-01-31 18:13:52 +01:00
|
|
|
"""One or more values associated with the element."""
|
2024-01-31 17:30:20 +01:00
|
|
|
attributes: Mapping[str, HeckValue]
|
2024-01-31 18:13:52 +01:00
|
|
|
"""Zero or more attributes associated with the element as a key-value pair."""
|
2024-01-31 17:30:20 +01:00
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
self.children = []
|
|
|
|
self.values = []
|
|
|
|
self.attributes = dict()
|
|
|
|
self.name = ""
|
|
|
|
self.unparsed = False
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
k=''
|
|
|
|
if self.unparsed:
|
|
|
|
k='Unparsed '
|
|
|
|
return f"<HeckElement {k}{self.name} c={self.children} v={self.values} a={self.attributes}>"
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
return self.__str__()
|
|
|
|
|
2024-02-03 18:42:20 +01:00
|
|
|
|
|
|
|
def to_struct(self) -> Mapping[str, Union[int, float, str, Mapping, List]]:
|
|
|
|
"""
|
|
|
|
Convert this HeckElement tree to a Python structure. The structure is a dictionary keyed by element name, where
|
|
|
|
the values are a list of each tree under that name, in order of their declaration in the tree, and futher, a
|
|
|
|
list of values.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
```
|
|
|
|
%%% heck
|
|
|
|
a b c
|
|
|
|
a c d
|
|
|
|
```
|
|
|
|
|
|
|
|
turns into:
|
|
|
|
|
|
|
|
{'a': [['b', 'c'], ['c', 'd']]}
|
|
|
|
|
|
|
|
Subelements are treated as a dictionary object added to the end of each value.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
```
|
|
|
|
%%% heck
|
|
|
|
a b
|
|
|
|
> c d
|
|
|
|
a e
|
|
|
|
> f g
|
|
|
|
```
|
|
|
|
|
|
|
|
turns into:
|
|
|
|
|
|
|
|
{'a': [['b'], {'c': [['d']]}, ['e', {'f': [['g']]}]]}
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def to_dict(self, merge=False) -> Mapping[str, Union[int, float, str, Mapping, List]]:
|
|
|
|
"""
|
|
|
|
As with `to_struct`, but attempts to merge all keys together at each level.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
```
|
|
|
|
%%% heck
|
|
|
|
a b c
|
|
|
|
a c d
|
|
|
|
```
|
|
|
|
|
|
|
|
turns into (with merge set to True):
|
|
|
|
|
|
|
|
{'a': ['b', 'c', 'c', 'd']}
|
|
|
|
|
|
|
|
or (with merge set to False):
|
|
|
|
|
|
|
|
{'a': ['c', 'd']}
|
|
|
|
|
|
|
|
For child elements, the list is replaced by a dictionary, and the values are stored in a special key '%%% values'
|
|
|
|
|
|
|
|
Thus:
|
|
|
|
|
|
|
|
```
|
|
|
|
%%% heck
|
|
|
|
a b
|
|
|
|
> c d
|
|
|
|
```
|
|
|
|
|
|
|
|
becomes:
|
|
|
|
|
|
|
|
{'a':{'%%%values': ['b'], 'c': ['d']}}
|
|
|
|
|
|
|
|
This cannot represent exactly the contents of the input tree, however it may be more convenient in many use
|
|
|
|
cases where repeated elements of the same key are not allowed, or if keys are being treated as unique.
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2024-01-31 18:13:52 +01:00
|
|
|
def _get_element(ast: List) -> HeckElement:
|
|
|
|
"""
|
|
|
|
Get an element from an element AST from the parser.
|
|
|
|
"""
|
2024-01-31 17:30:20 +01:00
|
|
|
if not (ast[0] == 'element'):
|
2024-02-03 18:42:20 +01:00
|
|
|
raise HeckParseException(f"Found a non-element where an element was expected. {ast}")
|
2024-01-31 17:30:20 +01:00
|
|
|
elm = HeckElement()
|
|
|
|
elm.name = ast[1];
|
|
|
|
for item in ast[2:]:
|
|
|
|
if item[0] == 'values':
|
|
|
|
elm.values = [x[1] for x in item[1:]]
|
|
|
|
elif item[0] == 'attributes':
|
|
|
|
elm.attributes.update({x[1]: x[2][1] for x in item[1:]})
|
|
|
|
return elm
|
|
|
|
|
|
|
|
def load_heck(inp: Iterable[str]) -> HeckElement:
|
2024-01-31 18:13:52 +01:00
|
|
|
"""
|
|
|
|
Load a HECKformat into a tree of HeckElements from a list of lines from the file.
|
|
|
|
"""
|
2024-01-31 17:30:20 +01:00
|
|
|
MODE_INIT = 0
|
|
|
|
MODE_ELM = 1
|
|
|
|
MODE_UNPARSE = 2
|
|
|
|
|
|
|
|
rootelm = HeckElement()
|
2024-02-03 18:42:20 +01:00
|
|
|
pelm = [rootelm] # parent for subelement
|
|
|
|
pdepth = 0
|
|
|
|
depth = 0
|
2024-01-31 17:30:20 +01:00
|
|
|
rootelm.name = "__ROOT__"
|
|
|
|
mode = MODE_INIT
|
|
|
|
for idx, line in enumerate(inp):
|
|
|
|
if mode == MODE_UNPARSE:
|
|
|
|
if (line.startswith('%%%')):
|
|
|
|
mode = MODE_INIT
|
|
|
|
else:
|
2024-02-03 18:42:20 +01:00
|
|
|
pelm[-1].values.append(line)
|
2024-01-31 17:30:20 +01:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
ast = parser.parse(line)
|
|
|
|
if ast:
|
|
|
|
if ast[0] == 'section':
|
|
|
|
if ast[1] == 'heck':
|
|
|
|
mode = MODE_ELM
|
2024-02-03 18:42:20 +01:00
|
|
|
pelm = [rootelm]
|
2024-01-31 17:30:20 +01:00
|
|
|
else:
|
|
|
|
mode = MODE_UNPARSE
|
2024-02-03 18:42:20 +01:00
|
|
|
pelm = [HeckElement()]
|
|
|
|
rootelm.children.append(pelm[-1])
|
|
|
|
pelm[-1].name = ast[1]
|
|
|
|
pelm[-1].unparsed = True
|
2024-01-31 17:30:20 +01:00
|
|
|
else:
|
|
|
|
if not mode == MODE_ELM:
|
|
|
|
raise HeckParseException("Didn't find heck preamble, line {idx}")
|
|
|
|
else:
|
2024-02-03 18:42:20 +01:00
|
|
|
if ast[0] == 'deep':
|
|
|
|
# we're in a subitem
|
|
|
|
depth = ast[1]
|
|
|
|
if (depth > pdepth):
|
|
|
|
# are we deeper than last time?
|
|
|
|
try:
|
|
|
|
pelm.append(pelm[-1].children[-1])
|
|
|
|
except:
|
|
|
|
raise HeckParseException("Tried to go deeper without a previous element, line {idx}")
|
|
|
|
elif (depth < pdepth):
|
|
|
|
# are we shallower than last time?
|
|
|
|
pelm.pop()
|
|
|
|
if (not len(pelm)):
|
|
|
|
raise HeckParseException("Tried to go shallower while already shallow, line {idx}")
|
|
|
|
ast = ast[2]
|
|
|
|
pdepth = depth
|
|
|
|
elif (pdepth > 0):
|
|
|
|
# we're no longer deep, just pop up to the top
|
|
|
|
pdepth = 0
|
|
|
|
pelm = [rootelm]
|
|
|
|
pelm[-1].children.append(_get_element(ast))
|
2024-01-31 17:30:20 +01:00
|
|
|
|
|
|
|
return rootelm
|
|
|
|
|
2024-01-31 18:13:52 +01:00
|
|
|
def load(infile: TextIO) -> HeckElement:
|
|
|
|
return load_heck(infile.readlines())
|
|
|
|
|
|
|
|
def loads(ins: str) -> HeckElement:
|
|
|
|
return load_heck(re.split(r'\n|\r|\r\n', ins))
|
|
|
|
|
2024-01-31 17:30:20 +01:00
|
|
|
|
|
|
|
TEST_HECK = """
|
|
|
|
%%% heck
|
|
|
|
# Website!
|
|
|
|
title "My Website" bold=True
|
|
|
|
subtitle "Yep it's a website"
|
|
|
|
scale 3.72
|
|
|
|
matrix 0 0 0 0 1 2 3 1 2 3 4 29394.2
|
|
|
|
tags hey man what are you doin
|
2024-02-03 18:42:20 +01:00
|
|
|
> more tag tag tag 1 2 3
|
|
|
|
>> we can go deeper
|
|
|
|
>>> we can go even deeper
|
|
|
|
test
|
|
|
|
> _val 1
|
|
|
|
> _val 2
|
|
|
|
> _val 3
|
|
|
|
valueless
|
2024-02-03 19:13:14 +01:00
|
|
|
_more.orless complexelement
|
|
|
|
.yooooo
|
2024-02-03 18:42:20 +01:00
|
|
|
boolean True
|
2024-01-31 17:30:20 +01:00
|
|
|
%%% markdown
|
|
|
|
# Some cheeky markdown to confuse our processing.
|
|
|
|
|
|
|
|
All my page content goes here.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
result = load_heck(TEST_HECK.split('\n'))
|
|
|
|
print(result)
|