diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 24c0458..6620700 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, polar chart (polar/radial lollipop), loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.6.2", + "version": "2.6.4", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 6a152f6..1510523 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, polar chart (polar/radial lollipop), loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.6.2", + "version": "2.6.4", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/.factory-plugin/plugin.json b/.factory-plugin/plugin.json index 24c0458..6620700 100644 --- a/.factory-plugin/plugin.json +++ b/.factory-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "diagram-design", "description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, polar chart (polar/radial lollipop), loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.", - "version": "2.6.2", + "version": "2.6.4", "author": { "name": "Cathryn Lavery", "url": "https://github.com/cathrynlavery" diff --git a/scripts/verify-mermaid-import.py b/scripts/verify-mermaid-import.py index 90dfc9a..4b41d7b 100644 --- a/scripts/verify-mermaid-import.py +++ b/scripts/verify-mermaid-import.py @@ -284,6 +284,11 @@ B--no-->A C-.fast.->D D==crit==>E E--tie---A +K<--both-->L +M o--circle--o N +O x--blocked--x P +Box--yes-->C +Echo--go-->D F --o G --> H I----->J """, @@ -291,18 +296,80 @@ I----->J ) compact = json.loads(run_extract([str(compact_file), "--json"]))["diagrams"][0] compact_ids = sorted(node["id"] for node in compact["nodes"]) - if compact_ids != ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]: + if compact_ids != [ + "A", "B", "Box", "C", "D", "E", "Echo", "F", "G", "H", "I", "J", + "K", "L", "M", "N", "O", "P", + ]: fail(f"compact edge labels materialized phantom nodes: {compact_ids}") compact_labels = [edge["label"] for edge in compact["edges"]] - if compact_labels[:6] != ["", "yes", "no", "fast", "crit", "tie"]: + if compact_labels[:11] != [ + "", "yes", "no", "fast", "crit", "tie", "both", "circle", "blocked", + "yes", "go", + ]: fail(f"compact edge labels were not retained: {compact_labels}") - if compact_labels[6:] != ["", "", ""]: + if compact_labels[11:] != ["", "", ""]: fail( "operator characters or chained links were misread as compact " f"labels: {compact_labels}" ) if compact["edges"][3]["style"] != "dashed" or compact["edges"][4]["style"] != "thick": fail("compact dotted/thick labeled links lost their styles") + arrow_bidir, circle_bidir, cross_bidir = compact["edges"][6:9] + if not arrow_bidir["bidirectional"] or arrow_bidir["arrowhead"] != "arrow": + fail("compact labeled `<-- -->` link lost bidirectional arrow semantics") + if not circle_bidir["bidirectional"] or circle_bidir["arrowhead"] != "circle": + fail("compact labeled `o-- --o` link lost bidirectional circle semantics") + if not cross_bidir["bidirectional"] or cross_bidir["arrowhead"] != "cross": + fail("compact labeled `x-- --x` link lost bidirectional cross semantics") + if compact["edges"][9]["source"] != "Box" or compact["edges"][10]["source"] != "Echo": + fail("source IDs ending in x/o were consumed as left edge markers") + + single_marker_source_file = tmp / "single-marker-source-links.mmd" + single_marker_source_file.write_text( + """flowchart LR +x--yes-->B +o--go-->C +""", + encoding="utf-8", + ) + single_marker_source = json.loads( + run_extract([str(single_marker_source_file), "--json"]) + )["diagrams"][0] + single_marker_edges = [ + (edge["source"], edge["target"], edge["label"]) + for edge in single_marker_source["edges"] + ] + if single_marker_edges != [ + ("x", "B", "yes"), + ("o", "C", "go"), + ]: + fail("single-character x/o source IDs were consumed as left edge markers") + + chained_marker_source_file = tmp / "chained-marker-source-links.mmd" + chained_marker_source_file.write_text( + """flowchart LR +A--yes-->x--go-->B +C--no--> o--wait-->D +E-->|maybe|x--next-->F +""", + encoding="utf-8", + ) + chained_marker_source = json.loads( + run_extract([str(chained_marker_source_file), "--json"]) + )["diagrams"][0] + chained_marker_edges = [ + (edge["source"], edge["target"], edge["label"]) + for edge in chained_marker_source["edges"] + ] + if chained_marker_edges != [ + ("A", "x", "yes"), + ("x", "B", "go"), + ("C", "o", "no"), + ("o", "D", "wait"), + ("E", "x", "maybe"), + ("x", "F", "next"), + ]: + fail("chained x/o endpoint IDs were consumed as left edge markers") modern_file = tmp / "modern-flowchart.mmd" modern_file.write_text( diff --git a/skills/diagram-design/scripts/mermaid_extract.py b/skills/diagram-design/scripts/mermaid_extract.py index 490b53c..03195e4 100644 --- a/skills/diagram-design/scripts/mermaid_extract.py +++ b/skills/diagram-design/scripts/mermaid_extract.py @@ -605,21 +605,39 @@ def _edge_operators(text: str) -> list[_Operator]: # Labeled links carry the label between the opening and closing operator: # `A-- text -->B`, `A-. retry .-> B`, `A== critical ==> B`, and the # undirected forms of each. The compact form drops the spaces — - # `B--yes-->C` — so its label may not contain whitespace, and the operator - # characters themselves may not open one (keeping `A----->B` unlabeled and - # `A --o B --> C` two separate links). + # `B--yes-->C` — and may retain a left arrow/circle/cross marker, as in + # `A<--yes-->B` or `A o--yes--o B`. Its label may not contain whitespace, + # and the operator characters themselves may not open one (keeping + # `A----->B` unlabeled and `A --o B --> C` two separate links). text_edge = re.compile( - r"(?:--|-\.|==)" - r"(?:\s+(.+?)\s+|(?![-=.\s])([^\s|<>]+?))" - r"(\.-+[>xo]|\.-+|-{2,}>|--[xo]|=+>|={2,}|-{3,})" + r"(?P" + r"<(?:--|-\.|==)" + r"|(?.+?)\s+|(?![-=.\s])(?P[^\s|<>]+?))" + r"(?P\.-+[>xo]|\.-+|-{2,}>|--[xo]|=+>|={2,}|-{3,})" + ) + trailing_operator = re.compile( + r"(?:\.-+[>xo]|\.-+|-{2,}>|--[xo]|=+>|={2,}|-{3,})" + r"(?:\|[^|\n]*\|)?\s*$" ) for match in text_edge.finditer(mask): - token = match.group(3) - label_group = 1 if match.group(1) is not None else 2 + opening = match.group("opening") + operator_start = match.start() + if opening.startswith(("x", "o")): + prefix = mask[:operator_start] + if not prefix.strip() or trailing_operator.search(prefix): + # Here x/o is the endpoint before a regular opening operator, + # not a left marker: `x--yes-->B` or `A-->x--go-->B`. + operator_start += 1 + opening = opening[1:] + token = opening + match.group("closing") + label_group = "spaced" if match.group("spaced") is not None else "compact" style, arrowhead, bidirectional, undirected = _operator_style(token) operators.append( _Operator( - match.start(), + operator_start, match.end(), clean_label(text[match.start(label_group) : match.end(label_group)]), style, @@ -628,7 +646,7 @@ def _edge_operators(text: str) -> list[_Operator]: undirected, ) ) - occupied.append((match.start(), match.end())) + occupied.append((operator_start, match.end())) pattern = re.compile( r"[xo][-=.]+[xo]|<[-=.]+>|-+\.-+>|=+>|-+(?:>|x|o)|-+\.-+|={3,}|-{3,}"