├── .gitignore ├── README.org ├── deps.edn ├── docs └── uberdoc.html ├── project.clj ├── src └── orgmode │ ├── block.clj │ ├── core.clj │ ├── html.clj │ ├── inline.clj │ └── util.clj └── test └── orgmode ├── core_test.clj ├── inline_test.clj └── test.org /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | *~ 12 | .cpcache/* 13 | 14 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Orgmode 2 | 3 | A Clojure library designed to parse org-mode files into clojure data 4 | structures. 5 | 6 | ** Status 7 | Usable, but not perfect, parse the [[test/orgmode/test.org][test]] file to see some of the issues. 8 | 9 | ** Installation 10 | 11 | Using =deps=, add the coordinate to your =deps.edn=: 12 | 13 | #+BEGIN_SRC clojure 14 | {:deps { 15 | orgmode {:git/url "https://github.com/bnbeckwith/orgmode" 16 | :sha "52c153649ebff4c90f50be32458d413ef416aeb7"} 17 | }} 18 | #+END_SRC 19 | 20 | If you are using =lein=, just add it as a dependency: 21 | 22 | #+BEGIN_SRC clojure 23 | [bnbeckwith.com/orgmode "0.7.5"] 24 | #+END_SRC 25 | 26 | ** Usage 27 | 28 | In your favorite terminal, launch a repl with 29 | 30 | #+begin_src bash 31 | clojure -Adev 32 | #+end_src 33 | 34 | #+BEGIN_SRC clojure 35 | (ns main 36 | (:require [orgmode.core :as org])) 37 | 38 | (org/parse-str "* Headline") 39 | ; {:content [{:type :headline, :text "Headline", :todo nil, :level 1, :content [], :tags nil}], :level 0} 40 | 41 | (org/parse-str "[[http://clojure.org][Clojure]]") 42 | ; {:content [{:inline true, :type :link, :uri "http://clojure.org", :content ["Clojure"]}], :level 0} 43 | 44 | (org/parse "File.org") 45 | ; File structure 46 | #+END_SRC 47 | 48 | * License 49 | 50 | Copyright © 2012-2020 Benjamin Beckwith 51 | 52 | Distributed under the Eclipse Public License, the same as Clojure. 53 | 54 | * Contributors 55 | 56 | - Benjamin Beckwith 57 | - David Pham 58 | - Kevin Pavao 59 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps 3 | {hiccup {:mvn/version "1.0.5"} 4 | org.apache.commons/commons-lang3 {:mvn/version "3.11"}} 5 | :aliases 6 | {:dev {:extra-paths ["test"] 7 | :extra-deps {midje {:mvn/version "1.9.9"}}}}} 8 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject bnbeckwith/orgmode "0.7.5" 2 | :description "Org-mode parser" 3 | :url "http://github.com/bnbeckwith/orgmode" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.10.1"] 7 | [hiccup "1.0.5"] 8 | [org.apache.commons/commons-lang3 "3.11"]] 9 | :profiles {:dev {:dependencies [[midje "1.9.9"]]}}) 10 | -------------------------------------------------------------------------------- /src/orgmode/block.clj: -------------------------------------------------------------------------------- 1 | ;; ## Block Element Formatting 2 | ;; 3 | ;; These functions parse an Org-mode file and generate the necessary 4 | ;; heirarchy of list and block elements. 5 | 6 | (ns orgmode.block 7 | (:require [clojure.string :as s] 8 | [clojure.zip :as zip]) 9 | (:use [orgmode.inline])) 10 | 11 | 12 | ;; ### Regular Expressions for Block Elements 13 | ;; 14 | ;; The following set of regular expressions match start and end 15 | ;; elements of blocks or block elements themselves. Note that some 16 | ;; items are line items, but I considered them blocks of the 17 | ;; smallest size. 18 | 19 | (def attrib-re 20 | "Attribute Regular Expression that captures the attribute name and 21 | any values (as a single string)" 22 | #"^#\+(\w+):\s*(.*)") 23 | 24 | (def prop-open-re 25 | "Regex for the beginning of a properties block" 26 | #"\s*:PROPERTIES:\s*") 27 | 28 | (def property-line-re 29 | "Regex that captures property keys and values from within a 30 | properties drawer" 31 | #"\s*:(\w+):\s*(.*)") 32 | 33 | (def block-open-re 34 | "Regex that matches the start of a begin_* block. It captures the 35 | type of block along with any parameters (as a single string)" 36 | #"^#\+(?i)BEGIN_(\w+)(?:\s+(.*))?") 37 | 38 | (def block-close-re 39 | "Regex that matches the end of a begin block" 40 | #"^#\+(?i)END_") 41 | 42 | (def comment-re 43 | "Regex that matches an inline comment" 44 | #"^\s*#\s+(.*)") 45 | 46 | (def headline-re 47 | "Regex that matches headlines and todo items. This regex does a 48 | bunch of work and captures the leading stars, TODO or DONE tags, 49 | headline text, and tags." 50 | ;; TODO -- capture priority here? 51 | #"^(\*+)\s+(?:(TODO|DONE)\s+)?(.*?)(?:\s+:(.*):)?") 52 | 53 | (def plain-list-re 54 | "Regex that matches a plain list. This regex captues leading spaces, 55 | the bullets or indices, and item text" 56 | #"^(\s*)([-+*]|[0-9]+[.)])\s(.*)") 57 | 58 | (def footnote-def-re 59 | "Regex denoting a footnote definition. Captures identifier and 60 | definition" 61 | #"^\s*\[(\d+|fn:(.*))\] (.*)") 62 | 63 | (def table-re 64 | "Regex to match table lines. Captures fields as one string" 65 | #"^\s*\|(.*)") 66 | 67 | (def table-formula-re 68 | "Regex to match table formula. Caputes the list of formulas as one 69 | string" 70 | #"\s*#\+TBLFM:\s*(.*)") 71 | 72 | ;; ### Processing Functions 73 | 74 | (defmacro handle-last-line 75 | "Macro to help break out of further processing if line is 76 | nil. Returns root of z" 77 | [[line z] & body] 78 | `(if-not (nil? ~line) 79 | (do ~@body) 80 | (zip/root ~z))) 81 | 82 | (declare next-line) 83 | 84 | (defn parse-attrib 85 | "At current location z, add attribute with name and values" 86 | [[& rest] z [_ name values]] 87 | (fn [] 88 | (next-line 89 | rest 90 | (zip/edit z 91 | #(assoc-in %1 [:attribs (keyword (.toLowerCase name))] values ))))) 92 | 93 | (defn parse-comment 94 | "At current location z, add comment with parsed text" 95 | [[& rest] z [_ comment]] 96 | (fn [] 97 | (next-line 98 | rest 99 | (zip/append-child 100 | z 101 | {:type :comment 102 | :content (orgmode.inline/parse-inline-elements comment)})))) 103 | 104 | 105 | (defn parse-footnote 106 | "Add current location z, add a footnote definition" 107 | [[& rest] z [_ nid fid text]] 108 | (fn [] 109 | (next-line 110 | rest 111 | (zip/append-child 112 | z 113 | {:type :footnote 114 | :id (or fid nid) 115 | :content (orgmode.inline/parse-inline-elements text)})))) 116 | 117 | 118 | (defn move-level 119 | "Given level, move up z until the location is an appropiate place to 120 | add elements at level." 121 | [z level] 122 | (let [cnode (zip/node z) 123 | clevel (get cnode :level level)] 124 | (if (> level clevel) 125 | z 126 | (recur (zip/up z) level)))) 127 | 128 | (defn parse-headline 129 | "Insert a headline at z and make it the new location." 130 | [[& rest] z [_ stars todo headline tag]] 131 | (let [lvl (count stars) 132 | zz (move-level z lvl)] 133 | #(next-line 134 | rest 135 | (-> zz 136 | (zip/append-child {:type :headline 137 | :text headline 138 | :todo todo 139 | :level lvl 140 | :content [] 141 | :tags (when tag 142 | (into #{} 143 | (s/split tag #":")))}) 144 | zip/down 145 | zip/rightmost)))) 146 | 147 | (defn parse-property 148 | "Parse a property block, adding property keys and values until an 149 | end tag is encountered" 150 | ([[line & rest] z] 151 | {:pre [(= :headline (-> z zip/node (:type)))]} 152 | (handle-last-line 153 | [line z] 154 | (if-let [[_ prop value] 155 | (re-matches property-line-re line)] 156 | (if (= prop "END") 157 | #(next-line rest z) 158 | (fn [] (parse-property 159 | rest 160 | (zip/edit 161 | z 162 | #(assoc-in % [:properties (keyword (.toLowerCase prop))] value))))) 163 | (throw (Exception. 164 | (str "No :END: for propery block (looking at \"" 165 | line "\"" )))))) 166 | ([[& rest] z _] #(parse-property rest z))) 167 | 168 | (defn parse-block 169 | "Parse a block structure, keeping attribues and adding lines until 170 | an end tag is encountered" 171 | ([[line & rest] z] 172 | (handle-last-line 173 | [line z] 174 | (let [type (name (:block (zip/node z))) 175 | end-re (re-pattern (str block-close-re type #"\s*"))] 176 | (if (re-matches end-re line) 177 | #(next-line rest (zip/up z)) 178 | #(parse-block rest (zip/append-child z line)))))) 179 | ([[& rest] z [_ type attribs]] 180 | #(parse-block 181 | rest 182 | (-> z 183 | (zip/append-child {:type :block 184 | :block (keyword (.toLowerCase type)) 185 | :content [] 186 | :attribs (when attribs 187 | (s/split attribs #"\s+"))}) 188 | zip/down 189 | zip/rightmost)))) 190 | 191 | 192 | (defn enclosing-plain-list 193 | "Move z up to appropiate level enclosing the current list" 194 | [z level] 195 | (if (or (not= :list (:type (zip/node z))) 196 | (= level (:listlevel (zip/node z)))) 197 | z 198 | (recur (zip/up z) level))) 199 | 200 | (defn parse-plain-list 201 | "Parse a plain list, keeping track of any nested heirarchy." 202 | ([[line & rest :as lines] z [_ lvl idx text :as params]] 203 | (let [level (+ (count lvl) (count idx)) 204 | listtype (if (re-find #"^[0-9]+" idx) :ol :ul)] 205 | (if (= :list (:type (zip/node z))) 206 | (if (= level (:listlevel (zip/node z))) 207 | (parse-plain-list 208 | lines 209 | (zip/append-child z 210 | {:type :listitem 211 | :content (orgmode.inline/parse-inline-elements text)})) 212 | (if (> level (:listlevel (zip/node z))) 213 | (parse-plain-list 214 | lines 215 | (-> z 216 | (zip/down) 217 | (zip/append-child {:type :list 218 | :listlevel level 219 | :listtype listtype 220 | :content [{:type :listitem 221 | :content (orgmode.inline/parse-inline-elements text)}]}) 222 | (zip/down) 223 | (zip/rightmost))) 224 | (parse-plain-list 225 | lines 226 | (enclosing-plain-list z level) 227 | params))) 228 | (parse-plain-list 229 | lines 230 | (-> z 231 | (zip/append-child {:type :list 232 | :listlevel level 233 | :listtype listtype 234 | :content [{:type :listitem 235 | :content (orgmode.inline/parse-inline-elements text)}]}) 236 | (zip/down) 237 | (zip/rightmost)))))) 238 | ([[line & rest :as lines] z] 239 | (handle-last-line 240 | [line z] 241 | (let [indent-re (re-pattern 242 | (format "(\\s{%d,})(.*)" 243 | (:listlevel (zip/node z))))] 244 | (condp re-matches line 245 | plain-list-re :>> (partial parse-plain-list rest z) 246 | indent-re :>> #(parse-plain-list 247 | rest 248 | (-> z 249 | (zip/down) 250 | (zip/append-child %1) 251 | (zip/up))) 252 | (next-line lines (zip/up z))))))) 253 | 254 | (defn append-table-line 255 | "Append table row s to location z. Splits s into fields" 256 | [z s] 257 | (let [row (if (re-matches #"[-+]+\|?" s) 258 | :tline 259 | (map s/trim (s/split s #"\|")))] 260 | (zip/edit z 261 | #(update-in % [:rows] conj row)))) 262 | 263 | (defn add-table-formula 264 | "Add table formula at z, splitting s into separate formulas." 265 | [z s] 266 | (zip/edit z 267 | #(update-in % [:formulas] (s/split s #"::")))) 268 | 269 | (defn parse-table 270 | "Parse a table to add at location z" 271 | [[& lines] z [_ tblline]] 272 | (let [fields (map s/trim (s/split tblline #"\|")) 273 | z' (-> z 274 | (zip/append-child {:type :table 275 | :content [] 276 | :rows [fields]}) 277 | (zip/down) 278 | (zip/rightmost))] 279 | (loop [ls lines 280 | z'' z'] 281 | (handle-last-line 282 | [(first ls) z''] 283 | (if-let [[_ l] (re-matches table-re (first ls))] 284 | (recur 285 | (rest ls) 286 | (append-table-line z'' l)) 287 | (if-let [[_ f] (re-matches table-formula-re 288 | (first ls))] 289 | (next-line 290 | (rest ls) 291 | (add-table-formula z'' f)) 292 | (next-line ls (zip/up z'')))))))) 293 | 294 | (defn add-line-element 295 | "Parse line for inline elements and add sequence at z." 296 | [lines z line] 297 | #(next-line 298 | lines 299 | (if (re-matches #"\s*" line) 300 | (-> z 301 | (zip/append-child {:type :p 302 | :content []}) 303 | (zip/down) 304 | (zip/rightmost)) 305 | 306 | (reduce 307 | zip/append-child 308 | z 309 | (orgmode.inline/parse-inline-elements line))))) 310 | 311 | (defn next-line [[line & rest] z] 312 | "Process each line with the list of block element parsers. Return 313 | the root of the created zipper structure" 314 | (handle-last-line 315 | [line z] 316 | (condp re-matches line 317 | block-open-re :>> (partial parse-block rest z) 318 | attrib-re :>> (partial parse-attrib rest z) 319 | headline-re :>> (partial parse-headline rest z) 320 | prop-open-re :>> (partial parse-property rest z) 321 | plain-list-re :>> (partial parse-plain-list rest z) 322 | table-re :>> (partial parse-table rest z) 323 | footnote-def-re :>> (partial parse-footnote rest z) 324 | comment-re :>> (partial parse-comment rest z) 325 | (add-line-element rest z line)))) 326 | 327 | (defn parse-lines 328 | "Parse coll as org-mode formatting" 329 | [coll] 330 | (let [root (zip/xml-zip {:level 0})] 331 | (trampoline next-line coll root))) 332 | -------------------------------------------------------------------------------- /src/orgmode/core.clj: -------------------------------------------------------------------------------- 1 | ;; ## Core functionality 2 | ;; 3 | ;; These are the main parsing functions 4 | 5 | (ns orgmode.core 6 | (:require [clojure.string :as s] 7 | [clojure.zip :as zip] 8 | [clojure.java.io :as io] 9 | [orgmode.block :as block] 10 | [orgmode.html :as html])) 11 | 12 | ;; ### Parsing functions 13 | ;; 14 | ;; These two are the workhorses that will parse an orgmode file, or 15 | ;; sequence of lines and produce a tree structures representing the 16 | ;; parsed elements. 17 | 18 | (defn parse-lines 19 | "Parse coll as a sequence of orgmode lines" 20 | [coll] 21 | (block/parse-lines coll)) 22 | 23 | (defn parse-str 24 | "Split a string and parse it" 25 | [x] 26 | (parse-lines (s/split-lines x))) 27 | 28 | (defn parse 29 | "Open x and parse lines" 30 | [x] 31 | (with-open [r (io/reader x)] 32 | (parse-lines (line-seq r)))) 33 | 34 | ;; ### Generated formatted text 35 | (defn convert 36 | "Convert the structure to html 37 | 38 | The user can supply a function for handling source blocks." 39 | ([r] 40 | (html/org-to-html r)) 41 | ([r f] 42 | (binding [html/*user-src-fn* f] 43 | (convert r)))) 44 | 45 | 46 | ;; ### Utilities 47 | ;; 48 | ;; The functions aid in the handling of the resulting tree from the 49 | ;; parsing functions. 50 | 51 | (defn zip 52 | "Returns a zipper from org elements (from orgmode/parse)" 53 | [root] 54 | (zip/xml-zip root)) 55 | 56 | (defn getall 57 | "Returns all nodes from root satisifying f" 58 | [root f] 59 | (loop [acc [], z root] 60 | (let [acc (if (f (zip/node z)) 61 | (conj acc (zip/node z)) 62 | acc)] 63 | (if (zip/end? z) 64 | acc 65 | (recur acc (zip/next z)))))) 66 | -------------------------------------------------------------------------------- /src/orgmode/html.clj: -------------------------------------------------------------------------------- 1 | ;; ## HTML output 2 | ;; 3 | ;; Functions to facilitate HTML output 4 | 5 | (ns orgmode.html 6 | (:require [hiccup.core :only [html]] 7 | [clojure.walk :as w] 8 | [clojure.string :as t]) 9 | (:import [org.apache.commons.lang3 StringEscapeUtils])) 10 | 11 | 12 | ;; ### Helping Items 13 | 14 | (def img-suffix-re 15 | #"(?i)(png|bmp|jpg|jpeg)$") 16 | 17 | (def ^:dynamic *user-src-fn* 18 | "User-defined function to use for SRC blocks of code. This 19 | function will be called with the SRC block map passed in." 20 | (fn [x] nil)) 21 | 22 | (defn squish-seq 23 | "For any consecutive sequences, merge them into one." 24 | [s] 25 | (letfn [(concat-seq ([] []) 26 | ([x y] 27 | (vec 28 | (if (seq? y) 29 | (concat x (squish-seq y)) 30 | (into x [y])))))] 31 | (reduce concat-seq [] s))) 32 | 33 | (defn maybe-img 34 | ([url] (maybe-img url url)) 35 | ([url alt] 36 | (if (re-find img-suffix-re url) 37 | [:figure [:img {:src url :alt alt}] (into [:figcaption] alt)] 38 | (into [:a {:href url}] alt)))) 39 | 40 | (defn elmtype [x] 41 | (if (map? x) 42 | (if-let [t (:type x)] 43 | t 44 | :root) 45 | (if (sequential? x) 46 | :seq 47 | :default))) 48 | 49 | (defmulti hiccupify elmtype) 50 | (defmulti blockprocess :block) 51 | 52 | (defn hiccup [r] 53 | (squish-seq (hiccupify r))) 54 | 55 | (defn org-to-html [r] 56 | (hiccup.core/html (hiccupify r))) 57 | 58 | (defmethod blockprocess :src [x] 59 | (if-let [c (and *user-src-fn* (*user-src-fn* x))] 60 | c 61 | [:pre 62 | [:code 63 | (StringEscapeUtils/escapeHtml4 (t/join "\n" (:content x)))]])) 64 | 65 | (defmethod blockprocess :default [x] 66 | (into [(:block x)] (:content x))) 67 | 68 | (defmethod hiccupify :headline [x] 69 | (if (and (:tags x) ((:tags x) "noexport")) 70 | "" 71 | [:section {:class (str "hsec" (inc (:level x)) (:tags x))} 72 | [(keyword (str "h" (inc (:level x)))) 73 | (:text x)] 74 | (hiccupify (:content x))])) 75 | 76 | (defmethod hiccupify :root [x] 77 | (into [:section {:id "root" } ] (hiccupify (:content x)))) 78 | 79 | (defmethod hiccupify :list [x] 80 | (into [(:listtype x)] (hiccupify (:content x)))) 81 | 82 | (defmethod hiccupify :listitem [x] 83 | (into [:li] (map hiccupify (:content x)))) 84 | 85 | (defmethod hiccupify :table [x] 86 | (let [rows (:rows x) 87 | hdr? (= (second rows) :tline) 88 | tbl [:table 89 | (when hdr? 90 | (into [:tr] (map (fn [x] [:th (hiccupify x)]) (first rows))))] 91 | rows (if hdr? (next (filter #(not= :tline %) rows)) rows)] 92 | (into tbl 93 | (for [row rows] 94 | (into [:tr] 95 | (map (fn [x] [:td (hiccupify x)]) row)))))) 96 | 97 | (defmethod hiccupify :block [x] 98 | (blockprocess x)) 99 | 100 | (defmethod hiccupify :link [x] 101 | (let [{:keys [uri content]} x] 102 | (maybe-img uri content))) 103 | 104 | ;; TODO -- consider using :cite? 105 | (defmethod hiccupify :footnote-ref [x] 106 | [:a.footnote {:href (str "#" (:id x))} 107 | (:id x)]) 108 | 109 | (defmethod hiccupify :footnote [x] 110 | (into [:a.footnote-def 111 | {:id (:id x) 112 | :href (str "#" (:id x))}] 113 | (hiccupify (:content x)))) 114 | 115 | ;; TODO -- figure out timestamps 116 | 117 | (defmethod hiccupify :bold [x] 118 | (into [:strong] (hiccupify (:content x)))) 119 | 120 | (defmethod hiccupify :italic [x] 121 | (into [:em] (hiccupify (:content x)))) 122 | 123 | (defmethod hiccupify :underline [x] 124 | (into [:u] (hiccupify (:content x)))) 125 | 126 | (defmethod hiccupify :code [x] 127 | (into [:code] (hiccupify (:content x)))) 128 | 129 | (defmethod hiccupify :comment [x] 130 | (list "" 133 | (when-let [tgts (filter #(= :target (:type %)) (:content x))] 134 | (map hiccupify tgts)))) 135 | 136 | (defmethod hiccupify :verbatim [x] 137 | (into [:verbatim] (:content x))) 138 | 139 | (defmethod hiccupify :target [x] 140 | [:a {:id (:id x) :href (str "#" (:id x))}]) 141 | 142 | (defmethod hiccupify :p [x] 143 | [:p (hiccupify (:content x))]) 144 | 145 | (defmethod hiccupify :seq [x] 146 | (map hiccupify x)) 147 | 148 | (defmethod hiccupify :default [x] 149 | (str x "\n")) 150 | -------------------------------------------------------------------------------- /src/orgmode/inline.clj: -------------------------------------------------------------------------------- 1 | ;; ## Inline Element Formatting 2 | ;; 3 | ;; Aside from the block elements handled in the core, there are 4 | ;; particular elements withing text that have meaning. There are 5 | ;; roughly two types of elements: links and formatting. 6 | 7 | (ns orgmode.inline 8 | (:require [clojure.string :as s])) 9 | 10 | ;; ### Link Elements 11 | 12 | (def link-re #"\[\[([^\]]+)(?:\]\[([^\]]+))?\]\]") 13 | 14 | (defn link-create 15 | "Create hyperlink structures from a list of regex matches" 16 | [coll] 17 | (for [[_ link text ] coll] 18 | {:type :link 19 | :uri link 20 | :content [(or text link)]})) 21 | 22 | (def footnote-re #"\[(\d+|fn:(.*))\]") 23 | 24 | (defn footnote-create 25 | "Creates footnote structures from a list of regex matches" 26 | [coll] 27 | (for [[_ ref name] coll] 28 | {:type :footnote-ref 29 | :id (if (nil? name) 30 | ref name)})) 31 | 32 | (def target-re #"<<\s*([^>]*?)\s*>>") 33 | 34 | (defn target-create 35 | "Creates target structures from a list of regex matches" 36 | [tgts] 37 | (for [[_ lbl] tgts] 38 | {:type :target 39 | :id lbl})) 40 | 41 | ;; ### Timestamp Elements 42 | 43 | ;; hack to make it work with ranges 44 | (def ts-base 45 | #"(\d{4})-(\d{2})-(\d{2}) (\w{3})(?: (\d{1,2}):(\d{2})(-(\d{1,2}):(\d{2})|.*)?)?") 46 | 47 | (def ts-active-re 48 | (re-pattern (str "<" ts-base ">"))) 49 | 50 | (defn ts-active-create 51 | "Creates active timestamps from a list of regex matches" 52 | [coll] 53 | (for [[_ Y M D d h m _ Eh Em] coll] 54 | (cond-> {:type :timestamp 55 | :timetype :active 56 | :year Y 57 | :month M 58 | :day D 59 | :dayname d 60 | :hour h 61 | :minute m} 62 | (and Eh Em) (merge {:end-hour Eh :end-minute Em}) 63 | :always or))) 64 | 65 | (def ts-inactive-re 66 | (re-pattern (str "\\[" ts-base "\\]"))) 67 | 68 | (defn ts-inactive-create 69 | "Create inactive timestamps from a list of regex matches" 70 | [coll] 71 | (map #(assoc % :timetype :inactive) 72 | (ts-active-create coll))) 73 | 74 | (def ts-range-re 75 | (re-pattern (str ts-active-re "--" ts-active-re))) 76 | 77 | (defn ts-range-create 78 | "Create timestamp ranges from a list of regex matches" 79 | [coll] 80 | (for [[_ BY BM BD Bd Bh Bm _ _ 81 | _ EY EM ED Ed Eh Em _ _] coll] 82 | {:type :timestamp 83 | :timetype :range 84 | :year BY 85 | :month BM 86 | :day BD 87 | :dayname Bd 88 | :hour Bh 89 | :minute Bm 90 | :end-year EY 91 | :end-month EM 92 | :end-day ED 93 | :end-dayname Ed 94 | :end-hour Eh 95 | :end-minute Em})) 96 | 97 | ;; ### Formatting Elements 98 | 99 | (defn fmt-create 100 | "Generic function to create a format element from given type" 101 | [type] 102 | (fn [ts] 103 | (for [[_ t] ts] 104 | {:type type 105 | :content [t]}))) 106 | 107 | (defn fmt-re 108 | "Create a re-pattern to match the given delimiter s" 109 | [s] 110 | (re-pattern (str s #"(\S(?:.*?\S)??)" s))) 111 | 112 | ; ### Inline Processing 113 | 114 | (defn re-interleave 115 | "Split l with on re, interleave this list with the inline elements 116 | constructed from ms using cfn" 117 | [re l cfn ms] 118 | (let [ls (s/split l re) 119 | els (vec (map #(assoc % :inline true) (cfn ms)))] 120 | (if (empty? ls) 121 | els 122 | (vec 123 | (interleave ls (conj els {:inline true :type :comment :text "FIX INTERLEAVING"})))))) 124 | 125 | (defn make-elem 126 | "Try to make any inline elements out of strings in coll using re and 127 | cstor to match and construct these elements" 128 | [coll re cstor] 129 | (flatten 130 | (for [elem coll] 131 | (if (string? elem) 132 | (if-let [ms (re-seq re elem)] 133 | (re-interleave re elem cstor ms) 134 | elem) 135 | elem)))) 136 | 137 | (defn parse-inline-elements 138 | "Takes line and breaks it into inline elements and interleaving 139 | text" 140 | [line] 141 | (-> [line] 142 | (make-elem link-re link-create) 143 | (make-elem footnote-re footnote-create) 144 | (make-elem target-re target-create) 145 | (make-elem ts-range-re ts-range-create) 146 | (make-elem ts-active-re ts-active-create) 147 | (make-elem ts-inactive-re ts-inactive-create) 148 | (make-elem (fmt-re "\\*") (fmt-create :bold)) 149 | (make-elem (fmt-re "/") (fmt-create :italic)) 150 | (make-elem (fmt-re "\\+") (fmt-create :strike-through)) 151 | (make-elem (fmt-re "_") (fmt-create :underline)) 152 | (make-elem (fmt-re "=") (fmt-create :verbatim)) 153 | (make-elem (fmt-re "~") (fmt-create :code)))) 154 | -------------------------------------------------------------------------------- /src/orgmode/util.clj: -------------------------------------------------------------------------------- 1 | (ns orgmode.util 2 | (:require [clojure.zip :as zip])) 3 | 4 | (defn fix-link [n] 5 | (merge n 6 | (when-let [ms (re-matches #"file:(.*)\.org" (:uri n))] 7 | {:uri (str (second ms) ".html")}))) 8 | 9 | (defn org-to-html-links [z] 10 | (if (zip/end? z) 11 | (zip/root z) 12 | (recur (zip/next 13 | (let [n (zip/node z)] 14 | (if (and (map? n) 15 | (= :link (:type n))) 16 | (zip/edit z fix-link) 17 | z)))))) 18 | -------------------------------------------------------------------------------- /test/orgmode/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns orgmode.core-test 2 | (:require [orgmode.core :as orgmode] 3 | [midje.sweet :refer :all])) 4 | 5 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 6 | ;; Testing inlines 7 | 8 | (def x (atom nil)) 9 | 10 | (defmacro test-inline-fmt "Perform a test on inline elements" 11 | [string content type] 12 | `(with-state-changes [(before :facts 13 | (reset! x (orgmode/parse-str ~string)))] 14 | (fact (get-in @x [:content 0 :content 0]) => ~content) 15 | (fact (get-in @x [:content 0 :type]) => ~type) 16 | (fact (get-in @x [:content 0 :inline]) => true))) 17 | 18 | 19 | (defmacro test-inline "Check inline elment parsing" 20 | [string & path-result-pairs] 21 | (when path-result-pairs 22 | (let [fs (for [[p r] (partition 2 path-result-pairs)] 23 | `(fact (get-in @x ~p) => ~r))] 24 | `(fact ~string 25 | (with-state-changes [(before :facts 26 | (reset! x (orgmode/parse-str ~string)))] 27 | ~@fs))))) 28 | 29 | ;; Formatting 30 | (fact "Formatting" 31 | (test-inline-fmt "*Bold text*" "Bold text" :bold) 32 | (test-inline-fmt "/Italic text/" "Italic text" :italic) 33 | (test-inline-fmt "+Striken text+" "Striken text" :strike-through) 34 | (test-inline-fmt "_Underline text_" "Underline text" :underline) 35 | (test-inline-fmt "=Verbatim text=" "Verbatim text" :verbatim) 36 | (test-inline-fmt "~Code text~" "Code text" :code)) 37 | 38 | ;; Footnotes 39 | (fact "Footnotes" 40 | (test-inline "Footnote [1]" 41 | [:content 1 :type ] :footnote-ref 42 | [:content 1 :id ] "1" 43 | [:content 1 :inline ] true) 44 | 45 | (test-inline "Foornote [fn:blue]" 46 | [:content 1 :type ] :footnote-ref 47 | [:content 1 :id ] "blue" 48 | [:content 1 :inline ] true)) 49 | 50 | ;; Links 51 | (fact "Links" 52 | (test-inline "[[http://bnbeckwith.com]]" 53 | [:content 0 :uri ] "http://bnbeckwith.com" 54 | [:content 0 :type ] :link 55 | [:content 0 :inline ] true 56 | [:content 0 :content 0 ] "http://bnbeckwith.com") 57 | 58 | (test-inline "[[http://bnbeckwith.com][website]]" 59 | [:content 0 :uri ] "http://bnbeckwith.com" 60 | [:content 0 :type ] :link 61 | [:content 0 :inline ] true 62 | [:content 0 :content 0 ] "website")) 63 | 64 | ;; Target 65 | (fact "Targets" 66 | (test-inline "<>" 67 | [:content 0 :id ] "target" 68 | [:content 0 :inline ] true 69 | [:content 0 :type ] :target) 70 | 71 | (test-inline "<>" 72 | [:content 0 :id ] "target two" 73 | [:content 0 :inline ] true 74 | [:content 0 :type ] :target)) 75 | 76 | ;; Active Timestamp 77 | (fact "Active Timestamps" 78 | (test-inline "<1900-01-01 Mon>" 79 | [:content 0 :inline ] true 80 | [:content 0 :type ] :timestamp 81 | [:content 0 :timetype ] :active 82 | [:content 0 :year ] "1900" 83 | [:content 0 :month ] "01" 84 | [:content 0 :day ] "01" 85 | [:content 0 :dayname ] "Mon" 86 | [:content 0 :hour ] nil 87 | [:content 0 :minute ] nil) 88 | 89 | (test-inline "<1900-01-01 Mon 12:12>" 90 | [:content 0 :inline ] true 91 | [:content 0 :type ] :timestamp 92 | [:content 0 :timetype ] :active 93 | [:content 0 :year ] "1900" 94 | [:content 0 :month ] "01" 95 | [:content 0 :day ] "01" 96 | [:content 0 :dayname ] "Mon" 97 | [:content 0 :hour ] "12" 98 | [:content 0 :minute ] "12") 99 | 100 | (test-inline "<1900-01-01 Mon 12:12-1:30>" 101 | [:content 0 :inline ] true 102 | [:content 0 :type ] :timestamp 103 | [:content 0 :timetype ] :active 104 | [:content 0 :year ] "1900" 105 | [:content 0 :month ] "01" 106 | [:content 0 :day ] "01" 107 | [:content 0 :dayname ] "Mon" 108 | [:content 0 :hour ] "12" 109 | [:content 0 :minute ] "12" 110 | [:content 0 :end-hour ] "1" 111 | [:content 0 :end-minute ] "30")) 112 | 113 | ;; Inactive Timestamp 114 | (fact "Inactive Timestamps" 115 | (test-inline "[1900-01-01 Mon]" 116 | [:content 0 :inline ] true 117 | [:content 0 :type ] :timestamp 118 | [:content 0 :timetype ] :inactive 119 | [:content 0 :year ] "1900" 120 | [:content 0 :month ] "01" 121 | [:content 0 :day ] "01" 122 | [:content 0 :dayname ] "Mon" 123 | [:content 0 :hour ] nil 124 | [:content 0 :minute ] nil) 125 | 126 | (test-inline "[1900-01-01 Mon 12:12]" 127 | [:content 0 :inline ] true 128 | [:content 0 :type ] :timestamp 129 | [:content 0 :timetype ] :inactive 130 | [:content 0 :year ] "1900" 131 | [:content 0 :month ] "01" 132 | [:content 0 :day ] "01" 133 | [:content 0 :dayname ] "Mon" 134 | [:content 0 :hour ] "12" 135 | [:content 0 :minute ] "12") 136 | 137 | (test-inline "[1900-01-01 Mon 12:12-1:30]" 138 | [:content 0 :inline ] true 139 | [:content 0 :type ] :timestamp 140 | [:content 0 :timetype ] :inactive 141 | [:content 0 :year ] "1900" 142 | [:content 0 :month ] "01" 143 | [:content 0 :day ] "01" 144 | [:content 0 :dayname ] "Mon" 145 | [:content 0 :hour ] "12" 146 | [:content 0 :minute ] "12" 147 | [:content 0 :end-hour ] "1" 148 | [:content 0 :end-minute ] "30")) 149 | 150 | ;; Range 151 | (fact "Timestamp Ranges" 152 | (test-inline "<1900-01-01 Mon>--<1901-01-01 Tue>" 153 | [:content 0 :inline ] true 154 | [:content 0 :type ] :timestamp 155 | [:content 0 :timetype ] :range 156 | [:content 0 :year ] "1900" 157 | [:content 0 :month ] "01" 158 | [:content 0 :day ] "01" 159 | [:content 0 :dayname ] "Mon" 160 | [:content 0 :hour ] nil 161 | [:content 0 :minute ] nil 162 | [:content 0 :end-year ] "1901" 163 | [:content 0 :end-month ] "01" 164 | [:content 0 :end-day ] "01" 165 | [:content 0 :end-dayname ] "Tue" 166 | [:content 0 :end-hour ] nil 167 | [:content 0 :end-minute ] nil) 168 | 169 | 170 | (test-inline "<1900-01-01 Mon 2:55>--<1901-01-01 Tue 6:40>" 171 | [:content 0 :inline ] true 172 | [:content 0 :type ] :timestamp 173 | [:content 0 :timetype ] :range 174 | [:content 0 :year ] "1900" 175 | [:content 0 :month ] "01" 176 | [:content 0 :day ] "01" 177 | [:content 0 :dayname ] "Mon" 178 | [:content 0 :hour ] "2" 179 | [:content 0 :minute ] "55" 180 | [:content 0 :end-year ] "1901" 181 | [:content 0 :end-month ] "01" 182 | [:content 0 :end-day ] "01" 183 | [:content 0 :end-dayname ] "Tue" 184 | [:content 0 :end-hour ] "6" 185 | [:content 0 :end-minute ] "40")) 186 | -------------------------------------------------------------------------------- /test/orgmode/inline_test.clj: -------------------------------------------------------------------------------- 1 | (ns orgmode.inline-test 2 | (:require [orgmode.inline :as sut] 3 | [midje.sweet :as midje])) 4 | 5 | ;; Active Timestamp 6 | (midje/fact 7 | "Active TimeStamp Regex Test" 8 | 9 | (->> "1900-01-01 Mon 12:12" (re-seq sut/ts-base) first) => 10 | ["1900-01-01 Mon 12:12" "1900" "01" "01" "Mon" "12" "12" "" nil nil] 11 | 12 | (->> "1900-01-01 Mon 12:00-13:15" (re-seq sut/ts-base) first ) => 13 | ["1900-01-01 Mon 12:00-13:15" "1900" "01" "01" "Mon" "12" "00" "-13:15" "13" "15"] 14 | 15 | (->> "1900-01-01 Mon 12:00" (re-seq sut/ts-base) sut/ts-active-create first) => 16 | {:day "01", 17 | :dayname "Mon", 18 | :hour "12", 19 | :minute "00", 20 | :month "01", 21 | :timetype :active, 22 | :type :timestamp, 23 | :year "1900"} 24 | 25 | (->> "1900-01-01 Mon 12:00-13:15" (re-seq sut/ts-base) sut/ts-active-create first) => 26 | {:day "01", 27 | :dayname "Mon", 28 | :end-hour "13", 29 | :end-minute "15", 30 | :hour "12", 31 | :minute "00", 32 | :month "01", 33 | :timetype :active, 34 | :type :timestamp, 35 | :year "1900"}) 36 | -------------------------------------------------------------------------------- /test/orgmode/test.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Test.org 2 | #+author: tester 3 | 4 | * Headings 5 | ** Level 2 6 | *** Level 3 7 | **** Level 4 8 | ***** Level 5 9 | ****** Level 6 10 | * Lists 11 | 12 | - One 13 | - Two 14 | - Three 15 | 16 | 17 | - [ ] First 18 | - [ ] Second 19 | - [ ] Third 20 | 21 | 22 | * Tables 23 | 24 | | Col 1 | Col 2 | 25 | |-------+-------| 26 | | a | 3 | 27 | | b | 4 | 28 | 29 | * Block/Line elements 30 | 31 | 32 | ** Line Elements 33 | # Comment 34 | 35 | : verbatim text 36 | 37 | ** Block Elements 38 | 39 | #+BEGIN_SRC perl 40 | my $foo; 41 | #+END_SRC 42 | 43 | * Links 44 | 45 | [[http://www.google.com]] 46 | 47 | [[http://www.google.com][google]] 48 | 49 | [[file:test.org]] 50 | 51 | [[file:test.org][test.org]] 52 | 53 | ** Targets 54 | # <<< target >>> 55 | 56 | * Inline Elements 57 | 58 | There are a few inline elements to test such as *bold*, =example=, 59 | /italic/ or even _underline_. This block of text itself should be 60 | considered a paragraph [fn:: It is possible to have footnotes]. 61 | --------------------------------------------------------------------------------