summaryrefslogtreecommitdiff
path: root/domllib.py
diff options
context:
space:
mode:
Diffstat (limited to 'domllib.py')
-rwxr-xr-xdomllib.py605
1 files changed, 605 insertions, 0 deletions
diff --git a/domllib.py b/domllib.py
new file mode 100755
index 0000000..7cae9cf
--- /dev/null
+++ b/domllib.py
@@ -0,0 +1,605 @@
+#!/usr/bin/env python
+#
+# Copyright 2026 David Vazgenovich Shakaryan
+
+import re
+import copy
+
+
+WS = set(' \t')
+WSNL = {*WS, '\n'}
+ESCAPES = {
+ '\n': '',
+ '\\': '\\',
+ '"': '"',
+ 'n': '\n',
+ 't': '\t',
+}
+
+RE_IDENT = re.compile(r'[A-Za-z0-9_]+')
+RE_NUM = re.compile(
+ r'[+-]?(?:'
+ r'0x[0-9a-fA-F]+(?:_[0-9a-fA-F]+)*|'
+ r'0o[0-7]+(?:_[0-7]+)*|'
+ r'0b[01]+(?:_[01]+)*|'
+ r'(?:0|[1-9][0-9]*(?:_[0-9]+)*)'
+ r'(\.[0-9]+(?:_[0-9]+)*(?:[eE][+-]?[0-9]+(?:_[0-9]+)*)?|'
+ r'[eE][+-]?[0-9]+(?:_[0-9]+)*)?|'
+ r'(inf|nan)'
+ r')')
+
+
+def _raise_unexpected_eof(ctx):
+ raise SyntaxError(f'Unexpected end of input while parsing {ctx}') from None
+
+
+def _raise_expected_char(char, pos):
+ if type(char) is tuple:
+ s = ' or '.join(repr(c) for c in char)
+ else:
+ s = repr(char)
+ raise SyntaxError(f'Expected {s} at position {pos}') from None
+
+
+def _raise_unexpected_char(char, pos, ctx=None):
+ ctx = f' {ctx}' if ctx else ''
+ raise SyntaxError(
+ f'Unexpected {repr(char)}{ctx} at position {pos}') from None
+
+
+def _raise_invalid_escape(seq, pos):
+ seq = ''.join(
+ '\\' if (r := repr(c)[1:-1]) == '\\\\'
+ else r if len(r) == 1
+ else f'<{r}>'
+ for c in seq)
+ raise SyntaxError(f'Invalid escape sequence {seq} at pos {pos}') from None
+
+
+def skip_chars(src, pos, chars, skip_comments=False):
+ try:
+ while True:
+ char = src[pos]
+ if skip_comments and char == '#':
+ while src[pos] != '\n':
+ pos += 1
+ continue
+ if char == '\\':
+ try:
+ if src[pos+1] != '\n':
+ _raise_expected_char('\n', pos + 1)
+ except IndexError:
+ _raise_unexpected_eof('line continuation')
+ pos += 2
+ continue
+ if char not in chars:
+ break
+ pos += 1
+ except IndexError:
+ pass
+ return pos
+
+
+def skip(src, pos, depth):
+ if depth:
+ return skip_chars(src, pos, WSNL, skip_comments=True)
+ return skip_chars(src, pos, WS)
+
+
+def ensure_path(d, path):
+ curr = d
+ for n in path:
+ if type(curr) is not dict:
+ raise SyntaxError(
+ f'Cannot resolve {repr(n)} in {type(curr).__name__}')
+ curr = curr.setdefault(n, {})
+ return curr
+
+
+def dict_unpack(d, val):
+ if type(val) is not dict:
+ raise SyntaxError(f'Cannot dict-unpack {type(val).__name__}')
+ for k, v in val.items():
+ d[k] = v
+
+
+def dict_assign(d, path, val):
+ tgt = ensure_path(d, path[:-1])
+ if type(tgt) is not dict:
+ raise SyntaxError(
+ f"Cannot assign key '{path[-1]}' in {type(tgt).__name__}")
+ tgt[path[-1]] = val
+
+
+def dict_delete(d, path):
+ try:
+ for n in path[:-1]:
+ d = d[n]
+ del d[path[-1]]
+ except KeyError, TypeError:
+ return
+
+
+def parse_dict_unpack(src, pos, depth, root, sect):
+ try:
+ if src[pos+1] == '*':
+ pos += 2
+ else:
+ _raise_expected_char('*', pos + 1)
+ except IndexError:
+ _raise_unexpected_eof('dict unpack')
+ pos = skip(src, pos, depth)
+ return parse_value(src, pos, depth, root, sect)
+
+
+def parse_sequence(src, pos, depth, delim, term, on_item):
+ pos += 1
+ depth += 1
+
+ while True:
+ pos = skip(src, pos, depth)
+
+ try:
+ char = src[pos]
+ except IndexError:
+ _raise_unexpected_eof('sequence')
+
+ if char == term:
+ return pos + 1
+
+ pos = on_item(src, pos, depth)
+ pos = skip(src, pos, depth)
+
+ try:
+ char = src[pos]
+ if char == delim:
+ pos += 1
+ elif char != term:
+ _raise_expected_char((delim, term), pos)
+ except IndexError:
+ _raise_unexpected_eof('sequence')
+
+
+def parse_inline_list(src, pos, depth, root, sect):
+ res = []
+
+ def on_item(src, pos, depth):
+ unpack = False
+ if src[pos] == '*':
+ unpack = True
+ pos = skip(src, pos + 1, depth)
+
+ pos, val = parse_value(src, pos, depth, root, sect)
+ if unpack:
+ if type(val) is not list:
+ raise SyntaxError(f'Cannot list-unpack {type(val).__name__}')
+ res.extend(val)
+ else:
+ res.append(val)
+ return pos
+
+ pos = parse_sequence(src, pos, depth, ',', ']', on_item)
+ return pos, res
+
+
+def parse_inline_dict(src, pos, depth, root, sect):
+ res = {}
+
+ def on_item(src, pos, depth):
+ char = src[pos]
+ if char == '*':
+ pos, val = parse_dict_unpack(src, pos, depth, root, sect)
+ dict_unpack(res, val)
+ else:
+ pos, key, val = parse_assign(src, pos, depth, root, sect)
+ dict_assign(res, key, val)
+ return pos
+
+ pos = parse_sequence(src, pos, depth, ',', '}', on_item)
+ return pos, res
+
+
+def parse_str(src, pos, multiline=False):
+ try:
+ if multiline:
+ pos += 3
+ if src[pos] == '\n':
+ pos += 1
+ else:
+ pos += 1
+
+ start = pos
+ res = ''
+
+ while True:
+ curr = src[pos]
+ if curr == '"' and (not multiline or src.startswith('"""', pos)):
+ res += src[start:pos]
+ break
+
+ if curr == '\\':
+ res += src[start:pos]
+ nxt = src[pos+1]
+
+ if nxt == 'u':
+ if src[pos+2] != '{':
+ _raise_expected_char('{', pos + 2)
+ end = src.find('}', pos + 3)
+ if end == -1:
+ raise IndexError
+ code = src[pos+3:end]
+ try:
+ code_int = int(code, 16)
+ if not (0 <= code_int <= 0xd7ff or
+ 0xe000 <= code_int <= 0x10ffff):
+ _raise_invalid_escape(f'\\u{{{code}}}', pos)
+ res += chr(code_int)
+ except ValueError:
+ _raise_invalid_escape(f'\\u{{{code}}}', pos)
+ pos = end + 1
+
+ else:
+ try:
+ res += ESCAPES[nxt]
+ except KeyError:
+ _raise_invalid_escape(f'\\{nxt}', pos)
+ pos += 2
+
+ start = pos
+ continue
+
+ code = ord(curr)
+ if ((code < 32 and (not multiline or code != 10 and code != 9)) or
+ code == 127):
+ _raise_unexpected_char(curr, pos, ctx='in string')
+ pos += 1
+ except IndexError:
+ _raise_unexpected_eof('string')
+ return pos + (3 if multiline else 1), res
+
+
+def parse_raw_str(src, pos, multiline=False):
+ try:
+ if multiline:
+ pos += 3
+ if src[pos] == '\n':
+ pos += 1
+ else:
+ pos += 1
+
+ start = pos
+
+ while (src[pos] != "'" or
+ (multiline and not src.startswith("'''", pos))):
+ code = ord(src[pos])
+ if ((code < 32 and (not multiline or code != 10 and code != 9)) or
+ code == 127):
+ _raise_unexpected_char(src[pos], pos, ctx='in raw string')
+ pos += 1
+ except IndexError:
+ _raise_unexpected_eof('raw string')
+ return pos + (3 if multiline else 1), src[start:pos]
+
+
+def parse_literal(src, pos, depth, root, sect):
+ char = src[pos] # guaranteed to exist by parse_value()
+
+ if char == "'":
+ return parse_raw_str(src, pos, multiline=src.startswith("'''", pos))
+ if char == '"':
+ return parse_str(src, pos, multiline=src.startswith('"""', pos))
+
+ if char == 't':
+ if src.startswith('true', pos):
+ return pos + 4, True
+ if char == 'f':
+ if src.startswith('false', pos):
+ return pos + 5, False
+ if char == 'n':
+ if src.startswith('null', pos):
+ return pos + 4, None
+
+ if char == '{':
+ return parse_inline_dict(src, pos, depth, root, sect)
+ if char == '[':
+ return parse_inline_list(src, pos, depth, root, sect)
+
+ if (m := RE_NUM.match(src, pos)):
+ return m.end(), float(m[0]) if m.lastindex else int(m[0], 0)
+
+ _raise_unexpected_char(char, pos)
+
+
+def parse_value(src, pos, depth, root, sect):
+ try:
+ char = src[pos]
+ except IndexError:
+ _raise_unexpected_eof('value')
+
+ ref = root if char == '$' else sect if char == '@' else None
+
+ if ref is None:
+ pos, val = parse_literal(src, pos, depth, root, sect)
+
+ if type(val) is str:
+ while True:
+ pos = skip(src, pos, depth)
+ try:
+ char = src[pos]
+ except IndexError:
+ break
+ if char == "'":
+ pos, v = parse_raw_str(
+ src, pos, multiline=src.startswith("'''", pos))
+ val += v
+ elif char == '"':
+ pos, v = parse_str(
+ src, pos, multiline=src.startswith('"""', pos))
+ val += v
+ else:
+ break
+ else:
+ pos = skip(src, pos + 1, depth)
+ pos, key = parse_key_segment(src, pos)
+ try:
+ val = ref[key]
+ except KeyError:
+ if ref is root:
+ raise SyntaxError(
+ f'Invalid global reference {repr(key)}') from None
+ else:
+ raise SyntaxError(
+ f'Invalid local reference {repr(key)}') from None
+
+ pos, val = parse_selectors(src, pos, depth, root, sect, val, copy_=ref)
+
+ return pos, val
+
+
+def parse_selectors(src, pos, depth, root, sect, val, copy_=False):
+ while True:
+ pos = skip(src, pos, depth)
+ try:
+ char = src[pos]
+ except IndexError:
+ break
+
+ if char == '.':
+ pos = skip(src, pos + 1, depth)
+ pos, key = parse_key_segment(src, pos)
+
+ elif char == '[':
+ depth += 1
+ pos = skip(src, pos + 1, depth)
+ try:
+ nxt = src[pos]
+ except IndexError:
+ _raise_unexpected_eof('selector')
+ continue_ = False
+
+ if nxt == '^':
+ pos = skip(src, pos + 1, depth)
+ def on_item(src, pos, depth):
+ nonlocal val, copy_
+ pos, key = parse_key(src, pos, depth)
+ if copy_:
+ val = copy.deepcopy(val)
+ copy_ = False
+ dict_delete(val, key)
+ return pos
+ if src[pos] == '{':
+ pos = parse_sequence(src, pos, depth, ',', '}', on_item)
+ else:
+ pos = on_item(src, pos, depth)
+ continue_ = True
+
+ elif nxt == '&':
+ pos = skip(src, pos + 1, depth)
+ nval = {}
+ def on_item(src, pos, depth):
+ pos, key = parse_key(src, pos, depth)
+ v = val
+ for n in key:
+ try:
+ v = v[n]
+ except KeyError, TypeError:
+ return pos
+ dict_assign(nval, key, v)
+ return pos
+ if src[pos] == '{':
+ pos = parse_sequence(src, pos, depth, ',', '}', on_item)
+ else:
+ pos = on_item(src, pos, depth)
+ val = nval
+ continue_ = True
+
+ else:
+ if nxt == ':':
+ key = None
+ else:
+ pos, key = parse_value(src, pos, depth, root, sect)
+ pos = skip(src, pos, depth)
+ try:
+ nxt = src[pos]
+ except IndexError:
+ _raise_unexpected_eof('selector')
+ if nxt == ':':
+ pos = skip(src, pos + 1, depth)
+ try:
+ nxt = src[pos]
+ except IndexError:
+ _raise_unexpected_eof('selector')
+ if nxt == ']':
+ stop = None
+ else:
+ pos, stop = parse_value(src, pos, depth, root, sect)
+ key = slice(key, stop)
+
+ pos = skip(src, pos, depth)
+ try:
+ if src[pos] != ']':
+ _raise_expected_char(']', pos)
+ except IndexError:
+ _raise_unexpected_eof('selector')
+ pos += 1
+
+ depth -= 1
+ if continue_:
+ continue
+
+ else:
+ break
+
+ try:
+ val = val[key]
+ except KeyError, IndexError, TypeError:
+ raise SyntaxError(
+ f'Cannot resolve {repr(key)} in {type(val).__name__}'
+ ) from None
+
+ return pos, copy.deepcopy(val) if copy_ else val
+
+
+def parse_key_segment(src, pos):
+ try:
+ char = src[pos]
+ except IndexError:
+ _raise_unexpected_eof('key segment')
+
+ if char == "'":
+ return parse_raw_str(src, pos)
+ if char == '"':
+ return parse_str(src, pos)
+
+ if (m := RE_IDENT.match(src, pos)):
+ return m.end(), m[0]
+ else:
+ raise SyntaxError(f'Invalid key identifier at position {pos}')
+
+
+def parse_key(src, pos, depth):
+ pos, seg = parse_key_segment(src, pos)
+ key = (seg,)
+ while True:
+ pos = skip(src, pos, depth)
+ try:
+ char = src[pos]
+ except IndexError:
+ return pos, key
+ if char != '.':
+ return pos, key
+ pos = skip(src, pos + 1, depth)
+ pos, seg = parse_key_segment(src, pos)
+ key += (seg,)
+
+
+def parse_assign(src, pos, depth, root, sect):
+ pos, key = parse_key(src, pos, depth)
+
+ pos = skip(src, pos, depth)
+ try:
+ char = src[pos]
+ except IndexError:
+ char = None
+ if char != '=':
+ _raise_expected_char('=', pos)
+ pos += 1
+
+ pos = skip(src, pos, depth)
+ pos, val = parse_value(src, pos, depth, root, sect)
+
+ return pos, key, val
+
+
+def parse_dict_header(src, pos, depth, root):
+ depth += 1
+ pos = skip(src, pos + 1, depth)
+ try:
+ if src[pos] == ']':
+ return pos + 1, root
+ except IndexError:
+ _raise_unexpected_eof('header')
+
+ pos, key = parse_key(src, pos, depth)
+ try:
+ if src[pos] != ']':
+ _raise_expected_char(']', pos)
+ except IndexError:
+ _raise_unexpected_eof('header')
+
+ sect = ensure_path(root, key)
+ if type(sect) is not dict:
+ raise SyntaxError(f'Cannot enter {type(sect).__name__} context: {key}')
+ return pos + 1, sect
+
+
+def parse_list_header(src, pos, depth, root):
+ depth += 1
+ pos = skip(src, pos + 2, depth)
+ pos, key = parse_key(src, pos, depth)
+ if not src.startswith(']]', pos):
+ _raise_expected_char(']]', pos)
+
+ if len(key) > 1:
+ d = ensure_path(root, key[:-1])
+ if type(d) is not dict:
+ raise SyntaxError(
+ f'Cannot resolve {repr(key[-1])} in {type(d).__name__}')
+ else:
+ d = root
+
+ l = d.setdefault(key[-1], [])
+ if type(l) is not list:
+ raise SyntaxError(f'Cannot append to {type(l).__name__}: {key}')
+ sect = {}
+ l.append(sect)
+ return pos + 2, sect
+
+
+def loads(src):
+ root = {}
+ sect = root
+ pos = 0
+
+ while True:
+ pos = skip_chars(src, pos, WSNL, skip_comments=True)
+
+ try:
+ char = src[pos]
+ except IndexError:
+ break
+
+ if char == '[':
+ try:
+ char = src[pos+1]
+ except IndexError:
+ _raise_unexpected_eof('header')
+
+ if char == '[':
+ pos, sect = parse_list_header(src, pos, 0, root)
+ else:
+ pos, sect = parse_dict_header(src, pos, 0, root)
+ elif char == '*':
+ pos, val = parse_dict_unpack(src, pos, 0, root, sect)
+ dict_unpack(sect, val)
+ else:
+ pos, key, val = parse_assign(src, pos, 0, root, sect)
+ dict_assign(sect, key, val)
+
+ pos = skip_chars(src, pos, WS, skip_comments=True)
+ try:
+ if src[pos] != '\n':
+ _raise_expected_char('\n', pos)
+ except IndexError:
+ pass
+
+ return root
+
+
+def load(f):
+ b = f.read()
+ try:
+ src = b.decode()
+ except AttributeError:
+ raise TypeError('File must be opened in binary mode')
+ return loads(src)