skilldrop-cli 1.1.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/catalogue.json +7 -8
- package/model-routing.json +0 -16
- package/package.json +1 -1
- package/packs/ai-engineering/loops/adopt/LOOP.md +120 -0
- package/packs/ai-engineering/loops/adopt/evals/eval_queries.json +10 -0
- package/packs/ai-engineering/loops/adopt/evals/evals.json +69 -0
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/ai-ideas.md +15 -0
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/org-context.md +27 -0
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-first-wave-to-sign-off/staff-survey.csv +11 -0
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/ai-ideas.md +13 -0
- package/packs/ai-engineering/loops/adopt/evals/files/adopt-sponsor-slice-blocked-by-client-data/firm-context.md +23 -0
- package/packs/ai-engineering/loops/adopt/loop.json +53 -0
- package/packs/ai-engineering/loops/ship-agent/LOOP.md +128 -0
- package/packs/ai-engineering/loops/ship-agent/evals/eval_queries.json +10 -0
- package/packs/ai-engineering/loops/ship-agent/evals/evals.json +70 -0
- package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-auto-reply-exfil-open/agent-brief.md +33 -0
- package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-changelog-config-reopens-path/agent-brief.md +33 -0
- package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-changelog-config-reopens-path/claude-settings.json +25 -0
- package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-changelog-config-reopens-path/sample-merged-prs.md +30 -0
- package/packs/ai-engineering/loops/ship-agent/loop.json +53 -0
- package/packs/ai-engineering/skills/ai-usage-report/examples/weekly-engineering-team.md +1 -1
- package/packs/ai-engineering/skills/ai-usage-report/templates/usage-event-schema.md +2 -2
- package/packs/converters/pack.json +3 -14
- package/packs/converters/skills/file-to-markdown/SKILL.md +28 -11
- package/packs/converters/skills/file-to-markdown/evals/evals.json +30 -0
- package/packs/converters/skills/file-to-markdown/evals/files/5/supplier_agreement_v3.docx +0 -0
- package/packs/converters/skills/file-to-markdown/evals/files/6/numbers.xlsx +0 -0
- package/packs/converters/skills/file-to-markdown/evals/files/6/offsite_q3.pptx +0 -0
- package/packs/converters/skills/file-to-markdown/examples/inputs/acme-brief.docx +0 -0
- package/packs/converters/skills/file-to-markdown/examples/inputs/northwind-roadmap.pptx +0 -0
- package/packs/converters/skills/file-to-markdown/examples/mixed-inputs.md +24 -16
- package/packs/converters/skills/file-to-markdown/manifest.json +2 -3
- package/packs/converters/skills/file-to-markdown/scripts/to_markdown.py +103 -10
- package/packs/converters/skills/md-to-confluence/SKILL.md +2 -2
- package/packs/converters/skills/md-to-confluence/manifest.json +2 -9
- package/packs/converters/skills/md-to-docx/SKILL.md +3 -4
- package/packs/converters/skills/md-to-docx/evals/evals.json +1 -1
- package/packs/converters/skills/md-to-docx/evals/files/1/brand/brand.json +1 -1
- package/packs/converters/skills/md-to-docx/manifest.json +4 -11
- package/packs/converters/skills/md-to-docx/reference.md +1 -1
- package/packs/converters/skills/md-to-docx/scripts/md_to_docx.py +1 -1
- package/packs/converters/skills/md-to-pptx/SKILL.md +3 -5
- package/packs/converters/skills/md-to-pptx/manifest.json +3 -16
- package/packs/converters/skills/md-to-pptx/scripts/md_to_pptx.py +1 -1
- package/packs/converters/skills/md-to-xlsx/SKILL.md +1 -1
- package/packs/converters/skills/md-to-xlsx/manifest.json +2 -2
- package/packs/core/skills/brief-intake/SKILL.md +2 -2
- package/packs/core/skills/brief-intake/manifest.json +2 -2
- package/packs/core/skills/brief-intake/templates/brief-deck.md +3 -3
- package/packs/core/skills/doc-critique/rubrics/deck.md +1 -1
- package/packs/design/pack.json +11 -21
- package/packs/design/skills/brand-kit/SKILL.md +6 -8
- package/packs/design/skills/brand-kit/manifest.json +3 -17
- package/packs/design/skills/brand-kit/templates/brand.json +1 -1
- package/packs/experience-design/loops/clarify/LOOP.md +114 -0
- package/packs/experience-design/loops/clarify/evals/eval_queries.json +10 -0
- package/packs/experience-design/loops/clarify/evals/evals.json +68 -0
- package/packs/experience-design/loops/clarify/evals/files/clarify-garden-waste-no-complaint-data/form-fields.csv +12 -0
- package/packs/experience-design/loops/clarify/evals/files/clarify-garden-waste-no-complaint-data/garden-waste-page.md +29 -0
- package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/backoffice-notes.md +14 -0
- package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/calls-q3.csv +8 -0
- package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/form-fields.csv +12 -0
- package/packs/experience-design/loops/clarify/evals/files/clarify-parking-permit-with-call-data/permit-page.md +27 -0
- package/packs/experience-design/loops/clarify/loop.json +35 -0
- package/packs/infra-as-code/loops/provision/LOOP.md +128 -0
- package/packs/infra-as-code/loops/provision/evals/eval_queries.json +9 -0
- package/packs/infra-as-code/loops/provision/evals/evals.json +67 -0
- package/packs/infra-as-code/loops/provision/evals/files/provision-rds-no-plan-yet/.terraform.lock.hcl.fixture +24 -0
- package/packs/infra-as-code/loops/provision/evals/files/provision-rds-no-plan-yet/network.md +36 -0
- package/packs/infra-as-code/loops/provision/evals/files/provision-replica-plan-replaces-primary/main.tf +74 -0
- package/packs/infra-as-code/loops/provision/evals/files/provision-replica-plan-replaces-primary/plan.json +901 -0
- package/packs/infra-as-code/loops/provision/evals/files/provision-replica-plan-replaces-primary/variables.tf +47 -0
- package/packs/infra-as-code/loops/provision/loop.json +41 -0
- package/packs/solution-architect/skills/interactive-diagram/SKILL.md +1 -1
- package/packs/solution-architect/skills/interactive-diagram/manifest.json +1 -2
- package/packs/stakeholder-comms/skills/audience-profile/SKILL.md +3 -3
- package/packs/stakeholder-comms/skills/audience-profile/manifest.json +2 -2
- package/packs/stakeholder-comms/skills/audience-profile/reference.md +1 -1
- package/packs/converters/skills/md-to-html/SKILL.md +0 -108
- package/packs/converters/skills/md-to-html/evals/eval_queries.json +0 -34
- package/packs/converters/skills/md-to-html/evals/evals.json +0 -44
- package/packs/converters/skills/md-to-html/evals/files/1/brand/brand.json +0 -80
- package/packs/converters/skills/md-to-html/evals/files/1/brand/logo-white.png +0 -0
- package/packs/converters/skills/md-to-html/evals/files/1/brand/logo.png +0 -0
- package/packs/converters/skills/md-to-html/evals/files/1/brand/mark.png +0 -0
- package/packs/converters/skills/md-to-html/evals/files/1/docs/q3-review.md +0 -35
- package/packs/converters/skills/md-to-html/evals/files/1/img/dashboard.png +0 -0
- package/packs/converters/skills/md-to-html/evals/files/2/brand.json +0 -13
- package/packs/converters/skills/md-to-html/evals/files/2/img/kick-net.png +0 -0
- package/packs/converters/skills/md-to-html/evals/files/2/river-survey-guide.md +0 -53
- package/packs/converters/skills/md-to-html/evals/files/3/nanas-lentil-soup.md +0 -21
- package/packs/converters/skills/md-to-html/evals/files/3/pot.png +0 -0
- package/packs/converters/skills/md-to-html/examples/inputs/chart.svg +0 -8
- package/packs/converters/skills/md-to-html/examples/inputs/ops-review.md +0 -48
- package/packs/converters/skills/md-to-html/examples/ops-review.md +0 -83
- package/packs/converters/skills/md-to-html/manifest.json +0 -43
- package/packs/converters/skills/md-to-html/reference.md +0 -70
- package/packs/converters/skills/md-to-html/scripts/md_to_html.py +0 -662
- package/packs/converters/skills/mermaid-render/SKILL.md +0 -117
- package/packs/converters/skills/mermaid-render/evals/eval_queries.json +0 -30
- package/packs/converters/skills/mermaid-render/evals/evals.json +0 -43
- package/packs/converters/skills/mermaid-render/evals/files/1/docs/contoso-orders.md +0 -23
- package/packs/converters/skills/mermaid-render/evals/files/2/dispensing-diagrams.md +0 -39
- package/packs/converters/skills/mermaid-render/examples/broken-diagrams.md +0 -147
- package/packs/converters/skills/mermaid-render/examples/inputs/broken-flowchart.mmd +0 -8
- package/packs/converters/skills/mermaid-render/examples/inputs/broken-sequence.mmd +0 -11
- package/packs/converters/skills/mermaid-render/examples/inputs/checkout-flow.mmd +0 -9
- package/packs/converters/skills/mermaid-render/examples/inputs/design-notes.md +0 -35
- package/packs/converters/skills/mermaid-render/examples/inputs/fixed-flowchart.mmd +0 -9
- package/packs/converters/skills/mermaid-render/examples/inputs/typo.mmd +0 -2
- package/packs/converters/skills/mermaid-render/manifest.json +0 -50
- package/packs/converters/skills/mermaid-render/reference.md +0 -70
- package/packs/converters/skills/mermaid-render/scripts/render_mermaid.py +0 -487
- package/packs/design/skills/deck-builder/SKILL.md +0 -209
- package/packs/design/skills/deck-builder/evals/eval_queries.json +0 -14
- package/packs/design/skills/deck-builder/evals/evals.json +0 -67
- package/packs/design/skills/deck-builder/evals/files/4/ashby-vale-board-notes.md +0 -36
- package/packs/design/skills/deck-builder/examples/exec-board-update.md +0 -108
- package/packs/design/skills/deck-builder/examples/templated-brand-deck.md +0 -148
- package/packs/design/skills/deck-builder/manifest.json +0 -18
- package/packs/design/skills/deck-builder/reference.md +0 -180
- package/packs/design/skills/deck-builder/requirements.txt +0 -3
- package/packs/design/skills/deck-builder/scripts/build_deck.py +0 -1144
- package/packs/design/skills/deck-builder/templates/deck-spec.json +0 -168
- package/packs/design/skills/deck-builder/templates/palettes.json +0 -59
- package/packs/design/skills/marketing-flyer/SKILL.md +0 -113
- package/packs/design/skills/marketing-flyer/evals/eval_queries.json +0 -30
- package/packs/design/skills/marketing-flyer/evals/evals.json +0 -45
- package/packs/design/skills/marketing-flyer/evals/files/1/brand/brand.json +0 -80
- package/packs/design/skills/marketing-flyer/evals/files/1/brand/logo-white.png +0 -0
- package/packs/design/skills/marketing-flyer/evals/files/1/brand/logo.png +0 -0
- package/packs/design/skills/marketing-flyer/evals/files/1/brand/mark.png +0 -0
- package/packs/design/skills/marketing-flyer/evals/files/2/brand.json +0 -14
- package/packs/design/skills/marketing-flyer/evals/files/2/larchfield-logo.svg +0 -4
- package/packs/design/skills/marketing-flyer/evals/files/2/manager-email.txt +0 -6
- package/packs/design/skills/marketing-flyer/evals/files/2/open-day-brief.md +0 -15
- package/packs/design/skills/marketing-flyer/examples/community-health-fair.md +0 -68
- package/packs/design/skills/marketing-flyer/manifest.json +0 -40
- package/packs/design/skills/marketing-flyer/reference.md +0 -54
- package/packs/design/skills/marketing-flyer/scripts/build_flyer.py +0 -332
- package/packs/design/skills/marketing-flyer/templates/flyer-spec.json +0 -26
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Lint Mermaid diagrams for the errors that stop them rendering, then render them.
|
|
3
|
-
|
|
4
|
-
Stdlib only, Python 3.9+.
|
|
5
|
-
|
|
6
|
-
Lint catches, with line numbers: an unknown diagram type on the first line, unbalanced
|
|
7
|
-
brackets or quotes in node labels, unquoted parentheses or brackets inside a label, the
|
|
8
|
-
reserved word `end` used as a flowchart node id, a missing or single-dash arrow, and a
|
|
9
|
-
subgraph (or sequence `alt`/`loop`/... block) with no `end`.
|
|
10
|
-
|
|
11
|
-
Render uses `mmdc` (mermaid-cli) for .svg/.png/.pdf when it is on PATH. Without it, the
|
|
12
|
-
script writes a standalone .html that draws the diagrams with a pinned Mermaid from
|
|
13
|
-
jsDelivr, which needs network when the file is opened.
|
|
14
|
-
|
|
15
|
-
Usage:
|
|
16
|
-
python3 render_mermaid.py diagram.mmd # lint only
|
|
17
|
-
python3 render_mermaid.py diagram.mmd -o diagram.svg # lint, then render
|
|
18
|
-
python3 render_mermaid.py design.md -o figs/arch.svg # every ```mermaid fence -> arch-1.svg, arch-2.svg ...
|
|
19
|
-
python3 render_mermaid.py design.md -o design.html # one HTML page with every diagram
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
import argparse
|
|
23
|
-
import base64
|
|
24
|
-
import difflib
|
|
25
|
-
import hashlib
|
|
26
|
-
import html
|
|
27
|
-
import re
|
|
28
|
-
import shutil
|
|
29
|
-
import subprocess
|
|
30
|
-
import sys
|
|
31
|
-
import tempfile
|
|
32
|
-
from pathlib import Path
|
|
33
|
-
|
|
34
|
-
MERMAID_VERSION = "11.4.1"
|
|
35
|
-
MERMAID_URL = "https://cdn.jsdelivr.net/npm/mermaid@%s/dist/mermaid.esm.min.mjs" % MERMAID_VERSION
|
|
36
|
-
MERMAID_INIT = ('import mermaid from "%s";'
|
|
37
|
-
'mermaid.initialize({startOnLoad:true,securityLevel:"strict"});' % MERMAID_URL)
|
|
38
|
-
MERMAID_INIT_HASH = base64.b64encode(hashlib.sha256(MERMAID_INIT.encode("utf-8")).digest()).decode("ascii")
|
|
39
|
-
|
|
40
|
-
# Diagram keywords Mermaid 11.4 recognises on the first line.
|
|
41
|
-
DIAGRAM_TYPES = [
|
|
42
|
-
"flowchart", "graph", "sequenceDiagram", "classDiagram", "classDiagram-v2", "stateDiagram",
|
|
43
|
-
"stateDiagram-v2", "erDiagram", "journey", "gantt", "pie", "quadrantChart", "requirementDiagram",
|
|
44
|
-
"gitGraph", "C4Context", "C4Container", "C4Component", "C4Dynamic", "C4Deployment", "mindmap",
|
|
45
|
-
"timeline", "zenuml", "sankey-beta", "xychart-beta", "block-beta", "packet-beta",
|
|
46
|
-
"architecture-beta", "kanban",
|
|
47
|
-
]
|
|
48
|
-
DIRECTIONS = {"TB", "TD", "BT", "RL", "LR"}
|
|
49
|
-
FLOW_KEYWORDS = ("classDef", "class", "style", "linkStyle", "click", "direction", "accTitle",
|
|
50
|
-
"accDescr", "title")
|
|
51
|
-
SEQ_OPEN = ("loop", "alt", "opt", "par", "critical", "break", "rect", "box")
|
|
52
|
-
SEQ_MIDDLE = {"else": ("alt",), "and": ("par",), "option": ("critical",)}
|
|
53
|
-
|
|
54
|
-
# Node shapes, longest opener first. Each maps to the closers that may end it.
|
|
55
|
-
SHAPES = [("(((", (")))",)), ("((", ("))",)), ("([", ("])",)), ("[[", ("]]",)), ("[(", (")]",)),
|
|
56
|
-
("[/", ("/]", "\\]")), ("[\\", ("\\]", "/]")), ("{{", ("}}",)), ("[", ("]",)),
|
|
57
|
-
("(", (")",)), ("{", ("}",)), (">", ("]",))]
|
|
58
|
-
ID_RE = re.compile(r"[^\W](?:[\w.]|-(?=\w))*", re.U)
|
|
59
|
-
LINK_RE = re.compile(r"\s*(?:<|(?<=\s)[ox](?=[-=]))?(?:-{2,}|={2,}|-\.+-|~{3,})(?:[->ox](?!\w))?>?\s*")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class Issue:
|
|
63
|
-
def __init__(self, line, level, msg):
|
|
64
|
-
self.line, self.level, self.msg = line, level, msg
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def strip_edge_labels(s):
|
|
68
|
-
s = re.sub(r"\|[^|]*\|", " ", s) # -->|label|
|
|
69
|
-
s = re.sub(r"-\.(?=[^.\->\s]|\s+[^.\->])\s*([^\n]*?)\s*\.-+>?", " -.-> ", s) # -. label .->
|
|
70
|
-
s = re.sub(r"(?<![-<.=])--(?!-|>|[ox]\s)\s*([^-|>\n][^|>\n]*?)\s*(?:-{2,}>|-{3,})", " --> ", s)
|
|
71
|
-
s = re.sub(r"(?<![=<])==(?!=|>)\s*([^=|>\n][^|>\n]*?)\s*(?:={2,}>|={3,})", " ==> ", s)
|
|
72
|
-
return s
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def split_statements(line):
|
|
76
|
-
"""Split on ; outside quotes."""
|
|
77
|
-
out, cur, q = [], "", False
|
|
78
|
-
for ch in line:
|
|
79
|
-
if ch == '"':
|
|
80
|
-
q = not q
|
|
81
|
-
if ch == ";" and not q:
|
|
82
|
-
out.append(cur)
|
|
83
|
-
cur = ""
|
|
84
|
-
else:
|
|
85
|
-
cur += ch
|
|
86
|
-
out.append(cur)
|
|
87
|
-
return [s for s in out if s.strip()]
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def scan_nodes(part, ln, issues):
|
|
91
|
-
"""Walk one link-free chunk: node ids, their shapes, & separators. Returns node ids."""
|
|
92
|
-
i, n = 0, len(part)
|
|
93
|
-
ids, need_sep = [], False
|
|
94
|
-
while i < n:
|
|
95
|
-
c = part[i]
|
|
96
|
-
if c.isspace():
|
|
97
|
-
i += 1
|
|
98
|
-
continue
|
|
99
|
-
if c == "&":
|
|
100
|
-
need_sep = False
|
|
101
|
-
i += 1
|
|
102
|
-
continue
|
|
103
|
-
m = ID_RE.match(part, i)
|
|
104
|
-
if not m:
|
|
105
|
-
if c in ")]}":
|
|
106
|
-
issues.append(Issue(ln, "error", "closing `%s` with no matching opening bracket" % c))
|
|
107
|
-
elif c in "([{":
|
|
108
|
-
issues.append(Issue(ln, "error", "a shape `%s` with no node id before it" % c))
|
|
109
|
-
elif c == '"':
|
|
110
|
-
issues.append(Issue(ln, "error", "a quoted label must sit inside a shape, as id[\"label\"]"))
|
|
111
|
-
elif part.startswith("->", i) or part.startswith("=>", i):
|
|
112
|
-
issues.append(Issue(ln, "error", "`%s` is not a flowchart arrow; use `-->` (or `==>`)"
|
|
113
|
-
% part[i:i + 2]))
|
|
114
|
-
i += 2
|
|
115
|
-
need_sep = False
|
|
116
|
-
continue
|
|
117
|
-
else:
|
|
118
|
-
issues.append(Issue(ln, "error", "unexpected `%s`" % c))
|
|
119
|
-
return ids
|
|
120
|
-
nid = m.group(0)
|
|
121
|
-
if need_sep:
|
|
122
|
-
issues.append(Issue(ln, "error", "no arrow between `%s` and `%s`; join them with `-->`"
|
|
123
|
-
% (ids[-1], nid)))
|
|
124
|
-
ids.append(nid)
|
|
125
|
-
if nid == "end":
|
|
126
|
-
issues.append(Issue(ln, "error", "`end` is reserved in flowcharts and breaks the diagram; "
|
|
127
|
-
"use `End`, `END` or `done` as the node id"))
|
|
128
|
-
i = m.end()
|
|
129
|
-
if part.startswith("@{", i):
|
|
130
|
-
j = part.find("}", i)
|
|
131
|
-
if j < 0:
|
|
132
|
-
issues.append(Issue(ln, "error", "`@{` shape block is never closed with `}`"))
|
|
133
|
-
return ids
|
|
134
|
-
i = j + 1
|
|
135
|
-
else:
|
|
136
|
-
for opener, closers in SHAPES:
|
|
137
|
-
if not part.startswith(opener, i):
|
|
138
|
-
continue
|
|
139
|
-
i += len(opener)
|
|
140
|
-
if part[i:i + 1] == '"':
|
|
141
|
-
j = part.find('"', i + 1)
|
|
142
|
-
if j < 0:
|
|
143
|
-
issues.append(Issue(ln, "error", "unbalanced quote in the label of `%s`" % nid))
|
|
144
|
-
return ids
|
|
145
|
-
i = j + 1
|
|
146
|
-
while i < n and part[i] == " ":
|
|
147
|
-
i += 1
|
|
148
|
-
hit = [cl for cl in closers if part.startswith(cl, i)]
|
|
149
|
-
if not hit:
|
|
150
|
-
issues.append(Issue(ln, "error", "label of `%s` opens with `%s` but does not close "
|
|
151
|
-
"with `%s` after the quotes" % (nid, opener, closers[0])))
|
|
152
|
-
return ids
|
|
153
|
-
i += len(hit[0])
|
|
154
|
-
else:
|
|
155
|
-
found = [(part.find(cl, i), cl) for cl in closers if part.find(cl, i) >= 0]
|
|
156
|
-
if not found:
|
|
157
|
-
issues.append(Issue(ln, "error", "unbalanced brackets: `%s%s` is never closed with `%s`"
|
|
158
|
-
% (nid, opener, closers[0])))
|
|
159
|
-
return ids
|
|
160
|
-
pos, cl = min(found)
|
|
161
|
-
label = part[i:pos]
|
|
162
|
-
bad = sorted({ch for ch in label if ch in '()[]{}"'})
|
|
163
|
-
if bad:
|
|
164
|
-
issues.append(Issue(ln, "error", "label of `%s` has unquoted %s; wrap the label in "
|
|
165
|
-
"quotes: %s%s\"%s\"%s" % (nid, " ".join("`%s`" % b for b in bad),
|
|
166
|
-
nid, opener, label.replace('"', "'"), cl)))
|
|
167
|
-
i = pos + len(cl)
|
|
168
|
-
break
|
|
169
|
-
m2 = re.match(r":::[\w-]+", part[i:])
|
|
170
|
-
if m2:
|
|
171
|
-
i += m2.end()
|
|
172
|
-
need_sep = True
|
|
173
|
-
return ids
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def lint_flowchart(lines, issues):
|
|
177
|
-
stack = []
|
|
178
|
-
for ln, raw in lines:
|
|
179
|
-
s = raw.strip()
|
|
180
|
-
if not s or s.startswith("%%"):
|
|
181
|
-
continue
|
|
182
|
-
word = s.split()[0]
|
|
183
|
-
if word == "subgraph":
|
|
184
|
-
stack.append(ln)
|
|
185
|
-
rest = s[len("subgraph"):].strip()
|
|
186
|
-
if rest.count('"') % 2:
|
|
187
|
-
issues.append(Issue(ln, "error", "unbalanced quote in the subgraph title"))
|
|
188
|
-
elif re.match(r'^"[^"]*"$', rest):
|
|
189
|
-
pass
|
|
190
|
-
elif re.match(r"^[^\W][\w.-]*\s*[\[({>]", rest, re.U):
|
|
191
|
-
# subgraph id[label], or id [label] as the Mermaid docs write it
|
|
192
|
-
scan_nodes(re.sub(r"^([^\W][\w.-]*)\s+", r"\1", rest, flags=re.U), ln, issues)
|
|
193
|
-
elif re.search(r"[\[\](){}]", rest):
|
|
194
|
-
issues.append(Issue(ln, "error", "subgraph title has unquoted brackets; write "
|
|
195
|
-
"subgraph id[\"%s\"]" % rest.replace('"', "'")))
|
|
196
|
-
continue
|
|
197
|
-
if s == "end":
|
|
198
|
-
if stack:
|
|
199
|
-
stack.pop()
|
|
200
|
-
else:
|
|
201
|
-
issues.append(Issue(ln, "error", "`end` with no open subgraph"))
|
|
202
|
-
continue
|
|
203
|
-
if word in FLOW_KEYWORDS or word.startswith(("accTitle:", "accDescr")):
|
|
204
|
-
continue
|
|
205
|
-
for stmt in split_statements(s):
|
|
206
|
-
if stmt.count('"') % 2:
|
|
207
|
-
issues.append(Issue(ln, "error", "unbalanced double quote"))
|
|
208
|
-
continue
|
|
209
|
-
if re.search(r"(?:-{2,}|={2,})[ox][A-Za-z0-9_]", stmt):
|
|
210
|
-
issues.append(Issue(ln, "warning", "`--o`/`--x` followed by a letter is read as a circle or "
|
|
211
|
-
"cross arrowhead; add a space if the node id starts with o or x"))
|
|
212
|
-
for lab in re.findall(r"\|([^|]*)\|", stmt):
|
|
213
|
-
inner = lab.strip()
|
|
214
|
-
bad = sorted({ch for ch in inner if ch in "()[]{}"})
|
|
215
|
-
if bad and not (inner.startswith('"') and inner.endswith('"')):
|
|
216
|
-
issues.append(Issue(ln, "error", "arrow label |%s| has unquoted %s; quote it: |\"%s\"|"
|
|
217
|
-
% (inner, " ".join("`%s`" % b for b in bad), inner)))
|
|
218
|
-
body = strip_edge_labels(stmt)
|
|
219
|
-
parts = LINK_RE.split(body)
|
|
220
|
-
if len(parts) > 1 and (not parts[0].strip() or not parts[-1].strip()):
|
|
221
|
-
issues.append(Issue(ln, "error", "an arrow with no node on one side"))
|
|
222
|
-
for part in parts:
|
|
223
|
-
if part.strip():
|
|
224
|
-
scan_nodes(part, ln, issues)
|
|
225
|
-
for ln in stack:
|
|
226
|
-
issues.append(Issue(ln, "error", "subgraph opened here has no matching `end`"))
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
def lint_sequence(lines, issues):
|
|
230
|
-
stack = []
|
|
231
|
-
for ln, raw in lines:
|
|
232
|
-
s = raw.strip()
|
|
233
|
-
if not s or s.startswith("%%"):
|
|
234
|
-
continue
|
|
235
|
-
word = s.split()[0]
|
|
236
|
-
if word in SEQ_OPEN:
|
|
237
|
-
stack.append((word, ln))
|
|
238
|
-
elif word in SEQ_MIDDLE:
|
|
239
|
-
if not stack or stack[-1][0] not in SEQ_MIDDLE[word]:
|
|
240
|
-
issues.append(Issue(ln, "error", "`%s` outside a `%s` block" % (word, SEQ_MIDDLE[word][0])))
|
|
241
|
-
elif s == "end":
|
|
242
|
-
if stack:
|
|
243
|
-
stack.pop()
|
|
244
|
-
else:
|
|
245
|
-
issues.append(Issue(ln, "error", "`end` with no open loop/alt/opt/par/critical/break/rect/box"))
|
|
246
|
-
elif re.search(r"(?:-{1,2}>>?|-{1,2}[x)])", s) and ":" not in s and not s.startswith(("participant", "actor", "Note", "note", "autonumber", "activate", "deactivate", "create", "destroy", "title", "link", "links")):
|
|
247
|
-
issues.append(Issue(ln, "error", "message without `: text`; write `A->>B: what is sent`"))
|
|
248
|
-
for word, ln in stack:
|
|
249
|
-
issues.append(Issue(ln, "error", "`%s` block opened here has no matching `end`" % word))
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def lint_braces(lines, issues):
|
|
253
|
-
depth, opened = 0, []
|
|
254
|
-
for ln, raw in lines:
|
|
255
|
-
s = re.sub(r'"[^"]*"', "", raw)
|
|
256
|
-
# erDiagram cardinality markers such as ||--o{ and }|..|{ are not braces
|
|
257
|
-
s = re.sub(r"(?:\|o|\|\||\}o|\}\|)(?:--|\.\.)(?:o\||\|\||o\{|\|\{)", " ", s)
|
|
258
|
-
if s.strip().startswith("%%"):
|
|
259
|
-
continue
|
|
260
|
-
for ch in s:
|
|
261
|
-
if ch == "{":
|
|
262
|
-
depth += 1
|
|
263
|
-
opened.append(ln)
|
|
264
|
-
elif ch == "}":
|
|
265
|
-
if depth == 0:
|
|
266
|
-
issues.append(Issue(ln, "error", "`}` with no matching `{`"))
|
|
267
|
-
else:
|
|
268
|
-
depth -= 1
|
|
269
|
-
opened.pop()
|
|
270
|
-
for ln in opened:
|
|
271
|
-
issues.append(Issue(ln, "error", "`{` opened here is never closed"))
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def lint(text, first_line=1):
|
|
275
|
-
"""Lint one diagram. first_line is the file line number of the diagram's first line."""
|
|
276
|
-
issues = []
|
|
277
|
-
lines = [(first_line + k, l) for k, l in enumerate(text.split("\n"))]
|
|
278
|
-
k = 0
|
|
279
|
-
# skip front matter (--- ... ---), %%{init}%% directives and comments
|
|
280
|
-
while k < len(lines) and not lines[k][1].strip():
|
|
281
|
-
k += 1
|
|
282
|
-
if k < len(lines) and lines[k][1].strip() == "---":
|
|
283
|
-
k += 1
|
|
284
|
-
while k < len(lines) and lines[k][1].strip() != "---":
|
|
285
|
-
k += 1
|
|
286
|
-
k += 1
|
|
287
|
-
while k < len(lines) and (not lines[k][1].strip() or lines[k][1].strip().startswith("%%")):
|
|
288
|
-
k += 1
|
|
289
|
-
if k >= len(lines):
|
|
290
|
-
return [Issue(first_line, "error", "the diagram is empty")], None
|
|
291
|
-
ln, head = lines[k]
|
|
292
|
-
words = head.strip().split()
|
|
293
|
-
kind = words[0].rstrip(";:")
|
|
294
|
-
if kind not in DIAGRAM_TYPES:
|
|
295
|
-
guess = difflib.get_close_matches(kind, DIAGRAM_TYPES, n=1, cutoff=0.6)
|
|
296
|
-
hint = " Did you mean `%s`?" % guess[0] if guess else " Start with one of: flowchart, sequenceDiagram, classDiagram, stateDiagram-v2, erDiagram, gantt."
|
|
297
|
-
issues.append(Issue(ln, "error", "unknown diagram type `%s` on the first line.%s" % (kind, hint)))
|
|
298
|
-
return issues, None
|
|
299
|
-
body = lines[k + 1:]
|
|
300
|
-
if kind in ("flowchart", "graph"):
|
|
301
|
-
if len(words) > 1 and words[1].rstrip(";") not in DIRECTIONS:
|
|
302
|
-
issues.append(Issue(ln, "error", "unknown direction `%s`; use TD, TB, BT, LR or RL" % words[1]))
|
|
303
|
-
lint_flowchart(body, issues)
|
|
304
|
-
elif kind == "sequenceDiagram":
|
|
305
|
-
lint_sequence(body, issues)
|
|
306
|
-
elif kind in ("classDiagram", "classDiagram-v2", "stateDiagram", "stateDiagram-v2", "erDiagram"):
|
|
307
|
-
lint_braces(body, issues)
|
|
308
|
-
if not any(l.strip() and not l.strip().startswith("%%") for _, l in body) and kind not in ("pie",):
|
|
309
|
-
issues.append(Issue(ln, "warning", "the diagram has a type but no content"))
|
|
310
|
-
return issues, kind
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
# ---------------------------------------------------------------- input
|
|
314
|
-
|
|
315
|
-
def extract_fences(text):
|
|
316
|
-
"""Return [(first_line_number, diagram_text)] for every ```mermaid / ~~~mermaid fence."""
|
|
317
|
-
out = []
|
|
318
|
-
lines = text.split("\n")
|
|
319
|
-
i = 0
|
|
320
|
-
while i < len(lines):
|
|
321
|
-
m = re.match(r"^\s{0,3}(`{3,}|~{3,})\s*mermaid\b", lines[i])
|
|
322
|
-
if m:
|
|
323
|
-
fence = m.group(1)
|
|
324
|
-
start = i + 2 # 1-based line number of the first diagram line
|
|
325
|
-
body = []
|
|
326
|
-
i += 1
|
|
327
|
-
while i < len(lines) and not re.match(r"^\s{0,3}%s%s*\s*$" % (re.escape(fence), re.escape(fence[0])), lines[i]):
|
|
328
|
-
body.append(lines[i])
|
|
329
|
-
i += 1
|
|
330
|
-
if i >= len(lines):
|
|
331
|
-
out.append((start, "\n".join(body), "unclosed"))
|
|
332
|
-
else:
|
|
333
|
-
out.append((start, "\n".join(body), None))
|
|
334
|
-
i += 1
|
|
335
|
-
return out
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
# ---------------------------------------------------------------- render
|
|
339
|
-
|
|
340
|
-
def html_page(diagrams, title):
|
|
341
|
-
csp = ("default-src 'none'; script-src 'sha256-%s' https://cdn.jsdelivr.net; "
|
|
342
|
-
"style-src 'unsafe-inline'; img-src data: https:; font-src data: https:; "
|
|
343
|
-
"connect-src https://cdn.jsdelivr.net" % MERMAID_INIT_HASH)
|
|
344
|
-
figs = []
|
|
345
|
-
for k, (label, text) in enumerate(diagrams, 1):
|
|
346
|
-
figs.append('<figure><pre class="mermaid">%s</pre><figcaption>%s</figcaption></figure>'
|
|
347
|
-
% (html.escape(text), html.escape(label)))
|
|
348
|
-
return """<!DOCTYPE html>
|
|
349
|
-
<html lang="en">
|
|
350
|
-
<head>
|
|
351
|
-
<meta charset="utf-8">
|
|
352
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
353
|
-
<meta http-equiv="Content-Security-Policy" content="%s">
|
|
354
|
-
<title>%s</title>
|
|
355
|
-
<style>
|
|
356
|
-
body{font-family:-apple-system,'Segoe UI',Helvetica,Arial,sans-serif;margin:2rem auto;max-width:72rem;padding:0 1rem;color:#222}
|
|
357
|
-
figure{margin:0 0 2.5rem;border:1px solid #e3e3e3;border-radius:6px;padding:1rem}
|
|
358
|
-
figcaption{font-size:.85rem;color:#666;margin-top:.5rem}
|
|
359
|
-
pre.mermaid{text-align:center;background:#fff;margin:0}
|
|
360
|
-
.note{font-size:.85rem;color:#666}
|
|
361
|
-
@media print{figure{break-inside:avoid;border:0}.note{display:none}}
|
|
362
|
-
</style>
|
|
363
|
-
</head>
|
|
364
|
-
<body>
|
|
365
|
-
<p class="note">Drawn by Mermaid %s loaded from cdn.jsdelivr.net; this page needs network when opened.</p>
|
|
366
|
-
%s
|
|
367
|
-
<script type="module">%s</script>
|
|
368
|
-
</body>
|
|
369
|
-
</html>
|
|
370
|
-
""" % (csp, html.escape(title), MERMAID_VERSION, "\n".join(figs), MERMAID_INIT)
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def run_mmdc(exe, text, out_path):
|
|
374
|
-
with tempfile.TemporaryDirectory() as tmp:
|
|
375
|
-
src = Path(tmp) / "diagram.mmd"
|
|
376
|
-
src.write_text(text, encoding="utf-8")
|
|
377
|
-
cmd = [exe, "-i", str(src), "-o", str(out_path)]
|
|
378
|
-
if out_path.suffix.lower() == ".png":
|
|
379
|
-
cmd += ["-b", "white", "-s", "2"]
|
|
380
|
-
try:
|
|
381
|
-
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=180, check=False)
|
|
382
|
-
except subprocess.TimeoutExpired:
|
|
383
|
-
return "mmdc timed out after 180 s"
|
|
384
|
-
if proc.returncode != 0:
|
|
385
|
-
return mmdc_error((proc.stderr or proc.stdout).decode("utf-8", "replace"))
|
|
386
|
-
return None
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def mmdc_error(raw):
|
|
390
|
-
"""Mermaid's own message (from "Error:" up to the stack trace), not the stack trace:
|
|
391
|
-
mmdc prints a browser-library trace after the parse error, and the parse error is the
|
|
392
|
-
part a person can act on."""
|
|
393
|
-
lines = raw.strip().splitlines()
|
|
394
|
-
start = next((k for k, l in enumerate(lines) if l.lstrip().startswith("Error")), 0)
|
|
395
|
-
keep = []
|
|
396
|
-
for l in lines[start:]:
|
|
397
|
-
if re.match(r"^\s+at\s", l) or re.search(r"\((https?|file)://[^)]*\)\s*$", l):
|
|
398
|
-
break
|
|
399
|
-
keep.append(l.rstrip())
|
|
400
|
-
msg = "\n".join(keep[:8]).strip()
|
|
401
|
-
return msg or raw.strip()[-800:]
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
def main(argv=None):
|
|
405
|
-
ap = argparse.ArgumentParser(description="Lint Mermaid diagrams, then render to svg/png/pdf (mmdc) or html.")
|
|
406
|
-
ap.add_argument("input", help=".mmd/.mermaid file with one diagram, or .md with ```mermaid fences")
|
|
407
|
-
ap.add_argument("-o", "--output", help="out.svg | out.png | out.pdf | out.html. Leave out to lint only.")
|
|
408
|
-
ap.add_argument("--force", action="store_true", help="render even when lint finds errors")
|
|
409
|
-
a = ap.parse_args(argv)
|
|
410
|
-
|
|
411
|
-
src = Path(a.input)
|
|
412
|
-
if not src.is_file():
|
|
413
|
-
sys.exit("error: input not found: %s" % a.input)
|
|
414
|
-
try:
|
|
415
|
-
text = src.read_text(encoding="utf-8-sig").replace("\r\n", "\n")
|
|
416
|
-
except UnicodeDecodeError:
|
|
417
|
-
sys.exit("error: %s is not UTF-8 text" % a.input)
|
|
418
|
-
|
|
419
|
-
if src.suffix.lower() in (".md", ".markdown"):
|
|
420
|
-
fences = extract_fences(text)
|
|
421
|
-
if not fences:
|
|
422
|
-
sys.exit("error: no ```mermaid fences found in %s" % a.input)
|
|
423
|
-
diagrams = fences
|
|
424
|
-
else:
|
|
425
|
-
diagrams = [(1, text, None)]
|
|
426
|
-
|
|
427
|
-
errors = warnings = 0
|
|
428
|
-
for k, (start, body, flag) in enumerate(diagrams, 1):
|
|
429
|
-
issues, kind = lint(body, start)
|
|
430
|
-
if flag == "unclosed":
|
|
431
|
-
issues.insert(0, Issue(start - 1, "error", "the ```mermaid fence is never closed"))
|
|
432
|
-
label = "diagram %d (%s)" % (k, kind or "unknown type") if len(diagrams) > 1 else (kind or "diagram")
|
|
433
|
-
if not issues:
|
|
434
|
-
print("%s: %s: ok" % (src.name, label))
|
|
435
|
-
for it in sorted(issues, key=lambda x: x.line):
|
|
436
|
-
print("%s:%d: %s: %s" % (src.name, it.line, it.level, it.msg))
|
|
437
|
-
if it.level == "error":
|
|
438
|
-
errors += 1
|
|
439
|
-
else:
|
|
440
|
-
warnings += 1
|
|
441
|
-
print("lint: %d diagram(s), %d error(s), %d warning(s)" % (len(diagrams), errors, warnings))
|
|
442
|
-
|
|
443
|
-
if not a.output:
|
|
444
|
-
return 1 if errors else 0
|
|
445
|
-
if errors and not a.force:
|
|
446
|
-
sys.stdout.flush()
|
|
447
|
-
print("not rendering: fix the errors above, or rerun with --force", file=sys.stderr)
|
|
448
|
-
return 1
|
|
449
|
-
|
|
450
|
-
out = Path(a.output)
|
|
451
|
-
if not out.parent.exists():
|
|
452
|
-
sys.exit("error: output folder does not exist: %s" % out.parent)
|
|
453
|
-
ext = out.suffix.lower()
|
|
454
|
-
if ext not in (".svg", ".png", ".pdf", ".html", ".htm"):
|
|
455
|
-
sys.exit("error: output must end in .svg, .png, .pdf or .html")
|
|
456
|
-
many = len(diagrams) > 1
|
|
457
|
-
labels = ["%s, diagram %d (line %d)" % (src.name, k, d[0]) if many else src.name
|
|
458
|
-
for k, d in enumerate(diagrams, 1)]
|
|
459
|
-
|
|
460
|
-
exe = shutil.which("mmdc") if ext in (".svg", ".png", ".pdf") else None
|
|
461
|
-
if exe:
|
|
462
|
-
failed = 0
|
|
463
|
-
for k, (start, body, _) in enumerate(diagrams, 1):
|
|
464
|
-
target = out.with_name("%s-%d%s" % (out.stem, k, out.suffix)) if many else out
|
|
465
|
-
err = run_mmdc(exe, body, target)
|
|
466
|
-
if err:
|
|
467
|
-
failed += 1
|
|
468
|
-
print("mmdc failed on %s:\n%s" % (labels[k - 1], err), file=sys.stderr)
|
|
469
|
-
else:
|
|
470
|
-
print("wrote %s" % target)
|
|
471
|
-
return 1 if failed else 0
|
|
472
|
-
|
|
473
|
-
target = out if ext in (".html", ".htm") else out.with_suffix(".html")
|
|
474
|
-
target.write_text(html_page([(labels[k], d[1]) for k, d in enumerate(diagrams)], src.name), encoding="utf-8")
|
|
475
|
-
if ext not in (".html", ".htm"):
|
|
476
|
-
print("mmdc (mermaid-cli) is not on PATH, so no %s was written. Wrote %s instead: it draws "
|
|
477
|
-
"the diagram(s) with Mermaid %s from cdn.jsdelivr.net and needs network when opened. "
|
|
478
|
-
"For %s output: npm install -g @mermaid-js/mermaid-cli, then rerun."
|
|
479
|
-
% (ext, target, MERMAID_VERSION, ext))
|
|
480
|
-
else:
|
|
481
|
-
print("wrote %s (%d diagram(s); draws with Mermaid %s from cdn.jsdelivr.net, needs network when opened)"
|
|
482
|
-
% (target, len(diagrams), MERMAID_VERSION))
|
|
483
|
-
return 0
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
if __name__ == "__main__":
|
|
487
|
-
sys.exit(main())
|