Commands.cpp 34 KB

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