automizy-email-editor
Version:
Design mobile friendly HTML emails in your application.
252 lines (227 loc) • 8.38 kB
JavaScript
// get accurate timing.
// This file must be loaded after prettify.js for this to work.
PR_SHOULD_USE_CONTINUATION = false;
var attribToHtml, textToHtml;
var getInnerHtml;
(function () {
/** is the given node's innerHTML normally unescaped? */
function isRawContent(node) {
return 'XMP' === node.tagName;
}
var newlineRe = /[\r\n]/g;
/**
* Are newlines and adjacent spaces significant in the given node's innerHTML?
*/
function isPreformatted(node, content) {
// PRE means preformatted, and is a very common case, so don't create
// unnecessary computed style objects.
if ('PRE' === node.tagName) { return true; }
if (!newlineRe.test(content)) { return true; } // Don't care
var whitespace = '';
// For disconnected nodes, IE has no currentStyle.
if (node.currentStyle) {
whitespace = node.currentStyle.whiteSpace;
} else if (window.getComputedStyle) {
// Firefox makes a best guess if node is disconnected whereas Safari
// returns the empty string.
whitespace = window.getComputedStyle(node, null).whiteSpace;
}
return !whitespace || whitespace === 'pre';
}
// Define regexps here so that the interpreter doesn't have to create an
// object each time the function containing them is called.
// The language spec requires a new object created even if you don't access
// the $1 members.
var pr_amp = /&/g;
var pr_lt = /</g;
var pr_gt = />/g;
var pr_quot = /\"/g;
/** escapest html special characters to html. */
textToHtml = function (str) {
return str.replace(pr_amp, '&')
.replace(pr_lt, '<')
.replace(pr_gt, '>');
};
/** like textToHtml but escapes double quotes to be attribute safe. */
attribToHtml = function (str) {
return str.replace(pr_amp, '&')
.replace(pr_lt, '<')
.replace(pr_gt, '>')
.replace(pr_quot, '"');
};
var PR_innerHtmlWorks = null;
getInnerHtml = function (node) {
// inner html is hopelessly broken in Safari 2.0.4 when the content is
// an html description of well formed XML and the containing tag is a PRE
// tag, so we detect that case and emulate innerHTML.
if (null === PR_innerHtmlWorks) {
var testNode = document.createElement('PRE');
testNode.appendChild(
document.createTextNode('<!DOCTYPE foo PUBLIC "foo bar">\n<foo />'));
PR_innerHtmlWorks = !/</.test(testNode.innerHTML);
}
if (PR_innerHtmlWorks) {
var content = node.innerHTML;
// XMP tags contain unescaped entities so require special handling.
if (isRawContent(node)) {
content = textToHtml(content);
} else if (!isPreformatted(node, content)) {
content = content.replace(/(<br\s*\/?>)[\r\n]+/g, '$1')
.replace(/(?:[\r\n]+[ \t]*)+/g, ' ');
}
return content;
}
var out = [];
for (var child = node.firstChild; child; child = child.nextSibling) {
normalizedHtml(child, out);
}
return out.join('');
};
})();
function normalizedHtml(node, out, opt_sortAttrs) {
switch (node.nodeType) {
case 1: // an element
var name = node.tagName.toLowerCase();
out.push('<', name);
var attrs = node.attributes;
var n = attrs.length;
if (n) {
if (opt_sortAttrs) {
var sortedAttrs = [];
for (var i = n; --i >= 0;) { sortedAttrs[i] = attrs[i]; }
sortedAttrs.sort(function (a, b) {
return (a.name < b.name) ? -1 : a.name === b.name ? 0 : 1;
});
attrs = sortedAttrs;
}
for (var i = 0; i < n; ++i) {
var attr = attrs[i];
if (!attr.specified) { continue; }
out.push(' ', attr.name.toLowerCase(),
'="', attribToHtml(attr.value), '"');
}
}
out.push('>');
for (var child = node.firstChild; child; child = child.nextSibling) {
normalizedHtml(child, out, opt_sortAttrs);
}
if (node.firstChild || !/^(?:br|link|img)$/.test(name)) {
out.push('<\/', name, '>');
}
break;
case 3: case 4: // text
out.push(textToHtml(node.nodeValue));
break;
}
}
/**
* @param golden a mapping from IDs of prettyprinted chunks to an abbreviated
* form of the expected output. See "var goldens" in prettify_test.html
* for an example.
*/
function go(goldens) {
startClock();
prettyPrint(function () { stopClock(); runTests(goldens); });
}
function runTests(goldens) {
/** number of characters in common at the end up to max. */
function commonPrefix(a, b) {
var n = Math.min(a.length, b.length);
var i;
for (i = 0; i < n; ++i) {
if (a.charAt(i) !== b.charAt(i)) { break; }
}
return i;
}
/** number of characters in common at the end up to max. */
function commonSuffix(a, b, max) {
var n = Math.min(a.length - max, b.length - max);
var i;
for (i = 0; i < n; ++i) {
if (a.charAt(a.length - i - 1) !== b.charAt(b.length - i - 1)) { break; }
}
return i;
}
/** convert a plain text string to html by escaping html special chars. */
function html(plainText) {
return attribToHtml(plainText).replace(/\xa0/g, ' ');
}
/**
* get normalized markup. innerHTML varies enough across browsers that we
* can't use it.
*/
function normalizedInnerHtml(node) {
var out = [];
for (var child = node.firstChild; child; child = child.nextSibling) {
normalizedHtml(child, out, true);
}
out = out.join('');
// more normalization to work around problems with non-ascii chars in
// regexps in Safari
for (var i = 0; (i = out.indexOf('\xa0')) >= 0;) {
out = out.substring(0, i) + ' ' + out.substring(i + 1);
}
return out.replace(/\r\n?/g, '\n');
}
var htmlOut = [];
var failures = 0;
document.getElementById('errorReport').innerHTML =
'<h1>Running tests…<\/h1>';
htmlOut.push('<h1>Test results<\/h1>');
for (var lang in goldens) {
var container = document.getElementById(lang);
// Convert abbreviations that start with `.
var golden = goldens[lang].replace(/`([A-Z]{3})/g, function (_, lbl) {
return (lbl == 'END'
? '<\/span>'
: '<span class="' + lbl.toLowerCase() + '">');
})
// Line numbers
.replace(/`#(?![0-9])/, '<li class="L0">')
.replace(/`#([0-9])/g, '</li><li class="L$1">');
var actual = normalizedInnerHtml(container);
if (golden !== actual) { // test failed
// write out
var pre = commonPrefix(golden, actual);
var post = commonSuffix(golden, actual, pre);
++failures;
htmlOut.push(
'<h2><a href="#' + html(lang) + '">'
+ html(lang) + '<\/a> Failed<\/h2>');
htmlOut.push(
'<tt>' + html(golden.substring(0, pre)) +
'»<span class="mismatch">' +
html(golden.substring(pre, golden.length - post)) +
'<\/span>«' +
html(golden.substring(golden.length - post)) +
'<br>!==<br>' +
html(actual.substring(0, pre)) +
'»<span class="mismatch">' +
html(actual.substring(pre, actual.length - post)) +
'<\/span>«' +
html(actual.substring(actual.length - post)) + '<\/tt>');
} else {
htmlOut.push(
'<h2><a href="#' + html(lang) + '">' + html(lang) + '<\/a> OK<\/h2>');
}
}
var summary = (
failures
? (failures + ' test' + (failures === 1 ? '' : 's') + ' failed')
: 'Tests Passed');
var summaryStr = '<h2>' + summary + '<\/h2>';
htmlOut.push(summaryStr);
htmlOut.splice(0, 0, summaryStr);
document.title += ' \u2014 ' + summary;
document.getElementById('errorReport').innerHTML =
htmlOut.join('').replace(/<br>/g, '<br>\n');
}
var startTime = null;
function startClock() {
startTime = (new Date).getTime();
}
function stopClock() {
var delta = (new Date).getTime() - startTime;
startTime = null;
document.getElementById('timing').innerHTML = 'Took ' + delta + ' ms';
}