
This reverts commit 4d2e46e7f6
.
Having trailing commans makes diffs easier to read as it avoids modifying a
line just to add a trailing comma if there is another item to add at the end
of a list. There are plans to switch to ES6 notation and to translate that
to ES5 as part of the build process. Since that translation would remove
trailing commas, the IE9 problems that originally motivated the commit
should vanish soon.
337 lines
9.1 KiB
JavaScript
337 lines
9.1 KiB
JavaScript
/**
|
|
* These objects store the data about the DOM nodes we create, as well as some
|
|
* extra data. They can then be transformed into real DOM nodes with the
|
|
* `toNode` function or HTML markup using `toMarkup`. They are useful for both
|
|
* storing extra properties on the nodes, as well as providing a way to easily
|
|
* work with the DOM.
|
|
*
|
|
* Similar functions for working with MathML nodes exist in mathMLTree.js.
|
|
*/
|
|
var unicodeRegexes = require("./unicodeRegexes");
|
|
var utils = require("./utils");
|
|
|
|
/**
|
|
* Create an HTML className based on a list of classes. In addition to joining
|
|
* with spaces, we also remove null or empty classes.
|
|
*/
|
|
var createClass = function(classes) {
|
|
classes = classes.slice();
|
|
for (var i = classes.length - 1; i >= 0; i--) {
|
|
if (!classes[i]) {
|
|
classes.splice(i, 1);
|
|
}
|
|
}
|
|
|
|
return classes.join(" ");
|
|
};
|
|
|
|
/**
|
|
* This node represents a span node, with a className, a list of children, and
|
|
* an inline style. It also contains information about its height, depth, and
|
|
* maxFontSize.
|
|
*/
|
|
function span(classes, children, options) {
|
|
this.classes = classes || [];
|
|
this.children = children || [];
|
|
this.height = 0;
|
|
this.depth = 0;
|
|
this.maxFontSize = 0;
|
|
this.style = {};
|
|
this.attributes = {};
|
|
if (options) {
|
|
if (options.style.isTight()) {
|
|
this.classes.push("mtight");
|
|
}
|
|
if (options.getColor()) {
|
|
this.style.color = options.getColor();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
|
|
* browsers support attributes the same, and having too many custom attributes
|
|
* is probably bad.
|
|
*/
|
|
span.prototype.setAttribute = function(attribute, value) {
|
|
this.attributes[attribute] = value;
|
|
};
|
|
|
|
span.prototype.tryCombine = function(sibling) {
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Convert the span into an HTML node
|
|
*/
|
|
span.prototype.toNode = function() {
|
|
var span = document.createElement("span");
|
|
|
|
// Apply the class
|
|
span.className = createClass(this.classes);
|
|
|
|
// Apply inline styles
|
|
for (var style in this.style) {
|
|
if (Object.prototype.hasOwnProperty.call(this.style, style)) {
|
|
span.style[style] = this.style[style];
|
|
}
|
|
}
|
|
|
|
// Apply attributes
|
|
for (var attr in this.attributes) {
|
|
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
|
span.setAttribute(attr, this.attributes[attr]);
|
|
}
|
|
}
|
|
|
|
// Append the children, also as HTML nodes
|
|
for (var i = 0; i < this.children.length; i++) {
|
|
span.appendChild(this.children[i].toNode());
|
|
}
|
|
|
|
return span;
|
|
};
|
|
|
|
/**
|
|
* Convert the span into an HTML markup string
|
|
*/
|
|
span.prototype.toMarkup = function() {
|
|
var markup = "<span";
|
|
|
|
// Add the class
|
|
if (this.classes.length) {
|
|
markup += " class=\"";
|
|
markup += utils.escape(createClass(this.classes));
|
|
markup += "\"";
|
|
}
|
|
|
|
var styles = "";
|
|
|
|
// Add the styles, after hyphenation
|
|
for (var style in this.style) {
|
|
if (this.style.hasOwnProperty(style)) {
|
|
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
|
|
}
|
|
}
|
|
|
|
if (styles) {
|
|
markup += " style=\"" + utils.escape(styles) + "\"";
|
|
}
|
|
|
|
// Add the attributes
|
|
for (var attr in this.attributes) {
|
|
if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
|
|
markup += " " + attr + "=\"";
|
|
markup += utils.escape(this.attributes[attr]);
|
|
markup += "\"";
|
|
}
|
|
}
|
|
|
|
markup += ">";
|
|
|
|
// Add the markup of the children, also as markup
|
|
for (var i = 0; i < this.children.length; i++) {
|
|
markup += this.children[i].toMarkup();
|
|
}
|
|
|
|
markup += "</span>";
|
|
|
|
return markup;
|
|
};
|
|
|
|
/**
|
|
* This node represents a document fragment, which contains elements, but when
|
|
* placed into the DOM doesn't have any representation itself. Thus, it only
|
|
* contains children and doesn't have any HTML properties. It also keeps track
|
|
* of a height, depth, and maxFontSize.
|
|
*/
|
|
function documentFragment(children) {
|
|
this.children = children || [];
|
|
this.height = 0;
|
|
this.depth = 0;
|
|
this.maxFontSize = 0;
|
|
}
|
|
|
|
/**
|
|
* Convert the fragment into a node
|
|
*/
|
|
documentFragment.prototype.toNode = function() {
|
|
// Create a fragment
|
|
var frag = document.createDocumentFragment();
|
|
|
|
// Append the children
|
|
for (var i = 0; i < this.children.length; i++) {
|
|
frag.appendChild(this.children[i].toNode());
|
|
}
|
|
|
|
return frag;
|
|
};
|
|
|
|
/**
|
|
* Convert the fragment into HTML markup
|
|
*/
|
|
documentFragment.prototype.toMarkup = function() {
|
|
var markup = "";
|
|
|
|
// Simply concatenate the markup for the children together
|
|
for (var i = 0; i < this.children.length; i++) {
|
|
markup += this.children[i].toMarkup();
|
|
}
|
|
|
|
return markup;
|
|
};
|
|
|
|
var iCombinations = {
|
|
'î': '\u0131\u0302',
|
|
'ï': '\u0131\u0308',
|
|
'í': '\u0131\u0301',
|
|
// 'ī': '\u0131\u0304', // enable when we add Extended Latin
|
|
'ì': '\u0131\u0300',
|
|
};
|
|
|
|
/**
|
|
* A symbol node contains information about a single symbol. It either renders
|
|
* to a single text node, or a span with a single text node in it, depending on
|
|
* whether it has CSS classes, styles, or needs italic correction.
|
|
*/
|
|
function symbolNode(value, height, depth, italic, skew, classes, style) {
|
|
this.value = value || "";
|
|
this.height = height || 0;
|
|
this.depth = depth || 0;
|
|
this.italic = italic || 0;
|
|
this.skew = skew || 0;
|
|
this.classes = classes || [];
|
|
this.style = style || {};
|
|
this.maxFontSize = 0;
|
|
|
|
// Mark CJK characters with specific classes so that we can specify which
|
|
// fonts to use. This allows us to render these characters with a serif
|
|
// font in situations where the browser would either default to a sans serif
|
|
// or render a placeholder character.
|
|
if (unicodeRegexes.cjkRegex.test(value)) {
|
|
// I couldn't find any fonts that contained Hangul as well as all of
|
|
// the other characters we wanted to test there for it gets its own
|
|
// CSS class.
|
|
if (unicodeRegexes.hangulRegex.test(value)) {
|
|
this.classes.push('hangul_fallback');
|
|
} else {
|
|
this.classes.push('cjk_fallback');
|
|
}
|
|
}
|
|
|
|
if (/[îïíì]/.test(this.value)) { // add ī when we add Extended Latin
|
|
this.value = iCombinations[this.value];
|
|
}
|
|
}
|
|
|
|
symbolNode.prototype.tryCombine = function(sibling) {
|
|
if (!sibling
|
|
|| !(sibling instanceof symbolNode)
|
|
|| this.italic > 0
|
|
|| createClass(this.classes) !== createClass(sibling.classes)
|
|
|| this.skew !== sibling.skew
|
|
|| this.maxFontSize !== sibling.maxFontSize) {
|
|
return false;
|
|
}
|
|
for (var style in this.style) {
|
|
if (this.style.hasOwnProperty(style)
|
|
&& this.style[style] !== sibling.style[style]) {
|
|
return false;
|
|
}
|
|
}
|
|
for (style in sibling.style) {
|
|
if (sibling.style.hasOwnProperty(style)
|
|
&& this.style[style] !== sibling.style[style]) {
|
|
return false;
|
|
}
|
|
}
|
|
this.value += sibling.value;
|
|
this.height = Math.max(this.height, sibling.height);
|
|
this.depth = Math.max(this.depth, sibling.depth);
|
|
this.italic = sibling.italic;
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Creates a text node or span from a symbol node. Note that a span is only
|
|
* created if it is needed.
|
|
*/
|
|
symbolNode.prototype.toNode = function() {
|
|
var node = document.createTextNode(this.value);
|
|
var span = null;
|
|
|
|
if (this.italic > 0) {
|
|
span = document.createElement("span");
|
|
span.style.marginRight = this.italic + "em";
|
|
}
|
|
|
|
if (this.classes.length > 0) {
|
|
span = span || document.createElement("span");
|
|
span.className = createClass(this.classes);
|
|
}
|
|
|
|
for (var style in this.style) {
|
|
if (this.style.hasOwnProperty(style)) {
|
|
span = span || document.createElement("span");
|
|
span.style[style] = this.style[style];
|
|
}
|
|
}
|
|
|
|
if (span) {
|
|
span.appendChild(node);
|
|
return span;
|
|
} else {
|
|
return node;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Creates markup for a symbol node.
|
|
*/
|
|
symbolNode.prototype.toMarkup = function() {
|
|
// TODO(alpert): More duplication than I'd like from
|
|
// span.prototype.toMarkup and symbolNode.prototype.toNode...
|
|
var needsSpan = false;
|
|
|
|
var markup = "<span";
|
|
|
|
if (this.classes.length) {
|
|
needsSpan = true;
|
|
markup += " class=\"";
|
|
markup += utils.escape(createClass(this.classes));
|
|
markup += "\"";
|
|
}
|
|
|
|
var styles = "";
|
|
|
|
if (this.italic > 0) {
|
|
styles += "margin-right:" + this.italic + "em;";
|
|
}
|
|
for (var style in this.style) {
|
|
if (this.style.hasOwnProperty(style)) {
|
|
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
|
|
}
|
|
}
|
|
|
|
if (styles) {
|
|
needsSpan = true;
|
|
markup += " style=\"" + utils.escape(styles) + "\"";
|
|
}
|
|
|
|
var escaped = utils.escape(this.value);
|
|
if (needsSpan) {
|
|
markup += ">";
|
|
markup += escaped;
|
|
markup += "</span>";
|
|
return markup;
|
|
} else {
|
|
return escaped;
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
span: span,
|
|
documentFragment: documentFragment,
|
|
symbolNode: symbolNode,
|
|
};
|