Repository: Textualize/rich (python)

Issue:
[BUG] `rich.print_json()` produces incorrect syntax highlighting with `indent=None`
**Describe the bug**

When using the `indent=None` parameter to `rich.print_json()`, it results in incorrect highlighting.

Minimal example:
```py
data = {"items": [{"name": "apple", "count": 1}]}
rich.print_json(data=data)  # Good
rich.print_json(data=data, indent=None)  # Bad
```

Screenshot:
![Screenshot of issue](https://user-images.githubusercontent.com/482367/156926758-af8cb1f4-f68c-4d24-980d-04a48704e949.png)

**Platform**
<details>
<summary>Click to expand</summary>

* Environment (SSH): Debian 11, Python 3.10.2
* Terminal: GNOME Terminal 3.38.1, Ubuntu 21.10

```
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface.                                                  │
│                                                                                  │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=162 ColorSystem.EIGHT_BIT>                                    │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                  │
│     color_system = '256'                                                         │
│         encoding = 'utf-8'                                                       │
│             file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│           height = 39                                                            │
│    is_alt_screen = False                                                         │
│ is_dumb_terminal = False                                                         │
│   is_interactive = True                                                          │
│       is_jupyter = False                                                         │
│      is_terminal = True                                                          │
│   legacy_windows = False                                                         │
│         no_color = False                                                         │
│          options = ConsoleOptions(                                               │
│                        size=ConsoleDimensions(width=162, height=39),             │
│                        legacy_windows=False,                                     │
│                        min_width=1,                                              │
│                        max_width=162,                                            │
│                        is_terminal=True,                                         │
│                        encoding='utf-8',                                         │
│                        max_height=39,                                            │
│                        justify=None,                                             │
│                        overflow=None,                                            │
│                        no_wrap=False,                                            │
│                        highlight=None,                                           │
│                        markup=None,                                              │
│                        height=None                                               │
│                    )                                                             │
│            quiet = False                                                         │
│           record = False                                                         │
│         safe_box = True                                                          │
│             size = ConsoleDimensions(width=162, height=39)                       │
│        soft_wrap = False                                                         │
│           stderr = False                                                         │
│            style = None                                                          │
│         tab_size = 8                                                             │
│            width = 162                                                           │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭─── <class 'rich._windows.WindowsConsoleFeatures'> ────╮
│ Windows features available.                           │
│                                                       │
│ ╭───────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=False, truecolor=False) │ │
│ ╰───────────────────────────────────────────────────╯ │
│                                                       │
│ truecolor = False                                     │
│        vt = False                                     │
╰───────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ {                                  │
│     'TERM': 'screen-256color',     │
│     'COLORTERM': None,             │
│     'CLICOLOR': None,              │
│     'NO_COLOR': None,              │
│     'TERM_PROGRAM': None,          │
│     'COLUMNS': None,               │
│     'LINES': None,                 │
│     'JPY_PARENT_PID': None,        │
│     'VSCODE_VERBOSE_LOGGING': None │
│ }                                  │
╰────────────────────────────────────╯
platform="Linux"

rich==11.2.0
```

</details>


Pull request:
Fix highlighting issue when printing JSON that isn't indented
## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

Previously the JSONHighlighter would do two passes with Text.highlight - the second pass would highlight JSON keys.

I've got rid of the second regex for the 2nd Text.highlight pass, and replaced it with a regex which captures the spans of strings, followed by some manual processing to determine whether they're keys or not. If the string is a key, we append the json.key Span.

<img width="258" alt="image" src="https://user-images.githubusercontent.com/5740731/157269164-ab043855-e74e-4917-b7a4-a7de43381df2.png">

<img width="297" alt="image" src="https://user-images.githubusercontent.com/5740731/157269230-89d1ae6c-91fb-4cb3-ad87-98152c287d17.png">

<img width="262" alt="image" src="https://user-images.githubusercontent.com/5740731/157269332-80df350e-9ba3-40aa-b4d8-916048b7e842.png">

<img width="196" alt="image" src="https://user-images.githubusercontent.com/5740731/157269454-ca76c7d4-a8c2-4778-9dbd-2d57124767f9.png">

Previously:

<img width="263" alt="image" src="https://user-images.githubusercontent.com/5740731/157270333-76a29e82-2a42-4658-85f1-ae2f833943f7.png">

Closes #2027

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 79da255b..b91cb3a4 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 e2d7a6d5..1eb2e3ee 100644
--- a/rich/console.py
+++ b/rich/console.py
@@ -2209,3 +2209,5 @@ if __name__ == "__main__":  # pragma: no cover
         }
     )
     console.log("foo")
+
+    console.print_json(data={"name": "apple", "count": 1}, indent=None)
diff --git a/rich/highlighter.py b/rich/highlighter.py
index 69d32602..6e10d56d 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"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")"
+    JSON_WHITESPACE = {" ", "\n", "\r", "\t"}
+
     base_style = "json."
     highlights = [
         _combine_regex(
             r"(?P<brace>[\{\[\(\)\]\}])",
             r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
             r"(?P<number>(?<!\w)\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
-            r"(?<![\\\w])(?P<str>b?\".*?(?<!\\)\")",
+            JSON_STR,
         ),
-        r"(?<![\\\w])(?P<key>b?\".*?(?<!\\)\")\:",
     ]
 
+    def highlight(self, text: Text) -> 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 @@ if __name__ == "__main__":  # pragma: no cover
     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)


Test patch:
diff --git a/tests/test_console.py b/tests/test_console.py
index 0989d8ba..06fb4a83 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -14,8 +14,8 @@ from rich.console import (
     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 c5303b5c..a34334e3 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"),
+    ]