From 7116a34ec207a42921fe1c6404d5e3af404dc294 Mon Sep 17 00:00:00 2001 From: Dave Nicholas Date: Mon, 22 Jan 2024 07:39:06 +0000 Subject: [PATCH] feat!: Remove DOM Parser (#6063) ## Background: The native DOM Parser can perform poorly on some older devices, this approach is faster on newer devices but is considerably better on older devices. This PR replaces the usage of the DOM Parser for DASH, MSS, VTT and TTML. The draw back of this approach that it does not include any validation at the cost of better performance. --- build/conformance.textproto | 4 +- build/types/core | 2 +- docs/tutorials/upgrade.md | 3 + externs/shaka/player.js | 34 +- lib/ads/ad_manager.js | 2 +- lib/dash/content_protection.js | 107 +-- lib/dash/dash_parser.js | 303 ++++---- lib/dash/mpd_utils.js | 137 ++-- lib/dash/segment_base.js | 28 +- lib/dash/segment_list.js | 18 +- lib/dash/segment_template.js | 2 +- lib/hls/hls_parser.js | 4 +- lib/media/drm_engine.js | 25 +- lib/mss/content_protection.js | 53 +- lib/mss/mss_parser.js | 102 +-- lib/text/ttml_text_parser.js | 208 ++--- lib/text/vtt_text_parser.js | 80 +- lib/util/string_utils.js | 36 + lib/util/tXml.js | 724 ++++++++++++++++++ lib/util/xml_utils.js | 461 ----------- .../dash_parser_content_protection_unit.js | 3 +- test/dash/dash_parser_live_unit.js | 4 +- test/dash/dash_parser_manifest_unit.js | 42 +- test/dash/mpd_utils_unit.js | 37 +- .../mss/mss_parser_content_protection_unit.js | 3 +- test/mss/mss_parser_unit.js | 35 +- test/test/util/util.js | 2 +- test/util/tXml_unit.js | 391 ++++++++++ test/util/xml_utils_unit.js | 444 ----------- 29 files changed, 1752 insertions(+), 1542 deletions(-) create mode 100644 lib/util/tXml.js delete mode 100644 lib/util/xml_utils.js create mode 100644 test/util/tXml_unit.js delete mode 100644 test/util/xml_utils_unit.js diff --git a/build/conformance.textproto b/build/conformance.textproto index 1698220e32..2ccb4dd038 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -378,8 +378,8 @@ requirement: { value: "DOMParser.prototype.parseFromString" error_message: "Using \"DOMParser.parseFromString\" directly is not allowed; " - "use shaka.util.XmlUtils.parseXmlString instead." - whitelist_regexp: "lib/util/xml_utils.js" + "use shaka.util.TXml.parseXmlString instead." + whitelist_regexp: "lib/util/tXml.js" whitelist_regexp: "test/" } diff --git a/build/types/core b/build/types/core index ebfd9b1c99..cb1b111383 100644 --- a/build/types/core +++ b/build/types/core @@ -117,7 +117,7 @@ +../../lib/util/timer.js +../../lib/util/ts_parser.js +../../lib/util/uint8array_utils.js -+../../lib/util/xml_utils.js ++../../lib/util/tXml.js +../../third_party/closure-uri/uri.js +../../third_party/closure-uri/utils.js diff --git a/docs/tutorials/upgrade.md b/docs/tutorials/upgrade.md index d63f29d8f2..2505d8e162 100644 --- a/docs/tutorials/upgrade.md +++ b/docs/tutorials/upgrade.md @@ -96,9 +96,12 @@ application: - Configuration changes: - `streaming.forceTransmuxTS` has been renamed to `streaming.forceTransmux` (deprecated in v4.3.0) + - `manifest.dash.manifestPreprocessor` callback now receives a type of `shaka.externs.xml.Node` instead of `Element`. + - `manifest.mss.manifestPreprocessor` callback now receives a type of `shaka.externs.xml.Node` instead of `Element`. - Plugin changes: - `Transmuxer` plugins now has three new parameters in `transmux()` method. - Player API Changes: - The constructor no longer takes `mediaElement` as a parameter; use the `attach` method to attach to a media element instead. (Deprecated in v4.6) + - The `TimelineRegionInfo.eventElement` property is now a type of `shaka.externs.xml.Node` instead of `Element` diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 5615ae0112..b3344f3177 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -521,7 +521,7 @@ shaka.extern.MetadataFrame; * startTime: number, * endTime: number, * id: string, - * eventElement: Element + * eventElement: ?shaka.extern.xml.Node * }} * * @description @@ -539,7 +539,7 @@ shaka.extern.MetadataFrame; * The presentation time (in seconds) that the region should end. * @property {string} id * Specifies an identifier for this instance of the region. - * @property {Element} eventElement + * @property {?shaka.extern.xml.Node} eventElement * The XML element that defines the Event. * @exportDoc */ @@ -840,6 +840,28 @@ shaka.extern.DrmConfiguration; shaka.extern.InitDataTransform; +/** + * @typedef {{ + * tagName: !string, + * attributes: !Object, + * children: !Array., + * parent: ?shaka.extern.xml.Node + * }} + * + * @description + * Data structure for xml nodes as simple objects + * + * @property {!string} tagName + * The name of the element + * @property {!object} attributes + * The attributes of the element + * @property {!Array.} children + * The child nodes or string body of the element + * @property {?shaka.extern.xml.Node} parent + * The parent of the current element + */ +shaka.extern.xml.Node; + /** * @typedef {{ * clockSyncUri: string, @@ -853,7 +875,7 @@ shaka.extern.InitDataTransform; * ignoreEmptyAdaptationSet: boolean, * ignoreMaxSegmentDuration: boolean, * keySystemsByURI: !Object., - * manifestPreprocessor: function(!Element), + * manifestPreprocessor: function(!shaka.extern.xml.Node), * sequenceMode: boolean, * enableAudioGroups: boolean, * multiTypeVariantsAllowed: boolean, @@ -907,7 +929,7 @@ shaka.extern.InitDataTransform; * @property {Object.} keySystemsByURI * A map of scheme URI to key system name. Defaults to default key systems * mapping handled by Shaka. - * @property {function(!Element)} manifestPreprocessor + * @property {function(!shaka.extern.xml.Node)} manifestPreprocessor * Called immediately after the DASH manifest has been parsed into an * XMLDocument. Provides a way for applications to perform efficient * preprocessing of the manifest. @@ -1025,12 +1047,12 @@ shaka.extern.HlsManifestConfiguration; /** * @typedef {{ - * manifestPreprocessor: function(!Element), + * manifestPreprocessor: function(!shaka.extern.xml.Node), * sequenceMode: boolean, * keySystemsBySystemId: !Object. * }} * - * @property {function(!Element)} manifestPreprocessor + * @property {function(!shaka.extern.xml.Node)} manifestPreprocessor * Called immediately after the MSS manifest has been parsed into an * XMLDocument. Provides a way for applications to perform efficient * preprocessing of the manifest. diff --git a/lib/ads/ad_manager.js b/lib/ads/ad_manager.js index c6a82c3d03..732f00ea84 100644 --- a/lib/ads/ad_manager.js +++ b/lib/ads/ad_manager.js @@ -706,7 +706,7 @@ shaka.ads.AdManager = class extends shaka.util.FakeEventTarget { if (this.ssAdManager_ && region.schemeIdUri == 'urn:google:dai:2018') { const type = region.schemeIdUri; const data = region.eventElement ? - region.eventElement.getAttribute('messageData') : null; + region.eventElement.attributes['messageData'] : null; const timestamp = region.startTime; this.ssAdManager_.onTimedMetadata(type, data, timestamp); } diff --git a/lib/dash/content_protection.js b/lib/dash/content_protection.js index 017474d96a..a63f30597d 100644 --- a/lib/dash/content_protection.js +++ b/lib/dash/content_protection.js @@ -13,8 +13,8 @@ goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); /** @@ -25,7 +25,7 @@ shaka.dash.ContentProtection = class { /** * Parses info from the ContentProtection elements at the AdaptationSet level. * - * @param {!Array.} elems + * @param {!Array.} elems * @param {boolean} ignoreDrmInfo * @param {!Object.} keySystemsByURI * @return {shaka.dash.ContentProtection.Context} @@ -137,7 +137,7 @@ shaka.dash.ContentProtection = class { * Parses the given ContentProtection elements found at the Representation * level. This may update the |context|. * - * @param {!Array.} elems + * @param {!Array.} elems * @param {shaka.dash.ContentProtection.Context} context * @param {boolean} ignoreDrmInfo * @param {!Object.} keySystemsByURI @@ -191,17 +191,20 @@ shaka.dash.ContentProtection = class { * @return {string} */ static getWidevineLicenseUrl(element) { - const dashIfLaurlNode = shaka.util.XmlUtils.findChildNS( + const dashIfLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.DashIfNamespaceUri_, 'Laurl', ); - if (dashIfLaurlNode && dashIfLaurlNode.textContent) { - return dashIfLaurlNode.textContent; + if (dashIfLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(dashIfLaurlNode); + if (textContents) { + return textContents; + } } - const mslaurlNode = shaka.util.XmlUtils.findChildNS( + const mslaurlNode = shaka.util.TXml.findChildNS( element.node, 'urn:microsoft', 'laurl'); if (mslaurlNode) { - return mslaurlNode.getAttribute('licenseUrl') || ''; + return mslaurlNode.attributes['licenseUrl'] || ''; } return ''; } @@ -214,21 +217,27 @@ shaka.dash.ContentProtection = class { * @return {string} */ static getClearKeyLicenseUrl(element) { - const dashIfLaurlNode = shaka.util.XmlUtils.findChildNS( + const dashIfLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.DashIfNamespaceUri_, 'Laurl', ); - if (dashIfLaurlNode && dashIfLaurlNode.textContent) { - return dashIfLaurlNode.textContent; + if (dashIfLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(dashIfLaurlNode); + if (textContents) { + return textContents; + } } - const clearKeyLaurlNode = shaka.util.XmlUtils.findChildNS( + const clearKeyLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.ClearKeyNamespaceUri_, 'Laurl', ); if (clearKeyLaurlNode && - clearKeyLaurlNode.getAttribute('Lic_type') === 'EME-1.0') { - if (clearKeyLaurlNode.textContent) { - return clearKeyLaurlNode.textContent; + clearKeyLaurlNode.attributes['Lic_type'] === 'EME-1.0') { + if (clearKeyLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(clearKeyLaurlNode); + if (textContents) { + return textContents; + } } } return ''; @@ -312,18 +321,21 @@ shaka.dash.ContentProtection = class { /** * PlayReady Header format: https://goo.gl/dBzxNA * - * @param {!Element} xml + * @param {!shaka.extern.xml.Node} xml * @return {string} * @private */ static getLaurl_(xml) { + const TXml = shaka.util.TXml; // LA_URL element is optional and no more than one is // allowed inside the DATA element. Only absolute URLs are allowed. // If the LA_URL element exists, it must not be empty. - for (const elem of xml.getElementsByTagName('DATA')) { - for (const child of elem.childNodes) { - if (child instanceof Element && child.tagName == 'LA_URL') { - return child.textContent; + for (const elem of TXml.getElementsByTagName(xml, 'DATA')) { + if (elem.children) { + for (const child of elem.children) { + if (child.tagName == 'LA_URL') { + return /** @type{string} */(shaka.util.TXml.getTextContents(child)); + } } } } @@ -340,25 +352,30 @@ shaka.dash.ContentProtection = class { * @return {string} */ static getPlayReadyLicenseUrl(element) { - const dashIfLaurlNode = shaka.util.XmlUtils.findChildNS( + const dashIfLaurlNode = shaka.util.TXml.findChildNS( element.node, shaka.dash.ContentProtection.DashIfNamespaceUri_, 'Laurl', ); - if (dashIfLaurlNode && dashIfLaurlNode.textContent) { - return dashIfLaurlNode.textContent; + if (dashIfLaurlNode) { + const textContents = shaka.util.TXml.getTextContents(dashIfLaurlNode); + if (textContents) { + return textContents; + } } - const proNode = shaka.util.XmlUtils.findChildNS( + const proNode = shaka.util.TXml.findChildNS( element.node, 'urn:microsoft:playready', 'pro'); - if (!proNode) { + if (!proNode || !shaka.util.TXml.getTextContents(proNode)) { return ''; } const ContentProtection = shaka.dash.ContentProtection; const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES; - const bytes = shaka.util.Uint8ArrayUtils.fromBase64(proNode.textContent); + const textContent = + /** @type {string} */ (shaka.util.TXml.getTextContents(proNode)); + const bytes = shaka.util.Uint8ArrayUtils.fromBase64(textContent); const records = ContentProtection.parseMsPro_(bytes); const record = records.filter((record) => { return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT; @@ -369,7 +386,7 @@ shaka.dash.ContentProtection = class { } const xml = shaka.util.StringUtils.fromUTF16(record.value, true); - const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER'); + const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER'); if (!rootElement) { return ''; } @@ -386,13 +403,16 @@ shaka.dash.ContentProtection = class { * @private */ static getInitDataFromPro_(element) { - const proNode = shaka.util.XmlUtils.findChildNS( + const proNode = shaka.util.TXml.findChildNS( element.node, 'urn:microsoft:playready', 'pro'); - if (!proNode) { + if (!proNode || !shaka.util.TXml.getTextContents(proNode)) { return null; } + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; - const data = Uint8ArrayUtils.fromBase64(proNode.textContent); + const textContent = + /** @type{string} */ (shaka.util.TXml.getTextContents(proNode)); + const data = Uint8ArrayUtils.fromBase64(textContent); const systemId = new Uint8Array([ 0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95, @@ -492,7 +512,7 @@ shaka.dash.ContentProtection = class { * Parses the given ContentProtection elements. If there is an error, it * removes those elements. * - * @param {!Array.} elems + * @param {!Array.} elems * @return {!Array.} * @private */ @@ -513,20 +533,21 @@ shaka.dash.ContentProtection = class { /** * Parses the given ContentProtection element. * - * @param {!Element} elem + * @param {!shaka.extern.xml.Node} elem * @return {?shaka.dash.ContentProtection.Element} * @private */ static parseElement_(elem) { const NS = shaka.dash.ContentProtection.CencNamespaceUri_; + const TXml = shaka.util.TXml; /** @type {?string} */ - let schemeUri = elem.getAttribute('schemeIdUri'); + let schemeUri = elem.attributes['schemeIdUri']; /** @type {?string} */ - let keyId = shaka.util.XmlUtils.getAttributeNS(elem, NS, 'default_KID'); + let keyId = TXml.getAttributeNS(elem, NS, 'default_KID'); /** @type {!Array.} */ - const psshs = shaka.util.XmlUtils.findChildrenNS(elem, NS, 'pssh') - .map(shaka.util.XmlUtils.getContents); + const psshs = TXml.findChildrenNS(elem, NS, 'pssh') + .map(TXml.getContents); if (!schemeUri) { shaka.log.error('Missing required schemeIdUri attribute on', @@ -590,7 +611,7 @@ shaka.dash.ContentProtection = class { } const namespace = 'urn:mpeg:dash:schema:sea:2012'; - const segmentEncryption = shaka.util.XmlUtils.findChildNS( + const segmentEncryption = shaka.util.TXml.findChildNS( element.node, namespace, 'SegmentEncryption'); if (!segmentEncryption) { @@ -602,7 +623,7 @@ shaka.dash.ContentProtection = class { const aesSchemeIdUri = 'urn:mpeg:dash:sea:aes128-cbc:2013'; const segmentEncryptionSchemeIdUri = - segmentEncryption.getAttribute('schemeIdUri'); + segmentEncryption.attributes['schemeIdUri']; if (segmentEncryptionSchemeIdUri != aesSchemeIdUri) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -610,7 +631,7 @@ shaka.dash.ContentProtection = class { shaka.util.Error.Code.DASH_UNSUPPORTED_AES_128); } - const cryptoPeriod = shaka.util.XmlUtils.findChildNS( + const cryptoPeriod = shaka.util.TXml.findChildNS( element.node, namespace, 'CryptoPeriod'); if (!cryptoPeriod) { @@ -620,8 +641,8 @@ shaka.dash.ContentProtection = class { shaka.util.Error.Code.DASH_UNSUPPORTED_AES_128); } - const ivHex = cryptoPeriod.getAttribute('IV'); - const keyUri = cryptoPeriod.getAttribute('keyUriTemplate'); + const ivHex = cryptoPeriod.attributes['IV']; + const keyUri = cryptoPeriod.attributes['keyUriTemplate']; if (!ivHex || !keyUri) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -721,7 +742,7 @@ shaka.dash.ContentProtection.Aes128Info; /** * @typedef {{ - * node: !Element, + * node: !shaka.extern.xml.Node, * schemeUri: string, * keyId: ?string, * init: Array. @@ -730,7 +751,7 @@ shaka.dash.ContentProtection.Aes128Info; * @description * The parsed result of a single ContentProtection element. * - * @property {!Element} node + * @property {!shaka.extern.xml.Node} node * The ContentProtection XML element. * @property {string} schemeUri * The scheme URI. diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 8e40b253d1..1dc9398fce 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -31,7 +31,7 @@ goog.require('shaka.util.OperationManager'); goog.require('shaka.util.PeriodCombiner'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -331,13 +331,16 @@ shaka.dash.DashParser = class { async parseManifest_(data, finalManifestUri) { const Error = shaka.util.Error; const MpdUtils = shaka.dash.MpdUtils; + const TXml = shaka.util.TXml; + + const mpd = TXml.parseXml(data, 'MPD'); - const mpd = shaka.util.XmlUtils.parseXml(data, 'MPD'); if (!mpd) { throw new Error( Error.Severity.CRITICAL, Error.Category.MANIFEST, Error.Code.DASH_INVALID_XML, finalManifestUri); } + const disableXlinkProcessing = this.config_.dash.disableXlinkProcessing; if (disableXlinkProcessing) { return this.processManifest_(mpd, finalManifestUri); @@ -357,14 +360,14 @@ shaka.dash.DashParser = class { /** * Takes a formatted MPD and converts it into a manifest. * - * @param {!Element} mpd + * @param {!shaka.extern.xml.Node} mpd * @param {string} finalManifestUri The final manifest URI, which may * differ from this.manifestUri_ if there has been a redirect. * @return {!Promise} * @private */ async processManifest_(mpd, finalManifestUri) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const manifestPreprocessor = this.config_.dash.manifestPreprocessor; if (manifestPreprocessor) { @@ -383,10 +386,10 @@ shaka.dash.DashParser = class { const locations = []; /** @type {!Map.} */ const locationsMapping = new Map(); - const locationsObjs = XmlUtils.findChildren(mpd, 'Location'); + const locationsObjs = TXml.findChildren(mpd, 'Location'); for (const locationsObj of locationsObjs) { - const serviceLocation = locationsObj.getAttribute('serviceLocation'); - const uri = XmlUtils.getContents(locationsObj); + const serviceLocation = locationsObj.attributes['serviceLocation']; + const uri = TXml.getContents(locationsObj); if (!uri) { continue; } @@ -416,10 +419,10 @@ shaka.dash.DashParser = class { let contentSteeringPromise = Promise.resolve(); - const contentSteering = XmlUtils.findChild(mpd, 'ContentSteering'); + const contentSteering = TXml.findChild(mpd, 'ContentSteering'); if (contentSteering && this.playerInterface_) { const defaultPathwayId = - contentSteering.getAttribute('defaultServiceLocation'); + contentSteering.attributes['defaultServiceLocation']; if (!this.contentSteeringManager_) { this.contentSteeringManager_ = new shaka.util.ContentSteeringManager(this.playerInterface_); @@ -428,11 +431,11 @@ shaka.dash.DashParser = class { shaka.media.ManifestParser.DASH); this.contentSteeringManager_.setBaseUris(manifestBaseUris); this.contentSteeringManager_.setDefaultPathwayId(defaultPathwayId); - const uri = XmlUtils.getContents(contentSteering); + const uri = TXml.getContents(contentSteering); if (uri) { const queryBeforeStart = - XmlUtils.parseAttr(contentSteering, 'queryBeforeStart', - XmlUtils.parseBoolean, /* defaultValue= */ false); + TXml.parseAttr(contentSteering, 'queryBeforeStart', + TXml.parseBoolean, /* defaultValue= */ false); if (queryBeforeStart) { contentSteeringPromise = this.contentSteeringManager_.requestInfo(uri); @@ -451,13 +454,13 @@ shaka.dash.DashParser = class { } } - const uriObjs = XmlUtils.findChildren(mpd, 'BaseURL'); + const uriObjs = TXml.findChildren(mpd, 'BaseURL'); let calculatedBaseUris; let someLocationValid = false; if (this.contentSteeringManager_) { for (const uriObj of uriObjs) { - const serviceLocation = uriObj.getAttribute('serviceLocation'); - const uri = XmlUtils.getContents(uriObj); + const serviceLocation = uriObj.attributes['serviceLocation']; + const uri = TXml.getContents(uriObj); if (serviceLocation && uri) { this.contentSteeringManager_.addLocation( 'BaseURL', serviceLocation, uri); @@ -466,7 +469,7 @@ shaka.dash.DashParser = class { } } if (!someLocationValid || !this.contentSteeringManager_) { - const uris = uriObjs.map(XmlUtils.getContents); + const uris = uriObjs.map(TXml.getContents); calculatedBaseUris = shaka.util.ManifestParserUtils.resolveUris( manifestBaseUris, uris); } @@ -483,41 +486,41 @@ shaka.dash.DashParser = class { let availabilityTimeOffset = 0; if (uriObjs && uriObjs.length) { - availabilityTimeOffset = XmlUtils.parseAttr( - uriObjs[0], 'availabilityTimeOffset', XmlUtils.parseFloat) || 0; + availabilityTimeOffset = TXml.parseAttr(uriObjs[0], + 'availabilityTimeOffset', TXml.parseFloat) || 0; } const ignoreMinBufferTime = this.config_.dash.ignoreMinBufferTime; let minBufferTime = 0; if (!ignoreMinBufferTime) { minBufferTime = - XmlUtils.parseAttr(mpd, 'minBufferTime', XmlUtils.parseDuration) || 0; + TXml.parseAttr(mpd, 'minBufferTime', TXml.parseDuration) || 0; } - this.updatePeriod_ = /** @type {number} */ (XmlUtils.parseAttr( - mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration, -1)); + this.updatePeriod_ = /** @type {number} */ (TXml.parseAttr( + mpd, 'minimumUpdatePeriod', TXml.parseDuration, -1)); - const presentationStartTime = XmlUtils.parseAttr( - mpd, 'availabilityStartTime', XmlUtils.parseDate); - let segmentAvailabilityDuration = XmlUtils.parseAttr( - mpd, 'timeShiftBufferDepth', XmlUtils.parseDuration); + const presentationStartTime = TXml.parseAttr( + mpd, 'availabilityStartTime', TXml.parseDate); + let segmentAvailabilityDuration = TXml.parseAttr( + mpd, 'timeShiftBufferDepth', TXml.parseDuration); const ignoreSuggestedPresentationDelay = this.config_.dash.ignoreSuggestedPresentationDelay; let suggestedPresentationDelay = null; if (!ignoreSuggestedPresentationDelay) { - suggestedPresentationDelay = XmlUtils.parseAttr( - mpd, 'suggestedPresentationDelay', XmlUtils.parseDuration); + suggestedPresentationDelay = TXml.parseAttr( + mpd, 'suggestedPresentationDelay', TXml.parseDuration); } const ignoreMaxSegmentDuration = this.config_.dash.ignoreMaxSegmentDuration; let maxSegmentDuration = null; if (!ignoreMaxSegmentDuration) { - maxSegmentDuration = XmlUtils.parseAttr( - mpd, 'maxSegmentDuration', XmlUtils.parseDuration); + maxSegmentDuration = TXml.parseAttr( + mpd, 'maxSegmentDuration', TXml.parseDuration); } - const mpdType = mpd.getAttribute('type') || 'static'; + const mpdType = mpd.attributes['type'] || 'static'; /** @type {!shaka.media.PresentationTimeline} */ let presentationTimeline; @@ -577,7 +580,7 @@ shaka.dash.DashParser = class { presentationTimeline.setSegmentAvailabilityDuration( segmentAvailabilityDuration); - const profiles = mpd.getAttribute('profiles') || ''; + const profiles = mpd.attributes['profiles'] || ''; /** @type {shaka.dash.DashParser.Context} */ const context = { @@ -665,8 +668,8 @@ shaka.dash.DashParser = class { // We only need to do clock sync when we're using presentation start // time. This condition also excludes VOD streams. if (presentationTimeline.usingPresentationStartTime()) { - const XmlUtils = shaka.util.XmlUtils; - const timingElements = XmlUtils.findChildren(mpd, 'UTCTiming'); + const TXml = shaka.util.TXml; + const timingElements = TXml.findChildren(mpd, 'UTCTiming'); const offset = await this.parseUtcTiming_(getBaseUris, timingElements); // Detect calls to stop(). if (!this.playerInterface_) { @@ -705,33 +708,33 @@ shaka.dash.DashParser = class { * Reads maxLatency and maxPlaybackRate properties from service * description element. * - * @param {!Element} mpd + * @param {!shaka.extern.xml.Node} mpd * @return {?shaka.extern.ServiceDescription} * @private */ parseServiceDescription_(mpd) { - const XmlUtils = shaka.util.XmlUtils; - const elem = XmlUtils.findChild(mpd, 'ServiceDescription'); + const TXml = shaka.util.TXml; + const elem = TXml.findChild(mpd, 'ServiceDescription'); if (!elem ) { return null; } - const latencyNode = XmlUtils.findChild(elem, 'Latency'); - const playbackRateNode = XmlUtils.findChild(elem, 'PlaybackRate'); + const latencyNode = TXml.findChild(elem, 'Latency'); + const playbackRateNode = TXml.findChild(elem, 'PlaybackRate'); - if ((latencyNode && latencyNode.getAttribute('max')) || playbackRateNode) { - const maxLatency = latencyNode && latencyNode.getAttribute('max') ? - parseInt(latencyNode.getAttribute('max'), 10) / 1000 : + if ((latencyNode && latencyNode.attributes['max']) || playbackRateNode) { + const maxLatency = latencyNode && latencyNode.attributes['max'] ? + parseInt(latencyNode.attributes['max'], 10) / 1000 : null; const maxPlaybackRate = playbackRateNode ? - parseFloat(playbackRateNode.getAttribute('max')) : + parseFloat(playbackRateNode.attributes['max']) : null; - const minLatency = latencyNode && latencyNode.getAttribute('min') ? - parseInt(latencyNode.getAttribute('min'), 10) / 1000 : + const minLatency = latencyNode && latencyNode.attributes['min'] ? + parseInt(latencyNode.attributes['min'], 10) / 1000 : null; const minPlaybackRate = playbackRateNode ? - parseFloat(playbackRateNode.getAttribute('min')) : + parseFloat(playbackRateNode.attributes['min']) : null; return { @@ -752,7 +755,7 @@ shaka.dash.DashParser = class { * * @param {shaka.dash.DashParser.Context} context * @param {function():!Array.} getBaseUris - * @param {!Element} mpd + * @param {!shaka.extern.xml.Node} mpd * @return {{ * periods: !Array., * duration: ?number, @@ -761,21 +764,21 @@ shaka.dash.DashParser = class { * @private */ parsePeriods_(context, getBaseUris, mpd) { - const XmlUtils = shaka.util.XmlUtils; - const presentationDuration = XmlUtils.parseAttr( - mpd, 'mediaPresentationDuration', XmlUtils.parseDuration); + const TXml = shaka.util.TXml; + const presentationDuration = TXml.parseAttr( + mpd, 'mediaPresentationDuration', TXml.parseDuration); const periods = []; let prevEnd = 0; - const periodNodes = XmlUtils.findChildren(mpd, 'Period'); + const periodNodes = TXml.findChildren(mpd, 'Period'); for (let i = 0; i < periodNodes.length; i++) { const elem = periodNodes[i]; const next = periodNodes[i + 1]; const start = /** @type {number} */ ( - XmlUtils.parseAttr(elem, 'start', XmlUtils.parseDuration, prevEnd)); - const periodId = elem.id; + TXml.parseAttr(elem, 'start', TXml.parseDuration, prevEnd)); + const periodId = elem.attributes['id']; const givenDuration = - XmlUtils.parseAttr(elem, 'duration', XmlUtils.parseDuration); + TXml.parseAttr(elem, 'duration', TXml.parseDuration); let periodDuration = null; if (next) { @@ -783,7 +786,7 @@ shaka.dash.DashParser = class { // of the following Period is the duration of the media content // represented by this Period." const nextStart = - XmlUtils.parseAttr(next, 'start', XmlUtils.parseDuration); + TXml.parseAttr(next, 'start', TXml.parseDuration); if (nextStart != null) { periodDuration = nextStart - start; } @@ -911,7 +914,7 @@ shaka.dash.DashParser = class { */ parsePeriod_(context, getBaseUris, periodInfo) { const Functional = shaka.util.Functional; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; context.period = this.createFrame_(periodInfo.node, null, getBaseUris); @@ -927,7 +930,7 @@ shaka.dash.DashParser = class { } const eventStreamNodes = - XmlUtils.findChildren(periodInfo.node, 'EventStream'); + TXml.findChildren(periodInfo.node, 'EventStream'); const availabilityStart = context.presentationTimeline.getSegmentAvailabilityStart(); @@ -937,7 +940,7 @@ shaka.dash.DashParser = class { } const adaptationSetNodes = - XmlUtils.findChildren(periodInfo.node, 'AdaptationSet'); + TXml.findChildren(periodInfo.node, 'AdaptationSet'); const adaptationSets = adaptationSetNodes .map((node) => this.parseAdaptationSet_(context, node)) .filter(Functional.isNotNull); @@ -1046,12 +1049,12 @@ shaka.dash.DashParser = class { * Parses an AdaptationSet XML element. * * @param {shaka.dash.DashParser.Context} context - * @param {!Element} elem The AdaptationSet element. + * @param {!shaka.extern.xml.Node} elem The AdaptationSet element. * @return {?shaka.dash.DashParser.AdaptationInfo} * @private */ parseAdaptationSet_(context, elem) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const Functional = shaka.util.Functional; const ManifestParserUtils = shaka.util.ManifestParserUtils; const ContentType = ManifestParserUtils.ContentType; @@ -1060,9 +1063,9 @@ shaka.dash.DashParser = class { context.adaptationSet = this.createFrame_(elem, context.period, null); let main = false; - const roleElements = XmlUtils.findChildren(elem, 'Role'); + const roleElements = TXml.findChildren(elem, 'Role'); const roleValues = roleElements.map((role) => { - return role.getAttribute('value'); + return role.attributes['value']; }).filter(Functional.isNotNull); // Default kind for text streams is 'subtitle' if unspecified in the @@ -1074,12 +1077,12 @@ shaka.dash.DashParser = class { } for (const roleElement of roleElements) { - const scheme = roleElement.getAttribute('schemeIdUri'); + const scheme = roleElement.attributes['schemeIdUri']; if (scheme == null || scheme == 'urn:mpeg:dash:role:2011') { // These only apply for the given scheme, but allow them to be specified // if there is no scheme specified. // See: DASH section 5.8.5.5 - const value = roleElement.getAttribute('value'); + const value = roleElement.attributes['value']; switch (value) { case 'main': main = true; @@ -1124,18 +1127,18 @@ shaka.dash.DashParser = class { }; const essentialProperties = - XmlUtils.findChildren(elem, 'EssentialProperty'); + TXml.findChildren(elem, 'EssentialProperty'); // ID of real AdaptationSet if this is a trick mode set: let trickModeFor = null; let isFastSwitching = false; let unrecognizedEssentialProperty = false; for (const prop of essentialProperties) { - const schemeId = prop.getAttribute('schemeIdUri'); + const schemeId = prop.attributes['schemeIdUri']; if (schemeId == 'http://dashif.org/guidelines/trickmode') { - trickModeFor = prop.getAttribute('value'); + trickModeFor = prop.attributes['value']; } else if (schemeId == transferCharacteristicsScheme) { videoRange = getVideoRangeFromTransferCharacteristicCICP( - parseInt(prop.getAttribute('value'), 10), + parseInt(prop.attributes['value'], 10), ); } else if (schemeId == colourPrimariesScheme || schemeId == matrixCoefficientsScheme) { @@ -1148,24 +1151,24 @@ shaka.dash.DashParser = class { } const supplementalProperties = - XmlUtils.findChildren(elem, 'SupplementalProperty'); + TXml.findChildren(elem, 'SupplementalProperty'); for (const prop of supplementalProperties) { - const schemeId = prop.getAttribute('schemeIdUri'); + const schemeId = prop.attributes['schemeIdUri']; if (schemeId == transferCharacteristicsScheme) { videoRange = getVideoRangeFromTransferCharacteristicCICP( - parseInt(prop.getAttribute('value'), 10), + parseInt(prop.attributes['value'], 10), ); } } - const accessibilities = XmlUtils.findChildren(elem, 'Accessibility'); + const accessibilities = TXml.findChildren(elem, 'Accessibility'); const LanguageUtils = shaka.util.LanguageUtils; const closedCaptions = new Map(); /** @type {?shaka.media.ManifestParser.AccessibilityPurpose} */ let accessibilityPurpose; for (const prop of accessibilities) { - const schemeId = prop.getAttribute('schemeIdUri'); - const value = prop.getAttribute('value'); + const schemeId = prop.attributes['schemeIdUri']; + const value = prop.attributes['value']; if (schemeId == 'urn:scte:dash:cc:cea-608:2015' ) { let channelId = 1; if (value != null) { @@ -1269,7 +1272,7 @@ shaka.dash.DashParser = class { } const contentProtectionElems = - XmlUtils.findChildren(elem, 'ContentProtection'); + TXml.findChildren(elem, 'ContentProtection'); const contentProtection = ContentProtection.parseFromAdaptationSet( contentProtectionElems, this.config_.dash.ignoreDrmInfo, @@ -1279,20 +1282,21 @@ shaka.dash.DashParser = class { context.adaptationSet.language || 'und'); // This attribute is currently non-standard, but it is supported by Kaltura. - let label = elem.getAttribute('label'); + let label = elem.attributes['label']; // See DASH IOP 4.3 here https://dashif.org/docs/DASH-IF-IOP-v4.3.pdf (page 35) - const labelElements = XmlUtils.findChildren(elem, 'Label'); + const labelElements = TXml.findChildren(elem, 'Label'); if (labelElements && labelElements.length) { // NOTE: Right now only one label field is supported. const firstLabelElement = labelElements[0]; - if (firstLabelElement.textContent) { - label = firstLabelElement.textContent; + const textContent = shaka.util.TXml.getTextContents(firstLabelElement); + if (textContent) { + label = textContent; } } // Parse Representations into Streams. - const representations = XmlUtils.findChildren(elem, 'Representation'); + const representations = TXml.findChildren(elem, 'Representation'); const streams = representations.map((representation) => { const parsedRepresentation = this.parseRepresentation_(context, contentProtection, kind, language, label, main, roleValues, @@ -1352,7 +1356,7 @@ shaka.dash.DashParser = class { } const repIds = representations - .map((node) => { return node.getAttribute('id'); }) + .map((node) => { return node.attributes['id']; }) .filter(shaka.util.Functional.isNotNull); return { @@ -1378,7 +1382,7 @@ shaka.dash.DashParser = class { * @param {boolean} isPrimary * @param {!Array.} roles * @param {Map.} closedCaptions - * @param {!Element} node + * @param {!shaka.extern.xml.Node} node * @param {?shaka.media.ManifestParser.AccessibilityPurpose} * accessibilityPurpose * @@ -1388,7 +1392,7 @@ shaka.dash.DashParser = class { */ parseRepresentation_(context, contentProtection, kind, language, label, isPrimary, roles, closedCaptions, node, accessibilityPurpose) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; context.representation = @@ -1410,7 +1414,7 @@ shaka.dash.DashParser = class { // To avoid NaN at the variant level on broken content, fall back to zero. // https://github.com/shaka-project/shaka-player/issues/938#issuecomment-317278180 context.bandwidth = - XmlUtils.parseAttr(node, 'bandwidth', XmlUtils.parsePositiveInt) || 0; + TXml.parseAttr(node, 'bandwidth', TXml.parsePositiveInt) || 0; /** @type {?shaka.dash.DashParser.StreamInfo} */ let streamInfo; @@ -1503,7 +1507,7 @@ shaka.dash.DashParser = class { } const contentProtectionElems = - XmlUtils.findChildren(node, 'ContentProtection'); + TXml.findChildren(node, 'ContentProtection'); const keyId = shaka.dash.ContentProtection.parseFromRepresentation( contentProtectionElems, contentProtection, this.config_.dash.ignoreDrmInfo, @@ -1513,12 +1517,12 @@ shaka.dash.DashParser = class { // Detect the presence of E-AC3 JOC audio content, using DD+JOC signaling. // See: ETSI TS 103 420 V1.2.1 (2018-10) const supplementalPropertyElems = - XmlUtils.findChildren(node, 'SupplementalProperty'); + TXml.findChildren(node, 'SupplementalProperty'); const hasJoc = supplementalPropertyElems.some((element) => { const expectedUri = 'tag:dolby.com,2018:dash:EC3_ExtensionType:2018'; const expectedValue = 'JOC'; - return element.getAttribute('schemeIdUri') == expectedUri && - element.getAttribute('value') == expectedValue; + return element.attributes['schemeIdUri'] == expectedUri && + element.attributes['value'] == expectedValue; }); let spatialAudio = false; if (hasJoc) { @@ -1536,16 +1540,16 @@ shaka.dash.DashParser = class { let tilesLayout; if (isImage) { const essentialPropertyElems = - XmlUtils.findChildren(node, 'EssentialProperty'); + TXml.findChildren(node, 'EssentialProperty'); const thumbnailTileElem = essentialPropertyElems.find((element) => { const expectedUris = [ 'http://dashif.org/thumbnail_tile', 'http://dashif.org/guidelines/thumbnail_tile', ]; - return expectedUris.includes(element.getAttribute('schemeIdUri')); + return expectedUris.includes(element.attributes['schemeIdUri']); }); if (thumbnailTileElem) { - tilesLayout = thumbnailTileElem.getAttribute('value'); + tilesLayout = thumbnailTileElem.attributes['value']; } // Filter image adaptation sets that has no tilesLayout. if (!tilesLayout) { @@ -1704,7 +1708,7 @@ shaka.dash.DashParser = class { /** * Creates a new inheritance frame for the given element. * - * @param {!Element} elem + * @param {!shaka.extern.xml.Node} elem * @param {?shaka.dash.DashParser.InheritanceFrame} parent * @param {?function():!Array.} getBaseUris * @return {shaka.dash.DashParser.InheritanceFrame} @@ -1714,7 +1718,7 @@ shaka.dash.DashParser = class { goog.asserts.assert(parent || getBaseUris, 'Must provide either parent or getBaseUris'); const ManifestParserUtils = shaka.util.ManifestParserUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; parent = parent || /** @type {shaka.dash.DashParser.InheritanceFrame} */ ({ contentType: '', mimeType: '', @@ -1729,17 +1733,17 @@ shaka.dash.DashParser = class { }); getBaseUris = getBaseUris || parent.getBaseUris; - const parseNumber = XmlUtils.parseNonNegativeInt; - const evalDivision = XmlUtils.evalDivision; + const parseNumber = TXml.parseNonNegativeInt; + const evalDivision = TXml.evalDivision; - const id = elem.getAttribute('id'); - const uriObjs = XmlUtils.findChildren(elem, 'BaseURL'); + const id = elem.attributes['id']; + const uriObjs = TXml.findChildren(elem, 'BaseURL'); let calculatedBaseUris; let someLocationValid = false; if (this.contentSteeringManager_) { for (const uriObj of uriObjs) { - const serviceLocation = uriObj.getAttribute('serviceLocation'); - const uri = XmlUtils.getContents(uriObj); + const serviceLocation = uriObj.attributes['serviceLocation']; + const uri = TXml.getContents(uriObj); if (serviceLocation && uri) { this.contentSteeringManager_.addLocation( id, serviceLocation, uri); @@ -1748,7 +1752,7 @@ shaka.dash.DashParser = class { } } if (!someLocationValid || !this.contentSteeringManager_) { - calculatedBaseUris = uriObjs.map(XmlUtils.getContents); + calculatedBaseUris = uriObjs.map(TXml.getContents); } const getFrameUris = () => { @@ -1764,55 +1768,55 @@ shaka.dash.DashParser = class { return []; }; - let contentType = elem.getAttribute('contentType') || parent.contentType; - const mimeType = elem.getAttribute('mimeType') || parent.mimeType; - const codecs = elem.getAttribute('codecs') || parent.codecs; + let contentType = elem.attributes['contentType'] || parent.contentType; + const mimeType = elem.attributes['mimeType'] || parent.mimeType; + const codecs = elem.attributes['codecs'] || parent.codecs; const frameRate = - XmlUtils.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate; + TXml.parseAttr(elem, 'frameRate', evalDivision) || parent.frameRate; const pixelAspectRatio = - elem.getAttribute('sar') || parent.pixelAspectRatio; + elem.attributes['sar'] || parent.pixelAspectRatio; const emsgSchemeIdUris = this.emsgSchemeIdUris_( - XmlUtils.findChildren(elem, 'InbandEventStream'), + TXml.findChildren(elem, 'InbandEventStream'), parent.emsgSchemeIdUris); const audioChannelConfigs = - XmlUtils.findChildren(elem, 'AudioChannelConfiguration'); + TXml.findChildren(elem, 'AudioChannelConfiguration'); const numChannels = this.parseAudioChannels_(audioChannelConfigs) || parent.numChannels; const audioSamplingRate = - XmlUtils.parseAttr(elem, 'audioSamplingRate', parseNumber) || + TXml.parseAttr(elem, 'audioSamplingRate', parseNumber) || parent.audioSamplingRate; if (!contentType) { contentType = shaka.dash.DashParser.guessContentType_(mimeType, codecs); } - const segmentBase = XmlUtils.findChild(elem, 'SegmentBase'); - const segmentTemplate = XmlUtils.findChild(elem, 'SegmentTemplate'); + const segmentBase = TXml.findChild(elem, 'SegmentBase'); + const segmentTemplate = TXml.findChild(elem, 'SegmentTemplate'); // The availabilityTimeOffset is the sum of all @availabilityTimeOffset // values that apply to the adaptation set, via BaseURL, SegmentBase, // or SegmentTemplate elements. const segmentBaseAto = segmentBase ? - (XmlUtils.parseAttr(segmentBase, 'availabilityTimeOffset', - XmlUtils.parseFloat) || 0) : 0; + (TXml.parseAttr(segmentBase, 'availabilityTimeOffset', + TXml.parseFloat) || 0) : 0; const segmentTemplateAto = segmentTemplate ? - (XmlUtils.parseAttr(segmentTemplate, 'availabilityTimeOffset', - XmlUtils.parseFloat) || 0) : 0; + (TXml.parseAttr(segmentTemplate, 'availabilityTimeOffset', + TXml.parseFloat) || 0) : 0; const baseUriAto = uriObjs && uriObjs.length ? - (XmlUtils.parseAttr(uriObjs[0], 'availabilityTimeOffset', - XmlUtils.parseFloat) || 0) : 0; + (TXml.parseAttr(uriObjs[0], 'availabilityTimeOffset', + TXml.parseFloat) || 0) : 0; const availabilityTimeOffset = parent.availabilityTimeOffset + baseUriAto + segmentBaseAto + segmentTemplateAto; let segmentSequenceCadence = null; const segmentSequenceProperties = - XmlUtils.findChild(elem, 'SegmentSequenceProperties'); + TXml.findChild(elem, 'SegmentSequenceProperties'); if (segmentSequenceProperties) { - const sap = XmlUtils.findChild(segmentSequenceProperties, 'SAP'); + const sap = TXml.findChild(segmentSequenceProperties, 'SAP'); if (sap) { - segmentSequenceCadence = XmlUtils.parseAttr(sap, 'cadence', - XmlUtils.parseInt); + segmentSequenceCadence = TXml.parseAttr(sap, 'cadence', + TXml.parseInt); } } @@ -1821,10 +1825,10 @@ shaka.dash.DashParser = class { () => ManifestParserUtils.resolveUris(getBaseUris(), getFrameUris()), segmentBase: segmentBase || parent.segmentBase, segmentList: - XmlUtils.findChild(elem, 'SegmentList') || parent.segmentList, + TXml.findChild(elem, 'SegmentList') || parent.segmentList, segmentTemplate: segmentTemplate || parent.segmentTemplate, - width: XmlUtils.parseAttr(elem, 'width', parseNumber) || parent.width, - height: XmlUtils.parseAttr(elem, 'height', parseNumber) || parent.height, + width: TXml.parseAttr(elem, 'width', parseNumber) || parent.width, + height: TXml.parseAttr(elem, 'height', parseNumber) || parent.height, contentType: contentType, mimeType: mimeType, codecs: codecs, @@ -1832,7 +1836,7 @@ shaka.dash.DashParser = class { pixelAspectRatio: pixelAspectRatio, emsgSchemeIdUris: emsgSchemeIdUris, id: id, - language: elem.getAttribute('lang'), + language: elem.attributes['lang'], numChannels: numChannels, audioSamplingRate: audioSamplingRate, availabilityTimeOffset: availabilityTimeOffset, @@ -1846,7 +1850,8 @@ shaka.dash.DashParser = class { * of the ones parsed from inBandEventStreams and the ones provided in * emsgSchemeIdUris. * - * @param {!Array.} inBandEventStreams Array of InbandEventStream + * @param {!Array.} inBandEventStreams + * Array of InbandEventStream * elements to parse and add to the returned array. * @param {!Array.} emsgSchemeIdUris Array of parsed * InbandEventStream schemeIdUri attributes to add to the returned array. @@ -1857,7 +1862,7 @@ shaka.dash.DashParser = class { emsgSchemeIdUris_(inBandEventStreams, emsgSchemeIdUris) { const schemeIdUris = emsgSchemeIdUris.slice(); for (const event of inBandEventStreams) { - const schemeIdUri = event.getAttribute('schemeIdUri'); + const schemeIdUri = event.attributes['schemeIdUri']; if (!schemeIdUris.includes(schemeIdUri)) { schemeIdUris.push(schemeIdUri); } @@ -1866,19 +1871,19 @@ shaka.dash.DashParser = class { } /** - * @param {!Array.} audioChannelConfigs An array of + * @param {!Array.} audioChannelConfigs An array of * AudioChannelConfiguration elements. * @return {?number} The number of audio channels, or null if unknown. * @private */ parseAudioChannels_(audioChannelConfigs) { for (const elem of audioChannelConfigs) { - const scheme = elem.getAttribute('schemeIdUri'); + const scheme = elem.attributes['schemeIdUri']; if (!scheme) { continue; } - const value = elem.getAttribute('value'); + const value = elem.attributes['value']; if (!value) { continue; } @@ -2049,15 +2054,15 @@ shaka.dash.DashParser = class { * Parses an array of UTCTiming elements. * * @param {function():!Array.} getBaseUris - * @param {!Array.} elems + * @param {!Array.} elems * @return {!Promise.} * @private */ async parseUtcTiming_(getBaseUris, elems) { const schemesAndValues = elems.map((elem) => { return { - scheme: elem.getAttribute('schemeIdUri'), - value: elem.getAttribute('value'), + scheme: elem.attributes['schemeIdUri'], + value: elem.attributes['value'], }; }); @@ -2121,23 +2126,23 @@ shaka.dash.DashParser = class { * * @param {number} periodStart * @param {?number} periodDuration - * @param {!Element} elem + * @param {!shaka.extern.xml.Node} elem * @param {number} availabilityStart * @private */ parseEventStream_(periodStart, periodDuration, elem, availabilityStart) { - const XmlUtils = shaka.util.XmlUtils; - const parseNumber = XmlUtils.parseNonNegativeInt; + const TXml = shaka.util.TXml; + const parseNumber = shaka.util.TXml.parseNonNegativeInt; - const schemeIdUri = elem.getAttribute('schemeIdUri') || ''; - const value = elem.getAttribute('value') || ''; - const timescale = XmlUtils.parseAttr(elem, 'timescale', parseNumber) || 1; + const schemeIdUri = elem.attributes['schemeIdUri'] || ''; + const value = elem.attributes['value'] || ''; + const timescale = TXml.parseAttr(elem, 'timescale', parseNumber) || 1; - for (const eventNode of XmlUtils.findChildren(elem, 'Event')) { + for (const eventNode of TXml.findChildren(elem, 'Event')) { const presentationTime = - XmlUtils.parseAttr(eventNode, 'presentationTime', parseNumber) || 0; + TXml.parseAttr(eventNode, 'presentationTime', parseNumber) || 0; const duration = - XmlUtils.parseAttr(eventNode, 'duration', parseNumber) || 0; + TXml.parseAttr(eventNode, 'duration', parseNumber) || 0; let startTime = presentationTime / timescale + periodStart; let endTime = startTime + (duration / timescale); @@ -2159,7 +2164,7 @@ shaka.dash.DashParser = class { value: value, startTime: startTime, endTime: endTime, - id: eventNode.getAttribute('id') || '', + id: eventNode.attributes['id'] || '', eventElement: eventNode, }; @@ -2248,9 +2253,9 @@ shaka.dash.DashParser.RequestSegmentCallback; /** * @typedef {{ - * segmentBase: Element, - * segmentList: Element, - * segmentTemplate: Element, + * segmentBase: ?shaka.extern.xml.Node, + * segmentList: ?shaka.extern.xml.Node, + * segmentTemplate: ?shaka.extern.xml.Node, * getBaseUris: function():!Array., * width: (number|undefined), * height: (number|undefined), @@ -2272,11 +2277,11 @@ shaka.dash.DashParser.RequestSegmentCallback; * A collection of elements and properties which are inherited across levels * of a DASH manifest. * - * @property {Element} segmentBase + * @property {?shaka.extern.xml.Node} segmentBase * The XML node for SegmentBase. - * @property {Element} segmentList + * @property {?shaka.extern.xml.Node} segmentList * The XML node for SegmentList. - * @property {Element} segmentTemplate + * @property {?shaka.extern.xml.Node} segmentTemplate * The XML node for SegmentTemplate. * @property {function():!Array.} getBaseUris * Function than returns an array of absolute base URIs for the frame. @@ -2361,7 +2366,7 @@ shaka.dash.DashParser.Context; * @typedef {{ * start: number, * duration: ?number, - * node: !Element, + * node: !shaka.extern.xml.Node, * isLastPeriod: boolean * }} * @@ -2373,7 +2378,7 @@ shaka.dash.DashParser.Context; * @property {?number} duration * The duration of the period; or null if the duration is not given. This * will be non-null for all periods except the last. - * @property {!Element} node + * @property {!shaka.extern.xml.Node} node * The XML Node for the Period. * @property {boolean} isLastPeriod * Whether this Period is the last one in the manifest. diff --git a/lib/dash/mpd_utils.js b/lib/dash/mpd_utils.js index 4a31dc8c46..14f00e2d52 100644 --- a/lib/dash/mpd_utils.js +++ b/lib/dash/mpd_utils.js @@ -13,7 +13,7 @@ goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); @@ -123,7 +123,7 @@ shaka.dash.MpdUtils = class { * Expands a SegmentTimeline into an array-based timeline. The results are in * seconds. * - * @param {!Element} segmentTimeline + * @param {!shaka.extern.xml.Node} segmentTimeline * @param {number} timescale * @param {number} unscaledPresentationTimeOffset * @param {number} periodDuration The Period's duration in seconds. @@ -141,9 +141,9 @@ shaka.dash.MpdUtils = class { periodDuration > 0, 'period duration must be a positive integer'); // Alias. - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; - const timePoints = XmlUtils.findChildren(segmentTimeline, 'S'); + const timePoints = TXml.findChildren(segmentTimeline, 'S'); /** @type {!Array.} */ const timeline = []; @@ -152,12 +152,12 @@ shaka.dash.MpdUtils = class { for (let i = 0; i < timePoints.length; ++i) { const timePoint = timePoints[i]; const next = timePoints[i + 1]; - let t = XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt); + let t = TXml.parseAttr(timePoint, 't', TXml.parseNonNegativeInt); const d = - XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt); - const r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt); + TXml.parseAttr(timePoint, 'd', TXml.parseNonNegativeInt); + const r = TXml.parseAttr(timePoint, 'r', TXml.parseInt); - const k = XmlUtils.parseAttr(timePoint, 'k', XmlUtils.parseInt); + const k = TXml.parseAttr(timePoint, 'k', TXml.parseInt); const partialSegments = k || 0; @@ -179,7 +179,7 @@ shaka.dash.MpdUtils = class { if (repeat < 0) { if (next) { const nextStartTime = - XmlUtils.parseAttr(next, 't', XmlUtils.parseNonNegativeInt); + TXml.parseAttr(next, 't', TXml.parseNonNegativeInt); if (nextStartTime == null) { shaka.log.warning( 'An "S" element cannot have a negative repeat', @@ -260,7 +260,8 @@ shaka.dash.MpdUtils = class { * Parses common segment info for SegmentList and SegmentTemplate. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * Gets the element that contains the segment info. * @return {shaka.dash.MpdUtils.SegmentInfo} */ @@ -269,23 +270,23 @@ shaka.dash.MpdUtils = class { callback(context.representation), 'There must be at least one element of the given type.'); const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const timescaleStr = MpdUtils.inheritAttribute(context, callback, 'timescale'); let timescale = 1; if (timescaleStr) { - timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1; + timescale = TXml.parsePositiveInt(timescaleStr) || 1; } const durationStr = MpdUtils.inheritAttribute(context, callback, 'duration'); - let segmentDuration = XmlUtils.parsePositiveInt(durationStr || ''); + let segmentDuration = TXml.parsePositiveInt(durationStr || ''); const ContentType = shaka.util.ManifestParserUtils.ContentType; // TODO: The specification is not clear, check this once it is resolved: // https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/404 if (context.representation.contentType == ContentType.IMAGE) { - segmentDuration = XmlUtils.parseFloat(durationStr || ''); + segmentDuration = TXml.parseFloat(durationStr || ''); } if (segmentDuration) { segmentDuration /= timescale; @@ -296,7 +297,7 @@ shaka.dash.MpdUtils = class { const unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute(context, callback, 'presentationTimeOffset')) || 0; - let startNumber = XmlUtils.parseNonNegativeInt(startNumberStr || ''); + let startNumber = TXml.parseNonNegativeInt(startNumberStr || ''); if (startNumberStr == null || startNumber == null) { startNumber = 1; } @@ -326,8 +327,9 @@ shaka.dash.MpdUtils = class { /** * Parses common attributes for Representation, AdaptationSet, and Period. * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback - * @return {!Array.} + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback + * @return {!Array.} */ static getNodes(context, callback) { const Functional = shaka.util.Functional; @@ -347,7 +349,8 @@ shaka.dash.MpdUtils = class { * Searches the inheritance for a Segment* with the given attribute. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * Gets the Element that contains the attribute to inherit. * @param {string} attribute * @return {?string} @@ -358,7 +361,7 @@ shaka.dash.MpdUtils = class { let result = null; for (const node of nodes) { - result = node.getAttribute(attribute); + result = node.attributes[attribute]; if (result) { break; } @@ -370,19 +373,20 @@ shaka.dash.MpdUtils = class { * Searches the inheritance for a Segment* with the given child. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * Gets the Element that contains the child to inherit. * @param {string} child - * @return {Element} + * @return {?shaka.extern.xml.Node} */ static inheritChild(context, callback, child) { const MpdUtils = shaka.dash.MpdUtils; const nodes = MpdUtils.getNodes(context, callback); - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; let result = null; for (const node of nodes) { - result = XmlUtils.findChild(node, child); + result = TXml.findChild(node, child); if (result) { break; } @@ -395,33 +399,35 @@ shaka.dash.MpdUtils = class { * It also strips the xlink properties off of the element, * even if the process fails. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.extern.RetryParameters} retryParameters * @param {boolean} failGracefully * @param {string} baseUri * @param {!shaka.net.NetworkingEngine} networkingEngine * @param {number} linkDepth - * @return {!shaka.util.AbortableOperation.} + * @return {!shaka.util.AbortableOperation.} * @private */ static handleXlinkInElement_( element, retryParameters, failGracefully, baseUri, networkingEngine, linkDepth) { const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const Error = shaka.util.Error; const ManifestParserUtils = shaka.util.ManifestParserUtils; const NS = MpdUtils.XlinkNamespaceUri_; - const xlinkHref = XmlUtils.getAttributeNS(element, NS, 'href'); + const xlinkHref = TXml.getAttributeNS(element, NS, 'href'); const xlinkActuate = - XmlUtils.getAttributeNS(element, NS, 'actuate') || 'onRequest'; + TXml.getAttributeNS(element, NS, 'actuate') || 'onRequest'; // Remove the xlink properties, so it won't download again // when re-processed. - for (const attribute of Array.from(element.attributes)) { - if (attribute.namespaceURI == NS) { - element.removeAttributeNS(attribute.namespaceURI, attribute.localName); + for (const key of Object.keys(element.attributes)) { + const segs = key.split(':'); + const namespace = shaka.util.TXml.getKnownNameSpace(NS); + if (segs[0] == namespace) { + delete element.attributes[key]; } } @@ -465,7 +471,7 @@ shaka.dash.MpdUtils = class { // top-level element. If there are multiple roots, it will be // rejected. const rootElem = - shaka.util.XmlUtils.parseXml(response.data, element.tagName); + TXml.parseXml(response.data, element.tagName); if (!rootElem) { // It was not valid XML. return shaka.util.AbortableOperation.failed(new Error( @@ -477,20 +483,20 @@ shaka.dash.MpdUtils = class { // the element can be changed further. // Remove the current contents of the node. - while (element.childNodes.length) { - element.removeChild(element.childNodes[0]); - } + element.children = []; // Move the children of the loaded xml into the current element. - while (rootElem.childNodes.length) { - const child = rootElem.childNodes[0]; - rootElem.removeChild(child); - element.appendChild(child); + while (rootElem.children.length) { + const child = rootElem.children.shift(); + if (TXml.isNode(child)) { + child.parent = element; + } + element.children.push(child); } // Move the attributes of the loaded xml into the current element. - for (const attribute of Array.from(rootElem.attributes)) { - element.setAttributeNode(attribute.cloneNode(/* deep= */ false)); + for (const key of Object.keys(rootElem.attributes)) { + element.attributes[key] = rootElem.attributes[key]; } return shaka.dash.MpdUtils.processXlinks( @@ -503,25 +509,26 @@ shaka.dash.MpdUtils = class { * Filter the contents of a node recursively, replacing xlink links * with their associated online data. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.extern.RetryParameters} retryParameters * @param {boolean} failGracefully * @param {string} baseUri * @param {!shaka.net.NetworkingEngine} networkingEngine * @param {number=} linkDepth, default set to 0 - * @return {!shaka.util.AbortableOperation.} + * @return {!shaka.util.AbortableOperation.} */ static processXlinks( - element, retryParameters, failGracefully, baseUri, networkingEngine, + element, retryParameters, + failGracefully, baseUri, networkingEngine, linkDepth = 0) { const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const NS = MpdUtils.XlinkNamespaceUri_; - if (XmlUtils.getAttributeNS(element, NS, 'href')) { + if (TXml.getAttributeNS(element, NS, 'href')) { let handled = MpdUtils.handleXlinkInElement_( - element, retryParameters, failGracefully, baseUri, networkingEngine, - linkDepth); + element, retryParameters, failGracefully, + baseUri, networkingEngine, linkDepth); if (failGracefully) { // Catch any error and go on. handled = handled.chain(undefined, (error) => { @@ -537,23 +544,23 @@ shaka.dash.MpdUtils = class { } const childOperations = []; - for (const child of Array.from(element.childNodes)) { - if (child instanceof Element) { - const resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013'; - if (XmlUtils.getAttributeNS(child, NS, 'href') == resolveToZeroString) { - // This is a 'resolve to zero' code; it means the element should - // be removed, as specified by the mpeg-dash rules for xlink. - element.removeChild(child); - } else if (child.tagName != 'SegmentTimeline') { - // Don't recurse into a SegmentTimeline since xlink attributes - // aren't valid in there and looking at each segment can take a long - // time with larger manifests. - - // Replace the child with its processed form. - childOperations.push(shaka.dash.MpdUtils.processXlinks( - /** @type {!Element} */ (child), retryParameters, failGracefully, - baseUri, networkingEngine, linkDepth)); - } + for (const child of shaka.util.TXml.getChildNodes(element)) { + const resolveToZeroString = 'urn:mpeg:dash:resolve-to-zero:2013'; + if (TXml.getAttributeNS(child, NS, 'href') == resolveToZeroString) { + // This is a 'resolve to zero' code; it means the element should + // be removed, as specified by the mpeg-dash rules for xlink. + element.children = element.children.filter( + (elem) => elem !== child); + } else if (child.tagName != 'SegmentTimeline') { + // Don't recurse into a SegmentTimeline since xlink attributes + // aren't valid in there and looking at each segment can take a long + // time with larger manifests. + + // Replace the child with its processed form. + childOperations.push(shaka.dash.MpdUtils.processXlinks( + /** @type {!shaka.extern.xml.Node} */ (child), + retryParameters, failGracefully, + baseUri, networkingEngine, linkDepth)); } } diff --git a/lib/dash/segment_base.js b/lib/dash/segment_base.js index a5f53d13e2..eaee5006af 100644 --- a/lib/dash/segment_base.js +++ b/lib/dash/segment_base.js @@ -16,7 +16,7 @@ goog.require('shaka.media.WebmSegmentIndexParser'); goog.require('shaka.util.Error'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.ObjectUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); goog.requireType('shaka.media.SegmentReference'); @@ -30,13 +30,14 @@ shaka.dash.SegmentBase = class { * Creates an init segment reference from a Context object. * * @param {shaka.dash.DashParser.Context} context - * @param {function(?shaka.dash.DashParser.InheritanceFrame):Element} callback + * @param {function(?shaka.dash.DashParser.InheritanceFrame): + * ?shaka.extern.xml.Node} callback * @param {shaka.extern.aesKey|undefined} aesKey * @return {shaka.media.InitSegmentReference} */ static createInitSegment(context, callback, aesKey) { const MpdUtils = shaka.dash.MpdUtils; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ManifestParserUtils = shaka.util.ManifestParserUtils; const initialization = @@ -46,15 +47,14 @@ shaka.dash.SegmentBase = class { } let resolvedUris = context.representation.getBaseUris(); - const uri = initialization.getAttribute('sourceURL'); + const uri = initialization.attributes['sourceURL']; if (uri) { resolvedUris = ManifestParserUtils.resolveUris(resolvedUris, [uri]); } let startByte = 0; let endByte = null; - const range = - XmlUtils.parseAttr(initialization, 'range', XmlUtils.parseRange); + const range = TXml.parseAttr(initialization, 'range', TXml.parseRange); if (range) { startByte = range.start; endByte = range.end; @@ -87,7 +87,7 @@ shaka.dash.SegmentBase = class { // the initial parse. const MpdUtils = shaka.dash.MpdUtils; const SegmentBase = shaka.dash.SegmentBase; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const unscaledPresentationTimeOffset = Number(MpdUtils.inheritAttribute( context, SegmentBase.fromInheritance_, 'presentationTimeOffset')) || 0; @@ -96,7 +96,7 @@ shaka.dash.SegmentBase = class { context, SegmentBase.fromInheritance_, 'timescale'); let timescale = 1; if (timescaleStr) { - timescale = XmlUtils.parsePositiveInt(timescaleStr) || 1; + timescale = TXml.parsePositiveInt(timescaleStr) || 1; } const scaledPresentationTimeOffset = @@ -201,7 +201,7 @@ shaka.dash.SegmentBase = class { /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame - * @return {Element} + * @return {?shaka.extern.xml.Node} * @private */ static fromInheritance_(frame) { @@ -218,17 +218,17 @@ shaka.dash.SegmentBase = class { static computeIndexRange_(context) { const MpdUtils = shaka.dash.MpdUtils; const SegmentBase = shaka.dash.SegmentBase; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const representationIndex = MpdUtils.inheritChild( context, SegmentBase.fromInheritance_, 'RepresentationIndex'); const indexRangeElem = MpdUtils.inheritAttribute( context, SegmentBase.fromInheritance_, 'indexRange'); - let indexRange = XmlUtils.parseRange(indexRangeElem || ''); + let indexRange = TXml.parseRange(indexRangeElem || ''); if (representationIndex) { - indexRange = XmlUtils.parseAttr( - representationIndex, 'range', XmlUtils.parseRange, indexRange); + indexRange = TXml.parseAttr( + representationIndex, 'range', TXml.parseRange, indexRange); } return indexRange; } @@ -250,7 +250,7 @@ shaka.dash.SegmentBase = class { let indexUris = context.representation.getBaseUris(); if (representationIndex) { - const representationUri = representationIndex.getAttribute('sourceURL'); + const representationUri = representationIndex.attributes['sourceURL']; if (representationUri) { indexUris = ManifestParserUtils.resolveUris( indexUris, [representationUri]); diff --git a/lib/dash/segment_list.js b/lib/dash/segment_list.js index 5b3fb4153f..7a4dbcfa52 100644 --- a/lib/dash/segment_list.js +++ b/lib/dash/segment_list.js @@ -16,7 +16,7 @@ goog.require('shaka.media.SegmentReference'); goog.require('shaka.util.Error'); goog.require('shaka.util.Functional'); goog.require('shaka.util.ManifestParserUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); goog.requireType('shaka.dash.DashParser'); goog.requireType('shaka.media.PresentationTimeline'); @@ -94,7 +94,7 @@ shaka.dash.SegmentList = class { /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame - * @return {Element} + * @return {?shaka.extern.xml.Node} * @private */ static fromInheritance_(frame) { @@ -282,21 +282,21 @@ shaka.dash.SegmentList = class { */ static parseMediaSegments_(context) { const Functional = shaka.util.Functional; - /** @type {!Array.} */ + /** @type {!Array.} */ const segmentLists = [ context.representation.segmentList, context.adaptationSet.segmentList, context.period.segmentList, ].filter(Functional.isNotNull); - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; // Search each SegmentList for one with at least one SegmentURL element, // select the first one, and convert each SegmentURL element to a tuple. return segmentLists - .map((node) => { return XmlUtils.findChildren(node, 'SegmentURL'); }) + .map((node) => { return TXml.findChildren(node, 'SegmentURL'); }) .reduce((all, part) => { return all.length > 0 ? all : part; }) .map((urlNode) => { - if (urlNode.getAttribute('indexRange') && + if (urlNode.attributes['indexRange'] && !context.indexRangeWarningGiven) { context.indexRangeWarningGiven = true; shaka.log.warning( @@ -305,9 +305,9 @@ shaka.dash.SegmentList = class { 'attribute or SegmentTimeline, which must be accurate.'); } - const uri = urlNode.getAttribute('media'); - const range = XmlUtils.parseAttr( - urlNode, 'mediaRange', XmlUtils.parseRange, + const uri = urlNode.attributes['media']; + const range = TXml.parseAttr( + urlNode, 'mediaRange', TXml.parseRange, {start: 0, end: null}); return {mediaUri: uri, start: range.start, end: range.end}; }); diff --git a/lib/dash/segment_template.js b/lib/dash/segment_template.js index 7fd3d18dad..fb66516cde 100644 --- a/lib/dash/segment_template.js +++ b/lib/dash/segment_template.js @@ -178,7 +178,7 @@ shaka.dash.SegmentTemplate = class { /** * @param {?shaka.dash.DashParser.InheritanceFrame} frame - * @return {Element} + * @return {?shaka.extern.xml.Node} * @private */ static fromInheritance_(frame) { diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 4259815660..47f49cc5e2 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -36,9 +36,9 @@ goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Pssh'); goog.require('shaka.media.SegmentUtils'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Platform'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); goog.requireType('shaka.hls.Segment'); @@ -3169,7 +3169,7 @@ shaka.hls.HlsParser = class { const dateTimeTag = shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME'); if (dateTimeTag && dateTimeTag.value) { - syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value); + syncTime = shaka.util.TXml.parseDate(dateTimeTag.value); goog.asserts.assert(syncTime != null, 'EXT-X-PROGRAM-DATE-TIME format not valid'); } diff --git a/lib/media/drm_engine.js b/lib/media/drm_engine.js index c3320c11f9..fbe56fd4d5 100644 --- a/lib/media/drm_engine.js +++ b/lib/media/drm_engine.js @@ -27,8 +27,8 @@ goog.require('shaka.util.PublicPromise'); goog.require('shaka.util.StreamUtils'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.Timer'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); /** @implements {shaka.util.IDestroyable} */ @@ -1625,6 +1625,7 @@ shaka.media.DrmEngine = class { // // // + const TXml = shaka.util.TXml; const xml = shaka.util.StringUtils.fromUTF16( request.body, /* littleEndian= */ true, /* noThrow= */ true); @@ -1638,24 +1639,28 @@ shaka.media.DrmEngine = class { return; } shaka.log.debug('Unwrapping PlayReady request.'); - const dom = shaka.util.XmlUtils.parseXmlString(xml, 'PlayReadyKeyMessage'); + const dom = TXml.parseXmlString(xml, 'PlayReadyKeyMessage'); goog.asserts.assert(dom, 'Failed to parse PlayReady XML!'); // Set request headers. - const headers = dom.getElementsByTagName('HttpHeader'); + const headers = TXml.getElementsByTagName(dom, 'HttpHeader'); for (const header of headers) { - const name = header.getElementsByTagName('name')[0]; - const value = header.getElementsByTagName('value')[0]; + const name = TXml.getElementsByTagName(header, 'name')[0]; + const value = TXml.getElementsByTagName(header, 'value')[0]; goog.asserts.assert(name && value, 'Malformed PlayReady headers!'); - request.headers[name.textContent] = value.textContent; + request.headers[ + /** @type {string} */(shaka.util.TXml.getTextContents(name))] = + /** @type {string} */(shaka.util.TXml.getTextContents(value)); } // Unpack the base64-encoded challenge. - const challenge = dom.getElementsByTagName('Challenge')[0]; - goog.asserts.assert(challenge, 'Malformed PlayReady challenge!'); - goog.asserts.assert(challenge.getAttribute('encoding') == 'base64encoded', + const challenge = TXml.getElementsByTagName(dom, 'Challenge')[0]; + goog.asserts.assert(challenge, + 'Malformed PlayReady challenge!'); + goog.asserts.assert(challenge.attributes['encoding'] == 'base64encoded', 'Unexpected PlayReady challenge encoding!'); - request.body = shaka.util.Uint8ArrayUtils.fromBase64(challenge.textContent); + request.body = shaka.util.Uint8ArrayUtils.fromBase64( + /** @type{string} */(shaka.util.TXml.getTextContents(challenge))); } /** diff --git a/lib/mss/content_protection.js b/lib/mss/content_protection.js index 90cb5a5371..a7e3edd67b 100644 --- a/lib/mss/content_protection.js +++ b/lib/mss/content_protection.js @@ -11,8 +11,8 @@ goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Pssh'); goog.require('shaka.util.StringUtils'); +goog.require('shaka.util.TXml'); goog.require('shaka.util.Uint8ArrayUtils'); -goog.require('shaka.util.XmlUtils'); /** @@ -23,19 +23,19 @@ shaka.mss.ContentProtection = class { /** * Parses info from the Protection elements. * - * @param {!Array.} elems + * @param {!Array.} elems * @param {!Object.} keySystemsBySystemId * @return {!Array.} */ static parseFromProtection(elems, keySystemsBySystemId) { const ContentProtection = shaka.mss.ContentProtection; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; - /** @type {!Array.} */ + /** @type {!Array.} */ let protectionHeader = []; for (const elem of elems) { protectionHeader = protectionHeader.concat( - XmlUtils.findChildren(elem, 'ProtectionHeader')); + TXml.findChildren(elem, 'ProtectionHeader')); } if (!protectionHeader.length) { return []; @@ -123,18 +123,19 @@ shaka.mss.ContentProtection = class { * Parse a PlayReady Header format: https://goo.gl/dBzxNA * a try to find the LA_URL value. * - * @param {!Element} xml + * @param {!shaka.extern.xml.Node} xml * @return {string} * @private */ static getLaurl_(xml) { + const TXml = shaka.util.TXml; // LA_URL element is optional and no more than one is // allowed inside the DATA element. Only absolute URLs are allowed. // If the LA_URL element exists, it must not be empty. - for (const elem of xml.getElementsByTagName('DATA')) { - const laUrl = shaka.util.XmlUtils.findChild(elem, 'LA_URL'); + for (const elem of TXml.getElementsByTagName(xml, 'DATA')) { + const laUrl = TXml.findChild(elem, 'LA_URL'); if (laUrl) { - return laUrl.textContent; + return /** @type {string} */ (shaka.util.TXml.getTextContents(laUrl)); } } @@ -148,7 +149,7 @@ shaka.mss.ContentProtection = class { * Gets a PlayReady license URL from a protection element * containing a PlayReady Header Object * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @return {string} */ static getPlayReadyLicenseUrl(element) { @@ -165,19 +166,21 @@ shaka.mss.ContentProtection = class { * Parse a PlayReady Header format: https://goo.gl/dBzxNA * a try to find the KID value. * - * @param {!Element} xml + * @param {!shaka.extern.xml.Node} xml * @return {?string} * @private */ static getKID_(xml) { + const TXml = shaka.util.TXml; // KID element is optional and no more than one is // allowed inside the DATA element. - for (const elem of xml.getElementsByTagName('DATA')) { - const kid = shaka.util.XmlUtils.findChild(elem, 'KID'); + for (const elem of TXml.getElementsByTagName(xml, 'DATA')) { + const kid = TXml.findChild(elem, 'KID'); if (kid) { // GUID: [DWORD, WORD, WORD, 8-BYTE] const guidBytes = - shaka.util.Uint8ArrayUtils.fromBase64(kid.textContent); + shaka.util.Uint8ArrayUtils.fromBase64( + /** @type{string} */ (shaka.util.TXml.getTextContents(kid))); // Reverse byte order from little-endian to big-endian const kidBytes = new Uint8Array([ guidBytes[3], guidBytes[2], guidBytes[1], guidBytes[0], @@ -197,7 +200,7 @@ shaka.mss.ContentProtection = class { * Gets a PlayReady KID from a protection element * containing a PlayReady Header Object * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @return {?string} * @private */ @@ -214,15 +217,16 @@ shaka.mss.ContentProtection = class { /** * Gets a PlayReady Header Object from a protection element * - * @param {!Element} element - * @return {?Element} + * @param {!shaka.extern.xml.Node} element + * @return {?shaka.extern.xml.Node} * @private */ static getPlayReadyHeaderObject_(element) { const ContentProtection = shaka.mss.ContentProtection; const PLAYREADY_RECORD_TYPES = ContentProtection.PLAYREADY_RECORD_TYPES; - const bytes = shaka.util.Uint8ArrayUtils.fromBase64(element.textContent); + const bytes = shaka.util.Uint8ArrayUtils.fromBase64( + /** @type{string} */ (shaka.util.TXml.getTextContents(element))); const records = ContentProtection.parseMsPro_(bytes); const record = records.filter((record) => { return record.type === PLAYREADY_RECORD_TYPES.RIGHTS_MANAGEMENT; @@ -233,7 +237,7 @@ shaka.mss.ContentProtection = class { } const xml = shaka.util.StringUtils.fromUTF16(record.value, true); - const rootElement = shaka.util.XmlUtils.parseXmlString(xml, 'WRMHEADER'); + const rootElement = shaka.util.TXml.parseXmlString(xml, 'WRMHEADER'); if (!rootElement) { return null; } @@ -243,7 +247,7 @@ shaka.mss.ContentProtection = class { /** * Gets a initData from a protection element. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {string} systemID * @param {?string} keyId * @return {?Array.} @@ -251,7 +255,8 @@ shaka.mss.ContentProtection = class { */ static getInitDataFromPro_(element, systemID, keyId) { const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; - const data = Uint8ArrayUtils.fromBase64(element.textContent); + const data = Uint8ArrayUtils.fromBase64( + /** @type{string} */ (shaka.util.TXml.getTextContents(element))); const systemId = Uint8ArrayUtils.fromHex(systemID.replace(/-/g, '')); const keyIds = new Set(); const psshVersion = 0; @@ -269,7 +274,7 @@ shaka.mss.ContentProtection = class { /** * Creates DrmInfo objects from an array of elements. * - * @param {!Array.} elements + * @param {!Array.} elements * @param {!Object.} keySystemsBySystemId * @return {!Array.} * @private @@ -284,7 +289,7 @@ shaka.mss.ContentProtection = class { for (let i = 0; i < elements.length; i++) { const element = elements[i]; - const systemID = element.getAttribute('SystemID').toLowerCase(); + const systemID = element.attributes['SystemID'].toLowerCase(); const keySystem = keySystemsBySystemId[systemID]; if (keySystem) { const KID = ContentProtection.getPlayReadyKID_(element); @@ -338,7 +343,7 @@ shaka.mss.ContentProtection.PLAYREADY_RECORD_TYPES = { /** * A map of key system name to license server url parser. * - * @const {!Map.} + * @const {!Map.} * @private */ shaka.mss.ContentProtection.licenseUrlParsers_ = new Map() diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index fb16df7a22..87e75832eb 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -22,7 +22,7 @@ goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.Mp4Generator'); goog.require('shaka.util.OperationManager'); goog.require('shaka.util.Timer'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -277,7 +277,7 @@ shaka.mss.MssParser = class { * @private */ parseManifest_(data, finalManifestUri) { - const mss = shaka.util.XmlUtils.parseXml(data, 'SmoothStreamingMedia'); + const mss = shaka.util.TXml.parseXml(data, 'SmoothStreamingMedia'); if (!mss) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -293,13 +293,13 @@ shaka.mss.MssParser = class { /** * Takes a formatted MSS and converts it into a manifest. * - * @param {!Element} mss + * @param {!shaka.extern.xml.Node} mss * @param {string} finalManifestUri The final manifest URI, which may * differ from this.manifestUri_ if there has been a redirect. * @private */ processManifest_(mss, finalManifestUri) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const manifestPreprocessor = this.config_.mss.manifestPreprocessor; if (manifestPreprocessor) { @@ -311,8 +311,8 @@ shaka.mss.MssParser = class { /* presentationStartTime= */ null, /* delay= */ 0); } - const isLive = XmlUtils.parseAttr(mss, 'IsLive', - XmlUtils.parseBoolean, /* defaultValue= */ false); + const isLive = TXml.parseAttr(mss, 'IsLive', + TXml.parseBoolean, /* defaultValue= */ false); if (isLive) { throw new shaka.util.Error( @@ -323,21 +323,21 @@ shaka.mss.MssParser = class { this.presentationTimeline_.setStatic(!isLive); - const timescale = XmlUtils.parseAttr(mss, 'TimeScale', - XmlUtils.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_); + const timescale = TXml.parseAttr(mss, 'TimeScale', + TXml.parseNonNegativeInt, shaka.mss.MssParser.DEFAULT_TIME_SCALE_); goog.asserts.assert(timescale && timescale >= 0, 'Timescale must be defined!'); - let dvrWindowLength = XmlUtils.parseAttr(mss, 'DVRWindowLength', - XmlUtils.parseNonNegativeInt); + let dvrWindowLength = TXml.parseAttr(mss, 'DVRWindowLength', + TXml.parseNonNegativeInt); // If the DVRWindowLength field is omitted for a live presentation or set // to 0, the DVR window is effectively infinite if (isLive && (dvrWindowLength === 0 || isNaN(dvrWindowLength))) { dvrWindowLength = Infinity; } // Start-over - const canSeek = XmlUtils.parseAttr(mss, 'CanSeek', - XmlUtils.parseBoolean, /* defaultValue= */ false); + const canSeek = TXml.parseAttr(mss, 'CanSeek', + TXml.parseBoolean, /* defaultValue= */ false); if (dvrWindowLength === 0 && canSeek) { dvrWindowLength = Infinity; } @@ -362,8 +362,8 @@ shaka.mss.MssParser = class { segmentAvailabilityDuration); // Duration in timescale units. - const duration = XmlUtils.parseAttr(mss, 'Duration', - XmlUtils.parseNonNegativeInt, Infinity); + const duration = TXml.parseAttr(mss, 'Duration', + TXml.parseNonNegativeInt, Infinity); goog.asserts.assert(duration && duration >= 0, 'Duration must be defined!'); @@ -413,25 +413,25 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} mss + * @param {!shaka.extern.xml.Node} mss * @param {!shaka.mss.MssParser.Context} context * @private */ parseStreamIndexes_(mss, context) { const ContentProtection = shaka.mss.ContentProtection; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; - const protectionElems = XmlUtils.findChildren(mss, 'Protection'); + const protectionElems = TXml.findChildren(mss, 'Protection'); const drmInfos = ContentProtection.parseFromProtection( protectionElems, this.config_.mss.keySystemsBySystemId); const audioStreams = []; const videoStreams = []; const textStreams = []; - const streamIndexes = XmlUtils.findChildren(mss, 'StreamIndex'); + const streamIndexes = TXml.findChildren(mss, 'StreamIndex'); for (const streamIndex of streamIndexes) { - const qualityLevels = XmlUtils.findChildren(streamIndex, 'QualityLevel'); + const qualityLevels = TXml.findChildren(streamIndex, 'QualityLevel'); const timeline = this.createTimeline_( streamIndex, context.timescale, context.duration); // For each QualityLevel node, create a stream element @@ -466,8 +466,8 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} streamIndex - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} streamIndex + * @param {!shaka.extern.xml.Node} qualityLevel * @param {!Array.} timeline * @param {!Array.} drmInfos * @param {!shaka.mss.MssParser.Context} context @@ -475,11 +475,11 @@ shaka.mss.MssParser = class { * @private */ createStream_(streamIndex, qualityLevel, timeline, drmInfos, context) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ContentType = shaka.util.ManifestParserUtils.ContentType; const MssParser = shaka.mss.MssParser; - const type = streamIndex.getAttribute('Type'); + const type = streamIndex.attributes['Type']; const isValidType = type === 'audio' || type === 'video' || type === 'text'; if (!isValidType) { @@ -487,19 +487,19 @@ shaka.mss.MssParser = class { return null; } - const lang = streamIndex.getAttribute('Language'); + const lang = streamIndex.attributes['Language']; const id = this.globalId_++; - const bandwidth = XmlUtils.parseAttr( - qualityLevel, 'Bitrate', XmlUtils.parsePositiveInt); - const width = XmlUtils.parseAttr( - qualityLevel, 'MaxWidth', XmlUtils.parsePositiveInt); - const height = XmlUtils.parseAttr( - qualityLevel, 'MaxHeight', XmlUtils.parsePositiveInt); - const channelsCount = XmlUtils.parseAttr( - qualityLevel, 'Channels', XmlUtils.parsePositiveInt); - const audioSamplingRate = XmlUtils.parseAttr( - qualityLevel, 'SamplingRate', XmlUtils.parsePositiveInt); + const bandwidth = TXml.parseAttr( + qualityLevel, 'Bitrate', TXml.parsePositiveInt); + const width = TXml.parseAttr( + qualityLevel, 'MaxWidth', TXml.parsePositiveInt); + const height = TXml.parseAttr( + qualityLevel, 'MaxHeight', TXml.parsePositiveInt); + const channelsCount = TXml.parseAttr( + qualityLevel, 'Channels', TXml.parsePositiveInt); + const audioSamplingRate = TXml.parseAttr( + qualityLevel, 'SamplingRate', TXml.parsePositiveInt); let duration = context.duration; if (timeline.length) { @@ -515,7 +515,7 @@ shaka.mss.MssParser = class { /** @type {!shaka.extern.Stream} */ const stream = { id: id, - originalId: streamIndex.getAttribute('Name') || String(id), + originalId: streamIndex.attributes['Name'] || String(id), groupId: null, createSegmentIndex: () => Promise.resolve(), closeSegmentIndex: () => Promise.resolve(), @@ -559,7 +559,7 @@ shaka.mss.MssParser = class { }; // This is specifically for text tracks. - const subType = streamIndex.getAttribute('Subtype'); + const subType = streamIndex.attributes['Subtype']; if (subType) { const role = MssParser.ROLE_MAPPING_[subType]; if (role) { @@ -570,12 +570,12 @@ shaka.mss.MssParser = class { } } - let fourCCValue = qualityLevel.getAttribute('FourCC'); + let fourCCValue = qualityLevel.attributes['FourCC']; // If FourCC not defined at QualityLevel level, // then get it from StreamIndex level if (fourCCValue === null || fourCCValue === '') { - fourCCValue = streamIndex.getAttribute('FourCC'); + fourCCValue = streamIndex.attributes['FourCC']; } // If still not defined (optional for audio stream, @@ -694,7 +694,7 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} qualityLevel * @param {string} type * @param {string} fourCCValue * @param {!shaka.extern.Stream} stream @@ -702,7 +702,7 @@ shaka.mss.MssParser = class { * @private */ getCodecPrivateData_(qualityLevel, type, fourCCValue, stream) { - const codecPrivateData = qualityLevel.getAttribute('CodecPrivateData'); + const codecPrivateData = qualityLevel.attributes['CodecPrivateData']; if (codecPrivateData) { return codecPrivateData; } @@ -780,7 +780,7 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} qualityLevel * @param {string} fourCCValue * @param {?string} codecPrivateData * @return {string} @@ -809,7 +809,7 @@ shaka.mss.MssParser = class { } /** - * @param {!Element} qualityLevel + * @param {!shaka.extern.xml.Node} qualityLevel * @param {?string} codecPrivateData * @return {string} * @private @@ -835,14 +835,14 @@ shaka.mss.MssParser = class { /** * @param {!shaka.media.InitSegmentReference} initSegmentRef * @param {!shaka.extern.Stream} stream - * @param {!Element} streamIndex + * @param {!shaka.extern.xml.Node} streamIndex * @param {!Array.} timeline * @return {!Array.} * @private */ createSegments_(initSegmentRef, stream, streamIndex, timeline) { const ManifestParserUtils = shaka.util.ManifestParserUtils; - const url = streamIndex.getAttribute('Url'); + const url = streamIndex.attributes['Url']; goog.asserts.assert(url, 'Missing URL for segments'); const mediaUrl = url.replace('{bitrate}', String(stream.bandwidth)); @@ -871,7 +871,7 @@ shaka.mss.MssParser = class { * Expands a streamIndex into an array-based timeline. The results are in * seconds. * - * @param {!Element} streamIndex + * @param {!shaka.extern.xml.Node} streamIndex * @param {number} timescale * @param {number} duration The duration in seconds. * @return {!Array.} @@ -884,9 +884,9 @@ shaka.mss.MssParser = class { goog.asserts.assert( duration > 0, 'duration must be a positive integer'); - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; - const timePoints = XmlUtils.findChildren(streamIndex, 'c'); + const timePoints = TXml.findChildren(streamIndex, 'c'); /** @type {!Array.} */ const timeline = []; @@ -896,10 +896,10 @@ shaka.mss.MssParser = class { const timePoint = timePoints[i]; const next = timePoints[i + 1]; const t = - XmlUtils.parseAttr(timePoint, 't', XmlUtils.parseNonNegativeInt); + TXml.parseAttr(timePoint, 't', TXml.parseNonNegativeInt); const d = - XmlUtils.parseAttr(timePoint, 'd', XmlUtils.parseNonNegativeInt); - const r = XmlUtils.parseAttr(timePoint, 'r', XmlUtils.parseInt); + TXml.parseAttr(timePoint, 'd', TXml.parseNonNegativeInt); + const r = TXml.parseAttr(timePoint, 'r', TXml.parseInt); if (!d) { shaka.log.warning( @@ -914,7 +914,7 @@ shaka.mss.MssParser = class { if (repeat < 0) { if (next) { const nextStartTime = - XmlUtils.parseAttr(next, 't', XmlUtils.parseNonNegativeInt); + TXml.parseAttr(next, 't', TXml.parseNonNegativeInt); if (nextStartTime == null) { shaka.log.warning( 'An "c" element cannot have a negative repeat', diff --git a/lib/text/ttml_text_parser.js b/lib/text/ttml_text_parser.js index ead042535e..221f478a3f 100644 --- a/lib/text/ttml_text_parser.js +++ b/lib/text/ttml_text_parser.js @@ -15,7 +15,7 @@ goog.require('shaka.text.TextEngine'); goog.require('shaka.util.ArrayUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -53,7 +53,7 @@ shaka.text.TtmlTextParser = class { */ parseMedia(data, time, uri) { const TtmlTextParser = shaka.text.TtmlTextParser; - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttpNs = TtmlTextParser.parameterNs_; const ttsNs = TtmlTextParser.styleNs_; const str = shaka.util.StringUtils.fromUTF8(data); @@ -65,7 +65,7 @@ shaka.text.TtmlTextParser = class { return cues; } - const tt = XmlUtils.parseXmlString(str, 'tt'); + const tt = TXml.parseXmlString(str, 'tt'); if (!tt) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -74,23 +74,23 @@ shaka.text.TtmlTextParser = class { 'Failed to parse TTML.'); } - const body = tt.getElementsByTagName('body')[0]; + const body = TXml.getElementsByTagName(tt, 'body')[0]; if (!body) { return []; } // Get the framerate, subFrameRate and frameRateMultiplier if applicable. - const frameRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRate'); - const subFrameRate = XmlUtils.getAttributeNSList( + const frameRate = TXml.getAttributeNSList(tt, ttpNs, 'frameRate'); + const subFrameRate = TXml.getAttributeNSList( tt, ttpNs, 'subFrameRate'); const frameRateMultiplier = - XmlUtils.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier'); - const tickRate = XmlUtils.getAttributeNSList(tt, ttpNs, 'tickRate'); + TXml.getAttributeNSList(tt, ttpNs, 'frameRateMultiplier'); + const tickRate = TXml.getAttributeNSList(tt, ttpNs, 'tickRate'); - const cellResolution = XmlUtils.getAttributeNSList( + const cellResolution = TXml.getAttributeNSList( tt, ttpNs, 'cellResolution'); - const spaceStyle = tt.getAttribute('xml:space') || 'default'; - const extent = XmlUtils.getAttributeNSList(tt, ttsNs, 'extent'); + const spaceStyle = tt.attributes['xml:space'] || 'default'; + const extent = TXml.getAttributeNSList(tt, ttsNs, 'extent'); if (spaceStyle != 'default' && spaceStyle != 'preserve') { throw new shaka.util.Error( @@ -107,10 +107,10 @@ shaka.text.TtmlTextParser = class { const cellResolutionInfo = TtmlTextParser.getCellResolution_(cellResolution); - const metadata = tt.getElementsByTagName('metadata')[0]; - const metadataElements = metadata ? XmlUtils.getChildren(metadata) : []; - const styles = Array.from(tt.getElementsByTagName('style')); - const regionElements = Array.from(tt.getElementsByTagName('region')); + const metadata = TXml.getElementsByTagName(tt, 'metadata')[0]; + const metadataElements = metadata ? metadata.children : []; + const styles = TXml.getElementsByTagName(tt, 'style'); + const regionElements = TXml.getElementsByTagName(tt, 'region'); const cueRegions = []; for (const region of regionElements) { @@ -125,7 +125,7 @@ shaka.text.TtmlTextParser = class { // elements. We used to allow this, but it is non-compliant, and the // loose nature of our previous parser made it difficult to implement TTML // nesting more fully. - if (XmlUtils.findChildren(body, 'p').length) { + if (TXml.findChildren(body, 'p').length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, @@ -133,9 +133,9 @@ shaka.text.TtmlTextParser = class { '

can only be inside

in TTML'); } - for (const div of XmlUtils.findChildren(body, 'div')) { + for (const div of TXml.findChildren(body, 'div')) { // A
element should only contain

, not . - if (XmlUtils.findChildren(div, 'span').length) { + if (TXml.findChildren(div, 'span').length) { throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT, @@ -165,16 +165,16 @@ shaka.text.TtmlTextParser = class { /** * Parses a TTML node into a Cue. * - * @param {!Node} cueNode + * @param {!shaka.extern.xml.Node} cueNode * @param {shaka.extern.TextParser.TimeContext} timeContext * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo - * @param {!Array.} metadataElements - * @param {!Array.} styles - * @param {!Array.} regionElements + * @param {!Array.} metadataElements + * @param {!Array.} styles + * @param {!Array.} regionElements * @param {!Array.} cueRegions * @param {boolean} whitespaceTrim * @param {?{columns: number, rows: number}} cellResolution - * @param {?Element} parentCueElement + * @param {?shaka.extern.xml.Node} parentCueElement * @param {boolean} isContent * @param {?(string|undefined)} uri * @return {shaka.text.Cue} @@ -184,17 +184,14 @@ shaka.text.TtmlTextParser = class { cueNode, timeContext, rateInfo, metadataElements, styles, regionElements, cueRegions, whitespaceTrim, cellResolution, parentCueElement, isContent, uri) { - /** @type {Element} */ + const TXml = shaka.util.TXml; + const StringUtils = shaka.util.StringUtils; + /** @type {shaka.extern.xml.Node} */ let cueElement; - /** @type {Element} */ - let parentElement = /** @type {Element} */ (cueNode.parentNode); + /** @type {?shaka.extern.xml.Node} */ + let parentElement = parentCueElement; - if (cueNode.nodeType == Node.COMMENT_NODE) { - // The comments do not contain information that interests us here. - return null; - } - - if (cueNode.nodeType == Node.TEXT_NODE) { + if (TXml.isText(cueNode)) { if (!isContent) { // Ignore text elements outside the content. For example, whitespace // on the same lexical level as the

elements, in a document with @@ -205,13 +202,16 @@ shaka.text.TtmlTextParser = class { // So pretend the element was a . parentElement was set above, so // we should still be able to correctly traverse up for timing // information later. - const span = document.createElement('span'); - span.textContent = cueNode.textContent; + /** @type {shaka.extern.xml.Node} */ + const span = { + tagName: 'span', + children: [TXml.getTextContents(cueNode)], + attributes: {}, + parent: null, + }; cueElement = span; } else { - goog.asserts.assert(cueNode.nodeType == Node.ELEMENT_NODE, - 'nodeType should be ELEMENT_NODE!'); - cueElement = /** @type {!Element} */(cueNode); + cueElement = cueNode; } goog.asserts.assert(cueElement, 'cueElement should be non-null!'); @@ -226,7 +226,7 @@ shaka.text.TtmlTextParser = class { } let imageUri = null; - const backgroundImage = shaka.util.XmlUtils.getAttributeNSList( + const backgroundImage = TXml.getAttributeNSList( cueElement, shaka.text.TtmlTextParser.smpteNsList_, 'backgroundImage'); @@ -239,27 +239,24 @@ shaka.text.TtmlTextParser = class { } } - if (cueNode.nodeName == 'p' || imageElement || imageUri) { + if (cueNode.tagName == 'p' || imageElement || imageUri) { isContent = true; } const parentIsContent = isContent; - const spaceStyle = cueElement.getAttribute('xml:space') || + const spaceStyle = cueElement.attributes['xml:space'] || (whitespaceTrim ? 'default' : 'preserve'); const localWhitespaceTrim = spaceStyle == 'default'; // Parse any nested cues first. - const isTextNode = (node) => { - return node.nodeType == Node.TEXT_NODE; - }; - const isLeafNode = Array.from(cueElement.childNodes).every(isTextNode); + const isLeafNode = cueElement.children.every(TXml.isText); const nestedCues = []; if (!isLeafNode) { // Otherwise, recurse into the children. Text nodes will convert into // anonymous spans, which will then be leaf nodes. - for (const childNode of cueElement.childNodes) { + for (const childNode of cueElement.children) { const nestedCue = shaka.text.TtmlTextParser.parseCue_( childNode, timeContext, @@ -284,12 +281,16 @@ shaka.text.TtmlTextParser = class { const isNested = /** @type {boolean} */ (parentCueElement != null); + const textContent = TXml.getTextContents(cueElement); // In this regex, "\S" means "non-whitespace character". - const hasTextContent = /\S/.test(cueElement.textContent); + const hasTextContent = cueElement.children.length && + textContent && + /\S/.test(textContent); + const hasTimeAttributes = - cueElement.hasAttribute('begin') || - cueElement.hasAttribute('end') || - cueElement.hasAttribute('dur'); + cueElement.attributes['begin'] || + cueElement.attributes['end'] || + cueElement.attributes['dur']; if (!hasTimeAttributes && !hasTextContent && cueElement.tagName != 'br' && nestedCues.length == 0) { @@ -310,11 +311,12 @@ shaka.text.TtmlTextParser = class { cueElement, rateInfo); // Resolve local time relative to parent elements. Time elements can appear // all the way up to 'body', but not 'tt'. - while (parentElement && parentElement.nodeType == Node.ELEMENT_NODE && + while (parentElement && TXml.isNode(parentElement) && parentElement.tagName != 'tt') { ({start, end} = shaka.text.TtmlTextParser.resolveTime_( parentElement, rateInfo, start, end)); - parentElement = /** @type {Element} */(parentElement.parentNode); + parentElement = + /** @type {shaka.extern.xml.Node} */ (parentElement.parent); } if (start == null) { @@ -357,7 +359,8 @@ shaka.text.TtmlTextParser = class { let payload = ''; if (isLeafNode) { // If the childNodes are all text, this is a leaf node. Get the payload. - payload = cueElement.textContent; + payload = StringUtils.htmlUnescape( + shaka.util.TXml.getTextContents(cueElement) || ''); if (localWhitespaceTrim) { // Trim leading and trailing whitespace. payload = payload.trim(); @@ -386,16 +389,16 @@ shaka.text.TtmlTextParser = class { // Do not actually apply that region unless it is non-inherited, though. // This makes it so that, if a parent element has a region, the children // don't also all independently apply the positioning of that region. - if (cueElement.hasAttribute('region')) { - if (regionElement && regionElement.getAttribute('xml:id')) { - const regionId = regionElement.getAttribute('xml:id'); + if (cueElement.attributes['region']) { + if (regionElement && regionElement.attributes['xml:id']) { + const regionId = regionElement.attributes['xml:id']; cue.region = cueRegions.filter((region) => region.id == regionId)[0]; } } let regionElementForStyle = regionElement; - if (parentCueElement && isNested && !cueElement.getAttribute('region') && - !cueElement.getAttribute('style')) { + if (parentCueElement && isNested && !cueElement.attributes['region'] && + !cueElement.attributes['style']) { regionElementForStyle = shaka.text.TtmlTextParser.getElementsFromCollection_( parentCueElement, 'region', regionElements, /* prefix= */ '')[0]; @@ -405,7 +408,7 @@ shaka.text.TtmlTextParser = class { cue, cueElement, regionElementForStyle, - imageElement, + /** @type {!shaka.extern.xml.Node} */(imageElement), imageUri, styles, /** isNested= */ parentIsContent, // "nested in a

" doesn't count. @@ -417,9 +420,9 @@ shaka.text.TtmlTextParser = class { /** * Parses an Element into a TextTrackCue or VTTCue. * - * @param {!Element} regionElement - * @param {!Array.} styles Defined in the top of tt element and - * used principally for images. + * @param {!shaka.extern.xml.Node} regionElement + * @param {!Array.} styles + * Defined in the top of tt element and used principally for images. * @param {?string} globalExtent * @return {shaka.text.CueRegion} * @private @@ -427,7 +430,7 @@ shaka.text.TtmlTextParser = class { static parseCueRegion_(regionElement, styles, globalExtent) { const TtmlTextParser = shaka.text.TtmlTextParser; const region = new shaka.text.CueRegion(); - const id = regionElement.getAttribute('xml:id'); + const id = regionElement.attributes['xml:id']; if (!id) { shaka.log.warning('TtmlTextParser parser encountered a region with ' + 'no id. Region will be ignored.'); @@ -511,11 +514,11 @@ shaka.text.TtmlTextParser = class { * Adds applicable style properties to a cue. * * @param {!shaka.text.Cue} cue - * @param {!Element} cueElement - * @param {Element} region - * @param {Element} imageElement + * @param {!shaka.extern.xml.Node} cueElement + * @param {shaka.extern.xml.Node} region + * @param {shaka.extern.xml.Node} imageElement * @param {?string} imageUri - * @param {!Array.} styles + * @param {!Array.} styles * @param {boolean} isNested * @param {boolean} isLeaf * @private @@ -524,6 +527,7 @@ shaka.text.TtmlTextParser = class { cue, cueElement, region, imageElement, imageUri, styles, isNested, isLeaf) { const TtmlTextParser = shaka.text.TtmlTextParser; + const TXml = shaka.util.TXml; const Cue = shaka.text.Cue; // Styles should be inherited from regions, if a style property is not @@ -681,10 +685,10 @@ shaka.text.TtmlTextParser = class { // in PR #1859, in April 2019, and first released in v2.5.0. // Now we check for both, although only imageType (camelCase) is to spec. const backgroundImageType = - imageElement.getAttribute('imageType') || - imageElement.getAttribute('imagetype'); - const backgroundImageEncoding = imageElement.getAttribute('encoding'); - const backgroundImageData = imageElement.textContent.trim(); + imageElement.attributes['imageType'] || + imageElement.attributes['imagetype']; + const backgroundImageEncoding = imageElement.attributes['encoding']; + const backgroundImageData = (TXml.getTextContents(imageElement)).trim(); if (backgroundImageType == 'PNG' && backgroundImageEncoding == 'Base64' && backgroundImageData) { @@ -823,9 +827,9 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on either the original cue element or its * associated region and returns the value if the attribute was found. * - * @param {!Element} cueElement - * @param {Element} region - * @param {!Array.} styles + * @param {!shaka.extern.xml.Node} cueElement + * @param {shaka.extern.xml.Node} region + * @param {!Array.} styles * @param {string} attribute * @param {boolean=} shouldInheritRegionStyles * @return {?string} @@ -853,21 +857,21 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on the element's associated region * and returns the value if the attribute was found. * - * @param {Element} region - * @param {!Array.} styles + * @param {shaka.extern.xml.Node} region + * @param {!Array.} styles * @param {string} attribute * @return {?string} * @private */ static getStyleAttributeFromRegion_(region, styles, attribute) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttsNs = shaka.text.TtmlTextParser.styleNs_; if (!region) { return null; } - const attr = XmlUtils.getAttributeNSList(region, ttsNs, attribute); + const attr = TXml.getAttributeNSList(region, ttsNs, attribute); if (attr) { return attr; } @@ -880,19 +884,19 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on the cue element and returns the value * if the attribute was found. * - * @param {!Element} cueElement - * @param {!Array.} styles + * @param {!shaka.extern.xml.Node} cueElement + * @param {!Array.} styles * @param {string} attribute * @return {?string} * @private */ static getStyleAttributeFromElement_(cueElement, styles, attribute) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttsNs = shaka.text.TtmlTextParser.styleNs_; // Styling on elements should take precedence // over the main styling attributes - const elementAttribute = XmlUtils.getAttributeNSList( + const elementAttribute = TXml.getAttributeNSList( cueElement, ttsNs, attribute); @@ -908,14 +912,14 @@ shaka.text.TtmlTextParser = class { * Finds a specified attribute on an element's styles and the styles those * styles inherit from. * - * @param {!Element} element - * @param {!Array.} styles + * @param {!shaka.extern.xml.Node} element + * @param {!Array.} styles * @param {string} attribute * @return {?string} * @private */ static getInheritedStyleAttribute_(element, styles, attribute) { - const XmlUtils = shaka.util.XmlUtils; + const TXml = shaka.util.TXml; const ttsNs = shaka.text.TtmlTextParser.styleNs_; const ebuttsNs = shaka.text.TtmlTextParser.styleEbuttsNs_; @@ -928,14 +932,14 @@ shaka.text.TtmlTextParser = class { // The last value in our styles stack takes the precedence over the others for (let i = 0; i < inheritedStyles.length; i++) { // Check ebu namespace first. - let styleAttributeValue = XmlUtils.getAttributeNS( + let styleAttributeValue = TXml.getAttributeNS( inheritedStyles[i], ebuttsNs, attribute); if (!styleAttributeValue) { // Fall back to tts namespace. - styleAttributeValue = XmlUtils.getAttributeNSList( + styleAttributeValue = TXml.getAttributeNSList( inheritedStyles[i], ttsNs, attribute); @@ -962,12 +966,12 @@ shaka.text.TtmlTextParser = class { * Selects items from |collection| whose id matches |attributeName| * from |element|. * - * @param {Element} element + * @param {shaka.extern.xml.Node} element * @param {string} attributeName - * @param {!Array.} collection + * @param {!Array.} collection * @param {string} prefixName * @param {string=} nsName - * @return {!Array.} + * @return {!Array.} * @private */ static getElementsFromCollection_( @@ -988,7 +992,7 @@ shaka.text.TtmlTextParser = class { for (const name of itemNames) { for (const item of collection) { - if ((prefixName + item.getAttribute('xml:id')) == name) { + if ((prefixName + item.attributes['xml:id']) == name) { items.push(item); break; } @@ -1003,7 +1007,7 @@ shaka.text.TtmlTextParser = class { /** * Traverses upwards from a given node until a given attribute is found. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {string} attributeName * @param {string=} nsName * @return {?string} @@ -1011,19 +1015,19 @@ shaka.text.TtmlTextParser = class { */ static getInheritedAttribute_(element, attributeName, nsName) { let ret = null; - const XmlUtils = shaka.util.XmlUtils; - while (element) { + const TXml = shaka.util.TXml; + while (!ret) { ret = nsName ? - XmlUtils.getAttributeNS(element, nsName, attributeName) : - element.getAttribute(attributeName); + TXml.getAttributeNS(element, nsName, attributeName) : + element.attributes[attributeName]; if (ret) { break; } // Element.parentNode can lead to XMLDocument, which is not an Element and // has no getAttribute(). - const parentNode = element.parentNode; - if (parentNode instanceof Element) { + const parentNode = element.parent; + if (parentNode) { element = parentNode; } else { break; @@ -1036,7 +1040,7 @@ shaka.text.TtmlTextParser = class { * Factor parent/ancestor time attributes into the parsed time of a * child/descendent. * - * @param {!Element} parentElement + * @param {!shaka.extern.xml.Node} parentElement * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo * @param {?number} start The child's start time * @param {?number} end The child's end time @@ -1074,18 +1078,18 @@ shaka.text.TtmlTextParser = class { /** * Parse TTML time attributes from the given element. * - * @param {!Element} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo * @return {{start: ?number, end: ?number}} * @private */ static parseTime_(element, rateInfo) { const start = shaka.text.TtmlTextParser.parseTimeAttribute_( - element.getAttribute('begin'), rateInfo); + element.attributes['begin'], rateInfo); let end = shaka.text.TtmlTextParser.parseTimeAttribute_( - element.getAttribute('end'), rateInfo); + element.attributes['end'], rateInfo); const duration = shaka.text.TtmlTextParser.parseTimeAttribute_( - element.getAttribute('dur'), rateInfo); + element.attributes['dur'], rateInfo); if (end == null && duration != null) { end = start + duration; } diff --git a/lib/text/vtt_text_parser.js b/lib/text/vtt_text_parser.js index 54379b8018..fc6b586856 100644 --- a/lib/text/vtt_text_parser.js +++ b/lib/text/vtt_text_parser.js @@ -15,7 +15,7 @@ goog.require('shaka.text.TextEngine'); goog.require('shaka.util.Error'); goog.require('shaka.util.StringUtils'); goog.require('shaka.util.TextParser'); -goog.require('shaka.util.XmlUtils'); +goog.require('shaka.util.TXml'); /** @@ -451,9 +451,11 @@ shaka.text.VttTextParser = class { */ static parseCueStyles(payload, rootCue, styles) { const VttTextParser = shaka.text.VttTextParser; + const StringUtils = shaka.util.StringUtils; + const TXml = shaka.util.TXml; // Optimization for unstyled payloads. if (!payload.includes('<')) { - rootCue.payload = VttTextParser.htmlUnescape_(payload); + rootCue.payload = StringUtils.htmlUnescape(payload); return; } if (styles.size === 0) { @@ -463,14 +465,19 @@ shaka.text.VttTextParser = class { payload = VttTextParser.replaceKaraokeStylePayload_(payload); payload = VttTextParser.replaceVoiceStylePayload_(payload); const xmlPayload = '' + payload + ''; - const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span'); + let element; + try { + element = TXml.parseXmlString(xmlPayload, 'span'); + } catch (e) { + shaka.log.warning('cue parse fail: ', e); + } + if (element) { - const childNodes = element.childNodes; + const childNodes = element.children; if (childNodes.length == 1) { const childNode = childNodes[0]; - if (childNode.nodeType == Node.TEXT_NODE || - childNode.nodeType == Node.CDATA_SECTION_NODE) { - rootCue.payload = VttTextParser.htmlUnescape_(payload); + if (!TXml.isNode(childNode)) { + rootCue.payload = StringUtils.htmlUnescape(payload); return; } } @@ -479,7 +486,7 @@ shaka.text.VttTextParser = class { } } else { shaka.log.warning('The cue\'s markup could not be parsed: ', payload); - rootCue.payload = VttTextParser.htmlUnescape_(payload); + rootCue.payload = StringUtils.htmlUnescape(payload); } } @@ -700,13 +707,14 @@ shaka.text.VttTextParser = class { } /** - * @param {!Node} element + * @param {!shaka.extern.xml.Node} element * @param {!shaka.text.Cue} rootCue * @param {!Map.} styles * @private */ static generateCueFromElement_(element, rootCue, styles) { const VttTextParser = shaka.text.VttTextParser; + const TXml = shaka.util.TXml; const nestedCue = rootCue.clone(); // We don't want propagate some properties. nestedCue.nestedCues = []; @@ -717,11 +725,12 @@ shaka.text.VttTextParser = class { nestedCue.region = new shaka.text.CueRegion(); nestedCue.position = null; nestedCue.size = 0; - if (element.nodeType === Node.ELEMENT_NODE && element.nodeName) { + + if (shaka.util.TXml.isNode(element)) { const bold = shaka.text.Cue.fontWeight.BOLD; const italic = shaka.text.Cue.fontStyle.ITALIC; const underline = shaka.text.Cue.textDecoration.UNDERLINE; - const tags = element.nodeName.split(/(?=[ .])+/g); + const tags = element.tagName.split(/(?=[ .])+/g); for (const tag of tags) { let styleTag = tag; // White blanks at start indicate that the style is a voice @@ -754,15 +763,14 @@ shaka.text.VttTextParser = class { nestedCue.textDecoration.push(underline); break; case 'font': { - const color = - /** @type {!Element} */(element).getAttribute('color'); + const color = element.attributes['color']; if (color) { nestedCue.color = color; } break; } case 'div': { - const time = /** @type {!Element} */(element).getAttribute('time'); + const time = element.attributes['time']; if (!time) { break; } @@ -784,13 +792,13 @@ shaka.text.VttTextParser = class { } } - const isTextNode = (item) => shaka.util.XmlUtils.isText(item); - const childNodes = element.childNodes; + const isTextNode = (item) => shaka.util.TXml.isText(item); + const childNodes = element.children; if (isTextNode(element) || (childNodes.length == 1 && isTextNode(childNodes[0]))) { // Trailing line breaks may lost when convert cue to HTML tag // Need to insert line break cue to preserve line breaks - const textArr = element.textContent.split('\n'); + const textArr = TXml.getTextContents(element).split('\n'); let isFirst = true; for (const text of textArr) { if (!isFirst) { @@ -800,7 +808,7 @@ shaka.text.VttTextParser = class { } if (text.length > 0) { const textCue = nestedCue.clone(); - textCue.payload = VttTextParser.htmlUnescape_(text); + textCue.payload = shaka.util.StringUtils.htmlUnescape(text); rootCue.nestedCues.push(textCue); } isFirst = false; @@ -1016,42 +1024,6 @@ shaka.text.VttTextParser = class { return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600); } - - /** - * This method converts the HTML entities &, <, >, ", ', - *  , ‎ and ‏ in string to their corresponding characters. - * - * @param {!string} input - * @return {string} - * @private - */ - static htmlUnescape_(input) { - // Used to map HTML entities to characters. - const htmlUnescapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': '\'', - ' ': '\u{a0}', - '‎': '\u{200e}', - '‏': '\u{200f}', - }; - - // Used to match HTML entities and HTML characters. - const reEscapedHtml = /&(?:amp|lt|gt|quot|#(0+)?39|nbsp|lrm|rlm);/g; - const reHasEscapedHtml = RegExp(reEscapedHtml.source); - // This check is an optimization, since replace always makes a copy - if (input && reHasEscapedHtml.test(input)) { - return input.replace(reEscapedHtml, (entity) => { - // The only thing that might not match the dictionary above is the - // single quote, which can be matched by many strings in the regex, but - // only has a single entry in the dictionary. - return htmlUnescapes[entity] || '\''; - }); - } - return input || ''; - } }; /** diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js index 40753123fc..c4b5404c32 100644 --- a/lib/util/string_utils.js +++ b/lib/util/string_utils.js @@ -276,6 +276,42 @@ shaka.util.StringUtils = class { static resetFromCharCode() { shaka.util.StringUtils.fromCharCodeImpl_.reset(); } + + /** + * This method converts the HTML entities &, <, >, ", ', + *  , ‎ and ‏ in string to their corresponding characters. + * + * @param {!string} input + * @return {string} + */ + static htmlUnescape(input) { + // Used to map HTML entities to characters. + const htmlUnescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': '\'', + ''': '\'', + ' ': '\u{a0}', + '‎': '\u{200e}', + '‏': '\u{200f}', + }; + + // Used to match HTML entities and HTML characters. + const reEscapedHtml = /&(?:amp|lt|gt|quot|apos|#(0+)?39|nbsp|lrm|rlm);/g; + const reHasEscapedHtml = RegExp(reEscapedHtml.source); + // This check is an optimization, since replace always makes a copy + if (input && reHasEscapedHtml.test(input)) { + return input.replace(reEscapedHtml, (entity) => { + // The only thing that might not match the dictionary above is the + // single quote, which can be matched by many strings in the regex, but + // only has a single entry in the dictionary. + return htmlUnescapes[entity] || '\''; + }); + } + return input || ''; + } }; diff --git a/lib/util/tXml.js b/lib/util/tXml.js new file mode 100644 index 0000000000..8eb7634f17 --- /dev/null +++ b/lib/util/tXml.js @@ -0,0 +1,724 @@ +goog.provide('shaka.util.TXml'); + +goog.require('shaka.util.StringUtils'); +goog.require('shaka.log'); + +/** + * This code is a modified version of the tXml library. + * + * @author: Tobias Nickel + * created: 06.04.2015 + * https://github.com/TobiasNickel/tXml + */ + +/** + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +shaka.util.TXml = class { + /** + * Parse some data + * @param {BufferSource} data + * @param {string=} expectedRootElemName + * @return {shaka.extern.xml.Node | null} + */ + static parseXml(data, expectedRootElemName) { + const xmlString = shaka.util.StringUtils.fromBytesAutoDetect(data); + return shaka.util.TXml.parseXmlString(xmlString, expectedRootElemName); + } + + /** + * Parse some data + * @param {string} xmlString + * @param {string=} expectedRootElemName + * @return {shaka.extern.xml.Node | null} + */ + static parseXmlString(xmlString, expectedRootElemName) { + const result = shaka.util.TXml.parse(xmlString); + if (!expectedRootElemName && result.length) { + return result[0]; + } + const rootNode = result.find((n) => n.tagName === expectedRootElemName); + if (rootNode) { + return rootNode; + } + + shaka.log.error('parseXml root element not found!'); + return null; + } + + /** + * Parse some data + * @param {string} schema + * @return {string} + */ + static getKnownNameSpace(schema) { + if (shaka.util.TXml.knownNameSpaces_.has(schema)) { + return shaka.util.TXml.knownNameSpaces_.get(schema); + } + return ''; + } + + /** + * Parse some data + * @param {string} schema + * @param {string} NS + */ + static setKnownNameSpace(schema, NS) { + shaka.util.TXml.knownNameSpaces_.set(schema, NS); + } + + /** + * parseXML / html into a DOM Object, + * with no validation and some failure tolerance + * @param {string} S your XML to parse + * @return {Array.} + */ + static parse(S) { + let pos = 0; + + const openBracket = '<'; + const openBracketCC = '<'.charCodeAt(0); + const closeBracket = '>'; + const closeBracketCC = '>'.charCodeAt(0); + const minusCC = '-'.charCodeAt(0); + const slashCC = '/'.charCodeAt(0); + const exclamationCC = '!'.charCodeAt(0); + const singleQuoteCC = '\''.charCodeAt(0); + const doubleQuoteCC = '"'.charCodeAt(0); + const openCornerBracketCC = '['.charCodeAt(0); + + /** + * parsing a list of entries + */ + function parseChildren(tagName, preserveSpace = false) { + /** @type {Array.} */ + const children = []; + while (S[pos]) { + if (S.charCodeAt(pos) == openBracketCC) { + if (S.charCodeAt(pos + 1) === slashCC) { + const closeStart = pos + 2; + pos = S.indexOf(closeBracket, pos); + + const closeTag = S.substring(closeStart, pos); + let indexOfCloseTag = closeTag.indexOf(tagName); + if (indexOfCloseTag == -1) { + // handle VTT closing tags like + const indexOfPeriod = tagName.indexOf('.'); + if (indexOfPeriod > 0) { + const shortTag = tagName.substring(0, indexOfPeriod); + indexOfCloseTag = closeTag.indexOf(shortTag); + } + } + // eslint-disable-next-line no-restricted-syntax + if (indexOfCloseTag == -1) { + const parsedText = S.substring(0, pos).split('\n'); + throw new Error( + 'Unexpected close tag\nLine: ' + (parsedText.length - 1) + + '\nColumn: ' + + (parsedText[parsedText.length - 1].length + 1) + + '\nChar: ' + S[pos], + ); + } + + if (pos + 1) { + pos += 1; + } + + return children; + } else if (S.charCodeAt(pos + 1) === exclamationCC) { + if (S.charCodeAt(pos + 2) == minusCC) { + while (pos !== -1 && !(S.charCodeAt(pos) === closeBracketCC && + S.charCodeAt(pos - 1) == minusCC && + S.charCodeAt(pos - 2) == minusCC && + pos != -1)) { + pos = S.indexOf(closeBracket, pos + 1); + } + if (pos === -1) { + pos = S.length; + } + } else if ( + S.charCodeAt(pos + 2) === openCornerBracketCC && + S.charCodeAt(pos + 8) === openCornerBracketCC && + S.substr(pos + 3, 5).toLowerCase() === 'cdata' + ) { + // cdata + const cdataEndIndex = S.indexOf(']]>', pos); + if (cdataEndIndex == -1) { + children.push(S.substr(pos + 9)); + pos = S.length; + } else { + children.push(S.substring(pos + 9, cdataEndIndex)); + pos = cdataEndIndex + 3; + } + continue; + } + pos++; + continue; + } + const node = parseNode(preserveSpace); + children.push(node); + if (typeof node === 'string') { + return children; + } + if (node.tagName[0] === '?' && node.children) { + children.push(...node.children); + node.children = []; + } + } else { + const text = parseText(); + if (preserveSpace) { + if (text.length > 0) { + children.push(text); + } + } else { + const trimmed = text.trim(); + if (trimmed.length > 0) { + children.push(text); + } + } + pos++; + } + } + return children; + } + + /** + * returns the text outside of texts until the first '<' + */ + function parseText() { + const start = pos; + pos = S.indexOf(openBracket, pos) - 1; + if (pos === -2) { + pos = S.length; + } + return S.slice(start, pos + 1); + } + /** + * returns text until the first nonAlphabetic letter + */ + const nameSpacer = '\r\n\t>/= '; + + /** + * Parse text in current context + * @return {string} + */ + function parseName() { + const start = pos; + while (nameSpacer.indexOf(S[pos]) === -1 && S[pos]) { + pos++; + } + return S.slice(start, pos); + } + + /** + * Parse text in current context + * @param {boolean} preserveSpace Preserve the space between nodes + * @return {shaka.extern.xml.Node | string} + */ + function parseNode(preserveSpace) { + pos++; + const tagName = parseName(); + const attributes = {}; + let children = []; + + // parsing attributes + while (S.charCodeAt(pos) !== closeBracketCC && S[pos]) { + const c = S.charCodeAt(pos); + // abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' + if ((c > 64 && c < 91) || (c > 96 && c < 123)) { + const name = parseName(); + // search beginning of the string + let code = S.charCodeAt(pos); + while (code && code !== singleQuoteCC && code !== doubleQuoteCC && + !((code > 64 && code < 91) || (code > 96 && code < 123)) && + code !== closeBracketCC) { + pos++; + code = S.charCodeAt(pos); + } + let value = parseString(); + if (code === singleQuoteCC || code === doubleQuoteCC) { + if (pos === -1) { + /** @type {shaka.extern.xml.Node} */ + const node = { + tagName, + attributes, + children, + parent: null, + }; + for (let i = 0; i < children.length; i++) { + if (typeof children[i] !== 'string') { + children[i].parent = node; + } + } + return node; + } + } else { + value = null; + pos--; + } + if (name.startsWith('xmlns:')) { + const segs = name.split(':'); + shaka.util.TXml.setKnownNameSpace( + /** @type {string} */ (value), segs[1]); + } + if (tagName === 'tt' && + name === 'xml:space' && + value === 'preserve') { + preserveSpace = true; + } + attributes[name] = value; + } + pos++; + } + + if (S.charCodeAt(pos - 1) !== slashCC) { + pos++; + const contents = parseChildren(tagName, preserveSpace); + children = contents; + } else { + pos++; + } + /** @type {shaka.extern.xml.Node} */ + const node = { + tagName, + attributes, + children, + parent: null, + }; + for (let i = 0; i < children.length; i++) { + if (typeof children[i] !== 'string') { + children[i].parent = node; + } + } + return node; + } + + /** + * Parse string in current context + * @return {string} + */ + function parseString() { + const startChar = S[pos]; + const startpos = pos + 1; + pos = S.indexOf(startChar, startpos); + return S.slice(startpos, pos); + } + + return parseChildren(''); + } + + /** + * Verifies if the element is a TXml node. + * @param {!shaka.extern.xml.Node} elem The XML element. + * @return {!boolean} Is the element a TXml node + */ + static isNode(elem) { + return !!(elem.tagName); + } + + /** + * Checks if a node is of type text. + * @param {!shaka.extern.xml.Node | string} elem The XML element. + * @return {boolean} True if it is a text node. + */ + static isText(elem) { + return typeof elem === 'string'; + } + + /** + * gets child XML elements. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @return {!Array.} The child XML elements. + */ + static getChildNodes(elem) { + const found = []; + if (!elem.children) { + return []; + } + for (const child of elem.children) { + if (typeof child !== 'string') { + found.push(child); + } + } + return found; + } + + /** + * Finds child XML elements. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @return {!Array.} The child XML elements. + */ + static findChildren(elem, name) { + const found = []; + if (!elem.children) { + return []; + } + for (const child of elem.children) { + if (child.tagName === name) { + found.push(child); + } + } + return found; + } + + /** + * Gets inner text. + * @param {!shaka.extern.xml.Node | string} node The XML element. + * @return {?string} The text contents, or null if there are none. + */ + static getTextContents(node) { + if (typeof node === 'string') { + return node; + } + const textContent = node.children.reduce( + (acc, curr) => (typeof curr === 'string' ? acc + curr : acc), + '', + ); + if (textContent === '') { + return null; + } + return textContent; + } + + /** + * Gets the text contents of a node. + * @param {!shaka.extern.xml.Node} node The XML element. + * @return {?string} The text contents, or null if there are none. + */ + static getContents(node) { + if (!Array.from(node.children).every( + (n) => typeof n === 'string' )) { + return null; + } + + // Read merged text content from all text nodes. + let text = shaka.util.TXml.getTextContents(node); + if (text) { + text = text.trim(); + } + return text; + } + + /** + * Finds child XML elements recursively. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @param {!Array.} found accumulator for found nodes + * @return {!Array.} The child XML elements. + */ + static getElementsByTagName(elem, name, found = []) { + if (elem.tagName === name) { + found.push(elem); + } + if (elem.children) { + for (const child of elem.children) { + shaka.util.TXml.getElementsByTagName(child, name, found); + } + } + return found; + } + + /** + * Finds a child XML element. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} name The child XML element's tag name. + * @return {shaka.extern.xml.Node | null} The child XML element, + * or null if a child XML element + * does not exist with the given tag name OR if there exists more than one + * child XML element with the given tag name. + */ + static findChild(elem, name) { + const children = shaka.util.TXml.findChildren(elem, name); + if (children.length != 1) { + return null; + } + return children[0]; + } + + /** + * Finds a namespace-qualified child XML element. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} ns The child XML element's namespace URI. + * @param {string} name The child XML element's local name. + * @return {shaka.extern.xml.Node | null} The child XML element, or null + * if a child XML element + * does not exist with the given tag name OR if there exists more than one + * child XML element with the given tag name. + */ + static findChildNS(elem, ns, name) { + const children = shaka.util.TXml.findChildrenNS(elem, ns, name); + if (children.length != 1) { + return null; + } + return children[0]; + } + + /** + * Parses an attribute by its name. + * @param {!shaka.extern.xml.Node} elem The XML element. + * @param {string} name The attribute name. + * @param {function(string): (T|null)} parseFunction A function that parses + * the attribute. + * @param {(T|null)=} defaultValue The attribute's default value, if not + * specified, the attibute's default value is null. + * @return {(T|null)} The parsed attribute on success, or the attribute's + * default value if the attribute does not exist or could not be parsed. + * @template T + */ + static parseAttr(elem, name, parseFunction, defaultValue = null) { + let parsedValue = null; + + const value = elem.attributes[name]; + if (value != null) { + parsedValue = parseFunction(value); + } + return parsedValue == null ? defaultValue : parsedValue; + } + + /** + * Gets a namespace-qualified attribute. + * @param {!shaka.extern.xml.Node} elem The element to get from. + * @param {string} ns The namespace URI. + * @param {string} name The local name of the attribute. + * @return {?string} The attribute's value, or null if not present. + */ + static getAttributeNS(elem, ns, name) { + const schemaNS = shaka.util.TXml.getKnownNameSpace(ns); + // Think this is equivalent + const attribute = elem.attributes[`${schemaNS}:${name}`]; + return attribute || null; + } + + /** + * Finds namespace-qualified child XML elements. + * @param {!shaka.extern.xml.Node} elem The parent XML element. + * @param {string} ns The child XML element's namespace URI. + * @param {string} name The child XML element's local name. + * @return {!Array.} The child XML elements. + */ + static findChildrenNS(elem, ns, name) { + const schemaNS = shaka.util.TXml.getKnownNameSpace(ns); + const found = []; + if (elem.children) { + for (const child of elem.children) { + if (child && child.tagName === `${schemaNS}:${name}`) { + found.push(child); + } + } + } + return found; + } + + /** + * Gets a namespace-qualified attribute. + * @param {!shaka.extern.xml.Node} elem The element to get from. + * @param {!Array.} nsList The lis of namespace URIs. + * @param {string} name The local name of the attribute. + * @return {?string} The attribute's value, or null if not present. + */ + static getAttributeNSList(elem, nsList, name) { + for (const ns of nsList) { + const attr = shaka.util.TXml.getAttributeNS( + elem, ns, name, + ); + if (attr) { + return attr; + } + } + return null; + } + + + /** + * Parses an XML date string. + * @param {string} dateString + * @return {?number} The parsed date in seconds on success; otherwise, return + * null. + */ + static parseDate(dateString) { + if (!dateString) { + return null; + } + + // Times in the manifest should be in UTC. If they don't specify a timezone, + // Date.parse() will use the local timezone instead of UTC. So manually add + // the timezone if missing ('Z' indicates the UTC timezone). + // Format: YYYY-MM-DDThh:mm:ss.ssssss + if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) { + dateString += 'Z'; + } + + const result = Date.parse(dateString); + return isNaN(result) ? null : (result / 1000.0); + } + + + /** + * Parses an XML duration string. + * Negative values are not supported. Years and months are treated as exactly + * 365 and 30 days respectively. + * @param {string} durationString The duration string, e.g., "PT1H3M43.2S", + * which means 1 hour, 3 minutes, and 43.2 seconds. + * @return {?number} The parsed duration in seconds on success; otherwise, + * return null. + * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html} + */ + static parseDuration(durationString) { + if (!durationString) { + return null; + } + + const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' + + '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$'; + const matches = new RegExp(re).exec(durationString); + + if (!matches) { + shaka.log.warning('Invalid duration string:', durationString); + return null; + } + + // Note: Number(null) == 0 but Number(undefined) == NaN. + const years = Number(matches[1] || null); + const months = Number(matches[2] || null); + const days = Number(matches[3] || null); + const hours = Number(matches[4] || null); + const minutes = Number(matches[5] || null); + const seconds = Number(matches[6] || null); + + // Assume a year always has 365 days and a month always has 30 days. + const d = (60 * 60 * 24 * 365) * years + + (60 * 60 * 24 * 30) * months + + (60 * 60 * 24) * days + + (60 * 60) * hours + + 60 * minutes + + seconds; + return isFinite(d) ? d : null; + } + + + /** + * Parses a range string. + * @param {string} rangeString The range string, e.g., "101-9213". + * @return {?{start: number, end: number}} The parsed range on success; + * otherwise, return null. + */ + static parseRange(rangeString) { + const matches = /([0-9]+)-([0-9]+)/.exec(rangeString); + + if (!matches) { + return null; + } + + const start = Number(matches[1]); + if (!isFinite(start)) { + return null; + } + + const end = Number(matches[2]); + if (!isFinite(end)) { + return null; + } + + return {start: start, end: end}; + } + + + /** + * Parses an integer. + * @param {string} intString The integer string. + * @return {?number} The parsed integer on success; otherwise, return null. + */ + static parseInt(intString) { + const n = Number(intString); + return (n % 1 === 0) ? n : null; + } + + + /** + * Parses a positive integer. + * @param {string} intString The integer string. + * @return {?number} The parsed positive integer on success; otherwise, + * return null. + */ + static parsePositiveInt(intString) { + const n = Number(intString); + return (n % 1 === 0) && (n > 0) ? n : null; + } + + + /** + * Parses a non-negative integer. + * @param {string} intString The integer string. + * @return {?number} The parsed non-negative integer on success; otherwise, + * return null. + */ + static parseNonNegativeInt(intString) { + const n = Number(intString); + return (n % 1 === 0) && (n >= 0) ? n : null; + } + + + /** + * Parses a floating point number. + * @param {string} floatString The floating point number string. + * @return {?number} The parsed floating point number on success; otherwise, + * return null. May return -Infinity or Infinity. + */ + static parseFloat(floatString) { + const n = Number(floatString); + return !isNaN(n) ? n : null; + } + + + /** + * Parses a boolean. + * @param {string} booleanString The boolean string. + * @return {boolean} The boolean + */ + static parseBoolean(booleanString) { + if (!booleanString) { + return false; + } + return booleanString.toLowerCase() === 'true'; + } + + + /** + * Evaluate a division expressed as a string. + * @param {string} exprString + * The expression to evaluate, e.g. "200/2". Can also be a single number. + * @return {?number} The evaluated expression as floating point number on + * success; otherwise return null. + */ + static evalDivision(exprString) { + let res; + let n; + if ((res = exprString.match(/^(\d+)\/(\d+)$/))) { + n = Number(res[1]) / Number(res[2]); + } else { + n = Number(exprString); + } + return !isNaN(n) ? n : null; + } +}; + +shaka.util.TXml.knownNameSpaces_ = new Map([]); diff --git a/lib/util/xml_utils.js b/lib/util/xml_utils.js deleted file mode 100644 index abe74880d6..0000000000 --- a/lib/util/xml_utils.js +++ /dev/null @@ -1,461 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -goog.provide('shaka.util.XmlUtils'); - -goog.require('goog.asserts'); -goog.require('shaka.log'); -goog.require('shaka.util.Lazy'); -goog.require('shaka.util.StringUtils'); - - -/** - * @summary A set of XML utility functions. - */ -shaka.util.XmlUtils = class { - /** - * Finds a child XML element. - * @param {!Node} elem The parent XML element. - * @param {string} name The child XML element's tag name. - * @return {Element} The child XML element, or null if a child XML element - * does not exist with the given tag name OR if there exists more than one - * child XML element with the given tag name. - */ - static findChild(elem, name) { - const children = shaka.util.XmlUtils.findChildren(elem, name); - if (children.length != 1) { - return null; - } - return children[0]; - } - - - /** - * Finds a namespace-qualified child XML element. - * @param {!Node} elem The parent XML element. - * @param {string} ns The child XML element's namespace URI. - * @param {string} name The child XML element's local name. - * @return {Element} The child XML element, or null if a child XML element - * does not exist with the given tag name OR if there exists more than one - * child XML element with the given tag name. - */ - static findChildNS(elem, ns, name) { - const children = shaka.util.XmlUtils.findChildrenNS(elem, ns, name); - if (children.length != 1) { - return null; - } - return children[0]; - } - - - /** - * Finds child XML elements. - * @param {!Node} elem The parent XML element. - * @param {string} name The child XML element's tag name. - * @return {!Array.} The child XML elements. - */ - static findChildren(elem, name) { - const found = []; - for (const child of elem.childNodes) { - if (child instanceof Element && child.tagName == name) { - found.push(child); - } - } - return found; - } - - - /** - * @param {!Node} elem the parent XML element. - * @return {!Array.} The child XML elements. - */ - static getChildren(elem) { - return Array.from(elem.childNodes).filter((child) => { - return child instanceof Element; - }); - } - - - /** - * Finds namespace-qualified child XML elements. - * @param {!Node} elem The parent XML element. - * @param {string} ns The child XML element's namespace URI. - * @param {string} name The child XML element's local name. - * @return {!Array.} The child XML elements. - */ - static findChildrenNS(elem, ns, name) { - const found = []; - for (const child of elem.childNodes) { - if (child instanceof Element && child.localName == name && - child.namespaceURI == ns) { - found.push(child); - } - } - return found; - } - - - /** - * Gets a namespace-qualified attribute. - * @param {!Element} elem The element to get from. - * @param {string} ns The namespace URI. - * @param {string} name The local name of the attribute. - * @return {?string} The attribute's value, or null if not present. - */ - static getAttributeNS(elem, ns, name) { - // Some browsers return the empty string when the attribute is missing, - // so check if it exists first. See: https://mzl.la/2L7F0UK - return elem.hasAttributeNS(ns, name) ? elem.getAttributeNS(ns, name) : null; - } - - - /** - * Gets a namespace-qualified attribute. - * @param {!Element} elem The element to get from. - * @param {!Array.} nsList The lis of namespace URIs. - * @param {string} name The local name of the attribute. - * @return {?string} The attribute's value, or null if not present. - */ - static getAttributeNSList(elem, nsList, name) { - // Some browsers return the empty string when the attribute is missing, - // so check if it exists first. See: https://mzl.la/2L7F0UK - for (const ns of nsList) { - if (elem.hasAttributeNS(ns, name)) { - return elem.getAttributeNS(ns, name); - } - } - return null; - } - - - /** - * Gets the text contents of a node. - * @param {!Node} elem The XML element. - * @return {?string} The text contents, or null if there are none. - */ - static getContents(elem) { - const XmlUtils = shaka.util.XmlUtils; - if (!Array.from(elem.childNodes).every(XmlUtils.isText)) { - return null; - } - - // Read merged text content from all text nodes. - return elem.textContent.trim(); - } - - /** - * Checks if a node is of type text. - * @param {!Node} elem The XML element. - * @return {boolean} True if it is a text node. - */ - static isText(elem) { - return elem.nodeType == Node.TEXT_NODE || - elem.nodeType == Node.CDATA_SECTION_NODE; - } - - /** - * Parses an attribute by its name. - * @param {!Element} elem The XML element. - * @param {string} name The attribute name. - * @param {function(string): (T|null)} parseFunction A function that parses - * the attribute. - * @param {(T|null)=} defaultValue The attribute's default value, if not - * specified, the attibute's default value is null. - * @return {(T|null)} The parsed attribute on success, or the attribute's - * default value if the attribute does not exist or could not be parsed. - * @template T - */ - static parseAttr( - elem, name, parseFunction, defaultValue = null) { - let parsedValue = null; - - const value = elem.getAttribute(name); - if (value != null) { - parsedValue = parseFunction(value); - } - return parsedValue == null ? defaultValue : parsedValue; - } - - - /** - * Parses an XML date string. - * @param {string} dateString - * @return {?number} The parsed date in seconds on success; otherwise, return - * null. - */ - static parseDate(dateString) { - if (!dateString) { - return null; - } - - // Times in the manifest should be in UTC. If they don't specify a timezone, - // Date.parse() will use the local timezone instead of UTC. So manually add - // the timezone if missing ('Z' indicates the UTC timezone). - // Format: YYYY-MM-DDThh:mm:ss.ssssss - if (/^\d+-\d+-\d+T\d+:\d+:\d+(\.\d+)?$/.test(dateString)) { - dateString += 'Z'; - } - - const result = Date.parse(dateString); - return isNaN(result) ? null : (result / 1000.0); - } - - - /** - * Parses an XML duration string. - * Negative values are not supported. Years and months are treated as exactly - * 365 and 30 days respectively. - * @param {string} durationString The duration string, e.g., "PT1H3M43.2S", - * which means 1 hour, 3 minutes, and 43.2 seconds. - * @return {?number} The parsed duration in seconds on success; otherwise, - * return null. - * @see {@link http://www.datypic.com/sc/xsd/t-xsd_duration.html} - */ - static parseDuration(durationString) { - if (!durationString) { - return null; - } - - const re = '^P(?:([0-9]*)Y)?(?:([0-9]*)M)?(?:([0-9]*)D)?' + - '(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$'; - const matches = new RegExp(re).exec(durationString); - - if (!matches) { - shaka.log.warning('Invalid duration string:', durationString); - return null; - } - - // Note: Number(null) == 0 but Number(undefined) == NaN. - const years = Number(matches[1] || null); - const months = Number(matches[2] || null); - const days = Number(matches[3] || null); - const hours = Number(matches[4] || null); - const minutes = Number(matches[5] || null); - const seconds = Number(matches[6] || null); - - // Assume a year always has 365 days and a month always has 30 days. - const d = (60 * 60 * 24 * 365) * years + - (60 * 60 * 24 * 30) * months + - (60 * 60 * 24) * days + - (60 * 60) * hours + - 60 * minutes + - seconds; - return isFinite(d) ? d : null; - } - - - /** - * Parses a range string. - * @param {string} rangeString The range string, e.g., "101-9213". - * @return {?{start: number, end: number}} The parsed range on success; - * otherwise, return null. - */ - static parseRange(rangeString) { - const matches = /([0-9]+)-([0-9]+)/.exec(rangeString); - - if (!matches) { - return null; - } - - const start = Number(matches[1]); - if (!isFinite(start)) { - return null; - } - - const end = Number(matches[2]); - if (!isFinite(end)) { - return null; - } - - return {start: start, end: end}; - } - - - /** - * Parses an integer. - * @param {string} intString The integer string. - * @return {?number} The parsed integer on success; otherwise, return null. - */ - static parseInt(intString) { - const n = Number(intString); - return (n % 1 === 0) ? n : null; - } - - - /** - * Parses a positive integer. - * @param {string} intString The integer string. - * @return {?number} The parsed positive integer on success; otherwise, - * return null. - */ - static parsePositiveInt(intString) { - const n = Number(intString); - return (n % 1 === 0) && (n > 0) ? n : null; - } - - - /** - * Parses a non-negative integer. - * @param {string} intString The integer string. - * @return {?number} The parsed non-negative integer on success; otherwise, - * return null. - */ - static parseNonNegativeInt(intString) { - const n = Number(intString); - return (n % 1 === 0) && (n >= 0) ? n : null; - } - - - /** - * Parses a floating point number. - * @param {string} floatString The floating point number string. - * @return {?number} The parsed floating point number on success; otherwise, - * return null. May return -Infinity or Infinity. - */ - static parseFloat(floatString) { - const n = Number(floatString); - return !isNaN(n) ? n : null; - } - - - /** - * Parses a boolean. - * @param {string} booleanString The boolean string. - * @return {boolean} The boolean - */ - static parseBoolean(booleanString) { - if (!booleanString) { - return false; - } - return booleanString.toLowerCase() === 'true'; - } - - - /** - * Evaluate a division expressed as a string. - * @param {string} exprString - * The expression to evaluate, e.g. "200/2". Can also be a single number. - * @return {?number} The evaluated expression as floating point number on - * success; otherwise return null. - */ - static evalDivision(exprString) { - let res; - let n; - if ((res = exprString.match(/^(\d+)\/(\d+)$/))) { - n = Number(res[1]) / Number(res[2]); - } else { - n = Number(exprString); - } - return !isNaN(n) ? n : null; - } - - - /** - * Parse a string and return the resulting root element if it was valid XML. - * - * @param {string} xmlString - * @param {string} expectedRootElemName - * @return {Element} - */ - static parseXmlString(xmlString, expectedRootElemName) { - const parser = new DOMParser(); - const unsafeXmlString = - shaka.util.XmlUtils.trustedHTMLFromString_.value()(xmlString); - let unsafeXml = null; - try { - unsafeXml = parser.parseFromString(unsafeXmlString, 'text/xml'); - } catch (exception) { - shaka.log.error('XML parsing exception:', exception); - return null; - } - - // According to MDN, parseFromString never returns null. - goog.asserts.assert(unsafeXml, 'Parsed XML document cannot be null!'); - - // Check for empty documents. - const rootElem = unsafeXml.documentElement; - if (!rootElem) { - shaka.log.error('XML document was empty!'); - return null; - } - - // Check for parser errors. - const parserErrorElements = rootElem.getElementsByTagName('parsererror'); - if (parserErrorElements.length) { - shaka.log.error('XML parser error found:', parserErrorElements[0]); - return null; - } - - // The top-level element in the loaded XML should have the name we expect. - if (rootElem.tagName != expectedRootElemName) { - shaka.log.error( - `XML tag name does not match expected "${expectedRootElemName}":`, - rootElem.tagName); - return null; - } - - // Cobalt browser doesn't support document.createNodeIterator. - if (!('createNodeIterator' in document)) { - return rootElem; - } - - // SECURITY: Verify that the document does not contain elements from the - // HTML or SVG namespaces, which could trigger script execution and XSS. - const iterator = document.createNodeIterator( - unsafeXml, - NodeFilter.SHOW_ALL, - ); - let currentNode; - while (currentNode = iterator.nextNode()) { - if (currentNode instanceof HTMLElement || - currentNode instanceof SVGElement) { - shaka.log.error('XML document embeds unsafe content!'); - return null; - } - } - - return rootElem; - } - - - /** - * Parse some data (auto-detecting the encoding) and return the resulting - * root element if it was valid XML. - * @param {BufferSource} data - * @param {string} expectedRootElemName - * @return {Element} - */ - static parseXml(data, expectedRootElemName) { - try { - const string = shaka.util.StringUtils.fromBytesAutoDetect(data); - return shaka.util.XmlUtils.parseXmlString(string, expectedRootElemName); - } catch (exception) { - shaka.log.error('parseXmlString threw!', exception); - return null; - } - } -}; - -/** - * Promote a string to TrustedHTML. This function is security-sensitive and - * should only be used with security approval where the string is guaranteed not - * to cause an XSS vulnerability. - * - * @private {!shaka.util.Lazy.} - */ -shaka.util.XmlUtils.trustedHTMLFromString_ = new shaka.util.Lazy(() => { - if (typeof trustedTypes !== 'undefined') { - // Create a Trusted Types policy for promoting the string to TrustedHTML. - // The Lazy wrapper ensures this policy is only created once. - const policy = trustedTypes.createPolicy('shaka-player#xml', { - createHTML: (s) => s, - }); - return (s) => policy.createHTML(s); - } - // Fall back to strings in environments that don't support Trusted Types. - return (s) => s; -}); - diff --git a/test/dash/dash_parser_content_protection_unit.js b/test/dash/dash_parser_content_protection_unit.js index 8785c6b15c..5a9fb51a6c 100644 --- a/test/dash/dash_parser_content_protection_unit.js +++ b/test/dash/dash_parser_content_protection_unit.js @@ -9,8 +9,7 @@ describe('DashParser ContentProtection', () => { const Dash = shaka.test.Dash; const ContentProtection = shaka.dash.ContentProtection; const strToXml = (str) => { - const parser = new DOMParser(); - return parser.parseFromString(str, 'application/xml').documentElement; + return shaka.util.TXml.parseXmlString(str); }; /** diff --git a/test/dash/dash_parser_live_unit.js b/test/dash/dash_parser_live_unit.js index dd0673cd41..90d78f0ef9 100644 --- a/test/dash/dash_parser_live_unit.js +++ b/test/dash/dash_parser_live_unit.js @@ -1300,7 +1300,7 @@ describe('DashParser Live', () => { startTime: 10, endTime: 60, id: '', - eventElement: jasmine.any(Element), + eventElement: jasmine.any(Object), }); expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith({ schemeIdUri: 'http://example.com', @@ -1308,7 +1308,7 @@ describe('DashParser Live', () => { startTime: 13, endTime: 23, id: 'abc', - eventElement: jasmine.any(Element), + eventElement: jasmine.any(Object), }); }); diff --git a/test/dash/dash_parser_manifest_unit.js b/test/dash/dash_parser_manifest_unit.js index be4ed43dc5..9ed899d3af 100644 --- a/test/dash/dash_parser_manifest_unit.js +++ b/test/dash/dash_parser_manifest_unit.js @@ -910,36 +910,6 @@ describe('DashParser Manifest', () => { }); describe('fails for', () => { - it('invalid XML', async () => { - const source = ' { - const source = [ - '', - ' ', - ' ', - ' ', - ' ', - ' ', - ' ', - '', - ].join('\n'); - const error = new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.DASH_INVALID_XML, - 'dummy://foo'); - await Dash.testFails(source, error); - }); - it('xlink problems when xlinkFailGracefully is false', async () => { const source = [ ' { fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.dash.manifestPreprocessor = (mpd) => { - const selector = 'AdaptationSet[mimeType="text/vtt"'; - const vttElements = mpd.querySelectorAll(selector); - for (const element of vttElements) { - element.parentNode.removeChild(element); - } + /** @type {shaka.extern.xml.Node} */ + const manifest = /** @type {shaka.extern.xml.Node} */ ( + /** @type {shaka.extern.xml.Node} */(mpd).children[0]); + manifest.children = [ + manifest.children[0], + manifest.children[1], + ]; }; parser.configure(config); diff --git a/test/dash/mpd_utils_unit.js b/test/dash/mpd_utils_unit.js index 9ce1ec892d..6cbab088bd 100644 --- a/test/dash/mpd_utils_unit.js +++ b/test/dash/mpd_utils_unit.js @@ -452,11 +452,9 @@ describe('MpdUtils', () => { ' />'); } xmlLines.push(''); - const parser = new DOMParser(); - const xml = - parser.parseFromString(xmlLines.join('\n'), 'application/xml'); - const segmentTimeline = xml.documentElement; - console.assert(segmentTimeline); + const segmentTimeline = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(xmlLines.join('\n'), + 'SegmentTimeline')); const timeline = MpdUtils.createTimeline( segmentTimeline, timescale, presentationTimeOffset, @@ -473,8 +471,6 @@ describe('MpdUtils', () => { let fakeNetEngine; /** @type {shaka.extern.RetryParameters} */ let retry; - /** @type {!DOMParser} */ - let parser; /** @type {boolean} */ let failGracefully; @@ -482,7 +478,6 @@ describe('MpdUtils', () => { failGracefully = false; retry = shaka.net.NetworkingEngine.defaultRetryParameters(); fakeNetEngine = new shaka.test.FakeNetworkingEngine(); - parser = new DOMParser(); }); it('will replace elements and children', async () => { @@ -541,18 +536,6 @@ describe('MpdUtils', () => { await testSucceeds(baseXMLString, desiredXMLString, 3); }); - it('fails if loaded file is invalid xml', async () => { - const baseXMLString = inBaseContainer( - ''); - // Note this does not have a close angle bracket. - const xlinkXMLString = ' { const baseXMLString = inBaseContainer( @@ -691,8 +674,8 @@ describe('MpdUtils', () => { /** @type {!shaka.util.PublicPromise} */ const continuePromise = fakeNetEngine.delayNextRequest(); - const xml = parser.parseFromString(baseXMLString, 'text/xml') - .documentElement; + const xml = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(baseXMLString)); /** @type {!shaka.extern.IAbortableOperation} */ const operation = MpdUtils.processXlinks( xml, retry, failGracefully, 'https://base', fakeNetEngine); @@ -730,11 +713,11 @@ describe('MpdUtils', () => { async function testSucceeds( baseXMLString, desiredXMLString, desiredNetCalls) { - const desiredXML = parser.parseFromString(desiredXMLString, 'text/xml') - .documentElement; + const desiredXML = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(desiredXMLString)); const finalXML = await testRequest(baseXMLString); expect(fakeNetEngine.request).toHaveBeenCalledTimes(desiredNetCalls); - expect(finalXML).toEqualElement(desiredXML); + expect(finalXML).toEqual(desiredXML); } async function testFails(baseXMLString, desiredError, desiredNetCalls) { @@ -785,8 +768,8 @@ describe('MpdUtils', () => { } function testRequest(baseXMLString) { - const xml = parser.parseFromString(baseXMLString, 'text/xml') - .documentElement; + const xml = /** @type {shaka.extern.xml.Node} */ ( + shaka.util.TXml.parseXmlString(baseXMLString)); return MpdUtils.processXlinks(xml, retry, failGracefully, 'https://base', fakeNetEngine).promise; } diff --git a/test/mss/mss_parser_content_protection_unit.js b/test/mss/mss_parser_content_protection_unit.js index cc160c0f10..10983f2989 100644 --- a/test/mss/mss_parser_content_protection_unit.js +++ b/test/mss/mss_parser_content_protection_unit.js @@ -9,8 +9,7 @@ describe('MssParser ContentProtection', () => { const ContentProtection = shaka.mss.ContentProtection; const strToXml = (str) => { - const parser = new DOMParser(); - return parser.parseFromString(str, 'application/xml').documentElement; + return shaka.util.TXml.parseXmlString(str); }; it('getPlayReadyLicenseURL', () => { diff --git a/test/mss/mss_parser_unit.js b/test/mss/mss_parser_unit.js index 41df61695b..3f52ad8d9e 100644 --- a/test/mss/mss_parser_unit.js +++ b/test/mss/mss_parser_unit.js @@ -85,35 +85,6 @@ describe('MssParser Manifest', () => { }); describe('fails for', () => { - it('invalid XML', async () => { - const source = ' { - const source = [ - '', - ' ', - ' ', - ' ', - ' ', - ].join('\n'); - const error = new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.MSS_INVALID_XML, - 'dummy://foo'); - await Mss.testFails(source, error); - }); - it('failed network requests', async () => { const expectedError = new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, @@ -273,11 +244,7 @@ describe('MssParser Manifest', () => { fakeNetEngine.setResponseText('dummy://foo', manifestText); const config = shaka.util.PlayerConfiguration.createDefault().manifest; config.mss.manifestPreprocessor = (mss) => { - const selector = 'StreamIndex[Name="text"'; - const vttElements = mss.querySelectorAll(selector); - for (const element of vttElements) { - element.parentNode.removeChild(element); - } + /** @type{shaka.extern.xml.Node} */ (mss).children.pop(); }; parser.configure(config); diff --git a/test/test/util/util.js b/test/test/util/util.js index d20b134e93..99b923ce85 100644 --- a/test/test/util/util.js +++ b/test/test/util/util.js @@ -154,7 +154,7 @@ shaka.test.Util = class { (expected['outerHTML'] || expected.textContent) + ': '; const getAttr = (obj, attr) => { if (attr.namespaceURI) { - return shaka.util.XmlUtils.getAttributeNS( + return shaka.util.TXml.getAttributeNS( obj, attr.namespaceURI, attr.localName); } else { return obj.getAttribute(attr.localName); diff --git a/test/util/tXml_unit.js b/test/util/tXml_unit.js new file mode 100644 index 0000000000..46f8576800 --- /dev/null +++ b/test/util/tXml_unit.js @@ -0,0 +1,391 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +describe('tXml', () => { + // A number that cannot be represented as a Javascript number. + const HUGE_NUMBER_STRING = new Array(500).join('7'); + + const TXml = shaka.util.TXml; + + describe('findChild', () => { + it('finds a child node', () => { + const xmlString = [ + '', + '', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.findChild(root, 'Child')).toBeTruthy(); + expect(TXml.findChild(root, 'DoesNotExist')).toBeNull(); + }); + + it('handles duplicate child nodes', () => { + const xmlString = [ + '', + '', + ' ', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.findChild(root, 'Child')).toBeNull(); + }); + }); + + it('findChildren', () => { + const xmlString = [ + '', + '', + ' ', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(root).toBeTruthy(); + + let children = TXml.findChildren(root, 'Child'); + expect(children.length).toBe(2); + + children = TXml.findChildren(root, 'DoesNotExist'); + expect(children.length).toBe(0); + }); + + describe('getContents', () => { + it('returns node contents', () => { + const xmlString = [ + '', + '', + ' foo bar', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.getContents(root)).toBe('foo bar'); + }); + + it('handles empty node contents', () => { + const xmlString = [ + '', + '', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.getContents(root)).toBeNull(); + }); + + it('handles null node contents', () => { + const xmlString = [ + '', + '', + '', + ].join('\n'); + const xml = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(xml, 'parseFromString should succeed'); + + expect(TXml.getContents(xml)).toBeNull(); + }); + + it('handles CDATA sections', () => { + const xmlString = [ + '', + '', + ' Bar]]>', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(TXml.getContents(root)).toBe(' Bar'); + }); + }); + + describe('parseAttr', () => { + /** @type {!shaka.extern.xml.Node} */ + let xml; + + beforeEach(() => { + const xmlString = [ + '', + '', + '', + ].join('\n'); + xml = /** @type {!shaka.extern.xml.Node} */ ( + TXml.parseXmlString(xmlString, 'Root')); + }); + + it('delegates to parser function', () => { + const root = xml; + expect(TXml.parseAttr(root, 'a', TXml.parseRange)).toEqual( + {start: 2, end: 7}); + expect(TXml.parseAttr(root, 'b', TXml.parseInt)).toBe(-5); + expect(TXml.parseAttr(root, 'c', TXml.parseInt)).toBe(0); + expect(TXml.parseAttr(root, 'd', TXml.parseInt)).toBeNull(); + }); + + it('supports default values', () => { + const root = xml; + goog.asserts.assert(root, 'findChild should find element'); + expect(TXml.parseAttr(root, 'd', TXml.parseInt, 9)).toBe(9); + }); + }); + + describe('parseXmlString', () => { + it('parses a simple XML document', () => { + const xmlString = [ + '', + '', + ' ', + '', + ].join('\n'); + const root = TXml.parseXmlString(xmlString, 'Root'); + goog.asserts.assert(root, 'parseFromString should succeed'); + + expect(root.tagName).toBe('Root'); + }); + + it('returns null on an empty XML document', () => { + const xmlString = ''; + const doc = TXml.parseXmlString(xmlString, 'Root'); + expect(doc).toBeNull(); + }); + + it('returns null on root element mismatch', () => { + const xmlString = [ + '', + '', + ' ', + '', + ].join('\n'); + const doc = TXml.parseXmlString(xmlString, 'Document'); + expect(doc).toBeNull(); + }); + }); + + it('parseDate', () => { + // Should be parsed as UTC independent of local timezone. + expect(TXml.parseDate('2015-11-30T12:46:33')).toBe(1448887593); + // Should be parsed using the given timezone, not the local timezone. + expect(TXml.parseDate('2015-11-30T12:46:33+06:00')).toBe(1448865993); + + expect(TXml.parseDate('November 30, 2015')).toBeTruthy(); + expect(TXml.parseDate('Apple')).toBeNull(); + expect(TXml.parseDate('')).toBeNull(); + }); + + it('parseDuration', () => { + // No time. + expect(TXml.parseDuration('P')).toBe(0); + expect(TXml.parseDuration('PT')).toBe(0); + + // Years only. 1 year has 365 or 366 days. + expect(TXml.parseDuration('P3Y')).toBeLessThan( + 3 * (60 * 60 * 24 * 366) + 1); + expect(TXml.parseDuration('P3Y')).toBeGreaterThan( + 3 * (60 * 60 * 24 * 365) - 1); + + // Months only. 1 month has 28 to 31 days. + expect(TXml.parseDuration('P2M')).toBeLessThan( + 2 * (60 * 60 * 24 * 31) + 1); + expect(TXml.parseDuration('P2M')).toBeGreaterThan( + 2 * (60 * 60 * 24 * 28) - 1); + + // Days only. + expect(TXml.parseDuration('P7D')).toBe(604800); + + // Hours only. + expect(TXml.parseDuration('PT1H')).toBe(3600); + + // Minutes only. + expect(TXml.parseDuration('PT1M')).toBe(60); + + // Seconds only (with no fractional part). + expect(TXml.parseDuration('PT1S')).toBe(1); + + // Seconds only (with no whole part). + expect(TXml.parseDuration('PT0.1S')).toBe(0.1); + expect(TXml.parseDuration('PT.1S')).toBe(0.1); + + // Seconds only (with whole part and fractional part). + expect(TXml.parseDuration('PT1.1S')).toBe(1.1); + + // Hours, and minutes. + expect(TXml.parseDuration('PT1H2M')).toBe(3720); + + // Hours, and seconds. + expect(TXml.parseDuration('PT1H2S')).toBe(3602); + expect(TXml.parseDuration('PT1H2.2S')).toBe(3602.2); + + // Minutes, and seconds. + expect(TXml.parseDuration('PT1M2S')).toBe(62); + expect(TXml.parseDuration('PT1M2.2S')).toBe(62.2); + + // Hours, minutes, and seconds. + expect(TXml.parseDuration('PT1H2M3S')).toBe(3723); + expect(TXml.parseDuration('PT1H2M3.3S')).toBe(3723.3); + + // Days, hours, minutes, and seconds. + expect(TXml.parseDuration('P1DT1H2M3S')).toBe(90123); + expect(TXml.parseDuration('P1DT1H2M3.3S')).toBe(90123.3); + + // Months, hours, minutes, and seconds. + expect(TXml.parseDuration('P1M1DT1H2M3S')).toBeLessThan( + (60 * 60 * 24 * 31) + 90123 + 1); + expect(TXml.parseDuration('P1M1DT1H2M3S')).toBeGreaterThan( + (60 * 60 * 24 * 28) + 90123 - 1); + + // Years, Months, hours, minutes, and seconds. + expect(TXml.parseDuration('P1Y1M1DT1H2M3S')).toBeLessThan( + (60 * 60 * 24 * 366) + (60 * 60 * 24 * 31) + 90123 + 1); + expect(TXml.parseDuration('P1Y1M1DT1H2M3S')).toBeGreaterThan( + (60 * 60 * 24 * 365) + (60 * 60 * 24 * 28) + 90123 - 1); + + expect(TXml.parseDuration('PT')).toBe(0); + expect(TXml.parseDuration('P')).toBe(0); + + // Error cases. + expect(TXml.parseDuration('-PT3S')).toBeNull(); + expect(TXml.parseDuration('PT-3S')).toBeNull(); + expect(TXml.parseDuration('P1Sasdf')).toBeNull(); + expect(TXml.parseDuration('1H2M3S')).toBeNull(); + expect(TXml.parseDuration('123')).toBeNull(); + expect(TXml.parseDuration('abc')).toBeNull(); + expect(TXml.parseDuration('')).toBeNull(); + + expect(TXml.parseDuration('P' + HUGE_NUMBER_STRING + 'Y')).toBeNull(); + expect(TXml.parseDuration('P' + HUGE_NUMBER_STRING + 'M')).toBeNull(); + expect(TXml.parseDuration('P' + HUGE_NUMBER_STRING + 'D')).toBeNull(); + expect(TXml.parseDuration('PT' + HUGE_NUMBER_STRING + 'H')).toBeNull(); + expect(TXml.parseDuration('PT' + HUGE_NUMBER_STRING + 'M')).toBeNull(); + expect(TXml.parseDuration('PT' + HUGE_NUMBER_STRING + 'S')).toBeNull(); + }); + + it('parseRange', () => { + expect(TXml.parseRange('0-0')).toEqual({start: 0, end: 0}); + expect(TXml.parseRange('1-1')).toEqual({start: 1, end: 1}); + expect(TXml.parseRange('1-50')).toEqual({start: 1, end: 50}); + expect(TXml.parseRange('50-1')).toEqual({start: 50, end: 1}); + + expect(TXml.parseRange('-1')).toBeNull(); + expect(TXml.parseRange('1-')).toBeNull(); + expect(TXml.parseRange('1')).toBeNull(); + expect(TXml.parseRange('-')).toBeNull(); + expect(TXml.parseRange('')).toBeNull(); + + expect(TXml.parseRange('abc')).toBeNull(); + expect(TXml.parseRange('a-')).toBeNull(); + expect(TXml.parseRange('-b')).toBeNull(); + expect(TXml.parseRange('a-b')).toBeNull(); + + expect(TXml.parseRange(HUGE_NUMBER_STRING + '-1')).toBeNull(); + expect(TXml.parseRange('1-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parseInt', () => { + expect(TXml.parseInt('0')).toBe(0); + expect(TXml.parseInt('1')).toBe(1); + expect(TXml.parseInt('191')).toBe(191); + + expect(TXml.parseInt('-0')).toBe(0); + expect(TXml.parseInt('-1')).toBe(-1); + expect(TXml.parseInt('-191')).toBe(-191); + + expect(TXml.parseInt('abc')).toBeNull(); + expect(TXml.parseInt('1abc')).toBeNull(); + expect(TXml.parseInt('abc1')).toBeNull(); + + expect(TXml.parseInt('0.0')).toBe(0); + expect(TXml.parseInt('-0.0')).toBe(0); + + expect(TXml.parseInt('0.1')).toBeNull(); + expect(TXml.parseInt('1.1')).toBeNull(); + + expect(TXml.parseInt(HUGE_NUMBER_STRING)).toBeNull(); + expect(TXml.parseInt('-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parsePositiveInt', () => { + expect(TXml.parsePositiveInt('0')).toBeNull(); + expect(TXml.parsePositiveInt('1')).toBe(1); + expect(TXml.parsePositiveInt('191')).toBe(191); + + expect(TXml.parsePositiveInt('-0')).toBeNull(); + expect(TXml.parsePositiveInt('-1')).toBeNull(); + expect(TXml.parsePositiveInt('-191')).toBeNull(); + + expect(TXml.parsePositiveInt('abc')).toBeNull(); + expect(TXml.parsePositiveInt('1abc')).toBeNull(); + expect(TXml.parsePositiveInt('abc1')).toBeNull(); + + expect(TXml.parsePositiveInt('0.0')).toBeNull(); + expect(TXml.parsePositiveInt('-0.0')).toBeNull(); + + expect(TXml.parsePositiveInt('0.1')).toBeNull(); + expect(TXml.parsePositiveInt('1.1')).toBeNull(); + + expect(TXml.parsePositiveInt(HUGE_NUMBER_STRING)).toBeNull(); + expect(TXml.parsePositiveInt('-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parseNonNegativeInt', () => { + expect(TXml.parseNonNegativeInt('0')).toBe(0); + expect(TXml.parseNonNegativeInt('1')).toBe(1); + expect(TXml.parseNonNegativeInt('191')).toBe(191); + + expect(TXml.parseNonNegativeInt('-0')).toBe(0); + expect(TXml.parseNonNegativeInt('-1')).toBeNull(); + expect(TXml.parseNonNegativeInt('-191')).toBeNull(); + + expect(TXml.parseNonNegativeInt('abc')).toBeNull(); + expect(TXml.parseNonNegativeInt('1abc')).toBeNull(); + expect(TXml.parseNonNegativeInt('abc1')).toBeNull(); + + expect(TXml.parseNonNegativeInt('0.0')).toBe(0); + expect(TXml.parseNonNegativeInt('-0.0')).toBe(0); + + expect(TXml.parseNonNegativeInt('0.1')).toBeNull(); + expect(TXml.parseNonNegativeInt('1.1')).toBeNull(); + + expect(TXml.parseNonNegativeInt(HUGE_NUMBER_STRING)).toBeNull(); + expect(TXml.parseNonNegativeInt('-' + HUGE_NUMBER_STRING)).toBeNull(); + }); + + it('parseFloat', () => { + expect(TXml.parseFloat('0')).toBe(0); + expect(TXml.parseFloat('1')).toBe(1); + expect(TXml.parseFloat('191')).toBe(191); + + expect(TXml.parseFloat('-0')).toBe(0); + expect(TXml.parseFloat('-1')).toBe(-1); + expect(TXml.parseFloat('-191')).toBe(-191); + + expect(TXml.parseFloat('abc')).toBeNull(); + expect(TXml.parseFloat('1abc')).toBeNull(); + expect(TXml.parseFloat('abc1')).toBeNull(); + + expect(TXml.parseFloat('0.0')).toBe(0); + expect(TXml.parseFloat('-0.0')).toBe(0); + + expect(TXml.parseFloat('0.1')).toBeCloseTo(0.1); + expect(TXml.parseFloat('1.1')).toBeCloseTo(1.1); + + expect(TXml.parseFloat('19.1134')).toBeCloseTo(19.1134); + expect(TXml.parseFloat('4e2')).toBeCloseTo(4e2); + expect(TXml.parseFloat('4e-2')).toBeCloseTo(4e-2); + + expect(TXml.parseFloat(HUGE_NUMBER_STRING)).toBe(Infinity); + expect(TXml.parseFloat('-' + HUGE_NUMBER_STRING)).toBe(-Infinity); + }); +}); diff --git a/test/util/xml_utils_unit.js b/test/util/xml_utils_unit.js deleted file mode 100644 index d89275b543..0000000000 --- a/test/util/xml_utils_unit.js +++ /dev/null @@ -1,444 +0,0 @@ -/*! @license - * Shaka Player - * Copyright 2016 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -describe('XmlUtils', () => { - // A number that cannot be represented as a Javascript number. - const HUGE_NUMBER_STRING = new Array(500).join('7'); - - const XmlUtils = shaka.util.XmlUtils; - - describe('findChild', () => { - it('finds a child node', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - - expect(XmlUtils.findChild(root, 'Child')).toBeTruthy(); - expect(XmlUtils.findChild(root, 'DoesNotExist')).toBeNull(); - }); - - it('handles duplicate child nodes', () => { - const xmlString = [ - '', - '', - ' ', - ' ', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - - expect(XmlUtils.findChild(root, 'Child')).toBeNull(); - }); - }); - - it('findChildren', () => { - const xmlString = [ - '', - '', - ' ', - ' ', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const roots = XmlUtils.findChildren(xml, 'Root'); - expect(roots).toBeTruthy(); - expect(roots.length).toBe(1); - - let children = XmlUtils.findChildren(roots[0], 'Child'); - expect(children.length).toBe(2); - - children = XmlUtils.findChildren(roots[0], 'DoesNotExist'); - expect(children.length).toBe(0); - }); - - describe('getContents', () => { - it('returns node contents', () => { - const xmlString = [ - '', - '', - ' foo bar', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.getContents(root)).toBe('foo bar'); - }); - - it('handles empty node contents', () => { - const xmlString = [ - '', - '', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.getContents(root)).toBe(''); - }); - - it('handles null node contents', () => { - const xmlString = [ - '', - '', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - expect(XmlUtils.getContents(xml)).toBeNull(); - }); - - it('handles CDATA sections', () => { - const xmlString = [ - '', - '', - ' Bar]]>', - '', - ].join('\n'); - const xml = new DOMParser().parseFromString(xmlString, 'application/xml'); - goog.asserts.assert(xml, 'parseFromString should succeed'); - - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.getContents(root)).toBe(' Bar'); - }); - }); - - describe('parseAttr', () => { - /** @type {!Document} */ - let xml; - - beforeEach(() => { - const xmlString = [ - '', - '', - '', - ].join('\n'); - xml = /** @type {!Document} */ ( - new DOMParser().parseFromString(xmlString, 'application/xml')); - }); - - it('delegates to parser function', () => { - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.parseAttr(root, 'a', XmlUtils.parseRange)).toEqual( - {start: 2, end: 7}); - expect(XmlUtils.parseAttr(root, 'b', XmlUtils.parseInt)).toBe(-5); - expect(XmlUtils.parseAttr(root, 'c', XmlUtils.parseInt)).toBe(0); - expect(XmlUtils.parseAttr(root, 'd', XmlUtils.parseInt)).toBeNull(); - }); - - it('supports default values', () => { - const root = XmlUtils.findChild(xml, 'Root'); - goog.asserts.assert(root, 'findChild should find element'); - expect(XmlUtils.parseAttr(root, 'd', XmlUtils.parseInt, 9)).toBe(9); - }); - }); - - it('parseDate', () => { - // Should be parsed as UTC independent of local timezone. - expect(XmlUtils.parseDate('2015-11-30T12:46:33')).toBe(1448887593); - // Should be parsed using the given timezone, not the local timezone. - expect(XmlUtils.parseDate('2015-11-30T12:46:33+06:00')).toBe(1448865993); - - expect(XmlUtils.parseDate('November 30, 2015')).toBeTruthy(); - expect(XmlUtils.parseDate('Apple')).toBeNull(); - expect(XmlUtils.parseDate('')).toBeNull(); - }); - - it('parseDuration', () => { - // No time. - expect(XmlUtils.parseDuration('P')).toBe(0); - expect(XmlUtils.parseDuration('PT')).toBe(0); - - // Years only. 1 year has 365 or 366 days. - expect(XmlUtils.parseDuration('P3Y')).toBeLessThan( - 3 * (60 * 60 * 24 * 366) + 1); - expect(XmlUtils.parseDuration('P3Y')).toBeGreaterThan( - 3 * (60 * 60 * 24 * 365) - 1); - - // Months only. 1 month has 28 to 31 days. - expect(XmlUtils.parseDuration('P2M')).toBeLessThan( - 2 * (60 * 60 * 24 * 31) + 1); - expect(XmlUtils.parseDuration('P2M')).toBeGreaterThan( - 2 * (60 * 60 * 24 * 28) - 1); - - // Days only. - expect(XmlUtils.parseDuration('P7D')).toBe(604800); - - // Hours only. - expect(XmlUtils.parseDuration('PT1H')).toBe(3600); - - // Minutes only. - expect(XmlUtils.parseDuration('PT1M')).toBe(60); - - // Seconds only (with no fractional part). - expect(XmlUtils.parseDuration('PT1S')).toBe(1); - - // Seconds only (with no whole part). - expect(XmlUtils.parseDuration('PT0.1S')).toBe(0.1); - expect(XmlUtils.parseDuration('PT.1S')).toBe(0.1); - - // Seconds only (with whole part and fractional part). - expect(XmlUtils.parseDuration('PT1.1S')).toBe(1.1); - - // Hours, and minutes. - expect(XmlUtils.parseDuration('PT1H2M')).toBe(3720); - - // Hours, and seconds. - expect(XmlUtils.parseDuration('PT1H2S')).toBe(3602); - expect(XmlUtils.parseDuration('PT1H2.2S')).toBe(3602.2); - - // Minutes, and seconds. - expect(XmlUtils.parseDuration('PT1M2S')).toBe(62); - expect(XmlUtils.parseDuration('PT1M2.2S')).toBe(62.2); - - // Hours, minutes, and seconds. - expect(XmlUtils.parseDuration('PT1H2M3S')).toBe(3723); - expect(XmlUtils.parseDuration('PT1H2M3.3S')).toBe(3723.3); - - // Days, hours, minutes, and seconds. - expect(XmlUtils.parseDuration('P1DT1H2M3S')).toBe(90123); - expect(XmlUtils.parseDuration('P1DT1H2M3.3S')).toBe(90123.3); - - // Months, hours, minutes, and seconds. - expect(XmlUtils.parseDuration('P1M1DT1H2M3S')).toBeLessThan( - (60 * 60 * 24 * 31) + 90123 + 1); - expect(XmlUtils.parseDuration('P1M1DT1H2M3S')).toBeGreaterThan( - (60 * 60 * 24 * 28) + 90123 - 1); - - // Years, Months, hours, minutes, and seconds. - expect(XmlUtils.parseDuration('P1Y1M1DT1H2M3S')).toBeLessThan( - (60 * 60 * 24 * 366) + (60 * 60 * 24 * 31) + 90123 + 1); - expect(XmlUtils.parseDuration('P1Y1M1DT1H2M3S')).toBeGreaterThan( - (60 * 60 * 24 * 365) + (60 * 60 * 24 * 28) + 90123 - 1); - - expect(XmlUtils.parseDuration('PT')).toBe(0); - expect(XmlUtils.parseDuration('P')).toBe(0); - - // Error cases. - expect(XmlUtils.parseDuration('-PT3S')).toBeNull(); - expect(XmlUtils.parseDuration('PT-3S')).toBeNull(); - expect(XmlUtils.parseDuration('P1Sasdf')).toBeNull(); - expect(XmlUtils.parseDuration('1H2M3S')).toBeNull(); - expect(XmlUtils.parseDuration('123')).toBeNull(); - expect(XmlUtils.parseDuration('abc')).toBeNull(); - expect(XmlUtils.parseDuration('')).toBeNull(); - - expect(XmlUtils.parseDuration('P' + HUGE_NUMBER_STRING + 'Y')).toBeNull(); - expect(XmlUtils.parseDuration('P' + HUGE_NUMBER_STRING + 'M')).toBeNull(); - expect(XmlUtils.parseDuration('P' + HUGE_NUMBER_STRING + 'D')).toBeNull(); - expect(XmlUtils.parseDuration('PT' + HUGE_NUMBER_STRING + 'H')).toBeNull(); - expect(XmlUtils.parseDuration('PT' + HUGE_NUMBER_STRING + 'M')).toBeNull(); - expect(XmlUtils.parseDuration('PT' + HUGE_NUMBER_STRING + 'S')).toBeNull(); - }); - - it('parseRange', () => { - expect(XmlUtils.parseRange('0-0')).toEqual({start: 0, end: 0}); - expect(XmlUtils.parseRange('1-1')).toEqual({start: 1, end: 1}); - expect(XmlUtils.parseRange('1-50')).toEqual({start: 1, end: 50}); - expect(XmlUtils.parseRange('50-1')).toEqual({start: 50, end: 1}); - - expect(XmlUtils.parseRange('-1')).toBeNull(); - expect(XmlUtils.parseRange('1-')).toBeNull(); - expect(XmlUtils.parseRange('1')).toBeNull(); - expect(XmlUtils.parseRange('-')).toBeNull(); - expect(XmlUtils.parseRange('')).toBeNull(); - - expect(XmlUtils.parseRange('abc')).toBeNull(); - expect(XmlUtils.parseRange('a-')).toBeNull(); - expect(XmlUtils.parseRange('-b')).toBeNull(); - expect(XmlUtils.parseRange('a-b')).toBeNull(); - - expect(XmlUtils.parseRange(HUGE_NUMBER_STRING + '-1')).toBeNull(); - expect(XmlUtils.parseRange('1-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parseInt', () => { - expect(XmlUtils.parseInt('0')).toBe(0); - expect(XmlUtils.parseInt('1')).toBe(1); - expect(XmlUtils.parseInt('191')).toBe(191); - - expect(XmlUtils.parseInt('-0')).toBe(0); - expect(XmlUtils.parseInt('-1')).toBe(-1); - expect(XmlUtils.parseInt('-191')).toBe(-191); - - expect(XmlUtils.parseInt('abc')).toBeNull(); - expect(XmlUtils.parseInt('1abc')).toBeNull(); - expect(XmlUtils.parseInt('abc1')).toBeNull(); - - expect(XmlUtils.parseInt('0.0')).toBe(0); - expect(XmlUtils.parseInt('-0.0')).toBe(0); - - expect(XmlUtils.parseInt('0.1')).toBeNull(); - expect(XmlUtils.parseInt('1.1')).toBeNull(); - - expect(XmlUtils.parseInt(HUGE_NUMBER_STRING)).toBeNull(); - expect(XmlUtils.parseInt('-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parsePositiveInt', () => { - expect(XmlUtils.parsePositiveInt('0')).toBeNull(); - expect(XmlUtils.parsePositiveInt('1')).toBe(1); - expect(XmlUtils.parsePositiveInt('191')).toBe(191); - - expect(XmlUtils.parsePositiveInt('-0')).toBeNull(); - expect(XmlUtils.parsePositiveInt('-1')).toBeNull(); - expect(XmlUtils.parsePositiveInt('-191')).toBeNull(); - - expect(XmlUtils.parsePositiveInt('abc')).toBeNull(); - expect(XmlUtils.parsePositiveInt('1abc')).toBeNull(); - expect(XmlUtils.parsePositiveInt('abc1')).toBeNull(); - - expect(XmlUtils.parsePositiveInt('0.0')).toBeNull(); - expect(XmlUtils.parsePositiveInt('-0.0')).toBeNull(); - - expect(XmlUtils.parsePositiveInt('0.1')).toBeNull(); - expect(XmlUtils.parsePositiveInt('1.1')).toBeNull(); - - expect(XmlUtils.parsePositiveInt(HUGE_NUMBER_STRING)).toBeNull(); - expect(XmlUtils.parsePositiveInt('-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parseNonNegativeInt', () => { - expect(XmlUtils.parseNonNegativeInt('0')).toBe(0); - expect(XmlUtils.parseNonNegativeInt('1')).toBe(1); - expect(XmlUtils.parseNonNegativeInt('191')).toBe(191); - - expect(XmlUtils.parseNonNegativeInt('-0')).toBe(0); - expect(XmlUtils.parseNonNegativeInt('-1')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('-191')).toBeNull(); - - expect(XmlUtils.parseNonNegativeInt('abc')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('1abc')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('abc1')).toBeNull(); - - expect(XmlUtils.parseNonNegativeInt('0.0')).toBe(0); - expect(XmlUtils.parseNonNegativeInt('-0.0')).toBe(0); - - expect(XmlUtils.parseNonNegativeInt('0.1')).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('1.1')).toBeNull(); - - expect(XmlUtils.parseNonNegativeInt(HUGE_NUMBER_STRING)).toBeNull(); - expect(XmlUtils.parseNonNegativeInt('-' + HUGE_NUMBER_STRING)).toBeNull(); - }); - - it('parseFloat', () => { - expect(XmlUtils.parseFloat('0')).toBe(0); - expect(XmlUtils.parseFloat('1')).toBe(1); - expect(XmlUtils.parseFloat('191')).toBe(191); - - expect(XmlUtils.parseFloat('-0')).toBe(0); - expect(XmlUtils.parseFloat('-1')).toBe(-1); - expect(XmlUtils.parseFloat('-191')).toBe(-191); - - expect(XmlUtils.parseFloat('abc')).toBeNull(); - expect(XmlUtils.parseFloat('1abc')).toBeNull(); - expect(XmlUtils.parseFloat('abc1')).toBeNull(); - - expect(XmlUtils.parseFloat('0.0')).toBe(0); - expect(XmlUtils.parseFloat('-0.0')).toBe(0); - - expect(XmlUtils.parseFloat('0.1')).toBeCloseTo(0.1); - expect(XmlUtils.parseFloat('1.1')).toBeCloseTo(1.1); - - expect(XmlUtils.parseFloat('19.1134')).toBeCloseTo(19.1134); - expect(XmlUtils.parseFloat('4e2')).toBeCloseTo(4e2); - expect(XmlUtils.parseFloat('4e-2')).toBeCloseTo(4e-2); - - expect(XmlUtils.parseFloat(HUGE_NUMBER_STRING)).toBe(Infinity); - expect(XmlUtils.parseFloat('-' + HUGE_NUMBER_STRING)).toBe(-Infinity); - }); - - describe('parseXmlString', () => { - it('parses a simple XML document', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).not.toBeNull(); - expect(doc.tagName).toBe('Root'); - }); - - it('returns null on an empty XML document', () => { - const xmlString = ''; - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - - it('returns null on malformed XML', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - - it('returns null on root element mismatch', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Document'); - expect(doc).toBeNull(); - }); - - it('returns null on XML that embeds HTML', () => { - const xmlString = [ - '', - '', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - - it('returns null on XML that embeds SVG', () => { - // Some platforms, such as Xbox One, don't recognize elements as SVG - // based on namespace alone. So the SVG element below needs to be a real - // SVG element. - const xmlString = [ - '', - '', - ' ', - ' ', - ' ', - '', - ].join('\n'); - const doc = XmlUtils.parseXmlString(xmlString, 'Root'); - expect(doc).toBeNull(); - }); - }); -}); -