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