### ----------------------------------------------------
### -- LibXML
### -- Licenses: Artistic-2.0
### -- Authors: David Warring
### -- File: lib/LibXML/Document.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 2

#| LibXML DOM Document Class
unit class LibXML::Document;

use LibXML::Node;
use LibXML::Raw;
use LibXML::_ParentNode;
use LibXML::_Rawish;
use W3C::DOM;

also is LibXML::Node;
also does LibXML::_ParentNode;
also does LibXML::_Rawish[xmlDoc, <encoding setCompression getCompression standalone URI wellFormed set-flags>];
also does W3C::DOM::Document;

use LibXML::Attr;
use LibXML::CDATA;
use LibXML::Comment;
use LibXML::DocumentFragment;
use LibXML::Dtd;
use LibXML::Dtd::Entity;
use LibXML::Element;
use LibXML::EntityRef;
use LibXML::Enums;
use LibXML::Item :dom-boxed;
use LibXML::Utils :&output-options;
use LibXML::PI;
use LibXML::Text;
use LibXML::Types :QName, :NCName, :NameVal, :resolve-package;
use LibXML::_Validator;
use Method::Also;
use NativeCall;

=begin pod
    =head2 Synopsis

        use LibXML::Document;
        # Only methods specific to Document nodes are listed here,
        # see the LibXML::Node documentation for other methods

        my LibXML::Document $doc  .= new: :$version, :$enc;
        $doc .= createDocument($version, $enc);
        $doc .= parse($string);

        my Str $URI = $doc.URI();
        $doc.setURI($URI);
        my Str $enc = $doc.encoding();
        $enc = $doc.actualEncoding();
        $doc.encoding = $new-encoding;
        my Version $doc-version = $doc.version();
        use LibXML::Document :XmlStandalone;
        if $doc.standalone == XmlStandaloneYes {...}
        $doc.standalone = XmlStandaloneNo;
        my Int $zip-level = 5; # zip-level (0..9), or -1 for no compression
        $doc.compression = $zip-level;
        my Str $html-tidy = $doc.Str: :$format, :$html;
        my Str $xml-c14n = $doc.Str: :C14N, :$comments, :$xpath, :$exclusive, :$selector;
        my Str $xml-tidy = $doc.serialize: :$format;
        my Int $state = $doc.write: :$file, :$format;
        $state = $doc.save: :io($fh), :$format;
        my Str $html = $doc.Str: :html;
        $html = $doc.serialize-html();
        try { $doc.validate(); }
        if $doc.is-valid() { ... }
        if $doc.is-valid($elem) { ... }

        my LibXML::Element $root = $doc.documentElement();
        $dom.documentElement = $root;
        my LibXML::Element $element = $doc.createElement( $nodename );
        $element = $doc.createElementNS( $namespaceURI, $nodename );
        my LibXML::Text $text = $doc.createTextNode( $content_text );
        my LibXML::Comment $comment = $doc.createComment( $comment_text );
        my LibXML::Attr $attr = $doc.createAttribute($name [,$value]);
        $attr = $doc.createAttributeNS( namespaceURI, $name [,$value] );
        my LibXML::DocumentFragment $fragment = $doc.createDocumentFragment();
        my LibXML::CDATA $cdata = $doc.createCDATASection( $cdata_content );
        my LibXML::PI $pi = $doc.createProcessingInstruction( $target, $data );
        my LibXML::EntityRef $entref = $doc.createEntityReference($refname);
        my LibXML::Dtd $dtd = $doc.createInternalSubset( $rootnode, $public, $system);
        $dtd = $doc.createExternalSubset( $rootnode_name, $publicId, $systemId);
        $doc.importNode( $node );
        $doc.adoptNode( $node );
        $dtd = $doc.externalSubset;
        $dtd = $doc.internalSubset;
        $doc.externalSubset = $dtd;
        $doc.internalSubset = $dtd;
        $dtd = $doc.removeExternalSubset();
        $dtd = $doc.removeInternalSubset();
        my LibXML::Element @found = $doc.getElementsByTagName($tagname);
        @found = $doc.getElementsByTagNameNS($nsURI,$tagname);
        @found = $doc.getElementsByLocalName($localname);
        my LibXML::Element $node = $doc.getElementById($id);
        $doc.indexElements();

    =head2 Description

    The Document Class is in most cases the result of a parsing process. But
    sometimes it is necessary to create a Document from scratch. The DOM Document
    Class provides functions that conform to the DOM Core naming style.

    It inherits all functions from L<LibXML::Node> as specified in the DOM specification. This enables access to the nodes besides
    the root element on document level - a C<DTD> for example. The support for these nodes is limited at the moment.

=end pod

=begin pod  
    =head2 Exports

    =head3 XML

    A subset of LibXML::Document that have node-type C<XML_DOCUMENT_NODE>. General characteristics include:
    =item element and attribute names are case sensitive
    =item opening and closing tags must always be paired

    =head3 HTML

    A subset of LibXML::Document that have node-type C<XML_HTML_DOCUMENT_NODE>. General characteristics include:
    =item HTML Parsing converts element and attribute names to lowercase. Closing tags can usually be omitted.

    =head3 DOCB

    A subset of LibXML::Document that have node-type C<XML_DOCB_DOCUMENT_NODE>. XML documents of type DocBook
=end pod

subset XML  is export(:XML)  of ::?CLASS:D where .nodeType == XML_DOCUMENT_NODE;
subset HTML is export(:HTML) of ::?CLASS:D where .nodeType == XML_HTML_DOCUMENT_NODE;
subset DOCB is export(:DOCB) of ::?CLASS:D where .nodeType == XML_DOCB_DOCUMENT_NODE;

constant InputCompressed = 1;

has $!parser-class;

method parser-class {
    $!parser-class === Any
        ?? ($!parser-class := resolve-package 'LibXML::Parser')
        !! $!parser-class
}

=begin pod
    =head2 Methods

    Many functions listed here are documented in the DOM Level 3 specification (L<http://www.w3.org/TR/DOM-Level-3-Core/>). Please refer to the specification for extensive documentation.
=end pod

submethod TWEAK(Bool :$input-compressed) {
    self.raw.set-flags(InputCompressed)
        if $input-compressed;
}

method new(
    Str  :$version,
    xmlEncodingStr :$enc,
    Str  :$URI,
    Bool :$html,
    Int  :$compression,
    xmlDoc :$raw = ($html ?? htmlDoc !! xmlDoc).new,
    LibXML::Config:D :$config = LibXML::Config.new,
    # obselete
    :$ctx,
    :$native, # obselete,
    |c
) {
    die 'new(:$native) option is obselete. Please use :$raw'
        with $native;
    die 'new(:$ctx) option is obselete.' with $ctx;
    $raw.version = $_ with $version;
    $raw.encoding = $_ with $enc;
    $raw.URI = $_ with $URI;
    $raw.setCompression($_) with $compression;

    self.bless: :$raw, :$config, |c;
}
=begin pod
    =head3 method new

        method new(
          xmlDoc :$native,
          Str :$version,
          xmlEncodingStr :$enc, # e.g. 'utf-8', 'utf-16'
          Str :$URI,
          Bool :$html,
          Int :$compression
        ) returns LibXML::Document

=end pod

method implementation returns W3C::DOM::Implementation { resolve-package('LibXML') }

# Perl compatibility
multi method createDocument(Str:D() $version where /^\d'.'\d$/, xmlEncodingStr $enc) {
    self.new: :$version, :$enc;
}

# DOM compatibility
multi method createDocument(Str $URI? is copy, QName $name?, W3C::DOM::DocumentType $dtd?, Str :URI($uri), *%opt) {
    $URI //= $uri;

    with $dtd {
        %opt<html> //= .is-XHTML;
    }
    my $doc = self.new: :$URI, |%opt;
    with $name {
        my LibXML::Node:D $elem = $doc.createElementNS($URI, $_);
        $doc.setDocumentElement($elem);
    }
    $doc.setInternalSubset($_) with $dtd;
    $doc;
}

=begin pod
    =head3 method createDocument

        multi method createDocument(Str() $version, xmlEncodingStr $enc
        ) returns LibXML::Document
        multi method createDocument(
             Str $URI?, QName $name?, Str $doc-type?
        )

    Raku or DOM-style constructors for the document class. As parameters it takes the version
    string and (optionally) the encoding string. Simply calling I<createDocument>() will create the document:
        =begin code :lang<xml>
        <?xml version="your version" encoding="your encoding"?>
        =end code
    Both parameters are optional. The default value for I<$version> is C<1.0>, of course. If the I<$encoding> parameter is not set, the encoding will be left unset, which means UTF-8 is
implied.

    The call of I<createDocument>() without any parameter will result the following code:
        =begin code :lang<xml>
        <?xml version="1.0"?>
        =end code
=end pod

=begin pod
    =head3 method URI

        my Str $URI = $doc.URI();
        $doc.URI = $URI;

    Gets or sets the URI (or filename) of the original document. For documents obtained
    by parsing a string of a FH without using the URI parsing argument of the
    corresponding C<parse_*> function, the result is a generated string unknown-XYZ where XYZ is some
    number; for documents created with the constructor C<new>, the URI is undefined.

    =head3 method encoding

        my Str $enc = $doc.encoding();
        $doc.encoding = $new-encoding;

    Gets or sets the encoding of the document.

    =item The `.Str` method treats the encoding as a subset. Any characters that fall outside the encoding set are encoded as entities (e.g. `&nbsp;`)
    =item The `.Blob` method will fully render the XML document in as a Blob with the specified encoding.

        my $doc = LibXML.createDocument( "1.0", "ISO-8859-15" );
        print $doc.encoding; # prints ISO-8859-15
        my $xml-with-entities = $doc.Str;
        'encoded.xml'.IO.spurt( $doc.Blob, :bin);

=end pod

method doc { self }

#| Returns the encoding in which the XML will be output by $doc.Blob() or $doc.write.
method actualEncoding returns xmlEncodingStr { $.encoding || 'UTF-8' }
=begin pod
    =para
    This is usually the original encoding of the document as declared in the XML
    declaration and returned by $doc.encoding. If the original encoding is not
    known (e.g. if created in memory or parsed from a XML without a declared
    encoding), 'UTF-8' is returned.

        my $doc = LibXML.createDocument( "1.0", "ISO-8859-15" );
        print $doc.encoding; # prints ISO-8859-15

=end pod

#| Gets or sets the version of the document
method version is rw returns Version {
    Proxy.new(
        FETCH => { Version.new($.raw.version) },
        STORE => -> $, Str() $_ {
            $.raw.version = .Version.Str;
    });
}

enum XmlStandalone is export(:XmlStandalone) (
    XmlStandaloneYes => 1,
    XmlStandaloneNo => 0,
    XmlStandaloneMu => -1,
);

# standalone() is handled by native() method
=begin pod
    =head3 method standalone

        use LibXML::Document :XmlStandalone;
        if $doc.standalone == XmlStandaloneYes { ... }

    Gets or sets the Numerical value of a documents XML declarations
    standalone attribute.

    It returns
    =item I<1 (XmlStandaloneYes)> if standalone="yes" was found,
    =item I<0 (XmlStandaloneNo)> if standalone="no" was found and
    =item I<-1 (XmlStandaloneMu)> if standalone was not specified (default on creation).
=end pod

#| Alter the value of a documents standalone attribute.
method setStandalone(Numeric $_) {
    $.raw.standalone = .defined
        ?? $_
        !! XmlStandaloneMu
}
=begin pod

        use LibXML::Document :XmlStandalone;
        $doc.setStandalone(XmlStandaloneYes);

    =para Set it to
    =item I<1 (XmlStandaloneYes)> to set standalone="yes",
    =item to I<0 (XmlStandaloneNo)> to set standalone="no" or
    =item to I<-1 (XmlStandaloneMu)> to remove the standalone attribute from the XML declaration.
=end pod

#| Gets or sets output compression
method compression is rw returns Int {
    Proxy.new(
        FETCH => { $.getCompression },
        STORE => -> $, UInt() $_ { $.setCompression($_) }
    );
}

#| Detect whether input was compressed (deprecated)
method input-compressed returns Bool is DEPRECATED {
   ? ($.raw.get-flags +& InputCompressed);
}
=begin pod
    =begin code :lang<raku>
    my LibXML::Document $doc .= parse<mydoc.xml.gz>;
    if $doc.input-compressed {
        my $zip-level = 5;
        $doc.compression = $zip-level;
        $doc.write: :file<test.xml.gz>;
    }
    else {
        $doc.write: :file<test.xml>;
    }
    =end code
    =para libxml2 allows reading of documents directly from gzipped files. The input-compressed method returns True if the input file was compressed.

    If one intends to write the document directly to a file, it is possible to set
    the compression level for a given document. This level can be in the range from
    0 to 8. If LibXML should not try to compress use I<-1> (default).

    Note that this feature will I<only> work if libxml2 is compiled with zlib support (`LibXML.have-compression` is True) ``and `.parse: :file(...)` is used for input and `.write` is used for output.
=end pod

method Str(
    ::?CLASS:D $doc is copy:
    LibXML::Config :$config = $.config,
    Bool :$skip-dtd = $config.skip-dtd,
    Bool :$C14N,
    Bool :$html = self.isHTMLish,
    |c --> Str) {

    if $skip-dtd && $doc.getInternalSubset.defined {
        $doc .= cloneNode: :deep;
        $doc.getInternalSubset.unbindNode;
    }

    if $C14N {
        $doc.canonicalize(|c)
    }
    else  {
        $doc.raw.Str: options => output-options(:$config, :$html, |c);
    }
}
=begin pod
    =head3 method Str
    =begin code :lang<raku>
    multi method Str(
            Bool :$skip-dtd,
            Bool :$format, Bool :$tag-expansion,
            Bool :$skip-xml-declaration,
            LibXML::Config :$config, # defaults for :$skip-dtd, :skip-xml-declaration and :tag-expansion
        ) returns Str;
    =end code

    I<Str> is a serializing function, so the DOM Tree is serialized into an XML
    string, ready for output.

        $file.IO.spurt: $doc.Str;

    regardless of the actual encoding of the document.

    The optional I<$format> flag sets the indenting of the output.

    If $format is False, or omitted, the document is dumped as it was originally parsed

    If $format is True, libxml2 will add ignorable white spaces, so the nodes content
    is easier to read. Existing text nodes will not be altered

    libxml2 uses a hard-coded indentation of 2 space characters per indentation
    level. This value can not be altered at run-time.

    Note that the `:C14N` and `:html` options match different multi-methods, with
    diffrent options, as below:

    =head4 `multi method Str: :C14N($!)!, Bool :$comments, Bool :$exclusive, :$xpath`

      my Str $xml-c14   = $doc.Str: :C14N, :$comment, :$xpath;
      my Str $xml-ec14n = $doc.Str: :C14N, :exclusive, :$xpath, :@prefix;

   C14N normalisation mode. See the documentation in L<LibXML::Node>.

    =head4 `multi method Str: :$html!, Bool :$format`

      my Str $html = $document.Str: :html;

    I<.Str: :html> serializes the tree to a string as HTML. With this
    method indenting is automatic and managed by libxml2 internally.

    =head3 method serialize

        my Str $xml-formatted = $doc.serialize(:$format);

    Similar to Str(), but doesn't interpret `:skip-dtd`, `:html` or `:C14N` options. This function was name added to be more consistent
    with libxml2.
=end pod

#| Serialize to HTML.
method serialize-html(|c --> Str) { self.Str: :html, |c; }
=begin pod
    =para Equivalent to: .Str: :html, but doesn't allow `:skip-dtd` option.
=end pod

    method Blob(
        ::?CLASS:D $doc is copy:
        LibXML::Config :$config,
        Bool() :$skip-xml-declaration is copy = $config.skip-xml-declaration,
        Bool() :$skip-dtd = $config.skip-dtd,
        xmlEncodingStr:D :$enc = self.encoding // 'UTF-8',
        Bool :$force,
        Bool :$html = self.isHTMLish,
        |c  --> Blob) {

    if $skip-xml-declaration {
        # losing the declaration that includes the encoding scheme; we need
        # to switch to UTF-8 (default encoding) to stay conformant.
        unless $force || $enc eq 'UTF-8' {
            warn "please use :force to allow :skip-xml-declaration for non-UTF-8 encoding '$enc'";
            $skip-xml-declaration = False;
        }
    }

    if $skip-dtd && $doc.getInternalSubset.defined {
        $doc .= cloneNode: :deep;
        $doc.getInternalSubset.unbindNode;
    }

    my $options = output-options(:$skip-xml-declaration, :$html, |c);
    $doc.raw.Blob: :$enc, :$options;
}
=begin pod
    =head3 method Blob() returns Blob

        method Blob(
            xmlEncodingStr :$enc = self.encoding // 'UTF-8',
            Bool :$format,
            Bool :$tag-expansion
            Bool :$skip-dtd,
            Bool :$skip-xml-declaration,
            Bool :$force,
        ) returns Blob;

    =para
    Returns a binary representation  of the XML
    document and it decendants encoded as `:$enc`.

    The option `:force` is needed to really allow the combination of
    a non-UTF8 encoding and :skip-xml-declaration.
=end pod

#| Write to a name file
method write(Str() :$file!, Bool :$format = False --> UInt) {
    my UInt $n = self.raw.write($file, :$format);
    fail "unable to save as xml: $file" if $n < 0;
    $n;
}

#| Write to a name file (equivalent to $.write: :$file)
method save-as(Str() $file --> UInt) { $.write(:$file) }

method is-valid(|c --> Bool) { $.validate(|c, :check); }
=begin pod
=head3 is-valid
=begin code :lang<raku>
multi method is-valid(LibXML::Dtd $dtd? --> Bool)
multi method is-valid(LibXML::Schema $schema? --> Bool)
multi method is-valid(LibXML::RelaxNG $rng? --> Bool)
multi method is-valid(LibXML::Element $elem --> Bool)
multi method is-valid(LibXML::Element $elem, LibXML::Attr $attr)
=end code

Checks that the document, or a an element in the document, is valid. Returns either True or False.

Optionally accepts an element to check. The element may be at any level in the document, and is checked as a sub-tree in isolation.

You may also pass in a L<LibXML::Dtd> object, to validate the document against an external DTD:

    =begin code :lang<raku>
    unless $doc.is-valid($dtd) {
        warn("document is not valid!");
    }
    =end code

=end pod

#| Whether the document was valid when it was parsed
method was-valid returns Bool {
    ? ($.raw.properties +& XML_DOC_DTDVALID)
}
method valid is DEPRECATED<was-valid> { $.was-valid }

=head3 method validate
=begin code :lang<raku>
multi method validate(LibXML::Dtd $dtd?)
multi method validate(LibXML::Schema $schema)
multi method validate(LibXML::RexlaxNG $rng)
multi method validate(LibXML::Element $elem)
multi method validate(LibXML::Element $elem, LibXML::Attr $attr)
=end code
multi method validate($doc: Bool :$check --> Bool) is hidden-from-backtrace {
    my LibXML::Dtd::ValidContext $valid-ctx = self.create: LibXML::Dtd::ValidContext;
    $valid-ctx.validate($doc, :$check);
}
multi method validate($doc: LibXML::_Validator $validator, Bool :$check --> Bool) is hidden-from-backtrace {
    $validator.validate($doc, :$check);
}
multi method validate($doc: LibXML::Element:D $elem, LibXML::Attr $attr?, Bool :$check --> Bool) is hidden-from-backtrace {
    my LibXML::Dtd::ValidContext $valid-ctx = self.create: LibXML::Dtd::ValidContext;
    $valid-ctx.validate($elem, $attr, :$doc, :$check);
}

=begin pod
    =para Validates, either the entire document, or an individual element.
    =para
    This is an exception throwing equivalent of is-valid. If the document is not
    valid it will throw an exception containing the error.

    It is also possible to write: `$elem.validate` as a shortcut for `$elem.ownerDocument.validate($elem)` and `$elem.is-valid` as a shortcut for `$elem.ownerDocument.is-valid($elem)`
=end pod


#| Gets or sets the root element of the Document.
method documentElement is rw is also<root> returns LibXML::Element {
        sub FETCH($) {
            self.getDocumentElement;
        }
        sub STORE($, LibXML::Node $elem) {
            self.setDocumentElement($elem);
        }
        Proxy.new: :&FETCH, :&STORE;
}
=begin pod
    =para
    A document can have just one root element to contain the documents data.
    If the document resides in a different document tree, it is automatically imported.
=end pod

#| Creates a new Element Node bound to the DOM with the given tag (name), Optionally bound to a given name-space;
method createElement(QName $name, Str :$href --> LibXML::Element) {
    $href
        ?? $.createElementNS($href, $name)
        !! self.box: LibXML::Element, $.raw.createElement($name);
}

#| equivalent to .createElement($name, :$href)
method createElementNS(Str $href, QName:D $name --> LibXML::Element) {
    self.box: LibXML::Element, $.raw.createElementNS($href, $name);
}

method !check-new-node($node, |) {
   if $node ~~ LibXML::Element {
       die "Document already has a root element"
           with $.documentElement;
   }
}

multi method createAttribute(NameVal $_!, |c) {
    $.createAttribute(.key, .value, |c);
}

#| Creates a new Attribute node
multi method createAttribute(
    QName:D $qname,
    Str $value = '',
    Str:D :$href!,
    --> LibXML::Attr
) {
    $.createAttributeNS($href, $qname, $value);
}

multi method createAttribute(QName:D $qname, Str $value = '' --> LibXML::Attr) {
    self.box: LibXML::Attr, $.raw.createAttribute($qname, $value);
}

multi method createAttributeNS(Str $href, NameVal $_!, |c) {
    $.createAttributeNS($href, .key, .value, |c);
}
#| Creates an Attribute bound to a name-space.
multi method createAttributeNS(Str $href,
                         QName:D $qname,
                         Str $value = '',
                         --> LibXML::Attr
                        ) {
    self.box: LibXML::Attr, $.raw.createAttributeNS($href, $qname, $value);
}

#| Creates a Document Fragment
method createDocumentFragment($doc: --> LibXML::DocumentFragment) {
    self.create: $.config.class-map[XML_DOCUMENT_FRAG_NODE], :$doc
}

#| Creates a Text Node bound to the DOM.
method createTextNode($doc: Str $content --> LibXML::Text) {
    self.create: $.config.class-map[XML_TEXT_NODE], :$doc, :$content
}

#| Create a Comment Node bound to the DOM
method createComment($doc: Str $content --> LibXML::Comment) {
    self.create: $.config.class-map[XML_COMMENT_NODE], :$doc, :$content
}

#| Create a CData Section bound to the DOM
method createCDATASection($doc: Str $content --> LibXML::CDATA) {
    self.create: $.config.class-map[XML_CDATA_SECTION_NODE], :$doc, :$content
}

#| Creates an Entity Reference
method createEntityReference($doc: Str $name --> LibXML::EntityRef) {
    self.create: $.config.class-map[XML_ENTITY_REF_NODE], :$doc, :$name;
}
=begin pod
    =para
    If a document has a DTD specified, one can create entity references by using
    this function. If one wants to add a entity reference to the document, this
    reference has to be created by this function.

    An entity reference is unique to a document and cannot be passed to other
    documents as other nodes can be passed.

    I<NOTE:> A text content containing something that looks like an entity reference, will
    not be expanded to a real entity reference unless it is a predefined entity

        my Str $text = '&foo;';
        $some_element.appendText( $text );
        print $some_element.textContent; # prints "&amp;foo;"

=end pod

# (or createProcessingInstruction) create a processing instruction node.
proto method createPI(|) is also<createProcessingInstruction> {*}
multi method createPI(NameVal $_!, |c) {
    $.createPI(.key, .value, |c);
}
multi method createPI($doc: NCName $name, Str $content? --> LibXML::PI) {
    self.create: $.config.class-map[XML_PI_NODE], :$doc, :$name, :$content
}

#| Creates a new external subset
method createExternalSubset($doc: Str $name, Str $external-id, Str $system-id --> LibXML::Dtd) {
    self.create: $.config.class-map[XML_DTD_NODE], :$doc, :type<external>, :$name, :$external-id, :$system-id
}
=para This function is similar to C<createInternalSubset()> but this DTD is considered to be external and is therefore not added to the
  document itself. Nevertheless it can be used for validation purposes.

#| Creates a new Internal Subset
method createInternalSubset($doc: Str $name, Str $external-id, Str $system-id --> LibXML::Dtd) {
    self.create: $.config.class-from(XML_DTD_NODE), :$doc, :type<internal>, :$name, :$external-id, :$system-id;
}
=begin pod
    =head3 method createInternalSubset

        my LibXML::Dtd
        $dtd = $doc.createInternalSubset( $rootnode, $public, $system);

    This function creates and adds an internal subset to the given document.
    Because the function automatically adds the DTD to the document there is no
    need to add the created node explicitly to the document.

        my LibXML::Document $doc = LibXML::Document.new();
        my LibXML::Dtd $dtd = $doc.createInternalSubset( "foo", undef, "foo.dtd" );

    will result in the following XML document:
        =begin code :lang<xml>
        <?xml version="1.0"?>
        <!DOCTYPE foo SYSTEM "foo.dtd">
        =end code
    By setting the public parameter it is possible to set PUBLIC DTDs to a given
    document. So

        my LibXML::Document $doc = LibXML::Document.new();
        my LibXML::Dtd $dtd = $doc.createInternalSubset( "foo", "-//FOO//DTD FOO 0.1//EN", undef );

    will cause the following declaration to be created on the document:
        =begin code :lang<xml>
        <?xml version="1.0"?>
        <!DOCTYPE foo PUBLIC "-//FOO//DTD FOO 0.1//EN">
        =end code
=end pod

#| Create a new external DTD
method createDTD(Str $name, Str $external-id, Str $system-id --> LibXML::Dtd) {
    self.create: $.config.class-map[XML_DTD_NODE], :$name, :$external-id, :$system-id, :type<external>
}

#| Lookup an entity in the document
method getEntity(Str $name --> LibXML::Dtd::Entity) {
    self.box: LibXML::Dtd::Entity, $.raw.GetEntity($name);
}

=para Searches any internal subset, external subset, and predefined entities

# don't allow more than one element in the document root
method appendChild(LibXML::Node:D $node)    { self!check-new-node($node); nextsame; }
method addChild(LibXML::Node:D $node)       { self!check-new-node($node); nextsame; }
method insertBefore(LibXML::Node:D $node, LibXML::Node $) { self!check-new-node($node); nextsame; }
method insertAfter(LibXML::Node:D $node, LibXML::Node $)  { self!check-new-node($node); nextsame; }

#| Imports a node from another DOM
proto method importNode(LibXML::Node:D $node --> LibXML::Node) {*}
multi method importNode(LibXML::Document $) { fail "Can't import Document nodes" }
multi method importNode(LibXML::Node:D $node --> LibXML::Node) {
     self.box: LibXML::Node, $.raw.importNode($node.raw);
}
=para If a node is not part of a document, it can be imported to another document. As
    specified in DOM Level 2 Specification the Node will not be altered or removed
    from its original document (C<$node.cloneNode(:deep)> will get called implicitly).

#| Adopts a node from another DOM
proto method adoptNode(LibXML::Node:D $node --> LibXML::Node) {*}
multi method adoptNode(LibXML::Document $) { fail "Can't adopt Document nodes" }
multi method adoptNode(LibXML::Node:D $node --> LibXML::Node)  {
    $node.keep: $.raw.adoptNode($node.raw);
}
=para If a node is not part of a document, it can be adopted by another document. As
    specified in DOM Level 3 Specification the Node will not be altered but it will
    removed from its original document.
=para After a document adopted a node, the node, its attributes and all its
    descendants belong to the new document. Because the node does not belong to the
    old document, it will be unlinked from its old location first.
=para I<NOTE:> Don't try to use importNode() or adoptNode() to import sub-trees that contain entity references -
    even if the entity reference is the root node of the sub-tree. This will cause
    serious problems to your program. This is a limitation of libxml2 and not of
    LibXML itself.

#| DOM compatible method to get the document element
method getDocumentElement returns LibXML::Element is dom-boxed {...}

#| DOM compatible method to set the document element
method setDocumentElement($doc: LibXML::Element:D $elem --> LibXML::Element) {
    # Preempt libxml which unlinks, but doesn't free any current root element
    self.box(LibXML::Element, $_).unbindNode
        with $.raw.getDocumentElement;

    $elem.setOwnerDocument($doc);
    self.raw.setDocumentElement($elem.raw);
    $elem;
}

method insertProcessingInstruction(|c) {
    my LibXML::PI:D $pi = $.createPI(|c);
    with $.documentElement -> $root {
        # this is actually not correct, but i guess it's what the user
        # intends
        $.insertBefore( $pi, $root );
    }
    else {
        # if no documentElement was found we just append the PI
        $.appendChild( $pi );
    }

}

method getInternalSubset(--> LibXML::Dtd) is dom-boxed is also<doctype> {...}

#|This method sets a DTD node as an internal subset of the given document.
method setInternalSubset(LibXML::Dtd:D $dtd is copy --> LibXML::Dtd) {
    with $dtd.raw.doc {
        $dtd .= clone unless .isSameNode($.raw);
    }
    $dtd.keep: self.raw.setInternalSubset: $dtd.raw;
}
=para Inserts a copy of the Dtd node into the document as its internal subset
    =begin code :lang<raku>
    my $new-dtd = $doc.setInternalSubset: $other-doc.getInternalSubset;
    =end code
=para Note: At this stage, only the `name`, `publicId` and `systemId` are copied.

=para This method is currently most useful for setting the document-type of an XML or HTML document:

=begin code :lang<raku>
use LibXML;
use LibXML::Dtd;
use LibXML::Document;
my $htmlPublic = "-//W3C//DTD XHTML 1.0 Transitional//EN";
my $htmlSystem = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd";

my LibXML::Dtd:D $dtd = LibXML.createDocumentType('xhtml', $htmlPublic, $htmlSystem);
my Bool $html = $dtd.is-XHTML;
my LibXML::Document $doc .= new: :$html;
$doc.setInternalSubset: $dtd;
$doc.setDocumentElement: $doc.createElement('xhtml');
say $doc.Str;
# <!DOCTYPE xhtml PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
# <xhtml></xhtml>

=end code

#| This method removes an external, if defined, from the document
method removeInternalSubset(--> LibXML::Dtd) is dom-boxed {...}
=para If a document has an internal subset defined it can be removed from the
    document by using this function. The removed dtd node will be returned.

method setURI(Str $uri) { self.URI = $_ }
method setEncoding(xmlEncodingStr $enc) { $.encoding = $enc }

#| Gets or sets the internal DTD for the document.
method internalSubset is rw returns LibXML::Dtd {
     sub FETCH($) { self.getInternalSubset }
     sub STORE($, LibXML::Dtd $dtd) {
         self.setInternalSubset($dtd);
     }
    Proxy.new: :&FETCH, :&STORE;
}

method getExternalSubset(--> LibXML::Dtd) is dom-boxed {...}

#| This method sets a DTD node as an external