termrender 4.10.0__tar.gz → 4.10.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 (78) hide show
  1. {termrender-4.10.0 → termrender-4.10.1}/CHANGELOG.md +24 -0
  2. {termrender-4.10.0 → termrender-4.10.1}/PKG-INFO +1 -1
  3. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_flow_layout.py +31 -7
  4. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_er.py +60 -0
  5. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_flow_corpus.py +7 -10
  6. {termrender-4.10.0 → termrender-4.10.1}/.github/workflows/publish.yml +0 -0
  7. {termrender-4.10.0 → termrender-4.10.1}/.gitignore +0 -0
  8. {termrender-4.10.0 → termrender-4.10.1}/CLAUDE.md +0 -0
  9. {termrender-4.10.0 → termrender-4.10.1}/LICENSE +0 -0
  10. {termrender-4.10.0 → termrender-4.10.1}/README.md +0 -0
  11. {termrender-4.10.0 → termrender-4.10.1}/design.json +0 -0
  12. {termrender-4.10.0 → termrender-4.10.1}/pyproject.toml +0 -0
  13. {termrender-4.10.0 → termrender-4.10.1}/requirements.json +0 -0
  14. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/CLAUDE.md +0 -0
  15. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/__init__.py +0 -0
  16. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/__main__.py +0 -0
  17. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/blocks.py +0 -0
  18. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/emit.py +0 -0
  19. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/layout.py +0 -0
  20. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/parser.py +0 -0
  21. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/py.typed +0 -0
  22. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/CLAUDE.md +0 -0
  23. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/__init__.py +0 -0
  24. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/borders.py +0 -0
  25. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/charts.py +0 -0
  26. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/code.py +0 -0
  27. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/columns.py +0 -0
  28. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/diff.py +0 -0
  29. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/divider.py +0 -0
  30. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid.py +0 -0
  31. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_class.py +0 -0
  32. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_er.py +0 -0
  33. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_flow.py +0 -0
  34. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_flow_model.py +0 -0
  35. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_flow_parser.py +0 -0
  36. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_gantt.py +0 -0
  37. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_journey.py +0 -0
  38. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_mindmap.py +0 -0
  39. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_pie.py +0 -0
  40. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_prelude.py +0 -0
  41. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_sequence.py +0 -0
  42. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_state.py +0 -0
  43. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/mermaid_timeline.py +0 -0
  44. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/panel.py +0 -0
  45. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/quote.py +0 -0
  46. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/stat.py +0 -0
  47. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/table.py +0 -0
  48. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/text.py +0 -0
  49. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/timeline.py +0 -0
  50. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/renderers/tree.py +0 -0
  51. {termrender-4.10.0 → termrender-4.10.1}/src/termrender/style.py +0 -0
  52. {termrender-4.10.0 → termrender-4.10.1}/tests/__init__.py +0 -0
  53. {termrender-4.10.0 → termrender-4.10.1}/tests/test_charts.py +0 -0
  54. {termrender-4.10.0 → termrender-4.10.1}/tests/test_cli_contract.py +0 -0
  55. {termrender-4.10.0 → termrender-4.10.1}/tests/test_column_alignment.py +0 -0
  56. {termrender-4.10.0 → termrender-4.10.1}/tests/test_diff.py +0 -0
  57. {termrender-4.10.0 → termrender-4.10.1}/tests/test_inline_badge.py +0 -0
  58. {termrender-4.10.0 → termrender-4.10.1}/tests/test_linebreak.py +0 -0
  59. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_class.py +0 -0
  60. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_dispatch.py +0 -0
  61. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_flow.py +0 -0
  62. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_flow_layout.py +0 -0
  63. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_flow_model.py +0 -0
  64. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_flow_parser.py +0 -0
  65. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_flow_shapes.py +0 -0
  66. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_gantt.py +0 -0
  67. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_journey.py +0 -0
  68. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_mindmap.py +0 -0
  69. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_pie.py +0 -0
  70. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_sequence.py +0 -0
  71. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_state.py +0 -0
  72. {termrender-4.10.0 → termrender-4.10.1}/tests/test_mermaid_timeline.py +0 -0
  73. {termrender-4.10.0 → termrender-4.10.1}/tests/test_myst_gaps.py +0 -0
  74. {termrender-4.10.0 → termrender-4.10.1}/tests/test_stat.py +0 -0
  75. {termrender-4.10.0 → termrender-4.10.1}/tests/test_tasklist.py +0 -0
  76. {termrender-4.10.0 → termrender-4.10.1}/tests/test_timeline.py +0 -0
  77. {termrender-4.10.0 → termrender-4.10.1}/tests/test_variable_colons.py +0 -0
  78. {termrender-4.10.0 → termrender-4.10.1}/uv.lock +0 -0
@@ -1,6 +1,30 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v4.10.1 (2026-07-07)
5
+
6
+ ### Bug Fixes
7
+
8
+ - **mermaid-flow**: Constant inter-layer gap regardless of node height
9
+ ([`eb1726f`](https://github.com/crouton-labs/termrender/commit/eb1726fa4bd05a57d29026b10715edc067ac4113))
10
+
11
+ The rank-band gap override that widens spacing to fit a labeled edge scaled with visual_len(label)
12
+ (the label's *text width*) even on TB/BT diagrams, where the label is drawn horizontally across a
13
+ vertical connector and only ever needs one clear row, not one row per character. Since ER/class
14
+ diagrams route almost every edge through a label (relationship text, cardinality), this made
15
+ compartmented-node TB layouts burn ~15-17 blank connector rows per edge hop while 1-line-node
16
+ flowcharts stayed compact — node height was a red herring; the real driver was label width feeding
17
+ a row count.
18
+
19
+ _rank_gap_overrides is now direction-aware: LR/RL keeps the width-scaled gap (the transpose turns
20
+ that native row-band into the final horizontal run the label reads along, so it genuinely needs
21
+ the width). TB/BT gets a small constant, _LABELED_ROW_GAP=3, regardless of label length.
22
+
23
+ Updates the two TB labeled-edge goldens in test_mermaid_flow_corpus.py to the new compact spacing,
24
+ and adds a regression test in test_mermaid_er.py pinning a row budget for compartmented entities
25
+ so this doesn't regress.
26
+
27
+
4
28
  ## v4.10.0 (2026-07-07)
5
29
 
6
30
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: termrender
3
- Version: 4.10.0
3
+ Version: 4.10.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
@@ -831,24 +831,45 @@ _GAP_X = 3 # minimum border-to-border gap between boxes in one rank/ban
831
831
  _ROW_GAP = 2 # minimum gap between rank bands
832
832
  _COMPONENT_GUTTER = 4 # gap between independently-laid-out components
833
833
  _LABEL_GAP_PAD = 1 # cells of breathing room either side of a label's own text
834
- # when it forces a rank-band gap wider than _ROW_GAP.
834
+ # when it forces a rank-band gap wider than _ROW_GAP
835
+ # along the axis the label's text actually runs.
836
+ _LABELED_ROW_GAP = 3 # inter-rank gap when a label sits on that band's vertical
837
+ # run in *final* screen space (TB/BT): one blank row, the
838
+ # label's own row, one more blank row. A constant, not
839
+ # scaled by the label's text length — the label reads
840
+ # horizontally on a single row regardless of how many
841
+ # characters it has, so the row *count* needed is fixed.
835
842
 
836
843
 
837
844
  def _rank_gap_overrides(
838
- edges: list[FlowEdge], rank_of: dict[str, int]
845
+ edges: list[FlowEdge], rank_of: dict[str, int], direction: Direction
839
846
  ) -> dict[int, int]:
840
847
  """Minimum inter-rank-band gap keyed by the *lower* rank of each
841
848
  adjacent-rank transition, widened past ``_ROW_GAP`` wherever a labeled
842
849
  edge directly connects that transition's two ranks. A forward edge
843
850
  between adjacent ranks routes as a single straight run (or a Z path
844
851
  that still crosses the same inter-rank band) whose only clear space is
845
- this gap — at the base ``_ROW_GAP`` a label wider than a couple of
846
- cells has nowhere to go but onto the boxes it connects, which is
847
- exactly the short LR/adjacent-rank clipped-label bug this closes.
852
+ this gap.
853
+
854
+ The rank axis is native rows here, pre-direction-transform (see the
855
+ adapter docstring) — whether that ends up as *vertical* or
856
+ *horizontal* space on screen depends on ``direction``. For LR/RL the
857
+ transpose turns this band into the final horizontal run the label
858
+ reads along, so it genuinely needs ``visual_len(label)`` cells of
859
+ width — at the base ``_ROW_GAP`` a label wider than a couple of cells
860
+ has nowhere to go but onto the boxes it connects (the short
861
+ LR/adjacent-rank clipped-label bug this closes). For TB/BT the band
862
+ stays vertical on screen and the label is drawn horizontally *across*
863
+ the connector column (:func:`_draw_label_on_segment`'s vertical-segment
864
+ branch) — it needs a little row headroom to have its own clear row,
865
+ not a row per character, so it gets the small constant
866
+ ``_LABELED_ROW_GAP`` regardless of the label's length.
867
+
848
868
  Non-adjacent-rank edges (back-edges, multi-rank spans) don't constrain
849
869
  the gap here — they route through their own side lane, not this band.
850
870
  """
851
871
  overrides: dict[int, int] = {}
872
+ horizontal_on_screen = direction in (Direction.LR, Direction.RL)
852
873
  for e in edges:
853
874
  if e.src == e.dst or not e.label:
854
875
  continue
@@ -859,7 +880,10 @@ def _rank_gap_overrides(
859
880
  lo_r, hi_r = (r_src, r_dst) if r_src <= r_dst else (r_dst, r_src)
860
881
  if hi_r - lo_r != 1:
861
882
  continue
862
- needed = visual_len(e.label) + 2 * _LABEL_GAP_PAD
883
+ if horizontal_on_screen:
884
+ needed = visual_len(e.label) + 2 * _LABEL_GAP_PAD
885
+ else:
886
+ needed = _LABELED_ROW_GAP
863
887
  overrides[lo_r] = max(overrides.get(lo_r, _ROW_GAP), needed)
864
888
  return overrides
865
889
 
@@ -922,7 +946,7 @@ def _place_nodes(
922
946
  ranks[sug.grx[v].rank].append(v)
923
947
 
924
948
  rank_of = {v.data: sug.grx[v].rank for v in comp_vertices}
925
- gap_overrides = _rank_gap_overrides(edges, rank_of)
949
+ gap_overrides = _rank_gap_overrides(edges, rank_of, direction)
926
950
 
927
951
  band_top_for_rank: dict[int, int] = {}
928
952
  cursor = 0
@@ -265,6 +265,66 @@ def test_order_example_golden_topology():
265
265
  assert cust_row < order_row < line_item_row
266
266
 
267
267
 
268
+ # --------------------------------------------------------------------------
269
+ # Vertical compactness (regression: constant inter-layer gap regardless of
270
+ # node height — see mermaid_flow_layout._LABELED_ROW_GAP)
271
+ # --------------------------------------------------------------------------
272
+
273
+
274
+ def test_compartmented_entities_get_constant_gap_not_height_scaled():
275
+ # Two tall (compartmented) entities joined by one labeled relationship
276
+ # used to burn ~15-17 blank connector rows for this single edge hop —
277
+ # the inter-rank gap scaled with the edge label's *text length*
278
+ # (visual_len), not with node height, but on ER/class diagrams nearly
279
+ # every edge carries a label, so the effect showed up exactly when
280
+ # nodes were tall. The gap between adjacent-rank boxes must now stay a
281
+ # small constant: enough for a connector row, the label's own row, and
282
+ # one more connector row — independent of both the label's length and
283
+ # the boxes' height.
284
+ src = (
285
+ "erDiagram\n"
286
+ " CUSTOMER ||--o{ ORDER : places\n"
287
+ " CUSTOMER {\n"
288
+ " string name PK\n"
289
+ " string email\n"
290
+ " }\n"
291
+ " ORDER {\n"
292
+ " int orderNumber PK\n"
293
+ " string status\n"
294
+ " }\n"
295
+ )
296
+ lines = _lines(src, width=100)
297
+ assert _has_box_glyphs(lines)
298
+ joined = "\n".join(lines)
299
+
300
+ # Full row budget: two 6-row compartmented boxes plus a small constant
301
+ # gap between them — well under the ~27 rows the scaling bug produced.
302
+ assert len(lines) <= 18, f"expected a compact render, got {len(lines)} rows:\n{joined}"
303
+
304
+ # The label survives in full, uncorrupted, and every box border row is
305
+ # single-glyph clean (no overlapping glyphs from a collapsed gap).
306
+ assert "places" in joined
307
+ for attr in ("string name PK", "string email", "int orderNumber PK", "string status"):
308
+ assert attr in joined
309
+
310
+ cust_row, _ = _row_col(lines, "CUSTOMER")
311
+ order_row, _ = _row_col(lines, "ORDER")
312
+ label_row, _ = _row_col(lines, "places")
313
+ assert cust_row < label_row < order_row
314
+
315
+ # The connector gap between the two boxes is a small constant, not
316
+ # scaled by the label's width: the blank/connector run directly above
317
+ # and below the label row is short.
318
+ cust_bottom = next(
319
+ r for r in range(cust_row, len(lines)) if "└" in lines[r]
320
+ )
321
+ order_top = next(
322
+ r for r in range(order_row, -1, -1) if "┌" in lines[r]
323
+ )
324
+ gap_rows = order_top - cust_bottom - 1
325
+ assert gap_rows <= 5, f"inter-layer gap grew to {gap_rows} rows:\n{joined}"
326
+
327
+
268
328
  # --------------------------------------------------------------------------
269
329
  # Degradation: malformed input, empty body, literal-glyph sanitization
270
330
  # --------------------------------------------------------------------------
@@ -195,9 +195,10 @@ def test_edge_style_bidirectional_golden():
195
195
 
196
196
 
197
197
  def test_pipe_label_golden():
198
- # The rank-band gap widens to fit the label in full (see
199
- # mermaid_flow_layout._rank_gap_overrides) a 2-char label needs a
200
- # taller gap than the base _ROW_GAP alone provides.
198
+ # The rank-band gap widens past the unlabeled _ROW_GAP to a small
199
+ # constant (see mermaid_flow_layout._LABELED_ROW_GAP) so the label gets
200
+ # its own clear row — the constant does not scale with the label's
201
+ # text length.
201
202
  assert _lines("graph TD\nA-->|hi|B\n") == [
202
203
  "┌───┐",
203
204
  "│ A │",
@@ -205,7 +206,6 @@ def test_pipe_label_golden():
205
206
  " │",
206
207
  " hi",
207
208
  " │",
208
- " │",
209
209
  "┌─▼─┐",
210
210
  "│ B │",
211
211
  "└───┘",
@@ -213,19 +213,16 @@ def test_pipe_label_golden():
213
213
 
214
214
 
215
215
  def test_inline_label_golden():
216
- # Same rank-band widening as above, scaled to the longer "go now" label.
216
+ # Same constant rank-band gap as above "go now" is longer than "hi"
217
+ # but the vertical gap stays the same _LABELED_ROW_GAP; only the
218
+ # label's own row is wider.
217
219
  assert _lines("graph TD\nA -- go now --> B\n") == [
218
220
  "┌───┐",
219
221
  "│ A │",
220
222
  "└───┘",
221
223
  " │",
222
- " │",
223
- " │",
224
224
  "go now",
225
225
  " │",
226
- " │",
227
- " │",
228
- " │",
229
226
  "┌─▼─┐",
230
227
  "│ B │",
231
228
  "└───┘",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes