├── example ├── todomvc │ ├── .gitignore │ ├── src │ │ └── todomvc │ │ │ ├── fx.cljs │ │ │ ├── constant.cljs │ │ │ ├── cofx.cljs │ │ │ ├── router.cljs │ │ │ ├── interceptor.cljs │ │ │ ├── core.cljs │ │ │ ├── sub.cljs │ │ │ ├── event.cljs │ │ │ └── view.cljs │ ├── package.json │ ├── shadow-cljs.edn │ ├── public │ │ ├── index.html │ │ └── css │ │ │ └── index.css │ ├── README.md │ └── deps.edn ├── test-app │ ├── .gitignore │ ├── package.json │ ├── shadow-cljs.edn │ ├── public │ │ └── index.html │ ├── README.md │ ├── deps.edn │ ├── src │ │ └── example │ │ │ ├── math_ml.cljs │ │ │ ├── core.cljs │ │ │ ├── reactive_data.cljs │ │ │ ├── ref.cljs │ │ │ ├── ui_component.cljs │ │ │ ├── vcup.cljs │ │ │ ├── context.cljs │ │ │ ├── svg.cljs │ │ │ └── reactive_fragment.cljs │ └── resource │ │ └── svg │ │ └── fox-origami.svg ├── react-interop │ ├── .gitignore │ ├── shadow-cljs.edn │ ├── public │ │ └── index.html │ ├── package.json │ ├── src │ │ └── react_interop │ │ │ ├── core.cljs │ │ │ └── interop.cljs │ ├── deps.edn │ └── README.md └── si-frame-simple │ ├── .gitignore │ ├── package.json │ ├── shadow-cljs.edn │ ├── public │ └── index.html │ ├── deps.edn │ ├── README.md │ └── src │ └── simple │ └── core.cljs ├── bin └── kaocha ├── package.json ├── playwright ├── .gitignore ├── tests │ ├── context.spec.ts │ ├── mathML.spec.ts │ ├── ref.spec.ts │ ├── svg.spec.ts │ ├── reactiveData.spec.ts │ ├── reactiveFragment.spec.ts │ └── vcup.spec.ts ├── package.json ├── playwright.config.js └── package-lock.json ├── tests.edn ├── .gitignore ├── doc ├── cljdoc.edn ├── ai-context-construction-prompt.txt ├── vcup-properties.md └── ai-context.md ├── test └── vrac │ ├── test │ └── util.cljc │ └── dsl │ ├── macro_test.cljc │ └── parser_test.cljc ├── .github └── workflows │ ├── cljdoc.yml │ ├── validate.yml │ └── release.yml ├── src └── vrac │ ├── dsl.cljc │ ├── dsl │ ├── macro.cljc │ ├── parser.cljc │ └── ast.cljc │ └── web.cljc ├── CHANGELOG.md ├── README.md ├── pom.xml ├── deps.edn └── LICENSE /example/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | public/js 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | clojure -M:test -m kaocha.runner "$@" 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ws": "^8.18.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /playwright/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Playwright 3 | node_modules/ 4 | /test-results/ 5 | /playwright-report/ 6 | /blob-report/ 7 | /playwright/.cache/ 8 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit 3 | :type :kaocha.type/clojure.test} 4 | {:id :unit-cljs 5 | :type :kaocha.type/cljs}]} 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | yarn.lock 4 | out/ 5 | 6 | .idea/ 7 | .cpcache/ 8 | .nrepl-port 9 | .shadow-cljs/ 10 | .cljs_node_repl/ 11 | .clj-kondo/ 12 | 13 | *.iml 14 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/fx.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.fx 2 | (:require [re-frame.core :as rf])) 3 | 4 | (rf/reg-fx 5 | :save-in-local-store 6 | (fn [{:keys [key value]}] 7 | (js/localStorage.setItem key value))) 8 | -------------------------------------------------------------------------------- /playwright/tests/context.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("", async ({ page }) => {}); 9 | -------------------------------------------------------------------------------- /playwright/tests/mathML.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("", async ({ page }) => {}); 9 | -------------------------------------------------------------------------------- /playwright/tests/ref.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("", async ({ page }) => {}); 9 | -------------------------------------------------------------------------------- /playwright/tests/svg.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("", async ({ page }) => {}); 9 | -------------------------------------------------------------------------------- /playwright/tests/reactiveData.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("", async ({ page }) => {}); 9 | -------------------------------------------------------------------------------- /playwright/tests/reactiveFragment.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("", async ({ page }) => {}); 9 | -------------------------------------------------------------------------------- /example/test-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package.json.lock 3 | package-lock.json 4 | yarn.lock 5 | out/ 6 | public/js/ 7 | 8 | .idea/ 9 | .cpcache/ 10 | .nrepl-port 11 | .shadow-cljs/ 12 | .cljs_node_repl/ 13 | 14 | *.iml 15 | -------------------------------------------------------------------------------- /example/react-interop/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package.json.lock 3 | package-lock.json 4 | yarn.lock 5 | out/ 6 | public/js/ 7 | 8 | .idea/ 9 | .cpcache/ 10 | .nrepl-port 11 | .shadow-cljs/ 12 | .cljs_node_repl/ 13 | 14 | *.iml 15 | -------------------------------------------------------------------------------- /example/si-frame-simple/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package.json.lock 3 | package-lock.json 4 | yarn.lock 5 | out/ 6 | public/js/ 7 | 8 | .idea/ 9 | .cpcache/ 10 | .nrepl-port 11 | .shadow-cljs/ 12 | .cljs_node_repl/ 13 | 14 | *.iml 15 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree [["Readme" {:file "README.md"}] 2 | ["Changes" {:file "CHANGELOG.md"}] 3 | ["Vcup properties" {:file "doc/vcup-properties.md"}] 4 | ["AI context" {:file "doc/ai-context.md"}]]} 5 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/constant.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.constant) 2 | 3 | (def enter-keycode 13) 4 | (def escape-keycode 27) 5 | 6 | (def initial-db 7 | {:next-todo-item-id 0 8 | :comp.input/title "" 9 | :todo-items {}}) 10 | 11 | (def todo-item-local-storage-key "todo-items") 12 | -------------------------------------------------------------------------------- /playwright/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright", 3 | "private": true, 4 | "scripts": { 5 | "test": "npx playwright test", 6 | "test-ui": "npx playwright test --ui" 7 | }, 8 | "devDependencies": { 9 | "@playwright/test": "1.55.0", 10 | "@types/node": "24.5.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "private": true, 4 | "scripts": { 5 | "start": "npx shadow-cljs watch :app", 6 | "release": "npx shadow-cljs release :app", 7 | "server": "npx shadow-cljs server" 8 | }, 9 | "devDependencies": { 10 | "shadow-cljs": "3.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/todomvc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "private": true, 4 | "scripts": { 5 | "start": "npx shadow-cljs watch :app", 6 | "release": "npx shadow-cljs release :app", 7 | "server": "npx shadow-cljs server" 8 | }, 9 | "devDependencies": { 10 | "shadow-cljs": "3.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/vrac/test/util.cljc: -------------------------------------------------------------------------------- 1 | (ns vrac.test.util) 2 | 3 | (defn make-gensym 4 | "Provides a function which returns symbols consistently & deterministically." 5 | [] 6 | (let [n (atom 0)] 7 | (fn gensym 8 | ([] (gensym "G__")) 9 | ([prefix-string] 10 | (symbol (str prefix-string (swap! n inc))))))) 11 | -------------------------------------------------------------------------------- /example/si-frame-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "si-frame-simple", 3 | "private": true, 4 | "scripts": { 5 | "start": "npx shadow-cljs watch :app", 6 | "release": "npx shadow-cljs release :app", 7 | "server": "npx shadow-cljs server" 8 | }, 9 | "devDependencies": { 10 | "shadow-cljs": "3.2.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/todomvc/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | 3 | :dev-http {3000 "public"} 4 | 5 | :builds {:app {:target :browser 6 | :output-dir "public/js" 7 | :asset-path "/js" 8 | :modules {:main {:init-fn todomvc.core/start}} 9 | :build-hooks [(shadow.cljs.build-report/hook)]}}} 10 | -------------------------------------------------------------------------------- /example/si-frame-simple/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | 3 | :dev-http {3000 "public"} 4 | 5 | :builds {:app {:target :browser 6 | :output-dir "public/js" 7 | :asset-path "/js" 8 | :modules {:main {:init-fn simple.core/run}} 9 | :build-hooks [(shadow.cljs.build-report/hook)]}}} 10 | -------------------------------------------------------------------------------- /example/test-app/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | 3 | :dev-http {3000 "public"} 4 | 5 | :builds {:app {:target :browser 6 | :output-dir "public/js" 7 | :asset-path "/js" 8 | :modules {:main {:init-fn example.core/start-app}} 9 | :build-hooks [(shadow.cljs.build-report/hook)]}}} 10 | -------------------------------------------------------------------------------- /example/react-interop/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps true 2 | 3 | :dev-http {3000 "public"} 4 | 5 | :builds {:app {:target :browser 6 | :output-dir "public/js" 7 | :asset-path "/js" 8 | :modules {:main {:init-fn react-interop.core/start-app}} 9 | :build-hooks [(shadow.cljs.build-report/hook)]}}} 10 | -------------------------------------------------------------------------------- /example/react-interop/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vrac + React 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/test-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vrac test app 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/si-frame-simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vrac + Si-frame 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/react-interop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-interop", 3 | "private": true, 4 | "scripts": { 5 | "start": "npx shadow-cljs watch :app", 6 | "release": "npx shadow-cljs release :app", 7 | "server": "npx shadow-cljs server" 8 | }, 9 | "devDependencies": { 10 | "shadow-cljs": "3.2.1" 11 | }, 12 | "dependencies": { 13 | "react": "^19.2.0", 14 | "react-dom": "^19.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/cofx.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.cofx 2 | (:require [cljs.reader :as reader] 3 | [re-frame.core :as rf] 4 | [todomvc.constant :as const])) 5 | 6 | (rf/reg-cofx 7 | :todo-items-from-local-store 8 | (fn [cofx _] 9 | (assoc cofx 10 | :todo-items-from-local-store 11 | (some-> (js/localStorage.getItem const/todo-item-local-storage-key) 12 | reader/read-string)))) 13 | -------------------------------------------------------------------------------- /example/test-app/README.md: -------------------------------------------------------------------------------- 1 | # Vrac test app 2 | 3 | ## How to run this project 4 | 5 | ```shell 6 | npm install 7 | npm start 8 | ``` 9 | 10 | This will run Shadow-CLJS. Once the project finished to compile, 11 | follow the [displayed link](http://localhost:3000) to see the app. 12 | 13 | ## Production build 14 | 15 | Compiling for production: 16 | 17 | ```shell 18 | npm run release 19 | ``` 20 | 21 | The report describing the size of the different parts is `public/js/report.html`. 22 | -------------------------------------------------------------------------------- /example/todomvc/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TodoMVC Hacking 6 | 7 | 8 | 9 | 10 | 11 |
Loading...
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/react-interop/src/react_interop/core.cljs: -------------------------------------------------------------------------------- 1 | (ns react-interop.core 2 | (:require [vrac.web :as vw :refer [$]] 3 | [react-interop.interop :refer [interop-demo]])) 4 | 5 | ;; Shadow-CLJS hooks: start & reload the app 6 | 7 | (defn ^:dev/after-load setup! [] 8 | (vw/render (js/document.getElementById "app") 9 | ($ interop-demo))) 10 | 11 | (defn ^:dev/before-load shutdown! [] 12 | (vw/dispose-render-effects)) 13 | 14 | (defn start-app [] 15 | (setup!)) 16 | -------------------------------------------------------------------------------- /example/test-app/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src" "resource"] 2 | 3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"} 4 | org.clojure/clojurescript {:mvn/version "1.12.42"} 5 | thheller/shadow-cljs {:mvn/version "3.2.1"} 6 | taipei.404.vrac/vrac {:local/root "../.."}} 7 | 8 | :aliases {;; clojure -M:outdated --upgrade 9 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}} 10 | :main-opts ["-m" "antq.core"]}}} 11 | -------------------------------------------------------------------------------- /example/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC 2 | 3 | A famous sample app implemented using Vrac. 4 | 5 | ## How to run this project 6 | 7 | ```shell 8 | npm install 9 | npm start 10 | ``` 11 | 12 | This will run Shadow-CLJS. Once the project finished to compile, 13 | follow the [displayed link](http://localhost:3000) to see the app. 14 | 15 | ## Production build 16 | 17 | Compiling for production: 18 | 19 | ```shell 20 | npm run release 21 | ``` 22 | 23 | The report describing the size of the different parts is `public/js/report.html`. 24 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/router.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.router 2 | (:require [re-frame.core :as rf] 3 | [reitit.frontend :as rtf] 4 | [reitit.frontend.easy :as rtfe])) 5 | 6 | (def routes 7 | ["/" 8 | ["" {:name :page/all-todo-items}] 9 | ["active" {:name :page/active-todo-items}] 10 | ["completed" {:name :page/completed-todo-items}]]) 11 | 12 | (defn start-router! [] 13 | (rtfe/start! 14 | (rtf/router routes) 15 | (fn [m] 16 | (rf/dispatch [:router/set-route-match m])) 17 | {:use-fragment true})) 18 | -------------------------------------------------------------------------------- /example/si-frame-simple/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"} 4 | org.clojure/clojurescript {:mvn/version "1.12.42"} 5 | thheller/shadow-cljs {:mvn/version "3.2.1"} 6 | taipei.404.vrac/vrac {:local/root "../.."} 7 | fi.metosin/si-frame {:mvn/version "1.4.3.0-no-siagent"}} 8 | 9 | :aliases {;; clojure -M:outdated --upgrade 10 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}} 11 | :main-opts ["-m" "antq.core"]}}} 12 | -------------------------------------------------------------------------------- /example/si-frame-simple/README.md: -------------------------------------------------------------------------------- 1 | # Vrac + Si-frame, "simple" demo ported from Re-frame 2 | 3 | This project shows how to use Si-frame together with Vrac. 4 | 5 | ## How to run this project 6 | 7 | ```shell 8 | npm install 9 | npm start 10 | ``` 11 | 12 | This will run Shadow-CLJS. Once the project finished to compile, 13 | follow the [displayed link](http://localhost:3000) to see the app. 14 | 15 | ## Production build 16 | 17 | Compiling for production: 18 | 19 | ```shell 20 | npm run release 21 | ``` 22 | 23 | The report describing the size of the different parts is `public/js/report.html`. 24 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/interceptor.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.interceptor 2 | (:require [re-frame.core :as rf] 3 | [todomvc.constant :as const])) 4 | 5 | (def save-todo-items-in-local-storage 6 | (rf/->interceptor 7 | :id :save-todo-items-in-local-storage 8 | :after (fn [context] 9 | (let [db (or (get-in context [:effects :db]) 10 | (get-in context [:coeffects :db]))] 11 | (update-in context [:effects :fx] conj 12 | [:save-in-local-store {:key const/todo-item-local-storage-key 13 | :value (str (:todo-items db))}]))))) 14 | -------------------------------------------------------------------------------- /example/todomvc/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"} 4 | org.clojure/clojurescript {:mvn/version "1.12.42"} 5 | thheller/shadow-cljs {:mvn/version "3.2.1"} 6 | metosin/reitit-frontend {:mvn/version "0.9.1"} 7 | taipei.404.vrac/vrac {:local/root "../.."} 8 | fi.metosin/si-frame {:mvn/version "1.4.3.0-no-siagent"}} 9 | 10 | 11 | :aliases {; clojure -M:outdated --upgrade 12 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}} 13 | :main-opts ["-m" "antq.core"]}}} 14 | -------------------------------------------------------------------------------- /.github/workflows/cljdoc.yml: -------------------------------------------------------------------------------- 1 | name: CljDoc 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | check-cljdoc: 12 | 13 | name: Check source code 14 | 15 | timeout-minutes: 60 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v5 21 | 22 | - name: Setup Clojure 23 | uses: DeLaGuardo/setup-clojure@master 24 | with: 25 | cli: latest 26 | 27 | - name: Build the jar and update pom.xml's version 28 | run: clojure -X:jar && mkdir target && mv *.jar target/ 29 | 30 | - name: CljDoc Check 31 | uses: cljdoc/cljdoc-check-action@v0.0.3 32 | -------------------------------------------------------------------------------- /src/vrac/dsl.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc vrac.dsl) 2 | 3 | ;; Keywords used in Vrac's DSL which are already defined in clojure.core/cljs.core: 4 | ;; defn, let, when, for 5 | 6 | (def reserved-symbols '[$ 7 | do if quote ;; <- keywords of the Clojure languages, not defined in clojure.core 8 | global context with-context 9 | once signal state memo 10 | effect effect-on on-clean-up]) 11 | 12 | ;; Those declarations help making the IDEs happy about the user's DSL expressions. 13 | (declare $ 14 | global context with-context 15 | once signal state memo 16 | effect effect-on on-clean-up) 17 | -------------------------------------------------------------------------------- /doc/ai-context-construction-prompt.txt: -------------------------------------------------------------------------------- 1 | Ultrathink: Read @src/vrac/web.cljc, @doc/vcup-properties.md, the files in directory @example/test-app/src/example/ and the @example/si-frame-simple/src/simple/core.cljs. 2 | From the patterns of how Vrac is used in those correct programs, I want you to write a summary of those patterns in 3 | @doc/ai-context.md which will be used by Claude Code for generating Clojurescript programs using Vrac, Si-Frame and Signaali. 4 | The information should not be application-specific. Only include the information which is really needed for generating a correct program, 5 | and avoid repeating yourself. 6 | Latest version of Vrac to use in file deps.edn is `taipei.404.vrac/vrac {:mvn/version "0.1.2"}` 7 | -------------------------------------------------------------------------------- /example/react-interop/deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps {org.clojure/clojure {:mvn/version "1.12.3"} 4 | org.clojure/clojurescript {:mvn/version "1.12.42"} 5 | thheller/shadow-cljs {:mvn/version "3.2.1"} 6 | taipei.404.vrac/vrac {:local/root "../.."} 7 | fi.metosin/si-frame {:mvn/version "1.4.3.0"} 8 | fi.metosin/siagent {:mvn/version "0.1.0"} 9 | com.pitch/uix.core {:mvn/version "1.4.5"} 10 | com.pitch/uix.dom {:mvn/version "1.4.5"}} 11 | 12 | :aliases {;; clojure -M:outdated --upgrade 13 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}} 14 | :main-opts ["-m" "antq.core"]}}} 15 | -------------------------------------------------------------------------------- /example/react-interop/README.md: -------------------------------------------------------------------------------- 1 | # Vrac + React 19, interop demo 2 | 3 | This project shows how to use Vrac and React 19 together. 4 | 1. How to embed React roots and components inside a Vrac app. 5 | 2. How to use the reactive data from the Vrac app inside the embedded React components. 6 | The source code shows how to do it: 7 | 1. using Siagent: just deref the reactive nodes, "et voilà". 8 | 2. using UIx: use the `use-reactive` React hook. 9 | 10 | ## How to run this project 11 | 12 | ```shell 13 | npm install 14 | npm start 15 | ``` 16 | 17 | This will run Shadow-CLJS. Once the project finished to compile, 18 | follow the [displayed link](http://localhost:3000) to see the app. 19 | 20 | ## Production build 21 | 22 | Compiling for production: 23 | 24 | ```shell 25 | npm run release 26 | ``` 27 | 28 | The report describing the size of the different parts is `public/js/report.html`. 29 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/core.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.core 2 | (:require [re-frame.core :as rf] 3 | [todomvc.cofx] 4 | [todomvc.fx] 5 | [todomvc.interceptor] 6 | [todomvc.event] 7 | [todomvc.sub] 8 | [todomvc.router :as router] 9 | [todomvc.view :as view] 10 | [vrac.web :as vw :refer [$]])) 11 | 12 | (defn mount-ui [] 13 | (vw/render (js/document.getElementById "app") 14 | ($ view/main-page))) 15 | 16 | (defn ^:dev/after-load clear-cache-and-render! [] 17 | (rf/clear-subscription-cache!) 18 | (router/start-router!) 19 | (mount-ui)) 20 | 21 | (defn ^:dev/before-load shutdown! [] 22 | (vw/dispose-render-effects)) 23 | 24 | (defn start [] 25 | (rf/dispatch-sync [:initialize-db]) 26 | (rf/dispatch-sync [:local-storage/load-todo-items]) 27 | (router/start-router!) 28 | (mount-ui)) 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | 11 | - A documentation for LLMs. 12 | 13 | ## [0.1.2] - 2025-10-04 14 | 15 | ### Fixed 16 | 17 | - Cljdoc documentation is fixed. 18 | - Fixed a bug which prevented reactive fragments (e.g. if, for) to be used directly inside each other. 19 | 20 | ### Changed 21 | 22 | - Bumped the dependencies on the example projects, notably Shadow-CLJS. 23 | 24 | ## [0.1.1] - 2025-04-24 25 | 26 | ### Changed 27 | 28 | - Uses a new format for naming the attributes, properties and event handlers (#5). 29 | - `vrac.web/attributes-effect` renamed to `vrac.web/props-effect`. 30 | 31 | ### Added 32 | 33 | - Documentation about the attributes and properties in Vcup. 34 | 35 | ## [0.1.0] - 2025-03-17 36 | 37 | ### Added 38 | 39 | - the `vrac.web` namespace 40 | - some examples in the `/examples` directory 41 | -------------------------------------------------------------------------------- /example/test-app/src/example/math_ml.cljs: -------------------------------------------------------------------------------- 1 | (ns example.math-ml 2 | (:require [vrac.web :as vw :refer [$]])) 3 | 4 | (defn- sample1-article [] 5 | ($ :article 6 | ($ :h2 "MathML sample 1") 7 | ($ :math {:display "block"} 8 | ($ :mrow 9 | ($ :mrow 10 | ($ :mo "|") 11 | ($ :mi "x") 12 | ($ :mo "|")) 13 | ($ :mo "=") 14 | ($ :mi "x")) 15 | ($ :mtext "\u00a0iff\u00a0") 16 | ($ :mrow 17 | ($ :mi "x") 18 | ($ :mo "≥") 19 | ($ :mn "0"))))) 20 | 21 | (defn- sample2-article [] 22 | ($ :article 23 | ($ :h2 "MathML sample 2") 24 | ($ :math {:display "block"} 25 | ($ :mfrac 26 | ($ :mrow 27 | ($ :mi "a") 28 | ($ :mo "+") 29 | ($ :mn "2")) 30 | ($ :mrow 31 | ($ :mn "3") 32 | ($ :mo "−") 33 | ($ :mi "b")))))) 34 | 35 | (defn math-ml-root [] 36 | ($ :div 37 | ($ sample1-article) 38 | ($ sample2-article))) 39 | -------------------------------------------------------------------------------- /playwright/tests/vcup.spec.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from "@playwright/test"; 3 | 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto("/"); 6 | }); 7 | 8 | test("vcup-root", async ({ page }) => { 9 | const vcupRoot = page.getByTestId("vcup-root"); 10 | await expect(vcupRoot).toMatchAriaSnapshot(` 11 | - article: 12 | - heading "Text nodes" [level=2] 13 | - heading "Text" [level=3] 14 | - text: Hello, world! 15 | - heading "Unicode characters" [level=3] 16 | - text: There is two non-breakable spaces between A and B. 17 | - heading "Numbers" [level=3] 18 | - text: 1 2 3 19 | - heading "Booleans" [level=3] 20 | - text: false true 21 | - heading "Keywords" [level=3] 22 | - text: :foo :bar :foo/bar 23 | - heading "Vectors" [level=3] 24 | - text: "[:div {:style {:color \\\"lime\\\"}} 1 2]" 25 | - 'heading "\`nil\` values" [level=3]' 26 | - article: 27 | - heading "Image" [level=2] 28 | - img 29 | - article: 30 | - heading "Vcup elements with funny names" [level=2] 31 | - text: :span#id1.class1.class2 :#id2.class3 :.class4 32 | - article: 33 | - heading "Fragments" [level=2] 34 | - text: 1a1b2a2b 35 | - article: 36 | - heading "Component with arguments" [level=2] 37 | - code: (is 123 (+ a b c)) 38 | `); 39 | }); 40 | -------------------------------------------------------------------------------- /example/test-app/src/example/core.cljs: -------------------------------------------------------------------------------- 1 | (ns example.core 2 | (:require [example.context :refer [context-root]] 3 | [example.math-ml :refer [math-ml-root]] 4 | [example.reactive-data :refer [reactive-data-root]] 5 | [example.reactive-fragment :refer [reactive-fragment-root]] 6 | [example.svg :refer [svg-root]] 7 | [example.ref :refer [ref-root]] 8 | [example.vcup :refer [vcup-root]] 9 | [example.ui-component :refer [ui-component-root]] 10 | [signaali.reactive :as sr] 11 | [vrac.web :as vw :refer [$]])) 12 | 13 | (defn- debug-prn [reactive-node event-type] 14 | (when-some [name (-> reactive-node meta :name)] 15 | (prn name event-type))) 16 | 17 | ;; Print the debug info in the Browser's console. 18 | ;;(set! sr/*notify-lifecycle-event* debug-prn) 19 | 20 | (defn root-component [] 21 | ($ :main 22 | ($ vcup-root) 23 | ($ reactive-data-root) 24 | ($ reactive-fragment-root) 25 | ($ ref-root) 26 | ($ context-root) 27 | ($ svg-root) 28 | ($ math-ml-root) 29 | ($ ui-component-root) 30 | ,)) 31 | 32 | ;; Shadow-CLJS hooks: start & reload the app 33 | 34 | (defn ^:dev/after-load setup! [] 35 | (vw/render (js/document.getElementById "app") 36 | ($ root-component))) 37 | 38 | (defn ^:dev/before-load shutdown! [] 39 | (vw/dispose-render-effects)) 40 | 41 | (defn start-app [] 42 | (setup!)) 43 | -------------------------------------------------------------------------------- /example/test-app/src/example/reactive_data.cljs: -------------------------------------------------------------------------------- 1 | (ns example.reactive-data 2 | (:require [clojure.string :as str] 3 | [signaali.reactive :as sr] 4 | [vrac.web :as vw :refer [$]])) 5 | 6 | ;; Updating the state makes the DOM update 7 | (defn- counter-component [counter-state] 8 | ($ :div 9 | "Counter value: " counter-state 10 | ($ :div 11 | ($ :button {:on/click #(swap! counter-state inc)} "Increment") 12 | ($ :button {:on/click #(reset! counter-state 0)} "Reset")))) 13 | 14 | (defn- counters-article [] 15 | ($ :article 16 | ($ :h2 "Reactive counters") 17 | (for [i (range 3)] 18 | (let [counter-state (sr/create-state (* i 100))] 19 | ($ counter-component counter-state))))) 20 | 21 | (defn- controlled-input-article [] 22 | ($ :article 23 | ($ :h2 "Controlled input") 24 | ($ :div "This input prevents its content from containing \"foobar\".") 25 | (let [text-signal (sr/create-signal "foo")] 26 | ($ :input 27 | (vw/props-effect (fn [] {:value @text-signal})) 28 | {:on/input (fn [^js event] 29 | (let [text (-> event .-target .-value)] 30 | (swap! text-signal (fn [previous-text] 31 | (if (str/includes? text "foobar") 32 | previous-text 33 | text)))))})))) 34 | 35 | (defn reactive-data-root [] 36 | ($ :div 37 | ($ counters-article) 38 | ($ controlled-input-article))) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vrac 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/taipei.404.vrac/vrac.svg)](https://clojars.org/taipei.404.vrac/vrac) 4 | [![Slack](https://img.shields.io/badge/slack-vrac-orange.svg?logo=slack)](https://clojurians.slack.com/app_redirect?channel=vrac) 5 | [![cljdoc badge](https://cljdoc.org/badge/taipei.404.vrac/vrac)](https://cljdoc.org/d/taipei.404.vrac/vrac) 6 | 7 | > The only way to stop using JS is to stop using JS. 8 | 9 | Vrac is a frontend rendering library in Clojure, for Clojurists. 10 | 11 | Features implemented so far: 12 | - 100% Written in Clojure(script) 13 | - Uses signals to do efficient fine-grained updates on the DOM 14 | - Has a good interop with React and Clojurescript React wrappers 15 | - 100% compatible with Re-frame via the fork [Si-frame](https://github.com/metosin/si-frame) 16 | - Has [concise and simple examples](example) showing how to use it 17 | - Carefully crafted [AI context for LLMs and AI agents](doc/ai-context.md). 18 | 19 | Features on the roadmap: 20 | - Declarative developer experience via a DSL 21 | - Pluggable data management 22 | - Transparent reactivity 23 | - In browser dev tools 24 | - Hot reloading 25 | - Client-server data sync 26 | 27 | ## Project status 28 | 29 | This project is currently in its early development. 30 | 31 | It doesn't have known bugs, is implemented enough to be useful to small projects 32 | but might not have enough built-in features to be used in large projects by large teams. 33 | 34 | ## License 35 | 36 | This project is distributed under the [Eclipse Public License v2.0](LICENSE). 37 | 38 | Copyright (c) Vincent Cantin and contributors. 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | taipei.404.vrac 6 | vrac 7 | 0.1.2 8 | vrac 9 | A frontend rendering library in Clojure, for Clojurists 10 | 2025 11 | 12 | 13 | EPL-2.0 14 | https://opensource.org/license/EPL-2.0 15 | 16 | 17 | 18 | https://github.com/green-coder/vrac 19 | scm:git:git://github.com/green-coder/vrac.git 20 | scm:git:ssh://git@github.com/green-coder/vrac.git 21 | v0.1.2 22 | 23 | 24 | 25 | org.clojure 26 | clojure 27 | 1.12.3 28 | 29 | 30 | fi.metosin 31 | signaali 32 | 0.1.0 33 | 34 | 35 | 36 | src 37 | 38 | 39 | 40 | clojars 41 | https://repo.clojars.org/ 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | test: 12 | 13 | name: Unit and e2e tests 14 | 15 | timeout-minutes: 60 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v5 21 | 22 | - uses: actions/setup-node@v5 23 | with: 24 | node-version: lts/hydrogen 25 | 26 | - name: Setup java 27 | uses: actions/setup-java@v3 28 | with: 29 | distribution: 'temurin' 30 | java-version: '21' 31 | 32 | - name: Setup Clojure 33 | uses: DeLaGuardo/setup-clojure@master 34 | with: 35 | cli: latest 36 | 37 | - name: Cache deps dependencies 38 | uses: actions/cache@v4 39 | with: 40 | path: ~/.m2/repository 41 | key: ${{ runner.os }}-clojure-${{ hashFiles('**/deps.edn') }} 42 | restore-keys: | 43 | ${{ runner.os }}-clojure 44 | 45 | - name: Install dependencies for unit tests 46 | run: npm ci 47 | 48 | - name: Unit tests 49 | run: ./bin/kaocha 50 | 51 | - name: Install dependencies 52 | working-directory: ./playwright 53 | run: npm ci 54 | 55 | - name: Install Playwright Browsers 56 | working-directory: ./playwright 57 | run: npx playwright install --with-deps 58 | 59 | - name: Run Playwright tests 60 | working-directory: ./playwright 61 | run: npm test 62 | 63 | - uses: actions/upload-artifact@v4 64 | if: ${{ !cancelled() }} 65 | with: 66 | name: playwright-report 67 | path: playwright/playwright-report/ 68 | retention-days: 30 69 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published # reacts to releases and pre-releases, but not their drafts 7 | 8 | jobs: 9 | 10 | test-and-release: 11 | 12 | name: Validate, Jar and Deploy 13 | 14 | timeout-minutes: 60 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v5 20 | 21 | - uses: actions/setup-node@v5 22 | with: 23 | node-version: lts/hydrogen 24 | 25 | - name: Setup java 26 | uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: '21' 30 | 31 | - name: Setup Clojure 32 | uses: DeLaGuardo/setup-clojure@master 33 | with: 34 | cli: latest 35 | 36 | - name: Install dependencies for unit tests 37 | run: npm ci 38 | 39 | - name: Unit tests 40 | run: ./bin/kaocha 41 | 42 | - name: Install dependencies 43 | working-directory: ./playwright 44 | run: npm ci 45 | 46 | - name: Install Playwright Browsers 47 | working-directory: ./playwright 48 | run: npx playwright install --with-deps 49 | 50 | - name: Run Playwright tests 51 | working-directory: ./playwright 52 | run: npm test 53 | 54 | - uses: actions/upload-artifact@v4 55 | if: ${{ !cancelled() }} 56 | with: 57 | name: playwright-report 58 | path: playwright/playwright-report/ 59 | retention-days: 30 60 | 61 | - name: Build the jar and update pom.xml's version 62 | run: clojure -X:jar 63 | 64 | - name: Deploy the jar and pom files to Clojars 65 | run: clojure -X:deploy 66 | env: 67 | CLOJARS_USERNAME: green-coder 68 | CLOJARS_PASSWORD: "${{ secrets.CLOJARS_DEPLOY_TOKEN }}" 69 | -------------------------------------------------------------------------------- /example/test-app/src/example/ref.cljs: -------------------------------------------------------------------------------- 1 | (ns example.ref 2 | (:require [signaali.reactive :as sr] 3 | [vrac.web :as vw :refer [$]])) 4 | 5 | (defn- ref-signal-article [element-ref] 6 | ($ :article 7 | ($ :h2 "Capture an element in a reactive signal via the special prop `ref`") 8 | (let [is-existing (sr/create-signal false)] 9 | ($ :div 10 | ($ :button {:on/click (fn [] (swap! is-existing not))} 11 | (sr/create-derived (fn [] (if @is-existing "Destroy element" "Create element")))) 12 | " " 13 | (vw/when-fragment is-existing 14 | ($ :span {:ref element-ref} "Referred element")))))) 15 | 16 | (defn- using-ref-somewhere-else-article1 [element-ref] 17 | ($ :article 18 | ($ :h2 "Use the ref's innerText via a derived data.") 19 | ($ :div (sr/create-derived 20 | (fn [] 21 | (if-some [^js element @element-ref] 22 | (.-innerText element) 23 | "")))))) 24 | 25 | (defn- using-ref-somewhere-else-article2 [element-ref] 26 | ($ :article 27 | ($ :h2 "Use the ref to change a color reactively.") 28 | "The square is:" 29 | ($ :ul 30 | ($ :li ($ :strong "red") " when the ref is nil, and") 31 | ($ :li ($ :strong "green") " when the ref is set to an element.")) 32 | ($ :div 33 | {:style {:width "50px" 34 | :height "50px"}} 35 | (vw/props-effect 36 | (fn [] 37 | {:style {:background-color (if (nil? @element-ref) 38 | "red" 39 | "green")}}))))) 40 | 41 | (defn ref-root [] 42 | (let [element-ref (sr/create-signal nil)] 43 | ($ :div 44 | ($ ref-signal-article element-ref) 45 | ($ using-ref-somewhere-else-article1 element-ref) 46 | ($ using-ref-somewhere-else-article2 element-ref)))) 47 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :deps {fi.metosin/signaali {:mvn/version "0.1.0"} 4 | ;; This is not used by Vrac yet 5 | taipei.404/mate {:mvn/version "0.1.0"}} 6 | 7 | :aliases {:dev {:extra-deps {org.clojure/clojure {:mvn/version "1.12.3"} 8 | org.clojure/clojurescript {:mvn/version "1.12.42"} 9 | djblue/portal {:mvn/version "0.61.0"} 10 | lambdaisland/deep-diff2 {:mvn/version "2.12.219"}}} 11 | 12 | :test {:extra-paths ["test" "test-resource"] 13 | :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"} 14 | lambdaisland/kaocha-cljs {:mvn/version "1.8.163"} 15 | lambdaisland/kaocha-junit-xml {:mvn/version "1.17.101"} 16 | org.clojure/test.check {:mvn/version "1.1.1"} 17 | org.clojure/data.json {:mvn/version "2.5.1"}}} 18 | 19 | ;; clojure -M:outdated --upgrade 20 | :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "2.11.1276"}} 21 | :main-opts ["-m" "antq.core"]} 22 | 23 | :jar {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.1.303"}} 24 | :exec-fn hf.depstar/jar 25 | :exec-args {:sync-pom true 26 | :group-id "taipei.404.vrac" 27 | :artifact-id "vrac" 28 | :version "0.1.2" 29 | :jar "vrac.jar"}} 30 | 31 | :deploy {:extra-deps {slipset/deps-deploy {:mvn/version "0.2.2"}} 32 | :exec-fn deps-deploy.deps-deploy/deploy 33 | :exec-args {:installer :remote 34 | :artifact "vrac.jar"}}}} 35 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/sub.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.sub 2 | (:require [re-frame.core :as rf])) 3 | 4 | (rf/reg-sub 5 | :router/route-match 6 | (fn [db _] 7 | (:router/route-match db))) 8 | 9 | (rf/reg-sub 10 | :comp.input/title 11 | (fn [db _] 12 | (:comp.input/title db))) 13 | 14 | (rf/reg-sub 15 | :comp.input/any-todo-item 16 | (fn [db _] 17 | (pos? (count (:todo-items db))))) 18 | 19 | (rf/reg-sub 20 | :all-todo-items-completed 21 | (fn [db _] 22 | (->> (vals (:todo-items db)) 23 | (every? :completed)))) 24 | 25 | (rf/reg-sub 26 | :todo-item-ids 27 | (fn [db _] 28 | (keys (:todo-items db)))) 29 | 30 | (rf/reg-sub 31 | :todo-item 32 | (fn [db [_ todo-item-id]] 33 | (-> db :todo-items (get todo-item-id)))) 34 | 35 | (rf/reg-sub 36 | :comp/display-type 37 | :<- [:router/route-match] 38 | (fn [router-match _] 39 | (-> router-match :data :name {:page/all-todo-items :all 40 | :page/active-todo-items :active 41 | :page/completed-todo-items :completed}))) 42 | 43 | (rf/reg-sub 44 | :show-todo-item 45 | (fn [[_ todo-item-id]] 46 | [(rf/subscribe [:comp/display-type]) 47 | (rf/subscribe [:todo-item todo-item-id])]) 48 | (fn [[display-type todo-item] _] 49 | (let [completed (:completed todo-item)] 50 | (case display-type 51 | :completed completed 52 | :active (not completed) 53 | #_:all true)))) 54 | 55 | (rf/reg-sub 56 | :active-todo-items 57 | (fn [db _] 58 | (->> (vals (:todo-items db)) 59 | (filterv (complement :completed))))) 60 | 61 | (rf/reg-sub 62 | :completed-todo-items 63 | (fn [db _] 64 | (->> (vals (:todo-items db)) 65 | (filterv :completed)))) 66 | 67 | (rf/reg-sub 68 | :active-todo-items-count 69 | :<- [:active-todo-items] 70 | (fn [active-todo-items _] 71 | (count active-todo-items))) 72 | 73 | (rf/reg-sub 74 | :complemented-todo-items-count 75 | :<- [:completed-todo-items] 76 | (fn [completed-todo-items _] 77 | (count completed-todo-items))) 78 | 79 | (rf/reg-sub 80 | :complemented-todo-items-to-clear 81 | :<- [:complemented-todo-items-count] 82 | (fn [complemented-todo-items-count _] 83 | (pos? complemented-todo-items-count))) 84 | -------------------------------------------------------------------------------- /doc/vcup-properties.md: -------------------------------------------------------------------------------- 1 | ## Vcup properties 2 | 3 | Also named "props" in the source code, the Vcup properties are always specified using Clojure keywords. 4 | 5 | When the keyword's namespace is `"a"` (e.g. `:a/for`), 6 | the keyword refers to [an HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/for). 7 | 8 | When the keyword's namespace is `"p"` (e.g. `:p/htmlFor`), 9 | the keyword refers to [a DOM element's property](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLabelElement/htmlFor). 10 | 11 | When the keyword's namespace is `"on"` (e.g. `:on/click`), 12 | the keyword refers to [an event](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event), 13 | and the value in an event handler function. 14 | 15 | When the keyword has no namespace, its meaning depends on its name: 16 | 1. Either the keyword refers to some Vrac specific properties which provide 17 | additional convenience to the user like `:style`, `:class`, `:ref`, or 18 | 2. if the keyword starts with `"data-"` then it represents 19 | [a custom data attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/data-*) 20 | (e.g. `:data-testid`), or else 21 | 3. if the prop is applied on an SVG on MathML element then it represents an attribute, or else 22 | 4. it represents a DOM element's property (e.g. `:htmlFor`, which is equivalent to `:p/htmlFor`). 23 | 24 | ### Letter case in prop names 25 | 26 | Vrac doesn't modify the letter case, the user is directly speaking the browser's language. 27 | 28 | In practice, it means that: 29 | 1. Attributes names are [the same as specified in this page](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes), 30 | e.g.: 31 | 1. `:a/readonly` 32 | 2. `:a/tabindex` 33 | 3. `:a/accept-charset` 34 | 2. DOM element's properties names are [the same as specified in this page](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement#instance_properties), 35 | e.g.: 36 | 1. `:p/readOnly` or `:readOnly` 37 | 2. `:p/tabIndex` or `:tabIndex` 38 | 3. `:p/autocorrect` or `:autocorrect` 39 | 3. Event names are [the same as specified in this page](https://developer.mozilla.org/en-US/docs/Web/API/Element#events), 40 | e.g.: 41 | 1. `:on/dblclick` 42 | 2. `:on/keydown` 43 | 3. `:on/fullscreenchange` 44 | -------------------------------------------------------------------------------- /example/test-app/src/example/ui_component.cljs: -------------------------------------------------------------------------------- 1 | (ns example.ui-component 2 | (:require [clojure.string :as str] 3 | [signaali.reactive :as sr] 4 | [vrac.web :as vw :refer [$]])) 5 | 6 | ;; ---------------------------------------------- 7 | ;; UI library's API 8 | 9 | (defn- component-from-context [component-key] 10 | (fn [props & children] 11 | (let [context (vw/get-context) 12 | component-resolver (sr/create-memo 13 | (fn [] 14 | (get @context component-key)))] 15 | (vw/reactive-fragment 16 | (fn [] 17 | ($ @component-resolver props children)))))) 18 | 19 | (def button 20 | (component-from-context :ui.component/button)) 21 | 22 | ;; ---------------------------------------------- 23 | ;; UI library's implementation 24 | 25 | ;; Components' implementation 26 | (defn default-button-component [props children] 27 | ($ :button props children)) 28 | 29 | ;; Bindings 30 | (def component-registry 31 | {:ui.component/button default-button-component}) 32 | 33 | ;; ---------------------------------------------- 34 | 35 | (defn my-custom-button [props children] 36 | ($ :div {:style {:display "inline-block" 37 | :background-color "lightblue" 38 | :padding "1em" 39 | :border-radius "0.5em"}} 40 | ($ :button 41 | props 42 | {:style {:color "white" 43 | :border-radius "0.5em"}} 44 | ($ :strong 45 | (map str/upper-case children))))) 46 | 47 | (defn overridable-ui-component [] 48 | ($ :article 49 | ($ :h2 "Overridable UI component") 50 | 51 | (vw/with-context (sr/create-signal component-registry) 52 | ($ :div 53 | ;; Default button 54 | ($ button {:style {:background-color "pink"}} "Button") 55 | 56 | (vw/with-context-update (fn [context] 57 | (-> @context 58 | (assoc :ui.component/button my-custom-button))) 59 | 60 | ;; Will resolve to the custom button component, 61 | ;; different behavior, structure and style. 62 | ($ button {:style {:background-color "pink"}} "Button")))))) 63 | 64 | (defn ui-component-root [] 65 | ($ :div 66 | ($ overridable-ui-component))) 67 | -------------------------------------------------------------------------------- /example/test-app/src/example/vcup.cljs: -------------------------------------------------------------------------------- 1 | (ns example.vcup 2 | (:require [vrac.web :as vw :refer [$]])) 3 | 4 | (defn- text-node-article [] 5 | ($ :article 6 | ($ :h2 "Text nodes") 7 | 8 | ($ :h3 "Text") 9 | "Hello, " "world!" 10 | 11 | ($ :h3 "Unicode characters") 12 | "There is two non-breakable spaces between A\u00a0and\u00a0B." 13 | 14 | ($ :h3 "Numbers") 15 | 1 " " 2 " " 3 16 | 17 | ($ :h3 "Booleans") 18 | false " " true 19 | 20 | ;; Because it could be mistaken with the element's props, this shall not work. 21 | ;;($ :h3 "Maps") 22 | ;;{:a 1 23 | ;; :b 2} 24 | 25 | ($ :h3 "Keywords") 26 | :foo " " :bar " " :foo/bar 27 | 28 | ($ :h3 "Vectors") 29 | [:div {:style {:color "lime"}} 1 2] 30 | 31 | ($ :h3 "`nil` values") 32 | nil 33 | nil 34 | 35 | ,)) 36 | 37 | (defn- image-article [] 38 | ($ :article 39 | ($ :h2 "Image") 40 | 41 | ;; Image from the article https://en.wikipedia.org/wiki/Arctic_fox 42 | ($ :img {:src "https://upload.wikimedia.org/wikipedia/commons/8/83/Iceland-1979445_%28cropped_3%29.jpg" 43 | :style {:background-color "grey" 44 | :width "400px" 45 | :height "400px" 46 | :object-fit "contain"}}))) 47 | 48 | (defn- element-article [] 49 | ($ :article 50 | ($ :h2 "Vcup elements with funny names") 51 | ($ :span#id1.class1.class2 {:class [:class3 :class4]} :span#id1.class1.class2) 52 | ($ :#id2.class3 :#id2.class3) 53 | ($ :.class4 :.class4))) 54 | 55 | (defn- fragment-article [] 56 | ($ :article 57 | ($ :h2 "Fragments") 58 | ($ :<> 59 | ($ :<> 60 | "1a" 61 | "1b") 62 | nil 63 | ($ :<> 64 | "2a" 65 | nil 66 | "2b") 67 | ($ :<> 68 | nil 69 | nil) 70 | ($ :<>)))) 71 | 72 | (defn- component-with-arguments-article [a b c] 73 | ($ :article 74 | ($ :h2 "Component with arguments") 75 | ($ :pre 76 | ($ :code 77 | "(is " (+ a b c) " (+ a b c))")))) 78 | 79 | (defn vcup-root [] 80 | ($ :div {:data-testid "vcup-root"} 81 | ($ text-node-article) 82 | ($ image-article) 83 | ($ element-article) 84 | ($ fragment-article) 85 | ($ component-with-arguments-article 100 20 3))) 86 | -------------------------------------------------------------------------------- /playwright/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from "@playwright/test"; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // import dotenv from 'dotenv'; 9 | // import path from 'path'; 10 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 11 | 12 | /** 13 | * @see https://playwright.dev/docs/test-configuration 14 | */ 15 | export default defineConfig({ 16 | testDir: "./tests", 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env.CI, 21 | /* Retry on CI only */ 22 | retries: process.env.CI ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env.CI ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: "html", 27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 28 | use: { 29 | /* Base URL to use in actions like `await page.goto('/')`. */ 30 | baseURL: "http://127.0.0.1:3000", 31 | 32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 33 | trace: "on-first-retry", 34 | }, 35 | 36 | /* Configure projects for major browsers */ 37 | projects: [ 38 | { 39 | name: "chromium", 40 | use: { ...devices["Desktop Chrome"] }, 41 | }, 42 | 43 | { 44 | name: "firefox", 45 | use: { ...devices["Desktop Firefox"] }, 46 | }, 47 | 48 | { 49 | name: "webkit", 50 | use: { ...devices["Desktop Safari"] }, 51 | }, 52 | 53 | /* Test against mobile viewports. */ 54 | // { 55 | // name: 'Mobile Chrome', 56 | // use: { ...devices['Pixel 5'] }, 57 | // }, 58 | // { 59 | // name: 'Mobile Safari', 60 | // use: { ...devices['iPhone 12'] }, 61 | // }, 62 | 63 | /* Test against branded browsers. */ 64 | // { 65 | // name: 'Microsoft Edge', 66 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 67 | // }, 68 | // { 69 | // name: 'Google Chrome', 70 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 71 | // }, 72 | ], 73 | 74 | /* Run your local dev server before starting the tests */ 75 | webServer: { 76 | timeout: 60 * 60 * 1000, // Waiting for shadow-cljs to download its dependencies. 77 | command: "cd ../example/test-app && npm install && npm run release && npm run server", 78 | url: "http://127.0.0.1:3000", 79 | reuseExistingServer: !process.env.CI, 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/event.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.event 2 | (:require [re-frame.core :as rf] 3 | [todomvc.constant :as const] 4 | [todomvc.interceptor :as interceptor])) 5 | 6 | (rf/reg-event-db 7 | :initialize-db 8 | (fn [db _] 9 | const/initial-db)) 10 | 11 | (rf/reg-event-db 12 | :router/set-route-match 13 | (fn [db [_ route-match]] 14 | (assoc db :router/route-match route-match))) 15 | 16 | (rf/reg-event-fx 17 | :local-storage/load-todo-items 18 | [(rf/inject-cofx :todo-items-from-local-store)] 19 | (fn [{:keys [db todo-items-from-local-store]} _] 20 | {:db (-> db 21 | (assoc :todo-items todo-items-from-local-store 22 | :next-todo-item-id (inc (apply max -1 (keys todo-items-from-local-store)))))})) 23 | 24 | (rf/reg-event-db 25 | :comp.input/on-title-changed 26 | (fn [db [_ title]] 27 | (assoc db :comp.input/title title))) 28 | 29 | (rf/reg-event-db 30 | :comp.input/add-todo-item 31 | [interceptor/save-todo-items-in-local-storage] 32 | (fn [db [_ title]] 33 | (let [todo-item-id (:next-todo-item-id db)] 34 | (-> db 35 | (update :next-todo-item-id inc) 36 | (assoc-in [:todo-items todo-item-id] {:id todo-item-id 37 | :title title 38 | :completed false}) 39 | (assoc :comp.input/title ""))))) 40 | 41 | (rf/reg-event-db 42 | :toggle-all-todo-items 43 | [interceptor/save-todo-items-in-local-storage] 44 | (fn [db [_ all-completed]] 45 | (-> db 46 | (update :todo-items 47 | update-vals 48 | (fn [todo-item] 49 | (assoc todo-item :completed (not all-completed))))))) 50 | 51 | (rf/reg-event-db 52 | :toggle-todo-item 53 | [interceptor/save-todo-items-in-local-storage] 54 | (fn [db [_ todo-item-id]] 55 | (-> db 56 | (update-in [:todo-items todo-item-id :completed] not)))) 57 | 58 | (rf/reg-event-db 59 | :delete-todo-item 60 | [interceptor/save-todo-items-in-local-storage] 61 | (fn [db [_ todo-item-id]] 62 | (-> db 63 | (update :todo-items dissoc todo-item-id)))) 64 | 65 | (rf/reg-event-db 66 | :set-todo-item-title 67 | [interceptor/save-todo-items-in-local-storage] 68 | (fn [db [_ todo-item-id title]] 69 | (-> db 70 | (assoc-in [:todo-items todo-item-id :title] title)))) 71 | 72 | (rf/reg-event-db 73 | :clean-completed-todo-items 74 | [interceptor/save-todo-items-in-local-storage] 75 | (fn [db _] 76 | (-> db 77 | (update :todo-items 78 | (fn [todo-items] 79 | (into {} 80 | (remove (comp :completed val)) 81 | todo-items)))))) 82 | -------------------------------------------------------------------------------- /example/test-app/resource/svg/fox-origami.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /playwright/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "playwright", 8 | "devDependencies": { 9 | "@playwright/test": "1.55.0", 10 | "@types/node": "24.5.0" 11 | } 12 | }, 13 | "node_modules/@playwright/test": { 14 | "version": "1.55.0", 15 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", 16 | "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", 17 | "dev": true, 18 | "license": "Apache-2.0", 19 | "dependencies": { 20 | "playwright": "1.55.0" 21 | }, 22 | "bin": { 23 | "playwright": "cli.js" 24 | }, 25 | "engines": { 26 | "node": ">=18" 27 | } 28 | }, 29 | "node_modules/@types/node": { 30 | "version": "24.5.0", 31 | "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.0.tgz", 32 | "integrity": "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg==", 33 | "dev": true, 34 | "license": "MIT", 35 | "dependencies": { 36 | "undici-types": "~7.12.0" 37 | } 38 | }, 39 | "node_modules/fsevents": { 40 | "version": "2.3.2", 41 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 42 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 43 | "dev": true, 44 | "hasInstallScript": true, 45 | "license": "MIT", 46 | "optional": true, 47 | "os": [ 48 | "darwin" 49 | ], 50 | "engines": { 51 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 52 | } 53 | }, 54 | "node_modules/playwright": { 55 | "version": "1.55.0", 56 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", 57 | "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", 58 | "dev": true, 59 | "license": "Apache-2.0", 60 | "dependencies": { 61 | "playwright-core": "1.55.0" 62 | }, 63 | "bin": { 64 | "playwright": "cli.js" 65 | }, 66 | "engines": { 67 | "node": ">=18" 68 | }, 69 | "optionalDependencies": { 70 | "fsevents": "2.3.2" 71 | } 72 | }, 73 | "node_modules/playwright-core": { 74 | "version": "1.55.0", 75 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", 76 | "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", 77 | "dev": true, 78 | "license": "Apache-2.0", 79 | "bin": { 80 | "playwright-core": "cli.js" 81 | }, 82 | "engines": { 83 | "node": ">=18" 84 | } 85 | }, 86 | "node_modules/undici-types": { 87 | "version": "7.12.0", 88 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", 89 | "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", 90 | "dev": true, 91 | "license": "MIT" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /example/react-interop/src/react_interop/interop.cljs: -------------------------------------------------------------------------------- 1 | (ns react-interop.interop 2 | (:require [signaali.reactive :as sr] 3 | [reagent.core :as r] 4 | [vrac.web :as vw] 5 | [uix.core :as uix] 6 | [uix.dom :as dom])) 7 | 8 | ;; --------------------------------------------------- 9 | ;; Interop utility function 10 | 11 | ;; Signaali effect which mounts some React vdom on a dom node. 12 | (defn mount-react-root-effect [dom-node-ref react-vdom-fn] 13 | (let [create-root-effect (sr/create-effect 14 | (fn [] 15 | (when-some [dom-node @dom-node-ref] 16 | (let [^js react-root (dom/create-root dom-node)] 17 | (sr/on-clean-up (fn [] (dom/unmount-root react-root))) 18 | ;; This value is returned when this effect is deref'd 19 | react-root)))) 20 | re-render-effect (sr/create-effect 21 | (fn [] 22 | (when-some [react-root @create-root-effect] 23 | (dom/render-root (react-vdom-fn) react-root))))] 24 | (vw/use-effects [create-root-effect re-render-effect]))) 25 | 26 | 27 | ;; --------------------------------------------------- 28 | ;; Siagent 29 | 30 | ;; React component implemented using Siagent 31 | ;; It demonstrates the interop with the reactive node from Signaali: 32 | ;; Basically no interop is needed, Siagent is taking care of it. 33 | (defn my-reagent-component [text text-in-reactive-node] 34 | [:div 35 | [:div "React function's input: \"" text "\""] 36 | [:div "Signaali reactive node: \"" @text-in-reactive-node "\""]]) 37 | 38 | ;; Vrac component 39 | (defn interop-with-reagent-component-article [text1 text2] 40 | (vw/$ :article 41 | (vw/$ :h2 "React component implemented via Siagent") 42 | (let [dom-node-ref (sr/create-signal nil)] 43 | (vw/$ :div {:ref dom-node-ref} 44 | (mount-react-root-effect dom-node-ref 45 | (fn [] 46 | (r/as-element [my-reagent-component @text1 text2]))))))) 47 | 48 | ;; --------------------------------------------------- 49 | ;; UIx 50 | 51 | ;; React component implemented using UIx 52 | ;; It demonstrates the interop with the reactive node from Signaali: 53 | ;; You need to use the `use-reactive` React hook. 54 | (uix/defui my-react-component [{:keys [text text-in-reactive-node]}] 55 | (uix/$ :div 56 | (uix/$ :div "React function's input: \"" text "\"") 57 | (uix/$ :div "Signaali reactive node: \"" (r/use-reactive text-in-reactive-node) "\""))) 58 | 59 | ;; Vrac component 60 | (defn interop-with-uix-component-article [text1 text2] 61 | (vw/$ :article 62 | (vw/$ :h2 "React component implemented via UIx") 63 | (let [dom-node-ref (sr/create-signal nil)] 64 | (vw/$ :div {:ref dom-node-ref} 65 | (mount-react-root-effect dom-node-ref 66 | (fn [] 67 | (uix/$ my-react-component {:text @text1 68 | :text-in-reactive-node text2}))))))) 69 | 70 | ;; --------------------------------------------------- 71 | 72 | ;; Vrac component 73 | (defn- text-input [label text-signal] 74 | (vw/$ :div 75 | label ": " 76 | (vw/$ :input 77 | (vw/props-effect (fn [] {:value @text-signal})) 78 | {:on/input (fn [^js event] 79 | (reset! text-signal (-> event .-target .-value)))}))) 80 | 81 | ;; Vrac component 82 | (defn interop-demo [] 83 | (vw/$ :main 84 | (vw/$ :h1 "Vrac + React interop demo") 85 | (let [text1 (sr/create-signal "text1") 86 | text2 (sr/create-signal "text2")] 87 | (vw/$ :div 88 | (vw/$ text-input "Text1" text1) 89 | (vw/$ text-input "Text2" text2) 90 | 91 | (vw/$ interop-with-reagent-component-article text1 text2) 92 | (vw/$ interop-with-uix-component-article text1 text2))))) 93 | -------------------------------------------------------------------------------- /example/si-frame-simple/src/simple/core.cljs: -------------------------------------------------------------------------------- 1 | (ns simple.core 2 | (:require [re-frame.core :as rf] 3 | [signaali.reactive :as sr] 4 | [vrac.web :as vw :refer [$]])) 5 | 6 | ;; A detailed walk-through of this source code is provided in the docs: 7 | ;; https://day8.github.io/re-frame/dominoes-live/ 8 | 9 | ;; -- Domino 1 - Event Dispatch ----------------------------------------------- 10 | 11 | (defn dispatch-timer-event 12 | [] 13 | (let [now (js/Date.)] 14 | (rf/dispatch [:timer now]))) ;; <-- dispatch used 15 | 16 | ;; Call the dispatching function every second. 17 | ;; `defonce` is like `def` but it ensures only one instance is ever 18 | ;; created in the face of figwheel hot-reloading of this file. 19 | (defonce do-timer (js/setInterval dispatch-timer-event 1000)) 20 | 21 | ;; -- Domino 2 - Event Handlers ----------------------------------------------- 22 | 23 | (rf/reg-event-db ;; sets up initial application state 24 | :initialize ;; usage: (dispatch [:initialize]) 25 | (fn [_ _] ;; the two parameters are not important here, so use _ 26 | {:time (js/Date.) ;; What it returns becomes the new application state 27 | :time-color "orange"})) ;; so the application state will initially be a map with two keys 28 | 29 | (rf/reg-event-db ;; usage: (dispatch [:time-color-change 34562]) 30 | :time-color-change ;; dispatched when the user enters a new colour into the UI text field 31 | (fn [db [_ new-color-value]] ;; -db event handlers given 2 parameters: current application state and event (a vector) 32 | (assoc db :time-color new-color-value))) ;; compute and return the new application state 33 | 34 | (rf/reg-event-db ;; usage: (dispatch [:timer a-js-Date]) 35 | :timer ;; every second an event of this kind will be dispatched 36 | (fn [db [_ new-time]] ;; note how the 2nd parameter is destructured to obtain the data value 37 | (assoc db :time new-time))) ;; compute and return the new application state 38 | 39 | ;; -- Domino 4 - Query ------------------------------------------------------- 40 | 41 | (rf/reg-sub 42 | :time 43 | (fn [db _] ;; db is current app state. 2nd unused param is query vector 44 | (:time db))) ;; return a query computation over the application state 45 | 46 | (rf/reg-sub 47 | :time-color 48 | (fn [db _] 49 | (:time-color db))) 50 | 51 | ;; -- Domino 5 - View Functions ---------------------------------------------- 52 | 53 | (defn clock [] 54 | (let [colour (rf/subscribe [:time-color]) 55 | time (sr/create-derived 56 | (fn [] 57 | (-> @(rf/subscribe [:time]) 58 | .toTimeString 59 | (clojure.string/split " ") 60 | first)))] 61 | ($ :div.example-clock 62 | (vw/props-effect (fn [] 63 | {:style {:color @colour}})) 64 | time))) 65 | 66 | (defn color-input [] 67 | ($ :div.color-input 68 | "Display color: " 69 | ($ :input 70 | (vw/props-effect (fn [] 71 | {:value @(rf/subscribe [:time-color])})) 72 | {:type "text" 73 | :style {:border "1px solid #CCC"} 74 | :on/input (fn [^js e] 75 | (let [text (-> e .-target .-value)] 76 | (rf/dispatch [:time-color-change text])))}))) 77 | 78 | (defn ui [] 79 | ($ :div 80 | ($ :h1 "The time is now:") 81 | ($ clock) 82 | ($ color-input))) 83 | 84 | ;; -- Entry Point ------------------------------------------------------------- 85 | 86 | (defn mount-ui [] 87 | (vw/render (js/document.getElementById "app") 88 | ($ ui))) 89 | 90 | (defn ^:dev/after-load clear-cache-and-render! [] 91 | (rf/clear-subscription-cache!) 92 | (mount-ui)) 93 | 94 | (defn ^:dev/before-load shutdown! [] 95 | (vw/dispose-render-effects)) 96 | 97 | (defn run [] 98 | (rf/dispatch-sync [:initialize]) 99 | (mount-ui)) 100 | -------------------------------------------------------------------------------- /example/test-app/src/example/context.cljs: -------------------------------------------------------------------------------- 1 | (ns example.context 2 | (:require [signaali.reactive :as sr] 3 | [vrac.web :as vw :refer [$]])) 4 | 5 | (def ^:dynamic *diy-context* (sr/create-signal {:a 10})) 6 | 7 | (defn- display-diy-context [label] 8 | (let [context *diy-context*] 9 | ($ :div label ": " context))) 10 | 11 | (defn- diy-context-article [] 12 | (let [counter (sr/create-signal 0)] 13 | ($ :article 14 | ($ :h2 "DIY context using dynamic variables") 15 | ($ :button {:on/click #(swap! counter inc)} "counter = " counter) 16 | (display-diy-context "global context") 17 | (let [context *diy-context* ;; The current context, expectedly a reactive node. 18 | local-context (sr/create-derived (fn [] (-> @context 19 | (assoc :counter @counter) 20 | (update :a + @counter))))] 21 | ;; Create a derived inner context, and set it as the new current one within this scope. 22 | (binding [*diy-context* local-context] 23 | ;; Read the new current context from somewhere in the scope. 24 | (display-diy-context "local context"))) 25 | (display-diy-context "back to the global context") 26 | 27 | ($ :div 28 | ($ :strong "Warning: ") 29 | "This won't work if we call the inner components using " 30 | ($ :code "($ display-diy-context ,,,)"))))) 31 | 32 | (defn- display-built-in-context [label] 33 | ($ :div label ": " (vw/get-context))) 34 | 35 | (defn- builtin-context-article [] 36 | (let [root-context (sr/create-signal {:a 10}) 37 | counter (sr/create-signal 0)] 38 | (vw/with-context root-context 39 | ($ :article 40 | ($ :h2 "Built-in context") 41 | ($ :button {:on/click #(swap! counter inc)} "counter = " counter) 42 | ($ display-built-in-context "global context") 43 | (vw/with-context-update (fn [parent-context] 44 | (-> @parent-context 45 | (assoc :counter @counter) 46 | (update :a + @counter))) 47 | ($ display-built-in-context "local context")) 48 | ($ display-built-in-context "back to the global context"))))) 49 | 50 | (defn- person-component [{:keys [name]}] 51 | ($ :<> 52 | name 53 | (if-some [context (vw/get-context)] 54 | (sr/create-derived (fn [] 55 | (when (:birthday-party @context) 56 | " says HAPPY BIRTHDAY !!! \uD83E\uDD73"))) 57 | " is there, where is the context ?!"))) 58 | 59 | (defn- context-on-reactive-fragment [] 60 | (let [root-context (sr/create-signal {:birthday-party true}) 61 | person-count (sr/create-signal 2) 62 | persons (sr/create-derived (fn [] 63 | (->> (cycle ["Alice" "Bob" "Connie" "Diva" "Elric" "Fred" "Giana"]) 64 | (take @person-count) 65 | (map-indexed (fn [index name] 66 | {:id index 67 | :name name})))))] 68 | ($ :article 69 | ($ :h2 "Context on a reactive fragment") 70 | ($ :button {:on/click #(swap! person-count inc)} "Add 1 person to the party") 71 | ($ :button {:on/click #(swap! root-context update :birthday-party not)} 72 | (sr/create-memo (fn [] 73 | (if (-> @root-context :birthday-party) 74 | "Turn the party OFF" 75 | "Turn the party ON")))) 76 | (vw/with-context root-context 77 | ($ :ul 78 | (vw/for-fragment* persons :id 79 | (fn [person] ($ :li 80 | ($ person-component person))))))))) 81 | 82 | (defn context-root [] 83 | ($ :div 84 | ($ diy-context-article) 85 | ($ builtin-context-article) 86 | ($ context-on-reactive-fragment) 87 | ,)) 88 | -------------------------------------------------------------------------------- /test/vrac/dsl/macro_test.cljc: -------------------------------------------------------------------------------- 1 | (ns vrac.dsl.macro-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [vrac.dsl.macro :as sut] 4 | [vrac.test.util :refer [make-gensym]])) 5 | 6 | (deftest thread-first-test 7 | (testing "the -> macro" 8 | (is (= `(prn (+ ~'a 1)) 9 | (sut/thread-first `(-> ~'a (+ 1) prn)))))) 10 | 11 | (deftest thread-last-test 12 | (testing "the ->> macro" 13 | (is (= `(prn (+ 1 ~'a)) 14 | (sut/thread-last `(->> ~'a (+ 1) prn)))))) 15 | 16 | (deftest thread-as-test 17 | (testing "the as-> macro" 18 | (is (= `(let [~'y (+ 1 2) 19 | ~'y (+ ~'y 3)] 20 | (+ ~'y 4)) 21 | (sut/thread-as `(as-> (+ 1 2) ~'y 22 | (+ ~'y 3) 23 | (+ ~'y 4))))))) 24 | 25 | (deftest destruct-test 26 | (testing "sequential destruct" 27 | (is (= `[~'d ~'x 28 | ~'a (nth ~'d 0) 29 | ~'b (nth ~'d 1) 30 | ~'c (nth ~'d 2) 31 | ~'rest (drop 3 ~'d)] 32 | (#'sut/destructure '[[a b c & rest :as d] x])))) 33 | 34 | (testing "associative destruct" 35 | (is (= '[x y 36 | a (:aa x) 37 | bb (:bb x) 38 | c (:cc bb) 39 | d (:dd bb)] 40 | (#'sut/destructure '[{a :aa 41 | {c :cc 42 | d :dd 43 | :as bb} :bb 44 | :as x} y])))) 45 | 46 | (testing "associative destruct inside sequential destruct" 47 | (with-redefs [gensym (make-gensym)] 48 | (is (= `[~'x ~'y 49 | ~'map__1 (nth ~'x 0) 50 | ~'a (:a ~'map__1) 51 | ~'map__2 (nth ~'x 1) 52 | ~'b (:b ~'map__2)] 53 | (#'sut/destructure '[[{a :a} {b :b} :as x] y]))))) 54 | 55 | (testing "sequential destruct inside associative destruct" 56 | (with-redefs [gensym (make-gensym)] 57 | (is (= `[~'x ~'y 58 | ~'vec__1 (:aa ~'x) 59 | ~'a1 (nth ~'vec__1 0) 60 | ~'a2 (nth ~'vec__1 1) 61 | ~'bb (:bb ~'x) 62 | ~'vec__2 (:cc ~'bb) 63 | ~'c1 (nth ~'vec__2 0) 64 | ~'c2 (nth ~'vec__2 1)] 65 | (#'sut/destructure '[{[a1 a2] :aa 66 | {[c1 c2] :cc 67 | :as bb} :bb 68 | :as x} y]))))) 69 | 70 | (testing "default values inside associative destruct" 71 | (is (= '[x y 72 | a (:aa x) 73 | b (:bb x :bb-default) 74 | i (:i x) 75 | j (:j x :jj-default) 76 | k (:foo/k x) 77 | l (:foo/l x :ll-default) 78 | p (:bar/p x) 79 | q (:bar/q x :qq-default) 80 | r (:foo/r x) 81 | s (:foo/s x :ss-default)] 82 | (#'sut/destructure '[{a :aa 83 | b :bb 84 | :keys [i j foo/k foo/l] 85 | :bar/keys [p q foo/r foo/s] 86 | :or {b :bb-default 87 | j :jj-default 88 | l :ll-default 89 | q :qq-default 90 | s :ss-default} 91 | :as x} y])))) 92 | 93 | 94 | (testing "the :& syntax in associative destruct" 95 | (is (= `[~'x ~'y 96 | ~'a (:a ~'x) 97 | ~'b (:b ~'x) 98 | ~'c (:c ~'x) 99 | ~'bar (:foo/bar ~'x) 100 | ~'baz (:foo/baz ~'x) 101 | ~'rest (dissoc ~'x :a :b :c :foo/bar :foo/baz)] 102 | (#'sut/destructure '[{a :a 103 | :keys [b c] 104 | :foo/keys [bar baz] 105 | :& rest 106 | :as x} y]))))) 107 | 108 | (deftest expand-let-test 109 | (testing "let bindings expansion" 110 | (with-redefs [gensym (make-gensym)] 111 | (is (= `(~'let [~'map__1 {:a 1} 112 | ~'a (:a ~'map__1)] 113 | ~'a) 114 | (sut/expand-let `(~'let [~'{a :a} {:a 1}] ~'a))))) 115 | (with-redefs [gensym (make-gensym)] 116 | (is (= `(~'let [~'a 1 117 | ~'b 2] 118 | (~'do 119 | ~'a 120 | ~'b)) 121 | (sut/expand-let `(~'let [~'a 1 122 | ~'b 2] 123 | ~'a 124 | ~'b))))))) 125 | 126 | (deftest expand-for-test 127 | (testing "for bindings expansion" 128 | (with-redefs [gensym (make-gensym)] 129 | (is (= `(~'for [~'item__1 [[1 2] [3 4]] 130 | :let [~'vec__2 ~'item__1 131 | ~'a (nth ~'vec__2 0) 132 | ~'b (nth ~'vec__2 1)] 133 | :let [~'vec__3 [10 20] 134 | ~'c (nth ~'vec__3 0) 135 | ~'d (nth ~'vec__3 1)]] 136 | [~'a ~'b ~'c ~'d]) 137 | (sut/expand-for `(~'for [~'[a b] ~[[1 2] [3 4]] 138 | :let [~'[c d] ~'[10 20]]] 139 | ~'[a b c d]))))))) 140 | 141 | (deftest expand-when-test 142 | (testing "when single body" 143 | (is (= '(when true (do 1 2)) 144 | (sut/expand-when '(when true 1 2)))))) 145 | 146 | (deftest expand-fn-test 147 | (testing "fn single body" 148 | (is (= '(fn [a] (do 1 2)) 149 | (sut/expand-fn '(fn [a] 1 2)))) 150 | (is (= '(fn foo [a] (do 1 2)) 151 | (sut/expand-fn '(fn foo [a] 1 2)))))) 152 | 153 | (deftest expand-defn-test 154 | (testing "defn single body" 155 | (is (= '(defn foo [a] (do 1 2)) 156 | (sut/expand-defn '(defn foo [a] 1 2)))))) 157 | -------------------------------------------------------------------------------- /example/test-app/src/example/svg.cljs: -------------------------------------------------------------------------------- 1 | (ns example.svg 2 | (:require [shadow.resource :as rc] 3 | [vrac.web :as vw :refer [$]])) 4 | 5 | (defn- fox-origami [] 6 | ;; TODO: Should I cache the dom and provide a clone on each usage? 7 | ;; Is there another way to reference the same SVG from multiple places? 8 | ;; Is the DOM in fact a directed graph? What happens if I add the same DOM element in multiple places? 9 | (vw/html-text-to-dom (rc/inline "svg/fox-origami.svg"))) 10 | 11 | (defn- string-to-svg-element [] 12 | ($ :article 13 | ($ :h2 "Using an SVG element built from a string") 14 | (fox-origami))) 15 | 16 | (defn- changing-an-existing-svg-element [] 17 | ($ :article 18 | ($ :h2 "Changing an existing SVG element") 19 | 20 | ($ :h3 "Custom width and height") 21 | ($ (fox-origami) 22 | {:width "20rem" 23 | :height "20rem"}) 24 | 25 | ($ :h3 "Then add children") 26 | ($ (fox-origami) 27 | {:width "20rem" 28 | :height "20rem"} 29 | ;; Right eye 30 | ($ :circle {:cx 14.5 31 | :cy 20 32 | :r 3.5 33 | :fill "white" 34 | :stroke "black" 35 | :stroke-width 1}) 36 | ;; Left eye 37 | ($ :circle {:cx 24.5 38 | :cy 20 39 | :r 3.5 40 | :fill "white" 41 | :stroke "black" 42 | :stroke-width 1})))) 43 | 44 | (defn- svg-element-using-vcup [] 45 | ($ :article 46 | ($ :h2 "SVG element using Vcup") 47 | 48 | ;; from https://www.svgrepo.com/svg/423821/fox-origami-paper 49 | ;; Author: Lima Studio 50 | ;; License: CC Attribution License 51 | ($ :svg {:width "800px" 52 | :height "800px" 53 | :viewBox "0 0 64 64" 54 | :xmlns "http://www.w3.org/2000/svg"} 55 | ($ :path {:d "M7.45,21.19l9.65-5.88L7.56,5.16c-.16,.12-.27,.31-.27,.54v14.64c0,.29,.06,.58,.16,.85h0Z" 56 | :fill "#fca65c"}) 57 | ($ :path {:d "M31.17,5.16l-9.54,10.15,9.81,6.04V5.69c0-.22-.11-.41-.27-.53Z" 58 | :fill "#fca65c"}) 59 | ($ :path {:d "M7.56,5.16l9.21,9.8-1.95,1.75-3.16,1.94L7.56,5.16Z" 60 | :fill "#f5934a"}) 61 | ($ :path {:d "M31.17,5.16l-9.21,9.8,1.95,1.75,3.16,1.94,4.1-13.5Z" 62 | :fill "#f5934a"}) 63 | ($ :path {:d "M8.05,5.01c-.18-.02-.35,.04-.49,.15l5.77,10.15h12.07l5.77-10.15c-.18-.15-.43-.21-.67-.11l-11.13,4.22L8.23,5.05c-.06-.02-.12-.04-.18-.04h0Z" 64 | :fill "#feb87e"}) 65 | ($ :path {:d "M12.78,26.84v26.44c0,.26,.05,.52,.14,.77l13.81-10.57,4.71-22.13-18.65,5.49Z" 66 | :fill "#f0f4f6"}) 67 | ($ :path {:d "M31.44,21.35L12.78,44.91V26.84l18.65-5.49Z" 68 | :fill "#e2eef2"}) 69 | ($ :path {:d "M31.44,21.34L12.92,54.04c.12,.35,.31,.68,.58,.95l3.3,3.3c.46,.46,1.07,.71,1.72,.71h13.49l19.14-7.05,1.15-16.09c-.23-.27-.49-.51-.78-.71l-20.07-13.81Z" 70 | :fill "#fca65c"}) 71 | ($ :path {:d "M49.88,34.03l-5.27,14.03-18.3,10.94h5.69l19.14-7.05,1.15-16.09c-.23-.27-.49-.51-.78-.71l-1.63-1.12Z" 72 | :fill "#f5934a"}) 73 | ($ :path {:d "M52.29,35.86l-4.72,13.71,6.76-.99,2.27-4.48c-.08-.33-.2-.65-.36-.96l-3.44-6.53c-.14-.27-.32-.52-.52-.75Z" 74 | :fill "#feb87e"}) 75 | ($ :path {:d "M25.4,15.31l-6.04-6.03-6.03,6.03h0l-5.88,5.88c.12,.32,.3,.62,.55,.87l4.78,4.78,1.23,1.23,5.16,1.6,5.54-1.6h0l6.72-6.72-6.04-6.04Z" 76 | :fill "#fdc99c"}) 77 | ($ :path {:d "M14.01,28.07l3.74,3.74c.89,.89,2.33,.89,3.22,0l3.74-3.74H14.01Z" 78 | :fill "#9dacb9"}) 79 | ($ :path {:d "M56.6,44.1l-9.03,5.47h0l-15.57,9.43h23.7c.56,0,1.01-.45,1.01-1.01v-12.96c0-.32-.04-.63-.11-.94h0Z" 80 | :fill "#fdc99c"}) 81 | ($ :path {:d "M19.36,9.27l-7.16,4.07,1.12,1.97,6.03-6.04Z" 82 | :fill "#fca65c"}) 83 | ($ :path {:d "M19.36,9.27l7.16,4.07-1.12,1.97-6.03-6.04Z" 84 | :fill "#fca65c"}) 85 | ($ :path {:d "M56.6,44.1l-7.64,1.43-1.39,4.04,9.03-5.47Z" 86 | :fill "#fca65c"}) 87 | ($ :path {:d "M12.79,55.7l3.3,3.3c.65,.65,1.51,1,2.42,1H55.7c1.11,0,2.01-.9,2.01-2.01v-12.96c0-.82-.2-1.63-.58-2.36l-3.43-6.53c-.38-.72-.94-1.35-1.62-1.82l-19.64-13.51V5.69c0-.56-.27-1.08-.73-1.39-.46-.32-1.04-.39-1.56-.19l-10.78,4.09L8.58,4.11c-.52-.2-1.1-.13-1.56,.19s-.73,.84-.73,1.39v14.64c0,.92,.36,1.78,1,2.42l4.49,4.49v26.02c0,.92,.36,1.78,1,2.42Zm1-26.45l3.26,3.26c1.28,1.28,3.36,1.28,4.63,0l5.26-5.26-13.16,23.23V29.25Zm2.65-.18h5.87l-2.03,2.03c-.5,.5-1.31,.5-1.81,0l-2.03-2.03Zm19.15,28.93l20.13-12.19v12.19h-20.13Zm19.77-14.39s0,.05,.02,.07l-5.96,3.61,3.11-9.05,2.83,5.37Zm-4.41-7.63c.08,.05,.11,.14,.18,.2l-4.38,12.72-15.02,9.1h-13.21c-.38,0-.74-.15-1.01-.42l-3.3-3.3c-.07-.07-.06-.19-.11-.28L31.77,22.79l19.17,13.19ZM12.08,15.14l-3.79,3.79V8.47l3.79,6.67Zm-2.55-8.53l8.03,3.05-4.02,4.02-4.02-7.06Zm20.9,1.86v10.46l-3.79-3.79,3.79-6.67Zm-5.26,5.2l-4.02-4.02,8.03-3.05-4.02,7.07Zm-16.47,7.67h0l10.66-10.66,10.66,10.66-5.72,5.72H14.43l-5.72-5.72Z"})))) 88 | 89 | (defn- svg-element-with-class-and-style [] 90 | ($ :article 91 | ($ :h2 "SVG element with a class and style") 92 | ($ :div 93 | ($ :svg {:xmlns "http://www.w3.org/2000/svg" 94 | :class "my-class" 95 | :style {:color "lightgreen"}} 96 | ($ :circle {:cx 50 97 | :cy 50 98 | :r 50 99 | :fill "currentColor"}))))) 100 | 101 | (defn- foreign-object-inside-svg [] 102 | ($ :article 103 | ($ :h2 " element inside SVG") 104 | ($ :svg {:xmlns "http://www.w3.org/2000/svg" 105 | :width "40rem" 106 | :height "40rem" 107 | :viewBox "0 0 200 200"} 108 | ($ :polygon {:points "5,5 195,10 185,185 10,195"}) 109 | ($ :foreignObject {:x "20" 110 | :y "20" 111 | :width "160" 112 | :height "160"} 113 | ($ :div {:xmlns "http://www.w3.org/1999/xhtml" 114 | :style {:background-color "purple" 115 | :color "pink" 116 | :height "100%"}} 117 | "This is an HTML div inside a SVG element. " 118 | "It is wrapped inside a 'foreignObject' SVG element."))))) 119 | 120 | (defn svg-root [] 121 | ($ :div 122 | ($ string-to-svg-element) 123 | ($ changing-an-existing-svg-element) 124 | ($ svg-element-using-vcup) 125 | ($ svg-element-with-class-and-style) 126 | ($ foreign-object-inside-svg) 127 | ,)) 128 | -------------------------------------------------------------------------------- /doc/ai-context.md: -------------------------------------------------------------------------------- 1 | # Vrac + Signaali + Si-Frame Code Generation Guide 2 | 3 | ## Dependencies 4 | ```clojure 5 | taipei.404.vrac/vrac {:mvn/version "0.1.2"} 6 | ``` 7 | 8 | ## Namespace Setup 9 | ```clojure 10 | (ns example.app 11 | (:require [vrac.web :as vw :refer [$]] 12 | [signaali.reactive :as sr] 13 | [re-frame.core :as rf])) ; Optional, for Si-Frame integration 14 | ``` 15 | 16 | ## Element Creation with `$` 17 | - **HTML elements**: `($ :div ...)` `($ :span#id.class1.class2 ...)` 18 | - **Fragments**: `($ :<> child1 child2 ...)` 19 | - **Components**: `($ component-fn arg1 arg2)` 20 | - **Children**: strings, numbers, booleans, keywords, vectors, nil (renders nothing), reactive nodes 21 | 22 | ## Properties (Props) 23 | 24 | ### Static Props 25 | ```clojure 26 | ($ :div.class1 {:style {:color "red" :padding "1em"} 27 | :class [:class2] 28 | :data-testid "my-div"} 29 | "content") 30 | ``` 31 | 32 | ### Reactive Props 33 | Use `vw/props-effect` with a function returning a prop map: 34 | ```clojure 35 | ($ :input 36 | (vw/props-effect (fn [] {:value @text-signal})) 37 | {:type "text" 38 | :on/input (fn [e] (reset! text-signal (.. e -target -value)))}) 39 | ``` 40 | 41 | ### Prop Namespaces 42 | - `:a/...` - HTML attributes (e.g., `:a/for`, `:a/readonly`) 43 | - `:p/...` - DOM properties (e.g., `:p/htmlFor`, `:p/innerHTML`) 44 | - `:on/...` - Event handlers (e.g., `:on/click`, `:on/input`) 45 | - No namespace - Vrac special props (`:style`, `:class`, `:ref`, `:data-*`) or DOM properties 46 | 47 | ### Multiple Prop Maps 48 | Props compose together; later values override earlier ones (except `:style` and `:class` which merge): 49 | ```clojure 50 | ($ :div {:class :base-class} 51 | {:class :extra-class} ; Results in both classes 52 | {:style {:color "red"}} 53 | {:style {:padding "1em"}}) ; Both styles applied 54 | ``` 55 | 56 | ## Reactive State (Signaali) 57 | 58 | ### Creating State 59 | ```clojure 60 | (let [counter (sr/create-state 0) ; Mutable state 61 | text (sr/create-signal "hello") ; Immutable signal 62 | doubled (sr/create-derived (fn [] (* 2 @counter))) ; Computed 63 | memoized (sr/create-memo (fn [] (expensive-calc @counter)))] ; Cached computed 64 | ...) 65 | ``` 66 | 67 | ### Using State 68 | - Read: `@signal-or-state` 69 | - Write: `(reset! signal-or-state new-value)` or `(swap! signal-or-state update-fn)` 70 | - Reactive nodes auto-update UI when dereferenced in render 71 | 72 | ## Reactive Fragments 73 | 74 | Don't use reactive fragment on static data 75 | 76 | ### Conditional Rendering 77 | ```clojure 78 | ;; If/else 79 | (vw/if-fragment condition-fn 80 | ($ :div "then branch") 81 | ($ :div "else branch")) 82 | 83 | ;; When (no else) 84 | (vw/when-fragment condition-fn 85 | ($ :div "shown when true")) 86 | 87 | ;; Case 88 | (vw/case-fragment value-fn 89 | :route/home ($ :div "Home") 90 | :route/blog ($ :div "Blog") 91 | ($ :div "Default")) 92 | 93 | ;; Cond 94 | (vw/cond-fragment 95 | (= @route :home) ($ :div "Home") 96 | (= @route :blog) ($ :div "Blog") 97 | :else ($ :div "Default")) 98 | ``` 99 | 100 | **Important**: Pass a **function** to `vw/if-fragment`, `vw/when-fragment`, and `vw/case-fragment`: 101 | ```clojure 102 | ;; Correct: 103 | (vw/if-fragment (fn [] (even? @counter)) ...) 104 | (vw/if-fragment counter ...) ; OK if counter is already a reactive node 105 | 106 | ;; Wrong: 107 | (vw/if-fragment (even? @counter) ...) ; Don't evaluate directly 108 | ``` 109 | 110 | ### List Rendering 111 | ```clojure 112 | (vw/for-fragment* 113 | coll-fn ; (fn [] @items) or just items-signal 114 | key-fn ; :id or (fn [item] (:id item)), optional 115 | item-component) ; (fn [item] ($ :div (:name item))) 116 | ``` 117 | 118 | ## Components 119 | Define as functions returning vcup: 120 | ```clojure 121 | (defn person-card [person] 122 | ($ :div.card 123 | ($ :h3 (:name person)) 124 | ($ :p (:bio person)))) 125 | 126 | ;; Use with $ 127 | ($ person-card {:name "Alice" :bio "Developer"}) 128 | ``` 129 | 130 | ## Context 131 | ```clojure 132 | ;; Set context 133 | (vw/with-context (sr/create-signal {:theme "dark"}) 134 | ($ child-component)) 135 | 136 | ;; Update based on parent context 137 | (vw/with-context-update (fn [parent-ctx] 138 | (assoc @parent-ctx :nested true)) 139 | ($ child-component)) 140 | 141 | ;; Read context 142 | (defn child-component [] 143 | (let [ctx (vw/get-context)] 144 | ($ :div "Theme: " (sr/create-derived (fn [] (:theme @ctx)))))) 145 | ``` 146 | 147 | ## Refs 148 | Capture DOM elements: 149 | ```clojure 150 | (let [element-ref (sr/create-signal nil)] 151 | ($ :div 152 | ($ :input {:ref element-ref}) 153 | ($ :button {:on/click (fn [] (.focus @element-ref))} "Focus input"))) 154 | ``` 155 | 156 | ## Re-frame Integration 157 | ```clojure 158 | ;; Subscriptions 159 | (rf/reg-sub :counter (fn [db _] (:counter db))) 160 | 161 | ;; In components - wrap subscriptions for derived values 162 | (defn counter-display [] 163 | (let [counter (rf/subscribe [:counter]) 164 | doubled (sr/create-derived (fn [] (* 2 @counter)))] 165 | ($ :div "Counter: " counter " Doubled: " doubled))) 166 | 167 | ;; Dispatch events 168 | ($ :button {:on/click #(rf/dispatch [:increment])} "+") 169 | ``` 170 | 171 | ## Application Entry Point 172 | ```clojure 173 | (defn mount-ui [] 174 | (vw/render (js/document.getElementById "app") 175 | ($ root-component))) 176 | 177 | (defn ^:dev/after-load reload! [] 178 | (mount-ui)) 179 | 180 | (defn ^:dev/before-load shutdown! [] 181 | (vw/dispose-render-effects)) 182 | 183 | (defn init [] 184 | (mount-ui)) 185 | ``` 186 | 187 | ## SVG and MathML 188 | Use standard element names: 189 | ```clojure 190 | ;; SVG 191 | ($ :svg {:width "100" :height "100" :xmlns "http://www.w3.org/2000/svg"} 192 | ($ :circle {:cx 50 :cy 50 :r 40 :fill "blue"})) 193 | 194 | ;; MathML 195 | ($ :math {:display "block"} 196 | ($ :mrow 197 | ($ :mi "x") 198 | ($ :mo "+") 199 | ($ :mn "1"))) 200 | 201 | ;; Convert HTML/SVG string to DOM 202 | (vw/html-text-to-dom "...") 203 | ``` 204 | 205 | ## Common Patterns 206 | 207 | ### Counter 208 | ```clojure 209 | (defn counter [] 210 | (let [count (sr/create-state 0)] 211 | ($ :div 212 | "Count: " count 213 | ($ :button {:on/click #(swap! count inc)} "+")))) 214 | ``` 215 | 216 | ### Controlled Input 217 | ```clojure 218 | (defn text-input [] 219 | (let [value (sr/create-signal "")] 220 | ($ :input 221 | (vw/props-effect (fn [] {:value @value})) 222 | {:on/input (fn [e] (reset! value (.. e -target -value)))}))) 223 | ``` 224 | 225 | ### Toggle Visibility 226 | ```clojure 227 | (defn collapsible [title content] 228 | (let [open? (sr/create-state false)] 229 | ($ :div 230 | ($ :button {:on/click #(swap! open? not)} title) 231 | (vw/when-fragment open? 232 | ($ :div content))))) 233 | ``` -------------------------------------------------------------------------------- /example/test-app/src/example/reactive_fragment.cljs: -------------------------------------------------------------------------------- 1 | (ns example.reactive-fragment 2 | (:require [clojure.string :as str] 3 | [signaali.reactive :as sr] 4 | [vrac.web :as vw :refer [$]])) 5 | 6 | (defn- counter-component [counter-state] 7 | ($ :div 8 | "Counter value: " counter-state 9 | ($ :div 10 | ($ :button {:on/click #(swap! counter-state inc)} "+1") 11 | ($ :button {:on/click #(swap! counter-state (fn [n] (+ n 2)))} "+2")))) 12 | 13 | (defn- if-fragment-article [] 14 | (let [counter-state (sr/create-state 0)] 15 | ($ :article 16 | ($ :h2 "If fragment") 17 | ($ counter-component counter-state) 18 | (vw/if-fragment (fn [] (even? @counter-state)) 19 | ($ :div "The value is even.") 20 | ($ :div "The value is odd."))))) 21 | 22 | (defn- case-fragment-article [] 23 | (let [state (sr/create-state {:current-route :route/homepage})] 24 | ($ :article 25 | ($ :h2 "Case fragment") 26 | ($ :button {:on/click #(swap! state assoc :current-route :route/homepage)} "Home") 27 | ($ :button {:on/click #(swap! state assoc :current-route :route/blog)} "Blog") 28 | ($ :button {:on/click #(swap! state assoc :current-route :route/about)} "About") 29 | ($ :button {:on/click #(swap! state assoc :current-route :route/bookmark)} "Non-existing route") 30 | (vw/case-fragment (fn [] (:current-route @state)) 31 | :route/homepage ($ :div "This is the homepage.") 32 | :route/blog ($ :div "This is the blog.") 33 | :route/about ($ :div "This is the about page.") 34 | ($ :div "This is the 'not found' page, for any other route."))))) 35 | 36 | (defn- cond-fragment-article [] 37 | (let [current-route (sr/create-state :route/homepage)] 38 | ($ :article 39 | ($ :h2 "Cond fragment") 40 | ($ :button {:on/click #(reset! current-route :route/homepage)} "Home") 41 | ($ :button {:on/click #(reset! current-route :route/blog)} "Blog") 42 | ($ :button {:on/click #(reset! current-route :route/about)} "About") 43 | ($ :button {:on/click #(reset! current-route :route/bookmark)} "Non-existing route") 44 | ;; The vw/cond-fragment macro groups the clauses' conditions and wraps them into a fn. 45 | (vw/cond-fragment 46 | (= @current-route :route/homepage) ($ :div "This is the homepage.") 47 | (= @current-route :route/blog) ($ :div "This is the blog.") 48 | (= @current-route :route/about) ($ :div "This is the about page.") 49 | :else ($ :div "This is the 'not found' page, for any other route."))))) 50 | 51 | (defn- for-fragment-article [] 52 | (let [state (sr/create-state {:persons [{:id 0 :name "Alice"} 53 | {:id 1 :name "Barbara"} 54 | {:id 2 :name "Cedric"}]})] 55 | ($ :article 56 | ($ :h2 "For fragment") 57 | 58 | (let [new-person-name (sr/create-state "Louise")] 59 | ($ :div 60 | ($ :input 61 | (vw/props-effect (fn [] {:value @new-person-name})) 62 | {:on/input (fn [event] 63 | (reset! new-person-name (-> event .-target .-value)))}) 64 | ($ :button {:on/click (fn [] 65 | (when-not (str/blank? @new-person-name) 66 | (swap! state update :persons 67 | (fn [persons] 68 | (-> persons 69 | (conj {:id (count persons) 70 | :name @new-person-name}))))) 71 | (reset! new-person-name ""))} 72 | "Add"))) 73 | 74 | ($ :div 75 | (vw/for-fragment* (fn [] (:persons @state)) ;; coll-fn 76 | :id ;; key-fn 77 | (fn [{:keys [id name]}] ;; item-component 78 | ($ :div "[" id "] " name))) 79 | #_ ;; ideally 80 | (vw/for-fragment [{:keys [id name]} (:persons @state)] 81 | ^{:key id} ($ :div "[" id "] " name)) 82 | 83 | #_ ;; nice to have: supports the Clojure for syntax 84 | (vw/for-fragment [person (:persons @state) 85 | :let [{:keys [id name]} person] 86 | :when (even? id) 87 | :while (< id 10)] 88 | ^{:key id} ($ :div "[" id "] " name)) 89 | #_ ;; That would translate to this: 90 | (vw/for-fragment* (fn [] 91 | (for [person (:persons @state) 92 | :let [{:keys [id name]} person] 93 | :when (even? id) 94 | :while (< id 10)] 95 | ;; Collects everything which is used in the body of the `for`, 96 | ;; either for the key-fn or the item-component 97 | {:id id 98 | :name name})) 99 | ;; Collects everything used in the key expression, here only `id`. 100 | (fn [{:keys [id]}] 101 | id) 102 | ;; Collects everything used in the item, here `id` and `name`. 103 | (fn [{:keys [id name]}] 104 | ($ :div "[" id "] " name))) 105 | 106 | ;; Idea: write some unit tests for the for-fragment macro 107 | ,)))) 108 | 109 | (defn dynamic-fragment-crash-test [] 110 | ($ :article 111 | ($ :h2 "Dynamic fragments composition (crash test)") 112 | (vw/for-fragment* (range 2) 113 | (fn [index1] 114 | (vw/for-fragment* (range 2) 115 | (fn [index2] 116 | (let [just-started (sr/create-state true) 117 | is-even (sr/create-state true)] 118 | (vw/if-fragment just-started 119 | ($ :div ($ :button {:on/click #(reset! just-started false)} "Start")) 120 | (vw/if-fragment is-even 121 | ($ :div 122 | (str index1 " " index2 " ") 123 | "The value is even." 124 | ($ :button {:on/click #(reset! is-even false)} "Make it odd.")) 125 | ($ :div 126 | (str index1 " " index2 " ") 127 | "The value is odd." 128 | ($ :button {:on/click #(reset! is-even true)} "Make it even."))))))))))) 129 | 130 | (defn reactive-fragment-root [] 131 | ($ :div 132 | ($ if-fragment-article) 133 | ($ case-fragment-article) 134 | ($ cond-fragment-article) 135 | ($ for-fragment-article) 136 | ($ dynamic-fragment-crash-test) 137 | ,)) 138 | -------------------------------------------------------------------------------- /example/todomvc/src/todomvc/view.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.view 2 | (:require [clojure.string :as str] 3 | [re-frame.core :as rf] 4 | [reitit.frontend.easy :as rtfe] 5 | [todomvc.constant :as const] 6 | [signaali.reactive :as sr] 7 | [vrac.web :as vw :refer [$]])) 8 | 9 | (defn- focus-on-create [ref] 10 | (sr/create-effect 11 | (fn [] 12 | (when-some [^js input-element @ref] 13 | (.focus input-element))))) 14 | 15 | (defn todo-item-creation-input [] 16 | (let [title (rf/subscribe [:comp.input/title]) 17 | ref (sr/create-signal nil)] 18 | ($ :<> 19 | (vw/use-effects [(focus-on-create ref)]) 20 | ($ :input.new-todo 21 | (vw/props-effect 22 | (fn [] 23 | {:value @title})) 24 | {:ref ref 25 | :type "text" 26 | :placeholder "What needs to be done?" 27 | :on/input (fn [^js event] 28 | (rf/dispatch [:comp.input/on-title-changed (-> event .-target .-value)])) 29 | :on/keydown (fn [^js event] 30 | (let [key-pressed (.-which event) 31 | trimmed-title (str/trim @title)] 32 | (when (and (= key-pressed const/enter-keycode) 33 | (not (str/blank? trimmed-title))) 34 | (rf/dispatch [:comp.input/add-todo-item trimmed-title]))))})))) 35 | 36 | (defn toggle-items-button [] 37 | (let [all-completed (rf/subscribe [:all-todo-items-completed])] 38 | ($ :<> 39 | ($ :input#toggle-all.toggle-all 40 | {:type "checkbox" 41 | :on/change (fn [] 42 | (rf/dispatch [:toggle-all-todo-items @all-completed]))} 43 | (vw/props-effect 44 | (fn [] 45 | {:checked @all-completed}))) 46 | ($ :label {:htmlFor "toggle-all"} "Mark all as complete")))) 47 | 48 | (defn todo-edit [todo-item editing] 49 | (let [ref (sr/create-signal nil) 50 | default (:title @todo-item) 51 | edit-title (sr/create-state default)] 52 | (vw/when-fragment editing 53 | ($ :<> 54 | (vw/use-effects [(focus-on-create ref)]) 55 | ($ :input.edit 56 | (vw/props-effect 57 | (fn [] 58 | {:value @edit-title})) 59 | {:ref ref 60 | :type "text" 61 | :on/input (fn [^js event] 62 | (reset! edit-title (-> event .-target .-value))) 63 | :on/blur (fn [_] 64 | (reset! editing false) 65 | (rf/dispatch [:set-todo-item-title (:id @todo-item) @edit-title])) 66 | :on/keydown (fn [^js event] 67 | (let [key-pressed (.-which event)] 68 | (cond 69 | (= key-pressed const/enter-keycode) 70 | (do (reset! editing false) 71 | (rf/dispatch [:set-todo-item-title (:id @todo-item) @edit-title])) 72 | 73 | (= key-pressed const/escape-keycode) 74 | (do (reset! editing false) 75 | (reset! edit-title default)))))}))))) 76 | 77 | (defn todo-item [todo-item-id] 78 | (let [editing (sr/create-state false) 79 | todo (rf/subscribe [:todo-item todo-item-id]) 80 | show-todo-item (rf/subscribe [:show-todo-item todo-item-id]) 81 | title (sr/create-memo (fn [] (:title @todo))) 82 | completed (sr/create-memo (fn [] (:completed @todo)))] 83 | ($ :li 84 | (vw/props-effect 85 | (fn [] 86 | {:class [(when @completed "completed") 87 | (when @editing "editing")] 88 | :style {:display (if @show-todo-item 89 | "list-item" 90 | "none")}})) 91 | ($ :div.view 92 | ($ :input.toggle 93 | {:type "checkbox" 94 | :on/change (fn [_] 95 | (rf/dispatch [:toggle-todo-item todo-item-id]))} 96 | (vw/props-effect 97 | (fn [] 98 | {:checked @completed}))) 99 | ($ :label {:on/dblclick (fn [_] 100 | (reset! editing true))} 101 | title) 102 | ($ :button.destroy {:on/click (fn [_] 103 | (rf/dispatch [:delete-todo-item todo-item-id]))})) 104 | ($ todo-edit todo editing)))) 105 | 106 | (defn todo-items-list [] 107 | (let [todo-item-ids (rf/subscribe [:todo-item-ids])] 108 | ($ :ul.todo-list 109 | (vw/for-fragment* todo-item-ids identity 110 | (fn [todo-item-id] 111 | ($ todo-item todo-item-id)))))) 112 | 113 | (defn todo-items-count [] 114 | (let [active-todo-item-count (rf/subscribe [:active-todo-items-count])] 115 | ($ :span.todo-count 116 | ($ :strong 117 | (sr/create-derived (fn [] 118 | (str @active-todo-item-count 119 | (if (= @active-todo-item-count 1) " item " " items ") 120 | "left"))))))) 121 | 122 | (defn todo-items-filters [] 123 | (let [display-type (rf/subscribe [:comp/display-type])] 124 | ($ :ul.filters 125 | ($ :li ($ :a 126 | (vw/props-effect 127 | (fn [] 128 | {:class [(when (= @display-type :all) "selected")]})) 129 | {:href (rtfe/href :page/all-todo-items)} 130 | "All")) 131 | ($ :li ($ :a 132 | (vw/props-effect 133 | (fn [] 134 | {:class [(when (= @display-type :active) "selected")]})) 135 | {:href (rtfe/href :page/active-todo-items)} 136 | "Active")) 137 | ($ :li ($ :a 138 | (vw/props-effect 139 | (fn [] 140 | {:class [(when (= @display-type :completed) "selected")]})) 141 | {:href (rtfe/href :page/completed-todo-items)} 142 | "Completed"))))) 143 | 144 | (defn clear-completed-button [] 145 | (let [complemented-todo-items-to-clear (rf/subscribe [:complemented-todo-items-to-clear])] 146 | ($ :button.clear-completed 147 | {:on/click (fn [_] 148 | (rf/dispatch [:clean-completed-todo-items]))} 149 | (vw/props-effect 150 | (fn [] 151 | {:style {:display (if @complemented-todo-items-to-clear "inline" "none")}})) 152 | "Clear completed"))) 153 | 154 | (defn main-page [] 155 | (let [any-todo-item (rf/subscribe [:comp.input/any-todo-item])] 156 | ($ :div 157 | ($ :section.todoapp 158 | ($ :header.header 159 | ($ :h1 "todos") 160 | ($ todo-item-creation-input)) 161 | ($ :div (vw/props-effect 162 | (fn [] 163 | {:style {:display (if @any-todo-item "inline" "none")}})) 164 | ($ :section.main 165 | ($ toggle-items-button) 166 | ($ todo-items-list)) 167 | ($ :footer.footer 168 | ($ todo-items-count) 169 | ($ todo-items-filters) 170 | ($ clear-completed-button)))) 171 | ($ :footer.info 172 | ($ :p "Double-click to edit a todo"))))) 173 | -------------------------------------------------------------------------------- /example/todomvc/public/css/index.css: -------------------------------------------------------------------------------- 1 | @charset 'utf-8'; 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | } 8 | 9 | button { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | background: none; 14 | font-size: 100%; 15 | vertical-align: baseline; 16 | font-family: inherit; 17 | font-weight: inherit; 18 | color: inherit; 19 | -webkit-appearance: none; 20 | appearance: none; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | body { 26 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 27 | line-height: 1.4em; 28 | background: #f5f5f5; 29 | color: #111111; 30 | min-width: 230px; 31 | max-width: 550px; 32 | margin: 0 auto; 33 | -webkit-font-smoothing: antialiased; 34 | -moz-osx-font-smoothing: grayscale; 35 | font-weight: 300; 36 | } 37 | 38 | .hidden { 39 | display: none; 40 | } 41 | 42 | .todoapp { 43 | background: #fff; 44 | margin: 130px 0 40px 0; 45 | position: relative; 46 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 47 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 48 | } 49 | 50 | .todoapp input::-webkit-input-placeholder { 51 | font-style: italic; 52 | font-weight: 400; 53 | color: rgba(0, 0, 0, 0.4); 54 | } 55 | 56 | .todoapp input::-moz-placeholder { 57 | font-style: italic; 58 | font-weight: 400; 59 | color: rgba(0, 0, 0, 0.4); 60 | } 61 | 62 | .todoapp input::input-placeholder { 63 | font-style: italic; 64 | font-weight: 400; 65 | color: rgba(0, 0, 0, 0.4); 66 | } 67 | 68 | .todoapp h1 { 69 | position: absolute; 70 | top: -140px; 71 | width: 100%; 72 | font-size: 80px; 73 | font-weight: 200; 74 | text-align: center; 75 | color: #b83f45; 76 | -webkit-text-rendering: optimizeLegibility; 77 | -moz-text-rendering: optimizeLegibility; 78 | text-rendering: optimizeLegibility; 79 | } 80 | 81 | .new-todo, 82 | .edit { 83 | position: relative; 84 | margin: 0; 85 | width: 100%; 86 | font-size: 24px; 87 | font-family: inherit; 88 | font-weight: inherit; 89 | line-height: 1.4em; 90 | color: inherit; 91 | padding: 6px; 92 | border: 1px solid #999; 93 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 94 | box-sizing: border-box; 95 | -webkit-font-smoothing: antialiased; 96 | -moz-osx-font-smoothing: grayscale; 97 | } 98 | 99 | .new-todo { 100 | padding: 16px 16px 16px 60px; 101 | height: 65px; 102 | border: none; 103 | background: rgba(0, 0, 0, 0.003); 104 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 105 | } 106 | 107 | .main { 108 | position: relative; 109 | z-index: 2; 110 | border-top: 1px solid #e6e6e6; 111 | } 112 | 113 | .toggle-all { 114 | width: 1px; 115 | height: 1px; 116 | border: none; /* Mobile Safari */ 117 | opacity: 0; 118 | position: absolute; 119 | right: 100%; 120 | bottom: 100%; 121 | } 122 | 123 | .toggle-all + label { 124 | display: flex; 125 | align-items: center; 126 | justify-content: center; 127 | width: 45px; 128 | height: 65px; 129 | font-size: 0; 130 | position: absolute; 131 | top: -65px; 132 | left: -0; 133 | } 134 | 135 | .toggle-all + label:before { 136 | content: '❯'; 137 | display: inline-block; 138 | font-size: 22px; 139 | color: #949494; 140 | padding: 10px 27px 10px 27px; 141 | -webkit-transform: rotate(90deg); 142 | transform: rotate(90deg); 143 | } 144 | 145 | .toggle-all:checked + label:before { 146 | color: #484848; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: calc(100% - 43px); 173 | padding: 12px 16px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle { 196 | opacity: 0; 197 | } 198 | 199 | .todo-list li .toggle + label { 200 | /* 201 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 202 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 203 | */ 204 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 205 | background-repeat: no-repeat; 206 | background-position: center left; 207 | } 208 | 209 | .todo-list li .toggle:checked + label { 210 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); 211 | } 212 | 213 | .todo-list li label { 214 | overflow-wrap: break-word; 215 | padding: 15px 15px 15px 60px; 216 | display: block; 217 | line-height: 1.2; 218 | transition: color 0.4s; 219 | font-weight: 400; 220 | color: #484848; 221 | } 222 | 223 | .todo-list li.completed label { 224 | color: #949494; 225 | text-decoration: line-through; 226 | } 227 | 228 | .todo-list li .destroy { 229 | display: none; 230 | position: absolute; 231 | top: 0; 232 | right: 10px; 233 | bottom: 0; 234 | width: 40px; 235 | height: 40px; 236 | margin: auto 0; 237 | font-size: 30px; 238 | color: #949494; 239 | transition: color 0.2s ease-out; 240 | } 241 | 242 | .todo-list li .destroy:hover, 243 | .todo-list li .destroy:focus { 244 | color: #C18585; 245 | } 246 | 247 | .todo-list li .destroy:after { 248 | content: '×'; 249 | display: block; 250 | height: 100%; 251 | line-height: 1.1; 252 | } 253 | 254 | .todo-list li:hover .destroy { 255 | display: block; 256 | } 257 | 258 | .todo-list li .edit { 259 | display: none; 260 | } 261 | 262 | .todo-list li.editing:last-child { 263 | margin-bottom: -1px; 264 | } 265 | 266 | .footer { 267 | padding: 10px 15px; 268 | height: 20px; 269 | text-align: center; 270 | font-size: 15px; 271 | border-top: 1px solid #e6e6e6; 272 | } 273 | 274 | .footer:before { 275 | content: ''; 276 | position: absolute; 277 | right: 0; 278 | bottom: 0; 279 | left: 0; 280 | height: 50px; 281 | overflow: hidden; 282 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 283 | 0 8px 0 -3px #f6f6f6, 284 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 285 | 0 16px 0 -6px #f6f6f6, 286 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 287 | } 288 | 289 | .todo-count { 290 | float: left; 291 | text-align: left; 292 | } 293 | 294 | .todo-count strong { 295 | font-weight: 300; 296 | } 297 | 298 | .filters { 299 | margin: 0; 300 | padding: 0; 301 | list-style: none; 302 | position: absolute; 303 | right: 0; 304 | left: 0; 305 | } 306 | 307 | .filters li { 308 | display: inline; 309 | } 310 | 311 | .filters li a { 312 | color: inherit; 313 | margin: 3px; 314 | padding: 3px 7px; 315 | text-decoration: none; 316 | border: 1px solid transparent; 317 | border-radius: 3px; 318 | } 319 | 320 | .filters li a:hover { 321 | border-color: #DB7676; 322 | } 323 | 324 | .filters li a.selected { 325 | border-color: #CE4646; 326 | } 327 | 328 | .clear-completed, 329 | html .clear-completed:active { 330 | float: right; 331 | position: relative; 332 | line-height: 19px; 333 | text-decoration: none; 334 | cursor: pointer; 335 | } 336 | 337 | .clear-completed:hover { 338 | text-decoration: underline; 339 | } 340 | 341 | .info { 342 | margin: 65px auto 0; 343 | color: #4d4d4d; 344 | font-size: 11px; 345 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 346 | text-align: center; 347 | } 348 | 349 | .info p { 350 | line-height: 1; 351 | } 352 | 353 | .info a { 354 | color: inherit; 355 | text-decoration: none; 356 | font-weight: 400; 357 | } 358 | 359 | .info a:hover { 360 | text-decoration: underline; 361 | } 362 | 363 | /* 364 | Hack to remove background from Mobile Safari. 365 | Can't use it globally since it destroys checkboxes in Firefox 366 | */ 367 | @media screen and (-webkit-min-device-pixel-ratio:0) { 368 | .toggle-all, 369 | .todo-list li .toggle { 370 | background: none; 371 | } 372 | 373 | .todo-list li .toggle { 374 | height: 40px; 375 | } 376 | } 377 | 378 | @media (max-width: 430px) { 379 | .footer { 380 | height: 50px; 381 | } 382 | 383 | .filters { 384 | bottom: 10px; 385 | } 386 | } 387 | 388 | :focus, 389 | .toggle:focus + label, 390 | .toggle-all:focus + label { 391 | box-shadow: 0 0 2px 2px #CF7D7D; 392 | outline: 0; 393 | } 394 | -------------------------------------------------------------------------------- /src/vrac/dsl/macro.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc vrac.dsl.macro 2 | (:refer-clojure :exclude [destructure]) 3 | (:require [mate.core :as mc])) 4 | 5 | (defn thread-first [[_ x & forms]] 6 | (loop [x x 7 | forms (seq forms)] 8 | (if (nil? forms) 9 | x 10 | (let [form (first forms)] 11 | (recur (if (seq? form) 12 | (-> `(~(first form) ~x ~@(next form)) 13 | (with-meta (meta form))) 14 | `(~form ~x)) 15 | (next forms)))))) 16 | 17 | (defn thread-last [[_ x & forms]] 18 | (loop [x x 19 | forms (seq forms)] 20 | (if (nil? forms) 21 | x 22 | (let [form (first forms)] 23 | (recur (if (seq? form) 24 | (-> `(~@form ~x) 25 | (with-meta (meta form))) 26 | `(~form ~x)) 27 | (next forms)))))) 28 | 29 | (defn thread-as [[_ expr name & forms]] 30 | `(let [~name ~expr 31 | ~@(interleave (repeat name) (butlast forms))] 32 | ~(if (empty? forms) 33 | name 34 | (last forms)))) 35 | 36 | (defn- destructure [bindings] 37 | (let [pairs (partition 2 bindings)] 38 | (if (every? (mc/comp-> first symbol?) pairs) 39 | bindings 40 | (let [destruct (fn destruct [[k-form v-form]] 41 | (cond 42 | ;; Sequential Destructuring 43 | (vector? k-form) 44 | (let [[k-form v-symb] (if (= (-> k-form pop peek) :as) 45 | [(-> k-form pop pop) (peek k-form)] 46 | [k-form (gensym "vec__")]) 47 | [k-form &-symb] (if (= (-> k-form pop peek) '&) 48 | [(-> k-form pop pop) (peek k-form)] 49 | [k-form nil])] 50 | (-> (into [v-symb v-form] 51 | (mc/mapcat-indexed (fn [index k] 52 | (if (simple-symbol? k) 53 | [k `(nth ~v-symb ~index)] 54 | ;; map or vector 55 | (destruct [k `(nth ~v-symb ~index)])))) 56 | k-form) 57 | (cond-> 58 | (some? &-symb) 59 | (conj &-symb `(drop ~(count k-form) ~v-symb))))) 60 | 61 | ;; Associative Destructuring 62 | (map? k-form) 63 | (let [v-symb (or (:as k-form) (gensym "map__")) 64 | or-hashmap (:or k-form)] 65 | (into [v-symb v-form] 66 | (mapcat (fn [[k v]] 67 | (cond 68 | (contains? #{:as :or} k) 69 | nil 70 | 71 | (= :& k) 72 | (let [kws (->> k-form 73 | (mapcat (fn [[k v]] 74 | (cond 75 | (simple-symbol? k) 76 | [v] 77 | 78 | (and (keyword? k) 79 | (= (name k) "keys")) 80 | (->> v 81 | (mapv (fn [s] 82 | (keyword (or (namespace s) (namespace k)) 83 | (name s)))))))) 84 | distinct)] 85 | [v `(dissoc ~v-symb ~@kws)]) 86 | 87 | (and (keyword? k) 88 | (= (name k) "keys")) 89 | (let [k-ns (namespace k) 90 | symbols v] 91 | (into [] 92 | (mapcat (fn [s] 93 | (let [s-name (name s) 94 | s-simple (symbol s-name) 95 | s-ns (namespace s) 96 | s-keyword (keyword (or s-ns k-ns) s-name) 97 | default (when (contains? or-hashmap s-simple) 98 | [(or-hashmap s-simple)])] 99 | [s-simple `(~s-keyword ~v-symb ~@default)]))) 100 | symbols)) 101 | 102 | (simple-symbol? k) 103 | (let [default (when (contains? or-hashmap k) 104 | [(or-hashmap k)])] 105 | [k `(~v ~v-symb ~@default)]) 106 | 107 | :else ;; map or vector 108 | (destruct [k `(~v ~v-symb)])))) 109 | k-form)) 110 | 111 | :else ;; No destructuring 112 | [k-form v-form]))] 113 | (into [] (mapcat destruct) pairs))))) 114 | 115 | (defn expand-let [original-form] 116 | (let [[_let bindings & bodies] original-form 117 | destructured-bindings (destructure bindings) 118 | has-multiple-bodies (> (count bodies) 1) 119 | body (if has-multiple-bodies 120 | `(~'do ~@bodies) 121 | (first bodies))] 122 | (if (and (identical? destructured-bindings bindings) 123 | (not has-multiple-bodies)) 124 | original-form 125 | `(~'let ~destructured-bindings ~body)))) 126 | 127 | (defn expand-for [original-form] 128 | (let [[_for bindings body] original-form 129 | new-bindings (->> (partition 2 bindings) 130 | (mapcat (fn [[k v]] 131 | (case k 132 | :let [:let (destructure v)] 133 | :when [:when v] 134 | :while [:while v] 135 | (if (simple-symbol? k) 136 | [k v] 137 | (let [item-symb (gensym "item__")] 138 | [item-symb v 139 | :let (destructure [k item-symb])]))))) 140 | vec)] 141 | (if (and (= (count new-bindings) (count bindings)) 142 | (->> (map vector new-bindings bindings) 143 | (every? (fn [[new-x x]] (identical? new-x x))))) 144 | original-form 145 | `(~'for ~new-bindings ~body)))) 146 | 147 | (defn expand-when [original-form] 148 | (let [[_when condition & bodies] original-form 149 | has-multiple-bodies (> (count bodies) 1) 150 | body (if has-multiple-bodies 151 | `(~'do ~@bodies) 152 | (first bodies))] 153 | (if (not has-multiple-bodies) 154 | original-form 155 | `(~'when ~condition ~body)))) 156 | 157 | (defn expand-fn [original-form] 158 | (let [[_fn fn-name params & bodies] (if (symbol? (second original-form)) ; is fn-name defined? 159 | original-form 160 | (list* 'fn nil (next original-form))) 161 | has-multiple-bodies (> (count bodies) 1) 162 | body (if has-multiple-bodies 163 | `(~'do ~@bodies) 164 | (first bodies))] 165 | ;; TODO: add support for the params destructuring 166 | (if (not has-multiple-bodies) 167 | original-form 168 | `(~'fn ~@(when (some? fn-name) [fn-name]) ~params ~body)))) 169 | 170 | (defn expand-defn [original-form] 171 | (let [[_defn fn-name params & bodies] original-form 172 | has-multiple-bodies (> (count bodies) 1) 173 | body (if has-multiple-bodies 174 | `(~'do ~@bodies) 175 | (first bodies))] 176 | ;; TODO: add support for the params destructuring 177 | (if (not has-multiple-bodies) 178 | original-form 179 | `(~'defn ~fn-name ~params ~body)))) 180 | 181 | (def default-macros 182 | {'-> thread-first 183 | '->> thread-last 184 | 'as-> thread-as 185 | 'let expand-let 186 | 'for expand-for 187 | 'when expand-when 188 | 'fn expand-fn 189 | 'defn expand-defn}) 190 | -------------------------------------------------------------------------------- /test/vrac/dsl/parser_test.cljc: -------------------------------------------------------------------------------- 1 | (ns vrac.dsl.parser-test 2 | (:require [clojure.test :refer [deftest testing is are]] 3 | [vrac.dsl :as dsl] 4 | [vrac.dsl.macro :as macro] 5 | [vrac.dsl.parser :as sut])) 6 | 7 | ;; If we want to test it on CLJS, we need to go through a macro 8 | ;; in order to get the env to be used in the `resolve` function. 9 | #?(:clj 10 | (deftest resolve-and-macro-expand-dsl-test 11 | (testing "expansion of the -> macro and symbol resolution in `let`" 12 | (is (= '(let [a 1] 13 | (clojure.core/inc a)) 14 | (sut/resolve-and-macro-expand-dsl '(let [a 1] 15 | (-> a 16 | inc)) 17 | (sut/symbol-resolver *ns* nil nil) 18 | macro/default-macros)))))) 19 | 20 | (deftest expand-dsl-test 21 | (testing "expansion of the -> macro and symbol resolution in `let`" 22 | (is (= '(let [a 1] 23 | (clojure.core/inc a)) 24 | (sut/expand-dsl 25 | (let [a 1] 26 | (-> a 27 | inc)))))) 28 | 29 | (testing "destructuring in `for`" 30 | (is (= '(for [a [1 2 3] 31 | :let [b (clojure.core/+ a 100) 32 | m {:x a 33 | :y b} 34 | x (:x m) 35 | y (:y m)] 36 | :when (clojure.core/< a b x y)] 37 | [a b x y]) 38 | (sut/expand-dsl 39 | (for [a [1 2 3] 40 | :let [b (+ a 100) 41 | {:keys [x y] :as m} {:x a 42 | :y b}] 43 | :when (< a b x y)] 44 | [a b x y]))))) 45 | 46 | (testing "quoted expressions" 47 | (is (= '(let [a 1 48 | b 'x 49 | c ''y] 50 | 'd) 51 | (sut/expand-dsl 52 | (let [a 1 53 | b 'x 54 | c ''y] 55 | 'd))))) 56 | 57 | (testing "a signal" 58 | (is (= '(vrac.dsl/signal 1) 59 | (sut/expand-dsl 60 | (dsl/signal 1))))) 61 | 62 | (testing "a signal inside a signal" 63 | (is (= '(vrac.dsl/signal (vrac.dsl/signal 1)) 64 | (sut/expand-dsl 65 | (dsl/signal (dsl/signal 1)))))) 66 | 67 | (testing "a signal inside a +" 68 | (is (= '(clojure.core/+ (vrac.dsl/signal 1)) 69 | (sut/expand-dsl 70 | (+ (dsl/signal 1)))))) 71 | 72 | (testing "1 signal inside a let body" 73 | (is (= '(let [] 74 | (vrac.dsl/signal 1)) 75 | (sut/expand-dsl 76 | (let [] 77 | (dsl/signal 1)))))) 78 | 79 | (testing "1 signal inside a let binding" 80 | (is (= '(let [a (vrac.dsl/signal 1)] 81 | a) 82 | (sut/expand-dsl 83 | (let [a (dsl/signal 1)] 84 | a))))) 85 | 86 | (testing "effect" 87 | (is (= '(let [a 1 88 | b 2] 89 | (vrac.dsl/effect 90 | (clojure.core/prn (clojure.core/+ a b)))) 91 | (sut/expand-dsl 92 | (let [a 1 93 | b 2] 94 | (dsl/effect 95 | (prn (+ a b)))))))) 96 | 97 | (testing "effect-on" 98 | (is (= '(let [a 1 99 | b 2] 100 | (vrac.dsl/effect-on [a (clojure.core/even? b)] 101 | (clojure.core/prn (clojure.core/+ a b)))) 102 | (sut/expand-dsl 103 | (let [a 1 104 | b 2] 105 | (dsl/effect-on [a (even? b)] 106 | (prn (+ a b)))))))) 107 | 108 | (testing "defn" 109 | (let [expanded-dsl (sut/expand-dsl 110 | (defn foo [a ^bar b] 111 | a))] 112 | (is (= '(defn foo [a b] 113 | a) 114 | expanded-dsl)) 115 | (testing "the metadata is preserved" 116 | (is (= {:tag 'bar} 117 | (-> expanded-dsl 118 | (nth 2) 119 | (nth 1) 120 | meta))))))) 121 | 122 | (deftest dsl->ast-test 123 | (are [expected-ast dsl] 124 | (= expected-ast (sut/dsl->ast dsl)) 125 | 126 | {:node-type :clj/var 127 | :symbol 'a} 128 | 'a 129 | 130 | {:node-type :clj/value 131 | :value ''a} 132 | ''a 133 | 134 | {:node-type :clj/let 135 | :bindings [{:node-type :clj/let-binding 136 | :symbol 'a 137 | :value {:node-type :clj/value 138 | :value 1}}] 139 | :body {:node-type :clj/var 140 | :symbol 'a}} 141 | '(let [a 1] 142 | a) 143 | 144 | {:node-type :clj/let 145 | :bindings [{:node-type :clj/let-binding 146 | :symbol 'a 147 | :value {:node-type :clj/value 148 | :value 1}} 149 | {:node-type :clj/let-binding 150 | :symbol 'b 151 | :value {:node-type :clj/value 152 | :value ''x}} 153 | {:node-type :clj/let-binding 154 | :symbol 'c 155 | :value {:node-type :clj/value 156 | :value '''y}}] 157 | :body {:node-type :clj/value 158 | :value ''d}} 159 | '(let [a 1 160 | b 'x 161 | c ''y] 162 | 'd) 163 | 164 | {:node-type :dsl/signal 165 | :body {:node-type :clj/value 166 | :value 1}} 167 | '(vrac.dsl/signal 1) 168 | 169 | {:node-type :dsl/state 170 | :body {:node-type :clj/value 171 | :value 1}} 172 | '(vrac.dsl/state 1) 173 | 174 | {:node-type :clj/invocation 175 | :function {:node-type :clj/var 176 | :symbol 'clojure.core/+} 177 | :args [{:node-type :clj/var 178 | :symbol 'a} 179 | {:node-type :clj/var 180 | :symbol 'b}]} 181 | '(clojure.core/+ a b) 182 | 183 | {:node-type :dsl/once 184 | :body {:node-type :clj/invocation 185 | :function {:node-type :clj/var 186 | :symbol 'clojure.core/+} 187 | :args [{:node-type :clj/var 188 | :symbol 'a} 189 | {:node-type :clj/var 190 | :symbol 'b}]}} 191 | '(vrac.dsl/once (clojure.core/+ a b)) 192 | 193 | {:node-type :dsl/memo 194 | :body {:node-type :clj/invocation 195 | :function {:node-type :clj/var 196 | :symbol 'clojure.core/+} 197 | :args [{:node-type :clj/var 198 | :symbol 'a} 199 | {:node-type :clj/var 200 | :symbol 'b}]}} 201 | '(vrac.dsl/memo (clojure.core/+ a b)) 202 | 203 | {:node-type :clj/do 204 | :bodies [{:node-type :clj/invocation 205 | :function {:node-type :clj/var 206 | :symbol 'clojure.core/prn} 207 | :args [{:node-type :clj/var 208 | :symbol 'a}]} 209 | {:node-type :clj/var 210 | :symbol 'a}]} 211 | '(do (clojure.core/prn a) 212 | a) 213 | 214 | {:node-type :clj/if 215 | :cond {:node-type :clj/value 216 | :value true} 217 | :then {:node-type :clj/value 218 | :value 10} 219 | :else {:node-type :clj/value 220 | :value 20}} 221 | '(if true 10 20) 222 | 223 | {:node-type :clj/when 224 | :cond {:node-type :clj/value 225 | :value true} 226 | :body {:node-type :clj/value 227 | :value 30}} 228 | '(when true 30) 229 | 230 | {:node-type :clj/for 231 | :bindings [{:node-type :clj/for-iteration 232 | :symbol 'i 233 | :value {:node-type :clj/vector 234 | :items [{:node-type :clj/value 235 | :value 1} 236 | {:node-type :clj/value 237 | :value 2} 238 | {:node-type :clj/value 239 | :value 3}]}} 240 | {:node-type :clj/for-let 241 | :bindings [{:node-type :clj/let-binding 242 | :symbol 'j 243 | :value {:node-type :clj/value 244 | :value 4}}]} 245 | {:node-type :clj/for-when 246 | :cond {:node-type :clj/invocation 247 | :function {:node-type :clj/var 248 | :symbol 'clojure.core/>} 249 | :args [{:node-type :clj/var 250 | :symbol 'a} 251 | {:node-type :clj/value 252 | :value 1}]}} 253 | {:node-type :clj/for-while 254 | :cond {:node-type :clj/invocation 255 | :function {:node-type :clj/var 256 | :symbol 'clojure.core/>} 257 | :args [{:node-type :clj/var 258 | :symbol 'b} 259 | {:node-type :clj/value 260 | :value 2}]}}] 261 | :body {:node-type :clj/value 262 | :value 10}} 263 | '(for [i [1 2 3] 264 | :let [j 4] 265 | :when (clojure.core/> a 1) 266 | :while (clojure.core/> b 2)] 267 | 10) 268 | 269 | {:node-type :dsl/effect 270 | :body {:node-type :clj/invocation 271 | :function {:node-type :clj/var 272 | :symbol 'clojure.core/prn} 273 | :args [{:node-type :clj/invocation 274 | :function {:node-type :clj/var 275 | :symbol 'clojure.core/+} 276 | :args [{:node-type :clj/var 277 | :symbol 'a} 278 | {:node-type :clj/var 279 | :symbol 'b}]}]}} 280 | '(vrac.dsl/effect 281 | (clojure.core/prn (clojure.core/+ a b))) 282 | 283 | {:node-type :dsl/effect-on 284 | :triggers [{:node-type :clj/var 285 | :symbol 'a} 286 | {:node-type :clj/invocation 287 | :function {:node-type :clj/var 288 | :symbol 'clojure.core/even?} 289 | :args [{:node-type :clj/var 290 | :symbol 'b}]}] 291 | :body {:node-type :clj/invocation 292 | :function {:node-type :clj/var 293 | :symbol 'clojure.core/prn} 294 | :args [{:node-type :clj/invocation 295 | :function {:node-type :clj/var 296 | :symbol 'clojure.core/+} 297 | :args [{:node-type :clj/var 298 | :symbol 'a} 299 | {:node-type :clj/var 300 | :symbol 'b}]}]}} 301 | '(vrac.dsl/effect-on [a (clojure.core/even? b)] 302 | (clojure.core/prn (clojure.core/+ a b))) 303 | 304 | {:node-type :clj/defn 305 | :fn-name 'foo 306 | :params [{:node-type :clj/fn-param 307 | :symbol 'a} 308 | {:node-type :clj/fn-param 309 | :metadata {:tag 'bar} 310 | :symbol 'b}] 311 | :body {:node-type :clj/var 312 | :symbol 'a}} 313 | 314 | '(defn foo [a ^bar b] 315 | a))) 316 | 317 | (deftest ast->dsl-test 318 | (are [expected-dsl dsl] 319 | (= expected-dsl (-> dsl sut/dsl->ast sut/ast->dsl)) 320 | 321 | '(fn [a b] a) 322 | '(fn [a b] a) 323 | 324 | '(fn foo [a b] a) 325 | '(fn foo [a b] a) 326 | 327 | '(defn foo [a b] a) 328 | '(defn foo [a b] a) 329 | 330 | '(let [a 1, b 2] a) 331 | '(let [a 1, b 2] a) 332 | 333 | '(do 1 2) 334 | '(do 1 2) 335 | 336 | '(if true 1 2) 337 | '(if true 1 2) 338 | 339 | '(when true 1) 340 | '(if true 1) 341 | 342 | '(when true 1) 343 | '(when true 1) 344 | 345 | '(for [x [1 2] 346 | :let [a 3, b 4] 347 | :when (clojure.core/even? a) 348 | :while (clojure.core/< a 10)] 349 | a) 350 | '(for [x [1 2] 351 | :let [a 3, b 4] 352 | :when (clojure.core/even? a) 353 | :while (clojure.core/< a 10)] 354 | a) 355 | 356 | '(clojure.core/+ 1 2) 357 | '(clojure.core/+ 1 2) 358 | 359 | '#{1 2} 360 | '#{1 2} 361 | 362 | '[1 2] 363 | '[1 2] 364 | 365 | '{:a 1, :b 2} 366 | '{:a 1, :b 2} 367 | 368 | `dsl/global 369 | `dsl/global 370 | 371 | `dsl/context 372 | `dsl/context 373 | 374 | ;;`(dsl/with-context {:a 1}) 375 | ;;`(dsl/with-context {:a 1}) 376 | 377 | `(dsl/once 1) 378 | `(dsl/once 1) 379 | 380 | `(dsl/signal 1) 381 | `(dsl/signal 1) 382 | 383 | `(dsl/state 1) 384 | `(dsl/state 1) 385 | 386 | `(dsl/memo (clojure.core/+ ~'a 1)) 387 | `(dsl/memo (clojure.core/+ ~'a 1)) 388 | 389 | `(dsl/effect (clojure.core/prn 1)) 390 | `(dsl/effect (clojure.core/prn 1)) 391 | 392 | `(dsl/effect-on ~'[a] 1) 393 | `(dsl/effect-on ~'[a] 1))) 394 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /src/vrac/dsl/parser.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc vrac.dsl.parser 2 | #?(:cljs (:require-macros [vrac.dsl.parser])) 3 | (:require [mate.core :as mc] 4 | [vrac.dsl :as dsl] 5 | [vrac.dsl.macro :as macro])) 6 | 7 | ;; Those are not functions or vars, they can't be resolved, so 8 | ;; we treat them as special cases which resolve to themselves. 9 | (def ^:private clj-reserved-symbols 10 | #{'do 11 | 'if 12 | 'when 13 | 'quote}) 14 | 15 | (defn- rename-symb-ns [x rename-map] 16 | (if (symbol? x) 17 | (let [x-ns (namespace x) 18 | x-name (name x) 19 | x-name (rename-map x-name x-name)] 20 | (symbol x-ns x-name)) 21 | x)) 22 | 23 | (defn symbol-resolver [ns env symb-ns-rename-map] 24 | (fn [x] 25 | #?(:clj 26 | (when-some [resolved-x-var (ns-resolve ns env x)] 27 | (cond-> (symbol resolved-x-var) 28 | (some? symb-ns-rename-map) (rename-symb-ns symb-ns-rename-map)))))) 29 | 30 | (defn resolve-and-macro-expand-dsl 31 | "Macro-expand and resolve the symbols in the DSL." 32 | [x resolve-symbol macros] 33 | (let [resolve-var (fn [x local-vars] 34 | (or (when (contains? clj-reserved-symbols x) x) 35 | (when (contains? macros x) x) 36 | (when (contains? local-vars x) x) 37 | (resolve-symbol x) 38 | (symbol (namespace x) (str (name x) "-not-found")))) 39 | resolve-and-expand (fn resolve-and-expand [x local-vars] 40 | (cond 41 | (seq? x) 42 | (if (zero? (count x)) 43 | x 44 | (let [[f & args] x 45 | expanded-f (resolve-and-expand f local-vars) 46 | x (cons expanded-f args) 47 | macro-fn (get macros expanded-f) 48 | expanded-form (if (nil? macro-fn) 49 | x 50 | (macro-fn x))] 51 | (if-not (identical? x expanded-form) 52 | (recur expanded-form local-vars) 53 | (let [[f & args] x] 54 | ;;(prn [:expanded x]) 55 | (cond 56 | ;; (defn ,,, [,,,] ,,,) 57 | (= f 'defn) 58 | (let [[fn-name params body] args 59 | local-vars (into local-vars params) 60 | body (resolve-and-expand body local-vars)] 61 | `(~'defn ~fn-name ~params ~body)) 62 | 63 | ;; (let [,,,] ,,,) 64 | (= f 'let) 65 | (let [[bindings body] args 66 | [bindings local-vars] (reduce (fn [[bindings local-vars] [symbol-form value-form]] 67 | [(conj bindings symbol-form (resolve-and-expand value-form local-vars)) 68 | (conj local-vars symbol-form)]) 69 | [[] local-vars] 70 | (partition 2 bindings)) 71 | body (resolve-and-expand body local-vars)] 72 | `(~'let ~bindings ~body)) 73 | 74 | ;; (for [,,,] ,,,) 75 | (= f 'for) 76 | (let [[bindings body] args 77 | [bindings local-vars] (reduce (fn [[bindings local-vars] [symbol-form value-form]] 78 | (case symbol-form 79 | :let (let [[let-bindings local-vars] (reduce (fn [[bindings local-vars] [symbol-form value-form]] 80 | [(conj bindings symbol-form (resolve-and-expand value-form local-vars)) 81 | (conj local-vars symbol-form)]) 82 | [[] local-vars] 83 | (partition 2 value-form))] 84 | [(conj bindings :let let-bindings) 85 | local-vars]) 86 | (:when :while) [(conj bindings symbol-form (resolve-and-expand value-form local-vars)) 87 | local-vars] 88 | [(conj bindings symbol-form (resolve-and-expand value-form local-vars)) 89 | (conj local-vars symbol-form)])) 90 | [[] local-vars] 91 | (partition 2 bindings)) 92 | body (resolve-and-expand body local-vars)] 93 | `(~'for ~bindings ~body)) 94 | 95 | ;; (quote ,,,) 96 | (= f 'quote) 97 | x ; "You can't touch this" 98 | 99 | ;; generic (f a b c) 100 | :else 101 | `(~expanded-f ~@(map (mc/partial-> resolve-and-expand local-vars) args))))))) 102 | 103 | (symbol? x) 104 | (resolve-var x local-vars) 105 | 106 | (vector? x) 107 | (mapv (mc/partial-> resolve-and-expand local-vars) x) 108 | 109 | (map? x) 110 | (-> x 111 | (update-keys (mc/partial-> resolve-and-expand local-vars)) 112 | (update-vals (mc/partial-> resolve-and-expand local-vars))) 113 | 114 | (set? x) 115 | (set (map (mc/partial-> resolve-and-expand local-vars) x)) 116 | 117 | :else 118 | x))] 119 | (resolve-and-expand x #{}))) 120 | 121 | (defmacro expand-dsl [dsl-form] 122 | #?(:clj 123 | (let [ns *ns* 124 | env &env 125 | is-compiling-cljs-code (some? (:ns env)) 126 | resolve-symbol (symbol-resolver ns env (when is-compiling-cljs-code {"cljs.core" "clojure.core"}))] 127 | `'~(resolve-and-macro-expand-dsl dsl-form 128 | resolve-symbol 129 | macro/default-macros)))) 130 | 131 | (defn dsl->ast [x] 132 | "Shallow transformation from a DSL expression to an AST." 133 | (cond 134 | 135 | (seq? x) 136 | (if (zero? (count x)) 137 | {:node-type :clj/value 138 | :value ()} 139 | (let [[f & args] x] 140 | (cond 141 | ;; (defn fn-name params body) 142 | (= f 'defn) 143 | (let [[fn-name params body] args] 144 | {:node-type :clj/defn 145 | :fn-name fn-name 146 | :params (->> params 147 | (mapv (fn [symbol] 148 | (let [metadata (meta symbol)] 149 | (-> {:node-type :clj/fn-param 150 | :symbol symbol} 151 | (cond-> (seq metadata) (assoc :metadata metadata))))))) 152 | :body (dsl->ast body)}) 153 | 154 | ;; (let [,,,] ,,,) 155 | (= f 'let) 156 | (let [[bindings body] args] 157 | {:node-type :clj/let 158 | :bindings (->> bindings 159 | (partition 2) 160 | (mapv (fn [[symbol value]] 161 | {:node-type :clj/let-binding 162 | :symbol symbol 163 | :value (dsl->ast value)}))) 164 | :body (dsl->ast body)}) 165 | 166 | ;; (do ,,,) 167 | (= f 'do) 168 | (let [bodies args] 169 | {:node-type :clj/do 170 | :bodies (mapv dsl->ast bodies)}) 171 | 172 | ;; (if cond then ?else) 173 | (= f 'if) 174 | (case (count args) 175 | 2 (let [[cond then] args] 176 | {:node-type :clj/when 177 | :cond (dsl->ast cond) 178 | :body (dsl->ast then)}) 179 | 3 (let [[cond then else] args] 180 | {:node-type :clj/if 181 | :cond (dsl->ast cond) 182 | :then (dsl->ast then) 183 | :else (dsl->ast else)})) 184 | 185 | ;; (quote x) 186 | (= f 'quote) 187 | {:node-type :clj/value 188 | :value x} 189 | 190 | ;; (when cond body) 191 | (= f 'when) 192 | (let [[cond body] args] 193 | {:node-type :clj/when 194 | :cond (dsl->ast cond) 195 | :body (dsl->ast body)}) 196 | 197 | ;; (with-context ,,,) 198 | (= f `dsl/with-context) 199 | (let [[context body] args] 200 | {:node-type :dsl/with-context 201 | :context (dsl->ast context) 202 | :body (dsl->ast body)}) 203 | 204 | ;; (for [,,,] ,,,) 205 | (= f 'for) 206 | (let [[bindings body] args] 207 | {:node-type :clj/for 208 | :bindings (->> bindings 209 | (partition 2) 210 | (mapv (fn [[symbol value]] 211 | (case symbol 212 | :let {:node-type :clj/for-let 213 | :bindings (->> value 214 | (partition 2) 215 | (mapv (fn [[symbol value]] 216 | {:node-type :clj/let-binding 217 | :symbol symbol 218 | :value (dsl->ast value)})))} 219 | :when {:node-type :clj/for-when 220 | :cond (dsl->ast value)} 221 | :while {:node-type :clj/for-while 222 | :cond (dsl->ast value)} 223 | {:node-type :clj/for-iteration 224 | :symbol symbol 225 | :value (dsl->ast value)})))) 226 | :body (dsl->ast body)}) 227 | 228 | ;; signal 229 | (= f `dsl/signal) 230 | (let [[body] args] 231 | {:node-type :dsl/signal 232 | :body (dsl->ast body)}) 233 | 234 | ;; state 235 | (= f `dsl/state) 236 | (let [[body] args] 237 | {:node-type :dsl/state 238 | :body (dsl->ast body)}) 239 | 240 | ;; memo 241 | (= f `dsl/memo) 242 | (let [[body] args] 243 | {:node-type :dsl/memo 244 | :body (dsl->ast body)}) 245 | 246 | ;; once 247 | (= f `dsl/once) 248 | (let [[body] args] 249 | {:node-type :dsl/once 250 | :body (dsl->ast body)}) 251 | 252 | ;; effect 253 | (= f `dsl/effect) 254 | (let [[body] args] 255 | {:node-type :dsl/effect 256 | :body (dsl->ast body)}) 257 | 258 | ;; effect-on 259 | (= f `dsl/effect-on) 260 | (let [[triggers body] args] 261 | (assert (vector? triggers) "The triggers should be a vector literal.") 262 | {:node-type :dsl/effect-on 263 | :triggers (mapv dsl->ast triggers) 264 | :body (dsl->ast body)}) 265 | 266 | :else 267 | {:node-type :clj/invocation 268 | :function (dsl->ast f) 269 | :args (mapv dsl->ast args)}))) 270 | 271 | (set? x) 272 | {:node-type :clj/set 273 | :items (mapv dsl->ast x)} 274 | 275 | (vector? x) 276 | {:node-type :clj/vector 277 | :items (mapv dsl->ast x)} 278 | 279 | (map? x) 280 | {:node-type :clj/map 281 | :entries (->> x 282 | (mapv (fn [[k v]] 283 | {:node-type :clj/map-entry 284 | :key (dsl->ast k) 285 | :value (dsl->ast v)})))} 286 | 287 | (= x `dsl/global) 288 | {:node-type :dsl/global} 289 | 290 | (= x `dsl/context) 291 | {:node-type :dsl/context} 292 | 293 | (symbol? x) 294 | {:node-type :clj/var 295 | :symbol x} 296 | 297 | :else 298 | {:node-type :clj/value 299 | :value x})) 300 | 301 | ;; This is potentially useful for making the tests more humane. 302 | (defn ast->dsl [x] 303 | (case (:node-type x) 304 | :clj/fn 305 | `(~'fn ~@(when-some [fn-name (:fn-name x)] [fn-name]) 306 | ~(mapv ast->dsl (:params x)) 307 | ~(ast->dsl (:body x))) 308 | 309 | :clj/defn 310 | `(~'defn ~(:fn-name x) 311 | ~(mapv ast->dsl (:params x)) 312 | ~(ast->dsl (:body x))) 313 | 314 | :clj/fn-param 315 | (:symbol x) ;; TODO: add the metadata 316 | 317 | :clj/let 318 | `(~'let ~(into [] (mapcat ast->dsl) (:bindings x)) 319 | ~(ast->dsl (:body x))) 320 | 321 | :clj/let-binding 322 | [(:symbol x) (ast->dsl (:value x))] 323 | 324 | :clj/do 325 | `(~'do ~@(mapv ast->dsl (:bodies x))) 326 | 327 | :clj/if 328 | `(~'if ~(ast->dsl (:cond x)) 329 | ~(ast->dsl (:then x)) 330 | ~(ast->dsl (:else x))) 331 | 332 | :clj/when 333 | `(~'when ~(ast->dsl (:cond x)) 334 | ~(ast->dsl (:body x))) 335 | 336 | :clj/for 337 | `(~'for ~(into [] (mapcat ast->dsl) (:bindings x)) 338 | ~(ast->dsl (:body x))) 339 | 340 | :clj/for-iteration 341 | [(:symbol x) (ast->dsl (:value x))] 342 | 343 | :clj/for-let 344 | [:let (into [] (mapcat ast->dsl) (:bindings x))] 345 | 346 | :clj/for-when 347 | [:when (ast->dsl (:cond x))] 348 | 349 | :clj/for-while 350 | [:while (ast->dsl (:cond x))] 351 | 352 | :clj/invocation 353 | (list* (ast->dsl (:function x)) 354 | (map ast->dsl (:args x))) 355 | 356 | :clj/var 357 | (:symbol x) 358 | 359 | :clj/value 360 | (:value x) 361 | 362 | :clj/set 363 | (into #{} (map ast->dsl) (:items x)) 364 | 365 | :clj/vector 366 | (into [] (map ast->dsl) (:items x)) 367 | 368 | :clj/map 369 | (into {} (map ast->dsl) (:entries x)) 370 | 371 | :clj/map-entry 372 | [(ast->dsl (:key x)) (ast->dsl (:value x))] 373 | 374 | :dsl/global 375 | `dsl/global 376 | 377 | :dsl/context 378 | `dsl/context 379 | 380 | :dsl/with-context 381 | :not-implemented-yet 382 | 383 | :dsl/once 384 | `(dsl/once ~(ast->dsl (:body x))) 385 | 386 | :dsl/signal 387 | `(dsl/signal ~(ast->dsl (:body x))) 388 | 389 | :dsl/state 390 | `(dsl/state ~(ast->dsl (:body x))) 391 | 392 | :dsl/memo 393 | `(dsl/memo ~(ast->dsl (:body x))) 394 | 395 | :dsl/effect 396 | `(dsl/effect ~(ast->dsl (:body x))) 397 | 398 | :dsl/effect-on 399 | `(dsl/effect-on ~(mapv ast->dsl (:triggers x)) 400 | ~(ast->dsl (:body x))))) 401 | -------------------------------------------------------------------------------- /src/vrac/dsl/ast.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc vrac.dsl.ast 2 | (:require #?(:clj [lambdaisland.deep-diff2 :refer [diff]]) 3 | [mate.core :as mc])) 4 | 5 | ;; node-type -> child-node -> #{:one :many} 6 | (def node-type->walkable-children 7 | {:clj/fn [[:params :many] 8 | [:body :one]] 9 | :clj/defn [[:params :many] 10 | [:body :one]] 11 | :clj/fn-param {} 12 | :clj/let [[:bindings :many] 13 | [:body :one]] 14 | :clj/let-binding {:value :one} 15 | :clj/do {:bodies :many} 16 | :clj/if {:cond :one 17 | :then :one 18 | :else :one} 19 | :clj/when {:cond :one 20 | :body :one} 21 | :clj/for [[:bindings :many] 22 | [:body :one]] 23 | :clj/for-iteration {:value :one} 24 | :clj/for-let {:bindings :many} 25 | :clj/for-when {:cond :one} 26 | :clj/for-while {:cond :one} 27 | :clj/invocation {:function :one 28 | :args :many} 29 | :clj/var {} 30 | :clj/value {} 31 | :clj/set {:items :many} 32 | :clj/vector {:items :many} 33 | :clj/map {:entries :many} 34 | :clj/map-entry {:key :one 35 | :value :one} 36 | :dsl/global {} 37 | :dsl/context {} 38 | :dsl/with-context {:context :one 39 | :body :one} 40 | :dsl/once {:body :one} 41 | :dsl/signal {:body :one} 42 | :dsl/state {:body :one} 43 | :dsl/memo {:body :one} 44 | :dsl/effect {:body :one} 45 | :dsl/effect-on {:triggers :many 46 | :body :one}}) 47 | 48 | (defn walk-ast 49 | "Walks and transforms a context containing the AST." 50 | [context pre-process post-process] 51 | (let [walk (fn walk [original-context] 52 | (let [path (:path original-context) 53 | {:keys [root-ast] :as context} (pre-process original-context) 54 | ast (get-in root-ast path) 55 | field->cardinality (-> ast :node-type node-type->walkable-children) 56 | context (reduce (fn [context [field cardinality]] 57 | (case cardinality 58 | :one (-> context 59 | (assoc :path (conj path field)) 60 | walk) 61 | :many (reduce (fn [context index] 62 | (-> context 63 | (assoc :path (conj path field index)) 64 | walk)) 65 | context 66 | (-> ast (get field) count range)))) 67 | context 68 | field->cardinality)] 69 | (-> context 70 | (assoc :path path) 71 | (assoc :original-context original-context) 72 | post-process 73 | (dissoc :original-context))))] 74 | (walk context))) 75 | 76 | (defn make-context 77 | "Returns an initial context from a given AST." 78 | [ast] 79 | {:root-ast ast 80 | :path []}) 81 | 82 | (defn push-ancestor-path [context] 83 | (-> context 84 | (update :ancestor-paths conj (:path context)))) 85 | 86 | (defn pop-ancestor-path [context] 87 | (-> context 88 | (update :ancestor-paths pop))) 89 | 90 | ;; ----------------------------------- 91 | 92 | (defn- assoc-from [m-to m-from k] 93 | (-> m-to 94 | (assoc k (get m-from k)))) 95 | 96 | (defn- assoc-existing-from [m-to m-from k] 97 | (cond-> m-to 98 | (contains? m-from k) 99 | (assoc k (get m-from k)))) 100 | 101 | ;; ----------------------------------- 102 | 103 | (defn- link-vars-pre-walk 104 | "On each var node, assoc `:var.definition/path` to point where its value is defined. 105 | Assoc :var/unbound true instead if the var is unbound." 106 | [{:keys [root-ast path symbol->definition-path] :as context}] 107 | (let [ast (get-in root-ast path)] 108 | (case (:node-type ast) 109 | :clj/var 110 | (let [symbol (:symbol ast) 111 | definition-path (symbol->definition-path symbol)] 112 | (-> context 113 | (assoc-in (cons :root-ast path) 114 | (-> ast 115 | (mc/if-> (nil? definition-path) 116 | (assoc :var/unbound true) 117 | (assoc :var.definition/path definition-path)))))) 118 | 119 | ;; else 120 | context))) 121 | 122 | (defn- link-vars-post-walk 123 | "Updates a hashmap symbol->value-path as we walk the AST, to keep track of 124 | what vars are in the current scope and where they are defined." 125 | [{:keys [root-ast path original-context] :as context}] 126 | (let [ast (get-in root-ast path)] 127 | (case (:node-type ast) 128 | ;; Add a var to the hashmap when we exit a let-binding, as it becomes available 129 | ;; in the next let-binding entries and the let's body. 130 | (:clj/fn-param :clj/let-binding :clj/for-iteration) 131 | (let [symbol (:symbol ast)] 132 | (-> context 133 | ;; Curate symbol->definition-path's content 134 | (update :symbol->definition-path 135 | assoc symbol path))) 136 | 137 | ;; Restore the hashmap when we exit the body/ies of its parent let or for node. 138 | (:clj/let :clj/for) 139 | (-> context 140 | ;; Pop symbol->definition-path back to its original state 141 | (assoc-from original-context :symbol->definition-path)) 142 | 143 | ;; else 144 | context))) 145 | 146 | (defn link-vars-to-their-definition-pass 147 | "An AST pass which links the vars to their definition via `:var.value/path`." 148 | [context] 149 | (-> context 150 | (assoc :symbol->definition-path {}) ;; pass setup 151 | (walk-ast link-vars-pre-walk link-vars-post-walk) 152 | (dissoc :symbol->definition-path))) ;; pass clean up 153 | 154 | ;; ----------------------------------- 155 | 156 | (defn- find-bound-value-usages-pre-walk 157 | "Collects all the var usages from the whole AST into 158 | a hashmap `:var.value/path` -> `:var.usage/paths`." 159 | [{:keys [root-ast path] :as context}] 160 | (let [ast (get-in root-ast path)] 161 | (case (:node-type ast) 162 | :clj/var 163 | (let [definition-path (:var.definition/path ast)] 164 | (-> context 165 | (cond-> (some? definition-path) 166 | (update-in [:definition-path->usage-paths definition-path] (fnil conj []) path)))) 167 | 168 | ;; else 169 | context))) 170 | 171 | (defn- find-bound-value-usages-clean-up 172 | "From the hashmap, write down in the AST the usages of each bound value." 173 | [{:keys [root-ast] :as context}] 174 | (let [definition-path->usage-paths (:definition-path->usage-paths context)] 175 | (-> context 176 | (assoc :root-ast (reduce (fn [root-ast [value-path usage-paths]] 177 | (-> root-ast 178 | (assoc-in (conj value-path :var.usage/paths) usage-paths))) 179 | root-ast 180 | definition-path->usage-paths)) 181 | (dissoc :definition-path->usage-paths)))) 182 | 183 | ;; This pass works after `link-vars-to-their-definition-pass` 184 | (defn add-var-usage-pass 185 | "An AST pass which annotates all the var usages bi-directionally, 186 | via `:var.value/path` and `:var.usage/paths`." 187 | [context] 188 | (-> context 189 | (walk-ast find-bound-value-usages-pre-walk identity) 190 | find-bound-value-usages-clean-up)) 191 | 192 | ;; ----------------------------------- 193 | 194 | (defn- lifespan-pre-walk 195 | [{:keys [root-ast path lifespan-path] :as context}] 196 | (let [ast (get-in root-ast path) 197 | ;; The lifespan defined on the node takes priority 198 | lifespan-path (:node.lifespan/path ast lifespan-path) 199 | ;; Saves the value in the node 200 | ast (-> ast 201 | (assoc :node.lifespan/path lifespan-path)) 202 | ;; 203 | ast (case (:node-type ast) 204 | :clj/defn 205 | (-> ast 206 | ;; Sets a lifespan on all the body node 207 | (assoc-in [:body :node.lifespan/path] (conj path :body))) 208 | 209 | :clj/if 210 | (-> ast 211 | ;; Sets a lifespan on the :then and :else nodes 212 | (assoc-in [:then :node.lifespan/path] (conj path :then)) 213 | (assoc-in [:else :node.lifespan/path] (conj path :else))) 214 | 215 | :clj/when 216 | (-> ast 217 | ;; Sets a lifespan on all the body node 218 | (assoc-in [:body :node.lifespan/path] (conj path :body))) 219 | 220 | :clj/for 221 | (let [[bindings lifespan-path] (reduce (fn [[bindings lifespan] [index binding]] 222 | ;; Sets the lifespan on each binding 223 | [(conj bindings (-> binding 224 | (assoc :node.lifespan/path lifespan))) 225 | (if (= (:node-type binding) :clj/for-iteration) 226 | (conj path :bindings index :symbol) 227 | lifespan)]) 228 | [[] lifespan-path] 229 | (mc/seq-indexed (:bindings ast)))] 230 | (-> ast 231 | (assoc :bindings bindings) 232 | ;; Sets a lifespan on the :body node 233 | (assoc-in [:body :node.lifespan/path] lifespan-path))) 234 | 235 | (:dsl/effect :dsl/effect-on) 236 | (-> ast 237 | (assoc-in [:body :node.lifespan/path] (conj path :body)) 238 | (assoc-in [:body :node.lifespan/type] :non-reactive)) 239 | 240 | ;; else 241 | ast)] 242 | (-> context 243 | (assoc :lifespan-path lifespan-path) 244 | (assoc-in (cons :root-ast path) ast)))) 245 | 246 | (defn- lifespan-post-walk 247 | [{:keys [original-context] :as context}] 248 | (-> context 249 | (assoc :lifespan-path (:lifespan-path original-context)))) 250 | 251 | (defn add-lifespan-pass 252 | "An AST pass which annotates all the nodes with a :node.lifespan/path 253 | pointing to the root of the scope which has the same lifespan." 254 | [context] 255 | (-> context 256 | (assoc :lifespan-path []) ; pass setup 257 | (walk-ast lifespan-pre-walk lifespan-post-walk) 258 | (dissoc :lifespan-path))) ; pass clean up 259 | 260 | ;; ----------------------------------- 261 | 262 | (defn add-reactivity-type-pre-walk [context] 263 | context) 264 | 265 | (defn- comp-reactivities [reactivity-set] 266 | (cond 267 | (contains? reactivity-set :signal) :signal 268 | (contains? reactivity-set :memo) :memo 269 | (contains? reactivity-set :value) :value 270 | :else nil)) 271 | 272 | (defn add-reactivity-type-post-walk [{:keys [root-ast path] :as context}] 273 | (let [ast (get-in root-ast path) 274 | ast (case (:node-type ast) 275 | :dsl/signal 276 | (-> ast 277 | (assoc :reactivity/type :signal)) 278 | 279 | :dsl/state 280 | (-> ast 281 | (assoc :reactivity/type :memo)) 282 | 283 | :dsl/memo 284 | (let [body-reactivity (:reactivity/type (:body ast)) 285 | ast-node-reactivity (if (contains? #{:signal :memo} body-reactivity) 286 | :memo 287 | :value)] 288 | (-> ast 289 | (assoc :reactivity/type ast-node-reactivity))) 290 | 291 | (:dsl/once :clj/value) 292 | (-> ast 293 | (assoc :reactivity/type :value)) 294 | 295 | :clj/var 296 | (let [{:keys [var.definition/path var/unbound]} ast 297 | definition (get-in root-ast path)] 298 | (-> ast 299 | (cond-> (and (not unbound) 300 | (contains? definition :reactivity/type)) 301 | (assoc :reactivity/type (:reactivity/type definition))))) 302 | 303 | :clj/invocation 304 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) (:args ast)))] 305 | (-> ast 306 | (cond-> (some? ast-node-reactivity-type) 307 | (assoc :reactivity/type ast-node-reactivity-type)))) 308 | 309 | (:clj/set :clj/vector) 310 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) (:items ast)))] 311 | (-> ast 312 | (cond-> (some? ast-node-reactivity-type) 313 | (assoc :reactivity/type ast-node-reactivity-type)))) 314 | 315 | :clj/map 316 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) (:entries ast)))] 317 | (-> ast 318 | (cond-> (some? ast-node-reactivity-type) 319 | (assoc :reactivity/type ast-node-reactivity-type)))) 320 | 321 | :clj/map-entry 322 | (let [ast-node-reactivity-type (comp-reactivities (into #{} (map :reactivity/type) [(:key ast) (:value ast)]))] 323 | (-> ast 324 | (cond-> (some? ast-node-reactivity-type) 325 | (assoc :reactivity/type ast-node-reactivity-type)))) 326 | 327 | :clj/let 328 | (let [ast-node-reactivity-type (-> ast :body :reactivity/type)] 329 | (-> ast 330 | (cond-> (some? ast-node-reactivity-type) 331 | (assoc :reactivity/type ast-node-reactivity-type)))) 332 | 333 | :clj/let-binding 334 | (let [ast-node-reactivity-type (-> ast :value :reactivity/type)] 335 | (-> ast 336 | (cond-> (some? ast-node-reactivity-type) 337 | (assoc :reactivity/type ast-node-reactivity-type)))) 338 | 339 | :clj/for 340 | (let [ast-node-reactivity-type (-> ast :body :reactivity/type)] 341 | (-> ast 342 | (cond-> (some? ast-node-reactivity-type) 343 | (assoc :reactivity/type ast-node-reactivity-type)))) 344 | 345 | 346 | :clj/for-iteration 347 | (let [ast-node-reactivity-type (-> ast :value :reactivity/type)] 348 | (-> ast 349 | (cond-> (some? ast-node-reactivity-type) 350 | (assoc :reactivity/type ast-node-reactivity-type)))) 351 | 352 | :clj/fn-param 353 | (let [ast-node-reactivity-type (-> ast :metadata :tag 354 | {'value :value 355 | 'signal :signal 356 | 'memo :memo})] 357 | (-> ast 358 | (cond-> (some? ast-node-reactivity-type) 359 | (assoc :reactivity/type ast-node-reactivity-type)))) 360 | 361 | ;; else 362 | ast)] 363 | (-> context 364 | (assoc-in (cons :root-ast path) ast)))) 365 | 366 | (defn add-reactivity-type-pass 367 | "" 368 | [context] 369 | (-> context 370 | (walk-ast add-reactivity-type-pre-walk add-reactivity-type-post-walk))) 371 | 372 | ;; ----------------------------------- 373 | 374 | (defn- hoist-invocations-pre-walk [{:keys [root-ast path] :as context}] 375 | context) 376 | 377 | (defn- hoist-invocations-post-walk [{:keys [root-ast path] :as context}] 378 | (let [ast (get-in root-ast path) 379 | ast (case (:node-type ast) 380 | ast)] 381 | (-> context 382 | (assoc-in (cons :root-ast path) ast)))) 383 | 384 | 385 | (defn hoist-invocations-pass 386 | [context] 387 | (-> context 388 | (walk-ast hoist-invocations-pre-walk hoist-invocations-post-walk))) 389 | 390 | ;; ----------------------------------- 391 | 392 | ;; This is a template for creating a new pass. 393 | ;; Copy/paste, rename those functions, then implement 394 | 395 | #_ 396 | (defn- xxx-pre-walk [context] 397 | context) 398 | 399 | #_ 400 | (defn- xxx-post-walk [{:keys [root-ast path] :as context}] 401 | (let [ast (get-in root-ast path) 402 | ast (case (:node-type ast) 403 | #_#_ 404 | :clj/value ast 405 | 406 | ;; else 407 | ast)] 408 | (-> context 409 | (assoc-in (cons :root-ast path) ast)))) 410 | 411 | #_ 412 | (defn xxx-pass 413 | [context] 414 | (-> context 415 | (walk-ast xxx-pre-walk xxx-post-walk))) 416 | 417 | ;; ----------------------------------- 418 | 419 | #_(diff *2 *1) 420 | -------------------------------------------------------------------------------- /src/vrac/web.cljc: -------------------------------------------------------------------------------- 1 | (ns vrac.web 2 | #?(:cljs (:require-macros [vrac.web :refer [with-context 3 | with-context-update 4 | if-fragment 5 | case-fragment 6 | cond-fragment]])) 7 | (:require [clojure.string :as str] 8 | #?(:cljs [goog.object :as gobj]) 9 | [signaali.reactive :as sr])) 10 | 11 | ;; ---------------------------------------------- 12 | 13 | (def xmlns-math-ml "http://www.w3.org/1998/Math/MathML") 14 | (def xmlns-html "http://www.w3.org/1999/xhtml") 15 | (def xmlns-svg "http://www.w3.org/2000/svg") 16 | 17 | #_(def xmlns-by-kw 18 | {:math xmlns-math-ml 19 | :html xmlns-html 20 | :svg xmlns-svg}) 21 | 22 | (def ^:private ^:dynamic *xmlns-kw* :none) 23 | 24 | ;; ---------------------------------------------- 25 | 26 | (def ^:private ^:dynamic *userland-context*) 27 | 28 | (defn get-context [] 29 | *userland-context*) 30 | 31 | (defmacro with-context [new-context vcup] 32 | #?(:clj 33 | `(binding [*userland-context* ~new-context] 34 | (process-vcup ~vcup)))) 35 | 36 | (defmacro with-context-update [context-fn vcup] 37 | #?(:clj 38 | `(let [parent-context# *userland-context* 39 | new-context# (sr/create-derived (fn [] (~context-fn parent-context#)))] 40 | (with-context new-context# ~vcup)))) 41 | 42 | ;; ---------------------------------------------- 43 | 44 | (defrecord VcupNode [node-type children]) 45 | (defrecord ReactiveFragment [reactive-node]) 46 | (defrecord PropEffect [reactive+-props]) 47 | (defrecord ComponentResult [effects elements]) 48 | 49 | ;; ---------------------------------------------- 50 | 51 | #?(:cljs 52 | (defn- dom-node? [x] 53 | (instance? js/Node x))) 54 | 55 | (defn- vcup-fragment? [x] 56 | (and (instance? VcupNode x) 57 | (= (:node-type x) :<>))) 58 | 59 | #?(:cljs 60 | (defn- vcup-element? [x] 61 | (and (instance? VcupNode x) 62 | (not= (:node-type x) :<>) 63 | (or (simple-keyword? (:node-type x)) 64 | (instance? js/Element (:node-type x)))))) 65 | 66 | (defn- component-invocation? [x] 67 | (and (instance? VcupNode x) 68 | (fn? (:node-type x)))) 69 | 70 | (defn- reactive-fragment? [x] 71 | (instance? ReactiveFragment x)) 72 | 73 | (defn- prop-effect? [x] 74 | (instance? PropEffect x)) 75 | 76 | (defn- component-result? [x] 77 | (instance? ComponentResult x)) 78 | 79 | (defn- reactive-node? [x] 80 | (instance? signaali.reactive.ReactiveNode x)) 81 | 82 | (defn- prop-map? [x] 83 | (and (map? x) 84 | (not (record? x)))) 85 | 86 | (defn- props? [x] 87 | (or (prop-map? x) 88 | (prop-effect? x))) 89 | 90 | ;; ---------------------------------------------- 91 | 92 | ;;(defn component-result [& sub-results] 93 | ;; (apply merge-with into sub-results)) 94 | 95 | ;; ---------------------------------------------- 96 | 97 | (defn- ensure-coll [x] 98 | (cond-> x (not (coll? x)) vector)) 99 | 100 | (defn- parse-element-tag [s] 101 | (reduce (fn [acc part] 102 | (case (subs part 0 1) 103 | "." (update acc :classes conj (subs part 1)) 104 | "#" (assoc acc :id (subs part 1)) 105 | (assoc acc :tag-name part))) 106 | {:tag-name "div" 107 | :id nil 108 | :classes []} 109 | (re-seq #"[#.]?[^#.]+" s))) 110 | 111 | (defn- style->str [x] 112 | (cond 113 | (map? x) (->> x 114 | (map (fn [[k v]] (str (name k) ": " v))) 115 | (str/join "; ") 116 | (not-empty)) 117 | :else x)) 118 | 119 | (defn- class->str [x] 120 | (when (some? x) 121 | (->> x 122 | (ensure-coll) 123 | (flatten) 124 | (remove nil?) 125 | (map name) 126 | (str/join " ") 127 | (not-empty)))) 128 | 129 | (defn- compose-prop-maps [base new] 130 | (let [style (into (or (:style base) {}) (:style new)) 131 | class (into (or (:class base) []) (some-> (:class new) ensure-coll))] 132 | (-> base 133 | (into (dissoc new :style :class)) 134 | (cond-> (seq style) (assoc :style style)) 135 | (cond-> (seq class) (assoc :class class))))) 136 | 137 | ;; ---------------------------------------------- 138 | 139 | (defn- deref+ [x] 140 | (cond 141 | (instance? signaali.reactive.ReactiveNode x) @x 142 | (fn? x) (x) 143 | :else x)) 144 | 145 | #?(:cljs 146 | (defn- refs-effect [^js/Element element refs] 147 | (sr/create-effect 148 | (fn [] 149 | (doseq [ref refs] 150 | (reset! ref element)) 151 | (sr/on-clean-up (fn [] 152 | (doseq [ref refs] 153 | (reset! ref nil)))))))) 154 | 155 | #?(:cljs 156 | (defn- set-element-prop [xmlns-kw ^js/Element element prop-kw prop-value] 157 | (let [prop-ns (namespace prop-kw) 158 | prop-name (name prop-kw)] 159 | (cond 160 | (= prop-ns "a") 161 | (-> element (.setAttribute prop-name prop-value)) 162 | 163 | (= prop-ns "p") 164 | (-> element (gobj/set prop-name prop-value)) 165 | 166 | (= prop-ns "on") 167 | (-> element (.addEventListener prop-name prop-value)) 168 | 169 | ;; TODO: see if we could use `classList` on the element 170 | (= prop-kw :class) 171 | (if (= xmlns-kw :none) 172 | (-> element (gobj/set "className" (class->str prop-value))) 173 | (-> element (.setAttribute "class" (class->str prop-value)))) 174 | 175 | (= prop-kw :style) 176 | (if (= xmlns-kw :none) 177 | (-> element (gobj/set "style" (style->str prop-value))) 178 | (-> element (.setAttribute "style" (style->str prop-value)))) 179 | 180 | (= prop-kw :ref) 181 | nil ;; no-op 182 | 183 | (str/starts-with? prop-name "data-") 184 | (-> element (.setAttribute prop-name prop-value)) 185 | 186 | :else 187 | (let [prop-value (when-not (false? prop-value) prop-value)] 188 | (if (= xmlns-kw :none) 189 | (-> element (gobj/set prop-name prop-value)) 190 | (-> element (.setAttribute prop-name prop-value)))))))) 191 | 192 | #?(:cljs 193 | (defn- unset-element-prop [xmlns-kw ^js/Element element prop-kw prop-value] 194 | (let [prop-ns (namespace prop-kw) 195 | prop-name (name prop-kw)] 196 | (cond 197 | (= prop-ns "a") 198 | (-> element (.removeAttribute prop-name)) 199 | 200 | (= prop-ns "p") 201 | (-> element (gobj/set prop-name nil)) 202 | 203 | (= prop-ns "on") 204 | (-> element (.removeEventListener prop-name prop-value)) 205 | 206 | (= prop-kw :class) 207 | (if (= xmlns-kw :none) 208 | (-> element (gobj/set "className" nil)) 209 | (-> element (.removeAttribute "className"))) 210 | 211 | (= prop-kw :style) 212 | (if (= xmlns-kw :none) 213 | (-> element (gobj/set "style" nil)) 214 | (-> element (.removeAttribute "style"))) 215 | 216 | (= prop-kw :ref) 217 | nil ;; no-op 218 | 219 | (str/starts-with? prop-name "data-") 220 | (-> element (.removeAttribute prop-name)) 221 | 222 | :else 223 | (if (= xmlns-kw :none) 224 | (-> element (gobj/set prop-name nil)) 225 | (-> element (.removeAttribute prop-name))))))) 226 | 227 | #?(:cljs 228 | (defn- dynamic-props-effect [xmlns-kw ^js/Element element attrs] 229 | (let [attrs (->> attrs 230 | ;; Combine the consecutive prop-maps together, and 231 | ;; unwrap the reactive+-props in PropEffect values. 232 | (into [] 233 | (comp 234 | (partition-by prop-effect?) 235 | (mapcat (fn [prop-group] 236 | (if (prop-map? (first prop-group)) 237 | [(reduce compose-prop-maps {} prop-group)] 238 | (mapv :reactive+-props prop-group))))))) 239 | old-props (atom nil)] 240 | (sr/create-effect (fn [] 241 | (let [props (transduce (map deref+) compose-prop-maps {} attrs)] 242 | (doseq [[prop-kw prop-value] @old-props 243 | :when (not (contains? props prop-kw))] 244 | (unset-element-prop xmlns-kw element prop-kw prop-value)) 245 | 246 | (doseq [[prop-kw prop-value] props] 247 | (set-element-prop xmlns-kw element prop-kw prop-value)) 248 | 249 | (reset! old-props props))))))) 250 | 251 | #?(:cljs 252 | (defn- collect-dom-nodes! [^js/Array array x] 253 | (cond 254 | (instance? js/Node x) 255 | (.push array x) 256 | 257 | (vector? x) 258 | (doseq [item x] 259 | (collect-dom-nodes! array item)) 260 | 261 | (reactive-fragment? x) 262 | (doseq [item @(:reactive-node x)] 263 | (collect-dom-nodes! array item))))) 264 | 265 | 266 | #?(:cljs 267 | (defn dynamic-children-effect 268 | "Dynamically update the DOM node so that its children keep representing the elements array. 269 | The elements are either js/Node (element or text node) or a reactive node whose value is 270 | a vector of js/Node instances." 271 | [^js/Element parent-element nodes] 272 | ;; TODO: This algorithm could be improved to only replace children where it is needed. 273 | ;; Would it be faster? less CPU-intensive? 274 | ;; maybe different algorithms depending on the size? 275 | ;; measurements needed. 276 | (let [xmlns-kw *xmlns-kw* 277 | userland-context *userland-context*] 278 | (sr/create-effect (fn [] 279 | (binding [*xmlns-kw* xmlns-kw 280 | *userland-context* userland-context] 281 | (let [new-children (make-array 0)] 282 | (collect-dom-nodes! new-children nodes) 283 | (-> parent-element .-replaceChildren (.apply parent-element new-children))))))))) 284 | 285 | ;; ---------------------------------------------- 286 | 287 | #?(:cljs 288 | (defn html-text-to-dom [html-text] 289 | (let [^js/Element element (js/document.createElement "div")] 290 | (set! (.-innerHTML element) html-text) 291 | (.-firstElementChild element)))) 292 | 293 | (def ^:private inline-seq-children-xf 294 | (mapcat (fn [child] ;; Inline when child is a seq. 295 | (if (seq? child) 296 | child 297 | [child])))) 298 | 299 | (defn $ [node-type & children] 300 | (VcupNode. node-type children)) 301 | 302 | ;; ---------------------------------------------- 303 | 304 | #?(:cljs 305 | (defn process-vcup [vcup] 306 | (let [all-effects (atom []) 307 | to-dom-elements (fn to-dom-elements [vcup] 308 | (cond 309 | (nil? vcup) 310 | [] 311 | 312 | (dom-node? vcup) 313 | [vcup] 314 | 315 | ;; Reactive fragment (i.e. if-fragment and for-fragment) 316 | (reactive-fragment? vcup) 317 | [vcup] 318 | 319 | (props? vcup) 320 | (throw (js/Error. "Props cannot be at the root of a scope.")) 321 | 322 | ;; Component result (when the component is directly invoked) 323 | (component-result? vcup) 324 | (let [{:keys [effects elements]} vcup] 325 | (swap! all-effects into effects) 326 | elements) 327 | 328 | ;; Component invocation 329 | (component-invocation? vcup) 330 | (let [{component-fn :node-type 331 | args :children} vcup] 332 | (recur (apply component-fn args))) 333 | 334 | ;; Vcup fragment 335 | (vcup-fragment? vcup) 336 | (into [] 337 | (comp inline-seq-children-xf 338 | (mapcat to-dom-elements)) 339 | (:children vcup)) 340 | 341 | ;; ($ :div ,,,) 342 | (vcup-element? vcup) 343 | (let [node-type (:node-type vcup) 344 | [xmlns-kw children-xmlns-kw ^js/Element element id classes] (if (instance? js/Element node-type) 345 | ;; DOM element 346 | (let [tag-name (.-tagName node-type) 347 | [xmlns-kw children-xmlns-kw] (case (str/lower-case tag-name) 348 | "svg" [:svg :svg] 349 | "math" [:math :math] 350 | "foreignobject" [*xmlns-kw* :none] 351 | [*xmlns-kw* *xmlns-kw*])] 352 | [xmlns-kw children-xmlns-kw node-type nil nil]) 353 | ;; keywords like :div and :div#id.class1.class2 354 | (let [{:keys [tag-name id classes]} (parse-element-tag (name node-type)) 355 | [xmlns-kw children-xmlns-kw] (case tag-name 356 | "svg" [:svg :svg] 357 | "math" [:math :math] 358 | "foreignObject" [*xmlns-kw* :none] 359 | [*xmlns-kw* *xmlns-kw*]) 360 | element (case xmlns-kw 361 | :svg (js/document.createElementNS xmlns-svg tag-name) 362 | :math (js/document.createElementNS xmlns-math-ml tag-name) 363 | :none (js/document.createElement tag-name))] 364 | [xmlns-kw children-xmlns-kw element id classes])) 365 | 366 | children (:children vcup) 367 | 368 | ;; Collect all the props. 369 | props (cons (cond-> {} 370 | (some? id) (assoc :id id) 371 | (seq classes) (assoc :class classes)) 372 | (filterv props? children)) 373 | 374 | ;; Convert the children into elements. 375 | child-elements (binding [*xmlns-kw* children-xmlns-kw] 376 | (into [] 377 | (comp (remove props?) 378 | inline-seq-children-xf 379 | (mapcat to-dom-elements)) 380 | children))] 381 | ;; TODO: Can we use on-dispose instead? 382 | ;; Create an effect bound to the element's lifespan. 383 | ;; It is limited to statically declared :ref props. 384 | (let [refs (into [] 385 | (comp (filter prop-map?) 386 | (keep :ref)) 387 | props)] 388 | (when (seq refs) 389 | (swap! all-effects conj (refs-effect element refs)))) 390 | 391 | ;; Set the element's props 392 | (if (every? prop-map? props) 393 | (let [composed-prop-maps (reduce compose-prop-maps {} props)] 394 | (doseq [[prop-kw prop-value] composed-prop-maps] 395 | (set-element-prop xmlns-kw element prop-kw prop-value))) 396 | (swap! all-effects conj (dynamic-props-effect xmlns-kw element props))) 397 | 398 | ;; Set the element's children 399 | (if (every? dom-node? child-elements) 400 | (doseq [child-element child-elements] 401 | (-> element (.appendChild child-element))) 402 | (swap! all-effects conj (dynamic-children-effect element child-elements))) 403 | 404 | ;; Result 405 | [element]) 406 | 407 | (reactive-node? vcup) 408 | (let [^js/Text text-node (js/document.createTextNode "") 409 | effect (sr/create-effect (fn [] 410 | (set! (.-nodeValue text-node) @vcup)))] 411 | (swap! all-effects conj effect) 412 | [text-node]) 413 | 414 | :else 415 | [(js/document.createTextNode vcup)])) 416 | 417 | elements (to-dom-elements vcup)] 418 | (ComponentResult. @all-effects elements)))) 419 | 420 | ;; ---------------------------------------------- 421 | 422 | (defn use-effects [effects] 423 | (ComponentResult. effects nil)) 424 | 425 | (defn props-effect [reactive+-props] 426 | (PropEffect. reactive+-props)) 427 | 428 | ;; ---------------------------------------------- 429 | 430 | ;; Q: Why was it an effect in the first place? 431 | ;; A: It is an effect because it owns effects, not computations. 432 | ;; The effect of the scope-effect is to trigger those effects when it is run, 433 | ;; which makes it effectful, so it needs to be an effect too. 434 | (defn scope-effect 435 | "This effects manages a static collection of effect's lifecycle so that they are 436 | first-run and disposed when this effect is run and cleaned up." 437 | ([owned-effects] 438 | (scope-effect owned-effects nil)) 439 | ([owned-effects options] 440 | (when-some [owned-effects (seq (remove nil? owned-effects))] 441 | (let [scope (sr/create-effect (fn [] 442 | (run! sr/run-if-needed owned-effects) 443 | (sr/on-clean-up (fn [] 444 | (run! sr/dispose owned-effects)))) 445 | options)] 446 | (doseq [owned-effect owned-effects] 447 | (sr/run-after owned-effect scope)) 448 | scope)))) 449 | 450 | (defn reactive-fragment 451 | ([vcup-fn] 452 | (reactive-fragment vcup-fn {:metadata {:name "reactive-fragment"}})) 453 | ([vcup-fn options] 454 | #?(:cljs 455 | (ReactiveFragment. 456 | (sr/create-derived (fn [] 457 | (let [{:keys [effects elements]} (process-vcup (vcup-fn))] 458 | ;; scope-effect is inside the ReactiveFragment's effect because 459 | ;; we want its lifespan to be the period between 2 re-runs of this reactive node. 460 | (some-> (scope-effect effects {:dispose-on-zero-signal-watchers true}) 461 | deref) 462 | elements)) 463 | options))))) 464 | 465 | (defmacro when-fragment [reactive+-condition then-vcup-expr] 466 | #?(:clj 467 | `(let [reactive+-condition# ~reactive+-condition 468 | boolean-condition# (sr/create-memo (fn [] 469 | (boolean (deref+ reactive+-condition#)))) 470 | vcup-fn# (fn [] 471 | (when @boolean-condition# 472 | ~then-vcup-expr))] 473 | (reactive-fragment vcup-fn# {:metadata {:name "when-fragment"}})))) 474 | 475 | (defmacro if-fragment [reactive+-condition then-vcup-expr else-vcup-expr] 476 | #?(:clj 477 | `(let [reactive+-condition# ~reactive+-condition 478 | boolean-condition# (sr/create-memo (fn [] 479 | (boolean (deref+ reactive+-condition#)))) 480 | vcup-fn# (fn [] 481 | (if @boolean-condition# 482 | ~then-vcup-expr 483 | ~else-vcup-expr))] 484 | (reactive-fragment vcup-fn# {:metadata {:name "if-fragment"}})))) 485 | 486 | (defn- indexed-fragment [reactive-matched-index-or-nil 487 | clause-index->clause-vcup 488 | default-clause] 489 | #?(:cljs 490 | (reactive-fragment (fn [] 491 | (let [matched-index @reactive-matched-index-or-nil] 492 | (cond 493 | (some? matched-index) 494 | (-> matched-index clause-index->clause-vcup) 495 | 496 | (= default-clause ::undefined) 497 | (throw (js/Error. "Missing default clause in indexed-fragment.")) 498 | 499 | :else 500 | default-clause))) 501 | {:metadata {:name "index-fragment"}}))) 502 | 503 | (defn case-fragment* [reactive+-value-expr 504 | clause-value->clause-index 505 | clause-index->clause-vcup 506 | default-clause] 507 | (let [reactive-matched-index-or-nil (sr/create-memo (fn [] (-> (deref+ reactive+-value-expr) 508 | clause-value->clause-index)))] 509 | (indexed-fragment reactive-matched-index-or-nil 510 | clause-index->clause-vcup 511 | default-clause))) 512 | 513 | (defmacro case-fragment [reactive+-value-expr & clauses] 514 | #?(:clj 515 | (let [[even-number-of-exprs default-clause] (if (even? (count clauses)) 516 | [clauses ::undefined] 517 | [(butlast clauses) (last clauses)]) 518 | clauses (partition 2 even-number-of-exprs) 519 | clause-value->clause-index (into {} 520 | (comp (map-indexed (fn [index [clause-value _clause-vcup]] 521 | (if (seq? clause-value) 522 | (->> clause-value 523 | (mapv (fn [clause-value-item] 524 | [clause-value-item index]))) 525 | [[clause-value index]]))) 526 | cat) 527 | clauses) 528 | clause-index->clause-vcup (mapv second clauses)] 529 | `(case-fragment* ~reactive+-value-expr 530 | ~clause-value->clause-index 531 | ~clause-index->clause-vcup 532 | ~default-clause)))) 533 | 534 | (defn cond-fragment* [reactive-index-fn 535 | clause-index->clause-vcup] 536 | (indexed-fragment (sr/create-memo reactive-index-fn) 537 | clause-index->clause-vcup 538 | nil)) 539 | 540 | (defmacro cond-fragment [& clauses] 541 | (assert (even? (count clauses)) "cond-fragment requires an even number of forms") 542 | #?(:clj 543 | (let [clauses (partition 2 clauses) 544 | clause-index->clause-vcup (mapv second clauses)] 545 | `(cond-fragment* (fn [] 546 | (cond 547 | ~@(into [] 548 | (comp (map-indexed (fn [index [clause-condition _clause-vcup]] 549 | [`~clause-condition index])) 550 | cat) 551 | clauses))) 552 | ~clause-index->clause-vcup)))) 553 | 554 | #?(:cljs 555 | (defn for-fragment* 556 | ([reactive+-coll item-component] 557 | (for-fragment* reactive+-coll identity item-component)) 558 | ([reactive+-coll key-fn item-component] 559 | (let [item-cache-atom (atom {})] ;; item-key -> [scope-effect elements] 560 | (ReactiveFragment. 561 | (sr/create-derived (fn [] 562 | (let [coll (deref+ reactive+-coll) 563 | 564 | ;; Update the cache: 565 | ;; - only keep the current item keys, 566 | ;; - compute values for things not already cached. 567 | old-item-cache @item-cache-atom 568 | new-item-cache (into {} 569 | (map (fn [item] 570 | (let [item-key (key-fn item)] 571 | [item-key 572 | (if (contains? old-item-cache item-key) 573 | (get old-item-cache item-key) 574 | (let [{:keys [effects elements]} (process-vcup ($ item-component item))] 575 | [(scope-effect effects {:dispose-on-zero-signal-watchers true}) 576 | elements]))]))) 577 | coll)] 578 | (reset! item-cache-atom new-item-cache) 579 | 580 | ;; Return the aggregated elements 581 | (into [] 582 | (mapcat (fn [item] 583 | (let [[scope elements] (get new-item-cache (key-fn item))] 584 | ;; Makes this signal depend on the scope. 585 | (some-> scope 586 | deref) 587 | 588 | ;; Return the elements 589 | elements))) 590 | coll))) 591 | {:metadata {:name "for-fragment"}})))))) 592 | 593 | ;; ---------------------------------------------- 594 | 595 | #?(:cljs 596 | (defn- re-run-stale-effectful-nodes-at-next-frame [] 597 | (js/requestAnimationFrame (fn [] 598 | (sr/re-run-stale-effectful-nodes) 599 | (re-run-stale-effectful-nodes-at-next-frame))))) 600 | 601 | #?(:cljs 602 | (defn render [^js/Element parent-element vcup] 603 | ;; Remove all the children 604 | (.replaceChildren parent-element) 605 | 606 | (let [{:keys [effects]} (process-vcup ($ parent-element vcup))] 607 | ;; Run all the effects once, without using deref. 608 | (run! sr/run-if-needed effects) 609 | 610 | ;; Automatically refresh the DOM by re-running the effects which need a re-run. 611 | (re-run-stale-effectful-nodes-at-next-frame)))) 612 | 613 | #?(:cljs 614 | (defn dispose-render-effects [] 615 | ;; TODO: dispose all the effects used for the rendering and the DOM updates. 616 | ;; In this article, we will skip this step. 617 | ,)) 618 | --------------------------------------------------------------------------------