(widget-choose): Fix use of character constant.
[bpt/emacs.git] / lisp / xml.el
CommitLineData
1cd7adc6 1;;; xml.el --- XML parser
47db06aa 2
1300e272 3;; Copyright (C) 2000, 2001 Free Software Foundation, Inc.
47db06aa
GM
4
5;; Author: Emmanuel Briot <briot@gnat.com>
6;; Maintainer: Emmanuel Briot <briot@gnat.com>
7;; Keywords: xml
8
9;; This file is part of GNU Emacs.
10
11;; GNU Emacs is free software; you can redistribute it and/or modify
12;; it under the terms of the GNU General Public License as published by
13;; the Free Software Foundation; either version 2, or (at your option)
14;; any later version.
15
16;; GNU Emacs is distributed in the hope that it will be useful,
17;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19;; GNU General Public License for more details.
20
21;; You should have received a copy of the GNU General Public License
22;; along with GNU Emacs; see the file COPYING. If not, write to the
23;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
24;; Boston, MA 02111-1307, USA.
25
26;;; Commentary:
27
28;; This file contains a full XML parser. It parses a file, and returns a list
29;; that can be used internally by any other lisp file.
30;; See some example in todo.el
31
32;;; FILE FORMAT
33
34;; It does not parse the DTD, if present in the XML file, but knows how to
35;; ignore it. The XML file is assumed to be well-formed. In case of error, the
36;; parsing stops and the XML file is shown where the parsing stopped.
37;;
38;; It also knows how to ignore comments, as well as the special ?xml? tag
39;; in the XML file.
40;;
41;; The XML file should have the following format:
653558a1
GM
42;; <node1 attr1="name1" attr2="name2" ...>value
43;; <node2 attr3="name3" attr4="name4">value2</node2>
44;; <node3 attr5="name5" attr6="name6">value3</node3>
47db06aa
GM
45;; </node1>
46;; Of course, the name of the nodes and attributes can be anything. There can
47;; be any number of attributes (or none), as well as any number of children
48;; below the nodes.
49;;
50;; There can be only top level node, but with any number of children below.
51
52;;; LIST FORMAT
53
54;; The functions `xml-parse-file' and `xml-parse-tag' return a list with
55;; the following format:
56;;
57;; xml-list ::= (node node ...)
58;; node ::= (tag_name attribute-list . child_node_list)
59;; child_node_list ::= child_node child_node ...
60;; child_node ::= node | string
61;; tag_name ::= string
62;; attribute_list ::= (("attribute" . "value") ("attribute" . "value") ...)
63;; | nil
64;; string ::= "..."
65;;
47db06aa
GM
66;; Some macros are provided to ease the parsing of this list
67
68;;; Code:
69
70;;*******************************************************************
71;;**
72;;** Macros to parse the list
73;;**
74;;*******************************************************************
75
971489ea 76(defsubst xml-node-name (node)
47db06aa
GM
77 "Return the tag associated with NODE.
78The tag is a lower-case symbol."
971489ea 79 (car node))
47db06aa 80
971489ea 81(defsubst xml-node-attributes (node)
47db06aa
GM
82 "Return the list of attributes of NODE.
83The list can be nil."
971489ea 84 (nth 1 node))
47db06aa 85
971489ea 86(defsubst xml-node-children (node)
47db06aa
GM
87 "Return the list of children of NODE.
88This is a list of nodes, and it can be nil."
971489ea 89 (cddr node))
47db06aa
GM
90
91(defun xml-get-children (node child-name)
92 "Return the children of NODE whose tag is CHILD-NAME.
93CHILD-NAME should be a lower case symbol."
971489ea
SM
94 (let ((match ()))
95 (dolist (child (xml-node-children node))
96 (if child
97 (if (equal (xml-node-name child) child-name)
98 (push child match))))
99 (nreverse match)))
47db06aa
GM
100
101(defun xml-get-attribute (node attribute)
102 "Get from NODE the value of ATTRIBUTE.
103An empty string is returned if the attribute was not found."
104 (if (xml-node-attributes node)
105 (let ((value (assoc attribute (xml-node-attributes node))))
106 (if value
107 (cdr value)
108 ""))
109 ""))
110
111;;*******************************************************************
112;;**
113;;** Creating the list
114;;**
115;;*******************************************************************
116
117(defun xml-parse-file (file &optional parse-dtd)
118 "Parse the well-formed XML FILE.
653558a1 119If FILE is already edited, this will keep the buffer alive.
47db06aa
GM
120Returns the top node with all its children.
121If PARSE-DTD is non-nil, the DTD is parsed rather than skipped."
653558a1
GM
122 (let ((keep))
123 (if (get-file-buffer file)
124 (progn
125 (set-buffer (get-file-buffer file))
126 (setq keep (point)))
127 (find-file file))
524425ae 128
653558a1
GM
129 (let ((xml (xml-parse-region (point-min)
130 (point-max)
131 (current-buffer)
132 parse-dtd)))
133 (if keep
134 (goto-char keep)
135 (kill-buffer (current-buffer)))
136 xml)))
47db06aa
GM
137
138(defun xml-parse-region (beg end &optional buffer parse-dtd)
139 "Parse the region from BEG to END in BUFFER.
140If BUFFER is nil, it defaults to the current buffer.
141Returns the XML list for the region, or raises an error if the region
142is not a well-formed XML file.
143If PARSE-DTD is non-nil, the DTD is parsed rather than skipped,
144and returned as the first element of the list"
145 (let (xml result dtd)
146 (save-excursion
147 (if buffer
148 (set-buffer buffer))
149 (goto-char beg)
150 (while (< (point) end)
151 (if (search-forward "<" end t)
152 (progn
153 (forward-char -1)
154 (if (null xml)
155 (progn
971489ea 156 (setq result (xml-parse-tag end parse-dtd))
47db06aa 157 (cond
971489ea 158 ((null result))
47db06aa 159 ((listp (car result))
971489ea 160 (setq dtd (car result))
47db06aa
GM
161 (add-to-list 'xml (cdr result)))
162 (t
163 (add-to-list 'xml result))))
164
165 ;; translation of rule [1] of XML specifications
1cd7adc6 166 (error "XML files can have only one toplevel tag")))
47db06aa
GM
167 (goto-char end)))
168 (if parse-dtd
169 (cons dtd (reverse xml))
170 (reverse xml)))))
171
172
173(defun xml-parse-tag (end &optional parse-dtd)
174 "Parse the tag that is just in front of point.
175The end tag must be found before the position END in the current buffer.
176If PARSE-DTD is non-nil, the DTD of the document, if any, is parsed and
177returned as the first element in the list.
178Returns one of:
179 - a list : the matching node
180 - nil : the point is not looking at a tag.
181 - a cons cell: the first element is the DTD, the second is the node"
182 (cond
183 ;; Processing instructions (like the <?xml version="1.0"?> tag at the
184 ;; beginning of a document)
185 ((looking-at "<\\?")
186 (search-forward "?>" end)
adb266ef 187 (goto-char (- (re-search-forward "[^[:space:]]") 1))
47db06aa
GM
188 (xml-parse-tag end))
189 ;; Character data (CDATA) sections, in which no tag should be interpreted
190 ((looking-at "<!\\[CDATA\\[")
191 (let ((pos (match-end 0)))
192 (unless (search-forward "]]>" end t)
193 (error "CDATA section does not end anywhere in the document"))
194 (buffer-substring-no-properties pos (match-beginning 0))))
195 ;; DTD for the document
196 ((looking-at "<!DOCTYPE")
197 (let (dtd)
198 (if parse-dtd
971489ea 199 (setq dtd (xml-parse-dtd end))
47db06aa 200 (xml-skip-dtd end))
adb266ef 201 (goto-char (- (re-search-forward "[^[:space:]]") 1))
47db06aa
GM
202 (if dtd
203 (cons dtd (xml-parse-tag end))
204 (xml-parse-tag end))))
205 ;; skip comments
206 ((looking-at "<!--")
207 (search-forward "-->" end)
971489ea 208 nil)
47db06aa
GM
209 ;; end tag
210 ((looking-at "</")
211 '())
212 ;; opening tag
adb266ef 213 ((looking-at "<\\([^/>[:space:]]+\\)")
971489ea
SM
214 (goto-char (match-end 1))
215 (let* ((case-fold-search nil) ;; XML is case-sensitive.
216 (node-name (match-string 1))
217 ;; Parse the attribute list.
218 (children (list (xml-parse-attlist end) (intern node-name)))
47db06aa 219 pos)
47db06aa
GM
220
221 ;; is this an empty element ?
adb266ef 222 (if (looking-at "/[[:space:]]*>")
47db06aa
GM
223 (progn
224 (forward-char 2)
971489ea 225 (nreverse (cons '("") children)))
47db06aa
GM
226
227 ;; is this a valid start tag ?
e54030af 228 (if (eq (char-after) ?>)
47db06aa
GM
229 (progn
230 (forward-char 1)
971489ea
SM
231 ;; Now check that we have the right end-tag. Note that this
232 ;; one might contain spaces after the tag name
adb266ef 233 (while (not (looking-at (concat "</" node-name "[[:space:]]*>")))
47db06aa
GM
234 (cond
235 ((looking-at "</")
236 (error (concat
237 "XML: invalid syntax -- invalid end tag (expecting "
238 node-name
653558a1 239 ") at pos " (number-to-string (point)))))
47db06aa 240 ((= (char-after) ?<)
971489ea
SM
241 (let ((tag (xml-parse-tag end)))
242 (when tag
243 (push tag children))))
47db06aa 244 (t
971489ea 245 (setq pos (point))
47db06aa
GM
246 (search-forward "<" end)
247 (forward-char -1)
248 (let ((string (buffer-substring-no-properties pos (point)))
249 (pos 0))
524425ae 250
47db06aa
GM
251 ;; Clean up the string (no newline characters)
252 ;; Not done, since as per XML specifications, the XML processor
253 ;; should always pass the whole string to the application.
254 ;; (while (string-match "\\s +" string pos)
971489ea
SM
255 ;; (setq string (replace-match " " t t string))
256 ;; (setq pos (1+ (match-beginning 0))))
257
258 (setq string (xml-substitute-special string))
259 (setq children
260 (if (stringp (car children))
261 ;; The two strings were separated by a comment.
262 (cons (concat (car children) string)
263 (cdr children))
264 (cons string children)))))))
47db06aa 265 (goto-char (match-end 0))
47db06aa 266 (if (> (point) end)
1cd7adc6 267 (error "XML: End tag for %s not found before end of region"
47db06aa 268 node-name))
971489ea 269 (nreverse children))
47db06aa
GM
270
271 ;; This was an invalid start tag
272 (error "XML: Invalid attribute list")
273 ))))
1300e272 274 (t ;; This is not a tag.
1cd7adc6 275 (error "XML: Invalid character"))
47db06aa
GM
276 ))
277
278(defun xml-parse-attlist (end)
279 "Return the attribute-list that point is looking at.
280The search for attributes end at the position END in the current buffer.
281Leaves the point on the first non-blank character after the tag."
971489ea 282 (let ((attlist ())
47db06aa 283 name)
adb266ef
JB
284 (goto-char (- (re-search-forward "[^[:space:]]") 1))
285 (while (looking-at "\\([a-zA-Z_:][-a-zA-Z0-9._:]*\\)[[:space:]]*=[[:space:]]*")
971489ea 286 (setq name (intern (match-string 1)))
47db06aa
GM
287 (goto-char (match-end 0))
288
289 ;; Do we have a string between quotes (or double-quotes),
290 ;; or a simple word ?
01adac0d
SZ
291 (unless (looking-at "\"\\([^\"]*\\)\"")
292 (unless (looking-at "'\\([^']*\\)'")
1cd7adc6 293 (error "XML: Attribute values must be given between quotes")))
47db06aa
GM
294
295 ;; Each attribute must be unique within a given element
296 (if (assoc name attlist)
1cd7adc6 297 (error "XML: each attribute must be unique within an element"))
524425ae 298
971489ea 299 (push (cons name (match-string-no-properties 1)) attlist)
47db06aa 300 (goto-char (match-end 0))
adb266ef 301 (goto-char (- (re-search-forward "[^[:space:]]") 1))
47db06aa 302 (if (> (point) end)
1cd7adc6 303 (error "XML: end of attribute list not found before end of region"))
47db06aa 304 )
971489ea 305 (nreverse attlist)))
47db06aa
GM
306
307;;*******************************************************************
308;;**
309;;** The DTD (document type declaration)
310;;** The following functions know how to skip or parse the DTD of
311;;** a document
312;;**
313;;*******************************************************************
314
315(defun xml-skip-dtd (end)
316 "Skip the DTD that point is looking at.
317The DTD must end before the position END in the current buffer.
318The point must be just before the starting tag of the DTD.
319This follows the rule [28] in the XML specifications."
320 (forward-char (length "<!DOCTYPE"))
adb266ef 321 (if (looking-at "[[:space:]]*>")
47db06aa
GM
322 (error "XML: invalid DTD (excepting name of the document)"))
323 (condition-case nil
324 (progn
325 (forward-word 1) ;; name of the document
adb266ef 326 (goto-char (- (re-search-forward "[^[:space:]]") 1))
47db06aa 327 (if (looking-at "\\[")
adb266ef 328 (re-search-forward "\\][[:space:]]*>" end)
47db06aa
GM
329 (search-forward ">" end)))
330 (error (error "XML: No end to the DTD"))))
331
332(defun xml-parse-dtd (end)
333 "Parse the DTD that point is looking at.
334The DTD must end before the position END in the current buffer."
971489ea 335 (forward-char (length "<!DOCTYPE"))
adb266ef 336 (goto-char (- (re-search-forward "[^[:space:]]") 1))
971489ea
SM
337 (if (looking-at ">")
338 (error "XML: invalid DTD (excepting name of the document)"))
524425ae 339
971489ea
SM
340 ;; Get the name of the document
341 (looking-at "\\sw+")
342 (let ((dtd (list (match-string-no-properties 0) 'dtd))
343 type element end-pos)
47db06aa
GM
344 (goto-char (match-end 0))
345
adb266ef 346 (goto-char (- (re-search-forward "[^[:space:]]") 1))
47db06aa
GM
347
348 ;; External DTDs => don't know how to handle them yet
349 (if (looking-at "SYSTEM")
1cd7adc6 350 (error "XML: Don't know how to handle external DTDs"))
524425ae 351
47db06aa 352 (if (not (= (char-after) ?\[))
1cd7adc6 353 (error "XML: Unknown declaration in the DTD"))
47db06aa
GM
354
355 ;; Parse the rest of the DTD
356 (forward-char 1)
adb266ef 357 (while (and (not (looking-at "[[:space:]]*\\]"))
47db06aa
GM
358 (<= (point) end))
359 (cond
360
361 ;; Translation of rule [45] of XML specifications
362 ((looking-at
adb266ef 363 "[[:space:]]*<!ELEMENT[[:space:]]+\\([a-zA-Z0-9.%;]+\\)[[:space:]]+\\([^>]+\\)>")
47db06aa 364
6eb6c4c1 365 (setq element (intern (match-string-no-properties 1))
47db06aa 366 type (match-string-no-properties 2))
971489ea 367 (setq end-pos (match-end 0))
524425ae 368
47db06aa
GM
369 ;; Translation of rule [46] of XML specifications
370 (cond
adb266ef 371 ((string-match "^EMPTY[[:space:]]*$" type) ;; empty declaration
971489ea 372 (setq type 'empty))
adb266ef 373 ((string-match "^ANY[[:space:]]*$" type) ;; any type of contents
971489ea 374 (setq type 'any))
adb266ef 375 ((string-match "^(\\(.*\\))[[:space:]]*$" type) ;; children ([47])
971489ea 376 (setq type (xml-parse-elem-type (match-string-no-properties 1 type))))
adb266ef 377 ((string-match "^%[^;]+;[[:space:]]*$" type) ;; substitution
47db06aa
GM
378 nil)
379 (t
380 (error "XML: Invalid element type in the DTD")))
381
382 ;; rule [45]: the element declaration must be unique
383 (if (assoc element dtd)
1cd7adc6 384 (error "XML: elements declaration must be unique in a DTD (<%s>)"
47db06aa 385 (symbol-name element)))
524425ae 386
47db06aa 387 ;; Store the element in the DTD
971489ea
SM
388 (push (list element type) dtd)
389 (goto-char end-pos))
47db06aa
GM
390
391
392 (t
393 (error "XML: Invalid DTD item"))
394 )
395 )
396
397 ;; Skip the end of the DTD
398 (search-forward ">" end)
971489ea 399 (nreverse dtd)))
47db06aa
GM
400
401
402(defun xml-parse-elem-type (string)
403 "Convert a STRING for an element type into an elisp structure."
404
405 (let (elem modifier)
406 (if (string-match "(\\([^)]+\\))\\([+*?]?\\)" string)
407 (progn
408 (setq elem (match-string 1 string)
409 modifier (match-string 2 string))
410 (if (string-match "|" elem)
971489ea 411 (setq elem (cons 'choice
47db06aa
GM
412 (mapcar 'xml-parse-elem-type
413 (split-string elem "|"))))
414 (if (string-match "," elem)
971489ea 415 (setq elem (cons 'seq
47db06aa
GM
416 (mapcar 'xml-parse-elem-type
417 (split-string elem ","))))
418 )))
adb266ef 419 (if (string-match "[[:space:]]*\\([^+*?]+\\)\\([+*?]?\\)" string)
47db06aa
GM
420 (setq elem (match-string 1 string)
421 modifier (match-string 2 string))))
422
971489ea
SM
423 (if (and (stringp elem) (string= elem "#PCDATA"))
424 (setq elem 'pcdata))
524425ae 425
971489ea
SM
426 (cond
427 ((string= modifier "+")
428 (list '+ elem))
429 ((string= modifier "*")
430 (list '* elem))
431 ((string= modifier "?")
432 (list '? elem))
433 (t
434 elem))))
47db06aa
GM
435
436
437;;*******************************************************************
438;;**
439;;** Substituting special XML sequences
440;;**
441;;*******************************************************************
442
443(defun xml-substitute-special (string)
444 "Return STRING, after subsituting special XML sequences."
47db06aa 445 (while (string-match "&lt;" string)
971489ea 446 (setq string (replace-match "<" t nil string)))
47db06aa 447 (while (string-match "&gt;" string)
971489ea 448 (setq string (replace-match ">" t nil string)))
47db06aa 449 (while (string-match "&apos;" string)
971489ea 450 (setq string (replace-match "'" t nil string)))
47db06aa 451 (while (string-match "&quot;" string)
971489ea 452 (setq string (replace-match "\"" t nil string)))
ceabd272 453 ;; This goes last so it doesn't confuse the matches above.
524425ae
TTN
454 (while (string-match "&amp;" string)
455 (setq string (replace-match "&" t nil string)))
47db06aa
GM
456 string)
457
458;;*******************************************************************
459;;**
460;;** Printing a tree.
461;;** This function is intended mainly for debugging purposes.
462;;**
463;;*******************************************************************
464
465(defun xml-debug-print (xml)
971489ea
SM
466 (dolist (node xml)
467 (xml-debug-print-internal node "")))
47db06aa 468
971489ea 469(defun xml-debug-print-internal (xml indent-string)
47db06aa
GM
470 "Outputs the XML tree in the current buffer.
471The first line indented with INDENT-STRING."
472 (let ((tree xml)
473 attlist)
47db06aa 474 (insert indent-string "<" (symbol-name (xml-node-name tree)))
524425ae 475
47db06aa 476 ;; output the attribute list
971489ea 477 (setq attlist (xml-node-attributes tree))
47db06aa
GM
478 (while attlist
479 (insert " ")
480 (insert (symbol-name (caar attlist)) "=\"" (cdar attlist) "\"")
971489ea 481 (setq attlist (cdr attlist)))
524425ae 482
47db06aa 483 (insert ">")
524425ae 484
971489ea 485 (setq tree (xml-node-children tree))
47db06aa
GM
486
487 ;; output the children
971489ea 488 (dolist (node tree)
47db06aa 489 (cond
971489ea 490 ((listp node)
47db06aa 491 (insert "\n")
971489ea
SM
492 (xml-debug-print-internal node (concat indent-string " ")))
493 ((stringp node) (insert node))
47db06aa 494 (t
971489ea 495 (error "Invalid XML tree"))))
47db06aa
GM
496
497 (insert "\n" indent-string
971489ea 498 "</" (symbol-name (xml-node-name xml)) ">")))
47db06aa
GM
499
500(provide 'xml)
501
502;;; xml.el ends here