Skip to content

SOUP utility functions

Ilmari Karonen edited this page Oct 5, 2018 · 5 revisions

The window.SOUP global object created by SOUP has a number of useful properties and methods for writing fixes, which are listed below.

Note that this "API" is not set in stone, and may be changed in future SOUP versions. Thus, while these properties and methods can technically be accessed from outside SOUP, some caution should be exercised if doing so. (Of course, the SE JavaScript API that SOUP heavily leans on is also subject to change without warning, so...)

BTW, checking for the existence of window.SOUP is the recommended way of detecting whether SOUP is enabled on a page, should you wish to do so for some reason.

Informational properties

These properties should generally be considered read-only, although there's nothing actually stopping you from changing them. Most of these properties are available to all fix JS code, but a few are only defined during the late setup stage, and so are not available to code running in the early, jqinit or mathjax phases.

A lot of useful information can also be found in the non-SOUP window.StackExchange global object, especially under StackExchange.options. Note that, in some cases (like on chat), this object may not be available, or may lack some of its usual features. If your fix code requires it, make sure to check for its existence first.

SOUP.isChat

True if this page is on Stack Exchange chat (i.e. on chat.stackexchange.com, chat.meta.stackexchange.com or chat.stackoverflow.com), false otherwise. Useful for enabling features that only apply to chat, or that should not be applied to chat.

For fixes that only need to run on chat (or that should never run on chat), you should use the sites and exclude fix metadata fields instead. When writing fixes that should run on both chat and the Q&A sites, keep in mind that the JavaScript environment differs in many ways.

SOUP.isMeta

True if this page is on a meta site (either meta.stackexchange.com or one of the per-site metas). As with SOUP.isChat, restricting fixes to or excluding them from meta sites is better done with fix metadata, but this property can still be useful for toggling meta-specific features inside a fix.

SOUP.isMobile (set in late setup)

True if this page is using the mobile web view. Note that mobile mode may be toggled on a per-site basis.

SOUP.isBeta (set in late setup)

True if this SE site is in (public or private) beta. Useful for fixes that are specific to the meta design, or which only apply to users with specific privileges (together with StackExchange.options.user.rep), since the rep thresholds are lower on beta sites. However, note that rep thresholds are actually customizable on a per-site basis, and some graduated sites may also have lowered thresholds for some privileges.

SOUP.isReady

Set to true at the end of the SOUP late setup.

General utility methods

SOUP.log( ... )

A simple wrapper around console.log(), originally introduced for compatibility with old Opera versions, where window.console might not always be available. May be deprecated in the future, but for now, might as well use it. Like console.log(), this method can accept multiple arguments; if you wish to log some data that's not a string, don't concatenate it to a string, but pass it as a separate argument.

SOUP.try( key, code, args )

Runs the function code with the arguments given in the array args inside a try block. If an exception occurs, it is caught and logged (using SOUP.log()). The key string is included in the logged error message to identify the piece of code where the error occurred (since line numbers are often unreliable with dynamically injected scripts like SOUP fixes).

This method is used internally by SOUP to execute all script JS code, with the fix ID as the key. Thus, a single crashed fix will not break the rest of SOUP (or the whole page). Most SOUP utility methods that accept callback functions also invoke them using this method. If your script includes some asynchronous callback code that runs in a context that doesn't already catch and log exceptions, it's highly recommended that you either use SOUP.try() or implement your own error handling with a try / catch block.

SOUP.ready( key, code )

Adds the function code to an internal queue (SOUP.readyQueue), to be executed (via SOUP.try()) when the DOM and jQuery (and the Stack Exchange framework, if available on the page) have loaded. Used internally by SOUP to run fix JS code; you probably don't need to call this yourself (except maybe from an early fix). If you do, note that the keys need to be unique.

SOUP.forEachTextNode( where, code )

Runs the callback function code for each DOM text node within the elements selected by jQuery $(where). Note that the callback is not automatically wrapped in a try block. Within the callback function, this is set to the text node being processed. The text contained in the node is passed as an argument to the callback, and any text returned by the callback will replace the content of the node.

This method is useful for filtering text on the page. For example, to replace all occurrences of "foo" on the page with "bar", you could use:

SOUP.forEachTextNode( document, function ( text ) {
	return text.replace( /foo/g, 'bar' );
} );

(Obviously, this would be a very silly thing to do in a real fix.) See SOUP.addContentFilter() for how to apply the replacement also to dynamically loaded content.

Hook methods

These utility methods are used to execute asynchronous callback functions ("hooks") when certain events, such as AJAX requests, editor preview updates, new chat posts or other things happen. Most SOUP JS fixes make use of these methods, if only to ensure that fixes are properly applied to new content loaded via AJAX.

SOUP.addEditorCallBack( code )

A fairly low-level wrapped for StackExchange.MarkdownEditor.creationCallbacks.add( code ), which executes code when a new Markdown editor instance is created. Does nothing if the SE Markdown editor framework is not available. Remember to wrap any non-trivial callback code in a try block (or use SOUP.try()) to avoid breaking the editor if your code fails.

The code function is passed two parameters:

  • editor: the Markdown editor object itself; use editor.getConverter() to access the Markdown converter. See the wmd.js source code for more information on what you can do with these objects.

  • postfix: a string appended to all the DOM IDs of elements corresponding to this editor instance; needed to correctly support multiple editors per page. May be empty. Use e.g. $("#wmd-preview" + postfix) to access the preview pane for this editor.

This method is used internally by SOUP.hookEditPreview(), which provides a slightly high-level interface (and also shows an example of how to write callbacks for this method).

SOUP.hookEditPreview( code )

Runs the callback function code whenever the Markdown editor preview pane is updated. The callback function is passed the same two parameters (editor and postfix) as for SOUP.addEditorCallBack(). In particular, the preview pane can be accessed using jQuery as $("#wmd-preview" + postfix).

SOUP.hookAjax( regex, code, delay )

Runs code whenever an AJAX request to a URL matching the regular expression regex completes. The code is executed either immediately after SE code has processed the AJAX response, or, optionally, after delay milliseconds. The code is automatically wrapped in a try block to catch and log errors.

This method is implemented using jQuery .ajaxComplete(), and the callback code is passed the same three parameters as .ajaxComplete() handlers, plus one extra parameter match:

  • event: the jQuery ajaxComplete Event object.
  • xhr: the jqXHR object, which extends the browser's native XMLHTTPRequest object. For JSON requests, the returned data can be obtained using $.parseJSON( xhr.responseText ).
  • settings: the options passed to the jQuery .ajax() call used to make this request. The settings.url property contains the URL the request was made to (and which regex is matched against).
  • match: the result of matching regex against settings.url, as returned by RegExp.exec().

The network tab in Firefox / Chrome developer tools is useful for seeing when and to which URLs AJAX requests are made, and what the returned data looks like. When writing regexps, note that SE AJAX requests typically use relative URLs that begin with a slash, e.g. /review/next-task.

This method is used internally by many other SOUP hook methods. For convenience, this method returns the object used to store the hook internally (in the SOUP.ajaxHooks array), which simply contains properties named regex, code and delay that store the respective parameters. A fairly common idiom (somewhat obsoleted by SOUP.addContentFilter()) for running some code both immediately and after certain AJAX events is:

SOUP.hookAjax( ... ).code();

SOUP.addContentFilter( filter, key, where, events )

A high-level hook method intended for fixes that filter or otherwise modify content on the page. The callback function filter is called (using SOUP.try() with the given key string) whenever any of the following named events occur:

  • "load": immediately,
  • "post": when a new post is loaded via AJAX (may also include comments),
  • "comments": when new comments are loaded via AJAX,
  • "preview": when the Markdown editor preview is updated,
  • "chat": when one or more new messages are received in chat, or
  • "usercard": when the mouse is hovered over a user card, causing the card to be expanded.

If the optional events array is given, the filter is only triggered for those events listed in the array. Otherwise, by default, it is triggered for all events. New event types may be added in the future.

The filter callback is passed a single argument where, which is a jQuery selector string, jQuery element set or DOM node containing the new content. All these can be converted into a jQuery element set using $(where). A useful idiom for only applying the filter to elements matching a certain selector within the updated content is:

var nodes = $(where).filter(selector).add( $(selector, where) );

This gives you a jQuery result set containing all the nodes matching selector that either match where or are descendants of nodes matching where.

For the "load" pseudo-event, the where argument defaults to document, but may be overridded by the optional parameter of the same name in the SOUP.addContentFilter() call.

Note: For historical reasons, the optional third and fourth parameters to SOUP.addContentFilter() are inconveniently ordered — many callers will want to select which events to handle, but have no need to override the default where value for "load" events. Such callers should pass null as the third parameter to SOUP.addContentFilter().

Note that there's no guarantee that the elements in $(where) contain only new content; some of it might have been processed before by the same filter. Thus, filters should be idempotent, in the sense that running them twice or more should have the same effect as running them only once.

Tip: The SOUP.forEachTextNode() utility method is useful for implementing content filters that modify text on the page. A basic code snippet for replacing all occurrences of "foo" on the page with "bar" would look like this:

SOUP.addContentFilter( function (where) {
	SOUP.forEachTextNode( where, function (text) {
		return text.replace( /foo/g, 'bar' );
	} );
}, 'example content filter' );

SOUP.subscribeToQuestion( code, key )

This method lets fixes hook into the StackExchange.realtime WebSocket-based notification system, which is used to report new posts, edits and comments and for live update of post scores and answer accept status. The callback function code is executed (using SOUP.try() with the given key) whenever a new realtime notification for the currently viewed question arrives.

The callback code receives a single argument, data, containing the parsed JSON content of the notification. The data.a property indicates the type of action the notification is about:

  • "score": a post was up- or downvoted,
  • "comment-add": new comments were added to a post,
  • "answer-add": a new answer was posted,
  • "accept": an answer was accepted,
  • "unaccept": an answer was unaccepted,
  • "post-edit": a post was edited.

Depending on the type of the action, various other properties may be present in the JSON data:

  • data.acctid: the ID of the user who performed the action, apparently present for all actions.
  • data.answerid: for "answer-add", "accept" and "unaccept" actions, the ID of the answer that was added or (un)accepted.
  • data.id: for "score", "comment-add" and "post-edit" actions, the ID of the post (question or answer) the action affects.
  • data.commentid: for "comment-add" actions, the ID of the comment that was posted.
  • data.score: for "score" actions, the new score of the post.

(Note: The information above about the notification JSON format is based on live observations and on the SE realtime code included in full.en.js, and of course is subject to change by SE without notice.)

TODO: There isn't yet any SOUP method for hooking into other StackExchange.realtime streams, like the topbar stream used to report new inbox notifications and achievements. This could be added if and when needed.