Bläddra i källkod

LibWeb: Implement the document title attribute closer to the spec

The main differences between our current implementation and the spec
are:

    * The title element need not be a child of the head element.

    * If the title element does not exist, the default value should be
      the empty string - we currently return a null string.

    * We've since added AOs for several of the spec steps here, so we
      do not need to implement those steps inline.
Timothy Flynn 2 år sedan
förälder
incheckning
f2a28e83de

+ 14 - 0
Tests/LibWeb/Text/expected/title.txt

@@ -0,0 +1,14 @@
+1: ""
+2a: 0
+2b: 1
+2c: "This is a title!"
+2d: "This is a title!"
+3: ""
+4a: 3
+4b: ""
+4c: ""
+4d: ""
+4e: 3
+4f: "This is another title!"
+4g: ""
+4h: ""

+ 42 - 0
Tests/LibWeb/Text/input/title.html

@@ -0,0 +1,42 @@
+<script src="include.js"></script>
+<script>
+    test(() => {
+        // The title is the empty string by default.
+        println(`1: "${document.title}"`);
+
+        // When the title is set, and a title element does not exist, one is added to to head element.
+        let titleElements = document.getElementsByTagName('title');
+        println(`2a: ${titleElements.length}`)
+
+        document.title = 'This is a title!';
+
+        titleElements = document.getElementsByTagName('title');
+        println(`2b: ${titleElements.length}`)
+        println(`2c: "${titleElements[0].innerText}"`);
+        println(`2d: "${document.title}"`);
+
+        // Removing the title element sets the title back to default.
+        titleElements[0].remove();
+        println(`3: "${document.title}"`);
+
+        // After adding several title elements to the body, setting the title updates the text
+        // content of only the first title element.
+        document.body.appendChild(document.createElement('title'));
+        document.body.appendChild(document.createElement('title'));
+        document.body.appendChild(document.createElement('title'));
+
+        titleElements = document.getElementsByTagName('title');
+        println(`4a: ${titleElements.length}`)
+        println(`4b: "${titleElements[0].innerText}"`);
+        println(`4c: "${titleElements[1].innerText}"`);
+        println(`4d: "${titleElements[2].innerText}"`);
+
+        document.title = 'This is another title!';
+
+        titleElements = document.getElementsByTagName('title');
+        println(`4e: ${titleElements.length}`)
+        println(`4f: "${titleElements[0].innerText}"`);
+        println(`4g: "${titleElements[1].innerText}"`);
+        println(`4h: "${titleElements[2].innerText}"`);
+    });
+</script>

+ 66 - 30
Userland/Libraries/LibWeb/DOM/Document.cpp

@@ -79,6 +79,7 @@
 #include <LibWeb/Page/Page.h>
 #include <LibWeb/PermissionsPolicy/AutoplayAllowlist.h>
 #include <LibWeb/Platform/Timer.h>
+#include <LibWeb/SVG/SVGElement.h>
 #include <LibWeb/SVG/TagNames.h>
 #include <LibWeb/Selection/Selection.h>
 #include <LibWeb/UIEvents/EventNames.h>
@@ -680,52 +681,87 @@ WebIDL::ExceptionOr<void> Document::set_body(HTML::HTMLElement* new_body)
     return {};
 }
 
+// https://html.spec.whatwg.org/multipage/dom.html#document.title
 DeprecatedString Document::title() const
 {
-    auto* head_element = head();
-    if (!head_element)
-        return {};
-
-    auto* title_element = head_element->first_child_of_type<HTML::HTMLTitleElement>();
-    if (!title_element)
-        return {};
+    auto value = DeprecatedString::empty();
 
-    auto raw_title = title_element->text_content();
+    // 1. If the document element is an SVG svg element, then let value be the child text content of the first SVG title
+    //    element that is a child of the document element.
+    if (auto const* document_element = this->document_element(); document_element && is<SVG::SVGElement>(document_element)) {
+        // FIXME: Implement the SVG title element and get its child text content.
+    }
 
-    StringBuilder builder;
-    bool last_was_space = false;
-    for (auto code_point : Utf8View(raw_title)) {
-        if (is_ascii_space(code_point)) {
-            last_was_space = true;
-        } else {
-            if (last_was_space && !builder.is_empty())
-                builder.append(' ');
-            builder.append_code_point(code_point);
-            last_was_space = false;
-        }
+    // 2. Otherwise, let value be the child text content of the title element, or the empty string if the title element
+    //    is null.
+    else if (auto title_element = this->title_element()) {
+        value = title_element->text_content();
     }
-    return builder.to_deprecated_string();
+
+    // 3. Strip and collapse ASCII whitespace in value.
+    auto title = Infra::strip_and_collapse_whitespace(value).release_value_but_fixme_should_propagate_errors();
+
+    // 4. Return value.
+    return title.to_deprecated_string();
 }
 
-void Document::set_title(DeprecatedString const& title)
+// https://html.spec.whatwg.org/multipage/dom.html#document.title
+WebIDL::ExceptionOr<void> Document::set_title(DeprecatedString const& title)
 {
-    auto* head_element = const_cast<HTML::HTMLHeadElement*>(head());
-    if (!head_element)
-        return;
+    auto* document_element = this->document_element();
 
-    JS::GCPtr<HTML::HTMLTitleElement> title_element = head_element->first_child_of_type<HTML::HTMLTitleElement>();
-    if (!title_element) {
-        title_element = &static_cast<HTML::HTMLTitleElement&>(*DOM::create_element(*this, HTML::TagNames::title, Namespace::HTML).release_value_but_fixme_should_propagate_errors());
-        MUST(head_element->append_child(*title_element));
+    // -> If the document element is an SVG svg element
+    if (is<SVG::SVGElement>(document_element)) {
+        // FIXME: 1. If there is an SVG title element that is a child of the document element, let element be the first such
+        //           element.
+        // FIXME: 2. Otherwise:
+        //            1. Let element be the result of creating an element given the document element's node document, title,
+        //               and the SVG namespace.
+        //            2. Insert element as the first child of the document element.
+        // FIXME: 3. String replace all with the given value within element.
     }
 
-    title_element->remove_all_children(true);
-    MUST(title_element->append_child(heap().allocate<Text>(realm(), *this, title).release_allocated_value_but_fixme_should_propagate_errors()));
+    // -> If the document element is in the HTML namespace
+    else if (document_element && document_element->namespace_() == Namespace::HTML) {
+        auto title_element = this->title_element();
+        auto* head_element = this->head();
+
+        // 1. If the title element is null and the head element is null, then return.
+        if (title_element == nullptr && head_element == nullptr)
+            return {};
+
+        JS::GCPtr<Element> element;
+
+        // 2. If the title element is non-null, let element be the title element.
+        if (title_element) {
+            element = title_element;
+        }
+        // 3. Otherwise:
+        else {
+            // 1. Let element be the result of creating an element given the document element's node document, title,
+            //    and the HTML namespace.
+            element = TRY(DOM::create_element(*this, HTML::TagNames::title, Namespace::HTML));
+
+            // 2. Append element to the head element.
+            TRY(head_element->append_child(*element));
+        }
+
+        // 4. String replace all with the given value within element.
+        element->string_replace_all(title);
+    }
+
+    // -> Otherwise
+    else {
+        // Do nothing.
+        return {};
+    }
 
     if (auto* page = this->page()) {
         if (browsing_context() == &page->top_level_browsing_context())
             page->client().page_did_change_title(title);
     }
+
+    return {};
 }
 
 void Document::tear_down_layout_tree()

+ 1 - 1
Userland/Libraries/LibWeb/DOM/Document.h

@@ -173,7 +173,7 @@ public:
     WebIDL::ExceptionOr<void> set_body(HTML::HTMLElement* new_body);
 
     DeprecatedString title() const;
-    void set_title(DeprecatedString const&);
+    WebIDL::ExceptionOr<void> set_title(DeprecatedString const&);
 
     HTML::BrowsingContext* browsing_context() { return m_browsing_context.ptr(); }
     HTML::BrowsingContext const* browsing_context() const { return m_browsing_context.ptr(); }