Doxygen
Loading...
Searching...
No Matches
markdown.cpp
Go to the documentation of this file.
1/******************************************************************************
2 *
3 * Copyright (C) 1997-2020 by Dimitri van Heesch.
4 *
5 * Permission to use, copy, modify, and distribute this software and its
6 * documentation under the terms of the GNU General Public License is hereby
7 * granted. No representations are made about the suitability of this software
8 * for any purpose. It is provided "as is" without express or implied warranty.
9 * See the GNU General Public License for more details.
10 *
11 * Documents produced by Doxygen are derivative works derived from the
12 * input used in their production; they are not affected by this license.
13 *
14 */
15
16/* Note: part of the code below is inspired by libupskirt written by
17 * Natacha Porté. Original copyright message follows:
18 *
19 * Copyright (c) 2008, Natacha Porté
20 *
21 * Permission to use, copy, modify, and distribute this software for any
22 * purpose with or without fee is hereby granted, provided that the above
23 * copyright notice and this permission notice appear in all copies.
24 *
25 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
26 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
27 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
28 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
29 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
30 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
31 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
32 */
33
34#include <stdio.h>
35
36#include <unordered_map>
37#include <functional>
38#include <atomic>
39#include <array>
40#include <string_view>
41
42#include "markdown.h"
43#include "debug.h"
44#include "util.h"
45#include "doxygen.h"
46#include "commentscan.h"
47#include "entry.h"
48#include "config.h"
49#include "message.h"
50#include "portable.h"
51#include "regex.h"
52#include "fileinfo.h"
53#include "trace.h"
54#include "anchor.h"
55#include "stringutil.h"
56
57#if !ENABLE_MARKDOWN_TRACING
58#undef AUTO_TRACE
59#undef AUTO_TRACE_ADD
60#undef AUTO_TRACE_EXIT
61#define AUTO_TRACE(...) (void)0
62#define AUTO_TRACE_ADD(...) (void)0
63#define AUTO_TRACE_EXIT(...) (void)0
64#endif
65
67{
68 explicitPage, /**< docs start with a page command */
69 explicitMainPage, /**< docs start with a mainpage command */
70 explicitOtherPage, /**< docs start with a dir / defgroup / addtogroup command */
71 notExplicit /**< docs doesn't start with either page or mainpage */
72};
73
74//-----------
75
76// is character c part of an identifier?
77#define isIdChar(c) \
78 ((c>='a' && c<='z') || \
79 (c>='A' && c<='Z') || \
80 (c>='0' && c<='9') || \
81 (static_cast<unsigned char>(c)>=0x80)) // unicode characters
82
83// is character allowed right at the beginning of an emphasis section
84#define extraChar(c) \
85 (c=='-' || c=='+' || c=='!' || \
86 c=='?' || c=='$' || c=='@' || \
87 c=='&' || c=='*' || c=='_' || c=='%' || \
88 c=='[' || c=='(' || c=='.' || \
89 c=='>' || c==':' || c==',' || \
90 c==';' || c=='\'' || c=='"' || c=='`')
91
92// is character at position i in data allowed before an emphasis section
93#define isOpenEmphChar(c) \
94 (c=='\n' || c==' ' || c=='\'' || c=='<' || \
95 c=='>' || c=='{' || c=='(' || c=='[' || \
96 c==',' || c==':' || c==';')
97
98// is character at position i in data an escape that prevents ending an emphasis section
99// so for example *bla (*.txt) is cool*
100#define ignoreCloseEmphChar(c,cn) \
101 (c=='(' || c=='{' || c=='[' || (c=='<' && cn!='/') || \
102 c=='\\' || \
103 c=='@')
104//----------
105
106struct TableCell
107{
108 TableCell() : colSpan(false) {}
109 QCString cellText;
110 bool colSpan;
111};
112
113struct Markdown::Private
114{
115 Private(const QCString &fn,int line,int indent)
116 : fileName(fn), lineNr(line), indentLevel(indent)
117 {
118 // setup callback table for special characters
119 actions[static_cast<unsigned int>('_')] = [this](std::string_view data,size_t offset) { return processEmphasis (data,offset); };
120 actions[static_cast<unsigned int>('*')] = [this](std::string_view data,size_t offset) { return processEmphasis (data,offset); };
121 actions[static_cast<unsigned int>('~')] = [this](std::string_view data,size_t offset) { return processEmphasis (data,offset); };
122 actions[static_cast<unsigned int>('`')] = [this](std::string_view data,size_t offset) { return processCodeSpan (data,offset); };
123 actions[static_cast<unsigned int>('\\')]= [this](std::string_view data,size_t offset) { return processSpecialCommand(data,offset); };
124 actions[static_cast<unsigned int>('@')] = [this](std::string_view data,size_t offset) { return processSpecialCommand(data,offset); };
125 actions[static_cast<unsigned int>('[')] = [this](std::string_view data,size_t offset) { return processLink (data,offset); };
126 actions[static_cast<unsigned int>('!')] = [this](std::string_view data,size_t offset) { return processLink (data,offset); };
127 actions[static_cast<unsigned int>('<')] = [this](std::string_view data,size_t offset) { return processHtmlTag (data,offset); };
128 actions[static_cast<unsigned int>('-')] = [this](std::string_view data,size_t offset) { return processNmdash (data,offset); };
129 actions[static_cast<unsigned int>('"')] = [this](std::string_view data,size_t offset) { return processQuoted (data,offset); };
130 }
131
132 QCString processQuotations(std::string_view data,size_t refIndent);
133 QCString processBlocks(std::string_view data,size_t indent);
134 QCString isBlockCommand(std::string_view data,size_t offset);
135 size_t isSpecialCommand(std::string_view data,size_t offset);
136 size_t findEndOfLine(std::string_view data,size_t offset);
137 int processHtmlTagWrite(std::string_view data,size_t offset,bool doWrite);
138 int processHtmlTag(std::string_view data,size_t offset);
139 int processEmphasis(std::string_view data,size_t offset);
140 int processEmphasis1(std::string_view data,char c);
141 int processEmphasis2(std::string_view data,char c);
142 int processEmphasis3(std::string_view data,char c);
143 int processNmdash(std::string_view data,size_t offset);
144 int processQuoted(std::string_view data,size_t offset);
145 int processCodeSpan(std::string_view data,size_t offset);
146 int processSpecialCommand(std::string_view data,size_t offset);
147 int processLink(std::string_view data,size_t offset);
148 size_t findEmphasisChar(std::string_view, char c, size_t c_size);
149 void addStrEscapeUtf8Nbsp(std::string_view data);
150 void processInline(std::string_view data);
151 void writeMarkdownImage(std::string_view fmt, bool inline_img, bool explicitTitle,
152 const QCString &title, const QCString &content,
153 const QCString &link, const QCString &attributes,
154 const FileDef *fd);
155 int isHeaderline(std::string_view data, bool allowAdjustLevel);
156 int isAtxHeader(std::string_view data, QCString &header,QCString &id,bool allowAdjustLevel,
157 bool *pIsIdGenerated=nullptr);
158 void writeOneLineHeaderOrRuler(std::string_view data);
159 void writeFencedCodeBlock(std::string_view data, std::string_view lang,
160 size_t blockStart,size_t blockEnd);
161 size_t writeBlockQuote(std::string_view data);
162 size_t writeCodeBlock(std::string_view,size_t refIndent);
163 size_t writeTableBlock(std::string_view data);
164 QCString extractTitleId(QCString &title, int level,bool *pIsIdGenerated=nullptr);
165
166 struct LinkRef
167 {
168 LinkRef(const QCString &l,const QCString &t) : link(l), title(t) {}
171 };
172 using Action_t = std::function<int(std::string_view,size_t)>;
173
174 std::unordered_map<std::string,LinkRef> linkRefs;
176 int lineNr = 0;
177 int indentLevel=0; // 0 is outside markdown, -1=page level
179 std::array<Action_t,256> actions;
180};
181
182Markdown::Markdown(const QCString &fileName,int lineNr,int indentLevel)
183 : prv(std::make_unique<Private>(fileName,lineNr,indentLevel))
184{
185 using namespace std::placeholders;
186 (void)lineNr; // not used yet
187}
188
189Markdown::~Markdown() = default;
190
191void Markdown::setIndentLevel(int level) { prv->indentLevel = level; }
192
193
195
196
197//---------- constants -------
198//
199const char *g_utf8_nbsp = "\xc2\xa0"; // UTF-8 nbsp
200const char *g_doxy_nbsp = "&_doxy_nbsp;"; // doxygen escape command for UTF-8 nbsp
201const size_t codeBlockIndent = 4;
202
203//---------- helpers -------
204
205// test if the next characters in data represent a new line (which can be character \n or string \ilinebr).
206// returns 0 if no newline is found, or the number of characters that make up the newline if found.
207inline size_t isNewline(std::string_view data)
208{
209 // normal newline
210 if (data[0] == '\n') return 1;
211 // artificial new line from ^^ in ALIASES
212 if (literal_at(data,"\\ilinebr"))
213 {
214 return (data.size()>8 && data[8]==' ') ? 9 : 8; // also count space after \ilinebr if present
215 }
216 return 0;
217}
218
219// escape double quotes in string
221{
222 AUTO_TRACE("s={}",Trace::trunc(s));
223 if (s.isEmpty()) return s;
224 QCString result;
225 const char *p=s.data();
226 char c=0, pc='\0';
227 while ((c=*p++))
228 {
229 if (c=='"' && pc!='\\') result+='\\';
230 result+=c;
231 pc=c;
232 }
233 AUTO_TRACE_EXIT("result={}",result);
234 return result;
235}
236
237// escape characters that have a special meaning later on.
239{
240 AUTO_TRACE("s={}",Trace::trunc(s));
241 if (s.isEmpty()) return s;
242 bool insideQuote=FALSE;
243 QCString result;
244 const char *p=s.data();
245 char c=0, pc='\0';
246 while ((c=*p++))
247 {
248 switch (c)
249 {
250 case '"':
251 if (pc!='\\') { insideQuote=!insideQuote; }
252 result+=c;
253 break;
254 case '<':
255 // fall through
256 case '>':
257 if (!insideQuote)
258 {
259 result+='\\';
260 result+=c;
261 if ((p[0]==':') && (p[1]==':'))
262 {
263 result+='\\';
264 result+=':';
265 p++;
266 }
267 }
268 else
269 {
270 result+=c;
271 }
272 break;
273 case '\\': if (!insideQuote) { result+='\\'; } result+='\\'; break;
274 case '@': if (!insideQuote) { result+='\\'; } result+='@'; break;
275 // commented out next line due to regression when using % to suppress a link
276 //case '%': if (!insideQuote) { result+='\\'; } result+='%'; break;
277 case '#': if (!insideQuote) { result+='\\'; } result+='#'; break;
278 case '$': if (!insideQuote) { result+='\\'; } result+='$'; break;
279 case '&': if (!insideQuote) { result+='\\'; } result+='&'; break;
280 default:
281 result+=c; break;
282 }
283 pc=c;
284 }
285 AUTO_TRACE_EXIT("result={}",result);
286 return result;
287}
288
289/** helper function to convert presence of left and/or right alignment markers
290 * to an alignment value
291 */
292static Alignment markersToAlignment(bool leftMarker,bool rightMarker)
293{
294 if (leftMarker && rightMarker)
295 {
296 return AlignCenter;
297 }
298 else if (leftMarker)
299 {
300 return AlignLeft;
301 }
302 else if (rightMarker)
303 {
304 return AlignRight;
305 }
306 else
307 {
308 return AlignNone;
309 }
310}
311
312/** parse the image attributes and return attributes for given format */
313static QCString getFilteredImageAttributes(std::string_view fmt, const QCString &attrs)
314{
315 AUTO_TRACE("fmt={} attrs={}",fmt,attrs);
316 StringVector attrList = split(attrs.str(),",");
317 for (const auto &attr_ : attrList)
318 {
319 QCString attr = QCString(attr_).stripWhiteSpace();
320 int i = attr.find(':');
321 if (i>0) // has format
322 {
323 QCString format = attr.left(i).stripWhiteSpace().lower();
324 if (format == fmt) // matching format
325 {
326 AUTO_TRACE_EXIT("result={}",attr.mid(i+1));
327 return attr.mid(i+1); // keep part after :
328 }
329 }
330 else // option that applies to all formats
331 {
332 AUTO_TRACE_EXIT("result={}",attr);
333 return attr;
334 }
335 }
336 return QCString();
337}
338
339// Check if data contains a block command. If so returned the command
340// that ends the block. If not an empty string is returned.
341// Note When offset>0 character position -1 will be inspected.
342//
343// Checks for and skip the following block commands:
344// {@code .. { .. } .. }
345// \dot .. \enddot
346// \code .. \endcode
347// \msc .. \endmsc
348// \f$..\f$
349// \f(..\f)
350// \f[..\f]
351// \f{..\f}
352// \verbatim..\endverbatim
353// \iliteral..\endiliteral
354// \latexonly..\endlatexonly
355// \htmlonly..\endhtmlonly
356// \xmlonly..\endxmlonly
357// \rtfonly..\endrtfonly
358// \manonly..\endmanonly
359// \startuml..\enduml
360QCString Markdown::Private::isBlockCommand(std::string_view data,size_t offset)
361{
362 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
363
364 using EndBlockFunc = QCString (*)(const std::string &,bool,char);
365
366 static const auto getEndBlock = [](const std::string &blockName,bool,char) -> QCString
367 {
368 return "end"+blockName;
369 };
370 static const auto getEndCode = [](const std::string &blockName,bool openBracket,char) -> QCString
371 {
372 return openBracket ? QCString("}") : "end"+blockName;
373 };
374 static const auto getEndUml = [](const std::string &/* blockName */,bool,char) -> QCString
375 {
376 return "enduml";
377 };
378 static const auto getEndFormula = [](const std::string &/* blockName */,bool,char nextChar) -> QCString
379 {
380 switch (nextChar)
381 {
382 case '$': return "f$";
383 case '(': return "f)";
384 case '[': return "f]";
385 case '{': return "f}";
386 }
387 return "";
388 };
389
390 // table mapping a block start command to a function that can return the matching end block string
391 static const std::unordered_map<std::string,EndBlockFunc> blockNames =
392 {
393 { "dot", getEndBlock },
394 { "code", getEndCode },
395 { "icode", getEndBlock },
396 { "msc", getEndBlock },
397 { "verbatim", getEndBlock },
398 { "iverbatim", getEndBlock },
399 { "iliteral", getEndBlock },
400 { "latexonly", getEndBlock },
401 { "htmlonly", getEndBlock },
402 { "xmlonly", getEndBlock },
403 { "rtfonly", getEndBlock },
404 { "manonly", getEndBlock },
405 { "docbookonly", getEndBlock },
406 { "startuml", getEndUml },
407 { "f", getEndFormula }
408 };
409
410 const size_t size = data.size();
411 bool openBracket = offset>0 && data.data()[-1]=='{';
412 bool isEscaped = offset>0 && (data.data()[-1]=='\\' || data.data()[-1]=='@');
413 if (isEscaped) return QCString();
414
415 size_t end=1;
416 while (end<size && (data[end]>='a' && data[end]<='z')) end++;
417 if (end==1) return QCString();
418 std::string blockName(data.substr(1,end-1));
419 auto it = blockNames.find(blockName);
420 QCString result;
421 if (it!=blockNames.end()) // there is a function assigned
422 {
423 result = it->second(blockName, openBracket, end<size ? data[end] : 0);
424 }
425 AUTO_TRACE_EXIT("result={}",result);
426 return result;
427}
428
429size_t Markdown::Private::isSpecialCommand(std::string_view data,size_t offset)
430{
431 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
432
433 using EndCmdFunc = size_t (*)(std::string_view,size_t);
434
435 static const auto endOfLine = [](std::string_view data_,size_t offset_) -> size_t
436 {
437 // skip until the end of line (allowing line continuation characters)
438 char lc = 0;
439 char c = 0;
440 while (offset_<data_.size() && ((c=data_[offset_])!='\n' || lc=='\\'))
441 {
442 if (c=='\\') lc='\\'; // last character was a line continuation
443 else if (c!=' ') lc=0; // rest line continuation
444 offset_++;
445 }
446 return offset_;
447 };
448
449 static const auto endOfLabels = [](std::string_view data_,size_t offset_,bool multi_) -> size_t
450 {
451 if (offset_<data_.size() && data_[offset_]==' ') // we expect a space before the label
452 {
453 char c = 0;
454 offset_++;
455 bool done=false;
456 while (!done)
457 {
458 // skip over spaces
459 while (offset_<data_.size() && data_[offset_]==' ')
460 {
461 offset_++;
462 }
463 // skip over label
464 while (offset_<data_.size() && (c=data_[offset_])!=' ' && c!=',' && c!='\\' && c!='@' && c!='\n')
465 {
466 offset_++;
467 }
468 // optionally skip over a comma separated list of labels
469 if (multi_ && offset_<data_.size() && (data_[offset_]==',' || data_[offset_]==' '))
470 {
471 size_t off = offset_;
472 while (off<data_.size() && data_[off]==' ')
473 {
474 off++;
475 }
476 if (off<data_.size() && data_[off]==',')
477 {
478 offset_ = ++off;
479 }
480 else // no next label found
481 {
482 done=true;
483 }
484 }
485 else
486 {
487 done=true;
488 }
489 }
490 return offset_;
491 }
492 return 0;
493 };
494
495 static const auto endOfLabel = [](std::string_view data_,size_t offset_) -> size_t
496 {
497 return endOfLabels(data_,offset_,false);
498 };
499
500 static const auto endOfLabelOpt = [](std::string_view data_,size_t offset_) -> size_t
501 {
502 size_t index=offset_;
503 if (index<data_.size() && data_[index]==' ') // skip over optional spaces
504 {
505 index++;
506 while (index<data_.size() && data_[index]==' ') index++;
507 }
508 if (index<data_.size() && data_[index]=='{') // find matching '}'
509 {
510 index++;
511 char c = 0;
512 while (index<data_.size() && (c=data_[index])!='}' && c!='\\' && c!='@' && c!='\n') index++;
513 if (index==data_.size() || data_[index]!='}') return 0; // invalid option
514 offset_=index+1; // part after {...} is the option
515 }
516 return endOfLabel(data_,offset_);
517 };
518
519 static const auto endOfParam = [](std::string_view data_,size_t offset_) -> size_t
520 {
521 size_t index=offset_;
522 if (index<data_.size() && data_[index]==' ') // skip over optional spaces
523 {
524 index++;
525 while (index<data_.size() && data_[index]==' ') index++;
526 }
527 if (index<data_.size() && data_[index]=='[') // find matching ']'
528 {
529 index++;
530 char c = 0;
531 while (index<data_.size() && (c=data_[index])!=']' && c!='\n') index++;
532 if (index==data_.size() || data_[index]!=']') return 0; // invalid parameter
533 offset_=index+1; // part after [...] is the parameter name
534 }
535 return endOfLabels(data_,offset_,true);
536 };
537
538 static const auto endOfRetVal = [](std::string_view data_,size_t offset_) -> size_t
539 {
540 return endOfLabels(data_,offset_,true);
541 };
542
543 static const auto endOfFuncLike = [](std::string_view data_,size_t offset_,bool allowSpaces) -> size_t
544 {
545 if (offset_<data_.size() && data_[offset_]==' ') // we expect a space before the name
546 {
547 char c=0;
548 offset_++;
549 // skip over spaces
550 while (offset_<data_.size() && data_[offset_]==' ')
551 {
552 offset_++;
553 }
554 // skip over name (and optionally type)
555 while (offset_<data_.size() && (c=data_[offset_])!='\n' && (allowSpaces || c!=' ') && c!='(')
556 {
557 if (literal_at(data_.substr(offset_),"\\ilinebr ")) break;
558 offset_++;
559 }
560 if (c=='(') // find the end of the function
561 {
562 int count=1;
563 offset_++;
564 while (offset_<data_.size() && (c=data_[offset_++]))
565 {
566 if (c=='(') count++;
567 else if (c==')') count--;
568 if (count==0) return offset_;
569 }
570 }
571 return offset_;
572 }
573 return 0;
574 };
575
576 static const auto endOfFunc = [](std::string_view data_,size_t offset_) -> size_t
577 {
578 return endOfFuncLike(data_,offset_,true);
579 };
580
581 static const auto endOfGuard = [](std::string_view data_,size_t offset_) -> size_t
582 {
583 return endOfFuncLike(data_,offset_,false);
584 };
585
586 static const std::unordered_map<std::string,EndCmdFunc> cmdNames =
587 {
588 { "a", endOfLabel },
589 { "addindex", endOfLine },
590 { "addtogroup", endOfLabel },
591 { "anchor", endOfLabel },
592 { "b", endOfLabel },
593 { "c", endOfLabel },
594 { "category", endOfLine },
595 { "cite", endOfLabel },
596 { "class", endOfLine },
597 { "concept", endOfLine },
598 { "copybrief", endOfFunc },
599 { "copydetails", endOfFunc },
600 { "copydoc", endOfFunc },
601 { "def", endOfFunc },
602 { "defgroup", endOfLabel },
603 { "diafile", endOfLine },
604 { "dir", endOfLine },
605 { "dockbookinclude",endOfLine },
606 { "dontinclude", endOfLine },
607 { "dotfile", endOfLine },
608 { "e", endOfLabel },
609 { "elseif", endOfGuard },
610 { "em", endOfLabel },
611 { "emoji", endOfLabel },
612 { "enum", endOfLabel },
613 { "example", endOfLine },
614 { "exception", endOfLine },
615 { "extends", endOfLabel },
616 { "file", endOfLine },
617 { "fn", endOfFunc },
618 { "headerfile", endOfLine },
619 { "htmlinclude", endOfLine },
620 { "ianchor", endOfLabelOpt },
621 { "idlexcept", endOfLine },
622 { "if", endOfGuard },
623 { "ifnot", endOfGuard },
624 { "image", endOfLine },
625 { "implements", endOfLine },
626 { "include", endOfLine },
627 { "includedoc", endOfLine },
628 { "includelineno", endOfLine },
629 { "ingroup", endOfLabel },
630 { "interface", endOfLine },
631 { "latexinclude", endOfLine },
632 { "maninclude", endOfLine },
633 { "memberof", endOfLabel },
634 { "mscfile", endOfLine },
635 { "namespace", endOfLabel },
636 { "noop", endOfLine },
637 { "overload", endOfLine },
638 { "p", endOfLabel },
639 { "package", endOfLabel },
640 { "page", endOfLabel },
641 { "paragraph", endOfLabel },
642 { "param", endOfParam },
643 { "property", endOfLine },
644 { "protocol", endOfLine },
645 { "qualifier", endOfLine },
646 { "ref", endOfLabel },
647 { "refitem", endOfLine },
648 { "related", endOfLabel },
649 { "relatedalso", endOfLabel },
650 { "relates", endOfLabel },
651 { "relatesalso", endOfLabel },
652 { "retval", endOfRetVal},
653 { "rtfinclude", endOfLine },
654 { "section", endOfLabel },
655 { "skip", endOfLine },
656 { "skipline", endOfLine },
657 { "snippet", endOfLine },
658 { "snippetdoc", endOfLine },
659 { "snippetlineno", endOfLine },
660 { "struct", endOfLine },
661 { "subpage", endOfLabel },
662 { "subparagraph", endOfLabel },
663 { "subsubparagraph",endOfLabel },
664 { "subsection", endOfLabel },
665 { "subsubsection", endOfLabel },
666 { "throw", endOfLabel },
667 { "throws", endOfLabel },
668 { "tparam", endOfLabel },
669 { "typedef", endOfLine },
670 { "plantumlfile", endOfLine },
671 { "union", endOfLine },
672 { "until", endOfLine },
673 { "var", endOfLine },
674 { "verbinclude", endOfLine },
675 { "weakgroup", endOfLabel },
676 { "xmlinclude", endOfLine },
677 { "xrefitem", endOfLabel }
678 };
679
680 bool isEscaped = offset>0 && (data.data()[-1]=='\\' || data.data()[-1]=='@');
681 if (isEscaped) return 0;
682
683 const size_t size = data.size();
684 size_t end=1;
685 while (end<size && (data[end]>='a' && data[end]<='z')) end++;
686 if (end==1) return 0;
687 std::string cmdName(data.substr(1,end-1));
688 size_t result=0;
689 auto it = cmdNames.find(cmdName);
690 if (it!=cmdNames.end()) // command with parameters that should be ignored by markdown
691 {
692 // find the end of the parameters
693 result = it->second(data,end);
694 }
695 AUTO_TRACE_EXIT("result={}",result);
696 return result;
697}
698
699/** looks for the next emph char, skipping other constructs, and
700 * stopping when either it is found, or we are at the end of a paragraph.
701 */
702size_t Markdown::Private::findEmphasisChar(std::string_view data, char c, size_t c_size)
703{
704 AUTO_TRACE("data='{}' c={} c_size={}",Trace::trunc(data),c,c_size);
705 size_t i = 1;
706 const size_t size = data.size();
707
708 while (i<size)
709 {
710 while (i<size && data[i]!=c &&
711 data[i]!='\\' && data[i]!='@' &&
712 !(data[i]=='/' && data[i-1]=='<') && // html end tag also ends emphasis
713 data[i]!='\n') i++;
714 // avoid overflow (unclosed emph token)
715 if (i==size)
716 {
717 return 0;
718 }
719 //printf("findEmphasisChar: data=[%s] i=%d c=%c\n",data,i,data[i]);
720
721 // not counting escaped chars or characters that are unlikely
722 // to appear as the end of the emphasis char
724 {
725 i++;
726 continue;
727 }
728 else
729 {
730 // get length of emphasis token
731 size_t len = 0;
732 while (i+len<size && data[i+len]==c)
733 {
734 len++;
735 }
736
737 if (len>0)
738 {
739 if (len!=c_size || (i+len<size && isIdChar(data[i+len]))) // to prevent touching some_underscore_identifier
740 {
741 i+=len;
742 continue;
743 }
744 AUTO_TRACE_EXIT("result={}",i);
745 return static_cast<int>(i); // found it
746 }
747 }
748
749 // skipping a code span
750 if (data[i]=='`')
751 {
752 int snb=0;
753 while (i<size && data[i]=='`') snb++,i++;
754
755 // find same pattern to end the span
756 int enb=0;
757 while (i<size && enb<snb)
758 {
759 if (data[i]=='`') enb++;
760 if (snb==1 && data[i]=='\'') break; // ` ended by '
761 i++;
762 }
763 }
764 else if (data[i]=='@' || data[i]=='\\')
765 { // skip over blocks that should not be processed
766 QCString endBlockName = isBlockCommand(data.substr(i),i);
767 if (!endBlockName.isEmpty())
768 {
769 i++;
770 size_t l = endBlockName.length();
771 while (i+l<size)
772 {
773 if ((data[i]=='\\' || data[i]=='@') && // command
774 data[i-1]!='\\' && data[i-1]!='@') // not escaped
775 {
776 if (qstrncmp(&data[i+1],endBlockName.data(),l)==0)
777 {
778 break;
779 }
780 }
781 i++;
782 }
783 }
784 else if (i+1<size && isIdChar(data[i+1])) // @cmd, stop processing, see bug 690385
785 {
786 return 0;
787 }
788 else
789 {
790 i++;
791 }
792 }
793 else if (data[i-1]=='<' && data[i]=='/') // html end tag invalidates emphasis
794 {
795 return 0;
796 }
797 else if (data[i]=='\n') // end * or _ at paragraph boundary
798 {
799 i++;
800 while (i<size && data[i]==' ') i++;
801 if (i>=size || data[i]=='\n')
802 {
803 return 0;
804 } // empty line -> paragraph
805 }
806 else // should not get here!
807 {
808 i++;
809 }
810 }
811 return 0;
812}
813
814/** process single emphasis */
815int Markdown::Private::processEmphasis1(std::string_view data, char c)
816{
817 AUTO_TRACE("data='{}' c={}",Trace::trunc(data),c);
818 size_t i = 0;
819 const size_t size = data.size();
820
821 /* skipping one symbol if coming from emph3 */
822 if (size>1 && data[0]==c && data[1]==c) { i=1; }
823
824 while (i<size)
825 {
826 size_t len = findEmphasisChar(data.substr(i), c, 1);
827 if (len==0) { return 0; }
828 i+=len;
829 if (i>=size) { return 0; }
830
831 if (i+1<size && data[i+1]==c)
832 {
833 i++;
834 continue;
835 }
836 if (data[i]==c && data[i-1]!=' ' && data[i-1]!='\n')
837 {
838 out+="<em>";
839 processInline(data.substr(0,i));
840 out+="</em>";
841 AUTO_TRACE_EXIT("result={}",i+1);
842 return static_cast<int>(i+1);
843 }
844 }
845 return 0;
846}
847
848/** process double emphasis */
849int Markdown::Private::processEmphasis2(std::string_view data, char c)
850{
851 AUTO_TRACE("data='{}' c={}",Trace::trunc(data),c);
852 size_t i = 0;
853 const size_t size = data.size();
854
855 while (i<size)
856 {
857 size_t len = findEmphasisChar(data.substr(i), c, 2);
858 if (len==0)
859 {
860 return 0;
861 }
862 i += len;
863 if (i+1<size && data[i]==c && data[i+1]==c && i && data[i-1]!=' ' && data[i-1]!='\n')
864 {
865 if (c == '~') out+="<strike>";
866 else out+="<strong>";
867 processInline(data.substr(0,i));
868 if (c == '~') out+="</strike>";
869 else out+="</strong>";
870 AUTO_TRACE_EXIT("result={}",i+2);
871 return static_cast<int>(i+2);
872 }
873 i++;
874 }
875 return 0;
876}
877
878/** Parsing triple emphasis.
879 * Finds the first closing tag, and delegates to the other emph
880 */
881int Markdown::Private::processEmphasis3(std::string_view data,char c)
882{
883 AUTO_TRACE("data='{}' c={}",Trace::trunc(data),c);
884 size_t i = 0;
885 const size_t size = data.size();
886
887 while (i<size)
888 {
889 size_t len = findEmphasisChar(data.substr(i), c, 3);
890 if (len==0)
891 {
892 return 0;
893 }
894 i+=len;
895
896 /* skip whitespace preceded symbols */
897 if (data[i]!=c || data[i-1]==' ' || data[i-1]=='\n')
898 {
899 continue;
900 }
901
902 if (i+2<size && data[i+1]==c && data[i+2]==c)
903 {
904 out+="<em><strong>";
905 processInline(data.substr(0,i));
906 out+="</strong></em>";
907 AUTO_TRACE_EXIT("result={}",i+3);
908 return static_cast<int>(i+3);
909 }
910 else if (i+1<size && data[i+1]==c)
911 {
912 // double symbol found, handing over to emph1
913 len = processEmphasis1(std::string_view(data.data()-2, size+2), c);
914 if (len==0)
915 {
916 return 0;
917 }
918 else
919 {
920 AUTO_TRACE_EXIT("result={}",len-2);
921 return static_cast<int>(len - 2);
922 }
923 }
924 else
925 {
926 // single symbol found, handing over to emph2
927 len = processEmphasis2(std::string_view(data.data()-1, size+1), c);
928 if (len==0)
929 {
930 return 0;
931 }
932 else
933 {
934 AUTO_TRACE_EXIT("result={}",len-1);
935 return static_cast<int>(len - 1);
936 }
937 }
938 }
939 return 0;
940}
941
942/** Process ndash and mdashes */
943int Markdown::Private::processNmdash(std::string_view data,size_t offset)
944{
945 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
946 const size_t size = data.size();
947 // precondition: data[0]=='-'
948 size_t i=1;
949 int count=1;
950 if (i<size && data[i]=='-') // found --
951 {
952 count++,i++;
953 }
954 if (i<size && data[i]=='-') // found ---
955 {
956 count++,i++;
957 }
958 if (i<size && data[i]=='-') // found ----
959 {
960 count++;
961 }
962 if (count>=2 && offset>=2 && literal_at(data.data()-2,"<!"))
963 { AUTO_TRACE_EXIT("result={}",1-count); return 1-count; } // start HTML comment
964 if (count==2 && size > 2 && data[2]=='>')
965 { return 0; } // end HTML comment
966 if (count==3 && size > 3 && data[3]=='>')
967 { return 0; } // end HTML comment
968 if (count==2 && (offset<8 || !literal_at(data.data()-8,"operator"))) // -- => ndash
969 {
970 out+="&ndash;";
971 AUTO_TRACE_EXIT("result=2");
972 return 2;
973 }
974 else if (count==3) // --- => ndash
975 {
976 out+="&mdash;";
977 AUTO_TRACE_EXIT("result=3");
978 return 3;
979 }
980 // not an ndash or mdash
981 return 0;
982}
983
984/** Process quoted section "...", can contain one embedded newline */
985int Markdown::Private::processQuoted(std::string_view data,size_t)
986{
987 AUTO_TRACE("data='{}'",Trace::trunc(data));
988 const size_t size = data.size();
989 size_t i=1;
990 int nl=0;
991 while (i<size && data[i]!='"' && nl<2)
992 {
993 if (data[i]=='\n') nl++;
994 i++;
995 }
996 if (i<size && data[i]=='"' && nl<2)
997 {
998 out+=data.substr(0,i+1);
999 AUTO_TRACE_EXIT("result={}",i+2);
1000 return static_cast<int>(i+1);
1001 }
1002 // not a quoted section
1003 return 0;
1004}
1005
1006/** Process a HTML tag. Note that <pre>..</pre> are treated specially, in
1007 * the sense that all code inside is written unprocessed
1008 */
1009int Markdown::Private::processHtmlTagWrite(std::string_view data,size_t offset,bool doWrite)
1010{
1011 AUTO_TRACE("data='{}' offset={} doWrite={}",Trace::trunc(data),offset,doWrite);
1012 if (offset>0 && data.data()[-1]=='\\') { return 0; } // escaped <
1013
1014 const size_t size = data.size();
1015
1016 // find the end of the html tag
1017 size_t i=1;
1018 size_t l=0;
1019 // compute length of the tag name
1020 while (i<size && isIdChar(data[i])) i++,l++;
1021 QCString tagName(data.substr(1,i-1));
1022 if (tagName.lower()=="pre") // found <pre> tag
1023 {
1024 bool insideStr=FALSE;
1025 while (i+6<size)
1026 {
1027 char c=data[i];
1028 if (!insideStr && c=='<') // potential start of html tag
1029 {
1030 if (data[i+1]=='/' &&
1031 tolower(data[i+2])=='p' && tolower(data[i+3])=='r' &&
1032 tolower(data[i+4])=='e' && tolower(data[i+5])=='>')
1033 { // found </pre> tag, copy from start to end of tag
1034 if (doWrite) out+=data.substr(0,i+6);
1035 //printf("found <pre>..</pre> [%d..%d]\n",0,i+6);
1036 AUTO_TRACE_EXIT("result={}",i+6);
1037 return static_cast<int>(i+6);
1038 }
1039 }
1040 else if (insideStr && c=='"')
1041 {
1042 if (data[i-1]!='\\') insideStr=FALSE;
1043 }
1044 else if (c=='"')
1045 {
1046 insideStr=TRUE;
1047 }
1048 i++;
1049 }
1050 }
1051 else // some other html tag
1052 {
1053 if (l>0 && i<size)
1054 {
1055 if (data[i]=='/' && i+1<size && data[i+1]=='>') // <bla/>
1056 {
1057 //printf("Found htmlTag={%s}\n",qPrint(QCString(data).left(i+2)));
1058 if (doWrite) out+=data.substr(0,i+2);
1059 AUTO_TRACE_EXIT("result={}",i+2);
1060 return static_cast<int>(i+2);
1061 }
1062 else if (data[i]=='>') // <bla>
1063 {
1064 //printf("Found htmlTag={%s}\n",qPrint(QCString(data).left(i+1)));
1065 if (doWrite) out+=data.substr(0,i+1);
1066 AUTO_TRACE_EXIT("result={}",i+1);
1067 return static_cast<int>(i+1);
1068 }
1069 else if (data[i]==' ') // <bla attr=...
1070 {
1071 i++;
1072 bool insideAttr=FALSE;
1073 while (i<size)
1074 {
1075 if (!insideAttr && data[i]=='"')
1076 {
1077 insideAttr=TRUE;
1078 }
1079 else if (data[i]=='"' && data[i-1]!='\\')
1080 {
1081 insideAttr=FALSE;
1082 }
1083 else if (!insideAttr && data[i]=='>') // found end of tag
1084 {
1085 //printf("Found htmlTag={%s}\n",qPrint(QCString(data).left(i+1)));
1086 if (doWrite) out+=data.substr(0,i+1);
1087 AUTO_TRACE_EXIT("result={}",i+1);
1088 return static_cast<int>(i+1);
1089 }
1090 i++;
1091 }
1092 }
1093 }
1094 }
1095 AUTO_TRACE_EXIT("not a valid html tag");
1096 return 0;
1097}
1098
1099int Markdown::Private::processHtmlTag(std::string_view data,size_t offset)
1100{
1101 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
1102 return processHtmlTagWrite(data,offset,true);
1103}
1104
1105int Markdown::Private::processEmphasis(std::string_view data,size_t offset)
1106{
1107 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
1108 const size_t size = data.size();
1109
1110 if ((offset>0 && !isOpenEmphChar(data.data()[-1])) || // invalid char before * or _
1111 (size>1 && data[0]!=data[1] && !(isIdChar(data[1]) || extraChar(data[1]))) || // invalid char after * or _
1112 (size>2 && data[0]==data[1] && !(isIdChar(data[2]) || extraChar(data[2])))) // invalid char after ** or __
1113 {
1114 AUTO_TRACE_EXIT("invalid surrounding characters");
1115 return 0;
1116 }
1117
1118 char c = data[0];
1119 int ret = 0;
1120 if (size>2 && c!='~' && data[1]!=c) // _bla or *bla
1121 {
1122 // whitespace cannot follow an opening emphasis
1123 if (data[1]==' ' || data[1]=='\n' ||
1124 (ret = processEmphasis1(data.substr(1), c)) == 0)
1125 {
1126 return 0;
1127 }
1128 AUTO_TRACE_EXIT("result={}",ret+1);
1129 return ret+1;
1130 }
1131 if (size>3 && data[1]==c && data[2]!=c) // __bla or **bla
1132 {
1133 if (data[2]==' ' || data[2]=='\n' ||
1134 (ret = processEmphasis2(data.substr(2), c)) == 0)
1135 {
1136 return 0;
1137 }
1138 AUTO_TRACE_EXIT("result={}",ret+2);
1139 return ret+2;
1140 }
1141 if (size>4 && c!='~' && data[1]==c && data[2]==c && data[3]!=c) // ___bla or ***bla
1142 {
1143 if (data[3]==' ' || data[3]=='\n' ||
1144 (ret = processEmphasis3(data.substr(3), c)) == 0)
1145 {
1146 return 0;
1147 }
1148 AUTO_TRACE_EXIT("result={}",ret+3);
1149 return ret+3;
1150 }
1151 return 0;
1152}
1153
1155 std::string_view fmt, bool inline_img, bool explicitTitle,
1156 const QCString &title, const QCString &content,
1157 const QCString &link, const QCString &attrs,
1158 const FileDef *fd)
1159{
1160 AUTO_TRACE("fmt={} inline_img={} explicitTitle={} title={} content={} link={} attrs={}",
1161 fmt,inline_img,explicitTitle,Trace::trunc(title),Trace::trunc(content),link,attrs);
1162 QCString attributes = getFilteredImageAttributes(fmt, attrs);
1163 out+="@image";
1164 if (inline_img)
1165 {
1166 out+="{inline}";
1167 }
1168 out+=" ";
1169 out+=fmt;
1170 out+=" ";
1171 out+=link.mid(fd ? 0 : 5);
1172 if (!explicitTitle && !content.isEmpty())
1173 {
1174 out+=" \"";
1175 out+=escapeDoubleQuotes(content);
1176 out+="\"";
1177 }
1178 else if ((content.isEmpty() || explicitTitle) && !title.isEmpty())
1179 {
1180 out+=" \"";
1181 out+=escapeDoubleQuotes(title);
1182 out+="\"";
1183 }
1184 else
1185 {
1186 out+=" ";// so the line break will not be part of the image name
1187 }
1188 if (!attributes.isEmpty())
1189 {
1190 out+=" ";
1191 out+=attributes;
1192 out+=" ";
1193 }
1194 out+="\\ilinebr ";
1195}
1196
1197int Markdown::Private::processLink(const std::string_view data,size_t offset)
1198{
1199 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
1200 const size_t size = data.size();
1201 QCString content;
1202 QCString link;
1203 QCString title;
1204 bool isImageLink = FALSE;
1205 bool isImageInline = FALSE;
1206 bool isToc = FALSE;
1207 size_t i=1;
1208 if (data[0]=='!')
1209 {
1210 isImageLink = TRUE;
1211 if (size<2 || data[1]!='[')
1212 {
1213 return 0;
1214 }
1215
1216 // if there is non-whitespace before the ![ within the scope of two new lines, the image
1217 // is considered inlined, i.e. the image is not preceded by an empty line
1218 int numNLsNeeded=2;
1219 int pos = -1;
1220 while (pos>=-static_cast<int>(offset) && numNLsNeeded>0)
1221 {
1222 if (data.data()[pos]=='\n') numNLsNeeded--;
1223 else if (data.data()[pos]!=' ') // found non-whitespace, stop searching
1224 {
1225 isImageInline=true;
1226 break;
1227 }
1228 pos--;
1229 }
1230 // skip '!['
1231 i++;
1232 }
1233 size_t contentStart=i;
1234 int level=1;
1235 int nlTotal=0;
1236 int nl=0;
1237 // find the matching ]
1238 while (i<size)
1239 {
1240 if (data[i-1]=='\\') // skip escaped characters
1241 {
1242 }
1243 else if (data[i]=='[')
1244 {
1245 level++;
1246 }
1247 else if (data[i]==']')
1248 {
1249 level--;
1250 if (level<=0) break;
1251 }
1252 else if (data[i]=='\n')
1253 {
1254 nl++;
1255 if (nl>1) { return 0; } // only allow one newline in the content
1256 }
1257 i++;
1258 }
1259 nlTotal += nl;
1260 nl = 0;
1261 if (i>=size) return 0; // premature end of comment -> no link
1262 size_t contentEnd=i;
1263 content = data.substr(contentStart,contentEnd-contentStart);
1264 //printf("processLink: content={%s}\n",qPrint(content));
1265 if (!isImageLink && content.isEmpty()) { return 0; } // no link text
1266 i++; // skip over ]
1267
1268 bool whiteSpace = false;
1269 // skip whitespace
1270 while (i<size && data[i]==' ') { whiteSpace = true; i++; }
1271 if (i<size && data[i]=='\n') // one newline allowed here
1272 {
1273 whiteSpace = true;
1274 i++;
1275 // skip more whitespace
1276 while (i<size && data[i]==' ') i++;
1277 }
1278 if (whiteSpace && i<size && (data[i]=='(' || data[i]=='[')) return 0;
1279
1280 bool explicitTitle=FALSE;
1281 if (i<size && data[i]=='(') // inline link
1282 {
1283 i++;
1284 while (i<size && data[i]==' ') i++;
1285 bool uriFormat=false;
1286 if (i<size && data[i]=='<') { i++; uriFormat=true; }
1287 size_t linkStart=i;
1288 int braceCount=1;
1289 while (i<size && data[i]!='\'' && data[i]!='"' && braceCount>0)
1290 {
1291 if (data[i]=='\n') // unexpected EOL
1292 {
1293 nl++;
1294 if (nl>1) { return 0; }
1295 }
1296 else if (data[i]=='(')
1297 {
1298 braceCount++;
1299 }
1300 else if (data[i]==')')
1301 {
1302 braceCount--;
1303 }
1304 if (braceCount>0)
1305 {
1306 i++;
1307 }
1308 }
1309 nlTotal += nl;
1310 nl = 0;
1311 if (i>=size || data[i]=='\n') { return 0; }
1312 link = data.substr(linkStart,i-linkStart);
1313 link = link.stripWhiteSpace();
1314 //printf("processLink: link={%s}\n",qPrint(link));
1315 if (link.isEmpty()) { return 0; }
1316 if (uriFormat && link.at(link.length()-1)=='>') link=link.left(link.length()-1);
1317
1318 // optional title
1319 if (data[i]=='\'' || data[i]=='"')
1320 {
1321 char c = data[i];
1322 i++;
1323 size_t titleStart=i;
1324 nl=0;
1325 while (i<size)
1326 {
1327 if (data[i]=='\n')
1328 {
1329 if (nl>1) { return 0; }
1330 nl++;
1331 }
1332 else if (data[i]=='\\') // escaped char in string
1333 {
1334 i++;
1335 }
1336 else if (data[i]==c)
1337 {
1338 i++;
1339 break;
1340 }
1341 i++;
1342 }
1343 if (i>=size)
1344 {
1345 return 0;
1346 }
1347 size_t titleEnd = i-1;
1348 // search back for closing marker
1349 while (titleEnd>titleStart && data[titleEnd]==' ') titleEnd--;
1350 if (data[titleEnd]==c) // found it
1351 {
1352 title = data.substr(titleStart,titleEnd-titleStart);
1353 explicitTitle=TRUE;
1354 while (i<size)
1355 {
1356 if (data[i]==' ')i++; // remove space after the closing quote and the closing bracket
1357 else if (data[i] == ')') break; // the end bracket
1358 else // illegal
1359 {
1360 return 0;
1361 }
1362 }
1363 }
1364 else
1365 {
1366 return 0;
1367 }
1368 }
1369 i++;
1370 }
1371 else if (i<size && data[i]=='[') // reference link
1372 {
1373 i++;
1374 size_t linkStart=i;
1375 nl=0;
1376 // find matching ]
1377 while (i<size && data[i]!=']')
1378 {
1379 if (data[i]=='\n')
1380 {
1381 nl++;
1382 if (nl>1) { return 0; }
1383 }
1384 i++;
1385 }
1386 if (i>=size) { return 0; }
1387 // extract link
1388 link = data.substr(linkStart,i-linkStart);
1389 //printf("processLink: link={%s}\n",qPrint(link));
1390 link = link.stripWhiteSpace();
1391 if (link.isEmpty()) // shortcut link
1392 {
1393 link=content;
1394 }
1395 // lookup reference
1396 QCString link_lower = link.lower();
1397 auto lr_it=linkRefs.find(link_lower.str());
1398 if (lr_it!=linkRefs.end()) // found it
1399 {
1400 link = lr_it->second.link;
1401 title = lr_it->second.title;
1402 //printf("processLink: ref: link={%s} title={%s}\n",qPrint(link),qPrint(title));
1403 }
1404 else // reference not found!
1405 {
1406 //printf("processLink: ref {%s} do not exist\n",link.qPrint(lower()));
1407 return 0;
1408 }
1409 i++;
1410 }
1411 else if (i<size && data[i]!=':' && !content.isEmpty()) // minimal link ref notation [some id]
1412 {
1413 QCString content_lower = content.lower();
1414 auto lr_it = linkRefs.find(content_lower.str());
1415 //printf("processLink: minimal link {%s} lr=%p",qPrint(content),lr);
1416 if (lr_it!=linkRefs.end()) // found it
1417 {
1418 link = lr_it->second.link;
1419 title = lr_it->second.title;
1420 explicitTitle=TRUE;
1421 i=contentEnd;
1422 }
1423 else if (content=="TOC")
1424 {
1425 isToc=TRUE;
1426 i=contentEnd;
1427 }
1428 else
1429 {
1430 return 0;
1431 }
1432 i++;
1433 }
1434 else
1435 {
1436 return 0;
1437 }
1438 nlTotal += nl;
1439
1440 // search for optional image attributes
1441 QCString attributes;
1442 if (isImageLink)
1443 {
1444 size_t j = i;
1445 // skip over whitespace
1446 while (j<size && data[j]==' ') { j++; }
1447 if (j<size && data[j]=='{') // we have attributes
1448 {
1449 i = j;
1450 // skip over '{'
1451 i++;
1452 size_t attributesStart=i;
1453 nl=0;
1454 // find the matching '}'
1455 while (i<size)
1456 {
1457 if (data[i-1]=='\\') // skip escaped characters
1458 {
1459 }
1460 else if (data[i]=='{')
1461 {
1462 level++;
1463 }
1464 else if (data[i]=='}')
1465 {
1466 level--;
1467 if (level<=0) break;
1468 }
1469 else if (data[i]=='\n')
1470 {
1471 nl++;
1472 if (nl>1) { return 0; } // only allow one newline in the content
1473 }
1474 i++;
1475 }
1476 nlTotal += nl;
1477 if (i>=size) return 0; // premature end of comment -> no attributes
1478 size_t attributesEnd=i;
1479 attributes = data.substr(attributesStart,attributesEnd-attributesStart);
1480 i++; // skip over '}'
1481 }
1482 if (!isImageInline)
1483 {
1484 // if there is non-whitespace after the image within the scope of two new lines, the image
1485 // is considered inlined, i.e. the image is not followed by an empty line
1486 int numNLsNeeded=2;
1487 size_t pos = i;
1488 while (pos<size && numNLsNeeded>0)
1489 {
1490 if (data[pos]=='\n') numNLsNeeded--;
1491 else if (data[pos]!=' ') // found non-whitespace, stop searching
1492 {
1493 isImageInline=true;
1494 break;
1495 }
1496 pos++;
1497 }
1498 }
1499 }
1500
1501 if (isToc) // special case for [TOC]
1502 {
1503 int toc_level = Config_getInt(TOC_INCLUDE_HEADINGS);
1504 if (toc_level>=SectionType::MinLevel && toc_level<=SectionType::MaxLevel)
1505 {
1506 out+="@tableofcontents{html:";
1507 out+=QCString().setNum(toc_level);
1508 out+="}";
1509 }
1510 }
1511 else if (isImageLink)
1512 {
1513 bool ambig = false;
1514 FileDef *fd=nullptr;
1515 if (link.find("@ref ")!=-1 || link.find("\\ref ")!=-1 ||
1517 // assume doxygen symbol link or local image link
1518 {
1519 // check if different handling is needed per format
1520 writeMarkdownImage("html", isImageInline, explicitTitle, title, content, link, attributes, fd);
1521 writeMarkdownImage("latex", isImageInline, explicitTitle, title, content, link, attributes, fd);
1522 writeMarkdownImage("rtf", isImageInline, explicitTitle, title, content, link, attributes, fd);
1523 writeMarkdownImage("docbook", isImageInline, explicitTitle, title, content, link, attributes, fd);
1524 writeMarkdownImage("xml", isImageInline, explicitTitle, title, content, link, attributes, fd);
1525 }
1526 else
1527 {
1528 out+="<img src=\"";
1529 out+=link;
1530 out+="\" alt=\"";
1531 out+=content;
1532 out+="\"";
1533 if (!title.isEmpty())
1534 {
1535 out+=" title=\"";
1536 out+=substitute(title.simplifyWhiteSpace(),"\"","&quot;");
1537 out+="\"";
1538 }
1539 out+="/>";
1540 }
1541 }
1542 else
1543 {
1545 int lp=-1;
1546 if ((lp=link.find("@ref "))!=-1 || (lp=link.find("\\ref "))!=-1 || (lang==SrcLangExt::Markdown && !isURL(link)))
1547 // assume doxygen symbol link
1548 {
1549 if (lp==-1) // link to markdown page
1550 {
1551 out+="@ref \"";
1552 if (!(Portable::isAbsolutePath(link) || isURL(link)))
1553 {
1554 FileInfo forg(link.str());
1555 if (forg.exists() && forg.isReadable())
1556 {
1557 link = forg.absFilePath();
1558 }
1559 else if (!(forg.exists() && forg.isReadable()))
1560 {
1561 FileInfo fi(fileName.str());
1562 QCString mdFile = fileName.left(fileName.length()-fi.fileName().length()) + link;
1563 FileInfo fmd(mdFile.str());
1564 if (fmd.exists() && fmd.isReadable())
1565 {
1566 link = fmd.absFilePath().data();
1567 }
1568 }
1569 }
1570 out+=link;
1571 out+="\"";
1572 }
1573 else
1574 {
1575 out+=link;
1576 }
1577 out+=" \"";
1578 if (explicitTitle && !title.isEmpty())
1579 {
1580 out+=substitute(title,"\"","&quot;");
1581 }
1582 else
1583 {
1584 processInline(std::string_view(substitute(content,"\"","&quot;").str()));
1585 }
1586 out+="\"";
1587 }
1588 else if ((lp=link.find('#'))!=-1 || link.find('/')!=-1 || link.find('.')!=-1)
1589 { // file/url link
1590 if (lp==0 || (lp>0 && !isURL(link) && Config_getEnum(MARKDOWN_ID_STYLE)==MARKDOWN_ID_STYLE_t::GITHUB))
1591 {
1592 out+="@ref \"";
1594 out+="\" \"";
1595 out+=substitute(content.simplifyWhiteSpace(),"\"","&quot;");
1596 out+="\"";
1597 }
1598 else
1599 {
1600 out+="<a href=\"";
1601 out+=link;
1602 out+="\"";
1603 for (int ii = 0; ii < nlTotal; ii++) out+="\n";
1604 if (!title.isEmpty())
1605 {
1606 out+=" title=\"";
1607 out+=substitute(title.simplifyWhiteSpace(),"\"","&quot;");
1608 out+="\"";
1609 }
1610 out+=" ";
1612 out+=">";
1613 content = content.simplifyWhiteSpace();
1614 processInline(std::string_view(content.str()));
1615 out+="</a>";
1616 }
1617 }
1618 else // avoid link to e.g. F[x](y)
1619 {
1620 //printf("no link for '%s'\n",qPrint(link));
1621 return 0;
1622 }
1623 }
1624 AUTO_TRACE_EXIT("result={}",i);
1625 return static_cast<int>(i);
1626}
1627
1628/** `` ` `` parsing a code span (assuming codespan != 0) */
1629int Markdown::Private::processCodeSpan(std::string_view data,size_t offset)
1630{
1631 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
1632 const size_t size = data.size();
1633
1634 /* counting the number of backticks in the delimiter */
1635 size_t nb=0, end=0;
1636 while (nb<size && data[nb]=='`')
1637 {
1638 nb++;
1639 }
1640
1641 /* finding the next delimiter with the same amount of backticks */
1642 size_t i = 0;
1643 char pc = '`';
1644 for (end=nb; end<size; end++)
1645 {
1646 //AUTO_TRACE_ADD("c={} nb={} i={} size={}",data[end],nb,i,size);
1647 if (data[end]=='`')
1648 {
1649 i++;
1650 if (nb==1) // `...`
1651 {
1652 if (end<size && data[end+1]=='`') // skip over `` inside `...`
1653 {
1654 AUTO_TRACE_ADD("case1.1");
1655 // skip
1656 end++;
1657 i=0;
1658 }
1659 else // normal end of `...`
1660 {
1661 AUTO_TRACE_ADD("case1.2");
1662 break;
1663 }
1664 }
1665 else if (i==nb) // ``...``
1666 {
1667 if (end<size && data[end+1]=='`') // do greedy match
1668 {
1669 // skip this quote and use the next one to terminate the sequence, e.g. ``X`Y```
1670 i--;
1671 AUTO_TRACE_ADD("case2.1");
1672 }
1673 else // normal end of ``...``
1674 {
1675 AUTO_TRACE_ADD("case2.2");
1676 break;
1677 }
1678 }
1679 }
1680 else if (data[end]=='\n')
1681 {
1682 // consecutive newlines
1683 if (pc == '\n')
1684 {
1685 AUTO_TRACE_EXIT("new paragraph");
1686 return 0;
1687 }
1688 pc = '\n';
1689 i = 0;
1690 }
1691 else if (data[end]=='\'' && nb==1 && (end==size-1 || (end+1<size && data[end+1]!='\'' && !isIdChar(data[end+1]))))
1692 { // look for quoted strings like 'some word', but skip strings like `it's cool`
1693 out+="&lsquo;";
1694 out+=data.substr(nb,end-nb);
1695 out+="&rsquo;";
1696 AUTO_TRACE_EXIT("quoted end={}",end+1);
1697 return static_cast<int>(end+1);
1698 }
1699 else if (data[end]=='\'' && nb==2 && end<size-1 && data[end+1]=='\'')
1700 { // look for '' to match a ``
1701 out+="&ldquo;";
1702 out+=data.substr(nb,end-nb);
1703 out+="&rdquo;";
1704 AUTO_TRACE_EXIT("double quoted end={}",end+1);
1705 return static_cast<int>(end+2);
1706 }
1707 else
1708 {
1709 if (data[end]!=' ') pc = data[end];
1710 i=0;
1711 }
1712 }
1713 if (i < nb && end >= size)
1714 {
1715 AUTO_TRACE_EXIT("no matching delimiter nb={} i={}",nb,i);
1716 if (nb>=3) // found ``` that is not at the start of the line, keep it as-is.
1717 {
1718 out+=data.substr(0,nb);
1719 return nb;
1720 }
1721 return 0; // no matching delimiter
1722 }
1723 while (end<size && data[end]=='`') // do greedy match in case we have more end backticks.
1724 {
1725 end++;
1726 }
1727
1728 //printf("found code span '%s'\n",qPrint(QCString(data+f_begin).left(f_end-f_begin)));
1729
1730 /* real code span */
1731 if (nb+nb < end)
1732 {
1733 QCString codeFragment = data.substr(nb, end-nb-nb);
1734 out+="<tt>";
1735 out+=escapeSpecialChars(codeFragment);
1736 out+="</tt>";
1737 }
1738 AUTO_TRACE_EXIT("result={} nb={}",end,nb);
1739 return static_cast<int>(end);
1740}
1741
1743{
1745 if (Portable::strnstr(data.data(),g_doxy_nbsp,data.size())==nullptr) // no escape needed -> fast
1746 {
1747 out+=data;
1748 }
1749 else // escape needed -> slow
1750 {
1752 }
1753}
1754
1755int Markdown::Private::processSpecialCommand(std::string_view data, size_t offset)
1756{
1758 const size_t size = data.size();
1759 size_t i=1;
1760 QCString endBlockName = isBlockCommand(data,offset);
1761 if (!endBlockName.isEmpty())
1762 {
1763 AUTO_TRACE_ADD("endBlockName={}",endBlockName);
1764 size_t l = endBlockName.length();
1765 while (i+l<size)
1766 {
1767 if ((data[i]=='\\' || data[i]=='@') && // command
1768 data[i-1]!='\\' && data[i-1]!='@') // not escaped
1769 {
1770 if (qstrncmp(&data[i+1],endBlockName.data(),l)==0)
1771 {
1772 //printf("found end at %d\n",i);
1773 addStrEscapeUtf8Nbsp(data.substr(0,i+1+l));
1774 AUTO_TRACE_EXIT("result={}",i+1+l);
1775 return static_cast<int>(i+1+l);
1776 }
1777 }
1778 i++;
1779 }
1780 }
1781 size_t endPos = isSpecialCommand(data,offset);
1782 if (endPos>0)
1783 {
1784 out+=data.substr(0,endPos);
1785 return static_cast<int>(endPos);
1786 }
1787 if (size>1 && data[0]=='\\') // escaped characters
1788 {
1789 char c=data[1];
1790 if (c=='[' || c==']' || c=='*' || c=='(' || c==')' || c=='`' || c=='_')
1791 {
1792 out+=data[1];
1793 AUTO_TRACE_EXIT("2");
1794 return 2;
1795 }
1796 else if (c=='\\' || c=='@')
1797 {
1798 out+=data.substr(0,2);
1799 AUTO_TRACE_EXIT("2");
1800 return 2;
1801 }
1802 else if (c=='-' && size>3 && data[2]=='-' && data[3]=='-') // \---
1803 {
1804 out+=data.substr(1,3);
1805 AUTO_TRACE_EXIT("2");
1806 return 4;
1807 }
1808 else if (c=='-' && size>2 && data[2]=='-') // \--
1809 {
1810 out+=data.substr(1,2);
1811 AUTO_TRACE_EXIT("3");
1812 return 3;
1813 }
1814 }
1815 else if (size>1 && data[0]=='@') // escaped characters
1816 {
1817 char c=data[1];
1818 if (c=='\\' || c=='@')
1819 {
1820 out+=data.substr(0,2);
1821 AUTO_TRACE_EXIT("2");
1822 return 2;
1823 }
1824 }
1825 return 0;
1826}
1827
1829{
1830 AUTO_TRACE("data='{}'",Trace::trunc(data));
1831 size_t i=0;
1832 size_t end=0;
1833 Action_t action;
1834 const size_t size = data.size();
1835 while (i<size)
1836 {
1837 // skip over characters that do not trigger a specific action
1838 while (end<size && ((action=actions[static_cast<uint8_t>(data[end])])==nullptr)) end++;
1839 // and add them to the output
1840 out+=data.substr(i,end-i);
1841 if (end>=size) break;
1842 i=end;
1843 // do the action matching a special character at i
1844 int iend = action(data.substr(i),i);
1845 if (iend<=0) // update end
1846 {
1847 end=i+1-iend;
1848 }
1849 else // skip until end
1850 {
1851 i+=iend;
1852 end=i;
1853 }
1854 }
1855}
1856
1857/** returns whether the line is a setext-style hdr underline */
1858int Markdown::Private::isHeaderline(std::string_view data, bool allowAdjustLevel)
1859{
1860 AUTO_TRACE("data='{}' allowAdjustLevel",Trace::trunc(data),allowAdjustLevel);
1861 size_t i=0, c=0;
1862 const size_t size = data.size();
1863 while (i<size && data[i]==' ') i++;
1864 if (i==size) return 0;
1865
1866 // test of level 1 header
1867 if (data[i]=='=')
1868 {
1869 while (i<size && data[i]=='=') i++,c++;
1870 while (i<size && data[i]==' ') i++;
1871 int level = (c>1 && (i>=size || data[i]=='\n')) ? 1 : 0;
1872 if (allowAdjustLevel && level==1 && indentLevel==-1)
1873 {
1874 // In case a page starts with a header line we use it as title, promoting it to @page.
1875 // We set g_indentLevel to -1 to promoting the other sections if they have a deeper
1876 // nesting level than the page header, i.e. @section..@subsection becomes @page..@section.
1877 // In case a section at the same level is found (@section..@section) however we need
1878 // to undo this (and the result will be @page..@section).
1879 indentLevel=0;
1880 }
1881 AUTO_TRACE_EXIT("result={}",indentLevel+level);
1882 return indentLevel+level;
1883 }
1884 // test of level 2 header
1885 if (data[i]=='-')
1886 {
1887 while (i<size && data[i]=='-') i++,c++;
1888 while (i<size && data[i]==' ') i++;
1889 return (c>1 && (i>=size || data[i]=='\n')) ? indentLevel+2 : 0;
1890 }
1891 return 0;
1892}
1893
1894/** returns true if this line starts a block quote */
1895static bool isBlockQuote(std::string_view data,size_t indent)
1896{
1897 AUTO_TRACE("data='{}' indent={}",Trace::trunc(data),indent);
1898 size_t i = 0;
1899 const size_t size = data.size();
1900 while (i<size && data[i]==' ') i++;
1901 if (i<indent+codeBlockIndent) // could be a quotation
1902 {
1903 // count >'s and skip spaces
1904 int level=0;
1905 while (i<size && (data[i]=='>' || data[i]==' '))
1906 {
1907 if (data[i]=='>') level++;
1908 i++;
1909 }
1910 // last characters should be a space or newline,
1911 // so a line starting with >= does not match, but only when level equals 1
1912 bool res = (level>0 && i<size && ((data[i-1]==' ') || data[i]=='\n')) || (level > 1);
1913 AUTO_TRACE_EXIT("result={}",res);
1914 return res;
1915 }
1916 else // too much indentation -> code block
1917 {
1918 AUTO_TRACE_EXIT("result=false: too much indentation");
1919 return false;
1920 }
1921}
1922
1923/** returns end of the link ref if this is indeed a link reference. */
1924static size_t isLinkRef(std::string_view data, QCString &refid, QCString &link, QCString &title)
1925{
1926 AUTO_TRACE("data='{}'",Trace::trunc(data));
1927 const size_t size = data.size();
1928 // format: start with [some text]:
1929 size_t i = 0;
1930 while (i<size && data[i]==' ') i++;
1931 if (i>=size || data[i]!='[') { return 0; }
1932 i++;
1933 size_t refIdStart=i;
1934 while (i<size && data[i]!='\n' && data[i]!=']') i++;
1935 if (i>=size || data[i]!=']') { return 0; }
1936 refid = data.substr(refIdStart,i-refIdStart);
1937 if (refid.isEmpty()) { return 0; }
1938 AUTO_TRACE_ADD("refid found {}",refid);
1939 //printf(" isLinkRef: found refid='%s'\n",qPrint(refid));
1940 i++;
1941 if (i>=size || data[i]!=':') { return 0; }
1942 i++;
1943
1944 // format: whitespace* \n? whitespace* (<url> | url)
1945 while (i<size && data[i]==' ') i++;
1946 if (i<size && data[i]=='\n')
1947 {
1948 i++;
1949 while (i<size && data[i]==' ') i++;
1950 }
1951 if (i>=size) { return 0; }
1952
1953 if (i<size && data[i]=='<') i++;
1954 size_t linkStart=i;
1955 while (i<size && data[i]!=' ' && data[i]!='\n') i++;
1956 size_t linkEnd=i;
1957 if (i<size && data[i]=='>') i++;
1958 if (linkStart==linkEnd) { return 0; } // empty link
1959 link = data.substr(linkStart,linkEnd-linkStart);
1960 AUTO_TRACE_ADD("link found {}",Trace::trunc(link));
1961 if (link=="@ref" || link=="\\ref")
1962 {
1963 size_t argStart=i;
1964 while (i<size && data[i]!='\n' && data[i]!='"') i++;
1965 link+=data.substr(argStart,i-argStart);
1966 }
1967
1968 title.clear();
1969
1970 // format: (whitespace* \n? whitespace* ( 'title' | "title" | (title) ))?
1971 size_t eol=0;
1972 while (i<size && data[i]==' ') i++;
1973 if (i<size && data[i]=='\n')
1974 {
1975 eol=i;
1976 i++;
1977 while (i<size && data[i]==' ') i++;
1978 }
1979 if (i>=size)
1980 {
1981 AUTO_TRACE_EXIT("result={}: end of isLinkRef while looking for title",i);
1982 return i; // end of buffer while looking for the optional title
1983 }
1984
1985 char c = data[i];
1986 if (c=='\'' || c=='"' || c=='(') // optional title present?
1987 {
1988 //printf(" start of title found! char='%c'\n",c);
1989 i++;
1990 if (c=='(') c=')'; // replace c by end character
1991 size_t titleStart=i;
1992 // search for end of the line
1993 while (i<size && data[i]!='\n') i++;
1994 eol = i;
1995
1996 // search back to matching character
1997 size_t end=i-1;
1998 while (end>titleStart && data[end]!=c) end--;
1999 if (end>titleStart)
2000 {
2001 title = data.substr(titleStart,end-titleStart);
2002 }
2003 AUTO_TRACE_ADD("title found {}",Trace::trunc(title));
2004 }
2005 while (i<size && data[i]==' ') i++;
2006 //printf("end of isLinkRef: i=%d size=%d data[i]='%c' eol=%d\n",
2007 // i,size,data[i],eol);
2008 if (i>=size) { AUTO_TRACE_EXIT("result={}",i); return i; } // end of buffer while ref id was found
2009 else if (eol>0) { AUTO_TRACE_EXIT("result={}",eol); return eol; } // end of line while ref id was found
2010 return 0; // invalid link ref
2011}
2012
2013static bool isHRuler(std::string_view data)
2014{
2015 AUTO_TRACE("data='{}'",Trace::trunc(data));
2016 size_t i=0;
2017 size_t size = data.size();
2018 if (size>0 && data[size-1]=='\n') size--; // ignore newline character
2019 while (i<size && data[i]==' ') i++;
2020 if (i>=size) { AUTO_TRACE_EXIT("result=false: empty line"); return false; } // empty line
2021 char c=data[i];
2022 if (c!='*' && c!='-' && c!='_')
2023 {
2024 AUTO_TRACE_EXIT("result=false: {} is not a hrule character",c);
2025 return false; // not a hrule character
2026 }
2027 int n=0;
2028 while (i<size)
2029 {
2030 if (data[i]==c)
2031 {
2032 n++; // count rule character
2033 }
2034 else if (data[i]!=' ')
2035 {
2036 AUTO_TRACE_EXIT("result=false: line contains non hruler characters");
2037 return false; // line contains non hruler characters
2038 }
2039 i++;
2040 }
2041 AUTO_TRACE_EXIT("result={}",n>=3);
2042 return n>=3; // at least 3 characters needed for a hruler
2043}
2044
2045QCString Markdown::Private::extractTitleId(QCString &title, int level, bool *pIsIdGenerated)
2046{
2047 AUTO_TRACE("title={} level={}",Trace::trunc(title),level);
2048 // match e.g. '{#id-b11} ' and capture 'id-b11'
2049 static const reg::Ex r2(R"({#(\a[\w-]*)}\s*$)");
2050 reg::Match match;
2051 std::string ti = title.str();
2052 if (reg::search(ti,match,r2))
2053 {
2054 std::string id = match[1].str();
2055 title = title.left(match.position());
2056 if (AnchorGenerator::instance().reserve(id)>0)
2057 {
2058 warn(fileName, lineNr, "An automatically generated id already has the name '{}'!", id);
2059 }
2060 //printf("found match id='%s' title=%s\n",id.c_str(),qPrint(title));
2061 AUTO_TRACE_EXIT("id={}",id);
2062 return id;
2063 }
2064 if (((level>0) && (level<=Config_getInt(TOC_INCLUDE_HEADINGS))) || (Config_getEnum(MARKDOWN_ID_STYLE)==MARKDOWN_ID_STYLE_t::GITHUB))
2065 {
2067 if (pIsIdGenerated) *pIsIdGenerated=true;
2068 //printf("auto-generated id='%s' title='%s'\n",qPrint(id),qPrint(title));
2069 AUTO_TRACE_EXIT("id={}",id);
2070 return id;
2071 }
2072 //printf("no id found in title '%s'\n",qPrint(title));
2073 return "";
2074}
2075
2076
2078 QCString &header,QCString &id,bool allowAdjustLevel,bool *pIsIdGenerated)
2079{
2080 AUTO_TRACE("data='{}' header={} id={} allowAdjustLevel={}",Trace::trunc(data),Trace::trunc(header),id,allowAdjustLevel);
2081 size_t i = 0;
2082 int level = 0, blanks=0;
2083 const size_t size = data.size();
2084
2085 // find start of header text and determine heading level
2086 while (i<size && data[i]==' ') i++;
2087 if (i>=size || data[i]!='#')
2088 {
2089 return 0;
2090 }
2091 while (i<size && data[i]=='#') i++,level++;
2092 if (level>SectionType::MaxLevel) // too many #'s -> no section
2093 {
2094 return 0;
2095 }
2096 while (i<size && data[i]==' ') i++,blanks++;
2097 if (level==1 && blanks==0)
2098 {
2099 return 0; // special case to prevent #someid seen as a header (see bug 671395)
2100 }
2101
2102 // find end of header text
2103 size_t end=i;
2104 while (end<size && data[end]!='\n') end++;
2105 while (end>i && (data[end-1]=='#' || data[end-1]==' ')) end--;
2106
2107 // store result
2108 header = data.substr(i,end-i);
2109 id = extractTitleId(header, level, pIsIdGenerated);
2110 if (!id.isEmpty()) // strip #'s between title and id
2111 {
2112 int idx=static_cast<int>(header.length())-1;
2113 while (idx>=0 && (header.at(idx)=='#' || header.at(idx)==' ')) idx--;
2114 header=header.left(idx+1);
2115 }
2116
2117 if (allowAdjustLevel && level==1 && indentLevel==-1)
2118 {
2119 // in case we find a `# Section` on a markdown page that started with the same level
2120 // header, we no longer need to artificially decrease the paragraph level.
2121 // So both
2122 // -------------------
2123 // # heading 1 <-- here we set g_indentLevel to -1
2124 // # heading 2 <-- here we set g_indentLevel back to 0 such that this will be a @section
2125 // -------------------
2126 // and
2127 // -------------------
2128 // # heading 1 <-- here we set g_indentLevel to -1
2129 // ## heading 2 <-- here we keep g_indentLevel at -1 such that @subsection will be @section
2130 // -------------------
2131 // will convert to
2132 // -------------------
2133 // @page md_page Heading 1
2134 // @section autotoc_md1 Heading 2
2135 // -------------------
2136
2137 indentLevel=0;
2138 }
2139 int res = level+indentLevel;
2140 AUTO_TRACE_EXIT("result={}",res);
2141 return res;
2142}
2143
2144static bool isEmptyLine(std::string_view data)
2145{
2146 AUTO_TRACE("data='{}'",Trace::trunc(data));
2147 size_t i=0;
2148 while (i<data.size())
2149 {
2150 if (data[i]=='\n') { AUTO_TRACE_EXIT("true"); return true; }
2151 if (data[i]!=' ') { AUTO_TRACE_EXIT("false"); return false; }
2152 i++;
2153 }
2154 AUTO_TRACE_EXIT("true");
2155 return true;
2156}
2157
2158#define isLiTag(i) \
2159 (data[(i)]=='<' && \
2160 (data[(i)+1]=='l' || data[(i)+1]=='L') && \
2161 (data[(i)+2]=='i' || data[(i)+2]=='I') && \
2162 (data[(i)+3]=='>'))
2163
2164// compute the indent from the start of the input, excluding list markers
2165// such as -, -#, *, +, 1., and <li>
2166static size_t computeIndentExcludingListMarkers(std::string_view data)
2167{
2168 AUTO_TRACE("data='{}'",Trace::trunc(data));
2169 size_t i=0;
2170 const size_t size=data.size();
2171 size_t indent=0;
2172 bool isDigit=FALSE;
2173 bool isLi=FALSE;
2174 bool listMarkerSkipped=FALSE;
2175 while (i<size &&
2176 (data[i]==' ' || // space
2177 (!listMarkerSkipped && // first list marker
2178 (data[i]=='+' || data[i]=='-' || data[i]=='*' || // unordered list char
2179 (data[i]=='#' && i>0 && data[i-1]=='-') || // -# item
2180 (isDigit=(data[i]>='1' && data[i]<='9')) || // ordered list marker?
2181 (isLi=(size>=3 && i+3<size && isLiTag(i))) // <li> tag
2182 )
2183 )
2184 )
2185 )
2186 {
2187 if (isDigit) // skip over ordered list marker '10. '
2188 {
2189 size_t j=i+1;
2190 while (j<size && ((data[j]>='0' && data[j]<='9') || data[j]=='.'))
2191 {
2192 if (data[j]=='.') // should be end of the list marker
2193 {
2194 if (j+1<size && data[j+1]==' ') // valid list marker
2195 {
2196 listMarkerSkipped=TRUE;
2197 indent+=j+1-i;
2198 i=j+1;
2199 break;
2200 }
2201 else // not a list marker
2202 {
2203 break;
2204 }
2205 }
2206 j++;
2207 }
2208 }
2209 else if (isLi)
2210 {
2211 i+=3; // skip over <li>
2212 indent+=3;
2213 listMarkerSkipped=TRUE;
2214 }
2215 else if (data[i]=='-' && size>=2 && i+2<size && data[i+1]=='#' && data[i+2]==' ')
2216 { // case "-# "
2217 listMarkerSkipped=TRUE; // only a single list marker is accepted
2218 i++; // skip over #
2219 indent++;
2220 }
2221 else if (data[i]!=' ' && i+1<size && data[i+1]==' ')
2222 { // case "- " or "+ " or "* "
2223 listMarkerSkipped=TRUE; // only a single list marker is accepted
2224 }
2225 if (data[i]!=' ' && !listMarkerSkipped)
2226 { // end of indent
2227 break;
2228 }
2229 indent++,i++;
2230 }
2231 AUTO_TRACE_EXIT("result={}",indent);
2232 return indent;
2233}
2234
2235static size_t isListMarker(std::string_view data)
2236{
2237 AUTO_TRACE("data='{}'",Trace::trunc(data));
2238 size_t normalIndent = 0;
2239 while (normalIndent<data.size() && data[normalIndent]==' ') normalIndent++;
2240 size_t listIndent = computeIndentExcludingListMarkers(data);
2241 size_t result = listIndent>normalIndent ? listIndent : 0;
2242 AUTO_TRACE_EXIT("result={}",result);
2243 return result;
2244}
2245
2246static bool isEndOfList(std::string_view data)
2247{
2248 AUTO_TRACE("data='{}'",Trace::trunc(data));
2249 int dots=0;
2250 size_t i=0;
2251 // end of list marker is an otherwise empty line with a dot.
2252 while (i<data.size())
2253 {
2254 if (data[i]=='.')
2255 {
2256 dots++;
2257 }
2258 else if (data[i]=='\n')
2259 {
2260 break;
2261 }
2262 else if (data[i]!=' ' && data[i]!='\t') // bail out if the line is not empty
2263 {
2264 AUTO_TRACE_EXIT("result=false");
2265 return false;
2266 }
2267 i++;
2268 }
2269 AUTO_TRACE_EXIT("result={}",dots==1);
2270 return dots==1;
2271}
2272
2273static bool isFencedCodeBlock(std::string_view data,size_t refIndent,
2274 QCString &lang,size_t &start,size_t &end,size_t &offset)
2275{
2276 AUTO_TRACE("data='{}' refIndent={}",Trace::trunc(data),refIndent);
2277 const char dot = '.';
2278 auto isAlphaChar = [ ](char c) { return (c>='A' && c<='Z') || (c>='a' && c<='z'); };
2279 auto isAlphaNChar = [ ](char c) { return (c>='A' && c<='Z') || (c>='a' && c<='z') || (c>='0' && c<='9') || (c=='+'); };
2280 auto isLangChar = [&](char c) { return c==dot || isAlphaChar(c); };
2281 // rules: at least 3 ~~~, end of the block same amount of ~~~'s, otherwise
2282 // return FALSE
2283 size_t i=0;
2284 size_t indent=0;
2285 int startTildes=0;
2286 const size_t size = data.size();
2287 while (i<size && data[i]==' ') indent++,i++;
2288 if (indent>=refIndent+4)
2289 {
2290 AUTO_TRACE_EXIT("result=false: content is part of code block indent={} refIndent={}",indent,refIndent);
2291 return FALSE;
2292 } // part of code block
2293 char tildaChar='~';
2294 if (i<size && data[i]=='`') tildaChar='`';
2295 while (i<size && data[i]==tildaChar) startTildes++,i++;
2296 if (startTildes<3)
2297 {
2298 AUTO_TRACE_EXIT("result=false: no fence marker found #tildes={}",startTildes);
2299 return FALSE;
2300 } // not enough tildes
2301 if (i<size && data[i]=='{') // extract .py from ```{.py} ... ```
2302 {
2303 i++; // skip over {
2304 if (data[i] == dot) i++; // skip over initial dot
2305 size_t startLang=i;
2306 while (i<size && (data[i]!='\n' && data[i]!='}')) i++; // find matching }
2307 if (i<size && data[i]=='}')
2308 {
2309 lang = data.substr(startLang,i-startLang);
2310 i++;
2311 }
2312 else // missing closing bracket, treat `{` as part of the content
2313 {
2314 i=startLang-1;
2315 lang="";
2316 }
2317 }
2318 else if (i<size && isLangChar(data[i])) /// extract python or .py from ```python...``` or ```.py...```
2319 {
2320 if (data[i] == dot) i++; // skip over initial dot
2321 size_t startLang=i;
2322 if (i<size && isAlphaChar(data[i])) //check first character of language specifier
2323 {
2324 i++;
2325 while (i<size && isAlphaNChar(data[i])) i++; // find end of language specifier
2326 }
2327 lang = data.substr(startLang,i-startLang);
2328 }
2329 else // no language specified
2330 {
2331 lang="";
2332 }
2333
2334 start=i;
2335 while (i<size)
2336 {
2337 if (data[i]==tildaChar)
2338 {
2339 end=i;
2340 int endTildes=0;
2341 while (i<size && data[i]==tildaChar) endTildes++,i++;
2342 while (i<size && data[i]==' ') i++;
2343 {
2344 if (endTildes==startTildes)
2345 {
2346 offset=i;
2347 AUTO_TRACE_EXIT("result=true: found end marker at offset {} lang='{}'",offset,lang);
2348 return true;
2349 }
2350 }
2351 }
2352 i++;
2353 }
2354 AUTO_TRACE_EXIT("result=false: no end marker found lang={}'",lang);
2355 return false;
2356}
2357
2358static bool isCodeBlock(std::string_view data, size_t offset,size_t &indent)
2359{
2360 AUTO_TRACE("data='{}' offset={}",Trace::trunc(data),offset);
2361 //printf("<isCodeBlock(offset=%d,size=%d,indent=%d)\n",offset,size,indent);
2362 // determine the indent of this line
2363 size_t i=0;
2364 size_t indent0=0;
2365 const size_t size = data.size();
2366 while (i<size && data[i]==' ') indent0++,i++;
2367
2368 if (indent0<codeBlockIndent)
2369 {
2370 AUTO_TRACE_EXIT("result={}: line is not indented enough {}<4",false,indent0);
2371 return false;
2372 }
2373 if (indent0>=size || data[indent0]=='\n') // empty line does not start a code block
2374 {
2375 AUTO_TRACE_EXIT("result={}: only spaces at the end of a comment block",false);
2376 return false;
2377 }
2378
2379 i=offset;
2380 int nl=0;
2381 int nl_pos[3];
2382 int offset_i = static_cast<int>(offset);
2383 // search back 3 lines and remember the start of lines -1 and -2
2384 while (i>0 && nl<3) // i counts down from offset to 1
2385 {
2386 int j = static_cast<int>(i)-offset_i-1; // j counts from -1 to -offset
2387 // since j can be negative we need to rewrap data in a std::string_view
2388 size_t nl_size = isNewline(std::string_view(data.data()+j,data.size()-j));
2389 if (nl_size>0)
2390 {
2391 nl_pos[nl++]=j+static_cast<int>(nl_size);
2392 }
2393 i--;
2394 }
2395
2396 // if there are only 2 preceding lines, then line -2 starts at -offset
2397 if (i==0 && nl==2) nl_pos[nl++]=-offset_i;
2398
2399 if (nl==3) // we have at least 2 preceding lines
2400 {
2401 //printf(" positions: nl_pos=[%d,%d,%d] line[-2]='%s' line[-1]='%s'\n",
2402 // nl_pos[0],nl_pos[1],nl_pos[2],
2403 // qPrint(QCString(data+nl_pos[1]).left(nl_pos[0]-nl_pos[1]-1)),
2404 // qPrint(QCString(data+nl_pos[2]).left(nl_pos[1]-nl_pos[2]-1)));
2405
2406 // check that line -1 is empty
2407 // Note that the offset is negative so we need to rewrap the string view
2408 if (!isEmptyLine(std::string_view(data.data()+nl_pos[1],nl_pos[0]-nl_pos[1]-1)))
2409 {
2410 AUTO_TRACE_EXIT("result={}",FALSE);
2411 return FALSE;
2412 }
2413
2414 // determine the indent of line -2
2415 // Note that the offset is negative so we need to rewrap the string view
2416 indent=std::max(indent,computeIndentExcludingListMarkers(
2417 std::string_view(data.data()+nl_pos[2],nl_pos[1]-nl_pos[2])));
2418
2419 //printf(">isCodeBlock local_indent %d>=%d+%d=%d\n",
2420 // indent0,indent,codeBlockIndent,indent0>=indent+codeBlockIndent);
2421 // if the difference is >4 spaces -> code block
2422 bool res = indent0>=indent+codeBlockIndent;
2423 AUTO_TRACE_EXIT("result={}: code block if indent difference >4 spaces",res);
2424 return res;
2425 }
2426 else // not enough lines to determine the relative indent, use global indent
2427 {
2428 // check that line -1 is empty
2429 // Note that the offset is negative so we need to rewrap the string view
2430 if (nl==1 && !isEmptyLine(std::string_view(data.data()-offset,offset-1)))
2431 {
2432 AUTO_TRACE_EXIT("result=false");
2433 return FALSE;
2434 }
2435 //printf(">isCodeBlock global indent %d>=%d+4=%d nl=%d\n",
2436 // indent0,indent,indent0>=indent+4,nl);
2437 bool res = indent0>=indent+codeBlockIndent;
2438 AUTO_TRACE_EXIT("result={}: code block if indent difference >4 spaces",res);
2439 return res;
2440 }
2441}
2442
2443/** Finds the location of the table's contains in the string \a data.
2444 * Only one line will be inspected.
2445 * @param[in] data pointer to the string buffer.
2446 * @param[out] start offset of the first character of the table content
2447 * @param[out] end offset of the last character of the table content
2448 * @param[out] columns number of table columns found
2449 * @returns The offset until the next line in the buffer.
2450 */
2451static size_t findTableColumns(std::string_view data,size_t &start,size_t &end,size_t &columns)
2452{
2453 AUTO_TRACE("data='{}'",Trace::trunc(data));
2454 const size_t size = data.size();
2455 size_t i=0,n=0;
2456 // find start character of the table line
2457 while (i<size && data[i]==' ') i++;
2458 if (i<size && data[i]=='|' && data[i]!='\n') i++,n++; // leading | does not count
2459 start = i;
2460
2461 // find end character of the table line
2462 size_t j = 0;
2463 while (i<size && (j = isNewline(data.substr(i)))==0) i++;
2464 size_t eol=i+j;
2465
2466 if (j>0 && i>0) i--; // move i to point before newline
2467 while (i>0 && data[i]==' ') i--;
2468 if (i>0 && data[i-1]!='\\' && data[i]=='|') i--,n++; // trailing or escaped | does not count
2469 end = i;
2470
2471 // count columns between start and end
2472 columns=0;
2473 if (end>start)
2474 {
2475 i=start;
2476 while (i<=end) // look for more column markers
2477 {
2478 if (data[i]=='|' && (i==0 || data[i-1]!='\\')) columns++;
2479 if (columns==1) columns++; // first | make a non-table into a two column table
2480 i++;
2481 }
2482 }
2483 if (n==2 && columns==0) // table row has | ... |
2484 {
2485 columns++;
2486 }
2487 AUTO_TRACE_EXIT("eol={} start={} end={} columns={}",eol,start,end,columns);
2488 return eol;
2489}
2490
2491/** Returns TRUE iff data points to the start of a table block */
2492static bool isTableBlock(std::string_view data)
2493{
2494 AUTO_TRACE("data='{}'",Trace::trunc(data));
2495 size_t cc0=0, start=0, end=0;
2496
2497 // the first line should have at least two columns separated by '|'
2498 size_t i = findTableColumns(data,start,end,cc0);
2499 if (i>=data.size() || cc0<1)
2500 {
2501 AUTO_TRACE_EXIT("result=false: no |'s in the header");
2502 return FALSE;
2503 }
2504
2505 size_t cc1 = 0;
2506 size_t ret = findTableColumns(data.substr(i),start,end,cc1);
2507 size_t j=i+start;
2508 // separator line should consist of |, - and : and spaces only
2509 while (j<=end+i)
2510 {
2511 if (data[j]!=':' && data[j]!='-' && data[j]!='|' && data[j]!=' ')
2512 {
2513 AUTO_TRACE_EXIT("result=false: invalid character '{}'",data[j]);
2514 return FALSE; // invalid characters in table separator
2515 }
2516 j++;
2517 }
2518 if (cc1!=cc0) // number of columns should be same as previous line
2519 {
2520 AUTO_TRACE_EXIT("result=false: different number of columns as previous line {}!={}",cc1,cc0);
2521 return FALSE;
2522 }
2523
2524 i+=ret; // goto next line
2525 size_t cc2 = 0;
2526 findTableColumns(data.substr(i),start,end,cc2);
2527
2528 AUTO_TRACE_EXIT("result={}",cc1==cc2);
2529 return cc1==cc2;
2530}
2531
2533{
2534 AUTO_TRACE("data='{}'",Trace::trunc(data));
2535 const size_t size = data.size();
2536
2537 size_t columns=0, start=0, end=0;
2538 size_t i = findTableColumns(data,start,end,columns);
2539 size_t headerStart = start;
2540 size_t headerEnd = end;
2541
2542 // read cell alignments
2543 size_t cc = 0;
2544 size_t ret = findTableColumns(data.substr(i),start,end,cc);
2545 size_t k=0;
2546 std::vector<int> columnAlignment(columns);
2547
2548 bool leftMarker=false, rightMarker=false, startFound=false;
2549 size_t j=start+i;
2550 while (j<=end+i)
2551 {
2552 if (!startFound)
2553 {
2554 if (data[j]==':') { leftMarker=TRUE; startFound=TRUE; }
2555 if (data[j]=='-') startFound=TRUE;
2556 //printf(" data[%d]=%c startFound=%d\n",j,data[j],startFound);
2557 }
2558 if (data[j]=='-') rightMarker=FALSE;
2559 else if (data[j]==':') rightMarker=TRUE;
2560 if (j<=end+i && (data[j]=='|' && (j==0 || data[j-1]!='\\')))
2561 {
2562 if (k<columns)
2563 {
2564 columnAlignment[k] = markersToAlignment(leftMarker,rightMarker);
2565 //printf("column[%d] alignment=%d\n",k,columnAlignment[k]);
2566 leftMarker=FALSE;
2567 rightMarker=FALSE;
2568 startFound=FALSE;
2569 }
2570 k++;
2571 }
2572 j++;
2573 }
2574 if (k<columns)
2575 {
2576 columnAlignment[k] = markersToAlignment(leftMarker,rightMarker);
2577 //printf("column[%d] alignment=%d\n",k,columnAlignment[k]);
2578 }
2579 // proceed to next line
2580 i+=ret;
2581
2582 // Store the table cell information by row then column. This
2583 // allows us to handle row spanning.
2584 std::vector<std::vector<TableCell> > tableContents;
2585
2586 size_t m = headerStart;
2587 std::vector<TableCell> headerContents(columns);
2588 for (k=0;k<columns;k++)
2589 {
2590 while (m<=headerEnd && (data[m]!='|' || (m>0 && data[m-1]=='\\')))
2591 {
2592 headerContents[k].cellText += data[m++];
2593 }
2594 m++;
2595 // do the column span test before stripping white space
2596 // || is spanning columns, | | is not
2597 headerContents[k].colSpan = headerContents[k].cellText.isEmpty();
2598 headerContents[k].cellText = headerContents[k].cellText.stripWhiteSpace();
2599 }
2600 tableContents.push_back(headerContents);
2601
2602 // write table cells
2603 while (i<size)
2604 {
2605 ret = findTableColumns(data.substr(i),start,end,cc);
2606 if (cc!=columns) break; // end of table
2607
2608 j=start+i;
2609 k=0;
2610 std::vector<TableCell> rowContents(columns);
2611 while (j<=end+i)
2612 {
2613 if (j<=end+i && (data[j]=='|' && (j==0 || data[j-1]!='\\')))
2614 {
2615 // do the column span test before stripping white space
2616 // || is spanning columns, | | is not
2617 rowContents[k].colSpan = rowContents[k].cellText.isEmpty();
2618 rowContents[k].cellText = rowContents[k].cellText.stripWhiteSpace();
2619 k++;
2620 } // if (j<=end+i && (data[j]=='|' && (j==0 || data[j-1]!='\\')))
2621 else
2622 {
2623 rowContents[k].cellText += data[j];
2624 } // else { if (j<=end+i && (data[j]=='|' && (j==0 || data[j-1]!='\\'))) }
2625 j++;
2626 } // while (j<=end+i)
2627 // do the column span test before stripping white space
2628 // || is spanning columns, | | is not
2629 rowContents[k].colSpan = rowContents[k].cellText.isEmpty();
2630 rowContents[k].cellText = rowContents[k].cellText.stripWhiteSpace();
2631 tableContents.push_back(rowContents);
2632
2633 // proceed to next line
2634 i+=ret;
2635 }
2636
2637 out+="<table class=\"markdownTable\">";
2638 QCString cellTag("th"), cellClass("class=\"markdownTableHead");
2639 for (size_t row = 0; row < tableContents.size(); row++)
2640 {
2641 if (row)
2642 {
2643 if (row % 2)
2644 {
2645 out+="\n<tr class=\"markdownTableRowOdd\">";
2646 }
2647 else
2648 {
2649 out+="\n<tr class=\"markdownTableRowEven\">";
2650 }
2651 }
2652 else
2653 {
2654 out+="\n <tr class=\"markdownTableHead\">";
2655 }
2656 for (size_t c = 0; c < columns; c++)
2657 {
2658 // save the cell text for use after column span computation
2659 QCString cellText(tableContents[row][c].cellText);
2660
2661 // Row span handling. Spanning rows will contain a caret ('^').
2662 // If the current cell contains just a caret, this is part of an
2663 // earlier row's span and the cell should not be added to the
2664 // output.
2665 if (tableContents[row][c].cellText == "^")
2666 {
2667 continue;
2668 }
2669 if (tableContents[row][c].colSpan)
2670 {
2671 int cr = static_cast<int>(c);
2672 while ( cr >= 0 && tableContents[row][cr].colSpan)
2673 {
2674 cr--;
2675 };
2676 if (cr >= 0 && tableContents[row][cr].cellText == "^") continue;
2677 }
2678 size_t rowSpan = 1, spanRow = row+1;
2679 while ((spanRow < tableContents.size()) &&
2680 (tableContents[spanRow][c].cellText == "^"))
2681 {
2682 spanRow++;
2683 rowSpan++;
2684 }
2685
2686 out+=" <" + cellTag + " " + cellClass;
2687 // use appropriate alignment style
2688 switch (columnAlignment[c])
2689 {
2690 case AlignLeft: out+="Left\""; break;
2691 case AlignRight: out+="Right\""; break;
2692 case AlignCenter: out+="Center\""; break;
2693 case AlignNone: out+="None\""; break;
2694 }
2695
2696 if (rowSpan > 1)
2697 {
2698 QCString spanStr;
2699 spanStr.setNum(rowSpan);
2700 out+=" rowspan=\"" + spanStr + "\"";
2701 }
2702 // Column span handling, assumes that column spans will have
2703 // empty strings, which would indicate the sequence "||", used
2704 // to signify spanning columns.
2705 size_t colSpan = 1;
2706 while ((c+1 < columns) && tableContents[row][c+1].colSpan)
2707 {
2708 c++;
2709 colSpan++;
2710 }
2711 if (colSpan > 1)
2712 {
2713 QCString spanStr;
2714 spanStr.setNum(colSpan);
2715 out+=" colspan=\"" + spanStr + "\"";
2716 }
2717 // need at least one space on either side of the cell text in
2718 // order for doxygen to do other formatting
2719 out+="> " + cellText + " \\ilinebr </" + cellTag + ">";
2720 }
2721 cellTag = "td";
2722 cellClass = "class=\"markdownTableBody";
2723 out+=" </tr>";
2724 }
2725 out+="</table>\n";
2726
2727 AUTO_TRACE_EXIT("i={}",i);
2728 return i;
2729}
2730
2731
2732static bool hasLineBreak(std::string_view data)
2733{
2734 AUTO_TRACE("data='{}'",Trace::trunc(data));
2735 size_t i=0;
2736 size_t j=0;
2737 // search for end of line and also check if it is not a completely blank
2738 while (i<data.size() && data[i]!='\n')
2739 {
2740 if (data[i]!=' ' && data[i]!='\t') j++; // some non whitespace
2741 i++;
2742 }
2743 if (i>=data.size()) { return 0; } // empty line
2744 if (i<2) { return 0; } // not long enough
2745 bool res = (j>0 && data[i-1]==' ' && data[i-2]==' '); // non blank line with at two spaces at the end
2746 AUTO_TRACE_EXIT("result={}",res);
2747 return res;
2748}
2749
2750
2752{
2753 AUTO_TRACE("data='{}'",Trace::trunc(data));
2754 int level=0;
2755 QCString header;
2756 QCString id;
2757 if (isHRuler(data))
2758 {
2759 out+="<hr>\n";
2760 }
2761 else if ((level=isAtxHeader(data,header,id,TRUE)))
2762 {
2763 QCString hTag;
2764 if (!id.isEmpty())
2765 {
2766 switch (level)
2767 {
2768 case SectionType::Section: out+="@section "; break;
2769 case SectionType::Subsection: out+="@subsection "; break;
2770 case SectionType::Subsubsection: out+="@subsubsection "; break;
2771 case SectionType::Paragraph: out+="@paragraph "; break;
2772 case SectionType::Subparagraph: out+="@subparagraph "; break;
2773 case SectionType::Subsubparagraph: out+="@subsubparagraph "; break;
2774 }
2775 out+=id;
2776 out+=" ";
2777 out+=header;
2778 out+="\n";
2779 }
2780 else
2781 {
2782 hTag.sprintf("h%d",level);
2783 out+="<"+hTag+">";
2784 out+=header;
2785 out+="</"+hTag+">\n";
2786 }
2787 }
2788 else if (data.size()>0) // nothing interesting -> just output the line
2789 {
2790 size_t tmpSize = data.size();
2791 if (data[data.size()-1] == '\n') tmpSize--;
2792 out+=data.substr(0,tmpSize);
2793
2794 if (hasLineBreak(data))
2795 {
2796 out+="\\ilinebr<br>";
2797 }
2798 if (tmpSize != data.size()) out+='\n';
2799 }
2800}
2801
2802static const std::unordered_map<std::string,std::string> g_quotationHeaderMap = {
2803 // GitHub style Doxygen command
2804 { "[!note]", "\\note" },
2805 { "[!warning]", "\\warning" },
2806 { "[!tip]", "\\remark" },
2807 { "[!caution]", "\\attention" },
2808 { "[!important]", "\\important" }
2809};
2810
2812{
2813 AUTO_TRACE("data='{}'",Trace::trunc(data));
2814 size_t i=0;
2815 int curLevel=0;
2816 size_t end=0;
2817 const size_t size = data.size();
2818 std::string startCmd;
2819 int isGitHubAlert = false;
2820 int isGitHubFirst = false;
2821 while (i<size)
2822 {
2823 // find end of this line
2824 end=i+1;
2825 while (end<=size && data[end-1]!='\n') end++;
2826 size_t j=i;
2827 int level=0;
2828 size_t indent=i;
2829 // compute the quoting level
2830 while (j<end && (data[j]==' ' || data[j]=='>'))
2831 {
2832 if (data[j]=='>') { level++; indent=j+1; }
2833 else if (j>0 && data[j-1]=='>') indent=j+1;
2834 j++;
2835 }
2836 if (indent>0 && j>0 && data[j-1]=='>' &&
2837 !(j==size || data[j]=='\n')) // disqualify last > if not followed by space
2838 {
2839 indent--;
2840 level--;
2841 j--;
2842 }
2843 AUTO_TRACE_ADD("indent={} i={} j={} end={} level={} line={}",indent,i,j,end,level,Trace::trunc(&data[i]));
2844 if (level==0 && j<end-1 && !isListMarker(data.substr(j)) && !isHRuler(data.substr(j)))
2845 {
2846 level = curLevel; // lazy
2847 }
2848 if (level==1)
2849 {
2850 QCString txt = stripWhiteSpace(data.substr(indent,end-indent));
2851 auto it = g_quotationHeaderMap.find(txt.lower().str()); // TODO: in C++20 the std::string can be dropped
2852 if (it != g_quotationHeaderMap.end())
2853 {
2854 isGitHubAlert = true;
2855 isGitHubFirst = true;
2856 startCmd = it->second;
2857 }
2858 }
2859 if (level>curLevel) // quote level increased => add start markers
2860 {
2861 if (level!=1 || !isGitHubAlert) // normal block quote
2862 {
2863 for (int l=curLevel;l<level-1;l++)
2864 {
2865 out+="<blockquote>";
2866 }
2867 out += "<blockquote>&zwj;"; // empty blockquotes are also shown
2868 }
2869 else if (!startCmd.empty()) // GitHub style alert
2870 {
2871 out += startCmd + " ";
2872 }
2873 }
2874 else if (level<curLevel) // quote level decreased => add end markers
2875 {
2876 int decrLevel = curLevel;
2877 if (level==0 && isGitHubAlert)
2878 {
2879 decrLevel--;
2880 }
2881 for (int l=level;l<decrLevel;l++)
2882 {
2883 out += "</blockquote>\\ilinebr ";
2884 }
2885 }
2886 if (level==0)
2887 {
2888 curLevel=0;
2889 break; // end of quote block
2890 }
2891 // copy line without quotation marks
2892 if (curLevel!=0 || !isGitHubAlert)
2893 {
2894 std::string_view txt = data.substr(indent,end-indent);
2895 if (stripWhiteSpace(txt).empty() && !startCmd.empty())
2896 {
2897 if (!isGitHubFirst) out += "<br>";
2898 out += "<br>\n";
2899 }
2900 else
2901 {
2902 out += txt;
2903 }
2904 isGitHubFirst = false;
2905 }
2906 else // GitHub alert section
2907 {
2908 out+= "\n";
2909 }
2910 curLevel=level;
2911 // proceed with next line
2912 i=end;
2913 }
2914 // end of comment within blockquote => add end markers
2915 if (isGitHubAlert) // GitHub alert doesn't have a blockquote
2916 {
2917 curLevel--;
2918 }
2919 for (int l=0;l<curLevel;l++)
2920 {
2921 out+="</blockquote>";
2922 }
2923 AUTO_TRACE_EXIT("i={}",i);
2924 return i;
2925}
2926
2927// For code blocks that are outputted as part of an indented include or snippet command, we need to filter out
2928// the location string, i.e. '\ifile "..." \iline \ilinebr'.
2929bool skipOverFileAndLineCommands(std::string_view data,size_t indent,size_t &offset,std::string &location)
2930{
2931 size_t i = offset;
2932 size_t size = data.size();
2933 while (i<data.size() && data[i]==' ') i++;
2934 if (literal_at(data.substr(i),"\\ifile \""))
2935 {
2936 size_t locStart = i;
2937 if (i>offset) locStart--; // include the space before \ifile
2938 i+=8;
2939 bool found=false;
2940 while (i+9<size && data[i]!='\n')
2941 {
2942 if (literal_at(data.substr(i),"\\ilinebr "))
2943 {
2944 found=true;
2945 break;
2946 }
2947 i++;
2948 }
2949 if (found)
2950 {
2951 i+=9;
2952 location=data.substr(locStart,i-locStart);
2953 location+='\n';
2954 while (indent>0 && i<size && data[i]==' ') i++,indent--;
2955 if (i<size && data[i]=='\n') i++;
2956 offset = i;
2957 return true;
2958 }
2959 }
2960 return false;
2961}
2962
2963size_t Markdown::Private::writeCodeBlock(std::string_view data,size_t refIndent)
2964{
2965 AUTO_TRACE("data='{}' refIndent={}",Trace::trunc(data),refIndent);
2966 const size_t size = data.size();
2967 size_t i=0;
2968 // no need for \ilinebr here as the previous line was empty and was skipped
2969 out+="@iverbatim\n";
2970 int emptyLines=0;
2971 std::string location;
2972 while (i<size)
2973 {
2974 // find end of this line
2975 size_t end=i+1;
2976 while (end<=size && data[end-1]!='\n') end++;
2977 size_t j=i;
2978 size_t indent=0;
2979 while (j<end && data[j]==' ') j++,indent++;
2980 //printf("j=%d end=%d indent=%d refIndent=%d tabSize=%d data={%s}\n",
2981 // j,end,indent,refIndent,Config_getInt(TAB_SIZE),qPrint(QCString(data+i).left(end-i-1)));
2982 if (j==end-1) // empty line
2983 {
2984 emptyLines++;
2985 i=end;
2986 }
2987 else if (indent>=refIndent+codeBlockIndent) // enough indent to continue the code block
2988 {
2989 while (emptyLines>0) // write skipped empty lines
2990 {
2991 // add empty line
2992 out+="\n";
2993 emptyLines--;
2994 }
2995 // add code line minus the indent
2996 size_t offset = i+refIndent+codeBlockIndent;
2997 std::string lineLoc;
2999 {
3000 location = lineLoc;
3001 }
3002 out+=data.substr(offset,end-offset);
3003 i=end;
3004 }
3005 else // end of code block
3006 {
3007 break;
3008 }
3009 }
3010 out+="@endiverbatim";
3011 if (!location.empty())
3012 {
3013 out+=location;
3014 }
3015 else
3016 {
3017 out+="\\ilinebr ";
3018 }
3019 while (emptyLines>0) // write skipped empty lines
3020 {
3021 // add empty line
3022 out+="\n";
3023 emptyLines--;
3024 }
3025 AUTO_TRACE_EXIT("i={}",i);
3026 return i;
3027}
3028
3029// start searching for the end of the line start at offset \a i
3030// keeping track of possible blocks that need to be skipped.
3031size_t Markdown::Private::findEndOfLine(std::string_view data,size_t offset)
3032{
3033 AUTO_TRACE("data='{}'",Trace::trunc(data));
3034 // find end of the line
3035 const size_t size = data.size();
3036 size_t nb=0, end=offset+1, j=0;
3037 while (end<=size && (j=isNewline(data.substr(end-1)))==0)
3038 {
3039 // while looking for the end of the line we might encounter a block
3040 // that needs to be passed unprocessed.
3041 if ((data[end-1]=='\\' || data[end-1]=='@') && // command
3042 (end<=1 || (data[end-2]!='\\' && data[end-2]!='@')) // not escaped
3043 )
3044 {
3045 QCString endBlockName = isBlockCommand(data.substr(end-1),end-1);
3046 end++;
3047 if (!endBlockName.isEmpty())
3048 {
3049 size_t l = endBlockName.length();
3050 for (;end+l+1<size;end++) // search for end of block marker
3051 {
3052 if ((data[end]=='\\' || data[end]=='@') &&
3053 data[end-1]!='\\' && data[end-1]!='@'
3054 )
3055 {
3056 if (qstrncmp(&data[end+1],endBlockName.data(),l)==0)
3057 {
3058 // found end marker, skip over this block
3059 //printf("feol.block out={%s}\n",qPrint(QCString(data+i).left(end+l+1-i)));
3060 end = end + l + 2;
3061 break;
3062 }
3063 }
3064 }
3065 }
3066 }
3067 else if (nb==0 && data[end-1]=='<' && size>=6 && end+6<size &&
3068 (end<=1 || (data[end-2]!='\\' && data[end-2]!='@'))
3069 )
3070 {
3071 if (tolower(data[end])=='p' && tolower(data[end+1])=='r' &&
3072 tolower(data[end+2])=='e' && (data[end+3]=='>' || data[end+3]==' ')) // <pre> tag
3073 {
3074 // skip part until including </pre>
3075 end = end + processHtmlTagWrite(data.substr(end-1),end-1,false);
3076 break;
3077 }
3078 else
3079 {
3080 end++;
3081 }
3082 }
3083 else if (nb==0 && data[end-1]=='`')
3084 {
3085 while (end<=size && data[end-1]=='`') end++,nb++;
3086 }
3087 else if (nb>0 && data[end-1]=='`')
3088 {
3089 size_t enb=0;
3090 while (end<=size && data[end-1]=='`') end++,enb++;
3091 if (enb==nb) nb=0;
3092 }
3093 else
3094 {
3095 end++;
3096 }
3097 }
3098 if (j>0) end+=j-1;
3099 AUTO_TRACE_EXIT("offset={} end={}",offset,end);
3100 return end;
3101}
3102
3103void Markdown::Private::writeFencedCodeBlock(std::string_view data,std::string_view lang,
3104 size_t blockStart,size_t blockEnd)
3105{
3106 AUTO_TRACE("data='{}' lang={} blockStart={} blockEnd={}",Trace::trunc(data),lang,blockStart,blockEnd);
3107 if (!lang.empty() && lang[0]=='.') lang=lang.substr(1);
3108 const size_t size=data.size();
3109 size_t i=0;
3110 while (i<size && (data[i]==' ' || data[i]=='\t'))
3111 {
3112 out+=data[i++];
3113 blockStart--;
3114 blockEnd--;
3115 }
3116 out+="@icode";
3117 if (!lang.empty())
3118 {
3119 out+="{"+lang+"}";
3120 }
3121 out+=" ";
3122 addStrEscapeUtf8Nbsp(data.substr(blockStart+i,blockEnd-blockStart));
3123 out+="@endicode ";
3124}
3125
3126QCString Markdown::Private::processQuotations(std::string_view data,size_t refIndent)
3127{
3128 AUTO_TRACE("data='{}' refIndex='{}'",Trace::trunc(data),refIndent);
3129 out.clear();
3130 size_t i=0,end=0;
3131 size_t pi=std::string::npos;
3132 bool newBlock = false;
3133 bool insideList = false;
3134 size_t currentIndent = refIndent;
3135 size_t listIndent = refIndent;
3136 const size_t size = data.size();
3137 QCString lang;
3138 while (i<size)
3139 {
3141 // line is now found at [i..end)
3142
3143 size_t lineIndent=0;
3144 while (lineIndent<end && data[i+lineIndent]==' ') lineIndent++;
3145 //printf("** lineIndent=%d line=(%s)\n",lineIndent,qPrint(QCString(data+i).left(end-i)));
3146
3147 if (newBlock)
3148 {
3149 //printf("** end of block\n");
3150 if (insideList && lineIndent<currentIndent) // end of list
3151 {
3152 //printf("** end of list\n");
3153 currentIndent = refIndent;
3154 insideList = false;
3155 }
3156 newBlock = false;
3157 }
3158
3159 if ((listIndent=isListMarker(data.substr(i,end-i)))) // see if we need to increase the indent level
3160 {
3161 if (listIndent<currentIndent+4)
3162 {
3163 //printf("** start of list\n");
3164 insideList = true;
3165 currentIndent = listIndent;
3166 }
3167 }
3168 else if (isEndOfList(data.substr(i,end-i)))
3169 {
3170 //printf("** end of list\n");
3171 insideList = false;
3172 currentIndent = listIndent;
3173 }
3174 else if (isEmptyLine(data.substr(i,end-i)))
3175 {
3176 //printf("** new block\n");
3177 newBlock = true;
3178 }
3179 //printf("currentIndent=%d listIndent=%d refIndent=%d\n",currentIndent,listIndent,refIndent);
3180
3181 if (pi!=std::string::npos)
3182 {
3183 size_t blockStart=0, blockEnd=0, blockOffset=0;
3184 if (isFencedCodeBlock(data.substr(pi),currentIndent,lang,blockStart,blockEnd,blockOffset))
3185 {
3186 auto addSpecialCommand = [&](const QCString &startCmd,const QCString &endCmd)
3187 {
3188 size_t cmdPos = pi+blockStart+1;
3189 QCString pl = data.substr(cmdPos,blockEnd-blockStart-1);
3190 size_t ii = 0;
3191 int nl = 1;
3192 // check for absence of start command, either @start<cmd>, or \\start<cmd>
3193 while (ii<pl.length() && qisspace(pl[ii]))
3194 {
3195 if (pl[ii]=='\n') nl++;
3196 ii++; // skip leading whitespace
3197 }
3198 bool addNewLines = false;
3199 if (ii+startCmd.length()>=pl.length() || // no room for start command
3200 (pl[ii]!='\\' && pl[ii]!='@') || // no @ or \ after whitespace
3201 qstrncmp(pl.data()+ii+1,startCmd.data(),startCmd.length())!=0) // no start command
3202 {
3203 // input: output:
3204 // ----------------------------------------------------
3205 // ```{plantuml} => @startuml
3206 // A->B A->B
3207 // ``` @enduml
3208 // ----------------------------------------------------
3209 pl = "@"+startCmd+"\n" + pl + "@"+endCmd;
3210 addNewLines = false;
3211 }
3212 else // we have a @start... command inside the code block
3213 {
3214 // input: output:
3215 // ----------------------------------------------------
3216 // ```{plantuml} \n
3217 // \n
3218 // @startuml => @startuml
3219 // A->B A->B
3220 // @enduml @enduml
3221 // ``` \n
3222 // ----------------------------------------------------
3223 addNewLines = true;
3224 }
3225 if (addNewLines) for (int j=0;j<nl;j++) out+='\n';
3226 processSpecialCommand(pl.view().substr(ii),ii);
3227 if (addNewLines) out+='\n';
3228 };
3229
3230 if (!Config_getString(PLANTUML_JAR_PATH).isEmpty() && lang=="plantuml")
3231 {
3232 addSpecialCommand("startuml","enduml");
3233 }
3234 else if (Config_getBool(HAVE_DOT) && lang=="dot")
3235 {
3236 addSpecialCommand("dot","enddot");
3237 }
3238 else if (lang=="msc") // msc is built-in
3239 {
3240 addSpecialCommand("msc","endmsc");
3241 }
3242 else // normal code block
3243 {
3244 writeFencedCodeBlock(data.substr(pi),lang.view(),blockStart,blockEnd);
3245 }
3246 i=pi+blockOffset;
3247 pi=std::string::npos;
3248 end=i+1;
3249 continue;
3250 }
3251 else if (isBlockQuote(data.substr(pi,i-pi),currentIndent))
3252 {
3253 i = pi+writeBlockQuote(data.substr(pi));
3254 pi=std::string::npos;
3255 end=i+1;
3256 continue;
3257 }
3258 else
3259 {
3260 //printf("quote out={%s}\n",QCString(data+pi).left(i-pi).data());
3261 out+=data.substr(pi,i-pi);
3262 }
3263 }
3264 pi=i;
3265 i=end;
3266 }
3267 if (pi!=std::string::npos && pi<size) // deal with the last line
3268 {
3269 if (isBlockQuote(data.substr(pi),currentIndent))
3270 {
3271 writeBlockQuote(data.substr(pi));
3272 }
3273 else
3274 {
3275 out+=data.substr(pi);
3276 }
3277 }
3278
3279 //printf("Process quotations\n---- input ----\n%s\n---- output ----\n%s\n------------\n",
3280 // qPrint(s),prv->out.get());
3281
3282 return out;
3283}
3284
3285QCString Markdown::Private::processBlocks(std::string_view data,const size_t indent)
3286{
3287 AUTO_TRACE("data='{}' indent={}",Trace::trunc(data),indent);
3288 out.clear();
3289 size_t pi = std::string::npos;
3290 QCString id,link,title;
3291
3292#if 0 // commented out, since starting with a comment block is probably a usage error
3293 // see also http://stackoverflow.com/q/20478611/784672
3294
3295 // special case when the documentation starts with a code block
3296 // since the first line is skipped when looking for a code block later on.
3297 if (end>codeBlockIndent && isCodeBlock(data,0,end,blockIndent))
3298 {
3299 i=writeCodeBlock(out,data,size,blockIndent);
3300 end=i+1;
3301 pi=-1;
3302 }
3303#endif
3304
3305 size_t currentIndent = indent;
3306 size_t listIndent = indent;
3307 bool insideList = false;
3308 bool newBlock = false;
3309 // process each line
3310 size_t i=0;
3311 while (i<data.size())
3312 {
3313 size_t end = findEndOfLine(data,i);
3314 // line is now found at [i..end)
3315
3316 size_t lineIndent=0;
3317 int level = 0;
3318 while (lineIndent<end && data[i+lineIndent]==' ') lineIndent++;
3319 //printf("** lineIndent=%d line=(%s)\n",lineIndent,qPrint(QCString(data+i).left(end-i)));
3320
3321 if (newBlock)
3322 {
3323 //printf("** end of block\n");
3324 if (insideList && lineIndent<currentIndent) // end of list
3325 {
3326 //printf("** end of list\n");
3327 currentIndent = indent;
3328 insideList = false;
3329 }
3330 newBlock = false;
3331 }
3332
3333 if ((listIndent=isListMarker(data.substr(i,end-i)))) // see if we need to increase the indent level
3334 {
3335 if (listIndent<currentIndent+4)
3336 {
3337 //printf("** start of list\n");
3338 insideList = true;
3339 currentIndent = listIndent;
3340 }
3341 }
3342 else if (isEndOfList(data.substr(i,end-i)))
3343 {
3344 //printf("** end of list\n");
3345 insideList = false;
3346 currentIndent = listIndent;
3347 }
3348 else if (isEmptyLine(data.substr(i,end-i)))
3349 {
3350 //printf("** new block\n");
3351 newBlock = true;
3352 }
3353
3354 //printf("indent=%d listIndent=%d blockIndent=%d\n",indent,listIndent,blockIndent);
3355
3356 //printf("findEndOfLine: pi=%d i=%d end=%d\n",pi,i,end);
3357
3358 if (pi!=std::string::npos)
3359 {
3360 size_t blockStart=0, blockEnd=0, blockOffset=0;
3361 QCString lang;
3362 size_t blockIndent = currentIndent;
3363 size_t ref = 0;
3364 //printf("isHeaderLine(%s)=%d\n",QCString(data+i).left(size-i).data(),level);
3365 QCString endBlockName;
3366 if (data[i]=='@' || data[i]=='\\') endBlockName = isBlockCommand(data.substr(i),i);
3367 if (!endBlockName.isEmpty())
3368 {
3369 // handle previous line
3370 if (isLinkRef(data.substr(pi,i-pi),id,link,title))
3371 {
3372 linkRefs.emplace(id.lower().str(),LinkRef(link,title));
3373 }
3374 else
3375 {
3376 writeOneLineHeaderOrRuler(data.substr(pi,i-pi));
3377 }
3378 out+=data[i];
3379 i++;
3380 size_t l = endBlockName.length();
3381 while (i+l<data.size())
3382 {
3383 if ((data[i]=='\\' || data[i]=='@') && // command
3384 data[i-1]!='\\' && data[i-1]!='@') // not escaped
3385 {
3386 if (qstrncmp(&data[i+1],endBlockName.data(),l)==0)
3387 {
3388 out+=data[i];
3389 out+=endBlockName;
3390 i+=l+1;
3391 break;
3392 }
3393 }
3394 out+=data[i];
3395 i++;
3396 }
3397 }
3398 else if ((level=isHeaderline(data.substr(i),TRUE))>0)
3399 {
3400 //printf("Found header at %d-%d\n",i,end);
3401 while (pi<data.size() && data[pi]==' ') pi++;
3402 QCString header = data.substr(pi,i-pi-1);
3403 id = extractTitleId(header, level);
3404 //printf("header='%s' is='%s'\n",qPrint(header),qPrint(id));
3405 if (!header.isEmpty())
3406 {
3407 if (!id.isEmpty())
3408 {
3409 out+=level==1?"@section ":"@subsection ";
3410 out+=id;
3411 out+=" ";
3412 out+=header;
3413 out+="\n\n";
3414 }
3415 else
3416 {
3417 out+=level==1?"<h1>":"<h2>";
3418 out+=header;
3419 out+=level==1?"\n</h1>\n":"\n</h2>\n";
3420 }
3421 }
3422 else
3423 {
3424 out+="\n<hr>\n";
3425 }
3426 pi=std::string::npos;
3427 i=end;
3428 end=i+1;
3429 continue;
3430 }
3431 else if ((ref=isLinkRef(data.substr(pi),id,link,title)))
3432 {
3433 //printf("found link ref: id='%s' link='%s' title='%s'\n",
3434 // qPrint(id),qPrint(link),qPrint(title));
3435 linkRefs.emplace(id.lower().str(),LinkRef(link,title));
3436 i=ref+pi;
3437 end=i+1;
3438 }
3439 else if (isFencedCodeBlock(data.substr(pi),currentIndent,lang,blockStart,blockEnd,blockOffset))
3440 {
3441 //printf("Found FencedCodeBlock lang='%s' start=%d end=%d code={%s}\n",
3442 // qPrint(lang),blockStart,blockEnd,QCString(data+pi+blockStart).left(blockEnd-blockStart).data());
3443 writeFencedCodeBlock(data.substr(pi),lang.view(),blockStart,blockEnd);
3444 i=pi+blockOffset;
3445 pi=std::string::npos;
3446 end=i+1;
3447 continue;
3448 }
3449 else if (isCodeBlock(data.substr(i,end-i),i,blockIndent))
3450 {
3451 // skip previous line (it is empty anyway)
3452 i+=writeCodeBlock(data.substr(i),blockIndent);
3453 pi=std::string::npos;
3454 end=i+1;
3455 continue;
3456 }
3457 else if (isTableBlock(data.substr(pi)))
3458 {
3459 i=pi+writeTableBlock(data.substr(pi));
3460 pi=std::string::npos;
3461 end=i+1;
3462 continue;
3463 }
3464 else
3465 {
3466 writeOneLineHeaderOrRuler(data.substr(pi,i-pi));
3467 }
3468 }
3469 pi=i;
3470 i=end;
3471 }
3472 //printf("last line %d size=%d\n",i,size);
3473 if (pi!=std::string::npos && pi<data.size()) // deal with the last line
3474 {
3475 if (isLinkRef(data.substr(pi),id,link,title))
3476 {
3477 //printf("found link ref: id='%s' link='%s' title='%s'\n",
3478 // qPrint(id),qPrint(link),qPrint(title));
3479 linkRefs.emplace(id.lower().str(),LinkRef(link,title));
3480 }
3481 else
3482 {
3483 writeOneLineHeaderOrRuler(data.substr(pi));
3484 }
3485 }
3486
3487 return out;
3488}
3489
3490static bool isOtherPage(std::string_view data)
3491{
3492#define OPC(x) if (literal_at(data,#x " ") || literal_at(data,#x "\n")) return true
3493 OPC(dir); OPC(defgroup); OPC(addtogroup); OPC(weakgroup); OPC(ingroup);
3494 OPC(fn); OPC(property); OPC(typedef); OPC(var); OPC(def);
3495 OPC(enum); OPC(namespace); OPC(class); OPC(concept); OPC(module);
3496 OPC(protocol); OPC(category); OPC(union); OPC(struct); OPC(interface);
3497 OPC(idlexcept); OPC(file);
3498#undef OPC
3499
3500 return false;
3501}
3502
3504{
3505 AUTO_TRACE("docs={}",Trace::trunc(docs));
3506 size_t i=0;
3507 std::string_view data(docs.str());
3508 const size_t size = data.size();
3509 if (!data.empty())
3510 {
3511 while (i<size && (data[i]==' ' || data[i]=='\n'))
3512 {
3513 i++;
3514 }
3515 if (literal_at(data.substr(i),"<!--!")) // skip over <!--! marker
3516 {
3517 i+=5;
3518 while (i<size && (data[i]==' ' || data[i]=='\n')) // skip over spaces after the <!--! marker
3519 {
3520 i++;
3521 }
3522 }
3523 if (i+1<size &&
3524 (data[i]=='\\' || data[i]=='@') &&
3525 (literal_at(data.substr(i+1),"page ") || literal_at(data.substr(i+1),"mainpage"))
3526 )
3527 {
3528 if (literal_at(data.substr(i+1),"page "))
3529 {
3530 AUTO_TRACE_EXIT("result=ExplicitPageResult::explicitPage");
3532 }
3533 else
3534 {
3535 AUTO_TRACE_EXIT("result=ExplicitPageResult::explicitMainPage");
3537 }
3538 }
3539 else if (i+1<size && (data[i]=='\\' || data[i]=='@') && isOtherPage(data.substr(i+1)))
3540 {
3541 AUTO_TRACE_EXIT("result=ExplicitPageResult::explicitOtherPage");
3543 }
3544 }
3545 AUTO_TRACE_EXIT("result=ExplicitPageResult::notExplicit");
3547}
3548
3549QCString Markdown::extractPageTitle(QCString &docs, QCString &id, int &prepend, bool &isIdGenerated)
3550{
3551 AUTO_TRACE("docs={} prepend={}",Trace::trunc(docs),id,prepend);
3552 // first first non-empty line
3553 prepend = 0;
3554 QCString title;
3555 size_t i=0;
3556 QCString docs_org(docs);
3557 std::string_view data(docs_org.str());
3558 const size_t size = data.size();
3559 docs.clear();
3560 while (i<size && (data[i]==' ' || data[i]=='\n'))
3561 {
3562 if (data[i]=='\n') prepend++;
3563 i++;
3564 }
3565 if (i>=size) { return QCString(); }
3566 size_t end1=i+1;
3567 while (end1<size && data[end1-1]!='\n') end1++;
3568 //printf("i=%d end1=%d size=%d line='%s'\n",i,end1,size,docs.mid(i,end1-i).data());
3569 // first line from i..end1
3570 if (end1<size)
3571 {
3572 // second line form end1..end2
3573 size_t end2=end1+1;
3574 while (end2<size && data[end2-1]!='\n') end2++;
3575 if (prv->isHeaderline(data.substr(end1),FALSE))
3576 {
3577 title = data.substr(i,end1-i-1);
3578 docs+="\n\n"+docs_org.mid(end2);
3579 id = prv->extractTitleId(title, 0, &isIdGenerated);
3580 //printf("extractPageTitle(title='%s' docs='%s' id='%s')\n",title.data(),docs.data(),id.data());
3581 AUTO_TRACE_EXIT("result={} id={} isIdGenerated={}",Trace::trunc(title),id,isIdGenerated);
3582 return title;
3583 }
3584 }
3585 if (i<end1 && prv->isAtxHeader(data.substr(i,end1-i),title,id,FALSE,&isIdGenerated)>0)
3586 {
3587 docs+="\n";
3588 docs+=docs_org.mid(end1);
3589 }
3590 else
3591 {
3592 docs=docs_org;
3593 id = prv->extractTitleId(title, 0, &isIdGenerated);
3594 }
3595 AUTO_TRACE_EXIT("result={} id={} isIdGenerated={}",Trace::trunc(title),id,isIdGenerated);
3596 return title;
3597}
3598
3599
3600//---------------------------------------------------------------------------
3601
3602QCString Markdown::process(const QCString &input, int &startNewlines, bool fromParseInput)
3603{
3604 if (input.isEmpty()) return input;
3605 size_t refIndent=0;
3606
3607 // for replace tabs by spaces
3608 QCString s = input;
3609 if (s.at(s.length()-1)!='\n') s += "\n"; // see PR #6766
3610 s = detab(s,refIndent);
3611 //printf("======== DeTab =========\n---- output -----\n%s\n---------\n",qPrint(s));
3612
3613 // then process quotation blocks (as these may contain other blocks)
3614 s = prv->processQuotations(s.view(),refIndent);
3615 //printf("======== Quotations =========\n---- output -----\n%s\n---------\n",qPrint(s));
3616
3617 // then process block items (headers, rules, and code blocks, references)
3618 s = prv->processBlocks(s.view(),refIndent);
3619 //printf("======== Blocks =========\n---- output -----\n%s\n---------\n",qPrint(s));
3620
3621 // finally process the inline markup (links, emphasis and code spans)
3622 prv->out.clear();
3623 prv->out.reserve(s.length());
3624 prv->processInline(s.view());
3625 if (fromParseInput)
3626 {
3627 Debug::print(Debug::Markdown,0,"---- output -----\n{}\n=========\n",qPrint(prv->out));
3628 }
3629 else
3630 {
3631 Debug::print(Debug::Markdown,0,"======== Markdown =========\n---- input ------- \n{}\n---- output -----\n{}\n=========\n",input,prv->out);
3632 }
3633
3634 // post processing
3635 QCString result = substitute(prv->out,g_doxy_nbsp,"&nbsp;");
3636 const char *p = result.data();
3637 if (p)
3638 {
3639 while (*p==' ') p++; // skip over spaces
3640 while (*p=='\n') {startNewlines++;p++;}; // skip over newlines
3641 if (literal_at(p,"<br>")) p+=4; // skip over <br>
3642 }
3643 if (p>result.data())
3644 {
3645 // strip part of the input
3646 result = result.mid(static_cast<int>(p-result.data()));
3647 }
3648 return result;
3649}
3650
3651//---------------------------------------------------------------------------
3652
3654{
3655 AUTO_TRACE("fileName={}",fileName);
3656 std::string absFileName = FileInfo(fileName.str()).absFilePath();
3657 QCString baseFn = stripFromPath(absFileName.c_str());
3658 int i = baseFn.findRev('.');
3659 if (i!=-1) baseFn = baseFn.left(i);
3660 QCString baseName = escapeCharsInString(baseFn,false,false);
3661 //printf("markdownFileNameToId(%s)=md_%s\n",qPrint(fileName),qPrint(baseName));
3662 QCString res = "md_"+baseName;
3663 AUTO_TRACE_EXIT("result={}",res);
3664 return res;
3665}
3666
3667//---------------------------------------------------------------------------
3668
3673
3675{
3676}
3677
3681
3683 const char *fileBuf,
3684 const std::shared_ptr<Entry> &root,
3685 ClangTUParser* /*clangParser*/)
3686{
3687 std::shared_ptr<Entry> current = std::make_shared<Entry>();
3688 int prepend = 0; // number of empty lines in front
3689 current->lang = SrcLangExt::Markdown;
3690 current->fileName = fileName;
3691 current->docFile = fileName;
3692 current->docLine = 1;
3693 QCString docs = stripIndentation(fileBuf);
3694 if (!docs.stripWhiteSpace().size()) return;
3695 Debug::print(Debug::Markdown,0,"======== Markdown =========\n---- input ------- \n{}\n",fileBuf);
3696 QCString id;
3697 Markdown markdown(fileName,1,0);
3698 bool isIdGenerated = false;
3699 QCString title = markdown.extractPageTitle(docs, id, prepend, isIdGenerated).stripWhiteSpace();
3700 QCString generatedId;
3701 if (isIdGenerated)
3702 {
3703 generatedId = id;
3704 id = "";
3705 }
3706 int indentLevel=title.isEmpty() ? 0 : -1;
3707 markdown.setIndentLevel(indentLevel);
3708 FileInfo fi(fileName.str());
3709 QCString fn = fi.fileName();
3711 QCString mdfileAsMainPage = Config_getString(USE_MDFILE_AS_MAINPAGE);
3712 QCString mdFileNameId = markdownFileNameToId(fileName);
3713 bool wasEmpty = id.isEmpty();
3714 if (wasEmpty) id = mdFileNameId;
3715 QCString relFileName = stripFromPath(fileName);
3716 bool isSubdirDocs = Config_getBool(IMPLICIT_DIR_DOCS) && relFileName.lower().endsWith("/readme.md");
3717 switch (isExplicitPage(docs))
3718 {
3720 if (!mdfileAsMainPage.isEmpty() &&
3721 (fi.absFilePath()==FileInfo(mdfileAsMainPage.str()).absFilePath()) // file reference with path
3722 )
3723 {
3724 docs.prepend("@ianchor{" + title + "} " + id + "\\ilinebr ");
3725 docs.prepend("@mainpage "+title+"\\ilinebr ");
3726 }
3727 else if (id=="mainpage" || id=="index")
3728 {
3729 if (title.isEmpty()) title = titleFn;
3730 docs.prepend("@ianchor{" + title + "} " + id + "\\ilinebr ");
3731 docs.prepend("@mainpage "+title+"\\ilinebr ");
3732 }
3733 else if (isSubdirDocs)
3734 {
3735 if (!generatedId.isEmpty() && !title.isEmpty())
3736 {
3737 docs.prepend("@section " + generatedId + " " + title + "\\ilinebr ");
3738 }
3739 docs.prepend("@dir\\ilinebr ");
3740 }
3741 else
3742 {
3743 if (title.isEmpty())
3744 {
3745 title = titleFn;
3746 prepend = 0;
3747 }
3748 if (!wasEmpty)
3749 {
3750 docs.prepend("@ianchor{" + title + "} " + id + "\\ilinebr @ianchor{" + relFileName + "} " + mdFileNameId + "\\ilinebr ");
3751 }
3752 else if (!generatedId.isEmpty())
3753 {
3754 docs.prepend("@ianchor " + generatedId + "\\ilinebr ");
3755 }
3756 else if (Config_getEnum(MARKDOWN_ID_STYLE)==MARKDOWN_ID_STYLE_t::GITHUB)
3757 {
3758 QCString autoId = AnchorGenerator::instance().generate(title.str());
3759 docs.prepend("@ianchor{" + title + "} " + autoId + "\\ilinebr ");
3760 }
3761 docs.prepend("@page "+id+" "+title+"\\ilinebr ");
3762 }
3763 for (int i = 0; i < prepend; i++) docs.prepend("\n");
3764 break;
3766 {
3767 // look for `@page label My Title\n` and capture `label` (match[1]) and ` My Title` (match[2])
3768 static const reg::Ex re(R"([ ]*[\\@]page\s+(\a[\w-]*)(\s*[^\n]*)\n)");
3769 reg::Match match;
3770 std::string s = docs.str();
3771 if (reg::search(s,match,re))
3772 {
3773 QCString orgLabel = match[1].str();
3774 QCString orgTitle = match[2].str();
3775 orgTitle = orgTitle.stripWhiteSpace();
3776 QCString newLabel = markdownFileNameToId(fileName);
3777 docs = docs.left(match[1].position())+ // part before label
3778 newLabel+ // new label
3779 match[2].str()+ // part between orgLabel and \n
3780 "\\ilinebr @ianchor{" + orgTitle + "} "+orgLabel+"\n"+ // add original anchor plus \n of above
3781 docs.right(docs.length()-match.length()); // add remainder of docs
3782 }
3783 }
3784 break;
3786 break;
3788 break;
3789 }
3790 int lineNr=1;
3791
3792 p->commentScanner.enterFile(fileName,lineNr);
3793 Protection prot = Protection::Public;
3794 bool needsEntry = false;
3795 int position=0;
3796 GuardedSectionStack guards;
3797 QCString processedDocs = markdown.process(docs,lineNr,true);
3798 while (p->commentScanner.parseCommentBlock(
3799 this,
3800 current.get(),
3801 processedDocs,
3802 fileName,
3803 lineNr,
3804 FALSE, // isBrief
3805 FALSE, // javadoc autobrief
3806 FALSE, // inBodyDocs
3807 prot, // protection
3808 position,
3809 needsEntry,
3810 true,
3811 &guards
3812 ))
3813 {
3814 if (needsEntry)
3815 {
3816 QCString docFile = current->docFile;
3817 root->moveToSubEntryAndRefresh(current);
3818 current->lang = SrcLangExt::Markdown;
3819 current->docFile = docFile;
3820 current->docLine = lineNr;
3821 }
3822 }
3823 if (needsEntry)
3824 {
3825 root->moveToSubEntryAndKeep(current);
3826 }
3827 p->commentScanner.leaveFile(fileName,lineNr);
3828}
3829
3831{
3832 Doxygen::parserManager->getOutlineParser("*.cpp")->parsePrototype(text);
3833}
3834
3835//------------------------------------------------------------------------
#define eol
The end of line string for this machine.
static AnchorGenerator & instance()
Returns the singleton instance.
Definition anchor.cpp:38
static std::string addPrefixIfNeeded(const std::string &anchor)
Definition anchor.cpp:46
std::string generate(const std::string &title)
generates an anchor for a section with title.
Definition anchor.cpp:59
Clang parser object for a single translation unit, which consists of a source file and the directly o...
Definition clangparser.h:25
@ Markdown
Definition debug.h:37
static void print(DebugMask mask, int prio, fmt::format_string< Args... > fmt, Args &&... args)
Definition debug.h:76
static ParserManager * parserManager
Definition doxygen.h:131
static FileNameLinkedMap * imageNameLinkedMap
Definition doxygen.h:106
A model of a file symbol.
Definition filedef.h:99
Minimal replacement for QFileInfo.
Definition fileinfo.h:23
bool exists() const
Definition fileinfo.cpp:30
std::string fileName() const
Definition fileinfo.cpp:118
bool isReadable() const
Definition fileinfo.cpp:44
std::string absFilePath() const
Definition fileinfo.cpp:101
Helper class to process markdown formatted text.
Definition markdown.h:32
std::unique_ptr< Private > prv
Definition markdown.h:43
void setIndentLevel(int level)
Definition markdown.cpp:191
QCString extractPageTitle(QCString &docs, QCString &id, int &prepend, bool &isIdGenerated)
Markdown(const QCString &fileName, int lineNr, int indentLevel=0)
Definition markdown.cpp:182
QCString process(const QCString &input, int &startNewlines, bool fromParseInput=false)
void parseInput(const QCString &fileName, const char *fileBuf, const std::shared_ptr< Entry > &root, ClangTUParser *clangParser) override
Parses a single input file with the goal to build an Entry tree.
~MarkdownOutlineParser() override
void parsePrototype(const QCString &text) override
Callback function called by the comment block scanner.
std::unique_ptr< Private > p
Definition markdown.h:60
This is an alternative implementation of QCString.
Definition qcstring.h:101
int find(char c, int index=0, bool cs=TRUE) const
Definition qcstring.cpp:43
QCString & prepend(const char *s)
Definition qcstring.h:407
size_t length() const
Returns the length of the string, not counting the 0-terminator.
Definition qcstring.h:153
QCString mid(size_t index, size_t len=static_cast< size_t >(-1)) const
Definition qcstring.h:226
QCString lower() const
Definition qcstring.h:234
bool endsWith(const char *s) const
Definition qcstring.h:509
char & at(size_t i)
Returns a reference to the character at index i.
Definition qcstring.h:578
bool isEmpty() const
Returns TRUE iff the string is empty.
Definition qcstring.h:150
QCString stripWhiteSpace() const
returns a copy of this string with leading and trailing whitespace removed
Definition qcstring.h:245
const std::string & str() const
Definition qcstring.h:537
QCString & setNum(short n)
Definition qcstring.h:444
QCString simplifyWhiteSpace() const
return a copy of this string with leading and trailing whitespace removed and multiple whitespace cha...
Definition qcstring.cpp:185
QCString right(size_t len) const
Definition qcstring.h:219
size_t size() const
Returns the length of the string, not counting the 0-terminator.
Definition qcstring.h:156
QCString & sprintf(const char *format,...)
Definition qcstring.cpp:29
int findRev(char c, int index=-1, bool cs=TRUE) const
Definition qcstring.cpp:91
const char * data() const
Returns a pointer to the contents of the string in the form of a 0-terminated C string.
Definition qcstring.h:159
std::string_view view() const
Definition qcstring.h:161
QCString left(size_t len) const
Definition qcstring.h:214
void clear()
Definition qcstring.h:169
static constexpr int Section
Definition section.h:33
static constexpr int MaxLevel
Definition section.h:39
static constexpr int Subsection
Definition section.h:34
static constexpr int Subsubsection
Definition section.h:35
static constexpr int MinLevel
Definition section.h:32
static constexpr int Paragraph
Definition section.h:36
static constexpr int Subsubparagraph
Definition section.h:38
static constexpr int Subparagraph
Definition section.h:37
Class representing a regular expression.
Definition regex.h:39
Object representing the matching results.
Definition regex.h:153
Interface for the comment block scanner.
std::stack< GuardedSection > GuardedSectionStack
Definition commentscan.h:48
#define Config_getInt(name)
Definition config.h:34
#define Config_getBool(name)
Definition config.h:33
#define Config_getString(name)
Definition config.h:32
#define Config_getEnum(name)
Definition config.h:35
std::vector< std::string > StringVector
Definition containers.h:33
DirIterator end(const DirIterator &) noexcept
Definition dir.cpp:175
#define AUTO_TRACE_ADD(...)
Definition docnode.cpp:47
#define AUTO_TRACE(...)
Definition docnode.cpp:46
#define AUTO_TRACE_EXIT(...)
Definition docnode.cpp:48
size_t size
Definition htmlgen.cpp:156
const char * data
Definition htmlgen.cpp:158
QCString s
Definition htmlgen.cpp:154
size_t i
Definition htmlgen.cpp:161
static bool isOtherPage(std::string_view data)
#define AUTO_TRACE(...)
Definition markdown.cpp:61
static bool hasLineBreak(std::string_view data)
#define isIdChar(c)
Definition markdown.cpp:77
ExplicitPageResult
Definition markdown.cpp:67
@ explicitMainPage
docs start with a mainpage command
Definition markdown.cpp:69
@ explicitPage
docs start with a page command
Definition markdown.cpp:68
@ notExplicit
docs doesn't start with either page or mainpage
Definition markdown.cpp:71
@ explicitOtherPage
docs start with a dir / defgroup / addtogroup command
Definition markdown.cpp:70
static bool isBlockQuote(std::string_view data, size_t indent)
returns true if this line starts a block quote
static size_t isLinkRef(std::string_view data, QCString &refid, QCString &link, QCString &title)
returns end of the link ref if this is indeed a link reference.
static QCString escapeDoubleQuotes(const QCString &s)
Definition markdown.cpp:220
static bool isEndOfList(std::string_view data)
static size_t computeIndentExcludingListMarkers(std::string_view data)
static Alignment markersToAlignment(bool leftMarker, bool rightMarker)
helper function to convert presence of left and/or right alignment markers to an alignment value
Definition markdown.cpp:292
const char * g_doxy_nbsp
Definition markdown.cpp:200
static QCString escapeSpecialChars(const QCString &s)
Definition markdown.cpp:238
#define OPC(x)
static bool isCodeBlock(std::string_view data, size_t offset, size_t &indent)
static bool isEmptyLine(std::string_view data)
#define AUTO_TRACE_EXIT(...)
Definition markdown.cpp:63
#define isLiTag(i)
static size_t findTableColumns(std::string_view data, size_t &start, size_t &end, size_t &columns)
Finds the location of the table's contains in the string data.
const size_t codeBlockIndent
Definition markdown.cpp:201
static ExplicitPageResult isExplicitPage(const QCString &docs)
const char * g_utf8_nbsp
Definition markdown.cpp:199
#define ignoreCloseEmphChar(c, cn)
Definition markdown.cpp:100
static const std::unordered_map< std::string, std::string > g_quotationHeaderMap
#define isOpenEmphChar(c)
Definition markdown.cpp:93
Alignment
Definition markdown.cpp:194
@ AlignLeft
Definition markdown.cpp:194
@ AlignNone
Definition markdown.cpp:194
@ AlignRight
Definition markdown.cpp:194
@ AlignCenter
Definition markdown.cpp:194
static bool isFencedCodeBlock(std::string_view data, size_t refIndent, QCString &lang, size_t &start, size_t &end, size_t &offset)
static size_t isListMarker(std::string_view data)
static bool isHRuler(std::string_view data)
static QCString getFilteredImageAttributes(std::string_view fmt, const QCString &attrs)
parse the image attributes and return attributes for given format
Definition markdown.cpp:313
bool skipOverFileAndLineCommands(std::string_view data, size_t indent, size_t &offset, std::string &location)
#define extraChar(c)
Definition markdown.cpp:84
static bool isTableBlock(std::string_view data)
Returns TRUE iff data points to the start of a table block.
size_t isNewline(std::string_view data)
Definition markdown.cpp:207
QCString markdownFileNameToId(const QCString &fileName)
processes string s and converts markdown into doxygen/html commands.
#define warn(file, line, fmt,...)
Definition message.h:97
bool isAbsolutePath(const QCString &fileName)
Definition portable.cpp:514
const char * strnstr(const char *haystack, const char *needle, size_t haystack_len)
Definition portable.cpp:617
QCString trunc(const QCString &s, size_t numChars=15)
Definition trace.h:56
Definition message.h:144
bool search(std::string_view str, Match &match, const Ex &re, size_t pos)
Search in a given string str starting at position pos for a match against regular expression re.
Definition regex.cpp:748
Portable versions of functions that are platform dependent.
static void decrLevel(yyscan_t yyscanner)
Definition pre.l:2179
QCString substitute(const QCString &s, const QCString &src, const QCString &dst)
substitute all occurrences of src in s by dst
Definition qcstring.cpp:477
int qstrncmp(const char *str1, const char *str2, size_t len)
Definition qcstring.h:75
bool qisspace(char c)
Definition qcstring.h:81
const char * qPrint(const char *s)
Definition qcstring.h:672
#define TRUE
Definition qcstring.h:37
#define FALSE
Definition qcstring.h:34
Some helper functions for std::string.
bool literal_at(const char *data, const char(&str)[N])
returns TRUE iff data points to a substring that matches string literal str
Definition stringutil.h:98
std::string_view stripWhiteSpace(std::string_view s)
Given a string view s, returns a new, narrower view on that string, skipping over any leading or trai...
Definition stringutil.h:72
int processEmphasis1(std::string_view data, char c)
process single emphasis
Definition markdown.cpp:815
int processQuoted(std::string_view data, size_t offset)
Process quoted section "...", can contain one embedded newline.
Definition markdown.cpp:985
void writeMarkdownImage(std::string_view fmt, bool inline_img, bool explicitTitle, const QCString &title, const QCString &content, const QCString &link, const QCString &attributes, const FileDef *fd)
size_t writeTableBlock(std::string_view data)
size_t writeBlockQuote(std::string_view data)
size_t isSpecialCommand(std::string_view data, size_t offset)
Definition markdown.cpp:429
std::function< int(std::string_view, size_t)> Action_t
Definition markdown.cpp:172
int processEmphasis3(std::string_view data, char c)
Parsing triple emphasis.
Definition markdown.cpp:881
int processCodeSpan(std::string_view data, size_t offset)
` parsing a code span (assuming codespan != 0)
int processSpecialCommand(std::string_view data, size_t offset)
QCString extractTitleId(QCString &title, int level, bool *pIsIdGenerated=nullptr)
void writeFencedCodeBlock(std::string_view data, std::string_view lang, size_t blockStart, size_t blockEnd)
int isHeaderline(std::string_view data, bool allowAdjustLevel)
returns whether the line is a setext-style hdr underline
size_t findEmphasisChar(std::string_view, char c, size_t c_size)
looks for the next emph char, skipping other constructs, and stopping when either it is found,...
Definition markdown.cpp:702
std::unordered_map< std::string, LinkRef > linkRefs
Definition markdown.cpp:174
void addStrEscapeUtf8Nbsp(std::string_view data)
QCString isBlockCommand(std::string_view data, size_t offset)
Definition markdown.cpp:360
size_t writeCodeBlock(std::string_view, size_t refIndent)
int processHtmlTag(std::string_view data, size_t offset)
QCString processQuotations(std::string_view data, size_t refIndent)
QCString processBlocks(std::string_view data, size_t indent)
int processEmphasis(std::string_view data, size_t offset)
int processLink(std::string_view data, size_t offset)
int processHtmlTagWrite(std::string_view data, size_t offset, bool doWrite)
Process a HTML tag.
int isAtxHeader(std::string_view data, QCString &header, QCString &id, bool allowAdjustLevel, bool *pIsIdGenerated=nullptr)
size_t findEndOfLine(std::string_view data, size_t offset)
int processEmphasis2(std::string_view data, char c)
process double emphasis
Definition markdown.cpp:849
void processInline(std::string_view data)
int processNmdash(std::string_view data, size_t offset)
Process ndash and mdashes.
Definition markdown.cpp:943
void writeOneLineHeaderOrRuler(std::string_view data)
std::array< Action_t, 256 > actions
Definition markdown.cpp:179
Protection
Definition types.h:32
SrcLangExt
Definition types.h:207
SrcLangExt getLanguageFromFileName(const QCString &fileName, SrcLangExt defLang)
Definition util.cpp:5719
QCString stripIndentation(const QCString &s, bool skipFirstLine)
Definition util.cpp:6463
QCString escapeCharsInString(const QCString &name, bool allowDots, bool allowUnderscore)
Definition util.cpp:3844
QCString stripExtensionGeneral(const QCString &fName, const QCString &ext)
Definition util.cpp:5425
bool isURL(const QCString &url)
Checks whether the given url starts with a supported protocol.
Definition util.cpp:6427
static QCString stripFromPath(const QCString &p, const StringVector &l)
Definition util.cpp:310
QCString detab(const QCString &s, size_t &refIndent)
Definition util.cpp:7232
StringVector split(const std::string &s, const std::string &delimiter)
split input string s by string delimiter delimiter.
Definition util.cpp:7130
QCString externalLinkTarget(const bool parent)
Definition util.cpp:6223
QCString getFileNameExtension(const QCString &fn)
Definition util.cpp:5761
FileDef * findFileDef(const FileNameLinkedMap *fnMap, const QCString &n, bool &ambig)
Definition util.cpp:3417
A bunch of utility functions.