ember-introjs
Version:
An Ember Component for intro.js
208 lines (172 loc) • 5.88 kB
JavaScript
'use strict';
const SimpleDOM = require('simple-dom');
const HTMLSerializer = new SimpleDOM.HTMLSerializer(SimpleDOM.voidMap);
const SHOEBOX_TAG_PATTERN = '<script type="fastboot/shoebox"';
const HTML_HEAD_REGEX = /^([\s\S]*<\/head>)([\s\S]*)/;
/**
* Represents the rendered result of visiting an Ember app at a particular URL.
* A `Result` object is returned from calling {@link FastBoot}'s `visit()`
* method.
*/
class Result {
constructor(options) {
this._instanceDestroyed = false;
this._doc = options.doc;
this._html = options.html;
this._fastbootInfo = options.fastbootInfo;
}
/**
* Returns the HTML representation of the rendered route, inserted
* into the application's `index.html`.
*
* @returns {Promise<String>} the application's DOM serialized to HTML
*/
html() {
let response = this._fastbootInfo.response;
let statusCode = response && this._fastbootInfo.response.statusCode;
if (statusCode === 204) {
this._html = '';
this._head = '';
this._body = '';
} else if (statusCode >= 300 && statusCode <= 399) {
let location = response.headers.get('location');
this._html = '<body><!-- EMBER_CLI_FASTBOOT_BODY --></body>';
this._head = '';
this._body = '';
if (location) {
this._body = `<h1>Redirecting to <a href="${location}">${location}</a></h1>`;
}
}
return insertIntoIndexHTML(this._html, this._head, this._body, this._bodyAttributes);
}
/**
* Returns the HTML representation of the rendered route, inserted
* into the application's `index.html`, split into chunks.
* The first chunk contains the document's head, the second contains the body
* until just before the shoebox tags (if there are any) and the last chunk
* contains the shoebox tags and the closing `body` tag. If there are no
* shoebox tags, there are only 2 chunks and the second one contains the
* complete document body, including the closing `body` tag.
*
* @returns {Promise<Array<String>>} the application's DOM serialized to HTML, split into chunks
*/
chunks() {
return insertIntoIndexHTML(this._html, this._head, this._body, this._bodyAttributes).then((html) => {
let docParts = html.match(HTML_HEAD_REGEX);
if (!docParts || docParts.length === 1) {
return [html];
}
let head = docParts[1];
let body = docParts[2];
if (!head || !body) {
throw new Error('Could not idenfity head and body of the document! Make sure the document is well formed.');
}
let chunks = [head];
let bodyParts = body.split(SHOEBOX_TAG_PATTERN);
let plainBody = bodyParts[0];
chunks.push(plainBody);
let shoeboxes = bodyParts.splice(1);
shoeboxes.forEach((shoebox) => {
chunks.push(`${SHOEBOX_TAG_PATTERN}${shoebox}`);
});
return chunks;
});
}
/**
* Returns the serialized representation of DOM HEAD and DOM BODY
*
* @returns {Object} serialized version of DOM
*/
domContents() {
return {
head: this._head,
body: this._body
};
}
/**
* @private
*
* Called once the Result has finished being constructed and the application
* instance has finished rendering. Once `finalize()` is called, state is
* gathered from the completed application instance and statically copied
* to this Result instance.
*/
_finalize() {
if (this.finalized) {
throw new Error("Results cannot be finalized more than once");
}
// Grab some metadata from the sandboxed application instance
// and copy it to this Result object.
let instance = this.instance;
if (instance) {
this._finalizeMetadata(instance);
}
this._finalizeHTML();
this.finalized = true;
return this;
}
_finalizeMetadata(instance) {
if (instance._booted) {
this.url = instance.getURL();
}
let response = this._fastbootInfo.response;
if (response) {
this.headers = response.headers;
this.statusCode = response.statusCode;
}
}
_destroyAppInstance() {
if (this.instance && !this._instanceDestroyed) {
this._instanceDestroyed = true;
this.instance.destroy();
return true;
}
return false;
}
_finalizeHTML() {
let head = this._doc.head;
let body = this._doc.body;
if (body.attributes.length > 0) {
this._bodyAttributes = HTMLSerializer.attributes(body.attributes);
} else {
this._bodyAttributes = null;
}
if (head) {
head = HTMLSerializer.serializeChildren(head);
}
body = HTMLSerializer.serializeChildren(body);
this._head = head;
this._body = body;
}
}
function missingTag(tag) {
return Promise.reject(new Error(`Fastboot was not able to find ${tag} in base HTML. It could not replace the contents.`));
}
function insertIntoIndexHTML(html, head, body, bodyAttributes) {
if (!html) { return Promise.resolve(html); }
let isBodyReplaced = false;
let isHeadReplaced = false;
html = html.replace(/<\!-- EMBER_CLI_FASTBOOT_(HEAD|BODY) -->/g, function(match, tag) {
if (tag === 'HEAD' && head && !isHeadReplaced) {
isHeadReplaced = true;
return head;
} else if (tag === 'BODY' && body && !isBodyReplaced) {
isBodyReplaced = true;
return '<script type="x/boundary" id="fastboot-body-start"></script>' + body + '<script type="x/boundary" id="fastboot-body-end"></script>';
}
return '';
});
if (bodyAttributes) {
html = html.replace(/<body[^>]*/i, function(match) {
return match + ' ' + bodyAttributes;
});
}
if (head && !isHeadReplaced) {
return missingTag('<!--EMBER_CLI_FASTBOOT_HEAD-->');
}
if (body && !isBodyReplaced) {
return missingTag('<!--EMBER_CLI_FASTBOOT_BODY-->');
}
return Promise.resolve(html);
}
module.exports = Result;