From 96f88f4cbe732d7ac48661ac50d9a843a1e26c2b Mon Sep 17 00:00:00 2001 From: Becky Smith Date: Wed, 25 Sep 2024 17:00:36 +0100 Subject: [PATCH] Add a formatter for log files that can render ansi colours Convert the text of the log file to HTML using ansi2html, which converts ANSI colour codes to css classes so we get the colour formatting from the original log. It produces a full HTML file, which isn't what we want to display. We just extract the
 tag which contains the log content,
and the ).*", text, flags=re.S).group(
+            "style"
+        )
+        pre_tag = re.match(r".*(?P).*", text, flags=re.S).group(
+            "pre_tag"
+        )
+        return {
+            "text": mark_safe(f"{style}{pre_tag}"),
+            "class": Path(self.filename).suffix.lstrip("."),
+        }
+
+
 FILE_RENDERERS = {
     ".csv": CSVRenderer,
-    ".log": TextRenderer,
+    ".log": LogRenderer,
     ".txt": TextRenderer,
     ".json": TextRenderer,
     ".md": TextRenderer,
diff --git a/requirements.prod.in b/requirements.prod.in
index 80a47b1f..c092513a 100644
--- a/requirements.prod.in
+++ b/requirements.prod.in
@@ -1,3 +1,4 @@
+ansi2html
 Django
 django-vite
 slippers
diff --git a/requirements.prod.txt b/requirements.prod.txt
index 6935e226..9fede944 100644
--- a/requirements.prod.txt
+++ b/requirements.prod.txt
@@ -4,6 +4,10 @@
 #
 #    pip-compile --allow-unsafe --generate-hashes --strip-extras requirements.prod.in
 #
+ansi2html==1.9.2 \
+    --hash=sha256:3453bf87535d37b827b05245faaa756dbab4ec3d69925e352b6319c3c955c0a5 \
+    --hash=sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5
+    # via -r requirements.prod.in
 asgiref==3.8.1 \
     --hash=sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47 \
     --hash=sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590
diff --git a/tests/unit/test_renderers.py b/tests/unit/test_renderers.py
index 0d813e06..e4c7dcb4 100644
--- a/tests/unit/test_renderers.py
+++ b/tests/unit/test_renderers.py
@@ -12,10 +12,12 @@
     (".png", "image/png", False, None),
     (".csv", "text/html", False, "airlock/templates/file_browser/csv.html"),
     (".txt", "text/html", False, "airlock/templates/file_browser/text.html"),
+    (".log", "text/html", False, "airlock/templates/file_browser/text.html"),
     (".html", "text/html", True, "airlock/templates/file_browser/plaintext.html"),
     (".png", "text/html", True, "airlock/templates/file_browser/plaintext.html"),
     (".csv", "text/html", True, "airlock/templates/file_browser/plaintext.html"),
     (".txt", "text/html", True, "airlock/templates/file_browser/plaintext.html"),
+    (".log", "text/html", True, "airlock/templates/file_browser/plaintext.html"),
 ]
 
 
@@ -145,3 +147,27 @@ def test_plaintext_renderer_handles_invalid_utf8(tmp_path):
     response.render()
     assert response.status_code == 200
     assert "invalid � continuation byte" in response.rendered_content
+
+
+def test_log_renderer_handles_ansi_colors(tmp_path):
+    log_file = tmp_path / "test.log"
+    # in ansi codes:
+    # \x1B[32m = foregrouund green
+    # \x1b[1m bold
+    # \x1b[0m resets formatting
+    log_file.write_bytes(
+        b"No ansi here \x1b[32m\x1b[1mThis is green and bold.\x1b[0m This is not."
+    )
+    relpath = log_file.relative_to(tmp_path)
+    Renderer = renderers.get_renderer(relpath)
+    renderer = Renderer.from_file(log_file, relpath)
+    response = renderer.get_response()
+    response.render()
+    assert response.status_code == 200
+
+    assert "ansi1 { font-weight: bold; }" in response.rendered_content
+    assert "ansi32 { color: #00aa00; }" in response.rendered_content
+    assert (
+        'This is green and bold.'
+        in response.rendered_content
+    )