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

1from __future__ import annotations 

2 

3""" 

4Monochrome prefix-based high-lighter 

5==================================== 

6 

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.*). 

11 

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: 

17 

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. 

21 

22This drop-in replacement keeps behaviour identical while removing the ANSI 

23round-trip. 

24""" 

25from typing import Mapping, Optional 

26 

27from rich.style import Style 

28from rich.text import Text 

29 

30from ..abstraction import LineHighlighter 

31 

32 

33class MonoLinesHighlighter(LineHighlighter): 

34 """Colourise one line based on a prefix lookup. 

35 

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:: 

49 

50 { 

51 "-": "red", 

52 "+": "green", 

53 "r": "cyan", 

54 "m": "cyan", 

55 } 

56 """ 

57 

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 

76 

77 # ------------------------------------------------------------------ 

78 # Public API 

79 # ------------------------------------------------------------------ 

80 def colorize_line(self, line: Text) -> Text: 

81 """Apply a single style pass **in place**. 

82 

83 Parameters 

84 ---------- 

85 line : 

86 The :class:`rich.text.Text` instance to be modified. 

87 

88 Returns 

89 ------- 

90 rich.text.Text 

91 **The very same instance** that was passed in—allowing fluent, 

92 chainable APIs. 

93 

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() 

100 

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 

106 

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