2015-02-10 13:18:56 +00:00
|
|
|
(ns sitetools.core
|
2014-11-29 17:30:24 +00:00
|
|
|
(:require [clojure.string :as string]
|
|
|
|
[goog.events :as evt]
|
|
|
|
[goog.history.EventType :as hevt]
|
|
|
|
[reagent.core :as reagent :refer [atom partial]]
|
|
|
|
[reagent.debug :refer-macros [dbg log dev?]]
|
|
|
|
[reagent.interop :as i :refer-macros [.' .!]])
|
|
|
|
(:import [goog History]
|
|
|
|
[goog.history Html5History]
|
|
|
|
[goog.net Jsonp]))
|
|
|
|
|
|
|
|
(when (exists? js/console)
|
|
|
|
(enable-console-print!))
|
|
|
|
|
|
|
|
(declare page-content)
|
|
|
|
|
|
|
|
|
|
|
|
;;; Configuration
|
|
|
|
|
2015-01-31 21:02:30 +00:00
|
|
|
(defonce config (atom {:page-map {"index.html" [:div "Empty"]}
|
2014-11-29 18:51:45 +00:00
|
|
|
:page-titles {}
|
2015-01-31 21:02:30 +00:00
|
|
|
:body [page-content]
|
2014-11-29 17:30:24 +00:00
|
|
|
:site-dir "outsite/public"
|
|
|
|
:css-infiles ["site/public/css/main.css"]
|
|
|
|
:css-file "css/built.css"
|
|
|
|
:js-file "js/main.js"
|
|
|
|
:js-dir "js/out"
|
2014-12-09 06:30:57 +00:00
|
|
|
:main-div "main-content"
|
2014-11-29 17:30:24 +00:00
|
|
|
:allow-html5-history false}))
|
|
|
|
|
|
|
|
(defonce page (atom "index.html"))
|
|
|
|
(defonce page-state (atom {:has-history false}))
|
|
|
|
|
2014-11-29 18:51:45 +00:00
|
|
|
(defn register-page
|
|
|
|
([pageurl comp]
|
|
|
|
(register-page pageurl comp nil))
|
|
|
|
([pageurl comp title]
|
|
|
|
(assert (string? pageurl)
|
|
|
|
(str "expected string, not " pageurl))
|
2015-01-31 21:02:30 +00:00
|
|
|
(assert (vector? comp)
|
|
|
|
(str "expected vector, not " (pr-str comp)))
|
2014-11-29 18:51:45 +00:00
|
|
|
(assert (or (nil? title)
|
|
|
|
(string? title)))
|
|
|
|
(swap! config update-in [:page-map] assoc pageurl comp)
|
2015-02-01 14:13:08 +00:00
|
|
|
(swap! config update-in [:page-titles] assoc pageurl title)
|
|
|
|
pageurl))
|
2014-11-29 17:30:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
;;; Components
|
|
|
|
|
|
|
|
(defn link
|
|
|
|
[props child]
|
|
|
|
(let [p (:href props)
|
|
|
|
f ((:page-map @config) p)]
|
2015-01-31 21:02:30 +00:00
|
|
|
(assert (vector? f) (str "couldn't resolve page " p))
|
2014-11-29 17:30:24 +00:00
|
|
|
(assert (string? p))
|
|
|
|
[:a (assoc props
|
2015-01-29 13:49:01 +00:00
|
|
|
:href p
|
2014-11-29 17:30:24 +00:00
|
|
|
:on-click (if (:has-history @page-state)
|
|
|
|
(fn [e]
|
|
|
|
(.preventDefault e)
|
|
|
|
(reset! page p)
|
|
|
|
(reagent/next-tick
|
|
|
|
#(set! (.-scrollTop (.-body js/document))
|
|
|
|
0)))
|
|
|
|
identity))
|
|
|
|
child]))
|
|
|
|
|
|
|
|
(defn page-content []
|
2015-01-31 21:02:30 +00:00
|
|
|
(get-in @config [:page-map @page]
|
|
|
|
(get-in @config [:page-map "index.html"])))
|
2014-11-29 17:30:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
;;; Implementation:
|
|
|
|
|
2014-11-29 18:51:45 +00:00
|
|
|
(defn get-title []
|
|
|
|
(get-in @config [:page-titles @page]
|
2014-11-29 22:26:42 +00:00
|
|
|
(or (get-in @config [:page-titles "index.html"])
|
|
|
|
"")))
|
2014-11-29 18:51:45 +00:00
|
|
|
|
2014-11-29 17:30:24 +00:00
|
|
|
(defn default-content []
|
|
|
|
[:div "Empty"])
|
|
|
|
|
|
|
|
(add-watch page ::title-watch
|
|
|
|
(fn [_ _ _ p]
|
2014-11-29 18:51:45 +00:00
|
|
|
(when reagent/is-client
|
|
|
|
(set! (.-title js/document) (get-title)))
|
2014-11-29 17:30:24 +00:00
|
|
|
;; First title on a page wins
|
2014-11-29 18:51:45 +00:00
|
|
|
#_(reset! page-title "")))
|
2014-11-29 17:30:24 +00:00
|
|
|
|
|
|
|
;;; History
|
|
|
|
|
|
|
|
(defn use-html5-history []
|
|
|
|
(when reagent/is-client
|
|
|
|
(let [proto (.' js/window :location.protocol)]
|
|
|
|
(and (:allow-html5-history @config)
|
|
|
|
(.isSupported Html5History)
|
|
|
|
(#{"http:" "https:"} proto)))))
|
|
|
|
|
2014-11-30 15:40:22 +00:00
|
|
|
(defn create-history [p]
|
2014-11-29 17:30:24 +00:00
|
|
|
(if (use-html5-history)
|
|
|
|
(doto (Html5History.)
|
|
|
|
(.setUseFragment false))
|
2014-11-30 15:40:22 +00:00
|
|
|
(let [h (History.)]
|
|
|
|
(when p
|
|
|
|
(.setToken h p))
|
|
|
|
h)))
|
2014-11-29 17:30:24 +00:00
|
|
|
|
2014-12-07 15:38:56 +00:00
|
|
|
(defonce history nil)
|
2014-11-29 17:30:24 +00:00
|
|
|
|
|
|
|
(defn token-base []
|
|
|
|
(if (use-html5-history)
|
|
|
|
(:base-path @config)))
|
|
|
|
|
2014-11-30 15:40:22 +00:00
|
|
|
(defn setup-history [p]
|
2014-11-29 17:30:24 +00:00
|
|
|
(when (nil? history)
|
2014-11-30 15:40:22 +00:00
|
|
|
(set! history (create-history p))
|
2014-11-29 17:30:24 +00:00
|
|
|
(swap! page-state assoc :has-history (some? history))
|
|
|
|
(when-let [h history]
|
|
|
|
(evt/listen h hevt/NAVIGATE
|
|
|
|
(fn [e]
|
|
|
|
(let [t (.-token e)
|
2014-11-30 08:40:12 +00:00
|
|
|
tb (token-base)]
|
|
|
|
(reset! page (if (and tb (== 0 (.indexOf t tb)))
|
|
|
|
(subs t (count tb))
|
2014-11-29 17:30:24 +00:00
|
|
|
t)))
|
|
|
|
(reagent/flush)))
|
|
|
|
(add-watch page ::history
|
|
|
|
(fn [_ _ oldp newp]
|
|
|
|
(when-not (= oldp newp)
|
|
|
|
(.setToken h (str (token-base) newp)))))
|
|
|
|
(.setEnabled h true))))
|
|
|
|
|
|
|
|
(defn base-path [loc p]
|
|
|
|
;; Find base-path for html5 history
|
|
|
|
(let [split #".[^/]*"
|
|
|
|
depth (->> (case p "" "." p) (re-seq split) count)
|
|
|
|
base (->> loc (re-seq split) (drop-last depth) (apply str))]
|
|
|
|
(string/replace (str base "/") #"^/" "")))
|
|
|
|
|
|
|
|
(defn set-start-page [p]
|
|
|
|
(when (and (not (:base-path @config))
|
|
|
|
(use-html5-history))
|
|
|
|
(swap! config assoc :base-path
|
|
|
|
(base-path (.' js/window -location.pathname) p)))
|
|
|
|
(reset! page p))
|
|
|
|
|
|
|
|
(defn prefix [href]
|
|
|
|
(let [depth (-> #"/" (re-seq @page) count)]
|
|
|
|
(str (->> "../" (repeat depth) (apply str)) href)))
|
|
|
|
|
|
|
|
|
|
|
|
;;; Static site generation
|
|
|
|
|
|
|
|
(defn body []
|
|
|
|
(let [b (:body @config)]
|
2015-01-31 21:02:30 +00:00
|
|
|
(assert (vector? b) (str "body is not a vector: " b))
|
|
|
|
b))
|
2014-11-29 17:30:24 +00:00
|
|
|
|
|
|
|
(defn danger [t s]
|
|
|
|
[t {:dangerouslySetInnerHTML {:__html s}}])
|
|
|
|
|
|
|
|
(defn html-template [{:keys [title body timestamp page-conf
|
|
|
|
opt-none req]}]
|
|
|
|
(let [c @config
|
2015-01-29 13:49:01 +00:00
|
|
|
main (str (:js-file c) timestamp)
|
|
|
|
css-file (:css-file c)
|
2014-11-29 17:30:24 +00:00
|
|
|
opt-none (:opt-none c)]
|
|
|
|
(reagent/render-to-static-markup
|
|
|
|
[:html
|
|
|
|
[:head
|
|
|
|
[:meta {:charset "utf-8"}]
|
|
|
|
[:meta {:name 'viewport
|
|
|
|
:content "width=device-width, initial-scale=1.0"}]
|
2015-01-29 13:49:01 +00:00
|
|
|
[:base {:href (prefix "")}]
|
2014-11-29 17:30:24 +00:00
|
|
|
[:link {:href (str css-file timestamp) :rel 'stylesheet}]
|
|
|
|
[:title title]]
|
|
|
|
[:body
|
2014-12-09 06:30:57 +00:00
|
|
|
[:div {:id (:main-div @config)}
|
|
|
|
(danger :div body)]
|
2014-11-29 17:30:24 +00:00
|
|
|
(danger :script (str "var pageConfig = " (-> page-conf
|
|
|
|
clj->js
|
|
|
|
js/JSON.stringify)))
|
2015-01-29 13:49:01 +00:00
|
|
|
[:script {:src main :type "text/javascript"}]]])))
|
2014-11-29 17:30:24 +00:00
|
|
|
|
|
|
|
(defn gen-page [page-name timestamp]
|
|
|
|
(reset! page page-name)
|
|
|
|
(let [b (reagent/render-component-to-string (body))]
|
|
|
|
(str "<!doctype html>"
|
2014-11-29 18:51:45 +00:00
|
|
|
(html-template {:title (get-title)
|
2014-11-29 17:30:24 +00:00
|
|
|
:body b
|
|
|
|
:page-conf {:allow-html5-history true
|
|
|
|
:page-name page-name}
|
|
|
|
:timestamp timestamp}))))
|
|
|
|
|
|
|
|
(defn mkdirs [f]
|
|
|
|
(let [fs (js/require "fs")
|
|
|
|
path (js/require "path")
|
|
|
|
items (as-> f _
|
|
|
|
(.' path dirname _)
|
|
|
|
(.' path normalize _)
|
|
|
|
(string/split _ #"/"))
|
|
|
|
parts (reductions #(str %1 "/" %2) items)]
|
|
|
|
(doseq [d parts]
|
|
|
|
(when-not (.' fs existsSync d)
|
|
|
|
(.' fs mkdirSync d)))))
|
|
|
|
|
|
|
|
(defn write-file [f content]
|
|
|
|
(let [fs (js/require "fs")]
|
|
|
|
(mkdirs f)
|
|
|
|
(.' fs writeFileSync f content)))
|
|
|
|
|
|
|
|
(defn read-file [f]
|
|
|
|
(let [fs (js/require "fs")]
|
|
|
|
(.' fs readFileSync f)))
|
|
|
|
|
|
|
|
(defn path-join [& paths]
|
|
|
|
(let [path (js/require "path")]
|
|
|
|
(apply (.' path :join) paths)))
|
|
|
|
|
|
|
|
(defn read-css []
|
2014-12-11 08:01:25 +00:00
|
|
|
(clojure.string/join "\n"
|
2014-11-29 17:30:24 +00:00
|
|
|
(map read-file (:css-infiles @config))))
|
|
|
|
|
|
|
|
(defn write-resources [dir]
|
|
|
|
(write-file (path-join dir (:css-file @config))
|
|
|
|
(read-css)))
|
|
|
|
|
|
|
|
|
|
|
|
;;; Main entry points
|
|
|
|
|
|
|
|
(defn ^:export genpages [opts]
|
|
|
|
(log "Generating site")
|
|
|
|
(swap! config merge (js->clj opts :keywordize-keys true))
|
|
|
|
(let [dir (:site-dir @config)
|
|
|
|
written (atom #{})
|
|
|
|
timestamp (str "?" (.' js/Date now))
|
|
|
|
one-page (fn [] (first (filter
|
|
|
|
(fn [x] (nil? (@written x)))
|
|
|
|
(keys (:page-map @config)))))]
|
|
|
|
(loop [f (one-page)]
|
|
|
|
(when f
|
|
|
|
(swap! written conj f)
|
|
|
|
(write-file (path-join dir f)
|
|
|
|
(gen-page f timestamp))
|
|
|
|
(recur (one-page))))
|
|
|
|
(write-resources dir))
|
|
|
|
(log "Wrote site"))
|
|
|
|
|
|
|
|
(defn start! [site-config]
|
|
|
|
(swap! config merge site-config)
|
|
|
|
(when reagent/is-client
|
|
|
|
(let [conf (when (exists? js/pageConfig)
|
|
|
|
(js->clj js/pageConfig :keywordize-keys true))
|
|
|
|
page-name (:page-name conf)]
|
2014-11-30 08:40:12 +00:00
|
|
|
(swap! config merge conf)
|
2014-12-07 15:38:56 +00:00
|
|
|
(when (nil? history)
|
|
|
|
(when page-name
|
|
|
|
(set-start-page page-name))
|
|
|
|
(setup-history page-name)
|
|
|
|
(set! (.-title js/document) (get-title)))
|
2014-11-29 17:30:24 +00:00
|
|
|
(reagent/render-component (body)
|
2014-12-09 06:30:57 +00:00
|
|
|
(.' js/document getElementById
|
|
|
|
(:main-div @config))))))
|