EditorCocoa.mm   [plain text]


/*
 * Copyright (C) 2006-2016 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. ``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
 * 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 "ArchiveResource.h"
#import "CSSValueList.h"
#import "CSSValuePool.h"
#import "CachedResourceLoader.h"
#import "ColorMac.h"
#import "DocumentFragment.h"
#import "DocumentLoader.h"
#import "Editing.h"
#import "EditingStyle.h"
#import "EditorClient.h"
#import "FontCascade.h"
#import "Frame.h"
#import "FrameLoader.h"
#import "FrameSelection.h"
#import "HTMLConverter.h"
#import "HTMLImageElement.h"
#import "HTMLSpanElement.h"
#import "LegacyWebArchive.h"
#import "NSAttributedStringSPI.h"
#import "Pasteboard.h"
#import "RenderElement.h"
#import "RenderStyle.h"
#import "Text.h"
#import <wtf/BlockObjCExceptions.h>
#import <wtf/SoftLinking.h>

#if PLATFORM(IOS)
SOFT_LINK_PRIVATE_FRAMEWORK(WebKitLegacy)
#endif

#if PLATFORM(MAC)
SOFT_LINK_FRAMEWORK_IN_UMBRELLA(WebKit, WebKitLegacy)
#endif

// FIXME: Get rid of this and change NSAttributedString conversion so it doesn't use WebKitLegacy (cf. rdar://problem/30597352).
SOFT_LINK(WebKitLegacy, _WebCreateFragment, void, (WebCore::Document& document, NSAttributedString *string, WebCore::FragmentAndResources& result), (document, string, result))

namespace WebCore {

void Editor::getTextDecorationAttributesRespectingTypingStyle(const RenderStyle& style, NSMutableDictionary* result) const
{
    RefPtr<EditingStyle> typingStyle = m_frame.selection().typingStyle();
    if (typingStyle && typingStyle->style()) {
        RefPtr<CSSValue> value = typingStyle->style()->getPropertyCSSValue(CSSPropertyWebkitTextDecorationsInEffect);
        if (value && value->isValueList()) {
            CSSValueList& valueList = downcast<CSSValueList>(*value);
            if (valueList.hasValue(CSSValuePool::singleton().createIdentifierValue(CSSValueLineThrough).ptr()))
                [result setObject:@(NSUnderlineStyleSingle) forKey:NSStrikethroughStyleAttributeName];
            if (valueList.hasValue(CSSValuePool::singleton().createIdentifierValue(CSSValueUnderline).ptr()))
                [result setObject:@(NSUnderlineStyleSingle) forKey:NSUnderlineStyleAttributeName];
        }
    } else {
        int decoration = style.textDecorationsInEffect();
        if (decoration & TextDecorationLineThrough)
            [result setObject:@(NSUnderlineStyleSingle) forKey:NSStrikethroughStyleAttributeName];
        if (decoration & TextDecorationUnderline)
            [result setObject:@(NSUnderlineStyleSingle) forKey:NSUnderlineStyleAttributeName];
    }
}

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

    RetainPtr<NSMutableDictionary> attributes = adoptNS([[NSMutableDictionary alloc] init]);

    if (auto ctFont = style->fontCascade().primaryFont().getCTFont())
        [attributes setObject:(id)ctFont forKey:NSFontAttributeName];

    // FIXME: Why would we not want to retrieve these attributes on iOS?
#if PLATFORM(MAC)
    if (style->visitedDependentColor(CSSPropertyBackgroundColor).isVisible())
        [attributes setObject:nsColor(style->visitedDependentColor(CSSPropertyBackgroundColor)) forKey:NSBackgroundColorAttributeName];

    if (style->visitedDependentColor(CSSPropertyColor).isValid() && !Color::isBlackColor(style->visitedDependentColor(CSSPropertyColor)))
        [attributes setObject:nsColor(style->visitedDependentColor(CSSPropertyColor)) forKey:NSForegroundColorAttributeName];

    const ShadowData* shadowData = style->textShadow();
    if (shadowData) {
        RetainPtr<NSShadow> platformShadow = adoptNS([[NSShadow alloc] init]);
        [platformShadow setShadowOffset:NSMakeSize(shadowData->x(), shadowData->y())];
        [platformShadow setShadowBlurRadius:shadowData->radius()];
        [platformShadow setShadowColor:nsColor(shadowData->color())];
        [attributes setObject:platformShadow.get() forKey:NSShadowAttributeName];
    }

    int superscriptInt = 0;
    switch (style->verticalAlign()) {
    case BASELINE:
    case BOTTOM:
    case BASELINE_MIDDLE:
    case LENGTH:
    case MIDDLE:
    case TEXT_BOTTOM:
    case TEXT_TOP:
    case TOP:
        break;
    case SUB:
        superscriptInt = -1;
        break;
    case SUPER:
        superscriptInt = 1;
        break;
    }
    if (superscriptInt)
        [attributes setObject:@(superscriptInt) forKey:NSSuperscriptAttributeName];
#endif

    getTextDecorationAttributesRespectingTypingStyle(*style, attributes.get());

    if (nodeToRemove)
        nodeToRemove->remove();

    return attributes;
}

FragmentAndResources Editor::createFragment(NSAttributedString *string)
{
    // FIXME: The algorithm to convert an attributed string into HTML should be implemented here in WebCore.
    // For now, though, we call into WebKitLegacy, which in turn calls into AppKit/TextKit.
    FragmentAndResources result;
    _WebCreateFragment(*m_frame.document(), string, result);
    return result;
}

static RefPtr<SharedBuffer> archivedDataForAttributedString(NSAttributedString *attributedString)
{
    if (!attributedString.length)
        return nullptr;

    return SharedBuffer::create([NSKeyedArchiver archivedDataWithRootObject:attributedString]);
}

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

    PasteboardWebContent content;
    content.canSmartCopyOrDelete = canSmartCopyOrDelete();
    content.dataInWebArchiveFormat = selectionInWebArchiveFormat();
    content.dataInRTFDFormat = attributedString.containsAttachments ? dataInRTFDFormat(attributedString) : nullptr;
    content.dataInRTFFormat = dataInRTFFormat(attributedString);
    content.dataInAttributedStringFormat = archivedDataForAttributedString(attributedString);
    // FIXME: Why don't we want this on iOS?
#if PLATFORM(MAC)
    content.dataInHTMLFormat = selectionInHTMLFormat();
#endif
    content.dataInStringFormat = stringSelectionForPasteboardWithImageAltText();
    client()->getClientPasteboardDataForRange(selectedRange().get(), content.clientTypes, content.clientData);

    pasteboard.write(content);
}

void Editor::writeSelection(PasteboardWriterData& pasteboardWriterData)
{
    NSAttributedString *attributedString = attributedStringFromRange(*selectedRange());

    PasteboardWriterData::WebContent webContent;
    webContent.canSmartCopyOrDelete = canSmartCopyOrDelete();
    webContent.dataInWebArchiveFormat = selectionInWebArchiveFormat();
    webContent.dataInRTFDFormat = attributedString.containsAttachments ? dataInRTFDFormat(attributedString) : nullptr;
    webContent.dataInRTFFormat = dataInRTFFormat(attributedString);
    webContent.dataInAttributedStringFormat = archivedDataForAttributedString(attributedString);
    // FIXME: Why don't we want this on iOS?
#if PLATFORM(MAC)
    webContent.dataInHTMLFormat = selectionInHTMLFormat();
#endif
    webContent.dataInStringFormat = stringSelectionForPasteboardWithImageAltText();
    client()->getClientPasteboardDataForRange(selectedRange().get(), webContent.clientTypes, webContent.clientData);

    pasteboardWriterData.setWebContent(WTFMove(webContent));
}

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

// FIXME: Makes no sense that selectedTextForDataTransfer always includes alt text, but stringSelectionForPasteboard does not.
// This was left in a bad state when selectedTextForDataTransfer was added. Need to look over clients and fix this.
String Editor::stringSelectionForPasteboard()
{
    if (!canCopy())
        return emptyString();
    String text = selectedText();
    text.replace(noBreakSpace, ' ');
    return text;
}

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

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().get(), EditorInsertAction::Pasted))
            pasteAsFragment(fragment.releaseNonNull(), false, false, mailBlockquoteHandling);
    } else {
        String text = attributedString.string;
        if (shouldInsertText(text, selectedRange().get(), EditorInsertAction::Pasted))
            pasteAsPlainText(text, false);
    }
}

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

    // FIXME: Why is this different?
#if PLATFORM(MAC)
    String resourceURL = resource->url().string();
#else
    NSURL *URL = resource->url();
    String resourceURL = URL.isFileURL ? URL.absoluteString : resource->url();
#endif

    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);
}

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

    BEGIN_BLOCK_OBJC_EXCEPTIONS;
    return SharedBuffer::create([string RTFDFromRange:NSMakeRange(0, length) documentAttributes:@{ }]);
    END_BLOCK_OBJC_EXCEPTIONS;

    return nullptr;
}

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

    BEGIN_BLOCK_OBJC_EXCEPTIONS;
    return SharedBuffer::create([string RTFFromRange:NSMakeRange(0, length) documentAttributes:@{ }]);
    END_BLOCK_OBJC_EXCEPTIONS;

    return nullptr;
}

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 (auto* loader = m_frame.loader().documentLoader()) {
        for (auto& resource : fragmentAndResources.resources)
            loader->addArchiveResource(WTFMove(resource));
    }

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

}