node-attribute-parser.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. /**
  7. * This module contains a small element attribute value parser. It's primary
  8. * goal is to extract link information from attribute values (like the href in
  9. * <a href="/some/link.html"> for example).
  10. *
  11. * There are several types of linkable attribute values:
  12. * - TYPE_URI: a URI (e.g. <a href="uri">).
  13. * - TYPE_URI_LIST: a space separated list of URIs (e.g. <a ping="uri1 uri2">).
  14. * - TYPE_IDREF: a reference to an other element in the same document via its id
  15. * (e.g. <label for="input-id"> or <key command="command-id">).
  16. * - TYPE_IDREF_LIST: a space separated list of IDREFs (e.g.
  17. * <output for="id1 id2">).
  18. * - TYPE_JS_RESOURCE_URI: a URI to a javascript resource that can be opened in
  19. * the devtools (e.g. <script src="uri">).
  20. * - TYPE_CSS_RESOURCE_URI: a URI to a css resource that can be opened in the
  21. * devtools (e.g. <link href="uri">).
  22. *
  23. * parseAttribute is the parser entry function, exported on this module.
  24. */
  25. const TYPE_STRING = "string";
  26. const TYPE_URI = "uri";
  27. const TYPE_URI_LIST = "uriList";
  28. const TYPE_IDREF = "idref";
  29. const TYPE_IDREF_LIST = "idrefList";
  30. const TYPE_JS_RESOURCE_URI = "jsresource";
  31. const TYPE_CSS_RESOURCE_URI = "cssresource";
  32. const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
  33. const HTML_NS = "http://www.w3.org/1999/xhtml";
  34. /* eslint-disable max-len */
  35. const ATTRIBUTE_TYPES = [
  36. {namespaceURI: HTML_NS, attributeName: "action", tagName: "form", type: TYPE_URI},
  37. {namespaceURI: HTML_NS, attributeName: "background", tagName: "body", type: TYPE_URI},
  38. {namespaceURI: HTML_NS, attributeName: "cite", tagName: "blockquote", type: TYPE_URI},
  39. {namespaceURI: HTML_NS, attributeName: "cite", tagName: "q", type: TYPE_URI},
  40. {namespaceURI: HTML_NS, attributeName: "cite", tagName: "del", type: TYPE_URI},
  41. {namespaceURI: HTML_NS, attributeName: "cite", tagName: "ins", type: TYPE_URI},
  42. {namespaceURI: HTML_NS, attributeName: "classid", tagName: "object", type: TYPE_URI},
  43. {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "object", type: TYPE_URI},
  44. {namespaceURI: HTML_NS, attributeName: "codebase", tagName: "applet", type: TYPE_URI},
  45. {namespaceURI: HTML_NS, attributeName: "command", tagName: "menuitem", type: TYPE_IDREF},
  46. {namespaceURI: "*", attributeName: "contextmenu", tagName: "*", type: TYPE_IDREF},
  47. {namespaceURI: HTML_NS, attributeName: "data", tagName: "object", type: TYPE_URI},
  48. {namespaceURI: HTML_NS, attributeName: "for", tagName: "label", type: TYPE_IDREF},
  49. {namespaceURI: HTML_NS, attributeName: "for", tagName: "output", type: TYPE_IDREF_LIST},
  50. {namespaceURI: HTML_NS, attributeName: "form", tagName: "button", type: TYPE_IDREF},
  51. {namespaceURI: HTML_NS, attributeName: "form", tagName: "fieldset", type: TYPE_IDREF},
  52. {namespaceURI: HTML_NS, attributeName: "form", tagName: "input", type: TYPE_IDREF},
  53. {namespaceURI: HTML_NS, attributeName: "form", tagName: "keygen", type: TYPE_IDREF},
  54. {namespaceURI: HTML_NS, attributeName: "form", tagName: "label", type: TYPE_IDREF},
  55. {namespaceURI: HTML_NS, attributeName: "form", tagName: "object", type: TYPE_IDREF},
  56. {namespaceURI: HTML_NS, attributeName: "form", tagName: "output", type: TYPE_IDREF},
  57. {namespaceURI: HTML_NS, attributeName: "form", tagName: "select", type: TYPE_IDREF},
  58. {namespaceURI: HTML_NS, attributeName: "form", tagName: "textarea", type: TYPE_IDREF},
  59. {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "button", type: TYPE_URI},
  60. {namespaceURI: HTML_NS, attributeName: "formaction", tagName: "input", type: TYPE_URI},
  61. {namespaceURI: HTML_NS, attributeName: "headers", tagName: "td", type: TYPE_IDREF_LIST},
  62. {namespaceURI: HTML_NS, attributeName: "headers", tagName: "th", type: TYPE_IDREF_LIST},
  63. {namespaceURI: HTML_NS, attributeName: "href", tagName: "a", type: TYPE_URI},
  64. {namespaceURI: HTML_NS, attributeName: "href", tagName: "area", type: TYPE_URI},
  65. {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_CSS_RESOURCE_URI,
  66. /* eslint-enable */
  67. isValid: (namespaceURI, tagName, attributes) => {
  68. return getAttribute(attributes, "rel") === "stylesheet";
  69. }},
  70. /* eslint-disable max-len */
  71. {namespaceURI: "*", attributeName: "href", tagName: "link", type: TYPE_URI},
  72. {namespaceURI: HTML_NS, attributeName: "href", tagName: "base", type: TYPE_URI},
  73. {namespaceURI: HTML_NS, attributeName: "icon", tagName: "menuitem", type: TYPE_URI},
  74. {namespaceURI: HTML_NS, attributeName: "list", tagName: "input", type: TYPE_IDREF},
  75. {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "img", type: TYPE_URI},
  76. {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "frame", type: TYPE_URI},
  77. {namespaceURI: HTML_NS, attributeName: "longdesc", tagName: "iframe", type: TYPE_URI},
  78. {namespaceURI: HTML_NS, attributeName: "manifest", tagName: "html", type: TYPE_URI},
  79. {namespaceURI: HTML_NS, attributeName: "menu", tagName: "button", type: TYPE_IDREF},
  80. {namespaceURI: HTML_NS, attributeName: "ping", tagName: "a", type: TYPE_URI_LIST},
  81. {namespaceURI: HTML_NS, attributeName: "ping", tagName: "area", type: TYPE_URI_LIST},
  82. {namespaceURI: HTML_NS, attributeName: "poster", tagName: "video", type: TYPE_URI},
  83. {namespaceURI: HTML_NS, attributeName: "profile", tagName: "head", type: TYPE_URI},
  84. {namespaceURI: "*", attributeName: "src", tagName: "script", type: TYPE_JS_RESOURCE_URI},
  85. {namespaceURI: HTML_NS, attributeName: "src", tagName: "input", type: TYPE_URI},
  86. {namespaceURI: HTML_NS, attributeName: "src", tagName: "frame", type: TYPE_URI},
  87. {namespaceURI: HTML_NS, attributeName: "src", tagName: "iframe", type: TYPE_URI},
  88. {namespaceURI: HTML_NS, attributeName: "src", tagName: "img", type: TYPE_URI},
  89. {namespaceURI: HTML_NS, attributeName: "src", tagName: "audio", type: TYPE_URI},
  90. {namespaceURI: HTML_NS, attributeName: "src", tagName: "embed", type: TYPE_URI},
  91. {namespaceURI: HTML_NS, attributeName: "src", tagName: "source", type: TYPE_URI},
  92. {namespaceURI: HTML_NS, attributeName: "src", tagName: "track", type: TYPE_URI},
  93. {namespaceURI: HTML_NS, attributeName: "src", tagName: "video", type: TYPE_URI},
  94. {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "img", type: TYPE_URI},
  95. {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "input", type: TYPE_URI},
  96. {namespaceURI: HTML_NS, attributeName: "usemap", tagName: "object", type: TYPE_URI},
  97. {namespaceURI: "*", attributeName: "xmlns", tagName: "*", type: TYPE_URI},
  98. {namespaceURI: XUL_NS, attributeName: "command", tagName: "key", type: TYPE_IDREF},
  99. {namespaceURI: XUL_NS, attributeName: "containment", tagName: "*", type: TYPE_URI},
  100. {namespaceURI: XUL_NS, attributeName: "context", tagName: "*", type: TYPE_IDREF},
  101. {namespaceURI: XUL_NS, attributeName: "datasources", tagName: "*", type: TYPE_URI_LIST},
  102. {namespaceURI: XUL_NS, attributeName: "insertafter", tagName: "*", type: TYPE_IDREF},
  103. {namespaceURI: XUL_NS, attributeName: "insertbefore", tagName: "*", type: TYPE_IDREF},
  104. {namespaceURI: XUL_NS, attributeName: "menu", tagName: "*", type: TYPE_IDREF},
  105. {namespaceURI: XUL_NS, attributeName: "observes", tagName: "*", type: TYPE_IDREF},
  106. {namespaceURI: XUL_NS, attributeName: "popup", tagName: "*", type: TYPE_IDREF},
  107. {namespaceURI: XUL_NS, attributeName: "ref", tagName: "*", type: TYPE_URI},
  108. {namespaceURI: XUL_NS, attributeName: "removeelement", tagName: "*", type: TYPE_IDREF},
  109. {namespaceURI: XUL_NS, attributeName: "sortResource", tagName: "*", type: TYPE_URI},
  110. {namespaceURI: XUL_NS, attributeName: "sortResource2", tagName: "*", type: TYPE_URI},
  111. {namespaceURI: XUL_NS, attributeName: "src", tagName: "stringbundle", type: TYPE_URI},
  112. {namespaceURI: XUL_NS, attributeName: "template", tagName: "*", type: TYPE_IDREF},
  113. {namespaceURI: XUL_NS, attributeName: "tooltip", tagName: "*", type: TYPE_IDREF},
  114. /* eslint-enable */
  115. // SVG links aren't handled yet, see bug 1158831.
  116. // {namespaceURI: SVG_NS, attributeName: "fill", tagName: "*", type: },
  117. // {namespaceURI: SVG_NS, attributeName: "stroke", tagName: "*", type: },
  118. // {namespaceURI: SVG_NS, attributeName: "markerstart", tagName: "*", type: },
  119. // {namespaceURI: SVG_NS, attributeName: "markermid", tagName: "*", type: },
  120. // {namespaceURI: SVG_NS, attributeName: "markerend", tagName: "*", type: },
  121. // {namespaceURI: SVG_NS, attributeName: "xlink:href", tagName: "*", type: }
  122. ];
  123. var parsers = {
  124. [TYPE_URI]: function (attributeValue) {
  125. return [{
  126. type: TYPE_URI,
  127. value: attributeValue
  128. }];
  129. },
  130. [TYPE_URI_LIST]: function (attributeValue) {
  131. let data = splitBy(attributeValue, " ");
  132. for (let token of data) {
  133. if (!token.type) {
  134. token.type = TYPE_URI;
  135. }
  136. }
  137. return data;
  138. },
  139. [TYPE_JS_RESOURCE_URI]: function (attributeValue) {
  140. return [{
  141. type: TYPE_JS_RESOURCE_URI,
  142. value: attributeValue
  143. }];
  144. },
  145. [TYPE_CSS_RESOURCE_URI]: function (attributeValue) {
  146. return [{
  147. type: TYPE_CSS_RESOURCE_URI,
  148. value: attributeValue
  149. }];
  150. },
  151. [TYPE_IDREF]: function (attributeValue) {
  152. return [{
  153. type: TYPE_IDREF,
  154. value: attributeValue
  155. }];
  156. },
  157. [TYPE_IDREF_LIST]: function (attributeValue) {
  158. let data = splitBy(attributeValue, " ");
  159. for (let token of data) {
  160. if (!token.type) {
  161. token.type = TYPE_IDREF;
  162. }
  163. }
  164. return data;
  165. }
  166. };
  167. /**
  168. * Parse an attribute value.
  169. * @param {String} namespaceURI The namespaceURI of the node that has the
  170. * attribute.
  171. * @param {String} tagName The tagName of the node that has the attribute.
  172. * @param {Array} attributes The list of all attributes of the node. This should
  173. * be an array of {name, value} objects.
  174. * @param {String} attributeName The name of the attribute to parse.
  175. * @return {Array} An array of tokens that represents the value. Each token is
  176. * an object {type: [string|uri|jsresource|cssresource|idref], value}.
  177. * For instance parsing the ping attribute in <a ping="uri1 uri2"> returns:
  178. * [
  179. * {type: "uri", value: "uri2"},
  180. * {type: "string", value: " "},
  181. * {type: "uri", value: "uri1"}
  182. * ]
  183. */
  184. function parseAttribute(namespaceURI, tagName, attributes, attributeName) {
  185. if (!hasAttribute(attributes, attributeName)) {
  186. throw new Error(`Attribute ${attributeName} isn't part of the ` +
  187. "provided attributes");
  188. }
  189. let type = getType(namespaceURI, tagName, attributes, attributeName);
  190. if (!type) {
  191. return [{
  192. type: TYPE_STRING,
  193. value: getAttribute(attributes, attributeName)
  194. }];
  195. }
  196. return parsers[type](getAttribute(attributes, attributeName));
  197. }
  198. /**
  199. * Get the type for links in this attribute if any.
  200. * @param {String} namespaceURI The node's namespaceURI.
  201. * @param {String} tagName The node's tagName.
  202. * @param {Array} attributes The node's attributes, as a list of {name, value}
  203. * objects.
  204. * @param {String} attributeName The name of the attribute to get the type for.
  205. * @return {Object} null if no type exist for this attribute on this node, the
  206. * type object otherwise.
  207. */
  208. function getType(namespaceURI, tagName, attributes, attributeName) {
  209. for (let typeData of ATTRIBUTE_TYPES) {
  210. let containsAttribute = attributeName === typeData.attributeName ||
  211. typeData.attributeName === "*";
  212. let hasNamespace = namespaceURI === typeData.namespaceURI ||
  213. typeData.namespaceURI === "*";
  214. let hasTagName = tagName.toLowerCase() === typeData.tagName ||
  215. typeData.tagName === "*";
  216. let isValid = typeData.isValid
  217. ? typeData.isValid(namespaceURI,
  218. tagName,
  219. attributes,
  220. attributeName)
  221. : true;
  222. if (containsAttribute && hasNamespace && hasTagName && isValid) {
  223. return typeData.type;
  224. }
  225. }
  226. return null;
  227. }
  228. function getAttribute(attributes, attributeName) {
  229. for (let {name, value} of attributes) {
  230. if (name === attributeName) {
  231. return value;
  232. }
  233. }
  234. return null;
  235. }
  236. function hasAttribute(attributes, attributeName) {
  237. for (let {name} of attributes) {
  238. if (name === attributeName) {
  239. return true;
  240. }
  241. }
  242. return false;
  243. }
  244. /**
  245. * Split a string by a given character and return an array of objects parts.
  246. * The array will contain objects for the split character too, marked with
  247. * TYPE_STRING type.
  248. * @param {String} value The string to parse.
  249. * @param {String} splitChar A 1 length split character.
  250. * @return {Array}
  251. */
  252. function splitBy(value, splitChar) {
  253. let data = [], i = 0, buffer = "";
  254. while (i <= value.length) {
  255. if (i === value.length && buffer) {
  256. data.push({value: buffer});
  257. }
  258. if (value[i] === splitChar) {
  259. if (buffer) {
  260. data.push({value: buffer});
  261. }
  262. data.push({
  263. type: TYPE_STRING,
  264. value: splitChar
  265. });
  266. buffer = "";
  267. } else {
  268. buffer += value[i];
  269. }
  270. i++;
  271. }
  272. return data;
  273. }
  274. exports.parseAttribute = parseAttribute;
  275. // Exported for testing only.
  276. exports.splitBy = splitBy;