Source code for scitex_todo._mermaid

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Mermaid adapter — render a validated task list as ``flowchart TB`` source.

Edges:
    depends_on  ->  normal arrow      dep --> task
    blocks      ->  inhibition arrow  blocker -- blocks --x target

Status -> fill color:
    goal        gold     (#ffe082, gold border)
    done        green    (#c8e6c9)
    in_progress yellow   (#fff9c4)
    blocked     orange   (#fff3e0)
    pending     grey     (#eceff1)
    deferred    grey     (#f5f5f5, dashed border)
    failed      red      (#ffcdd2)
"""

from __future__ import annotations

import sys

# fill, stroke, stroke-dasharray (empty = solid border)
STATUS_STYLE: dict[str, tuple[str, str, str]] = {
    "goal": ("#ffe082", "#ff6f00", ""),
    "done": ("#c8e6c9", "#2e7d32", ""),
    "in_progress": ("#fff9c4", "#f9a825", ""),
    "blocked": ("#fff3e0", "#ef6c00", ""),
    "pending": ("#eceff1", "#90a4ae", ""),
    "deferred": ("#f5f5f5", "#bdbdbd", "5 3"),
    "failed": ("#ffcdd2", "#c62828", ""),
}


def _sanitize_label(text: str) -> str:
    """Make a string safe inside a mermaid ``["..."]`` node label."""
    return str(text).replace('"', "'").replace("\n", " ").strip()


[docs] def build_mermaid(tasks: list[dict]) -> str: """Render the task list as a mermaid ``flowchart TB`` source string. Parameters ---------- tasks : list of dict Validated tasks, typically from :func:`scitex_todo.load_tasks`. Returns ------- str Mermaid ``flowchart TB`` source, newline-terminated. Includes node labels, ``depends_on`` and ``blocks`` edges, and per-status ``classDef`` styling. Notes ----- Edges referencing an unknown task id are skipped with a warning on stderr rather than raising — a partial graph is more useful than none. Examples -------- >>> src = build_mermaid([{"id": "a", "title": "A", "status": "done"}]) >>> src.startswith("flowchart TB") True """ ids = {task["id"] for task in tasks} lines: list[str] = ["flowchart TB"] # Nodes for task in tasks: tid = task["id"] label = _sanitize_label(task["title"]) note = task.get("note") if note: label = f"{label}<br/>({_sanitize_label(note)})" lines.append(f' {tid}["{label}"]') lines.append("") # depends_on edges: dependency --> task for task in tasks: tid = task["id"] for dep in task.get("depends_on", []) or []: if dep not in ids: sys.stderr.write( f"WARN: task {tid!r} depends_on unknown id {dep!r}; skipping edge\n" ) continue lines.append(f" {dep} --> {tid}") # blocks edges: blocker -- blocks --x target (inhibition / cross arrowhead) for task in tasks: tid = task["id"] for target in task.get("blocks", []) or []: if target not in ids: sys.stderr.write( f"WARN: task {tid!r} blocks unknown id {target!r}; skipping edge\n" ) continue lines.append(f" {tid} -- blocks --x {target}") lines.append("") # Per-status class definitions for status, (fill, stroke, dash) in STATUS_STYLE.items(): style = f"fill:{fill},stroke:{stroke},stroke-width:1px,color:#222" if dash: style += f",stroke-dasharray:{dash}" lines.append(f" classDef {status} {style}") # Class assignments grouped by status by_status: dict[str, list[str]] = {} for task in tasks: by_status.setdefault(task["status"], []).append(task["id"]) for status, members in by_status.items(): lines.append(f" class {','.join(members)} {status}") return "\n".join(lines) + "\n"
# EOF