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