- sx_render.ml: add raw! handler to HTML renderer (inject pre-rendered content without HTML escaping) - docker-compose.yml: move SX_USE_OCAML/SX_OCAML_BIN to shared env (available to all services, not just sx_docs) - hosts/ocaml/Dockerfile: OCaml kernel build stage - shared/sx/tests/: golden test data + generator for OCaml render tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
6.3 KiB
Python
169 lines
6.3 KiB
Python
"""Generate golden HTML/aser test data from the Python evaluator.
|
|
|
|
Evaluates curated component calls through the Python ref evaluator and
|
|
writes golden_data.json — a list of {name, sx_input, expected_html,
|
|
expected_aser} triples.
|
|
|
|
Usage:
|
|
python3 shared/sx/tests/generate_golden.py
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
_project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
|
|
if _project_root not in sys.path:
|
|
sys.path.insert(0, _project_root)
|
|
|
|
|
|
# Curated test cases — component calls and plain expressions that
|
|
# exercise the rendering pipeline. Each entry is (name, sx_input).
|
|
GOLDEN_CASES = [
|
|
# --- Basic HTML rendering ---
|
|
("div_simple", '(div "hello")'),
|
|
("div_class", '(div :class "card" "content")'),
|
|
("p_text", '(p "paragraph text")'),
|
|
("nested_tags", '(div (p "a") (p "b"))'),
|
|
("void_br", "(br)"),
|
|
("void_hr", "(hr)"),
|
|
("void_img", '(img :src "/photo.jpg" :alt "A photo")'),
|
|
("void_input", '(input :type "text" :name "q" :placeholder "Search")'),
|
|
("fragment", '(<> (p "a") (p "b"))'),
|
|
("boolean_attr", '(input :type "checkbox" :checked true)'),
|
|
("nil_attr", '(div :class nil "content")'),
|
|
("empty_string_attr", '(div :class "" "visible")'),
|
|
|
|
# --- Control flow ---
|
|
("if_true", '(if true (p "yes") (p "no"))'),
|
|
("if_false", '(if false (p "yes") (p "no"))'),
|
|
("when_true", '(when true (p "shown"))'),
|
|
("when_false", '(when false (p "hidden"))'),
|
|
("let_binding", '(let ((x "hi")) (p x))'),
|
|
("let_multiple", '(let ((x "a") (y "b")) (div (p x) (p y)))'),
|
|
("cond_form", '(cond (= 1 2) (p "no") (= 1 1) (p "yes") :else (p "default"))'),
|
|
("case_form", '(case "b" "a" "A" "b" "B" :else "?")'),
|
|
("and_short", '(and true false)'),
|
|
("or_short", '(or false "found")'),
|
|
|
|
# --- Higher-order forms ---
|
|
("map_li", '(map (fn (x) (li x)) (list "a" "b" "c"))'),
|
|
("filter_even", '(filter even? (list 1 2 3 4 5))'),
|
|
("reduce_sum", '(reduce + 0 (list 1 2 3 4 5))'),
|
|
|
|
# --- String operations ---
|
|
("str_concat", '(str "hello" " " "world")'),
|
|
("str_upcase", '(upcase "hello")'),
|
|
|
|
# --- Component definitions and calls ---
|
|
("defcomp_simple",
|
|
'(do (defcomp ~test-badge (&key label) (span :class "badge" label)) (~test-badge :label "New"))'),
|
|
("defcomp_children",
|
|
'(do (defcomp ~test-wrap (&rest children) (div :class "wrap" children)) (~test-wrap (p "inside")))'),
|
|
("defcomp_multi_key",
|
|
'(do (defcomp ~test-card (&key title subtitle) (div (h2 title) (when subtitle (p subtitle)))) '
|
|
'(~test-card :title "Title" :subtitle "Sub"))'),
|
|
("defcomp_no_optional",
|
|
'(do (defcomp ~test-card2 (&key title subtitle) (div (h2 title) (when subtitle (p subtitle)))) '
|
|
'(~test-card2 :title "Only Title"))'),
|
|
|
|
# --- Nested components ---
|
|
("nested_components",
|
|
'(do (defcomp ~inner (&key text) (span :class "inner" text)) '
|
|
'(defcomp ~outer (&key title &rest children) (div :class "outer" (h2 title) children)) '
|
|
'(~outer :title "Hello" (~inner :text "World")))'),
|
|
|
|
# --- Macros ---
|
|
("macro_unless",
|
|
'(do (defmacro unless (cond &rest body) (list \'if (list \'not cond) (cons \'do body))) '
|
|
'(unless false (p "shown")))'),
|
|
|
|
# --- Special rendering patterns ---
|
|
("do_block", '(div (do (p "a") (p "b")))'),
|
|
("nil_child", '(div nil "after-nil")'),
|
|
("number_child", '(div 42)'),
|
|
("bool_child", '(div true)'),
|
|
|
|
# --- Data attributes ---
|
|
("data_attr", '(div :data-id "123" :data-name "test" "content")'),
|
|
|
|
# --- raw! (inject pre-rendered HTML) ---
|
|
("raw_simple", '(raw! "<b>bold</b>")'),
|
|
("raw_in_div", '(div (raw! "<em>italic</em>"))'),
|
|
("raw_component",
|
|
'(do (defcomp ~rich (&key html) (raw! html)) '
|
|
'(~rich :html "<p>CMS</p>"))'),
|
|
|
|
# --- Shared template components (if available) ---
|
|
("misc_error_inline",
|
|
'(do (defcomp ~shared:misc/error-inline (&key (message :as string)) '
|
|
'(div :class "text-red-600 text-sm" message)) '
|
|
'(~shared:misc/error-inline :message "Something went wrong"))'),
|
|
("misc_notification_badge",
|
|
'(do (defcomp ~shared:misc/notification-badge (&key (count :as number)) '
|
|
'(span :class "bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5" count)) '
|
|
'(~shared:misc/notification-badge :count 5))'),
|
|
("misc_cache_cleared",
|
|
'(do (defcomp ~shared:misc/cache-cleared (&key (time-str :as string)) '
|
|
'(span :class "text-green-600 font-bold" "Cache cleared at " time-str)) '
|
|
'(~shared:misc/cache-cleared :time-str "12:00"))'),
|
|
("misc_error_list_item",
|
|
'(do (defcomp ~shared:misc/error-list-item (&key (message :as string)) (li message)) '
|
|
'(~shared:misc/error-list-item :message "Bad input"))'),
|
|
("misc_fragment_error",
|
|
'(do (defcomp ~shared:misc/fragment-error (&key (service :as string)) '
|
|
'(p :class "text-sm text-red-600" "Service " (b service) " is unavailable.")) '
|
|
'(~shared:misc/fragment-error :service "blog"))'),
|
|
]
|
|
|
|
|
|
def _generate_html(sx_input: str) -> str:
|
|
"""Evaluate SX and render to HTML using the Python evaluator."""
|
|
from shared.sx.ref.sx_ref import evaluate, render
|
|
from shared.sx.parser import parse_all
|
|
|
|
env = {}
|
|
exprs = parse_all(sx_input)
|
|
|
|
# For multi-expression inputs (defcomp then call), use evaluate
|
|
# to process all defs, then render the final expression
|
|
if len(exprs) > 1:
|
|
# Evaluate all expressions — defs install into env
|
|
result = None
|
|
for expr in exprs:
|
|
result = evaluate(expr, env)
|
|
# Render the final result
|
|
return render(result, env)
|
|
else:
|
|
# Single expression — render directly
|
|
return render(exprs[0], env)
|
|
|
|
|
|
def main():
|
|
golden = []
|
|
ok = 0
|
|
failed = 0
|
|
for name, sx_input in GOLDEN_CASES:
|
|
try:
|
|
html = _generate_html(sx_input)
|
|
golden.append({
|
|
"name": name,
|
|
"sx_input": sx_input,
|
|
"expected_html": html,
|
|
})
|
|
ok += 1
|
|
except Exception as e:
|
|
print(f" SKIP {name}: {e}")
|
|
failed += 1
|
|
|
|
outpath = os.path.join(os.path.dirname(__file__), "golden_data.json")
|
|
with open(outpath, "w") as f:
|
|
json.dump(golden, f, indent=2, ensure_ascii=False)
|
|
|
|
print(f"Generated {ok} golden cases ({failed} skipped) → {outpath}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|