;;; tree-exp-mode.el --- Minor mode for expanding tree-like syntax -*- lexical-binding: t; -*- (eval-when-compile (require 'cl-macs) (require 'tempo)) (defgroup tree-exp nil "Customization group for `emmet-mode'." :group 'convenience) (defcustom tree-exp-html-self-closing-style "" "Self-closing tag style for HTML. This setting specifies how `tree-exp' should generate self-closing tags when expanding to HTML. When generating a self-closing tag the value of `tree-exp-html-self-closing-style' is inserted verbatim between the end of the element attributes and the closing angle-bracket. Note that the variable of this setting has no bearing on the validity of the generated HTML; infact the HTML standard instructs parsers to ignore a trailing slash." :type '(choice (const :tag "") (const :tag "/") (const :tag " /")) :package-version '(tree-exp . "1.0.0") :group 'tree-exp) (defcustom tree-exp-set-marks t "TODO" :type 'boolean :package-version '(tree-exp . "1.0.0") :group 'tree-exp) (defvar-keymap tree-exp-mode-map :doc "TODO") (defvar tree-exp-expand-functions '(((html-mode mhtml-mode html-ts-mode) . tree-exp-expand-html) (gsp-ts-mode . tree-exp-expand-gsp)) "TODO") (defvar tree-exp-operator-alist-alist '(((html-mode mhtml-mode html-ts-mode gsp-ts-mode) . ((?+ . sibling) (?^ . parent) (?> . child) (?. . extra) (?# . extra) (?@ . extra) (?* . repeat)))) "TODO") (defvar tree-exp-after-expansion-hook nil "TODO") ;; Private Variables (defvar-local tree-exp--mark-index 0) (defconst tree-exp--html-void-elements #s(hash-table size 13 test equal purecopy t data ("area" t "base" t "br" t "col" t "embed" t "hr" t "img" t "input" t "link" t "meta" t "source" t "track" t "wbr" t))) ;; Parsing (defun tree-exp--pattern-start () (max (save-excursion (skip-syntax-backward "^ ") (point)) (pos-bol))) (defun tree-exp--pattern-bounds () (if (use-region-p) (progn (when (region-noncontiguous-p) (error "Noncontiguous regions are not supported yet.")) (car (region-bounds))) (cons (tree-exp--pattern-start) (point)))) (defun tree-exp--symbol-end (operator-alist) (while (and (memq (car (syntax-after (point))) '(2 3)) (not (assq (char-after (point)) operator-alist))) (goto-char (1+ (point)))) (point)) (defun tree-exp--set-children-of-leaves (tree values) (dolist (node tree) (if-let ((children (nth 2 node))) (tree-exp--set-children-of-leaves children values) (setf (nth 2 node) values))) tree) (defun tree-exp--append-extra-to-leaves (tree value) (dolist (node tree) (if-let ((children (nth 2 node))) (tree-exp--append-extra-to-leaves children value) (setf (nth 1 node) (append (nth 1 node) (cons value nil))))) tree) (defun tree-exp--parse-count () (let* ((start (point)) (end (+ start (skip-chars-forward "0-9"))) (num (string-to-number (buffer-substring-no-properties start end) 10))) (unless (zerop num) num))) (defun tree-exp--parse-atom (operator-alist) (let ((start (point)) (end (tree-exp--symbol-end operator-alist))) (if (= start end) (when (= ?\( (char-after end)) (goto-char (1+ end)) (let ((tree (tree-exp--parse-expr operator-alist)) (char (char-after (point)))) (if (eq ?\) char) (progn (goto-char (1+ (point))) tree) (message (if char (format "Invalid operator `%c'" char) "Missing closing parenthesis")) nil))) (list (list (buffer-substring-no-properties start end) nil nil))))) (defun tree-exp--parse-term (operator-alist) (let ((terms (tree-exp--parse-atom operator-alist))) (catch 'loop (while t (let* ((operator (assq (char-after (point)) operator-alist)) (op-char (car-safe operator)) (op-type (cdr-safe operator)) start end) (unless (eq op-type 'extra) (throw 'loop terms)) (setq start (goto-char (1+ (point))) end (tree-exp--symbol-end operator-alist)) (when (= start end) (message "Operator `%c' missing right-hand side" op-char) (throw 'loop nil)) (tree-exp--append-extra-to-leaves terms (cons op-char (buffer-substring-no-properties start end)))))))) ;; Gross hack (defun tree-exp--parse-expr-with-lhs (operator-alist lhs) (let* ((op-char (char-after (point))) (op-type (alist-get op-char operator-alist)) rhs) (cond ((not lhs) nil) ((or (not op-char) (not op-type)) lhs) (t (goto-char (1+ (point))) (setq rhs (if (eq op-type 'repeat) (tree-exp--parse-count) (tree-exp--parse-expr operator-alist))) (when rhs (pcase op-type ('child (tree-exp--set-children-of-leaves lhs rhs)) ('parent (tree-exp--set-children-of-leaves rhs lhs)) ('sibling (append lhs rhs)) ('repeat (tree-exp--parse-expr-with-lhs operator-alist ;; FIXME: We need to call ‘copy-tree’ or else the ;; generated AST has cycles in it… why is that? How ;; can we fix it? (cl-loop for _ from 1 to rhs append (copy-tree lhs)))))))))) (defun tree-exp--parse-expr (operator-alist) (tree-exp--parse-expr-with-lhs operator-alist (tree-exp--parse-term operator-alist))) (defun tree-exp--build-ast (bounds) (save-excursion (with-restriction (car bounds) (cdr bounds) (goto-char 1) (when-let* ((operator-alist (tree-exp--alist-get major-mode tree-exp-operator-alist-alist)) (tree (tree-exp--parse-expr operator-alist))) (if (= (point) (point-max)) tree (message "Superfluous character `%c'" (char-after (point))) nil))))) ;; Expansion (defun tree-exp--alist-get (key alist) (cdr (or (assq key alist) (catch 'tree-exp--break (dolist (pair alist) (when (and (listp (car pair)) (memq key (car pair))) (throw 'tree-exp--break pair))))))) (defun tree-exp--ast-siblings-p (ast) (cond ((not ast) nil) ((length> ast 1) t) (t (tree-exp--ast-siblings-p (caddar ast))))) (defun tree-exp-expand-html--format-attr (attr) (declare ((pure t) (side-effect-free t))) (let* ((parts (string-split attr "=")) (name (car parts)) (value-parts (cdr parts))) (if value-parts (format "%s=\"%s\"" name (string-join value-parts "=")) name))) (defun tree-exp-expand-html--helper (ast indentp) (dolist (node ast) (let* ((name (nth 0 node)) (attrs (nth 1 node)) (children (nth 2 node)) (classes (cl-loop for (op . attr) in attrs if (= ?. op) collect attr))) (insert (format "<%s" name)) (when classes (insert (format " class=\"%s\"" (string-join classes " ")))) (cl-loop for (op . attr) in attrs if (= ?# op) do (insert (format " id=\"%s\"" attr)) else if (= ?@ op) do (thread-last attr (tree-exp-expand-html--format-attr) (concat " ") (insert))) (if (and (not children) (gethash name tree-exp--html-void-elements)) (insert (format "%s>" tree-exp-html-self-closing-style)) (insert ?>) (if children (progn (when indentp (insert ?\n)) (tree-exp-expand-html--helper children indentp)) (when tree-exp-set-marks (insert #x1B))) (insert (format "" name)))) (when indentp (insert ?\n)))) (defun tree-exp-expand-html (ast) "TODO" (tree-exp-expand-html--helper ast (tree-exp--ast-siblings-p ast)) (when (= ?\n (char-before (point-max))) (delete-region (1- (point-max)) (point-max))) (when tree-exp-set-marks (insert #x1B))) ;; (defun tree-exp-expand-gsp--helper (ast indentp) ;; (dolist (node ast) ;; (let ((name (nth 0 node)) ;; (attrs (nth 1 node)) ;; (children (nth 2 node))) ;; ;; (insert (format "<%s" name)) ;; ;; (dolist (attr attrs) ;; ;; (insert (format " %s" (cdr attr)))) ;; ;; (if (and (not children) ;; ;; (gethash name tree-exp--html-void-elements)) ;; ;; (insert (format "%s>" tree-exp-html-self-closing-style)) ;; ;; (insert ?>) ;; ;; (if children ;; ;; (progn ;; ;; (when indentp ;; ;; (insert ?\n)) ;; ;; (tree-exp-expand-html--helper children indentp)) ;; ;; (when tree-exp-set-marks ;; ;; (insert #x1B))) ;; ;; (insert (format "" name))) ;; ) ;; (when indentp ;; (insert ?\n)))) (defun tree-exp-expand-gsp (ast) "TODO" (tree-exp-expand-gsp--helper ast (tree-exp--ast-siblings-p ast)) (when (= ?\n (char-before (point-max))) (delete-region (1- (point-max)) (point-max))) (when tree-exp-set-marks (insert #x1B))) ;;;###autoload (defun tree-exp-expand () "TODO" (interactive) (when-let* ((current-buffer (current-buffer)) (bounds (tree-exp--pattern-bounds)) (ast (tree-exp--build-ast bounds)) (expander (tree-exp--alist-get major-mode tree-exp-expand-functions))) (delete-region (car bounds) (cdr bounds)) (with-temp-buffer (funcall expander ast) (insert-into-buffer current-buffer)) (let ((start-pos (car bounds)) (end-pos (point-marker)) (delete-active-region nil) ; For ‘delete-backward-char’ marks) (when tree-exp-set-marks (save-excursion (goto-char start-pos) (while (search-forward "\x1B" end-pos :noerror) (delete-backward-char 1) (push (point-marker) marks))) (push (point-marker) marks) (setq marks (nreverse marks)) (goto-char (car marks)) (mapc #'tempo-insert-mark marks)) ;; Set the start of the region to the start bound (unless (region-active-p) (set-mark start-pos)) (indent-region start-pos end-pos) (run-hooks tree-exp-after-expansion-hook)))) ;;;###autoload (define-minor-mode tree-exp-mode "TODO" :lighter " Tree-Expand" :keymap tree-exp-mode-map) (provide 'tree-exp)