aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Tromey2017-03-23 11:34:18 -0600
committerTom Tromey2017-04-05 15:53:39 -0600
commit335174ee5037a2751c31bfd9ecb87cedb4bc3cda (patch)
treed2faaab5f48303aa08ef707d842d2e22e31650f6
parentd392b6e82460d94b11627998da87e33880664060 (diff)
downloademacs-335174ee5037a2751c31bfd9ecb87cedb4bc3cda.tar.gz
emacs-335174ee5037a2751c31bfd9ecb87cedb4bc3cda.zip
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.texi10
-rw-r--r--etc/NEWS7
-rw-r--r--lisp/textmodes/mhtml-mode.el390
-rw-r--r--test/lisp/textmodes/mhtml-mode-tests.el58
-rw-r--r--test/manual/indent/html-multi.html30
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.
1899@findex html-mode 1899@findex html-mode
1900 1900
1901 The major modes for SGML and HTML provide indentation support and 1901 The major modes for SGML and HTML provide indentation support and
1902commands for operating on tags. HTML mode is a slightly customized 1902commands for operating on tags.
1903variant of SGML mode. 1903
1904 HTML consists of two modes---one, a basic mode called
1905@code{html-mode} is a slightly customized variant of SGML mode. The
1906other, which is used by default for HTML files, is called
1907@code{mhtml-mode}, and attempts to properly handle Javascript enclosed
1908in a @code{<script>} element and CSS embedded in a @code{<style>}
1909element.
1904 1910
1905@table @kbd 1911@table @kbd
1906@item C-c C-n 1912@item C-c C-n
diff --git a/etc/NEWS b/etc/NEWS
index 8b17f1648bf..3a3503b133c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -753,6 +753,13 @@ processes on exit.
753 753
754** New library 'xdg' with utilities for some XDG standards and specs. 754** New library 'xdg' with utilities for some XDG standards and specs.
755 755
756** HTML
757
758+++
759*** A new submode of `html-mode', `mthml-mode', is now the default
760 mode for *.html files. This mode handles indentation,
761 fontification, and commenting for embedded JavaScript and CSS.
762
756 763
757* Incompatible Lisp Changes in Emacs 26.1 764* Incompatible Lisp Changes in Emacs 26.1
758 765
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 @@
1;;; mhtml-mode.el --- HTML editing mode that handles CSS and JS -*- lexical-binding:t -*-
2
3;; Copyright (C) 2017 Free Software Foundation, Inc.
4
5;; Keywords: wp, hypermedia, comm, languages
6
7;; This file is part of GNU Emacs.
8
9;; GNU Emacs is free software: you can redistribute it and/or modify
10;; it under the terms of the GNU General Public License as published by
11;; the Free Software Foundation, either version 3 of the License, or
12;; (at your option) any later version.
13
14;; GNU Emacs is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;; GNU General Public License for more details.
18
19;; You should have received a copy of the GNU General Public License
20;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
21
22;;; Code:
23
24(eval-and-compile
25 (require 'flyspell)
26 (require 'sgml-mode))
27(require 'js)
28(require 'css-mode)
29(require 'prog-mode)
30(require 'font-lock)
31
32(defcustom mhtml-tag-relative-indent t
33 "How <script> and <style> bodies are indented relative to the tag.
34
35When t, indentation looks like:
36
37 <script>
38 code();
39 </script>
40
41When nil, indentation of the script body starts just below the
42tag, like:
43
44 <script>
45 code();
46 </script>
47
48When `ignore', the script body starts in the first column, like:
49
50 <script>
51code();
52 </script>"
53 :group 'sgml
54 :type '(choice (const nil) (const t) (const ignore))
55 :safe 'symbolp
56 :version "26.1")
57
58(cl-defstruct mhtml--submode
59 ;; Name of this submode.
60 name
61 ;; HTML end tag.
62 end-tag
63 ;; Syntax table.
64 syntax-table
65 ;; Propertize function.
66 propertize
67 ;; Keymap.
68 keymap
69 ;; Captured locals that are set when entering a region.
70 crucial-captured-locals
71 ;; Other captured local variables; these are not set when entering a
72 ;; region but let-bound during certain operations, e.g.,
73 ;; indentation.
74 captured-locals)
75
76(defconst mhtml--crucial-variable-prefix
77 (regexp-opt '("comment-" "uncomment-" "electric-indent-"
78 "smie-" "forward-sexp-function"))
79 "Regexp matching the prefix of \"crucial\" buffer-locals we want to capture.")
80
81(defconst mhtml--variable-prefix
82 (regexp-opt '("font-lock-" "indent-line-function" "major-mode"))
83 "Regexp matching the prefix of buffer-locals we want to capture.")
84
85(defun mhtml--construct-submode (mode &rest args)
86 "A wrapper for make-mhtml--submode that computes the buffer-local variables."
87 (let ((captured-locals nil)
88 (crucial-captured-locals nil)
89 (submode (apply #'make-mhtml--submode args)))
90 (with-temp-buffer
91 (funcall mode)
92 ;; Make sure font lock is all set up.
93 (font-lock-set-defaults)
94 ;; This has to be set to a value other than the mthml-mode
95 ;; value, to avoid recursion.
96 (unless (variable-binding-locus 'font-lock-fontify-region-function)
97 (setq-local font-lock-fontify-region-function
98 #'font-lock-default-fontify-region))
99 (dolist (iter (buffer-local-variables))
100 (when (string-match mhtml--crucial-variable-prefix
101 (symbol-name (car iter)))
102 (push iter crucial-captured-locals))
103 (when (string-match mhtml--variable-prefix (symbol-name (car iter)))
104 (push iter captured-locals)))
105 (setf (mhtml--submode-crucial-captured-locals submode)
106 crucial-captured-locals)
107 (setf (mhtml--submode-captured-locals submode) captured-locals))
108 submode))
109
110(defun mhtml--mark-buffer-locals (submode)
111 (dolist (iter (mhtml--submode-captured-locals submode))
112 (make-local-variable (car iter))))
113
114(defvar-local mhtml--crucial-variables nil
115 "List of all crucial variable symbols.")
116
117(defun mhtml--mark-crucial-buffer-locals (submode)
118 (dolist (iter (mhtml--submode-crucial-captured-locals submode))
119 (make-local-variable (car iter))
120 (push (car iter) mhtml--crucial-variables)))
121
122(defconst mhtml--css-submode
123 (mhtml--construct-submode 'css-mode
124 :name "CSS"
125 :end-tag "</style>"
126 :syntax-table css-mode-syntax-table
127 :propertize css-syntax-propertize-function
128 :keymap css-mode-map))
129
130(defconst mhtml--js-submode
131 (mhtml--construct-submode 'js-mode
132 :name "JS"
133 :end-tag "</script>"
134 :syntax-table js-mode-syntax-table
135 :propertize #'js-syntax-propertize
136 :keymap js-mode-map))
137
138(defmacro mhtml--with-locals (submode &rest body)
139 (declare (indent 1))
140 `(cl-progv
141 (when ,submode (mapcar #'car (mhtml--submode-captured-locals ,submode)))
142 (when ,submode (mapcar #'cdr (mhtml--submode-captured-locals ,submode)))
143 (cl-progv
144 (when ,submode (mapcar #'car (mhtml--submode-crucial-captured-locals
145 ,submode)))
146 (when ,submode (mapcar #'cdr (mhtml--submode-crucial-captured-locals
147 ,submode)))
148 ,@body)))
149
150(defun mhtml--submode-lighter ()
151 "Mode-line lighter indicating the current submode."
152 (let ((submode (get-text-property (point) 'mhtml-submode)))
153 (if submode
154 (mhtml--submode-name submode)
155 "")))
156
157(defvar font-lock-beg)
158(defvar font-lock-end)
159
160(defun mhtml--extend-font-lock-region ()
161 "Extend the font lock region according to HTML sub-mode needs.
162
163This is used via `font-lock-extend-region-functions'. It ensures
164that the font-lock region is extended to cover either whole
165lines, or to the spot where the submode changes, whichever is
166smallest."
167 (let ((orig-beg font-lock-beg)
168 (orig-end font-lock-end))
169 ;; The logic here may look odd but it is needed to ensure that we
170 ;; do the right thing when trying to limit the search.
171 (save-excursion
172 (goto-char font-lock-beg)
173 ;; previous-single-property-change starts by looking at the
174 ;; previous character, but we're trying to extend a region to
175 ;; include just characters with the same submode as this
176 ;; character.
177 (unless (eobp)
178 (forward-char))
179 (setq font-lock-beg (previous-single-property-change
180 (point) 'mhtml-submode nil
181 (line-beginning-position)))
182 (unless (eq (get-text-property font-lock-beg 'mhtml-submode)
183 (get-text-property orig-beg 'mhtml-submode))
184 (cl-incf font-lock-beg))
185
186 (goto-char font-lock-end)
187 (unless (bobp)
188 (backward-char))
189 (setq font-lock-end (next-single-property-change
190 (point) 'mhtml-submode nil
191 (line-beginning-position 2)))
192 (unless (eq (get-text-property font-lock-end 'mhtml-submode)
193 (get-text-property orig-end 'mhtml-submode))
194 (cl-decf font-lock-end)))
195
196 (or (/= font-lock-beg orig-beg)
197 (/= font-lock-end orig-end))))
198
199(defun mhtml--submode-fontify-one-region (submode beg end &optional loudly)
200 (if submode
201 (mhtml--with-locals submode
202 (save-restriction
203 (font-lock-fontify-region beg end loudly)))
204 (font-lock-set-defaults)
205 (font-lock-default-fontify-region beg end loudly)))
206
207(defun mhtml--submode-fontify-region (beg end loudly)
208 (syntax-propertize end)
209 (let ((orig-beg beg)
210 (orig-end end)
211 (new-beg beg)
212 (new-end end))
213 (while (< beg end)
214 (let ((submode (get-text-property beg 'mhtml-submode))
215 (this-end (next-single-property-change beg 'mhtml-submode
216 nil end)))
217 (let ((extended (mhtml--submode-fontify-one-region submode beg
218 this-end loudly)))
219 ;; If the call extended the region, take note. We track the
220 ;; bounds we were passed and take the union of any extended
221 ;; bounds.
222 (when (and (consp extended)
223 (eq (car extended) 'jit-lock-bounds))
224 (setq new-beg (min new-beg (cadr extended)))
225 ;; Make sure that the next region starts where the
226 ;; extension of this region ends.
227 (setq this-end (cddr extended))
228 (setq new-end (max new-end this-end))))
229 (setq beg this-end)))
230 (when (or (/= orig-beg new-beg)
231 (/= orig-end new-end))
232 (cons 'jit-lock-bounds (cons new-beg new-end)))))
233
234(defvar-local mhtml--last-submode nil
235 "Record the last visited submode, so the cursor-sensor function
236can function properly.")
237
238(defvar-local mhtml--stashed-crucial-variables nil
239 "Alist of stashed values of the crucial variables.")
240
241(defun mhtml--stash-crucial-variables ()
242 (setq mhtml--stashed-crucial-variables
243 (mapcar (lambda (sym)
244 (cons sym (buffer-local-value sym (current-buffer))))
245 mhtml--crucial-variables)))
246
247(defun mhtml--map-in-crucial-variables (alist)
248 (dolist (item alist)
249 (set (car item) (cdr item))))
250
251(defun mhtml--pre-command ()
252 (let ((submode (get-text-property (point) 'mhtml-submode)))
253 (unless (eq submode mhtml--last-submode)
254 ;; If we're entering a submode, and the previous submode was
255 ;; nil, then stash the current values first. This lets the user
256 ;; at least modify some values directly. FIXME maybe always
257 ;; stash into the current mode?
258 (when (and submode (not mhtml--last-submode))
259 (mhtml--stash-crucial-variables))
260 (mhtml--map-in-crucial-variables
261 (if submode
262 (mhtml--submode-crucial-captured-locals submode)
263 mhtml--stashed-crucial-variables))
264 (setq mhtml--last-submode submode))))
265
266(defun mhtml--syntax-propertize-submode (submode end)
267 (save-excursion
268 (when (search-forward (mhtml--submode-end-tag submode) end t)
269 (setq end (match-beginning 0))))
270 (set-text-properties (point) end
271 (list 'mhtml-submode submode
272 'syntax-table (mhtml--submode-syntax-table submode)
273 ;; We want local-map here so that we act
274 ;; more like the sub-mode and don't
275 ;; override minor mode maps.
276 'local-map (mhtml--submode-keymap submode)))
277 (funcall (mhtml--submode-propertize submode) (point) end)
278 (goto-char end))
279
280(defun mhtml-syntax-propertize (start end)
281 ;; First remove our special settings from the affected text. They
282 ;; will be re-applied as needed.
283 (remove-list-of-text-properties start end
284 '(syntax-table local-map mhtml-submode))
285 (goto-char start)
286 (when (and
287 ;; Don't search in a comment or string
288 (not (syntax-ppss-context (syntax-ppss)))
289 ;; Be sure to look back one character, because START won't
290 ;; yet have been propertized.
291 (not (bobp)))
292 (when-let ((submode (get-text-property (1- (point)) 'mhtml-submode)))
293 (mhtml--syntax-propertize-submode submode end)))
294 (funcall
295 (syntax-propertize-rules
296 ("<style.*?>"
297 (0 (ignore
298 (goto-char (match-end 0))
299 ;; Don't apply in a comment.
300 (unless (syntax-ppss-context (syntax-ppss))
301 (mhtml--syntax-propertize-submode mhtml--css-submode end)))))
302 ("<script.*?>"
303 (0 (ignore
304 (goto-char (match-end 0))
305 ;; Don't apply in a comment.
306 (unless (syntax-ppss-context (syntax-ppss))
307 (mhtml--syntax-propertize-submode mhtml--js-submode end)))))
308 sgml-syntax-propertize-rules)
309 ;; Make sure to handle the situation where
310 ;; mhtml--syntax-propertize-submode moved point.
311 (point) end))
312
313(defun mhtml-indent-line ()
314 "Indent the current line as HTML, JS, or CSS, according to its context."
315 (interactive)
316 (let ((submode (save-excursion
317 (back-to-indentation)
318 (get-text-property (point) 'mhtml-submode))))
319 (if submode
320 (save-restriction
321 (let* ((region-start
322 (or (previous-single-property-change (point) 'mhtml-submode)
323 (point)))
324 (base-indent (save-excursion
325 (goto-char region-start)
326 (sgml-calculate-indent))))
327 (cond
328 ((eq mhtml-tag-relative-indent nil)
329 (setq base-indent (- base-indent sgml-basic-offset)))
330 ((eq mhtml-tag-relative-indent 'ignore)
331 (setq base-indent 0)))
332 (narrow-to-region region-start (point-max))
333 (let ((prog-indentation-context (list base-indent
334 (cons (point-min) nil)
335 nil)))
336 (mhtml--with-locals submode
337 ;; indent-line-function was rebound by
338 ;; mhtml--with-locals.
339 (funcall indent-line-function)))))
340 ;; HTML.
341 (sgml-indent-line))))
342
343(defun mhtml--flyspell-check-word ()
344 (let ((submode (get-text-property (point) 'mhtml-submode)))
345 (if submode
346 (flyspell-generic-progmode-verify)
347 t)))
348
349;;;###autoload
350(define-derived-mode mhtml-mode html-mode
351 '((sgml-xml-mode "XHTML+" "HTML+") (:eval (mhtml--submode-lighter)))
352 "Major mode based on `html-mode', but works with embedded JS and CSS.
353
354Code inside a <script> element is indented using the rules from
355`js-mode'; and code inside a <style> element is indented using
356the rules from `css-mode'."
357 (cursor-sensor-mode)
358 (setq-local indent-line-function #'mhtml-indent-line)
359 (setq-local parse-sexp-lookup-properties t)
360 (setq-local syntax-propertize-function #'mhtml-syntax-propertize)
361 (setq-local font-lock-fontify-region-function
362 #'mhtml--submode-fontify-region)
363 (setq-local font-lock-extend-region-functions
364 '(mhtml--extend-font-lock-region
365 font-lock-extend-region-multiline))
366
367 ;; Attach this to both pre- and post- hooks just in case it ever
368 ;; changes a key binding that might be accessed from the menu bar.
369 (add-hook 'pre-command-hook #'mhtml--pre-command nil t)
370 (add-hook 'post-command-hook #'mhtml--pre-command nil t)
371
372 ;; Make any captured variables buffer-local.
373 (mhtml--mark-buffer-locals mhtml--css-submode)
374 (mhtml--mark-buffer-locals mhtml--js-submode)
375
376 (mhtml--mark-crucial-buffer-locals mhtml--css-submode)
377 (mhtml--mark-crucial-buffer-locals mhtml--js-submode)
378 (setq mhtml--crucial-variables (delete-dups mhtml--crucial-variables))
379
380 ;: Hack
381 (js--update-quick-match-re)
382
383 ;; This is sort of a prog-mode as well as a text mode.
384 (run-hooks 'prog-mode-hook))
385
386(put 'mhtml-mode 'flyspell-mode-predicate #'mhtml--flyspell-check-word)
387
388(provide 'mhtml-mode)
389
390;;; 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 @@
1;;; mhtml-mode-tests.el --- Tests for mhtml-mode
2
3;; Copyright (C) 2017 Free Software Foundation, Inc.
4
5;; Keywords: tests
6
7;; This file is part of GNU Emacs.
8
9;; GNU Emacs is free software: you can redistribute it and/or modify
10;; it under the terms of the GNU General Public License as published by
11;; the Free Software Foundation, either version 3 of the License, or
12;; (at your option) any later version.
13
14;; GNU Emacs is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;; GNU General Public License for more details.
18
19;; You should have received a copy of the GNU General Public License
20;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
21
22;;; Commentary:
23
24;;; Code:
25
26(require 'mhtml-mode)
27(require 'ert)
28
29(defun mhtml-test-syntax (before after what)
30 (with-temp-buffer
31 (mhtml-mode)
32 (insert before)
33 (save-excursion
34 (insert after))
35 (font-lock-ensure)
36 (should (eq (syntax-ppss-context (syntax-ppss)) what))))
37
38(ert-deftest mthml-comment-js ()
39 (mhtml-test-syntax "<html><script>\n/* "
40 " some text */<script></html>"
41 'comment))
42
43(ert-deftest mthml-string-js ()
44 (mhtml-test-syntax "<html><script>\n\" "
45 " some text \"<script></html>"
46 'string))
47
48(ert-deftest mthml-comment-css ()
49 (mhtml-test-syntax "<html><style>\n/* "
50 " some text */<style></html>"
51 'comment))
52
53(ert-deftest mthml-string-css ()
54 (mhtml-test-syntax "<html><style>\n\" "
55 " some text \"<style></html>"
56 'string))
57
58;;; 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 @@
1<!DOCTYPE html>
2<html>
3 <head>
4 <title>test</title>
5 <!--
6 <script>
7 // No indent in a comment
8 if (true) {
9 do not change;
10 }
11 </script>
12 -->
13
14 <style type="text/css">
15 h1 {
16 font-family: 'Spinnaker', sans-serif;
17 }
18 </style>
19 <script>
20 var a = 4 < 5;
21 function() {
22 if (x > 75)
23 return 25;
24 return "hello";
25 }
26 </script>
27 </head>
28 <body>
29 </body>
30</html>