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 };