├── .gitignore ├── README.md ├── project.clj ├── src └── doric │ ├── core.clj │ ├── csv.clj │ ├── hssf.clj │ ├── html.clj │ ├── org.clj │ └── raw.clj └── test └── doric └── test ├── core.clj ├── doctest.clj └── readme.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Doric 2 | 3 | Doric is a library for rendering pretty emacs-style tables from 4 | Clojure data. Data is passed into Doric as nested collections, and 5 | comes out as a string suitable for printing. 6 | 7 | Add this to your project.clj :dependencies list: 8 | 9 | ```clojure 10 | [doric "0.9.0"] 11 | ``` 12 | 13 | ## Usage 14 | 15 | In most cases, you'll just want to use/require the table function. 16 | 17 | ```clojure 18 | > (use '[doric.core :only [table]]) 19 | nil 20 | ``` 21 | 22 | But you can access other things if you'd like, for instance if you 23 | want to use the other formats. 24 | 25 | ```clojure 26 | > (use '[doric.core :only [table csv html org raw]]) 27 | nil 28 | ``` 29 | 30 | Rows are maps, columns are entries in the maps. Column titles are 31 | driven from the keys, by default, :like-this becomes Like This. Since 32 | we can't know the original order of the keys in your maps, you'll 33 | probably want to provide a columns argument that specifies the order 34 | of your keys (more on this later). 35 | 36 | ```clojure 37 | > (println (table [:a :b] [{:a 1 :b 2}])) 38 | |---+---| 39 | | A | B | 40 | |---+---| 41 | | 1 | 2 | 42 | |---+---| 43 | nil 44 | ``` 45 | 46 | The default formatting is emacs org-mode tables, which are awesome. 47 | 48 | ```clojure 49 | > (println (table [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}])) 50 | |---+---+---| 51 | | A | B | C | 52 | |---+---+---| 53 | | 1 | 2 | 3 | 54 | | 4 | 5 | 6 | 55 | |---+---+---| 56 | nil 57 | ``` 58 | 59 | But you can also have raw, csv, and html tables pretty easily: 60 | 61 | ```clojure 62 | > (println (table {:format raw} [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}])) 63 | A B C 64 | 1 2 3 65 | 4 5 6 66 | nil 67 | 68 | > (println (table {:format csv} [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}])) 69 | A,B,C 70 | 1,2,3 71 | 4,5,6 72 | nil 73 | 74 | > (println (table {:format html} [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}])) 75 | ;; omg lots of s and s here 76 | ``` 77 | 78 | You can also use a custom table format by specifying a namespace that 79 | contains the functions th, td, and render. 80 | 81 | ```clojure 82 | > (println (table {:format 'my.sweet.ns} [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}])) 83 | ;; the sky's the limit, buddy 84 | ``` 85 | 86 | Individual columns are optional, each column automatically sizes 87 | itself to hold the data. 88 | 89 | ```clojure 90 | > (println (table [:lang :strength :safety] 91 | [{:lang "Clojure" :strength "strong" :safety "safe"} 92 | {:lang "Java" :strength "strong" :safety "safe"} 93 | {:lang "JavaScript" :strength "weak"}])) 94 | |------------+----------+--------| 95 | | Lang | Strength | Safety | 96 | |------------+----------+--------| 97 | | Clojure | strong | safe | 98 | | Java | strong | safe | 99 | | JavaScript | weak | | 100 | |------------+----------+--------| 101 | nil 102 | ``` 103 | 104 | Or, you can substitute (per column) a map for a keyword, and change 105 | lots of things about the way the data is displayed. 106 | 107 | ```clojure 108 | > (println (table [{:name :lang :title "Language" :align :center :width 12} 109 | {:name :safety :width 12 :align :left} 110 | {:name :strength :width 12 :align :left}] 111 | [{:lang "Clojure" :strength "strong" :safety "safe"} 112 | {:lang "Java" :strength "strong" :safety "safe"} 113 | {:lang "JavaScript" :strength "weak"}])) 114 | |--------------+--------------+--------------| 115 | | Language | Safety | Strength | 116 | |--------------+--------------+--------------| 117 | | Clojure | safe | strong | 118 | | Java | safe | strong | 119 | | JavaScript | | weak | 120 | |--------------+--------------+--------------| 121 | nil 122 | ``` 123 | 124 | Which probably seems like a lot of syntax, but consider that in actual 125 | use it would probably look more like this, which isn't nearly as bad: 126 | 127 | ```clojure 128 | > (println (table [{:name :lang :title "Language" :align :center :width 12} 129 | {:name :safety :width 12 :align :left} 130 | {:name :strength :width 12 :align :left}] 131 | (select-languages-from-db))) 132 | ;; assuming select-languages-from-db is some useful function 133 | |--------------+--------------+--------------| 134 | | Language | Safety | Strength | 135 | |--------------+--------------+--------------| 136 | | Clojure | safe | strong | 137 | | Java | safe | strong | 138 | | JavaScript | | weak | 139 | |--------------+--------------+--------------| 140 | nil 141 | ``` 142 | 143 | Each column can also take a format function to alter the way the cells 144 | are displayed. For example, there's an included bar function for 145 | creating text bar charts: 146 | 147 | ```clojure 148 | > (use '[doric.core :only [bar]]) 149 | nil 150 | > (println (table {:format raw} [:a :b {:name :c :format bar}] 151 | [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}])) 152 | A B C 153 | 1 2 ### 154 | 4 5 ###### 155 | nil 156 | ``` 157 | 158 | Column level options include: 159 | 160 | * :align - :left, :right, :center, defaults to :left 161 | * :title - a string, defaults to your column name, title-cased 162 | * :title-align - like align, and defaults to the same as :align 163 | * :format - a function to call on the values in the column, pre-output 164 | * :when - a boolean, allows you to turn columns on and off 165 | * :width - how wide to make the column, defaults to wide enough 166 | * :ellipsis - a boolean, whether or not to ... truncated values, defaults to false 167 | 168 | ## License 169 | 170 | Copyright (C) 2014 Joe Gallo and Dan Larkin 171 | 172 | Distributed under the Eclipse Public License, the same as Clojure. 173 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject doric "0.10.0-SNAPSHOT" 2 | :description "Clojure table layout" 3 | :url "https://github.com/joegallo/doric" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :profiles {:1.2 {:dependencies [[org.clojure/clojure "1.2.1"]]} 7 | :1.3 {:dependencies [[org.clojure/clojure "1.3.0"]]} 8 | :1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]} 9 | :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} 10 | :dev {:dependencies [[org.clojure/clojure "1.6.0"] 11 | [org.apache.poi/poi "3.10.1"]]}} 12 | :aliases {"all" ["with-profile" "dev,1.2:dev,1.3:dev,1.4:dev,1.5:dev"]}) 13 | -------------------------------------------------------------------------------- /src/doric/core.clj: -------------------------------------------------------------------------------- 1 | (ns doric.core 2 | (:refer-clojure :exclude [format name join split when]) 3 | (:use [clojure.string :only [join split]])) 4 | 5 | (defn- title-case-word [w] 6 | (if (zero? (count w)) 7 | w 8 | (str (Character/toTitleCase (first w)) 9 | (subs w 1)))) 10 | 11 | (defn title-case [s] 12 | (join " " (map title-case-word (split s #"\s")))) 13 | 14 | (defn align [col & [data]] 15 | (or (keyword (:align col)) 16 | :left)) 17 | 18 | (defn format [col & [data]] 19 | (or (:format col) 20 | identity)) 21 | 22 | (defn title [col & [data]] 23 | (or (:title col) 24 | (title-case 25 | (.replaceAll (clojure.core/name (let [n (:name col)] 26 | (if (number? n) 27 | (str n) 28 | n))) 29 | "-" " ")))) 30 | 31 | (defn title-align [col & [data]] 32 | (keyword (or (:title-align col) 33 | (:align col) 34 | :center))) 35 | 36 | (defn when [col & [data]] 37 | (:when col true)) 38 | 39 | (defn width [col & [data]] 40 | (or (:width col) 41 | (apply max (map count (cons (:title col) 42 | (map str data)))))) 43 | 44 | (defn format-cell [col s] 45 | ((:format col) s)) 46 | 47 | (defn align-cell [col s align] 48 | (let [width (:width col) 49 | s (str s) 50 | s (cond (<= (count s) width) s 51 | (:ellipsis col) (str (subs s 0 (- width 3)) "...") 52 | :else (subs s 0 width)) 53 | len (count s) 54 | pad #(apply str (take % (repeat " "))) 55 | padding (- width len) 56 | half-padding (/ (- width len) 2)] 57 | (case align 58 | :left (str s (pad padding)) 59 | :right (str (pad padding) s) 60 | :center (str (pad (Math/ceil half-padding)) 61 | s 62 | (pad (Math/floor half-padding)))))) 63 | 64 | (defn header [th cols] 65 | (for [col cols :when (:when col)] 66 | (th col))) 67 | 68 | (defn body [td cols rows] 69 | (for [row rows] 70 | (for [col cols :when (:when col)] 71 | (td col row)))) 72 | 73 | (defn- col-data [col rows] 74 | (map #(get % (:name col)) rows)) 75 | 76 | (defn- column1 [col & [data]] 77 | {:align (align col data) 78 | :format (format col data) 79 | :title (title col data) 80 | :title-align (title-align col data) 81 | :when (when col data)}) 82 | 83 | (defn- column-map [col] 84 | (if (map? col) 85 | col 86 | {:name col})) 87 | 88 | (defn- columns1 [cols rows] 89 | (for [col cols :let [col (column-map col)]] 90 | (merge col 91 | (column1 col (col-data col rows))))) 92 | 93 | (defn- format-rows [cols rows] 94 | (for [row rows] 95 | (into {} 96 | (for [col cols :let [name (:name col)]] 97 | [name (format-cell col (row name))])))) 98 | 99 | (defn- column2 [col & [data]] 100 | {:width (width col data)}) 101 | 102 | (defn- columns2 [cols rows] 103 | (for [col cols] 104 | (merge col 105 | (column2 col (col-data col rows))))) 106 | 107 | ;; data formats 108 | (defn bar [x] 109 | (apply str (repeat x "#"))) 110 | 111 | ;; table formats 112 | (def csv 'doric.csv) 113 | (def html 'doric.html) 114 | (def org 'doric.org) 115 | (def raw 'doric.raw) 116 | 117 | ;; table format helpers 118 | ;; aligned th and td are useful for whitespace sensitive formats, like 119 | ;; raw and org 120 | (defn aligned-th [col] 121 | (align-cell col (:title col) (:title-align col))) 122 | 123 | (defn aligned-td [col row] 124 | (align-cell col (row (:name col)) (:align col))) 125 | 126 | ;; unalighed-th and td are useful for whitespace immune formats, like 127 | ;; csv and html 128 | (defn unaligned-th [col] 129 | (:title col)) 130 | 131 | (defn unaligned-td [col row] 132 | (row (:name col))) 133 | 134 | (defn mapify [rows] 135 | (let [example (first rows)] 136 | (cond (map? rows) (for [k (sort (keys rows))] 137 | {:key k :val (rows k)} ) 138 | (vector? example) (for [row rows] 139 | (into {} 140 | (map-indexed (fn [i x] [i x]) row))) 141 | (map? example) rows))) 142 | 143 | (defn table* 144 | {:arglists '[[rows] 145 | [opts rows] 146 | [cols rows] 147 | [opts cols rows]]} 148 | [& args] 149 | (let [rows (mapify (last args)) 150 | [opts cols] (case (count args) 151 | 1 [nil nil] 152 | 2 (if (map? (first args)) 153 | [(first args) nil] 154 | [nil (first args)]) 155 | 3 [(first args) (second args)]) 156 | cols (or cols (keys (first rows))) 157 | format (or (:format opts) org) 158 | _ (require format) 159 | th (ns-resolve format 'th) 160 | td (ns-resolve format 'td) 161 | render (ns-resolve format 'render) 162 | cols (columns1 cols rows) 163 | rows (format-rows cols rows) 164 | cols (columns2 cols rows)] 165 | (render (cons (header th cols) (body td cols rows))))) 166 | 167 | (defn table 168 | {:arglists '[[rows] 169 | [opts rows] 170 | [cols rows] 171 | [otps cols rows]]} 172 | [& args] 173 | (apply str (join "\n" (apply table* args)))) 174 | -------------------------------------------------------------------------------- /src/doric/csv.clj: -------------------------------------------------------------------------------- 1 | (ns doric.csv 2 | (:refer-clojure :exclude [join]) 3 | (:use [clojure.string :only [join]] 4 | [doric.core :only [unaligned-th unaligned-td]])) 5 | 6 | (def th unaligned-th) 7 | 8 | (def td unaligned-td) 9 | 10 | (defn escape [s] 11 | (let [s (.replaceAll (str s) "\"" "\"\"")] 12 | (if (re-find #"[,\n\"]" s) 13 | (str "\"" s "\"") 14 | s))) 15 | 16 | (defn render [table] 17 | (cons (join "," (map escape (first table))) 18 | (for [tr (rest table)] 19 | (join "," (map escape tr))))) 20 | -------------------------------------------------------------------------------- /src/doric/hssf.clj: -------------------------------------------------------------------------------- 1 | (ns doric.hssf 2 | (:import (org.apache.poi.hssf.usermodel HSSFSheet HSSFWorkbook) 3 | (org.apache.poi.ss.usermodel CellStyle Font))) 4 | 5 | (defn th [col] 6 | [(:title col) col]) 7 | 8 | (defn td [col row] 9 | [(row (:name col)) col]) 10 | 11 | (defn cell-style [wb align & [font]] 12 | (let [cs (.createCellStyle wb)] 13 | (.setAlignment cs (condp = align 14 | :left CellStyle/ALIGN_LEFT 15 | :center CellStyle/ALIGN_CENTER 16 | :right CellStyle/ALIGN_RIGHT)) 17 | (when font 18 | (.setFont cs font)) 19 | cs)) 20 | 21 | (defn render [table] 22 | (let [wb (HSSFWorkbook.) 23 | ^HSSFSheet sh (.createSheet wb "Sheet1") 24 | ^Font bold (.createFont wb) 25 | _ (.setBoldweight bold Font/BOLDWEIGHT_BOLD)] 26 | (let [row (.createRow sh 0)] 27 | (doseq [[j [s m]] (map-indexed vector (first table))] 28 | (let [c (.createCell row j)] 29 | (.setCellValue c s) 30 | (.setCellStyle c (cell-style wb (:title-align m) bold))))) 31 | (doseq [[i r] (map-indexed vector (rest table))] 32 | (let [row (.createRow sh (inc i))] 33 | (doseq [[j [s m]] (map-indexed vector r)] 34 | (let [c (.createCell row j)] 35 | (.setCellValue c (if (number? s) 36 | (double s) 37 | s)) 38 | (.setCellStyle c (cell-style wb (:align m))))))) 39 | wb)) 40 | 41 | ;; this is an undocumented alpha format that i wrote primarily just to 42 | ;; see how well doric could handle it -- if you use it, please be 43 | ;; aware that this is not supported in any way. that said, you could 44 | ;; use it like this, more or less: 45 | 46 | #_ 47 | (with-open [f (java.io.FileOutputStream. "out.xls")] 48 | (.write (table* ^{:format 'doric.hssf} [{:a 1 :b 2}{:a 3 :b 4}]))) 49 | 50 | ;; fun, huh? 51 | -------------------------------------------------------------------------------- /src/doric/html.clj: -------------------------------------------------------------------------------- 1 | (ns doric.html 2 | (:refer-clojure :exclude [join]) 3 | (:use [clojure.string :only [join]] 4 | [doric.core :only [unaligned-th unaligned-td]])) 5 | 6 | (def th unaligned-th) 7 | 8 | (def td unaligned-td) 9 | 10 | (defn render [table] 11 | (concat ["" 12 | (str "" (join (for [c (first table)] 13 | (str ""))) "")] 14 | (for [tr (rest table)] 15 | (str "" (join (for [c tr] 16 | (str ""))) "")) 17 | ["
" c "
" c "
"])) 18 | -------------------------------------------------------------------------------- /src/doric/org.clj: -------------------------------------------------------------------------------- 1 | (ns doric.org 2 | (:refer-clojure :exclude [join]) 3 | (:use [clojure.string :only [join]] 4 | [doric.core :only [aligned-th aligned-td]])) 5 | 6 | (def th aligned-th) 7 | 8 | (def td aligned-td) 9 | 10 | (defn render [table] 11 | (let [spacer (str "|-" 12 | (join "-+-" 13 | (map #(apply str (repeat (.length %) "-")) 14 | (first table))) 15 | "-|")] 16 | (concat [spacer 17 | (str "| " (join " | " (first table)) " |") 18 | spacer] 19 | (for [tr (rest table)] 20 | (str "| " (join " | " tr) " |")) 21 | [spacer]))) 22 | -------------------------------------------------------------------------------- /src/doric/raw.clj: -------------------------------------------------------------------------------- 1 | (ns doric.raw 2 | (:refer-clojure :exclude [join]) 3 | (:use [clojure.string :only [join]] 4 | [doric.core :only [aligned-th aligned-td]])) 5 | 6 | (def th aligned-th) 7 | 8 | (def td aligned-td) 9 | 10 | (defn render [table] 11 | (cons (join " " (first table)) 12 | (for [tr (rest table)] 13 | (join " " tr)))) 14 | -------------------------------------------------------------------------------- /test/doric/test/core.clj: -------------------------------------------------------------------------------- 1 | (ns doric.test.core 2 | (:refer-clojure :exclude [format name when]) 3 | (:use [doric.core] 4 | [clojure.test] 5 | [doric.org :only [th td render]])) 6 | 7 | (deftest test-title-case 8 | (is (= "Foo" (title-case "foo"))) 9 | (is (= "Foo-bar" (title-case "foo-bar"))) 10 | (is (= "Foo Bar" (title-case "foo bar"))) 11 | (is (= "Foo Bar" (title-case "foo bar")))) 12 | 13 | (deftest test-align 14 | (is (= :left (align {}))) 15 | (is (= :right (align {:align :right})))) 16 | 17 | (deftest test-format 18 | (is (= identity (format {}))) 19 | (is (= str (format {:format str})))) 20 | 21 | (deftest test-title 22 | (is (= "foo" (title {:title "foo"}))) 23 | (is (= "Foo" (title {:name "foo"})))) 24 | 25 | (deftest test-title-align 26 | (is (= :center (title-align {}))) 27 | (is (= :left (title-align {:align :left}))) 28 | (is (= :left (title-align {:align 'left}))) 29 | (is (= :left (title-align {:align "left"}))) 30 | (is (= :right (title-align {:align :left :title-align :right}))) 31 | (is (= :right (title-align {:align :left :title-align :right})))) 32 | 33 | (deftest test-when 34 | (is (re-find #"Foo" (table [{:name :foo}] [{:foo :bar}]))) 35 | (is (re-find #"bar" (table [{:name :foo}] [{:foo :bar}]))) 36 | (is (re-find #"Foo" (table [{:name :foo :when true}] [{:foo :bar}]))) 37 | (is (re-find #"bar" (table [{:name :foo :when true}] [{:foo :bar}]))) 38 | (is (not (re-find #"Foo" (table [{:name :foo :when false}] [{:foo :bar}])))) 39 | (is (not (re-find #"bar" (table [{:name :foo :when false}] [{:foo :bar}]))))) 40 | 41 | (deftest test-width 42 | (is (= 5 (width {:width 5}))) 43 | (is (= 5 (width {:width 5 :name :foobar}))) 44 | (is (= 7 (width {:name :foobar} ["foobar2"])))) 45 | 46 | (deftest test-format-cell 47 | (is (= 2 (format-cell {:format inc} 1)))) 48 | 49 | (deftest test-align-cell 50 | (is (= "." (align-cell {:width 1} "." :left))) 51 | (is (= "." (align-cell {:width 1} "." :center))) 52 | (is (= "." (align-cell {:width 1} "." :right))) 53 | (is (= ". " (align-cell {:width 3} "." :left))) 54 | (is (= " . " (align-cell {:width 3} "." :center))) 55 | (is (= " ." (align-cell {:width 3} "." :right))) 56 | (is (= ". " (align-cell {:width 4} "." :left))) 57 | (is (= " . " (align-cell {:width 4} "." :center))) 58 | (is (= " ." (align-cell {:width 4} "." :right)))) 59 | 60 | (deftest test-th 61 | (is (= "Title " (th {:title "Title" :width 7 :title-align :left}))) 62 | (is (= " Title " (th {:title "Title" :width 7 :title-align :center}))) 63 | (is (= " Title" (th {:title "Title" :width 7 :title-align :right})))) 64 | 65 | (deftest test-td 66 | (is (= ". " (td {:name :t :width 3 :align :left} {:t "."}))) 67 | (is (= " . " (td {:name :t :width 3 :align :center} {:t "."}))) 68 | (is (= " ." (td {:name :t :width 3 :align :right} {:t "."})))) 69 | 70 | ;; TODO (deftest test-header) 71 | 72 | ;; TODO (deftest test-body) 73 | 74 | (deftest test-render 75 | (let [rendered (render [["1" "2"]["3" "4"]])] 76 | (is (.contains rendered "| 1 | 2 |")) 77 | (is (.contains rendered "| 3 | 4 |")) 78 | (is (.contains rendered "|---+---|")))) 79 | 80 | ;; TODO embiggen these tests 81 | (deftest test-table 82 | (let [rendered (table [{:1 3 :2 4}])] 83 | (is (.contains rendered "| 1 | 2 |")) 84 | (is (.contains rendered "| 3 | 4 |")) 85 | (is (.contains rendered "|---+---|")))) 86 | 87 | (deftest test-table*-laziness 88 | (let [calls (atom 0) 89 | inc #(do (swap! calls inc) %)] 90 | (testing "formats are not lazy" 91 | (let [seq (table* [{:name :1 :format inc} 92 | {:name :2 :format inc}] 93 | [{:1 3 :2 4}])] 94 | (is (= 2 @calls)))) 95 | (reset! calls 0) 96 | (testing "unless you provide widths" 97 | (let [seq (table* [{:name :1 :format inc :width 10} 98 | {:name :2 :format inc :width 10}] 99 | [{:1 3 :2 4}])] 100 | (is (= 0 @calls)))) 101 | (reset! calls 0) 102 | (testing "even for formats that should be automatically lazy, like csv" 103 | (let [seq (table* ^{:format csv} 104 | [{:name :1 :format inc :width 0} 105 | {:name :2 :format inc :width 0}] 106 | [{:1 3 :2 4}])] 107 | (is (= 0 @calls)))))) 108 | 109 | (deftest test-empty-table 110 | (let [empty-table "|--|\n| |\n|--|\n|--|"] 111 | (is (= empty-table (table []))) 112 | (is (= empty-table (table nil))) 113 | (is (= empty-table (table [] []))) 114 | (is (= empty-table (table [] nil))) 115 | (is (= empty-table (table nil []))) 116 | (is (= empty-table (table nil nil))))) 117 | -------------------------------------------------------------------------------- /test/doric/test/doctest.clj: -------------------------------------------------------------------------------- 1 | (ns doric.test.doctest 2 | (:use [clojure.java.io :only [file]] 3 | [clojure.test]) 4 | (:import (java.io PushbackReader StringReader))) 5 | 6 | (defn fenced-blocks 7 | "detect and extract github-style fenced blocks in a file" 8 | [s] 9 | (map second 10 | (re-seq #"(?m)(?s)^```clojure\n(.*?)\n^```" s))) 11 | 12 | (def prompt 13 | ;; regex for finding 'foo.bar>' repl prompts 14 | "(?m)\n*^\\S*>\\s*") 15 | 16 | (defn skip? 17 | "is a result skippable?" 18 | ;; if it's a comment, the answer is yes 19 | [s] 20 | (.startsWith s ";")) 21 | 22 | (defn reps 23 | "given a string of read-eval-print sequences, separate the different 24 | 'r-e-p's from each other" 25 | [prompt s] 26 | (rest (.split s prompt))) 27 | 28 | (defn markdown-tests 29 | "extract all the tests from a markdown file" 30 | [f] 31 | (->> f 32 | slurp 33 | fenced-blocks 34 | (mapcat (partial reps prompt)))) 35 | 36 | (defn repl-tests 37 | "extract all the tests from a repl-session-like file" 38 | [f] 39 | (->> f 40 | slurp 41 | (reps prompt))) 42 | 43 | (defn temp-ns 44 | "create a temporary ns, and return its name" 45 | [] 46 | (binding [*ns* *ns*] 47 | (in-ns (gensym)) 48 | (use 'clojure.core) 49 | (.getName *ns*))) 50 | 51 | (defn eval-in-ns 52 | "evaluate a form inside the given ns-name" 53 | [ns form] 54 | (binding [*ns* *ns*] 55 | (in-ns ns) 56 | (eval form))) 57 | 58 | (defn run-doctest 59 | "run a single doctest, reporting success or failure" 60 | [file idx ns test] 61 | (let [r (PushbackReader. (StringReader. test)) 62 | form (read r) 63 | expected (.trim (slurp r)) 64 | actual (when-not (skip? expected) 65 | (.trim (try 66 | (with-out-str 67 | (pr (eval-in-ns ns form)) 68 | (flush)) 69 | (catch Exception _ 70 | (println _) 71 | (.toString (gensym))))))] 72 | (if (or (skip? expected) 73 | (= actual expected)) 74 | (report {:type :pass}) 75 | (report {:type :fail 76 | :file file :line idx 77 | :expected expected :actual actual})))) 78 | 79 | (defn run-doctests 80 | "use text-extract-fn to get all the tests out of file, and run them 81 | all, reporting success or failure" 82 | [test-extract-fn file] 83 | (let [ns (temp-ns)] 84 | (doseq [[idx t] (map-indexed vector (test-extract-fn file))] 85 | (run-doctest file idx ns t)) 86 | (remove-ns ns))) 87 | 88 | 89 | (comment 90 | ;; example usage 91 | (deftest bar-repl 92 | (run-doctests repl-tests "test/bar.repl"))) 93 | -------------------------------------------------------------------------------- /test/doric/test/readme.clj: -------------------------------------------------------------------------------- 1 | (ns doric.test.readme 2 | (:use [clojure.test] 3 | [doric.test.doctest])) 4 | 5 | (deftest readme 6 | (run-doctests markdown-tests "README.md")) 7 | --------------------------------------------------------------------------------