Fix Rmail editing with reapplying encoding to message body
[emacs.git] / lisp / textmodes / mhtml-mode.el
blob2f2257d96bdb818a4121818b0bddebc487e87c7f
1 ;;; mhtml-mode.el --- HTML editing mode that handles CSS and JS -*- lexical-binding:t -*-
3 ;; Copyright (C) 2017 Free Software Foundation, Inc.
5 ;; Keywords: wp, hypermedia, comm, languages
7 ;; This file is part of GNU Emacs.
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.
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.
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/>.
22 ;;; Code:
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)
32 (defcustom mhtml-tag-relative-indent t
33 "How <script> and <style> bodies are indented relative to the tag.
35 When t, indentation looks like:
37 <script>
38 code();
39 </script>
41 When nil, indentation of the script body starts just below the
42 tag, like:
44 <script>
45 code();
46 </script>
48 When `ignore', the script body starts in the first column, like:
50 <script>
51 code();
52 </script>"
53 :group 'sgml
54 :type '(choice (const nil) (const t) (const ignore))
55 :safe 'symbolp
56 :version "26.1")
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)
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.")
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.")
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 mhtml-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))
110 (defun mhtml--mark-buffer-locals (submode)
111 (dolist (iter (mhtml--submode-captured-locals submode))
112 (make-local-variable (car iter))))
114 (defvar-local mhtml--crucial-variables nil
115 "List of all crucial variable symbols.")
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)))
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))
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))
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)))
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 "")))
157 (defvar font-lock-beg)
158 (defvar font-lock-end)
160 (defun mhtml--extend-font-lock-region ()
161 "Extend the font lock region according to HTML sub-mode needs.
163 This is used via `font-lock-extend-region-functions'. It ensures
164 that the font-lock region is extended to cover either whole
165 lines, or to the spot where the submode changes, whichever is
166 smallest."
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))
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)))
196 (or (/= font-lock-beg orig-beg)
197 (/= font-lock-end orig-end))))
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)))
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)))))
234 (defvar-local mhtml--last-submode nil
235 "Record the last visited submode, so the cursor-sensor function
236 can function properly.")
238 (defvar-local mhtml--stashed-crucial-variables nil
239 "Alist of stashed values of the crucial variables.")
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)))
247 (defun mhtml--map-in-crucial-variables (alist)
248 (dolist (item alist)
249 (set (car item) (cdr item))))
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))))
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))
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 ;; Be sure to look back one character, because START won't yet have
287 ;; been propertized.
288 (unless (bobp)
289 (let ((submode (get-text-property (1- (point)) 'mhtml-submode)))
290 (if submode
291 ;; Don't search in a comment or string
292 (unless (syntax-ppss-context (syntax-ppss))
293 (mhtml--syntax-propertize-submode submode end))
294 ;; No submode, so do what sgml-mode does.
295 (sgml-syntax-propertize-inside end))))
296 (funcall
297 (syntax-propertize-rules
298 ("<style.*?>"
299 (0 (ignore
300 (goto-char (match-end 0))
301 ;; Don't apply in a comment.
302 (unless (syntax-ppss-context (syntax-ppss))
303 (mhtml--syntax-propertize-submode mhtml--css-submode end)))))
304 ("<script.*?>"
305 (0 (ignore
306 (goto-char (match-end 0))
307 ;; Don't apply in a comment.
308 (unless (syntax-ppss-context (syntax-ppss))
309 (mhtml--syntax-propertize-submode mhtml--js-submode end)))))
310 sgml-syntax-propertize-rules)
311 ;; Make sure to handle the situation where
312 ;; mhtml--syntax-propertize-submode moved point.
313 (point) end))
315 (defun mhtml-indent-line ()
316 "Indent the current line as HTML, JS, or CSS, according to its context."
317 (interactive)
318 (let ((submode (save-excursion
319 (back-to-indentation)
320 (get-text-property (point) 'mhtml-submode))))
321 (if submode
322 (save-restriction
323 (let* ((region-start
324 (or (previous-single-property-change (point) 'mhtml-submode)
325 (point)))
326 (base-indent (save-excursion
327 (goto-char region-start)
328 (sgml-calculate-indent))))
329 (cond
330 ((eq mhtml-tag-relative-indent nil)
331 (setq base-indent (- base-indent sgml-basic-offset)))
332 ((eq mhtml-tag-relative-indent 'ignore)
333 (setq base-indent 0)))
334 (narrow-to-region region-start (point-max))
335 (let ((prog-indentation-context (list base-indent
336 (cons (point-min) nil)
337 nil)))
338 (mhtml--with-locals submode
339 ;; indent-line-function was rebound by
340 ;; mhtml--with-locals.
341 (funcall indent-line-function)))))
342 ;; HTML.
343 (sgml-indent-line))))
345 (defun mhtml--flyspell-check-word ()
346 (let ((submode (get-text-property (point) 'mhtml-submode)))
347 (if submode
348 (flyspell-generic-progmode-verify)
349 t)))
351 ;;;###autoload
352 (define-derived-mode mhtml-mode html-mode
353 '((sgml-xml-mode "XHTML+" "HTML+") (:eval (mhtml--submode-lighter)))
354 "Major mode based on `html-mode', but works with embedded JS and CSS.
356 Code inside a <script> element is indented using the rules from
357 `js-mode'; and code inside a <style> element is indented using
358 the rules from `css-mode'."
359 (cursor-sensor-mode)
360 (setq-local indent-line-function #'mhtml-indent-line)
361 (setq-local parse-sexp-lookup-properties t)
362 (setq-local syntax-propertize-function #'mhtml-syntax-propertize)
363 (setq-local font-lock-fontify-region-function
364 #'mhtml--submode-fontify-region)
365 (setq-local font-lock-extend-region-functions
366 '(mhtml--extend-font-lock-region
367 font-lock-extend-region-multiline))
369 ;; Attach this to both pre- and post- hooks just in case it ever
370 ;; changes a key binding that might be accessed from the menu bar.
371 (add-hook 'pre-command-hook #'mhtml--pre-command nil t)
372 (add-hook 'post-command-hook #'mhtml--pre-command nil t)
374 ;; Make any captured variables buffer-local.
375 (mhtml--mark-buffer-locals mhtml--css-submode)
376 (mhtml--mark-buffer-locals mhtml--js-submode)
378 (mhtml--mark-crucial-buffer-locals mhtml--css-submode)
379 (mhtml--mark-crucial-buffer-locals mhtml--js-submode)
380 (setq mhtml--crucial-variables (delete-dups mhtml--crucial-variables))
382 ;: Hack
383 (js--update-quick-match-re)
385 ;; This is sort of a prog-mode as well as a text mode.
386 (run-hooks 'prog-mode-hook))
388 (put 'mhtml-mode 'flyspell-mode-predicate #'mhtml--flyspell-check-word)
390 (provide 'mhtml-mode)
392 ;;; mhtml-mode.el ends here