Bug 71207

Summary: An extra line break is inserted when pasting into a font element
Product: WebKit Reporter: equinux AG <dev-bugzilla>
Component: HTML EditingAssignee: Nobody <webkit-unassigned>
Status: RESOLVED FIXED    
Severity: Normal CC: enrica, max.hong.shen, rniwa, sullivan, webkit.review.bot
Priority: P2    
Version: 528+ (Nightly build)   
Hardware: Unspecified   
OS: OS X 10.7   
Attachments:
Description Flags
Sample file.
none
First try
rniwa: review-
updated patch based on Niwa's review
none
updated patch without using a help function rniwa: review+, webkit.review.bot: commit-queue-

Description equinux AG 2011-10-31 00:43:00 PDT
Created attachment 113020 [details]
Sample file.

Summary:

WebKit editing: Under certain circumstances an additional linebreak is inserted when pasting text from the pasteboard.


Steps to Reproduce:

1. Open the attached document in Safari.
2. Select any of the words in red.
3. Choose Edit > Copy.
4. Place the insertion point anywhere within the red text.
5. Choose Edit > Paste.


Expected Results:

The copied word is inserted at insertion point.


Actual Results:

An additional linebreak is inserted after the word.


Regression:

Reproducible with latest WebKit nightly build (r98835).


Notes:

A similar bug (#71018) has recently been fixed.
Comment 1 Ryosuke Niwa 2011-10-31 00:55:38 PDT
I wouldn't call this a regression. The behavior on Safari 5.1 is far worse (it inserts the pasted content into the previous line AND inserts a line break at where the caret was.
Comment 2 Ryosuke Niwa 2011-10-31 00:56:40 PDT
Confirmed on nightly build.
Comment 3 Yi Shen 2012-03-22 13:13:13 PDT
Created attachment 133329 [details]
First try
Comment 4 Ryosuke Niwa 2012-03-22 13:23:23 PDT
Comment on attachment 133329 [details]
First try

View in context: https://bugs.webkit.org/attachment.cgi?id=133329&action=review

> Source/WebCore/editing/ReplaceSelectionCommand.cpp:116
> +static Position nextPositionInContainerNode(Position pos)

Please don't use abbreviations like pos.

> Source/WebCore/editing/ReplaceSelectionCommand.cpp:120
> +    Node* n = pos.deprecatedNode();    

You should use pos.containerNode(). Also please don't use one-letter variable like n.

> Source/WebCore/editing/ReplaceSelectionCommand.cpp:125
> +    return createLegacyEditingPosition(parent, n->nodeIndex() + 1);

Why do we always want to move up in the node hierarchy? If we had a bunch of siblings after pos's container node, then we'll still want to skip those. r- because I don't think this is a correct fix.
Comment 5 Yi Shen 2012-03-22 15:04:19 PDT
Thanks for the reviewing.
(In reply to comment #4)
> (From update of attachment 133329 [details])
> View in context: https://bugs.webkit.org/attachment.cgi?id=133329&action=review
> 
> > Source/WebCore/editing/ReplaceSelectionCommand.cpp:116
> > +static Position nextPositionInContainerNode(Position pos)
> 
> Please don't use abbreviations like pos.
Will fix it.
> 
> > Source/WebCore/editing/ReplaceSelectionCommand.cpp:120
> > +    Node* n = pos.deprecatedNode();    
> 
> You should use pos.containerNode(). Also please don't use one-letter variable like n.
Will fix it.
> 
> > Source/WebCore/editing/ReplaceSelectionCommand.cpp:125
> > +    return createLegacyEditingPosition(parent, n->nodeIndex() + 1);
> 
> Why do we always want to move up in the node hierarchy? If we had a bunch of siblings after pos's container node, then we'll still want to skip those. r- because I don't think this is a correct fix.

The insertion position only move up if the visible positions of the current position and the high level node's position are the same. So, if we had a bunch of siblings after pos's container node, they won't get skipped because of the difference visible positions.
Comment 6 Ryosuke Niwa 2012-03-22 15:05:33 PDT
Comment on attachment 133329 [details]
First try

View in context: https://bugs.webkit.org/attachment.cgi?id=133329&action=review

>>> Source/WebCore/editing/ReplaceSelectionCommand.cpp:125
>>> +    return createLegacyEditingPosition(parent, n->nodeIndex() + 1);
>> 
>> Why do we always want to move up in the node hierarchy? If we had a bunch of siblings after pos's container node, then we'll still want to skip those. r- because I don't think this is a correct fix.
> 
> The insertion position only move up if the visible positions of the current position and the high level node's position are the same. So, if we had a bunch of siblings after pos's container node, they won't get skipped because of the difference visible positions.

Right, but what if they're empty? If that case, I think we do want to skip those empty elements but moving to the next position in the parent may not be possible due to it being a different visible position.
Comment 7 Yi Shen 2012-03-22 18:28:17 PDT
(In reply to comment #6)
> (From update of attachment 133329 [details])
> View in context: https://bugs.webkit.org/attachment.cgi?id=133329&action=review
> 
> >>> Source/WebCore/editing/ReplaceSelectionCommand.cpp:125
> >>> +    return createLegacyEditingPosition(parent, n->nodeIndex() + 1);
> >> 
> >> Why do we always want to move up in the node hierarchy? If we had a bunch of siblings after pos's container node, then we'll still want to skip those. r- because I don't think this is a correct fix.
> > 
> > The insertion position only move up if the visible positions of the current position and the high level node's position are the same. So, if we had a bunch of siblings after pos's container node, they won't get skipped because of the difference visible positions.
> 
> Right, but what if they're empty? If that case, I think we do want to skip those empty elements but moving to the next position in the parent may not be possible due to it being a different visible position.

Correct me if I am wrong :) In the context, the empty elements means they don't occupy any space, right? If so, all the empty sibling elements would have the same visible position as the next position in the parent. For example,
<div><u>abc^<u></u><u></u></u></div>, "^" is the insertion position. After calling the positionAvoidingPrecedingNodes(), it should be <div><u>abc<u></u><u></u></u>^</div>
Comment 8 Ryosuke Niwa 2012-03-22 18:36:11 PDT
Comment on attachment 133329 [details]
First try

View in context: https://bugs.webkit.org/attachment.cgi?id=133329&action=review

>>>>> Source/WebCore/editing/ReplaceSelectionCommand.cpp:125
>>>>> +    return createLegacyEditingPosition(parent, n->nodeIndex() + 1);
>>>> 
>>>> Why do we always want to move up in the node hierarchy? If we had a bunch of siblings after pos's container node, then we'll still want to skip those. r- because I don't think this is a correct fix.
>>> 
>>> The insertion position only move up if the visible positions of the current position and the high level node's position are the same. So, if we had a bunch of siblings after pos's container node, they won't get skipped because of the difference visible positions.
>> 
>> Right, but what if they're empty? If that case, I think we do want to skip those empty elements but moving to the next position in the parent may not be possible due to it being a different visible position.
> 
> Correct me if I am wrong :) In the context, the empty elements means they don't occupy any space, right? If so, all the empty sibling elements would have the same visible position as the next position in the parent. For example,
> <div><u>abc^<u></u><u></u></u></div>, "^" is the insertion position. After calling the positionAvoidingPrecedingNodes(), it should be <div><u>abc<u></u><u></u></u>^</div>

Yes, but your code doesn't do that at the moment because you always retrieve the parent of the deprecated node. i.e. the first call to nextPositionInContainerNode would move the position to be anchored at div, then the next call will try to move out of div.

In general, we shouldn't try to out-smart what Position does. Position.next/previous and all other editing code has been well-tested and deals with all sorts of edge-cases. Adding completely different algorithm to deal with this particular situation is extremely undesirable.
Comment 9 Yi Shen 2012-03-22 19:01:51 PDT
(In reply to comment #8)
> (From update of attachment 133329 [details])
> View in context: https://bugs.webkit.org/attachment.cgi?id=133329&action=review
> 
> >>>>> Source/WebCore/editing/ReplaceSelectionCommand.cpp:125
> >>>>> +    return createLegacyEditingPosition(parent, n->nodeIndex() + 1);
> >>>> 
> >>>> Why do we always want to move up in the node hierarchy? If we had a bunch of siblings after pos's container node, then we'll still want to skip those. r- because I don't think this is a correct fix.
> >>> 
> >>> The insertion position only move up if the visible positions of the current position and the high level node's position are the same. So, if we had a bunch of siblings after pos's container node, they won't get skipped because of the difference visible positions.
> >> 
> >> Right, but what if they're empty? If that case, I think we do want to skip those empty elements but moving to the next position in the parent may not be possible due to it being a different visible position.
> > 
> > Correct me if I am wrong :) In the context, the empty elements means they don't occupy any space, right? If so, all the empty sibling elements would have the same visible position as the next position in the parent. For example,
> > <div><u>abc^<u></u><u></u></u></div>, "^" is the insertion position. After calling the positionAvoidingPrecedingNodes(), it should be <div><u>abc<u></u><u></u></u>^</div>
> 
> Yes, but your code doesn't do that at the moment because you always retrieve the parent of the deprecated node. i.e. the first call to nextPositionInContainerNode would move the position to be anchored at div, then the next call will try to move out of div.

I think my code does move the position to be anchored at div. In above case, let's say the start insertion position is (#text, 3), first call to nextPositionInContainerNode returns the position (#u, 1) and then the position moves to (#u, 1). The second call to nextPositionInContainerNode returns the position (#div 1) and move the position to be anchored at div. However, it wouldn't move the position out of div because we always check the position node  with the enclosingBlockFlowElement to make sure the position stay at the same block.

> 
> In general, we shouldn't try to out-smart what Position does. Position.next/previous and all other editing code has been well-tested and deals with all sorts of edge-cases. Adding completely different algorithm to deal with this particular situation is extremely undesirable.

The reason why I don't use Position.next here because it may return the child node's position and then moves the insertion position into the child node.
e.g without my changes, <div><u><u>abc^</u><u>efg</u></u></div> ==> <div><u><u>abc</u><u>^efg</u></u></div>
Comment 10 Ryosuke Niwa 2012-03-22 19:08:08 PDT
(In reply to comment #9)
> > In general, we shouldn't try to out-smart what Position does. Position.next/previous and all other editing code has been well-tested and deals with all sorts of edge-cases. Adding completely different algorithm to deal with this particular situation is extremely undesirable.
> 
> The reason why I don't use Position.next here because it may return the child node's position and then moves the insertion position into the child node.
> e.g without my changes, <div><u><u>abc^</u><u>efg</u></u></div> ==> <div><u><u>abc</u><u>^efg</u></u></div>

What you need here is to use/make a function like nodeToSplitToAvoidPastingIntoInlineNodesWithStyle that finds the position outside of the inline elements.
Comment 11 Yi Shen 2012-03-22 19:20:31 PDT
(In reply to comment #10)
> (In reply to comment #9)
> > > In general, we shouldn't try to out-smart what Position does. Position.next/previous and all other editing code has been well-tested and deals with all sorts of edge-cases. Adding completely different algorithm to deal with this particular situation is extremely undesirable.
> > 
> > The reason why I don't use Position.next here because it may return the child node's position and then moves the insertion position into the child node.
> > e.g without my changes, <div><u><u>abc^</u><u>efg</u></u></div> ==> <div><u><u>abc</u><u>^efg</u></u></div>
> 
> What you need here is to use/make a function like nodeToSplitToAvoidPastingIntoInlineNodesWithStyle that finds the position outside of the inline elements.

Thanks, will look at it and provide a new patch :)
Comment 12 Yi Shen 2012-03-23 14:23:20 PDT
Created attachment 133563 [details]
updated patch based on Niwa's review
Comment 13 Ryosuke Niwa 2012-03-23 22:00:50 PDT
Anyway, my preference is to do something along the line of:

Index: Source/WebCore/editing/ReplaceSelectionCommand.cpp
===================================================================
--- Source/WebCore/editing/ReplaceSelectionCommand.cpp	(revision 111896)
+++ Source/WebCore/editing/ReplaceSelectionCommand.cpp	(working copy)
@@ -121,9 +121,14 @@
     // same.  E.g.,
     //   <div>foo^</div>^
     // The two positions above are the same visual position, but we want to stay in the same block.
-    Node* stopNode = pos.deprecatedNode()->enclosingBlockFlowElement();
-    while (stopNode != pos.deprecatedNode() && VisiblePosition(pos) == VisiblePosition(pos.next()))
-        pos = pos.next();
+    Node* enclosingBlock = pos.containerNode()->enclosingBlockFlowElement();
+    for (Position nextPosition = pos; ;pos = nextPosition) {
+        nextPosition = pos.next();
+        if (nextPosition == pos
+            || nextPosition.containerNode()->enclosingBlockFlowElement() != enclosingBlock
+            || VisiblePosition(pos) != VisiblePosition(nextPosition))
+            break;
+    }
     return pos;
 }

Note that a couple of tests fail with this change along, and there's about a dozen test to be rebaselined.
Comment 14 Yi Shen 2012-03-26 09:49:53 PDT
Thanks for the follow up :) But I have two concerns about your code,

1) Since the Position::next() is still used here, it is possible to move the insertion position into sibling node's child, which seems incorrect (that's why my patch doesn't use position::next to avoid it). e.g
<div><u><u>text underline<u>^<u>test underline</u></u></div>
==>
<div><u><u>text underline<u><u>^test underline</u></u></div>

2) It is possible to move the insertion position to the end of the enclosingBlock. That's why some tests failed.
e.g.
<div><u>abc^</u>#text "\n"<div> ==> <div><u>abc^</u>#text "\n"<div>^
This can be fixed by changing the for statement to 
for (Position nextPosition = pos; nextPosition.containerNode() != enclosingBlock; pos = nextPosition) {
 ...
}

(In reply to comment #13)
> Anyway, my preference is to do something along the line of:
> 
> Index: Source/WebCore/editing/ReplaceSelectionCommand.cpp
> ===================================================================
> --- Source/WebCore/editing/ReplaceSelectionCommand.cpp    (revision 111896)
> +++ Source/WebCore/editing/ReplaceSelectionCommand.cpp    (working copy)
> @@ -121,9 +121,14 @@
>      // same.  E.g.,
>      //   <div>foo^</div>^
>      // The two positions above are the same visual position, but we want to stay in the same block.
> -    Node* stopNode = pos.deprecatedNode()->enclosingBlockFlowElement();
> -    while (stopNode != pos.deprecatedNode() && VisiblePosition(pos) == VisiblePosition(pos.next()))
> -        pos = pos.next();
> +    Node* enclosingBlock = pos.containerNode()->enclosingBlockFlowElement();
> +    for (Position nextPosition = pos; ;pos = nextPosition) {
> +        nextPosition = pos.next();
> +        if (nextPosition == pos
> +            || nextPosition.containerNode()->enclosingBlockFlowElement() != enclosingBlock
> +            || VisiblePosition(pos) != VisiblePosition(nextPosition))
> +            break;
> +    }
>      return pos;
>  }
> 
> Note that a couple of tests fail with this change along, and there's about a dozen test to be rebaselined.
Comment 15 Yi Shen 2012-03-27 07:14:50 PDT
Created attachment 134055 [details]
updated patch without using a help function
Comment 16 Ryosuke Niwa 2012-03-27 10:39:03 PDT
Comment on attachment 134055 [details]
updated patch without using a help function

View in context: https://bugs.webkit.org/attachment.cgi?id=134055&action=review

> Source/WebCore/editing/ReplaceSelectionCommand.cpp:127
> +        if (pos.containerNode()->nonShadowBoundaryParentNode())
> +            nextPosition = positionInParentAfterNode(pos.containerNode());

Again, this is incorrect. You need to skip siblings of pos.containerNode() first.
I'd say that any solution that involves calling positionInParentAfterNode won't be correct.
On my second thought, we should be able to replace this enture function by pos.downstream().
Comment 17 Yi Shen 2012-03-27 14:00:09 PDT
Well, I think I misunderstood your comments #6 & #8. 

Regarding the pos.downstream(), I have tried it on my code and seems it doesn't work. Also, you can see it by checking Tony's comments (#10) in bug Bug 21840.

Now my question is why we need to skip the siblings of pos.containerNode()? Considering following case,

e.g.   <div><u>abc^</u>/n</div>

if we skip all the siblings, then the new insertion position is after the "/n", which seems incorrect for me.

(In reply to comment #16)
> (From update of attachment 134055 [details])
> View in context: https://bugs.webkit.org/attachment.cgi?id=134055&action=review
> 
> > Source/WebCore/editing/ReplaceSelectionCommand.cpp:127
> > +        if (pos.containerNode()->nonShadowBoundaryParentNode())
> > +            nextPosition = positionInParentAfterNode(pos.containerNode());
> 
> Again, this is incorrect. You need to skip siblings of pos.containerNode() first.
> I'd say that any solution that involves calling positionInParentAfterNode won't be correct.
> On my second thought, we should be able to replace this enture function by pos.downstream().
Comment 18 Ryosuke Niwa 2012-03-27 23:01:24 PDT
Comment on attachment 134055 [details]
updated patch without using a help function

Okay, I thought about this for a while but I couldn't come up with a better solution. So let's land this patch as is. We'll need to refactor ReplaceSelectionCommand in the future anyway so let us think through by then.

Thanks for the patch and explanations.
Comment 19 WebKit Review Bot 2012-03-27 23:04:42 PDT
Comment on attachment 134055 [details]
updated patch without using a help function

Rejecting attachment 134055 [details] from commit-queue.

Failed to run "['/mnt/git/webkit-commit-queue/Tools/Scripts/webkit-patch', '--status-host=queues.webkit.org', '-..." exit_code: 1

Last 500 characters of output:
ueue/Tools/Scripts/webkitpy/tool/commands/stepsequence.py", line 70, in run_and_handle_errors
    self._run(tool, options, state)
  File "/mnt/git/webkit-commit-queue/Tools/Scripts/webkitpy/tool/commands/stepsequence.py", line 64, in _run
    step(tool, options).run(state)
  File "/mnt/git/webkit-commit-queue/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py", line 50, in run
    if changelog_entry.has_valid_reviewer():
AttributeError: 'NoneType' object has no attribute 'has_valid_reviewer'

Full output: http://queues.webkit.org/results/12146860
Comment 20 WebKit Review Bot 2012-03-27 23:10:16 PDT
Comment on attachment 134055 [details]
updated patch without using a help function

Rejecting attachment 134055 [details] from commit-queue.

Failed to run "['/mnt/git/webkit-commit-queue/Tools/Scripts/webkit-patch', '--status-host=queues.webkit.org', '-..." exit_code: 1

Last 500 characters of output:
ueue/Tools/Scripts/webkitpy/tool/commands/stepsequence.py", line 70, in run_and_handle_errors
    self._run(tool, options, state)
  File "/mnt/git/webkit-commit-queue/Tools/Scripts/webkitpy/tool/commands/stepsequence.py", line 64, in _run
    step(tool, options).run(state)
  File "/mnt/git/webkit-commit-queue/Tools/Scripts/webkitpy/tool/steps/validatereviewer.py", line 50, in run
    if changelog_entry.has_valid_reviewer():
AttributeError: 'NoneType' object has no attribute 'has_valid_reviewer'

Full output: http://queues.webkit.org/results/12147943
Comment 21 Ryosuke Niwa 2012-03-27 23:13:49 PDT
Comment on attachment 134055 [details]
updated patch without using a help function

View in context: https://bugs.webkit.org/attachment.cgi?id=134055&action=review

> Source/WebCore/ChangeLog:1
> +02012-03-27  Yi Shen  <yi.4.shen@nokia.com>,  Ryosuke Niwa  <rniwa@webkit.org>

Oops, you need to fix this line :( Please delete my name.

> LayoutTests/editing/inserting/insert-text-into-font.html:1
> +<body contentEditable="true">

No DOCTYPE?

> LayoutTests/editing/inserting/insert-text-into-font.html:39
> +    // Inserting HTML to replace the "line" in the "Second line" should not insert an extra div (line break).
> +    var targetDiv = document.getElementById("secondDiv");
> +    var targetText = targetDiv.firstChild;
> +    execSetSelectionCommand(targetText, 8, targetText, targetText.textContent.length);
> +    document.execCommand("inserthtml", false, "<span id='red' style='color:red'>line</span>");
> +
> +    // Verify that the font still only has one div.
> +    var font = document.getElementById("fonts");
> +    var divs = font.querySelectorAll("div");
> +    if (divs.length != 1)
> +        fail("An extra div is inserted which is incorrect. There are " + divs.length + " divs inside font element.");
> +
> +    // Inserting HTML into "Second line" should not insert an extra div (line break).
> +    execSetSelectionCommand(targetText, 8, targetText, 8);
> +    document.execCommand("inserthtml", false, "<span id='green' style='color:green'>green</span>");
> +
> +    // Verify that the font still only has one div.
> +    var font = document.getElementById("fonts");
> +    var divs = font.querySelectorAll("div");
> +    if (divs.length != 1)
> +        fail("An extra div is inserted which is incorrect. There are " + divs.length + " divs inside font element.");

It's probably better to use dump-as-markup.js here.
Comment 22 Yi Shen 2012-03-28 08:01:16 PDT
Committed r112399: <http://trac.webkit.org/changeset/112399>
Comment 23 Yi Shen 2012-03-28 08:02:20 PDT
Thanks for the review :)

(In reply to comment #21)
> (From update of attachment 134055 [details])
> View in context: https://bugs.webkit.org/attachment.cgi?id=134055&action=review
> 
> > Source/WebCore/ChangeLog:1
> > +02012-03-27  Yi Shen  <yi.4.shen@nokia.com>,  Ryosuke Niwa  <rniwa@webkit.org>
> 
> Oops, you need to fix this line :( Please delete my name.
> 
> > LayoutTests/editing/inserting/insert-text-into-font.html:1
> > +<body contentEditable="true">
> 
> No DOCTYPE?
> 
> > LayoutTests/editing/inserting/insert-text-into-font.html:39
> > +    // Inserting HTML to replace the "line" in the "Second line" should not insert an extra div (line break).
> > +    var targetDiv = document.getElementById("secondDiv");
> > +    var targetText = targetDiv.firstChild;
> > +    execSetSelectionCommand(targetText, 8, targetText, targetText.textContent.length);
> > +    document.execCommand("inserthtml", false, "<span id='red' style='color:red'>line</span>");
> > +
> > +    // Verify that the font still only has one div.
> > +    var font = document.getElementById("fonts");
> > +    var divs = font.querySelectorAll("div");
> > +    if (divs.length != 1)
> > +        fail("An extra div is inserted which is incorrect. There are " + divs.length + " divs inside font element.");
> > +
> > +    // Inserting HTML into "Second line" should not insert an extra div (line break).
> > +    execSetSelectionCommand(targetText, 8, targetText, 8);
> > +    document.execCommand("inserthtml", false, "<span id='green' style='color:green'>green</span>");
> > +
> > +    // Verify that the font still only has one div.
> > +    var font = document.getElementById("fonts");
> > +    var divs = font.querySelectorAll("div");
> > +    if (divs.length != 1)
> > +        fail("An extra div is inserted which is incorrect. There are " + divs.length + " divs inside font element.");
> 
> It's probably better to use dump-as-markup.js here.
Comment 24 Simon Fraser (smfr) 2012-04-19 14:34:31 PDT
Comment on attachment 133563 [details]
updated patch based on Niwa's review

I assume this patch is obsolete since the bug is RESOLVED