termrender 4.10.2__tar.gz → 4.10.3__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.10.2 → termrender-4.10.3}/CHANGELOG.md +25 -0
- {termrender-4.10.2 → termrender-4.10.3}/PKG-INFO +1 -1
- {termrender-4.10.2 → termrender-4.10.3}/design.json +8 -8
- {termrender-4.10.2 → termrender-4.10.3}/pyproject.toml +6 -0
- {termrender-4.10.2 → termrender-4.10.3}/requirements.json +6 -6
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/CLAUDE.md +2 -2
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid.py +5 -29
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_class.py +70 -26
- termrender-4.10.3/src/termrender/renderers/mermaid_degradation.py +34 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_er.py +34 -26
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_flow.py +17 -33
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_flow_parser.py +71 -29
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_state.py +36 -31
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_class.py +50 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_er.py +36 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_flow.py +59 -1
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_flow_parser.py +36 -13
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_state.py +60 -11
- termrender-4.10.3/uv.lock +198 -0
- termrender-4.10.2/uv.lock +0 -70
- {termrender-4.10.2 → termrender-4.10.3}/.github/workflows/publish.yml +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/.gitignore +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/CLAUDE.md +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/LICENSE +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/README.md +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/CLAUDE.md +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/__init__.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/__main__.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/blocks.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/emit.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/layout.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/parser.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/py.typed +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/__init__.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/borders.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/charts.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/code.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/columns.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/diff.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/divider.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_flow_layout.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_flow_model.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_gantt.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_journey.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_mindmap.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_pie.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_prelude.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_sequence.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/mermaid_timeline.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/panel.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/quote.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/stat.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/table.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/text.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/timeline.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/renderers/tree.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/src/termrender/style.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/__init__.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_charts.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_cli_contract.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_column_alignment.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_diff.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_inline_badge.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_linebreak.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_dispatch.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_flow_corpus.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_flow_layout.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_flow_model.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_flow_shapes.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_gantt.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_journey.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_mindmap.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_pie.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_sequence.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_mermaid_timeline.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_myst_gaps.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_stat.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_tasklist.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_timeline.py +0 -0
- {termrender-4.10.2 → termrender-4.10.3}/tests/test_variable_colons.py +0 -0
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v4.10.3 (2026-07-07)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **mermaid**: Strict degradation — unrecognized syntax raw-echoes instead of half-rendering
|
|
9
|
+
([`904d1cc`](https://github.com/crouton-labs/termrender/commit/904d1ccaa5c012bbeefdc8f351fb25164b0c3778))
|
|
10
|
+
|
|
11
|
+
Enforce the native Mermaid degradation contract across flowchart, classDiagram, stateDiagram-v2, and
|
|
12
|
+
erDiagram renderers: only well-formed presentational/accessibility directives are skipped, while
|
|
13
|
+
unsupported statements, dangling connectors, stray closers, and unterminated structural blocks now
|
|
14
|
+
fail parsing and raw-echo the entire original source.
|
|
15
|
+
|
|
16
|
+
Consolidate raw-echo glyph scrubbing in mermaid_degradation.py and update the dispatcher/renderers
|
|
17
|
+
to import the shared helper so every degradation path removes box/geometric glyphs consistently.
|
|
18
|
+
|
|
19
|
+
Flip the parser-level tests that previously pinned best-effort recovery for malformed flowcharts to
|
|
20
|
+
expect FlowchartError, and flip the state stray-close-brace render test to expect raw echo. Add
|
|
21
|
+
regression coverage for the review repros, malformed directive-shaped lines, valid accessibility
|
|
22
|
+
colon forms, and native rendering of valid presentational directives.
|
|
23
|
+
|
|
24
|
+
Refresh renderer docs and architecture metadata away from the removed Go/mermaid-ascii path, and add
|
|
25
|
+
minimal pytest project config so the required exact 'uv run pytest' gate imports the src package
|
|
26
|
+
in this repository.
|
|
27
|
+
|
|
28
|
+
|
|
4
29
|
## v4.10.2 (2026-07-07)
|
|
5
30
|
|
|
6
31
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termrender
|
|
3
|
-
Version: 4.10.
|
|
3
|
+
Version: 4.10.3
|
|
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
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
"id": "DES-005",
|
|
100
100
|
"title": "Mermaid block detection",
|
|
101
101
|
"description": "How mermaid code blocks are identified and stored in the AST",
|
|
102
|
-
"content": "Mermaid blocks are standard fenced code blocks with the 'mermaid' language tag:\n\n ```mermaid\n graph TD\n A[Build] --> B[Test]\n ```\n\nThe markdown parser already handles fenced code blocks. We just need to:\n\n1. Detect code blocks where language == 'mermaid'\n2. Store the raw mermaid source in the Block's attrs\n3. At render time,
|
|
103
|
-
"agentNotes": "We
|
|
102
|
+
"content": "Mermaid blocks are standard fenced code blocks with the 'mermaid' language tag:\n\n ```mermaid\n graph TD\n A[Build] --> B[Test]\n ```\n\nThe markdown parser already handles fenced code blocks. We just need to:\n\n1. Detect code blocks where language == 'mermaid'\n2. Store the raw mermaid source in the Block's attrs\n3. At render time, dispatch the source to the native renderer for its diagram type\n\nThe Block node:\n\n Block(\n type=BlockType.MERMAID,\n children=[],\n text=[],\n attrs={\"source\": \"graph TD\\n A[Build] --> B[Test]\"}\n )\n\nMermaid syntax is parsed by termrender's own native Python parsers, one per diagram type (renderers/mermaid_flow_parser.py, mermaid_class.py, mermaid_state.py, mermaid_er.py, and dedicated modules for pie/gantt/sequence/mindmap/journey/timeline) — there is no external mermaid rendering dependency.",
|
|
103
|
+
"agentNotes": "We parse mermaid syntax ourselves with a native Python parser per diagram type; the flowchart/class/state/ER parsers share one grandalf-based layout engine (mermaid_flow_layout.py) via a common FlowGraph model. We still only need the rendered output's dimensions (width x height in characters) for the layout engine.",
|
|
104
104
|
"status": "draft",
|
|
105
105
|
"userNotes": "",
|
|
106
106
|
"reviewAction": null,
|
|
@@ -219,8 +219,8 @@
|
|
|
219
219
|
{
|
|
220
220
|
"id": "DES-012",
|
|
221
221
|
"title": "Mermaid renderer",
|
|
222
|
-
"description": "How mermaid diagrams are rendered
|
|
223
|
-
"content": "Mermaid blocks
|
|
222
|
+
"description": "How mermaid diagrams are rendered by termrender's native per-type renderers",
|
|
223
|
+
"content": "Mermaid blocks dispatch by diagram type to a native Python renderer:\n\n from termrender.renderers.mermaid import render_mermaid_lines\n\n def render_mermaid(block: Block) -> list[str]:\n source = block.attrs['source']\n lines = render_mermaid_lines(source, block.width)\n return [visual_ljust(line, block.width) for line in lines]\n\nrender_mermaid_lines reads the diagram's first real line to pick its renderer (flowchart/class/state/ER share a grandalf-based Sugiyama layout engine; pie/gantt/sequence/mindmap/journey/timeline each have their own dedicated renderer). Any unrecognized type, or any input a renderer can't parse under its supported grammar, degrades to a raw echo of the source lines rather than a half-rendered diagram — see renderers/CLAUDE.md's degradation contract.\n\nDuring the layout pass, we render the mermaid once and cache the output on the block to avoid double-rendering:\n\n block.attrs['_rendered'] = '\\n'.join(render_mermaid_lines(source, width))\n block.height = block.attrs['_rendered'].count('\\n') + 1",
|
|
224
224
|
"decision": {
|
|
225
225
|
"proposal": {
|
|
226
226
|
"title": "Pre-render during layout",
|
|
@@ -238,7 +238,7 @@
|
|
|
238
238
|
"simplicity": "Pre-render is actually simpler — no estimation heuristics to maintain"
|
|
239
239
|
}
|
|
240
240
|
},
|
|
241
|
-
"agentNotes": "This also answers OQ1 from the parser section — pre-render is the way to go. The cached output is stored as a private _rendered attr.",
|
|
241
|
+
"agentNotes": "This also answers OQ1 from the parser section — pre-render is the way to go. The cached output is stored as a private _rendered attr. No external mermaid rendering dependency: every diagram type is a native Python parser in renderers/.",
|
|
242
242
|
"status": "draft",
|
|
243
243
|
"userNotes": "",
|
|
244
244
|
"reviewAction": "agree",
|
|
@@ -362,7 +362,7 @@
|
|
|
362
362
|
"id": "DES-018",
|
|
363
363
|
"title": "Project structure",
|
|
364
364
|
"description": "Directory layout and module organization",
|
|
365
|
-
"content": " termrender/\n ├── pyproject.toml\n ├── src/\n │ └── termrender/\n │ ├── __init__.py # render() — the public API\n │ ├── __main__.py # CLI entry point\n │ ├── parser.py # markdown + directive parsing\n │ ├── layout.py # width/height resolution\n │ ├── emit.py # ANSI string generation\n │ ├── style.py # ANSI color/style helpers\n │ ├── blocks.py # Block, InlineSpan dataclasses\n │ ├── renderers/\n │ │ ├── __init__.py\n │ │ ├── panel.py # panel + callout\n │ │ ├── columns.py # columns + col\n │ │ ├── tree.py # tree rendering\n │ │ ├── mermaid.py # mermaid-
|
|
365
|
+
"content": " termrender/\n ├── pyproject.toml\n ├── src/\n │ └── termrender/\n │ ├── __init__.py # render() — the public API\n │ ├── __main__.py # CLI entry point\n │ ├── parser.py # markdown + directive parsing\n │ ├── layout.py # width/height resolution\n │ ├── emit.py # ANSI string generation\n │ ├── style.py # ANSI color/style helpers\n │ ├── blocks.py # Block, InlineSpan dataclasses\n │ ├── renderers/\n │ │ ├── __init__.py\n │ │ ├── panel.py # panel + callout\n │ │ ├── columns.py # columns + col\n │ │ ├── tree.py # tree rendering\n │ │ ├── mermaid.py # mermaid dispatch + native per-type renderers\n │ │ ├── code.py # syntax-highlighted code\n │ │ ├── quote.py # blockquote with attribution\n │ │ ├── divider.py # horizontal rule with label\n │ │ └── text.py # paragraphs, headings, lists\n │ └── py.typed # PEP 561 type hint marker\n └── tests/\n ├── test_parser.py\n ├── test_layout.py\n ├── test_emit.py\n └── fixtures/ # .md input + expected output pairs\n\nUsing src/ layout (PEP 517) for clean packaging. Each renderer is a separate module for isolation — they can be tested independently.",
|
|
366
366
|
"agentNotes": "The renderer-per-file structure means adding a new directive type is: add a file in renderers/, add the BlockType enum value, register it in the emit dispatcher. No existing code needs to change.",
|
|
367
367
|
"status": "draft",
|
|
368
368
|
"userNotes": "",
|
|
@@ -374,8 +374,8 @@
|
|
|
374
374
|
"id": "DES-019",
|
|
375
375
|
"title": "Dependencies",
|
|
376
376
|
"description": "Runtime and dev dependencies",
|
|
377
|
-
"content": "Runtime dependencies:\n\n Dependency Purpose Size\n ────────── ─────── ────\n mistune >=3.0 Markdown parsing ~2k lines\n mermaid
|
|
378
|
-
"agentNotes": "Three deps is lean for what this does. Pygments is the heaviest but it's already installed in most Python environments (IPython, Jupyter, Sphinx all depend on it).
|
|
377
|
+
"content": "Runtime dependencies:\n\n Dependency Purpose Size\n ────────── ─────── ────\n mistune >=3.0 Markdown parsing ~2k lines\n grandalf >=0.8 Graph layout (mermaid) small, pure Python\n pygments Syntax highlighting large but ubiquitous\n\nDev dependencies:\n\n pytest Testing\n ruff Linting + formatting\n mypy Type checking\n build Package building\n\nTotal runtime deps: 3. All pure Python, no native extensions, no subprocess calls.\n\npyproject.toml:\n\n [project]\n name = \"termrender\"\n requires-python = \">=3.10\"\n dependencies = [\n \"mistune>=3.0\",\n \"pygments>=2.0\",\n \"grandalf>=0.8\",\n ]\n\n [project.scripts]\n termrender = \"termrender.__main__:main\"\n\n [build-system]\n requires = [\"hatchling\"]\n build-backend = \"hatchling.build\"",
|
|
378
|
+
"agentNotes": "Three deps is lean for what this does. Pygments is the heaviest but it's already installed in most Python environments (IPython, Jupyter, Sphinx all depend on it). grandalf is a small pure-Python graph layout library used only by the mermaid flowchart-model renderers (flow/class/state/ER).",
|
|
379
379
|
"status": "draft",
|
|
380
380
|
"userNotes": "",
|
|
381
381
|
"reviewAction": null,
|
|
@@ -37,12 +37,18 @@ Homepage = "https://github.com/CaptainCrouton89/termrender"
|
|
|
37
37
|
Repository = "https://github.com/CaptainCrouton89/termrender"
|
|
38
38
|
Issues = "https://github.com/CaptainCrouton89/termrender/issues"
|
|
39
39
|
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = ["pytest>=9.0"]
|
|
42
|
+
|
|
40
43
|
[project.scripts]
|
|
41
44
|
termrender = "termrender.__main__:main"
|
|
42
45
|
|
|
43
46
|
[tool.hatch.version]
|
|
44
47
|
source = "vcs"
|
|
45
48
|
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
pythonpath = ["src"]
|
|
51
|
+
|
|
46
52
|
[tool.hatch.build.targets.wheel]
|
|
47
53
|
packages = ["src/termrender"]
|
|
48
54
|
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"title": "Mermaid code blocks",
|
|
113
113
|
"ears": {
|
|
114
114
|
"when": "When the input contains a fenced code block with language 'mermaid'",
|
|
115
|
-
"shall": "the system shall parse the mermaid syntax and render it as an ASCII/Unicode flowchart inline with surrounding content, using
|
|
115
|
+
"shall": "the system shall parse the mermaid syntax and render it as an ASCII/Unicode flowchart inline with surrounding content, using termrender's own native Python parsers (no external mermaid rendering dependency)"
|
|
116
116
|
},
|
|
117
117
|
"criteria": [
|
|
118
118
|
{
|
|
@@ -136,12 +136,12 @@
|
|
|
136
136
|
"checked": false
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
|
-
"text": "
|
|
139
|
+
"text": "diagrams are parsed and rendered in-process by native Python parsers, with no subprocess or vendored binary dependency",
|
|
140
140
|
"checked": false
|
|
141
141
|
}
|
|
142
142
|
],
|
|
143
143
|
"status": "draft",
|
|
144
|
-
"agentNotes": "
|
|
144
|
+
"agentNotes": "Each mermaid diagram type has its own native Python parser in renderers/ (mermaid_flow_parser.py, mermaid_class.py, mermaid_state.py, mermaid_er.py, plus dedicated pie/gantt/sequence/mindmap/journey/timeline modules); the flowchart/class/state/ER parsers share one grandalf-based Sugiyama layout engine via a common FlowGraph model.",
|
|
145
145
|
"userNotes": "User: 'We're going to make this a python package, so it's okay if it has python deps'",
|
|
146
146
|
"questions": [],
|
|
147
147
|
"startedAt": "2026-04-05T03:25:14.714Z",
|
|
@@ -479,7 +479,7 @@
|
|
|
479
479
|
"title": "Standalone Python package",
|
|
480
480
|
"ears": {
|
|
481
481
|
"while": "While the package is distributed",
|
|
482
|
-
"shall": "the system shall be a standalone Python package on PyPI, installable independently of sisyphus, with
|
|
482
|
+
"shall": "the system shall be a standalone Python package on PyPI, installable independently of sisyphus, with mistune (markdown) and grandalf (graph layout for native mermaid rendering) as its primary dependencies"
|
|
483
483
|
},
|
|
484
484
|
"criteria": [
|
|
485
485
|
{
|
|
@@ -491,7 +491,7 @@
|
|
|
491
491
|
"checked": false
|
|
492
492
|
},
|
|
493
493
|
{
|
|
494
|
-
"text": "Dependencies:
|
|
494
|
+
"text": "Dependencies: mistune (markdown parsing), grandalf (graph layout), pygments (syntax highlighting)",
|
|
495
495
|
"checked": false
|
|
496
496
|
},
|
|
497
497
|
{
|
|
@@ -500,7 +500,7 @@
|
|
|
500
500
|
}
|
|
501
501
|
],
|
|
502
502
|
"status": "draft",
|
|
503
|
-
"agentNotes": "
|
|
503
|
+
"agentNotes": "Python package on PyPI. Mermaid diagrams render via termrender's own native parsers plus grandalf for graph layout — no external mermaid rendering dependency.",
|
|
504
504
|
"userNotes": "User: 'It can be python — it doesn't need to be ts'",
|
|
505
505
|
"questions": [
|
|
506
506
|
{
|
|
@@ -53,7 +53,7 @@ Layout (`layout.py::resolve_height`, `BlockType.MERMAID` branch) precomputes `bl
|
|
|
53
53
|
|
|
54
54
|
## `mermaid_pie.py` — re-expressed as a bar chart, not a text circle
|
|
55
55
|
|
|
56
|
-
A text pie chart is worse than a labeled bar chart at the same information density (per the ratified plan), so `parse_pie` extracts `(title, items)` from the `pie [showData]` / `title …` / `"label" : value` grammar and `render` hands the items to `charts.py::render_bar` via a synthetic `Block(type=BlockType.BAR, ...)` — no duplicated bar-drawing logic. Each item's `unit` attr is set to the pre-formatted percentage string (e.g. `" (40.0%)"`) so `render_bar`'s existing value-formatting appends it for free. Called with `color=False` always — mermaid output is monochrome by design (the attach viewer relies on `--color on == --color off`). Unparseable/empty input degrades to the raw source lines, same contract
|
|
56
|
+
A text pie chart is worse than a labeled bar chart at the same information density (per the ratified plan), so `parse_pie` extracts `(title, items)` from the `pie [showData]` / `title …` / `"label" : value` grammar and `render` hands the items to `charts.py::render_bar` via a synthetic `Block(type=BlockType.BAR, ...)` — no duplicated bar-drawing logic. Each item's `unit` attr is set to the pre-formatted percentage string (e.g. `" (40.0%)"`) so `render_bar`'s existing value-formatting appends it for free. Called with `color=False` always — mermaid output is monochrome by design (the attach viewer relies on `--color on == --color off`). Unparseable/empty input degrades to the raw source lines, same degradation contract every other mermaid renderer in this package upholds.
|
|
57
57
|
|
|
58
58
|
## `mermaid_gantt.py` — dates resolved via a running `last_end` cursor, not a strict grammar
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ A text pie chart is worse than a labeled bar chart at the same information densi
|
|
|
65
65
|
|
|
66
66
|
## `mermaid_sequence.py` — deterministic column layout, no graph-layout search
|
|
67
67
|
|
|
68
|
-
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
|
|
68
|
+
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/class/state/ER renderers. 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.).
|
|
69
69
|
|
|
70
70
|
## `mermaid_mindmap.py` — mindmap source is already `tree.py`'s shape
|
|
71
71
|
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
5
|
from termrender.blocks import Block
|
|
8
6
|
from termrender.renderers import (
|
|
9
7
|
mermaid_class,
|
|
@@ -17,18 +15,10 @@ from termrender.renderers import (
|
|
|
17
15
|
mermaid_state,
|
|
18
16
|
mermaid_timeline,
|
|
19
17
|
)
|
|
18
|
+
from termrender.renderers.mermaid_degradation import raw_echo
|
|
20
19
|
from termrender.renderers.mermaid_prelude import strip_prelude_lines
|
|
21
20
|
from termrender.style import visual_ljust
|
|
22
21
|
|
|
23
|
-
# Same box-drawing/geometric glyph ranges the native renderers (mermaid_flow,
|
|
24
|
-
# mermaid_class, mermaid_state, mermaid_er) guard their own raw-echo paths
|
|
25
|
-
# with, and the same ranges the downstream crouter viewer keys on to detect
|
|
26
|
-
# render *success*. An exotic/unrecognized diagram type has no renderer at
|
|
27
|
-
# all, so its degradation lives here rather than in a shared renderer module;
|
|
28
|
-
# it must uphold the identical contract — no such glyph survives the echo,
|
|
29
|
-
# even one present verbatim in the source itself.
|
|
30
|
-
_GLYPH_RANGE_RE = re.compile("[\u2500-\u259f\u25a0-\u25ff]")
|
|
31
|
-
|
|
32
22
|
|
|
33
23
|
def _first_line_type(source: str) -> str:
|
|
34
24
|
"""Return the lowercased first real line's leading keyword.
|
|
@@ -52,21 +42,6 @@ def _stripped(source: str) -> str:
|
|
|
52
42
|
return "\n".join(strip_prelude_lines(source.splitlines()))
|
|
53
43
|
|
|
54
44
|
|
|
55
|
-
def _raw_echo(source: str) -> list[str]:
|
|
56
|
-
"""Degrade an exotic/unrecognized diagram type to its raw source lines.
|
|
57
|
-
|
|
58
|
-
No renderer exists for these types (sankey, C4, gitgraph, block, packet,
|
|
59
|
-
kanban, quadrantChart, …, and anything unrecognized): the source is
|
|
60
|
-
echoed back verbatim, with box-drawing/geometric glyphs neutralized so
|
|
61
|
-
the crouter viewer's "no glyphs survived" success check can't be fooled
|
|
62
|
-
by one present in the raw source itself — the same guarantee every
|
|
63
|
-
native renderer's own raw-echo path upholds.
|
|
64
|
-
"""
|
|
65
|
-
return [
|
|
66
|
-
_GLYPH_RANGE_RE.sub("?", line.rstrip()) for line in source.splitlines()
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
|
|
70
45
|
def render_mermaid_lines(source: str, width: int) -> list[str]:
|
|
71
46
|
"""Dispatch a mermaid source to its type's renderer and return raw lines.
|
|
72
47
|
|
|
@@ -76,8 +51,9 @@ def render_mermaid_lines(source: str, width: int) -> list[str]:
|
|
|
76
51
|
``classDiagram``, ``stateDiagram``/``stateDiagram-v2``, and ``erDiagram``.
|
|
77
52
|
Any other type (sankey, C4, gitgraph, block, packet, kanban, …, or
|
|
78
53
|
anything unrecognized) degrades to a raw echo of the source with no
|
|
79
|
-
box-drawing glyphs — see :func
|
|
80
|
-
callers apply width padding
|
|
54
|
+
box-drawing glyphs — see :func:`~termrender.renderers.mermaid_degradation.
|
|
55
|
+
raw_echo`. Lines are returned unpadded; callers apply width padding
|
|
56
|
+
uniformly.
|
|
81
57
|
"""
|
|
82
58
|
diagram_type = _first_line_type(source)
|
|
83
59
|
if diagram_type.startswith("pie"):
|
|
@@ -100,7 +76,7 @@ def render_mermaid_lines(source: str, width: int) -> list[str]:
|
|
|
100
76
|
return mermaid_state.render_state(source, width)
|
|
101
77
|
if diagram_type.startswith("erdiagram"):
|
|
102
78
|
return mermaid_er.render_er(source, width)
|
|
103
|
-
return
|
|
79
|
+
return raw_echo(source)
|
|
104
80
|
|
|
105
81
|
|
|
106
82
|
def render(block: Block, color: bool) -> list[str]:
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
"""Native renderer for mermaid ``classDiagram`` sources.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
``mermaid_sequence.py`` and ``mermaid_flow.py``.
|
|
3
|
+
Wired into ``mermaid.py``'s dispatcher: exposes a single pure function,
|
|
4
|
+
:func:`render_class`. Own parser, own tests, no dependency on ``Block``
|
|
5
|
+
or the render pipeline, matching the shape of ``mermaid_sequence.py``
|
|
6
|
+
and ``mermaid_flow.py``.
|
|
8
7
|
|
|
9
8
|
This module does no layout or rasterization of its own. It parses mermaid
|
|
10
9
|
``classDiagram`` source into the *same* :class:`~termrender.renderers.
|
|
@@ -53,7 +52,8 @@ Grammar supported
|
|
|
53
52
|
centers this single combined label on the edge's longest straight run;
|
|
54
53
|
the UML convention of a cardinality glyph pinned at each endpoint
|
|
55
54
|
separately isn't supported (see *Known degradations*).
|
|
56
|
-
- ``%%`` comments
|
|
55
|
+
- ``%%`` comments and well-formed presentational directives (``classDef``/
|
|
56
|
+
``style``/``cssClass``/``accTitle``/``accDescr``) are dropped.
|
|
57
57
|
|
|
58
58
|
Never crashes: any unparseable input (missing header, or a parse/layout
|
|
59
59
|
exception) degrades to a **raw echo** of the source lines with no
|
|
@@ -68,8 +68,9 @@ Known degradations (by design, not bugs)
|
|
|
68
68
|
(per-endpoint cardinality glyphs, common in dedicated UML tools) — the
|
|
69
69
|
underlying engine places one label per edge.
|
|
70
70
|
- ``note`` statements, `<<...>>` generic constraints beyond the ``~T~``
|
|
71
|
-
substitution, and class-diagram namespaces are not recognized
|
|
72
|
-
|
|
71
|
+
substitution, and class-diagram namespaces are not recognized; they
|
|
72
|
+
raise ``ClassDiagramError`` and the public renderer raw-echoes instead
|
|
73
|
+
of half-rendering the diagram.
|
|
73
74
|
- See ``mermaid_flow_layout.py``'s docstring for the inherited engine
|
|
74
75
|
degradations (dense-graph crossings, CJK wrap width, minimum-box-size
|
|
75
76
|
cosmetics) — this renderer's output goes through the same rasterizer and
|
|
@@ -80,6 +81,7 @@ from __future__ import annotations
|
|
|
80
81
|
|
|
81
82
|
import re
|
|
82
83
|
|
|
84
|
+
from termrender.renderers.mermaid_degradation import raw_echo
|
|
83
85
|
from termrender.renderers.mermaid_flow_layout import layout_flowgraph
|
|
84
86
|
from termrender.renderers.mermaid_flow_model import (
|
|
85
87
|
Direction,
|
|
@@ -105,6 +107,21 @@ class ClassDiagramError(Exception):
|
|
|
105
107
|
_HEADER_RE = re.compile(r"^classDiagram(?:-v2)?\b", re.IGNORECASE)
|
|
106
108
|
_DIRECTION_RE = re.compile(r"^direction\s+(TB|TD|LR|RL|BT)\b", re.IGNORECASE)
|
|
107
109
|
_COMMENT_RE = re.compile(r"^%%")
|
|
110
|
+
_CLASSDEF_RE = re.compile(r"^classDef\b\s+\S+\s+\S.*$", re.IGNORECASE)
|
|
111
|
+
_STYLE_RE = re.compile(r"^style\b\s+\S+\s+\S.*$", re.IGNORECASE)
|
|
112
|
+
_CSSCLASS_RE = re.compile(
|
|
113
|
+
r'^cssClass\b\s+(?:"[^"]+"|\S+)(?:\s*,\s*(?:"[^"]+"|\S+))*\s+\S+\s*$',
|
|
114
|
+
re.IGNORECASE,
|
|
115
|
+
)
|
|
116
|
+
_ACCTITLE_RE = re.compile(r"^accTitle\b\s*:?\s+\S.*$", re.IGNORECASE)
|
|
117
|
+
_ACCDESCR_RE = re.compile(r"^accDescr\b\s*:?\s+\S.*$", re.IGNORECASE)
|
|
118
|
+
_CLASS_ASSIGN_RE = re.compile(
|
|
119
|
+
r"^class\b\s+(?:\S+(?:\s*,\s*\S+)*)\s+(?!\{)\S+\s*$", re.IGNORECASE
|
|
120
|
+
)
|
|
121
|
+
_CLASS_SHORTHAND_RE = re.compile(r"^class\b\s+\S+:::\S+\s*$", re.IGNORECASE)
|
|
122
|
+
_BODY_DIRECTIVE_RE = re.compile(
|
|
123
|
+
r"^(?:classDef|class|style|cssClass|accTitle|accDescr)\b", re.IGNORECASE
|
|
124
|
+
)
|
|
108
125
|
|
|
109
126
|
_CLASS_OPEN_RE = re.compile(r"^class\s+(\S+)\s*\{(.*)$")
|
|
110
127
|
_CLASS_BARE_RE = re.compile(r"^class\s+(\S+)\s*$")
|
|
@@ -124,8 +141,6 @@ _REL_RIGHT_RE = re.compile(
|
|
|
124
141
|
r'^\s*(?:"(?P<card>[^"]*)")?\s*(?P<id>[^\s":]+)\s*(?::\s*(?P<label>.*))?$'
|
|
125
142
|
)
|
|
126
143
|
|
|
127
|
-
_GLYPH_RANGE_RE = re.compile("[\u2500-\u259f\u25a0-\u25ff]")
|
|
128
|
-
|
|
129
144
|
|
|
130
145
|
class _ClassDef:
|
|
131
146
|
"""Mutable parse-time record for one UML class — converted to a
|
|
@@ -173,6 +188,31 @@ def _get_or_create(classes: dict[str, _ClassDef], token: str) -> _ClassDef:
|
|
|
173
188
|
return cls
|
|
174
189
|
|
|
175
190
|
|
|
191
|
+
def _is_presentational_line(line: str) -> bool:
|
|
192
|
+
return any(
|
|
193
|
+
regex.match(line)
|
|
194
|
+
for regex in (
|
|
195
|
+
_CLASSDEF_RE,
|
|
196
|
+
_STYLE_RE,
|
|
197
|
+
_CSSCLASS_RE,
|
|
198
|
+
_ACCTITLE_RE,
|
|
199
|
+
_ACCDESCR_RE,
|
|
200
|
+
_CLASS_ASSIGN_RE,
|
|
201
|
+
_CLASS_SHORTHAND_RE,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _consume_block_text(cls: _ClassDef, text: str) -> None:
|
|
207
|
+
for part in text.split(";"):
|
|
208
|
+
part = part.strip()
|
|
209
|
+
if part and _BODY_DIRECTIVE_RE.match(part):
|
|
210
|
+
raise ClassDiagramError(
|
|
211
|
+
f"unrecognized class diagram statement: {part!r}"
|
|
212
|
+
)
|
|
213
|
+
_consume_members(cls, text)
|
|
214
|
+
|
|
215
|
+
|
|
176
216
|
def _consume_members(cls: _ClassDef, text: str) -> None:
|
|
177
217
|
"""Split ``text`` (one physical line, or the inline body of a
|
|
178
218
|
``class Name { ... }`` opener/closer) on ``;`` into individual member/
|
|
@@ -271,11 +311,15 @@ def _build_graph(source: str) -> FlowGraph:
|
|
|
271
311
|
continue
|
|
272
312
|
|
|
273
313
|
if block_cls is not None:
|
|
314
|
+
if _COMMENT_RE.match(line):
|
|
315
|
+
continue
|
|
274
316
|
if line.endswith("}"):
|
|
275
|
-
|
|
317
|
+
body = line[:-1].rstrip()
|
|
318
|
+
if body:
|
|
319
|
+
_consume_block_text(block_cls, body)
|
|
276
320
|
block_cls = None
|
|
277
321
|
else:
|
|
278
|
-
|
|
322
|
+
_consume_block_text(block_cls, line)
|
|
279
323
|
continue
|
|
280
324
|
|
|
281
325
|
if not seen_header:
|
|
@@ -283,7 +327,7 @@ def _build_graph(source: str) -> FlowGraph:
|
|
|
283
327
|
seen_header = True
|
|
284
328
|
continue
|
|
285
329
|
|
|
286
|
-
if _COMMENT_RE.match(line):
|
|
330
|
+
if _COMMENT_RE.match(line) or _is_presentational_line(line):
|
|
287
331
|
continue
|
|
288
332
|
|
|
289
333
|
dm = _DIRECTION_RE.match(line)
|
|
@@ -299,9 +343,9 @@ def _build_graph(source: str) -> FlowGraph:
|
|
|
299
343
|
cls.has_block = True
|
|
300
344
|
body = inline_body.rstrip()
|
|
301
345
|
if body.endswith("}"):
|
|
302
|
-
|
|
346
|
+
_consume_block_text(cls, body[:-1])
|
|
303
347
|
else:
|
|
304
|
-
|
|
348
|
+
_consume_block_text(cls, body)
|
|
305
349
|
block_cls = cls
|
|
306
350
|
continue
|
|
307
351
|
|
|
@@ -326,7 +370,13 @@ def _build_graph(source: str) -> FlowGraph:
|
|
|
326
370
|
_consume_members(cls, mm.group("member"))
|
|
327
371
|
continue
|
|
328
372
|
|
|
329
|
-
|
|
373
|
+
if _CLASS_ASSIGN_RE.match(line) or _CLASS_SHORTHAND_RE.match(line):
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
raise ClassDiagramError(f"unrecognized class diagram statement: {line!r}")
|
|
377
|
+
|
|
378
|
+
if block_cls is not None:
|
|
379
|
+
raise ClassDiagramError("unterminated class body")
|
|
330
380
|
|
|
331
381
|
if not seen_header:
|
|
332
382
|
raise ClassDiagramError(
|
|
@@ -354,12 +404,6 @@ def _build_graph(source: str) -> FlowGraph:
|
|
|
354
404
|
return FlowGraph(direction=direction, nodes=nodes, edges=edges, subgraphs=[])
|
|
355
405
|
|
|
356
406
|
|
|
357
|
-
def _raw_echo(source: str) -> list[str]:
|
|
358
|
-
return [
|
|
359
|
-
_GLYPH_RANGE_RE.sub("?", line.rstrip()) for line in source.splitlines()
|
|
360
|
-
]
|
|
361
|
-
|
|
362
|
-
|
|
363
407
|
def render_class(source: str, width: int) -> list[str]:
|
|
364
408
|
"""Render a mermaid ``classDiagram`` source to unicode lines.
|
|
365
409
|
|
|
@@ -381,17 +425,17 @@ def render_class(source: str, width: int) -> list[str]:
|
|
|
381
425
|
try:
|
|
382
426
|
graph = _build_graph(source)
|
|
383
427
|
except ClassDiagramError:
|
|
384
|
-
return
|
|
428
|
+
return raw_echo(source)
|
|
385
429
|
|
|
386
430
|
if not graph.nodes:
|
|
387
|
-
return
|
|
431
|
+
return raw_echo(source)
|
|
388
432
|
|
|
389
433
|
try:
|
|
390
434
|
lines = layout_flowgraph(graph, width)
|
|
391
435
|
except Exception:
|
|
392
|
-
return
|
|
436
|
+
return raw_echo(source)
|
|
393
437
|
|
|
394
438
|
if not lines:
|
|
395
|
-
return
|
|
439
|
+
return raw_echo(source)
|
|
396
440
|
|
|
397
441
|
return lines
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Shared raw-echo degradation contract for the native mermaid renderers.
|
|
2
|
+
|
|
3
|
+
Every native mermaid renderer in this package (``mermaid.py``'s exotic-type
|
|
4
|
+
fallback, and the flowchart-model-backed ``mermaid_flow.py``,
|
|
5
|
+
``mermaid_class.py``, ``mermaid_state.py``, ``mermaid_er.py``) must degrade
|
|
6
|
+
identically on any input it cannot fully render under its supported
|
|
7
|
+
grammar: echo the source lines verbatim (``.rstrip()``-ed), with every
|
|
8
|
+
box-drawing/geometric glyph replaced by ``?``. The downstream crouter
|
|
9
|
+
attach viewer keys on the *presence* of one of these glyphs to decide
|
|
10
|
+
whether a native render succeeded and the original mermaid fence can be
|
|
11
|
+
dropped — so the echo path must never contain one, even when malformed or
|
|
12
|
+
degenerate source happens to contain a literal glyph of its own (e.g. a
|
|
13
|
+
hand-typed ``"\u250c"`` in otherwise-non-mermaid text).
|
|
14
|
+
|
|
15
|
+
:func:`raw_echo` is the one shared implementation of that contract; every
|
|
16
|
+
renderer module imports it rather than keeping its own copy of the glyph
|
|
17
|
+
regex and echo function.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
__all__ = ["GLYPH_RANGE_RE", "raw_echo"]
|
|
25
|
+
|
|
26
|
+
GLYPH_RANGE_RE = re.compile("[\u2500-\u259f\u25a0-\u25ff]")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def raw_echo(source: str) -> list[str]:
|
|
30
|
+
"""Echo ``source``'s lines verbatim, minus trailing whitespace, with
|
|
31
|
+
every box-drawing/geometric glyph (:data:`GLYPH_RANGE_RE`) replaced by
|
|
32
|
+
``?`` so the echo can never be mistaken for a successful native
|
|
33
|
+
render."""
|
|
34
|
+
return [GLYPH_RANGE_RE.sub("?", line.rstrip()) for line in source.splitlines()]
|