Skip to content

Commit

Permalink
Add a JSON extention
Browse files Browse the repository at this point in the history
  • Loading branch information
Azzaare committed Sep 5, 2024
1 parent 5e26b40 commit 03d66e3
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 45 deletions.
7 changes: 7 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ version = "0.3.4"
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"

[weakdeps]
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"

[extensions]
XMLJSONExt = ["JSON"]

[compat]
JSON = "0.21"
OrderedCollections = "1.4, 1.5"
julia = "1.6"
58 changes: 58 additions & 0 deletions ext/XMLJSONExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module XMLJSONExt

using JSON
using OrderedCollections
using XML

function XML.xml2dicts(node::Node)
if nodetype(node) == XML.Document
# root node has no tag and 1 child, so it is special, just apply to its child
return XML.xml2dicts(only(node.children))
elseif nodetype(node) == XML.Text
# text nodes have no tag, and just have contents
return OrderedDict("_" => node.value)
elseif nodetype(node) == XML.Element
# normal case
dict = OrderedDict{String,Any}()
# first put in the attributes
if !isnothing(attributes(node))
merge!(dict, attributes(node))
end
# then any children
for child in children(node)
child_result = XML.xml2dicts(child)
for (key, value) in child_result
if haskey(dict, key)
if isa(dict[key], Vector)
push!(dict[key], value)
else
dict[key] = [dict[key], value]
end
else
dict[key] = value
end
end
end
return OrderedDict(tag(node) => dict)
else
throw(DomainError(nodetype(node), "unsupported node type"))
end
end



function XML.xml2json(xml::Node, json="")
dict_result = XML.xml2dicts(xml)

if isdir(dirname(json))
open(json, "w") do io
JSON.print(io, dict_result, 2)
end
else
return JSON.json(dict_result)
end
end

XML.xml2json(xml::IO, json="") = XML.xml2json(read(xml, String), json)

end # module
68 changes: 37 additions & 31 deletions src/XML.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export
# Interface:
children, nodetype, tag, attributes, value, is_simple, simplevalue, simple_value,
# Extended Interface for LazyNode:
parent, depth, next, prev
parent, depth, next, prev,
# Extension XMLJSONExt:
xml2dicts, xml2json

#-----------------------------------------------------------------------------# escape/unescape
const escape_chars = ('&' => "&amp;", '<' => "&lt;", '>' => "&gt;", "'" => "&apos;", '"' => "&quot;")
Expand Down Expand Up @@ -69,9 +71,9 @@ A Lazy representation of an XML node.
"""
mutable struct LazyNode <: AbstractXMLNode
raw::Raw
tag::Union{Nothing, String}
attributes::Union{Nothing, OrderedDict{String, String}}
value::Union{Nothing, String}
tag::Union{Nothing,String}
attributes::Union{Nothing,OrderedDict{String,String}}
value::Union{Nothing,String}
end
LazyNode(raw::Raw) = LazyNode(raw, nothing, nothing, nothing)

Expand Down Expand Up @@ -126,33 +128,33 @@ A representation of an XML DOM node. For simpler construction, use `(::NodeType
"""
struct Node <: AbstractXMLNode
nodetype::NodeType
tag::Union{Nothing, String}
attributes::Union{Nothing, OrderedDict{String, String}}
value::Union{Nothing, String}
children::Union{Nothing, Vector{Node}}
tag::Union{Nothing,String}
attributes::Union{Nothing,OrderedDict{String,String}}
value::Union{Nothing,String}
children::Union{Nothing,Vector{Node}}

function Node(nodetype::NodeType, tag=nothing, attributes=nothing, value=nothing, children=nothing)
new(nodetype,
isnothing(tag) ? nothing : string(tag),
isnothing(attributes) ? nothing : OrderedDict(string(k) => string(v) for (k, v) in pairs(attributes)),
isnothing(value) ? nothing : string(value),
isnothing(children) ? nothing :
children isa Node ? [children] :
children isa Vector{Node} ? children :
children isa Vector ? map(Node, children) :
children isa Tuple ? map(Node, collect(children)) :
[Node(children)]
children isa Node ? [children] :
children isa Vector{Node} ? children :
children isa Vector ? map(Node, children) :
children isa Tuple ? map(Node, collect(children)) :
[Node(children)]
)
end
end

function Node(o::Node, x...; kw...)
attrs = !isnothing(kw) ?
merge(
OrderedDict(string(k) => string(v) for (k,v) in pairs(kw)),
isnothing(o.attributes) ? OrderedDict{String, String}() : o.attributes
) :
o.attributes
merge(
OrderedDict(string(k) => string(v) for (k, v) in pairs(kw)),
isnothing(o.attributes) ? OrderedDict{String,String}() : o.attributes
) :
o.attributes
children = isempty(x) ? o.children : vcat(isnothing(o.children) ? [] : o.children, collect(x))
Node(o.nodetype, o.tag, attrs, o.value, children)
end
Expand All @@ -171,7 +173,7 @@ Node(data::Raw) = Node(LazyNode(data))
# Anything that's not Vector{UInt8} or a (Lazy)Node is converted to a Text Node
Node(x) = Node(Text, nothing, nothing, string(x), nothing)

h(tag::Union{Symbol, String}, children...; kw...) = Node(Element, tag, kw, nothing, children)
h(tag::Union{Symbol,String}, children...; kw...) = Node(Element, tag, kw, nothing, children)
Base.getproperty(::typeof(h), tag::Symbol) = h(tag)
(o::Node)(children...; kw...) = Node(o, Node.(children)...; kw...)

Expand Down Expand Up @@ -261,7 +263,7 @@ next(o) = missing
prev(o) = missing

is_simple(o) = nodetype(o) == Element && (isnothing(attributes(o)) || isempty(attributes(o))) &&
length(children(o)) == 1 && nodetype(only(o)) in (Text, CData)
length(children(o)) == 1 && nodetype(only(o)) in (Text, CData)

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

Expand All @@ -274,22 +276,22 @@ function nodes_equal(a, b)
out &= XML.attributes(a) == XML.attributes(b)
out &= XML.value(a) == XML.value(b)
out &= length(XML.children(a)) == length(XML.children(b))
out &= all(nodes_equal(ai, bi) for (ai,bi) in zip(XML.children(a), XML.children(b)))
out &= all(nodes_equal(ai, bi) for (ai, bi) in zip(XML.children(a), XML.children(b)))
return out
end

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

#-----------------------------------------------------------------------------# parse
Base.parse(::Type{T}, str::AbstractString) where {T <: AbstractXMLNode} = parse(str, T)
Base.parse(::Type{T}, str::AbstractString) where {T<:AbstractXMLNode} = parse(str, T)

#-----------------------------------------------------------------------------# indexing
Base.getindex(o::Union{Raw, AbstractXMLNode}) = o
Base.getindex(o::Union{Raw, AbstractXMLNode}, i::Integer) = children(o)[i]
Base.getindex(o::Union{Raw, AbstractXMLNode}, ::Colon) = children(o)
Base.lastindex(o::Union{Raw, AbstractXMLNode}) = lastindex(children(o))
Base.getindex(o::Union{Raw,AbstractXMLNode}) = o
Base.getindex(o::Union{Raw,AbstractXMLNode}, i::Integer) = children(o)[i]
Base.getindex(o::Union{Raw,AbstractXMLNode}, ::Colon) = children(o)
Base.lastindex(o::Union{Raw,AbstractXMLNode}) = lastindex(children(o))

Base.only(o::Union{Raw, AbstractXMLNode}) = only(children(o))
Base.only(o::Union{Raw,AbstractXMLNode}) = only(children(o))

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

Expand Down Expand Up @@ -338,7 +340,7 @@ end
function _print_attrs(io::IO, o; color=:normal)
attr = attributes(o)
isnothing(attr) && return nothing
for (k,v) in attr
for (k, v) in attr
# printstyled(io, ' ', k, '=', '"', v, '"'; color)
print(io, ' ', k, '=', '"', v, '"')
end
Expand All @@ -356,13 +358,13 @@ write(x; kw...) = (io = IOBuffer(); write(io, x; kw...); String(take!(io)))
write(filename::AbstractString, x; kw...) = open(io -> write(io, x; kw...), filename, "w")

function write(io::IO, x; indentsize::Int=2, depth::Int=depth(x))
indent = ' ' ^ indentsize
indent = ' '^indentsize
nodetype = XML.nodetype(x)
tag = XML.tag(x)
value = XML.value(x)
children = XML.children(x)

padding = indent ^ max(0, depth - 1)
padding = indent^max(0, depth - 1)
print(io, padding)
if nodetype === Text
print(io, value)
Expand All @@ -377,7 +379,7 @@ function write(io::IO, x; indentsize::Int=2, depth::Int=depth(x))
else
println(io)
foreach(children) do child
write(io, child; indentsize, depth = depth + 1)
write(io, child; indentsize, depth=depth + 1)
println(io)
end
print(io, padding, "</", tag, '>')
Expand Down Expand Up @@ -407,4 +409,8 @@ function write(io::IO, x; indentsize::Int=2, depth::Int=depth(x))
end
end

# Extension XMLJSONExt
function xml2dicts end
function xml2json end

end
8 changes: 8 additions & 0 deletions test/JSONExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using JSON

@testset "XML to JSON" begin
xml = read("data/toJSON.xml", Node)
json = xml2json(xml)
d = xml2dicts(xml)
@test JSON.parse(json) == d
end
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[deps]
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
23 changes: 23 additions & 0 deletions test/data/toJSON.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<instance
format="XCSP3"
type="CSP">
<variables>
<var id="x">0 1</var>
<var id="y">0 1</var>
<var id="z">0 1</var>
</variables>
<constraints>
<extension>
<list>x y</list>
<supports>(0,0) (1,1)</supports>
</extension>
<extension>
<list>x z</list>
<supports>(0,0) (1,1)</supports>
</extension>
<extension>
<list>y z</list>
<supports>(0,1) (1,0)</supports>
</extension>
</constraints>
</instance>
30 changes: 16 additions & 14 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,28 @@ end
#-----------------------------------------------------------------------------# Raw
@testset "Raw tag/attributes/value" begin
examples = [
(xml = "<!DOCTYPE html>",
nodetype = DTD,
(xml="<!DOCTYPE html>",
nodetype=DTD,
tag=nothing,
attributes=nothing,
value="html"),
(xml = "<?xml version=\"1.0\" key=\"value\"?>",
nodetype = Declaration,
(xml="<?xml version=\"1.0\" key=\"value\"?>",
nodetype=Declaration,
tag=nothing,
attributes=Dict("version" => "1.0", "key" => "value"),
value=nothing),
(xml = "<tag _id=\"1\", x=\"abc\" />",
nodetype = Element,
(xml="<tag _id=\"1\", x=\"abc\" />",
nodetype=Element,
tag="tag",
attributes=Dict("_id" => "1", "x" => "abc"),
value=nothing),
(xml = "<!-- comment -->",
nodetype = Comment,
(xml="<!-- comment -->",
nodetype=Comment,
tag=nothing,
attributes=nothing,
value=" comment "),
(xml = "<![CData[cdata test]]>",
nodetype = CData,
(xml="<![CData[cdata test]]>",
nodetype=CData,
tag=nothing,
attributes=nothing,
value="cdata test"),
Expand Down Expand Up @@ -129,7 +129,7 @@ end

idx = findall(next_res .!= prev_res)

for (a,b) in zip(next_res, prev_res)
for (a, b) in zip(next_res, prev_res)
@test a == b
end
end
Expand Down Expand Up @@ -172,7 +172,7 @@ end
@test node == node2

#For debugging:
for (a,b) in zip(AbstractTrees.Leaves(node), AbstractTrees.Leaves(node2))
for (a, b) in zip(AbstractTrees.Leaves(node), AbstractTrees.Leaves(node2))
if a != b
@info path
@info a
Expand All @@ -192,7 +192,7 @@ end
ProcessingInstruction("xml-stylesheet", href="mystyle.css", type="text/css"),
Element("root_tag", CData("cdata"), Text("text"))
)
@test map(nodetype, children(doc)) == [DTD,Declaration,Comment,ProcessingInstruction,Element]
@test map(nodetype, children(doc)) == [DTD, Declaration, Comment, ProcessingInstruction, Element]
@test length(children(doc[end])) == 2
@test nodetype(doc[end][1]) == XML.CData
@test nodetype(doc[end][2]) == XML.Text
Expand Down Expand Up @@ -221,6 +221,8 @@ end

# https://github.com/JuliaComputing/XML.jl/issues/14 (Sorted Attributes)
kw = NamedTuple(OrderedDict(Symbol(k) => Int(k) for k in 'a':'z'))
xyz = XML.Element("point"; kw...)
xyz = XML.Element("point"; kw...)
@test collect(keys(attributes(xyz))) == string.(collect('a':'z'))
end

include("JSONExt.jl")

0 comments on commit 03d66e3

Please sign in to comment.