www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

domTree.js (9377B)


      1 /**
      2  * These objects store the data about the DOM nodes we create, as well as some
      3  * extra data. They can then be transformed into real DOM nodes with the
      4  * `toNode` function or HTML markup using `toMarkup`. They are useful for both
      5  * storing extra properties on the nodes, as well as providing a way to easily
      6  * work with the DOM.
      7  *
      8  * Similar functions for working with MathML nodes exist in mathMLTree.js.
      9  */
     10 const unicodeRegexes = require("./unicodeRegexes");
     11 const utils = require("./utils");
     12 
     13 /**
     14  * Create an HTML className based on a list of classes. In addition to joining
     15  * with spaces, we also remove null or empty classes.
     16  */
     17 const createClass = function(classes) {
     18     classes = classes.slice();
     19     for (let i = classes.length - 1; i >= 0; i--) {
     20         if (!classes[i]) {
     21             classes.splice(i, 1);
     22         }
     23     }
     24 
     25     return classes.join(" ");
     26 };
     27 
     28 /**
     29  * This node represents a span node, with a className, a list of children, and
     30  * an inline style. It also contains information about its height, depth, and
     31  * maxFontSize.
     32  */
     33 function span(classes, children, options) {
     34     this.classes = classes || [];
     35     this.children = children || [];
     36     this.height = 0;
     37     this.depth = 0;
     38     this.maxFontSize = 0;
     39     this.style = {};
     40     this.attributes = {};
     41     if (options) {
     42         if (options.style.isTight()) {
     43             this.classes.push("mtight");
     44         }
     45         if (options.getColor()) {
     46             this.style.color = options.getColor();
     47         }
     48     }
     49 }
     50 
     51 /**
     52  * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
     53  * browsers support attributes the same, and having too many custom attributes
     54  * is probably bad.
     55  */
     56 span.prototype.setAttribute = function(attribute, value) {
     57     this.attributes[attribute] = value;
     58 };
     59 
     60 span.prototype.tryCombine = function(sibling) {
     61     return false;
     62 };
     63 
     64 /**
     65  * Convert the span into an HTML node
     66  */
     67 span.prototype.toNode = function() {
     68     const span = document.createElement("span");
     69 
     70     // Apply the class
     71     span.className = createClass(this.classes);
     72 
     73     // Apply inline styles
     74     for (const style in this.style) {
     75         if (Object.prototype.hasOwnProperty.call(this.style, style)) {
     76             span.style[style] = this.style[style];
     77         }
     78     }
     79 
     80     // Apply attributes
     81     for (const attr in this.attributes) {
     82         if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
     83             span.setAttribute(attr, this.attributes[attr]);
     84         }
     85     }
     86 
     87     // Append the children, also as HTML nodes
     88     for (let i = 0; i < this.children.length; i++) {
     89         span.appendChild(this.children[i].toNode());
     90     }
     91 
     92     return span;
     93 };
     94 
     95 /**
     96  * Convert the span into an HTML markup string
     97  */
     98 span.prototype.toMarkup = function() {
     99     let markup = "<span";
    100 
    101     // Add the class
    102     if (this.classes.length) {
    103         markup += " class=\"";
    104         markup += utils.escape(createClass(this.classes));
    105         markup += "\"";
    106     }
    107 
    108     let styles = "";
    109 
    110     // Add the styles, after hyphenation
    111     for (const style in this.style) {
    112         if (this.style.hasOwnProperty(style)) {
    113             styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
    114         }
    115     }
    116 
    117     if (styles) {
    118         markup += " style=\"" + utils.escape(styles) + "\"";
    119     }
    120 
    121     // Add the attributes
    122     for (const attr in this.attributes) {
    123         if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
    124             markup += " " + attr + "=\"";
    125             markup += utils.escape(this.attributes[attr]);
    126             markup += "\"";
    127         }
    128     }
    129 
    130     markup += ">";
    131 
    132     // Add the markup of the children, also as markup
    133     for (let i = 0; i < this.children.length; i++) {
    134         markup += this.children[i].toMarkup();
    135     }
    136 
    137     markup += "</span>";
    138 
    139     return markup;
    140 };
    141 
    142 /**
    143  * This node represents a document fragment, which contains elements, but when
    144  * placed into the DOM doesn't have any representation itself. Thus, it only
    145  * contains children and doesn't have any HTML properties. It also keeps track
    146  * of a height, depth, and maxFontSize.
    147  */
    148 function documentFragment(children) {
    149     this.children = children || [];
    150     this.height = 0;
    151     this.depth = 0;
    152     this.maxFontSize = 0;
    153 }
    154 
    155 /**
    156  * Convert the fragment into a node
    157  */
    158 documentFragment.prototype.toNode = function() {
    159     // Create a fragment
    160     const frag = document.createDocumentFragment();
    161 
    162     // Append the children
    163     for (let i = 0; i < this.children.length; i++) {
    164         frag.appendChild(this.children[i].toNode());
    165     }
    166 
    167     return frag;
    168 };
    169 
    170 /**
    171  * Convert the fragment into HTML markup
    172  */
    173 documentFragment.prototype.toMarkup = function() {
    174     let markup = "";
    175 
    176     // Simply concatenate the markup for the children together
    177     for (let i = 0; i < this.children.length; i++) {
    178         markup += this.children[i].toMarkup();
    179     }
    180 
    181     return markup;
    182 };
    183 
    184 const iCombinations = {
    185     'î': '\u0131\u0302',
    186     'ï': '\u0131\u0308',
    187     'í': '\u0131\u0301',
    188     // 'ī': '\u0131\u0304', // enable when we add Extended Latin
    189     'ì': '\u0131\u0300',
    190 };
    191 
    192 /**
    193  * A symbol node contains information about a single symbol. It either renders
    194  * to a single text node, or a span with a single text node in it, depending on
    195  * whether it has CSS classes, styles, or needs italic correction.
    196  */
    197 function symbolNode(value, height, depth, italic, skew, classes, style) {
    198     this.value = value || "";
    199     this.height = height || 0;
    200     this.depth = depth || 0;
    201     this.italic = italic || 0;
    202     this.skew = skew || 0;
    203     this.classes = classes || [];
    204     this.style = style || {};
    205     this.maxFontSize = 0;
    206 
    207     // Mark CJK characters with specific classes so that we can specify which
    208     // fonts to use.  This allows us to render these characters with a serif
    209     // font in situations where the browser would either default to a sans serif
    210     // or render a placeholder character.
    211     if (unicodeRegexes.cjkRegex.test(value)) {
    212         // I couldn't find any fonts that contained Hangul as well as all of
    213         // the other characters we wanted to test there for it gets its own
    214         // CSS class.
    215         if (unicodeRegexes.hangulRegex.test(value)) {
    216             this.classes.push('hangul_fallback');
    217         } else {
    218             this.classes.push('cjk_fallback');
    219         }
    220     }
    221 
    222     if (/[îïíì]/.test(this.value)) {    // add ī when we add Extended Latin
    223         this.value = iCombinations[this.value];
    224     }
    225 }
    226 
    227 symbolNode.prototype.tryCombine = function(sibling) {
    228     if (!sibling
    229         || !(sibling instanceof symbolNode)
    230         || this.italic > 0
    231         || createClass(this.classes) !== createClass(sibling.classes)
    232         || this.skew !== sibling.skew
    233         || this.maxFontSize !== sibling.maxFontSize) {
    234         return false;
    235     }
    236     for (const style in this.style) {
    237         if (this.style.hasOwnProperty(style)
    238             && this.style[style] !== sibling.style[style]) {
    239             return false;
    240         }
    241     }
    242     for (const style in sibling.style) {
    243         if (sibling.style.hasOwnProperty(style)
    244             && this.style[style] !== sibling.style[style]) {
    245             return false;
    246         }
    247     }
    248     this.value += sibling.value;
    249     this.height = Math.max(this.height, sibling.height);
    250     this.depth = Math.max(this.depth, sibling.depth);
    251     this.italic = sibling.italic;
    252     return true;
    253 };
    254 
    255 /**
    256  * Creates a text node or span from a symbol node. Note that a span is only
    257  * created if it is needed.
    258  */
    259 symbolNode.prototype.toNode = function() {
    260     const node = document.createTextNode(this.value);
    261     let span = null;
    262 
    263     if (this.italic > 0) {
    264         span = document.createElement("span");
    265         span.style.marginRight = this.italic + "em";
    266     }
    267 
    268     if (this.classes.length > 0) {
    269         span = span || document.createElement("span");
    270         span.className = createClass(this.classes);
    271     }
    272 
    273     for (const style in this.style) {
    274         if (this.style.hasOwnProperty(style)) {
    275             span = span || document.createElement("span");
    276             span.style[style] = this.style[style];
    277         }
    278     }
    279 
    280     if (span) {
    281         span.appendChild(node);
    282         return span;
    283     } else {
    284         return node;
    285     }
    286 };
    287 
    288 /**
    289  * Creates markup for a symbol node.
    290  */
    291 symbolNode.prototype.toMarkup = function() {
    292     // TODO(alpert): More duplication than I'd like from
    293     // span.prototype.toMarkup and symbolNode.prototype.toNode...
    294     let needsSpan = false;
    295 
    296     let markup = "<span";
    297 
    298     if (this.classes.length) {
    299         needsSpan = true;
    300         markup += " class=\"";
    301         markup += utils.escape(createClass(this.classes));
    302         markup += "\"";
    303     }
    304 
    305     let styles = "";
    306 
    307     if (this.italic > 0) {
    308         styles += "margin-right:" + this.italic + "em;";
    309     }
    310     for (const style in this.style) {
    311         if (this.style.hasOwnProperty(style)) {
    312             styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
    313         }
    314     }
    315 
    316     if (styles) {
    317         needsSpan = true;
    318         markup += " style=\"" + utils.escape(styles) + "\"";
    319     }
    320 
    321     const escaped = utils.escape(this.value);
    322     if (needsSpan) {
    323         markup += ">";
    324         markup += escaped;
    325         markup += "</span>";
    326         return markup;
    327     } else {
    328         return escaped;
    329     }
    330 };
    331 
    332 module.exports = {
    333     span: span,
    334     documentFragment: documentFragment,
    335     symbolNode: symbolNode,
    336 };