From 30ce13b6581ce69b9020aaba2c9cb05e17957d22 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 01:58:23 +0000 Subject: [PATCH 01/14] Modernize test matrix --- project.clj | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/project.clj b/project.clj index 9b78edb..dce8fb9 100644 --- a/project.clj +++ b/project.clj @@ -3,10 +3,9 @@ :url "https://github.com/joegallo/doric" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :profiles {:1.2 {:dependencies [[org.clojure/clojure "1.2.1"]]} - :1.3 {:dependencies [[org.clojure/clojure "1.3.0"]]} - :1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]} - :1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} - :dev {:dependencies [[org.clojure/clojure "1.6.0"] + :profiles {:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} + :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} + :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} + :dev {:dependencies [[org.clojure/clojure "1.9.0-alpha15"] [org.apache.poi/poi "3.10.1"]]}} - :aliases {"all" ["with-profile" "dev,1.2:dev,1.3:dev,1.4:dev,1.5:dev"]}) + :aliases {"all" ["with-profile" "dev,1.6:dev,1.7:dev,1.8:dev"]}) From 434ce8bef2121136e2b36ed0bbb0fe1dc372bb99 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 02:00:41 +0000 Subject: [PATCH 02/14] Extract formatting ns Also, modernize ns decls --- src/doric/core.clj | 125 ++++++++------------------------- src/doric/csv.clj | 9 ++- src/doric/formatting.clj | 58 +++++++++++++++ src/doric/html.clj | 9 ++- src/doric/org.clj | 11 ++- src/doric/raw.clj | 9 ++- test/doric/test/core.clj | 41 ++--------- test/doric/test/formatting.clj | 21 ++++++ 8 files changed, 132 insertions(+), 151 deletions(-) create mode 100644 src/doric/formatting.clj create mode 100644 test/doric/test/formatting.clj diff --git a/src/doric/core.clj b/src/doric/core.clj index f3f67f4..6e9d998 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -1,94 +1,52 @@ (ns doric.core - (:refer-clojure :exclude [format name join split when]) - (:use [clojure.string :only [join split]])) - -(defn- title-case-word [w] - (if (zero? (count w)) - w - (str (Character/toTitleCase (first w)) - (subs w 1)))) - -(defn title-case [s] - (join " " (map title-case-word (split s #"\s")))) - -(defn align [col & [data]] - (or (keyword (:align col)) - :left)) - -(defn format [col & [data]] - (or (:format col) - identity)) - -(defn title [col & [data]] - (or (:title col) - (title-case - (.replaceAll (clojure.core/name (let [n (:name col)] - (if (number? n) - (str n) - n))) - "-" " ")))) - -(defn title-align [col & [data]] - (keyword (or (:title-align col) - (:align col) - :center))) - -(defn when [col & [data]] - (:when col true)) + (:refer-clojure :exclude [format name join split]) + (:require [doric.formatting :refer [titleize]] + [clojure.string :as str])) + +(defn column-defaults [col] + (merge col + {:align (keyword (get col :align :left)) + :format (or (:format col) + identity) + :title (or (:title col) + (titleize (:name col))) + :title-align (keyword (or (:title-align col) + (:align col) + :center)) + :when (:when col true)})) + +(defn column-map [col] + (if (map? col) + col + {:name col})) + +(def columnize (comp column-defaults column-map)) (defn width [col & [data]] (or (:width col) (apply max (map count (cons (:title col) (map str data)))))) +(defn bar [x] + (apply str (repeat x "#"))) + (defn format-cell [col s] ((:format col) s)) -(defn align-cell [col s align] - (let [width (:width col) - s (str s) - s (cond (<= (count s) width) s - (:ellipsis col) (str (subs s 0 (- width 3)) "...") - :else (subs s 0 width)) - len (count s) - pad #(apply str (take % (repeat " "))) - padding (- width len) - half-padding (/ (- width len) 2)] - (case align - :left (str s (pad padding)) - :right (str (pad padding) s) - :center (str (pad (Math/ceil half-padding)) - s - (pad (Math/floor half-padding)))))) - (defn header [th cols] - (for [col cols :when (:when col)] + (for [col cols + :when (:when col)] (th col))) (defn body [td cols rows] (for [row rows] - (for [col cols :when (:when col)] + (for [col cols + :when (:when col)] (td col row)))) (defn- col-data [col rows] (map #(get % (:name col)) rows)) -(defn- column1 [col & [data]] - {:align (align col data) - :format (format col data) - :title (title col data) - :title-align (title-align col data) - :when (when col data)}) - -(defn- column-map [col] - (if (map? col) - col - {:name col})) - -(defn- columns1 [cols rows] - (for [col cols :let [col (column-map col)]] - (merge col - (column1 col (col-data col rows))))) (defn- format-rows [cols rows] (for [row rows] @@ -104,33 +62,12 @@ (merge col (column2 col (col-data col rows))))) -;; data formats -(defn bar [x] - (apply str (repeat x "#"))) - ;; table formats (def csv 'doric.csv) (def html 'doric.html) (def org 'doric.org) (def raw 'doric.raw) -;; table format helpers -;; aligned th and td are useful for whitespace sensitive formats, like -;; raw and org -(defn aligned-th [col] - (align-cell col (:title col) (:title-align col))) - -(defn aligned-td [col row] - (align-cell col (row (:name col)) (:align col))) - -;; 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 mapify [rows] (let [example (first rows)] (cond (map? rows) (for [k (sort (keys rows))] @@ -159,7 +96,7 @@ th (ns-resolve format 'th) td (ns-resolve format 'td) render (ns-resolve format 'render) - cols (columns1 cols rows) + cols (map columnize cols) rows (format-rows cols rows) cols (columns2 cols rows)] (render (cons (header th cols) (body td cols rows))))) @@ -170,4 +107,4 @@ [cols rows] [otps cols rows]]} [& args] - (apply str (join "\n" (apply table* args)))) + (apply str (str/join "\n" (apply table* args)))) diff --git a/src/doric/csv.clj b/src/doric/csv.clj index 2e396b1..0aadcdb 100644 --- a/src/doric/csv.clj +++ b/src/doric/csv.clj @@ -1,7 +1,6 @@ (ns doric.csv - (:refer-clojure :exclude [join]) - (:use [clojure.string :only [join]] - [doric.core :only [unaligned-th unaligned-td]])) + (:require [clojure.string :as str] + [doric.formatting :refer [unaligned-th unaligned-td]])) (def th unaligned-th) @@ -14,6 +13,6 @@ s))) (defn render [table] - (cons (join "," (map escape (first table))) + (cons (str/join "," (map escape (first table))) (for [tr (rest table)] - (join "," (map escape tr))))) + (str/join "," (map escape tr))))) diff --git a/src/doric/formatting.clj b/src/doric/formatting.clj new file mode 100644 index 0000000..5f2c5a1 --- /dev/null +++ b/src/doric/formatting.clj @@ -0,0 +1,58 @@ +(ns doric.formatting + (:require [clojure.string :as str])) + +(defn- title-case-word [w] + (if (zero? (count w)) + w + (str (Character/toTitleCase (first w)) + (subs w 1)))) + +(defn title-case [s] + (str/join " " (map title-case-word (str/split s #"\s")))) + +(defn titleize [n] + (title-case + (.replaceAll ^String (name (if (number? n) + (str n) + n)) + "-" " "))) + +(defn align-cell [col s align] + (let [width (:width col) + s (str s) + s (cond (<= (count s) width) s + (:ellipsis col) (str (subs s 0 (- width 3)) "...") + :else (subs s 0 width)) + len (count s) + pad #(apply str (take % (repeat " "))) + padding (- width len) + half-padding (/ (- width len) 2)] + (case align + :left (str s (pad padding)) + :right (str (pad padding) s) + :center (str (pad (Math/ceil half-padding)) + s + (pad (Math/floor half-padding)))))) + +;; table format helpers + +;; 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))) + +;; aligned th and td are useful for whitespace sensitive formats, like +;; raw and org + +(defn aligned-th [col] + (align-cell col + (unaligned-th col) + (:title-align col))) + +(defn aligned-td [col row] + (align-cell col + (unaligned-td col row) + (:align col))) diff --git a/src/doric/html.clj b/src/doric/html.clj index 9eb0d91..f36ed7b 100644 --- a/src/doric/html.clj +++ b/src/doric/html.clj @@ -1,7 +1,6 @@ (ns doric.html - (:refer-clojure :exclude [join]) - (:use [clojure.string :only [join]] - [doric.core :only [unaligned-th unaligned-td]])) + (:use [clojure.string :as str] + [doric.formatting :refer [unaligned-th unaligned-td]])) (def th unaligned-th) @@ -9,9 +8,9 @@ (defn render [table] (concat ["" - (str "" (join (for [c (first table)] + (str "" (str/join (for [c (first table)] (str ""))) "")] (for [tr (rest table)] - (str "" (join (for [c tr] + (str "" (str/join (for [c tr] (str ""))) "")) ["
" c "
" c "
"])) diff --git a/src/doric/org.clj b/src/doric/org.clj index bf17aab..be99992 100644 --- a/src/doric/org.clj +++ b/src/doric/org.clj @@ -1,7 +1,6 @@ (ns doric.org - (:refer-clojure :exclude [join]) - (:use [clojure.string :only [join]] - [doric.core :only [aligned-th aligned-td]])) + (:require [clojure.string :as str] + [doric.formatting :refer [aligned-th aligned-td]])) (def th aligned-th) @@ -9,13 +8,13 @@ (defn render [table] (let [spacer (str "|-" - (join "-+-" + (str/join "-+-" (map #(apply str (repeat (.length %) "-")) (first table))) "-|")] (concat [spacer - (str "| " (join " | " (first table)) " |") + (str "| " (str/join " | " (first table)) " |") spacer] (for [tr (rest table)] - (str "| " (join " | " tr) " |")) + (str "| " (str/join " | " tr) " |")) [spacer]))) diff --git a/src/doric/raw.clj b/src/doric/raw.clj index 8d8bd4b..aac513b 100644 --- a/src/doric/raw.clj +++ b/src/doric/raw.clj @@ -1,13 +1,12 @@ (ns doric.raw - (:refer-clojure :exclude [join]) - (:use [clojure.string :only [join]] - [doric.core :only [aligned-th aligned-td]])) + (:require [clojure.string :as str] + [doric.formatting :refer [aligned-th aligned-td]])) (def th aligned-th) (def td aligned-td) (defn render [table] - (cons (join " " (first table)) + (cons (str/join " " (first table)) (for [tr (rest table)] - (join " " tr)))) + (str/join " " tr)))) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 43e75ed..ca0a72e 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -4,31 +4,11 @@ [clojure.test] [doric.org :only [th td render]])) -(deftest test-title-case - (is (= "Foo" (title-case "foo"))) - (is (= "Foo-bar" (title-case "foo-bar"))) - (is (= "Foo Bar" (title-case "foo bar"))) - (is (= "Foo Bar" (title-case "foo bar")))) - -(deftest test-align - (is (= :left (align {}))) - (is (= :right (align {:align :right})))) - -(deftest test-format - (is (= identity (format {}))) - (is (= str (format {:format str})))) - -(deftest test-title - (is (= "foo" (title {:title "foo"}))) - (is (= "Foo" (title {:name "foo"})))) - -(deftest test-title-align - (is (= :center (title-align {}))) - (is (= :left (title-align {:align :left}))) - (is (= :left (title-align {:align 'left}))) - (is (= :left (title-align {:align "left"}))) - (is (= :right (title-align {:align :left :title-align :right}))) - (is (= :right (title-align {:align :left :title-align :right})))) +(deftest test-column-defaults + (is (= "foo" (:title (columnize {:title "foo"})))) + (is (= "Foo" (:title (columnize {:name "foo"})))) + (is (= "Foo Bar" (:title (columnize {:name "foo bar"})))) + (is (= "Foo" (:title (columnize :foo))))) (deftest test-when (is (re-find #"Foo" (table [{:name :foo}] [{:foo :bar}]))) @@ -46,17 +26,6 @@ (deftest test-format-cell (is (= 2 (format-cell {:format inc} 1)))) -(deftest test-align-cell - (is (= "." (align-cell {:width 1} "." :left))) - (is (= "." (align-cell {:width 1} "." :center))) - (is (= "." (align-cell {:width 1} "." :right))) - (is (= ". " (align-cell {:width 3} "." :left))) - (is (= " . " (align-cell {:width 3} "." :center))) - (is (= " ." (align-cell {:width 3} "." :right))) - (is (= ". " (align-cell {:width 4} "." :left))) - (is (= " . " (align-cell {:width 4} "." :center))) - (is (= " ." (align-cell {:width 4} "." :right)))) - (deftest test-th (is (= "Title " (th {:title "Title" :width 7 :title-align :left}))) (is (= " Title " (th {:title "Title" :width 7 :title-align :center}))) diff --git a/test/doric/test/formatting.clj b/test/doric/test/formatting.clj new file mode 100644 index 0000000..9943519 --- /dev/null +++ b/test/doric/test/formatting.clj @@ -0,0 +1,21 @@ +(ns doric.test.formatting + (:require [doric.formatting :refer :all] + [clojure.test :refer :all])) + +(deftest test-title-case + (is (= "Foo" (title-case "foo"))) + (is (= "Foo-bar" (title-case "foo-bar"))) + (is (= "Foo Bar" (title-case "foo bar"))) + (is (= "Foo Bar" (title-case "foo bar")))) + +(deftest test-align-cell + (is (= "." (align-cell {:width 1} "." :left))) + (is (= "." (align-cell {:width 1} "." :center))) + (is (= "." (align-cell {:width 1} "." :right))) + (is (= ". " (align-cell {:width 3} "." :left))) + (is (= " . " (align-cell {:width 3} "." :center))) + (is (= " ." (align-cell {:width 3} "." :right))) + (is (= ". " (align-cell {:width 4} "." :left))) + (is (= " . " (align-cell {:width 4} "." :center))) + (is (= " ." (align-cell {:width 4} "." :right)))) + From ac027673caeae675c20c20638e3b10ca3ba3353d Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 02:43:34 +0000 Subject: [PATCH 03/14] Some minor refactoring --- src/doric/core.clj | 48 ++++++++++++++++++++++------------------ test/doric/test/core.clj | 8 +++---- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/doric/core.clj b/src/doric/core.clj index 6e9d998..4dc5894 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -22,7 +22,7 @@ (def columnize (comp column-defaults column-map)) -(defn width [col & [data]] +(defn width [col data] (or (:width col) (apply max (map count (cons (:title col) (map str data)))))) @@ -30,9 +30,6 @@ (defn bar [x] (apply str (repeat x "#"))) -(defn format-cell [col s] - ((:format col) s)) - (defn header [th cols] (for [col cols :when (:when col)] @@ -44,23 +41,22 @@ :when (:when col)] (td col row)))) -(defn- col-data [col rows] - (map #(get % (:name col)) rows)) - - -(defn- format-rows [cols rows] +(defn format-rows [cols rows] (for [row rows] - (into {} - (for [col cols :let [name (:name col)]] - [name (format-cell col (row name))])))) + (reduce + (fn [m {:keys [name] :as col}] + (assoc m name + ((:format col) (get row name)))) + {} + cols))) -(defn- column2 [col & [data]] - {:width (width col data)}) +(defn- col-data [col rows] + (map #(get % (:name col)) rows)) -(defn- columns2 [cols rows] +(defn columns-with-widths [cols rows] (for [col cols] (merge col - (column2 col (col-data col rows))))) + {:width (width col (col-data col rows))}))) ;; table formats (def csv 'doric.csv) @@ -77,28 +73,38 @@ (map-indexed (fn [i x] [i x]) row))) (map? example) rows))) +(defn conform + "Given an optional colspec and a sequence of maps, returns tuple + of [conformed-columns formatted-rows]" + ([rows] + (conform nil rows)) + ([cols rows] + (let [rows (mapify rows) + cols (map columnize (or cols + (keys (first rows)))) + rows (format-rows cols rows) + cols (columns-with-widths cols rows)] + [cols rows]))) + (defn table* {:arglists '[[rows] [opts rows] [cols rows] [opts cols rows]]} [& args] - (let [rows (mapify (last 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)]) - cols (or cols (keys (first rows))) format (or (:format opts) org) _ (require format) th (ns-resolve format 'th) td (ns-resolve format 'td) render (ns-resolve format 'render) - cols (map columnize cols) - rows (format-rows cols rows) - cols (columns2 cols rows)] + [cols rows] (conform cols rows)] (render (cons (header th cols) (body td cols rows))))) (defn table diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index ca0a72e..6347ad4 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -19,13 +19,11 @@ (is (not (re-find #"bar" (table [{:name :foo :when false}] [{:foo :bar}]))))) (deftest test-width - (is (= 5 (width {:width 5}))) - (is (= 5 (width {:width 5 :name :foobar}))) + (is (= 5 (width {:width 5} ["no matter what"]))) + (is (= 9 (width {:title "TitleCase"} ["hi"]))) + (is (= 8 (width {:title "Title"} ["whatever" "is" "largest"]))) (is (= 7 (width {:name :foobar} ["foobar2"])))) -(deftest test-format-cell - (is (= 2 (format-cell {:format inc} 1)))) - (deftest test-th (is (= "Title " (th {:title "Title" :width 7 :title-align :left}))) (is (= " Title " (th {:title "Title" :width 7 :title-align :center}))) From 290b2e72b1e3a9261562cb34fc70139fa0e550fd Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 17:06:33 +0000 Subject: [PATCH 04/14] Major refactor: switch to protocol-based render --- README.md | 22 ++++------- src/doric/core.clj | 69 +++++++++++++++++----------------- src/doric/csv.clj | 16 ++++---- src/doric/formatting.clj | 16 +++----- src/doric/html.clj | 19 +++++----- src/doric/org.clj | 19 +++++----- src/doric/protocols.clj | 30 +++++++++++++++ src/doric/raw.clj | 15 ++++---- test/doric/test/core.clj | 17 ++------- test/doric/test/formatting.clj | 9 +++++ 10 files changed, 127 insertions(+), 105 deletions(-) create mode 100644 src/doric/protocols.clj diff --git a/README.md b/README.md index 497e339..826b7b1 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -59,19 +51,19 @@ 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 s and s here ``` @@ -79,7 +71,7 @@ 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 ``` @@ -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 ### diff --git a/src/doric/core.clj b/src/doric/core.clj index 4dc5894..7b53541 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -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 @@ -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))) @@ -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)] @@ -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] @@ -84,14 +80,10 @@ (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] @@ -99,13 +91,19 @@ [(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] @@ -113,4 +111,5 @@ [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))) diff --git a/src/doric/csv.clj b/src/doric/csv.clj index 0aadcdb..f709991 100644 --- a/src/doric/csv.clj +++ b/src/doric/csv.clj @@ -1,10 +1,8 @@ (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) "\"" "\"\"")] @@ -12,7 +10,11 @@ (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})) diff --git a/src/doric/formatting.clj b/src/doric/formatting.clj index 5f2c5a1..0997bf5 100644 --- a/src/doric/formatting.clj +++ b/src/doric/formatting.clj @@ -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))) diff --git a/src/doric/html.clj b/src/doric/html.clj index f36ed7b..5092153 100644 --- a/src/doric/html.clj +++ b/src/doric/html.clj @@ -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 ["" - (str "" (str/join (for [c (first table)] + (str "" (str/join (for [c (first rows)] (str ""))) "")] - (for [tr (rest table)] + (for [tr (rest rows)] (str "" (str/join (for [c tr] (str ""))) "")) ["
" c "
" c "
"])) + +(def renderer (tabular-renderer {:th unaligned-th + :td unaligned-td + :assemble assemble})) diff --git a/src/doric/org.clj b/src/doric/org.clj index be99992..dfe381d 100644 --- a/src/doric/org.clj +++ b/src/doric/org.clj @@ -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})) diff --git a/src/doric/protocols.clj b/src/doric/protocols.clj new file mode 100644 index 0000000..ef01b47 --- /dev/null +++ b/src/doric/protocols.clj @@ -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)) diff --git a/src/doric/raw.clj b/src/doric/raw.clj index aac513b..b7d303e 100644 --- a/src/doric/raw.clj +++ b/src/doric/raw.clj @@ -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})) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 6347ad4..698e7f9 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -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"})))) @@ -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 "|---+---|")))) @@ -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}])] diff --git a/test/doric/test/formatting.clj b/test/doric/test/formatting.clj index 9943519..6f2a817 100644 --- a/test/doric/test/formatting.clj +++ b/test/doric/test/formatting.clj @@ -19,3 +19,12 @@ (is (= " . " (align-cell {:width 4} "." :center))) (is (= " ." (align-cell {:width 4} "." :right)))) +(deftest test-aligned-th + (is (= "Title " (aligned-th {:width 7 :title-align :left} "Title"))) + (is (= " Title " (aligned-th {:width 7 :title-align :center} "Title"))) + (is (= " Title" (aligned-th {:width 7 :title-align :right} "Title")))) + +(deftest test-aligned-td + (is (= ". " (aligned-td {:width 3 :align :left} "."))) + (is (= " . " (aligned-td {:width 3 :align :center} "."))) + (is (= " ." (aligned-td {:width 3 :align :right} ".")))) From bc9d10af772f06b83be30d76f3c1cbfa6992526c Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 19:15:43 +0000 Subject: [PATCH 05/14] Add basic json renderers --- project.clj | 1 + src/doric/core.clj | 7 +++++-- src/doric/json.clj | 16 ++++++++++++++++ test/doric/test/core.clj | 17 ++++++++++++++--- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/doric/json.clj diff --git a/project.clj b/project.clj index dce8fb9..f848503 100644 --- a/project.clj +++ b/project.clj @@ -3,6 +3,7 @@ :url "https://github.com/joegallo/doric" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} + :dependencies [[cheshire "5.7.0"]] :profiles {:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]} :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} diff --git a/src/doric/core.clj b/src/doric/core.clj index 7b53541..31f8093 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -6,7 +6,8 @@ [doric.org] [doric.raw] [doric.html] - [doric.csv])) + [doric.csv] + [doric.json])) (defn column-defaults [col] (merge col @@ -58,7 +59,9 @@ (def renderers {:csv doric.csv/renderer :html doric.html/renderer :org doric.org/renderer - :raw doric.raw/renderer}) + :raw doric.raw/renderer + :json (doric.json/make-renderer) + :json-pretty (doric.json/make-renderer true)}) (defn mapify [rows] (let [example (first rows)] diff --git a/src/doric/json.clj b/src/doric/json.clj new file mode 100644 index 0000000..4505da2 --- /dev/null +++ b/src/doric/json.clj @@ -0,0 +1,16 @@ +(ns doric.json + (:require [cheshire.core :as json] + [doric.protocols :as proto])) + +(defrecord JSONRenderer [cheshire-opts] + proto/Render + (-render-lazy [_ cols data] + (map #(json/generate-string % cheshire-opts) data)) + (-render [_ cols data] + (json/generate-string data cheshire-opts))) + +(defn make-renderer + ([] + (make-renderer false)) + ([pretty] + (->JSONRenderer {:pretty pretty}))) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 698e7f9..936c4d0 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -1,8 +1,9 @@ (ns doric.test.core (:refer-clojure :exclude [format name when]) - (:use [doric.core] - [clojure.test] - [doric.org :only [assemble]])) + (:require [clojure.test :refer :all] + [cheshire.core :as json] + [doric.core :refer :all] + [doric.org :refer [assemble]])) (deftest test-column-defaults (is (= "foo" (:title (columnize {:title "foo"})))) @@ -64,6 +65,16 @@ [{:1 3 :2 4}])] (is (= 0 @calls)))))) +(deftest test-json + (let [data [{:a 1 :b "2"} {:a 2 :b "42"}] + out (table {:format :json} data)] + (is (= data (json/parse-string out true))))) + +(deftest test-json-table* + (let [data [{:a 1 :b "2"} {:a 2 :b "42"}] + out (table* {:format :json} data)] + (is (= data (map #(json/parse-string % true) out))))) + (deftest test-empty-table (let [empty-table "|--|\n| |\n|--|\n|--|"] (is (= empty-table (table []))) From f2ee1586b0fd67b93d5244cb97f808f81f95518a Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 19:43:48 +0000 Subject: [PATCH 06/14] Escape for table-based layouts --- src/doric/core.clj | 13 +++++++------ src/doric/formatting.clj | 6 ++++++ src/doric/protocols.clj | 7 ++++--- test/doric/test/core.clj | 17 ++++++++++++----- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/doric/core.clj b/src/doric/core.clj index 31f8093..95c0cee 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -1,6 +1,5 @@ (ns doric.core - (:refer-clojure :exclude [format name join split]) - (:require [doric.formatting :refer [titleize]] + (:require [doric.formatting :refer [titleize escape]] [doric.protocols :refer [render render-lazy]] [clojure.string :as str] [doric.org] @@ -14,6 +13,8 @@ {:align (keyword (get col :align :left)) :format (or (:format col) identity) + :escape (or (:escape col) + escape) :title (or (:title col) (titleize (:name col))) :title-align (keyword (or (:title-align col) @@ -28,10 +29,10 @@ (def columnize (comp column-defaults column-map)) -(defn width [col data] - (or (:width col) - (apply max (map count (cons (:title col) - (map str data)))))) +(defn width [{:keys [title escape width]} data] + (or width + (apply max (map count (cons title + (map (comp escape str) data)))))) (defn bar [x] (apply str (repeat x "#"))) diff --git a/src/doric/formatting.clj b/src/doric/formatting.clj index 0997bf5..16c9bbe 100644 --- a/src/doric/formatting.clj +++ b/src/doric/formatting.clj @@ -17,6 +17,12 @@ n)) "-" " "))) +(defn escape [s] + (if (string? s) + (str/escape s {\newline "\\n" + \tab "\\t"}) + s)) + (defn align-cell [col s align] (let [width (:width col) s (str s) diff --git a/src/doric/protocols.clj b/src/doric/protocols.clj index ef01b47..7cb63af 100644 --- a/src/doric/protocols.clj +++ b/src/doric/protocols.clj @@ -1,5 +1,6 @@ (ns doric.protocols - (:require [clojure.string :as str])) + (:require [clojure.string :as str] + [doric.formatting :refer [escape]])) (defprotocol Render (-render-lazy [_ cols data]) @@ -14,11 +15,11 @@ (assemble (cons (for [col cols :when (:when col)] - (th col (:title col))) + (th col (escape (:title col)))) (for [row data] (for [col cols :when (:when col)] - (td col (get row (:name col))))))))) + (td col (escape (get row (:name col)))))))))) (defn render-lazy [renderer cols data] (-render-lazy renderer cols data)) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 936c4d0..a79008e 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -21,15 +21,13 @@ (deftest test-width (is (= 5 (width {:width 5} ["no matter what"]))) - (is (= 9 (width {:title "TitleCase"} ["hi"]))) - (is (= 8 (width {:title "Title"} ["whatever" "is" "largest"]))) - (is (= 7 (width {:name :foobar} ["foobar2"])))) + (is (= 9 (width {:title "TitleCase" :escape identity} ["hi"]))) + (is (= 8 (width {:title "Title" :escape identity} ["whatever" "is" "largest"]))) + (is (= 7 (width {:name :foobar :escape identity} ["foobar2"])))) ;; TODO (deftest test-header) -;; TODO (deftest test-body) - (deftest test-assemble (let [rendered (assemble [["1" "2"]["3" "4"]])] (is (.contains rendered "| 1 | 2 |")) @@ -43,6 +41,15 @@ (is (.contains rendered "| 3 | 4 |")) (is (.contains rendered "|---+---|")))) +(deftest test-escaping + (let [rendered (table* [:a :b] [{:a "foo\nbar" :b "what\tever"}])] + (is (= rendered + ["|----------+------------|" + "| A | B |" + "|----------+------------|" + "| foo\\nbar | what\\tever |" ;; lines up when printed + "|----------+------------|"])))) + (deftest test-table*-laziness (let [calls (atom 0) inc #(do (swap! calls inc) %)] From 122a6cec0e79a0d32b2b96b49c52861bc27e4200 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 19:53:29 +0000 Subject: [PATCH 07/14] Allow :when to work with json --- src/doric/core.clj | 5 +++-- src/doric/protocols.clj | 6 ++---- test/doric/test/core.clj | 9 ++++++++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/doric/core.clj b/src/doric/core.clj index 95c0cee..d99bd49 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -80,8 +80,9 @@ (conform nil rows)) ([cols rows] (let [rows (mapify rows) - cols (map columnize (or cols - (keys (first rows)))) + cols (filter :when + (map columnize (or cols + (keys (first rows))))) rows (format-rows cols rows) cols (columns-with-widths cols rows)] {:cols cols, :rows rows}))) diff --git a/src/doric/protocols.clj b/src/doric/protocols.clj index 7cb63af..960a090 100644 --- a/src/doric/protocols.clj +++ b/src/doric/protocols.clj @@ -13,12 +13,10 @@ (-render-lazy this cols data))) (-render-lazy [_ cols data] (assemble - (cons (for [col cols - :when (:when col)] + (cons (for [col cols] (th col (escape (:title col)))) (for [row data] - (for [col cols - :when (:when col)] + (for [col cols] (td col (escape (get row (:name col)))))))))) (defn render-lazy [renderer cols data] diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index a79008e..3ae3405 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -1,5 +1,4 @@ (ns doric.test.core - (:refer-clojure :exclude [format name when]) (:require [clojure.test :refer :all] [cheshire.core :as json] [doric.core :refer :all] @@ -77,6 +76,14 @@ out (table {:format :json} data)] (is (= data (json/parse-string out true))))) +(deftest test-json-format-and-when + (let [data [{:a "1" :b 2} {:a "2" :b 42}] + out (table {:format :json} + [{:name :a :when false} {:name :b :format inc}] + data)] + (is (= [{:b 3} {:b 43}] + (json/parse-string out true))))) + (deftest test-json-table* (let [data [{:a 1 :b "2"} {:a 2 :b "42"}] out (table* {:format :json} data)] From 3361d2ffc313e20e15c7a5badd08db6b378b8402 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 20:31:38 +0000 Subject: [PATCH 08/14] Extract tabular ns --- src/doric/core.clj | 16 +--------- src/doric/csv.clj | 5 ++-- src/doric/formatting.clj | 19 ------------ src/doric/html.clj | 5 ++-- src/doric/org.clj | 5 ++-- src/doric/protocols.clj | 20 +------------ src/doric/raw.clj | 5 ++-- src/doric/tabular.clj | 54 ++++++++++++++++++++++++++++++++++ test/doric/test/core.clj | 10 ++----- test/doric/test/formatting.clj | 10 ------- test/doric/test/tabular.clj | 19 ++++++++++++ 11 files changed, 89 insertions(+), 79 deletions(-) create mode 100644 src/doric/tabular.clj create mode 100644 test/doric/test/tabular.clj diff --git a/src/doric/core.clj b/src/doric/core.clj index d99bd49..4ccb099 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -29,11 +29,6 @@ (def columnize (comp column-defaults column-map)) -(defn width [{:keys [title escape width]} data] - (or width - (apply max (map count (cons title - (map (comp escape str) data)))))) - (defn bar [x] (apply str (repeat x "#"))) @@ -48,14 +43,6 @@ {} cols))) -(defn- col-data [col rows] - (map #(get % (:name col)) rows)) - -(defn columns-with-widths [cols rows] - (for [col cols] - (merge col - {:width (width col (col-data col rows))}))) - ;; table formats (def renderers {:csv doric.csv/renderer :html doric.html/renderer @@ -83,8 +70,7 @@ cols (filter :when (map columnize (or cols (keys (first rows))))) - rows (format-rows cols rows) - cols (columns-with-widths cols rows)] + rows (format-rows cols rows)] {:cols cols, :rows rows}))) (defn -parse-args diff --git a/src/doric/csv.clj b/src/doric/csv.clj index f709991..afbecb3 100644 --- a/src/doric/csv.clj +++ b/src/doric/csv.clj @@ -1,7 +1,8 @@ (ns doric.csv (:require [clojure.string :as str] - [doric.protocols :refer [tabular-renderer]] - [doric.formatting :refer [unaligned-th unaligned-td]])) + [doric.tabular :refer [tabular-renderer + unaligned-td + unaligned-th]])) (defn escape [s] diff --git a/src/doric/formatting.clj b/src/doric/formatting.clj index 16c9bbe..f816f0a 100644 --- a/src/doric/formatting.clj +++ b/src/doric/formatting.clj @@ -39,22 +39,3 @@ :center (str (pad (Math/ceil half-padding)) s (pad (Math/floor half-padding)))))) - -;; table format helpers - -;; unalighed-th and td are useful for whitespace immune formats, like -;; csv and html -(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 cell-data] - (align-cell col - cell-data - (:title-align col))) - -(defn aligned-td [col cell-data] - (align-cell col - cell-data - (:align col))) diff --git a/src/doric/html.clj b/src/doric/html.clj index 5092153..de5b1fb 100644 --- a/src/doric/html.clj +++ b/src/doric/html.clj @@ -1,7 +1,8 @@ (ns doric.html (:require [clojure.string :as str] - [doric.protocols :refer [tabular-renderer]] - [doric.formatting :refer [unaligned-th unaligned-td]])) + [doric.tabular :refer [tabular-renderer + unaligned-td + unaligned-th]])) (defn assemble [rows] (concat ["" diff --git a/src/doric/org.clj b/src/doric/org.clj index dfe381d..9b89355 100644 --- a/src/doric/org.clj +++ b/src/doric/org.clj @@ -1,7 +1,8 @@ (ns doric.org (:require [clojure.string :as str] - [doric.protocols :refer [tabular-renderer]] - [doric.formatting :refer [aligned-th aligned-td]])) + [doric.tabular :refer [tabular-renderer + aligned-th + aligned-td]])) (defn assemble [rows] (let [spacer (str "|-" diff --git a/src/doric/protocols.clj b/src/doric/protocols.clj index 960a090..7abf88a 100644 --- a/src/doric/protocols.clj +++ b/src/doric/protocols.clj @@ -1,29 +1,11 @@ -(ns doric.protocols - (:require [clojure.string :as str] - [doric.formatting :refer [escape]])) +(ns doric.protocols) (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] - (th col (escape (:title col)))) - (for [row data] - (for [col cols] - (td col (escape (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)) diff --git a/src/doric/raw.clj b/src/doric/raw.clj index b7d303e..15ba91a 100644 --- a/src/doric/raw.clj +++ b/src/doric/raw.clj @@ -1,7 +1,8 @@ (ns doric.raw (:require [clojure.string :as str] - [doric.protocols :refer [tabular-renderer]] - [doric.formatting :refer [aligned-th aligned-td]])) + [doric.tabular :refer [tabular-renderer + aligned-th + aligned-td]])) (defn assemble [rows] (cons (str/join " " (first rows)) diff --git a/src/doric/tabular.clj b/src/doric/tabular.clj new file mode 100644 index 0000000..5a9b502 --- /dev/null +++ b/src/doric/tabular.clj @@ -0,0 +1,54 @@ +(ns doric.tabular + (:require [clojure.string :as str] + [doric.protocols :refer :all] + [doric.formatting :refer [escape align-cell]])) + +(defn- col-data [col rows] + (map #(get % (:name col)) rows)) + +(defn width [{:keys [title escape width]} data] + (or width + (apply max (map count (cons title + (map (comp escape str) data)))))) + +(defn columns-with-widths [cols rows] + (for [col cols] + (merge col + {:width (width col (col-data col rows))}))) + + +(defrecord TabularRender [th td assemble] + Render + (-render [this cols data] + (str/join "\n" + (-render-lazy this cols data))) + (-render-lazy [_ cols data] + (let [cols (columns-with-widths cols data)] + (assemble + (cons (for [col cols] + (th col (escape (:title col)))) + (for [row data] + (for [col cols] + (td col (escape (get row (:name col))))))))))) + +(defn tabular-renderer [{:keys [td th assemble] :as fns}] + (map->TabularRender fns)) + +;; table format helpers + +;; unalighed-th and td are useful for whitespace immune formats, like +;; csv and html +(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 cell-data] + (align-cell col + cell-data + (:title-align col))) + +(defn aligned-td [col cell-data] + (align-cell col + cell-data + (:align col))) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 3ae3405..48b239b 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -2,7 +2,8 @@ (:require [clojure.test :refer :all] [cheshire.core :as json] [doric.core :refer :all] - [doric.org :refer [assemble]])) + [doric.org :refer [assemble]] + [doric.tabular :refer [width]])) (deftest test-column-defaults (is (= "foo" (:title (columnize {:title "foo"})))) @@ -18,13 +19,6 @@ (is (not (re-find #"Foo" (table [{:name :foo :when false}] [{:foo :bar}])))) (is (not (re-find #"bar" (table [{:name :foo :when false}] [{:foo :bar}]))))) -(deftest test-width - (is (= 5 (width {:width 5} ["no matter what"]))) - (is (= 9 (width {:title "TitleCase" :escape identity} ["hi"]))) - (is (= 8 (width {:title "Title" :escape identity} ["whatever" "is" "largest"]))) - (is (= 7 (width {:name :foobar :escape identity} ["foobar2"])))) - - ;; TODO (deftest test-header) (deftest test-assemble diff --git a/test/doric/test/formatting.clj b/test/doric/test/formatting.clj index 6f2a817..a225b6a 100644 --- a/test/doric/test/formatting.clj +++ b/test/doric/test/formatting.clj @@ -18,13 +18,3 @@ (is (= ". " (align-cell {:width 4} "." :left))) (is (= " . " (align-cell {:width 4} "." :center))) (is (= " ." (align-cell {:width 4} "." :right)))) - -(deftest test-aligned-th - (is (= "Title " (aligned-th {:width 7 :title-align :left} "Title"))) - (is (= " Title " (aligned-th {:width 7 :title-align :center} "Title"))) - (is (= " Title" (aligned-th {:width 7 :title-align :right} "Title")))) - -(deftest test-aligned-td - (is (= ". " (aligned-td {:width 3 :align :left} "."))) - (is (= " . " (aligned-td {:width 3 :align :center} "."))) - (is (= " ." (aligned-td {:width 3 :align :right} ".")))) diff --git a/test/doric/test/tabular.clj b/test/doric/test/tabular.clj new file mode 100644 index 0000000..2b5b979 --- /dev/null +++ b/test/doric/test/tabular.clj @@ -0,0 +1,19 @@ +(ns doric.test.tabular + (:require [doric.tabular :refer :all] + [clojure.test :refer :all])) + +(deftest test-aligned-th + (is (= "Title " (aligned-th {:width 7 :title-align :left} "Title"))) + (is (= " Title " (aligned-th {:width 7 :title-align :center} "Title"))) + (is (= " Title" (aligned-th {:width 7 :title-align :right} "Title")))) + +(deftest test-aligned-td + (is (= ". " (aligned-td {:width 3 :align :left} "."))) + (is (= " . " (aligned-td {:width 3 :align :center} "."))) + (is (= " ." (aligned-td {:width 3 :align :right} ".")))) + +(deftest test-width + (is (= 5 (width {:width 5} ["no matter what"]))) + (is (= 9 (width {:title "TitleCase" :escape identity} ["hi"]))) + (is (= 8 (width {:title "Title" :escape identity} ["whatever" "is" "largest"]))) + (is (= 7 (width {:name :foobar :escape identity} ["foobar2"])))) From 52f59e3b9bba5e82a4bcc8ffc883da62795eda36 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Fri, 17 Mar 2017 20:46:21 +0000 Subject: [PATCH 09/14] Escape in same loop --- src/doric/tabular.clj | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/doric/tabular.clj b/src/doric/tabular.clj index 5a9b502..baa75b1 100644 --- a/src/doric/tabular.clj +++ b/src/doric/tabular.clj @@ -3,20 +3,21 @@ [doric.protocols :refer :all] [doric.formatting :refer [escape align-cell]])) -(defn- col-data [col rows] - (map #(get % (:name col)) rows)) +(defn- col-data [{:keys [name]} rows] + (map (comp escape str name) rows)) -(defn width [{:keys [title escape width]} data] +(defn width [{:keys [title escape width]} cells] (or width - (apply max (map count (cons title - (map (comp escape str) data)))))) + (->> cells + (cons (escape title)) + (map count) + (apply max)))) (defn columns-with-widths [cols rows] (for [col cols] (merge col {:width (width col (col-data col rows))}))) - (defrecord TabularRender [th td assemble] Render (-render [this cols data] From 32195f7dc9566d4d284e496a247f20f51aa06244 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Mon, 27 Mar 2017 13:09:14 +0000 Subject: [PATCH 10/14] Make escaping per renderer --- src/doric/core.clj | 4 +-- src/doric/csv.clj | 3 ++- src/doric/html.clj | 8 +++++- src/doric/org.clj | 4 ++- src/doric/tabular.clj | 51 ++++++++++++++++++++----------------- test/doric/test/core.clj | 13 ++++++++-- test/doric/test/tabular.clj | 11 ++++---- 7 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/doric/core.clj b/src/doric/core.clj index 4ccb099..e5c5ea7 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -1,5 +1,5 @@ (ns doric.core - (:require [doric.formatting :refer [titleize escape]] + (:require [doric.formatting :refer [titleize]] [doric.protocols :refer [render render-lazy]] [clojure.string :as str] [doric.org] @@ -13,8 +13,6 @@ {:align (keyword (get col :align :left)) :format (or (:format col) identity) - :escape (or (:escape col) - escape) :title (or (:title col) (titleize (:name col))) :title-align (keyword (or (:title-align col) diff --git a/src/doric/csv.clj b/src/doric/csv.clj index afbecb3..d2693d9 100644 --- a/src/doric/csv.clj +++ b/src/doric/csv.clj @@ -18,4 +18,5 @@ (def renderer (tabular-renderer {:th (comp escape unaligned-th) :td (comp escape unaligned-td) - :assemble assemble})) + :assemble assemble + :escape escape})) diff --git a/src/doric/html.clj b/src/doric/html.clj index de5b1fb..ca84e93 100644 --- a/src/doric/html.clj +++ b/src/doric/html.clj @@ -3,6 +3,11 @@ [doric.tabular :refer [tabular-renderer unaligned-td unaligned-th]])) +(defn escape [^String s] + (-> s + (str/replace "&" "&") + (str/replace "<" "<") + (str/replace ">" ">"))) (defn assemble [rows] (concat ["
" @@ -15,4 +20,5 @@ (def renderer (tabular-renderer {:th unaligned-th :td unaligned-td - :assemble assemble})) + :assemble assemble + :escape escape})) diff --git a/src/doric/org.clj b/src/doric/org.clj index 9b89355..2005247 100644 --- a/src/doric/org.clj +++ b/src/doric/org.clj @@ -1,5 +1,6 @@ (ns doric.org (:require [clojure.string :as str] + [doric.formatting :refer [escape]] [doric.tabular :refer [tabular-renderer aligned-th aligned-td]])) @@ -19,4 +20,5 @@ (def renderer (tabular-renderer {:th aligned-th :td aligned-td - :assemble assemble})) + :assemble assemble + :escape escape})) diff --git a/src/doric/tabular.clj b/src/doric/tabular.clj index baa75b1..cf91326 100644 --- a/src/doric/tabular.clj +++ b/src/doric/tabular.clj @@ -1,30 +1,32 @@ (ns doric.tabular (:require [clojure.string :as str] [doric.protocols :refer :all] - [doric.formatting :refer [escape align-cell]])) - -(defn- col-data [{:keys [name]} rows] - (map (comp escape str name) rows)) - -(defn width [{:keys [title escape width]} cells] - (or width - (->> cells - (cons (escape title)) - (map count) - (apply max)))) - -(defn columns-with-widths [cols rows] - (for [col cols] - (merge col - {:width (width col (col-data col rows))}))) - -(defrecord TabularRender [th td assemble] + [doric.formatting :refer [align-cell]])) + + +(defn calculate-width + ([col rows] + (calculate-width col rows identity)) + ([{:keys [title name]} rows escape] + (->> rows + (map (comp escape str name)) + (cons (escape title)) + (map count) + (apply max)))) + +(defn columns-with-widths [escape cols rows] + (for [{:keys [width] :as col} cols] + (assoc col + :width (or width + (calculate-width col rows escape))))) + +(defrecord TabularRender [th td assemble escape] Render (-render [this cols data] (str/join "\n" (-render-lazy this cols data))) (-render-lazy [_ cols data] - (let [cols (columns-with-widths cols data)] + (let [cols (columns-with-widths escape cols data)] (assemble (cons (for [col cols] (th col (escape (:title col)))) @@ -32,9 +34,6 @@ (for [col cols] (td col (escape (get row (:name col))))))))))) -(defn tabular-renderer [{:keys [td th assemble] :as fns}] - (map->TabularRender fns)) - ;; table format helpers ;; unalighed-th and td are useful for whitespace immune formats, like @@ -52,4 +51,10 @@ (defn aligned-td [col cell-data] (align-cell col cell-data - (:align col))) + (:align col))) + +(defn tabular-renderer [{:keys [td th assemble escape]}] + (map->TabularRender {:td (or td unaligned-td) + :th (or th unaligned-th) + :escape (or escape identity) + :assemble assemble})) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 48b239b..01b048e 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -2,8 +2,7 @@ (:require [clojure.test :refer :all] [cheshire.core :as json] [doric.core :refer :all] - [doric.org :refer [assemble]] - [doric.tabular :refer [width]])) + [doric.org :refer [assemble]])) (deftest test-column-defaults (is (= "foo" (:title (columnize {:title "foo"})))) @@ -43,6 +42,16 @@ "| foo\\nbar | what\\tever |" ;; lines up when printed "|----------+------------|"])))) +(deftest test-escaping-html + (let [rendered (table* {:format :html} + [:a :b] + [{:a "foo < bar" :b "what & ever"}])] + (is (= rendered + ["
" + "" + "" + "
AB
foo < barwhat & ever
"])))) + (deftest test-table*-laziness (let [calls (atom 0) inc #(do (swap! calls inc) %)] diff --git a/test/doric/test/tabular.clj b/test/doric/test/tabular.clj index 2b5b979..ee5278d 100644 --- a/test/doric/test/tabular.clj +++ b/test/doric/test/tabular.clj @@ -12,8 +12,9 @@ (is (= " . " (aligned-td {:width 3 :align :center} "."))) (is (= " ." (aligned-td {:width 3 :align :right} ".")))) -(deftest test-width - (is (= 5 (width {:width 5} ["no matter what"]))) - (is (= 9 (width {:title "TitleCase" :escape identity} ["hi"]))) - (is (= 8 (width {:title "Title" :escape identity} ["whatever" "is" "largest"]))) - (is (= 7 (width {:name :foobar :escape identity} ["foobar2"])))) +(deftest test-calculate-width + (is (= 9 (calculate-width {:title "TitleCase" :name :a} ["hi"]))) + (is (= 8 (calculate-width {:title "Title" :name :a} [{:a "whatever"} + {:a "is"} + {:a "largest"}]))) + (is (= 7 (calculate-width {:name :foobar} [{:foobar "foobar2"}])))) From e85280476d89571a03f8ac816fac6cba1eb834fc Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Tue, 28 Mar 2017 13:37:41 +0000 Subject: [PATCH 11/14] Add unicode renderer --- src/doric/core.clj | 4 +++- src/doric/unicode.clj | 25 +++++++++++++++++++++++++ test/doric/test/core.clj | 13 +++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/doric/unicode.clj diff --git a/src/doric/core.clj b/src/doric/core.clj index e5c5ea7..c79f46b 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -6,7 +6,8 @@ [doric.raw] [doric.html] [doric.csv] - [doric.json])) + [doric.json] + [doric.unicode])) (defn column-defaults [col] (merge col @@ -46,6 +47,7 @@ :html doric.html/renderer :org doric.org/renderer :raw doric.raw/renderer + :unicode doric.unicode/renderer :json (doric.json/make-renderer) :json-pretty (doric.json/make-renderer true)}) diff --git a/src/doric/unicode.clj b/src/doric/unicode.clj new file mode 100644 index 0000000..5537728 --- /dev/null +++ b/src/doric/unicode.clj @@ -0,0 +1,25 @@ +(ns doric.unicode + (:require [clojure.string :as str] + [doric.formatting :refer [escape]] + [doric.tabular :refer [tabular-renderer + aligned-th + aligned-td]])) + +(defn assemble [rows] + (let [spacer (fn [l c r] + (str l + (str/join c + (map #(apply str (repeat (.length %) "─")) + (first rows))) + r))] + (concat [(spacer "┌─" "─┬─" "─┐") + (str "│ " (str/join " │ " (first rows)) " │") + (spacer "├─" "─┼─" "─┤")] + (for [tr (rest rows)] + (str "│ " (str/join " │ " tr) " │")) + [(spacer "└─" "─┴─" "─┘")]))) + +(def renderer (tabular-renderer {:th aligned-th + :td aligned-td + :assemble assemble + :escape escape})) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 01b048e..57b81bb 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -33,6 +33,19 @@ (is (.contains rendered "| 3 | 4 |")) (is (.contains rendered "|---+---|")))) +(deftest test-unicode-table + (let [rendered (table* {:format :unicode} + [{:name :foo} {:name :bar :width 9}] + [{:foo "what" :bar 4} + {:foo "who" :bar 87}])] + (is (= rendered + ["┌──────┬───────────┐" + "│ Foo │ Bar │" + "├──────┼───────────┤" + "│ what │ 4 │" + "│ who │ 87 │" + "└──────┴───────────┘"])))) + (deftest test-escaping (let [rendered (table* [:a :b] [{:a "foo\nbar" :b "what\tever"}])] (is (= rendered From 6458c7e870c1c40b31549aa8a24e11a8e6064607 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Tue, 23 May 2017 09:25:57 -0400 Subject: [PATCH 12/14] Fix typo --- src/doric/core.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/doric/core.clj b/src/doric/core.clj index c79f46b..c350ab4 100644 --- a/src/doric/core.clj +++ b/src/doric/core.clj @@ -100,7 +100,7 @@ {:arglists '[[rows] [opts rows] [cols rows] - [otps cols rows]]} + [opts cols rows]]} [& args] (let [{:keys [renderer cols rows]} (-parse-args args)] (render renderer cols rows))) From 8c479956364a40ac48e36976087d97f844018a31 Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Tue, 23 May 2017 09:26:41 -0400 Subject: [PATCH 13/14] Allow more data types as title --- src/doric/formatting.clj | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/doric/formatting.clj b/src/doric/formatting.clj index f816f0a..9bc9aff 100644 --- a/src/doric/formatting.clj +++ b/src/doric/formatting.clj @@ -1,21 +1,24 @@ (ns doric.formatting (:require [clojure.string :as str])) +(defn string ^String [elem] + (str (cond + (keyword? elem) (name elem) + (symbol? elem) (name elem) + :else elem))) + (defn- title-case-word [w] (if (zero? (count w)) w (str (Character/toTitleCase (first w)) (subs w 1)))) -(defn title-case [s] +(defn title-case [^String s] (str/join " " (map title-case-word (str/split s #"\s")))) (defn titleize [n] (title-case - (.replaceAll ^String (name (if (number? n) - (str n) - n)) - "-" " "))) + (.replaceAll (string n) "-" " "))) (defn escape [s] (if (string? s) From 088faecc621a55243af8bee6b9b8889b954dde5a Mon Sep 17 00:00:00 2001 From: Joshua Davey Date: Tue, 23 May 2017 09:28:06 -0400 Subject: [PATCH 14/14] Fix vectors bug --- src/doric/tabular.clj | 3 +-- test/doric/test/core.clj | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/doric/tabular.clj b/src/doric/tabular.clj index cf91326..fdd3f7c 100644 --- a/src/doric/tabular.clj +++ b/src/doric/tabular.clj @@ -3,13 +3,12 @@ [doric.protocols :refer :all] [doric.formatting :refer [align-cell]])) - (defn calculate-width ([col rows] (calculate-width col rows identity)) ([{:keys [title name]} rows escape] (->> rows - (map (comp escape str name)) + (map (comp escape str #(get % name))) (cons (escape title)) (map count) (apply max)))) diff --git a/test/doric/test/core.clj b/test/doric/test/core.clj index 57b81bb..240d63f 100644 --- a/test/doric/test/core.clj +++ b/test/doric/test/core.clj @@ -28,10 +28,24 @@ ;; TODO embiggen these tests (deftest test-table - (let [rendered (table [{:1 3 :2 4}])] - (is (.contains rendered "| 1 | 2 |")) - (is (.contains rendered "| 3 | 4 |")) - (is (.contains rendered "|---+---|")))) + (let [rendered (table* [{:1 3 :2 4}])] + (is (= rendered + ["|---+---|" + "| 1 | 2 |" + "|---+---|" + "| 3 | 4 |" + "|---+---|"])))) + +(deftest test-render-vectors + (let [rendered (table* [[1 2] ["3" "4"] [:a :b]])] + (is (= rendered + ["|----+----|" + "| 0 | 1 |" + "|----+----|" + "| 1 | 2 |" + "| 3 | 4 |" + "| :a | :b |" + "|----+----|"])))) (deftest test-unicode-table (let [rendered (table* {:format :unicode}