├── doc └── intro.md ├── CHANGES.md ├── .gitignore ├── project.clj ├── test └── zip │ └── visit_test.cljc ├── LICENSE ├── src └── zip │ └── visit.cljc └── README.md /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to zip.visit 2 | 3 | Todo: finish this. 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.1.0 4 | 5 | - Clojure 1.7.0 support. 6 | - ClojureScript version. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | *jar 3 | /lib/ 4 | /classes/ 5 | /targets/ 6 | .lein-deps-sum 7 | .idea 8 | .DS_Store 9 | *.iml 10 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject zip-visit "1.1.0" 2 | :description "Clojure zipper-based visitor library." 3 | :url "https://github.com/akhudek/zip-visit" 4 | :license {:name "MIT" 5 | :url "http://opensource.org/licenses/MIT"} 6 | :dependencies [] 7 | :profiles {:provided 8 | {:dependencies [[org.clojure/clojure "1.7.0"] 9 | [org.clojure/clojurescript "0.0-3308"]]} 10 | :dev 11 | {:dependencies [[org.clojure/tools.nrepl "0.2.10"]]}} 12 | :scm {:name "git" 13 | :url "https://github.com/akhudek/zip-visit"} 14 | :deploy-repositories 15 | [["clojars" {:signing {:gpg-key "D8B883CA"}}]]) 16 | -------------------------------------------------------------------------------- /test/zip/visit_test.cljc: -------------------------------------------------------------------------------- 1 | (ns zip.visit-test 2 | (:require 3 | [clojure.zip :as z] 4 | #?@(:clj 5 | [[clojure.test :refer :all] 6 | [zip.visit :as zv]]) 7 | #?@(:cljs 8 | [[cljs.test :refer-macros [deftest testing is]] 9 | [zip.visit :as zv :include-macros true]]))) 10 | 11 | (defn -xor [a b] (and (or a b) (not (and a b)))) 12 | 13 | (deftest false-is-not-nil 14 | (testing "false node propagation" 15 | (is (= {:node [false true true false true true], :state nil} 16 | (zv/visit 17 | (z/vector-zip [0 1 1 2 3 5]) 18 | nil 19 | [(zv/visitor :pre [n s] (if (number? n) {:node (odd? n)}))])))) 20 | 21 | (testing "false state propagation" 22 | (is (= {:node [0 1 1 2 3 5], 23 | :state (reduce -xor false [0 1 1 2 3 5])} 24 | (zv/visit 25 | (z/vector-zip [0 1 1 2 3 5]) 26 | false 27 | [(zv/visitor :pre [n s] (if (number? n) {:state (-xor s (odd? n))}))]))))) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Alexander K. Hudek 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 | -------------------------------------------------------------------------------- /src/zip/visit.cljc: -------------------------------------------------------------------------------- 1 | (ns zip.visit 2 | (:require 3 | [clojure.zip :as z])) 4 | 5 | (defn- visit-node 6 | [dir node state visitors] 7 | (loop [node node 8 | state state 9 | [v & visitors] visitors] 10 | (if v 11 | (let [{node* :node state* :state b :break c :cut} (v dir node state)] 12 | (if (or b c) 13 | {:node node* :state state* :break b :cut c} 14 | (recur (if (nil? node*) node node*) 15 | (if (nil? state*) state state*) 16 | visitors))) 17 | {:node node :state state}))) 18 | 19 | (defn- visit-location 20 | [dir loc state visitors] 21 | (let [node (z/node loc) 22 | context (visit-node dir (z/node loc) state visitors)] 23 | {:loc (if (nil? (:node context)) loc (z/replace loc (:node context))) 24 | :state (if (nil? (:state context)) state (:state context)) 25 | :break (:break context) 26 | :cut (:cut context)})) 27 | 28 | (defn- break-visit 29 | [{loc :loc state :state}] 30 | {:node (z/root loc) :state state}) 31 | 32 | (defn visit 33 | "Visit a tree in a stateful manner. Visitor fuctions return maps with optional keys: 34 | :node - replacement node (doesn't work in :in state) 35 | :state - new state 36 | :cut - in a :pre state, stop downward walk 37 | :break - stop entire walk and return" 38 | [loc state visitors] 39 | (loop [loc loc 40 | next-node :down 41 | state state] 42 | (case next-node 43 | :down 44 | (let [{loc :loc state :state :as r} (visit-location :pre loc state visitors)] 45 | (if (:break r) 46 | (break-visit r) 47 | (if-let [loc* (and (not (:cut r)) (z/down loc))] 48 | (recur loc* :down state) 49 | (recur loc :right state)))) 50 | 51 | :right 52 | (let [{loc :loc state :state :as r} (visit-location :post loc state visitors)] 53 | ;(println (:break r)) 54 | (if (:break r) 55 | (break-visit r) 56 | (if-let [loc* (z/right loc)] 57 | (let [{state* :state :as r} (visit-node :in (z/node (z/up loc)) state visitors) 58 | state** (or state* state)] 59 | (if (:break r) 60 | (break-visit {:loc loc* :state state**}) 61 | (recur loc* :down state**))) 62 | (if-let [loc* (z/up loc)] 63 | (recur loc* :right state) 64 | {:node (z/node loc) :state state}))))))) 65 | 66 | #?(:clj 67 | (defmacro visitor 68 | [type bindings & body] 69 | `(fn [d# n# s#] 70 | (when (= ~type d#) 71 | (loop [n*# n# s*# s#] (let [~bindings [n*# s*#]] ~@body)))))) 72 | 73 | #?(:clj 74 | (defmacro defvisitor 75 | [sym type bindings & body] 76 | `(def ~sym (visitor ~type ~bindings ~@body)))) 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zip-visit 2 | 3 | A Clojure(Script) library implementing functional visitors over zippers. This library 4 | was inspired partly by http://www.ibm.com/developerworks/library/j-treevisit/ (dead link, 5 | alternative: https://insideclojure.org/images/j-treevisit-pdf.pdf) and my own needs 6 | for walking and modifying tree data structures in clojure. 7 | 8 | ## Usage 9 | 10 | Add the dependency: 11 | 12 | ```clojure 13 | [zip-visit "1.1.0"] 14 | ``` 15 | 16 | Require the library. 17 | 18 | ```clojure 19 | (require '[zip.visit :refer :all]) 20 | ``` 21 | 22 | (When requiring in ClojureScript, make sure you add `:refer-macros`:) 23 | 24 | ```clojure 25 | (ns my-awesome-ns 26 | (:require [zip.visit :as v :refer-macros [visitor defvisitor]])) 27 | ``` 28 | 29 | Visitors operate over zippers, let's require that: 30 | 31 | ```clojure 32 | (require '[clojure.zip :as z]) 33 | ``` 34 | 35 | Zippers can operate over any tree or sequence type data. As an example, we 36 | will walk over XML. Let's load a short HTML example into a zipper: 37 | 38 | ```clojure 39 | (require '[clojure.xml :as xml]) 40 | 41 | (def s "
Hello Mr. Foo!
") 42 | (def root (z/xml-zip (xml/parse (java.io.ByteArrayInputStream. (.getBytes s))))) 43 | ``` 44 | 45 | The visit function provides events for pre-order, post-order, and partial in-order 46 | traversal. To illustrate this, let's create an event printer from scratch. 47 | 48 | ```clojure 49 | (defn printer [evt n s] (println evt (str n s))) 50 | ``` 51 | 52 | Visitors are just functions that take three arguments. The first is the event, either 53 | ``:pre``, ``:post``, or ``:in``. The second is the current node and the third is 54 | the current state. We will explain the state in detail later. For now, let's 55 | visit our HTML. 56 | 57 | ```clojure 58 | user=> (visit root nil [printer]) 59 | :pre {:tag :div, :attrs nil, :content [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} "!"]} 60 | :pre {:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} 61 | :pre Hello 62 | :post Hello 63 | :post {:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} 64 | :in {:tag :div, :attrs nil, :content [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} "!"]} 65 | :pre {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} 66 | :pre Mr. Foo 67 | :post Mr. Foo 68 | :post {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} 69 | :in {:tag :div, :attrs nil, :content [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} "!"]} 70 | :pre ! 71 | :post ! 72 | :post {:tag :div, :attrs nil, :content [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} "!"]} 73 | {:node {:tag :div, :attrs nil, :content [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} "!"]}, :state nil} 74 | ``` 75 | 76 | The ``visit`` function does all the work and takes as arguments: the zipper, 77 | the initial state, and a sequence of visitor functions. It returns a map with 78 | two keys: ``:node``, and ``:state``. If visitor functions modify the tree, 79 | then ``:node`` contains the final modified zipper. Similarly, ``:state`` is 80 | the state at the end of the walk. In this example, we get back the original 81 | unchanged input zipper. 82 | 83 | ### Modifying Data 84 | 85 | Now let's talk about modifying the tree. Say we want to modify the span 86 | with id #name. Consider the following function. 87 | 88 | ```clojure 89 | (defn replace-element [id replacement] 90 | (visitor :pre [n s] 91 | (if (= (:id (:attrs n)) id) {:node replacement}))) 92 | ``` 93 | 94 | There is a fair bit going on here, so let's break it down into pieces. On the top 95 | most level we have defined a function, ``replace-element`` that returns a visitor 96 | function. The ``replace-element`` function takes an id and replacement string. 97 | 98 | Next, we introduce one of zip-visit's helper functions: ``visitor``. This is a 99 | macro that creates a visitor function which fires only on a particular event. As 100 | in our first example, ``n`` is the tree node and ``s`` is the state. In this 101 | example our visitor will only fire in a pre-order walk. 102 | 103 | Finally, the core of the function checks that the id matches, and if so, it replaces 104 | the current node with the desired replacement. If a visitor function returns nothing, 105 | the tree is unmodified. However, if it returns a map, either the tree, the state, 106 | or both are modified. To modify the tree we supply a new value for the ``:node`` key. 107 | 108 | Let's try it: 109 | 110 | ```clojure 111 | user=> (pprint (:node (visit root nil [(replace-element "name" "Mr. Smith")]))) 112 | {:tag :div, 113 | :attrs nil, 114 | :content 115 | [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} 116 | "Mr. Smith" 117 | "!"]} 118 | ``` 119 | 120 | But what if we wanted to modify other parts of the tree? Do we need to 121 | do multiple walks? Create huge conditionals in our visitor? Fortunately not! 122 | Remember that visit takes a seq of visitor functions. What happens when we 123 | supply more then one function? At every step, each function is applied in the order 124 | is appears in the seq. Successive functions get the node and state values 125 | from the previous functions. 126 | 127 | ```clojure 128 | user=> (:node (visit root nil [(replace-element "greeting" "Greetings") (replace-element "name" "Mr. Smith")])) 129 | {:tag :div, :attrs nil, :content ["Greetings" "Mr. Smith" "!"]} 130 | ``` 131 | 132 | ### State 133 | 134 | Ok, so what about state? The state is completely user defined and evolves along 135 | with the zippered data during the walk. As an example, let's collect all the spans 136 | in our example data. 137 | 138 | ```clojure 139 | (defvisitor collect-spans :pre [n s] 140 | (if (= :span (:tag n)) {:state (conj s (:content n))})) 141 | ``` 142 | 143 | Here we introduce the second visit helper macro, ``defvisitor``. This is the same 144 | as ``visitor`` but binds the result to a symbol. The ``collect-spans`` function 145 | expects state that is just a set. 146 | 147 | ```clojure 148 | user=> (:state (visit root #{} [collect-spans])) 149 | #{["Mr. Foo"] ["Hello"]} 150 | ``` 151 | 152 | And remember, we can mix and match any visitor functions: 153 | 154 | ```clojure 155 | user=> (pprint (visit root #{} [collect-spans (replace-element "name" "Mr. Smith")])) 156 | {:node 157 | {:tag :div, 158 | :attrs nil, 159 | :content 160 | [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} 161 | "Mr. Smith" 162 | "!"]}, 163 | :state #{["Mr. Foo"] ["Hello"]}} 164 | ``` 165 | 166 | Just be aware of the order! 167 | 168 | ```clojure 169 | user=> (pprint (visit root #{} [(replace-element "name" "Mr. Smith") collect-spans])) 170 | {:node 171 | {:tag :div, 172 | :attrs nil, 173 | :content 174 | [{:tag :span, :attrs {:id "greeting"}, :content ["Hello"]} 175 | "Mr. Smith" 176 | "!"]}, 177 | :state #{["Hello"]}} 178 | ``` 179 | 180 | ### Walking Replacements and Cuts 181 | 182 | What does ``visit`` do when you replace a node? After applying all the visitor functions, 183 | at a given step, it continues walking the replaced content. Consider this example. 184 | 185 | ```clojure 186 | (def s* {:tag :span, :attrs {:id "other-name"}, :content ["Mrs. Foo"]}) 187 | 188 | (defvisitor extended-greeting :pre [n s] 189 | (if (= "greeting" (:id (:attrs n))) {:node (update-in n [:content] conj s*)})) 190 | 191 | user=> (pprint (:node (visit root nil [extended-greeting (replace-element "other-name" "Mrs. Smith")]))) 192 | {:tag :div, 193 | :attrs nil, 194 | :content 195 | [{:tag :span, 196 | :attrs {:id "greeting"}, 197 | :content ["Hello" "Mrs. Smith"]} 198 | {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} 199 | "!"]} 200 | 201 | ``` 202 | 203 | Notice that ``"Mrs. Foo"`` has been resplaced with ``"Mrs. Smith"``. This happened because the walk 204 | continued into the replaced code. *Beware of recursive replacements!* 205 | 206 | What if you don't want this behaviour? You can disable it on a per function basis by using the a 207 | *cut*. 208 | 209 | ```clojure 210 | (defvisitor extended-greeting :pre 211 | [n s] 212 | (if (= "greeting" (:id (:attrs n))) {:node (update-in n [:content] conj s*) :cut true})) 213 | 214 | user=> (pprint (:node (visit root nil [extended-greeting (replace-element "other-name" "Mrs. Smith")]))) 215 | {:tag :div, 216 | :attrs nil, 217 | :content 218 | [{:tag :span, 219 | :attrs {:id "greeting"}, 220 | :content 221 | ["Hello" 222 | {:content ["Mrs. Foo"], :attrs {:id "other-name"}, :tag :span}]} 223 | {:tag :span, :attrs {:id "name"}, :content ["Mr. Foo"]} 224 | "!"]} 225 | ``` 226 | 227 | ### Breaks 228 | 229 | Let's try to build a tree based version of the ``some`` function from the standard library. Recall 230 | that some takes a function and a collection and returns the first non-nil value of that function. 231 | 232 | ```clojure 233 | (defn some-tree-visitor [f] 234 | (visitor :pre [n s] (if-let [v (f n)] {:state v}))) 235 | 236 | (defn some-tree [f zipper] 237 | (:state (visit zipper nil [(some-tree-visitor f)]))) 238 | ``` 239 | 240 | And let's use a different sort of zipper, because we can! 241 | 242 | ```clojure 243 | (def my-zip (z/vector-zip [1 2 3 [4 5 [6] 7 [8 9]]])) 244 | 245 | user=> (some-tree #(if (and (number? %) (even? %)) %) my-zip) 246 | 8 247 | ``` 248 | 249 | Whoops! I bet some of you saw that coming. Some is supposed to return the first value that 250 | matches, but here we got the last value. We could modify ``some-tree-visitor`` to adjust for this, 251 | but if we really want to match the behaviour of ``some`` we need to stop the walk as soon as we 252 | find the right value. Enter *break*! 253 | 254 | ```clojure 255 | (defn some-tree-visitor [f] 256 | (visitor :pre [n s] (if-let [v (f n)] {:state v :break true}))) 257 | 258 | user=> (some-tree #(if (and (number? %) (even? %)) %) my-zip) 259 | 2 260 | ``` 261 | 262 | When break is set the walk stops and immediately zips back up to the root. 263 | 264 | ## Contributors 265 | - rubicks 266 | - Dave Della Costa 267 | 268 | ## License 269 | 270 | The MIT License (MIT) 271 | 272 | Copyright (c) 2013-2017 Alexander K. Hudek 273 | 274 | See LICENSE file. 275 | --------------------------------------------------------------------------------