1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3#include "docparser.h"
4
5#include "codemarker.h"
6#include "doc.h"
7#include "docprivate.h"
8#include "editdistance.h"
9#include "macro.h"
10#include "openedlist.h"
11#include "tokenizer.h"
12
13#include <QtCore/qfile.h>
14#include <QtCore/qregularexpression.h>
15#include <QtCore/qtextstream.h>
16
17#include <cctype>
18#include <climits>
19#include <functional>
20
21QT_BEGIN_NAMESPACE
22
23using namespace Qt::StringLiterals;
24
25DocUtilities &DocParser::s_utilities = DocUtilities::instance();
26
27enum {
28 CMD_A,
29 CMD_ANNOTATEDLIST,
30 CMD_B,
31 CMD_BADCODE,
32 CMD_BOLD,
33 CMD_BR,
34 CMD_BRIEF,
35 CMD_C,
36 CMD_CAPTION,
37 CMD_CODE,
38 CMD_CODELINE,
39 CMD_DETAILS,
40 CMD_DIV,
41 CMD_DOTS,
42 CMD_E,
43 CMD_ELSE,
44 CMD_ENDCODE,
45 CMD_ENDDETAILS,
46 CMD_ENDDIV,
47 CMD_ENDFOOTNOTE,
48 CMD_ENDIF,
49 CMD_ENDLEGALESE,
50 CMD_ENDLINK,
51 CMD_ENDLIST,
52 CMD_ENDMAPREF,
53 CMD_ENDOMIT,
54 CMD_ENDQUOTATION,
55 CMD_ENDRAW,
56 CMD_ENDSECTION1,
57 CMD_ENDSECTION2,
58 CMD_ENDSECTION3,
59 CMD_ENDSECTION4,
60 CMD_ENDSIDEBAR,
61 CMD_ENDTABLE,
62 CMD_FOOTNOTE,
63 CMD_GENERATELIST,
64 CMD_HEADER,
65 CMD_HR,
66 CMD_I,
67 CMD_IF,
68 CMD_IMAGE,
69 CMD_IMPORTANT,
70 CMD_INCLUDE,
71 CMD_INLINEIMAGE,
72 CMD_INDEX,
73 CMD_INPUT,
74 CMD_KEYWORD,
75 CMD_L,
76 CMD_LEGALESE,
77 CMD_LI,
78 CMD_LINK,
79 CMD_LIST,
80 CMD_META,
81 CMD_NOTE,
82 CMD_O,
83 CMD_OMIT,
84 CMD_OMITVALUE,
85 CMD_OVERLOAD,
86 CMD_PRINTLINE,
87 CMD_PRINTTO,
88 CMD_PRINTUNTIL,
89 CMD_QUOTATION,
90 CMD_QUOTEFILE,
91 CMD_QUOTEFROMFILE,
92 CMD_RAW,
93 CMD_ROW,
94 CMD_SA,
95 CMD_SECTION1,
96 CMD_SECTION2,
97 CMD_SECTION3,
98 CMD_SECTION4,
99 CMD_SIDEBAR,
100 CMD_SINCELIST,
101 CMD_SKIPLINE,
102 CMD_SKIPTO,
103 CMD_SKIPUNTIL,
104 CMD_SNIPPET,
105 CMD_SPAN,
106 CMD_SUB,
107 CMD_SUP,
108 CMD_TABLE,
109 CMD_TABLEOFCONTENTS,
110 CMD_TARGET,
111 CMD_TT,
112 CMD_UICONTROL,
113 CMD_UNDERLINE,
114 CMD_UNICODE,
115 CMD_VALUE,
116 CMD_WARNING,
117 CMD_QML,
118 CMD_ENDQML,
119 CMD_CPP,
120 CMD_ENDCPP,
121 CMD_CPPTEXT,
122 CMD_ENDCPPTEXT,
123 NOT_A_CMD
124};
125
126static struct
127{
128 const char *name;
129 int no;
130} cmds[] = { { .name: "a", .no: CMD_A },
131 { .name: "annotatedlist", .no: CMD_ANNOTATEDLIST },
132 { .name: "b", .no: CMD_B },
133 { .name: "badcode", .no: CMD_BADCODE },
134 { .name: "bold", .no: CMD_BOLD },
135 { .name: "br", .no: CMD_BR },
136 { .name: "brief", .no: CMD_BRIEF },
137 { .name: "c", .no: CMD_C },
138 { .name: "caption", .no: CMD_CAPTION },
139 { .name: "code", .no: CMD_CODE },
140 { .name: "codeline", .no: CMD_CODELINE },
141 { .name: "details", .no: CMD_DETAILS },
142 { .name: "div", .no: CMD_DIV },
143 { .name: "dots", .no: CMD_DOTS },
144 { .name: "e", .no: CMD_E },
145 { .name: "else", .no: CMD_ELSE },
146 { .name: "endcode", .no: CMD_ENDCODE },
147 { .name: "enddetails", .no: CMD_ENDDETAILS },
148 { .name: "enddiv", .no: CMD_ENDDIV },
149 { .name: "endfootnote", .no: CMD_ENDFOOTNOTE },
150 { .name: "endif", .no: CMD_ENDIF },
151 { .name: "endlegalese", .no: CMD_ENDLEGALESE },
152 { .name: "endlink", .no: CMD_ENDLINK },
153 { .name: "endlist", .no: CMD_ENDLIST },
154 { .name: "endmapref", .no: CMD_ENDMAPREF },
155 { .name: "endomit", .no: CMD_ENDOMIT },
156 { .name: "endquotation", .no: CMD_ENDQUOTATION },
157 { .name: "endraw", .no: CMD_ENDRAW },
158 { .name: "endsection1", .no: CMD_ENDSECTION1 }, // ### don't document for now
159 { .name: "endsection2", .no: CMD_ENDSECTION2 }, // ### don't document for now
160 { .name: "endsection3", .no: CMD_ENDSECTION3 }, // ### don't document for now
161 { .name: "endsection4", .no: CMD_ENDSECTION4 }, // ### don't document for now
162 { .name: "endsidebar", .no: CMD_ENDSIDEBAR },
163 { .name: "endtable", .no: CMD_ENDTABLE },
164 { .name: "footnote", .no: CMD_FOOTNOTE },
165 { .name: "generatelist", .no: CMD_GENERATELIST },
166 { .name: "header", .no: CMD_HEADER },
167 { .name: "hr", .no: CMD_HR },
168 { .name: "i", .no: CMD_I },
169 { .name: "if", .no: CMD_IF },
170 { .name: "image", .no: CMD_IMAGE },
171 { .name: "important", .no: CMD_IMPORTANT },
172 { .name: "include", .no: CMD_INCLUDE },
173 { .name: "inlineimage", .no: CMD_INLINEIMAGE },
174 { .name: "index", .no: CMD_INDEX }, // ### don't document for now
175 { .name: "input", .no: CMD_INPUT },
176 { .name: "keyword", .no: CMD_KEYWORD },
177 { .name: "l", .no: CMD_L },
178 { .name: "legalese", .no: CMD_LEGALESE },
179 { .name: "li", .no: CMD_LI },
180 { .name: "link", .no: CMD_LINK },
181 { .name: "list", .no: CMD_LIST },
182 { .name: "meta", .no: CMD_META },
183 { .name: "note", .no: CMD_NOTE },
184 { .name: "o", .no: CMD_O },
185 { .name: "omit", .no: CMD_OMIT },
186 { .name: "omitvalue", .no: CMD_OMITVALUE },
187 { .name: "overload", .no: CMD_OVERLOAD },
188 { .name: "printline", .no: CMD_PRINTLINE },
189 { .name: "printto", .no: CMD_PRINTTO },
190 { .name: "printuntil", .no: CMD_PRINTUNTIL },
191 { .name: "quotation", .no: CMD_QUOTATION },
192 { .name: "quotefile", .no: CMD_QUOTEFILE },
193 { .name: "quotefromfile", .no: CMD_QUOTEFROMFILE },
194 { .name: "raw", .no: CMD_RAW },
195 { .name: "row", .no: CMD_ROW },
196 { .name: "sa", .no: CMD_SA },
197 { .name: "section1", .no: CMD_SECTION1 },
198 { .name: "section2", .no: CMD_SECTION2 },
199 { .name: "section3", .no: CMD_SECTION3 },
200 { .name: "section4", .no: CMD_SECTION4 },
201 { .name: "sidebar", .no: CMD_SIDEBAR },
202 { .name: "sincelist", .no: CMD_SINCELIST },
203 { .name: "skipline", .no: CMD_SKIPLINE },
204 { .name: "skipto", .no: CMD_SKIPTO },
205 { .name: "skipuntil", .no: CMD_SKIPUNTIL },
206 { .name: "snippet", .no: CMD_SNIPPET },
207 { .name: "span", .no: CMD_SPAN },
208 { .name: "sub", .no: CMD_SUB },
209 { .name: "sup", .no: CMD_SUP },
210 { .name: "table", .no: CMD_TABLE },
211 { .name: "tableofcontents", .no: CMD_TABLEOFCONTENTS },
212 { .name: "target", .no: CMD_TARGET },
213 { .name: "tt", .no: CMD_TT },
214 { .name: "uicontrol", .no: CMD_UICONTROL },
215 { .name: "underline", .no: CMD_UNDERLINE },
216 { .name: "unicode", .no: CMD_UNICODE },
217 { .name: "value", .no: CMD_VALUE },
218 { .name: "warning", .no: CMD_WARNING },
219 { .name: "qml", .no: CMD_QML },
220 { .name: "endqml", .no: CMD_ENDQML },
221 { .name: "cpp", .no: CMD_CPP },
222 { .name: "endcpp", .no: CMD_ENDCPP },
223 { .name: "cpptext", .no: CMD_CPPTEXT },
224 { .name: "endcpptext", .no: CMD_ENDCPPTEXT },
225 { .name: nullptr, .no: 0 } };
226
227int DocParser::s_tabSize;
228QStringList DocParser::s_ignoreWords;
229bool DocParser::s_quoting = false;
230FileResolver *DocParser::file_resolver{ nullptr };
231
232static QString cleanLink(const QString &link)
233{
234 qsizetype colonPos = link.indexOf(c: ':');
235 if ((colonPos == -1) || (!link.startsWith(s: "file:") && !link.startsWith(s: "mailto:")))
236 return link;
237 return link.mid(position: colonPos + 1).simplified();
238}
239
240void DocParser::initialize(const Config &config, FileResolver &file_resolver)
241{
242 s_tabSize = config.get(CONFIG_TABSIZE).asInt();
243 s_ignoreWords = config.get(CONFIG_IGNOREWORDS).asStringList();
244
245 int i = 0;
246 while (cmds[i].name) {
247 s_utilities.cmdHash.insert(key: cmds[i].name, value: cmds[i].no);
248
249 if (cmds[i].no != i)
250 Location::internalError(QStringLiteral("command %1 missing").arg(a: i));
251 ++i;
252 }
253
254 // If any of the formats define quotinginformation, activate quoting
255 DocParser::s_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool();
256 const auto &outputFormats = config.getOutputFormats();
257 for (const auto &format : outputFormats)
258 DocParser::s_quoting = DocParser::s_quoting
259 || config.get(var: format + Config::dot + CONFIG_QUOTINGINFORMATION).asBool();
260
261 // KLUDGE: file_resolver is temporarily a pointer. See the
262 // comment for file_resolver in the header file for more context.
263 DocParser::file_resolver = &file_resolver;
264}
265
266/*!
267 Parse the \a source string to build a Text data structure
268 in \a docPrivate. The Text data structure is a linked list
269 of Atoms.
270
271 \a metaCommandSet is the set of metacommands that may be
272 found in \a source. These metacommands are not markup text
273 commands. They are topic commands and related metacommands.
274 */
275void DocParser::parse(const QString &source, DocPrivate *docPrivate,
276 const QSet<QString> &metaCommandSet, const QSet<QString> &possibleTopics)
277{
278 m_input = source;
279 m_position = 0;
280 m_inputLength = m_input.size();
281 m_cachedLocation = docPrivate->m_start_loc;
282 m_cachedPosition = 0;
283 m_private = docPrivate;
284 m_private->m_text << Atom::Nop;
285 m_private->m_topics.clear();
286
287 m_paragraphState = OutsideParagraph;
288 m_inTableHeader = false;
289 m_inTableRow = false;
290 m_inTableItem = false;
291 m_indexStartedParagraph = false;
292 m_pendingParagraphLeftType = Atom::Nop;
293 m_pendingParagraphRightType = Atom::Nop;
294
295 m_braceDepth = 0;
296 m_currentSection = Doc::NoSection;
297 m_openedCommands.push(t: CMD_OMIT);
298 m_quoter.reset();
299
300 CodeMarker *marker = nullptr;
301 Atom *currentLinkAtom = nullptr;
302 QString p1, p2;
303 QStack<bool> preprocessorSkipping;
304 int numPreprocessorSkipping = 0;
305
306 while (m_position < m_inputLength) {
307 QChar ch = m_input.at(i: m_position);
308
309 switch (ch.unicode()) {
310 case '\\': {
311 QString cmdStr;
312 m_backslashPosition = m_position;
313 ++m_position;
314 while (m_position < m_inputLength) {
315 ch = m_input.at(i: m_position);
316 if (ch.isLetterOrNumber()) {
317 cmdStr += ch;
318 ++m_position;
319 } else {
320 break;
321 }
322 }
323 m_endPosition = m_position;
324 if (cmdStr.isEmpty()) {
325 if (m_position < m_inputLength) {
326 enterPara();
327 if (m_input.at(i: m_position).isSpace()) {
328 skipAllSpaces();
329 appendChar(ch: QLatin1Char(' '));
330 } else {
331 appendChar(ch: m_input.at(i: m_position++));
332 }
333 }
334 } else {
335 // Ignore quoting atoms to make appendToCode()
336 // append to the correct atom.
337 if (!s_quoting || !isQuote(atom: m_private->m_text.lastAtom()))
338 m_lastAtom = m_private->m_text.lastAtom();
339
340 int cmd = s_utilities.cmdHash.value(key: cmdStr, defaultValue: NOT_A_CMD);
341 switch (cmd) {
342 case CMD_A:
343 enterPara();
344 p1 = getArgument();
345 append(type: Atom::FormattingLeft, ATOM_FORMATTING_PARAMETER);
346 append(type: Atom::String, string: p1);
347 append(type: Atom::FormattingRight, ATOM_FORMATTING_PARAMETER);
348 m_private->m_params.insert(value: p1);
349 break;
350 case CMD_BADCODE:
351 leavePara();
352 append(type: Atom::CodeBad,
353 string: getCode(cmd: CMD_BADCODE, marker, argStr: getMetaCommandArgument(cmdStr)));
354 break;
355 case CMD_BR:
356 enterPara();
357 append(type: Atom::BR);
358 break;
359 case CMD_BOLD:
360 location().warning(QStringLiteral("'\\bold' is deprecated. Use '\\b'"));
361 Q_FALLTHROUGH();
362 case CMD_B:
363 startFormat(ATOM_FORMATTING_BOLD, cmd);
364 break;
365 case CMD_BRIEF:
366 leavePara();
367 enterPara(leftType: Atom::BriefLeft, rightType: Atom::BriefRight);
368 break;
369 case CMD_C:
370 enterPara();
371 p1 = untabifyEtc(str: getArgument(verbatim: true));
372 marker = CodeMarker::markerForCode(code: p1);
373 append(type: Atom::C, string: marker->markedUpCode(code: p1, nullptr, location()));
374 break;
375 case CMD_CAPTION:
376 leavePara();
377 enterPara(leftType: Atom::CaptionLeft, rightType: Atom::CaptionRight);
378 break;
379 case CMD_CODE:
380 leavePara();
381 append(type: Atom::Code, string: getCode(cmd: CMD_CODE, marker: nullptr, argStr: getMetaCommandArgument(cmdStr)));
382 break;
383 case CMD_QML:
384 leavePara();
385 append(type: Atom::Qml,
386 string: getCode(cmd: CMD_QML, marker: CodeMarker::markerForLanguage(lang: QLatin1String("QML")),
387 argStr: getMetaCommandArgument(cmdStr)));
388 break;
389 case CMD_DETAILS:
390 leavePara();
391 append(type: Atom::DetailsLeft, string: getArgument());
392 m_openedCommands.push(t: cmd);
393 break;
394 case CMD_ENDDETAILS:
395 leavePara();
396 append(type: Atom::DetailsRight);
397 closeCommand(endCmd: cmd);
398 break;
399 case CMD_DIV:
400 leavePara();
401 p1 = getArgument(verbatim: true);
402 append(type: Atom::DivLeft, string: p1);
403 m_openedCommands.push(t: cmd);
404 break;
405 case CMD_ENDDIV:
406 leavePara();
407 append(type: Atom::DivRight);
408 closeCommand(endCmd: cmd);
409 break;
410 case CMD_CODELINE:
411 if (s_quoting) {
412 append(type: Atom::CodeQuoteCommand, string: cmdStr);
413 append(type: Atom::CodeQuoteArgument, string: " ");
414 }
415 if (isCode(atom: m_lastAtom) && m_lastAtom->string().endsWith(s: "\n\n"))
416 m_lastAtom->chopString();
417 appendToCode(code: "\n");
418 break;
419 case CMD_DOTS: {
420 QString arg = getOptionalArgument();
421 if (arg.isEmpty())
422 arg = "4";
423 if (s_quoting) {
424 append(type: Atom::CodeQuoteCommand, string: cmdStr);
425 append(type: Atom::CodeQuoteArgument, string: arg);
426 }
427 if (isCode(atom: m_lastAtom) && m_lastAtom->string().endsWith(s: "\n\n"))
428 m_lastAtom->chopString();
429
430 int indent = arg.toInt();
431 for (int i = 0; i < indent; ++i)
432 appendToCode(code: " ");
433 appendToCode(code: "...\n");
434 break;
435 }
436 case CMD_ELSE:
437 if (!preprocessorSkipping.empty()) {
438 if (preprocessorSkipping.top()) {
439 --numPreprocessorSkipping;
440 } else {
441 ++numPreprocessorSkipping;
442 }
443 preprocessorSkipping.top() = !preprocessorSkipping.top();
444 (void)getRestOfLine(); // ### should ensure that it's empty
445 if (numPreprocessorSkipping)
446 skipToNextPreprocessorCommand();
447 } else {
448 location().warning(
449 QStringLiteral("Unexpected '\\%1'").arg(a: cmdName(cmd: CMD_ELSE)));
450 }
451 break;
452 case CMD_ENDCODE:
453 closeCommand(endCmd: cmd);
454 break;
455 case CMD_ENDQML:
456 closeCommand(endCmd: cmd);
457 break;
458 case CMD_ENDFOOTNOTE:
459 if (closeCommand(endCmd: cmd)) {
460 leavePara();
461 append(type: Atom::FootnoteRight);
462 }
463 break;
464 case CMD_ENDIF:
465 if (preprocessorSkipping.size() > 0) {
466 if (preprocessorSkipping.pop())
467 --numPreprocessorSkipping;
468 (void)getRestOfLine(); // ### should ensure that it's empty
469 if (numPreprocessorSkipping)
470 skipToNextPreprocessorCommand();
471 } else {
472 location().warning(
473 QStringLiteral("Unexpected '\\%1'").arg(a: cmdName(cmd: CMD_ENDIF)));
474 }
475 break;
476 case CMD_ENDLEGALESE:
477 if (closeCommand(endCmd: cmd)) {
478 leavePara();
479 append(type: Atom::LegaleseRight);
480 }
481 break;
482 case CMD_ENDLINK:
483 if (closeCommand(endCmd: cmd)) {
484 if (m_private->m_text.lastAtom()->type() == Atom::String
485 && m_private->m_text.lastAtom()->string().endsWith(c: QLatin1Char(' ')))
486 m_private->m_text.lastAtom()->chopString();
487 append(type: Atom::FormattingRight, ATOM_FORMATTING_LINK);
488 }
489 break;
490 case CMD_ENDLIST:
491 if (closeCommand(endCmd: cmd)) {
492 leavePara();
493 if (m_openedLists.top().isStarted()) {
494 append(type: Atom::ListItemRight, string: m_openedLists.top().styleString());
495 append(type: Atom::ListRight, string: m_openedLists.top().styleString());
496 }
497 m_openedLists.pop();
498 }
499 break;
500 case CMD_ENDOMIT:
501 closeCommand(endCmd: cmd);
502 break;
503 case CMD_ENDQUOTATION:
504 if (closeCommand(endCmd: cmd)) {
505 leavePara();
506 append(type: Atom::QuotationRight);
507 }
508 break;
509 case CMD_ENDRAW:
510 location().warning(
511 QStringLiteral("Unexpected '\\%1'").arg(a: cmdName(cmd: CMD_ENDRAW)));
512 break;
513 case CMD_ENDSECTION1:
514 endSection(unit: Doc::Section1, endCmd: cmd);
515 break;
516 case CMD_ENDSECTION2:
517 endSection(unit: Doc::Section2, endCmd: cmd);
518 break;
519 case CMD_ENDSECTION3:
520 endSection(unit: Doc::Section3, endCmd: cmd);
521 break;
522 case CMD_ENDSECTION4:
523 endSection(unit: Doc::Section4, endCmd: cmd);
524 break;
525 case CMD_ENDSIDEBAR:
526 if (closeCommand(endCmd: cmd)) {
527 leavePara();
528 append(type: Atom::SidebarRight);
529 }
530 break;
531 case CMD_ENDTABLE:
532 if (closeCommand(endCmd: cmd)) {
533 leaveTableRow();
534 append(type: Atom::TableRight);
535 }
536 break;
537 case CMD_FOOTNOTE:
538 if (openCommand(cmd)) {
539 enterPara();
540 append(type: Atom::FootnoteLeft);
541 }
542 break;
543 case CMD_ANNOTATEDLIST:
544 append(type: Atom::AnnotatedList, string: getArgument());
545 break;
546 case CMD_SINCELIST:
547 leavePara();
548 append(type: Atom::SinceList, string: getRestOfLine().simplified());
549 break;
550 case CMD_GENERATELIST: {
551 QString arg1 = getArgument();
552 QString arg2 = getOptionalArgument();
553 if (!arg2.isEmpty())
554 arg1 += " " + arg2;
555 append(type: Atom::GeneratedList, string: arg1);
556 } break;
557 case CMD_HEADER:
558 if (m_openedCommands.top() == CMD_TABLE) {
559 leaveTableRow();
560 append(type: Atom::TableHeaderLeft);
561 m_inTableHeader = true;
562 } else {
563 if (m_openedCommands.contains(t: CMD_TABLE))
564 location().warning(QStringLiteral("Cannot use '\\%1' within '\\%2'")
565 .arg(args: cmdName(cmd: CMD_HEADER),
566 args: cmdName(cmd: m_openedCommands.top())));
567 else
568 location().warning(
569 QStringLiteral("Cannot use '\\%1' outside of '\\%2'")
570 .arg(args: cmdName(cmd: CMD_HEADER), args: cmdName(cmd: CMD_TABLE)));
571 }
572 break;
573 case CMD_I:
574 location().warning(QStringLiteral(
575 "'\\i' is deprecated. Use '\\e' for italic or '\\li' for list item"));
576 Q_FALLTHROUGH();
577 case CMD_E:
578 startFormat(ATOM_FORMATTING_ITALIC, cmd);
579 break;
580 case CMD_HR:
581 leavePara();
582 append(type: Atom::HR);
583 break;
584 case CMD_IF:
585 preprocessorSkipping.push(t: !Tokenizer::isTrue(condition: getRestOfLine()));
586 if (preprocessorSkipping.top())
587 ++numPreprocessorSkipping;
588 if (numPreprocessorSkipping)
589 skipToNextPreprocessorCommand();
590 break;
591 case CMD_IMAGE:
592 leaveValueList();
593 append(type: Atom::Image, string: getArgument());
594 append(type: Atom::ImageText, string: getRestOfLine());
595 break;
596 case CMD_IMPORTANT:
597 leavePara();
598 enterPara(leftType: Atom::ImportantLeft, rightType: Atom::ImportantRight);
599 break;
600 case CMD_INCLUDE:
601 case CMD_INPUT: {
602 QString fileName = getArgument();
603 QStringList parameters;
604 QString identifier;
605 if (isLeftBraceAhead()) {
606 identifier = getArgument();
607 while (isLeftBraceAhead() && parameters.size() < 9)
608 parameters << getArgument();
609 } else {
610 identifier = getRestOfLine();
611 }
612 include(fileName, identifier, parameters);
613 break;
614 }
615 case CMD_INLINEIMAGE:
616 enterPara();
617 append(type: Atom::InlineImage, string: getArgument());
618 //Append ImageText only if the following
619 //argument is enclosed in braces.
620 if (isLeftBraceAhead()) {
621 append(type: Atom::ImageText, string: getArgument());
622 append(type: Atom::String, string: " ");
623 }
624 break;
625 case CMD_INDEX:
626 if (m_paragraphState == OutsideParagraph) {
627 enterPara();
628 m_indexStartedParagraph = true;
629 } else {
630 const Atom *last = m_private->m_text.lastAtom();
631 if (m_indexStartedParagraph
632 && (last->type() != Atom::FormattingRight
633 || last->string() != ATOM_FORMATTING_INDEX))
634 m_indexStartedParagraph = false;
635 }
636 startFormat(ATOM_FORMATTING_INDEX, cmd);
637 break;
638 case CMD_KEYWORD:
639 leavePara();
640 insertKeyword(keyword: getRestOfLine());
641 break;
642 case CMD_L:
643 enterPara();
644 if (isLeftBracketAhead())
645 p2 = getBracketedArgument();
646 if (isLeftBraceAhead()) {
647 p1 = getArgument();
648 append(p1, p2);
649 if (!p2.isEmpty() && !(m_private->m_text.lastAtom()->error().isEmpty()))
650 location().warning(
651 QStringLiteral(
652 "Check parameter in '[ ]' of '\\l' command: '%1', "
653 "possible misspelling, or unrecognized module name")
654 .arg(a: m_private->m_text.lastAtom()->error()));
655 if (isLeftBraceAhead()) {
656 currentLinkAtom = m_private->m_text.lastAtom();
657 startFormat(ATOM_FORMATTING_LINK, cmd);
658 } else {
659 append(type: Atom::FormattingLeft, ATOM_FORMATTING_LINK);
660 append(type: Atom::String, string: cleanLink(link: p1));
661 append(type: Atom::FormattingRight, ATOM_FORMATTING_LINK);
662 }
663 } else {
664 p1 = getArgument();
665 append(p1, p2);
666 if (!p2.isEmpty() && !(m_private->m_text.lastAtom()->error().isEmpty()))
667 location().warning(
668 QStringLiteral(
669 "Check parameter in '[ ]' of '\\l' command: '%1', "
670 "possible misspelling, or unrecognized module name")
671 .arg(a: m_private->m_text.lastAtom()->error()));
672 append(type: Atom::FormattingLeft, ATOM_FORMATTING_LINK);
673 append(type: Atom::String, string: cleanLink(link: p1));
674 append(type: Atom::FormattingRight, ATOM_FORMATTING_LINK);
675 }
676 p2.clear();
677 break;
678 case CMD_LEGALESE:
679 leavePara();
680 if (openCommand(cmd))
681 append(type: Atom::LegaleseLeft);
682 docPrivate->m_hasLegalese = true;
683 break;
684 case CMD_LINK:
685 if (openCommand(cmd)) {
686 enterPara();
687 p1 = getArgument();
688 append(string: p1);
689 append(type: Atom::FormattingLeft, ATOM_FORMATTING_LINK);
690 skipSpacesOrOneEndl();
691 }
692 break;
693 case CMD_LIST:
694 if (openCommand(cmd)) {
695 leavePara();
696 m_openedLists.push(t: OpenedList(location(), getOptionalArgument()));
697 }
698 break;
699 case CMD_META:
700 m_private->constructExtra();
701 p1 = getArgument();
702 m_private->extra->m_metaMap.insert(key: p1, value: getArgument());
703 break;
704 case CMD_NOTE:
705 leavePara();
706 enterPara(leftType: Atom::NoteLeft, rightType: Atom::NoteRight);
707 break;
708 case CMD_O:
709 location().warning(QStringLiteral("'\\o' is deprecated. Use '\\li'"));
710 Q_FALLTHROUGH();
711 case CMD_LI:
712 leavePara();
713 if (m_openedCommands.top() == CMD_LIST) {
714 if (m_openedLists.top().isStarted())
715 append(type: Atom::ListItemRight, string: m_openedLists.top().styleString());
716 else
717 append(type: Atom::ListLeft, string: m_openedLists.top().styleString());
718 m_openedLists.top().next();
719 append(type: Atom::ListItemNumber, string: m_openedLists.top().numberString());
720 append(type: Atom::ListItemLeft, string: m_openedLists.top().styleString());
721 enterPara();
722 } else if (m_openedCommands.top() == CMD_TABLE) {
723 p1 = "1,1";
724 p2.clear();
725 if (isLeftBraceAhead()) {
726 p1 = getArgument();
727 if (isLeftBraceAhead())
728 p2 = getArgument();
729 }
730
731 if (!m_inTableHeader && !m_inTableRow) {
732 location().warning(
733 QStringLiteral("Missing '\\%1' or '\\%2' before '\\%3'")
734 .arg(args: cmdName(cmd: CMD_HEADER), args: cmdName(cmd: CMD_ROW),
735 args: cmdName(cmd: CMD_LI)));
736 append(type: Atom::TableRowLeft);
737 m_inTableRow = true;
738 } else if (m_inTableItem) {
739 append(type: Atom::TableItemRight);
740 m_inTableItem = false;
741 }
742
743 append(type: Atom::TableItemLeft, p1, p2);
744 m_inTableItem = true;
745 } else
746 location().warning(
747 QStringLiteral("Command '\\%1' outside of '\\%2' and '\\%3'")
748 .arg(args: cmdName(cmd), args: cmdName(cmd: CMD_LIST), args: cmdName(cmd: CMD_TABLE)));
749 break;
750 case CMD_OMIT:
751 getUntilEnd(cmd);
752 break;
753 case CMD_OMITVALUE: {
754 leavePara();
755 p1 = getArgument();
756 if (!m_private->m_enumItemList.contains(str: p1))
757 m_private->m_enumItemList.append(t: p1);
758 if (!m_private->m_omitEnumItemList.contains(str: p1))
759 m_private->m_omitEnumItemList.append(t: p1);
760 skipSpacesOrOneEndl();
761 // Skip potential description paragraph
762 while (m_position < m_inputLength && !isBlankLine()) {
763 skipAllSpaces();
764 if (qsizetype pos = m_position; pos < m_input.size()
765 && m_input.at(i: pos++).unicode() == '\\') {
766 QString nextCmdStr;
767 while (pos < m_input.size() && m_input[pos].isLetterOrNumber())
768 nextCmdStr += m_input[pos++];
769 int nextCmd = s_utilities.cmdHash.value(key: cmdStr, defaultValue: NOT_A_CMD);
770 if (nextCmd == cmd || nextCmd == CMD_VALUE)
771 break;
772 }
773 getRestOfLine();
774 }
775 break;
776 }
777 case CMD_PRINTLINE: {
778 leavePara();
779 QString rest = getRestOfLine();
780 if (s_quoting) {
781 append(type: Atom::CodeQuoteCommand, string: cmdStr);
782 append(type: Atom::CodeQuoteArgument, string: rest);
783 }
784 appendToCode(code: m_quoter.quoteLine(docLocation: location(), command: cmdStr, pattern: rest));
785 break;
786 }
787 case CMD_PRINTTO: {
788 leavePara();
789 QString rest = getRestOfLine();
790 if (s_quoting) {
791 append(type: Atom::CodeQuoteCommand, string: cmdStr);
792 append(type: Atom::CodeQuoteArgument, string: rest);
793 }
794 appendToCode(code: m_quoter.quoteTo(docLocation: location(), command: cmdStr, pattern: rest));
795 break;
796 }
797 case CMD_PRINTUNTIL: {
798 leavePara();
799 QString rest = getRestOfLine();
800 if (s_quoting) {
801 append(type: Atom::CodeQuoteCommand, string: cmdStr);
802 append(type: Atom::CodeQuoteArgument, string: rest);
803 }
804 appendToCode(code: m_quoter.quoteUntil(docLocation: location(), command: cmdStr, pattern: rest));
805 break;
806 }
807 case CMD_QUOTATION:
808 if (openCommand(cmd)) {
809 leavePara();
810 append(type: Atom::QuotationLeft);
811 }
812 break;
813 case CMD_QUOTEFILE: {
814 leavePara();
815
816 QString fileName = getArgument();
817 quoteFromFile(filename: fileName);
818 if (s_quoting) {
819 append(type: Atom::CodeQuoteCommand, string: cmdStr);
820 append(type: Atom::CodeQuoteArgument, string: fileName);
821 }
822 append(type: Atom::Code, string: m_quoter.quoteTo(docLocation: location(), command: cmdStr, pattern: QString()));
823 m_quoter.reset();
824 break;
825 }
826 case CMD_QUOTEFROMFILE: {
827 leavePara();
828 QString arg = getArgument();
829 if (s_quoting) {
830 append(type: Atom::CodeQuoteCommand, string: cmdStr);
831 append(type: Atom::CodeQuoteArgument, string: arg);
832 }
833 quoteFromFile(filename: arg);
834 break;
835 }
836 case CMD_RAW:
837 leavePara();
838 p1 = getRestOfLine();
839 if (p1.isEmpty())
840 location().warning(QStringLiteral("Missing format name after '\\%1'")
841 .arg(a: cmdName(cmd: CMD_RAW)));
842 append(type: Atom::FormatIf, string: p1);
843 append(type: Atom::RawString, string: untabifyEtc(str: getUntilEnd(cmd)));
844 append(type: Atom::FormatElse);
845 append(type: Atom::FormatEndif);
846 break;
847 case CMD_ROW:
848 if (m_openedCommands.top() == CMD_TABLE) {
849 p1.clear();
850 if (isLeftBraceAhead())
851 p1 = getArgument(verbatim: true);
852 leaveTableRow();
853 append(type: Atom::TableRowLeft, string: p1);
854 m_inTableRow = true;
855 } else {
856 if (m_openedCommands.contains(t: CMD_TABLE))
857 location().warning(QStringLiteral("Cannot use '\\%1' within '\\%2'")
858 .arg(args: cmdName(cmd: CMD_ROW),
859 args: cmdName(cmd: m_openedCommands.top())));
860 else
861 location().warning(QStringLiteral("Cannot use '\\%1' outside of '\\%2'")
862 .arg(args: cmdName(cmd: CMD_ROW), args: cmdName(cmd: CMD_TABLE)));
863 }
864 break;
865 case CMD_SA:
866 parseAlso();
867 break;
868 case CMD_SECTION1:
869 startSection(unit: Doc::Section1, cmd);
870 break;
871 case CMD_SECTION2:
872 startSection(unit: Doc::Section2, cmd);
873 break;
874 case CMD_SECTION3:
875 startSection(unit: Doc::Section3, cmd);
876 break;
877 case CMD_SECTION4:
878 startSection(unit: Doc::Section4, cmd);
879 break;
880 case CMD_SIDEBAR:
881 if (openCommand(cmd)) {
882 leavePara();
883 append(type: Atom::SidebarLeft);
884 }
885 break;
886 case CMD_SKIPLINE: {
887 leavePara();
888 QString rest = getRestOfLine();
889 if (s_quoting) {
890 append(type: Atom::CodeQuoteCommand, string: cmdStr);
891 append(type: Atom::CodeQuoteArgument, string: rest);
892 }
893 m_quoter.quoteLine(docLocation: location(), command: cmdStr, pattern: rest);
894 break;
895 }
896 case CMD_SKIPTO: {
897 leavePara();
898 QString rest = getRestOfLine();
899 if (s_quoting) {
900 append(type: Atom::CodeQuoteCommand, string: cmdStr);
901 append(type: Atom::CodeQuoteArgument, string: rest);
902 }
903 m_quoter.quoteTo(docLocation: location(), command: cmdStr, pattern: rest);
904 break;
905 }
906 case CMD_SKIPUNTIL: {
907 leavePara();
908 QString rest = getRestOfLine();
909 if (s_quoting) {
910 append(type: Atom::CodeQuoteCommand, string: cmdStr);
911 append(type: Atom::CodeQuoteArgument, string: rest);
912 }
913 m_quoter.quoteUntil(docLocation: location(), command: cmdStr, pattern: rest);
914 break;
915 }
916 case CMD_SPAN:
917 p1 = ATOM_FORMATTING_SPAN + getArgument(verbatim: true);
918 startFormat(format: p1, cmd);
919 break;
920 case CMD_SNIPPET: {
921 leavePara();
922 QString snippet = getArgument();
923 QString identifier = getRestOfLine();
924 if (s_quoting) {
925 append(type: Atom::SnippetCommand, string: cmdStr);
926 append(type: Atom::SnippetLocation, string: snippet);
927 append(type: Atom::SnippetIdentifier, string: identifier);
928 }
929 marker = CodeMarker::markerForFileName(fileName: snippet);
930 quoteFromFile(filename: snippet);
931 appendToCode(code: m_quoter.quoteSnippet(docLocation: location(), identifier), defaultType: marker->atomType());
932 break;
933 }
934 case CMD_SUB:
935 startFormat(ATOM_FORMATTING_SUBSCRIPT, cmd);
936 break;
937 case CMD_SUP:
938 startFormat(ATOM_FORMATTING_SUPERSCRIPT, cmd);
939 break;
940 case CMD_TABLE:
941 p1 = getOptionalArgument();
942 p2 = getOptionalArgument();
943 if (openCommand(cmd)) {
944 leavePara();
945 append(type: Atom::TableLeft, p1, p2);
946 m_inTableHeader = false;
947 m_inTableRow = false;
948 m_inTableItem = false;
949 }
950 break;
951 case CMD_TABLEOFCONTENTS:
952 p1 = "1";
953 if (isLeftBraceAhead())
954 p1 = getArgument();
955 p1 += QLatin1Char(',');
956 p1 += QString::number((int)getSectioningUnit());
957 append(type: Atom::TableOfContents, string: p1);
958 break;
959 case CMD_TARGET:
960 insertTarget(target: getRestOfLine());
961 break;
962 case CMD_TT:
963 startFormat(ATOM_FORMATTING_TELETYPE, cmd);
964 break;
965 case CMD_UICONTROL:
966 startFormat(ATOM_FORMATTING_UICONTROL, cmd);
967 break;
968 case CMD_UNDERLINE:
969 startFormat(ATOM_FORMATTING_UNDERLINE, cmd);
970 break;
971 case CMD_UNICODE: {
972 enterPara();
973 p1 = getArgument();
974 bool ok;
975 uint unicodeChar = p1.toUInt(ok: &ok, base: 0);
976 if (!ok || (unicodeChar == 0x0000) || (unicodeChar > 0xFFFE))
977 location().warning(
978 QStringLiteral("Invalid Unicode character '%1' specified with '%2'")
979 .arg(args&: p1, args: cmdName(cmd: CMD_UNICODE)));
980 else
981 append(type: Atom::String, string: QChar(unicodeChar));
982 break;
983 }
984 case CMD_VALUE:
985 leaveValue();
986 if (m_openedLists.top().style() == OpenedList::Value) {
987 QString p2;
988 p1 = getArgument();
989 if (p1.startsWith(s: QLatin1String("[since "))
990 && p1.endsWith(s: QLatin1String("]"))) {
991 p2 = p1.mid(position: 7, n: p1.size() - 8);
992 p1 = getArgument();
993 }
994 if (!m_private->m_enumItemList.contains(str: p1))
995 m_private->m_enumItemList.append(t: p1);
996
997 m_openedLists.top().next();
998 append(type: Atom::ListTagLeft, ATOM_LIST_VALUE);
999 append(type: Atom::String, string: p1);
1000 append(type: Atom::ListTagRight, ATOM_LIST_VALUE);
1001 if (!p2.isEmpty()) {
1002 append(type: Atom::SinceTagLeft, ATOM_LIST_VALUE);
1003 append(type: Atom::String, string: p2);
1004 append(type: Atom::SinceTagRight, ATOM_LIST_VALUE);
1005 }
1006 append(type: Atom::ListItemLeft, ATOM_LIST_VALUE);
1007
1008 skipSpacesOrOneEndl();
1009 if (isBlankLine())
1010 append(type: Atom::Nop);
1011 } else {
1012 // ### unknown problems
1013 }
1014 break;
1015 case CMD_WARNING:
1016 leavePara();
1017 enterPara(leftType: Atom::WarningLeft, rightType: Atom::WarningRight);
1018 break;
1019 case CMD_OVERLOAD:
1020 leavePara();
1021 m_private->m_metacommandsUsed.insert(value: cmdStr);
1022 p1.clear();
1023 if (!isBlankLine())
1024 p1 = getRestOfLine();
1025 if (!p1.isEmpty()) {
1026 append(type: Atom::ParaLeft);
1027 append(type: Atom::String, string: "This function overloads ");
1028 append(type: Atom::AutoLink, string: p1);
1029 append(type: Atom::String, string: ".");
1030 append(type: Atom::ParaRight);
1031 } else {
1032 append(type: Atom::ParaLeft);
1033 append(type: Atom::String, string: "This is an overloaded function.");
1034 append(type: Atom::ParaRight);
1035 p1 = getMetaCommandArgument(cmdStr);
1036 }
1037 m_private->m_metaCommandMap[cmdStr].append(t: ArgPair(p1, QString()));
1038 break;
1039 case NOT_A_CMD:
1040 if (metaCommandSet.contains(value: cmdStr)) {
1041 QString arg;
1042 QString bracketedArg;
1043 m_private->m_metacommandsUsed.insert(value: cmdStr);
1044 if (isLeftBracketAhead())
1045 bracketedArg = getBracketedArgument();
1046 // Force a linebreak after \obsolete or \deprecated
1047 // to treat potential arguments as a new text paragraph.
1048 if (m_position < m_inputLength
1049 && (cmdStr == QLatin1String("obsolete")
1050 || cmdStr == QLatin1String("deprecated")))
1051 m_input[m_position] = '\n';
1052 else
1053 arg = getMetaCommandArgument(cmdStr);
1054 m_private->m_metaCommandMap[cmdStr].append(t: ArgPair(arg, bracketedArg));
1055 if (possibleTopics.contains(value: cmdStr)) {
1056 if (!cmdStr.endsWith(s: QLatin1String("propertygroup")))
1057 m_private->m_topics.append(t: Topic(cmdStr, arg));
1058 }
1059 } else if (s_utilities.macroHash.contains(key: cmdStr)) {
1060 const Macro &macro = s_utilities.macroHash.value(key: cmdStr);
1061 QStringList macroArgs;
1062 int numPendingFi = 0;
1063 int numFormatDefs = 0;
1064 for (auto it = macro.m_otherDefs.constBegin();
1065 it != macro.m_otherDefs.constEnd(); ++it) {
1066 if (it.key() != "match") {
1067 if (numFormatDefs == 0)
1068 macroArgs = getMacroArguments(name: cmdStr, macro);
1069 append(type: Atom::FormatIf, string: it.key());
1070 expandMacro(def: *it, args: macroArgs);
1071 ++numFormatDefs;
1072 if (it == macro.m_otherDefs.constEnd()) {
1073 append(type: Atom::FormatEndif);
1074 } else {
1075 append(type: Atom::FormatElse);
1076 ++numPendingFi;
1077 }
1078 }
1079 }
1080 while (numPendingFi-- > 0)
1081 append(type: Atom::FormatEndif);
1082
1083 if (!macro.m_defaultDef.isEmpty()) {
1084 if (numFormatDefs > 0) {
1085 macro.m_defaultDefLocation.warning(
1086 QStringLiteral("Macro cannot have both "
1087 "format-specific and qdoc-"
1088 "syntax definitions"));
1089 } else {
1090 QString expanded = expandMacroToString(name: cmdStr, macro);
1091 m_input.replace(i: m_backslashPosition,
1092 len: m_endPosition - m_backslashPosition, after: expanded);
1093 m_inputLength = m_input.size();
1094 m_position = m_backslashPosition;
1095 }
1096 }
1097 } else if (isAutoLinkString(word: cmdStr)) {
1098 appendWord(word: cmdStr);
1099 } else {
1100 if (!cmdStr.endsWith(s: "propertygroup")) {
1101 // The QML property group commands are no longer required
1102 // for grouping QML properties. They are allowed but ignored.
1103 location().warning(QStringLiteral("Unknown command '\\%1'").arg(a: cmdStr),
1104 details: detailsUnknownCommand(metaCommandSet, str: cmdStr));
1105 }
1106 enterPara();
1107 append(type: Atom::UnknownCommand, string: cmdStr);
1108 }
1109 }
1110 } // case '\\' (qdoc markup command)
1111 break;
1112 }
1113 case '-': { // Catch en-dash (--) and em-dash (---) markup here.
1114 enterPara();
1115 qsizetype dashCount = 1;
1116 ++m_position;
1117
1118 // Figure out how many hyphens in a row.
1119 while ((m_position < m_inputLength) && (m_input.at(i: m_position) == '-')) {
1120 ++dashCount;
1121 ++m_position;
1122 }
1123
1124 if (dashCount == 3) {
1125 // 3 hyphens, append an em-dash character.
1126 const QChar emDash(8212);
1127 appendChar(ch: emDash);
1128 } else if (dashCount == 2) {
1129 // 2 hyphens; append an en-dash character.
1130 const QChar enDash(8211);
1131 appendChar(ch: enDash);
1132 } else {
1133 // dashCount is either one or more than three. Append a hyphen
1134 // the appropriate number of times. This ensures '----' doesn't
1135 // end up as an em-dash followed by a hyphen in the output.
1136 for (qsizetype i = 0; i < dashCount; ++i)
1137 appendChar(ch: '-');
1138 }
1139 break;
1140 }
1141 case '{':
1142 enterPara();
1143 appendChar(ch: '{');
1144 ++m_braceDepth;
1145 ++m_position;
1146 break;
1147 case '}': {
1148 --m_braceDepth;
1149 ++m_position;
1150
1151 auto format = m_pendingFormats.find(key: m_braceDepth);
1152 if (format == m_pendingFormats.end()) {
1153 enterPara();
1154 appendChar(ch: '}');
1155 } else {
1156 append(type: Atom::FormattingRight, string: *format);
1157 if (*format == ATOM_FORMATTING_INDEX) {
1158 if (m_indexStartedParagraph)
1159 skipAllSpaces();
1160 } else if (*format == ATOM_FORMATTING_LINK) {
1161 // hack for C++ to support links like
1162 // \l{QString::}{count()}
1163 if (currentLinkAtom && currentLinkAtom->string().endsWith(s: "::")) {
1164 QString suffix =
1165 Text::subText(begin: currentLinkAtom, end: m_private->m_text.lastAtom())
1166 .toString();
1167 currentLinkAtom->appendString(string: suffix);
1168 }
1169 currentLinkAtom = nullptr;
1170 }
1171 m_pendingFormats.erase(it: format);
1172 }
1173 break;
1174 }
1175 // Do not parse content after '//!' comments
1176 case '/': {
1177 if (m_position + 2 < m_inputLength)
1178 if (m_input.at(i: m_position + 1) == '/')
1179 if (m_input.at(i: m_position + 2) == '!') {
1180 m_position += 2;
1181 getRestOfLine();
1182 if (m_input.at(i: m_position - 1) == '\n')
1183 --m_position;
1184 break;
1185 }
1186 Q_FALLTHROUGH(); // fall through
1187 }
1188 default: {
1189 bool newWord;
1190 switch (m_private->m_text.lastAtom()->type()) {
1191 case Atom::ParaLeft:
1192 newWord = true;
1193 break;
1194 default:
1195 newWord = false;
1196 }
1197
1198 if (m_paragraphState == OutsideParagraph) {
1199 if (ch.isSpace()) {
1200 ++m_position;
1201 newWord = false;
1202 } else {
1203 enterPara();
1204 newWord = true;
1205 }
1206 } else {
1207 if (ch.isSpace()) {
1208 ++m_position;
1209 if ((ch == '\n')
1210 && (m_paragraphState == InSingleLineParagraph || isBlankLine())) {
1211 leavePara();
1212 newWord = false;
1213 } else {
1214 appendChar(ch: ' ');
1215 newWord = true;
1216 }
1217 } else {
1218 newWord = true;
1219 }
1220 }
1221
1222 if (newWord) {
1223 qsizetype startPos = m_position;
1224 // No auto-linking inside links
1225 bool autolink = (!m_pendingFormats.isEmpty() &&
1226 m_pendingFormats.last() == ATOM_FORMATTING_LINK) ?
1227 false : isAutoLinkString(word: m_input, curPos&: m_position);
1228 if (m_position == startPos) {
1229 if (!ch.isSpace()) {
1230 appendChar(ch);
1231 ++m_position;
1232 }
1233 } else {
1234 QString word = m_input.mid(position: startPos, n: m_position - startPos);
1235 if (autolink) {
1236 if (s_ignoreWords.contains(str: word) || word.startsWith(s: QString("__")))
1237 appendWord(word);
1238 else
1239 append(type: Atom::AutoLink, string: word);
1240 } else {
1241 appendWord(word);
1242 }
1243 }
1244 }
1245 } // default:
1246 } // switch (ch.unicode())
1247 }
1248 leaveValueList();
1249
1250 // for compatibility
1251 if (m_openedCommands.top() == CMD_LEGALESE) {
1252 append(type: Atom::LegaleseRight);
1253 m_openedCommands.pop();
1254 }
1255
1256 if (m_openedCommands.top() != CMD_OMIT) {
1257 location().warning(
1258 QStringLiteral("Missing '\\%1'").arg(a: endCmdName(cmd: m_openedCommands.top())));
1259 } else if (preprocessorSkipping.size() > 0) {
1260 location().warning(QStringLiteral("Missing '\\%1'").arg(a: cmdName(cmd: CMD_ENDIF)));
1261 }
1262
1263 if (m_currentSection > Doc::NoSection) {
1264 append(type: Atom::SectionRight, string: QString::number(m_currentSection));
1265 m_currentSection = Doc::NoSection;
1266 }
1267
1268 m_private->m_text.stripFirstAtom();
1269}
1270
1271/*!
1272 Returns the current location.
1273 */
1274Location &DocParser::location()
1275{
1276 while (!m_openedInputs.isEmpty() && m_openedInputs.top() <= m_position) {
1277 m_cachedLocation.pop();
1278 m_cachedPosition = m_openedInputs.pop();
1279 }
1280 while (m_cachedPosition < m_position)
1281 m_cachedLocation.advance(ch: m_input.at(i: m_cachedPosition++));
1282 return m_cachedLocation;
1283}
1284
1285QString DocParser::detailsUnknownCommand(const QSet<QString> &metaCommandSet, const QString &str)
1286{
1287 QSet<QString> commandSet = metaCommandSet;
1288 int i = 0;
1289 while (cmds[i].name != nullptr) {
1290 commandSet.insert(value: cmds[i].name);
1291 ++i;
1292 }
1293
1294 QString best = nearestName(actual: str, candidates: commandSet);
1295 if (best.isEmpty())
1296 return QString();
1297 return QStringLiteral("Maybe you meant '\\%1'?").arg(a: best);
1298}
1299
1300/*!
1301 \internal
1302
1303 Issues a warning about the duplicate definition of a target or keyword in
1304 at \a location. \a duplicateDefinition is the target being processed; the
1305 already registered definition is \a previousDefinition.
1306 */
1307static void warnAboutPreexistingTarget(const Location &location, const QString &duplicateDefinition, const QString &previousDefinition)
1308{
1309 location.warning(
1310 QStringLiteral("Duplicate target name '%1'. The previous occurrence is here: %2")
1311 .arg(args: duplicateDefinition, args: previousDefinition));
1312}
1313
1314/*!
1315 \internal
1316
1317 \brief Registers \a target as a linkable entity.
1318
1319 The main purpose of this method is to register a target as defined
1320 along with its location, so that becomes a valid link target from other
1321 parts of the documentation.
1322
1323 If the \a target name is already registered, a warning is issued,
1324 as multiple definitions are problematic.
1325
1326 \sa insertKeyword, target-command
1327 */
1328void DocParser::insertTarget(const QString &target)
1329{
1330 if (m_targetMap.contains(key: target))
1331 return warnAboutPreexistingTarget(location: location(), duplicateDefinition: target, previousDefinition: m_targetMap[target].toString());
1332
1333 m_targetMap.insert(key: target, value: location());
1334 m_private->constructExtra();
1335
1336 append(type: Atom::Target, string: target);
1337 m_private->extra->m_targets.append(t: m_private->m_text.lastAtom());
1338}
1339
1340/*!
1341 \internal
1342
1343 \brief Registers \a keyword as a linkable entity.
1344
1345 The main purpose of this method is to register a keyword as defined
1346 along with its location, so that becomes a valid link target from other
1347 parts of the documentation.
1348
1349 If the \a keyword name is already registered, a warning is issued,
1350 as multiple definitions are problematic.
1351
1352 \sa insertTarget, keyword-command
1353 */
1354void DocParser::insertKeyword(const QString &keyword)
1355{
1356 if (m_targetMap.contains(key: keyword))
1357 return warnAboutPreexistingTarget(location: location(), duplicateDefinition: keyword, previousDefinition: m_targetMap[keyword].toString());
1358
1359 m_targetMap.insert(key: keyword, value: location());
1360 m_private->constructExtra();
1361
1362 append(type: Atom::Keyword, string: keyword);
1363 m_private->extra->m_keywords.append(t: m_private->m_text.lastAtom());
1364}
1365
1366void DocParser::include(const QString &fileName, const QString &identifier, const QStringList &parameters)
1367{
1368 if (location().depth() > 16)
1369 location().fatal(QStringLiteral("Too many nested '\\%1's").arg(a: cmdName(cmd: CMD_INCLUDE)));
1370 QString filePath = Config::instance().getIncludeFilePath(fileName);
1371 if (filePath.isEmpty()) {
1372 location().warning(QStringLiteral("Cannot find qdoc include file '%1'").arg(a: fileName));
1373 } else {
1374 QFile inFile(filePath);
1375 if (!inFile.open(flags: QFile::ReadOnly)) {
1376 location().warning(
1377 QStringLiteral("Cannot open qdoc include file '%1'").arg(a: filePath));
1378 } else {
1379 location().push(filePath: fileName);
1380 QTextStream inStream(&inFile);
1381 QString includedContent = inStream.readAll();
1382 inFile.close();
1383
1384 if (identifier.isEmpty()) {
1385 expandArgumentsInString(str&: includedContent, args: parameters);
1386 m_input.insert(i: m_position, s: includedContent);
1387 m_inputLength = m_input.size();
1388 m_openedInputs.push(t: m_position + includedContent.size());
1389 } else {
1390 QStringList lineBuffer = includedContent.split(sep: QLatin1Char('\n'));
1391 qsizetype bufLen{lineBuffer.size()};
1392 qsizetype i;
1393 QStringView trimmedLine;
1394 for (i = 0; i < bufLen; ++i) {
1395 trimmedLine = QStringView{lineBuffer[i]}.trimmed();
1396 if (trimmedLine.startsWith(s: QLatin1String("//!")) &&
1397 trimmedLine.contains(s: identifier))
1398 break;
1399 }
1400 if (i < bufLen - 1) {
1401 ++i;
1402 } else {
1403 location().warning(
1404 QStringLiteral("Cannot find '%1' in '%2'").arg(args: identifier, args&: filePath));
1405 return;
1406 }
1407 QString result;
1408 do {
1409 trimmedLine = QStringView{lineBuffer[i]}.trimmed();
1410 if (trimmedLine.startsWith(s: QLatin1String("//!")) &&
1411 trimmedLine.contains(s: identifier))
1412 break;
1413 else
1414 result += lineBuffer[i] + QLatin1Char('\n');
1415 ++i;
1416 } while (i < bufLen);
1417
1418 expandArgumentsInString(str&: result, args: parameters);
1419 if (result.isEmpty()) {
1420 location().warning(QStringLiteral("Empty qdoc snippet '%1' in '%2'")
1421 .arg(args: identifier, args&: filePath));
1422 } else {
1423 m_input.insert(i: m_position, s: result);
1424 m_inputLength = m_input.size();
1425 m_openedInputs.push(t: m_position + result.size());
1426 }
1427 }
1428 }
1429 }
1430}
1431
1432void DocParser::startFormat(const QString &format, int cmd)
1433{
1434 enterPara();
1435
1436 for (const auto &item : std::as_const(t&: m_pendingFormats)) {
1437 if (item == format) {
1438 location().warning(QStringLiteral("Cannot nest '\\%1' commands").arg(a: cmdName(cmd)));
1439 return;
1440 }
1441 }
1442
1443 append(type: Atom::FormattingLeft, string: format);
1444
1445 if (isLeftBraceAhead()) {
1446 skipSpacesOrOneEndl();
1447 m_pendingFormats.insert(key: m_braceDepth, value: format);
1448 ++m_braceDepth;
1449 ++m_position;
1450 } else {
1451 append(type: Atom::String, string: getArgument());
1452 append(type: Atom::FormattingRight, string: format);
1453 if (format == ATOM_FORMATTING_INDEX && m_indexStartedParagraph) {
1454 skipAllSpaces();
1455 m_indexStartedParagraph = false;
1456 }
1457 }
1458}
1459
1460bool DocParser::openCommand(int cmd)
1461{
1462 int outer = m_openedCommands.top();
1463 bool ok = true;
1464
1465 if (cmd != CMD_LINK) {
1466 if (outer == CMD_LIST) {
1467 ok = (cmd == CMD_FOOTNOTE || cmd == CMD_LIST);
1468 } else if (outer == CMD_SIDEBAR) {
1469 ok = (cmd == CMD_LIST || cmd == CMD_QUOTATION || cmd == CMD_SIDEBAR);
1470 } else if (outer == CMD_QUOTATION) {
1471 ok = (cmd == CMD_LIST);
1472 } else if (outer == CMD_TABLE) {
1473 ok = (cmd == CMD_LIST || cmd == CMD_FOOTNOTE || cmd == CMD_QUOTATION);
1474 } else if (outer == CMD_FOOTNOTE || outer == CMD_LINK) {
1475 ok = false;
1476 }
1477 }
1478
1479 if (ok) {
1480 m_openedCommands.push(t: cmd);
1481 } else {
1482 location().warning(
1483 QStringLiteral("Can't use '\\%1' in '\\%2'").arg(args: cmdName(cmd), args: cmdName(cmd: outer)));
1484 }
1485 return ok;
1486}
1487
1488/*!
1489 Returns \c true if \a word qualifies for auto-linking.
1490
1491 A word qualifies for auto-linking if either:
1492
1493 \list
1494 \li It is composed of only upper and lowercase characters
1495 \li AND It contains at least one uppercase character that is not
1496 the first character of word
1497 \li AND it contains at least two lowercase characters
1498 \endlist
1499
1500 Or
1501
1502 \list
1503 \li It is composed only of uppercase characters, lowercase
1504 characters, characters in [_@] and the \c {"::"} sequence.
1505 \li It contains at least one uppercase character that is not
1506 the first character of word or it contains at least one
1507 lowercase character
1508 \li AND it contains at least one character in [_@] or it
1509 contains at least one \c {"::"} sequence.
1510 \endlist
1511
1512 Inserting or suffixing, but not prefixing, any sequence in [0-9]+
1513 in a word that qualifies for auto-linking by the above rules
1514 preserves the auto-linkability of the word.
1515
1516 Suffixing the sequence \c {"()"} to a word that qualifies for
1517 auto-linking by the above rules preserves the auto-linkability of
1518 a word.
1519
1520 FInally, a word qualifies for auto-linking if:
1521
1522 \list
1523 \li It is composed of only uppercase characters, lowercase
1524 characters and the sequence \c {"()"}
1525 \li AND it contains one lowercase character and a sequence of zero, one
1526 or two upper or lowercase characters
1527 \li AND it contains exactly one sequence \c {"()"}
1528 \li AND it contains one sequence \c {"()"} as the last two
1529 characters of word
1530 \endlist
1531
1532 For example, \c {"fOo"}, \c {"FooBar"} and \c {"foobaR"} qualify
1533 for auto-linking by the first rule.
1534
1535 \c {"QT_DEBUG"}, \c {"::Qt"} and \c {"std::move"} qualifies for
1536 auto-linking by the second rule.
1537
1538 \c {"SIMDVector256"} qualifies by suffixing \c {"SIMDVector"},
1539 which qualifies by the first rule, with the sequence \c {"256"}
1540
1541 \c {"FooBar::Bar()"} qualifies by suffixing \c {"FooBar::Bar"},
1542 which qualifies by the first and second rule, with the sequence \c
1543 {"()"}.
1544
1545 \c {"Foo()"} and \c {"a()"} qualifies by the last rule.
1546
1547 Instead, \c {"Q"}, \c {"flower"}, \c {"_"} and \c {"()"} do not
1548 qualify for auto-linking.
1549
1550 The rules are intended as a heuristic to catch common cases in the
1551 Qt documentation where a word might represent an important
1552 documented element such as a class or a method that could be
1553 linked to while at the same time avoiding catching common words
1554 such as \c {"A"} or \c {"Nonetheless"}.
1555
1556 The heuristic assumes that Qt's codebase respects a style where
1557 camelCasing is the standard for most of the elements, a function
1558 call is identified by the use of parenthesis and certain elements,
1559 such as macros, might be fully uppercase.
1560
1561 Furthemore, it assumes that the Qt codebase is written in a
1562 language that has an identifier grammar similar to the one for
1563 C++.
1564*/
1565inline bool DocParser::isAutoLinkString(const QString &word)
1566{
1567 qsizetype start = 0;
1568 return isAutoLinkString(word, curPos&: start) && (start == word.size());
1569}
1570
1571/*!
1572 Returns \c true if a prefix of a substring of \a word qualifies
1573 for auto-linking.
1574
1575 Respects the same parsing rules as the unary overload.
1576
1577 \a curPos defines the offset, from the first character of \ word,
1578 at which the parsed substring starts.
1579
1580 When the call completes, \a curPos represents the offset, from the
1581 first character of word, that is the successor of the offset of
1582 the last parsed character.
1583
1584 If the return value of the call is \c true, it is guaranteed that
1585 the prefix of the substring of \word that contains the characters
1586 from the initial value of \a curPos and up to but not including \a
1587 curPos qualifies for auto-linking.
1588
1589 If \a curPos is initially zero, the considered substring is the
1590 entirety of \a word.
1591*/
1592bool DocParser::isAutoLinkString(const QString &word, qsizetype &curPos)
1593{
1594 qsizetype len = word.size();
1595 qsizetype startPos = curPos;
1596 int numUppercase = 0;
1597 int numLowercase = 0;
1598 int numStrangeSymbols = 0;
1599
1600 while (curPos < len) {
1601 unsigned char latin1Ch = word.at(i: curPos).toLatin1();
1602 if (islower(latin1Ch)) {
1603 ++numLowercase;
1604 ++curPos;
1605 } else if (isupper(latin1Ch)) {
1606 if (curPos > startPos)
1607 ++numUppercase;
1608 ++curPos;
1609 } else if (isdigit(latin1Ch)) {
1610 if (curPos > startPos)
1611 ++curPos;
1612 else
1613 break;
1614 } else if (latin1Ch == '_' || latin1Ch == '@') {
1615 ++numStrangeSymbols;
1616 ++curPos;
1617 } else if ((latin1Ch == ':') && (curPos < len - 1)
1618 && (word.at(i: curPos + 1) == QLatin1Char(':'))) {
1619 ++numStrangeSymbols;
1620 curPos += 2;
1621 } else if (latin1Ch == '(') {
1622 if ((curPos < len - 1) && (word.at(i: curPos + 1) == QLatin1Char(')'))) {
1623 ++numStrangeSymbols;
1624 m_position += 2;
1625 }
1626
1627 break;
1628 } else {
1629 break;
1630 }
1631 }
1632
1633 return ((numUppercase >= 1 && numLowercase >= 2) || (numStrangeSymbols > 0 && (numUppercase + numLowercase >= 1)));
1634}
1635
1636bool DocParser::closeCommand(int endCmd)
1637{
1638 if (endCmdFor(cmd: m_openedCommands.top()) == endCmd && m_openedCommands.size() > 1) {
1639 m_openedCommands.pop();
1640 return true;
1641 } else {
1642 bool contains = false;
1643 QStack<int> opened2 = m_openedCommands;
1644 while (opened2.size() > 1) {
1645 if (endCmdFor(cmd: opened2.top()) == endCmd) {
1646 contains = true;
1647 break;
1648 }
1649 opened2.pop();
1650 }
1651
1652 if (contains) {
1653 while (endCmdFor(cmd: m_openedCommands.top()) != endCmd && m_openedCommands.size() > 1) {
1654 location().warning(
1655 QStringLiteral("Missing '\\%1' before '\\%2'")
1656 .arg(args: endCmdName(cmd: m_openedCommands.top()), args: cmdName(cmd: endCmd)));
1657 m_openedCommands.pop();
1658 }
1659 } else {
1660 location().warning(QStringLiteral("Unexpected '\\%1'").arg(a: cmdName(cmd: endCmd)));
1661 }
1662 return false;
1663 }
1664}
1665
1666void DocParser::startSection(Doc::Sections unit, int cmd)
1667{
1668 leaveValueList();
1669
1670 if (m_currentSection == Doc::NoSection) {
1671 m_currentSection = (Doc::Sections)(unit);
1672 m_private->constructExtra();
1673 } else
1674 endSection(unit, endCmd: cmd);
1675
1676 append(type: Atom::SectionLeft, string: QString::number(unit));
1677 m_private->constructExtra();
1678 m_private->extra->m_tableOfContents.append(t: m_private->m_text.lastAtom());
1679 m_private->extra->m_tableOfContentsLevels.append(t: unit);
1680 enterPara(leftType: Atom::SectionHeadingLeft, rightType: Atom::SectionHeadingRight, string: QString::number(unit));
1681 m_currentSection = unit;
1682}
1683
1684void DocParser::endSection(int, int) // (int unit, int endCmd)
1685{
1686 leavePara();
1687 append(type: Atom::SectionRight, string: QString::number(m_currentSection));
1688 m_currentSection = (Doc::NoSection);
1689}
1690
1691/*!
1692 \internal
1693 \brief Parses arguments to QDoc's see also command.
1694
1695 Parses space or comma separated arguments passed to the \\sa command.
1696 Multi-line input requires that the arguments are comma separated. Wrap
1697 arguments in curly braces for multi-word targets, and for scope resolution
1698 (for example, {QString::}{count()}).
1699
1700 This method updates the list of links for the See also section.
1701
1702 \sa {DocPrivate::}{addAlso()}, getArgument()
1703 */
1704void DocParser::parseAlso()
1705{
1706 auto line_comment = [this]() -> bool {
1707 skipSpacesOnLine();
1708 if (m_position + 2 > m_inputLength)
1709 return false;
1710 if (m_input[m_position].unicode() == '/') {
1711 if (m_input[m_position + 1].unicode() == '/') {
1712 if (m_input[m_position + 2].unicode() == '!') {
1713 return true;
1714 }
1715 }
1716 }
1717 return false;
1718 };
1719
1720 auto skip_everything_until_newline = [this]() -> void {
1721 while (m_position < m_inputLength && m_input[m_position] != '\n')
1722 ++m_position;
1723 };
1724
1725 leavePara();
1726 skipSpacesOnLine();
1727 while (m_position < m_inputLength && m_input[m_position] != '\n') {
1728 QString target;
1729 QString str;
1730 bool skipMe = false;
1731
1732 if (m_input[m_position] == '{') {
1733 target = getArgument();
1734 skipSpacesOnLine();
1735 if (m_position < m_inputLength && m_input[m_position] == '{') {
1736 str = getArgument();
1737
1738 // hack for C++ to support links like \l{QString::}{count()}
1739 if (target.endsWith(s: "::"))
1740 target += str;
1741 } else {
1742 str = target;
1743 }
1744 } else {
1745 target = getArgument();
1746 str = cleanLink(link: target);
1747 if (target == QLatin1String("and") || target == QLatin1String("."))
1748 skipMe = true;
1749 }
1750
1751 if (!skipMe) {
1752 Text also;
1753 also << Atom(Atom::Link, target) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1754 << str << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1755 m_private->addAlso(also);
1756 }
1757
1758 skipSpacesOnLine();
1759
1760 if (line_comment())
1761 skip_everything_until_newline();
1762
1763 if (m_position < m_inputLength && m_input[m_position] == ',') {
1764 m_position++;
1765 if (line_comment())
1766 skip_everything_until_newline();
1767 skipSpacesOrOneEndl();
1768 } else if (m_position >= m_inputLength || m_input[m_position] != '\n') {
1769 location().warning(QStringLiteral("Missing comma in '\\%1'").arg(a: cmdName(cmd: CMD_SA)));
1770 }
1771 }
1772}
1773
1774void DocParser::append(Atom::AtomType type, const QString &string)
1775{
1776 Atom::AtomType lastType = m_private->m_text.lastAtom()->type();
1777 if ((lastType == Atom::Code)
1778 && m_private->m_text.lastAtom()->string().endsWith(s: QLatin1String("\n\n")))
1779 m_private->m_text.lastAtom()->chopString();
1780 m_private->m_text << Atom(type, string);
1781}
1782
1783void DocParser::append(const QString &string)
1784{
1785 Atom::AtomType lastType = m_private->m_text.lastAtom()->type();
1786 if ((lastType == Atom::Code)
1787 && m_private->m_text.lastAtom()->string().endsWith(s: QLatin1String("\n\n")))
1788 m_private->m_text.lastAtom()->chopString();
1789 m_private->m_text << Atom(Atom::Link, string);
1790}
1791
1792void DocParser::append(Atom::AtomType type, const QString &p1, const QString &p2)
1793{
1794 Atom::AtomType lastType = m_private->m_text.lastAtom()->type();
1795 if ((lastType == Atom::Code)
1796 && m_private->m_text.lastAtom()->string().endsWith(s: QLatin1String("\n\n")))
1797 m_private->m_text.lastAtom()->chopString();
1798 m_private->m_text << Atom(type, p1, p2);
1799}
1800
1801void DocParser::append(const QString &p1, const QString &p2)
1802{
1803 Atom::AtomType lastType = m_private->m_text.lastAtom()->type();
1804 if ((lastType == Atom::Code)
1805 && m_private->m_text.lastAtom()->string().endsWith(s: QLatin1String("\n\n")))
1806 m_private->m_text.lastAtom()->chopString();
1807 if (p2.isEmpty())
1808 m_private->m_text << Atom(Atom::Link, p1);
1809 else
1810 m_private->m_text << LinkAtom(p1, p2);
1811}
1812
1813void DocParser::appendChar(QChar ch)
1814{
1815 if (m_private->m_text.lastAtom()->type() != Atom::String)
1816 append(type: Atom::String);
1817 Atom *atom = m_private->m_text.lastAtom();
1818 if (ch == QLatin1Char(' ')) {
1819 if (!atom->string().endsWith(c: QLatin1Char(' ')))
1820 atom->appendChar(ch: QLatin1Char(' '));
1821 } else
1822 atom->appendChar(ch);
1823}
1824
1825void DocParser::appendWord(const QString &word)
1826{
1827 if (m_private->m_text.lastAtom()->type() != Atom::String) {
1828 append(type: Atom::String, string: word);
1829 } else
1830 m_private->m_text.lastAtom()->appendString(string: word);
1831}
1832
1833void DocParser::appendToCode(const QString &markedCode)
1834{
1835 if (!isCode(atom: m_lastAtom)) {
1836 append(type: Atom::Code);
1837 m_lastAtom = m_private->m_text.lastAtom();
1838 }
1839 m_lastAtom->appendString(string: markedCode);
1840}
1841
1842void DocParser::appendToCode(const QString &markedCode, Atom::AtomType defaultType)
1843{
1844 if (!isCode(atom: m_lastAtom)) {
1845 append(type: defaultType, string: markedCode);
1846 m_lastAtom = m_private->m_text.lastAtom();
1847 } else {
1848 m_lastAtom->appendString(string: markedCode);
1849 }
1850}
1851
1852void DocParser::enterPara(Atom::AtomType leftType, Atom::AtomType rightType, const QString &string)
1853{
1854 if (m_paragraphState != OutsideParagraph)
1855 return;
1856
1857 if ((m_private->m_text.lastAtom()->type() != Atom::ListItemLeft)
1858 && (m_private->m_text.lastAtom()->type() != Atom::DivLeft)
1859 && (m_private->m_text.lastAtom()->type() != Atom::DetailsLeft)) {
1860 leaveValueList();
1861 }
1862
1863 append(type: leftType, string);
1864 m_indexStartedParagraph = false;
1865 m_pendingParagraphLeftType = leftType;
1866 m_pendingParagraphRightType = rightType;
1867 m_pendingParagraphString = string;
1868 if (leftType == Atom::SectionHeadingLeft) {
1869 m_paragraphState = InSingleLineParagraph;
1870 } else {
1871 m_paragraphState = InMultiLineParagraph;
1872 }
1873 skipSpacesOrOneEndl();
1874}
1875
1876void DocParser::leavePara()
1877{
1878 if (m_paragraphState == OutsideParagraph)
1879 return;
1880
1881 if (!m_pendingFormats.isEmpty()) {
1882 location().warning(QStringLiteral("Missing '}'"));
1883 m_pendingFormats.clear();
1884 }
1885
1886 if (m_private->m_text.lastAtom()->type() == m_pendingParagraphLeftType) {
1887 m_private->m_text.stripLastAtom();
1888 } else {
1889 if (m_private->m_text.lastAtom()->type() == Atom::String
1890 && m_private->m_text.lastAtom()->string().endsWith(c: QLatin1Char(' '))) {
1891 m_private->m_text.lastAtom()->chopString();
1892 }
1893 append(type: m_pendingParagraphRightType, string: m_pendingParagraphString);
1894 }
1895 m_paragraphState = OutsideParagraph;
1896 m_indexStartedParagraph = false;
1897 m_pendingParagraphRightType = Atom::Nop;
1898 m_pendingParagraphString.clear();
1899}
1900
1901void DocParser::leaveValue()
1902{
1903 leavePara();
1904 if (m_openedLists.isEmpty()) {
1905 m_openedLists.push(t: OpenedList(OpenedList::Value));
1906 append(type: Atom::ListLeft, ATOM_LIST_VALUE);
1907 } else {
1908 if (m_private->m_text.lastAtom()->type() == Atom::Nop)
1909 m_private->m_text.stripLastAtom();
1910 append(type: Atom::ListItemRight, ATOM_LIST_VALUE);
1911 }
1912}
1913
1914void DocParser::leaveValueList()
1915{
1916 leavePara();
1917 if (!m_openedLists.isEmpty() && (m_openedLists.top().style() == OpenedList::Value)) {
1918 if (m_private->m_text.lastAtom()->type() == Atom::Nop)
1919 m_private->m_text.stripLastAtom();
1920 append(type: Atom::ListItemRight, ATOM_LIST_VALUE);
1921 append(type: Atom::ListRight, ATOM_LIST_VALUE);
1922 m_openedLists.pop();
1923 }
1924}
1925
1926void DocParser::leaveTableRow()
1927{
1928 if (m_inTableItem) {
1929 leavePara();
1930 append(type: Atom::TableItemRight);
1931 m_inTableItem = false;
1932 }
1933 if (m_inTableHeader) {
1934 append(type: Atom::TableHeaderRight);
1935 m_inTableHeader = false;
1936 }
1937 if (m_inTableRow) {
1938 append(type: Atom::TableRowRight);
1939 m_inTableRow = false;
1940 }
1941}
1942
1943void DocParser::quoteFromFile(const QString &filename)
1944{
1945 // KLUDGE: We dereference file_resolver as it is temporarily a pointer.
1946 // See the comment for file_resolver in the header files for more context.
1947 //
1948 // We spefically dereference it, instead of using the arrow
1949 // operator, to better represent that we do not consider this as
1950 // an actual pointer, as it should not be.
1951 //
1952 // Do note that we are considering it informally safe to
1953 // dereference the pointer, as we expect it to always hold a value
1954 // at this point, but actual enforcement of this appears nowhere
1955 // in the codebase.
1956 auto maybe_resolved_file{(*file_resolver).resolve(filename)};
1957 if (!maybe_resolved_file) {
1958 // TODO: [uncentralized-admonition][failed-resolve-file]
1959 // This warning is required in multiple places.
1960 // To ensure the consistency of the warning and avoid
1961 // duplicating code everywhere, provide a centralized effort
1962 // where the warning message can be generated (but not
1963 // issued).
1964 // The current format is based on what was used before, review
1965 // it when it is moved out.
1966 QString details = std::transform_reduce(
1967 first: (*file_resolver).get_search_directories().cbegin(),
1968 last: (*file_resolver).get_search_directories().cend(),
1969 init: u"Searched directories:"_s,
1970 binary_op: std::plus(),
1971 unary_op: [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
1972 );
1973
1974 location().warning(message: u"Cannot find file to quote from: %1"_s.arg(a: filename), details);
1975
1976 // REMARK: The following is duplicated from
1977 // Doc::quoteFromFile. If, for some reason (such as a file
1978 // that is inaccessible), the quoting fails but, previously,
1979 // the logic duplicated here was still run.
1980 // This is not true anymore as quoteFromFile does require a
1981 // resolved file to be run now.
1982 // It is not entirely clear if this is required for the
1983 // semantics of DocParser to be preserved, but for the sake of
1984 // avoiding premature breakages this was retained.
1985 // Do note that this should be considered temporary as the
1986 // quoter state, if any will be preserved, should not be
1987 // managed in such a spread and unlocal way.
1988 m_quoter.reset();
1989
1990 CodeMarker *marker = CodeMarker::markerForFileName(fileName: QString{});
1991 m_quoter.quoteFromFile(userFriendlyFileName: filename, plainCode: QString{}, markedCode: marker->markedUpCode(code: QString{}, nullptr, location()));
1992 } else Doc::quoteFromFile(location: location(), quoter&: m_quoter, resolved_file: *maybe_resolved_file);
1993}
1994
1995/*!
1996 Expands a macro in-place in input.
1997
1998 Expects the current \e pos in the input to point to a backslash, and the macro to have a
1999 default definition. Format-specific macros are currently not expanded.
2000
2001 \note In addition to macros, a valid use for a backslash in an argument include
2002 escaping non-alnum characters, and splitting a single argument across multiple
2003 lines by escaping newlines. Escaping is also handled here.
2004
2005 Returns \c true on successful macro expansion.
2006 */
2007bool DocParser::expandMacro()
2008{
2009 Q_ASSERT(m_input[m_position].unicode() == '\\');
2010
2011 QString cmdStr;
2012 qsizetype backslashPos = m_position++;
2013 while (m_position < m_input.size() && m_input[m_position].isLetterOrNumber())
2014 cmdStr += m_input[m_position++];
2015
2016 m_endPosition = m_position;
2017 if (!cmdStr.isEmpty()) {
2018 if (s_utilities.macroHash.contains(key: cmdStr)) {
2019 const Macro &macro = s_utilities.macroHash.value(key: cmdStr);
2020 if (!macro.m_defaultDef.isEmpty()) {
2021 QString expanded = expandMacroToString(name: cmdStr, macro);
2022 m_input.replace(i: backslashPos, len: m_position - backslashPos, after: expanded);
2023 m_inputLength = m_input.size();
2024 m_position = backslashPos;
2025 return true;
2026 } else {
2027 location().warning(QStringLiteral("Macro '%1' does not have a default definition")
2028 .arg(a: cmdStr));
2029 }
2030 } else {
2031 location().warning(QStringLiteral("Unknown macro '%1'").arg(a: cmdStr));
2032 m_position = ++backslashPos;
2033 }
2034 } else if (m_input[m_position].isSpace()) {
2035 skipAllSpaces();
2036 } else if (m_input[m_position].unicode() == '\\') {
2037 // allow escaping a backslash
2038 m_input.remove(i: m_position--, len: 1);
2039 --m_inputLength;
2040 }
2041 return false;
2042}
2043
2044void DocParser::expandMacro(const QString &def, const QStringList &args)
2045{
2046 if (args.isEmpty()) {
2047 append(type: Atom::RawString, string: def);
2048 } else {
2049 QString rawString;
2050
2051 for (int j = 0; j < def.size(); ++j) {
2052 if (int paramNo = def[j].unicode(); paramNo >= 1 && paramNo <= args.length()) {
2053 if (!rawString.isEmpty()) {
2054 append(type: Atom::RawString, string: rawString);
2055 rawString.clear();
2056 }
2057 append(type: Atom::String, string: args[paramNo - 1]);
2058 } else {
2059 rawString += def[j];
2060 }
2061 }
2062 if (!rawString.isEmpty())
2063 append(type: Atom::RawString, string: rawString);
2064 }
2065}
2066
2067QString DocParser::expandMacroToString(const QString &name, const Macro &macro)
2068{
2069 const QString &def{macro.m_defaultDef};
2070 QString rawString;
2071
2072 if (macro.numParams == 0) {
2073 rawString = macro.m_defaultDef;
2074 } else {
2075 QStringList args{getMacroArguments(name, macro)};
2076
2077 for (int j = 0; j < def.size(); ++j) {
2078 int paramNo = def[j].unicode();
2079 rawString += (paramNo >= 1 && paramNo <= args.length()) ? args[paramNo - 1] : def[j];
2080 }
2081 }
2082 QString matchExpr{macro.m_otherDefs.value(key: "match")};
2083 if (matchExpr.isEmpty())
2084 return rawString;
2085
2086 QString result;
2087 QRegularExpression re(matchExpr);
2088 int capStart = (re.captureCount() > 0) ? 1 : 0;
2089 qsizetype i = 0;
2090 QRegularExpressionMatch match;
2091 while ((match = re.match(subject: rawString, offset: i)).hasMatch()) {
2092 for (int c = capStart; c <= re.captureCount(); ++c)
2093 result += match.captured(nth: c);
2094 i = match.capturedEnd();
2095 }
2096
2097 return result;
2098}
2099
2100Doc::Sections DocParser::getSectioningUnit()
2101{
2102 QString name = getOptionalArgument();
2103
2104 if (name == "section1") {
2105 return Doc::Section1;
2106 } else if (name == "section2") {
2107 return Doc::Section2;
2108 } else if (name == "section3") {
2109 return Doc::Section3;
2110 } else if (name == "section4") {
2111 return Doc::Section4;
2112 } else if (name.isEmpty()) {
2113 return Doc::NoSection;
2114 } else {
2115 location().warning(QStringLiteral("Invalid section '%1'").arg(a: name));
2116 return Doc::NoSection;
2117 }
2118}
2119
2120/*!
2121 Gets an argument that is enclosed in braces and returns it
2122 without the enclosing braces. On entry, the current character
2123 is the left brace. On exit, the current character is the one
2124 that comes after the right brace.
2125
2126 If \a verbatim is true, extra whitespace is retained in the
2127 returned string. Otherwise, extra whitespace is removed.
2128 */
2129QString DocParser::getBracedArgument(bool verbatim)
2130{
2131 QString arg;
2132 int delimDepth = 0;
2133 if (m_position < m_input.size() && m_input[m_position] == '{') {
2134 ++m_position;
2135 while (m_position < m_input.size() && delimDepth >= 0) {
2136 switch (m_input[m_position].unicode()) {
2137 case '{':
2138 ++delimDepth;
2139 arg += QLatin1Char('{');
2140 ++m_position;
2141 break;
2142 case '}':
2143 --delimDepth;
2144 if (delimDepth >= 0)
2145 arg += QLatin1Char('}');
2146 ++m_position;
2147 break;
2148 case '\\':
2149 if (verbatim || !expandMacro())
2150 arg += m_input[m_position++];
2151 break;
2152 default:
2153 if (m_input[m_position].isSpace() && !verbatim)
2154 arg += QChar(' ');
2155 else
2156 arg += m_input[m_position];
2157 ++m_position;
2158 }
2159 }
2160 if (delimDepth > 0)
2161 location().warning(QStringLiteral("Missing '}'"));
2162 }
2163 m_endPosition = m_position;
2164 return arg;
2165}
2166
2167/*!
2168 Typically, an argument ends at the next white-space. However,
2169 braces can be used to group words:
2170
2171 {a few words}
2172
2173 Also, opening and closing parentheses have to match. Thus,
2174
2175 printf("%d\n", x)
2176
2177 is an argument too, although it contains spaces. Finally,
2178 trailing punctuation is not included in an argument, nor is 's.
2179*/
2180QString DocParser::getArgument(bool verbatim)
2181{
2182 skipSpacesOrOneEndl();
2183
2184 int delimDepth = 0;
2185 qsizetype startPos = m_position;
2186 QString arg = getBracedArgument(verbatim);
2187 if (arg.isEmpty()) {
2188 while ((m_position < m_input.size())
2189 && ((delimDepth > 0) || ((delimDepth == 0) && !m_input[m_position].isSpace()))) {
2190 switch (m_input[m_position].unicode()) {
2191 case '(':
2192 case '[':
2193 case '{':
2194 ++delimDepth;
2195 arg += m_input[m_position];
2196 ++m_position;
2197 break;
2198 case ')':
2199 case ']':
2200 case '}':
2201 --delimDepth;
2202 if (m_position == startPos || delimDepth >= 0) {
2203 arg += m_input[m_position];
2204 ++m_position;
2205 }
2206 break;
2207 case '\\':
2208 if (verbatim || !expandMacro())
2209 arg += m_input[m_position++];
2210 break;
2211 default:
2212 arg += m_input[m_position];
2213 ++m_position;
2214 }
2215 }
2216 m_endPosition = m_position;
2217 if ((arg.size() > 1) && (QString(".,:;!?").indexOf(c: m_input[m_position - 1]) != -1)
2218 && !arg.endsWith(s: "...")) {
2219 arg.truncate(pos: arg.size() - 1);
2220 --m_position;
2221 }
2222 if (arg.size() > 2 && m_input.mid(position: m_position - 2, n: 2) == "'s") {
2223 arg.truncate(pos: arg.size() - 2);
2224 m_position -= 2;
2225 }
2226 }
2227 return arg.simplified();
2228}
2229
2230/*!
2231 Gets an argument that is enclosed in brackets and returns it
2232 without the enclosing brackets. On entry, the current character
2233 is the left bracket. On exit, the current character is the one
2234 that comes after the right bracket.
2235 */
2236QString DocParser::getBracketedArgument()
2237{
2238 QString arg;
2239 int delimDepth = 0;
2240 skipSpacesOrOneEndl();
2241 if (m_position < m_input.size() && m_input[m_position] == '[') {
2242 ++m_position;
2243 while (m_position < m_input.size() && delimDepth >= 0) {
2244 switch (m_input[m_position].unicode()) {
2245 case '[':
2246 ++delimDepth;
2247 arg += QLatin1Char('[');
2248 ++m_position;
2249 break;
2250 case ']':
2251 --delimDepth;
2252 if (delimDepth >= 0)
2253 arg += QLatin1Char(']');
2254 ++m_position;
2255 break;
2256 case '\\':
2257 arg += m_input[m_position];
2258 ++m_position;
2259 break;
2260 default:
2261 arg += m_input[m_position];
2262 ++m_position;
2263 }
2264 }
2265 if (delimDepth > 0)
2266 location().warning(QStringLiteral("Missing ']'"));
2267 }
2268 return arg;
2269}
2270
2271
2272/*!
2273 Returns the list of arguments passed to a \a macro with name \a name.
2274
2275 If a macro takes more than a single argument, they are expected to be
2276 wrapped in braces.
2277*/
2278QStringList DocParser::getMacroArguments(const QString &name, const Macro &macro)
2279{
2280 QStringList args;
2281 for (int i = 0; i < macro.numParams; ++i) {
2282 if (macro.numParams == 1 || isLeftBraceAhead()) {
2283 args << getArgument();
2284 } else {
2285 location().warning(QStringLiteral("Macro '\\%1' invoked with too few"
2286 " arguments (expected %2, got %3)")
2287 .arg(a: name)
2288 .arg(a: macro.numParams)
2289 .arg(a: i));
2290 break;
2291 }
2292 }
2293 return args;
2294}
2295
2296QString DocParser::getOptionalArgument()
2297{
2298 skipSpacesOrOneEndl();
2299 if (m_position + 1 < m_input.size() && m_input[m_position] == '\\'
2300 && m_input[m_position + 1].isLetterOrNumber()) {
2301 return QString();
2302 } else {
2303 return getArgument();
2304 }
2305}
2306
2307/*!
2308 \brief Create a string that may optionally span multiple lines as one line.
2309
2310 Process a block of text that may span multiple lines using trailing
2311 backslashes (`\`) as line continuation character. Trailing backslashes and
2312 any newline character that follow them are removed.
2313
2314 Returns a string as if it was one continuous line of text, stripped of
2315 leading and trailing whitespace characters.
2316 */
2317QString DocParser::getRestOfLine()
2318{
2319 auto lineHasTrailingBackslash = [this](bool trailingBackslash) -> bool {
2320 while (m_position < m_inputLength && m_input[m_position] != '\n') {
2321 if (m_input[m_position] == '\\' && !trailingBackslash) {
2322 trailingBackslash = true;
2323 ++m_position;
2324 skipSpacesOnLine();
2325 } else {
2326 trailingBackslash = false;
2327 ++m_position;
2328 }
2329 }
2330 return trailingBackslash;
2331 };
2332
2333 QString rest_of_line;
2334 skipSpacesOnLine();
2335 bool trailing_backslash{ false };
2336
2337 for (qsizetype start_of_line = m_position; m_position < m_inputLength; ++m_position) {
2338 trailing_backslash = lineHasTrailingBackslash(trailing_backslash);
2339
2340 if (!rest_of_line.isEmpty())
2341 rest_of_line += QLatin1Char(' ');
2342 rest_of_line += m_input.sliced(pos: start_of_line, n: m_position - start_of_line);
2343
2344 if (trailing_backslash)
2345 rest_of_line.chop(n: 1);
2346
2347 if (m_position < m_inputLength)
2348 ++m_position;
2349
2350 if (!trailing_backslash)
2351 break;
2352 }
2353
2354 return rest_of_line.simplified();
2355}
2356
2357/*!
2358 The metacommand argument is normally the remaining text to
2359 the right of the metacommand itself. The extra blanks are
2360 stripped and the argument string is returned.
2361 */
2362QString DocParser::getMetaCommandArgument(const QString &cmdStr)
2363{
2364 skipSpacesOnLine();
2365
2366 qsizetype begin = m_position;
2367 int parenDepth = 0;
2368
2369 while (m_position < m_input.size() && (m_input[m_position] != '\n' || parenDepth > 0)) {
2370 if (m_input.at(i: m_position) == '(')
2371 ++parenDepth;
2372 else if (m_input.at(i: m_position) == ')')
2373 --parenDepth;
2374 else if (m_input.at(i: m_position) == '\\' && expandMacro())
2375 continue;
2376 ++m_position;
2377 }
2378 if (m_position == m_input.size() && parenDepth > 0) {
2379 m_position = begin;
2380 location().warning(QStringLiteral("Unbalanced parentheses in '%1'").arg(a: cmdStr));
2381 }
2382
2383 QString t = m_input.mid(position: begin, n: m_position - begin).simplified();
2384 skipSpacesOnLine();
2385 return t;
2386}
2387
2388QString DocParser::getUntilEnd(int cmd)
2389{
2390 int endCmd = endCmdFor(cmd);
2391 QRegularExpression rx("\\\\" + cmdName(cmd: endCmd) + "\\b");
2392 QString t;
2393 auto match = rx.match(subject: m_input, offset: m_position);
2394
2395 if (!match.hasMatch()) {
2396 location().warning(QStringLiteral("Missing '\\%1'").arg(a: cmdName(cmd: endCmd)));
2397 m_position = m_input.size();
2398 } else {
2399 qsizetype end = match.capturedStart();
2400 t = m_input.mid(position: m_position, n: end - m_position);
2401 m_position = match.capturedEnd();
2402 }
2403 return t;
2404}
2405
2406void DocParser::expandArgumentsInString(QString &str, const QStringList &args)
2407{
2408 if (args.isEmpty())
2409 return;
2410
2411 qsizetype paramNo;
2412 qsizetype j = 0;
2413 while (j < str.size()) {
2414 if (str[j] == '\\' && j < str.size() - 1 && (paramNo = str[j + 1].digitValue()) >= 1
2415 && paramNo <= args.size()) {
2416 const QString &r = args[paramNo - 1];
2417 str.replace(i: j, len: 2, after: r);
2418 j += qMin(a: 1, b: r.size());
2419 } else {
2420 ++j;
2421 }
2422 }
2423}
2424
2425/*!
2426 Returns the marked-up code following the code-quoting command \a cmd, expanding
2427 any arguments passed in \a argStr.
2428
2429 Uses the \a marker to mark up the code. If it's \c nullptr, resolve the marker
2430 based on the topic and the quoted code itself.
2431*/
2432QString DocParser::getCode(int cmd, CodeMarker *marker, const QString &argStr)
2433{
2434 QString code = untabifyEtc(str: getUntilEnd(cmd));
2435 expandArgumentsInString(str&: code, args: argStr.split(sep: " ", behavior: Qt::SkipEmptyParts));
2436
2437 int indent = indentLevel(str: code);
2438 code = dedent(level: indent, str: code);
2439
2440 // If we're in a QML topic, check if the QML marker recognizes the code
2441 if (!marker && !m_private->m_topics.isEmpty()
2442 && m_private->m_topics[0].m_topic.startsWith(s: "qml")) {
2443 auto qmlMarker = CodeMarker::markerForLanguage(lang: "QML");
2444 marker = (qmlMarker && qmlMarker->recognizeCode(code)) ? qmlMarker : nullptr;
2445 }
2446 if (marker == nullptr)
2447 marker = CodeMarker::markerForCode(code);
2448 return marker->markedUpCode(code, nullptr, location());
2449}
2450
2451bool DocParser::isBlankLine()
2452{
2453 qsizetype i = m_position;
2454
2455 while (i < m_inputLength && m_input[i].isSpace()) {
2456 if (m_input[i] == '\n')
2457 return true;
2458 ++i;
2459 }
2460 return false;
2461}
2462
2463bool DocParser::isLeftBraceAhead()
2464{
2465 int numEndl = 0;
2466 qsizetype i = m_position;
2467
2468 while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) {
2469 // ### bug with '\\'
2470 if (m_input[i] == '\n')
2471 numEndl++;
2472 ++i;
2473 }
2474 return numEndl < 2 && i < m_inputLength && m_input[i] == '{';
2475}
2476
2477bool DocParser::isLeftBracketAhead()
2478{
2479 int numEndl = 0;
2480 qsizetype i = m_position;
2481
2482 while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) {
2483 // ### bug with '\\'
2484 if (m_input[i] == '\n')
2485 numEndl++;
2486 ++i;
2487 }
2488 return numEndl < 2 && i < m_inputLength && m_input[i] == '[';
2489}
2490
2491/*!
2492 Skips to the next non-space character or EOL.
2493 */
2494void DocParser::skipSpacesOnLine()
2495{
2496 while ((m_position < m_input.size()) && m_input[m_position].isSpace()
2497 && (m_input[m_position].unicode() != '\n'))
2498 ++m_position;
2499}
2500
2501/*!
2502 Skips spaces and one EOL.
2503 */
2504void DocParser::skipSpacesOrOneEndl()
2505{
2506 qsizetype firstEndl = -1;
2507 while (m_position < m_input.size() && m_input[m_position].isSpace()) {
2508 QChar ch = m_input[m_position];
2509 if (ch == '\n') {
2510 if (firstEndl == -1) {
2511 firstEndl = m_position;
2512 } else {
2513 m_position = firstEndl;
2514 break;
2515 }
2516 }
2517 ++m_position;
2518 }
2519}
2520
2521void DocParser::skipAllSpaces()
2522{
2523 while (m_position < m_inputLength && m_input[m_position].isSpace())
2524 ++m_position;
2525}
2526
2527void DocParser::skipToNextPreprocessorCommand()
2528{
2529 QRegularExpression rx("\\\\(?:" + cmdName(cmd: CMD_IF) + QLatin1Char('|') + cmdName(cmd: CMD_ELSE)
2530 + QLatin1Char('|') + cmdName(cmd: CMD_ENDIF) + ")\\b");
2531 auto match = rx.match(subject: m_input, offset: m_position + 1); // ### + 1 necessary?
2532
2533 if (!match.hasMatch())
2534 m_position = m_input.size();
2535 else
2536 m_position = match.capturedStart();
2537}
2538
2539int DocParser::endCmdFor(int cmd)
2540{
2541 switch (cmd) {
2542 case CMD_BADCODE:
2543 return CMD_ENDCODE;
2544 case CMD_CODE:
2545 return CMD_ENDCODE;
2546 case CMD_DETAILS:
2547 return CMD_ENDDETAILS;
2548 case CMD_DIV:
2549 return CMD_ENDDIV;
2550 case CMD_QML:
2551 return CMD_ENDQML;
2552 case CMD_FOOTNOTE:
2553 return CMD_ENDFOOTNOTE;
2554 case CMD_LEGALESE:
2555 return CMD_ENDLEGALESE;
2556 case CMD_LINK:
2557 return CMD_ENDLINK;
2558 case CMD_LIST:
2559 return CMD_ENDLIST;
2560 case CMD_OMIT:
2561 return CMD_ENDOMIT;
2562 case CMD_QUOTATION:
2563 return CMD_ENDQUOTATION;
2564 case CMD_RAW:
2565 return CMD_ENDRAW;
2566 case CMD_SECTION1:
2567 return CMD_ENDSECTION1;
2568 case CMD_SECTION2:
2569 return CMD_ENDSECTION2;
2570 case CMD_SECTION3:
2571 return CMD_ENDSECTION3;
2572 case CMD_SECTION4:
2573 return CMD_ENDSECTION4;
2574 case CMD_SIDEBAR:
2575 return CMD_ENDSIDEBAR;
2576 case CMD_TABLE:
2577 return CMD_ENDTABLE;
2578 default:
2579 return cmd;
2580 }
2581}
2582
2583QString DocParser::cmdName(int cmd)
2584{
2585 return cmds[cmd].name;
2586}
2587
2588QString DocParser::endCmdName(int cmd)
2589{
2590 return cmdName(cmd: endCmdFor(cmd));
2591}
2592
2593QString DocParser::untabifyEtc(const QString &str)
2594{
2595 QString result;
2596 result.reserve(asize: str.size());
2597 int column = 0;
2598
2599 for (const auto &character : str) {
2600 if (character == QLatin1Char('\r'))
2601 continue;
2602 if (character == QLatin1Char('\t')) {
2603 result += &" "[column % s_tabSize];
2604 column = ((column / s_tabSize) + 1) * s_tabSize;
2605 continue;
2606 }
2607 if (character == QLatin1Char('\n')) {
2608 while (result.endsWith(c: QLatin1Char(' ')))
2609 result.chop(n: 1);
2610 result += character;
2611 column = 0;
2612 continue;
2613 }
2614 result += character;
2615 ++column;
2616 }
2617
2618 while (result.endsWith(s: "\n\n"))
2619 result.truncate(pos: result.size() - 1);
2620 while (result.startsWith(c: QLatin1Char('\n')))
2621 result = result.mid(position: 1);
2622
2623 return result;
2624}
2625
2626int DocParser::indentLevel(const QString &str)
2627{
2628 int minIndent = INT_MAX;
2629 int column = 0;
2630
2631 for (const auto &character : str) {
2632 if (character == '\n') {
2633 column = 0;
2634 } else {
2635 if (character != ' ' && column < minIndent)
2636 minIndent = column;
2637 ++column;
2638 }
2639 }
2640 return minIndent;
2641}
2642
2643QString DocParser::dedent(int level, const QString &str)
2644{
2645 if (level == 0)
2646 return str;
2647
2648 QString result;
2649 int column = 0;
2650
2651 for (const auto &character : str) {
2652 if (character == QLatin1Char('\n')) {
2653 result += '\n';
2654 column = 0;
2655 } else {
2656 if (column >= level)
2657 result += character;
2658 ++column;
2659 }
2660 }
2661 return result;
2662}
2663
2664/*!
2665 Returns \c true if \a atom represents a code snippet.
2666 */
2667bool DocParser::isCode(const Atom *atom)
2668{
2669 Atom::AtomType type = atom->type();
2670 return (type == Atom::Code || type == Atom::Qml);
2671}
2672
2673/*!
2674 Returns \c true if \a atom represents quoting information.
2675 */
2676bool DocParser::isQuote(const Atom *atom)
2677{
2678 Atom::AtomType type = atom->type();
2679 return (type == Atom::CodeQuoteArgument || type == Atom::CodeQuoteCommand
2680 || type == Atom::SnippetCommand || type == Atom::SnippetIdentifier
2681 || type == Atom::SnippetLocation);
2682}
2683
2684QT_END_NAMESPACE
2685

source code of qttools/src/qdoc/qdoc/docparser.cpp