(eval-when-compile
(require 'skeleton)
- (require 'outline))
+ (require 'outline)
+ (require 'cl))
(defgroup sgml nil
"SGML editing mode"
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")))
(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
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)
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
"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.
(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
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")
((\"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.
((\"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)))
(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
(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
(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)
(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."
(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.
(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)
(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")
(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)
(eq (preceding-char) ?>)))
(backward-list)
(forward-list)))))))
+
\f
(autoload 'compile-internal "compile")
(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.
(replace-match (cdr (assq (char-before) '((?& . "&")
(?< . "<")
(?> . ">"))))))))
+
+(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."
(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.
(> (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)
map)
"Keymap for commands for use in HTML mode.")
-
(defvar html-face-tag-alist
'((bold . "b")
(italic . "i")
("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")))
("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"
;; (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.
(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."