332 lines
7.0 KiB
Python
332 lines
7.0 KiB
Python
# Copyright (c) 2024 Síle Ekaterin Liszka
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|
# a copy of this software and associated documentation files (the
|
|
# "Software"), to deal in the Software without restriction, including
|
|
# without limitation the rights to use, copy, modify, merge, publish,
|
|
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
# permit persons to whom the Software is furnished to do so, subject to
|
|
# the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
import re
|
|
|
|
from collections.abc import Mapping, Sequence
|
|
|
|
from pyparsing import (
|
|
alphas,
|
|
CharsNotIn,
|
|
DelimitedList,
|
|
Forward,
|
|
Group,
|
|
nums,
|
|
Literal,
|
|
OneOrMore,
|
|
Optional,
|
|
Regex,
|
|
restOfLine,
|
|
QuotedString,
|
|
Word,
|
|
ZeroOrMore
|
|
)
|
|
|
|
from .utils import normalise
|
|
from .version import version as __version__
|
|
|
|
class Node:
|
|
def __init__(self, *args, type=None):
|
|
if isinstance(args[0], str):
|
|
self.args = args[0]
|
|
elif isinstance(args[0][0], str):
|
|
self.args = args[0][0]
|
|
else:
|
|
self.args = args[0][0][0]
|
|
self.type = type
|
|
|
|
@property
|
|
def key(self):
|
|
return self.args[0]
|
|
|
|
class Key(Node):
|
|
def __init__(self, *args):
|
|
self.args = args[0]
|
|
self.type = 'key'
|
|
|
|
class KeyValue(Node):
|
|
def __init__(self, *args):
|
|
super().__init__(args, type='kv')
|
|
|
|
@property
|
|
def op(self):
|
|
return self.args[1]
|
|
|
|
@property
|
|
def value(self):
|
|
return self.args[2]
|
|
|
|
class Block(Node):
|
|
def __init__(self, *args):
|
|
super().__init__(args, type='block')
|
|
|
|
@property
|
|
def contents(self):
|
|
return self.args[1]
|
|
|
|
key_fragment = Word(alphas + nums + '_-')
|
|
key = DelimitedList(key_fragment, delim='.', combine=True)
|
|
|
|
bareval = CharsNotIn('{}#=+:;,\n\'"')
|
|
strvals = QuotedString("'", multiline=True, unquote_results=False)
|
|
strvald = QuotedString('"', multiline=True, unquote_results=False)
|
|
value = bareval | strvald | strvals
|
|
|
|
assign = Literal('=')
|
|
update = Literal(':=')
|
|
append = Literal('+=')
|
|
op = assign | update | append
|
|
semi = Literal(';').suppress()
|
|
lbrace = Literal('{').suppress()
|
|
rbrace = Literal('}').suppress()
|
|
terminal = Word(';\n').suppress()
|
|
|
|
NL = Literal('\n').suppress()
|
|
WS = Word(' \t').suppress()
|
|
WS_NL = Word(' \t\n').suppress()
|
|
comment = Literal('#') + restOfLine
|
|
|
|
values = Group(value + ZeroOrMore(Literal(',').suppress() + Optional(WS_NL) + value), aslist=True)
|
|
|
|
keyvalue = Group(key + Optional(WS) + op + Optional(WS) + values, aslist=True)
|
|
keyvalue.set_parse_action(lambda x: KeyValue(x))
|
|
|
|
key_stmt = key + Optional(WS) + Optional(assign).suppress() + Optional(WS)
|
|
key_stmt.set_parse_action(lambda x: Key(x))
|
|
|
|
block = Forward()
|
|
statement = (keyvalue | key_stmt)
|
|
statements = DelimitedList(statement, delim=terminal)
|
|
segment = Group(OneOrMore(block | statements), aslist=True)
|
|
|
|
block << Group(key + Optional(WS) + lbrace + segment + rbrace + Optional(NL))
|
|
block.set_parse_action(lambda x: Block(x))
|
|
|
|
data = OneOrMore(segment)
|
|
|
|
XBCParser = data
|
|
|
|
XBCParser.ignore(comment)
|
|
|
|
class ParseError(Exception):
|
|
pass
|
|
|
|
def lex(data):
|
|
tree = XBCParser.parseString(data).asList()
|
|
return tree
|
|
|
|
def unquote(val):
|
|
if val[0] in '\'"' and val[0] == val[-1]:
|
|
return val[1:-1]
|
|
return val.strip()
|
|
|
|
def key_walk(d, key):
|
|
split = key.split('.')
|
|
|
|
for i in range(len(split) - 1, 0, -1):
|
|
x = '.'.join(split[:i])
|
|
print(x)
|
|
if x not in d:
|
|
d[x] = False
|
|
|
|
def parse_block(key, seq):
|
|
if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list):
|
|
seq = seq[0]
|
|
|
|
ret = {}
|
|
|
|
for item in seq:
|
|
if key is not None:
|
|
k = f'{key}.{item.key}'
|
|
else:
|
|
k = item.key
|
|
|
|
if isinstance(item, Key):
|
|
if k not in ret:
|
|
ret[k] = True
|
|
else:
|
|
raise ParseError(f'key {k} already defined')
|
|
elif isinstance(item, KeyValue):
|
|
value = item.value
|
|
op = item.op
|
|
|
|
if op == '=':
|
|
if k in ret:
|
|
raise ParseError(f'key {k} already defined')
|
|
assign = value
|
|
else:
|
|
if k not in ret:
|
|
raise ParseError(f'key {k} not defined')
|
|
|
|
if op == '+=':
|
|
if isinstance(ret[k], str):
|
|
assign = [ret[k]]
|
|
else:
|
|
assign = ret[k]
|
|
if isinstance(value, str):
|
|
assign.append(value)
|
|
else:
|
|
assign.extend(value)
|
|
else:
|
|
assign = value
|
|
|
|
if isinstance(assign, list):
|
|
for i in range(len(assign)):
|
|
assign[i] = unquote(assign[i])
|
|
else:
|
|
assign = unquote(assign)
|
|
|
|
if isinstance(assign, list) and len(assign) == 1:
|
|
assign = assign[0]
|
|
|
|
ret[k] = assign
|
|
|
|
if '.' in k:
|
|
key_walk(ret, k)
|
|
elif isinstance(item, Block):
|
|
value = item.contents
|
|
|
|
if k not in ret:
|
|
ret[k] = False
|
|
|
|
if not isinstance(value, list):
|
|
value = [value]
|
|
|
|
d = parse_block(k, value)
|
|
|
|
for k, v in d.items():
|
|
if k in ret:
|
|
continue
|
|
ret[k] = v
|
|
|
|
return ret
|
|
|
|
def parse(data):
|
|
tree = lex(data)
|
|
|
|
d = parse_block(None, tree)
|
|
|
|
return d
|
|
|
|
def loads_xbc(data):
|
|
return parse(data)
|
|
|
|
def load_xbc(fp):
|
|
with open(fp, mode='r') as f:
|
|
return loads_xbc(f.read())
|
|
|
|
def longest_key(L):
|
|
lens = [len(x) for x in L]
|
|
shortest = min(lens)
|
|
|
|
if shortest < 1:
|
|
return None
|
|
|
|
ret = []
|
|
|
|
for i in range(shortest):
|
|
count = {}
|
|
|
|
for item in L:
|
|
j = item[i]
|
|
|
|
if j not in count:
|
|
count[j] = 0
|
|
|
|
count[j] += 1
|
|
if len(count.keys()) == 1:
|
|
ret.append(L[0][i])
|
|
else:
|
|
return '.'.join(ret)
|
|
return None
|
|
|
|
def longest_keys(keys):
|
|
keys = [k.split('.') for k in keys]
|
|
ret = set()
|
|
|
|
for i in range(len(keys)):
|
|
for j in range(1, len(keys)):
|
|
longest = longest_key([keys[i], keys[j]])
|
|
if longest is not None:
|
|
ret.add(longest)
|
|
ret.discard('')
|
|
return ret
|
|
|
|
def make_block(data):
|
|
ret = []
|
|
|
|
leafs = []
|
|
blocks = set()
|
|
block_keys = []
|
|
|
|
for key in data.keys():
|
|
if '.' not in key:
|
|
leafs.append(key)
|
|
else:
|
|
k, rest = key.split('.', maxsplit=1)
|
|
blocks.add(k)
|
|
|
|
keys = [k for k in data.keys() if '.' in k]
|
|
temp = longest_keys(keys)
|
|
if len(temp):
|
|
mindots = 99
|
|
for i in temp:
|
|
if 0 < i.count('.') < mindots:
|
|
mindots = i.count('.')
|
|
temp = [i for i in temp if i.count('.') == mindots]
|
|
blocks = set(temp)
|
|
|
|
for key in leafs:
|
|
if data[key] is True:
|
|
ret.append(f'{key}')
|
|
elif data[key] is False:
|
|
continue
|
|
else:
|
|
value = normalise(data[key])
|
|
ret.append(f'{key} = {value}')
|
|
|
|
for key in blocks:
|
|
block = {}
|
|
klen = len(key) + 1
|
|
|
|
for k, v in data.items():
|
|
if not k.startswith(f'{key}.'):
|
|
continue
|
|
block[k[klen:]] = v
|
|
|
|
chunk = make_block(block)
|
|
ret.append(key + ' {')
|
|
for line in chunk:
|
|
ret.append(f'\t{line}')
|
|
ret.append('}')
|
|
|
|
return ret
|
|
|
|
def saves_xbc(data):
|
|
ret = make_block(data)
|
|
return '\n'.join(ret)
|
|
|
|
def save_xbc(data, filename):
|
|
with open(filename, mode='w') as f:
|
|
f.write(saves_xbc(data))
|
|
|
|
__all__ = ['loads_xbc', 'load_xbc', 'saves_xbc', 'save_xbc', 'ParseError']
|