diff options
author | Tom Tromey <tom@tromey.com> | 2017-03-23 11:34:18 -0600 |
---|---|---|
committer | Tom Tromey <tom@tromey.com> | 2017-04-05 15:53:39 -0600 |
commit | 335174ee5037a2751c31bfd9ecb87cedb4bc3cda (patch) | |
tree | d2faaab5f48303aa08ef707d842d2e22e31650f6 | |
parent | d392b6e82460d94b11627998da87e33880664060 (diff) | |
download | emacs-335174ee5037a2751c31bfd9ecb87cedb4bc3cda.tar.gz |
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.
-rw-r--r-- | doc/emacs/text.texi | 10 | ||||
-rw-r--r-- | etc/NEWS | 7 | ||||
-rw-r--r-- | lisp/textmodes/mhtml-mode.el | 390 | ||||
-rw-r--r-- | test/lisp/textmodes/mhtml-mode-tests.el | 58 | ||||
-rw-r--r-- | test/manual/indent/html-multi.html | 30 |
5 files changed, 493 insertions, 2 deletions
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{<script>} element and CSS embedded in a @code{<style>} +element. @table @kbd @item C-c C-n @@ -753,6 +753,13 @@ processes on exit. ** New library 'xdg' with utilities for some XDG standards and specs. +** HTML + ++++ +*** A new submode of `html-mode', `mthml-mode', is now the default + mode for *.html files. This mode handles indentation, + fontification, and commenting for embedded JavaScript and CSS. + * Incompatible Lisp Changes in Emacs 26.1 diff --git a/lisp/textmodes/mhtml-mode.el b/lisp/textmodes/mhtml-mode.el new file mode 100644 index 00000000000..e9e09d4d959 --- /dev/null +++ b/lisp/textmodes/mhtml-mode.el @@ -0,0 +1,390 @@ +;;; mhtml-mode.el --- HTML editing mode that handles CSS and JS -*- lexical-binding:t -*- + +;; Copyright (C) 2017 Free Software Foundation, Inc. + +;; Keywords: wp, hypermedia, comm, languages + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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. If not, see <http://www.gnu.org/licenses/>. + +;;; Code: + +(eval-and-compile + (require 'flyspell) + (require 'sgml-mode)) +(require 'js) +(require 'css-mode) +(require 'prog-mode) +(require 'font-lock) + +(defcustom mhtml-tag-relative-indent t + "How <script> and <style> bodies are indented relative to the tag. + +When t, indentation looks like: + + <script> + code(); + </script> + +When nil, indentation of the script body starts just below the +tag, like: + + <script> + code(); + </script> + +When `ignore', the script body starts in the first column, like: + + <script> +code(); + </script>" + :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 "</style>" + :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 "</script>" + :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 + ("<style.*?>" + (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))))) + ("<script.*?>" + (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 <script> element is indented using the rules from +`js-mode'; and code inside a <style> element is indented using +the rules from `css-mode'." + (cursor-sensor-mode) + (setq-local indent-line-function #'mhtml-indent-line) + (setq-local parse-sexp-lookup-properties t) + (setq-local syntax-propertize-function #'mhtml-syntax-propertize) + (setq-local font-lock-fontify-region-function + #'mhtml--submode-fontify-region) + (setq-local font-lock-extend-region-functions + '(mhtml--extend-font-lock-region + font-lock-extend-region-multiline)) + + ;; Attach this to both pre- and post- hooks just in case it ever + ;; changes a key binding that might be accessed from the menu bar. + (add-hook 'pre-command-hook #'mhtml--pre-command nil t) + (add-hook 'post-command-hook #'mhtml--pre-command nil t) + + ;; Make any captured variables buffer-local. + (mhtml--mark-buffer-locals mhtml--css-submode) + (mhtml--mark-buffer-locals mhtml--js-submode) + + (mhtml--mark-crucial-buffer-locals mhtml--css-submode) + (mhtml--mark-crucial-buffer-locals mhtml--js-submode) + (setq mhtml--crucial-variables (delete-dups mhtml--crucial-variables)) + + ;: Hack + (js--update-quick-match-re) + + ;; This is sort of a prog-mode as well as a text mode. + (run-hooks 'prog-mode-hook)) + +(put 'mhtml-mode 'flyspell-mode-predicate #'mhtml--flyspell-check-word) + +(provide 'mhtml-mode) + +;;; mhtml-mode.el ends here diff --git a/test/lisp/textmodes/mhtml-mode-tests.el b/test/lisp/textmodes/mhtml-mode-tests.el new file mode 100644 index 00000000000..d8eeb27643c --- /dev/null +++ b/test/lisp/textmodes/mhtml-mode-tests.el @@ -0,0 +1,58 @@ +;;; mhtml-mode-tests.el --- Tests for mhtml-mode + +;; Copyright (C) 2017 Free Software Foundation, Inc. + +;; Keywords: tests + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs 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. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;;; Code: + +(require 'mhtml-mode) +(require 'ert) + +(defun mhtml-test-syntax (before after what) + (with-temp-buffer + (mhtml-mode) + (insert before) + (save-excursion + (insert after)) + (font-lock-ensure) + (should (eq (syntax-ppss-context (syntax-ppss)) what)))) + +(ert-deftest mthml-comment-js () + (mhtml-test-syntax "<html><script>\n/* " + " some text */<script></html>" + 'comment)) + +(ert-deftest mthml-string-js () + (mhtml-test-syntax "<html><script>\n\" " + " some text \"<script></html>" + 'string)) + +(ert-deftest mthml-comment-css () + (mhtml-test-syntax "<html><style>\n/* " + " some text */<style></html>" + 'comment)) + +(ert-deftest mthml-string-css () + (mhtml-test-syntax "<html><style>\n\" " + " some text \"<style></html>" + 'string)) + +;;; mhtml-mode-tests.el ends here diff --git a/test/manual/indent/html-multi.html b/test/manual/indent/html-multi.html new file mode 100644 index 00000000000..a563c5eb244 --- /dev/null +++ b/test/manual/indent/html-multi.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> + <head> + <title>test</title> + <!-- + <script> + // No indent in a comment + if (true) { + do not change; + } + </script> + --> + + <style type="text/css"> + h1 { + font-family: 'Spinnaker', sans-serif; + } + </style> + <script> + var a = 4 < 5; + function() { + if (x > 75) + return 25; + return "hello"; + } + </script> + </head> + <body> + </body> +</html> |