Repository: Textualize/rich
Pull request #3043 (2023-07-20 10:17:16)

Fix the HTML export template
This PR fixes the HTML export template, addressing the issue raised in #3021

In doing so there's also an update to the pre-commit configuration (without it I wasn't able to commit the change -- the update is stolen from the same config file in Textual).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 75b988eb..1d27aae3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -35,9 +35,10 @@ repos:
     hooks:
       - id: black
         exclude: ^benchmarks/
-  - repo: https://github.com/PyCQA/isort
-    rev: 5.10.1
+  - repo: https://github.com/pycqa/isort
+    rev: 5.12.0
     hooks:
       - id: isort
-        exclude: ^benchmarks/
+        name: isort (python)
+        language_version: '3.11'
         args: ["--profile", "black"]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2cac5fe5..bbbfecf8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed TimeElapsedColumn from showing negative.
 - Fix for escaping strings with a trailing backslash https://github.com/Textualize/rich/issues/2987
 - Fixed exception in Markdown with partial table https://github.com/Textualize/rich/issues/3053 
+- Fixed the HTML export template so that the `<html>` tag comes before the `<head>` tag https://github.com/Textualize/rich/issues/3021
 
 ### Added
 
@@ -24,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Text.tab_size now defaults to `None` to indicate that Console.tab_size should be used.
 
+
 ## [13.4.2] - 2023-06-12
 
 ### Changed
@@ -95,7 +97,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Fixed failing tests due to Pygments dependency https://github.com/Textualize/rich/issues/2757
 - Relaxed ipywidgets https://github.com/Textualize/rich/issues/2767
 
-### Added 
+### Added
 
 - Added `encoding` parameter in `Theme.read`
 
diff --git a/rich/_export_format.py b/rich/_export_format.py
index 094d2dc2..998a9b0d 100644
--- a/rich/_export_format.py
+++ b/rich/_export_format.py
@@ -1,5 +1,6 @@
 CONSOLE_HTML_FORMAT = """\
 <!DOCTYPE html>
+<html>
 <head>
 <meta charset="UTF-8">
 <style>
@@ -10,7 +11,6 @@ body {{
 }}
 </style>
 </head>
-<html>
 <body>
     <pre style="font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace"><code>{code}</code></pre>
 </body>


Test patch:
diff --git a/tests/test_console.py b/tests/test_console.py
index 990dce14..a3b73793 100644
--- a/tests/test_console.py
+++ b/tests/test_console.py
@@ -529,7 +529,7 @@ def test_export_html():
     console.print("[b]foo <script> 'test' [link=https://example.org]Click[/link]")
     html = console.export_html()
     print(repr(html))
-    expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n.r1 {font-weight: bold}\n.r2 {color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold}\n.r3 {color: #008000; text-decoration-color: #008000; font-weight: bold}\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n    <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><code><span class="r1">foo &lt;</span><span class="r2">script</span><span class="r1">&gt; </span><span class="r3">&#x27;test&#x27;</span><span class="r1"> </span><a class="r1" href="https://example.org">Click</a>\n</code></pre>\n</body>\n</html>\n'
+    expected = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<style>\n.r1 {font-weight: bold}\n.r2 {color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold}\n.r3 {color: #008000; text-decoration-color: #008000; font-weight: bold}\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<body>\n    <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><code><span class="r1">foo &lt;</span><span class="r2">script</span><span class="r1">&gt; </span><span class="r3">&#x27;test&#x27;</span><span class="r1"> </span><a class="r1" href="https://example.org">Click</a>\n</code></pre>\n</body>\n</html>\n'
     assert html == expected
 
 
@@ -538,7 +538,7 @@ def test_export_html_inline():
     console.print("[b]foo [link=https://example.org]Click[/link]")
     html = console.export_html(inline_styles=True)
     print(repr(html))
-    expected = '<!DOCTYPE html>\n<head>\n<meta charset="UTF-8">\n<style>\n\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n    <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><code><span style="font-weight: bold">foo </span><span style="font-weight: bold"><a href="https://example.org">Click</a></span>\n</code></pre>\n</body>\n</html>\n'
+    expected = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<style>\n\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<body>\n    <pre style="font-family:Menlo,\'DejaVu Sans Mono\',consolas,\'Courier New\',monospace"><code><span style="font-weight: bold">foo </span><span style="font-weight: bold"><a href="https://example.org">Click</a></span>\n</code></pre>\n</body>\n</html>\n'
     assert html == expected
 
 
@@ -591,7 +591,7 @@ def test_save_text():
 
 
 def test_save_html():
-    expected = "<!DOCTYPE html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<html>\n<body>\n    <pre style=\"font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><code>foo\n</code></pre>\n</body>\n</html>\n"
+    expected = "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style>\n\nbody {\n    color: #000000;\n    background-color: #ffffff;\n}\n</style>\n</head>\n<body>\n    <pre style=\"font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><code>foo\n</code></pre>\n</body>\n</html>\n"
     console = Console(record=True, width=100)
     console.print("foo")
     with tempfile.TemporaryDirectory() as path: