LibWeb: Implement <template> parsing
Note that there is currently no way to display them as we can't currently clone nodes. Adds special case for templates for dumping to console. Doesn't add it to the DOM inspector as I'm not sure how to do it.
This commit is contained in:
parent
f48feae0b2
commit
7902d215b3
Notes:
sideshowbarker
2024-07-19 03:23:09 +09:00
Author: https://github.com/Lubrsi Commit: https://github.com/SerenityOS/serenity/commit/7902d215b39 Pull-request: https://github.com/SerenityOS/serenity/pull/3224 Reviewed-by: https://github.com/awesomekling
14 changed files with 174 additions and 28 deletions
|
@ -167,6 +167,13 @@ public:
|
|||
|
||||
void set_focused_element(Element*);
|
||||
|
||||
bool created_for_appropriate_template_contents() const { return m_created_for_appropriate_template_contents; }
|
||||
void set_created_for_appropriate_template_contents(bool value) { m_created_for_appropriate_template_contents = value; }
|
||||
|
||||
Document* associated_inert_template_document() { return m_associated_inert_template_document; }
|
||||
const Document* associated_inert_template_document() const { return m_associated_inert_template_document; }
|
||||
void set_associated_inert_template_document(Document& document) { m_associated_inert_template_document = document; }
|
||||
|
||||
private:
|
||||
virtual RefPtr<LayoutNode> create_layout_node(const CSS::StyleProperties* parent_style) override;
|
||||
|
||||
|
@ -199,6 +206,9 @@ private:
|
|||
bool m_editable { false };
|
||||
|
||||
WeakPtr<Element> m_focused_element;
|
||||
|
||||
bool m_created_for_appropriate_template_contents { false };
|
||||
RefPtr<Document> m_associated_inert_template_document;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include <AK/FlyString.h>
|
||||
#include <LibWeb/DOM/NonElementParentNode.h>
|
||||
#include <LibWeb/DOM/ParentNode.h>
|
||||
#include <LibWeb/DOM/Element.h>
|
||||
|
||||
namespace Web::DOM {
|
||||
|
||||
|
@ -36,10 +37,20 @@ class DocumentFragment
|
|||
: public ParentNode
|
||||
, public NonElementParentNode<DocumentFragment> {
|
||||
public:
|
||||
DocumentFragment(Document& document);
|
||||
using WrapperType = Bindings::DocumentFragmentWrapper;
|
||||
|
||||
explicit DocumentFragment(Document& document);
|
||||
virtual ~DocumentFragment() override;
|
||||
|
||||
virtual FlyString node_name() const override { return "#document-fragment"; }
|
||||
|
||||
Element& host() { return *m_host; }
|
||||
const Element& host() const { return *m_host; }
|
||||
|
||||
void set_host(Element& host) { m_host = host; }
|
||||
|
||||
private:
|
||||
RefPtr<Element> m_host;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -63,6 +63,8 @@ GUI::ModelIndex DOMTreeModel::parent_index(const GUI::ModelIndex& index) const
|
|||
if (!node.parent())
|
||||
return {};
|
||||
|
||||
// FIXME: Handle the template element (child elements are not stored in it, all of its children are in its document fragment "content")
|
||||
|
||||
// No grandparent? Parent is the document!
|
||||
if (!node.parent()->parent()) {
|
||||
return create_index(0, 0, m_document.ptr());
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
#include <LibWeb/DOM/Element.h>
|
||||
#include <LibWeb/DOM/Text.h>
|
||||
#include <LibWeb/Dump.h>
|
||||
#include <LibWeb/HTML/HTMLTemplateElement.h>
|
||||
#include <LibWeb/Layout/LayoutBlock.h>
|
||||
#include <LibWeb/Layout/LayoutNode.h>
|
||||
#include <LibWeb/Layout/LayoutText.h>
|
||||
|
@ -67,9 +68,14 @@ void dump_tree(const DOM::Node& node)
|
|||
}
|
||||
++indent;
|
||||
if (is<DOM::ParentNode>(node)) {
|
||||
static_cast<const DOM::ParentNode&>(node).for_each_child([](auto& child) {
|
||||
dump_tree(child);
|
||||
});
|
||||
if (!is<HTML::HTMLTemplateElement>(node)) {
|
||||
static_cast<const DOM::ParentNode&>(node).for_each_child([](auto& child) {
|
||||
dump_tree(child);
|
||||
});
|
||||
} else {
|
||||
auto& template_element = downcast<HTML::HTMLTemplateElement>(node);
|
||||
dump_tree(template_element.content());
|
||||
}
|
||||
}
|
||||
--indent;
|
||||
}
|
||||
|
|
|
@ -159,6 +159,7 @@ namespace Web::Bindings {
|
|||
class CanvasRenderingContext2DWrapper;
|
||||
class CharacterDataWrapper;
|
||||
class CommentWrapper;
|
||||
class DocumentFragmentWrapper;
|
||||
class DocumentTypeWrapper;
|
||||
class DocumentWrapper;
|
||||
class ElementWrapper;
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <LibWeb/DOM/Document.h>
|
||||
#include <LibWeb/HTML/HTMLTemplateElement.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
@ -31,10 +32,30 @@ namespace Web::HTML {
|
|||
HTMLTemplateElement::HTMLTemplateElement(DOM::Document& document, const FlyString& tag_name)
|
||||
: HTMLElement(document, tag_name)
|
||||
{
|
||||
m_content = adopt(*new DOM::DocumentFragment(appropriate_template_contents_owner_document(document)));
|
||||
m_content->set_host(*this);
|
||||
}
|
||||
|
||||
HTMLTemplateElement::~HTMLTemplateElement()
|
||||
{
|
||||
}
|
||||
|
||||
DOM::Document& HTMLTemplateElement::appropriate_template_contents_owner_document(DOM::Document& document)
|
||||
{
|
||||
if (!document.created_for_appropriate_template_contents()) {
|
||||
if (!document.associated_inert_template_document()) {
|
||||
DOM::Document new_document;
|
||||
new_document.set_created_for_appropriate_template_contents(true);
|
||||
|
||||
// FIXME: If doc is an HTML document, mark new doc as an HTML document also.
|
||||
|
||||
document.set_associated_inert_template_document(new_document);
|
||||
}
|
||||
|
||||
return *document.associated_inert_template_document();
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <LibWeb/DOM/DocumentFragment.h>
|
||||
#include <LibWeb/HTML/HTMLElement.h>
|
||||
|
||||
namespace Web::HTML {
|
||||
|
@ -36,6 +37,14 @@ public:
|
|||
|
||||
HTMLTemplateElement(DOM::Document&, const FlyString& local_name);
|
||||
virtual ~HTMLTemplateElement() override;
|
||||
|
||||
NonnullRefPtr<DOM::DocumentFragment> content() { return *m_content; }
|
||||
const NonnullRefPtr<DOM::DocumentFragment> content() const { return *m_content; }
|
||||
|
||||
private:
|
||||
DOM::Document& appropriate_template_contents_owner_document(DOM::Document&);
|
||||
|
||||
RefPtr<DOM::DocumentFragment> m_content;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
interface HTMLTemplateElement : HTMLElement {
|
||||
|
||||
|
||||
readonly attribute DocumentFragment content;
|
||||
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
#include <LibWeb/HTML/HTMLFormElement.h>
|
||||
#include <LibWeb/HTML/HTMLHeadElement.h>
|
||||
#include <LibWeb/HTML/HTMLScriptElement.h>
|
||||
#include <LibWeb/HTML/HTMLTemplateElement.h>
|
||||
#include <LibWeb/HTML/Parser/HTMLDocumentParser.h>
|
||||
#include <LibWeb/HTML/Parser/HTMLToken.h>
|
||||
|
||||
|
@ -385,18 +386,33 @@ DOM::Element& HTMLDocumentParser::node_before_current_node()
|
|||
HTMLDocumentParser::AdjustedInsertionLocation HTMLDocumentParser::find_appropriate_place_for_inserting_node()
|
||||
{
|
||||
auto& target = current_node();
|
||||
HTMLDocumentParser::AdjustedInsertionLocation adjusted_insertion_location;
|
||||
|
||||
if (m_foster_parenting && target.local_name().is_one_of(HTML::TagNames::table, HTML::TagNames::tbody, HTML::TagNames::tfoot, HTML::TagNames::thead, HTML::TagNames::tr)) {
|
||||
// FIXME: There's a bunch of steps for <template> elements here.
|
||||
auto last_template = m_stack_of_open_elements.last_element_with_tag_name(HTML::TagNames::template_);
|
||||
auto last_table = m_stack_of_open_elements.last_element_with_tag_name(HTML::TagNames::table);
|
||||
if (!last_table) {
|
||||
if (last_template.element && (!last_table.element || last_template.index > last_table.index)) {
|
||||
// This returns the template content, so no need to check the parent is a template.
|
||||
return { downcast<HTMLTemplateElement>(last_template.element)->content(), nullptr };
|
||||
}
|
||||
if (!last_table.element) {
|
||||
ASSERT(m_parsing_fragment);
|
||||
// Guaranteed not to be a template element (it will be the html element),
|
||||
// so no need to check the parent is a template.
|
||||
return { m_stack_of_open_elements.elements().first(), nullptr };
|
||||
}
|
||||
if (last_table->parent_node())
|
||||
return { last_table->parent_node(), last_table };
|
||||
return { m_stack_of_open_elements.element_before(*last_table), nullptr };
|
||||
if (last_table.element->parent_node())
|
||||
adjusted_insertion_location = { last_table.element->parent_node(), last_table.element };
|
||||
else
|
||||
adjusted_insertion_location = { m_stack_of_open_elements.element_before(*last_table.element), nullptr };
|
||||
} else {
|
||||
adjusted_insertion_location = { target, nullptr };
|
||||
}
|
||||
return { target, nullptr };
|
||||
|
||||
if (is<HTMLTemplateElement>(*adjusted_insertion_location.parent))
|
||||
return { downcast<HTMLTemplateElement>(*adjusted_insertion_location.parent).content(), nullptr };
|
||||
|
||||
return adjusted_insertion_location;
|
||||
}
|
||||
|
||||
NonnullRefPtr<DOM::Element> HTMLDocumentParser::create_element_for(const HTMLToken& token)
|
||||
|
@ -557,15 +573,29 @@ void HTMLDocumentParser::handle_in_head(HTMLToken& token)
|
|||
}
|
||||
|
||||
if (token.is_start_tag() && token.tag_name() == HTML::TagNames::template_) {
|
||||
// FIXME: Support this properly
|
||||
insert_html_element(token);
|
||||
m_list_of_active_formatting_elements.add_marker();
|
||||
m_frameset_ok = false;
|
||||
m_insertion_mode = InsertionMode::InTemplate;
|
||||
m_stack_of_template_insertion_modes.append(InsertionMode::InTemplate);
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.is_end_tag() && token.tag_name() == HTML::TagNames::template_) {
|
||||
// FIXME: Support this properly
|
||||
ASSERT(current_node().local_name() == HTML::TagNames::template_);
|
||||
m_stack_of_open_elements.pop();
|
||||
if (!m_stack_of_open_elements.contains(HTML::TagNames::template_)) {
|
||||
PARSE_ERROR();
|
||||
return;
|
||||
}
|
||||
|
||||
generate_all_implied_end_tags_thoroughly();
|
||||
|
||||
if (current_node().local_name() != HTML::TagNames::template_)
|
||||
PARSE_ERROR();
|
||||
|
||||
m_stack_of_open_elements.pop_until_an_element_with_tag_name_has_been_popped(HTML::TagNames::template_);
|
||||
m_list_of_active_formatting_elements.clear_up_to_the_last_marker();
|
||||
m_stack_of_template_insertion_modes.take_last();
|
||||
reset_the_insertion_mode_appropriately();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -739,6 +769,12 @@ void HTMLDocumentParser::generate_implied_end_tags(const FlyString& exception)
|
|||
m_stack_of_open_elements.pop();
|
||||
}
|
||||
|
||||
void HTMLDocumentParser::generate_all_implied_end_tags_thoroughly()
|
||||
{
|
||||
while (current_node().local_name().is_one_of(HTML::TagNames::caption, HTML::TagNames::colgroup, HTML::TagNames::dd, HTML::TagNames::dt, HTML::TagNames::li, HTML::TagNames::optgroup, HTML::TagNames::option, HTML::TagNames::p, HTML::TagNames::rb, HTML::TagNames::rp, HTML::TagNames::rt, HTML::TagNames::rtc, HTML::TagNames::tbody, HTML::TagNames::td, HTML::TagNames::tfoot, HTML::TagNames::th, HTML::TagNames::thead, HTML::TagNames::tr))
|
||||
m_stack_of_open_elements.pop();
|
||||
}
|
||||
|
||||
void HTMLDocumentParser::close_a_p_element()
|
||||
{
|
||||
generate_implied_end_tags(HTML::TagNames::p);
|
||||
|
@ -1084,14 +1120,17 @@ void HTMLDocumentParser::handle_in_body(HTMLToken& token)
|
|||
}
|
||||
|
||||
if (token.is_end_of_file()) {
|
||||
// FIXME: If the stack of template insertion modes is not empty,
|
||||
// then process the token using the rules for the "in template" insertion mode.
|
||||
if (!m_stack_of_template_insertion_modes.is_empty()) {
|
||||
process_using_the_rules_for(InsertionMode::InTemplate, token);
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: If there is a node in the stack of open elements that is not either
|
||||
// a dd element, a dt element, an li element, an optgroup element, an option element,
|
||||
// a p element, an rb element, an rp element, an rt element, an rtc element,
|
||||
// a tbody element, a td element, a tfoot element, a th element, a thead element,
|
||||
// a tr element, the body element, or the html element, then this is a parse error.
|
||||
for (auto& node : m_stack_of_open_elements.elements()) {
|
||||
if (!node.local_name().is_one_of(HTML::TagNames::dd, HTML::TagNames::dt, HTML::TagNames::li, HTML::TagNames::optgroup, HTML::TagNames::option, HTML::TagNames::p, HTML::TagNames::rb, HTML::TagNames::rp, HTML::TagNames::rt, HTML::TagNames::rtc, HTML::TagNames::tbody, HTML::TagNames::td, HTML::TagNames::tfoot, HTML::TagNames::th, HTML::TagNames::thead, HTML::TagNames::tr, HTML::TagNames::body, HTML::TagNames::html)) {
|
||||
PARSE_ERROR();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stop_parsing();
|
||||
return;
|
||||
|
@ -2697,7 +2736,8 @@ void HTMLDocumentParser::reset_the_insertion_mode_appropriately()
|
|||
}
|
||||
|
||||
if (node->local_name() == HTML::TagNames::template_) {
|
||||
TODO();
|
||||
m_insertion_mode = m_stack_of_template_insertion_modes.last();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!last && node->local_name() == HTML::TagNames::head) {
|
||||
|
@ -2774,7 +2814,7 @@ NonnullRefPtrVector<DOM::Node> HTMLDocumentParser::parse_html_fragment(DOM::Elem
|
|||
parser.m_stack_of_open_elements.push(root);
|
||||
|
||||
if (context_element.local_name() == HTML::TagNames::template_) {
|
||||
TODO();
|
||||
parser.m_stack_of_template_insertion_modes.append(InsertionMode::InTemplate);
|
||||
}
|
||||
|
||||
// FIXME: Create a start tag token whose name is the local name of context and whose attributes are the attributes of context.
|
||||
|
|
|
@ -115,6 +115,7 @@ private:
|
|||
void stop_parsing() { m_stop_parsing = true; }
|
||||
|
||||
void generate_implied_end_tags(const FlyString& exception = {});
|
||||
void generate_all_implied_end_tags_thoroughly();
|
||||
bool stack_of_open_elements_has_element_with_tag_name_in_scope(const FlyString& tag_name);
|
||||
NonnullRefPtr<DOM::Element> create_element_for(const HTMLToken&);
|
||||
|
||||
|
|
|
@ -133,14 +133,14 @@ DOM::Element* StackOfOpenElements::topmost_special_node_below(const DOM::Element
|
|||
return found_element;
|
||||
}
|
||||
|
||||
DOM::Element* StackOfOpenElements::last_element_with_tag_name(const FlyString& tag_name)
|
||||
StackOfOpenElements::LastElementResult StackOfOpenElements::last_element_with_tag_name(const FlyString& tag_name)
|
||||
{
|
||||
for (ssize_t i = m_elements.size() - 1; i >= 0; --i) {
|
||||
auto& element = m_elements[i];
|
||||
if (element.local_name() == tag_name)
|
||||
return &element;
|
||||
return { &element, i };
|
||||
}
|
||||
return nullptr;
|
||||
return { nullptr, -1 };
|
||||
}
|
||||
|
||||
DOM::Element* StackOfOpenElements::element_before(const DOM::Element& target)
|
||||
|
|
|
@ -65,7 +65,11 @@ public:
|
|||
|
||||
DOM::Element* topmost_special_node_below(const DOM::Element&);
|
||||
|
||||
DOM::Element* last_element_with_tag_name(const FlyString&);
|
||||
struct LastElementResult {
|
||||
DOM::Element* element;
|
||||
ssize_t index;
|
||||
};
|
||||
LastElementResult last_element_with_tag_name(const FlyString&);
|
||||
DOM::Element* element_before(const DOM::Element&);
|
||||
|
||||
private:
|
||||
|
|
27
Libraries/LibWeb/Tests/HTML/HTMLTemplateElement.js
Normal file
27
Libraries/LibWeb/Tests/HTML/HTMLTemplateElement.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
loadPage("file:///home/anon/web-tests/Pages/Template.html");
|
||||
|
||||
afterInitialPageLoad(() => {
|
||||
test("Basic functionality", () => {
|
||||
const template = document.getElementById("template");
|
||||
expect(template).not.toBeNull();
|
||||
|
||||
// The contents of a template element are not children of the actual element.
|
||||
// The document fragment is not a child of the element either.
|
||||
expect(template.firstChild).toBeNull();
|
||||
|
||||
// FIXME: Add this in once DocumentFragment's constructor is implemented.
|
||||
//expect(template.content).toBeInstanceOf(DocumentFragment);
|
||||
expect(template.content.nodeName).toBe("#document-fragment");
|
||||
|
||||
const templateDiv = template.content.getElementById("templatediv");
|
||||
expect(templateDiv.nodeName).toBe("div");
|
||||
expect(templateDiv.textContent).toBe("Hello template!");
|
||||
});
|
||||
|
||||
test("Templates are inert (e.g. scripts won't run)", () => {
|
||||
// The page has a template element with a script element in it.
|
||||
// Templates are inert, for example, they won't run scripts.
|
||||
// That script will set "templateScriptRan" to true if it ran.
|
||||
expect(window.templateScriptRan).toBeUndefined();
|
||||
});
|
||||
});
|
14
Libraries/LibWeb/Tests/Pages/Template.html
Normal file
14
Libraries/LibWeb/Tests/Pages/Template.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
<template id="template">
|
||||
<div id="templatediv">Hello template!</div>
|
||||
<script>
|
||||
// I shouldn't be run.
|
||||
window.templateScriptRan = true;
|
||||
</script>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Reference in a new issue