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