Source: lib/util/tXml.js

  1. /*! @license
  2. * tXml
  3. * Copyright 2015 Tobias Nickel
  4. * SPDX-License-Identifier: MIT
  5. */
  6. goog.provide('shaka.util.TXml');
  7. goog.require('shaka.util.ObjectUtils');
  8. goog.require('shaka.util.StringUtils');
  9. goog.require('shaka.log');
  10. /**
  11. * This code is a modified version of the tXml library.
  12. *
  13. * @author: Tobias Nickel
  14. * created: 06.04.2015
  15. * https://github.com/TobiasNickel/tXml
  16. */
  17. /**
  18. * Permission is hereby granted, free of charge, to any person obtaining a copy
  19. * of this software and associated documentation files (the "Software"), to deal
  20. * in the Software without restriction, including without limitation the rights
  21. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  22. * copies of the Software, and to permit persons to whom the Software is
  23. * furnished to do so, subject to the following conditions:
  24. *
  25. * The above copyright notice and this permission notice shall be included in
  26. * all copies or substantial portions of the Software.
  27. *
  28. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  29. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  30. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  31. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  32. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  33. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  34. * SOFTWARE.
  35. */
  36. shaka.util.TXml = class {
  37. /**
  38. * Parse some data
  39. * @param {BufferSource} data
  40. * @param {string=} expectedRootElemName
  41. * @param {boolean=} includeParent
  42. * @return {shaka.extern.xml.Node | null}
  43. */
  44. static parseXml(data, expectedRootElemName, includeParent = false) {
  45. const xmlString = shaka.util.StringUtils.fromBytesAutoDetect(data);
  46. return shaka.util.TXml.parseXmlString(
  47. xmlString, expectedRootElemName, includeParent);
  48. }
  49. /**
  50. * Parse some data
  51. * @param {string} xmlString
  52. * @param {string=} expectedRootElemName
  53. * @return {shaka.extern.xml.Node | null}
  54. */
  55. static parseXmlString(xmlString, expectedRootElemName,
  56. includeParent = false) {
  57. const result = shaka.util.TXml.parse(xmlString, includeParent);
  58. if (!expectedRootElemName && result.length) {
  59. return result[0];
  60. }
  61. const rootNode = result.find((n) =>
  62. expectedRootElemName.split(',').includes(n.tagName));
  63. if (rootNode) {
  64. return rootNode;
  65. }
  66. shaka.log.error('parseXml root element not found!');
  67. return null;
  68. }
  69. /**
  70. * Get namespace based on schema
  71. * @param {string} schema
  72. * @return {string}
  73. */
  74. static getKnownNameSpace(schema) {
  75. if (shaka.util.TXml.uriToNameSpace_.has(schema)) {
  76. return shaka.util.TXml.uriToNameSpace_.get(schema);
  77. }
  78. return '';
  79. }
  80. /**
  81. * Get schema based on namespace
  82. * @param {string} NS
  83. * @return {string}
  84. */
  85. static getKnownSchema(NS) {
  86. if (shaka.util.TXml.nameSpaceToUri_.has(NS)) {
  87. return shaka.util.TXml.nameSpaceToUri_.get(NS);
  88. }
  89. return '';
  90. }
  91. /**
  92. * Sets NS <-> schema bidirectional mapping
  93. * @param {string} schema
  94. * @param {string} NS
  95. */
  96. static setKnownNameSpace(schema, NS) {
  97. shaka.util.TXml.uriToNameSpace_.set(schema, NS);
  98. shaka.util.TXml.nameSpaceToUri_.set(NS, schema);
  99. }
  100. /**
  101. * parseXML / html into a DOM Object,
  102. * with no validation and some failure tolerance
  103. * @param {string} S your XML to parse
  104. * @param {boolean} includeParent
  105. * @return {Array.<shaka.extern.xml.Node>}
  106. */
  107. static parse(S, includeParent) {
  108. let pos = 0;
  109. const openBracket = '<';
  110. const openBracketCC = '<'.charCodeAt(0);
  111. const closeBracket = '>';
  112. const closeBracketCC = '>'.charCodeAt(0);
  113. const minusCC = '-'.charCodeAt(0);
  114. const slashCC = '/'.charCodeAt(0);
  115. const exclamationCC = '!'.charCodeAt(0);
  116. const singleQuoteCC = '\''.charCodeAt(0);
  117. const doubleQuoteCC = '"'.charCodeAt(0);
  118. const openCornerBracketCC = '['.charCodeAt(0);
  119. /**
  120. * parsing a list of entries
  121. */
  122. function parseChildren(tagName, preserveSpace = false) {
  123. /** @type {Array.<shaka.extern.xml.Node | string>} */
  124. const children = [];
  125. while (S[pos]) {
  126. if (S.charCodeAt(pos) == openBracketCC) {
  127. if (S.charCodeAt(pos + 1) === slashCC) {
  128. const closeStart = pos + 2;
  129. pos = S.indexOf(closeBracket, pos);
  130. const closeTag = S.substring(closeStart, pos);
  131. let indexOfCloseTag = closeTag.indexOf(tagName);
  132. if (indexOfCloseTag == -1) {
  133. // handle VTT closing tags like <c.lime></c>
  134. const indexOfPeriod = tagName.indexOf('.');
  135. if (indexOfPeriod > 0) {
  136. const shortTag = tagName.substring(0, indexOfPeriod);
  137. indexOfCloseTag = closeTag.indexOf(shortTag);
  138. }
  139. }
  140. // eslint-disable-next-line no-restricted-syntax
  141. if (indexOfCloseTag == -1) {
  142. const parsedText = S.substring(0, pos).split('\n');
  143. throw new Error(
  144. 'Unexpected close tag\nLine: ' + (parsedText.length - 1) +
  145. '\nColumn: ' +
  146. (parsedText[parsedText.length - 1].length + 1) +
  147. '\nChar: ' + S[pos],
  148. );
  149. }
  150. if (pos + 1) {
  151. pos += 1;
  152. }
  153. return children;
  154. } else if (S.charCodeAt(pos + 1) === exclamationCC) {
  155. if (S.charCodeAt(pos + 2) == minusCC) {
  156. while (pos !== -1 && !(S.charCodeAt(pos) === closeBracketCC &&
  157. S.charCodeAt(pos - 1) == minusCC &&
  158. S.charCodeAt(pos - 2) == minusCC &&
  159. pos != -1)) {
  160. pos = S.indexOf(closeBracket, pos + 1);
  161. }
  162. if (pos === -1) {
  163. pos = S.length;
  164. }
  165. } else if (
  166. S.charCodeAt(pos + 2) === openCornerBracketCC &&
  167. S.charCodeAt(pos + 8) === openCornerBracketCC &&
  168. S.substr(pos + 3, 5).toLowerCase() === 'cdata'
  169. ) {
  170. // cdata
  171. const cdataEndIndex = S.indexOf(']]>', pos);
  172. if (cdataEndIndex == -1) {
  173. children.push(S.substr(pos + 9));
  174. pos = S.length;
  175. } else {
  176. children.push(S.substring(pos + 9, cdataEndIndex));
  177. pos = cdataEndIndex + 3;
  178. }
  179. continue;
  180. }
  181. pos++;
  182. continue;
  183. }
  184. const node = parseNode(preserveSpace);
  185. children.push(node);
  186. if (typeof node === 'string') {
  187. return children;
  188. }
  189. if (node.tagName[0] === '?' && node.children) {
  190. children.push(...node.children);
  191. node.children = [];
  192. }
  193. } else {
  194. const text = parseText();
  195. if (preserveSpace) {
  196. if (text.length > 0) {
  197. children.push(text);
  198. }
  199. } else if (children.length &&
  200. text.length == 1 && text[0] == '\n') {
  201. children.push(text);
  202. } else {
  203. const trimmed = text.trim();
  204. if (trimmed.length > 0) {
  205. children.push(text);
  206. }
  207. }
  208. pos++;
  209. }
  210. }
  211. return children;
  212. }
  213. /**
  214. * returns the text outside of texts until the first '<'
  215. */
  216. function parseText() {
  217. const start = pos;
  218. pos = S.indexOf(openBracket, pos) - 1;
  219. if (pos === -2) {
  220. pos = S.length;
  221. }
  222. return S.slice(start, pos + 1);
  223. }
  224. /**
  225. * returns text until the first nonAlphabetic letter
  226. */
  227. const nameSpacer = '\r\n\t>/= ';
  228. /**
  229. * Parse text in current context
  230. * @return {string}
  231. */
  232. function parseName() {
  233. const start = pos;
  234. while (nameSpacer.indexOf(S[pos]) === -1 && S[pos]) {
  235. pos++;
  236. }
  237. return S.slice(start, pos);
  238. }
  239. /**
  240. * Parse text in current context
  241. * @param {boolean} preserveSpace Preserve the space between nodes
  242. * @return {shaka.extern.xml.Node | string}
  243. */
  244. function parseNode(preserveSpace) {
  245. pos++;
  246. const tagName = parseName();
  247. const attributes = {};
  248. let children = [];
  249. // parsing attributes
  250. while (S.charCodeAt(pos) !== closeBracketCC && S[pos]) {
  251. const c = S.charCodeAt(pos);
  252. // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
  253. if ((c > 64 && c < 91) || (c > 96 && c < 123)) {
  254. const name = parseName();
  255. // search beginning of the string
  256. let code = S.charCodeAt(pos);
  257. while (code && code !== singleQuoteCC && code !== doubleQuoteCC &&
  258. !((code > 64 && code < 91) || (code > 96 && code < 123)) &&
  259. code !== closeBracketCC) {
  260. pos++;
  261. code = S.charCodeAt(pos);
  262. }
  263. let value = parseString();
  264. if (code === singleQuoteCC || code === doubleQuoteCC) {
  265. if (pos === -1) {
  266. /** @type {shaka.extern.xml.Node} */
  267. const node = {
  268. tagName,
  269. attributes,
  270. children,
  271. parent: null,
  272. };
  273. if (includeParent) {
  274. for (let i = 0; i < children.length; i++) {
  275. if (typeof children[i] !== 'string') {
  276. children[i].parent = node;
  277. }
  278. }
  279. }
  280. return node;
  281. }
  282. } else {
  283. value = null;
  284. pos--;
  285. }
  286. if (name.startsWith('xmlns:')) {
  287. const segs = name.split(':');
  288. shaka.util.TXml.setKnownNameSpace(
  289. /** @type {string} */ (value), segs[1]);
  290. }
  291. if (tagName === 'tt' &&
  292. name === 'xml:space' &&
  293. value === 'preserve') {
  294. preserveSpace = true;
  295. }
  296. attributes[name] = value;
  297. }
  298. pos++;
  299. }
  300. if (S.charCodeAt(pos - 1) !== slashCC) {
  301. pos++;
  302. const contents = parseChildren(tagName, preserveSpace);
  303. children = contents;
  304. } else {
  305. pos++;
  306. }
  307. /** @type {shaka.extern.xml.Node} */
  308. const node = {
  309. tagName,
  310. attributes,
  311. children,
  312. parent: null,
  313. };
  314. const childrenLength = children.length;
  315. for (let i = 0; i < childrenLength; i++) {
  316. const childrenValue = children[i];
  317. if (typeof childrenValue !== 'string') {
  318. if (includeParent) {
  319. childrenValue.parent = node;
  320. }
  321. } else if (i == childrenLength - 1 && childrenValue == '\n') {
  322. children.pop();
  323. }
  324. }
  325. return node;
  326. }
  327. /**
  328. * Parse string in current context
  329. * @return {string}
  330. */
  331. function parseString() {
  332. const startChar = S[pos];
  333. const startpos = pos + 1;
  334. pos = S.indexOf(startChar, startpos);
  335. return S.slice(startpos, pos);
  336. }
  337. return parseChildren('');
  338. }
  339. /**
  340. * Verifies if the element is a TXml node.
  341. * @param {!shaka.extern.xml.Node} elem The XML element.
  342. * @return {!boolean} Is the element a TXml node
  343. */
  344. static isNode(elem) {
  345. return !!(elem.tagName);
  346. }
  347. /**
  348. * Checks if a node is of type text.
  349. * @param {!shaka.extern.xml.Node | string} elem The XML element.
  350. * @return {boolean} True if it is a text node.
  351. */
  352. static isText(elem) {
  353. return typeof elem === 'string';
  354. }
  355. /**
  356. * gets child XML elements.
  357. * @param {!shaka.extern.xml.Node} elem The parent XML element.
  358. * @return {!Array.<!shaka.extern.xml.Node>} The child XML elements.
  359. */
  360. static getChildNodes(elem) {
  361. const found = [];
  362. if (!elem.children) {
  363. return [];
  364. }
  365. for (const child of elem.children) {
  366. if (typeof child !== 'string') {
  367. found.push(child);
  368. }
  369. }
  370. return found;
  371. }
  372. /**
  373. * Finds child XML elements.
  374. * @param {!shaka.extern.xml.Node} elem The parent XML element.
  375. * @param {string} name The child XML element's tag name.
  376. * @return {!Array.<!shaka.extern.xml.Node>} The child XML elements.
  377. */
  378. static findChildren(elem, name) {
  379. const found = [];
  380. if (!elem.children) {
  381. return [];
  382. }
  383. for (const child of elem.children) {
  384. if (child.tagName === name) {
  385. found.push(child);
  386. }
  387. }
  388. return found;
  389. }
  390. /**
  391. * Gets inner text.
  392. * @param {!shaka.extern.xml.Node | string} node The XML element.
  393. * @return {?string} The text contents, or null if there are none.
  394. */
  395. static getTextContents(node) {
  396. const StringUtils = shaka.util.StringUtils;
  397. if (typeof node === 'string') {
  398. return StringUtils.htmlUnescape(node);
  399. }
  400. const textContent = node.children.reduce(
  401. (acc, curr) => (typeof curr === 'string' ? acc + curr : acc),
  402. '',
  403. );
  404. if (textContent === '') {
  405. return null;
  406. }
  407. return StringUtils.htmlUnescape(textContent);
  408. }
  409. /**
  410. * Gets the text contents of a node.
  411. * @param {!shaka.extern.xml.Node} node The XML element.
  412. * @return {?string} The text contents, or null if there are none.
  413. */
  414. static getContents(node) {
  415. if (!Array.from(node.children).every(
  416. (n) => typeof n === 'string' )) {
  417. return null;
  418. }
  419. // Read merged text content from all text nodes.
  420. let text = shaka.util.TXml.getTextContents(node);
  421. if (text) {
  422. text = text.trim();
  423. }
  424. return text;
  425. }
  426. /**
  427. * Finds child XML elements recursively.
  428. * @param {!shaka.extern.xml.Node} elem The parent XML element.
  429. * @param {string} name The child XML element's tag name.
  430. * @param {!Array.<!shaka.extern.xml.Node>} found accumulator for found nodes
  431. * @return {!Array.<!shaka.extern.xml.Node>} The child XML elements.
  432. */
  433. static getElementsByTagName(elem, name, found = []) {
  434. if (elem.tagName === name) {
  435. found.push(elem);
  436. }
  437. if (elem.children) {
  438. for (const child of elem.children) {
  439. shaka.util.TXml.getElementsByTagName(child, name, found);
  440. }
  441. }
  442. return found;
  443. }
  444. /**
  445. * Finds a child XML element.
  446. * @param {!shaka.extern.xml.Node} elem The parent XML element.
  447. * @param {string} name The child XML element's tag name.
  448. * @return {shaka.extern.xml.Node | null} The child XML element,
  449. * or null if a child XML element
  450. * does not exist with the given tag name OR if there exists more than one
  451. * child XML element with the given tag name.
  452. */
  453. static findChild(elem, name) {
  454. const children = shaka.util.TXml.findChildren(elem, name);
  455. if (children.length != 1) {
  456. return null;
  457. }
  458. return children[0];
  459. }
  460. /**
  461. * Finds a namespace-qualified child XML element.
  462. * @param {!shaka.extern.xml.Node} elem The parent XML element.
  463. * @param {string} ns The child XML element's namespace URI.
  464. * @param {string} name The child XML element's local name.
  465. * @return {shaka.extern.xml.Node | null} The child XML element, or null
  466. * if a child XML element
  467. * does not exist with the given tag name OR if there exists more than one
  468. * child XML element with the given tag name.
  469. */
  470. static findChildNS(elem, ns, name) {
  471. const children = shaka.util.TXml.findChildrenNS(elem, ns, name);
  472. if (children.length != 1) {
  473. return null;
  474. }
  475. return children[0];
  476. }
  477. /**
  478. * Parses an attribute by its name.
  479. * @param {!shaka.extern.xml.Node} elem The XML element.
  480. * @param {string} name The attribute name.
  481. * @param {function(string): (T|null)} parseFunction A function that parses
  482. * the attribute.
  483. * @param {(T|null)=} defaultValue The attribute's default value, if not
  484. * specified, the attibute's default value is null.
  485. * @return {(T|null)} The parsed attribute on success, or the attribute's
  486. * default value if the attribute does not exist or could not be parsed.
  487. * @template T
  488. */
  489. static parseAttr(elem, name, parseFunction, defaultValue = null) {
  490. let parsedValue = null;
  491. const value = elem.attributes[name];
  492. if (value != null) {
  493. parsedValue = parseFunction(value);
  494. }
  495. return parsedValue == null ? defaultValue : parsedValue;
  496. }
  497. /**
  498. * Gets a namespace-qualified attribute.
  499. * @param {!shaka.extern.xml.Node} elem The element to get from.
  500. * @param {string} ns The namespace URI.
  501. * @param {string} name The local name of the attribute.
  502. * @return {?string} The attribute's value, or null if not present.
  503. */
  504. static getAttributeNS(elem, ns, name) {
  505. const schemaNS = shaka.util.TXml.getKnownNameSpace(ns);
  506. // Think this is equivalent
  507. const attribute = elem.attributes[`${schemaNS}:${name}`];
  508. return attribute || null;
  509. }
  510. /**
  511. * Finds namespace-qualified child XML elements.
  512. * @param {!shaka.extern.xml.Node} elem The parent XML element.
  513. * @param {string} ns The child XML element's namespace URI.
  514. * @param {string} name The child XML element's local name.
  515. * @return {!Array.<!shaka.extern.xml.Node>} The child XML elements.
  516. */
  517. static findChildrenNS(elem, ns, name) {
  518. const schemaNS = shaka.util.TXml.getKnownNameSpace(ns);
  519. const found = [];
  520. if (elem.children) {
  521. for (const child of elem.children) {
  522. if (child && child.tagName === `${schemaNS}:${name}`) {
  523. found.push(child);
  524. }
  525. }
  526. }
  527. return found;
  528. }
  529. /**
  530. * Gets a namespace-qualified attribute.
  531. * @param {!shaka.extern.xml.Node} elem The element to get from.
  532. * @param {!Array.<string>} nsList The lis of namespace URIs.
  533. * @param {string} name The local name of the attribute.
  534. * @return {?string} The attribute's value, or null if not present.
  535. */
  536. static getAttributeNSList(elem, nsList, name) {
  537. for (const ns of nsList) {
  538. const attr = shaka.util.TXml.getAttributeNS(
  539. elem, ns, name,
  540. );
  541. if (attr) {
  542. return attr;
  543. }
  544. }
  545. return null;
  546. }
  547. /**
  548. * Parses an XML date string.
  549. * @param {string} dateString
  550. * @return {?number} The parsed date in seconds on success; otherwise, return
  551. * null.
  552. */
  553. static parseDate(dateString) {
  554. if (!dateString) {
  555. return null;
  556. }
  557. // Times in the manifest should be in UTC. If they don't specify a timezone,
  558. // Date.parse() will use the local timezone instead of UTC. So manually add
  559. // the timezone if missing ('Z' indicates the UTC timezone).
  560. // Format: YYYY-MM-DDThh:mm:ss.ssssss
  561. if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) {
  562. dateString += 'Z';
  563. }
  564. const result = Date.parse(dateString);
  565. return isNaN(result) ? null : (result / 1000.0);
  566. }
  567. /**
  568. * Parses an XML duration string.
  569. * Negative values are not supported. Years and months are treated as exactly
  570. * 365 and 30 days respectively.
  571. * @param {string} durationString The duration string, e.g., "PT1H3M43.2S",
  572. * which means 1 hour, 3 minutes, and 43.2 seconds.
  573. * @return {?number} The parsed duration in seconds on success; otherwise,
  574. * return null.
  575. * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html}
  576. */
  577. static parseDuration(durationString) {
  578. if (!durationString) {
  579. return null;
  580. }
  581. const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' +
  582. '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$';
  583. const matches = new RegExp(re).exec(durationString);
  584. if (!matches) {
  585. shaka.log.warning('Invalid duration string:', durationString);
  586. return null;
  587. }
  588. // Note: Number(null) == 0 but Number(undefined) == NaN.
  589. const years = Number(matches[1] || null);
  590. const months = Number(matches[2] || null);
  591. const days = Number(matches[3] || null);
  592. const hours = Number(matches[4] || null);
  593. const minutes = Number(matches[5] || null);
  594. const seconds = Number(matches[6] || null);
  595. // Assume a year always has 365 days and a month always has 30 days.
  596. const d = (60 * 60 * 24 * 365) * years +
  597. (60 * 60 * 24 * 30) * months +
  598. (60 * 60 * 24) * days +
  599. (60 * 60) * hours +
  600. 60 * minutes +
  601. seconds;
  602. return isFinite(d) ? d : null;
  603. }
  604. /**
  605. * Parses a range string.
  606. * @param {string} rangeString The range string, e.g., "101-9213".
  607. * @return {?{start: number, end: number}} The parsed range on success;
  608. * otherwise, return null.
  609. */
  610. static parseRange(rangeString) {
  611. const matches = /([0-9]+)-([0-9]+)/.exec(rangeString);
  612. if (!matches) {
  613. return null;
  614. }
  615. const start = Number(matches[1]);
  616. if (!isFinite(start)) {
  617. return null;
  618. }
  619. const end = Number(matches[2]);
  620. if (!isFinite(end)) {
  621. return null;
  622. }
  623. return {start: start, end: end};
  624. }
  625. /**
  626. * Parses an integer.
  627. * @param {string} intString The integer string.
  628. * @return {?number} The parsed integer on success; otherwise, return null.
  629. */
  630. static parseInt(intString) {
  631. const n = Number(intString);
  632. return (n % 1 === 0) ? n : null;
  633. }
  634. /**
  635. * Parses a positive integer.
  636. * @param {string} intString The integer string.
  637. * @return {?number} The parsed positive integer on success; otherwise,
  638. * return null.
  639. */
  640. static parsePositiveInt(intString) {
  641. const n = Number(intString);
  642. return (n % 1 === 0) && (n > 0) ? n : null;
  643. }
  644. /**
  645. * Parses a non-negative integer.
  646. * @param {string} intString The integer string.
  647. * @return {?number} The parsed non-negative integer on success; otherwise,
  648. * return null.
  649. */
  650. static parseNonNegativeInt(intString) {
  651. const n = Number(intString);
  652. return (n % 1 === 0) && (n >= 0) ? n : null;
  653. }
  654. /**
  655. * Parses a floating point number.
  656. * @param {string} floatString The floating point number string.
  657. * @return {?number} The parsed floating point number on success; otherwise,
  658. * return null. May return -Infinity or Infinity.
  659. */
  660. static parseFloat(floatString) {
  661. const n = Number(floatString);
  662. return !isNaN(n) ? n : null;
  663. }
  664. /**
  665. * Parses a boolean.
  666. * @param {string} booleanString The boolean string.
  667. * @return {boolean} The boolean
  668. */
  669. static parseBoolean(booleanString) {
  670. if (!booleanString) {
  671. return false;
  672. }
  673. return booleanString.toLowerCase() === 'true';
  674. }
  675. /**
  676. * Evaluate a division expressed as a string.
  677. * @param {string} exprString
  678. * The expression to evaluate, e.g. "200/2". Can also be a single number.
  679. * @return {?number} The evaluated expression as floating point number on
  680. * success; otherwise return null.
  681. */
  682. static evalDivision(exprString) {
  683. let res;
  684. let n;
  685. if ((res = exprString.match(/^(\d+)\/(\d+)$/))) {
  686. n = Number(res[1]) / Number(res[2]);
  687. } else {
  688. n = Number(exprString);
  689. }
  690. return !isNaN(n) ? n : null;
  691. }
  692. /**
  693. * Parse xPath strings for segments and id targets.
  694. * @param {string} exprString
  695. * @return {!Array<!shaka.util.TXml.PathNode>}
  696. */
  697. static parseXpath(exprString) {
  698. const StringUtils = shaka.util.StringUtils;
  699. const returnPaths = [];
  700. // Split string by paths but ignore '/' in quotes
  701. const paths = StringUtils.htmlUnescape(exprString)
  702. .split(/\/+(?=(?:[^'"]*['"][^'"]*['"])*[^'"]*$)/);
  703. for (const path of paths) {
  704. const nodeName = path.match(/^([\w]+)/);
  705. if (nodeName) {
  706. // We only want the id attribute in which case
  707. // /'(.*?)'/ will suffice to get it.
  708. const idAttr = path.match(/(@id='(.*?)')/);
  709. const tAttr = path.match(/(@t='(\d+)')/);
  710. const numberIndex = path.match(/(@n='(\d+)')/);
  711. const position = path.match(/\[(\d+)\]/);
  712. returnPaths.push({
  713. name: nodeName[0],
  714. id: idAttr ?
  715. idAttr[0].match(/'(.*?)'/)[0].replace(/'/gm, '') : null,
  716. t: tAttr ?
  717. Number(tAttr[0].match(/'(.*?)'/)[0].replace(/'/gm, '')) : null,
  718. n: numberIndex ?
  719. Number(numberIndex[0].match(/'(.*?)'/)[0].replace(/'/gm, '')):
  720. null,
  721. // position is counted from 1, so make it readable for devs
  722. position: position ? Number(position[1]) - 1 : null,
  723. attribute: path.split('/@')[1] || null,
  724. });
  725. } else if (path.startsWith('@') && returnPaths.length) {
  726. returnPaths[returnPaths.length - 1].attribute = path.slice(1);
  727. }
  728. }
  729. return returnPaths;
  730. }
  731. /**
  732. * Modifies nodes in specified array by adding or removing nodes
  733. * and updating attributes.
  734. * @param {!Array<shaka.extern.xml.Node>} nodes
  735. * @param {!shaka.extern.xml.Node} patchNode
  736. */
  737. static modifyNodes(nodes, patchNode) {
  738. const TXml = shaka.util.TXml;
  739. const paths = TXml.parseXpath(patchNode.attributes['sel'] || '');
  740. if (!paths.length) {
  741. return;
  742. }
  743. const lastNode = paths[paths.length - 1];
  744. const position = patchNode.attributes['pos'] || null;
  745. let index = lastNode.position;
  746. if (index == null) {
  747. if (lastNode.t !== null) {
  748. index = TXml.nodePositionByAttribute_(nodes, 't', lastNode.t);
  749. }
  750. if (lastNode.n !== null) {
  751. index = TXml.nodePositionByAttribute_(nodes, 'n', lastNode.n);
  752. }
  753. }
  754. if (index === null) {
  755. index = position === 'prepend' ? 0 : nodes.length;
  756. } else if (position === 'prepend') {
  757. --index;
  758. } else if (position === 'after') {
  759. ++index;
  760. }
  761. const action = patchNode.tagName;
  762. const attribute = lastNode.attribute;
  763. // Modify attribute
  764. if (attribute && nodes[index]) {
  765. TXml.modifyNodeAttribute(nodes[index], action, attribute,
  766. TXml.getContents(patchNode) || '');
  767. // Rearrange nodes
  768. } else {
  769. if (action === 'remove' || action === 'replace') {
  770. nodes.splice(index, 1);
  771. }
  772. if (action === 'add' || action === 'replace') {
  773. const newNodes = patchNode.children;
  774. nodes.splice(index, 0, ...newNodes);
  775. }
  776. }
  777. }
  778. /**
  779. * Search the node index by the t attribute
  780. * and return the index. if not found return null
  781. * @param {!Array<shaka.extern.xml.Node>} nodes
  782. * @param {!string} attribute
  783. * @param {!number} value
  784. * @private
  785. */
  786. static nodePositionByAttribute_(nodes, attribute, value) {
  787. let index = 0;
  788. for (const node of nodes) {
  789. const attrs = node.attributes;
  790. const val = Number(attrs[attribute]);
  791. if (val === value) {
  792. return index;
  793. }
  794. index++;
  795. }
  796. return null;
  797. }
  798. /**
  799. * @param {!shaka.extern.xml.Node} node
  800. * @param {string} action
  801. * @param {string} attribute
  802. * @param {string} value
  803. */
  804. static modifyNodeAttribute(node, action, attribute, value) {
  805. if (action === 'remove') {
  806. delete node.attributes[attribute];
  807. } else if (action === 'add' || action === 'replace') {
  808. node.attributes[attribute] = value;
  809. }
  810. }
  811. /**
  812. * Converts a tXml node to DOM element.
  813. * @param {shaka.extern.xml.Node} node
  814. * @return {!Element}
  815. */
  816. static txmlNodeToDomElement(node) {
  817. const TXml = shaka.util.TXml;
  818. let namespace = '';
  819. const parts = node.tagName.split(':');
  820. if (parts.length > 0) {
  821. namespace = TXml.getKnownSchema(parts[0]);
  822. }
  823. const element = document.createElementNS(namespace, node.tagName);
  824. for (const k in node.attributes) {
  825. const v = node.attributes[k];
  826. element.setAttribute(k, v);
  827. }
  828. for (const child of node.children) {
  829. let childElement;
  830. if (typeof child == 'string') {
  831. childElement = new Text(child);
  832. } else {
  833. childElement = TXml.txmlNodeToDomElement(child);
  834. }
  835. element.appendChild(childElement);
  836. }
  837. return element;
  838. }
  839. /**
  840. * Clones node and its children recursively. Skips parent.
  841. * @param {?shaka.extern.xml.Node} node
  842. * @return {?shaka.extern.xml.Node}
  843. */
  844. static cloneNode(node) {
  845. if (!node) {
  846. return null;
  847. }
  848. /** @type {!shaka.extern.xml.Node} */
  849. const clone = {
  850. tagName: node.tagName,
  851. attributes: shaka.util.ObjectUtils.shallowCloneObject(node.attributes),
  852. children: [],
  853. parent: null,
  854. };
  855. for (const child of node.children) {
  856. if (typeof child === 'string') {
  857. clone.children.push(child);
  858. } else {
  859. const clonedChild = shaka.util.TXml.cloneNode(child);
  860. clonedChild.parent = clone;
  861. clone.children.push(clonedChild);
  862. }
  863. }
  864. return clone;
  865. }
  866. };
  867. /** @private {!Map<string, string>} */
  868. shaka.util.TXml.uriToNameSpace_ = new Map();
  869. /** @private {!Map<string, string>} */
  870. shaka.util.TXml.nameSpaceToUri_ = new Map();
  871. /**
  872. * @typedef {{
  873. * name: string,
  874. * id: ?string,
  875. * t: ?number,
  876. * n: ?number,
  877. * position: ?number,
  878. * attribute: ?string
  879. * }}
  880. */
  881. shaka.util.TXml.PathNode;