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.
Files changed (72) hide show
  1. {termrender-4.2.0 → termrender-4.3.1}/CHANGELOG.md +83 -0
  2. {termrender-4.2.0 → termrender-4.3.1}/PKG-INFO +1 -1
  3. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/CLAUDE.md +21 -3
  4. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/mermaid.py +29 -9
  5. termrender-4.3.1/src/termrender/renderers/mermaid_flow_model.py +127 -0
  6. termrender-4.3.1/src/termrender/renderers/mermaid_gantt.py +459 -0
  7. termrender-4.3.1/src/termrender/renderers/mermaid_journey.py +121 -0
  8. termrender-4.3.1/src/termrender/renderers/mermaid_mindmap.py +99 -0
  9. termrender-4.3.1/src/termrender/renderers/mermaid_pie.py +128 -0
  10. termrender-4.3.1/src/termrender/renderers/mermaid_prelude.py +55 -0
  11. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/mermaid_sequence.py +8 -5
  12. termrender-4.3.1/src/termrender/renderers/mermaid_timeline.py +118 -0
  13. termrender-4.3.1/tests/test_mermaid_dispatch.py +195 -0
  14. termrender-4.3.1/tests/test_mermaid_flow_model.py +113 -0
  15. termrender-4.3.1/tests/test_mermaid_gantt.py +333 -0
  16. termrender-4.3.1/tests/test_mermaid_journey.py +128 -0
  17. termrender-4.3.1/tests/test_mermaid_mindmap.py +115 -0
  18. {termrender-4.2.0 → termrender-4.3.1}/tests/test_mermaid_pie.py +63 -2
  19. termrender-4.3.1/tests/test_mermaid_timeline.py +107 -0
  20. termrender-4.2.0/src/termrender/renderers/mermaid_gantt.py +0 -222
  21. termrender-4.2.0/src/termrender/renderers/mermaid_pie.py +0 -84
  22. termrender-4.2.0/tests/test_mermaid_dispatch.py +0 -89
  23. termrender-4.2.0/tests/test_mermaid_gantt.py +0 -154
  24. {termrender-4.2.0 → termrender-4.3.1}/.github/workflows/publish.yml +0 -0
  25. {termrender-4.2.0 → termrender-4.3.1}/.gitignore +0 -0
  26. {termrender-4.2.0 → termrender-4.3.1}/CLAUDE.md +0 -0
  27. {termrender-4.2.0 → termrender-4.3.1}/LICENSE +0 -0
  28. {termrender-4.2.0 → termrender-4.3.1}/README.md +0 -0
  29. {termrender-4.2.0 → termrender-4.3.1}/design.json +0 -0
  30. {termrender-4.2.0 → termrender-4.3.1}/pyproject.toml +0 -0
  31. {termrender-4.2.0 → termrender-4.3.1}/requirements.json +0 -0
  32. {termrender-4.2.0 → termrender-4.3.1}/scripts/build-mermaid-ascii.sh +0 -0
  33. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/CLAUDE.md +0 -0
  34. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/__init__.py +0 -0
  35. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/__main__.py +0 -0
  36. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/_bin/mermaid-ascii-darwin-arm64 +0 -0
  37. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/_mermaid_bin.py +0 -0
  38. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/blocks.py +0 -0
  39. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/emit.py +0 -0
  40. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/layout.py +0 -0
  41. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/parser.py +0 -0
  42. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/py.typed +0 -0
  43. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/__init__.py +0 -0
  44. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/borders.py +0 -0
  45. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/charts.py +0 -0
  46. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/code.py +0 -0
  47. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/columns.py +0 -0
  48. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/diff.py +0 -0
  49. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/divider.py +0 -0
  50. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/panel.py +0 -0
  51. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/quote.py +0 -0
  52. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/stat.py +0 -0
  53. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/table.py +0 -0
  54. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/text.py +0 -0
  55. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/timeline.py +0 -0
  56. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/renderers/tree.py +0 -0
  57. {termrender-4.2.0 → termrender-4.3.1}/src/termrender/style.py +0 -0
  58. {termrender-4.2.0 → termrender-4.3.1}/tests/__init__.py +0 -0
  59. {termrender-4.2.0 → termrender-4.3.1}/tests/test_charts.py +0 -0
  60. {termrender-4.2.0 → termrender-4.3.1}/tests/test_cli_contract.py +0 -0
  61. {termrender-4.2.0 → termrender-4.3.1}/tests/test_column_alignment.py +0 -0
  62. {termrender-4.2.0 → termrender-4.3.1}/tests/test_diff.py +0 -0
  63. {termrender-4.2.0 → termrender-4.3.1}/tests/test_inline_badge.py +0 -0
  64. {termrender-4.2.0 → termrender-4.3.1}/tests/test_linebreak.py +0 -0
  65. {termrender-4.2.0 → termrender-4.3.1}/tests/test_mermaid_compat.py +0 -0
  66. {termrender-4.2.0 → termrender-4.3.1}/tests/test_mermaid_sequence.py +0 -0
  67. {termrender-4.2.0 → termrender-4.3.1}/tests/test_myst_gaps.py +0 -0
  68. {termrender-4.2.0 → termrender-4.3.1}/tests/test_stat.py +0 -0
  69. {termrender-4.2.0 → termrender-4.3.1}/tests/test_tasklist.py +0 -0
  70. {termrender-4.2.0 → termrender-4.3.1}/tests/test_timeline.py +0 -0
  71. {termrender-4.2.0 → termrender-4.3.1}/tests/test_variable_colons.py +0 -0
  72. {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.2.0
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 non-blank line, case-insensitive** (`_first_line_type`): `pie` and `gantt` route to the native Python renderers in `mermaid_pie.py` / `mermaid_gantt.py`. Every other type — `graph`/`flowchart`, `sequenceDiagram`, `classDiagram`, `stateDiagram`, `erDiagram`, `journey`, `mindmap`, … — routes to `_render_via_binary`, the pre-existing single path through the vendored Go `mermaid-ascii` binary. **Only `pie` and `gantt` 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.
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
- **`_render_via_binary`** (used for flowchart/graph/sequence/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.
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
- **`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/sequence output silently exceeds the layout boundary; the native pie/gantt renderers respect `width` by construction (they size their own bars to it) and don't have this problem.
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 mermaid_gantt, mermaid_pie
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 non-blank line's leading keyword.
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``, …). Returns ``""`` for an empty/blank source.
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
- first = next((l.strip() for l in source.splitlines() if l.strip()), "")
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`` and ``gantt`` get dedicated native Python renderers. Every other
160
- type (flowchart/graph, sequence, class, state, ER, journey, mindmap, …)
161
- keeps today's exact path: the vendored Go mermaid-ascii binary, which
162
- echoes unparseable/unsupported source back verbatim. Lines are returned
163
- unpadded; callers apply width padding uniformly.
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)