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/SMILTimeValueSpecParams.h"
13 #include "mozilla/SMILTypes.h"
14 #include "mozilla/SMILValue.h"
15 #include "mozilla/SVGContentUtils.h"
16 #include "mozilla/TextUtils.h"
17 #include "nsContentUtils.h"
18 #include "nsCharSeparatedTokenizer.h"
20 using namespace mozilla::dom
;
21 //------------------------------------------------------------------------------
22 // Helper functions and Constants
26 using namespace mozilla
;
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;
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
37 inline bool SkipWhitespace(RangedPtr
<const char16_t
>& aIter
,
38 const RangedPtr
<const char16_t
>& aEnd
) {
39 while (aIter
!= aEnd
) {
40 if (!nsContentUtils::IsHTMLWhitespace(*aIter
)) {
48 inline bool ParseColon(RangedPtr
<const char16_t
>& aIter
,
49 const RangedPtr
<const char16_t
>& aEnd
) {
50 if (aIter
== aEnd
|| *aIter
!= ':') {
58 * Exactly two digits in the range 00 - 59 are expected.
60 bool ParseSecondsOrMinutes(RangedPtr
<const char16_t
>& aIter
,
61 const RangedPtr
<const char16_t
>& aEnd
,
63 if (aIter
== aEnd
|| !mozilla::IsAsciiDigit(*aIter
)) {
67 RangedPtr
<const char16_t
> iter(aIter
);
69 if (++iter
== aEnd
|| !mozilla::IsAsciiDigit(*iter
)) {
73 uint32_t value
= 10 * mozilla::AsciiAlphanumericToNumber(*aIter
) +
74 mozilla::AsciiAlphanumericToNumber(*iter
);
78 if (++iter
!= aEnd
&& mozilla::IsAsciiDigit(*iter
)) {
87 inline bool ParseClockMetric(RangedPtr
<const char16_t
>& aIter
,
88 const RangedPtr
<const char16_t
>& aEnd
,
89 uint32_t& aMultiplier
) {
91 aMultiplier
= MSEC_PER_SEC
;
97 if (++aIter
== aEnd
) {
98 aMultiplier
= MSEC_PER_HOUR
;
103 const nsAString
& metric
= Substring(aIter
.get(), aEnd
.get());
104 if (metric
.EqualsLiteral("min")) {
105 aMultiplier
= MSEC_PER_MIN
;
109 if (metric
.EqualsLiteral("ms")) {
117 if (++aIter
== aEnd
) {
118 aMultiplier
= MSEC_PER_SEC
;
126 * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax
128 bool ParseClockValue(RangedPtr
<const char16_t
>& aIter
,
129 const RangedPtr
<const char16_t
>& aEnd
,
130 SMILTimeValue::Rounding aRounding
,
131 SMILTimeValue
* aResult
) {
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.
150 if (clockType
== FULL_CLOCK_VALUE
) {
159 // Exclude anything invalid (for clock values)
160 // that number parsing might otherwise allow.
164 } while (iter
!= aEnd
);
168 int32_t hours
= 0, timecount
= 0;
169 double fraction
= 0.0;
170 uint32_t minutes
, seconds
, multiplier
;
173 case FULL_CLOCK_VALUE
:
174 if (!SVGContentUtils::ParseInteger(iter
, aEnd
, hours
) ||
175 !ParseColon(iter
, aEnd
)) {
179 case PARTIAL_CLOCK_VALUE
:
180 if (!ParseSecondsOrMinutes(iter
, aEnd
, minutes
) ||
181 !ParseColon(iter
, aEnd
) ||
182 !ParseSecondsOrMinutes(iter
, aEnd
, seconds
)) {
185 if (iter
!= aEnd
&& (*iter
!= '.' || !SVGContentUtils::ParseNumber(
186 iter
, aEnd
, fraction
))) {
189 aResult
->SetMillis(SMILTime(hours
) * MSEC_PER_HOUR
+
190 minutes
* MSEC_PER_MIN
+
191 (seconds
+ fraction
) * MSEC_PER_SEC
,
195 case TIMECOUNT_VALUE
:
197 !SVGContentUtils::ParseInteger(iter
, aEnd
, timecount
)) {
200 if (iter
!= aEnd
&& *iter
== '.' &&
201 !SVGContentUtils::ParseNumber(iter
, aEnd
, fraction
)) {
204 if (!ParseClockMetric(iter
, aEnd
, multiplier
)) {
207 aResult
->SetMillis((timecount
+ fraction
) * multiplier
, aRounding
);
215 bool ParseOffsetValue(RangedPtr
<const char16_t
>& aIter
,
216 const RangedPtr
<const char16_t
>& aEnd
,
217 SMILTimeValue
* aResult
) {
218 RangedPtr
<const char16_t
> iter(aIter
);
221 if (!SVGContentUtils::ParseOptionalSign(iter
, aEnd
, sign
) ||
222 !SkipWhitespace(iter
, aEnd
) ||
223 !ParseClockValue(iter
, aEnd
, SMILTimeValue::Rounding::Nearest
, aResult
)) {
227 aResult
->SetMillis(-aResult
->GetMillis());
233 bool ParseOffsetValue(const nsAString
& aSpec
, SMILTimeValue
* aResult
) {
234 RangedPtr
<const char16_t
> iter(SVGContentUtils::GetStartRangedPtr(aSpec
));
235 const RangedPtr
<const char16_t
> end(SVGContentUtils::GetEndRangedPtr(aSpec
));
237 return ParseOffsetValue(iter
, end
, aResult
) && iter
== end
;
240 bool ParseOptionalOffset(RangedPtr
<const char16_t
>& aIter
,
241 const RangedPtr
<const char16_t
>& aEnd
,
242 SMILTimeValue
* aResult
) {
244 *aResult
= SMILTimeValue::Zero();
248 return SkipWhitespace(aIter
, aEnd
) && ParseOffsetValue(aIter
, aEnd
, aResult
);
251 void MoveToNextToken(RangedPtr
<const char16_t
>& aIter
,
252 const RangedPtr
<const char16_t
>& aEnd
, bool aBreakOnDot
,
253 bool& aIsAnyCharEscaped
) {
254 aIsAnyCharEscaped
= false;
256 bool isCurrentCharEscaped
= false;
258 while (aIter
!= aEnd
&& !nsContentUtils::IsHTMLWhitespace(*aIter
)) {
259 if (isCurrentCharEscaped
) {
260 isCurrentCharEscaped
= false;
262 if (*aIter
== '+' || *aIter
== '-' || (aBreakOnDot
&& *aIter
== '.')) {
265 if (*aIter
== '\\') {
266 isCurrentCharEscaped
= true;
267 aIsAnyCharEscaped
= true;
274 already_AddRefed
<nsAtom
> ConvertUnescapedTokenToAtom(const nsAString
& aToken
) {
275 // Whether the token is an id-ref or event-symbol it should be a valid NCName
276 if (aToken
.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken
, false)))
278 return NS_Atomize(aToken
);
281 already_AddRefed
<nsAtom
> ConvertTokenToAtom(const nsAString
& aToken
,
282 bool aUnescapeToken
) {
283 // Unescaping involves making a copy of the string which we'd like to avoid if
285 if (!aUnescapeToken
) {
286 return ConvertUnescapedTokenToAtom(aToken
);
289 nsAutoString
token(aToken
);
291 const char16_t
* read
= token
.BeginReading();
292 const char16_t
* const end
= token
.EndReading();
293 char16_t
* write
= token
.BeginWriting();
296 while (read
!= end
) {
297 MOZ_ASSERT(write
<= read
, "Writing past where we've read");
298 if (!escape
&& *read
== '\\') {
306 token
.Truncate(write
- token
.BeginReading());
308 return ConvertUnescapedTokenToAtom(token
);
311 bool ParseElementBaseTimeValueSpec(const nsAString
& aSpec
,
312 SMILTimeValueSpecParams
& aResult
) {
313 SMILTimeValueSpecParams result
;
316 // The spec will probably look something like one of these
318 // element-name.begin
319 // element-name.event-name
321 // element-name.repeat(3)
324 // Technically `repeat(3)' is permitted but the behaviour in this case is not
325 // defined (for SMIL Animation) so we don't support it here.
328 RangedPtr
<const char16_t
> start(SVGContentUtils::GetStartRangedPtr(aSpec
));
329 RangedPtr
<const char16_t
> end(SVGContentUtils::GetEndRangedPtr(aSpec
));
335 RangedPtr
<const char16_t
> tokenEnd(start
);
337 bool requiresUnescaping
;
338 MoveToNextToken(tokenEnd
, end
, true, requiresUnescaping
);
340 RefPtr
<nsAtom
> atom
= ConvertTokenToAtom(
341 Substring(start
.get(), tokenEnd
.get()), requiresUnescaping
);
342 if (atom
== nullptr) {
346 // Parse the second token if there is one
347 if (tokenEnd
!= end
&& *tokenEnd
== '.') {
348 result
.mDependentElemID
= atom
;
352 MoveToNextToken(tokenEnd
, end
, false, requiresUnescaping
);
354 const nsAString
& token2
= Substring(start
.get(), tokenEnd
.get());
356 // element-name.begin
357 if (token2
.EqualsLiteral("begin")) {
358 result
.mType
= SMILTimeValueSpecParams::SYNCBASE
;
359 result
.mSyncBegin
= true;
361 } else if (token2
.EqualsLiteral("end")) {
362 result
.mType
= SMILTimeValueSpecParams::SYNCBASE
;
363 result
.mSyncBegin
= false;
364 // element-name.repeat(digit+)
365 } else if (StringBeginsWith(token2
, REPEAT_PREFIX
)) {
366 start
+= REPEAT_PREFIX
.Length();
368 if (start
== tokenEnd
|| *start
== '+' || *start
== '-' ||
369 !SVGContentUtils::ParseInteger(start
, tokenEnd
, repeatValue
)) {
372 if (start
== tokenEnd
|| *start
!= ')') {
375 result
.mType
= SMILTimeValueSpecParams::REPEAT
;
376 result
.mRepeatIteration
= repeatValue
;
377 // element-name.event-symbol
379 atom
= ConvertTokenToAtom(token2
, requiresUnescaping
);
380 if (atom
== nullptr) {
383 result
.mType
= SMILTimeValueSpecParams::EVENT
;
384 result
.mEventSymbol
= atom
;
388 result
.mType
= SMILTimeValueSpecParams::EVENT
;
389 result
.mEventSymbol
= atom
;
392 // We've reached the end of the token, so we should now be either looking at
393 // a '+', '-' (possibly with whitespace before it), or the end.
394 if (!ParseOptionalOffset(tokenEnd
, end
, &result
.mOffset
) || tokenEnd
!= end
) {
405 //------------------------------------------------------------------------------
408 const nsDependentSubstring
SMILParserUtils::TrimWhitespace(
409 const nsAString
& aString
) {
410 nsAString::const_iterator start
, end
;
412 aString
.BeginReading(start
);
413 aString
.EndReading(end
);
415 // Skip whitespace characters at the beginning
416 while (start
!= end
&& nsContentUtils::IsHTMLWhitespace(*start
)) {
420 // Skip whitespace characters at the end.
421 while (end
!= start
) {
424 if (!nsContentUtils::IsHTMLWhitespace(*end
)) {
425 // Step back to the last non-whitespace character.
432 return Substring(start
, end
);
435 bool SMILParserUtils::ParseKeySplines(
436 const nsAString
& aSpec
, FallibleTArray
<SMILKeySpline
>& aKeySplines
) {
437 for (const auto& controlPoint
:
438 nsCharSeparatedTokenizerTemplate
<nsContentUtils::IsHTMLWhitespace
>(aSpec
,
441 nsCharSeparatedTokenizerTemplate
<nsContentUtils::IsHTMLWhitespace
,
442 nsTokenizerFlags::SeparatorOptional
>
443 tokenizer(controlPoint
, ',');
446 for (auto& value
: values
) {
447 if (!tokenizer
.hasMoreTokens() ||
448 !SVGContentUtils::ParseNumber(tokenizer
.nextToken(), value
) ||
449 value
> 1.0 || value
< 0.0) {
453 if (tokenizer
.hasMoreTokens() || tokenizer
.separatorAfterCurrentToken() ||
454 !aKeySplines
.AppendElement(
455 SMILKeySpline(values
[0], values
[1], values
[2], values
[3]),
461 return !aKeySplines
.IsEmpty();
464 bool SMILParserUtils::ParseSemicolonDelimitedProgressList(
465 const nsAString
& aSpec
, bool aNonDecreasing
,
466 FallibleTArray
<double>& aArray
) {
467 nsCharSeparatedTokenizerTemplate
<nsContentUtils::IsHTMLWhitespace
> tokenizer(
470 double previousValue
= -1.0;
472 while (tokenizer
.hasMoreTokens()) {
474 if (!SVGContentUtils::ParseNumber(tokenizer
.nextToken(), value
)) {
478 if (value
> 1.0 || value
< 0.0 ||
479 (aNonDecreasing
&& value
< previousValue
)) {
483 if (!aArray
.AppendElement(value
, fallible
)) {
486 previousValue
= value
;
489 return !aArray
.IsEmpty();
492 // Helper class for ParseValues
493 class MOZ_STACK_CLASS SMILValueParser
494 : public SMILParserUtils::GenericValueParser
{
496 SMILValueParser(const SVGAnimationElement
* aSrcElement
,
497 const SMILAttr
* aSMILAttr
,
498 FallibleTArray
<SMILValue
>* aValuesArray
,
499 bool* aPreventCachingOfSandwich
)
500 : mSrcElement(aSrcElement
),
501 mSMILAttr(aSMILAttr
),
502 mValuesArray(aValuesArray
),
503 mPreventCachingOfSandwich(aPreventCachingOfSandwich
) {}
505 bool Parse(const nsAString
& aValueStr
) override
{
507 if (NS_FAILED(mSMILAttr
->ValueFromString(aValueStr
, mSrcElement
, newValue
,
508 *mPreventCachingOfSandwich
)))
511 if (!mValuesArray
->AppendElement(newValue
, fallible
)) {
518 const SVGAnimationElement
* mSrcElement
;
519 const SMILAttr
* mSMILAttr
;
520 FallibleTArray
<SMILValue
>* mValuesArray
;
521 bool* mPreventCachingOfSandwich
;
524 bool SMILParserUtils::ParseValues(const nsAString
& aSpec
,
525 const SVGAnimationElement
* aSrcElement
,
526 const SMILAttr
& aAttribute
,
527 FallibleTArray
<SMILValue
>& aValuesArray
,
528 bool& aPreventCachingOfSandwich
) {
529 // Assume all results can be cached, until we find one that can't.
530 aPreventCachingOfSandwich
= false;
531 SMILValueParser
valueParser(aSrcElement
, &aAttribute
, &aValuesArray
,
532 &aPreventCachingOfSandwich
);
533 return ParseValuesGeneric(aSpec
, valueParser
);
536 bool SMILParserUtils::ParseValuesGeneric(const nsAString
& aSpec
,
537 GenericValueParser
& aParser
) {
538 nsCharSeparatedTokenizerTemplate
<nsContentUtils::IsHTMLWhitespace
> tokenizer(
540 if (!tokenizer
.hasMoreTokens()) { // Empty list
544 while (tokenizer
.hasMoreTokens()) {
545 if (!aParser
.Parse(tokenizer
.nextToken())) {
553 bool SMILParserUtils::ParseRepeatCount(const nsAString
& aSpec
,
554 SMILRepeatCount
& aResult
) {
555 const nsAString
& spec
= SMILParserUtils::TrimWhitespace(aSpec
);
557 if (spec
.EqualsLiteral("indefinite")) {
558 aResult
.SetIndefinite();
563 if (!SVGContentUtils::ParseNumber(spec
, value
) || value
<= 0.0) {
570 bool SMILParserUtils::ParseTimeValueSpecParams(
571 const nsAString
& aSpec
, SMILTimeValueSpecParams
& aResult
) {
572 const nsAString
& spec
= TrimWhitespace(aSpec
);
574 if (spec
.EqualsLiteral("indefinite")) {
575 aResult
.mType
= SMILTimeValueSpecParams::INDEFINITE
;
580 if (ParseOffsetValue(spec
, &aResult
.mOffset
)) {
581 aResult
.mType
= SMILTimeValueSpecParams::OFFSET
;
586 if (StringBeginsWith(spec
, WALLCLOCK_PREFIX
)) {
587 return false; // Wallclock times not implemented
591 if (StringBeginsWith(spec
, ACCESSKEY_PREFIX_LC
) ||
592 StringBeginsWith(spec
, ACCESSKEY_PREFIX_CC
)) {
593 return false; // accesskey is not supported
596 // event, syncbase, or repeat
597 return ParseElementBaseTimeValueSpec(spec
, aResult
);
600 bool SMILParserUtils::ParseClockValue(const nsAString
& aSpec
,
601 SMILTimeValue::Rounding aRounding
,
602 SMILTimeValue
* aResult
) {
603 RangedPtr
<const char16_t
> iter(SVGContentUtils::GetStartRangedPtr(aSpec
));
604 RangedPtr
<const char16_t
> end(SVGContentUtils::GetEndRangedPtr(aSpec
));
606 return ::ParseClockValue(iter
, end
, aRounding
, aResult
) && iter
== end
;
609 int32_t SMILParserUtils::CheckForNegativeNumber(const nsAString
& aStr
) {
610 int32_t absValLocation
= -1;
612 RangedPtr
<const char16_t
> start(SVGContentUtils::GetStartRangedPtr(aStr
));
613 RangedPtr
<const char16_t
> iter
= start
;
614 RangedPtr
<const char16_t
> end(SVGContentUtils::GetEndRangedPtr(aStr
));
616 // Skip initial whitespace
617 while (iter
!= end
&& nsContentUtils::IsHTMLWhitespace(*iter
)) {
622 if (iter
!= end
&& *iter
== '-') {
624 // Check for numeric character
625 if (iter
!= end
&& mozilla::IsAsciiDigit(*iter
)) {
626 absValLocation
= iter
- start
;
629 return absValLocation
;
632 } // namespace mozilla