From e115af86d86e01d2858be76f65e2d37381516a13 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 05:13:44 +0000 Subject: [PATCH] content: video/audio media block (media.sx) + 15 tests (683/683) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/content/conformance.sh | 3 +- lib/content/media.sx | 52 ++++++++++++++++++++++++++++++++ lib/content/scoreboard.json | 5 ++-- lib/content/scoreboard.md | 3 +- lib/content/tests/media.sx | 59 +++++++++++++++++++++++++++++++++++++ lib/content/validate.sx | 13 ++++++++ plans/content-on-sx.md | 8 ++++- 7 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 lib/content/media.sx create mode 100644 lib/content/tests/media.sx diff --git a/lib/content/conformance.sh b/lib/content/conformance.sh index 482edf4d..01ad9ba1 100755 --- a/lib/content/conformance.sh +++ b/lib/content/conformance.sh @@ -15,7 +15,7 @@ if [ ! -x "$SX_SERVER" ]; then fi fi -SUITES=(block doc render api meta page page-full markdown text section compose tree-edit move clone query toc anchor outline flatten transform normalize find-replace stats table callout data wire validate store snapshot crdt crdt-store sync md-import md-doc fed) +SUITES=(block doc render api meta page page-full markdown text section compose tree-edit move clone query toc anchor outline flatten transform normalize find-replace stats table callout media data wire validate store snapshot crdt crdt-store sync md-import md-doc fed) OUT_JSON="lib/content/scoreboard.json" OUT_MD="lib/content/scoreboard.md" @@ -60,6 +60,7 @@ run_suite() { (load "lib/content/stats.sx") (load "lib/content/table.sx") (load "lib/content/callout.sx") +(load "lib/content/media.sx") (load "lib/content/data.sx") (load "lib/content/wire.sx") (load "lib/content/page.sx") diff --git a/lib/content/media.sx b/lib/content/media.sx new file mode 100644 index 00000000..76e07979 --- /dev/null +++ b/lib/content/media.sx @@ -0,0 +1,52 @@ +;; content-on-sx — video/audio media block. +;; +;; CtMedia holds a `kind` (video/audio) and `src`. Self-contained: answers +;; asHTML/asSx/asText/asMarkdown: so it composes with the render boundary with no +;; changes elsewhere. HTML src is htmlEscaped, SX src sxEscaped. +;; +;; Requires (loaded by harness): block.sx, doc.sx, render.sx (escapers); +;; markdown.sx / text.sx for those formats. + +(define + content-bootstrap-media! + (fn + () + (begin + (st-class-define! "CtMedia" "CtBlock" (list "kind" "src")) + (ct-def-method! "CtMedia" "kind" "kind ^ kind") + (ct-def-method! "CtMedia" "src" "src ^ src") + (ct-def-method! "CtMedia" "type" "type ^ #media") + (ct-def-method! + "CtMedia" + "asHTML" + "asHTML ^ '<' , kind , ' src=\"' , src htmlEscaped , '\" controls>'") + (ct-def-method! + "CtMedia" + "asSx" + "asSx ^ '(' , kind , ' :src \"' , src sxEscaped , '\")'") + (ct-def-method! "CtMedia" "asText" "asText ^ ''") + (ct-def-method! + "CtMedia" + "asMarkdown:" + "asMarkdown: nl ^ '[' , kind , '](' , src , ')'") + true))) + +(define + mk-media + (fn + (id kind src) + (st-iv-set! + (st-iv-set! + (st-iv-set! (st-make-instance "CtMedia") "id" id) + "kind" + kind) + "src" + src))) + +(define + media? + (fn (b) (and (st-instance? b) (= (get b :class) "CtMedia")))) +(define media-kind (fn (b) (st-send b "kind" (list)))) + +(define mk-video (fn (id src) (mk-media id "video" src))) +(define mk-audio (fn (id src) (mk-media id "audio" src))) diff --git a/lib/content/scoreboard.json b/lib/content/scoreboard.json index b0fd1304..8b6a7f87 100644 --- a/lib/content/scoreboard.json +++ b/lib/content/scoreboard.json @@ -25,6 +25,7 @@ "stats": {"pass": 17, "fail": 0}, "table": {"pass": 15, "fail": 0}, "callout": {"pass": 12, "fail": 0}, + "media": {"pass": 15, "fail": 0}, "data": {"pass": 21, "fail": 0}, "wire": {"pass": 11, "fail": 0}, "validate": {"pass": 23, "fail": 0}, @@ -37,7 +38,7 @@ "md-doc": {"pass": 12, "fail": 0}, "fed": {"pass": 20, "fail": 0} }, - "total_pass": 668, + "total_pass": 683, "total_fail": 0, - "total": 668 + "total": 683 } diff --git a/lib/content/scoreboard.md b/lib/content/scoreboard.md index d70fa4c2..3708c988 100644 --- a/lib/content/scoreboard.md +++ b/lib/content/scoreboard.md @@ -29,6 +29,7 @@ _Generated by `lib/content/conformance.sh`_ | stats | 17 | 0 | 17 | | table | 15 | 0 | 15 | | callout | 12 | 0 | 12 | +| media | 15 | 0 | 15 | | data | 21 | 0 | 21 | | wire | 11 | 0 | 11 | | validate | 23 | 0 | 23 | @@ -40,4 +41,4 @@ _Generated by `lib/content/conformance.sh`_ | md-import | 38 | 0 | 38 | | md-doc | 12 | 0 | 12 | | fed | 20 | 0 | 20 | -| **Total** | **668** | **0** | **668** | +| **Total** | **683** | **0** | **683** | diff --git a/lib/content/tests/media.sx b/lib/content/tests/media.sx new file mode 100644 index 00000000..1c2b4384 --- /dev/null +++ b/lib/content/tests/media.sx @@ -0,0 +1,59 @@ +;; Extension — video/audio media block. + +(st-bootstrap-classes!) +(content/bootstrap!) +(content-bootstrap-markdown!) +(content-bootstrap-text!) +(content-bootstrap-media!) + +(define v (mk-video "v" "/clip.mp4")) +(define a (mk-audio "a" "/song.mp3")) + +;; ── identity ── +(content-test "media is block" (block? v) true) +(content-test "media? yes" (media? v) true) +(content-test "video type" (blk-type v) "media") +(content-test "video kind" (media-kind v) "video") +(content-test "audio kind" (media-kind a) "audio") + +;; ── render ── +(content-test + "video html" + (asHTML v) + "") +(content-test + "audio html" + (asHTML a) + "") +(content-test "video sx" (asSx v) "(video :src \"/clip.mp4\")") +(content-test "video text" (asText v) "") +(content-test "video markdown" (asMarkdown v) "[video](/clip.mp4)") +(content-test "audio markdown" (asMarkdown a) "[audio](/song.mp3)") + +;; ── html escapes src ── +(content-test + "media html escapes" + (asHTML (mk-video "v" "/a.mp4?x=1&y=2")) + "") + +;; ── in a document ── +(define + d + (doc-append + (doc-append (doc-empty "d") (mk-heading "h" 1 "Watch")) + v)) +(content-test + "doc with media html" + (asHTML d) + "

Watch

") + +;; ── validation ── +(content-test + "valid media" + (content/valid? (doc-append (doc-empty "d") v)) + true) +(content-test + "bad media kind flagged" + (content/issue-kinds + (doc-append (doc-empty "d") (mk-media "m" "movie" "/x"))) + (list "field")) diff --git a/lib/content/validate.sx b/lib/content/validate.sx index c7d679f2..8880a790 100644 --- a/lib/content/validate.sx +++ b/lib/content/validate.sx @@ -170,6 +170,19 @@ id (string? (blk-get b "text")) "callout text must be a string"))) + ((= t "media") + (append + (ct-field-issue + id + (if + (= (blk-get b "kind") "video") + true + (= (blk-get b "kind") "audio")) + "media kind must be video or audio") + (ct-field-issue + id + (string? (blk-get b "src")) + "media src must be a string"))) (else (list (ct-issue id "type" (str "unknown block type: " t)))))))) (define diff --git a/plans/content-on-sx.md b/plans/content-on-sx.md index 79440e4e..1d01e0be 100644 --- a/plans/content-on-sx.md +++ b/plans/content-on-sx.md @@ -19,7 +19,7 @@ injected adapter, not core. ## Status (rolling) -`bash lib/content/conformance.sh` → **668/668** (Phases 1–4 COMPLETE + ~30 extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees + deep editing + flatten + relative reorder, doc stats, table + callout blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization) +`bash lib/content/conformance.sh` → **683/683** (Phases 1–4 COMPLETE + ~31 extensions: HTML/SX escaping, Markdown render + import/export incl. tables & frontmatter (full round-trip), CRDT replication, tree-aware validation, snapshot cache, doc metadata, plain-text render, nested block trees + deep editing + flatten + relative reorder, doc stats, table + callout + media blocks, HTML page wrapper + SEO page, doc composition + id-remap, portable data + wire serialization, block query + transforms + find/replace, TOC + anchored headings + outline, normalization) ## Ground rules @@ -90,6 +90,7 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ - [x] document statistics (`stats.sx`: word/char/block counts, reading time) - [x] table block (`table.sx`: CtTable, renders html/sx/text/md, validated) - [x] callout block (`callout.sx`: CtCallout note/warning/tip, renders html/sx/text/md, validated) +- [x] media block (`media.sx`: CtMedia video/audio, renders html/sx/text/md, validated) - [x] HTML page wrapper (`page.sx`: content/page, escaped title from metadata) - [x] SEO page (`page-full.sx`: content/page-full, lang + meta description from excerpt) - [x] document composition (`compose.sx`: concat/prepend/concat-all/wrap-section) @@ -109,6 +110,11 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─ ## Progress log +- 2026-06-07 — Extension: video/audio media block (`media.sx`). `CtMedia` holds + kind (video/audio) + src; answers asHTML (`