├── .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 "" c " | "))) " ")]
14 | (for [tr (rest table)]
15 | (str "" (join (for [c tr]
16 | (str "" c " | "))) " "))
17 | [" "]))
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 |
--------------------------------------------------------------------------------
|