mhtml-mode.el 15 KB

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