"""Block templates, addresses and formats declared in NX post definition (.def) files.""" import re from dataclasses import dataclass from pathlib import Path BLOCK_TEMPLATE = "block_template" ADDRESS = "address" FORMAT = "format" _KINDS = {"BLOCK_TEMPLATE": BLOCK_TEMPLATE, "ADDRESS": ADDRESS, "FORMAT": FORMAT} _HEADER_RE = re.compile(r"^\s*(BLOCK_TEMPLATE|ADDRESS|FORMAT)\s+([^\s{]+)") # A block template element is an address followed by its expression: X[$mom_pos(0)]. _ELEMENT_RE = re.compile(r"^\s*([A-Za-z_]\w*)\[") _PROPERTY_RE = re.compile(r"^\s*([A-Za-z_]+)\s*(.*?)\s*$") @dataclass(frozen=True) class DefDeclaration: kind: str name: str # 0-based line and UTF-16 columns of the name. line: int start: int end: int end_line: int text: str # ADDRESS: body properties such as ("LEADER", '"X"'); FORMAT: (("FORMAT", '"%d"'),). properties: tuple[tuple[str, str], ...] = () @dataclass(frozen=True) class DefReference: """An address used as element of a block template.""" kind: str name: str line: int start: int end: int container: str @dataclass(frozen=True) class DefDocument: declarations: tuple[DefDeclaration, ...] = () references: tuple[DefReference, ...] = () def names(self, kind: str) -> tuple[str, ...]: return tuple(dict.fromkeys(item.name for item in self.declarations if item.kind == kind)) @dataclass(frozen=True) class DefSymbols: block_templates: tuple[str, ...] = () addresses: tuple[str, ...] = () def _utf16_length(text: str) -> int: return len(text.encode("utf-16-le")) // 2 def _is_comment(line: str) -> bool: return line.lstrip().startswith("#") def _body(lines: list[str], header: int, rest: str) -> tuple[int, int, int] | None: """Return (first body line, first body column, closing line) of a braced body. The body starts after the first "{" on the header line or on a following line and ends at the next line starting with "}". """ if "{" in rest: after = rest.split("{", 1)[1] if "}" in after: return None start = header + 1 else: start = header + 1 while start < len(lines) and (not lines[start].strip() or _is_comment(lines[start])): start += 1 if start >= len(lines) or not lines[start].lstrip().startswith("{"): return None start += 1 end = start while end < len(lines) and not lines[end].lstrip().startswith("}"): end += 1 return start, 0, min(end, len(lines) - 1) def parse_def_document(source: str) -> DefDocument: """Parse the block templates, addresses and formats of a .def source.""" lines = source.splitlines() declarations: list[DefDeclaration] = [] references: list[DefReference] = [] index = 0 while index < len(lines): line = lines[index] match = None if _is_comment(line) else _HEADER_RE.match(line) if match is None: index += 1 continue kind = _KINDS[match.group(1)] name = match.group(2) start = _utf16_length(line[: match.start(2)]) end = start + _utf16_length(name) rest = line[match.end(2) :] if kind == FORMAT: declarations.append( DefDeclaration(kind, name, index, start, end, index, line.strip(), (("FORMAT", rest.strip()),)) ) index += 1 continue body = _body(lines, index, rest) end_line = index if body is None else body[2] properties: list[tuple[str, str]] = [] if body is not None: for number in range(body[0], body[2]): body_line = lines[number] if _is_comment(body_line) or not body_line.strip(): continue if kind == BLOCK_TEMPLATE: element = _ELEMENT_RE.match(body_line) if element is not None: element_start = _utf16_length(body_line[: element.start(1)]) references.append( DefReference( ADDRESS, element.group(1), number, element_start, element_start + _utf16_length(element.group(1)), name, ) ) else: prop = _PROPERTY_RE.match(body_line) if prop is not None: properties.append((prop.group(1).upper(), prop.group(2))) declarations.append( DefDeclaration( kind, name, index, start, end, end_line, "\n".join(lines[index : end_line + 1]), tuple(properties), ) ) index = end_line + 1 return DefDocument(tuple(declarations), tuple(references)) def parse_def_symbols(source: str) -> DefSymbols: """Return the BLOCK_TEMPLATE and ADDRESS names of a .def source in declaration order.""" document = parse_def_document(source) return DefSymbols( block_templates=document.names(BLOCK_TEMPLATE), addresses=document.names(ADDRESS), ) def read_def_source(path: Path) -> str: data = path.read_bytes() try: return data.decode("utf-8-sig") except UnicodeDecodeError: # Older Windows NX layers use the ANSI code page. return data.decode("cp1252") def read_def_symbols(path: Path) -> DefSymbols: return parse_def_symbols(read_def_source(path))