Bug 1776680 [wpt PR 34603] - [@container] Test invalidation of font-relative units...
[gecko.git] / dom / smil / SMILParserUtils.cpp
blob902b0ab1811bcfdd920f22ccf5a77b93b9e9a742
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/. */
7 #include "SMILParserUtils.h"
9 #include "mozilla/SMILAttr.h"
10 #include "mozilla/SMILKeySpline.h"
11 #include "mozilla/SMILRepeatCount.h"
12 #include "mozilla/SMILTimeValue.h"
13 #include "mozilla/SMILTimeValueSpecParams.h"
14 #include "mozilla/SMILTypes.h"
15 #include "mozilla/SMILValue.h"
16 #include "mozilla/SVGContentUtils.h"
17 #include "mozilla/TextUtils.h"
18 #include "nsContentUtils.h"
19 #include "nsCharSeparatedTokenizer.h"
21 using namespace mozilla::dom;
22 //------------------------------------------------------------------------------
23 // Helper functions and Constants
25 namespace {
27 using namespace mozilla;
29 const uint32_t MSEC_PER_SEC = 1000;
30 const uint32_t MSEC_PER_MIN = 1000 * 60;
31 const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60;
33 #define ACCESSKEY_PREFIX_LC u"accesskey("_ns // SMIL2+
34 #define ACCESSKEY_PREFIX_CC u"accessKey("_ns // SVG/SMIL ANIM
35 #define REPEAT_PREFIX u"repeat("_ns
36 #define WALLCLOCK_PREFIX u"wallclock("_ns
38 inline bool SkipWhitespace(RangedPtr<const char16_t>& aIter,
39 const RangedPtr<const char16_t>& aEnd) {
40 while (aIter != aEnd) {
41 if (!nsContentUtils::IsHTMLWhitespace(*aIter)) {
42 return true;
44 ++aIter;
46 return false;
49 inline bool ParseColon(RangedPtr<const char16_t>& aIter,
50 const RangedPtr<const char16_t>& aEnd) {
51 if (aIter == aEnd || *aIter != ':') {
52 return false;
54 ++aIter;
55 return true;
59 * Exactly two digits in the range 00 - 59 are expected.
61 bool ParseSecondsOrMinutes(RangedPtr<const char16_t>& aIter,
62 const RangedPtr<const char16_t>& aEnd,
63 uint32_t& aValue) {
64 if (aIter == aEnd || !mozilla::IsAsciiDigit(*aIter)) {
65 return false;
68 RangedPtr<const char16_t> iter(aIter);
70 if (++iter == aEnd || !mozilla::IsAsciiDigit(*iter)) {
71 return false;
74 uint32_t value = 10 * mozilla::AsciiAlphanumericToNumber(*aIter) +
75 mozilla::AsciiAlphanumericToNumber(*iter);
76 if (value > 59) {
77 return false;
79 if (++iter != aEnd && mozilla::IsAsciiDigit(*iter)) {
80 return false;
83 aValue = value;
84 aIter = iter;
85 return true;
88 inline bool ParseClockMetric(RangedPtr<const char16_t>& aIter,
89 const RangedPtr<const char16_t>& aEnd,
90 uint32_t& aMultiplier) {
91 if (aIter == aEnd) {
92 aMultiplier = MSEC_PER_SEC;
93 return true;
96 switch (*aIter) {
97 case 'h':
98 if (++aIter == aEnd) {
99 aMultiplier = MSEC_PER_HOUR;
100 return true;
102 return false;
103 case 'm': {
104 const nsAString& metric = Substring(aIter.get(), aEnd.get());
105 if (metric.EqualsLiteral("min")) {
106 aMultiplier = MSEC_PER_MIN;
107 aIter = aEnd;
108 return true;
110 if (metric.EqualsLiteral("ms")) {
111 aMultiplier = 1;
112 aIter = aEnd;
113 return true;
116 return false;
117 case 's':
118 if (++aIter == aEnd) {
119 aMultiplier = MSEC_PER_SEC;
120 return true;
123 return false;
127 * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax
129 bool ParseClockValue(RangedPtr<const char16_t>& aIter,
130 const RangedPtr<const char16_t>& aEnd,
131 SMILTimeValue* aResult) {
132 if (aIter == aEnd) {
133 return false;
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 };
141 int32_t clockType = TIMECOUNT_VALUE;
143 RangedPtr<const char16_t> iter(aIter);
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;
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;
163 ++iter;
164 } while (iter != aEnd);
166 iter = aIter;
168 int32_t hours = 0, timecount;
169 double fraction = 0.0;
170 uint32_t minutes, seconds, multiplier;
172 switch (clockType) {
173 case FULL_CLOCK_VALUE:
174 if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) ||
175 !ParseColon(iter, aEnd)) {
176 return false;
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;
185 if (iter != aEnd && (*iter != '.' || !SVGContentUtils::ParseNumber(
186 iter, aEnd, fraction))) {
187 return false;
189 aResult->SetMillis(SMILTime(hours) * MSEC_PER_HOUR +
190 minutes * MSEC_PER_MIN + seconds * MSEC_PER_SEC +
191 NS_round(fraction * MSEC_PER_SEC));
192 aIter = iter;
193 return true;
194 case TIMECOUNT_VALUE:
195 if (!SVGContentUtils::ParseInteger(iter, aEnd, timecount)) {
196 return false;
198 if (iter != aEnd && *iter == '.' &&
199 !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) {
200 return false;
202 if (!ParseClockMetric(iter, aEnd, multiplier)) {
203 return false;
205 aResult->SetMillis(SMILTime(timecount) * multiplier +
206 NS_round(fraction * multiplier));
207 aIter = iter;
208 return true;
211 return false;
214 bool ParseOffsetValue(RangedPtr<const char16_t>& aIter,
215 const RangedPtr<const char16_t>& aEnd,
216 SMILTimeValue* aResult) {
217 RangedPtr<const char16_t> iter(aIter);
219 int32_t sign;
220 if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) ||
221 !SkipWhitespace(iter, aEnd) || !ParseClockValue(iter, aEnd, aResult)) {
222 return false;
224 if (sign == -1) {
225 aResult->SetMillis(-aResult->GetMillis());
227 aIter = iter;
228 return true;
231 bool ParseOffsetValue(const nsAString& aSpec, SMILTimeValue* aResult) {
232 RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
233 const RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
235 return ParseOffsetValue(iter, end, aResult) && iter == end;
238 bool ParseOptionalOffset(RangedPtr<const char16_t>& aIter,
239 const RangedPtr<const char16_t>& aEnd,
240 SMILTimeValue* aResult) {
241 if (aIter == aEnd) {
242 aResult->SetMillis(0L);
243 return true;
246 return SkipWhitespace(aIter, aEnd) && ParseOffsetValue(aIter, aEnd, aResult);
249 void MoveToNextToken(RangedPtr<const char16_t>& aIter,
250 const RangedPtr<const char16_t>& aEnd, bool aBreakOnDot,
251 bool& aIsAnyCharEscaped) {
252 aIsAnyCharEscaped = false;
254 bool isCurrentCharEscaped = false;
256 while (aIter != aEnd && !nsContentUtils::IsHTMLWhitespace(*aIter)) {
257 if (isCurrentCharEscaped) {
258 isCurrentCharEscaped = false;
259 } else {
260 if (*aIter == '+' || *aIter == '-' || (aBreakOnDot && *aIter == '.')) {
261 break;
263 if (*aIter == '\\') {
264 isCurrentCharEscaped = true;
265 aIsAnyCharEscaped = true;
268 ++aIter;
272 already_AddRefed<nsAtom> ConvertUnescapedTokenToAtom(const nsAString& aToken) {
273 // Whether the token is an id-ref or event-symbol it should be a valid NCName
274 if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false)))
275 return nullptr;
276 return NS_Atomize(aToken);
279 already_AddRefed<nsAtom> ConvertTokenToAtom(const nsAString& aToken,
280 bool aUnescapeToken) {
281 // Unescaping involves making a copy of the string which we'd like to avoid if
282 // possible
283 if (!aUnescapeToken) {
284 return ConvertUnescapedTokenToAtom(aToken);
287 nsAutoString token(aToken);
289 const char16_t* read = token.BeginReading();
290 const char16_t* const end = token.EndReading();
291 char16_t* write = token.BeginWriting();
292 bool escape = false;
294 while (read != end) {
295 MOZ_ASSERT(write <= read, "Writing past where we've read");
296 if (!escape && *read == '\\') {
297 escape = true;
298 ++read;
299 } else {
300 *write++ = *read++;
301 escape = false;
304 token.Truncate(write - token.BeginReading());
306 return ConvertUnescapedTokenToAtom(token);
309 bool ParseElementBaseTimeValueSpec(const nsAString& aSpec,
310 SMILTimeValueSpecParams& aResult) {
311 SMILTimeValueSpecParams result;
314 // The spec will probably look something like one of these
316 // element-name.begin
317 // element-name.event-name
318 // event-name
319 // element-name.repeat(3)
320 // event\.name
322 // Technically `repeat(3)' is permitted but the behaviour in this case is not
323 // defined (for SMIL Animation) so we don't support it here.
326 RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aSpec));
327 RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
329 if (start == end) {
330 return false;
333 RangedPtr<const char16_t> tokenEnd(start);
335 bool requiresUnescaping;
336 MoveToNextToken(tokenEnd, end, true, requiresUnescaping);
338 RefPtr<nsAtom> atom = ConvertTokenToAtom(
339 Substring(start.get(), tokenEnd.get()), requiresUnescaping);
340 if (atom == nullptr) {
341 return false;
344 // Parse the second token if there is one
345 if (tokenEnd != end && *tokenEnd == '.') {
346 result.mDependentElemID = atom;
348 ++tokenEnd;
349 start = tokenEnd;
350 MoveToNextToken(tokenEnd, end, false, requiresUnescaping);
352 const nsAString& token2 = Substring(start.get(), tokenEnd.get());
354 // element-name.begin
355 if (token2.EqualsLiteral("begin")) {
356 result.mType = SMILTimeValueSpecParams::SYNCBASE;
357 result.mSyncBegin = true;
358 // element-name.end
359 } else if (token2.EqualsLiteral("end")) {
360 result.mType = SMILTimeValueSpecParams::SYNCBASE;
361 result.mSyncBegin = false;
362 // element-name.repeat(digit+)
363 } else if (StringBeginsWith(token2, REPEAT_PREFIX)) {
364 start += REPEAT_PREFIX.Length();
365 int32_t repeatValue;
366 if (start == tokenEnd || *start == '+' || *start == '-' ||
367 !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) {
368 return false;
370 if (start == tokenEnd || *start != ')') {
371 return false;
373 result.mType = SMILTimeValueSpecParams::REPEAT;
374 result.mRepeatIteration = repeatValue;
375 // element-name.event-symbol
376 } else {
377 atom = ConvertTokenToAtom(token2, requiresUnescaping);
378 if (atom == nullptr) {
379 return false;
381 result.mType = SMILTimeValueSpecParams::EVENT;
382 result.mEventSymbol = atom;
384 } else {
385 // event-symbol
386 result.mType = SMILTimeValueSpecParams::EVENT;
387 result.mEventSymbol = atom;
390 // We've reached the end of the token, so we should now be either looking at
391 // a '+', '-' (possibly with whitespace before it), or the end.
392 if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) {
393 return false;
395 aResult = result;
396 return true;
399 } // namespace
401 namespace mozilla {
403 //------------------------------------------------------------------------------
404 // Implementation
406 const nsDependentSubstring SMILParserUtils::TrimWhitespace(
407 const nsAString& aString) {
408 nsAString::const_iterator start, end;
410 aString.BeginReading(start);
411 aString.EndReading(end);
413 // Skip whitespace characters at the beginning
414 while (start != end && nsContentUtils::IsHTMLWhitespace(*start)) {
415 ++start;
418 // Skip whitespace characters at the end.
419 while (end != start) {
420 --end;
422 if (!nsContentUtils::IsHTMLWhitespace(*end)) {
423 // Step back to the last non-whitespace character.
424 ++end;
426 break;
430 return Substring(start, end);
433 bool SMILParserUtils::ParseKeySplines(
434 const nsAString& aSpec, FallibleTArray<SMILKeySpline>& aKeySplines) {
435 for (const auto& controlPoint :
436 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace>(aSpec,
437 ';')
438 .ToRange()) {
439 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace,
440 nsTokenizerFlags::SeparatorOptional>
441 tokenizer(controlPoint, ',');
443 double values[4];
444 for (auto& value : values) {
445 if (!tokenizer.hasMoreTokens() ||
446 !SVGContentUtils::ParseNumber(tokenizer.nextToken(), value) ||
447 value > 1.0 || value < 0.0) {
448 return false;
451 if (tokenizer.hasMoreTokens() || tokenizer.separatorAfterCurrentToken() ||
452 !aKeySplines.AppendElement(
453 SMILKeySpline(values[0], values[1], values[2], values[3]),
454 fallible)) {
455 return false;
459 return !aKeySplines.IsEmpty();
462 bool SMILParserUtils::ParseSemicolonDelimitedProgressList(
463 const nsAString& aSpec, bool aNonDecreasing,
464 FallibleTArray<double>& aArray) {
465 nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tokenizer(
466 aSpec, ';');
468 double previousValue = -1.0;
470 while (tokenizer.hasMoreTokens()) {
471 double value;
472 if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) {
473 return false;
476 if (value > 1.0 || value < 0.0 ||
477 (aNonDecreasing && value < previousValue)) {
478 return false;
481 if (!aArray.AppendElement(value, fallible)) {
482 return false;
484 previousValue = value;
487 return !aArray.IsEmpty();
490 // Helper class for ParseValues
491 class MOZ_STACK_CLASS SMILValueParser
492 : public SMILParserUtils::GenericValueParser {
493 public:
494 SMILValueParser(const SVGAnimationElement* aSrcElement,
495 const SMILAttr* aSMILAttr,
496 FallibleTArray<SMILValue>* aValuesArray,
497 bool* aPreventCachingOfSandwich)
498 : mSrcElement(aSrcElement),
499 mSMILAttr(aSMILAttr),
500 mValuesArray(aValuesArray),
501 mPreventCachingOfSandwich(aPreventCachingOfSandwich) {}
503 virtual bool Parse(const nsAString& aValueStr) override {
504 SMILValue newValue;
505 bool tmpPreventCachingOfSandwich = false;
506 if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue,
507 tmpPreventCachingOfSandwich)))
508 return false;
510 if (!mValuesArray->AppendElement(newValue, fallible)) {
511 return false;
513 if (tmpPreventCachingOfSandwich) {
514 *mPreventCachingOfSandwich = true;
516 return true;
519 protected:
520 const SVGAnimationElement* mSrcElement;
521 const SMILAttr* mSMILAttr;
522 FallibleTArray<SMILValue>* mValuesArray;
523 bool* mPreventCachingOfSandwich;
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);
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;
546 while (tokenizer.hasMoreTokens()) {
547 if (!aParser.Parse(tokenizer.nextToken())) {
548 return false;
552 return true;
555 bool SMILParserUtils::ParseRepeatCount(const nsAString& aSpec,
556 SMILRepeatCount& aResult) {
557 const nsAString& spec = SMILParserUtils::TrimWhitespace(aSpec);
559 if (spec.EqualsLiteral("indefinite")) {
560 aResult.SetIndefinite();
561 return true;
564 double value;
565 if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) {
566 return false;
568 aResult = value;
569 return true;
572 bool SMILParserUtils::ParseTimeValueSpecParams(
573 const nsAString& aSpec, SMILTimeValueSpecParams& aResult) {
574 const nsAString& spec = TrimWhitespace(aSpec);
576 if (spec.EqualsLiteral("indefinite")) {
577 aResult.mType = SMILTimeValueSpecParams::INDEFINITE;
578 return true;
581 // offset type
582 if (ParseOffsetValue(spec, &aResult.mOffset)) {
583 aResult.mType = SMILTimeValueSpecParams::OFFSET;
584 return true;
587 // wallclock type
588 if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) {
589 return false; // Wallclock times not implemented
592 // accesskey type
593 if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) ||
594 StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) {
595 return false; // accesskey is not supported
598 // event, syncbase, or repeat
599 return ParseElementBaseTimeValueSpec(spec, aResult);
602 bool SMILParserUtils::ParseClockValue(const nsAString& aSpec,
603 SMILTimeValue* aResult) {
604 RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
605 RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
607 return ::ParseClockValue(iter, end, aResult) && iter == end;
610 int32_t SMILParserUtils::CheckForNegativeNumber(const nsAString& aStr) {
611 int32_t absValLocation = -1;
613 RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aStr));
614 RangedPtr<const char16_t> iter = start;
615 RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aStr));
617 // Skip initial whitespace
618 while (iter != end && nsContentUtils::IsHTMLWhitespace(*iter)) {
619 ++iter;
622 // Check for dash
623 if (iter != end && *iter == '-') {
624 ++iter;
625 // Check for numeric character
626 if (iter != end && mozilla::IsAsciiDigit(*iter)) {
627 absValLocation = iter - start;
630 return absValLocation;
633 } // namespace mozilla