Reviewed by Darin.
[WebKit-https.git] / WebKit / Misc / WebNSAttributedStringExtras.mm
1 /*
2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23     * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import "WebNSAttributedStringExtras.h"
30
31 #import "DOMRangeInternal.h"
32 #import "WebDataSourcePrivate.h"
33 #import "WebFrame.h"
34 #import "WebFrameBridge.h"
35 #import "WebFrameInternal.h"
36 #import <WebCore/csshelper.h>
37 #import <WebCore/BlockExceptions.h>
38 #import <WebCore/Document.h>
39 #import <WebCore/Element.h>
40 #import <WebCore/FontData.h>
41 #import <WebCore/FrameLoader.h>
42 #import <WebCore/FrameMac.h>
43 #import <WebCore/HTMLNames.h>
44 #import <WebCore/Image.h>
45 #import <WebCore/InlineTextBox.h>
46 #import <WebCore/KURL.h>
47 #import <WebCore/Range.h>
48 #import <WebCore/RenderImage.h>
49 #import <WebCore/RenderListItem.h>
50 #import <WebCore/RenderObject.h>
51 #import <WebCore/RenderStyle.h>
52 #import <WebCore/RenderText.h>
53 #import <WebCore/Text.h>
54
55 using namespace WebCore;
56 using namespace HTMLNames;
57
58 struct ListItemInfo {
59     unsigned start;
60     unsigned end;
61 };
62
63 static Element* listParent(Element* item)
64 {
65     while (!item->hasTagName(ulTag) && !item->hasTagName(olTag)) {
66         item = static_cast<Element*>(item->parentNode());
67         if (!item)
68             break;
69     }
70     return item;
71 }
72
73 static Node* isTextFirstInListItem(Node* e)
74 {
75     if (!e->isTextNode())
76         return 0;
77     Node* par = e->parentNode();
78     while (par) {
79         if (par->firstChild() != e)
80             return 0;
81         if (par->hasTagName(liTag))
82             return par;
83         e = par;
84         par = par->parentNode();
85     }
86     return 0;
87 }
88
89 static NSFileWrapper *fileWrapperForElement(Element* e)
90 {
91     NSFileWrapper *wrapper = nil;
92     BEGIN_BLOCK_OBJC_EXCEPTIONS;
93     
94     const AtomicString& attr = e->getAttribute(srcAttr);
95     if (!attr.isEmpty()) {
96         NSURL *URL = KURL(e->document()->completeURL(attr.deprecatedString())).getNSURL();
97         wrapper = [[kit(e->document()->frame()) dataSource] _fileWrapperForURL:URL];
98     }
99     if (!wrapper) {
100         RenderImage* renderer = static_cast<RenderImage*>(e->renderer());
101         if (renderer->cachedImage() && !renderer->cachedImage()->isErrorImage()) {
102             wrapper = [[NSFileWrapper alloc] initRegularFileWithContents:(NSData *)(renderer->cachedImage()->image()->getTIFFRepresentation())];
103             [wrapper setPreferredFilename:@"image.tiff"];
104             [wrapper autorelease];
105         }
106     }
107
108     return wrapper;
109
110     END_BLOCK_OBJC_EXCEPTIONS;
111
112     return nil;
113 }
114
115 @implementation NSAttributedString (WebKitExtras)
116
117 - (NSAttributedString *)_web_attributedStringByStrippingAttachmentCharacters
118 {
119     // This code was originally copied from NSTextView
120     NSRange attachmentRange;
121     NSString *originalString = [self string];
122     static NSString *attachmentCharString = nil;
123     
124     if (!attachmentCharString) {
125         unichar chars[2];
126         if (!attachmentCharString) {
127             chars[0] = NSAttachmentCharacter;
128             chars[1] = 0;
129             attachmentCharString = [[NSString alloc] initWithCharacters:chars length:1];
130         }
131     }
132     
133     attachmentRange = [originalString rangeOfString:attachmentCharString];
134     if (attachmentRange.location != NSNotFound && attachmentRange.length > 0) {
135         NSMutableAttributedString *newAttributedString = [[self mutableCopyWithZone:NULL] autorelease];
136         
137         while (attachmentRange.location != NSNotFound && attachmentRange.length > 0) {
138             [newAttributedString replaceCharactersInRange:attachmentRange withString:@""];
139             attachmentRange = [[newAttributedString string] rangeOfString:attachmentCharString];
140         }
141         return newAttributedString;
142     }
143     
144     return self;
145 }
146
147 // FIXME: Use WebCore::TextIterator to iterate text runs.
148
149 + (NSAttributedString *)_web_attributedStringFromRange:(Range*)range
150 {
151     ListItemInfo info;
152     ExceptionCode ec = 0; // dummy variable -- we ignore DOM exceptions
153     NSMutableAttributedString *result;
154     BEGIN_BLOCK_OBJC_EXCEPTIONS;
155
156     if (!range || !range->boundaryPointsValid())
157         return nil;
158     
159     Node* firstNode = range->startNode();
160     if (!firstNode)
161         return nil;
162     Node* pastEndNode = range->pastEndNode();
163     
164     int startOffset = range->startOffset(ec);
165     int endOffset = range->endOffset(ec);
166     Node* endNode = range->endContainer(ec);
167
168     result = [[[NSMutableAttributedString alloc] init] autorelease];
169     
170     bool hasNewLine = true;
171     bool addedSpace = true;
172     NSAttributedString *pendingStyledSpace = nil;
173     bool hasParagraphBreak = true;
174     const Element *linkStartNode = 0;
175     unsigned linkStartLocation = 0;
176     Vector<Element*> listItems;
177     Vector<ListItemInfo> listItemLocations;
178     float maxMarkerWidth = 0;
179     
180     Node *currentNode = firstNode;
181     
182     // If the first item is the entire text of a list item, use the list item node as the start of the 
183     // selection, not the text node.  The user's intent was probably to select the list.
184     if (currentNode->isTextNode() && startOffset == 0) {
185         Node *startListNode = isTextFirstInListItem(firstNode);
186         if (startListNode){
187             firstNode = startListNode;
188             currentNode = firstNode;
189         }
190     }
191     
192     while (currentNode && currentNode != pastEndNode) {
193         RenderObject *renderer = currentNode->renderer();
194         if (renderer) {
195             RenderStyle *style = renderer->style();
196             NSFont *font = style->font().primaryFont()->getNSFont();
197             bool needSpace = pendingStyledSpace != nil;
198             if (currentNode->isTextNode()) {
199                 if (hasNewLine) {
200                     addedSpace = true;
201                     needSpace = false;
202                     [pendingStyledSpace release];
203                     pendingStyledSpace = nil;
204                     hasNewLine = false;
205                 }
206                 DeprecatedString text;
207                 DeprecatedString str = currentNode->nodeValue().deprecatedString();
208                 int start = (currentNode == firstNode) ? startOffset : -1;
209                 int end = (currentNode == endNode) ? endOffset : -1;
210                 if (renderer->isText()) {
211                     if (!style->collapseWhiteSpace()) {
212                         if (needSpace && !addedSpace) {
213                             if (text.isEmpty() && linkStartLocation == [result length])
214                                 ++linkStartLocation;
215                             [result appendAttributedString:pendingStyledSpace];
216                         }
217                         int runStart = (start == -1) ? 0 : start;
218                         int runEnd = (end == -1) ? str.length() : end;
219                         text += str.mid(runStart, runEnd-runStart);
220                         [pendingStyledSpace release];
221                         pendingStyledSpace = nil;
222                         addedSpace = u_charDirection(str[runEnd - 1].unicode()) == U_WHITE_SPACE_NEUTRAL;
223                     }
224                     else {
225                         RenderText* textObj = static_cast<RenderText*>(renderer);
226                         if (!textObj->firstTextBox() && str.length() > 0 && !addedSpace) {
227                             // We have no runs, but we do have a length.  This means we must be
228                             // whitespace that collapsed away at the end of a line.
229                             text += ' ';
230                             addedSpace = true;
231                         }
232                         else {
233                             addedSpace = false;
234                             for (InlineTextBox* box = textObj->firstTextBox(); box; box = box->nextTextBox()) {
235                                 int runStart = (start == -1) ? box->m_start : start;
236                                 int runEnd = (end == -1) ? box->m_start + box->m_len : end;
237                                 if (runEnd > box->m_start + box->m_len)
238                                     runEnd = box->m_start + box->m_len;
239                                 if (runStart >= box->m_start &&
240                                     runStart < box->m_start + box->m_len) {
241                                     if (box == textObj->firstTextBox() && box->m_start == runStart && runStart > 0)
242                                         needSpace = true; // collapsed space at the start
243                                     if (needSpace && !addedSpace) {
244                                         if (pendingStyledSpace != nil) {
245                                             if (text.isEmpty() && linkStartLocation == [result length])
246                                                 ++linkStartLocation;
247                                             [result appendAttributedString:pendingStyledSpace];
248                                         } else
249                                             text += ' ';
250                                     }
251                                     DeprecatedString runText = str.mid(runStart, runEnd - runStart);
252                                     runText.replace('\n', ' ');
253                                     text += runText;
254                                     int nextRunStart = box->nextTextBox() ? box->nextTextBox()->m_start : str.length(); // collapsed space between runs or at the end
255                                     needSpace = nextRunStart > runEnd;
256                                     [pendingStyledSpace release];
257                                     pendingStyledSpace = nil;
258                                     addedSpace = u_charDirection(str[runEnd - 1].unicode()) == U_WHITE_SPACE_NEUTRAL;
259                                     start = -1;
260                                 }
261                                 if (end != -1 && runEnd >= end)
262                                     break;
263                             }
264                         }
265                     }
266                 }
267                 
268                 text.replace('\\', renderer->backslashAsCurrencySymbol());
269     
270                 if (text.length() > 0 || needSpace) {
271                     NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init];
272                     [attrs setObject:font forKey:NSFontAttributeName];
273                     if (style && style->color().isValid() && style->color().alpha() != 0)
274                         [attrs setObject:nsColor(style->color()) forKey:NSForegroundColorAttributeName];
275                     if (style && style->backgroundColor().isValid() && style->backgroundColor().alpha() != 0)
276                         [attrs setObject:nsColor(style->backgroundColor()) forKey:NSBackgroundColorAttributeName];
277
278                     if (text.length() > 0) {
279                         hasParagraphBreak = false;
280                         NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:text.getNSString() attributes:attrs];
281                         [result appendAttributedString: partialString];                
282                         [partialString release];
283                     }
284
285                     if (needSpace) {
286                         [pendingStyledSpace release];
287                         pendingStyledSpace = [[NSAttributedString alloc] initWithString:@" " attributes:attrs];
288                     }
289
290                     [attrs release];
291                 }
292             } else {
293                 // This is our simple HTML -> ASCII transformation:
294                 DeprecatedString text;
295                 if (currentNode->hasTagName(aTag)) {
296                     // Note the start of the <a> element.  We will add the NSLinkAttributeName
297                     // attribute to the attributed string when navigating to the next sibling 
298                     // of this node.
299                     linkStartLocation = [result length];
300                     linkStartNode = static_cast<Element*>(currentNode);
301                 } else if (currentNode->hasTagName(brTag)) {
302                     text += "\n";
303                     hasNewLine = true;
304                 } else if (currentNode->hasTagName(liTag)) {
305                     DeprecatedString listText;
306                     Element *itemParent = listParent(static_cast<Element*>(currentNode));
307                     
308                     if (!hasNewLine)
309                         listText += '\n';
310                     hasNewLine = true;
311
312                     listItems.append(static_cast<Element*>(currentNode));
313                     info.start = [result length];
314                     info.end = 0;
315                     listItemLocations.append (info);
316                     
317                     listText += '\t';
318                     if (itemParent && renderer->isListItem()) {
319                         RenderListItem* listRenderer = static_cast<RenderListItem*>(renderer);
320
321                         maxMarkerWidth = MAX([font pointSize], maxMarkerWidth);
322
323                         String marker = listRenderer->markerText();
324                         if (!marker.isEmpty()) {
325                             listText += marker.deprecatedString();
326                             // Use AppKit metrics, since this will be rendered by AppKit.
327                             NSString *markerNSString = marker;
328                             float markerWidth = [markerNSString sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]].width;
329                             maxMarkerWidth = MAX(markerWidth, maxMarkerWidth);
330                         }
331
332                         listText += ' ';
333                         listText += '\t';
334
335                         NSMutableDictionary *attrs = [[NSMutableDictionary alloc] init];
336                         [attrs setObject:font forKey:NSFontAttributeName];
337                         if (style && style->color().isValid())
338                             [attrs setObject:nsColor(style->color()) forKey:NSForegroundColorAttributeName];
339                         if (style && style->backgroundColor().isValid())
340                             [attrs setObject:nsColor(style->backgroundColor()) forKey:NSBackgroundColorAttributeName];
341
342                         NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:listText.getNSString() attributes:attrs];
343                         [attrs release];
344                         [result appendAttributedString: partialString];                
345                         [partialString release];
346                     }
347                 } else if (currentNode->hasTagName(olTag) || currentNode->hasTagName(ulTag)) {
348                     if (!hasNewLine)
349                         text += "\n";
350                     hasNewLine = true;
351                 } else if (currentNode->hasTagName(blockquoteTag)
352                         || currentNode->hasTagName(ddTag)
353                         || currentNode->hasTagName(divTag)
354                         || currentNode->hasTagName(dlTag)
355                         || currentNode->hasTagName(dtTag)
356                         || currentNode->hasTagName(hrTag)
357                         || currentNode->hasTagName(listingTag)
358                         || currentNode->hasTagName(preTag)
359                         || currentNode->hasTagName(tdTag)
360                         || currentNode->hasTagName(thTag)) {
361                     if (!hasNewLine)
362                         text += '\n';
363                     hasNewLine = true;
364                 } else if (currentNode->hasTagName(h1Tag)
365                         || currentNode->hasTagName(h2Tag)
366                         || currentNode->hasTagName(h3Tag)
367                         || currentNode->hasTagName(h4Tag)
368                         || currentNode->hasTagName(h5Tag)
369                         || currentNode->hasTagName(h6Tag)
370                         || currentNode->hasTagName(pTag)
371                         || currentNode->hasTagName(trTag)) {
372                     if (!hasNewLine)
373                         text += '\n';
374                     
375                     // In certain cases, emit a paragraph break.
376                     int bottomMargin = renderer->collapsedMarginBottom();
377                     int fontSize = style->fontDescription().computedPixelSize();
378                     if (bottomMargin * 2 >= fontSize) {
379                         if (!hasParagraphBreak) {
380                             text += '\n';
381                             hasParagraphBreak = true;
382                         }
383                     }
384                     
385                     hasNewLine = true;
386                 }
387                 else if (currentNode->hasTagName(imgTag)) {
388                     if (pendingStyledSpace != nil) {
389                         if (linkStartLocation == [result length])
390                             ++linkStartLocation;
391                         [result appendAttributedString:pendingStyledSpace];
392                         [pendingStyledSpace release];
393                         pendingStyledSpace = nil;
394                     }
395                     NSFileWrapper *fileWrapper = fileWrapperForElement(static_cast<Element*>(currentNode));
396                     NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper:fileWrapper];
397                     NSAttributedString *iString = [NSAttributedString attributedStringWithAttachment:attachment];
398                     [result appendAttributedString: iString];
399                     [attachment release];
400                 }
401
402                 NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:text.getNSString()];
403                 [result appendAttributedString: partialString];
404                 [partialString release];
405             }
406         }
407
408         Node *nextNode = currentNode->firstChild();
409         if (!nextNode)
410             nextNode = currentNode->nextSibling();
411
412         while (!nextNode && currentNode->parentNode()) {
413             DeprecatedString text;
414             currentNode = currentNode->parentNode();
415             if (currentNode == pastEndNode)
416                 break;
417             nextNode = currentNode->nextSibling();
418
419             if (currentNode->hasTagName(aTag)) {
420                 // End of a <a> element.  Create an attributed string NSLinkAttributeName attribute
421                 // for the range of the link.  Note that we create the attributed string from the DOM, which
422                 // will have corrected any illegally nested <a> elements.
423                 if (linkStartNode && currentNode == linkStartNode) {
424                     String href = parseURL(linkStartNode->getAttribute(hrefAttr));
425                     KURL kURL = linkStartNode->document()->frame()->loader()->completeURL(href.deprecatedString());
426                     
427                     NSURL *URL = kURL.getNSURL();
428                     NSRange tempRange = { linkStartLocation, [result length]-linkStartLocation }; // workaround for 4213314
429                     [result addAttribute:NSLinkAttributeName value:URL range:tempRange];
430                     linkStartNode = 0;
431                 }
432             }
433             else if (currentNode->hasTagName(olTag) || currentNode->hasTagName(ulTag)) {
434                 if (!hasNewLine)
435                     text += '\n';
436                 hasNewLine = true;
437             } else if (currentNode->hasTagName(liTag)) {
438                 
439                 int i, count = listItems.size();
440                 for (i = 0; i < count; i++){
441                     if (listItems[i] == currentNode){
442                         listItemLocations[i].end = [result length];
443                         break;
444                     }
445                 }
446                 if (!hasNewLine)
447                     text += '\n';
448                 hasNewLine = true;
449             } else if (currentNode->hasTagName(blockquoteTag) ||
450                        currentNode->hasTagName(ddTag) ||
451                        currentNode->hasTagName(divTag) ||
452                        currentNode->hasTagName(dlTag) ||
453                        currentNode->hasTagName(dtTag) ||
454                        currentNode->hasTagName(hrTag) ||
455                        currentNode->hasTagName(listingTag) ||
456                        currentNode->hasTagName(preTag) ||
457                        currentNode->hasTagName(tdTag) ||
458                        currentNode->hasTagName(thTag)) {
459                 if (!hasNewLine)
460                     text += '\n';
461                 hasNewLine = true;
462             } else if (currentNode->hasTagName(pTag) ||
463                        currentNode->hasTagName(trTag) ||
464                        currentNode->hasTagName(h1Tag) ||
465                        currentNode->hasTagName(h2Tag) ||
466                        currentNode->hasTagName(h3Tag) ||
467                        currentNode->hasTagName(h4Tag) ||
468                        currentNode->hasTagName(h5Tag) ||
469                        currentNode->hasTagName(h6Tag)) {
470                 if (!hasNewLine)
471                     text += '\n';
472                 // An extra newline is needed at the start, not the end, of these types of tags,
473                 // so don't add another here.
474                 hasNewLine = true;
475             }
476             
477             NSAttributedString *partialString = [[NSAttributedString alloc] initWithString:text.getNSString()];
478             [result appendAttributedString:partialString];
479             [partialString release];
480         }
481
482         currentNode = nextNode;
483     }
484     
485     [pendingStyledSpace release];
486     
487     // Apply paragraph styles from outside in.  This ensures that nested lists correctly
488     // override their parent's paragraph style.
489     {
490         unsigned i, count = listItems.size();
491         Element *e;
492
493 #ifdef POSITION_LIST
494         Node *containingBlock;
495         int containingBlockX, containingBlockY;
496         
497         // Determine the position of the outermost containing block.  All paragraph
498         // styles and tabs should be relative to this position.  So, the horizontal position of 
499         // each item in the list (in the resulting attributed string) will be relative to position 
500         // of the outermost containing block.
501         if (count > 0){
502             containingBlock = firstNode;
503             while (containingBlock->renderer()->isInline()){
504                 containingBlock = containingBlock->parentNode();
505             }
506             containingBlock->renderer()->absolutePosition(containingBlockX, containingBlockY);
507         }
508 #endif
509         
510         for (i = 0; i < count; i++){
511             e = listItems[i];
512             info = listItemLocations[i];
513             
514             if (info.end < info.start)
515                 info.end = [result length];
516                 
517             RenderObject *r = e->renderer();
518             RenderStyle *style = r->style();
519
520             int rx;
521             NSFont *font = style->font().primaryFont()->getNSFont();
522             float pointSize = [font pointSize];
523
524 #ifdef POSITION_LIST
525             int ry;
526             r->absolutePosition(rx, ry);
527             rx -= containingBlockX;
528             
529             // Ensure that the text is indented at least enough to allow for the markers.
530             rx = MAX(rx, (int)maxMarkerWidth);
531 #else
532             rx = (int)MAX(maxMarkerWidth, pointSize);
533 #endif
534
535             // The bullet text will be right aligned at the first tab marker, followed
536             // by a space, followed by the list item text.  The space is arbitrarily
537             // picked as pointSize*2/3.  The space on the first line of the text item
538             // is established by a left aligned tab, on subsequent lines it's established
539             // by the head indent.
540             NSMutableParagraphStyle *mps = [[NSMutableParagraphStyle alloc] init];
541             [mps setFirstLineHeadIndent: 0];
542             [mps setHeadIndent: rx];
543             [mps setTabStops:[NSArray arrayWithObjects:
544                         [[[NSTextTab alloc] initWithType:NSRightTabStopType location:rx-(pointSize*2/3)] autorelease],
545                         [[[NSTextTab alloc] initWithType:NSLeftTabStopType location:rx] autorelease],
546                         nil]];
547             NSRange tempRange = { info.start, info.end-info.start }; // workaround for 4213314
548             [result addAttribute:NSParagraphStyleAttributeName value:mps range:tempRange];
549             [mps release];
550         }
551     }
552
553     return result;
554
555     END_BLOCK_OBJC_EXCEPTIONS;
556
557     return nil;
558 }
559
560 @end