diff --git a/web/backend/app/services/altium_parser.py b/web/backend/app/services/altium_parser.py index 27af848..80dace2 100644 --- a/web/backend/app/services/altium_parser.py +++ b/web/backend/app/services/altium_parser.py @@ -446,17 +446,11 @@ class AltiumParser: def make_complex_string(text: str, props: list[ComponentProperty]) -> str: - text = sanitize_text(text) - if not text.startswith("="): - return text - expr = text[1:] + from app.services.designators import resolve_expression + mapping = {p.name: p.text for p in props} - - def repl(m: re.Match) -> str: - key = m.group(1) - return mapping.get(key, "") - - return sanitize_text(re.sub(r"['\"]([^'\"]+)['\"]", repl, expr)) + designator = mapping.get("Designator", "") + return sanitize_text(resolve_expression(text, mapping, designator)) def find_prjpcb(extract_dir: Path) -> Optional[Path]: diff --git a/web/backend/app/services/designators.py b/web/backend/app/services/designators.py index cca40ff..47b8d4c 100644 --- a/web/backend/app/services/designators.py +++ b/web/backend/app/services/designators.py @@ -146,33 +146,67 @@ def split_long_text(text: str, max_length: int) -> list[str]: return parts or [""] -def resolve_expression(value: str, props: dict[str, str]) -> str: +def _split_expression_parts(expr: str) -> list[str]: + """Split Altium expression by + outside quotes.""" + parts: list[str] = [] + current: list[str] = [] + in_double = False + in_single = False + for ch in expr: + if ch == '"' and not in_single: + in_double = not in_double + current.append(ch) + elif ch == "'" and not in_double: + in_single = not in_single + current.append(ch) + elif ch == "+" and not in_double and not in_single: + if current: + parts.append("".join(current).strip()) + current = [] + else: + current.append(ch) + if current: + parts.append("".join(current).strip()) + return parts + + +def _resolve_property(name: str, props: dict[str, str], designator: str = "") -> str: + if name == "Designator": + raw = designator or props.get("Designator", "") + else: + raw = props.get(name, "") + if not raw: + return "" + if raw.startswith("=") or (raw.startswith('"') and raw.endswith('"') and len(raw) > 1): + return resolve_expression(raw, props, designator) + return raw + + +def resolve_expression(value: str, props: dict[str, str], designator: str = "") -> str: + """ + Altium complex string: ='literal'+"Property"+Name + - single quotes = literal text + - double quotes = component property reference + - bare identifiers = property reference + """ + if not value: + return "" + value = value.strip() + if value.startswith('"') and value.endswith('"') and not value.startswith('="'): + return _resolve_property(value[1:-1], props, designator) if not value.startswith("="): return value - expr = value[1:] - result = [] - i = 0 - while i < len(expr): - ch = expr[i] - if ch in "'\"": - quote = ch - i += 1 - start = i - while i < len(expr) and expr[i] != quote: - i += 1 - result.append(expr[start:i]) - i += 1 - elif ch == "+": - i += 1 - elif ch.isspace(): - i += 1 + + result: list[str] = [] + for part in _split_expression_parts(value[1:]): + if not part: + continue + if part.startswith("'") and part.endswith("'") and len(part) >= 2: + result.append(part[1:-1]) + elif part.startswith('"') and part.endswith('"') and len(part) >= 2: + result.append(_resolve_property(part[1:-1], props, designator)) + elif part == "Designator": + result.append(designator or props.get("Designator", "")) else: - start = i - while i < len(expr) and expr[i] not in "+'\"": - i += 1 - key = expr[start:i].strip() - val = props.get(key, "") - if val.startswith("="): - val = resolve_expression(val, props) - result.append(val) + result.append(_resolve_property(part, props, designator)) return "".join(result) diff --git a/web/backend/app/services/table_generators.py b/web/backend/app/services/table_generators.py index eedbb5a..a6803dc 100644 --- a/web/backend/app/services/table_generators.py +++ b/web/backend/app/services/table_generators.py @@ -24,8 +24,12 @@ class ComponentView: is_fitted: bool = True def get(self, key: str, default: str = "") -> str: + if key == "Designator": + return self.designator or self.properties.get("Designator", default) raw = self.properties.get(key, default) - return resolve_expression(raw, self.properties) if raw else default + if not raw: + return default + return resolve_expression(raw, self.properties, self.designator) @dataclass