Repository: Textualize/rich
Pull request #2301 (2022-05-26 19:09:29)

Add Iso8601Highlighter
## Type of changes

- [ ] Bug fix
- [X] 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

As suggesterd by @willmcgugan in #2295, I've added a date-time highlighter.


tomers: @willmcgugan, I've force-pushed without realizing that you submitted to the remote branch. Can you re-introduce your changes? Sorry for that.

Patch:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 5562f14e..47cc07b8 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -42,3 +42,4 @@ The following people have contributed to the development of Rich:
 - [Handhika Yanuar Pratama](https://github.com/theDreamer911)
 - [za](https://github.com/za)
 - [Motahhar Mokfi](https://github.com/motahhar)
+- [Tomer Shalev](https://github.com/tomers)
diff --git a/docs/source/highlighting.rst b/docs/source/highlighting.rst
index 34274d97..0bd68292 100644
--- a/docs/source/highlighting.rst
+++ b/docs/source/highlighting.rst
@@ -4,6 +4,7 @@ Highlighting
 ============
 
 Rich can apply styles to patterns in text which you :meth:`~rich.console.Console.print` or :meth:`~rich.console.Console.log`. With the default settings, Rich will highlight things such as numbers, strings, collections, booleans, None, and a few more exotic patterns such as file paths, URLs and UUIDs.
+Additional non-default highlighter are available, such as :class:`~rich.highlighter.ISO8601Highlighter` to highlight date and time.
 
 You can disable highlighting either by setting ``highlight=False`` on :meth:`~rich.console.Console.print` or :meth:`~rich.console.Console.log`, or by setting ``highlight=False`` on the :class:`~rich.console.Console` constructor which disables it everywhere. If you disable highlighting on the constructor, you can still selectively *enable* highlighting with ``highlight=True`` on print/log.
 
diff --git a/rich/default_styles.py b/rich/default_styles.py
index 1969bb78..f8ec672e 100644
--- a/rich/default_styles.py
+++ b/rich/default_styles.py
@@ -157,6 +157,9 @@ DEFAULT_STYLES: Dict[str, Style] = {
     "markdown.h7": Style(italic=True, dim=True),
     "markdown.link": Style(color="bright_blue"),
     "markdown.link_url": Style(color="blue"),
+    "iso8601.date": Style(color="blue"),
+    "iso8601.time": Style(color="magenta"),
+    "iso8601.timezone": Style(color="yellow"),
 }
 
 
diff --git a/rich/highlighter.py b/rich/highlighter.py
index 193261c4..82293dff 100644
--- a/rich/highlighter.py
+++ b/rich/highlighter.py
@@ -140,6 +140,64 @@ class JSONHighlighter(RegexHighlighter):
                 break
 
 
+class ISO8601Highlighter(RegexHighlighter):
+    """Highlights the ISO8601 date time strings.
+    Regex reference: https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s07.html
+    """
+
+    base_style = "iso8601."
+    highlights = [
+        #
+        # Dates
+        #
+        # Calendar month (e.g. 2008-08). The hyphen is required
+        r"^(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])$",
+        # Calendar date w/o hyphens (e.g. 20080830)
+        r"^(?P<date>(?P<year>[0-9]{4})(?P<month>1[0-2]|0[1-9])(?P<day>3[01]|0[1-9]|[12][0-9]))$",
+        # Ordinal date (e.g. 2008-243). The hyphen is optional
+        r"^(?P<date>(?P<year>[0-9]{4})-?(?P<day>36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9]))$",
+        #
+        # Weeks
+        #
+        # Week of the year (e.g., 2008-W35). The hyphen is optional
+        r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))$",
+        # Week date (e.g., 2008-W35-6). The hyphens are optional
+        r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9])-?(?P<day>[1-7]))$",
+        #
+        # Times
+        #
+        # Hours and minutes (e.g., 17:21). The colon is optional
+        r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))$",
+        # Hours, minutes, and seconds w/o colons (e.g., 172159)
+        r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
+        # Time zone designator (e.g., Z, +07 or +07:00). The colons and the minutes are optional
+        r"^(?P<timezone>(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?))$",
+        # Hours, minutes, and seconds with time zone designator (e.g., 17:21:59+07:00).
+        # All the colons are optional. The minutes in the time zone designator are also optional
+        r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)$",
+        #
+        # Date and Time
+        #
+        # Calendar date with hours, minutes, and seconds (e.g., 2008-08-30 17:21:59 or 20080830 172159).
+        # A space is required between the date and the time. The hyphens and colons are optional.
+        # This regex matches dates and times that specify some hyphens or colons but omit others.
+        # This does not follow ISO 8601
+        r"^(?P<date>(?P<year>[0-9]{4})(?P<hyphen>-)?(?P<month>1[0-2]|0[1-9])(?(hyphen)-)(?P<day>3[01]|0[1-9]|[12][0-9])) (?P<time>(?P<hour>2[0-3]|[01][0-9])(?(hyphen):)(?P<minute>[0-5][0-9])(?(hyphen):)(?P<second>[0-5][0-9]))$",
+        #
+        # XML Schema dates and times
+        #
+        # Date, with optional time zone (e.g., 2008-08-30 or 2008-08-30+07:00).
+        # Hyphens are required. This is the XML Schema 'date' type
+        r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
+        # Time, with optional fractional seconds and time zone (e.g., 01:45:36 or 01:45:36.123+07:00).
+        # There is no limit on the number of digits for the fractional seconds. This is the XML Schema 'time' type
+        r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<frac>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
+        # Date and time, with optional fractional seconds and time zone (e.g., 2008-08-30T01:45:36 or 2008-08-30T01:45:36.123Z).
+        # This is the XML Schema 'dateTime' type
+        r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))T(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<ms>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
+    ]
+
+
 if __name__ == "__main__":  # pragma: no cover
     from .console import Console
 


Test patch:
diff --git a/tests/test_highlighter.py b/tests/test_highlighter.py
index 0a036b72..a9d036ba 100644
--- a/tests/test_highlighter.py
+++ b/tests/test_highlighter.py
@@ -4,7 +4,12 @@ from typing import List
 
 import pytest
 
-from rich.highlighter import JSONHighlighter, NullHighlighter, ReprHighlighter
+from rich.highlighter import (
+    JSONHighlighter,
+    NullHighlighter,
+    ReprHighlighter,
+    ISO8601Highlighter,
+)
 from rich.text import Span, Text
 
 
@@ -179,3 +184,249 @@ def test_highlight_json_no_indent():
         Span(1, 7, "json.key"),
         Span(18, 25, "json.key"),
     ]
+
+
+iso8601_highlight_tests = [
+    ("2008-08", [Span(0, 4, "iso8601.year"), Span(5, 7, "iso8601.month")]),
+    (
+        "2008-08-30",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.month"),
+            Span(8, 10, "iso8601.day"),
+        ],
+    ),
+    (
+        "20080830",
+        [
+            Span(0, 8, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(4, 6, "iso8601.month"),
+            Span(6, 8, "iso8601.day"),
+        ],
+    ),
+    (
+        "2008-243",
+        [
+            Span(0, 8, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 8, "iso8601.day"),
+        ],
+    ),
+    (
+        "2008243",
+        [
+            Span(0, 7, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(4, 7, "iso8601.day"),
+        ],
+    ),
+    (
+        "2008-W35",
+        [
+            Span(0, 8, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(6, 8, "iso8601.week"),
+        ],
+    ),
+    (
+        "2008W35",
+        [
+            Span(0, 7, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.week"),
+        ],
+    ),
+    (
+        "2008-W35-6",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(6, 8, "iso8601.week"),
+            Span(9, 10, "iso8601.day"),
+        ],
+    ),
+    (
+        "2008W356",
+        [
+            Span(0, 8, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.week"),
+            Span(7, 8, "iso8601.day"),
+        ],
+    ),
+    (
+        "17:21",
+        [
+            Span(0, 5, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(3, 5, "iso8601.minute"),
+        ],
+    ),
+    (
+        "1721",
+        [
+            Span(0, 4, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(2, 4, "iso8601.minute"),
+        ],
+    ),
+    (
+        "172159",
+        [
+            Span(0, 6, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(2, 4, "iso8601.minute"),
+            Span(4, 6, "iso8601.second"),
+        ],
+    ),
+    ("Z", [Span(0, 1, "iso8601.timezone")]),
+    ("+07", [Span(0, 3, "iso8601.timezone")]),
+    ("+07:00", [Span(0, 6, "iso8601.timezone")]),
+    (
+        "17:21:59+07:00",
+        [
+            Span(0, 8, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(3, 5, "iso8601.minute"),
+            Span(6, 8, "iso8601.second"),
+            Span(8, 14, "iso8601.timezone"),
+        ],
+    ),
+    (
+        "172159+0700",
+        [
+            Span(0, 6, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(2, 4, "iso8601.minute"),
+            Span(4, 6, "iso8601.second"),
+            Span(6, 11, "iso8601.timezone"),
+        ],
+    ),
+    (
+        "172159+07",
+        [
+            Span(0, 6, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(2, 4, "iso8601.minute"),
+            Span(4, 6, "iso8601.second"),
+            Span(6, 9, "iso8601.timezone"),
+        ],
+    ),
+    (
+        "2008-08-30 17:21:59",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(4, 5, "iso8601.hyphen"),
+            Span(5, 7, "iso8601.month"),
+            Span(8, 10, "iso8601.day"),
+            Span(11, 19, "iso8601.time"),
+            Span(11, 13, "iso8601.hour"),
+            Span(14, 16, "iso8601.minute"),
+            Span(17, 19, "iso8601.second"),
+        ],
+    ),
+    (
+        "20080830 172159",
+        [
+            Span(0, 8, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(4, 6, "iso8601.month"),
+            Span(6, 8, "iso8601.day"),
+            Span(9, 15, "iso8601.time"),
+            Span(9, 11, "iso8601.hour"),
+            Span(11, 13, "iso8601.minute"),
+            Span(13, 15, "iso8601.second"),
+        ],
+    ),
+    (
+        "2008-08-30",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.month"),
+            Span(8, 10, "iso8601.day"),
+        ],
+    ),
+    (
+        "2008-08-30+07:00",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.month"),
+            Span(8, 10, "iso8601.day"),
+            Span(10, 16, "iso8601.timezone"),
+        ],
+    ),
+    (
+        "01:45:36",
+        [
+            Span(0, 8, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(3, 5, "iso8601.minute"),
+            Span(6, 8, "iso8601.second"),
+        ],
+    ),
+    (
+        "01:45:36.123+07:00",
+        [
+            Span(0, 12, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(3, 5, "iso8601.minute"),
+            Span(6, 8, "iso8601.second"),
+            Span(8, 12, "iso8601.frac"),
+            Span(12, 18, "iso8601.timezone"),
+        ],
+    ),
+    (
+        "01:45:36.123+07:00",
+        [
+            Span(0, 12, "iso8601.time"),
+            Span(0, 2, "iso8601.hour"),
+            Span(3, 5, "iso8601.minute"),
+            Span(6, 8, "iso8601.second"),
+            Span(8, 12, "iso8601.frac"),
+            Span(12, 18, "iso8601.timezone"),
+        ],
+    ),
+    (
+        "2008-08-30T01:45:36",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.month"),
+            Span(8, 10, "iso8601.day"),
+            Span(11, 19, "iso8601.time"),
+            Span(11, 13, "iso8601.hour"),
+            Span(14, 16, "iso8601.minute"),
+            Span(17, 19, "iso8601.second"),
+        ],
+    ),
+    (
+        "2008-08-30T01:45:36.123Z",
+        [
+            Span(0, 10, "iso8601.date"),
+            Span(0, 4, "iso8601.year"),
+            Span(5, 7, "iso8601.month"),
+            Span(8, 10, "iso8601.day"),
+            Span(11, 23, "iso8601.time"),
+            Span(11, 13, "iso8601.hour"),
+            Span(14, 16, "iso8601.minute"),
+            Span(17, 19, "iso8601.second"),
+            Span(19, 23, "iso8601.ms"),
+            Span(23, 24, "iso8601.timezone"),
+        ],
+    ),
+]
+
+
+@pytest.mark.parametrize("test, spans", iso8601_highlight_tests)
+def test_highlight_iso8601_regex(test: str, spans: List[Span]):
+    """Tests for the regular expressions used in ISO8601Highlighter."""
+    text = Text(test)
+    highlighter = ISO8601Highlighter()
+    highlighter.highlight(text)
+    print(text.spans)
+    assert text.spans == spans