5d4edd72a6d726ae55443053eba49b51560c1ceb
[WebKit-https.git] / WebCore / khtml / editing / markup.cpp
1 /*
2  * Copyright (C) 2004 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  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "markup.h"
27
28 #include "htmlediting.h"
29
30 #include "css/css_computedstyle.h"
31 #include "css/css_valueimpl.h"
32 #include "editing/visible_position.h"
33 #include "editing/visible_units.h"
34 #include "html/html_elementimpl.h"
35 #include "xml/dom_position.h"
36 #include "xml/dom2_rangeimpl.h"
37 #include "rendering/render_text.h"
38 #include "htmlnames.h"
39
40 using DOM::AttributeImpl;
41 using DOM::CommentImpl;
42 using DOM::CSSComputedStyleDeclarationImpl;
43 using DOM::CSSMutableStyleDeclarationImpl;
44 using DOM::DocumentFragmentImpl;
45 using DOM::DocumentImpl;
46 using DOM::DOMString;
47 using DOM::ElementImpl;
48 using DOM::TagStatusForbidden;
49 using DOM::HTMLElementImpl;
50 using DOM::NamedAttrMapImpl;
51 using DOM::Node;
52 using DOM::NodeImpl;
53 using DOM::Position;
54 using DOM::RangeImpl;
55 using DOM::TextImpl;
56 using namespace HTMLNames;
57
58 #if APPLE_CHANGES
59 #include "KWQAssertions.h"
60 #include "KWQLogging.h"
61 #else
62 #define ASSERT(assertion) ((void)0)
63 #define ASSERT_WITH_MESSAGE(assertion, formatAndArgs...) ((void)0)
64 #define ASSERT_NOT_REACHED() ((void)0)
65 #define LOG(channel, formatAndArgs...) ((void)0)
66 #define ERROR(formatAndArgs...) ((void)0)
67 #define ASSERT(assertion) assert(assertion)
68 #endif
69
70 namespace khtml {
71
72 static QString escapeHTML(const QString &in)
73 {
74     QString s = "";
75
76     unsigned len = in.length();
77     for (unsigned i = 0; i < len; ++i) {
78         switch (in[i].latin1()) {
79             case '&':
80                 s += "&amp;";
81                 break;
82             case '<':
83                 s += "&lt;";
84                 break;
85             case '>':
86                 s += "&gt;";
87                 break;
88             default:
89                 s += in[i];
90         }
91     }
92
93     return s;
94 }
95
96 static QString stringValueForRange(const NodeImpl *node, const RangeImpl *range)
97 {
98     DOMString str = node->nodeValue().copy();
99     if (range) {
100         int exceptionCode;
101         if (node == range->endContainer(exceptionCode)) {
102             str.truncate(range->endOffset(exceptionCode));
103         }
104         if (node == range->startContainer(exceptionCode)) {
105             str.remove(0, range->startOffset(exceptionCode));
106         }
107     }
108     return str.string();
109 }
110
111 static QString renderedText(const NodeImpl *node, const RangeImpl *range)
112 {
113     RenderObject *r = node->renderer();
114     if (!r)
115         return QString();
116     
117     if (!node->isTextNode())
118         return QString();
119
120     QString result = "";
121
122     int exceptionCode;
123     const TextImpl *textNode = static_cast<const TextImpl *>(node);
124     unsigned startOffset = 0;
125     unsigned endOffset = textNode->length();
126
127     if (range && node == range->startContainer(exceptionCode))
128         startOffset = range->startOffset(exceptionCode);
129     if (range && node == range->endContainer(exceptionCode))
130         endOffset = range->endOffset(exceptionCode);
131     
132     RenderText *textRenderer = static_cast<RenderText *>(r);
133     QString str = node->nodeValue().string();
134     for (InlineTextBox *box = textRenderer->firstTextBox(); box; box = box->nextTextBox()) {
135         unsigned start = box->m_start;
136         unsigned end = box->m_start + box->m_len;
137         if (endOffset < start)
138             break;
139         if (startOffset <= end) {
140             unsigned s = kMax(start, startOffset);
141             unsigned e = kMin(end, endOffset);
142             result.append(str.mid(s, e-s));
143             if (e == end) {
144                 // now add in collapsed-away spaces if at the end of the line
145                 InlineTextBox *nextBox = box->nextTextBox();
146                 if (nextBox && box->root() != nextBox->root()) {
147                     const char nonBreakingSpace = '\xa0';
148                     // count the number of characters between the end of the
149                     // current box and the start of the next box.
150                     int collapsedStart = e;
151                     int collapsedPastEnd = kMin((unsigned)nextBox->m_start, endOffset + 1);
152                     bool addNextNonNBSP = true;
153                     for (int i = collapsedStart; i < collapsedPastEnd; i++) {
154                         if (str[i] == nonBreakingSpace) {
155                             result.append(str[i]);
156                             addNextNonNBSP = true;
157                         }
158                         else if (addNextNonNBSP) {
159                             result.append(str[i]);
160                             addNextNonNBSP = false;
161                         }
162                     }
163                 }
164             }
165         }
166
167     }
168     
169     return result;
170 }
171
172 static QString startMarkup(const NodeImpl *node, const RangeImpl *range, EAnnotateForInterchange annotate, CSSMutableStyleDeclarationImpl *defaultStyle)
173 {
174     unsigned short type = node->nodeType();
175     switch (type) {
176         case Node::TEXT_NODE: {
177             if (node->parentNode()) {
178                 if (node->parentNode()->hasTagName(preTag) ||
179                     node->parentNode()->hasTagName(scriptTag) ||
180                     node->parentNode()->hasTagName(styleTag) ||
181                     node->parentNode()->hasTagName(textareaTag))
182                     return stringValueForRange(node, range);
183             }
184             QString markup = annotate ? escapeHTML(renderedText(node, range)) : escapeHTML(stringValueForRange(node, range));            
185             if (defaultStyle) {
186                 NodeImpl *element = node->parentNode();
187                 if (element) {
188                     CSSMutableStyleDeclarationImpl *style = Position(element, 0).computedStyle()->copyInheritableProperties();
189                     style->ref();
190                     defaultStyle->diff(style);
191                     if (style->length() > 0) {
192                         // FIXME: Handle case where style->cssText() has illegal characters in it, like "
193                         QString openTag = QString("<span class=\"") + AppleStyleSpanClass + "\" style=\"" + style->cssText().string() + "\">";
194                         markup = openTag + markup + "</span>";
195                     }
196                     style->deref();
197                 }            
198             }
199             return annotate ? convertHTMLTextToInterchangeFormat(markup) : markup;
200         }
201         case Node::COMMENT_NODE:
202             return static_cast<const CommentImpl *>(node)->toString().string();
203         case Node::DOCUMENT_NODE:
204             return "";
205         default: {
206             QString markup = QChar('<') + node->nodeName().string();
207             if (type == Node::ELEMENT_NODE) {
208                 const ElementImpl *el = static_cast<const ElementImpl *>(node);
209                 DOMString additionalStyle;
210                 if (defaultStyle && el->isHTMLElement()) {
211                     CSSMutableStyleDeclarationImpl *style = Position(const_cast<ElementImpl *>(el), 0).computedStyle()->copyInheritableProperties();
212                     style->ref();
213                     defaultStyle->diff(style);
214                     if (style->length() > 0) {
215                         CSSMutableStyleDeclarationImpl *inlineStyleDecl = static_cast<const HTMLElementImpl *>(el)->inlineStyleDecl();
216                         if (inlineStyleDecl)
217                             inlineStyleDecl->diff(style);
218                         additionalStyle = style->cssText();
219                     }
220                     style->deref();
221                 }
222                 NamedAttrMapImpl *attrs = el->attributes();
223                 unsigned long length = attrs->length();
224                 if (length == 0 && additionalStyle.length() > 0) {
225                     // FIXME: Handle case where additionalStyle has illegal characters in it, like "
226                     markup += " " +  styleAttr.localName().string() + "=\"" + additionalStyle.string() + "\"";
227                 }
228                 else {
229                     for (unsigned int i=0; i<length; i++) {
230                         AttributeImpl *attr = attrs->attributeItem(i);
231                         DOMString value = attr->value();
232                         if (attr->name() == styleAttr && additionalStyle.length() > 0)
233                             value += "; " + additionalStyle;
234                         // FIXME: Handle case where value has illegal characters in it, like "
235                         // FIXME: Namespaces! XML! Ack!
236                         markup += " " + attr->name().localName().string() + "=\"" + value.string() + "\"";
237                     }
238                 }
239             }
240             markup += node->isHTMLElement() ? ">" : "/>";
241             return markup;
242         }
243     }
244 }
245
246 static QString endMarkup(const NodeImpl *node)
247 {
248     bool hasEndTag = node->isElementNode();
249     if (node->isHTMLElement()) {
250         const HTMLElementImpl* htmlElt = static_cast<const HTMLElementImpl*>(node);
251         hasEndTag = (htmlElt->endTagRequirement() != TagStatusForbidden);
252     }
253     if (hasEndTag)
254         return "</" + node->nodeName().string() + ">";
255     return "";
256 }
257
258 static QString markup(const NodeImpl *startNode, bool onlyIncludeChildren, bool includeSiblings, QPtrList<NodeImpl> *nodes)
259 {
260     // Doesn't make sense to only include children and include siblings.
261     ASSERT(!(onlyIncludeChildren && includeSiblings));
262     QString me = "";
263     for (const NodeImpl *current = startNode; current != NULL; current = includeSiblings ? current->nextSibling() : NULL) {
264         if (!onlyIncludeChildren) {
265             if (nodes) {
266                 nodes->append(current);
267             }
268             me += startMarkup(current, 0, DoNotAnnotateForInterchange, 0);
269         }
270         
271         bool container = true;
272         if (current->isHTMLElement()) {
273             const HTMLElementImpl* h = static_cast<const HTMLElementImpl*>(current);
274             container = h->endTagRequirement() != TagStatusForbidden;
275         }
276         if (container) {
277             // print children
278             if (NodeImpl *n = current->firstChild()) {
279                 me += markup(n, false, true, nodes);
280             }
281             // Print my ending tag
282             if (!onlyIncludeChildren) {
283                 me += endMarkup(current);
284             }
285         }
286     }
287     return me;
288 }
289
290 static void completeURLs(NodeImpl *node, const QString &baseURL)
291 {
292     NodeImpl *end = node->traverseNextSibling();
293     for (NodeImpl *n = node; n != end; n = n->traverseNextNode()) {
294         if (n->nodeType() == Node::ELEMENT_NODE) {
295             ElementImpl *e = static_cast<ElementImpl *>(n);
296             NamedAttrMapImpl *attrs = e->attributes();
297             unsigned long length = attrs->length();
298             for (unsigned long i = 0; i < length; i++) {
299                 AttributeImpl *attr = attrs->attributeItem(i);
300                 if (e->isURLAttribute(attr))
301                     e->setAttribute(attr->name(), KURL(baseURL, attr->value().string()).url());
302             }
303         }
304     }
305 }
306
307 QString createMarkup(const RangeImpl *range, QPtrList<NodeImpl> *nodes, EAnnotateForInterchange annotate)
308 {
309     if (!range || range->isDetached())
310         return QString();
311
312     static const QString interchangeNewlineString = QString("<br class=\"") + AppleInterchangeNewline + "\">";
313
314     int exceptionCode = 0;
315     NodeImpl *commonAncestor = range->commonAncestorContainer(exceptionCode);
316     ASSERT(exceptionCode == 0);
317
318     DocumentImpl *doc = commonAncestor->getDocument();
319     doc->updateLayout();
320
321     NodeImpl *commonAncestorBlock = 0;
322     if (commonAncestor != 0) {
323         commonAncestorBlock = commonAncestor->enclosingBlockFlowElement();
324     }
325     if (commonAncestorBlock == 0) {
326         return "";    
327     }
328
329     QStringList markups;
330     NodeImpl *pastEnd = range->pastEndNode();
331     NodeImpl *lastClosed = 0;
332     QPtrList<NodeImpl> ancestorsToClose;
333
334     // calculate the "default style" for this markup
335     Position pos(doc->documentElement(), 0);
336     CSSMutableStyleDeclarationImpl *defaultStyle = pos.computedStyle()->copyInheritableProperties();
337     defaultStyle->ref();
338     
339     NodeImpl *startNode = range->startNode();
340     VisiblePosition visibleStart(range->startPosition(), VP_DEFAULT_AFFINITY);
341     VisiblePosition visibleEnd(range->endPosition(), VP_DEFAULT_AFFINITY);
342     if (!inSameBlock(visibleStart, visibleStart.next())) {
343         if (visibleStart == visibleEnd.previous())
344             return interchangeNewlineString;
345         markups.append(interchangeNewlineString);
346         startNode = startNode->traverseNextNode();
347     }
348     
349     // Iterate through the nodes of the range.
350     NodeImpl *next;
351     for (NodeImpl *n = startNode; n != pastEnd; n = next) {
352         next = n->traverseNextNode();
353
354         if (n->isBlockFlow() && next == pastEnd) {
355             // Don't write out an empty block.
356             continue;
357         }
358         
359         // Add the node to the markup.
360         if (n->renderer()) {
361             markups.append(startMarkup(n, range, annotate, defaultStyle));
362             if (nodes) {
363                 nodes->append(n);
364             }
365         }
366         
367         if (n->firstChild() == 0) {
368             // Node has no children, add its close tag now.
369             if (n->renderer()) {
370                 markups.append(endMarkup(n));
371                 lastClosed = n;
372             }
373             
374             // Check if the node is the last leaf of a tree.
375             if (n->nextSibling() == 0 || next == pastEnd) {
376                 if (!ancestorsToClose.isEmpty()) {
377                     // Close up the ancestors.
378                     while (NodeImpl *ancestor = ancestorsToClose.last()) {
379                         if (next != pastEnd && ancestor == next->parentNode()) {
380                             break;
381                         }
382                         // Not at the end of the range, close ancestors up to sibling of next node.
383                         markups.append(endMarkup(ancestor));
384                         lastClosed = ancestor;
385                         ancestorsToClose.removeLast();
386                     }
387                 } else {
388                     // No ancestors to close, but need to add ancestors not in range since next node is in another tree. 
389                     if (next != pastEnd) {
390                         NodeImpl *nextParent = next->parentNode();
391                         if (n != nextParent) {
392                             for (NodeImpl *parent = n->parent(); parent != 0 && parent != nextParent; parent = parent->parentNode()) {
393                                 markups.prepend(startMarkup(parent, range, annotate, defaultStyle));
394                                 markups.append(endMarkup(parent));
395                                 if (nodes) {
396                                     nodes->append(parent);
397                                 }                            
398                                 lastClosed = parent;
399                             }
400                         }
401                     }
402                 }
403             }
404         } else if (n->renderer()) {
405             // Node is an ancestor, set it to close eventually.
406             ancestorsToClose.append(n);
407         }
408     }
409     
410     NodeImpl *rangeStartNode = range->startNode();
411     int rangeStartOffset = range->startOffset(exceptionCode);
412     ASSERT(exceptionCode == 0);
413     
414     // Add ancestors up to the common ancestor block so inline ancestors such as FONT and B are part of the markup.
415     if (lastClosed) {
416         for (NodeImpl *ancestor = lastClosed->parentNode(); ancestor; ancestor = ancestor->parentNode()) {
417             if (RangeImpl::compareBoundaryPoints(ancestor, 0, rangeStartNode, rangeStartOffset) >= 0) {
418                 // we have already added markup for this node
419                 continue;
420             }
421             bool breakAtEnd = false;
422             if (commonAncestorBlock == ancestor) {
423                 // Include ancestors that are required to retain the appearance of the copied markup.
424                 if (ancestor->hasTagName(preTag) || ancestor->hasTagName(tableTag) ||
425                     ancestor->hasTagName(olTag) || ancestor->hasTagName(ulTag)) {
426                     breakAtEnd = true;
427                 } else {
428                     break;
429                 }
430             }
431             markups.prepend(startMarkup(ancestor, range, annotate, defaultStyle));
432             markups.append(endMarkup(ancestor));
433             if (nodes) {
434                 nodes->append(ancestor);
435             }        
436             if (breakAtEnd) {
437                 break;
438             }
439         }
440     }
441
442     if (annotate) {
443         if (!inSameBlock(visibleEnd, visibleEnd.previous()))
444             markups.append(interchangeNewlineString);
445     }
446
447     // Retain the Mail quote level by including all ancestor mail block quotes.
448     for (NodeImpl *ancestor = commonAncestorBlock; ancestor; ancestor = ancestor->parentNode()) {
449         if (isMailBlockquote(ancestor)) {
450             markups.prepend(startMarkup(ancestor, range, annotate, defaultStyle));
451             markups.append(endMarkup(ancestor));
452         }
453     }
454     
455     // add in the "default style" for this markup
456     // FIXME: Handle case where value has illegal characters in it, like "
457     QString openTag = QString("<span class=\"") + AppleStyleSpanClass + "\" style=\"" + defaultStyle->cssText().string() + "\">";
458     markups.prepend(openTag);
459     markups.append("</span>");
460     defaultStyle->deref();
461
462     return markups.join("");
463 }
464
465 DocumentFragmentImpl *createFragmentFromMarkup(DocumentImpl *document, const QString &markup, const QString &baseURL)
466 {
467     // FIXME: What if the document element is not an HTML element?
468     HTMLElementImpl *element = static_cast<HTMLElementImpl *>(document->documentElement());
469
470     DocumentFragmentImpl *fragment = element->createContextualFragment(markup);
471     ASSERT(fragment);
472
473     if (!baseURL.isEmpty() && baseURL != document->baseURL())
474         completeURLs(fragment, baseURL);
475
476     return fragment;
477 }
478
479 QString createMarkup(const DOM::NodeImpl *node, EChildrenOnly includeChildren,
480     QPtrList<DOM::NodeImpl> *nodes, EAnnotateForInterchange annotate)
481 {
482     ASSERT(annotate == DoNotAnnotateForInterchange); // annotation not yet implemented for this code path
483
484     // FIXME: We could take out this if statement if we had more time to test.
485     // I'm concerned that making this crash when the document is nil might be too risky a change at the moment.
486     DocumentImpl *doc = node->getDocument();
487     assert(doc);
488     if (doc)
489         doc->updateLayout();
490
491     return markup(node, includeChildren, false, nodes);
492 }
493
494 static void createParagraphContentsFromString(DOM::DocumentImpl *document, ElementImpl *paragraph, const QString &string)
495 {
496     int exceptionCode = 0;
497     if (string.isEmpty()) {
498         NodeImpl *placeHolder = createBlockPlaceholderElement(document);
499         paragraph->appendChild(placeHolder, exceptionCode);
500         ASSERT(exceptionCode == 0);
501         return;
502     }
503
504     assert(string.find('\n') == -1);
505
506     QStringList tabList = QStringList::split('\t', string, true);
507     QString tabText = "";
508     while (!tabList.isEmpty()) {
509         QString s = tabList.first();
510         tabList.pop_front();
511
512         // append the non-tab textual part
513         if (!s.isEmpty()) {
514             if (tabText != "") {
515                 paragraph->appendChild(createTabSpanElement(document, &tabText), exceptionCode);
516                 ASSERT(exceptionCode == 0);
517                 tabText = "";
518             }
519             NodeImpl *textNode = document->createTextNode(s);
520             paragraph->appendChild(textNode, exceptionCode);
521             ASSERT(exceptionCode == 0);
522         }
523
524         // there is a tab after every entry, except the last entry
525         // (if the last character is a tab, the list gets an extra empty entry)
526         if (!tabList.isEmpty()) {
527 //#ifdef COALESCE_TAB_SPANS
528 #if 1
529             tabText += '\t';
530 #else
531             paragraph->appendChild(createTabSpanElement(document), exceptionCode);
532             ASSERT(exceptionCode == 0);
533 #endif
534         } else if (tabText != "") {
535             paragraph->appendChild(createTabSpanElement(document, &tabText), exceptionCode);
536             ASSERT(exceptionCode == 0);
537         }
538     }
539 }
540
541 DOM::DocumentFragmentImpl *createFragmentFromText(DOM::DocumentImpl *document, const QString &text)
542 {
543     if (!document)
544         return 0;
545
546     DocumentFragmentImpl *fragment = document->createDocumentFragment();
547     fragment->ref();
548     
549     if (!text.isEmpty()) {
550         QString string = text;
551
552         // FIXME: Wrap the NBSP's in a span that says "converted space".
553         int offset = 0;
554         int stringLength = string.length();
555         while (1) {
556             // FIXME: This only converts plain old spaces, and does not
557             // deal with more exotic whitespace. Note that we want to 
558             // leave newlines and returns alone at this point anyway, 
559             // since those are handled specially later.
560             int spacesPos = string.find("  ", offset);
561             if (spacesPos < 0)
562                 break;
563
564             // Found two adjoining spaces.
565             // Now, lookahead to see if these two spaces are followed by:
566             //   1. another space and then a non-space
567             //   2. another space and then the end of the string
568             // If either 1 or 2 is true, replace the three spaces found with nbsp+nbsp+space, 
569             // otherwise, replace the first two spaces with nbsp+space.
570             if ((spacesPos + 3 < stringLength && string[spacesPos + 2] == ' ' && string[spacesPos + 3] != ' ')
571                     || (spacesPos + 3 == stringLength && string[spacesPos + 2] == ' ')) {
572                 string.replace(spacesPos, 3, "\xA0\xA0 ");
573             } else {
574                 string.replace(spacesPos, 2, "\xA0 ");
575             }
576             offset = spacesPos + 2;
577         }
578
579         // Break string into paragraphs. Extra line breaks turn into empty paragraphs.
580         string.replace(QString("\r\n"), "\n");
581         string.replace('\r', '\n');
582         QStringList list = QStringList::split('\n', string, true); // true gets us empty strings in the list
583         while (!list.isEmpty()) {
584             QString s = list.first();
585             list.pop_front();
586
587             int exceptionCode = 0;
588             ElementImpl *element;
589             if (s.isEmpty() && list.isEmpty()) {
590                 // For last line, use the "magic BR" rather than a P.
591                 element = document->createElementNS(xhtmlNamespaceURI, "br", exceptionCode);
592                 ASSERT(exceptionCode == 0);
593                 element->ref();
594                 element->setAttribute(classAttr, AppleInterchangeNewline);            
595             } else {
596                 element = createDefaultParagraphElement(document);
597                 createParagraphContentsFromString(document, element, s);
598                 element->ref();
599             }
600             fragment->appendChild(element, exceptionCode);
601             ASSERT(exceptionCode == 0);
602             element->deref();
603         }
604     }
605     
606     // Trick to get the fragment back to the floating state, with 0
607     // refs but not destroyed.
608     fragment->setParent(document);
609     fragment->deref();
610     fragment->setParent(0);
611     
612     return fragment;
613 }
614
615 }