├── pnpm-workspace.yaml ├── .gitignore ├── tests.edn ├── doc.clj ├── test └── tubax │ └── tests │ ├── main.cljs │ ├── helpers_test.cljs │ └── core_test.cljs ├── shadow-cljs.edn ├── package.json ├── deps.edn ├── src └── tubax │ ├── core.cljs │ └── helpers.cljs ├── README.md ├── doc └── index.adoc ├── LICENSE └── pnpm-lock.yaml /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | shamefullyHoist: true 2 | recursiveInstall: true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /target 3 | /.lein-failures 4 | /.lein-repl-history 5 | /.nrepl-history 6 | /output 7 | /pom.xml 8 | /out 9 | *.jar 10 | *.class 11 | \#*\# 12 | *~ 13 | .\#* 14 | .cpcache 15 | .rebel_readline_history 16 | .shadow-cljs -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :unit 3 | :test-paths ["test/tubax/tests"] 4 | :source-paths ["src"] 5 | :ns-patterns ["test-"]}] 6 | ;; :reporter kaocha.report.progress/progress 7 | ;; :plugins [:profiling :notifier] 8 | } 9 | -------------------------------------------------------------------------------- /doc.clj: -------------------------------------------------------------------------------- 1 | (require '[codox.main :as codox]) 2 | 3 | (codox/generate-docs 4 | {:output-path "doc/dist/latest" 5 | :metadata {:doc/format :markdown} 6 | :language :clojurescript 7 | :name "funcool/tubax" 8 | :themes [:rdash] 9 | :source-paths ["src"] 10 | :namespaces [#"^tubax\."] 11 | :source-uri "https://github.com/funcool/tubax/blob/master/{filepath}#L{line}"}) 12 | -------------------------------------------------------------------------------- /test/tubax/tests/main.cljs: -------------------------------------------------------------------------------- 1 | (ns tubax.tests.main 2 | (:require 3 | [cljs.test :as t] 4 | [tubax.tests.core-test] 5 | [tubax.tests.helpers-test])) 6 | 7 | (enable-console-print!) 8 | 9 | (defmethod t/report [:cljs.test/default :end-run-tests] 10 | [m] 11 | (if (t/successful? m) 12 | (set! (.-exitCode js/process) 0) 13 | (set! (.-exitCode js/process) 1))) 14 | 15 | (defn init 16 | [] 17 | (t/run-tests 18 | 'tubax.tests.core-test 19 | 'tubax.tests.helpers-test)) 20 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | {:deps {:aliases [:dev]} 2 | :http {:port 3448} 3 | :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"] 4 | :dev-http {8888 "classpath:public"} 5 | 6 | :builds 7 | {:tests 8 | {:target :esm 9 | :output-dir "target/tests/" 10 | 11 | :modules 12 | {:test {:init-fn tubax.tests.main/init}} 13 | 14 | :compiler-options 15 | {:output-feature-set :es2020 16 | :output-wrapper false 17 | :source-map true 18 | :source-map-include-sources-content true 19 | :source-map-detail-level :all 20 | :warnings {:fn-deprecated false}}}}} 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tubax", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "test:compile": "clojure -M:dev:shadow-cljs compile tests", 7 | "test": "clojure -M:dev:shadow-cljs compile tests && node target/tests/test.js", 8 | "test:watch": "mkdir -p target/tests && concurrently \"clojure -M:dev:shadow-cljs watch tests\" \"nodemon -C -d 2 -w target/tests --exec 'node target/tests/test.js'\"" 9 | }, 10 | "author": "", 11 | "type": "module", 12 | "license": "APACHE-2.0", 13 | "dependencies": { 14 | "sax": "^1.4.3", 15 | "stream": "^0.0.3", 16 | "string_decoder": "^1.3.0", 17 | "buffer": "^6.0.3" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^22.0.0", 21 | "concurrently": "^9.2.1", 22 | "nodemon": "^3.1.11", 23 | "source-map-support": "^0.5.21", 24 | "ws": "^8.18.0" 25 | }, 26 | "packageManager": "pnpm@10.24.0+sha512.01ff8ae71b4419903b65c60fb2dc9d34cf8bb6e06d03bde112ef38f7a34d6904c424ba66bea5cdcf12890230bf39f9580473140ed9c946fef328b6e5238a345a" 27 | } 28 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"], 2 | :aliases 3 | {:dev 4 | {:extra-deps 5 | {com.bhauman/rebel-readline-cljs {:mvn/version "RELEASE"}, 6 | com.bhauman/rebel-readline {:mvn/version "RELEASE"}, 7 | org.clojure/tools.namespace {:mvn/version "RELEASE"}, 8 | org.clojure/core.async {:mvn/version "1.6.673"} 9 | org.clojure/clojure {:mvn/version "1.12.1"} 10 | criterium/criterium {:mvn/version "RELEASE"} 11 | thheller/shadow-cljs {:mvn/version "RELEASE"}} 12 | 13 | :extra-paths ["test" "dev"]} 14 | 15 | :repl 16 | {:main-opts ["-m" "rebel-readline.main"]} 17 | 18 | :shadow-cljs 19 | {:main-opts ["-m" "shadow.cljs.devtools.cli"] 20 | :jvm-opts ["--sun-misc-unsafe-memory-access=allow"]} 21 | 22 | :codox 23 | {:extra-deps 24 | {codox/codox {:mvn/version "RELEASE"} 25 | org.clojure/tools.reader {:mvn/version "RELEASE"} 26 | codox-theme-rdash/codox-theme-rdash {:mvn/version "RELEASE"}}} 27 | 28 | :build 29 | {:extra-paths ["resources"] 30 | :extra-deps 31 | {io.github.clojure/tools.build {:git/tag "v0.9.3" :git/sha "e537cd1"} 32 | org.clojure/clojurescript {:mvn/version "RELEASE"} 33 | org.clojure/tools.deps.alpha {:mvn/version "RELEASE"}} 34 | :ns-default build} 35 | 36 | :outdated 37 | {:extra-deps 38 | {com.github.liquidz/antq {:mvn/version "RELEASE"} 39 | org.slf4j/slf4j-nop {:mvn/version "RELEASE"}} 40 | :main-opts ["-m" "antq.core"]}}} 41 | -------------------------------------------------------------------------------- /src/tubax/core.cljs: -------------------------------------------------------------------------------- 1 | (ns tubax.core 2 | (:require 3 | ["sax" :as sax])) 4 | 5 | (defn start-document [] 6 | {:stack [] 7 | :current nil}) 8 | 9 | (defn parse-node 10 | ([node] 11 | (parse-node node nil)) 12 | 13 | ([node {:keys [keywordize-keys] 14 | :or {keywordize-keys true}}] 15 | (let [tag (cond-> (.-name node) 16 | keywordize-keys (keyword)) 17 | 18 | attrs (js->clj (.-attributes node) :keywordize-keys keywordize-keys) 19 | attrs (when-not (empty? attrs) attrs)] 20 | {:tag tag 21 | :attrs attrs 22 | :content nil}))) 23 | 24 | (defn push-node 25 | [{:keys [stack current] :as document} node] 26 | (let [new-current (parse-node node) 27 | new-stack (cond-> stack (some? current) (conj current))] 28 | (assoc document 29 | :stack new-stack 30 | :current new-current))) 31 | 32 | (defn pop-node 33 | [{:keys [stack current] :as document} node] 34 | 35 | (let [tag (keyword node)] 36 | (if (empty? stack) 37 | document 38 | 39 | (let [new-stack (pop stack) 40 | new-current (-> (peek stack) 41 | (update :content (fnil conj []) current))] 42 | (assoc document 43 | :stack new-stack 44 | :current new-current))))) 45 | 46 | (defn push-text 47 | [document text] 48 | (cond-> document 49 | (not (empty? text)) 50 | (update-in [:current :content] (fnil conj []) text))) 51 | 52 | (defn- create-parser 53 | [{:keys [strict trim normalize 54 | lowercase xmlns position 55 | strict-entities] 56 | :or {strict true 57 | trim true 58 | normalize false 59 | lowercase true 60 | position true 61 | strict-entities false}}] 62 | 63 | (sax/parser strict 64 | #js {"trim" trim 65 | "normalize" normalize 66 | "lowercase" lowercase 67 | "xmlns" xmlns 68 | "position" position 69 | "strictEntities" strict-entities})) 70 | 71 | (defn xml->clj 72 | ([source] (xml->clj source {})) 73 | ([source options] 74 | (let [parser (create-parser options) 75 | document (atom (start-document)) 76 | result (atom nil)] 77 | 78 | ;; OPEN TAG 79 | (set! (.-onopentag parser) 80 | #(swap! document push-node %)) 81 | 82 | ;; CLOSE TAG 83 | (set! (.-onclosetag parser) 84 | #(swap! document pop-node %)) 85 | 86 | ;; GET TEXT 87 | (set! (.-ontext parser) 88 | #(swap! document push-text %)) 89 | 90 | ;; CDATA HANDLING 91 | (set! (.-oncdata parser) 92 | #(swap! document push-text %)) 93 | 94 | ;; END PARSING 95 | (set! (.-onend parser) 96 | #(when-not (some? @result) 97 | (reset! result {:success (:current @document)}))) 98 | 99 | ;; ERROR 100 | (set! (.-onerror parser) 101 | #(reset! result {:error (str %)})) 102 | 103 | (.write parser source) 104 | (.close parser) 105 | 106 | (or (:success @result) 107 | (throw (ex-info (str (:error @result)) {})))))) 108 | -------------------------------------------------------------------------------- /src/tubax/helpers.cljs: -------------------------------------------------------------------------------- 1 | (ns tubax.helpers) 2 | 3 | ;; Datastructure access 4 | (defn is-node 5 | "Checks if the parameter matchs the tubax node contract" 6 | [node] 7 | (and (map? node) 8 | (contains? node :tag) 9 | (keyword? (:tag node)) 10 | (contains? node :attrs) 11 | (map? (:attrs node)) 12 | (contains? node :content) 13 | (vector? (:content node)))) 14 | 15 | (defn tag [{:keys [tag]}] tag) 16 | (defn attrs [{:keys [attrs]}] attrs) 17 | 18 | (defn attributes 19 | [{:keys [attrs]}] attrs) 20 | 21 | (defn children [{:keys [content]}] content) 22 | 23 | (defn text [node] 24 | (let [[value & _] (children node)] 25 | (if (string? value) value nil))) 26 | 27 | (defn seq-tree [tree] 28 | (tree-seq is-node children tree)) 29 | 30 | (defn filter-tree [search-fn tree] 31 | (->> tree seq-tree (filter search-fn))) 32 | 33 | (defn first-tree [search-fn tree] 34 | (->> tree (filter-tree search-fn) first)) 35 | 36 | (defn find-first-by-path [path-left node] 37 | (cond 38 | (empty? path-left) node 39 | (nil? node) nil 40 | (string? node) nil 41 | :else 42 | (let [subtree (some #(if (= (tag %) (first path-left)) % nil) 43 | (children node))] 44 | (recur (rest path-left) 45 | subtree)))) 46 | 47 | (defn find-all-by-path [path node] 48 | (cond 49 | (empty? path) '() 50 | (and (= (count path) 1) (= (tag node) (first path))) (list node) 51 | (and (= (count path) 1)) '() 52 | :else 53 | (if (= (tag node) (first path)) 54 | (apply concat (map (partial find-all-by-path (rest path)) 55 | (children node))) 56 | '()))) 57 | 58 | ;; Dispatcher function for both 'find-first' an 'find-all' 59 | (defn- find-multi-dispatcher [_ param] 60 | (let [key (-> param keys first)] 61 | (cond 62 | (and (= key :attribute) 63 | (keyword? (get param key))) 64 | [:attribute :keyword] 65 | (and (= key :attribute) 66 | (vector? (get param key))) 67 | [:attribute :vector] 68 | :else key 69 | ))) 70 | 71 | ;; Find first 72 | (defmulti find-first find-multi-dispatcher) 73 | 74 | (defmethod find-first :tag [tree param] 75 | (first-tree #(= (tag %) (:tag param)) tree)) 76 | 77 | (defmethod find-first [:attribute :keyword] [tree param] 78 | (first-tree #(contains? (attributes %) (:attribute param)) tree)) 79 | 80 | (defmethod find-first [:attribute :vector] [tree param] 81 | (let [[key value] (:attribute param)] 82 | (first-tree #(and (contains? (attributes %) key) 83 | (= (get (attributes %) key) value)) tree))) 84 | 85 | (defmethod find-first :path [tree {:keys [path]}] 86 | (if (and (not (empty? path)) (= (first path) (tag tree))) 87 | (find-first-by-path (rest path) tree) 88 | nil)) 89 | 90 | ;; Find all 91 | (defmulti find-all find-multi-dispatcher) 92 | 93 | (defmethod find-all :tag [tree param] 94 | (filter-tree #(= (tag %) (:tag param)) tree)) 95 | 96 | (defmethod find-all [:attribute :keyword] [tree param] 97 | (filter-tree #(contains? (attributes %) (:attribute param)) tree)) 98 | 99 | (defmethod find-all [:attribute :vector] [tree param] 100 | (let [[key value] (:attribute param)] 101 | (filter-tree #(and (contains? (attributes %) key) 102 | (= (get (attributes %) key) value)) tree))) 103 | 104 | (defmethod find-all :path [tree {:keys [path]}] 105 | (find-all-by-path path tree)) 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tubax 2 | 3 | http://en.wikipedia.org/wiki/Tubax 4 | 5 |
6 | While the timbre of the E♭ tubax is more focused and compact than that of the full-sized contrabass saxophone. 7 |
8 | 9 | funcool/tubax 10 | {:git/tag "v2025.11.28" 11 | :git/sha "2d9a986" 12 | :git/url "https://github.com/funcool/tubax.git"} 13 | ``` 14 | 15 | ## Rationale 16 | 17 | Currently there is no good way to parse XML and other markup languages 18 | with Clojurescript. There are no Clojurescript-based libraries and 19 | most of the Javascript ones require access to the DOM. 20 | 21 | This last point is critical because HTML5 Web Workers don't have 22 | access to these APIs so an alternative is necessary. 23 | 24 | Another alternative to XML processing is to go to a 25 | middle-ground. There are some libraries that will parse XML into a 26 | JSON format. 27 | 28 | The problem with these is that JSON is not a faithfull representation 29 | of the XML format. There are some XML that couldn't be represented as 30 | JSON. 31 | 32 | For example, the following XML will loss information when transformed into JSON. 33 | 34 | ```xml 35 | 36 | A 37 | B 38 | A 39 | ``` 40 | 41 | Another main objective of *tubax* is to be fully compatible with the 42 | `clojure.xml` format so we can access the functionality currently in 43 | the Clojure API like zippers. 44 | 45 | 46 | ## Getting Started 47 | 48 | 49 | *Tubax* uses behind the scenes 50 | [sax-js](https://github.com/isaacs/sax-js) a very lightweight library 51 | for for XML parsing based on SAX (simple api for xml). 52 | 53 | All examples will use this XML as if it existed in a `(def xml-data "...")` definition. 54 | 55 | ```xml 56 | 57 | 58 | RSS Title 59 | This is an example of an RSS feed 60 | http://www.example.com/main.html 61 | Mon, 06 Sep 2010 00:01:00 +0000 62 | Sun, 06 Sep 2009 16:20:00 +0000 63 | 1800 64 | 65 | Example entry 66 | Here is some text containing an interesting description. 67 | http://www.example.com/blog/post/1 68 | 7bd204c6-1655-4c27-aeee-53f933c5395f 69 | Sun, 06 Sep 2009 16:20:00 +0000 70 | 71 | 72 | Example entry2 73 | Here is some text containing an interesting description. 74 | http://www.example.com/blog/post/1 75 | 7bd204c6-1655-4c27-aeee-53f933c5395f 76 | Sun, 06 Sep 2009 16:20:00 +0000 77 | 78 | 79 | 80 | ``` 81 | 82 | 83 | In order to parse a XML file you only have to make a call to the `xml->clj` function 84 | 85 | ```clojure 86 | (core/xml->clj xml-data) 87 | ;; => {:tag :rss :attributes {:version "2.0"} 88 | ;; :content 89 | ;; [{:tag :channel :attributes {} 90 | ;; :content 91 | ;; [{:tag :title :attributes {} :content ["RSS Title"]} 92 | ;; {:tag :description :attributes {} :content ["This is an example of an RSS feed"]} 93 | ;; {:tag :link :attributes {} :content ["http://www.example.com/main.html"]} 94 | ;; {:tag :lastBuildDate :attributes {} :content ["Mon, 06 Sep 2010 00:01:00 +0000"]} 95 | ;; {:tag :pubDate :attributes {} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]} 96 | ;; {:tag :ttl :attributes {} :content ["1800"]} 97 | ;; {:tag :item :attributes {} 98 | ;; :content 99 | ;; [{:tag :title :attributes {} :content ["Example entry"]} 100 | ;; {:tag :description :attributes {} :content ["Here is some text containing an interesting description."]} 101 | ;; {:tag :link :attributes {} :content ["http://www.example.com/blog/post/1"]} 102 | ;; {:tag :guid :attributes {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]} 103 | ;; {:tag :pubDate :attributes {} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]}]} 104 | ;; {:tag :item :attributes {} 105 | ;; :content 106 | ;; [{:tag :title :attributes {} :content ["Example entry2"]} 107 | ;; {:tag :description :attributes {} :content ["Here is some text containing an interesting description."]} 108 | ;; {:tag :link :attributes {} :content ["http://www.example.com/blog/post/2"]} 109 | ;; {:tag :guid :attributes {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]} 110 | ;; {:tag :pubDate :attributes {} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]}]}]}]} 111 | ``` 112 | 113 | This data structure is fully compatible with the XML zipper inside `clojure.zip` 114 | 115 | ``` 116 | (require '[clojure.zip :as z]) 117 | 118 | (-> xml core/xml->clj 119 | z/xml-zip 120 | z/down 121 | z/down 122 | z/rightmost 123 | z/node 124 | :content 125 | first) 126 | 127 | ;; => "Example entry2" 128 | ``` 129 | 130 | The `xml->clj` function accept the following options: 131 | 132 | - `:strict` - Enables strict parsing mode; defaults to `true` 133 | - `:trim` - Enables triming of whitespaces; defaults to `true` 134 | - `:normalize` - Enables whitespace normalization; defaults to `true` (the 135 | normalization replaces all whitespaces-characters (like tabs, end of lines, 136 | ...) for whitespaces. 137 | - `:xmlns` - Enables support for xml namespaces; defaults to `false` 138 | - `:strict-entities`: Enables stricter parser for xml entities; defaults to 139 | `false` (raises an error if non-predefined entity is found. 140 | 141 | 142 | ## License 143 | 144 | Licensed under [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0) 145 | -------------------------------------------------------------------------------- /test/tubax/tests/helpers_test.cljs: -------------------------------------------------------------------------------- 1 | (ns tubax.tests.helpers-test 2 | (:require 3 | [tubax.helpers :as helpers] 4 | [cljs.test :as t])) 5 | 6 | (def testing-data 7 | {:tag :rss :attrs {:version "2.0"} 8 | :content 9 | [{:tag :channel :attrs {} 10 | :content 11 | [{:tag :title :attrs {} :content ["RSS Title"]} 12 | {:tag :description :attrs {} :content ["This is an example of an RSS feed"]} 13 | {:tag :link :attrs {} :content ["http://www.example.com/main.html"]} 14 | {:tag :lastBuildDate :attrs {} :content ["Mon, 06 Sep 2010 00:01:00 +0000"]} 15 | {:tag :pubDate :attrs {} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]} 16 | {:tag :ttl :attrs {} :content ["1800"]} 17 | {:tag :item :attrs {} 18 | :content 19 | [{:tag :title :attrs {} :content ["Example entry"]} 20 | {:tag :description :attrs {} :content ["Here is some text containing an interesting description."]} 21 | {:tag :link :attrs {} :content ["http://www.example.com/blog/post/1"]} 22 | {:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]} 23 | {:tag :pubDate :attrs {:year "2013"} :content ["Sun, 06 Sep 2013 16:20:00 +0000"]}]} 24 | {:tag :item :attrs {} 25 | :content 26 | [{:tag :title :attrs {} :content ["Example entry2"]} 27 | {:tag :description :attrs {} :content ["Here is some text containing an interesting description."]} 28 | {:tag :link :attrs {} :content ["http://www.example.com/blog/post/2"]} 29 | {:tag :guid :attrs {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-bbbb-222222222222"]} 30 | {:tag :pubDate :attrs {:year "2009"} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]} 31 | {:tag :author :attrs {} :content ["John McCarthy"]}]}]}]}) 32 | 33 | (t/deftest helpers-access 34 | (t/testing "Helpers access" 35 | (let [node {:tag :item :attrs {:att1 "att"} :content ["value"]}] 36 | (t/is (= (helpers/tag node) :item)) 37 | (t/is (= (helpers/attributes node) {:att1 "att"})) 38 | (t/is (= (helpers/children node) ["value"])) 39 | (t/is (= (helpers/text node) "value")))) 40 | 41 | (t/testing "Unexpected values" 42 | (t/is (= (helpers/text {:tag :item :attrs {} :content [{:tag :itemb :attrs {} :content ["value"]}]}) nil))) 43 | 44 | (t/testing "Check if node" 45 | (t/is (= (helpers/is-node {:tag :item :attrs {} :content []}) true)) 46 | (t/is (= (helpers/is-node "test") false)) 47 | (t/is (= (helpers/is-node {:tag :item :content []}) false)) 48 | (t/is (= (helpers/is-node [:tag "test" :attrs {} :content []]) false)))) 49 | 50 | (t/deftest find-first 51 | (t/testing "Find first tag" 52 | (t/is (= (helpers/find-first testing-data {:tag :link}) 53 | {:tag :link :attrs {} :content ["http://www.example.com/main.html"]})) 54 | (t/is (= (helpers/find-first testing-data {:tag :guid}) 55 | {:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]})) 56 | (t/is (= (helpers/find-first testing-data {:tag :author}) 57 | {:tag :author :attrs {} :content ["John McCarthy"]})) 58 | (t/is (= (helpers/find-first testing-data {:tag :no-tag}) 59 | nil))) 60 | (t/testing "Find first path" 61 | (t/is (= (helpers/find-first testing-data {:path [:rss :channel :ttl]}) 62 | {:tag :ttl :attrs {} :content ["1800"]})) 63 | (t/is (= (helpers/find-first testing-data {:path [:rss :channel :item :link]}) 64 | {:tag :link :attrs {} :content ["http://www.example.com/blog/post/1"]})) 65 | (t/is (= (helpers/find-first testing-data {:path [:rss :channel :item :notexists]}) 66 | nil)) 67 | (t/is (= (helpers/find-first testing-data {:path nil}) 68 | nil)) 69 | (t/is (= (helpers/find-first testing-data {:path []}) 70 | nil)) 71 | (t/is (= (helpers/find-first testing-data {:path [:badroot]}) 72 | nil))) 73 | (t/testing "Find first keyword" 74 | (t/is (= (helpers/find-first testing-data {:attribute :isPermaLink}) 75 | {:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]})) 76 | (t/is (= (helpers/find-first testing-data {:attribute :year}) 77 | {:tag :pubDate :attrs {:year "2013"} :content ["Sun, 06 Sep 2013 16:20:00 +0000"]})) 78 | (t/is (= (helpers/find-first testing-data {:attribute :not-existing}) 79 | nil))) 80 | (t/testing "Find first keyword equality" 81 | (t/is (= (helpers/find-first testing-data {:attribute [:isPermaLink "false"]}) 82 | {:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]})) 83 | (t/is (= (helpers/find-first testing-data {:attribute [:isPermaLink "true"]}) 84 | {:tag :guid :attrs {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-bbbb-222222222222"]})) 85 | (t/is (= (helpers/find-first testing-data {:attribute [:year "2013"]}) 86 | {:tag :pubDate :attrs {:year "2013"} :content ["Sun, 06 Sep 2013 16:20:00 +0000"]})) 87 | (t/is (= (helpers/find-first testing-data {:attribute [:year "2009"]}) 88 | {:tag :pubDate :attrs {:year "2009"} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]})) 89 | (t/is (= (helpers/find-first testing-data {:attribute [:year "2010"]}) 90 | nil)) 91 | (t/is (= (helpers/find-first testing-data {:attribute [:not-existing true]}) 92 | nil)) 93 | (t/is (= (helpers/find-first testing-data {:attribute [:shouldfail]}) 94 | nil)))) 95 | 96 | (t/deftest find-all 97 | (t/testing "Find all by tag" 98 | (t/is (= (helpers/find-all testing-data {:tag :link}) 99 | '({:tag :link :attrs {} :content ["http://www.example.com/main.html"]} 100 | {:tag :link :attrs {} :content ["http://www.example.com/blog/post/1"]} 101 | {:tag :link :attrs {} :content ["http://www.example.com/blog/post/2"]}))) 102 | (t/is (= (helpers/find-all testing-data {:tag :guid}) 103 | '({:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]} 104 | {:tag :guid :attrs {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-bbbb-222222222222"]}))) 105 | (t/is (= (helpers/find-all testing-data {:tag :author}) 106 | '({:tag :author :attrs {} :content ["John McCarthy"]}))) 107 | (t/is (= (helpers/find-all testing-data {:tag :no-tag}) 108 | '()))) 109 | (t/testing "Find first path" 110 | (t/is (= (helpers/find-all testing-data {:path [:rss :channel :ttl]}) 111 | '({:tag :ttl :attrs {} :content ["1800"]}))) 112 | (t/is (= (helpers/find-all testing-data {:path [:rss :channel :item :link]}) 113 | '({:tag :link :attrs {} :content ["http://www.example.com/blog/post/1"]} 114 | {:tag :link :attrs {} :content ["http://www.example.com/blog/post/2"]}))) 115 | (t/is (= (helpers/find-all testing-data {:path [:rss :channel :item :notexists]}) 116 | '())) 117 | (t/is (= (helpers/find-all testing-data {:path nil}) 118 | '())) 119 | (t/is (= (helpers/find-all testing-data {:path []}) 120 | '())) 121 | (t/is (= (helpers/find-all testing-data {:path [:badroot]}) 122 | '())) 123 | ) 124 | (t/testing "Find all keyword" 125 | (t/is (= (helpers/find-all testing-data {:attribute :isPermaLink}) 126 | '({:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]} 127 | {:tag :guid :attrs {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-bbbb-222222222222"]}))) 128 | (t/is (= (helpers/find-all testing-data {:attribute :year}) 129 | '({:tag :pubDate :attrs {:year "2013"} :content ["Sun, 06 Sep 2013 16:20:00 +0000"]} 130 | {:tag :pubDate :attrs {:year "2009"} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]}))) 131 | (t/is (= (helpers/find-all testing-data {:attribute :not-existing}) 132 | '()))) 133 | (t/testing "Find all keyword equality" 134 | (t/is (= (helpers/find-all testing-data {:attribute [:isPermaLink "false"]}) 135 | '({:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aaaa-111111111111"]}))) 136 | (t/is (= (helpers/find-all testing-data {:attribute [:isPermaLink "true"]}) 137 | '({:tag :guid :attrs {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-bbbb-222222222222"]}))) 138 | (t/is (= (helpers/find-all testing-data {:attribute [:year "2013"]}) 139 | '({:tag :pubDate :attrs {:year "2013"} :content ["Sun, 06 Sep 2013 16:20:00 +0000"]}))) 140 | (t/is (= (helpers/find-all testing-data {:attribute [:year "2009"]}) 141 | '({:tag :pubDate :attrs {:year "2009"} :content ["Sun, 06 Sep 2009 16:20:00 +0000"]}))) 142 | (t/is (= (helpers/find-all testing-data {:attribute [:year "2010"]}) 143 | '())) 144 | (t/is (= (helpers/find-all testing-data {:attribute [:not-existing true]}) 145 | '())) 146 | (t/is (= (helpers/find-all testing-data {:attribute [:shouldfail]}) 147 | '())))) 148 | -------------------------------------------------------------------------------- /doc/index.adoc: -------------------------------------------------------------------------------- 1 | = Tubax 2 | Alonso Torres, 3 | :toc: left 4 | :numbered: 5 | :source-highlighter: coderay 6 | :sectlinks: 7 | 8 | == Introduction 9 | 10 | *Tubax* is a library to parse and convert XML raw data into native Clojurescript data structures. 11 | 12 | It uses https://github.com/isaacs/sax-js[sax.js] under the hood to provide a fast way to convert from XML. 13 | 14 | == Rationale 15 | 16 | Currently there is no good way to parse XML and other markup languages with Clojurescript. There are no Clojurescript-based libraries and most of the Javascript ones require access to the DOM. 17 | 18 | This last point is critical because HTML5 Web Workers don't have access to these APIs so an alternative is necessary. 19 | 20 | Another alternative to XML processing is to go to a middle-ground. There are some libraries that will parse XML into a JSON format. 21 | 22 | The problem with these is that JSON is not a faithfull representation of the XML format. There are some XML that couldn't be represented as JSON. 23 | 24 | For example, the following XML will loss information when transformed into JSON. 25 | 26 | [source,xml] 27 | ---- 28 | 29 | A 30 | B 31 | A 32 | 33 | ---- 34 | 35 | Another main objective of *tubax* is to be fully compatible with the `clojure.xml` format so we can access the functionality currently in the Clojure API like zippers. 36 | 37 | == Install 38 | 39 | WARNING: Not on clojars yet. I'll update the information when it's available 40 | 41 | // If you're using leingen just include it in your 42 | // 43 | // [source,clojure] 44 | // ---- 45 | // [funcool/tubax "0.1.0"] 46 | // ---- 47 | 48 | == Usage 49 | 50 | All examples will use this XML as if it existed in a `(def xml-data "...")` definition. 51 | 52 | [source,xml] 53 | ---- 54 | 55 | 56 | RSS Title 57 | This is an example of an RSS feed 58 | http://www.example.com/main.html 59 | Mon, 06 Sep 2010 00:01:00 +0000 60 | Sun, 06 Sep 2009 16:20:00 +0000 61 | 1800 62 | 63 | Example entry 64 | Here is some text containing an interesting description. 65 | http://www.example.com/blog/post/1 66 | 7bd204c6-1655-4c27-aeee-53f933c5395f 67 | Sun, 06 Sep 2009 16:20:00 +0000 68 | 69 | 70 | Example entry2 71 | Here is some text containing an interesting description. 72 | http://www.example.com/blog/post/1 73 | 7bd204c6-1655-4c27-aeee-53f933c5395f 74 | Sun, 06 Sep 2009 16:20:00 +0000 75 | 76 | 77 | 78 | ---- 79 | 80 | === Basic usage 81 | 82 | In order to parse a XML file you only have to make a call to the `xml->clj` function 83 | 84 | [source,clojure] 85 | ---- 86 | (require '[tubax.core :refer [xml->clj]]) 87 | 88 | (xml->clj xml-data) 89 | ---- 90 | 91 | === Additional options 92 | 93 | The library bundles https://github.com/isaacs/sax-js[sax.js library] as it's main dependency. You can pass the following options to the conversion to customize some behaviour. 94 | 95 | ==== Strict mode 96 | 97 | *default* true 98 | 99 | When not in _strict mode_ the parser will be more forgiving on XML structure. If in strict mode, when there is a format failure the parsing will throw an exception. 100 | 101 | WARNING: Some "loosy" formats could cause unexpected behaviour so it's not recommended. 102 | 103 | [source,clojure] 104 | ---- 105 | (def xml-data "") 106 | 107 | (core/xml->clj xml-data {:strict false}) 108 | 109 | ;; => {:tag :a :attributes {} :content {:tag :b :attributes {} :content []}} 110 | 111 | (core/xml->clj xml-data {:strict true}) 112 | 113 | ;; => js/Error #Parse error 114 | ---- 115 | 116 | ==== Trim whitespaces 117 | 118 | *default* true 119 | 120 | This option will make the parsing to remove all the leading and trailing whitespaces in the text nodes. 121 | 122 | [source,clojure] 123 | ---- 124 | (def xml-data " test ") 125 | 126 | (core/xml->clj xml-data {:trim false}) 127 | 128 | ;; => {:tag :a :attributes {} :content [" test "]} 129 | 130 | (core/xml->clj xml-data {:trim true}) 131 | 132 | ;; => {:tag :a :attributes {} :content ["test"]} 133 | ---- 134 | 135 | ==== Normalize whitespaces 136 | 137 | *default* false 138 | 139 | Replace all whitespaces-characters (like tabs, end of lines, etc..) for whitespaces. 140 | 141 | [source,clojure] 142 | ---- 143 | (def xml-data "normalize\ntest") 144 | 145 | (core/xml->clj xml-data {:normalize false}) 146 | 147 | ;; => {:tag :a :attributes {} :content ["normalize\ntest"]} 148 | 149 | (core/xml->clj xml-data {:normalize true}) 150 | 151 | ;; => {:tag :a :attributes {} :content ["normalize test"]} 152 | ---- 153 | 154 | ==== Lowercase (non-strict mode only) 155 | 156 | *default* true 157 | 158 | When on non-strict mode, all tags and attributes can be made upper-case just by setting this option. 159 | 160 | [source,clojure] 161 | ---- 162 | (def xml-data "test") 163 | 164 | (core/xml->clj xml-data {:strict false :lowercase true}) 165 | 166 | ;; => {:tag :root :attributes {:att1 "t1"} :content ["test"]} 167 | 168 | (core/xml->clj xml-data {:strict false :lowercase false}) 169 | 170 | ;; => {:tag :ROOT :attributes {:ATT1 "t1"} :content ["test"]} 171 | ---- 172 | 173 | ==== Support for XML namespaces 174 | 175 | *default* false 176 | 177 | By default there is no additional data when a http://en.wikipedia.org/wiki/XML_namespace[XML namespace] is found. 178 | 179 | When the option _xmlns_ is activated there will be more information regarding the namespaces inside the node elements. 180 | 181 | [source,clojure] 182 | ---- 183 | (def xml-data "value") 184 | 185 | (core/xml->clj xml-data {:xmlns false}) 186 | 187 | ;; => {:tag :element :attributes {:xmlns "http://foo"} :content ["value"]} 188 | 189 | (core/xml->clj xml-data {:xmlns true}) 190 | 191 | ;; => {:tag :element :attributes {:xmlns {:name "xmlns" :value "http://foo" :prefix "xmlns" :local "" :uri "http://www.w3.org/2000/xmlns/"}} :content ["value"]} 192 | ---- 193 | 194 | ==== Strict entities 195 | 196 | *default* false 197 | 198 | When activated, it makes the parser to fail when it founds http://www.w3.org/TR/REC-xml/#sec-predefined-ent[a non-predefined entity] 199 | 200 | [source,clojure] 201 | ---- 202 | (def xml-data "á") 203 | 204 | (core/xml->clj xml-data {:strict-entities false}) 205 | 206 | ;; => {:tag :element :attributes {} :content ["á"]} 207 | 208 | (core/xml->clj xml-data {:strict-entities true}) 209 | 210 | ;; => js/Error #Parser error 211 | ---- 212 | 213 | === Utility functions 214 | 215 | [source,clojure] 216 | ---- 217 | (require '[tubax.helpers :as th]) 218 | ---- 219 | 220 | For simplicity the following examples suppose: 221 | 222 | [source,clojure] 223 | ---- 224 | (require '[tubax.core :refer [xml->clj]]) 225 | 226 | (def result (xml->clj xml-data)) 227 | ---- 228 | 229 | ==== Access data-structure 230 | 231 | [source,clojure] 232 | ---- 233 | (th/tag {:tag :item :attribute {} :content ["Text"]}) 234 | ;; => :item 235 | ---- 236 | 237 | [source,clojure] 238 | ---- 239 | (th/attributes {:tag :item :attribute {} :content ["Text"]}) 240 | ;; => {} 241 | ---- 242 | 243 | [source,clojure] 244 | ---- 245 | (th/children {:tag :item :attribute {} :content ["Text"]}) 246 | ;; => ["Text"] 247 | ---- 248 | 249 | [source,clojure] 250 | ---- 251 | (th/text {:tag :item :attribute {} :content ["Text"]}) 252 | ;; => Text 253 | 254 | (th/text {:tag :item {} :content [{:tag :item :attributes {} :content [...]}]}) 255 | ;; => nil 256 | ---- 257 | 258 | ==== Find first node 259 | 260 | These methods retrieve the first node that match the query passed as argument. 261 | 262 | [source,clojure] 263 | ---- 264 | (th/find-first result {:tag :item}) 265 | 266 | ;; => {:tag :item :attributes {} :content [{:content :title :attributes {} :content ["Hello world"]}]} 267 | ---- 268 | 269 | [source,clojure] 270 | ---- 271 | (th/find-first result {:path [:rss :channel :description]}) 272 | 273 | ;; => {:tag :description :attributes {} :content ["This is an example of an RSS feed"]} 274 | ---- 275 | 276 | Search for the first element that have the attribute defined 277 | 278 | [source,clojure] 279 | ---- 280 | (th/find-first result {:attribute :isPermaLink}) 281 | 282 | ;; => {:tag :guid :attributes {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]} 283 | ---- 284 | 285 | Search for the first element that have an attribute with the specified value 286 | 287 | [source,clojure] 288 | ---- 289 | (th/find-first result {:attribute [:isPermaLink true]}) 290 | 291 | ;; => {:tag :guid :attributes {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]} 292 | ---- 293 | 294 | ==== Find all nodes 295 | 296 | These methods retrieve a lazy sequence with the elements which match the query used as argument. 297 | 298 | [source,clojure] 299 | ---- 300 | (th/find-all result {:tag :link}) 301 | 302 | ;; => ({:tag :link :attributes {} :content ["http://www.example.com/main.html"]} 303 | ;; {:tag :link :attributes {} :content ["http://www.example.com/blog/post/1"]}) 304 | ---- 305 | 306 | [source,clojure] 307 | ---- 308 | (th/find-all result {:path [:rss :channel :item :title]}) 309 | 310 | ;; => ({:tag :title :attributes {} :content ["Example entry"]} 311 | ;; {:tag :title :attributes {} :content ["Example entry2"]}) 312 | ---- 313 | 314 | [source,clojure] 315 | ---- 316 | (th/find-all result {:attribute :isPermaLink}) 317 | 318 | ;; => ({:tag :guid :attributes {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]} 319 | ;; {:tag :guid :attributes {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]}) 320 | ---- 321 | 322 | [source,clojure] 323 | ---- 324 | (th/find-all result {:attribute [:isPermaLink "true"]}) 325 | 326 | ;; => ({:tag :guid :attributes {:isPermaLink "true"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]}) 327 | ---- 328 | 329 | == Contribute 330 | 331 | Tubax does not have many restrictions for contributions. Just open an issue or pull request. 332 | 333 | == Runing tests 334 | 335 | [source] 336 | ---- 337 | lein test 338 | ---- 339 | 340 | == License 341 | 342 | This library is under the https://www.apache.org/licenses/LICENSE-2.0[Apache 2.0 License]. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /test/tubax/tests/core_test.cljs: -------------------------------------------------------------------------------- 1 | (ns tubax.tests.core-test 2 | (:require 3 | [tubax.core :as core] 4 | [cljs.test :as t])) 5 | 6 | ;; TEST SUCCESS 7 | (t/deftest parser-case1 8 | (t/testing "Case 1 - Empty element" 9 | (let [xml "" 10 | result (-> xml core/xml->clj)] 11 | (t/is (= result {:tag :element :attrs nil :content nil}))))) 12 | 13 | (t/deftest parser-case2 14 | (t/testing "Case 2 - Empty element with attributes" 15 | (let [xml "" 16 | result (-> xml core/xml->clj)] 17 | (t/is (= result {:tag :element :attrs {:att1 "a" :att2 "b"} :content nil}))))) 18 | 19 | (t/deftest parser-case3 20 | (t/testing "Case 3 - Text element" 21 | (let [xml "value" 22 | result (-> xml core/xml->clj)] 23 | (t/is (= result {:tag :element :attrs nil :content ["value"]}))))) 24 | 25 | (t/deftest parser-case4 26 | (t/testing "Case 4 - Text + attributes element" 27 | (let [xml "value" 28 | result (-> xml core/xml->clj)] 29 | (t/is (= result {:tag :element :attrs {:att1 "a" :att2 "b"} :content ["value"]}))))) 30 | 31 | (t/deftest parser-case5 32 | (t/testing "Case 5 - Subtree elements (no repetition)" 33 | (let [xml " 34 | 1 35 | 2 36 | 3 37 | " 38 | result (-> xml core/xml->clj)] 39 | (t/is (= result {:tag :element 40 | :attrs nil 41 | :content 42 | [{:tag :elem-a :attrs nil :content ["1"]} 43 | {:tag :elem-b :attrs nil :content ["2"]} 44 | {:tag :elem-c :attrs nil :content ["3"]}]}))))) 45 | 46 | (t/deftest parser-case6 47 | (t/testing "Case 6 - Subtree elements (repetition)" 48 | (let [xml " 49 | 1 50 | 2 51 | 3 52 | " 53 | result (-> xml core/xml->clj)] 54 | (t/is (= result {:tag :element 55 | :attrs nil 56 | :content 57 | [{:tag :elem-a :attrs nil :content ["1"]} 58 | {:tag :elem-a :attrs nil :content ["2"]} 59 | {:tag :elem-b :attrs nil :content ["3"]}]}))))) 60 | 61 | (t/deftest parser-case7 62 | (t/testing "Case 7 - Nested tree" 63 | (let [xml " 64 | 65 | 66 | test 67 | 68 | 69 | " 70 | result (-> xml core/xml->clj)] 71 | (t/is (= result {:tag :element 72 | :attrs nil 73 | :content 74 | [{:tag :elem-a 75 | :attrs nil 76 | :content 77 | [{:tag :elem-b 78 | :attrs nil 79 | :content 80 | [{:tag :elem-c 81 | :attrs nil 82 | :content ["test"]}]}]}]}))))) 83 | 84 | (t/deftest parser-case8 85 | (t/testing "Case 8 - Nested tree with children" 86 | (let [xml " 87 | 88 | 89 | test1 90 | test2 91 | test3 92 | 93 | 94 | other 95 | " 96 | result (-> xml core/xml->clj)] 97 | (t/is (= result 98 | {:tag :element 99 | :attrs nil 100 | :content 101 | [{:tag :elem-a 102 | :attrs nil 103 | :content 104 | [{:tag :elem-b 105 | :attrs nil 106 | :content 107 | [{:tag :elem-c :attrs nil :content ["test1"]} 108 | {:tag :elem-c :attrs nil :content ["test2"]} 109 | {:tag :elem-c :attrs nil :content ["test3"]}]}]} 110 | {:tag :elem-a 111 | :attrs nil 112 | :content ["other"]}]}))))) 113 | 114 | (t/deftest parser-full 115 | (t/testing "Full parser" 116 | (let [xml " 117 | 118 | RSS Title 119 | This is an example of an RSS feed 120 | http://www.example.com/main.html 121 | Mon, 06 Sep 2010 00:01:00 +0000 122 | Sun, 06 Sep 2009 16:20:00 +0000 123 | 1800 124 | 125 | Example entry 126 | Here is some text containing an interesting description. 127 | http://www.example.com/blog/post/1 128 | 7bd204c6-1655-4c27-aeee-53f933c5395f 129 | Sun, 06 Sep 2009 16:20:00 +0000 130 | 131 | 132 | Example entry2 133 | Here is some text containing an interesting description. 134 | http://www.example.com/blog/post/1 135 | 7bd204c6-1655-4c27-aeee-53f933c5395f 136 | Sun, 06 Sep 2009 16:20:00 +0000 137 | 138 | 139 | " 140 | result (-> xml core/xml->clj)] 141 | (t/is (= result 142 | {:tag :rss :attrs {:version "2.0"} 143 | :content 144 | [{:tag :channel :attrs nil 145 | :content 146 | [{:tag :title :attrs nil :content ["RSS Title"]} 147 | {:tag :description :attrs nil :content ["This is an example of an RSS feed"]} 148 | {:tag :link :attrs nil :content ["http://www.example.com/main.html"]} 149 | {:tag :lastBuildDate :attrs nil :content ["Mon, 06 Sep 2010 00:01:00 +0000"]} 150 | {:tag :pubDate :attrs nil :content ["Sun, 06 Sep 2009 16:20:00 +0000"]} 151 | {:tag :ttl :attrs nil :content ["1800"]} 152 | {:tag :item :attrs nil 153 | :content 154 | [{:tag :title :attrs nil :content ["Example entry"]} 155 | {:tag :description :attrs nil :content ["Here is some text containing an interesting description."]} 156 | {:tag :link :attrs nil :content ["http://www.example.com/blog/post/1"]} 157 | {:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]} 158 | {:tag :pubDate :attrs nil :content ["Sun, 06 Sep 2009 16:20:00 +0000"]}]} 159 | {:tag :item :attrs nil 160 | :content 161 | [{:tag :title :attrs nil :content ["Example entry2"]} 162 | {:tag :description :attrs nil :content ["Here is some text containing an interesting description."]} 163 | {:tag :link :attrs nil :content ["http://www.example.com/blog/post/1"]} 164 | {:tag :guid :attrs {:isPermaLink "false"} :content ["7bd204c6-1655-4c27-aeee-53f933c5395f"]} 165 | {:tag :pubDate :attrs nil :content ["Sun, 06 Sep 2009 16:20:00 +0000"]}]}]}]}))))) 166 | 167 | 168 | ;;; TEST ERRORS 169 | (t/deftest parser-error1 170 | (t/testing "Error 1 - XML Syntax error" 171 | (let [xml ""] 172 | (t/is (thrown? js/Error (-> xml core/xml->clj)))))) 173 | 174 | (t/deftest parser-error2 175 | (t/testing "Error 2 - Unfinished XML" 176 | (let [xml ""] 177 | (t/is (thrown? js/Error (-> xml core/xml->clj)))))) 178 | 179 | ;;; TEST OPTIONS 180 | (t/deftest parser-options-strict 181 | (t/testing "Option 1 - Strict mode" 182 | (let [xml ""] 183 | (t/is (thrown? js/Error (= (core/xml->clj xml {:strict true})))) 184 | (t/is (= (core/xml->clj xml {:strict false}) 185 | {:tag :element 186 | :attrs nil 187 | :content 188 | [{:tag :a 189 | :attrs nil 190 | :content 191 | [{:tag :b :attrs nil :content nil}]}]}))) 192 | 193 | (let [xml ""] 194 | (t/is (thrown? js/Error (= (core/xml->clj xml {:strict true})))) 195 | (t/is (= (core/xml->clj xml {:strict false}) 196 | {:tag :element 197 | :attrs nil 198 | :content 199 | [{:tag :a 200 | :attrs nil 201 | :content 202 | [{:tag :b 203 | :attrs nil 204 | :content 205 | [{:tag :c 206 | :attrs nil 207 | :content nil}]}]}]}))))) 208 | 209 | (t/deftest parser-options-trim 210 | (t/testing "Option 2 - Trim" 211 | (let [xml " test "] 212 | (t/is (= (core/xml->clj xml {:trim false}) {:tag :element :attrs nil :content [" test "]})) 213 | (t/is (= (core/xml->clj xml {:trim true}) {:tag :element :attrs nil :content ["test"]})) 214 | (t/is (= (core/xml->clj xml) {:tag :element :attrs nil :content ["test"]}))))) 215 | 216 | (t/deftest parser-options-normalize 217 | (t/testing "Option 3 - Normalize" 218 | (let [xml "t/testing\nnormalize"] 219 | (t/is (= (core/xml->clj xml {:normalize false}) {:tag :element :attrs nil :content ["t/testing\nnormalize"]})) 220 | (t/is (= (core/xml->clj xml {:normalize true}) {:tag :element :attrs nil :content ["t/testing normalize"]})) 221 | (t/is (= (core/xml->clj xml) {:tag :element :attrs nil :content ["t/testing\nnormalize"]}))))) 222 | 223 | (t/deftest parser-options-lowercase 224 | (t/testing "Option 4 - Lowercase" 225 | (let [xml "test"] 226 | (t/is (= (core/xml->clj xml {:strict false :lowercase false}) {:tag :ELEMENT :attrs {:ATT1 "att"} :content ["test"]})) 227 | (t/is (= (core/xml->clj xml {:strict false :lowercase true}) {:tag :element :attrs {:att1 "att"} :content ["test"]})) 228 | (t/is (= (core/xml->clj xml {:strict false}) {:tag :element :attrs {:att1 "att"} :content ["test"]}))))) 229 | 230 | (t/deftest parser-options-xmlns 231 | (t/testing "Option 5 - XMLNS" 232 | (let [xml " 233 | a 234 | b 235 | "] 236 | (t/is (= (core/xml->clj xml {:xmlns false}) 237 | {:tag :element :attrs {:xmlns "http://foo", :xmlns:t "http://t", :t:att1 "att"} 238 | :content 239 | [{:tag :t:test :attrs nil :content ["a"]} 240 | {:tag :test :attrs nil :content ["b"]}]})) 241 | 242 | (t/is (= (core/xml->clj xml {:xmlns true}) 243 | {:tag :element 244 | :attrs 245 | {:xmlns {:name "xmlns" 246 | :value "http://foo" 247 | :prefix "xmlns" 248 | :local "" 249 | :uri "http://www.w3.org/2000/xmlns/"} 250 | :xmlns:t {:name "xmlns:t" 251 | :value "http://t" 252 | :prefix "xmlns" 253 | :local "t" 254 | :uri "http://www.w3.org/2000/xmlns/"} 255 | :t:att1 {:name "t:att1" 256 | :value "att" 257 | :prefix "t" 258 | :local "att1" 259 | :uri "http://t"}} 260 | :content 261 | [{:tag :t:test :attrs nil :content ["a"]} 262 | {:tag :test :attrs nil :content ["b"]}]})) 263 | (t/is (= (core/xml->clj xml) 264 | {:tag :element 265 | :attrs {:xmlns "http://foo", :xmlns:t "http://t", :t:att1 "att"} 266 | :content 267 | [{:tag :t:test :attrs nil :content ["a"]} 268 | {:tag :test :attrs nil :content ["b"]}]}))))) 269 | 270 | 271 | (t/deftest parser-options-strict-entities 272 | (t/testing "Option 6 - Strict Entities" 273 | (let [xml "&<á"] 274 | (t/is (= (core/xml->clj xml {:strict-entities false}) {:tag :element :attrs {:att1 "&<á"} :content ["&<á"]})) 275 | (t/is (thrown? js/Error (= (core/xml->clj xml {:strict-entities true})))) 276 | (t/is (= (core/xml->clj xml) {:tag :element :attrs {:att1 "&<á"} :content ["&<á"]}))))) 277 | 278 | ;; TEST CDATA 279 | (t/deftest parser-cdata 280 | (t/testing "Cdata value simple value" 281 | (let [xml "" 282 | result (-> xml core/xml->clj)] 283 | (t/is (= result {:tag :element :attrs nil :content ["value"]})))) 284 | (t/testing "Cdata value multiple lines" 285 | (let [xml "" 286 | result (-> xml core/xml->clj)] 287 | (t/is (= result {:tag :element :attrs nil :content ["value\n\n\nvalue"]})))) 288 | (t/testing "Cdata value with xml inside" 289 | (let [xml "]]>" 290 | result (-> xml core/xml->clj)] 291 | (t/is (= result {:tag :element :attrs nil :content [""]}))))) 292 | 293 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | buffer: 12 | specifier: ^6.0.3 13 | version: 6.0.3 14 | sax: 15 | specifier: ^1.4.3 16 | version: 1.4.3 17 | stream: 18 | specifier: ^0.0.3 19 | version: 0.0.3 20 | string_decoder: 21 | specifier: ^1.3.0 22 | version: 1.3.0 23 | devDependencies: 24 | '@types/node': 25 | specifier: ^22.0.0 26 | version: 22.19.1 27 | concurrently: 28 | specifier: ^9.2.1 29 | version: 9.2.1 30 | nodemon: 31 | specifier: ^3.1.11 32 | version: 3.1.11 33 | source-map-support: 34 | specifier: ^0.5.21 35 | version: 0.5.21 36 | ws: 37 | specifier: ^8.18.0 38 | version: 8.18.3 39 | 40 | packages: 41 | 42 | '@types/node@22.19.1': 43 | resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} 44 | 45 | ansi-regex@5.0.1: 46 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 47 | engines: {node: '>=8'} 48 | 49 | ansi-styles@4.3.0: 50 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 51 | engines: {node: '>=8'} 52 | 53 | anymatch@3.1.3: 54 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 55 | engines: {node: '>= 8'} 56 | 57 | balanced-match@1.0.2: 58 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 59 | 60 | base64-js@1.5.1: 61 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} 62 | 63 | binary-extensions@2.3.0: 64 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 65 | engines: {node: '>=8'} 66 | 67 | brace-expansion@1.1.12: 68 | resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 69 | 70 | braces@3.0.3: 71 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 72 | engines: {node: '>=8'} 73 | 74 | buffer-from@1.1.2: 75 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 76 | 77 | buffer@6.0.3: 78 | resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} 79 | 80 | chalk@4.1.2: 81 | resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 82 | engines: {node: '>=10'} 83 | 84 | chokidar@3.6.0: 85 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 86 | engines: {node: '>= 8.10.0'} 87 | 88 | cliui@8.0.1: 89 | resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} 90 | engines: {node: '>=12'} 91 | 92 | color-convert@2.0.1: 93 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 94 | engines: {node: '>=7.0.0'} 95 | 96 | color-name@1.1.4: 97 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 98 | 99 | component-emitter@2.0.0: 100 | resolution: {integrity: sha512-4m5s3Me2xxlVKG9PkZpQqHQR7bgpnN7joDMJ4yvVkVXngjoITG76IaZmzmywSeRTeTpc6N6r3H3+KyUurV8OYw==} 101 | engines: {node: '>=18'} 102 | 103 | concat-map@0.0.1: 104 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 105 | 106 | concurrently@9.2.1: 107 | resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} 108 | engines: {node: '>=18'} 109 | hasBin: true 110 | 111 | debug@4.4.3: 112 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} 113 | engines: {node: '>=6.0'} 114 | peerDependencies: 115 | supports-color: '*' 116 | peerDependenciesMeta: 117 | supports-color: 118 | optional: true 119 | 120 | emoji-regex@8.0.0: 121 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 122 | 123 | escalade@3.2.0: 124 | resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 125 | engines: {node: '>=6'} 126 | 127 | fill-range@7.1.1: 128 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 129 | engines: {node: '>=8'} 130 | 131 | fsevents@2.3.3: 132 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 133 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 134 | os: [darwin] 135 | 136 | get-caller-file@2.0.5: 137 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 138 | engines: {node: 6.* || 8.* || >= 10.*} 139 | 140 | glob-parent@5.1.2: 141 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 142 | engines: {node: '>= 6'} 143 | 144 | has-flag@3.0.0: 145 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 146 | engines: {node: '>=4'} 147 | 148 | has-flag@4.0.0: 149 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 150 | engines: {node: '>=8'} 151 | 152 | ieee754@1.2.1: 153 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} 154 | 155 | ignore-by-default@1.0.1: 156 | resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} 157 | 158 | is-binary-path@2.1.0: 159 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 160 | engines: {node: '>=8'} 161 | 162 | is-extglob@2.1.1: 163 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 164 | engines: {node: '>=0.10.0'} 165 | 166 | is-fullwidth-code-point@3.0.0: 167 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 168 | engines: {node: '>=8'} 169 | 170 | is-glob@4.0.3: 171 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 172 | engines: {node: '>=0.10.0'} 173 | 174 | is-number@7.0.0: 175 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 176 | engines: {node: '>=0.12.0'} 177 | 178 | minimatch@3.1.2: 179 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 180 | 181 | ms@2.1.3: 182 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 183 | 184 | nodemon@3.1.11: 185 | resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==} 186 | engines: {node: '>=10'} 187 | hasBin: true 188 | 189 | normalize-path@3.0.0: 190 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 191 | engines: {node: '>=0.10.0'} 192 | 193 | picomatch@2.3.1: 194 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 195 | engines: {node: '>=8.6'} 196 | 197 | pstree.remy@1.1.8: 198 | resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} 199 | 200 | readdirp@3.6.0: 201 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 202 | engines: {node: '>=8.10.0'} 203 | 204 | require-directory@2.1.1: 205 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 206 | engines: {node: '>=0.10.0'} 207 | 208 | rxjs@7.8.2: 209 | resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} 210 | 211 | safe-buffer@5.2.1: 212 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 213 | 214 | sax@1.4.3: 215 | resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} 216 | 217 | semver@7.7.3: 218 | resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} 219 | engines: {node: '>=10'} 220 | hasBin: true 221 | 222 | shell-quote@1.8.3: 223 | resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} 224 | engines: {node: '>= 0.4'} 225 | 226 | simple-update-notifier@2.0.0: 227 | resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} 228 | engines: {node: '>=10'} 229 | 230 | source-map-support@0.5.21: 231 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 232 | 233 | source-map@0.6.1: 234 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 235 | engines: {node: '>=0.10.0'} 236 | 237 | stream@0.0.3: 238 | resolution: {integrity: sha512-aMsbn7VKrl4A2T7QAQQbzgN7NVc70vgF5INQrBXqn4dCXN1zy3L9HGgLO5s7PExmdrzTJ8uR/27aviW8or8/+A==} 239 | 240 | string-width@4.2.3: 241 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 242 | engines: {node: '>=8'} 243 | 244 | string_decoder@1.3.0: 245 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} 246 | 247 | strip-ansi@6.0.1: 248 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 249 | engines: {node: '>=8'} 250 | 251 | supports-color@5.5.0: 252 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 253 | engines: {node: '>=4'} 254 | 255 | supports-color@7.2.0: 256 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 257 | engines: {node: '>=8'} 258 | 259 | supports-color@8.1.1: 260 | resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} 261 | engines: {node: '>=10'} 262 | 263 | to-regex-range@5.0.1: 264 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 265 | engines: {node: '>=8.0'} 266 | 267 | touch@3.1.1: 268 | resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} 269 | hasBin: true 270 | 271 | tree-kill@1.2.2: 272 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 273 | hasBin: true 274 | 275 | tslib@2.8.1: 276 | resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} 277 | 278 | undefsafe@2.0.5: 279 | resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} 280 | 281 | undici-types@6.21.0: 282 | resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} 283 | 284 | wrap-ansi@7.0.0: 285 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 286 | engines: {node: '>=10'} 287 | 288 | ws@8.18.3: 289 | resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} 290 | engines: {node: '>=10.0.0'} 291 | peerDependencies: 292 | bufferutil: ^4.0.1 293 | utf-8-validate: '>=5.0.2' 294 | peerDependenciesMeta: 295 | bufferutil: 296 | optional: true 297 | utf-8-validate: 298 | optional: true 299 | 300 | y18n@5.0.8: 301 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 302 | engines: {node: '>=10'} 303 | 304 | yargs-parser@21.1.1: 305 | resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} 306 | engines: {node: '>=12'} 307 | 308 | yargs@17.7.2: 309 | resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} 310 | engines: {node: '>=12'} 311 | 312 | snapshots: 313 | 314 | '@types/node@22.19.1': 315 | dependencies: 316 | undici-types: 6.21.0 317 | 318 | ansi-regex@5.0.1: {} 319 | 320 | ansi-styles@4.3.0: 321 | dependencies: 322 | color-convert: 2.0.1 323 | 324 | anymatch@3.1.3: 325 | dependencies: 326 | normalize-path: 3.0.0 327 | picomatch: 2.3.1 328 | 329 | balanced-match@1.0.2: {} 330 | 331 | base64-js@1.5.1: {} 332 | 333 | binary-extensions@2.3.0: {} 334 | 335 | brace-expansion@1.1.12: 336 | dependencies: 337 | balanced-match: 1.0.2 338 | concat-map: 0.0.1 339 | 340 | braces@3.0.3: 341 | dependencies: 342 | fill-range: 7.1.1 343 | 344 | buffer-from@1.1.2: {} 345 | 346 | buffer@6.0.3: 347 | dependencies: 348 | base64-js: 1.5.1 349 | ieee754: 1.2.1 350 | 351 | chalk@4.1.2: 352 | dependencies: 353 | ansi-styles: 4.3.0 354 | supports-color: 7.2.0 355 | 356 | chokidar@3.6.0: 357 | dependencies: 358 | anymatch: 3.1.3 359 | braces: 3.0.3 360 | glob-parent: 5.1.2 361 | is-binary-path: 2.1.0 362 | is-glob: 4.0.3 363 | normalize-path: 3.0.0 364 | readdirp: 3.6.0 365 | optionalDependencies: 366 | fsevents: 2.3.3 367 | 368 | cliui@8.0.1: 369 | dependencies: 370 | string-width: 4.2.3 371 | strip-ansi: 6.0.1 372 | wrap-ansi: 7.0.0 373 | 374 | color-convert@2.0.1: 375 | dependencies: 376 | color-name: 1.1.4 377 | 378 | color-name@1.1.4: {} 379 | 380 | component-emitter@2.0.0: {} 381 | 382 | concat-map@0.0.1: {} 383 | 384 | concurrently@9.2.1: 385 | dependencies: 386 | chalk: 4.1.2 387 | rxjs: 7.8.2 388 | shell-quote: 1.8.3 389 | supports-color: 8.1.1 390 | tree-kill: 1.2.2 391 | yargs: 17.7.2 392 | 393 | debug@4.4.3(supports-color@5.5.0): 394 | dependencies: 395 | ms: 2.1.3 396 | optionalDependencies: 397 | supports-color: 5.5.0 398 | 399 | emoji-regex@8.0.0: {} 400 | 401 | escalade@3.2.0: {} 402 | 403 | fill-range@7.1.1: 404 | dependencies: 405 | to-regex-range: 5.0.1 406 | 407 | fsevents@2.3.3: 408 | optional: true 409 | 410 | get-caller-file@2.0.5: {} 411 | 412 | glob-parent@5.1.2: 413 | dependencies: 414 | is-glob: 4.0.3 415 | 416 | has-flag@3.0.0: {} 417 | 418 | has-flag@4.0.0: {} 419 | 420 | ieee754@1.2.1: {} 421 | 422 | ignore-by-default@1.0.1: {} 423 | 424 | is-binary-path@2.1.0: 425 | dependencies: 426 | binary-extensions: 2.3.0 427 | 428 | is-extglob@2.1.1: {} 429 | 430 | is-fullwidth-code-point@3.0.0: {} 431 | 432 | is-glob@4.0.3: 433 | dependencies: 434 | is-extglob: 2.1.1 435 | 436 | is-number@7.0.0: {} 437 | 438 | minimatch@3.1.2: 439 | dependencies: 440 | brace-expansion: 1.1.12 441 | 442 | ms@2.1.3: {} 443 | 444 | nodemon@3.1.11: 445 | dependencies: 446 | chokidar: 3.6.0 447 | debug: 4.4.3(supports-color@5.5.0) 448 | ignore-by-default: 1.0.1 449 | minimatch: 3.1.2 450 | pstree.remy: 1.1.8 451 | semver: 7.7.3 452 | simple-update-notifier: 2.0.0 453 | supports-color: 5.5.0 454 | touch: 3.1.1 455 | undefsafe: 2.0.5 456 | 457 | normalize-path@3.0.0: {} 458 | 459 | picomatch@2.3.1: {} 460 | 461 | pstree.remy@1.1.8: {} 462 | 463 | readdirp@3.6.0: 464 | dependencies: 465 | picomatch: 2.3.1 466 | 467 | require-directory@2.1.1: {} 468 | 469 | rxjs@7.8.2: 470 | dependencies: 471 | tslib: 2.8.1 472 | 473 | safe-buffer@5.2.1: {} 474 | 475 | sax@1.4.3: {} 476 | 477 | semver@7.7.3: {} 478 | 479 | shell-quote@1.8.3: {} 480 | 481 | simple-update-notifier@2.0.0: 482 | dependencies: 483 | semver: 7.7.3 484 | 485 | source-map-support@0.5.21: 486 | dependencies: 487 | buffer-from: 1.1.2 488 | source-map: 0.6.1 489 | 490 | source-map@0.6.1: {} 491 | 492 | stream@0.0.3: 493 | dependencies: 494 | component-emitter: 2.0.0 495 | 496 | string-width@4.2.3: 497 | dependencies: 498 | emoji-regex: 8.0.0 499 | is-fullwidth-code-point: 3.0.0 500 | strip-ansi: 6.0.1 501 | 502 | string_decoder@1.3.0: 503 | dependencies: 504 | safe-buffer: 5.2.1 505 | 506 | strip-ansi@6.0.1: 507 | dependencies: 508 | ansi-regex: 5.0.1 509 | 510 | supports-color@5.5.0: 511 | dependencies: 512 | has-flag: 3.0.0 513 | 514 | supports-color@7.2.0: 515 | dependencies: 516 | has-flag: 4.0.0 517 | 518 | supports-color@8.1.1: 519 | dependencies: 520 | has-flag: 4.0.0 521 | 522 | to-regex-range@5.0.1: 523 | dependencies: 524 | is-number: 7.0.0 525 | 526 | touch@3.1.1: {} 527 | 528 | tree-kill@1.2.2: {} 529 | 530 | tslib@2.8.1: {} 531 | 532 | undefsafe@2.0.5: {} 533 | 534 | undici-types@6.21.0: {} 535 | 536 | wrap-ansi@7.0.0: 537 | dependencies: 538 | ansi-styles: 4.3.0 539 | string-width: 4.2.3 540 | strip-ansi: 6.0.1 541 | 542 | ws@8.18.3: {} 543 | 544 | y18n@5.0.8: {} 545 | 546 | yargs-parser@21.1.1: {} 547 | 548 | yargs@17.7.2: 549 | dependencies: 550 | cliui: 8.0.1 551 | escalade: 3.2.0 552 | get-caller-file: 2.0.5 553 | require-directory: 2.1.1 554 | string-width: 4.2.3 555 | y18n: 5.0.8 556 | yargs-parser: 21.1.1 557 | --------------------------------------------------------------------------------