content: asSx wire string-escaping (String>>sxEscaped) + 5 tests (243/243)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 58s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 58s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
;; children's renderings, so (asHTML doc) / (asSx doc) are pure polymorphic
|
||||
;; sends with no type dispatch in the SX layer.
|
||||
;;
|
||||
;; HTML escaping happens HERE, at the boundary: text and attribute values are
|
||||
;; passed through String>>htmlEscaped (& < > "), so untrusted content cannot
|
||||
;; break out of its element. asSx wire output is not yet string-escaped (next).
|
||||
;; Escaping happens HERE, at the boundary. asHTML routes text/attrs through
|
||||
;; String>>htmlEscaped (& < > "); asSx routes them through String>>sxEscaped
|
||||
;; (\ and ") so values cannot break out of an element or an SX string literal.
|
||||
|
||||
(define
|
||||
content-bootstrap-render!
|
||||
@@ -19,6 +19,10 @@
|
||||
"String"
|
||||
"htmlEscaped"
|
||||
"htmlEscaped | out i n c | out := ''. n := self size. i := 1. [i <= n] whileTrue: [c := self at: i. (c = $&) ifTrue: [out := out , '&'] ifFalse: [(c = $<) ifTrue: [out := out , '<'] ifFalse: [(c = $>) ifTrue: [out := out , '>'] ifFalse: [(c = $\") ifTrue: [out := out , '"'] ifFalse: [out := out , c asString]]]]. i := i + 1]. ^ out")
|
||||
(ct-def-method!
|
||||
"String"
|
||||
"sxEscaped"
|
||||
"sxEscaped | out i n c | out := ''. n := self size. i := 1. [i <= n] whileTrue: [c := self at: i. (c = $\\) ifTrue: [out := out , '\\\\'] ifFalse: [(c = $\") ifTrue: [out := out , '\\\"'] ifFalse: [out := out , c asString]]. i := i + 1]. ^ out")
|
||||
(ct-def-method!
|
||||
"CtHeading"
|
||||
"asHTML"
|
||||
@@ -55,20 +59,29 @@
|
||||
(ct-def-method!
|
||||
"CtHeading"
|
||||
"asSx"
|
||||
"asSx | t | t := level printString. ^ '(h' , t , ' \"' , text , '\")'")
|
||||
(ct-def-method! "CtText" "asSx" "asSx ^ '(p \"' , text , '\")'")
|
||||
(ct-def-method! "CtCode" "asSx" "asSx ^ '(pre (code \"' , text , '\"))'")
|
||||
(ct-def-method! "CtQuote" "asSx" "asSx ^ '(blockquote \"' , text , '\")'")
|
||||
"asSx | t | t := level printString. ^ '(h' , t , ' \"' , text sxEscaped , '\")'")
|
||||
(ct-def-method! "CtText" "asSx" "asSx ^ '(p \"' , text sxEscaped , '\")'")
|
||||
(ct-def-method!
|
||||
"CtCode"
|
||||
"asSx"
|
||||
"asSx ^ '(pre (code \"' , text sxEscaped , '\"))'")
|
||||
(ct-def-method!
|
||||
"CtQuote"
|
||||
"asSx"
|
||||
"asSx ^ '(blockquote \"' , text sxEscaped , '\")'")
|
||||
(ct-def-method!
|
||||
"CtImage"
|
||||
"asSx"
|
||||
"asSx ^ '(img :src \"' , src , '\" :alt \"' , alt , '\")'")
|
||||
(ct-def-method! "CtEmbed" "asSx" "asSx ^ '(iframe :src \"' , url , '\")'")
|
||||
"asSx ^ '(img :src \"' , src sxEscaped , '\" :alt \"' , alt sxEscaped , '\")'")
|
||||
(ct-def-method!
|
||||
"CtEmbed"
|
||||
"asSx"
|
||||
"asSx ^ '(iframe :src \"' , url sxEscaped , '\")'")
|
||||
(ct-def-method! "CtDivider" "asSx" "asSx ^ '(hr)'")
|
||||
(ct-def-method!
|
||||
"CtList"
|
||||
"asSx"
|
||||
"asSx | tag | tag := ordered ifTrue: ['ol'] ifFalse: ['ul']. ^ '(' , tag , ' ' , (items inject: '' into: [:a :x | a , '(li \"' , x , '\")']) , ')'")
|
||||
"asSx | tag | tag := ordered ifTrue: ['ol'] ifFalse: ['ul']. ^ '(' , tag , ' ' , (items inject: '' into: [:a :x | a , '(li \"' , x sxEscaped , '\")']) , ')'")
|
||||
(ct-def-method!
|
||||
"CtDoc"
|
||||
"asSx"
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
"suites": {
|
||||
"block": {"pass": 38, "fail": 0},
|
||||
"doc": {"pass": 40, "fail": 0},
|
||||
"render": {"pass": 37, "fail": 0},
|
||||
"render": {"pass": 42, "fail": 0},
|
||||
"api": {"pass": 26, "fail": 0},
|
||||
"store": {"pass": 29, "fail": 0},
|
||||
"crdt": {"pass": 34, "fail": 0},
|
||||
"sync": {"pass": 14, "fail": 0},
|
||||
"fed": {"pass": 20, "fail": 0}
|
||||
},
|
||||
"total_pass": 238,
|
||||
"total_pass": 243,
|
||||
"total_fail": 0,
|
||||
"total": 238
|
||||
"total": 243
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ _Generated by `lib/content/conformance.sh`_
|
||||
|-------|-----:|-----:|------:|
|
||||
| block | 38 | 0 | 38 |
|
||||
| doc | 40 | 0 | 40 |
|
||||
| render | 37 | 0 | 37 |
|
||||
| render | 42 | 0 | 42 |
|
||||
| api | 26 | 0 | 26 |
|
||||
| store | 29 | 0 | 29 |
|
||||
| crdt | 34 | 0 | 34 |
|
||||
| sync | 14 | 0 | 14 |
|
||||
| fed | 20 | 0 | 20 |
|
||||
| **Total** | **238** | **0** | **238** |
|
||||
| **Total** | **243** | **0** | **243** |
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
;; Phase 1 — render boundary. asHTML / asSx are polymorphic message sends on
|
||||
;; blocks and the document. HTML escaping happens at the boundary.
|
||||
;; blocks and the document. Escaping happens at the boundary.
|
||||
|
||||
(st-bootstrap-classes!)
|
||||
(content-bootstrap-blocks!)
|
||||
@@ -109,3 +109,27 @@
|
||||
"escape code body"
|
||||
(asHTML (mk-code "xc" "html" "<div> & </div>"))
|
||||
"<pre><code class=\"language-html\"><div> & </div></code></pre>")
|
||||
|
||||
;; ── asSx string-escaping (build expected via q/bs to avoid miscounts) ──
|
||||
(define q1 (str "\""))
|
||||
(define bs (str "\\"))
|
||||
(content-test
|
||||
"asSx escapes quote"
|
||||
(asSx (mk-text "qt" (str "say " q1 "hi" q1)))
|
||||
(str "(p " q1 "say " bs q1 "hi" bs q1 q1 ")"))
|
||||
(content-test
|
||||
"asSx escapes backslash"
|
||||
(asSx (mk-text "qb" (str "a" bs "b")))
|
||||
(str "(p " q1 "a" bs bs "b" q1 ")"))
|
||||
(content-test
|
||||
"asSx plain unchanged"
|
||||
(asSx (mk-text "pp" "plain"))
|
||||
"(p \"plain\")")
|
||||
(content-test
|
||||
"asSx escapes image attr"
|
||||
(asSx (mk-image "im" (str "/a" q1) "x"))
|
||||
(str "(img :src " q1 "/a" bs q1 q1 " :alt " q1 "x" q1 ")"))
|
||||
(content-test
|
||||
"asSx escapes list item"
|
||||
(asSx (mk-list "lq" false (list (str "i" q1) "j")))
|
||||
(str "(ul (li " q1 "i" bs q1 q1 ")(li " q1 "j" q1 "))"))
|
||||
|
||||
@@ -19,7 +19,7 @@ injected adapter, not core.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/content/conformance.sh` → **238/238** (Phases 1–4 COMPLETE + extensions: HTML escaping)
|
||||
`bash lib/content/conformance.sh` → **243/243** (Phases 1–4 COMPLETE + extensions: HTML + SX escaping)
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -77,10 +77,15 @@ lib/content/api.sx ── (content/edit) (content/render) (content/history) ─
|
||||
|
||||
## Extensions (post-roadmap)
|
||||
- [x] HTML escaping at the render boundary (`String>>htmlEscaped`: & < > ")
|
||||
- [ ] asSx wire string-escaping (" and \ in SX string literals)
|
||||
- [x] asSx wire string-escaping (`String>>sxEscaped`: \ and " in SX literals)
|
||||
|
||||
## Progress log
|
||||
|
||||
- 2026-06-07 — Extension: asSx wire string-escaping. Added `String>>sxEscaped`
|
||||
(escapes `\`→`\\` then `"`→`\"`) and routed every `asSx` text/attr/list-item
|
||||
through it, so the SX wire format stays valid when content contains quotes or
|
||||
backslashes. +5 render tests (expected strings built from `q`/`bs` helpers to
|
||||
avoid escaping miscounts). Suite 243/243.
|
||||
- 2026-06-07 — Extension: HTML escaping at the render boundary. Added
|
||||
`String>>htmlEscaped` (recursive char walk escaping & < > ", order-safe so &
|
||||
isn't double-escaped) and routed every `asHTML` text/attr through it — heading,
|
||||
|
||||
Reference in New Issue
Block a user