SMILParserUtils.cpp (18332B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "SMILParserUtils.h" 8 9 #include "mozilla/SMILAttr.h" 10 #include "mozilla/SMILKeySpline.h" 11 #include "mozilla/SMILRepeatCount.h" 12 #include "mozilla/SMILTimeValueSpecParams.h" 13 #include "mozilla/SMILTypes.h" 14 #include "mozilla/SMILValue.h" 15 #include "mozilla/SVGContentUtils.h" 16 #include "mozilla/TextUtils.h" 17 #include "nsCharSeparatedTokenizer.h" 18 #include "nsContentUtils.h" 19 20 using namespace mozilla::dom; 21 //------------------------------------------------------------------------------ 22 // Helper functions and Constants 23 24 namespace { 25 26 using namespace mozilla; 27 28 const uint32_t MSEC_PER_SEC = 1000; 29 const uint32_t MSEC_PER_MIN = 1000 * 60; 30 const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60; 31 32 #define ACCESSKEY_PREFIX_LC u"accesskey("_ns // SMIL2+ 33 #define ACCESSKEY_PREFIX_CC u"accessKey("_ns // SVG/SMIL ANIM 34 #define REPEAT_PREFIX u"repeat("_ns 35 #define WALLCLOCK_PREFIX u"wallclock("_ns 36 37 inline bool SkipWhitespace(nsAString::const_iterator& aIter, 38 const nsAString::const_iterator& aEnd) { 39 while (aIter != aEnd) { 40 if (!nsContentUtils::IsHTMLWhitespace(*aIter)) { 41 return true; 42 } 43 ++aIter; 44 } 45 return false; 46 } 47 48 inline bool ParseColon(nsAString::const_iterator& aIter, 49 const nsAString::const_iterator& aEnd) { 50 if (aIter == aEnd || *aIter != ':') { 51 return false; 52 } 53 ++aIter; 54 return true; 55 } 56 57 /* 58 * Exactly two digits in the range 00 - 59 are expected. 59 */ 60 bool ParseSecondsOrMinutes(nsAString::const_iterator& aIter, 61 const nsAString::const_iterator& aEnd, 62 uint32_t& aValue) { 63 if (aIter == aEnd || !mozilla::IsAsciiDigit(*aIter)) { 64 return false; 65 } 66 67 nsAString::const_iterator iter(aIter); 68 69 if (++iter == aEnd || !mozilla::IsAsciiDigit(*iter)) { 70 return false; 71 } 72 73 uint32_t value = 10 * mozilla::AsciiAlphanumericToNumber(*aIter) + 74 mozilla::AsciiAlphanumericToNumber(*iter); 75 if (value > 59) { 76 return false; 77 } 78 if (++iter != aEnd && mozilla::IsAsciiDigit(*iter)) { 79 return false; 80 } 81 82 aValue = value; 83 aIter = iter; 84 return true; 85 } 86 87 inline bool ParseClockMetric(nsAString::const_iterator& aIter, 88 const nsAString::const_iterator& aEnd, 89 uint32_t& aMultiplier) { 90 if (aIter == aEnd) { 91 aMultiplier = MSEC_PER_SEC; 92 return true; 93 } 94 95 switch (*aIter) { 96 case 'h': 97 if (++aIter == aEnd) { 98 aMultiplier = MSEC_PER_HOUR; 99 return true; 100 } 101 return false; 102 case 'm': { 103 const nsAString& metric = Substring(aIter, aEnd); 104 if (metric.EqualsLiteral("min")) { 105 aMultiplier = MSEC_PER_MIN; 106 aIter = aEnd; 107 return true; 108 } 109 if (metric.EqualsLiteral("ms")) { 110 aMultiplier = 1; 111 aIter = aEnd; 112 return true; 113 } 114 } 115 return false; 116 case 's': 117 if (++aIter == aEnd) { 118 aMultiplier = MSEC_PER_SEC; 119 return true; 120 } 121 } 122 return false; 123 } 124 125 /** 126 * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax 127 */ 128 bool ParseClockValue(nsAString::const_iterator& aIter, 129 const nsAString::const_iterator& aEnd, 130 SMILTimeValue::Rounding aRounding, 131 SMILTimeValue* aResult) { 132 if (aIter == aEnd) { 133 return false; 134 } 135 136 // TIMECOUNT_VALUE ::= Timecount ("." Fraction)? (Metric)? 137 // PARTIAL_CLOCK_VALUE ::= Minutes ":" Seconds ("." Fraction)? 138 // FULL_CLOCK_VALUE ::= Hours ":" Minutes ":" Seconds ("." Fraction)? 139 enum ClockType { TIMECOUNT_VALUE, PARTIAL_CLOCK_VALUE, FULL_CLOCK_VALUE }; 140 141 int32_t clockType = TIMECOUNT_VALUE; 142 143 nsAString::const_iterator iter(aIter); 144 145 // Determine which type of clock value we have by counting the number 146 // of colons in the string. 147 do { 148 switch (*iter) { 149 case ':': 150 if (clockType == FULL_CLOCK_VALUE) { 151 return false; 152 } 153 ++clockType; 154 break; 155 case 'e': 156 case 'E': 157 case '-': 158 case '+': 159 // Exclude anything invalid (for clock values) 160 // that number parsing might otherwise allow. 161 return false; 162 } 163 ++iter; 164 } while (iter != aEnd); 165 166 iter = aIter; 167 168 int32_t hours = 0, timecount = 0; 169 double fraction = 0.0; 170 uint32_t minutes, seconds, multiplier; 171 172 switch (clockType) { 173 case FULL_CLOCK_VALUE: 174 if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) || 175 !ParseColon(iter, aEnd)) { 176 return false; 177 } 178 [[fallthrough]]; 179 case PARTIAL_CLOCK_VALUE: 180 if (!ParseSecondsOrMinutes(iter, aEnd, minutes) || 181 !ParseColon(iter, aEnd) || 182 !ParseSecondsOrMinutes(iter, aEnd, seconds)) { 183 return false; 184 } 185 if (iter != aEnd && (*iter != '.' || !SVGContentUtils::ParseNumber( 186 iter, aEnd, fraction))) { 187 return false; 188 } 189 aResult->SetMillis(SMILTime(hours) * MSEC_PER_HOUR + 190 minutes * MSEC_PER_MIN + 191 (seconds + fraction) * MSEC_PER_SEC, 192 aRounding); 193 aIter = iter; 194 return true; 195 case TIMECOUNT_VALUE: 196 if (*iter != '.' && 197 !SVGContentUtils::ParseInteger(iter, aEnd, timecount)) { 198 return false; 199 } 200 if (iter != aEnd && *iter == '.' && 201 !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) { 202 return false; 203 } 204 if (!ParseClockMetric(iter, aEnd, multiplier)) { 205 return false; 206 } 207 aResult->SetMillis((timecount + fraction) * multiplier, aRounding); 208 aIter = iter; 209 return true; 210 } 211 212 return false; 213 } 214 215 bool ParseOffsetValue(nsAString::const_iterator& aIter, 216 const nsAString::const_iterator& aEnd, 217 SMILTimeValue* aResult) { 218 nsAString::const_iterator iter(aIter); 219 220 int32_t sign; 221 if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) || 222 !SkipWhitespace(iter, aEnd) || 223 !ParseClockValue(iter, aEnd, SMILTimeValue::Rounding::Nearest, aResult)) { 224 return false; 225 } 226 if (sign == -1) { 227 aResult->SetMillis(-aResult->GetMillis()); 228 } 229 aIter = iter; 230 return true; 231 } 232 233 bool ParseOffsetValue(const nsAString& aSpec, SMILTimeValue* aResult) { 234 nsAString::const_iterator iter, end; 235 aSpec.BeginReading(iter); 236 aSpec.EndReading(end); 237 238 return ParseOffsetValue(iter, end, aResult) && iter == end; 239 } 240 241 bool ParseOptionalOffset(nsAString::const_iterator& aIter, 242 const nsAString::const_iterator& aEnd, 243 SMILTimeValue* aResult) { 244 if (aIter == aEnd) { 245 *aResult = SMILTimeValue::Zero(); 246 return true; 247 } 248 249 return SkipWhitespace(aIter, aEnd) && ParseOffsetValue(aIter, aEnd, aResult); 250 } 251 252 void MoveToNextToken(nsAString::const_iterator& aIter, 253 const nsAString::const_iterator& aEnd, bool aBreakOnDot, 254 bool& aIsAnyCharEscaped) { 255 aIsAnyCharEscaped = false; 256 257 bool isCurrentCharEscaped = false; 258 259 while (aIter != aEnd && !nsContentUtils::IsHTMLWhitespace(*aIter)) { 260 if (isCurrentCharEscaped) { 261 isCurrentCharEscaped = false; 262 } else { 263 if (*aIter == '+' || *aIter == '-' || (aBreakOnDot && *aIter == '.')) { 264 break; 265 } 266 if (*aIter == '\\') { 267 isCurrentCharEscaped = true; 268 aIsAnyCharEscaped = true; 269 } 270 } 271 ++aIter; 272 } 273 } 274 275 already_AddRefed<nsAtom> ConvertUnescapedTokenToAtom(const nsAString& aToken) { 276 // Whether the token is an id-ref or event-symbol it should be a valid NCName 277 if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false))) 278 return nullptr; 279 return NS_Atomize(aToken); 280 } 281 282 already_AddRefed<nsAtom> ConvertTokenToAtom(const nsAString& aToken, 283 bool aUnescapeToken) { 284 // Unescaping involves making a copy of the string which we'd like to avoid if 285 // possible 286 if (!aUnescapeToken) { 287 return ConvertUnescapedTokenToAtom(aToken); 288 } 289 290 nsAutoString token(aToken); 291 292 const char16_t* read = token.BeginReading(); 293 const char16_t* const end = token.EndReading(); 294 char16_t* write = token.BeginWriting(); 295 bool escape = false; 296 297 while (read != end) { 298 MOZ_ASSERT(write <= read, "Writing past where we've read"); 299 if (!escape && *read == '\\') { 300 escape = true; 301 ++read; 302 } else { 303 *write++ = *read++; 304 escape = false; 305 } 306 } 307 token.Truncate(write - token.BeginReading()); 308 309 return ConvertUnescapedTokenToAtom(token); 310 } 311 312 bool ParseElementBaseTimeValueSpec(const nsAString& aSpec, 313 SMILTimeValueSpecParams& aResult) { 314 SMILTimeValueSpecParams result; 315 316 // 317 // The spec will probably look something like one of these 318 // 319 // element-name.begin 320 // element-name.event-name 321 // event-name 322 // element-name.repeat(3) 323 // event\.name 324 // 325 // Technically `repeat(3)' is permitted but the behaviour in this case is not 326 // defined (for SMIL Animation) so we don't support it here. 327 // 328 329 nsAString::const_iterator start, end; 330 aSpec.BeginReading(start); 331 aSpec.EndReading(end); 332 333 if (start == end) { 334 return false; 335 } 336 337 nsAString::const_iterator tokenEnd(start); 338 339 bool requiresUnescaping; 340 MoveToNextToken(tokenEnd, end, true, requiresUnescaping); 341 342 RefPtr<nsAtom> atom = 343 ConvertTokenToAtom(Substring(start, tokenEnd), requiresUnescaping); 344 if (atom == nullptr) { 345 return false; 346 } 347 348 // Parse the second token if there is one 349 if (tokenEnd != end && *tokenEnd == '.') { 350 result.mDependentElemID = atom; 351 352 ++tokenEnd; 353 start = tokenEnd; 354 MoveToNextToken(tokenEnd, end, false, requiresUnescaping); 355 356 const nsAString& token2 = Substring(start, tokenEnd); 357 358 // element-name.begin 359 if (token2.EqualsLiteral("begin")) { 360 result.mType = SMILTimeValueSpecParams::SYNCBASE; 361 result.mSyncBegin = true; 362 // element-name.end 363 } else if (token2.EqualsLiteral("end")) { 364 result.mType = SMILTimeValueSpecParams::SYNCBASE; 365 result.mSyncBegin = false; 366 // element-name.repeat(digit+) 367 } else if (StringBeginsWith(token2, REPEAT_PREFIX)) { 368 start.advance(REPEAT_PREFIX.Length()); 369 int32_t repeatValue; 370 if (start == tokenEnd || *start == '+' || *start == '-' || 371 !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) { 372 return false; 373 } 374 if (start == tokenEnd || *start != ')') { 375 return false; 376 } 377 result.mType = SMILTimeValueSpecParams::REPEAT; 378 result.mRepeatIteration = repeatValue; 379 // element-name.event-symbol 380 } else { 381 atom = ConvertTokenToAtom(token2, requiresUnescaping); 382 if (atom == nullptr) { 383 return false; 384 } 385 result.mType = SMILTimeValueSpecParams::EVENT; 386 result.mEventSymbol = atom; 387 } 388 } else { 389 // event-symbol 390 result.mType = SMILTimeValueSpecParams::EVENT; 391 result.mEventSymbol = atom; 392 } 393 394 // We've reached the end of the token, so we should now be either looking at 395 // a '+', '-' (possibly with whitespace before it), or the end. 396 if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) { 397 return false; 398 } 399 aResult = result; 400 return true; 401 } 402 403 } // namespace 404 405 namespace mozilla { 406 407 //------------------------------------------------------------------------------ 408 // Implementation 409 410 const nsDependentSubstring SMILParserUtils::TrimWhitespace( 411 const nsAString& aString) { 412 nsAString::const_iterator start, end; 413 414 aString.BeginReading(start); 415 aString.EndReading(end); 416 417 // Skip whitespace characters at the beginning 418 while (start != end && nsContentUtils::IsHTMLWhitespace(*start)) { 419 ++start; 420 } 421 422 // Skip whitespace characters at the end. 423 while (end != start) { 424 --end; 425 426 if (!nsContentUtils::IsHTMLWhitespace(*end)) { 427 // Step back to the last non-whitespace character. 428 ++end; 429 430 break; 431 } 432 } 433 434 return Substring(start, end); 435 } 436 437 bool SMILParserUtils::ParseKeySplines( 438 const nsAString& aSpec, FallibleTArray<SMILKeySpline>& aKeySplines) { 439 for (const auto& controlPoint : 440 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace>(aSpec, 441 ';') 442 .ToRange()) { 443 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace, 444 nsTokenizerFlags::SeparatorOptional> 445 tokenizer(controlPoint, ','); 446 447 double values[4]; 448 for (auto& value : values) { 449 if (!tokenizer.hasMoreTokens() || 450 !SVGContentUtils::ParseNumber(tokenizer.nextToken(), value) || 451 value > 1.0 || value < 0.0) { 452 return false; 453 } 454 } 455 if (tokenizer.hasMoreTokens() || tokenizer.separatorAfterCurrentToken() || 456 !aKeySplines.AppendElement( 457 SMILKeySpline(values[0], values[1], values[2], values[3]), 458 fallible)) { 459 return false; 460 } 461 } 462 463 return !aKeySplines.IsEmpty(); 464 } 465 466 bool SMILParserUtils::ParseSemicolonDelimitedProgressList( 467 const nsAString& aSpec, bool aNonDecreasing, 468 FallibleTArray<double>& aArray) { 469 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tokenizer( 470 aSpec, ';'); 471 472 double previousValue = -1.0; 473 474 while (tokenizer.hasMoreTokens()) { 475 double value; 476 if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) { 477 return false; 478 } 479 480 if (value > 1.0 || value < 0.0 || 481 (aNonDecreasing && value < previousValue)) { 482 return false; 483 } 484 485 if (!aArray.AppendElement(value, fallible)) { 486 return false; 487 } 488 previousValue = value; 489 } 490 491 return !aArray.IsEmpty(); 492 } 493 494 // Helper class for ParseValues 495 class MOZ_STACK_CLASS SMILValueParser 496 : public SMILParserUtils::GenericValueParser { 497 public: 498 SMILValueParser(const SVGAnimationElement* aSrcElement, 499 const SMILAttr* aSMILAttr, 500 FallibleTArray<SMILValue>* aValuesArray, 501 bool* aPreventCachingOfSandwich) 502 : mSrcElement(aSrcElement), 503 mSMILAttr(aSMILAttr), 504 mValuesArray(aValuesArray), 505 mPreventCachingOfSandwich(aPreventCachingOfSandwich) {} 506 507 bool Parse(const nsAString& aValueStr) override { 508 SMILValue newValue; 509 if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue, 510 *mPreventCachingOfSandwich))) 511 return false; 512 513 if (!mValuesArray->AppendElement(newValue, fallible)) { 514 return false; 515 } 516 return true; 517 } 518 519 protected: 520 const SVGAnimationElement* mSrcElement; 521 const SMILAttr* mSMILAttr; 522 FallibleTArray<SMILValue>* mValuesArray; 523 bool* mPreventCachingOfSandwich; 524 }; 525 526 bool SMILParserUtils::ParseValues(const nsAString& aSpec, 527 const SVGAnimationElement* aSrcElement, 528 const SMILAttr& aAttribute, 529 FallibleTArray<SMILValue>& aValuesArray, 530 bool& aPreventCachingOfSandwich) { 531 // Assume all results can be cached, until we find one that can't. 532 aPreventCachingOfSandwich = false; 533 SMILValueParser valueParser(aSrcElement, &aAttribute, &aValuesArray, 534 &aPreventCachingOfSandwich); 535 return ParseValuesGeneric(aSpec, valueParser); 536 } 537 538 bool SMILParserUtils::ParseValuesGeneric(const nsAString& aSpec, 539 GenericValueParser& aParser) { 540 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tokenizer( 541 aSpec, ';'); 542 if (!tokenizer.hasMoreTokens()) { // Empty list 543 return false; 544 } 545 546 while (tokenizer.hasMoreTokens()) { 547 if (!aParser.Parse(tokenizer.nextToken())) { 548 return false; 549 } 550 } 551 552 return true; 553 } 554 555 bool SMILParserUtils::ParseRepeatCount(const nsAString& aSpec, 556 SMILRepeatCount& aResult) { 557 const nsAString& spec = SMILParserUtils::TrimWhitespace(aSpec); 558 559 if (spec.EqualsLiteral("indefinite")) { 560 aResult.SetIndefinite(); 561 return true; 562 } 563 564 double value; 565 if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) { 566 return false; 567 } 568 aResult = value; 569 return true; 570 } 571 572 bool SMILParserUtils::ParseTimeValueSpecParams( 573 const nsAString& aSpec, SMILTimeValueSpecParams& aResult) { 574 const nsAString& spec = TrimWhitespace(aSpec); 575 576 if (spec.EqualsLiteral("indefinite")) { 577 aResult.mType = SMILTimeValueSpecParams::INDEFINITE; 578 return true; 579 } 580 581 // offset type 582 if (ParseOffsetValue(spec, &aResult.mOffset)) { 583 aResult.mType = SMILTimeValueSpecParams::OFFSET; 584 return true; 585 } 586 587 // wallclock type 588 if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) { 589 return false; // Wallclock times not implemented 590 } 591 592 // accesskey type 593 if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) || 594 StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) { 595 return false; // accesskey is not supported 596 } 597 598 // event, syncbase, or repeat 599 return ParseElementBaseTimeValueSpec(spec, aResult); 600 } 601 602 bool SMILParserUtils::ParseClockValue(const nsAString& aSpec, 603 SMILTimeValue::Rounding aRounding, 604 SMILTimeValue* aResult) { 605 nsAString::const_iterator iter, end; 606 aSpec.BeginReading(iter); 607 aSpec.EndReading(end); 608 609 return ::ParseClockValue(iter, end, aRounding, aResult) && iter == end; 610 } 611 612 int32_t SMILParserUtils::CheckForNegativeNumber(const nsAString& aStr) { 613 int32_t absValLocation = -1; 614 615 nsAString::const_iterator start, iter, end; 616 aStr.BeginReading(start); 617 aStr.EndReading(end); 618 iter = start; 619 620 // Skip initial whitespace 621 while (iter != end && nsContentUtils::IsHTMLWhitespace(*iter)) { 622 ++iter; 623 } 624 625 // Check for dash 626 if (iter != end && *iter == '-') { 627 ++iter; 628 // Check for numeric character 629 if (iter != end && mozilla::IsAsciiDigit(*iter)) { 630 absValLocation = iter - start; 631 } 632 } 633 return absValLocation; 634 } 635 636 } // namespace mozilla