From: Tom Tromey <tom@tromey.com>
To: Dmitry Gutov <dgutov@yandex.ru>
Cc: Tom Tromey <tom@tromey.com>,
Stefan Monnier <monnier@IRO.UMontreal.CA>,
emacs-devel@gnu.org
Subject: Re: html, css, and js modes working together
Date: Sun, 12 Feb 2017 19:48:11 -0700 [thread overview]
Message-ID: <87inoe24d0.fsf@tromey.com> (raw)
In-Reply-To: <5a7078f5-3dbc-acc5-f293-6200e77f73b2@yandex.ru> (Dmitry Gutov's message of "Mon, 13 Feb 2017 03:59:05 +0200")
>>>>> "Dmitry" == Dmitry Gutov <dgutov@yandex.ru> writes:
>> I've appended the current code.
Dmitry> I'm getting "Symbol’s value as variable is void:
Dmitry> sgml-syntax-propertize-rules". Does it still require parts of the
Dmitry> previous patch?
Yeah, sorry, I only appended the new mode's file since I thought you
only wanted it to see what it was doing.
Here's my current patch, now with pre- and post-command hooks and
flyspell integration. Font lock mostly works but has some oddities, and
I still didn't fix the html comment/syntax bug that Clément found.
This is way smaller than I thought it would end up being, basically I
think because syntax-propertizing is a nice tool.
Tom
diff --git a/lisp/emacs-lisp/smie.el b/lisp/emacs-lisp/smie.el
index 4d02b75..7baccbc 100644
--- a/lisp/emacs-lisp/smie.el
+++ b/lisp/emacs-lisp/smie.el
@@ -123,6 +123,8 @@
(eval-when-compile (require 'cl-lib))
+(require 'prog-mode)
+
(defgroup smie nil
"Simple Minded Indentation Engine."
:group 'languages)
@@ -1455,7 +1457,7 @@ smie-indent-bob
;; Start the file at column 0.
(save-excursion
(forward-comment (- (point)))
- (if (bobp) 0)))
+ (if (bobp) (prog-first-column))))
(defun smie-indent-close ()
;; Align close paren with opening paren.
diff --git a/lisp/files.el b/lisp/files.el
index b7d1048..77c1e41 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -2422,7 +2422,7 @@ auto-mode-alist
(lambda (elt)
(cons (purecopy (car elt)) (cdr elt)))
`(;; do this first, so that .html.pl is Polish html, not Perl
- ("\\.[sx]?html?\\(\\.[a-zA-Z_]+\\)?\\'" . html-mode)
+ ("\\.[sx]?html?\\(\\.[a-zA-Z_]+\\)?\\'" . mhtml-mode)
("\\.svgz?\\'" . image-mode)
("\\.svgz?\\'" . xml-mode)
("\\.x[bp]m\\'" . image-mode)
@@ -2784,8 +2784,8 @@ magic-fallback-mode-alist
comment-re "*"
"\\(?:!DOCTYPE[ \t\r\n]+[^>]*>[ \t\r\n]*<[ \t\r\n]*" comment-re "*\\)?"
"[Hh][Tt][Mm][Ll]"))
- . html-mode)
- ("<!DOCTYPE[ \t\r\n]+[Hh][Tt][Mm][Ll]" . html-mode)
+ . mhtml-mode)
+ ("<!DOCTYPE[ \t\r\n]+[Hh][Tt][Mm][Ll]" . mhtml-mode)
;; These two must come after html, because they are more general:
("<\\?xml " . xml-mode)
(,(let* ((incomment-re "\\(?:[^-]\\|-[^-]\\)")
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index b42b2bc..05bf635 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -53,6 +53,7 @@
(require 'moz nil t)
(require 'json nil t)
(require 'sgml-mode)
+(require 'prog-mode)
(eval-when-compile
(require 'cl-lib)
@@ -2109,7 +2110,7 @@ js--proper-indentation
((js--continued-expression-p)
(+ js-indent-level js-expr-indent-offset))
- (t 0))))
+ (t (prog-first-column)))))
;;; JSX Indentation
diff --git a/lisp/textmodes/mhtml-mode.el b/lisp/textmodes/mhtml-mode.el
new file mode 100644
index 0000000..a647060
--- /dev/null
+++ b/lisp/textmodes/mhtml-mode.el
@@ -0,0 +1,274 @@
+;;; 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 'sgml-mode))
+(require 'js)
+(require 'css-mode)
+(require 'prog-mode)
+(require 'font-lock)
+
+(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)
+ (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)
+ "")))
+
+(defun mhtml--submode-fontify-one-region (submode beg end &optional loudly)
+ (if submode
+ (mhtml--with-locals submode
+ (save-restriction
+ (narrow-to-region beg end)
+ (font-lock-set-defaults)
+ (font-lock-default-fontify-region (point-min) (point-max) loudly)))
+ (font-lock-set-defaults)
+ (font-lock-default-fontify-region beg end loudly)))
+
+(defun mhtml--submode-fontify-region (beg end loudly)
+ (while (< beg end)
+ (let ((submode (get-text-property beg 'mhtml-submode))
+ (this-end (next-single-property-change beg 'mhtml-submode
+ nil end)))
+ (mhtml--submode-fontify-one-region submode beg this-end loudly)
+ (setq beg this-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)
+ (goto-char start)
+ (when (get-text-property (point) 'mhtml-submode)
+ (mhtml--syntax-propertize-submode (get-text-property (point) 'mhtml-submode)
+ end))
+ (funcall
+ (syntax-propertize-rules
+ ("<style.*?>"
+ (0 (ignore
+ (goto-char (match-end 0))
+ (mhtml--syntax-propertize-submode mhtml--css-submode end))))
+ ("<script.*?>"
+ (0 (ignore
+ (goto-char (match-end 0))
+ (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 (previous-single-property-change (point)
+ 'mhtml-submode))
+ (base-indent (save-excursion
+ (goto-char region-start)
+ (sgml-calculate-indent))))
+ (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)
+
+ ;; 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))
+
+(put 'mhtml-mode 'flyspell-mode-predicate #'mhtml--flyspell-check-word)
+
+(provide 'mhtml-mode)
+
+;;; mhtml-mode.el ends here
diff --git a/lisp/textmodes/sgml-mode.el b/lisp/textmodes/sgml-mode.el
index e148b06..8ad7cfb 100644
--- a/lisp/textmodes/sgml-mode.el
+++ b/lisp/textmodes/sgml-mode.el
@@ -341,19 +341,23 @@ sgml-font-lock-keywords-2
(defvar sgml-font-lock-keywords sgml-font-lock-keywords-1
"Rules for highlighting SGML code. See also `sgml-tag-face-alist'.")
+(eval-and-compile
+ (defconst sgml-syntax-propertize-rules
+ (syntax-propertize-precompile-rules
+ ;; Use the `b' style of comments to avoid interference with the -- ... --
+ ;; comments recognized when `sgml-specials' includes ?-.
+ ;; FIXME: beware of <!--> blabla <!--> !!
+ ("\\(<\\)!--" (1 "< b"))
+ ("--[ \t\n]*\\(>\\)" (1 "> b"))
+ ;; Double quotes outside of tags should not introduce strings.
+ ;; Be careful to call `syntax-ppss' on a position before the one we're
+ ;; going to change, so as not to need to flush the data we just computed.
+ ("\"" (0 (if (prog1 (zerop (car (syntax-ppss (match-beginning 0))))
+ (goto-char (match-end 0)))
+ (string-to-syntax ".")))))))
+
(defconst sgml-syntax-propertize-function
- (syntax-propertize-rules
- ;; Use the `b' style of comments to avoid interference with the -- ... --
- ;; comments recognized when `sgml-specials' includes ?-.
- ;; FIXME: beware of <!--> blabla <!--> !!
- ("\\(<\\)!--" (1 "< b"))
- ("--[ \t\n]*\\(>\\)" (1 "> b"))
- ;; Double quotes outside of tags should not introduce strings.
- ;; Be careful to call `syntax-ppss' on a position before the one we're
- ;; going to change, so as not to need to flush the data we just computed.
- ("\"" (0 (if (prog1 (zerop (car (syntax-ppss (match-beginning 0))))
- (goto-char (match-end 0)))
- (string-to-syntax ".")))))
+ (syntax-propertize-rules sgml-syntax-propertize-rules)
"Syntactic keywords for `sgml-mode'.")
;; internal
@@ -1284,13 +1288,24 @@ sgml-tag-text-p
(let ((pps (parse-partial-sexp start end 2)))
(and (= (nth 0 pps) 0))))))
+(defun sgml--find-<>-backward (limit)
+ "Search backward for a '<' or '>' character.
+The character must have open or close syntax.
+Returns t if found, nil otherwise."
+ (catch 'found
+ (while (re-search-backward "[<>]" limit 'move)
+ ;; If this character has "open" or "close" syntax, then we've
+ ;; found the one we want.
+ (when (memq (syntax-class (syntax-after (point))) '(4 5))
+ (throw 'found t)))))
+
(defun sgml-parse-tag-backward (&optional limit)
"Parse an SGML tag backward, and return information about the tag.
Assume that parsing starts from within a textual context.
Leave point at the beginning of the tag."
(catch 'found
(let (tag-type tag-start tag-end name)
- (or (re-search-backward "[<>]" limit 'move)
+ (or (sgml--find-<>-backward limit)
(error "No tag found"))
(when (eq (char-after) ?<)
;; Oops!! Looks like we were not in a textual context after all!.
next prev parent reply other threads:[~2017-02-13 2:48 UTC|newest]
Thread overview: 55+ messages / expand[flat|nested] mbox.gz Atom feed top
2017-01-31 20:34 html, css, and js modes working together Tom Tromey
2017-02-01 7:29 ` Clément Pit-Claudel
2017-02-07 4:33 ` Tom Tromey
2017-02-10 2:31 ` Tom Tromey
2017-02-02 14:19 ` Stefan Monnier
[not found] ` <87mvdy7f2g.fsf@tromey.com>
2017-02-07 14:33 ` Stefan Monnier
2017-02-06 3:08 ` Dmitry Gutov
2017-02-06 3:26 ` Tom Tromey
2017-02-06 3:46 ` Dmitry Gutov
2017-02-06 6:50 ` Clément Pit-Claudel
2017-02-06 14:17 ` Stefan Monnier
2017-02-06 20:25 ` Tom Tromey
2017-02-06 20:58 ` Dmitry Gutov
2017-02-06 22:42 ` Stefan Monnier
2017-02-06 23:51 ` Lennart Borgman
2017-02-07 3:40 ` Tom Tromey
2017-02-07 11:28 ` Dmitry Gutov
2017-02-09 23:45 ` Tom Tromey
2017-02-10 20:05 ` Stefan Monnier
2017-02-10 21:15 ` Tom Tromey
2017-02-10 23:45 ` Stefan Monnier
2017-02-11 17:46 ` Tom Tromey
2017-02-11 20:20 ` Stefan Monnier
2017-02-12 16:17 ` Dmitry Gutov
2017-02-12 16:20 ` Dmitry Gutov
2017-02-12 16:52 ` Tom Tromey
2017-02-13 1:12 ` Dmitry Gutov
2017-02-13 1:59 ` Dmitry Gutov
2017-02-13 2:48 ` Tom Tromey [this message]
2017-02-14 1:34 ` Dmitry Gutov
2017-03-19 17:30 ` Tom Tromey
2017-03-21 9:15 ` Dmitry Gutov
2017-03-24 3:18 ` Tom Tromey
2017-03-24 12:02 ` Stefan Monnier
2017-03-24 12:08 ` Toon Claes
2017-03-24 12:59 ` Noam Postavsky
2017-03-24 14:17 ` Tom Tromey
2017-04-05 22:01 ` Tom Tromey
2017-04-06 2:28 ` Leo Liu
2017-04-06 14:12 ` Dmitry Gutov
2017-02-11 17:39 ` Tom Tromey
2017-02-11 19:48 ` Stefan Monnier
2017-02-12 3:49 ` Tom Tromey
2017-02-12 5:26 ` Stefan Monnier
2017-02-12 6:14 ` Tom Tromey
2017-02-12 7:02 ` Stefan Monnier
2017-02-12 7:22 ` Tom Tromey
2017-02-12 11:14 ` martin rudalics
2017-02-12 16:01 ` Eli Zaretskii
2017-02-12 16:32 ` Tom Tromey
2017-02-12 17:14 ` Stefan Monnier
2017-02-12 17:36 ` Tom Tromey
2017-02-12 18:17 ` Eli Zaretskii
2017-02-12 16:59 ` Stefan Monnier
2017-02-11 0:28 ` Please ack Richard Stallman
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/emacs/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=87inoe24d0.fsf@tromey.com \
--to=tom@tromey.com \
--cc=dgutov@yandex.ru \
--cc=emacs-devel@gnu.org \
--cc=monnier@IRO.UMontreal.CA \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).