├── 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 |
--------------------------------------------------------------------------------