Coverage for jsonschema_diff/color/base.py: 38%
29 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-25 07:00 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-25 07:00 +0000
1from __future__ import annotations
3"""
4Composable *Rich*-native colouring pipeline
5==========================================
7Provides :class:`HighlighterPipeline`, an orchestrator that feeds raw
8multi-line strings through a chain of :class:`LineHighlighter` stages.
9Each stage operates on :class:`rich.text.Text` so it can add style spans
10without mutating the text itself.
12Typical usage
13-------------
15>>> pipeline = HighlighterPipeline([MySyntaxHL(), MyDiffHL()])
16>>> ansi_output = pipeline.colorize_and_render(src_string)
17print(ansi_output)
18"""
20from typing import TYPE_CHECKING, Iterable
22from rich.console import Console
23from rich.text import Text
25if TYPE_CHECKING: # pragma: no cover
26 from .abstraction import LineHighlighter # noqa: F401 (imported for typing only)
29class HighlighterPipeline: # noqa: D101
30 """Chain of :pyclass:`LineHighlighter` stages.
32 Parameters
33 ----------
34 stages :
35 Iterable of :class:`LineHighlighter` instances. The iterable is
36 immediately materialised into a list so the pipeline can be reused.
38 Note
39 ----
40 * Each input line is passed through **every** stage in order.
41 * If a stage exposes a bulk :pyfunc:`colorize_lines` method it is
42 preferred over per-line iteration for performance.
43 """
45 def __init__(self, stages: Iterable["LineHighlighter"]):
46 self.stages: list["LineHighlighter"] = list(stages)
48 # ------------------------------------------------------------------
49 # Public helpers
50 # ------------------------------------------------------------------
51 def colorize(self, text: str) -> Text: # noqa: D401
52 """Return a rich ``Text`` object with all styles applied.
54 Parameters
55 ----------
56 text :
57 Multi-line string to be colourised.
59 Returns
60 -------
61 One composite `Text` built by joining all styled lines with
62 ``\\n`` separators.
63 """
64 lines = text.splitlines()
65 rich_lines = [Text(line) for line in lines]
67 for stage in self.stages:
68 colorize_lines = getattr(stage, "colorize_lines", None)
69 if callable(colorize_lines):
70 colorize_lines(rich_lines)
71 else:
72 for rl in rich_lines:
73 stage.colorize_line(rl)
74 return Text("\n").join(rich_lines)
76 def colorize_and_render(self, text: str) -> str:
77 """Colourise and immediately render to ANSI.
79 Parameters
80 ----------
81 text :
82 Multi-line input string.
84 Returns
85 -------
86 ANSI-encoded string ready for terminal output.
87 """
88 rich_lines = self.colorize(text)
90 console = Console(
91 force_terminal=True,
92 color_system="truecolor",
93 width=self._detect_width(),
94 legacy_windows=False,
95 )
96 with console.capture() as cap:
97 console.print(rich_lines, end="")
98 return cap.get()
100 # ------------------------------------------------------------------
101 # Internal helpers
102 # ------------------------------------------------------------------
103 @staticmethod
104 def _detect_width(default: int = 512) -> int: # noqa: D401
105 """Best-effort terminal width detection.
107 Falls back to *default* when a real TTY is not present
108 (e.g. in CI).
110 Parameters
111 ----------
112 default :
113 Width to use when detection fails. Defaults to ``512``.
115 Returns
116 -------
117 Column count deemed safe for rendering.
118 """
119 try:
120 from shutil import get_terminal_size
122 return max(get_terminal_size().columns, 20)
123 except Exception: # pragma: no cover
124 return default