Element.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. /*
  2. * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <AK/AnyOf.h>
  7. #include <AK/StringBuilder.h>
  8. #include <LibWeb/CSS/Parser/Parser.h>
  9. #include <LibWeb/CSS/PropertyID.h>
  10. #include <LibWeb/CSS/StyleInvalidator.h>
  11. #include <LibWeb/DOM/DOMException.h>
  12. #include <LibWeb/DOM/Document.h>
  13. #include <LibWeb/DOM/Element.h>
  14. #include <LibWeb/DOM/ExceptionOr.h>
  15. #include <LibWeb/DOM/HTMLCollection.h>
  16. #include <LibWeb/DOM/ShadowRoot.h>
  17. #include <LibWeb/DOM/Text.h>
  18. #include <LibWeb/HTML/Parser/HTMLDocumentParser.h>
  19. #include <LibWeb/Layout/BlockBox.h>
  20. #include <LibWeb/Layout/InlineNode.h>
  21. #include <LibWeb/Layout/ListItemBox.h>
  22. #include <LibWeb/Layout/TableBox.h>
  23. #include <LibWeb/Layout/TableCellBox.h>
  24. #include <LibWeb/Layout/TableRowBox.h>
  25. #include <LibWeb/Layout/TableRowGroupBox.h>
  26. #include <LibWeb/Layout/TreeBuilder.h>
  27. #include <LibWeb/Namespace.h>
  28. namespace Web::DOM {
  29. Element::Element(Document& document, QualifiedName qualified_name)
  30. : ParentNode(document, NodeType::ELEMENT_NODE)
  31. , m_qualified_name(move(qualified_name))
  32. {
  33. make_html_uppercased_qualified_name();
  34. }
  35. Element::~Element()
  36. {
  37. }
  38. Attribute* Element::find_attribute(const FlyString& name)
  39. {
  40. for (auto& attribute : m_attributes) {
  41. if (attribute.name() == name)
  42. return &attribute;
  43. }
  44. return nullptr;
  45. }
  46. const Attribute* Element::find_attribute(const FlyString& name) const
  47. {
  48. for (auto& attribute : m_attributes) {
  49. if (attribute.name() == name)
  50. return &attribute;
  51. }
  52. return nullptr;
  53. }
  54. String Element::attribute(const FlyString& name) const
  55. {
  56. if (auto* attribute = find_attribute(name))
  57. return attribute->value();
  58. return {};
  59. }
  60. ExceptionOr<void> Element::set_attribute(const FlyString& name, const String& value)
  61. {
  62. // FIXME: Proper name validation
  63. if (name.is_empty())
  64. return InvalidCharacterError::create("Attribute name must not be empty");
  65. CSS::StyleInvalidator style_invalidator(document());
  66. if (auto* attribute = find_attribute(name))
  67. attribute->set_value(value);
  68. else
  69. m_attributes.empend(name, value);
  70. parse_attribute(name, value);
  71. return {};
  72. }
  73. void Element::remove_attribute(const FlyString& name)
  74. {
  75. CSS::StyleInvalidator style_invalidator(document());
  76. m_attributes.remove_first_matching([&](auto& attribute) { return attribute.name() == name; });
  77. }
  78. bool Element::has_class(const FlyString& class_name, CaseSensitivity case_sensitivity) const
  79. {
  80. return any_of(m_classes, [&](auto& it) {
  81. return case_sensitivity == CaseSensitivity::CaseSensitive
  82. ? it == class_name
  83. : it.to_lowercase() == class_name.to_lowercase();
  84. });
  85. }
  86. RefPtr<Layout::Node> Element::create_layout_node()
  87. {
  88. auto style = document().style_resolver().resolve_style(*this);
  89. const_cast<Element&>(*this).m_specified_css_values = style;
  90. auto display = style->display();
  91. if (display == CSS::Display::None)
  92. return nullptr;
  93. if (local_name() == "noscript" && document().is_scripting_enabled())
  94. return nullptr;
  95. switch (display) {
  96. case CSS::Display::None:
  97. VERIFY_NOT_REACHED();
  98. break;
  99. case CSS::Display::Block:
  100. return adopt_ref(*new Layout::BlockBox(document(), this, move(style)));
  101. case CSS::Display::Inline:
  102. if (style->float_().value_or(CSS::Float::None) != CSS::Float::None)
  103. return adopt_ref(*new Layout::BlockBox(document(), this, move(style)));
  104. return adopt_ref(*new Layout::InlineNode(document(), *this, move(style)));
  105. case CSS::Display::ListItem:
  106. return adopt_ref(*new Layout::ListItemBox(document(), *this, move(style)));
  107. case CSS::Display::Table:
  108. return adopt_ref(*new Layout::TableBox(document(), this, move(style)));
  109. case CSS::Display::TableRow:
  110. return adopt_ref(*new Layout::TableRowBox(document(), this, move(style)));
  111. case CSS::Display::TableCell:
  112. return adopt_ref(*new Layout::TableCellBox(document(), this, move(style)));
  113. case CSS::Display::TableRowGroup:
  114. case CSS::Display::TableHeaderGroup:
  115. case CSS::Display::TableFooterGroup:
  116. return adopt_ref(*new Layout::TableRowGroupBox(document(), *this, move(style)));
  117. case CSS::Display::InlineBlock: {
  118. auto inline_block = adopt_ref(*new Layout::BlockBox(document(), this, move(style)));
  119. inline_block->set_inline(true);
  120. return inline_block;
  121. }
  122. case CSS::Display::Flex:
  123. return adopt_ref(*new Layout::BlockBox(document(), this, move(style)));
  124. case CSS::Display::TableColumn:
  125. case CSS::Display::TableColumnGroup:
  126. case CSS::Display::TableCaption:
  127. // FIXME: This is just an incorrect placeholder until we improve table layout support.
  128. return adopt_ref(*new Layout::BlockBox(document(), this, move(style)));
  129. }
  130. VERIFY_NOT_REACHED();
  131. }
  132. void Element::parse_attribute(const FlyString& name, const String& value)
  133. {
  134. if (name == HTML::AttributeNames::class_) {
  135. auto new_classes = value.split_view(' ');
  136. m_classes.clear();
  137. m_classes.ensure_capacity(new_classes.size());
  138. for (auto& new_class : new_classes) {
  139. m_classes.unchecked_append(new_class);
  140. }
  141. } else if (name == HTML::AttributeNames::style) {
  142. m_inline_style = parse_css_declaration(CSS::ParsingContext(document()), value);
  143. set_needs_style_update(true);
  144. }
  145. }
  146. enum class StyleDifference {
  147. None,
  148. NeedsRepaint,
  149. NeedsRelayout,
  150. };
  151. static StyleDifference compute_style_difference(const CSS::StyleProperties& old_style, const CSS::StyleProperties& new_style, const Document& document)
  152. {
  153. if (old_style == new_style)
  154. return StyleDifference::None;
  155. bool needs_repaint = false;
  156. bool needs_relayout = false;
  157. if (new_style.display() != old_style.display())
  158. needs_relayout = true;
  159. if (new_style.color_or_fallback(CSS::PropertyID::Color, document, Color::Black) != old_style.color_or_fallback(CSS::PropertyID::Color, document, Color::Black))
  160. needs_repaint = true;
  161. else if (new_style.color_or_fallback(CSS::PropertyID::BackgroundColor, document, Color::Black) != old_style.color_or_fallback(CSS::PropertyID::BackgroundColor, document, Color::Black))
  162. needs_repaint = true;
  163. if (needs_relayout)
  164. return StyleDifference::NeedsRelayout;
  165. if (needs_repaint)
  166. return StyleDifference::NeedsRepaint;
  167. return StyleDifference::None;
  168. }
  169. void Element::recompute_style()
  170. {
  171. set_needs_style_update(false);
  172. VERIFY(parent());
  173. auto old_specified_css_values = m_specified_css_values;
  174. auto new_specified_css_values = document().style_resolver().resolve_style(*this);
  175. m_specified_css_values = new_specified_css_values;
  176. if (!layout_node()) {
  177. if (new_specified_css_values->display() == CSS::Display::None)
  178. return;
  179. // We need a new layout tree here!
  180. Layout::TreeBuilder tree_builder;
  181. tree_builder.build(*this);
  182. return;
  183. }
  184. auto diff = StyleDifference::NeedsRelayout;
  185. if (old_specified_css_values)
  186. diff = compute_style_difference(*old_specified_css_values, *new_specified_css_values, document());
  187. if (diff == StyleDifference::None)
  188. return;
  189. layout_node()->apply_style(*new_specified_css_values);
  190. if (diff == StyleDifference::NeedsRelayout) {
  191. document().schedule_forced_layout();
  192. return;
  193. }
  194. if (diff == StyleDifference::NeedsRepaint) {
  195. layout_node()->set_needs_display();
  196. }
  197. }
  198. NonnullRefPtr<CSS::StyleProperties> Element::computed_style()
  199. {
  200. // FIXME: This implementation is not doing anything it's supposed to.
  201. auto properties = m_specified_css_values->clone();
  202. if (layout_node() && layout_node()->has_style()) {
  203. CSS::PropertyID box_model_metrics[] = {
  204. CSS::PropertyID::MarginTop,
  205. CSS::PropertyID::MarginBottom,
  206. CSS::PropertyID::MarginLeft,
  207. CSS::PropertyID::MarginRight,
  208. CSS::PropertyID::PaddingTop,
  209. CSS::PropertyID::PaddingBottom,
  210. CSS::PropertyID::PaddingLeft,
  211. CSS::PropertyID::PaddingRight,
  212. CSS::PropertyID::BorderTopWidth,
  213. CSS::PropertyID::BorderBottomWidth,
  214. CSS::PropertyID::BorderLeftWidth,
  215. CSS::PropertyID::BorderRightWidth,
  216. };
  217. for (CSS::PropertyID id : box_model_metrics) {
  218. auto prop = m_specified_css_values->property(id);
  219. if (prop.has_value())
  220. properties->set_property(id, prop.value());
  221. }
  222. }
  223. return properties;
  224. }
  225. void Element::set_inner_html(StringView markup)
  226. {
  227. auto new_children = HTML::HTMLDocumentParser::parse_html_fragment(*this, markup);
  228. remove_all_children();
  229. while (!new_children.is_empty()) {
  230. append_child(new_children.take_first());
  231. }
  232. set_needs_style_update(true);
  233. document().invalidate_layout();
  234. }
  235. String Element::inner_html() const
  236. {
  237. auto escape_string = [](const StringView& string, bool attribute_mode) -> String {
  238. // https://html.spec.whatwg.org/multipage/parsing.html#escapingString
  239. StringBuilder builder;
  240. for (auto& ch : string) {
  241. if (ch == '&')
  242. builder.append("&amp;");
  243. // FIXME: also replace U+00A0 NO-BREAK SPACE with &nbsp;
  244. else if (ch == '"' && attribute_mode)
  245. builder.append("&quot;");
  246. else if (ch == '<' && !attribute_mode)
  247. builder.append("&lt;");
  248. else if (ch == '>' && !attribute_mode)
  249. builder.append("&gt;");
  250. else
  251. builder.append(ch);
  252. }
  253. return builder.to_string();
  254. };
  255. StringBuilder builder;
  256. Function<void(const Node&)> recurse = [&](auto& node) {
  257. for (auto* child = node.first_child(); child; child = child->next_sibling()) {
  258. if (child->is_element()) {
  259. auto& element = verify_cast<Element>(*child);
  260. builder.append('<');
  261. builder.append(element.local_name());
  262. element.for_each_attribute([&](auto& name, auto& value) {
  263. builder.append(' ');
  264. builder.append(name);
  265. builder.append('=');
  266. builder.append('"');
  267. builder.append(escape_string(value, true));
  268. builder.append('"');
  269. });
  270. builder.append('>');
  271. recurse(*child);
  272. // FIXME: This should be skipped for void elements
  273. builder.append("</");
  274. builder.append(element.local_name());
  275. builder.append('>');
  276. }
  277. if (child->is_text()) {
  278. auto& text = verify_cast<Text>(*child);
  279. builder.append(escape_string(text.data(), false));
  280. }
  281. // FIXME: Also handle Comment, ProcessingInstruction, DocumentType
  282. }
  283. };
  284. recurse(*this);
  285. return builder.to_string();
  286. }
  287. bool Element::is_focused() const
  288. {
  289. return document().focused_element() == this;
  290. }
  291. bool Element::is_active() const
  292. {
  293. return document().active_element() == this;
  294. }
  295. NonnullRefPtr<HTMLCollection> Element::get_elements_by_tag_name(FlyString const& tag_name)
  296. {
  297. // FIXME: Support "*" for tag_name
  298. // https://dom.spec.whatwg.org/#concept-getelementsbytagname
  299. return HTMLCollection::create(*this, [tag_name](Element const& element) {
  300. if (element.namespace_() == Namespace::HTML)
  301. return element.local_name().to_lowercase() == tag_name.to_lowercase();
  302. return element.local_name() == tag_name;
  303. });
  304. }
  305. NonnullRefPtr<HTMLCollection> Element::get_elements_by_class_name(FlyString const& class_name)
  306. {
  307. return HTMLCollection::create(*this, [class_name, quirks_mode = document().in_quirks_mode()](Element const& element) {
  308. return element.has_class(class_name, quirks_mode ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive);
  309. });
  310. }
  311. void Element::set_shadow_root(RefPtr<ShadowRoot> shadow_root)
  312. {
  313. if (m_shadow_root == shadow_root)
  314. return;
  315. m_shadow_root = move(shadow_root);
  316. invalidate_style();
  317. }
  318. NonnullRefPtr<CSS::CSSStyleDeclaration> Element::style_for_bindings()
  319. {
  320. if (!m_inline_style)
  321. m_inline_style = CSS::ElementInlineCSSStyleDeclaration::create(*this);
  322. return *m_inline_style;
  323. }
  324. // https://dom.spec.whatwg.org/#element-html-uppercased-qualified-name
  325. void Element::make_html_uppercased_qualified_name()
  326. {
  327. // This is allowed by the spec: "User agents could optimize qualified name and HTML-uppercased qualified name by storing them in internal slots."
  328. if (namespace_() == Namespace::HTML /* FIXME: and its node document is an HTML document */)
  329. m_html_uppercased_qualified_name = qualified_name().to_uppercase();
  330. else
  331. m_html_uppercased_qualified_name = qualified_name();
  332. }
  333. }