termrender 0.7.1__tar.gz → 0.7.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-0.7.1 → termrender-0.7.3}/CHANGELOG.md +30 -0
- {termrender-0.7.1 → termrender-0.7.3}/PKG-INFO +1 -1
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/__main__.py +13 -49
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/layout.py +5 -2
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/parser.py +97 -10
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/code.py +15 -3
- {termrender-0.7.1 → termrender-0.7.3}/.github/workflows/publish.yml +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/.gitignore +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/CLAUDE.md +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/LICENSE +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/README.md +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/design.json +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/pyproject.toml +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/requirements.json +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/CLAUDE.md +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/__init__.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/blocks.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/emit.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/py.typed +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/CLAUDE.md +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/__init__.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/borders.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/charts.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/columns.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/diff.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/divider.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/mermaid.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/panel.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/quote.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/stat.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/table.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/text.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/timeline.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/renderers/tree.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/src/termrender/style.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/__init__.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_charts.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_column_alignment.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_diff.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_inline_badge.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_myst_gaps.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_stat.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_tasklist.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_timeline.py +0 -0
- {termrender-0.7.1 → termrender-0.7.3}/tests/test_variable_colons.py +0 -0
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.7.3 (2026-04-15)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- **code**: Wrap long code lines to fit layout width
|
|
9
|
+
([`31c6e59`](https://github.com/CaptainCrouton89/termrender/commit/31c6e595a438c4ced8c61fff679b59d4ae55f938))
|
|
10
|
+
|
|
11
|
+
Code blocks previously used raw line count for height and let render_box grow beyond the layout
|
|
12
|
+
allocation. Now wraps source lines to the available content width in both layout and renderer.
|
|
13
|
+
|
|
14
|
+
- **parser**: Add directive trace and file-absolute line numbers to error messages
|
|
15
|
+
([`0f99ea0`](https://github.com/CaptainCrouton89/termrender/commit/0f99ea0310116f8fa06e933cd26126246d7a3b43))
|
|
16
|
+
|
|
17
|
+
Stray-closer and unclosed-directive errors now print the full open/close trace and, when nested
|
|
18
|
+
directives share a colon count, name the specific cause and suggest the fix. Recursive body
|
|
19
|
+
parsing reports file-absolute line numbers via _line_offset threading through parse →
|
|
20
|
+
_split_directives → _directive_to_block.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## v0.7.2 (2026-04-09)
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
- **cli**: Default --tmux pane to 1/3 window width
|
|
28
|
+
([`d9c1bcc`](https://github.com/CaptainCrouton89/termrender/commit/d9c1bccbe95a4e5cf1f975b82cbafde6d9d3807a))
|
|
29
|
+
|
|
30
|
+
Instead of preview-rendering at 80 cols to measure content width, default to (window_width - 2) // 3
|
|
31
|
+
for a consistent 1/3 split.
|
|
32
|
+
|
|
33
|
+
|
|
4
34
|
## v0.7.1 (2026-04-08)
|
|
5
35
|
|
|
6
36
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: termrender
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -298,17 +298,9 @@ def main() -> None:
|
|
|
298
298
|
from termrender.parser import parse as _parse
|
|
299
299
|
_parse(source)
|
|
300
300
|
except DirectiveError as e:
|
|
301
|
-
_error(
|
|
302
|
-
f"syntax error: {e}",
|
|
303
|
-
fix="check directive openers have matching ::: closers and attribute syntax is key=\"value\"",
|
|
304
|
-
code=EXIT_SYNTAX,
|
|
305
|
-
)
|
|
301
|
+
_error(f"syntax error: {e}", code=EXIT_SYNTAX)
|
|
306
302
|
except ValueError as e:
|
|
307
|
-
_error(
|
|
308
|
-
f"nesting error: {e}",
|
|
309
|
-
fix="reduce directive nesting depth (max 50 levels)",
|
|
310
|
-
code=EXIT_SYNTAX,
|
|
311
|
-
)
|
|
303
|
+
_error(f"nesting error: {e}", code=EXIT_SYNTAX)
|
|
312
304
|
|
|
313
305
|
import shlex
|
|
314
306
|
import subprocess
|
|
@@ -343,28 +335,17 @@ def main() -> None:
|
|
|
343
335
|
if args.width:
|
|
344
336
|
pane_width = args.width
|
|
345
337
|
else:
|
|
346
|
-
#
|
|
347
|
-
from termrender.style import visual_len
|
|
338
|
+
# Default: 1/3 of window width (minus separator)
|
|
348
339
|
try:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
default=40,
|
|
340
|
+
result = subprocess.run(
|
|
341
|
+
["tmux", "display-message", "-p", "#{window_width}"],
|
|
342
|
+
capture_output=True, text=True, check=True,
|
|
353
343
|
)
|
|
354
|
-
|
|
344
|
+
window_width = int(result.stdout.strip())
|
|
345
|
+
pane_width = (window_width - 2) // 3
|
|
355
346
|
except Exception:
|
|
356
|
-
pane_width =
|
|
347
|
+
pane_width = 60
|
|
357
348
|
|
|
358
|
-
# Cap to available tmux space (leave room for the source pane)
|
|
359
|
-
try:
|
|
360
|
-
result = subprocess.run(
|
|
361
|
-
["tmux", "display-message", "-p", "#{pane_width}"],
|
|
362
|
-
capture_output=True, text=True, check=True,
|
|
363
|
-
)
|
|
364
|
-
available = int(result.stdout.strip())
|
|
365
|
-
pane_width = min(pane_width, available - 10)
|
|
366
|
-
except Exception:
|
|
367
|
-
pass
|
|
368
349
|
pane_width = max(pane_width, 20) # absolute minimum
|
|
369
350
|
|
|
370
351
|
# Watch mode points the new pane at the user's real file so edits
|
|
@@ -458,17 +439,9 @@ def main() -> None:
|
|
|
458
439
|
from termrender.parser import parse
|
|
459
440
|
parse(source)
|
|
460
441
|
except DirectiveError as e:
|
|
461
|
-
_error(
|
|
462
|
-
f"syntax error: {e}",
|
|
463
|
-
fix="check directive openers have matching ::: closers and attribute syntax is key=\"value\"",
|
|
464
|
-
code=EXIT_SYNTAX,
|
|
465
|
-
)
|
|
442
|
+
_error(f"syntax error: {e}", code=EXIT_SYNTAX)
|
|
466
443
|
except ValueError as e:
|
|
467
|
-
_error(
|
|
468
|
-
f"nesting error: {e}",
|
|
469
|
-
fix="reduce directive nesting depth (max 50 levels)",
|
|
470
|
-
code=EXIT_SYNTAX,
|
|
471
|
-
)
|
|
444
|
+
_error(f"nesting error: {e}", code=EXIT_SYNTAX)
|
|
472
445
|
print("ok", file=sys.stderr)
|
|
473
446
|
sys.exit(EXIT_OK)
|
|
474
447
|
|
|
@@ -485,18 +458,9 @@ def main() -> None:
|
|
|
485
458
|
code=EXIT_TERMINAL,
|
|
486
459
|
)
|
|
487
460
|
except DirectiveError as e:
|
|
488
|
-
_error(
|
|
489
|
-
f"syntax error: {e}",
|
|
490
|
-
fix="check directive openers have matching ::: closers and attribute syntax is key=\"value\"",
|
|
491
|
-
hint="run: termrender --check <file> to validate before rendering",
|
|
492
|
-
code=EXIT_SYNTAX,
|
|
493
|
-
)
|
|
461
|
+
_error(f"syntax error: {e}", code=EXIT_SYNTAX)
|
|
494
462
|
except ValueError as e:
|
|
495
|
-
_error(
|
|
496
|
-
f"nesting error: {e}",
|
|
497
|
-
fix="reduce directive nesting depth (max 50 levels)",
|
|
498
|
-
code=EXIT_SYNTAX,
|
|
499
|
-
)
|
|
463
|
+
_error(f"nesting error: {e}", code=EXIT_SYNTAX)
|
|
500
464
|
|
|
501
465
|
sys.stdout.write(output)
|
|
502
466
|
|
|
@@ -92,8 +92,11 @@ def resolve_height(block: Block) -> None:
|
|
|
92
92
|
|
|
93
93
|
elif bt == BlockType.CODE:
|
|
94
94
|
source = block.attrs.get("source") or _plain_text(block.text)
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
raw_lines = source.split("\n") if source else [""]
|
|
96
|
+
border_v = visual_len("│")
|
|
97
|
+
content_w = max(width - 2 * border_v - 2, 1)
|
|
98
|
+
total_lines = sum(len(wrap_text(line, content_w)) for line in raw_lines)
|
|
99
|
+
block.height = total_lines + 2
|
|
97
100
|
|
|
98
101
|
elif bt == BlockType.COLUMNS:
|
|
99
102
|
block.height = max((c.height or 0 for c in block.children), default=0)
|
|
@@ -405,21 +405,80 @@ def _parse_markdown(source: str, _depth: int = 0) -> list[Block]:
|
|
|
405
405
|
return _convert_ast(ast_nodes, _depth=_depth)
|
|
406
406
|
|
|
407
407
|
|
|
408
|
-
def
|
|
408
|
+
def _find_nested_directives(body_lines: list[str]) -> list[str]:
|
|
409
|
+
"""Return names of directive openers found in body lines."""
|
|
410
|
+
names: list[str] = []
|
|
411
|
+
for line in body_lines:
|
|
412
|
+
m = _DIRECTIVE_OPEN.match(line)
|
|
413
|
+
if m:
|
|
414
|
+
names.append(m.group(2))
|
|
415
|
+
return names
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _stray_closer_message(
|
|
419
|
+
abs_line: int, close_colons: str, trace: list[str],
|
|
420
|
+
last_closed: dict | None,
|
|
421
|
+
) -> str:
|
|
422
|
+
"""Format a stray-closer error with trace and fix suggestion."""
|
|
423
|
+
msg = f"line {abs_line}: stray '{close_colons}' closer — no directive is open"
|
|
424
|
+
if trace:
|
|
425
|
+
msg += "\n\n directive trace:\n" + "\n".join(trace)
|
|
426
|
+
|
|
427
|
+
if last_closed:
|
|
428
|
+
nested = _find_nested_directives(last_closed["body_lines"])
|
|
429
|
+
if nested:
|
|
430
|
+
outer = last_closed["name"]
|
|
431
|
+
inner = nested[0]
|
|
432
|
+
fix_colons = ":" * (last_closed["colon_count"] + 1)
|
|
433
|
+
msg += (
|
|
434
|
+
f"\n\n Likely cause: :::{outer} at line {last_closed['open_line']} "
|
|
435
|
+
f"contains a nested :::{inner} with the same colon count.\n"
|
|
436
|
+
f" The inner ':::' closer (line {last_closed['close_line']}) "
|
|
437
|
+
f"matched the outer directive instead.\n"
|
|
438
|
+
f" Fix: use {fix_colons}{outer} for the outer directive"
|
|
439
|
+
)
|
|
440
|
+
return msg
|
|
441
|
+
|
|
442
|
+
msg += (
|
|
443
|
+
"\n\n Fix: remove this stray closer, or if nesting is intended,\n"
|
|
444
|
+
" use more colons on outer directives (::::outer wraps :::inner)"
|
|
445
|
+
)
|
|
446
|
+
return msg
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _unclosed_directive_message(
|
|
450
|
+
open_line: int, colons: str, name: str, trace: list[str],
|
|
451
|
+
body_lines: list[str],
|
|
452
|
+
) -> str:
|
|
453
|
+
"""Format an unclosed-directive error with trace and fix suggestion."""
|
|
454
|
+
msg = f"line {open_line}: unclosed '{colons}{name}' — add '{colons}' on a new line to close it"
|
|
455
|
+
if trace:
|
|
456
|
+
msg += "\n\n directive trace:\n" + "\n".join(trace)
|
|
457
|
+
return msg
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _split_directives(source: str, _line_offset: int = 0) -> list[dict]:
|
|
409
461
|
"""Split source into directive and markdown segments.
|
|
410
462
|
|
|
411
463
|
Returns a list of segments, each being either:
|
|
412
464
|
{"type": "markdown", "content": str}
|
|
413
|
-
{"type": "directive", "name": str, "attrs": dict, "body": str
|
|
465
|
+
{"type": "directive", "name": str, "attrs": dict, "body": str,
|
|
466
|
+
"body_start_offset": int}
|
|
467
|
+
|
|
468
|
+
On error, raises DirectiveError with a directive trace showing the
|
|
469
|
+
sequence of opens/closes that led to the error state.
|
|
414
470
|
"""
|
|
415
471
|
lines = source.split("\n")
|
|
416
472
|
segments: list[dict] = []
|
|
417
473
|
current_md_lines: list[str] = []
|
|
418
474
|
stack: list[dict] = [] # stack of open directives
|
|
475
|
+
trace: list[str] = [] # event log for error diagnostics
|
|
476
|
+
last_closed: dict | None = None # most recently closed directive entry
|
|
419
477
|
|
|
420
478
|
i = 0
|
|
421
479
|
while i < len(lines):
|
|
422
480
|
line = lines[i]
|
|
481
|
+
abs_line = _line_offset + i + 1 # 1-indexed file-absolute line number
|
|
423
482
|
|
|
424
483
|
# Check for directive opener
|
|
425
484
|
m_open = _DIRECTIVE_OPEN.match(line)
|
|
@@ -440,6 +499,8 @@ def _split_directives(source: str) -> list[dict]:
|
|
|
440
499
|
"attrs_raw": attrs_raw,
|
|
441
500
|
"body_lines": [],
|
|
442
501
|
"colon_count": len(colons),
|
|
502
|
+
"open_line": abs_line,
|
|
503
|
+
"body_start_offset": _line_offset + i + 1,
|
|
443
504
|
}
|
|
444
505
|
# Self-closing directives (no body content expected)
|
|
445
506
|
if entry["name"] in _SELF_CLOSING_DIRECTIVES:
|
|
@@ -448,9 +509,12 @@ def _split_directives(source: str) -> list[dict]:
|
|
|
448
509
|
"name": entry["name"],
|
|
449
510
|
"attrs": _parse_attrs(entry["attrs_raw"]),
|
|
450
511
|
"body": "",
|
|
512
|
+
"body_start_offset": entry["body_start_offset"],
|
|
451
513
|
})
|
|
514
|
+
trace.append(f" line {abs_line}: {colons}{name} (self-closing)")
|
|
452
515
|
else:
|
|
453
516
|
stack.append(entry)
|
|
517
|
+
trace.append(f" line {abs_line}: {colons}{name} opened")
|
|
454
518
|
else:
|
|
455
519
|
# Nested directive — always treat as body content
|
|
456
520
|
stack[-1]["body_lines"].append(line)
|
|
@@ -462,8 +526,9 @@ def _split_directives(source: str) -> list[dict]:
|
|
|
462
526
|
if m_close and not stack:
|
|
463
527
|
if not _any_self_closing_before(lines, i):
|
|
464
528
|
close_colons = m_close.group(1)
|
|
529
|
+
trace.append(f" line {abs_line}: {close_colons} stray closer (nothing is open)")
|
|
465
530
|
raise DirectiveError(
|
|
466
|
-
|
|
531
|
+
_stray_closer_message(abs_line, close_colons, trace, last_closed)
|
|
467
532
|
)
|
|
468
533
|
# Stray closer after a self-closing directive like divider — skip
|
|
469
534
|
i += 1
|
|
@@ -476,11 +541,30 @@ def _split_directives(source: str) -> list[dict]:
|
|
|
476
541
|
else:
|
|
477
542
|
# Closing the open directive
|
|
478
543
|
entry = stack.pop()
|
|
544
|
+
closed_colons = ":" * entry["colon_count"]
|
|
545
|
+
nested = _find_nested_directives(entry["body_lines"])
|
|
546
|
+
nested_note = ""
|
|
547
|
+
if nested:
|
|
548
|
+
names = ", ".join(f":::{n}" for n in nested[:3])
|
|
549
|
+
nested_note = f" — body has nested {names}"
|
|
550
|
+
trace.append(
|
|
551
|
+
f" line {abs_line}: {closed_colons} "
|
|
552
|
+
f"closed {entry['name']} (opened line {entry['open_line']})"
|
|
553
|
+
f"{nested_note}"
|
|
554
|
+
)
|
|
555
|
+
last_closed = {
|
|
556
|
+
"name": entry["name"],
|
|
557
|
+
"colon_count": entry["colon_count"],
|
|
558
|
+
"open_line": entry["open_line"],
|
|
559
|
+
"close_line": abs_line,
|
|
560
|
+
"body_lines": entry["body_lines"],
|
|
561
|
+
}
|
|
479
562
|
segments.append({
|
|
480
563
|
"type": "directive",
|
|
481
564
|
"name": entry["name"],
|
|
482
565
|
"attrs": _parse_attrs(entry["attrs_raw"]),
|
|
483
566
|
"body": "\n".join(entry["body_lines"]),
|
|
567
|
+
"body_start_offset": entry["body_start_offset"],
|
|
484
568
|
})
|
|
485
569
|
i += 1
|
|
486
570
|
continue
|
|
@@ -504,8 +588,10 @@ def _split_directives(source: str) -> list[dict]:
|
|
|
504
588
|
unclosed = stack[-1]
|
|
505
589
|
colons = ":" * unclosed["colon_count"]
|
|
506
590
|
name = unclosed["name"]
|
|
591
|
+
open_line = unclosed["open_line"]
|
|
592
|
+
trace.append(f" line {open_line}: {colons}{name} ← still open at end of input")
|
|
507
593
|
raise DirectiveError(
|
|
508
|
-
|
|
594
|
+
_unclosed_directive_message(open_line, colons, name, trace, unclosed["body_lines"])
|
|
509
595
|
)
|
|
510
596
|
|
|
511
597
|
return segments
|
|
@@ -514,7 +600,7 @@ def _split_directives(source: str) -> list[dict]:
|
|
|
514
600
|
_MAX_PARSE_DEPTH = 50
|
|
515
601
|
|
|
516
602
|
|
|
517
|
-
def _directive_to_block(name: str, attrs: dict[str, Any], body: str, _depth: int = 0) -> Block:
|
|
603
|
+
def _directive_to_block(name: str, attrs: dict[str, Any], body: str, _depth: int = 0, _line_offset: int = 0) -> Block:
|
|
518
604
|
"""Convert a parsed directive into a Block."""
|
|
519
605
|
# Strip option lines from body; inline attrs take precedence over options
|
|
520
606
|
options, body = _strip_options(body)
|
|
@@ -545,12 +631,12 @@ def _directive_to_block(name: str, attrs: dict[str, Any], body: str, _depth: int
|
|
|
545
631
|
|
|
546
632
|
# Stat: optional caption body parsed as markdown
|
|
547
633
|
if block_type == BlockType.STAT:
|
|
548
|
-
body_doc = parse(body, _depth=_depth + 1) if body.strip() else Block(type=BlockType.DOCUMENT)
|
|
634
|
+
body_doc = parse(body, _depth=_depth + 1, _line_offset=_line_offset) if body.strip() else Block(type=BlockType.DOCUMENT)
|
|
549
635
|
return Block(type=block_type, children=body_doc.children, attrs=attrs)
|
|
550
636
|
|
|
551
637
|
# Tasklist alias: parse body, find the inner list, force tasklist styling
|
|
552
638
|
if name == "tasklist":
|
|
553
|
-
body_doc = parse(body, _depth=_depth + 1)
|
|
639
|
+
body_doc = parse(body, _depth=_depth + 1, _line_offset=_line_offset)
|
|
554
640
|
for child in body_doc.children:
|
|
555
641
|
if child.type == BlockType.LIST:
|
|
556
642
|
child.attrs["tasklist"] = True
|
|
@@ -563,7 +649,7 @@ def _directive_to_block(name: str, attrs: dict[str, Any], body: str, _depth: int
|
|
|
563
649
|
return Block(type=BlockType.LIST, attrs={"tasklist": True, **attrs})
|
|
564
650
|
|
|
565
651
|
# Recursively parse the body through the full two-pass pipeline
|
|
566
|
-
body_doc = parse(body, _depth=_depth + 1)
|
|
652
|
+
body_doc = parse(body, _depth=_depth + 1, _line_offset=_line_offset)
|
|
567
653
|
return Block(
|
|
568
654
|
type=block_type,
|
|
569
655
|
children=body_doc.children,
|
|
@@ -641,14 +727,14 @@ def _apply_tasklist_markers(block: Block) -> Block:
|
|
|
641
727
|
return block
|
|
642
728
|
|
|
643
729
|
|
|
644
|
-
def parse(source: str, _depth: int = 0) -> Block:
|
|
730
|
+
def parse(source: str, _depth: int = 0, _line_offset: int = 0) -> Block:
|
|
645
731
|
"""Parse markdown+directive source into a Block tree.
|
|
646
732
|
|
|
647
733
|
Returns a Block with type=DOCUMENT as root.
|
|
648
734
|
"""
|
|
649
735
|
if _depth > _MAX_PARSE_DEPTH:
|
|
650
736
|
raise ValueError(f"Maximum directive nesting depth ({_MAX_PARSE_DEPTH}) exceeded")
|
|
651
|
-
segments = _split_directives(source)
|
|
737
|
+
segments = _split_directives(source, _line_offset=_line_offset)
|
|
652
738
|
children: list[Block] = []
|
|
653
739
|
|
|
654
740
|
for seg in segments:
|
|
@@ -657,6 +743,7 @@ def parse(source: str, _depth: int = 0) -> Block:
|
|
|
657
743
|
else:
|
|
658
744
|
children.append(_directive_to_block(
|
|
659
745
|
seg["name"], seg["attrs"], seg["body"], _depth=_depth,
|
|
746
|
+
_line_offset=seg.get("body_start_offset", 0),
|
|
660
747
|
))
|
|
661
748
|
|
|
662
749
|
# Walk the tree to auto-promote any markdown list with [ ]/[x] markers
|
|
@@ -10,6 +10,7 @@ from pygments.lexers import TextLexer, get_lexer_by_name
|
|
|
10
10
|
|
|
11
11
|
from termrender.blocks import Block
|
|
12
12
|
from termrender.renderers.borders import render_box
|
|
13
|
+
from termrender.style import visual_len, wrap_text
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def render(
|
|
@@ -19,18 +20,29 @@ def render(
|
|
|
19
20
|
source = block.attrs.get("source", "")
|
|
20
21
|
lang = block.attrs.get("lang")
|
|
21
22
|
|
|
23
|
+
# Wrap raw source lines to fit within the box before highlighting,
|
|
24
|
+
# so render_box doesn't need to grow beyond the layout allocation.
|
|
25
|
+
border_v = visual_len("│")
|
|
26
|
+
content_w = max((block.width or 1) - 2 * border_v - 2, 1)
|
|
27
|
+
raw_lines = source.split("\n") if source else [""]
|
|
28
|
+
wrapped_lines = []
|
|
29
|
+
for line in raw_lines:
|
|
30
|
+
wrapped_lines.extend(wrap_text(line, content_w))
|
|
31
|
+
|
|
32
|
+
wrapped_source = "\n".join(wrapped_lines)
|
|
33
|
+
|
|
22
34
|
# Syntax highlight (or plain text)
|
|
23
|
-
if color and
|
|
35
|
+
if color and wrapped_source:
|
|
24
36
|
try:
|
|
25
37
|
lexer = get_lexer_by_name(lang) if lang else TextLexer()
|
|
26
38
|
except Exception:
|
|
27
39
|
lexer = TextLexer()
|
|
28
|
-
highlighted = highlight(
|
|
40
|
+
highlighted = highlight(wrapped_source, lexer, TerminalFormatter())
|
|
29
41
|
# Pygments adds a trailing newline — strip it
|
|
30
42
|
highlighted = highlighted.rstrip("\n")
|
|
31
43
|
code_lines = highlighted.split("\n")
|
|
32
44
|
else:
|
|
33
|
-
code_lines =
|
|
45
|
+
code_lines = wrapped_lines
|
|
34
46
|
|
|
35
47
|
return render_box(
|
|
36
48
|
code_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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|