emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

html, css, and js modes working together


From: Tom Tromey
Subject: html, css, and js modes working together
Date: Tue, 31 Jan 2017 13:34:40 -0700

I tried to send this last night but it bounced for some reason.

This patch changes the html, css, and js modes to work together a bit.
With this, the contents of a <style> element are syntax-highlighted and
indented according to css-mode rules, and the contents of a <script>
element are syntax-highlighted and indented according to js-mode rules.
I'd appreciate comments on this approach.

This required small changes in SMIE and a minor unrelated change to
break the now-cyclic sgml/css-mode dependency.  I chose to break the
sgml/js cyclic dependency in a different way; neither one seems really
great.  Maybe moving html-mode to its own file would help.

* I didn't read SMIE deeply enough to see if the indentation parts
  relied heavily on the forward/backward-a-token rules.  If so then the
  new macro will need some additional work.

* I used the existing (but apparently unused) prog-indentation-context
  variable in this.  I was initially skeptical of using a global
  variable but it did turn out to be pretty handy for SMIE.

* This work doesn't address the need for per-region font locking at all.
  That would be nice to have.  I'd like to hear if there are plans for
  how to do this.

* Likewise there are other things provided by a major mode that aren't
  handled here, for example imenu, add-log-current-defun-function,
  comment-*, electric indent keys, ... probably more that I am not
  thinking of or that css-mode isn't using.  It might also be nice if
  dir-locals.el settings for css-mode and js-mode affected html-mode as
  well.

* Not sure but maybe I also need to define
  syntax-propertize-extend-region-functions now?

Tom

diff --git a/lisp/emacs-lisp/smie.el b/lisp/emacs-lisp/smie.el
index 4d02b75..d793eca 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.
@@ -1838,6 +1840,16 @@ smie-auto-fill
         (funcall do-auto-fill)))))
 
 
+(defmacro with-smie-rules (spec &rest body)
+  "Temporarily set up SMIE indentation and evaluate BODY.
+SPEC is of the form (GRAMMAR RULES-FUNCTION); see `smie-setup'.
+BODY is evaluated with the relevant SMIE variables temporarily bound."
+  (declare (indent 1))
+  `(let ((smie-grammar ,(car spec))
+         (smie-rules-function ,(cadr spec))
+         (indent-line-function #'smie-indent-line))
+     ,@body))
+
 (defun smie-setup (grammar rules-function &rest keywords)
   "Setup SMIE navigation and indentation.
 GRAMMAR is a grammar table generated by `smie-prec2->grammar'.
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index 74dd4ad..f3d90a9 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)
@@ -2102,7 +2103,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/css-mode.el b/lisp/textmodes/css-mode.el
index 19f74da..14bacdc 100644
--- a/lisp/textmodes/css-mode.el
+++ b/lisp/textmodes/css-mode.el
@@ -33,7 +33,6 @@
 ;;; Code:
 
 (require 'seq)
-(require 'sgml-mode)
 (require 'smie)
 (require 'eww)
 
@@ -869,10 +868,6 @@ css--complete-property-value
                 (append '("inherit" "initial" "unset")
                         (css--property-values property))))))))
 
-(defvar css--html-tags (mapcar #'car html-tag-alist)
-  "List of HTML tags.
-Used to provide completion of HTML tags in selectors.")
-
 (defvar css--nested-selectors-allowed nil
   "Non-nil if nested selectors are allowed in the current mode.")
 (make-variable-buffer-local 'css--nested-selectors-allowed)
@@ -900,6 +895,8 @@ css--foreign-completions
         (funcall (symbol-value extractor))))
     (buffer-list))))
 
+(declare-function html-memoized-tag-list "sgml-mode")
+
 (defun css--complete-selector ()
   "Complete part of a CSS selector at point."
   (when (or (= (nth 0 (syntax-ppss)) 0) css--nested-selectors-allowed)
@@ -916,7 +913,7 @@ css--complete-selector
                 (css--foreign-completions 'css-class-list-function))
                ((eq start-char ?#)
                 (css--foreign-completions 'css-id-list-function))
-               (t css--html-tags))))))))))
+               (t (html-memoized-tag-list)))))))))))
 
 (defun css-completion-at-point ()
   "Complete current symbol at point.
diff --git a/lisp/textmodes/sgml-mode.el b/lisp/textmodes/sgml-mode.el
index e148b06..676ebcf 100644
--- a/lisp/textmodes/sgml-mode.el
+++ b/lisp/textmodes/sgml-mode.el
@@ -32,7 +32,9 @@
 
 ;;; Code:
 
+(require 'css-mode)
 (require 'dom)
+(require 'prog-mode)
 (require 'seq)
 (require 'subr-x)
 (eval-when-compile
@@ -341,19 +343,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-when-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
@@ -2024,6 +2030,13 @@ html-tag-alist
       ("wbr" t)))
   "Value of `sgml-tag-alist' for HTML mode.")
 
+(defvar html-tag-list (mapcar #'car html-tag-alist)
+  "List of HTML tags.")
+
+;;;###autoload
+(defun html-memoized-tag-list ()
+  html-tag-list)
+
 (defvar html-tag-help
   `(,@sgml-tag-help
     ("a" . "Anchor of point or link elsewhere")
@@ -2233,6 +2246,68 @@ html-current-buffer-ids
         ids))))
 

+;; js.el is loaded when entering html-mode.
+(defvar js-mode-syntax-table)
+(declare-function js-indent-line "js")
+(declare-function js-syntax-propertize "js")
+
+(defconst html-syntax-propertize-function
+  (syntax-propertize-rules
+   ("<style.*?>\\(\\(\n\\|.\\)*?\\)</style>"
+    (1
+     (prog1 css-mode-syntax-table
+       (let ((start (nth 2 (match-data)))
+             (end (nth 3 (match-data))))
+       (funcall css-syntax-propertize-function start end)
+       (add-text-properties start end '(syntax-multiline t))))))
+   ("<script.*?>\\(\\(\n\\|.\\)*?\\)</script>"
+    (1
+     (prog1 js-mode-syntax-table
+       (let ((start (nth 2 (match-data)))
+             (end (nth 3 (match-data))))
+         (js-syntax-propertize start end)
+         (add-text-properties start end '(syntax-multiline t))))))
+   sgml-syntax-propertize-rules)
+  "Syntactic keywords for `html-mode'.")
+
+(defun html-indent-line ()
+  "Indent the current line as HTML."
+  (interactive)
+  (let* ((context (save-excursion (car (last (sgml-get-context)))))
+         (tag (when (and context
+                         (eq (sgml-tag-type context) 'open)
+                         (> (point) (sgml-tag-start context)))
+                (sgml-tag-name context))))
+    (cond
+     ((equal tag "style")
+      ;; CSS.
+      (save-restriction
+        (let ((base-indent (save-excursion
+                             (goto-char (sgml-tag-end context))
+                             (sgml-calculate-indent))))
+          (narrow-to-region (sgml-tag-end context) (point-max))
+          (let ((prog-indentation-context (list base-indent
+                                                (cons (point-min) nil)
+                                                nil)))
+            (with-smie-rules (css-smie-grammar #'css-smie-rules)
+              (smie-indent-line))))))
+     ((equal tag "script")
+      ;; Javascript.
+      (save-restriction
+        (let ((base-indent (save-excursion
+                             (goto-char (sgml-tag-end context))
+                             (sgml-calculate-indent))))
+          (narrow-to-region (sgml-tag-end context) (point-max))
+          (let ((prog-indentation-context (list base-indent
+                                                (cons (point-min) nil)
+                                                nil)))
+            (js-indent-line)))))
+     (t
+      ;; HTML.
+      (sgml-indent-line)))))
+
+
+
 ;;;###autoload
 (define-derived-mode html-mode sgml-mode '(sgml-xml-mode "XHTML" "HTML")
   "Major mode based on SGML mode for editing HTML documents.
@@ -2270,6 +2345,8 @@ html-mode
    (eval-after-load \"sgml-mode\" \\='(aset sgml-char-names ?\\=' nil))
 
 \\{html-mode-map}"
+  ;; A hack to avoid a circular dependency.
+  (require 'js)
   (setq-local sgml-display-text html-display-text)
   (setq-local sgml-tag-face-alist html-tag-face-alist)
   (setq-local sgml-tag-alist html-tag-alist)
@@ -2281,6 +2358,11 @@ html-mode
              (lambda () (char-before (match-end 0))))
   (setq-local add-log-current-defun-function #'html-current-defun-name)
   (setq-local sentence-end-base "[.?!][]\"'”)}]*\\(<[^>]*>\\)*")
+  (setq-local indent-line-function #'html-indent-line)
+
+  (setq-local syntax-propertize-function html-syntax-propertize-function)
+  (add-hook 'syntax-propertize-extend-region-functions
+            #'syntax-propertize-multiline 'append 'local)
 
   (when (fboundp 'libxml-parse-html-region)
     (defvar css-class-list-function)



reply via email to

[Prev in Thread] Current Thread [Next in Thread]