static parseCue_(
      cueNode, timeContext, rateInfo, metadataElements, styles, regionElements,
      cueRegions, collapseMultipleSpaces, cellResolution, parentCueElement,
      isContent, uri, images) {
    const TXml = shaka.util.TXml;
    const StringUtils = shaka.util.StringUtils;
    /** @type {shaka.extern.xml.Node} */
    let cueElement;
    /** @type {?shaka.extern.xml.Node} */
    let parentElement = parentCueElement;

    if (TXml.isText(cueNode)) {
      if (!isContent) {
        // Ignore text elements outside the content. For example, whitespace
        // on the same lexical level as the <p> elements, in a document with
        // xml:space="preserve", should not be renderer.
        return null;
      }
      // This should generate an "anonymous span" according to the TTML spec.
      // So pretend the element was a <span>.  parentElement was set above, so
      // we should still be able to correctly traverse up for timing
      // information later.
      /** @type {shaka.extern.xml.Node} */
      const span = {
        tagName: 'span',
        children: [TXml.getTextContents(cueNode)],
        attributes: {},
        parent: null,
      };
      cueElement = span;
    } else {
      cueElement = cueNode;
    }
    goog.asserts.assert(cueElement, 'cueElement should be non-null!');

    let imageElement = null;
    for (const nameSpace of shaka.text.TtmlTextParser.smpteNsList_) {
      imageElement = shaka.text.TtmlTextParser.getElementsFromCollection_(
          cueElement, 'backgroundImage', metadataElements, '#',
          nameSpace)[0];
      if (imageElement) {
        break;
      }
    }

    let imageUri = null;
    const backgroundImage = TXml.getAttributeNSList(
        cueElement,
        shaka.text.TtmlTextParser.smpteNsList_,
        'backgroundImage');
    const imsc1ImgUrnTester =
        /^(urn:)(mpeg:[a-z0-9][a-z0-9-]{0,31}:)(subs:)([0-9]+)$/;
    if (backgroundImage && imsc1ImgUrnTester.test(backgroundImage)) {
      const index = parseInt(backgroundImage.split(':').pop(), 10) -1;
      if (index >= images.length) {
        return null;
      }
      imageUri = images[index];
    } else if (uri && backgroundImage && !backgroundImage.startsWith('#')) {
      const baseUri = new goog.Uri(uri);
      const relativeUri = new goog.Uri(backgroundImage);
      const newUri = baseUri.resolve(relativeUri).toString();
      if (newUri) {
        imageUri = newUri;
      }
    }

    if (cueNode.tagName == 'p' || imageElement || imageUri) {
      isContent = true;
    }

    const parentIsContent = isContent;

    const spaceStyle = cueElement.attributes['xml:space'] ||
        (collapseMultipleSpaces ? 'default' : 'preserve');

    const localCollapseMultipleSpaces = spaceStyle == 'default';

    // Parse any nested cues first.
    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.children) {
        const nestedCue = shaka.text.TtmlTextParser.parseCue_(
            childNode,
            timeContext,
            rateInfo,
            metadataElements,
            styles,
            regionElements,
            cueRegions,
            localCollapseMultipleSpaces,
            cellResolution,
            cueElement,
            isContent,
            uri,
            images,
        );

        // This node may or may not generate a nested cue.
        if (nestedCue) {
          nestedCues.push(nestedCue);
        }
      }
    }

    const isNested = /** @type {boolean} */ (parentCueElement != null);

    const textContent = TXml.getTextContents(cueElement);
    // In this regex, "\S" means "non-whitespace character".
    const hasTextContent = cueElement.children.length &&
        textContent &&
        /\S/.test(textContent);

    const hasTimeAttributes =
        cueElement.attributes['begin'] ||
        cueElement.attributes['end'] ||
        cueElement.attributes['dur'];

    if (!hasTimeAttributes && !hasTextContent && cueElement.tagName != 'br' &&
        nestedCues.length == 0) {
      if (!isNested) {
        // Disregards empty <p> elements without time attributes nor content.
        // <p begin="..." smpte:backgroundImage="..." /> will go through,
        // as some information could be held by its attributes.
        // <p /> won't, as it would not be displayed.
        return null;
      } else if (localCollapseMultipleSpaces) {
        // Disregards empty anonymous spans when (local) trim is true.
        return null;
      }
    }

    // Get local time attributes.
    let {start, end} = shaka.text.TtmlTextParser.parseTime_(
        cueElement, rateInfo);
    // Resolve local time relative to parent elements.  Time elements can appear
    // all the way up to 'body', but not 'tt'.
    while (parentElement && TXml.isNode(parentElement) &&
        parentElement.tagName != 'tt') {
      ({start, end} = shaka.text.TtmlTextParser.resolveTime_(
          parentElement, rateInfo, start, end));
      parentElement =
        /** @type {shaka.extern.xml.Node} */ (parentElement.parent);
    }

    if (start == null) {
      start = 0;
    }
    start += timeContext.periodStart;

    // If end is null, that means the duration is effectively infinite.
    if (end == null) {
      end = Infinity;
    } else {
      end += timeContext.periodStart;
    }

    // Clip times to segment boundaries.
    // https://github.com/shaka-project/shaka-player/issues/4631
    start = Math.max(start, timeContext.segmentStart);
    end = Math.min(end, timeContext.segmentEnd);

    if (!hasTimeAttributes && nestedCues.length > 0) {
      // If no time is defined for this cue, base the timing information on
      // the time of the nested cues. In the case of multiple nested cues with
      // different start times, it is the text displayer's responsibility to
      // make sure that only the appropriate nested cue is drawn at any given
      // time.
      start = Infinity;
      end = 0;
      for (const cue of nestedCues) {
        start = Math.min(start, cue.startTime);
        end = Math.max(end, cue.endTime);
      }
    }

    if (cueElement.tagName == 'br') {
      const cue = new shaka.text.Cue(start, end, '');
      cue.lineBreak = true;
      return cue;
    }

    let payload = '';
    if (isLeafNode) {
      // If the childNodes are all text, this is a leaf node.  Get the payload.
      payload = StringUtils.htmlUnescape(
          shaka.util.TXml.getTextContents(cueElement) || '');
      if (localCollapseMultipleSpaces) {
        // Collapse multiple spaces into one.
        payload = payload.replace(/\s+/g, ' ');
      }
    }

    const cue = new shaka.text.Cue(start, end, payload);
    cue.nestedCues = nestedCues;

    if (!isContent) {
      // If this is not a <p> element or a <div> with images, and it has no
      // parent that was a <p> element, then it's part of the outer containers
      // (e.g. the <body> or a normal <div> element within it).
      cue.isContainer = true;
    }

    if (cellResolution) {
      cue.cellResolution = cellResolution;
    }

    // Get other properties if available.
    const regionElement = shaka.text.TtmlTextParser.getElementsFromCollection_(
        cueElement, 'region', regionElements, /* prefix= */ '')[0];
    // 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.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.attributes['region'] &&
      !cueElement.attributes['style']) {
      regionElementForStyle =
          shaka.text.TtmlTextParser.getElementsFromCollection_(
              parentCueElement, 'region', regionElements, /* prefix= */ '')[0];
    }

    shaka.text.TtmlTextParser.addStyle_(
        cue,
        cueElement,
        regionElementForStyle,
        /** @type {!shaka.extern.xml.Node} */(imageElement),
        imageUri,
        styles,
        /** isNested= */ parentIsContent, // "nested in a <div>" doesn't count.
        /** isLeaf= */ (nestedCues.length == 0));

    return cue;
  }
Parses a TTML node into a Cue.

@param {!shaka.extern.xml.Node} cueNode
@param {shaka.extern.TextParser.TimeContext} timeContext
@param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
@param {!Array<!shaka.extern.xml.Node>} metadataElements
@param {!Array<!shaka.extern.xml.Node>} styles
@param {!Array<!shaka.extern.xml.Node>} regionElements
@param {!Array<!shaka.text.CueRegion>} cueRegions
@param {boolean} collapseMultipleSpaces
@param {?{columns: number, rows: number}} cellResolution
@param {?shaka.extern.xml.Node} parentCueElement
@param {boolean} isContent
@param {?(string|undefined)} uri
@param {!Array<string>} images
@return {shaka.text.Cue}
@private