termrender 0.7.3__tar.gz → 0.8.0__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 (47) hide show
  1. {termrender-0.7.3 → termrender-0.8.0}/CHANGELOG.md +14 -0
  2. {termrender-0.7.3 → termrender-0.8.0}/PKG-INFO +1 -1
  3. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/CLAUDE.md +3 -1
  4. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/layout.py +2 -2
  5. termrender-0.8.0/src/termrender/renderers/mermaid.py +98 -0
  6. termrender-0.8.0/tests/test_mermaid_compat.py +115 -0
  7. termrender-0.7.3/src/termrender/renderers/mermaid.py +0 -48
  8. {termrender-0.7.3 → termrender-0.8.0}/.github/workflows/publish.yml +0 -0
  9. {termrender-0.7.3 → termrender-0.8.0}/.gitignore +0 -0
  10. {termrender-0.7.3 → termrender-0.8.0}/CLAUDE.md +0 -0
  11. {termrender-0.7.3 → termrender-0.8.0}/LICENSE +0 -0
  12. {termrender-0.7.3 → termrender-0.8.0}/README.md +0 -0
  13. {termrender-0.7.3 → termrender-0.8.0}/design.json +0 -0
  14. {termrender-0.7.3 → termrender-0.8.0}/pyproject.toml +0 -0
  15. {termrender-0.7.3 → termrender-0.8.0}/requirements.json +0 -0
  16. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/__init__.py +0 -0
  17. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/__main__.py +0 -0
  18. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/blocks.py +0 -0
  19. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/emit.py +0 -0
  20. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/parser.py +0 -0
  21. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/py.typed +0 -0
  22. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/CLAUDE.md +0 -0
  23. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/__init__.py +0 -0
  24. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/borders.py +0 -0
  25. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/charts.py +0 -0
  26. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/code.py +0 -0
  27. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/columns.py +0 -0
  28. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/diff.py +0 -0
  29. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/divider.py +0 -0
  30. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/panel.py +0 -0
  31. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/quote.py +0 -0
  32. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/stat.py +0 -0
  33. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/table.py +0 -0
  34. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/text.py +0 -0
  35. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/timeline.py +0 -0
  36. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/renderers/tree.py +0 -0
  37. {termrender-0.7.3 → termrender-0.8.0}/src/termrender/style.py +0 -0
  38. {termrender-0.7.3 → termrender-0.8.0}/tests/__init__.py +0 -0
  39. {termrender-0.7.3 → termrender-0.8.0}/tests/test_charts.py +0 -0
  40. {termrender-0.7.3 → termrender-0.8.0}/tests/test_column_alignment.py +0 -0
  41. {termrender-0.7.3 → termrender-0.8.0}/tests/test_diff.py +0 -0
  42. {termrender-0.7.3 → termrender-0.8.0}/tests/test_inline_badge.py +0 -0
  43. {termrender-0.7.3 → termrender-0.8.0}/tests/test_myst_gaps.py +0 -0
  44. {termrender-0.7.3 → termrender-0.8.0}/tests/test_stat.py +0 -0
  45. {termrender-0.7.3 → termrender-0.8.0}/tests/test_tasklist.py +0 -0
  46. {termrender-0.7.3 → termrender-0.8.0}/tests/test_timeline.py +0 -0
  47. {termrender-0.7.3 → termrender-0.8.0}/tests/test_variable_colons.py +0 -0
@@ -1,6 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v0.8.0 (2026-04-18)
5
+
6
+ ### Features
7
+
8
+ - **mermaid**: Preprocess sequence diagrams for mermaid-ascii compatibility
9
+ ([`a642576`](https://github.com/CaptainCrouton89/termrender/commit/a642576d41d5dbde372d7de2ab47745296a78e32))
10
+
11
+ mermaid-ascii only parses ->> / -->> arrows, participants, and self-loops; every other common
12
+ sequence-diagram construct made it fail and fall back to raw source. Rewrite Note lines into
13
+ self-loops, map -> / -x / --x / -) / --) / -- > onto the supported arrow pair, drop block keywords
14
+ (loop/alt/activate/ autonumber/end/…), and flatten <br/> to ' / '. Non-sequence diagrams pass
15
+ through unchanged.
16
+
17
+
4
18
  ## v0.7.3 (2026-04-15)
5
19
 
6
20
  ### Bug Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: termrender
3
- Version: 0.7.3
3
+ Version: 0.8.0
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
@@ -8,7 +8,9 @@
8
8
 
9
9
  `layout.py:119–134` runs `mermaid-ascii` and caches the result in `block.attrs["_rendered"]`. The `mermaid` renderer (renderers/mermaid.py) reads this cache key; if it's absent, it runs the subprocess again. A failed layout subprocess (tool missing, timeout) silently stores the raw source diagram in `_rendered`, so the renderer falls back to printing source — no error is raised. Both sites have a 30s timeout.
10
10
 
11
- `layout.py` imports `fix_mermaid_encoding` from `renderers/mermaid.py` — the only reverse dependency from layout into renderers. Reorganizing `renderers/` must account for this import. The two subprocess call sites differ: layout uses `check=True` (non-zero exit raises `CalledProcessError` → caught → raw source fallback); the renderer omits `check` (non-zero exit silently reads `stdout`, which may be empty or partial). See `renderers/CLAUDE.md` for encoding-fix details.
11
+ `layout.py` imports `fix_mermaid_encoding` and `preprocess_mermaid_for_ascii` from `renderers/mermaid.py` — the only reverse dependency from layout into renderers. Reorganizing `renderers/` must account for these imports. The two subprocess call sites differ: layout uses `check=True` (non-zero exit raises `CalledProcessError` → caught → raw source fallback); the renderer omits `check` (non-zero exit silently reads `stdout`, which may be empty or partial). See `renderers/CLAUDE.md` for encoding-fix details.
12
+
13
+ `preprocess_mermaid_for_ascii` rewrites sequence diagrams into the subset `mermaid-ascii` parses (it only supports `->>` / `-->>` arrows, `participant`, and self-loops). `Note over|left of|right of X[,Y]: msg` becomes a self-loop `X->>X: 📝 msg`; `->`, `-x`, `--x`, `-)`, `--)`, and bare `-->` are mapped to `->>`/`-->>`; block keywords (`loop`/`alt`/`opt`/`par`/`critical`/`break`/`rect`/`activate`/`deactivate`/`autonumber`/`else`/`and`/`end`) are dropped so the inner arrow lines still render; `<br/>` is flattened to ` / `. Non-sequence diagrams (`flowchart`, `graph`, etc.) pass through unchanged. Semantics are lossy by design — `-x` (fail arrow) renders as a plain arrow, and block scoping is lost — but the flow diagram renders instead of silently degrading to raw source.
12
14
 
13
15
  ## Directive nesting: outer must have more colons than inner
14
16
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import subprocess
6
6
 
7
7
  from termrender.blocks import Block, BlockType
8
- from termrender.renderers.mermaid import fix_mermaid_encoding
8
+ from termrender.renderers.mermaid import fix_mermaid_encoding, preprocess_mermaid_for_ascii
9
9
  from termrender.style import wrap_text, visual_len
10
10
 
11
11
 
@@ -148,7 +148,7 @@ def resolve_height(block: Block) -> None:
148
148
  try:
149
149
  result = subprocess.run(
150
150
  ["mermaid-ascii", "-f", "-", "-w", str(block.width or 80), "-y", "1"],
151
- input=source,
151
+ input=preprocess_mermaid_for_ascii(source),
152
152
  capture_output=True,
153
153
  text=True,
154
154
  check=True,
@@ -0,0 +1,98 @@
1
+ """Mermaid diagram renderer for termrender."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import subprocess
7
+
8
+ from termrender.blocks import Block
9
+ from termrender.style import visual_ljust
10
+
11
+
12
+ def fix_mermaid_encoding(text: str) -> str:
13
+ """Undo mermaid-ascii's double-encoding of UTF-8 characters.
14
+
15
+ mermaid-ascii misinterprets UTF-8 input bytes as Latin-1 and re-encodes
16
+ to UTF-8, corrupting multi-byte characters (e.g. → becomes â\\x86\\x92).
17
+ Reversing the process: encode back to Latin-1 to recover the original
18
+ UTF-8 bytes, then decode as UTF-8.
19
+ """
20
+ try:
21
+ return text.encode("latin-1").decode("utf-8")
22
+ except (UnicodeDecodeError, UnicodeEncodeError):
23
+ return text
24
+
25
+
26
+ _NOTE_RE = re.compile(
27
+ r"^(\s*)[Nn]ote\s+(?:over|left\s+of|right\s+of)\s+([^:]+?)\s*:\s*(.*)$"
28
+ )
29
+ _BR_RE = re.compile(r"<br\s*/?>", re.IGNORECASE)
30
+ _UNSUPPORTED_BLOCK_RE = re.compile(
31
+ r"^\s*(?:loop|alt|else|opt|par|and|critical|option|break|rect|"
32
+ r"activate|deactivate|autonumber|end)\b.*$",
33
+ re.IGNORECASE,
34
+ )
35
+
36
+
37
+ def preprocess_mermaid_for_ascii(source: str) -> str:
38
+ """Rewrite mermaid sequence diagrams into the subset mermaid-ascii supports.
39
+
40
+ mermaid-ascii only understands ``->>`` and ``-->>`` arrows plus ``participant``
41
+ declarations. This helper converts ``Note`` lines into self-loops, maps the
42
+ other arrow variants (``->``, ``-x``, ``--x``, ``-)``, ``--)``, ``-->``) to
43
+ the supported pair, drops block keywords (``loop``, ``alt``, ``activate``…),
44
+ and flattens ``<br/>`` tags. Non-sequence diagrams are returned unchanged.
45
+ """
46
+ lines = source.splitlines()
47
+ first = next((l.strip() for l in lines if l.strip()), "")
48
+ if not first.lower().startswith("sequencediagram"):
49
+ return source
50
+
51
+ out: list[str] = []
52
+ for line in lines:
53
+ m = _NOTE_RE.match(line)
54
+ if m:
55
+ indent, parts, msg = m.group(1), m.group(2), m.group(3)
56
+ first_p = parts.split(",")[0].strip()
57
+ msg = _BR_RE.sub(" / ", msg)
58
+ out.append(f"{indent}{first_p}->>{first_p}: 📝 {msg}")
59
+ continue
60
+
61
+ if _UNSUPPORTED_BLOCK_RE.match(line):
62
+ continue
63
+
64
+ line = _BR_RE.sub(" / ", line)
65
+ line = re.sub(r"--x(?=\s|\w|\()", "-->>", line)
66
+ line = re.sub(r"-x(?=\s|\w|\()", "->>", line)
67
+ line = re.sub(r"--\)(?=\s|\w|\()", "-->>", line)
68
+ line = re.sub(r"-\)(?=\s|\w|\()", "->>", line)
69
+ line = re.sub(r"-->(?!>)", "-->>", line)
70
+ line = re.sub(r"(?<!-)->(?!>)", "->>", line)
71
+ out.append(line)
72
+ return "\n".join(out)
73
+
74
+
75
+ def render(block: Block, color: bool) -> list[str]:
76
+ """Render a mermaid diagram from pre-rendered or on-the-fly ASCII output."""
77
+ w = block.width
78
+ rendered = block.attrs.get("_rendered")
79
+
80
+ if rendered is None:
81
+ source = block.attrs.get("source", "")
82
+ try:
83
+ result = subprocess.run(
84
+ ["mermaid-ascii", "-f", "-", "-w", str(block.width or 80), "-y", "1"],
85
+ input=preprocess_mermaid_for_ascii(source),
86
+ capture_output=True,
87
+ text=True,
88
+ timeout=30,
89
+ )
90
+ rendered = fix_mermaid_encoding(result.stdout)
91
+ except Exception:
92
+ rendered = source
93
+
94
+ lines: list[str] = []
95
+ for raw_line in rendered.split("\n"):
96
+ lines.append(visual_ljust(raw_line, w))
97
+
98
+ return lines
@@ -0,0 +1,115 @@
1
+ import unittest
2
+
3
+ from termrender.renderers.mermaid import preprocess_mermaid_for_ascii
4
+
5
+
6
+ class TestMermaidPreprocessor(unittest.TestCase):
7
+
8
+ def test_non_sequence_diagrams_pass_through(self):
9
+ src = "flowchart TD\n A-->B\n B-->C"
10
+ self.assertEqual(preprocess_mermaid_for_ascii(src), src)
11
+
12
+ def test_note_over_becomes_self_loop(self):
13
+ src = "sequenceDiagram\n participant A\n participant B\n Note over A: hello"
14
+ out = preprocess_mermaid_for_ascii(src)
15
+ self.assertIn("A->>A: 📝 hello", out)
16
+ self.assertNotIn("Note over", out)
17
+
18
+ def test_note_over_multi_participant_picks_first(self):
19
+ src = "sequenceDiagram\n participant A\n participant B\n Note over A,B: shared"
20
+ out = preprocess_mermaid_for_ascii(src)
21
+ self.assertIn("A->>A: 📝 shared", out)
22
+
23
+ def test_note_left_and_right_of(self):
24
+ src = (
25
+ "sequenceDiagram\n"
26
+ " participant X\n"
27
+ " Note left of X: L\n"
28
+ " Note right of X: R"
29
+ )
30
+ out = preprocess_mermaid_for_ascii(src)
31
+ self.assertIn("X->>X: 📝 L", out)
32
+ self.assertIn("X->>X: 📝 R", out)
33
+
34
+ def test_br_tags_flattened_in_note(self):
35
+ src = "sequenceDiagram\n participant A\n Note over A: line1<br/>line2"
36
+ out = preprocess_mermaid_for_ascii(src)
37
+ self.assertIn("A->>A: 📝 line1 / line2", out)
38
+
39
+ def test_br_tags_flattened_in_arrow_message(self):
40
+ src = "sequenceDiagram\n participant A\n participant B\n A->>B: a<br/>b<br />c"
41
+ out = preprocess_mermaid_for_ascii(src)
42
+ self.assertIn("A->>B: a / b / c", out)
43
+
44
+ def test_arrow_variants_rewritten(self):
45
+ src = (
46
+ "sequenceDiagram\n"
47
+ " participant A\n"
48
+ " participant B\n"
49
+ " A-xB: fail\n"
50
+ " A--xB: dashed fail\n"
51
+ " A-)B: async\n"
52
+ " A--)B: dashed async\n"
53
+ " A->B: single\n"
54
+ " A-->B: dashed single\n"
55
+ )
56
+ out = preprocess_mermaid_for_ascii(src)
57
+ self.assertNotIn("-x", out)
58
+ self.assertNotIn("-)", out)
59
+ # Each single-dash variant should be solid ->>
60
+ self.assertIn("A->>B: fail", out)
61
+ self.assertIn("A->>B: async", out)
62
+ self.assertIn("A->>B: single", out)
63
+ # Each double-dash variant should be dashed -->>
64
+ self.assertIn("A-->>B: dashed fail", out)
65
+ self.assertIn("A-->>B: dashed async", out)
66
+ self.assertIn("A-->>B: dashed single", out)
67
+
68
+ def test_existing_double_arrow_preserved(self):
69
+ src = "sequenceDiagram\n participant A\n participant B\n A->>B: x\n A-->>B: y"
70
+ out = preprocess_mermaid_for_ascii(src)
71
+ self.assertIn("A->>B: x", out)
72
+ self.assertIn("A-->>B: y", out)
73
+ # Should not have inflated to ->>>
74
+ self.assertNotIn("->>>", out)
75
+ self.assertNotIn("-->>>", out)
76
+
77
+ def test_block_keywords_dropped(self):
78
+ src = (
79
+ "sequenceDiagram\n"
80
+ " participant A\n"
81
+ " participant B\n"
82
+ " activate A\n"
83
+ " loop forever\n"
84
+ " A->>B: x\n"
85
+ " end\n"
86
+ " deactivate A\n"
87
+ " autonumber\n"
88
+ " alt happy\n"
89
+ " A->>B: y\n"
90
+ " else sad\n"
91
+ " A->>B: z\n"
92
+ " end\n"
93
+ )
94
+ out = preprocess_mermaid_for_ascii(src)
95
+ for dropped in ("activate", "deactivate", "loop", "end", "alt ", "else ", "autonumber"):
96
+ self.assertNotIn(dropped, out.lower())
97
+ # But arrow lines survive
98
+ self.assertIn("A->>B: x", out)
99
+ self.assertIn("A->>B: y", out)
100
+ self.assertIn("A->>B: z", out)
101
+
102
+ def test_participant_aliases_with_parens_preserved(self):
103
+ src = (
104
+ "sequenceDiagram\n"
105
+ " participant C1 as Core (PID 92348)\n"
106
+ " participant C2 as Core (PID 93684)\n"
107
+ " C1->>C2: x"
108
+ )
109
+ out = preprocess_mermaid_for_ascii(src)
110
+ self.assertIn("participant C1 as Core (PID 92348)", out)
111
+ self.assertIn("participant C2 as Core (PID 93684)", out)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
@@ -1,48 +0,0 @@
1
- """Mermaid diagram renderer for termrender."""
2
-
3
- from __future__ import annotations
4
-
5
- import subprocess
6
-
7
- from termrender.blocks import Block
8
- from termrender.style import visual_ljust
9
-
10
-
11
- def fix_mermaid_encoding(text: str) -> str:
12
- """Undo mermaid-ascii's double-encoding of UTF-8 characters.
13
-
14
- mermaid-ascii misinterprets UTF-8 input bytes as Latin-1 and re-encodes
15
- to UTF-8, corrupting multi-byte characters (e.g. → becomes â\\x86\\x92).
16
- Reversing the process: encode back to Latin-1 to recover the original
17
- UTF-8 bytes, then decode as UTF-8.
18
- """
19
- try:
20
- return text.encode("latin-1").decode("utf-8")
21
- except (UnicodeDecodeError, UnicodeEncodeError):
22
- return text
23
-
24
-
25
- def render(block: Block, color: bool) -> list[str]:
26
- """Render a mermaid diagram from pre-rendered or on-the-fly ASCII output."""
27
- w = block.width
28
- rendered = block.attrs.get("_rendered")
29
-
30
- if rendered is None:
31
- source = block.attrs.get("source", "")
32
- try:
33
- result = subprocess.run(
34
- ["mermaid-ascii", "-f", "-", "-w", str(block.width or 80), "-y", "1"],
35
- input=source,
36
- capture_output=True,
37
- text=True,
38
- timeout=30,
39
- )
40
- rendered = fix_mermaid_encoding(result.stdout)
41
- except Exception:
42
- rendered = source
43
-
44
- lines: list[str] = []
45
- for raw_line in rendered.split("\n"):
46
- lines.append(visual_ljust(raw_line, w))
47
-
48
- return lines
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes