Coverage for jsonschema_diff/color/base.py: 38%
29 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 18:01 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-15 18:01 +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
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:
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(
77 self,
78 text: str,
79 auto_line_wrapping: bool = False,
80 ) -> str:
81 """Colourise and immediately render to ANSI.
83 Parameters
84 ----------
85 text :
86 Multi-line input string.
88 Returns
89 -------
90 ANSI-encoded string ready for terminal output.
91 """
92 rich_lines = self.colorize(text)
94 console = Console(
95 force_terminal=True,
96 color_system="truecolor",
97 width=self._detect_width() if auto_line_wrapping else None,
98 legacy_windows=False,
99 )
100 with console.capture() as cap:
101 console.print(rich_lines, end="")
102 return cap.get()
104 # ------------------------------------------------------------------
105 # Internal helpers
106 # ------------------------------------------------------------------
107 @staticmethod
108 def _detect_width(default: int = 2048) -> int: # noqa: D401
109 """Best-effort terminal width detection.
111 Falls back to *default* when a real TTY is not present
112 (e.g. in CI).
114 Parameters
115 ----------
116 default :
117 Width to use when detection fails. Defaults to ``512``.
119 Returns
120 -------
121 Column count deemed safe for rendering.
122 """
123 try:
124 from shutil import get_terminal_size
126 return max(get_terminal_size().columns, 20)
127 except Exception: # pragma: no cover
128 return default