node-attribute-parser.js (12765B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * This module contains a small element attribute value parser. It's primary 9 * goal is to extract link information from attribute values (like the href in 10 * <a href="/some/link.html"> for example). 11 * 12 * There are several types of linkable attribute values: 13 * - TYPE_URI: a URI (e.g. <a href="uri">). 14 * - TYPE_URI_LIST: a space separated list of URIs (e.g. <a ping="uri1 uri2">). 15 * - TYPE_IDREF: a reference to an other element in the same document via its id 16 * (e.g. <label for="input-id"> or <key command="command-id">). 17 * - TYPE_IDREF_LIST: a space separated list of IDREFs (e.g. 18 * <output for="id1 id2">). 19 * - TYPE_JS_RESOURCE_URI: a URI to a javascript resource that can be opened in 20 * the devtools (e.g. <script src="uri">). 21 * - TYPE_CSS_RESOURCE_URI: a URI to a css resource that can be opened in the 22 * devtools (e.g. <link href="uri">). 23 * 24 * parseAttribute is the parser entry function, exported on this module. 25 */ 26 27 const TYPE_STRING = "string"; 28 const TYPE_URI = "uri"; 29 const TYPE_URI_LIST = "uriList"; 30 const TYPE_IDREF = "idref"; 31 const TYPE_IDREF_LIST = "idrefList"; 32 const TYPE_JS_RESOURCE_URI = "jsresource"; 33 const TYPE_CSS_RESOURCE_URI = "cssresource"; 34 35 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 36 const HTML_NS = "http://www.w3.org/1999/xhtml"; 37 38 const WILDCARD = Symbol(); 39 40 const ATTRIBUTE_TYPES = new Map([ 41 ["action", { form: { namespaceURI: HTML_NS, type: TYPE_URI } }], 42 [ 43 "aria-activedescendant", 44 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF } }, 45 ], 46 [ 47 "aria-controls", 48 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST } }, 49 ], 50 [ 51 "aria-describedby", 52 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST } }, 53 ], 54 [ 55 "aria-details", 56 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST } }, 57 ], 58 [ 59 "aria-errormessage", 60 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF } }, 61 ], 62 [ 63 "aria-flowto", 64 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST } }, 65 ], 66 [ 67 "aria-labelledby", 68 { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST } }, 69 ], 70 ["aria-owns", { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST } }], 71 ["background", { body: { namespaceURI: HTML_NS, type: TYPE_URI } }], 72 [ 73 "cite", 74 { 75 blockquote: { namespaceURI: HTML_NS, type: TYPE_URI }, 76 q: { namespaceURI: HTML_NS, type: TYPE_URI }, 77 del: { namespaceURI: HTML_NS, type: TYPE_URI }, 78 ins: { namespaceURI: HTML_NS, type: TYPE_URI }, 79 }, 80 ], 81 ["classid", { object: { namespaceURI: HTML_NS, type: TYPE_URI } }], 82 [ 83 "codebase", 84 { 85 object: { namespaceURI: HTML_NS, type: TYPE_URI }, 86 applet: { namespaceURI: HTML_NS, type: TYPE_URI }, 87 }, 88 ], 89 [ 90 "command", 91 { 92 menuitem: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 93 key: { namespaceURI: XUL_NS, type: TYPE_IDREF }, 94 }, 95 ], 96 ["commandfor", { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF } }], 97 [ 98 "contextmenu", 99 { 100 WILDCARD: { namespaceURI: WILDCARD, type: TYPE_IDREF }, 101 }, 102 ], 103 ["data", { object: { namespaceURI: HTML_NS, type: TYPE_URI } }], 104 [ 105 "for", 106 { 107 label: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 108 output: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST }, 109 }, 110 ], 111 [ 112 "form", 113 { 114 button: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 115 fieldset: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 116 input: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 117 keygen: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 118 label: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 119 object: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 120 output: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 121 select: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 122 textarea: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 123 }, 124 ], 125 [ 126 "formaction", 127 { 128 button: { namespaceURI: HTML_NS, type: TYPE_URI }, 129 input: { namespaceURI: HTML_NS, type: TYPE_URI }, 130 }, 131 ], 132 [ 133 "headers", 134 { 135 td: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST }, 136 th: { namespaceURI: HTML_NS, type: TYPE_IDREF_LIST }, 137 }, 138 ], 139 [ 140 "href", 141 { 142 a: { namespaceURI: HTML_NS, type: TYPE_URI }, 143 area: { namespaceURI: HTML_NS, type: TYPE_URI }, 144 link: [ 145 { 146 namespaceURI: WILDCARD, 147 type: TYPE_CSS_RESOURCE_URI, 148 isValid: attributes => { 149 return getAttribute(attributes, "rel") === "stylesheet"; 150 }, 151 }, 152 { namespaceURI: WILDCARD, type: TYPE_URI }, 153 ], 154 base: { namespaceURI: HTML_NS, type: TYPE_URI }, 155 }, 156 ], 157 [ 158 "icon", 159 { 160 menuitem: { namespaceURI: HTML_NS, type: TYPE_URI }, 161 }, 162 ], 163 ["list", { input: { namespaceURI: HTML_NS, type: TYPE_IDREF } }], 164 [ 165 "longdesc", 166 { 167 img: { namespaceURI: HTML_NS, type: TYPE_URI }, 168 frame: { namespaceURI: HTML_NS, type: TYPE_URI }, 169 iframe: { namespaceURI: HTML_NS, type: TYPE_URI }, 170 }, 171 ], 172 ["manifest", { html: { namespaceURI: HTML_NS, type: TYPE_URI } }], 173 [ 174 "menu", 175 { 176 button: { namespaceURI: HTML_NS, type: TYPE_IDREF }, 177 WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF }, 178 }, 179 ], 180 [ 181 "ping", 182 { 183 a: { namespaceURI: HTML_NS, type: TYPE_URI_LIST }, 184 area: { namespaceURI: HTML_NS, type: TYPE_URI_LIST }, 185 }, 186 ], 187 ["poster", { video: { namespaceURI: HTML_NS, type: TYPE_URI } }], 188 ["profile", { head: { namespaceURI: HTML_NS, type: TYPE_URI } }], 189 [ 190 "src", 191 { 192 script: { namespaceURI: WILDCARD, type: TYPE_JS_RESOURCE_URI }, 193 input: { namespaceURI: HTML_NS, type: TYPE_URI }, 194 frame: { namespaceURI: HTML_NS, type: TYPE_URI }, 195 iframe: { namespaceURI: HTML_NS, type: TYPE_URI }, 196 img: { namespaceURI: HTML_NS, type: TYPE_URI }, 197 audio: { namespaceURI: HTML_NS, type: TYPE_URI }, 198 embed: { namespaceURI: HTML_NS, type: TYPE_URI }, 199 source: { namespaceURI: HTML_NS, type: TYPE_URI }, 200 track: { namespaceURI: HTML_NS, type: TYPE_URI }, 201 video: { namespaceURI: HTML_NS, type: TYPE_URI }, 202 stringbundle: { namespaceURI: XUL_NS, type: TYPE_URI }, 203 }, 204 ], 205 [ 206 "usemap", 207 { 208 img: { namespaceURI: HTML_NS, type: TYPE_URI }, 209 input: { namespaceURI: HTML_NS, type: TYPE_URI }, 210 object: { namespaceURI: HTML_NS, type: TYPE_URI }, 211 }, 212 ], 213 ["xmlns", { WILDCARD: { namespaceURI: WILDCARD, type: TYPE_URI } }], 214 ["containment", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_URI } }], 215 ["context", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 216 ["datasources", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_URI_LIST } }], 217 ["insertafter", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 218 ["insertbefore", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 219 ["observes", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 220 ["popovertarget", { WILDCARD: { namespaceURI: HTML_NS, type: TYPE_IDREF } }], 221 ["popup", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 222 ["ref", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_URI } }], 223 ["removeelement", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 224 ["template", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 225 ["tooltip", { WILDCARD: { namespaceURI: XUL_NS, type: TYPE_IDREF } }], 226 // SVG links aren't handled yet, see bug 1158831. 227 // ["fill", { 228 // WILDCARD: {namespaceURI: SVG_NS, type: }, 229 // }], 230 // ["stroke", { 231 // WILDCARD: {namespaceURI: SVG_NS, type: }, 232 // }], 233 // ["markerstart", { 234 // WILDCARD: {namespaceURI: SVG_NS, type: }, 235 // }], 236 // ["markermid", { 237 // WILDCARD: {namespaceURI: SVG_NS, type: }, 238 // }], 239 // ["markerend", { 240 // WILDCARD: {namespaceURI: SVG_NS, type: }, 241 // }], 242 // ["xlink:href", { 243 // WILDCARD: {namespaceURI: SVG_NS, type: }, 244 // }], 245 ]); 246 247 var parsers = { 248 [TYPE_URI](attributeValue) { 249 return [ 250 { 251 type: TYPE_URI, 252 value: attributeValue, 253 }, 254 ]; 255 }, 256 [TYPE_URI_LIST](attributeValue) { 257 const data = splitBy(attributeValue, " "); 258 for (const token of data) { 259 if (!token.type) { 260 token.type = TYPE_URI; 261 } 262 } 263 return data; 264 }, 265 [TYPE_JS_RESOURCE_URI](attributeValue) { 266 return [ 267 { 268 type: TYPE_JS_RESOURCE_URI, 269 value: attributeValue, 270 }, 271 ]; 272 }, 273 [TYPE_CSS_RESOURCE_URI](attributeValue) { 274 return [ 275 { 276 type: TYPE_CSS_RESOURCE_URI, 277 value: attributeValue, 278 }, 279 ]; 280 }, 281 [TYPE_IDREF](attributeValue) { 282 return [ 283 { 284 type: TYPE_IDREF, 285 value: attributeValue, 286 }, 287 ]; 288 }, 289 [TYPE_IDREF_LIST](attributeValue) { 290 const data = splitBy(attributeValue, " "); 291 for (const token of data) { 292 if (!token.type) { 293 token.type = TYPE_IDREF; 294 } 295 } 296 return data; 297 }, 298 }; 299 300 /** 301 * Parse an attribute value. 302 * 303 * @param {string} namespaceURI The namespaceURI of the node that has the 304 * attribute. 305 * @param {string} tagName The tagName of the node that has the attribute. 306 * @param {Array} attributes The list of all attributes of the node. This should 307 * be an array of {name, value} objects. 308 * @param {string} attributeName The name of the attribute to parse. 309 * @param {string} attributeValue The value of the attribute to parse. 310 * @return {Array} An array of tokens that represents the value. Each token is 311 * an object {type: [string|uri|jsresource|cssresource|idref], value}. 312 * For instance parsing the ping attribute in <a ping="uri1 uri2"> returns: 313 * [ 314 * {type: "uri", value: "uri2"}, 315 * {type: "string", value: " "}, 316 * {type: "uri", value: "uri1"} 317 * ] 318 */ 319 function parseAttribute( 320 namespaceURI, 321 tagName, 322 attributes, 323 attributeName, 324 attributeValue 325 ) { 326 const type = getType(namespaceURI, tagName, attributes, attributeName); 327 if (!type) { 328 return [ 329 { 330 type: TYPE_STRING, 331 value: attributeValue, 332 }, 333 ]; 334 } 335 336 return parsers[type](attributeValue); 337 } 338 339 /** 340 * Get the type for links in this attribute if any. 341 * 342 * @param {string} namespaceURI The node's namespaceURI. 343 * @param {string} tagName The node's tagName. 344 * @param {Array} attributes The node's attributes, as a list of {name, value} 345 * objects. 346 * @param {string} attributeName The name of the attribute to get the type for. 347 * @return {object} null if no type exist for this attribute on this node, the 348 * type object otherwise. 349 */ 350 function getType(namespaceURI, tagName, attributes, attributeName) { 351 const attributeType = ATTRIBUTE_TYPES.get(attributeName); 352 if (!attributeType) { 353 return null; 354 } 355 356 const lcTagName = tagName.toLowerCase(); 357 const typeData = attributeType[lcTagName] || attributeType.WILDCARD; 358 359 if (!typeData) { 360 return null; 361 } 362 363 if (Array.isArray(typeData)) { 364 for (const data of typeData) { 365 const hasNamespace = 366 data.namespaceURI === WILDCARD || data.namespaceURI === namespaceURI; 367 const isValid = data.isValid ? data.isValid(attributes) : true; 368 369 if (hasNamespace && isValid) { 370 return data.type; 371 } 372 } 373 374 return null; 375 } else if ( 376 typeData.namespaceURI === WILDCARD || 377 typeData.namespaceURI === namespaceURI 378 ) { 379 return typeData.type; 380 } 381 382 return null; 383 } 384 385 function getAttribute(attributes, attributeName) { 386 const attribute = attributes.find(x => x.name === attributeName); 387 return attribute ? attribute.value : null; 388 } 389 390 /** 391 * Split a string by a given character and return an array of objects parts. 392 * The array will contain objects for the split character too, marked with 393 * TYPE_STRING type. 394 * 395 * @param {string} value The string to parse. 396 * @param {string} splitChar A 1 length split character. 397 * @return {Array} 398 */ 399 function splitBy(value, splitChar) { 400 const data = []; 401 402 let i = 0, 403 buffer = ""; 404 while (i <= value.length) { 405 if (i === value.length && buffer) { 406 data.push({ value: buffer }); 407 } 408 if (value[i] === splitChar) { 409 if (buffer) { 410 data.push({ value: buffer }); 411 } 412 data.push({ 413 type: TYPE_STRING, 414 value: splitChar, 415 }); 416 buffer = ""; 417 } else { 418 buffer += value[i]; 419 } 420 421 i++; 422 } 423 return data; 424 } 425 426 exports.parseAttribute = parseAttribute; 427 exports.ATTRIBUTE_TYPES = { 428 TYPE_STRING, 429 TYPE_URI, 430 TYPE_URI_LIST, 431 TYPE_IDREF, 432 TYPE_IDREF_LIST, 433 TYPE_JS_RESOURCE_URI, 434 TYPE_CSS_RESOURCE_URI, 435 }; 436 437 // Exported for testing only. 438 exports.splitBy = splitBy;