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