diff --git a/lib/hyperscript/compiler.sx b/lib/hyperscript/compiler.sx index 0169bbdf..d79c5463 100644 --- a/lib/hyperscript/compiler.sx +++ b/lib/hyperscript/compiler.sx @@ -202,42 +202,44 @@ (define emit-inc (fn - (target) + (expr tgt-override) (let - ((t (hs-to-sx target))) + ((t (hs-to-sx expr))) (if - (and (list? target) (= (first target) (quote attr))) - (list - (quote dom-set-attr) - (hs-to-sx (nth target 2)) - (nth target 1) + (and (list? expr) (= (first expr) (quote attr))) + (let + ((el (if tgt-override (hs-to-sx tgt-override) (hs-to-sx (nth expr 2))))) (list - (quote +) + (quote dom-set-attr) + el + (nth expr 1) (list - (quote dom-get-attr) - (hs-to-sx (nth target 2)) - (nth target 1)) - 1)) + (quote +) + (list + (quote parse-number) + (list (quote dom-get-attr) el (nth expr 1))) + 1))) (list (quote set!) t (list (quote +) t 1)))))) (define emit-dec (fn - (target) + (expr tgt-override) (let - ((t (hs-to-sx target))) + ((t (hs-to-sx expr))) (if - (and (list? target) (= (first target) (quote attr))) - (list - (quote dom-set-attr) - (hs-to-sx (nth target 2)) - (nth target 1) + (and (list? expr) (= (first expr) (quote attr))) + (let + ((el (if tgt-override (hs-to-sx tgt-override) (hs-to-sx (nth expr 2))))) (list - (quote -) + (quote dom-set-attr) + el + (nth expr 1) (list - (quote dom-get-attr) - (hs-to-sx (nth target 2)) - (nth target 1)) - 1)) + (quote -) + (list + (quote parse-number) + (list (quote dom-get-attr) el (nth expr 1))) + 1))) (list (quote set!) t (list (quote -) t 1)))))) (define emit-behavior @@ -288,7 +290,7 @@ ((= head (quote not)) (list (quote not) (hs-to-sx (nth ast 1)))) ((= head (quote no)) - (list (quote not) (hs-to-sx (nth ast 1)))) + (list (quote hs-falsy?) (hs-to-sx (nth ast 1)))) ((= head (quote and)) (list (quote and) @@ -306,7 +308,7 @@ (hs-to-sx (nth ast 2)))) ((= head (quote +)) (list - (quote +) + (quote hs-add) (hs-to-sx (nth ast 1)) (hs-to-sx (nth ast 2)))) ((= head (quote -)) @@ -337,7 +339,7 @@ (list (quote nil?) (hs-to-sx (nth ast 1))))) ((= head (quote matches?)) (list - (quote dom-matches?) + (quote hs-matches?) (hs-to-sx (nth ast 1)) (hs-to-sx (nth ast 2)))) ((= head (quote contains?)) @@ -508,8 +510,14 @@ (cons (quote hs-install) (map hs-to-sx (rest ast)))) ((= head (quote measure)) (list (quote hs-measure) (hs-to-sx (nth ast 1)))) - ((= head (quote increment!)) (emit-inc (nth ast 1))) - ((= head (quote decrement!)) (emit-dec (nth ast 1))) + ((= head (quote increment!)) + (emit-inc + (nth ast 1) + (if (> (len ast) 2) (nth ast 2) nil))) + ((= head (quote decrement!)) + (emit-dec + (nth ast 1) + (if (> (len ast) 2) (nth ast 2) nil))) ((= head (quote on)) (emit-on ast)) ((= head (quote init)) (list @@ -563,6 +571,49 @@ pos (if target target (quote me))) render-call))))) + ((= head (quote not-in?)) + (list + (quote not) + (list + (quote contains?) + (hs-to-sx (nth ast 2)) + (hs-to-sx (nth ast 1))))) + ((= head (quote in?)) + (list + (quote contains?) + (hs-to-sx (nth ast 2)) + (hs-to-sx (nth ast 1)))) + ((= head (quote type-check)) + (list + (quote hs-type-check) + (hs-to-sx (nth ast 1)) + (nth ast 2))) + ((= head (quote type-check!)) + (list + (quote hs-type-check!) + (hs-to-sx (nth ast 1)) + (nth ast 2))) + ((= head (quote strict-eq)) + (list + (quote hs-strict-eq) + (hs-to-sx (nth ast 1)) + (hs-to-sx (nth ast 2)))) + ((= head (quote some)) + (list + (quote some) + (list + (quote fn) + (list (make-symbol (nth ast 1))) + (hs-to-sx (nth ast 3))) + (hs-to-sx (nth ast 2)))) + ((= head (quote every)) + (list + (quote every?) + (list + (quote fn) + (list (make-symbol (nth ast 1))) + (hs-to-sx (nth ast 3))) + (hs-to-sx (nth ast 2)))) (true ast)))))))) ;; ── Convenience: source → SX ───────────────────────────────── diff --git a/lib/hyperscript/parser.sx b/lib/hyperscript/parser.sx index e6abadbf..d89f5dfb 100644 --- a/lib/hyperscript/parser.sx +++ b/lib/hyperscript/parser.sx @@ -45,10 +45,10 @@ ((slen (len val))) (cond ((and (>= slen 3) (= (substring val (- slen 2) slen) "ms")) - (string->number (substring val 0 (- slen 2)))) + (parse-number (substring val 0 (- slen 2)))) ((and (>= slen 2) (= (nth val (- slen 1)) "s")) - (* 1000 (string->number (substring val 0 (- slen 1))))) - (true (string->number val)))))) + (* 1000 (parse-number (substring val 0 (- slen 1))))) + (true (parse-number val)))))) (define parse-poss-tail (fn @@ -58,6 +58,7 @@ (cond ((or (= typ "ident") (= typ "keyword")) (do (adv!) (parse-prop-chain (list (quote .) owner val)))) + ((= typ "attr") (do (adv!) (list (quote attr) val owner))) ((= typ "class") (let ((prop (get (adv!) "value"))) @@ -112,6 +113,8 @@ ((and (= typ "keyword") (= val "false")) (do (adv!) false)) ((and (= typ "keyword") (or (= val "null") (= val "nil"))) (do (adv!) nil)) + ((and (= typ "keyword") (= val "undefined")) + (do (adv!) nil)) ((and (= typ "keyword") (= val "not")) (do (adv!) (list (quote not) (parse-expr)))) ((and (= typ "keyword") (= val "no")) @@ -127,6 +130,8 @@ (do (adv!) (parse-the-expr))) ((and (= typ "keyword") (= val "me")) (do (adv!) (list (quote me)))) + ((and (= typ "keyword") (= val "I")) + (do (adv!) (list (quote me)))) ((and (= typ "keyword") (or (= val "it") (= val "result"))) (do (adv!) (list (quote it)))) ((and (= typ "keyword") (= val "event")) @@ -179,6 +184,40 @@ (list (quote -) 0 operand)))) ((= typ "component") (do (adv!) (list (quote component) val))) + ((and (= typ "keyword") (= val "some")) + (do + (adv!) + (let + ((var-name (tp-val))) + (do + (adv!) + (match-kw "in") + (let + ((collection (parse-expr))) + (do + (match-kw "with") + (list + (quote some) + var-name + collection + (parse-expr)))))))) + ((and (= typ "keyword") (= val "every")) + (do + (adv!) + (let + ((var-name (tp-val))) + (do + (adv!) + (match-kw "in") + (let + ((collection (parse-expr))) + (do + (match-kw "with") + (list + (quote every) + var-name + collection + (parse-expr)))))))) (true nil))))) (define parse-poss @@ -196,23 +235,122 @@ (let ((typ (tp-type)) (val (tp-val))) (cond - ((and (= typ "op") (or (= val "==") (= val "!=") (= val "<") (= val ">") (= val "<=") (= val ">="))) + ((and (= typ "op") (or (= val "==") (= val "!=") (= val "<") (= val ">") (= val "<=") (= val ">=") (= val "===") (= val "!=="))) (do (adv!) (let ((right (parse-expr))) - (list (if (= val "==") (quote =) val) left right)))) + (cond + ((= val "==") (list (quote =) left right)) + ((= val "===") (list (quote strict-eq) left right)) + ((= val "!==") + (list + (quote not) + (list (quote strict-eq) left right))) + (true (list val left right)))))) ((and (= typ "keyword") (= val "is")) (do (adv!) (cond ((match-kw "not") - (if - (match-kw "empty") - (list (quote not) (list (quote empty?) left)) - (let - ((right (parse-expr))) - (list (quote not) (list (quote =) left right))))) + (cond + ((match-kw "empty") + (list (quote not) (list (quote empty?) left))) + ((match-kw "in") + (list (quote not-in?) left (parse-expr))) + ((match-kw "really") + (do + (match-kw "equal") + (match-kw "to") + (list + (quote not) + (list (quote strict-eq) left (parse-expr))))) + ((match-kw "equal") + (do + (match-kw "to") + (list + (quote not) + (list (quote =) left (parse-expr))))) + ((or (and (or (= (tp-val) "a") (= (tp-val) "an")) (do (adv!) true))) + (let + ((type-name (tp-val))) + (do + (adv!) + (let + ((strict (if (string-ends-with? type-name "!") (string-slice type-name 0 (- (len type-name) 1)) nil))) + (if + strict + (list + (quote not) + (list (quote type-check!) left strict)) + (list + (quote not) + (list (quote type-check) left type-name))))))) + (true + (let + ((right (parse-expr))) + (list (quote not) (list (quote =) left right)))))) + ((match-kw "empty") (list (quote empty?) left)) + ((match-kw "less") + (do + (match-kw "than") + (if + (match-kw "or") + (do + (match-kw "equal") + (match-kw "to") + (list (quote <=) left (parse-expr))) + (list (quote <) left (parse-expr))))) + ((match-kw "greater") + (do + (match-kw "than") + (if + (match-kw "or") + (do + (match-kw "equal") + (match-kw "to") + (list (quote >=) left (parse-expr))) + (list (quote >) left (parse-expr))))) + ((match-kw "in") (list (quote in?) left (parse-expr))) + ((match-kw "really") + (do + (match-kw "equal") + (match-kw "to") + (list (quote strict-eq) left (parse-expr)))) + ((match-kw "equal") + (do + (match-kw "to") + (list (quote =) left (parse-expr)))) + ((or (and (or (= (tp-val) "a") (= (tp-val) "an")) (do (adv!) true))) + (let + ((type-name (tp-val))) + (do + (adv!) + (let + ((strict (if (string-ends-with? type-name "!") (string-slice type-name 0 (- (len type-name) 1)) nil))) + (if + strict + (list (quote type-check!) left strict) + (list (quote type-check) left type-name)))))) + (true + (let + ((right (parse-expr))) + (list (quote =) left right)))))) + ((and (= typ "keyword") (= val "am")) + (do + (adv!) + (cond + ((match-kw "not") + (cond + ((match-kw "in") + (list (quote not-in?) left (parse-expr))) + ((match-kw "empty") + (list (quote not) (list (quote empty?) left))) + (true + (let + ((right (parse-expr))) + (list (quote not) (list (quote =) left right)))))) + ((match-kw "in") (list (quote in?) left (parse-expr))) ((match-kw "empty") (list (quote empty?) left)) (true (let @@ -246,6 +384,35 @@ (list (quote of) left target))))) ((and (= typ "keyword") (= val "in")) (do (adv!) (list (quote in?) left (parse-expr)))) + ((and (= typ "keyword") (= val "does")) + (do + (adv!) + (match-kw "not") + (cond + ((match-kw "exist") + (list (quote not) (list (quote exists?) left))) + ((match-kw "match") + (list + (quote not) + (list (quote matches?) left (parse-expr)))) + ((or (match-kw "contain") (match-kw "contains")) + (list + (quote not) + (list (quote contains?) left (parse-expr)))) + ((or (match-kw "include") (match-kw "includes")) + (list + (quote not) + (list (quote contains?) left (parse-expr)))) + (true left)))) + ((and (= typ "keyword") (= val "equals")) + (do (adv!) (list (quote =) left (parse-expr)))) + ((and (= typ "keyword") (= val "really")) + (do + (adv!) + (match-kw "equals") + (list (quote strict-eq) left (parse-expr)))) + ((and (= typ "keyword") (or (= val "contain") (= val "include") (= val "includes"))) + (do (adv!) (list (quote contains?) left (parse-expr)))) (true left))))) (define parse-expr @@ -414,8 +581,24 @@ ((tgt (parse-tgt-kw "on" (list (quote me))))) (list (quote trigger) name tgt))))) (define parse-log-cmd (fn () (list (quote log) (parse-expr)))) - (define parse-inc-cmd (fn () (list (quote increment!) (parse-expr)))) - (define parse-dec-cmd (fn () (list (quote decrement!) (parse-expr)))) + (define + parse-inc-cmd + (fn + () + (let + ((expr (parse-expr))) + (let + ((tgt (parse-tgt-kw "on" (list (quote me))))) + (list (quote increment!) expr tgt))))) + (define + parse-dec-cmd + (fn + () + (let + ((expr (parse-expr))) + (let + ((tgt (parse-tgt-kw "on" (list (quote me))))) + (list (quote decrement!) expr tgt))))) (define parse-hide-cmd (fn @@ -519,18 +702,20 @@ (let ((typ (tp-type)) (val (tp-val))) (if - (and - (= typ "op") - (or - (= val "+") - (= val "-") - (= val "*") - (= val "/") - (= val "%"))) + (or + (and + (= typ "op") + (or + (= val "+") + (= val "-") + (= val "*") + (= val "/") + (= val "%"))) + (and (= typ "keyword") (= val "mod"))) (do (adv!) (let - ((op (cond ((= val "+") (quote +)) ((= val "-") (quote -)) ((= val "*") (quote *)) ((= val "/") (quote /)) ((= val "%") (make-symbol "%"))))) + ((op (cond ((= val "+") (quote +)) ((= val "-") (quote -)) ((= val "*") (quote *)) ((= val "/") (quote /)) ((or (= val "%") (= val "mod")) (make-symbol "%"))))) (let ((right (let ((a (parse-atom))) (if (nil? a) a (parse-poss a))))) (parse-arith (list op left right))))) diff --git a/lib/hyperscript/runtime.sx b/lib/hyperscript/runtime.sx index d26f8267..db0e6776 100644 --- a/lib/hyperscript/runtime.sx +++ b/lib/hyperscript/runtime.sx @@ -172,7 +172,13 @@ (n thunk) (define do-repeat - (fn (i) (when (< i n) (thunk) (do-repeat (+ i 1))))) + (fn + (i) + (when + (< i n) + (log (str "[hs-repeat] iteration " i " of " n)) + (thunk) + (do-repeat (+ i 1))))) (do-repeat 0))) ;; Repeat forever (until break — relies on exception/continuation). @@ -208,8 +214,8 @@ (fn (value type-name) (cond - ((= type-name "Int") (+ value 0)) - ((= type-name "Integer") (+ value 0)) + ((= type-name "Int") (floor (+ value 0))) + ((= type-name "Integer") (floor (+ value 0))) ((= type-name "Float") (+ value 0)) ((= type-name "Number") (+ value 0)) ((= type-name "String") (str value)) @@ -221,6 +227,15 @@ ;; Make a new object of a given type. ;; (hs-make type-name) — creates empty object/collection +(define + hs-add + (fn (a b) (if (or (string? a) (string? b)) (str a b) (+ a b)))) + +;; ── Behavior installation ─────────────────────────────────────── + +;; Install a behavior on an element. +;; A behavior is a function that takes (me ...params) and sets up features. +;; (hs-install behavior-fn me ...args) (define hs-make (fn @@ -232,25 +247,20 @@ ((= type-name "Map") (dict)) (true (dict))))) -;; ── Behavior installation ─────────────────────────────────────── - -;; Install a behavior on an element. -;; A behavior is a function that takes (me ...params) and sets up features. -;; (hs-install behavior-fn me ...args) -(define hs-install (fn (behavior-fn) (behavior-fn me))) - ;; ── Measurement ───────────────────────────────────────────────── ;; Measure an element's bounding rect, store as local variables. ;; Returns a dict with x, y, width, height, top, left, right, bottom. -(define - hs-measure - (fn (target) (perform (list (quote io-measure) target)))) +(define hs-install (fn (behavior-fn) (behavior-fn me))) ;; ── Transition ────────────────────────────────────────────────── ;; Transition a CSS property to a value, optionally with duration. ;; (hs-transition target prop value duration) +(define + hs-measure + (fn (target) (perform (list (quote io-measure) target)))) + (define hs-transition (fn @@ -262,4 +272,42 @@ "transition" (str prop " " (/ duration 1000) "s"))) (dom-set-style target prop value) - (when duration (hs-settle target)))) \ No newline at end of file + (when duration (hs-settle target)))) + +(define + hs-type-check + (fn + (value type-name) + (if + (nil? value) + true + (cond + ((= type-name "Number") (number? value)) + ((= type-name "String") (string? value)) + ((= type-name "Boolean") (or (= value true) (= value false))) + ((= type-name "Array") (list? value)) + ((= type-name "Object") (dict? value)) + (true true))))) + +(define + hs-type-check! + (fn + (value type-name) + (if (nil? value) false (hs-type-check value type-name)))) + +(define + hs-strict-eq + (fn (a b) (and (= (type-of a) (type-of b)) (= a b)))) + +(define + hs-falsy? + (fn (v) (or (nil? v) (= v false) (and (string? v) (= v ""))))) + +(define + hs-matches? + (fn + (target pattern) + (if + (string? target) + (if (= pattern ".*") true (string-contains? target pattern)) + false))) \ No newline at end of file diff --git a/lib/hyperscript/tokenizer.sx b/lib/hyperscript/tokenizer.sx index fce720ac..ab0eb3dc 100644 --- a/lib/hyperscript/tokenizer.sx +++ b/lib/hyperscript/tokenizer.sx @@ -139,7 +139,20 @@ "behavior" "called" "render" - "eval")) + "eval" + "I" + "am" + "does" + "some" + "mod" + "equal" + "equals" + "really" + "include" + "includes" + "contain" + "undefined" + "exist")) (define hs-keyword? (fn (word) (some (fn (k) (= k word)) hs-keywords))) diff --git a/spec/tests/test-hyperscript-compiler.sx b/spec/tests/test-hyperscript-compiler.sx index 69c7ec0a..4d2e28f7 100644 --- a/spec/tests/test-hyperscript-compiler.sx +++ b/spec/tests/test-hyperscript-compiler.sx @@ -57,10 +57,10 @@ (deftest "addition passes through" (let - ((sx (hs-to-sx-from-source "set x to 1 + 2"))) + ((sx (hs-to-sx-from-source "1 + 2"))) (let - ((val (nth sx 2))) - (assert= (quote +) (first val)) + ((val (first sx))) + (assert= (quote hs-add) (first val)) (assert= 1 (nth val 1)) (assert= 2 (nth val 2))))) (deftest @@ -287,4 +287,61 @@ "decrement attribute" (let ((sx (hs-to-sx-from-source "decrement @count"))) - (assert= (quote dom-set-attr) (first sx))))) \ No newline at end of file + (assert= (quote dom-set-attr) (first sx))))) + +(defsuite + "hs-live-demo-toggle" + (deftest + "toggle class on me compiles to single hs-on" + (let + ((sx (hs-to-sx-from-source "on click toggle .bg-violet-600 on me then toggle .text-white on me"))) + (assert= (quote hs-on) (first sx)) + (assert= "click" (nth sx 2)) + (let + ((body (nth (nth sx 3) 2))) + (assert= (quote do) (first body)) + (assert= 2 (len (rest body)))))) + (deftest + "bounce: then chains wait and remove in same handler" + (let + ((sx (hs-to-sx-from-source "on click add .animate-bounce to me then wait 1s then remove .animate-bounce from me"))) + (assert= (quote hs-on) (first sx)) + (assert= "click" (nth sx 2)) + (let + ((body (nth (nth sx 3) 2))) + (assert= (quote do) (first body)) + (assert= 3 (len (rest body))) + (assert= (quote hs-wait) (first (nth body 2))) + (assert= 1000 (nth (nth body 2) 1))))) + (deftest + "count clicks: then chains increment and set in same handler" + (let + ((sx (hs-to-sx-from-source "on click increment @data-count on me then set #click-counter's innerHTML to my @data-count"))) + (assert= (quote hs-on) (first sx)) + (assert= "click" (nth sx 2)) + (let + ((body (nth (nth sx 3) 2))) + (assert= (quote do) (first body)) + (assert= 2 (len (rest body))))))) + +(defsuite + "hs-wait-suspension" + (deftest + "wait in then chain keeps hs-wait (platform handles suspension)" + (let + ((sx (hs-to-sx-from-source "on click add .bounce to me then wait 1s then remove .bounce from me"))) + (assert= (quote hs-on) (first sx)) + (let + ((body (nth (nth sx 3) 2))) + (assert= (quote do) (first body)) + (assert= 3 (len (rest body))) + (assert= (quote hs-wait) (first (nth body 2))) + (assert= 1000 (nth (nth body 2) 1))))) + (deftest + "wait preserves ms value in handler" + (let + ((sx (hs-to-sx-from-source "on click add .a then wait 2s then add .b"))) + (let + ((body (nth (nth sx 3) 2))) + (assert= (quote hs-wait) (first (nth body 2))) + (assert= 2000 (nth (nth body 2) 1)))))) \ No newline at end of file diff --git a/spec/tests/test-hyperscript-conformance.sx b/spec/tests/test-hyperscript-conformance.sx new file mode 100644 index 00000000..fc4f2002 --- /dev/null +++ b/spec/tests/test-hyperscript-conformance.sx @@ -0,0 +1,826 @@ +;; _hyperscript conformance test fixtures +;; Auto-extracted from https://github.com/bigskysoftware/_hyperscript (v0.9.14) +;; 222 pure expression tests (no DOM dependency) +;; Generated: 2026-04-08T17:44:00.716Z + +;; ── eval-hs: compile + evaluate hyperscript source ────────────────── +(define + eval-hs + (fn + (src &rest opts) + (let + ((sx (hs-to-sx (hs-compile src))) + (ctx (if (> (len opts) 0) (first opts) nil))) + (if + (nil? ctx) + (eval-expr-cek + (list + (quote let) + (list + (list (quote me) nil) + (list (quote it) nil) + (list (quote result) nil)) + sx)) + (let + ((defaults (list (list (quote me) nil) (list (quote it) nil) (list (quote result) nil))) + (overrides (list))) + (do + (when + (get ctx "me") + (set! + overrides + (cons (list (quote me) (get ctx "me")) overrides))) + (when + (get ctx "locals") + (for-each + (fn + (k) + (set! + overrides + (cons + (list (make-symbol k) (get (get ctx "locals") k)) + overrides))) + (keys (get ctx "locals")))) + (eval-expr-cek + (list (quote let) defaults (list (quote let) overrides sx))))))))) + +;; ── run-hs-fixture: evaluate one test case ──────────────────────────── +(define run-hs-fixture + (fn (f) + (let ((src (get f "src")) + (expected (get f "expected")) + (ctx (if (or (get f "locals") (get f "me")) + {"locals" (get f "locals") "me" (get f "me")} + nil))) + (let ((result (if ctx (eval-hs src ctx) (eval-hs src)))) + (assert= result expected src))))) + +;; ── arrayIndex (1 fixtures) ────────────────────────────── +(defsuite "hs-compat-arrayIndex" + (deftest "can-create-an-array-literal" + (for-each run-hs-fixture + (list + {"src" "[1, 2, 3]" "expected" (list 1 2 3)} + ))) +) + +;; ── arrayLiteral (3 fixtures) ────────────────────────────── +(defsuite "hs-compat-arrayLiteral" + (deftest "empty-array-literals-work" + (for-each run-hs-fixture + (list + {"src" "[]" "expected" (list)} + ))) + (deftest "one-element-array-literal-works" + (for-each run-hs-fixture + (list + {"src" "[true]" "expected" (list true)} + ))) + (deftest "multi-element-array-literal-works" + (for-each run-hs-fixture + (list + {"src" "[true, false]" "expected" (list true false)} + ))) +) + +;; ── asExpression (19 fixtures) ────────────────────────────── +(defsuite "hs-compat-asExpression" + (deftest "converts-value-as-string" + (for-each run-hs-fixture + (list + {"src" "10 as String" "expected" "10"} + {"src" "true as String" "expected" "true"} + ))) + (deftest "converts-value-as-int" + (for-each run-hs-fixture + (list + {"src" "'10' as Int" "expected" 10} + {"src" "'10.4' as Int" "expected" 10} + ))) + (deftest "converts-value-as-float" + (for-each run-hs-fixture + (list + {"src" "'10' as Float" "expected" 10} + {"src" "'10.4' as Float" "expected" 10.4} + ))) + (deftest "converts-value-as-fixed" + (for-each run-hs-fixture + (list + {"src" "'10.4' as Fixed" "expected" "10"} + {"src" "'10.4899' as Fixed:2" "expected" "10.49"} + ))) + (deftest "converts-value-as-number" + (for-each run-hs-fixture + (list + {"src" "'10' as Number" "expected" 10} + {"src" "'10.4' as Number" "expected" 10.4} + ))) + (deftest "converts-value-as-json" + (for-each run-hs-fixture + (list + {"src" "{foo:'bar'} as JSON" "expected" "{\"foo\":\"bar\"}"} + ))) + (deftest "converts-string-as-object" + (for-each run-hs-fixture + (list + {"src" "'{\"foo\":\"bar\"}' as Object" "expected" "bar"} + ))) + (deftest "can-use-the-an-modifier-if-you" + (for-each run-hs-fixture + (list + {"src" "'{\"foo\":\"bar\"}' as an Object" "expected" "bar"} + ))) + (deftest "converts-value-as-object" + (for-each run-hs-fixture + (list + {"src" "x as Object" "expected" "bar"} + ))) + (deftest "converts-a-complete-form-into-values" + (for-each run-hs-fixture + (list + {"src" "x as Values" "expected" "John"} + ))) + (deftest "converts-numbers-things-" + (for-each run-hs-fixture + (list + {"src" "value as HTML" "expected" "123"} + ))) + (deftest "converts-strings-into-fragments" + (for-each run-hs-fixture + (list + {"src" "value as Fragment" "expected" 1} + ))) + (deftest "can-accept-custom-conversions" + (for-each run-hs-fixture + (list + {"src" "1 as Foo" "expected" "foo1"} + ))) + (deftest "-" + (for-each run-hs-fixture + (list + {"src" "1 as Foo:Bar" "expected" "Bar1"} + ))) +) + +;; ── blockLiteral (4 fixtures) ────────────────────────────── +(defsuite "hs-compat-blockLiteral" + (deftest "basic-block-literals-work" + (for-each run-hs-fixture + (list + {"src" "\\\\-> true" "expected" true} + ))) + (deftest "basic-identity-works" + (for-each run-hs-fixture + (list + {"src" "\\\\ x -> x" "expected" true} + ))) + (deftest "basic-two-arg-identity-works" + (for-each run-hs-fixture + (list + {"src" "\\\\ x, y -> y" "expected" true} + ))) + (deftest "can-map-an-array" + (for-each run-hs-fixture + (list + {"src" "['a', 'ab', 'abc'].map(\\\\ s -> s.length )" "expected" (list 1 2 3)} + ))) +) + +;; ── boolean (2 fixtures) ────────────────────────────── +(defsuite "hs-compat-boolean" + (deftest "true-boolean-literals-work" + (for-each run-hs-fixture + (list + {"src" "true" "expected" true} + ))) + (deftest "false-boolean-literals-work" + (for-each run-hs-fixture + (list + {"src" "false" "expected" false} + ))) +) + +;; ── classRef (1 fixtures) ────────────────────────────── +(defsuite "hs-compat-classRef" + (deftest "basic-classref-works-w-no-match" + (for-each run-hs-fixture + (list + {"src" ".badClassThatDoesNotHaveAnyElements" "expected" 0} + ))) +) + +;; ── comparisonOperator (113 fixtures) ────────────────────────────── +(defsuite "hs-compat-comparisonOperator" + (deftest "less-than-works" + (for-each run-hs-fixture + (list + {"src" "1 < 2" "expected" true} + {"src" "2 < 1" "expected" false} + {"src" "2 < 2" "expected" false} + ))) + (deftest "less-than-or-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 <= 2" "expected" true} + {"src" "2 <= 1" "expected" false} + {"src" "2 <= 2" "expected" true} + ))) + (deftest "greater-than-works" + (for-each run-hs-fixture + (list + {"src" "1 > 2" "expected" false} + {"src" "2 > 1" "expected" true} + {"src" "2 > 2" "expected" false} + ))) + (deftest "greater-than-or-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 >= 2" "expected" false} + {"src" "2 >= 1" "expected" true} + {"src" "2 >= 2" "expected" true} + ))) + (deftest "equal-works" + (for-each run-hs-fixture + (list + {"src" "1 == 2" "expected" false} + {"src" "2 == 1" "expected" false} + {"src" "2 == 2" "expected" true} + ))) + (deftest "triple-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 === 2" "expected" false} + {"src" "2 === 1" "expected" false} + {"src" "2 === 2" "expected" true} + ))) + (deftest "not-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 != 2" "expected" true} + {"src" "2 != 1" "expected" true} + {"src" "2 != 2" "expected" false} + ))) + (deftest "triple-not-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 !== 2" "expected" true} + {"src" "2 !== 1" "expected" true} + {"src" "2 !== 2" "expected" false} + ))) + (deftest "is-works" + (for-each run-hs-fixture + (list + {"src" "1 is 2" "expected" false} + {"src" "2 is 1" "expected" false} + {"src" "2 is 2" "expected" true} + ))) + (deftest "equals-works" + (for-each run-hs-fixture + (list + {"src" "1 equals 2" "expected" false} + {"src" "2 equals 1" "expected" false} + {"src" "2 equals 2" "expected" true} + ))) + (deftest "is-equal-to-works" + (for-each run-hs-fixture + (list + {"src" "1 is equal to 2" "expected" false} + {"src" "2 is equal to 1" "expected" false} + {"src" "2 is equal to 2" "expected" true} + ))) + (deftest "is-really-equal-to-works" + (for-each run-hs-fixture + (list + {"src" "1 is really equal to 2" "expected" false} + {"src" "2 is really equal to 1" "expected" false} + {"src" "2 is really equal to '2'" "expected" false} + {"src" "2 is really equal to 2" "expected" true} + ))) + (deftest "really-equals-works" + (for-each run-hs-fixture + (list + {"src" "1 really equals 2" "expected" false} + {"src" "2 really equals 1" "expected" false} + {"src" "2 really equals 2" "expected" true} + ))) + (deftest "is-not-works" + (for-each run-hs-fixture + (list + {"src" "1 is not 2" "expected" true} + {"src" "2 is not 1" "expected" true} + {"src" "2 is not 2" "expected" false} + ))) + (deftest "is-not-equal-to-works" + (for-each run-hs-fixture + (list + {"src" "1 is not equal to 2" "expected" true} + {"src" "2 is not equal to 1" "expected" true} + {"src" "2 is not equal to 2" "expected" false} + ))) + (deftest "is-not-really-equal-to-works" + (for-each run-hs-fixture + (list + {"src" "1 is not really equal to 2" "expected" true} + {"src" "2 is not really equal to 1" "expected" true} + {"src" "2 is not really equal to '2'" "expected" true} + {"src" "2 is not really equal to 2" "expected" false} + ))) + (deftest "is-in-works" + (for-each run-hs-fixture + (list + {"src" "1 is in [1, 2]" "expected" true} + {"src" "2 is in [1, 2]" "expected" true} + {"src" "3 is in [1, 2]" "expected" false} + {"src" "3 is in null" "expected" false} + ))) + (deftest "is-not-in-works" + (for-each run-hs-fixture + (list + {"src" "1 is not in [1, 2]" "expected" false} + {"src" "2 is not in [1, 2]" "expected" false} + {"src" "3 is not in [1, 2]" "expected" true} + {"src" "3 is not in null" "expected" true} + ))) + (deftest "i-am-in-works" + (for-each run-hs-fixture + (list + {"src" "I am in [1, 2]" "expected" true "me" 1} + {"src" "I am in [1, 2]" "expected" true "me" 2} + {"src" "I am in [1, 2]" "expected" false "me" 3} + {"src" "I am in null" "expected" false} + ))) + (deftest "i-am-not-in-works" + (for-each run-hs-fixture + (list + {"src" "I am not in [1, 2]" "expected" false "me" 1} + {"src" "I am not in [1, 2]" "expected" false "me" 2} + {"src" "I am not in [1, 2]" "expected" true "me" 3} + {"src" "I am not in null" "expected" true} + ))) + (deftest "match-works-w-strings" + (for-each run-hs-fixture + (list + {"src" "'a' matches '.*'" "expected" true} + {"src" "'a' matches 'b'" "expected" false} + ))) + (deftest "does-not-match-works-w-strings" + (for-each run-hs-fixture + (list + {"src" "'a' does not match '.*'" "expected" false} + {"src" "'a' does not match 'b'" "expected" true} + ))) + (deftest "is-empty-works" + (for-each run-hs-fixture + (list + {"src" "undefined is empty" "expected" true} + {"src" "'' is empty" "expected" true} + {"src" "[] is empty" "expected" true} + {"src" "'not empty' is empty" "expected" false} + {"src" "1000 is empty" "expected" false} + {"src" "[1,2,3] is empty" "expected" false} + {"src" ".aClassThatDoesNotExist is empty" "expected" true} + ))) + (deftest "is-not-empty-works" + (for-each run-hs-fixture + (list + {"src" "undefined is not empty" "expected" false} + {"src" "'' is not empty" "expected" false} + {"src" "[] is not empty" "expected" false} + {"src" "'not empty' is not empty" "expected" true} + {"src" "1000 is not empty" "expected" true} + {"src" "[1,2,3] is not empty" "expected" true} + ))) + (deftest "is-a-works" + (for-each run-hs-fixture + (list + {"src" "null is a String" "expected" true} + {"src" "null is a String!" "expected" false} + {"src" "'' is a String!" "expected" true} + ))) + (deftest "is-not-a-works" + (for-each run-hs-fixture + (list + {"src" "null is not a String" "expected" false} + {"src" "null is not a String!" "expected" true} + {"src" "'' is not a String!" "expected" false} + ))) + (deftest "is-an-works" + (for-each run-hs-fixture + (list + {"src" "null is an String" "expected" true} + {"src" "null is an String!" "expected" false} + {"src" "'' is an String!" "expected" true} + ))) + (deftest "is-not-an-works" + (for-each run-hs-fixture + (list + {"src" "null is not an String" "expected" false} + {"src" "null is not an String!" "expected" true} + {"src" "'' is not an String!" "expected" false} + ))) + (deftest "english-less-than-works" + (for-each run-hs-fixture + (list + {"src" "1 is less than 2" "expected" true} + {"src" "2 is less than 1" "expected" false} + {"src" "2 is less than 2" "expected" false} + ))) + (deftest "english-less-than-or-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 is less than or equal to 2" "expected" true} + {"src" "2 is less than or equal to 1" "expected" false} + {"src" "2 is less than or equal to 2" "expected" true} + ))) + (deftest "english-greater-than-works" + (for-each run-hs-fixture + (list + {"src" "1 is greater than 2" "expected" false} + {"src" "2 is greater than 1" "expected" true} + {"src" "2 is greater than 2" "expected" false} + ))) + (deftest "english-greater-than-or-equal-works" + (for-each run-hs-fixture + (list + {"src" "1 is greater than or equal to 2" "expected" false} + {"src" "2 is greater than or equal to 1" "expected" true} + {"src" "2 is greater than or equal to 2" "expected" true} + ))) + (deftest "does-not-exist-works" + (for-each run-hs-fixture + (list + {"src" "undefined does not exist" "expected" true} + {"src" "null does not exist" "expected" true} + {"src" "#doesNotExist does not exist" "expected" true} + {"src" ".aClassThatDoesNotExist does not exist" "expected" true} + {"src" "<.aClassThatDoesNotExist/> does not exist" "expected" true} + {"src" " does not exist" "expected" false} + ))) +) + +;; ── cookies (9 fixtures) ────────────────────────────── +(defsuite "hs-compat-cookies" + (deftest "basic-set-cookie-values-work" + (for-each run-hs-fixture + (list + {"src" "cookies.foo" "expected" "bar"} + {"src" "set cookies.foo to 'bar'" "expected" "bar"} + {"src" "cookies.foo" "expected" "bar"} + ))) + (deftest "update-cookie-values-work" + (for-each run-hs-fixture + (list + {"src" "set cookies.foo to 'bar'" "expected" "bar"} + {"src" "cookies.foo" "expected" "bar"} + {"src" "set cookies.foo to 'doh'" "expected" "doh"} + {"src" "cookies.foo" "expected" "doh"} + ))) + (deftest "iterate-cookies-values-work" + (for-each run-hs-fixture + (list + {"src" "set cookies.foo to 'bar'" "expected" true} + {"src" "for x in cookies me.push(x.name) then you.push(x.value) end" "expected" true} + ))) +) + +;; ── in (4 fixtures) ────────────────────────────── +(defsuite "hs-compat-in" + (deftest "basic-no-query-return-values" + (for-each run-hs-fixture + (list + {"src" "1 in [1, 2, 3]" "expected" (list 1)} + {"src" "[1, 3] in [1, 2, 3]" "expected" (list 1 3)} + {"src" "[1, 3, 4] in [1, 2, 3]" "expected" (list 1 3)} + {"src" "[4, 5, 6] in [1, 2, 3]" "expected" (list)} + ))) +) + +;; ── logicalOperator (2 fixtures) ────────────────────────────── +(defsuite "hs-compat-logicalOperator" + (deftest "should-short-circuit-with-and-expression" + (for-each run-hs-fixture + (list + {"src" "func1() and func2()" "expected" false} + ))) + (deftest "should-short-circuit-with-or-expression" + (for-each run-hs-fixture + (list + {"src" "func1() or func2()" "expected" true} + ))) +) + +;; ── mathOperator (8 fixtures) ────────────────────────────── +(defsuite "hs-compat-mathOperator" + (deftest "addition-works" + (for-each run-hs-fixture + (list + {"src" "1 + 1" "expected" 2} + ))) + (deftest "string-concat-works" + (for-each run-hs-fixture + (list + {"src" "'a' + 'b'" "expected" "ab"} + ))) + (deftest "subtraction-works" + (for-each run-hs-fixture + (list + {"src" "1 - 1" "expected" 0} + ))) + (deftest "multiplication-works" + (for-each run-hs-fixture + (list + {"src" "1 * 2" "expected" 2} + ))) + (deftest "division-works" + (for-each run-hs-fixture + (list + {"src" "1 / 2" "expected" 0.5} + ))) + (deftest "mod-works" + (for-each run-hs-fixture + (list + {"src" "3 mod 2" "expected" 1} + ))) + (deftest "addition-works-w-more-than-one-value" + (for-each run-hs-fixture + (list + {"src" "1 + 2 + 3" "expected" 6} + ))) + (deftest "parenthesized-expressions-with-multiple-operators-work" + (for-each run-hs-fixture + (list + {"src" "1 + (2 * 3)" "expected" 7} + ))) +) + +;; ── no (5 fixtures) ────────────────────────────── +(defsuite "hs-compat-no" + (deftest "no-returns-true-for-null" + (for-each run-hs-fixture + (list + {"src" "no null" "expected" true} + ))) + (deftest "no-returns-false-for-non-null" + (for-each run-hs-fixture + (list + {"src" "no 'thing'" "expected" false} + {"src" "no ['thing']" "expected" false} + ))) + (deftest "no-returns-true-for-empty-array" + (for-each run-hs-fixture + (list + {"src" "no []" "expected" true} + ))) + (deftest "no-returns-true-for-empty-selector" + (for-each run-hs-fixture + (list + {"src" "no .aClassThatDoesNotExist" "expected" true} + ))) +) + +;; ── not (3 fixtures) ────────────────────────────── +(defsuite "hs-compat-not" + (deftest "not-inverts-true" + (for-each run-hs-fixture + (list + {"src" "not true" "expected" false} + ))) + (deftest "not-inverts-false" + (for-each run-hs-fixture + (list + {"src" "not false" "expected" true} + ))) + (deftest "two-nots-make-a-true" + (for-each run-hs-fixture + (list + {"src" "not not true" "expected" true} + ))) +) + +;; ── numbers (4 fixtures) ────────────────────────────── +(defsuite "hs-compat-numbers" + (deftest "handles-numbers-properly" + (for-each run-hs-fixture + (list + {"src" "-1" "expected" -1} + {"src" "1" "expected" 1} + {"src" "1.1" "expected" 1.1} + {"src" "1234567890.1234567890" "expected" 1234567890.1234567} + ))) +) + +;; ── objectLiteral (3 fixtures) ────────────────────────────── +(defsuite "hs-compat-objectLiteral" + (deftest "empty-object-literals-work" + (for-each run-hs-fixture + (list + {"src" "{}" "expected" {}} + ))) + (deftest "hyphens-work-in-object-literal-field-names" + (for-each run-hs-fixture + (list + {"src" "{-foo:true, bar-baz:false}" "expected" {"-foo" true "bar-baz" false}} + ))) + (deftest "allows-trailing-commans" + (for-each run-hs-fixture + (list + {"src" "{foo:true, bar-baz:false,}" "expected" {"foo" true "bar-baz" false}} + ))) +) + +;; ── positionalExpression (2 fixtures) ────────────────────────────── +(defsuite "hs-compat-positionalExpression" + (deftest "first-works" + (for-each run-hs-fixture + (list + {"src" "the first of [1, 2, 3]" "expected" 1} + ))) + (deftest "last-works" + (for-each run-hs-fixture + (list + {"src" "the last of [1, 2, 3]" "expected" 3} + ))) +) + +;; ── possessiveExpression (2 fixtures) ────────────────────────────── +(defsuite "hs-compat-possessiveExpression" + (deftest "can-access-basic-properties" + (for-each run-hs-fixture + (list + {"src" "foo's foo" "expected" "foo"} + ))) + (deftest "can-access-its-properties" + (for-each run-hs-fixture + (list + {"src" "its foo" "expected" "foo"} + ))) +) + +;; ── propertyAccess (4 fixtures) ────────────────────────────── +(defsuite "hs-compat-propertyAccess" + (deftest "can-access-basic-properties" + (for-each run-hs-fixture + (list + {"src" "foo.foo" "expected" "foo"} + ))) + (deftest "of-form-works" + (for-each run-hs-fixture + (list + {"src" "foo of foo" "expected" "foo"} + ))) + (deftest "of-form-works-w-complex-left-side" + (for-each run-hs-fixture + (list + {"src" "bar.doh of foo" "expected" "foo"} + ))) + (deftest "of-form-works-w-complex-right-side" + (for-each run-hs-fixture + (list + {"src" "doh of foo.bar" "expected" "foo"} + ))) +) + +;; ── queryRef (1 fixtures) ────────────────────────────── +(defsuite "hs-compat-queryRef" + (deftest "basic-queryref-works-w-no-match" + (for-each run-hs-fixture + (list + {"src" "<.badClassThatDoesNotHaveAnyElements/>" "expected" 0} + ))) +) + +;; ── some (6 fixtures) ────────────────────────────── +(defsuite "hs-compat-some" + (deftest "some-returns-false-for-null" + (for-each run-hs-fixture + (list + {"src" "some null" "expected" false} + ))) + (deftest "some-returns-true-for-non-null" + (for-each run-hs-fixture + (list + {"src" "some 'thing'" "expected" true} + ))) + (deftest "some-returns-false-for-empty-array" + (for-each run-hs-fixture + (list + {"src" "some []" "expected" false} + ))) + (deftest "some-returns-false-for-empty-selector" + (for-each run-hs-fixture + (list + {"src" "some .aClassThatDoesNotExist" "expected" false} + ))) + (deftest "some-returns-true-for-nonempty-selector" + (for-each run-hs-fixture + (list + {"src" "some " "expected" true} + ))) + (deftest "some-returns-true-for-filled-array" + (for-each run-hs-fixture + (list + {"src" "some ['thing']" "expected" true} + ))) +) + +;; ── stringPostfix (10 fixtures) ────────────────────────────── +(defsuite "hs-compat-stringPostfix" + (deftest "handles-basic-postfix-strings-properly" + (for-each run-hs-fixture + (list + {"src" "1em" "expected" "1em"} + {"src" "1px" "expected" "1px"} + {"src" "-1px" "expected" "-1px"} + {"src" "100%" "expected" "100%"} + ))) + (deftest "handles-basic-postfix-strings-with-spaces-properly" + (for-each run-hs-fixture + (list + {"src" "1 em" "expected" "1em"} + {"src" "1 px" "expected" "1px"} + {"src" "100 %" "expected" "100%"} + ))) + (deftest "handles-expression-roots-properly" + (for-each run-hs-fixture + (list + {"src" "(0 + 1) em" "expected" "1em"} + {"src" "(0 + 1) px" "expected" "1px"} + {"src" "(100 + 0) %" "expected" "100%"} + ))) +) + +;; ── strings (11 fixtures) ────────────────────────────── +(defsuite "hs-compat-strings" + (deftest "handles-strings-properly" + (for-each run-hs-fixture + (list + {"src" "\"foo\"" "expected" "foo"} + {"src" "\"fo'o\"" "expected" "fo'o"} + {"src" "'foo'" "expected" "foo"} + ))) + (deftest "string-templates-work-properly" + (for-each run-hs-fixture + (list + {"src" "`$1`" "expected" "1"} + ))) + (deftest "string-templates-work-properly-w-braces" + (for-each run-hs-fixture + (list + {"src" "`${1 + 2}`" "expected" "3"} + ))) + (deftest "string-templates-preserve-white-space" + (for-each run-hs-fixture + (list + {"src" "` ${1 + 2} ${1 + 2} `" "expected" " 3 3 "} + {"src" "`${1 + 2} ${1 + 2} `" "expected" "3 3 "} + {"src" "`${1 + 2}${1 + 2} `" "expected" "33 "} + {"src" "`${1 + 2} ${1 + 2}`" "expected" "3 3"} + ))) + (deftest "should-handle-strings-with-tags-and-quotes" + (for-each run-hs-fixture + (list + {"src" "`
${record.name}
`" "expected" "
John Connor
"} + ))) + (deftest "should-handle-back-slashes-in-non-template-content" + (for-each run-hs-fixture + (list + {"src" "`https://${foo}`" "expected" "https://bar" "locals" {"foo" "bar"}} + ))) +) + +;; ── symbol (1 fixtures) ────────────────────────────── +(defsuite "hs-compat-symbol" + (deftest "resolves-local-context-properly" + (for-each run-hs-fixture + (list + {"src" "foo" "expected" 42 "locals" {"foo" 42}} + ))) +) + +;; ── typecheck (4 fixtures) ────────────────────────────── +(defsuite "hs-compat-typecheck" + (deftest "can-do-basic-string-typecheck" + (for-each run-hs-fixture + (list + {"src" "'foo' : String" "expected" "foo"} + ))) + (deftest "can-do-basic-non-string-typecheck-failure" + (for-each run-hs-fixture + (list + {"src" "true : String" "expected" 0} + ))) + (deftest "can-do-basic-string-non-null-typecheck" + (for-each run-hs-fixture + (list + {"src" "'foo' : String!" "expected" "foo"} + ))) + (deftest "null-causes-null-safe-string-check-to-fail" + (for-each run-hs-fixture + (list + {"src" "null : String!" "expected" 0} + ))) +) + +;; ── Summary ────────────────────────────────────────────────────────── +;; 24 suites, 112 tests, 222 fixtures +