(diff-mode): Don't evaluate `compilation-last-buffer' unless it's bound.
[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))
128
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)
187 (skip-chars-forward " \t\n")
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
GM
200 (xml-skip-dtd end))
201 (skip-chars-forward " \t\n")
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
af9bd539 213 ((looking-at "<\\([^/> \t\n]+\\)")
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 ?
222 (if (looking-at "/>")
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
653558a1 233 (while (not (looking-at (concat "</" node-name "[ \t\n]*>")))
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))
250
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
GM
283 name)
284 (skip-chars-forward " \t\n")
653558a1 285 (while (looking-at "\\([a-zA-Z_:][-a-zA-Z0-9._:]*\\)[ \t\n]*=[ \t\n]*")
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"))
47db06aa 298
971489ea 299 (push (cons name (match-string-no-properties 1)) attlist)
47db06aa
GM
300 (goto-char (match-end 0))
301 (skip-chars-forward " \t\n")
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"))
321 (if (looking-at "[ \t\n]*>")
322 (error "XML: invalid DTD (excepting name of the document)"))
323 (condition-case nil
324 (progn
325 (forward-word 1) ;; name of the document
326 (skip-chars-forward " \t\n")
327 (if (looking-at "\\[")
328 (re-search-forward "\\][ \t\n]*>" end)
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
SM
335 (forward-char (length "<!DOCTYPE"))
336 (skip-chars-forward " \t\n")
337 (if (looking-at ">")
338 (error "XML: invalid DTD (excepting name of the document)"))
339
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
346 (skip-chars-forward " \t\n")
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"))
47db06aa
GM
351
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)
357 (while (and (not (looking-at "[ \t\n]*\\]"))
358 (<= (point) end))
359 (cond
360
361 ;; Translation of rule [45] of XML specifications
362 ((looking-at
363 "[\t \n]*<!ELEMENT[ \t\n]+\\([a-zA-Z0-9.%;]+\\)[ \t\n]+\\([^>]+\\)>")
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))
47db06aa
GM
368
369 ;; Translation of rule [46] of XML specifications
370 (cond
371 ((string-match "^EMPTY[ \t\n]*$" type) ;; empty declaration
971489ea 372 (setq type 'empty))
47db06aa 373 ((string-match "^ANY[ \t\n]*$" type) ;; any type of contents
971489ea 374 (setq type 'any))
47db06aa 375 ((string-match "^(\\(.*\\))[ \t\n]*$" type) ;; children ([47])
971489ea 376 (setq type (xml-parse-elem-type (match-string-no-properties 1 type))))
47db06aa
GM
377 ((string-match "^%[^;]+;[ \t\n]*$" type) ;; substitution
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
GM
385 (symbol-name element)))
386
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 )))
419 (if (string-match "[ \t\n]*\\([^+*?]+\\)\\([+*?]?\\)" string)
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))
47db06aa 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."
445 (while (string-match "&amp;" string)
971489ea 446 (setq string (replace-match "&" t nil string)))
47db06aa 447 (while (string-match "&lt;" string)
971489ea 448 (setq string (replace-match "<" t nil string)))
47db06aa 449 (while (string-match "&gt;" string)
971489ea 450 (setq string (replace-match ">" t nil string)))
47db06aa 451 (while (string-match "&apos;" string)
971489ea 452 (setq string (replace-match "'" t nil string)))
47db06aa 453 (while (string-match "&quot;" string)
971489ea 454 (setq string (replace-match "\"" t nil string)))
47db06aa
GM
455 string)
456
457;;*******************************************************************
458;;**
459;;** Printing a tree.
460;;** This function is intended mainly for debugging purposes.
461;;**
462;;*******************************************************************
463
464(defun xml-debug-print (xml)
971489ea
SM
465 (dolist (node xml)
466 (xml-debug-print-internal node "")))
47db06aa 467
971489ea 468(defun xml-debug-print-internal (xml indent-string)
47db06aa
GM
469 "Outputs the XML tree in the current buffer.
470The first line indented with INDENT-STRING."
471 (let ((tree xml)
472 attlist)
47db06aa
GM
473 (insert indent-string "<" (symbol-name (xml-node-name tree)))
474
475 ;; output the attribute list
971489ea 476 (setq attlist (xml-node-attributes tree))
47db06aa
GM
477 (while attlist
478 (insert " ")
479 (insert (symbol-name (caar attlist)) "=\"" (cdar attlist) "\"")
971489ea 480 (setq attlist (cdr attlist)))
47db06aa
GM
481
482 (insert ">")
483
971489ea 484 (setq tree (xml-node-children tree))
47db06aa
GM
485
486 ;; output the children
971489ea 487 (dolist (node tree)
47db06aa 488 (cond
971489ea 489 ((listp node)
47db06aa 490 (insert "\n")
971489ea
SM
491 (xml-debug-print-internal node (concat indent-string " ")))
492 ((stringp node) (insert node))
47db06aa 493 (t
971489ea 494 (error "Invalid XML tree"))))
47db06aa
GM
495
496 (insert "\n" indent-string
971489ea 497 "</" (symbol-name (xml-node-name xml)) ">")))
47db06aa
GM
498
499(provide 'xml)
500
501;;; xml.el ends here