/* -*- 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<nsString> ImportScanner::Stop() {
if (mState == State::AfterRuleValue) {
EmitUrl();
}
mState = State::OutsideOfStyleElement;
ResetState();
return std::move(mUrlsFound);
}
nsTArray<nsString> ImportScanner::Scan(Span<
const char16_t> 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:
// <style>
// <!--
// @import url(stuff);
// -->
// </style>
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