Skip to content

Commit 27c2ba6

Browse files
authored
Merge pull request #2038 from Textualize/print-json-indent-fix
Fix highlighting issue when printing JSON that isn't indented
2 parents b9b99ad + 94a1074 commit 27c2ba6

File tree

5 files changed

+97
-6
lines changed

5 files changed

+97
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
- Collapsed definitions for single-character spinners, to save memory and reduce import time.
2727
- Fix print_json indent type in __init__.py
2828
- Fix error when inspecting object defined in REPL https://github.com/Textualize/rich/pull/2037
29+
- Fix incorrect highlighting of non-indented JSON https://github.com/Textualize/rich/pull/2038
2930

3031
### Changed
3132

rich/console.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2209,3 +2209,5 @@ def save_html(
22092209
}
22102210
)
22112211
console.log("foo")
2212+
2213+
console.print_json(data={"name": "apple", "count": 1}, indent=None)

rich/highlighter.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import re
2+
import string
13
from abc import ABC, abstractmethod
24
from typing import List, Union
35

4-
from .text import Text
6+
from .text import Span, Text
57

68

79
def _combine_regex(*regexes: str) -> str:
@@ -104,17 +106,39 @@ class ReprHighlighter(RegexHighlighter):
104106
class JSONHighlighter(RegexHighlighter):
105107
"""Highlights JSON"""
106108

109+
# Captures the start and end of JSON strings, handling escaped quotes
110+
JSON_STR = r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")"
111+
JSON_WHITESPACE = {" ", "\n", "\r", "\t"}
112+
107113
base_style = "json."
108114
highlights = [
109115
_combine_regex(
110116
r"(?P<brace>[\{\[\(\)\]\}])",
111117
r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
112118
r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
113-
r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")",
119+
JSON_STR,
114120
),
115-
r"(?<![\\\w])(?P<key>b?\".*?(?<!\\)\")\:",
116121
]
117122

123+
def highlight(self, text: Text) -> None:
124+
super().highlight(text)
125+
126+
# Additional work to handle highlighting JSON keys
127+
plain = text.plain
128+
append = text.spans.append
129+
whitespace = self.JSON_WHITESPACE
130+
for match in re.finditer(self.JSON_STR, plain):
131+
start, end = match.span()
132+
cursor = end
133+
while cursor < len(plain):
134+
char = plain[cursor]
135+
cursor += 1
136+
if char == ":":
137+
append(Span(start, end, "json.key"))
138+
elif char in whitespace:
139+
continue
140+
break
141+
118142

119143
if __name__ == "__main__": # pragma: no cover
120144
from .console import Console
@@ -145,3 +169,6 @@ class JSONHighlighter(RegexHighlighter):
145169
console.print(
146170
"127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo"
147171
)
172+
import json
173+
174+
console.print_json(json.dumps(obj={"name": "apple", "count": 1}), indent=None)

tests/test_console.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
Console,
1515
ConsoleDimensions,
1616
ConsoleOptions,
17-
group,
1817
ScreenUpdate,
18+
group,
1919
)
2020
from rich.control import Control
2121
from rich.measure import measure_renderables
@@ -185,6 +185,15 @@ def test_print_json_ensure_ascii():
185185
assert result == expected
186186

187187

188+
def test_print_json_indent_none():
189+
console = Console(file=io.StringIO(), color_system="truecolor")
190+
data = {"name": "apple", "count": 1}
191+
console.print_json(data=data, indent=None)
192+
result = console.file.getvalue()
193+
expected = '\x1b[1m{\x1b[0m\x1b[1;34m"name"\x1b[0m: \x1b[32m"apple"\x1b[0m, \x1b[1;34m"count"\x1b[0m: \x1b[1;36m1\x1b[0m\x1b[1m}\x1b[0m\n'
194+
assert result == expected
195+
196+
188197
def test_log():
189198
console = Console(
190199
file=io.StringIO(),

tests/test_highlighter.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Tests for the highlighter classes."""
2-
import pytest
2+
import json
33
from typing import List
44

5-
from rich.highlighter import NullHighlighter, ReprHighlighter
5+
import pytest
6+
7+
from rich.highlighter import JSONHighlighter, NullHighlighter, ReprHighlighter
68
from rich.text import Span, Text
79

810

@@ -92,3 +94,53 @@ def test_highlight_regex(test: str, spans: List[Span]):
9294
highlighter.highlight(text)
9395
print(text.spans)
9496
assert text.spans == spans
97+
98+
99+
def test_highlight_json_with_indent():
100+
json_string = json.dumps({"name": "apple", "count": 1}, indent=4)
101+
text = Text(json_string)
102+
highlighter = JSONHighlighter()
103+
highlighter.highlight(text)
104+
assert text.spans == [
105+
Span(0, 1, "json.brace"),
106+
Span(6, 12, "json.str"),
107+
Span(14, 21, "json.str"),
108+
Span(27, 34, "json.str"),
109+
Span(36, 37, "json.number"),
110+
Span(38, 39, "json.brace"),
111+
Span(6, 12, "json.key"),
112+
Span(27, 34, "json.key"),
113+
]
114+
115+
116+
def test_highlight_json_string_only():
117+
json_string = '"abc"'
118+
text = Text(json_string)
119+
highlighter = JSONHighlighter()
120+
highlighter.highlight(text)
121+
assert text.spans == [Span(0, 5, "json.str")]
122+
123+
124+
def test_highlight_json_empty_string_only():
125+
json_string = '""'
126+
text = Text(json_string)
127+
highlighter = JSONHighlighter()
128+
highlighter.highlight(text)
129+
assert text.spans == [Span(0, 2, "json.str")]
130+
131+
132+
def test_highlight_json_no_indent():
133+
json_string = json.dumps({"name": "apple", "count": 1}, indent=None)
134+
text = Text(json_string)
135+
highlighter = JSONHighlighter()
136+
highlighter.highlight(text)
137+
assert text.spans == [
138+
Span(0, 1, "json.brace"),
139+
Span(1, 7, "json.str"),
140+
Span(9, 16, "json.str"),
141+
Span(18, 25, "json.str"),
142+
Span(27, 28, "json.number"),
143+
Span(28, 29, "json.brace"),
144+
Span(1, 7, "json.key"),
145+
Span(18, 25, "json.key"),
146+
]

0 commit comments

Comments
 (0)