├── .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 |
--------------------------------------------------------------------------------