main
noah amfm-receiver-molecule: KiCad project, fab files, 3D and README (7/7) ef18fb3 1d ago
"""Minimal S-expression reader/writer for KiCad files."""
import re

class Sym(str):
    """Unquoted atom (keyword / number / bare word)."""
    __slots__ = ()

_tok = re.compile(r'\s*(?:(\()|(\))|("(?:[^"\\]|\\.)*")|([^\s()"]+))', re.S)

def parse(text):
    pos = 0
    stack = [[]]
    n = len(text)
    while pos < n:
        m = _tok.match(text, pos)
        if not m:
            if text[pos:].strip() == '':
                break
            raise ValueError('bad token at %d: %r' % (pos, text[pos:pos+40]))
        pos = m.end()
        if m.group(1):
            stack.append([])
        elif m.group(2):
            lst = stack.pop()
            stack[-1].append(lst)
        elif m.group(3) is not None:
            s = m.group(3)[1:-1]
            s = s.replace('\\"', '"').replace('\\n', '\n').replace('\\\\', '\\')
            stack[-1].append(s)
        else:
            stack[-1].append(Sym(m.group(4)))
    assert len(stack) == 1, 'unbalanced'
    return stack[0][0] if len(stack[0]) == 1 else stack[0]

def q(s):
    return '"' + str(s).replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + '"'

def dumps(node, indent=0):
    """Serialize: lists -> (..), Sym -> bare, str -> quoted, numbers -> bare."""
    if isinstance(node, list):
        if not node:
            return '()'
        # short lists on one line
        parts = [dumps(x, indent + 1) for x in node]
        oneline = '(' + ' '.join(parts) + ')'
        if len(oneline) < 100 and '\n' not in oneline:
            return oneline
        head = parts[0]
        out = '(' + head
        i = 1
        # keep leading atoms on the first line
        while i < len(node) and not isinstance(node[i], list):
            out += ' ' + parts[i]
            i += 1
        for p in parts[i:]:
            out += '\n' + '\t' * (indent + 1) + p
        out += '\n' + '\t' * indent + ')'
        return out
    if isinstance(node, Sym):
        return str(node)
    if isinstance(node, bool):
        return 'yes' if node else 'no'
    if isinstance(node, int):
        return str(node)
    if isinstance(node, float):
        s = ('%.6f' % node).rstrip('0').rstrip('.')
        return s if s not in ('-0', '') else '0'
    return q(node)

def find(node, key):
    """First child list whose head == key."""
    for x in node:
        if isinstance(x, list) and x and x[0] == key:
            return x
    return None

def findall(node, key):
    return [x for x in node if isinstance(x, list) and x and x[0] == key]