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

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