/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et tw=78: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ImportScanner.h" #include "mozilla/ServoBindings.h" #include "mozilla/StaticPrefs_layout.h" #include "nsContentUtils.h" namespace mozilla { static inline bool IsWhitespace(char16_t aChar) { return nsContentUtils::IsHTMLWhitespace(aChar); } static inline bool OptionalSupportsMatches(const nsAString& aAfterRuleValue) { // Ensure pref for @import supports() is enabled before wanting to check. if (!StaticPrefs::layout_css_import_supports_enabled()) { return true; } // Empty, don't bother checking. if (aAfterRuleValue.IsEmpty()) { return true; } NS_ConvertUTF16toUTF8 value(aAfterRuleValue); return Servo_CSSSupportsForImport(&value); } void ImportScanner::ResetState() { mInImportRule = false; // We try to avoid freeing the buffers here. mRuleName.Truncate(0); mRuleValue.Truncate(0); mAfterRuleValue.Truncate(0); } void ImportScanner::Start() { Stop(); mState = State::Idle; } void ImportScanner::EmitUrl() { MOZ_ASSERT(mState == State::AfterRuleValue); if (mInImportRule) { // Trim trailing whitespace from an unquoted URL. if (mUrlValueDelimiterClosingChar == ')') { // FIXME: Add a convenience function in nsContentUtils or something? mRuleValue.Trim(" \t\n\r\f", false); } // If a supports(...) condition is given as part of import conditions, // only emit the URL if it matches, as there is no use preloading // imports for features we do not support, as this cannot change // mid-page. if (OptionalSupportsMatches(mAfterRuleValue)) { mUrlsFound.AppendElement(std::move(mRuleValue)); } } ResetState(); MOZ_ASSERT(mRuleValue.IsEmpty()); } nsTArray ImportScanner::Stop() { if (mState == State::AfterRuleValue) { EmitUrl(); } mState = State::OutsideOfStyleElement; ResetState(); return std::move(mUrlsFound); } nsTArray ImportScanner::Scan(Span aFragment) { MOZ_ASSERT(ShouldScan()); for (char16_t c : aFragment) { mState = Scan(c); if (mState == State::Done) { break; } } return std::move(mUrlsFound); } auto ImportScanner::Scan(char16_t aChar) -> State { switch (mState) { case State::OutsideOfStyleElement: case State::Done: MOZ_ASSERT_UNREACHABLE("How?"); return mState; case State::Idle: { // TODO(emilio): Maybe worth caring about html-style comments like: // if (IsWhitespace(aChar)) { return mState; } if (aChar == '/') { return State::MaybeAtCommentStart; } if (aChar == '@') { MOZ_ASSERT(mRuleName.IsEmpty()); return State::AtRuleName; } return State::Done; } case State::MaybeAtCommentStart: { return aChar == '*' ? State::AtComment : State::Done; } case State::AtComment: { return aChar == '*' ? State::MaybeAtCommentEnd : mState; } case State::MaybeAtCommentEnd: { return aChar == '/' ? State::Idle : State::AtComment; } case State::AtRuleName: { if (IsAsciiAlpha(aChar)) { if (mRuleName.Length() > kMaxRuleNameLength - 1) { return State::Done; } mRuleName.Append(aChar); return mState; } if (IsWhitespace(aChar)) { mInImportRule = mRuleName.LowerCaseEqualsLiteral("import"); if (mInImportRule) { return State::AtRuleValue; } // Ignorable rules, we skip until the next semi-colon for these. if (mRuleName.LowerCaseEqualsLiteral("charset") || mRuleName.LowerCaseEqualsLiteral("layer")) { MOZ_ASSERT(mRuleValue.IsEmpty()); return State::AfterRuleValue; } } return State::Done; } case State::AtRuleValue: { MOZ_ASSERT(mInImportRule, "Should only get to this state for @import"); if (mRuleValue.IsEmpty()) { if (IsWhitespace(aChar)) { return mState; } if (aChar == '"' || aChar == '\'') { mUrlValueDelimiterClosingChar = aChar; return State::AtRuleValueDelimited; } if (aChar == 'u' || aChar == 'U') { mRuleValue.Append('u'); return mState; } return State::Done; } if (mRuleValue.Length() == 1) { MOZ_ASSERT(mRuleValue.EqualsLiteral("u")); if (aChar == 'r' || aChar == 'R') { mRuleValue.Append('r'); return mState; } return State::Done; } if (mRuleValue.Length() == 2) { MOZ_ASSERT(mRuleValue.EqualsLiteral("ur")); if (aChar == 'l' || aChar == 'L') { mRuleValue.Append('l'); } return mState; } if (mRuleValue.Length() == 3) { MOZ_ASSERT(mRuleValue.EqualsLiteral("url")); if (aChar == '(') { mUrlValueDelimiterClosingChar = ')'; mRuleValue.Truncate(0); return State::AtRuleValueDelimited; } return State::Done; } MOZ_ASSERT_UNREACHABLE( "How? We should find a paren or a string delimiter"); return State::Done; } case State::AtRuleValueDelimited: { MOZ_ASSERT(mInImportRule, "Should only get to this state for @import"); if (aChar == mUrlValueDelimiterClosingChar) { return State::AfterRuleValue; } if (mUrlValueDelimiterClosingChar == ')' && mRuleValue.IsEmpty()) { if (IsWhitespace(aChar)) { return mState; } if (aChar == '"' || aChar == '\'') { // Handle url("") and url(''). mUrlValueDelimiterClosingChar = aChar; return mState; } } if (!mRuleValue.Append(aChar, mozilla::fallible)) { mRuleValue.Truncate(0); return State::Done; } return mState; } case State::AfterRuleValue: { if (aChar == ';') { EmitUrl(); return State::Idle; } // If there's a selector here and the import was unterminated, just give // up. if (aChar == '{') { return State::Done; } if (!mAfterRuleValue.Append(aChar, mozilla::fallible)) { mAfterRuleValue.Truncate(0); return State::Done; } return mState; // There can be all sorts of stuff here like media // queries or what not. } } MOZ_ASSERT_UNREACHABLE("Forgot to handle a state?"); return State::Done; } } // namespace mozilla