Skip to content

Commit

Permalink
Major refactor: switch to protocol-based render
Browse files Browse the repository at this point in the history
  • Loading branch information
Joshua Davey committed Mar 17, 2017
1 parent ac02767 commit 290b2e7
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 105 deletions.
22 changes: 7 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,7 @@ Add this to your project.clj :dependencies list:
In most cases, you'll just want to use/require the table function.

```clojure
> (use '[doric.core :only [table]])
nil
```

But you can access other things if you'd like, for instance if you
want to use the other formats.

```clojure
> (use '[doric.core :only [table csv html org raw]])
> (require '[doric.core :refer [table]])
nil
```

Expand Down Expand Up @@ -59,27 +51,27 @@ nil
But you can also have raw, csv, and html tables pretty easily:

```clojure
> (println (table {:format raw} [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
> (println (table {:format :raw} [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
A B C
1 2 3
4 5 6
nil

> (println (table {:format csv} [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
> (println (table {:format :csv} [:a :b :c] [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
A,B,C
1,2,3
4,5,6
nil

> (println (table {:format html} [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
> (println (table {:format :html} [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
;; omg lots of <tr>s and <td>s here
```

You can also use a custom table format by specifying a namespace that
contains the functions th, td, and render.

```clojure
> (println (table {:format 'my.sweet.ns} [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
> (println (table {:format thing-that-implements-Render} [{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
;; the sky's the limit, buddy
```

Expand Down Expand Up @@ -145,9 +137,9 @@ are displayed. For example, there's an included bar function for
creating text bar charts:

```clojure
> (use '[doric.core :only [bar]])
> (require '[doric.core :refer [table bar]])
nil
> (println (table {:format raw} [:a :b {:name :c :format bar}]
> (println (table {:format :raw} [:a :b {:name :c :format bar}]
[{:a 1 :b 2 :c 3}{:a 4 :b 5 :c 6}]))
A B C
1 2 ###
Expand Down
69 changes: 34 additions & 35 deletions src/doric/core.clj
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
(ns doric.core
(:refer-clojure :exclude [format name join split])
(:require [doric.formatting :refer [titleize]]
[clojure.string :as str]))
[doric.protocols :refer [render render-lazy]]
[clojure.string :as str]
[doric.org]
[doric.raw]
[doric.html]
[doric.csv]))

(defn column-defaults [col]
(merge col
Expand Down Expand Up @@ -30,23 +35,14 @@
(defn bar [x]
(apply str (repeat x "#")))

(defn header [th cols]
(for [col cols
:when (:when col)]
(th col)))

(defn body [td cols rows]
(for [row rows]
(for [col cols
:when (:when col)]
(td col row))))

(defn format-rows [cols rows]
(for [row rows]
(reduce
(fn [m {:keys [name] :as col}]
(fn [m {:keys [name format] :as col}]
(assoc m name
((:format col) (get row name))))
(-> row
(get name)
format)))
{}
cols)))

Expand All @@ -59,10 +55,10 @@
{:width (width col (col-data col rows))})))

;; table formats
(def csv 'doric.csv)
(def html 'doric.html)
(def org 'doric.org)
(def raw 'doric.raw)
(def renderers {:csv doric.csv/renderer
:html doric.html/renderer
:org doric.org/renderer
:raw doric.raw/renderer})

(defn mapify [rows]
(let [example (first rows)]
Expand All @@ -74,8 +70,8 @@
(map? example) rows)))

(defn conform
"Given an optional colspec and a sequence of maps, returns tuple
of [conformed-columns formatted-rows]"
"Given an optional colspec and a sequence of maps, returns map with
keys :cols, :rows"
([rows]
(conform nil rows))
([cols rows]
Expand All @@ -84,33 +80,36 @@
(keys (first rows))))
rows (format-rows cols rows)
cols (columns-with-widths cols rows)]
[cols rows])))
{:cols cols, :rows rows})))

(defn table*
{:arglists '[[rows]
[opts rows]
[cols rows]
[opts cols rows]]}
[& args]
(defn -parse-args
[args]
(let [rows (last args)
[opts cols] (case (count args)
1 [nil nil]
2 (if (map? (first args))
[(first args) nil]
[nil (first args)])
3 [(first args) (second args)])
format (or (:format opts) org)
_ (require format)
th (ns-resolve format 'th)
td (ns-resolve format 'td)
render (ns-resolve format 'render)
[cols rows] (conform cols rows)]
(render (cons (header th cols) (body td cols rows)))))
format (or (:format opts) :org)
renderer (renderers format format)
cols-rows (conform cols rows)]
(merge {:renderer renderer} cols-rows)))

(defn table*
{:arglists '[[rows]
[opts rows]
[cols rows]
[opts cols rows]]}
[& args]
(let [{:keys [renderer cols rows]} (-parse-args args)]
(render-lazy renderer cols rows)))

(defn table
{:arglists '[[rows]
[opts rows]
[cols rows]
[otps cols rows]]}
[& args]
(apply str (str/join "\n" (apply table* args))))
(let [{:keys [renderer cols rows]} (-parse-args args)]
(render renderer cols rows)))
16 changes: 9 additions & 7 deletions src/doric/csv.clj
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
(ns doric.csv
(:require [clojure.string :as str]
[doric.protocols :refer [tabular-renderer]]
[doric.formatting :refer [unaligned-th unaligned-td]]))

(def th unaligned-th)

(def td unaligned-td)

(defn escape [s]
(let [s (.replaceAll (str s) "\"" "\"\"")]
(if (re-find #"[,\n\"]" s)
(str "\"" s "\"")
s)))

(defn render [table]
(cons (str/join "," (map escape (first table)))
(for [tr (rest table)]
(str/join "," (map escape tr)))))
(defn assemble [rows]
(cons (str/join "," (first rows))
(for [tr (rest rows)]
(str/join "," tr))))

(def renderer (tabular-renderer {:th (comp escape unaligned-th)
:td (comp escape unaligned-td)
:assemble assemble}))
16 changes: 6 additions & 10 deletions src/doric/formatting.clj
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,17 @@

;; unalighed-th and td are useful for whitespace immune formats, like
;; csv and html
(defn unaligned-th [col]
(:title col))

(defn unaligned-td [col row]
(row (:name col)))
(defn unaligned-th [_ data] data)
(defn unaligned-td [_ data] data)

;; aligned th and td are useful for whitespace sensitive formats, like
;; raw and org

(defn aligned-th [col]
(defn aligned-th [col cell-data]
(align-cell col
(unaligned-th col)
cell-data
(:title-align col)))

(defn aligned-td [col row]
(defn aligned-td [col cell-data]
(align-cell col
(unaligned-td col row)
cell-data
(:align col)))
19 changes: 10 additions & 9 deletions src/doric/html.clj
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
(ns doric.html
(:use [clojure.string :as str]
[doric.formatting :refer [unaligned-th unaligned-td]]))
(:require [clojure.string :as str]
[doric.protocols :refer [tabular-renderer]]
[doric.formatting :refer [unaligned-th unaligned-td]]))

(def th unaligned-th)

(def td unaligned-td)

(defn render [table]
(defn assemble [rows]
(concat ["<table>"
(str "<tr>" (str/join (for [c (first table)]
(str "<tr>" (str/join (for [c (first rows)]
(str "<th>" c "</th>"))) "</tr>")]
(for [tr (rest table)]
(for [tr (rest rows)]
(str "<tr>" (str/join (for [c tr]
(str "<td>" c "</td>"))) "</tr>"))
["</table>"]))

(def renderer (tabular-renderer {:th unaligned-th
:td unaligned-td
:assemble assemble}))
19 changes: 10 additions & 9 deletions src/doric/org.clj
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
(ns doric.org
(:require [clojure.string :as str]
[doric.protocols :refer [tabular-renderer]]
[doric.formatting :refer [aligned-th aligned-td]]))

(def th aligned-th)

(def td aligned-td)

(defn render [table]
(defn assemble [rows]
(let [spacer (str "|-"
(str/join "-+-"
(map #(apply str (repeat (.length %) "-"))
(first table)))
(map #(apply str (repeat (.length %) "-"))
(first rows)))
"-|")]
(concat [spacer
(str "| " (str/join " | " (first table)) " |")
(str "| " (str/join " | " (first rows)) " |")
spacer]
(for [tr (rest table)]
(for [tr (rest rows)]
(str "| " (str/join " | " tr) " |"))
[spacer])))

(def renderer (tabular-renderer {:th aligned-th
:td aligned-td
:assemble assemble}))
30 changes: 30 additions & 0 deletions src/doric/protocols.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
(ns doric.protocols
(:require [clojure.string :as str]))

(defprotocol Render
(-render-lazy [_ cols data])
(-render [_ cols data]))

(defrecord TabularRender [th td assemble]
Render
(-render [this cols data]
(str/join "\n"
(-render-lazy this cols data)))
(-render-lazy [_ cols data]
(assemble
(cons (for [col cols
:when (:when col)]
(th col (:title col)))
(for [row data]
(for [col cols
:when (:when col)]
(td col (get row (:name col)))))))))

(defn render-lazy [renderer cols data]
(-render-lazy renderer cols data))

(defn render [renderer cols data]
(-render renderer cols data))

(defn tabular-renderer [{:keys [td th assemble] :as fns}]
(map->TabularRender fns))
15 changes: 8 additions & 7 deletions src/doric/raw.clj
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
(ns doric.raw
(:require [clojure.string :as str]
[doric.protocols :refer [tabular-renderer]]
[doric.formatting :refer [aligned-th aligned-td]]))

(def th aligned-th)

(def td aligned-td)

(defn render [table]
(cons (str/join " " (first table))
(for [tr (rest table)]
(defn assemble [rows]
(cons (str/join " " (first rows))
(for [tr (rest rows)]
(str/join " " tr))))

(def renderer (tabular-renderer {:th aligned-th
:td aligned-td
:assemble assemble}))
17 changes: 4 additions & 13 deletions test/doric/test/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
(:refer-clojure :exclude [format name when])
(:use [doric.core]
[clojure.test]
[doric.org :only [th td render]]))
[doric.org :only [assemble]]))

(deftest test-column-defaults
(is (= "foo" (:title (columnize {:title "foo"}))))
Expand All @@ -24,22 +24,13 @@
(is (= 8 (width {:title "Title"} ["whatever" "is" "largest"])))
(is (= 7 (width {:name :foobar} ["foobar2"]))))

(deftest test-th
(is (= "Title " (th {:title "Title" :width 7 :title-align :left})))
(is (= " Title " (th {:title "Title" :width 7 :title-align :center})))
(is (= " Title" (th {:title "Title" :width 7 :title-align :right}))))

(deftest test-td
(is (= ". " (td {:name :t :width 3 :align :left} {:t "."})))
(is (= " . " (td {:name :t :width 3 :align :center} {:t "."})))
(is (= " ." (td {:name :t :width 3 :align :right} {:t "."}))))

;; TODO (deftest test-header)

;; TODO (deftest test-body)

(deftest test-render
(let [rendered (render [["1" "2"]["3" "4"]])]
(deftest test-assemble
(let [rendered (assemble [["1" "2"]["3" "4"]])]
(is (.contains rendered "| 1 | 2 |"))
(is (.contains rendered "| 3 | 4 |"))
(is (.contains rendered "|---+---|"))))
Expand Down Expand Up @@ -67,7 +58,7 @@
(is (= 0 @calls))))
(reset! calls 0)
(testing "even for formats that should be automatically lazy, like csv"
(let [seq (table* ^{:format csv}
(let [seq (table* {:format :csv}
[{:name :1 :format inc :width 0}
{:name :2 :format inc :width 0}]
[{:1 3 :2 4}])]
Expand Down
Loading

0 comments on commit 290b2e7

Please sign in to comment.