From 355075793eff5a58dac83756d96881b6932d5838 Mon Sep 17 00:00:00 2001 From: Vincenzo Pupillo Date: Fri, 29 Nov 2024 22:48:45 +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. * lisp/textmodes/mhtml-ts-mode.el: New file. --- etc/NEWS | 8 + lisp/textmodes/mhtml-ts-mode.el | 429 ++++++++++++++++++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 lisp/textmodes/mhtml-ts-mode.el diff --git a/etc/NEWS b/etc/NEWS index 4d2a2c893d0..8f9a04dcf01 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -797,6 +797,14 @@ 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 diff --git a/lisp/textmodes/mhtml-ts-mode.el b/lisp/textmodes/mhtml-ts-mode.el new file mode 100644 index 00000000000..746300efc33 --- /dev/null +++ b/lisp/textmodes/mhtml-ts-mode.el @@ -0,0 +1,429 @@ +;;; mhtml-ts-mode.el --- Major mode for HTML using tree-sitter -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Free Software Foundation, Inc. + +;; Author: Vincenzo Pupillo +;; Maintainer: Vincenzo Pupillo +;; Created: Nov 2024 +;; Keywords: HTML language 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 +;; * 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.0")) + (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript" "v0.23.0")) + (jsdoc . ("https://github.com/tree-sitter/tree-sitter-jsdoc" "v0.23.0")) + (css . ("https://github.com/tree-sitter/tree-sitter-css" "v0.23.0"))) + "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 script body starts just below the +tag, like: + + + +When `ignore', the script 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) + +;; 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 + ((string-equal "(script_element (raw_text))" node-query) 'javascript) + ((string-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