Interim commit.
[yaffs-website] / node_modules / videojs-vtt.js / dist / vtt.js
1 /* videojs-vtt.js - v0.12.3 (https://github.com/gkatsev/vtt.js) built on 22-03-2017 */
2
3 (function(root) {
4   var vttjs = root.vttjs = {};
5   var cueShim = vttjs.VTTCue;
6   var regionShim = vttjs.VTTRegion;
7   var oldVTTCue = root.VTTCue;
8   var oldVTTRegion = root.VTTRegion;
9
10   vttjs.shim = function() {
11     vttjs.VTTCue = cueShim;
12     vttjs.VTTRegion = regionShim;
13   };
14
15   vttjs.restore = function() {
16     vttjs.VTTCue = oldVTTCue;
17     vttjs.VTTRegion = oldVTTRegion;
18   };
19 }(this));
20
21 /**
22  * Copyright 2013 vtt.js Contributors
23  *
24  * Licensed under the Apache License, Version 2.0 (the "License");
25  * you may not use this file except in compliance with the License.
26  * You may obtain a copy of the License at
27  *
28  *   http://www.apache.org/licenses/LICENSE-2.0
29  *
30  * Unless required by applicable law or agreed to in writing, software
31  * distributed under the License is distributed on an "AS IS" BASIS,
32  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
33  * See the License for the specific language governing permissions and
34  * limitations under the License.
35  */
36
37 (function(root, vttjs) {
38
39   var autoKeyword = "auto";
40   var directionSetting = {
41     "": true,
42     "lr": true,
43     "rl": true
44   };
45   var alignSetting = {
46     "start": true,
47     "middle": true,
48     "end": true,
49     "left": true,
50     "right": true
51   };
52
53   function findDirectionSetting(value) {
54     if (typeof value !== "string") {
55       return false;
56     }
57     var dir = directionSetting[value.toLowerCase()];
58     return dir ? value.toLowerCase() : false;
59   }
60
61   function findAlignSetting(value) {
62     if (typeof value !== "string") {
63       return false;
64     }
65     var align = alignSetting[value.toLowerCase()];
66     return align ? value.toLowerCase() : false;
67   }
68
69   function extend(obj) {
70     var i = 1;
71     for (; i < arguments.length; i++) {
72       var cobj = arguments[i];
73       for (var p in cobj) {
74         obj[p] = cobj[p];
75       }
76     }
77
78     return obj;
79   }
80
81   function VTTCue(startTime, endTime, text) {
82     var cue = this;
83     var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent);
84     var baseObj = {};
85
86     if (isIE8) {
87       cue = document.createElement('custom');
88     } else {
89       baseObj.enumerable = true;
90     }
91
92     /**
93      * Shim implementation specific properties. These properties are not in
94      * the spec.
95      */
96
97     // Lets us know when the VTTCue's data has changed in such a way that we need
98     // to recompute its display state. This lets us compute its display state
99     // lazily.
100     cue.hasBeenReset = false;
101
102     /**
103      * VTTCue and TextTrackCue properties
104      * http://dev.w3.org/html5/webvtt/#vttcue-interface
105      */
106
107     var _id = "";
108     var _pauseOnExit = false;
109     var _startTime = startTime;
110     var _endTime = endTime;
111     var _text = text;
112     var _region = null;
113     var _vertical = "";
114     var _snapToLines = true;
115     var _line = "auto";
116     var _lineAlign = "start";
117     var _position = 50;
118     var _positionAlign = "middle";
119     var _size = 50;
120     var _align = "middle";
121
122     Object.defineProperty(cue,
123       "id", extend({}, baseObj, {
124         get: function() {
125           return _id;
126         },
127         set: function(value) {
128           _id = "" + value;
129         }
130       }));
131
132     Object.defineProperty(cue,
133       "pauseOnExit", extend({}, baseObj, {
134         get: function() {
135           return _pauseOnExit;
136         },
137         set: function(value) {
138           _pauseOnExit = !!value;
139         }
140       }));
141
142     Object.defineProperty(cue,
143       "startTime", extend({}, baseObj, {
144         get: function() {
145           return _startTime;
146         },
147         set: function(value) {
148           if (typeof value !== "number") {
149             throw new TypeError("Start time must be set to a number.");
150           }
151           _startTime = value;
152           this.hasBeenReset = true;
153         }
154       }));
155
156     Object.defineProperty(cue,
157       "endTime", extend({}, baseObj, {
158         get: function() {
159           return _endTime;
160         },
161         set: function(value) {
162           if (typeof value !== "number") {
163             throw new TypeError("End time must be set to a number.");
164           }
165           _endTime = value;
166           this.hasBeenReset = true;
167         }
168       }));
169
170     Object.defineProperty(cue,
171       "text", extend({}, baseObj, {
172         get: function() {
173           return _text;
174         },
175         set: function(value) {
176           _text = "" + value;
177           this.hasBeenReset = true;
178         }
179       }));
180
181     Object.defineProperty(cue,
182       "region", extend({}, baseObj, {
183         get: function() {
184           return _region;
185         },
186         set: function(value) {
187           _region = value;
188           this.hasBeenReset = true;
189         }
190       }));
191
192     Object.defineProperty(cue,
193       "vertical", extend({}, baseObj, {
194         get: function() {
195           return _vertical;
196         },
197         set: function(value) {
198           var setting = findDirectionSetting(value);
199           // Have to check for false because the setting an be an empty string.
200           if (setting === false) {
201             throw new SyntaxError("An invalid or illegal string was specified.");
202           }
203           _vertical = setting;
204           this.hasBeenReset = true;
205         }
206       }));
207
208     Object.defineProperty(cue,
209       "snapToLines", extend({}, baseObj, {
210         get: function() {
211           return _snapToLines;
212         },
213         set: function(value) {
214           _snapToLines = !!value;
215           this.hasBeenReset = true;
216         }
217       }));
218
219     Object.defineProperty(cue,
220       "line", extend({}, baseObj, {
221         get: function() {
222           return _line;
223         },
224         set: function(value) {
225           if (typeof value !== "number" && value !== autoKeyword) {
226             throw new SyntaxError("An invalid number or illegal string was specified.");
227           }
228           _line = value;
229           this.hasBeenReset = true;
230         }
231       }));
232
233     Object.defineProperty(cue,
234       "lineAlign", extend({}, baseObj, {
235         get: function() {
236           return _lineAlign;
237         },
238         set: function(value) {
239           var setting = findAlignSetting(value);
240           if (!setting) {
241             throw new SyntaxError("An invalid or illegal string was specified.");
242           }
243           _lineAlign = setting;
244           this.hasBeenReset = true;
245         }
246       }));
247
248     Object.defineProperty(cue,
249       "position", extend({}, baseObj, {
250         get: function() {
251           return _position;
252         },
253         set: function(value) {
254           if (value < 0 || value > 100) {
255             throw new Error("Position must be between 0 and 100.");
256           }
257           _position = value;
258           this.hasBeenReset = true;
259         }
260       }));
261
262     Object.defineProperty(cue,
263       "positionAlign", extend({}, baseObj, {
264         get: function() {
265           return _positionAlign;
266         },
267         set: function(value) {
268           var setting = findAlignSetting(value);
269           if (!setting) {
270             throw new SyntaxError("An invalid or illegal string was specified.");
271           }
272           _positionAlign = setting;
273           this.hasBeenReset = true;
274         }
275       }));
276
277     Object.defineProperty(cue,
278       "size", extend({}, baseObj, {
279         get: function() {
280           return _size;
281         },
282         set: function(value) {
283           if (value < 0 || value > 100) {
284             throw new Error("Size must be between 0 and 100.");
285           }
286           _size = value;
287           this.hasBeenReset = true;
288         }
289       }));
290
291     Object.defineProperty(cue,
292       "align", extend({}, baseObj, {
293         get: function() {
294           return _align;
295         },
296         set: function(value) {
297           var setting = findAlignSetting(value);
298           if (!setting) {
299             throw new SyntaxError("An invalid or illegal string was specified.");
300           }
301           _align = setting;
302           this.hasBeenReset = true;
303         }
304       }));
305
306     /**
307      * Other <track> spec defined properties
308      */
309
310     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-display-state
311     cue.displayState = undefined;
312
313     if (isIE8) {
314       return cue;
315     }
316   }
317
318   /**
319    * VTTCue methods
320    */
321
322   VTTCue.prototype.getCueAsHTML = function() {
323     // Assume WebVTT.convertCueToDOMTree is on the global.
324     return WebVTT.convertCueToDOMTree(window, this.text);
325   };
326
327   root.VTTCue = root.VTTCue || VTTCue;
328   vttjs.VTTCue = VTTCue;
329 }(this, (this.vttjs || {})));
330
331 /**
332  * Copyright 2013 vtt.js Contributors
333  *
334  * Licensed under the Apache License, Version 2.0 (the "License");
335  * you may not use this file except in compliance with the License.
336  * You may obtain a copy of the License at
337  *
338  *   http://www.apache.org/licenses/LICENSE-2.0
339  *
340  * Unless required by applicable law or agreed to in writing, software
341  * distributed under the License is distributed on an "AS IS" BASIS,
342  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
343  * See the License for the specific language governing permissions and
344  * limitations under the License.
345  */
346
347 (function(root, vttjs) {
348
349   var scrollSetting = {
350     "": true,
351     "up": true
352   };
353
354   function findScrollSetting(value) {
355     if (typeof value !== "string") {
356       return false;
357     }
358     var scroll = scrollSetting[value.toLowerCase()];
359     return scroll ? value.toLowerCase() : false;
360   }
361
362   function isValidPercentValue(value) {
363     return typeof value === "number" && (value >= 0 && value <= 100);
364   }
365
366   // VTTRegion shim http://dev.w3.org/html5/webvtt/#vttregion-interface
367   function VTTRegion() {
368     var _width = 100;
369     var _lines = 3;
370     var _regionAnchorX = 0;
371     var _regionAnchorY = 100;
372     var _viewportAnchorX = 0;
373     var _viewportAnchorY = 100;
374     var _scroll = "";
375
376     Object.defineProperties(this, {
377       "width": {
378         enumerable: true,
379         get: function() {
380           return _width;
381         },
382         set: function(value) {
383           if (!isValidPercentValue(value)) {
384             throw new Error("Width must be between 0 and 100.");
385           }
386           _width = value;
387         }
388       },
389       "lines": {
390         enumerable: true,
391         get: function() {
392           return _lines;
393         },
394         set: function(value) {
395           if (typeof value !== "number") {
396             throw new TypeError("Lines must be set to a number.");
397           }
398           _lines = value;
399         }
400       },
401       "regionAnchorY": {
402         enumerable: true,
403         get: function() {
404           return _regionAnchorY;
405         },
406         set: function(value) {
407           if (!isValidPercentValue(value)) {
408             throw new Error("RegionAnchorX must be between 0 and 100.");
409           }
410           _regionAnchorY = value;
411         }
412       },
413       "regionAnchorX": {
414         enumerable: true,
415         get: function() {
416           return _regionAnchorX;
417         },
418         set: function(value) {
419           if(!isValidPercentValue(value)) {
420             throw new Error("RegionAnchorY must be between 0 and 100.");
421           }
422           _regionAnchorX = value;
423         }
424       },
425       "viewportAnchorY": {
426         enumerable: true,
427         get: function() {
428           return _viewportAnchorY;
429         },
430         set: function(value) {
431           if (!isValidPercentValue(value)) {
432             throw new Error("ViewportAnchorY must be between 0 and 100.");
433           }
434           _viewportAnchorY = value;
435         }
436       },
437       "viewportAnchorX": {
438         enumerable: true,
439         get: function() {
440           return _viewportAnchorX;
441         },
442         set: function(value) {
443           if (!isValidPercentValue(value)) {
444             throw new Error("ViewportAnchorX must be between 0 and 100.");
445           }
446           _viewportAnchorX = value;
447         }
448       },
449       "scroll": {
450         enumerable: true,
451         get: function() {
452           return _scroll;
453         },
454         set: function(value) {
455           var setting = findScrollSetting(value);
456           // Have to check for false as an empty string is a legal value.
457           if (setting === false) {
458             throw new SyntaxError("An invalid or illegal string was specified.");
459           }
460           _scroll = setting;
461         }
462       }
463     });
464   }
465
466   root.VTTRegion = root.VTTRegion || VTTRegion;
467   vttjs.VTTRegion = VTTRegion;
468 }(this, (this.vttjs || {})));
469
470 /**
471  * Copyright 2013 vtt.js Contributors
472  *
473  * Licensed under the Apache License, Version 2.0 (the "License");
474  * you may not use this file except in compliance with the License.
475  * You may obtain a copy of the License at
476  *
477  *   http://www.apache.org/licenses/LICENSE-2.0
478  *
479  * Unless required by applicable law or agreed to in writing, software
480  * distributed under the License is distributed on an "AS IS" BASIS,
481  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
482  * See the License for the specific language governing permissions and
483  * limitations under the License.
484  */
485
486 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
487 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
488
489 (function(global) {
490
491   var _objCreate = Object.create || (function() {
492     function F() {}
493     return function(o) {
494       if (arguments.length !== 1) {
495         throw new Error('Object.create shim only accepts one parameter.');
496       }
497       F.prototype = o;
498       return new F();
499     };
500   })();
501
502   // Creates a new ParserError object from an errorData object. The errorData
503   // object should have default code and message properties. The default message
504   // property can be overriden by passing in a message parameter.
505   // See ParsingError.Errors below for acceptable errors.
506   function ParsingError(errorData, message) {
507     this.name = "ParsingError";
508     this.code = errorData.code;
509     this.message = message || errorData.message;
510   }
511   ParsingError.prototype = _objCreate(Error.prototype);
512   ParsingError.prototype.constructor = ParsingError;
513
514   // ParsingError metadata for acceptable ParsingErrors.
515   ParsingError.Errors = {
516     BadSignature: {
517       code: 0,
518       message: "Malformed WebVTT signature."
519     },
520     BadTimeStamp: {
521       code: 1,
522       message: "Malformed time stamp."
523     }
524   };
525
526   // Try to parse input as a time stamp.
527   function parseTimeStamp(input) {
528
529     function computeSeconds(h, m, s, f) {
530       return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
531     }
532
533     var m = input.match(/^(\d+):(\d{2})(:\d{2})?\.(\d{3})/);
534     if (!m) {
535       return null;
536     }
537
538     if (m[3]) {
539       // Timestamp takes the form of [hours]:[minutes]:[seconds].[milliseconds]
540       return computeSeconds(m[1], m[2], m[3].replace(":", ""), m[4]);
541     } else if (m[1] > 59) {
542       // Timestamp takes the form of [hours]:[minutes].[milliseconds]
543       // First position is hours as it's over 59.
544       return computeSeconds(m[1], m[2], 0,  m[4]);
545     } else {
546       // Timestamp takes the form of [minutes]:[seconds].[milliseconds]
547       return computeSeconds(0, m[1], m[2], m[4]);
548     }
549   }
550
551   // A settings object holds key/value pairs and will ignore anything but the first
552   // assignment to a specific key.
553   function Settings() {
554     this.values = _objCreate(null);
555   }
556
557   Settings.prototype = {
558     // Only accept the first assignment to any key.
559     set: function(k, v) {
560       if (!this.get(k) && v !== "") {
561         this.values[k] = v;
562       }
563     },
564     // Return the value for a key, or a default value.
565     // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
566     // a number of possible default values as properties where 'defaultKey' is
567     // the key of the property that will be chosen; otherwise it's assumed to be
568     // a single value.
569     get: function(k, dflt, defaultKey) {
570       if (defaultKey) {
571         return this.has(k) ? this.values[k] : dflt[defaultKey];
572       }
573       return this.has(k) ? this.values[k] : dflt;
574     },
575     // Check whether we have a value for a key.
576     has: function(k) {
577       return k in this.values;
578     },
579     // Accept a setting if its one of the given alternatives.
580     alt: function(k, v, a) {
581       for (var n = 0; n < a.length; ++n) {
582         if (v === a[n]) {
583           this.set(k, v);
584           break;
585         }
586       }
587     },
588     // Accept a setting if its a valid (signed) integer.
589     integer: function(k, v) {
590       if (/^-?\d+$/.test(v)) { // integer
591         this.set(k, parseInt(v, 10));
592       }
593     },
594     // Accept a setting if its a valid percentage.
595     percent: function(k, v) {
596       var m;
597       if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) {
598         v = parseFloat(v);
599         if (v >= 0 && v <= 100) {
600           this.set(k, v);
601           return true;
602         }
603       }
604       return false;
605     }
606   };
607
608   // Helper function to parse input into groups separated by 'groupDelim', and
609   // interprete each group as a key/value pair separated by 'keyValueDelim'.
610   function parseOptions(input, callback, keyValueDelim, groupDelim) {
611     var groups = groupDelim ? input.split(groupDelim) : [input];
612     for (var i in groups) {
613       if (typeof groups[i] !== "string") {
614         continue;
615       }
616       var kv = groups[i].split(keyValueDelim);
617       if (kv.length !== 2) {
618         continue;
619       }
620       var k = kv[0];
621       var v = kv[1];
622       callback(k, v);
623     }
624   }
625
626   function parseCue(input, cue, regionList) {
627     // Remember the original input if we need to throw an error.
628     var oInput = input;
629     // 4.1 WebVTT timestamp
630     function consumeTimeStamp() {
631       var ts = parseTimeStamp(input);
632       if (ts === null) {
633         throw new ParsingError(ParsingError.Errors.BadTimeStamp,
634                               "Malformed timestamp: " + oInput);
635       }
636       // Remove time stamp from input.
637       input = input.replace(/^[^\sa-zA-Z-]+/, "");
638       return ts;
639     }
640
641     // 4.4.2 WebVTT cue settings
642     function consumeCueSettings(input, cue) {
643       var settings = new Settings();
644
645       parseOptions(input, function (k, v) {
646         switch (k) {
647         case "region":
648           // Find the last region we parsed with the same region id.
649           for (var i = regionList.length - 1; i >= 0; i--) {
650             if (regionList[i].id === v) {
651               settings.set(k, regionList[i].region);
652               break;
653             }
654           }
655           break;
656         case "vertical":
657           settings.alt(k, v, ["rl", "lr"]);
658           break;
659         case "line":
660           var vals = v.split(","),
661               vals0 = vals[0];
662           settings.integer(k, vals0);
663           settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
664           settings.alt(k, vals0, ["auto"]);
665           if (vals.length === 2) {
666             settings.alt("lineAlign", vals[1], ["start", "middle", "end"]);
667           }
668           break;
669         case "position":
670           vals = v.split(",");
671           settings.percent(k, vals[0]);
672           if (vals.length === 2) {
673             settings.alt("positionAlign", vals[1], ["start", "middle", "end"]);
674           }
675           break;
676         case "size":
677           settings.percent(k, v);
678           break;
679         case "align":
680           settings.alt(k, v, ["start", "middle", "end", "left", "right"]);
681           break;
682         }
683       }, /:/, /\s/);
684
685       // Apply default values for any missing fields.
686       cue.region = settings.get("region", null);
687       cue.vertical = settings.get("vertical", "");
688       cue.line = settings.get("line", "auto");
689       cue.lineAlign = settings.get("lineAlign", "start");
690       cue.snapToLines = settings.get("snapToLines", true);
691       cue.size = settings.get("size", 100);
692       cue.align = settings.get("align", "middle");
693       cue.position = settings.get("position", {
694         start: 0,
695         left: 0,
696         middle: 50,
697         end: 100,
698         right: 100
699       }, cue.align);
700       cue.positionAlign = settings.get("positionAlign", {
701         start: "start",
702         left: "start",
703         middle: "middle",
704         end: "end",
705         right: "end"
706       }, cue.align);
707     }
708
709     function skipWhitespace() {
710       input = input.replace(/^\s+/, "");
711     }
712
713     // 4.1 WebVTT cue timings.
714     skipWhitespace();
715     cue.startTime = consumeTimeStamp();   // (1) collect cue start time
716     skipWhitespace();
717     if (input.substr(0, 3) !== "-->") {     // (3) next characters must match "-->"
718       throw new ParsingError(ParsingError.Errors.BadTimeStamp,
719                              "Malformed time stamp (time stamps must be separated by '-->'): " +
720                              oInput);
721     }
722     input = input.substr(3);
723     skipWhitespace();
724     cue.endTime = consumeTimeStamp();     // (5) collect cue end time
725
726     // 4.1 WebVTT cue settings list.
727     skipWhitespace();
728     consumeCueSettings(input, cue);
729   }
730
731   var ESCAPE = {
732     "&amp;": "&",
733     "&lt;": "<",
734     "&gt;": ">",
735     "&lrm;": "\u200e",
736     "&rlm;": "\u200f",
737     "&nbsp;": "\u00a0"
738   };
739
740   var TAG_NAME = {
741     c: "span",
742     i: "i",
743     b: "b",
744     u: "u",
745     ruby: "ruby",
746     rt: "rt",
747     v: "span",
748     lang: "span"
749   };
750
751   var TAG_ANNOTATION = {
752     v: "title",
753     lang: "lang"
754   };
755
756   var NEEDS_PARENT = {
757     rt: "ruby"
758   };
759
760   // Parse content into a document fragment.
761   function parseContent(window, input) {
762     function nextToken() {
763       // Check for end-of-string.
764       if (!input) {
765         return null;
766       }
767
768       // Consume 'n' characters from the input.
769       function consume(result) {
770         input = input.substr(result.length);
771         return result;
772       }
773
774       var m = input.match(/^([^<]*)(<[^>]+>?)?/);
775       // If there is some text before the next tag, return it, otherwise return
776       // the tag.
777       return consume(m[1] ? m[1] : m[2]);
778     }
779
780     // Unescape a string 's'.
781     function unescape1(e) {
782       return ESCAPE[e];
783     }
784     function unescape(s) {
785       while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) {
786         s = s.replace(m[0], unescape1);
787       }
788       return s;
789     }
790
791     function shouldAdd(current, element) {
792       return !NEEDS_PARENT[element.localName] ||
793              NEEDS_PARENT[element.localName] === current.localName;
794     }
795
796     // Create an element for this tag.
797     function createElement(type, annotation) {
798       var tagName = TAG_NAME[type];
799       if (!tagName) {
800         return null;
801       }
802       var element = window.document.createElement(tagName);
803       element.localName = tagName;
804       var name = TAG_ANNOTATION[type];
805       if (name && annotation) {
806         element[name] = annotation.trim();
807       }
808       return element;
809     }
810
811     var rootDiv = window.document.createElement("div"),
812         current = rootDiv,
813         t,
814         tagStack = [];
815
816     while ((t = nextToken()) !== null) {
817       if (t[0] === '<') {
818         if (t[1] === "/") {
819           // If the closing tag matches, move back up to the parent node.
820           if (tagStack.length &&
821               tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
822             tagStack.pop();
823             current = current.parentNode;
824           }
825           // Otherwise just ignore the end tag.
826           continue;
827         }
828         var ts = parseTimeStamp(t.substr(1, t.length - 2));
829         var node;
830         if (ts) {
831           // Timestamps are lead nodes as well.
832           node = window.document.createProcessingInstruction("timestamp", ts);
833           current.appendChild(node);
834           continue;
835         }
836         var m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
837         // If we can't parse the tag, skip to the next tag.
838         if (!m) {
839           continue;
840         }
841         // Try to construct an element, and ignore the tag if we couldn't.
842         node = createElement(m[1], m[3]);
843         if (!node) {
844           continue;
845         }
846         // Determine if the tag should be added based on the context of where it
847         // is placed in the cuetext.
848         if (!shouldAdd(current, node)) {
849           continue;
850         }
851         // Set the class list (as a list of classes, separated by space).
852         if (m[2]) {
853           node.className = m[2].substr(1).replace('.', ' ');
854         }
855         // Append the node to the current node, and enter the scope of the new
856         // node.
857         tagStack.push(m[1]);
858         current.appendChild(node);
859         current = node;
860         continue;
861       }
862
863       // Text nodes are leaf nodes.
864       current.appendChild(window.document.createTextNode(unescape(t)));
865     }
866
867     return rootDiv;
868   }
869
870   // This is a list of all the Unicode characters that have a strong
871   // right-to-left category. What this means is that these characters are
872   // written right-to-left for sure. It was generated by pulling all the strong
873   // right-to-left characters out of the Unicode data table. That table can
874   // found at: http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
875   var strongRTLRanges = [[0x5be, 0x5be], [0x5c0, 0x5c0], [0x5c3, 0x5c3], [0x5c6, 0x5c6],
876    [0x5d0, 0x5ea], [0x5f0, 0x5f4], [0x608, 0x608], [0x60b, 0x60b], [0x60d, 0x60d],
877    [0x61b, 0x61b], [0x61e, 0x64a], [0x66d, 0x66f], [0x671, 0x6d5], [0x6e5, 0x6e6],
878    [0x6ee, 0x6ef], [0x6fa, 0x70d], [0x70f, 0x710], [0x712, 0x72f], [0x74d, 0x7a5],
879    [0x7b1, 0x7b1], [0x7c0, 0x7ea], [0x7f4, 0x7f5], [0x7fa, 0x7fa], [0x800, 0x815],
880    [0x81a, 0x81a], [0x824, 0x824], [0x828, 0x828], [0x830, 0x83e], [0x840, 0x858],
881    [0x85e, 0x85e], [0x8a0, 0x8a0], [0x8a2, 0x8ac], [0x200f, 0x200f],
882    [0xfb1d, 0xfb1d], [0xfb1f, 0xfb28], [0xfb2a, 0xfb36], [0xfb38, 0xfb3c],
883    [0xfb3e, 0xfb3e], [0xfb40, 0xfb41], [0xfb43, 0xfb44], [0xfb46, 0xfbc1],
884    [0xfbd3, 0xfd3d], [0xfd50, 0xfd8f], [0xfd92, 0xfdc7], [0xfdf0, 0xfdfc],
885    [0xfe70, 0xfe74], [0xfe76, 0xfefc], [0x10800, 0x10805], [0x10808, 0x10808],
886    [0x1080a, 0x10835], [0x10837, 0x10838], [0x1083c, 0x1083c], [0x1083f, 0x10855],
887    [0x10857, 0x1085f], [0x10900, 0x1091b], [0x10920, 0x10939], [0x1093f, 0x1093f],
888    [0x10980, 0x109b7], [0x109be, 0x109bf], [0x10a00, 0x10a00], [0x10a10, 0x10a13],
889    [0x10a15, 0x10a17], [0x10a19, 0x10a33], [0x10a40, 0x10a47], [0x10a50, 0x10a58],
890    [0x10a60, 0x10a7f], [0x10b00, 0x10b35], [0x10b40, 0x10b55], [0x10b58, 0x10b72],
891    [0x10b78, 0x10b7f], [0x10c00, 0x10c48], [0x1ee00, 0x1ee03], [0x1ee05, 0x1ee1f],
892    [0x1ee21, 0x1ee22], [0x1ee24, 0x1ee24], [0x1ee27, 0x1ee27], [0x1ee29, 0x1ee32],
893    [0x1ee34, 0x1ee37], [0x1ee39, 0x1ee39], [0x1ee3b, 0x1ee3b], [0x1ee42, 0x1ee42],
894    [0x1ee47, 0x1ee47], [0x1ee49, 0x1ee49], [0x1ee4b, 0x1ee4b], [0x1ee4d, 0x1ee4f],
895    [0x1ee51, 0x1ee52], [0x1ee54, 0x1ee54], [0x1ee57, 0x1ee57], [0x1ee59, 0x1ee59],
896    [0x1ee5b, 0x1ee5b], [0x1ee5d, 0x1ee5d], [0x1ee5f, 0x1ee5f], [0x1ee61, 0x1ee62],
897    [0x1ee64, 0x1ee64], [0x1ee67, 0x1ee6a], [0x1ee6c, 0x1ee72], [0x1ee74, 0x1ee77],
898    [0x1ee79, 0x1ee7c], [0x1ee7e, 0x1ee7e], [0x1ee80, 0x1ee89], [0x1ee8b, 0x1ee9b],
899    [0x1eea1, 0x1eea3], [0x1eea5, 0x1eea9], [0x1eeab, 0x1eebb], [0x10fffd, 0x10fffd]];
900
901   function isStrongRTLChar(charCode) {
902     for (var i = 0; i < strongRTLRanges.length; i++) {
903       var currentRange = strongRTLRanges[i];
904       if (charCode >= currentRange[0] && charCode <= currentRange[1]) {
905         return true;
906       }
907     }
908
909     return false;
910   }
911
912   function determineBidi(cueDiv) {
913     var nodeStack = [],
914         text = "",
915         charCode;
916
917     if (!cueDiv || !cueDiv.childNodes) {
918       return "ltr";
919     }
920
921     function pushNodes(nodeStack, node) {
922       for (var i = node.childNodes.length - 1; i >= 0; i--) {
923         nodeStack.push(node.childNodes[i]);
924       }
925     }
926
927     function nextTextNode(nodeStack) {
928       if (!nodeStack || !nodeStack.length) {
929         return null;
930       }
931
932       var node = nodeStack.pop(),
933           text = node.textContent || node.innerText;
934       if (text) {
935         // TODO: This should match all unicode type B characters (paragraph
936         // separator characters). See issue #115.
937         var m = text.match(/^.*(\n|\r)/);
938         if (m) {
939           nodeStack.length = 0;
940           return m[0];
941         }
942         return text;
943       }
944       if (node.tagName === "ruby") {
945         return nextTextNode(nodeStack);
946       }
947       if (node.childNodes) {
948         pushNodes(nodeStack, node);
949         return nextTextNode(nodeStack);
950       }
951     }
952
953     pushNodes(nodeStack, cueDiv);
954     while ((text = nextTextNode(nodeStack))) {
955       for (var i = 0; i < text.length; i++) {
956         charCode = text.charCodeAt(i);
957         if (isStrongRTLChar(charCode)) {
958           return "rtl";
959         }
960       }
961     }
962     return "ltr";
963   }
964
965   function computeLinePos(cue) {
966     if (typeof cue.line === "number" &&
967         (cue.snapToLines || (cue.line >= 0 && cue.line <= 100))) {
968       return cue.line;
969     }
970     if (!cue.track || !cue.track.textTrackList ||
971         !cue.track.textTrackList.mediaElement) {
972       return -1;
973     }
974     var track = cue.track,
975         trackList = track.textTrackList,
976         count = 0;
977     for (var i = 0; i < trackList.length && trackList[i] !== track; i++) {
978       if (trackList[i].mode === "showing") {
979         count++;
980       }
981     }
982     return ++count * -1;
983   }
984
985   function StyleBox() {
986   }
987
988   // Apply styles to a div. If there is no div passed then it defaults to the
989   // div on 'this'.
990   StyleBox.prototype.applyStyles = function(styles, div) {
991     div = div || this.div;
992     for (var prop in styles) {
993       if (styles.hasOwnProperty(prop)) {
994         div.style[prop] = styles[prop];
995       }
996     }
997   };
998
999   StyleBox.prototype.formatStyle = function(val, unit) {
1000     return val === 0 ? 0 : val + unit;
1001   };
1002
1003   // Constructs the computed display state of the cue (a div). Places the div
1004   // into the overlay which should be a block level element (usually a div).
1005   function CueStyleBox(window, cue, styleOptions) {
1006     var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent);
1007     var color = "rgba(255, 255, 255, 1)";
1008     var backgroundColor = "rgba(0, 0, 0, 0.8)";
1009
1010     if (isIE8) {
1011       color = "rgb(255, 255, 255)";
1012       backgroundColor = "rgb(0, 0, 0)";
1013     }
1014
1015     StyleBox.call(this);
1016     this.cue = cue;
1017
1018     // Parse our cue's text into a DOM tree rooted at 'cueDiv'. This div will
1019     // have inline positioning and will function as the cue background box.
1020     this.cueDiv = parseContent(window, cue.text);
1021     var styles = {
1022       color: color,
1023       backgroundColor: backgroundColor,
1024       position: "relative",
1025       left: 0,
1026       right: 0,
1027       top: 0,
1028       bottom: 0,
1029       display: "inline"
1030     };
1031
1032     if (!isIE8) {
1033       styles.writingMode = cue.vertical === "" ? "horizontal-tb"
1034                                                : cue.vertical === "lr" ? "vertical-lr"
1035                                                                        : "vertical-rl";
1036       styles.unicodeBidi = "plaintext";
1037     }
1038     this.applyStyles(styles, this.cueDiv);
1039
1040     // Create an absolutely positioned div that will be used to position the cue
1041     // div. Note, all WebVTT cue-setting alignments are equivalent to the CSS
1042     // mirrors of them except "middle" which is "center" in CSS.
1043     this.div = window.document.createElement("div");
1044     styles = {
1045       textAlign: cue.align === "middle" ? "center" : cue.align,
1046       font: styleOptions.font,
1047       whiteSpace: "pre-line",
1048       position: "absolute"
1049     };
1050
1051     if (!isIE8) {
1052       styles.direction = determineBidi(this.cueDiv);
1053       styles.writingMode = cue.vertical === "" ? "horizontal-tb"
1054                                                : cue.vertical === "lr" ? "vertical-lr"
1055                                                                        : "vertical-rl".
1056       stylesunicodeBidi =  "plaintext";
1057     }
1058
1059     this.applyStyles(styles);
1060
1061     this.div.appendChild(this.cueDiv);
1062
1063     // Calculate the distance from the reference edge of the viewport to the text
1064     // position of the cue box. The reference edge will be resolved later when
1065     // the box orientation styles are applied.
1066     var textPos = 0;
1067     switch (cue.positionAlign) {
1068     case "start":
1069       textPos = cue.position;
1070       break;
1071     case "middle":
1072       textPos = cue.position - (cue.size / 2);
1073       break;
1074     case "end":
1075       textPos = cue.position - cue.size;
1076       break;
1077     }
1078
1079     // Horizontal box orientation; textPos is the distance from the left edge of the
1080     // area to the left edge of the box and cue.size is the distance extending to
1081     // the right from there.
1082     if (cue.vertical === "") {
1083       this.applyStyles({
1084         left:  this.formatStyle(textPos, "%"),
1085         width: this.formatStyle(cue.size, "%")
1086       });
1087     // Vertical box orientation; textPos is the distance from the top edge of the
1088     // area to the top edge of the box and cue.size is the height extending
1089     // downwards from there.
1090     } else {
1091       this.applyStyles({
1092         top: this.formatStyle(textPos, "%"),
1093         height: this.formatStyle(cue.size, "%")
1094       });
1095     }
1096
1097     this.move = function(box) {
1098       this.applyStyles({
1099         top: this.formatStyle(box.top, "px"),
1100         bottom: this.formatStyle(box.bottom, "px"),
1101         left: this.formatStyle(box.left, "px"),
1102         right: this.formatStyle(box.right, "px"),
1103         height: this.formatStyle(box.height, "px"),
1104         width: this.formatStyle(box.width, "px")
1105       });
1106     };
1107   }
1108   CueStyleBox.prototype = _objCreate(StyleBox.prototype);
1109   CueStyleBox.prototype.constructor = CueStyleBox;
1110
1111   // Represents the co-ordinates of an Element in a way that we can easily
1112   // compute things with such as if it overlaps or intersects with another Element.
1113   // Can initialize it with either a StyleBox or another BoxPosition.
1114   function BoxPosition(obj) {
1115     var isIE8 = (/MSIE\s8\.0/).test(navigator.userAgent);
1116
1117     // Either a BoxPosition was passed in and we need to copy it, or a StyleBox
1118     // was passed in and we need to copy the results of 'getBoundingClientRect'
1119     // as the object returned is readonly. All co-ordinate values are in reference
1120     // to the viewport origin (top left).
1121     var lh, height, width, top;
1122     if (obj.div) {
1123       height = obj.div.offsetHeight;
1124       width = obj.div.offsetWidth;
1125       top = obj.div.offsetTop;
1126
1127       var rects = (rects = obj.div.childNodes) && (rects = rects[0]) &&
1128                   rects.getClientRects && rects.getClientRects();
1129       obj = obj.div.getBoundingClientRect();
1130       // In certain cases the outter div will be slightly larger then the sum of
1131       // the inner div's lines. This could be due to bold text, etc, on some platforms.
1132       // In this case we should get the average line height and use that. This will
1133       // result in the desired behaviour.
1134       lh = rects ? Math.max((rects[0] && rects[0].height) || 0, obj.height / rects.length)
1135                  : 0;
1136
1137     }
1138     this.left = obj.left;
1139     this.right = obj.right;
1140     this.top = obj.top || top;
1141     this.height = obj.height || height;
1142     this.bottom = obj.bottom || (top + (obj.height || height));
1143     this.width = obj.width || width;
1144     this.lineHeight = lh !== undefined ? lh : obj.lineHeight;
1145
1146     if (isIE8 && !this.lineHeight) {
1147       this.lineHeight = 13;
1148     }
1149   }
1150
1151   // Move the box along a particular axis. Optionally pass in an amount to move
1152   // the box. If no amount is passed then the default is the line height of the
1153   // box.
1154   BoxPosition.prototype.move = function(axis, toMove) {
1155     toMove = toMove !== undefined ? toMove : this.lineHeight;
1156     switch (axis) {
1157     case "+x":
1158       this.left += toMove;
1159       this.right += toMove;
1160       break;
1161     case "-x":
1162       this.left -= toMove;
1163       this.right -= toMove;
1164       break;
1165     case "+y":
1166       this.top += toMove;
1167       this.bottom += toMove;
1168       break;
1169     case "-y":
1170       this.top -= toMove;
1171       this.bottom -= toMove;
1172       break;
1173     }
1174   };
1175
1176   // Check if this box overlaps another box, b2.
1177   BoxPosition.prototype.overlaps = function(b2) {
1178     return this.left < b2.right &&
1179            this.right > b2.left &&
1180            this.top < b2.bottom &&
1181            this.bottom > b2.top;
1182   };
1183
1184   // Check if this box overlaps any other boxes in boxes.
1185   BoxPosition.prototype.overlapsAny = function(boxes) {
1186     for (var i = 0; i < boxes.length; i++) {
1187       if (this.overlaps(boxes[i])) {
1188         return true;
1189       }
1190     }
1191     return false;
1192   };
1193
1194   // Check if this box is within another box.
1195   BoxPosition.prototype.within = function(container) {
1196     return this.top >= container.top &&
1197            this.bottom <= container.bottom &&
1198            this.left >= container.left &&
1199            this.right <= container.right;
1200   };
1201
1202   // Check if this box is entirely within the container or it is overlapping
1203   // on the edge opposite of the axis direction passed. For example, if "+x" is
1204   // passed and the box is overlapping on the left edge of the container, then
1205   // return true.
1206   BoxPosition.prototype.overlapsOppositeAxis = function(container, axis) {
1207     switch (axis) {
1208     case "+x":
1209       return this.left < container.left;
1210     case "-x":
1211       return this.right > container.right;
1212     case "+y":
1213       return this.top < container.top;
1214     case "-y":
1215       return this.bottom > container.bottom;
1216     }
1217   };
1218
1219   // Find the percentage of the area that this box is overlapping with another
1220   // box.
1221   BoxPosition.prototype.intersectPercentage = function(b2) {
1222     var x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
1223         y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
1224         intersectArea = x * y;
1225     return intersectArea / (this.height * this.width);
1226   };
1227
1228   // Convert the positions from this box to CSS compatible positions using
1229   // the reference container's positions. This has to be done because this
1230   // box's positions are in reference to the viewport origin, whereas, CSS
1231   // values are in referecne to their respective edges.
1232   BoxPosition.prototype.toCSSCompatValues = function(reference) {
1233     return {
1234       top: this.top - reference.top,
1235       bottom: reference.bottom - this.bottom,
1236       left: this.left - reference.left,
1237       right: reference.right - this.right,
1238       height: this.height,
1239       width: this.width
1240     };
1241   };
1242
1243   // Get an object that represents the box's position without anything extra.
1244   // Can pass a StyleBox, HTMLElement, or another BoxPositon.
1245   BoxPosition.getSimpleBoxPosition = function(obj) {
1246     var height = obj.div ? obj.div.offsetHeight : obj.tagName ? obj.offsetHeight : 0;
1247     var width = obj.div ? obj.div.offsetWidth : obj.tagName ? obj.offsetWidth : 0;
1248     var top = obj.div ? obj.div.offsetTop : obj.tagName ? obj.offsetTop : 0;
1249
1250     obj = obj.div ? obj.div.getBoundingClientRect() :
1251                   obj.tagName ? obj.getBoundingClientRect() : obj;
1252     var ret = {
1253       left: obj.left,
1254       right: obj.right,
1255       top: obj.top || top,
1256       height: obj.height || height,
1257       bottom: obj.bottom || (top + (obj.height || height)),
1258       width: obj.width || width
1259     };
1260     return ret;
1261   };
1262
1263   // Move a StyleBox to its specified, or next best, position. The containerBox
1264   // is the box that contains the StyleBox, such as a div. boxPositions are
1265   // a list of other boxes that the styleBox can't overlap with.
1266   function moveBoxToLinePosition(window, styleBox, containerBox, boxPositions) {
1267
1268     // Find the best position for a cue box, b, on the video. The axis parameter
1269     // is a list of axis, the order of which, it will move the box along. For example:
1270     // Passing ["+x", "-x"] will move the box first along the x axis in the positive
1271     // direction. If it doesn't find a good position for it there it will then move
1272     // it along the x axis in the negative direction.
1273     function findBestPosition(b, axis) {
1274       var bestPosition,
1275           specifiedPosition = new BoxPosition(b),
1276           percentage = 1; // Highest possible so the first thing we get is better.
1277
1278       for (var i = 0; i < axis.length; i++) {
1279         while (b.overlapsOppositeAxis(containerBox, axis[i]) ||
1280                (b.within(containerBox) && b.overlapsAny(boxPositions))) {
1281           b.move(axis[i]);
1282         }
1283         // We found a spot where we aren't overlapping anything. This is our
1284         // best position.
1285         if (b.within(containerBox)) {
1286           return b;
1287         }
1288         var p = b.intersectPercentage(containerBox);
1289         // If we're outside the container box less then we were on our last try
1290         // then remember this position as the best position.
1291         if (percentage > p) {
1292           bestPosition = new BoxPosition(b);
1293           percentage = p;
1294         }
1295         // Reset the box position to the specified position.
1296         b = new BoxPosition(specifiedPosition);
1297       }
1298       return bestPosition || specifiedPosition;
1299     }
1300
1301     var boxPosition = new BoxPosition(styleBox),
1302         cue = styleBox.cue,
1303         linePos = computeLinePos(cue),
1304         axis = [];
1305
1306     // If we have a line number to align the cue to.
1307     if (cue.snapToLines) {
1308       var size;
1309       switch (cue.vertical) {
1310       case "":
1311         axis = [ "+y", "-y" ];
1312         size = "height";
1313         break;
1314       case "rl":
1315         axis = [ "+x", "-x" ];
1316         size = "width";
1317         break;
1318       case "lr":
1319         axis = [ "-x", "+x" ];
1320         size = "width";
1321         break;
1322       }
1323
1324       var step = boxPosition.lineHeight,
1325           position = step * Math.round(linePos),
1326           maxPosition = containerBox[size] + step,
1327           initialAxis = axis[0];
1328
1329       // If the specified intial position is greater then the max position then
1330       // clamp the box to the amount of steps it would take for the box to
1331       // reach the max position.
1332       if (Math.abs(position) > maxPosition) {
1333         position = position < 0 ? -1 : 1;
1334         position *= Math.ceil(maxPosition / step) * step;
1335       }
1336
1337       // If computed line position returns negative then line numbers are
1338       // relative to the bottom of the video instead of the top. Therefore, we
1339       // need to increase our initial position by the length or width of the
1340       // video, depending on the writing direction, and reverse our axis directions.
1341       if (linePos < 0) {
1342         position += cue.vertical === "" ? containerBox.height : containerBox.width;
1343         axis = axis.reverse();
1344       }
1345
1346       // Move the box to the specified position. This may not be its best
1347       // position.
1348       boxPosition.move(initialAxis, position);
1349
1350     } else {
1351       // If we have a percentage line value for the cue.
1352       var calculatedPercentage = (boxPosition.lineHeight / containerBox.height) * 100;
1353
1354       switch (cue.lineAlign) {
1355       case "middle":
1356         linePos -= (calculatedPercentage / 2);
1357         break;
1358       case "end":
1359         linePos -= calculatedPercentage;
1360         break;
1361       }
1362
1363       // Apply initial line position to the cue box.
1364       switch (cue.vertical) {
1365       case "":
1366         styleBox.applyStyles({
1367           top: styleBox.formatStyle(linePos, "%")
1368         });
1369         break;
1370       case "rl":
1371         styleBox.applyStyles({
1372           left: styleBox.formatStyle(linePos, "%")
1373         });
1374         break;
1375       case "lr":
1376         styleBox.applyStyles({
1377           right: styleBox.formatStyle(linePos, "%")
1378         });
1379         break;
1380       }
1381
1382       axis = [ "+y", "-x", "+x", "-y" ];
1383
1384       // Get the box position again after we've applied the specified positioning
1385       // to it.
1386       boxPosition = new BoxPosition(styleBox);
1387     }
1388
1389     var bestPosition = findBestPosition(boxPosition, axis);
1390     styleBox.move(bestPosition.toCSSCompatValues(containerBox));
1391   }
1392
1393   function WebVTT() {
1394     // Nothing
1395   }
1396
1397   // Helper to allow strings to be decoded instead of the default binary utf8 data.
1398   WebVTT.StringDecoder = function() {
1399     return {
1400       decode: function(data) {
1401         if (!data) {
1402           return "";
1403         }
1404         if (typeof data !== "string") {
1405           throw new Error("Error - expected string data.");
1406         }
1407         return decodeURIComponent(encodeURIComponent(data));
1408       }
1409     };
1410   };
1411
1412   WebVTT.convertCueToDOMTree = function(window, cuetext) {
1413     if (!window || !cuetext) {
1414       return null;
1415     }
1416     return parseContent(window, cuetext);
1417   };
1418
1419   var FONT_SIZE_PERCENT = 0.05;
1420   var FONT_STYLE = "sans-serif";
1421   var CUE_BACKGROUND_PADDING = "1.5%";
1422
1423   // Runs the processing model over the cues and regions passed to it.
1424   // @param overlay A block level element (usually a div) that the computed cues
1425   //                and regions will be placed into.
1426   WebVTT.processCues = function(window, cues, overlay) {
1427     if (!window || !cues || !overlay) {
1428       return null;
1429     }
1430
1431     // Remove all previous children.
1432     while (overlay.firstChild) {
1433       overlay.removeChild(overlay.firstChild);
1434     }
1435
1436     var paddedOverlay = window.document.createElement("div");
1437     paddedOverlay.style.position = "absolute";
1438     paddedOverlay.style.left = "0";
1439     paddedOverlay.style.right = "0";
1440     paddedOverlay.style.top = "0";
1441     paddedOverlay.style.bottom = "0";
1442     paddedOverlay.style.margin = CUE_BACKGROUND_PADDING;
1443     overlay.appendChild(paddedOverlay);
1444
1445     // Determine if we need to compute the display states of the cues. This could
1446     // be the case if a cue's state has been changed since the last computation or
1447     // if it has not been computed yet.
1448     function shouldCompute(cues) {
1449       for (var i = 0; i < cues.length; i++) {
1450         if (cues[i].hasBeenReset || !cues[i].displayState) {
1451           return true;
1452         }
1453       }
1454       return false;
1455     }
1456
1457     // We don't need to recompute the cues' display states. Just reuse them.
1458     if (!shouldCompute(cues)) {
1459       for (var i = 0; i < cues.length; i++) {
1460         paddedOverlay.appendChild(cues[i].displayState);
1461       }
1462       return;
1463     }
1464
1465     var boxPositions = [],
1466         containerBox = BoxPosition.getSimpleBoxPosition(paddedOverlay),
1467         fontSize = Math.round(containerBox.height * FONT_SIZE_PERCENT * 100) / 100;
1468     var styleOptions = {
1469       font: fontSize + "px " + FONT_STYLE
1470     };
1471
1472     (function() {
1473       var styleBox, cue;
1474
1475       for (var i = 0; i < cues.length; i++) {
1476         cue = cues[i];
1477
1478         // Compute the intial position and styles of the cue div.
1479         styleBox = new CueStyleBox(window, cue, styleOptions);
1480         paddedOverlay.appendChild(styleBox.div);
1481
1482         // Move the cue div to it's correct line position.
1483         moveBoxToLinePosition(window, styleBox, containerBox, boxPositions);
1484
1485         // Remember the computed div so that we don't have to recompute it later
1486         // if we don't have too.
1487         cue.displayState = styleBox.div;
1488
1489         boxPositions.push(BoxPosition.getSimpleBoxPosition(styleBox));
1490       }
1491     })();
1492   };
1493
1494   WebVTT.Parser = function(window, vttjs, decoder) {
1495     if (!decoder) {
1496       decoder = vttjs;
1497       vttjs = {};
1498     }
1499     if (!vttjs) {
1500       vttjs = {};
1501     }
1502
1503     this.window = window;
1504     this.vttjs = vttjs;
1505     this.state = "INITIAL";
1506     this.buffer = "";
1507     this.decoder = decoder || new TextDecoder("utf8");
1508     this.regionList = [];
1509   };
1510
1511   WebVTT.Parser.prototype = {
1512     // If the error is a ParsingError then report it to the consumer if
1513     // possible. If it's not a ParsingError then throw it like normal.
1514     reportOrThrowError: function(e) {
1515       if (e instanceof ParsingError) {
1516         this.onparsingerror && this.onparsingerror(e);
1517       } else {
1518         throw e;
1519       }
1520     },
1521     parse: function (data) {
1522       var self = this;
1523
1524       // If there is no data then we won't decode it, but will just try to parse
1525       // whatever is in buffer already. This may occur in circumstances, for
1526       // example when flush() is called.
1527       if (data) {
1528         // Try to decode the data that we received.
1529         self.buffer += self.decoder.decode(data, {stream: true});
1530       }
1531
1532       function collectNextLine() {
1533         var buffer = self.buffer;
1534         var pos = 0;
1535         while (pos < buffer.length && buffer[pos] !== '\r' && buffer[pos] !== '\n') {
1536           ++pos;
1537         }
1538         var line = buffer.substr(0, pos);
1539         // Advance the buffer early in case we fail below.
1540         if (buffer[pos] === '\r') {
1541           ++pos;
1542         }
1543         if (buffer[pos] === '\n') {
1544           ++pos;
1545         }
1546         self.buffer = buffer.substr(pos);
1547         return line;
1548       }
1549
1550       // 3.4 WebVTT region and WebVTT region settings syntax
1551       function parseRegion(input) {
1552         var settings = new Settings();
1553
1554         parseOptions(input, function (k, v) {
1555           switch (k) {
1556           case "id":
1557             settings.set(k, v);
1558             break;
1559           case "width":
1560             settings.percent(k, v);
1561             break;
1562           case "lines":
1563             settings.integer(k, v);
1564             break;
1565           case "regionanchor":
1566           case "viewportanchor":
1567             var xy = v.split(',');
1568             if (xy.length !== 2) {
1569               break;
1570             }
1571             // We have to make sure both x and y parse, so use a temporary
1572             // settings object here.
1573             var anchor = new Settings();
1574             anchor.percent("x", xy[0]);
1575             anchor.percent("y", xy[1]);
1576             if (!anchor.has("x") || !anchor.has("y")) {
1577               break;
1578             }
1579             settings.set(k + "X", anchor.get("x"));
1580             settings.set(k + "Y", anchor.get("y"));
1581             break;
1582           case "scroll":
1583             settings.alt(k, v, ["up"]);
1584             break;
1585           }
1586         }, /=/, /\s/);
1587
1588         // Create the region, using default values for any values that were not
1589         // specified.
1590         if (settings.has("id")) {
1591           var region = new (self.vttjs.VTTRegion || self.window.VTTRegion)();
1592           region.width = settings.get("width", 100);
1593           region.lines = settings.get("lines", 3);
1594           region.regionAnchorX = settings.get("regionanchorX", 0);
1595           region.regionAnchorY = settings.get("regionanchorY", 100);
1596           region.viewportAnchorX = settings.get("viewportanchorX", 0);
1597           region.viewportAnchorY = settings.get("viewportanchorY", 100);
1598           region.scroll = settings.get("scroll", "");
1599           // Register the region.
1600           self.onregion && self.onregion(region);
1601           // Remember the VTTRegion for later in case we parse any VTTCues that
1602           // reference it.
1603           self.regionList.push({
1604             id: settings.get("id"),
1605             region: region
1606           });
1607         }
1608       }
1609
1610       // draft-pantos-http-live-streaming-20
1611       // https://tools.ietf.org/html/draft-pantos-http-live-streaming-20#section-3.5
1612       // 3.5 WebVTT
1613       function parseTimestampMap(input) {
1614         var settings = new Settings();
1615
1616         parseOptions(input, function(k, v) {
1617           switch(k) {
1618           case "MPEGT":
1619             settings.integer(k + 'S', v);
1620             break;
1621           case "LOCA":
1622             settings.set(k + 'L', parseTimeStamp(v));
1623             break;
1624           }
1625         }, /[^\d]:/, /,/);
1626
1627         self.ontimestampmap && self.ontimestampmap({
1628           "MPEGTS": settings.get("MPEGTS"),
1629           "LOCAL": settings.get("LOCAL")
1630         });
1631       }
1632
1633       // 3.2 WebVTT metadata header syntax
1634       function parseHeader(input) {
1635         if (input.match(/X-TIMESTAMP-MAP/)) {
1636           // This line contains HLS X-TIMESTAMP-MAP metadata
1637           parseOptions(input, function(k, v) {
1638             switch(k) {
1639             case "X-TIMESTAMP-MAP":
1640               parseTimestampMap(v);
1641               break;
1642             }
1643           }, /=/);
1644         } else {
1645           parseOptions(input, function (k, v) {
1646             switch (k) {
1647             case "Region":
1648               // 3.3 WebVTT region metadata header syntax
1649               parseRegion(v);
1650               break;
1651             }
1652           }, /:/);
1653         }
1654
1655       }
1656
1657       // 5.1 WebVTT file parsing.
1658       try {
1659         var line;
1660         if (self.state === "INITIAL") {
1661           // We can't start parsing until we have the first line.
1662           if (!/\r\n|\n/.test(self.buffer)) {
1663             return this;
1664           }
1665
1666           line = collectNextLine();
1667
1668           var m = line.match(/^WEBVTT([ \t].*)?$/);
1669           if (!m || !m[0]) {
1670             throw new ParsingError(ParsingError.Errors.BadSignature);
1671           }
1672
1673           self.state = "HEADER";
1674         }
1675
1676         var alreadyCollectedLine = false;
1677         while (self.buffer) {
1678           // We can't parse a line until we have the full line.
1679           if (!/\r\n|\n/.test(self.buffer)) {
1680             return this;
1681           }
1682
1683           if (!alreadyCollectedLine) {
1684             line = collectNextLine();
1685           } else {
1686             alreadyCollectedLine = false;
1687           }
1688
1689           switch (self.state) {
1690           case "HEADER":
1691             // 13-18 - Allow a header (metadata) under the WEBVTT line.
1692             if (/:/.test(line)) {
1693               parseHeader(line);
1694             } else if (!line) {
1695               // An empty line terminates the header and starts the body (cues).
1696               self.state = "ID";
1697             }
1698             continue;
1699           case "NOTE":
1700             // Ignore NOTE blocks.
1701             if (!line) {
1702               self.state = "ID";
1703             }
1704             continue;
1705           case "ID":
1706             // Check for the start of NOTE blocks.
1707             if (/^NOTE($|[ \t])/.test(line)) {
1708               self.state = "NOTE";
1709               break;
1710             }
1711             // 19-29 - Allow any number of line terminators, then initialize new cue values.
1712             if (!line) {
1713               continue;
1714             }
1715             self.cue = new (self.vttjs.VTTCue || self.window.VTTCue)(0, 0, "");
1716             self.state = "CUE";
1717             // 30-39 - Check if self line contains an optional identifier or timing data.
1718             if (line.indexOf("-->") === -1) {
1719               self.cue.id = line;
1720               continue;
1721             }
1722             // Process line as start of a cue.
1723             /*falls through*/
1724           case "CUE":
1725             // 40 - Collect cue timings and settings.
1726             try {
1727               parseCue(line, self.cue, self.regionList);
1728             } catch (e) {
1729               self.reportOrThrowError(e);
1730               // In case of an error ignore rest of the cue.
1731               self.cue = null;
1732               self.state = "BADCUE";
1733               continue;
1734             }
1735             self.state = "CUETEXT";
1736             continue;
1737           case "CUETEXT":
1738             var hasSubstring = line.indexOf("-->") !== -1;
1739             // 34 - If we have an empty line then report the cue.
1740             // 35 - If we have the special substring '-->' then report the cue,
1741             // but do not collect the line as we need to process the current
1742             // one as a new cue.
1743             if (!line || hasSubstring && (alreadyCollectedLine = true)) {
1744               // We are done parsing self cue.
1745               self.oncue && self.oncue(self.cue);
1746               self.cue = null;
1747               self.state = "ID";
1748               continue;
1749             }
1750             if (self.cue.text) {
1751               self.cue.text += "\n";
1752             }
1753             self.cue.text += line;
1754             continue;
1755           case "BADCUE": // BADCUE
1756             // 54-62 - Collect and discard the remaining cue.
1757             if (!line) {
1758               self.state = "ID";
1759             }
1760             continue;
1761           }
1762         }
1763       } catch (e) {
1764         self.reportOrThrowError(e);
1765
1766         // If we are currently parsing a cue, report what we have.
1767         if (self.state === "CUETEXT" && self.cue && self.oncue) {
1768           self.oncue(self.cue);
1769         }
1770         self.cue = null;
1771         // Enter BADWEBVTT state if header was not parsed correctly otherwise
1772         // another exception occurred so enter BADCUE state.
1773         self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
1774       }
1775       return this;
1776     },
1777     flush: function () {
1778       var self = this;
1779       try {
1780         // Finish decoding the stream.
1781         self.buffer += self.decoder.decode();
1782         // Synthesize the end of the current cue or region.
1783         if (self.cue || self.state === "HEADER") {
1784           self.buffer += "\n\n";
1785           self.parse();
1786         }
1787         // If we've flushed, parsed, and we're still on the INITIAL state then
1788         // that means we don't have enough of the stream to parse the first
1789         // line.
1790         if (self.state === "INITIAL") {
1791           throw new ParsingError(ParsingError.Errors.BadSignature);
1792         }
1793       } catch(e) {
1794         self.reportOrThrowError(e);
1795       }
1796       self.onflush && self.onflush();
1797       return this;
1798     }
1799   };
1800
1801   global.WebVTT = WebVTT;
1802
1803 }(this, (this.vttjs || {})));