| /* |
| * Copyright (C) 2005, 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. |
| * 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/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/SimpleFontData.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 = e->document()->completeURL(attr); |
| 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; |
| } |
| String text; |
| String str = currentNode->nodeValue(); |
| 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.substring(runStart, runEnd-runStart); |
| [pendingStyledSpace release]; |
| pendingStyledSpace = nil; |
| addedSpace = u_charDirection(str.characters()[runEnd - 1]) == 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.append(' '); |
| 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.append(' '); |
| } |
| String runText = str.substring(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.characters()[runEnd - 1]) == 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 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: |
| String 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.append('\n'); |
| hasNewLine = true; |
| } else if (currentNode->hasTagName(liTag)) { |
| String listText; |
| Element *itemParent = listParent(static_cast<Element*>(currentNode)); |
| |
| if (!hasNewLine) |
| listText.append('\n'); |
| hasNewLine = true; |
| |
| listItems.append(static_cast<Element*>(currentNode)); |
| info.start = [result length]; |
| info.end = 0; |
| listItemLocations.append (info); |
| |
| listText.append('\t'); |
| if (itemParent && renderer->isListItem()) { |
| RenderListItem* listRenderer = static_cast<RenderListItem*>(renderer); |
| |
| maxMarkerWidth = MAX([font pointSize], maxMarkerWidth); |
| |
| String marker = listRenderer->markerText(); |
| if (!marker.isEmpty()) { |
| listText.append(marker); |
| // 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.append(' '); |
| listText.append('\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 attributes:attrs]; |
| [attrs release]; |
| [result appendAttributedString: partialString]; |
| [partialString release]; |
| } |
| } else if (currentNode->hasTagName(olTag) || currentNode->hasTagName(ulTag)) { |
| if (!hasNewLine) |
| text.append('\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.append('\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.append('\n'); |
| |
| // In certain cases, emit a paragraph break. |
| int bottomMargin = renderer->collapsedMarginBottom(); |
| int fontSize = style->fontDescription().computedPixelSize(); |
| if (bottomMargin * 2 >= fontSize) { |
| if (!hasParagraphBreak) { |
| text.append('\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]; |
| [result appendAttributedString: partialString]; |
| [partialString release]; |
| } |
| } |
| |
| Node* nextNode = currentNode->firstChild(); |
| if (!nextNode) |
| nextNode = currentNode->nextSibling(); |
| |
| while (!nextNode && currentNode->parentNode()) { |
| String 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) { |
| NSURL *URL = linkStartNode->document()->frame()->loader()->completeURL(parseURL(linkStartNode->getAttribute(hrefAttr))); |
| 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.append('\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.append('\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.append('\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.append('\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]; |
| [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 |