- 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>
232 lines
7.5 KiB
JSON
232 lines
7.5 KiB
JSON
[
|
|
{
|
|
"name": "div_simple",
|
|
"sx_input": "(div \"hello\")",
|
|
"expected_html": "<div>hello</div>"
|
|
},
|
|
{
|
|
"name": "div_class",
|
|
"sx_input": "(div :class \"card\" \"content\")",
|
|
"expected_html": "<div class=\"card\">content</div>"
|
|
},
|
|
{
|
|
"name": "p_text",
|
|
"sx_input": "(p \"paragraph text\")",
|
|
"expected_html": "<p>paragraph text</p>"
|
|
},
|
|
{
|
|
"name": "nested_tags",
|
|
"sx_input": "(div (p \"a\") (p \"b\"))",
|
|
"expected_html": "<div><p>a</p><p>b</p></div>"
|
|
},
|
|
{
|
|
"name": "void_br",
|
|
"sx_input": "(br)",
|
|
"expected_html": "<br />"
|
|
},
|
|
{
|
|
"name": "void_hr",
|
|
"sx_input": "(hr)",
|
|
"expected_html": "<hr />"
|
|
},
|
|
{
|
|
"name": "void_img",
|
|
"sx_input": "(img :src \"/photo.jpg\" :alt \"A photo\")",
|
|
"expected_html": "<img src=\"/photo.jpg\" alt=\"A photo\" />"
|
|
},
|
|
{
|
|
"name": "void_input",
|
|
"sx_input": "(input :type \"text\" :name \"q\" :placeholder \"Search\")",
|
|
"expected_html": "<input type=\"text\" name=\"q\" placeholder=\"Search\" />"
|
|
},
|
|
{
|
|
"name": "fragment",
|
|
"sx_input": "(<> (p \"a\") (p \"b\"))",
|
|
"expected_html": "<p>a</p><p>b</p>"
|
|
},
|
|
{
|
|
"name": "boolean_attr",
|
|
"sx_input": "(input :type \"checkbox\" :checked true)",
|
|
"expected_html": "<input type=\"checkbox\" checked />"
|
|
},
|
|
{
|
|
"name": "nil_attr",
|
|
"sx_input": "(div :class nil \"content\")",
|
|
"expected_html": "<div>content</div>"
|
|
},
|
|
{
|
|
"name": "empty_string_attr",
|
|
"sx_input": "(div :class \"\" \"visible\")",
|
|
"expected_html": "<div class=\"\">visible</div>"
|
|
},
|
|
{
|
|
"name": "if_true",
|
|
"sx_input": "(if true (p \"yes\") (p \"no\"))",
|
|
"expected_html": "<p>yes</p>"
|
|
},
|
|
{
|
|
"name": "if_false",
|
|
"sx_input": "(if false (p \"yes\") (p \"no\"))",
|
|
"expected_html": "<p>no</p>"
|
|
},
|
|
{
|
|
"name": "when_true",
|
|
"sx_input": "(when true (p \"shown\"))",
|
|
"expected_html": "<p>shown</p>"
|
|
},
|
|
{
|
|
"name": "when_false",
|
|
"sx_input": "(when false (p \"hidden\"))",
|
|
"expected_html": ""
|
|
},
|
|
{
|
|
"name": "let_binding",
|
|
"sx_input": "(let ((x \"hi\")) (p x))",
|
|
"expected_html": "<p>hi</p>"
|
|
},
|
|
{
|
|
"name": "let_multiple",
|
|
"sx_input": "(let ((x \"a\") (y \"b\")) (div (p x) (p y)))",
|
|
"expected_html": "<div><p>a</p><p>b</p></div>"
|
|
},
|
|
{
|
|
"name": "cond_form",
|
|
"sx_input": "(cond (= 1 2) (p \"no\") (= 1 1) (p \"yes\") :else (p \"default\"))",
|
|
"expected_html": "<p>yes</p>"
|
|
},
|
|
{
|
|
"name": "case_form",
|
|
"sx_input": "(case \"b\" \"a\" \"A\" \"b\" \"B\" :else \"?\")",
|
|
"expected_html": "B"
|
|
},
|
|
{
|
|
"name": "and_short",
|
|
"sx_input": "(and true false)",
|
|
"expected_html": "false"
|
|
},
|
|
{
|
|
"name": "or_short",
|
|
"sx_input": "(or false \"found\")",
|
|
"expected_html": "found"
|
|
},
|
|
{
|
|
"name": "map_li",
|
|
"sx_input": "(map (fn (x) (li x)) (list \"a\" \"b\" \"c\"))",
|
|
"expected_html": "<li>a</li><li>b</li><li>c</li>"
|
|
},
|
|
{
|
|
"name": "filter_even",
|
|
"sx_input": "(filter even? (list 1 2 3 4 5))",
|
|
"expected_html": "<filter><function <lambda> at 0x7c1551c5fe20>12345</filter>"
|
|
},
|
|
{
|
|
"name": "reduce_sum",
|
|
"sx_input": "(reduce + 0 (list 1 2 3 4 5))",
|
|
"expected_html": "15"
|
|
},
|
|
{
|
|
"name": "str_concat",
|
|
"sx_input": "(str \"hello\" \" \" \"world\")",
|
|
"expected_html": "hello world"
|
|
},
|
|
{
|
|
"name": "str_upcase",
|
|
"sx_input": "(upcase \"hello\")",
|
|
"expected_html": "HELLO"
|
|
},
|
|
{
|
|
"name": "defcomp_simple",
|
|
"sx_input": "(do (defcomp ~test-badge (&key label) (span :class \"badge\" label)) (~test-badge :label \"New\"))",
|
|
"expected_html": "<span class=\"badge\">New</span>"
|
|
},
|
|
{
|
|
"name": "defcomp_children",
|
|
"sx_input": "(do (defcomp ~test-wrap (&rest children) (div :class \"wrap\" children)) (~test-wrap (p \"inside\")))",
|
|
"expected_html": "<div class=\"wrap\"><p>inside</p></div>"
|
|
},
|
|
{
|
|
"name": "defcomp_multi_key",
|
|
"sx_input": "(do (defcomp ~test-card (&key title subtitle) (div (h2 title) (when subtitle (p subtitle)))) (~test-card :title \"Title\" :subtitle \"Sub\"))",
|
|
"expected_html": "<div><h2>Title</h2><p>Sub</p></div>"
|
|
},
|
|
{
|
|
"name": "defcomp_no_optional",
|
|
"sx_input": "(do (defcomp ~test-card2 (&key title subtitle) (div (h2 title) (when subtitle (p subtitle)))) (~test-card2 :title \"Only Title\"))",
|
|
"expected_html": "<div><h2>Only Title</h2></div>"
|
|
},
|
|
{
|
|
"name": "nested_components",
|
|
"sx_input": "(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\")))",
|
|
"expected_html": "<div class=\"outer\"><h2>Hello</h2><span class=\"inner\">World</span></div>"
|
|
},
|
|
{
|
|
"name": "macro_unless",
|
|
"sx_input": "(do (defmacro unless (cond &rest body) (list 'if (list 'not cond) (cons 'do body))) (unless false (p \"shown\")))",
|
|
"expected_html": "<p>shown</p>"
|
|
},
|
|
{
|
|
"name": "do_block",
|
|
"sx_input": "(div (do (p \"a\") (p \"b\")))",
|
|
"expected_html": "<div><p>a</p><p>b</p></div>"
|
|
},
|
|
{
|
|
"name": "nil_child",
|
|
"sx_input": "(div nil \"after-nil\")",
|
|
"expected_html": "<div>after-nil</div>"
|
|
},
|
|
{
|
|
"name": "number_child",
|
|
"sx_input": "(div 42)",
|
|
"expected_html": "<div>42</div>"
|
|
},
|
|
{
|
|
"name": "bool_child",
|
|
"sx_input": "(div true)",
|
|
"expected_html": "<div>true</div>"
|
|
},
|
|
{
|
|
"name": "data_attr",
|
|
"sx_input": "(div :data-id \"123\" :data-name \"test\" \"content\")",
|
|
"expected_html": "<div data-id=\"123\" data-name=\"test\">content</div>"
|
|
},
|
|
{
|
|
"name": "raw_simple",
|
|
"sx_input": "(raw! \"<b>bold</b>\")",
|
|
"expected_html": "<b>bold</b>"
|
|
},
|
|
{
|
|
"name": "raw_in_div",
|
|
"sx_input": "(div (raw! \"<em>italic</em>\"))",
|
|
"expected_html": "<div><em>italic</em></div>"
|
|
},
|
|
{
|
|
"name": "raw_component",
|
|
"sx_input": "(do (defcomp ~rich (&key html) (raw! html)) (~rich :html \"<p>CMS</p>\"))",
|
|
"expected_html": "<p>CMS</p>"
|
|
},
|
|
{
|
|
"name": "misc_error_inline",
|
|
"sx_input": "(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\"))",
|
|
"expected_html": "<div class=\"text-red-600 text-sm\">Something went wrong</div>"
|
|
},
|
|
{
|
|
"name": "misc_notification_badge",
|
|
"sx_input": "(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))",
|
|
"expected_html": "<span class=\"bg-red-500 text-white text-xs rounded-full px-1.5 py-0.5\">5</span>"
|
|
},
|
|
{
|
|
"name": "misc_cache_cleared",
|
|
"sx_input": "(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\"))",
|
|
"expected_html": "<span class=\"text-green-600 font-bold\">Cache cleared at 12:00</span>"
|
|
},
|
|
{
|
|
"name": "misc_error_list_item",
|
|
"sx_input": "(do (defcomp ~shared:misc/error-list-item (&key (message :as string)) (li message)) (~shared:misc/error-list-item :message \"Bad input\"))",
|
|
"expected_html": "<li>Bad input</li>"
|
|
},
|
|
{
|
|
"name": "misc_fragment_error",
|
|
"sx_input": "(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\"))",
|
|
"expected_html": "<p class=\"text-sm text-red-600\">Service <b>blog</b> is unavailable.</p>"
|
|
}
|
|
] |