Add syntax highlighting for fenced code blocks in markdown artifacts
Enables codehilite on the markdown renderer (Pygments, 'friendly' style, vendored as static/themes/pygments.css) so fenced code blocks get real syntax highlighting with zero client-side JS - matching the "themes are CSS-only" stance in framework_artifact_rendering.md. Verified live against the running dev instance: a fenced python block renders with highlighted keyword/string/function spans. This was Gap 2 from planning/2026-09-02-artifact_kind_gaps_for_frequent_agent_output.md, picked as the first fix since it needed no new kind or design decision - just enabling an existing markdown extension. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d9fc28aec0
commit
af9ecf28a9
5 changed files with 102 additions and 2 deletions
|
|
@ -502,7 +502,16 @@ def create_text_artifact(data, request, user):
|
||||||
if kind == Artifact.HTML_FULL:
|
if kind == Artifact.HTML_FULL:
|
||||||
rendered = content
|
rendered = content
|
||||||
elif kind == Artifact.MARKDOWN:
|
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")
|
(target_dir / f"{artifact_id}.md").write_text(content, encoding="utf-8")
|
||||||
elif kind == Artifact.DECK:
|
elif kind == Artifact.DECK:
|
||||||
rendered = wrap_deck(content, title, request)
|
rendered = wrap_deck(content, title, request)
|
||||||
|
|
@ -595,6 +604,7 @@ def create_binary_artifact(data, user):
|
||||||
|
|
||||||
def wrap_html(body, title, theme, request):
|
def wrap_html(body, title, theme, request):
|
||||||
stylesheet = request.build_absolute_uri(f"/static/themes/{theme}.css")
|
stylesheet = request.build_absolute_uri(f"/static/themes/{theme}.css")
|
||||||
|
pygments_stylesheet = request.build_absolute_uri("/static/themes/pygments.css")
|
||||||
return f"""<!doctype html>
|
return f"""<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -602,6 +612,7 @@ def wrap_html(body, title, theme, request):
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{html.escape(title)}</title>
|
<title>{html.escape(title)}</title>
|
||||||
<link rel="stylesheet" href="{stylesheet}">
|
<link rel="stylesheet" href="{stylesheet}">
|
||||||
|
<link rel="stylesheet" href="{pygments_stylesheet}">
|
||||||
</head>
|
</head>
|
||||||
<body class="artifact">
|
<body class="artifact">
|
||||||
{body}
|
{body}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ Currently supported kinds:
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `html_fragment` | JSON text | Wrapped with a Daybook theme | No |
|
| `html_fragment` | JSON text | Wrapped with a Daybook theme | No |
|
||||||
| `html_full` | JSON text | Stored as complete HTML | Yes |
|
| `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 |
|
| `deck` | JSON text | Wrapped as a simple HTML slide/deck page | No |
|
||||||
| `image` | Multipart file | Browser image preview | No |
|
| `image` | Multipart file | Browser image preview | No |
|
||||||
| `pdf` | Multipart file | Browser PDF preview | No |
|
| `pdf` | Multipart file | Browser PDF preview | No |
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
Django>=5.0,<6.0
|
Django>=5.0,<6.0
|
||||||
djangorestframework>=3.15,<4.0
|
djangorestframework>=3.15,<4.0
|
||||||
Markdown>=3.6,<4.0
|
Markdown>=3.6,<4.0
|
||||||
|
Pygments>=2.17,<3.0
|
||||||
gunicorn>=23.0,<24.0
|
gunicorn>=23.0,<24.0
|
||||||
whitenoise>=6.7,<7.0
|
whitenoise>=6.7,<7.0
|
||||||
|
|
|
||||||
75
static/themes/pygments.css
Normal file
75
static/themes/pygments.css
Normal file
|
|
@ -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 */
|
||||||
|
|
@ -356,3 +356,16 @@ class ArtifactAPITestCase(APITestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 400, field)
|
self.assertEqual(response.status_code, 400, field)
|
||||||
self.assertIn(field, response.data["detail"])
|
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('<span class="k">def</span>', stored_html)
|
||||||
|
self.assertIn('href="http://testserver/static/themes/pygments.css"', stored_html)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue