166 lines
5.9 KiB
EmacsLisp
166 lines
5.9 KiB
EmacsLisp
![]() |
;;; skewer-html.el --- skewer support for live-interaction HTML -*- lexical-binding: t; -*-
|
||
|
|
||
|
;; This is free and unencumbered software released into the public domain.
|
||
|
|
||
|
;;; Commentary:
|
||
|
|
||
|
;; This minor mode provides functionality for HTML like plain Skewer
|
||
|
;; does for JavaScript. There's no clean way to replace the body and
|
||
|
;; head elements of a live document, so "evaluating" these elements is
|
||
|
;; not supported.
|
||
|
|
||
|
;; * C-M-x -- `skewer-html-eval-tag'
|
||
|
|
||
|
;; See also `skewer-html-fetch-selector-into-buffer' for grabbing the
|
||
|
;; page as it current exists.
|
||
|
|
||
|
;;; Code:
|
||
|
|
||
|
(require 'cl-lib)
|
||
|
(require 'sgml-mode)
|
||
|
(require 'skewer-mode)
|
||
|
|
||
|
;; Macros
|
||
|
|
||
|
(defmacro skewer-html--with-html-mode (&rest body)
|
||
|
"Evaluate BODY as if in `html-mode', using a temp buffer if necessary."
|
||
|
(declare (indent 0))
|
||
|
(let ((orig-buffer (make-symbol "orig-buffer"))
|
||
|
(temp-buffer (make-symbol "temp-buffer"))
|
||
|
(orig-point (make-symbol "orig-point")))
|
||
|
`(let ((,temp-buffer (and (not (eq major-mode 'html-mode))
|
||
|
(generate-new-buffer " *skewer-html*")))
|
||
|
(,orig-buffer (current-buffer))
|
||
|
(,orig-point (point)))
|
||
|
(unwind-protect
|
||
|
(with-current-buffer (or ,temp-buffer ,orig-buffer)
|
||
|
(when ,temp-buffer
|
||
|
(insert-buffer-substring ,orig-buffer)
|
||
|
(setf (point) ,orig-point)
|
||
|
(html-mode))
|
||
|
,@body)
|
||
|
(when ,temp-buffer
|
||
|
(kill-buffer ,temp-buffer))))))
|
||
|
|
||
|
;; Selector computation
|
||
|
|
||
|
(defun skewer-html--cleanup (tag)
|
||
|
"Cleanup TAG name from sgml-mode."
|
||
|
(skewer-html--with-html-mode
|
||
|
(replace-regexp-in-string "/$" "" (sgml-tag-name tag))))
|
||
|
|
||
|
(defun skewer-html--tag-after-point ()
|
||
|
"Return the tag struct for the tag immediately following point."
|
||
|
(skewer-html--with-html-mode
|
||
|
(save-excursion
|
||
|
(forward-char 1)
|
||
|
(sgml-parse-tag-backward))))
|
||
|
|
||
|
(defun skewer-html--get-context ()
|
||
|
"Like `sgml-get-context' but to the root, skipping close tags."
|
||
|
(skewer-html--with-html-mode
|
||
|
(save-excursion
|
||
|
(cl-loop for context = (sgml-get-context)
|
||
|
while context
|
||
|
nconc (nreverse context) into tags
|
||
|
finally return (cl-delete 'close tags :key #'sgml-tag-type)))))
|
||
|
|
||
|
(cl-defun skewer-html-compute-tag-nth (&optional (point (point)))
|
||
|
"Compute the position of this tag within its parent."
|
||
|
(skewer-html--with-html-mode
|
||
|
(save-excursion
|
||
|
(setf (point) point)
|
||
|
(let ((context (skewer-html--get-context)))
|
||
|
(when context
|
||
|
(let ((tag-name (skewer-html--cleanup (car context)))
|
||
|
(target-depth (1- (length context))))
|
||
|
(cl-loop with n = 0
|
||
|
;; If point doesn't move, we're at the root.
|
||
|
for point-start = (point)
|
||
|
do (sgml-skip-tag-backward 1)
|
||
|
until (= (point) point-start)
|
||
|
;; If depth changed, we're done.
|
||
|
for current-depth = (length (skewer-html--get-context))
|
||
|
until (< current-depth target-depth)
|
||
|
;; Examine the sibling tag.
|
||
|
for current-name = (save-excursion
|
||
|
(forward-char)
|
||
|
(sgml-parse-tag-name))
|
||
|
when (equal current-name tag-name)
|
||
|
do (cl-incf n)
|
||
|
finally return n)))))))
|
||
|
|
||
|
(defun skewer-html-compute-tag-ancestry ()
|
||
|
"Compute the ancestry chain at point."
|
||
|
(skewer-html--with-html-mode
|
||
|
(nreverse
|
||
|
(cl-loop for tag in (skewer-html--get-context)
|
||
|
for nth = (skewer-html-compute-tag-nth (1+ (sgml-tag-start tag)))
|
||
|
for name = (skewer-html--cleanup tag)
|
||
|
unless (equal name "html")
|
||
|
collect (list name nth)))))
|
||
|
|
||
|
(defun skewer-html-compute-selector ()
|
||
|
"Compute the selector for exactly the tag around point."
|
||
|
(let ((ancestry (skewer-html-compute-tag-ancestry)))
|
||
|
(mapconcat (lambda (tag)
|
||
|
(format "%s:nth-of-type(%d)" (cl-first tag) (cl-second tag)))
|
||
|
ancestry " > ")))
|
||
|
|
||
|
;; Fetching
|
||
|
|
||
|
(defun skewer-html-fetch-selector (selector)
|
||
|
"Fetch the innerHTML of a selector."
|
||
|
(let ((result (skewer-eval-synchronously selector :type "fetchselector")))
|
||
|
(if (skewer-success-p result)
|
||
|
(cdr (assoc 'value result))
|
||
|
"")))
|
||
|
|
||
|
(defun skewer-html-fetch-selector-into-buffer (selector)
|
||
|
"Fetch the innerHTML of a selector and insert it into the active buffer."
|
||
|
(interactive "sSelector: ")
|
||
|
(insert (skewer-html-fetch-selector selector)))
|
||
|
|
||
|
;; Evaluation
|
||
|
|
||
|
(defun skewer-html-eval (string ancestry &optional append)
|
||
|
"Load HTML into a selector, optionally appending."
|
||
|
(let ((ancestry* (cl-coerce ancestry 'vector))) ; for JSON
|
||
|
(skewer-eval string nil :type "html" :extra `((ancestry . ,ancestry*)
|
||
|
(append . ,append)))))
|
||
|
|
||
|
(defun skewer-html-eval-tag ()
|
||
|
"Load HTML from the immediately surrounding tag."
|
||
|
(interactive)
|
||
|
(let ((ancestry (skewer-html-compute-tag-ancestry)))
|
||
|
(save-excursion
|
||
|
;; Move to beginning of opening tag
|
||
|
(let* ((beg (skewer-html--with-html-mode
|
||
|
(sgml-skip-tag-forward 1) (point)))
|
||
|
(end (skewer-html--with-html-mode
|
||
|
(sgml-skip-tag-backward 1) (point)))
|
||
|
(region (buffer-substring-no-properties beg end)))
|
||
|
(skewer-flash-region beg end)
|
||
|
(if (= (length ancestry) 1)
|
||
|
(error "Error: cannot eval body and head tags.")
|
||
|
(skewer-html-eval region ancestry nil))))))
|
||
|
|
||
|
;; Minor mode definition
|
||
|
|
||
|
(defvar skewer-html-mode-map
|
||
|
(let ((map (make-sparse-keymap)))
|
||
|
(prog1 map
|
||
|
(define-key map (kbd "C-M-x") 'skewer-html-eval-tag)))
|
||
|
"Keymap for skewer-html-mode")
|
||
|
|
||
|
;;;###autoload
|
||
|
(define-minor-mode skewer-html-mode
|
||
|
"Minor mode for interactively loading new HTML."
|
||
|
:lighter " skewer-html"
|
||
|
:keymap skewer-html-mode-map
|
||
|
:group 'skewer)
|
||
|
|
||
|
(provide 'skewer-html)
|
||
|
|
||
|
;;; skewer-html.el ends here
|