Commands.cpp 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830
  1. /*
  2. * Copyright (c) 2024, Jelle Raaijmakers <jelle@ladybird.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
  7. #include <LibWeb/DOM/Comment.h>
  8. #include <LibWeb/DOM/Document.h>
  9. #include <LibWeb/DOM/DocumentFragment.h>
  10. #include <LibWeb/DOM/ElementFactory.h>
  11. #include <LibWeb/DOM/Range.h>
  12. #include <LibWeb/DOM/Text.h>
  13. #include <LibWeb/Editing/CommandNames.h>
  14. #include <LibWeb/Editing/Commands.h>
  15. #include <LibWeb/Editing/Internal/Algorithms.h>
  16. #include <LibWeb/HTML/HTMLAnchorElement.h>
  17. #include <LibWeb/HTML/HTMLBRElement.h>
  18. #include <LibWeb/HTML/HTMLHRElement.h>
  19. #include <LibWeb/HTML/HTMLImageElement.h>
  20. #include <LibWeb/HTML/HTMLLIElement.h>
  21. #include <LibWeb/HTML/HTMLTableElement.h>
  22. #include <LibWeb/Layout/Node.h>
  23. #include <LibWeb/Namespace.h>
  24. namespace Web::Editing {
  25. // https://w3c.github.io/editing/docs/execCommand/#the-defaultparagraphseparator-command
  26. bool command_default_paragraph_separator_action(DOM::Document& document, String const& input_value)
  27. {
  28. // Let value be converted to ASCII lowercase.
  29. auto value = input_value.to_ascii_lowercase();
  30. // If value is then equal to "p" or "div", set the context object's default single-line
  31. // container name to value, then return true.
  32. if (value == HTML::TagNames::p) {
  33. document.set_default_single_line_container_name(HTML::TagNames::p);
  34. return true;
  35. }
  36. if (value == HTML::TagNames::div) {
  37. document.set_default_single_line_container_name(HTML::TagNames::div);
  38. return true;
  39. }
  40. // Otherwise, return false.
  41. return false;
  42. }
  43. // https://w3c.github.io/editing/docs/execCommand/#the-defaultparagraphseparator-command
  44. String command_default_paragraph_separator_value(DOM::Document const& document)
  45. {
  46. // Return the context object's default single-line container name.
  47. return document.default_single_line_container_name().to_string();
  48. }
  49. // https://w3c.github.io/editing/docs/execCommand/#the-delete-command
  50. bool command_delete_action(DOM::Document& document, String const&)
  51. {
  52. // 1. If the active range is not collapsed, delete the selection and return true.
  53. auto& selection = *document.get_selection();
  54. auto& active_range = *selection.range();
  55. if (!active_range.collapsed()) {
  56. delete_the_selection(selection);
  57. return true;
  58. }
  59. // 2. Canonicalize whitespace at the active range's start.
  60. canonicalize_whitespace(active_range.start());
  61. // 3. Let node and offset be the active range's start node and offset.
  62. GC::Ptr<DOM::Node> node = active_range.start_container();
  63. int offset = active_range.start_offset();
  64. // 4. Repeat the following steps:
  65. GC::Ptr<DOM::Node> offset_minus_one_child;
  66. while (true) {
  67. offset_minus_one_child = node->child_at_index(offset - 1);
  68. // 1. If offset is zero and node's previousSibling is an editable invisible node, remove
  69. // node's previousSibling from its parent.
  70. if (auto* previous_sibling = node->previous_sibling()) {
  71. if (offset == 0 && previous_sibling->is_editable() && is_invisible_node(*previous_sibling)) {
  72. previous_sibling->remove();
  73. continue;
  74. }
  75. }
  76. // 2. Otherwise, if node has a child with index offset − 1 and that child is an editable
  77. // invisible node, remove that child from node, then subtract one from offset.
  78. if (offset_minus_one_child && offset_minus_one_child->is_editable() && is_invisible_node(*offset_minus_one_child)) {
  79. offset_minus_one_child->remove();
  80. --offset;
  81. continue;
  82. }
  83. // 3. Otherwise, if offset is zero and node is an inline node, or if node is an invisible
  84. // node, set offset to the index of node, then set node to its parent.
  85. if ((offset == 0 && is_inline_node(*node)) || is_invisible_node(*node)) {
  86. offset = node->index();
  87. node = *node->parent();
  88. continue;
  89. }
  90. // 4. Otherwise, if node has a child with index offset − 1 and that child is an editable a,
  91. // remove that child from node, preserving its descendants. Then return true.
  92. if (is<HTML::HTMLAnchorElement>(offset_minus_one_child.ptr()) && offset_minus_one_child->is_editable()) {
  93. remove_node_preserving_its_descendants(*offset_minus_one_child);
  94. return true;
  95. }
  96. // 5. Otherwise, if node has a child with index offset − 1 and that child is not a block
  97. // node or a br or an img, set node to that child, then set offset to the length of node.
  98. if (offset_minus_one_child && !is_block_node(*offset_minus_one_child)
  99. && !is<HTML::HTMLBRElement>(*offset_minus_one_child) && !is<HTML::HTMLImageElement>(*offset_minus_one_child)) {
  100. node = *offset_minus_one_child;
  101. offset = node->length();
  102. continue;
  103. }
  104. // 6. Otherwise, break from this loop.
  105. break;
  106. }
  107. // 5. If node is a Text node and offset is not zero, or if node is a block node that has a child
  108. // with index offset − 1 and that child is a br or hr or img:
  109. bool block_node_child_is_relevant_type = false;
  110. if (is_block_node(*node)) {
  111. if (auto* child_node = node->child_at_index(offset - 1)) {
  112. auto& child_element = static_cast<DOM::Element&>(*child_node);
  113. block_node_child_is_relevant_type = child_element.local_name().is_one_of(HTML::TagNames::br, HTML::TagNames::hr, HTML::TagNames::img);
  114. }
  115. }
  116. if ((is<DOM::Text>(*node) && offset != 0) || block_node_child_is_relevant_type) {
  117. // 1. Call collapse(node, offset) on the context object's selection.
  118. MUST(selection.collapse(node, offset));
  119. // 2. Call extend(node, offset − 1) on the context object's selection.
  120. MUST(selection.extend(*node, offset - 1));
  121. // 3. Delete the selection.
  122. delete_the_selection(selection);
  123. // 4. Return true.
  124. return true;
  125. }
  126. // 6. If node is an inline node, return true.
  127. if (is_inline_node(*node))
  128. return true;
  129. // 7. If node is an li or dt or dd and is the first child of its parent, and offset is zero:
  130. auto& node_element = static_cast<DOM::Element&>(*node);
  131. if (offset == 0 && node->index() == 0
  132. && node_element.local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd)) {
  133. // 1. Let items be a list of all lis that are ancestors of node.
  134. auto items = Vector<GC::Ref<DOM::Element>>();
  135. GC::Ptr<DOM::Node> ancestor = node->parent();
  136. while (ancestor) {
  137. if (is<HTML::HTMLLIElement>(*ancestor))
  138. items.append(static_cast<DOM::Element&>(*ancestor));
  139. ancestor = ancestor->parent();
  140. }
  141. // 2. Normalize sublists of each item in items.
  142. for (auto item : items)
  143. normalize_sublists_in_node(*item);
  144. // 3. Record the values of the one-node list consisting of node, and let values be the
  145. // result.
  146. auto values = record_the_values_of_nodes({ *node });
  147. // 4. Split the parent of the one-node list consisting of node.
  148. split_the_parent_of_nodes({ *node });
  149. // 5. Restore the values from values.
  150. restore_the_values_of_nodes(values);
  151. // 6. If node is a dd or dt, and it is not an allowed child of any of its ancestors in the
  152. // same editing host, set the tag name of node to the default single-line container name
  153. // and let node be the result.
  154. if (node_element.local_name().is_one_of(HTML::TagNames::dd, HTML::TagNames::dt)) {
  155. ancestor = node->parent();
  156. bool allowed_child_of_any_ancestor = false;
  157. while (ancestor) {
  158. if (is_in_same_editing_host(*node, *ancestor) && is_allowed_child_of_node(GC::Ref { *node }, GC::Ref { *ancestor })) {
  159. allowed_child_of_any_ancestor = true;
  160. break;
  161. }
  162. ancestor = ancestor->parent();
  163. }
  164. if (!allowed_child_of_any_ancestor)
  165. node = set_the_tag_name(node_element, document.default_single_line_container_name());
  166. }
  167. // 7. Fix disallowed ancestors of node.
  168. fix_disallowed_ancestors_of_node(*node);
  169. // 8. Return true.
  170. return true;
  171. }
  172. // 8. Let start node equal node and let start offset equal offset.
  173. auto start_node = node;
  174. auto start_offset = offset;
  175. // 9. Repeat the following steps:
  176. while (true) {
  177. // AD-HOC: If start node is not a Node, return false. This prevents a crash by dereferencing a null pointer in
  178. // step 1 below. Edits outside of <body> might be prohibited: https://github.com/w3c/editing/issues/405
  179. if (!start_node)
  180. return false;
  181. // 1. If start offset is zero, set start offset to the index of start node and then set
  182. // start node to its parent.
  183. if (start_offset == 0) {
  184. start_offset = start_node->index();
  185. start_node = start_node->parent();
  186. continue;
  187. }
  188. // 2. Otherwise, if start node has an editable invisible child with index start offset minus
  189. // one, remove it from start node and subtract one from start offset.
  190. offset_minus_one_child = start_node->child_at_index(start_offset - 1);
  191. if (offset_minus_one_child && offset_minus_one_child->is_editable() && is_invisible_node(*offset_minus_one_child)) {
  192. offset_minus_one_child->remove();
  193. --start_offset;
  194. continue;
  195. }
  196. // 3. Otherwise, break from this loop.
  197. break;
  198. }
  199. // FIXME: 10. If offset is zero, and node has an editable inclusive ancestor in the same editing host
  200. // that's an indentation element:
  201. if (false) {
  202. // FIXME: 1. Block-extend the range whose start and end are both (node, 0), and let new range be
  203. // the result.
  204. // FIXME: 2. Let node list be a list of nodes, initially empty.
  205. // FIXME: 3. For each node current node contained in new range, append current node to node list if
  206. // the last member of node list (if any) is not an ancestor of current node, and current
  207. // node is editable but has no editable descendants.
  208. // FIXME: 4. Outdent each node in node list.
  209. // 5. Return true.
  210. return true;
  211. }
  212. // 11. If the child of start node with index start offset is a table, return true.
  213. if (is<HTML::HTMLTableElement>(start_node->child_at_index(start_offset)))
  214. return true;
  215. // 12. If start node has a child with index start offset − 1, and that child is a table:
  216. offset_minus_one_child = start_node->child_at_index(start_offset - 1);
  217. if (is<HTML::HTMLTableElement>(offset_minus_one_child.ptr())) {
  218. // 1. Call collapse(start node, start offset − 1) on the context object's selection.
  219. MUST(selection.collapse(start_node, start_offset - 1));
  220. // 2. Call extend(start node, start offset) on the context object's selection.
  221. MUST(selection.extend(*start_node, start_offset));
  222. // 3. Return true.
  223. return true;
  224. }
  225. // 13. If offset is zero; and either the child of start node with index start offset minus one
  226. // is an hr, or the child is a br whose previousSibling is either a br or not an inline
  227. // node:
  228. if (offset == 0 && is<DOM::Element>(offset_minus_one_child.ptr())) {
  229. auto& child_element = static_cast<DOM::Element&>(*offset_minus_one_child);
  230. auto* previous_sibling = child_element.previous_sibling();
  231. if (is<HTML::HTMLHRElement>(child_element)
  232. || (is<HTML::HTMLBRElement>(child_element) && previous_sibling && (is<HTML::HTMLBRElement>(*previous_sibling) || !is_inline_node(*previous_sibling)))) {
  233. // 1. Call collapse(start node, start offset − 1) on the context object's selection.
  234. MUST(selection.collapse(start_node, start_offset - 1));
  235. // 2. Call extend(start node, start offset) on the context object's selection.
  236. MUST(selection.extend(*start_node, start_offset));
  237. // 3. Delete the selection.
  238. delete_the_selection(selection);
  239. // 4. Call collapse(node, offset) on the selection.
  240. MUST(selection.collapse(node, offset));
  241. // 5. Return true.
  242. return true;
  243. }
  244. }
  245. // 14. If the child of start node with index start offset is an li or dt or dd, and that child's
  246. // firstChild is an inline node, and start offset is not zero:
  247. // NOTE: step 9 above guarantees start_offset cannot be 0 here.
  248. auto is_li_dt_or_dd = [](DOM::Element const& node) {
  249. return node.local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd);
  250. };
  251. auto* start_offset_child = start_node->child_at_index(start_offset);
  252. if (is<DOM::Element>(start_offset_child) && is_li_dt_or_dd(static_cast<DOM::Element&>(*start_offset_child))
  253. && start_offset_child->has_children() && is_inline_node(*start_offset_child->first_child())) {
  254. // 1. Let previous item be the child of start node with index start offset minus one.
  255. GC::Ref<DOM::Node> previous_item = *start_node->child_at_index(start_offset - 1);
  256. // 2. If previous item's lastChild is an inline node other than a br, call
  257. // createElement("br") on the context object and append the result as the last child of
  258. // previous item.
  259. GC::Ptr<DOM::Node> previous_item_last_child = previous_item->last_child();
  260. if (previous_item_last_child && is_inline_node(*previous_item_last_child) && !is<HTML::HTMLBRElement>(*previous_item_last_child)) {
  261. auto br_element = MUST(DOM::create_element(previous_item->document(), HTML::TagNames::br, Namespace::HTML));
  262. MUST(previous_item->append_child(br_element));
  263. }
  264. // 3. If previous item's lastChild is an inline node, call createElement("br") on the
  265. // context object and append the result as the last child of previous item.
  266. if (previous_item_last_child && is_inline_node(*previous_item_last_child)) {
  267. auto br_element = MUST(DOM::create_element(previous_item->document(), HTML::TagNames::br, Namespace::HTML));
  268. MUST(previous_item->append_child(br_element));
  269. }
  270. }
  271. // 15. If start node's child with index start offset is an li or dt or dd, and that child's
  272. // previousSibling is also an li or dt or dd:
  273. if (is<DOM::Element>(start_offset_child) && is_li_dt_or_dd(static_cast<DOM::Element&>(*start_offset_child))
  274. && is<DOM::Element>(start_offset_child->previous_sibling())
  275. && is_li_dt_or_dd(static_cast<DOM::Element&>(*start_offset_child->previous_sibling()))) {
  276. // 1. Call cloneRange() on the active range, and let original range be the result.
  277. auto original_range = active_range.clone_range();
  278. // 2. Set start node to its child with index start offset − 1.
  279. start_node = start_node->child_at_index(start_offset - 1);
  280. // 3. Set start offset to start node's length.
  281. start_offset = start_node->length();
  282. // 4. Set node to start node's nextSibling.
  283. node = start_node->next_sibling();
  284. // 5. Call collapse(start node, start offset) on the context object's selection.
  285. MUST(selection.collapse(start_node, start_offset));
  286. // 6. Call extend(node, 0) on the context object's selection.
  287. MUST(selection.extend(*node, 0));
  288. // 7. Delete the selection.
  289. delete_the_selection(selection);
  290. // 8. Call removeAllRanges() on the context object's selection.
  291. selection.remove_all_ranges();
  292. // 9. Call addRange(original range) on the context object's selection.
  293. selection.add_range(original_range);
  294. // 10. Return true.
  295. return true;
  296. }
  297. // 16. While start node has a child with index start offset minus one:
  298. while (start_node->child_at_index(start_offset - 1) != nullptr) {
  299. // 1. If start node's child with index start offset minus one is editable and invisible,
  300. // remove it from start node, then subtract one from start offset.
  301. offset_minus_one_child = start_node->child_at_index(start_offset - 1);
  302. if (offset_minus_one_child->is_editable() && is_invisible_node(*offset_minus_one_child)) {
  303. offset_minus_one_child->remove();
  304. --start_offset;
  305. }
  306. // 2. Otherwise, set start node to its child with index start offset minus one, then set
  307. // start offset to the length of start node.
  308. else {
  309. start_node = *offset_minus_one_child;
  310. start_offset = start_node->length();
  311. }
  312. }
  313. // 17. Call collapse(start node, start offset) on the context object's selection.
  314. MUST(selection.collapse(start_node, start_offset));
  315. // 18. Call extend(node, offset) on the context object's selection.
  316. MUST(selection.extend(*node, offset));
  317. // 19. Delete the selection, with direction "backward".
  318. delete_the_selection(selection, true, true, Selection::Direction::Backwards);
  319. // 20. Return true.
  320. return true;
  321. }
  322. // https://w3c.github.io/editing/docs/execCommand/#the-insertlinebreak-command
  323. bool command_insert_linebreak_action(DOM::Document& document, String const&)
  324. {
  325. // 1. Delete the selection, with strip wrappers false.
  326. auto& selection = *document.get_selection();
  327. delete_the_selection(selection, true, false);
  328. // 2. If the active range's start node is neither editable nor an editing host, return true.
  329. auto& active_range = *selection.range();
  330. auto start_node = active_range.start_container();
  331. if (!start_node->is_editable_or_editing_host())
  332. return true;
  333. // 3. If the active range's start node is an Element, and "br" is not an allowed child of it, return true.
  334. if (is<DOM::Element>(*start_node) && !is_allowed_child_of_node(HTML::TagNames::br, start_node))
  335. return true;
  336. // 4. If the active range's start node is not an Element, and "br" is not an allowed child of the active range's
  337. // start node's parent, return true.
  338. if (!is<DOM::Element>(*start_node) && start_node->parent() && !is_allowed_child_of_node(HTML::TagNames::br, GC::Ref { *start_node->parent() }))
  339. return true;
  340. // 5. If the active range's start node is a Text node and its start offset is zero, call collapse() on the context
  341. // object's selection, with first argument equal to the active range's start node's parent and second argument
  342. // equal to the active range's start node's index.
  343. if (is<DOM::Text>(*start_node) && active_range.start_offset() == 0)
  344. MUST(selection.collapse(start_node->parent(), start_node->index()));
  345. // 6. If the active range's start node is a Text node and its start offset is the length of its start node, call
  346. // collapse() on the context object's selection, with first argument equal to the active range's start node's
  347. // parent and second argument equal to one plus the active range's start node's index.
  348. if (is<DOM::Text>(*start_node) && active_range.start_offset() == start_node->length())
  349. MUST(selection.collapse(start_node->parent(), start_node->index() + 1));
  350. // AD-HOC: If the active range's start node is a Text node and its resolved value for "white-space" is one of "pre",
  351. // "pre-line" or "pre-wrap":
  352. // * Insert a newline (\n) character at the active range's start offset;
  353. // * Collapse the selection with active range's start node as the first argument and one plus active range's
  354. // start offset as the second argument
  355. // * Insert another newline (\n) character if the active range's start offset is equal to the length of the
  356. // active range's start node.
  357. // * Return true.
  358. if (is<DOM::Text>(*start_node)) {
  359. auto& text_node = static_cast<DOM::Text&>(*start_node);
  360. auto resolved_white_space = resolved_keyword(*start_node, CSS::PropertyID::WhiteSpace);
  361. if (resolved_white_space.has_value()
  362. && first_is_one_of(resolved_white_space.value(), CSS::Keyword::Pre, CSS::Keyword::PreLine, CSS::Keyword::PreWrap)) {
  363. MUST(text_node.insert_data(active_range.start_offset(), "\n"_string));
  364. MUST(selection.collapse(start_node, active_range.start_offset() + 1));
  365. if (selection.range()->start_offset() == start_node->length())
  366. MUST(text_node.insert_data(active_range.start_offset(), "\n"_string));
  367. return true;
  368. }
  369. }
  370. // 7. Let br be the result of calling createElement("br") on the context object.
  371. auto br = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
  372. // 8. Call insertNode(br) on the active range.
  373. MUST(active_range.insert_node(br));
  374. // 9. Call collapse() on the context object's selection, with br's parent as the first argument and one plus br's
  375. // index as the second argument.
  376. MUST(selection.collapse(br->parent(), br->index() + 1));
  377. // 10. If br is a collapsed line break, call createElement("br") on the context object and let extra br be the
  378. // result, then call insertNode(extra br) on the active range.
  379. if (is_collapsed_line_break(br)) {
  380. auto extra_br = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
  381. MUST(active_range.insert_node(extra_br));
  382. }
  383. // 11. Return true.
  384. return true;
  385. }
  386. // https://w3c.github.io/editing/docs/execCommand/#the-insertparagraph-command
  387. bool command_insert_paragraph_action(DOM::Document& document, String const&)
  388. {
  389. // 1. Delete the selection.
  390. auto& selection = *document.get_selection();
  391. delete_the_selection(selection);
  392. // 2. If the active range's start node is neither editable nor an editing host, return true.
  393. GC::Ref<DOM::Range> active_range = *selection.range();
  394. GC::Ptr<DOM::Node> node = active_range->start_container();
  395. if (!node->is_editable_or_editing_host())
  396. return true;
  397. // 3. Let node and offset be the active range's start node and offset.
  398. // NOTE: node is set in step 2
  399. auto offset = active_range->start_offset();
  400. // 4. If node is a Text node, and offset is neither 0 nor the length of node, call splitText(offset) on node.
  401. if (is<DOM::Text>(*node) && offset != 0 && offset != node->length())
  402. MUST(static_cast<DOM::Text&>(*node).split_text(offset));
  403. // 5. If node is a Text node and offset is its length, set offset to one plus the index of node, then set node to
  404. // its parent.
  405. if (is<DOM::Text>(*node) && offset == node->length()) {
  406. offset = node->index() + 1;
  407. node = node->parent();
  408. }
  409. // 6. If node is a Text or Comment node, set offset to the index of node, then set node to its parent.
  410. if (is<DOM::Text>(*node) || is<DOM::Comment>(*node)) {
  411. offset = node->index();
  412. node = node->parent();
  413. }
  414. // 7. Call collapse(node, offset) on the context object's selection.
  415. MUST(selection.collapse(node, offset));
  416. active_range = *selection.range();
  417. // 8. Let container equal node.
  418. auto container = node;
  419. // 9. While container is not a single-line container, and container's parent is editable and in the same editing
  420. // host as node, set container to its parent.
  421. while (!is_single_line_container(*container)) {
  422. auto container_parent = container->parent();
  423. if (!container_parent->is_editable() || !is_in_same_editing_host(*node, *container_parent))
  424. break;
  425. container = container_parent;
  426. }
  427. // 10. If container is an editable single-line container in the same editing host as node, and its local name is "p"
  428. // or "div":
  429. if (container->is_editable() && is_single_line_container(*container) && is_in_same_editing_host(*container, *node)
  430. && is<DOM::Element>(*container)
  431. && static_cast<DOM::Element&>(*container).local_name().is_one_of(HTML::TagNames::p, HTML::TagNames::div)) {
  432. // 1. Let outer container equal container.
  433. auto outer_container = container;
  434. // 2. While outer container is not a dd or dt or li, and outer container's parent is editable, set outer
  435. // container to its parent.
  436. auto is_li_dt_or_dd = [](DOM::Element const& node) {
  437. return node.local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd);
  438. };
  439. while (!is<DOM::Element>(*outer_container) || !is_li_dt_or_dd(static_cast<DOM::Element&>(*outer_container))) {
  440. auto outer_container_parent = outer_container->parent();
  441. if (!outer_container_parent->is_editable())
  442. break;
  443. outer_container = outer_container_parent;
  444. }
  445. // 3. If outer container is a dd or dt or li, set container to outer container.
  446. if (is<DOM::Element>(*outer_container) && is_li_dt_or_dd(static_cast<DOM::Element&>(*outer_container)))
  447. container = outer_container;
  448. }
  449. // 11. If container is not editable or not in the same editing host as node or is not a single-line container:
  450. if (!container->is_editable() || !is_in_same_editing_host(*container, *node) || !is_single_line_container(*container)) {
  451. // 1. Let tag be the default single-line container name.
  452. auto tag = document.default_single_line_container_name();
  453. // 2. Block-extend the active range, and let new range be the result.
  454. auto new_range = block_extend_a_range(active_range);
  455. // 3. Let node list be a list of nodes, initially empty.
  456. Vector<GC::Ref<DOM::Node>> node_list;
  457. // 4. Append to node list the first node in tree order that is contained in new range and is an allowed child of
  458. // "p", if any.
  459. new_range->start_container()->for_each_in_inclusive_subtree([&](DOM::Node& node) {
  460. if (is_allowed_child_of_node(GC::Ref { node }, HTML::TagNames::p) && new_range->contains_node(node)) {
  461. node_list.append(node);
  462. return TraversalDecision::Break;
  463. }
  464. return TraversalDecision::Continue;
  465. });
  466. // 5. If node list is empty:
  467. if (node_list.is_empty()) {
  468. // 1. If tag is not an allowed child of the active range's start node, return true.
  469. if (!is_allowed_child_of_node(tag, active_range->start_container()))
  470. return true;
  471. // 2. Set container to the result of calling createElement(tag) on the context object.
  472. container = MUST(DOM::create_element(document, tag, Namespace::HTML));
  473. // 3. Call insertNode(container) on the active range.
  474. MUST(active_range->insert_node(*container));
  475. // 4. Call createElement("br") on the context object, and append the result as the last child of container.
  476. MUST(container->append_child(MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML))));
  477. // 5. Call collapse(container, 0) on the context object's selection.
  478. MUST(selection.collapse(container, 0));
  479. // 6. Return true.
  480. return true;
  481. }
  482. // 6. While the nextSibling of the last member of node list is not null and is an allowed child of "p", append
  483. // it to node list.
  484. auto next_sibling = node_list.last()->next_sibling();
  485. while (next_sibling && is_allowed_child_of_node(GC::Ref { *next_sibling }, HTML::TagNames::p)) {
  486. node_list.append(*next_sibling);
  487. next_sibling = next_sibling->next_sibling();
  488. }
  489. // 7. Wrap node list, with sibling criteria returning false and new parent instructions returning the result of
  490. // calling createElement(tag) on the context object. Set container to the result.
  491. wrap(
  492. node_list,
  493. [](auto) { return false; },
  494. [&] { return MUST(DOM::create_element(document, tag, Namespace::HTML)); });
  495. }
  496. // 12. If container's local name is "address", "listing", or "pre":
  497. if (is<DOM::Element>(*container)
  498. && static_cast<DOM::Element&>(*container)
  499. .local_name()
  500. .is_one_of(HTML::TagNames::address, HTML::TagNames::listing, HTML::TagNames::pre)) {
  501. // 1. Let br be the result of calling createElement("br") on the context object.
  502. auto br = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
  503. // 2. Call insertNode(br) on the active range.
  504. MUST(active_range->insert_node(br));
  505. // 3. Call collapse(node, offset + 1) on the context object's selection.
  506. MUST(selection.collapse(node, offset + 1));
  507. active_range = *selection.range();
  508. // 4. If br is the last descendant of container, let br be the result of calling createElement("br") on the
  509. // context object, then call insertNode(br) on the active range.
  510. GC::Ptr<DOM::Node> last_descendant = container->last_child();
  511. while (last_descendant->has_children())
  512. last_descendant = last_descendant->last_child();
  513. if (br == last_descendant) {
  514. br = MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML));
  515. MUST(active_range->insert_node(br));
  516. }
  517. // 5. Return true.
  518. return true;
  519. }
  520. // 13. If container's local name is "li", "dt", or "dd"; and either it has no children or it has a single child and
  521. // that child is a br:
  522. if (is<DOM::Element>(*container)
  523. && static_cast<DOM::Element&>(*container).local_name().is_one_of(HTML::TagNames::li, HTML::TagNames::dt, HTML::TagNames::dd)
  524. && (!container->has_children() || (container->child_count() == 1 && is<HTML::HTMLBRElement>(container->first_child())))) {
  525. // 1. Split the parent of the one-node list consisting of container.
  526. split_the_parent_of_nodes({ *container });
  527. // 2. If container has no children, call createElement("br") on the context object and append the result as the
  528. // last child of container.
  529. if (!container->has_children())
  530. MUST(container->append_child(MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML))));
  531. // 3. If container is a dd or dt, and it is not an allowed child of any of its ancestors in the same editing
  532. // host, set the tag name of container to the default single-line container name and let container be the
  533. // result.
  534. if (static_cast<DOM::Element&>(*container).local_name().is_one_of(HTML::TagNames::dd, HTML::TagNames::dt)) {
  535. bool allowed_child_of_any_ancestor = false;
  536. GC::Ptr<DOM::Node> ancestor = container->parent();
  537. while (ancestor) {
  538. if (is_allowed_child_of_node(GC::Ref { *container }, GC::Ref { *ancestor })
  539. && is_in_same_editing_host(*container, *ancestor)) {
  540. allowed_child_of_any_ancestor = true;
  541. break;
  542. }
  543. ancestor = ancestor->parent();
  544. }
  545. if (!allowed_child_of_any_ancestor)
  546. container = set_the_tag_name(static_cast<DOM::Element&>(*container), document.default_single_line_container_name());
  547. }
  548. // 4. Fix disallowed ancestors of container.
  549. fix_disallowed_ancestors_of_node(*container);
  550. // 5. Return true.
  551. return true;
  552. }
  553. // 14. Let new line range be a new range whose start is the same as the active range's, and whose end is (container,
  554. // length of container).
  555. auto new_line_range = DOM::Range::create(active_range->start_container(), active_range->start_offset(), *container, container->length());
  556. // 15. While new line range's start offset is zero and its start node is not a prohibited paragraph child, set its
  557. // start to (parent of start node, index of start node).
  558. GC::Ptr<DOM::Node> start_container = new_line_range->start_container();
  559. while (start_container->parent() && new_line_range->start_offset() == 0 && !is_prohibited_paragraph_child(*start_container)) {
  560. MUST(new_line_range->set_start(*start_container->parent(), start_container->index()));
  561. start_container = start_container->parent();
  562. }
  563. // 16. While new line range's start offset is the length of its start node and its start node is not a prohibited
  564. // paragraph child, set its start to (parent of start node, 1 + index of start node).
  565. start_container = new_line_range->start_container();
  566. while (start_container->parent() && new_line_range->start_offset() == start_container->length()
  567. && !is_prohibited_paragraph_child(*start_container)) {
  568. MUST(new_line_range->set_start(*start_container->parent(), start_container->index() + 1));
  569. start_container = start_container->parent();
  570. }
  571. // 17. Let end of line be true if new line range contains either nothing or a single br, and false otherwise.
  572. auto end_of_line = new_line_range->collapsed()
  573. || ((new_line_range->start_container() == new_line_range->end_container() && new_line_range->start_offset() == new_line_range->end_offset() - 1)
  574. && is<HTML::HTMLBRElement>(*new_line_range->start_container()));
  575. auto& container_element = verify_cast<DOM::Element>(*container);
  576. auto new_container_name = [&] -> FlyString {
  577. // 18. If the local name of container is "h1", "h2", "h3", "h4", "h5", or "h6", and end of line is true, let new
  578. // container name be the default single-line container name.
  579. if (end_of_line && is_heading(container_element.local_name()))
  580. return document.default_single_line_container_name();
  581. // 19. Otherwise, if the local name of container is "dt" and end of line is true, let new container name be "dd".
  582. if (container_element.local_name() == HTML::TagNames::dt && end_of_line)
  583. return HTML::TagNames::dd;
  584. // 20. Otherwise, if the local name of container is "dd" and end of line is true, let new container name be "dt".
  585. if (container_element.local_name() == HTML::TagNames::dd && end_of_line)
  586. return HTML::TagNames::dt;
  587. // 21. Otherwise, let new container name be the local name of container.
  588. return container_element.local_name();
  589. }();
  590. // 22. Let new container be the result of calling createElement(new container name) on the context object.
  591. auto new_container = MUST(DOM::create_element(document, new_container_name, Namespace::HTML));
  592. // 23. Copy all attributes of container to new container.
  593. container_element.for_each_attribute([&new_container](FlyString const& name, String const& value) {
  594. MUST(new_container->set_attribute(name, value));
  595. });
  596. // 24. If new container has an id attribute, unset it.
  597. if (new_container->has_attribute(HTML::AttributeNames::id))
  598. new_container->remove_attribute(HTML::AttributeNames::id);
  599. // 25. Insert new container into the parent of container immediately after container.
  600. container->parent()->insert_before(*new_container, container->next_sibling());
  601. // 26. Let contained nodes be all nodes contained in new line range.
  602. // FIXME: this is probably wildly inefficient
  603. Vector<GC::Ref<DOM::Node>> contained_nodes;
  604. auto common_ancestor = new_line_range->common_ancestor_container();
  605. common_ancestor->for_each_in_subtree([&](GC::Ref<DOM::Node> child_node) {
  606. if (!new_line_range->contains_node(child_node))
  607. return TraversalDecision::SkipChildrenAndContinue;
  608. contained_nodes.append(child_node);
  609. return TraversalDecision::Continue;
  610. });
  611. // 27. Let frag be the result of calling extractContents() on new line range.
  612. auto frag = MUST(new_line_range->extract_contents());
  613. // 28. Unset the id attribute (if any) of each Element descendant of frag that is not in contained nodes.
  614. frag->for_each_in_subtree_of_type<DOM::Element>([&contained_nodes](GC::Ref<DOM::Element> descendant) {
  615. if (!contained_nodes.contains_slow(descendant))
  616. descendant->remove_attribute(HTML::AttributeNames::id);
  617. return TraversalDecision::Continue;
  618. });
  619. // 29. Call appendChild(frag) on new container.
  620. MUST(new_container->append_child(frag));
  621. // 30. While container's lastChild is a prohibited paragraph child, set container to its lastChild.
  622. while (container->last_child() && is_prohibited_paragraph_child(*container->last_child()))
  623. container = container->last_child();
  624. // 31. While new container's lastChild is a prohibited paragraph child, set new container to its lastChild.
  625. while (new_container->last_child() && is_prohibited_paragraph_child(*new_container->last_child())) {
  626. // NOTE: is_prohibited_paragraph_child() ensures that last_child() is an HTML::HTMLElement
  627. new_container = static_cast<HTML::HTMLElement&>(*new_container->last_child());
  628. }
  629. // 32. If container has no visible children, call createElement("br") on the context object, and append the result
  630. // as the last child of container.
  631. if (!has_visible_children(*container))
  632. MUST(container->append_child(MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML))));
  633. // 33. If new container has no visible children, call createElement("br") on the context object, and append the
  634. // result as the last child of new container.
  635. if (!has_visible_children(*new_container))
  636. MUST(new_container->append_child(MUST(DOM::create_element(document, HTML::TagNames::br, Namespace::HTML))));
  637. // 34. Call collapse(new container, 0) on the context object's selection.
  638. MUST(document.get_selection()->collapse(new_container, 0));
  639. // 35. Return true
  640. return true;
  641. }
  642. // https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
  643. bool command_style_with_css_action(DOM::Document& document, String const& value)
  644. {
  645. // If value is an ASCII case-insensitive match for the string "false", set the CSS styling flag to false.
  646. // Otherwise, set the CSS styling flag to true.
  647. document.set_css_styling_flag(!value.equals_ignoring_ascii_case("false"sv));
  648. // Either way, return true.
  649. return true;
  650. }
  651. // https://w3c.github.io/editing/docs/execCommand/#the-stylewithcss-command
  652. bool command_style_with_css_state(DOM::Document const& document)
  653. {
  654. // True if the CSS styling flag is true, otherwise false.
  655. return document.css_styling_flag();
  656. }
  657. static Array const commands {
  658. CommandDefinition {
  659. .command = CommandNames::delete_,
  660. .action = command_delete_action,
  661. },
  662. CommandDefinition {
  663. .command = CommandNames::defaultParagraphSeparator,
  664. .action = command_default_paragraph_separator_action,
  665. .value = command_default_paragraph_separator_value,
  666. },
  667. CommandDefinition {
  668. .command = CommandNames::insertLineBreak,
  669. .action = command_insert_linebreak_action,
  670. },
  671. CommandDefinition {
  672. .command = CommandNames::insertParagraph,
  673. .action = command_insert_paragraph_action,
  674. },
  675. CommandDefinition {
  676. .command = CommandNames::styleWithCSS,
  677. .action = command_style_with_css_action,
  678. .state = command_style_with_css_state,
  679. },
  680. };
  681. Optional<CommandDefinition const&> find_command_definition(FlyString const& command)
  682. {
  683. for (auto& definition : commands) {
  684. if (command.equals_ignoring_ascii_case(definition.command))
  685. return definition;
  686. }
  687. return {};
  688. }
  689. }