;;; mhtml-ts-mode.el --- Major mode for HTML using tree-sitter -*- lexical-binding: t; -*- ;; Copyright (C) 2024-2025 Free Software Foundation, Inc. ;; Author: Vincenzo Pupillo ;; Maintainer: Vincenzo Pupillo ;; 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 . ;;; 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 When nil, indentation of the tag body starts just below the tag, like: When `ignore', the tag body starts in the first column, like: " :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) ;; 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) (js--treesit-language-at-point 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