├── resources └── public │ └── css │ └── base.css ├── logo ├── logo.png ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── mstile-70x70.png ├── apple-touch-icon.png ├── mstile-144x144.png ├── mstile-150x150.png ├── mstile-310x150.png ├── mstile-310x310.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── browserconfig.xml ├── site.webmanifest ├── logo.svg └── safari-pinned-tab.svg ├── docs ├── README.md ├── package.json ├── resources │ ├── style.css │ ├── highlight.pack.css │ ├── codemirror.css │ └── highlight.pack.js ├── templates │ └── page.html ├── compile.cljs ├── package-lock.json └── md │ └── demo.md ├── .gitignore ├── env └── dev │ ├── cljs │ └── domino │ │ ├── dev.cljs │ │ └── test_page.cljs │ ├── clj │ ├── user.clj │ └── domino │ │ └── server.clj │ └── resources │ └── public │ └── css │ └── site.css ├── src └── domino │ ├── util.cljc │ ├── visualize │ ├── mermaid.clj │ └── graphviz.clj │ ├── validation.cljc │ ├── effects.cljc │ ├── core.cljc │ ├── model.cljc │ ├── graph.cljc │ └── events.cljc ├── test └── domino │ ├── runner.cljs │ ├── util_test.cljc │ ├── effects_test.cljc │ ├── async.clj │ ├── benchmark.clj │ ├── model_test.cljc │ ├── validation_test.cljc │ ├── events_test.cljc │ └── core_test.cljc ├── CHANGES.md ├── .circleci └── config.yml ├── project.clj ├── LICENSE └── README.md /resources/public/css/base.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/logo.png -------------------------------------------------------------------------------- /logo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/favicon.ico -------------------------------------------------------------------------------- /logo/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/favicon-16x16.png -------------------------------------------------------------------------------- /logo/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/favicon-32x32.png -------------------------------------------------------------------------------- /logo/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/mstile-70x70.png -------------------------------------------------------------------------------- /logo/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/apple-touch-icon.png -------------------------------------------------------------------------------- /logo/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/mstile-144x144.png -------------------------------------------------------------------------------- /logo/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/mstile-150x150.png -------------------------------------------------------------------------------- /logo/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/mstile-310x150.png -------------------------------------------------------------------------------- /logo/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/mstile-310x310.png -------------------------------------------------------------------------------- /logo/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/android-chrome-192x192.png -------------------------------------------------------------------------------- /logo/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domino-clj/domino/HEAD/logo/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### Docs 2 | 3 | The script in `compile.cljs` will compile this repo's `README.md` into HTML and spit out the result in `out/index.html`. Markdown files in the `md` folder will also be converted into HTML pages. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | figwheel_server.log 4 | *.iml 5 | .nrepl-port 6 | .lein-failures 7 | .rebel_readline_history 8 | .lein-repl-history 9 | pom.xml 10 | node_modules/ 11 | out/ 12 | docs/out/ 13 | pom.xml.asc 14 | build.xml 15 | -------------------------------------------------------------------------------- /env/dev/cljs/domino/dev.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:figwheel-no-load domino.dev 2 | (:require 3 | [domino.test-page :as test-page] 4 | [devtools.core :as devtools])) 5 | 6 | (devtools/install!) 7 | 8 | (enable-console-print!) 9 | 10 | (test-page/init!) 11 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "domino-docs", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "lumo-cljs": "1.10.1", 6 | "nunjucks": "3.2.4", 7 | "markdown-clj": "1.10.1", 8 | "fs-extra": "9.0.0" 9 | }, 10 | "scripts": { 11 | "start": "./compile.cljs" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /logo/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #74a995 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /env/dev/clj/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require 3 | [figwheel-sidecar.repl-api :as ra] 4 | [clojure.tools.namespace.repl :as repl])) 5 | 6 | (def refresh repl/refresh) 7 | 8 | (defn start-fw [] 9 | (ra/start-figwheel!)) 10 | 11 | (defn stop-fw [] 12 | (ra/stop-figwheel!)) 13 | 14 | (defn cljs [] 15 | (ra/cljs-repl)) 16 | -------------------------------------------------------------------------------- /src/domino/util.cljc: -------------------------------------------------------------------------------- 1 | (ns domino.util) 2 | 3 | (defn generate-sub-paths 4 | "Given a `path`, generate a list of all sub-paths including `path`" 5 | [path] 6 | (loop [paths [] 7 | path path] 8 | (if (not-empty path) 9 | (recur (conj paths (vec path)) (drop-last path)) 10 | paths))) 11 | 12 | (defn map-by-id [items] 13 | (->> items 14 | (filter #(contains? % :id)) 15 | (map (juxt :id identity)) 16 | (into {}))) -------------------------------------------------------------------------------- /test/domino/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns domino.runner 2 | (:require 3 | [doo.runner :refer-macros [doo-tests]] 4 | [domino.core-test] 5 | [domino.effects-test] 6 | [domino.events-test] 7 | [domino.model-test] 8 | [domino.util-test] 9 | [domino.validation-test])) 10 | 11 | (doo-tests 'domino.core-test 12 | 'domino.effects-test 13 | 'domino.events-test 14 | 'domino.model-test 15 | 'domino.util-test 16 | 'domino.validation-test) 17 | -------------------------------------------------------------------------------- /logo/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /env/dev/resources/public/css/site.css: -------------------------------------------------------------------------------- 1 | .body-container { 2 | font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif; 3 | max-width: 60vw; 4 | margin: 0 auto; 5 | padding-top: 72px; 6 | -webkit-font-smoothing: antialiased; 7 | font-size: 1.125em; 8 | color: #333; 9 | line-height: 1.5em; 10 | } 11 | 12 | h1, h2, h3 { 13 | color: #000; 14 | } 15 | h1 { 16 | font-size: 2.5em 17 | } 18 | 19 | h2 { 20 | font-size: 2em 21 | } 22 | 23 | h3 { 24 | font-size: 1.5em 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | color: #09f; 30 | } 31 | 32 | a:hover { 33 | text-decoration: underline; 34 | } 35 | -------------------------------------------------------------------------------- /logo/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/domino/visualize/mermaid.clj: -------------------------------------------------------------------------------- 1 | (ns domino.visualize.mermaid 2 | (:require 3 | [clojure.string :as string])) 4 | 5 | (defn- clean-arg [s] 6 | (-> s str 7 | (.replace "-" "_") 8 | (.replace ":" ""))) 9 | 10 | (defn format-md [s & args] 11 | (apply format s (map clean-arg args))) 12 | 13 | (defn- node [{:keys [id handler inputs outputs]}] 14 | (concat 15 | (for [input inputs] 16 | (format-md "%s --> %s" input (or id handler))) 17 | (for [output outputs] 18 | (format-md "%s --> %s" (or id handler) output)))) 19 | 20 | (defn- nodes [{:keys [events effects]}] 21 | (mapcat node (concat events effects))) 22 | 23 | (defn state-diagram [schema] 24 | (string/join "\n" 25 | (conj (nodes schema) "stateDiagram-v2"))) 26 | -------------------------------------------------------------------------------- /test/domino/util_test.cljc: -------------------------------------------------------------------------------- 1 | (ns domino.util-test 2 | (:require [domino.util :as util] 3 | #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [is are deftest testing use-fixtures]]))) 5 | 6 | (deftest generate-sub-paths-test 7 | (is (= (util/generate-sub-paths [:child]) (list [:child]))) 8 | (is (= (util/generate-sub-paths [:parent :child]) (list [:parent :child] 9 | [:parent]))) 10 | (is (= (util/generate-sub-paths [:grand-parent :parent :child]) (list [:grand-parent :parent :child] 11 | [:grand-parent :parent] 12 | [:grand-parent])))) 13 | -------------------------------------------------------------------------------- /test/domino/effects_test.cljc: -------------------------------------------------------------------------------- 1 | (ns domino.effects-test 2 | (:require 3 | [domino.effects :as effects] 4 | [domino.model :as model] 5 | #?(:clj [clojure.test :refer :all] 6 | :cljs [cljs.test :refer-macros [is are deftest testing use-fixtures]]))) 7 | 8 | (deftest effects-test 9 | (let [data (atom nil)] 10 | (effects/execute-effects! 11 | {:domino.core/change-history [[[:a] 1] [[:b] 1]] 12 | :domino.core/db {:a 1 :b 1} 13 | :domino.core/model (model/model->paths [[:a {:id :a}] 14 | [:b {:id :b}]]) 15 | :domino.core/effects 16 | (effects/effects-by-paths 17 | [{:inputs [[:a]] :handler (fn [ctx inputs] 18 | (reset! data inputs))}])}) 19 | (is (= {:a 1} @data)))) 20 | -------------------------------------------------------------------------------- /src/domino/validation.cljc: -------------------------------------------------------------------------------- 1 | (ns domino.validation) 2 | 3 | (defn check-valid-model [{:keys [model] :as ctx}] 4 | (reduce 5 | (fn [{:keys [path-ids] :as ctx} item] 6 | (if (map? item) 7 | (if-let [id (:id item)] 8 | (if (contains? path-ids id) 9 | (update ctx :errors conj [(str "duplicate id " id " in the model") {:id id}]) 10 | (update ctx :path-ids conj id))) 11 | ctx)) 12 | ctx 13 | (mapcat flatten model))) 14 | 15 | (defn check-valid-events [{:keys [events path-ids] :as ctx}] 16 | (let [id-in-path? (partial contains? path-ids)] 17 | (reduce 18 | (fn [ctx id] 19 | (if (id-in-path? id) 20 | ctx 21 | (update ctx :errors conj [(str "no path found for " id " in the model") {:id id}]))) 22 | ctx 23 | (mapcat (comp flatten (juxt :inputs :outputs)) events)))) 24 | 25 | (defn maybe-throw-exception [{:keys [errors]}] 26 | (when (not-empty errors) 27 | (throw (ex-info (str "errors found while validating schema") {:errors errors})))) 28 | 29 | (defn validate-schema [ctx] 30 | (-> ctx 31 | (assoc :path-ids #{} 32 | :errors []) 33 | check-valid-model 34 | check-valid-events)) -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ### 0.3.2 2 | 3 | - - **[BREAKING]** namespaced `:change-history` key as `:domino.core/change-history` 4 | 5 | ### 0.3.1 6 | 7 | - added `domino.core/event` macro helper for declaring events 8 | 9 | ### 0.3.0 10 | - fix: trigger effect with nested model 11 | - `trigger-events` removed, `trigger-effects` should be used instead 12 | - events will now be executed by the `initialize` function when initial state is provided 13 | 14 | ### 0.2.1 15 | - async event support 16 | - `trigger-effects` fn added to `domino.core` allowing triggering of effects via ids 17 | 18 | ### 0.2.0 19 | - **[BREAKING]** renamed `initialize!` to `initialize` since it's a pure function 20 | - **[BREAKING]** inputs and outputs for events are now maps containing the keys 21 | specified in the `:inputs` and `:outputs` vectors 22 | - **[BREAKING]** event handler functions now must return a map with keys 23 | matching the keys specified in the `:outputs` 24 | - updated the model parser to handle segments without an options map 25 | - introduces `:pre` and `:post` conditions 26 | - `trigger-events` fn added to `domino.core`, allowing triggering events via ids 27 | - add schema definition validation 28 | - validate for duplicate ids in model 29 | -------------------------------------------------------------------------------- /env/dev/clj/domino/server.clj: -------------------------------------------------------------------------------- 1 | (ns domino.server 2 | (:require [compojure.core :refer [GET defroutes]] 3 | [compojure.route :refer [not-found resources]] 4 | [hiccup.page :refer [include-js include-css html5]] 5 | [ring.middleware.reload :refer [wrap-reload]] 6 | [ring.util.response :as response])) 7 | 8 | (def mount-target 9 | [:div#app 10 | [:h3 "ClojureScript has not been compiled!"] 11 | [:p "please run " 12 | [:b "lein figwheel"] 13 | " in order to start the compiler"]]) 14 | 15 | (defn head [] 16 | [:head 17 | [:meta {:charset "utf-8"}] 18 | [:meta {:name "viewport" 19 | :content "width=device-width, initial-scale=1"}] 20 | (include-css "/css/base.css" 21 | "/css/site.css")]) 22 | 23 | (defn loading-page [] 24 | (html5 25 | (head) 26 | [:body {:class "body-container"} 27 | mount-target 28 | (include-js "/js/app.js")])) 29 | 30 | (defroutes routes 31 | (GET "/" [] 32 | (-> (response/response (loading-page)) 33 | (response/content-type "text/html"))) 34 | (resources "/" {:root "public"}) 35 | (not-found "Not Found")) 36 | 37 | (def app (wrap-reload #'routes {:dir ["env/dev/clj"]})) 38 | -------------------------------------------------------------------------------- /docs/resources/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Verdana, Helvetica, Arial, sans-serif; 3 | max-width: 800px; 4 | margin: 0 auto; 5 | padding: 72px 0; 6 | -webkit-font-smoothing: antialiased; 7 | font-size: 1.125em; 8 | color: #333; 9 | line-height: 1.5em; 10 | } 11 | 12 | h1, h2, h3, h4, h5, h6 { 13 | line-height: 1; 14 | } 15 | 16 | h1 { 17 | font-size: 2.5em 18 | } 19 | 20 | h2 { 21 | font-size: 1.85em; 22 | padding-bottom: 0.3em; 23 | border-bottom: 1px solid #eaecef; 24 | } 25 | 26 | h3 { 27 | font-size: 1.35em 28 | } 29 | 30 | h4 { 31 | font-size: 1.2em 32 | } 33 | 34 | h5 { 35 | font-size: 1.15em 36 | } 37 | 38 | h6 { 39 | font-size: 1.1em 40 | } 41 | 42 | a { 43 | text-decoration: none; 44 | color: #4165a2; 45 | } 46 | 47 | a:hover { 48 | border-bottom: 1px dotted black; 49 | } 50 | pre { 51 | font-size: 16px; 52 | padding: 16px 18px; 53 | border-radius: 5px; 54 | background-color: rgb(240, 242, 245); 55 | } 56 | 57 | *:not(pre) > code { 58 | font-size: 1.15em; 59 | padding: 3px 5px; 60 | border-radius: 5px; 61 | background-color: rgb(240, 242, 245); 62 | } 63 | 64 | .hidden { 65 | display: none; 66 | } 67 | -------------------------------------------------------------------------------- /docs/resources/highlight.pack.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Arduino® Light Theme - Stefania Mellai 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 |

Domino

3 | 4 | [![CircleCI](https://img.shields.io/circleci/build/gh/domino-clj/domino?label=CircleCI&logo=circleci&style=flat-square)](https://circleci.com/gh/domino-clj/domino) [![Clojars Project](https://img.shields.io/clojars/v/domino/core?&style=flat-square)](https://clojars.org/domino/core) [![Slack](https://img.shields.io/badge/slack-%40clojurians%2Fdomino--clj-blue?logo=slack&style=flat-square)](https://clojurians.slack.com/messages/domino-clj) [![Clojars Downloads](https://img.shields.io/clojars/dt/domino/core?color=blue&style=flat-square)](https://clojars.org/domino/core) [![GitHub Stars](https://img.shields.io/github/stars/domino-clj/domino?logo=github&style=flat-square)](https://github.com/domino-clj/domino/stargazers) 5 | 6 | 7 | 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+=""}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:""}]}]},{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:""},{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.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:/`]+/}]}]}]};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:"|$)",e:">",k:{name:"style"},c:[e],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[e],starts:{e:"<\/script>",rE:!0,sL:["actionscript","javascript","handlebars","xml","vbscript"]}},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},e]}]}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^\\s*([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}|\t)",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}});hljs.registerLanguage("makefile",function(e){var i={cN:"variable",v:[{b:"\\$\\("+e.UIR+"\\)",c:[e.BE]},{b:/\$[@%&#'",r="["+t+"]["+t+"0-9/;:]*",n={b:r,r:0},a={cN:"number",b:"[-+]?\\d+(\\.\\d+)?",r:0},o=e.inherit(e.QSM,{i:null}),s=e.C(";","$",{r:0}),c={cN:"literal",b:/\b(true|false|nil)\b/},i={b:"[\\[\\{]",e:"[\\]\\}]"},d={cN:"comment",b:"\\^"+r},l=e.C("\\^\\{","\\}"),m={cN:"symbol",b:"[:]{1,2}"+r},p={b:"\\(",e:"\\)"},u={eW:!0,r:0},f={k:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"},l:r,cN:"name",b:r,starts:u},h=[p,o,d,l,s,m,i,a,c,n];return p.c=[e.C("comment",""),f,u],u.c=h,i.c=h,l.c=[i],{aliases:["clj"],i:/\S/,c:[p,o,d,l,s,m,i,a,c]}});hljs.registerLanguage("http",function(e){var t="HTTP/[0-9\\.]+";return{aliases:["https"],i:"\\S",c:[{b:"^"+t,e:"$",c:[{cN:"number",b:"\\b\\d{3}\\b"}]},{b:"^[A-Z]+ (.*?) "+t+"$",rB:!0,e:"$",c:[{cN:"string",b:" ",e:" ",eB:!0,eE:!0},{b:t},{cN:"keyword",b:"[A-Z]+"}]},{cN:"attribute",b:"^\\w",e:": ",eE:!0,i:"\\n|\\s|=",starts:{e:"$",r:0}},{b:"\\n\\n",starts:{sL:[],eW:!0}}]}});hljs.registerLanguage("python",function(e){var r={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10",built_in:"Ellipsis NotImplemented",literal:"False None True"},b={cN:"meta",b:/^(>>>|\.\.\.) /},c={cN:"subst",b:/\{/,e:/\}/,k:r,i:/#/},a={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[e.BE,b],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[e.BE,b],r:10},{b:/(fr|rf|f)'''/,e:/'''/,c:[e.BE,b,c]},{b:/(fr|rf|f)"""/,e:/"""/,c:[e.BE,b,c]},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},{b:/(fr|rf|f)'/,e:/'/,c:[e.BE,c]},{b:/(fr|rf|f)"/,e:/"/,c:[e.BE,c]},e.ASM,e.QSM]},i={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",b,i,a]};return c.c=[a,i,b],{aliases:["py","gyp","ipython"],k:r,i:/(<\/|->|\?)|=>/,c:[b,i,a,e.HCM,{v:[{cN:"function",bK:"def"},{cN:"class",bK:"class"}],e:/:/,i:/[${=;\n,]/,c:[e.UTM,l,{b:/->/,eW:!0,k:"None"}]},{cN:"meta",b:/^[\t ]*@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("cpp",function(t){var e={cN:"keyword",b:"\\b[a-z\\d_]*_t\\b"},r={cN:"string",v:[{b:'(u8?|U|L)?"',e:'"',i:"\\n",c:[t.BE]},{b:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\((?:.|\n)*?\)\1"/},{b:"'\\\\?.",e:"'",i:"."}]},s={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},i={cN:"meta",b:/#\s*[a-z]+\b/,e:/$/,k:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"},c:[{b:/\\\n/,r:0},t.inherit(r,{cN:"meta-string"}),{cN:"meta-string",b:/<[^\n>]*>/,e:/$/,i:"\\n"},t.CLCM,t.CBCM]},a=t.IR+"\\s*\\(",c={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not",built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr",literal:"true false nullptr NULL"},n=[e,t.CLCM,t.CBCM,s,r];return{aliases:["c","cc","h","c++","h++","hpp","hh","hxx","cxx"],k:c,i:"",k:c,c:["self",e]},{b:t.IR+"::",k:c},{v:[{b:/=/,e:/;/},{b:/\(/,e:/\)/},{bK:"new throw return else",e:/;/}],k:c,c:n.concat([{b:/\(/,e:/\)/,k:c,c:n.concat(["self"]),r:0}]),r:0},{cN:"function",b:"("+t.IR+"[\\*&\\s]+)+"+a,rB:!0,e:/[{;=]/,eE:!0,k:c,i:/[^\w\s\*&]/,c:[{b:a,rB:!0,c:[t.TM],r:0},{cN:"params",b:/\(/,e:/\)/,k:c,r:0,c:[t.CLCM,t.CBCM,r,s,e,{b:/\(/,e:/\)/,k:c,r:0,c:["self",t.CLCM,t.CBCM,r,s,e]}]},t.CLCM,t.CBCM,i]},{cN:"class",bK:"class struct",e:/[{;:]/,c:[{b://,c:["self"]},t.TM]}]),exports:{preprocessor:i,strings:r,k:c}}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},s={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={b:"html`",e:"",starts:{e:"`",rE:!1,c:[e.BE,s],sL:"xml"}},n={b:"css`",e:"",starts:{e:"`",rE:!1,c:[e.BE,s],sL:"css"}},o={cN:"string",b:"`",e:"`",c:[e.BE,s]};s.c=[e.ASM,e.QSM,c,n,o,a,e.RM];var i=s.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,n,o,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:i}]}]},{cN:"",b:/\s/,e:/\s*/,skip:!0},{b://,sL:"xml",c:[{b:/<[A-Za-z0-9\\._:-]+\s*\/>/,skip:!0},{b:/<[A-Za-z0-9\\._:-]+/,e:/(\/[A-Za-z0-9\\._:-]+|[A-Za-z0-9\\._:-]+\/)>/,skip:!0,c:[{b:/<[A-Za-z0-9\\._:-]+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:i}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor get set",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("sql",function(e){var t=e.C("--","$");return{cI:!0,i:/[<>{}*]/,c:[{bK:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment values with",e:/;/,eW:!0,l:/[\w\.]+/,k:{keyword:"as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select self semi sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek",literal:"true false null unknown",built_in:"array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text time timestamp tinyint varchar varying void"},c:[{cN:"string",b:"'",e:"'",c:[e.BE,{b:"''"}]},{cN:"string",b:'"',e:'"',c:[e.BE,{b:'""'}]},{cN:"string",b:"`",e:"`",c:[e.BE]},e.CNM,e.CBCM,t,e.HCM]},e.CBCM,t,e.HCM]}});hljs.registerLanguage("shell",function(s){return{aliases:["console"],c:[{cN:"meta",b:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{e:"$",sL:"bash"}}]}}); --------------------------------------------------------------------------------