diff --git a/docs/src/references.md b/docs/src/references.md
index 2aafdc5..459c822 100644
--- a/docs/src/references.md
+++ b/docs/src/references.md
@@ -35,6 +35,7 @@ TextNode
CommentNode
CDataNode
AttributeNode
+DTDNode
```
Node types
@@ -78,9 +79,12 @@ isattribute(::Node)
EzXML.istext(::Node)
iscdata(::Node)
iscomment(::Node)
+isdtd(::Node)
countnodes(::Node)
countelements(::Node)
countattributes(::Node)
+systemID(::Node)
+externalID(::Node)
```
Node modifiers
@@ -97,6 +101,7 @@ DOM tree accessors
```@docs
document
root
+dtd
parentnode
parentelement
firstnode
@@ -114,6 +119,7 @@ elements
eachattribute
attributes
hasroot
+hasdtd
hasnode
hasnextnode
hasprevnode
@@ -130,6 +136,7 @@ DOM tree modifiers
```@docs
setroot!
+setdtd!
link!
linknext!
linkprev!
diff --git a/src/EzXML.jl b/src/EzXML.jl
index 3dc602e..c1a1195 100644
--- a/src/EzXML.jl
+++ b/src/EzXML.jl
@@ -17,6 +17,7 @@ export
CommentNode,
CDataNode,
AttributeNode,
+ DTDNode,
# document constructors
XMLDocument,
@@ -52,18 +53,24 @@ export
hasroot,
root,
setroot!,
+ hasdtd,
+ dtd,
+ setdtd!,
nodetype,
iselement,
isattribute,
# istext,
iscdata,
iscomment,
+ isdtd,
hasdocument,
document,
name,
setname!,
content,
setcontent!,
+ systemID,
+ externalID,
eachnode,
nodes,
eachelement,
diff --git a/src/document.jl b/src/document.jl
index 42e822b..0be2899 100644
--- a/src/document.jl
+++ b/src/document.jl
@@ -245,3 +245,54 @@ function setroot!(doc::Document, root::Node)
end
return root
end
+
+"""
+ hasdtd(doc::Document)
+
+Return if `doc` has a DTD node.
+"""
+function hasdtd(doc::Document)
+ dtd_ptr = ccall(
+ (:xmlGetIntSubset, libxml2),
+ Ptr{_Node},
+ (Ptr{Void},),
+ doc.node.ptr)
+ return dtd_ptr != C_NULL
+end
+
+"""
+ dtd(doc::Document)
+
+Return the DTD node of `doc`.
+"""
+function dtd(doc::Document)
+ if !hasdtd(doc)
+ throw(ArgumentError("no DTD"))
+ end
+ dtd_ptr = ccall(
+ (:xmlGetIntSubset, libxml2),
+ Ptr{_Node},
+ (Ptr{Void},),
+ doc.node.ptr)
+ return Node(dtd_ptr)
+end
+
+"""
+ setdtd!(doc::Document, node::Node)
+
+Set the DTD node of `doc` to `node` and return the DTD node.
+"""
+function setdtd!(doc::Document, node::Node)
+ if !isdtd(node)
+ throw(ArgumentError("not a DTD node"))
+ elseif hasdtd(doc)
+ unlink!(dtd(doc))
+ end
+ # Insert `node` as the first child of `doc.node`.
+ if hasnode(doc.node)
+ linkprev!(firstnode(doc.node), node)
+ else
+ link!(doc.node, node)
+ end
+ return node
+end
diff --git a/src/node.jl b/src/node.jl
index 05ec53c..f34d60a 100644
--- a/src/node.jl
+++ b/src/node.jl
@@ -163,6 +163,26 @@ immutable _Attribute
psvi::Ptr{Void}
end
+# Fields of DTD node (_xmlDtd).
+immutable _Dtd
+ _private::Ptr{Void}
+ typ::Cint
+ name::Cstring
+ children::Ptr{_Node}
+ last::Ptr{_Node}
+ parent::Ptr{_Node}
+ next::Ptr{_Node}
+ prev::Ptr{_Node}
+ doc::Ptr{_Node}
+
+ notations::Ptr{Void}
+ elements::Ptr{Void}
+ attributes::Ptr{Void}
+ entities::Ptr{Void}
+ externalID::Cstring
+ systemID::Cstring
+ pentities::Ptr{Void}
+end
# Node type
# ---------
@@ -448,6 +468,36 @@ function AttributeNode(name::AbstractString, value::AbstractString)
return Node(node_ptr)
end
+"""
+ DTDNode(name, [systemID, [externalID]])
+
+Create a DTD node with `name`, `systemID`, and `externalID`.
+"""
+function DTDNode(name::AbstractString, systemID::AbstractString, externalID::AbstractString)
+ return make_dtd_node(name, systemID, externalID)
+end
+
+function DTDNode(name::AbstractString, systemID::AbstractString)
+ return make_dtd_node(name, systemID, C_NULL)
+end
+
+function DTDNode(name::AbstractString)
+ return make_dtd_node(name, C_NULL, C_NULL)
+end
+
+function make_dtd_node(name, systemID, externalID)
+ doc_ptr = C_NULL
+ node_ptr = ccall(
+ (:xmlCreateIntSubset, libxml2),
+ Ptr{_Node},
+ (Ptr{Void}, Cstring, Cstring, Cstring),
+ doc_ptr, name, externalID, systemID)
+ if node_ptr == C_NULL
+ throw_xml_error()
+ end
+ return Node(node_ptr)
+end
+
# DOM
# ---
@@ -982,6 +1032,15 @@ function iscomment(node::Node)
return nodetype(node) === COMMENT_NODE
end
+"""
+ isdtd(node::Node)
+
+Return if `node` is a DTD node.
+"""
+function isdtd(node::Node)
+ return nodetype(node) === DTD_NODE
+end
+
"""
hasdocument(node::Node)
@@ -1062,6 +1121,30 @@ function setcontent!(node::Node, content::AbstractString)
return node
end
+"""
+ systemID(node::Node)
+
+Return the system ID of `node`.
+"""
+function systemID(node::Node)
+ if !isdtd(node)
+ throw(ArgumentError("not a DTD node"))
+ end
+ return unsafe_string(unsafe_load(convert(Ptr{_Dtd}, node.ptr)).systemID)
+end
+
+"""
+ externalID(node::Node)
+
+Return the external ID of `node`.
+"""
+function externalID(node::Node)
+ if !isdtd(node)
+ throw(ArgumentError("not a DTD node"))
+ end
+ return unsafe_string(unsafe_load(convert(Ptr{_Dtd}, node.ptr)).externalID)
+end
+
# Attributes
# ----------
diff --git a/test/runtests.jl b/test/runtests.jl
index e26c063..263fd10 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -138,6 +138,8 @@ end
""")
@test isa(doc, Document)
@test nodetype(doc.node) === EzXML.HTML_DOCUMENT_NODE
+ @test hasdtd(doc)
+ @test name(dtd(doc)) == "html"
doc = parse(Document, """
@@ -151,6 +153,7 @@ end
""")
@test isa(doc, Document)
@test nodetype(doc.node) === EzXML.HTML_DOCUMENT_NODE
+ @test hasdtd(doc)
doc = parse(Document, """
@@ -319,6 +322,34 @@ end
@test isattribute(n)
@test_throws ArgumentError document(n)
+ n = DTDNode("open-hatch")
+ @test isa(n, Node)
+ @test isdtd(n)
+ @test n.owner === n
+ @test nodetype(n) === EzXML.DTD_NODE
+ @test name(n) == "open-hatch"
+ @test_throws ArgumentError systemID(n)
+ @test_throws ArgumentError externalID(n)
+
+ n = DTDNode("open-hatch",
+ "http://www.textuality.com/boilerplate/OpenHatch.xml")
+ @test isa(n, Node)
+ @test isdtd(n)
+ @test n.owner === n
+ @test nodetype(n) === EzXML.DTD_NODE
+ @test systemID(n) == "http://www.textuality.com/boilerplate/OpenHatch.xml"
+ @test_throws ArgumentError externalID(n)
+
+ n = DTDNode("open-hatch",
+ "http://www.textuality.com/boilerplate/OpenHatch.xml",
+ "-//Textuality//TEXT Standard open-hatch boilerplate//EN")
+ @test isa(n, Node)
+ @test isdtd(n)
+ @test n.owner === n
+ @test nodetype(n) === EzXML.DTD_NODE
+ @test systemID(n) == "http://www.textuality.com/boilerplate/OpenHatch.xml"
+ @test externalID(n) == "-//Textuality//TEXT Standard open-hatch boilerplate//EN"
+
doc = XMLDocument()
@test isa(doc, Document)
@test doc.node.owner === doc.node
@@ -345,6 +376,7 @@ end
@testset "Traversal" begin
doc = parsexml("