Source: lib/util/xml_utils.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.util.XmlUtils');
  18. goog.require('shaka.log');
  19. goog.require('shaka.util.StringUtils');
  20. /**
  21. * @namespace shaka.util.XmlUtils
  22. * @summary A set of XML utility functions.
  23. */
  24. /**
  25. * Finds a child XML element.
  26. * @param {!Node} elem The parent XML element.
  27. * @param {string} name The child XML element's tag name.
  28. * @return {Element} The child XML element, or null if a child XML element does
  29. * not exist with the given tag name OR if there exists more than one
  30. * child XML element with the given tag name.
  31. */
  32. shaka.util.XmlUtils.findChild = function(elem, name) {
  33. let children = shaka.util.XmlUtils.findChildren(elem, name);
  34. if (children.length != 1) {
  35. return null;
  36. }
  37. return children[0];
  38. };
  39. /**
  40. * Finds a namespace-qualified child XML element.
  41. * @param {!Node} elem The parent XML element.
  42. * @param {string} ns The child XML element's namespace URI.
  43. * @param {string} name The child XML element's local name.
  44. * @return {Element} The child XML element, or null if a child XML element does
  45. * not exist with the given tag name OR if there exists more than one
  46. * child XML element with the given tag name.
  47. */
  48. shaka.util.XmlUtils.findChildNS = function(elem, ns, name) {
  49. let children = shaka.util.XmlUtils.findChildrenNS(elem, ns, name);
  50. if (children.length != 1) {
  51. return null;
  52. }
  53. return children[0];
  54. };
  55. /**
  56. * Finds child XML elements.
  57. * @param {!Node} elem The parent XML element.
  58. * @param {string} name The child XML element's tag name.
  59. * @return {!Array.<!Element>} The child XML elements.
  60. */
  61. shaka.util.XmlUtils.findChildren = function(elem, name) {
  62. return Array.prototype.filter.call(elem.childNodes, function(child) {
  63. return child instanceof Element && child.tagName == name;
  64. });
  65. };
  66. /**
  67. * Finds namespace-qualified child XML elements.
  68. * @param {!Node} elem The parent XML element.
  69. * @param {string} ns The child XML element's namespace URI.
  70. * @param {string} name The child XML element's local name.
  71. * @return {!Array.<!Element>} The child XML elements.
  72. */
  73. shaka.util.XmlUtils.findChildrenNS = function(elem, ns, name) {
  74. return Array.prototype.filter.call(elem.childNodes, function(child) {
  75. return child instanceof Element && child.localName == name &&
  76. child.namespaceURI == ns;
  77. });
  78. };
  79. /**
  80. * Gets a namespace-qualified attribute.
  81. * @param {!Element} elem The element to get from.
  82. * @param {string} ns The namespace URI.
  83. * @param {string} name The local name of the attribute.
  84. * @return {?string} The attribute's value, or null if not present.
  85. */
  86. shaka.util.XmlUtils.getAttributeNS = function(elem, ns, name) {
  87. // Some browsers return the empty string when the attribute is missing,
  88. // so check if it exists first. See: https://mzl.la/2L7F0UK
  89. return elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
  90. };
  91. /**
  92. * Gets the text contents of a node.
  93. * @param {!Node} elem The XML element.
  94. * @return {?string} The text contents, or null if there are none.
  95. */
  96. shaka.util.XmlUtils.getContents = function(elem) {
  97. let isText = (child) => {
  98. return child.nodeType == Node.TEXT_NODE ||
  99. child.nodeType == Node.CDATA_SECTION_NODE;
  100. };
  101. if (!Array.prototype.every.call(elem.childNodes, isText)) {
  102. return null;
  103. }
  104. // Read merged text content from all text nodes.
  105. return elem.textContent.trim();
  106. };
  107. /**
  108. * Parses an attribute by its name.
  109. * @param {!Element} elem The XML element.
  110. * @param {string} name The attribute name.
  111. * @param {function(string): (T|null)} parseFunction A function that parses
  112. * the attribute.
  113. * @param {(T|null)=} defaultValue The attribute's default value, if not
  114. * specified, the attibute's default value is null.
  115. * @return {(T|null)} The parsed attribute on success, or the attribute's
  116. * default value if the attribute does not exist or could not be parsed.
  117. * @template T
  118. */
  119. shaka.util.XmlUtils.parseAttr = function(
  120. elem, name, parseFunction, defaultValue = null) {
  121. let parsedValue = null;
  122. let value = elem.getAttribute(name);
  123. if (value != null) {
  124. parsedValue = parseFunction(value);
  125. }
  126. return parsedValue == null ? defaultValue : parsedValue;
  127. };
  128. /**
  129. * Parses an XML date string.
  130. * @param {string} dateString
  131. * @return {?number} The parsed date in seconds on success; otherwise, return
  132. * null.
  133. */
  134. shaka.util.XmlUtils.parseDate = function(dateString) {
  135. if (!dateString) {
  136. return null;
  137. }
  138. // Times in the manifest should be in UTC. If they don't specify a timezone,
  139. // Date.parse() will use the local timezone instead of UTC. So manually add
  140. // the timezone if missing ('Z' indicates the UTC timezone).
  141. // Format: YYYY-MM-DDThh:mm:ss.ssssss
  142. if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) {
  143. dateString += 'Z';
  144. }
  145. let result = Date.parse(dateString);
  146. return (!isNaN(result) ? Math.floor(result / 1000.0) : null);
  147. };
  148. /**
  149. * Parses an XML duration string.
  150. * Negative values are not supported. Years and months are treated as exactly
  151. * 365 and 30 days respectively.
  152. * @param {string} durationString The duration string, e.g., "PT1H3M43.2S",
  153. * which means 1 hour, 3 minutes, and 43.2 seconds.
  154. * @return {?number} The parsed duration in seconds on success; otherwise,
  155. * return null.
  156. * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html}
  157. */
  158. shaka.util.XmlUtils.parseDuration = function(durationString) {
  159. if (!durationString) {
  160. return null;
  161. }
  162. let re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' +
  163. '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$';
  164. let matches = new RegExp(re).exec(durationString);
  165. if (!matches) {
  166. shaka.log.warning('Invalid duration string:', durationString);
  167. return null;
  168. }
  169. // Note: Number(null) == 0 but Number(undefined) == NaN.
  170. let years = Number(matches[1] || null);
  171. let months = Number(matches[2] || null);
  172. let days = Number(matches[3] || null);
  173. let hours = Number(matches[4] || null);
  174. let minutes = Number(matches[5] || null);
  175. let seconds = Number(matches[6] || null);
  176. // Assume a year always has 365 days and a month always has 30 days.
  177. let d = (60 * 60 * 24 * 365) * years +
  178. (60 * 60 * 24 * 30) * months +
  179. (60 * 60 * 24) * days +
  180. (60 * 60) * hours +
  181. 60 * minutes +
  182. seconds;
  183. return isFinite(d) ? d : null;
  184. };
  185. /**
  186. * Parses a range string.
  187. * @param {string} rangeString The range string, e.g., "101-9213".
  188. * @return {?{start: number, end: number}} The parsed range on success;
  189. * otherwise, return null.
  190. */
  191. shaka.util.XmlUtils.parseRange = function(rangeString) {
  192. let matches = /([0-9]+)-([0-9]+)/.exec(rangeString);
  193. if (!matches) {
  194. return null;
  195. }
  196. let start = Number(matches[1]);
  197. if (!isFinite(start)) {
  198. return null;
  199. }
  200. let end = Number(matches[2]);
  201. if (!isFinite(end)) {
  202. return null;
  203. }
  204. return {start: start, end: end};
  205. };
  206. /**
  207. * Parses an integer.
  208. * @param {string} intString The integer string.
  209. * @return {?number} The parsed integer on success; otherwise, return null.
  210. */
  211. shaka.util.XmlUtils.parseInt = function(intString) {
  212. let n = Number(intString);
  213. return (n % 1 === 0) ? n : null;
  214. };
  215. /**
  216. * Parses a positive integer.
  217. * @param {string} intString The integer string.
  218. * @return {?number} The parsed positive integer on success; otherwise,
  219. * return null.
  220. */
  221. shaka.util.XmlUtils.parsePositiveInt = function(intString) {
  222. let n = Number(intString);
  223. return (n % 1 === 0) && (n > 0) ? n : null;
  224. };
  225. /**
  226. * Parses a non-negative integer.
  227. * @param {string} intString The integer string.
  228. * @return {?number} The parsed non-negative integer on success; otherwise,
  229. * return null.
  230. */
  231. shaka.util.XmlUtils.parseNonNegativeInt = function(intString) {
  232. let n = Number(intString);
  233. return (n % 1 === 0) && (n >= 0) ? n : null;
  234. };
  235. /**
  236. * Parses a floating point number.
  237. * @param {string} floatString The floating point number string.
  238. * @return {?number} The parsed floating point number on success; otherwise,
  239. * return null. May return -Infinity or Infinity.
  240. */
  241. shaka.util.XmlUtils.parseFloat = function(floatString) {
  242. let n = Number(floatString);
  243. return !isNaN(n) ? n : null;
  244. };
  245. /**
  246. * Evaluate a division expressed as a string.
  247. * @param {string} exprString
  248. * The expression to evaluate, e.g. "200/2". Can also be a single number.
  249. * @return {?number} The evaluated expression as floating point number on
  250. * success; otherwise return null.
  251. */
  252. shaka.util.XmlUtils.evalDivision = function(exprString) {
  253. let res;
  254. let n;
  255. if ((res = exprString.match(/^(\d+)\/(\d+)$/))) {
  256. n = Number(res[1] / res[2]);
  257. } else {
  258. n = Number(exprString);
  259. }
  260. return !isNaN(n) ? n : null;
  261. };
  262. /**
  263. * Parse a string and return the resulting root element if
  264. * it was valid XML.
  265. * @param {string} xmlString
  266. * @param {string} expectedRootElemName
  267. * @return {Element|undefined}
  268. */
  269. shaka.util.XmlUtils.parseXmlString = function(xmlString, expectedRootElemName) {
  270. const parser = new DOMParser();
  271. let rootElem;
  272. let xml;
  273. try {
  274. xml = parser.parseFromString(xmlString, 'text/xml');
  275. } catch (exception) {}
  276. if (xml) {
  277. // The top-level element in the loaded xml should have the
  278. // same type as the element linking.
  279. if (xml.documentElement.tagName == expectedRootElemName) {
  280. rootElem = xml.documentElement;
  281. }
  282. }
  283. if (rootElem && rootElem.getElementsByTagName('parsererror').length > 0) {
  284. return null;
  285. } // It had a parser error in it.
  286. return rootElem;
  287. };
  288. /**
  289. * Parse some UTF8 data and return the resulting root element if
  290. * it was valid XML.
  291. * @param {ArrayBuffer} data
  292. * @param {string} expectedRootElemName
  293. * @return {Element|undefined}
  294. */
  295. shaka.util.XmlUtils.parseXml = function(data, expectedRootElemName) {
  296. try {
  297. const string = shaka.util.StringUtils.fromUTF8(data);
  298. return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName);
  299. } catch (exception) {}
  300. };