diff --git a/CHANGELOG.md b/CHANGELOG.md index 79da255bf..b91cb3a4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Collapsed definitions for single-character spinners, to save memory and reduce import time. - Fix print_json indent type in __init__.py - Fix error when inspecting object defined in REPL https://github.com/Textualize/rich/pull/2037 +- Fix incorrect highlighting of non-indented JSON https://github.com/Textualize/rich/pull/2038 ### Changed diff --git a/rich/console.py b/rich/console.py index e2d7a6d52..1eb2e3eeb 100644 --- a/rich/console.py +++ b/rich/console.py @@ -2209,3 +2209,5 @@ def save_html( } ) console.log("foo") + + console.print_json(data={"name": "apple", "count": 1}, indent=None) diff --git a/rich/highlighter.py b/rich/highlighter.py index 69d326021..6e10d56d7 100644 --- a/rich/highlighter.py +++ b/rich/highlighter.py @@ -1,7 +1,9 @@ +import re +import string from abc import ABC, abstractmethod from typing import List, Union -from .text import Text +from .text import Span, Text def _combine_regex(*regexes: str) -> str: @@ -104,17 +106,39 @@ class ReprHighlighter(RegexHighlighter): class JSONHighlighter(RegexHighlighter): """Highlights JSON""" + # Captures the start and end of JSON strings, handling escaped quotes + JSON_STR = r"(?b?\".*?(?[\{\[\(\)\]\}])", r"\b(?Ptrue)\b|\b(?Pfalse)\b|\b(?Pnull)\b", r"(?P(?b?\".*?(?b?\".*?(? None: + super().highlight(text) + + # Additional work to handle highlighting JSON keys + plain = text.plain + append = text.spans.append + whitespace = self.JSON_WHITESPACE + for match in re.finditer(self.JSON_STR, plain): + start, end = match.span() + cursor = end + while cursor < len(plain): + char = plain[cursor] + cursor += 1 + if char == ":": + append(Span(start, end, "json.key")) + elif char in whitespace: + continue + break + if __name__ == "__main__": # pragma: no cover from .console import Console @@ -145,3 +169,6 @@ class JSONHighlighter(RegexHighlighter): console.print( "127.0.1.1 bar 192.168.1.4 2001:0db8:85a3:0000:0000:8a2e:0370:7334 foo" ) + import json + + console.print_json(json.dumps(obj={"name": "apple", "count": 1}), indent=None) diff --git a/tests/test_console.py b/tests/test_console.py index 0989d8ba1..06fb4a835 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -14,8 +14,8 @@ Console, ConsoleDimensions, ConsoleOptions, - group, ScreenUpdate, + group, ) from rich.control import Control from rich.measure import measure_renderables @@ -185,6 +185,15 @@ def test_print_json_ensure_ascii(): assert result == expected +def test_print_json_indent_none(): + console = Console(file=io.StringIO(), color_system="truecolor") + data = {"name": "apple", "count": 1} + console.print_json(data=data, indent=None) + result = console.file.getvalue() + 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' + assert result == expected + + def test_log(): console = Console( file=io.StringIO(), diff --git a/tests/test_highlighter.py b/tests/test_highlighter.py index c5303b5c0..a34334e3d 100644 --- a/tests/test_highlighter.py +++ b/tests/test_highlighter.py @@ -1,8 +1,10 @@ """Tests for the highlighter classes.""" -import pytest +import json from typing import List -from rich.highlighter import NullHighlighter, ReprHighlighter +import pytest + +from rich.highlighter import JSONHighlighter, NullHighlighter, ReprHighlighter from rich.text import Span, Text @@ -92,3 +94,53 @@ def test_highlight_regex(test: str, spans: List[Span]): highlighter.highlight(text) print(text.spans) assert text.spans == spans + + +def test_highlight_json_with_indent(): + json_string = json.dumps({"name": "apple", "count": 1}, indent=4) + text = Text(json_string) + highlighter = JSONHighlighter() + highlighter.highlight(text) + assert text.spans == [ + Span(0, 1, "json.brace"), + Span(6, 12, "json.str"), + Span(14, 21, "json.str"), + Span(27, 34, "json.str"), + Span(36, 37, "json.number"), + Span(38, 39, "json.brace"), + Span(6, 12, "json.key"), + Span(27, 34, "json.key"), + ] + + +def test_highlight_json_string_only(): + json_string = '"abc"' + text = Text(json_string) + highlighter = JSONHighlighter() + highlighter.highlight(text) + assert text.spans == [Span(0, 5, "json.str")] + + +def test_highlight_json_empty_string_only(): + json_string = '""' + text = Text(json_string) + highlighter = JSONHighlighter() + highlighter.highlight(text) + assert text.spans == [Span(0, 2, "json.str")] + + +def test_highlight_json_no_indent(): + json_string = json.dumps({"name": "apple", "count": 1}, indent=None) + text = Text(json_string) + highlighter = JSONHighlighter() + highlighter.highlight(text) + assert text.spans == [ + Span(0, 1, "json.brace"), + Span(1, 7, "json.str"), + Span(9, 16, "json.str"), + Span(18, 25, "json.str"), + Span(27, 28, "json.number"), + Span(28, 29, "json.brace"), + Span(1, 7, "json.key"), + Span(18, 25, "json.key"), + ]