From b07f69fb5186bcc14b0879e5d2c434aaec93914e Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 1 Jun 2020 10:46:58 +0100 Subject: [PATCH] Upversioned to "0.1.6-SNAPSHOT" in line with mw-engine --- .gitignore | 2 + docs/uberdoc.html | 3690 +++++++++++++++++++ project.clj | 29 +- resources/public/docs/mw-engine | 1 + resources/public/docs/mw-parser | 1 + resources/public/docs/mw-ui | 1 + resources/public/img/heightmaps/mgi_med.png | Bin 0 -> 27805 bytes 7 files changed, 3710 insertions(+), 14 deletions(-) create mode 100644 docs/uberdoc.html create mode 120000 resources/public/docs/mw-engine create mode 120000 resources/public/docs/mw-parser create mode 120000 resources/public/docs/mw-ui create mode 100644 resources/public/img/heightmaps/mgi_med.png diff --git a/.gitignore b/.gitignore index cf11b31..7e819cf 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ resources/public/docs/mw-*/uberdoc.html # Artefacts: mw_ui.log pom.xml + +buildall.tmp.*/ diff --git a/docs/uberdoc.html b/docs/uberdoc.html new file mode 100644 index 0000000..4d7c64b --- /dev/null +++ b/docs/uberdoc.html @@ -0,0 +1,3690 @@ + +mw-ui -- Marginalia

mw-ui

0.1.5


Web-based user interface for MicroWorld

+

dependencies

org.clojure/clojure
1.6.0
mw-engine
0.1.5
mw-parser
0.1.5
lib-noir
0.8.4
ring-server
0.3.1
selmer
0.6.8
hiccup
1.0.5
com.taoensso/timbre
3.2.1
com.taoensso/tower
2.0.2
markdown-clj
0.9.44
environ
0.5.0
noir-exception
0.2.2



(this space intentionally left almost blank)
 

Set up and tear down the request handler.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.handler
+  (:require [compojure.core :refer [defroutes]]
+            [mw-ui.routes.home :refer [home-routes]]
+            [mw-ui.middleware :refer [load-middleware]]
+            [noir.response :refer [redirect]]
+            [noir.util.middleware :refer [app-handler]]
+            [compojure.route :as route]
+            [taoensso.timbre :as timbre]
+            [taoensso.timbre.appenders.rotor :as rotor]
+            [selmer.parser :as parser]
+            [environ.core :refer [env]]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defroutes app-routes
+  (route/resources "/")
+  (route/not-found "Not Found"))

init will be called once when + app is deployed as a servlet on + an app server such as Tomcat + put any initialization code here

+
(defn init
+  []
+  (timbre/set-config!
+    [:appenders :rotor]
+    {:min-level :info
+     :enabled? true
+     :async? false ; should be always false for rotor
+     :max-message-per-msecs nil
+     :fn rotor/appender-fn})
+  (timbre/set-config!
+    [:shared-appender-config :rotor]
+    {:path "mw_ui.log" :max-size (* 512 1024) :backlog 10})
+  (if (env :dev) (parser/cache-off!))
+  (timbre/info "mw-ui started successfully"))

destroy will be called when your application + shuts down, put any clean up code here

+
(defn destroy
+  []
+  (timbre/info "mw-ui is shutting down..."))
+
(def app (app-handler
+           ;; add your application routes here
+           [home-routes app-routes]
+           ;; add custom middleware here
+           :middleware (load-middleware)
+           ;; timeout sessions after 30 minutes
+           :session-options {:timeout (* 60 30)
+                             :timeout-response (redirect "/")}
+           ;; add access rules here
+           :access-rules []
+           ;; serialize/deserialize the following data formats
+           ;; available formats:
+           ;; :json :json-kw :yaml :yaml-kw :edn :yaml-in-html
+           :formats [:json-kw :edn]))
 

Layout content as HTML.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.layout
+  (:require [selmer.parser :as parser]
+            [clojure.string :as s]
+            [ring.util.response :refer [content-type response]]
+            [compojure.response :refer [Renderable]]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(def template-path "templates/")
+
(deftype RenderableTemplate [template params]
+  Renderable
+  (render [this request]
+    (content-type
+      (->> (assoc (merge params {:version (System/getProperty "mw-ui.version")})
+                  (keyword (s/replace template #".html" "-selected")) "active"
+                  :servlet-context
+                  (if-let [context (:servlet-context request)]
+                    (.getContextPath context)))
+        (parser/render-file (str template-path template))
+        response)
+      "text/html; charset=utf-8")))
+
(defn render [template & [params]]
+  (RenderableTemplate. template params))
 

In truth, boilerplate from Luminus.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.middleware
+  (:require [taoensso.timbre :as timbre]
+            [selmer.parser :as parser]
+            [environ.core :refer [env]]
+            [selmer.middleware :refer [wrap-error-page]]
+            [noir-exception.core
+              :refer [wrap-internal-error wrap-exceptions]]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn log-request [handler]
+  (fn [req]
+    (timbre/debug req)
+    (handler req)))
+
(def development-middleware
+  [log-request
+   wrap-error-page
+   wrap-exceptions])
+
(def production-middleware
+  [#(wrap-internal-error % :log (fn [e] (timbre/error e)))])
+
(defn load-middleware []
+  (concat (when (env :dev) development-middleware)
+          production-middleware))
 

Render the state of the world as an HTML table.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.render-world
+  (:require [clojure.java.io :as jio]
+            [mw-engine.core :as engine]
+            [mw-engine.world :as world]
+            [mw-engine.heightmap :as heightmap]
+            [mw-parser.bulk :as compiler]
+            [hiccup.core :refer [html]]
+            [noir.io :as io]
+            [noir.session :as session]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn format-css-class [statekey]
+  "Format this statekey, assumed to be a keyword indicating a state in the
+   world, into a CSS class"
+  (subs (str statekey) 1))

Render this statekey, assumed to be a keyword indicating a state in the + world, into a path which should recover the corresponding image file.

+
(defn format-image-path
+  [statekey]
+  (format "img/tiles/%s.png" (format-css-class statekey)))
+
(defn format-mouseover [cell]
+  (str cell))

Render this world cell as a Hiccup table cell.

+
(defn render-cell
+  [cell]
+  (let [state (:state cell)]
+    [:td {:class (format-css-class state) :title (format-mouseover cell)}
+     [:a {:href (format "inspect?x=%d&y=%d" (:x cell) (:y cell))}
+      [:img {:alt (:state cell) :src (format-image-path state)}]]]))

Render this world row as a Hiccup table row.

+
(defn render-world-row
+  [row]
+  (apply vector (cons :tr (map render-cell row))))

Render the world implied by the current session as a complete HTML table in a DIV.

+
(defn render-world-table
+  []
+  (let [world (or (session/get :world)
+                  (heightmap/apply-heightmap
+                      (io/get-resource "/img/heightmaps/small_hill.png")))
+        rules (or (session/get :rules)
+                  (do (session/put! :rules
+                                    (compiler/compile-file
+                                      (io/get-resource "/rulesets/basic.txt")))
+                    (session/get :rules)))
+        generation (inc (or (session/get :generation) 0))
+        w2 (engine/transform-world world rules)
+        ]
+    (session/put! :world w2)
+    (session/put! :generation generation)
+    [:div {:class "world"}
+      (apply vector
+                 (cons :table
+                       (map render-world-row w2)))
+      [:p
+       (str "Generation " generation)]]))

Render in Hiccup format the HTML content of an inspector on this cell.

+
(defn render-inspector
+  [cell table]
+  [:table {:class "music-ruled"}
+   [:tr
+    [:td {:colspan 2 :style "text-align: center;"}
+     [:img {:src (str "img/tiles/" (name (:state cell)) ".png")
+            :width 64
+            :height 64}]]]
+   [:tr [:th "Key"][:th "Value"]]
+   (map #(vector :tr (vector :th %)(vector :td (cell %))) (keys cell))])
 

In truth, boilerplate from Luminus.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.repl
+  (:use mw-ui.handler
+        ring.server.standalone
+        [ring.middleware file-info file])
+  (:gen-class))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defonce server (atom nil))
+
(defn get-handler []
+  ;; #'app expands to (var app) so that when we reload our code,
+  ;; the server is forced to re-resolve the symbol in the var
+  ;; rather than having its own copy. When the root binding
+  ;; changes, the server picks it up without having to restart.
+  (-> #'app
+      ; Makes static assets in $PROJECT_DIR/resources/public/ available.
+      (wrap-file "resources")
+      ; Content-Type, Content-Length, and Last Modified headers for files in body
+      (wrap-file-info)))

used for starting the server in development mode from REPL

+
(defn start-server
+  [& [port]]
+  (let [port (if port (Integer/parseInt port) 3000)]
+    (reset! server
+            (serve (get-handler)
+                   {:port port
+                    :init init
+                    :auto-reload? true
+                    :destroy destroy
+                    :join? false}))
+    (println (str "You can view the site at http://localhost:" port))))
+
(defn stop-server []
+  (.stop @server)
+  (reset! server nil))
+
(defn -main []
+  (start-server))
 

Routes which serve the main pages of the application.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.routes.home
+  (:use clojure.walk
+        compojure.core)
+  (:require [clojure.pprint :only [pprint]]
+            [hiccup.core :refer [html]]
+            [mw-engine.utils :as engine-utils]
+            [mw-ui.layout :as layout]
+            [mw-ui.render-world :as world]
+            [mw-ui.routes.load :as load]
+            [mw-ui.routes.rules :as rules]
+            [mw-ui.routes.params :as params]
+            [mw-ui.routes.save :as save]
+            [mw-ui.util :as util]
+            [noir.io :as io]
+            [noir.session :as session]
+            [ring.util.response :as response]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn list-states []
+  (sort
+    (filter #(not (nil? %))
+            (map #(first (rest (re-matches #"([0-9a-z-]+).png" (.getName %))))
+                 (file-seq (clojure.java.io/file "resources/public/img/tiles"))))))
+
(defn about-page []
+  (layout/render "trusted-content.html"
+                 {:title "About MicroWorld"
+                  :about-selected "active"
+                  :content (util/md->html "/md/about.md")
+                  :version (System/getProperty "mw-ui.version")}))
+
(defn docs-page []
+  (layout/render "docs.html" {:title "Documentation"
+                              :parser (util/md->html "/md/mw-parser.md" )
+                              :states (util/list-resources "/img/tiles" #"([0-9a-z-_]+).png")
+                              :lessons (util/list-resources "/md/lesson-plans"  #"([0-9a-z-_]+).md")
+                              :components ["mw-engine" "mw-parser" "mw-ui"]
+                              :version (System/getProperty "mw-ui.version")}))
+
(defn home-page []
+  "Render the home page."
+  (layout/render "trusted-content.html" {:title "Welcome to MicroWorld"
+                              :content (util/md->html "/md/mw-ui.md")
+                              :version (System/getProperty "mw-ui.version")}))
+
(defn inspect-page [request]
+  "Open an inspector on the cell at the co-ordinates specified in this request"
+  (let [params (keywordize-keys (:params request))
+        xs (:x params)
+        ys (:y params)
+        x (if (seq xs) (read-string xs) 0)
+        y (if (seq ys) (read-string ys) 0)
+        world (session/get :world)
+        cell (engine-utils/get-cell world x y)
+        state (:state params)]
+    (cond state
+      (do
+        (session/put! :world (engine-utils/set-property world cell :state (keyword state)))
+        (response/redirect "world"))
+      true
+      (layout/render "inspector.html"
+                     {:title (format "Inspect cell at %d, %d" x y)
+                      :content (html (world/render-inspector cell world))
+                      :cell cell
+                      :x (:x cell)
+                      :y (:y cell)
+                      :states (util/list-resources
+                                "/img/tiles" #"([0-9a-z-_]+).png")}))))

Render the markdown page specified in this request, if any. Probably undesirable, + should be removed.

+
(defn md-page
+  [request]
+  (let [params (keywordize-keys (:params request))
+        content (or (:content params) "missing.md")]
+    (layout/render "trusted-content.html"
+                   {:title "Welcome to MicroWorld"
+                    :content (util/md->html (str "/md/" content))})))
+
(defn world-page []
+  "Render the world in the current session (or a default one if none)."
+  (layout/render "trusted-content.html"
+                 {:title "Watch your world grow"
+                 :world-selected "active"
+                 :content (html (world/render-world-table))
+                 :pause (or (session/get :pause) 5)
+                 :maybe-refresh "refresh"}))
+
(defroutes home-routes
+  (GET  "/" [] (home-page))
+  (GET  "/about" [] (about-page))
+  (GET  "/docs"  [] (docs-page))
+  (GET  "/inspect" request (inspect-page request))
+  (POST "/inspect" request (inspect-page request))
+  (GET  "/load" [] (load/load-page))
+  (POST "/load" request (load/load-page request))
+  (GET  "/md" request (md-page request))
+  (GET  "/params" [] (params/params-page))
+  (POST "/params" request (params/params-page request))
+  (GET  "/rules" request (rules/rules-page request))
+  (POST "/rules" request (rules/rules-page request))
+  (GET  "/saved-map.mwm" [] (save/save-page))
+  (GET  "/world"  [] (world-page)))
 

Route which handles the upload of worlds/rules from the client.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.routes.load
+  (:use clojure.walk
+        compojure.core)
+  (:require [hiccup.core :refer [html]]
+            [noir.io :as io]
+            [noir.session :as session]
+            [ring.util.response :as response]
+            [mw-ui.layout :as layout]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn- upload [file]
+  (io/upload-file "/tmp/" file)
+  (cond
+   (session/put! :world
+                (with-open [eddi (java.io.FileReader. (:tempfile file))] (read)))
+    (str "Successfully loaded your world from " (:filename file))))

If no args, show the load form; with args, load a world file from the client.

+ +

NOTE that this reads a Clojure form from an untrusted client and should almost + certainly NOT be enabled on a public-facing site, especially not on the Internet.

+ +

TODO doesn't work yet.

+
(defn load-page
+  ([]
+   (load-page nil))
+  ([request]
+     (let [params (keywordize-keys (:params request))
+           file (:file request)]
+       (try
+         (layout/render "load.html"
+                        {:title "Load World"
+                         :message (upload file)})
+         (catch Exception any
+               (layout/render "load.html"
+                            {:title "Load World"
+                             :message "Failed to load your world"
+                             :error (str (.getName (.getClass any)) ": " (.getMessage any))}))))))
 

Route which serves and handles the parameters page.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.routes.params
+  (:use clojure.walk
+        clojure.java.io
+        compojure.core)
+  (:require [hiccup.core :refer [html]]
+            [mw-engine.heightmap :as heightmap]
+            [mw-parser.bulk :as compiler]
+            [mw-ui.layout :as layout]
+            [mw-ui.util :as util]
+            [mw-ui.render-world :as world]
+            [noir.io :as io]
+            [noir.session :as session]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn- send-params []
+  {:title "Choose your world"
+   :heightmaps (util/list-resources "/img/heightmaps" #"([0-9a-z-_]+).png")
+   :pause (or (session/get :pause) 5)
+   :rulesets (util/list-resources "/rulesets" #"([0-9a-z-_]+).txt")
+   })

Handler for params request. If no request passed, show empty params form. + If request is passed, put parameters from request into session and show + the world page.

+
(defn params-page
+  ([]
+    (layout/render "params.html" (send-params)))
+  ([request]
+    (try
+      (let [params (keywordize-keys (:form-params request))
+            map (:heightmap params)
+            pause (:pause params)
+            rulefile (:ruleset params)
+            rulepath (str "/rulesets/" rulefile ".txt")]
+        (if (not= map "")
+          (session/put! :world
+                        (heightmap/apply-heightmap
+                          (io/get-resource (str "/img/heightmaps/" map ".png")))))
+        (when (not= rulefile "")
+          (session/put! :rule-text (io/slurp-resource rulepath))
+          (session/put! :rules (compiler/compile-file (io/get-resource rulepath))))
+        (if (not= pause "")
+          (session/put! :pause pause))
+        (layout/render "params.html"
+                       (merge (send-params)
+                              {:r rulefile
+                               :h map
+                               :message "Your parameters are saved, now look at your world"})))
+      (catch Exception e
+        (let [params (keywordize-keys (:form-params request))]
+          (layout/render "params.html"
+                         (merge (send-params)
+                                {:title "Choose your world"
+                                 :r (:ruleset params)
+                                 :h (:heightmap params)
+                                 :message "Your paramters are not saved"
+                                 :error (str (.getName (.getClass e)) ": " (.getMessage e) "; " params)})))))))
 

Route which serves and handles the rules page.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.routes.rules
+  (:use clojure.walk
+        compojure.core)
+  (:require [hiccup.core :refer [html]]
+            [mw-parser.bulk :as compiler]
+            [mw-ui.layout :as layout]
+            [mw-ui.util :as util]
+            [mw-ui.render-world :as world]
+            [noir.io :as io]
+            [noir.session :as session]
+            [ring.util.response :as response]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn process-rules-request
+  [request]
+  (let [src (:src (keywordize-keys (:form-params request)))]
+      (try
+        (cond src
+          (let [rules (compiler/compile-string src)]
+            {:rule-text src
+             :rules rules
+             :message (str "Successfully compiled "
+                           (count rules)
+                           " rules")           })
+          true {:rule-text (or
+                             (session/get :rule-text)
+                             (io/slurp-resource "/rulesets/basic.txt"))
+                :message "No rules found in request; loading defaults"})
+        (catch Exception e
+          {:rule-text src
+           :message "An error occurred during compilation"
+           :error (str (.getName (.getClass e)) ": " (.getMessage e))}))))

Request handler for the rules request. If the request contains a value + for :src, treat that as rule source and try to compile it. If compilation + succeeds, stash the compiled rules and the rule text on the session, and + provide feedback; if not, provide feedback.

+ +

If request doesn't contain a value for :src, load basic rule source from + the session or from resources/rulesets/basic.txt and pass that back.

+
(defn rules-page
+  ([request]
+    (let [processed (process-rules-request request)]
+      (if (:rules processed)
+        (session/put! :rules (:rules processed)))
+      (if (:rule-text processed)
+        (session/put! :rule-text (:rule-text processed)))
+      (layout/render "rules.html"
+                     (merge {:title "Edit Rules"} processed))))
+  ([]
+    (rules-page nil)))
 

Route which handles the saving of world state the client.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.routes.save
+  (:require [clojure.pprint :as pretty :only [pprint]]
+            [noir.session :as session]
+            [noir.response :as response]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+
+
(defn save-page []
+  "Save the current world to the browser, using our own custom mime-type in
+   an attempt to prevent the browser trying to do anything clever with it.
+   Note that it is saved as a raw Clojure data structure, not as XML or
+   any proprietary format."
+  (response/content-type
+    "application/journeyman-mwm; charset=utf-8"
+    (with-out-str (pretty/pprint  (session/get :world)))))
 

Utility functions used by other namespaces in this package.

+
(ns ^{:doc 
+      :author "Simon Brooke"}
+  mw-ui.util
+  (:require [noir.io :as io]
+            [noir.session :as session]
+            [markdown.core :as md]))

mw-ui: a servlet user/visualisation interface for MicroWorld.

+ +

This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version.

+ +

This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details.

+ +

You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +USA.

+ +

Copyright (C) 2014 Simon Brooke

+

reads a markdown file from public/md and returns an HTML string

+
(defn md->html
+  [filename]
+  (->>
+    (io/slurp-resource filename)
+    (md/md-to-html-string)))
+
(defn list-resources [directory pattern]
+  "List resource files matching `pattern` in `directory`."
+  (let
+    [path (str (io/resource-path) directory)]
+    (session/put! :list-resources-path path)
+    (sort
+      (remove nil?
+            (map #(first (rest (re-matches pattern (.getName %))))
+                 (file-seq (clojure.java.io/file path)))))))
 
\ No newline at end of file diff --git a/project.clj b/project.clj index a3a3dfe..2fee3f8 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject mw-ui "0.1.5-SNAPSHOT" +(defproject mw-ui "0.1.6-SNAPSHOT" :description "Web-based user interface for MicroWorld" :url "http://www.journeyman.cc/microworld" :manifest { @@ -8,17 +8,18 @@ "build-signature-timestamp" "unset" "Implementation-Version" "unset" } - :dependencies [[org.clojure/clojure "1.6.0"] - [mw-engine "0.1.5-SNAPSHOT"] - [mw-parser "0.1.5-SNAPSHOT"] - [lib-noir "0.8.4"] - [ring-server "0.3.1"] - [selmer "0.6.8"] - [com.taoensso/timbre "3.2.1"] - [com.taoensso/tower "2.0.2"] - [markdown-clj "0.9.44"] - [environ "0.5.0"] - [noir-exception "0.2.2"]] + :dependencies [[org.clojure/clojure "1.8.0"] + [mw-engine "0.1.6-SNAPSHOT"] + [mw-parser "0.1.6-SNAPSHOT"] + [lib-noir "0.9.9"] + [ring-server "0.5.0"] + [selmer "1.12.25"] + [hiccup "1.0.5"] + [com.taoensso/timbre "4.10.0"] + [com.taoensso/tower "3.0.2"] + [markdown-clj "1.10.4"] + [environ "1.2.0"] + [noir-exception "0.2.5"]] :repl-options {:init-ns mw-ui.repl} :plugins [[lein-ring "0.8.11"] @@ -40,8 +41,8 @@ :stacktraces? false :auto-reload? false}} :dev {:dependencies [[ring-mock "0.1.5"] - [ring/ring-devel "1.3.0"] - [pjstadig/humane-test-output "0.6.0"]] + [ring/ring-devel "1.8.1"] + [pjstadig/humane-test-output "0.10.0"]] :injections [(require 'pjstadig.humane-test-output) (pjstadig.humane-test-output/activate!)] :env {:dev true}}} diff --git a/resources/public/docs/mw-engine b/resources/public/docs/mw-engine new file mode 120000 index 0000000..09eca7a --- /dev/null +++ b/resources/public/docs/mw-engine @@ -0,0 +1 @@ +../../../../mw-engine/docs \ No newline at end of file diff --git a/resources/public/docs/mw-parser b/resources/public/docs/mw-parser new file mode 120000 index 0000000..5ed9c16 --- /dev/null +++ b/resources/public/docs/mw-parser @@ -0,0 +1 @@ +../../../../mw-parser/docs \ No newline at end of file diff --git a/resources/public/docs/mw-ui b/resources/public/docs/mw-ui new file mode 120000 index 0000000..a2968b0 --- /dev/null +++ b/resources/public/docs/mw-ui @@ -0,0 +1 @@ +../../../../mw-ui/docs \ No newline at end of file diff --git a/resources/public/img/heightmaps/mgi_med.png b/resources/public/img/heightmaps/mgi_med.png new file mode 100644 index 0000000000000000000000000000000000000000..cb4e875a1dad444cee571bc9d3b50ab8df457b26 GIT binary patch literal 27805 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 E2L~