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 &
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 = ">";
914 break;
915
916 case '<':
917 replace = "<";
918 break;
919
920 case '\'':
921 replace = "'";
922 break;
923
924 case '"':
925 replace = """;
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