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