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