Element.cpp 13 KB

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