EditorIOS.mm   [plain text]


/*
 * Copyright (C) 2006, 2007, 2013 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 INC. AND ITS CONTRIBUTORS ``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 INC. OR ITS 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.
 */

#import "config.h"
#import "Editor.h"

#import "CSSComputedStyleDeclaration.h"
#import "CSSPrimitiveValueMappings.h"
#import "CachedImage.h"
#import "CachedResourceLoader.h"
#import "DataTransfer.h"
#import "DictationCommandIOS.h"
#import "DocumentFragment.h"
#import "DocumentLoader.h"
#import "DocumentMarkerController.h"
#import "EditorClient.h"
#import "FontCascade.h"
#import "Frame.h"
#import "FrameLoaderClient.h"
#import "HTMLAnchorElement.h"
#import "HTMLConverter.h"
#import "HTMLImageElement.h"
#import "HTMLInputElement.h"
#import "HTMLNames.h"
#import "HTMLParserIdioms.h"
#import "HTMLTextAreaElement.h"
#import "LegacyWebArchive.h"
#import "NSAttributedStringSPI.h"
#import "NodeTraversal.h"
#import "Page.h"
#import "Pasteboard.h"
#import "RenderBlock.h"
#import "RenderImage.h"
#import "SharedBuffer.h"
#import "SoftLinking.h"
#import "StyleProperties.h"
#import "Text.h"
#import "TypingCommand.h"
#import "WAKAppKitStubs.h"
#import "htmlediting.h"
#import "markup.h"
#import <MobileCoreServices/MobileCoreServices.h>
#import <wtf/BlockObjCExceptions.h>
#include <wtf/text/StringBuilder.h>

SOFT_LINK_FRAMEWORK(AppSupport)
SOFT_LINK(AppSupport, CPSharedResourcesDirectory, CFStringRef, (void), ())

@interface NSAttributedString (NSAttributedStringKitAdditions)
- (id)initWithRTF:(NSData *)data documentAttributes:(NSDictionary **)dict;
- (id)initWithRTFD:(NSData *)data documentAttributes:(NSDictionary **)dict;
- (NSData *)RTFFromRange:(NSRange)range documentAttributes:(NSDictionary *)dict;
- (NSData *)RTFDFromRange:(NSRange)range documentAttributes:(NSDictionary *)dict;
- (BOOL)containsAttachments;
@end

namespace WebCore {

using namespace HTMLNames;

void Editor::showFontPanel()
{
}

void Editor::showStylesPanel()
{
}

void Editor::showColorPanel()
{
}

void Editor::setTextAlignmentForChangedBaseWritingDirection(WritingDirection direction)
{
    // Note that the passed-in argument is the direction that has been changed to by
    // some code or user interaction outside the scope of this function. The former
    // direction is not known, nor is it required for the kind of text alignment
    // changes done by this function.
    //
    // Rules:
    // When text has no explicit alignment, set to alignment to match the writing direction.
    // If the text has left or right alignment, flip left->right and right->left. 
    // Otherwise, do nothing.

    RefPtr<EditingStyle> selectionStyle = EditingStyle::styleAtSelectionStart(m_frame.selection().selection());
    if (!selectionStyle || !selectionStyle->style())
         return;

    RefPtr<CSSPrimitiveValue> value = static_pointer_cast<CSSPrimitiveValue>(selectionStyle->style()->getPropertyCSSValue(CSSPropertyTextAlign));
    if (!value)
        return;
        
    const char *newValue = nullptr;
    ETextAlign textAlign = *value;
    switch (textAlign) {
        case TASTART:
        case TAEND:
        {
            switch (direction) {
                case NaturalWritingDirection:
                    // no-op
                    break;
                case LeftToRightWritingDirection:
                    newValue = "left";
                    break;
                case RightToLeftWritingDirection:
                    newValue = "right";
                    break;
            }
            break;
        }
        case LEFT:
        case WEBKIT_LEFT:
            newValue = "right";
            break;
        case RIGHT:
        case WEBKIT_RIGHT:
            newValue = "left";
            break;
        case CENTER:
        case WEBKIT_CENTER:
        case JUSTIFY:
            // no-op
            break;
    }

    if (!newValue)
        return;

    Element* focusedElement = m_frame.document()->focusedElement();
    if (focusedElement && (is<HTMLTextAreaElement>(*focusedElement) || (is<HTMLInputElement>(*focusedElement)
        && (downcast<HTMLInputElement>(*focusedElement).isTextField()
            || downcast<HTMLInputElement>(*focusedElement).isSearchField())))) {
        if (direction == NaturalWritingDirection)
            return;
        downcast<HTMLElement>(*focusedElement).setAttributeWithoutSynchronization(alignAttr, newValue);
        m_frame.document()->updateStyleIfNeeded();
        return;
    }

    RefPtr<MutableStyleProperties> style = MutableStyleProperties::create();
    style->setProperty(CSSPropertyTextAlign, newValue);
    applyParagraphStyle(style.get());
}

const Font* Editor::fontForSelection(bool& hasMultipleFonts) const
{
    hasMultipleFonts = false;

    if (!m_frame.selection().isRange()) {
        Node* nodeToRemove;
        auto* style = styleForSelectionStart(&m_frame, nodeToRemove); // sets nodeToRemove

        const Font* result = nullptr;
        if (style) {
            result = &style->fontCascade().primaryFont();
            if (nodeToRemove)
                nodeToRemove->remove();
        }

        return result;
    }

    const Font* font = nullptr;
    RefPtr<Range> range = m_frame.selection().toNormalizedRange();
    if (Node* startNode = adjustedSelectionStartForStyleComputation(m_frame.selection().selection()).deprecatedNode()) {
        Node* pastEnd = range->pastLastNode();
        // In the loop below, n should eventually match pastEnd and not become nil, but we've seen at least one
        // unreproducible case where this didn't happen, so check for null also.
        for (Node* node = startNode; node && node != pastEnd; node = NodeTraversal::next(*node)) {
            auto renderer = node->renderer();
            if (!renderer)
                continue;
            // FIXME: Are there any node types that have renderers, but that we should be skipping?
            const Font& primaryFont = renderer->style().fontCascade().primaryFont();
            if (!font)
                font = &primaryFont;
            else if (font != &primaryFont) {
                hasMultipleFonts = true;
                break;
            }
        }
    }

    return font;
}

NSDictionary* Editor::fontAttributesForSelectionStart() const
{
    Node* nodeToRemove;
    auto* style = styleForSelectionStart(&m_frame, nodeToRemove);
    if (!style)
        return nil;

    NSMutableDictionary* result = [NSMutableDictionary dictionary];
    
    CTFontRef font = style->fontCascade().primaryFont().getCTFont();
    if (font)
        [result setObject:(id)font forKey:NSFontAttributeName];

    getTextDecorationAttributesRespectingTypingStyle(*style, result);

    if (nodeToRemove)
        nodeToRemove->remove();
    
    return result;
}

void Editor::removeUnchangeableStyles()
{
    // This function removes styles that the user cannot modify by applying their default values.
    
    RefPtr<EditingStyle> editingStyle = EditingStyle::create(m_frame.document()->bodyOrFrameset());
    RefPtr<MutableStyleProperties> defaultStyle = editingStyle.get()->style()->mutableCopy();
    
    // Text widgets implement background color via the UIView property. Their body element will not have one.
    defaultStyle->setProperty(CSSPropertyBackgroundColor, "rgba(255, 255, 255, 0.0)");
    
    // Remove properties that the user can modify, like font-weight. 
    // Also remove font-family, per HI spec.
    // FIXME: it'd be nice if knowledge about which styles were unchangeable was not hard-coded here.
    defaultStyle->removeProperty(CSSPropertyFontWeight);
    defaultStyle->removeProperty(CSSPropertyFontStyle);
    defaultStyle->removeProperty(CSSPropertyFontVariantCaps);
    // FIXME: we should handle also pasted quoted text, strikethrough, etc. <rdar://problem/9255115>
    defaultStyle->removeProperty(CSSPropertyTextDecoration);
    defaultStyle->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); // implements underline

    // FIXME add EditActionMatchStlye <rdar://problem/9156507> Undo rich text's paste & match style should say "Undo Match Style"
    applyStyleToSelection(defaultStyle.get(), EditActionChangeAttributes);
}

static RefPtr<SharedBuffer> dataInRTFDFormat(NSAttributedString *string)
{
    NSUInteger length = string.length;
    if (!length)
        return nullptr;

    BEGIN_BLOCK_OBJC_EXCEPTIONS;
    return SharedBuffer::wrapNSData([string RTFDFromRange:NSMakeRange(0, length) documentAttributes:nil]);
    END_BLOCK_OBJC_EXCEPTIONS;

    return nullptr;
}

static RefPtr<SharedBuffer> dataInRTFFormat(NSAttributedString *string)
{
    NSUInteger length = string.length;
    if (!length)
        return nullptr;

    BEGIN_BLOCK_OBJC_EXCEPTIONS;
    return SharedBuffer::wrapNSData([string RTFFromRange:NSMakeRange(0, length) documentAttributes:nil]);
    END_BLOCK_OBJC_EXCEPTIONS;

    return nullptr;
}

String Editor::stringSelectionForPasteboardWithImageAltText()
{
    String text = selectedTextForDataTransfer();
    text.replace(noBreakSpace, ' ');
    return text;
}

RefPtr<SharedBuffer> Editor::selectionInWebArchiveFormat()
{
    RefPtr<LegacyWebArchive> archive = LegacyWebArchive::createFromSelection(&m_frame);
    if (!archive)
        return nullptr;
    return SharedBuffer::wrapCFData(archive->rawDataRepresentation().get());
}

void Editor::writeSelectionToPasteboard(Pasteboard& pasteboard)
{
    NSAttributedString *attributedString = attributedStringFromRange(*selectedRange());

    PasteboardWebContent content;
    content.canSmartCopyOrDelete = canSmartCopyOrDelete();
    content.dataInWebArchiveFormat = selectionInWebArchiveFormat();
    content.dataInRTFDFormat = [attributedString containsAttachments] ? dataInRTFDFormat(attributedString) : 0;
    content.dataInRTFFormat = dataInRTFFormat(attributedString);
    content.dataInStringFormat = stringSelectionForPasteboardWithImageAltText();
    client()->getClientPasteboardDataForRange(selectedRange().get(), content.clientTypes, content.clientData);

    pasteboard.write(content);
}

static void getImage(Element& imageElement, RefPtr<Image>& image, CachedImage*& cachedImage)
{
    auto* renderer = imageElement.renderer();
    if (!is<RenderImage>(renderer))
        return;

    CachedImage* tentativeCachedImage = downcast<RenderImage>(*renderer).cachedImage();
    if (!tentativeCachedImage || tentativeCachedImage->errorOccurred())
        return;

    image = tentativeCachedImage->imageForRenderer(renderer);
    if (!image)
        return;
    
    cachedImage = tentativeCachedImage;
}

void Editor::writeImageToPasteboard(Pasteboard& pasteboard, Element& imageElement, const URL&, const String& title)
{
    PasteboardImage pasteboardImage;

    CachedImage* cachedImage;
    getImage(imageElement, pasteboardImage.image, cachedImage);
    if (!pasteboardImage.image)
        return;
    ASSERT(cachedImage);

    pasteboardImage.url.url = imageElement.document().completeURL(stripLeadingAndTrailingHTMLSpaces(imageElement.imageSourceURL()));
    pasteboardImage.url.title = title;
    pasteboardImage.resourceMIMEType = pasteboard.resourceMIMEType(cachedImage->response().mimeType());
    pasteboardImage.resourceData = cachedImage->resourceBuffer();

    pasteboard.write(pasteboardImage);
}

class Editor::WebContentReader final : public PasteboardWebContentReader {
public:
    WebContentReader(Frame& frame, Range& context, bool allowPlainText)
        : frame(frame)
        , context(context)
        , allowPlainText(allowPlainText)
        , madeFragmentFromPlainText(false)
    {
    }

    void addFragment(RefPtr<DocumentFragment>&&);

    Frame& frame;
    Range& context;
    const bool allowPlainText;

    RefPtr<DocumentFragment> fragment;
    bool madeFragmentFromPlainText;

private:
    bool readWebArchive(SharedBuffer*) override;
    bool readFilenames(const Vector<String>&) override;
    bool readHTML(const String&) override;
    bool readRTFD(SharedBuffer&) override;
    bool readRTF(SharedBuffer&) override;
    bool readImage(Ref<SharedBuffer>&&, const String& type) override;
    bool readURL(const URL&, const String& title) override;
    bool readPlainText(const String&) override;
};

void Editor::WebContentReader::addFragment(RefPtr<DocumentFragment>&& newFragment)
{
    if (fragment) {
        if (newFragment && newFragment->firstChild())
            fragment->appendChild(*newFragment->firstChild());
    } else
        fragment = WTFMove(newFragment);
}

bool Editor::WebContentReader::readWebArchive(SharedBuffer* buffer)
{
    if (!frame.document())
        return false;

    if (!buffer)
        return false;

    RefPtr<LegacyWebArchive> archive = LegacyWebArchive::create(URL(), *buffer);
    if (!archive)
        return false;

    RefPtr<ArchiveResource> mainResource = archive->mainResource();
    if (!mainResource)
        return false;

    const String& type = mainResource->mimeType();

    if (frame.loader().client().canShowMIMETypeAsHTML(type)) {
        // FIXME: The code in createFragmentAndAddResources calls setDefersLoading(true). Don't we need that here?
        if (DocumentLoader* loader = frame.loader().documentLoader())
            loader->addAllArchiveResources(archive.get());

        String markupString = String::fromUTF8(mainResource->data().data(), mainResource->data().size());
        addFragment(createFragmentFromMarkup(*frame.document(), markupString, mainResource->url(), DisallowScriptingAndPluginContent));
        return true;
    }

    return false;
}

bool Editor::WebContentReader::readFilenames(const Vector<String>&)
{
    return false;
}

bool Editor::WebContentReader::readHTML(const String& string)
{
    if (!frame.document())
        return false;

    addFragment(createFragmentFromMarkup(*frame.document(), string, emptyString(), DisallowScriptingAndPluginContent));
    return true;
}

bool Editor::WebContentReader::readRTFD(SharedBuffer& buffer)
{
    addFragment(frame.editor().createFragmentAndAddResources(adoptNS([[NSAttributedString alloc] initWithRTFD:buffer.createNSData().get() documentAttributes:nullptr]).get()));
    return fragment;
}

bool Editor::WebContentReader::readRTF(SharedBuffer& buffer)
{
    addFragment(frame.editor().createFragmentAndAddResources(adoptNS([[NSAttributedString alloc] initWithRTF:buffer.createNSData().get() documentAttributes:nullptr]).get()));
    return fragment;
}

bool Editor::WebContentReader::readImage(Ref<SharedBuffer>&& buffer, const String& type)
{
    RetainPtr<CFStringRef> stringType = type.createCFString();
    RetainPtr<NSString> filenameExtension = adoptNS((NSString *)UTTypeCopyPreferredTagWithClass(stringType.get(), kUTTagClassFilenameExtension));
    NSString *relativeURLPart = [@"image" stringByAppendingString:filenameExtension.get()];
    RetainPtr<NSString> mimeType = adoptNS((NSString *)UTTypeCopyPreferredTagWithClass(stringType.get(), kUTTagClassMIMEType));

    addFragment(frame.editor().createFragmentForImageResourceAndAddResource(ArchiveResource::create(WTFMove(buffer), URL::fakeURLWithRelativePart(relativeURLPart), mimeType.get(), emptyString(), emptyString())));
    return fragment;
}

bool Editor::WebContentReader::readURL(const URL& url, const String&)
{
    if (url.isEmpty())
        return false;

    if (!frame.editor().client()->hasRichlyEditableSelection()) {
        if (readPlainText([(NSURL *)url absoluteString]))
            return true;
    }

    if ([(NSURL *)url isFileURL]) {
        NSString *localPath = [(NSURL *)url relativePath];
        // Only allow url attachments from ~/Media for now.
        if (![localPath hasPrefix:[(NSString *)CPSharedResourcesDirectory() stringByAppendingString:@"/Media/DCIM/"]])
            return false;

        RetainPtr<NSString> fileType = adoptNS((NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (CFStringRef)[localPath pathExtension], NULL));
        NSData *data = [NSData dataWithContentsOfFile:localPath];
        if (UTTypeConformsTo((CFStringRef)fileType.get(), kUTTypePNG)) {
            addFragment(frame.editor().createFragmentForImageResourceAndAddResource(ArchiveResource::create(SharedBuffer::wrapNSData([[data copy] autorelease]), URL::fakeURLWithRelativePart("image.png"), @"image/png", emptyString(), emptyString())));
            return fragment;
        } else if (UTTypeConformsTo((CFStringRef)fileType.get(), kUTTypeJPEG)) {
            addFragment(frame.editor().createFragmentForImageResourceAndAddResource(ArchiveResource::create(SharedBuffer::wrapNSData([[data copy] autorelease]), URL::fakeURLWithRelativePart("image.jpg"), @"image/jpg", emptyString(), emptyString())));
            return fragment;
        }
    } else {
        auto anchor = HTMLAnchorElement::create(*frame.document());
        anchor->setAttributeWithoutSynchronization(HTMLNames::hrefAttr, url.string());
        anchor->appendChild(frame.document()->createTextNode([[(NSURL *)url absoluteString] precomposedStringWithCanonicalMapping]));

        auto newFragment = frame.document()->createDocumentFragment();
        newFragment->appendChild(anchor);
        addFragment(WTFMove(newFragment));
        return true;
    }
    return false;
}

bool Editor::WebContentReader::readPlainText(const String& text)
{
    if (!allowPlainText)
        return false;

    addFragment(createFragmentFromText(context, [text precomposedStringWithCanonicalMapping]));
    if (!fragment)
        return false;

    madeFragmentFromPlainText = true;
    return true;
}

// FIXME: Should give this function a name that makes it clear it adds resources to the document loader as a side effect.
// Or refactor so it does not do that.
RefPtr<DocumentFragment> Editor::webContentFromPasteboard(Pasteboard& pasteboard, Range& context, bool allowPlainText, bool& chosePlainText)
{
    WebContentReader reader(m_frame, context, allowPlainText);
    pasteboard.read(reader);
    chosePlainText = reader.madeFragmentFromPlainText;
    return WTFMove(reader.fragment);
}

void Editor::pasteWithPasteboard(Pasteboard* pasteboard, bool allowPlainText, MailBlockquoteHandling mailBlockquoteHandling)
{
    RefPtr<Range> range = selectedRange();
    WebContentReader reader(m_frame, *range, allowPlainText);
    int numberOfPasteboardItems = client()->getPasteboardItemsCount();
    for (int i = 0; i < numberOfPasteboardItems; ++i) {
        RefPtr<DocumentFragment> fragment = client()->documentFragmentFromDelegate(i);
        if (!fragment)
            continue;

        reader.addFragment(WTFMove(fragment));
    }

    RefPtr<DocumentFragment> fragment = reader.fragment;
    if (!fragment) {
        bool chosePlainTextIgnored;
        fragment = webContentFromPasteboard(*pasteboard, *range, allowPlainText, chosePlainTextIgnored);
    }

    if (fragment && shouldInsertFragment(fragment, range, EditorInsertActionPasted))
        pasteAsFragment(fragment.releaseNonNull(), canSmartReplaceWithPasteboard(*pasteboard), false, mailBlockquoteHandling);
}

RefPtr<DocumentFragment> Editor::createFragmentAndAddResources(NSAttributedString *string)
{
    if (!m_frame.page() || !m_frame.document())
        return nullptr;

    auto& document = *m_frame.document();
    if (!document.isHTMLDocument() || !string)
        return nullptr;

    bool wasDeferringCallbacks = m_frame.page()->defersLoading();
    if (!wasDeferringCallbacks)
        m_frame.page()->setDefersLoading(true);

    auto& cachedResourceLoader = document.cachedResourceLoader();
    bool wasImagesEnabled = cachedResourceLoader.imagesEnabled();
    if (wasImagesEnabled)
        cachedResourceLoader.setImagesEnabled(false);

    auto fragmentAndResources = createFragment(string);

    if (DocumentLoader* loader = m_frame.loader().documentLoader()) {
        for (auto& resource : fragmentAndResources.resources) {
            if (resource)
                loader->addArchiveResource(resource.releaseNonNull());
        }
    }

    if (wasImagesEnabled)
        cachedResourceLoader.setImagesEnabled(true);
    if (!wasDeferringCallbacks)
        m_frame.page()->setDefersLoading(false);
    
    return WTFMove(fragmentAndResources.fragment);
}

RefPtr<DocumentFragment> Editor::createFragmentForImageResourceAndAddResource(RefPtr<ArchiveResource>&& resource)
{
    if (!resource)
        return nullptr;

    NSURL *URL = resource->url();
    String resourceURL = [URL isFileURL] ? [URL absoluteString] : resource->url();

    if (DocumentLoader* loader = m_frame.loader().documentLoader())
        loader->addArchiveResource(resource.releaseNonNull());

    auto imageElement = HTMLImageElement::create(*m_frame.document());
    imageElement->setAttributeWithoutSynchronization(HTMLNames::srcAttr, resourceURL);

    auto fragment = m_frame.document()->createDocumentFragment();
    fragment->appendChild(imageElement);

    return WTFMove(fragment);
}

void Editor::replaceSelectionWithAttributedString(NSAttributedString *attributedString, MailBlockquoteHandling mailBlockquoteHandling)
{
    if (m_frame.selection().isNone())
        return;

    if (m_frame.selection().selection().isContentRichlyEditable()) {
        RefPtr<DocumentFragment> fragment = createFragmentAndAddResources(attributedString);
        if (fragment && shouldInsertFragment(fragment, selectedRange(), EditorInsertActionPasted))
            pasteAsFragment(fragment.releaseNonNull(), false, false, mailBlockquoteHandling);
    } else {
        String text = [attributedString string];
        if (shouldInsertText(text, selectedRange().get(), EditorInsertActionPasted))
            pasteAsPlainText(text, false);
    }
}

void Editor::insertDictationPhrases(Vector<Vector<String>>&& dictationPhrases, RetainPtr<id> metadata)
{
    if (m_frame.selection().isNone())
        return;

    if (dictationPhrases.isEmpty())
        return;

    applyCommand(DictationCommandIOS::create(document(), WTFMove(dictationPhrases), WTFMove(metadata)));
}

void Editor::setDictationPhrasesAsChildOfElement(const Vector<Vector<String>>& dictationPhrases, RetainPtr<id> metadata, Element& element)
{
    // Clear the composition.
    clear();

    // Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack.
    // Some day we could make them Undoable, and let callers clear the Undo stack explicitly if they wish.
    clearUndoRedoOperations();

    m_frame.selection().clear();

    element.removeChildren();

    if (dictationPhrases.isEmpty()) {
        client()->respondToChangedContents();
        return;
    }

    RefPtr<Range> context = document().createRange();
    context->selectNodeContents(element);

    StringBuilder dictationPhrasesBuilder;
    for (auto& interpretations : dictationPhrases)
        dictationPhrasesBuilder.append(interpretations[0]);

    element.appendChild(createFragmentFromText(*context, dictationPhrasesBuilder.toString()));

    // We need a layout in order to add markers below.
    document().updateLayout();

    if (!element.firstChild()->isTextNode()) {
        // Shouldn't happen.
        ASSERT(element.firstChild()->isTextNode());
        return;
    }

    Text& textNode = downcast<Text>(*element.firstChild());
    int previousDictationPhraseStart = 0;
    for (auto& interpretations : dictationPhrases) {
        int dictationPhraseLength = interpretations[0].length();
        int dictationPhraseEnd = previousDictationPhraseStart + dictationPhraseLength;
        if (interpretations.size() > 1) {
            auto dictationPhraseRange = Range::create(document(), &textNode, previousDictationPhraseStart, &textNode, dictationPhraseEnd);
            document().markers().addDictationPhraseWithAlternativesMarker(dictationPhraseRange.ptr(), interpretations);
        }
        previousDictationPhraseStart = dictationPhraseEnd;
    }

    auto resultRange = Range::create(document(), &textNode, 0, &textNode, textNode.length());
    document().markers().addDictationResultMarker(resultRange.ptr(), metadata);

    client()->respondToChangedContents();
}

void Editor::confirmMarkedText()
{
    // FIXME: This is a hacky workaround for the keyboard calling this method too late -
    // after the selection and focus have already changed. See <rdar://problem/5975559>.
    Element* focused = document().focusedElement();
    Node* composition = compositionNode();
    if (composition && focused && focused != composition && !composition->isDescendantOrShadowDescendantOf(focused)) {
        cancelComposition();
        document().setFocusedElement(focused);
    } else
        confirmComposition();
}

void Editor::setTextAsChildOfElement(const String& text, Element& element)
{
    // Clear the composition
    clear();

    // Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack.
    // Some day we could make them Undoable, and let callers clear the Undo stack explicitly if they wish.
    clearUndoRedoOperations();

    // If the element is empty already and we're not adding text, we can early return and avoid clearing/setting
    // a selection at [0, 0] and the expense involved in creation VisiblePositions.
    if (!element.firstChild() && text.isEmpty())
        return;

    // As a side effect this function sets a caret selection after the inserted content. Much of what
    // follows is more expensive if there is a selection, so clear it since it's going to change anyway.
    m_frame.selection().clear();

    // clear out all current children of element
    element.removeChildren();

    if (text.length()) {
        // insert new text
        // remove element from tree while doing it
        // FIXME: The element we're inserting into is often the body element. It seems strange to be removing it
        // (even if it is only temporary). ReplaceSelectionCommand doesn't bother doing this when it inserts
        // content, why should we here?
        RefPtr<Node> parent = element.parentNode();
        RefPtr<Node> siblingAfter = element.nextSibling();
        if (parent)
            element.remove();

        auto context = document().createRange();
        context->selectNodeContents(element);
        element.appendChild(createFragmentFromText(context, text));

        // restore element to document
        if (parent) {
            if (siblingAfter)
                parent->insertBefore(element, siblingAfter.get());
            else
                parent->appendChild(element);
        }
    }

    // set the selection to the end
    VisibleSelection selection;

    Position pos = createLegacyEditingPosition(&element, element.countChildNodes());

    VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY);
    if (visiblePos.isNull())
        return;

    selection.setBase(visiblePos);
    selection.setExtent(visiblePos);

    m_frame.selection().setSelection(selection);

    client()->respondToChangedContents();
}

// If the selection is adjusted from UIKit without closing the typing, the typing command may
// have a stale selection.
void Editor::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping()
{
    TypingCommand::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping(&m_frame, m_frame.selection().selection());
}

} // namespace WebCore