diff --git a/docs/syntax.md b/docs/syntax.md index ae6a8508..0811699a 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -85,6 +85,9 @@ tweak: # optional tweaking of .gv output # loops loops: # every list item is itself a list of exactly two pins # on the connector that are to be shorted + + # optional tweaking of .gv output executed for each instance of this connector + tweak: # see tweak section below ``` ## Cable attributes @@ -148,6 +151,8 @@ tweak: # optional tweaking of .gv output show_wirecount: # defaults to true show_wirenumbers: # defaults to true for cables; false for bundles + # optional tweaking of .gv output executed for each instance of this cable + tweak: # see tweak section below ``` ## Connection sets @@ -457,6 +462,12 @@ Alternatively items can be added to just the BOM by putting them in the section # This feature is experimental and might change # or be removed in future versions. + placeholder: # Substring to be replaced with the node name in + # any per-connector / per-cable tweak overrides and append entries. + # An empty string disables placeholder substitution for that node. + # When omitted at the per-node level, the global placeholder + # (in the top-level tweak: section) is used as the fallback. + override: # dict of .gv entries to override # Each entry is identified by its leading string # in lines beginning with a TAB character. diff --git a/src/wireviz/DataClasses.py b/src/wireviz/DataClasses.py index 66b92c38..68ec326d 100644 --- a/src/wireviz/DataClasses.py +++ b/src/wireviz/DataClasses.py @@ -79,6 +79,7 @@ def __post_init__(self): @dataclass class Tweak: + placeholder: Optional[PlainText] = None override: Optional[Dict[Designator, Dict[str, Optional[str]]]] = None append: Union[str, List[str], None] = None @@ -170,10 +171,13 @@ class Connector: loops: List[List[Pin]] = field(default_factory=list) ignore_in_bom: bool = False additional_components: List[AdditionalComponent] = field(default_factory=list) + tweak: Optional[Tweak] = None def __post_init__(self) -> None: if isinstance(self.image, dict): self.image = Image(**self.image) + if isinstance(self.tweak, dict): + self.tweak = Tweak(**self.tweak) self.ports_left = False self.ports_right = False @@ -335,10 +339,13 @@ class Cable: show_wirenumbers: Optional[bool] = None ignore_in_bom: bool = False additional_components: List[AdditionalComponent] = field(default_factory=list) + tweak: Optional[Tweak] = None def __post_init__(self) -> None: if isinstance(self.image, dict): self.image = Image(**self.image) + if isinstance(self.tweak, dict): + self.tweak = Tweak(**self.tweak) if isinstance(self.gauge, str): # gauge and unit specified try: diff --git a/src/wireviz/Harness.py b/src/wireviz/Harness.py index c753cae1..66461362 100644 --- a/src/wireviz/Harness.py +++ b/src/wireviz/Harness.py @@ -30,6 +30,7 @@ component_table_entry, generate_bom, get_additional_component_table, + make_list, pn_info_string, ) from wireviz.wv_colors import get_color_hex, translate_color @@ -84,9 +85,57 @@ def __post_init__(self): def add_connector(self, name: str, *args, **kwargs) -> None: check_old(f"Connector '{name}'", OLD_CONNECTOR_ATTR, kwargs) self.connectors[name] = Connector(name, *args, **kwargs) + self._extend_tweak(self.connectors[name]) def add_cable(self, name: str, *args, **kwargs) -> None: self.cables[name] = Cable(name, *args, **kwargs) + self._extend_tweak(self.cables[name]) + + def _extend_tweak(self, node: Union[Connector, Cable]) -> None: + """Fold ``node.tweak`` into ``self.tweak`` after substituting the + node's name for the placeholder string. + + Per-connector / per-cable ``tweak:`` entries let users author a + single template and have its ``override`` keys / ``append`` lines + rewritten with the actual designator at instantiation time. This + is the only place the placeholder substitution happens — the + global tweak is applied unchanged at graph emission time. + """ + if not node.tweak: + return + ph = node.tweak.placeholder + # An empty string is a legal value to opt out of the global + # placeholder; only None falls back. + if ph is None: + ph = self.tweak.placeholder + # The replacement target may be None when an override deletes a + # key (``key: null`` in YAML), so guard the str.replace call. + if ph: + rph = lambda s: s.replace(ph, node.name) if isinstance(s, str) else s + else: + rph = lambda s: s + + n_override = node.tweak.override or {} + s_override = self.tweak.override or {} + for ident, n_dict in n_override.items(): + ident = rph(ident) + s_dict = s_override.get(ident, {}) + for k, v in n_dict.items(): + k, v = rph(k), rph(v) + if k in s_dict and v != s_dict[k]: + raise ValueError( + f"{node.name}.tweak.override.{ident}.{k} conflicts with another" + ) + s_dict[k] = v + # Keep the empty dict rather than collapsing to None — the + # graph-emission code (Harness.create_graph) expects values + # in self.tweak.override to be dicts, not None. + s_override[ident] = s_dict + self.tweak.override = s_override or None + self.tweak.append = ( + make_list(self.tweak.append) + + [rph(v) for v in make_list(node.tweak.append)] + ) or None def add_mate_pin(self, from_name, from_pin, to_name, to_pin, arrow_type) -> None: self.mates.append(MatePin(from_name, from_pin, to_name, to_pin, arrow_type))