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(Atom(Atom::Link, p1));
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 // hack for C++ to support links like
1226 // \l{QString::}{count()}
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(Atom(Atom::AutoLink, word));
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 // hack for C++ to support links like \l{QString::}{count()}
1914 if (target.endsWith("::"))
1915 target += str;
1916 } else {
1917 str = target;
1918 }
1919 } else {
1920 target = getArgument();
1921 str = cleanLink(target);
1922 if (target == QLatin1String("and") || target == QLatin1String("."))
1923 skipMe = true;
1924 }
1925
1926 if (!skipMe) {
1927 Text also;
1928 also << Atom(Atom::Link, target) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1930 m_private->addAlso(also);
1931 }
1932
1933 skipSpacesOnLine();
1934
1935 if (line_comment())
1936 skip_everything_until_newline();
1937
1938 if (m_position < m_inputLength && m_input[m_position] == ',') {
1939 m_position++;
1940 if (line_comment())
1941 skip_everything_until_newline();
1942 skipSpacesOrOneEndl();
1943 } else if (m_position >= m_inputLength || m_input[m_position] != '\n') {
1944 location().warning(QStringLiteral("Missing comma in '\\%1'").arg(cmdName(CMD_SA)));
1945 }
1946 }
1947}
1948
1949void DocParser::appendAtom(const Atom& atom) {
1950 m_private->m_text << atom;
1951}
1952
1953void DocParser::appendAtom(const LinkAtom& atom) {
1954 m_private->m_text << atom;
1955}
1956
1957void DocParser::appendChar(QChar ch)
1958{
1960 appendAtom(Atom(Atom::String));
1961 Atom *atom = m_private->m_text.lastAtom();
1962 if (ch == QLatin1Char(' ')) {
1963 if (!atom->string().endsWith(QLatin1Char(' ')))
1964 atom->appendChar(QLatin1Char(' '));
1965 } else
1966 atom->appendChar(ch);
1967}
1968
1969void DocParser::appendWord(const QString &word)
1970{
1971 if (m_private->m_text.lastAtom()->type() != Atom::String) {
1972 appendAtom(Atom(Atom::String, word));
1973 } else
1974 m_private->m_text.lastAtom()->concatenateString(word);
1975}
1976
1977/*!
1978 Handles escaped backslash sequences (\\‍) that may be followed by an identifier.
1979
1980 When a double backslash is followed by alphanumeric characters (such as \\section1),
1981 the backslash and identifier are wrapped in notranslate formatting to preserve
1982 them verbatim in the output.
1983
1984 When a double backslash is not followed by an identifier, a literal backslash
1985 is appended.
1986
1987 \note Skips \\r in CRLF sequences to prevent spurious whitespace in the output.
1988*/
1989void DocParser::appendEscapedIdentifier()
1990{
1991 Q_ASSERT(m_position < m_inputLength);
1992 Q_ASSERT(m_input.at(m_position) == '\\');
1993
1994 ++m_position;
1995
1996 QString identifier;
1997 while (m_position < m_inputLength && m_input.at(m_position).isLetterOrNumber()) {
1998 identifier += m_input.at(m_position);
1999 ++m_position;
2000 }
2001
2002 enterPara();
2003 if (!identifier.isEmpty()) {
2005 appendAtom(Atom(Atom::String, '\\' + identifier));
2007
2008 if (m_position + 1 < m_inputLength && m_input.at(m_position) == '\r' && m_input.at(m_position + 1) == '\n')
2009 ++m_position;
2010 } else {
2011 appendChar(QLatin1Char('\\'));
2012 }
2013}
2014
2015void DocParser::appendToCode(const QString &markedCode)
2016{
2017 if (!isCode(m_lastAtom)) {
2018 appendAtom(Atom(Atom::Code));
2019 m_lastAtom = m_private->m_text.lastAtom();
2020 }
2021 m_lastAtom->concatenateString(markedCode);
2022}
2023
2024/*!
2025 Appends \a markedCode to the current documentation as an atom with the
2026 \a defaultType. The \a language specifies the programming language that
2027 the code is written in so that generators can include it as part of the
2028 meta-data they produce.
2029*/
2030void DocParser::appendToCode(const QString &markedCode, Atom::AtomType defaultType, const QString &language)
2031{
2032 if (!isCode(m_lastAtom)) {
2033 appendAtom(Atom(defaultType, markedCode, language));
2034 m_lastAtom = m_private->m_text.lastAtom();
2035 } else {
2036 m_lastAtom->concatenateString(markedCode);
2037 }
2038}
2039
2040void DocParser::enterPara(Atom::AtomType leftType, Atom::AtomType rightType, const QString &string)
2041{
2042 if (m_paragraphState != OutsideParagraph)
2043 return;
2044
2048 leaveValueList();
2049 }
2050
2051 appendAtom(Atom(leftType, string));
2052 m_indexStartedParagraph = false;
2053 m_pendingParagraphLeftType = leftType;
2054 m_pendingParagraphRightType = rightType;
2055 m_pendingParagraphString = string;
2056 if (leftType == Atom::SectionHeadingLeft || leftType == Atom::TitleLeft) {
2057 m_paragraphState = InSingleLineParagraph;
2058 } else if (leftType == Atom::DetailsSummaryLeft) {
2059 m_paragraphState = InBraceDelimitedParagraph;
2060 } else {
2061 m_paragraphState = InMultiLineParagraph;
2062 }
2063 skipSpacesOrOneEndl();
2064}
2065
2066void DocParser::leavePara()
2067{
2068 if (m_paragraphState == OutsideParagraph)
2069 return;
2070 if (!m_pendingFormats.isEmpty() || (m_paragraphState == InBraceDelimitedParagraph && m_braceDepth > 0)) {
2071 location().warning(u"Missing '}'"_s);
2072 m_pendingFormats.clear();
2073 m_braceDepth = 0;
2074 }
2075
2076 if (m_private->m_text.lastAtom()->type() == m_pendingParagraphLeftType) {
2077 m_private->m_text.stripLastAtom();
2078 } else {
2080 && m_private->m_text.lastAtom()->string().endsWith(QLatin1Char(' '))) {
2082 }
2083 if (m_pendingParagraphRightType == Atom::TitleRight) {
2084 // Extract title
2087 } else {
2088 appendAtom(Atom(m_pendingParagraphRightType, m_pendingParagraphString));
2089 }
2090 }
2091 m_paragraphState = OutsideParagraph;
2092 m_indexStartedParagraph = false;
2093 m_pendingParagraphRightType = Atom::Nop;
2094 m_pendingParagraphString.clear();
2095}
2096
2097void DocParser::leaveValue()
2098{
2099 leavePara();
2100 if (m_openedLists.isEmpty()) {
2101 m_openedLists.push(OpenedList(OpenedList::Value));
2102 appendAtom(Atom(Atom::ListLeft, ATOM_LIST_VALUE));
2103 } else {
2104 if (m_private->m_text.lastAtom()->type() == Atom::Nop)
2105 m_private->m_text.stripLastAtom();
2106 appendAtom(Atom(Atom::ListItemRight, ATOM_LIST_VALUE));
2107 }
2108}
2109
2110void DocParser::leaveValueList()
2111{
2112 leavePara();
2113 if (!m_openedLists.isEmpty() && (m_openedLists.top().style() == OpenedList::Value)) {
2114 if (m_private->m_text.lastAtom()->type() == Atom::Nop)
2115 m_private->m_text.stripLastAtom();
2116 appendAtom(Atom(Atom::ListItemRight, ATOM_LIST_VALUE));
2117 appendAtom(Atom(Atom::ListRight, ATOM_LIST_VALUE));
2118 m_openedLists.pop();
2119 }
2120}
2121
2122void DocParser::leaveTableRow()
2123{
2124 if (m_inTableItem) {
2125 leavePara();
2126 appendAtom(Atom(Atom::TableItemRight));
2127 m_inTableItem = false;
2128 }
2129 if (m_inTableHeader) {
2130 appendAtom(Atom(Atom::TableHeaderRight));
2131 m_inTableHeader = false;
2132 }
2133 if (m_inTableRow) {
2134 appendAtom(Atom(Atom::TableRowRight));
2135 m_inTableRow = false;
2136 }
2137}
2138
2139/*!
2140 Quotes from the file with the given \a filename using an optional \a marker
2141 to mark up the quoted text. If no marker is provided, a marker is selected
2142 based on the file name.
2143*/
2144void DocParser::quoteFromFile(const QString &filename, CodeMarker *marker)
2145{
2146 // KLUDGE: We dereference file_resolver as it is temporarily a pointer.
2147 // See the comment for file_resolver in the header files for more context.
2148 //
2149 // We spefically dereference it, instead of using the arrow
2150 // operator, to better represent that we do not consider this as
2151 // an actual pointer, as it should not be.
2152 //
2153 // Do note that we are considering it informally safe to
2154 // dereference the pointer, as we expect it to always hold a value
2155 // at this point, but actual enforcement of this appears nowhere
2156 // in the codebase.
2157 auto maybe_resolved_file{(*file_resolver).resolve(filename)};
2158 if (!maybe_resolved_file) {
2159 // TODO: [uncentralized-admonition][failed-resolve-file]
2160 // This warning is required in multiple places.
2161 // To ensure the consistency of the warning and avoid
2162 // duplicating code everywhere, provide a centralized effort
2163 // where the warning message can be generated (but not
2164 // issued).
2165 // The current format is based on what was used before, review
2166 // it when it is moved out.
2167 QString details = std::transform_reduce(
2168 (*file_resolver).get_search_directories().cbegin(),
2169 (*file_resolver).get_search_directories().cend(),
2170 u"Searched directories:"_s,
2171 std::plus(),
2172 [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
2173 );
2174
2175 location().warning(u"Cannot find file to quote from: %1"_s.arg(filename), details);
2176
2177 // REMARK: The following is duplicated from
2178 // Doc::quoteFromFile. If, for some reason (such as a file
2179 // that is inaccessible), the quoting fails but, previously,
2180 // the logic duplicated here was still run.
2181 // This is not true anymore as quoteFromFile does require a
2182 // resolved file to be run now.
2183 // It is not entirely clear if this is required for the
2184 // semantics of DocParser to be preserved, but for the sake of
2185 // avoiding premature breakages this was retained.
2186 // Do note that this should be considered temporary as the
2187 // quoter state, if any will be preserved, should not be
2188 // managed in such a spread and unlocal way.
2189 m_quoter.reset();
2190
2191 if (!marker)
2192 marker = CodeMarker::markerForFileName(QString{});
2193 m_quoter.quoteFromFile(filename, QString{}, marker->markedUpCode(QString{}, nullptr, location()));
2194 } else
2195 Doc::quoteFromFile(location(), m_quoter, *maybe_resolved_file, marker);
2196}
2197
2198/*!
2199 Expands a macro in-place in input.
2200
2201 Expects the current \e pos in the input to point to a backslash, and the macro to have a
2202 default definition. Format-specific macros are not expanded.
2203
2204 Behavior depends on \a options:
2205
2206 \value ArgumentParsingOptions::Default
2207 Default macro expansion; the string following the backslash
2208 must be a macro with a default definition.
2209 \value ArgumentParsingOptions::Verbatim
2210 The string following the backslash is rendered verbatim;
2211 No macro expansion is performed.
2212 \value ArgumentParsingOptions::MacroArguments
2213 Used for parsing argument(s) for a macro. Allows expanding
2214 macros, and also preserves a subset of commands (formatting
2215 commands) within the macro argument.
2216
2217 \note In addition to macros, a valid use for a backslash in an argument include
2218 escaping non-alnum characters, and splitting a single argument across multiple
2219 lines by escaping newlines. Escaping is also handled here.
2220
2221 Returns \c true on successful macro expansion.
2222 */
2223bool DocParser::expandMacro(ArgumentParsingOptions options)
2224{
2225 Q_ASSERT(m_input[m_position].unicode() == '\\');
2226
2227 if (options == ArgumentParsingOptions::Verbatim)
2228 return false;
2229
2230 QString cmdStr;
2231 qsizetype backslashPos = m_position++;
2232 while (m_position < m_input.size() && m_input[m_position].isLetterOrNumber())
2233 cmdStr += m_input[m_position++];
2234
2235 m_endPosition = m_position;
2236 if (!cmdStr.isEmpty()) {
2237 if (s_utilities.macroHash.contains(cmdStr)) {
2238 const Macro &macro = s_utilities.macroHash.value(cmdStr);
2239 if (!macro.m_defaultDef.isEmpty()) {
2240 QString expanded = expandMacroToString(cmdStr, macro);
2241 m_input.replace(backslashPos, m_position - backslashPos, expanded);
2242 m_inputLength = m_input.size();
2243 m_position = backslashPos;
2244 return true;
2245 } else {
2246 location().warning("Macro '%1' does not have a default definition"_L1.arg(cmdStr));
2247 }
2248 } else {
2249 int cmd = s_utilities.cmdHash.value(cmdStr, NOT_A_CMD);
2250 m_position = backslashPos;
2251 if (options != ArgumentParsingOptions::MacroArguments
2252 || cmd == NOT_A_CMD || !cmds[cmd].is_formatting_command) {
2253 location().warning("Unknown macro '%1'"_L1.arg(cmdStr));
2254 ++m_position;
2255 }
2256 }
2257 } else if (m_input[m_position].isSpace()) {
2258 skipAllSpaces();
2259 } else if (m_input[m_position].unicode() == '\\') {
2260 // allow escaping a backslash
2261 m_input.remove(m_position--, 1);
2262 --m_inputLength;
2263 }
2264 return false;
2265}
2266
2267void DocParser::expandMacro(const QString &def, const QStringList &args)
2268{
2269 if (args.isEmpty()) {
2270 appendAtom(Atom(Atom::RawString, def));
2271 } else {
2272 QString rawString;
2273
2274 for (int j = 0; j < def.size(); ++j) {
2275 if (int paramNo = def[j].unicode(); paramNo >= 1 && paramNo <= args.length()) {
2276 if (!rawString.isEmpty()) {
2277 appendAtom(Atom(Atom::RawString, rawString));
2278 rawString.clear();
2279 }
2280 appendAtom(Atom(Atom::String, args[paramNo - 1]));
2281 } else {
2282 rawString += def[j];
2283 }
2284 }
2285 if (!rawString.isEmpty())
2286 appendAtom(Atom(Atom::RawString, rawString));
2287 }
2288}
2289
2290QString DocParser::expandMacroToString(const QString &name, const Macro &macro)
2291{
2292 const QString &def{macro.m_defaultDef};
2293 QString rawString;
2294
2295 if (macro.numParams == 0) {
2296 rawString = macro.m_defaultDef;
2297 } else {
2298 QStringList args{getMacroArguments(name, macro)};
2299
2300 for (int j = 0; j < def.size(); ++j) {
2301 int paramNo = def[j].unicode();
2302 rawString += (paramNo >= 1 && paramNo <= args.length()) ? args[paramNo - 1] : def[j];
2303 }
2304 }
2305 QString matchExpr{macro.m_otherDefs.value("match")};
2306 if (matchExpr.isEmpty())
2307 return rawString;
2308
2309 QString result;
2310 QRegularExpression re(matchExpr);
2311 int capStart = (re.captureCount() > 0) ? 1 : 0;
2312 qsizetype i = 0;
2313 QRegularExpressionMatch match;
2314 while ((match = re.match(rawString, i)).hasMatch()) {
2315 for (int c = capStart; c <= re.captureCount(); ++c)
2316 result += match.captured(c);
2317 i = match.capturedEnd();
2318 }
2319
2320 return result;
2321}
2322
2323Doc::Sections DocParser::getSectioningUnit()
2324{
2325 QString name = getOptionalArgument();
2326
2327 if (name == "section1") {
2328 return Doc::Section1;
2329 } else if (name == "section2") {
2330 return Doc::Section2;
2331 } else if (name == "section3") {
2332 return Doc::Section3;
2333 } else if (name == "section4") {
2334 return Doc::Section4;
2335 } else if (name.isEmpty()) {
2336 return Doc::NoSection;
2337 } else {
2338 location().warning(QStringLiteral("Invalid section '%1'").arg(name));
2339 return Doc::NoSection;
2340 }
2341}
2342
2343/*!
2344 Gets an argument that is enclosed in braces and returns it
2345 without the enclosing braces. On entry, the current character
2346 is the left brace. On exit, the current character is the one
2347 that comes after the right brace.
2348
2349 If \a options is ArgumentParsingOptions::Verbatim, no macro
2350 expansion is performed, nor is the returned string stripped
2351 of extra whitespace.
2352 */
2353QString DocParser::getBracedArgument(ArgumentParsingOptions options)
2354{
2355 QString arg;
2356 int delimDepth = 0;
2357 if (m_position < m_input.size() && m_input[m_position] == '{') {
2358 ++m_position;
2359 while (m_position < m_input.size() && delimDepth >= 0) {
2360 switch (m_input[m_position].unicode()) {
2361 case '{':
2362 ++delimDepth;
2363 arg += QLatin1Char('{');
2364 ++m_position;
2365 break;
2366 case '}':
2367 --delimDepth;
2368 if (delimDepth >= 0)
2369 arg += QLatin1Char('}');
2370 ++m_position;
2371 break;
2372 case '\\':
2373 if (!expandMacro(options))
2374 arg += m_input[m_position++];
2375 break;
2376 default:
2377 if (m_input[m_position].isSpace() && options != ArgumentParsingOptions::Verbatim)
2378 arg += QChar(' ');
2379 else
2380 arg += m_input[m_position];
2381 ++m_position;
2382 }
2383 }
2384 if (delimDepth > 0)
2385 location().warning(QStringLiteral("Missing '}'"));
2386 }
2387 m_endPosition = m_position;
2388 return arg;
2389}
2390
2391/*!
2392 Parses and returns an argument for a command, using
2393 specific parsing \a options.
2394
2395 Typically, an argument ends at the next white-space. However,
2396 braces can be used to group words:
2397
2398 {a few words}
2399
2400 Also, opening and closing parentheses have to match. Thus,
2401
2402 printf("%d\n", x)
2403
2404 is an argument too, although it contains spaces. Finally,
2405 trailing punctuation is not included in an argument, nor is 's.
2406*/
2407QString DocParser::getArgument(ArgumentParsingOptions options)
2408{
2409 skipSpacesOrOneEndl();
2410
2411 int delimDepth = 0;
2412 qsizetype startPos = m_position;
2413 QString arg = getBracedArgument(options);
2414 if (arg.isEmpty()) {
2415 while ((m_position < m_input.size())
2416 && ((delimDepth > 0) || ((delimDepth == 0) && !m_input[m_position].isSpace()))) {
2417 switch (m_input[m_position].unicode()) {
2418 case '(':
2419 case '[':
2420 case '{':
2421 ++delimDepth;
2422 arg += m_input[m_position];
2423 ++m_position;
2424 break;
2425 case ')':
2426 case ']':
2427 case '}':
2428 --delimDepth;
2429 if (m_position == startPos || delimDepth >= 0) {
2430 arg += m_input[m_position];
2431 ++m_position;
2432 }
2433 break;
2434 case '\\':
2435 if (!expandMacro(options))
2436 arg += m_input[m_position++];
2437 break;
2438 default:
2439 arg += m_input[m_position];
2440 ++m_position;
2441 }
2442 }
2443 m_endPosition = m_position;
2444 if ((arg.size() > 1) && (QString(".,:;!?").indexOf(m_input[m_position - 1]) != -1)
2445 && !arg.endsWith("...")) {
2446 arg.truncate(arg.size() - 1);
2447 --m_position;
2448 }
2449 if (arg.size() > 2 && m_input.mid(m_position - 2, 2) == "'s") {
2450 arg.truncate(arg.size() - 2);
2451 m_position -= 2;
2452 }
2453 }
2454 return arg.simplified();
2455}
2456
2457/*!
2458 Gets an argument that is enclosed in brackets and returns it
2459 without the enclosing brackets. On entry, the current character
2460 is the left bracket. On exit, the current character is the one
2461 that comes after the right bracket.
2462 */
2463QString DocParser::getBracketedArgument()
2464{
2465 QString arg;
2466 int delimDepth = 0;
2467 skipSpacesOrOneEndl();
2468 if (m_position < m_input.size() && m_input[m_position] == '[') {
2469 ++m_position;
2470 while (m_position < m_input.size() && delimDepth >= 0) {
2471 switch (m_input[m_position].unicode()) {
2472 case '[':
2473 ++delimDepth;
2474 arg += QLatin1Char('[');
2475 ++m_position;
2476 break;
2477 case ']':
2478 --delimDepth;
2479 if (delimDepth >= 0)
2480 arg += QLatin1Char(']');
2481 ++m_position;
2482 break;
2483 case '\\':
2484 arg += m_input[m_position];
2485 ++m_position;
2486 break;
2487 default:
2488 arg += m_input[m_position];
2489 ++m_position;
2490 }
2491 }
2492 if (delimDepth > 0)
2493 location().warning(QStringLiteral("Missing ']'"));
2494 }
2495 return arg;
2496}
2497
2498/*!
2499 Reads an optional bracketed language argument, updates the supplied
2500 \a marker to refer to an appropriate code marker for the language and
2501 returns a lower case representation of the language.
2502
2503 If no suitable marker is found, sets the marker to null and returns
2504 an empty string.
2505*/
2506QString DocParser::getLanguageArgument(CodeMarker **marker)
2507{
2508 QString value{};
2509 if (isLeftBracketAhead(0)) {
2510 value = getBracketedArgument();
2511 *marker = markerForLanguage(value);
2512 } else {
2513 value = ""_L1;
2514 *marker = nullptr;
2515 }
2516 return value.toLower();
2517}
2518
2519/*!
2520 Returns the list of arguments passed to a \a macro with name \a name.
2521
2522 If a macro takes more than a single argument, they are expected to be
2523 wrapped in braces.
2524*/
2525QStringList DocParser::getMacroArguments(const QString &name, const Macro &macro)
2526{
2527 QStringList args;
2528 for (int i = 0; i < macro.numParams; ++i) {
2529 if (macro.numParams == 1 || isLeftBraceAhead()) {
2530 args << getArgument(ArgumentParsingOptions::MacroArguments);
2531 } else {
2532 location().warning(QStringLiteral("Macro '\\%1' invoked with too few"
2533 " arguments (expected %2, got %3)")
2534 .arg(name)
2535 .arg(macro.numParams)
2536 .arg(i));
2537 break;
2538 }
2539 }
2540 return args;
2541}
2542
2543/*!
2544 Returns the next token as an optional argument unless the token is
2545 another command. The next token can be preceded by spaces and an optional
2546 newline.
2547*/
2548QString DocParser::getOptionalArgument()
2549{
2550 skipSpacesOrOneEndl();
2551 if (m_position + 1 < m_input.size() && m_input[m_position] == '\\'
2552 && m_input[m_position + 1].isLetterOrNumber()) {
2553 return QString();
2554 } else {
2555 return getArgument();
2556 }
2557}
2558
2559/*!
2560 \brief Create a string that may optionally span multiple lines as one line.
2561
2562 Process a block of text that may span multiple lines using trailing
2563 backslashes (`\`) as line continuation character. Trailing backslashes and
2564 any newline character that follow them are removed.
2565
2566 Returns a string as if it was one continuous line of text. If trailing
2567 backslashes are removed, the method returns a "simplified" QString, which
2568 means any sequence of internal whitespace is replaced with a single space.
2569
2570 Whitespace at the start and end is always removed from the returned string.
2571
2572 \sa QString::simplified(), QString::trimmed().
2573 */
2574QString DocParser::getRestOfLine()
2575{
2576 auto lineHasTrailingBackslash = [this](bool trailingBackslash) -> bool {
2577 while (m_position < m_inputLength && m_input[m_position] != '\n') {
2578 if (m_input[m_position] == '\\' && !trailingBackslash) {
2579 trailingBackslash = true;
2580 ++m_position;
2581 skipSpacesOnLine();
2582 } else {
2583 trailingBackslash = false;
2584 ++m_position;
2585 }
2586 }
2587 return trailingBackslash;
2588 };
2589
2590 QString rest_of_line;
2591 skipSpacesOnLine();
2592 bool trailing_backslash{ false };
2593 bool return_simplified_string{ false };
2594
2595 for (qsizetype start_position = m_position; m_position < m_inputLength; ++m_position) {
2596 trailing_backslash = lineHasTrailingBackslash(trailing_backslash);
2597
2598 if (!rest_of_line.isEmpty())
2599 rest_of_line += QLatin1Char(' ');
2600 rest_of_line += m_input.sliced(start_position, m_position - start_position);
2601
2602 if (trailing_backslash) {
2603 rest_of_line.truncate(rest_of_line.lastIndexOf('\\'));
2604 return_simplified_string = true;
2605 }
2606
2607 if (m_position < m_inputLength)
2608 ++m_position;
2609
2610 if (!trailing_backslash)
2611 break;
2612 start_position = m_position;
2613 }
2614
2615 if (return_simplified_string)
2616 return rest_of_line.simplified();
2617
2618 return rest_of_line.trimmed();
2619}
2620
2621/*!
2622 The metacommand argument is normally the remaining text to
2623 the right of the metacommand itself. The extra blanks are
2624 stripped and the argument string is returned.
2625 */
2626QString DocParser::getMetaCommandArgument(const QString &cmdStr)
2627{
2628 skipSpacesOnLine();
2629
2630 qsizetype begin = m_position;
2631 int parenDepth = 0;
2632
2633 while (m_position < m_input.size() && (m_input[m_position] != '\n' || parenDepth > 0)) {
2634 if (m_input.at(m_position) == '(')
2635 ++parenDepth;
2636 else if (m_input.at(m_position) == ')')
2637 --parenDepth;
2638 else if (m_input.at(m_position) == '\\' && expandMacro(ArgumentParsingOptions::Default))
2639 continue;
2640 ++m_position;
2641 }
2642 if (m_position == m_input.size() && parenDepth > 0) {
2643 m_position = begin;
2644 location().warning(QStringLiteral("Unbalanced parentheses in '%1'").arg(cmdStr));
2645 }
2646
2647 QString t = m_input.mid(begin, m_position - begin).simplified();
2648 skipSpacesOnLine();
2649 return t;
2650}
2651
2652QString DocParser::getUntilEnd(int cmd)
2653{
2654 int endCmd = endCmdFor(cmd);
2655 QRegularExpression rx("\\\\" + cmdName(endCmd) + "\\b");
2656 QString t;
2657 auto match = rx.match(m_input, m_position);
2658
2659 if (!match.hasMatch()) {
2660 location().warning(QStringLiteral("Missing '\\%1'").arg(cmdName(endCmd)));
2661 m_position = m_input.size();
2662 } else {
2663 qsizetype end = match.capturedStart();
2664 t = m_input.mid(m_position, end - m_position);
2665 m_position = match.capturedEnd();
2666 }
2667 return t;
2668}
2669
2670void DocParser::expandArgumentsInString(QString &str, const QStringList &args)
2671{
2672 if (args.isEmpty())
2673 return;
2674
2675 qsizetype paramNo;
2676 qsizetype j = 0;
2677 while (j < str.size()) {
2678 if (str[j] == '\\' && j < str.size() - 1 && (paramNo = str[j + 1].digitValue()) >= 1
2679 && paramNo <= args.size()) {
2680 const QString &r = args[paramNo - 1];
2681 str.replace(j, 2, r);
2682 j += qMin(1, r.size());
2683 } else {
2684 ++j;
2685 }
2686 }
2687}
2688
2689/*!
2690 Returns the marked-up code following the code-quoting command \a cmd, expanding
2691 any arguments passed in \a argStr.
2692
2693 Uses the \a marker to mark up the code. If it's \c nullptr, resolve the marker
2694 based on the topic and the quoted code itself.
2695*/
2696QString DocParser::getCode(int cmd, CodeMarker *marker, const QString &argStr)
2697{
2698 QString code = untabifyEtc(getUntilEnd(cmd));
2699 expandArgumentsInString(code, argStr.split(" ", Qt::SkipEmptyParts));
2700
2701 int indent = indentLevel(code);
2702 code = dedent(indent, code);
2703
2704 // If we're in a QML topic, check if the QML marker recognizes the code
2705 if (!marker && !m_private->m_topics.isEmpty()
2706 && m_private->m_topics[0].m_topic.startsWith("qml")) {
2707 auto qmlMarker = CodeMarker::markerForLanguage("QML");
2708 marker = (qmlMarker && qmlMarker->recognizeCode(code)) ? qmlMarker : nullptr;
2709 }
2710 if (marker == nullptr)
2711 marker = CodeMarker::markerForCode(code);
2712 return marker->markedUpCode(code, nullptr, location());
2713}
2714
2715bool DocParser::isBlankLine()
2716{
2717 qsizetype i = m_position;
2718
2719 while (i < m_inputLength && m_input[i].isSpace()) {
2720 if (m_input[i] == '\n')
2721 return true;
2722 ++i;
2723 }
2724 return false;
2725}
2726
2727bool DocParser::isLeftBraceAhead()
2728{
2729 int numEndl = 0;
2730 qsizetype i = m_position;
2731
2732 while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) {
2733 // ### bug with '\\'
2734 if (m_input[i] == '\n')
2735 numEndl++;
2736 ++i;
2737 }
2738 return numEndl < 2 && i < m_inputLength && m_input[i] == '{';
2739}
2740
2741bool DocParser::isLeftBracketAhead(int maxNewlines)
2742{
2743 int numEndl = 0;
2744 qsizetype i = m_position;
2745
2746 while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) {
2747 // ### bug with '\\'
2748 if (m_input[i] == '\n')
2749 numEndl++;
2750 ++i;
2751 }
2752 return numEndl <= maxNewlines && i < m_inputLength && m_input[i] == '[';
2753}
2754
2755/*!
2756 Skips to the next non-space character or EOL.
2757 */
2758void DocParser::skipSpacesOnLine()
2759{
2760 while ((m_position < m_input.size()) && m_input[m_position].isSpace()
2761 && (m_input[m_position].unicode() != '\n'))
2762 ++m_position;
2763}
2764
2765/*!
2766 Skips spaces and one EOL.
2767 */
2768void DocParser::skipSpacesOrOneEndl()
2769{
2770 qsizetype firstEndl = -1;
2771 while (m_position < m_input.size() && m_input[m_position].isSpace()) {
2772 QChar ch = m_input[m_position];
2773 if (ch == '\n') {
2774 if (firstEndl == -1) {
2775 firstEndl = m_position;
2776 } else {
2777 m_position = firstEndl;
2778 break;
2779 }
2780 }
2781 ++m_position;
2782 }
2783}
2784
2785void DocParser::skipAllSpaces()
2786{
2787 while (m_position < m_inputLength && m_input[m_position].isSpace())
2788 ++m_position;
2789}
2790
2791void DocParser::skipToNextPreprocessorCommand()
2792{
2793 QRegularExpression rx("\\\\‍(?:" + cmdName(CMD_IF) + QLatin1Char('|') + cmdName(CMD_ELSE)
2794 + QLatin1Char('|') + cmdName(CMD_ENDIF) + ")\\b");
2795 auto match = rx.match(m_input, m_position + 1); // ### + 1 necessary?
2796
2797 if (!match.hasMatch())
2798 m_position = m_input.size();
2799 else
2800 m_position = match.capturedStart();
2801}
2802
2804{
2805 switch (cmd) {
2806 case CMD_BADCODE:
2807 return CMD_ENDCODE;
2808 case CMD_CODE:
2809 return CMD_ENDCODE;
2810 case CMD_COMPARESWITH:
2811 return CMD_ENDCOMPARESWITH;
2812 case CMD_DETAILS:
2813 return CMD_ENDDETAILS;
2814 case CMD_DIV:
2815 return CMD_ENDDIV;
2816 case CMD_QML:
2817 return CMD_ENDQML;
2818 case CMD_FOOTNOTE:
2819 return CMD_ENDFOOTNOTE;
2820 case CMD_LEGALESE:
2821 return CMD_ENDLEGALESE;
2822 case CMD_LINK:
2823 return CMD_ENDLINK;
2824 case CMD_LIST:
2825 return CMD_ENDLIST;
2826 case CMD_OMIT:
2827 return CMD_ENDOMIT;
2828 case CMD_QUOTATION:
2829 return CMD_ENDQUOTATION;
2830 case CMD_RAW:
2831 return CMD_ENDRAW;
2832 case CMD_SECTION1:
2833 return CMD_ENDSECTION1;
2834 case CMD_SECTION2:
2835 return CMD_ENDSECTION2;
2836 case CMD_SECTION3:
2837 return CMD_ENDSECTION3;
2838 case CMD_SECTION4:
2839 return CMD_ENDSECTION4;
2840 case CMD_SIDEBAR:
2841 return CMD_ENDSIDEBAR;
2842 case CMD_TABLE:
2843 return CMD_ENDTABLE;
2844 case CMD_TOC:
2845 return CMD_ENDTOC;
2846 default:
2847 return cmd;
2848 }
2849}
2850
2851QString DocParser::cmdName(int cmd)
2852{
2853 return cmds[cmd].name;
2854}
2855
2856QString DocParser::endCmdName(int cmd)
2857{
2858 return cmdName(endCmdFor(cmd));
2859}
2860
2861QString DocParser::untabifyEtc(const QString &str)
2862{
2863 QString result;
2864 result.reserve(str.size());
2865 int column = 0;
2866
2867 for (const auto &character : str) {
2868 if (character == QLatin1Char('\r'))
2869 continue;
2870 if (character == QLatin1Char('\t')) {
2871 result += &" "[column % s_tabSize];
2872 column = ((column / s_tabSize) + 1) * s_tabSize;
2873 continue;
2874 }
2875 if (character == QLatin1Char('\n')) {
2876 while (result.endsWith(QLatin1Char(' ')))
2877 result.chop(1);
2878 result += character;
2879 column = 0;
2880 continue;
2881 }
2882 result += character;
2883 ++column;
2884 }
2885
2886 while (result.endsWith("\n\n"))
2887 result.truncate(result.size() - 1);
2888 while (result.startsWith(QLatin1Char('\n')))
2889 result = result.mid(1);
2890
2891 return result;
2892}
2893
2894int DocParser::indentLevel(const QString &str)
2895{
2896 int minIndent = INT_MAX;
2897 int column = 0;
2898
2899 for (const auto &character : str) {
2900 if (character == '\n') {
2901 column = 0;
2902 } else {
2903 if (character != ' ' && column < minIndent)
2904 minIndent = column;
2905 ++column;
2906 }
2907 }
2908 return minIndent;
2909}
2910
2911QString DocParser::dedent(int level, const QString &str)
2912{
2913 if (level == 0)
2914 return str;
2915
2916 QString result;
2917 int column = 0;
2918
2919 for (const auto &character : str) {
2920 if (character == QLatin1Char('\n')) {
2921 result += '\n';
2922 column = 0;
2923 } else {
2924 if (column >= level)
2925 result += character;
2926 ++column;
2927 }
2928 }
2929 return result;
2930}
2931
2932/*!
2933 Returns \c true if \a atom represents a code snippet.
2934 */
2935bool DocParser::isCode(const Atom *atom)
2936{
2937 Atom::AtomType type = atom->type();
2938 return (type == Atom::Code || type == Atom::Qml);
2939}
2940
2941/*!
2942 Returns \c true if \a atom represents quoting information.
2943 */
2944bool DocParser::isQuote(const Atom *atom)
2945{
2946 Atom::AtomType type = atom->type();
2947 return (type == Atom::CodeQuoteArgument || type == Atom::CodeQuoteCommand
2948 || type == Atom::SnippetCommand || type == Atom::SnippetIdentifier
2949 || type == Atom::SnippetLocation);
2950}
2951
2952/*!
2953 \internal
2954 Returns a marker for the given \a language.
2955 Returns \nullptr and warns if no suitable marker is found.
2956*/
2957CodeMarker *DocParser::markerForLanguage(const QString &language)
2958{
2959 CodeMarker *marker = CodeMarker::markerForLanguage(language.toLower());
2960 // Suppress warning for explicitly allowed languages.
2961 if (!marker && !s_allowedLanguages.contains(language, Qt::CaseInsensitive))
2962 location().warning(QStringLiteral("Unrecognized markup language '%1'").arg(language));
2963
2964 return marker;
2965}
2966
2967/*!
2968 \internal
2969 Processes the arguments passed to the \\compareswith block command.
2970 The arguments are stored as text within the first atom of the block
2971 (Atom::ComparesLeft).
2972
2973 Extracts the comparison category and the list of types, and stores
2974 the information into a map accessed via \a priv.
2975*/
2976static void processComparesWithCommand(DocPrivate *priv, const Location &location)
2977{
2978 static auto take_while = [](QStringView input, auto predicate) {
2979 QStringView::size_type end{0};
2980
2981 while (end < input.size() && std::invoke(predicate, input[end]))
2982 ++end;
2983
2984 return std::make_tuple(input.sliced(0, end), input.sliced(end));
2985 };
2986
2987 static auto peek = [](QStringView input, QChar c) {
2988 return !input.empty() && input.first() == c;
2989 };
2990
2991 static auto skip_one = [](QStringView input) {
2992 if (input.empty()) return std::make_tuple(QStringView{}, input);
2993 else return std::make_tuple(input.sliced(0, 1), input.sliced(1));
2994 };
2995
2996 static auto enclosed = [](QStringView input, QChar open, QChar close) {
2997 if (!peek(input, open)) return std::make_tuple(QStringView{}, input);
2998
2999 auto [opened, without_open] = skip_one(input);
3000 auto [parsed, remaining] = take_while(without_open, [close](QChar c){ return c != close; });
3001
3002 if (remaining.empty()) return std::make_tuple(QStringView{}, input);
3003
3004 auto [closed, without_close] = skip_one(remaining);
3005
3006 return std::make_tuple(parsed.trimmed(), without_close);
3007 };
3008
3009 static auto one_of = [](auto first, auto second) {
3010 return [first, second](QStringView input) {
3011 auto [parsed, remaining] = std::invoke(first, input);
3012
3013 if (parsed.empty()) return std::invoke(second, input);
3014 else return std::make_tuple(parsed, remaining);
3015 };
3016 };
3017
3018 static auto collect = [](QStringView input, auto parser) {
3019 QStringList collected{};
3020
3021 while (true) {
3022 auto [parsed, remaining] = std::invoke(parser, input);
3023
3024 if (parsed.empty()) break;
3025 collected.append(parsed.toString());
3026
3027 input = remaining;
3028 };
3029
3030 return collected;
3031 };
3032
3033 static auto spaces = [](QStringView input) {
3034 return take_while(input, [](QChar c){ return c.isSpace(); });
3035 };
3036
3037 static auto word = [](QStringView input) {
3038 return take_while(input, [](QChar c){ return !c.isSpace(); });
3039 };
3040
3041 static auto parse_argument = [](QStringView input) {
3042 auto [_, without_spaces] = spaces(input);
3043
3044 return one_of(
3045 [](QStringView input){ return enclosed(input, '{', '}'); },
3046 word
3047 )(without_spaces);
3048 };
3049
3050 const QString cmd{DocParser::cmdName(CMD_COMPARESWITH)};
3052
3053 auto *atom = text.firstAtom();
3054 QStringList segments = collect(atom->string(), parse_argument);
3055
3056 QString categoryString;
3057 if (!segments.isEmpty())
3058 categoryString = segments.takeFirst();
3059 auto category = comparisonCategoryFromString(categoryString.toStdString());
3060
3061 if (category == ComparisonCategory::None) {
3062 location.warning(u"Invalid argument to \\%1 command: `%2`"_s.arg(cmd, categoryString),
3063 u"Valid arguments are `strong`, `weak`, `partial`, or `equality`."_s);
3064 return;
3065 }
3066
3067 if (segments.isEmpty()) {
3068 location.warning(u"Missing argument to \\%1 command."_s.arg(cmd),
3069 u"Provide at least one type name, or a list of types separated by spaces."_s);
3070 return;
3071 }
3072
3073 // Store cleaned-up type names back into the atom
3074 segments.removeDuplicates();
3075 atom->setString(segments.join(QLatin1Char(';')));
3076
3077 // Add an entry to meta-command map for error handling in CppCodeParser
3078 priv->m_metaCommandMap[cmd].append(ArgPair(categoryString, atom->string()));
3079 priv->m_metacommandsUsed.insert(cmd);
3080
3082 const auto end{priv->extra->m_comparesWithMap.cend()};
3083 priv->extra->m_comparesWithMap.insert(end, category, text);
3084}
3085
3086QT_END_NAMESPACE
#define ATOM_FORMATTING_TELETYPE
Definition atom.h:213
#define ATOM_FORMATTING_UNDERLINE
Definition atom.h:216
#define ATOM_FORMATTING_NOTRANSLATE
Definition atom.h:208
#define ATOM_FORMATTING_SPAN
Definition atom.h:210
#define ATOM_FORMATTING_SUBSCRIPT
Definition atom.h:211
#define ATOM_FORMATTING_BOLD
Definition atom.h:204
#define ATOM_FORMATTING_TRADEMARK
Definition atom.h:214
#define ATOM_LIST_VALUE
Definition atom.h:220
#define ATOM_FORMATTING_ITALIC
Definition atom.h:206
#define ATOM_FORMATTING_LINK
Definition atom.h:207
#define ATOM_FORMATTING_SUPERSCRIPT
Definition atom.h:212
#define ATOM_FORMATTING_INDEX
Definition atom.h:205
#define ATOM_FORMATTING_UICONTROL
Definition atom.h:215
#define ATOM_FORMATTING_PARAMETER
Definition atom.h:209
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
@ 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
@ Link
Definition atom.h:63
@ 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
@ 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:85
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:461
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:313
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:442
#define CONFIG_IGNOREWORDS
Definition config.h:398
#define CONFIG_CODELANGUAGES
Definition config.h:370
#define CONFIG_QUOTINGINFORMATION
Definition config.h:430
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