/* * 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 #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import 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(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(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 listItems; Vector 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(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 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(currentNode); } else if (currentNode->hasTagName(brTag)) { text += "\n"; hasNewLine = true; } else if (currentNode->hasTagName(liTag)) { DeprecatedString listText; Element *itemParent = listParent(static_cast(currentNode)); if (!hasNewLine) listText += '\n'; hasNewLine = true; listItems.append(static_cast(currentNode)); info.start = [result length]; info.end = 0; listItemLocations.append (info); listText += '\t'; if (itemParent && renderer->isListItem()) { RenderListItem* listRenderer = static_cast(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(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 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 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