1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2025-12-31 09:20:54 -08:00
emacs/lisp/textmodes/xml-lite.el
2002-03-27 22:27:44 +00:00

435 lines
13 KiB
EmacsLisp
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; 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