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. /**
  20. * @namespace shaka.util.XmlUtils
  21. * @summary A set of XML utility functions.
  22. */
  23. /**
  24. * Finds a child XML element.
  25. * @param {!Node} elem The parent XML element.
  26. * @param {string} name The child XML element's tag name.
  27. * @return {Element} The child XML element, or null if a child XML element does
  28. * not exist with the given tag name OR if there exists more than one
  29. * child XML element with the given tag name.
  30. */
  31. shaka.util.XmlUtils.findChild = function(elem, name) {
  32. let children = shaka.util.XmlUtils.findChildren(elem, name);
  33. if (children.length != 1) {
  34. return null;
  35. }
  36. return children[0];
  37. };
  38. /**
  39. * Finds child XML elements.
  40. * @param {!Node} elem The parent XML element.
  41. * @param {string} name The child XML element's tag name.
  42. * @return {!Array.<!Element>} The child XML elements.
  43. */
  44. shaka.util.XmlUtils.findChildren = function(elem, name) {
  45. return Array.prototype.filter.call(elem.childNodes, function(child) {
  46. return child instanceof Element && child.tagName == name;
  47. });
  48. };
  49. /**
  50. * Finds namespace-qualified child XML elements.
  51. * @param {!Node} elem The parent XML element.
  52. * @param {string} ns The child XML element's namespace URI.
  53. * @param {string} name The child XML element's local name.
  54. * @return {!Array.<!Element>} The child XML elements.
  55. */
  56. shaka.util.XmlUtils.findChildrenNS = function(elem, ns, name) {
  57. return Array.prototype.filter.call(elem.childNodes, function(child) {
  58. return child instanceof Element && child.localName == name &&
  59. child.namespaceURI == ns;
  60. });
  61. };
  62. /**
  63. * Gets a namespace-qualified attribute.
  64. * @param {!Element} elem The element to get from.
  65. * @param {string} ns The namespace URI.
  66. * @param {string} name The local name of the attribute.
  67. * @return {?string} The attribute's value, or null if not present.
  68. */
  69. shaka.util.XmlUtils.getAttributeNS = function(elem, ns, name) {
  70. // Some browsers return the empty string when the attribute is missing,
  71. // so check if it exists first. See: https://mzl.la/2L7F0UK
  72. return elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null;
  73. };
  74. /**
  75. * Gets the text contents of a node.
  76. * @param {!Node} elem The XML element.
  77. * @return {?string} The text contents, or null if there are none.
  78. */
  79. shaka.util.XmlUtils.getContents = function(elem) {
  80. let isText = (child) => {
  81. return child.nodeType == Node.TEXT_NODE ||
  82. child.nodeType == Node.CDATA_SECTION_NODE;
  83. };
  84. if (!Array.prototype.every.call(elem.childNodes, isText)) {
  85. return null;
  86. }
  87. // Read merged text content from all text nodes.
  88. return elem.textContent.trim();
  89. };
  90. /**
  91. * Parses an attribute by its name.
  92. * @param {!Element} elem The XML element.
  93. * @param {string} name The attribute name.
  94. * @param {function(string): (T|null)} parseFunction A function that parses
  95. * the attribute.
  96. * @param {(T|null)=} opt_defaultValue The attribute's default value, if not
  97. * specified, the attibute's default value is null.
  98. * @return {(T|null)} The parsed attribute on success, or the attribute's
  99. * default value if the attribute does not exist or could not be parsed.
  100. * @template T
  101. */
  102. shaka.util.XmlUtils.parseAttr = function(
  103. elem, name, parseFunction, opt_defaultValue) {
  104. let parsedValue = null;
  105. let value = elem.getAttribute(name);
  106. if (value != null) {
  107. parsedValue = parseFunction(value);
  108. }
  109. if (parsedValue == null) {
  110. return opt_defaultValue != undefined ? opt_defaultValue : null;
  111. }
  112. return parsedValue;
  113. };
  114. /**
  115. * Parses an XML date string.
  116. * @param {string} dateString
  117. * @return {?number} The parsed date in seconds on success; otherwise, return
  118. * null.
  119. */
  120. shaka.util.XmlUtils.parseDate = function(dateString) {
  121. if (!dateString) {
  122. return null;
  123. }
  124. // Times in the manifest should be in UTC. If they don't specify a timezone,
  125. // Date.parse() will use the local timezone instead of UTC. So manually add
  126. // the timezone if missing ('Z' indicates the UTC timezone).
  127. // Format: YYYY-MM-DDThh:mm:ss.ssssss
  128. if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) {
  129. dateString += 'Z';
  130. }
  131. let result = Date.parse(dateString);
  132. return (!isNaN(result) ? Math.floor(result / 1000.0) : null);
  133. };
  134. /**
  135. * Parses an XML duration string.
  136. * Negative values are not supported. Years and months are treated as exactly
  137. * 365 and 30 days respectively.
  138. * @param {string} durationString The duration string, e.g., "PT1H3M43.2S",
  139. * which means 1 hour, 3 minutes, and 43.2 seconds.
  140. * @return {?number} The parsed duration in seconds on success; otherwise,
  141. * return null.
  142. * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html}
  143. */
  144. shaka.util.XmlUtils.parseDuration = function(durationString) {
  145. if (!durationString) {
  146. return null;
  147. }
  148. let re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' +
  149. '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$';
  150. let matches = new RegExp(re).exec(durationString);
  151. if (!matches) {
  152. shaka.log.warning('Invalid duration string:', durationString);
  153. return null;
  154. }
  155. // Note: Number(null) == 0 but Number(undefined) == NaN.
  156. let years = Number(matches[1] || null);
  157. let months = Number(matches[2] || null);
  158. let days = Number(matches[3] || null);
  159. let hours = Number(matches[4] || null);
  160. let minutes = Number(matches[5] || null);
  161. let seconds = Number(matches[6] || null);
  162. // Assume a year always has 365 days and a month always has 30 days.
  163. let d = (60 * 60 * 24 * 365) * years +
  164. (60 * 60 * 24 * 30) * months +
  165. (60 * 60 * 24) * days +
  166. (60 * 60) * hours +
  167. 60 * minutes +
  168. seconds;
  169. return isFinite(d) ? d : null;
  170. };
  171. /**
  172. * Parses a range string.
  173. * @param {string} rangeString The range string, e.g., "101-9213".
  174. * @return {?{start: number, end: number}} The parsed range on success;
  175. * otherwise, return null.
  176. */
  177. shaka.util.XmlUtils.parseRange = function(rangeString) {
  178. let matches = /([0-9]+)-([0-9]+)/.exec(rangeString);
  179. if (!matches) {
  180. return null;
  181. }
  182. let start = Number(matches[1]);
  183. if (!isFinite(start)) {
  184. return null;
  185. }
  186. let end = Number(matches[2]);
  187. if (!isFinite(end)) {
  188. return null;
  189. }
  190. return {start: start, end: end};
  191. };
  192. /**
  193. * Parses an integer.
  194. * @param {string} intString The integer string.
  195. * @return {?number} The parsed integer on success; otherwise, return null.
  196. */
  197. shaka.util.XmlUtils.parseInt = function(intString) {
  198. let n = Number(intString);
  199. return (n % 1 === 0) ? n : null;
  200. };
  201. /**
  202. * Parses a positive integer.
  203. * @param {string} intString The integer string.
  204. * @return {?number} The parsed positive integer on success; otherwise,
  205. * return null.
  206. */
  207. shaka.util.XmlUtils.parsePositiveInt = function(intString) {
  208. let n = Number(intString);
  209. return (n % 1 === 0) && (n > 0) ? n : null;
  210. };
  211. /**
  212. * Parses a non-negative integer.
  213. * @param {string} intString The integer string.
  214. * @return {?number} The parsed non-negative integer on success; otherwise,
  215. * return null.
  216. */
  217. shaka.util.XmlUtils.parseNonNegativeInt = function(intString) {
  218. let n = Number(intString);
  219. return (n % 1 === 0) && (n >= 0) ? n : null;
  220. };
  221. /**
  222. * Parses a floating point number.
  223. * @param {string} floatString The floating point number string.
  224. * @return {?number} The parsed floating point number on success; otherwise,
  225. * return null. May return -Infinity or Infinity.
  226. */
  227. shaka.util.XmlUtils.parseFloat = function(floatString) {
  228. let n = Number(floatString);
  229. return !isNaN(n) ? n : null;
  230. };
  231. /**
  232. * Evaluate a division expressed as a string.
  233. * @param {string} exprString
  234. * The expression to evaluate, e.g. "200/2". Can also be a single number.
  235. * @return {?number} The evaluated expression as floating point number on
  236. * success; otherwise return null.
  237. */
  238. shaka.util.XmlUtils.evalDivision = function(exprString) {
  239. let res;
  240. let n;
  241. if ((res = exprString.match(/^(\d+)\/(\d+)$/))) {
  242. n = Number(res[1] / res[2]);
  243. } else {
  244. n = Number(exprString);
  245. }
  246. return !isNaN(n) ? n : null;
  247. };