"""Parser for the INI-ish text Boxsim stores inside its containers. Shape: `#Section` headers, `key=value` pairs, CRLF line ends, German decimal commas, and arrays as `;`-separated values. Section names are case sensitive: a project contains both `#AktFilter1` and `#Aktfilter1`. """ from __future__ import annotations from collections.abc import Iterator class Section(dict): """One `#Name` block. A plain dict of str -> str, plus the section name.""" def __init__(self, name: str): super().__init__() self.name = name def __repr__(self) -> str: # pragma: no cover - debugging aid return f"Section({self.name!r}, {len(self)} keys)" class IniDocument: """Ordered collection of sections, tolerant of repeated section names.""" def __init__(self) -> None: self.sections: list[Section] = [] def __iter__(self) -> Iterator[Section]: return iter(self.sections) def __contains__(self, name: str) -> bool: return any(s.name == name for s in self.sections) def first(self, name: str) -> Section | None: for s in self.sections: if s.name == name: return s return None def require(self, name: str) -> Section: s = self.first(name) if s is None: raise KeyError(f"missing section #{name}") return s def all(self, name: str) -> list[Section]: return [s for s in self.sections if s.name == name] def merged(self, *names: str) -> Section: """Merge several sections into one, for split blocks like AktFilter.""" out = Section(names[0]) for name in names: for s in self.all(name): out.update(s) return out def parse(text: str) -> IniDocument: doc = IniDocument() current: Section | None = None for raw_line in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"): line = raw_line.rstrip("\n") if not line.strip(): continue if line.startswith("#"): current = Section(line[1:].strip()) doc.sections.append(current) continue if current is None: # Content before any header: keep it in an unnamed section rather # than dropping it silently. current = Section("") doc.sections.append(current) key, sep, value = line.partition("=") if not sep: continue current[key.strip()] = value.strip() return doc def number(value: str) -> float: """Parse a Boxsim number. German decimal comma, exponents like `1,E-5`.""" return float(value.strip().replace(",", ".")) def integer(value: str) -> int: return int(float(value.strip().replace(",", "."))) def number_list(value: str) -> list[float]: """Parse a `;`-separated array, tolerating trailing separators.""" return [number(p) for p in value.split(";") if p.strip()]