webengine/osswebengine/WebKit/Misc/WebNSAttributedStringExtras.mm
changeset 0 dd21522fd290
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webengine/osswebengine/WebKit/Misc/WebNSAttributedStringExtras.mm	Mon Mar 30 12:54:55 2009 +0300
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2005 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. 
+ * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ *     its contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission. 
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "WebNSAttributedStringExtras.h"
+
+#import "DOMRangeInternal.h"
+#import "WebDataSourcePrivate.h"
+#import "WebFrame.h"
+#import "WebFrameBridge.h"
+#import "WebFrameInternal.h"
+#import <WebCore/BlockExceptions.h>
+#import <WebCore/ColorMac.h>
+#import <WebCore/CSSHelper.h>
+#import <WebCore/Document.h>
+#import <WebCore/Element.h>
+#import <WebCore/FontData.h>
+#import <WebCore/Frame.h>
+#import <WebCore/FrameLoader.h>
+#import <WebCore/HTMLNames.h>
+#import <WebCore/Image.h>
+#import <WebCore/InlineTextBox.h>
+#import <WebCore/KURL.h>
+#import <WebCore/Range.h>
+#import <WebCore/RenderImage.h>
+#import <WebCore/RenderListItem.h>
+#import <WebCore/RenderObject.h>
+#import <WebCore/RenderStyle.h>
+#import <WebCore/RenderText.h>
+#import <WebCore/Text.h>
+
+using namespace WebCore;
+using namespace HTMLNames;
+
+struct ListItemInfo {
+    unsigned start;
+    unsigned end;
+};
+
+static Element* listParent(Element* item)
+{
+    while (!item->hasTagName(ulTag) && !item->hasTagName(olTag)) {
+        item = static_cast<Element*>(item->parentNode());
+        if (!item)
+            break;
+    }
+    return item;
+}
+
+static Node* isTextFirstInListItem(Node* e)
+{
+    if (!e->isTextNode())
+        return 0;
+    Node* par = e->parentNode();
+    while (par) {
+        if (par->firstChild() != e)
+            return 0;
+        if (par->hasTagName(liTag))
+            return par;
+        e = par;
+        par = par->parentNode();
+    }
+    return 0;
+}
+
+static NSFileWrapper *fileWrapperForElement(Element* e)
+{
+    NSFileWrapper *wrapper = nil;
+    BEGIN_BLOCK_OBJC_EXCEPTIONS;
+    
+    const AtomicString& attr = e->getAttribute(srcAttr);
+    if (!attr.isEmpty()) {
+        NSURL *URL = KURL(e->document()->completeURL(attr.deprecatedString())).getNSURL();
+        wrapper = [[kit(e->document()->frame()) _dataSource] _fileWrapperForURL:URL];
+    }
+    if (!wrapper) {
+        RenderImage* renderer = static_cast<RenderImage*>(e->renderer());
+        if (renderer->cachedImage() && !renderer->cachedImage()->errorOccurred()) {
+            wrapper = [[NSFileWrapper alloc] initRegularFileWithContents:(NSData *)(renderer->cachedImage()->image()->getTIFFRepresentation())];
+            [wrapper setPreferredFilename:@"image.tiff"];
+            [wrapper autorelease];
+        }
+    }
+
+    return wrapper;
+
+    END_BLOCK_OBJC_EXCEPTIONS;
+
+    return nil;
+}
+
+@implementation NSAttributedString (WebKitExtras)
+
+- (NSAttributedString *)_web_attributedStringByStrippingAttachmentCharacters
+{
+    // This code was originally copied from NSTextView
+    NSRange attachmentRange;
+    NSString *originalString = [self string];
+    static NSString *attachmentCharString = nil;
+    
+    if (!attachmentCharString) {
+        unichar chars[2];
+        if (!attachmentCharString) {
+            chars[0] = NSAttachmentCharacter;
+            chars[1] = 0;
+            attachmentCharString = [[NSString alloc] initWithCharacters:chars length:1];
+        }
+    }
+    
+    attachmentRange = [originalString rangeOfString:attachmentCharString];
+    if (attachmentRange.location != NSNotFound && attachmentRange.length > 0) {
+        NSMutableAttributedString *newAttributedString = [[self mutableCopyWithZone:NULL] autorelease];
+        
+        while (attachmentRange.location != NSNotFound && attachmentRange.length > 0) {
+            [newAttributedString replaceCharactersInRange:attachmentRange withString:@""];
+            attachmentRange = [[newAttributedString string] rangeOfString:attachmentCharString];
+        }
+        return newAttributedString;
+    }
+    
+    return self;
+}
+
+// FIXME: Use WebCore::TextIterator to iterate text runs.
+
++ (NSAttributedString *)_web_attributedStringFromRange:(Range*)range
+{
+    ListItemInfo info;
+    ExceptionCode ec = 0; // dummy variable -- we ignore DOM exceptions
+    NSMutableAttributedString *result;
+    BEGIN_BLOCK_OBJC_EXCEPTIONS;
+
+    if (!range || !range->boundaryPointsValid())
+        return nil;
+    
+    Node* firstNode = range->startNode();
+    if (!firstNode)
+        return nil;
+    Node* pastEndNode = range->pastEndNode();
+    
+    int startOffset = range->startOffset(ec);
+    int endOffset = range->endOffset(ec);
+    Node* endNode = range->endContainer(ec);
+
+    result = [[[NSMutableAttributedString alloc] init] autorelease];
+    
+    bool hasNewLine = true;
+    bool addedSpace = true;
+    NSAttributedString *pendingStyledSpace = nil;
+    bool hasParagraphBreak = true;
+    const Element *linkStartNode = 0;
+    unsigned linkStartLocation = 0;
+    Vector<Element*> listItems;
+    Vector<ListItemInfo> listItemLocations;
+    float maxMarkerWidth = 0;
+    
+    Node *currentNode = firstNode;
+    
+    // If the first item is the entire text of a list item, use the list item node as the start of the 
+    // selection, not the text node.  The user's intent was probably to select the list.
+    if (currentNode->isTextNode() && startOffset == 0) {
+        Node *startListNode = isTextFirstInListItem(firstNode);
+        if (startListNode){
+            firstNode = startListNode;
+            currentNode = firstNode;
+        }
+    }
+    
+    while (currentNode && currentNode != pastEndNode) {
+        RenderObject *renderer = currentNode->renderer();
+        if (renderer) {
+            RenderStyle *style = renderer->style();
+            NSFont *font = style->font().primaryFont()->getNSFont();
+            bool needSpace = pendingStyledSpace != nil;
+            if (currentNode->isTextNode()) {
+                if (hasNewLine) {
+                    addedSpace = true;
+                    needSpace = false;
+                    [pendingStyledSpace release];
+                    pendingStyledSpace = nil;
+                    hasNewLine = false;
+                }
+                DeprecatedString text;
+                DeprecatedString str = currentNode->nodeValue().deprecatedString();
+                int start = (currentNode == firstNode) ? startOffset : -1;
+                int end = (currentNode == endNode) ? endOffset : -1;
+                if (renderer->isText()) {
+                    if (!style->collapseWhiteSpace()) {
+                        if (needSpace && !addedSpace) {
+                            if (text.isEmpty() && linkStartLocation == [result length])
+                                ++linkStartLocation;
+                            [result appendAttributedString:pendingStyledSpace];
+                        }
+                        int runStart = (start == -1) ? 0 : start;
+                        int runEnd = (end == -1) ? str.length() : end;
+                        text += str.mid(runStart, runEnd-runStart);
+                        [pendingStyledSpace release];
+                        pendingStyledSpace = nil;
+                        addedSpace = u_charDirection(str[runEnd - 1].unicode()) == U_WHITE_SPACE_NEUTRAL;
+                    }
+                    else {
+                        RenderText* textObj = static_cast<RenderText*>(renderer);
+                        if (!textObj->firstTextBox() && str.length() > 0 && !addedSpace) {
+                            // We have no runs, but we do have a length.  This means we must be
+                            // whitespace that collapsed away at the end of a line.
+                            text += ' ';
+                            addedSpace = true;
+                        }
+                        else {
+                            addedSpace = false;
+                            for (InlineTextBox* box = textObj->firstTextBox(); box; box = box->nextTextBox()) {
+                                int runStart = (start == -1) ? box->m_start : start;
+                                int runEnd = (end == -1) ? box->m_start + box->m_len : end;
+                                if (runEnd > box->m_start + box->m_len)
+                                    runEnd = box->m_start + box->m_len;
+                                if (runStart >= box->m_start &&
+                                    runStart < box->m_start + box->m_len) {
+                                    if (box == textObj->firstTextBox() && box->m_start == runStart && runStart > 0)
+                                        needSpace = true; // collapsed space at the start
+                                    if (needSpace && !addedSpace) {
+                                        if (pendingStyledSpace != nil) {
+                                            if (text.isEmpty() && linkStartLocation == [result length])
+                                                ++linkStartLocation;
+                                            [result appendAttributedString:pendingStyledSpace];
+                                        } else
+                                            text += ' ';
+                                    }
+                                    DeprecatedString runText = str.mid(runStart, runEnd - runStart);
+                                    runText.replace('\n', ' ');
+                                    text += runText;
+                                    int nextRunStart = box->nextTextBox() ? box->nextTextBox()->m_start : str.length(); // collapsed space between runs or at the end
+                                    needSpace = nextRunStart > runEnd;
+                                    [pendingStyledSpace release];
+                                    pendingStyledSpace = nil;
+                                    addedSpace = u_charDirection(str[runEnd - 1].unicode()) == U_WHITE_SPACE_NEUTRAL;
+                                    start = -1;
+                                }
+                                if (end != -1 && runEnd >= end)
+                                    break;
+                            }
+                        }
+                    }
+                }
+                
+                text.replace('\\', renderer->backslashAsCurrencySymbol());
+    
+                if (text.length() > 0 || needSpace) {
+                    NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init];
+                    [attrs setObject:font forKey:NSFontAttributeName];
+                    if (style && style->color().isValid() && style->color().alpha() != 0)
+                        [attrs setObject:nsColor(style->color()) forKey:NSForegroundColorAttributeName];
+                    if (style && style->backgroundColor().isValid() && style->backgroundColor().alpha() != 0)
+                        [attrs setObject:nsColor(style->backgroundColor()) forKey:NSBackgroundColorAttributeName];
+
+                    if (text.length() > 0) {
+                        hasParagraphBreak = false;
+                        NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:text.getNSString() attributes:attrs];
+                        [result appendAttributedString: partialString];                
+                        [partialString release];
+                    }
+
+                    if (needSpace) {
+                        [pendingStyledSpace release];
+                        pendingStyledSpace = [[NSAttributedString alloc] initWithString:@" " attributes:attrs];
+                    }
+
+                    [attrs release];
+                }
+            } else {
+                // This is our simple HTML -> ASCII transformation:
+                DeprecatedString text;
+                if (currentNode->hasTagName(aTag)) {
+                    // Note the start of the <a> element.  We will add the NSLinkAttributeName
+                    // attribute to the attributed string when navigating to the next sibling 
+                    // of this node.
+                    linkStartLocation = [result length];
+                    linkStartNode = static_cast<Element*>(currentNode);
+                } else if (currentNode->hasTagName(brTag)) {
+                    text += "\n";
+                    hasNewLine = true;
+                } else if (currentNode->hasTagName(liTag)) {
+                    DeprecatedString listText;
+                    Element *itemParent = listParent(static_cast<Element*>(currentNode));
+                    
+                    if (!hasNewLine)
+                        listText += '\n';
+                    hasNewLine = true;
+
+                    listItems.append(static_cast<Element*>(currentNode));
+                    info.start = [result length];
+                    info.end = 0;
+                    listItemLocations.append (info);
+                    
+                    listText += '\t';
+                    if (itemParent && renderer->isListItem()) {
+                        RenderListItem* listRenderer = static_cast<RenderListItem*>(renderer);
+
+                        maxMarkerWidth = MAX([font pointSize], maxMarkerWidth);
+
+                        String marker = listRenderer->markerText();
+                        if (!marker.isEmpty()) {
+                            listText += marker.deprecatedString();
+                            // Use AppKit metrics, since this will be rendered by AppKit.
+                            NSString *markerNSString = marker;
+                            float markerWidth = [markerNSString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]].width;
+                            maxMarkerWidth = MAX(markerWidth, maxMarkerWidth);
+                        }
+
+                        listText += ' ';
+                        listText += '\t';
+
+                        NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init];
+                        [attrs setObject:font forKey:NSFontAttributeName];
+                        if (style && style->color().isValid())
+                            [attrs setObject:nsColor(style->color()) forKey:NSForegroundColorAttributeName];
+                        if (style && style->backgroundColor().isValid())
+                            [attrs setObject:nsColor(style->backgroundColor()) forKey:NSBackgroundColorAttributeName];
+
+                        NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:listText.getNSString() attributes:attrs];
+                        [attrs release];
+                        [result appendAttributedString: partialString];                
+                        [partialString release];
+                    }
+                } else if (currentNode->hasTagName(olTag) || currentNode->hasTagName(ulTag)) {
+                    if (!hasNewLine)
+                        text += "\n";
+                    hasNewLine = true;
+                } else if (currentNode->hasTagName(blockquoteTag)
+                        || currentNode->hasTagName(ddTag)
+                        || currentNode->hasTagName(divTag)
+                        || currentNode->hasTagName(dlTag)
+                        || currentNode->hasTagName(dtTag)
+                        || currentNode->hasTagName(hrTag)
+                        || currentNode->hasTagName(listingTag)
+                        || currentNode->hasTagName(preTag)
+                        || currentNode->hasTagName(tdTag)
+                        || currentNode->hasTagName(thTag)) {
+                    if (!hasNewLine)
+                        text += '\n';
+                    hasNewLine = true;
+                } else if (currentNode->hasTagName(h1Tag)
+                        || currentNode->hasTagName(h2Tag)
+                        || currentNode->hasTagName(h3Tag)
+                        || currentNode->hasTagName(h4Tag)
+                        || currentNode->hasTagName(h5Tag)
+                        || currentNode->hasTagName(h6Tag)
+                        || currentNode->hasTagName(pTag)
+                        || currentNode->hasTagName(trTag)) {
+                    if (!hasNewLine)
+                        text += '\n';
+                    
+                    // In certain cases, emit a paragraph break.
+                    int bottomMargin = renderer->collapsedMarginBottom();
+                    int fontSize = style->fontDescription().computedPixelSize();
+                    if (bottomMargin * 2 >= fontSize) {
+                        if (!hasParagraphBreak) {
+                            text += '\n';
+                            hasParagraphBreak = true;
+                        }
+                    }
+                    
+                    hasNewLine = true;
+                }
+                else if (currentNode->hasTagName(imgTag)) {
+                    if (pendingStyledSpace != nil) {
+                        if (linkStartLocation == [result length])
+                            ++linkStartLocation;
+                        [result appendAttributedString:pendingStyledSpace];
+                        [pendingStyledSpace release];
+                        pendingStyledSpace = nil;
+                    }
+                    NSFileWrapper *fileWrapper = fileWrapperForElement(static_cast<Element*>(currentNode));
+                    NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper:fileWrapper];
+                    NSAttributedString *iString = [NSAttributedString attributedStringWithAttachment:attachment];
+                    [result appendAttributedString: iString];
+                    [attachment release];
+                }
+
+                NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:text.getNSString()];
+                [result appendAttributedString: partialString];
+                [partialString release];
+            }
+        }
+
+        Node *nextNode = currentNode->firstChild();
+        if (!nextNode)
+            nextNode = currentNode->nextSibling();
+
+        while (!nextNode && currentNode->parentNode()) {
+            DeprecatedString text;
+            currentNode = currentNode->parentNode();
+            if (currentNode == pastEndNode)
+                break;
+            nextNode = currentNode->nextSibling();
+
+            if (currentNode->hasTagName(aTag)) {
+                // End of a <a> element.  Create an attributed string NSLinkAttributeName attribute
+                // for the range of the link.  Note that we create the attributed string from the DOM, which
+                // will have corrected any illegally nested <a> elements.
+                if (linkStartNode && currentNode == linkStartNode) {
+                    String href = parseURL(linkStartNode->getAttribute(hrefAttr));
+                    KURL kURL = linkStartNode->document()->frame()->loader()->completeURL(href.deprecatedString());
+                    
+                    NSURL *URL = kURL.getNSURL();
+                    NSRange tempRange = { linkStartLocation, [result length]-linkStartLocation }; // workaround for 4213314
+                    [result addAttribute:NSLinkAttributeName value:URL range:tempRange];
+                    linkStartNode = 0;
+                }
+            }
+            else if (currentNode->hasTagName(olTag) || currentNode->hasTagName(ulTag)) {
+                if (!hasNewLine)
+                    text += '\n';
+                hasNewLine = true;
+            } else if (currentNode->hasTagName(liTag)) {
+                
+                int i, count = listItems.size();
+                for (i = 0; i < count; i++){
+                    if (listItems[i] == currentNode){
+                        listItemLocations[i].end = [result length];
+                        break;
+                    }
+                }
+                if (!hasNewLine)
+                    text += '\n';
+                hasNewLine = true;
+            } else if (currentNode->hasTagName(blockquoteTag) ||
+                       currentNode->hasTagName(ddTag) ||
+                       currentNode->hasTagName(divTag) ||
+                       currentNode->hasTagName(dlTag) ||
+                       currentNode->hasTagName(dtTag) ||
+                       currentNode->hasTagName(hrTag) ||
+                       currentNode->hasTagName(listingTag) ||
+                       currentNode->hasTagName(preTag) ||
+                       currentNode->hasTagName(tdTag) ||
+                       currentNode->hasTagName(thTag)) {
+                if (!hasNewLine)
+                    text += '\n';
+                hasNewLine = true;
+            } else if (currentNode->hasTagName(pTag) ||
+                       currentNode->hasTagName(trTag) ||
+                       currentNode->hasTagName(h1Tag) ||
+                       currentNode->hasTagName(h2Tag) ||
+                       currentNode->hasTagName(h3Tag) ||
+                       currentNode->hasTagName(h4Tag) ||
+                       currentNode->hasTagName(h5Tag) ||
+                       currentNode->hasTagName(h6Tag)) {
+                if (!hasNewLine)
+                    text += '\n';
+                // An extra newline is needed at the start, not the end, of these types of tags,
+                // so don't add another here.
+                hasNewLine = true;
+            }
+            
+            NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:text.getNSString()];
+            [result appendAttributedString:partialString];
+            [partialString release];
+        }
+
+        currentNode = nextNode;
+    }
+    
+    [pendingStyledSpace release];
+    
+    // Apply paragraph styles from outside in.  This ensures that nested lists correctly
+    // override their parent's paragraph style.
+    {
+        unsigned i, count = listItems.size();
+        Element *e;
+
+#ifdef POSITION_LIST
+        Node *containingBlock;
+        int containingBlockX, containingBlockY;
+        
+        // Determine the position of the outermost containing block.  All paragraph
+        // styles and tabs should be relative to this position.  So, the horizontal position of 
+        // each item in the list (in the resulting attributed string) will be relative to position 
+        // of the outermost containing block.
+        if (count > 0){
+            containingBlock = firstNode;
+            while (containingBlock->renderer()->isInline()){
+                containingBlock = containingBlock->parentNode();
+            }
+            containingBlock->renderer()->absolutePosition(containingBlockX, containingBlockY);
+        }
+#endif
+        
+        for (i = 0; i < count; i++){
+            e = listItems[i];
+            info = listItemLocations[i];
+            
+            if (info.end < info.start)
+                info.end = [result length];
+                
+            RenderObject *r = e->renderer();
+            RenderStyle *style = r->style();
+
+            int rx;
+            NSFont *font = style->font().primaryFont()->getNSFont();
+            float pointSize = [font pointSize];
+
+#ifdef POSITION_LIST
+            int ry;
+            r->absolutePosition(rx, ry);
+            rx -= containingBlockX;
+            
+            // Ensure that the text is indented at least enough to allow for the markers.
+            rx = MAX(rx, (int)maxMarkerWidth);
+#else
+            rx = (int)MAX(maxMarkerWidth, pointSize);
+#endif
+
+            // The bullet text will be right aligned at the first tab marker, followed
+            // by a space, followed by the list item text.  The space is arbitrarily
+            // picked as pointSize*2/3.  The space on the first line of the text item
+            // is established by a left aligned tab, on subsequent lines it's established
+            // by the head indent.
+            NSMutableParagraphStyle *mps = [[NSMutableParagraphStyle alloc] init];
+            [mps setFirstLineHeadIndent: 0];
+            [mps setHeadIndent: rx];
+            [mps setTabStops:[NSArray arrayWithObjects:
+                        [[[NSTextTab alloc] initWithType:NSRightTabStopType location:rx-(pointSize*2/3)] autorelease],
+                        [[[NSTextTab alloc] initWithType:NSLeftTabStopType location:rx] autorelease],
+                        nil]];
+            NSRange tempRange = { info.start, info.end-info.start }; // workaround for 4213314
+            [result addAttribute:NSParagraphStyleAttributeName value:mps range:tempRange];
+            [mps release];
+        }
+    }
+
+    return result;
+
+    END_BLOCK_OBJC_EXCEPTIONS;
+
+    return nil;
+}
+
+@end