mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-13 11:45:56 +00:00
Add Draw.io Diagram Generator skill and instructions (#1179)
* Add draw-io diagram generator skill for awesome github copilot * Add comprehensive shape libraries and style reference documentation for draw.io - Introduced a new markdown file for draw.io shape libraries detailing various built-in shapes, their style keys, and usage. - Added a complete style reference for `<mxCell>` elements, including universal style keys, shape-specific keys, edge styles, and color palettes. - Included examples for common styles and shapes to aid users in creating diagrams effectively. * Add draw-io diagram validation and shape addition scripts * Add new diagram templates for flowchart, sequence, and UML class diagrams - Created a flowchart template with a structured layout including start, steps, decision points, and end. - Added a sequence diagram template illustrating interactions between a client, API server, and database with activation boxes and message arrows. - Introduced a UML class diagram template featuring an interface, classes, attributes, methods, and relationships, including composition and realization. * Add draw-io diagram generator skill to README with detailed usage instructions and bundled assets * Add draw.io instructions with workflow, XML structure rules, style conventions, and validation checklist * Add draw.io diagram standards to README instructions for enhanced diagram creation and editing * Moving diagram templates to assets/ to follow agentskills structure - Moved flowchart template with start, steps, decision points, and end nodes. - Moved sequence diagram template illustrating interactions between a client, API server, and database. - Moved UML class diagram template featuring an interface, classes, attributes, methods, and relationships. * Clarify installation instructions for draw.io VS Code extension in SKILL.md
This commit is contained in:
5
skills/draw-io-diagram-generator/scripts/.gitignore
vendored
Normal file
5
skills/draw-io-diagram-generator/scripts/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
124
skills/draw-io-diagram-generator/scripts/README.md
Normal file
124
skills/draw-io-diagram-generator/scripts/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# draw-io Scripts
|
||||
|
||||
Utility scripts for working with `.drawio` diagram files in the cxp-bu-order-ms project.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- No external dependencies (uses standard library only: `xml.etree.ElementTree`, `argparse`, `json`, `sys`, `pathlib`)
|
||||
|
||||
## Scripts
|
||||
|
||||
### `validate-drawio.py`
|
||||
|
||||
Validates the XML structure of a `.drawio` file against required constraints.
|
||||
|
||||
**Usage**
|
||||
|
||||
```bash
|
||||
python scripts/validate-drawio.py <path-to-diagram.drawio>
|
||||
```
|
||||
|
||||
**Examples**
|
||||
|
||||
```bash
|
||||
# Validate a single file
|
||||
python scripts/validate-drawio.py docs/architecture.drawio
|
||||
|
||||
# Validate all drawio files in a directory
|
||||
for f in docs/**/*.drawio; do python scripts/validate-drawio.py "$f"; done
|
||||
```
|
||||
|
||||
**Checks performed**
|
||||
|
||||
| Check | Description |
|
||||
|-------|-------------|
|
||||
| Root cells | Verifies id="0" and id="1" cells are present in every diagram page |
|
||||
| Unique IDs | All `mxCell` id values are unique within a diagram |
|
||||
| Edge connectivity | Every edge has valid `source` and `target` attributes pointing to existing cells |
|
||||
| Geometry | Every vertex cell has an `mxGeometry` child element |
|
||||
| Parent chain | Every cell's `parent` attribute references an existing cell id |
|
||||
| XML well-formedness | File is valid XML |
|
||||
|
||||
**Exit codes**
|
||||
|
||||
- `0` — Validation passed
|
||||
- `1` — One or more validation errors found (errors printed to stdout)
|
||||
|
||||
---
|
||||
|
||||
### `add-shape.py`
|
||||
|
||||
Adds a new shape (vertex cell) to an existing `.drawio` diagram file.
|
||||
|
||||
**Usage**
|
||||
|
||||
```bash
|
||||
python scripts/add-shape.py <diagram.drawio> <label> <x> <y> [options]
|
||||
```
|
||||
|
||||
**Arguments**
|
||||
|
||||
| Argument | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `diagram` | Yes | Path to the `.drawio` file |
|
||||
| `label` | Yes | Text label for the new shape |
|
||||
| `x` | Yes | X coordinate (pixels from top-left) |
|
||||
| `y` | Yes | Y coordinate (pixels from top-left) |
|
||||
|
||||
**Options**
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `--width` | `120` | Shape width in pixels |
|
||||
| `--height` | `60` | Shape height in pixels |
|
||||
| `--style` | `"rounded=1;whiteSpace=wrap;html=1;"` | draw.io style string |
|
||||
| `--diagram-index` | `0` | Index of the diagram page (0-based) |
|
||||
| `--dry-run` | false | Print the new cell XML without modifying the file |
|
||||
|
||||
**Examples**
|
||||
|
||||
```bash
|
||||
# Add a basic rounded box
|
||||
python scripts/add-shape.py docs/flowchart.drawio "New Step" 400 300
|
||||
|
||||
# Add a custom styled shape
|
||||
python scripts/add-shape.py docs/flowchart.drawio "Decision" 400 400 \
|
||||
--width 160 --height 80 \
|
||||
--style "rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;"
|
||||
|
||||
# Preview without writing
|
||||
python scripts/add-shape.py docs/architecture.drawio "Service X" 600 200 --dry-run
|
||||
```
|
||||
|
||||
**Output**
|
||||
|
||||
Prints the new cell id on success:
|
||||
```
|
||||
Added shape id="auto_abc123" to page 0 of docs/flowchart.drawio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Validate before committing
|
||||
|
||||
```bash
|
||||
# Validate all diagrams
|
||||
find . -name "*.drawio" -not -path "*/node_modules/*" | \
|
||||
xargs -I{} python scripts/validate-drawio.py {}
|
||||
```
|
||||
|
||||
### Quickly add a placeholder node
|
||||
|
||||
```bash
|
||||
python scripts/add-shape.py docs/architecture.drawio "TODO: Service" 800 400 \
|
||||
--style "rounded=1;whiteSpace=wrap;html=1;fillColor=#f8cecc;strokeColor=#b85450;"
|
||||
```
|
||||
|
||||
### Check a template is valid
|
||||
|
||||
```bash
|
||||
python scripts/validate-drawio.py .github/skills/draw-io-diagram-generator/templates/flowchart.drawio
|
||||
```
|
||||
213
skills/draw-io-diagram-generator/scripts/add-shape.py
Normal file
213
skills/draw-io-diagram-generator/scripts/add-shape.py
Normal file
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
add-shape.py — Add a new vertex shape to an existing .drawio diagram file.
|
||||
|
||||
Usage:
|
||||
python scripts/add-shape.py <diagram.drawio> <label> <x> <y> [options]
|
||||
|
||||
Examples:
|
||||
python scripts/add-shape.py docs/flowchart.drawio "New Step" 400 300
|
||||
python scripts/add-shape.py docs/arch.drawio "Decision" 400 400 \\
|
||||
--width 160 --height 80 \\
|
||||
--style "rhombus;whiteSpace=wrap;html=1;fillColor=#fff2cc;strokeColor=#d6b656;"
|
||||
python scripts/add-shape.py docs/arch.drawio "Preview Node" 200 200 --dry-run
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_STYLE = "rounded=1;whiteSpace=wrap;html=1;"
|
||||
|
||||
|
||||
def _indent_xml(elem: ET.Element, level: int = 0) -> None:
|
||||
"""Indent XML tree in-place. Replaces ET.indent() for Python 3.8 compatibility."""
|
||||
indent = "\n" + " " * level
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = indent + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = indent
|
||||
for child in elem:
|
||||
_indent_xml(child, level + 1)
|
||||
# last child tail
|
||||
if not child.tail or not child.tail.strip():
|
||||
child.tail = indent
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = indent
|
||||
if not level:
|
||||
elem.tail = "\n"
|
||||
|
||||
|
||||
def _generate_id(label: str, x: int, y: int) -> str:
|
||||
"""Generate a short deterministic-ish id based on label + position + time."""
|
||||
seed = f"{label}:{x}:{y}:{time.time_ns()}"
|
||||
return "auto_" + hashlib.sha1(seed.encode()).hexdigest()[:8]
|
||||
|
||||
|
||||
def add_shape(
|
||||
path: Path,
|
||||
label: str,
|
||||
x: int,
|
||||
y: int,
|
||||
width: int = 120,
|
||||
height: int = 60,
|
||||
style: str = DEFAULT_STYLE,
|
||||
diagram_index: int = 0,
|
||||
dry_run: bool = False,
|
||||
) -> int:
|
||||
"""
|
||||
Parse the .drawio file, insert a new vertex cell into the specified diagram page,
|
||||
and write the file back (unless dry_run is True).
|
||||
|
||||
Returns:
|
||||
0 on success, 1 on failure.
|
||||
"""
|
||||
# Preserve the original XML declaration / indentation by writing raw bytes.
|
||||
ET.register_namespace("", "")
|
||||
|
||||
try:
|
||||
tree = ET.parse(path)
|
||||
except ET.ParseError as exc:
|
||||
print(f"ERROR: XML parse error in '{path}': {exc}")
|
||||
return 1
|
||||
|
||||
mxfile = tree.getroot()
|
||||
if mxfile.tag != "mxfile":
|
||||
print(f"ERROR: Root element must be <mxfile>, got <{mxfile.tag}>")
|
||||
return 1
|
||||
|
||||
diagrams = mxfile.findall("diagram")
|
||||
if diagram_index >= len(diagrams):
|
||||
print(
|
||||
f"ERROR: diagram-index {diagram_index} is out of range "
|
||||
f"(file has {len(diagrams)} diagram(s))"
|
||||
)
|
||||
return 1
|
||||
|
||||
diagram = diagrams[diagram_index]
|
||||
graph_model = diagram.find("mxGraphModel")
|
||||
if graph_model is None:
|
||||
print(
|
||||
"ERROR: <mxGraphModel> not found as direct child. "
|
||||
"Compressed diagrams are not supported."
|
||||
)
|
||||
return 1
|
||||
|
||||
root_elem = graph_model.find("root")
|
||||
if root_elem is None:
|
||||
print("ERROR: <root> element not found inside <mxGraphModel>")
|
||||
return 1
|
||||
|
||||
# Determine parent id — default to "1" (the default layer)
|
||||
parent_id = "1"
|
||||
existing_ids = {c.get("id") for c in root_elem.findall("mxCell") if c.get("id")}
|
||||
if parent_id not in existing_ids:
|
||||
# Fallback to the first cell id that isn't "0"
|
||||
for c in root_elem.findall("mxCell"):
|
||||
cid = c.get("id")
|
||||
if cid and cid != "0":
|
||||
parent_id = cid
|
||||
break
|
||||
|
||||
# Generate a unique id
|
||||
new_id = _generate_id(label, x, y)
|
||||
while new_id in existing_ids:
|
||||
new_id = _generate_id(label + "_", x, y)
|
||||
|
||||
# Build the new mxCell element
|
||||
new_cell = ET.Element("mxCell")
|
||||
new_cell.set("id", new_id)
|
||||
new_cell.set("value", label)
|
||||
new_cell.set("style", style)
|
||||
new_cell.set("vertex", "1")
|
||||
new_cell.set("parent", parent_id)
|
||||
|
||||
geom = ET.SubElement(new_cell, "mxGeometry")
|
||||
geom.set("x", str(x))
|
||||
geom.set("y", str(y))
|
||||
geom.set("width", str(width))
|
||||
geom.set("height", str(height))
|
||||
geom.set("as", "geometry")
|
||||
|
||||
if dry_run:
|
||||
print("DRY RUN — new cell XML (not written):")
|
||||
print(ET.tostring(new_cell, encoding="unicode"))
|
||||
print(f"\nWould add to diagram '{diagram.get('name', diagram_index)}' in '{path}'")
|
||||
return 0
|
||||
|
||||
root_elem.append(new_cell)
|
||||
|
||||
# Write back preserving XML declaration (uses _indent_xml for Python 3.8 compat)
|
||||
_indent_xml(tree.getroot())
|
||||
tree.write(str(path), encoding="utf-8", xml_declaration=True)
|
||||
|
||||
print(
|
||||
f"Added shape id=\"{new_id}\" to page {diagram_index} "
|
||||
f"('{diagram.get('name', '')}') of {path}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Add a shape to an existing .drawio diagram file.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument("diagram", help="Path to the .drawio file")
|
||||
parser.add_argument("label", help="Text label for the new shape")
|
||||
parser.add_argument("x", type=int, help="X coordinate (pixels)")
|
||||
parser.add_argument("y", type=int, help="Y coordinate (pixels)")
|
||||
parser.add_argument("--width", type=int, default=120, help="Shape width (default: 120)")
|
||||
parser.add_argument("--height", type=int, default=60, help="Shape height (default: 60)")
|
||||
parser.add_argument(
|
||||
"--style",
|
||||
default=DEFAULT_STYLE,
|
||||
help=f'draw.io style string (default: "{DEFAULT_STYLE}")',
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diagram-index",
|
||||
type=int,
|
||||
default=0,
|
||||
help="0-based index of the diagram page to add to (default: 0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print the new cell XML without writing to file",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = _parse_args(argv)
|
||||
path = Path(args.diagram)
|
||||
|
||||
if not path.exists():
|
||||
print(f"ERROR: File not found: {path}")
|
||||
return 1
|
||||
if not path.is_file():
|
||||
print(f"ERROR: Not a file: {path}")
|
||||
return 1
|
||||
|
||||
return add_shape(
|
||||
path=path,
|
||||
label=args.label,
|
||||
x=args.x,
|
||||
y=args.y,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
style=args.style,
|
||||
diagram_index=args.diagram_index,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
214
skills/draw-io-diagram-generator/scripts/validate-drawio.py
Normal file
214
skills/draw-io-diagram-generator/scripts/validate-drawio.py
Normal file
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
validate-drawio.py — Validate the XML structure of a .drawio diagram file.
|
||||
|
||||
Usage:
|
||||
python scripts/validate-drawio.py <path-to-file.drawio>
|
||||
|
||||
Exit codes:
|
||||
0 All checks passed
|
||||
1 One or more validation errors found
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _error(msg: str, errors: list) -> None:
|
||||
errors.append(msg)
|
||||
print(f" ERROR: {msg}")
|
||||
|
||||
|
||||
def validate_file(path: Path) -> list[str]:
|
||||
"""Parse and validate a single .drawio file. Returns list of error strings."""
|
||||
errors: list[str] = []
|
||||
|
||||
# --- XML well-formedness ---
|
||||
try:
|
||||
tree = ET.parse(path)
|
||||
except ET.ParseError as exc:
|
||||
return [f"XML parse error: {exc}"]
|
||||
|
||||
root = tree.getroot()
|
||||
if root.tag != "mxfile":
|
||||
_error(f"Root element must be <mxfile>, got <{root.tag}>", errors)
|
||||
return errors
|
||||
|
||||
diagrams = root.findall("diagram")
|
||||
if not diagrams:
|
||||
_error("No <diagram> elements found inside <mxfile>", errors)
|
||||
return errors
|
||||
|
||||
for d_idx, diagram in enumerate(diagrams):
|
||||
d_name = diagram.get("name", f"page-{d_idx}")
|
||||
prefix = f"[diagram '{d_name}']"
|
||||
|
||||
# Find mxGraphModel (may be direct child or base64-encoded; we handle direct only)
|
||||
graph_model = diagram.find("mxGraphModel")
|
||||
if graph_model is None:
|
||||
print(f" SKIP {prefix}: mxGraphModel not found as direct child (may be compressed)")
|
||||
continue
|
||||
|
||||
root_elem = graph_model.find("root")
|
||||
if root_elem is None:
|
||||
_error(f"{prefix} Missing <root> element inside <mxGraphModel>", errors)
|
||||
continue
|
||||
|
||||
cells = root_elem.findall("mxCell")
|
||||
cell_ids: dict[str, ET.Element] = {}
|
||||
has_id0 = False
|
||||
has_id1 = False
|
||||
|
||||
# --- Collect all IDs, check for root cells ---
|
||||
for cell in cells:
|
||||
cid = cell.get("id")
|
||||
if cid is None:
|
||||
_error(f"{prefix} Found <mxCell> without an 'id' attribute", errors)
|
||||
continue
|
||||
if cid in cell_ids:
|
||||
_error(f"{prefix} Duplicate cell id='{cid}'", errors)
|
||||
cell_ids[cid] = cell
|
||||
if cid == "0":
|
||||
has_id0 = True
|
||||
if cid == "1":
|
||||
has_id1 = True
|
||||
|
||||
if not has_id0:
|
||||
_error(f"{prefix} Missing required root cell id='0'", errors)
|
||||
if not has_id1:
|
||||
_error(f"{prefix} Missing required default-layer cell id='1'", errors)
|
||||
|
||||
# L2: id="0" must be the first cell, id="1" must be the second cell
|
||||
if len(cells) >= 1 and cells[0].get("id") != "0":
|
||||
_error(
|
||||
f"{prefix} First <mxCell> must have id='0', "
|
||||
f"got id='{cells[0].get('id')}'",
|
||||
errors,
|
||||
)
|
||||
if len(cells) >= 2 and cells[1].get("id") != "1":
|
||||
_error(
|
||||
f"{prefix} Second <mxCell> must have id='1', "
|
||||
f"got id='{cells[1].get('id')}'",
|
||||
errors,
|
||||
)
|
||||
# L3: id="1" must have parent="0"
|
||||
for cell in cells:
|
||||
if cell.get("id") == "1" and cell.get("parent") != "0":
|
||||
_error(
|
||||
f"{prefix} Cell id='1' must have parent='0', "
|
||||
f"got parent='{cell.get('parent')}'",
|
||||
errors,
|
||||
)
|
||||
# H2: Every diagram page must contain a title cell
|
||||
# (a vertex with style containing 'text;' and 'fontSize=18')
|
||||
def _is_title_style(style: str) -> bool:
|
||||
"""Return True if the style string identifies a draw.io title text cell."""
|
||||
return (
|
||||
(style.startswith("text;") or ";text;" in style)
|
||||
and "fontSize=18" in style
|
||||
)
|
||||
|
||||
has_title_cell = any(
|
||||
c.get("vertex") == "1" and _is_title_style(c.get("style") or "")
|
||||
for c in cells
|
||||
)
|
||||
if not has_title_cell:
|
||||
_error(
|
||||
f"{prefix} No title cell found — add a vertex with style "
|
||||
"containing 'text;' and 'fontSize=18' at the top of the page",
|
||||
errors,
|
||||
)
|
||||
|
||||
# --- Check each cell for structural validity ---
|
||||
for cell in cells:
|
||||
cid = cell.get("id", "<unknown>")
|
||||
is_vertex = cell.get("vertex") == "1"
|
||||
is_edge = cell.get("edge") == "1"
|
||||
|
||||
# Parent must exist (skip the root cell id=0 which has no parent)
|
||||
parent = cell.get("parent")
|
||||
if cid != "0":
|
||||
if parent is None:
|
||||
_error(f"{prefix} Cell id='{cid}' is missing a 'parent' attribute", errors)
|
||||
elif parent not in cell_ids:
|
||||
_error(
|
||||
f"{prefix} Cell id='{cid}' references unknown parent='{parent}'",
|
||||
errors,
|
||||
)
|
||||
|
||||
# Vertex cells must have mxGeometry
|
||||
if is_vertex:
|
||||
geom = cell.find("mxGeometry")
|
||||
if geom is None:
|
||||
_error(
|
||||
f"{prefix} Vertex cell id='{cid}' is missing <mxGeometry>",
|
||||
errors,
|
||||
)
|
||||
|
||||
# Edge cells must have source and target, both must exist.
|
||||
# Exception: floating edges (e.g. sequence diagram lifelines) use
|
||||
# sourcePoint/targetPoint in mxGeometry instead of source/target attributes.
|
||||
if is_edge:
|
||||
source = cell.get("source")
|
||||
target = cell.get("target")
|
||||
geom = cell.find("mxGeometry")
|
||||
has_source_point = geom is not None and any(
|
||||
p.get("as") == "sourcePoint" for p in geom.findall("mxPoint")
|
||||
)
|
||||
has_target_point = geom is not None and any(
|
||||
p.get("as") == "targetPoint" for p in geom.findall("mxPoint")
|
||||
)
|
||||
if source is None and not has_source_point:
|
||||
_error(
|
||||
f"{prefix} Edge cell id='{cid}' is missing 'source' attribute "
|
||||
f"(and no sourcePoint in mxGeometry)",
|
||||
errors,
|
||||
)
|
||||
elif source is not None and source not in cell_ids:
|
||||
_error(
|
||||
f"{prefix} Edge id='{cid}' references unknown source='{source}'",
|
||||
errors,
|
||||
)
|
||||
if target is None and not has_target_point:
|
||||
_error(
|
||||
f"{prefix} Edge cell id='{cid}' is missing 'target' attribute "
|
||||
f"(and no targetPoint in mxGeometry)",
|
||||
errors,
|
||||
)
|
||||
elif target is not None and target not in cell_ids:
|
||||
_error(
|
||||
f"{prefix} Edge id='{cid}' references unknown target='{target}'",
|
||||
errors,
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python validate-drawio.py <diagram.drawio>")
|
||||
return 1
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
if not path.exists():
|
||||
print(f"File not found: {path}")
|
||||
return 1
|
||||
if not path.is_file():
|
||||
print(f"Not a file: {path}")
|
||||
return 1
|
||||
|
||||
print(f"Validating: {path}")
|
||||
errors = validate_file(path)
|
||||
|
||||
if errors:
|
||||
print(f"\nFAIL — {len(errors)} error(s) found.")
|
||||
return 1
|
||||
|
||||
print("PASS — No errors found.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user