X-Git-Url: https://code.delx.au/gnu-emacs/blobdiff_plain/5f5c9e79243614837b422a197ad360ef1110b969..d3857d85ab7e18b5dafdb6395e811bb566f8184b:/lisp/textmodes/sgml-mode.el diff --git a/lisp/textmodes/sgml-mode.el b/lisp/textmodes/sgml-mode.el index c1d7d33090..584056bf30 100644 --- a/lisp/textmodes/sgml-mode.el +++ b/lisp/textmodes/sgml-mode.el @@ -1,11 +1,11 @@ ;;; sgml-mode.el --- SGML- and HTML-editing modes -;; Copyright (C) 1992, 1995, 1996, 1998 Free Software Foundation, Inc. +;; Copyright (C) 1992,95,96,98,2001,2002, 2003 Free Software Foundation, Inc. ;; Author: James Clark +;; Maintainer: FSF ;; Adapted-By: ESR, Daniel Pfeiffer , ;; F.Potorti@cnuce.cnr.it -;; Maintainer: ??? ;; Keywords: wp, hypermedia, comm, languages ;; This file is part of GNU Emacs. @@ -35,12 +35,18 @@ (eval-when-compile (require 'skeleton) - (require 'outline)) + (require 'outline) + (require 'cl)) (defgroup sgml nil "SGML editing mode" :group 'languages) +(defcustom sgml-basic-offset 2 + "*Specifies the basic indentation level for `sgml-indent-line'." + :type 'integer + :group 'sgml) + (defcustom sgml-transformation 'identity "*Default value for `skeleton-transformation' (which see) in SGML mode." :type 'function @@ -60,7 +66,7 @@ ;; kludgy kind of tradeoff. (defvar sgml-specials '(?\") "List of characters that have a special meaning for SGML mode. -This list is used when first loading the sgml-mode library. +This list is used when first loading the `sgml-mode' library. The supported characters and potential disadvantages are: ?\\\" Makes \" in text start a string. @@ -75,12 +81,11 @@ Including ?- has the problem of affecting dashes that have nothing to do with comments, so we normally turn it off.") (defvar sgml-quick-keys nil - "Use <, >, &, SPC and `sgml-specials' keys \"electrically\" when non-nil. -This takes effect when first loading the sgml-mode library.") - + "Use <, >, &, /, SPC and `sgml-specials' keys \"electrically\" when non-nil. +This takes effect when first loading the `sgml-mode' library.") (defvar sgml-mode-map - (let ((map (list 'keymap (make-vector 256 nil))) + (let ((map (make-keymap)) ;`sparse' doesn't allow binding to charsets. (menu-map (make-sparse-keymap "SGML"))) (define-key map "\C-c\C-i" 'sgml-tags-invisible) (define-key map "/" 'sgml-slash) @@ -94,18 +99,18 @@ This takes effect when first loading the sgml-mode library.") (define-key map "\C-c\C-d" 'sgml-delete-tag) (define-key map "\C-c\^?" 'sgml-delete-tag) (define-key map "\C-c?" 'sgml-tag-help) + (define-key map "\C-c/" 'sgml-close-tag) (define-key map "\C-c8" 'sgml-name-8bit-mode) (define-key map "\C-c\C-v" 'sgml-validate) - (if sgml-quick-keys - (progn - (define-key map "&" 'sgml-name-char) - (define-key map "<" 'sgml-tag) - (define-key map " " 'sgml-auto-attributes) - (define-key map ">" 'sgml-maybe-end-tag) - (if (memq ?\" sgml-specials) - (define-key map "\"" 'sgml-name-self)) - (if (memq ?' sgml-specials) - (define-key map "'" 'sgml-name-self)))) + (when sgml-quick-keys + (define-key map "&" 'sgml-name-char) + (define-key map "<" 'sgml-tag) + (define-key map " " 'sgml-auto-attributes) + (define-key map ">" 'sgml-maybe-end-tag) + (when (memq ?\" sgml-specials) + (define-key map "\"" 'sgml-name-self)) + (when (memq ?' sgml-specials) + (define-key map "'" 'sgml-name-self))) (let ((c 127) (map (nth 1 map))) (while (< (setq c (1+ c)) 256) @@ -130,23 +135,33 @@ This takes effect when first loading the sgml-mode library.") map) "Keymap for SGML mode. See also `sgml-specials'.") - -(defvar sgml-mode-syntax-table - (let ((table (copy-syntax-table text-mode-syntax-table))) +(defun sgml-make-syntax-table (specials) + (let ((table (make-syntax-table text-mode-syntax-table))) (modify-syntax-entry ?< "(>" table) (modify-syntax-entry ?> ")<" table) - (if (memq ?- sgml-specials) + (modify-syntax-entry ?: "_" table) + (modify-syntax-entry ?_ "_" table) + (modify-syntax-entry ?. "_" table) + (if (memq ?- specials) (modify-syntax-entry ?- "_ 1234" table)) - (if (memq ?\" sgml-specials) + (if (memq ?\" specials) (modify-syntax-entry ?\" "\"\"" table)) - (if (memq ?' sgml-specials) + (if (memq ?' specials) (modify-syntax-entry ?\' "\"'" table)) - table) + table)) + +(defvar sgml-mode-syntax-table (sgml-make-syntax-table sgml-specials) "Syntax table used in SGML mode. See also `sgml-specials'.") +(defconst sgml-tag-syntax-table + (let ((table (sgml-make-syntax-table '(?- ?\" ?\')))) + (dolist (char '(?\( ?\) ?\{ ?\} ?\[ ?\] ?$ ?% ?& ?* ?+ ?/)) + (modify-syntax-entry char "." table)) + table) + "Syntax table used to parse SGML tags.") (defcustom sgml-name-8bit-mode nil - "*When non-nil, insert 8 bit characters with their names." + "*When non-nil, insert non-ASCII characters as named entities." :type 'boolean :group 'sgml) @@ -185,6 +200,19 @@ This takes effect when first loading the sgml-mode library.") "oslash" "ugrave" "uacute" "ucirc" "uuml" "yacute" "thorn" "yuml"] "Vector of symbolic character names without `&' and `;'.") +(put 'sgml-table 'char-table-extra-slots 0) + +(defvar sgml-char-names-table + (let ((table (make-char-table 'sgml-table)) + (i 32) + elt) + (while (< i 128) + (setq elt (aref sgml-char-names i)) + (if elt (aset table (make-char 'latin-iso8859-1 i) elt)) + (setq i (1+ i))) + table) + "A table for mapping non-ASCII characters into SGML entity names. +Currently, only Latin-1 characters are supported.") ;; nsgmls is a free SGML parser in the SP suite available from ;; ftp.jclark.com and otherwise packaged for GNU systems. @@ -202,34 +230,64 @@ separated by a space." (defvar sgml-saved-validate-command nil "The command last used to validate in this buffer.") - -;;; I doubt that null end tags are used much for large elements, -;;; so use a small distance here. +;; I doubt that null end tags are used much for large elements, +;; so use a small distance here. (defcustom sgml-slash-distance 1000 "*If non-nil, is the maximum distance to search for matching `/'." :type '(choice (const nil) integer) :group 'sgml) -(defconst sgml-start-tag-regex - "<[A-Za-z]\\([-.A-Za-z0-9= \n\t]\\|\"[^\"]*\"\\|'[^']*'\\)*" +(defconst sgml-namespace-re "[_[:alpha:]][-_.[:alnum:]]*") +(defconst sgml-name-re "[_:[:alpha:]][-_.:[:alnum:]]*") +(defconst sgml-tag-name-re (concat "<\\([!/?]?" sgml-name-re "\\)")) +(defconst sgml-attrs-re "\\(?:[^\"'/><]\\|\"[^\"]*\"\\|'[^']*'\\)*") +(defconst sgml-start-tag-regex (concat "<" sgml-name-re sgml-attrs-re) "Regular expression that matches a non-empty start tag. Any terminating `>' or `/' is not matched.") +(defface sgml-namespace-face + '((t (:inherit font-lock-builtin-face))) + "`sgml-mode' face used to highlight the namespace part of identifiers.") +(defvar sgml-namespace-face 'sgml-namespace-face) ;; internal (defconst sgml-font-lock-keywords-1 - '(("<\\([!?][a-z][-.a-z0-9]*\\)" 1 font-lock-keyword-face) - ("<\\(/?[a-z][-.a-z0-9]*\\)" 1 font-lock-function-name-face) - ("[&%][a-z][-.a-z0-9]*;?" . font-lock-variable-name-face) - ("" . font-lock-comment-face))) - -(defconst sgml-font-lock-keywords-2 ()) + `((,(concat "<\\([!?]" sgml-name-re "\\)") 1 font-lock-keyword-face) + ;; We could use the simpler "\\(" sgml-namespace-re ":\\)?" instead, + ;; but it would cause a bit more backtracking in the re-matcher. + (,(concat "]*\\)?>\\([^<]+\\)") + '(3 (cdr (assoc (downcase (match-string 1)) + sgml-tag-face-alist)) prepend)))))) ;; for font-lock, but must be defvar'ed after ;; sgml-font-lock-keywords-1 and sgml-font-lock-keywords-2 above (defvar sgml-font-lock-keywords sgml-font-lock-keywords-1 "*Rules for highlighting SGML code. See also `sgml-tag-face-alist'.") +(defvar sgml-font-lock-syntactic-keywords + ;; Use the `b' style of comments to avoid interference with the -- ... -- + ;; comments recognized when `sgml-specials' includes ?-. + ;; FIXME: beware of blabla !! + '(("\\(<\\)!--" (1 "< b")) + ("--[ \t\n]*\\(>\\)" (1 "> b"))) + "Syntactic keywords for `sgml-mode'.") + ;; internal (defvar sgml-face-tag-alist () "Alist of face and tag name for facemenu.") @@ -239,14 +297,12 @@ Any terminating `>' or `/' is not matched.") When `font-lock-maximum-decoration' is 1 this is always used for fontifying. When more these are fontified together with `sgml-font-lock-keywords'.") - (defvar sgml-display-text () "Tag names as lowercase symbols, and display string when invisible.") ;; internal (defvar sgml-tags-invisible nil) - (defcustom sgml-tag-alist '(("![" ("ignore" t) ("include" t)) ("!attlist") @@ -259,8 +315,8 @@ This alist is made up as ((\"tag\" . TAGRULE) ...) -TAGRULE is a list of optionally `t' (no endtag) or `\\n' (separate endtag by -newlines) or a skeleton with `nil', `t' or `\\n' in place of the interactor +TAGRULE is a list of optionally t (no endtag) or `\\n' (separate endtag by +newlines) or a skeleton with nil, t or `\\n' in place of the interactor followed by an ATTRIBUTERULE (for an always present attribute) or an attribute alist. @@ -269,7 +325,7 @@ The attribute alist is made up as ((\"attribute\" . ATTRIBUTERULE) ...) -ATTRIBUTERULE is a list of optionally `t' (no value when no input) followed by +ATTRIBUTERULE is a list of optionally t (no value when no input) followed by an optional alist of possible values." :type '(repeat (cons (string :tag "Tag Name") (repeat :tag "Tag Rule" sexp))) @@ -287,77 +343,45 @@ an optional alist of possible values." (string :tag "Description"))) :group 'sgml) -(defvar v2) ; free for skeleton +(defcustom sgml-xml-mode nil + "*When non-nil, tag insertion functions will be XML-compliant. +If this variable is customized, the custom value is used always. +Otherwise, it is set to be buffer-local when the file has + a DOCTYPE or an XML declaration." + :type 'boolean + :version "21.4" + :group 'sgml) -(defun sgml-mode-common (sgml-tag-face-alist sgml-display-text) - "Common code for setting up `sgml-mode' and derived modes. -SGML-TAG-FACE-ALIST is used for calculating `sgml-font-lock-keywords-2'. -SGML-DISPLAY-TEXT sets up alternate text for when tags are invisible (see -varables of same name)." - (setq local-abbrev-table text-mode-abbrev-table) - (set-syntax-table sgml-mode-syntax-table) - (make-local-variable 'indent-line-function) - (make-local-variable 'paragraph-start) - (make-local-variable 'paragraph-separate) - (make-local-variable 'adaptive-fill-regexp) - (make-local-variable 'sgml-saved-validate-command) - (make-local-variable 'comment-start) - (make-local-variable 'comment-end) - (make-local-variable 'comment-indent-function) - (make-local-variable 'comment-indent-function) - (make-local-variable 'sgml-tags-invisible) - (make-local-variable 'skeleton-transformation) - (make-local-variable 'skeleton-further-elements) - (make-local-variable 'skeleton-end-hook) - (make-local-variable 'font-lock-defaults) - (make-local-variable 'sgml-font-lock-keywords-1) - (make-local-variable 'sgml-font-lock-keywords-2) - (make-local-variable 'facemenu-add-face-function) - (make-local-variable 'facemenu-end-add-face) - ;;(make-local-variable 'facemenu-remove-face-function) - (and sgml-tag-face-alist - (not (assq 1 sgml-tag-face-alist)) - (nconc sgml-tag-face-alist - `((1 (,(concat "<\\(" - (mapconcat 'car sgml-tag-face-alist "\\|") - "\\)\\([ \t].+\\)?>\\(.+\\)") - 3 (cdr (assoc (downcase (match-string 1)) - ',sgml-tag-face-alist))))))) - (setq indent-line-function 'indent-relative-maybe - ;; A start or end tag by itself on a line separates a paragraph. - ;; This is desirable because SGML discards a newline that appears - ;; immediately after a start tag or immediately before an end tag. - paragraph-separate "[ \t]*$\\|\ -\[ \t]*$" - paragraph-start "[ \t]*$\\|\ -\[ \t]*" - adaptive-fill-regexp "[ \t]*" - comment-start "" - comment-indent-function 'sgml-comment-indent - skeleton-transformation sgml-transformation - skeleton-further-elements '((completion-ignore-case t)) - skeleton-end-hook (lambda () - (or (eolp) - (not (or (eq v2 '\n) - (eq (car-safe v2) '\n))) - (newline-and-indent))) - sgml-font-lock-keywords-2 (append - sgml-font-lock-keywords-1 - (cdr (assq 1 sgml-tag-face-alist))) - font-lock-defaults '((sgml-font-lock-keywords - sgml-font-lock-keywords-1 - sgml-font-lock-keywords-2) - nil - t) - facemenu-add-face-function 'sgml-mode-facemenu-add-face-function) - ;; This will allow existing comments within declarations to be - ;; recognized. - (set (make-local-variable 'comment-start-skip) "\\(?:\\)?") - (dolist (pair sgml-display-text) - (put (car pair) 'before-string (cdr pair)))) +(defvar sgml-empty-tags nil + "List of tags whose !ELEMENT definition says EMPTY.") + +(defvar sgml-unclosed-tags nil + "List of tags whose !ELEMENT definition says the end-tag is optional.") + +(defun sgml-xml-guess () + "Guess whether the current buffer is XML." + (save-excursion + (goto-char (point-min)) + (when (or (string= "xml" (file-name-extension (or buffer-file-name ""))) + (looking-at "\\s-*<\\?xml") + (when (re-search-forward + (eval-when-compile + (mapconcat 'identity + '("")) (concat "<" face ">")) - (error "Face not configured for %s mode." mode-name))) - + (error "Face not configured for %s mode" mode-name))) ;;;###autoload -(defun sgml-mode () +(define-derived-mode sgml-mode text-mode "SGML" "Major mode for editing SGML documents. -Makes > match <. Makes / blink matching /. -Keys <, &, SPC within <>, \" and ' can be electric depending on +Makes > match <. +Keys <, &, SPC within <>, \", / and ' can be electric depending on `sgml-quick-keys'. An argument of N to a tag-inserting command means to wrap it around @@ -387,26 +410,77 @@ Use \\[sgml-validate] to validate your document with an SGML parser. Do \\[describe-variable] sgml- SPC to see available variables. Do \\[describe-key] on the following bindings to discover what they do. \\{sgml-mode-map}" - (interactive) - (kill-all-local-variables) - (setq mode-name "SGML" - major-mode 'sgml-mode) - (sgml-mode-common sgml-tag-face-alist sgml-display-text) - ;; Set imenu-generic-expression here, rather than in sgml-mode-common, - ;; because this definition probably is not useful in HTML mode. - (make-local-variable 'imenu-generic-expression) - (setq imenu-generic-expression - "")) + (set (make-local-variable 'paragraph-separate) + (concat paragraph-start "$")) + (set (make-local-variable 'adaptive-fill-regexp) "[ \t]*") + (set (make-local-variable 'indent-line-function) 'sgml-indent-line) + (set (make-local-variable 'comment-start) "") + (set (make-local-variable 'comment-indent-function) 'sgml-comment-indent) + (set (make-local-variable 'comment-line-break-function) + 'sgml-comment-indent-new-line) + (set (make-local-variable 'skeleton-further-elements) + '((completion-ignore-case t))) + (set (make-local-variable 'skeleton-end-hook) + (lambda () + (or (eolp) + (not (or (eq v2 '\n) (eq (car-safe v2) '\n))) + (newline-and-indent)))) + (set (make-local-variable 'font-lock-defaults) + '((sgml-font-lock-keywords + sgml-font-lock-keywords-1 + sgml-font-lock-keywords-2) + nil t nil nil + (font-lock-syntactic-keywords + . sgml-font-lock-syntactic-keywords))) + (set (make-local-variable 'facemenu-add-face-function) + 'sgml-mode-facemenu-add-face-function) + (sgml-xml-guess) + (if sgml-xml-mode + (setq mode-name "XML") + (set (make-local-variable 'skeleton-transformation) sgml-transformation)) + ;; This will allow existing comments within declarations to be + ;; recognized. + (set (make-local-variable 'comment-start-skip) "\\(?:\\)?") + ;; This definition probably is not useful in derived modes. + (set (make-local-variable 'imenu-generic-expression) + (concat " (length sgml-tag-last) 0) + (format "Tag (default %s): " sgml-tag-last) + "Tag: ") + sgml-tag-alist nil nil nil 'sgml-tag-history sgml-tag-last))) ?< str | (("") -1 '(undo-boundary) (identity "<")) | ; see comment above `(("") '(setq v2 (sgml-attributes ,str t)) ?> - (if (string= "![" ,str) - (prog1 '(("") " [ " _ " ]]") - (backward-char)) - (if (or (eq v2 t) - (string-match "^[/!?]" ,str)) - () - (if (symbolp v2) - ;; We go use `identity' to prevent skeleton from passing - ;; `str' through skeleton-transformation a second time. - '(("") v2 _ v2 ") - (if (eq (car v2) t) - (cons '("") (cdr v2)) - (append '(("") (car v2)) - (cdr v2) - '(resume: (car v2) _ ")))))))) + (cond + ((string= "![" ,str) + (backward-char) + '(("") " [ " _ " ]]")) + ((and (eq v2 t) sgml-xml-mode (member ,str sgml-empty-tags)) + '(("") -1 "/>")) + ((or (and (eq v2 t) (not sgml-xml-mode)) (string-match "^[/!?]" ,str)) + nil) + ((symbolp v2) + ;; Make sure we don't fall into an infinite loop. + ;; For xhtml's `tr' tag, we should maybe use \n instead. + (if (eq v2 t) (setq v2 nil)) + ;; We use `identity' to prevent skeleton from passing + ;; `str' through skeleton-transformation a second time. + '(("") v2 _ v2 ")) + ((eq (car v2) t) + (cons '("") (cdr v2))) + (t + (append '(("") (car v2)) + (cdr v2) + '(resume: (car v2) _ ")))))) (autoload 'skeleton-read "skeleton") @@ -578,7 +678,6 @@ With prefix argument, only self insert." (or (> (point) point) (self-insert-command 1))))) - (defun sgml-tag-help (&optional tag) "Display description of tag TAG. If TAG is omitted, use the tag at point." (interactive) @@ -596,70 +695,72 @@ With prefix argument, only self insert." (cdr (assoc (downcase (substring tag 1)) sgml-tag-help))) "No description available"))) - -(defun sgml-maybe-end-tag () - "Name self unless in position to end a tag." - (interactive) - (or (condition-case nil - (save-excursion (up-list -1)) - (error - (sgml-name-self) - t)) - (condition-case nil - (progn - (save-excursion (up-list 1)) - (sgml-name-self)) - (error (self-insert-command 1))))) - +(defun sgml-maybe-end-tag (&optional arg) + "Name self unless in position to end a tag or a prefix ARG is given." + (interactive "P") + (if (or arg (eq (car (sgml-lexical-context)) 'tag)) + (self-insert-command (prefix-numeric-value arg)) + (sgml-name-self))) (defun sgml-skip-tag-backward (arg) "Skip to beginning of tag or matching opening tag if present. With prefix argument ARG, repeat this ARG times." (interactive "p") + ;; FIXME: use sgml-get-context or something similar. (while (>= arg 1) (search-backward "<" nil t) (if (looking-at "]+\\)") ;; end tag, skip any nested pairs (let ((case-fold-search t) - (re (concat ". + "\\([^>]*[^/>]\\)?>"))) (while (and (re-search-backward re nil t) (eq (char-after (1+ (point))) ?/)) (forward-char 1) (sgml-skip-tag-backward 1)))) (setq arg (1- arg)))) -(defun sgml-skip-tag-forward (arg &optional return) +(defun sgml-skip-tag-forward (arg) "Skip to end of tag or matching closing tag if present. With prefix argument ARG, repeat this ARG times. Return t iff after a closing tag." (interactive "p") - (setq return t) - (while (>= arg 1) - (skip-chars-forward "^<>") - (if (eq (following-char) ?>) - (up-list -1)) - (if (looking-at "<\\([^/ \n\t>]+\\)") - ;; start tag, skip any nested same pairs _and_ closing tag - (let ((case-fold-search t) - (re (concat " if the

+ ;; we're skipping has no matching

. + (let ((return t)) + (with-syntax-table sgml-tag-syntax-table + (while (>= arg 1) + (skip-chars-forward "^<>") + (if (eq (following-char) ?>) + (up-list -1)) + (if (looking-at "<\\([^/ \n\t>]+\\)\\([^>]*[^/>]\\)?>") + ;; start tag, skip any nested same pairs _and_ closing tag + (let ((case-fold-search t) + (re (concat ". + "\\([^>]*[^/>]\\)?>")) + point close) + (forward-list 1) + (setq point (point)) + ;; FIXME: This re-search-forward will mistakenly match + ;; tag-like text inside attributes. + (while (and (re-search-forward re nil t) + (not (setq close + (eq (char-after (1+ (match-beginning 0))) ?/))) + (goto-char (match-beginning 0)) + (sgml-skip-tag-forward 1)) + (setq close nil)) + (unless close + (goto-char point) + (setq return nil))) + (forward-list 1)) + (setq arg (1- arg))) + return))) (defun sgml-delete-tag (arg) + ;; FIXME: Should be called sgml-kill-tag or should not touch the kill-ring. "Delete tag on or after cursor, and matching closing or opening tag. With prefix argument ARG, repeat this ARG times." (interactive "p") @@ -693,20 +794,23 @@ With prefix argument ARG, repeat this ARG times." (goto-char close) (kill-sexp 1)) (setq open (point)) - (sgml-skip-tag-forward 1) - (backward-list) - (forward-char) - (if (eq (aref (sgml-beginning-of-tag) 0) ?/) - (kill-sexp 1))) + (when (sgml-skip-tag-forward 1) + (kill-sexp -1))) + ;; Delete any resulting empty line. If we didn't kill-sexp, + ;; this *should* do nothing, because we're right after the tag. + (if (progn (forward-line 0) (looking-at "\\(?:[ \t]*$\\)\n?")) + (delete-region (match-beginning 0) (match-end 0))) (goto-char open) - (kill-sexp 1))) + (kill-sexp 1) + (if (progn (forward-line 0) (looking-at "\\(?:[ \t]*$\\)\n?")) + (delete-region (match-beginning 0) (match-end 0))))) (setq arg (1- arg)))) + ;; Put read-only last to enable setting this even when read-only enabled. (or (get 'sgml-tag 'invisible) (setplist 'sgml-tag (append '(invisible t - intangible t point-entered sgml-point-entered rear-nonsticky t read-only t) @@ -717,34 +821,41 @@ With prefix argument ARG, repeat this ARG times." (interactive "P") (let ((modified (buffer-modified-p)) (inhibit-read-only t) + (inhibit-modification-hooks t) + ;; Avoid spurious the `file-locked' checks. + (buffer-file-name nil) ;; This is needed in case font lock gets called, ;; since it moves point and might call sgml-point-entered. + ;; How could it get called? -stef (inhibit-point-motion-hooks t) - symbol) - (save-excursion - (goto-char (point-min)) - (if (setq sgml-tags-invisible - (if arg - (>= (prefix-numeric-value arg) 0) - (not sgml-tags-invisible))) - (while (re-search-forward "<\\([!/?A-Za-z][-A-Za-z0-9]*\\)" - nil t) - (setq symbol (intern-soft (downcase (match-string 1)))) - (goto-char (match-beginning 0)) - (and (get symbol 'before-string) - (not (overlays-at (point))) - (overlay-put (make-overlay (point) - (match-beginning 1)) - 'category symbol)) - (put-text-property (point) - (progn (forward-list) (point)) - 'category 'sgml-tag)) - (let ((pos (point))) - (while (< (setq pos (next-overlay-change pos)) (point-max)) - (delete-overlay (car (overlays-at pos))))) - (remove-text-properties (point-min) (point-max) - '(category sgml-tag intangible t)))) - (set-buffer-modified-p modified) + string) + (unwind-protect + (save-excursion + (goto-char (point-min)) + (if (set (make-local-variable 'sgml-tags-invisible) + (if arg + (>= (prefix-numeric-value arg) 0) + (not sgml-tags-invisible))) + (while (re-search-forward sgml-tag-name-re nil t) + (setq string + (cdr (assq (intern-soft (downcase (match-string 1))) + sgml-display-text))) + (goto-char (match-beginning 0)) + (and (stringp string) + (not (overlays-at (point))) + (let ((ol (make-overlay (point) (match-beginning 1)))) + (overlay-put ol 'before-string string) + (overlay-put ol 'sgml-tag t))) + (put-text-property (point) + (progn (forward-list) (point)) + 'category 'sgml-tag)) + (let ((pos (point-min))) + (while (< (setq pos (next-overlay-change pos)) (point-max)) + (dolist (ol (overlays-at pos)) + (if (overlay-get ol 'sgml-tag) + (delete-overlay ol))))) + (remove-text-properties (point-min) (point-max) '(category nil)))) + (restore-buffer-modified-p modified)) (run-hooks 'sgml-tags-invisible-hook) (message ""))) @@ -753,7 +864,8 @@ With prefix argument ARG, repeat this ARG times." (let ((inhibit-point-motion-hooks t)) (save-excursion (message "Invisible tag: %s" - (buffer-substring + ;; Strip properties, otherwise, the text is invisible. + (buffer-substring-no-properties (point) (if (or (and (> x y) (not (eq (following-char) ?<))) @@ -761,6 +873,7 @@ With prefix argument ARG, repeat this ARG times." (eq (preceding-char) ?>))) (backward-list) (forward-list))))))) + (autoload 'compile-internal "compile") @@ -782,48 +895,486 @@ and move to the line in the SGML document that caused it." (save-some-buffers (not compilation-ask-about-save) nil) (compile-internal command "No more errors")) +(defsubst sgml-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 sgml-lexical-context (&optional limit) + "Return the lexical context at point as (TYPE . START). +START is the location of the start of the lexical element. +TYPE is one of `string', `comment', `tag', `cdata', or `text'. + +Optional argument LIMIT is the position to start parsing from. +If nil, start from a preceding tag at indentation." + (save-excursion + (let ((pos (point)) + text-start state) + (if limit + (goto-char limit) + ;; Skip tags backwards until we find one at indentation + (while (and (ignore-errors (sgml-parse-tag-backward)) + (not (sgml-at-indentation-p))))) + (with-syntax-table sgml-tag-syntax-table + (while (< (point) pos) + ;; When entering this loop we're inside text. + (setq text-start (point)) + (skip-chars-forward "^<" pos) + (setq state + (cond + ((= (point) pos) + ;; We got to the end without seeing a tag. + nil) + ((looking-at "" pos 'move) + (list 0 nil nil 'cdata nil nil nil nil cdata-start)))) + (t + ;; We've reached a tag. Parse it. + ;; FIXME: Handle net-enabling start-tags + (parse-partial-sexp (point) pos 0)))))) + (cond + ((eq (nth 3 state) 'cdata) (cons 'cdata (nth 8 state))) + ((nth 3 state) (cons 'string (nth 8 state))) + ((nth 4 state) (cons 'comment (nth 8 state))) + ((and state (> (nth 0 state) 0)) (cons 'tag (nth 1 state))) + (t (cons 'text text-start)))))) (defun sgml-beginning-of-tag (&optional top-level) "Skip to beginning of tag and return its name. -If this can't be done, return t." - (or (if top-level - (condition-case nil - (up-list -1) - (error t)) - (>= (point) - (if (search-backward "<" nil t) - (save-excursion - (forward-list) - (point)) - 0))) - (if (looking-at "<[!?/]?[[A-Za-z][A-Za-z0-9]*") - (buffer-substring-no-properties - (1+ (point)) - (match-end 0)) - t))) +If this can't be done, return nil." + (let ((context (sgml-lexical-context))) + (if (eq (car context) 'tag) + (progn + (goto-char (cdr context)) + (when (looking-at sgml-tag-name-re) + (match-string-no-properties 1))) + (if top-level nil + (when (not (eq (car context) 'text)) + (goto-char (cdr context)) + (sgml-beginning-of-tag t)))))) (defun sgml-value (alist) - "Interactively insert value taken from attributerule ALIST. -See `sgml-tag-alist' for info about attributerules.." + "Interactively insert value taken from attribute-rule ALIST. +See `sgml-tag-alist' for info about attribute rules." (setq alist (cdr alist)) (if (stringp (car alist)) (insert "=\"" (car alist) ?\") - (if (eq (car alist) t) - (if (cdr alist) - (progn - (insert "=\"") - (setq alist (skeleton-read '(completing-read - "Value: " (cdr alist)))) - (if (string< "" alist) - (insert alist ?\") - (delete-backward-char 2)))) + (if (and (eq (car alist) t) (not sgml-xml-mode)) + (when (cdr alist) + (insert "=\"") + (setq alist (skeleton-read '(completing-read "Value: " (cdr alist)))) + (if (string< "" alist) + (insert alist ?\") + (delete-backward-char 2))) (insert "=\"") - (if alist - (insert (skeleton-read '(completing-read "Value: " alist)))) + (when alist + (insert (skeleton-read '(completing-read "Value: " alist)))) (insert ?\")))) -(provide 'sgml-mode) +(defun sgml-quote (start end &optional unquotep) + "Quote SGML text in region START ... END. +Only &, < and > are quoted, the rest is left untouched. +With prefix argument UNQUOTEP, unquote the region." + (interactive "r\nP") + (save-restriction + (narrow-to-region start end) + (goto-char (point-min)) + (if unquotep + ;; FIXME: We should unquote other named character references as well. + (while (re-search-forward + "\\(&\\(amp\\|\\(l\\|\\(g\\)\\)t\\)\\)[][<>&;\n\t \"%!'(),/=?]" + nil t) + (replace-match (if (match-end 4) ">" (if (match-end 3) "<" "&")) t t + nil (if (eq (char-before (match-end 0)) ?\;) 0 1))) + (while (re-search-forward "[&<>]" nil t) + (replace-match (cdr (assq (char-before) '((?& . "&") + (?< . "<") + (?> . ">")))) + t t))))) + +(defun sgml-pretty-print (beg end) + "Simple-minded pretty printer for SGML. +Re-indents the code and inserts newlines between BEG and END. +You might want to turn on `auto-fill-mode' to get better results." + ;; TODO: + ;; - insert newline between some start-tag and text. + ;; - don't insert newline in front of some end-tags. + (interactive "r") + (save-excursion + (if (< beg end) + (goto-char beg) + (goto-char end) + (setq end beg) + (setq beg (point))) + ;; Don't use narrowing because it screws up auto-indent. + (setq end (copy-marker end t)) + (with-syntax-table sgml-tag-syntax-table + (while (re-search-forward "<" end t) + (goto-char (match-beginning 0)) + (unless (or ;;(looking-at "= start (point-min)) + (equal str (buffer-substring-no-properties start (point)))))) + +(defun sgml-parse-tag-backward (&optional limit) + "Parse an SGML tag backward, and return information about the tag. +Assume that parsing starts from within a textual context. +Leave point at the beginning of the tag." + (let (tag-type tag-start tag-end name) + (or (re-search-backward "[<>]" limit 'move) + (error "No tag found")) + (when (eq (char-after) ?<) + ;; Oops!! Looks like we were not in a textual context after all!. + ;; Let's try to recover. + (with-syntax-table sgml-tag-syntax-table + (forward-sexp) + (forward-char -1))) + (setq tag-end (1+ (point))) + (cond + ((sgml-looking-back-at "--") ; comment + (setq tag-type 'comment + tag-start (search-backward "")) + (cdata (insert "]]>")) + (pi (insert " ?>")) + (jsp (insert " %>")) + (tag (insert " />")) + (text + (let ((context (save-excursion (sgml-get-context)))) + (if context + (progn + (insert "") + (indent-according-to-mode))))) + (otherwise + (error "Nothing to close")))) + +(defun sgml-empty-tag-p (tag-name) + "Return non-nil if TAG-NAME is an implicitly empty tag." + (and (not sgml-xml-mode) + (member-ignore-case tag-name sgml-empty-tags))) + +(defun sgml-unclosed-tag-p (tag-name) + "Return non-nil if TAG-NAME is a tag for which an end-tag is optional." + (and (not sgml-xml-mode) + (member-ignore-case tag-name sgml-unclosed-tags))) + +(defun sgml-calculate-indent (&optional lcon) + "Calculate the column to which this line should be indented. +LCON is the lexical context, if any." + (unless lcon (setq lcon (sgml-lexical-context))) + + ;; Indent comment-start markers inside