Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Reorganize cascade.request/initialize to take a handler function for …
…HTML routes as a parameter

Change deftemplate to return a Ring response map
Allow '-' inside implicit id and class attribute defined from a keyword
Implement CSS asset importing
  • Loading branch information
hlship committed Nov 8, 2011
1 parent 7da5be6 commit 30adbee
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 51 deletions.
10 changes: 3 additions & 7 deletions src/main/clojure/cascade.clj
Expand Up @@ -26,11 +26,12 @@

(defmacro defview
"Defines a Cascade view function, which uses an embedded template. A view function may have a doc string and meta data
preceding the parameters vector. The function's forms are an implicit inline block."
preceding the parameters vector. The function's forms are an implicit inline block. The function returns a Ring response
map, consisting of a single key, :body, consisting of the DOM nodes rendering by the implicit template."
[& forms]
(let [[fn-name fn-params template-forms] (parse-function-def forms)]
`(defn ~fn-name ~(or (meta fn-name) {}) ~fn-params
(template ~@template-forms))))
{:body (template ~@template-forms)})))

(defmacro block
"Encapsulates a block of template forms as a function with parameters, typically used as
Expand All @@ -56,8 +57,3 @@ preceding the parameters vector. The function's forms are an implicit inline blo
"Creates a comment DOM node."
[comment]
(comment-node comment))

(defview stylesheet
"Creates a <link> element for a stylesheet. The resource should be either a String or a cascade.asset/Asset."
[resource]
:link {:rel :stylesheet :type "text/css" :href resource})
33 changes: 25 additions & 8 deletions src/main/clojure/cascade/import.clj
Expand Up @@ -17,18 +17,18 @@
(:use
[cascade dom]))

(def wrap-install-cascade-atom-into-request [handler]
(defn wrap-install-cascade-atom-into-request [handler]
"Wraps the handler with a new handler that installs the :cascade key into the request.
The value is an atom containing a map of the data needed to track imports."
(fn [req]
(handler (assoc req :cascade (atom {:stylesheets [] :js-inits []})))))
(handler (assoc req :cascade (atom {:stylesheets []})))))

(defn add-if-not-present
[list value]
(if (contains? list value)
list
; Dependent on the list being a vector that conj-es at the end
conj list value))
(conj list value)))

(defn import-in-cascade-key
([req key value]
Expand All @@ -40,14 +40,31 @@ The value is an atom containing a map of the data needed to track imports."
[req stylesheet-asset]
(import-in-cascade-key req :stylesheets stylesheet-asset))

(defn to-element-node
"Converts an asset into a <link> element node."
[asset]
(element-node :link {:rel :stylesheet :type "text/css" :href asset} nil))

(defn add-stylesheet-nodes
[dom-nodes stylesheet-assets]
; TODO: optimize when no assets
(extend-dom dom-nodes [[[:html :head :script] :before]
[[:html :head :link] :before]
[[:html :head] :bottom]] (map to-element-node stylesheet-assets)))

(def wrap-import-stylesheets
(defn wrap-import-stylesheets
"Middleware around a handler that consumes a request and returns a seq of DOM nodes (or nil). The DOM nodes are
post-processed to add new <link> tags for any imported stylesheets."
post-processed to add new <link> elements for any imported stylesheets."
[handler]
(fn [req]
(let [dom-nodes (handler req)]
(if dom-nodes
(add-stylesheet-nodes dom-nodes (-> req :cascade deref :stylesheets))))))
(let [response (handler req)]
(and response
(update-in response [:body] add-stylesheet-nodes (-> req :cascade deref :stylesheets))))))

(defn wrap-imports
"Wraps a request-to-DOM-nodes handler with support for imports."
[handler]
(->
handler
wrap-import-stylesheets
wrap-install-cascade-atom-into-request))
2 changes: 1 addition & 1 deletion src/main/clojure/cascade/internal/viewbuilder.clj
Expand Up @@ -41,7 +41,7 @@
[element-name]
(let [name-str (name element-name)]
; match sequences of word characters prefixed with '.' or '#' within the overall name
(loop [matcher (re-matcher #"([.#])(\w+)" name-str)
(loop [matcher (re-matcher #"([.#])([\w-]+)" name-str)
result []]
(if (.find matcher)
(recur matcher (conj result [(.substring name-str 0 (.start matcher))
Expand Down
46 changes: 37 additions & 9 deletions src/main/clojure/cascade/request.clj
Expand Up @@ -21,7 +21,7 @@
[ring.middleware.file-info :as file-info])
(:use
[compojure core]
[cascade dom asset]))
[cascade dom asset import]))

(defn wrap-exception-handling
"Middleware for standard Cascade exception reporting; exceptions are caught and reported using the Cascade
Expand Down Expand Up @@ -60,9 +60,33 @@ path
.getTime
(.format format))))

(def placeholder-routes
"Returns a placeholder request handling function that always returns nil."
(routes))

(defn wrap-serialize-html
"Wraps a handler that produces a seq of DOM nodes (e.g., one created via defview or template) so that
the returned dom-nodes are converted into a seq of strings (the markup to be streamed to the client)."
[handler]
(fn [req]
(let [response (handler req)]
(and response
(update-in response [:body] serialize-html)))))

(defn wrap-html-markup
"Wraps the handler (which renders a request to DOM nodes) with full rendering support, including imports."
[handler]
(->
handler
wrap-imports
wrap-serialize-html))

(defn initialize
"Initializes asset handling for Cascade. This sets an application version (a value incorporated into URLs, which
should change with each new deployment. Named arguments:
:html-routes
Routes that produce full-page rendered HTML markup. The provided handlers should render the request to a seq
of DOM nodes.
:virtual-folder (default \"assets\")
The root folder under which assets will be exposed to the client.
:public-folder (default \"public\")
Expand All @@ -72,7 +96,7 @@ should change with each new deployment. Named arguments:
:asset-factories
Additional asset dispatcher mappings. Keys are domain keywords, values are functions that accept a path within that domain.
The functions should construct and return a cascade/Asset."
[application-version & {:keys [virtual-folder public-folder file-extensions asset-factories]
[application-version & {:keys [virtual-folder public-folder file-extensions asset-factories html-routes]
:or {virtual-folder "assets"
public-folder "public"}}]
(let [root (str "/" virtual-folder "/" application-version)
Expand All @@ -86,15 +110,19 @@ The functions should construct and return a cascade/Asset."
:assets-folder root
:file-extensions (merge mime-type/default-mime-types file-extensions)})
(printf "Initialized asset access at virtual folder %s\n" root)
(wrap-exception-handling
(GET [(str root "/:domain/:path") :path #".*"]
[domain path] (asset-handler asset-factories (keyword domain) path)))))
(->
(routes
(GET [(str root "/:domain/:path") :path #".*"]
[domain path] (asset-handler asset-factories (keyword domain) path))
(wrap-html-markup (or html-routes placeholder-routes)))
wrap-exception-handling)))

(defn wrap-html
"Ring middleware that wraps a handler so that the return value from the handler (a seq of DOM nodes)
is serialized to HTML (as lazy seq of strings)."
[handler]
(fn [req]
(->
(handler req)
serialize-html)))
(->
handler
wrap-import-stylesheets
serialize-html
wrap-install-cascade-atom-into-request))
7 changes: 3 additions & 4 deletions src/test/clojure/cascade/test_cascade.clj
Expand Up @@ -36,9 +36,8 @@
(let [input-path (str "expected/" name ".txt")
expected (slurp (find-classpath-resource input-path))
trimmed-expected (minimize-ws expected)
dom (apply view-fn rest)
; _ (pprint dom)
streamed (serialize-to-string dom)
response (apply view-fn rest)
streamed (serialize-to-string (:body response))
trimmed-actual (minimize-ws streamed)]
(is (= trimmed-actual trimmed-expected))))

Expand Down Expand Up @@ -125,7 +124,7 @@
(deftest block-macro
(serialize-test list-accounts-with-loop "block-macro" {}))

(defn symbol-view []
(defview symbol-view []
(let [copyright (template
linebreak :hr :p [
(raw "&copy; 2009 ")
Expand Down
14 changes: 12 additions & 2 deletions src/test/clojure/cascade/test_viewbuilder.clj
Expand Up @@ -24,8 +24,18 @@
dom-node dom-node
"any string" (text-node "any string")
123.4 (text-node "123.4"))))

(deftest combine-a-non-dom-node-is-failure
(is
(thrown? RuntimeException ; Should check the message, but RE hell
(combine {:not-an :element-node}))))
(combine {:not-an :element-node}))))

(deftest element-name-factoring
(are
[element-keyword element-name attributes]
(= [element-name attributes] (factor-element-name element-keyword))

:fred :fred nil
:fred.bar :fred {:class :bar}
:div.alert-message.error :div {:class "alert-message error"}
:div#mark.alert-message :div {:class :alert-message :id :mark}))
54 changes: 34 additions & 20 deletions src/test/main.clj
@@ -1,37 +1,51 @@
(ns main
(:use compojure.core cascade cascade.asset ring.adapter.jetty)
(:use compojure.core cascade cascade.asset cascade.import ring.adapter.jetty)
(:require
[cascade.request :as cr]
[compojure.route :as route]
[compojure.handler :as handler]))

(set! *warn-on-reflection* true)

(defview hello-world [request]
:html
[:head [:title ["Cascade Hello World"]
(stylesheet (file-asset "css/bootstrap.css"))
(stylesheet (classpath-asset "cascade/cascade.css"))]
:body [
:h1 ["Hello World"]
:p [
"The page rendered at "
:em [(str (java.util.Date.))]
"."
]
:p [
:a {:href "/hello"} ["click to refresh"]
(defn layout [req title body]
(import-stylesheet req (file-asset "css/bootstrap.css"))
(import-stylesheet req (classpath-asset "cascade/cascade.css"))
(template
:html [
:head [:title [title]]
:body [
:div.container [
:h1 [title]
body
:hr
:&copy " 2011 Howard M. Lewis Ship"
]]]))

(defview hello-world [req]
(layout req "Cascade Hello World"
(template
:div.alert-message.success [
:p ["This page rendered at "
:strong [(str (java.util.Date.))]
"."
]
]
]])
:p [
:a.btn.primary.large {:href "/hello"} ["Refresh"]
])))

(defroutes html-routes
(GET "/hello" [] hello-world))

(defroutes main-routes
(defroutes master-routes
; Temporary: eventually we'll pass a couple of routes
;; into cr/initialize
(GET "/hello" [] (cr/wrap-html hello-world))
(cr/initialize "1.0" :public-folder "src/test/webapp")
(cr/initialize "1.0"
:public-folder "src/test/webapp"
:html-routes html-routes)
(route/not-found "Cascade Demo: No such resource"))

(def app
(handler/site main-routes))
(handler/site master-routes))

(run-jetty app {:port 8080})

0 comments on commit 30adbee

Please sign in to comment.