pax_global_header00006660000000000000000000000064121707220440014511gustar00rootroot0000000000000052 comment=eb6d203bb76cecbb08ce6809c6bf8db721df121a pathetic-0.5.1/000077500000000000000000000000001217072204400133155ustar00rootroot00000000000000pathetic-0.5.1/.gitignore000066400000000000000000000001141217072204400153010ustar00rootroot00000000000000pom.xml *jar /lib/ /target/ .lein-failures .lein-deps-sum .lein-repl-historypathetic-0.5.1/README.md000066400000000000000000000136141217072204400146010ustar00rootroot00000000000000# pathetic Simple unix-style path manipulation in Clojure. This library provides a small number of functions meant to ease the manipulation of file paths. For example, it provides functions to normalize (remove superfluous or roundabout path components, like "." and ".."), relativize (transform one path to be relative to another), and resolve (add path components to an existing path) paths. Path handling is filled with all sorts of platform-specific behaviors and expectations. Java 7 provides some APIs for dealing with paths in a cross-platform way, but even they leave some behavior unimplemented, or defined as "platform-specific." Pathetic aims much lower: simple unix-style paths. Windows or HFS paths aren't going to work here, this is pretty much limited to the simplest things you might do on a unix system or a URL. Anyhow, if you can't use Java 7 yet, those APIs won't help you much now. ## Usage Most of the work in this library is done by parsing a path into a vector representation, which is referred to in the code and documentation as a "path vector." If a path vector's first element is `:root`, then the path is absolute. If the path vector represents a relative path, then the first element is `:cwd`. You can turn a string containing a path into a path vector using the function `parse-path`, and turn a path vector back into a string using the function `render-path`. You could also conceivably generate your own path vectors from some other file system and use the functions that work on path vectors. Note that nothing in pathetic looks at the actual file-system! All logic is based on the semantics of paths, and unrelated to whether any files actually exist or not. - `absolute-path? [path]` Returns true if the given string argument is an absolute path. - `up-dir [path-vector]` Returns a new path vector that has gone up one directory (as if ".." was the last component of the path). - `normalize* [path-vector]` Cleans up a path vector so that it has no removable same-/parent-dir references. - `normalize [path]` Cleans up a path so that it has no leading/trailing whitespace, and removes any removable same-/parent-dir references. - `relativize* [base-path-vec dest-path-vec]` Takes two absolute path vectors or two relative path vectors, and returns a relative path vector that indicates the same file system location as the destination path, but relative to the base path. - `relativize [base-path dest-path]` Takes two absolute paths or two relative paths, and returns a relative path that indicates the same file system location as destination-path, but relative to base-path. - `resolve* [base-path-vec other-path-vec]` Resolve the "other" path vector against the "base" path vector. If the "other" path vector is absolute, the result is the "other" path vector. If the "other" path is empty/nil, the result is the "base" path vector. - `resolve [base-path other-path]` Resolve the other-path against the base-path. If other-path is absolute, the result is other-path. If other-path is nil, the result is base-path. Otherwise, the result is other-path concatenated onto base-path. Does not normalize its output. - `ensure-trailing-separator [path]` If the path given does not have a trailing separator, returns a new path that has one. Otherwise, returns the original path. - `split-url-on-path [url-or-string]` Given a URL or string containing a URL, returns a vector of the three component strings: the stuff before the path, the path, and the stuff after the path. - `url-normalize [url-or-string]` Behaves like normalize on the path part of a URL, but takes a j.n.URL or string containing a URL, and returns a string containing the same URL instead of just a path. Everything but the path part of the URL is unchanged (query, anchor, protocol, etc). - `url-ensure-trailing-separator [url-or-string]` Behaves like ensure-trailing-separator on the path part of a URL, but takes a j.n.URL or string containing a URL, and returns a string containing the same URL instead of just a path. Everything but the path part of the URL is unchanged (query, anchor, protocol, etc). ## News - Released version 0.5.1 - clojurescript.test was erroneously added as a dependency in the 0.5.0 release; this release simply moves it to a dev dependency, as it should have always been. Thanks to [Pierre Carrier](https://github.com/pcarrier) for the fix. - Released version 0.5.0 - Now works on both Clojure and ClojureScript, thanks to the work of [Julien Eluard](https://github.com/jeluard). - Now requires Clojure 1.4 or newer, to enable ClojureScript interop. This version contains no new functionality or bug fixes relative to 0.4.0, so that is still usable with earlier versions of Clojure. - Released version 0.4.0 - The separator character is no longer configurable, it is assumed to be "/". The associated arities have been removed. - The use of java.io.File has been removed. Should work better on Windows now. - Released version 0.3.1 - Added lower-level functions `normalize*`, `relativize*`, and `resolve*`, which take path vectors and return path vectors. `normalize`, `relativize`, and `resolve` have been refactored to use these versions. As a result, the API is more flexible and the code should run a small bit faster due to savings on reparsing. - Released version 0.3.0 - Most core functions now accept an additional, optional path-separator argument. - Addition of ensure-trailing-separator function. - Addition of URL utility functions `split-url-on-path`, `url-normalize`, and `url-ensure-trailing-separator`. - Released version 0.2.0 - All core functions now accept File arguments as well as strings. (Actually, anything that will give a path when it has `str` called on it). ## Obtaining If you are using Leiningen, you can simply add [pathetic "0.5.1"] to your project.clj. ## License Copyright (C) 2011 David Santiago Distributed under the Eclipse Public License, the same as Clojure. pathetic-0.5.1/project.clj000066400000000000000000000027211217072204400154570ustar00rootroot00000000000000(defproject pathetic "0.5.1" :description "The missing path handling." :dependencies [[org.clojure/clojure "1.5.1"]] :source-paths ["target/generated-src"] :test-paths ["target/generated-test"] :plugins [[com.keminglabs/cljx "0.3.0"] [lein-cljsbuild "0.3.2"]] :hooks [cljx.hooks] :cljx {:builds [{:source-paths ["src"] :output-path "target/generated-src" :rules :clj} {:source-paths ["src"] :output-path "target/generated-src" :rules :cljs} {:source-paths ["test"] :output-path "target/generated-test" :rules :clj} {:source-paths ["test"] :output-path "target/generated-test" :rules :cljs}]} :profiles {:clojure1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]} :clojure1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]} :dev {:dependencies [[com.cemerick/clojurescript.test "0.0.4"]]}} :cljsbuild {:builds [{:source-paths ["target/generated-src" "target/generated-test"] :compiler {:output-to "target/cljs/testable.js" :optimizations :whitespace :pretty-print true}}] :test-commands {"unit-tests" ["runners/phantomjs.js" "target/cljs/testable.js"]}}) pathetic-0.5.1/runners/000077500000000000000000000000001217072204400150115ustar00rootroot00000000000000pathetic-0.5.1/runners/phantomjs.js000077500000000000000000000014211217072204400173530ustar00rootroot00000000000000#!/usr/bin/env phantomjs // reusable phantomjs script for running clojurescript.test tests // see http://github.com/cemerick/clojurescript.test for more info var p = require('webpage').create(); var sys = require('system'); p.injectJs(sys.args[1]); p.onConsoleMessage = function (x) { var line = x; if (line !== "[NEWLINE]") { console.log(line.replace(/\[NEWLINE\]/g, "\n")); } }; p.evaluate(function () { cemerick.cljs.test.set_print_fn_BANG_(function(x) { console.log(x.replace(/\n/g, "[NEWLINE]")); // since console.log *itself* adds a newline }); }); var success = p.evaluate(function () { var results = cemerick.cljs.test.run_all_tests(); console.log(results); return cemerick.cljs.test.successful_QMARK_(results); }); phantom.exit(success ? 0 : 1); pathetic-0.5.1/src/000077500000000000000000000000001217072204400141045ustar00rootroot00000000000000pathetic-0.5.1/src/pathetic/000077500000000000000000000000001217072204400157055ustar00rootroot00000000000000pathetic-0.5.1/src/pathetic/core.cljx000066400000000000000000000250721217072204400175250ustar00rootroot00000000000000#+clj (ns pathetic.core (:refer-clojure :exclude [resolve]) (:require [clojure.string :as str])) #+cljs (ns pathetic.core (:refer-clojure :exclude [resolve]) (:require [clojure.string :as str] [goog.Uri :as uri] [goog.string :as string])) (def ^{:private true} separator "/") (def ^{:private true} separator-pattern (re-pattern separator)) ;; A note about the internal representation we work with for paths in this code. ;; --- ;; We work with vectors of path components, basically (that is, strings of the ;; pieces between File/separator). If the path is an absolute path, the first ;; component will be :root, so that if during processing everything else is ;; removed, we know to render "/" and not ".". Similarly, if the path is a ;; relative path, the first component will be :cwd. The rest of the vector is ;; the path components as strings. In this file, I'll call this data structure ;; a "path vector." ;; ;; Utility Functions ;; (defn- common-prefix "Given two collections, returns a sequence containing the prefix they share. Example: (common-prefix [\\a \\b] [\\a \\b \\c \\d]) -> (\\a \\b)" [coll1 coll2] (map first (take-while #(= (first %) (second %)) (map #(vector %1 %2) coll1 coll2)))) (defn- unique-suffix "Returns the elements of interesting-coll that are not part of the common prefix with uninteresting-coll." [uninteresting-coll interesting-coll] (let [common-parts (common-prefix uninteresting-coll interesting-coll)] (drop (count common-parts) interesting-coll))) #+clj (defn split [path] (str/split (str path) separator-pattern)) #+cljs (defn split [path] (if (= path separator) [] (str/split (str path) separator-pattern))) (defn parse-path "Given a j.io.File or string containing a relative or absolute path, returns the corresponding path vector data structure described at the top of the file. This function does not do any normalization or simplification. However, because there is more than one way to write some paths, some simplification might happen anyways, such as if the path starts with a (redundant) \".\"." [path] ;; We have to check first if path is empty because when we try to parse ;; say the root path, it will be separated into an empty list, making it ;; indistinguishable. This avoids having an empty path parsed into [:root]. (if (empty? (str path)) nil (let [path-pieces (split path)] ;; (str/split "/" #"/") => [], so we check for this case first. (if (= 0 (count path-pieces)) [:root] (case (first path-pieces) ;; If first item is "", we split a path that started with "/". ;; Then we need to skip the "" at the start of path-pieces. "" (apply vector :root (rest path-pieces)) ;; If the first item is ".", note that we start with ;; :cwd and then discard the ".". "." (apply vector :cwd (rest path-pieces)) (apply vector :cwd path-pieces)))))) (defn render-path "Given a seq of path elements as created by parse-path, returns a string containing the path represented. This function will only ever use unix-style path rules, so an absolute path will always start with the \"/\" separator. NOTE: It is NOT the goal of this function to perform normalization, it just renders what it is given. HOWEVER, that does NOT mean that it is always true that (= (render-path (parse-path some-path)) some-path). That is, you may not render the exact same string you parsed. This is because the path syntax does not have exactly one way to write every path." [path-pieces] (case (first path-pieces) :root (str separator (str/join separator (rest path-pieces))) :cwd (if (next path-pieces) (str/join separator (rest path-pieces)) ".") (str/join separator path-pieces))) ;; ;; Core Functions ;; #+clj (defn ^:clj starts-with [^String s ^String prefix] (.startsWith s prefix)) #+cljs (defn starts-with [s prefix] (goog.string.startsWith s prefix)) (defn absolute-path? "Returns true if the given argument is an absolute path." [path] (starts-with path separator)) (defn up-dir "Given a seq of path elements as created by parse-path, returns a new seq of path elements, but having gone \"up\" one directory. That is, applies a \"..\" component to the path." [path-pieces] (case (last path-pieces) ;; If the only thing in the path is :cwd, we reached the end of a ;; relative path, and need to add the ".." to keep track of the ;; intention for the relative path. Similarly, if the previous ;; part is a "..", then we should add another, instead of removing the ;; previous one. (:cwd "..") (conj path-pieces "..") ;; Going "up" from root just gives you root (it's its own parent). :root path-pieces (pop path-pieces))) (defn normalize* "Cleans up a path so that it has no leading/trailing whitespace, and removes any removable same-/parent-dir references. path-pieces should be a path vector in the format returned by parse-path; return value is a vector in the same format." [path-pieces] (loop [result [(first path-pieces)] remaining-path (rest path-pieces)] (let [[curr & remainder] remaining-path] (condp = curr nil result ;; Ignore a repeated separator (empty path component) or ;; a same-dir component. "" (recur result remainder) "." (recur result remainder) ".." (recur (up-dir result) remainder) (recur (conj result curr) remainder))))) (defn normalize "Cleans up a path so that it has no leading/trailing whitespace, and removes any unremovable same-/parent-dir references. Takes the path argument as a string and returns its result as a string." [path] (render-path (normalize* (parse-path path)))) (defn relativize* "Takes two absolute paths or two relative paths, and returns a relative path that indicates the same file system location as dest-path, but relative to base-path. Paths should be path vectors, and the return value is also a path vector." [base-path dest-path] (let [common-path (common-prefix base-path dest-path) base-suffix (drop (count common-path) base-path) dest-suffix (drop (count common-path) dest-path)] (if (nil? common-path) (throw (ex-info "Paths contain no common components." {}))) (concat [:cwd] (repeat (count base-suffix) "..") (loop [suffix [] remainder dest-suffix] (let [curr (first remainder)] (condp = curr nil suffix "" (recur suffix (rest remainder)) "." (recur suffix (rest remainder)) ".." (recur (conj suffix "..") (rest remainder)) (recur (conj suffix curr) (rest remainder)))))))) (defn relativize "Takes two absolute paths or two relative paths, and returns a relative path that indicates the same file system location as destination-path, but relative to base-path." [base-path dest-path] (let [base-path (normalize* (parse-path base-path)) dest-path (normalize* (parse-path dest-path))] (render-path (relativize* base-path dest-path)))) (defn resolve* "Resolve the other-path against the base-path. If other-path is absolute, the result is other-path. If other-path is nil, the result is base-path. Otherwise, the result is other-path concatenated onto base-path. Does not normalize its output. All inputs and outputs are path vectors." [base-path other-path] (cond (nil? other-path) base-path (= :root (first other-path)) ;; Is it absolute? other-path :else (let [base-components (normalize* base-path) ;; Skip the first element to get rid of the :cwd other-components (rest (normalize* other-path))] (concat base-components other-components)))) (defn resolve "Resolve the other-path against the base-path. If other-path is absolute, the result is other-path. If other-path is nil, the result is base-path. Otherwise, the result is other-path concatenated onto base-path. Does not normalize its output. Accepts an optional third argument containing a string with the path separator to use." [base-path other-path] (render-path (resolve* (parse-path base-path) (parse-path other-path)))) #+clj (defn ends-with [^String s ^String suffix] (.endsWith s suffix)) #+cljs (defn ends-with [s suffix] (goog.string.endsWith s suffix)) (defn ensure-trailing-separator "If the path given does not have a trailing separator, returns a new path that has one." [path] (if (ends-with path separator) path (str path separator))) ;; ;; URL Utilities ;; #+clj (defn as-url [url-or-string] (if (instance? java.net.URL url-or-string) url-or-string (java.net.URL. url-or-string))) #+cljs (defn as-url [url-or-string] (if (instance? goog.Uri url-or-string) url-or-string (goog.Uri. url-or-string))) (defn split-url-on-path "Given a URL or string containing a URL, returns a vector of the three component strings: the stuff before the path, the path, and the stuff after the path. Useful for destructuring." [url-or-string] ;; We borrow j.n.URL's or goog.Uri's parser just to make sure we get the right path. (let [url (as-url url-or-string) url-string (str url) path (.getPath url) path-idx (.lastIndexOf url-string path) pre-path (.substring url-string 0 path-idx) post-path (.substring url-string (+ path-idx (count path)))] [pre-path path post-path])) (defn url-normalize "Behaves like normalize on the path part of a URL, but takes a j.n.URL or string containing a URL, and returns a string containing the same URL instead of just a path. Everything but the path part of the URL is unchanged (query, anchor, protocol, etc)." [url-or-string] (let [[pre-path path post-path] (split-url-on-path url-or-string)] (str pre-path (normalize path) post-path))) (defn url-ensure-trailing-separator "Behaves like ensure-trailing-separator on the path part of a URL, but takes a j.n.URL or string containing a URL, and returns a string containing the same URL instead of just a path. Everything but the path part of the URL is unchanged (query, anchor, protocol, etc)." [url-or-string] (let [[pre-path path post-path] (split-url-on-path url-or-string)] (str pre-path (ensure-trailing-separator path) post-path))) pathetic-0.5.1/test/000077500000000000000000000000001217072204400142745ustar00rootroot00000000000000pathetic-0.5.1/test/pathetic/000077500000000000000000000000001217072204400160755ustar00rootroot00000000000000pathetic-0.5.1/test/pathetic/test/000077500000000000000000000000001217072204400170545ustar00rootroot00000000000000pathetic-0.5.1/test/pathetic/test/core.cljx000066400000000000000000000150051217072204400206670ustar00rootroot00000000000000#+clj (ns pathetic.test.core (:refer-clojure :exclude [resolve]) (:use pathetic.core clojure.test)) #+cljs (ns pathetic.test.core (:refer-clojure :exclude [resolve]) (:require-macros [cemerick.cljs.test :refer (is deftest with-test run-tests testing)]) (:use [pathetic.core :only [parse-path render-path up-dir normalize url-normalize relativize resolve split-url-on-path ensure-trailing-separator]]) (:require [cemerick.cljs.test :as t])) (deftest test-parse-path (is (= nil (parse-path nil))) (is (= nil (parse-path ""))) (is (= [:root] (parse-path "/"))) (is (= [:root "A"] (parse-path "/A"))) (is (= [:root "A" "B"] (parse-path "/A/B"))) (is (= [:cwd] (parse-path "."))) (is (= [:cwd "A"] (parse-path "./A"))) (is (= [:cwd "A" "B"] (parse-path "./A/B"))) (is (= [:cwd "A"] (parse-path "A"))) (is (= [:cwd "A" "B"] (parse-path "A/B"))) (is (= [:cwd ".." "A"] (parse-path "../A")))) (deftest test-render-path (is (= "/A" (render-path [:root "A"]))) (is (= "/A/B" (render-path [:root "A" "B"]))) (is (= "." (render-path [:cwd]))) (is (= "A" (render-path [:cwd "A"]))) (is (= "A/B" (render-path [:cwd "A" "B"]))) (is (= ".." (render-path [:cwd ".."]))) (is (= "../A" (render-path [:cwd ".." "A"])))) (deftest test-up-dir (is (= [:cwd ".."] (up-dir [:cwd]))) (is (= [:cwd] (up-dir [:cwd "A"]))) (is (= [:root] (up-dir [:root "A"]))) (is (= [:root] (up-dir [:root]))) (is (= [:cwd "A"] (up-dir [:cwd "A" "B"]))) (is (= [:root "A"] (up-dir [:root "A" "B"])))) (deftest test-normalize* (is (= [:root "A" "B"] (normalize* [:root "A" "B" "C" ".."]))) (is (= [:root "A" "B"] (normalize* [:root "A" "B" "."]))) (is (= [:root "A" "B"] (normalize* [:root "A" "C" ".." "B"]))) (is (= [:cwd "A" "B"] (normalize* [:cwd "A" "B"]))) (is (= [:cwd ".." "A"] (normalize* [:cwd ".." "A"]))) (is (= [:cwd] (normalize* [:cwd "A" ".."]))) (is (= [:cwd "A"] (normalize* [:cwd "." "A"]))) (is (= [:cwd "A"] (normalize* [:cwd "." "." "A"]))) (is (= [:cwd] (normalize* [:cwd]))) (is (= [:cwd] (normalize* [:cwd "."]))) (is (= [:cwd] (normalize* [:cwd "." "."]))) (is (= [:cwd ".." ".."] (normalize* [:cwd ".." ".."])))) (deftest test-normalize (is (= "/A/B" (normalize "/A/B/C/.."))) (is (= "/A/B" (normalize "/A/B/."))) (is (= "/A/B" (normalize "/A/B/C/../"))) (is (= "/A/B" (normalize "/A/B/./"))) (is (= "/A/B" (normalize "/A/C/../B/"))) (is (= "A/B" (normalize "A/B"))) (is (= "../A" (normalize "../A"))) (is (= "." (normalize "A/.."))) (is (= "A" (normalize "./A"))) (is (= "A" (normalize "././A"))) (is (= "." (normalize "."))) (is (= "." (normalize "./."))) (is (= "../.." (normalize "../..")))) (deftest test-relativize* (is (= [:cwd "B"] (relativize* [:root "A"] [:root "A" "B"]))) (is (= [:cwd "B"] (relativize* [:root "A"] [:root "A" "." "B"]))) (is (= [:cwd "B" ".." ".."] (relativize* [:root "A"] [:root "A" "B" ".." ".."]))) (is (= [:cwd ".."] (relativize* [:root "A"] [:root]))) (is (= [:cwd "B"] (relativize* [:cwd "A"] [:cwd "A" "B"]))) (is (= [:cwd "B"] (relativize* [:cwd "A"] [:cwd "A" "." "B"]))) (is (= [:cwd "B" ".." ".."] (relativize* [:cwd "A"] [:cwd "A" "B" ".." ".."]))) (is (= [:cwd ".."] (relativize* [:cwd "A"] [:cwd]))) (is (= [:cwd ".." "E" "F"] (relativize* [:root "A" "B" "C" "D"] [:root "A" "B" "C" "E" "F"])))) (deftest test-relativize (is (= "B" (relativize "/A" "/A/B"))) (is (= "B" (relativize "/A" "/A/./B"))) (is (= "B" (relativize "/A" "/A/B/"))) (is (= ".." (relativize "/A" "/A/B/../.."))) (is (= ".." (relativize "/A" "/A/B/../../"))) (is (= "B" (relativize "A" "A/B"))) (is (= "B" (relativize "A" "A/./B"))) (is (= ".." (relativize "A" "A/B/../.."))) (is (= "../E/F" (relativize "/A/B/C/D" "/A/B/C/E/F")))) (deftest test-resolve* (is (= [:root "A" "B"] (resolve* [:root "A"] [:cwd "B"]))) (is (= [:root "A"] (resolve* [:root "A"] nil))) (is (= [:root "B"] (resolve* [:root "A"] [:root "B"]))) (is (= [:cwd "A" "B"] (resolve* [:cwd "A"] [:cwd "B"]))) ) (deftest test-resolve (is (= "/A/B" (resolve "/A/" "B"))) (is (= "/A/B" (resolve "/A" "B"))) (is (= "/A" (resolve "/A" nil))) (is (= "/B" (resolve "/A" "/B"))) (is (= "/B" (resolve "/A/" "/B"))) (is (= "A/B" (resolve "A" "B"))) (is (= "A/B" (resolve "A/" "B/")))) ;; In JDK7, java.nio.file.Path guarantees that if p and q are normalized paths, ;; and q does not start at root, then ;; (= q (relativize p (resolve p q))) ;; Just a few sanity checks here. (deftest test-relativize-resolve-sanity (is (= "B/C" (relativize "A" (resolve "A" "B/C")))) (is (= "B/C" (relativize "/A" (resolve "/A" "B/C")))) (is (= ".." (relativize "/A" (resolve "/A" "..")))) (is (= ".." (relativize "A" (resolve "A" "..")))) (is (= "../.." (relativize "A" (resolve "A" "../.."))))) (deftest test-ensure-trailing-separator (is (= "/A/B/" (ensure-trailing-separator "/A/B/"))) (is (= "/A/B/" (ensure-trailing-separator "/A/B"))) (is (= "A/B/" (ensure-trailing-separator "A/B/"))) (is (= "A/B/" (ensure-trailing-separator "A/B"))) (is (= "A/B/" (ensure-trailing-separator "A/B/"))) (is (= "A/B/" (ensure-trailing-separator "A/B")))) (deftest test-split-url-on-path (is (= ["http://a.b.c" "/d/e/f" "?g=h"] (split-url-on-path "http://a.b.c/d/e/f?g=h"))) (is (= ["http://a.b.c" "/d/e/f" ""] (split-url-on-path "http://a.b.c/d/e/f"))) (is (= ["http://a.b.c" "///d/e/f/" "?g=h"] (split-url-on-path "http://a.b.c///d/e/f/?g=h")))) (deftest test-url-normalize (is (= "http://a.b.c/d/e/f?g=h" (url-normalize "http://a.b.c/d/e/f?g=h"))) (is (= "http://a.b.c/d/e/f?g=h" (url-normalize "http://a.b.c///d/e/f?g=h"))) (is (= "http://a.b.c/d/e/f?g=h#i" (url-normalize "http://a.b.c/d/e/f?g=h#i"))) (is (= "http://a.b.c/d/e/f?g=h#i" (url-normalize "http://a.b.c/d/e/f/g/..?g=h#i"))) (is (= "http://a.b.c/d/e/f?g=h#i" (url-normalize "http://a.b.c/d/e/f/g/../?g=h#i"))) (is (= "http://don:dr4p3r@a.b.c:8080/d/e/f?g=h#i" (url-normalize "http://don:dr4p3r@a.b.c:8080/d/e/f?g=h#i"))) (is (= "http://don:dr4p3r@a.b.c:8080/d/e/f?g=h#i" (url-normalize "http://don:dr4p3r@a.b.c:8080////d/e//f?g=h#i"))))