]> code.delx.au - gnu-emacs/blobdiff - lisp/textmodes/sgml-mode.el
(sgml-lexical-context): Use sgml-parse-tag-backward to find start point.
[gnu-emacs] / lisp / textmodes / sgml-mode.el
index ce42a5bcf7df9cb647d687a951e933aa983a4cb2..9df19cc1180b3e20d5d33072e088fb517fa6b451 100644 (file)
@@ -35,7 +35,8 @@
 
 (eval-when-compile
   (require 'skeleton)
-  (require 'outline))
+  (require 'outline)
+  (require 'cl))
 
 (defgroup sgml nil
   "SGML editing mode"
@@ -80,10 +81,9 @@ 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.
+  "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 (make-keymap))    ;`sparse' doesn't allow binding to charsets.
        (menu-map (make-sparse-keymap "SGML")))
@@ -99,6 +99,7 @@ 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)
     (when sgml-quick-keys
@@ -136,7 +137,6 @@ This takes effect when first loading the `sgml-mode' library.")
     map)
   "Keymap for SGML mode.  See also `sgml-specials'.")
 
-
 (defun sgml-make-syntax-table (specials)
   (let ((table (make-syntax-table text-mode-syntax-table)))
     (modify-syntax-entry ?< "(>" table)
@@ -162,7 +162,6 @@ This takes effect when first loading the `sgml-mode' library.")
     table)
   "Syntax table used to parse SGML tags.")
 
-
 (defcustom sgml-name-8bit-mode nil
   "*When non-nil, insert non-ASCII characters as named entities."
   :type 'boolean
@@ -217,7 +216,6 @@ This takes effect when first loading the `sgml-mode' library.")
   "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.
 ;; Its error messages can be parsed by next-error.
@@ -234,7 +232,6 @@ 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.
 (defcustom sgml-slash-distance 1000
@@ -290,14 +287,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")
@@ -310,8 +305,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.
 
@@ -320,7 +315,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)))
@@ -371,6 +366,13 @@ Otherwise, it is set to be buffer-local when the file has
 
 (defvar v2)                            ; free for skeleton
 
+(defun sgml-comment-indent-new-line (&optional soft)
+  (let ((comment-start "-- ")
+       (comment-start-skip "\\(<!\\)?--[ \t]*")
+       (comment-end " --")
+       (comment-style 'plain))
+    (comment-indent-new-line soft)))
+
 (defun sgml-mode-facemenu-add-face-function (face end)
   (if (setq face (cdr (assq face sgml-face-tag-alist)))
       (progn
@@ -379,12 +381,11 @@ Otherwise, it is set to be buffer-local when the file has
        (concat "<" face ">"))
     (error "Face not configured for %s mode" mode-name)))
 
-
 ;;;###autoload
 (define-derived-mode sgml-mode text-mode "SGML"
   "Major mode for editing SGML documents.
 Makes > match <.
-Keys <, &, SPC within <>, \" and ' can be electric depending on
+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
@@ -410,9 +411,12 @@ Do \\[describe-key] on the following bindings to discover what they do.
   (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-end) " -->")
   (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)
@@ -442,13 +446,26 @@ Do \\[describe-key] on the following bindings to discover what they do.
        (concat "<!\\(element\\|entity\\)[ \t\n]+%?[ \t\n]*\\("
               sgml-name-re "\\)")))
 
-
 (defun sgml-comment-indent ()
   (if (looking-at "--") comment-column 0))
 
-
-
 (defun sgml-slash (arg)
+  "Insert ARG slash characters.
+Behaves electrically if `sgml-quick-keys' is non-nil."
+  (interactive "p")
+  (cond
+   ((not (and (eq (char-before) ?<) (= arg 1)))
+    (sgml-slash-matching arg))
+   ((eq sgml-quick-keys 'indent)
+    (insert-char ?/ 1)
+    (indent-according-to-mode))
+   ((eq sgml-quick-keys 'close)
+    (delete-backward-char 1)
+    (sgml-close-tag))
+   (t
+    (sgml-slash-matching arg))))
+
+(defun sgml-slash-matching (arg)
   "Insert `/' and display any previous matching `/'.
 Two `/'s are treated as matching if the first `/' ends a net-enabling
 start tag, and the second `/' is the corresponding null end tag."
@@ -488,7 +505,6 @@ start tag, and the second `/' is the corresponding null end tag."
                        (buffer-substring (line-beginning-position)
                                          (1+ blinkpos)))))))))
 
-
 ;; Why doesn't this use the iso-cvt table or, preferably, generate the
 ;; inverse of the extensive table in the SGML Quail input method?  -- fx
 ;; I guess that's moot since it only works with Latin-1 anyhow.
@@ -638,7 +654,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)
@@ -656,7 +671,6 @@ With prefix argument, only self insert."
                    (cdr (assoc (downcase (substring tag 1)) sgml-tag-help)))
               "No description available")))
 
-
 (defun sgml-maybe-end-tag (&optional arg)
   "Name self unless in position to end a tag or a prefix ARG is given."
   (interactive "P")
@@ -753,6 +767,7 @@ With prefix argument ARG, repeat this ARG times."
        (goto-char open)
        (kill-sexp 1)))
     (setq arg (1- arg))))
+
 \f
 ;; Put read-only last to enable setting this even when read-only enabled.
 (or (get 'sgml-tag 'invisible)
@@ -821,6 +836,7 @@ With prefix argument ARG, repeat this ARG times."
                             (eq (preceding-char) ?>)))
                    (backward-list)
                  (forward-list)))))))
+
 \f
 (autoload 'compile-internal "compile")
 
@@ -842,38 +858,46 @@ 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"))
 
-
 (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', or `text'.
+TYPE is one of `string', `comment', `tag', `cdata', or `text'.
 
-If non-nil LIMIT is a nearby position before point outside of any tag."
-  ;; As usual, it's difficult to get a reliable answer without parsing the
-  ;; whole buffer.  We'll assume that a tag at indentation is outside of
-  ;; any string or tag or comment or ...
+Optional argument LIMIT is the position to start parsing from.
+If nil, start from a preceding tag at indentation."
   (save-excursion
     (let ((pos (point))
-         (state nil)
-         textstart)
-      (if limit (goto-char limit)
-       ;; Hopefully this regexp will match something that's not inside
-       ;; a tag and also hopefully the match is nearby.
-       (re-search-backward "^[ \t]*<[_:[:alpha:]/%!?#]" nil 'move))
-      (setq textstart (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 textstart (point))
+         (setq text-start (point))
          (skip-chars-forward "^<" pos)
-         ;; We skipped text and reached a tag.  Parse it.
-         ;; FIXME: Handle net-enabling start-tags and <![CDATA[ ...]]>.
-         (setq state (parse-partial-sexp (point) pos 0)))
-       (cond
-        ((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 textstart)))))))
+          (setq state
+                (cond
+                 ((= (point) pos)
+                  ;; We got to the end without seeing a tag.
+                  nil)
+                 ((looking-at "<!\\[[A-Z]+\\[")
+                  ;; We've found a CDATA section or similar.
+                  (let ((cdata-start (point)))
+                    (unless (search-forward "]]>" 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.
@@ -922,7 +946,169 @@ With prefix argument, unquote the region."
       (replace-match (cdr (assq (char-before) '((?& . "&amp;")
                                                (?< . "&lt;")
                                                (?> . "&gt;"))))))))
+
+(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)))
+
+\f
+;; Parsing
+
+(defstruct (sgml-tag
+            (:constructor sgml-make-tag (type start end name)))
+  type start end name)
+
+(defsubst sgml-parse-tag-name ()
+  "Skip past a tag-name, and return the name."
+  (buffer-substring-no-properties
+   (point) (progn (skip-syntax-forward "w_") (point))))
+
+(defsubst sgml-looking-back-at (str)
+  "Return t if the test before point matches STR."
+  (let ((start (- (point) (length str))))
+    (and (>= start (point-min))
+         (equal str (buffer-substring-no-properties start (point))))))
+
+(defun sgml-parse-tag-backward ()
+  "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)
+    (search-backward ">")
+    (setq tag-end (1+ (point)))
+    (cond
+     ((sgml-looking-back-at "--")   ; comment
+      (setq tag-type 'comment
+            tag-start (search-backward "<!--" nil t)))
+     ((sgml-looking-back-at "]]")   ; cdata
+      (setq tag-type 'cdata
+            tag-start (re-search-backward "<!\\[[A-Z]+\\[" nil t)))
+     (t
+      (setq tag-start
+            (with-syntax-table sgml-tag-syntax-table
+              (goto-char tag-end)
+              (backward-sexp)
+              (point)))
+      (goto-char (1+ tag-start))
+      (case (char-after)
+        (?!                             ; declaration
+         (setq tag-type 'decl))
+        (??                             ; processing-instruction
+         (setq tag-type 'pi))
+        (?/                             ; close-tag
+         (forward-char 1)
+         (setq tag-type 'close
+               name (sgml-parse-tag-name)))
+        (?%                             ; JSP tags
+         (setq tag-type 'jsp))
+        (t                              ; open or empty tag
+         (setq tag-type 'open
+               name (sgml-parse-tag-name))
+         (if (or (eq ?/ (char-before (- tag-end 1)))
+                 (sgml-empty-tag-p name))
+             (setq tag-type 'empty))))))
+    (goto-char tag-start)
+    (sgml-make-tag tag-type tag-start tag-end name)))
+
+(defun sgml-get-context (&optional full)
+  "Determine the context of the current position.
+If FULL is `empty', return even if the context is empty (i.e.
+we just skipped over some element and got to a beginning of line).
+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 nil)
+       (context nil)
+       tag-info)
+    ;; CONTEXT keeps track of the tag-stack
+    ;; IGNORE keeps track of the nesting level of point relative to the
+    ;;   first (outermost) tag on the context.  This is the list of
+    ;;   enclosing start-tags we'll have to ignore.
+    (skip-chars-backward " \t\n")      ; Make sure we're not at indentation.
+    (while
+       (and (or ignore 
+                 (not (if full (eq full 'empty) context))
+                (not (sgml-at-indentation-p))
+                (and context
+                     (/= (point) (sgml-tag-start (car context)))
+                      (sgml-unclosed-tag-p (sgml-tag-name (car context)))))
+            (setq tag-info (ignore-errors (sgml-parse-tag-backward))))
+      
+      ;; This tag may enclose things we thought were tags.  If so,
+      ;; discard them.
+      (while (and context
+                  (> (sgml-tag-end tag-info)
+                     (sgml-tag-end (car context))))
+        (setq context (cdr context)))
+           
+      (cond
+
+       ;; start-tag
+       ((eq (sgml-tag-type tag-info) 'open)
+       (cond
+        ((null ignore)
+         (if (and context
+                   (sgml-unclosed-tag-p (sgml-tag-name tag-info))
+                  (eq t (compare-strings
+                         (sgml-tag-name tag-info) nil nil
+                         (sgml-tag-name (car context)) nil nil t)))
+             ;; There was an implicit end-tag.
+             nil
+           (push tag-info context)))
+        ((eq t (compare-strings (sgml-tag-name tag-info) nil nil
+                                (car ignore) nil nil t))
+         (setq ignore (cdr ignore)))
+        (t
+         ;; The open and close tags don't match.
+         (if (not sgml-xml-mode)
+             ;; Assume the open tag is simply not closed.
+             (unless (sgml-unclosed-tag-p (sgml-tag-name tag-info))
+               (message "Unclosed tag <%s>" (sgml-tag-name tag-info)))
+           (message "Unmatched tags <%s> and </%s>"
+                    (sgml-tag-name tag-info) (pop ignore))))))
+
+       ;; end-tag
+       ((eq (sgml-tag-type tag-info) 'close)
+       (if (sgml-empty-tag-p (sgml-tag-name tag-info))
+           (message "Spurious </%s>: empty tag" (sgml-tag-name tag-info))
+         (push (sgml-tag-name tag-info) ignore)))
+       ))
+
+    ;; return context
+    context))
+
+(defun sgml-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 (save-excursion (sgml-get-context full)))))
+
 \f
+;; Editing shortcuts
+
+(defun sgml-close-tag ()
+  "Insert an close-tag for the current element."
+  (interactive)
+  (case (car (sgml-lexical-context))
+    (comment   (insert " -->"))
+    (cdata     (insert "]]>"))
+    (pi        (insert " ?>"))
+    (jsp       (insert " %>"))
+    (tag       (insert " />"))
+    (text
+     (let ((context (save-excursion (sgml-get-context))))
+       (if context
+           (progn 
+             (insert "</" (sgml-tag-name (car (last context))) ">")
+             (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."
@@ -972,6 +1158,9 @@ With prefix argument, unquote the region."
           (forward-char 2) (skip-chars-forward " \t"))
         (current-column)))
 
+      (cdata
+       (current-column))
+
       (tag
        (goto-char (1+ (cdr lcon)))
        (skip-chars-forward "^ \t\n")   ;Skip tag name.
@@ -1002,19 +1191,19 @@ With prefix argument, unquote the region."
                        (> (point) (cdr lcon)))
                   nil
                 (goto-char here)
-                (nreverse (xml-lite-get-context (if unclosed nil 'empty)))))
+                (nreverse (sgml-get-context (if unclosed nil 'empty)))))
              (there (point)))
         ;; Ignore previous unclosed start-tag in context.
         (while (and context unclosed
                     (eq t (compare-strings
-                           (xml-lite-tag-name (car context)) nil nil
+                           (sgml-tag-name (car context)) nil nil
                            unclosed nil nil t)))
           (setq context (cdr context)))
         ;; Indent to reflect nesting.
         (if (and context
-                 (goto-char (xml-lite-tag-end (car context)))
+                 (goto-char (sgml-tag-end (car context)))
                  (skip-chars-forward " \t\n")
-                 (< (point) here) (xml-lite-at-indentation-p))
+                 (< (point) here) (sgml-at-indentation-p))
             (current-column)
           (goto-char there)
           (+ (current-column)
@@ -1127,7 +1316,6 @@ This takes effect when first loading the library.")
     map)
   "Keymap for commands for use in HTML mode.")
 
-
 (defvar html-face-tag-alist
   '((bold . "b")
     (italic . "i")
@@ -1158,14 +1346,13 @@ This takes effect when first loading the library.")
     ("var" . italic))
   "Value of `sgml-tag-face-alist' for HTML mode.")
 
-
 (defvar html-display-text
   '((img . "[/]")
     (hr . "----------")
     (li . "o "))
   "Value of `sgml-display-text' for HTML mode.")
-\f
 
+\f
 ;; should code exactly HTML 3 here when that is finished
 (defvar html-tag-alist
   (let* ((1-7 '(("1") ("2") ("3") ("4") ("5") ("6") ("7")))
@@ -1404,6 +1591,7 @@ This takes effect when first loading the library.")
     ("var" . "Math variable face")
     ("wbr" . "Enable <br> within <nobr>"))
 "*Value of `sgml-tag-help' for HTML mode.")
+
 \f
 ;;;###autoload
 (define-derived-mode html-mode sgml-mode "HTML"
@@ -1477,7 +1665,7 @@ To work around that, do:
   ;; (make-local-variable 'imenu-sort-function)
   ;; (setq imenu-sort-function nil) ; sorting the menu defeats the purpose
   )
-\f
+
 (defvar html-imenu-regexp
   "\\s-*<h\\([1-9]\\)[^\n<>]*>\\(<[^\n<>]*>\\)*\\s-*\\([^\n<>]*\\)"
   "*A regular expression matching a head line to be added to the menu.
@@ -1513,6 +1701,7 @@ Can be used as a value for `html-mode-hook'."
     (add-hook 'after-save-hook 'browse-url-of-buffer nil t))
   (message "Autoviewing turned %s."
           (if arg "off" "on")))
+
 \f
 (define-skeleton html-href-anchor
   "HTML anchor tag with href attribute."