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