1"""Tiny DOT builder implementing the subset used by WireViz.23WireViz imports the Python graphviz package only to construct DOT. The real4layout process is deliberately left to the shared GraphViz-Wasm renderer.5"""6import re78def _quote(value):9 value = str(value)10 return value if value.startswith('<') and value.endswith('>') else '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'1112def _attrs(attributes):13 return ' '.join(f'{key}={_quote(value)}' for key, value in attributes.items())1415def _endpoint(value):16 """Quote only the node portion of `node:port:compass` endpoints."""17 match = re.match(r'^(.*?)(?=:[^:]+(?::[a-z]+)?$)', str(value), re.IGNORECASE)18 if not match:19 return value if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', str(value)) else _quote(value)20 node, suffix = str(value)[:match.end()], str(value)[match.end():]21 return (node if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*', node) else _quote(node)) + suffix2223class Graph:24 def __init__(self, name='G', **_kwargs):25 self.name = name26 self.body = []27 self._graph = {}28 self._node = {}29 self._edge = {}3031 def attr(self, kw=None, _attributes=None, **attrs):32 target = kw or 'graph'33 values = dict(_attributes or {})34 values.update(attrs)35 if target == 'graph': self._graph.update(values)36 elif target == 'node': self._node.update(values)37 elif target == 'edge': self._edge.update(values)3839 def node(self, name, label=None, **attrs):40 values = dict(attrs)41 if label is not None: values['label'] = label42 self.body.append(f'{_quote(name)} [{_attrs(values)}]')4344 def edge(self, tail, head, **attrs):45 values = dict(self._edge)46 values.update(attrs)47 self.body.append(f'{_endpoint(tail)} -- {_endpoint(head)}' + (f' [{_attrs(values)}]' if values else ''))484950 def source(self):51 lines = ['graph {']52 if self._graph: lines.append(f'\tgraph [{_attrs(self._graph)}]')53 if self._node: lines.append(f'\tnode [{_attrs(self._node)}]')54 if self._edge: lines.append(f'\tedge [{_attrs(self._edge)}]')55 lines.extend(f'\t{entry}' for entry in self.body)56 lines.append('}')57 return '\n'.join(lines)