diff --git a/.eslintrc b/.eslintrc index 293a92806..3af0afdc2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -84,6 +84,13 @@ "operator-linebreak": [2, "after"], "space-in-parens": [2, "never"], "no-debugger": "error", + + // + // To enable soon + // + + //"require-jsdoc": "error", + //"valid-jsdoc": "error", // // Disabled rules diff --git a/Gruntfile.js b/Gruntfile.js index 4f0853b88..42921b5e8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -685,7 +685,8 @@ module.exports = function() { destination: "build/doc", package: "package.json", readme: "README.md", - configure: "jsdoc.conf.json" + configure: "jsdoc.conf.json", + template: "doc-template" } } }, @@ -728,7 +729,8 @@ module.exports = function() { files: [ "boomerang.js", "plugins/*.js", - "doc/**/**" + "doc/**/**", + "README.md" ], tasks: ["clean", "jsdoc"] } diff --git a/README.md b/README.md index 79d918720..87422124e 100644 --- a/README.md +++ b/README.md @@ -1,104 +1,199 @@ -Copyright (c) 2011, Yahoo! Inc. All rights reserved. -Copyright (c) 2011-2012, Log-Normal Inc. All rights reserved. -Copyright (c) 2012-2017 SOASTA, Inc. All rights reserved. -Copyright (c) 2017, Akamai Technologies, Inc. All rights reserved. - -Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms. - -boomerang always comes back, except when it hits something. - -summary ---- - -[![Join the chat at https://gitter.im/SOASTA/boomerang](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SOASTA/boomerang?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - -boomerang is a JavaScript library that measures the page load time experienced by real users, commonly called RUM. - -Apart from page load time, boomerang measures a whole bunch of performance characteristics of your user's web browsing experience. All you have to do is stick it into your web pages and call the -init() method. - -usage ---- - +* _Copyright (c) 2011, Yahoo! Inc. All rights reserved._ +* _Copyright (c) 2011-2012, Log-Normal Inc. All rights reserved._ +* _Copyright (c) 2012-2017 SOASTA, Inc. All rights reserved._ +* _Copyright (c) 2017, Akamai Technologies, Inc. All rights reserved._ +* _Copyrights licensed under the BSD License. See the accompanying LICENSE.txt file for terms._ + +**boomerang always comes back, except when it hits something.** + +# Summary + +[![Join the chat at https://gitter.im/SOASTA/boomerang](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/SOASTA/boomerang) + +boomerang is a JavaScript library that measures the page load time experienced by +real users, commonly called RUM (Real User Measurement). It has the ability to +send this data back to your server for further analysis. With boomerang, you +find out exactly how fast your users think your site is. + +Apart from page load time, boomerang measures performance timings, metrics and +characteristics of your user's web browsing experience. All you have to do is +include it in your web pages and call the `BOOMR.init()` method. Once the +performance data is captured, it will be beaconed to your chosen URL. + +boomerang is designed to be a performant and flexible library that can be adapted +to your site's needs. It has an extensive plugin architecture, and works with +both traditional and modern websites (including Single Page Apps). + +boomerang's goal is to not affect the load time of the page (avoiding the +[Observer Effect](https://en.wikipedia.org/wiki/Observer_effect_(information_technology)). +It can be loaded in an asynchronous way that will not delay the page load even +if `boomerang.js` is unavailable. + +# Features + +* Supports: + * IE 6+, Edge, all major versions of Firefox, Chrome, Opera, and Safari + * Desktop and mobile devices +* Captures (all optional): + * Page characteristics such as the URL and Referrer + * Overall page load times (via [NavigationTiming](https://www.w3.org/TR/navigation-timing/) if available) + * DNS, TCP, Request and Response timings (via [NavigationTiming](https://www.w3.org/TR/navigation-timing/)) + * Browser characteristics such as screen size, orientation, memory usage, visibility state + * DOM characteristics such as the number of nodes, HTML length, number of images, scripts, etc + * [ResourceTiming](https://www.w3.org/TR/resource-timing-1/) data (to reconstruct the page's Waterfall) + * Bandwidth + * Mobile connection data + * DNS latency + * JavaScript Errors + * XMLHttpRequest instrumentation + * Third-Party analytics providers IDs + * Single Page App interactions + +# Usage + +boomerang can be included on your page in one of two ways: [synchronously](#synchronously) or [asynchronously](#asynchronously). + +The asynchronous method is recommended. + + ## The simple synchronous way ```html - - + + + ``` -**Note** - you must include at least one plugin (it doesn't have to be rt) or else the beacon will never actually be called. +**Note:** You must include at least one plugin (it doesn't have to be `RT`) or +else the beacon will never fire. + +Each plugin has its own configuration as well -- these configuration options +should be included in the `BOOMR.init()` call: + +```html +BOOMR.init({ + beacon_url: "http://yoursite.com/beacon/", + ResourceTiming: { + enabled: true, + clearOnBeacon: true + } +}); +``` + ## The faster, more involved, asynchronous way -This is what I like to do for sites I control. +Loading boomerang asynchronously ensures that even if `boomerang.js` is +unavailable (or loads slowly), your host page will not be affected. ### 1. Add a plugin to init your code -Create a plugin (call it zzz_init.js or whatever you like) with your init code in there: +Create a plugin (or use the sample `zzz-last-plugin.js`) with a call +to `BOOMR.init`: + ```javascript BOOMR.init({ - config: parameters, - ... + config: parameters, + ... }); +BOOMR.t_end = new Date().getTime(); ``` -You could also include any other code you need. For example, I include a timer to measure when boomerang has finished loading. -I call my plugin `zzz_init.js` to remind me to include it last in the plugin list +You could also include any other code you need. For example, you could include +a timer to measure when boomerang has finished loading (as above). ### 2. Build boomerang -The build process picks up all the plugins referenced in the `plugins.json` file. To change the plugins included in the boomerang build, change the contents of the file to your needs. + +The [build process](#documentation) bundles `boomerang.js` and all of the plugins +listed in `plugins.json` (in that order). + +To build boomerang with all of your desired plugins, you would run: ```bash grunt clean build ``` -This creates deployable boomerang versions in the `build` directory, e.g. `build/boomerang-.min.js`. +This creates a deployable boomerang in the `build` directory, e.g. `build/boomerang-.min.js`. -Install this file on your web server or origin server where your CDN can pick it up. Set a far future max-age header for it. This file will never change. +Install this file on your web server or origin server where your CDN can pick it +up. Set a far future +[max-age](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) +header for it. This file will never change. ### 3. Asynchronously include the script on your page +There are two methods of asynchronously including boomerang on your page: by +adding it to your main document, or via an IFRAME. + +The former method could block your `onload` event (affecting the measured +performance of your page), so the later method is recommended. + + #### 3.1. Adding it to the main document + Include the following code at the *top* of your HTML document: + ```html ``` -Yes, the best practices say to include scripts at the bottom. That's different. That's for scripts that block downloading of other resources. Including a script this -way will not block other resources, however it _will_ block onload. Including the script at the top of your page gives it a good chance of loading -before the rest of your page does thereby reducing the probability of it blocking the `onload` event. If you don't want to block `onload` either, follow Stoyan's -advice from the Meebo team. +Best practices will suggest including all scripts at the bottom of your page. +However, that only applies to scripts that block downloading of other resources. + +Including a script this way will not block other resources, however it _will_ +block `onload`. + +Including the script at the top of your page gives it a good chance of loading +before the rest of your page does, thereby reducing the probability of it +blocking the `onload` event. + +If you don't want to block `onload` either, use the following IFRAME method: -#### 3.2. Adding it via an iframe + +#### 3.2. Adding it via an IFRAME -The method described in 3.1 will still block `onload` on most browsers (Internet Explorer not included). To avoid -blocking `onload`, we could load boomerang in an iframe. Stoyan's documented -the technique on his blog. We've modified it to work across browsers with different configurations, documented on -the lognormal blog. +The method described in 3.1 will still block `onload` on most browsers. -For boomerang, this is the code you'll include: +To avoid blocking `onload`, we can load boomerang in an asynchronous IFRAME. +The general process is documented on in +[this blog post](http://www.lognormal.com/blog/2012/12/12/the-script-loader-pattern/). + +For boomerang, the asynchronous loader snippet you'll use is: ```html ``` -The `id` of the script node created by this code MUST be `boomr-if-as` as boomerang looks for that id to determine if it's running within an iframe or not. -Boomerang will still export the `BOOMR` object to the parent window if running inside an iframe, so the rest of your code should remain unchanged. +The `id` of the script node created by this code MUST be `boomr-if-as` as +boomerang looks for that id to determine if it's running within an IFRAME or not. + +boomerang will still export the `BOOMR` object to the parent window if running +inside an IFRAME, so the rest of your code should remain unchanged. #### 3.3. Identifying when boomerang has loaded -If you load boomerang asynchronously, there's some uncertainty in when boomerang has completed loading. To get around this, you can subscribe to the +If you load boomerang asynchronously, there's some uncertainty in when boomerang +has completed loading. To get around this, you can subscribe to the `onBoomerangLoaded` Custom Event on the `document` object: ```javascript - // Modern browsers - if (document.addEventListener) { - document.addEventListener("onBoomerangLoaded", function(e) { - // e.detail.BOOMR is a reference to the BOOMR global object - }); - } - // IE 6, 7, 8 we use onPropertyChange and look for propertyName === "onBoomerangLoaded" - else if (document.attachEvent) { - document.attachEvent("onpropertychange", function(e) { - if (!e) e=event; - if (e.propertyName === "onBoomerangLoaded") { - // e.detail.BOOMR is a reference to the BOOMR global object - } - }); - } - +// Modern browsers +if (document.addEventListener) { + document.addEventListener("onBoomerangLoaded", function(e) { + // e.detail.BOOMR is a reference to the BOOMR global object + }); +} +// IE 6, 7, 8 we use onPropertyChange and look for propertyName === "onBoomerangLoaded" +else if (document.attachEvent) { + document.attachEvent("onpropertychange", function(e) { + if (!e) e=event; + if (e.propertyName === "onBoomerangLoaded") { + // e.detail.BOOMR is a reference to the BOOMR global object + } + }); +} ``` -Note that this only works on browsers that support the CustomEvent interface, which at this time is Chrome (including Android), Firefox 6+ (including Android), -Opera (including Android, but not Opera Mini), Safari (including iOS), IE 6+ (but see the code above for the special way to listen for the event on IE6, 7 & 8). +Note that this only works on browsers that support the CustomEvent interface, +which is Chrome (including Android), Firefox 6+ (including Android), Opera +(including Android, but not Opera Mini), Safari (including iOS), IE 6+ +(but see the code above for the special way to listen for the event on IE6, 7 & 8). -Boomerang also fires the `onBeforeBoomerangBeacon` and `onBoomerangBeacon` events just before and during beaconing. +boomerang also fires the `onBeforeBoomerangBeacon` and `onBoomerangBeacon` +events just before and during beaconing. -#### 3.4. Method queue pattern + +# Documentation -If you want to call a public method that lives on `BOOMR`, but either don't know if Boomerang has loaded or don't want to wait, you can use the method queue pattern! +Documentation is in the `docs/` directory. Boomerang documentation is +written in Markdown and is built via [JSDoc](http://usejsdoc.org/). -Instead of: -```javascript -BOOMR.addVar('myVarName', 'myVarValue') -``` +You can build the current documentation by running Grunt: -... you can write: -```javascript -BOOMR_mq = window.BOOMR_mq || []; -BOOMR_mq.push(['addVar', 'myVarName', 'myVarValue']); ``` - -Or, if you care about the return value, instead of: -```javascript -var hasMyVar = BOOMR.hasVar('myVarName'); -``` -... you can write: -```javascript -var hasMyVar; -BOOMR_mq = window.BOOMR_mq || []; -BOOMR_mq.push({ - arguments: ['hasVar', 'myVarName'], - callback: function(returnValue) { - hasMyVar = returnValue; - } -}); +grunt jsdoc ``` -docs ---- -Documentation is in the docs/ sub directory, and is written in HTML. Your best bet is to check it out and view it locally, though it works best through a web server (you'll need cookies). -Thanks to github's awesome `gh-pages` feature, we're able to host the boomerang docs right here on github. Visit http://soasta.github.com/boomerang/doc/ for a browsable version where all -the examples work. +HTML files will be built under `build/docs`. + +Documentation is also currently published at [docs.soasta.com/boomerang-api/](https://docs.soasta.com/boomerang-api/). + +mPulse-specific Boomerang documentation is also available at [docs.soasta.com/boomerang/](https://docs.soasta.com/boomerang/). + +There is a lot more documentation available: + +- [API Documentation](https://docs.soasta.com/boomerang-api/): The `BOOMR` API +- [Building Boomerang](https://docs.soasta.com/boomerang-api/tutorial-building.html): How to build boomerang with plugins +- [Contributing](https://docs.soasta.com/boomerang-api/tutorial-contributing.html): Contributing to the open-source project +- [Creating Plugins](https://docs.soasta.com/boomerang-api/tutorial-creating-plugins.html): Creating a plugin +- [Methodology](https://docs.soasta.com/boomerang-api/tutorial-methodology.html): How boomerang works internally +- [How-Tos](https://docs.soasta.com/boomerang-api/tutorial-howtos.html): Short recipes on how to do a bunch of things with boomerang + +# Source code + +The boomerang source code is primarily on GitHub at [github.com/SOASTA/boomerang](https://github.com/SOASTA/boomerang). + +Feel free to fork it and [contribute](https://docs.soasta.com/boomerang-api/tutorial-contributing.html) to it. + +You can also get a [check out the releases](https://github.com/SOASTA/boomerang/releases) +or download a [tarball](https://github.com/SOASTA/boomerang/archive/master.tar.gz) or +[zip](http://github.com/SOASTA/boomerang/archive/master.zip) of the code. + +# Support + +We use [GitHub Issues](https://github.com/SOASTA/boomerang/issues) for discussions, +feature requests and bug reports. + +Get in touch at [github.com/SOASTA/boomerang/issues](https://github.com/SOASTA/boomerang/issues). + +boomerang is supported by the developers at [Akamai](http://akamai.com/), and the +awesome community of open-source developers that use and hack it. That's you. Thank you! -In case you're browsing this elsewhere, the latest development version of the code and docs are available at https://github.com/bluesmoon/boomerang/, while the latest stable version is -at https://github.com/SOASTA/boomerang/ +# Contributions -support ---- -We use github issues for discussions, feature requests and bug reports. Get in touch at https://github.com/SOASTA/boomerang/issues -You'll need a github account to participate, but then you'll need one to check out the code as well :) +Boomerang is brought to you by: -Thanks for dropping by, and please leave us a message telling us if you use boomerang. +* the former [Exceptional Performance](http://developer.yahoo.com/performance/) team at the company once known as + [Yahoo!](http://www.yahoo.com/), aided by the [Yahoo! Developer Network](http://developer.yahoo.com/), +* the folks at [LogNormal](http://www.lognormal.com/), continued by +* the mPulse team at [SOASTA](https://www.soasta.com/), ongoing by +* the mPulse team at [Akamai](https://www.akamai.com/), and +* many independent contributors whose contributions are cemented in our git history -boomerang is supported by the devs at Akamai, and the awesome community of opensource developers that use -and hack it. That's you. Thank you! +To help out, please read our [contributing](https://docs.soasta.com/boomerang-api/tutorial-contributing.html) page. diff --git a/boomerang.js b/boomerang.js index 2eb531463..fdc376846 100644 --- a/boomerang.js +++ b/boomerang.js @@ -7,7 +7,7 @@ */ /** - * @namespace Boomerang + * @class BOOMR * @desc * boomerang measures various performance characteristics of your user's browsing * experience and beacons it back to your server. @@ -15,34 +15,74 @@ * To use this you'll need a web site, lots of users and the ability to do * something with the data you collect. How you collect the data is up to * you, but we have a few ideas. -*/ + * + * Everything in boomerang is accessed through the `BOOMR` object, which is + * available on `window.BOOMR`. It contains the public API, utility functions + * ({@link BOOMR.utils}) and all of the plugins ({@link BOOMR.plugins}). + * + * Each plugin has its own API, but is reachable through {@link BOOMR.plugins}. + * + * ## Beacon Parameters + * + * The core boomerang object will add the following parameters to the beacon. + * + * Note that each individual {@link BOOMR.plugins plugin} will add its own + * parameters as well. + * + * * `v`: Boomerang version + * * `u`: The page's URL (for most beacons), or the `XMLHttpRequest` URL + * * `pgu`: The page's URL (for `XMLHttpRequest` beacons) + * * `pid`: Page ID (8 characters) + * * `r`: Navigation referrer (from the cookie) + * * `r2`: Navigation referrer (from `document.location`, if different than `r`) + * * `vis.pre`: `1` if the page transitioned from prerender to visible + * * `xhr.pg`: The `XMLHttpRequest` page group + * * `errors`: Error messages of errors detected in Boomerang code, separated by a newline + */ /** - * @memberof Boomerang + * @typedef TimeStamp + * @type {number} + * + * @desc + * A [Unix Epoch](https://en.wikipedia.org/wiki/Unix_time) timestamp (milliseconds + * since 1970) created by [BOOMR.now()]{@link BOOMR.now}. + * + * If `DOMHighResTimeStamp` (`performance.now()`) is supported, it is + * a `DOMHighResTimeStamp` (with microsecond resolution in the fractional), + * otherwise, it is `Date.now()`. + */ + +/** + * @global * @type {TimeStamp} * @desc - * Measure the time the script started - * This has to be global so that we don't wait for the entire - * BOOMR function to download and execute before measuring the + * Timestamp the boomerang.js script started executing. + * + * This has to be global so that we don't wait for this entire + * script to download and execute before measuring the * time. We also declare it without `var` so that we can later - * `delete` it. This is the only way that works on Internet Explorer -*/ + * `delete` it. This is the only way that works on Internet Explorer. + */ BOOMR_start = new Date().getTime(); /** * @function + * @global * @desc - * Check the value of document.domain and fix it if incorrect. + * Check the value of `document.domain` and fix it if incorrect. + * * This function is run at the top of boomerang, and then whenever - * init() is called. If boomerang is running within an iframe, this + * {@link BOOMR.init} is called. If boomerang is running within an IFRAME, this * function checks to see if it can access elements in the parent - * iframe. If not, it will fudge around with document.domain until + * IFRAME. If not, it will fudge around with `document.domain` until * it finds a value that works. * - * This allows site owners to change the value of document.domain at + * This allows site owners to change the value of `document.domain` at * any point within their page's load process, and we will adapt to * it. - * @param {string} domain - domain name as retrieved from page url + * + * @param {string} domain Domain name as retrieved from page URL */ function BOOMR_check_doc_domain(domain) { /*eslint no-unused-vars:0*/ @@ -106,11 +146,9 @@ function BOOMR_check_doc_domain(domain) { BOOMR_check_doc_domain(); - -// beaconing section -// the parameter is the window +// Construct BOOMR +// w is window (function(w) { - var impl, boomr, d, myurl, createCustomEvent, dispatchEvent, visibilityState, visibilityChange, orig_w = w; // This is the only block where we use document without the w. qualifier @@ -124,18 +162,66 @@ BOOMR_check_doc_domain(); d = w.document; // Short namespace because I don't want to keep typing BOOMERANG - if (!w.BOOMR) { w.BOOMR = {}; } + if (!w.BOOMR) { + w.BOOMR = {}; + } + BOOMR = w.BOOMR; + // don't allow this code to be included twice if (BOOMR.version) { return; } + /** + * Boomerang version, formatted as major.minor.patchlevel. + * + * This variable is replaced during build (`grunt build`). + * + * @type {string} + * + * @memberof BOOMR + */ BOOMR.version = "%boomerang_version%"; + + /** + * The main document window. + * * If Boomerang was loaded in an IFRAME, this is the parent window + * * If Boomerang was loaded inline, this is the current window + * + * @type {Window} + * + * @memberof BOOMR + */ BOOMR.window = w; + + /** + * The Boomerang frame: + * * If Boomerang was loaded in an IFRAME, this is the IFRAME + * * If Boomerang was loaded inline, this is the current window + * + * @type {Window} + * + * @memberof BOOMR + */ BOOMR.boomerang_frame = orig_w; - if (!BOOMR.plugins) { BOOMR.plugins = {}; } + /** + * @class BOOMR.plugins + * @desc + * Boomerang plugin namespace. + * + * All plugins should add their plugin object to `BOOMR.plugins`. + * + * A plugin should have, at minimum, the following exported functions: + * * `init(config)` + * * `is_complete()` + * + * See {@tutorial creating-plugins} for details. + */ + if (!BOOMR.plugins) { + BOOMR.plugins = {}; + } // CustomEvent proxy for IE9 & 10 from https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent (function() { @@ -181,12 +267,12 @@ BOOMR_check_doc_domain(); }()); /** - dispatch a custom event to the browser - @param e_name The custom event name that consumers can subscribe to - @param e_data Any data passed to subscribers of the custom event via the `event.detail` property - @param async By default, custom events are dispatched immediately. - Set to true if the event should be dispatched once the browser has finished its current - JavaScript execution. + * Dispatch a custom event to the browser + * @param {string} e_name The custom event name that consumers can subscribe to + * @param {object} e_data Any data passed to subscribers of the custom event via the `event.detail` property + * @param {boolean} async By default, custom events are dispatched immediately. + * Set to true if the event should be dispatched once the browser has finished its current + * JavaScript execution. */ dispatchEvent = function(e_name, e_data, async) { var ev = createCustomEvent(e_name, {"detail": e_data}); @@ -223,7 +309,7 @@ BOOMR_check_doc_domain(); // https://developer.mozilla.org/en-US/docs/Web/Guide/User_experience/Using_the_Page_Visibility_API // Set the name of the hidden property and the change event for visibility - if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support + if (typeof document.hidden !== "undefined") { visibilityState = "visibilityState"; visibilityChange = "visibilitychange"; } @@ -240,30 +326,37 @@ BOOMR_check_doc_domain(); visibilityChange = "webkitvisibilitychange"; } - // impl is a private object not reachable from outside the BOOMR object - // users can set properties by passing in to the init() method + // impl is a private object not reachable from outside the BOOMR object. + // Users can set properties by passing in to the init() method. impl = { - // properties + // Beacon URL beacon_url: "", - // beacon request method, either GET, POST or AUTO. AUTO will check the - // request size then use GET if the request URL is less than MAX_GET_LENGTH chars - // otherwise it will fall back to a POST request. + + // Beacon request method, either GET, POST or AUTO. AUTO will check the + // request size then use GET if the request URL is less than MAX_GET_LENGTH + // chars. Otherwise, it will fall back to a POST request. beacon_type: "AUTO", - // beacon authorization key value. Most systems will use the 'Authentication' keyword, but some - // some services use keys like 'X-Auth-Token' or other custom keys + + // Beacon authorization key value. Most systems will use the 'Authentication' + // keyword, but some some services use keys like 'X-Auth-Token' or other + // custom keys. beacon_auth_key: "Authorization", - // beacon authorization token. This is only needed if your are using a POST and - // the beacon requires an Authorization token to accept your data + + // Beacon authorization token. This is only needed if your are using a POST + // and the beacon requires an Authorization token to accept your data. beacon_auth_token: undefined, - // strip out everything except last two parts of hostname. + + // Strip out everything except last two parts of hostname. // This doesn't work well for domains that end with a country tld, // but we allow the developer to override site_domain for that. - // You can disable all cookies by setting site_domain to a falsy value + // You can disable all cookies by setting site_domain to a falsy value. site_domain: w.location.hostname. replace(/.*?([^.]+\.[^.]+)\.?$/, "$1"). toLowerCase(), - //! User's ip address determined on the server. Used for the BA cookie + + // User's ip address determined on the server. Used for the BW cookie. user_ip: "", + // Whether or not to send beacons on page load autorun: true, @@ -276,37 +369,326 @@ BOOMR_check_doc_domain(); // document.referrer r2: undefined, - //! strip_query_string: false, + // strip_query_string: false, - //! onloadfired: false, + // onloadfired: false, - //! handlers_attached: false, + // handlers_attached: false, events: { + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when the page is usable by the user. + * + * By default this is fired when `window.onload` fires, but if you + * set `autorun` to false when calling {@link BOOMR.init}, then you + * must explicitly fire this event by calling {@link BOOMR#event:page_ready}. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onload} + * @event BOOMR#page_ready + * @property {Event} [event] Event triggering the page_ready + */ "page_ready": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired just before the browser unloads the page. + * + * The first event of `window.pagehide`, `window.beforeunload`, + * or `window.unload` will trigger this. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/pagehide} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onunload} + * @event BOOMR#page_unload + */ "page_unload": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired before the document is about to be unloaded. + * + * `window.beforeunload` will trigger this. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload} + * @event BOOMR#before_unload + */ "before_unload": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired on `document.DOMContentLoaded`. + * + * The `DOMContentLoaded` event is fired when the initial HTML document + * has been completely loaded and parsed, without waiting for stylesheets, + * images, and subframes to finish loading + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded} + * @event BOOMR#dom_loaded + */ "dom_loaded": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired on `document.visibilitychange`. + * + * The `visibilitychange` event is fired when the content of a tab has + * become visible or has been hidden. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/visibilitychange} + * @event BOOMR#visibility_changed + */ "visibility_changed": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when the `visibilityState` of the document has changed from + * `prerender` to `visible` + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/Events/visibilitychange} + * @event BOOMR#prerender_to_visible + */ "prerender_to_visible": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when a beacon is about to be sent. + * + * The subscriber can still add variables to the beacon at this point, + * either by modifying the `vars` paramter or calling {@link BOOMR.addVar}. + * + * @event BOOMR#before_beacon + * @property {object} vars Beacon variables + */ "before_beacon": [], - "onbeacon": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when a beacon was sent. + * + * The beacon variables cannot be modified at this point. Any calls + * to {@link BOOMR.addVar} or {@link BOOMR.removeVar} will apply to the + * next beacon. + * + * Also known as `onbeacon`. + * + * @event BOOMR#beacon + * @property {object} vars Beacon variables + */ + "beacon": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when the page load beacon has been sent. + * + * This event should only happen once on a page. It does not apply + * to SPA soft navigations. + * + * @event BOOMR#page_load_beacon + * @property {object} vars Beacon variables + */ "page_load_beacon": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when an XMLHttpRequest has finished, or, if something calls + * {@link BOOMR.responseEnd}. + * + * @event BOOMR#xhr_load + * @property {object} data Event data + */ "xhr_load": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when the `click` event has happened on the `document`. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onclick} + * @event BOOMR#click + */ "click": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired when any `FORM` element is submitted. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit} + * @event BOOMR#form_submit + */ "form_submit": [], - "onconfig": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever new configuration data is applied via {@link BOOMR.init}. + * + * Also known as `onconfig`. + * + * @event BOOMR#config + * @property {object} data Configuration data + */ + "config": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever `XMLHttpRequest.open` is called. + * + * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. + * + * @event BOOMR#xhr_init + * @property {string} type XHR type ("xhr") + */ "xhr_init": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever a SPA plugin is about to track a new navigation. + * + * @event BOOMR#spa_init + * @property {string} navType Navigation type (`spa` or `spa_hard`) + * @property {object} param SPA navigation parameters + */ "spa_init": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever a SPA navigation is complete. + * + * @event BOOMR#spa_navigation + */ "spa_navigation": [], - "xhr_send": [] + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever `XMLHttpRequest.send` is called. + * + * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. + * + * @event BOOMR#xhr_send + * @property {object} xhr `XMLHttpRequest` object + */ + "xhr_send": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever and `XMLHttpRequest` has an error (if its `status` is + * set). + * + * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. + * + * Also known as `onxhrerror`. + * + * @event BOOMR#xhr_error + * @property {object} data XHR data + */ + "xhr_error": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever a page error has happened. + * + * This event will only happen if {@link BOOMR.plugins.Errors} is enabled. + * + * Also known as `onerror`. + * + * @event BOOMR#error + * @property {object} err Error + */ + "error": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever an `XMLHttpRequest.send()` is called + * + * This event will only happen if {@link BOOMR.plugins.AutoXHR} is enabled. + * + * @event BOOMR#xhr_send + * @property {object} req XMLHttpRequest + */ + "xhr_send": [], + + /** + * Boomerang event, subscribe via {@link BOOMR.subscribe}. + * + * Fired whenever connection information changes via the + * Network Information API. + * + * This event will only happen if {@link BOOMR.plugins.Mobile} is enabled. + * + * @event BOOMR#netinfo + * @property {object} connection `navigator.connection` + */ + "netinfo": [] }, + /** + * Public events + */ public_events: { + /** + * Public event (fired on `document`), and can be subscribed via + * `document.addEventListener("onBeforeBoomerangBeacon", ...)` or + * `document.attachEvent("onpropertychange", ...)`. + * + * Maps to {@link BOOMR#event:before_beacon} + * + * @event document#onBeforeBoomerangBeacon + * @property {object} vars Beacon variables + */ "before_beacon": "onBeforeBoomerangBeacon", - "onbeacon": "onBoomerangBeacon", + + /** + * Public event (fired on `document`), and can be subscribed via + * `document.addEventListener("onBoomerangBeacon", ...)` or + * `document.attachEvent("onpropertychange", ...)`. + * + * Maps to {@link BOOMR#event:before_beacon} + * + * @event document#onBoomerangBeacon + * @property {object} vars Beacon variables + */ + "beacon": "onBoomerangBeacon", + + /** + * Public event (fired on `document`), and can be subscribed via + * `document.addEventListener("onBoomerangLoaded", ...)` or + * `document.attachEvent("onpropertychange", ...)`. + * + * Fired when {@link BOOMR} has loaded and can be used. + * + * @event document#onBoomerangLoaded + */ "onboomerangloaded": "onBoomerangLoaded" }, + /** + * Maps old event names to their updated name + */ + translate_events: { + "onbeacon": "beacon", + "onconfig": "config", + "onerror": "error", + "onxhrerror": "xhr_error" + }, + listenerCallbacks: {}, vars: {}, @@ -338,7 +720,9 @@ BOOMR_check_doc_domain(); // don't capture events on flash objects // because of context slowdowns in PepperFlash - if (target && target.nodeName.toUpperCase() === "OBJECT" && target.type === "application/x-shockwave-flash") { + if (target && + target.nodeName.toUpperCase() === "OBJECT" && + target.type === "application/x-shockwave-flash") { return; } impl.fireEvent(type, target); @@ -379,6 +763,11 @@ BOOMR_check_doc_domain(); e_name = e_name.toLowerCase(); + // translate old names + if (this.translate_events[e_name]) { + e_name = this.translate_events[e_name]; + } + if (!this.events.hasOwnProperty(e_name)) { return;// false; } @@ -391,7 +780,7 @@ BOOMR_check_doc_domain(); // Before we fire any event listeners, let's call real_sendBeacon() to flush // any beacon that is being held by the setImmediate. - if (e_name !== "before_beacon" && e_name !== "onbeacon") { + if (e_name !== "before_beacon" && e_name !== "beacon") { BOOMR.real_sendBeacon(); } @@ -430,26 +819,78 @@ BOOMR_check_doc_domain(); // we don't overwrite anything additional that was added to BOOMR before this // was called... for example, a plugin. boomr = { - //! t_lstart: value of BOOMR_lstart set in host page + /** + * The timestamp when boomerang.js showed up on the page. + * + * This is the value of `BOOMR_start` we set earlier. + * @type {TimeStamp} + * + * @memberof BOOMR + */ t_start: BOOMR_start, - //! t_end: value set in zzz-last-plugin.js + /** + * When the Boomerang plugins have all run. + * + * This value is generally set in zzz-last-plugin.js. + * @type {TimeStamp} + * + * @memberof BOOMR + */ + t_end: undefined, + + /** + * URL of boomerang.js. This is only set if using the asynchronous loader snippet. + * + * @type {string} + * + * @memberof BOOMR + */ url: myurl, - // constants visible to the world + /** + * Whether or not Boomerang was loaded after the `onload` event. + * + * @type {boolean} + * + * @memberof BOOMR + */ + loadedLate: false, + + /** + * Constants visible to the world + * @class BOOMR.constants + */ constants: { - // SPA beacon types + /** + * SPA beacon types + * + * @type {string[]} + * + * @memberof BOOMR.constants + */ BEACON_TYPE_SPAS: ["spa", "spa_hard"], - // using 2000 here as a de facto maximum URL length based on: - // http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers + + /** + * Maximum GET URL length. + * Using 2000 here as a de facto maximum URL length based on: + * http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers + * + * @type {number} + * + * @memberof BOOMR.constants + */ MAX_GET_LENGTH: 2000 }, - // Utility functions + /** + * @class BOOMR.utils + */ utils: { /** - * Validate that the current frame has support for postMessage and can send iframe postMessages - * @returns {boolean} - true if we have postMessage support, false if we don't + * Determines whether or not the browser has `postMessage` support + * + * @returns {boolean} True if supported */ hasPostMessageSupport: function() { if (!w.postMessage || typeof w.postMessage !== "function" && typeof w.postMessage !== "object") { @@ -457,6 +898,18 @@ BOOMR_check_doc_domain(); } return true; }, + + /** + * Converts an object to a string. + * + * @param {object} o Object + * @param {string} separator Member separator + * @param {number} nest_level Number of levels to recruse + * + * @returns {string} String representation of the object + * + * @memberof BOOMR.utils + */ objectToString: function(o, separator, nest_level) { var value = [], k; @@ -519,6 +972,15 @@ BOOMR_check_doc_domain(); return value.join(separator); }, + /** + * Gets the value of the cookie identified by `name`. + * + * @param {string} name Cookie name + * + * @returns {string|null} Cookie value, if set. + * + * @memberof BOOMR.utils + */ getCookie: function(name) { if (!name) { return null; @@ -535,6 +997,23 @@ BOOMR_check_doc_domain(); } }, + /** + * Sets the cookie named `name` to the serialized value of `subcookies`. + * + * @param {string} name The name of the cookie + * @param {object} subcookies Key/value pairs to write into the cookie. + * These will be serialized as an & separated list of URL encoded key=value pairs. + * @param {number} max_age Lifetime in seconds of the cookie. + * Set this to 0 to create a session cookie that expires when + * the browser is closed. If not set, defaults to 0. + * + * @returns {boolean} True if the cookie was set successfully + * + * @example + * BOOMR.utils.setCookie("RT", { s: t_start, r: url }); + * + * @memberof BOOMR.utils + */ setCookie: function(name, subcookies, max_age) { var value, nameval, savedval, c, exp; @@ -542,7 +1021,8 @@ BOOMR_check_doc_domain(); BOOMR.debug("No cookie name or site domain: " + name + "/" + impl.site_domain); BOOMR.addVar("nocookie", 1); - return null; + + return false; } value = this.objectToString(subcookies, "&"); @@ -573,6 +1053,18 @@ BOOMR_check_doc_domain(); return false; }, + /** + * Parse a cookie string returned by {@link BOOMR.utils.getCookie} and + * split it into its constituent subcookies. + * + * @param {string} cookie Cookie value + * + * @returns {object} On success, an object of key/value pairs of all + * sub cookies. Note that some subcookies may have empty values. + * `null` if `cookie` was not set or did not contain valid subcookies. + * + * @memberof BOOMR.utils + */ getSubCookies: function(cookie) { var cookies_a, i, l, kv, @@ -602,6 +1094,14 @@ BOOMR_check_doc_domain(); return gotcookies ? cookies : null; }, + /** + * Removes the cookie identified by `name` by nullifying its value, + * and making it a session cookie. + * + * @param {string} name Cookie name + * + * @memberof BOOMR.utils + */ removeCookie: function(name) { return this.setCookie(name, {}, -86400); }, @@ -614,6 +1114,8 @@ BOOMR_check_doc_domain(); * @param {number} urlLimit Maximum size, in characters, of the URL * * @returns {string} Cleaned up URL + * + * @memberof BOOMR.utils */ cleanupURL: function(url, urlLimit) { if (!url || BOOMR.utils.isArray(url)) { @@ -639,6 +1141,16 @@ BOOMR_check_doc_domain(); return url; }, + /** + * Gets the URL with the query string replaced with a MD5 hash of its contents. + * + * @param {string} url URL + * @param {boolean} stripHash Whether or not to strip the hash + * + * @returns {string} URL with query string hashed + * + * @memberof BOOMR.utils + */ hashQueryString: function(url, stripHash) { if (!url) { return url; @@ -660,9 +1172,27 @@ BOOMR_check_doc_domain(); if (!BOOMR.utils.MD5) { return url; } - return url.replace(/\?([^#]*)/, function(m0, m1) { return "?" + (m1.length > 10 ? BOOMR.utils.MD5(m1) : m1); }); + return url.replace(/\?([^#]*)/, function(m0, m1) { + return "?" + (m1.length > 10 ? BOOMR.utils.MD5(m1) : m1); + }); }, + /** + * Sets the object's properties if anything in config matches + * one of the property names. + * + * @param {object} o The plugin's `impl` object within which it stores + * all its configuration and private properties + * @param {object} config The config object passed in to the plugin's + * `init()` method. + * @param {string} plugin_name The plugin's name in the {@link BOOMR.plugins} object. + * @param {string[]} properties An array containing a list of all configurable + * properties that this plugin has. + * + * @returns {boolean} True if a property was set + * + * @memberof BOOMR.utils + */ pluginConfig: function(o, config, plugin_name, properties) { var i, props = 0; @@ -679,13 +1209,16 @@ BOOMR_check_doc_domain(); return (props > 0); }, + /** * `filter` for arrays * - * @private * @param {Array} array The array to iterate over. * @param {Function} predicate The function invoked per iteration. + * * @returns {Array} Returns the new filtered array. + * + * @memberof BOOMR.utils */ arrayFilter: function(array, predicate) { var result = []; @@ -712,13 +1245,30 @@ BOOMR_check_doc_domain(); } return result; }, + /** - * `find` for arrays + * The callback function may return a falsy value to disconnect the + * observer after it returns, or a truthy value to keep watching for + * mutations. If the return value is numeric and greater than 0, then + * this will be the new timeout. If it is boolean instead, then the + * timeout will not fire any more so the caller MUST call disconnect() + * at some point. * - * @private - * @param {Array} array The array to iterate over. - * @param {Function} predicate The function invoked per iteration. - * @returns {Array} Returns the value of first element that satisfies the predicate. + * @callback BOOMR~addObserverCallback + * @param {object[]} mutations List of mutations detected by the observer or `undefined` if the observer timed out + * @param {object} callback_data Is the passed in `callback_data` parameter without modifications + */ + + /** + * `find` for Arrays + * + * @param {Array} array The array to iterate over + * @param {Function} predicate The function invoked per iteration + * + * @returns {Array} Returns the value of first element that satisfies + * the predicate + * + * @memberof BOOMR.utils */ arrayFind: function(array, predicate) { if (!(BOOMR.utils.isArray(array) || (array && typeof array.length === "number")) || @@ -743,32 +1293,29 @@ BOOMR_check_doc_domain(); return undefined; } }, + /** - * @desc * Add a MutationObserver for a given element and terminate after `timeout`ms. - * @param el DOM element to watch for mutations - * @param config MutationObserverInit object (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationObserverInit) - * @param timeout Number of milliseconds of no mutations after which the observer should be automatically disconnected - * If set to a falsy value, the observer will wait indefinitely for Mutations. - * @param callback Callback function to call either on timeout or if mutations are detected. The signature of this method is: - * function(mutations, callback_data) - * Where: - * mutations is the list of mutations detected by the observer or `undefined` if the observer timed out - * callback_data is the passed in `callback_data` parameter without modifications - * - * The callback function may return a falsy value to disconnect the observer after it returns, or a truthy value to - * keep watching for mutations. If the return value is numeric and greater than 0, then this will be the new timeout - * if it is boolean instead, then the timeout will not fire any more so the caller MUST call disconnect() at some point - * @param callback_data Any data to be passed to the callback function as its second parameter - * @param callback_ctx An object that represents the `this` object of the `callback` method. Leave unset the callback function is not a method of an object - * - * @returns {?object} - `null` if a MutationObserver could not be created OR - * - An object containing the observer and the timer object: - * { observer: , timer: } - * - * The caller can use this to disconnect the observer at any point by calling `retval.observer.disconnect()` - * Note that the caller should first check to see if `retval.observer` is set before calling `disconnect()` as it may - * have been cleared automatically. + * + * @param {DOMElement} el DOM element to watch for mutations + * @param {MutationObserverInit} config MutationObserverInit object (https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationObserverInit) + * @param {number} timeout Number of milliseconds of no mutations after which the observer should be automatically disconnected. + * If set to a falsy value, the observer will wait indefinitely for Mutations. + * @param {BOOMR~addObserverCallback} callback Callback function to call either on timeout or if mutations are detected. + * @param {object} callback_data Any data to be passed to the callback function as its second parameter. + * @param {object} callback_ctx An object that represents the `this` object of the `callback` method. + * Leave unset the callback function is not a method of an object. + * + * @returns {object|null} + * - `null` if a MutationObserver could not be created OR + * - An object containing the observer and the timer object: + * `{ observer: , timer: }` + * - The caller can use this to disconnect the observer at any point + * by calling `retval.observer.disconnect()` + * - Note that the caller should first check to see if `retval.observer` + * is set before calling `disconnect()` as it may have been cleared automatically. + * + * @memberof BOOMR.utils */ addObserver: function(el, config, timeout, callback, callback_data, callback_ctx) { var o = {observer: null, timer: null}; @@ -814,6 +1361,15 @@ BOOMR_check_doc_domain(); return o; }, + /** + * Adds an event listener. + * + * @param {DOMElement} el DOM element + * @param {string} type Event name + * @param {function} fn Callback function + * + * @memberof BOOMR.utils + */ addListener: function(el, type, fn) { if (el.addEventListener) { el.addEventListener(type, fn, false); @@ -829,6 +1385,15 @@ BOOMR_check_doc_domain(); impl.listenerCallbacks[type].push({ el: el, fn: fn}); }, + /** + * Removes an event listener. + * + * @param {DOMElement} el DOM element + * @param {string} type Event name + * @param {function} fn Callback function + * + * @memberof BOOMR.utils + */ removeListener: function(el, type, fn) { var i; @@ -841,45 +1406,38 @@ BOOMR_check_doc_domain(); if (impl.listenerCallbacks.hasOwnProperty(type)) { for (var i = 0; i < impl.listenerCallbacks[type].length; i++) { - if (fn === impl.listenerCallbacks[type][i].fn && - el === impl.listenerCallbacks[type][i].el) { - impl.listenerCallbacks[type].splice(i, 1); - return; - } - } - } - }, - - pushVars: function(form, vars, prefix) { - var k, i, l = 0, input; - - for (k in vars) { - if (vars.hasOwnProperty(k)) { - if (BOOMR.utils.isArray(vars[k])) { - for (i = 0; i < vars[k].length; ++i) { - l += BOOMR.utils.pushVars(form, vars[k][i], k + "[" + i + "]"); - } - } - else { - input = document.createElement("input"); - input.type = "hidden"; // we need `hidden` to preserve newlines. see commit message for more details - input.name = (prefix ? (prefix + "[" + k + "]") : k); - input.value = (vars[k] === undefined || vars[k] === null ? "" : vars[k]); - - form.appendChild(input); - - l += encodeURIComponent(input.name).length + encodeURIComponent(input.value).length + 2; + if (fn === impl.listenerCallbacks[type][i].fn && + el === impl.listenerCallbacks[type][i].el) { + impl.listenerCallbacks[type].splice(i, 1); + return; } } } - - return l; }, + /** + * Determines if the specified object is an `Array` or not + * + * @param {object} ary Object in question + * + * @returns {boolean} True if the object is an `Array` + * + * @memberof BOOMR.utils + */ isArray: function(ary) { return Object.prototype.toString.call(ary) === "[object Array]"; }, + /** + * Determines if the specified value is in the array + * + * @param {object} val Value to check + * @param {object} ary Object in question + * + * @returns {boolean} True if the value is in the Array + * + * @memberof BOOMR.utils + */ inArray: function(val, ary) { var i; @@ -900,9 +1458,12 @@ BOOMR_check_doc_domain(); * Get a query parameter value from a URL's query string * * @param {string} param Query parameter name - * @param {string|Object} [url] URL containing the query string, or a link object. Defaults to BOOMR.window.location + * @param {string|Object} [url] URL containing the query string, or a link object. + * Defaults to `BOOMR.window.location` * * @returns {string|null} URI decoded value or null if param isn't a query parameter + * + * @memberof BOOMR.utils */ getQueryParamValue: function(param, url) { var l, params, i, kv; @@ -940,6 +1501,8 @@ BOOMR_check_doc_domain(); * https://en.wikipedia.org/wiki/Universally_unique_identifier * * @returns {string} UUID + * + * @memberof BOOMR.utils */ generateUUID: function() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { @@ -954,7 +1517,10 @@ BOOMR_check_doc_domain(); * characters a-z0-9. * * @param {number} chars Number of characters (max 40) + * * @returns {string} Random ID + * + * @memberof BOOMR.utils */ generateId: function(chars) { return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".substr(0, chars || 40).replace(/x/g, function(c) { @@ -1004,19 +1570,54 @@ BOOMR_check_doc_domain(); }, // closes `utils` + /** + * Initializes Boomerang by applying the specified configuration. + * + * All plugins' `init()` functions will be called with the same config as well. + * + * @param {object} config Configuration object + * @param {boolean} [config.autorun] By default, boomerang runs automatically + * and attaches its `page_ready` handler to the `window.onload` event. + * If you set `autorun` to `false`, this will not happen and you will + * need to call {@link BOOMR.page_ready} yourself. + * @param {string} config.beacon_auth_key Beacon authorization key value + * @param {string} config.beacon_auth_token Beacon authorization token. + * @param {string} config.beacon_url The URL to beacon results back to. + * If not set, no beacon will be sent. + * @param {string} config.beacon_type `GET`, `POST` or `AUTO` + * @param {string[]} [config.secondary_beacons] Additional beacon URLs to send data to + * @param {string} [config.site_domain] The domain that all cookies should be set on + * Boomerang will try to auto-detect this, but unless your site is of the + * `foo.com` format, it will probably get it wrong. It's a good idea + * to set this to whatever part of your domain you'd like to share + * bandwidth and performance measurements across. + * Set this to a falsy value to disable all cookies. + * @param {boolean} [config.strip_query_string] Whether or not to strip query strings from all URLs (e.g. `u`, `pgu`, etc.) + * @param {string} [config.user_ip] Despite its name, this is really a free-form + * string used to uniquely identify the user's current internet + * connection. It's used primarily by the bandwidth test to determine + * whether it should re-measure the user's bandwidth or just use the + * value stored in the cookie. You may use IPv4, IPv6 or anything else + * that you think can be used to identify the user's network connection. + * @param {function} [config.log] Logger to use. Set to `null` to disable logging. + * @param {function} [] Each plugin has its own section + * + * @returns {BOOMR} Boomerang object + * + * @memberof BOOMR + */ init: function(config) { var i, k, properties = [ - "beacon_url", - "beacon_type", + "autorun", "beacon_auth_key", "beacon_auth_token", + "beacon_url", + "beacon_type", + "secondary_beacons", "site_domain", - "user_ip", "strip_query_string", - "secondary_beacons", - "autorun", - "site_domain" + "user_ip" ]; BOOMR_check_doc_domain(); @@ -1112,8 +1713,8 @@ BOOMR_check_doc_domain(); } BOOMR.utils.addListener(w, "DOMContentLoaded", function() { impl.fireEvent("dom_loaded"); }); - BOOMR.fireEvent("onconfig", config); - BOOMR.subscribe("onconfig", function(beaconConfig) { + BOOMR.fireEvent("config", config); + BOOMR.subscribe("config", function(beaconConfig) { if (beaconConfig.beacon_url) { impl.beacon_url = beaconConfig.beacon_url; } @@ -1174,7 +1775,9 @@ BOOMR_check_doc_domain(); * Attach a callback to the `pageshow` or `onload` event if `onload` has not * been fired otherwise queue it to run immediately * - * @param {function} cb - Callback to run when `onload` fires or page is visible (`pageshow`) + * @param {function} cb Callback to run when `onload` fires or page is visible (`pageshow`) + * + * @memberof BOOMR */ attach_page_ready: function(cb) { if (BOOMR.hasBrowserOnloadFired()) { @@ -1194,8 +1797,12 @@ BOOMR_check_doc_domain(); }, /** - * Sends the page_ready beacon only if 'autorun' is still true after init - * is called. + * Sends the `page_ready` event only if `autorun` is still true after + * {@link BOOMR.init} is called. + * + * @param {Event} ev Event + * + * @memberof BOOMR */ page_ready_autorun: function(ev) { if (impl.autorun) { @@ -1203,8 +1810,24 @@ BOOMR_check_doc_domain(); } }, - // The page dev calls this method when they determine the page is usable. - // Only call this if autorun is explicitly set to false + /** + * Method that fires the {@link BOOMR#event:page_ready} event. Call this + * only if you've set `autorun` to `false` when calling the {@link BOOMR.init} + * method. You should call this method when you determine that your page + * is ready to be used by your user. This will be the end-time used in + * the page load time measurement. + * + * @param {Event} ev Ready event + * + * @returns {BOOMR} Boomerang object + * + * @example + * BOOMR.init({ autorun: false, ... }); + * // wait until the page is ready, i.e. your view has loaded + * BOOMR.page_ready(); + * + * @memberof BOOMR + */ page_ready: function(ev, auto) { if (!ev) { ev = w.event; @@ -1254,24 +1877,44 @@ BOOMR_check_doc_domain(); /** * Determines whether or not the page's `onload` event has fired, or - * if `autorun` is false, whether `BOOMR.page_ready()` was called. + * if `autorun` is false, whether {@link BOOMR.page_ready} was called. * - * @returns {boolean} True if onload or page_ready() were called + * @returns {boolean} True if `onload` or {@link BOOMR.page_ready} were called + * + * @memberof BOOMR */ onloadFired: function() { return impl.onloadfired; }, /** - * Defer the function `fn` until the next instant the browser is free from user tasks - * @param [Function] fn The callback function. This function accepts the following arguments: - * - data: The passed in data object - * - cb_data: The passed in cb_data object - * - call stack: An Error object that holds the callstack for when setImmediate was called, used to determine what called the callback - * @param [object] data Any data to pass to the callback function - * @param [object] cb_data Any passthrough data for the callback function. This differs from `data` when setImmediate is called via an event handler and `data` is the Event object - * @param [object] cb_scope The scope of the callback function if it is a method of an object + * The callback function may return a falsy value to disconnect the observer + * after it returns, or a truthy value to keep watching for mutations. If + * the return value is numeric and greater than 0, then this will be the new timeout. + * If it is boolean instead, then the timeout will not fire any more so + * the caller MUST call disconnect() at some point + * + * @callback BOOMR~setImmediateCallback + * @param {object} data The passed in `data` object + * @param {object} cb_data The passed in `cb_data` object + * @param {Error} callstack An Error object that holds the callstack for + * when `setImmediate` was called, used to determine what called the callback + */ + + /** + * Defer the function `fn` until the next instant the browser is free from + * user tasks. + * + * @param {BOOMR~setImmediateCallback} fn The callback function. + * @param {object} [data] Any data to pass to the callback function + * @param {object} [cb_data] Any passthrough data for the callback function. + * This differs from `data` when `setImmediate` is called via an event + * handler and `data` is the Event object + * @param {object} [cb_scope] The scope of the callback function if it is a method of an object + * * @returns nothing + * + * @memberof BOOMR */ setImmediate: function(fn, data, cb_data, cb_scope) { var cb, cstack; @@ -1302,16 +1945,29 @@ BOOMR_check_doc_domain(); /** * Gets the current time in milliseconds since the Unix Epoch (Jan 1 1970). * - * In browsers that support DOMHighResTimeStamp, this will be replaced - * by a function that adds BOOMR.now() to navigationStart + * In browsers that support `DOMHighResTimeStamp`, this will be replaced + * by a function that adds `performance.now()` to `navigationStart` * (with milliseconds.microseconds resolution). * - * @returns {Number} Milliseconds since Unix Epoch + * @function + * + * @returns {TimeStamp} Milliseconds since Unix Epoch + * + * @memberof BOOMR */ now: (function() { return Date.now || function() { return new Date().getTime(); }; }()), + /** + * Gets the `window.performance` object of the root window. + * + * Checks vendor prefixes for older browsers (e.g. IE9). + * + * @returns {Performance|undefined} `window.performance` if it exists + * + * @memberof BOOMR + */ getPerformance: function() { try { if (BOOMR.window) { @@ -1320,7 +1976,9 @@ BOOMR_check_doc_domain(); } // vendor-prefixed fallbacks - return BOOMR.window.msPerformance || BOOMR.window.webkitPerformance || BOOMR.window.mozPerformance; + return BOOMR.window.msPerformance || + BOOMR.window.webkitPerformance || + BOOMR.window.mozPerformance; } } catch (ignore) { @@ -1328,16 +1986,39 @@ BOOMR_check_doc_domain(); } }, - visibilityState: (visibilityState === undefined ? function() { return "visible"; } : function() { return d[visibilityState]; }), + /** + * Gets the `document.visibilityState`, or `visible` if Page Visibility + * is not supported. + * + * @function + * + * @returns {string} Visibility state + * + * @memberof BOOMR + */ + visibilityState: (visibilityState === undefined ? function() { + return "visible"; + } : function() { + return d[visibilityState]; + }), + /** + * An mapping of visibliity event states to the latest time they happened + * + * @type {object} + * + * @memberof BOOMR + */ lastVisibilityEvent: {}, /** - * Registers an event + * Registers a Boomerang event. * * @param {string} e_name Event name * * @returns {BOOMR} Boomerang object + * + * @memberof BOOMR */ registerEvent: function(e_name) { if (impl.events.hasOwnProperty(e_name)) { @@ -1355,6 +2036,8 @@ BOOMR_check_doc_domain(); * Disables boomerang from doing anything further: * 1. Clears event handlers (such as onload) * 2. Clears all event listeners + * + * @memberof BOOMR */ disable: function() { impl.clearEvents(); @@ -1362,33 +2045,50 @@ BOOMR_check_doc_domain(); }, /** - * Fires an event + * Fires a Boomerang event * * @param {string} e_name Event name * @param {object} data Event payload * * @returns {BOOMR} Boomerang object + * + * @memberof BOOMR */ fireEvent: function(e_name, data) { return impl.fireEvent(e_name, data); }, /** - * Subscribe to an event + * @callback BOOMR~subscribeCallback + * @param {object} eventData Event data + * @param {object} cb_data Callback data + */ + + /** + * Subscribes to a Boomerang event * - * @param {string} e_name Event name - * @param {function} fn callback function - * @param {object} cb_data Any passthrough data for the callback function - * @param {object} cb_scope The scope of the callback function if it is a method of an object - * @param {Boolean} once If true subscribe to only one event call + * @param {string} e_name Event name, i.e. {@link BOOMR#event:page_ready}. + * @param {BOOMR~subscribeCallback} fn Callback function + * @param {object} cb_data Callback data, passed as the second parameter to the callback function + * @param {object} cb_scope Callback scope. If set to an object, then the + * callback function is called as a method of this object, and all + * references to `this` within the callback function will refer to `cb_scope`. + * @param {boolean} once Whether or not this callback should only be run once * * @returns {BOOMR} Boomerang object + * + * @memberof BOOMR */ subscribe: function(e_name, fn, cb_data, cb_scope, once) { var i, handler, ev; e_name = e_name.toLowerCase(); + // translate old names + if (impl.translate_events[e_name]) { + e_name = impl.translate_events[e_name]; + } + if (!impl.events.hasOwnProperty(e_name)) { // allow subscriptions before they're registered impl.events[e_name] = []; @@ -1455,6 +2155,19 @@ BOOMR_check_doc_domain(); return this; }, + /** + * Logs an internal Boomerang error. + * + * If the {@link BOOMR.plugins.Errors} plugin is enabled, this data will + * be compressed on the `err` beacon parameter. If not, it will be included + * in uncompressed form on the `errors` parameter. + * + * @param {string|object} err Error + * @param {string} [src] Source + * @param {object} [extra] Extra data + * + * @memberof BOOMR + */ addError: function BOOMR_addError(err, src, extra) { var str, E = BOOMR.plugins.Errors; @@ -1507,6 +2220,15 @@ BOOMR_check_doc_domain(); } }, + /** + * Determines if the specified Error is a Cross-Origin error. + * + * @param {string|object} err Error + * + * @returns {boolean} True if the Error is a Cross-Origin error. + * + * @memberof BOOMR + */ isCrossOriginError: function(err) { // These are expected for cross-origin iframe access, although the Internet Explorer check will only // work for browsers using English. @@ -1515,7 +2237,36 @@ BOOMR_check_doc_domain(); (err.name === "Error" && err.message && err.message.match(/^(Permission|Access is) denied/)); }, - addVar: function(name, value, singleBeacon) { + /** + * Add one or more parameters to the beacon. + * + * This method may either be called with a single object containing + * key/value pairs, or with two parameters, the first is the variable + * name and the second is its value. + * + * All names should be strings usable in a URL's query string. + * + * We recommend only using alphanumeric characters and underscores, but you + * can use anything you like. + * + * Values should be strings (or numbers), and have the same restrictions + * as names. + * + * Parameters will be on all subsequent beacons unless `singleBeacon` is + * set. + * + * @param {string} name Variable name + * @param {string|object} val Value + * + * @returns {BOOMR} Boomerang object + * + * @example + * BOOMR.addVar("page_id", 123); + * BOOMR.addVar({"page_id": 123, "user_id": "Person1"}); + * + * @memberof BOOMR + */ + addVar: function(name, value, singleBeacon) { if (typeof name === "string") { impl.vars[name] = value; } @@ -1535,6 +2286,22 @@ BOOMR_check_doc_domain(); return this; }, + /** + * Removes one or more variables from the beacon URL. This is useful within + * a plugin to reset the values of parameters that it is about to set. + * + * Plugins can also use this in the {@link BOOMR#event:beacon} event to clear + * any variables that should only live on a single beacon. + * + * This method accepts either a list of variable names, or a single + * array containing a list of variable names. + * + * @param {string[]|string} name Variable name or list + * + * @returns {BOOMR} Boomerang object + * + * @memberof BOOMR + */ removeVar: function(arg0) { var i, params; if (!arguments.length) { @@ -1557,6 +2324,15 @@ BOOMR_check_doc_domain(); return this; }, + /** + * Determines whether or not the beacon has the specified variable. + * + * @param {string} name Variable name + * + * @returns {boolean} True if the variable is set. + * + * @memberof BOOMR + */ hasVar: function(name) { return impl.vars.hasOwnProperty(name); }, @@ -1586,6 +2362,10 @@ BOOMR_check_doc_domain(); * * @param {string} name Variable name * @param {number} pri Priority (-1 or 1) + * + * @returns {BOOMR} Boomerang object + * + * @memberof BOOMR */ setVarPriority: function(name, pri) { if (typeof pri !== "number" || Math.abs(pri) !== 1) { @@ -1598,9 +2378,12 @@ BOOMR_check_doc_domain(); }, /** - * Sets the Referrers + * Sets the Referrers variables. + * * @param {string} r Referrer from the cookie * @param {string} [r2] Referrer from document.referrer, if different + * + * @memberof BOOMR */ setReferrer: function(r, r2) { // cookie referrer @@ -1615,6 +2398,24 @@ BOOMR_check_doc_domain(); } }, + /** + * Starts a timer for a dynamic request. + * + * Once the named request has completed, call `loaded()` to send a beacon + * with the duration. + * + * @example + * var timer = BOOMR.requestStart("my-timer"); + * // do stuff + * timer.loaded(); + * + * @param {string} name Timer name + * + * @returns {object} An object with a `.loaded()` function that you can call + * when the dynamic timer is complete. + * + * @memberof BOOMR + */ requestStart: function(name) { var t_start = BOOMR.now(); BOOMR.plugins.RT.startTimer("xhr_" + name, t_start); @@ -1627,14 +2428,16 @@ BOOMR_check_doc_domain(); }, /** - * Determines is Boomerang can send a beacon. + * Determines if Boomerang can send a beacon. * - * Queryies all plugins to see if they implement readyToSend(), - * and if so, that they return true; + * Queryies all plugins to see if they implement `readyToSend()`, + * and if so, that they return `true`. * * If not, the beacon cannot be sent. * * @returns {boolean} True if Boomerang can send a beacon + * + * @memberof BOOMR */ readyToSend: function() { var plugin; @@ -1656,6 +2459,18 @@ BOOMR_check_doc_domain(); return true; }, + /** + * Sends a beacon for a dynamic request. + * + * @param {string|object} name Timer name or timer object data. + * @param {string} [name.initiator] Initiator, such as `xhr` or `spa` + * @param {string} [name.url] URL of the request + * @param {TimeStamp} t_start Start time + * @param {object} data Request data + * @param {TimeStamp} t_end End time + * + * @memberof BOOMR + */ responseEnd: function(name, t_start, data, t_end) { // take the now timestamp for start and end, if unspecified, in case we delay this beacon t_start = typeof t_start === "number" ? t_start : BOOMR.now(); @@ -1714,16 +2529,59 @@ BOOMR_check_doc_domain(); // by auto-xhr.js if active. // /** - * Undo XMLHttpRequest instrumentation and reset the original + * Undo XMLHttpRequest instrumentation and reset the original `XMLHttpRequest` + * object + * + * This is implemented in `plugins/auto-xhr.js` {@link BOOMR.plugins.AutoXHR}. + * + * @memberof BOOMR */ uninstrumentXHR: function() { }, + /** - * Instrument all requests made via XMLHttpRequest to send beacons - * This is implemented in plugins/auto-xhr.js + * Instrument all requests made via XMLHttpRequest to send beacons. + * + * This is implemented in `plugins/auto-xhr.js` {@link BOOMR.plugins.AutoXHR}. + * + * @memberof BOOMR */ instrumentXHR: function() { }, + /** + * Request boomerang to send its beacon with all queued beacon data + * (via {@link BOOMR.addVar}). + * + * Boomerang may ignore this request. + * + * When this method is called, boomerang checks all plugins. If any + * plugin has not completed its checks (ie, the plugin's `is_complete()` + * method returns `false`, then this method does nothing. + * + * If all plugins have completed, then this method fires the + * {@link BOOMR#event:before_beacon} event with all variables that will be + * sent on the beacon. + * + * After all {@link BOOMR#event:before_beacon} handlers return, this method + * checks if a `beacon_url` has been configured and if there are any + * beacon parameters to be sent. If both are true, it fires the beacon. + * + * The {@link BOOMR#event:beacon} event is then fired. + * + * `sendBeacon()` should be called any time a plugin goes from + * `is_complete() = false` to `is_complete = true` so the beacon is + * sent. + * + * The actual beaconing is handled in {@link BOOMR.real_sendBeacon} after + * a short delay (via {@link BOOMR.setImmediate}). If other calls to + * `sendBeacon` happen before {@link BOOMR.real_sendBeacon} is called, + * those calls will be discarded (so it's OK to call this in quick + * succession). + * + * @param {string} [beacon_url_override] Beacon URL override + * + * @memberof BOOMR + */ sendBeacon: function(beacon_url_override) { // This plugin wants the beacon to go somewhere else, // so update the location @@ -1739,6 +2597,16 @@ BOOMR_check_doc_domain(); return true; }, + /** + * Sends all beacon data. + * + * This function should be called directly any time a "new" beacon is about + * to be constructed. For example, if you're creating a new XHR or other + * custom beacon, you should ensure the existing beacon data is flushed + * by calling `BOOMR.real_sendBeacon();` first. + * + * @memberof BOOMR + */ real_sendBeacon: function() { var k, form, url, errors = [], params = [], paramsJoined, varsSent = {}; @@ -1846,7 +2714,7 @@ BOOMR_check_doc_domain(); impl.fireEvent("before_beacon", impl.vars); // clone the vars object for two reasons: first, so all listeners of - // onbeacon get an exact clone (in case listeners are doing + // 'beacon' get an exact clone (in case listeners are doing // BOOMR.removeVar), and second, to help build our priority list of vars. for (k in impl.vars) { if (impl.vars.hasOwnProperty(k)) { @@ -1917,7 +2785,7 @@ BOOMR_check_doc_domain(); } // If we reach here, we've figured out all of the beacon data we'll send. - impl.fireEvent("onbeacon", data); + impl.fireEvent("beacon", data); // get high- and low-priority variables first, which remove any of // those vars from data @@ -2006,13 +2874,28 @@ BOOMR_check_doc_domain(); this.sendXhrPostBeacon(xhr, paramsJoined); } } + + return true; + }, + + /** + * Determines whether or not a Page Load beacon has been sent. + * + * @returns {boolean} True if a Page Load beacon has been sent. + * + * @memberof BOOMR + */ + hasSentPageLoadBeacon: function() { + return impl.hasSentPageLoadBeacon; }, /** - * Sends an XHR beacon + * Sends a beacon via XMLHttpRequest * * @param {object} xhr XMLHttpRequest object * @param {object} [paramsJoined] XMLHttpRequest.send() argument + * + * @memberof BOOMR */ sendXhrPostBeacon: function(xhr, paramsJoined) { xhr.open("POST", impl.beacon_url); @@ -2037,6 +2920,8 @@ BOOMR_check_doc_domain(); * @param {number} pri Priority (-1, 0, or 1) * * @return {string[]} Array of URI-encoded vars + * + * @memberof BOOMR */ getVarsOfPriority: function(vars, pri) { var name, url = []; @@ -2076,6 +2961,8 @@ BOOMR_check_doc_domain(); * @param {string} value Value * * @returns {string} URI-encoded string + * + * @memberof BOOMR */ getUriEncodedVar: function(name, value) { if (value === undefined || value === null) { @@ -2093,12 +2980,17 @@ BOOMR_check_doc_domain(); }, /** - * Gets the latest ResourceTiming entry for the specified URL - * Default sort order is chronological startTime + * Gets the latest ResourceTiming entry for the specified URL. + * + * Default sort order is chronological startTime. + * * @param {string} url Resource URL * @param {function} [sort] Sort the entries before returning the last one + * * @returns {PerformanceEntry|undefined} Entry, or undefined if ResourceTiming is not - * supported or if the entry doesn't exist + * supported or if the entry doesn't exist + * + * @memberof BOOMR */ getResourceTiming: function(url, sort) { var entries, p = BOOMR.getPerformance(); @@ -2123,7 +3015,22 @@ BOOMR_check_doc_domain(); delete BOOMR_start; + /** + * @global + * @type {TimeStamp} + * @name BOOMR_lstart + * @desc + * Time the loader script started fetching boomerang.js (if the asynchronous + * loader snippet is used). + */ if (typeof BOOMR_lstart === "number") { + /** + * Time the loader script started fetching boomerang.js (if using the + * asynchronous loader snippet) (`BOOMR_lstart`) + * @type {TimeStamp} + * + * @memberof BOOMR + */ boomr.t_lstart = BOOMR_lstart; delete BOOMR_lstart; } @@ -2131,7 +3038,24 @@ BOOMR_check_doc_domain(); boomr.t_lstart = BOOMR.window.BOOMR_lstart; } + /** + * Time the `window.onload` event fired (if using the asynchronous loader snippet). + * + * This timestamp is logged in the case boomerang.js loads after the onload event + * for browsers that don't support NavigationTiming. + * + * @global + * @name BOOMR_onload + * @type {TimeStamp} + */ if (typeof BOOMR.window.BOOMR_onload === "number") { + /** + * Time the loader script started (if using the asynchronous loader snippet) + * (`BOOMR_onload`) + * + * @type {TimeStamp} + * @memberof BOOMR + */ boomr.t_onload = BOOMR.window.BOOMR_onload; } @@ -2139,7 +3063,24 @@ BOOMR_check_doc_domain(); var make_logger; if (typeof console === "object" && console.log !== undefined) { - boomr.log = function(m, l, s) { console.log("(" + BOOMR.now() + ") " + "{" + BOOMR.pageId + "}" + ": " + s + ": [" + l + "] " + m); }; + /** + * Logs the message to the console + * + * @param {string} m Message + * @param {string} l Log level + * @param {string} [s] Source + * + * @function log + * + * @memberof BOOMR + */ + boomr.log = function(m, l, s) { + console.log("(" + BOOMR.now() + ") " + + "{" + BOOMR.pageId + "}" + + ": " + s + + ": [" + l + "] " + + m); + }; } make_logger = function(l) { @@ -2149,9 +3090,54 @@ BOOMR_check_doc_domain(); }; }; + /** + * Logs debug messages to the console + * + * Debug messages are stripped out of production builds. + * + * @param {string} m Message + * @param {string} [s] Source + * + * @function debug + * + * @memberof BOOMR + */ boomr.debug = make_logger("debug"); + + /** + * Logs info messages to the console + * + * @param {string} m Message + * @param {string} [s] Source + * + * @function info + * + * @memberof BOOMR + */ boomr.info = make_logger("info"); + + /** + * Logs warning messages to the console + * + * @param {string} m Message + * @param {string} [s] Source + * + * @function warn + * + * @memberof BOOMR + */ boomr.warn = make_logger("warn"); + + /** + * Logs error messages to the console + * + * @param {string} m Message + * @param {string} [s] Source + * + * @function error + * + * @memberof BOOMR + */ boomr.error = make_logger("error"); }()); @@ -2160,7 +3146,8 @@ BOOMR_check_doc_domain(); var p = boomr.getPerformance(); if (p && typeof p.now === "function" && - /\[native code\]/.test(String(p.now)) && // #545 handle bogus performance.now from broken shims + // #545 handle bogus performance.now from broken shims + /\[native code\]/.test(String(p.now)) && p.timing && p.timing.navigationStart) { boomr.now = function() { @@ -2179,8 +3166,26 @@ BOOMR_check_doc_domain(); BOOMR[ident] = boomr[ident]; } } + if (!BOOMR.xhr_excludes) { - //! URLs to exclude from automatic XHR instrumentation + /** + * URLs to exclude from automatic `XMLHttpRequest` instrumentation. + * + * You can put any of the following in it: + * * A full URL + * * A hostname + * * A path + * + * @example + * BOOMR = window.BOOMR || {}; + * BOOMR.xhr_excludes = { + * "mysite.com": true, + * "/dashboard/": true, + * "https://mysite.com/dashboard/": true + * }; + * + * @memberof BOOMR + */ BOOMR.xhr_excludes = {}; } }()); diff --git a/doc-template/README.txt b/doc-template/README.txt new file mode 100644 index 000000000..990e632c6 --- /dev/null +++ b/doc-template/README.txt @@ -0,0 +1,24 @@ +Modified from docstrap: https://github.com/docstrap/docstrap + +Copyright (c) 2012-15 Terry Weiss & Contributors. All rights reserved. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc-template/publish.js b/doc-template/publish.js new file mode 100644 index 000000000..99718ae54 --- /dev/null +++ b/doc-template/publish.js @@ -0,0 +1,881 @@ +"use strict"; + +/** + * @module template/publish + * @type {*} + */ +/*global env: true */ + +var template = require('jsdoc/template'), + doop = require('jsdoc/util/doop'), + fs = require('jsdoc/fs'), + _ = require('underscore'), + path = require('jsdoc/path'), + + taffy = require('taffydb').taffy, + handle = require('jsdoc/util/error').handle, + helper = require('jsdoc/util/templateHelper'), + moment = require("moment"), + htmlsafe = helper.htmlsafe, + sanitizeHtml = require('sanitize-html'), + linkto = helper.linkto, + resolveAuthorLinks = helper.resolveAuthorLinks, + scopeToPunc = helper.scopeToPunc, + hasOwnProp = Object.prototype.hasOwnProperty, + conf = env.conf.templates || {}, + data, + view, + outdir = env.opts.destination, + searchEnabled = conf.search !== false; + +var globalUrl = helper.getUniqueFilename('global'); +var indexUrl = helper.getUniqueFilename('index'); + +var navOptions = { + includeDate: conf.includeDate !== false, + logoFile: conf.logoFile, + systemName: conf.systemName || "Documentation", + navType: conf.navType || "vertical", + footer: conf.footer || "", + copyright: conf.copyright || "", + theme: conf.theme || "simplex", + syntaxTheme: conf.syntaxTheme || "default", + linenums: conf.linenums, + collapseSymbols: conf.collapseSymbols || false, + inverseNav: conf.inverseNav, + outputSourceFiles: conf.outputSourceFiles === true, + sourceRootPath: conf.sourceRootPath, + disablePackagePath: conf.disablePackagePath, + outputSourcePath: conf.outputSourcePath, + dateFormat: conf.dateFormat, + analytics: conf.analytics || null, + methodHeadingReturns: conf.methodHeadingReturns === true, + sort: conf.sort, + search: searchEnabled +}; +var searchableDocuments = {}; + +var navigationMaster = { + index: { + title: navOptions.systemName, + link: indexUrl, + members: [] + }, + namespace: { + title: "Namespaces", + link: helper.getUniqueFilename("namespaces.list"), + members: [] + }, + module: { + title: "Modules", + link: helper.getUniqueFilename("modules.list"), + members: [] + }, + class: { + title: "Classes", + link: helper.getUniqueFilename('classes.list'), + members: [] + }, + + mixin: { + title: "Mixins", + link: helper.getUniqueFilename("mixins.list"), + members: [] + }, + event: { + title: "Events", + link: helper.getUniqueFilename("events.list"), + members: [] + }, + interface: { + title: "Interfaces", + link: helper.getUniqueFilename("interfaces.list"), + members: [] + }, + tutorial: { + title: "Tutorials", + link: helper.getUniqueFilename("tutorials.list"), + members: [] + }, + global: { + title: "Global", + link: globalUrl, + members: [] + + }, + external: { + title: "Externals", + link: helper.getUniqueFilename("externals.list"), + members: [] + } +}; + +function find(spec) { + return helper.find(data, spec); +} + +function tutoriallink(tutorial) { + return helper.toTutorial(tutorial, null, { + tag: 'em', + classname: 'disabled', + prefix: 'Tutorial: ' + }); +} + +function getAncestorLinks(doclet) { + return helper.getAncestorLinks(data, doclet); +} + +function hashToLink(doclet, hash) { + if (!/^(#.+)/.test(hash)) { + return hash; + } + + var url = helper.createLink(doclet); + + url = url.replace(/(#.+|$)/, hash); + return '' + hash + ''; +} + +function needsSignature(doclet) { + var needsSig = false; + + // function and class definitions always get a signature + if (doclet.kind === 'function' || doclet.kind === 'class') { + needsSig = true; + } + // typedefs that contain functions get a signature, too + else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names && + doclet.type.names.length) { + for (var i = 0, l = doclet.type.names.length; i < l; i++) { + if (doclet.type.names[i].toLowerCase() === 'function') { + needsSig = true; + break; + } + } + } + + return needsSig; +} + +function addSignatureParams(f) { + var optionalClass = 'optional'; + var params = helper.getSignatureParams(f, optionalClass); + + f.signature = (f.signature || '') + '('; + + for (var i = 0, l = params.length; i < l; i++) { + var element = params[i]; + var seperator = (i > 0) ? ', ' : ''; + + if (!new RegExp("class=[\"|']"+optionalClass+"[\"|']").test(element)) { + f.signature += seperator + element; + } else { + var regExp = new RegExp("(.*?)<\\/span>", "i"); + f.signature += element.replace(regExp, " $`["+seperator+"$1$']"); + } + + } + + f.signature += ')'; +} + +function addSignatureReturns(f) { + if (navOptions.methodHeadingReturns) { + var returnTypes = helper.getSignatureReturns(f); + + f.signature = '' + (f.signature || '') + '' + '' + (returnTypes.length ? ' → {' + returnTypes.join('|') + '}' : '') + ''; + } + else { + f.signature = f.signature || ''; + } +} + +function addSignatureTypes(f) { + var types = helper.getSignatureTypes(f); + + f.signature = (f.signature || '') + '' + (types.length ? ' :' + types.join('|') : '') + ''; +} + +function addAttribs(f) { + var attribs = helper.getAttribs(f); + + f.attribs = '' + htmlsafe(attribs.length ? '<' + attribs.join(', ') + '> ' : '') + ''; +} + +function shortenPaths(files, commonPrefix) { + Object.keys(files).forEach(function(file) { + files[file].shortened = files[file].resolved.replace(commonPrefix, '') + // always use forward slashes + .replace(/\\/g, '/'); + }); + + + return files; +} + +function getPathFromDoclet(doclet) { + if (!doclet.meta) { + return; + } + + return path.normalize(doclet.meta.path && doclet.meta.path !== 'null' ? + doclet.meta.path + '/' + doclet.meta.filename : + doclet.meta.filename); +} + +function searchData(html) { + var startOfContent = html.indexOf("
"); + if (startOfContent > 0) { + var startOfSecondContent = html.indexOf("
", startOfContent + 2); + if (startOfSecondContent > 0) { + startOfContent = startOfSecondContent; + } + html = html.slice(startOfContent); + } + var endOfContent = html.indexOf(""); + if (endOfContent > 0) { + html = html.substring(0, endOfContent); + } + var stripped = sanitizeHtml(html, {allowedTags: [], allowedAttributes: []}); + stripped = stripped.replace(/\s+/g, ' '); + return stripped; +} + +function generate(docType, title, docs, filename, resolveLinks) { + resolveLinks = resolveLinks === false ? false : true; + + var docData = { + title: title, + docs: docs, + docType: docType + }; + + var outpath = path.join(outdir, filename), + html = view.render('container.tmpl', docData); + + if (resolveLinks) { + html = helper.resolveLinks(html); // turn {@link foo} into foo + } + + if (searchEnabled) { + searchableDocuments[filename] = { + "id": filename, + "title": title, + "body": searchData(html) + }; + } + + fs.writeFileSync(outpath, html, 'utf8'); +} + +function generateSourceFiles(sourceFiles) { + Object.keys(sourceFiles).forEach(function(file) { + var source; + // links are keyed to the shortened path in each doclet's `meta.shortpath` property + var sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened); + helper.registerLink(sourceFiles[file].shortened, sourceOutfile); + + try { + source = { + kind: 'source', + code: helper.htmlsafe(fs.readFileSync(sourceFiles[file].resolved, 'utf8')) + }; + } catch (e) { + handle(e); + } + + generate('source', 'Source: ' + sourceFiles[file].shortened, [source], sourceOutfile, + false); + }); +} + +/** + * Look for classes or functions with the same name as modules (which indicates that the module + * exports only that class or function), then attach the classes or functions to the `module` + * property of the appropriate module doclets. The name of each class or function is also updated + * for display purposes. This function mutates the original arrays. + * + * @private + * @param {Array.} doclets - The array of classes and functions to + * check. + * @param {Array.} modules - The array of module doclets to search. + */ +function attachModuleSymbols(doclets, modules) { + var symbols = {}; + + // build a lookup table + doclets.forEach(function(symbol) { + symbols[symbol.longname] = symbols[symbol.longname] || []; + symbols[symbol.longname].push(symbol); + }); + + return modules.map(function(module) { + if (symbols[module.longname]) { + module.modules = symbols[module.longname] + // Only show symbols that have a description. Make an exception for classes, because + // we want to show the constructor-signature heading no matter what. + .filter(function(symbol) { + return symbol.description || symbol.kind === 'class'; + }) + .map(function(symbol) { + symbol = doop(symbol); + + if (symbol.kind === 'class' || symbol.kind === 'function') { + symbol.name = symbol.name.replace('module:', '(require("') + '"))'; + } + + return symbol; + }); + } + }); +} + +/** + * Create the navigation sidebar. + * @param {object} members The members that will be used to create the sidebar. + * @param {array} members.classes + * @param {array} members.externals + * @param {array} members.globals + * @param {array} members.mixins + * @param {array} members.interfaces + * @param {array} members.modules + * @param {array} members.namespaces + * @param {array} members.tutorials + * @param {array} members.events + * @return {string} The HTML for the navigation sidebar. + */ +function buildNav(members) { + + var seen = {}; + var nav = navigationMaster; + if (members.modules.length) { + + members.modules.forEach(function(m) { + if (!hasOwnProp.call(seen, m.longname)) { + + nav.module.members.push(linkto(m.longname, m.longname.replace("module:", ""))); + } + seen[m.longname] = true; + }); + } + + if (members.externals.length) { + + members.externals.forEach(function(e) { + if (!hasOwnProp.call(seen, e.longname)) { + + nav.external.members.push(linkto(e.longname, e.name.replace(/(^"|"$)/g, ''))); + } + seen[e.longname] = true; + }); + } + + if (members.classes.length) { + + members.classes.forEach(function(c) { + if (!hasOwnProp.call(seen, c.longname)) { + + nav.class.members.push(linkto(c.longname, c.longname.replace("module:", ""))); + } + seen[c.longname] = true; + }); + + } + + if (members.events.length) { + + members.events.forEach(function(e) { + if (!hasOwnProp.call(seen, e.longname)) { + + nav.event.members.push(linkto(e.longname, e.longname.replace("module:", ""))); + } + seen[e.longname] = true; + }); + + } + + if (members.namespaces.length) { + + members.namespaces.forEach(function(n) { + if (!hasOwnProp.call(seen, n.longname)) { + + nav.namespace.members.push(linkto(n.longname, n.longname.replace("module:", ""))); + } + seen[n.longname] = true; + }); + + } + + if (members.mixins.length) { + + members.mixins.forEach(function(m) { + if (!hasOwnProp.call(seen, m.longname)) { + + nav.mixin.members.push(linkto(m.longname, m.longname.replace("module:", ""))); + } + seen[m.longname] = true; + }); + + } + + if (members.interfaces && members.interfaces.length) { + + members.interfaces.forEach(function(m) { + if (!hasOwnProp.call(seen, m.longname)) { + + nav.interface.members.push(linkto(m.longname, m.longname.replace("module:", ""))); + } + seen[m.longname] = true; + }); + + } + + if (members.tutorials.length) { + + members.tutorials.forEach(function(t) { + + nav.tutorial.members.push(tutoriallink(t.name)); + }); + + } + + if (members.globals.length) { + members.globals.forEach(function(g) { + if (g.kind !== 'typedef' && !hasOwnProp.call(seen, g.longname)) { + + nav.global.members.push(linkto(g.longname, g.longname.replace("module:", ""))); + } + seen[g.longname] = true; + }); + + // even if there are no links, provide a link to the global page. + if (nav.global.members.length === 0) { + nav.global.members.push(linkto("global", "Global")); + } + } + + var topLevelNav = []; + _.each(nav, function(entry, name) { + if (entry.members.length > 0 && name !== "index") { + topLevelNav.push({ + title: entry.title, + link: entry.link, + members: entry.members + }); + } + }); + nav.topLevelNav = topLevelNav; +} + +/** + @param {TAFFY} taffyData See . + @param {object} opts + @param {Tutorial} tutorials + */ +exports.publish = function(taffyData, opts, tutorials) { + data = taffyData; + + conf['default'] = conf['default'] || {}; + + var templatePath = opts.template; + view = new template.Template(templatePath + '/tmpl'); + + // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness + // doesn't try to hand them out later + // var indexUrl = helper.getUniqueFilename( 'index' ); + // don't call registerLink() on this one! 'index' is also a valid longname + + // var globalUrl = helper.getUniqueFilename( 'global' ); + helper.registerLink('global', globalUrl); + + // set up templating + // set up templating + view.layout = conf['default'].layoutFile ? + path.getResourcePath(path.dirname(conf['default'].layoutFile), + path.basename(conf['default'].layoutFile) ) : 'layout.tmpl'; + + // set up tutorials for helper + helper.setTutorials(tutorials); + + data = helper.prune(data); + + var sortOption = navOptions.sort === undefined ? opts.sort : navOptions.sort; + sortOption = sortOption === undefined ? true : sortOption; + sortOption = sortOption === true ? 'longname, version, since' : sortOption; + if (sortOption) { + data.sort(sortOption); + } + helper.addEventListeners(data); + + var sourceFiles = {}; + var sourceFilePaths = []; + data().each(function(doclet) { + doclet.attribs = ''; + + if (doclet.examples) { + doclet.examples = doclet.examples.map(function(example) { + var caption, lang; + + // allow using a markdown parser on the examples captions (surrounded by useless HTML p tags) + if (example.match(/^\s*(

)?([\s\S]+?)<\/caption>(\s*)([\s\S]+?)(<\/p>)?$/i)) { + caption = RegExp.$2; + example = RegExp.$4 + (RegExp.$1 ? '' : RegExp.$5); + } + + var lang = /{@lang (.*?)}/.exec(example); + + if (lang && lang[1]) { + example = example.replace(lang[0], ""); + lang = lang[1]; + + } else { + lang = null; + } + + return { + caption: caption || '', + code: example, + lang: lang || "javascript" + }; + }); + } + if (doclet.see) { + doclet.see.forEach(function(seeItem, i) { + doclet.see[i] = hashToLink(doclet, seeItem); + }); + } + + // build a list of source files + var sourcePath; + if (doclet.meta) { + sourcePath = getPathFromDoclet(doclet); + sourceFiles[sourcePath] = { + resolved: sourcePath, + shortened: null + }; + + //Check to see if the array of source file paths already contains + // the source path, if not then add it + if (sourceFilePaths.indexOf(sourcePath) === -1) { + sourceFilePaths.push(sourcePath) + } + } + }); + + // update outdir if necessary, then create outdir + var packageInfo = (find({ + kind: 'package' + }) || [])[0]; + if (navOptions.disablePackagePath !== true && packageInfo && packageInfo.name) { + if (packageInfo.version) { + outdir = path.join(outdir, packageInfo.name, packageInfo.version); + } else { + outdir = path.join(outdir, packageInfo.name); + } + } + fs.mkPath(outdir); + + // copy the template's static files to outdir + var fromDir = path.join( templatePath, 'static' ); + var staticFiles = fs.ls( fromDir, 3 ); + staticFiles.forEach( function ( fileName ) { + var toFile = fileName.replace( fromDir, outdir ); + var toDir = fs.toDir( toFile ); + fs.mkPath( toDir ); + fs.copyFileSync( fileName, '', toFile ); + } ); + + // copy user-specified static files to outdir + var staticFilePaths; + var staticFileFilter; + var staticFileScanner; + if (conf.default.staticFiles) { + // The canonical property name is `include`. We accept `paths` for backwards compatibility + // with a bug in JSDoc 3.2.x. + staticFilePaths = conf.default.staticFiles.include || + conf.default.staticFiles.paths || + []; + staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf.default.staticFiles); + staticFileScanner = new (require('jsdoc/src/scanner')).Scanner(); + + staticFilePaths.forEach(function(filePath) { + var extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); + + extraStaticFiles.forEach(function(fileName) { + var sourcePath = fs.toDir(filePath); + var toDir = fs.toDir( fileName.replace(sourcePath, outdir) ); + fs.mkPath(toDir); + fs.copyFileSync(fileName, toDir, fileName.replace(sourcePath, '')); + }); + }); + } + + if (sourceFilePaths.length) { + var payload = navOptions.sourceRootPath; + if (!payload) { + payload = path.commonPrefix(sourceFilePaths); + } + sourceFiles = shortenPaths(sourceFiles, payload); + } + data().each(function(doclet) { + var url = helper.createLink(doclet); + helper.registerLink(doclet.longname, url); + + // add a shortened version of the full path + var docletPath; + if (doclet.meta) { + docletPath = getPathFromDoclet(doclet); + if (!_.isEmpty(sourceFiles[docletPath])) { + docletPath = sourceFiles[docletPath].shortened; + if (docletPath) { + doclet.meta.shortpath = docletPath; + } + } + } + }); + + data().each(function(doclet) { + var url = helper.longnameToUrl[doclet.longname]; + + if (url.indexOf('#') > -1) { + doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); + } else { + doclet.id = doclet.name; + } + + if (needsSignature(doclet)) { + addSignatureParams(doclet); + addSignatureReturns(doclet); + addAttribs(doclet); + } + }); + + // do this after the urls have all been generated + data().each(function(doclet) { + doclet.ancestors = getAncestorLinks(doclet); + + if (doclet.kind === 'member') { + addSignatureTypes(doclet); + addAttribs(doclet); + } + + if (doclet.kind === 'constant') { + addSignatureTypes(doclet); + addAttribs(doclet); + doclet.kind = 'member'; + } + }); + + var members = helper.getMembers(data); + members.tutorials = tutorials.children; + + // add template helpers + view.find = find; + view.linkto = linkto; + view.resolveAuthorLinks = resolveAuthorLinks; + view.tutoriallink = tutoriallink; + view.htmlsafe = htmlsafe; + view.moment = moment; + + // once for all + buildNav(members); + view.nav = navigationMaster; + view.navOptions = navOptions; + attachModuleSymbols(find({ + kind: ['class', 'function'], + longname: { + left: 'module:' + } + }), + members.modules); + + // only output pretty-printed source files if requested; do this before generating any other + // pages, so the other pages can link to the source files + if (navOptions.outputSourceFiles) { + generateSourceFiles(sourceFiles); + } + + if (members.globals.length) { + generate('global', 'Global', [{ + kind: 'globalobj' + }], globalUrl); + } + + // some browsers can't make the dropdown work + if (view.nav.module && view.nav.module.members.length) { + generate('module', view.nav.module.title, [{ + kind: 'sectionIndex', + contents: view.nav.module + }], navigationMaster.module.link); + } + + if (view.nav.class && view.nav.class.members.length) { + generate('class', view.nav.class.title, [{ + kind: 'sectionIndex', + contents: view.nav.class + }], navigationMaster.class.link); + } + + if (view.nav.namespace && view.nav.namespace.members.length) { + generate('namespace', view.nav.namespace.title, [{ + kind: 'sectionIndex', + contents: view.nav.namespace + }], navigationMaster.namespace.link); + } + + if (view.nav.mixin && view.nav.mixin.members.length) { + generate('mixin', view.nav.mixin.title, [{ + kind: 'sectionIndex', + contents: view.nav.mixin + }], navigationMaster.mixin.link); + } + + if (view.nav.interface && view.nav.interface.members.length) { + generate('interface', view.nav.interface.title, [{ + kind: 'sectionIndex', + contents: view.nav.interface + }], navigationMaster.interface.link); + } + + if (view.nav.external && view.nav.external.members.length) { + generate('external', view.nav.external.title, [{ + kind: 'sectionIndex', + contents: view.nav.external + }], navigationMaster.external.link); + } + + if (view.nav.tutorial && view.nav.tutorial.members.length) { + generate('tutorial', view.nav.tutorial.title, [{ + kind: 'sectionIndex', + contents: view.nav.tutorial + }], navigationMaster.tutorial.link); + } + + // index page displays information from package.json and lists files + var files = find({ + kind: 'file' + }), + packages = find({ + kind: 'package' + }); + + generate('index', 'Index', + packages.concat( + [{ + kind: 'mainpage', + readme: opts.readme, + longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page' + }] + ).concat(files), + indexUrl); + + // set up the lists that we'll use to generate pages + var classes = taffy(members.classes); + var modules = taffy(members.modules); + var namespaces = taffy(members.namespaces); + var mixins = taffy(members.mixins); + var interfaces = taffy(members.interfaces); + var externals = taffy(members.externals); + + for (var longname in helper.longnameToUrl) { + if (hasOwnProp.call(helper.longnameToUrl, longname)) { + var myClasses = helper.find(classes, { + longname: longname + }); + if (myClasses.length) { + generate('class', 'Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname]); + } + + var myModules = helper.find(modules, { + longname: longname + }); + if (myModules.length) { + generate('module', 'Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname]); + } + + var myNamespaces = helper.find(namespaces, { + longname: longname + }); + if (myNamespaces.length) { + generate('namespace', 'Namespace: ' + myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]); + } + + var myMixins = helper.find(mixins, { + longname: longname + }); + if (myMixins.length) { + generate('mixin', 'Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname]); + } + + var myInterfaces = helper.find(interfaces, { + longname: longname + }); + if (myInterfaces.length) { + generate('interface', 'Interface: ' + myInterfaces[0].name, myInterfaces, helper.longnameToUrl[longname]); + } + + var myExternals = helper.find(externals, { + longname: longname + }); + if (myExternals.length) { + generate('external', 'External: ' + myExternals[0].name, myExternals, helper.longnameToUrl[longname]); + } + } + } + + function generateTutorial(title, tutorial, filename) { + var tutorialData = { + title: title, + header: tutorial.title, + content: tutorial.parse(), + children: tutorial.children, + docs: null + }; + + var tutorialPath = path.join(outdir, filename), + html = view.render('tutorial.tmpl', tutorialData); + + // yes, you can use {@link} in tutorials too! + html = helper.resolveLinks(html); // turn {@link foo} into foo + + if (searchEnabled) { + searchableDocuments[filename] = { + "id": filename, + "title": title, + "body": searchData(html) + }; + } + + fs.writeFileSync(tutorialPath, html, 'utf8'); + } + + // tutorials can have only one parent so there is no risk for loops + function saveChildren(node) { + node.children.forEach(function(child) { + generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name)); + saveChildren(child); + }); + } + + function generateQuickTextSearch(templatePath, searchableDocuments, navOptions) { + var data = { + searchableDocuments: JSON.stringify(searchableDocuments), + navOptions: navOptions + }; + + var tmplString = fs.readFileSync(templatePath + "/quicksearch.tmpl").toString(), + tmpl = _.template(tmplString); + + var html = tmpl(data), + outpath = path.join(outdir, "quicksearch.html"); + + fs.writeFileSync(outpath, html, "utf8"); + } + + saveChildren(tutorials); + + if (searchEnabled) { + generateQuickTextSearch(templatePath + '/tmpl', searchableDocuments, navOptions); + } +}; diff --git a/doc-template/static/fonts/glyphicons-halflings-regular.eot b/doc-template/static/fonts/glyphicons-halflings-regular.eot new file mode 100644 index 000000000..b93a4953f Binary files /dev/null and b/doc-template/static/fonts/glyphicons-halflings-regular.eot differ diff --git a/doc-template/static/fonts/glyphicons-halflings-regular.svg b/doc-template/static/fonts/glyphicons-halflings-regular.svg new file mode 100644 index 000000000..94fb5490a --- /dev/null +++ b/doc-template/static/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc-template/static/fonts/glyphicons-halflings-regular.ttf b/doc-template/static/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..1413fc609 Binary files /dev/null and b/doc-template/static/fonts/glyphicons-halflings-regular.ttf differ diff --git a/doc-template/static/fonts/glyphicons-halflings-regular.woff b/doc-template/static/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..9e612858f Binary files /dev/null and b/doc-template/static/fonts/glyphicons-halflings-regular.woff differ diff --git a/doc-template/static/fonts/glyphicons-halflings-regular.woff2 b/doc-template/static/fonts/glyphicons-halflings-regular.woff2 new file mode 100644 index 000000000..64539b54c Binary files /dev/null and b/doc-template/static/fonts/glyphicons-halflings-regular.woff2 differ diff --git a/doc-template/static/img/glyphicons-halflings-white.png b/doc-template/static/img/glyphicons-halflings-white.png new file mode 100644 index 000000000..3bf6484a2 Binary files /dev/null and b/doc-template/static/img/glyphicons-halflings-white.png differ diff --git a/doc-template/static/img/glyphicons-halflings.png b/doc-template/static/img/glyphicons-halflings.png new file mode 100644 index 000000000..a99699932 Binary files /dev/null and b/doc-template/static/img/glyphicons-halflings.png differ diff --git a/doc-template/static/scripts/docstrap.lib.js b/doc-template/static/scripts/docstrap.lib.js new file mode 100644 index 000000000..09d9272a0 --- /dev/null +++ b/doc-template/static/scripts/docstrap.lib.js @@ -0,0 +1,11 @@ +if(!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){function c(a){var b="length"in a&&a.length,c=_.type(a);return"function"!==c&&!_.isWindow(a)&&(!(1!==a.nodeType||!b)||("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a))}function d(a,b,c){if(_.isFunction(b))return _.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return _.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(ha.test(b))return _.filter(b,a,c);b=_.filter(b,a)}return _.grep(a,function(a){return U.call(b,a)>=0!==c})}function e(a,b){for(;(a=a[b])&&1!==a.nodeType;);return a}function f(a){var b=oa[a]={};return _.each(a.match(na)||[],function(a,c){b[c]=!0}),b}function g(){Z.removeEventListener("DOMContentLoaded",g,!1),a.removeEventListener("load",g,!1),_.ready()}function h(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=_.expando+h.uid++}function i(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(ua,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c||"false"!==c&&("null"===c?null:+c+""===c?+c:ta.test(c)?_.parseJSON(c):c)}catch(a){}sa.set(a,b,c)}else c=void 0;return c}function j(){return!0}function k(){return!1}function l(){try{return Z.activeElement}catch(a){}}function m(a,b){return _.nodeName(a,"table")&&_.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function n(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function o(a){var b=Ka.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function p(a,b){for(var c=0,d=a.length;d>c;c++)ra.set(a[c],"globalEval",!b||ra.get(b[c],"globalEval"))}function q(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(ra.hasData(a)&&(f=ra.access(a),g=ra.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)_.event.add(b,e,j[e][c])}sa.hasData(a)&&(h=sa.access(a),i=_.extend({},h),sa.set(b,i))}}function r(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&_.nodeName(a,b)?_.merge([a],c):c}function s(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ya.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}function t(b,c){var d,e=_(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:_.css(e[0],"display");return e.detach(),f}function u(a){var b=Z,c=Oa[a];return c||(c=t(a,b),"none"!==c&&c||(Na=(Na||_("