Coverage for jsonschema_diff/color/stages/mono_lines.py: 100%
26 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"""
4Monochrome prefix-based high-lighter
5====================================
7A :class:`~jsonschema_diff.color.abstraction.LineHighlighter` that decorates a
8single :class:`rich.text.Text` *in-place* by looking at its **first matching
9prefix**—typically the leading character produced by *unified diff* output
10(``-``, ``+``, *etc.*).
12Why a “Rich-native” rewrite?
13----------------------------
14The original *jsonschema-diff* implementation rendered to a string containing
15ANSI escape codes. In interactive TUI applications you often want to keep the
16object as a real ``Text`` so you can:
18* put it into a :class:`rich.table.Table`,
19* display it inside a :class:`rich.panel.Panel`,
20* or update it live without re-parsing ANSI.
22This drop-in replacement keeps behaviour identical while removing the ANSI
23round-trip.
24"""
25from typing import Mapping, Optional
27from rich.style import Style
28from rich.text import Text
30from ..abstraction import LineHighlighter
33class MonoLinesHighlighter(LineHighlighter):
34 """Colourise one line based on a prefix lookup.
36 Parameters
37 ----------
38 bold :
39 Apply the *bold* attribute together with the foreground colour.
40 Enabled by default to keep parity with the original.
41 default_color :
42 Fallback colour when no rule matches. If *None* the line is left
43 unchanged (except for *bold* when that option is *True*).
44 case_sensitive :
45 Whether prefix matching should be case-sensitive. Defaults to *False*.
46 rules :
47 Mapping ``prefix → colour``. The first match wins; order is therefore
48 significant. If *None*, the following defaults are used::
50 {
51 "-": "red",
52 "+": "green",
53 "r": "cyan",
54 "m": "cyan",
55 }
56 """
58 def __init__(
59 self,
60 bold: bool = True,
61 default_color: Optional[str] = None,
62 case_sensitive: bool = False,
63 rules: Mapping[str, str] | None = None,
64 ) -> None:
65 if rules is None:
66 rules = {
67 "-": "red",
68 "+": "green",
69 "r": "cyan",
70 "m": "cyan",
71 }
72 self.bold = bold
73 self.default_color = default_color
74 self.case_sensitive = case_sensitive
75 self.rules: Mapping[str, str] = dict(rules) # preserve order
77 # ------------------------------------------------------------------
78 # Public API
79 # ------------------------------------------------------------------
80 def colorize_line(self, line: Text) -> Text:
81 """Apply a single style pass **in place**.
83 Parameters
84 ----------
85 line :
86 The :class:`rich.text.Text` instance to be modified.
88 Returns
89 -------
90 rich.text.Text
91 **The very same instance** that was passed in—allowing fluent,
92 chainable APIs.
94 Note
95 ----
96 Only the *first* matching prefix is honoured; subsequent rules are
97 ignored, mirroring classic *grep* / *sed* behaviour.
98 """
99 probe = line.plain if self.case_sensitive else line.plain.lower()
101 for prefix, color in self.rules.items():
102 pref = prefix if self.case_sensitive else prefix.lower()
103 if probe.startswith(pref):
104 line.stylize(Style(color=color, bold=self.bold), 0, len(line))
105 return line # first match wins
107 # --- fall-back -------------------------------------------------
108 if self.default_color is not None:
109 line.stylize(Style(color=self.default_color, bold=self.bold), 0, len(line))
110 elif self.bold:
111 line.stylize(Style(bold=True), 0, len(line))
112 return line