diff --git a/artifacts/views.py b/artifacts/views.py index f9eaf4b..faab737 100644 --- a/artifacts/views.py +++ b/artifacts/views.py @@ -502,7 +502,16 @@ def create_text_artifact(data, request, user): if kind == Artifact.HTML_FULL: rendered = content elif kind == Artifact.MARKDOWN: - rendered = wrap_html(markdown.markdown(content, extensions=["extra", "sane_lists"]), title, theme, request) + rendered = wrap_html( + markdown.markdown( + content, + extensions=["extra", "sane_lists", "codehilite"], + extension_configs={"codehilite": {"guess_lang": False}}, + ), + title, + theme, + request, + ) (target_dir / f"{artifact_id}.md").write_text(content, encoding="utf-8") elif kind == Artifact.DECK: rendered = wrap_deck(content, title, request) @@ -595,6 +604,7 @@ def create_binary_artifact(data, user): def wrap_html(body, title, theme, request): stylesheet = request.build_absolute_uri(f"/static/themes/{theme}.css") + pygments_stylesheet = request.build_absolute_uri("/static/themes/pygments.css") return f""" @@ -602,6 +612,7 @@ def wrap_html(body, title, theme, request): {html.escape(title)} + {body} diff --git a/docs/api.md b/docs/api.md index f2a986b..5001d8a 100644 --- a/docs/api.md +++ b/docs/api.md @@ -30,7 +30,7 @@ Currently supported kinds: |---|---|---|---| | `html_fragment` | JSON text | Wrapped with a Daybook theme | No | | `html_full` | JSON text | Stored as complete HTML | Yes | -| `markdown` | JSON text | Rendered to HTML and original `.md` is preserved | Yes | +| `markdown` | JSON text | Rendered to HTML (fenced code blocks get syntax highlighting) and original `.md` is preserved | Yes | | `deck` | JSON text | Wrapped as a simple HTML slide/deck page | No | | `image` | Multipart file | Browser image preview | No | | `pdf` | Multipart file | Browser PDF preview | No | diff --git a/requirements.txt b/requirements.txt index 696bd2b..9da4882 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ Django>=5.0,<6.0 djangorestframework>=3.15,<4.0 Markdown>=3.6,<4.0 +Pygments>=2.17,<3.0 gunicorn>=23.0,<24.0 whitenoise>=6.7,<7.0 diff --git a/static/themes/pygments.css b/static/themes/pygments.css new file mode 100644 index 0000000..4a1e617 --- /dev/null +++ b/static/themes/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: #666666; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: #666666; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.codehilite .hll { background-color: #ffffcc } +.codehilite { background: #f0f0f0; } +.codehilite .c { color: #60A0B0; font-style: italic } /* Comment */ +.codehilite .err { border: 1px solid #F00 } /* Error */ +.codehilite .k { color: #007020; font-weight: bold } /* Keyword */ +.codehilite .o { color: #666 } /* Operator */ +.codehilite .ch { color: #60A0B0; font-style: italic } /* Comment.Hashbang */ +.codehilite .cm { color: #60A0B0; font-style: italic } /* Comment.Multiline */ +.codehilite .cp { color: #007020 } /* Comment.Preproc */ +.codehilite .cpf { color: #60A0B0; font-style: italic } /* Comment.PreprocFile */ +.codehilite .c1 { color: #60A0B0; font-style: italic } /* Comment.Single */ +.codehilite .cs { color: #60A0B0; background-color: #FFF0F0 } /* Comment.Special */ +.codehilite .gd { color: #A00000 } /* Generic.Deleted */ +.codehilite .ge { font-style: italic } /* Generic.Emph */ +.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.codehilite .gr { color: #F00 } /* Generic.Error */ +.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.codehilite .gi { color: #00A000 } /* Generic.Inserted */ +.codehilite .go { color: #888 } /* Generic.Output */ +.codehilite .gp { color: #C65D09; font-weight: bold } /* Generic.Prompt */ +.codehilite .gs { font-weight: bold } /* Generic.Strong */ +.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.codehilite .gt { color: #04D } /* Generic.Traceback */ +.codehilite .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.codehilite .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.codehilite .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.codehilite .kp { color: #007020 } /* Keyword.Pseudo */ +.codehilite .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.codehilite .kt { color: #902000 } /* Keyword.Type */ +.codehilite .m { color: #40A070 } /* Literal.Number */ +.codehilite .s { color: #4070A0 } /* Literal.String */ +.codehilite .na { color: #4070A0 } /* Name.Attribute */ +.codehilite .nb { color: #007020 } /* Name.Builtin */ +.codehilite .nc { color: #0E84B5; font-weight: bold } /* Name.Class */ +.codehilite .no { color: #60ADD5 } /* Name.Constant */ +.codehilite .nd { color: #555; font-weight: bold } /* Name.Decorator */ +.codehilite .ni { color: #D55537; font-weight: bold } /* Name.Entity */ +.codehilite .ne { color: #007020 } /* Name.Exception */ +.codehilite .nf { color: #06287E } /* Name.Function */ +.codehilite .nl { color: #002070; font-weight: bold } /* Name.Label */ +.codehilite .nn { color: #0E84B5; font-weight: bold } /* Name.Namespace */ +.codehilite .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.codehilite .nv { color: #BB60D5 } /* Name.Variable */ +.codehilite .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.codehilite .w { color: #BBB } /* Text.Whitespace */ +.codehilite .mb { color: #40A070 } /* Literal.Number.Bin */ +.codehilite .mf { color: #40A070 } /* Literal.Number.Float */ +.codehilite .mh { color: #40A070 } /* Literal.Number.Hex */ +.codehilite .mi { color: #40A070 } /* Literal.Number.Integer */ +.codehilite .mo { color: #40A070 } /* Literal.Number.Oct */ +.codehilite .sa { color: #4070A0 } /* Literal.String.Affix */ +.codehilite .sb { color: #4070A0 } /* Literal.String.Backtick */ +.codehilite .sc { color: #4070A0 } /* Literal.String.Char */ +.codehilite .dl { color: #4070A0 } /* Literal.String.Delimiter */ +.codehilite .sd { color: #4070A0; font-style: italic } /* Literal.String.Doc */ +.codehilite .s2 { color: #4070A0 } /* Literal.String.Double */ +.codehilite .se { color: #4070A0; font-weight: bold } /* Literal.String.Escape */ +.codehilite .sh { color: #4070A0 } /* Literal.String.Heredoc */ +.codehilite .si { color: #70A0D0; font-style: italic } /* Literal.String.Interpol */ +.codehilite .sx { color: #C65D09 } /* Literal.String.Other */ +.codehilite .sr { color: #235388 } /* Literal.String.Regex */ +.codehilite .s1 { color: #4070A0 } /* Literal.String.Single */ +.codehilite .ss { color: #517918 } /* Literal.String.Symbol */ +.codehilite .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.codehilite .fm { color: #06287E } /* Name.Function.Magic */ +.codehilite .vc { color: #BB60D5 } /* Name.Variable.Class */ +.codehilite .vg { color: #BB60D5 } /* Name.Variable.Global */ +.codehilite .vi { color: #BB60D5 } /* Name.Variable.Instance */ +.codehilite .vm { color: #BB60D5 } /* Name.Variable.Magic */ +.codehilite .il { color: #40A070 } /* Literal.Number.Integer.Long */ diff --git a/tests/api/test_artifacts.py b/tests/api/test_artifacts.py index 333b527..eac2119 100644 --- a/tests/api/test_artifacts.py +++ b/tests/api/test_artifacts.py @@ -356,3 +356,16 @@ class ArtifactAPITestCase(APITestCase): ) self.assertEqual(response.status_code, 400, field) self.assertIn(field, response.data["detail"]) + + # -- syntax highlighting ------------------------------------------------- + + def test_markdown_fenced_code_block_gets_syntax_highlighted(self): + response = self.create_artifact( + content="# Test\n\n```python\ndef hello():\n return \"world\"\n```" + ) + artifact = Artifact.objects.get(pk=response.data["id"]) + stored_html = (self.artifacts_root / artifact.path).read_text(encoding="utf-8") + + self.assertIn('class="codehilite"', stored_html) + self.assertIn('def', stored_html) + self.assertIn('href="http://testserver/static/themes/pygments.css"', stored_html)