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