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