diff --git a/src/main/clojure/cascade.clj b/src/main/clojure/cascade.clj
index 132b99e..c156be7 100644
--- a/src/main/clojure/cascade.clj
+++ b/src/main/clojure/cascade.clj
@@ -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
@@ -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 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})
\ No newline at end of file
diff --git a/src/main/clojure/cascade/import.clj b/src/main/clojure/cascade/import.clj
index caf15b2..5b6bf74 100644
--- a/src/main/clojure/cascade/import.clj
+++ b/src/main/clojure/cascade/import.clj
@@ -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]
@@ -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 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 tags for any imported stylesheets."
+post-processed to add new 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))
diff --git a/src/main/clojure/cascade/internal/viewbuilder.clj b/src/main/clojure/cascade/internal/viewbuilder.clj
index 9a739e9..beeb9b9 100644
--- a/src/main/clojure/cascade/internal/viewbuilder.clj
+++ b/src/main/clojure/cascade/internal/viewbuilder.clj
@@ -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))
diff --git a/src/main/clojure/cascade/request.clj b/src/main/clojure/cascade/request.clj
index 3ada871..8c19492 100644
--- a/src/main/clojure/cascade/request.clj
+++ b/src/main/clojure/cascade/request.clj
@@ -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
@@ -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\")
@@ -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)
@@ -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)))
\ No newline at end of file
+ (->
+ handler
+ wrap-import-stylesheets
+ serialize-html
+ wrap-install-cascade-atom-into-request))
\ No newline at end of file
diff --git a/src/test/clojure/cascade/test_cascade.clj b/src/test/clojure/cascade/test_cascade.clj
index 56e20f9..033cc51 100644
--- a/src/test/clojure/cascade/test_cascade.clj
+++ b/src/test/clojure/cascade/test_cascade.clj
@@ -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))))
@@ -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 "© 2009 ")
diff --git a/src/test/clojure/cascade/test_viewbuilder.clj b/src/test/clojure/cascade/test_viewbuilder.clj
index 383362f..aea4ae4 100644
--- a/src/test/clojure/cascade/test_viewbuilder.clj
+++ b/src/test/clojure/cascade/test_viewbuilder.clj
@@ -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}))))
\ No newline at end of file
+ (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}))
\ No newline at end of file
diff --git a/src/test/main.clj b/src/test/main.clj
index 480f6cf..49cf148 100644
--- a/src/test/main.clj
+++ b/src/test/main.clj
@@ -1,5 +1,5 @@
(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]
@@ -7,31 +7,45 @@
(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
+ :© " 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})
\ No newline at end of file