Subject: Re: bug#74610: 31.0.50; Submitting mhtml-ts-mode, treesitter alternative to mhtml-mode

From: Vincenzo Pupillo <[email-masked]>
Date: Sun, 19 Jan 2025 18:09:31 +0100

Ciao Yuan,
In data sabato 18 gennaio 2025 02:37:53 Ora standard dell’Europa centrale, 
Yuan Fu ha scritto:
> > On Jan 14, 2025, at 1:41 PM, Vincenzo Pupillo <[email-masked]> wrote:
> > 
> > Ciao Yuan and Juri,
> > this is an updated version of mhtml-ts-mode.
> > I have tried to reduce as much as possible copies of parts of the major
> > modes from which it is derived.
> > To do this, I had to move some values that were assigned directly to
> > treesit's own variables  (in ccs-mode.el, in js.el, and in
> > html-ts-mode.el) into new variables.
> > I also added three new functions to treesit.el to make it easier to
> > combine
> > parts derived from the other major modes. So now any changes to these new
> > variables are directly reflected in the behavior of mhtml-ts-mode.
> > There are a few things I would like to highlight:
> > 1. treesit-font-lock-feature-lists are not defined per parser, so simply
> > merging the different lists will cause display differences from the
> > original major-modes; for example “function” is defined at level 3 in
> > css-ts-mode but at level 4 in js-ts-mode.
> 
> Yeah, that’s not pretty, I don’t have a good solution for now. Technically a
> major mode can use treesit-font-lock-recompute-features after
> treesit-major-mode-setup to do fine adjustments, but it’s ugly.
> > 2. treesit-defun-type-regexp has the same problem as treesit-font-lock-
> > feature-list, so I had to define it myself.
> 
> Maybe you can use treesit-thing-settings instead?
Ok, done.

> 
> > But other than that, it works pretty well.
> > 
> > IMHO, a global "list" where you can define "font-lock", "indent-list",
> > "font- lock-feature", etc. by language (perhaps with getter and setter
> > methods) might make it easier to define new multilingual major-modes. It
> > could improve the decoupling between multilingual major-modes and the
> > major-modes they are derived from. It could also better decouple the
> > internal implementation of treesit.el from the treesitter-based
> > major-modes.
> > 
> > Let me know what you think.
> 
> That’s what I had in mind. That’s also why I added a new variable
> treesit-aggregated-simple-imenu-settings instead of piggybacking on
> treesit-simple-imenu-settings. A separate variable is simpler to borrow
> settings from.
Yes, in this new patch I used treesit-aggregated-simple-imenu-settings.

> But I didn’t have time to think it though yet. (By language, or by mode and
> language? Should packages set it on load, or define a function that loads
> these settings? Etc.)
>
I would say by mode and language, both mhtml-ts mode and html-ts mode have the 
same primary parser. For the second question, I don't know. I don't have 
enough experience with it to know what the pros and cons are.

> Yuan

Vincenzo
From 199af27f1d761d100f6a8a6163c27e35e6ab2ea0 Mon Sep 17 00:00:00 2001
From: Vincenzo Pupillo <[email-masked]>
Date: Sun, 19 Jan 2025 17:49:30 +0100
Subject: [PATCH] Add mhtml-ts-mode.

New major-mode alternative to mhtml-mode, based on treesitter, for
editing files containing html, javascript and css.

* etc/NEWS: Mention the new mode and new functions.
* lisp/textmodes/mhtml-ts-mode.el: New file.
* lisp/progmodes/js.el (js--treesit-thing-settings): New variable.
(js--treesit-font-lock-feature-list); New variable.
(js--treesit-simple-imenu-settings): New variable.
(js--treesit-defun-type-regexp): New variable.
(js--treesit-jsdoc-comment-regexp): New variable.
(js-ts-mode): Use of new variables instead of direct assignment of
values.
* lisp/textmodes/css-mode.el (css-mode--menu): New variable.
(css-mode-map): Use new variable.
(css--treesit-font-lock-feature-list): New variable.
(css--treesit-simple-imenu-settings): New variable.
(css--treesit-defun-type-regexp): New variable.
(cs-ts-mode): Use of new variables instead of direct assignment of
values.
* lisp/textmodes/html-ts-mode.el
(html-ts-mode--treesit-things-settings): New variable.
(html-ts-mode--treesit-font-lock-feature-list): New variable.
(html-ts-mode--treesit-simple-imenu-settings): New variable.
(html-ts-mode--treesit-defun-type-regexp): New variable.
(html-ts-mode): Use of new variables instead of direct assignment of
values.
* lisp/treesit.el
(treesit-merge-font-lock-feature-list): New fuction.
(treesit-replace-font-lock-feature-settings): New fuction.
(treesit-modify-indent-rules): New function.
---
 etc/NEWS                        |  28 ++
 lisp/progmodes/js.el            |  72 ++--
 lisp/textmodes/css-mode.el      |  47 ++-
 lisp/textmodes/html-ts-mode.el  |  45 ++-
 lisp/textmodes/mhtml-ts-mode.el | 600 ++++++++++++++++++++++++++++++++
 lisp/treesit.el                 |  65 ++++
 6 files changed, 798 insertions(+), 59 deletions(-)
 create mode 100644 lisp/textmodes/mhtml-ts-mode.el

diff --git a/etc/NEWS b/etc/NEWS
index 0b849dec450..6360a082e9f 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -965,6 +965,12 @@ destination window is chosen using 'display-buffer-alist'.  Example:
 
 
 * New Modes and Packages in Emacs 31.1
+** New major modes based on the tree-sitter library
+
+*** New major mode 'mhtml-ts-mode'.
+An optional major mode based on the tree-sitter library for editing html
+files. This mode handles indentation, fontification, and commenting for
+embedded JavaScript and CSS.
 
 
 * Incompatible Lisp Changes in Emacs 31.1
@@ -1098,6 +1104,28 @@ language symbol.  For example, 'cpp' is translated to "C++".  A new
 variable 'treesit-language-display-name-alist' holds the translations of
 language symbols where that translation is not trivial.
 
++++
+*** New function 'treesit-merge-font-lock-feature-list'.
+This function the merge two tree-sitter font lock feature lists.
+Returns a new font lock feature list with no duplicates in the same level.
+It can be used to merge font lock feature lists in a multi-language major mode.
+
++++
+*** New function 'treesit-replace-font-lock-feature-settings'.
+Given two treesit-font-lock-settings replaces the feature in the second
+font-lock-settings with the same feature in the first
+font-lock-settings. In a multi-linguage major mode it is sometimes
+necessary to replace features from one of the major modes, with others
+that are better suited to the new multilingual context.
+
++++
+*** New function 'treesit-modify-indent-rules'.
+Given two treesit ident rules, it replaces, adds, or prepends the new
+rules to the old ones, then returns a new treesit indent rules.
+In a multi-linguage major mode it is sometimes necessary to modify rules
+from one of the major modes, with others that are better suited to the
+new multilingual context.
+
 +++
 *** New command 'treesit-explore'
 This command replaces 'treesit-explore-mode'.  It turns on
diff --git a/lisp/progmodes/js.el b/lisp/progmodes/js.el
index 101b882c718..be9d6f64aef 100644
--- a/lisp/progmodes/js.el
+++ b/lisp/progmodes/js.el
@@ -3901,6 +3901,44 @@ js--treesit-list-nodes
 (defvar js--treesit-jsdoc-beginning-regexp (rx bos "/**")
   "Regular expression matching the beginning of a jsdoc block comment.")
 
+(defvar js--treesit-thing-settings
+  `((javascript
+     (sexp ,(js--regexp-opt-symbol js--treesit-sexp-nodes))
+     (list ,(js--regexp-opt-symbol js--treesit-list-nodes))
+     (sentence ,(js--regexp-opt-symbol js--treesit-sentence-nodes))
+     (text ,(js--regexp-opt-symbol '("comment"
+                                     "string_fragment")))))
+  "Settings for `treesit-thing-settings'.")
+
+(defvar js--treesit-font-lock-feature-list
+  '(( comment document definition)
+    ( keyword string)
+    ( assignment constant escape-sequence jsx number
+      pattern string-interpolation)
+    ( bracket delimiter function operator property))
+  "Settings for `treesit-font-lock-feature-list'.")
+
+(defvar js--treesit-simple-imenu-settings
+  `(("Function" "\\`function_declaration\\'" nil nil)
+    ("Variable" "\\`lexical_declaration\\'"
+     js--treesit-valid-imenu-entry nil)
+    ("Class" ,(rx bos (or "class_declaration"
+                          "method_definition")
+                  eos)
+     nil nil))
+  "Settings for `treesit-simple-imenu'.")
+
+(defvar js--treesit-defun-type-regexp
+  (rx (or "class_declaration"
+          "method_definition"
+          "function_declaration"
+          "lexical_declaration"))
+  "Settings for `treesit-defun-type-regexp'.")
+
+(defvar js--treesit-jsdoc-comment-regexp
+  (rx (or "comment" "line_comment" "block_comment" "description"))
+  "Regexp for `c-ts-common--comment-regexp'.")
+
 ;;;###autoload
 (define-derived-mode js-ts-mode js-base-mode "JavaScript"
   "Major mode for editing JavaScript.
@@ -3931,29 +3969,15 @@ js-ts-mode
     ;; Indent.
     (setq-local treesit-simple-indent-rules js--treesit-indent-rules)
     ;; Navigation.
-    (setq-local treesit-defun-type-regexp
-                (rx (or "class_declaration"
-                        "method_definition"
-                        "function_declaration"
-                        "lexical_declaration")))
+    (setq-local treesit-defun-type-regexp js--treesit-defun-type-regexp)
+
     (setq-local treesit-defun-name-function #'js--treesit-defun-name)
 
-    (setq-local treesit-thing-settings
-                `((javascript
-                   (sexp ,(js--regexp-opt-symbol js--treesit-sexp-nodes))
-                   (list ,(js--regexp-opt-symbol js--treesit-list-nodes))
-                   (sentence ,(js--regexp-opt-symbol js--treesit-sentence-nodes))
-                   (text ,(js--regexp-opt-symbol '("comment"
-                                                   "string_fragment"))))))
+    (setq-local treesit-thing-settings js--treesit-thing-settings)
 
     ;; Fontification.
     (setq-local treesit-font-lock-settings js--treesit-font-lock-settings)
-    (setq-local treesit-font-lock-feature-list
-                '(( comment document definition)
-                  ( keyword string)
-                  ( assignment constant escape-sequence jsx number
-                    pattern string-interpolation)
-                  ( bracket delimiter function operator property)))
+    (setq-local treesit-font-lock-feature-list js--treesit-font-lock-feature-list)
 
     (when (treesit-ready-p 'jsdoc t)
       (setq-local treesit-range-settings
@@ -3963,17 +3987,11 @@ js-ts-mode
                    :local t
                    `(((comment) @capture (:match ,js--treesit-jsdoc-beginning-regexp @capture)))))
 
-      (setq c-ts-common--comment-regexp (rx (or "comment" "line_comment" "block_comment" "description"))))
+      (setq c-ts-common--comment-regexp js--treesit-jsdoc-comment-regexp))
 
     ;; Imenu
-    (setq-local treesit-simple-imenu-settings
-                `(("Function" "\\`function_declaration\\'" nil nil)
-                  ("Variable" "\\`lexical_declaration\\'"
-                   js--treesit-valid-imenu-entry nil)
-                  ("Class" ,(rx bos (or "class_declaration"
-                                        "method_definition")
-                                eos)
-                   nil nil)))
+    (setq-local treesit-simple-imenu-settings js--treesit-simple-imenu-settings)
+
     (treesit-major-mode-setup)
 
     (add-to-list 'auto-mode-alist
diff --git a/lisp/textmodes/css-mode.el b/lisp/textmodes/css-mode.el
index 53340195386..35c61e4f66d 100644
--- a/lisp/textmodes/css-mode.el
+++ b/lisp/textmodes/css-mode.el
@@ -893,13 +893,7 @@ css-mode-syntax-table
     (modify-syntax-entry ?? "." st)
     st))
 
-(defvar-keymap css-mode-map
-  :doc "Keymap used in `css-mode'."
-  "<remap> <info-lookup-symbol>" #'css-lookup-symbol
-  ;; `info-complete-symbol' is not used.
-  "<remap> <complete-symbol>" #'completion-at-point
-  "C-c C-f" #'css-cycle-color-format
-  :menu
+(defvar css-mode--menu
   '("CSS"
     :help "CSS-specific features"
     ["Reformat block" fill-paragraph
@@ -910,7 +904,17 @@ css-mode-map
     ["Describe symbol" css-lookup-symbol
      :help "Display documentation for a CSS symbol"]
     ["Complete symbol" completion-at-point
-     :help "Complete symbol before point"]))
+     :help "Complete symbol before point"])
+    "Menu bar for `css-mode'")
+
+(defvar-keymap css-mode-map
+  :doc "Keymap used in `css-mode'."
+  "<remap> <info-lookup-symbol>" #'css-lookup-symbol
+  ;; `info-complete-symbol' is not used.
+  "<remap> <complete-symbol>" #'completion-at-point
+  "C-c C-f" #'css-cycle-color-format
+  :menu
+  css-mode--menu)
 
 (eval-and-compile
   (defconst css--uri-re
@@ -1771,6 +1775,21 @@ css--extract-index-name
               (replace-regexp-in-string "[\n ]+" " " s)))
            res)))))))
 
+(defvar css--treesit-font-lock-feature-list
+  '((selector comment query keyword)
+    (property constant string)
+    (error variable function operator bracket))
+  "Settings for `treesit-font-lock-feature-list'.")
+
+(defvar css--treesit-simple-imenu-settings
+  `(( nil ,(rx bos (or "rule_set" "media_statement") eos)
+      nil nil))
+  "Settings for `treesit-simple-imenu'.")
+
+(defvar css--treesit-defun-type-regexp
+  "rule_set"
+  "Settings for `treesit-defun-type-regexp'.")
+
 (define-derived-mode css-base-mode prog-mode "CSS"
   "Generic mode to edit Cascading Style Sheets (CSS).
 
@@ -1825,16 +1844,12 @@ css-ts-mode
     ;; Tree-sitter specific setup.
     (setq treesit-primary-parser (treesit-parser-create 'css))
     (setq-local treesit-simple-indent-rules css--treesit-indent-rules)
-    (setq-local treesit-defun-type-regexp "rule_set")
+    (setq-local treesit-defun-type-regexp css--treesit-defun-type-regexp)
     (setq-local treesit-defun-name-function #'css--treesit-defun-name)
     (setq-local treesit-font-lock-settings css--treesit-settings)
-    (setq-local treesit-font-lock-feature-list
-                '((selector comment query keyword)
-                  (property constant string)
-                  (error variable function operator bracket)))
-    (setq-local treesit-simple-imenu-settings
-                `(( nil ,(rx bos (or "rule_set" "media_statement") eos)
-                    nil nil)))
+    (setq-local treesit-font-lock-feature-list css--treesit-font-lock-feature-list)
+    (setq-local treesit-simple-imenu-settings css--treesit-simple-imenu-settings)
+
     (treesit-major-mode-setup)
 
     (add-to-list 'auto-mode-alist '("\\.css\\'" . css-ts-mode))))
diff --git a/lisp/textmodes/html-ts-mode.el b/lisp/textmodes/html-ts-mode.el
index dad49b7ed4c..5700c2b406b 100644
--- a/lisp/textmodes/html-ts-mode.el
+++ b/lisp/textmodes/html-ts-mode.el
@@ -87,6 +87,31 @@ html-ts-mode--font-lock-settings
    `((attribute_name) @font-lock-variable-name-face))
   "Tree-sitter font-lock settings for `html-ts-mode'.")
 
+(defvar html-ts-mode--treesit-things-settings
+  `((html
+     (sexp ,(regexp-opt '("element"
+                          "text"
+                          "attribute"
+                          "value")))
+     (list ,(regexp-opt '("element")) 'symbols)
+     (sentence "tag")
+     (text ,(regexp-opt '("comment" "text")))))
+  "Settings for `treesit-thing-settings'.")
+
+(defvar html-ts-mode--treesit-font-lock-feature-list
+  '((comment keyword definition)
+    (property string)
+    () ())
+  "Settings for `treesit-font-lock-feature-list'.")
+
+(defvar html-ts-mode--treesit-simple-imenu-settings
+  '(("Element" "\\`tag_name\\'" nil nil))
+  "Settings for `treesit-simple-imenu'.")
+
+(defvar html-ts-mode--treesit-defun-type-regexp
+  "element"
+  "Settings for `treesit-defun-type-regexp'.")
+
 (defun html-ts-mode--defun-name (node)
   "Return the defun name of NODE.
 Return nil if there is no name or if NODE is not a defun node."
@@ -107,30 +132,18 @@ html-ts-mode
   (setq-local treesit-simple-indent-rules html-ts-mode--indent-rules)
 
   ;; Navigation.
-  (setq-local treesit-defun-type-regexp "element")
+  (setq-local treesit-defun-type-regexp html-ts-mode--treesit-defun-type-regexp)
 
   (setq-local treesit-defun-name-function #'html-ts-mode--defun-name)
 
-  (setq-local treesit-thing-settings
-              `((html
-                 (sexp ,(regexp-opt '("element"
-                                      "text"
-                                      "attribute"
-                                      "value")))
-                 (list ,(regexp-opt '("element")) 'symbols)
-                 (sentence "tag")
-                 (text ,(regexp-opt '("comment" "text"))))))
+  (setq-local treesit-thing-settings html-ts-mode--treesit-things-settings)
 
   ;; Font-lock.
   (setq-local treesit-font-lock-settings html-ts-mode--font-lock-settings)
-  (setq-local treesit-font-lock-feature-list
-              '((comment keyword definition)
-                (property string)
-                () ()))
+  (setq-local treesit-font-lock-feature-list html-ts-mode--treesit-font-lock-feature-list)
 
   ;; Imenu.
-  (setq-local treesit-simple-imenu-settings
-              '(("Element" "\\`tag_name\\'" nil nil)))
+  (setq-local treesit-simple-imenu-settings html-ts-mode--treesit-simple-imenu-settings)
 
   ;; Outline minor mode.
   (setq-local treesit-outline-predicate "\\`element\\'")
diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el
new file mode 100644
index 00000000000..272d00a1ef6
--- /dev/null
+++ b/lisp/textmodes/mhtml-ts-mode.el
@@ -0,0 +1,600 @@
+;;; mhtml-ts-mode.el --- Major mode for HTML using tree-sitter -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2024 Free Software Foundation, Inc.
+
+;; Author: Vincenzo Pupillo <[email-masked]>
+;; Maintainer: Vincenzo Pupillo <[email-masked]>
+;; Created: Nov 2024
+;; Keywords: HTML languages hypermedia tree-sitter
+
+;; 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 <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; This package provides `mhtml-ts-mode' which is a major mode
+;; for editing HTML files with embedded JavaScript and CSS.
+;; Tree Sitter is used to parse each of these languages.
+;;
+;; Please note that this package requires `html-ts-mode', which
+;; registers itself as the major mode for editing HTML.
+;;
+;; This package is compatible and has been tested with the following
+;; tree-sitter grammars:
+;; * https://github.com/tree-sitter/tree-sitter-html
+;; * https://github.com/tree-sitter/tree-sitter-javascript
+;; * https://github.com/tree-sitter/tree-sitter-jsdoc
+;; * https://github.com/tree-sitter/tree-sitter-css
+;;
+;; Features
+;;
+;; * Indent
+;; * Flymake
+;; * IMenu
+;; * Navigation
+;; * Which-function
+;; * Tree-sitter parser installation helper
+
+;;; Code:
+
+(require 'treesit)
+(require 'html-ts-mode)
+(require 'css-mode) ;; for embed css into html
+(require 'js) ;; for embed javascript into html
+
+(eval-when-compile
+  (require 'rx))
+
+;; This tells the byte-compiler where the functions are defined.
+;; Is only needed when a file needs to be able to byte-compile
+;; in a Emacs not built with tree-sitter library.
+(treesit-declare-unavailable-functions)
+
+;; In a multi-language major mode can be useful to have an "installer" to
+;; simplify the installation of the grammars supported by the major-mode.
+(defvar mhtml-ts-mode--language-source-alist
+  '((html . ("https://github.com/tree-sitter/tree-sitter-html"  "v0.23.2"))
+    (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.23.1"))
+    (jsdoc . ("https://github.com/tree-sitter/tree-sitter-jsdoc" "v0.23.2"))
+    (css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.23.1")))
+  "Treesitter language parsers required by `mhtml-ts-mode'.
+You can customize this variable if you want to stick to a specific
+commit and/or use different parsers.")
+
+(defun mhtml-ts-mode-install-parsers ()
+  "Install all the required treesitter parsers.
+`mhtml-ts-mode--language-source-alist' defines which parsers to install."
+  (interactive)
+  (let ((treesit-language-source-alist mhtml-ts-mode--language-source-alist))
+    (dolist (item mhtml-ts-mode--language-source-alist)
+      (treesit-install-language-grammar (car item)))))
+
+;;; Custom variables
+
+(defgroup mhtml-ts-mode nil
+  "Major mode for editing HTML files, based on `html-ts-mode'.
+Works with JS and CSS and for that use `js-ts-mode' and `css-ts-mode'."
+  :prefix "mhtml-ts-mode-"
+  ;; :group 'languages
+  :group 'html)
+
+(defcustom mhtml-ts-mode-js-css-indent-offset 2
+  "JavaScript and CSS indent spaces related to the <script> and <style> HTML tags.
+By default should have same value as `html-ts-mode-indent-offset'."
+  :tag "HTML javascript or css indent offset"
+  :version "31.1"
+  :type 'integer
+  :safe 'integerp)
+
+(defcustom mhtml-ts-mode-pretty-print-command
+  ;; prefer tidy because it's used by sgml-mode
+  (let ((executable nil))
+    (cond ((setq executable (executable-find "tidy"))
+           (format
+            "%s --gnu-emacs yes --wrap 0 --indent-spaces %s -q -i -"
+            executable html-ts-mode-indent-offset))
+          ((setq executable (executable-find "xmllint"))
+           (format "%s --html --quiet --format -" executable))
+          (t "Install tidy, ore some other HTML pretty print tool, and set `mhtml-ts-mode-pretty-print-command'.")))
+  "The command to pretty print the current HTML buffer."
+  :type 'string
+  :version "31.1")
+
+(defvar mhtml-ts-mode--js-css-indent-offset
+  mhtml-ts-mode-js-css-indent-offset
+  "Internal copy of `mhtml-ts-mode-js-css-indent-offset'.
+The value changes, by `mhtml-ts-mode--tag-relative-indent-offset' according to
+the value of `mhtml-ts-mode-tag-relative-indent'.")
+
+(defun mhtml-ts-mode--tag-relative-indent-offset (sym val)
+  "Custom setter for `mhtml-ts-mode-tag-relative-indent'.
+Apart from setting the default value of SYM to VAL, also change the
+value of SYM in `mhtml-ts-mode' buffers to VAL.  SYM should be
+`mhtml-ts-mode-tag-relative-indent', and VAL should be t, nil or
+`ignore'.  When sym is `mhtml-ts-mode-tag-relative-indent' set the
+value of `mhtml-ts-mode--js-css-indent-offset' to 0 if VAL is t,
+otherwise to `mhtml-ts-mode-js-css-indent-offset'."
+  (set-default sym val)
+  (when (eq sym 'mhtml-ts-mode-tag-relative-indent)
+    (setq
+     mhtml-ts-mode--js-css-indent-offset
+     (if (eq val t)
+         mhtml-ts-mode-js-css-indent-offset
+       0))))
+
+(defcustom mhtml-ts-mode-tag-relative-indent t
+  "How <script> and <style> bodies are indented relative to the tag.
+
+When t, indentation looks like:
+
+  <script>
+    code();
+  </script>
+
+When nil, indentation of the tag body starts just below the
+tag, like:
+
+  <script>
+  code();
+  </script>
+
+When `ignore', the tag body starts in the first column, like:
+
+  <script>
+code();
+  </script>"
+  :type '(choice (const nil) (const t) (const ignore))
+  :safe 'symbolp
+  :set #'mhtml-ts-mode--tag-relative-indent-offset
+  :version "31.1")
+
+(defcustom mhtml-ts-mode-css-fontify-colors t
+  "Whether CSS colors should be fontified using the color as the background.
+If non-nil, text representing a CSS color will be fontified
+such that its background is the color itself.
+Works like `css--fontify-region'."
+  :tag "HTML colors the CSS properties values."
+  :version "31.1"
+  :type 'boolean
+  :safe 'booleanp)
+
+(defvar mhtml-ts-mode-saved-pretty-print-command nil
+  "The command last used to pretty print in this buffer.")
+
+(defun mhtml-ts-mode-pretty-print (command)
+  "Prettify the current buffer.
+Argument COMMAND The command to use."
+  (interactive
+   (list (read-string
+          "Prettify command: "
+          (or mhtml-ts-mode-saved-pretty-print-command
+              (concat mhtml-ts-mode-pretty-print-command " ")))))
+  (setq mhtml-ts-mode-saved-pretty-print-command command)
+  (save-excursion
+    (shell-command-on-region
+     (point-min) (point-max)
+     command (buffer-name) t
+     "*mhtml-ts-mode-pretty-pretty-print-errors*" t)))
+
+(defun mhtml-ts-mode--switch-fill-defun (&rest arguments)
+  "Switch between `fill-paragraph' and `prog-fill-reindent-defun'.
+In an HTML region it calls `fill-paragraph' as does `html-ts-mode',
+otherwise it calls `prog-fill-reindent-defun'.
+Optional ARGUMENTS to to be passed to it."
+  (interactive)
+  (if (eq (treesit-language-at (point)) 'html)
+      (funcall-interactively #'fill-paragraph arguments)
+    (funcall-interactively #'prog-fill-reindent-defun arguments)))
+
+(defvar-keymap mhtml-ts-mode-map
+  :doc "Keymap for `mhtml-ts-mode' buffers."
+  :parent html-mode-map
+  ;; `mhtml-ts-mode' derive from `html-ts-mode' so the keymap is the
+  ;; same, we need to add some mapping from others languages.
+  "C-c C-f" #'css-cycle-color-format
+  "M-q" #'mhtml-ts-mode--switch-fill-defun)
+
+;; Place the CSS menu in the menu bar as well.
+(easy-menu-define mhtml-ts-mode-menu mhtml-ts-mode-map
+  "Menu bar for `mhtml-ts-mode'."
+  css-mode--menu)
+
+;; Not used at the moment.
+(defun mthml-ts-mode--js-language-at-point (point)
+  "Return the language at POINT assuming the point is within a Javascript region."
+  (let* ((node (treesit-node-at point 'javascript))
+         (node-type (treesit-node-type node))
+         (node-start (treesit-node-start node))
+         (node-end (treesit-node-end node)))
+    (if (not (treesit-ready-p 'jsdoc t))
+        'javascript
+      (if (equal node-type "comment")
+          (save-excursion
+            ;; (message "node start = %s , end = %s" node-start node-end)
+            (goto-char node-start)
+            (if (search-forward "/**" node-end t)
+                'jsdoc
+              'javascript))
+        'javascript))))
+
+;; To enable some basic treesiter functionality, you should define
+;; a function that recognizes which grammar is used at-point.
+;; This function should be assigned to `treesit-language-at-point-function'
+(defun mhtml-ts-mode--language-at-point (point)
+  "Return the language at POINT assuming the point is within a HTML buffer."
+  (let* ((node (treesit-node-at point 'html))
+         (parent (treesit-node-parent node))
+         (node-query (format "(%s (%s))"
+                             (treesit-node-type parent)
+                             (treesit-node-type node))))
+    (cond
+     ((equal "(script_element (raw_text))" node-query) 'javascript)
+     ;; ((equal "(script_element (raw_text))" node-query) 'mthml-ts-mode--js-language-at-point)
+     ((equal "(style_element (raw_text))" node-query) 'css)
+     (t 'html))))
+
+;; Custom font-lock function that's used to apply color to css color
+;; The signature of the function should be conforming to signature
+;; QUERY-SPEC required by `treesit-font-lock-rules'.
+(defun mhtml-ts-mode--colorize-css-value (node override start end &rest _)
+  "Colorize CSS property value like `css--fontify-region'.
+For NODE, OVERRIDE, START, and END, see `treesit-font-lock-rules'."
+  (if (and mhtml-ts-mode-css-fontify-colors
+           (string-equal "plain_value" (treesit-node-type node)))
+      (let ((color (css--compute-color start (treesit-node-text node t))))
+        (when color
+          (with-silent-modifications
+            (add-text-properties
+             (treesit-node-start node) (treesit-node-end node)
+             (list 'face (list :background color
+                               :foreground (readable-foreground-color
+                                            color)
+                               :box '(:line-width -1)))))))
+    (treesit-fontify-with-override
+     (treesit-node-start node) (treesit-node-end node)
+     'font-lock-variable-name-face
+     override start end)))
+
+;; Embedded languages ​​should be indented according to the language
+;; that embeds them.
+;; This function signature complies with `treesit-simple-indent-rules'
+;; ANCHOR.
+(defun mhtml-ts-mode--js-css-tag-bol (_node _parent &rest _)
+  "Find the first non-space characters of html tags <script> or <style>.
+Return `line-beginning-position' when `treesit-node-at' is html, or
+`mhtml-ts-mode-tag-relative-indent' is equal to ignore.
+NODE and PARENT are ignored."
+  (if (or (eq (treesit-language-at (point)) 'html)
+          (eq mhtml-ts-mode-tag-relative-indent 'ignore))
+      (line-beginning-position)
+    ;; Ok, we are in js or css block.
+    (save-excursion
+      (re-search-backward "<script.*>\\|<style.*>" nil t))))
+
+;; Treesit supports 4 level of decoration, `treesit-font-lock-level'
+;; define which level to use.  Major modes categorize their fontification
+;; features, these categories are defined by `treesit-font-lock-rules' of
+;; each major-mode using :feature keyword.
+;; In a multiple language Major mode it's a good idea to provide, for each
+;; level, the union of the :feature of the same level.
+;; TODO: Since the feature-list is not defined per "parser" (like, for
+;; example, the thing-settings), the same feature can appear in
+;; different levels, so the appearance of a multiple main mode can be
+;; different from the main mode used.  For e.g the feature "function" is
+;; at level 4 for Javascript while it is at level 3 for CSS.
+(defvar mhtml-ts-mode--treesit-font-lock-feature-list
+  (treesit-merge-font-lock-feature-list
+   html-ts-mode--treesit-font-lock-feature-list
+   (treesit-merge-font-lock-feature-list
+    js--treesit-font-lock-feature-list
+    css--treesit-font-lock-feature-list))
+  "Settings for `treesit-font-lock-feature-list'.")
+
+(defvar mhtml-ts-mode--treesit-font-lock-settings
+  (append html-ts-mode--font-lock-settings
+          js--treesit-font-lock-settings
+          ;; Let's replace a css rule with a new one that adds color to
+          ;; the css value.
+          (treesit-replace-font-lock-feature-settings
+           (treesit-font-lock-rules
+            :language 'css
+            :override t
+            :feature 'variable
+            '((plain_value) @font-lock-variable-name-face
+              (plain_value) @mhtml-ts-mode--colorize-css-value))
+           css--treesit-settings))
+  "Settings for `treesit-font-lock-settings'.")
+
+(defvar mhtml-ts-mode--treesit-thing-settings
+  ;; In addition to putting together the various definitions, we need to
+  ;; add 'defun' which is used to support `imenu' and 'which-function'.
+  (list
+   ;; HTML thing settings
+   (append
+    (car html-ts-mode--treesit-things-settings)
+    `((defun ,(regexp-opt (list html-ts-mode--treesit-defun-type-regexp)))))
+   ;; Javascript thing settings
+   (append
+    (car js--treesit-thing-settings)
+    `((defun ,js--treesit-defun-type-regexp)))
+   ;; CSS thing settings
+   `(css
+     (defun ,(regexp-opt (list css--treesit-defun-type-regexp)))))
+  "Settings for `treesit-thing-settings'.")
+
+(defvar mhtml-ts-mode--treesit-indent-rules
+  (treesit--indent-rules-optimize
+   (append html-ts-mode--indent-rules
+           ;; Extended rules for js and css, to
+           ;; indent appropriately when injected
+           ;; into html
+           (treesit-modify-indent-rules
+            `((javascript ((parent-is "program")
+                           mhtml-ts-mode--js-css-tag-bol
+                           mhtml-ts-mode--js-css-indent-offset)))
+            js--treesit-indent-rules
+            :replace)
+           (treesit-modify-indent-rules
+            `((css ((parent-is "stylesheet")
+                    mhtml-ts-mode--js-css-tag-bol
+                    mhtml-ts-mode--js-css-indent-offset)))
+            css--treesit-indent-rules 'prepend)
+           :replace))
+  "Settings for `treesit-simple-indent-rules'.")
+
+(defvar mhtml-ts-mode--treesit-aggregated-simple-imenu-settings
+  `((html ,@html-ts-mode--treesit-simple-imenu-settings)
+    (javascript ,@js--treesit-simple-imenu-settings)
+    (css ,@css--treesit-simple-imenu-settings))
+  "Settings for `treesit-simple-imenu'.")
+
+;; TODO: treesit-defun-type-regexp should have an aggregated version,
+;; like treesit-aggregated-simple-imenu-settings. Otherwise we can't
+;; reuse the regex defined in the major mode we use.
+(defvar mhtml-ts-mode--treesit-defun-type-regexp
+  (regexp-opt '("class_declaration"
+                "method_definition"
+                "function_declaration"
+                "lexical_declaration"
+                "element"
+                "rule_set"))
+  "Settings for `treesit-defun-type-regexp'.")
+
+;; In order to support `prettify-symbols-mode', just `append' the prettify
+;; alist of all the languages. In our case only javascript defined this alist.
+(defvar mhtml-ts-mode--prettify-symbols-alist js--prettify-symbols-alist
+  "Alist of symbol prettifications for various supported languages.")
+
+;; In order to support `which-fuction-mode' we should define
+;; a function that return the defun name.
+;; In a multilingual treesit mode, this can be implemented simply by
+;; calling language-specific functions.
+(defun mhtml-ts-mode--defun-name (node)
+  "Return the defun name of NODE.
+Return nil if there is no name or if NODE is not a defun node."
+    (let ((html-name (html-ts-mode--defun-name node))
+          (js-name (js--treesit-defun-name node))
+          (css-name (css--treesit-defun-name node)))
+      (cond
+       (html-name html-name)
+       (js-name js-name)
+       (css-name css-name))))
+
+;;; Flymake integration
+
+(defvar-local mhtml-ts-mode--flymake-process nil
+  "Store the Flymake process.")
+
+(defun mhtml-ts-mode-flymake-mhtml (report-fn &rest _args)
+  "MHTML backend for Flymake.
+Calls REPORT-FN directly.  Requires tidy."
+  (when (process-live-p mhtml-ts-mode--flymake-process)
+    (kill-process mhtml-ts-mode--flymake-process))
+  (let ((tidy (executable-find "tidy"))
+        (source (current-buffer))
+        (diagnostics-pattern (eval-when-compile
+                               (rx bol
+                                   "line " (group (+ num))    ;; :1 line
+                                   " column " (group (+ num)) ;; :2 column
+                                   " - " (group (+? nonl))    ;; :3 type
+                                   ": " (group (+? nonl))     ;; :4 msg
+                                   eol))))
+    (if (not tidy)
+        (error "Unable to find tidy command")
+      (save-restriction
+        (widen)
+        (setq mhtml-ts-mode--flymake-process
+              (make-process
+               :name "mhtml-ts-mode-flymake"
+               :noquery t
+               :connection-type 'pipe
+               :buffer (generate-new-buffer "*mhtml-ts-mode-flymake*")
+               :command `(,tidy "--gnu-emacs" "yes" "-e" "-q")
+               :sentinel
+               (lambda (proc _event)
+                 (when (eq 'exit (process-status proc))
+                   (unwind-protect
+                       (if (with-current-buffer source
+                             (eq proc mhtml-ts-mode--flymake-process))
+                           (with-current-buffer (process-buffer proc)
+                             (goto-char (point-min))
+                             (let (diags)
+                               (while (search-forward-regexp diagnostics-pattern nil t)
+                                 (let* ((pos
+                                         (flymake-diag-region
+                                          source
+                                          (string-to-number (match-string 1))
+                                          (string-to-number (match-string 2)))) ;; line and column
+                                        (type (cond ((equal (match-string 3) "Warning") :warning)
+                                                    ((equal (match-string 3) "Error") :error))) ;; type of message
+                                        (msg (match-string 4))) ;; message
+                                   (push (flymake-make-diagnostic source (car pos) (cdr pos) type msg)
+                                         diags)))
+                               (funcall report-fn diags)))
+                         (flymake-log :warning "Canceling obsolete check %s" proc))
+                     (kill-buffer (process-buffer proc)))))))
+        (process-send-region mhtml-ts-mode--flymake-process (point-min) (point-max))
+        (process-send-eof mhtml-ts-mode--flymake-process)))))
+
+(define-derived-mode mhtml-ts-mode html-ts-mode
+  '("HTML+" (:eval (let ((lang (mhtml-ts-mode--language-at-point (point))))
+                     (cond ((eq lang 'html) "")
+                           ((eq lang 'javascript) "JS")
+                           ((eq lang 'css) "CSS")))))
+  "Major mode for editing HTML with embedded JavaScript and CSS.
+Powered by tree-sitter."
+  (if (not (and
+            (treesit-ready-p 'html)
+            (treesit-ready-p 'javascript)
+            (treesit-ready-p 'css)))
+      (error "Tree-sitter parsers for HTML isn't available.  You can
+    install the parsers with M-x `mhtml-ts-mode-install-parsers'")
+
+    ;; When an language is embedded, you should initialize some variable
+    ;; just like it's done in the original mode.
+
+    ;; Comment.
+    ;; indenting settings for js-ts-mode.
+    (c-ts-common-comment-setup)
+    (setq-local comment-multi-line t)
+
+    ;; Font-lock.
+
+    ;; There are two ways to handle embedded code:
+    ;; 1. Use a single parser for all the embedded code in the buffer. In
+    ;; this case, the embedded code blocks are concatenated together and are
+    ;; seen as a single continuous document to the parser.
+    ;; 2. Each embedded code block gets its own parser. Each parser only sees
+    ;; that particular code block.
+
+    ;; If you go with 2 for a language, the local parsers are created and
+    ;; destroyed automatically by Emacs. So don't create a global parser for
+    ;; that embedded language here.
+
+    ;; Create the parsers, only the global ones.
+    ;; jsdoc is a local parser, don't create a parser for it.
+    (treesit-parser-create 'css)
+    (treesit-parser-create 'javascript)
+
+    ;; Multi-language modes must set the  primary parser.
+    (setq-local treesit-primary-parser (treesit-parser-create 'html))
+
+    (setq-local treesit-range-settings
+                (treesit-range-rules
+                 :embed 'javascript
+                 :host 'html
+                 '((script_element
+                    (start_tag (tag_name))
+                    (raw_text) @cap))
+
+                 ;; Another rule could be added that when it matches an
+                 ;; attribute_value that has as its parent an
+                 ;; attribute_name "style" it captures it and then
+                 ;; passes it to the css parser.
+                 :embed 'css
+                 :host 'html
+                 '((style_element
+                    (start_tag (tag_name))
+                    (raw_text) @cap))))
+
+    ;; jsdoc is not mandatory for js-ts-mode, so we respect this by
+    ;; adding jsdoc range rules only when jsdoc is available.
+    (when (treesit-ready-p 'jsdoc t)
+      (setq-local treesit-range-settings
+                  (append treesit-range-settings
+                          (treesit-range-rules
+                           :embed 'jsdoc
+                           :host 'javascript
+                           :local t
+                           `(((comment) @cap
+                              (:match ,js--treesit-jsdoc-beginning-regexp @cap))))))
+      (setq-local c-ts-common--comment-regexp
+                  js--treesit-jsdoc-comment-regexp))
+
+
+    ;; Many treesit fuctions need to know the language at-point.
+    ;; So you should define such a function.
+    (setq-local treesit-language-at-point-function #'mhtml-ts-mode--language-at-point)
+    (setq-local prettify-symbols-alist mhtml-ts-mode--prettify-symbols-alist)
+
+    ;; Indent.
+
+    ;; Since `mhtml-ts-mode' inherits indentation rules from `html-ts-mode', `js-ts-mode'
+    ;; and `css-ts-mode', if you want to change the offset you have to act on the
+    ;; *-offset variables defined for those languages.
+
+    ;; JavaScript and CSS must be indented relative to their code block.
+    ;; This is done by inserting a special rule before the normal
+    ;; indentation rules of these languages.
+    ;; The value of `mhtml-ts-mode--js-css-indent-offset' changes based on
+    ;; `mhtml-ts-mode-tag-relative-indent' and can be used to indent
+    ;; JavaScript and CSS code relative to the HTML that contains them,
+    ;; just like in mhtml-mode.
+    (setq-local treesit-simple-indent-rules mhtml-ts-mode--treesit-indent-rules)
+
+    ;; Navigation.
+
+    ;; This is for which-function-mode.
+    ;; Since mhtml-ts-mode is derived from html-ts-mode, which sets
+    ;; the value of `treesit-defun-type-regexp', you have to reset it to nil
+    ;; otherwise `imenu' and `which-function-mode' will not work.
+    (setq-local treesit-defun-type-regexp nil)
+
+    ;; This is for finding defun name, it's used by IMenu as default
+    ;; function no specific functions are defined.
+    (setq-local treesit-defun-name-function #'mhtml-ts-mode--defun-name)
+
+    ;; Define what are 'thing' for treesit.
+    ;; 'Thing' is a symbol representing the thing, like `defun', `sexp', or
+    ;; `sentence'.
+    ;; As an alternative, if you want just defun, you can define a `treesit-defun-type-regexp'.
+    (setq-local treesit-thing-settings mhtml-ts-mode--treesit-thing-settings)
+
+    ;; Font-lock.
+
+    ;; In a multi-language scenario, font lock settings are usually a
+    ;; concatenation of language rules. As you can see, it is possible
+    ;; to extend/modify the default rule or use a different set of
+    ;; rules. See `php-ts-mode--custom-html-font-lock-settings' for more
+    ;; advanced usage.
+    (setq-local treesit-font-lock-settings mhtml-ts-mode--treesit-font-lock-settings)
+
+    ;; Tells treesit the list of features to fontify.
+    (setq-local treesit-font-lock-feature-list mhtml-ts-mode--treesit-font-lock-feature-list)
+
+    ;; Imenu
+
+    ;; Setup Imenu: if no function is specified, try to find an object
+    ;; using `treesit-defun-name-function'.
+    (setq-local treesit-aggregated-simple-imenu-settings
+                mhtml-ts-mode--treesit-aggregated-simple-imenu-settings)
+
+    (treesit-major-mode-setup)
+
+    ;; This is sort of a prog-mode as well as a text mode.
+    (run-mode-hooks 'prog-mode-hook)
+
+    ;; Flymake
+    (add-hook 'flymake-diagnostic-functions #'mhtml-ts-mode-flymake-mhtml nil 'local)))
+
+;; Add nome extra parents.
+(derived-mode-add-parents 'mhtml-ts-mode '(css-mode js-mode))
+
+(when (and (treesit-ready-p 'html) (treesit-ready-p 'javascript) (treesit-ready-p 'css))
+  (add-to-list
+   'auto-mode-alist '("\\.[sx]?html?\\(\\.[a-zA-Z_]+\\)?\\'" . mhtml-ts-mode)))
+
+(provide 'mhtml-ts-mode)
+;;; mhtml-ts-mode.el ends here
diff --git a/lisp/treesit.el b/lisp/treesit.el
index 8d86d142e3f..00b7d266e74 100644
--- a/lisp/treesit.el
+++ b/lisp/treesit.el
@@ -1297,6 +1297,40 @@ treesit-font-lock-recompute-features
                        ((memq feature remove-list) nil)
                        (t current-value))))))
 
+(defun treesit-merge-font-lock-feature-list (features-list-1 features-list-2)
+  "Merge two tree-sitter font lock feature lists.
+Returns a new font lock feature list with no duplicates in the same level.
+It can be used to merge font lock feature lists in a multi-language major mode.
+FEATURES-LIST-1 and FEATURES-LIST-2 are list of lists of feature symbols."
+    (let ((result nil)
+	(features-1 (car features-list-1))
+	(features-2 (car features-list-2)))
+    (while (or features-1 features-2)
+      (cond
+       ((and features-1 (not features-2)) (push features-1 result))
+       ((and (not features-1) features-2) (push features-2 result))
+       ((and features-1 features-2) (push (cl-union features-1 features-2) result)))
+      (setq features-list-1 (cdr features-list-1)
+	    features-list-2 (cdr features-list-2)
+	    features-1 (car features-list-1)
+            features-2 (car features-list-2)))
+    (nreverse result)))
+
+(defun treesit-replace-font-lock-feature-settings (new-settings settings)
+  "Replaces :feature in SETTINGS with :feature from NEW-SETTINGS.
+Both SETTINGS and NEW-SETTINGS must be a value suitable for
+`treesit-font-lock-settings'.
+Return a value suitable for `treesit-font-lock-settings'"
+  (let ((result nil))
+    (dolist (new-setting new-settings)
+      (let ((new-feature (treesit-font-lock-setting-feature new-setting)))
+	(dolist (setting settings)
+	  (let ((feature (treesit-font-lock-setting-feature setting)))
+	    (if (eq new-feature feature)
+		(push new-setting result)
+	      (push setting result))))))
+    (nreverse result)))
+
 (defun treesit-add-font-lock-rules (rules &optional how feature)
   "Add font-lock RULES to the current buffer.
 
@@ -2401,6 +2435,37 @@ treesit--indent-rules-optimize
                             offset)))))
              (cons lang (mapcar #'optimize-rule indent-rules)))))
 
+(defun treesit-modify-indent-rules (new-rules rules &optional how)
+  "Modify RULES using NEW-RULES.
+As default replace rules with the same anchor.
+When HOW is :prepend NEW-RULES are prepend to RULES, when
+:append NEW-RULES are appended to RULES, when :replace (the default)
+NEW-RULES replace rule in RULES which the same anchor."
+  (let ((n-lang (car (car new-rules)))
+	(lang (car (car rules))))
+    (when (not (eq n-lang lang))
+      (error "The language must be the same"))
+    (let* ((nr (cdr (car new-rules)))
+           (r (cdr (car rules)))
+           (tmp nil)
+           (result
+            (cond
+             ((eq how :prepend)
+	      (append nr r))
+             ((eq how :append)
+              (append r nr))
+             ((or (eq how :replace) t)
+              (nreverse
+               (progn
+                 (dolist (new-rule nr)
+	           (dolist (rule r)
+	             (if (equal (nth 0 new-rule) (nth 0 rule))
+		         (push new-rule tmp)
+		       (push rule tmp))))
+                 tmp))))))
+      (push lang result)
+      `(,result))))
+
 ;;; Search
 
 (defun treesit-search-forward-goto
-- 
2.48.1