This commit is contained in:
Raymundo Vasquez Ruiz
2019-10-09 20:36:55 +02:00
commit 87240f58db
223 changed files with 100952 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
;;; cache-table.el --- a hash table with expiring entries -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;; Author: Christopher Wellons <mosquitopsu@gmail.com>
;; Version: 1.0
;;; Commentary:
;; See the docstring of `cache-table-create'. There is no
;; `cache-table-put': use `setf' on `cache-table-get' instead.
;;; Code:
(require 'cl-lib)
(cl-defstruct (cache-table (:constructor cache-table--create))
"A cache table with expiring entries."
expire-time table)
(defun cache-table-create (expire-time &rest keyword-args)
"Create a new cache-table with entries automatically removed
from the table after EXPIRE-TIME seconds. This function accepts
the same keyword arguments as `make-hash-table'. Entries are not
actually removed from the cache-table until an access is made to
the cache-table.
Use `cache-table-get' to get and put (via setf) entries."
(cache-table--create :expire-time expire-time
:table (apply #'make-hash-table keyword-args)))
(defun cache-table-clear-expired (cache-table)
"Remove all expired entries from CACHE-TABLE."
(cl-loop with expire-time = (cache-table-expire-time cache-table)
with table = (cache-table-table cache-table)
with dead-time = (- (float-time) expire-time)
for key being the hash-keys of table using (hash-value entry)
for (time . value) = entry
when (< time dead-time) do (remhash key table)))
(defun cache-table-get (key cache-table &optional default)
"Access the value for KEY in CACHE-TABLE if it has not yet
expired. Behaves just like `gethash'."
(cache-table-clear-expired cache-table)
(cdr (gethash key (cache-table-table cache-table) (cons 0 default))))
(gv-define-setter cache-table-get (value key cache-table)
"Put an entry in the hash table, like (setf (gethash key table) value)."
`(progn
(cache-table-clear-expired ,cache-table)
(puthash ,key (cons (float-time) ,value)
(cache-table-table ,cache-table))))
(defun cache-table-map (f cache-table)
"Like `maphash', call F for all non-expired entries in CACHE-TABLE."
(cache-table-clear-expired cache-table)
(maphash (lambda (k v) (funcall f k (cdr v)))
(cache-table-table cache-table)))
(defun cache-table-count (cache-table)
"Like `hash-table-count', count the number of non-expired entries."
(hash-table-count (cache-table-table cache-table)))
(provide 'cache-table)
;;; cache-table.el ends here

Binary file not shown.

View File

@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Skewer</title>
<script src="/skewer"></script>
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,217 @@
;;; skewer-bower.el --- dynamic library loading -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;;; Commentary:
;; This package loads libraries into the current page using the bower
;; infrastructure. Note: bower is not actually used by this package
;; and so does *not* need to be installed. Only git is required (see
;; `skewer-bower-git-executable'). It will try to learn how to run git
;; from Magit if available.
;; The interactive command for loading libraries is
;; `skewer-bower-load'. It will prompt for a library and a version,
;; automatically fetching it from the bower infrastructure if needed.
;; For example, I often find it handy to load some version of jQuery
;; when poking around at a page that doesn't already have it loaded.
;; Caveat: unfortunately the bower infrastructure is a mess; many
;; packages are in some sort of broken state -- missing dependencies,
;; missing metadata, broken metadata, or an invalid repository URL.
;; Some of this is due to under-specification of the metadata by the
;; bower project. Broken packages are unlikely to be loadable by
;; skewer-bower.
;;; Code:
(require 'cl-lib)
(require 'skewer-mode)
(require 'simple-httpd)
(require 'magit nil t) ; optional
(defcustom skewer-bower-cache-dir (locate-user-emacs-file "skewer-cache")
"Location of library cache (git repositories)."
:type 'string
:group 'skewer)
(defcustom skewer-bower-endpoint "https://bower.herokuapp.com"
"Endpoint for accessing package information."
:type 'string
:group 'skewer)
(defcustom skewer-bower-json '("bower.json" "package.json" "component.json")
"Files to search for package metadata."
:type 'list
:group 'skewer)
; Try to match Magit's configuration if available
(defcustom skewer-bower-git-executable "git"
"Name of the git executable."
:type 'string
:group 'skewer)
(defvar skewer-bower-packages nil
"Alist of all packages known to bower.")
(defvar skewer-bower-refreshed nil
"List of packages that have been refreshed recently. This keeps
them from hitting the network frequently.")
;;;###autoload
(defun skewer-bower-refresh ()
"Update the package listing and packages synchronously."
(interactive)
(cl-declare (special url-http-end-of-headers))
(setf skewer-bower-refreshed nil)
(with-current-buffer
(url-retrieve-synchronously (concat skewer-bower-endpoint "/packages"))
(setf (point) url-http-end-of-headers)
(setf skewer-bower-packages
(cl-sort
(cl-loop for package across (json-read)
collect (cons (cdr (assoc 'name package))
(cdr (assoc 'url package))))
#'string< :key #'car))))
;; Git functions
(defun skewer-bower-cache (package)
"Return the cache repository directory for PACKAGE."
(unless (file-exists-p skewer-bower-cache-dir)
(make-directory skewer-bower-cache-dir t))
(expand-file-name package skewer-bower-cache-dir))
(defun skewer-bower-git (package &rest args)
"Run git for PACKAGE's repository with ARGS."
(with-temp-buffer
(when (zerop (apply #'call-process skewer-bower-git-executable nil t nil
(format "--git-dir=%s" (skewer-bower-cache package))
args))
(buffer-string))))
(defun skewer-bower-git-clone (url package)
"Clone or fetch PACKAGE's repository from URL if needed."
(if (member package skewer-bower-refreshed)
t
(let* ((cache (skewer-bower-cache package))
(status
(if (file-exists-p cache)
(when (skewer-bower-git package "fetch")
(push package skewer-bower-refreshed))
(skewer-bower-git package "clone" "--bare" url cache))))
(not (null status)))))
(defun skewer-bower-git-show (package version file)
"Grab FILE from PACKAGE at version VERSION."
(when (string-match-p "^\\./" file) ; avoid relative paths
(setf file (substring file 2)))
(skewer-bower-git package "show" (format "%s:%s" version file)))
(defun skewer-bower-git-tag (package)
"List all the tags in PACKAGE's repository."
(split-string (skewer-bower-git package "tag")))
;; Bower functions
(defun skewer-bower-package-ensure (package)
"Ensure a package is installed in the cache and up to date.
Emit an error if the package could not be ensured."
(when (null skewer-bower-packages) (skewer-bower-refresh))
(let ((url (cdr (assoc package skewer-bower-packages))))
(when (null url)
(error "Unknown package: %s" package))
(when (null (skewer-bower-git-clone url package))
(error "Failed to fetch: %s" url))
t))
(defun skewer-bower-package-versions (package)
"List the available versions for a package. Always returns at
least one version."
(skewer-bower-package-ensure package)
(or (sort (skewer-bower-git-tag package) #'string<)
(list "master")))
(defun skewer-bower-get-config (package &optional version)
"Get the configuration alist for PACKAGE at VERSION. Return nil
if no configuration could be found."
(skewer-bower-package-ensure package)
(unless version (setf version "master"))
(json-read-from-string
(cl-loop for file in skewer-bower-json
for config = (skewer-bower-git-show package version file)
when config return it
finally (return "null"))))
;; Serving the library
(defvar skewer-bower-history ()
"Library selection history for `completing-read'.")
(defun skewer-bowser--path (package version main)
"Return the simple-httpd hosted path for PACKAGE."
(format "/skewer/bower/%s/%s/%s" package (or version "master") main))
(defun skewer-bower-prompt-package ()
"Prompt for a package and version from the user."
(when (null skewer-bower-packages) (skewer-bower-refresh))
;; ido-completing-read bug workaround:
(when (> (length skewer-bower-history) 32)
(setf skewer-bower-history (cl-subseq skewer-bower-history 0 16)))
(let* ((packages (mapcar #'car skewer-bower-packages))
(selection (nconc skewer-bower-history packages))
(package (completing-read "Library: " selection nil t nil
'skewer-bower-history))
(versions (reverse (skewer-bower-package-versions package)))
(version (completing-read "Version: " versions
nil t nil nil (car versions))))
(list package version)))
(defun skewer-bower--js-p (filename)
"Return non-nil if FILENAME looks like JavaScript."
(string-match "\\.js$" filename))
(defun skewer-bower-guess-main (package version config)
"Attempt to determine the main entrypoints from a potentially
incomplete or incorrect bower configuration. Returns nil if
guessing failed."
(let ((check (apply-partially #'skewer-bower-git-show package version))
(main (cdr (assoc 'main config))))
(cond ((and (vectorp main) (cl-some check main))
(cl-coerce (cl-remove-if-not #'skewer-bower--js-p main) 'list))
((and (stringp main) (funcall check main))
(list main))
((funcall check (concat package ".js"))
(list (concat package ".js")))
((funcall check package)
(list package)))))
;;;###autoload
(defun skewer-bower-load (package &optional version)
"Dynamically load a library from bower into the current page."
(interactive (skewer-bower-prompt-package))
(let* ((config (skewer-bower-get-config package version))
(deps (cdr (assoc 'dependencies config)))
(main (skewer-bower-guess-main package version config)))
(when (null main)
(error "Could not load %s (%s): no \"main\" entrypoint specified"
package version))
(cl-loop for (dep . version) in deps
do (skewer-bower-load (format "%s" dep) version))
(cl-loop for entrypoint in main
for path = (skewer-bowser--path package version entrypoint)
do (skewer-eval path nil :type "script"))))
(defservlet skewer/bower "application/javascript; charset=utf-8" (path)
"Serve a script from the local bower repository cache."
(cl-destructuring-bind (_ _skewer _bower package version . parts)
(split-string path "/")
(let* ((file (mapconcat #'identity parts "/"))
(contents (skewer-bower-git-show package version file)))
(if contents
(insert contents)
(httpd-error t 404)))))
(provide 'skewer-bower)
;;; skewer-bower.el ends here

Binary file not shown.

View File

@@ -0,0 +1,134 @@
;;; skewer-css.el --- skewer support for live-interaction CSS -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;;; Commentary:
;; This minor mode provides functionality for CSS like plain Skewer
;; does for JavaScript.
;; * C-x C-e -- `skewer-css-eval-current-declaration'
;; * C-M-x -- `skewer-css-eval-current-rule'
;; * C-c C-k -- `skewer-css-eval-buffer'
;; These functions assume there are no comments within a CSS rule,
;; *especially* not within a declaration. In the former case, if you
;; keep the comment free of CSS syntax it should be able to manage
;; reasonably well. This may be fixed someday.
;;; Code:
(require 'css-mode)
(require 'skewer-mode)
(defun skewer-css-trim (string)
"Trim and compress whitespace in the string."
(let ((cleaned (replace-regexp-in-string "[\t\n ]+" " " string)))
(replace-regexp-in-string "^[\t\n ]+\\|[\t\n ]+$" "" cleaned)))
;; Parsing
(defun skewer-css-beginning-of-rule ()
"Move to the beginning of the current rule and return point."
(skewer-css-end-of-rule)
(re-search-backward "{")
(when (re-search-backward "[}/]" nil 'start)
(forward-char))
(re-search-forward "[^ \t\n]")
(backward-char)
(point))
(defun skewer-css-end-of-rule ()
"Move to the end of the current rule and return point."
(if (eql (char-before) ?})
(point)
(re-search-forward "}")))
(defun skewer-css-end-of-declaration ()
"Move to the end of the current declaration and return point."
(if (eql (char-before) ?\;)
(point)
(re-search-forward ";")))
(defun skewer-css-beginning-of-declaration ()
"Move to the end of the current declaration and return point."
(skewer-css-end-of-declaration)
(re-search-backward ":")
(backward-sexp 1)
(point))
(defun skewer-css-selectors ()
"Return the selectors for the current rule."
(save-excursion
(let ((start (skewer-css-beginning-of-rule))
(end (1- (re-search-forward "{"))))
(skewer-css-trim
(buffer-substring-no-properties start end)))))
(defun skewer-css-declaration ()
"Return the current declaration as a pair of strings."
(save-excursion
(let ((start (skewer-css-beginning-of-declaration))
(end (skewer-css-end-of-declaration)))
(let* ((clip (buffer-substring-no-properties start end))
(pair (split-string clip ":")))
(mapcar #'skewer-css-trim pair)))))
;; Evaluation
(defun skewer-css (rule)
"Add RULE as a new stylesheet."
(skewer-eval rule nil :type "css"))
(defun skewer-css-eval-current-declaration ()
"Evaluate the declaration at the point."
(interactive)
(save-excursion
(let ((selectors (skewer-css-selectors))
(rule (skewer-css-declaration))
(start (skewer-css-beginning-of-declaration))
(end (skewer-css-end-of-declaration)))
(skewer-flash-region start end)
(skewer-css (apply #'format "%s { %s: %s }" selectors rule)))))
(defun skewer-css-eval-current-rule ()
"Evaluate the rule at the point."
(interactive)
(save-excursion
(let* ((start (skewer-css-beginning-of-rule))
(end (skewer-css-end-of-rule))
(rule (buffer-substring-no-properties start end)))
(skewer-flash-region start end)
(skewer-css (skewer-css-trim rule)))))
(defun skewer-css-eval-buffer ()
"Send the entire current buffer as a new stylesheet."
(interactive)
(skewer-css (buffer-substring-no-properties (point-min) (point-max))))
(defun skewer-css-clear-all ()
"Remove *all* Skewer-added styles from the document."
(interactive)
(skewer-eval nil nil :type "cssClearAll"))
;; Minor mode definition
(defvar skewer-css-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(define-key map (kbd "C-x C-e") 'skewer-css-eval-current-declaration)
(define-key map (kbd "C-M-x") 'skewer-css-eval-current-rule)
(define-key map (kbd "C-c C-k") 'skewer-css-eval-buffer)
(define-key map (kbd "C-c C-c") 'skewer-css-clear-all)))
"Keymap for skewer-css-mode.")
;;;###autoload
(define-minor-mode skewer-css-mode
"Minor mode for interactively loading new CSS rules."
:lighter " skewer-css"
:keymap skewer-css-mode-map
:group 'skewer)
(provide 'skewer-css)
;;; skewer-css.el ends here

Binary file not shown.

View File

@@ -0,0 +1,54 @@
// ==UserScript==
// @name Skewer Everything
// @description Add a toggle button to run Skewer on the current page
// @lastupdated 2015-09-14
// @version 1.3
// @license Public Domain
// @include /^https?:///
// @grant none
// @run-at document-start
// ==/UserScript==
window.skewerNativeXHR = XMLHttpRequest;
window.skewerInject = inject;
var host = 'http://localhost:8080';
var toggle = document.createElement('div');
toggle.onclick = inject;
toggle.style.width = '0px';
toggle.style.height = '0px';
toggle.style.borderStyle = 'solid';
toggle.style.borderWidth = '0 12px 12px 0';
toggle.style.borderColor = 'transparent #F00 transparent transparent';
toggle.style.position = 'absolute';
toggle.style.right = 0;
toggle.style.top = 0;
toggle.style.zIndex = 214748364;
var injected = false;
function inject() {
if (!injected) {
var script = document.createElement('script');
script.src = host + '/skewer';
document.body.appendChild(script);
toggle.style.borderRightColor = '#0F0';
} else {
/* break skewer to disable it */
skewer.fn = null;
toggle.style.borderRightColor = '#F00';
}
injected = !injected;
localStorage._autoskewered = JSON.stringify(injected);
}
document.addEventListener('DOMContentLoaded', function() {
/* Don't use on iframes. */
if (window.top === window.self) {
document.body.appendChild(toggle);
if (JSON.parse(localStorage._autoskewered || 'false')) {
inject();
}
}
});

View File

@@ -0,0 +1,165 @@
;;; 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

Binary file not shown.

View File

@@ -0,0 +1,128 @@
;;; skewer-mode-autoloads.el --- automatically extracted autoloads
;;
;;; Code:
(add-to-list 'load-path (directory-file-name
(or (file-name-directory #$) (car load-path))))
;;;### (autoloads nil "cache-table" "cache-table.el" (0 0 0 0))
;;; Generated autoloads from cache-table.el
(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "cache-table" '("cache-table-")))
;;;***
;;;### (autoloads nil "skewer-bower" "skewer-bower.el" (0 0 0 0))
;;; Generated autoloads from skewer-bower.el
(autoload 'skewer-bower-refresh "skewer-bower" "\
Update the package listing and packages synchronously.
\(fn)" t nil)
(autoload 'skewer-bower-load "skewer-bower" "\
Dynamically load a library from bower into the current page.
\(fn PACKAGE &optional VERSION)" t nil)
(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "skewer-bower" '("skewer")))
;;;***
;;;### (autoloads nil "skewer-css" "skewer-css.el" (0 0 0 0))
;;; Generated autoloads from skewer-css.el
(autoload 'skewer-css-mode "skewer-css" "\
Minor mode for interactively loading new CSS rules.
\(fn &optional ARG)" t nil)
(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "skewer-css" '("skewer-css")))
;;;***
;;;### (autoloads nil "skewer-html" "skewer-html.el" (0 0 0 0))
;;; Generated autoloads from skewer-html.el
(autoload 'skewer-html-mode "skewer-html" "\
Minor mode for interactively loading new HTML.
\(fn &optional ARG)" t nil)
(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "skewer-html" '("skewer-html-")))
;;;***
;;;### (autoloads nil "skewer-mode" "skewer-mode.el" (0 0 0 0))
;;; Generated autoloads from skewer-mode.el
(autoload 'list-skewer-clients "skewer-mode" "\
List the attached browsers in a buffer.
\(fn)" t nil)
(autoload 'skewer-mode "skewer-mode" "\
Minor mode for interacting with a browser.
\(fn &optional ARG)" t nil)
(autoload 'run-skewer "skewer-mode" "\
Attach a browser to Emacs for a skewer JavaScript REPL. Uses
`browse-url' to launch a browser.
With a prefix arugment (C-u), it will ask the filename of the
root document. With two prefix arguments (C-u C-u), it will use
the contents of the current buffer as the root document.
\(fn &optional ARG)" t nil)
(autoload 'skewer-run-phantomjs "skewer-mode" "\
Connect an inferior PhantomJS process to Skewer, returning the process.
\(fn)" t nil)
(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "skewer-mode" '("skewer" "phantomjs-program-name" "httpd/skewer/")))
;;;***
;;;### (autoloads nil "skewer-repl" "skewer-repl.el" (0 0 0 0))
;;; Generated autoloads from skewer-repl.el
(autoload 'skewer-repl--response-hook "skewer-repl" "\
Catches all browser messages logging some to the REPL.
\(fn RESPONSE)" nil nil)
(autoload 'skewer-repl "skewer-repl" "\
Start a JavaScript REPL to be evaluated in the visiting browser.
\(fn)" t nil)
(eval-after-load 'skewer-mode '(progn (add-hook 'skewer-response-hook #'skewer-repl--response-hook) (add-hook 'skewer-repl-mode-hook #'skewer-repl-mode-compilation-shell-hook) (define-key skewer-mode-map (kbd "C-c C-z") #'skewer-repl)))
(if (fboundp 'register-definition-prefixes) (register-definition-prefixes "skewer-repl" '("skewer-" "company-skewer-repl")))
;;;***
;;;### (autoloads nil "skewer-setup" "skewer-setup.el" (0 0 0 0))
;;; Generated autoloads from skewer-setup.el
(autoload 'skewer-setup "skewer-setup" "\
Fully integrate Skewer into js2-mode, css-mode, and html-mode buffers.
\(fn)" nil nil)
;;;***
;;;### (autoloads nil nil ("skewer-mode-pkg.el") (0 0 0 0))
;;;***
;; Local Variables:
;; version-control: never
;; no-byte-compile: t
;; no-update-autoloads: t
;; coding: utf-8
;; End:
;;; skewer-mode-autoloads.el ends here

View File

@@ -0,0 +1,7 @@
(define-package "skewer-mode" "20180706.1807" "live browser JavaScript, CSS, and HTML interaction"
'((simple-httpd "1.4.0")
(js2-mode "20090723")
(emacs "24")))
;; Local Variables:
;; no-byte-compile: t
;; End:

View File

@@ -0,0 +1,620 @@
;;; skewer-mode.el --- live browser JavaScript, CSS, and HTML interaction -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;; Author: Christopher Wellons <wellons@nullprogram.com>
;; URL: https://github.com/skeeto/skewer-mode
;;; Commentary:
;; Quick start (without package.el):
;; 1. Put this directory in your `load-path'
;; 2. Load skewer-mode.el
;; 3. M-x `run-skewer' to attach a browser to Emacs
;; 4. From a `js2-mode' buffer with `skewer-mode' minor mode enabled,
;; send forms to the browser to evaluate
;; The function `skewer-setup' can be used to configure all of mode
;; hooks (previously this was the default). This can also be done
;; manually like so,
;; (add-hook 'js2-mode-hook 'skewer-mode)
;; (add-hook 'css-mode-hook 'skewer-css-mode)
;; (add-hook 'html-mode-hook 'skewer-html-mode)
;; The keybindings for evaluating expressions in the browser are just
;; like the Lisp modes. These are provided by the minor mode
;; `skewer-mode'.
;; * C-x C-e -- `skewer-eval-last-expression'
;; * C-M-x -- `skewer-eval-defun'
;; * C-c C-k -- `skewer-load-buffer'
;; The result of the expression is echoed in the minibuffer.
;; Additionally, `css-mode' and `html-mode' get a similar set of
;; bindings for modifying the CSS rules and updating HTML on the
;; current page.
;; Note: `run-skewer' uses `browse-url' to launch the browser. This
;; may require further setup depending on your operating system and
;; personal preferences.
;; Multiple browsers and browser tabs can be attached to Emacs at
;; once. JavaScript forms are sent to all attached clients
;; simultaneously, and each will echo back the result
;; individually. Use `list-skewer-clients' to see a list of all
;; currently attached clients.
;; Sometimes Skewer's long polls from the browser will timeout after a
;; number of hours of inactivity. If you find the browser disconnected
;; from Emacs for any reason, use the browser's console to call
;; skewer() to reconnect. This avoids a page reload, which would lose
;; any fragile browser state you might care about.
;; To skewer your own document rather than the provided blank page,
;; 1. Load the dependencies
;; 2. Load skewer-mode.el
;; 3. Start the HTTP server (`httpd-start')
;; 4. Include "http://localhost:8080/skewer" as a script
;; (see `example.html' and check your `httpd-port')
;; 5. Visit the document from your browser
;; Skewer fully supports CORS, so the document need not be hosted by
;; Emacs itself. A Greasemonkey userscript and a bookmarklet are
;; provided for injecting Skewer into any arbitrary page you're
;; visiting without needing to modify the page on the host.
;; With skewer-repl.el loaded, a REPL into the browser can be created
;; with M-x `skewer-repl', or C-c C-z. This should work like a console
;; within the browser. Messages can be logged to this REPL with
;; skewer.log() (just like console.log()).
;; Extending Skewer:
;; Skewer is flexible and open to extension. The REPL and the CSS and
;; HTML minor modes are a partial examples of this. You can extend
;; skewer.js with your own request handlers and talk to them from
;; Emacs using `skewer-eval' (or `skewer-eval-synchronously') with
;; your own custom :type. The :type string chooses the dispatch
;; function under the skewer.fn object. To inject your own JavaScript
;; into skewer.js, use `skewer-js-hook'.
;; You can also catch messages sent from the browser not in response
;; to an explicit request. Use `skewer-response-hook' to see all
;; incoming objects.
;;; History:
;; Version 1.8.0: features
;; * Work around XMLHttpRequest tampering in userscript
;; * Add Makefile "run" target for testing
;; Version 1.7.0: features and fixes
;; * Support for other major modes (including web-mode) in skewer-html-mode
;; * Opportunistic support for company-mode completions
;; * Always serve content as UTF-8
;; * Improve skewer-everything.js portability
;; Version 1.6.2: fixes
;; * skewer.log() takes multiple arguments
;; * comint and encoding fixes
;; Version 1.6.1: fixes
;; * Add `skewer-css-clear-all'
;; * Better IE8 compatibility
;; * User interface tweaks
;; Version 1.6.0: fixes
;; * Bring up to speed with Emacs 24.3
;; * Switch to cl-lib from cl
;; Version 1.5.3: features
;; * Add `skewer-run-phantomjs'
;; Version 1.5.2: small cleanup
;; * Add `skewer-apply' and `skewer-funall'
;; * Improved safeStringify
;; Version 1.5.1: features
;; * No more automatic hook setup (see `skewer-setup')
;; * Support for HTML interaction
;; * Support for loading Bower packages
;; * Drop jQuery dependency
;; * Many small improvements
;; Version 1.4: features
;; * Full CSS interaction
;; * Greasemonkey userscript for injection
;; * Full, working CORS support
;; * Better browser presence detection
;; Version 1.3: features and fixes
;; * Full offline support
;; * No more callback registering
;; * Fix 64-bit support
;; * Two new hooks for improved extension support
;; * More uniform keybindings with other interactive modes
;; Version 1.2: features
;; * Add a skewer-eval-print-last-expression
;; * Display evaluation time when it's long
;; * Flash the region on eval
;; * Improve JS stringification
;; Version 1.1: features and fixes
;; * Added `list-skewer-clients'
;; * Reduce the number of HTTP requests needed
;; * Fix stringification issues
;; Version 1.0: initial release
;;; Code:
(require 'cl-lib)
(require 'json)
(require 'url-util)
(require 'simple-httpd)
(require 'js2-mode)
(require 'cache-table)
(defgroup skewer nil
"Live browser JavaScript interaction."
:group 'languages)
(defvar skewer-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(define-key map (kbd "C-x C-e") 'skewer-eval-last-expression)
(define-key map (kbd "C-M-x") 'skewer-eval-defun)
(define-key map (kbd "C-c C-k") 'skewer-load-buffer)))
"Keymap for skewer-mode.")
(defvar skewer-data-root (file-name-directory load-file-name)
"Location of data files needed by impatient-mode.")
(defvar skewer-js-hook ()
"Hook to run when skewer.js is being served to the browser.
When hook functions are called, the current buffer is the buffer
to be served to the client (a defservlet), with skewer.js script
already inserted. This is the chance for other packages to insert
their own JavaScript to extend skewer in the browser, such as
adding a new type handler.")
(defvar skewer-response-hook ()
"Hook to run when a response arrives from the browser. Used for
catching messages from the browser with no associated
callback. The response object is passed to the hook function.")
(defvar skewer-timeout 3600
"Maximum time to wait on the browser to respond, in seconds.")
(defvar skewer-clients ()
"Browsers awaiting JavaScript snippets.")
(defvar skewer-callbacks (cache-table-create skewer-timeout :test 'equal)
"Maps evaluation IDs to local callbacks.")
(defvar skewer-queue ()
"Queued messages for the browser.")
(defvar skewer--last-timestamp 0
"Timestamp of the last browser response. Use
`skewer-last-seen-seconds' to access this.")
(cl-defstruct skewer-client
"A client connection awaiting a response."
proc agent)
(defun skewer-process-queue ()
"Send all queued messages to clients."
(when (and skewer-queue skewer-clients)
(let ((message (pop skewer-queue))
(sent nil))
(while skewer-clients
(ignore-errors
(progn
(let ((proc (skewer-client-proc (pop skewer-clients))))
(with-temp-buffer
(insert (json-encode message))
(httpd-send-header proc "text/plain" 200
:Cache-Control "no-cache"
:Access-Control-Allow-Origin "*")))
(setq skewer--last-timestamp (float-time))
(setq sent t))))
(if (not sent) (push message skewer-queue)))
(skewer-process-queue)))
(defun skewer-clients-tabulate ()
"Prepare client list for tabulated-list-mode."
(cl-loop for client in skewer-clients collect
(let ((proc (skewer-client-proc client))
(agent (skewer-client-agent client)))
(cl-destructuring-bind (host port) (process-contact proc)
`(,client [,host ,(format "%d" port) ,agent])))))
(define-derived-mode skewer-clients-mode tabulated-list-mode "skewer-clients"
"Mode for listing browsers attached to Emacs for skewer-mode."
(setq tabulated-list-format [("Host" 12 t)
("Port" 5 t)
("User Agent" 0 t)])
(setq tabulated-list-entries #'skewer-clients-tabulate)
(tabulated-list-init-header))
(define-key skewer-clients-mode-map (kbd "g")
(lambda ()
(interactive)
(skewer-ping)
(revert-buffer)))
(defun skewer-update-list-buffer ()
"Revert the client list, due to an update."
(save-window-excursion
(let ((list-buffer (get-buffer "*skewer-clients*")))
(when list-buffer
(with-current-buffer list-buffer
(revert-buffer))))))
;;;###autoload
(defun list-skewer-clients ()
"List the attached browsers in a buffer."
(interactive)
(pop-to-buffer (get-buffer-create "*skewer-clients*"))
(skewer-clients-mode)
(tabulated-list-print))
(defun skewer-queue-client (proc req)
"Add a client to the queue, given the HTTP header."
(let ((agent (cl-second (assoc "User-Agent" req))))
(push (make-skewer-client :proc proc :agent agent) skewer-clients))
(skewer-update-list-buffer)
(skewer-process-queue))
;; Servlets
(defservlet skewer "text/javascript; charset=UTF-8" ()
(insert-file-contents (expand-file-name "skewer.js" skewer-data-root))
(goto-char (point-max))
(run-hooks 'skewer-js-hook))
(defun httpd/skewer/get (proc _path _query req &rest _args)
(skewer-queue-client proc req))
(defun httpd/skewer/post (proc _path _query req &rest _args)
(let* ((result (json-read-from-string (cadr (assoc "Content" req))))
(id (cdr (assoc 'id result)))
(callback (cache-table-get id skewer-callbacks)))
(setq skewer--last-timestamp (float-time))
(when callback
(funcall callback result))
(if id
(skewer-queue-client proc req)
(with-temp-buffer
(httpd-send-header proc "text/plain" 200
:Access-Control-Allow-Origin "*")))
(dolist (hook skewer-response-hook)
(funcall hook result))))
(defvar skewer-demo-source
(expand-file-name "example.html" skewer-data-root)
"Source file name or buffer for `httpd/skewer/demo' servlet.")
(defservlet skewer/demo "text/html; charset=UTF-8" ()
(cl-etypecase skewer-demo-source
(buffer (insert-buffer-substring skewer-demo-source))
(string (insert-file-contents skewer-demo-source))))
;; Minibuffer display
(defun skewer-success-p (result)
"Return T if result was a success."
(equal "success" (cdr (assoc 'status result))))
(define-derived-mode skewer-error-mode special-mode "skewer-error"
:group 'skewer
"Mode for displaying JavaScript errors returned by skewer-mode."
(setq truncate-lines t))
(defface skewer-error-face
'((((class color) (background light))
:foreground "red" :underline t)
(((class color) (background dark))
:foreground "red" :underline t))
"Face for JavaScript errors."
:group 'skewer)
(defun skewer--error (string)
"Return STRING propertized as an error message."
(propertize (or string "<unknown>") 'font-lock-face 'skewer-error-face))
(defun skewer-post-minibuffer (result)
"Report results in the minibuffer or the error buffer."
(if (skewer-success-p result)
(let ((value (cdr (assoc 'value result)))
(time (cdr (assoc 'time result))))
(if (and time (> time 1.0))
(message "%s (%.3f seconds)" value time)
(message "%s" value)))
(with-current-buffer (pop-to-buffer (get-buffer-create "*skewer-error*"))
(let ((inhibit-read-only t)
(error (cdr (assoc 'error result))))
(erase-buffer)
(skewer-error-mode)
(insert (skewer--error (cdr (assoc 'name error))) ": ")
(insert (or (cdr (assoc 'message error)) "") "\n\n")
(insert (or (cdr (assoc 'stack error)) "") "\n\n")
(insert (format "Expression: %s\n\n"
(if (cdr (assoc 'strict result)) "(strict)" ""))
(cdr (assoc 'eval error)))
(goto-char (point-min))))))
;; Evaluation functions
(cl-defun skewer-eval (string &optional callback
&key verbose strict (type "eval") extra)
"Evaluate STRING in the waiting browsers, giving the result to CALLBACK.
:VERBOSE -- if T, the return will try to be JSON encoded
:STRICT -- if T, expression is evaluated with 'use strict'
:TYPE -- chooses the JavaScript handler (default: eval)
:EXTRA -- additional alist keys to append to the request object"
(let* ((id (format "%x" (random most-positive-fixnum)))
(request `((type . ,type)
(eval . ,string)
(id . ,id)
(verbose . ,verbose)
(strict . ,strict)
,@extra)))
(prog1 request
(setf (cache-table-get id skewer-callbacks) callback)
(setq skewer-queue (append skewer-queue (list request)))
(skewer-process-queue))))
(defun skewer-eval-synchronously (string &rest args)
"Just like `skewer-eval' but synchronously, so don't provide a
callback. Use with caution."
(let ((result nil))
(apply #'skewer-eval string (lambda (v) (setq result v)) args)
(cl-loop until result
do (accept-process-output nil 0.01)
finally (return result))))
(defun skewer-apply (function args)
"Synchronously apply FUNCTION in the browser with the supplied
arguments, returning the result. All ARGS must be printable by
`json-encode'. For example,
(skewer-apply \"Math.atan2\" '(1 -2)) ; => 2.677945044588987
Uncaught exceptions propagate to Emacs as an error."
(let ((specials '(("undefined" . nil)
("NaN" . 0.0e+NaN)
("Infinity" . 1.0e+INF)
("-Infinity" . -1.0e+INF))))
(let* ((expr (concat function "(" (mapconcat #'json-encode args ", ") ")"))
(result (skewer-eval-synchronously expr :verbose t))
(value (cdr (assoc 'value result))))
(if (skewer-success-p result)
(if (assoc value specials)
(cdr (assoc value specials))
(condition-case _
(json-read-from-string value)
(json-readtable-error value)))
(signal 'javascript
(list (cdr (assoc 'message (cdr (assoc'error result))))))))))
(defun skewer-funcall (function &rest args)
"Synchronously call FUNCTION with the supplied ARGS. All ARGS
must be printable by `json-read-from-string. For example,
(skewer-funcall \"Math.sin\" 0.5) ; => 0.479425538604203
Uncaught exceptions propagate to Emacs as an error."
(skewer-apply function args))
(defun skewer--save-point (f &rest args)
"Return a function that calls F with point at the current point."
(let ((saved-point (point)))
(lambda (&rest more)
(save-excursion
(goto-char saved-point)
(apply f (append args more))))))
(defun skewer-ping ()
"Ping the browser to test that it's still alive."
(unless (null skewer-clients) ; don't queue pings
(skewer-eval (prin1-to-string (float-time)) nil :type "ping")))
(defun skewer-last-seen-seconds ()
"Return the number of seconds since the browser was last seen."
(skewer-ping) ; make sure it's still alive next request
(- (float-time) skewer--last-timestamp))
(defun skewer-mode-strict-p ()
"Return T if buffer contents indicates strict mode."
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(js2-forward-sws)
(forward-char 1)
(let* ((stricts '("\"use strict\"" "'use strict'"))
(node (js2-node-at-point))
(code (buffer-substring-no-properties (js2-node-abs-pos node)
(js2-node-abs-end node))))
(and (member code stricts) t)))))
(defun skewer-flash-region (start end &optional timeout)
"Temporarily highlight region from START to END."
(let ((overlay (make-overlay start end)))
(overlay-put overlay 'face 'secondary-selection)
(run-with-timer (or timeout 0.2) nil 'delete-overlay overlay)))
(defun skewer-get-last-expression ()
"Return the JavaScript expression before the point as a
list: (string start end)."
(save-excursion
(js2-backward-sws)
(backward-char)
(let ((node (js2-node-at-point nil t)))
(when (eq js2-FUNCTION (js2-node-type (js2-node-parent node)))
(setq node (js2-node-parent node)))
(when (js2-ast-root-p node)
(error "no expression found"))
(let ((start (js2-node-abs-pos node))
(end (js2-node-abs-end node)))
(list (buffer-substring-no-properties start end) start end)))))
(defun skewer-eval-last-expression (&optional prefix)
"Evaluate the JavaScript expression before the point in the
waiting browser. If invoked with a prefix argument, insert the
result into the current buffer."
(interactive "P")
(if prefix
(skewer-eval-print-last-expression)
(if js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse
(skewer--save-point #'skewer-eval-last-expression))
(cl-destructuring-bind (string start end) (skewer-get-last-expression)
(skewer-flash-region start end)
(skewer-eval string #'skewer-post-minibuffer)))))
(defun skewer-get-defun ()
"Return the toplevel JavaScript expression around the point as
a list: (string start end)."
(save-excursion
(js2-backward-sws)
(backward-char)
(let ((node (js2-node-at-point nil t)))
(when (js2-ast-root-p node)
(error "no expression found"))
(while (and (js2-node-parent node)
(not (js2-ast-root-p (js2-node-parent node))))
(setf node (js2-node-parent node)))
(let ((start (js2-node-abs-pos node))
(end (js2-node-abs-end node)))
(list (buffer-substring-no-properties start end) start end)))))
(defun skewer-eval-defun ()
"Evaluate the JavaScript expression before the point in the
waiting browser."
(interactive)
(if js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse (skewer--save-point #'skewer-eval-defun))
(cl-destructuring-bind (string start end) (skewer-get-defun)
(skewer-flash-region start end)
(skewer-eval string #'skewer-post-minibuffer))))
;; Print last expression
(defvar skewer-eval-print-map (cache-table-create skewer-timeout :test 'equal)
"A mapping of evaluation IDs to insertion points.")
(defun skewer-post-print (result)
"Insert the result after its source expression."
(if (not (skewer-success-p result))
(skewer-post-minibuffer result)
(let* ((id (cdr (assoc 'id result)))
(pos (cache-table-get id skewer-eval-print-map)))
(when pos
(with-current-buffer (car pos)
(goto-char (cdr pos))
(insert (cdr (assoc 'value result)) "\n"))))))
(defun skewer-eval-print-last-expression ()
"Evaluate the JavaScript expression before the point in the
waiting browser and insert the result in the buffer at point."
(interactive)
(if js2-mode-buffer-dirty-p
(js2-mode-wait-for-parse
(skewer--save-point #'skewer-eval-print-last-expression))
(cl-destructuring-bind (string start end) (skewer-get-defun)
(skewer-flash-region start end)
(insert "\n")
(let* ((request (skewer-eval string #'skewer-post-print :verbose t))
(id (cdr (assoc 'id request)))
(pos (cons (current-buffer) (point))))
(setf (cache-table-get id skewer-eval-print-map) pos)))))
;; Script loading
(defvar skewer-hosted-scripts (cache-table-create skewer-timeout)
"Map of hosted scripts to IDs.")
(defun skewer-host-script (string)
"Host script STRING from the script servlet, returning the script ID."
(let ((id (random most-positive-fixnum)))
(prog1 id
(setf (cache-table-get id skewer-hosted-scripts) string))))
(defun skewer-load-buffer ()
"Load the entire current buffer into the browser. A snapshot of
the buffer is hosted so that browsers visiting late won't see an
inconsistent buffer."
(interactive)
(let ((id (skewer-host-script (buffer-string)))
(buffer-name (buffer-name)))
(skewer-eval (format "/skewer/script/%d/%s"
id (url-hexify-string buffer-name))
(lambda (_) (message "%s loaded" buffer-name))
:type "script")))
(defservlet skewer/script "text/javascript; charset=UTF-8" (path)
(let ((id (string-to-number (nth 3 (split-string path "/")))))
(insert (cache-table-get id skewer-hosted-scripts ""))))
;; Define the minor mode
;;;###autoload
(define-minor-mode skewer-mode
"Minor mode for interacting with a browser."
:lighter " skewer"
:keymap skewer-mode-map
:group 'skewer)
;;;###autoload
(defun run-skewer (&optional arg)
"Attach a browser to Emacs for a skewer JavaScript REPL. Uses
`browse-url' to launch a browser.
With a prefix arugment (C-u), it will ask the filename of the
root document. With two prefix arguments (C-u C-u), it will use
the contents of the current buffer as the root document."
(interactive "p")
(cl-case arg
(4 (setf skewer-demo-source (read-file-name "Skewer filename: ")))
(16 (setf skewer-demo-source (current-buffer))))
(httpd-start)
(browse-url (format "http://127.0.0.1:%d/skewer/demo" httpd-port)))
;; PhantomJS
(defvar phantomjs-program-name "/usr/bin/phantomjs"
"Path to the phantomjs executable.")
(defvar skewer-phantomjs-processes ()
"List of phantomjs processes connected to Skewer.")
(defun skewer-phantomjs-sentinel (proc event)
"Cleanup after phantomjs exits."
(when (cl-some (lambda (s) (string-match-p s event))
'("finished" "abnormal" "killed"))
(delete-file (process-get proc 'tempfile))))
;;;###autoload
(defun skewer-run-phantomjs ()
"Connect an inferior PhantomJS process to Skewer, returning the process."
(interactive)
(httpd-start)
(let ((script (make-temp-file "phantomjs-"))
(url (format "http://0:%d/skewer/demo" httpd-port)))
(with-temp-buffer
(insert (format "require('webpage').create().open('%s')" url))
(write-region nil nil script nil 0)
(let ((proc (start-process "phantomjs" nil
phantomjs-program-name script)))
(prog1 proc
(push proc skewer-phantomjs-processes)
(process-put proc 'tempfile script)
(set-process-sentinel proc 'skewer-phantomjs-sentinel))))))
(defun skewer-phantomjs-kill ()
"Kill all inferior phantomjs processes connected to Skewer."
(interactive)
(mapc #'delete-process skewer-phantomjs-processes)
(setf skewer-phantomjs-processes nil))
(provide 'skewer-mode)
;;; skewer-mode.el ends here

Binary file not shown.

View File

@@ -0,0 +1,208 @@
;;; skewer-repl.el --- create a REPL in a visiting browser -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;;; Commentary:
;; This is largely based on of IELM's code. Run `skewer-repl' to
;; switch to the REPL buffer and evaluate code. Use
;; `skewer-repl-toggle-strict-mode' to turn strict mode on and off.
;; If `compilation-search-path' is set up properly, along with
;; `skewer-path-strip-level', asynchronous errors will provide
;; clickable error messages that will take you to the source file of
;; the error. This is done using `compilation-shell-minor-mode'.
;;; Code:
(require 'comint)
(require 'compile)
(require 'skewer-mode)
(defcustom skewer-repl-strict-p nil
"When non-NIL, all REPL evaluations are done in strict mode."
:type 'boolean
:group 'skewer)
(defcustom skewer-repl-prompt "js> "
"Prompt string for JavaScript REPL."
:type 'string
:group 'skewer)
(defvar skewer-repl-welcome
(propertize "*** Welcome to Skewer ***\n"
'font-lock-face 'font-lock-comment-face)
"Header line to show at the top of the REPL buffer. Hack
notice: this allows log messages to appear before anything is
evaluated because it provides insertable space at the top of the
buffer.")
(defun skewer-repl-process ()
"Return the process for the skewer REPL."
(get-buffer-process (current-buffer)))
(defface skewer-repl-log-face
'((((class color) (background light))
:foreground "#77F")
(((class color) (background dark))
:foreground "#77F"))
"Face for skewer.log() messages."
:group 'skewer)
(define-derived-mode skewer-repl-mode comint-mode "js-REPL"
"Provide a REPL into the visiting browser."
:group 'skewer
:syntax-table emacs-lisp-mode-syntax-table
(setq comint-prompt-regexp (concat "^" (regexp-quote skewer-repl-prompt))
comint-input-sender 'skewer-input-sender
comint-process-echoes nil)
;; Make opportunistic use of company-mode, but don't require it.
;; This means company-backends may be undeclared, so don't emit a
;; warning about it.
(with-no-warnings
(setq-local company-backends '(company-skewer-repl)))
(unless (comint-check-proc (current-buffer))
(insert skewer-repl-welcome)
(start-process "skewer-repl" (current-buffer) nil)
(set-process-query-on-exit-flag (skewer-repl-process) nil)
(goto-char (point-max))
(set (make-local-variable 'comint-inhibit-carriage-motion) t)
(comint-output-filter (skewer-repl-process) skewer-repl-prompt)
(set-process-filter (skewer-repl-process) 'comint-output-filter)))
(defun skewer-repl-toggle-strict-mode ()
"Toggle strict mode for expressions evaluated by the REPL."
(interactive)
(setq skewer-repl-strict-p (not skewer-repl-strict-p))
(message "REPL strict mode %s"
(if skewer-repl-strict-p "enabled" "disabled")))
(defun skewer-input-sender (_ input)
"REPL comint handler."
(skewer-eval input 'skewer-post-repl
:verbose t :strict skewer-repl-strict-p))
(defun skewer-post-repl (result)
"Callback for reporting results in the REPL."
(let ((buffer (get-buffer "*skewer-repl*"))
(output (cdr (assoc 'value result))))
(when buffer
(with-current-buffer buffer
(comint-output-filter (skewer-repl-process)
(concat output "\n" skewer-repl-prompt))))))
(defvar skewer-repl-types
'(("log" . skewer-repl-log-face)
("error" . skewer-error-face))
"Faces to use for different types of log messages.")
(defun skewer-log-filename (log)
"Create a log string for the source file in LOG if present."
(let ((name (cdr (assoc 'filename log)))
(line (cdr (assoc 'line log)))
(column (cdr (assoc 'column log))))
(when name
(concat (format "\n at %s:%s" name line)
(if column (format ":%s" column))))))
(defun skewer-post-log (log)
"Callback for logging messages to the REPL."
(let* ((buffer (get-buffer "*skewer-repl*"))
(face (cdr (assoc (cdr (assoc 'type log)) skewer-repl-types)))
(value (or (cdr (assoc 'value log)) "<unspecified error>"))
(output (propertize value 'font-lock-face face)))
(when buffer
(with-current-buffer buffer
(save-excursion
(goto-char (point-max))
(forward-line 0)
(backward-char)
(insert (concat "\n" output (skewer-log-filename log))))))))
(defcustom skewer-path-strip-level 1
"Number of folders which will be stripped from url when discovering paths.
Use this to limit path matching to files in your filesystem. You
may want to add some folders to `compilation-search-path', so
matched files can be found."
:type 'number
:group 'skewer)
(defun skewer-repl-mode-compilation-shell-hook ()
"Setup compilation shell minor mode for highlighting files"
(let ((error-re (format "^[ ]*at https?://[^/]+/\\(?:[^/]+/\\)\\{%d\\}\\([^:?#]+\\)\\(?:[?#][^:]*\\)?:\\([[:digit:]]+\\)\\(?::\\([[:digit:]]+\\)\\)?$" skewer-path-strip-level)))
(setq-local compilation-error-regexp-alist `((,error-re 1 2 3 2))))
(compilation-shell-minor-mode 1))
;;;###autoload
(defun skewer-repl--response-hook (response)
"Catches all browser messages logging some to the REPL."
(let ((type (cdr (assoc 'type response))))
(when (member type '("log" "error"))
(skewer-post-log response))))
;;;###autoload
(defun skewer-repl ()
"Start a JavaScript REPL to be evaluated in the visiting browser."
(interactive)
(when (not (get-buffer "*skewer-repl*"))
(with-current-buffer (get-buffer-create "*skewer-repl*")
(skewer-repl-mode)))
(pop-to-buffer (get-buffer "*skewer-repl*")))
(defun company-skewer-repl (command &optional arg &rest _args)
"Skewerl REPL backend for company-mode.
See `company-backends' for more info about COMMAND and ARG."
(interactive (list 'interactive))
(cl-case command
(interactive
(with-no-warnings ;; opportunistic use of company-mode
(company-begin-backend 'company-skewer-repl)))
(prefix (skewer-repl-company-prefix))
(ignore-case t)
(sorted t)
(candidates (cons :async
(lambda (callback)
(skewer-repl-get-completions arg callback))))))
(defun skewer-repl-get-completions (arg callback)
"Get the completion list matching the prefix ARG.
Evaluate CALLBACK with the completion candidates."
(let* ((expression (skewer-repl--get-completion-expression arg))
(pattern (if expression
(substring arg (1+ (length expression)))
arg)))
(skewer-eval (or expression "window")
(lambda (result)
(cl-loop with value = (cdr (assoc 'value result))
for key being the elements of value
when expression
collect (concat expression "." key) into results
else
collect key into results
finally (funcall callback results)))
:type "completions"
:extra `((regexp . ,pattern)))))
(defun skewer-repl--get-completion-expression (arg)
"Get completion expression from ARG."
(let ((components (split-string arg "\\.")))
(when (> (length components) 1)
(mapconcat #'identity (cl-subseq components 0 -1) "."))))
(defun skewer-repl-company-prefix ()
"Prefix for company."
(and (eq major-mode 'skewer-repl-mode)
(or (with-no-warnings ;; opportunistic use of company-mode
(company-grab-symbol-cons "\\." 1))
'stop)))
;;;###autoload
(eval-after-load 'skewer-mode
'(progn
(add-hook 'skewer-response-hook #'skewer-repl--response-hook)
(add-hook 'skewer-repl-mode-hook #'skewer-repl-mode-compilation-shell-hook)
(define-key skewer-mode-map (kbd "C-c C-z") #'skewer-repl)))
(provide 'skewer-repl)
;;; skewer-repl.el ends here

Binary file not shown.

View File

@@ -0,0 +1,21 @@
;;; skewer-setup.el --- automatic setup for the Skewer minor modes -*- lexical-binding: t; -*-
;; This is free and unencumbered software released into the public domain.
;;; Commentary:
;; This exists as a separate file so that Skewer need not be fully
;; loaded just to use this setup function.
;;; Code:
;;;###autoload
(defun skewer-setup ()
"Fully integrate Skewer into js2-mode, css-mode, and html-mode buffers."
(add-hook 'js2-mode-hook 'skewer-mode)
(add-hook 'css-mode-hook 'skewer-css-mode)
(add-hook 'html-mode-hook 'skewer-html-mode))
(provide 'skewer-setup)
;;; skewer-setup.el ends here

Binary file not shown.

View File

@@ -0,0 +1,436 @@
/**
* @fileOverview Live browser interaction with Emacs
* @version 1.4
*/
/**
* Connects to Emacs and waits for a request. After handling the
* request it sends back the results and queues itself for another
* request.
* @namespace Holds all of Skewer's functionality.
*/
function skewer() {
function callback(request) {
var result = skewer.fn[request.type](request);
if (result) {
result = skewer.extend({
id: request.id,
type: request.type,
status: 'success',
value: ''
}, result);
skewer.postJSON(skewer.host + "/skewer/post", result, callback);
} else {
skewer.getJSON(skewer.host + "/skewer/get", callback);
}
};
skewer.getJSON(skewer.host + "/skewer/get", callback);
}
/**
* Get a JSON-encoded object from a server.
* @param {String} url The location of the remote server
* @param {Function} [callback] The callback to receive a response object
*/
skewer.getJSON = function(url, callback) {
var XHR = window.skewerNativeXHR || XMLHttpRequest;
var xhr = new XHR();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
callback(JSON.parse(xhr.responseText));
}
};
xhr.open('GET', url, true);
xhr.send();
};
/**
* Send a JSON-encoded object to a server.
* @param {String} url The location of the remote server
* @param {Object} object The object to transmit to the server
* @param {Function} [callback] The callback to receive a response object
*/
skewer.postJSON = function(url, object, callback) {
var XHR = window.skewerNativeXHR || XMLHttpRequest;
var xhr = new XHR();
xhr.onreadystatechange = function() {
if (callback && xhr.readyState === 4 && xhr.status === 200) {
callback(JSON.parse(xhr.responseText));
}
};
xhr.open('POST', url, true);
xhr.setRequestHeader("Content-Type", "text/plain"); // CORS
xhr.send(JSON.stringify(object));
};
/**
* Add the properties other objects to a target object (jQuery.extend).
* @param {Object} target The object to receive new properties
* @param {...Object} objects Source objects for properties
* @returns The target object
*/
skewer.extend = function(target) {
for (var i = 1; i < arguments.length; i++) {
var object = arguments[i];
for (var key in object) {
if (object.hasOwnProperty(key)) {
target[key] = object[key];
}
}
}
return target;
};
/**
* Globally evaluate an expression and return the result. This
* <i>only</i> works when the implementation's indirect eval performs
* a global eval. If not, there's no alternative, since a return value
* is essential.
*
* @see http://perfectionkills.com/global-eval-what-are-the-options/
*
* @param expression A string containing an expression to evaluate
* @returns The result of the evaluation
*/
skewer.globalEval = (function() {
var eval0 = (function(original, Object) {
try {
return [eval][0]('Object') === original;
} catch (e) {
return false;
}
}(Object, false));
if (eval0) {
return function(expression) {
return [eval][0](expression);
};
} else {
return function(expression) { // Safari
return eval.call(window, expression);
};
}
}());
/**
* Same as Date.now(), supplied for pre-ES5 JS (<=IE8).
* @returns {number} The epoch time in milliseconds
*/
skewer.now = function() {
return new Date().valueOf();
};
/**
* Handlers accept a request object from Emacs and return either a
* logical false (no response) or an object to return to Emacs.
* @namespace Request handlers.
*/
skewer.fn = {};
/**
* Handles an code evaluation request from Emacs.
* @param request The request object sent by Emacs
* @returns The result object to be returned to Emacs
*/
skewer.fn.eval = function(request) {
var result = {
strict: request.strict
};
var start = skewer.now();
try {
var prefix = request.strict ? '"use strict";\n' : "";
var value = skewer.globalEval(prefix + request.eval);
result.value = skewer.safeStringify(value, request.verbose);
} catch (error) {
result = skewer.errorResult(error, result, request);
}
result.time = (skewer.now() - start) / 1000;
return result;
};
/**
* Load a hosted script named by the request.
* @param request The request object sent by Emacs
* @returns The result object to be returned to Emacs
*/
skewer.fn.script = function(request) {
var script = document.createElement('script');
script.src = skewer.host + request.eval;
document.body.appendChild(script);
return {value: JSON.stringify(request.eval)};
};
/**
* A keep-alive and connecton testing handler.
* @param request The request object sent by Emacs
* @returns The result object to be returned to Emacs
*/
skewer.fn.ping = function(request) {
return {
type: 'pong',
date: skewer.now() / 1000,
value: request.eval
};
};
/**
* Establish a new stylesheet with the provided value.
*/
skewer.fn.css = function(request) {
var style = document.createElement('style');
style.type = 'text/css';
style.className = 'skewer';
if (style.styleSheet) { // < IE9
style.styleSheet.cssText = request.eval;
} else {
style.appendChild(document.createTextNode(request.eval));
}
document.body.appendChild(style);
return {};
};
/**
* Remove all of Skewer's style tags from the document.
*/
skewer.fn.cssClearAll = function(request) {
var styles = document.body.querySelectorAll('style.skewer');
for (var i = 0; i < styles.length; i++) {
styles[i].parentNode.removeChild(styles[i]);
}
return {};
};
/**
* HTML evaluator, appends or replaces a selection with given HTML.
*/
skewer.fn.html = function(request) {
function buildSelector(ancestry) {
return ancestry.map(function(tag) {
return tag[0] + ':nth-of-type(' + tag[1] + ')';
}).join(' > ');
}
function query(ancestry) {
return document.querySelector(buildSelector(ancestry));
}
function htmlToNode(html) {
var wrapper = document.createElement('div');
wrapper.innerHTML = html;
return wrapper.firstChild;
}
var target = query(request.ancestry);
if (target == null) {
/* Determine missing part of the ancestry. */
var path = request.ancestry.slice(0); // copy
var missing = [];
while (query(path) == null) {
missing.push(path.pop());
}
/* Build up the missing elements. */
target = query(path);
while (missing.length > 0) {
var tag = missing.pop(),
name = tag[0],
nth = tag[1];
var empty = null;
var count = target.querySelectorAll(name).length;
for (; count < nth; count++) {
empty = document.createElement(tag[0]);
target.appendChild(empty);
}
target = empty;
}
}
target.parentNode.replaceChild(htmlToNode(request.eval), target);
return {};
};
/**
* Fetch the HTML contents of selector.
*/
skewer.fn.fetchselector = function(request) {
var element = document.querySelector(request.eval);
return { value: element.innerHTML };
};
/**
* Return a list of completions for an object.
*/
skewer.fn.completions = function(request) {
var object = skewer.globalEval(request.eval);
var keys = new Set();
var regex = new RegExp(request.regexp);
for (var key in object) {
if (regex.test(key)) {
keys.add(key);
}
}
var props = object != null ? Object.getOwnPropertyNames(object) : [];
for (var i = 0; i < props.length; i++) {
if (regex.test(props[i])) {
keys.add(props[i]);
}
}
return { value: Array.from(keys).sort() };
};
/**
* Host of the skewer script (CORS support).
* @type string
*/
(function() {
var script = document.querySelector('script[src$="/skewer"]');
if (script) {
skewer.host = script.src.match(/\w+:\/\/[^/]+/)[0];
} else {
skewer.host = ''; // default to the current host
}
}());
/**
* Stringify a potentially circular object without throwing an exception.
* @param object The object to be printed.
* @param {boolean} verbose Enable more verbose output.
* @returns {string} The printed object.
*/
skewer.safeStringify = function (object, verbose) {
"use strict";
var circular = "#<Circular>";
var seen = [];
var stringify = function(obj) {
if (obj === true) {
return "true";
} else if (obj === false) {
return "false";
} else if (obj === undefined) {
return "undefined";
} else if (obj === null) {
return "null";
} else if (typeof obj === "number") {
return obj.toString();
} else if (obj instanceof Array) {
if (seen.indexOf(obj) >= 0) {
return circular;
} else {
seen.push(obj);
return "[" + obj.map(function(e) {
return stringify(e);
}).join(", ") + "]";
}
} else if (typeof obj === "string") {
return JSON.stringify(obj);
} else if (window.Node != null && obj instanceof Node) {
return obj.toString(); // DOM elements can't stringify
} else if (typeof obj === "function") {
if (verbose)
return obj.toString();
else
return "Function";
} else if (Object.prototype.toString.call(obj) === '[object Date]') {
if (verbose)
return JSON.stringify(obj);
else
return obj.toString();
} else {
if (verbose) {
if (seen.indexOf(obj) >= 0)
return circular;
else
seen.push(obj);
var pairs = [];
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
var pair = JSON.stringify(key) + ":";
pair += stringify(obj[key]);
pairs.push(pair);
}
}
return "{" + pairs.join(',') + "}";
} else {
try {
return obj.toString();
} catch (error) {
return ({}).toString();
}
}
}
};
try {
return stringify(object);
} catch (error) {
return skewer.safeStringify(object, false);
}
};
/**
* Log an object to the Skewer REPL in Emacs (console.log).
* @param message The object to be logged.
*/
skewer.log = function() {
"use strict";
for (var i = 0; i < arguments.length; i++) {
var log = {
type: "log",
value: skewer.safeStringify(arguments[i], true)
};
skewer.postJSON(skewer.host + "/skewer/post", log);
}
};
/**
* Report an error event to the REPL.
* @param event An error event object.
*/
skewer.error = function(event) {
"use strict";
var log = {
type: "error",
value: event.message,
filename: event.filename,
line: event.lineno,
column: event.column
};
skewer.postJSON(skewer.host + "/skewer/post", log);
};
/**
* Prepare a result when an error occurs evaluating Javascript code.
* @param error The error object given by catch.
* @param result The resutl object to return to Emacs.
* @param request The request object from Emacs.
* @return The result object to send back to Emacs.
*/
skewer.errorResult = function(error, result, request) {
"use strict";
return skewer.extend({}, result, {
value: error.toString(),
status: 'error',
error: {
name: error.name,
stack: error.stack,
type: error.type,
message: error.message,
eval: request.eval
}
});
};
if (window.addEventListener) {
window.addEventListener('error', skewer.error);
if (document.readyState === 'complete') {
skewer();
} else {
window.addEventListener('load', skewer);
}
} else { // < IE9
window.attachEvent('onerror', skewer.error);
if (document.readyState === 'complete') {
skewer();
} else {
window.attachEvent('onload', skewer);
}
}