瀏覽代碼

LibWeb: Implement `CSSStyleSheet.replace()`

This method asynchronously replaces the content of the given stylesheet
with the content passed to it.

An exception is thrown if this method is used by a stylesheet not
created with the `CSSStyleSheet()` constructor.
Tim Ledbetter 1 年之前
父節點
當前提交
81c67d34eb

+ 7 - 0
Tests/LibWeb/Text/expected/css/CSSStyleSheet-replace.txt

@@ -0,0 +1,7 @@
+Exception thrown when calling replace() on non-constructed stylesheet: NotAllowedError
+Number of CSS rules after replace(): 2
+Rule: .test { font-size: 14px; }
+Rule: .test2 { font-size: 16px; }
+cssRules returns the same object before and after replace(): true
+@import rule should be not appear below:
+Rule: .test { padding: 100px; }

+ 50 - 0
Tests/LibWeb/Text/input/css/CSSStyleSheet-replace.html

@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<style>
+    .test {
+        font-size: 12px;
+    }
+</style>
+<script src="../include.js"></script>
+<script>
+    asyncTest(async done => {
+        const newStyle = `
+        .test {
+            font-size: 14px;
+        }
+        .test2 {
+            font-size: 16px;
+        }`;
+        const newStyle2 = `.test {
+            padding: 100px;
+        }`;
+
+        const nonConstructedSheet = document.styleSheets[0];
+        try {
+            await nonConstructedSheet.replace(newStyle);
+            println("FAIL");
+        } catch (e) {
+            println(`Exception thrown when calling replace() on non-constructed stylesheet: ${e.name}`);
+        }
+
+        const sheet = new CSSStyleSheet();
+        const cssRules = sheet.cssRules;
+
+        await sheet.replace(newStyle);
+        println(`Number of CSS rules after replace(): ${sheet.cssRules.length}`);
+        for (const rule of sheet.cssRules) {
+            println(`Rule: ${rule.cssText}`);
+        }
+
+        const cssRulesAfterReplace = sheet.cssRules;
+        println(`cssRules returns the same object before and after replace(): ${cssRules === cssRulesAfterReplace}`);
+
+        const importRule = `@import url("test.css");`;
+        await sheet.replace(`${newStyle2} ${importRule}`);
+
+        println(`@import rule should be not appear below:`);
+        for (const rule of sheet.cssRules) {
+            println(`Rule: ${rule.cssText}`);
+        }
+        done();
+    });
+</script>

+ 54 - 2
Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp

@@ -13,6 +13,7 @@
 #include <LibWeb/CSS/StyleSheetList.h>
 #include <LibWeb/DOM/Document.h>
 #include <LibWeb/HTML/Window.h>
+#include <LibWeb/Platform/EventLoopPlugin.h>
 #include <LibWeb/WebIDL/ExceptionOr.h>
 
 namespace Web::CSS {
@@ -130,7 +131,9 @@ WebIDL::ExceptionOr<unsigned> CSSStyleSheet::insert_rule(StringView rule, unsign
 {
     // FIXME: 1. If the origin-clean flag is unset, throw a SecurityError exception.
 
-    // FIXME: 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
+    // If the disallow modification flag is set, throw a NotAllowedError DOMException.
+    if (disallow_modification())
+        return WebIDL::NotAllowedError::create(realm(), "Can't call insert_rule() on non-modifiable stylesheets."_fly_string);
 
     // 3. Let parsed rule be the return value of invoking parse a rule with rule.
     auto context = m_style_sheet_list ? CSS::Parser::ParsingContext { m_style_sheet_list->document() } : CSS::Parser::ParsingContext { realm() };
@@ -165,7 +168,9 @@ WebIDL::ExceptionOr<void> CSSStyleSheet::delete_rule(unsigned index)
 {
     // FIXME: 1. If the origin-clean flag is unset, throw a SecurityError exception.
 
-    // FIXME: 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
+    // 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
+    if (disallow_modification())
+        return WebIDL::NotAllowedError::create(realm(), "Can't call delete_rule() on non-modifiable stylesheets."_fly_string);
 
     // 3. Remove a CSS rule in the CSS rules at index.
     auto result = m_rules->remove_a_css_rule(index);
@@ -178,6 +183,53 @@ WebIDL::ExceptionOr<void> CSSStyleSheet::delete_rule(unsigned index)
     return result;
 }
 
+// https://drafts.csswg.org/cssom/#dom-cssstylesheet-replace
+JS::NonnullGCPtr<JS::Promise> CSSStyleSheet::replace(String text)
+{
+    // 1. Let promise be a promise
+    auto promise = JS::Promise::create(realm());
+
+    // 2. If the constructed flag is not set, or the disallow modification flag is set, reject promise with a NotAllowedError DOMException and return promise.
+    if (!constructed()) {
+        promise->reject(WebIDL::NotAllowedError::create(realm(), "Can't call replace() on non-constructed stylesheets"_fly_string));
+        return promise;
+    }
+
+    if (disallow_modification()) {
+        promise->reject(WebIDL::NotAllowedError::create(realm(), "Can't call replace() on non-modifiable stylesheets"_fly_string));
+        return promise;
+    }
+
+    // 3. Set the disallow modification flag.
+    set_disallow_modification(true);
+
+    // 4. In parallel, do these steps:
+    Platform::EventLoopPlugin::the().deferred_invoke([this, text = move(text), promise] {
+        // 1. Let rules be the result of running parse a stylesheet’s contents from text.
+        auto context = m_style_sheet_list ? CSS::Parser::ParsingContext { m_style_sheet_list->document() } : CSS::Parser::ParsingContext { realm() };
+        auto* parsed_stylesheet = parse_css_stylesheet(context, text);
+        auto& rules = parsed_stylesheet->rules();
+
+        // 2. If rules contains one or more @import rules, remove those rules from rules.
+        JS::MarkedVector<JS::NonnullGCPtr<CSSRule>> rules_without_import(realm().heap());
+        for (auto rule : rules) {
+            if (rule->type() != CSSRule::Type::Import)
+                rules_without_import.append(rule);
+        }
+
+        // 3. Set sheet’s CSS rules to rules.
+        m_rules->set_rules({}, rules_without_import);
+
+        // 4. Unset sheet’s disallow modification flag.
+        set_disallow_modification(false);
+
+        // 5. Resolve promise with sheet.
+        promise->fulfill(this);
+    });
+
+    return promise;
+}
+
 // https://www.w3.org/TR/cssom/#dom-cssstylesheet-removerule
 WebIDL::ExceptionOr<void> CSSStyleSheet::remove_rule(unsigned index)
 {

+ 6 - 0
Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h

@@ -52,6 +52,8 @@ public:
     WebIDL::ExceptionOr<void> remove_rule(unsigned index);
     WebIDL::ExceptionOr<void> delete_rule(unsigned index);
 
+    JS::NonnullGCPtr<JS::Promise> replace(String text);
+
     void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
     // Returns whether the match state of any media queries changed after evaluation.
     bool evaluate_media_queries(HTML::Window const&);
@@ -70,6 +72,8 @@ public:
     JS::GCPtr<DOM::Document const> constructor_document() const { return m_constructor_document; }
     void set_constructor_document(JS::GCPtr<DOM::Document const> constructor_document) { m_constructor_document = constructor_document; }
 
+    bool disallow_modification() const { return m_disallow_modification; }
+
 private:
     CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional<AK::URL> location);
 
@@ -79,6 +83,7 @@ private:
     void recalculate_namespaces();
 
     void set_constructed(bool constructed) { m_constructed = constructed; }
+    void set_disallow_modification(bool disallow_modification) { m_disallow_modification = disallow_modification; }
 
     JS::GCPtr<CSSRuleList> m_rules;
     JS::GCPtr<CSSNamespaceRule> m_default_namespace_rule;
@@ -90,6 +95,7 @@ private:
     Optional<AK::URL> m_base_url;
     JS::GCPtr<DOM::Document const> m_constructor_document;
     bool m_constructed { false };
+    bool m_disallow_modification { false };
 };
 
 }

+ 1 - 1
Userland/Libraries/LibWeb/CSS/CSSStyleSheet.idl

@@ -13,7 +13,7 @@ interface CSSStyleSheet : StyleSheet {
     unsigned long insertRule(CSSOMString rule, optional unsigned long index = 0);
     undefined deleteRule(unsigned long index);
 
-    // FIXME: Promise<CSSStyleSheet> replace(USVString text);
+    Promise<CSSStyleSheet> replace(USVString text);
     // FIXME: undefined replaceSync(USVString text);
 
     // https://drafts.csswg.org/cssom/#legacy-css-style-sheet-members