Skip to content

Commit 03d66e3

Browse files
committed
Add a JSON extention
1 parent 5e26b40 commit 03d66e3

File tree

7 files changed

+150
-45
lines changed

7 files changed

+150
-45
lines changed

Project.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ version = "0.3.4"
77
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
88
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
99

10+
[weakdeps]
11+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
12+
13+
[extensions]
14+
XMLJSONExt = ["JSON"]
15+
1016
[compat]
17+
JSON = "0.21"
1118
OrderedCollections = "1.4, 1.5"
1219
julia = "1.6"

ext/XMLJSONExt.jl

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module XMLJSONExt
2+
3+
using JSON
4+
using OrderedCollections
5+
using XML
6+
7+
function XML.xml2dicts(node::Node)
8+
if nodetype(node) == XML.Document
9+
# root node has no tag and 1 child, so it is special, just apply to its child
10+
return XML.xml2dicts(only(node.children))
11+
elseif nodetype(node) == XML.Text
12+
# text nodes have no tag, and just have contents
13+
return OrderedDict("_" => node.value)
14+
elseif nodetype(node) == XML.Element
15+
# normal case
16+
dict = OrderedDict{String,Any}()
17+
# first put in the attributes
18+
if !isnothing(attributes(node))
19+
merge!(dict, attributes(node))
20+
end
21+
# then any children
22+
for child in children(node)
23+
child_result = XML.xml2dicts(child)
24+
for (key, value) in child_result
25+
if haskey(dict, key)
26+
if isa(dict[key], Vector)
27+
push!(dict[key], value)
28+
else
29+
dict[key] = [dict[key], value]
30+
end
31+
else
32+
dict[key] = value
33+
end
34+
end
35+
end
36+
return OrderedDict(tag(node) => dict)
37+
else
38+
throw(DomainError(nodetype(node), "unsupported node type"))
39+
end
40+
end
41+
42+
43+
44+
function XML.xml2json(xml::Node, json="")
45+
dict_result = XML.xml2dicts(xml)
46+
47+
if isdir(dirname(json))
48+
open(json, "w") do io
49+
JSON.print(io, dict_result, 2)
50+
end
51+
else
52+
return JSON.json(dict_result)
53+
end
54+
end
55+
56+
XML.xml2json(xml::IO, json="") = XML.xml2json(read(xml, String), json)
57+
58+
end # module

src/XML.jl

Lines changed: 37 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export
99
# Interface:
1010
children, nodetype, tag, attributes, value, is_simple, simplevalue, simple_value,
1111
# Extended Interface for LazyNode:
12-
parent, depth, next, prev
12+
parent, depth, next, prev,
13+
# Extension XMLJSONExt:
14+
xml2dicts, xml2json
1315

1416
#-----------------------------------------------------------------------------# escape/unescape
1517
const escape_chars = ('&' => "&amp;", '<' => "&lt;", '>' => "&gt;", "'" => "&apos;", '"' => "&quot;")
@@ -69,9 +71,9 @@ A Lazy representation of an XML node.
6971
"""
7072
mutable struct LazyNode <: AbstractXMLNode
7173
raw::Raw
72-
tag::Union{Nothing, String}
73-
attributes::Union{Nothing, OrderedDict{String, String}}
74-
value::Union{Nothing, String}
74+
tag::Union{Nothing,String}
75+
attributes::Union{Nothing,OrderedDict{String,String}}
76+
value::Union{Nothing,String}
7577
end
7678
LazyNode(raw::Raw) = LazyNode(raw, nothing, nothing, nothing)
7779

@@ -126,33 +128,33 @@ A representation of an XML DOM node. For simpler construction, use `(::NodeType
126128
"""
127129
struct Node <: AbstractXMLNode
128130
nodetype::NodeType
129-
tag::Union{Nothing, String}
130-
attributes::Union{Nothing, OrderedDict{String, String}}
131-
value::Union{Nothing, String}
132-
children::Union{Nothing, Vector{Node}}
131+
tag::Union{Nothing,String}
132+
attributes::Union{Nothing,OrderedDict{String,String}}
133+
value::Union{Nothing,String}
134+
children::Union{Nothing,Vector{Node}}
133135

134136
function Node(nodetype::NodeType, tag=nothing, attributes=nothing, value=nothing, children=nothing)
135137
new(nodetype,
136138
isnothing(tag) ? nothing : string(tag),
137139
isnothing(attributes) ? nothing : OrderedDict(string(k) => string(v) for (k, v) in pairs(attributes)),
138140
isnothing(value) ? nothing : string(value),
139141
isnothing(children) ? nothing :
140-
children isa Node ? [children] :
141-
children isa Vector{Node} ? children :
142-
children isa Vector ? map(Node, children) :
143-
children isa Tuple ? map(Node, collect(children)) :
144-
[Node(children)]
142+
children isa Node ? [children] :
143+
children isa Vector{Node} ? children :
144+
children isa Vector ? map(Node, children) :
145+
children isa Tuple ? map(Node, collect(children)) :
146+
[Node(children)]
145147
)
146148
end
147149
end
148150

149151
function Node(o::Node, x...; kw...)
150152
attrs = !isnothing(kw) ?
151-
merge(
152-
OrderedDict(string(k) => string(v) for (k,v) in pairs(kw)),
153-
isnothing(o.attributes) ? OrderedDict{String, String}() : o.attributes
154-
) :
155-
o.attributes
153+
merge(
154+
OrderedDict(string(k) => string(v) for (k, v) in pairs(kw)),
155+
isnothing(o.attributes) ? OrderedDict{String,String}() : o.attributes
156+
) :
157+
o.attributes
156158
children = isempty(x) ? o.children : vcat(isnothing(o.children) ? [] : o.children, collect(x))
157159
Node(o.nodetype, o.tag, attrs, o.value, children)
158160
end
@@ -171,7 +173,7 @@ Node(data::Raw) = Node(LazyNode(data))
171173
# Anything that's not Vector{UInt8} or a (Lazy)Node is converted to a Text Node
172174
Node(x) = Node(Text, nothing, nothing, string(x), nothing)
173175

174-
h(tag::Union{Symbol, String}, children...; kw...) = Node(Element, tag, kw, nothing, children)
176+
h(tag::Union{Symbol,String}, children...; kw...) = Node(Element, tag, kw, nothing, children)
175177
Base.getproperty(::typeof(h), tag::Symbol) = h(tag)
176178
(o::Node)(children...; kw...) = Node(o, Node.(children)...; kw...)
177179

@@ -261,7 +263,7 @@ next(o) = missing
261263
prev(o) = missing
262264

263265
is_simple(o) = nodetype(o) == Element && (isnothing(attributes(o)) || isempty(attributes(o))) &&
264-
length(children(o)) == 1 && nodetype(only(o)) in (Text, CData)
266+
length(children(o)) == 1 && nodetype(only(o)) in (Text, CData)
265267

266268
simple_value(o) = is_simple(o) ? value(only(o)) : error("`XML.simple_value` is only defined for simple nodes.")
267269

@@ -274,22 +276,22 @@ function nodes_equal(a, b)
274276
out &= XML.attributes(a) == XML.attributes(b)
275277
out &= XML.value(a) == XML.value(b)
276278
out &= length(XML.children(a)) == length(XML.children(b))
277-
out &= all(nodes_equal(ai, bi) for (ai,bi) in zip(XML.children(a), XML.children(b)))
279+
out &= all(nodes_equal(ai, bi) for (ai, bi) in zip(XML.children(a), XML.children(b)))
278280
return out
279281
end
280282

281283
Base.:(==)(a::AbstractXMLNode, b::AbstractXMLNode) = nodes_equal(a, b)
282284

283285
#-----------------------------------------------------------------------------# parse
284-
Base.parse(::Type{T}, str::AbstractString) where {T <: AbstractXMLNode} = parse(str, T)
286+
Base.parse(::Type{T}, str::AbstractString) where {T<:AbstractXMLNode} = parse(str, T)
285287

286288
#-----------------------------------------------------------------------------# indexing
287-
Base.getindex(o::Union{Raw, AbstractXMLNode}) = o
288-
Base.getindex(o::Union{Raw, AbstractXMLNode}, i::Integer) = children(o)[i]
289-
Base.getindex(o::Union{Raw, AbstractXMLNode}, ::Colon) = children(o)
290-
Base.lastindex(o::Union{Raw, AbstractXMLNode}) = lastindex(children(o))
289+
Base.getindex(o::Union{Raw,AbstractXMLNode}) = o
290+
Base.getindex(o::Union{Raw,AbstractXMLNode}, i::Integer) = children(o)[i]
291+
Base.getindex(o::Union{Raw,AbstractXMLNode}, ::Colon) = children(o)
292+
Base.lastindex(o::Union{Raw,AbstractXMLNode}) = lastindex(children(o))
291293

292-
Base.only(o::Union{Raw, AbstractXMLNode}) = only(children(o))
294+
Base.only(o::Union{Raw,AbstractXMLNode}) = only(children(o))
293295

294296
Base.length(o::AbstractXMLNode) = length(children(o))
295297

@@ -338,7 +340,7 @@ end
338340
function _print_attrs(io::IO, o; color=:normal)
339341
attr = attributes(o)
340342
isnothing(attr) && return nothing
341-
for (k,v) in attr
343+
for (k, v) in attr
342344
# printstyled(io, ' ', k, '=', '"', v, '"'; color)
343345
print(io, ' ', k, '=', '"', v, '"')
344346
end
@@ -356,13 +358,13 @@ write(x; kw...) = (io = IOBuffer(); write(io, x; kw...); String(take!(io)))
356358
write(filename::AbstractString, x; kw...) = open(io -> write(io, x; kw...), filename, "w")
357359

358360
function write(io::IO, x; indentsize::Int=2, depth::Int=depth(x))
359-
indent = ' ' ^ indentsize
361+
indent = ' '^indentsize
360362
nodetype = XML.nodetype(x)
361363
tag = XML.tag(x)
362364
value = XML.value(x)
363365
children = XML.children(x)
364366

365-
padding = indent ^ max(0, depth - 1)
367+
padding = indent^max(0, depth - 1)
366368
print(io, padding)
367369
if nodetype === Text
368370
print(io, value)
@@ -377,7 +379,7 @@ function write(io::IO, x; indentsize::Int=2, depth::Int=depth(x))
377379
else
378380
println(io)
379381
foreach(children) do child
380-
write(io, child; indentsize, depth = depth + 1)
382+
write(io, child; indentsize, depth=depth + 1)
381383
println(io)
382384
end
383385
print(io, padding, "</", tag, '>')
@@ -407,4 +409,8 @@ function write(io::IO, x; indentsize::Int=2, depth::Int=depth(x))
407409
end
408410
end
409411

412+
# Extension XMLJSONExt
413+
function xml2dicts end
414+
function xml2json end
415+
410416
end

test/JSONExt.jl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using JSON
2+
3+
@testset "XML to JSON" begin
4+
xml = read("data/toJSON.xml", Node)
5+
json = xml2json(xml)
6+
d = xml2dicts(xml)
7+
@test JSON.parse(json) == d
8+
end

test/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[deps]
22
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
33
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
4+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
45
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

test/data/toJSON.xml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<instance
2+
format="XCSP3"
3+
type="CSP">
4+
<variables>
5+
<var id="x">0 1</var>
6+
<var id="y">0 1</var>
7+
<var id="z">0 1</var>
8+
</variables>
9+
<constraints>
10+
<extension>
11+
<list>x y</list>
12+
<supports>(0,0) (1,1)</supports>
13+
</extension>
14+
<extension>
15+
<list>x z</list>
16+
<supports>(0,0) (1,1)</supports>
17+
</extension>
18+
<extension>
19+
<list>y z</list>
20+
<supports>(0,1) (1,0)</supports>
21+
</extension>
22+
</constraints>
23+
</instance>

test/runtests.jl

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,28 @@ end
5858
#-----------------------------------------------------------------------------# Raw
5959
@testset "Raw tag/attributes/value" begin
6060
examples = [
61-
(xml = "<!DOCTYPE html>",
62-
nodetype = DTD,
61+
(xml="<!DOCTYPE html>",
62+
nodetype=DTD,
6363
tag=nothing,
6464
attributes=nothing,
6565
value="html"),
66-
(xml = "<?xml version=\"1.0\" key=\"value\"?>",
67-
nodetype = Declaration,
66+
(xml="<?xml version=\"1.0\" key=\"value\"?>",
67+
nodetype=Declaration,
6868
tag=nothing,
6969
attributes=Dict("version" => "1.0", "key" => "value"),
7070
value=nothing),
71-
(xml = "<tag _id=\"1\", x=\"abc\" />",
72-
nodetype = Element,
71+
(xml="<tag _id=\"1\", x=\"abc\" />",
72+
nodetype=Element,
7373
tag="tag",
7474
attributes=Dict("_id" => "1", "x" => "abc"),
7575
value=nothing),
76-
(xml = "<!-- comment -->",
77-
nodetype = Comment,
76+
(xml="<!-- comment -->",
77+
nodetype=Comment,
7878
tag=nothing,
7979
attributes=nothing,
8080
value=" comment "),
81-
(xml = "<![CData[cdata test]]>",
82-
nodetype = CData,
81+
(xml="<![CData[cdata test]]>",
82+
nodetype=CData,
8383
tag=nothing,
8484
attributes=nothing,
8585
value="cdata test"),
@@ -129,7 +129,7 @@ end
129129

130130
idx = findall(next_res .!= prev_res)
131131

132-
for (a,b) in zip(next_res, prev_res)
132+
for (a, b) in zip(next_res, prev_res)
133133
@test a == b
134134
end
135135
end
@@ -172,7 +172,7 @@ end
172172
@test node == node2
173173

174174
#For debugging:
175-
for (a,b) in zip(AbstractTrees.Leaves(node), AbstractTrees.Leaves(node2))
175+
for (a, b) in zip(AbstractTrees.Leaves(node), AbstractTrees.Leaves(node2))
176176
if a != b
177177
@info path
178178
@info a
@@ -192,7 +192,7 @@ end
192192
ProcessingInstruction("xml-stylesheet", href="mystyle.css", type="text/css"),
193193
Element("root_tag", CData("cdata"), Text("text"))
194194
)
195-
@test map(nodetype, children(doc)) == [DTD,Declaration,Comment,ProcessingInstruction,Element]
195+
@test map(nodetype, children(doc)) == [DTD, Declaration, Comment, ProcessingInstruction, Element]
196196
@test length(children(doc[end])) == 2
197197
@test nodetype(doc[end][1]) == XML.CData
198198
@test nodetype(doc[end][2]) == XML.Text
@@ -221,6 +221,8 @@ end
221221

222222
# https://github.com/JuliaComputing/XML.jl/issues/14 (Sorted Attributes)
223223
kw = NamedTuple(OrderedDict(Symbol(k) => Int(k) for k in 'a':'z'))
224-
xyz = XML.Element("point"; kw...)
224+
xyz = XML.Element("point"; kw...)
225225
@test collect(keys(attributes(xyz))) == string.(collect('a':'z'))
226226
end
227+
228+
include("JSONExt.jl")

0 commit comments

Comments
 (0)