a73f655d060c3331b89eb0fab7900182dda27b52
[WebKit-https.git] / Source / WebCore / html / track / VTTCue.cpp
1 /*
2  * Copyright (C) 2011, 2013 Google Inc.  All rights reserved.
3  * Copyright (C) 2011-2014 Apple Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are
7  * met:
8  *
9  *     * Redistributions of source code must retain the above copyright
10  * notice, this list of conditions and the following disclaimer.
11  *     * Redistributions in binary form must reproduce the above
12  * copyright notice, this list of conditions and the following disclaimer
13  * in the documentation and/or other materials provided with the
14  * distribution.
15  *     * Neither the name of Google Inc. nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31
32 #include "config.h"
33
34 #if ENABLE(VIDEO_TRACK)
35 #include "VTTCue.h"
36
37 #include "CSSPropertyNames.h"
38 #include "CSSValueKeywords.h"
39 #include "DocumentFragment.h"
40 #include "Event.h"
41 #include "HTMLDivElement.h"
42 #include "HTMLSpanElement.h"
43 #include "Logging.h"
44 #include "NodeTraversal.h"
45 #include "RenderVTTCue.h"
46 #include "Text.h"
47 #include "TextTrack.h"
48 #include "TextTrackCueList.h"
49 #include "VTTRegionList.h"
50 #include "VTTScanner.h"
51 #include "WebVTTElement.h"
52 #include "WebVTTParser.h"
53 #include <wtf/MathExtras.h>
54 #include <wtf/text/StringBuilder.h>
55
56 namespace WebCore {
57
58 // This constant should correspond with the percentage returned by CaptionUserPreferences::captionFontSizeScaleAndImportance.
59 const static double DEFAULTCAPTIONFONTSIZEPERCENTAGE = 5;
60
61 static const int undefinedPosition = -1;
62
63 static const CSSValueID displayWritingModeMap[] = {
64     CSSValueHorizontalTb, CSSValueVerticalRl, CSSValueVerticalLr
65 };
66 COMPILE_ASSERT(WTF_ARRAY_LENGTH(displayWritingModeMap) == VTTCue::NumberOfWritingDirections, displayWritingModeMap_has_wrong_size);
67
68 static const CSSValueID displayAlignmentMap[] = {
69     CSSValueStart, CSSValueCenter, CSSValueEnd, CSSValueLeft, CSSValueRight
70 };
71 COMPILE_ASSERT(WTF_ARRAY_LENGTH(displayAlignmentMap) == VTTCue::NumberOfAlignments, displayAlignmentMap_has_wrong_size);
72
73 static const String& startKeyword()
74 {
75     static NeverDestroyed<const String> start(ASCIILiteral("start"));
76     return start;
77 }
78
79 static const String& middleKeyword()
80 {
81     static NeverDestroyed<const String> middle(ASCIILiteral("middle"));
82     return middle;
83 }
84
85 static const String& endKeyword()
86 {
87     static NeverDestroyed<const String> end(ASCIILiteral("end"));
88     return end;
89 }
90
91 static const String& leftKeyword()
92 {
93     static NeverDestroyed<const String> left("left");
94     return left;
95 }
96
97 static const String& rightKeyword()
98 {
99     static NeverDestroyed<const String> right("right");
100     return right;
101 }
102
103 static const String& horizontalKeyword()
104 {
105     return emptyString();
106 }
107
108 static const String& verticalGrowingLeftKeyword()
109 {
110     static NeverDestroyed<const String> verticalrl(ASCIILiteral("rl"));
111     return verticalrl;
112 }
113
114 static const String& verticalGrowingRightKeyword()
115 {
116     static NeverDestroyed<const String> verticallr(ASCIILiteral("lr"));
117     return verticallr;
118 }
119
120 // ----------------------------
121
122 Ref<VTTCueBox> VTTCueBox::create(Document& document, VTTCue& cue)
123 {
124     VTTCueBox& cueBox = *new VTTCueBox(document, cue);
125     cueBox.setPseudo(VTTCueBox::vttCueBoxShadowPseudoId());
126     return adoptRef(cueBox);
127 }
128
129 VTTCueBox::VTTCueBox(Document& document, VTTCue& cue)
130     : HTMLElement(divTag, document)
131     , m_cue(cue)
132 {
133     setPseudo(vttCueBoxShadowPseudoId());
134 }
135
136 VTTCue* VTTCueBox::getCue() const
137 {
138     return &m_cue;
139 }
140
141 void VTTCueBox::applyCSSProperties(const IntSize& videoSize)
142 {
143     // FIXME: Apply all the initial CSS positioning properties. http://wkb.ug/79916
144     if (!m_cue.regionId().isEmpty()) {
145         setInlineStyleProperty(CSSPropertyPosition, CSSValueRelative);
146         return;
147     }
148
149     // 3.5.1 On the (root) List of WebVTT Node Objects:
150
151     // the 'position' property must be set to 'absolute'
152     setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
153
154     //  the 'unicode-bidi' property must be set to 'plaintext'
155     setInlineStyleProperty(CSSPropertyUnicodeBidi, CSSValueWebkitPlaintext);
156
157     // the 'direction' property must be set to direction
158     setInlineStyleProperty(CSSPropertyDirection, m_cue.getCSSWritingDirection());
159
160     // the 'writing-mode' property must be set to writing-mode
161     setInlineStyleProperty(CSSPropertyWebkitWritingMode, m_cue.getCSSWritingMode(), false);
162
163     std::pair<float, float> position = m_cue.getCSSPosition();
164
165     // the 'top' property must be set to top,
166     setInlineStyleProperty(CSSPropertyTop, static_cast<double>(position.second), CSSPrimitiveValue::CSS_PERCENTAGE);
167
168     // the 'left' property must be set to left
169     setInlineStyleProperty(CSSPropertyLeft, static_cast<double>(position.first), CSSPrimitiveValue::CSS_PERCENTAGE);
170
171     double authorFontSize = std::min(videoSize.width(), videoSize.height()) * DEFAULTCAPTIONFONTSIZEPERCENTAGE / 100.0;
172     double multiplier = 1.0;
173     if (authorFontSize)
174         multiplier = m_fontSizeFromCaptionUserPrefs / authorFontSize;
175
176     double textPosition = m_cue.position();
177     double maxSize = 100.0;
178     CSSValueID alignment = m_cue.getCSSAlignment();
179     if (alignment == CSSValueEnd || alignment == CSSValueRight)
180         maxSize = textPosition;
181     else if (alignment == CSSValueStart || alignment == CSSValueLeft)
182         maxSize = 100.0 - textPosition;
183
184     double newCueSize = std::min(m_cue.getCSSSize() * multiplier, 100.0);
185     // the 'width' property must be set to width, and the 'height' property  must be set to height
186     if (m_cue.vertical() == horizontalKeyword()) {
187         setInlineStyleProperty(CSSPropertyWidth, newCueSize, CSSPrimitiveValue::CSS_PERCENTAGE);
188         setInlineStyleProperty(CSSPropertyHeight, CSSValueAuto);
189         setInlineStyleProperty(CSSPropertyMinWidth, "-webkit-min-content");
190         setInlineStyleProperty(CSSPropertyMaxWidth, maxSize, CSSPrimitiveValue::CSS_PERCENTAGE);
191         if ((alignment == CSSValueMiddle || alignment == CSSValueCenter) && multiplier != 1.0)
192             setInlineStyleProperty(CSSPropertyLeft, static_cast<double>(position.first - (newCueSize - m_cue.getCSSSize()) / 2), CSSPrimitiveValue::CSS_PERCENTAGE);
193     } else {
194         setInlineStyleProperty(CSSPropertyWidth, CSSValueAuto);
195         setInlineStyleProperty(CSSPropertyHeight, newCueSize, CSSPrimitiveValue::CSS_PERCENTAGE);
196         setInlineStyleProperty(CSSPropertyMinHeight, "-webkit-min-content");
197         setInlineStyleProperty(CSSPropertyMaxHeight, maxSize, CSSPrimitiveValue::CSS_PERCENTAGE);
198         if ((alignment == CSSValueMiddle || alignment == CSSValueCenter) && multiplier != 1.0)
199             setInlineStyleProperty(CSSPropertyTop, static_cast<double>(position.second - (newCueSize - m_cue.getCSSSize()) / 2), CSSPrimitiveValue::CSS_PERCENTAGE);
200     }
201
202     // The 'text-align' property on the (root) List of WebVTT Node Objects must
203     // be set to the value in the second cell of the row of the table below
204     // whose first cell is the value of the corresponding cue's text track cue
205     // alignment:
206     setInlineStyleProperty(CSSPropertyTextAlign, m_cue.getCSSAlignment());
207     
208     if (!m_cue.snapToLines()) {
209         // 10.13.1 Set up x and y:
210         // Note: x and y are set through the CSS left and top above.
211
212         // 10.13.2 Position the boxes in boxes such that the point x% along the
213         // width of the bounding box of the boxes in boxes is x% of the way
214         // across the width of the video's rendering area, and the point y%
215         // along the height of the bounding box of the boxes in boxes is y%
216         // of the way across the height of the video's rendering area, while
217         // maintaining the relative positions of the boxes in boxes to each
218         // other.
219         setInlineStyleProperty(CSSPropertyTransform,
220             String::format("translate(-%.2f%%, -%.2f%%)", position.first, position.second));
221
222         setInlineStyleProperty(CSSPropertyWhiteSpace, CSSValuePre);
223     }
224 }
225
226 const AtomicString& VTTCueBox::vttCueBoxShadowPseudoId()
227 {
228     static NeverDestroyed<const AtomicString> trackDisplayBoxShadowPseudoId("-webkit-media-text-track-display", AtomicString::ConstructFromLiteral);
229     return trackDisplayBoxShadowPseudoId;
230 }
231
232 RenderPtr<RenderElement> VTTCueBox::createElementRenderer(RenderStyle&& style, const RenderTreePosition&)
233 {
234     return createRenderer<RenderVTTCue>(*this, WTFMove(style));
235 }
236
237 // ----------------------------
238
239 const AtomicString& VTTCue::cueBackdropShadowPseudoId()
240 {
241     static NeverDestroyed<const AtomicString> cueBackdropShadowPseudoId("-webkit-media-text-track-display-backdrop", AtomicString::ConstructFromLiteral);
242     return cueBackdropShadowPseudoId;
243 }
244
245 Ref<VTTCue> VTTCue::create(ScriptExecutionContext& context, const WebVTTCueData& data)
246 {
247     return adoptRef(*new VTTCue(context, data));
248 }
249
250 VTTCue::VTTCue(ScriptExecutionContext& context, const MediaTime& start, const MediaTime& end, const String& content)
251     : TextTrackCue(context, start, end)
252     , m_content(content)
253 {
254     initialize(context);
255 }
256
257 VTTCue::VTTCue(ScriptExecutionContext& context, const WebVTTCueData& cueData)
258     : TextTrackCue(context, MediaTime::zeroTime(), MediaTime::zeroTime())
259 {
260     initialize(context);
261     setText(cueData.content());
262     setStartTime(cueData.startTime());
263     setEndTime(cueData.endTime());
264     setId(cueData.id());
265     setCueSettings(cueData.settings());
266     m_originalStartTime = cueData.originalStartTime();
267 }
268
269 VTTCue::~VTTCue()
270 {
271     // FIXME: We should set m_cue in VTTCueBox to nullptr instead.
272     if (m_displayTree && m_displayTree->document().refCount())
273         m_displayTree->remove(ASSERT_NO_EXCEPTION);
274 }
275
276 void VTTCue::initialize(ScriptExecutionContext& context)
277 {
278     m_linePosition = undefinedPosition;
279     m_computedLinePosition = undefinedPosition;
280     m_textPosition = 50;
281     m_cueSize = 100;
282     m_writingDirection = Horizontal;
283     m_cueAlignment = Middle;
284     m_webVTTNodeTree = nullptr;
285     m_cueBackdropBox = HTMLDivElement::create(downcast<Document>(context));
286     m_cueHighlightBox = HTMLSpanElement::create(spanTag, downcast<Document>(context));
287     m_displayDirection = CSSValueLtr;
288     m_displaySize = 0;
289     m_snapToLines = true;
290     m_displayTreeShouldChange = true;
291     m_notifyRegion = true;
292     m_originalStartTime = MediaTime::zeroTime();
293 }
294
295 PassRefPtr<VTTCueBox> VTTCue::createDisplayTree()
296 {
297     return VTTCueBox::create(ownerDocument(), *this);
298 }
299
300 VTTCueBox* VTTCue::displayTreeInternal()
301 {
302     if (!m_displayTree)
303         m_displayTree = createDisplayTree();
304     return m_displayTree.get();
305 }
306
307 void VTTCue::didChange()
308 {
309     TextTrackCue::didChange();
310     m_displayTreeShouldChange = true;
311 }
312
313 const String& VTTCue::vertical() const
314 {
315     switch (m_writingDirection) {
316     case Horizontal: 
317         return horizontalKeyword();
318     case VerticalGrowingLeft:
319         return verticalGrowingLeftKeyword();
320     case VerticalGrowingRight:
321         return verticalGrowingRightKeyword();
322     default:
323         ASSERT_NOT_REACHED();
324         return emptyString();
325     }
326 }
327
328 void VTTCue::setVertical(const String& value, ExceptionCode& ec)
329 {
330     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#dom-texttrackcue-vertical
331     // On setting, the text track cue writing direction must be set to the value given 
332     // in the first cell of the row in the table above whose second cell is a 
333     // case-sensitive match for the new value, if any. If none of the values match, then
334     // the user agent must instead throw a SyntaxError exception.
335     
336     WritingDirection direction = m_writingDirection;
337     if (value == horizontalKeyword())
338         direction = Horizontal;
339     else if (value == verticalGrowingLeftKeyword())
340         direction = VerticalGrowingLeft;
341     else if (value == verticalGrowingRightKeyword())
342         direction = VerticalGrowingRight;
343     else
344         ec = SYNTAX_ERR;
345     
346     if (direction == m_writingDirection)
347         return;
348
349     willChange();
350     m_writingDirection = direction;
351     didChange();
352 }
353
354 void VTTCue::setSnapToLines(bool value)
355 {
356     if (m_snapToLines == value)
357         return;
358     
359     willChange();
360     m_snapToLines = value;
361     didChange();
362 }
363
364 void VTTCue::setLine(double position, ExceptionCode& ec)
365 {
366     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#dom-texttrackcue-line
367     // On setting, if the text track cue snap-to-lines flag is not set, and the new
368     // value is negative or greater than 100, then throw an IndexSizeError exception.
369     if (!m_snapToLines && (position < 0 || position > 100)) {
370         ec = INDEX_SIZE_ERR;
371         return;
372     }
373
374     // Otherwise, set the text track cue line position to the new value.
375     if (m_linePosition == position)
376         return;
377
378     willChange();
379     m_linePosition = position;
380     m_computedLinePosition = calculateComputedLinePosition();
381     didChange();
382 }
383
384 void VTTCue::setPosition(double position, ExceptionCode& ec)
385 {
386     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#dom-texttrackcue-position
387     // On setting, if the new value is negative or greater than 100, then throw an IndexSizeError exception.
388     // Otherwise, set the text track cue text position to the new value.
389     if (position < 0 || position > 100) {
390         ec = INDEX_SIZE_ERR;
391         return;
392     }
393     
394     // Otherwise, set the text track cue line position to the new value.
395     if (m_textPosition == position)
396         return;
397     
398     willChange();
399     m_textPosition = position;
400     didChange();
401 }
402
403 void VTTCue::setSize(int size, ExceptionCode& ec)
404 {
405     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#dom-texttrackcue-size
406     // On setting, if the new value is negative or greater than 100, then throw an IndexSizeError
407     // exception. Otherwise, set the text track cue size to the new value.
408     if (size < 0 || size > 100) {
409         ec = INDEX_SIZE_ERR;
410         return;
411     }
412     
413     // Otherwise, set the text track cue line position to the new value.
414     if (m_cueSize == size)
415         return;
416     
417     willChange();
418     m_cueSize = size;
419     didChange();
420 }
421
422 const String& VTTCue::align() const
423 {
424     switch (m_cueAlignment) {
425     case Start:
426         return startKeyword();
427     case Middle:
428         return middleKeyword();
429     case End:
430         return endKeyword();
431     case Left:
432         return leftKeyword();
433     case Right:
434         return rightKeyword();
435     default:
436         ASSERT_NOT_REACHED();
437         return emptyString();
438     }
439 }
440
441 void VTTCue::setAlign(const String& value, ExceptionCode& ec)
442 {
443     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#dom-texttrackcue-align
444     // On setting, the text track cue alignment must be set to the value given in the 
445     // first cell of the row in the table above whose second cell is a case-sensitive
446     // match for the new value, if any. If none of the values match, then the user
447     // agent must instead throw a SyntaxError exception.
448     
449     CueAlignment alignment = m_cueAlignment;
450     if (value == startKeyword())
451         alignment = Start;
452     else if (value == middleKeyword())
453         alignment = Middle;
454     else if (value == endKeyword())
455         alignment = End;
456     else if (value == leftKeyword())
457         alignment = Left;
458     else if (value == rightKeyword())
459         alignment = Right;
460     else
461         ec = SYNTAX_ERR;
462     
463     if (alignment == m_cueAlignment)
464         return;
465
466     willChange();
467     m_cueAlignment = alignment;
468     didChange();
469 }
470     
471 void VTTCue::setText(const String& text)
472 {
473     if (m_content == text)
474         return;
475     
476     willChange();
477     // Clear the document fragment but don't bother to create it again just yet as we can do that
478     // when it is requested.
479     m_webVTTNodeTree = nullptr;
480     m_content = text;
481     didChange();
482 }
483
484 void VTTCue::createWebVTTNodeTree()
485 {
486     if (!m_webVTTNodeTree)
487         m_webVTTNodeTree = WebVTTParser::createDocumentFragmentFromCueText(ownerDocument(), m_content);
488 }
489
490 void VTTCue::copyWebVTTNodeToDOMTree(ContainerNode* webVTTNode, ContainerNode* parent)
491 {
492     for (Node* node = webVTTNode->firstChild(); node; node = node->nextSibling()) {
493         RefPtr<Node> clonedNode;
494         if (is<WebVTTElement>(*node))
495             clonedNode = downcast<WebVTTElement>(*node).createEquivalentHTMLElement(ownerDocument());
496         else
497             clonedNode = node->cloneNode(false);
498         parent->appendChild(*clonedNode, ASSERT_NO_EXCEPTION);
499         if (is<ContainerNode>(*node))
500             copyWebVTTNodeToDOMTree(downcast<ContainerNode>(node), downcast<ContainerNode>(clonedNode.get()));
501     }
502 }
503
504 RefPtr<DocumentFragment> VTTCue::getCueAsHTML()
505 {
506     createWebVTTNodeTree();
507     if (!m_webVTTNodeTree)
508         return nullptr;
509
510     auto clonedFragment = DocumentFragment::create(ownerDocument());
511     copyWebVTTNodeToDOMTree(m_webVTTNodeTree.get(), clonedFragment.ptr());
512     return WTFMove(clonedFragment);
513 }
514
515 RefPtr<DocumentFragment> VTTCue::createCueRenderingTree()
516 {
517     createWebVTTNodeTree();
518     if (!m_webVTTNodeTree)
519         return nullptr;
520
521     auto clonedFragment = DocumentFragment::create(ownerDocument());
522     m_webVTTNodeTree->cloneChildNodes(clonedFragment);
523     return WTFMove(clonedFragment);
524 }
525
526 void VTTCue::setRegionId(const String& regionId)
527 {
528     if (m_regionId == regionId)
529         return;
530
531     willChange();
532     m_regionId = regionId;
533     didChange();
534 }
535
536 void VTTCue::notifyRegionWhenRemovingDisplayTree(bool notifyRegion)
537 {
538     m_notifyRegion = notifyRegion;
539 }
540
541 void VTTCue::setIsActive(bool active)
542 {
543     TextTrackCue::setIsActive(active);
544
545     if (!active) {
546         if (!hasDisplayTree())
547             return;
548
549         // Remove the display tree as soon as the cue becomes inactive.
550         removeDisplayTree();
551     }
552 }
553
554 int VTTCue::calculateComputedLinePosition()
555 {
556     // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#text-track-cue-computed-line-position
557
558     // If the text track cue line position is numeric, then that is the text
559     // track cue computed line position.
560     if (m_linePosition != undefinedPosition)
561         return m_linePosition;
562
563     // If the text track cue snap-to-lines flag of the text track cue is not
564     // set, the text track cue computed line position is the value 100;
565     if (!m_snapToLines)
566         return 100;
567
568     // Otherwise, it is the value returned by the following algorithm:
569
570     // If cue is not associated with a text track, return -1 and abort these
571     // steps.
572     if (!track())
573         return -1;
574
575     // Let n be the number of text tracks whose text track mode is showing or
576     // showing by default and that are in the media element's list of text
577     // tracks before track.
578     int n = track()->trackIndexRelativeToRenderedTracks();
579
580     // Increment n by one.
581     n++;
582
583     // Negate n.
584     n = -n;
585
586     return n;
587 }
588
589 static bool isCueParagraphSeparator(UChar character)
590 {
591     // Within a cue, paragraph boundaries are only denoted by Type B characters,
592     // such as U+000A LINE FEED (LF), U+0085 NEXT LINE (NEL), and U+2029 PARAGRAPH SEPARATOR.
593     return u_charType(character) == U_PARAGRAPH_SEPARATOR;
594 }
595
596 void VTTCue::determineTextDirection()
597 {
598     static NeverDestroyed<const String> rtTag(ASCIILiteral("rt"));
599     createWebVTTNodeTree();
600     if (!m_webVTTNodeTree)
601         return;
602
603     // Apply the Unicode Bidirectional Algorithm's Paragraph Level steps to the
604     // concatenation of the values of each WebVTT Text Object in nodes, in a
605     // pre-order, depth-first traversal, excluding WebVTT Ruby Text Objects and
606     // their descendants.
607     StringBuilder paragraphBuilder;
608     for (Node* node = m_webVTTNodeTree->firstChild(); node; node = NodeTraversal::next(*node, m_webVTTNodeTree.get())) {
609         // FIXME: The code does not match the comment above. This does not actually exclude Ruby Text Object descendant.
610         if (!node->isTextNode() || node->localName() == rtTag)
611             continue;
612
613         paragraphBuilder.append(node->nodeValue());
614     }
615
616     String paragraph = paragraphBuilder.toString();
617     if (!paragraph.length())
618         return;
619
620     for (size_t i = 0; i < paragraph.length(); ++i) {
621         UChar current = paragraph[i];
622         if (!current || isCueParagraphSeparator(current))
623             return;
624
625         if (UChar current = paragraph[i]) {
626             UCharDirection charDirection = u_charDirection(current);
627             if (charDirection == U_LEFT_TO_RIGHT) {
628                 m_displayDirection = CSSValueLtr;
629                 return;
630             }
631             if (charDirection == U_RIGHT_TO_LEFT || charDirection == U_RIGHT_TO_LEFT_ARABIC) {
632                 m_displayDirection = CSSValueRtl;
633                 return;
634             }
635         }
636     }
637 }
638
639 void VTTCue::calculateDisplayParameters()
640 {
641     // Steps 10.2, 10.3
642     determineTextDirection();
643
644     // 10.4 If the text track cue writing direction is horizontal, then let
645     // block-flow be 'tb'. Otherwise, if the text track cue writing direction is
646     // vertical growing left, then let block-flow be 'lr'. Otherwise, the text
647     // track cue writing direction is vertical growing right; let block-flow be
648     // 'rl'.
649
650     // The above step is done through the writing direction static map.
651
652     // 10.5 Determine the value of maximum size for cue as per the appropriate
653     // rules from the following list:
654     int maximumSize = m_textPosition;
655     if ((m_writingDirection == Horizontal && m_cueAlignment == Start && m_displayDirection == CSSValueLtr)
656         || (m_writingDirection == Horizontal && m_cueAlignment == End && m_displayDirection == CSSValueRtl)
657         || (m_writingDirection == Horizontal && m_cueAlignment == Left)
658         || (m_writingDirection == VerticalGrowingLeft && (m_cueAlignment == Start || m_cueAlignment == Left))
659         || (m_writingDirection == VerticalGrowingRight && (m_cueAlignment == Start || m_cueAlignment == Left))) {
660         maximumSize = 100 - m_textPosition;
661     } else if ((m_writingDirection == Horizontal && m_cueAlignment == End && m_displayDirection == CSSValueLtr)
662         || (m_writingDirection == Horizontal && m_cueAlignment == Start && m_displayDirection == CSSValueRtl)
663         || (m_writingDirection == Horizontal && m_cueAlignment == Right)
664         || (m_writingDirection == VerticalGrowingLeft && (m_cueAlignment == End || m_cueAlignment == Right))
665         || (m_writingDirection == VerticalGrowingRight && (m_cueAlignment == End || m_cueAlignment == Right))) {
666         maximumSize = m_textPosition;
667     } else if (m_cueAlignment == Middle) {
668         maximumSize = m_textPosition <= 50 ? m_textPosition : (100 - m_textPosition);
669         maximumSize = maximumSize * 2;
670     } else
671         ASSERT_NOT_REACHED();
672
673     // 10.6 If the text track cue size is less than maximum size, then let size
674     // be text track cue size. Otherwise, let size be maximum size.
675     m_displaySize = std::min(m_cueSize, maximumSize);
676
677     // FIXME: Understand why step 10.7 is missing (just a copy/paste error?)
678     // Could be done within a spec implementation check - http://crbug.com/301580
679
680     // 10.8 Determine the value of x-position or y-position for cue as per the
681     // appropriate rules from the following list:
682     if (m_writingDirection == Horizontal) {
683         switch (m_cueAlignment) {
684         case Start:
685             if (m_displayDirection == CSSValueLtr)
686                 m_displayPosition.first = m_textPosition;
687             else
688                 m_displayPosition.first = 100 - m_textPosition - m_displaySize;
689             break;
690         case End:
691             if (m_displayDirection == CSSValueRtl)
692                 m_displayPosition.first = 100 - m_textPosition;
693             else
694                 m_displayPosition.first = m_textPosition - m_displaySize;
695             break;
696         case Left:
697             if (m_displayDirection == CSSValueLtr)
698                 m_displayPosition.first = m_textPosition;
699             else
700                 m_displayPosition.first = 100 - m_textPosition;
701             break;
702         case Right:
703             if (m_displayDirection == CSSValueLtr)
704                 m_displayPosition.first = m_textPosition - m_displaySize;
705             else
706                 m_displayPosition.first = 100 - m_textPosition - m_displaySize;
707             break;
708         case Middle:
709             if (m_displayDirection == CSSValueLtr)
710                 m_displayPosition.first = m_textPosition - m_displaySize / 2;
711             else
712                 m_displayPosition.first = 100 - m_textPosition - m_displaySize / 2;
713             break;
714         case NumberOfAlignments:
715             ASSERT_NOT_REACHED();
716         }
717     }
718
719     // A text track cue has a text track cue computed line position whose value
720     // is defined in terms of the other aspects of the cue.
721     m_computedLinePosition = calculateComputedLinePosition();
722
723     // 10.9 Determine the value of whichever of x-position or y-position is not
724     // yet calculated for cue as per the appropriate rules from the following
725     // list:
726     if (m_snapToLines && m_displayPosition.second == undefinedPosition && m_writingDirection == Horizontal)
727         m_displayPosition.second = 0;
728
729     if (!m_snapToLines && m_displayPosition.second == undefinedPosition && m_writingDirection == Horizontal)
730         m_displayPosition.second = m_computedLinePosition;
731
732     if (m_snapToLines && m_displayPosition.first == undefinedPosition
733         && (m_writingDirection == VerticalGrowingLeft || m_writingDirection == VerticalGrowingRight))
734         m_displayPosition.first = 0;
735
736     if (!m_snapToLines && (m_writingDirection == VerticalGrowingLeft || m_writingDirection == VerticalGrowingRight))
737         m_displayPosition.first = m_computedLinePosition;
738 }
739     
740 void VTTCue::markFutureAndPastNodes(ContainerNode* root, const MediaTime& previousTimestamp, const MediaTime& movieTime)
741 {
742     static NeverDestroyed<const String> timestampTag(ASCIILiteral("timestamp"));
743     
744     bool isPastNode = true;
745     MediaTime currentTimestamp = previousTimestamp;
746     if (currentTimestamp > movieTime)
747         isPastNode = false;
748     
749     for (Node* child = root->firstChild(); child; child = NodeTraversal::next(*child, root)) {
750         if (child->nodeName() == timestampTag) {
751             MediaTime currentTimestamp;
752             bool check = WebVTTParser::collectTimeStamp(child->nodeValue(), currentTimestamp);
753             ASSERT_UNUSED(check, check);
754             
755             currentTimestamp += m_originalStartTime;
756             if (currentTimestamp > movieTime)
757                 isPastNode = false;
758         }
759         
760         if (is<WebVTTElement>(*child)) {
761             downcast<WebVTTElement>(*child).setIsPastNode(isPastNode);
762             // Make an elemenet id match a cue id for style matching purposes.
763             if (!id().isEmpty())
764                 downcast<WebVTTElement>(*child).setIdAttribute(id());
765         }
766     }
767 }
768
769 void VTTCue::updateDisplayTree(const MediaTime& movieTime)
770 {
771     // The display tree may contain WebVTT timestamp objects representing
772     // timestamps (processing instructions), along with displayable nodes.
773
774     if (!track()->isRendered())
775         return;
776
777     // Clear the contents of the set.
778     m_cueHighlightBox->removeChildren();
779
780     // Update the two sets containing past and future WebVTT objects.
781     RefPtr<DocumentFragment> referenceTree = createCueRenderingTree();
782     if (!referenceTree)
783         return;
784
785     markFutureAndPastNodes(referenceTree.get(), startMediaTime(), movieTime);
786     m_cueHighlightBox->appendChild(*referenceTree);
787 }
788
789 VTTCueBox* VTTCue::getDisplayTree(const IntSize& videoSize, int fontSize)
790 {
791     RefPtr<VTTCueBox> displayTree = displayTreeInternal();
792     if (!m_displayTreeShouldChange || !track()->isRendered())
793         return displayTree.get();
794
795     // 10.1 - 10.10
796     calculateDisplayParameters();
797
798     // 10.11. Apply the terms of the CSS specifications to nodes within the
799     // following constraints, thus obtaining a set of CSS boxes positioned
800     // relative to an initial containing block:
801     displayTree->removeChildren();
802
803     // The document tree is the tree of WebVTT Node Objects rooted at nodes.
804
805     // The children of the nodes must be wrapped in an anonymous box whose
806     // 'display' property has the value 'inline'. This is the WebVTT cue
807     // background box.
808
809     // Note: This is contained by default in m_cueHighlightBox.
810     m_cueHighlightBox->setPseudo(cueShadowPseudoId());
811
812     m_cueBackdropBox->setPseudo(cueBackdropShadowPseudoId());
813     m_cueBackdropBox->appendChild(*m_cueHighlightBox, ASSERT_NO_EXCEPTION);
814     displayTree->appendChild(*m_cueBackdropBox, ASSERT_NO_EXCEPTION);
815
816     // FIXME(BUG 79916): Runs of children of WebVTT Ruby Objects that are not
817     // WebVTT Ruby Text Objects must be wrapped in anonymous boxes whose
818     // 'display' property has the value 'ruby-base'.
819
820     displayTree->setFontSizeFromCaptionUserPrefs(fontSize);
821     displayTree->applyCSSProperties(videoSize);
822
823     m_displayTreeShouldChange = false;
824
825     // 10.15. Let cue's text track cue display state have the CSS boxes in
826     // boxes.
827     return displayTree.get();
828 }
829
830 void VTTCue::removeDisplayTree()
831 {
832     // The region needs to be informed about the cue removal.
833     if (m_notifyRegion && track()) {
834         if (VTTRegionList* regions = track()->regions()) {
835             if (VTTRegion* region = regions->getRegionById(m_regionId))
836                 region->willRemoveTextTrackCueBox(m_displayTree.get());
837         }
838     }
839
840     if (!hasDisplayTree())
841         return;
842     displayTreeInternal()->remove(ASSERT_NO_EXCEPTION);
843 }
844
845 std::pair<double, double> VTTCue::getPositionCoordinates() const
846 {
847     // This method is used for setting x and y when snap to lines is not set.
848     std::pair<double, double> coordinates;
849
850     if (m_writingDirection == Horizontal && m_displayDirection == CSSValueLtr) {
851         coordinates.first = m_textPosition;
852         coordinates.second = m_computedLinePosition;
853
854         return coordinates;
855     }
856
857     if (m_writingDirection == Horizontal && m_displayDirection == CSSValueRtl) {
858         coordinates.first = 100 - m_textPosition;
859         coordinates.second = m_computedLinePosition;
860
861         return coordinates;
862     }
863
864     if (m_writingDirection == VerticalGrowingLeft) {
865         coordinates.first = 100 - m_computedLinePosition;
866         coordinates.second = m_textPosition;
867
868         return coordinates;
869     }
870
871     if (m_writingDirection == VerticalGrowingRight) {
872         coordinates.first = m_computedLinePosition;
873         coordinates.second = m_textPosition;
874
875         return coordinates;
876     }
877
878     ASSERT_NOT_REACHED();
879
880     return coordinates;
881 }
882
883 VTTCue::CueSetting VTTCue::settingName(VTTScanner& input)
884 {
885     CueSetting parsedSetting = None;
886     if (input.scan("vertical"))
887         parsedSetting = Vertical;
888     else if (input.scan("line"))
889         parsedSetting = Line;
890     else if (input.scan("position"))
891         parsedSetting = Position;
892     else if (input.scan("size"))
893         parsedSetting = Size;
894     else if (input.scan("align"))
895         parsedSetting = Align;
896     else if (input.scan("region"))
897         parsedSetting = RegionId;
898
899     // Verify that a ':' follows.
900     if (parsedSetting != None && input.scan(':'))
901         return parsedSetting;
902
903     return None;
904 }
905
906 void VTTCue::setCueSettings(const String& inputString)
907 {
908     if (inputString.isEmpty())
909         return;
910
911     VTTScanner input(inputString);
912
913     while (!input.isAtEnd()) {
914
915         // The WebVTT cue settings part of a WebVTT cue consists of zero or more of the following components, in any order, 
916         // separated from each other by one or more U+0020 SPACE characters or U+0009 CHARACTER TABULATION (tab) characters.
917         input.skipWhile<WebVTTParser::isValidSettingDelimiter>();
918         if (input.isAtEnd())
919             break;
920
921         // When the user agent is to parse the WebVTT settings given by a string input for a text track cue cue, 
922         // the user agent must run the following steps:
923         // 1. Let settings be the result of splitting input on spaces.
924         // 2. For each token setting in the list settings, run the following substeps:
925         //    1. If setting does not contain a U+003A COLON character (:), or if the first U+003A COLON character (:) 
926         //       in setting is either the first or last character of setting, then jump to the step labeled next setting.
927         //    2. Let name be the leading substring of setting up to and excluding the first U+003A COLON character (:) in that string.
928         CueSetting name = settingName(input);
929
930         // 3. Let value be the trailing substring of setting starting from the character immediately after the first U+003A COLON character (:) in that string.
931         VTTScanner::Run valueRun = input.collectUntil<WebVTTParser::isValidSettingDelimiter>();
932
933         // 4. Run the appropriate substeps that apply for the value of name, as follows:
934         switch (name) {
935         case Vertical: {
936             // If name is a case-sensitive match for "vertical"
937             // 1. If value is a case-sensitive match for the string "rl", then let cue's text track cue writing direction 
938             //    be vertical growing left.
939             if (input.scanRun(valueRun, verticalGrowingLeftKeyword()))
940                 m_writingDirection = VerticalGrowingLeft;
941             
942             // 2. Otherwise, if value is a case-sensitive match for the string "lr", then let cue's text track cue writing 
943             //    direction be vertical growing right.
944             else if (input.scanRun(valueRun, verticalGrowingRightKeyword()))
945                 m_writingDirection = VerticalGrowingRight;
946
947             else
948                 LOG(Media, "VTTCue::setCueSettings, invalid Vertical");
949             break;
950         }
951         case Line: {
952             bool isValid = false;
953             do {
954                 // 1-2 - Collect chars that are either '-', '%', or a digit.
955                 // 1. If value contains any characters other than U+002D HYPHEN-MINUS characters (-), U+0025 PERCENT SIGN
956                 //    characters (%), and characters in the range U+0030 DIGIT ZERO (0) to U+0039 DIGIT NINE (9), then jump
957                 //    to the step labeled next setting.
958                 float linePosition;
959                 bool isNegative;
960                 if (!input.scanFloat(linePosition, &isNegative))
961                     break;
962
963                 bool isPercentage = input.scan('%');
964                 if (!input.isAt(valueRun.end()))
965                     break;
966
967                 // 2. If value does not contain at least one character in the range U+0030 DIGIT ZERO (0) to U+0039 DIGIT
968                 //    NINE (9), then jump to the step labeled next setting.
969                 // 3. If any character in value other than the first character is a U+002D HYPHEN-MINUS character (-), then
970                 //    jump to the step labeled next setting.
971                 // 4. If any character in value other than the last character is a U+0025 PERCENT SIGN character (%), then
972                 //    jump to the step labeled next setting.
973                 // 5. If the first character in value is a U+002D HYPHEN-MINUS character (-) and the last character in value is a
974                 //    U+0025 PERCENT SIGN character (%), then jump to the step labeled next setting.
975                 if (isPercentage && isNegative)
976                     break;
977
978                 // 6. Ignoring the trailing percent sign, if any, interpret value as a (potentially signed) integer, and
979                 //    let number be that number.
980                 // 7. If the last character in value is a U+0025 PERCENT SIGN character (%), but number is not in the range
981                 //    0 ≤ number ≤ 100, then jump to the step labeled next setting.
982                 // 8. Let cue's text track cue line position be number.
983                 // 9. If the last character in value is a U+0025 PERCENT SIGN character (%), then let cue's text track cue
984                 //    snap-to-lines flag be false. Otherwise, let it be true.
985                 if (isPercentage) {
986                     if (linePosition < 0 || linePosition > 100)
987                         break;
988
989                     // 10 - If '%' then set snap-to-lines flag to false.
990                     m_snapToLines = false;
991                 } else {
992                     if (linePosition - static_cast<int>(linePosition))
993                         break;
994
995                     m_snapToLines = true;
996                 }
997                 
998                 m_linePosition = linePosition;
999                 isValid = true;
1000             } while (0);
1001
1002             if (!isValid)
1003                 LOG(Media, "VTTCue::setCueSettings, invalid Line");
1004
1005             break;
1006         }
1007         case Position: {
1008             float position;
1009             if (WebVTTParser::parseFloatPercentageValue(input, position) && input.isAt(valueRun.end()))
1010                 m_textPosition = position;
1011             else
1012                 LOG(Media, "VTTCue::setCueSettings, invalid Position");
1013             break;
1014         }
1015         case Size: {
1016             float cueSize;
1017             if (WebVTTParser::parseFloatPercentageValue(input, cueSize) && input.isAt(valueRun.end()))
1018                 m_cueSize = cueSize;
1019             else
1020                 LOG(Media, "VTTCue::setCueSettings, invalid Size");
1021             break;
1022         }
1023         case Align: {
1024             // 1. If value is a case-sensitive match for the string "start", then let cue's text track cue alignment be start alignment.
1025             if (input.scanRun(valueRun, startKeyword()))
1026                 m_cueAlignment = Start;
1027
1028             // 2. If value is a case-sensitive match for the string "middle", then let cue's text track cue alignment be middle alignment.
1029             else if (input.scanRun(valueRun, middleKeyword()))
1030                 m_cueAlignment = Middle;
1031
1032             // 3. If value is a case-sensitive match for the string "end", then let cue's text track cue alignment be end alignment.
1033             else if (input.scanRun(valueRun, endKeyword()))
1034                 m_cueAlignment = End;
1035
1036             // 4. If value is a case-sensitive match for the string "left", then let cue's text track cue alignment be left alignment.
1037             else if (input.scanRun(valueRun, leftKeyword()))
1038                 m_cueAlignment = Left;
1039
1040             // 5. If value is a case-sensitive match for the string "right", then let cue's text track cue alignment be right alignment.
1041             else if (input.scanRun(valueRun, rightKeyword()))
1042                 m_cueAlignment = Right;
1043
1044             else
1045                 LOG(Media, "VTTCue::setCueSettings, invalid Align");
1046
1047             break;
1048         }
1049         case RegionId:
1050             m_regionId = input.extractString(valueRun);
1051             break;
1052         case None:
1053             break;
1054         }
1055
1056         // Make sure the entire run is consumed.
1057         input.skipRun(valueRun);
1058     }
1059
1060     // If cue's line position is not auto or cue's size is not 100 or cue's
1061     // writing direction is not horizontal, but cue's region identifier is not
1062     // the empty string, let cue's region identifier be the empty string.
1063     if (m_regionId.isEmpty())
1064         return;
1065
1066     if (m_linePosition != undefinedPosition || m_cueSize != 100 || m_writingDirection != Horizontal)
1067         m_regionId = emptyString();
1068 }
1069
1070 CSSValueID VTTCue::getCSSAlignment() const
1071 {
1072     return displayAlignmentMap[m_cueAlignment];
1073 }
1074
1075 CSSValueID VTTCue::getCSSWritingDirection() const
1076 {
1077     return m_displayDirection;
1078 }
1079
1080 CSSValueID VTTCue::getCSSWritingMode() const
1081 {
1082     return displayWritingModeMap[m_writingDirection];
1083 }
1084
1085 int VTTCue::getCSSSize() const
1086 {
1087     return m_displaySize;
1088 }
1089
1090 std::pair<double, double> VTTCue::getCSSPosition() const
1091 {
1092     if (!m_snapToLines)
1093         return getPositionCoordinates();
1094
1095     return m_displayPosition;
1096 }
1097
1098 bool VTTCue::cueContentsMatch(const TextTrackCue& cue) const
1099 {
1100     const VTTCue* vttCue = toVTTCue(&cue);
1101     if (text() != vttCue->text())
1102         return false;
1103     if (cueSettings() != vttCue->cueSettings())
1104         return false;
1105     if (position() != vttCue->position())
1106         return false;
1107     if (line() != vttCue->line())
1108         return false;
1109     if (size() != vttCue->size())
1110         return false;
1111     if (align() != vttCue->align())
1112         return false;
1113     
1114     return true;
1115 }
1116
1117 bool VTTCue::isEqual(const TextTrackCue& cue, TextTrackCue::CueMatchRules match) const
1118 {
1119     if (!TextTrackCue::isEqual(cue, match))
1120         return false;
1121
1122     if (cue.cueType() != WebVTT)
1123         return false;
1124
1125     return cueContentsMatch(cue);
1126 }
1127
1128 bool VTTCue::doesExtendCue(const TextTrackCue& cue) const
1129 {
1130     if (!cueContentsMatch(cue))
1131         return false;
1132     
1133     return TextTrackCue::doesExtendCue(cue);
1134 }
1135     
1136 void VTTCue::setFontSize(int fontSize, const IntSize&, bool important)
1137 {
1138     if (!hasDisplayTree() || !fontSize)
1139         return;
1140     
1141     LOG(Media, "TextTrackCue::setFontSize - setting cue font size to %i", fontSize);
1142
1143     m_displayTreeShouldChange = true;
1144     displayTreeInternal()->setInlineStyleProperty(CSSPropertyFontSize, fontSize, CSSPrimitiveValue::CSS_PX, important);
1145 }
1146
1147 VTTCue* toVTTCue(TextTrackCue* cue)
1148 {
1149     return const_cast<VTTCue*>(toVTTCue(const_cast<const TextTrackCue*>(cue)));
1150 }
1151
1152 const VTTCue* toVTTCue(const TextTrackCue* cue)
1153 {
1154     ASSERT_WITH_SECURITY_IMPLICATION(cue->isRenderable());
1155     return static_cast<const VTTCue*>(cue);
1156 }
1157
1158 } // namespace WebCore
1159
1160 #endif