dom.el

Download

;;; dom.el --- DOM implementation

;; Copyright (C) 2001  Alex Schroeder <alex@gnu.org>

;; Author: Alex Schroeder <alex@gnu.org>
;;	Henrik.Motakef <elisp@henrik-motakef.de>
;; Maintainer: Henrik.Motakef <elisp@henrik-motakef.de>
;; Version: 1.0.1
;; Keywords: xml
;; URL: http://www.emacswiki.org/cgi-bin/wiki.pl?XmlParser
;; Version: $Id: dom.el,v 1.1 2002/08/14 20:22:21 henrik Exp henrik $

;; This file is not part of GNU Emacs.

;; This is free software; you can redistribute it and/or modify it under
;; the terms of the GNU General Public License as published by the Free
;; Software Foundation; either version 2, or (at your option) any later
;; version.

;; This is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; If you are working with XML documents, the parsed data structure
;; returned by the XML parser (xml.el) may be enough for you: Lists of
;; lists, symbols, strings, plus a number of accessor functions.
;;
;; If you want a more elaborate data structure to work with your XML
;; document, you can create a document object model (DOM) from the XML
;; data structure using dom.el.
;;
;; You can create a DOM from XML using `dom-make-document-from-xml'.
;; Note that `xml-parse-file' will return a list of top level elements
;; found in the file, so you must choose one element in that list.
;; Here's an example:
;;
;; (setq doc (dom-make-document-from-xml (car (xml-parse-file "sample.xml"))))
;;
;; Note that this DOM implementation uses the attributes and tag names
;; used by the XML parser.  If the XML parser uses symbols instead of
;; string (like xml.el does), then dom.el will also use symbols.  If the
;; XML parsers uses strings (like xml-parse.el does), then dom.el will
;; use strings.
;;
;; It should be trivial to write functions analogous to the
;; dom-*-from-xml functions in order to use an another XML parsers (from
;; psgml.el, for example).

;;; On Interfaces and Classes

;; The elisp DOM implementation uses the dom-node structure to store all
;; attributes.  The various interfaces consist of sets of functions to
;; manipulate these dom-nodes.  The functions of a certain interface
;; share the same prefix.

;;; Test:

;; The test code assumes a file named sample.xml with the following
;; content:

;;   <book id="compiler">
;;     <bookinfo>
;;       <bookbiblio>
;;         <title>My own book!</title>
;;         <edition>First</edition>
;;         <authorgroup>
;;           <author>
;;             <firstname>John</firstname>
;;             <surname>Wiegley</surname>
;;           </author>
;;         </authorgroup>
;;       </bookbiblio>
;;     </bookinfo>
;;     <chapter>
;;       <title>A very small chapter</title>
;;       <para>Wonder where the content is...</para>
;;     </chapter>
;;   </book>

;;; Code:

(require 'cl)
(require 'xml)

;;; Exception DOMException

;; DOM operations only raise exceptions in "exceptional" circumstances,
;; i.e., when an operation is impossible to perform (either for logical
;; reasons, because data is lost, or because the implementation has
;; become unstable). In general, DOM methods return specific error
;; values in ordinary processing situations, such as out-of-bound errors
;; when using NodeList.

;; Implementations should raise other exceptions under other
;; circumstances. For example, implementations should raise an
;; implementation-dependent exception if a null argument is passed.

;; Some languages and object systems do not support the concept of
;; exceptions. For such systems, error conditions may be indicated using
;; native error reporting mechanisms. For some bindings, for example,
;; methods may return error codes similar to those listed in the
;; corresponding method descriptions.

(let ((errors
       ;; Note that the numeric code is not used at the moment.
       '((dom-hierarchy-request-err 3
	  "Node doesn't belong here")
	 (dom-wrong-document-err 4
	  "Node is used in a different document than the one that created it")
	 (dom-not-found-err 8
	  "A reference to a node was made in a context where it does not exist"))))
  (dolist (err errors)
    (put (nth 0 err)
	 'error-conditions
	 (list 'error 'dom-exception (nth 0 err)))
    (put (nth 0 err)
	 'error-message
	 (nth 2 err))))

(defun dom-exception (exception &rest data)
  "Signal error EXCEPTION, possibly providing DATA.
The error signaled has the condition 'dom-exception in addition
to the catch-all 'error and EXCEPTION itself."
  ;; FIXME: Redefine this to do something else?
  (signal exception data))

;;; Interface Document

;; The Document interface represents the entire HTML or XML document.
;; Conceptually, it is the root of the document tree, and provides the
;; primary access to the document's data.

;; Since elements, text nodes, comments, processing instructions, etc.
;; cannot exist outside the context of a Document, the Document interface
;; also contains the factory methods needed to create these objects. The
;; Node objects created have a ownerDocument attribute which associates
;; them with the Document within whose context they were created.

;; createAttribute

;; Creates an Attr of the given name. Note that the Attr instance can
;; then be set on an Element using the setAttributeNode method.

(defun dom-document-create-attribute (doc name)
  "Create an attribute of the given NAME.
DOC is the owner-document."
  (when (stringp name)
    (setq name (intern name)))
  (make-dom-attr
   :name name
   :type dom-attribute-node
   :owner-document doc))

;; createElement

;; Creates an element of the type specified. Note that the instance
;; returned implements the Element interface, so attributes can be
;; specified directly on the returned object.

;; FIXME: In addition, if there are known attributes with default
;; values, Attr nodes representing them are automatically created and
;; attached to the element.  (not implemented)

(defun dom-document-create-element (doc type)
  "Create an element of the given TYPE.
TYPE will be interned, if it is a string.
DOC is the owner-document."
  (when (stringp type)
    (setq type (intern type)))
  (make-dom-element
   :name type
   :type dom-element-node
   :owner-document doc))

;; createTextNode

;; Creates a Text node given the specified string.

(defun dom-document-create-text-node (doc data)
  "Create an element of the type specified by the tag NAME.
DOC is the owner-document."
  (make-dom-text
   :name dom-text-node-name
   :value data
   :type dom-text-node
   :owner-document doc))

;; getElementsByTagName

;; Returns a NodeList of all the Elements with a given tag name in the
;; order in which they are encountered in a preorder traversal of the
;; Document tree.

(defun dom-document-get-elements-by-tag-name (doc tagname)
  "Return a list of all the elements with the given tagname.
The elements are returned in the order in which they are encountered in
a preorder traversal of the document tree.  The special value \"*\"
matches all tags."
  (dom-element-get-elements-by-tag-name-1
   (dom-document-element doc)
   tagname))

;;; Interface Node

;; The Node interface is the primary datatype for the entire Document
;; Object Model. It represents a single node in the document tree. While
;; all objects implementing the Node interface expose methods for dealing
;; with children, not all objects implementing the Node interface may have
;; children. For example, Text nodes may not have children, and adding
;; children to such nodes results in a DOMException being raised.

;; The attributes name, value and attributes are included as a mechanism
;; to get at node information without casting down to the specific
;; derived interface. In cases where there is no obvious mapping of
;; these attributes for a specific type (e.g., value for an Element or
;; attributes for a Comment), this returns null. Note that the
;; specialized interfaces may contain additional and more convenient
;; mechanisms to get and set the relevant information.

;; FIXME: Use symbols instead of numbers?
(defconst dom-element-node 1)
(defconst dom-attribute-node 2)
(defconst dom-text-node 3)
; (defconst dom-cdata-section-node 4)
; (defconst dom-entity-reference-node 5)
; (defconst dom-entity-node 6)
; (defconst dom-processing-instruction-node 7)
; (defconst dom-comment-node 8)
(defconst dom-document-node 9)
; (defconst dom-document-type-node 10)
; (defconst dom-document-fragment-node 11)
; (defconst dom-notation-node 12)

;; Default names used for Text and Document nodes.

(defconst dom-text-node-name '\#text)
(defconst dom-document-node-name '\#document)

;; readonly attribute DOMString        nodeName;
;;          attribute DOMString        nodeValue;
;; readonly attribute unsigned short   nodeType;
;; readonly attribute Node             parentNode;
;; readonly attribute NodeList         childNodes;
;; readonly attribute Node             firstChild;
;; readonly attribute Node             lastChild;
;; readonly attribute Node             previousSibling;
;; readonly attribute Node             nextSibling;
;; readonly attribute NamedNodeMap     attributes;
;; readonly attribute Document         ownerDocument;

(defstruct dom-node
  (name nil :read-only t)
  value
  (type nil :read-only t)
  parent-node
  child-nodes
  attributes
  owner-document)

(defstruct (dom-document (:include dom-node))
  element)

(defstruct (dom-element (:include dom-node)))

(defstruct (dom-attr (:include dom-node))
  owner-element
  specified)

(defstruct (dom-character-data (:include dom-node)))

(defstruct (dom-text (:include dom-character-data)))

;; All functions defined for nodes are defined for documents and
;; elements as well.  Use `dom-node-defun' to define aliases.

(defun dom-node-defun (func)
  "Define aliases for symbol FUNC.
FUNC must have the form dom-node-foo.  The aliases created will be named
dom-document-foo, dom-element-foo, and dom-attr-foo."
  (if (and (fboundp func)
	   (string-match "^dom-node-" (symbol-name func)))
      (let ((method (substring (symbol-name func) 9)))
	(mapc (lambda (prefix)
		(defalias
		  (intern (concat prefix method)) func))
	      '("dom-document-" "dom-element-" "dom-attr-")))
    (error "%S is not a dom function" func)))

;; The followin functions implement the virtual attributes firstChild,
;; lastChild, previousSibling and nextSibling.

(defun dom-node-first-child (node)
  (car (dom-node-child-nodes node)))
(dom-node-defun 'dom-node-first-child)

(defun dom-node-last-child (node)
  (car (last (dom-node-child-nodes node))))
(dom-node-defun 'dom-node-last-child)

(defun dom-node-previous-sibling (node)
  (let ((parent (dom-node-parent-node node)))
    (when parent
      (let ((list (dom-node-child-nodes parent))
	    prev
	    done)
	(while (and (not done) list)
	  (if (eq (car list) node)
	      (setq done t)
	    (setq prev (car list)
		  list (cdr list))))
	prev))))
(dom-node-defun 'dom-node-previous-sibling)

(defun dom-node-next-sibling (node)
  (let ((parent (dom-node-parent-node node)))
    (when parent
      (nth 1 (memq node (dom-node-child-nodes parent))))))
(dom-node-defun 'dom-node-next-sibling)

;; appendChild

;; Adds the node newChild to the end of the list of children of
;; this node. If the newChild is already in the tree, it is
;; first removed.

;; FIXME: newChild of type Node: The node to add.  If it is a DocumentFragment
;; object, the entire contents of the document fragment are moved into
;; the child list of this node

(defun dom-node-append-child (node new-child)
  "Adds NEW-CHILD to the end of the list of children of NODE.
If NEW-CHILD is already in the document tree, it is first removed.
NEW-CHILD will be removed from anywhere in the document!
Return the node added."
  (dom-node-test-new-child node new-child)
  (dom-node-unlink-child-from-parent new-child)
  ;; add new-child at the end of the list
  (let ((children (dom-node-child-nodes node)))
    (setf (dom-node-child-nodes node) (nconc children (list new-child))))
  (setf (dom-node-parent-node new-child) node)
  new-child)
(dom-node-defun 'dom-node-append-child)

;; cloneNode

;; Returns a duplicate of this node, i.e., serves as a generic copy
;; constructor for nodes. The duplicate node has no parent; (parentNode
;; is null.).

;; FIXME: Cloning an Element copies all attributes and their values,
;; including those generated by the XML processor to represent defaulted
;; attributes, but this method does not copy any text it contains unless
;; it is a deep clone, since the text is contained in a child Text
;; node. Cloning an Attribute directly, as opposed to be cloned as part
;; of an Element cloning operation, returns a specified attribute
;; (specified is true). Cloning any other type of node simply returns a
;; copy of this node.  (the attribute specified is not implemented)

;; FIXME: Note that cloning an immutable subtree results in a mutable
;; copy, but the children of an EntityReference clone are readonly. In
;; addition, clones of unspecified Attr nodes are specified. And,
;; cloning Document, DocumentType, Entity, and Notation nodes is
;; implementation dependent.  (immutable subtrees not implemented)

;; FIXME: The specification says nothing about nextSibling and
;; previousSibling.  We set these to nil as well, matching parentNode.

(defun dom-node-clone-node (node &optional deep)
  "Return a duplicate of NODE.
The duplicate node has no parent.  Cloning will copy all attributes and
their values, but this method does not copy any text it contains unless
it is a DEEP clone, since the text is contained in a child text node.

When the optional argument DEEP is non-nil, this recursively clones the
subtree under the specified node; if false, clone only the node itself
\(and its attributes, if it has any)."
  ;; We don't want to call this recursively because of performance.
  (let* ((first-copy (copy-dom-node node))
	 (copy first-copy)
	 stack)
    ;; unlink neighbours of the first copy
    (setf (dom-node-parent-node first-copy) nil)
    (while copy
      ;; prevent sharing of text in text nodes
      (let ((value (dom-node-value copy)))
	(when (and value (sequencep value))
	  (setf (dom-node-value copy) (copy-sequence value))))
      ;; copy attributes, and prevent sharing of text in attribute nodes
      (let ((attributes (mapcar 'copy-dom-node (dom-node-attributes copy))))
	(mapc (lambda (attr)
		(let ((value (dom-node-value attr)))
		  (when (and value (sequencep value))
		    (setf (dom-node-value attr) (copy-sequence value)))))
	      attributes)
	(setf (dom-node-attributes copy) attributes))
      (if (not deep)
	  ;; if this is not a deep copy, we are done
	  (setq copy nil)
	;; first clone all children
	(let ((children (mapcar 'copy-dom-node (dom-node-child-nodes copy)))
	      (parent copy))
	  (when children
	    ;; set the children info for the parent
	    (setf (dom-node-child-nodes parent) children)
	    ;; set parent for all children
	    (mapc (lambda (child)
		    (setf (dom-node-parent-node child) parent))
		  children)))
	;; move to the next copy, depth first, storing missed branches
	;; on the stack -- note that "node" continues to refer to the
	;; original node, it should not be used within the while copy
	;; loop!
	(setq copy
	      (cond ((dom-element-first-child copy)
		     (when (dom-element-next-sibling copy)
		       (push (dom-element-next-sibling copy) stack))
		     (dom-element-first-child copy))
		    ((dom-element-next-sibling copy))
		    (t (pop stack))))))
    first-copy))
(dom-node-defun 'dom-node-clone-node)

;; hasAttributes introduced in DOM Level 2

;; Returns whether this node (if it is an element) has any
;; attributes.

(defun dom-node-has-attributes (node)
  "Return t when NODE has any attributes."
  (not (null (dom-node-attributes node))))
(dom-node-defun 'dom-node-has-attributes)

;; hasChildNodes

;; Returns whether this node has any children.

(defun dom-node-has-child-nodes (node)
  "Return t when NODE has any child nodes."
  (not (null (dom-node-child-nodes node))))
(dom-node-defun 'dom-node-has-child-nodes)

;; insertBefore

;; Inserts the node newChild before the existing child node refChild. If
;; refChild is null, insert newChild at the end of the list of children.

;; FIXME: If newChild is a DocumentFragment object, all of its children
;; are inserted, in the same order, before refChild. If the newChild is
;; already in the tree, it is first removed.

(defun dom-node-insert-before (node new-child &optional ref-child)
  "Insert NEW-CHILD before NODE's existing child REF-CHILD.
If optional argument REF-CHILD is nil or not given, insert NEW-CHILD at
the end of the list of NODE's children.
If NEW-CHILD is already in the document tree, it is first removed.
NEW-CHILD will be removed from anywhere in the document!
Return the node added."
  ;; without ref-child, append it at the end of the list
  (if (not ref-child)
      (dom-node-append-child node new-child)
    (dom-node-test-new-child node new-child)
    (dom-node-unlink-child-from-parent new-child)
    ;; find the correct position and insert new-child
    (let ((children (dom-node-child-nodes node))
	  child-cell done)
      (while (and (not done) children)
	(if (eq ref-child (car children))
	    (progn
	      ;; if the first child is ref-child, set the list anew
	      (if (not child-cell)
		  (setf (dom-node-child-nodes node)
			(cons new-child children))
		;; else splice new-child into the list
		(setcdr child-cell (cons new-child children)))
	      (setq done t))
	  ;; if we didn't find it, advance
	  (setq child-cell children
		children (cdr children))))
      (unless done
	(dom-exception 'dom-not-found-err)))
    new-child))
(dom-node-defun 'dom-node-insert-before)

;; removeChild

;; Removes the child node indicated by oldChild from the list of
;; children, and returns it.

(defun dom-node-remove-child (node old-child)
  "Remove OLD-CHILD from the list of NODE's children and return it.
This is very similar to `dom-node-unlink-child-from-parent' but it will
raise an exception if OLD-CHILD is NODE's child."
  (let ((children (dom-node-child-nodes node)))
    (if (memq old-child children)
	(setf (dom-node-child-nodes node) (delq old-child children)
	      (dom-node-parent-node old-child) nil)
      (dom-exception 'dom-not-found-err))
    old-child))
(dom-node-defun 'dom-node-remove-child)

;; replaceChild

;; Replaces the child node oldChild with newChild in the list of
;; children, and returns the oldChild node.  

;; FIXME: If newChild is a DocumentFragment object, oldChild is replaced
;; by all of the DocumentFragment children, which are inserted in the
;; same order.

;; If the newChild is already in the tree, it is first removed.

(defun dom-node-replace-child (node new-child old-child)
  "Replace OLD-CHILD with NEW-CHILD in the list NODE's children.
Return OLD-CHILD."
  (dom-node-test-new-child node new-child)
  (dom-node-unlink-child-from-parent new-child)
  (let ((children (dom-node-child-nodes node)))
    (unless (memq old-child children)
      (dom-exception 'dom-not-found-err))
    (setf (dom-node-child-nodes node)
	  (nsubstitute new-child old-child children)))
  ;; set parent of new-child and old-child
  (setf (dom-node-parent-node old-child) nil
	(dom-node-parent-node new-child) node))
(dom-node-defun 'dom-node-replace-child)

;; textContent of type DOMString, introduced in DOM Level 3

;; This attribute returns the text content of this node and its
;; descendants.

;;  FIXME: When set, any possible children this node may have are
;; removed and replaced by a single Text node containing the string this
;; attribute is set to.  (not implemented yet)

;; On getting, no serialization is performed, the returned string does
;; not contain any markup. Similarly, on setting, no parsing is
;; performed either, the input string is taken as pure textual content.

(defun dom-node-text-content (node)
  "Return the text content of NODE and its children.
If NODE is an attribute or a text node, its value is returned."
  (if (or (dom-attr-p node)
	  (dom-text-p node))
      (dom-node-value node)
    (apply 'concat
	   (mapcar 'dom-node-value
		   (dom-element-get-elements-by-tag-name
		    node dom-text-node-name)))))
(dom-node-defun 'dom-node-text-content)

(defun dom-node-set-text-content (node data)
  "Set the text content of NODE, replacing all its children.
If NODE is an attribute or a text node, its value is set."
  (if (or (dom-attr-p node)
	  (dom-text-p node))
      (setf (dom-node-value node) data)
    (setf (dom-node-child-nodes node)
	  (list (dom-document-create-text-node
		 (dom-node-owner-document node)
		 data)))))
(dom-node-defun 'dom-node-set-text-content)

(defsetf dom-node-text-content dom-node-set-text-content)

;;; Utility functions

;; These utility functions are defined for nodes only.

(defun dom-node-ancestor-p (node ancestor)
  "Return t if ANCESTOR is an ancestor of NODE in the tree."
  (let ((parent (dom-node-parent-node node))
	result)
    (while (and (not result) parent)
      (setq result (eq parent ancestor)
	    parent (dom-node-parent-node parent)))
    result))

(defun dom-node-valid-child (node child)
  "Return t if CHILD is a valid child for NODE.
This depends on the node-type of NODE and CHILD."
  ;; FIXME: Add stuff as we go along.
  t)

(defun dom-node-test-new-child (node new-child)
  "Check wether NEW-CHILD is acceptable addition to NODE's children."
  (when (or (dom-node-ancestor-p node new-child)
	    (eq new-child node)
	    (not (dom-node-valid-child node new-child)))
    (dom-exception 'dom-hierarchy-request-err))
  (when (not (eq (dom-node-owner-document node)
		 (dom-node-owner-document new-child)))
    (dom-exception 'dom-wrong-document-err))
  new-child)

(defun dom-node-unlink-child-from-parent (node)
  "Unlink NODE from is previous location.
This is very similar to `dom-node-remove-child' but it will check wether
this node is the child of a particular other node."
  ;; remove node from it's old position
  (let ((parent (dom-node-parent-node node)))
    (when parent
      ;; remove from parent's child-nodes and set own parent to nil
      (setf (dom-node-child-nodes parent)
	    (delq node (dom-node-child-nodes parent))
	    (dom-node-parent-node node)
	    nil)))
  node)

;;; Interface NodeList

;; The NodeList interface provides the abstraction of an ordered
;; collection of nodes, without defining or constraining how this
;; collection is implemented. NodeList objects in the DOM are live.

;; The items in the NodeList are accessible via an integral index,
;; starting from 0.

;; This provides alternate names for plain lisp list accessor functions.

(defalias 'dom-node-list-length 'length)

(defun dom-node-list-item (list index); for the sake of argument order
  "Return element at INDEX in LIST.
Equivalent to (nth INDEX NODE)."
  (nth index list))

;; Interface Attr

;; The Attr interface represents an attribute in an Element object.
;; Typically the allowable values for the attribute are defined in a
;; document type definition.

;; Attr objects inherit the Node interface, but since they are not
;; actually child nodes of the element they describe, the DOM does not
;; consider them part of the document tree. Thus, the Node attributes
;; parentNode, previousSibling, and nextSibling have a null value for Attr
;; objects. The DOM takes the view that attributes are properties of
;; elements rather than having a separate identity from the elements they
;; are associated with; this should make it more efficient to implement
;; such features as default attributes associated with all elements of a
;; given type. Furthermore, Attr nodes may not be immediate children of a
;; DocumentFragment. However, they can be associated with Element nodes
;; contained within a DocumentFragment. In short, users and implementors
;; of the DOM need to be aware that Attr nodes have some things in common
;; with other objects inheriting the Node interface, but they also are
;; quite distinct.

;; The attribute's effective value is determined as follows: if this
;; attribute has been explicitly assigned any value, that value is the
;; attribute's effective value; otherwise, if there is a declaration for
;; this attribute, and that declaration includes a default value, then
;; that default value is the attribute's effective value; otherwise, the
;; attribute does not exist on this element in the structure model until
;; it has been explicitly added. Note that the nodeValue attribute on the
;; Attr instance can also be used to retrieve the string version of the
;; attribute's value(s).

;; In XML, where the value of an attribute can contain entity references,
;; the child nodes of the Attr node may be either Text or EntityReference
;; nodes (when these are in use; see the description of EntityReference
;; for discussion). Because the DOM Core is not aware of attribute types,
;; it treats all attribute values as simple strings, even if the DTD or
;; schema declares them as having tokenized types.

;; ownerElement of type Element, readonly, introduced in DOM Level 2

;; The Element node this attribute is attached to or null if
;; this attribute is not in use.

;; Interface Element

;; The Element interface represents an element in an HTML or XML
;; document.  Elements may have attributes associated with them; since
;; the Element interface inherits from Node, the generic Node interface
;; attribute attributes may be used to retrieve the set of all
;; attributes for an element. There are methods on the Element interface
;; to retrieve either an Attr object by name or an attribute value by
;; name. In XML, where an attribute value may contain entity references,
;; an Attr object should be retrieved to examine the possibly fairly
;; complex sub-tree representing the attribute value. On the other hand,
;; in HTML, where all attributes have simple string values, methods to
;; directly access an attribute value can safely be used as a
;; convenience.

(defun dom-element-get-elements-by-tag-name-1 (element name)
  "Return a list of elements with tag NAME.
The elements are ELEMENT, its siblings, and their descendants.
This is used by `dom-element-get-elements-by-tag-name' and
`dom-document-get-elements-by-tag-name'."
  ;; We don't want to call this recursively because of performance.
  (let (stack result)
    (while element
      (when (or (string= name "*")
		(string= name (dom-node-name element)))
	(setq result (cons element result)))
      (setq element
	    (cond ((dom-node-first-child element)
		   (when (dom-node-next-sibling element)
		     (push (dom-node-next-sibling element) stack))
		   (dom-node-first-child element))
		  ((dom-node-next-sibling element))
		  (t (pop stack)))))
    (nreverse result)))

(defun dom-element-get-elements-by-tag-name (element name)
  "Return a list of all descendant of ELEMENT with tag NAME.
The elements are returned in the order in which they are encountered in
a preorder traversal of this element tree."
  (dom-element-get-elements-by-tag-name-1
   (dom-element-first-child element)
   name))

;; Interface Text

;; The Text interface inherits from CharacterData and represents the
;; textual content (termed character data in XML) of an Element or Attr.
;; If there is no markup inside an element's content, the text is
;; contained in a single object implementing the Text interface that is
;; the only child of the element. If there is markup, it is parsed into
;; the information items (elements, comments, etc.) and Text nodes that
;; form the list of children of the element.

;; When a document is first made available via the DOM, there is only one
;; Text node for each block of text. Users may create adjacent Text nodes
;; that represent the contents of a given element without any intervening
;; markup, but should be aware that there is no way to represent the
;; separations between these nodes in XML or HTML, so they will not (in
;; general) persist between DOM editing sessions. The normalize() method
;; on Node merges any such adjacent Text objects into a single node for
;; each block of text.

;; Character data is represented as a plain string.



;;; Converting XML to DOM

;; Converting XML (hierarchy of nodes, simple lists, symbols and
;; strings) to DOM (hierarchy of dom-nodes, defstructs from CL)

(defun dom-make-attribute-from-xml (attribute element doc)
  "Make a DOM node of attributes based on ATTRIBUTE.
Called from `dom-make-element-from-xml'.
ELEMENT is the owner-element.
DOC is the owner-document."
  (let* ((name (car attribute))
	 (value (cdr attribute))
	 (attr (dom-document-create-attribute doc name)))
    (setf (dom-attr-value attr) value
	  (dom-attr-owner-element attr) element)
    attr))

(defun dom-add-children (parent children)
  "Add CHILDREN to PARENT.
CHILDREN is a list of XML NODE elements.  Each must
be converted to a dom-node first."
  (when children
    (setf (dom-node-child-nodes parent)
	  (mapcar (lambda (child)
		    (dom-make-node-from-xml
		     child
		     (dom-node-owner-document parent)))
		  children))
    (mapc (lambda (child)
	    (setf (dom-node-parent-node child)
		  parent))
	  (dom-node-child-nodes parent))))

(defun dom-make-element-from-xml (node owner)
  "Make a DOM element based on NODE.
Called from `dom-make-node-from-xml'.
The atttributes are created by `dom-make-attribute-from-xml'.
OWNER is stored as the owner-document."
  (let* ((children (xml-node-children node))
	 (attributes (xml-node-attributes node))
	 (type (xml-node-name node))
	 (element (dom-document-create-element owner type)))
    (when attributes
      (setf (dom-node-attributes element)
	    (mapcar (lambda (attribute)
		      (dom-make-attribute-from-xml attribute element owner))
		    attributes)))
    (when children
      (dom-add-children element children))
    element))

(defun dom-make-node-from-xml (node owner)
  "Make a DOM node based on NODE.
If NODE is a list, the node is created by `dom-make-element-from-xml'.
OWNER is stored as the owner-document."
  (cond ((stringp node)
	 (dom-document-create-text-node owner node))
	((listp node)
	 (dom-make-element-from-xml node owner))
	(t
	 (error "Illegal node: %S" node))))

(defun dom-make-document-from-xml (node)
  "Return a DOM document based on NODE.
NODE is a node as returned by `xml-parse-file', either
a string or a list.  The DOM nodes are created using
`dom-make-node-from-xml'.

Note that `xml-parse-file' returns a list of elements.
You can only pass one of these nodes as NODE."
  (let* ((doc (make-dom-document
	       :name dom-document-node-name
	       :type dom-document-node))
	 (node (dom-make-node-from-xml node doc)))
    (setf (dom-document-owner-document doc) doc; required in dom-add-children
	  (dom-document-element doc) node)
    doc))

;;; Test stuff

(eval-when-compile
  (when (file-readable-p "sample.xml")
    (let ((data (car (xml-parse-file "sample.xml"))))
      ;; (setq data (car (xml-parse-file "sample.xml")))
      (assert (fboundp 'dom-node-name))
      (assert (fboundp 'dom-document-name))
      (assert (fboundp 'dom-element-name))
      (assert (fboundp 'dom-attr-name))

      (let ((attr (dom-make-attribute-from-xml
		   (car (xml-node-attributes data)) 'none 'none)))
	(assert (string= "id" (dom-node-name attr)))
	(assert (string= "compiler" (dom-node-value attr)))
	(assert (eq dom-attribute-node (dom-node-type attr))))

      (let ((element (dom-make-node-from-xml data 'no-owner)))
	(assert (string= "book" (dom-node-name element)))
	(assert (string= "id" (dom-node-name
			       (car (dom-node-attributes element)))))
	(assert (string= "compiler"
			 (dom-node-value
			  (car (dom-node-attributes element)))))
	(assert (string= "bookinfo"
		    (dom-node-name 
		     (first (dom-node-child-nodes element)))))
	(assert (string= "chapter"
			 (dom-node-name
			  (second (dom-node-child-nodes element)))))
	(let ((title (first
		      (dom-node-child-nodes
		       (first
			(dom-node-child-nodes
			 (first
			  (dom-node-child-nodes element))))))))
	  (assert (eq 'title (dom-node-name title)))
	  (assert (string= "My own book!"
			   (dom-node-value
			    (first (dom-node-child-nodes title)))))))

      (let ((doc (dom-make-document-from-xml data)))
	(assert (eq dom-document-node-name (dom-document-name doc)))
	(assert (string= "book" (dom-node-name (dom-document-element doc))))
	(assert (eq (dom-node-parent-node
		     (first (dom-node-child-nodes (dom-document-element doc))))
		    (dom-document-element doc)))
	(assert (eq (first (dom-node-child-nodes (dom-document-element doc)))
		    (dom-node-first-child (dom-document-element doc))))
	(assert (eq (dom-node-next-sibling
		     (first (dom-node-child-nodes (dom-document-element doc))))
		    (second (dom-node-child-nodes (dom-document-element doc)))))
	(assert (eq doc
		    (dom-node-owner-document
		     (dom-node-first-child (dom-document-element doc)))))
	(assert (string= "chapter"
			 (dom-node-name
			  (dom-element-last-child
			   (dom-document-element doc)))))
	(assert (eq nil (dom-node-previous-sibling (dom-document-element doc)))))

      (assert (eq 3 (dom-node-list-length '(1 2 3))))

      (assert (eq 2 (dom-node-list-item '(1 2 3) 1)))

      (let ((doc (dom-make-document-from-xml data)))
	(assert (equal (mapcar 'dom-node-name
			       (dom-document-get-elements-by-tag-name
				doc '*))
		       '(book bookinfo bookbiblio title \#text edition
			      \#text authorgroup author firstname \#text
			      surname \#text chapter title \#text para
			      \#text)))
	(assert (equal (mapcar 'dom-node-name
			       (dom-document-get-elements-by-tag-name
				doc 'title))
		       '(title title)))
	(assert (equal (mapcar 'dom-node-name
			       (dom-element-get-elements-by-tag-name
				(dom-document-element doc) 'title))
		       '(title title)))
	(assert (equal (mapcar (lambda (element)
				 (dom-node-value
				  (dom-element-first-child element)))
			       (dom-document-get-elements-by-tag-name
				doc 'title))
		       '("My own book!" "A very small chapter"))))

      (let* ((doc (dom-make-document-from-xml data))
	     (ancestor (dom-document-element doc))
	     (child (car (dom-document-get-elements-by-tag-name doc 'title))))
	(assert (dom-node-ancestor-p child ancestor)))

      (let* ((doc (dom-make-document-from-xml data))
	     (book (dom-document-element doc))
	     (old-chapter (dom-element-last-child book))
	     (new-chapter (dom-document-create-element doc 'chapter)))
	(assert (string= (dom-node-name
			  (dom-element-append-child book new-chapter))
			 "chapter"))
	(assert (equal (mapcar 'dom-element-name
			       (dom-element-child-nodes book))
		       '(bookinfo chapter chapter)))
	(assert (eq (dom-element-last-child book) new-chapter))
	(assert (not (eq (dom-element-last-child book) old-chapter)))
	(assert (eq (dom-element-next-sibling old-chapter) new-chapter))
	(assert (eq (dom-element-previous-sibling new-chapter) old-chapter))
	(assert (eq (dom-element-parent-node new-chapter) book))
	(assert (dom-node-ancestor-p new-chapter book))
	(assert (not (eq t (condition-case var
			       (dom-element-append-child book new-chapter)
			     ('dom-hierarchy-request-err
			      t)))))
	(assert (eq t (condition-case var
			  (dom-element-append-child new-chapter book)
			('dom-hierarchy-request-err
			 t)))))

      (let* ((doc (dom-make-document-from-xml data))
	     (book (dom-document-element doc))
	     (old-chapter (dom-element-last-child book))
	     (new-chapter (dom-document-create-element doc 'chapter))
	     (new-title (dom-document-create-element doc 'title))
	     (text (dom-document-create-text-node doc "Test Chapter")))
	(assert (eq text (dom-element-append-child
			  (dom-element-append-child
			   (dom-element-append-child book new-chapter)
			   new-title)
			  text)))
	(assert (= 2 (length (dom-node-child-nodes old-chapter))))
	(assert (= 1 (length (dom-node-child-nodes new-chapter))))
	(assert (string= "title" (dom-node-name
			    (car (dom-node-child-nodes new-chapter)))))
	(assert (eq (car (dom-node-child-nodes new-chapter))
		    (dom-node-first-child new-chapter)))
	(assert (eq new-title
		    (dom-node-first-child new-chapter)))
	(assert (eq text
		    (dom-node-first-child new-title)))
	(assert (equal
		 (mapcar (lambda (node)
			   (dom-node-value
			    (dom-node-first-child node)))
			 (dom-document-get-elements-by-tag-name doc 'title))
		 '("My own book!" "A very small chapter" "Test Chapter"))))

      (let* ((doc (dom-make-document-from-xml data))
	     (book (dom-document-element doc))
	     (copy (dom-node-clone-node book)))
	(assert (not (eq book copy)))
	(assert (eq (dom-node-child-nodes book)
		    (dom-node-child-nodes copy)))
	(assert (eq (car (dom-node-child-nodes book))
		    (car (dom-node-child-nodes copy))))
	(assert (eq (dom-node-first-child book)
		    (dom-node-first-child copy)))
	(assert (eq (dom-node-last-child book)
		    (dom-node-last-child copy)))
	(assert (not (eq (dom-node-attributes book)
			 (dom-node-attributes copy))))
	(assert (eq (dom-node-name (car (dom-node-attributes book)))
		    (dom-node-name (car (dom-node-attributes copy)))))
	(assert (not (eq (dom-node-value (car (dom-node-attributes book)))
			 (dom-node-value (car (dom-node-attributes copy))))))
	(assert (equal (dom-node-value (car (dom-node-attributes book)))
		       (dom-node-value (car (dom-node-attributes copy))))))

      (let* ((doc (dom-make-document-from-xml data))
	     (book (dom-document-element doc))
	     (deepcopy (dom-node-clone-node book t)))
	(assert (not (eq book deepcopy)))
	(assert (equal (dom-node-attributes book)
		       (dom-node-attributes deepcopy)))
	(assert (not (eq (dom-node-attributes book)
			 (dom-node-attributes deepcopy))))
	(assert (equal
		 (mapcar 'dom-node-name
			 (dom-element-get-elements-by-tag-name book '*))
		 (mapcar 'dom-node-name
			 (dom-element-get-elements-by-tag-name deepcopy '*))))
	(assert (equal
		 (mapcar 'dom-node-value
			 (dom-element-get-elements-by-tag-name book '*))
		 (mapcar 'dom-node-value
			 (dom-element-get-elements-by-tag-name deepcopy '*))))
	(assert (not (eq (car (dom-element-get-elements-by-tag-name
			       book 'firstname))
			 (car (dom-element-get-elements-by-tag-name
			       deepcopy 'firstname)))))
	(assert (not (eq (dom-text-value
			  (third (dom-element-get-elements-by-tag-name
				  book '\#text)))
			 (dom-text-value
			  (third (dom-element-get-elements-by-tag-name
				  deepcopy '\#text))))))
	(assert (string= (dom-text-value
			  (third (dom-element-get-elements-by-tag-name
				  book '\#text)))
			 (dom-text-value
			  (third (dom-element-get-elements-by-tag-name
				  deepcopy '\#text)))))
	(assert (not (eq (dom-text-value
			  (third (dom-element-get-elements-by-tag-name
				  book '\#text)))
			 (dom-text-value
			  (third (dom-element-get-elements-by-tag-name
				  deepcopy '\#text)))))))
      
      (let* ((doc (dom-make-document-from-xml data))
	     (book (dom-document-element doc))
	     (old-chapter (dom-element-last-child book))
	     (new-chapter (dom-document-create-element doc 'chapter)))
	(assert (eq (dom-node-name (dom-element-insert-before book new-chapter))
		    'chapter))
	(assert (equal (mapcar 'dom-element-name
			       (dom-element-child-nodes book))
		       '(bookinfo chapter chapter)))
	(assert (eq new-chapter (dom-element-insert-before 
				 book new-chapter
				 (dom-element-first-child book))))
	(assert (equal (mapcar 'dom-element-name
			       (dom-element-child-nodes book))
		       '(chapter bookinfo chapter)))
	(let ((new-bookinfo (dom-document-create-element doc 'bookinfo)))
	   (dom-element-insert-before book new-bookinfo old-chapter))
	(assert (equal (mapcar 'dom-element-name
			       (dom-element-child-nodes book))
		       '(chapter bookinfo bookinfo chapter))))

      ;; FIXME: some more tests for `dom-node-remove-child' and
      ;; `dom-node-replace-child' would be nice...  :)
      (let* ((doc (dom-make-document-from-xml data))
	     (book (dom-document-element doc))
	     (old-chapter (dom-element-last-child book))
	     (new-chapter (dom-document-create-element doc 'chapter)))
	(dom-node-remove-child book old-chapter)
	(assert (equal (mapcar 'dom-node-name (dom-node-child-nodes book))
		       '(bookinfo)))
	(dom-node-replace-child book new-chapter
				(dom-node-first-child book))
	(assert (equal (mapcar 'dom-node-name (dom-node-child-nodes book))
		       '(chapter))))

      (let* ((doc (make-dom-document))
	     (par (dom-document-create-element doc 'p))
	     (part1 (dom-document-create-text-node doc "This is "))
	     (part2 (dom-document-create-element doc 'b))
	     (part3 (dom-document-create-text-node doc ".")))
	(dom-element-append-child 
	 part2 (dom-document-create-text-node doc "bold"))
	(dom-element-append-child par part1)
	(dom-element-append-child par part2)
	(dom-element-append-child par part3)
	(setf (dom-document-owner-document doc) doc
	      (dom-document-element doc) par)
	(assert (eq (dom-document-element doc) par))
	(assert (string= (dom-node-text-content par)
			 "This is bold."))
	(dom-node-set-text-content par "This is plain.")
	(assert (string= (dom-node-text-content par)
			 "This is plain."))
	(assert (equal (mapcar 'dom-node-name (dom-node-child-nodes par))
		       '(\#text)))
	(setf (dom-node-text-content par) "New text.")
	(assert (string= (dom-node-text-content par)
			 "New text."))
	(setf (dom-element-text-content par) "Different text.")
	(assert (string= (dom-element-text-content par)
			 "Different text."))
	(let ((at (dom-document-create-attribute doc 'foo)))
	  (setf (dom-attr-value at) "domino"
		(dom-element-attributes par) (list at))
	  (assert (string= "domino"
			   (dom-node-value
			    (dom-node-list-item
			     (dom-element-attributes par)
			     0))))
	  (assert (string= "domino"
			   (dom-node-text-content
			    (dom-node-list-item
			     (dom-element-attributes par)
			     0))))))

      (let* ((doc (dom-make-document-from-xml data))
	     (title (car (dom-document-get-elements-by-tag-name doc "title"))))
	(assert (equal (dom-element-text-content title)
		       "My own book!"))))))

(provide 'dom)

;;; dom.el ends here.