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