Some improvements for paste, including some new code to annotate
whitespace when writing to the pasteboard to ensure that the meaning
of the markup on the pasteboard is unambiguous.
There is also new code for reading this annotated markup from the pasteboard,
removing the nodes that were added only to prevent ambiguity.
* WebCore.pbproj/project.pbxproj: Added html_interchange.h and html_interchange.cpp files.
The header should have been added earlier, but I did not do so.
* khtml/editing/html_interchange.cpp: Added.
(convertHTMLTextToInterchangeFormat):
* khtml/editing/html_interchange.h: Added some new constants for use with whitespace annotations.
* khtml/editing/htmlediting.cpp:
(khtml::ReplacementFragment::ReplacementFragment): Now looks for and removes annotations added for whitespace.
Also fixed a bug in the code that counts blocks in a fragment.
(khtml::ReplacementFragment::isInterchangeConvertedSpaceSpan): New helper. Recognizes annotation spans.
(khtml::ReplacementFragment::insertNodeBefore): New helper.
(khtml::ReplaceSelectionCommand::doApply): Fixed a bug in the code that sets the start position
for the replacement after deleting. This was causing a bug when pasting at the end of a block.
* khtml/editing/htmlediting.h: Add some new declarations.
* khtml/xml/dom2_rangeimpl.cpp:
(DOM::RangeImpl::toHTML): Calls to startMarkup now pass true for the new annotate flag.
* khtml/xml/dom_nodeimpl.cpp:
(NodeImpl::stringValueForRange): New helper.
(NodeImpl::renderedText): New helper to return only the rendered text in a node.
(NodeImpl::startMarkup): Now takes an additional flag to control whether interchange annotations
should be added. Called by the paste code.
* khtml/xml/dom_nodeimpl.h: Added and modified function declarations.
New test to check the khtml::ReplaceSelectionCommand::doApply fix.
* layout-tests/editing/pasteboard/paste-text-010-expected.txt: Added.
* layout-tests/editing/pasteboard/paste-text-010.html: Added.
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@8096
268f45cc-cd09-0410-ab3c-
d52691b4dbfc
--- /dev/null
+layer at (0,0) size 800x600
+ RenderCanvas at (0,0) size 800x600
+layer at (0,0) size 800x600
+ RenderBlock {HTML} at (0,0) size 800x600
+ RenderBody {BODY} at (8,8) size 784x584
+ RenderBlock {DIV} at (0,0) size 784x112 [border: (2px solid #FF0000)]
+ RenderText {TEXT} at (14,14) size 348x28
+ text run at (14,14) width 348: "There is a tide in the affairs of men."
+ RenderBR {BR} at (0,0) size 0x0
+ RenderBR {BR} at (14,42) size 0x28
+ RenderText {TEXT} at (14,70) size 74x28
+ text run at (14,70) width 74: "of men."
+ RenderText {TEXT} at (88,70) size 74x28
+ text run at (88,70) width 74: "of men."
+ RenderBR {BR} at (0,0) size 0x0
+selection is CARET:
+start: position 7 of child 5 {TEXT} of child 2 {DIV} of root {BODY}
+upstream: position 7 of child 5 {TEXT} of child 2 {DIV} of root {BODY}
+downstream: position 0 of child 6 {BR} of child 2 {DIV} of root {BODY}
--- /dev/null
+<html>
+<head>
+
+<style>
+.editing {
+ border: 2px solid red;
+ padding: 12px;
+ font-size: 24px;
+}
+</style>
+<script src=../editing.js language="JavaScript" type="text/JavaScript" ></script>
+
+<script>
+
+// There was a bug when pasting at the end of the block. The content was inserted at the
+// start of the block instead of the end. This tests the insert-at-end case.
+
+function editingTest() {
+ for (i = 0; i < 31; i++)
+ moveSelectionForwardByCharacterCommand();
+ for (i = 0; i < 7; i++)
+ extendSelectionForwardByCharacterCommand();
+ copyCommand();
+ moveSelectionForwardByCharacterCommand();
+ insertNewlineCommand();
+ insertNewlineCommand();
+ pasteCommand();
+ pasteCommand();
+}
+
+</script>
+
+<title>Editing Test</title>
+</head>
+<body contenteditable id="root">
+
+<div class="editing" id="test">There is a tide in the affairs of men.</div>
+
+<script>
+runEditingTest();
+</script>
+
+</body>
+</html>
+2004-12-01 Ken Kocienda <kocienda@apple.com>
+
+ Reviewed by Hyatt
+
+ Some improvements for paste, including some new code to annotate
+ whitespace when writing to the pasteboard to ensure that the meaning
+ of the markup on the pasteboard is unambiguous.
+
+ There is also new code for reading this annotated markup from the pasteboard,
+ removing the nodes that were added only to prevent ambiguity.
+
+ * WebCore.pbproj/project.pbxproj: Added html_interchange.h and html_interchange.cpp files.
+ The header should have been added earlier, but I did not do so.
+ * khtml/editing/html_interchange.cpp: Added.
+ (convertHTMLTextToInterchangeFormat):
+ * khtml/editing/html_interchange.h: Added some new constants for use with whitespace annotations.
+ * khtml/editing/htmlediting.cpp:
+ (khtml::ReplacementFragment::ReplacementFragment): Now looks for and removes annotations added for whitespace.
+ Also fixed a bug in the code that counts blocks in a fragment.
+ (khtml::ReplacementFragment::isInterchangeConvertedSpaceSpan): New helper. Recognizes annotation spans.
+ (khtml::ReplacementFragment::insertNodeBefore): New helper.
+ (khtml::ReplaceSelectionCommand::doApply): Fixed a bug in the code that sets the start position
+ for the replacement after deleting. This was causing a bug when pasting at the end of a block.
+ * khtml/editing/htmlediting.h: Add some new declarations.
+ * khtml/xml/dom2_rangeimpl.cpp:
+ (DOM::RangeImpl::toHTML): Calls to startMarkup now pass true for the new annotate flag.
+ * khtml/xml/dom_nodeimpl.cpp:
+ (NodeImpl::stringValueForRange): New helper.
+ (NodeImpl::renderedText): New helper to return only the rendered text in a node.
+ (NodeImpl::startMarkup): Now takes an additional flag to control whether interchange annotations
+ should be added. Called by the paste code.
+ * khtml/xml/dom_nodeimpl.h: Added and modified function declarations.
+
+ New test to check the khtml::ReplaceSelectionCommand::doApply fix.
+ * layout-tests/editing/pasteboard/paste-text-010-expected.txt: Added.
+ * layout-tests/editing/pasteboard/paste-text-010.html: Added.
+
2004-11-30 Chris Blumenberg <cblu@apple.com>
* ChangeLog: removed conflict marker
932B9835070297DC0032804F,
939FF8EF0702B1B100979E5E,
BECE67BE07087B250007C14B,
+ BEA5E01E075CEDAC0098A432,
);
isa = PBXHeadersBuildPhase;
runOnlyForDeploymentPostprocessing = 0;
93ABE072070285F600BD91F9,
93ABE074070285F600BD91F9,
939FF8EE0702B1B100979E5E,
+ BEA5DBDB075CEDA00098A432,
);
isa = PBXSourcesBuildPhase;
runOnlyForDeploymentPostprocessing = 0;
settings = {
};
};
+ BEA5DBDA075CEDA00098A432 = {
+ fileEncoding = 30;
+ isa = PBXFileReference;
+ lastKnownFileType = sourcecode.cpp.cpp;
+ name = html_interchange.cpp;
+ path = editing/html_interchange.cpp;
+ refType = 4;
+ sourceTree = "<group>";
+ };
+ BEA5DBDB075CEDA00098A432 = {
+ fileRef = BEA5DBDA075CEDA00098A432;
+ isa = PBXBuildFile;
+ settings = {
+ };
+ };
+ BEA5E01D075CEDAC0098A432 = {
+ fileEncoding = 30;
+ isa = PBXFileReference;
+ lastKnownFileType = sourcecode.c.h;
+ name = html_interchange.h;
+ path = editing/html_interchange.h;
+ refType = 4;
+ sourceTree = "<group>";
+ };
+ BEA5E01E075CEDAC0098A432 = {
+ fileRef = BEA5E01D075CEDAC0098A432;
+ isa = PBXBuildFile;
+ settings = {
+ };
+ };
BEB1DD0805C197DF00DD1F43 = {
children = (
BE9185DD05EE59B80081354D,
BE9185E005EE59B80081354D,
+ BEA5DBDA075CEDA00098A432,
+ BEA5E01D075CEDAC0098A432,
BE02D4E6066F908A0076809F,
BE02D4E7066F908A0076809F,
93ABE067070285F600BD91F9,
--- /dev/null
+/*
+ * Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "html_interchange.h"
+
+#include "qstring.h"
+
+namespace {
+
+QString convertedSpaceString()
+{
+ static QString convertedSpaceString;
+ if (convertedSpaceString.length() == 0) {
+ convertedSpaceString = "<span class=\"";
+ convertedSpaceString += AppleConvertedSpace;
+ convertedSpaceString += "\">";
+ convertedSpaceString += QChar(0xa0);
+ convertedSpaceString += "</span>";
+ }
+ return convertedSpaceString;
+}
+
+bool isWSTreatedAsSpace(const QChar &c)
+{
+ static QChar nbsp = QChar(0xa0);
+ return c.isSpace() && c != nbsp;
+}
+
+} // end anonymous namespace
+
+QString convertHTMLTextToInterchangeFormat(const QString &in)
+{
+ QString s;
+
+ unsigned int i = 0;
+ unsigned int consumed = 0;
+ while (i < in.length()) {
+ consumed = 1;
+ const QChar &c(in[i].latin1());
+ if (isWSTreatedAsSpace(c)) {
+ // count number of adjoining spaces
+ unsigned int j = i + 1;
+ while (j < in.length() && isWSTreatedAsSpace(in[j].latin1()))
+ j++;
+ unsigned int count = j - i;
+ consumed = count;
+ while (count) {
+ unsigned int add = count % 3;
+ switch (add) {
+ case 0:
+ s += convertedSpaceString();
+ s += ' ';
+ s += convertedSpaceString();
+ add = 3;
+ break;
+ case 1:
+ if (i == 0 || i + 1 == in.length()) // at start or end of string
+ s += convertedSpaceString();
+ else
+ s += ' ';
+ break;
+ case 2:
+ if (i == 0) {
+ // at start of string
+ s += convertedSpaceString();
+ s += ' ';
+ }
+ else if (i + 2 == in.length()) {
+ // at end of string
+ s += convertedSpaceString();
+ s += convertedSpaceString();
+ }
+ else {
+ s += convertedSpaceString();
+ s += ' ';
+ }
+ break;
+ }
+ count -= add;
+ }
+ }
+ else {
+ s += in[i];
+ }
+ i += consumed;
+ }
+
+ return s;
+}
#ifndef KHTML_EDITING_HTML_INTERCHANGE_H
#define KHTML_EDITING_HTML_INTERCHANGE_H
+class QString;
+
#define KHTMLInterchangeNewline "KHTMLInterchangeNewline"
+#define AppleConvertedSpace "Apple-converted-space"
enum EAnnotateForInterchange { DoNotAnnotateForInterchange, AnnotateForInterchange };
+QString convertHTMLTextToInterchangeFormat(const QString &);
+
#endif
m_type = TreeFragment;
NodeImpl *node = firstChild;
- int blockCount = 0;
+ int realBlockCount = 0;
NodeImpl *commentToDelete = 0;
while (node) {
NodeImpl *next = node->traverseNextNode();
m_hasInterchangeNewlineComment = true;
commentToDelete = node;
}
+ else if (isInterchangeConvertedSpaceSpan(node)) {
+ NodeImpl *n = 0;
+ while ((n = node->firstChild())) {
+ n->ref();
+ removeNode(n);
+ insertNodeBefore(n, node);
+ n->deref();
+ }
+ removeNode(node);
+ if (n)
+ next = n->traverseNextNode();
+ }
else if (isProbablyBlock(node))
- blockCount++;
+ realBlockCount++;
node = next;
- }
+ }
- if (commentToDelete)
+ if (commentToDelete)
removeNode(commentToDelete);
+ int blockCount = realBlockCount;
firstChild = m_fragment->firstChild();
lastChild = m_fragment->lastChild();
if (!isProbablyBlock(firstChild))
blockCount++;
- if (!isProbablyBlock(lastChild) && firstChild != lastChild)
+ if (!isProbablyBlock(lastChild) && realBlockCount > 0)
blockCount++;
if (blockCount > 1)
return isComment(node) && node->nodeValue() == KHTMLInterchangeNewline;
}
+bool ReplacementFragment::isInterchangeConvertedSpaceSpan(const NodeImpl *node)
+{
+ static DOMString convertedSpaceSpanClass(AppleConvertedSpace);
+ return node->isHTMLElement() && static_cast<const HTMLElementImpl *>(node)->getAttribute(ATTR_CLASS) == convertedSpaceSpanClass;
+}
+
void ReplacementFragment::removeNode(NodeImpl *node)
{
if (!node)
int exceptionCode = 0;
parent->removeChild(node, exceptionCode);
ASSERT(exceptionCode == 0);
+}
+
+void ReplacementFragment::insertNodeBefore(NodeImpl *node, NodeImpl *refNode)
+{
+ if (!node || !refNode)
+ return;
+
+ NodeImpl *parent = refNode->parentNode();
+ if (!parent)
+ return;
+
+ int exceptionCode = 0;
+ parent->insertBefore(node, refNode, exceptionCode);
+ ASSERT(exceptionCode == 0);
}
+
bool isComment(const NodeImpl *node)
{
return node && node->nodeType() == Node::COMMENT_NODE;
case ID_PRE:
case ID_TD:
case ID_TH:
- case ID_TR:
case ID_UL:
return true;
}
}
selection = endingSelection();
- if (!startAtBlockBoundary || !startPos.node()->inDocument())
+ if (startAtStartOfBlock && startBlock->inDocument())
+ startPos = Position(startBlock, 0);
+ else if (startAtEndOfBlock)
+ startPos = selection.start().downstream(StayInBlock);
+ else
startPos = selection.start().upstream(upstreamStayInBlock);
endPos = selection.end().downstream();
ReplacementFragment &operator=(const ReplacementFragment &);
static bool isInterchangeNewlineComment(const DOM::NodeImpl *);
+ static bool isInterchangeConvertedSpaceSpan(const DOM::NodeImpl *);
+
+ // A couple simple DOM helpers
void removeNode(DOM::NodeImpl *);
+ void insertNodeBefore(DOM::NodeImpl *node, DOM::NodeImpl *refNode);
EFragmentType m_type;
DOM::DocumentFragmentImpl *m_fragment;
}
// Add the node to the markup.
- markups.append(n->startMarkup(this));
+ markups.append(n->startMarkup(this, annotate));
if (nodes) {
nodes->append(n);
}
NodeImpl *nextParent = next->parentNode();
if (n != nextParent) {
for (NodeImpl *parent = n->parent(); parent != 0 && parent != nextParent; parent = parent->parentNode()) {
- markups.prepend(parent->startMarkup(this));
+ markups.prepend(parent->startMarkup(this, annotate));
markups.append(parent->endMarkup());
if (nodes) {
nodes->append(parent);
break;
}
}
- markups.prepend(ancestor->startMarkup(this));
+ markups.prepend(ancestor->startMarkup(this, annotate));
markups.append(ancestor->endMarkup());
if (nodes) {
nodes->append(ancestor);
addCommentToHTMLMarkup(KHTMLInterchangeNewline, markups, AppendToMarkup);
}
}
-
- return markups.join("");
+
+ return markups.join("");;
}
#include "xml/dom2_rangeimpl.h"
#include "css/csshelper.h"
#include "css/cssstyleselector.h"
+#include "editing/html_interchange.h"
#include "editing/selection.h"
#include <kglobal.h>
return s;
}
-QString NodeImpl::startMarkup(const RangeImpl *range) const
+QString NodeImpl::stringValueForRange(const RangeImpl *range) const
+{
+ DOMString str = nodeValue().copy();
+ if (range) {
+ int exceptionCode;
+ if (this == range->endContainer(exceptionCode)) {
+ str.truncate(range->endOffset(exceptionCode));
+ }
+ if (this == range->startContainer(exceptionCode)) {
+ str.remove(0, range->startOffset(exceptionCode));
+ }
+ }
+ return str.string();
+}
+
+QString NodeImpl::renderedText(const RangeImpl *range) const
+{
+ QString result;
+
+ RenderObject *r = renderer();
+ if (!r)
+ return result;
+
+ if (!isTextNode())
+ return result;
+
+ int exceptionCode;
+ const TextImpl *textNode = static_cast<const TextImpl *>(this);
+ unsigned startOffset = 0;
+ unsigned endOffset = textNode->length();
+
+ if (range && this == range->startContainer(exceptionCode))
+ startOffset = range->startOffset(exceptionCode);
+ if (range && this == range->endContainer(exceptionCode))
+ endOffset = range->endOffset(exceptionCode);
+
+ RenderText *textRenderer = static_cast<RenderText *>(r);
+ QString str = nodeValue().string();
+ for (InlineTextBox *box = textRenderer->firstTextBox(); box; box = box->nextTextBox()) {
+ unsigned start = box->m_start;
+ unsigned end = box->m_start + box->m_len;
+ if (endOffset < start)
+ break;
+ if (startOffset <= end) {
+ unsigned s = kMax(start, startOffset);
+ unsigned e = kMin(end, endOffset);
+ result.append(str.mid(s, e-s));
+ }
+ }
+
+ return result;
+}
+
+QString NodeImpl::startMarkup(const RangeImpl *range, EAnnotateForInterchange annotate) const
{
unsigned short type = nodeType();
if (type == Node::TEXT_NODE) {
DOMString str = nodeValue().copy();
- if (range) {
- int exceptionCode;
- if (this == range->endContainer(exceptionCode)) {
- str.truncate(range->endOffset(exceptionCode));
- }
- if (this == range->startContainer(exceptionCode)) {
- str.remove(0, range->startOffset(exceptionCode));
- }
- }
Id parentID = parentNode()->id();
bool dontEscape = (parentID == ID_SCRIPT || parentID == ID_TEXTAREA || parentID == ID_STYLE);
- return dontEscape ? str.string() : escapeHTML(str.string());
+ if (dontEscape)
+ return stringValueForRange(range);
+ if (annotate)
+ return convertHTMLTextToInterchangeFormat(escapeHTML(renderedText(range)));
+ return escapeHTML(stringValueForRange(range));
} else if (type == Node::COMMENT_NODE) {
return static_cast<const CommentImpl *>(this)->toString().string();
} else if (type != Node::DOCUMENT_NODE) {
#include "dom/dom_misc.h"
#include "dom/dom_string.h"
#include "dom/dom_node.h"
+#include "editing/html_interchange.h"
#include "misc/helper.h"
#include "misc/shared.h"
#include "dom_atomicstring.h"
virtual bool isMouseFocusable() const;
virtual bool isInline() const;
- QString startMarkup(const RangeImpl *range) const;
+ QString stringValueForRange(const RangeImpl *range) const;
+ QString renderedText(const RangeImpl *range) const;
+ QString startMarkup(const RangeImpl *range, EAnnotateForInterchange annotate=DoNotAnnotateForInterchange) const;
QString endMarkup(void) const;
virtual QString toHTML() const;
QString recursive_toHTML(bool onlyIncludeChildren=false, QPtrList<NodeImpl> *nodes=NULL) const;