Published demo site

This commit is contained in:
Julien Eluard 2018-05-10 11:57:22 +02:00
parent a91faf961c
commit 5761bf27dc
No known key found for this signature in database
GPG Key ID: 6FD7DB5437FCBEF6
22 changed files with 2827 additions and 195 deletions

View File

@ -1,18 +1,8 @@
Launch figwheel with ./scripts/figwheel.sh then open index.html
Launch figwheel with ./scripts/figwheel.sh then open http://127.0.0.1/index.html
### IPFS
You need to start a daemon locally, as the main gateway has CORS. The mobile app
does not have this limitation and can hit directly the gateway.
Before starting the dameon setup CORS:
```
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["GET", "POST"]'
```
and then
To upload a directory to IPFS:
```
ipfs daemon

View File

@ -10,8 +10,8 @@
(def compiler-config {:main 'pluto.demo
:asset-path "js/out"
:output-to "resources/public/js/pluto.js"
:output-dir "resources/public/js/out"
:output-to "resources/public/assets/pluto.js"
:output-dir "resources/public/assets/pluto"
:optimizations :none
:static-fns true
:elide-asserts false
@ -19,6 +19,12 @@
:preloads ['day8.re-frame-10x.preload]
:source-map true})
(def compiler-release-config {:main 'pluto.demo
:output-to "resources/public/assets/pluto.js"
:optimizations :advanced
:static-fns true
:elide-asserts true})
(def test-config {:main 'pluto.runner
:output-to "target/test.js"
:output-dir "target/test"
@ -65,7 +71,7 @@
;;; Compiling task.
(defn compile-once []
(api/build source-dir compiler-config))
(api/build source-dir compiler-release-config))
(defn compile-refresh []
(api/watch source-dir compiler-config))
@ -118,4 +124,4 @@
;;; Build script entrypoint.
(task *command-line-args*)
(task (map string/trim *command-line-args*))

44
demo/index.html Normal file
View File

@ -0,0 +1,44 @@
<html>
<script type="text/javascript" src="assets/instascan.min.js"></script>
<script type="text/javascript" src="assets/qrcode.min.js"></script>
<body>
<main>
<div id="selection">
<span>Scan this QR code to load from IPFS</span>
<div id="ipfs"></div>
<div>
Or load from HTTP <button onclick="pluto.demo.load_and_render('ipfs:QmSKP6f2uUsFq4mk1Afe4ZktxwQifrLb4xRQYNE1LxidKz')">Demo</button>
</div>
</div>
<div id="extension">
<iframe id="frame" srcdoc="<body><main></main></body>"></iframe>
<div id="errors"></div>
</div>
<video id="preview"></video>
</main>
</body>
</html>
<script src="assets/pluto.js"></script>
<script>
var qrcode = new QRCode("ethereum:status-extension:ipfs:QmSKP6f2uUsFq4mk1Afe4ZktxwQifrLb4xRQYNE1LxidKz");
var svg = qrcode.svg();
document.getElementById("ipfs").innerHTML = svg;
</script>
<script type="text/javascript">
let scanner = new Instascan.Scanner({ video: document.getElementById('preview') });
scanner.addListener('scan', function (content) {
console.log(content);
pluto.demo.load_and_render(content.replace("ethereum:status-extension:", ""));
});
Instascan.Camera.getCameras().then(function (cameras) {
if (cameras.length > 0) {
scanner.start(cameras[0]);
} else {
console.error('No cameras found.');
}
}).catch(function (e) {
console.error(e);
});
</script>

1244
demo/pluto.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
resources/public/assets/qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,12 +0,0 @@
{:hooks/main
#view [:main]
:views/main
[view {}
[text "Hello"]]}
; (let [cond? #query [:random-boolean]]
; (when cond?
; [text {}]
; "World"))

View File

@ -0,0 +1,19 @@
{:extension/meta
{:name "Simple Demo"
:description "A simple demo of extension"
:documentation "Nothing. Just see a text with dynamic random color."}
:hooks/main
{:name ""
:description ""
:view #status/view [:main]}
:views/main
[view {}
[text {} "Hello"]
(let [cond? #status/query [:random-boolean]]
(if cond?
[text {:style {:color "green"}}
"World?"]
[text {:style {:color "red"}}
"World?"]))]}

View File

@ -7,19 +7,19 @@
{:on-activated #event [:fetch-all-posts]}
:events/fetch-all-posts
[:ethereum/logs {:address "0xfa28ec7198028438514b49a3cf353bca5541ce1d"
:topics ["PeepEth()"]
:inputs [{:name :hash :type :string}] ;; Allows to decode transaction data
:on-log #event [:fetch-ipfs]}] ;; A map of decoded data will be injected
[:status/ethereum.logs {:address "0xfa28ec7198028438514b49a3cf353bca5541ce1d"
:topics ["PeepEth()"]
:inputs [{:name :hash :type :string}] ;; Allows to decode transaction data
:on-log #status/event [:fetch-ipfs]}] ;; A map of decoded data will be injected
:events/fetch-ipfs
(let [{:keys [hash]} properties]
[:ipfs/get {:hash hash
:on-success #event [:db/append {:path [:all-posts]}]}])
[:status/ipfs.get {:hash hash
:on-success #status/event [:status/db.append {:path [:all-posts]}]}])
:queries/all-posts
[:db/get {:path [:all-posts]
:limit 20}]
[:status/db.get {:path [:all-posts]
:limit 20}]
:views/post
;; TODO get account details
@ -43,10 +43,10 @@
{:en "Peepeth !!"}
:hooks/main
[screen {:style #style [:screen]}
[screen {:style #status/style [:screen]}
[toolbar {}
[text {}
#i18n [:title]]]
(let {posts #query [:all-posts]}
(let [posts #status/query [:all-posts]]
[list {:data posts
:template #view [:post]}])]}
:template #status/view [:post]}])]}

View File

@ -1,10 +1,44 @@
<html>
<script type="text/javascript" src="assets/instascan.min.js"></script>
<script type="text/javascript" src="assets/qrcode.min.js"></script>
<body>
<iframe id="frame" srcdoc="<body><main></main></body>"></iframe>
<main>
<div id="selection">
<span>Scan this QR code to load from IPFS</span>
<div id="ipfs"></div>
<div>
Or load from HTTP <button onclick="pluto.demo.load_and_render('ipfs:QmSKP6f2uUsFq4mk1Afe4ZktxwQifrLb4xRQYNE1LxidKz')">Demo</button>
</div>
</div>
<div id="extension">
<iframe id="frame" srcdoc="<body><main></main></body>"></iframe>
<div id="errors"></div>
</div>
<video id="preview"></video>
</main>
</body>
</html>
<script src="js/pluto.js"></script>
<script src="assets/pluto.js"></script>
<script>
pluto.demo.run();
var qrcode = new QRCode("ethereum:status-extension:ipfs:QmSKP6f2uUsFq4mk1Afe4ZktxwQifrLb4xRQYNE1LxidKz");
var svg = qrcode.svg();
document.getElementById("ipfs").innerHTML = svg;
</script>
<script type="text/javascript">
let scanner = new Instascan.Scanner({ video: document.getElementById('preview') });
scanner.addListener('scan', function (content) {
console.log(content);
pluto.demo.load_and_render(content.replace("ethereum:status-extension:", ""));
});
Instascan.Camera.getCameras().then(function (cameras) {
if (cameras.length > 0) {
scanner.start(cameras[0]);
} else {
console.error('No cameras found.');
}
}).catch(function (e) {
console.error(e);
});
</script>

4
scripts/publish.sh Normal file
View File

@ -0,0 +1,4 @@
mkdir -p demo
clj -R:repl build.clj compile once
cp resources/public/assets/pluto.js demo/
cp resources/public/index.html demo/

View File

@ -3,11 +3,11 @@
(defn view [props & content]
(into [:div props] content))
(defn button [props content]
[:button props content])
(defn button [props & content]
(into [:button props] content))
(defn text [props content]
[:span props content])
(defn text [props & content]
(into [:span props] content))
(def components {'view view
'button button

View File

@ -1,5 +1,6 @@
(ns pluto.demo
(:require [pluto.components.html :as html]
(:require [clojure.string :as string]
[pluto.components.html :as html]
[pluto.reader :as reader]
[pluto.storage :as storage]
[pluto.storage.http :as http]
@ -34,19 +35,18 @@
(let [frame (js/document.getElementById "frame")]
(reagent/render (h) (.. (aget frame "contentWindow" "document") -body -firstChild))))
(defn header []
[:div
"Random boolean: "
(let [b @(re-frame/subscribe [:random-boolean])]
[:span {:style {:color (if b :green :red)}}
(str b)])])
(defn wrap-with-cartouche [o]
(println ">>>>" o)
(defn main-browser
"A simple hook for :hooks/main"
[{:keys [data errors]}]
(fn []
[:div
^{:key 1} [header]
^{:key 2} o]))
(let [{:views/keys [main]} data]
main)
(when (seq errors)
(into [:ul]
(for [{:keys [type] :as m} errors]
[:li
[:span [:b (str type)] (pr-str (dissoc m :type))]])))]))
(defn wrap-extensions [os]
(fn []
@ -56,23 +56,28 @@
[:h2 (:name o)]
[:p (:content o)]])]))
(defn ^:export run
[]
(defn storage-for [type]
(condp = type
"url" (http/HTTPStorage.)
"ipfs" (ipfs/IPFSStorage. "https://gateway.ipfs.io")))
(defn fetch [uri cb]
(let [[type id] (string/split uri ":")]
(storage/fetch
(storage-for type)
{:value id} cb)))
(defn ^:export load-and-render
[s]
(re-frame/clear-subscription-cache!)
(storage/fetch (ipfs/IPFSStorage. "http://localhost:8080")
{:id "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" :URL "/demo.edn"}
#(->> %
wrap-extensions
render))
(storage/fetch (http/HTTPStorage.)
{:URL "/demo.edn"}
(fetch s
#(-> %
;; TODO merge all returned files in a map
:value
first
:content
reader/read
:data
((fn [m] (println "<<<" m) (reader/parse {:components html/components :valid-hooks #{:hooks/main}} m)))
:data
:views/main
wrap-with-cartouche
((fn [m] (reader/parse {:components html/components :valid-extensions #{:extension/meta} :valid-hooks #{:hooks/main}} m)))
main-browser
render)))

View File

@ -49,10 +49,10 @@
{:data
(try
(edn/read-string {:default #(do (accumulate-reader-error! errors {:type :unknown-tag :tag %1 :value %2}) %2)
:readers {'query #(mark-reference :query %)
'event #(mark-reference :event %)
'view #(mark-reference :view %)
'style #(mark-reference :style %)}}
:readers {'status/query #(mark-reference :query %)
'status/event #(mark-reference :event %)
'status/view #(mark-reference :view %)
'status/style #(mark-reference :style %)}}
s)
(catch #?(:clj Exception :cljs :default) e
(accumulate-reader-error! errors (assoc (ex-data e) :message (ex-message e)))
@ -101,27 +101,26 @@
(reduce #(let [{:keys [data errors]} (parse-view opts %2)]
(cond-> (update %1 :data conj data)
(seq errors) (accumulate-errors errors)))
{} children))
{:data []} children))
(defn parse-hiccup-element [{:keys [components] :as opts} o]
;; TODO permissions
;; TODO replace elements
(cond
(primitive? o) {:data o}
(vector? o)
(let [[element properties & children] o
component (element components)]
(cond-> (let [m (parse-hiccup-children opts children)]
;; Reduce parsed children to a single map and wrap them in a hiccup element
;; whose component has been translated to the local platform
(update m :data #(apply conj [(or component element) properties] %)))
(nil? component) (accumulate-errors [{:type :unknown-component :element element}])))))
(or (symbol? o) (primitive? o)) {:data o}
(vector? o)
(let [[element properties & children] o
component (if (fn? element) element (get components element))]
(cond-> (let [m (parse-hiccup-children opts children)]
;; Reduce parsed children to a single map and wrap them in a hiccup element
;; whose component has been translated to the local platform
(update m :data #(apply conj [(or component element) properties] %)))
(nil? component) (accumulate-errors [{:type :unknown-component :element element}])))
:else 3))
(defn parse-view [opts o]
(cond
(instance? Reference o) (resolve-reference (:tag o) (:value o))
(list? o) (blocks/parse opts o)
:else (parse-hiccup-element opts o)))
(list? o) (parse-view opts (:data (blocks/parse opts o)))
:else (parse-hiccup-element opts o)))
(defmulti parse-value
"Parse a definition element value.
@ -131,9 +130,12 @@
* :errors a collection of errors"
(fn [_ k _] (namespace k)))
(defmethod parse-value "extension" [opts _ v] v)
(defmethod parse-value "views" [opts _ v] (parse-view opts v))
;; TODO extension, events, queries, i18n, styles
(defmethod parse-value :default [_ k _] {:errors [{:type :unkown-element-type :value k}]})
(defmethod parse-value :default [_ k _] {:errors [{:type :unknown-element-type :value k}]})
(defn merge-parsed-value
"Merge result of parse-value into a map.
@ -157,10 +159,6 @@
* :permissions a vector of required permissions
* :errors a vector of errors map triggered during parse"
[opts m]
;; TODO
; Replace lookup refs with values
; Replace conditional blocks with reagent components
; Errors; Permissions; env cascade (:outer)
(let [errors (validate-keys opts (keys m))]
(cond-> (reduce-kv #(merge-parsed-value opts %1 %2 %3) {} m)
(seq errors) (accumulate-errors errors))))
(seq errors) (accumulate-errors (map (fn [error] {:type (key error) :value (val error)}) errors)))))

View File

@ -0,0 +1,56 @@
(ns pluto.reader.blocks
(:require [clojure.walk :as walk]
[re-frame.core :as re-frame]))
(defmulti parse
""
(fn [_ [type]] type))
(defn resolve-queries [env]
;; TODO only resolve encoded queries
(reduce-kv #(assoc %1 %2 (if (record? %3) @(re-frame/subscribe (:value %3))) %3)
{}
env))
(defn let-block [{:keys [env]} child]
(cond
(coll? child) (walk/prewalk-replace (resolve-queries env) child)))
(defn bindings->env [v]
;; TODO errors: pair number of arguments, keys are symbols, values are primitives or queries
;; TODO destructuring
(apply hash-map v))
(defmethod parse 'let [_ [_ bindings & body]]
{:data
;; TODO error if multiple body as let only considers last
(let [m (bindings->env bindings)
child (last body)]
[let-block {:env m} child])})
(defn when-block [{:keys [test]} body]
;; TODO warning if test is not of boolean type
(when test
body))
(defmethod parse 'when [_ [_ test & body]]
(cond
(symbol? test)
{:data (apply conj [when-block {:test test}] body)}
:else
{:errors [{:type :unsupported-type :value test}]}))
(defn if-block [{:keys [test]} & body]
;; TODO warning if test is not of boolean type
(if test
(first body)
(second body)))
(defmethod parse 'if [_ [_ test then else]]
(cond
(symbol? test)
{:data (apply conj [if-block {:test test}] (list then else))}
:else
{:errors [{:type :unsupported-type :value test}]}))
(defmethod parse :default [opts block] {:errors [{:type :unknown-block-type :opts opts :block block}]})

View File

@ -1,73 +0,0 @@
(ns pluto.reader.blocks
(:require [re-frame.core :as re-frame]))
(defmulti parse (fn [_ [type]] type))
(defn bindings->env [v]
(apply hash-map v))
(defn primitive? [o]
(or (boolean? o)
(int? o)
(float? o)
(string? o)))
(defmulti encode-value (fn [[f & _]] f))
(defmethod encode-value 'query [[_ & args]] (apply conj args))
(defn env-value [o]
;; TODO encode query as record. Validate query exists and potentially that data type matches
;; Ensure rest is pure data or arguments
(println "ENV" o (type o))
(cond
(list? o) (encode-value o)
(primitive? o) o
#_(and (instance? Reference o)
(= :query (:type o)))
;o
;; TODO accumulate errors
:else {:errors [{:type :incorrect-let-value :value o}]}))
(defn env [m]
(reduce-kv #(assoc %1 %2 (env-value %3))
{}
m))
(defn resolve-queries [env]
;; TODO only resolve encoded queries
(reduce-kv #(assoc %1 %2 @(re-frame/subscribe %3))
{}
env))
(defn let-block [{:keys [env]} [k p children]]
;; Propagate env as meta on children element
;; TODO support multiple children
;; TODO figure out envs as part of properties, with cascading scopes (delegate to :outer)
;; Disallow symbol shadowing
[k (assoc p :env (resolve-queries env)) (with-meta children {:env (resolve-queries env)})])
(defmethod parse 'let [_ [_ bindings & body]]
{:data
;; TODO validate pairs of data
;; keys -> symbols
;; values -> primitive types, query
;; TODO warning if multiple body as let only considers last
(let [m (bindings->env bindings)]
[let-block {:env (env m)} (last body)])})
(defn test? [{:keys [env test]}]
(cond
(boolean? test) test
(symbol? test) (test env)))
(defn when-block [props body]
(when (test? props)
body))
(defmethod parse 'when [[props test & body]]
;; TODO test can only be a symbol. This symbol must point to primitive type or query.
;; TODO If value known at read time, resolve statically
(apply conj [when-block {:test test :env (:env (meta body))}] body))
(defmethod parse :default [opts block] {:errors [{:type :unknown-block-type :opts opts :block block}]})

View File

@ -1,12 +1,18 @@
(ns pluto.storage.http
(:require [pluto.storage :as storage]))
(defn result [xhr]
(let [status (.-status xhr)]
(if (= 404 status)
{:type :error :value status}
{:type :success :value (.-responseText xhr)})))
(deftype HTTPStorage []
storage/Storage
(fetch [_ id callback]
(fetch [_ {:keys [value]} callback]
(let [xhr (js/XMLHttpRequest.)]
(.open xhr "GET" (:URL id) true)
(.open xhr "GET" (str value "/extension.edn") true)
(.send xhr nil)
(set! (.-onreadystatechange xhr)
#(when (= (.-readyState xhr) 4)
(callback (.-response xhr)))))))
(callback (result xhr)))))))

View File

@ -1,16 +1,18 @@
(ns pluto.storage.ipfs
(:require [pluto.storage :as storage]))
(:require [clojure.string :as string]
[pluto.storage :as storage]))
(defn- ipfs->extension [ipfs-extension]
{:extension-id (:Hash ipfs-extension)
:name (:Name ipfs-extension)})
(defn parse-directory [response]
(->> (js->clj (js/JSON.parse response) :keywordize-keys true)
:Objects
first
:Links
(map ipfs->extension)))
(when-not (string/blank? response)
(->> (js->clj (js/JSON.parse response) :keywordize-keys true)
:Objects
first
:Links
(map ipfs->extension))))
(defn fetch-promise [url]
(new js/Promise (fn [resolve reject]
@ -19,7 +21,8 @@
(.send xhr nil)
(set! (.-onreadystatechange xhr)
#(when (= (.-readyState xhr) 4)
(resolve (.-response xhr))))))))
;; TODO handle error codes
(resolve (.-responseText xhr))))))))
(defn list-all [gateway-url directory]
(fetch-promise (str gateway-url "/api/v0/ls?arg=" directory)))
@ -34,14 +37,14 @@
(defn fetch-all [gateway-url extensions]
(let [promises (js/Promise.all (clj->js (mapv #(fetch gateway-url %) extensions)))]
(.then promises
#(js->clj % :keywordize-keys true))))
(.then promises
#(js->clj % :keywordize-keys true))))
(defrecord IPFSStorage [gateway-url]
storage/Storage
(fetch [this extension callback]
(..
(list-all gateway-url (:id extension))
(list-all gateway-url (:value extension))
(then parse-directory)
(then (partial fetch-all gateway-url))
(then callback))))
(then #(callback {:type :success :value %})))))

View File

@ -0,0 +1,7 @@
(ns pluto.reader.blocks-test
(:require [cljs.test :refer-macros [is deftest async use-fixtures]]
[pluto.reader.blocks :as blocks]))
(deftest parse
(is (= nil (blocks/parse {} '(let [s "Hello"] s))))
(is (= nil (blocks/parse {} '(let [s "Hello"] ['test {} s])))))

View File

@ -1,6 +1,7 @@
(ns pluto.reader-test
(:require [cljs.test :refer-macros [is deftest async use-fixtures]]
[pluto.reader :as reader :refer [Reference]]))
[pluto.reader :as reader :refer [Reference]]
[pluto.reader.blocks :as blocks]))
(deftest read
(is (= {:data nil} (reader/read "")))
@ -37,10 +38,11 @@
(is (= {:invalid-hooks #{:hooks/unknown}}
(reader/validate-keys {:valid-hooks #{:hooks/main}} #{:hooks/main :hooks/unknown}))))
#_
(deftest parse-hiccup-children
(is (= {:data (list [:text {} ""])} (reader/parse-hiccup-children {:components {'text :text}} (list ['text {} ""])))))
#_
(deftest parse
(is (= {} (reader/parse {} {})))
(is (= {:data {:views/main ['text {} "Hello"]}
@ -51,13 +53,16 @@
(is (= {:data {:views/main [:text {} "Hello"]}}
(reader/parse {:components {'text :text}} {:views/main ['text {} "Hello"]}))))
#_
(deftest parse-references
(is (= {:data {:views/main [:pluto.reader-test/main]}} (reader/parse {} {:views/main (Reference. :view [::main])}))))
#_
(deftest parse-blocks
(is (= nil (reader/parse {} {:views/main (list 'let ['cond? true] ['text])})))
(is (= nil (reader/parse {} {:views/main (list 'let ['cond? (Reference. :query [::query])] ['text])})))
(is (= nil (reader/parse {} {:views/main (list 'let ['cond? (Reference. :query [::query])]
(list 'when 'cond?
['text {} "World"]))}))))
(deftest parse-let-blocks
(is (= {:data {:views/main [blocks/let-block {:env {'s "Hello"}} [:text {} 's]]}}
(reader/parse {:components {'text :text}} {:views/main (list 'let ['s "Hello"] ['text {} 's])})))
#_
(is (= nil (reader/parse {:components {'text :text}} {:views/main (list 'let ['cond? (Reference. :query [::query])] ['text])})))
#_
(is (= nil (reader/parse {:components {'text :text}} {:views/main (list 'let ['cond? (Reference. :query [::query])]
(list 'when 'cond?
['text {} "World"]))}))))

View File

@ -1,5 +1,7 @@
(ns pluto.runner
(:require [doo.runner :refer-macros [doo-tests]]
[pluto.reader-test]))
[pluto.reader-test]
[pluto.reader.blocks-test]))
(doo-tests 'pluto.reader-test)
(doo-tests 'pluto.reader-test
'pluto.reader.blocks-test)