fix(mermaid): parse compact multidirectional labels (#131)

* fix(mermaid): parse compact multidirectional labels

* fix(mermaid): preserve source ids ending in markers

* chore: bump plugin version to 2.6.2

* chore: bump plugin version to 2.6.4

* fix: preserve single-character Mermaid source IDs

* fix: preserve chained Mermaid marker IDs

---------

Co-authored-by: Cathryn Lavery <cathryn@bestself.co>
This commit is contained in:
CHACON MONDRAGON
2026-08-21 14:31:30 -05:00
committed by GitHub
co-authored by Cathryn Lavery
parent ddaeae936e
commit c79585edb4
5 changed files with 101 additions and 16 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "diagram-design", "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.", "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": { "author": {
"name": "Cathryn Lavery", "name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery" "url": "https://github.com/cathrynlavery"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "diagram-design", "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.", "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": { "author": {
"name": "Cathryn Lavery", "name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery" "url": "https://github.com/cathrynlavery"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "diagram-design", "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.", "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": { "author": {
"name": "Cathryn Lavery", "name": "Cathryn Lavery",
"url": "https://github.com/cathrynlavery" "url": "https://github.com/cathrynlavery"
+70 -3
View File
@@ -284,6 +284,11 @@ B--no-->A
C-.fast.->D C-.fast.->D
D==crit==>E D==crit==>E
E--tie---A 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 F --o G --> H
I----->J I----->J
""", """,
@@ -291,18 +296,80 @@ I----->J
) )
compact = json.loads(run_extract([str(compact_file), "--json"]))["diagrams"][0] compact = json.loads(run_extract([str(compact_file), "--json"]))["diagrams"][0]
compact_ids = sorted(node["id"] for node in compact["nodes"]) 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}") fail(f"compact edge labels materialized phantom nodes: {compact_ids}")
compact_labels = [edge["label"] for edge in compact["edges"]] 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}") fail(f"compact edge labels were not retained: {compact_labels}")
if compact_labels[6:] != ["", "", ""]: if compact_labels[11:] != ["", "", ""]:
fail( fail(
"operator characters or chained links were misread as compact " "operator characters or chained links were misread as compact "
f"labels: {compact_labels}" f"labels: {compact_labels}"
) )
if compact["edges"][3]["style"] != "dashed" or compact["edges"][4]["style"] != "thick": if compact["edges"][3]["style"] != "dashed" or compact["edges"][4]["style"] != "thick":
fail("compact dotted/thick labeled links lost their styles") 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 = tmp / "modern-flowchart.mmd"
modern_file.write_text( modern_file.write_text(
@@ -605,21 +605,39 @@ def _edge_operators(text: str) -> list[_Operator]:
# Labeled links carry the label between the opening and closing operator: # Labeled links carry the label between the opening and closing operator:
# `A-- text -->B`, `A-. retry .-> B`, `A== critical ==> B`, and the # `A-- text -->B`, `A-. retry .-> B`, `A== critical ==> B`, and the
# undirected forms of each. The compact form drops the spaces — # undirected forms of each. The compact form drops the spaces —
# `B--yes-->C` — so its label may not contain whitespace, and the operator # `B--yes-->C` — and may retain a left arrow/circle/cross marker, as in
# characters themselves may not open one (keeping `A----->B` unlabeled and # `A<--yes-->B` or `A o--yes--o B`. Its label may not contain whitespace,
# `A --o B --> C` two separate links). # 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( text_edge = re.compile(
r"(?:--|-\.|==)" r"(?P<opening>"
r"(?:\s+(.+?)\s+|(?![-=.\s])([^\s|<>]+?))" r"<(?:--|-\.|==)"
r"(\.-+[>xo]|\.-+|-{2,}>|--[xo]|=+>|={2,}|-{3,})" r"|(?<![\w.:-])[xo](?:--|-\.|==)"
r"|(?:--|-\.|==)"
r")"
r"(?:\s+(?P<spaced>.+?)\s+|(?![-=.\s])(?P<compact>[^\s|<>]+?))"
r"(?P<closing>\.-+[>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): for match in text_edge.finditer(mask):
token = match.group(3) opening = match.group("opening")
label_group = 1 if match.group(1) is not None else 2 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) style, arrowhead, bidirectional, undirected = _operator_style(token)
operators.append( operators.append(
_Operator( _Operator(
match.start(), operator_start,
match.end(), match.end(),
clean_label(text[match.start(label_group) : match.end(label_group)]), clean_label(text[match.start(label_group) : match.end(label_group)]),
style, style,
@@ -628,7 +646,7 @@ def _edge_operators(text: str) -> list[_Operator]:
undirected, undirected,
) )
) )
occupied.append((match.start(), match.end())) occupied.append((operator_start, match.end()))
pattern = re.compile( pattern = re.compile(
r"[xo][-=.]+[xo]|<[-=.]+>|-+\.-+>|=+>|-+(?:>|x|o)|-+\.-+|={3,}|-{3,}" r"[xo][-=.]+[xo]|<[-=.]+>|-+\.-+>|=+>|-+(?:>|x|o)|-+\.-+|={3,}|-{3,}"