aboutsummaryrefslogtreecommitdiffstats
path: root/lisp/textmodes
diff options
context:
space:
mode:
authorTom Tromey2017-03-23 11:34:18 -0600
committerTom Tromey2017-04-05 15:53:39 -0600
commit335174ee5037a2751c31bfd9ecb87cedb4bc3cda (patch)
treed2faaab5f48303aa08ef707d842d2e22e31650f6 /lisp/textmodes
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.
Diffstat (limited to 'lisp/textmodes')
-rw-r--r--lisp/textmodes/mhtml-mode.el390
1 files changed, 390 insertions, 0 deletions
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