diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:30:52 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:30:52 -0800 |
commit | 8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2 (patch) | |
tree | 11425ea0b299d6fb89c6d3618a22d97d5bf68d0f /WebCore/editing/markup.cpp | |
parent | 648161bb0edfc3d43db63caed5cc5213bc6cb78f (diff) | |
download | external_webkit-8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2.zip external_webkit-8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2.tar.gz external_webkit-8e35f3cfc7fba1d1c829dc557ebad6409cbe16a2.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'WebCore/editing/markup.cpp')
-rw-r--r-- | WebCore/editing/markup.cpp | 1219 |
1 files changed, 1219 insertions, 0 deletions
diff --git a/WebCore/editing/markup.cpp b/WebCore/editing/markup.cpp new file mode 100644 index 0000000..b067002 --- /dev/null +++ b/WebCore/editing/markup.cpp @@ -0,0 +1,1219 @@ +/* + * Copyright (C) 2004, 2005, 2006, 2007, 2008 Apple 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 "config.h" +#include "markup.h" + +#include "CDATASection.h" +#include "CharacterNames.h" +#include "Comment.h" +#include "CSSComputedStyleDeclaration.h" +#include "CSSPrimitiveValue.h" +#include "CSSProperty.h" +#include "CSSPropertyNames.h" +#include "CSSRule.h" +#include "CSSRuleList.h" +#include "CSSStyleRule.h" +#include "CSSStyleSelector.h" +#include "CSSValue.h" +#include "CSSValueKeywords.h" +#include "DeleteButtonController.h" +#include "Document.h" +#include "DocumentFragment.h" +#include "DocumentType.h" +#include "Editor.h" +#include "Frame.h" +#include "HTMLElement.h" +#include "HTMLNames.h" +#include "InlineTextBox.h" +#include "Logging.h" +#include "ProcessingInstruction.h" +#include "QualifiedName.h" +#include "Range.h" +#include "Selection.h" +#include "TextIterator.h" +#include "htmlediting.h" +#include "visible_units.h" + +using namespace std; + +namespace WebCore { + +using namespace HTMLNames; + +static inline bool shouldSelfClose(const Node *node); + +class AttributeChange { +public: + AttributeChange() + : m_name(nullAtom, nullAtom, nullAtom) + { + } + + AttributeChange(PassRefPtr<Element> element, const QualifiedName& name, const String& value) + : m_element(element), m_name(name), m_value(value) + { + } + + void apply() + { + m_element->setAttribute(m_name, m_value); + } + +private: + RefPtr<Element> m_element; + QualifiedName m_name; + String m_value; +}; + +static void appendAttributeValue(Vector<UChar>& result, const String& attr, bool escapeNBSP) +{ + const UChar* uchars = attr.characters(); + unsigned len = attr.length(); + unsigned lastCopiedFrom = 0; + + static const String ampEntity("&"); + static const String gtEntity(">"); + static const String ltEntity("<"); + static const String quotEntity("""); + static const String nbspEntity(" "); + + for (unsigned i = 0; i < len; ++i) { + UChar c = uchars[i]; + switch (c) { + case '&': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, ampEntity); + lastCopiedFrom = i + 1; + break; + case '<': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, ltEntity); + lastCopiedFrom = i + 1; + break; + case '>': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, gtEntity); + lastCopiedFrom = i + 1; + break; + case '"': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, quotEntity); + lastCopiedFrom = i + 1; + break; + case noBreakSpace: + if (escapeNBSP) { + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, nbspEntity); + lastCopiedFrom = i + 1; + } + break; + } + } + + result.append(uchars + lastCopiedFrom, len - lastCopiedFrom); +} + +static void appendEscapedContent(Vector<UChar>& result, pair<const UChar*, size_t> range, bool escapeNBSP) +{ + const UChar* uchars = range.first; + unsigned len = range.second; + unsigned lastCopiedFrom = 0; + + static const String ampEntity("&"); + static const String gtEntity(">"); + static const String ltEntity("<"); + static const String nbspEntity(" "); + + for (unsigned i = 0; i < len; ++i) { + UChar c = uchars[i]; + switch (c) { + case '&': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, ampEntity); + lastCopiedFrom = i + 1; + break; + case '<': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, ltEntity); + lastCopiedFrom = i + 1; + break; + case '>': + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, gtEntity); + lastCopiedFrom = i + 1; + break; + case noBreakSpace: + if (escapeNBSP) { + result.append(uchars + lastCopiedFrom, i - lastCopiedFrom); + append(result, nbspEntity); + lastCopiedFrom = i + 1; + } + break; + } + } + + result.append(uchars + lastCopiedFrom, len - lastCopiedFrom); +} + +static String escapeContentText(const String& in, bool escapeNBSP) +{ + Vector<UChar> buffer; + appendEscapedContent(buffer, make_pair(in.characters(), in.length()), escapeNBSP); + return String::adopt(buffer); +} + +static void appendQuotedURLAttributeValue(Vector<UChar>& result, const String& urlString) +{ + UChar quoteChar = '\"'; + String strippedURLString = urlString.stripWhiteSpace(); + if (protocolIs(strippedURLString, "javascript")) { + // minimal escaping for javascript urls + if (strippedURLString.contains('"')) { + if (strippedURLString.contains('\'')) + strippedURLString.replace('\"', """); + else + quoteChar = '\''; + } + result.append(quoteChar); + append(result, strippedURLString); + result.append(quoteChar); + return; + } + + // FIXME: This does not fully match other browsers. Firefox percent-escapes non-ASCII characters for innerHTML. + result.append(quoteChar); + appendAttributeValue(result, urlString, false); + result.append(quoteChar); +} + +static String stringValueForRange(const Node* node, const Range* range) +{ + if (!range) + return node->nodeValue(); + + String str = node->nodeValue(); + ExceptionCode ec; + if (node == range->endContainer(ec)) + str.truncate(range->endOffset(ec)); + if (node == range->startContainer(ec)) + str.remove(0, range->startOffset(ec)); + return str; +} + +static inline pair<const UChar*, size_t> ucharRange(const Node *node, const Range *range) +{ + String str = node->nodeValue(); + const UChar* characters = str.characters(); + size_t length = str.length(); + + if (range) { + ExceptionCode ec; + if (node == range->endContainer(ec)) + length = range->endOffset(ec); + if (node == range->startContainer(ec)) { + size_t start = range->startOffset(ec); + characters += start; + length -= start; + } + } + + return make_pair(characters, length); +} + +static inline void appendUCharRange(Vector<UChar>& result, const pair<const UChar*, size_t> range) +{ + result.append(range.first, range.second); +} + +static String renderedText(const Node* node, const Range* range) +{ + if (!node->isTextNode()) + return String(); + + ExceptionCode ec; + const Text* textNode = static_cast<const Text*>(node); + unsigned startOffset = 0; + unsigned endOffset = textNode->length(); + + if (range && node == range->startContainer(ec)) + startOffset = range->startOffset(ec); + if (range && node == range->endContainer(ec)) + endOffset = range->endOffset(ec); + + Position start(const_cast<Node*>(node), startOffset); + Position end(const_cast<Node*>(node), endOffset); + return plainText(Range::create(node->document(), start, end).get()); +} + +static PassRefPtr<CSSMutableStyleDeclaration> styleFromMatchedRulesForElement(Element* element, bool authorOnly = true) +{ + RefPtr<CSSMutableStyleDeclaration> style = CSSMutableStyleDeclaration::create(); + RefPtr<CSSRuleList> matchedRules = element->document()->styleSelector()->styleRulesForElement(element, authorOnly); + if (matchedRules) { + for (unsigned i = 0; i < matchedRules->length(); i++) { + if (matchedRules->item(i)->type() == CSSRule::STYLE_RULE) { + RefPtr<CSSMutableStyleDeclaration> s = static_cast<CSSStyleRule*>(matchedRules->item(i))->style(); + style->merge(s.get(), true); + } + } + } + + return style.release(); +} + +static void removeEnclosingMailBlockquoteStyle(CSSMutableStyleDeclaration* style, Node* node) +{ + Node* blockquote = nearestMailBlockquote(node); + if (!blockquote || !blockquote->parentNode()) + return; + + RefPtr<CSSMutableStyleDeclaration> parentStyle = Position(blockquote->parentNode(), 0).computedStyle()->copyInheritableProperties(); + RefPtr<CSSMutableStyleDeclaration> blockquoteStyle = Position(blockquote, 0).computedStyle()->copyInheritableProperties(); + parentStyle->diff(blockquoteStyle.get()); + blockquoteStyle->diff(style); +} + +static void removeDefaultStyles(CSSMutableStyleDeclaration* style, Document* document) +{ + if (!document || !document->documentElement()) + return; + + RefPtr<CSSMutableStyleDeclaration> documentStyle = computedStyle(document->documentElement())->copyInheritableProperties(); + documentStyle->diff(style); +} + +static bool shouldAddNamespaceElem(const Element* elem) +{ + // Don't add namespace attribute if it is already defined for this elem. + const AtomicString& prefix = elem->prefix(); + AtomicString attr = !prefix.isEmpty() ? "xmlns:" + prefix : "xmlns"; + return !elem->hasAttribute(attr); +} + +static bool shouldAddNamespaceAttr(const Attribute* attr, HashMap<AtomicStringImpl*, AtomicStringImpl*>& namespaces) +{ + // Don't add namespace attributes twice + static const AtomicString xmlnsURI = "http://www.w3.org/2000/xmlns/"; + static const QualifiedName xmlnsAttr(nullAtom, "xmlns", xmlnsURI); + if (attr->name() == xmlnsAttr) { + namespaces.set(emptyAtom.impl(), attr->value().impl()); + return false; + } + + QualifiedName xmlnsPrefixAttr("xmlns", attr->localName(), xmlnsURI); + if (attr->name() == xmlnsPrefixAttr) { + namespaces.set(attr->localName().impl(), attr->value().impl()); + return false; + } + + return true; +} + +static void appendNamespace(Vector<UChar>& result, const AtomicString& prefix, const AtomicString& ns, HashMap<AtomicStringImpl*, AtomicStringImpl*>& namespaces) +{ + if (ns.isEmpty()) + return; + + // Use emptyAtoms's impl() for both null and empty strings since the HashMap can't handle 0 as a key + AtomicStringImpl* pre = prefix.isEmpty() ? emptyAtom.impl() : prefix.impl(); + AtomicStringImpl* foundNS = namespaces.get(pre); + if (foundNS != ns.impl()) { + namespaces.set(pre, ns.impl()); + static const String xmlns("xmlns"); + result.append(' '); + append(result, xmlns); + if (!prefix.isEmpty()) { + result.append(':'); + append(result, prefix); + } + + result.append('='); + result.append('"'); + appendAttributeValue(result, ns, false); + result.append('"'); + } +} + +static void appendDocumentType(Vector<UChar>& result, const DocumentType* n) +{ + if (n->name().isEmpty()) + return; + + append(result, "<!DOCTYPE "); + append(result, n->name()); + if (!n->publicId().isEmpty()) { + append(result, " PUBLIC \""); + append(result, n->publicId()); + append(result, "\""); + if (!n->systemId().isEmpty()) { + append(result, " \""); + append(result, n->systemId()); + append(result, "\""); + } + } else if (!n->systemId().isEmpty()) { + append(result, " SYSTEM \""); + append(result, n->systemId()); + append(result, "\""); + } + if (!n->internalSubset().isEmpty()) { + append(result, " ["); + append(result, n->internalSubset()); + append(result, "]"); + } + append(result, ">"); +} + +static void appendStartMarkup(Vector<UChar>& result, const Node *node, const Range *range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0) +{ + bool documentIsHTML = node->document()->isHTMLDocument(); + switch (node->nodeType()) { + case Node::TEXT_NODE: { + if (Node* parent = node->parentNode()) { + if (parent->hasTagName(scriptTag) + || parent->hasTagName(styleTag) + || parent->hasTagName(textareaTag) + || parent->hasTagName(xmpTag)) { + appendUCharRange(result, ucharRange(node, range)); + break; + } + } + if (!annotate) { + appendEscapedContent(result, ucharRange(node, range), documentIsHTML); + break; + } + + bool useRenderedText = !enclosingNodeWithTag(Position(const_cast<Node*>(node), 0), selectTag); + String markup = escapeContentText(useRenderedText ? renderedText(node, range) : stringValueForRange(node, range), false); + if (annotate) + markup = convertHTMLTextToInterchangeFormat(markup, static_cast<const Text*>(node)); + append(result, markup); + break; + } + case Node::COMMENT_NODE: + // FIXME: Comment content is not escaped, but XMLSerializer (and possibly other callers) should raise an exception if it includes "-->". + append(result, "<!--"); + append(result, static_cast<const Comment*>(node)->nodeValue()); + append(result, "-->"); + break; + case Node::DOCUMENT_NODE: + case Node::DOCUMENT_FRAGMENT_NODE: + break; + case Node::DOCUMENT_TYPE_NODE: + appendDocumentType(result, static_cast<const DocumentType*>(node)); + break; + case Node::PROCESSING_INSTRUCTION_NODE: { + // FIXME: PI data is not escaped, but XMLSerializer (and possibly other callers) this should raise an exception if it includes "?>". + const ProcessingInstruction* n = static_cast<const ProcessingInstruction*>(node); + append(result, "<?"); + append(result, n->target()); + append(result, " "); + append(result, n->data()); + append(result, "?>"); + break; + } + case Node::ELEMENT_NODE: { + result.append('<'); + const Element* el = static_cast<const Element*>(node); + bool convert = convertBlocksToInlines & isBlock(const_cast<Node*>(node)); + append(result, el->nodeNamePreservingCase()); + NamedAttrMap *attrs = el->attributes(); + unsigned length = attrs->length(); + if (!documentIsHTML && namespaces && shouldAddNamespaceElem(el)) + appendNamespace(result, el->prefix(), el->namespaceURI(), *namespaces); + + for (unsigned int i = 0; i < length; i++) { + Attribute *attr = attrs->attributeItem(i); + // We'll handle the style attribute separately, below. + if (attr->name() == styleAttr && el->isHTMLElement() && (annotate || convert)) + continue; + result.append(' '); + + if (documentIsHTML) + append(result, attr->name().localName()); + else + append(result, attr->name().toString()); + + result.append('='); + + if (el->isURLAttribute(attr)) + appendQuotedURLAttributeValue(result, attr->value()); + else { + result.append('\"'); + appendAttributeValue(result, attr->value(), documentIsHTML); + result.append('\"'); + } + + if (!documentIsHTML && namespaces && shouldAddNamespaceAttr(attr, *namespaces)) + appendNamespace(result, attr->prefix(), attr->namespaceURI(), *namespaces); + } + + if (el->isHTMLElement() && (annotate || convert)) { + Element* element = const_cast<Element*>(el); + RefPtr<CSSMutableStyleDeclaration> style = static_cast<HTMLElement*>(element)->getInlineStyleDecl()->copy(); + if (annotate) { + RefPtr<CSSMutableStyleDeclaration> styleFromMatchedRules = styleFromMatchedRulesForElement(const_cast<Element*>(el)); + // Styles from the inline style declaration, held in the variable "style", take precedence + // over those from matched rules. + styleFromMatchedRules->merge(style.get()); + style = styleFromMatchedRules; + + RefPtr<CSSComputedStyleDeclaration> computedStyleForElement = computedStyle(element); + RefPtr<CSSMutableStyleDeclaration> fromComputedStyle = CSSMutableStyleDeclaration::create(); + + DeprecatedValueListConstIterator<CSSProperty> end; + for (DeprecatedValueListConstIterator<CSSProperty> it = style->valuesIterator(); it != end; ++it) { + const CSSProperty& property = *it; + CSSValue* value = property.value(); + // The property value, if it's a percentage, may not reflect the actual computed value. + // For example: style="height: 1%; overflow: visible;" in quirksmode + // FIXME: There are others like this, see <rdar://problem/5195123> Slashdot copy/paste fidelity problem + if (value->cssValueType() == CSSValue::CSS_PRIMITIVE_VALUE) + if (static_cast<CSSPrimitiveValue*>(value)->primitiveType() == CSSPrimitiveValue::CSS_PERCENTAGE) + if (RefPtr<CSSValue> computedPropertyValue = computedStyleForElement->getPropertyCSSValue(property.id())) + fromComputedStyle->addParsedProperty(CSSProperty(property.id(), computedPropertyValue)); + } + + style->merge(fromComputedStyle.get()); + } + if (convert) + style->setProperty(CSSPropertyDisplay, CSSValueInline, true); + if (style->length() > 0) { + static const String stylePrefix(" style=\""); + append(result, stylePrefix); + appendAttributeValue(result, style->cssText(), documentIsHTML); + result.append('\"'); + } + } + + if (shouldSelfClose(el)) { + if (el->isHTMLElement()) + result.append(' '); // XHTML 1.0 <-> HTML compatibility. + result.append('/'); + } + result.append('>'); + break; + } + case Node::CDATA_SECTION_NODE: { + // FIXME: CDATA content is not escaped, but XMLSerializer (and possibly other callers) should raise an exception if it includes "]]>". + const CDATASection* n = static_cast<const CDATASection*>(node); + append(result, "<![CDATA["); + append(result, n->data()); + append(result, "]]>"); + break; + } + case Node::ATTRIBUTE_NODE: + case Node::ENTITY_NODE: + case Node::ENTITY_REFERENCE_NODE: + case Node::NOTATION_NODE: + case Node::XPATH_NAMESPACE_NODE: + ASSERT_NOT_REACHED(); + break; + } +} + +static String getStartMarkup(const Node *node, const Range *range, EAnnotateForInterchange annotate, bool convertBlocksToInlines = false, HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0) +{ + Vector<UChar> result; + appendStartMarkup(result, node, range, annotate, convertBlocksToInlines, namespaces); + return String::adopt(result); +} + +static inline bool doesHTMLForbidEndTag(const Node *node) +{ + if (node->isHTMLElement()) { + const HTMLElement* htmlElt = static_cast<const HTMLElement*>(node); + return (htmlElt->endTagRequirement() == TagStatusForbidden); + } + return false; +} + +// Rules of self-closure +// 1. No elements in HTML documents use the self-closing syntax. +// 2. Elements w/ children never self-close because they use a separate end tag. +// 3. HTML elements which do not have a "forbidden" end tag will close with a separate end tag. +// 4. Other elements self-close. +static inline bool shouldSelfClose(const Node *node) +{ + if (node->document()->isHTMLDocument()) + return false; + if (node->hasChildNodes()) + return false; + if (node->isHTMLElement() && !doesHTMLForbidEndTag(node)) + return false; + return true; +} + +static void appendEndMarkup(Vector<UChar>& result, const Node* node) +{ + if (!node->isElementNode() || shouldSelfClose(node) || (!node->hasChildNodes() && doesHTMLForbidEndTag(node))) + return; + + result.append('<'); + result.append('/'); + append(result, static_cast<const Element*>(node)->nodeNamePreservingCase()); + result.append('>'); +} + +static String getEndMarkup(const Node *node) +{ + Vector<UChar> result; + appendEndMarkup(result, node); + return String::adopt(result); +} + +static void appendMarkup(Vector<UChar>& result, Node* startNode, bool onlyIncludeChildren, Vector<Node*>* nodes, const HashMap<AtomicStringImpl*, AtomicStringImpl*>* namespaces = 0) +{ + HashMap<AtomicStringImpl*, AtomicStringImpl*> namespaceHash; + if (namespaces) + namespaceHash = *namespaces; + + if (!onlyIncludeChildren) { + if (nodes) + nodes->append(startNode); + + appendStartMarkup(result,startNode, 0, DoNotAnnotateForInterchange, false, &namespaceHash); + } + // print children + if (!(startNode->document()->isHTMLDocument() && doesHTMLForbidEndTag(startNode))) + for (Node* current = startNode->firstChild(); current; current = current->nextSibling()) + appendMarkup(result, current, false, nodes, &namespaceHash); + + // Print my ending tag + if (!onlyIncludeChildren) + appendEndMarkup(result, startNode); +} + +static void completeURLs(Node* node, const String& baseURL) +{ + Vector<AttributeChange> changes; + + KURL parsedBaseURL(baseURL); + + Node* end = node->traverseNextSibling(); + for (Node* n = node; n != end; n = n->traverseNextNode()) { + if (n->isElementNode()) { + Element* e = static_cast<Element*>(n); + NamedAttrMap* attrs = e->attributes(); + unsigned length = attrs->length(); + for (unsigned i = 0; i < length; i++) { + Attribute* attr = attrs->attributeItem(i); + if (e->isURLAttribute(attr)) + changes.append(AttributeChange(e, attr->name(), KURL(parsedBaseURL, attr->value()).string())); + } + } + } + + size_t numChanges = changes.size(); + for (size_t i = 0; i < numChanges; ++i) + changes[i].apply(); +} + +static bool needInterchangeNewlineAfter(const VisiblePosition& v) +{ + VisiblePosition next = v.next(); + Node* upstreamNode = next.deepEquivalent().upstream().node(); + Node* downstreamNode = v.deepEquivalent().downstream().node(); + // Add an interchange newline if a paragraph break is selected and a br won't already be added to the markup to represent it. + return isEndOfParagraph(v) && isStartOfParagraph(next) && !(upstreamNode->hasTagName(brTag) && upstreamNode == downstreamNode); +} + +static PassRefPtr<CSSMutableStyleDeclaration> styleFromMatchedRulesAndInlineDecl(const Node* node) +{ + if (!node->isHTMLElement()) + return 0; + + // FIXME: Having to const_cast here is ugly, but it is quite a bit of work to untangle + // the non-const-ness of styleFromMatchedRulesForElement. + HTMLElement* element = const_cast<HTMLElement*>(static_cast<const HTMLElement*>(node)); + RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesForElement(element); + RefPtr<CSSMutableStyleDeclaration> inlineStyleDecl = element->getInlineStyleDecl(); + style->merge(inlineStyleDecl.get()); + return style.release(); +} + +static bool propertyMissingOrEqualToNone(CSSMutableStyleDeclaration* style, int propertyID) +{ + if (!style) + return false; + RefPtr<CSSValue> value = style->getPropertyCSSValue(propertyID); + if (!value) + return true; + if (!value->isPrimitiveValue()) + return false; + return static_cast<CSSPrimitiveValue*>(value.get())->getIdent() == CSSValueNone; +} + +static bool elementHasTextDecorationProperty(const Node* node) +{ + RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(node); + if (!style) + return false; + return !propertyMissingOrEqualToNone(style.get(), CSSPropertyTextDecoration); +} + +String joinMarkups(const Vector<String> preMarkups, const Vector<String>& postMarkups) +{ + size_t length = 0; + + size_t preCount = preMarkups.size(); + for (size_t i = 0; i < preCount; ++i) + length += preMarkups[i].length(); + + size_t postCount = postMarkups.size(); + for (size_t i = 0; i < postCount; ++i) + length += postMarkups[i].length(); + + Vector<UChar> result; + result.reserveCapacity(length); + + for (size_t i = preCount; i > 0; --i) + append(result, preMarkups[i - 1]); + + for (size_t i = 0; i < postCount; ++i) + append(result, postMarkups[i]); + + return String::adopt(result); +} + +// FIXME: Shouldn't we omit style info when annotate == DoNotAnnotateForInterchange? +// FIXME: At least, annotation and style info should probably not be included in range.markupString() +String createMarkup(const Range* range, Vector<Node*>* nodes, EAnnotateForInterchange annotate, bool convertBlocksToInlines) +{ + static const String interchangeNewlineString = String("<br class=\"") + AppleInterchangeNewline + "\">"; + + if (!range) + return ""; + + Document* document = range->ownerDocument(); + if (!document) + return ""; + + bool documentIsHTML = document->isHTMLDocument(); + + // Disable the delete button so it's elements are not serialized into the markup, + // but make sure neither endpoint is inside the delete user interface. + Frame* frame = document->frame(); + DeleteButtonController* deleteButton = frame ? frame->editor()->deleteButtonController() : 0; + RefPtr<Range> updatedRange = avoidIntersectionWithNode(range, deleteButton ? deleteButton->containerElement() : 0); + if (!updatedRange) + return ""; + + if (deleteButton) + deleteButton->disable(); + + ExceptionCode ec = 0; + bool collapsed = updatedRange->collapsed(ec); + ASSERT(ec == 0); + if (collapsed) + return ""; + Node* commonAncestor = updatedRange->commonAncestorContainer(ec); + ASSERT(ec == 0); + if (!commonAncestor) + return ""; + + document->updateLayoutIgnorePendingStylesheets(); + + Vector<String> markups; + Vector<String> preMarkups; + Node* pastEnd = updatedRange->pastLastNode(); + Node* lastClosed = 0; + Vector<Node*> ancestorsToClose; + + Node* startNode = updatedRange->firstNode(); + VisiblePosition visibleStart(updatedRange->startPosition(), VP_DEFAULT_AFFINITY); + VisiblePosition visibleEnd(updatedRange->endPosition(), VP_DEFAULT_AFFINITY); + if (annotate && needInterchangeNewlineAfter(visibleStart)) { + if (visibleStart == visibleEnd.previous()) { + if (deleteButton) + deleteButton->enable(); + return interchangeNewlineString; + } + + markups.append(interchangeNewlineString); + startNode = visibleStart.next().deepEquivalent().node(); + } + + Node* next; + for (Node* n = startNode; n != pastEnd; n = next) { + + // According to <rdar://problem/5730668>, it is possible for n to blow past pastEnd and become null here. This + // shouldn't be possible. This null check will prevent crashes (but create too much markup) and the ASSERT will + // hopefully lead us to understanding the problem. + ASSERT(n); + if (!n) + break; + + next = n->traverseNextNode(); + bool skipDescendants = false; + bool addMarkupForNode = true; + + if (!n->renderer() && !enclosingNodeWithTag(Position(n, 0), selectTag)) { + skipDescendants = true; + addMarkupForNode = false; + next = n->traverseNextSibling(); + // Don't skip over pastEnd. + if (pastEnd && pastEnd->isDescendantOf(n)) + next = pastEnd; + } + + if (isBlock(n) && canHaveChildrenForEditing(n) && next == pastEnd) + // Don't write out empty block containers that aren't fully selected. + continue; + + // Add the node to the markup. + if (addMarkupForNode) { + markups.append(getStartMarkup(n, updatedRange.get(), annotate)); + if (nodes) + nodes->append(n); + } + + if (n->firstChild() == 0 || skipDescendants) { + // Node has no children, or we are skipping it's descendants, add its close tag now. + if (addMarkupForNode) { + markups.append(getEndMarkup(n)); + lastClosed = n; + } + + // Check if the node is the last leaf of a tree. + if (!n->nextSibling() || next == pastEnd) { + if (!ancestorsToClose.isEmpty()) { + // Close up the ancestors. + do { + Node *ancestor = ancestorsToClose.last(); + if (next != pastEnd && next->isDescendantOf(ancestor)) + break; + // Not at the end of the range, close ancestors up to sibling of next node. + markups.append(getEndMarkup(ancestor)); + lastClosed = ancestor; + ancestorsToClose.removeLast(); + } while (!ancestorsToClose.isEmpty()); + } + + // Surround the currently accumulated markup with markup for ancestors we never opened as we leave the subtree(s) rooted at those ancestors. + Node* nextParent = next ? next->parentNode() : 0; + if (next != pastEnd && n != nextParent) { + Node* lastAncestorClosedOrSelf = n->isDescendantOf(lastClosed) ? lastClosed : n; + for (Node *parent = lastAncestorClosedOrSelf->parent(); parent != 0 && parent != nextParent; parent = parent->parentNode()) { + // All ancestors that aren't in the ancestorsToClose list should either be a) unrendered: + if (!parent->renderer()) + continue; + // or b) ancestors that we never encountered during a pre-order traversal starting at startNode: + ASSERT(startNode->isDescendantOf(parent)); + preMarkups.append(getStartMarkup(parent, updatedRange.get(), annotate)); + markups.append(getEndMarkup(parent)); + if (nodes) + nodes->append(parent); + lastClosed = parent; + } + } + } + } else if (addMarkupForNode && !skipDescendants) + // We added markup for this node, and we're descending into it. Set it to close eventually. + ancestorsToClose.append(n); + } + + // Include ancestors that aren't completely inside the range but are required to retain + // the structure and appearance of the copied markup. + Node* specialCommonAncestor = 0; + Node* commonAncestorBlock = commonAncestor ? enclosingBlock(commonAncestor) : 0; + if (annotate && commonAncestorBlock) { + if (commonAncestorBlock->hasTagName(tbodyTag) || commonAncestorBlock->hasTagName(trTag)) { + Node* table = commonAncestorBlock->parentNode(); + while (table && !table->hasTagName(tableTag)) + table = table->parentNode(); + if (table) + specialCommonAncestor = table; + } else if (commonAncestorBlock->hasTagName(listingTag) + || commonAncestorBlock->hasTagName(olTag) + || commonAncestorBlock->hasTagName(preTag) + || commonAncestorBlock->hasTagName(tableTag) + || commonAncestorBlock->hasTagName(ulTag) + || commonAncestorBlock->hasTagName(xmpTag)) + specialCommonAncestor = commonAncestorBlock; + } + + bool selectedOneOrMoreParagraphs = startOfParagraph(visibleStart) != startOfParagraph(visibleEnd) || + isStartOfParagraph(visibleStart) && isEndOfParagraph(visibleEnd); + + // Retain the Mail quote level by including all ancestor mail block quotes. + if (lastClosed && annotate && selectedOneOrMoreParagraphs) { + for (Node *ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) + if (isMailBlockquote(ancestor)) + specialCommonAncestor = ancestor; + } + + Node* checkAncestor = specialCommonAncestor ? specialCommonAncestor : commonAncestor; + if (checkAncestor->renderer()) { + RefPtr<CSSMutableStyleDeclaration> checkAncestorStyle = computedStyle(checkAncestor)->copyInheritableProperties(); + if (!propertyMissingOrEqualToNone(checkAncestorStyle.get(), CSSPropertyWebkitTextDecorationsInEffect)) + specialCommonAncestor = enclosingNodeOfType(Position(checkAncestor, 0), &elementHasTextDecorationProperty); + } + + // If a single tab is selected, commonAncestor will be a text node inside a tab span. + // If two or more tabs are selected, commonAncestor will be the tab span. + // In either case, if there is a specialCommonAncestor already, it will necessarily be above + // any tab span that needs to be included. + if (!specialCommonAncestor && isTabSpanTextNode(commonAncestor)) + specialCommonAncestor = commonAncestor->parentNode(); + if (!specialCommonAncestor && isTabSpanNode(commonAncestor)) + specialCommonAncestor = commonAncestor; + + if (Node *enclosingAnchor = enclosingNodeWithTag(Position(specialCommonAncestor ? specialCommonAncestor : commonAncestor, 0), aTag)) + specialCommonAncestor = enclosingAnchor; + + Node* body = enclosingNodeWithTag(Position(commonAncestor, 0), bodyTag); + // FIXME: Only include markup for a fully selected root (and ancestors of lastClosed up to that root) if + // there are styles/attributes on those nodes that need to be included to preserve the appearance of the copied markup. + // FIXME: Do this for all fully selected blocks, not just the body. + Node* fullySelectedRoot = body && *Selection::selectionFromContentsOfNode(body).toRange() == *updatedRange ? body : 0; + if (annotate && fullySelectedRoot) + specialCommonAncestor = fullySelectedRoot; + + if (specialCommonAncestor && lastClosed) { + // Also include all of the ancestors of lastClosed up to this special ancestor. + for (Node* ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) { + if (ancestor == fullySelectedRoot && !convertBlocksToInlines) { + RefPtr<CSSMutableStyleDeclaration> style = styleFromMatchedRulesAndInlineDecl(fullySelectedRoot); + + // Bring the background attribute over, but not as an attribute because a background attribute on a div + // appears to have no effect. + if (!style->getPropertyCSSValue(CSSPropertyBackgroundImage) && static_cast<Element*>(fullySelectedRoot)->hasAttribute(backgroundAttr)) + style->setProperty(CSSPropertyBackgroundImage, "url('" + static_cast<Element*>(fullySelectedRoot)->getAttribute(backgroundAttr) + "')"); + + if (style->length()) { + Vector<UChar> openTag; + static const String divStyle("<div style=\""); + append(openTag, divStyle); + appendAttributeValue(openTag, style->cssText(), documentIsHTML); + openTag.append('\"'); + openTag.append('>'); + preMarkups.append(String::adopt(openTag)); + + static const String divCloseTag("</div>"); + markups.append(divCloseTag); + } + } else { + preMarkups.append(getStartMarkup(ancestor, updatedRange.get(), annotate, convertBlocksToInlines)); + markups.append(getEndMarkup(ancestor)); + } + if (nodes) + nodes->append(ancestor); + + lastClosed = ancestor; + + if (ancestor == specialCommonAncestor) + break; + } + } + + static const String styleSpanOpen = String("<span class=\"" AppleStyleSpanClass "\" style=\""); + static const String styleSpanClose("</span>"); + + // Add a wrapper span with the styles that all of the nodes in the markup inherit. + Node* parentOfLastClosed = lastClosed ? lastClosed->parentNode() : 0; + if (parentOfLastClosed && parentOfLastClosed->renderer()) { + RefPtr<CSSMutableStyleDeclaration> style = computedStyle(parentOfLastClosed)->copyInheritableProperties(); + + // Styles that Mail blockquotes contribute should only be placed on the Mail blockquote, to help + // us differentiate those styles from ones that the user has applied. This helps us + // get the color of content pasted into blockquotes right. + removeEnclosingMailBlockquoteStyle(style.get(), parentOfLastClosed); + + // Document default styles will be added on another wrapper span. + removeDefaultStyles(style.get(), document); + + // Since we are converting blocks to inlines, remove any inherited block properties that are in the style. + // This cuts out meaningless properties and prevents properties from magically affecting blocks later + // if the style is cloned for a new block element during a future editing operation. + if (convertBlocksToInlines) + style->removeBlockProperties(); + + if (style->length() > 0) { + Vector<UChar> openTag; + append(openTag, styleSpanOpen); + appendAttributeValue(openTag, style->cssText(), documentIsHTML); + openTag.append('\"'); + openTag.append('>'); + preMarkups.append(String::adopt(openTag)); + + markups.append(styleSpanClose); + } + } + + if (lastClosed && lastClosed != document->documentElement()) { + // Add a style span with the document's default styles. We add these in a separate + // span so that at paste time we can differentiate between document defaults and user + // applied styles. + RefPtr<CSSMutableStyleDeclaration> defaultStyle = computedStyle(document->documentElement())->copyInheritableProperties(); + + if (defaultStyle->length() > 0) { + Vector<UChar> openTag; + append(openTag, styleSpanOpen); + appendAttributeValue(openTag, defaultStyle->cssText(), documentIsHTML); + openTag.append('\"'); + openTag.append('>'); + preMarkups.append(String::adopt(openTag)); + markups.append(styleSpanClose); + } + } + + // FIXME: The interchange newline should be placed in the block that it's in, not after all of the content, unconditionally. + if (annotate && needInterchangeNewlineAfter(visibleEnd.previous())) + markups.append(interchangeNewlineString); + + if (deleteButton) + deleteButton->enable(); + + return joinMarkups(preMarkups, markups); +} + +PassRefPtr<DocumentFragment> createFragmentFromMarkup(Document* document, const String& markup, const String& baseURL) +{ + ASSERT(document->documentElement()->isHTMLElement()); + // FIXME: What if the document element is not an HTML element? + HTMLElement *element = static_cast<HTMLElement*>(document->documentElement()); + + RefPtr<DocumentFragment> fragment = element->createContextualFragment(markup); + + if (fragment && !baseURL.isEmpty() && baseURL != blankURL() && baseURL != document->baseURL()) + completeURLs(fragment.get(), baseURL); + + return fragment.release(); +} + +String createMarkup(const Node* node, EChildrenOnly includeChildren, Vector<Node*>* nodes) +{ + Vector<UChar> result; + + if (!node) + return ""; + + Document* document = node->document(); + Frame* frame = document->frame(); + DeleteButtonController* deleteButton = frame ? frame->editor()->deleteButtonController() : 0; + + // disable the delete button so it's elements are not serialized into the markup + if (deleteButton) { + if (node->isDescendantOf(deleteButton->containerElement())) + return ""; + deleteButton->disable(); + } + + appendMarkup(result, const_cast<Node*>(node), includeChildren, nodes); + + if (deleteButton) + deleteButton->enable(); + + return String::adopt(result); +} + +static void fillContainerFromString(ContainerNode* paragraph, const String& string) +{ + Document* document = paragraph->document(); + + ExceptionCode ec = 0; + if (string.isEmpty()) { + paragraph->appendChild(createBlockPlaceholderElement(document), ec); + ASSERT(ec == 0); + return; + } + + ASSERT(string.find('\n') == -1); + + Vector<String> tabList; + string.split('\t', true, tabList); + String tabText = ""; + bool first = true; + size_t numEntries = tabList.size(); + for (size_t i = 0; i < numEntries; ++i) { + const String& s = tabList[i]; + + // append the non-tab textual part + if (!s.isEmpty()) { + if (!tabText.isEmpty()) { + paragraph->appendChild(createTabSpanElement(document, tabText), ec); + ASSERT(ec == 0); + tabText = ""; + } + RefPtr<Node> textNode = document->createTextNode(stringWithRebalancedWhitespace(s, first, i + 1 == numEntries)); + paragraph->appendChild(textNode.release(), ec); + ASSERT(ec == 0); + } + + // there is a tab after every entry, except the last entry + // (if the last character is a tab, the list gets an extra empty entry) + if (i + 1 != numEntries) + tabText.append('\t'); + else if (!tabText.isEmpty()) { + paragraph->appendChild(createTabSpanElement(document, tabText), ec); + ASSERT(ec == 0); + } + + first = false; + } +} + +PassRefPtr<DocumentFragment> createFragmentFromText(Range* context, const String& text) +{ + if (!context) + return 0; + + Node* styleNode = context->firstNode(); + if (!styleNode) { + styleNode = context->startPosition().node(); + if (!styleNode) + return 0; + } + + Document* document = styleNode->document(); + RefPtr<DocumentFragment> fragment = document->createDocumentFragment(); + + if (text.isEmpty()) + return fragment.release(); + + String string = text; + string.replace("\r\n", "\n"); + string.replace('\r', '\n'); + + ExceptionCode ec = 0; + RenderObject* renderer = styleNode->renderer(); + if (renderer && renderer->style()->preserveNewline()) { + fragment->appendChild(document->createTextNode(string), ec); + ASSERT(ec == 0); + if (string.endsWith("\n")) { + RefPtr<Element> element; + element = document->createElementNS(xhtmlNamespaceURI, "br", ec); + ASSERT(ec == 0); + element->setAttribute(classAttr, AppleInterchangeNewline); + fragment->appendChild(element.release(), ec); + ASSERT(ec == 0); + } + return fragment.release(); + } + + // A string with no newlines gets added inline, rather than being put into a paragraph. + if (string.find('\n') == -1) { + fillContainerFromString(fragment.get(), string); + return fragment.release(); + } + + // Break string into paragraphs. Extra line breaks turn into empty paragraphs. + Node* block = enclosingBlock(context->firstNode()); + bool useClonesOfEnclosingBlock = block && !block->hasTagName(bodyTag) && !block->hasTagName(htmlTag) && block != editableRootForPosition(context->startPosition()); + + Vector<String> list; + string.split('\n', true, list); // true gets us empty strings in the list + size_t numLines = list.size(); + for (size_t i = 0; i < numLines; ++i) { + const String& s = list[i]; + + RefPtr<Element> element; + if (s.isEmpty() && i + 1 == numLines) { + // For last line, use the "magic BR" rather than a P. + element = document->createElementNS(xhtmlNamespaceURI, "br", ec); + ASSERT(ec == 0); + element->setAttribute(classAttr, AppleInterchangeNewline); + } else { + element = useClonesOfEnclosingBlock ? static_cast<Element*>(block->cloneNode(false).get()) : createDefaultParagraphElement(document); + fillContainerFromString(element.get(), s); + } + fragment->appendChild(element.release(), ec); + ASSERT(ec == 0); + } + return fragment.release(); +} + +PassRefPtr<DocumentFragment> createFragmentFromNodes(Document *document, const Vector<Node*>& nodes) +{ + if (!document) + return 0; + + // disable the delete button so it's elements are not serialized into the markup + if (document->frame()) + document->frame()->editor()->deleteButtonController()->disable(); + + RefPtr<DocumentFragment> fragment = document->createDocumentFragment(); + + ExceptionCode ec = 0; + size_t size = nodes.size(); + for (size_t i = 0; i < size; ++i) { + RefPtr<Element> element = createDefaultParagraphElement(document); + element->appendChild(nodes[i], ec); + ASSERT(ec == 0); + fragment->appendChild(element.release(), ec); + ASSERT(ec == 0); + } + + if (document->frame()) + document->frame()->editor()->deleteButtonController()->enable(); + + return fragment.release(); +} + +String createFullMarkup(const Node* node) +{ + if (!node) + return String(); + + Document* document = node->document(); + if (!document) + return String(); + + Frame* frame = document->frame(); + if (!frame) + return String(); + + // FIXME: This is never "for interchange". Is that right? + String markupString = createMarkup(node, IncludeNode, 0); + Node::NodeType nodeType = node->nodeType(); + if (nodeType != Node::DOCUMENT_NODE && nodeType != Node::DOCUMENT_TYPE_NODE) + markupString = frame->documentTypeString() + markupString; + + return markupString; +} + +String createFullMarkup(const Range* range) +{ + if (!range) + return String(); + + Node* node = range->startContainer(); + if (!node) + return String(); + + Document* document = node->document(); + if (!document) + return String(); + + Frame* frame = document->frame(); + if (!frame) + return String(); + + // FIXME: This is always "for interchange". Is that right? See the previous method. + return frame->documentTypeString() + createMarkup(range, 0, AnnotateForInterchange); +} + +} |