From 335174ee5037a2751c31bfd9ecb87cedb4bc3cda Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Thu, 23 Mar 2017 11:34:18 -0600 Subject: [PATCH] add mhtml-mode.el * etc/NEWS: Update. * lisp/textmodes/mhtml-mode.el: New file. * test/manual/indent/html-multi.html: New file. * test/lisp/textmodes/mhtml-mode-tests.el: New file. * doc/emacs/text.texi (HTML Mode): Mention mhtml-mode. --- doc/emacs/text.texi | 10 +- etc/NEWS | 7 + lisp/textmodes/mhtml-mode.el | 390 ++++++++++++++++++++++++ test/lisp/textmodes/mhtml-mode-tests.el | 58 ++++ test/manual/indent/html-multi.html | 30 ++ 5 files changed, 493 insertions(+), 2 deletions(-) create mode 100644 lisp/textmodes/mhtml-mode.el create mode 100644 test/lisp/textmodes/mhtml-mode-tests.el create mode 100644 test/manual/indent/html-multi.html diff --git a/doc/emacs/text.texi b/doc/emacs/text.texi index 5f02d0b6920..d1e451175ed 100644 --- a/doc/emacs/text.texi +++ b/doc/emacs/text.texi @@ -1899,8 +1899,14 @@ between Latin-1 encoded files and @TeX{}-encoded equivalents. @findex html-mode The major modes for SGML and HTML provide indentation support and -commands for operating on tags. HTML mode is a slightly customized -variant of SGML mode. +commands for operating on tags. + + HTML consists of two modes---one, a basic mode called +@code{html-mode} is a slightly customized variant of SGML mode. The +other, which is used by default for HTML files, is called +@code{mhtml-mode}, and attempts to properly handle Javascript enclosed +in a @code{ + +When nil, indentation of the script body starts just below the +tag, like: + + + +When `ignore', the script body starts in the first column, like: + + " + :group 'sgml + :type '(choice (const nil) (const t) (const ignore)) + :safe 'symbolp + :version "26.1") + +(cl-defstruct mhtml--submode + ;; Name of this submode. + name + ;; HTML end tag. + end-tag + ;; Syntax table. + syntax-table + ;; Propertize function. + propertize + ;; Keymap. + keymap + ;; Captured locals that are set when entering a region. + crucial-captured-locals + ;; Other captured local variables; these are not set when entering a + ;; region but let-bound during certain operations, e.g., + ;; indentation. + captured-locals) + +(defconst mhtml--crucial-variable-prefix + (regexp-opt '("comment-" "uncomment-" "electric-indent-" + "smie-" "forward-sexp-function")) + "Regexp matching the prefix of \"crucial\" buffer-locals we want to capture.") + +(defconst mhtml--variable-prefix + (regexp-opt '("font-lock-" "indent-line-function" "major-mode")) + "Regexp matching the prefix of buffer-locals we want to capture.") + +(defun mhtml--construct-submode (mode &rest args) + "A wrapper for make-mhtml--submode that computes the buffer-local variables." + (let ((captured-locals nil) + (crucial-captured-locals nil) + (submode (apply #'make-mhtml--submode args))) + (with-temp-buffer + (funcall mode) + ;; Make sure font lock is all set up. + (font-lock-set-defaults) + ;; This has to be set to a value other than the mthml-mode + ;; value, to avoid recursion. + (unless (variable-binding-locus 'font-lock-fontify-region-function) + (setq-local font-lock-fontify-region-function + #'font-lock-default-fontify-region)) + (dolist (iter (buffer-local-variables)) + (when (string-match mhtml--crucial-variable-prefix + (symbol-name (car iter))) + (push iter crucial-captured-locals)) + (when (string-match mhtml--variable-prefix (symbol-name (car iter))) + (push iter captured-locals))) + (setf (mhtml--submode-crucial-captured-locals submode) + crucial-captured-locals) + (setf (mhtml--submode-captured-locals submode) captured-locals)) + submode)) + +(defun mhtml--mark-buffer-locals (submode) + (dolist (iter (mhtml--submode-captured-locals submode)) + (make-local-variable (car iter)))) + +(defvar-local mhtml--crucial-variables nil + "List of all crucial variable symbols.") + +(defun mhtml--mark-crucial-buffer-locals (submode) + (dolist (iter (mhtml--submode-crucial-captured-locals submode)) + (make-local-variable (car iter)) + (push (car iter) mhtml--crucial-variables))) + +(defconst mhtml--css-submode + (mhtml--construct-submode 'css-mode + :name "CSS" + :end-tag "" + :syntax-table css-mode-syntax-table + :propertize css-syntax-propertize-function + :keymap css-mode-map)) + +(defconst mhtml--js-submode + (mhtml--construct-submode 'js-mode + :name "JS" + :end-tag "" + :syntax-table js-mode-syntax-table + :propertize #'js-syntax-propertize + :keymap js-mode-map)) + +(defmacro mhtml--with-locals (submode &rest body) + (declare (indent 1)) + `(cl-progv + (when ,submode (mapcar #'car (mhtml--submode-captured-locals ,submode))) + (when ,submode (mapcar #'cdr (mhtml--submode-captured-locals ,submode))) + (cl-progv + (when ,submode (mapcar #'car (mhtml--submode-crucial-captured-locals + ,submode))) + (when ,submode (mapcar #'cdr (mhtml--submode-crucial-captured-locals + ,submode))) + ,@body))) + +(defun mhtml--submode-lighter () + "Mode-line lighter indicating the current submode." + (let ((submode (get-text-property (point) 'mhtml-submode))) + (if submode + (mhtml--submode-name submode) + ""))) + +(defvar font-lock-beg) +(defvar font-lock-end) + +(defun mhtml--extend-font-lock-region () + "Extend the font lock region according to HTML sub-mode needs. + +This is used via `font-lock-extend-region-functions'. It ensures +that the font-lock region is extended to cover either whole +lines, or to the spot where the submode changes, whichever is +smallest." + (let ((orig-beg font-lock-beg) + (orig-end font-lock-end)) + ;; The logic here may look odd but it is needed to ensure that we + ;; do the right thing when trying to limit the search. + (save-excursion + (goto-char font-lock-beg) + ;; previous-single-property-change starts by looking at the + ;; previous character, but we're trying to extend a region to + ;; include just characters with the same submode as this + ;; character. + (unless (eobp) + (forward-char)) + (setq font-lock-beg (previous-single-property-change + (point) 'mhtml-submode nil + (line-beginning-position))) + (unless (eq (get-text-property font-lock-beg 'mhtml-submode) + (get-text-property orig-beg 'mhtml-submode)) + (cl-incf font-lock-beg)) + + (goto-char font-lock-end) + (unless (bobp) + (backward-char)) + (setq font-lock-end (next-single-property-change + (point) 'mhtml-submode nil + (line-beginning-position 2))) + (unless (eq (get-text-property font-lock-end 'mhtml-submode) + (get-text-property orig-end 'mhtml-submode)) + (cl-decf font-lock-end))) + + (or (/= font-lock-beg orig-beg) + (/= font-lock-end orig-end)))) + +(defun mhtml--submode-fontify-one-region (submode beg end &optional loudly) + (if submode + (mhtml--with-locals submode + (save-restriction + (font-lock-fontify-region beg end loudly))) + (font-lock-set-defaults) + (font-lock-default-fontify-region beg end loudly))) + +(defun mhtml--submode-fontify-region (beg end loudly) + (syntax-propertize end) + (let ((orig-beg beg) + (orig-end end) + (new-beg beg) + (new-end end)) + (while (< beg end) + (let ((submode (get-text-property beg 'mhtml-submode)) + (this-end (next-single-property-change beg 'mhtml-submode + nil end))) + (let ((extended (mhtml--submode-fontify-one-region submode beg + this-end loudly))) + ;; If the call extended the region, take note. We track the + ;; bounds we were passed and take the union of any extended + ;; bounds. + (when (and (consp extended) + (eq (car extended) 'jit-lock-bounds)) + (setq new-beg (min new-beg (cadr extended))) + ;; Make sure that the next region starts where the + ;; extension of this region ends. + (setq this-end (cddr extended)) + (setq new-end (max new-end this-end)))) + (setq beg this-end))) + (when (or (/= orig-beg new-beg) + (/= orig-end new-end)) + (cons 'jit-lock-bounds (cons new-beg new-end))))) + +(defvar-local mhtml--last-submode nil + "Record the last visited submode, so the cursor-sensor function +can function properly.") + +(defvar-local mhtml--stashed-crucial-variables nil + "Alist of stashed values of the crucial variables.") + +(defun mhtml--stash-crucial-variables () + (setq mhtml--stashed-crucial-variables + (mapcar (lambda (sym) + (cons sym (buffer-local-value sym (current-buffer)))) + mhtml--crucial-variables))) + +(defun mhtml--map-in-crucial-variables (alist) + (dolist (item alist) + (set (car item) (cdr item)))) + +(defun mhtml--pre-command () + (let ((submode (get-text-property (point) 'mhtml-submode))) + (unless (eq submode mhtml--last-submode) + ;; If we're entering a submode, and the previous submode was + ;; nil, then stash the current values first. This lets the user + ;; at least modify some values directly. FIXME maybe always + ;; stash into the current mode? + (when (and submode (not mhtml--last-submode)) + (mhtml--stash-crucial-variables)) + (mhtml--map-in-crucial-variables + (if submode + (mhtml--submode-crucial-captured-locals submode) + mhtml--stashed-crucial-variables)) + (setq mhtml--last-submode submode)))) + +(defun mhtml--syntax-propertize-submode (submode end) + (save-excursion + (when (search-forward (mhtml--submode-end-tag submode) end t) + (setq end (match-beginning 0)))) + (set-text-properties (point) end + (list 'mhtml-submode submode + 'syntax-table (mhtml--submode-syntax-table submode) + ;; We want local-map here so that we act + ;; more like the sub-mode and don't + ;; override minor mode maps. + 'local-map (mhtml--submode-keymap submode))) + (funcall (mhtml--submode-propertize submode) (point) end) + (goto-char end)) + +(defun mhtml-syntax-propertize (start end) + ;; First remove our special settings from the affected text. They + ;; will be re-applied as needed. + (remove-list-of-text-properties start end + '(syntax-table local-map mhtml-submode)) + (goto-char start) + (when (and + ;; Don't search in a comment or string + (not (syntax-ppss-context (syntax-ppss))) + ;; Be sure to look back one character, because START won't + ;; yet have been propertized. + (not (bobp))) + (when-let ((submode (get-text-property (1- (point)) 'mhtml-submode))) + (mhtml--syntax-propertize-submode submode end))) + (funcall + (syntax-propertize-rules + ("" + (0 (ignore + (goto-char (match-end 0)) + ;; Don't apply in a comment. + (unless (syntax-ppss-context (syntax-ppss)) + (mhtml--syntax-propertize-submode mhtml--css-submode end))))) + ("" + (0 (ignore + (goto-char (match-end 0)) + ;; Don't apply in a comment. + (unless (syntax-ppss-context (syntax-ppss)) + (mhtml--syntax-propertize-submode mhtml--js-submode end))))) + sgml-syntax-propertize-rules) + ;; Make sure to handle the situation where + ;; mhtml--syntax-propertize-submode moved point. + (point) end)) + +(defun mhtml-indent-line () + "Indent the current line as HTML, JS, or CSS, according to its context." + (interactive) + (let ((submode (save-excursion + (back-to-indentation) + (get-text-property (point) 'mhtml-submode)))) + (if submode + (save-restriction + (let* ((region-start + (or (previous-single-property-change (point) 'mhtml-submode) + (point))) + (base-indent (save-excursion + (goto-char region-start) + (sgml-calculate-indent)))) + (cond + ((eq mhtml-tag-relative-indent nil) + (setq base-indent (- base-indent sgml-basic-offset))) + ((eq mhtml-tag-relative-indent 'ignore) + (setq base-indent 0))) + (narrow-to-region region-start (point-max)) + (let ((prog-indentation-context (list base-indent + (cons (point-min) nil) + nil))) + (mhtml--with-locals submode + ;; indent-line-function was rebound by + ;; mhtml--with-locals. + (funcall indent-line-function))))) + ;; HTML. + (sgml-indent-line)))) + +(defun mhtml--flyspell-check-word () + (let ((submode (get-text-property (point) 'mhtml-submode))) + (if submode + (flyspell-generic-progmode-verify) + t))) + +;;;###autoload +(define-derived-mode mhtml-mode html-mode + '((sgml-xml-mode "XHTML+" "HTML+") (:eval (mhtml--submode-lighter))) + "Major mode based on `html-mode', but works with embedded JS and CSS. + +Code inside a + + + + -- 2.39.2