├── .travis.yml ├── .gitignore ├── project.clj ├── src └── mapdown │ └── core.clj ├── test └── mapdown │ └── core_test.clj └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | lein: lein2 3 | script: lein2 midje 4 | jdk: 5 | - openjdk6 6 | - openjdk7 7 | - oraclejdk7 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject mapdown "0.2.1" 2 | :description "A lightweight markup format to turn strings into maps." 3 | :url "http://github.com/magnars/mapdown" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [stasis "1.0.0"]] 8 | :profiles {:dev {:dependencies [[midje "1.6.0"] 9 | [test-with-files "0.1.0"]] 10 | :plugins [[lein-midje "3.1.3"]]}}) 11 | -------------------------------------------------------------------------------- /src/mapdown/core.clj: -------------------------------------------------------------------------------- 1 | (ns mapdown.core 2 | (:require [clojure.string :as str] 3 | [stasis.core :as stasis])) 4 | 5 | (def ^:private keyword-re #"(?m)^:[^ \n\t\r]+") 6 | 7 | (def ^:private eighty-dashes 8 | "--------------------------------------------------------------------------------") 9 | 10 | (def ^:private eighty-dashes-re (re-pattern eighty-dashes)) 11 | 12 | (defn- parse-1 [^String s] 13 | (when-not (re-find #"^[ \n\t\r]*:" s) 14 | (throw (Exception. "Mapdown content must start with a key - or the content has nowhere to go."))) 15 | (zipmap 16 | (map #(keyword (subs % 1)) (re-seq keyword-re s)) 17 | (map str/trim (drop 1 (str/split s keyword-re))))) 18 | 19 | (defn- ordinal [num] 20 | (if (contains? (set (range 11 14)) (mod num 100)) 21 | (str num "th") 22 | (let [modulus (mod num 10)] 23 | (cond 24 | (= modulus 1) (str num "st") 25 | (= modulus 2) (str num "nd") 26 | (= modulus 3) (str num "rd") 27 | :else (str num "th"))))) 28 | 29 | (defn- parse-with-index [idx s] 30 | (try 31 | (parse-1 s) 32 | (catch Exception e 33 | (throw (Exception. (str "Error in " (ordinal (inc idx)) " entry: " (.getMessage e))))))) 34 | 35 | (defn- parse-list [^String s] 36 | (->> (str/split s eighty-dashes-re) 37 | (map str/trim) 38 | (remove empty?) 39 | (map-indexed parse-with-index))) 40 | 41 | (defn parse [^String s] 42 | (let [s (str/trim s)] 43 | (if (.startsWith s eighty-dashes) 44 | (parse-list s) 45 | (parse-1 s)))) 46 | 47 | (defn- parse-with-known-path [path contents] 48 | (try 49 | (parse contents) 50 | (catch Exception e 51 | (throw (Exception. (str "Error when parsing '" path "': " (.getMessage e))))))) 52 | 53 | (defn parse-file [path] 54 | (parse-with-known-path path (slurp path))) 55 | 56 | (defn slurp-directory [dir regexp] 57 | (let [files (stasis/slurp-directory dir regexp)] 58 | (->> files 59 | (map (fn [[path contents]] 60 | [path (parse-with-known-path (str dir path) contents)])) 61 | (into {})))) 62 | -------------------------------------------------------------------------------- /test/mapdown/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns mapdown.core-test 2 | (:require [mapdown.core :refer :all] 3 | [midje.sweet :refer :all] 4 | [test-with-files.core :refer [with-files tmp-dir]])) 5 | 6 | (fact 7 | (parse ":title abc") => {:title "abc"} 8 | (parse " 9 | :title abc 10 | :desc def") => {:title "abc", :desc "def"}) 11 | 12 | (fact 13 | (parse " 14 | :section1 15 | abc 16 | :section2 17 | def 18 | ghi") => {:section1 "abc", :section2 "def\nghi"}) 19 | 20 | (fact 21 | "Empty sections are excluded." 22 | 23 | (parse " 24 | :title abc 25 | 26 | :section1 27 | 28 | def 29 | 30 | :illustration ghi 31 | 32 | :section2") => {:title "abc", :section1 "def", :illustration "ghi"}) 33 | 34 | (fact 35 | "80 dashes means we're creating a list of maps." 36 | 37 | (parse " 38 | -------------------------------------------------------------------------------- 39 | :title First item 40 | :body 41 | 42 | Some text 43 | -------------------------------------------------------------------------------- 44 | :title Second item 45 | :body 46 | 47 | More text 48 | -------------------------------------------------------------------------------- 49 | ") => [{:title "First item", :body "Some text"} 50 | {:title "Second item", :body "More text"}]) 51 | 52 | (fact 53 | "To avoid nasty surprises, we don't tolerate text prior the first 54 | key. It would have nowhere to go, becoming quite literally trailing 55 | trash." 56 | 57 | (parse "trash 58 | :title abc") => (throws Exception "Mapdown content must start with a key - or the content has nowhere to go.")) 59 | 60 | (fact 61 | "To help find errors, report the list location." 62 | 63 | (parse " 64 | -------------------------------------------------------------------------------- 65 | :title abc 66 | -------------------------------------------------------------------------------- 67 | trash 68 | :title def 69 | -------------------------------------------------------------------------------- 70 | ") => (throws Exception "Error in 2nd entry: Mapdown content must start with a key - or the content has nowhere to go.")) 71 | 72 | (fact 73 | "You can parse files, just to get proper error messages." 74 | 75 | (with-files [["/file.md" ":title abc"]] 76 | (parse-file (str tmp-dir "/file.md")) => {:title "abc"}) 77 | 78 | (with-files [["/file.md" "bleh"]] 79 | (parse-file (str tmp-dir "/file.md")) 80 | => (throws Exception (str "Error when parsing '" tmp-dir "/file.md': Mapdown content must start with a key - or the content has nowhere to go.")))) 81 | 82 | (fact 83 | "You can slurp entire directories in, finding files based on a regexp." 84 | 85 | (with-files [["/file.md" ":title abc"] 86 | ["/more/stuff.md" ":title def"]] 87 | (slurp-directory tmp-dir #"\.md$") 88 | => {"/file.md" {:title "abc"} 89 | "/more/stuff.md" {:title "def"}}) 90 | 91 | (with-files [["/file.md" "bleh"]] 92 | (slurp-directory tmp-dir #"\.md$") 93 | => (throws Exception (str "Error when parsing '" tmp-dir "/file.md': Mapdown content must start with a key - or the content has nowhere to go.")))) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapdown [![Build Status](https://secure.travis-ci.org/magnars/mapdown.png)](http://travis-ci.org/magnars/mapdown) 2 | 3 | A lightweight markup format to turn strings into maps. 4 | 5 | Why? Well, sometimes you want metadata with your markup format of 6 | choice. Let's say you're writing some markdown articles, and want to 7 | add information about the author and date. 8 | 9 | - Do you write .edn files, putting your markdown inside multiline 10 | strings with the escape nightmares that entails? 11 | 12 | - Or do you put the metadata in another file to write markdown in its 13 | natural environment? 14 | 15 | With Mapdown you add the metadata alongside your content, while 16 | avoiding string escaping and regaining editor support. 17 | 18 | ## Install 19 | 20 | Add `[mapdown "0.2.1"]` to `:dependencies` in your `project.clj`. 21 | 22 | ## Usage 23 | 24 | Given this file, say in `intro.md`: 25 | 26 | ```text 27 | :title Mapdown example 28 | :author Magnar Sveen 29 | :body 30 | 31 | Here's an example of how mapdown works. 32 | 33 | It's, like, text with keywords. 34 | 35 | :aside 36 | 37 | There's not much to it, really. 38 | ``` 39 | 40 | Turn it into a map like this: 41 | 42 | ```clj 43 | (ns example.core 44 | (:require [mapdown.core :as mapdown])) 45 | 46 | (mapdown/parse (slurp "intro.md")) 47 | 48 | ;; => 49 | {:title "Mapdown example" 50 | :author "Magnar Sveen" 51 | :body "Here's an example of how mapdown works.\n\nIt's, like, text with keywords." 52 | :aside "There's not much to it, really."} 53 | ``` 54 | 55 | ### A list of maps 56 | 57 | If the text starts with exactly 80 dashes, mapdown will interpret it as a list 58 | of maps. Like so: 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- 62 | :title First item 63 | :body 64 | 65 | Some text 66 | -------------------------------------------------------------------------------- 67 | :title Second item 68 | :body 69 | 70 | More text 71 | -------------------------------------------------------------------------------- 72 | ``` 73 | 74 | turns into: 75 | 76 | ```clj 77 | [{:title "First item", :body "Some text"} 78 | {:title "Second item", :body "More text"}] 79 | ``` 80 | 81 | ### Supplementary features 82 | 83 | You can also parse the contents of a file with `(parse-file path)`. 84 | The only reason to use this over just slurping it yourself, is to get 85 | error messages that include the file path. 86 | 87 | There's also `(slurp-directory path regexp)`, which slurps in an 88 | entire directory tree of files matching the regexp, parsing 89 | everything. 90 | 91 | ```clj 92 | (def articles (slurp-directory "resources/articles/" #"\.md$")) 93 | ``` 94 | 95 | This works just like the same function in 96 | [Stasis](https://github.com/magnars/stasis), except it also parses the 97 | files. Again, this is to get better error messages. 98 | 99 | ## Contribute 100 | 101 | Yes, please do. And add tests for your feature or fix, or I'll 102 | certainly break it later. 103 | 104 | #### Running the tests 105 | 106 | `lein midje` will run all tests. 107 | 108 | `lein midje namespace.*` will run only tests beginning with "namespace.". 109 | 110 | `lein midje :autotest` will run all the tests indefinitely. It sets up a 111 | watcher on the code files. If they change, only the relevant tests will be 112 | run again. 113 | 114 | ## License 115 | 116 | Copyright © 2013-2014 Magnar Sveen 117 | 118 | Distributed under the Eclipse Public License, the same as Clojure. 119 | --------------------------------------------------------------------------------