tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

vtt.sys.mjs (56958B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * Code below is vtt.js the JS WebVTT implementation.
      7 * Current source code can be found at http://github.com/mozilla/vtt.js
      8 *
      9 * Code taken from commit b89bfd06cd788a68c67e03f44561afe833db0849
     10 */
     11 /**
     12 * Copyright 2013 vtt.js Contributors
     13 *
     14 * Licensed under the Apache License, Version 2.0 (the "License");
     15 * you may not use this file except in compliance with the License.
     16 * You may obtain a copy of the License at
     17 *
     18 *   http://www.apache.org/licenses/LICENSE-2.0
     19 *
     20 * Unless required by applicable law or agreed to in writing, software
     21 * distributed under the License is distributed on an "AS IS" BASIS,
     22 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     23 * See the License for the specific language governing permissions and
     24 * limitations under the License.
     25 */
     26 
     27 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     28 
     29 const lazy = {};
     30 
     31 XPCOMUtils.defineLazyPreferenceGetter(lazy, "DEBUG_LOG",
     32                                      "media.webvtt.debug.logging", false);
     33 
     34 function LOG(message) {
     35  if (lazy.DEBUG_LOG) {
     36    dump("[vtt] " + message + "\n");
     37  }
     38 }
     39 
     40 var _objCreate = Object.create || (function() {
     41  function F() {}
     42  return function(o) {
     43    if (arguments.length !== 1) {
     44      throw new Error('Object.create shim only accepts one parameter.');
     45    }
     46    F.prototype = o;
     47    return new F();
     48  };
     49 })();
     50 
     51 // Creates a new ParserError object from an errorData object. The errorData
     52 // object should have default code and message properties. The default message
     53 // property can be overriden by passing in a message parameter.
     54 // See ParsingError.Errors below for acceptable errors.
     55 function ParsingError(errorData, message) {
     56  this.name = "ParsingError";
     57  this.code = errorData.code;
     58  this.message = message || errorData.message;
     59 }
     60 ParsingError.prototype = _objCreate(Error.prototype);
     61 ParsingError.prototype.constructor = ParsingError;
     62 
     63 // ParsingError metadata for acceptable ParsingErrors.
     64 ParsingError.Errors = {
     65  BadSignature: {
     66    code: 0,
     67    message: "Malformed WebVTT signature."
     68  },
     69  BadTimeStamp: {
     70    code: 1,
     71    message: "Malformed time stamp."
     72  }
     73 };
     74 
     75 // See spec, https://w3c.github.io/webvtt/#collect-a-webvtt-timestamp.
     76 function collectTimeStamp(input) {
     77  function computeSeconds(h, m, s, f) {
     78    if (m > 59 || s > 59) {
     79      return null;
     80    }
     81    // The attribute of the milli-seconds can only be three digits.
     82    if (f.length !== 3) {
     83      return null;
     84    }
     85    return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
     86  }
     87 
     88  let timestamp = input.match(/^(\d+:)?(\d{2}):(\d{2})\.(\d+)/);
     89  if (!timestamp || timestamp.length !== 5) {
     90    return null;
     91  }
     92 
     93  let hours = timestamp[1]? timestamp[1].replace(":", "") : 0;
     94  let minutes = timestamp[2];
     95  let seconds = timestamp[3];
     96  let milliSeconds = timestamp[4];
     97 
     98  return computeSeconds(hours, minutes, seconds, milliSeconds);
     99 }
    100 
    101 // A settings object holds key/value pairs and will ignore anything but the first
    102 // assignment to a specific key.
    103 function Settings() {
    104  this.values = _objCreate(null);
    105 }
    106 
    107 Settings.prototype = {
    108  set: function(k, v) {
    109    if (v !== "") {
    110      this.values[k] = v;
    111    }
    112  },
    113  // Return the value for a key, or a default value.
    114  // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
    115  // a number of possible default values as properties where 'defaultKey' is
    116  // the key of the property that will be chosen; otherwise it's assumed to be
    117  // a single value.
    118  get: function(k, dflt, defaultKey) {
    119    if (defaultKey) {
    120      return this.has(k) ? this.values[k] : dflt[defaultKey];
    121    }
    122    return this.has(k) ? this.values[k] : dflt;
    123  },
    124  // Check whether we have a value for a key.
    125  has: function(k) {
    126    return k in this.values;
    127  },
    128  // Accept a setting if its one of the given alternatives.
    129  alt: function(k, v, a) {
    130    for (let n = 0; n < a.length; ++n) {
    131      if (v === a[n]) {
    132        this.set(k, v);
    133        return true;
    134      }
    135    }
    136    return false;
    137  },
    138  // Accept a setting if its a valid digits value (int or float)
    139  digitsValue: function(k, v) {
    140    if (/^-0+(\.[0]*)?$/.test(v)) { // special case for -0.0
    141      this.set(k, 0.0);
    142    } else if (/^-?\d+(\.[\d]*)?$/.test(v)) {
    143      this.set(k, parseFloat(v));
    144    }
    145  },
    146  // Accept a setting if its a valid percentage.
    147  percent: function(k, v) {
    148    let m;
    149    if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) {
    150      v = parseFloat(v);
    151      if (v >= 0 && v <= 100) {
    152        this.set(k, v);
    153        return true;
    154      }
    155    }
    156    return false;
    157  },
    158  // Delete a setting
    159  del: function (k) {
    160    if (this.has(k)) {
    161      delete this.values[k];
    162    }
    163  },
    164 };
    165 
    166 // Helper function to parse input into groups separated by 'groupDelim', and
    167 // interprete each group as a key/value pair separated by 'keyValueDelim'.
    168 function parseOptions(input, callback, keyValueDelim, groupDelim) {
    169  let groups = groupDelim ? input.split(groupDelim) : [input];
    170  for (let i in groups) {
    171    if (typeof groups[i] !== "string") {
    172      continue;
    173    }
    174    let kv = groups[i].split(keyValueDelim);
    175    if (kv.length !== 2) {
    176      continue;
    177    }
    178    let k = kv[0];
    179    let v = kv[1];
    180    callback(k, v);
    181  }
    182 }
    183 
    184 function parseCue(input, cue, regionList) {
    185  // Remember the original input if we need to throw an error.
    186  let oInput = input;
    187  // 4.1 WebVTT timestamp
    188  function consumeTimeStamp() {
    189    let ts = collectTimeStamp(input);
    190    if (ts === null) {
    191      throw new ParsingError(ParsingError.Errors.BadTimeStamp,
    192                            "Malformed timestamp: " + oInput);
    193    }
    194    // Remove time stamp from input.
    195    input = input.replace(/^[^\s\uFFFDa-zA-Z-]+/, "");
    196    return ts;
    197  }
    198 
    199  // 4.4.2 WebVTT cue settings
    200  function consumeCueSettings(input, cue) {
    201    let settings = new Settings();
    202    parseOptions(input, function (k, v) {
    203      switch (k) {
    204      case "region":
    205        // Find the last region we parsed with the same region id.
    206        for (let i = regionList.length - 1; i >= 0; i--) {
    207          if (regionList[i].id === v) {
    208            settings.set(k, regionList[i].region);
    209            break;
    210          }
    211        }
    212        break;
    213      case "vertical":
    214        settings.alt(k, v, ["rl", "lr"]);
    215        break;
    216      case "line": {
    217        let vals = v.split(",");
    218        let vals0 = vals[0];
    219        settings.digitsValue(k, vals0);
    220        settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
    221        settings.alt(k, vals0, ["auto"]);
    222        if (vals.length === 2) {
    223          settings.alt("lineAlign", vals[1], ["start", "center", "end"]);
    224        }
    225        break;
    226      }
    227      case "position": {
    228        let vals = v.split(",");
    229        if (settings.percent(k, vals[0])) {
    230          if (vals.length === 2) {
    231            if (!settings.alt("positionAlign", vals[1], ["line-left", "center", "line-right"])) {
    232              // Remove the "position" value because the "positionAlign" is not expected value.
    233              // It will be set to default value below.
    234              settings.del(k);
    235            }
    236          }
    237        }
    238        break;
    239      }
    240      case "size":
    241        settings.percent(k, v);
    242        break;
    243      case "align":
    244        settings.alt(k, v, ["start", "center", "end", "left", "right"]);
    245        break;
    246      }
    247    }, /:/, /\t|\n|\f|\r| /); // groupDelim is ASCII whitespace
    248 
    249    // Apply default values for any missing fields.
    250    // https://w3c.github.io/webvtt/#collect-a-webvtt-block step 11.4.1.3
    251    cue.region = settings.get("region", null);
    252    cue.vertical = settings.get("vertical", "");
    253    cue.line = settings.get("line", "auto");
    254    cue.lineAlign = settings.get("lineAlign", "start");
    255    cue.snapToLines = settings.get("snapToLines", true);
    256    cue.size = settings.get("size", 100);
    257    cue.align = settings.get("align", "center");
    258    cue.position = settings.get("position", "auto");
    259    cue.positionAlign = settings.get("positionAlign", "auto");
    260  }
    261 
    262  function skipWhitespace() {
    263    input = input.replace(/^[ \f\n\r\t]+/, "");
    264  }
    265 
    266  // 4.1 WebVTT cue timings.
    267  skipWhitespace();
    268  cue.startTime = consumeTimeStamp();   // (1) collect cue start time
    269  skipWhitespace();
    270  if (input.substr(0, 3) !== "-->") {     // (3) next characters must match "-->"
    271    throw new ParsingError(ParsingError.Errors.BadTimeStamp,
    272                            "Malformed time stamp (time stamps must be separated by '-->'): " +
    273                            oInput);
    274  }
    275  input = input.substr(3);
    276  skipWhitespace();
    277  cue.endTime = consumeTimeStamp();     // (5) collect cue end time
    278 
    279  // 4.1 WebVTT cue settings list.
    280  skipWhitespace();
    281  consumeCueSettings(input, cue);
    282 }
    283 
    284 function emptyOrOnlyContainsWhiteSpaces(input) {
    285  return input == "" || /^[ \f\n\r\t]+$/.test(input);
    286 }
    287 
    288 function containsTimeDirectionSymbol(input) {
    289  return input.includes("-->");
    290 }
    291 
    292 function maybeIsTimeStampFormat(input) {
    293  return /^\s*(\d+:)?(\d{2}):(\d{2})\.(\d+)\s*-->\s*(\d+:)?(\d{2}):(\d{2})\.(\d+)\s*/.test(input);
    294 }
    295 
    296 var ESCAPE = {
    297  "&amp;": "&",
    298  "&lt;": "<",
    299  "&gt;": ">",
    300  "&lrm;": "\u200e",
    301  "&rlm;": "\u200f",
    302  "&nbsp;": "\u00a0"
    303 };
    304 
    305 var TAG_NAME = {
    306  c: "span",
    307  i: "i",
    308  b: "b",
    309  u: "u",
    310  ruby: "ruby",
    311  rt: "rt",
    312  v: "span",
    313  lang: "span"
    314 };
    315 
    316 var TAG_ANNOTATION = {
    317  v: "title",
    318  lang: "lang"
    319 };
    320 
    321 var NEEDS_PARENT = {
    322  rt: "ruby"
    323 };
    324 
    325 const PARSE_CONTENT_MODE = {
    326  NORMAL_CUE: "normal_cue",
    327  DOCUMENT_FRAGMENT: "document_fragment",
    328  REGION_CUE: "region_cue",
    329 }
    330 // Parse content into a document fragment.
    331 function parseContent(window, input, mode) {
    332  function nextToken() {
    333    // Check for end-of-string.
    334    if (!input) {
    335      return null;
    336    }
    337 
    338    // Consume 'n' characters from the input.
    339    function consume(result) {
    340      input = input.substr(result.length);
    341      return result;
    342    }
    343 
    344    let m = input.match(/^([^<]*)(<[^>]+>?)?/);
    345    // The input doesn't contain a complete tag.
    346    if (!m[0]) {
    347      return null;
    348    }
    349    // If there is some text before the next tag, return it, otherwise return
    350    // the tag.
    351    return consume(m[1] ? m[1] : m[2]);
    352  }
    353 
    354  const unescapeHelper = window.document.createElement("div");
    355  function unescapeEntities(s) {
    356    let match;
    357 
    358    // Decimal numeric character reference
    359    s = s.replace(/&#(\d+);?/g, (candidate, number) => {
    360      try {
    361        const codepoint = parseInt(number);
    362        return String.fromCodePoint(codepoint);
    363      } catch (_) {
    364        return candidate;
    365      }
    366    });
    367 
    368    // Hexadecimal numeric character reference
    369    s = s.replace(/&#x([\dA-Fa-f]+);?/g, (candidate, number) => {
    370      try {
    371        const codepoint = parseInt(number, 16);
    372        return String.fromCodePoint(codepoint);
    373      } catch (_) {
    374        return candidate;
    375      }
    376    });
    377 
    378    // Named character references
    379    s = s.replace(/&\w[\w\d]*;?/g, candidate => {
    380      // The list of entities is huge, so we use innerHTML instead.
    381      // We should probably use setHTML instead once that is available (bug 1650370).
    382      // Ideally we would be able to use a faster/simpler variant of setHTML (bug 1731215).
    383      unescapeHelper.innerHTML = candidate;
    384      const unescaped = unescapeHelper.innerText;
    385      if (unescaped == candidate) { // not a valid entity
    386        return candidate;
    387      }
    388      return unescaped;
    389    });
    390    unescapeHelper.innerHTML = "";
    391 
    392    return s;
    393  }
    394 
    395  function shouldAdd(current, element) {
    396    return !NEEDS_PARENT[element.localName] ||
    397            NEEDS_PARENT[element.localName] === current.localName;
    398  }
    399 
    400  // Create an element for this tag.
    401  function createElement(type, annotation) {
    402    let tagName = TAG_NAME[type];
    403    if (!tagName) {
    404      return null;
    405    }
    406    let element = window.document.createElement(tagName);
    407    let name = TAG_ANNOTATION[type];
    408    if (name) {
    409      element[name] = annotation ? annotation.trim() : "";
    410    }
    411    return element;
    412  }
    413 
    414  // https://w3c.github.io/webvtt/#webvtt-timestamp-object
    415  // Return hhhhh:mm:ss.fff
    416  function normalizedTimeStamp(secondsWithFrag) {
    417    let totalsec = parseInt(secondsWithFrag, 10);
    418    let hours = Math.floor(totalsec / 3600);
    419    let minutes = Math.floor(totalsec % 3600 / 60);
    420    let seconds = Math.floor(totalsec % 60);
    421    if (hours < 10) {
    422      hours = "0" + hours;
    423    }
    424    if (minutes < 10) {
    425      minutes = "0" + minutes;
    426    }
    427    if (seconds < 10) {
    428      seconds = "0" + seconds;
    429    }
    430    let f = secondsWithFrag.toString().split(".");
    431    if (f[1]) {
    432      f = f[1].slice(0, 3).padEnd(3, "0");
    433    } else {
    434      f = "000";
    435    }
    436    return hours + ':' + minutes + ':' + seconds + '.' + f;
    437  }
    438 
    439  let root;
    440  switch (mode) {
    441    case PARSE_CONTENT_MODE.NORMAL_CUE:
    442      root = window.document.createElement("span", {pseudo: "::cue"});
    443      break;
    444    case PARSE_CONTENT_MODE.REGION_CUE:
    445      root = window.document.createElement("span");
    446      break;
    447    case PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT:
    448      root = window.document.createDocumentFragment();
    449      break;
    450  }
    451 
    452  if (!input) {
    453    root.appendChild(window.document.createTextNode(""));
    454    return root;
    455  }
    456 
    457  let current = root,
    458      t,
    459      tagStack = [];
    460 
    461  while ((t = nextToken()) !== null) {
    462    if (t[0] === '<') {
    463      if (t[1] === "/") {
    464        const endTag = t.slice(2, -1);
    465        const stackEnd = tagStack.at(-1);
    466 
    467        // If the closing tag matches, move back up to the parent node.
    468        if (stackEnd == endTag) {
    469          tagStack.pop();
    470          current = current.parentNode;
    471 
    472        // If the closing tag is <ruby> and we're at an <rt>, move back up to
    473        // the <ruby>'s parent node.
    474        } else if (endTag == "ruby" && current.nodeName == "RT") {
    475          tagStack.pop();
    476          current = current.parentNode.parentNode;
    477        }
    478 
    479        // Otherwise just ignore the end tag.
    480        continue;
    481      }
    482      let ts = collectTimeStamp(t.substr(1, t.length - 1));
    483      let node;
    484      if (ts) {
    485        // Timestamps are lead nodes as well.
    486        node = window.document.createProcessingInstruction("timestamp", normalizedTimeStamp(ts));
    487        current.appendChild(node);
    488        continue;
    489      }
    490      let m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
    491      // If we can't parse the tag, skip to the next tag.
    492      if (!m) {
    493        continue;
    494      }
    495      // Try to construct an element, and ignore the tag if we couldn't.
    496      node = createElement(m[1], m[3]);
    497      if (!node) {
    498        continue;
    499      }
    500      // Determine if the tag should be added based on the context of where it
    501      // is placed in the cuetext.
    502      if (!shouldAdd(current, node)) {
    503        continue;
    504      }
    505      // Set the class list (as a list of classes, separated by space).
    506      if (m[2]) {
    507        node.className = m[2].substr(1).replace('.', ' ');
    508      }
    509      // Append the node to the current node, and enter the scope of the new
    510      // node.
    511      tagStack.push(m[1]);
    512      current.appendChild(node);
    513      current = node;
    514      continue;
    515    }
    516 
    517    // Text nodes are leaf nodes.
    518    current.appendChild(window.document.createTextNode(unescapeEntities(t)));
    519  }
    520 
    521  return root;
    522 }
    523 
    524 function StyleBox() {
    525 }
    526 
    527 // Apply styles to a div. If there is no div passed then it defaults to the
    528 // div on 'this'.
    529 StyleBox.prototype.applyStyles = function(styles, div) {
    530  div = div || this.div;
    531  for (let prop in styles) {
    532    if (styles.hasOwnProperty(prop)) {
    533      div.style[prop] = styles[prop];
    534    }
    535  }
    536 };
    537 
    538 StyleBox.prototype.formatStyle = function(val, unit) {
    539  return val === 0 ? 0 : val + unit;
    540 };
    541 
    542 // TODO(alwu): remove StyleBox and change other style box to class-based.
    543 class StyleBoxBase {
    544  applyStyles(styles, div) {
    545    div = div || this.div;
    546    Object.assign(div.style, styles);
    547  }
    548 
    549  formatStyle(val, unit) {
    550    return val === 0 ? 0 : val + unit;
    551  }
    552 }
    553 
    554 // Constructs the computed display state of the cue (a div). Places the div
    555 // into the overlay which should be a block level element (usually a div).
    556 class CueStyleBox extends StyleBoxBase {
    557  constructor(window, cue, containerBox) {
    558    super();
    559    this.cue = cue;
    560    this.div = window.document.createElement("div");
    561    this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.NORMAL_CUE);
    562    this.div.appendChild(this.cueDiv);
    563 
    564    this.containerHeight = containerBox.height;
    565    this.containerWidth = containerBox.width;
    566    this.fontSize = this._getFontSize(containerBox);
    567    this.isCueStyleBox = true;
    568 
    569    // As pseudo element won't inherit the parent div's style, so we have to
    570    // set the font size explicitly.
    571    this._applyDefaultStylesOnBackgroundNode();
    572    this._applyDefaultStylesOnRootNode();
    573  }
    574 
    575  getCueBoxPositionAndSize() {
    576    // As `top`, `left`, `width` and `height` are all represented by the
    577    // percentage of the container, we need to convert them to the actual
    578    // number according to the container's size.
    579    const isWritingDirectionHorizontal = this.cue.vertical == "";
    580    let top =
    581          this.containerHeight * this._tranferPercentageToFloat(this.div.style.top),
    582        left =
    583          this.containerWidth * this._tranferPercentageToFloat(this.div.style.left),
    584        width = isWritingDirectionHorizontal ?
    585          this.containerWidth * this._tranferPercentageToFloat(this.div.style.width) :
    586          this.div.clientWidthDouble,
    587        height = isWritingDirectionHorizontal ?
    588          this.div.clientHeightDouble :
    589          this.containerHeight * this._tranferPercentageToFloat(this.div.style.height);
    590    return { top, left, width, height };
    591  }
    592 
    593  getFirstLineBoxSize() {
    594    // This size would be automatically adjusted by writing direction. When
    595    // direction is horizontal, it represents box's height. When direction is
    596    // vertical, it represents box's width.
    597    return this.div.firstLineBoxBSize;
    598  }
    599 
    600  setBidiRule() {
    601    // This function is a workaround which is used to force the reflow in order
    602    // to use the correct alignment for bidi text. Now this function would be
    603    // called after calculating the final position of the cue box to ensure the
    604    // rendering result is correct. See bug1557882 comment3 for more details.
    605    // TODO : remove this function and set `unicode-bidi` when initiailizing
    606    // the CueStyleBox, after fixing bug1558431.
    607    this.applyStyles({ "unicode-bidi": "plaintext" });
    608  }
    609 
    610  /**
    611   * Following methods are private functions, should not use them outside this
    612   * class.
    613   */
    614  _tranferPercentageToFloat(input) {
    615    return input.replace("%", "") / 100.0;
    616  }
    617 
    618  _getFontSize(containerBox) {
    619    // In https://www.w3.org/TR/webvtt1/#applying-css-properties, the spec
    620    // said the font size is '5vh', which means 5% of the viewport height.
    621    // However, if we use 'vh' as a basic unit, it would eventually become
    622    // 5% of screen height, instead of video's viewport height. Therefore, we
    623    // have to use 'px' here to make sure we have the correct font size.
    624    //
    625    // Note Chromium uses min(width, height) instead of just height, to not
    626    // make the font unexpectedly large on portrait videos. This matches that
    627    // behavior.
    628    // TODO: Update this when the spec has settled
    629    //       https://github.com/w3c/webvtt/issues/529.
    630    return Math.min(containerBox.width, containerBox.height) * 0.05 + "px";
    631  }
    632 
    633  _applyDefaultStylesOnBackgroundNode() {
    634    // most of the properties have been defined in `::cue` in `html.css`, but
    635    // there are some css properties we have to set them dynamically.
    636    // FIXME(emilio): These are observable by content. Ideally the style
    637    // attribute will work like for ::part() and we wouldn't need this.
    638    this.cueDiv.style.setProperty("--cue-font-size", this.fontSize, "important");
    639    this.cueDiv.style.setProperty("--cue-writing-mode", this._getCueWritingMode(), "important");
    640  }
    641 
    642  // spec https://www.w3.org/TR/webvtt1/#applying-css-properties
    643  _applyDefaultStylesOnRootNode() {
    644    // The variables writing-mode, top, left, width, and height are calculated
    645    // in the spec 7.2, https://www.w3.org/TR/webvtt1/#processing-cue-settings
    646    // spec 7.2.1, calculate 'writing-mode'.
    647    const writingMode = this._getCueWritingMode();
    648 
    649    // spec 7.2.2 ~ 7.2.7, calculate 'width', 'height', 'left' and 'top'.
    650    const {width, height, left, top} = this._getCueSizeAndPosition();
    651 
    652    // TODO: https://github.com/w3c/webvtt/issues/530 for potentially making
    653    // the cue container's font-size 0.
    654    //
    655    // The inline cue element cannot make the size of the parent element, this
    656    // container, smaller. If the inline cue element is larger, the line height
    657    // grows. If it's smaller than the container, it's stuck with the
    658    // container's height. This becomes a problem when the container's font-size
    659    // is large and a site wants to style the ::cue pseudo element significantly
    660    // smaller. This is less of a problem when using the equivalent of 5vmin
    661    // instead of 5vh of course, but it's still a problem. It would be most
    662    // visible in large videos with 1:1 aspect ratio that a site tries to scale
    663    // down.
    664    //
    665    // All WebVTT use videos with min(width, height) 180, 5% of which is 9px.
    666    // 9px font-size keeps tests passing.
    667    const fontSize = "9px";
    668 
    669    this.applyStyles({
    670      "position": "absolute",
    671      // "unicode-bidi": "plaintext", (uncomment this line after fixing bug1558431)
    672      "writing-mode": writingMode,
    673      "top": top,
    674      "left": left,
    675      "width": width,
    676      "height": height,
    677      "overflow-wrap": "break-word",
    678      // "text-wrap": "balance", (we haven't supported this CSS attribute yet)
    679      "white-space": "pre-line",
    680      "font": `${fontSize} sans-serif`,
    681      "color": "rgba(255, 255, 255, 1)",
    682      "white-space": "pre-line",
    683      "text-align": this.cue.align,
    684    });
    685  }
    686 
    687  _getCueWritingMode() {
    688    const cue = this.cue;
    689    if (cue.vertical == "") {
    690      return "horizontal-tb";
    691    }
    692    return cue.vertical == "lr" ? "vertical-lr" : "vertical-rl";
    693  }
    694 
    695  _getCueSizeAndPosition() {
    696    const cue = this.cue;
    697    // spec 7.2.2, determine the value of maximum size for cue as per the
    698    // appropriate rules from the following list.
    699    let maximumSize;
    700    let computedPosition = cue.computedPosition;
    701    switch (cue.computedPositionAlign) {
    702      case "line-left":
    703        maximumSize = 100 - computedPosition;
    704        break;
    705      case "line-right":
    706        maximumSize = computedPosition;
    707        break;
    708      case "center":
    709        maximumSize = computedPosition <= 50 ?
    710          computedPosition * 2 : (100 - computedPosition) * 2;
    711        break;
    712    }
    713    const size = Math.min(cue.size, maximumSize);
    714 
    715    // spec 7.2.5, determine the value of x-position or y-position for cue as
    716    // per the appropriate rules from the following list.
    717    let xPosition = 0.0, yPosition = 0.0;
    718    const isWritingDirectionHorizontal = cue.vertical == "";
    719    switch (cue.computedPositionAlign) {
    720      case "line-left":
    721        if (isWritingDirectionHorizontal) {
    722          xPosition = cue.computedPosition;
    723        } else {
    724          yPosition = cue.computedPosition;
    725        }
    726        break;
    727      case "center":
    728        if (isWritingDirectionHorizontal) {
    729          xPosition = cue.computedPosition - (size / 2);
    730        } else {
    731          yPosition = cue.computedPosition - (size / 2);
    732        }
    733        break;
    734      case "line-right":
    735        if (isWritingDirectionHorizontal) {
    736          xPosition = cue.computedPosition - size;
    737        } else {
    738          yPosition = cue.computedPosition - size;
    739        }
    740        break;
    741    }
    742 
    743    // spec 7.2.6, determine the value of whichever of x-position or
    744    // y-position is not yet calculated for cue as per the appropriate rules
    745    // from the following list.
    746    if (!cue.snapToLines) {
    747      if (isWritingDirectionHorizontal) {
    748        yPosition = cue.computedLine;
    749      } else {
    750        xPosition = cue.computedLine;
    751      }
    752    } else {
    753      if (isWritingDirectionHorizontal) {
    754        yPosition = 0;
    755      } else {
    756        xPosition = 0;
    757      }
    758    }
    759    return {
    760      left: xPosition + "%",
    761      top: yPosition + "%",
    762      width: isWritingDirectionHorizontal ? size + "%" : "auto",
    763      height: isWritingDirectionHorizontal ? "auto" : size + "%",
    764    };
    765  }
    766 }
    767 
    768 function RegionNodeBox(window, region, container) {
    769  StyleBox.call(this);
    770 
    771  // TODO: Update this when the spec has settled
    772  //       https://github.com/w3c/webvtt/issues/529.
    773  let boxLineHeight = Math.min(container.width, container.height) * 0.0533 // 0.0533vh ? 5.33vh
    774  let boxHeight = boxLineHeight * region.lines;
    775  let boxWidth = container.width * region.width / 100; // convert percentage to px
    776 
    777  let regionNodeStyles = {
    778    position: "absolute",
    779    height: boxHeight + "px",
    780    width: boxWidth + "px",
    781    top: (region.viewportAnchorY * container.height / 100) - (region.regionAnchorY * boxHeight / 100) + "px",
    782    left: (region.viewportAnchorX * container.width / 100) - (region.regionAnchorX * boxWidth / 100) + "px",
    783    lineHeight: boxLineHeight + "px",
    784    writingMode: "horizontal-tb",
    785    backgroundColor: "rgba(0, 0, 0, 0.8)",
    786    wordWrap: "break-word",
    787    overflowWrap: "break-word",
    788    font: (boxLineHeight/1.3) + "px sans-serif",
    789    color: "rgba(255, 255, 255, 1)",
    790    overflow: "hidden",
    791    minHeight: "0px",
    792    maxHeight: boxHeight + "px",
    793    display: "inline-flex",
    794    flexFlow: "column",
    795    justifyContent: "flex-end",
    796  };
    797 
    798  this.div = window.document.createElement("div");
    799  this.div.id = region.id; // useless?
    800  this.applyStyles(regionNodeStyles);
    801 }
    802 RegionNodeBox.prototype = _objCreate(StyleBox.prototype);
    803 RegionNodeBox.prototype.constructor = RegionNodeBox;
    804 
    805 function RegionCueStyleBox(window, cue) {
    806  StyleBox.call(this);
    807  this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.REGION_CUE);
    808 
    809  let regionCueStyles = {
    810    position: "relative",
    811    writingMode: "horizontal-tb",
    812    unicodeBidi: "plaintext",
    813    width: "auto",
    814    height: "auto",
    815    textAlign: cue.align,
    816  };
    817  // TODO: fix me, LTR and RTL ? using margin replace the "left/right"
    818  // 6.1.14.3.3
    819  let offset = cue.computedPosition * cue.region.width / 100;
    820  // 6.1.14.3.4
    821  switch (cue.align) {
    822    case "start":
    823    case "left":
    824      regionCueStyles.left = offset + "%";
    825      regionCueStyles.right = "auto";
    826      break;
    827    case "end":
    828    case "right":
    829      regionCueStyles.left = "auto";
    830      regionCueStyles.right = offset + "%";
    831      break;
    832    case "middle":
    833      break;
    834  }
    835 
    836  this.div = window.document.createElement("div");
    837  this.applyStyles(regionCueStyles);
    838  this.div.appendChild(this.cueDiv);
    839 }
    840 RegionCueStyleBox.prototype = _objCreate(StyleBox.prototype);
    841 RegionCueStyleBox.prototype.constructor = RegionCueStyleBox;
    842 
    843 // Represents the co-ordinates of an Element in a way that we can easily
    844 // compute things with such as if it overlaps or intersects with other boxes.
    845 class BoxPosition {
    846  constructor(obj) {
    847    // Get dimensions by calling getCueBoxPositionAndSize on a CueStyleBox, by
    848    // getting offset properties from an HTMLElement (from the object or its
    849    // `div` property), otherwise look at the regular box properties on the
    850    // object.
    851    const isHTMLElement = !obj.isCueStyleBox && (obj.div || obj.tagName);
    852    obj = obj.isCueStyleBox ? obj.getCueBoxPositionAndSize() : obj.div || obj;
    853    this.top = isHTMLElement ? obj.offsetTop : obj.top;
    854    this.left = isHTMLElement ? obj.offsetLeft : obj.left;
    855    this.width = isHTMLElement ? obj.offsetWidth : obj.width;
    856    this.height = isHTMLElement ? obj.offsetHeight : obj.height;
    857    // This value is smaller than 1 app unit (~= 0.0166 px).
    858    this.fuzz = 0.01;
    859  }
    860 
    861  get bottom() {
    862    return this.top + this.height;
    863  }
    864 
    865  get right() {
    866    return this.left + this.width;
    867  }
    868 
    869  // This function is used for debugging, it will return the box's information.
    870  getBoxInfoInChars() {
    871    return `top=${this.top}, bottom=${this.bottom}, left=${this.left}, ` +
    872            `right=${this.right}, width=${this.width}, height=${this.height}`;
    873  }
    874 
    875  // Move the box along a particular axis. Optionally pass in an amount to move
    876  // the box. If no amount is passed then the default is the line height of the
    877  // box.
    878  move(axis, toMove) {
    879    switch (axis) {
    880    case "+x":
    881      LOG(`box's left moved from ${this.left} to ${this.left + toMove}`);
    882      this.left += toMove;
    883      break;
    884    case "-x":
    885      LOG(`box's left moved from ${this.left} to ${this.left - toMove}`);
    886      this.left -= toMove;
    887      break;
    888    case "+y":
    889      LOG(`box's top moved from ${this.top} to ${this.top + toMove}`);
    890      this.top += toMove;
    891      break;
    892    case "-y":
    893      LOG(`box's top moved from ${this.top} to ${this.top - toMove}`);
    894      this.top -= toMove;
    895      break;
    896    }
    897  }
    898 
    899  // Check if this box overlaps another box, b2.
    900  overlaps(b2) {
    901    return (this.left < b2.right - this.fuzz) &&
    902            (this.right > b2.left + this.fuzz) &&
    903            (this.top < b2.bottom - this.fuzz) &&
    904            (this.bottom > b2.top + this.fuzz);
    905  }
    906 
    907  // Check if this box overlaps any other boxes in boxes.
    908  overlapsAny(boxes) {
    909    for (let i = 0; i < boxes.length; i++) {
    910      if (this.overlaps(boxes[i])) {
    911        return true;
    912      }
    913    }
    914    return false;
    915  }
    916 
    917  // Check if this box is within another box.
    918  within(container) {
    919    return (this.top >= container.top - this.fuzz) &&
    920            (this.bottom <= container.bottom + this.fuzz) &&
    921            (this.left >= container.left - this.fuzz) &&
    922            (this.right <= container.right + this.fuzz);
    923  }
    924 
    925  // Check whether this box is passed over the specfic axis boundary. The axis
    926  // is based on the canvas coordinates, the `+x` is rightward and `+y` is
    927  // downward.
    928  isOutsideTheAxisBoundary(container, axis) {
    929    switch (axis) {
    930    case "+x":
    931      return this.right > container.right + this.fuzz;
    932    case "-x":
    933      return this.left < container.left - this.fuzz;
    934    case "+y":
    935      return this.bottom > container.bottom + this.fuzz;
    936    case "-y":
    937      return this.top < container.top - this.fuzz;
    938    }
    939  }
    940 
    941  // Find the percentage of the area that this box is overlapping with another
    942  // box.
    943  intersectPercentage(b2) {
    944    let x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
    945        y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
    946        intersectArea = x * y;
    947    return intersectArea / (this.height * this.width);
    948  }
    949 }
    950 
    951 BoxPosition.prototype.clone = function(){
    952  return new BoxPosition(this);
    953 };
    954 
    955 function adjustBoxPosition(styleBox, containerBox, controlBarBox, outputBoxes) {
    956  const cue = styleBox.cue;
    957  const isWritingDirectionHorizontal = cue.vertical == "";
    958  let box = new BoxPosition(styleBox);
    959  if (!box.width || !box.height) {
    960    LOG(`No way to adjust a box with zero width or height.`);
    961    return;
    962  }
    963 
    964  // Spec 7.2.10, adjust the positions of boxes according to the appropriate
    965  // steps from the following list. Also, we use offsetHeight/offsetWidth here
    966  // in order to prevent the incorrect positioning caused by CSS transform
    967  // scale.
    968  const fullDimension = isWritingDirectionHorizontal ?
    969    containerBox.height : containerBox.width;
    970  if (cue.snapToLines) {
    971    LOG(`Adjust position when 'snap-to-lines' is true.`);
    972    // The step is the height or width of the line box. We should use font
    973    // size directly, instead of using text box's width or height, because the
    974    // width or height of the box would be changed when the text is wrapped to
    975    // different line. Ex. if text is wrapped to two line, the height or width
    976    // of the box would become 2 times of font size.
    977    let step = styleBox.getFirstLineBoxSize();
    978    if (step == 0) {
    979      return;
    980    }
    981 
    982    // spec 7.2.10.4 ~ 7.2.10.6
    983    let line = Math.floor(cue.computedLine + 0.5);
    984    if (cue.vertical == "rl") {
    985      line = -1 * (line + 1);
    986    }
    987 
    988    // spec 7.2.10.7 ~ 7.2.10.8
    989    let position = step * line;
    990    if (cue.vertical == "rl") {
    991      position = position - box.width + step;
    992    }
    993 
    994    // spec 7.2.10.9
    995    if (line < 0) {
    996      position += fullDimension;
    997      step = -1 * step;
    998    }
    999 
   1000    // spec 7.2.10.10, move the box to the specific position along the direction.
   1001    const movingDirection = isWritingDirectionHorizontal ? "+y" : "+x";
   1002    box.move(movingDirection, position);
   1003 
   1004    // spec 7.2.10.11, remember the position as specified position.
   1005    let specifiedPosition = box.clone();
   1006 
   1007    // spec 7.2.10.12, let title area be a box that covers all of the video’s
   1008    // rendering area.
   1009    const titleAreaBox = containerBox.clone();
   1010    if (controlBarBox) {
   1011      titleAreaBox.height -= controlBarBox.height;
   1012    }
   1013 
   1014    function isBoxOutsideTheRenderingArea() {
   1015      if (isWritingDirectionHorizontal) {
   1016        // the top side of the box is above the rendering area, or the bottom
   1017        // side of the box is below the rendering area.
   1018        return step < 0 && box.top < 0 ||
   1019                step > 0 && box.bottom > fullDimension;
   1020      }
   1021      // the left side of the box is outside the left side of the rendering
   1022      // area, or the right side of the box is outside the right side of the
   1023      // rendering area.
   1024      return step < 0 && box.left < 0 ||
   1025              step > 0 && box.right > fullDimension;
   1026    }
   1027 
   1028    // spec 7.2.10.13, if none of the boxes in boxes would overlap any of the
   1029    // boxes in output, and all of the boxes in boxes are entirely within the
   1030    // title area box.
   1031    let switched = false;
   1032    while (!box.within(titleAreaBox) || box.overlapsAny(outputBoxes)) {
   1033      // spec 7.2.10.14, check if we need to switch the direction.
   1034      if (isBoxOutsideTheRenderingArea()) {
   1035        // spec 7.2.10.17, if `switched` is true, remove all the boxes in
   1036        // `boxes`, which means we shouldn't apply any CSS boxes for this cue.
   1037        // Therefore, returns null box.
   1038        if (switched) {
   1039          return null;
   1040        }
   1041        // spec 7.2.10.18 ~ 7.2.10.20
   1042        switched = true;
   1043        box = specifiedPosition.clone();
   1044        step = -1 * step;
   1045      }
   1046      // spec 7.2.10.15, moving box along the specific direction.
   1047      box.move(movingDirection, step);
   1048    }
   1049 
   1050    if (isWritingDirectionHorizontal) {
   1051      styleBox.applyStyles({
   1052        top: getPercentagePosition(box.top, fullDimension),
   1053      });
   1054    } else {
   1055      styleBox.applyStyles({
   1056        left: getPercentagePosition(box.left, fullDimension),
   1057      });
   1058    }
   1059  } else {
   1060    LOG(`Adjust position when 'snap-to-lines' is false.`);
   1061    // (snap-to-lines if false) spec 7.2.10.1 ~ 7.2.10.2
   1062    if (cue.lineAlign != "start") {
   1063      const isCenterAlign = cue.lineAlign == "center";
   1064      const movingDirection = isWritingDirectionHorizontal ? "-y" : "-x";
   1065      if (isWritingDirectionHorizontal) {
   1066        box.move(movingDirection, isCenterAlign ? box.height : box.height / 2);
   1067      } else {
   1068        box.move(movingDirection, isCenterAlign ? box.width : box.width / 2);
   1069      }
   1070    }
   1071 
   1072    // spec 7.2.10.3
   1073    let bestPosition = {},
   1074        specifiedPosition = box.clone(),
   1075        outsideAreaPercentage = 1; // Highest possible so the first thing we get is better.
   1076    let hasFoundBestPosition = false;
   1077 
   1078    // For the different writing directions, we should have different priority
   1079    // for the moving direction. For example, if the writing direction is
   1080    // horizontal, which means the cues will grow from the top to the bottom,
   1081    // then moving cues along the `y` axis should be more important than moving
   1082    // cues along the `x` axis, and vice versa for those cues growing from the
   1083    // left to right, or from the right to the left. We don't follow the exact
   1084    // way which the spec requires, see the reason in bug1575460.
   1085    function getAxis(writingDirection) {
   1086      if (writingDirection == "") {
   1087        return ["+y", "-y", "+x", "-x"];
   1088      }
   1089      // Growing from left to right.
   1090      if (writingDirection == "lr") {
   1091        return ["+x", "-x", "+y", "-y"];
   1092      }
   1093      // Growing from right to left.
   1094      return ["-x", "+x", "+y", "-y"];
   1095    }
   1096    const axis = getAxis(cue.vertical);
   1097 
   1098    // This factor effects the granularity of the moving unit, when using the
   1099    // factor=1 often moves too much and results in too many redudant spaces
   1100    // between boxes. So we can increase the factor to slightly reduce the
   1101    // move we do every time, but still can preverse the reasonable spaces
   1102    // between boxes.
   1103    const factor = 4;
   1104    const toMove = styleBox.getFirstLineBoxSize() / factor;
   1105    for (let i = 0; i < axis.length && !hasFoundBestPosition; i++) {
   1106      while (!box.isOutsideTheAxisBoundary(containerBox, axis[i]) &&
   1107              (!box.within(containerBox) || box.overlapsAny(outputBoxes))) {
   1108        box.move(axis[i], toMove);
   1109      }
   1110      // We found a spot where we aren't overlapping anything. This is our
   1111      // best position.
   1112      if (box.within(containerBox)) {
   1113        bestPosition = box.clone();
   1114        hasFoundBestPosition = true;
   1115        break;
   1116      }
   1117      let p = box.intersectPercentage(containerBox);
   1118      // If we're outside the container box less then we were on our last try
   1119      // then remember this position as the best position.
   1120      if (outsideAreaPercentage > p) {
   1121        bestPosition = box.clone();
   1122        outsideAreaPercentage = p;
   1123      }
   1124      // Reset the box position to the specified position.
   1125      box = specifiedPosition.clone();
   1126    }
   1127 
   1128    // Can not find a place to place this box inside the rendering area.
   1129    if (!box.within(containerBox)) {
   1130      return null;
   1131    }
   1132 
   1133    styleBox.applyStyles({
   1134      top: getPercentagePosition(box.top, containerBox.height),
   1135      left: getPercentagePosition(box.left, containerBox.width),
   1136    });
   1137  }
   1138 
   1139  // In order to not be affected by CSS scale, so we use '%' to make sure the
   1140  // cue can stick in the right position.
   1141  function getPercentagePosition(position, fullDimension) {
   1142    return (position / fullDimension) * 100 + "%";
   1143  }
   1144 
   1145  return box;
   1146 }
   1147 
   1148 export function WebVTT() {
   1149  this.isProcessingCues = false;
   1150  // Nothing
   1151 }
   1152 
   1153 // Helper to allow strings to be decoded instead of the default binary utf8 data.
   1154 WebVTT.StringDecoder = function() {
   1155  return {
   1156    decode: function(data) {
   1157      if (!data) {
   1158        return "";
   1159      }
   1160      if (typeof data !== "string") {
   1161        throw new Error("Error - expected string data.");
   1162      }
   1163      return decodeURIComponent(encodeURIComponent(data));
   1164    }
   1165  };
   1166 };
   1167 
   1168 WebVTT.convertCueToDOMTree = function(window, cuetext) {
   1169  if (!window) {
   1170    return null;
   1171  }
   1172  return parseContent(window, cuetext, PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT);
   1173 };
   1174 
   1175 function clearAllCuesDiv(overlay) {
   1176  while (overlay.firstChild) {
   1177    overlay.firstChild.remove();
   1178  }
   1179 }
   1180 
   1181 // It's used to record how many cues we process in the last `processCues` run.
   1182 var lastDisplayedCueNums = 0;
   1183 
   1184 const DIV_COMPUTING_STATE = {
   1185  REUSE : 0,
   1186  REUSE_AND_CLEAR : 1,
   1187  COMPUTE_AND_CLEAR : 2
   1188 };
   1189 
   1190 // Runs the processing model over the cues and regions passed to it.
   1191 // Spec https://www.w3.org/TR/webvtt1/#processing-model
   1192 // @parem window : JS window
   1193 // @param cues : the VTT cues are going to be displayed.
   1194 // @param overlay : A block level element (usually a div) that the computed cues
   1195 //                and regions will be placed into.
   1196 // @param controls : A Control bar element. Cues' position will be
   1197 //                 affected and repositioned according to it.
   1198 function processCuesInternal(window, cues, overlay, controls) {
   1199  LOG(`=== processCues ===`);
   1200  if (!cues) {
   1201    LOG(`clear display and abort processing because of no cue.`);
   1202    clearAllCuesDiv(overlay);
   1203    lastDisplayedCueNums = 0;
   1204    return;
   1205  }
   1206 
   1207  let controlBar, controlBarShown;
   1208  if (controls) {
   1209    // controls is a <div> that is the children of the UA Widget Shadow Root.
   1210    controlBar = controls.parentNode.getElementById("controlBar");
   1211    controlBarShown = controlBar ? !controlBar.hidden : false;
   1212  } else {
   1213    // There is no controls element. This only happen to UA Widget because
   1214    // it is created lazily.
   1215    controlBarShown = false;
   1216  }
   1217 
   1218  /**
   1219   * This function is used to tell us if we have to recompute or reuse current
   1220   * cue's display state. Display state is a DIV element with corresponding
   1221   * CSS style to display cue on the screen. When the cue is being displayed
   1222   * first time, we will compute its display state. After that, we could reuse
   1223   * its state until following conditions happen.
   1224   * (1) control changes : it means the rendering area changes so we should
   1225   * recompute cues' position.
   1226   * (2) cue's `hasBeenReset` flag is true : it means cues' line or position
   1227   * property has been modified, we also need to recompute cues' position.
   1228   * (3) the amount of showing cues changes : it means some cue would disappear
   1229   * but other cues should stay at the same place without recomputing, so we
   1230   * can resume their display state.
   1231   */
   1232  function getDIVComputingState(cues) {
   1233    if (overlay.lastControlBarShownStatus != controlBarShown) {
   1234      return DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR;
   1235    }
   1236 
   1237    for (let i = 0; i < cues.length; i++) {
   1238      if (cues[i].hasBeenReset || !cues[i].displayState) {
   1239        return DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR;
   1240      }
   1241    }
   1242 
   1243    if (lastDisplayedCueNums != cues.length) {
   1244      return DIV_COMPUTING_STATE.REUSE_AND_CLEAR;
   1245    }
   1246    return DIV_COMPUTING_STATE.REUSE;
   1247  }
   1248 
   1249  const divState = getDIVComputingState(cues);
   1250  overlay.lastControlBarShownStatus = controlBarShown;
   1251 
   1252  if (divState == DIV_COMPUTING_STATE.REUSE) {
   1253    LOG(`reuse current cue's display state and abort processing`);
   1254    return;
   1255  }
   1256 
   1257  clearAllCuesDiv(overlay);
   1258  let rootOfCues = window.document.createElement("div");
   1259  rootOfCues.style.position = "absolute";
   1260  rootOfCues.style.left = "0";
   1261  rootOfCues.style.right = "0";
   1262  rootOfCues.style.top = "0";
   1263  rootOfCues.style.bottom = "0";
   1264  overlay.appendChild(rootOfCues);
   1265 
   1266  if (divState == DIV_COMPUTING_STATE.REUSE_AND_CLEAR) {
   1267    LOG(`clear display but reuse cues' display state.`);
   1268    for (let cue of cues) {
   1269      rootOfCues.appendChild(cue.displayState);
   1270    }
   1271  } else if (divState == DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR) {
   1272    LOG(`clear display and recompute cues' display state.`);
   1273    let boxPositions = [],
   1274      containerBox = new BoxPosition(rootOfCues);
   1275 
   1276    let styleBox, cue, controlBarBox;
   1277    if (controlBarShown) {
   1278      controlBarBox = new BoxPosition(controlBar);
   1279      // Add an empty output box that cover the same region as video control bar.
   1280      boxPositions.push(controlBarBox);
   1281    }
   1282 
   1283    // https://w3c.github.io/webvtt/#processing-model 6.1.12.1
   1284    // Create regionNode
   1285    let regionNodeBoxes = {};
   1286    let regionNodeBox;
   1287 
   1288    LOG(`lastDisplayedCueNums=${lastDisplayedCueNums}, currentCueNums=${cues.length}`);
   1289    lastDisplayedCueNums = cues.length;
   1290    for (let i = 0; i < cues.length; i++) {
   1291      cue = cues[i];
   1292      if (cue.region != null) {
   1293        // 6.1.14.1
   1294        styleBox = new RegionCueStyleBox(window, cue);
   1295 
   1296        if (!regionNodeBoxes[cue.region.id]) {
   1297          // create regionNode
   1298          // Adjust the container hieght to exclude the controlBar
   1299          let adjustContainerBox = new BoxPosition(rootOfCues);
   1300          if (controlBarShown) {
   1301            adjustContainerBox.height -= controlBarBox.height;
   1302            adjustContainerBox.bottom += controlBarBox.height;
   1303          }
   1304          regionNodeBox = new RegionNodeBox(window, cue.region, adjustContainerBox);
   1305          regionNodeBoxes[cue.region.id] = regionNodeBox;
   1306        }
   1307        // 6.1.14.3
   1308        let currentRegionBox = regionNodeBoxes[cue.region.id];
   1309        let currentRegionNodeDiv = currentRegionBox.div;
   1310        // 6.1.14.3.2
   1311        // TODO: fix me, it looks like the we need to set/change "top" attribute at the styleBox.div
   1312        // to do the "scroll up", however, we do not implement it yet?
   1313        if (cue.region.scroll == "up" && currentRegionNodeDiv.childElementCount > 0) {
   1314          styleBox.div.style.transitionProperty = "top";
   1315          styleBox.div.style.transitionDuration = "0.433s";
   1316        }
   1317 
   1318        currentRegionNodeDiv.appendChild(styleBox.div);
   1319        rootOfCues.appendChild(currentRegionNodeDiv);
   1320        cue.displayState = styleBox.div;
   1321        boxPositions.push(new BoxPosition(currentRegionBox));
   1322      } else {
   1323        // Compute the intial position and styles of the cue div.
   1324        styleBox = new CueStyleBox(window, cue, containerBox);
   1325        rootOfCues.appendChild(styleBox.div);
   1326 
   1327        // Move the cue to correct position, we might get the null box if the
   1328        // result of algorithm doesn't want us to show the cue when we don't
   1329        // have any room for this cue.
   1330        let cueBox = adjustBoxPosition(styleBox, containerBox, controlBarBox, boxPositions);
   1331        if (cueBox) {
   1332          styleBox.setBidiRule();
   1333          // Remember the computed div so that we don't have to recompute it later
   1334          // if we don't have too.
   1335          cue.displayState = styleBox.div;
   1336          boxPositions.push(cueBox);
   1337          LOG(`cue ${i}, ` + cueBox.getBoxInfoInChars());
   1338        } else {
   1339          LOG(`can not find a proper position to place cue ${i}`);
   1340          // Clear the display state and clear the reset flag in the cue as well,
   1341          // which controls whether the task for updating the cue display is
   1342          // dispatched.
   1343          cue.displayState = null;
   1344          rootOfCues.removeChild(styleBox.div);
   1345        }
   1346      }
   1347    }
   1348  } else {
   1349    LOG(`[ERROR] unknown div computing state`);
   1350  }
   1351 };
   1352 
   1353 WebVTT.processCues = function(window, cues, overlay, controls) {
   1354  // When accessing `offsetXXX` attributes of element, it would trigger reflow
   1355  // and might result in a re-entry of this function. In order to avoid doing
   1356  // redundant computation, we would only do one processing at a time.
   1357  if (this.isProcessingCues) {
   1358    return;
   1359  }
   1360  this.isProcessingCues = true;
   1361  processCuesInternal(window, cues, overlay, controls);
   1362  this.isProcessingCues = false;
   1363 };
   1364 
   1365 WebVTT.Parser = function(window, decoder) {
   1366  this.window = window;
   1367  this.state = "INITIAL";
   1368  this.substate = "";
   1369  this.substatebuffer = "";
   1370  this.buffer = "";
   1371  this.decoder = decoder || new TextDecoder("utf8");
   1372  this.regionList = [];
   1373  this.isPrevLineBlank = false;
   1374 };
   1375 
   1376 WebVTT.Parser.prototype = {
   1377  // If the error is a ParsingError then report it to the consumer if
   1378  // possible. If it's not a ParsingError then throw it like normal.
   1379  reportOrThrowError: function(e) {
   1380    if (e instanceof ParsingError) {
   1381      this.onparsingerror && this.onparsingerror(e);
   1382    } else {
   1383      throw e;
   1384    }
   1385  },
   1386  parse: function (data) {
   1387    // If there is no data then we won't decode it, but will just try to parse
   1388    // whatever is in buffer already. This may occur in circumstances, for
   1389    // example when flush() is called.
   1390    if (data) {
   1391      // Try to decode the data that we received.
   1392      this.buffer += this.decoder.decode(data, {stream: true});
   1393    }
   1394 
   1395    // This parser is line-based. Let's see if we have a line to parse.
   1396    while (/\r\n|\n|\r/.test(this.buffer)) {
   1397      let buffer = this.buffer;
   1398      let pos = 0;
   1399      while (buffer[pos] !== '\r' && buffer[pos] !== '\n') {
   1400        ++pos;
   1401      }
   1402      let line = buffer.substr(0, pos);
   1403      // Advance the buffer early in case we fail below.
   1404      if (buffer[pos] === '\r') {
   1405        ++pos;
   1406      }
   1407      if (buffer[pos] === '\n') {
   1408        ++pos;
   1409      }
   1410      this.buffer = buffer.substr(pos);
   1411 
   1412      // Spec defined replacement.
   1413      line = line.replace(/[\u0000]/g, "\uFFFD");
   1414 
   1415      // Detect the comment. We parse line on the fly, so we only check if the
   1416      // comment block is preceded by a blank line and won't check if it's
   1417      // followed by another blank line.
   1418      // https://www.w3.org/TR/webvtt1/#introduction-comments
   1419      // TODO (1703895): according to the spec, the comment represents as a
   1420      // comment block, so we need to refactor the parser in order to better
   1421      // handle the comment block.
   1422      if (this.isPrevLineBlank && /^NOTE($|[ \t])/.test(line)) {
   1423        LOG("Ignore comment that starts with 'NOTE'");
   1424      } else {
   1425        this.parseLine(line);
   1426      }
   1427      this.isPrevLineBlank = emptyOrOnlyContainsWhiteSpaces(line);
   1428    }
   1429 
   1430    return this;
   1431  },
   1432  parseLine: function(line) {
   1433    let self = this;
   1434 
   1435    function createCueIfNeeded() {
   1436      if (!self.cue) {
   1437        self.cue = new self.window.VTTCue(0, 0, "");
   1438      }
   1439    }
   1440 
   1441    // Parsing cue identifier and the identifier should be unique.
   1442    // Return true if the input is a cue identifier.
   1443    function parseCueIdentifier(input) {
   1444      if (maybeIsTimeStampFormat(input)) {
   1445        self.state = "CUE";
   1446        return false;
   1447      }
   1448 
   1449      createCueIfNeeded();
   1450      // TODO : ensure the cue identifier is unique among all cue identifiers.
   1451      self.cue.id = containsTimeDirectionSymbol(input) ? "" : input;
   1452      self.state = "CUE";
   1453      return true;
   1454    }
   1455 
   1456    // Parsing the timestamp and cue settings.
   1457    // See spec, https://w3c.github.io/webvtt/#collect-webvtt-cue-timings-and-settings
   1458    function parseCueMayThrow(input) {
   1459      try {
   1460        createCueIfNeeded();
   1461        parseCue(input, self.cue, self.regionList);
   1462        self.state = "CUETEXT";
   1463      } catch (e) {
   1464        self.reportOrThrowError(e);
   1465        // In case of an error ignore rest of the cue.
   1466        self.cue = null;
   1467        self.state = "BADCUE";
   1468      }
   1469    }
   1470 
   1471    // 3.4 WebVTT region and WebVTT region settings syntax
   1472    function parseRegion(input) {
   1473      let settings = new Settings();
   1474      parseOptions(input, function (k, v) {
   1475        switch (k) {
   1476        case "id":
   1477          settings.set(k, v);
   1478          break;
   1479        case "width":
   1480          settings.percent(k, v);
   1481          break;
   1482        case "lines":
   1483          settings.digitsValue(k, v);
   1484          break;
   1485        case "regionanchor":
   1486        case "viewportanchor": {
   1487          let xy = v.split(',');
   1488          if (xy.length !== 2) {
   1489            break;
   1490          }
   1491          // We have to make sure both x and y parse, so use a temporary
   1492          // settings object here.
   1493          let anchor = new Settings();
   1494          anchor.percent("x", xy[0]);
   1495          anchor.percent("y", xy[1]);
   1496          if (!anchor.has("x") || !anchor.has("y")) {
   1497            break;
   1498          }
   1499          settings.set(k + "X", anchor.get("x"));
   1500          settings.set(k + "Y", anchor.get("y"));
   1501          break;
   1502        }
   1503        case "scroll":
   1504          settings.alt(k, v, ["up"]);
   1505          break;
   1506        }
   1507      }, /:/, /\t|\n|\f|\r| /); // groupDelim is ASCII whitespace
   1508      // https://infra.spec.whatwg.org/#ascii-whitespace, U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE
   1509 
   1510      // Create the region, using default values for any values that were not
   1511      // specified.
   1512      if (settings.has("id")) {
   1513        try {
   1514          let region = new self.window.VTTRegion();
   1515          region.id = settings.get("id", "");
   1516          region.width = settings.get("width", 100);
   1517          region.lines = settings.get("lines", 3);
   1518          region.regionAnchorX = settings.get("regionanchorX", 0);
   1519          region.regionAnchorY = settings.get("regionanchorY", 100);
   1520          region.viewportAnchorX = settings.get("viewportanchorX", 0);
   1521          region.viewportAnchorY = settings.get("viewportanchorY", 100);
   1522          region.scroll = settings.get("scroll", "");
   1523          // Register the region.
   1524          self.onregion && self.onregion(region);
   1525          // Remember the VTTRegion for later in case we parse any VTTCues that
   1526          // reference it.
   1527          self.regionList.push({
   1528            id: settings.get("id"),
   1529            region: region
   1530          });
   1531        } catch(e) {
   1532          dump("VTTRegion Error " + e + "\n");
   1533        }
   1534      }
   1535    }
   1536 
   1537    // Parsing the WebVTT signature, it contains parsing algo step1 to step9.
   1538    // See spec, https://w3c.github.io/webvtt/#file-parsing
   1539    function parseSignatureMayThrow(signature) {
   1540      if (!/^WEBVTT([ \t].*)?$/.test(signature)) {
   1541        throw new ParsingError(ParsingError.Errors.BadSignature);
   1542      } else {
   1543        self.state = "HEADER";
   1544      }
   1545    }
   1546 
   1547    function parseRegionOrStyle(input) {
   1548      switch (self.substate) {
   1549        case "REGION":
   1550          parseRegion(input);
   1551        break;
   1552        case "STYLE":
   1553          // TODO : not supported yet.
   1554        break;
   1555      }
   1556    }
   1557    // Parsing the region and style information.
   1558    // See spec, https://w3c.github.io/webvtt/#collect-a-webvtt-block
   1559    //
   1560    // There are sereval things would appear in header,
   1561    //   1. Region or Style setting
   1562    //   2. Garbage (meaningless string)
   1563    //   3. Empty line
   1564    //   4. Cue's timestamp
   1565    // The case 4 happens when there is no line interval between the header
   1566    // and the cue blocks. In this case, we should preserve the line for the
   1567    // next phase parsing, returning "true".
   1568    function parseHeader(line) {
   1569      if (!self.substate && /^REGION|^STYLE/.test(line)) {
   1570        self.substate = /^REGION/.test(line) ? "REGION" : "STYLE";
   1571        return false;
   1572      }
   1573 
   1574      if (self.substate === "REGION" || self.substate === "STYLE") {
   1575        if (maybeIsTimeStampFormat(line) ||
   1576            emptyOrOnlyContainsWhiteSpaces(line) ||
   1577            containsTimeDirectionSymbol(line)) {
   1578          parseRegionOrStyle(self.substatebuffer);
   1579          self.substatebuffer = "";
   1580          self.substate = null;
   1581 
   1582          // This is the end of the region or style state.
   1583          return parseHeader(line);
   1584        }
   1585 
   1586        if (/^REGION|^STYLE/.test(line)) {
   1587          // The line is another REGION/STYLE, parse and reset substatebuffer.
   1588          // Don't break the while loop to parse the next REGION/STYLE.
   1589          parseRegionOrStyle(self.substatebuffer);
   1590          self.substatebuffer = "";
   1591          self.substate = /^REGION/.test(line) ? "REGION" : "STYLE";
   1592          return false;
   1593        }
   1594 
   1595        // We weren't able to parse the line as a header. Accumulate and
   1596        // return.
   1597        self.substatebuffer += " " + line;
   1598        return false;
   1599      }
   1600 
   1601      if (emptyOrOnlyContainsWhiteSpaces(line)) {
   1602        // empty line, whitespaces, nothing to do.
   1603        return false;
   1604      }
   1605 
   1606      if (maybeIsTimeStampFormat(line)) {
   1607        self.state = "CUE";
   1608        // We want to process the same line again.
   1609        return true;
   1610      }
   1611 
   1612      // string contains "-->" or an ID
   1613      self.state = "ID";
   1614      return true;
   1615    }
   1616 
   1617    try {
   1618      LOG(`state=${self.state}, line=${line}`)
   1619      // 5.1 WebVTT file parsing.
   1620      if (self.state === "INITIAL") {
   1621        parseSignatureMayThrow(line);
   1622        return;
   1623      }
   1624 
   1625      if (self.state === "HEADER") {
   1626        // parseHeader returns false if the same line doesn't need to be
   1627        // parsed again.
   1628        if (!parseHeader(line)) {
   1629          return;
   1630        }
   1631      }
   1632 
   1633      if (self.state === "ID") {
   1634        // If there is no cue identifier, read the next line.
   1635        if (line == "") {
   1636          return;
   1637        }
   1638 
   1639        // If there is no cue identifier, parse the line again.
   1640        if (!parseCueIdentifier(line)) {
   1641          return self.parseLine(line);
   1642        }
   1643        return;
   1644      }
   1645 
   1646      if (self.state === "CUE") {
   1647        parseCueMayThrow(line);
   1648        return;
   1649      }
   1650 
   1651      if (self.state === "CUETEXT") {
   1652        // Report the cue when (1) get an empty line (2) get the "-->""
   1653        if (emptyOrOnlyContainsWhiteSpaces(line) ||
   1654            containsTimeDirectionSymbol(line)) {
   1655          // We are done parsing self cue.
   1656          self.oncue && self.oncue(self.cue);
   1657          self.cue = null;
   1658          self.state = "ID";
   1659 
   1660          if (emptyOrOnlyContainsWhiteSpaces(line)) {
   1661            return;
   1662          }
   1663 
   1664          // Reuse the same line.
   1665          return self.parseLine(line);
   1666        }
   1667        if (self.cue.text) {
   1668          self.cue.text += "\n";
   1669        }
   1670        self.cue.text += line;
   1671        return;
   1672      }
   1673 
   1674      if (self.state === "BADCUE") {
   1675        // 54-62 - Collect and discard the remaining cue.
   1676        self.state = "ID";
   1677        return self.parseLine(line);
   1678      }
   1679    } catch (e) {
   1680      self.reportOrThrowError(e);
   1681 
   1682      // If we are currently parsing a cue, report what we have.
   1683      if (self.state === "CUETEXT" && self.cue && self.oncue) {
   1684        self.oncue(self.cue);
   1685      }
   1686      self.cue = null;
   1687      // Enter BADWEBVTT state if header was not parsed correctly otherwise
   1688      // another exception occurred so enter BADCUE state.
   1689      self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
   1690    }
   1691    return this;
   1692  },
   1693  flush: function () {
   1694    let self = this;
   1695    try {
   1696      // Finish decoding the stream.
   1697      self.buffer += self.decoder.decode();
   1698      self.buffer += "\n\n";
   1699      self.parse();
   1700    } catch(e) {
   1701      self.reportOrThrowError(e);
   1702    }
   1703    self.isPrevLineBlank = false;
   1704    self.onflush && self.onflush();
   1705    return this;
   1706  }
   1707 };