2 Commits

Author SHA1 Message Date
e4e43177a8 Fix code blocks + add violet bg classes to tw.css
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
- Pass :code keyword to ~doc-code and ~example-source components
  (highlighted content was positional but components use &key code)
- Rebuild tw.css (v3.4.19) with sx/sxc and sx/content in content paths
  so highlight.py classes (text-violet-600, text-rose-600, etc.) are included
- Add bg-violet-{100-500} classes for the sx app's violet menu bar
- Add highlight.py custom syntax highlighter (sx, python, bash)

IMPORTANT: tw.css must contain bg-violet-{100-500} rules for the sx
app's menu bar. Do not rebuild tw.css without ensuring violet classes
are included (via safelist or content paths).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:13:01 +00:00
8445c36270 Remove last Jinja fragment templates, use sx_components directly
Events fragment routes now call render_fragment_container_cards(),
render_fragment_account_tickets(), and render_fragment_account_bookings()
from sx_components instead of render_template(). Account sx_components
handles both SxExpr (text/sx) and HTML (text/html) fragment responses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:07:02 +00:00
9 changed files with 303 additions and 173 deletions

View File

@@ -294,26 +294,49 @@ async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str:
# Public API: Fragment pages # Public API: Fragment pages
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def render_fragment_page(ctx: dict, page_fragment_html: str) -> str: async def render_fragment_page(ctx: dict, page_fragment: str) -> str:
"""Full page: fragment-provided content.""" """Full page: fragment-provided content.
*page_fragment* may be sx source (from text/sx fragments wrapped in
SxExpr) or HTML (from text/html fragments). Sx source is embedded
directly; HTML is wrapped in ``~rich-text``.
"""
from shared.sx.parser import SxExpr
hdr = root_header_sx(ctx) hdr = root_header_sx(ctx)
hdr_child = header_child_sx(_auth_header_sx(ctx)) hdr_child = header_child_sx(_auth_header_sx(ctx))
header_rows = "(<> " + hdr + " " + hdr_child + ")" header_rows = "(<> " + hdr + " " + hdr_child + ")"
content = _fragment_content(page_fragment)
return full_page_sx(ctx, header_rows=header_rows, return full_page_sx(ctx, header_rows=header_rows,
content=f'(~rich-text :html "{_sx_escape(page_fragment_html)}")', content=content,
menu=_auth_nav_mobile_sx(ctx)) menu=_auth_nav_mobile_sx(ctx))
async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str: async def render_fragment_oob(ctx: dict, page_fragment: str) -> str:
"""OOB response for fragment pages.""" """OOB response for fragment pages."""
oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")" oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
content = _fragment_content(page_fragment)
return oob_page_sx(oobs=oobs, return oob_page_sx(oobs=oobs,
content=f'(~rich-text :html "{_sx_escape(page_fragment_html)}")', content=content,
menu=_auth_nav_mobile_sx(ctx)) menu=_auth_nav_mobile_sx(ctx))
def _fragment_content(frag: object) -> str:
"""Convert a fragment response to sx content string.
SxExpr (from text/sx responses) is embedded as-is; plain strings
(from text/html) are wrapped in ``~rich-text``.
"""
from shared.sx.parser import SxExpr
if isinstance(frag, SxExpr):
return frag.source
s = str(frag) if frag else ""
if not s:
return ""
return f'(~rich-text :html "{_sx_escape(s)}")'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public API: Auth pages (login, device) # Public API: Auth pages (login, device)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -3,16 +3,17 @@
Exposes sx fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
Most handlers are defined declaratively in .sx files under All handlers are defined declaratively in .sx files under
``events/sx/handlers/`` and dispatched via the sx handler registry. ``events/sx/handlers/`` and dispatched via the sx handler registry.
Jinja HTML handlers (container-cards, account-page) remain as Python container-cards and account-page remain as Python handlers because they
because they return ``text/html`` templates, not sx source. call domain service methods and return batched/conditional content, but
they use sx_call() for rendering (no Jinja templates).
""" """
from __future__ import annotations from __future__ import annotations
from quart import Blueprint, Response, g, render_template, request from quart import Blueprint, Response, g, request
from shared.infrastructure.fragments import FRAGMENT_HEADER from shared.infrastructure.fragments import FRAGMENT_HEADER
from shared.services.registry import services from shared.services.registry import services
@@ -24,8 +25,8 @@ def register():
_handlers: dict[str, object] = {} _handlers: dict[str, object] = {}
# Fragment types that return HTML (Jinja templates) # Fragment types that return HTML (comment-delimited batch)
_html_types = {"container-cards", "account-page"} _html_types = {"container-cards"}
@bp.before_request @bp.before_request
async def _require_fragment_header(): async def _require_fragment_header():
@@ -34,7 +35,7 @@ def register():
@bp.get("/<fragment_type>") @bp.get("/<fragment_type>")
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
# 1. Check Python handlers first (Jinja HTML types) # 1. Check Python handlers first
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is not None: if handler is not None:
result = await handler() result = await handler()
@@ -51,9 +52,13 @@ def register():
return Response("", status=200, content_type="text/sx") return Response("", status=200, content_type="text/sx")
# --- container-cards fragment: entries for blog listing cards (Jinja HTML) -- # --- container-cards fragment: entries for blog listing cards -----------
# Returns text/html with <!-- card-widget:POST_ID --> comment markers
# so the blog consumer can split per-post fragments.
async def _container_cards_handler(): async def _container_cards_handler():
from sx.sx_components import render_fragment_container_cards
post_ids_raw = request.args.get("post_ids", "") post_ids_raw = request.args.get("post_ids", "")
post_slugs_raw = request.args.get("post_slugs", "") post_slugs_raw = request.args.get("post_slugs", "")
post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()] post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()]
@@ -66,16 +71,19 @@ def register():
slug_map[pid] = post_slugs[i] if i < len(post_slugs) else "" slug_map[pid] = post_slugs[i] if i < len(post_slugs) else ""
batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids) batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids)
return await render_template( return render_fragment_container_cards(batch, post_ids, slug_map)
"fragments/container_cards_entries.html",
batch=batch, post_ids=post_ids, slug_map=slug_map,
)
_handlers["container-cards"] = _container_cards_handler _handlers["container-cards"] = _container_cards_handler
# --- account-page fragment: tickets or bookings panel (Jinja HTML) ------ # --- account-page fragment: tickets or bookings panel ------------------
# Returns text/sx — the account app embeds this as sx source.
async def _account_page_handler(): async def _account_page_handler():
from sx.sx_components import (
render_fragment_account_tickets,
render_fragment_account_bookings,
)
slug = request.args.get("slug", "") slug = request.args.get("slug", "")
user_id = request.args.get("user_id", type=int) user_id = request.args.get("user_id", type=int)
if not user_id: if not user_id:
@@ -83,16 +91,10 @@ def register():
if slug == "tickets": if slug == "tickets":
tickets = await services.calendar.user_tickets(g.s, user_id=user_id) tickets = await services.calendar.user_tickets(g.s, user_id=user_id)
return await render_template( return render_fragment_account_tickets(tickets)
"fragments/account_page_tickets.html",
tickets=tickets,
)
elif slug == "bookings": elif slug == "bookings":
bookings = await services.calendar.user_bookings(g.s, user_id=user_id) bookings = await services.calendar.user_bookings(g.s, user_id=user_id)
return await render_template( return render_fragment_account_bookings(bookings)
"fragments/account_page_bookings.html",
bookings=bookings,
)
return "" return ""
_handlers["account-page"] = _account_page_handler _handlers["account-page"] = _account_page_handler

View File

@@ -1,23 +0,0 @@
{# Account nav items: tickets + bookings links for the account dashboard #}
<div class="relative nav-group">
<a href="{{ account_url('/tickets/') }}"
sx-get="{{ account_url('/tickets/') }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
class="{{styles.nav_button}}">
tickets
</a>
</div>
<div class="relative nav-group">
<a href="{{ account_url('/bookings/') }}"
sx-get="{{ account_url('/bookings/') }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
class="{{styles.nav_button}}">
bookings
</a>
</div>

View File

@@ -1,44 +0,0 @@
<div class="w-full max-w-3xl mx-auto px-4 py-6">
<div class="bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6">
<h1 class="text-xl font-semibold tracking-tight">Bookings</h1>
{% if bookings %}
<div class="divide-y divide-stone-100">
{% for booking in bookings %}
<div class="py-4 first:pt-0 last:pb-0">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-stone-800">{{ booking.name }}</p>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500">
<span>{{ booking.start_at.strftime('%d %b %Y, %H:%M') }}</span>
{% if booking.end_at %}
<span>&ndash; {{ booking.end_at.strftime('%H:%M') }}</span>
{% endif %}
{% if booking.calendar_name %}
<span>&middot; {{ booking.calendar_name }}</span>
{% endif %}
{% if booking.cost %}
<span>&middot; &pound;{{ booking.cost }}</span>
{% endif %}
</div>
</div>
<div class="flex-shrink-0">
{% if booking.state == 'confirmed' %}
<span class="inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700">confirmed</span>
{% elif booking.state == 'provisional' %}
<span class="inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700">provisional</span>
{% else %}
<span class="inline-flex items-center rounded-full bg-stone-50 border border-stone-200 px-2.5 py-0.5 text-xs font-medium text-stone-600">{{ booking.state }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-sm text-stone-500">No bookings yet.</p>
{% endif %}
</div>
</div>

View File

@@ -1,44 +0,0 @@
<div class="w-full max-w-3xl mx-auto px-4 py-6">
<div class="bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6">
<h1 class="text-xl font-semibold tracking-tight">Tickets</h1>
{% if tickets %}
<div class="divide-y divide-stone-100">
{% for ticket in tickets %}
<div class="py-4 first:pt-0 last:pb-0">
<div class="flex items-start justify-between gap-4">
<div class="min-w-0 flex-1">
<a href="{{ events_url('/tickets/' ~ ticket.code ~ '/') }}"
class="text-sm font-medium text-stone-800 hover:text-emerald-700 transition">
{{ ticket.entry_name }}
</a>
<div class="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-stone-500">
<span>{{ ticket.entry_start_at.strftime('%d %b %Y, %H:%M') }}</span>
{% if ticket.calendar_name %}
<span>&middot; {{ ticket.calendar_name }}</span>
{% endif %}
{% if ticket.ticket_type_name %}
<span>&middot; {{ ticket.ticket_type_name }}</span>
{% endif %}
</div>
</div>
<div class="flex-shrink-0">
{% if ticket.state == 'checked_in' %}
<span class="inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-2.5 py-0.5 text-xs font-medium text-blue-700">checked in</span>
{% elif ticket.state == 'confirmed' %}
<span class="inline-flex items-center rounded-full bg-emerald-50 border border-emerald-200 px-2.5 py-0.5 text-xs font-medium text-emerald-700">confirmed</span>
{% else %}
<span class="inline-flex items-center rounded-full bg-amber-50 border border-amber-200 px-2.5 py-0.5 text-xs font-medium text-amber-700">{{ ticket.state }}</span>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-sm text-stone-500">No tickets yet.</p>
{% endif %}
</div>
</div>

View File

@@ -1,33 +0,0 @@
{# Calendar entries for blog listing cards — served as fragment from events app.
Each post's entries are delimited by comment markers so the consumer can
extract per-post HTML via simple string splitting. #}
{% for post_id in post_ids %}
<!-- card-widget:{{ post_id }} -->
{% set widget_entries = batch.get(post_id, []) %}
{% if widget_entries %}
<div class="mt-4 mb-2">
<h3 class="text-sm font-semibold text-stone-700 mb-2 px-2">Events:</h3>
<div class="overflow-x-auto scrollbar-hide" style="scroll-behavior: smooth;">
<div class="flex gap-2 px-2">
{% for entry in widget_entries %}
{% set _post_slug = slug_map.get(post_id, '') %}
{% set _entry_path = '/' + _post_slug + '/' + entry.calendar_slug + '/' + entry.start_at.year|string + '/' + entry.start_at.month|string + '/' + entry.start_at.day|string + '/entries/' + entry.id|string + '/' %}
<a
href="{{ events_url(_entry_path) }}"
class="flex flex-col gap-1 px-3 py-2 bg-stone-50 hover:bg-stone-100 rounded border border-stone-200 transition text-sm whitespace-nowrap flex-shrink-0 min-w-[180px]">
<div class="font-medium text-stone-900 truncate">{{ entry.name }}</div>
<div class="text-xs text-stone-600">
{{ entry.start_at.strftime('%a, %b %d') }}
</div>
<div class="text-xs text-stone-500">
{{ entry.start_at.strftime('%H:%M') }}
{% if entry.end_at %} {{ entry.end_at.strftime('%H:%M') }}{% endif %}
</div>
</a>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- /card-widget:{{ post_id }} -->
{% endfor %}

File diff suppressed because one or more lines are too long

249
sx/content/highlight.py Normal file
View File

@@ -0,0 +1,249 @@
"""Syntax highlighting using Tailwind classes.
Produces sx source with coloured spans — no external CSS dependencies.
Showcases the on-demand CSS system.
"""
from __future__ import annotations
import re
def highlight_sx(code: str) -> str:
"""Highlight s-expression source code as sx with Tailwind spans."""
tokens = _tokenize_sx(code)
parts = []
for kind, text in tokens:
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
if kind == "comment":
parts.append(f'(span :class "text-stone-400 italic" "{escaped}")')
elif kind == "string":
parts.append(f'(span :class "text-emerald-700" "{escaped}")')
elif kind == "keyword":
parts.append(f'(span :class "text-violet-600" "{escaped}")')
elif kind == "component":
parts.append(f'(span :class "text-rose-600 font-semibold" "{escaped}")')
elif kind == "special":
parts.append(f'(span :class "text-sky-700 font-semibold" "{escaped}")')
elif kind == "paren":
parts.append(f'(span :class "text-stone-400" "{escaped}")')
elif kind == "number":
parts.append(f'(span :class "text-amber-700" "{escaped}")')
elif kind == "boolean":
parts.append(f'(span :class "text-orange-600" "{escaped}")')
else:
parts.append(f'(span "{escaped}")')
return "(<> " + " ".join(parts) + ")"
_SX_SPECIALS = {
"defcomp", "defrelation", "define",
"if", "when", "cond", "case", "and", "or", "not",
"let", "let*", "lambda", "fn",
"do", "begin", "quote",
"->", "map", "filter", "reduce", "some", "every?",
"map-indexed", "for-each",
"&key", "&rest",
}
_SX_TOKEN_RE = re.compile(
r'(;[^\n]*)' # comment
r'|("(?:[^"\\]|\\.)*")' # string
r'|(:[a-zA-Z][\w?!-]*)' # keyword
r'|(~[\w-]+)' # component
r'|([()[\]{}])' # parens/brackets
r'|(\d+\.?\d*)' # number
r'|(true|false|nil)\b' # boolean/nil
r'|([\w?!+\-*/<>=&.]+)' # symbol
r'|(\s+)' # whitespace
r'|(.)' # other
)
def _tokenize_sx(code: str) -> list[tuple[str, str]]:
tokens = []
for m in _SX_TOKEN_RE.finditer(code):
if m.group(1):
tokens.append(("comment", m.group(1)))
elif m.group(2):
tokens.append(("string", m.group(2)))
elif m.group(3):
tokens.append(("keyword", m.group(3)))
elif m.group(4):
tokens.append(("component", m.group(4)))
elif m.group(5):
tokens.append(("paren", m.group(5)))
elif m.group(6):
tokens.append(("number", m.group(6)))
elif m.group(7):
tokens.append(("boolean", m.group(7)))
elif m.group(8):
text = m.group(8)
if text in _SX_SPECIALS:
tokens.append(("special", text))
else:
tokens.append(("symbol", text))
elif m.group(9):
tokens.append(("ws", m.group(9)))
else:
tokens.append(("other", m.group(10)))
return tokens
def highlight_python(code: str) -> str:
"""Highlight Python source code as sx with Tailwind spans."""
tokens = _tokenize_python(code)
parts = []
for kind, text in tokens:
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
if kind == "comment":
parts.append(f'(span :class "text-stone-400 italic" "{escaped}")')
elif kind == "string":
parts.append(f'(span :class "text-emerald-700" "{escaped}")')
elif kind == "keyword":
parts.append(f'(span :class "text-violet-600 font-semibold" "{escaped}")')
elif kind == "builtin":
parts.append(f'(span :class "text-sky-700" "{escaped}")')
elif kind == "decorator":
parts.append(f'(span :class "text-amber-600" "{escaped}")')
elif kind == "number":
parts.append(f'(span :class "text-amber-700" "{escaped}")')
else:
parts.append(f'(span "{escaped}")')
return "(<> " + " ".join(parts) + ")"
_PY_KEYWORDS = {
"False", "None", "True", "and", "as", "assert", "async", "await",
"break", "class", "continue", "def", "del", "elif", "else", "except",
"finally", "for", "from", "global", "if", "import", "in", "is",
"lambda", "nonlocal", "not", "or", "pass", "raise", "return",
"try", "while", "with", "yield",
}
_PY_BUILTINS = {
"print", "len", "range", "str", "int", "float", "list", "dict",
"set", "tuple", "type", "isinstance", "getattr", "setattr", "hasattr",
"super", "property", "staticmethod", "classmethod", "enumerate", "zip",
"map", "filter", "sorted", "reversed", "any", "all", "min", "max",
"abs", "sum", "open", "input", "format", "repr", "id", "hash",
}
_PY_TOKEN_RE = re.compile(
r'(#[^\n]*)' # comment
r'|("""[\s\S]*?"""|\'\'\'[\s\S]*?\'\'\')' # triple-quoted string
r'|("(?:[^"\\]|\\.)*")' # double-quoted string
r"|('(?:[^'\\]|\\.)*')" # single-quoted string
r'|(@\w+)' # decorator
r'|(\d+\.?\d*(?:e[+-]?\d+)?)' # number
r'|([a-zA-Z_]\w*)' # identifier
r'|(\s+)' # whitespace
r'|(.)' # other
)
def _tokenize_python(code: str) -> list[tuple[str, str]]:
tokens = []
for m in _PY_TOKEN_RE.finditer(code):
if m.group(1):
tokens.append(("comment", m.group(1)))
elif m.group(2):
tokens.append(("string", m.group(2)))
elif m.group(3):
tokens.append(("string", m.group(3)))
elif m.group(4):
tokens.append(("string", m.group(4)))
elif m.group(5):
tokens.append(("decorator", m.group(5)))
elif m.group(6):
tokens.append(("number", m.group(6)))
elif m.group(7):
text = m.group(7)
if text in _PY_KEYWORDS:
tokens.append(("keyword", text))
elif text in _PY_BUILTINS:
tokens.append(("builtin", text))
else:
tokens.append(("ident", text))
elif m.group(8):
tokens.append(("ws", m.group(8)))
else:
tokens.append(("other", m.group(9)))
return tokens
def highlight_bash(code: str) -> str:
"""Highlight bash source code as sx with Tailwind spans."""
tokens = _tokenize_bash(code)
parts = []
for kind, text in tokens:
escaped = text.replace("\\", "\\\\").replace('"', '\\"')
if kind == "comment":
parts.append(f'(span :class "text-stone-400 italic" "{escaped}")')
elif kind == "string":
parts.append(f'(span :class "text-emerald-700" "{escaped}")')
elif kind == "variable":
parts.append(f'(span :class "text-violet-600" "{escaped}")')
elif kind == "keyword":
parts.append(f'(span :class "text-sky-700 font-semibold" "{escaped}")')
elif kind == "flag":
parts.append(f'(span :class "text-amber-700" "{escaped}")')
else:
parts.append(f'(span "{escaped}")')
return "(<> " + " ".join(parts) + ")"
_BASH_KEYWORDS = {
"if", "then", "else", "elif", "fi", "for", "while", "do", "done",
"case", "esac", "in", "function", "return", "exit", "export",
"source", "local", "readonly", "declare", "set", "unset",
}
_BASH_TOKEN_RE = re.compile(
r'(#[^\n]*)' # comment
r'|("(?:[^"\\]|\\.)*")' # double-quoted string
r"|('(?:[^'\\]|\\.)*')" # single-quoted string
r'|(\$\{?\w+\}?|\$\()' # variable
r'|(--?[\w-]+)' # flag
r'|([a-zA-Z_][\w-]*)' # word
r'|(\s+)' # whitespace
r'|(.)' # other
)
def _tokenize_bash(code: str) -> list[tuple[str, str]]:
tokens = []
for m in _BASH_TOKEN_RE.finditer(code):
if m.group(1):
tokens.append(("comment", m.group(1)))
elif m.group(2):
tokens.append(("string", m.group(2)))
elif m.group(3):
tokens.append(("string", m.group(3)))
elif m.group(4):
tokens.append(("variable", m.group(4)))
elif m.group(5):
tokens.append(("flag", m.group(5)))
elif m.group(6):
text = m.group(6)
if text in _BASH_KEYWORDS:
tokens.append(("keyword", text))
else:
tokens.append(("word", text))
elif m.group(7):
tokens.append(("ws", m.group(7)))
else:
tokens.append(("other", m.group(8)))
return tokens
def highlight(code: str, language: str = "lisp") -> str:
"""Highlight code in the given language. Returns sx source."""
if language in ("lisp", "sx", "sexp"):
return highlight_sx(code)
elif language in ("python", "py"):
return highlight_python(code)
elif language in ("bash", "sh", "shell"):
return highlight_bash(code)
# Fallback: no highlighting, just escaped text
escaped = code.replace("\\", "\\\\").replace('"', '\\"')
return f'(span "{escaped}")'

View File

@@ -25,13 +25,13 @@ def _full_page(ctx: dict, **kwargs) -> str:
def _code(code: str, language: str = "lisp") -> str: def _code(code: str, language: str = "lisp") -> str:
"""Build a ~doc-code component with highlighted content.""" """Build a ~doc-code component with highlighted content."""
highlighted = highlight(code, language) highlighted = highlight(code, language)
return f'(~doc-code {highlighted})' return f'(~doc-code :code {highlighted})'
def _example_code(code: str) -> str: def _example_code(code: str) -> str:
"""Build an ~example-source component with highlighted content.""" """Build an ~example-source component with highlighted content."""
highlighted = highlight(code, "lisp") highlighted = highlight(code, "lisp")
return f'(~example-source {highlighted})' return f'(~example-source :code {highlighted})'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------