├── .gitignore ├── project.clj ├── test └── treely │ ├── fixtures │ ├── unicode.txt │ └── ascii.txt │ └── core_test.clj ├── src └── treely │ ├── style.clj │ └── core.clj ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | /target 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ 13 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject treely "0.1.0" 2 | :description "Library for generating tree diagram of nested data structure" 3 | :url "http://github.com/junegunn/treely" 4 | :license {:name "MIT"} 5 | :plugins [[codox "0.8.13"]] 6 | :codox {:defaults {:doc/format :markdown}} 7 | :dependencies [[org.clojure/clojure "1.6.0"]]) 8 | -------------------------------------------------------------------------------- /test/treely/fixtures/unicode.txt: -------------------------------------------------------------------------------- 1 | ├── 1 2 | │   ├── 2 3 | │   └── 3 4 | ├── 4 5 | │   ├── 5 6 | │   └── 6 7 | ├── 7 8 | │   ├── 8 9 | │   │   └── 9 10 | │   │   └── 10 11 | │   ├── 11 12 | │   │   foo 13 | │   │   bar 14 | │   └── 12 15 | ├── 13 16 | │   └── 14 17 | │   foo 18 | │   bar 19 | │   └── 15 20 | │   foo 21 | │   bar 22 | └── 16 23 | ├── 17 24 | │   └── 18 25 | │   └── 26 | │   ├── 20 27 | │   │   └── 21 28 | │   └── 22 29 | │   └── 23 30 | │   └── 24 31 | └── 25 32 | -------------------------------------------------------------------------------- /test/treely/fixtures/ascii.txt: -------------------------------------------------------------------------------- 1 | +-- (1) 2 | | +-- (2) 3 | | `-- (3) 4 | +-- (4) 5 | | +-- (5) 6 | | `-- (6) 7 | +-- (7) 8 | | +-- (8) 9 | | | `-- (9) 10 | | | `-- (10) 11 | | +-- (11 12 | | | foo 13 | | | bar) 14 | | `-- (12) 15 | +-- (13) 16 | | `-- (14 17 | | foo 18 | | bar) 19 | | `-- (15 20 | | foo 21 | | bar) 22 | `-- (16) 23 | +-- (17) 24 | | `-- (18) 25 | | `-- ( 26 | | ) 27 | | +-- (20) 28 | | | `-- (21) 29 | | `-- (22) 30 | | `-- (23) 31 | | `-- (24) 32 | `-- (25) 33 | -------------------------------------------------------------------------------- /src/treely/style.clj: -------------------------------------------------------------------------------- 1 | (ns treely.style 2 | "A predefined set of maps for tree styles. 3 | 4 | Each map defines the following attributes: 5 | 6 | - `:indent` 7 | - `:bar` 8 | - `:branch` 9 | - `:last-branch`") 10 | 11 | (def unicode 12 | "Tree style with Unicode characters (default)" 13 | {:indent " " 14 | :bar "│   " 15 | :branch "├── " 16 | :last-branch "└── "}) 17 | 18 | (def unicode-slim 19 | "Tree style with Unicode characters (slimmer)" 20 | {:indent " " 21 | :bar "│ " 22 | :branch "├ " 23 | :last-branch "└ "}) 24 | 25 | (def ascii 26 | "Tree style with ascii characters" 27 | {:indent " " 28 | :bar "| " 29 | :branch "+-- " 30 | :last-branch "`-- "}) 31 | 32 | -------------------------------------------------------------------------------- /test/treely/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns treely.core-test 2 | (:require [clojure.test :refer :all] 3 | [clojure.string :refer [trimr]] 4 | [treely.core :refer :all] 5 | [treely.style])) 6 | 7 | (def unicode-result (trimr (slurp "test/treely/fixtures/unicode.txt"))) 8 | (def ascii-result (trimr (slurp "test/treely/fixtures/ascii.txt"))) 9 | (def sample-tree 10 | [1 [2 3] 11 | 4 [5] [6] 12 | 7 [8 [9 [10]] "11\nfoo\nbar" 12] 13 | 13 ["14\nfoo\nbar" ["15\nfoo\nbar"]] 14 | 16 [17 [18 ["\n" [20 [21] 22 [23 [24]]]]] 25]]) 15 | 16 | (deftest tree-test 17 | (testing "default" 18 | (is (= unicode-result 19 | (tree sample-tree)))) 20 | (testing "unicode" 21 | (is (= unicode-result 22 | (tree sample-tree treely.style/unicode)))) 23 | (testing "ascii + :formatter" 24 | (is (= ascii-result 25 | (tree sample-tree 26 | (assoc treely.style/ascii 27 | :formatter 28 | #(str "(" % ")"))))))) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Junegunn Choi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # treely 2 | 3 | A simple Clojure library for generating tree diagram of nested data structure. 4 | 5 | ## Leiningen 6 | 7 | [![Clojars Project](http://clojars.org/treely/latest-version.svg)](http://clojars.org/treely) 8 | 9 | ## Usage 10 | 11 | ### `tree` 12 | 13 | user=> (use 'treely.core) 14 | 15 | user=> (println 16 | #_=> (tree [1 [2 3] 17 | #_=> 4 [5 6] 18 | #_=> 7 [8 [9 [10]] 11 12]])) 19 | ├── 1 20 | │   ├── 2 21 | │   └── 3 22 | ├── 4 23 | │   ├── 5 24 | │   └── 6 25 | └── 7 26 | ├── 8 27 | │   └── 9 28 | │   └── 10 29 | ├── 11 30 | └── 12 31 | 32 | user=> (println 33 | #_=> (tree 34 | #_=> '(for [a (range 10) 35 | #_=> b (range 10)] 36 | #_=> (println (* a b))) 37 | #_=> treely.style/ascii)) ; using predefined ascii style 38 | `-- for 39 | +-- a 40 | | +-- range 41 | | `-- 10 42 | +-- b 43 | | +-- range 44 | | `-- 10 45 | `-- println 46 | +-- * 47 | +-- a 48 | `-- b 49 | 50 | ### `lazy-tree` 51 | 52 | user=> (doseq [str (take 5 (lazy-tree [1 [2 3] 53 | #_=> 4 [5 6] 54 | #_=> 7 [8 [9 [10]] 11 12]]))] 55 | #_=> (println str)) 56 | ├── 1 57 | │   ├── 2 58 | │   └── 3 59 | ├── 4 60 | │   ├── 5 61 | 62 | ### `dir-tree` / `lazy-dir-tree` 63 | 64 | The optional map to `dir-tree` or `lazy-dir-tree` can additionally have 65 | `:filter` function which determines whether a java.io.File instance should be 66 | included or not. Circular symlinks are not handled by default. 67 | 68 | ```clojure 69 | ;;; A poor man's tree 70 | (defn list-files [path] 71 | (doseq [line 72 | (letfn [(name [^java.io.File f] (.getName f)) 73 | (dir? [^java.io.File f] (.isDirectory f)) 74 | (blue [s] (str \u001b "[34;1m" s \u001b "[m"))] 75 | (lazy-dir-tree path 76 | {:filter #(not= ".git" (name %)) 77 | :formatter #((if (dir? %) blue identity) (name %))}))] 78 | (println line))) 79 | (list-files ".") 80 | ``` 81 | 82 | ### Options 83 | 84 | #### Keys 85 | 86 | | Key | Type | Default | 87 | | --- | --- | --- | 88 | | `:indent` | String | `"  "` | 89 | | `:bar` | String | `"│  "` | 90 | | `:branch` | String | `"├── "` | 91 | | `:last-branch` | String | `"└── "` | 92 | | `:formatter` | Function | `str` | 93 | 94 | #### Predifined styles (`treely.style`) 95 | 96 | | Key | unicode | unicode-slim | ascii | 97 | | --- | --- | --- | --- | 98 | | `:indent` | `"  "` | `" "` | ``" "`` | 99 | | `:bar` | `"│  "` | `"│ "` | ``"| "`` | 100 | | `:branch` | `"├── "` | `"├ "` | ``"+-- "`` | 101 | | `:last-branch` | `"└── "` | `"└ "` | ``"`-- "`` | 102 | 103 | ## License 104 | 105 | MIT 106 | -------------------------------------------------------------------------------- /src/treely/core.clj: -------------------------------------------------------------------------------- 1 | (ns treely.core 2 | "Functions to generate tree diagram of nested data structure" 3 | (:require [clojure.string :as s] 4 | [clojure.java.io :as io] 5 | [treely.style])) 6 | 7 | ;;; For a cleanly laid-out data structure, it is straightforward to obtain the 8 | ;;; tree representation of it using a simple recursive algorithm. 9 | ;;; 10 | ;;; => (println (tree [1 2 [3 [4 5] 6]])) 11 | ;;; 12 | ;;; ├── 1 13 | ;;; └── 2 14 | ;;; ├── 3 15 | ;;; │   ├── 4 16 | ;;; │   └── 5 17 | ;;; └── 6 18 | ;;; 19 | ;;; However, if there are contiguous sub-lists of the same level, a naive 20 | ;;; method will generate broken tree diagram. 21 | ;;; 22 | ;;; => (println (tree [1 2 [3 [4] [5] 6]])) 23 | ;;; 24 | ;;; ├── 1 25 | ;;; └── 2 26 | ;;; ├── 3 27 | ;;; │   └── 4 28 | ;;; │   └── 5 29 | ;;; └── 6 30 | ;;; 31 | ;;; Such cases are often found when dealing with s-expressions. 32 | ;;; 33 | ;;; => (println 34 | ;;; => (tree 35 | ;;; => '(for [a (range 10) 36 | ;;; => b (range 10)] 37 | ;;; => (println (* a b))))) 38 | ;;; 39 | ;;; └── for 40 | ;;;   ├── a 41 | ;;;   │  ├── range 42 | ;;;   │  └── 10 43 | ;;;   └── b 44 | ;;;     ├── range 45 | ;;;     └── 10 46 | ;;;   └── println 47 | ;;;     ├── * 48 | ;;;     ├── a 49 | ;;;     └── b 50 | ;;; 51 | ;;; So in order not to yield broken tree diagram, we have to recursively merge 52 | ;;; contiguous lists of the same level. But I was unable to find 53 | ;;; a non-stack-consuming way of doing it, and I ended up transforming the 54 | ;;; algorithm into an iterative version with lazy sequences which yielded much 55 | ;;; better performance and avoided stack overflows. However, determining the 56 | ;;; shape of the branch with the iterative algorithm inherently requires 57 | ;;; unbounded lookahead which can be problematic for very large lists, so some 58 | ;;; tricks are used to minimize unnecessary lookaheads. 59 | 60 | (defn- options-for 61 | "Returns the default options extended by the given map" 62 | [options] 63 | (merge {:formatter str} treely.style/unicode options)) 64 | 65 | (defn- container? 66 | "Returns if the given argument is a container for child elements" 67 | [e] 68 | (or (vector? e) (seq? e))) 69 | 70 | (defn- ^String elem->str 71 | "Returns the line for the element in the tree diagram. Can handle multi-line 72 | elements." 73 | [elem indent last? options] 74 | (let [lines (s/split-lines ((:formatter options) elem)) 75 | ;; If the string representation of the element only contains new line 76 | ;; characters, it will be presented as an empty string 77 | lines (or (seq lines) [""])] 78 | (s/join "\n" (map-indexed #(str indent 79 | ((if last? 80 | (if (zero? %) :last-branch :indent) 81 | (if (zero? %) :branch :bar)) options) 82 | %2) lines)))) 83 | 84 | (defn- find-last-hint 85 | "Generates hints to avoid unnecessary lookahead when determining if a node is 86 | the last branch in the subtree it belongs to. This function is non-lazy and 87 | goes over the entire collection, so you may notice a hiccup if the subtree is 88 | considerably large. But the iteration stays at the top level and does not 89 | descend into the subtrees, so it is still more responsive than the 90 | alternative approaches." 91 | [coll] 92 | (loop [last-non-container -1 93 | container-ends #{} 94 | coll (map-indexed vector coll)] 95 | (let [[[idx1 head] 96 | [idx2 sec] & tail] coll 97 | head-cont? (container? head) 98 | sec-cont? (container? sec) 99 | head-elem? (and head (not head-cont?)) 100 | sec-elem? (and sec (not sec-cont?)) 101 | last-non-container (cond sec-elem? idx2 102 | head-elem? idx1 103 | :else last-non-container)] 104 | (if head 105 | (recur last-non-container 106 | (if (and head-cont? (not sec-cont?)) 107 | (conj container-ends idx1) container-ends) 108 | (if sec-elem? tail (rest coll))) 109 | {:last-non-container last-non-container 110 | :container-ends container-ends})))) 111 | 112 | (defn- depths 113 | "Returns the lazy sequence of [element depth can-be-last?] triples for the 114 | given nested data structure" 115 | ([coll] (depths coll 0 true)) 116 | ([coll depth can-be-last?] 117 | (let [{:keys [last-non-container container-ends]} (find-last-hint coll)] 118 | (mapcat #(if (container? %) 119 | (depths % (inc depth) (container-ends %2)) 120 | [[% depth (and can-be-last? (= last-non-container %2))]]) 121 | coll 122 | (iterate inc 0))))) 123 | 124 | (defn- follow 125 | "Iterates over the sequence of triples obtained by depths function and 126 | returns the lazy sequence of strings" 127 | [depths options] 128 | ((fn nxt [depths indents depth was-last?] 129 | (let [[[head head-depth can-be-last?] & tail] depths 130 | desc (take-while #(>= (second %) head-depth) tail) 131 | last? (and can-be-last? 132 | (or (empty? desc) 133 | (empty? (filter #(= (second %) head-depth) desc))))] 134 | (when head 135 | (let [indents 136 | (cond (= head-depth depth) indents 137 | (> head-depth depth) 138 | (conj indents ((if was-last? :indent :bar) options)) 139 | :else (vec (take head-depth indents)))] 140 | (cons 141 | (elem->str head (apply str indents) last? options) 142 | (lazy-seq (nxt tail indents head-depth last?))))))) 143 | depths [] 0 false)) 144 | 145 | (defn lazy-tree 146 | "Returns the lazy sequence of strings that constitute the tree diagram of the 147 | given nested data structure. Optionally takes a map that determines the style 148 | of the output. Valid keys to the map are as follows. 149 | 150 | | Key | Default | 151 | | --- | --- | 152 | | `:indent` | `\"  \"` | 153 | | `:bar` | `\"│  \"` | 154 | | `:branch` | `\"├── \"` | 155 | | `:last-branch` | `\"└── \"` | 156 | | `:formatter` | `str` |" 157 | [elems & [options]] 158 | (follow (depths elems) (options-for options))) 159 | 160 | (defn ^String tree 161 | "Returns the tree diagram of the given nested data structure in String. This 162 | is essentially the same as the following: 163 | 164 | (clojure.string/join \"\\n\" (lazy-tree elems options))" 165 | [elems & [options]] 166 | (s/join "\n" (lazy-tree elems options))) 167 | 168 | (defn lazy-dir-tree 169 | "Returns the lazy sequence of strings that constitute the tree diagram of the 170 | directory contents. The optional map can additionally have `:filter` function 171 | which determines whether a java.io.File instance should be included or not." 172 | [path & [options]] 173 | (let [root (io/file path) 174 | filt (get options :filter identity) 175 | opts (options-for (dissoc options :filter)) 176 | fmt (:formatter opts) 177 | walk (fn walk [node] 178 | (when (filt node) 179 | (cons node (when (.isDirectory node) 180 | (remove nil? (map walk (.listFiles node)))))))] 181 | (cons 182 | (fmt root) 183 | (when (.isDirectory root) 184 | (let [children (filter filt (.listFiles root))] 185 | (lazy-tree (mapcat walk children) opts)))))) 186 | 187 | (defn ^String dir-tree 188 | "Returns the tree diagram of the directory contents under the given path in 189 | String" 190 | [path & [options]] 191 | (s/join "\n" (lazy-dir-tree path options))) 192 | --------------------------------------------------------------------------------