LibWeb: Extend logic for extraneous line breaks in block elements

While editing, we need to consider whether removing a <br> has any
effect on layout to determine whether its extraneous. This new condition
finds most cases for extraneous <br>s inside block elements.
This commit is contained in:
Jelle Raaijmakers 2025-09-15 13:56:19 +02:00 committed by Tim Flynn
parent f77f169824
commit b9da7baac4
5 changed files with 55 additions and 25 deletions

View File

@ -1944,7 +1944,7 @@ bool is_collapsed_line_break(GC::Ref<DOM::Node> node)
return false;
// that begins a line box which has nothing else in it, and therefore has zero height.
// NOTE: We check this on the DOM-level by seeing if the next node is neither a non-empty text node nor a <br>.
// AD-HOC: We check this on the DOM level by seeing if the next node is neither a non-empty text node nor a <br>.
if (auto text_node = as_if<DOM::Text>(node->next_sibling()))
return text_node->text_content().value_or({}).is_empty();
return !is<HTML::HTMLBRElement>(node->next_sibling());
@ -2088,8 +2088,18 @@ bool is_extraneous_line_break(GC::Ref<DOM::Node> node)
if (is<HTML::HTMLLIElement>(parent.ptr()) && parent->child_count() == 1)
return false;
// FIXME: ...that has no visual effect, in that removing it from the DOM
// would not change layout,
// ...that has no visual effect, in that removing it from the DOM would not change layout,
// AD-HOC: If node's parent is a block node, and node either has no next sibling or its next sibling is a block
// node, and its previous sibling is a visible inline node but not a <br>, node is extraneous.
if (parent && is_block_node(*parent) && node->previous_sibling()
&& (!node->next_sibling() || is_block_node(*node->next_sibling()))
&& is_visible_node(*node->previous_sibling()) && is_inline_node(*node->previous_sibling())
&& !is<HTML::HTMLBRElement>(*node->previous_sibling())) {
return true;
}
// FIXME: implement more cases that would cause removing a <br> not to have any effect on the layout.
return false;
}

View File

@ -7,3 +7,12 @@ After: foobar
--- c ---
Before: foo<div contenteditable="">bar</div>
After: foobar
--- d ---
Before: foo<br><br><div>bar<br>baz</div>
After: foo<br><div>bar<br>baz</div>
--- e ---
Before: <p>foo</p><br><p>bar</p>
After: <p>foo</p><p>bar</p>
--- f ---
Before: <p><span>abc</span><br></p>
After: <p><br></p>

View File

@ -22,3 +22,6 @@ After: &nbsp;b
--- h ---
Before: foo👩🏼👨🏻bar
After: foobar
--- i ---
Before: foo<div>bar<br>baz</div>
After: foobar<div>baz</div>

View File

@ -3,16 +3,19 @@
<div id="a" contenteditable>foobar</div>
<div id="b" contenteditable>foo👩🏼👨🏻bar</div>
<div id="c" contenteditable>foo<div contenteditable>bar</div></div>
<div id="d" contenteditable>foo<br><br><div>bar<br>baz</div></div>
<div id="e" contenteditable><p>foo</p><br><p>bar</p></div>
<div id="f" contenteditable><p><span>abc</span><br></p></div>
<script>
test(() => {
const testDelete = function (divId, anchorExpression, position) {
const testDelete = function (divId, anchorExpression, start, end = start) {
println(`--- ${divId} ---`);
const divElm = document.querySelector(`div#${divId}`);
println(`Before: ${divElm.innerHTML}`);
// Place cursor
const anchor = anchorExpression(divElm);
getSelection().setBaseAndExtent(anchor, position, anchor, position);
getSelection().setBaseAndExtent(anchor, start, anchor, end);
// Press backspace
document.execCommand("delete");
@ -23,5 +26,8 @@
testDelete("a", (node) => node.firstChild, 3);
testDelete("b", (node) => node.firstChild, 15);
testDelete("c", (node) => node.childNodes[1].firstChild, 0);
testDelete("d", (node) => node.childNodes[3].firstChild, 0);
testDelete("e", (node) => node.childNodes[2].firstChild, 0);
testDelete("f", (node) => node.firstChild.firstChild.firstChild, 0, 3);
});
</script>

View File

@ -1,37 +1,39 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<div id="a" contenteditable="true">foobar</div>
<div id="b" contenteditable="true">a&nbsp;&nbsp;&nbsp;</div>
<div id="c" contenteditable="true">a&nbsp;&nbsp;b</div>
<div id="d" contenteditable="true">a&nbsp;&nbsp;&nbsp;b</div>
<div id="e" contenteditable="true">a&nbsp;&nbsp;&nbsp;&nbsp;b</div>
<div id="f" contenteditable="true">a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b</div>
<div id="g" contenteditable="true">&nbsp;&nbsp;b</div>
<div id="h" contenteditable="true">foo👩🏼👨🏻bar</div>
<div id="a" contenteditable>foobar</div>
<div id="b" contenteditable>a&nbsp;&nbsp;&nbsp;</div>
<div id="c" contenteditable>a&nbsp;&nbsp;b</div>
<div id="d" contenteditable>a&nbsp;&nbsp;&nbsp;b</div>
<div id="e" contenteditable>a&nbsp;&nbsp;&nbsp;&nbsp;b</div>
<div id="f" contenteditable>a&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;b</div>
<div id="g" contenteditable>&nbsp;&nbsp;b</div>
<div id="h" contenteditable>foo👩🏼👨🏻bar</div>
<div id="i" contenteditable>foo<div>bar<br>baz</div></div>
<script>
test(() => {
const testForwardDelete = function(divId, position) {
const testForwardDelete = function(divId, anchorExpression, start, end = start) {
println(`--- ${divId} ---`);
const divElm = document.querySelector(`div#${divId}`);
println(`Before: ${divElm.innerHTML}`);
// Place cursor
const node = divElm.childNodes[0];
getSelection().setBaseAndExtent(node, position, node, position);
const anchor = anchorExpression(divElm);
getSelection().setBaseAndExtent(anchor, start, anchor, end);
// Press delete
document.execCommand('forwardDelete');
document.execCommand("forwardDelete");
println(`After: ${divElm.innerHTML}`);
};
testForwardDelete('a', 3);
testForwardDelete('b', 1);
testForwardDelete('c', 1);
testForwardDelete('d', 1);
testForwardDelete('e', 1);
testForwardDelete('f', 1);
testForwardDelete('g', 0);
testForwardDelete('h', 3);
testForwardDelete("a", (node) => node.firstChild, 3);
testForwardDelete("b", (node) => node.firstChild, 1);
testForwardDelete("c", (node) => node.firstChild, 1);
testForwardDelete("d", (node) => node.firstChild, 1);
testForwardDelete("e", (node) => node.firstChild, 1);
testForwardDelete("f", (node) => node.firstChild, 1);
testForwardDelete("g", (node) => node.firstChild, 0);
testForwardDelete("h", (node) => node.firstChild, 3);
testForwardDelete("i", (node) => node.firstChild, 3);
});
</script>