mirror of
git://git.sv.gnu.org/emacs.git
synced 2025-12-31 09:20:54 -08:00
435 lines
13 KiB
EmacsLisp
435 lines
13 KiB
EmacsLisp
;;; xml-lite.el --- an indentation-engine for XML
|
||
|
||
;; Copyright (C) 2002 Free Software Foundation, Inc.
|
||
|
||
;; Author: Mike Williams <mdub@bigfoot.com>
|
||
;; Created: February 2001
|
||
;; Keywords: xml
|
||
|
||
;; This file is part of GNU Emacs.
|
||
|
||
;; This program 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 of the License, or
|
||
;; (at your option) any later version.
|
||
;;
|
||
;; This program 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:
|
||
;;
|
||
;; This package provides a simple indentation engine for XML. It is
|
||
;; intended for use in situations where the full power of the popular PSGML
|
||
;; package (DTD parsing, syntax checking) is not required.
|
||
;;
|
||
;; xml-lite is designed to be used in conjunction with the default GNU
|
||
;; Emacs sgml-mode, to provide a lightweight XML-editing environment.
|
||
|
||
;;; Thanks:
|
||
;;
|
||
;; Jens Schmidt <Jens.Schmidt@oracle.com>
|
||
;; for his feedback and suggestions
|
||
|
||
;;; Code:
|
||
|
||
(eval-when-compile (require 'cl))
|
||
(require 'sgml-mode)
|
||
|
||
|
||
;; Variables
|
||
|
||
(defgroup xml-lite nil
|
||
"Customizable variables for XML-Lite mode."
|
||
:group 'languages
|
||
)
|
||
|
||
(defcustom xml-lite-basic-offset 2
|
||
"*Specifies the basic indentation level for `xml-lite-indent-line'."
|
||
:type 'integer
|
||
:group 'xml-lite
|
||
)
|
||
|
||
(defcustom xml-lite-electric-slash 'close
|
||
"*If non-nil, inserting a '/' after a '<' behaves electrically.
|
||
If set to `indent', typing '</' just triggers reindentation.
|
||
If set to `close', typing '</' inserts an end-tag for the
|
||
enclosing XML element."
|
||
:type '(choice (const :tag "Indent" indent)
|
||
|
||
(const :tag "Close" close)
|
||
(const :tag "No" nil))
|
||
|
||
:group 'xml-lite
|
||
)
|
||
|
||
(defcustom xml-lite-mode-line-string " XML"
|
||
"*String to display in the modeline when `xml-lite-mode' is active.
|
||
Set this to nil if you don't want a modeline indicator for xml-lite-mode."
|
||
:type 'string
|
||
:group 'xml-lite)
|
||
|
||
(defcustom xml-lite-mode-hook nil
|
||
"*Hook called by `xml-lite-mode'."
|
||
:type 'hook
|
||
:group 'xml-lite)
|
||
|
||
;;;###autoload
|
||
(defvar xml-lite-mode nil
|
||
"Non-nil if `xml-lite-mode' is enabled.")
|
||
(make-variable-buffer-local 'xml-lite-mode)
|
||
|
||
|
||
;; Syntax analysis
|
||
|
||
(defsubst xml-lite-at-indentation-p ()
|
||
"Return true if point is at the first non-whitespace character on the line."
|
||
(save-excursion
|
||
(skip-chars-backward " \t")
|
||
(bolp)))
|
||
|
||
(defun xml-lite-in-string-p (&optional limit)
|
||
"Determine whether point is inside a string.
|
||
|
||
Parse begins from LIMIT, which defaults to the preceding occurence of a tag
|
||
at the beginning of a line."
|
||
(let (syntax-info)
|
||
(or limit
|
||
(setq limit (or (save-excursion
|
||
(re-search-backward "^[ \t]*<" nil t))
|
||
(point-min))))
|
||
(setq syntax-info (parse-partial-sexp limit (point)))
|
||
(if (nth 3 syntax-info) (nth 8 syntax-info))))
|
||
|
||
|
||
;; Parsing
|
||
(defstruct (xml-lite-tag
|
||
(:constructor xml-lite-make-tag (type start end name name-end)))
|
||
type start end name name-end)
|
||
(defsubst xml-lite-parse-tag-name ()
|
||
"Skip past a tag-name, and return the name."
|
||
(buffer-substring-no-properties
|
||
(point) (progn (skip-syntax-forward "w_") (point))))
|
||
|
||
(defsubst xml-lite-looking-back-at (s)
|
||
(let ((limit (max (- (point) (length s)) (point-min))))
|
||
(equal s (buffer-substring-no-properties limit (point)))))
|
||
|
||
(defsubst xml-lite-looking-at (s)
|
||
(let ((limit (min (+ (point) (length s)))))
|
||
(equal s (buffer-substring-no-properties (point) limit))))
|
||
|
||
(defun xml-lite-parse-tag-backward ()
|
||
"Get information about the parent tag."
|
||
(let ((limit (point))
|
||
tag-type tag-start tag-end name name-end)
|
||
|
||
(cond
|
||
|
||
((null (re-search-backward "[<>]" nil t)))
|
||
|
||
((= ?> (char-after)) ;--- found tag-end ---
|
||
(setq tag-end (1+ (point)))
|
||
(goto-char tag-end)
|
||
(cond
|
||
((xml-lite-looking-back-at "--") ; comment
|
||
(setq tag-type 'comment
|
||
tag-start (search-backward "<!--" nil t)))
|
||
((xml-lite-looking-back-at "]]>") ; cdata
|
||
(setq tag-type 'cdata
|
||
tag-start (search-backward "![CDATA[" nil t)))
|
||
(t
|
||
(setq tag-start
|
||
(ignore-errors (backward-sexp) (point))))))
|
||
|
||
((= ?< (char-after)) ;--- found tag-start ---
|
||
(setq tag-start (point))
|
||
(goto-char (1+ tag-start))
|
||
(cond
|
||
((xml-lite-looking-at "!--") ; comment
|
||
(setq tag-type 'comment
|
||
tag-end (search-forward "-->" nil t)))
|
||
((xml-lite-looking-at "![CDATA[") ; cdata
|
||
(setq tag-type 'cdata
|
||
tag-end (search-forward "]]>" nil t)))
|
||
(t
|
||
(goto-char tag-start)
|
||
(setq tag-end
|
||
(ignore-errors (forward-sexp) (point))))))
|
||
|
||
)
|
||
|
||
(cond
|
||
|
||
((or tag-type (null tag-start)))
|
||
|
||
((= ?! (char-after (1+ tag-start))) ; declaration
|
||
(setq tag-type 'decl))
|
||
|
||
((= ?? (char-after (1+ tag-start))) ; processing-instruction
|
||
(setq tag-type 'pi))
|
||
|
||
((= ?/ (char-after (1+ tag-start))) ; close-tag
|
||
(goto-char (+ 2 tag-start))
|
||
(setq tag-type 'close
|
||
name (xml-lite-parse-tag-name)
|
||
name-end (point)))
|
||
|
||
((member ; JSP tags etc
|
||
(char-after (1+ tag-start))
|
||
'(?% ?#))
|
||
(setq tag-type 'unknown))
|
||
|
||
(t
|
||
(goto-char (1+ tag-start))
|
||
(setq tag-type 'open
|
||
name (xml-lite-parse-tag-name)
|
||
name-end (point))
|
||
;; check whether it's an empty tag
|
||
(if (or (and tag-end (eq ?/ (char-before (- tag-end 1))))
|
||
(and (not sgml-xml-mode)
|
||
(member-ignore-case name sgml-empty-tags)))
|
||
(setq tag-type 'empty))))
|
||
|
||
(cond
|
||
(tag-start
|
||
(goto-char tag-start)
|
||
(xml-lite-make-tag tag-type tag-start tag-end name name-end)))))
|
||
|
||
(defsubst xml-lite-inside-tag-p (tag-info &optional point)
|
||
"Return true if TAG-INFO contains the POINT."
|
||
(let ((end (xml-lite-tag-end tag-info))
|
||
(point (or point (point))))
|
||
(or (null end)
|
||
(> end point))))
|
||
|
||
(defun xml-lite-get-context (&optional full)
|
||
"Determine the context of the current position.
|
||
If FULL is non-nil, parse back to the beginning of the buffer, otherwise
|
||
parse until we find a start-tag as the first thing on a line.
|
||
|
||
The context is a list of tag-info structures. The last one is the tag
|
||
immediately enclosing the current position."
|
||
(let ((here (point))
|
||
(ignore-depth 0)
|
||
tag-info context)
|
||
;; CONTEXT keeps track of the tag-stack
|
||
;; IGNORE-DEPTH keeps track of the nesting level of point relative to the
|
||
;; first (outermost) tag on the context. This is the number of
|
||
;; enclosing start-tags we'll have to ignore.
|
||
(save-excursion
|
||
|
||
(while
|
||
(and (or (not context)
|
||
full
|
||
(not (xml-lite-at-indentation-p)))
|
||
(setq tag-info (xml-lite-parse-tag-backward)))
|
||
|
||
;; This tag may enclose things we thought were tags. If so,
|
||
;; discard them.
|
||
(while (and context
|
||
(> (xml-lite-tag-end tag-info)
|
||
(xml-lite-tag-end (car context))))
|
||
(setq context (cdr context)))
|
||
|
||
(cond
|
||
|
||
;; inside a tag ...
|
||
((xml-lite-inside-tag-p tag-info here)
|
||
(push tag-info context))
|
||
|
||
;; start-tag
|
||
((eq (xml-lite-tag-type tag-info) 'open)
|
||
(setq ignore-depth (1- ignore-depth))
|
||
(when (= ignore-depth -1)
|
||
(push tag-info context)
|
||
(setq ignore-depth 0)))
|
||
|
||
;; end-tag
|
||
((eq (xml-lite-tag-type tag-info) 'close)
|
||
(setq ignore-depth (1+ ignore-depth)))
|
||
|
||
)))
|
||
|
||
;; return context
|
||
context
|
||
))
|
||
|
||
(defun xml-lite-show-context (&optional full)
|
||
"Display the current context.
|
||
If FULL is non-nil, parse back to the beginning of the buffer."
|
||
(interactive "P")
|
||
(with-output-to-temp-buffer "*XML Context*"
|
||
(pp (xml-lite-get-context full))))
|
||
|
||
|
||
;; Indenting
|
||
|
||
(defun xml-lite-calculate-indent ()
|
||
"Calculate the column to which this line should be indented."
|
||
(let* ((here (point))
|
||
(context (xml-lite-get-context))
|
||
(ref-tag-info (car context))
|
||
(last-tag-info (car (last context))))
|
||
|
||
(save-excursion
|
||
(cond
|
||
|
||
;; no context
|
||
((null context) 0)
|
||
|
||
;; inside a comment
|
||
((eq 'comment (xml-lite-tag-type last-tag-info))
|
||
(let ((mark (looking-at "--")))
|
||
(goto-char (xml-lite-tag-start last-tag-info))
|
||
(forward-char 2)
|
||
(if mark (current-column)
|
||
(forward-char 2)
|
||
(+ (if (zerop (skip-chars-forward " \t")) 1 0)
|
||
(current-column)))))
|
||
|
||
;; inside a tag
|
||
((xml-lite-inside-tag-p last-tag-info here)
|
||
|
||
(let ((start-of-enclosing-string
|
||
(xml-lite-in-string-p (xml-lite-tag-start last-tag-info))))
|
||
(cond
|
||
;; inside an attribute value
|
||
(start-of-enclosing-string
|
||
(goto-char start-of-enclosing-string)
|
||
(1+ (current-column)))
|
||
;; if we have a tag-name, base indent on that
|
||
((and (xml-lite-tag-name-end last-tag-info)
|
||
(progn
|
||
(goto-char (xml-lite-tag-name-end last-tag-info))
|
||
(not (looking-at "[ \t]*$"))))
|
||
(1+ (current-column)))
|
||
;; otherwise, add indent-offset
|
||
(t
|
||
(goto-char (xml-lite-tag-start last-tag-info))
|
||
(+ (current-column) xml-lite-basic-offset)))))
|
||
|
||
;; inside an element
|
||
(t
|
||
;; indent to start of tag
|
||
(let ((indent-offset xml-lite-basic-offset))
|
||
;; add xml-lite-basic-offset, unless we're looking at the
|
||
;; matching end-tag
|
||
(if (and (eq (length context) 1)
|
||
(xml-lite-looking-at "</"))
|
||
(setq indent-offset 0))
|
||
(goto-char (xml-lite-tag-start ref-tag-info))
|
||
(+ (current-column) indent-offset)))
|
||
|
||
))))
|
||
|
||
(defun xml-lite-indent-line ()
|
||
"Indent the current line as XML."
|
||
(interactive)
|
||
(let* ((savep (point))
|
||
(indent-col
|
||
(save-excursion
|
||
(beginning-of-line)
|
||
(skip-chars-forward " \t")
|
||
(if (>= (point) savep) (setq savep nil))
|
||
;; calculate basic indent
|
||
(xml-lite-calculate-indent))))
|
||
(if savep
|
||
(save-excursion (indent-line-to indent-col))
|
||
(indent-line-to indent-col))))
|
||
|
||
|
||
;; Editing shortcuts
|
||
|
||
(defun xml-lite-insert-end-tag ()
|
||
"Insert an end-tag for the current element."
|
||
(interactive)
|
||
(let* ((context (xml-lite-get-context))
|
||
(tag-info (car (last context)))
|
||
(type (and tag-info (xml-lite-tag-type tag-info))))
|
||
|
||
(cond
|
||
|
||
((null context)
|
||
(error "Nothing to close"))
|
||
|
||
;; inside a tag
|
||
((xml-lite-inside-tag-p tag-info)
|
||
(insert (cond
|
||
((eq type 'open) " />")
|
||
((eq type 'comment) " -->")
|
||
((eq type 'cdata) "]]>")
|
||
((eq type 'jsp) "%>")
|
||
((eq type 'pi) "?>")
|
||
(t ">"))))
|
||
|
||
;; inside an element
|
||
((eq type 'open)
|
||
(insert "</" (xml-lite-tag-name tag-info) ">")
|
||
(indent-according-to-mode))
|
||
|
||
(t
|
||
(error "Nothing to close")))))
|
||
|
||
(defun xml-lite-slash (arg)
|
||
"Insert ARG slash characters.
|
||
Behaves electrically if `xml-lite-electric-slash' is non-nil."
|
||
(interactive "p")
|
||
(cond
|
||
((not (and (eq (char-before) ?<) (= arg 1)))
|
||
(insert-char ?/ arg))
|
||
((eq xml-lite-electric-slash 'indent)
|
||
(insert-char ?/ 1)
|
||
(indent-according-to-mode))
|
||
((eq xml-lite-electric-slash 'close)
|
||
(delete-backward-char 1)
|
||
(xml-lite-insert-end-tag))
|
||
(t
|
||
(insert-char ?/ arg))))
|
||
|
||
|
||
;; Keymap
|
||
|
||
(defvar xml-lite-mode-map
|
||
(let ((map (make-sparse-keymap)))
|
||
(define-key map "\C-c/" 'xml-lite-insert-end-tag)
|
||
(define-key map "\C-c\C-s" 'xml-lite-show-context)
|
||
(define-key map "/" 'xml-lite-slash)
|
||
map)
|
||
"Key bindings for `xml-lite-mode'.")
|
||
|
||
|
||
;; Minor mode
|
||
|
||
;;;###autoload
|
||
(define-minor-mode xml-lite-mode
|
||
"Toggle `xml-lite-mode'.
|
||
With ARG, enable xml-lite-mode if and only if ARG is positive.
|
||
|
||
xml-lite-mode provides indentation for XML tags. The value of
|
||
`xml-lite-basic-offset' determines the amount of indentation.
|
||
|
||
Key bindings:
|
||
\\{xml-lite-mode-map}"
|
||
nil ; initial value
|
||
" XML" ; mode indicator
|
||
'xml-lite-mode-map ; keymap
|
||
(if xml-lite-mode
|
||
(progn
|
||
(if (eq major-mode 'fundamental-mode) (sgml-mode))
|
||
(set (make-local-variable 'sgml-xml-mode) t)
|
||
(set (make-local-variable 'xml-lite-orig-indent-line-function)
|
||
indent-line-function)
|
||
(set (make-local-variable 'indent-line-function) 'xml-lite-indent-line))
|
||
(kill-local-variable 'sgml-xml-mode)
|
||
(setq indent-line-function xml-lite-orig-indent-line-function)))
|
||
|
||
(provide 'xml-lite)
|
||
|
||
;;; xml-lite.el ends here
|