diff --git a/.gitignore b/.gitignore index e653c91..24ebcf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1 @@ -Makefile -PrivateMakefile dump.rdb -resources/public/cljs -pom.xml -*jar -/lib/ -/classes/ -.lein-* -.crossover-cljs -target/ -.nrepl-port -.idea/ -NoteHub.iml diff --git a/Procfile b/Procfile deleted file mode 100644 index 0fef382..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: lein with-profile production ring server-headless $PORT diff --git a/project.clj b/project.clj deleted file mode 100644 index 42b4412..0000000 --- a/project.clj +++ /dev/null @@ -1,26 +0,0 @@ -(defproject NoteHub "2.0.0" - :description "A free and anonymous hosting for markdown pages." - :dependencies [[org.clojure/clojure "1.6.0"] - [org.clojure/core.cache "0.6.4"] - [hiccup "1.0.5"] - [zeus "0.1.0"] - [garden "1.2.5"] - [org.pegdown/pegdown "1.4.2"] - [iokv "0.1.1"] - [cheshire "5.3.1"] - [ring "1.3.1"] - [com.taoensso/carmine "2.7.0" :exclusions [org.clojure/clojure]] - [compojure "1.2.0"]] - :main notehub.handler - :min-lein-version "2.0.0" - :plugins [[lein-ring "0.8.12"]] - :ring {:handler notehub.handler/app} - :profiles {:uberjar {:aot :all} - :production {:ring {:auto-reload? false - :auto-refresh? false}} - :dev {:ring {:auto-reload? true - :auto-refresh? true} - :dependencies - [[javax.servlet/servlet-api "2.5"] - [ring-mock "0.1.5"]]}} - :jvm-opts ["-Dfile.encoding=utf-8"]) diff --git a/scripts/expose.sh b/scripts/expose.sh deleted file mode 100755 index f70f4fc..0000000 --- a/scripts/expose.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -unset DEVMODE - -if ! pgrep "redis-server" > /dev/null; then - redis-server & -fi - -lein uberjar -java -jar target/NoteHub-2.0.0-standalone.jar diff --git a/scripts/run.sh b/scripts/run.sh deleted file mode 100755 index e62edac..0000000 --- a/scripts/run.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -export DEVMODE=1 - -if ! pgrep "redis-server" > /dev/null; then - redis-server & -fi - -lein ring server-headless 8080 diff --git a/src/notehub/api.clj b/src/notehub/api.clj deleted file mode 100644 index 2a920f5..0000000 --- a/src/notehub/api.clj +++ /dev/null @@ -1,126 +0,0 @@ -(ns notehub.api - (:import - [java.util Calendar]) - (:use - [iokv.core] - [ring.util.codec :only [url-encode]] - [clojure.string :rename {replace sreplace} - :only [replace blank? trim lower-case split-lines split]]) - (:require - [ring.util.codec] - [hiccup.util :as util] - [notehub.storage :as storage])) - -(def version "1.4") - -(defn log - "Logs args to the server stdout" - [string & args] - (apply printf (str "%s:" string) (str (storage/get-current-date) ":LOG") args) - (println)) - -(defn url - "Creates a local url from the given substrings" - [& args] - (apply str (interpose "/" (cons "" (map url-encode args))))) - -; Concatenates all fields to a string -(defn build-key - "Returns a storage-key for the given note coordinates" - [year month day title] - (apply str (interpose "/" [year month day title]))) - -(defn derive-title [md-text] - (sreplace (first (split-lines md-text)) - #"(#+|_|\*+|<.*?>)" "")) - -(defn get-date - "Returns today's date" - [] - (map #(+ (second %) (.get (Calendar/getInstance) (first %))) - {Calendar/YEAR 0, Calendar/MONTH 1, Calendar/DAY_OF_MONTH 0})) - -(defn- create-response - ([success] {:success success}) - ([success message & params] - (assoc (create-response success) :message (apply format message params)))) - -(defn- get-path [host-url token & [description]] - (if (= :url description) - (str host-url "/" token) - (let [[year month day title] (split token #"/")] - (if description - (str host-url "/" (storage/create-short-url token {:year year :month month :day day :title title})) - (str host-url (url year month day title)))))) - -(defn version-manager [f params] - (if-let [req-version (:version params)] - (let [req-version (Double/parseDouble req-version) - version (Double/parseDouble version)] - (if (< req-version version) - {:status (create-response false "Deprecated API version")} - (f params))) - {:status (create-response false "API version expected")})) - -(defn get-note [{:keys [noteID hostURL]}] - (if (storage/note-exists? noteID) - (let [note (storage/get-note noteID)] - (storage/increment-note-view noteID) - {:note note - :title (derive-title note) - :longURL (get-path hostURL noteID) - :shortURL (get-path hostURL noteID :id) - :statistics (storage/get-note-statistics noteID) - :status (create-response true) - :publisher (storage/get-publisher noteID)}) - {:status (create-response false "noteID '%s' unknown" noteID)})) - -(defn propose-title [note] - (let [raw-title (filter #(or (= \- %) (Character/isLetterOrDigit %)) - (-> note derive-title trim (sreplace " " "-") lower-case)) - max-length (get-setting :max-title-length #(Integer/parseInt %) 80)] - (apply str (take max-length raw-title)))) - -(defn post-note - [{:keys [note pid signature password hostURL] :as params}] - ;(log "post-note: %s" {:pid pid :signature signature :password password :note note}) - (let [errors (filter identity - [(when-not (storage/valid-publisher? pid) "pid invalid") - (when-not (= signature (storage/sign pid (storage/get-psk pid) note)) - "signature invalid") - (when (blank? note) "note is empty")])] - (if (empty? errors) - (let [[year month day] (map str (get-date)) - params (select-keys params [:text-size :header-size :text-font :header-font :theme]) - proposed-title (propose-title note) - title (first (drop-while #(storage/note-exists? (build-key year month day %)) - (cons proposed-title - (map #(str proposed-title "-" (+ 2 %)) (range))))) - noteID (build-key year month day title) - new-params (assoc params :year year :month month :day day :title title) - short-url (get-path hostURL (storage/create-short-url noteID new-params) :url) - long-url (get-path hostURL noteID)] - (do - (storage/add-note noteID note pid password) - {:noteID noteID - :longURL (if (empty? params) long-url (str (util/url long-url params))) - :shortURL short-url - :status (create-response true)})) - {:status (create-response false (first errors))}))) - - -(defn update-note [{:keys [noteID note pid signature password hostURL]}] - ;(log "update-note: %s" {:pid pid :noteID noteID :signature signature :password password :note note}) - (let [errors (filter identity - [(when-not (storage/valid-publisher? pid) "pid invalid") - (when-not (= signature (storage/sign pid (storage/get-psk pid) noteID note password)) - "signature invalid") - (when (blank? note) "note is empty") - (when-not (storage/valid-password? noteID password) "password invalid")])] - (if (empty? errors) - (do - (storage/edit-note noteID note) - {:longURL (get-path hostURL noteID) - :shortURL (get-path hostURL noteID :id) - :status (create-response true)}) - {:status (create-response false (first errors))}))) diff --git a/src/notehub/css.clj b/src/notehub/css.clj deleted file mode 100644 index 4597010..0000000 --- a/src/notehub/css.clj +++ /dev/null @@ -1,260 +0,0 @@ -(ns notehub.css - (:require [garden.core :refer [css]] - [garden.stylesheet :refer [at-media]] - [garden.units :as u :refer [px pt em]])) - -(def themes - { - "dark" - { - :background { - :normal "#333", - :halftone "#444" - }, - :foreground { - :normal "#ccc", - :halftone "#bbb" - }, - :link { - :fresh "#6b8", - :visited "#496", - :hover "#7c9" - } - }, - "solarized-light" - { - :background { - :normal "#fdf6e3", - :halftone "#eee8d5" - }, - :foreground { - :normal "#657b83", - :halftone "#839496" - }, - :link { - :fresh "#b58900", - :visited "#cb4b16", - :hover "#dc322f" - } - }, - "solarized-dark" - { - :background { - :normal "#073642", - :halftone "#002b36" - }, - :foreground { - :normal "#93a1a1", - :halftone "#839191" - }, - :link { - :fresh "#cb4b16", - :visited "#b58900", - :hover "#dc322f" - } - }, - "default" - { - :background { - :normal "#fff", - :halftone "#efefef" - }, - :foreground { - :normal "#333", - :halftone "#888" - }, - :link { - :fresh "#097", - :visited "#054", - :hover "#0a8" - } - } - } - ) - -(defn generate [params] - (let [theme (themes (params "theme" "default")) - ; VARIABLES - background (get-in theme [:background :normal]) - foreground (get-in theme [:foreground :normal]) - background-halftone (get-in theme [:background :halftone]) - foreground-halftone (get-in theme [:foreground :halftone]) - link-fresh (get-in theme [:link :fresh]) - link-visited (get-in theme [:link :visited]) - link-hover (get-in theme [:link :hover]) - width (px 800) - header-font (or (params "header-font") "Noticia Text") - text-font (or (params "text-font") "Georgia") - header-size-factor (Float/parseFloat (or (params "header-size") "1")) - text-size-factor (Float/parseFloat (or (params "text-size") "1")) - - ; MIXINS - helvetica { - :font-weight 300 - :font-family "'Helvetica Neue','Helvetica','Arial','Lucida Grande','sans-serif'" - } - central-element { - :margin-left "auto" - :margin-right "auto" - } - thin-border { - :border (print-str "1px solid" foreground) - }] - (css - [:.ui-border { :border-radius (px 3) } thin-border] - - [:a { - :color link-fresh - :text-decoration "none" - :border-bottom "1px dotted" - }] - [:a:hover { :color link-hover }] - [:a:visited { :color link-visited }] - [:#draft { - :margin-bottom (em 3) - }] - [:.button { - :cursor "pointer" - }] - [:.ui-elem { - :border-radius (px 3) - :padding (em 0.3) - :opacity 0.8 - :font-size (em 1) - :background background - } - helvetica thin-border] - [:.landing-button, :textarea, :fieldset { :border "none" }] - [:.landing-button { - :box-shadow "0 2px 5px #aaa" - :text-decoration "none" - :font-size (em 1.5) - :background "#0a2" - :border-radius (px 10) - :padding (px 10) - } - helvetica] - [:.landing-button:hover { :background "#0b2" }] - - [:.helvetica helvetica] - - [:#footer { - :width "100%" - :font-size (em 0.8) - :padding-bottom (em 1) - :text-align "center" - } - helvetica] - (at-media {:screen true :max-width (px 767)} [:#footer {:font-size (em 0.4)}]) - ["#footer a" { :border "none" }] - - [:html, :body { - :background background - :color foreground - :margin 0 - :padding 0 - }] - [:#hero { - :padding-top (em 5) - :padding-bottom (em 5) - :text-align "center" - }] - [:h1, :h2, :h3, :h4, :h5, :h6 { - :font-weight "bold" - :font-family (str header-font ",'Noticia Text','PT Serif','Georgia'") - }] - [:h1 { :font-size (em (* 1.8 header-size-factor)) }] - [:h2 { :font-size (em (* 1.6 header-size-factor)) }] - [:h3 { :font-size (em (* 1.4 header-size-factor)) }] - [:h4 { :font-size (em (* 1.2 header-size-factor)) }] - [:h5 { :font-size (em (* 1.1 header-size-factor)) }] - [:h6 { :font-size (em (* 1 header-size-factor)) }] - - ["#hero h1" { :font-size (em 2.5) }] - ["#hero h2" { :margin (em 2) } helvetica ] - - [:article { - :font-family (str text-font ", 'Georgia'") - :margin-top (em 5) - :text-align "justify" - :flex 1 - :-webkit-flex 1 - } - central-element] - - (at-media {:screen true :min-width (px 1024)} [:article {:width width}]) - (at-media {:screen true :max-width (px 1023)} [:article {:width "90%"}]) - - [:.central-element central-element] - - (at-media {:screen true :min-width (px 1024)} [:.central-element {:width width}]) - (at-media {:screen true :max-width (px 1023)} [:.central-element {:width "90%"}]) - - ["article img" { :max-width "100%" }] - - ["article p" { - :font-size (em (* 1.2 text-size-factor)) - :line-height "140%" - }] - - ["article > h1:first-child" { - :text-align "center" - :font-size (em (* 2 header-size-factor)) - :margin (em 2) - }] - - [:.centered { :text-align "center" }] - [:.bottom-space { :margin-bottom (em 7) }] - [:code, :pre { - :font-family "monospace" - :background background-halftone - :font-size (em (* 1.2 text-size-factor)) - }] - - [:pre { - :border-radius (px 3) - :padding (em 0.5) - :border (str "1px dotted" foreground-halftone) - }] - - ["*:focus" { :outline "0px none transparent" }] - (at-media {:screen true :min-width (px 1024)} [:textarea {:width width}]) - - [:textarea { - :border-radius (px 5) - :font-family "Courier" - :font-size (em 1) - :height (px 500) - }] - [:.hidden { :display "none" }] - [:#dashed-line { - :border-bottom (str "1px dashed" foreground-halftone) - :margin-top (em 3) - :margin-bottom (em 3) - }] - [:table { - :width "100%" - :border-collapse "collapse" - } - helvetica] - [:th { - :padding (em 0.3) - :line-height (em 2.5) - :background-color background-halftone - }] - [:td { - :border-top (str "1px dotted" foreground-halftone) - :padding (em 0.3) - :line-height (em 2.5) - }] - [:.middot { :padding (em 0.5) }] - - [:body { :display "-webkit-flex" }] - - [:body { - :display "flex" - :min-height "100vh" - :flex-direction "column" - :-webkit-flex-direction "column" - }] - ))) diff --git a/src/notehub/handler.clj b/src/notehub/handler.clj deleted file mode 100644 index 0f30409..0000000 --- a/src/notehub/handler.clj +++ /dev/null @@ -1,169 +0,0 @@ -(ns notehub.handler - (:use - compojure.core - iokv.core - notehub.views - [clojure.string :rename {replace sreplace} :only [replace]]) - (:require - [ring.adapter.jetty :as jetty] - [clojure.core.cache :as cache] - [hiccup.util :as util] - [compojure.handler :as handler] - [compojure.route :as route] - [notehub.api :as api] - [notehub.storage :as storage] - [cheshire.core :refer :all]) - (:gen-class)) - -(defn current-timestamp [] - (quot (System/currentTimeMillis) 100000000)) - -; note page cache -(def page-cache (atom (cache/lru-cache-factory {}))) - -; TODO: make sure the status is really set to the response!!!! -(defn- response - "Sets a custom message for each needed HTTP status. - The message to be assigned is extracted with a dynamically generated key" - [code] - {:status code - :body (let [message (get-message (keyword (str "status-" code)))] - (layout :no-js {} message - [:article [:h1 message]]))}) - -(defn redirect [url] - {:status 302 - :headers {"Location" (str url)} - :body ""}) - -(defn return-content-type [ctype content] - {:headers {"Content-Type" ctype} - :body content}) - -(defroutes api-routes - (GET "/note" {params :params} - (generate-string (api/version-manager api/get-note params))) - - (POST "/note" {params :params} - (generate-string (api/version-manager api/post-note params))) - - (PUT "/note" {params :params} - (generate-string (api/version-manager api/update-note params)))) - -(defroutes app-routes - (GET "/api" [] (layout :no-js {} (get-message :api-title) - [:article (md-to-html (slurp "API.md"))])) - - (context "/api" [] - #(ring.util.response/content-type (api-routes %) "application/json")) - - (GET "/" [] landing-page) - - (POST "/propose-title" {body :body} - (let [note (slurp body)] - (return-content-type - "text/plain; charset=utf-8" - (api/propose-title note)))) - - (GET "/:year/:month/:day/:title/export" [year month day title] - (when-let [md-text (:note (api/get-note {:noteID (api/build-key year month day title)}))] - (return-content-type "text/plain; charset=utf-8" md-text))) - - (GET "/:year/:month/:day/:title/stats" [year month day title] - (let [note-id (api/build-key year month day title)] - (statistics-page (api/derive-title (storage/get-note note-id)) - (storage/get-note-statistics note-id) - (storage/get-publisher note-id)))) - - (GET "/:year/:month/:day/:title/edit" [year month day title] - (let [note-id (api/build-key year month day title)] - (note-update-page - note-id - (:note (api/get-note {:noteID note-id}))))) - - (GET "/new" [] (new-note-page - (str - (current-timestamp) - (storage/sign (rand-int Integer/MAX_VALUE))))) - - (GET "/:year/:month/:day/:title" [year month day title :as params] - (let [params (assoc (:query-params params) - :year year :month month :day day :title title) - note-id (api/build-key year month day title) - short-url (storage/create-short-url note-id params)] - (when (storage/note-exists? note-id) - (if (cache/has? @page-cache short-url) - (do - (swap! page-cache cache/hit short-url) - (storage/increment-note-view note-id)) - (swap! page-cache cache/miss short-url - (note-page (api/get-note {:noteID note-id}) - (api/url short-url) - params))) - (cache/lookup @page-cache short-url)))) - - (GET "/:short-url" [short-url] - (when-let [params (storage/resolve-url short-url)] - (let [{:keys [year month day title]} params - rest-params (dissoc params :year :month :day :title) - core-url (api/url year month day title) - long-url (if (empty? rest-params) core-url (util/url core-url rest-params))] - (redirect long-url)))) - - (POST "/post-note" [session note signature password] - (if (and session - (.startsWith session - (str (current-timestamp))) - (= signature (storage/sign session note))) - (let [pid "NoteHub" - psk (storage/get-psk pid) - params {:note note - :pid pid - :signature (storage/sign pid psk note) - :password password}] - (if (storage/valid-publisher? pid) - (let [resp (api/post-note params)] - (if (get-in resp [:status :success]) - (redirect (:longURL resp)) - (response 400))) - (response 500))) - (response 400))) - - (POST "/update-note" [noteID note password] - (let [pid "NoteHub" - psk (storage/get-psk pid) - params {:noteID noteID :note note :password password :pid pid}] - (if (storage/valid-publisher? pid) - (let [resp (api/update-note (assoc params - :signature - (storage/sign pid psk noteID note password)))] - (if (get-in resp [:status :success]) - (do - (doseq [url (storage/get-short-urls noteID)] - (swap! page-cache cache/evict url)) - (redirect (:longURL resp))) - (response 403))) - (response 500)))) - - (route/resources "/") - (route/not-found (response 404))) - -(def app - (let [handler (handler/site app-routes)] - (fn [request] - (let [{:keys [server-name server-port]} request - hostURL (str "https://" server-name - (when (not= 80 server-port) (str ":" server-port))) - request (assoc-in request [:params :hostURL] hostURL)] - (if (get-setting :dev-mode) - (handler request) - (try (handler request) - (catch Exception e - (do - ;TODO (log e) - (response 500))))))))) - -(defn -main [& [port]] - (jetty/run-jetty #'app - {:port (if port (Integer/parseInt port) 8080)})) - diff --git a/src/notehub/storage.clj b/src/notehub/storage.clj deleted file mode 100644 index 450784a..0000000 --- a/src/notehub/storage.clj +++ /dev/null @@ -1,147 +0,0 @@ -(ns notehub.storage - (:use [iokv.core] - [zeus.core] - [clojure.string :only (blank? replace) :rename {replace sreplace}]) - (:require [taoensso.carmine :as car :refer (wcar)])) - -(def conn {:pool {} :spec {:uri (get-setting :db-url)}}) - -(let [md5Instance (java.security.MessageDigest/getInstance "MD5")] - (defn sign - "Returns the MD5 hash for the concatenation of all passed parameters" - [& args] - (let [input (sreplace (apply str args) #"\r+" "")] - (do (.reset md5Instance) - (.update md5Instance (.getBytes input)) - (apply str - (map #(let [c (Integer/toHexString (bit-and 0xff %))] - (if (= 1 (count c)) (str "0" c) c)) - (.digest md5Instance))))))) - -(defmacro redis [cmd & body] - `(car/wcar conn - (~(symbol "car" (name cmd)) - ~@body))) - -(defn get-current-date [] - (.getTime (java.util.Date.))) - -(defn valid-publisher? [pid] - (= 1 (redis :hexists :publisher-key pid))) - -(defn register-publisher [pid] - "Returns nil if given PID exists or a PSK otherwise" - (when (not (valid-publisher? pid)) - (let [psk (sign (str (rand-int Integer/MAX_VALUE) pid))] - (redis :hset :publisher-key pid psk) - psk))) - -(when (and (get-setting :dev-mode) - (not (valid-publisher? "NoteHub")) - (register-publisher "NoteHub"))) - -(defn revoke-publisher [pid] - (redis :hdel :publisher-key pid)) - -(defn get-psk [pid] - (redis :hget :publisher-key pid)) - -(defn edit-note [noteID text] - (redis :hset :edited noteID (get-current-date)) - (redis :hset :note noteID (zip text))) - -(defn add-note - ([noteID text pid] (add-note noteID text pid nil)) - ([noteID text pid passwd] - (redis :hset :note noteID (zip text)) - (redis :hset :published noteID (get-current-date)) - (redis :hset :publisher noteID pid) - (when (not (blank? passwd)) - (redis :hset :password noteID passwd)))) - -(defn valid-password? [noteID passwd] - (let [stored (redis :hget :password noteID)] - (and (not= 0 stored) (= stored passwd)))) - -(defn get-note-views [noteID] - (redis :hget :views noteID)) - -(defn get-publisher [noteID] - (redis :hget :publisher noteID)) - -(defn get-note-statistics [noteID] - {:views (get-note-views noteID) - :published (redis :hget :published noteID) - :edited (redis :hget :edited noteID)}) - -(defn note-exists? [noteID] - (= 1 (redis :hexists :note noteID))) - -(defn get-note [noteID] - (when (note-exists? noteID) - (unzip (redis :hget :note noteID)))) - -(defn increment-note-view [noteID] - (redis :hincrby :views noteID 1)) - -(defn short-url-exists? [url] - (= 1 (redis :hexists :short-url url))) - -(defn resolve-url [url] - (let [value (redis :hget :short-url url)] - (when value ; TODO: necessary? - (read-string value)))) - -(defn delete-short-url [url] - (when-let [params (redis :hget :short-url url)] - (redis :hdel :short-url params) - (redis :hdel :short-url url))) - -(defn get-short-urls [noteID] - (redis :smembers (str noteID :urls))) - -(defn delete-note [noteID] - (doseq [kw [:password :views :note :published :edited :publisher]] - (redis :hdel kw noteID)) - (doseq [url (get-short-urls noteID)] - (delete-short-url url)) - (redis :del (str noteID :urls))) - -(defn create-short-url - "Creates a short url for the given request metadata or extracts - one if it was already created" - [noteID params] - (let [key (str (into (sorted-map) (clojure.walk/keywordize-keys params)))] - (if (short-url-exists? key) - (redis :hget :short-url key) - (let [hash-stream (partition 5 (repeatedly #(rand-int 36))) - hash-to-string (fn [hash] - (apply str - ; map first 10 numbers to digits - ; and the rest to chars - (map #(char (+ (if (< 9 %) 87 48) %)) hash))) - url (first - (remove short-url-exists? - (map hash-to-string hash-stream)))] - ; we create two mappings: key params -> short url and back, - ; s.t. we can later easily check whether a short url already exists - (redis :hset :short-url url key) - (redis :hset :short-url key url) - ; we save all short urls of a note for removal later - (redis :sadd (str noteID :urls) url) - url)))) - -(defn gc [password dry] - (println (get-setting :admin-pw)) - (when (= password (get-setting :admin-pw)) - (let [N 30 - timestamp (- (get-current-date) (* N 24 60 60 1000)) - all-notes (map first (partition 2 (redis :hgetall :note))) - old-notes (filter #(< (Long/parseLong (redis :hget :published %)) timestamp) all-notes) - unpopular-notes (filter #(< (try (Long/parseLong (redis :hget :views %)) - (catch Exception a 0)) N) old-notes)] - (println "timestamp:" (str (java.util.Date. timestamp))) - (doseq [note-id unpopular-notes] - (do (println (if dry "to be deleted" "deleting") note-id) - (when-not dry (delete-note note-id)))) - (println (count unpopular-notes) "deleted")))) diff --git a/src/notehub/views.clj b/src/notehub/views.clj deleted file mode 100644 index 391e75a..0000000 --- a/src/notehub/views.clj +++ /dev/null @@ -1,124 +0,0 @@ -(ns notehub.views - (:use - iokv.core - [clojure.string :rename {replace sreplace} :only [replace]] - [hiccup.form] - [hiccup.core] - [hiccup.element] - [hiccup.util :only [escape-html]] - [hiccup.page :only [include-js html5]]) - (:require [notehub.css :as css]) - (:import (org.pegdown PegDownProcessor Extensions))) - -(def get-message (get-map "messages")) - -(def md-processor - (PegDownProcessor. (int (bit-and-not Extensions/ALL Extensions/HARDWRAPS)))) - -(defn md-to-html [md-text] - (.markdownToHtml md-processor md-text)) - -; Creates the main html layout -(defn layout - [js? style title & content] - (html5 - [:head - [:title (print-str (get-message :name) "—" title)] - [:meta {:charset "UTF-8"}] - [:meta {:name "viewport" :content "width=device-width, initial-scale=1.0"}] - [:link {:rel "stylesheet" :type "text/css" - :href - (format "https://fonts.googleapis.com/css?family=PT+Serif:700|Noticia+Text:700%s&subset=latin,cyrillic" - (reduce (fn [acc e] - (if-let [font (style e)] - (str acc "|" (sreplace font #" " "+")) - acc)) "" ["text-font" "header-font"]))}] - [:style (css/generate style)] - (if (= :js js?) - (html - (include-js "//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js") - (include-js "//cdnjs.cloudflare.com/ajax/libs/blueimp-md5/1.0.1/js/md5.min.js") - (include-js "/js/publishing.js") - [:body {:onload "onLoad()"} content]) - [:body content])])) - -(defn- sanitize - "Breakes all usages of