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