diff --git a/sx/sx/sx-tools-editor.sx b/sx/sx/sx-tools-editor.sx index badb8d12..5cd2e37e 100644 --- a/sx/sx/sx-tools-editor.sx +++ b/sx/sx/sx-tools-editor.sx @@ -1 +1 @@ -(defisland ~sx-tools/tree-editor () (let ((source (signal "(defcomp ~card (&key title subtitle)\n (div :class \"card\"\n (h2 title)\n (when subtitle\n (p :class \"sub\" subtitle))))")) (view-mode (signal "tree")) (selected-path (signal nil)) (parsed (signal nil)) (bindings (signal (dict)))) (letrec ((fmt-path (fn (path) (str "[" (join "," (map str path)) "]"))) (node-disp (fn (node) (cond (nil? node) "nil" (= (type-of node) "symbol") (symbol-name node) (= (type-of node) "keyword") (str ":" (keyword-name node)) (string? node) (let ((s (if (> (len node) 35) (str (slice node 0 32) "...") node))) (str "\"" s "\"")) (number? node) (str node) (= (type-of node) "boolean") (if node "true" "false") (list? node) (if (empty? node) "()" (str "(" (node-disp (first node)) (if (> (len node) 1) " ..." "") ")")) :else (str node)))) (is-compact (fn (node) (and (list? node) (<= (len node) 4) (not (some (fn (c) (list? c)) (rest node)))))) (build-entries (fn (node path depth result) (if (list? node) (if (empty? node) (append! result {:expandable false :depth depth :text "()" :is-list false :path path}) (if (is-compact node) (append! result {:expandable false :depth depth :text (str "(" (join " " (map node-disp node)) ")") :is-list true :path path}) (do (append! result {:expandable true :depth depth :text (str "(" (node-disp (first node))) :is-list true :path path}) (for-each (fn (i) (build-entries (nth node i) (concat path (list i)) (+ depth 1) result)) (range 1 (len node)))))) (append! result {:expandable false :depth depth :text (node-disp node) :is-list false :path path})))) (nav (fn (tree path) (reduce (fn (current idx) (if (or (nil? current) (not (list? current)) (>= idx (len current))) nil (nth current idx))) tree path))) (build-context (fn (tree path) (let ((result (list))) (for-each (fn (depth) (let ((prefix (slice path 0 (+ depth 1))) (node (nav tree prefix))) (when (not (nil? node)) (let ((is-target (= (+ depth 1) (len path)))) (append! result {:depth depth :is-target is-target :text (if (list? node) (if (empty? node) "()" (let ((head (node-disp (first node)))) (if (> (len node) 3) (str "(" head " " (node-disp (nth node 1)) " ...)") (str "(" (join " " (map node-disp node)) ")")))) (node-disp node)) :path prefix}))))) (range 0 (len path))) result))) (do-validate (fn (tree) (let ((errors (list))) (letrec ((check (fn (node path) (when (list? node) (when (not (empty? node)) (let ((head (first node))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "letrec")) (>= (len node) 2)) (let ((bindings (nth node 1))) (when (list? bindings) (for-each (fn (i) (let ((pair (nth bindings i))) (when (not (and (list? pair) (>= (len pair) 2) (= (type-of (first pair)) "symbol"))) (append! errors (str "Binding " i " at " (fmt-path (concat path (list 1 i))) " is not a (name value) pair"))))) (range 0 (len bindings)))))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "defcomp") (= (symbol-name head) "defisland"))) (when (< (len node) 4) (append! errors (str (symbol-name head) " at " (fmt-path path) " missing body")))))) (for-each (fn (i) (check (nth node i) (concat path (list i)))) (range 0 (len node))))))) (for-each (fn (i) (check (nth tree i) (list i))) (range 0 (len tree)))) (if (empty? errors) "OK" (join "\n" errors))))) (extract-params (fn (expr) (let ((head (if (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol")) (symbol-name (first expr)) nil)) (params-list (if (and (list? expr) (> (len expr) 2)) (nth expr 2) nil))) (if (and (or (= head "defcomp") (= head "defisland")) (list? params-list)) (let ((result (list)) (in-key false)) (for-each (fn (p) (cond (and (= (type-of p) "symbol") (= (symbol-name p) "&key")) (set! in-key true) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")) (set! in-key false) in-key (cond (and (list? p) (not (empty? p))) (append! result (if (= (type-of (first p)) "symbol") (symbol-name (first p)) (str (first p)))) (= (type-of p) "symbol") (append! result (symbol-name p)) :else nil))) params-list) result) (list)))))) (reset! parsed (sx-parse (deref source))) (div :class "space-y-4" (div :class "space-y-2" (label :class "text-sm font-medium text-stone-700" "SX Source") (textarea :class "w-full font-mono text-xs bg-stone-50 border border-stone-200 rounded p-3 leading-relaxed" :rows 6 :bind source)) (div :class "flex gap-2 items-center" (button :class "px-3 py-1 text-xs rounded bg-stone-700 text-white hover:bg-stone-800" :on-click (fn (e) (reset! parsed (sx-parse (deref source))) (reset! selected-path nil)) "Parse") (for-each (fn (mode) (button :class (str "px-3 py-1 text-xs rounded border transition-colors " (if (= (deref view-mode) mode) "bg-violet-600 text-white border-violet-600" "bg-white text-stone-600 border-stone-200 hover:border-violet-300")) :on-click (fn (e) (reset! view-mode mode)) mode)) (list "tree" "context" "validate" "render" "hypersx"))) (when (= (deref view-mode) "render") (let ((tree (deref parsed)) (all-params (let ((acc (list))) (when (and tree (not (empty? tree))) (for-each (fn (expr) (for-each (fn (p) (when (not (some (fn (x) (= x p)) acc)) (append! acc p))) (extract-params expr))) tree)) acc))) (when (not (empty? all-params)) (div :class "space-y-2 p-3 bg-violet-50 rounded border border-violet-200" (div :class "text-xs font-semibold text-violet-700 mb-1" "Parameters") (map (fn (pname) (div :class "flex items-center gap-2" (label :class "text-xs font-mono text-stone-600 w-24 text-right" pname) (input :type "text" :class "flex-1 px-2 py-1 text-xs font-mono border border-stone-300 rounded bg-white" :placeholder (str pname " value") :on-input (fn (e) (reset! bindings (assoc (deref bindings) pname (element-value (host-get e "target")))))))) all-params))))) (if (or (nil? (deref parsed)) (empty? (deref parsed))) (div :class "text-red-500 text-sm font-mono p-3 bg-red-50 rounded" "Parse error — check your s-expression syntax") (div :class "bg-stone-50 rounded border border-stone-200 p-3 font-mono text-xs leading-relaxed overflow-x-auto" (cond (= (deref view-mode) "tree") (let ((entries (list)) (tree (deref parsed))) (for-each (fn (i) (build-entries (nth tree i) (list i) 0 entries)) (range 0 (len tree))) (div :class "space-y-0" (map (fn (entry) (let ((path (get entry "path")) (sel (deref selected-path)) (is-selected (and (not (nil? sel)) (= (fmt-path sel) (fmt-path path))))) (div :class (str "py-0.5 cursor-pointer hover:bg-violet-50 rounded px-1 " (if is-selected "bg-violet-100" "")) :style (str "padding-left:" (* (get entry "depth") 16) "px") :on-click (fn (e) (reset! selected-path path)) (span :class "text-stone-400 mr-2 select-none" (fmt-path path)) (span :class (if (get entry "is-list") "text-sky-700" "text-stone-600") (get entry "text"))))) entries))) (= (deref view-mode) "context") (if (nil? (deref selected-path)) (p :class "text-stone-400 italic" "Click a node in tree view to see its context") (let ((ctx (build-context (deref parsed) (deref selected-path)))) (div :class "space-y-1" (map (fn (item) (div :style (str "padding-left:" (* (get item "depth") 16) "px") :class (if (get item "is-target") "text-violet-700 font-semibold" "text-stone-600") (span :class "text-stone-400 mr-2" (if (get item "is-target") "→ " " ") (fmt-path (get item "path"))) (span (get item "text")))) ctx)))) (= (deref view-mode) "validate") (let ((result (do-validate (deref parsed)))) (div :class (if (= result "OK") "text-emerald-600" "text-amber-600 whitespace-pre-wrap") result)) (= (deref view-mode) "render") (let ((tree (deref parsed))) (div :class "prose prose-sm max-w-none font-sans" (map (fn (expr) (if (not (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol"))) (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)) (let ((head-name (symbol-name (first expr)))) (cond (is-html-tag? head-name) expr (or (= head-name "defcomp") (= head-name "defisland")) (let ((full-name (if (= (type-of (nth expr 1)) "symbol") (symbol-name (nth expr 1)) (str (nth expr 1))))) (sx-load-components (sx-serialize expr)) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " full-name " — live preview")) (div :class "border border-violet-200 rounded p-4" (list (make-symbol full-name))))) (= head-name "let") (last expr) :else (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)))))) tree))) (= (deref view-mode) "hypersx") (pre :class "font-mono text-xs text-stone-700 whitespace-pre-wrap overflow-x-auto" (sx->hypersx (deref parsed))) :else (p "Select a view mode")))))))) +(defisland ~sx-tools/tree-editor () (let ((source (signal "(defcomp ~card (&key title subtitle)\n (div :class \"card\"\n (h2 title)\n (when subtitle\n (p :class \"sub\" subtitle))))")) (view-mode (signal "tree")) (selected-path (signal nil)) (parsed (signal nil)) (bindings (signal (dict)))) (letrec ((fmt-path (fn (path) (str "[" (join "," (map str path)) "]"))) (node-disp (fn (node) (cond (nil? node) "nil" (= (type-of node) "symbol") (symbol-name node) (= (type-of node) "keyword") (str ":" (keyword-name node)) (string? node) (let ((s (if (> (len node) 35) (str (slice node 0 32) "...") node))) (str "\"" s "\"")) (number? node) (str node) (= (type-of node) "boolean") (if node "true" "false") (list? node) (if (empty? node) "()" (str "(" (node-disp (first node)) (if (> (len node) 1) " ..." "") ")")) :else (str node)))) (is-compact (fn (node) (and (list? node) (<= (len node) 4) (not (some (fn (c) (list? c)) (rest node)))))) (build-entries (fn (node path depth result) (if (list? node) (if (empty? node) (append! result {:expandable false :depth depth :text "()" :is-list false :path path}) (if (is-compact node) (append! result {:expandable false :depth depth :text (str "(" (join " " (map node-disp node)) ")") :is-list true :path path}) (do (append! result {:expandable true :depth depth :text (str "(" (node-disp (first node))) :is-list true :path path}) (for-each (fn (i) (build-entries (nth node i) (concat path (list i)) (+ depth 1) result)) (range 1 (len node)))))) (append! result {:expandable false :depth depth :text (node-disp node) :is-list false :path path})))) (nav (fn (tree path) (reduce (fn (current idx) (if (or (nil? current) (not (list? current)) (>= idx (len current))) nil (nth current idx))) tree path))) (build-context (fn (tree path) (let ((result (list))) (for-each (fn (depth) (let ((prefix (slice path 0 (+ depth 1))) (node (nav tree prefix))) (when (not (nil? node)) (let ((is-target (= (+ depth 1) (len path)))) (append! result {:depth depth :is-target is-target :text (if (list? node) (if (empty? node) "()" (let ((head (node-disp (first node)))) (if (> (len node) 3) (str "(" head " " (node-disp (nth node 1)) " ...)") (str "(" (join " " (map node-disp node)) ")")))) (node-disp node)) :path prefix}))))) (range 0 (len path))) result))) (do-validate (fn (tree) (let ((errors (list))) (letrec ((check (fn (node path) (when (list? node) (when (not (empty? node)) (let ((head (first node))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "letrec")) (>= (len node) 2)) (let ((bindings (nth node 1))) (when (list? bindings) (for-each (fn (i) (let ((pair (nth bindings i))) (when (not (and (list? pair) (>= (len pair) 2) (= (type-of (first pair)) "symbol"))) (append! errors (str "Binding " i " at " (fmt-path (concat path (list 1 i))) " is not a (name value) pair"))))) (range 0 (len bindings)))))) (when (and (= (type-of head) "symbol") (or (= (symbol-name head) "defcomp") (= (symbol-name head) "defisland"))) (when (< (len node) 4) (append! errors (str (symbol-name head) " at " (fmt-path path) " missing body")))))) (for-each (fn (i) (check (nth node i) (concat path (list i)))) (range 0 (len node))))))) (for-each (fn (i) (check (nth tree i) (list i))) (range 0 (len tree)))) (if (empty? errors) "OK" (join "\n" errors))))) (extract-params (fn (expr) (let ((head (if (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol")) (symbol-name (first expr)) nil)) (params-list (if (and (list? expr) (> (len expr) 2)) (nth expr 2) nil))) (if (and (or (= head "defcomp") (= head "defisland")) (list? params-list)) (let ((result (list)) (in-key false)) (for-each (fn (p) (cond (and (= (type-of p) "symbol") (= (symbol-name p) "&key")) (set! in-key true) (and (= (type-of p) "symbol") (= (symbol-name p) "&rest")) (set! in-key false) in-key (cond (and (list? p) (not (empty? p))) (append! result (if (= (type-of (first p)) "symbol") (symbol-name (first p)) (str (first p)))) (= (type-of p) "symbol") (append! result (symbol-name p)) :else nil))) params-list) result) (list)))))) (reset! parsed (sx-parse (deref source))) (div :class "space-y-4" (div :class "space-y-2" (label :class "text-sm font-medium text-stone-700" "SX Source") (textarea :class "w-full font-mono text-xs bg-stone-50 border border-stone-200 rounded p-3 leading-relaxed" :rows 6 :bind source)) (div :class "flex gap-2 items-center" (button :class "px-3 py-1 text-xs rounded bg-stone-700 text-white hover:bg-stone-800" :on-click (fn (e) (reset! parsed (sx-parse (deref source))) (reset! selected-path nil)) "Parse") (for-each (fn (mode) (button :class (str "px-3 py-1 text-xs rounded border transition-colors " (if (= (deref view-mode) mode) "bg-violet-600 text-white border-violet-600" "bg-white text-stone-600 border-stone-200 hover:border-violet-300")) :on-click (fn (e) (reset! view-mode mode)) mode)) (list "tree" "context" "validate" "render" "hypersx"))) (when (= (deref view-mode) "render") (let ((tree (deref parsed)) (all-params (let ((acc (list))) (when (and tree (not (empty? tree))) (for-each (fn (expr) (for-each (fn (p) (when (not (some (fn (x) (= x p)) acc)) (append! acc p))) (extract-params expr))) tree)) acc))) (when (not (empty? all-params)) (div :class "space-y-2 p-3 bg-violet-50 rounded border border-violet-200" (div :class "text-xs font-semibold text-violet-700 mb-1" "Parameters") (map (fn (pname) (div :class "flex items-center gap-2" (label :class "text-xs font-mono text-stone-600 w-24 text-right" pname) (input :type "text" :class "flex-1 px-2 py-1 text-xs font-mono border border-stone-300 rounded bg-white" :placeholder (str pname " value") :on-input (fn (e) (reset! bindings (assoc (deref bindings) pname (element-value (host-get e "target")))))))) all-params))))) (if (or (nil? (deref parsed)) (empty? (deref parsed))) (div :class "text-red-500 text-sm font-mono p-3 bg-red-50 rounded" "Parse error — check your s-expression syntax") (div :class "bg-stone-50 rounded border border-stone-200 p-3 font-mono text-xs leading-relaxed overflow-x-auto" (cond (= (deref view-mode) "tree") (let ((entries (list)) (tree (deref parsed))) (for-each (fn (i) (build-entries (nth tree i) (list i) 0 entries)) (range 0 (len tree))) (div :class "space-y-0" (map (fn (entry) (let ((path (get entry "path")) (sel (deref selected-path)) (is-selected (and (not (nil? sel)) (= (fmt-path sel) (fmt-path path))))) (div :class (str "py-0.5 cursor-pointer hover:bg-violet-50 rounded px-1 " (if is-selected "bg-violet-100" "")) :style (str "padding-left:" (* (get entry "depth") 16) "px") :on-click (fn (e) (reset! selected-path path)) (span :class "text-stone-400 mr-2 select-none" (fmt-path path)) (span :class (if (get entry "is-list") "text-sky-700" "text-stone-600") (get entry "text"))))) entries))) (= (deref view-mode) "context") (if (nil? (deref selected-path)) (p :class "text-stone-400 italic" "Click a node in tree view to see its context") (let ((ctx (build-context (deref parsed) (deref selected-path)))) (div :class "space-y-1" (map (fn (item) (div :style (str "padding-left:" (* (get item "depth") 16) "px") :class (if (get item "is-target") "text-violet-700 font-semibold" "text-stone-600") (span :class "text-stone-400 mr-2" (if (get item "is-target") "→ " " ") (fmt-path (get item "path"))) (span (get item "text")))) ctx)))) (= (deref view-mode) "validate") (let ((result (do-validate (deref parsed)))) (div :class (if (= result "OK") "text-emerald-600" "text-amber-600 whitespace-pre-wrap") result)) (= (deref view-mode) "render") (let ((tree (deref parsed))) (div :class "prose prose-sm max-w-none font-sans" (map (fn (expr) (if (not (and (list? expr) (not (empty? expr)) (= (type-of (first expr)) "symbol"))) (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)) (let ((head-name (symbol-name (first expr)))) (cond (is-html-tag? head-name) expr (= head-name "defcomp") (let ((body (last expr)) (params (extract-params expr)) (b (deref bindings))) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " (node-disp (nth expr 1)))) (letrec ((subst (fn (node) (cond (and (= (type-of node) "symbol") (some (fn (p) (= p (symbol-name node))) params)) (or (get b (symbol-name node)) "") (list? node) (map subst node) :else node)))) (subst body)))) (= head-name "defisland") (let ((full-name (if (= (type-of (nth expr 1)) "symbol") (symbol-name (nth expr 1)) (str (nth expr 1))))) (sx-load-components (sx-serialize expr)) (div (div :class "text-xs text-stone-400 mb-2 font-mono" (str head-name " " full-name " — live preview")) (div :class "border border-violet-200 rounded p-4" (list (make-symbol full-name))))) (= head-name "let") (last expr) :else (div :class "font-mono text-xs text-stone-400 p-2 bg-stone-100 rounded" (sx-serialize expr)))))) tree))) (= (deref view-mode) "hypersx") (pre :class "font-mono text-xs text-stone-700 whitespace-pre-wrap overflow-x-auto" (sx->hypersx (deref parsed))) :else (p "Select a view mode"))))))))