1 //
2 // Copyright (c) ZeroC, Inc. All rights reserved.
3 //
4 
5 #include <IceUtil/ConsoleUtil.h>
6 #include <ConfluenceOutput.h>
7 #include <iostream>
8 #include <sstream>
9 #include <algorithm>
10 
11 #if defined(_MSC_VER)
12 #   pragma warning(disable:4456) // shadow
13 #   pragma warning(disable:4457) // shadow
14 #   pragma warning(disable:4459) // shadow
15 #   pragma warning(disable:4100) // unreferenced parameter
16 #elif defined(__clang__)
17 #   pragma clang diagnostic ignored "-Wshadow"
18 #   pragma clang diagnostic ignored "-Wshadow-all"
19 #   pragma clang diagnostic ignored "-Wunused-parameter"
20 #elif defined(__GNUC__)
21 #   pragma GCC diagnostic ignored "-Wshadow"
22 #   pragma GCC diagnostic ignored "-Wunused-parameter"
23 #endif
24 
25 using namespace std;
26 using namespace IceUtilInternal;
27 
28 namespace Confluence
29 {
30 
31     EndElement ee;
32 
33 }
34 
35 // ----------------------------------------------------------------------
36 // ConfluenceOutput
37 // ----------------------------------------------------------------------
38 
39 const string Confluence::ConfluenceOutput::TEMP_ESCAPER_START = "$$$$$$$";
40 const string Confluence::ConfluenceOutput::TEMP_ESCAPER_END = "!!!!!!!";
41 
ConfluenceOutput()42 Confluence::ConfluenceOutput::ConfluenceOutput() :
43     OutputBase(),
44     _se(false),
45     _text(false),
46     _escape(false),
47     _listMarkers(""),
48     _commentListMarkers("")
49 {
50 }
51 
ConfluenceOutput(ostream & os)52 Confluence::ConfluenceOutput::ConfluenceOutput(ostream& os) :
53     OutputBase(os),
54     _se(false),
55     _text(false),
56     _escape(false),
57     _listMarkers(""),
58     _commentListMarkers("")
59 {
60 }
61 
ConfluenceOutput(const char * s)62 Confluence::ConfluenceOutput::ConfluenceOutput(const char* s) :
63     OutputBase(s),
64     _se(false),
65     _text(false),
66     _escape(false),
67     _listMarkers(""),
68     _commentListMarkers("")
69 {
70 }
71 
72 void
print(const string & s)73 Confluence::ConfluenceOutput::print(const string& s)
74 {
75     if(_se)
76     {
77         _se = false;
78     }
79     _text = true;
80 
81     if(_escape)
82     {
83         string escaped = escape(s);
84         OutputBase::print(escaped);
85     }
86     else
87     {
88         OutputBase::print(s);
89     }
90 }
91 
92 string
escapeComment(string comment)93 Confluence::ConfluenceOutput::escapeComment(string comment)
94 {
95     list< pair<unsigned int,unsigned int> > escaperLimits = getMarkerLimits(comment);
96     string escapeChars = "\\{}-_+*|[]";
97 
98     // For each escape character
99     for(string::iterator i = escapeChars.begin(); i < escapeChars.end(); ++i)
100     {
101         string c(1, *i);
102         string replacement;
103 
104         if(c == "\\")
105         {
106             replacement = "\\\\";
107         }
108         else if(c == "{")
109         {
110             replacement = "\\{";
111         }
112         else if(c == "}")
113         {
114             replacement = "\\}";
115         }
116         else if(c == "-")
117         {
118             replacement = "\\-";
119         }
120         else if(c == "*")
121         {
122             replacement = "\\*";
123         }
124         else if(c == "|")
125         {
126             replacement = "\\|";
127         }
128         else if(c == "_")
129         {
130             replacement = "\\_";
131         }
132         else if(c == "+")
133         {
134             replacement = "\\+";
135         }
136         else if(c == "[")
137         {
138             replacement = "\\[";
139         }
140         else if(c == "]")
141         {
142             replacement = "\\]";
143         }
144 
145         size_t pos = comment.find(c);
146 
147         // For each position of a found escape character
148         while(pos != string::npos)
149         {
150             pair<unsigned int,unsigned int> *region = ICE_NULLPTR;
151 
152             // Is this pos in an escaped section?
153             for(list<pair<unsigned int,unsigned int> >::iterator i = escaperLimits.begin(); i != escaperLimits.end();
154                 ++i)
155             {
156                 if(pos >= i->first && pos <= i->second)
157                 {
158                     region = &*i;
159                     break;
160                 }
161             }
162 
163             if(region == ICE_NULLPTR)
164             {
165                 comment.replace(pos, c.size(), replacement);
166                 pos = comment.find(c, pos + replacement.size());
167             }
168             else
169             {
170                 // Skip ahead past the marked section
171                 pos = comment.find(c, region->second + 1);
172             }
173         }
174     }
175     comment = removeMarkers(comment);
176     return comment;
177 }
178 
179 string
convertCommentHTML(string comment)180 Confluence::ConfluenceOutput::convertCommentHTML(string comment)
181 {
182     comment = escapeComment(comment);
183 
184     bool italics = false;
185 
186     size_t tagStart = comment.find("<");
187     while(tagStart != string::npos)
188     {
189         size_t tagEnd = comment.find(">", tagStart);
190         string tag = comment.substr(tagStart + 1, tagEnd - (tagStart + 1));
191         string replacement = "";
192         bool isEndTag = tag[0] == '/';
193         if(isEndTag)
194         {
195             // Strip slash character
196             tag.erase(remove(tag.begin(), tag.end(), '/'), tag.end());
197         }
198 
199         size_t spacepos = tag.find(" ");
200         list<pair<string,string> > attributes;
201 
202         string rest;
203         if(spacepos != string::npos)
204         {
205             // Get the rest and separate into attrs
206             rest = tag.substr(spacepos);
207 
208             // Get just the tag
209             tag = tag.substr(0, spacepos);
210 
211             size_t nextSpace = 0;
212             size_t lastSpace = 0;
213             do
214             {
215                 lastSpace = nextSpace;
216                 nextSpace = rest.find(" ", lastSpace + 1); // Rest starts with a space
217 
218                 string setting;
219                 if(nextSpace == string::npos)
220                 {
221                     setting = rest.substr(lastSpace);
222                 }
223                 else {
224                     setting = rest.substr(lastSpace, nextSpace - lastSpace);
225                 }
226 
227                 size_t eqPos = setting.find("=");
228                 if(eqPos != string::npos)
229                 {
230                     string aName = setting.substr(1, eqPos - 1);
231                     string aVal = setting.substr(eqPos + 1);
232                     // Remove quotes from val
233                     size_t qPos = aVal.find("\"");
234                     while(qPos != string::npos)
235                     {
236                         aVal.erase(qPos, 1);
237                         qPos = aVal.find("\"");
238                     }
239 
240                     pair<string,string> p = make_pair(aName, aVal);
241                     attributes.push_back(p);
242                 }
243                 else
244                 {
245                     // Bad attribute, ignore
246                 }
247             } while(nextSpace != string::npos);
248         }
249 
250         if(tag == "tt")
251         {
252             if(!isEndTag)
253             {
254                 replacement = "{{";
255             }
256             else
257             {
258                 replacement = "}}";
259             }
260         }
261         else if(tag == "p")
262         {
263             // Special case: Some classes add markup
264             for(list<pair<string,string> >::iterator i = attributes.begin(); i != attributes.end(); ++i)
265             {
266                 if(i->first == "class" && i->second == "Note")
267                 {
268                     italics = true;
269                     break;
270                 }
271                 if(i->first == "class" && i->second == "Deprecated")
272                 {
273                     italics = true;
274                     break;
275                 }
276             }
277 
278             if(!isEndTag)
279             {
280                 if(italics)
281                 {
282                     replacement = "\n\n_";
283                 }
284                 else
285                 {
286                     replacement = "\n\n";
287                 }
288             }
289             else
290             {
291                 if(italics)
292                 {
293                     replacement = "_\n\n";
294                     italics = false;
295                 }
296                 else
297                 {
298                     replacement = "\n\n";
299                 }
300             }
301         }
302         else if(tag == "ol")
303         {
304             if(!isEndTag)
305             {
306                 if(_commentListMarkers.empty())
307                 {
308                     replacement = "\n";
309                 }
310                 _commentListMarkers.append("#");
311 
312             }
313             else
314             {
315                 _commentListMarkers.erase(_commentListMarkers.size() - 1);
316             }
317         }
318         else if(tag == "ul")
319         {
320             if(!isEndTag)
321             {
322                 if(_commentListMarkers.empty())
323                 {
324                     replacement = "\n";
325                 }
326                 _commentListMarkers.append("*");
327             }
328             else
329             {
330                 _commentListMarkers.erase(_commentListMarkers.size() - 1);
331             }
332         }
333         else if(tag == "li")
334         {
335             if(!isEndTag)
336             {
337                 ostringstream oss;
338                 oss << "\n" << _commentListMarkers << " ";
339                 replacement = oss.str();
340             }
341             // Do nothing for end tag
342         }
343         else if(tag == "dl")
344         {
345             if(!isEndTag)
346             {
347                 replacement = "\n";
348             }
349             else
350             {
351                 replacement = "\n";
352             }
353         }
354         else if(tag == "dt")
355         {
356             if(!isEndTag)
357             {
358                 replacement = "";
359             }
360             else
361             {
362                 replacement = " ";
363             }
364         }
365         else if(tag == "dd")
366         {
367             if(!isEndTag)
368             {
369                 replacement = "--- ";
370             }
371             else
372             {
373                 replacement = "\n";
374             }
375         }
376         else if(tag == "em")
377         {
378             if(!isEndTag)
379             {
380                 replacement = "_";
381             }
382             else
383             {
384                 replacement = "_";
385             }
386         }
387         else
388         {
389             replacement = "*{{UNRECOGNIZED MARKUP: " + tag + "}}*";
390         }
391 
392         // Apply replacement
393         if(tag == "p")
394         {
395             comment.erase(tagStart, tagEnd + 1 - tagStart);
396             size_t displace = comment.find_first_not_of(" \n\r\t", tagStart); // Skip ahead over whitespace
397             comment.insert(displace, replacement);
398         }
399         else
400         {
401             comment.replace(tagStart, tagEnd + 1 - tagStart, replacement); // Don't skip whitespace
402         }
403 
404         // Special case: terminate <p> (and any italics) on double newline or end of comment
405         size_t dnl = comment.find("\n\n", tagStart + replacement.size());
406         tagStart = comment.find("<");
407 
408         if(italics)
409         {
410             if(tagStart == string::npos && dnl == string::npos)
411             {
412                 // End italics before javadoc markup
413                 size_t atPos = comment.find("@", tagStart + replacement.size());
414                 if(atPos != string::npos)
415                 {
416                     // Found markup. now move to the last non-whitespace char before the markup and end italics
417                     string before = comment.substr(0, atPos);
418                     size_t endLocation = before.find_last_not_of(" \n\r\t");
419                     comment.insert(endLocation, "_");
420                     italics = false;
421                 }
422                 else
423                 {
424                     // No markup; end of comment
425                     size_t endLocation = comment.find_last_not_of(" \n\r\t");
426                     comment.insert(endLocation, "_");
427                     italics = false;
428                 }
429             }
430             else if(dnl != string::npos && (tagStart == string::npos || dnl < tagStart))
431             {
432                 string before = comment.substr(0, dnl);
433                 size_t endLocation = before.find_last_not_of(" \n\r\t");
434                 comment.insert(endLocation, "_");
435                 italics = false;
436             }
437         }
438 
439     }
440     return comment;
441 }
442 
443 void
newline()444 Confluence::ConfluenceOutput::newline()
445 {
446     if(_se)
447     {
448         _se = false;
449     }
450     OutputBase::newline();
451 }
452 
453 void
startElement(const string & element)454 Confluence::ConfluenceOutput::startElement(const string& element)
455 {
456     string escaped;
457     if(_escape)
458     {
459         escaped = escape(element);
460     }
461     else
462     {
463         escaped = element;
464     }
465 
466     string::size_type tagpos = element.find_first_of(" ");
467     const string tagname = element.substr(0, tagpos).c_str();
468 
469     if(tagname == "p")
470     {
471         _out << "\n";
472     }
473     else if(tagname == "b")
474     {
475         _out << "*";
476     }
477     else if(tagname == "panel")
478     {
479         _out << "{panel}";
480     }
481     else if(tagname == "blockquote")
482     {
483         _out << "{section}{column:width=10px}{column} {column}";
484     }
485     else if(tagname == "dl")
486     {
487         _out << "\n";
488     }
489     else if(tagname == "dt")
490     {
491         _out << "";
492     }
493     else if(tagname == "dd")
494     {
495         _out << "--- ";
496     }
497     else if(tagname == "table")
498     {
499         _out << "{table}\n";
500     }
501     else if(tagname == "tr")
502     {
503         _out << "{tr}\n";
504     }
505     else if(tagname == "td")
506     {
507         _out << "{td}";
508     }
509     else if(tagname == "th")
510     {
511         _out << "{th}";
512     }
513     else if(tagname == "div")
514     {
515         _out << "{div}";
516     }
517     else if(tagname == "span")
518     {
519         _out << "{span}";
520     }
521     else if(tagname == "ol")
522     {
523         if(_listMarkers.empty())
524         {
525             _out << "\n";
526         }
527         _listMarkers.append("#");
528     }
529     else if(tagname == "ul")
530     {
531         if(_listMarkers.empty())
532         {
533             _out << "\n";
534         }
535         _listMarkers.append("*");
536     }
537     else if(tagname == "li")
538     {
539         _out << "\n" << _listMarkers << " ";
540     }
541     else if(tagname == "hr")
542     {
543         _out << "----";
544     }
545     else if(tagname == "h1")
546     {
547         _out << "\nh1. ";
548     }
549     else if(tagname == "h2")
550     {
551         _out << "\nh2. ";
552     }
553     else if(tagname == "h3")
554     {
555         _out << "\nh3. ";
556     }
557     else if(tagname == "h4")
558     {
559         _out << "\nh4. ";
560     }
561     else if(tagname == "h5")
562     {
563         _out << "\nh5. ";
564     }
565     else if(tagname == "h6")
566     {
567         _out << "\nh6. ";
568     }
569     else if(tagname == "tt")
570     {
571         _out << "{{";
572     }
573     else
574     {
575         _out << "{" << escaped << "}";
576     }
577 
578     _se = true;
579     _text = false;
580 
581     string::size_type pos = element.find_first_of(" \t");
582     if(pos == string::npos)
583     {
584         _elementStack.push(element);
585     }
586     else
587     {
588         _elementStack.push(element.substr(0, pos));
589     }
590 
591     ++_pos; // TODO: ???
592     inc();
593     _separator = false;
594 }
595 
596 void
endElement()597 Confluence::ConfluenceOutput::endElement()
598 {
599     string element = _elementStack.top();
600     _elementStack.pop();
601 
602     string escaped;
603     if(_escape)
604     {
605         escaped = escape(element);
606     }
607     else
608     {
609         escaped = element;
610     }
611 
612     string::size_type tagpos = element.find_first_of(" ");
613     const string tagname = element.substr(0, tagpos).c_str();
614 
615     if(tagname == "p")
616     {
617         _out << "\n";
618     }
619     else if(tagname == "b")
620     {
621         _out << "*";
622     }
623     else if(tagname == "panel")
624     {
625         _out << "{panel}\n";
626     }
627     else if(tagname == "blockquote")
628     {
629         _out << "{column}{section}\n";
630     }
631     else if(tagname == "dl")
632     {
633         _out << "\n";
634     }
635     else if(tagname == "dt")
636     {
637         _out << " ";
638     }
639     else if(tagname == "dd")
640     {
641         _out << "\n";
642     }
643     else if(tagname == "table")
644     {
645         _out << "{table}\n";
646     }
647     else if(tagname == "tr")
648     {
649         _out << "{tr}\n";
650     }
651     else if(tagname == "td")
652     {
653         _out << "{td}\n";
654     }
655     else if(tagname == "th")
656     {
657         _out << "";
658     }
659     else if(tagname == "div")
660     {
661         _out << "{div}";
662     }
663     else if(tagname == "span")
664     {
665         _out << "{span}";
666     }
667     else if(tagname == "ol")
668     {
669         _listMarkers.erase(_listMarkers.size() - 1);
670         if(_listMarkers.empty())
671         {
672             _out << "\n";
673         }
674     }
675     else if(tagname == "ul")
676     {
677         _listMarkers.erase(_listMarkers.size() - 1);
678         if(_listMarkers.empty())
679         {
680             _out << "\n";
681         }
682     }
683     else if(tagname == "li")
684     {
685         // Nothing to do
686     }
687     else if(tagname == "hr")
688     {
689         _out << "\n\n";
690     }
691     else if(tagname == "h1")
692     {
693         _out << "\n\n";
694     }
695     else if(tagname == "h2")
696     {
697         _out << "\n\n";
698     }
699     else if(tagname == "h3")
700     {
701         _out << "\n\n";
702     }
703     else if(tagname == "h4")
704     {
705         _out << "\n\n";
706     }
707     else if(tagname == "h5")
708     {
709         _out << "\n\n";
710     }
711     else if(tagname == "h6")
712     {
713         _out << "\n\n";
714     }
715     else if(tagname == "tt")
716     {
717         _out << "}}";
718     }
719     else
720     {
721         _out << "{" << escaped << "}";
722     }
723 
724     dec();
725     --_pos; // TODO: ???
726 
727     _se = false;
728     _text = false;
729 }
730 
731 string
getLinkMarkup(const std::string & url,const std::string & text,const std::string & anchor,const std::string & tip)732 Confluence::ConfluenceOutput::getLinkMarkup(const std::string& url, const std::string& text, const std::string& anchor,
733                                             const std::string& tip)
734 {
735     ostringstream oss;
736     oss << "[";
737     if(!text.empty()) {
738         oss << text << "|";
739     }
740     oss << url;
741     if(!anchor.empty())
742     {
743         oss << "#" << anchor;
744     }
745     if(!tip.empty())
746     {
747         oss << "|" << tip;
748     }
749     oss << "]";
750     return oss.str();
751 }
752 
753 string
getImageMarkup(const string & url,const string & title)754 Confluence::ConfluenceOutput::getImageMarkup(const string& url, const string& title)
755 {
756     ostringstream oss;
757     oss << "!" << url;
758     if(!title.empty())
759     {
760         oss << "|" << title;
761     }
762     oss << "!";
763     return oss.str();
764 }
765 
766 string
getAnchorMarkup(const std::string & anchor,const std::string & text)767 Confluence::ConfluenceOutput::getAnchorMarkup(const std::string& anchor, const std::string& text)
768 {
769     ostringstream oss;
770     oss << "{anchor:" << anchor << "}";
771     if(!text.empty())
772     {
773         oss << text << "\n";
774     }
775     return oss.str();
776 }
777 
778 string
getNavMarkup(const std::string & prevLink,const std::string & nextLink)779 Confluence::ConfluenceOutput::getNavMarkup(const std::string& prevLink, const std::string& nextLink)
780 {
781     ostringstream oss;
782     oss << "{znav:";
783     if(!prevLink.empty())
784     {
785         oss << "prev=" << prevLink << "|";
786     }
787     if(!nextLink.empty())
788     {
789         oss << "next=" << nextLink;
790     }
791     oss << "}\n";
792     oss << "{section}{section}\n";
793     return oss.str();
794 }
795 
796 list< pair<unsigned int,unsigned int> >
getMarkerLimits(const string & str)797 Confluence::ConfluenceOutput::getMarkerLimits(const string& str)
798 {
799     list< pair<unsigned int,unsigned int> > pairs;
800 
801     size_t start = str.find(TEMP_ESCAPER_START);
802     size_t end;
803     while(start != string::npos)
804     {
805         end = str.find(TEMP_ESCAPER_END, start + TEMP_ESCAPER_START.size());
806         if(end != string::npos)
807         {
808             pair<unsigned int, unsigned int> p =
809                 make_pair(static_cast<unsigned int>(start), static_cast<unsigned int>(end+TEMP_ESCAPER_END.size()));
810             pairs.push_back(p);
811             start = str.find(TEMP_ESCAPER_START, end+TEMP_ESCAPER_END.size());
812         }
813         else
814         {
815             consoleErr << "getMarkerLimits FOUND START OF ESCAPE MARKER WITH NO MATCHING END IN STRING:"
816                        << endl << str.substr(start) << endl;
817             break;
818         }
819     }
820 
821     return pairs;
822 }
823 
824 string
removeMarkers(string str)825 Confluence::ConfluenceOutput::removeMarkers(string str)
826 {
827     // Remove starts
828     size_t start = str.find(TEMP_ESCAPER_START);
829     while(start != string::npos)
830     {
831         str.erase(start, TEMP_ESCAPER_START.size());
832         start = str.find(TEMP_ESCAPER_START, start);
833     }
834 
835     // Remove ends
836     size_t end = str.find(TEMP_ESCAPER_END);
837     while(end != string::npos)
838     {
839         str.erase(end, TEMP_ESCAPER_END.size());
840         end = str.find(TEMP_ESCAPER_END, end);
841     }
842     return str;
843 }
844 
845 void
attr(const string & name,const string & value)846 Confluence::ConfluenceOutput::attr(const string& name, const string& value)
847 {
848     //
849     // Precondition: Attributes can only be attached to elements.
850     //
851     assert(_se);
852     _out << " " << name << "=\"" << escape(value) << "\"";
853 }
854 
855 void
startEscapes()856 Confluence::ConfluenceOutput::startEscapes()
857 {
858     _escape = true;
859 }
860 
861 void
endEscapes()862 Confluence::ConfluenceOutput::endEscapes()
863 {
864     _escape = false;
865 }
866 
867 string
currentElement() const868 Confluence::ConfluenceOutput::currentElement() const
869 {
870     if(_elementStack.size() > 0)
871     {
872         return _elementStack.top();
873     }
874     else
875     {
876         return string();
877     }
878 }
879 
880 string
escape(const string & input) const881 Confluence::ConfluenceOutput::escape(const string& input) const
882 {
883     string v = input;
884 
885     //
886     // Find out whether there is a reserved character to avoid
887     // conversion if not necessary.
888     //
889     const string allReserved = "<>'\"&{}";
890     if(v.find_first_of(allReserved) != string::npos)
891     {
892         //
893         // First convert all & to &amp;
894         //
895         size_t pos = 0;
896         while((pos = v.find_first_of('&', pos)) != string::npos)
897         {
898             v.insert(pos + 1, "amp;");
899             pos += 4;
900         }
901 
902         //
903         // Next convert remaining reserved characters.
904         //
905         const string reserved = "<>'\"{}";
906         pos = 0;
907         while((pos = v.find_first_of(reserved, pos)) != string::npos)
908         {
909             string replace;
910             switch(v[pos])
911             {
912                 case '>':
913                     replace = "&gt;";
914                     break;
915 
916                 case '<':
917                     replace = "&lt;";
918                     break;
919 
920                 case '\'':
921                     replace = "&apos;";
922                     break;
923 
924                 case '"':
925                     replace = "&quot;";
926                     break;
927 
928                 case '{':
929                     replace = "\\{";
930                     break;
931 
932                 case '}':
933                     replace = "\\}";
934                     break;
935 
936                 default:
937                     assert(false);
938             }
939 
940             v.erase(pos, 1);
941             v.insert(pos, replace);
942             pos += replace.size();
943         }
944     }
945     return v;
946 }
947 
948 Confluence::ConfluenceOutput&
operator <<(ConfluenceOutput & out,ios_base & (* val)(ios_base &))949 Confluence::operator<<(ConfluenceOutput& out, ios_base& (*val)(ios_base&))
950 {
951     ostringstream s;
952     s << val;
953     out.print(s.str().c_str());
954     return out;
955 }
956 
StartElement(const string & name)957 Confluence::StartElement::StartElement(const string& name) :
958 _name(name)
959 {
960 }
961 
962 const string&
getName() const963 Confluence::StartElement::getName() const
964 {
965     return _name;
966 }
967 
Attribute(const string & name,const string & value)968 Confluence::Attribute::Attribute(const string& name, const string& value) :
969     _name(name),
970     _value(value)
971 {
972 }
973 
974 const string&
getName() const975 Confluence::Attribute::getName() const
976 {
977     return _name;
978 }
979 
980 const string&
getValue() const981 Confluence::Attribute::getValue() const
982 {
983     return _value;
984 }
985