├── .gitignore ├── Makefile ├── README.md ├── UNLICENSE ├── cache-table.el ├── example.html ├── skewer-bower.el ├── skewer-css.el ├── skewer-everything.user.js ├── skewer-html.el ├── skewer-mode-pkg.el ├── skewer-mode.el ├── skewer-repl.el ├── skewer-setup.el └── skewer.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | *.tar 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Clone the dependencies of this package in sibling directories: 2 | # $ git clone https://github.com/mooz/js2-mode ../js2-mode 3 | # $ git clone https://github.com/skeeto/emacs-web-server ../simple-httpd 4 | # 5 | # Or set LDFLAGS to point at these packages elsewhere: 6 | # $ make LDFLAGS='-L path/to/js2-mode -L path/to/simple-httpd' 7 | .POSIX: 8 | .SUFFIXES: .el .elc 9 | 10 | EMACS = emacs 11 | LDFLAGS = -L ../simple-httpd -L ../js2-mode 12 | BATCH = $(EMACS) -Q -batch -L . $(LDFLAGS) 13 | COMPILE = $(BATCH) -f batch-byte-compile 14 | VERSION = 1.8.0 15 | 16 | EL = skewer-mode.el skewer-setup.el cache-table.el \ 17 | skewer-bower.el skewer-css.el skewer-html.el skewer-repl.el 18 | ELC = $(EL:.el=.elc) 19 | PKG = skewer-mode-pkg.el 20 | DIST = $(PKG) $(EL) skewer.js example.html README.md UNLICENSE 21 | 22 | compile: $(ELC) 23 | all: compile package 24 | package: skewer-mode-$(VERSION).tar 25 | 26 | skewer-mode-$(VERSION): $(DIST) 27 | mkdir -p $@ 28 | cp $(DIST) $@/ 29 | touch $@/ 30 | 31 | skewer-mode-$(VERSION).tar: skewer-mode-$(VERSION) 32 | tar cf $@ skewer-mode-$(VERSION)/ 33 | 34 | compile: $(ELC) 35 | 36 | run: compile 37 | $(EMACS) -Q -L . $(LDFLAGS) \ 38 | -l skewer-mode.elc -l skewer-setup.elc -f skewer-setup \ 39 | --eval "(setf initial-scratch-message nil)" \ 40 | -f js2-mode -f run-skewer 41 | 42 | clean: 43 | rm -rf skewer-mode-$(VERSION) skewer-mode-$(VERSION).tar $(ELC) 44 | 45 | .el.elc: 46 | $(COMPILE) $< 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Skewer: live web development with Emacs 2 | 3 | Provides live interaction with JavaScript, CSS, and HTML in a web 4 | browser. Expressions are sent on-the-fly from an editing buffer to be 5 | evaluated in the browser, just like Emacs does with an inferior Lisp 6 | process in Lisp modes. 7 | 8 | * Watch the [**demo video** on YouTube](http://youtu.be/4tyTgyzUJqM) 9 | ([webm](http://nullprogram.s3.amazonaws.com/skewer/demo.webm)) 10 | 11 | **Skewer is available from [MELPA][melpa]**, which will install the 12 | dependencies for you. This package and its dependencies are pure 13 | Elisp, meaning setup is a breeze, the whole thing is highly portable, 14 | and it works with many browsers. 15 | 16 | Dependencies: 17 | 18 | * [simple-httpd][simple-httpd] (available on MELPA) 19 | * [js2-mode][js2-mode] (available on ELPA) 20 | 21 | Skewer requires Emacs 24.3 or later. 22 | 23 | ## Usage 24 | 25 | ### Quick version 26 | 27 | If Skewer was installed from MELPA, skip to step 3. 28 | 29 | 1. Put this repository directory in your `load-path` 30 | 2. Load skewer-mode.el 31 | 3. M-x `run-skewer` to attach a browser to Emacs 32 | 4. From a `js2-mode` buffer with `skewer-mode` minor mode enabled, 33 | send forms to the browser to evaluate 34 | 35 | The function `skewer-setup` can be used to configure all of mode hooks 36 | (previously this was the default). This can also be done manually like 37 | so, 38 | 39 | ```el 40 | (add-hook 'js2-mode-hook 'skewer-mode) 41 | (add-hook 'css-mode-hook 'skewer-css-mode) 42 | (add-hook 'html-mode-hook 'skewer-html-mode) 43 | ``` 44 | 45 | The keybindings for evaluating expressions in the browser are just 46 | like the Lisp modes. These are provided by the minor mode 47 | `skewer-mode`. 48 | 49 | * C-x C-e: Evaluate the form before the point and display 50 | the result in the minibuffer. If given a prefix argument, insert 51 | the result into the current buffer. 52 | * C-M-x: Evaluate the top-level form around the point. 53 | * C-c C-k: Load the current buffer. 54 | * C-c C-z: Select the REPL buffer. 55 | 56 | The result of the expression is echoed in the minibuffer. 57 | 58 | Additionally, `css-mode` and `html-mode` get similar sets of bindings 59 | for modifying the CSS rules and HTML on the current page. 60 | 61 | #### CSS 62 | 63 | * C-x C-e: Load the declaration at the point. 64 | * C-M-x: Load the entire rule around the point. 65 | * C-c C-k: Load the current buffer as a stylesheet. 66 | 67 | #### HTML 68 | 69 | * C-M-x: Load the HTML tag immediately around the point. 70 | 71 | Note: `run-skewer` uses `browse-url` to launch the browser. This may 72 | require further setup depending on your operating system and personal 73 | preferences. 74 | 75 | Multiple browsers and browser tabs can be attached to Emacs at once. 76 | JavaScript forms are sent to all attached clients simultaneously, and 77 | each will echo back the result individually. Use `list-skewer-clients` 78 | to see a list of all currently attached clients. 79 | 80 | Sometimes Skewer's long polls from the browser will timeout after a 81 | number of hours of inactivity. If you find the browser disconnected 82 | from Emacs for any reason, use the browser's console to call 83 | `skewer()` to reconnect. This avoids a page reload, which would lose 84 | any fragile browser state you might care about. 85 | 86 | ### Manual version 87 | 88 | To skewer your own document rather than the provided blank one, 89 | 90 | 1. Load the dependencies 91 | 2. Load `skewer-mode.el` 92 | 3. Start the HTTP server (`httpd-start`) 93 | 4. Include "http://localhost:8080/skewer" as a script 94 | (see example.html and check your `httpd-port`) 95 | 5. Visit the document from your browser 96 | 97 | Skewer fully supports CORS so the document need not be hosted by Emacs 98 | itself. A Greasemonkey userscript is provided, *Skewer Everything*, 99 | for injecting Skewer into any arbitrary page you're visiting without 100 | needing to modify the page on the host. More information below. 101 | 102 | **Don't copy `skewer.js` anywhere or use it directly**. Emacs hosts 103 | this script itself, manipulating it in memory before it reaches the 104 | browser. Always access it through the servlet on the Emacs webserver 105 | as `/skewer`. 106 | 107 | ### Browser Support 108 | 109 | Skewer is known to work properly with Firefox, Chrome, Safari, Opera, 110 | and IE8+. Except for CSS and HTML, Skewer will work in IE7 when 111 | `document.querySelector` and `JSON` are polyfilled. If you find any 112 | other JavaScript-supported browser that doesn't work with Skewer, 113 | please report it. 114 | 115 | ## REPL 116 | 117 | A REPL into the browser can be created with `M-x skewer-repl`, or 118 | C-c C-z. This should work like a console within the 119 | browser. Messages can be logged to this REPL with `skewer.log()` (like 120 | `console.log()`). 121 | 122 | Results of expressions evaluated in the REPL are printed more 123 | verbosely than in the minibuffer, when possible. This may help in 124 | debugging. 125 | 126 | ## Skewering with CORS 127 | 128 | Skewer supports [Cross-origin Resource Sharing (CORS)][cors]. This 129 | means you can Skewer a document hosted from any server without needing 130 | any special changes on that server, except for including `/skewer` as 131 | a script in that document. 132 | 133 | If you don't control the server from which you want to skewer pages -- 134 | such that you can't add the Skewer's script -- the provided 135 | Greasemonkey userscript (`.user.js`) can be used to inject it into any 136 | page you visit. Note that this userscript will assume you're running 137 | the Skewer server at http://localhost:8080/ (simple-httpd's default 138 | port). If this isn't true, you need to edit the top of the userscript. 139 | 140 | The script isn't actually injected until you switch the toggle in the 141 | top-right corner, the red/green triangle. 142 | 143 | Alternatively, the following bookmarklet will load skewer on demand: 144 | 145 | ```js 146 | javascript:(function(){var d=document;var s=d.createElement('script');s.src='http://localhost:8080/skewer';d.body.appendChild(s);})() 147 | ``` 148 | 149 | With a browser plugin like 150 | [Custom Javascript for Websites](https://chrome.google.com/webstore/detail/custom-javascript-for-web/poakhlngfciodnhlhhgnaaelnpjljija?hl=en), 151 | you can use the bookmarklet to auto-skewer specific domains, saving you a 152 | mouse click on each reload. 153 | 154 | ## bower 155 | 156 | Also provided are some functions for loading libraries from the bower 157 | infrastructure on the fly. This is accessed with `skewer-bower-load`. 158 | For example, I often find it useful to load jQuery when skewering a 159 | page that doesn't have jQuery installed. 160 | 161 | Note: to use this **bower does *not* need to be installed**, only git. 162 | It's just the bower infrastructure being used. Unfortunately this 163 | infrastructure is a mess right now; many packages are in some sort of 164 | broken state -- missing dependencies, missing metadata, broken 165 | metadata, or an invalid repository URL. Some of this is due to 166 | under-specification of the metadata by the bower project. 167 | 168 | ## Motivation 169 | 170 | I wanted something like [swank-js][swank-js] but without all the 171 | painful setup. Having already written an Emacs web server I was 172 | halfway there. It took relatively little code to accomplish. 173 | 174 | I also didn't want to rely a browser-specific feature, like MozRepl or 175 | WebKit's remote debugger ([kite][kite]). 176 | 177 | The name refers to the idea that Emacs is *skewering* the browser from 178 | server-side. 179 | 180 | [simple-httpd]: https://github.com/skeeto/emacs-http-server 181 | [js2-mode]: https://github.com/mooz/js2-mode 182 | [melpa]: https://melpa.org/ 183 | [swank-js]: https://github.com/swank-js/swank-js 184 | [cors]: http://en.wikipedia.org/wiki/Cross-origin_resource_sharing 185 | [kite]: https://github.com/jscheid/kite 186 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /cache-table.el: -------------------------------------------------------------------------------- 1 | ;;; cache-table.el --- a hash table with expiring entries -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;; Author: Christopher Wellons 6 | ;; Version: 1.0 7 | 8 | ;;; Commentary: 9 | 10 | ;; See the docstring of `cache-table-create'. There is no 11 | ;; `cache-table-put': use `setf' on `cache-table-get' instead. 12 | 13 | ;;; Code: 14 | 15 | (require 'cl-lib) 16 | 17 | (cl-defstruct (cache-table (:constructor cache-table--create)) 18 | "A cache table with expiring entries." 19 | expire-time table) 20 | 21 | (defun cache-table-create (expire-time &rest keyword-args) 22 | "Create a new cache-table with entries automatically removed 23 | from the table after EXPIRE-TIME seconds. This function accepts 24 | the same keyword arguments as `make-hash-table'. Entries are not 25 | actually removed from the cache-table until an access is made to 26 | the cache-table. 27 | 28 | Use `cache-table-get' to get and put (via setf) entries." 29 | (cache-table--create :expire-time expire-time 30 | :table (apply #'make-hash-table keyword-args))) 31 | 32 | (defun cache-table-clear-expired (cache-table) 33 | "Remove all expired entries from CACHE-TABLE." 34 | (cl-loop with expire-time = (cache-table-expire-time cache-table) 35 | with table = (cache-table-table cache-table) 36 | with dead-time = (- (float-time) expire-time) 37 | for key being the hash-keys of table using (hash-value entry) 38 | for (time . value) = entry 39 | when (< time dead-time) do (remhash key table))) 40 | 41 | (defun cache-table-get (key cache-table &optional default) 42 | "Access the value for KEY in CACHE-TABLE if it has not yet 43 | expired. Behaves just like `gethash'." 44 | (cache-table-clear-expired cache-table) 45 | (cdr (gethash key (cache-table-table cache-table) (cons 0 default)))) 46 | 47 | (gv-define-setter cache-table-get (value key cache-table) 48 | "Put an entry in the hash table, like (setf (gethash key table) value)." 49 | `(progn 50 | (cache-table-clear-expired ,cache-table) 51 | (puthash ,key (cons (float-time) ,value) 52 | (cache-table-table ,cache-table)))) 53 | 54 | (defun cache-table-map (f cache-table) 55 | "Like `maphash', call F for all non-expired entries in CACHE-TABLE." 56 | (cache-table-clear-expired cache-table) 57 | (maphash (lambda (k v) (funcall f k (cdr v))) 58 | (cache-table-table cache-table))) 59 | 60 | (defun cache-table-count (cache-table) 61 | "Like `hash-table-count', count the number of non-expired entries." 62 | (hash-table-count (cache-table-table cache-table))) 63 | 64 | (provide 'cache-table) 65 | 66 | ;;; cache-table.el ends here 67 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Skewer 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /skewer-bower.el: -------------------------------------------------------------------------------- 1 | ;;; skewer-bower.el --- dynamic library loading -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;;; Commentary: 6 | 7 | ;; This package loads libraries into the current page using the bower 8 | ;; infrastructure. Note: bower is not actually used by this package 9 | ;; and so does *not* need to be installed. Only git is required (see 10 | ;; `skewer-bower-git-executable'). It will try to learn how to run git 11 | ;; from Magit if available. 12 | 13 | ;; The interactive command for loading libraries is 14 | ;; `skewer-bower-load'. It will prompt for a library and a version, 15 | ;; automatically fetching it from the bower infrastructure if needed. 16 | ;; For example, I often find it handy to load some version of jQuery 17 | ;; when poking around at a page that doesn't already have it loaded. 18 | 19 | ;; Caveat: unfortunately the bower infrastructure is a mess; many 20 | ;; packages are in some sort of broken state -- missing dependencies, 21 | ;; missing metadata, broken metadata, or an invalid repository URL. 22 | ;; Some of this is due to under-specification of the metadata by the 23 | ;; bower project. Broken packages are unlikely to be loadable by 24 | ;; skewer-bower. 25 | 26 | ;;; Code: 27 | 28 | (require 'cl-lib) 29 | (require 'skewer-mode) 30 | (require 'simple-httpd) 31 | (require 'magit nil t) ; optional 32 | 33 | (defcustom skewer-bower-cache-dir (locate-user-emacs-file "skewer-cache") 34 | "Location of library cache (git repositories)." 35 | :type 'string 36 | :group 'skewer) 37 | 38 | (defcustom skewer-bower-endpoint "https://bower.herokuapp.com" 39 | "Endpoint for accessing package information." 40 | :type 'string 41 | :group 'skewer) 42 | 43 | (defcustom skewer-bower-json '("bower.json" "package.json" "component.json") 44 | "Files to search for package metadata." 45 | :type 'list 46 | :group 'skewer) 47 | 48 | ; Try to match Magit's configuration if available 49 | (defcustom skewer-bower-git-executable "git" 50 | "Name of the git executable." 51 | :type 'string 52 | :group 'skewer) 53 | 54 | (defvar skewer-bower-packages nil 55 | "Alist of all packages known to bower.") 56 | 57 | (defvar skewer-bower-refreshed nil 58 | "List of packages that have been refreshed recently. This keeps 59 | them from hitting the network frequently.") 60 | 61 | ;;;###autoload 62 | (defun skewer-bower-refresh () 63 | "Update the package listing and packages synchronously." 64 | (interactive) 65 | (cl-declare (special url-http-end-of-headers)) 66 | (setf skewer-bower-refreshed nil) 67 | (with-current-buffer 68 | (url-retrieve-synchronously (concat skewer-bower-endpoint "/packages")) 69 | (setf (point) url-http-end-of-headers) 70 | (setf skewer-bower-packages 71 | (cl-sort 72 | (cl-loop for package across (json-read) 73 | collect (cons (cdr (assoc 'name package)) 74 | (cdr (assoc 'url package)))) 75 | #'string< :key #'car)))) 76 | 77 | ;; Git functions 78 | 79 | (defun skewer-bower-cache (package) 80 | "Return the cache repository directory for PACKAGE." 81 | (unless (file-exists-p skewer-bower-cache-dir) 82 | (make-directory skewer-bower-cache-dir t)) 83 | (expand-file-name package skewer-bower-cache-dir)) 84 | 85 | (defun skewer-bower-git (package &rest args) 86 | "Run git for PACKAGE's repository with ARGS." 87 | (with-temp-buffer 88 | (when (zerop (apply #'call-process skewer-bower-git-executable nil t nil 89 | (format "--git-dir=%s" (skewer-bower-cache package)) 90 | args)) 91 | (buffer-string)))) 92 | 93 | (defun skewer-bower-git-clone (url package) 94 | "Clone or fetch PACKAGE's repository from URL if needed." 95 | (if (member package skewer-bower-refreshed) 96 | t 97 | (let* ((cache (skewer-bower-cache package)) 98 | (status 99 | (if (file-exists-p cache) 100 | (when (skewer-bower-git package "fetch") 101 | (push package skewer-bower-refreshed)) 102 | (skewer-bower-git package "clone" "--bare" url cache)))) 103 | (not (null status))))) 104 | 105 | (defun skewer-bower-git-show (package version file) 106 | "Grab FILE from PACKAGE at version VERSION." 107 | (when (string-match-p "^\\./" file) ; avoid relative paths 108 | (setf file (substring file 2))) 109 | (skewer-bower-git package "show" (format "%s:%s" version file))) 110 | 111 | (defun skewer-bower-git-tag (package) 112 | "List all the tags in PACKAGE's repository." 113 | (split-string (skewer-bower-git package "tag"))) 114 | 115 | ;; Bower functions 116 | 117 | (defun skewer-bower-package-ensure (package) 118 | "Ensure a package is installed in the cache and up to date. 119 | Emit an error if the package could not be ensured." 120 | (when (null skewer-bower-packages) (skewer-bower-refresh)) 121 | (let ((url (cdr (assoc package skewer-bower-packages)))) 122 | (when (null url) 123 | (error "Unknown package: %s" package)) 124 | (when (null (skewer-bower-git-clone url package)) 125 | (error "Failed to fetch: %s" url)) 126 | t)) 127 | 128 | (defun skewer-bower-package-versions (package) 129 | "List the available versions for a package. Always returns at 130 | least one version." 131 | (skewer-bower-package-ensure package) 132 | (or (sort (skewer-bower-git-tag package) #'string<) 133 | (list "master"))) 134 | 135 | (defun skewer-bower-get-config (package &optional version) 136 | "Get the configuration alist for PACKAGE at VERSION. Return nil 137 | if no configuration could be found." 138 | (skewer-bower-package-ensure package) 139 | (unless version (setf version "master")) 140 | (json-read-from-string 141 | (cl-loop for file in skewer-bower-json 142 | for config = (skewer-bower-git-show package version file) 143 | when config return it 144 | finally (return "null")))) 145 | 146 | ;; Serving the library 147 | 148 | (defvar skewer-bower-history () 149 | "Library selection history for `completing-read'.") 150 | 151 | (defun skewer-bowser--path (package version main) 152 | "Return the simple-httpd hosted path for PACKAGE." 153 | (format "/skewer/bower/%s/%s/%s" package (or version "master") main)) 154 | 155 | (defun skewer-bower-prompt-package () 156 | "Prompt for a package and version from the user." 157 | (when (null skewer-bower-packages) (skewer-bower-refresh)) 158 | ;; ido-completing-read bug workaround: 159 | (when (> (length skewer-bower-history) 32) 160 | (setf skewer-bower-history (cl-subseq skewer-bower-history 0 16))) 161 | (let* ((packages (mapcar #'car skewer-bower-packages)) 162 | (selection (nconc skewer-bower-history packages)) 163 | (package (completing-read "Library: " selection nil t nil 164 | 'skewer-bower-history)) 165 | (versions (reverse (skewer-bower-package-versions package))) 166 | (version (completing-read "Version: " versions 167 | nil t nil nil (car versions)))) 168 | (list package version))) 169 | 170 | (defun skewer-bower--js-p (filename) 171 | "Return non-nil if FILENAME looks like JavaScript." 172 | (string-match "\\.js$" filename)) 173 | 174 | (defun skewer-bower-guess-main (package version config) 175 | "Attempt to determine the main entrypoints from a potentially 176 | incomplete or incorrect bower configuration. Returns nil if 177 | guessing failed." 178 | (let ((check (apply-partially #'skewer-bower-git-show package version)) 179 | (main (cdr (assoc 'main config)))) 180 | (cond ((and (vectorp main) (cl-some check main)) 181 | (cl-coerce (cl-remove-if-not #'skewer-bower--js-p main) 'list)) 182 | ((and (stringp main) (funcall check main)) 183 | (list main)) 184 | ((funcall check (concat package ".js")) 185 | (list (concat package ".js"))) 186 | ((funcall check package) 187 | (list package))))) 188 | 189 | ;;;###autoload 190 | (defun skewer-bower-load (package &optional version) 191 | "Dynamically load a library from bower into the current page." 192 | (interactive (skewer-bower-prompt-package)) 193 | (let* ((config (skewer-bower-get-config package version)) 194 | (deps (cdr (assoc 'dependencies config))) 195 | (main (skewer-bower-guess-main package version config))) 196 | (when (null main) 197 | (error "Could not load %s (%s): no \"main\" entrypoint specified" 198 | package version)) 199 | (cl-loop for (dep . version) in deps 200 | do (skewer-bower-load (format "%s" dep) version)) 201 | (cl-loop for entrypoint in main 202 | for path = (skewer-bowser--path package version entrypoint) 203 | do (skewer-eval path nil :type "script")))) 204 | 205 | (defservlet skewer/bower "application/javascript; charset=utf-8" (path) 206 | "Serve a script from the local bower repository cache." 207 | (cl-destructuring-bind (_ _skewer _bower package version . parts) 208 | (split-string path "/") 209 | (let* ((file (mapconcat #'identity parts "/")) 210 | (contents (skewer-bower-git-show package version file))) 211 | (if contents 212 | (insert contents) 213 | (httpd-error t 404))))) 214 | 215 | (provide 'skewer-bower) 216 | 217 | ;;; skewer-bower.el ends here 218 | -------------------------------------------------------------------------------- /skewer-css.el: -------------------------------------------------------------------------------- 1 | ;;; skewer-css.el --- skewer support for live-interaction CSS -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;;; Commentary: 6 | 7 | ;; This minor mode provides functionality for CSS like plain Skewer 8 | ;; does for JavaScript. 9 | 10 | ;; * C-x C-e -- `skewer-css-eval-current-declaration' 11 | ;; * C-M-x -- `skewer-css-eval-current-rule' 12 | ;; * C-c C-k -- `skewer-css-eval-buffer' 13 | 14 | ;; These functions assume there are no comments within a CSS rule, 15 | ;; *especially* not within a declaration. In the former case, if you 16 | ;; keep the comment free of CSS syntax it should be able to manage 17 | ;; reasonably well. This may be fixed someday. 18 | 19 | ;;; Code: 20 | 21 | (require 'css-mode) 22 | (require 'skewer-mode) 23 | 24 | (defun skewer-css-trim (string) 25 | "Trim and compress whitespace in the string." 26 | (let ((cleaned (replace-regexp-in-string "[\t\n ]+" " " string))) 27 | (replace-regexp-in-string "^[\t\n ]+\\|[\t\n ]+$" "" cleaned))) 28 | 29 | ;; Parsing 30 | 31 | (defun skewer-css-beginning-of-rule () 32 | "Move to the beginning of the current rule and return point." 33 | (skewer-css-end-of-rule) 34 | (re-search-backward "{") 35 | (when (re-search-backward "[}/]" nil 'start) 36 | (forward-char)) 37 | (re-search-forward "[^ \t\n]") 38 | (backward-char) 39 | (point)) 40 | 41 | (defun skewer-css-end-of-rule () 42 | "Move to the end of the current rule and return point." 43 | (if (eql (char-before) ?}) 44 | (point) 45 | (re-search-forward "}"))) 46 | 47 | (defun skewer-css-end-of-declaration () 48 | "Move to the end of the current declaration and return point." 49 | (if (eql (char-before) ?\;) 50 | (point) 51 | (re-search-forward ";"))) 52 | 53 | (defun skewer-css-beginning-of-declaration () 54 | "Move to the end of the current declaration and return point." 55 | (skewer-css-end-of-declaration) 56 | (re-search-backward ":") 57 | (backward-sexp 1) 58 | (point)) 59 | 60 | (defun skewer-css-selectors () 61 | "Return the selectors for the current rule." 62 | (save-excursion 63 | (let ((start (skewer-css-beginning-of-rule)) 64 | (end (1- (re-search-forward "{")))) 65 | (skewer-css-trim 66 | (buffer-substring-no-properties start end))))) 67 | 68 | (defun skewer-css-declaration () 69 | "Return the current declaration as a pair of strings." 70 | (save-excursion 71 | (let ((start (skewer-css-beginning-of-declaration)) 72 | (end (skewer-css-end-of-declaration))) 73 | (let* ((clip (buffer-substring-no-properties start end)) 74 | (pair (split-string clip ":"))) 75 | (mapcar #'skewer-css-trim pair))))) 76 | 77 | ;; Evaluation 78 | 79 | (defun skewer-css (rule) 80 | "Add RULE as a new stylesheet." 81 | (skewer-eval rule nil :type "css")) 82 | 83 | (defun skewer-css-eval-current-declaration () 84 | "Evaluate the declaration at the point." 85 | (interactive) 86 | (save-excursion 87 | (let ((selectors (skewer-css-selectors)) 88 | (rule (skewer-css-declaration)) 89 | (start (skewer-css-beginning-of-declaration)) 90 | (end (skewer-css-end-of-declaration))) 91 | (skewer-flash-region start end) 92 | (skewer-css (apply #'format "%s { %s: %s }" selectors rule))))) 93 | 94 | (defun skewer-css-eval-current-rule () 95 | "Evaluate the rule at the point." 96 | (interactive) 97 | (save-excursion 98 | (let* ((start (skewer-css-beginning-of-rule)) 99 | (end (skewer-css-end-of-rule)) 100 | (rule (buffer-substring-no-properties start end))) 101 | (skewer-flash-region start end) 102 | (skewer-css (skewer-css-trim rule))))) 103 | 104 | (defun skewer-css-eval-buffer () 105 | "Send the entire current buffer as a new stylesheet." 106 | (interactive) 107 | (skewer-css (buffer-substring-no-properties (point-min) (point-max)))) 108 | 109 | (defun skewer-css-clear-all () 110 | "Remove *all* Skewer-added styles from the document." 111 | (interactive) 112 | (skewer-eval nil nil :type "cssClearAll")) 113 | 114 | ;; Minor mode definition 115 | 116 | (defvar skewer-css-mode-map 117 | (let ((map (make-sparse-keymap))) 118 | (prog1 map 119 | (define-key map (kbd "C-x C-e") 'skewer-css-eval-current-declaration) 120 | (define-key map (kbd "C-M-x") 'skewer-css-eval-current-rule) 121 | (define-key map (kbd "C-c C-k") 'skewer-css-eval-buffer) 122 | (define-key map (kbd "C-c C-c") 'skewer-css-clear-all))) 123 | "Keymap for skewer-css-mode.") 124 | 125 | ;;;###autoload 126 | (define-minor-mode skewer-css-mode 127 | "Minor mode for interactively loading new CSS rules." 128 | :lighter " skewer-css" 129 | :keymap skewer-css-mode-map 130 | :group 'skewer) 131 | 132 | (provide 'skewer-css) 133 | 134 | ;;; skewer-css.el ends here 135 | -------------------------------------------------------------------------------- /skewer-everything.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Skewer Everything 3 | // @description Add a toggle button to run Skewer on the current page 4 | // @lastupdated 2015-09-14 5 | // @version 1.3 6 | // @license Public Domain 7 | // @include /^https?:/// 8 | // @grant none 9 | // @run-at document-start 10 | // ==/UserScript== 11 | 12 | window.skewerNativeXHR = XMLHttpRequest; 13 | window.skewerInject = inject; 14 | 15 | var host = 'http://localhost:8080'; 16 | 17 | var toggle = document.createElement('div'); 18 | toggle.onclick = inject; 19 | toggle.style.width = '0px'; 20 | toggle.style.height = '0px'; 21 | toggle.style.borderStyle = 'solid'; 22 | toggle.style.borderWidth = '0 12px 12px 0'; 23 | toggle.style.borderColor = 'transparent #F00 transparent transparent'; 24 | toggle.style.position = 'fixed'; 25 | toggle.style.right = 0; 26 | toggle.style.top = 0; 27 | toggle.style.zIndex = 214748364; 28 | 29 | var injected = false; 30 | 31 | function inject() { 32 | if (!injected) { 33 | var script = document.createElement('script'); 34 | script.src = host + '/skewer'; 35 | document.body.appendChild(script); 36 | toggle.style.borderRightColor = '#0F0'; 37 | } else { 38 | /* break skewer to disable it */ 39 | skewer.fn = null; 40 | toggle.style.borderRightColor = '#F00'; 41 | } 42 | injected = !injected; 43 | localStorage._autoskewered = JSON.stringify(injected); 44 | } 45 | 46 | document.addEventListener('DOMContentLoaded', function() { 47 | /* Don't use on iframes. */ 48 | if (window.top === window.self) { 49 | document.body.appendChild(toggle); 50 | if (JSON.parse(localStorage._autoskewered || 'false')) { 51 | inject(); 52 | } 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /skewer-html.el: -------------------------------------------------------------------------------- 1 | ;;; skewer-html.el --- skewer support for live-interaction HTML -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;;; Commentary: 6 | 7 | ;; This minor mode provides functionality for HTML like plain Skewer 8 | ;; does for JavaScript. There's no clean way to replace the body and 9 | ;; head elements of a live document, so "evaluating" these elements is 10 | ;; not supported. 11 | 12 | ;; * C-M-x -- `skewer-html-eval-tag' 13 | 14 | ;; See also `skewer-html-fetch-selector-into-buffer' for grabbing the 15 | ;; page as it current exists. 16 | 17 | ;;; Code: 18 | 19 | (require 'cl-lib) 20 | (require 'sgml-mode) 21 | (require 'skewer-mode) 22 | 23 | ;; Macros 24 | 25 | (defmacro skewer-html--with-html-mode (&rest body) 26 | "Evaluate BODY as if in `html-mode', using a temp buffer if necessary." 27 | (declare (indent 0)) 28 | (let ((orig-buffer (make-symbol "orig-buffer")) 29 | (temp-buffer (make-symbol "temp-buffer")) 30 | (orig-point (make-symbol "orig-point"))) 31 | `(let ((,temp-buffer (and (not (eq major-mode 'html-mode)) 32 | (generate-new-buffer " *skewer-html*"))) 33 | (,orig-buffer (current-buffer)) 34 | (,orig-point (point))) 35 | (unwind-protect 36 | (with-current-buffer (or ,temp-buffer ,orig-buffer) 37 | (when ,temp-buffer 38 | (insert-buffer-substring ,orig-buffer) 39 | (setf (point) ,orig-point) 40 | (html-mode)) 41 | ,@body) 42 | (when ,temp-buffer 43 | (kill-buffer ,temp-buffer)))))) 44 | 45 | ;; Selector computation 46 | 47 | (defun skewer-html--cleanup (tag) 48 | "Cleanup TAG name from sgml-mode." 49 | (skewer-html--with-html-mode 50 | (replace-regexp-in-string "/$" "" (sgml-tag-name tag)))) 51 | 52 | (defun skewer-html--tag-after-point () 53 | "Return the tag struct for the tag immediately following point." 54 | (skewer-html--with-html-mode 55 | (save-excursion 56 | (forward-char 1) 57 | (sgml-parse-tag-backward)))) 58 | 59 | (defun skewer-html--get-context () 60 | "Like `sgml-get-context' but to the root, skipping close tags." 61 | (skewer-html--with-html-mode 62 | (save-excursion 63 | (cl-loop for context = (sgml-get-context) 64 | while context 65 | nconc (nreverse context) into tags 66 | finally return (cl-delete 'close tags :key #'sgml-tag-type))))) 67 | 68 | (cl-defun skewer-html-compute-tag-nth (&optional (point (point))) 69 | "Compute the position of this tag within its parent." 70 | (skewer-html--with-html-mode 71 | (save-excursion 72 | (setf (point) point) 73 | (let ((context (skewer-html--get-context))) 74 | (when context 75 | (let ((tag-name (skewer-html--cleanup (car context))) 76 | (target-depth (1- (length context)))) 77 | (cl-loop with n = 0 78 | ;; If point doesn't move, we're at the root. 79 | for point-start = (point) 80 | do (sgml-skip-tag-backward 1) 81 | until (= (point) point-start) 82 | ;; If depth changed, we're done. 83 | for current-depth = (length (skewer-html--get-context)) 84 | until (< current-depth target-depth) 85 | ;; Examine the sibling tag. 86 | for current-name = (save-excursion 87 | (forward-char) 88 | (sgml-parse-tag-name)) 89 | when (equal current-name tag-name) 90 | do (cl-incf n) 91 | finally return n))))))) 92 | 93 | (defun skewer-html-compute-tag-ancestry () 94 | "Compute the ancestry chain at point." 95 | (skewer-html--with-html-mode 96 | (nreverse 97 | (cl-loop for tag in (skewer-html--get-context) 98 | for nth = (skewer-html-compute-tag-nth (1+ (sgml-tag-start tag))) 99 | for name = (skewer-html--cleanup tag) 100 | unless (equal name "html") 101 | collect (list name nth))))) 102 | 103 | (defun skewer-html-compute-selector () 104 | "Compute the selector for exactly the tag around point." 105 | (let ((ancestry (skewer-html-compute-tag-ancestry))) 106 | (mapconcat (lambda (tag) 107 | (format "%s:nth-of-type(%d)" (cl-first tag) (cl-second tag))) 108 | ancestry " > "))) 109 | 110 | ;; Fetching 111 | 112 | (defun skewer-html-fetch-selector (selector) 113 | "Fetch the innerHTML of a selector." 114 | (let ((result (skewer-eval-synchronously selector :type "fetchselector"))) 115 | (if (skewer-success-p result) 116 | (cdr (assoc 'value result)) 117 | ""))) 118 | 119 | (defun skewer-html-fetch-selector-into-buffer (selector) 120 | "Fetch the innerHTML of a selector and insert it into the active buffer." 121 | (interactive "sSelector: ") 122 | (insert (skewer-html-fetch-selector selector))) 123 | 124 | ;; Evaluation 125 | 126 | (defun skewer-html-eval (string ancestry &optional append) 127 | "Load HTML into a selector, optionally appending." 128 | (let ((ancestry* (cl-coerce ancestry 'vector))) ; for JSON 129 | (skewer-eval string nil :type "html" :extra `((ancestry . ,ancestry*) 130 | (append . ,append))))) 131 | 132 | (defun skewer-html-eval-tag () 133 | "Load HTML from the immediately surrounding tag." 134 | (interactive) 135 | (let ((ancestry (skewer-html-compute-tag-ancestry))) 136 | (save-excursion 137 | ;; Move to beginning of opening tag 138 | (let* ((beg (skewer-html--with-html-mode 139 | (sgml-skip-tag-forward 1) (point))) 140 | (end (skewer-html--with-html-mode 141 | (sgml-skip-tag-backward 1) (point))) 142 | (region (buffer-substring-no-properties beg end))) 143 | (skewer-flash-region beg end) 144 | (if (= (length ancestry) 1) 145 | (error "Error: cannot eval body and head tags.") 146 | (skewer-html-eval region ancestry nil)))))) 147 | 148 | ;; Minor mode definition 149 | 150 | (defvar skewer-html-mode-map 151 | (let ((map (make-sparse-keymap))) 152 | (prog1 map 153 | (define-key map (kbd "C-M-x") 'skewer-html-eval-tag))) 154 | "Keymap for skewer-html-mode") 155 | 156 | ;;;###autoload 157 | (define-minor-mode skewer-html-mode 158 | "Minor mode for interactively loading new HTML." 159 | :lighter " skewer-html" 160 | :keymap skewer-html-mode-map 161 | :group 'skewer) 162 | 163 | (provide 'skewer-html) 164 | 165 | ;;; skewer-html.el ends here 166 | -------------------------------------------------------------------------------- /skewer-mode-pkg.el: -------------------------------------------------------------------------------- 1 | (define-package "skewer-mode" "1.8.0" 2 | "live browser JavaScript, CSS, and HTML interaction" 3 | '((simple-httpd "1.4.0") (js2-mode "20090723") (emacs "24"))) 4 | -------------------------------------------------------------------------------- /skewer-mode.el: -------------------------------------------------------------------------------- 1 | ;;; skewer-mode.el --- live browser JavaScript, CSS, and HTML interaction -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;; Author: Christopher Wellons 6 | ;; URL: https://github.com/skeeto/skewer-mode 7 | 8 | ;;; Commentary: 9 | 10 | ;; Quick start (without package.el): 11 | 12 | ;; 1. Put this directory in your `load-path' 13 | ;; 2. Load skewer-mode.el 14 | ;; 3. M-x `run-skewer' to attach a browser to Emacs 15 | ;; 4. From a `js2-mode' buffer with `skewer-mode' minor mode enabled, 16 | ;; send forms to the browser to evaluate 17 | 18 | ;; The function `skewer-setup' can be used to configure all of mode 19 | ;; hooks (previously this was the default). This can also be done 20 | ;; manually like so, 21 | 22 | ;; (add-hook 'js2-mode-hook 'skewer-mode) 23 | ;; (add-hook 'css-mode-hook 'skewer-css-mode) 24 | ;; (add-hook 'html-mode-hook 'skewer-html-mode) 25 | 26 | ;; The keybindings for evaluating expressions in the browser are just 27 | ;; like the Lisp modes. These are provided by the minor mode 28 | ;; `skewer-mode'. 29 | 30 | ;; * C-x C-e -- `skewer-eval-last-expression' 31 | ;; * C-M-x -- `skewer-eval-defun' 32 | ;; * C-c C-k -- `skewer-load-buffer' 33 | 34 | ;; The result of the expression is echoed in the minibuffer. 35 | 36 | ;; Additionally, `css-mode' and `html-mode' get a similar set of 37 | ;; bindings for modifying the CSS rules and updating HTML on the 38 | ;; current page. 39 | 40 | ;; Note: `run-skewer' uses `browse-url' to launch the browser. This 41 | ;; may require further setup depending on your operating system and 42 | ;; personal preferences. 43 | 44 | ;; Multiple browsers and browser tabs can be attached to Emacs at 45 | ;; once. JavaScript forms are sent to all attached clients 46 | ;; simultaneously, and each will echo back the result 47 | ;; individually. Use `list-skewer-clients' to see a list of all 48 | ;; currently attached clients. 49 | 50 | ;; Sometimes Skewer's long polls from the browser will timeout after a 51 | ;; number of hours of inactivity. If you find the browser disconnected 52 | ;; from Emacs for any reason, use the browser's console to call 53 | ;; skewer() to reconnect. This avoids a page reload, which would lose 54 | ;; any fragile browser state you might care about. 55 | 56 | ;; To skewer your own document rather than the provided blank page, 57 | 58 | ;; 1. Load the dependencies 59 | ;; 2. Load skewer-mode.el 60 | ;; 3. Start the HTTP server (`httpd-start') 61 | ;; 4. Include "http://localhost:8080/skewer" as a script 62 | ;; (see `example.html' and check your `httpd-port') 63 | ;; 5. Visit the document from your browser 64 | 65 | ;; Skewer fully supports CORS, so the document need not be hosted by 66 | ;; Emacs itself. A Greasemonkey userscript and a bookmarklet are 67 | ;; provided for injecting Skewer into any arbitrary page you're 68 | ;; visiting without needing to modify the page on the host. 69 | 70 | ;; With skewer-repl.el loaded, a REPL into the browser can be created 71 | ;; with M-x `skewer-repl', or C-c C-z. This should work like a console 72 | ;; within the browser. Messages can be logged to this REPL with 73 | ;; skewer.log() (just like console.log()). 74 | 75 | ;; Extending Skewer: 76 | 77 | ;; Skewer is flexible and open to extension. The REPL and the CSS and 78 | ;; HTML minor modes are a partial examples of this. You can extend 79 | ;; skewer.js with your own request handlers and talk to them from 80 | ;; Emacs using `skewer-eval' (or `skewer-eval-synchronously') with 81 | ;; your own custom :type. The :type string chooses the dispatch 82 | ;; function under the skewer.fn object. To inject your own JavaScript 83 | ;; into skewer.js, use `skewer-js-hook'. 84 | 85 | ;; You can also catch messages sent from the browser not in response 86 | ;; to an explicit request. Use `skewer-response-hook' to see all 87 | ;; incoming objects. 88 | 89 | ;;; History: 90 | 91 | ;; Version 1.8.0: features 92 | ;; * Work around XMLHttpRequest tampering in userscript 93 | ;; * Add Makefile "run" target for testing 94 | ;; Version 1.7.0: features and fixes 95 | ;; * Support for other major modes (including web-mode) in skewer-html-mode 96 | ;; * Opportunistic support for company-mode completions 97 | ;; * Always serve content as UTF-8 98 | ;; * Improve skewer-everything.js portability 99 | ;; Version 1.6.2: fixes 100 | ;; * skewer.log() takes multiple arguments 101 | ;; * comint and encoding fixes 102 | ;; Version 1.6.1: fixes 103 | ;; * Add `skewer-css-clear-all' 104 | ;; * Better IE8 compatibility 105 | ;; * User interface tweaks 106 | ;; Version 1.6.0: fixes 107 | ;; * Bring up to speed with Emacs 24.3 108 | ;; * Switch to cl-lib from cl 109 | ;; Version 1.5.3: features 110 | ;; * Add `skewer-run-phantomjs' 111 | ;; Version 1.5.2: small cleanup 112 | ;; * Add `skewer-apply' and `skewer-funall' 113 | ;; * Improved safeStringify 114 | ;; Version 1.5.1: features 115 | ;; * No more automatic hook setup (see `skewer-setup') 116 | ;; * Support for HTML interaction 117 | ;; * Support for loading Bower packages 118 | ;; * Drop jQuery dependency 119 | ;; * Many small improvements 120 | ;; Version 1.4: features 121 | ;; * Full CSS interaction 122 | ;; * Greasemonkey userscript for injection 123 | ;; * Full, working CORS support 124 | ;; * Better browser presence detection 125 | ;; Version 1.3: features and fixes 126 | ;; * Full offline support 127 | ;; * No more callback registering 128 | ;; * Fix 64-bit support 129 | ;; * Two new hooks for improved extension support 130 | ;; * More uniform keybindings with other interactive modes 131 | ;; Version 1.2: features 132 | ;; * Add a skewer-eval-print-last-expression 133 | ;; * Display evaluation time when it's long 134 | ;; * Flash the region on eval 135 | ;; * Improve JS stringification 136 | ;; Version 1.1: features and fixes 137 | ;; * Added `list-skewer-clients' 138 | ;; * Reduce the number of HTTP requests needed 139 | ;; * Fix stringification issues 140 | ;; Version 1.0: initial release 141 | 142 | ;;; Code: 143 | 144 | (require 'cl-lib) 145 | (require 'json) 146 | (require 'url-util) 147 | (require 'simple-httpd) 148 | (require 'js2-mode) 149 | (require 'cache-table) 150 | 151 | (defgroup skewer nil 152 | "Live browser JavaScript interaction." 153 | :group 'languages) 154 | 155 | (defvar skewer-mode-map 156 | (let ((map (make-sparse-keymap))) 157 | (prog1 map 158 | (define-key map (kbd "C-x C-e") 'skewer-eval-last-expression) 159 | (define-key map (kbd "C-M-x") 'skewer-eval-defun) 160 | (define-key map (kbd "C-c C-k") 'skewer-load-buffer))) 161 | "Keymap for skewer-mode.") 162 | 163 | (defvar skewer-data-root (file-name-directory load-file-name) 164 | "Location of data files needed by impatient-mode.") 165 | 166 | (defvar skewer-js-hook () 167 | "Hook to run when skewer.js is being served to the browser. 168 | 169 | When hook functions are called, the current buffer is the buffer 170 | to be served to the client (a defservlet), with skewer.js script 171 | already inserted. This is the chance for other packages to insert 172 | their own JavaScript to extend skewer in the browser, such as 173 | adding a new type handler.") 174 | 175 | (defvar skewer-response-hook () 176 | "Hook to run when a response arrives from the browser. Used for 177 | catching messages from the browser with no associated 178 | callback. The response object is passed to the hook function.") 179 | 180 | (defvar skewer-timeout 3600 181 | "Maximum time to wait on the browser to respond, in seconds.") 182 | 183 | (defvar skewer-clients () 184 | "Browsers awaiting JavaScript snippets.") 185 | 186 | (defvar skewer-callbacks (cache-table-create skewer-timeout :test 'equal) 187 | "Maps evaluation IDs to local callbacks.") 188 | 189 | (defvar skewer-queue () 190 | "Queued messages for the browser.") 191 | 192 | (defvar skewer--last-timestamp 0 193 | "Timestamp of the last browser response. Use 194 | `skewer-last-seen-seconds' to access this.") 195 | 196 | (cl-defstruct skewer-client 197 | "A client connection awaiting a response." 198 | proc agent) 199 | 200 | (defun skewer-process-queue () 201 | "Send all queued messages to clients." 202 | (when (and skewer-queue skewer-clients) 203 | (let ((message (pop skewer-queue)) 204 | (sent nil)) 205 | (while skewer-clients 206 | (ignore-errors 207 | (progn 208 | (let ((proc (skewer-client-proc (pop skewer-clients)))) 209 | (with-temp-buffer 210 | (insert (json-encode message)) 211 | (httpd-send-header proc "text/plain" 200 212 | :Cache-Control "no-cache" 213 | :Access-Control-Allow-Origin "*"))) 214 | (setq skewer--last-timestamp (float-time)) 215 | (setq sent t)))) 216 | (if (not sent) (push message skewer-queue))) 217 | (skewer-process-queue))) 218 | 219 | (defun skewer-clients-tabulate () 220 | "Prepare client list for tabulated-list-mode." 221 | (cl-loop for client in skewer-clients collect 222 | (let ((proc (skewer-client-proc client)) 223 | (agent (skewer-client-agent client))) 224 | (cl-destructuring-bind (host port) (process-contact proc) 225 | `(,client [,host ,(format "%d" port) ,agent]))))) 226 | 227 | (define-derived-mode skewer-clients-mode tabulated-list-mode "skewer-clients" 228 | "Mode for listing browsers attached to Emacs for skewer-mode." 229 | (setq tabulated-list-format [("Host" 12 t) 230 | ("Port" 5 t) 231 | ("User Agent" 0 t)]) 232 | (setq tabulated-list-entries #'skewer-clients-tabulate) 233 | (tabulated-list-init-header)) 234 | 235 | (define-key skewer-clients-mode-map (kbd "g") 236 | (lambda () 237 | (interactive) 238 | (skewer-ping) 239 | (revert-buffer))) 240 | 241 | (defun skewer-update-list-buffer () 242 | "Revert the client list, due to an update." 243 | (save-window-excursion 244 | (let ((list-buffer (get-buffer "*skewer-clients*"))) 245 | (when list-buffer 246 | (with-current-buffer list-buffer 247 | (revert-buffer)))))) 248 | 249 | ;;;###autoload 250 | (defun list-skewer-clients () 251 | "List the attached browsers in a buffer." 252 | (interactive) 253 | (pop-to-buffer (get-buffer-create "*skewer-clients*")) 254 | (skewer-clients-mode) 255 | (tabulated-list-print)) 256 | 257 | (defun skewer-queue-client (proc req) 258 | "Add a client to the queue, given the HTTP header." 259 | (let ((agent (cl-second (assoc "User-Agent" req)))) 260 | (push (make-skewer-client :proc proc :agent agent) skewer-clients)) 261 | (skewer-update-list-buffer) 262 | (skewer-process-queue)) 263 | 264 | ;; Servlets 265 | 266 | (defservlet skewer "text/javascript; charset=UTF-8" () 267 | (insert-file-contents (expand-file-name "skewer.js" skewer-data-root)) 268 | (goto-char (point-max)) 269 | (run-hooks 'skewer-js-hook)) 270 | 271 | (defun httpd/skewer/get (proc _path _query req &rest _args) 272 | (skewer-queue-client proc req)) 273 | 274 | (defun httpd/skewer/post (proc _path _query req &rest _args) 275 | (let* ((result (json-read-from-string (cadr (assoc "Content" req)))) 276 | (id (cdr (assoc 'id result))) 277 | (callback (cache-table-get id skewer-callbacks))) 278 | (setq skewer--last-timestamp (float-time)) 279 | (when callback 280 | (funcall callback result)) 281 | (if id 282 | (skewer-queue-client proc req) 283 | (with-temp-buffer 284 | (httpd-send-header proc "text/plain" 200 285 | :Access-Control-Allow-Origin "*"))) 286 | (dolist (hook skewer-response-hook) 287 | (funcall hook result)))) 288 | 289 | (defvar skewer-demo-source 290 | (expand-file-name "example.html" skewer-data-root) 291 | "Source file name or buffer for `httpd/skewer/demo' servlet.") 292 | 293 | (defservlet skewer/demo "text/html; charset=UTF-8" () 294 | (cl-etypecase skewer-demo-source 295 | (buffer (insert-buffer-substring skewer-demo-source)) 296 | (string (insert-file-contents skewer-demo-source)))) 297 | 298 | ;; Minibuffer display 299 | 300 | (defun skewer-success-p (result) 301 | "Return T if result was a success." 302 | (equal "success" (cdr (assoc 'status result)))) 303 | 304 | (define-derived-mode skewer-error-mode special-mode "skewer-error" 305 | :group 'skewer 306 | "Mode for displaying JavaScript errors returned by skewer-mode." 307 | (setq truncate-lines t)) 308 | 309 | (defface skewer-error-face 310 | '((((class color) (background light)) 311 | :foreground "red" :underline t) 312 | (((class color) (background dark)) 313 | :foreground "red" :underline t)) 314 | "Face for JavaScript errors." 315 | :group 'skewer) 316 | 317 | (defun skewer--error (string) 318 | "Return STRING propertized as an error message." 319 | (propertize (or string "") 'font-lock-face 'skewer-error-face)) 320 | 321 | (defun skewer-post-minibuffer (result) 322 | "Report results in the minibuffer or the error buffer." 323 | (if (skewer-success-p result) 324 | (let ((value (cdr (assoc 'value result))) 325 | (time (cdr (assoc 'time result)))) 326 | (if (and time (> time 1.0)) 327 | (message "%s (%.3f seconds)" value time) 328 | (message "%s" value))) 329 | (with-current-buffer (pop-to-buffer (get-buffer-create "*skewer-error*")) 330 | (let ((inhibit-read-only t) 331 | (error (cdr (assoc 'error result)))) 332 | (erase-buffer) 333 | (skewer-error-mode) 334 | (insert (skewer--error (cdr (assoc 'name error))) ": ") 335 | (insert (or (cdr (assoc 'message error)) "") "\n\n") 336 | (insert (or (cdr (assoc 'stack error)) "") "\n\n") 337 | (insert (format "Expression: %s\n\n" 338 | (if (cdr (assoc 'strict result)) "(strict)" "")) 339 | (cdr (assoc 'eval error))) 340 | (goto-char (point-min)))))) 341 | 342 | ;; Evaluation functions 343 | 344 | (cl-defun skewer-eval (string &optional callback 345 | &key verbose strict (type "eval") extra) 346 | "Evaluate STRING in the waiting browsers, giving the result to CALLBACK. 347 | 348 | :VERBOSE -- if T, the return will try to be JSON encoded 349 | :STRICT -- if T, expression is evaluated with 'use strict' 350 | :TYPE -- chooses the JavaScript handler (default: eval) 351 | :EXTRA -- additional alist keys to append to the request object" 352 | (let* ((id (format "%x" (random most-positive-fixnum))) 353 | (request `((type . ,type) 354 | (eval . ,string) 355 | (id . ,id) 356 | (verbose . ,verbose) 357 | (strict . ,strict) 358 | ,@extra))) 359 | (prog1 request 360 | (setf (cache-table-get id skewer-callbacks) callback) 361 | (setq skewer-queue (append skewer-queue (list request))) 362 | (skewer-process-queue)))) 363 | 364 | (defun skewer-eval-synchronously (string &rest args) 365 | "Just like `skewer-eval' but synchronously, so don't provide a 366 | callback. Use with caution." 367 | (let ((result nil)) 368 | (apply #'skewer-eval string (lambda (v) (setq result v)) args) 369 | (cl-loop until result 370 | do (accept-process-output nil 0.01) 371 | finally (return result)))) 372 | 373 | (defun skewer-apply (function args) 374 | "Synchronously apply FUNCTION in the browser with the supplied 375 | arguments, returning the result. All ARGS must be printable by 376 | `json-encode'. For example, 377 | 378 | (skewer-apply \"Math.atan2\" '(1 -2)) ; => 2.677945044588987 379 | 380 | Uncaught exceptions propagate to Emacs as an error." 381 | (let ((specials '(("undefined" . nil) 382 | ("NaN" . 0.0e+NaN) 383 | ("Infinity" . 1.0e+INF) 384 | ("-Infinity" . -1.0e+INF)))) 385 | (let* ((expr (concat function "(" (mapconcat #'json-encode args ", ") ")")) 386 | (result (skewer-eval-synchronously expr :verbose t)) 387 | (value (cdr (assoc 'value result)))) 388 | (if (skewer-success-p result) 389 | (if (assoc value specials) 390 | (cdr (assoc value specials)) 391 | (condition-case _ 392 | (json-read-from-string value) 393 | (json-readtable-error value))) 394 | (signal 'javascript 395 | (list (cdr (assoc 'message (cdr (assoc'error result)))))))))) 396 | 397 | (defun skewer-funcall (function &rest args) 398 | "Synchronously call FUNCTION with the supplied ARGS. All ARGS 399 | must be printable by `json-read-from-string. For example, 400 | 401 | (skewer-funcall \"Math.sin\" 0.5) ; => 0.479425538604203 402 | 403 | Uncaught exceptions propagate to Emacs as an error." 404 | (skewer-apply function args)) 405 | 406 | (defun skewer--save-point (f &rest args) 407 | "Return a function that calls F with point at the current point." 408 | (let ((saved-point (point))) 409 | (lambda (&rest more) 410 | (save-excursion 411 | (goto-char saved-point) 412 | (apply f (append args more)))))) 413 | 414 | (defun skewer-ping () 415 | "Ping the browser to test that it's still alive." 416 | (unless (null skewer-clients) ; don't queue pings 417 | (skewer-eval (prin1-to-string (float-time)) nil :type "ping"))) 418 | 419 | (defun skewer-last-seen-seconds () 420 | "Return the number of seconds since the browser was last seen." 421 | (skewer-ping) ; make sure it's still alive next request 422 | (- (float-time) skewer--last-timestamp)) 423 | 424 | (defun skewer-mode-strict-p () 425 | "Return T if buffer contents indicates strict mode." 426 | (save-excursion 427 | (save-restriction 428 | (widen) 429 | (goto-char (point-min)) 430 | (js2-forward-sws) 431 | (forward-char 1) 432 | (let* ((stricts '("\"use strict\"" "'use strict'")) 433 | (node (js2-node-at-point)) 434 | (code (buffer-substring-no-properties (js2-node-abs-pos node) 435 | (js2-node-abs-end node)))) 436 | (and (member code stricts) t))))) 437 | 438 | (defun skewer-flash-region (start end &optional timeout) 439 | "Temporarily highlight region from START to END." 440 | (let ((overlay (make-overlay start end))) 441 | (overlay-put overlay 'face 'secondary-selection) 442 | (run-with-timer (or timeout 0.2) nil 'delete-overlay overlay))) 443 | 444 | (defun skewer-get-last-expression () 445 | "Return the JavaScript expression before the point as a 446 | list: (string start end)." 447 | (save-excursion 448 | (js2-backward-sws) 449 | (backward-char) 450 | (let ((node (js2-node-at-point nil t))) 451 | (when (eq js2-FUNCTION (js2-node-type (js2-node-parent node))) 452 | (setq node (js2-node-parent node))) 453 | (when (js2-ast-root-p node) 454 | (error "no expression found")) 455 | (let ((start (js2-node-abs-pos node)) 456 | (end (js2-node-abs-end node))) 457 | (list (buffer-substring-no-properties start end) start end))))) 458 | 459 | (defun skewer-eval-last-expression (&optional prefix) 460 | "Evaluate the JavaScript expression before the point in the 461 | waiting browser. If invoked with a prefix argument, insert the 462 | result into the current buffer." 463 | (interactive "P") 464 | (if prefix 465 | (skewer-eval-print-last-expression) 466 | (if js2-mode-buffer-dirty-p 467 | (js2-mode-wait-for-parse 468 | (skewer--save-point #'skewer-eval-last-expression)) 469 | (cl-destructuring-bind (string start end) (skewer-get-last-expression) 470 | (skewer-flash-region start end) 471 | (skewer-eval string #'skewer-post-minibuffer))))) 472 | 473 | (defun skewer-get-defun () 474 | "Return the toplevel JavaScript expression around the point as 475 | a list: (string start end)." 476 | (save-excursion 477 | (js2-backward-sws) 478 | (backward-char) 479 | (let ((node (js2-node-at-point nil t))) 480 | (when (js2-ast-root-p node) 481 | (error "no expression found")) 482 | (while (and (js2-node-parent node) 483 | (not (js2-ast-root-p (js2-node-parent node)))) 484 | (setf node (js2-node-parent node))) 485 | (let ((start (js2-node-abs-pos node)) 486 | (end (js2-node-abs-end node))) 487 | (list (buffer-substring-no-properties start end) start end))))) 488 | 489 | (defun skewer-eval-defun () 490 | "Evaluate the JavaScript expression before the point in the 491 | waiting browser." 492 | (interactive) 493 | (if js2-mode-buffer-dirty-p 494 | (js2-mode-wait-for-parse (skewer--save-point #'skewer-eval-defun)) 495 | (cl-destructuring-bind (string start end) (skewer-get-defun) 496 | (skewer-flash-region start end) 497 | (skewer-eval string #'skewer-post-minibuffer)))) 498 | 499 | ;; Print last expression 500 | 501 | (defvar skewer-eval-print-map (cache-table-create skewer-timeout :test 'equal) 502 | "A mapping of evaluation IDs to insertion points.") 503 | 504 | (defun skewer-post-print (result) 505 | "Insert the result after its source expression." 506 | (if (not (skewer-success-p result)) 507 | (skewer-post-minibuffer result) 508 | (let* ((id (cdr (assoc 'id result))) 509 | (pos (cache-table-get id skewer-eval-print-map))) 510 | (when pos 511 | (with-current-buffer (car pos) 512 | (goto-char (cdr pos)) 513 | (insert (cdr (assoc 'value result)) "\n")))))) 514 | 515 | (defun skewer-eval-print-last-expression () 516 | "Evaluate the JavaScript expression before the point in the 517 | waiting browser and insert the result in the buffer at point." 518 | (interactive) 519 | (if js2-mode-buffer-dirty-p 520 | (js2-mode-wait-for-parse 521 | (skewer--save-point #'skewer-eval-print-last-expression)) 522 | (cl-destructuring-bind (string start end) (skewer-get-defun) 523 | (skewer-flash-region start end) 524 | (insert "\n") 525 | (let* ((request (skewer-eval string #'skewer-post-print :verbose t)) 526 | (id (cdr (assoc 'id request))) 527 | (pos (cons (current-buffer) (point)))) 528 | (setf (cache-table-get id skewer-eval-print-map) pos))))) 529 | 530 | ;; Script loading 531 | 532 | (defvar skewer-hosted-scripts (cache-table-create skewer-timeout) 533 | "Map of hosted scripts to IDs.") 534 | 535 | (defun skewer-host-script (string) 536 | "Host script STRING from the script servlet, returning the script ID." 537 | (let ((id (random most-positive-fixnum))) 538 | (prog1 id 539 | (setf (cache-table-get id skewer-hosted-scripts) string)))) 540 | 541 | (defun skewer-load-buffer () 542 | "Load the entire current buffer into the browser. A snapshot of 543 | the buffer is hosted so that browsers visiting late won't see an 544 | inconsistent buffer." 545 | (interactive) 546 | (let ((id (skewer-host-script (buffer-string))) 547 | (buffer-name (buffer-name))) 548 | (skewer-eval (format "/skewer/script/%d/%s" 549 | id (url-hexify-string buffer-name)) 550 | (lambda (_) (message "%s loaded" buffer-name)) 551 | :type "script"))) 552 | 553 | (defservlet skewer/script "text/javascript; charset=UTF-8" (path) 554 | (let ((id (string-to-number (nth 3 (split-string path "/"))))) 555 | (insert (cache-table-get id skewer-hosted-scripts "")))) 556 | 557 | ;; Define the minor mode 558 | 559 | ;;;###autoload 560 | (define-minor-mode skewer-mode 561 | "Minor mode for interacting with a browser." 562 | :lighter " skewer" 563 | :keymap skewer-mode-map 564 | :group 'skewer) 565 | 566 | ;;;###autoload 567 | (defun run-skewer (&optional arg) 568 | "Attach a browser to Emacs for a skewer JavaScript REPL. Uses 569 | `browse-url' to launch a browser. 570 | 571 | With a prefix arugment (C-u), it will ask the filename of the 572 | root document. With two prefix arguments (C-u C-u), it will use 573 | the contents of the current buffer as the root document." 574 | (interactive "p") 575 | (cl-case arg 576 | (4 (setf skewer-demo-source (read-file-name "Skewer filename: "))) 577 | (16 (setf skewer-demo-source (current-buffer)))) 578 | (httpd-start) 579 | (browse-url (format "http://127.0.0.1:%d/skewer/demo" httpd-port))) 580 | 581 | ;; PhantomJS 582 | 583 | (defvar phantomjs-program-name "/usr/bin/phantomjs" 584 | "Path to the phantomjs executable.") 585 | 586 | (defvar skewer-phantomjs-processes () 587 | "List of phantomjs processes connected to Skewer.") 588 | 589 | (defun skewer-phantomjs-sentinel (proc event) 590 | "Cleanup after phantomjs exits." 591 | (when (cl-some (lambda (s) (string-match-p s event)) 592 | '("finished" "abnormal" "killed")) 593 | (delete-file (process-get proc 'tempfile)))) 594 | 595 | ;;;###autoload 596 | (defun skewer-run-phantomjs () 597 | "Connect an inferior PhantomJS process to Skewer, returning the process." 598 | (interactive) 599 | (httpd-start) 600 | (let ((script (make-temp-file "phantomjs-")) 601 | (url (format "http://0:%d/skewer/demo" httpd-port))) 602 | (with-temp-buffer 603 | (insert (format "require('webpage').create().open('%s')" url)) 604 | (write-region nil nil script nil 0) 605 | (let ((proc (start-process "phantomjs" nil 606 | phantomjs-program-name script))) 607 | (prog1 proc 608 | (push proc skewer-phantomjs-processes) 609 | (process-put proc 'tempfile script) 610 | (set-process-sentinel proc 'skewer-phantomjs-sentinel)))))) 611 | 612 | (defun skewer-phantomjs-kill () 613 | "Kill all inferior phantomjs processes connected to Skewer." 614 | (interactive) 615 | (mapc #'delete-process skewer-phantomjs-processes) 616 | (setf skewer-phantomjs-processes nil)) 617 | 618 | (provide 'skewer-mode) 619 | 620 | ;;; skewer-mode.el ends here 621 | -------------------------------------------------------------------------------- /skewer-repl.el: -------------------------------------------------------------------------------- 1 | ;;; skewer-repl.el --- create a REPL in a visiting browser -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;;; Commentary: 6 | 7 | ;; This is largely based on of IELM's code. Run `skewer-repl' to 8 | ;; switch to the REPL buffer and evaluate code. Use 9 | ;; `skewer-repl-toggle-strict-mode' to turn strict mode on and off. 10 | 11 | ;; If `compilation-search-path' is set up properly, along with 12 | ;; `skewer-path-strip-level', asynchronous errors will provide 13 | ;; clickable error messages that will take you to the source file of 14 | ;; the error. This is done using `compilation-shell-minor-mode'. 15 | 16 | ;;; Code: 17 | 18 | (require 'comint) 19 | (require 'compile) 20 | (require 'skewer-mode) 21 | 22 | (defcustom skewer-repl-strict-p nil 23 | "When non-NIL, all REPL evaluations are done in strict mode." 24 | :type 'boolean 25 | :group 'skewer) 26 | 27 | (defcustom skewer-repl-prompt "js> " 28 | "Prompt string for JavaScript REPL." 29 | :type 'string 30 | :group 'skewer) 31 | 32 | (defvar skewer-repl-welcome 33 | (propertize "*** Welcome to Skewer ***\n" 34 | 'font-lock-face 'font-lock-comment-face) 35 | "Header line to show at the top of the REPL buffer. Hack 36 | notice: this allows log messages to appear before anything is 37 | evaluated because it provides insertable space at the top of the 38 | buffer.") 39 | 40 | (defun skewer-repl-process () 41 | "Return the process for the skewer REPL." 42 | (get-buffer-process (current-buffer))) 43 | 44 | (defface skewer-repl-log-face 45 | '((((class color) (background light)) 46 | :foreground "#77F") 47 | (((class color) (background dark)) 48 | :foreground "#77F")) 49 | "Face for skewer.log() messages." 50 | :group 'skewer) 51 | 52 | (define-derived-mode skewer-repl-mode comint-mode "js-REPL" 53 | "Provide a REPL into the visiting browser." 54 | :group 'skewer 55 | :syntax-table emacs-lisp-mode-syntax-table 56 | (setq comint-prompt-regexp (concat "^" (regexp-quote skewer-repl-prompt)) 57 | comint-input-sender 'skewer-input-sender 58 | comint-process-echoes nil) 59 | ;; Make opportunistic use of company-mode, but don't require it. 60 | ;; This means company-backends may be undeclared, so don't emit a 61 | ;; warning about it. 62 | (with-no-warnings 63 | (setq-local company-backends '(company-skewer-repl))) 64 | (unless (comint-check-proc (current-buffer)) 65 | (insert skewer-repl-welcome) 66 | (start-process "skewer-repl" (current-buffer) nil) 67 | (set-process-query-on-exit-flag (skewer-repl-process) nil) 68 | (goto-char (point-max)) 69 | (set (make-local-variable 'comint-inhibit-carriage-motion) t) 70 | (comint-output-filter (skewer-repl-process) skewer-repl-prompt) 71 | (set-process-filter (skewer-repl-process) 'comint-output-filter))) 72 | 73 | (defun skewer-repl-toggle-strict-mode () 74 | "Toggle strict mode for expressions evaluated by the REPL." 75 | (interactive) 76 | (setq skewer-repl-strict-p (not skewer-repl-strict-p)) 77 | (message "REPL strict mode %s" 78 | (if skewer-repl-strict-p "enabled" "disabled"))) 79 | 80 | (defun skewer-input-sender (_ input) 81 | "REPL comint handler." 82 | (skewer-eval input 'skewer-post-repl 83 | :verbose t :strict skewer-repl-strict-p)) 84 | 85 | (defun skewer-post-repl (result) 86 | "Callback for reporting results in the REPL." 87 | (let ((buffer (get-buffer "*skewer-repl*")) 88 | (output (cdr (assoc 'value result)))) 89 | (when buffer 90 | (with-current-buffer buffer 91 | (comint-output-filter (skewer-repl-process) 92 | (concat output "\n" skewer-repl-prompt)))))) 93 | 94 | (defvar skewer-repl-types 95 | '(("log" . skewer-repl-log-face) 96 | ("error" . skewer-error-face)) 97 | "Faces to use for different types of log messages.") 98 | 99 | (defun skewer-log-filename (log) 100 | "Create a log string for the source file in LOG if present." 101 | (let ((name (cdr (assoc 'filename log))) 102 | (line (cdr (assoc 'line log))) 103 | (column (cdr (assoc 'column log)))) 104 | (when name 105 | (concat (format "\n at %s:%s" name line) 106 | (if column (format ":%s" column)))))) 107 | 108 | (defun skewer-post-log (log) 109 | "Callback for logging messages to the REPL." 110 | (let* ((buffer (get-buffer "*skewer-repl*")) 111 | (face (cdr (assoc (cdr (assoc 'type log)) skewer-repl-types))) 112 | (value (or (cdr (assoc 'value log)) "")) 113 | (output (propertize value 'font-lock-face face))) 114 | (when buffer 115 | (with-current-buffer buffer 116 | (save-excursion 117 | (goto-char (point-max)) 118 | (forward-line 0) 119 | (if (bobp) 120 | (insert (concat output (skewer-log-filename log) "\n")) 121 | (backward-char) 122 | (insert (concat "\n" output (skewer-log-filename log))))))))) 123 | 124 | (defcustom skewer-path-strip-level 1 125 | "Number of folders which will be stripped from url when discovering paths. 126 | Use this to limit path matching to files in your filesystem. You 127 | may want to add some folders to `compilation-search-path', so 128 | matched files can be found." 129 | :type 'number 130 | :group 'skewer) 131 | 132 | (defun skewer-repl-mode-compilation-shell-hook () 133 | "Setup compilation shell minor mode for highlighting files" 134 | (let ((error-re (format "^[ ]*at https?://[^/]+/\\(?:[^/]+/\\)\\{%d\\}\\([^:?#]+\\)\\(?:[?#][^:]*\\)?:\\([[:digit:]]+\\)\\(?::\\([[:digit:]]+\\)\\)?$" skewer-path-strip-level))) 135 | (setq-local compilation-error-regexp-alist `((,error-re 1 2 3 2)))) 136 | (compilation-shell-minor-mode 1)) 137 | 138 | ;;;###autoload 139 | (defun skewer-repl--response-hook (response) 140 | "Catches all browser messages logging some to the REPL." 141 | (let ((type (cdr (assoc 'type response)))) 142 | (when (member type '("log" "error")) 143 | (skewer-post-log response)))) 144 | 145 | ;;;###autoload 146 | (defun skewer-repl () 147 | "Start a JavaScript REPL to be evaluated in the visiting browser." 148 | (interactive) 149 | (when (not (get-buffer "*skewer-repl*")) 150 | (with-current-buffer (get-buffer-create "*skewer-repl*") 151 | (skewer-repl-mode))) 152 | (pop-to-buffer (get-buffer "*skewer-repl*"))) 153 | 154 | (defun company-skewer-repl (command &optional arg &rest _args) 155 | "Skewerl REPL backend for company-mode. 156 | See `company-backends' for more info about COMMAND and ARG." 157 | (interactive (list 'interactive)) 158 | (cl-case command 159 | (interactive 160 | (with-no-warnings ;; opportunistic use of company-mode 161 | (company-begin-backend 'company-skewer-repl))) 162 | (prefix (skewer-repl-company-prefix)) 163 | (ignore-case t) 164 | (sorted t) 165 | (candidates (cons :async 166 | (lambda (callback) 167 | (skewer-repl-get-completions arg callback)))))) 168 | 169 | (defun skewer-repl-get-completions (arg callback) 170 | "Get the completion list matching the prefix ARG. 171 | Evaluate CALLBACK with the completion candidates." 172 | (let* ((expression (skewer-repl--get-completion-expression arg)) 173 | (pattern (if expression 174 | (substring arg (1+ (length expression))) 175 | arg))) 176 | (skewer-eval (or expression "window") 177 | (lambda (result) 178 | (cl-loop with value = (cdr (assoc 'value result)) 179 | for key being the elements of value 180 | when expression 181 | collect (concat expression "." key) into results 182 | else 183 | collect key into results 184 | finally (funcall callback results))) 185 | :type "completions" 186 | :extra `((regexp . ,pattern))))) 187 | 188 | (defun skewer-repl--get-completion-expression (arg) 189 | "Get completion expression from ARG." 190 | (let ((components (split-string arg "\\."))) 191 | (when (> (length components) 1) 192 | (mapconcat #'identity (cl-subseq components 0 -1) ".")))) 193 | 194 | (defun skewer-repl-company-prefix () 195 | "Prefix for company." 196 | (and (eq major-mode 'skewer-repl-mode) 197 | (or (with-no-warnings ;; opportunistic use of company-mode 198 | (company-grab-symbol-cons "\\." 1)) 199 | 'stop))) 200 | 201 | ;;;###autoload 202 | (eval-after-load 'skewer-mode 203 | '(progn 204 | (add-hook 'skewer-response-hook #'skewer-repl--response-hook) 205 | (add-hook 'skewer-repl-mode-hook #'skewer-repl-mode-compilation-shell-hook) 206 | (define-key skewer-mode-map (kbd "C-c C-z") #'skewer-repl))) 207 | 208 | (provide 'skewer-repl) 209 | 210 | ;;; skewer-repl.el ends here 211 | -------------------------------------------------------------------------------- /skewer-setup.el: -------------------------------------------------------------------------------- 1 | ;;; skewer-setup.el --- automatic setup for the Skewer minor modes -*- lexical-binding: t; -*- 2 | 3 | ;; This is free and unencumbered software released into the public domain. 4 | 5 | ;;; Commentary: 6 | 7 | ;; This exists as a separate file so that Skewer need not be fully 8 | ;; loaded just to use this setup function. 9 | 10 | ;;; Code: 11 | 12 | ;;;###autoload 13 | (defun skewer-setup () 14 | "Fully integrate Skewer into js2-mode, css-mode, and html-mode buffers." 15 | (add-hook 'js2-mode-hook 'skewer-mode) 16 | (add-hook 'css-mode-hook 'skewer-css-mode) 17 | (add-hook 'html-mode-hook 'skewer-html-mode)) 18 | 19 | (provide 'skewer-setup) 20 | 21 | ;;; skewer-setup.el ends here 22 | -------------------------------------------------------------------------------- /skewer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Live browser interaction with Emacs 3 | * @version 1.4 4 | */ 5 | 6 | /** 7 | * Connects to Emacs and waits for a request. After handling the 8 | * request it sends back the results and queues itself for another 9 | * request. 10 | * @namespace Holds all of Skewer's functionality. 11 | */ 12 | function skewer() { 13 | function callback(request) { 14 | var result = skewer.fn[request.type](request); 15 | if (result) { 16 | result = skewer.extend({ 17 | id: request.id, 18 | type: request.type, 19 | status: 'success', 20 | value: '' 21 | }, result); 22 | skewer.postJSON(skewer.host + "/skewer/post", result, callback); 23 | } else { 24 | skewer.getJSON(skewer.host + "/skewer/get", callback); 25 | } 26 | }; 27 | skewer.getJSON(skewer.host + "/skewer/get", callback); 28 | } 29 | 30 | /** 31 | * Get a JSON-encoded object from a server. 32 | * @param {String} url The location of the remote server 33 | * @param {Function} [callback] The callback to receive a response object 34 | */ 35 | skewer.getJSON = function(url, callback) { 36 | var XHR = window.skewerNativeXHR || XMLHttpRequest; 37 | var xhr = new XHR(); 38 | xhr.onreadystatechange = function() { 39 | if (xhr.readyState === 4 && xhr.status === 200) { 40 | callback(JSON.parse(xhr.responseText)); 41 | } 42 | }; 43 | xhr.open('GET', url, true); 44 | xhr.send(); 45 | }; 46 | 47 | /** 48 | * Send a JSON-encoded object to a server. 49 | * @param {String} url The location of the remote server 50 | * @param {Object} object The object to transmit to the server 51 | * @param {Function} [callback] The callback to receive a response object 52 | */ 53 | skewer.postJSON = function(url, object, callback) { 54 | var XHR = window.skewerNativeXHR || XMLHttpRequest; 55 | var xhr = new XHR(); 56 | xhr.onreadystatechange = function() { 57 | if (callback && xhr.readyState === 4 && xhr.status === 200) { 58 | callback(JSON.parse(xhr.responseText)); 59 | } 60 | }; 61 | xhr.open('POST', url, true); 62 | xhr.setRequestHeader("Content-Type", "text/plain"); // CORS 63 | xhr.send(JSON.stringify(object)); 64 | }; 65 | 66 | /** 67 | * Add the properties other objects to a target object (jQuery.extend). 68 | * @param {Object} target The object to receive new properties 69 | * @param {...Object} objects Source objects for properties 70 | * @returns The target object 71 | */ 72 | skewer.extend = function(target) { 73 | for (var i = 1; i < arguments.length; i++) { 74 | var object = arguments[i]; 75 | for (var key in object) { 76 | if (object.hasOwnProperty(key)) { 77 | target[key] = object[key]; 78 | } 79 | } 80 | } 81 | return target; 82 | }; 83 | 84 | /** 85 | * Globally evaluate an expression and return the result. This 86 | * only works when the implementation's indirect eval performs 87 | * a global eval. If not, there's no alternative, since a return value 88 | * is essential. 89 | * 90 | * @see http://perfectionkills.com/global-eval-what-are-the-options/ 91 | * 92 | * @param expression A string containing an expression to evaluate 93 | * @returns The result of the evaluation 94 | */ 95 | skewer.globalEval = (function() { 96 | var eval0 = (function(original, Object) { 97 | try { 98 | return [eval][0]('Object') === original; 99 | } catch (e) { 100 | return false; 101 | } 102 | }(Object, false)); 103 | if (eval0) { 104 | return function(expression) { 105 | return [eval][0](expression); 106 | }; 107 | } else { 108 | return function(expression) { // Safari 109 | return eval.call(window, expression); 110 | }; 111 | } 112 | }()); 113 | 114 | /** 115 | * Same as Date.now(), supplied for pre-ES5 JS (<=IE8). 116 | * @returns {number} The epoch time in milliseconds 117 | */ 118 | skewer.now = function() { 119 | return new Date().valueOf(); 120 | }; 121 | 122 | /** 123 | * Handlers accept a request object from Emacs and return either a 124 | * logical false (no response) or an object to return to Emacs. 125 | * @namespace Request handlers. 126 | */ 127 | skewer.fn = {}; 128 | 129 | /** 130 | * Handles an code evaluation request from Emacs. 131 | * @param request The request object sent by Emacs 132 | * @returns The result object to be returned to Emacs 133 | */ 134 | skewer.fn.eval = function(request) { 135 | var result = { 136 | strict: request.strict 137 | }; 138 | var start = skewer.now(); 139 | try { 140 | var prefix = request.strict ? '"use strict";\n' : ""; 141 | var value = skewer.globalEval(prefix + request.eval); 142 | result.value = skewer.safeStringify(value, request.verbose); 143 | } catch (error) { 144 | result = skewer.errorResult(error, result, request); 145 | } 146 | result.time = (skewer.now() - start) / 1000; 147 | return result; 148 | }; 149 | 150 | /** 151 | * Load a hosted script named by the request. 152 | * @param request The request object sent by Emacs 153 | * @returns The result object to be returned to Emacs 154 | */ 155 | skewer.fn.script = function(request) { 156 | var script = document.createElement('script'); 157 | script.src = skewer.host + request.eval; 158 | document.body.appendChild(script); 159 | return {value: JSON.stringify(request.eval)}; 160 | }; 161 | 162 | /** 163 | * A keep-alive and connecton testing handler. 164 | * @param request The request object sent by Emacs 165 | * @returns The result object to be returned to Emacs 166 | */ 167 | skewer.fn.ping = function(request) { 168 | return { 169 | type: 'pong', 170 | date: skewer.now() / 1000, 171 | value: request.eval 172 | }; 173 | }; 174 | 175 | /** 176 | * Establish a new stylesheet with the provided value. 177 | */ 178 | skewer.fn.css = function(request) { 179 | var style = document.createElement('style'); 180 | style.type = 'text/css'; 181 | style.className = 'skewer'; 182 | if (style.styleSheet) { // < IE9 183 | style.styleSheet.cssText = request.eval; 184 | } else { 185 | style.appendChild(document.createTextNode(request.eval)); 186 | } 187 | document.body.appendChild(style); 188 | return {}; 189 | }; 190 | 191 | /** 192 | * Remove all of Skewer's style tags from the document. 193 | */ 194 | skewer.fn.cssClearAll = function(request) { 195 | var styles = document.body.querySelectorAll('style.skewer'); 196 | for (var i = 0; i < styles.length; i++) { 197 | styles[i].parentNode.removeChild(styles[i]); 198 | } 199 | return {}; 200 | }; 201 | 202 | /** 203 | * HTML evaluator, appends or replaces a selection with given HTML. 204 | */ 205 | skewer.fn.html = function(request) { 206 | function buildSelector(ancestry) { 207 | return ancestry.map(function(tag) { 208 | return tag[0] + ':nth-of-type(' + tag[1] + ')'; 209 | }).join(' > '); 210 | } 211 | function query(ancestry) { 212 | return document.querySelector(buildSelector(ancestry)); 213 | } 214 | function htmlToNode(html) { 215 | var wrapper = document.createElement('div'); 216 | wrapper.innerHTML = html; 217 | return wrapper.firstChild; 218 | } 219 | 220 | var target = query(request.ancestry); 221 | 222 | if (target == null) { 223 | /* Determine missing part of the ancestry. */ 224 | var path = request.ancestry.slice(0); // copy 225 | var missing = []; 226 | while (query(path) == null) { 227 | missing.push(path.pop()); 228 | } 229 | 230 | /* Build up the missing elements. */ 231 | target = query(path); 232 | while (missing.length > 0) { 233 | var tag = missing.pop(), 234 | name = tag[0], 235 | nth = tag[1]; 236 | var empty = null; 237 | var count = target.querySelectorAll(name).length; 238 | for (; count < nth; count++) { 239 | empty = document.createElement(tag[0]); 240 | target.appendChild(empty); 241 | } 242 | target = empty; 243 | } 244 | } 245 | 246 | target.parentNode.replaceChild(htmlToNode(request.eval), target); 247 | return {}; 248 | }; 249 | 250 | /** 251 | * Fetch the HTML contents of selector. 252 | */ 253 | skewer.fn.fetchselector = function(request) { 254 | var element = document.querySelector(request.eval); 255 | return { value: element.innerHTML }; 256 | }; 257 | 258 | /** 259 | * Return a list of completions for an object. 260 | */ 261 | skewer.fn.completions = function(request) { 262 | var object = skewer.globalEval(request.eval); 263 | var keys = new Set(); 264 | var regex = new RegExp(request.regexp); 265 | for (var key in object) { 266 | if (regex.test(key)) { 267 | keys.add(key); 268 | } 269 | } 270 | var props = object != null ? Object.getOwnPropertyNames(object) : []; 271 | for (var i = 0; i < props.length; i++) { 272 | if (regex.test(props[i])) { 273 | keys.add(props[i]); 274 | } 275 | } 276 | return { value: Array.from(keys).sort() }; 277 | }; 278 | 279 | /** 280 | * Host of the skewer script (CORS support). 281 | * @type string 282 | */ 283 | (function() { 284 | var script = document.querySelector('script[src$="/skewer"]'); 285 | if (script) { 286 | skewer.host = script.src.match(/\w+:\/\/[^/]+/)[0]; 287 | } else { 288 | skewer.host = ''; // default to the current host 289 | } 290 | }()); 291 | 292 | /** 293 | * Stringify a potentially circular object without throwing an exception. 294 | * @param object The object to be printed. 295 | * @param {boolean} verbose Enable more verbose output. 296 | * @returns {string} The printed object. 297 | */ 298 | skewer.safeStringify = function (object, verbose) { 299 | "use strict"; 300 | var circular = "#"; 301 | var seen = []; 302 | 303 | var stringify = function(obj) { 304 | if (obj === true) { 305 | return "true"; 306 | } else if (obj === false) { 307 | return "false"; 308 | } else if (obj === undefined) { 309 | return "undefined"; 310 | } else if (obj === null) { 311 | return "null"; 312 | } else if (typeof obj === "number") { 313 | return obj.toString(); 314 | } else if (obj instanceof Array) { 315 | if (seen.indexOf(obj) >= 0) { 316 | return circular; 317 | } else { 318 | seen.push(obj); 319 | return "[" + obj.map(function(e) { 320 | return stringify(e); 321 | }).join(", ") + "]"; 322 | } 323 | } else if (typeof obj === "string") { 324 | return JSON.stringify(obj); 325 | } else if (window.Node != null && obj instanceof Node) { 326 | return obj.toString(); // DOM elements can't stringify 327 | } else if (typeof obj === "function") { 328 | if (verbose) 329 | return obj.toString(); 330 | else 331 | return "Function"; 332 | } else if (Object.prototype.toString.call(obj) === '[object Date]') { 333 | if (verbose) 334 | return JSON.stringify(obj); 335 | else 336 | return obj.toString(); 337 | } else { 338 | if (verbose) { 339 | if (seen.indexOf(obj) >= 0) 340 | return circular; 341 | else 342 | seen.push(obj); 343 | var pairs = []; 344 | for (var key in obj) { 345 | if (obj.hasOwnProperty(key)) { 346 | var pair = JSON.stringify(key) + ":"; 347 | pair += stringify(obj[key]); 348 | pairs.push(pair); 349 | } 350 | } 351 | return "{" + pairs.join(',') + "}"; 352 | } else { 353 | try { 354 | return obj.toString(); 355 | } catch (error) { 356 | return ({}).toString(); 357 | } 358 | } 359 | } 360 | }; 361 | 362 | try { 363 | return stringify(object); 364 | } catch (error) { 365 | return skewer.safeStringify(object, false); 366 | } 367 | }; 368 | 369 | /** 370 | * Log an object to the Skewer REPL in Emacs (console.log). 371 | * @param message The object to be logged. 372 | */ 373 | skewer.log = function() { 374 | "use strict"; 375 | for (var i = 0; i < arguments.length; i++) { 376 | var log = { 377 | type: "log", 378 | value: skewer.safeStringify(arguments[i], true) 379 | }; 380 | skewer.postJSON(skewer.host + "/skewer/post", log); 381 | } 382 | }; 383 | 384 | /** 385 | * Report an error event to the REPL. 386 | * @param event An error event object. 387 | */ 388 | skewer.error = function(event) { 389 | "use strict"; 390 | var log = { 391 | type: "error", 392 | value: event.message, 393 | filename: event.filename, 394 | line: event.lineno, 395 | column: event.column 396 | }; 397 | skewer.postJSON(skewer.host + "/skewer/post", log); 398 | }; 399 | 400 | /** 401 | * Prepare a result when an error occurs evaluating Javascript code. 402 | * @param error The error object given by catch. 403 | * @param result The resutl object to return to Emacs. 404 | * @param request The request object from Emacs. 405 | * @return The result object to send back to Emacs. 406 | */ 407 | skewer.errorResult = function(error, result, request) { 408 | "use strict"; 409 | return skewer.extend({}, result, { 410 | value: error.toString(), 411 | status: 'error', 412 | error: { 413 | name: error.name, 414 | stack: error.stack, 415 | type: error.type, 416 | message: error.message, 417 | eval: request.eval 418 | } 419 | }); 420 | }; 421 | 422 | if (window.addEventListener) { 423 | window.addEventListener('error', skewer.error); 424 | if (document.readyState === 'complete') { 425 | skewer(); 426 | } else { 427 | window.addEventListener('load', skewer); 428 | } 429 | } else { // < IE9 430 | window.attachEvent('onerror', skewer.error); 431 | if (document.readyState === 'complete') { 432 | skewer(); 433 | } else { 434 | window.attachEvent('onload', skewer); 435 | } 436 | } 437 | --------------------------------------------------------------------------------