termrender 4.2.0__tar.gz → 4.3.1__tar.gz
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.
- {termrender-4.2.0 → termrender-4.3.1}/CHANGELOG.md +83 -0
- {termrender-4.2.0 → termrender-4.3.1}/PKG-INFO +1 -1
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/CLAUDE.md +21 -3
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/mermaid.py +29 -9
- termrender-4.3.1/src/termrender/renderers/mermaid_flow_model.py +127 -0
- termrender-4.3.1/src/termrender/renderers/mermaid_gantt.py +459 -0
- termrender-4.3.1/src/termrender/renderers/mermaid_journey.py +121 -0
- termrender-4.3.1/src/termrender/renderers/mermaid_mindmap.py +99 -0
- termrender-4.3.1/src/termrender/renderers/mermaid_pie.py +128 -0
- termrender-4.3.1/src/termrender/renderers/mermaid_prelude.py +55 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/mermaid_sequence.py +8 -5
- termrender-4.3.1/src/termrender/renderers/mermaid_timeline.py +118 -0
- termrender-4.3.1/tests/test_mermaid_dispatch.py +195 -0
- termrender-4.3.1/tests/test_mermaid_flow_model.py +113 -0
- termrender-4.3.1/tests/test_mermaid_gantt.py +333 -0
- termrender-4.3.1/tests/test_mermaid_journey.py +128 -0
- termrender-4.3.1/tests/test_mermaid_mindmap.py +115 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_mermaid_pie.py +63 -2
- termrender-4.3.1/tests/test_mermaid_timeline.py +107 -0
- termrender-4.2.0/src/termrender/renderers/mermaid_gantt.py +0 -222
- termrender-4.2.0/src/termrender/renderers/mermaid_pie.py +0 -84
- termrender-4.2.0/tests/test_mermaid_dispatch.py +0 -89
- termrender-4.2.0/tests/test_mermaid_gantt.py +0 -154
- {termrender-4.2.0 → termrender-4.3.1}/.github/workflows/publish.yml +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/.gitignore +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/CLAUDE.md +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/LICENSE +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/README.md +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/design.json +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/pyproject.toml +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/requirements.json +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/scripts/build-mermaid-ascii.sh +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/CLAUDE.md +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/__init__.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/__main__.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/_bin/mermaid-ascii-darwin-arm64 +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/_mermaid_bin.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/blocks.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/emit.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/layout.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/parser.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/py.typed +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/__init__.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/borders.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/charts.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/code.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/columns.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/diff.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/divider.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/panel.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/quote.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/stat.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/table.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/text.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/timeline.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/tree.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/src/termrender/style.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/__init__.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_charts.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_cli_contract.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_column_alignment.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_diff.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_inline_badge.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_linebreak.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_mermaid_compat.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_mermaid_sequence.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_myst_gaps.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_stat.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_tasklist.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_timeline.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/tests/test_variable_colons.py +0 -0
- {termrender-4.2.0 → termrender-4.3.1}/uv.lock +0 -0
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v4.3.1 (2026-07-06)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **mermaid-gantt**: Close remaining overflow and includes gaps from review
|
|
9
|
+
([`73a1354`](https://github.com/crouton-labs/termrender/commit/73a135465644073d58e293ed69e6ca968d4f5a05))
|
|
10
|
+
|
|
11
|
+
- _skip_excluded() and the implicit +1-day default end could still raise OverflowError past
|
|
12
|
+
datetime.max; both call sites now catch it and degrade the whole diagram to source instead of
|
|
13
|
+
crashing. - includes weekends was accepted but silently no-op'd (excludes weekends + includes
|
|
14
|
+
weekends still rendered the excluded schedule); includes now only supports explicit YYYY-MM-DD
|
|
15
|
+
dates and rejects weekends as _Unsupported rather than pretending to cancel the exclusion.
|
|
16
|
+
|
|
17
|
+
fix(mermaid-pie): reject invalid trailing header grammar
|
|
18
|
+
|
|
19
|
+
pie bogus (anything after pie/pie showData that isn't a valid inline title) was silently accepted
|
|
20
|
+
and rendered a plausible chart; it now degrades the whole diagram to source, matching the
|
|
21
|
+
strictness already applied to other invalid pie lines.
|
|
22
|
+
|
|
23
|
+
- **mermaid-gantt**: Implement excludes/includes, milestone, until; never-crash on bad
|
|
24
|
+
dateFormat/overflow
|
|
25
|
+
([`e182ae1`](https://github.com/crouton-labs/termrender/commit/e182ae1346af7fa4a531ad0014878e06caf5304e))
|
|
26
|
+
|
|
27
|
+
Several valid Mermaid gantt constructs rendered false schedules, and malformed input could crash:
|
|
28
|
+
|
|
29
|
+
- A malformed dateFormat (e.g. YYYY-YYYY, whose translated strptime pattern has a duplicate capture
|
|
30
|
+
group) raised re.error instead of being caught; the format is now validated by round-tripping a
|
|
31
|
+
sample date and an invalid format degrades the whole diagram to source. - Huge numeric durations
|
|
32
|
+
(e.g. 1000000000d) overflowed timedelta/datetime arithmetic; overflow is now caught and degrades
|
|
33
|
+
the whole diagram. - excludes weekends / excludes <YYYY-MM-DD> and includes <YYYY-MM-DD> are now
|
|
34
|
+
applied when resolving durations and auto-anchored start dates, instead of being ignored as
|
|
35
|
+
decorative metadata. An excludes/includes form we don't implement (e.g. day names) degrades the
|
|
36
|
+
whole diagram rather than rendering a false schedule. - milestone tasks are now tracked as
|
|
37
|
+
point-in-time markers (rendered as a single diamond marker) instead of ordinary duration spans. -
|
|
38
|
+
until <taskId> is now resolved to the referenced task's start date; an unknown reference degrades
|
|
39
|
+
the whole diagram. - %% comments (whole-line and trailing inline) are stripped before
|
|
40
|
+
tokenization, so a comment containing a colon can no longer be misparsed as a task.
|
|
41
|
+
|
|
42
|
+
- **mermaid-pie**: Degrade to source on invalid grammar, negative or overflowing values
|
|
43
|
+
([`765f613`](https://github.com/crouton-labs/termrender/commit/765f613d9e1b72ff79e8f0c32ac8d9f9cdceef69))
|
|
44
|
+
|
|
45
|
+
Native pie parsing previously treated any non-matching line as a silent ignorable and accepted
|
|
46
|
+
negative slice values, so malformed input rendered a plausible-but-wrong partial chart instead of
|
|
47
|
+
falling back to source. Arbitrarily long numeric literals could also reach charts._format_value()
|
|
48
|
+
as float('inf') and raise OverflowError.
|
|
49
|
+
|
|
50
|
+
- Invalid grammar (anything beyond header/title/accTitle/accDescr/%% comments/slice lines) now
|
|
51
|
+
discards the whole diagram and falls back to raw source, matching Mermaid's actual pie grammar. -
|
|
52
|
+
Negative slice values are rejected the same way (Mermaid disallows them). - Non-finite values
|
|
53
|
+
(from huge numeric literals) are rejected in parse_pie, and the summed total is re-checked in
|
|
54
|
+
render() as a second guard, both falling back to source instead of crashing. - Slice labels now
|
|
55
|
+
also accept single-quoted strings and escaped quotes inside double-quoted strings, per Mermaid's
|
|
56
|
+
real string grammar.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## v4.3.0 (2026-07-06)
|
|
60
|
+
|
|
61
|
+
### Features
|
|
62
|
+
|
|
63
|
+
- Add flowchart renderer data model
|
|
64
|
+
([`cf3caef`](https://github.com/crouton-labs/termrender/commit/cf3caef28411d2ace3f71c63f218af52ba460da0))
|
|
65
|
+
|
|
66
|
+
- Wire sequence renderer and add native mindmap/journey/timeline renderers
|
|
67
|
+
([`120b51c`](https://github.com/crouton-labs/termrender/commit/120b51cddf58cd91ee948706c143c84ca42c7e9f))
|
|
68
|
+
|
|
69
|
+
- mermaid.py dispatch now routes sequenceDiagram to the finished mermaid_sequence.render_sequence,
|
|
70
|
+
and mindmap/journey/timeline to new native renderers, leaving flowchart/graph/class/state/ER on
|
|
71
|
+
the vendored Go binary path unchanged. - New shared mermaid_prelude.strip_prelude_lines skips
|
|
72
|
+
leading %% comments, %%{init}%% directives, and --- YAML frontmatter before sniffing a diagram's
|
|
73
|
+
type keyword, fixing valid diagrams with those preludes mis-routing to the binary fallback.
|
|
74
|
+
mermaid_sequence.py's own header guard uses the same helper. - mermaid_mindmap.py:
|
|
75
|
+
indentation-based mindmap source maps directly onto tree.py's shape; strips the mindmap header,
|
|
76
|
+
::icon()/:::class decoration, and node-shape delimiters down to label text. - mermaid_journey.py:
|
|
77
|
+
section/task outline mapped onto tree.py, tasks rendered as depth-1 children with a star rating
|
|
78
|
+
and actor list. - mermaid_timeline.py: sections rendered as separate timeline.py sub-timelines,
|
|
79
|
+
with period-less continuation lines correctly attaching to the previous row instead of repeating
|
|
80
|
+
the date. - Dispatch integration tests for every new route plus all three prelude forms; parse +
|
|
81
|
+
golden-output render tests per new module. - CLAUDE.md updated to describe the current
|
|
82
|
+
dispatch/prelude state.
|
|
83
|
+
|
|
84
|
+
Full suite: 230 -> 292 tests, all green.
|
|
85
|
+
|
|
86
|
+
|
|
4
87
|
## v4.2.0 (2026-07-06)
|
|
5
88
|
|
|
6
89
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termrender
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.1
|
|
4
4
|
Summary: Rich terminal rendering of directive-flavored markdown
|
|
5
5
|
Project-URL: Homepage, https://github.com/CaptainCrouton89/termrender
|
|
6
6
|
Project-URL: Repository, https://github.com/CaptainCrouton89/termrender
|
|
@@ -43,11 +43,13 @@ Attribution line is rendered for `block.attrs["author"]` **or** `block.attrs["by
|
|
|
43
43
|
|
|
44
44
|
`mermaid.py`'s `render(block, color)` (leaf signature, no `render_child` — the parent `CLAUDE.md` incorrectly lists mermaid as a Container renderer) is a thin wrapper: it reads `block.attrs["_rendered"]` if layout already precomputed it, else calls `render_mermaid_lines(source, width)`, then pads every line to `block.width` with `visual_ljust`. All the type-routing logic lives in `render_mermaid_lines`, shared with `layout.py`'s height pass (see below) so there is exactly one dispatch path, not two.
|
|
45
45
|
|
|
46
|
-
**Dispatch is by first
|
|
46
|
+
**Dispatch is by first real line, case-insensitive** (`_first_line_type`), after skipping any prelude via `mermaid_prelude.strip_prelude_lines` (see below): `pie`, `gantt`, `sequenceDiagram`, `mindmap`, `journey`, and `timeline` route to native Python renderers (`mermaid_pie.py`, `mermaid_gantt.py`, `mermaid_sequence.py`, `mermaid_mindmap.py`, `mermaid_journey.py`, `mermaid_timeline.py`). Every other type — `graph`/`flowchart`, `classDiagram`, `stateDiagram`, `erDiagram`, … — routes to `_render_via_binary`, the pre-existing single path through the vendored Go `mermaid-ascii` binary. **Only the six listed types have native renderers; every other type's degradation is byte-for-byte what it was before this dispatcher existed** — do not add a bordered fallback panel for them, since crouter's attach viewer detects render failure by the *absence* of box-drawing glyphs in the output, and a decorative border would defeat that.
|
|
47
47
|
|
|
48
|
-
**`
|
|
48
|
+
**`mermaid_prelude.py`** — a tiny shared module (no dependency on `Block` or the rest of the pipeline) implementing `strip_prelude_lines(lines)`, used by both `mermaid.py`'s dispatch sniff and `mermaid_sequence.py`'s own header check. It skips leading blank lines, `%%` line comments, `%%{init: ...}%%` directives, and `---`-delimited YAML frontmatter, permissively: an unterminated frontmatter block or directive is left alone rather than eating real content. Prelude stripping is for *sniffing/validating the type line only* — the per-type body parsers still receive the full untouched source; `mermaid_sequence.py`'s `_parse` already tolerates arbitrary prelude lines before its header (they fail every line pattern and are skipped via its `seen_header` gate), and `mermaid_pie.py`/`mermaid_gantt.py`/the mindmap/journey/timeline parsers likewise just skip lines that don't match their grammar — a frontmatter `title: X` line could in principle collide with a `title` grammar rule, but this is accepted as the same class of pragmatic-parsing tradeoff as any other unrecognized line, not something the prelude skip needs to prevent.
|
|
49
49
|
|
|
50
|
-
**`
|
|
50
|
+
**`_render_via_binary`** (used for flowchart/graph/class/state/ER/everything-without-a-native-renderer): normalizes via `preprocess_mermaid_for_ascii`, shells out with `check=True`, and undoes the Latin-1 double-encoding via `fix_mermaid_encoding` (`mermaid-ascii` misreads UTF-8 input as Latin-1 and re-encodes, corrupting multi-byte characters — e.g. `→` becomes `â\x86\x92`; `text.encode("latin-1").decode("utf-8")` reverses it, and on failure silently returns the corrupted string). Any failure — `CalledProcessError` (including a non-zero exit, now actually checked via `check=True`), missing binary, or timeout — degrades to the raw source text. A labeled back-edge (`X -->|lbl| Y` in `graph LR`) panics the binary on both the PyPI 1.2.0 wheel and vendored master; the resulting non-zero exit degrades the same way. Note `preprocess_mermaid_for_ascii` has its own independent first-line sniff (not `mermaid_prelude`-aware) to decide sequence-vs-flowchart-vs-other normalization — a prelude ahead of `graph`/`flowchart`/`sequenceDiagram` here means no shape/arrow normalization runs, an existing (unfixed) limitation of the binary path, orthogonal to dispatch routing.
|
|
51
|
+
|
|
52
|
+
**`visual_ljust` pads but never truncates**: every output line (from any type — binary or native) is padded to `block.width` in the outer `render()`. Lines wider than `block.width` overflow without clipping. `mermaid-ascii` has no width-control flag (`--maxWidth`/`-w` do not exist — the call passes only `-f - -y 1`), so wide flowchart output silently exceeds the layout boundary; the native renderers respect `width` by construction (pie/gantt/timeline size their own bars to it; sequence/mindmap/journey have intrinsic width and may overflow, matching the binary path's contract) — see each renderer's own note below.
|
|
51
53
|
|
|
52
54
|
Layout (`layout.py::resolve_height`, `BlockType.MERMAID` branch) precomputes `block.attrs["_rendered"]` by calling the same `render_mermaid_lines`, so `mermaid.py`'s `render()` only recomputes when that key is absent (e.g. calling `render()` on a block that skipped the layout pass).
|
|
53
55
|
|
|
@@ -66,3 +68,19 @@ A text pie chart is worse than a labeled bar chart at the same information densi
|
|
|
66
68
|
**Missing start dates fall back to `last_end`**, a running cursor updated after every successfully-placed task (mirroring mermaid's own default-start-follows-previous-task semantics): a bare `Task2 :5d` with no date and no `after` picks up where the previous task left off; an `after <id>` referencing an id that was never seen falls back the same way rather than dropping the task. A task with no start anchor at all (first task in the diagram, no date, no `after`) is skipped — degrade, don't crash.
|
|
67
69
|
|
|
68
70
|
`render` scales every task's `(start, end)` into a ratio over the full `[min_start, max_end]` span and draws a `░`/`█` track via `_draw_span`; section names are their own row, un-indented, with all task rows below sharing one global label-column width for alignment across sections. Diagrams with zero resolved tasks (parse produced no sections) degrade to the raw source lines.
|
|
71
|
+
|
|
72
|
+
## `mermaid_sequence.py` — deterministic column layout, no graph-layout search
|
|
73
|
+
|
|
74
|
+
The one native renderer not built by reusing another primitive (there's no existing "sequence diagram" shape in termrender to reuse) — `render_sequence(source, width)` is a standalone module with its own parser (`_parse`, producing `Arrow`/`NoteEvent`/`BlockBoundary`/`PlainLabel` events) and its own column-layout pass (`_layout_columns`, a 1D constraint solve: message order already fixes the vertical axis, declaration order fixes the column axis, so gaps between adjacent columns are widened only enough for whatever message text crosses them). `width` is advisory-only and never wraps/truncates — sequence diagrams have intrinsic width and may overflow, same contract as the flowchart binary path. Parsing never raises except when the source isn't a sequence diagram at all (checked via `mermaid_prelude.strip_prelude_lines` against the first real line); every malformed/unrecognized construct within a real sequence diagram (a stray `else`/`end`, an unsupported arrow punctuation, a flowchart-only `-.->`) degrades to a plain text line instead. See the module's own docstring for the full grammar supported and known degradations (CJK column misalignment, `Note` multi-participant simplification, etc.).
|
|
75
|
+
|
|
76
|
+
## `mermaid_mindmap.py` — mindmap source is already `tree.py`'s shape
|
|
77
|
+
|
|
78
|
+
Mindmap is indentation-based exactly like `tree.py`'s input format, so `parse_mindmap` does no layout of its own: it drops the `mindmap` header line and `::icon(...)`/`:::className` decoration lines, strips each node's shape delimiters (`(())`, `{{}}`, `))((`, `)(`, `[]`, `()`, checked most-specific-first the same way `mermaid.py`'s `_FLOWCHART_SHAPE_SUBS` is ordered) down to label text, and preserves each surviving line's original leading whitespace verbatim — depth then falls out of `tree.py`'s own indentation auto-detection for free. `render` hands the transformed text to `tree.py::render` via a synthetic `Block(type=BlockType.TREE, ...)`, always `color=False`. A diagram with nothing left after stripping (blank, or header-only) degrades to the raw source lines.
|
|
79
|
+
|
|
80
|
+
## `mermaid_journey.py` — section/task outline mapped onto `tree.py`
|
|
81
|
+
|
|
82
|
+
A journey is a two-level outline (`section` → tasks), so `parse_journey` splits `name: score: actors` task lines (tolerating a missing/non-numeric score as `None` and missing actors as `[]`, never raising) into section buckets — a task before any `section` line lands in a leading unnamed bucket, matching `mermaid_gantt.py`'s same convention. `render` builds one synthetic indented-tree source string (section names at depth 0, `_format_task` output — `name ★★★☆☆ (actors)`, stars clamped to the 0–5 range — as depth-1 children under a two-space indent) and renders it through `tree.py::render` via a synthetic `Block`, same pattern as mindmap. The diagram title (if any) is a plain line prepended above the tree, not part of it. Sections with no tasks are dropped; zero surviving sections degrades to the raw source lines.
|
|
83
|
+
|
|
84
|
+
## `mermaid_timeline.py` — sections rendered as separate `timeline.py` sub-timelines
|
|
85
|
+
|
|
86
|
+
`parse_timeline` handles `title`, `section`, and `period : event[ : event ...]` lines; a period-less line (blank before the first `:`) continues the *previous* row's period rather than introducing a new one — tracked via a `shows_date` flag so only the row that actually introduces a period (same-line or most recent prior line) prints it, everything else in that group prints a blank date column for alignment. `render` calls `timeline.py::render` once per section (each section's `entries` in a synthetic `Block(type=BlockType.TIMELINE, ...)`), inserting the section name as its own plain row above each sub-timeline and a blank separator row between sections — reuses the existing bullet/connector rendering rather than a bespoke one. A diagram with zero resolved entries degrades to the raw source lines.
|
|
@@ -7,7 +7,15 @@ import subprocess
|
|
|
7
7
|
|
|
8
8
|
from termrender._mermaid_bin import mermaid_ascii_bin
|
|
9
9
|
from termrender.blocks import Block
|
|
10
|
-
from termrender.renderers import
|
|
10
|
+
from termrender.renderers import (
|
|
11
|
+
mermaid_gantt,
|
|
12
|
+
mermaid_journey,
|
|
13
|
+
mermaid_mindmap,
|
|
14
|
+
mermaid_pie,
|
|
15
|
+
mermaid_sequence,
|
|
16
|
+
mermaid_timeline,
|
|
17
|
+
)
|
|
18
|
+
from termrender.renderers.mermaid_prelude import strip_prelude_lines
|
|
11
19
|
from termrender.style import visual_ljust
|
|
12
20
|
|
|
13
21
|
|
|
@@ -118,13 +126,16 @@ def preprocess_mermaid_for_ascii(source: str) -> str:
|
|
|
118
126
|
|
|
119
127
|
|
|
120
128
|
def _first_line_type(source: str) -> str:
|
|
121
|
-
"""Return the lowercased first
|
|
129
|
+
"""Return the lowercased first real line's leading keyword.
|
|
122
130
|
|
|
123
131
|
This is the dispatch key: mermaid identifies a diagram's type from its
|
|
124
132
|
first line (``graph``/``flowchart``, ``pie``, ``gantt``, ``sequenceDiagram``,
|
|
125
|
-
``classDiagram``, …)
|
|
133
|
+
``classDiagram``, …), after skipping any prelude (blank lines, ``%%``
|
|
134
|
+
comments/directives, ``---`` YAML frontmatter — see ``mermaid_prelude.py``).
|
|
135
|
+
Returns ``""`` for a source with no real content past its prelude.
|
|
126
136
|
"""
|
|
127
|
-
|
|
137
|
+
lines = strip_prelude_lines(source.splitlines())
|
|
138
|
+
first = next((l.strip() for l in lines if l.strip()), "")
|
|
128
139
|
return first.lower()
|
|
129
140
|
|
|
130
141
|
|
|
@@ -156,17 +167,26 @@ def _render_via_binary(source: str) -> str:
|
|
|
156
167
|
def render_mermaid_lines(source: str, width: int) -> list[str]:
|
|
157
168
|
"""Dispatch a mermaid source to its type's renderer and return raw lines.
|
|
158
169
|
|
|
159
|
-
``pie
|
|
160
|
-
|
|
161
|
-
keeps today's exact path: the
|
|
162
|
-
|
|
163
|
-
unpadded; callers apply width
|
|
170
|
+
``pie``, ``gantt``, ``sequenceDiagram``, ``mindmap``, ``journey``, and
|
|
171
|
+
``timeline`` get dedicated native Python renderers. Every other type
|
|
172
|
+
(flowchart/graph, class, state, ER, …) keeps today's exact path: the
|
|
173
|
+
vendored Go mermaid-ascii binary, which echoes unparseable/unsupported
|
|
174
|
+
source back verbatim. Lines are returned unpadded; callers apply width
|
|
175
|
+
padding uniformly.
|
|
164
176
|
"""
|
|
165
177
|
diagram_type = _first_line_type(source)
|
|
166
178
|
if diagram_type.startswith("pie"):
|
|
167
179
|
return mermaid_pie.render(source, width)
|
|
168
180
|
if diagram_type.startswith("gantt"):
|
|
169
181
|
return mermaid_gantt.render(source, width)
|
|
182
|
+
if diagram_type.startswith("sequencediagram"):
|
|
183
|
+
return mermaid_sequence.render_sequence(source, width)
|
|
184
|
+
if diagram_type.startswith("mindmap"):
|
|
185
|
+
return mermaid_mindmap.render(source, width)
|
|
186
|
+
if diagram_type.startswith("journey"):
|
|
187
|
+
return mermaid_journey.render(source, width)
|
|
188
|
+
if diagram_type.startswith("timeline"):
|
|
189
|
+
return mermaid_timeline.render(source, width)
|
|
170
190
|
return _render_via_binary(source).split("\n")
|
|
171
191
|
|
|
172
192
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Data model for the native mermaid flowchart/graph renderer.
|
|
2
|
+
|
|
3
|
+
Pure dataclasses + enums, zero logic — the contract between the parser
|
|
4
|
+
(``mermaid_flow_parser.py``, source → :class:`FlowGraph`) and the layout
|
|
5
|
+
engine (``mermaid_flow_layout.py``, :class:`FlowGraph` → char grid). Neither
|
|
6
|
+
side imports the other; both import this module. Nothing here has behavior.
|
|
7
|
+
|
|
8
|
+
Model shape
|
|
9
|
+
-----------
|
|
10
|
+
A :class:`FlowGraph` carries a :class:`Direction`, a declaration-ordered
|
|
11
|
+
list of unique :class:`FlowNode` (id is the graph key, label is the drawn
|
|
12
|
+
text), a declaration-ordered list of :class:`FlowEdge` with ``&`` fan-out
|
|
13
|
+
already expanded to individual edges, and the top-level :class:`Subgraph`
|
|
14
|
+
blocks (nesting via ``Subgraph.children``). Arrowheads are two per-endpoint
|
|
15
|
+
booleans (``dst_arrow``/``src_arrow``) independent of the line
|
|
16
|
+
:class:`EdgeStyle`. Ignored source directives (class/classDef/style/click/
|
|
17
|
+
linkStyle) leave no trace in the model — the parser drops them.
|
|
18
|
+
|
|
19
|
+
See ``flow-design.md`` (this node's orchestrator context dir) for the full
|
|
20
|
+
architecture and the decisions baked into these shapes.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from enum import Enum
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"Direction",
|
|
30
|
+
"NodeShape",
|
|
31
|
+
"EdgeStyle",
|
|
32
|
+
"FlowNode",
|
|
33
|
+
"FlowEdge",
|
|
34
|
+
"Subgraph",
|
|
35
|
+
"FlowGraph",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Direction(Enum):
|
|
40
|
+
"""Rank-flow direction. ``TD`` is a mermaid alias for ``TB`` and is
|
|
41
|
+
normalized to ``TB`` by the parser (never stored as a separate member)."""
|
|
42
|
+
|
|
43
|
+
TB = "TB" # top → bottom
|
|
44
|
+
LR = "LR" # left → right
|
|
45
|
+
RL = "RL" # right → left
|
|
46
|
+
BT = "BT" # bottom → top
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class NodeShape(Enum):
|
|
50
|
+
"""Declared node shape. The engine renders ``DIAMOND``, ``ROUND``/
|
|
51
|
+
``STADIUM`` (rounded corners) and ``CIRCLE`` distinctly where cheap; every
|
|
52
|
+
other shape falls back to a rectangle border. Only the shape class + label
|
|
53
|
+
text are load-bearing."""
|
|
54
|
+
|
|
55
|
+
RECT = "rect" # [text]
|
|
56
|
+
ROUND = "round" # (text)
|
|
57
|
+
STADIUM = "stadium" # ([text])
|
|
58
|
+
SUBROUTINE = "subroutine" # [[text]]
|
|
59
|
+
CYLINDER = "cylinder" # [(text)]
|
|
60
|
+
CIRCLE = "circle" # ((text))
|
|
61
|
+
DIAMOND = "diamond" # {text}
|
|
62
|
+
HEXAGON = "hexagon" # {{text}}
|
|
63
|
+
PARALLELOGRAM = "parallelogram" # [/text/]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EdgeStyle(Enum):
|
|
67
|
+
"""Edge line style. Arrowheads are orthogonal to style (see
|
|
68
|
+
:class:`FlowEdge`)."""
|
|
69
|
+
|
|
70
|
+
SOLID = "solid" # --- / -->
|
|
71
|
+
THICK = "thick" # === / ==>
|
|
72
|
+
DOTTED = "dotted" # -.- / -.->
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class FlowNode:
|
|
77
|
+
"""A graph node. ``id`` is the graph key (dedup + edge endpoints);
|
|
78
|
+
``label`` is the drawn text. Bare ``A`` yields ``id == label == "A"``.
|
|
79
|
+
Re-declaring an id keeps the first-seen node; a later declaration that
|
|
80
|
+
carries a shape/label updates it in place (parser policy)."""
|
|
81
|
+
|
|
82
|
+
id: str
|
|
83
|
+
label: str
|
|
84
|
+
shape: NodeShape = NodeShape.RECT
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class FlowEdge:
|
|
89
|
+
"""A directed edge between node ids. ``dst_arrow``/``src_arrow`` are the
|
|
90
|
+
drawn arrowheads: ``-->`` → dst_arrow=True, src_arrow=False; ``---`` → both
|
|
91
|
+
False (plain line); ``<-->`` → both True. ``label`` is the optional edge
|
|
92
|
+
label (``|text|`` or the ``-- text -->`` inline form); ``None`` when absent
|
|
93
|
+
(empty-string labels are stored as ``None``)."""
|
|
94
|
+
|
|
95
|
+
src: str
|
|
96
|
+
dst: str
|
|
97
|
+
style: EdgeStyle = EdgeStyle.SOLID
|
|
98
|
+
label: str | None = None
|
|
99
|
+
dst_arrow: bool = True
|
|
100
|
+
src_arrow: bool = False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class Subgraph:
|
|
105
|
+
"""A ``subgraph id [title] ... end`` block. ``node_ids`` are the ids
|
|
106
|
+
declared directly inside this block (NOT those inside nested children).
|
|
107
|
+
``children`` are nested subgraphs. ``title`` is the display title (defaults
|
|
108
|
+
to id when the block has no explicit title)."""
|
|
109
|
+
|
|
110
|
+
id: str
|
|
111
|
+
title: str
|
|
112
|
+
node_ids: list[str] = field(default_factory=list)
|
|
113
|
+
children: list["Subgraph"] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class FlowGraph:
|
|
118
|
+
"""Parsed flowchart. ``nodes`` is in first-seen declaration order, unique
|
|
119
|
+
by id. ``edges`` is in declaration order, with ``&`` fan-out already
|
|
120
|
+
expanded to individual edges. ``subgraphs`` are the top-level blocks
|
|
121
|
+
(nesting via ``Subgraph.children``). Ignored lines (class/classDef/style/
|
|
122
|
+
click/linkStyle) leave no trace."""
|
|
123
|
+
|
|
124
|
+
direction: Direction
|
|
125
|
+
nodes: list[FlowNode] = field(default_factory=list)
|
|
126
|
+
edges: list[FlowEdge] = field(default_factory=list)
|
|
127
|
+
subgraphs: list[Subgraph] = field(default_factory=list)
|