Add admin preview views + fix markdown converter
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m31s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m31s
- Fix _markdown() in lexical_to_sx.py: render markdown to HTML with mistune.html() before storing in ~kg-html - Add shared/sx/prettify.py: sx_to_pretty_sx and json_to_pretty_sx produce sx AST for syntax-highlighted DOM (uses canonical serialize()) - Add preview tab to admin header nav - Add GET /preview/ route with 4 views: prettified sx, prettified lexical JSON, sx rendered HTML, lexical rendered HTML - Add ~blog-preview-panel and ~blog-preview-section components - Add syntax highlight CSS for sx/JSON tokens Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -205,6 +205,7 @@ def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
|
||||
("cart_url", f"/{slug}/admin/payments/", "payments"),
|
||||
("blog_url", f"/{slug}/admin/entries/", "entries"),
|
||||
("blog_url", f"/{slug}/admin/data/", "data"),
|
||||
("blog_url", f"/{slug}/admin/preview/", "preview"),
|
||||
("blog_url", f"/{slug}/admin/edit/", "edit"),
|
||||
("blog_url", f"/{slug}/admin/settings/", "settings"),
|
||||
]
|
||||
|
||||
149
shared/sx/prettify.py
Normal file
149
shared/sx/prettify.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
Prettifiers that produce s-expression source for syntax-highlighted DOM.
|
||||
|
||||
``sx_to_pretty_sx(source)`` — parse sx, emit sx that renders as coloured DOM.
|
||||
``json_to_pretty_sx(json_str)`` — parse JSON, emit sx that renders as coloured DOM.
|
||||
|
||||
The output is *not* HTML — it's sx source that, when evaluated and rendered by
|
||||
the sx engine, produces ``<pre>`` blocks with ``<span>`` elements carrying
|
||||
CSS classes for syntax highlighting.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from .parser import parse, serialize
|
||||
from .types import Keyword, Symbol, NIL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers — build sx AST (lists), then serialize once at the end
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _span(cls: str, text: str) -> list:
|
||||
"""Build an sx AST node: (span :class "cls" "text")."""
|
||||
return [Symbol("span"), Keyword("class"), cls, text]
|
||||
|
||||
|
||||
def _str_display(cls: str, value: str) -> list:
|
||||
"""Build a span showing a quoted string value.
|
||||
|
||||
Uses curly quotes so the display delimiters don't conflict with
|
||||
sx string syntax.
|
||||
"""
|
||||
return [Symbol("span"), Keyword("class"), cls,
|
||||
[Symbol("span"), Keyword("class"), f"{cls}-q", "\u201c"],
|
||||
value,
|
||||
[Symbol("span"), Keyword("class"), f"{cls}-q", "\u201d"]]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# S-expression prettifier
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def sx_to_pretty_sx(source: str) -> str:
|
||||
"""Parse *source* as sx and return sx source that renders as highlighted DOM."""
|
||||
try:
|
||||
expr = parse(source)
|
||||
except Exception:
|
||||
return serialize([Symbol("pre"), Keyword("class"), "sx-pretty", source])
|
||||
inner = _sx_node(expr, depth=0)
|
||||
return serialize([Symbol("pre"), Keyword("class"), "sx-pretty", inner])
|
||||
|
||||
|
||||
def _sx_node(expr: Any, depth: int) -> list:
|
||||
"""Recursively convert a parsed sx value to an sx AST for pretty display."""
|
||||
if isinstance(expr, list):
|
||||
if not expr:
|
||||
return [_span("sx-paren", "("), _span("sx-paren", ")")]
|
||||
return _sx_list(expr, depth)
|
||||
if isinstance(expr, Symbol):
|
||||
return _span("sx-sym", expr.name)
|
||||
if isinstance(expr, Keyword):
|
||||
return _span("sx-kw", f":{expr.name}")
|
||||
if isinstance(expr, str):
|
||||
return _str_display("sx-str", expr)
|
||||
if isinstance(expr, bool):
|
||||
return _span("sx-bool", "true" if expr else "false")
|
||||
if isinstance(expr, (int, float)):
|
||||
return _span("sx-num", str(expr))
|
||||
if expr is None or expr is NIL:
|
||||
return _span("sx-sym", "nil")
|
||||
return _span("sx-sym", str(expr))
|
||||
|
||||
|
||||
def _sx_list(items: list, depth: int) -> list:
|
||||
"""Format a list as a prettified sx AST node."""
|
||||
children: list = []
|
||||
for item in items:
|
||||
children.append(_sx_node(item, depth + 1))
|
||||
indent_style = f"margin-left: {depth * 16}px"
|
||||
return [Symbol("div"), Keyword("class"), "sx-list",
|
||||
Keyword("style"), indent_style,
|
||||
_span("sx-paren", "("),
|
||||
*children,
|
||||
_span("sx-paren", ")")]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# JSON prettifier
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def json_to_pretty_sx(json_str: str) -> str:
|
||||
"""Parse *json_str* as JSON and return sx source that renders as highlighted DOM."""
|
||||
try:
|
||||
data = json.loads(json_str)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return serialize([Symbol("pre"), Keyword("class"), "json-pretty",
|
||||
json_str or ""])
|
||||
inner = _json_node(data, depth=0)
|
||||
return serialize([Symbol("pre"), Keyword("class"), "json-pretty", inner])
|
||||
|
||||
|
||||
def _json_node(val: Any, depth: int) -> list:
|
||||
"""Recursively convert a JSON value to an sx AST for pretty display."""
|
||||
if isinstance(val, dict):
|
||||
return _json_object(val, depth)
|
||||
if isinstance(val, list):
|
||||
return _json_array(val, depth)
|
||||
if isinstance(val, str):
|
||||
return _str_display("json-str", val)
|
||||
if isinstance(val, bool):
|
||||
return _span("json-lit", "true" if val else "false")
|
||||
if val is None:
|
||||
return _span("json-lit", "null")
|
||||
if isinstance(val, (int, float)):
|
||||
return _span("json-num", str(val))
|
||||
return _span("json-str", str(val))
|
||||
|
||||
|
||||
def _json_object(obj: dict, depth: int) -> list:
|
||||
if not obj:
|
||||
return [_span("json-brace", "{"), _span("json-brace", "}")]
|
||||
indent_style = f"margin-left: {depth * 16}px"
|
||||
fields: list = []
|
||||
for key, val in obj.items():
|
||||
key_node = _str_display("json-key", key)
|
||||
val_node = _json_node(val, depth + 1)
|
||||
fields.append([Symbol("div"), Keyword("class"), "json-field",
|
||||
key_node, ": ", val_node])
|
||||
return [Symbol("div"), Keyword("class"), "json-obj",
|
||||
Keyword("style"), indent_style,
|
||||
_span("json-brace", "{"),
|
||||
*fields,
|
||||
_span("json-brace", "}")]
|
||||
|
||||
|
||||
def _json_array(arr: list, depth: int) -> list:
|
||||
if not arr:
|
||||
return [_span("json-bracket", "["), _span("json-bracket", "]")]
|
||||
indent_style = f"margin-left: {depth * 16}px"
|
||||
items: list = []
|
||||
for item in arr:
|
||||
items.append(_json_node(item, depth + 1))
|
||||
return [Symbol("div"), Keyword("class"), "json-arr",
|
||||
Keyword("style"), indent_style,
|
||||
_span("json-bracket", "["),
|
||||
*items,
|
||||
_span("json-bracket", "]")]
|
||||
Reference in New Issue
Block a user