4 |
5 | */
6 |
7 | .hljs {
8 | display: block;
9 | overflow-x: auto;
10 | }
11 |
12 | .hljs,
13 | .hljs-subst {
14 | color: #434f54;
15 | }
16 |
17 | .hljs-name {
18 | font-weight: bold;
19 | color: #800;
20 | }
21 |
22 | .hljs-built_in,
23 | .hljs-literal,
24 | .hljs-bullet,
25 | .hljs-code,
26 | .hljs-addition {
27 | color: #D35400;
28 | }
29 |
30 | .hljs-keyword,
31 | .hljs-attribute,
32 | .hljs-selector-tag,
33 | .hljs-doctag,
34 | .hljs-regexp,
35 | .hljs-symbol,
36 | .hljs-variable,
37 | .hljs-template-variable,
38 | .hljs-link,
39 | .hljs-selector-attr,
40 | .hljs-selector-pseudo {
41 | color: #01808e;
42 | }
43 |
44 | .hljs-type,
45 | .hljs-string,
46 | .hljs-selector-id,
47 | .hljs-selector-class,
48 | .hljs-quote,
49 | .hljs-template-tag,
50 | .hljs-deletion {
51 | color: #770000;
52 | }
53 |
54 | .hljs-title,
55 | .hljs-section {
56 | color: #880000;
57 | font-weight: bold;
58 | }
59 |
60 | .hljs-comment {
61 | color: rgba(149,165,166,.8);
62 | }
63 |
64 | .hljs-meta-keyword {
65 | color: #728E00;
66 | }
67 |
68 | .hljs-meta {
69 | color: #728E00;
70 | color: #434f54;
71 | }
72 |
73 | .hljs-emphasis {
74 | font-style: italic;
75 | }
76 |
77 | .hljs-strong {
78 | font-weight: bold;
79 | }
80 |
81 | .hljs-function {
82 | color: #728E00;
83 | }
84 |
85 | .hljs-number {
86 | color: #008800;
87 | }
88 |
--------------------------------------------------------------------------------
/docs/templates/page.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | domino-clj
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | {{ content | safe}}
31 |
32 |
33 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/domino/visualize/graphviz.clj:
--------------------------------------------------------------------------------
1 | (ns domino.vizualize.graphviz
2 | (:require
3 | [rhizome.viz :as viz]))
4 |
5 | (defn zip-keys [keys v]
6 | (zipmap keys (repeat v)))
7 |
8 | (defn- model-ids [model]
9 | (keep :id (flatten model)))
10 | (defn- id-or-fn [{:keys [id handler]}]
11 | (or id (str handler)))
12 | (defn- make-node [type node]
13 | {:id (id-or-fn node) :type type})
14 |
15 | (defn all-nodes [{:keys [model events effects]}]
16 | (concat
17 | (for [id (model-ids model)]
18 | {:id id :type :model})
19 | (for [event events]
20 | (make-node :event event))
21 | (for [effect effects]
22 | (make-node :effect effect))))
23 |
24 | (defn- node-inputs [type node]
25 | (zip-keys
26 | (for [id (:inputs node)] {:id id :type :model})
27 | [(make-node type node)]))
28 | (defn- nodes-inputs [type nodes]
29 | (->> nodes
30 | (map #(node-inputs type %))
31 | (apply merge-with concat)))
32 |
33 | (defn- nodes-outputs [type nodes]
34 | (into {}
35 | (for [node nodes]
36 | [(make-node type node)
37 | (for [id (:outputs node)]
38 | {:id id :type :model})])))
39 |
40 | (defn adjacency [{:keys [events effects]}]
41 | (merge
42 | (merge-with concat (nodes-inputs :event events) (nodes-inputs :effect effects))
43 | (nodes-outputs :event events)
44 | (nodes-outputs :effect effects)))
45 |
46 | (defn node->descriptor [{:keys [id type]}]
47 | {:label id
48 | :fillcolor (case type :event "aliceblue" :effect "cornsilk" "white")
49 | :style "filled"})
50 |
51 | (defn view-schema [schema]
52 | (viz/view-graph (all-nodes schema) (adjacency schema)
53 | :node->descriptor node->descriptor))
54 |
--------------------------------------------------------------------------------
/test/domino/async.clj:
--------------------------------------------------------------------------------
1 | (ns domino.async
2 | (:require
3 | [domino.core :as core]
4 | [clojure.test :refer :all]))
5 |
6 | (deftest async-handler-test
7 | (let [result (atom nil)
8 | ctx (core/initialize {:model [[:foo {:id :foo}]
9 | [:bar {:id :bar}]
10 | [:baz {:id :baz}]
11 | [:buz {:id :buz}]]
12 | :events [{:async? true
13 | :inputs [:foo]
14 | :outputs [:bar]
15 | :handler (fn [ctx {:keys [foo]} _ cb]
16 | (cb {:bar (inc foo)}))}
17 | {:async? true
18 | :inputs [:bar]
19 | :outputs [:baz]
20 | :handler (fn [ctx {:keys [bar]} _ cb]
21 | (cb {:baz (inc bar)}))}
22 | {:inputs [:baz]
23 | :outputs [:buz]
24 | :handler (fn [ctx {:keys [baz]} _]
25 | {:buz (inc baz)})}]
26 | :effects [{:inputs [:buz]
27 | :handler (fn [ctx {:keys [buz]}]
28 | (reset! result buz))}]})]
29 | (:domino.core/db (core/transact ctx [[[:foo] 1]]))
30 | (is (= 4 @result))))
31 |
--------------------------------------------------------------------------------
/docs/compile.cljs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env lumo
2 | (ns compile.core
3 | (:require
4 | [cljs.tools.reader :as reader :refer [read]]
5 | [cljs.tools.reader.reader-types :refer [string-push-back-reader read-char]]
6 | [clojure.string :as string]
7 | [markdown.core :as markdown]
8 | ["fs-extra" :as fs]
9 | ["nunjucks" :as nj])
10 | (:import goog.string.StringBuffer))
11 |
12 | (nj/configure "templates")
13 |
14 | (defn slurp [path]
15 | (fs/readFileSync path #js{:encoding "UTF-8"}))
16 |
17 | (defn spit [f content]
18 | (fs/writeFile f
19 | content
20 | (fn [err]
21 | (if err (println err) (println "wrote file " f)))))
22 |
23 | (defn read-to-eof [rdr]
24 | (loop [c (read-char rdr)
25 | s (StringBuffer.)]
26 | (if c
27 | (recur (read-char rdr) (.append s c))
28 | (str s))))
29 |
30 | (defn md->html [s]
31 | (markdown/md->html s))
32 |
33 | (defn path [& args]
34 | (clojure.string/join "/" (remove nil? args)))
35 |
36 | ;;================= MAIN ============================
37 | (let [readme (slurp (path ".." "README.md"))
38 | docs (try (fs/readdirSync "md" #js{:encoding "UTF-8"}) (catch js/Error _))
39 | resources (try (fs/readdirSync "resources" #js{:encoding "UTF-8"}) (catch js/Error _))]
40 |
41 | (fs/copySync (path ".." "logo") (path "out" "logo"))
42 | (doseq [resource resources]
43 | (fs/copySync (path "resources" resource) (path "out" resource)))
44 |
45 | (spit
46 | (path "out" "index.html")
47 | (nj/render
48 | "page.html"
49 | (clj->js
50 | {:content (md->html readme)})))
51 |
52 | (doseq [doc docs]
53 | (spit
54 | (path "out" (str (first (string/split doc #"\.")) ".html"))
55 | (nj/render
56 | "page.html"
57 | (clj->js
58 | {:content (->> (path "md" doc)
59 | (slurp)
60 | (md->html))}))))
61 | )
62 |
63 |
--------------------------------------------------------------------------------
/src/domino/effects.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.effects
2 | (:require
3 | [domino.events :as events]
4 | [domino.util :refer [generate-sub-paths]]))
5 |
6 | (defn effects-by-paths [effects]
7 | (reduce
8 | (fn [out {:keys [inputs] :as effect}]
9 | (reduce
10 | (fn [effects path]
11 | (update effects path (fnil conj []) effect))
12 | out
13 | inputs))
14 | {}
15 | effects))
16 |
17 | (defn change-effects [effects changes]
18 | (mapcat (fn [path] (get effects path))
19 | changes))
20 |
21 | (defn execute-effect! [{:domino.core/keys [model db] :as ctx} {:keys [inputs handler]}]
22 | (handler ctx (events/get-db-paths model db inputs)))
23 |
24 | (defn execute-effects!
25 | [{:domino.core/keys [change-history effects] :as ctx}]
26 | (reduce
27 | (fn [visited effect]
28 | (if-not (contains? visited effect)
29 | (do (execute-effect! ctx effect)
30 | (conj visited effect))
31 | visited))
32 | #{}
33 | (->> (map first change-history)
34 | (mapcat generate-sub-paths)
35 | distinct
36 | (change-effects effects)))) ;; TODO: double check this approach when changes is a sequential history
37 |
38 | (defn try-effect [{:keys [handler] :as effect} ctx db old-outputs]
39 | (try
40 | (handler ctx old-outputs)
41 | (catch #?(:clj Exception :cljs js/Error) e
42 | (throw (ex-info "failed to execute effect" {:effect effect :context ctx :db db} e)))))
43 |
44 | (defn effect-outputs-as-changes [{:domino.core/keys [effects-by-id db model] :as ctx} effect-ids]
45 | (let [id->effect #(get-in effects-by-id [%])
46 | id->path #(get-in model [:id->path %])
47 | res->change (juxt (comp id->path first) second)
48 | old-outputs #(events/get-db-paths model db (map id->path (:outputs %)))
49 | run-effect #(try-effect % ctx db (old-outputs %))]
50 | (->> effect-ids
51 | ;; TODO: transduce
52 | (map id->effect)
53 | (map run-effect)
54 | (mapcat identity)
55 | (map res->change))))
56 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | jobs:
4 | deploy-docs:
5 | docker:
6 | - image: circleci/node:latest
7 | steps:
8 | - checkout
9 | - run:
10 | name: build site?
11 | command: |
12 | COMMIT_RANGE=$(echo $CIRCLE_COMPARE_URL | sed 's:^.*/compare/::g')
13 | echo "Commit range" $COMMIT_RANGE
14 | echo
15 | if [[ $(git diff --name-only $COMMIT_RANGE | grep -E '^(README.md|docs/|\.circleci/config.yml)') != "" ]]
16 | then
17 | echo "Building docs"
18 | cd docs
19 | npm install
20 | npx lumo compile.cljs
21 | echo
22 | git clone https://${GH_TOKEN}@github.com/domino-clj/domino-clj.github.io.git
23 | echo
24 | echo "Copying files to GH pages repo"
25 | cp -r out/* domino-clj.github.io/
26 | cd domino-clj.github.io
27 | git config user.email "carmen.wla@gmail.com"
28 | git config user.name "Carmen La"
29 | git diff-index --quiet HEAD || (git add --all && git commit -am "$CIRCLE_BUILD_URL" && git push --force origin master)
30 | else
31 | echo "No changes to docs/ folder or README.md, ending build"
32 | fi
33 |
34 | test:
35 | docker:
36 | - image: circleci/clojure:lein-2.9.1-node
37 | steps:
38 | - checkout
39 | - restore_cache:
40 | keys:
41 | - deps-{{ checksum "project.clj"}}
42 | - save_cache:
43 | paths:
44 | - ~/.m2
45 | key: deps-{{ checksum "project.clj"}}
46 | - run:
47 | name: test clj
48 | command: lein test2junit
49 | - run:
50 | name: test cljs
51 | command: lein with-profile test doo node once
52 | - store_test_results:
53 | path: "target/test2junit"
54 |
55 |
56 | workflows:
57 | version: 2
58 | test-deploy:
59 | jobs:
60 | - test
61 | - deploy-docs:
62 | requires:
63 | - test
64 | filters:
65 | branches:
66 | only:
67 | - master
68 |
--------------------------------------------------------------------------------
/test/domino/benchmark.clj:
--------------------------------------------------------------------------------
1 | (ns domino.benchmark
2 | (:require
3 | [domino.core :as core]
4 | [clojure.test :refer :all]
5 | [criterium.core :as criterium]))
6 |
7 | (deftest ^:benchmark bench-initialize
8 | (let [state (atom {})]
9 | (criterium/bench
10 | (core/initialize
11 | {:model [[:user {:id :user}
12 | [:first-name {:id :fname}]
13 | [:last-name {:id :lname}]
14 | [:full-name {:id :full-name}]]]
15 | :effects [{:inputs [:fname :lname :full-name]
16 | :handler (fn [_ {:keys [fname lname full-name]}]
17 | (swap! state assoc
18 | :first-name fname
19 | :last-name lname
20 | :full-name full-name))}]
21 | :events [{:inputs [:fname :lname]
22 | :outputs [:full-name]
23 | :handler (fn [_ {:keys [fname lname]} _]
24 | {:full-name (or (when (and fname lname) (str lname ", " fname))
25 | fname
26 | lname)})}]}))))
27 |
28 | (deftest ^:benchmark bench-transact
29 | (let [ctx (core/initialize
30 | {:model [[:f {:id :f}
31 | [:a {:id :a}]
32 | [:b {:id :b}]
33 | [:c {:id :c}]
34 | [:d {:id :d}]]]
35 | :events [{:inputs [:a :b]
36 | :outputs [:c]
37 | :handler (fn [_ {:keys [a b]} _]
38 | {:c "C"})}
39 | {:inputs [:b :c]
40 | :outputs [:d]
41 | :handler (fn [_ {:keys [b c]} _]
42 | {:d "D"})}
43 | {:inputs [:d]
44 | :outputs [:a]
45 | :handler (fn [_ {:keys [d]} _]
46 | {:a "A"})}]})]
47 | (criterium/bench (core/transact ctx [[[:a] 1] [[:b] 1]]))))
48 |
--------------------------------------------------------------------------------
/src/domino/core.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.core
2 | (:require
3 | [domino.effects :as effects]
4 | [domino.events :as events]
5 | [domino.graph :as graph]
6 | [domino.model :as model]
7 | [domino.validation :as validation]
8 | [domino.util :as util]))
9 |
10 | #?(:clj
11 | (defmacro event [[_ in out :as args] & body]
12 | (let [in-ks# (mapv keyword (:keys in))
13 | out-ks# (mapv keyword (:keys out))]
14 | {:inputs in-ks#
15 | :outputs out-ks#
16 | :handler `(fn ~(vec args) ~@body)})))
17 |
18 | (defn transact
19 | "Take the context and the changes which are an ordered collection of changes
20 |
21 | Assumes all changes are associative changes (i.e. vectors or hashmaps)"
22 | [ctx changes]
23 | (let [updated-ctx (events/execute-events ctx changes)]
24 | (effects/execute-effects! updated-ctx)
25 | updated-ctx))
26 |
27 | (defn initial-transaction
28 | "If initial-db is not empty, transact with initial db as changes"
29 | [{::keys [model] :as ctx} initial-db]
30 | (if (empty? initial-db)
31 | ctx
32 | (transact ctx
33 | (reduce
34 | (fn [inputs [_ path]]
35 | (if-some [v (get-in initial-db path)]
36 | (conj inputs [path v])
37 | inputs))
38 | []
39 | (:id->path model)))))
40 |
41 | (defn initialize
42 | "Takes a schema of :model, :events, and :effects
43 |
44 | 1. Parse the model
45 | 2. Inject paths into events
46 | 3. Generate the events graph
47 | 4. Reset the local ctx and return value
48 |
49 | ctx is a map of:
50 | ::model => a map of model keys to paths
51 | ::events => a vector of events with pure functions that transform the state
52 | ::effects => a vector of effects with functions that produce external effects
53 | ::state => the state of actual working data
54 | "
55 | ([schema]
56 | (initialize schema {}))
57 | ([{:keys [model effects events] :as schema} initial-db]
58 | ;; Validate schema
59 | (validation/maybe-throw-exception (validation/validate-schema schema))
60 | ;; Construct ctx
61 | (let [model (model/model->paths model)
62 | events (model/connect-events model events)]
63 | (initial-transaction
64 | {::model model
65 | ::events events
66 | ::events-by-id (util/map-by-id events)
67 | ::effects (effects/effects-by-paths (model/connect-effects model effects))
68 | ::effects-by-id (util/map-by-id effects)
69 | ::db initial-db
70 | ::graph (graph/gen-ev-graph events)}
71 | initial-db))))
72 |
73 | (defn trigger-effects
74 | "Triggers effects by ids as opposed to data changes
75 |
76 | Accepts the context, and a collection of effect ids"
77 | [ctx effect-ids]
78 | (transact ctx (effects/effect-outputs-as-changes ctx effect-ids)))
79 |
--------------------------------------------------------------------------------
/env/dev/cljs/domino/test_page.cljs:
--------------------------------------------------------------------------------
1 | (ns domino.test-page
2 | (:require [domino.core :as core]
3 | [reagent.core :as r]
4 | [cljs.pprint :refer [pprint]]))
5 |
6 | (def ctx
7 | (r/atom
8 | (let [model {:model
9 | [[:user {}
10 | [:first-name {:id :fname}]
11 | [:last-name {:id :lname}]
12 | [:full-name {:id :full-name}]
13 | [:weight {:id :weight}
14 | [:lb {:id :lb}]
15 | [:kg {:id :kg}]]]
16 | [:physician {}
17 | [:first-name {:id :physician-fname}]]]
18 | :effects
19 | [{:inputs [:full-name]
20 | :handler (fn [_ {:keys [full-name]}]
21 | (when (= "Bobberton, Bob" full-name)
22 | (js/alert "launching missiles!")))}]
23 | :events
24 | [{:inputs [:fname :lname]
25 | :outputs [:full-name]
26 | :handler (fn [_ {:keys [fname lname]} _]
27 | {:full-name (or (when (and fname lname) (str lname ", " fname)) fname lname)})}
28 | {:inputs [:kg]
29 | :outputs [:lb]
30 | :handler (fn [_ {:keys [kg]} _]
31 | {:lb (* kg 2.20462)})}
32 | {:inputs [:lb]
33 | :outputs [:kg]
34 | :handler (fn [_ {:keys [lb]} _]
35 | {:kg (/ lb 2.20462)})}]}]
36 | (core/initialize model {}))))
37 |
38 | (defn transact [path value]
39 | (swap! ctx core/transact [[path value]]))
40 |
41 | (defn db-value [path]
42 | (get-in @ctx (into [::core/db] path)))
43 |
44 | (defn target-value [e]
45 | (.. e -target -value))
46 |
47 | (defn state-atom [path]
48 | (let [state (r/atom nil)]
49 | (add-watch ctx path
50 | (fn [path _ old-state new-state]
51 | (let [ctx-path (into [::core/db] path)
52 | old-value (get-in old-state ctx-path)
53 | new-value (get-in new-state ctx-path)]
54 | (when (not= old-value new-value)
55 | (reset! state new-value)))))
56 | state))
57 |
58 | (defn input [label path & [fmt]]
59 | (r/with-let [local-state (state-atom path)
60 | save-value #(reset! local-state (if fmt (fmt (target-value %)) (target-value %)))]
61 | [:div
62 | [:label label " "]
63 | [:input
64 | {:value @local-state
65 | :on-change save-value
66 | :on-blur #(transact path @local-state)}]]))
67 |
68 | (defn home-page []
69 | [:div
70 | [input "First name" [:user :first-name]]
71 | [input "Last name" [:user :last-name]]
72 | [input "Weight (kg)" [:user :weight :kg] (fnil js/parseFloat 0)]
73 | [input "Weight (lb)" [:user :weight :lb] (fnil js/parseFloat 0)]
74 | [:label "Full name " (db-value [:user :full-name])]
75 | [:hr]
76 | [:h4 "DB state"]
77 | [:pre (with-out-str (pprint (:domino.core/db @ctx)))]])
78 |
79 | (defn mount-root []
80 | (r/render [home-page] (.getElementById js/document "app")))
81 |
82 | (defn init! []
83 | (mount-root))
84 |
--------------------------------------------------------------------------------
/test/domino/model_test.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.model-test
2 | (:require
3 | [domino.core :as core]
4 | [domino.graph :as graph]
5 | [domino.events :as events]
6 | [domino.model :as model]
7 | #?(:clj [clojure.test :refer :all]
8 | :cljs [cljs.test :refer-macros [is are deftest testing use-fixtures]])))
9 |
10 | (deftest model-parse-test
11 | (let [model [[:title {:validation []}]
12 | [:user {:id :user}
13 | [:first-name {:id :fname}]
14 | [:last-name {:id :lname}]
15 | [:profile {}
16 | [:address {:id :address}
17 | [:street {}]
18 | [:city {:id :city}]]]]]]
19 | (is (= {:user [:user],
20 | :fname [:user :first-name],
21 | :lname [:user :last-name],
22 | :address [:user :profile :address],
23 | :city [:user :profile :address :city]}
24 | (:id->path (model/model->paths model))))))
25 |
26 | (deftest id-lookup-test
27 | (let [model [[:title {:validation []}]
28 | [:user {:id :user}
29 | [:first-name {:id :fname}]
30 | [:last-name {:id :lname}]
31 | [:profile
32 | [:address {:id :address}
33 | [:street]
34 | [:city {:id :city}]]]]]
35 | ctx (model/model->paths model)]
36 |
37 | (is (= :fname (model/id-for-path ctx [:user :first-name])))
38 | (is (= :address (model/id-for-path ctx [:user :profile :address :street])))
39 | (is (nil? (model/id-for-path ctx [:profile :address :street])))))
40 |
41 | (deftest connect-events-to-model
42 | (let [model [[:user {:id :user}
43 | [:first-name {:id :fname}]
44 | [:last-name {:id :lname}]
45 | [:full-name {:id :full-name}]]]
46 | {:keys [id->path] :as model-paths} (model/model->paths model)
47 | events [{:inputs [:fname :lname]
48 | :outputs [:full-name]
49 | :handler (fn [_ [fname lname] _]
50 | [(or (when (and fname lname) (str lname ", " fname)) fname lname)])}]
51 | connected-events (model/connect-events model-paths events)]
52 | (is (= {:inputs [[:user :first-name] [:user :last-name]]
53 | :outputs [[:user :full-name]]}
54 | (dissoc (first connected-events) :handler)))
55 | (is (=
56 | {::core/db {:user {:first-name "Bob"}}
57 | ::core/change-history [[[:user :first-name] "Bob"]]}
58 | (select-keys
59 | (events/execute-events {::core/db {}
60 | ::core/graph (graph/gen-ev-graph events)}
61 | [[(id->path :fname) "Bob"]])
62 | [::core/db ::core/change-history])))
63 | (is (=
64 | {::core/db {:user {:first-name "Bob"
65 | :last-name "Bobberton"}}
66 | ::core/change-history [[[:user :first-name] "Bob"] [[:user :last-name] "Bobberton"]]}
67 | (select-keys
68 | (events/execute-events {::core/db {}
69 | ::core/graph (graph/gen-ev-graph events)}
70 | [[(id->path :fname) "Bob"]
71 | [(id->path :lname) "Bobberton"]])
72 | [::core/db ::core/change-history])))))
73 |
74 |
--------------------------------------------------------------------------------
/docs/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "domino-docs",
3 | "version": "0.0.1",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "a-sync-waterfall": {
8 | "version": "1.0.1",
9 | "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
10 | "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA=="
11 | },
12 | "asap": {
13 | "version": "2.0.6",
14 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
15 | "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
16 | },
17 | "at-least-node": {
18 | "version": "1.0.0",
19 | "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
20 | "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="
21 | },
22 | "commander": {
23 | "version": "5.1.0",
24 | "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
25 | "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="
26 | },
27 | "fs-extra": {
28 | "version": "9.0.0",
29 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz",
30 | "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==",
31 | "requires": {
32 | "at-least-node": "^1.0.0",
33 | "graceful-fs": "^4.2.0",
34 | "jsonfile": "^6.0.1",
35 | "universalify": "^1.0.0"
36 | }
37 | },
38 | "graceful-fs": {
39 | "version": "4.2.3",
40 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
41 | "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
42 | },
43 | "jsonfile": {
44 | "version": "6.0.1",
45 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.0.1.tgz",
46 | "integrity": "sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==",
47 | "requires": {
48 | "graceful-fs": "^4.1.6",
49 | "universalify": "^1.0.0"
50 | }
51 | },
52 | "lumo-cljs": {
53 | "version": "1.10.1",
54 | "resolved": "https://registry.npmjs.org/lumo-cljs/-/lumo-cljs-1.10.1.tgz",
55 | "integrity": "sha512-pqgygbEEnzOjFUxejr/jK7gRhuQx0acd3PLyJwkz4ZyVHWyzGGhUxwzgYX/df4wKm7Zixfp73G0S0PFXiNHULQ=="
56 | },
57 | "markdown-clj": {
58 | "version": "1.10.1",
59 | "resolved": "https://registry.npmjs.org/markdown-clj/-/markdown-clj-1.10.1.tgz",
60 | "integrity": "sha512-zx7tQ2caKk+r1VWd3zwrkYk30dSQRywmYWkGQ6Kw/SD4bTg8jD1+kIngbVaFfKfnlun+OKexjIU49M3psGh34Q=="
61 | },
62 | "nunjucks": {
63 | "version": "3.2.4",
64 | "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
65 | "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
66 | "requires": {
67 | "a-sync-waterfall": "^1.0.0",
68 | "asap": "^2.0.3",
69 | "commander": "^5.1.0"
70 | }
71 | },
72 | "universalify": {
73 | "version": "1.0.0",
74 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
75 | "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug=="
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject domino/core "0.3.3"
2 | :description "Clojure(script) data flow engine"
3 | :url "https://github.com/domino-clj/domino"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies
7 | [[org.clojure/clojure "1.10.1" :scope "provided"]
8 | [org.clojure/clojurescript "1.10.520" :scope "provided"]
9 | [rhizome "0.2.9"]]
10 |
11 | :plugins
12 | [[lein-cljsbuild "1.1.7"]
13 | [lein-figwheel "0.5.19"]
14 | [cider/cider-nrepl "0.21.1"]
15 | [lein-doo "0.1.10"]
16 | [com.jakemccrary/lein-test-refresh "0.24.1"]
17 | [test2junit "1.4.2"]]
18 |
19 | :test2junit-output-dir "target/test2junit"
20 |
21 | :clojurescript? true
22 | :jar-exclusions [#"\.swp|\.swo|\.DS_Store"]
23 | :clean-targets ^{:protect false}
24 | [:target-path
25 | [:cljsbuild :builds :app :compiler :output-dir]
26 | [:cljsbuild :builds :app :compiler :output-to]]
27 |
28 | :test-selectors {:default (complement :benchmark)
29 | :benchmark :benchmark
30 | :all (constantly true)}
31 |
32 | :profiles
33 | {:dev
34 | {:dependencies
35 | [[reagent "0.8.1"]
36 | [ring-server "0.5.0"]
37 | [ring-webjars "0.2.0"]
38 | [ring "1.7.1"]
39 | [ring/ring-defaults "0.3.2"]
40 | [compojure "1.6.1"]
41 | [hiccup "1.0.5"]
42 | [nrepl "0.6.0"]
43 | [binaryage/devtools "0.9.10"]
44 | [cider/piggieback "0.4.0"]
45 | [figwheel-sidecar "0.5.19"]
46 | [cheshire "5.8.1"]
47 | [pjstadig/humane-test-output "0.9.0"]
48 | [criterium "0.4.5"]
49 | [org.clojure/tools.namespace "0.3.1"]]
50 |
51 | :injections [(require 'pjstadig.humane-test-output)
52 | (pjstadig.humane-test-output/activate!)]
53 |
54 | :source-paths ["src" "env/dev/clj" "env/dev/cljs"]
55 | :resource-paths ["resources" "env/dev/resources" "target/cljsbuild"]
56 |
57 | :figwheel
58 | {:server-port 3450
59 | :nrepl-port 7000
60 | :nrepl-middleware [cider.piggieback/wrap-cljs-repl
61 | cider.nrepl/cider-middleware]
62 | :css-dirs ["resources/public/css" "env/dev/resources/public/css"]
63 | :ring-handler domino.server/app}
64 |
65 | :cljsbuild
66 | {:builds
67 | {:app
68 | {:source-paths ["src" "env/dev/cljs"]
69 | :figwheel {:on-jsload "domino.test-page/mount-root"}
70 | :compiler {:main domino.dev
71 | :asset-path "/js/out"
72 | :output-to "target/cljsbuild/public/js/app.js"
73 | :output-dir "target/cljsbuild/public/js/out"
74 | :source-map-timestamp true
75 | :source-map true
76 | :optimizations :none
77 | :pretty-print true}}}}}
78 | :test
79 | {:cljsbuild
80 | {:builds
81 | {:test
82 | {:source-paths ["src" "test"]
83 | :compiler {:main domino.runner
84 | :output-to "target/test/core.js"
85 | :target :nodejs
86 | :optimizations :none
87 | :source-map true
88 | :pretty-print true}}}}
89 | :doo {:build "test"}}}
90 | :aliases
91 | {#_#_"test"
92 | ["do"
93 | ["clean"]
94 | ["with-profile" "test" "doo" "node" "once"]]
95 | "test-watch"
96 | ["do"
97 | ["clean"]
98 | ["with-profile" "test" "doo" "node"]]})
99 |
--------------------------------------------------------------------------------
/logo/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
42 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/domino/model.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.model
2 | (:require
3 | [domino.util :as util]))
4 |
5 | (defn normalize [path-segment]
6 | (if (map? (second path-segment))
7 | path-segment
8 | (into [(first path-segment) {}] (rest path-segment))))
9 |
10 | (defn paths-by-id
11 | ([root] (paths-by-id {} [] root))
12 | ([mapped-paths path path-segment]
13 | (let [[segment opts & children] (normalize path-segment)]
14 | (if segment
15 | (let [path (conj path segment)
16 | mapped-paths (if-let [id (:id opts)]
17 | (assoc mapped-paths id {:path path
18 | :opts opts})
19 | mapped-paths)]
20 | (if-not (empty? children)
21 | (apply merge (map (partial paths-by-id mapped-paths path) children))
22 | mapped-paths))
23 | mapped-paths))))
24 |
25 | (defn model->paths [model]
26 | (reduce
27 | (fn [model [id {:keys [path opts]}]]
28 | (-> model
29 | (update :id->path assoc id path)
30 | (update :path->id assoc path id)
31 | (update :id->opts assoc id opts)))
32 | {}
33 | (apply merge (map paths-by-id model))))
34 |
35 | (defn id-for-path [{:keys [path->id]} path]
36 | (loop [path-segment path]
37 | (when-not (empty? path-segment)
38 | (if-let [id (get path->id path-segment)]
39 | id
40 | (recur (butlast path-segment))))))
41 |
42 | (defn wrap-pre [handler pre]
43 | (let [[interceptor & pre] (reverse pre)]
44 | (reduce
45 | (fn [handler interceptor]
46 | (interceptor handler))
47 | (interceptor handler)
48 | pre)))
49 |
50 | (defn wrap-post [post]
51 | (reduce
52 | (fn [handler interceptor]
53 | (interceptor handler))
54 | identity
55 | (reverse post)))
56 |
57 | (defn wrap [handler pre post]
58 | (cond
59 | (and (empty? pre) (empty? post))
60 | handler
61 |
62 | (empty? post)
63 | (wrap-pre handler pre)
64 |
65 | (empty? pre)
66 | (let [post (wrap-post post)]
67 | (fn [ctx inputs outputs]
68 | (post (handler ctx inputs outputs))))
69 |
70 | :else
71 | (let [handler (wrap-pre handler pre)
72 | post (wrap-post post)]
73 | (fn [ctx inputs outputs]
74 | (post (handler ctx inputs outputs))))))
75 |
76 | (defn ids-to-interceptors
77 | "finds the interceptors based on the provided ids
78 | the lookup will consider parent path segments"
79 | [path->id id->path id->opts ids k]
80 | (->> (map id->path ids)
81 | (mapcat util/generate-sub-paths)
82 | (mapcat #(get-in id->opts [(path->id %) k]))
83 | (distinct)
84 | (remove nil?)
85 | (not-empty)))
86 |
87 | ;;TODO ensure all keys are unique!
88 | (defn connect-events [{:keys [path->id id->path id->opts]} events]
89 | (let [path-for-id (fn [id] (get id->path id))]
90 | (mapv
91 | (fn [{:keys [inputs] :as event}]
92 | (let [pre (ids-to-interceptors path->id id->path id->opts inputs :pre)
93 | post (ids-to-interceptors path->id id->path id->opts inputs :post)]
94 | (-> event
95 | (update :inputs #(map path-for-id %))
96 | (update :outputs #(map path-for-id %))
97 | (update :handler wrap pre post))))
98 | events)))
99 |
100 | (defn connect-effects [{:keys [id->path]} events]
101 | (let [path-for-id (fn [id] (get id->path id))]
102 | (mapv
103 | (fn [event]
104 | (-> event
105 | (update :inputs #(map path-for-id %))
106 | (update :outputs #(map path-for-id %))))
107 | events)))
108 |
109 |
--------------------------------------------------------------------------------
/src/domino/graph.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.graph
2 | (:require
3 | [domino.util :refer [generate-sub-paths]]
4 | [clojure.set :refer [union]]))
5 |
6 | (def conj-set (fnil conj #{}))
7 |
8 | (def into-set (fnil into #{}))
9 |
10 | (defn find-related
11 | "finds other nodes related by eventset"
12 | [input-node events]
13 | (->> events
14 | (keep (fn [{:keys [inputs outputs]}]
15 | (when (some #{input-node} outputs)
16 | (remove #(= input-node %) inputs))))
17 | (apply concat)))
18 |
19 | (defn add-nodes
20 | [ctx inputs]
21 | (reduce
22 | (fn [[{:keys [nodes] :as ctx} inputs] input]
23 | (let [related (find-related input nodes)]
24 | [(-> ctx
25 | (update :visited conj-set input)
26 | (update :graph update input into-set related))
27 | (into inputs related)]))
28 | [ctx #{}]
29 | inputs))
30 |
31 | (defn base-graph-ctx
32 | [nodes]
33 | {:nodes nodes
34 | :graph {}})
35 |
36 | (defn input-nodes
37 | [events]
38 | (distinct (mapcat :inputs events)))
39 |
40 | (defn gen-graph
41 | [events]
42 | (reduce
43 | (fn [g {i :inputs o :outputs}]
44 | (merge-with
45 | union
46 | g
47 | (zipmap i (repeat (set o)))))
48 | {}
49 | events))
50 |
51 | (defn reverse-edge-direction
52 | [graph]
53 | (reduce-kv
54 | (fn [inverted i o]
55 | (merge-with
56 | union
57 | inverted
58 | (zipmap o (repeat #{i}))))
59 | {}
60 | graph))
61 |
62 | (defn validate-event [{:keys [outputs handler] :as ev} errors]
63 | (if-let [event-errors (not-empty
64 | (cond-> []
65 | (empty? outputs) (conj "event :outputs must contain at least one target")
66 | (not (fn? handler)) (conj "event :handler must be a function")))]
67 | (assoc errors ev event-errors)
68 | errors))
69 |
70 | (defn gen-ev-graph
71 | [events]
72 | (let [[graph errors]
73 | (reduce
74 | (fn [[g errors] {i :inputs o :outputs :as ev}]
75 | [(merge-with
76 | union
77 | g
78 | (zipmap i (repeat #{{:edge ev :relationship :input :connections (set o)}}))
79 | (zipmap o (repeat #{{:edge ev :relationship :output :connections (set i)}})))
80 | (validate-event ev errors)])
81 | [{} {}]
82 | events)]
83 | (if-not (empty? errors)
84 | (throw (ex-info
85 | "graph contains invalid events"
86 | {:errors errors}))
87 | graph)))
88 |
89 | (defn traversed-edges [origin graph edge-filter]
90 | (let [edges (filter edge-filter (get graph origin #{}))
91 | related-nodes (filter
92 | (partial contains? graph)
93 | (disj
94 | (reduce
95 | union
96 | #{}
97 | (map :connections edges))
98 | origin))]
99 | (apply union
100 | (set edges)
101 | (map
102 | #(traversed-edges
103 | %
104 | (dissoc graph origin)
105 | edge-filter)
106 | related-nodes))))
107 |
108 | (defn connected-nodes-map [graph edge-filter]
109 | (->> graph
110 | keys
111 | (map
112 | (juxt identity
113 | #(->> (traversed-edges % graph edge-filter)
114 | (map :connections)
115 | (apply union))))
116 | (into {})))
117 |
118 | (defn subgraphs [graph]
119 | (->> (connected-nodes-map graph (constantly true))
120 | vals
121 | distinct
122 | (remove empty?)
123 | (map #(select-keys graph %))))
124 |
--------------------------------------------------------------------------------
/docs/md/demo.md:
--------------------------------------------------------------------------------
1 | # Example of Domino with re-frame
2 |
3 |
4 | (require '[domino.core :as domino])
5 |
6 |
7 | ```clojure lang-eval-clojure
8 | (require '[reagent.core :as reagent])
9 | (require '[re-frame.core :as rf])
10 | (require '[goog.string :as string])
11 | (require '[goog.string.format])
12 | (require '[cljs.pprint :refer [pprint]])
13 | ```
14 |
15 | ```clojure lang-eval-clojure
16 | ;;initialize the Domino context using the supplied schema
17 | (rf/reg-event-db
18 | :init
19 | (fn [_ [_ schema]]
20 | (domino/initialize schema)))
21 |
22 | ;;trigger effects with the provided ids
23 | (rf/reg-event-db
24 | :trigger-effects
25 | (fn [ctx [_ effect-ids]]
26 | (domino/trigger-effects ctx effect-ids)))
27 |
28 | ;;dispatch an input event
29 | (rf/reg-event-db
30 | :event
31 | (fn [ctx [_ id value]]
32 | (domino/transact ctx [[(get-in (::domino/model ctx) [:id->path id]) value]])))
33 |
34 | ;;subscribe to the value at the given id
35 | (rf/reg-sub
36 | :id
37 | (fn [ctx [_ id]]
38 | (get-in (::domino/db ctx) (get-in (::domino/model ctx) [:id->path id]))))
39 |
40 | ;;returns Domino db
41 | (rf/reg-sub
42 | :db
43 | (fn [ctx _]
44 | (::domino/db ctx)))
45 |
46 | (defn parse-float [s]
47 | (let [value (js/parseFloat s)]
48 | (when-not (js/isNaN value) value)))
49 |
50 | (defn format-number [n]
51 | (when n (string/format "%.2f" n)))
52 |
53 | (defn text-input [label id]
54 | [:div
55 | [:label label]
56 | [:input
57 | {:type :text
58 | :value @(rf/subscribe [:id id])
59 | :on-change #(rf/dispatch [:event id (-> % .-target .-value)])}]])
60 |
61 | (defn numeric-input [label id]
62 | (reagent/with-let [value (reagent/atom nil)]
63 | [:div
64 | [:label label]
65 | [:input
66 | {:type :text
67 | :value @value
68 | :on-focus #(reset! value @(rf/subscribe [:id id]))
69 | :on-blur #(rf/dispatch [:event id (parse-float @value)])
70 | :on-change #(reset! value (-> % .-target .-value))}]]))
71 |
72 | ;;initialize Domino
73 | (rf/dispatch-sync
74 | [:init
75 | {:model
76 | [[:demographics
77 | [:first-name {:id :first-name}]
78 | [:last-name {:id :last-name}]
79 | [:full-name {:id :full-name}]]
80 | [:vitals
81 | [:height {:id :height}]
82 | [:weight {:id :weight}]
83 | [:bmi {:id :bmi}]]
84 | [:counter {:id :counter}]]
85 | :effects
86 | [{:id :increment
87 | :outputs [:counter]
88 | :handler (fn [_ state]
89 | (update state :counter (fnil inc 0)))}
90 | {:inputs [:full-name]
91 | :handler (fn [_ {:keys [full-name]}]
92 | (when (= "Bobberton, Bob" full-name)
93 | (js/alert "Hi Bob!")))}]
94 | :events
95 | [{:inputs [:first-name :last-name]
96 | :outputs [:full-name]
97 | :handler (fn [_ {:keys [first-name last-name]} _]
98 | {:full-name (or (when (and first-name last-name)
99 | (str last-name ", " first-name))
100 | first-name
101 | last-name)})}
102 | {:inputs [:height :weight]
103 | :outputs [:bmi]
104 | :handler (fn [_ {:keys [height weight]} {:keys [bmi]}]
105 | {:bmi (if (and height weight)
106 | (/ weight (* height height))
107 | bmi)})}]}])
108 |
109 | ;;render the UI
110 | (defn home-page []
111 | [:div
112 | [:h3 "Patient demographics"]
113 | [text-input "First name" :first-name]
114 | [text-input "Last name" :last-name]
115 | [numeric-input "Height (M)" :height (fnil js/parseFloat 0)]
116 | [numeric-input "Weight (KG)" :weight (fnil js/parseFloat 0)]
117 | [:button
118 | {:on-click #(rf/dispatch [:trigger-effects [:increment]])}
119 | "increment count"]
120 | [:p>label "Full name " @(rf/subscribe [:id :full-name])]
121 | [:p>label "BMI " (format-number @(rf/subscribe [:id :bmi]))]
122 | [:p>label "Counter " @(rf/subscribe [:id :counter])]
123 | [:hr]
124 | [:h4 "DB state"]
125 | [:pre (with-out-str (pprint @(rf/subscribe [:db])))]])
126 |
127 | (reagent/render-component [home-page] js/klipse-container)
128 | ```
129 |
--------------------------------------------------------------------------------
/test/domino/validation_test.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.validation-test
2 | (:require [domino.validation :as validation]
3 | #?(:clj [clojure.test :refer :all]
4 | :cljs [cljs.test :refer-macros [is are deftest testing use-fixtures]])))
5 |
6 | (defn ->hex [s]
7 | #?(:clj (format "%02x" (int s))
8 | :cljs (.toString (.charCodeAt s 0) 16)))
9 |
10 | (deftest duplicate-id-keys-present
11 | (is (= [["duplicate id :fname in the model" {:id :fname}]]
12 | (:errors (validation/check-valid-model {:model [[:user {:id :user}
13 | [:first-name {:id :fname}]
14 | [:last-name {:id :fname}]]]
15 | :path-ids #{}}))))
16 | (is (= [["duplicate id :fname in the model" {:id :fname}]]
17 | (:errors (validation/check-valid-model {:model [[:user {:id :fname}
18 | [:first-name {:id :user}]
19 | [:last-name {:id :fname}]]]
20 | :path-ids #{}}))))
21 | (is (= [["duplicate id :fname in the model" {:id :fname}]]
22 | (:errors (validation/check-valid-model {:model [[:user {:id :fname}
23 | [:first-name {:id :user}]
24 | [:last-name {:id :lname}
25 | [:inner {:id :fname}]]]]
26 | :path-ids #{}})))))
27 |
28 | (deftest id-not-in-model
29 | (is (= [["no path found for :full-name in the model"
30 | {:id :full-name}]]
31 | (:errors (validation/validate-schema {:model [[:user {:id :user}
32 | [:first-name {:id :fname}]
33 | [:last-name {:id :lname}]]]
34 | :events [{:inputs [:fname :lname]
35 | :outputs [:full-name]
36 | :handler (fn [_ _ _])}]})))))
37 |
38 | (deftest valid-ctx
39 | (is (empty? (:errors (validation/validate-schema {:model [[:user {:id :user}
40 | [:first-name {:id :fname}]
41 | [:last-name {:id :lname}]
42 | [:full-name {:id :full-name}]]
43 | [:user-hex {:id :user-hex}]]
44 |
45 | :effects [{:inputs [:fname :lname :full-name]
46 | :handler (fn [_ [fname lname full-name]]
47 | )}
48 |
49 | {:inputs [:user-hex]
50 | :handler (fn [_ [user-hex]]
51 | )}]
52 |
53 | :events [{:inputs [:fname :lname]
54 | :outputs [:full-name]
55 | :handler (fn [_ [fname lname] _]
56 | [(or (when (and fname lname) (str lname ", " fname))
57 | fname
58 | lname)])}
59 |
60 | {:inputs [:user]
61 | :outputs [:user-hex]
62 | :handler (fn [_ [{:keys [first-name last-name full-name]
63 | :or {first-name "" last-name "" full-name ""}}] _]
64 | [(->> (str first-name last-name full-name)
65 | (map ->hex)
66 | (apply str))])}]})))))
67 |
--------------------------------------------------------------------------------
/src/domino/events.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.events
2 | (:require
3 | [domino.model :as model]
4 | [domino.util :refer [generate-sub-paths]]
5 | [clojure.set :refer [union]]))
6 |
7 | (defn get-db-paths [model db paths]
8 | (reduce
9 | (fn [id->value path]
10 | (let [parent (get-in db (butlast path))]
11 | (if (contains? parent (last path))
12 | (assoc id->value (model/id-for-path model path) (get-in db path))
13 | id->value)))
14 | {}
15 | paths))
16 |
17 | (def empty-queue
18 | #?(:clj clojure.lang.PersistentQueue/EMPTY
19 | :cljs cljs.core/PersistentQueue.EMPTY))
20 |
21 | #?(:clj
22 | (defmethod clojure.core/print-method clojure.lang.PersistentQueue
23 | [queue writer]
24 | (.write writer (str "#"))))
25 |
26 | (defn try-event
27 | ([event ctx db old-outputs] (try-event event ctx db old-outputs nil))
28 | ([{:keys [handler inputs] :as event} {:domino.core/keys [model] :as ctx} db old-outputs cb]
29 | (try
30 | (if cb
31 | (handler ctx (get-db-paths model db inputs) old-outputs
32 | (fn [result] (cb (or result old-outputs))))
33 | (or
34 | (handler ctx (get-db-paths model db inputs) old-outputs)
35 | old-outputs))
36 | (catch #?(:clj Exception :cljs js/Error) e
37 | (throw (ex-info "failed to execute event" {:event event :context ctx :db db} e))))))
38 |
39 | (defn update-ctx [ctx model old-outputs new-outputs]
40 | (reduce-kv
41 | (fn [ctx id new-value]
42 | ;;todo validate that the id matches an ide declared in outputs
43 | (if (not= (get old-outputs id) new-value)
44 | (let [path (get-in model [:id->path id])]
45 | (-> ctx
46 | (update ::changed-paths (fnil (partial reduce conj) empty-queue)
47 | (generate-sub-paths path))
48 | (update ::db assoc-in path new-value)
49 | (update ::changes conj [path new-value])))
50 | ctx))
51 | ctx
52 | new-outputs))
53 |
54 | (defn ctx-updater
55 | "Reducer that updates context with new values updated in ctx from
56 | handler of each edge. New values are only stored when they are different
57 | from old values.
58 |
59 | In changed cases, the following keys are updated:
60 | ::changed-paths => queue of affected paths
61 | ::db => temporary relevant db within context
62 | ::change-history => sequential history of changes. List of tuples of path-value pairs"
63 | [edges {::keys [db] :domino.core/keys [model] :as ctx}]
64 | (reduce
65 | (fn [ctx {{:keys [async? outputs] :as event} :edge}]
66 | (let [old-outputs (get-db-paths (:domino.core/model ctx) db outputs)]
67 | (if async?
68 | (try-event event ctx db old-outputs
69 | (fn [new-outputs]
70 | (update-ctx ctx model old-outputs new-outputs)))
71 | (update-ctx ctx model old-outputs (try-event event ctx db old-outputs)))))
72 | ctx
73 | edges))
74 |
75 | (defn input? [edge]
76 | (= :input (:relationship edge)))
77 |
78 | (defn origin-path [graph origin]
79 | (loop [origin (vec origin)]
80 | (or (when (empty? origin) ::does-not-exist)
81 | (when (contains? graph origin) origin)
82 | (recur (subvec origin 0 (dec (count origin)))))))
83 |
84 | (defn eval-traversed-edges
85 | "Given an origin and graph, update context with edges.
86 |
87 | When an node has been visited (as an input), it cannot be considered for an output"
88 | ([{::keys [changed-paths] :as ctx} graph]
89 | (let [x (peek changed-paths)
90 | xs (pop changed-paths)]
91 | ;; Select the first change to evaluate
92 | (eval-traversed-edges (assoc ctx ::changed-paths xs) graph x)))
93 | ([{::keys [changes] :as ctx} graph origin]
94 | ;; Handle the change (origin) passed in, and recur as needed
95 | (let [;; Select the relevant parent of the change
96 | focal-origin (origin-path graph origin)
97 | ;; Get the edges of type :input for the focal-origin
98 | edges (filter input? (get graph focal-origin #{}))
99 | ;; Get the new graph with the handled origin removed
100 | removed-origin (dissoc graph focal-origin)
101 | ;; Call `ctx-updater` to handle the changes associated with the given edge
102 | {::keys [changed-paths] :as new-ctx} (ctx-updater edges ctx)
103 | x (peek changed-paths)
104 | xs (pop changed-paths)]
105 | (if x
106 | (recur (assoc new-ctx ::changed-paths xs) removed-origin x)
107 | new-ctx))))
108 |
109 | (defn execute-events [{:domino.core/keys [db graph] :as ctx} inputs]
110 | (let [{::keys [db changes]} (eval-traversed-edges
111 | (reduce
112 | (fn [ctx [path value]]
113 | (-> ctx
114 | (update ::db assoc-in path value)
115 | (update ::changed-paths conj path)
116 | (update ::changes conj [path value])))
117 | (assoc ctx ::db db
118 | ::changed-paths empty-queue
119 | ;; ::executed-events #{}
120 | ::changes [])
121 | inputs)
122 | graph)]
123 | (assoc ctx :domino.core/db db
124 | :domino.core/change-history changes)))
125 |
126 | (defn events-inputs-as-changes [{:domino.core/keys [events-by-id db]} event-ids]
127 | (reduce
128 | (fn [changes event-id]
129 | (let [inputs (get-in events-by-id [event-id :inputs])]
130 | (concat changes
131 | (map (fn [path]
132 | [path (get-in db path)])
133 | inputs))))
134 | []
135 | event-ids))
136 |
--------------------------------------------------------------------------------
/test/domino/events_test.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.events-test
2 | (:require
3 | #?(:clj [clojure.test :refer :all]
4 | :cljs [cljs.test :refer-macros [is are deftest testing use-fixtures]])
5 | [domino.graph :as graph]
6 | [domino.events :as events]
7 | [domino.model :as model]
8 | [domino.core :as core]))
9 |
10 | (def test-model
11 | (model/model->paths
12 | [[:a {:id :a}]
13 | [:b {:id :b}]
14 | [:c {:id :c}]
15 | [:d {:id :d}]
16 | [:e {:id :e}]
17 | [:f {:id :f}]
18 | [:g {:id :g}]
19 | [:h {:id :h}
20 | [:i {:id :i}]]]))
21 |
22 | (def default-db {:a 0, :b 0, :c 0, :d 0, :e 0, :f 0, :g 0 :h {:i 0}})
23 |
24 | (defn test-graph-events
25 | ([events inputs expected-result]
26 | (test-graph-events default-db events inputs expected-result))
27 | ([db events inputs expected-result]
28 | (test-graph-events {} db events inputs expected-result))
29 | ([ctx db events inputs expected-result]
30 | (is
31 | (= expected-result
32 | (-> (merge
33 | ctx
34 | {::core/model test-model
35 | ::core/db db
36 | ::core/graph (graph/gen-ev-graph events)})
37 | (events/execute-events inputs)
38 | (select-keys [::core/db ::core/change-history]))))))
39 |
40 | (deftest no-events
41 | (test-graph-events
42 | []
43 | [[[:a] 1]]
44 | {::core/db (assoc default-db :a 1)
45 | ::core/change-history [[[:a] 1]]}))
46 |
47 | (deftest nil-output-ignored
48 | (test-graph-events
49 | [{:inputs [[:a]]
50 | :outputs [[:b]]
51 | :handler (fn [_ _ _])}]
52 | [[[:a] 1]]
53 | {::core/db (assoc default-db :a 1)
54 | ::core/change-history [[[:a] 1]]}))
55 |
56 | (deftest single-input-output
57 | (test-graph-events
58 | [{:inputs [[:a]]
59 | :outputs [[:b]]
60 | :handler (fn [ctx {:keys [a]} {:keys [b]}] {:b (+ a b)})}]
61 | [[[:a] 1]]
62 | {::core/db (assoc default-db :a 1 :b 1)
63 | ::core/change-history [[[:a] 1] [[:b] 1]]}))
64 |
65 | (deftest unmatched-event
66 | (test-graph-events
67 | [{:inputs [[:a]]
68 | :outputs [[:b]]
69 | :handler (fn [ctx _ _] {:b 5})}]
70 | [[[:c] 1]]
71 | {::core/db (assoc default-db :c 1)
72 | ::core/change-history [[[:c] 1]]}))
73 |
74 | (deftest nil-value
75 | (test-graph-events
76 | [{:inputs [[:a]]
77 | :outputs [[:b]]
78 | :handler (fn [_ _ _] {:b nil})}]
79 | [[[:a] 1]]
80 | {::core/db (assoc default-db :a 1 :b nil)
81 | ::core/change-history [[[:a] 1] [[:b] nil]]}))
82 |
83 | (deftest output-dependent-event
84 | (test-graph-events
85 | [{:inputs [[:a] [:b]]
86 | :outputs [[:c]]
87 | :handler (fn [_ {:keys [a b]} _] {:c (+ a b)})}
88 | {:inputs [[:a] [:c]]
89 | :outputs [[:d]]
90 | :handler (fn [_ {:keys [a c]} _] {:d (+ a c)})}]
91 | [[[:a] 1]]
92 | {::core/db (assoc default-db :a 1 :c 1 :d 2)
93 | ::core/change-history [[[:a] 1] [[:c] 1] [[:d] 1] [[:d] 2]]}))
94 |
95 | (deftest exception-bubbles-up
96 | (is
97 | (thrown?
98 | #?(:clj clojure.lang.ExceptionInfo :cljs js/Error)
99 | (events/execute-events
100 | {::core/db default-db
101 | ::core/graph (graph/gen-ev-graph
102 | [{:inputs [[:a]]
103 | :outputs [[:b]]
104 | :handler (fn [ctx {:keys [a]} {:keys [b]}]
105 | (throw (ex-info "test" {:test :error})))}])}
106 | [[[:a] 1]]))))
107 |
108 | (deftest single-unchanged-input
109 | ;; todo might be better to not run any events if the inputs are the same as the current model
110 | (test-graph-events
111 | [{:inputs [[:a]]
112 | :outputs [[:b]]
113 | :handler (fn [ctx {:keys [a]} {:keys [b]}] {:b (inc a)})}]
114 | [[[:a] 0]]
115 | {::core/db (assoc default-db :b 1)
116 | ::core/change-history [[[:a] 0] [[:b] 1]]}))
117 |
118 | (deftest same-input-as-output
119 | (test-graph-events
120 | [{:inputs [[:a]]
121 | :outputs [[:a]]
122 | :handler (fn [ctx {:keys [a]} _] {:a (inc a)})}]
123 | [[[:a] 1]]
124 | {::core/db (assoc default-db :a 2)
125 | ::core/change-history [[[:a] 1]
126 | [[:a] 2]]}))
127 |
128 | (deftest cyclic-inputs
129 | (test-graph-events
130 | [{:inputs [[:a]]
131 | :outputs [[:b] [:c]]
132 | :handler (fn [ctx {:keys [a]} {:keys [b c]}] {:b (inc b) :c c})}
133 | {:inputs [[:b]]
134 | :outputs [[:a]]
135 | :handler (fn [ctx {:keys [b]} {:keys [a]}] {:a (inc a)})}]
136 | [[[:a] 1]]
137 | {::core/db (assoc default-db :b 1 :a 2)
138 | ::core/change-history [[[:a] 1]
139 | [[:b] 1]
140 | [[:a] 2]]})
141 | (test-graph-events
142 | [{:inputs [[:a]]
143 | :outputs [[:b] [:c]]
144 | :handler (fn [ctx {:keys [a]} {:keys [b c]}] {:b (+ a b) :c c})}
145 | {:inputs [[:b]]
146 | :outputs [[:a]]
147 | :handler (fn [ctx {:keys [b]} {:keys [a]}] {:a (+ b a)})}]
148 | [[[:a] 1] [[:b] 2]]
149 | {::core/db (assoc default-db :b 3 :a 4)
150 | ::core/change-history [[[:a] 1]
151 | [[:b] 2]
152 | [[:b] 3]
153 | [[:a] 4]]}))
154 |
155 | (deftest test-cascading-events
156 | (test-graph-events
157 | [{:inputs [[:a]]
158 | :outputs [[:b] [:c]]
159 | :handler (fn [ctx {:keys [a]} {:keys [b c]}] {:b (+ a b) :c (+ a c)})}
160 | {:inputs [[:c]]
161 | :outputs [[:d]]
162 | :handler (fn [ctx {:keys [c]} _] {:d (inc c)})}]
163 | [[[:a] 1] [[:b] 1]]
164 | {::core/db (assoc default-db :a 1 :b 2 :c 1 :d 2)
165 | ::core/change-history [[[:a] 1]
166 | [[:b] 1]
167 | [[:b] 2]
168 | [[:c] 1]
169 | [[:d] 2]]}))
170 |
171 | (deftest multi-input-event
172 | (test-graph-events
173 | [{:inputs [[:a] [:b]]
174 | :outputs [[:c]]
175 | :handler (fn [ctx {:keys [a b]} {:keys [c]}] {:c (+ a b)})}]
176 | [[[:a] 1] [[:b] 1]]
177 | {::core/db (assoc default-db :a 1 :b 1 :c 2)
178 | ::core/change-history [[[:a] 1] [[:b] 1] [[:c] 2]]}))
179 |
180 | (deftest multi-output-event
181 | (test-graph-events
182 | [{:inputs [[:a]]
183 | :outputs [[:b] [:c]]
184 | :handler (fn [ctx {:keys [a]} {:keys [b c]}] {:b (+ a b) :c (inc c)})}]
185 | [[[:a] 1]]
186 | {::core/db (assoc default-db :a 1 :b 1 :c 1)
187 | ::core/change-history [[[:a] 1] [[:b] 1] [[:c] 1]]}))
188 |
189 | (deftest multi-input-output-event-omitted-unchanged-results
190 | (test-graph-events
191 | [{:inputs [[:a] [:b]]
192 | :outputs [[:c] [:d] [:e]]
193 | :handler (fn [ctx {:keys [a b]} _] {:c (+ a b)})}]
194 | [[[:a] 1] [[:b] 1]]
195 | {::core/db (assoc default-db :a 1 :b 1 :c 2)
196 | ::core/change-history [[[:a] 1]
197 | [[:b] 1]
198 | [[:c] 2]]}))
199 |
200 | (deftest multi-input-output-event
201 | (test-graph-events
202 | [{:inputs [[:a] [:b]]
203 | :outputs [[:c] [:d] [:e]]
204 | :handler (fn [ctx {:keys [a b]} {:keys [d e]}] {:c (+ a b) :d d :e e})}]
205 | [[[:a] 1] [[:b] 1]]
206 | {::core/db (assoc default-db :a 1 :c 2 :b 1)
207 | ::core/change-history [[[:a] 1]
208 | [[:b] 1]
209 | [[:c] 2]]}))
210 |
211 | (deftest unrelated-events
212 | (test-graph-events
213 | [{:inputs [[:a]]
214 | :outputs [[:b]]
215 | :handler (fn [ctx {:keys [a]} _] {:b (inc a)})}
216 | {:inputs [[:c]]
217 | :outputs [[:d]]
218 | :handler (fn [ctx {:keys [c]} _] {:d (dec c)})}]
219 | [[[:a] 1]]
220 | {::core/db (assoc default-db :a 1 :b 2)
221 | ::core/change-history [[[:a] 1] [[:b] 2]]}))
222 |
223 | (deftest context-access
224 | (test-graph-events
225 | {:action #(+ % 5)}
226 | default-db
227 | [{:inputs [[:a]]
228 | :outputs [[:b]]
229 | :handler (fn [ctx {:keys [a]} _] {:b ((:action ctx) a)})}]
230 | [[[:a] 1]]
231 | {::core/db (assoc default-db :a 1 :b 6)
232 | ::core/change-history [[[:a] 1] [[:b] 6]]}))
233 |
234 | (deftest triggering-sub-path
235 | (test-graph-events
236 | [{:inputs [[:h]]
237 | :outputs [[:h]]
238 | :handler (fn [ctx {h :h} {old-h :h}]
239 | {:h (update old-h :i + (:i h))})}]
240 | [[[:h :i] 1]] ;; [[[:h] {:i 2}]]
241 | {::core/db (assoc default-db :h {:i 2})
242 | ::core/change-history [[[:h :i] 1]
243 | [[:h] {:i 2}]]}))
244 |
--------------------------------------------------------------------------------
/docs/resources/codemirror.css:
--------------------------------------------------------------------------------
1 | /* BASICS */
2 |
3 | .CodeMirror {
4 | /* Set height, width, borders, and global font properties here */
5 | font-family: monospace;
6 | height: 100%;
7 | color: #333;
8 | border: solid 1px #90B4FE;
9 | background: white;
10 | }
11 |
12 | /* PADDING */
13 |
14 | .CodeMirror-lines {
15 | padding: 4px 0; /* Vertical padding around content */
16 | }
17 | .CodeMirror pre {
18 | padding: 0 4px; /* Horizontal padding of content */
19 | }
20 |
21 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
22 | background-color: white; /* The little square between H and V scrollbars */
23 | }
24 |
25 | /* GUTTER */
26 |
27 | .CodeMirror-gutters {
28 | border-right: 1px solid #ddd;
29 | background-color: #f7f7f7;
30 | white-space: nowrap;
31 | }
32 | .CodeMirror-linenumbers {}
33 | .CodeMirror-linenumber {
34 | padding: 0 3px 0 5px;
35 | min-width: 20px;
36 | text-align: right;
37 | color: #999;
38 | white-space: nowrap;
39 | }
40 |
41 | .CodeMirror-guttermarker { color: black; }
42 | .CodeMirror-guttermarker-subtle { color: #999; }
43 |
44 | /* CURSOR */
45 |
46 | .CodeMirror-cursor {
47 | border-left: 1px solid black;
48 | border-right: none;
49 | width: 0;
50 | }
51 | /* Shown when moving in bi-directional text */
52 | .CodeMirror div.CodeMirror-secondarycursor {
53 | border-left: 1px solid silver;
54 | }
55 | .cm-fat-cursor .CodeMirror-cursor {
56 | width: auto;
57 | border: 0;
58 | background: #7e7;
59 | }
60 | .cm-fat-cursor div.CodeMirror-cursors {
61 | z-index: 1;
62 | }
63 |
64 | .cm-animate-fat-cursor {
65 | width: auto;
66 | border: 0;
67 | -webkit-animation: blink 1.06s steps(1) infinite;
68 | -moz-animation: blink 1.06s steps(1) infinite;
69 | animation: blink 1.06s steps(1) infinite;
70 | background-color: #7e7;
71 | }
72 | @-moz-keyframes blink {
73 | 0% {}
74 | 50% { background-color: transparent; }
75 | 100% {}
76 | }
77 | @-webkit-keyframes blink {
78 | 0% {}
79 | 50% { background-color: transparent; }
80 | 100% {}
81 | }
82 | @keyframes blink {
83 | 0% {}
84 | 50% { background-color: transparent; }
85 | 100% {}
86 | }
87 |
88 | /* Can style cursor different in overwrite (non-insert) mode */
89 | .CodeMirror-overwrite .CodeMirror-cursor {}
90 |
91 | .cm-tab { display: inline-block; text-decoration: inherit; }
92 |
93 | .CodeMirror-ruler {
94 | border-left: 1px solid #ccc;
95 | position: absolute;
96 | }
97 |
98 | /* DEFAULT THEME */
99 |
100 | .cm-s-default .cm-header {color: blue;}
101 | .cm-s-default .cm-quote {color: #090;}
102 | .cm-negative {color: #d44;}
103 | .cm-positive {color: #292;}
104 | .cm-header, .cm-strong {font-weight: bold;}
105 | .cm-em {font-style: italic;}
106 | .cm-link {text-decoration: underline;}
107 | .cm-strikethrough {text-decoration: line-through;}
108 |
109 | .cm-s-default .cm-keyword {
110 | color: #880000;
111 | font-weight: bold;
112 | }
113 | .cm-s-default .cm-atom {color: #01808e;}
114 | .cm-s-default .cm-number {color: #008800;}
115 | .cm-s-default .cm-def {color: #00f;}
116 | .cm-s-default .cm-variable,
117 | .cm-s-default .cm-punctuation,
118 | .cm-s-default .cm-property,
119 | .cm-s-default .cm-operator {}
120 | .cm-s-default .cm-variable-2 {}
121 | .cm-s-default .cm-variable-3 {}
122 | .cm-s-default .cm-comment {color: #a50;}
123 | .cm-s-default .cm-string {color: #770000;}
124 | .cm-s-default .cm-string-2 {color: #770000;}
125 | .cm-s-default .cm-meta {color: #555;}
126 | .cm-s-default .cm-qualifier {color: #555;}
127 | .cm-s-default .cm-builtin {
128 | color: #880000;
129 | font-weight: bold;
130 | }
131 | .cm-s-default .cm-bracket {}
132 | .cm-s-default .cm-tag {color: #170;}
133 | .cm-s-default .cm-attribute {color: #00c;}
134 | .cm-s-default .cm-hr {color: #999;}
135 | .cm-s-default .cm-link {color: #00c;}
136 |
137 | .cm-s-default .cm-error {color: #f00;}
138 | .cm-invalidchar {color: #f00;}
139 |
140 | .CodeMirror-composing { border-bottom: 2px solid; }
141 |
142 | /* Default styles for common addons */
143 |
144 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
145 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
146 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
147 | .CodeMirror-activeline-background {background: #e8f2ff;}
148 |
149 | /* STOP */
150 |
151 | /* The rest of this file contains styles related to the mechanics of
152 | the editor. You probably shouldn't touch them. */
153 |
154 | .CodeMirror {
155 | position: relative;
156 | overflow: hidden;
157 | }
158 |
159 | .CodeMirror-scroll {
160 | overflow: scroll !important; /* Things will break if this is overridden */
161 | /* 30px is the magic margin used to hide the element's real scrollbars */
162 | /* See overflow: hidden in .CodeMirror */
163 | margin-bottom: -30px; margin-right: -30px;
164 | padding-bottom: 30px;
165 | height: 100%;
166 | outline: none; /* Prevent dragging from highlighting the element */
167 | position: relative;
168 | }
169 | .CodeMirror-sizer {
170 | position: relative;
171 | border-right: 30px solid transparent;
172 | }
173 |
174 | /* The fake, visible scrollbars. Used to force redraw during scrolling
175 | before actual scrolling happens, thus preventing shaking and
176 | flickering artifacts. */
177 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
178 | position: absolute;
179 | z-index: 6;
180 | display: none;
181 | }
182 | .CodeMirror-vscrollbar {
183 | right: 0; top: 0;
184 | overflow-x: hidden;
185 | overflow-y: scroll;
186 | }
187 | .CodeMirror-hscrollbar {
188 | bottom: 0; left: 0;
189 | overflow-y: hidden;
190 | overflow-x: scroll;
191 | }
192 | .CodeMirror-scrollbar-filler {
193 | right: 0; bottom: 0;
194 | }
195 | .CodeMirror-gutter-filler {
196 | left: 0; bottom: 0;
197 | }
198 |
199 | .CodeMirror-gutters {
200 | position: absolute; left: 0; top: 0;
201 | z-index: 3;
202 | }
203 | .CodeMirror-gutter {
204 | white-space: normal;
205 | height: 100%;
206 | display: inline-block;
207 | margin-bottom: -30px;
208 | /* Hack to make IE7 behave */
209 | *zoom:1;
210 | *display:inline;
211 | }
212 | .CodeMirror-gutter-wrapper {
213 | position: absolute;
214 | z-index: 4;
215 | background: none !important;
216 | border: none !important;
217 | }
218 | .CodeMirror-gutter-background {
219 | position: absolute;
220 | top: 0; bottom: 0;
221 | z-index: 4;
222 | }
223 | .CodeMirror-gutter-elt {
224 | position: absolute;
225 | cursor: default;
226 | z-index: 4;
227 | }
228 | .CodeMirror-gutter-wrapper {
229 | -webkit-user-select: none;
230 | -moz-user-select: none;
231 | user-select: none;
232 | }
233 |
234 | .CodeMirror-lines {
235 | cursor: text;
236 | min-height: 1px; /* prevents collapsing before first draw */
237 | }
238 | .CodeMirror pre {
239 | /* Reset some styles that the rest of the page might have set */
240 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
241 | border-width: 0;
242 | background: transparent;
243 | font-family: inherit;
244 | font-size: inherit;
245 | margin: 0;
246 | white-space: pre;
247 | word-wrap: normal;
248 | line-height: inherit;
249 | color: inherit;
250 | z-index: 2;
251 | position: relative;
252 | overflow: visible;
253 | -webkit-tap-highlight-color: transparent;
254 | }
255 | .CodeMirror-wrap pre {
256 | word-wrap: break-word;
257 | white-space: pre-wrap;
258 | word-break: normal;
259 | }
260 |
261 | .CodeMirror-linebackground {
262 | position: absolute;
263 | left: 0; right: 0; top: 0; bottom: 0;
264 | z-index: 0;
265 | }
266 |
267 | .CodeMirror-linewidget {
268 | position: relative;
269 | z-index: 2;
270 | overflow: auto;
271 | }
272 |
273 | .CodeMirror-widget {}
274 |
275 | .CodeMirror-code {
276 | outline: none;
277 | }
278 |
279 | /* Force content-box sizing for the elements where we expect it */
280 | .CodeMirror-scroll,
281 | .CodeMirror-sizer,
282 | .CodeMirror-gutter,
283 | .CodeMirror-gutters,
284 | .CodeMirror-linenumber {
285 | -moz-box-sizing: content-box;
286 | box-sizing: content-box;
287 | }
288 |
289 | .CodeMirror-measure {
290 | position: absolute;
291 | width: 100%;
292 | height: 0;
293 | overflow: hidden;
294 | visibility: hidden;
295 | }
296 |
297 | .CodeMirror-cursor { position: absolute; }
298 | .CodeMirror-measure pre { position: static; }
299 |
300 | div.CodeMirror-cursors {
301 | visibility: hidden;
302 | position: relative;
303 | z-index: 3;
304 | }
305 | div.CodeMirror-dragcursors {
306 | visibility: visible;
307 | }
308 |
309 | .CodeMirror-focused div.CodeMirror-cursors {
310 | visibility: visible;
311 | }
312 |
313 | .CodeMirror-selected { background: #d9d9d9; }
314 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
315 | .CodeMirror-crosshair { cursor: crosshair; }
316 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
317 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
318 |
319 | .cm-searching {
320 | background: #ffa;
321 | background: rgba(255, 255, 0, .4);
322 | }
323 |
324 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */
325 | .CodeMirror span { *vertical-align: text-bottom; }
326 |
327 | /* Used to force a border model for a node */
328 | .cm-force-border { padding-right: .1px; }
329 |
330 | @media print {
331 | /* Hide the cursor when printing */
332 | .CodeMirror div.CodeMirror-cursors {
333 | visibility: hidden;
334 | }
335 | }
336 |
337 | /* See issue #2901 */
338 | .cm-tab-wrap-hack:after { content: ''; }
339 |
340 | /* Help users use markselection to safely style text background */
341 | span.CodeMirror-selectedtext { background: none; }
342 |
343 | /* rules for autocompletio add on */
344 | .CodeMirror-hints {
345 | position: absolute;
346 | z-index: 10;
347 | overflow: hidden;
348 | list-style: none;
349 |
350 | margin: 0;
351 | padding: 2px;
352 |
353 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
354 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
355 | box-shadow: 2px 3px 5px rgba(0,0,0,.2);
356 | border-radius: 3px;
357 | border: 1px solid silver;
358 |
359 | background: white;
360 | font-size: 90%;
361 | font-family: monospace;
362 |
363 | max-height: 20em;
364 | overflow-y: auto;
365 | }
366 |
367 | .CodeMirror-hint {
368 | margin: 0;
369 | padding: 0 4px;
370 | border-radius: 2px;
371 | white-space: pre;
372 | color: black;
373 | cursor: pointer;
374 | }
375 |
376 | li.CodeMirror-hint-active {
377 | background: #08f;
378 | color: white;
379 | }
380 | /* rules for autocompletio add on */
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of New York and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
215 |
--------------------------------------------------------------------------------
/test/domino/core_test.cljc:
--------------------------------------------------------------------------------
1 | (ns domino.core-test
2 | (:require
3 | [domino.core :as core]
4 | [domino.effects]
5 | [domino.graph]
6 | #?(:clj [clojure.test :refer :all]
7 | :cljs [cljs.test :refer-macros [is are deftest testing use-fixtures]])))
8 |
9 | (defn ->hex [s]
10 | #?(:clj (format "%02x" (int s))
11 | :cljs (.toString (.charCodeAt s 0) 16)))
12 |
13 | (defn init-ctx [state]
14 | (atom
15 | (core/initialize {:model [[:user {:id :user}
16 | [:first-name {:id :fname}]
17 | [:last-name {:id :lname}]
18 | [:full-name {:id :full-name}]]
19 | [:user-hex {:id :user-hex}]]
20 |
21 | :effects [{:inputs [:fname :lname :full-name]
22 | :handler (fn [_ {:keys [fname lname full-name]}]
23 | (swap! state assoc
24 | :first-name fname
25 | :last-name lname
26 | :full-name full-name))}
27 | {:inputs [:user-hex]
28 | :handler (fn [_ {:keys [user-hex]}]
29 | (swap! state assoc :user-hex user-hex))}]
30 |
31 | :events [{:inputs [:fname :lname]
32 | :outputs [:full-name]
33 | :handler (fn [_ {:keys [fname lname]} _]
34 | {:full-name (or (when (and fname lname) (str lname ", " fname))
35 | fname
36 | lname)})}
37 | {:inputs [:user]
38 | :outputs [:user-hex]
39 | :handler (fn [_ {{:keys [first-name last-name full-name]
40 | :or {first-name "" last-name "" full-name ""}} :user} _]
41 | {:user-hex (->> (str first-name last-name full-name)
42 | (map ->hex)
43 | (apply str))})}]}
44 | {})))
45 |
46 | (deftest transaction-test
47 | (let [external-state (atom {})
48 | ctx (init-ctx external-state)]
49 | (swap! ctx core/transact [[[:user :first-name] "Bob"]])
50 | (is (= {:first-name "Bob" :last-name nil :full-name "Bob" :user-hex "426f62426f62"} @external-state))
51 | (swap! ctx core/transact [[[:user :last-name] "Bobberton"]])
52 | (is (= {:first-name "Bob" :last-name "Bobberton" :full-name "Bobberton, Bob" :user-hex "426f62426f62626572746f6e426f62626572746f6e2c20426f62"} @external-state))))
53 |
54 | (deftest triggering-parent-test
55 | (let [result (atom nil)
56 | ctx (core/initialize {:model [[:foo {:id :foo}
57 | [:bar {:id :bar}]]
58 | [:baz {:id :baz}]
59 | [:buz {:id :buz}]]
60 | :events [{:inputs [:baz]
61 | :outputs [:bar]
62 | :handler (fn [ctx {:keys [baz]} _]
63 | {:bar (inc baz)})}
64 | {:inputs [:foo]
65 | :outputs [:buz]
66 | :handler (fn [ctx {:keys [foo]} _]
67 | {:buz (inc (:bar foo))})}]
68 | :effects [{:inputs [:foo]
69 | :handler (fn [ctx {:keys [foo]}]
70 | (reset! result foo))}]})]
71 | (is (= {:baz 1, :foo {:bar 2}, :buz 3} (:domino.core/db (core/transact ctx [[[:baz] 1]]))))
72 | (is (= {:bar 2} @result))))
73 |
74 | (deftest run-events-on-init
75 | (let [ctx (core/initialize {:model [[:foo {:id :foo}
76 | [:bar {:id :bar}]]
77 | [:baz {:id :baz}]
78 | [:buz {:id :buz}]]
79 | :events [{:inputs [:baz]
80 | :outputs [:bar]
81 | :handler (fn [ctx {:keys [baz]} _]
82 | {:bar (inc baz)})}
83 | {:inputs [:foo]
84 | :outputs [:buz]
85 | :handler (fn [ctx {:keys [foo]} _]
86 | {:buz (inc (:bar foo))})}]}
87 | {:baz 1})]
88 | (is (= {:foo {:bar 2} :baz 1 :buz 3} (:domino.core/db ctx)))))
89 |
90 | (deftest trigger-effects-test
91 | (let [ctx (core/initialize {:model [[:n {:id :n}]
92 | [:m {:id :m}]]
93 | :effects [{:id :match-n
94 | :outputs [:m]
95 | :handler (fn [_ _]
96 | {:m 10})}]}
97 | {:n 10 :m 0})]
98 | (is (= {:n 10 :m 10} (:domino.core/db (core/trigger-effects ctx [:match-n]))))))
99 |
100 | (deftest trigger-effect-to-update-existing-value
101 | (let [ctx (core/initialize {:model [[:total {:id :total}]]
102 | :effects [{:id :increment-total
103 | :outputs [:total]
104 | :handler (fn [_ current-state]
105 | (update current-state :total inc))}]}
106 | {:total 0})]
107 | (is (= {:total 1} (:domino.core/db (core/trigger-effects ctx [:increment-total]))))))
108 |
109 | (deftest trigger-effects-without-input
110 | (let [ctx (core/initialize {:model [[:foo
111 | [:o {:id :o}]
112 | [:p {:id :p}]]
113 | [:n {:id :n}]
114 | [:m {:id :m}]]
115 | :effects [{:id :match-n
116 | :outputs [:m :n]
117 | :handler (fn [_ {:keys [n]}]
118 | {:m n})}
119 | {:id :match-deep
120 | :outputs [:o :p]
121 | :handler (fn [_ {:keys [p]}]
122 | {:o p})}]}
123 | {:n 10 :m 0 :foo {:p 20}})]
124 | (is (= {:n 10 :m 10 :foo {:p 20}} (:domino.core/db (core/trigger-effects ctx [:match-n]))))
125 | (is (= {:n 10 :m 0 :foo {:p 20 :o 20}} (:domino.core/db (core/trigger-effects ctx [:match-deep]))))))
126 |
127 | (deftest pre-post-interceptors
128 | (let [result (atom nil)
129 | ctx (core/initialize {:model [[:foo {:id :foo
130 | :pre [(fn [handler]
131 | (fn [ctx inputs outputs]
132 | (handler ctx inputs outputs)))
133 | (fn [handler]
134 | (fn [ctx inputs outputs]
135 | (handler ctx inputs outputs)))]}
136 | [:bar {:id :bar}]]
137 | [:baz {:id :baz
138 | :pre [(fn [handler]
139 | (fn [ctx inputs outputs]
140 | (handler ctx inputs outputs)))]
141 | :post [(fn [handler]
142 | (fn [result]
143 | (handler (update result :buz inc))))]}]
144 | [:buz {:id :buz}]]
145 | :events [{:inputs [:foo :baz]
146 | :outputs [:buz]
147 | :handler (fn [ctx {:keys [baz]} _]
148 | {:buz (inc baz)})}]
149 | :effects [{:inputs [:buz]
150 | :handler (fn [ctx {:keys [buz]}]
151 | (reset! result buz))}]})]
152 | (is (= {:baz 1 :buz 3} (:domino.core/db (core/transact ctx [[[:baz] 1]]))))
153 | (is (= 3 @result))))
154 |
155 | (deftest interceptor-short-circuit
156 | (let [result (atom nil)
157 | ctx (core/initialize {:model [[:foo {:id :foo
158 | :pre [(fn [handler]
159 | (fn [ctx {:keys [baz] :as inputs} outputs]
160 | (when (> baz 2)
161 | (handler ctx inputs outputs))))]}
162 | ;; returning nil prevents handler execution
163 |
164 | [:bar {:id :bar}]]
165 | [:baz {:id :baz}]
166 | [:buz {:id :buz}]]
167 | :events [{:inputs [:foo :baz]
168 | :outputs [:buz]
169 | :handler (fn [ctx {:keys [baz]} _]
170 | {:buz (inc baz)})}]
171 | :effects [{:inputs [:buz]
172 | :handler (fn [ctx {:keys [buz]}]
173 | (reset! result buz))}]})]
174 | (is (= {:baz 1} (:domino.core/db (core/transact ctx [[[:baz] 1]]))))
175 | (is (nil? @result))))
176 |
177 | (deftest interceptor-on-parent
178 | (let [result (atom nil)
179 | ctx (core/initialize {:model [[:foo {:id :foo
180 | :pre [(fn [handler]
181 | (fn [ctx inputs outputs]
182 | (handler ctx
183 | (assoc inputs :bar 5)
184 | outputs)))]}
185 | [:bar {:id :bar}]]
186 | [:baz {:id :baz}]
187 | [:buz {:id :buz}]]
188 | :events [{:inputs [:bar :baz]
189 | :outputs [:buz]
190 | :handler (fn [ctx {:keys [bar baz]} _]
191 | {:buz (+ bar baz)})}]
192 | :effects [{:inputs [:buz]
193 | :handler (fn [ctx {:keys [buz]}]
194 | (reset! result buz))}]})]
195 | (is (= {:baz 1 :buz 6} (:domino.core/db (core/transact ctx [[[:baz] 1]]))))))
196 |
197 | (deftest no-key-at-path
198 | (let [ctx (core/initialize {:model [[:foo {:id :foo}]
199 | [:bar {:id :bar}]
200 | [:baz {:id :baz}]]
201 | :events [{:inputs [:foo :bar]
202 | :outputs [:baz]
203 | :handler (fn [ctx {:keys [foo bar] :or {foo :default}} _]
204 | {:bar (inc bar) :baz foo})}]})]
205 | (:domino.core/db (core/transact ctx [[[:bar] 1]]))
206 | (is (= {:bar 2 :baz :default} (:domino.core/db (core/transact ctx [[[:bar] 1]]))))))
207 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | [](https://circleci.com/gh/domino-clj/domino) [](https://clojars.org/domino/core) [](https://clojurians.slack.com/messages/domino-clj) [](https://clojars.org/domino/core) [](https://github.com/domino-clj/domino/stargazers)
5 |
6 |
7 | See here for interactive documentation.
8 |
9 | Domino is a data flow engine that helps you organize the interactions between your data model and events. Domino allows you to declare your business logic using a directed acyclic graph of events and effects. Whenever an external change is transacted to the data model, the graph determines the chain of events that will be executed, and side effects triggered as a result of the computation.
10 |
11 | Without a way to formalize the interactions between different parts of the application, relationships in code become implicit. This results in code that's difficult to maintain because of the mental overhead involved in tracking these relationships. Domino makes the interactions between pieces of business logic explicit and centralized.
12 |
13 | Domino explicitly separates logic that makes changes to the data model from side effectful functions. Business logic functions in Domino explicitly declare how they interact with the data model by declaring their inputs and outputs. Domino then uses these declarations to build a graphs of related events. This approach handles cascading business logic out of the box, and provides a data specification for relationships in code. Once the changes are transacted, the effectful functions are called against the new state.
14 |
15 | ## Version 0.4.0
16 |
17 | Version 0.4.0 is in pre-alpha and is not on the master branch.
18 | We do not recommend using it at this point (or even looking at it really).
19 | Please continue to use version 0.3.3 for the time being.
20 |
21 | ## Concepts
22 |
23 | Domino consists of three main concepts:
24 |
25 | ### 1. Model
26 |
27 | The model represents the paths within an EDN data structure. These paths will typically represent fields within a document. Each path entry is a tuple where the first value is the path segment, and the second value is the metadata associated with it. If the path is to be used for effects and/or events, the metadata must contain the `:id` key.
28 |
29 | For example, `[:amount {:id :amount}]` is the path entry to the `:amount` key within the data model and can be referenced in your events and effects as `:amount` (defined by the `:id`). You can nest paths within each other, such as the following model definition:
30 |
31 | ```clojure
32 | [[:patient [:first-name {:id :fname}]]]
33 | ```
34 |
35 | ### 2. Events
36 |
37 | The events define the business logic associated with the changes of the model. Whenever a value is transacted, associated events are computed. Events are defined by three keys; an `:inputs` vector, an `:outputs` vector, and a `:handler` function.
38 |
39 | The handler accepts three arguments: a context containing the current state of the engine, a list of the input values, and a list of the output values. The function should produce a vector of outputs matching the declared `:outputs` key. For example:
40 |
41 | ```clojure
42 | {:inputs [:amount]
43 | :outputs [:total]
44 | :handler (fn [ctx {:keys [amount]} {:keys [total]}]
45 | {:total (+ total amount)})}
46 | ```
47 |
48 |
49 | Domino also provides a `domino.core/event` helper for declaring events, so the above
50 | event can also be written as follows:
51 |
52 | ```clojure
53 | (domino.core/event [ctx {:keys [amount]} {:keys [total]}]
54 | {:total (+ total amount)})
55 | ```
56 |
57 | The macro requires that the `:keys` destructuring syntax is used for input and outputs, and
58 | expands the the event map with the `:inputs` and `:outputs` keys being inferred from the
59 | ones specified using the `:keys` in the event declaration.
60 |
61 | It's also possible to declare async events by providing the `:async?` key, e.g:
62 |
63 | ```clojure
64 | {:async? true
65 | :inputs [:amount]
66 | :outputs [:total]
67 | :handler (fn [ctx {:keys [amount]} {:keys [total]} callback]
68 | (callback {:total (+ total amount)}))}
69 | ```
70 |
71 | Async event handler takes an additional argument that specifies the callback function
72 | that should be called with the result.
73 |
74 | ### 3. Effects
75 |
76 | Effects are used for effectful operations, such as IO, that happen at the edges of
77 | the computation. The effects do not cascade. An effect can contain the following keys:
78 |
79 | * `:id` - optional unique identifier for the event
80 | * `:inputs` - optional set of inputs that trigger the event to run when changed
81 | * `:outputs` - optional set of outpus that the event will produce when running the handler
82 | * `:handler` - a function that handles the business logic for the effect
83 |
84 | #### Incoming Effects
85 |
86 | Effects that declare `:outputs` are used to generate the initial input to the
87 | engine. For example, an effect that injects a timestamp can look as follows:
88 |
89 | ```clojure
90 | {:id :timestamp
91 | :outputs [:ts]
92 | :handler (fn [_ {:keys [ts]}]
93 | {:ts (.getTime (java.util.Date.))})}
94 | ```
95 |
96 | The effect has an `:id` key specifying the unique identifier that is used to trigger the event
97 | by calling the `domino.core/trigger-effects` function. This function accepts a collection of
98 | event ids, e.g: `(trigger-effects ctx [:timestamp])`.
99 |
100 | The handler accepts two arguments: a context containing the current state of the engine, and a list of output values.
101 |
102 | #### Outgoing Effects
103 |
104 | Effects that declare `:inputs` will be run after events have been transacted and the new context is produced. These effects are defined as a map of `:inputs` and a `:handler` function.
105 |
106 | The handler accepts two arguments: a context containing the current state of the engine, and a list of input values.
107 |
108 | For example:
109 |
110 | ```clojure
111 | {:inputs [:total]
112 | :handler (fn [ctx {:keys [total]}]
113 | (when (> total 1337)
114 | (println "Woah. That's a lot.")))}
115 | ```
116 |
117 | ## Usage
118 |
119 | **1. Require `domino.core`**
120 |
121 |
122 | (require '[domino.core :as domino])
123 |
124 |
125 | **2. Declare your schema**
126 |
127 | Let's take a look at a simple engine that accumulates a total. Whenever an amount is set, this value is added to the current value of the total. If the total exceeds `1337` at any point, it prints out a statement that says `"Woah. That's a lot."`
128 |
129 | ```clojure lang-eval-clojure
130 | (def schema
131 | {:model [[:amount {:id :amount}]
132 | [:total {:id :total}]]
133 | :events [{:id :update-total
134 | :inputs [:amount]
135 | :outputs [:total]
136 | :handler (fn [ctx {:keys [amount]} {:keys [total]}]
137 | {:total (+ total amount)})}]
138 | :effects [{:inputs [:total]
139 | :handler (fn [ctx {:keys [total]}]
140 | (when (> total 1337)
141 | (js/alert "Woah. That's a lot.")))}]})
142 | ```
143 |
144 | This schema declaration is a map containing three keys:
145 |
146 | * The `:model` key declares the shape of the data model used by Domino.
147 | * The `:events` key contains pure functions that represent events that are triggered when their inputs change. The events produce updated values that are persisted in the state.
148 | * The `:effects` key contains the functions that produce side effects based on the updated state.
149 |
150 | Using a unified model referenced by the event functions allows us to easily tell how a particular piece of business logic is triggered.
151 |
152 | The event engine generates a direct acyclic graph (DAG) based on the `:input` keys declared by each event that's used to compute the new state in a transaction. This approach removes any ambiguity regarding when and how business logic is executed.
153 |
154 | Domino explicitly separates the code that modifies the state of the data from the code that causes side effects. This encourages keeping business logic pure and keeping the effects at the edges of the application.
155 |
156 | **3. Initialize the engine**
157 |
158 | The `schema` that we declared above provides a specification for the internal data model and the code that operates on it. Once we've created a schema, we will need to initialize the data flow engine. This is done by calling the `domino/initialize` function. This function can be called by providing a schema along with an optional initial state map. In our example, we will give it the `schema` that we defined above, and an initial value for the state with the `:total` set to `0`.
159 |
160 | ```clojure lang-eval-clojure
161 | (def ctx (atom (domino/initialize schema {:total 0})))
162 | ```
163 |
164 | Calling the `initialize` function creates a context `ctx` that's used as the initial state for the engine. The context will contain the model, events, effects, event graph, and db (state). In our example we use an atom in order to easily update the state of the engine.
165 |
166 | **4. Transact your external data changes**
167 |
168 | We can update the state of the data by calling `domino/transact` that accepts the current `ctx` along with an inputs vector, returning the updated `ctx`. The input vector is a collection of path-value pairs. For example, to set the value of `:amount` to `10`, you would pass in the following input vector `[[[:amount] 10]]`.
169 |
170 | ```clojure lang-eval-clojure
171 | (swap! ctx domino/transact [[[:amount] 10]])
172 | ```
173 |
174 | The updated `ctx` contains `:domino.core/change-history` key which is a simple vector of all the changes as they were applied to the data in execution order of the events that were triggered.
175 |
176 | ```clojure lang-eval-clojure
177 | (:domino.core/change-history @ctx)
178 | ```
179 |
180 | We can see the new context contains the updated total amount and the change history shows the order in which the changes were applied.
181 |
182 | The `:domino.core/db` key in the context will contain the updated state reflecting the changes applied by running the events.
183 |
184 | ```clojure lang-eval-clojure
185 | (:domino.core/db @ctx)
186 | ```
187 |
188 | Finally, let's update the `:amount` to a value that triggers an effect.
189 |
190 | ```clojure lang-eval-clojure
191 | (require '[reagent.core :as reagent])
192 |
193 | (defn button []
194 | [:button
195 | {:on-click #(swap! ctx domino/transact [[[:amount] 2000]])}
196 | "trigger effect"])
197 |
198 | (reagent/render-component [button] js/klipse-container)
199 | ```
200 |
201 | ### Interceptors
202 |
203 | Domino provides the ability to add interceptors pre and post event execution. Interceptors are defined in the schema's model. If there are multiple interceptors applicable, they are composed together.
204 |
205 | In the metadata map for a model key, you can add a `:pre` and `:post` key to define these interceptors.
206 | Returning a `nil` value from an interceptor will short circuit execution. For example, we could check
207 | if the context is authorized before running the events as follows:
208 |
209 | ```clojure lang-eval-clojure
210 | (let [ctx (domino/initialize
211 | {:model [[:foo {:id :foo
212 | :pre [(fn [handler]
213 | (fn [ctx inputs outputs]
214 | ;; only run the handler if ctx contains
215 | ;; :authorized key
216 | (when (:authorized ctx)
217 | (handler ctx inputs outputs))))]
218 | :post [(fn [handler]
219 | (fn [result]
220 | (handler (update result :foo #(or % -1)))))]}]]
221 | :events [{:inputs [:foo]
222 | :outputs [:foo]
223 | :handler (fn [ctx {:keys [foo]} outputs]
224 | {:foo (inc foo)})}]})]
225 | (map :domino.core/db
226 | [(domino/transact ctx [[[:foo] 0]])
227 | (domino/transact (assoc ctx :authorized true) [[[:foo] 0]])]))
228 | ```
229 |
230 | ### Triggering Effects
231 |
232 | Effects can act as inputs to the data flow engine. For example, this might happen when a button is clicked and you want a value to increment. This can be accomplished with a call to `trigger-effects`.
233 |
234 | `trigger-effects` takes a list of effects that you would like trigger and calls `transact` with the current state of the data from all the inputs of the effects. For example:
235 |
236 | ```clojure lang-eval-clojure
237 | (let [ctx
238 | (domino.core/initialize
239 | {:model [[:total {:id :total}]]
240 | :effects [{:id :increment-total
241 | :outputs [:total]
242 | :handler (fn [_ current-state]
243 | (update current-state :total inc))}]}
244 | {:total 0})]
245 |
246 | (:domino.core/db (domino.core/trigger-effects ctx [:increment-total])))
247 | ```
248 |
249 | This wraps up everything you need to know to start using Domino. You can see a more detailed example using Domino with re-frame [here](https://domino-clj.github.io/demo).
250 |
251 | ## Possible Use Cases
252 |
253 | - UI state management
254 | - FSM
255 | - Reactive systems / spreadsheet-like models
256 |
257 | ## Example App
258 |
259 | * demo applications can be found [here](https://github.com/domino-clj/examples)
260 |
261 | ## Inspirations
262 |
263 | - [re-frame](https://github.com/Day8/re-frame)
264 | - [javelin](https://github.com/hoplon/javelin)
265 | - [reitit](https://github.com/metosin/reitit)
266 |
267 | ## License
268 |
269 | Copyright © 2019
270 |
271 | Distributed under the Eclipse Public License either version 1.0 or (at
272 | your option) any later version.
273 |
--------------------------------------------------------------------------------
/docs/resources/highlight.pack.js:
--------------------------------------------------------------------------------
1 | /*! highlight.js v9.15.10 | BSD3 License | git.io/hljslicense */
2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"==typeof exports||exports.nodeType?n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs})):e(exports)}(function(a){var f=[],u=Object.keys,N={},c={},n=/^(no-?highlight|plain|text)$/i,s=/\blang(?:uage)?-([\w-]+)\b/i,t=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,r={case_insensitive:"cI",lexemes:"l",contains:"c",keywords:"k",subLanguage:"sL",className:"cN",begin:"b",beginKeywords:"bK",end:"e",endsWithParent:"eW",illegal:"i",excludeBegin:"eB",excludeEnd:"eE",returnBegin:"rB",returnEnd:"rE",relevance:"r",variants:"v",IDENT_RE:"IR",UNDERSCORE_IDENT_RE:"UIR",NUMBER_RE:"NR",C_NUMBER_RE:"CNR",BINARY_NUMBER_RE:"BNR",RE_STARTERS_RE:"RSR",BACKSLASH_ESCAPE:"BE",APOS_STRING_MODE:"ASM",QUOTE_STRING_MODE:"QSM",PHRASAL_WORDS_MODE:"PWM",C_LINE_COMMENT_MODE:"CLCM",C_BLOCK_COMMENT_MODE:"CBCM",HASH_COMMENT_MODE:"HCM",NUMBER_MODE:"NM",C_NUMBER_MODE:"CNM",BINARY_NUMBER_MODE:"BNM",CSS_NUMBER_MODE:"CSSNM",REGEXP_MODE:"RM",TITLE_MODE:"TM",UNDERSCORE_TITLE_MODE:"UTM",COMMENT:"C",beginRe:"bR",endRe:"eR",illegalRe:"iR",lexemesRe:"lR",terminators:"t",terminator_end:"tE"},b="",h={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};function _(e){return e.replace(/&/g,"&").replace(//g,">")}function E(e){return e.nodeName.toLowerCase()}function v(e,n){var t=e&&e.exec(n);return t&&0===t.index}function l(e){return n.test(e)}function g(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function R(e){var a=[];return function e(n,t){for(var r=n.firstChild;r;r=r.nextSibling)3===r.nodeType?t+=r.nodeValue.length:1===r.nodeType&&(a.push({event:"start",offset:t,node:r}),t=e(r,t),E(r).match(/br|hr|img|input/)||a.push({event:"stop",offset:t,node:r}));return t}(e,0),a}function i(e){if(r&&!e.langApiRestored){for(var n in e.langApiRestored=!0,r)e[n]&&(e[r[n]]=e[n]);(e.c||[]).concat(e.v||[]).forEach(i)}}function m(o){function s(e){return e&&e.source||e}function c(e,n){return new RegExp(s(e),"m"+(o.cI?"i":"")+(n?"g":""))}!function n(t,e){if(!t.compiled){if(t.compiled=!0,t.k=t.k||t.bK,t.k){function r(t,e){o.cI&&(e=e.toLowerCase()),e.split(" ").forEach(function(e){var n=e.split("|");a[n[0]]=[t,n[1]?Number(n[1]):1]})}var a={};"string"==typeof t.k?r("keyword",t.k):u(t.k).forEach(function(e){r(e,t.k[e])}),t.k=a}t.lR=c(t.l||/\w+/,!0),e&&(t.bK&&(t.b="\\b("+t.bK.split(" ").join("|")+")\\b"),t.b||(t.b=/\B|\b/),t.bR=c(t.b),t.endSameAsBegin&&(t.e=t.b),t.e||t.eW||(t.e=/\B|\b/),t.e&&(t.eR=c(t.e)),t.tE=s(t.e)||"",t.eW&&e.tE&&(t.tE+=(t.e?"|":"")+e.tE)),t.i&&(t.iR=c(t.i)),null==t.r&&(t.r=1),t.c||(t.c=[]),t.c=Array.prototype.concat.apply([],t.c.map(function(e){return function(n){return n.v&&!n.cached_variants&&(n.cached_variants=n.v.map(function(e){return g(n,{v:null},e)})),n.cached_variants||n.eW&&[g(n)]||[n]}("self"===e?t:e)})),t.c.forEach(function(e){n(e,t)}),t.starts&&n(t.starts,e);var i=t.c.map(function(e){return e.bK?"\\.?(?:"+e.b+")\\.?":e.b}).concat([t.tE,t.i]).map(s).filter(Boolean);t.t=i.length?c(function(e,n){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i')+n+(t?"":b):n}function o(){E+=null!=l.sL?function(){var e="string"==typeof l.sL;if(e&&!N[l.sL])return _(g);var n=e?C(l.sL,g,!0,f[l.sL]):O(g,l.sL.length?l.sL:void 0);return 0")+'"');return g+=n,n.length||1}var s=B(e);if(!s)throw new Error('Unknown language: "'+e+'"');m(s);var a,l=t||s,f={},E="";for(a=l;a!==s;a=a.parent)a.cN&&(E=c(a.cN,"",!0)+E);var g="",R=0;try{for(var d,p,M=0;l.t.lastIndex=M,d=l.t.exec(n);)p=r(n.substring(M,d.index),d[0]),M=d.index+p;for(r(n.substr(M)),a=l;a.parent;a=a.parent)a.cN&&(E+=b);return{r:R,value:E,language:e,top:l}}catch(e){if(e.message&&-1!==e.message.indexOf("Illegal"))return{r:0,value:_(n)};throw e}}function O(t,e){e=e||h.languages||u(N);var r={r:0,value:_(t)},a=r;return e.filter(B).filter(M).forEach(function(e){var n=C(e,t,!1);n.language=e,n.r>a.r&&(a=n),n.r>r.r&&(a=r,r=n)}),a.language&&(r.second_best=a),r}function d(e){return h.tabReplace||h.useBR?e.replace(t,function(e,n){return h.useBR&&"\n"===e?" ":h.tabReplace?n.replace(/\t/g,h.tabReplace):""}):e}function o(e){var n,t,r,a,i,o=function(e){var n,t,r,a,i=e.className+" ";if(i+=e.parentNode?e.parentNode.className:"",t=s.exec(i))return B(t[1])?t[1]:"no-highlight";for(n=0,r=(i=i.split(/\s+/)).length;n/g,"\n"):n=e,i=n.textContent,r=o?C(o,i,!0):O(i),(t=R(n)).length&&((a=document.createElementNS("http://www.w3.org/1999/xhtml","div")).innerHTML=r.value,r.value=function(e,n,t){var r=0,a="",i=[];function o(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function u(e){a+=""+E(e)+">"}function s(e){("start"===e.event?c:u)(e.node)}for(;e.length||n.length;){var l=o();if(a+=_(t.substring(r,l[0].offset)),r=l[0].offset,l===e){for(i.reverse().forEach(u);s(l.splice(0,1)[0]),(l=o())===e&&l.length&&l[0].offset===r;);i.reverse().forEach(c)}else"start"===l[0].event?i.push(l[0].node):i.pop(),s(l.splice(0,1)[0])}return a+_(t.substr(r))}(t,R(a),i)),r.value=d(r.value),e.innerHTML=r.value,e.className=function(e,n,t){var r=n?c[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}(e.className,o,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function p(){if(!p.called){p.called=!0;var e=document.querySelectorAll("pre code");f.forEach.call(e,o)}}function B(e){return e=(e||"").toLowerCase(),N[e]||N[c[e]]}function M(e){var n=B(e);return n&&!n.disableAutodetect}return a.highlight=C,a.highlightAuto=O,a.fixMarkup=d,a.highlightBlock=o,a.configure=function(e){h=g(h,e)},a.initHighlighting=p,a.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",p,!1),addEventListener("load",p,!1)},a.registerLanguage=function(n,e){var t=N[n]=e(a);i(t),t.aliases&&t.aliases.forEach(function(e){c[e]=n})},a.listLanguages=function(){return u(N)},a.getLanguage=B,a.autoDetection=M,a.inherit=g,a.IR=a.IDENT_RE="[a-zA-Z]\\w*",a.UIR=a.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*",a.NR=a.NUMBER_RE="\\b\\d+(\\.\\d+)?",a.CNR=a.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",a.BNR=a.BINARY_NUMBER_RE="\\b(0b[01]+)",a.RSR=a.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",a.BE=a.BACKSLASH_ESCAPE={b:"\\\\[\\s\\S]",r:0},a.ASM=a.APOS_STRING_MODE={cN:"string",b:"'",e:"'",i:"\\n",c:[a.BE]},a.QSM=a.QUOTE_STRING_MODE={cN:"string",b:'"',e:'"',i:"\\n",c:[a.BE]},a.PWM=a.PHRASAL_WORDS_MODE={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},a.C=a.COMMENT=function(e,n,t){var r=a.inherit({cN:"comment",b:e,e:n,c:[]},t||{});return r.c.push(a.PWM),r.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),r},a.CLCM=a.C_LINE_COMMENT_MODE=a.C("//","$"),a.CBCM=a.C_BLOCK_COMMENT_MODE=a.C("/\\*","\\*/"),a.HCM=a.HASH_COMMENT_MODE=a.C("#","$"),a.NM=a.NUMBER_MODE={cN:"number",b:a.NR,r:0},a.CNM=a.C_NUMBER_MODE={cN:"number",b:a.CNR,r:0},a.BNM=a.BINARY_NUMBER_MODE={cN:"number",b:a.BNR,r:0},a.CSSNM=a.CSS_NUMBER_MODE={cN:"number",b:a.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},a.RM=a.REGEXP_MODE={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[a.BE,{b:/\[/,e:/\]/,r:0,c:[a.BE]}]},a.TM=a.TITLE_MODE={cN:"title",b:a.IR,r:0},a.UTM=a.UNDERSCORE_TITLE_MODE={cN:"title",b:a.UIR,r:0},a.METHOD_GUARD={b:"\\.\\s*"+a.UIR,r:0},a});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:s.c=o}});hljs.registerLanguage("ini",function(e){var b={cN:"string",c:[e.BE],v:[{b:"'''",e:"'''",r:10},{b:'"""',e:'"""',r:10},{b:'"',e:'"'},{b:"'",e:"'"}]};return{aliases:["toml"],cI:!0,i:/\S/,c:[e.C(";","$"),e.HCM,{cN:"section",b:/^\s*\[+/,e:/\]+/},{b:/^[a-z0-9\[\]_\.-]+\s*=\s*/,e:"$",rB:!0,c:[{cN:"attr",b:/[a-z0-9\[\]_\.-]+/},{b:/=/,eW:!0,r:0,c:[e.C(";","$"),e.HCM,{cN:"literal",b:/\bon|off|true|false|yes|no\b/},{cN:"variable",v:[{b:/\$[\w\d"][\w\d_]*/},{b:/\$\{(.*?)}/}]},b,{cN:"number",b:/([\+\-]+)?[\d]+_[\d_]+/},e.NM]}]}]}});hljs.registerLanguage("ruby",function(e){var b="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?",r={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},c={cN:"doctag",b:"@[A-Za-z]+"},a={b:"#<",e:">"},s=[e.C("#","$",{c:[c]}),e.C("^\\=begin","^\\=end",{c:[c],r:10}),e.C("^__END__","\\n$")],n={cN:"subst",b:"#\\{",e:"}",k:r},t={cN:"string",c:[e.BE,n],v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/`/,e:/`/},{b:"%[qQwWx]?\\(",e:"\\)"},{b:"%[qQwWx]?\\[",e:"\\]"},{b:"%[qQwWx]?{",e:"}"},{b:"%[qQwWx]?<",e:">"},{b:"%[qQwWx]?/",e:"/"},{b:"%[qQwWx]?%",e:"%"},{b:"%[qQwWx]?-",e:"-"},{b:"%[qQwWx]?\\|",e:"\\|"},{b:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{b:/<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/,rB:!0,c:[{b:/<<[-~]?'?/},{b:/\w+/,endSameAsBegin:!0,c:[e.BE,n]}]}]},i={cN:"params",b:"\\(",e:"\\)",endsParent:!0,k:r},d=[t,a,{cN:"class",bK:"class module",e:"$|;",i:/=/,c:[e.inherit(e.TM,{b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{b:"<\\s*",c:[{b:"("+e.IR+"::)?"+e.IR}]}].concat(s)},{cN:"function",bK:"def",e:"$|;",c:[e.inherit(e.TM,{b:b}),i].concat(s)},{b:e.IR+"::"},{cN:"symbol",b:e.UIR+"(\\!|\\?)?:",r:0},{cN:"symbol",b:":(?!\\s)",c:[t,{b:b}],r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{cN:"params",b:/\|/,e:/\|/,k:r},{b:"("+e.RSR+"|unless)\\s*",k:"unless",c:[a,{cN:"regexp",c:[e.BE,n],i:/\n/,v:[{b:"/",e:"/[a-z]*"},{b:"%r{",e:"}[a-z]*"},{b:"%r\\(",e:"\\)[a-z]*"},{b:"%r!",e:"![a-z]*"},{b:"%r\\[",e:"\\][a-z]*"}]}].concat(s),r:0}].concat(s);n.c=d;var l=[{b:/^\s*=>/,starts:{e:"$",c:i.c=d}},{cN:"meta",b:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{e:"$",c:d}}];return{aliases:["rb","gemspec","podspec","thor","irb"],k:r,i:/\/\*/,c:s.concat(l).concat(d)}});hljs.registerLanguage("yaml",function(e){var b="true false yes no null",a="^[ \\-]*",r="[a-zA-Z_][\\w\\-]*",t={cN:"attr",v:[{b:a+r+":"},{b:a+'"'+r+'":'},{b:a+"'"+r+"':"}]},c={cN:"string",r:0,v:[{b:/'/,e:/'/},{b:/"/,e:/"/},{b:/\S+/}],c:[e.BE,{cN:"template-variable",v:[{b:"{{",e:"}}"},{b:"%{",e:"}"}]}]};return{cI:!0,aliases:["yml","YAML","yaml"],c:[t,{cN:"meta",b:"^---s*$",r:10},{cN:"string",b:"[\\|>] *$",rE:!0,c:c.c,e:t.v[0].b},{b:"<%[%=-]?",e:"[%-]?%>",sL:"ruby",eB:!0,eE:!0,r:0},{cN:"type",b:"!"+e.UIR},{cN:"type",b:"!!"+e.UIR},{cN:"meta",b:"&"+e.UIR+"$"},{cN:"meta",b:"\\*"+e.UIR+"$"},{cN:"bullet",b:"^ *-",r:0},e.HCM,{bK:b,k:{literal:b}},e.CNM,c]}});hljs.registerLanguage("properties",function(r){var t="[ \\t\\f]*",e="("+t+"[:=]"+t+"|[ \\t\\f]+)",s="([^\\\\\\W:= \\t\\f\\n]|\\\\.)+",n="([^\\\\:= \\t\\f\\n]|\\\\.)+",a={e:e,r:0,starts:{cN:"string",e:/$/,r:0,c:[{b:"\\\\\\n"}]}};return{cI:!0,i:/\S/,c:[r.C("^\\s*[!#]","$"),{b:s+e,rB:!0,c:[{cN:"attr",b:s,endsParent:!0,r:0}],starts:a},{b:n+e,rB:!0,r:0,c:[{cN:"meta",b:n,endsParent:!0,r:0}],starts:a},{cN:"attr",r:0,b:n+t+"$"}]}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,{cN:"",b:/\\"/},{cN:"string",b:/'/,e:/'/},t]}});hljs.registerLanguage("php",function(e){var c={b:"\\$+[a-zA-Z_-ÿ][a-zA-Z0-9_-ÿ]*"},i={cN:"meta",b:/<\?(php)?|\?>/},t={cN:"string",c:[e.BE,i],v:[{b:'b"',e:'"'},{b:"b'",e:"'"},e.inherit(e.ASM,{i:null}),e.inherit(e.QSM,{i:null})]},a={v:[e.BNM,e.CNM]};return{aliases:["php","php3","php4","php5","php6","php7"],cI:!0,k:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally",c:[e.HCM,e.C("//","$",{c:[i]}),e.C("/\\*","\\*/",{c:[{cN:"doctag",b:"@[A-Za-z]+"}]}),e.C("__halt_compiler.+?;",!1,{eW:!0,k:"__halt_compiler",l:e.UIR}),{cN:"string",b:/<<<['"]?\w+['"]?$/,e:/^\w+;?$/,c:[e.BE,{cN:"subst",v:[{b:/\$\w+/},{b:/\{\$/,e:/\}/}]}]},i,{cN:"keyword",b:/\$this\b/},c,{b:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/},{cN:"function",bK:"function",e:/[;{]/,eE:!0,i:"\\$|\\[|%",c:[e.UTM,{cN:"params",b:"\\(",e:"\\)",c:["self",c,e.CBCM,t,a]}]},{cN:"class",bK:"class interface",e:"{",eE:!0,i:/[:\(\$"]/,c:[{bK:"extends implements"},e.UTM]},{bK:"namespace",e:";",i:/[\.']/,c:[e.UTM]},{bK:"use",e:";",c:[e.UTM]},{b:"=>"},t,a]}});hljs.registerLanguage("objectivec",function(e){var t=/[a-zA-Z@][a-zA-Z0-9_]*/,_="@interface @class @protocol @implementation";return{aliases:["mm","objc","obj-c"],k:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN",literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},l:t,i:"",c:[{cN:"built_in",b:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},e.CLCM,e.CBCM,e.CNM,e.QSM,{cN:"string",v:[{b:'@"',e:'"',i:"\\n",c:[e.BE]},{b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"}]},{cN:"meta",b:"#",e:"$",c:[{cN:"meta-string",v:[{b:'"',e:'"'},{b:"<",e:">"}]}]},{cN:"class",b:"("+_.split(" ").join("|")+")\\b",e:"({|$)",eE:!0,k:_,l:t,c:[e.UTM]},{b:"\\."+e.UIR,r:0}]}});hljs.registerLanguage("apache",function(e){var r={cN:"number",b:"[\\$%]\\d+"};return{aliases:["apacheconf"],cI:!0,c:[e.HCM,{cN:"section",b:"?",e:">"},{cN:"attribute",b:/\w+/,r:0,k:{nomarkup:"order deny allow setenv rewriterule rewriteengine rewritecond documentroot sethandler errordocument loadmodule options header listen serverroot servername"},starts:{e:/$/,r:0,k:{literal:"on off all"},c:[{cN:"meta",b:"\\s\\[",e:"\\]$"},{cN:"variable",b:"[\\$%]\\{",e:"\\}",c:["self",r]},r,e.QSM]}}],i:/\S/}});hljs.registerLanguage("cs",function(e){var i={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield",literal:"null false true"},r={cN:"number",v:[{b:"\\b(0b[01']+)"},{b:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{b:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],r:0},t={cN:"string",b:'@"',e:'"',c:[{b:'""'}]},a=e.inherit(t,{i:/\n/}),c={cN:"subst",b:"{",e:"}",k:i},n=e.inherit(c,{i:/\n/}),s={cN:"string",b:/\$"/,e:'"',i:/\n/,c:[{b:"{{"},{b:"}}"},e.BE,n]},b={cN:"string",b:/\$@"/,e:'"',c:[{b:"{{"},{b:"}}"},{b:'""'},c]},l=e.inherit(b,{i:/\n/,c:[{b:"{{"},{b:"}}"},{b:'""'},n]});c.c=[b,s,t,e.ASM,e.QSM,r,e.CBCM],n.c=[l,s,a,e.ASM,e.QSM,r,e.inherit(e.CBCM,{i:/\n/})];var o={v:[b,s,t,e.ASM,e.QSM]},d=e.IR+"(<"+e.IR+"(\\s*,\\s*"+e.IR+")*>)?(\\[\\])?";return{aliases:["csharp","c#"],k:i,i:/::/,c:[e.C("///","$",{rB:!0,c:[{cN:"doctag",v:[{b:"///",r:0},{b:"\x3c!--|--\x3e"},{b:"?",e:">"}]}]}),e.CLCM,e.CBCM,{cN:"meta",b:"#",e:"$",k:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},o,r,{bK:"class interface",e:/[{;=]/,i:/[^\s:,]/,c:[e.TM,e.CLCM,e.CBCM]},{bK:"namespace",e:/[{;=]/,i:/[^\s:]/,c:[e.inherit(e.TM,{b:"[a-zA-Z](\\.?\\w)*"}),e.CLCM,e.CBCM]},{cN:"meta",b:"^\\s*\\[",eB:!0,e:"\\]",eE:!0,c:[{cN:"meta-string",b:/"/,e:/"/}]},{bK:"new return throw await else",r:0},{cN:"function",b:"("+d+"\\s+)+"+e.IR+"\\s*\\(",rB:!0,e:/\s*[{;=]/,eE:!0,k:i,c:[{b:e.IR+"\\s*\\(",rB:!0,c:[e.TM],r:0},{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,k:i,r:0,c:[o,r,e.CBCM]},e.CLCM,e.CBCM]}]}});hljs.registerLanguage("nginx",function(e){var r={cN:"variable",v:[{b:/\$\d+/},{b:/\$\{/,e:/}/},{b:"[\\$\\@]"+e.UIR}]},b={eW:!0,l:"[a-z/_]+",k:{literal:"on off yes no true false none blocked debug info notice warn error crit select break last permanent redirect kqueue rtsig epoll poll /dev/poll"},r:0,i:"=>",c:[e.HCM,{cN:"string",c:[e.BE,r],v:[{b:/"/,e:/"/},{b:/'/,e:/'/}]},{b:"([a-z]+):/",e:"\\s",eW:!0,eE:!0,c:[r]},{cN:"regexp",c:[e.BE,r],v:[{b:"\\s\\^",e:"\\s|{|;",rE:!0},{b:"~\\*?\\s+",e:"\\s|{|;",rE:!0},{b:"\\*(\\.[a-z\\-]+)+"},{b:"([a-z\\-]+\\.)+\\*"}]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b"},{cN:"number",b:"\\b\\d+[kKmMgGdshdwy]*\\b",r:0},r]};return{aliases:["nginxconf"],c:[e.HCM,{b:e.UIR+"\\s+{",rB:!0,e:"{",c:[{cN:"section",b:e.UIR}],r:0},{b:e.UIR+"\\s",e:";|{",rB:!0,c:[{cN:"attribute",b:e.UIR,starts:b}],r:0}],i:"[^\\s\\}]"}});hljs.registerLanguage("diff",function(e){return{aliases:["patch"],c:[{cN:"meta",r:10,v:[{b:/^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/},{b:/^\*\*\* +\d+,\d+ +\*\*\*\*$/},{b:/^\-\-\- +\d+,\d+ +\-\-\-\-$/}]},{cN:"comment",v:[{b:/Index: /,e:/$/},{b:/={3,}/,e:/$/},{b:/^\-{3}/,e:/$/},{b:/^\*{3} /,e:/$/},{b:/^\+{3}/,e:/$/},{b:/\*{5}/,e:/\*{5}$/}]},{cN:"addition",b:"^\\+",e:"$"},{cN:"deletion",b:"^\\-",e:"$"},{cN:"addition",b:"^\\!",e:"$"}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("java",function(e){var a="false synchronized int abstract float private char boolean var static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",t={cN:"number",b:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",r:0};return{aliases:["jsp"],k:a,i:/<\/|#/,c:[e.C("/\\*\\*","\\*/",{r:0,c:[{b:/\w+@/,r:0},{cN:"doctag",b:"@[A-Za-z]+"}]}),e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return else",r:0},{cN:"function",b:"([À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(<[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*(\\s*,\\s*[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*)*>)?\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:a,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:a,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},t,{cN:"meta",b:"@[A-Za-z]+"}]}});hljs.registerLanguage("coffeescript",function(e){var c={keyword:"in if for while finally new do return else break catch instanceof throw try this switch continue typeof delete debugger super yield import export from as default await then unless until loop of by when and or is isnt not",literal:"true false null undefined yes no on off",built_in:"npm require console print module global window document"},n="[A-Za-z$_][0-9A-Za-z$_]*",r={cN:"subst",b:/#\{/,e:/}/,k:c},i=[e.BNM,e.inherit(e.CNM,{starts:{e:"(\\s*/)?",r:0}}),{cN:"string",v:[{b:/'''/,e:/'''/,c:[e.BE]},{b:/'/,e:/'/,c:[e.BE]},{b:/"""/,e:/"""/,c:[e.BE,r]},{b:/"/,e:/"/,c:[e.BE,r]}]},{cN:"regexp",v:[{b:"///",e:"///",c:[r,e.HCM]},{b:"//[gim]*",r:0},{b:/\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/}]},{b:"@"+n},{sL:"javascript",eB:!0,eE:!0,v:[{b:"```",e:"```"},{b:"`",e:"`"}]}];r.c=i;var s=e.inherit(e.TM,{b:n}),t="(\\(.*\\))?\\s*\\B[-=]>",o={cN:"params",b:"\\([^\\(]",rB:!0,c:[{b:/\(/,e:/\)/,k:c,c:["self"].concat(i)}]};return{aliases:["coffee","cson","iced"],k:c,i:/\/\*/,c:i.concat([e.C("###","###"),e.HCM,{cN:"function",b:"^\\s*"+n+"\\s*=\\s*"+t,e:"[-=]>",rB:!0,c:[s,o]},{b:/[:\(,=]\s*/,r:0,c:[{cN:"function",b:t,e:"[-=]>",rB:!0,c:[o]}]},{cN:"class",bK:"class",e:"$",i:/[:="\[\]]/,c:[{bK:"extends",eW:!0,i:/[:="\[\]]/,c:[s]},s]},{b:n+":",e:":",rB:!0,rE:!0,r:0}])}});hljs.registerLanguage("css",function(e){var c={b:/(?:[A-Z\_\.\-]+|--[a-zA-Z0-9_-]+)\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:"[a-zA-Z-][a-zA-Z0-9_-]*",r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,c]}]}});hljs.registerLanguage("xml",function(s){var e={eW:!0,i:/,r:0,c:[{cN:"attr",b:"[A-Za-z0-9\\._:-]+",r:0},{b:/=\s*/,r:0,c:[{cN:"string",endsParent:!0,v:[{b:/"/,e:/"/},{b:/'/,e:/'/},{b:/[^\s"'=<>`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("\x3c!--","--\x3e",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"meta",b:/<\?xml/,e:/\?>/,r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0},{b:'b"',e:'"',skip:!0},{b:"b'",e:"'",skip:!0},s.inherit(s.ASM,{i:null,cN:null,c:null,skip:!0}),s.inherit(s.QSM,{i:null,cN:null,c:null,skip:!0})]},{cN:"tag",b:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"