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
246bool DocParser::s_quoting = false;
247FileResolver *DocParser::file_resolver{ nullptr };
248static void processComparesWithCommand(DocPrivate *priv, const Location &location);
249
250/*!
251 Returns a cleaned version of the given \a link text. This replaces escaped
252 hash characters (\\#) with hash characters (#) and removes the URL scheme
253 prefix, if present.
254*/
255static QString cleanLink(const QString &link)
256{
257 qsizetype colonPos = link.indexOf(':');
258 QString cleaned{link};
259 cleaned.replace("\\#"_L1, "#"_L1);
260 if ((colonPos == -1) || (!link.startsWith("file:") && !link.startsWith("mailto:")))
261 return cleaned;
262 return cleaned.mid(colonPos + 1).simplified();
263}
264
265void DocParser::initialize(const Config &config, FileResolver &file_resolver)
266{
267 s_tabSize = config.get(CONFIG_TABSIZE).asInt();
268 s_ignoreWords = config.get(CONFIG_IGNOREWORDS).asStringList();
269
270 int i = 0;
271 while (cmds[i].name) {
272 s_utilities.cmdHash.insert(cmds[i].name, cmds[i].no);
273
274 if (cmds[i].no != i)
275 Location::internalError(QStringLiteral("command %1 missing").arg(i));
276 ++i;
277 }
278
279 // If any of the formats define quotinginformation, activate quoting
280 DocParser::s_quoting = config.get(CONFIG_QUOTINGINFORMATION).asBool();
281 const auto &outputFormats = config.getOutputFormats();
282 for (const auto &format : outputFormats)
283 DocParser::s_quoting = DocParser::s_quoting
284 || config.get(format + Config::dot + CONFIG_QUOTINGINFORMATION).asBool();
285
286 // KLUDGE: file_resolver is temporarily a pointer. See the
287 // comment for file_resolver in the header file for more context.
288 DocParser::file_resolver = &file_resolver;
289}
290
291/*!
292 Parse the \a source string to build a Text data structure
293 in \a docPrivate. The Text data structure is a linked list
294 of Atoms.
295
296 \a metaCommandSet is the set of metacommands that may be
297 found in \a source. These metacommands are not markup text
298 commands. They are topic commands and related metacommands.
299 */
300void DocParser::parse(const QString &source, DocPrivate *docPrivate,
301 const QSet<QString> &metaCommandSet, const QSet<QString> &possibleTopics)
302{
303 m_input = source;
304 m_position = 0;
305 m_inputLength = m_input.size();
306 m_cachedLocation = docPrivate->m_start_loc;
307 m_cachedPosition = 0;
308 m_private = docPrivate;
309 m_private->m_text << Atom::Nop;
310 m_private->m_topics.clear();
311
312 m_paragraphState = OutsideParagraph;
313 m_inTableHeader = false;
314 m_inTableRow = false;
315 m_inTableItem = false;
316 m_indexStartedParagraph = false;
317 m_pendingParagraphLeftType = Atom::Nop;
318 m_pendingParagraphRightType = Atom::Nop;
319
320 m_braceDepth = 0;
321 m_currentSection = Doc::NoSection;
322 m_openedCommands.push(CMD_OMIT);
323 m_quoter.reset();
324
325 CodeMarker *marker = nullptr;
326 Atom *currentLinkAtom = nullptr;
327 QString p1, p2;
328 QStack<bool> preprocessorSkipping;
329 int numPreprocessorSkipping = 0;
330
331 while (m_position < m_inputLength) {
332 QChar ch = m_input.at(m_position);
333
334 switch (ch.unicode()) {
335 case '\\': {
336 QString cmdStr;
337 m_backslashPosition = m_position;
338 ++m_position;
339 while (m_position < m_inputLength) {
340 ch = m_input.at(m_position);
341 if (ch.isLetterOrNumber()) {
342 cmdStr += ch;
343 ++m_position;
344 } else {
345 break;
346 }
347 }
348 m_endPosition = m_position;
349 if (cmdStr.isEmpty()) {
350 if (m_position < m_inputLength) {
351 QChar nextCh = m_input.at(m_position);
352 if (nextCh == '\\') {
353 appendEscapedIdentifier();
354 } else if (nextCh.isSpace()) {
355 enterPara();
356 skipAllSpaces();
357 appendChar(QLatin1Char(' '));
358 } else {
359 enterPara();
360 appendChar(m_input.at(m_position++));
361 }
362 }
363 } else {
364 // Ignore quoting atoms to make appendToCode()
365 // append to the correct atom.
366 if (!s_quoting || !isQuote(m_private->m_text.lastAtom()))
367 m_lastAtom = m_private->m_text.lastAtom();
368
369 int cmd = s_utilities.cmdHash.value(cmdStr, NOT_A_CMD);
370 switch (cmd) {
371 case CMD_A:
372 enterPara();
373 p1 = getArgument();
375 appendAtom(Atom(Atom::String, p1));
377 m_private->m_params.insert(p1);
378 break;
379 case CMD_BADCODE:
380 leavePara();
381 appendAtom(Atom(Atom::CodeBad,
382 getCode(CMD_BADCODE, marker, getMetaCommandArgument(cmdStr))));
383 break;
384 case CMD_BR:
385 enterPara();
386 appendAtom(Atom(Atom::BR));
387 break;
388 case CMD_BOLD:
389 location().warning(QStringLiteral("'\\bold' is deprecated. Use '\\b'"));
390 Q_FALLTHROUGH();
391 case CMD_B:
392 startFormat(ATOM_FORMATTING_BOLD, cmd);
393 break;
394 case CMD_BRIEF:
395 leavePara();
396 enterPara(Atom::BriefLeft, Atom::BriefRight);
397 break;
398 case CMD_C:
399 enterPara();
400 p1 = untabifyEtc(getArgument(ArgumentParsingOptions::Verbatim));
401 marker = CodeMarker::markerForCode(p1);
402 appendAtom(Atom(Atom::C, marker->markedUpCode(p1, nullptr, location())));
403 break;
404 case CMD_CAPTION:
405 leavePara();
406 enterPara(Atom::CaptionLeft, Atom::CaptionRight);
407 break;
408 case CMD_CODE:
409 // Only allow arguments on the same line as the command.
410 if (isLeftBracketAhead(0)) {
411 p1 = getBracketedArgument();
412 marker = CodeMarker::markerForLanguage(p1);
413 if (!marker)
414 location().warning(QStringLiteral("Unrecognized markup language '%1'").arg(p1));
415 } else {
416 p1 = ""_L1;
417 marker = nullptr;
418 }
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)), p1.toLower()));
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 leavePara();
829 QString rest = getRestOfLine();
830 if (s_quoting) {
831 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
832 appendAtom(Atom(Atom::CodeQuoteArgument, rest));
833 }
834 appendToCode(m_quoter.quoteLine(location(), cmdStr, rest));
835 break;
836 }
837 case CMD_PRINTTO: {
838 leavePara();
839 QString rest = getRestOfLine();
840 if (s_quoting) {
841 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
842 appendAtom(Atom(Atom::CodeQuoteArgument, rest));
843 }
844 appendToCode(m_quoter.quoteTo(location(), cmdStr, rest));
845 break;
846 }
847 case CMD_PRINTUNTIL: {
848 leavePara();
849 QString rest = getRestOfLine();
850 if (s_quoting) {
851 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
852 appendAtom(Atom(Atom::CodeQuoteArgument, rest));
853 }
854 appendToCode(m_quoter.quoteUntil(location(), cmdStr, rest));
855 break;
856 }
857 case CMD_QUOTATION:
858 if (openCommand(cmd)) {
859 leavePara();
860 appendAtom(Atom(Atom::QuotationLeft));
861 }
862 break;
863 case CMD_QUOTEFILE: {
864 leavePara();
865
866 QString fileName = getArgument();
867 quoteFromFile(fileName);
868 if (s_quoting) {
869 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
870 appendAtom(Atom(Atom::CodeQuoteArgument, fileName));
871 }
872 appendAtom(Atom(Atom::Code, m_quoter.quoteTo(location(), cmdStr, QString())));
873 m_quoter.reset();
874 break;
875 }
876 case CMD_QUOTEFROMFILE: {
877 leavePara();
878 QString arg = getArgument();
879 if (s_quoting) {
880 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
881 appendAtom(Atom(Atom::CodeQuoteArgument, arg));
882 }
883 quoteFromFile(arg);
884 break;
885 }
886 case CMD_RAW:
887 leavePara();
888 p1 = getRestOfLine();
889 if (p1.isEmpty())
890 location().warning(QStringLiteral("Missing format name after '\\%1'")
891 .arg(cmdName(CMD_RAW)));
892 appendAtom(Atom(Atom::FormatIf, p1));
893 appendAtom(Atom(Atom::RawString, untabifyEtc(getUntilEnd(cmd))));
894 appendAtom(Atom(Atom::FormatElse));
895 appendAtom(Atom(Atom::FormatEndif));
896 break;
897 case CMD_ROW:
898 if (m_openedCommands.top() == CMD_TABLE) {
899 p1.clear();
900 if (isLeftBraceAhead())
901 p1 = getArgument(ArgumentParsingOptions::Verbatim);
902 leaveTableRow();
903 appendAtom(Atom(Atom::TableRowLeft, p1));
904 m_inTableRow = true;
905 } else {
906 if (m_openedCommands.contains(CMD_TABLE))
907 location().warning(QStringLiteral("Cannot use '\\%1' within '\\%2'")
908 .arg(cmdName(CMD_ROW),
909 cmdName(m_openedCommands.top())));
910 else
911 location().warning(QStringLiteral("Cannot use '\\%1' outside of '\\%2'")
912 .arg(cmdName(CMD_ROW), cmdName(CMD_TABLE)));
913 }
914 break;
915 case CMD_SA:
916 parseAlso();
917 break;
918 case CMD_SECTION1:
919 startSection(Doc::Section1, cmd);
920 break;
921 case CMD_SECTION2:
922 startSection(Doc::Section2, cmd);
923 break;
924 case CMD_SECTION3:
925 startSection(Doc::Section3, cmd);
926 break;
927 case CMD_SECTION4:
928 startSection(Doc::Section4, cmd);
929 break;
930 case CMD_SIDEBAR:
931 if (openCommand(cmd)) {
932 leavePara();
933 appendAtom(Atom(Atom::SidebarLeft));
934 }
935 break;
936 case CMD_SKIPLINE: {
937 leavePara();
938 QString rest = getRestOfLine();
939 if (s_quoting) {
940 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
941 appendAtom(Atom(Atom::CodeQuoteArgument, rest));
942 }
943 m_quoter.quoteLine(location(), cmdStr, rest);
944 break;
945 }
946 case CMD_SKIPTO: {
947 leavePara();
948 QString rest = getRestOfLine();
949 if (s_quoting) {
950 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
951 appendAtom(Atom(Atom::CodeQuoteArgument, rest));
952 }
953 m_quoter.quoteTo(location(), cmdStr, rest);
954 break;
955 }
956 case CMD_SKIPUNTIL: {
957 leavePara();
958 QString rest = getRestOfLine();
959 if (s_quoting) {
960 appendAtom(Atom(Atom::CodeQuoteCommand, cmdStr));
961 appendAtom(Atom(Atom::CodeQuoteArgument, rest));
962 }
963 m_quoter.quoteUntil(location(), cmdStr, rest);
964 break;
965 }
966 case CMD_SPAN:
967 p1 = ATOM_FORMATTING_SPAN + getArgument(ArgumentParsingOptions::Verbatim);
968 startFormat(p1, cmd);
969 break;
970 case CMD_SNIPPET: {
971 leavePara();
972 QString snippet = getArgument();
973 QString identifier = getRestOfLine();
974 if (s_quoting) {
975 appendAtom(Atom(Atom::SnippetCommand, cmdStr));
976 appendAtom(Atom(Atom::SnippetLocation, snippet));
977 appendAtom(Atom(Atom::SnippetIdentifier, identifier));
978 }
979 marker = CodeMarker::markerForFileName(snippet);
980 quoteFromFile(snippet);
981 appendToCode(m_quoter.quoteSnippet(location(), identifier), marker->atomType());
982 break;
983 }
984 case CMD_SUB:
985 startFormat(ATOM_FORMATTING_SUBSCRIPT, cmd);
986 break;
987 case CMD_SUP:
988 startFormat(ATOM_FORMATTING_SUPERSCRIPT, cmd);
989 break;
990 case CMD_TABLE:
991 leaveValueList();
992 p1 = getOptionalArgument();
993 p2 = getOptionalArgument();
994 if (openCommand(cmd)) {
995 leavePara();
996 appendAtom(Atom(Atom::TableLeft, p1, p2));
997 m_inTableHeader = false;
998 m_inTableRow = false;
999 m_inTableItem = false;
1000 }
1001 break;
1003 location().report("\\%1 is deprecated and will be removed in a future version."_L1.arg(cmdName(cmd)));
1004 if (isLeftBraceAhead())
1005 getArgument();
1006 break;
1007 case CMD_TARGET:
1008 if (m_openedCommands.top() == CMD_TABLE && !m_inTableItem) {
1009 location().warning("Found a \\target command outside table item in a table.\n"
1010 "Move the \\target inside the \\li to resolve this warning.");
1011 }
1012 insertTarget(getRestOfLine());
1013 break;
1014 case CMD_TITLE:
1015 leavePara();
1016 enterPara(Atom::TitleLeft, Atom::TitleRight);
1017 break;
1018 case CMD_TM:
1019 // Ignore command while parsing \section<N> or \title argument
1020 if (m_paragraphState != InSingleLineParagraph)
1021 startFormat(ATOM_FORMATTING_TRADEMARK, cmd);
1022 break;
1023 case CMD_TOC:
1024 if (openCommand(cmd)) {
1025 leavePara();
1026 appendAtom(Atom(Atom::TableOfContentsLeft));
1027 }
1028 break;
1029 case CMD_ENDTOC:
1030 if (closeCommand(cmd)) {
1031 leavePara();
1032 appendAtom(Atom(Atom::TableOfContentsRight));
1033 }
1034 break;
1035 case CMD_TT:
1036 startFormat(ATOM_FORMATTING_TELETYPE, cmd);
1037 break;
1038 case CMD_UICONTROL:
1039 startFormat(ATOM_FORMATTING_UICONTROL, cmd);
1040 break;
1041 case CMD_UNDERLINE:
1042 startFormat(ATOM_FORMATTING_UNDERLINE, cmd);
1043 break;
1044 case CMD_UNICODE: {
1045 enterPara();
1046 p1 = getArgument();
1047 bool ok;
1048 uint unicodeChar = p1.toUInt(&ok, 0);
1049 if (!ok || (unicodeChar == 0x0000) || (unicodeChar > 0xFFFE))
1050 location().warning(
1051 QStringLiteral("Invalid Unicode character '%1' specified with '%2'")
1052 .arg(p1, cmdName(CMD_UNICODE)));
1053 else
1054 appendAtom(Atom(Atom::String, QChar(unicodeChar)));
1055 break;
1056 }
1057 case CMD_VALUE:
1058 leaveValue();
1059 if (m_openedLists.top().style() == OpenedList::Value) {
1060 QString p2;
1061 p1 = getArgument();
1062 if (p1.startsWith(QLatin1String("[since "))
1063 && p1.endsWith(QLatin1String("]"))) {
1064 p2 = p1.mid(7, p1.size() - 8);
1065 p1 = getArgument();
1066 }
1067 if (!m_private->m_enumItemList.contains(p1))
1068 m_private->m_enumItemList.append(p1);
1069
1070 m_openedLists.top().next();
1071 appendAtom(Atom(Atom::ListTagLeft, ATOM_LIST_VALUE));
1072 appendAtom(Atom(Atom::String, p1));
1073 appendAtom(Atom(Atom::ListTagRight, ATOM_LIST_VALUE));
1074 if (!p2.isEmpty()) {
1075 appendAtom(Atom(Atom::SinceTagLeft, ATOM_LIST_VALUE));
1076 appendAtom(Atom(Atom::String, p2));
1077 appendAtom(Atom(Atom::SinceTagRight, ATOM_LIST_VALUE));
1078 }
1079 appendAtom(Atom(Atom::ListItemLeft, ATOM_LIST_VALUE));
1080
1081 skipSpacesOrOneEndl();
1082 if (isBlankLine())
1083 appendAtom(Atom(Atom::Nop));
1084 } else {
1085 // ### unknown problems
1086 }
1087 break;
1088 case CMD_WARNING:
1089 leavePara();
1090 enterPara(Atom::WarningLeft, Atom::WarningRight);
1091 break;
1092 case CMD_OVERLOAD:
1093 cmd_overload();
1094 break;
1095 case NOT_A_CMD:
1096 if (metaCommandSet.contains(cmdStr)) {
1097 QString arg;
1098 QString bracketedArg;
1099 m_private->m_metacommandsUsed.insert(cmdStr);
1100 if (isLeftBracketAhead())
1101 bracketedArg = getBracketedArgument();
1102 // Force a linebreak after \obsolete or \deprecated
1103 // to treat potential arguments as a new text paragraph.
1104 if (m_position < m_inputLength
1105 && (cmdStr == QLatin1String("obsolete")
1106 || cmdStr == QLatin1String("deprecated")))
1107 m_input[m_position] = '\n';
1108 else
1109 arg = getMetaCommandArgument(cmdStr);
1110 m_private->m_metaCommandMap[cmdStr].append(ArgPair(arg, bracketedArg));
1111 if (possibleTopics.contains(cmdStr)) {
1112 if (!cmdStr.endsWith(QLatin1String("propertygroup")))
1113 m_private->m_topics.append(Topic(cmdStr, std::move(arg)));
1114 }
1115 } else if (s_utilities.macroHash.contains(cmdStr)) {
1116 const Macro &macro = s_utilities.macroHash.value(cmdStr);
1117 QStringList macroArgs;
1118 int numPendingFi = 0;
1119 int numFormatDefs = 0;
1120 for (auto it = macro.m_otherDefs.constBegin();
1121 it != macro.m_otherDefs.constEnd(); ++it) {
1122 if (it.key() != "match") {
1123 if (numFormatDefs == 0)
1124 macroArgs = getMacroArguments(cmdStr, macro);
1125 appendAtom(Atom(Atom::FormatIf, it.key()));
1126 expandMacro(*it, macroArgs);
1127 ++numFormatDefs;
1128 if (it == macro.m_otherDefs.constEnd()) {
1129 appendAtom(Atom(Atom::FormatEndif));
1130 } else {
1131 appendAtom(Atom(Atom::FormatElse));
1132 ++numPendingFi;
1133 }
1134 }
1135 }
1136 while (numPendingFi-- > 0)
1137 appendAtom(Atom(Atom::FormatEndif));
1138
1139 if (!macro.m_defaultDef.isEmpty()) {
1140 if (numFormatDefs > 0) {
1141 macro.m_defaultDefLocation.warning(
1142 QStringLiteral("Macro cannot have both "
1143 "format-specific and qdoc-"
1144 "syntax definitions"));
1145 } else {
1146 QString expanded = expandMacroToString(cmdStr, macro);
1147 m_input.replace(m_backslashPosition,
1148 m_endPosition - m_backslashPosition, expanded);
1149 m_inputLength = m_input.size();
1150 m_position = m_backslashPosition;
1151 }
1152 }
1153 } else if (isAutoLinkString(cmdStr)) {
1154 appendWord(cmdStr);
1155 } else {
1156 if (!cmdStr.endsWith("propertygroup")) {
1157 // The QML property group commands are no longer required
1158 // for grouping QML properties. They are allowed but ignored.
1159 location().warning(QStringLiteral("Unknown command '\\%1'").arg(cmdStr),
1160 detailsUnknownCommand(metaCommandSet, cmdStr));
1161 }
1162 enterPara();
1163 appendAtom(Atom(Atom::UnknownCommand, cmdStr));
1164 }
1165 }
1166 } // case '\\' (qdoc markup command)
1167 break;
1168 }
1169 case '-': { // Catch en-dash (--) and em-dash (---) markup here.
1170 enterPara();
1171 qsizetype dashCount = 1;
1172 ++m_position;
1173
1174 // Figure out how many hyphens in a row.
1175 while ((m_position < m_inputLength) && (m_input.at(m_position) == '-')) {
1176 ++dashCount;
1177 ++m_position;
1178 }
1179
1180 if (dashCount == 3) {
1181 // 3 hyphens, append an em-dash character.
1182 const QChar emDash(8212);
1183 appendChar(emDash);
1184 } else if (dashCount == 2) {
1185 // 2 hyphens; append an en-dash character.
1186 const QChar enDash(8211);
1187 appendChar(enDash);
1188 } else {
1189 // dashCount is either one or more than three. Append a hyphen
1190 // the appropriate number of times. This ensures '----' doesn't
1191 // end up as an em-dash followed by a hyphen in the output.
1192 for (qsizetype i = 0; i < dashCount; ++i)
1193 appendChar('-');
1194 }
1195 break;
1196 }
1197 case '{':
1198 enterPara();
1199 appendChar('{');
1200 ++m_braceDepth;
1201 ++m_position;
1202 break;
1203 case '}': {
1204 --m_braceDepth;
1205 ++m_position;
1206
1207 auto format = m_pendingFormats.find(m_braceDepth);
1208 if (format == m_pendingFormats.end()) {
1209 enterPara();
1210 appendChar('}');
1211 } else {
1212 const auto &last{m_private->m_text.lastAtom()->string()};
1213 appendAtom(Atom(Atom::FormattingRight, *format));
1214 if (*format == ATOM_FORMATTING_INDEX) {
1215 if (m_indexStartedParagraph)
1216 skipAllSpaces();
1217 } else if (*format == ATOM_FORMATTING_LINK) {
1218 // hack for C++ to support links like
1219 // \l{QString::}{count()}
1220 if (currentLinkAtom && currentLinkAtom->string().endsWith("::")) {
1221 QString suffix =
1222 Text::subText(currentLinkAtom, m_private->m_text.lastAtom())
1223 .toString();
1224 currentLinkAtom->concatenateString(suffix);
1225 }
1226 currentLinkAtom = nullptr;
1227 } else if (*format == ATOM_FORMATTING_TRADEMARK) {
1228 m_private->m_text.lastAtom()->append(last);
1229 }
1230 m_pendingFormats.erase(format);
1231 }
1232 break;
1233 }
1234 // Do not parse content after '//!' comments
1235 case '/': {
1236 if (m_position + 2 < m_inputLength)
1237 if (m_input.at(m_position + 1) == '/')
1238 if (m_input.at(m_position + 2) == '!') {
1239 m_position += 2;
1240 getRestOfLine();
1241 if (m_input.at(m_position - 1) == '\n')
1242 --m_position;
1243 break;
1244 }
1245 Q_FALLTHROUGH(); // fall through
1246 }
1247 default: {
1248 bool newWord;
1249 switch (m_private->m_text.lastAtom()->type()) {
1250 case Atom::ParaLeft:
1251 newWord = true;
1252 break;
1253 default:
1254 newWord = false;
1255 }
1256
1257 if (m_paragraphState == OutsideParagraph) {
1258 if (ch.isSpace()) {
1259 ++m_position;
1260 newWord = false;
1261 } else {
1262 enterPara();
1263 newWord = true;
1264 }
1265 } else {
1266 if (ch.isSpace()) {
1267 ++m_position;
1268 if ((ch == '\n')
1269 && (m_paragraphState == InSingleLineParagraph || isBlankLine())) {
1270 leavePara();
1271 newWord = false;
1272 } else {
1273 appendChar(' ');
1274 newWord = true;
1275 }
1276 } else {
1277 newWord = true;
1278 }
1279 }
1280
1281 if (newWord) {
1282 qsizetype startPos = m_position;
1283 // No auto-linking inside links or (section) titles
1284 bool autolink = (!m_pendingFormats.isEmpty() &&
1285 m_pendingFormats.last() == ATOM_FORMATTING_LINK)
1286 || m_paragraphState == InSingleLineParagraph ?
1287 false : isAutoLinkString(m_input, m_position);
1288 if (m_position == startPos) {
1289 if (!ch.isSpace()) {
1290 appendChar(ch);
1291 ++m_position;
1292 }
1293 } else {
1294 QString word = m_input.mid(startPos, m_position - startPos);
1295 if (autolink) {
1296 if (s_ignoreWords.contains(word) || word.startsWith(QString("__")))
1297 appendWord(word);
1298 else
1299 appendAtom(Atom(Atom::AutoLink, word));
1300 } else {
1301 appendWord(word);
1302 }
1303 }
1304 }
1305 } // default:
1306 } // switch (ch.unicode())
1307 }
1308 leaveValueList();
1309
1310 // for compatibility
1311 if (m_openedCommands.top() == CMD_LEGALESE) {
1312 appendAtom(Atom(Atom::LegaleseRight));
1313 m_openedCommands.pop();
1314 }
1315
1316 if (m_openedCommands.top() != CMD_OMIT) {
1317 location().warning(
1318 QStringLiteral("Missing '\\%1'").arg(endCmdName(m_openedCommands.top())));
1319 } else if (preprocessorSkipping.size() > 0) {
1320 location().warning(QStringLiteral("Missing '\\%1'").arg(cmdName(CMD_ENDIF)));
1321 }
1322
1323 if (m_currentSection > Doc::NoSection) {
1324 appendAtom(Atom(Atom::SectionRight, QString::number(m_currentSection)));
1325 m_currentSection = Doc::NoSection;
1326 }
1327
1329}
1330
1331/*!
1332 Returns the current location.
1333 */
1334Location &DocParser::location()
1335{
1336 while (!m_openedInputs.isEmpty() && m_openedInputs.top() <= m_position) {
1337 m_cachedLocation.pop();
1338 m_cachedPosition = m_openedInputs.pop();
1339 }
1340 while (m_cachedPosition < m_position)
1341 m_cachedLocation.advance(m_input.at(m_cachedPosition++));
1342 return m_cachedLocation;
1343}
1344
1345QString DocParser::detailsUnknownCommand(const QSet<QString> &metaCommandSet, const QString &str)
1346{
1347 QSet<QString> commandSet = metaCommandSet;
1348 int i = 0;
1349 while (cmds[i].name != nullptr) {
1350 commandSet.insert(cmds[i].name);
1351 ++i;
1352 }
1353
1354 return "Maybe you meant '\\%1'?"_L1.arg(suggestName(str, commandSet));
1355}
1356
1357/*!
1358 \internal
1359
1360 Issues a warning about an empty or duplicate definition of either a
1361 \\target or \\keyword command (determined by \a cmdString) at
1362 \a location. \a duplicateDefinition is the target being processed;
1363 the already registered definition is \a previousDefinition.
1364 */
1365static void warnAboutEmptyOrPreexistingTarget(const Location &location, const QString &duplicateDefinition,
1366 const QString &cmdString, const QString &previousDefinition)
1367{
1368 if (duplicateDefinition.isEmpty()) {
1369 location.warning("Expected an argument for \\%1"_L1.arg(cmdString));
1370 } else {
1371 location.warning(
1372 "Duplicate %3 name '%1'. The previous occurrence is here: %2"_L1
1373 .arg(duplicateDefinition, previousDefinition, cmdString));
1374 }
1375}
1376
1377/*!
1378 \internal
1379
1380 \brief Registers \a target as a linkable entity.
1381
1382 The main purpose of this method is to register a target as defined
1383 along with its location, so that becomes a valid link target from other
1384 parts of the documentation.
1385
1386 If the \a target name is already registered, a warning is issued,
1387 as multiple definitions are problematic.
1388
1389 \sa insertKeyword, target-command
1390 */
1391void DocParser::insertTarget(const QString &target)
1392{
1393 if (target.isEmpty() || m_targetMap.contains(target))
1394 return warnAboutEmptyOrPreexistingTarget(location(), target,
1395 s_utilities.cmdHash.key(CMD_TARGET), m_targetMap[target].toString());
1396
1397 m_targetMap.insert(target, location());
1398 m_private->constructExtra();
1399
1400 appendAtom(Atom(Atom::Target, target));
1401 m_private->extra->m_targets.append(m_private->m_text.lastAtom());
1402}
1403
1404/*!
1405 \internal
1406
1407 \brief Registers \a keyword as a linkable entity.
1408
1409 The main purpose of this method is to register a keyword as defined
1410 along with its location, so that becomes a valid link target from other
1411 parts of the documentation.
1412
1413 If the \a keyword name is already registered, a warning is issued,
1414 as multiple definitions are problematic.
1415
1416 \sa insertTarget, keyword-command
1417 */
1418void DocParser::insertKeyword(const QString &keyword)
1419{
1420 if (keyword.isEmpty() || m_targetMap.contains(keyword))
1421 return warnAboutEmptyOrPreexistingTarget(location(), keyword,
1422 s_utilities.cmdHash.key(CMD_KEYWORD), m_targetMap[keyword].toString());
1423
1424 m_targetMap.insert(keyword, location());
1425 m_private->constructExtra();
1426
1427 appendAtom(Atom(Atom::Keyword, keyword));
1428 m_private->extra->m_keywords.append(m_private->m_text.lastAtom());
1429}
1430
1431void DocParser::include(const QString &fileName, const QString &identifier, const QStringList &parameters)
1432{
1433 if (location().depth() > 16)
1434 location().fatal(QStringLiteral("Too many nested '\\%1's").arg(cmdName(CMD_INCLUDE)));
1435 QString filePath = Config::instance().getIncludeFilePath(fileName);
1436 if (filePath.isEmpty()) {
1437 location().warning(QStringLiteral("Cannot find qdoc include file '%1'").arg(fileName));
1438 } else {
1439 QFile inFile(filePath);
1440 if (!inFile.open(QFile::ReadOnly)) {
1441 location().warning(
1442 QStringLiteral("Cannot open qdoc include file '%1'").arg(filePath));
1443 } else {
1444 location().push(fileName);
1445 QTextStream inStream(&inFile);
1446 QString includedContent = inStream.readAll();
1447 inFile.close();
1448
1449 if (identifier.isEmpty()) {
1450 expandArgumentsInString(includedContent, parameters);
1451 m_input.insert(m_position, includedContent);
1452 m_inputLength = m_input.size();
1453 m_openedInputs.push(m_position + includedContent.size());
1454 } else {
1455 QStringList lineBuffer = includedContent.split(QLatin1Char('\n'));
1456 qsizetype bufLen{lineBuffer.size()};
1457 qsizetype i;
1458 QStringView trimmedLine;
1459 for (i = 0; i < bufLen; ++i) {
1460 trimmedLine = QStringView{lineBuffer[i]}.trimmed();
1461 if (trimmedLine.startsWith(QLatin1String("//!")) &&
1462 trimmedLine.contains(identifier))
1463 break;
1464 }
1465 if (i < bufLen - 1) {
1466 ++i;
1467 } else {
1468 location().warning(
1469 QStringLiteral("Cannot find '%1' in '%2'").arg(identifier, filePath));
1470 return;
1471 }
1472 QString result;
1473 do {
1474 trimmedLine = QStringView{lineBuffer[i]}.trimmed();
1475 if (trimmedLine.startsWith(QLatin1String("//!")) &&
1476 trimmedLine.contains(identifier))
1477 break;
1478 else
1479 result += lineBuffer[i] + QLatin1Char('\n');
1480 ++i;
1481 } while (i < bufLen);
1482
1483 expandArgumentsInString(result, parameters);
1484 if (result.isEmpty()) {
1485 location().warning(QStringLiteral("Empty qdoc snippet '%1' in '%2'")
1486 .arg(identifier, filePath));
1487 } else {
1488 m_input.insert(m_position, result);
1489 m_inputLength = m_input.size();
1490 m_openedInputs.push(m_position + result.size());
1491 }
1492 }
1493 }
1494 }
1495}
1496
1497void DocParser::startFormat(const QString &format, int cmd)
1498{
1499 enterPara();
1500
1501 for (const auto &item : std::as_const(m_pendingFormats)) {
1502 if (item == format) {
1503 location().warning(QStringLiteral("Cannot nest '\\%1' commands").arg(cmdName(cmd)));
1504 return;
1505 }
1506 }
1507
1508 appendAtom(Atom(Atom::FormattingLeft, format));
1509
1510 if (isLeftBraceAhead()) {
1511 skipSpacesOrOneEndl();
1512 m_pendingFormats.insert(m_braceDepth, format);
1513 ++m_braceDepth;
1514 ++m_position;
1515 } else {
1516 const auto &arg{getArgument()};
1517 appendAtom(Atom(Atom::String, arg));
1518 appendAtom(Atom(Atom::FormattingRight, format));
1519 if (format == ATOM_FORMATTING_INDEX && m_indexStartedParagraph) {
1520 skipAllSpaces();
1521 m_indexStartedParagraph = false;
1522 } else if (format == ATOM_FORMATTING_TRADEMARK) {
1523 m_private->m_text.lastAtom()->append(arg);
1524 }
1525 }
1526}
1527
1528bool DocParser::openCommand(int cmd)
1529{
1530 int outer = m_openedCommands.top();
1531 bool ok = true;
1532
1533 if ((cmd == CMD_COMPARESWITH || cmd == CMD_TOC)
1534 && m_openedCommands.contains(cmd)) {
1535 location().warning(u"Cannot nest '\\%1' commands"_s.arg(cmdName(cmd)));
1536 return false;
1537 } else if (cmd != CMD_LINK) {
1538 if (outer == CMD_LIST) {
1539 ok = (cmd == CMD_FOOTNOTE || cmd == CMD_LIST);
1540 } else if (outer == CMD_SIDEBAR) {
1541 ok = (cmd == CMD_LIST || cmd == CMD_QUOTATION || cmd == CMD_SIDEBAR);
1542 } else if (outer == CMD_QUOTATION) {
1543 ok = (cmd == CMD_LIST);
1544 } else if (outer == CMD_TABLE) {
1545 ok = (cmd == CMD_LIST || cmd == CMD_FOOTNOTE || cmd == CMD_QUOTATION);
1546 } else if (outer == CMD_FOOTNOTE || outer == CMD_LINK) {
1547 ok = false;
1548 }
1549 }
1550
1551 if (ok) {
1552 m_openedCommands.push(cmd);
1553 } else {
1554 location().warning(
1555 QStringLiteral("Can't use '\\%1' in '\\%2'").arg(cmdName(cmd), cmdName(outer)));
1556 }
1557 return ok;
1558}
1559
1560/*!
1561 Returns \c true if \a word qualifies for auto-linking.
1562
1563 A word qualifies for auto-linking if either:
1564
1565 \list
1566 \li It is composed of only upper and lowercase characters
1567 \li AND It contains at least one uppercase character that is not
1568 the first character of word
1569 \li AND it contains at least two lowercase characters
1570 \endlist
1571
1572 Or
1573
1574 \list
1575 \li It is composed only of uppercase characters, lowercase
1576 characters, characters in [_@] and the \c {"::"} sequence.
1577 \li It contains at least one uppercase character that is not
1578 the first character of word or it contains at least one
1579 lowercase character
1580 \li AND it contains at least one character in [_@] or it
1581 contains at least one \c {"::"} sequence.
1582 \endlist
1583
1584 Inserting or suffixing, but not prefixing, any sequence in [0-9]+
1585 in a word that qualifies for auto-linking by the above rules
1586 preserves the auto-linkability of the word.
1587
1588 Suffixing the sequence \c {"()"} to a word that qualifies for
1589 auto-linking by the above rules preserves the auto-linkability of
1590 a word.
1591
1592 FInally, a word qualifies for auto-linking if:
1593
1594 \list
1595 \li It is composed of only uppercase characters, lowercase
1596 characters and the sequence \c {"()"}
1597 \li AND it contains one lowercase character and a sequence of zero, one
1598 or two upper or lowercase characters
1599 \li AND it contains exactly one sequence \c {"()"}
1600 \li AND it contains one sequence \c {"()"} as the last two
1601 characters of word
1602 \endlist
1603
1604 For example, \c {"fOo"}, \c {"FooBar"} and \c {"foobaR"} qualify
1605 for auto-linking by the first rule.
1606
1607 \c {"QT_DEBUG"}, \c {"::Qt"} and \c {"std::move"} qualifies for
1608 auto-linking by the second rule.
1609
1610 \c {"SIMDVector256"} qualifies by suffixing \c {"SIMDVector"},
1611 which qualifies by the first rule, with the sequence \c {"256"}
1612
1613 \c {"FooBar::Bar()"} qualifies by suffixing \c {"FooBar::Bar"},
1614 which qualifies by the first and second rule, with the sequence \c
1615 {"()"}.
1616
1617 \c {"Foo()"} and \c {"a()"} qualifies by the last rule.
1618
1619 Instead, \c {"Q"}, \c {"flower"}, \c {"_"} and \c {"()"} do not
1620 qualify for auto-linking.
1621
1622 The rules are intended as a heuristic to catch common cases in the
1623 Qt documentation where a word might represent an important
1624 documented element such as a class or a method that could be
1625 linked to while at the same time avoiding catching common words
1626 such as \c {"A"} or \c {"Nonetheless"}.
1627
1628 The heuristic assumes that Qt's codebase respects a style where
1629 camelCasing is the standard for most of the elements, a function
1630 call is identified by the use of parenthesis and certain elements,
1631 such as macros, might be fully uppercase.
1632
1633 Furthemore, it assumes that the Qt codebase is written in a
1634 language that has an identifier grammar similar to the one for
1635 C++.
1636*/
1637inline bool DocParser::isAutoLinkString(const QString &word)
1638{
1639 qsizetype start = 0;
1640 return isAutoLinkString(word, start) && (start == word.size());
1641}
1642
1643/*!
1644 Returns \c true if a prefix of a substring of \a word qualifies
1645 for auto-linking.
1646
1647 Respects the same parsing rules as the unary overload.
1648
1649 \a curPos defines the offset, from the first character of \ word,
1650 at which the parsed substring starts.
1651
1652 When the call completes, \a curPos represents the offset, from the
1653 first character of word, that is the successor of the offset of
1654 the last parsed character.
1655
1656 If the return value of the call is \c true, it is guaranteed that
1657 the prefix of the substring of \word that contains the characters
1658 from the initial value of \a curPos and up to but not including \a
1659 curPos qualifies for auto-linking.
1660
1661 If \a curPos is initially zero, the considered substring is the
1662 entirety of \a word.
1663*/
1664bool DocParser::isAutoLinkString(const QString &word, qsizetype &curPos)
1665{
1666 qsizetype len = word.size();
1667 qsizetype startPos = curPos;
1668 int numUppercase = 0;
1669 int numLowercase = 0;
1670 int numStrangeSymbols = 0;
1671
1672 while (curPos < len) {
1673 unsigned char latin1Ch = word.at(curPos).toLatin1();
1674 if (islower(latin1Ch)) {
1675 ++numLowercase;
1676 ++curPos;
1677 } else if (isupper(latin1Ch)) {
1678 if (curPos > startPos)
1679 ++numUppercase;
1680 ++curPos;
1681 } else if (isdigit(latin1Ch)) {
1682 if (curPos > startPos)
1683 ++curPos;
1684 else
1685 break;
1686 } else if (latin1Ch == '_' || latin1Ch == '@') {
1687 ++numStrangeSymbols;
1688 ++curPos;
1689 } else if ((latin1Ch == ':') && (curPos < len - 1)
1690 && (word.at(curPos + 1) == QLatin1Char(':'))) {
1691 ++numStrangeSymbols;
1692 curPos += 2;
1693 } else if (latin1Ch == '(') {
1694 if ((curPos < len - 1) && (word.at(curPos + 1) == QLatin1Char(')'))) {
1695 ++numStrangeSymbols;
1696 curPos += 2;
1697 }
1698
1699 break;
1700 } else {
1701 break;
1702 }
1703 }
1704
1705 return ((numUppercase >= 1 && numLowercase >= 2) || (numStrangeSymbols > 0 && (numUppercase + numLowercase >= 1)));
1706}
1707
1708bool DocParser::closeCommand(int endCmd)
1709{
1710 if (endCmdFor(m_openedCommands.top()) == endCmd && m_openedCommands.size() > 1) {
1711 m_openedCommands.pop();
1712 return true;
1713 } else {
1714 bool contains = false;
1715 QStack<int> opened2 = m_openedCommands;
1716 while (opened2.size() > 1) {
1717 if (endCmdFor(opened2.top()) == endCmd) {
1718 contains = true;
1719 break;
1720 }
1721 opened2.pop();
1722 }
1723
1724 if (contains) {
1725 while (endCmdFor(m_openedCommands.top()) != endCmd && m_openedCommands.size() > 1) {
1726 location().warning(
1727 QStringLiteral("Missing '\\%1' before '\\%2'")
1728 .arg(endCmdName(m_openedCommands.top()), cmdName(endCmd)));
1729 m_openedCommands.pop();
1730 }
1731 } else {
1732 location().warning(QStringLiteral("Unexpected '\\%1'").arg(cmdName(endCmd)));
1733 }
1734 return false;
1735 }
1736}
1737
1738void DocParser::startSection(Doc::Sections unit, int cmd)
1739{
1740 leaveValueList();
1741
1742 if (m_currentSection == Doc::NoSection) {
1743 m_currentSection = static_cast<Doc::Sections>(unit);
1744 m_private->constructExtra();
1745 } else {
1746 endSection(unit, cmd);
1747 }
1748
1749 appendAtom(Atom(Atom::SectionLeft, QString::number(unit)));
1750 m_private->constructExtra();
1751 m_private->extra->m_tableOfContents.append(m_private->m_text.lastAtom());
1752 m_private->extra->m_tableOfContentsLevels.append(unit);
1753 enterPara(Atom::SectionHeadingLeft, Atom::SectionHeadingRight, QString::number(unit));
1754 m_currentSection = unit;
1755}
1756
1757void DocParser::endSection(int, int) // (int unit, int endCmd)
1758{
1759 leavePara();
1760 appendAtom(Atom(Atom::SectionRight, QString::number(m_currentSection)));
1761 m_currentSection = (Doc::NoSection);
1762}
1763
1764/*!
1765 \internal
1766
1767 \brief Processes CMD_IMAGE and CMD_INLINEIMAGE, as specified by \a cmd.
1768
1769 The first argument to the command is the image file name. The rest of the
1770 line is an optional string that's used as the text description of the image
1771 (e.g. the HTML <img> alt attribute). The optional argument can be wrapped in
1772 curly braces, in which case it can span multiple lines.
1773
1774 This function may modify the optional argument by removing one pair of
1775 double quotes, if they wrap the string.
1776 */
1777void DocParser::cmd_image(int cmd) {
1778 Atom::AtomType imageAtom{};
1779 switch (cmd) {
1780 case CMD_IMAGE: {
1781 leaveValueList();
1782 imageAtom = Atom::AtomType::Image;
1783 break;
1784 }
1785 case CMD_INLINEIMAGE: {
1786 enterPara();
1787 imageAtom = Atom::AtomType::InlineImage;
1788 break;
1789 }
1790 default:
1791 break;
1792 }
1793
1794 const QString imageFileName = getArgument();
1795 QString imageText;
1796 bool hasAltTextArgument{false};
1797 if (isLeftBraceAhead()) {
1798 hasAltTextArgument = true;
1799 imageText = getArgument();
1800 } else if (cmd == CMD_IMAGE) {
1801 imageText = getRestOfLine();
1802 }
1803
1804 if (imageText.length() > 1) {
1805 if (imageText.front() == '"' && imageText.back() == '"') {
1806 imageText.removeFirst();
1807 imageText.removeLast();
1808 }
1809 }
1810
1811 if (!hasAltTextArgument && imageText.isEmpty() && Config::instance().reportMissingAltTextForImages())
1812 location().report(QStringLiteral("\\%1 %2 is without a textual description, "
1813 "QDoc will not generate an alt text for the image.")
1814 .arg(cmdName(cmd))
1815 .arg(imageFileName));
1816 appendAtom(Atom(imageAtom, imageFileName));
1817 appendAtom(Atom(Atom::ImageText, imageText));
1818}
1819
1820/*!
1821 \brief Processes the \\overload command in documentation comments.
1822
1823 This function registers metadata when the \\overload command is used in a
1824 documentation comment. It records the use of the command and stores any
1825 arguments to the command. Arguments are optional and can be passed with or
1826 without curly braces. This allows the user to use either of:
1827
1828 \badcode
1829 \overload someFunction
1830 \overload {someFunction}
1831 \endcode
1832 */
1833void DocParser::cmd_overload()
1834{
1835 const QString cmd{"overload"};
1836
1837 leavePara();
1838 m_private->m_metacommandsUsed.insert(cmd);
1839 QString overloadArgument = isBlankLine() ? getMetaCommandArgument(cmd) : getRestOfLine();
1840
1841 // Handle special case: \overload primary
1842 // Keep the "primary" flag for the code parser, but mark it specially
1843 // so generators know not to treat it as a target function name
1844 if (overloadArgument.trimmed() == "primary")
1845 overloadArgument = "__qdoc_primary_overload__"_L1;
1846
1847 m_private->m_metaCommandMap[cmd].append(ArgPair(overloadArgument, QString()));
1848}
1849
1850/*!
1851 \internal
1852 \brief Parses arguments to QDoc's see also command.
1853
1854 Parses space or comma separated arguments passed to the \\sa command.
1855 Multi-line input requires that the arguments are comma separated. Wrap
1856 arguments in curly braces for multi-word targets, and for scope resolution
1857 (for example, {QString::}{count()}).
1858
1859 This method updates the list of links for the See also section.
1860
1861 \sa {DocPrivate::}{addAlso()}, getArgument()
1862 */
1863void DocParser::parseAlso()
1864{
1865 auto line_comment = [this]() -> bool {
1866 skipSpacesOnLine();
1867 if (m_position + 2 > m_inputLength)
1868 return false;
1869 if (m_input[m_position].unicode() == '/') {
1870 if (m_input[m_position + 1].unicode() == '/') {
1871 if (m_input[m_position + 2].unicode() == '!') {
1872 return true;
1873 }
1874 }
1875 }
1876 return false;
1877 };
1878
1879 auto skip_everything_until_newline = [this]() -> void {
1880 while (m_position < m_inputLength && m_input[m_position] != '\n')
1881 ++m_position;
1882 };
1883
1884 leavePara();
1885 skipSpacesOnLine();
1886 while (m_position < m_inputLength && m_input[m_position] != '\n') {
1887 QString target;
1888 QString str;
1889 bool skipMe = false;
1890
1891 if (m_input[m_position] == '{') {
1892 target = getArgument();
1893 if (isLeftBraceAhead()) {
1894 str = getArgument();
1895
1896 // hack for C++ to support links like \l{QString::}{count()}
1897 if (target.endsWith("::"))
1898 target += str;
1899 } else {
1900 str = target;
1901 }
1902 } else {
1903 target = getArgument();
1904 str = cleanLink(target);
1905 if (target == QLatin1String("and") || target == QLatin1String("."))
1906 skipMe = true;
1907 }
1908
1909 if (!skipMe) {
1910 Text also;
1911 also << Atom(Atom::Link, target) << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1913 m_private->addAlso(also);
1914 }
1915
1916 skipSpacesOnLine();
1917
1918 if (line_comment())
1919 skip_everything_until_newline();
1920
1921 if (m_position < m_inputLength && m_input[m_position] == ',') {
1922 m_position++;
1923 if (line_comment())
1924 skip_everything_until_newline();
1925 skipSpacesOrOneEndl();
1926 } else if (m_position >= m_inputLength || m_input[m_position] != '\n') {
1927 location().warning(QStringLiteral("Missing comma in '\\%1'").arg(cmdName(CMD_SA)));
1928 }
1929 }
1930}
1931
1932void DocParser::appendAtom(const Atom& atom) {
1933 m_private->m_text << atom;
1934}
1935
1936void DocParser::appendAtom(const LinkAtom& atom) {
1937 m_private->m_text << atom;
1938}
1939
1940void DocParser::appendChar(QChar ch)
1941{
1943 appendAtom(Atom(Atom::String));
1944 Atom *atom = m_private->m_text.lastAtom();
1945 if (ch == QLatin1Char(' ')) {
1946 if (!atom->string().endsWith(QLatin1Char(' ')))
1947 atom->appendChar(QLatin1Char(' '));
1948 } else
1949 atom->appendChar(ch);
1950}
1951
1952void DocParser::appendWord(const QString &word)
1953{
1954 if (m_private->m_text.lastAtom()->type() != Atom::String) {
1955 appendAtom(Atom(Atom::String, word));
1956 } else
1957 m_private->m_text.lastAtom()->concatenateString(word);
1958}
1959
1960/*!
1961 Handles escaped backslash sequences (\\‍) that may be followed by an identifier.
1962
1963 When a double backslash is followed by alphanumeric characters (such as \\section1),
1964 the backslash and identifier are wrapped in notranslate formatting to preserve
1965 them verbatim in the output.
1966
1967 When a double backslash is not followed by an identifier, a literal backslash
1968 is appended.
1969
1970 \note Skips \\r in CRLF sequences to prevent spurious whitespace in the output.
1971*/
1972void DocParser::appendEscapedIdentifier()
1973{
1974 Q_ASSERT(m_position < m_inputLength);
1975 Q_ASSERT(m_input.at(m_position) == '\\');
1976
1977 ++m_position;
1978
1979 QString identifier;
1980 while (m_position < m_inputLength && m_input.at(m_position).isLetterOrNumber()) {
1981 identifier += m_input.at(m_position);
1982 ++m_position;
1983 }
1984
1985 enterPara();
1986 if (!identifier.isEmpty()) {
1988 appendAtom(Atom(Atom::String, '\\' + identifier));
1990
1991 if (m_position + 1 < m_inputLength && m_input.at(m_position) == '\r' && m_input.at(m_position + 1) == '\n')
1992 ++m_position;
1993 } else {
1994 appendChar(QLatin1Char('\\'));
1995 }
1996}
1997
1998void DocParser::appendToCode(const QString &markedCode)
1999{
2000 if (!isCode(m_lastAtom)) {
2001 appendAtom(Atom(Atom::Code));
2002 m_lastAtom = m_private->m_text.lastAtom();
2003 }
2004 m_lastAtom->concatenateString(markedCode);
2005}
2006
2007void DocParser::appendToCode(const QString &markedCode, Atom::AtomType defaultType)
2008{
2009 if (!isCode(m_lastAtom)) {
2010 appendAtom(Atom(defaultType, markedCode));
2011 m_lastAtom = m_private->m_text.lastAtom();
2012 } else {
2013 m_lastAtom->concatenateString(markedCode);
2014 }
2015}
2016
2017void DocParser::enterPara(Atom::AtomType leftType, Atom::AtomType rightType, const QString &string)
2018{
2019 if (m_paragraphState != OutsideParagraph)
2020 return;
2021
2025 leaveValueList();
2026 }
2027
2028 appendAtom(Atom(leftType, string));
2029 m_indexStartedParagraph = false;
2030 m_pendingParagraphLeftType = leftType;
2031 m_pendingParagraphRightType = rightType;
2032 m_pendingParagraphString = string;
2033 if (leftType == Atom::SectionHeadingLeft || leftType == Atom::TitleLeft) {
2034 m_paragraphState = InSingleLineParagraph;
2035 } else {
2036 m_paragraphState = InMultiLineParagraph;
2037 }
2038 skipSpacesOrOneEndl();
2039}
2040
2041void DocParser::leavePara()
2042{
2043 if (m_paragraphState == OutsideParagraph)
2044 return;
2045
2046 if (!m_pendingFormats.isEmpty()) {
2047 location().warning(QStringLiteral("Missing '}'"));
2048 m_pendingFormats.clear();
2049 }
2050
2051 if (m_private->m_text.lastAtom()->type() == m_pendingParagraphLeftType) {
2052 m_private->m_text.stripLastAtom();
2053 } else {
2055 && m_private->m_text.lastAtom()->string().endsWith(QLatin1Char(' '))) {
2057 }
2058 if (m_pendingParagraphRightType == Atom::TitleRight) {
2059 // Extract title
2062 } else {
2063 appendAtom(Atom(m_pendingParagraphRightType, m_pendingParagraphString));
2064 }
2065 }
2066 m_paragraphState = OutsideParagraph;
2067 m_indexStartedParagraph = false;
2068 m_pendingParagraphRightType = Atom::Nop;
2069 m_pendingParagraphString.clear();
2070}
2071
2072void DocParser::leaveValue()
2073{
2074 leavePara();
2075 if (m_openedLists.isEmpty()) {
2076 m_openedLists.push(OpenedList(OpenedList::Value));
2077 appendAtom(Atom(Atom::ListLeft, ATOM_LIST_VALUE));
2078 } else {
2079 if (m_private->m_text.lastAtom()->type() == Atom::Nop)
2080 m_private->m_text.stripLastAtom();
2081 appendAtom(Atom(Atom::ListItemRight, ATOM_LIST_VALUE));
2082 }
2083}
2084
2085void DocParser::leaveValueList()
2086{
2087 leavePara();
2088 if (!m_openedLists.isEmpty() && (m_openedLists.top().style() == OpenedList::Value)) {
2089 if (m_private->m_text.lastAtom()->type() == Atom::Nop)
2090 m_private->m_text.stripLastAtom();
2091 appendAtom(Atom(Atom::ListItemRight, ATOM_LIST_VALUE));
2092 appendAtom(Atom(Atom::ListRight, ATOM_LIST_VALUE));
2093 m_openedLists.pop();
2094 }
2095}
2096
2097void DocParser::leaveTableRow()
2098{
2099 if (m_inTableItem) {
2100 leavePara();
2101 appendAtom(Atom(Atom::TableItemRight));
2102 m_inTableItem = false;
2103 }
2104 if (m_inTableHeader) {
2105 appendAtom(Atom(Atom::TableHeaderRight));
2106 m_inTableHeader = false;
2107 }
2108 if (m_inTableRow) {
2109 appendAtom(Atom(Atom::TableRowRight));
2110 m_inTableRow = false;
2111 }
2112}
2113
2114void DocParser::quoteFromFile(const QString &filename)
2115{
2116 // KLUDGE: We dereference file_resolver as it is temporarily a pointer.
2117 // See the comment for file_resolver in the header files for more context.
2118 //
2119 // We spefically dereference it, instead of using the arrow
2120 // operator, to better represent that we do not consider this as
2121 // an actual pointer, as it should not be.
2122 //
2123 // Do note that we are considering it informally safe to
2124 // dereference the pointer, as we expect it to always hold a value
2125 // at this point, but actual enforcement of this appears nowhere
2126 // in the codebase.
2127 auto maybe_resolved_file{(*file_resolver).resolve(filename)};
2128 if (!maybe_resolved_file) {
2129 // TODO: [uncentralized-admonition][failed-resolve-file]
2130 // This warning is required in multiple places.
2131 // To ensure the consistency of the warning and avoid
2132 // duplicating code everywhere, provide a centralized effort
2133 // where the warning message can be generated (but not
2134 // issued).
2135 // The current format is based on what was used before, review
2136 // it when it is moved out.
2137 QString details = std::transform_reduce(
2138 (*file_resolver).get_search_directories().cbegin(),
2139 (*file_resolver).get_search_directories().cend(),
2140 u"Searched directories:"_s,
2141 std::plus(),
2142 [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
2143 );
2144
2145 location().warning(u"Cannot find file to quote from: %1"_s.arg(filename), details);
2146
2147 // REMARK: The following is duplicated from
2148 // Doc::quoteFromFile. If, for some reason (such as a file
2149 // that is inaccessible), the quoting fails but, previously,
2150 // the logic duplicated here was still run.
2151 // This is not true anymore as quoteFromFile does require a
2152 // resolved file to be run now.
2153 // It is not entirely clear if this is required for the
2154 // semantics of DocParser to be preserved, but for the sake of
2155 // avoiding premature breakages this was retained.
2156 // Do note that this should be considered temporary as the
2157 // quoter state, if any will be preserved, should not be
2158 // managed in such a spread and unlocal way.
2159 m_quoter.reset();
2160
2161 CodeMarker *marker = CodeMarker::markerForFileName(QString{});
2162 m_quoter.quoteFromFile(filename, QString{}, marker->markedUpCode(QString{}, nullptr, location()));
2163 } else Doc::quoteFromFile(location(), m_quoter, *maybe_resolved_file);
2164}
2165
2166/*!
2167 Expands a macro in-place in input.
2168
2169 Expects the current \e pos in the input to point to a backslash, and the macro to have a
2170 default definition. Format-specific macros are not expanded.
2171
2172 Behavior depends on \a options:
2173
2174 \value ArgumentParsingOptions::Default
2175 Default macro expansion; the string following the backslash
2176 must be a macro with a default definition.
2177 \value ArgumentParsingOptions::Verbatim
2178 The string following the backslash is rendered verbatim;
2179 No macro expansion is performed.
2180 \value ArgumentParsingOptions::MacroArguments
2181 Used for parsing argument(s) for a macro. Allows expanding
2182 macros, and also preserves a subset of commands (formatting
2183 commands) within the macro argument.
2184
2185 \note In addition to macros, a valid use for a backslash in an argument include
2186 escaping non-alnum characters, and splitting a single argument across multiple
2187 lines by escaping newlines. Escaping is also handled here.
2188
2189 Returns \c true on successful macro expansion.
2190 */
2191bool DocParser::expandMacro(ArgumentParsingOptions options)
2192{
2193 Q_ASSERT(m_input[m_position].unicode() == '\\');
2194
2195 if (options == ArgumentParsingOptions::Verbatim)
2196 return false;
2197
2198 QString cmdStr;
2199 qsizetype backslashPos = m_position++;
2200 while (m_position < m_input.size() && m_input[m_position].isLetterOrNumber())
2201 cmdStr += m_input[m_position++];
2202
2203 m_endPosition = m_position;
2204 if (!cmdStr.isEmpty()) {
2205 if (s_utilities.macroHash.contains(cmdStr)) {
2206 const Macro &macro = s_utilities.macroHash.value(cmdStr);
2207 if (!macro.m_defaultDef.isEmpty()) {
2208 QString expanded = expandMacroToString(cmdStr, macro);
2209 m_input.replace(backslashPos, m_position - backslashPos, expanded);
2210 m_inputLength = m_input.size();
2211 m_position = backslashPos;
2212 return true;
2213 } else {
2214 location().warning("Macro '%1' does not have a default definition"_L1.arg(cmdStr));
2215 }
2216 } else {
2217 int cmd = s_utilities.cmdHash.value(cmdStr, NOT_A_CMD);
2218 m_position = backslashPos;
2219 if (options != ArgumentParsingOptions::MacroArguments
2220 || cmd == NOT_A_CMD || !cmds[cmd].is_formatting_command) {
2221 location().warning("Unknown macro '%1'"_L1.arg(cmdStr));
2222 ++m_position;
2223 }
2224 }
2225 } else if (m_input[m_position].isSpace()) {
2226 skipAllSpaces();
2227 } else if (m_input[m_position].unicode() == '\\') {
2228 // allow escaping a backslash
2229 m_input.remove(m_position--, 1);
2230 --m_inputLength;
2231 }
2232 return false;
2233}
2234
2235void DocParser::expandMacro(const QString &def, const QStringList &args)
2236{
2237 if (args.isEmpty()) {
2238 appendAtom(Atom(Atom::RawString, def));
2239 } else {
2240 QString rawString;
2241
2242 for (int j = 0; j < def.size(); ++j) {
2243 if (int paramNo = def[j].unicode(); paramNo >= 1 && paramNo <= args.length()) {
2244 if (!rawString.isEmpty()) {
2245 appendAtom(Atom(Atom::RawString, rawString));
2246 rawString.clear();
2247 }
2248 appendAtom(Atom(Atom::String, args[paramNo - 1]));
2249 } else {
2250 rawString += def[j];
2251 }
2252 }
2253 if (!rawString.isEmpty())
2254 appendAtom(Atom(Atom::RawString, rawString));
2255 }
2256}
2257
2258QString DocParser::expandMacroToString(const QString &name, const Macro &macro)
2259{
2260 const QString &def{macro.m_defaultDef};
2261 QString rawString;
2262
2263 if (macro.numParams == 0) {
2264 rawString = macro.m_defaultDef;
2265 } else {
2266 QStringList args{getMacroArguments(name, macro)};
2267
2268 for (int j = 0; j < def.size(); ++j) {
2269 int paramNo = def[j].unicode();
2270 rawString += (paramNo >= 1 && paramNo <= args.length()) ? args[paramNo - 1] : def[j];
2271 }
2272 }
2273 QString matchExpr{macro.m_otherDefs.value("match")};
2274 if (matchExpr.isEmpty())
2275 return rawString;
2276
2277 QString result;
2278 QRegularExpression re(matchExpr);
2279 int capStart = (re.captureCount() > 0) ? 1 : 0;
2280 qsizetype i = 0;
2281 QRegularExpressionMatch match;
2282 while ((match = re.match(rawString, i)).hasMatch()) {
2283 for (int c = capStart; c <= re.captureCount(); ++c)
2284 result += match.captured(c);
2285 i = match.capturedEnd();
2286 }
2287
2288 return result;
2289}
2290
2291Doc::Sections DocParser::getSectioningUnit()
2292{
2293 QString name = getOptionalArgument();
2294
2295 if (name == "section1") {
2296 return Doc::Section1;
2297 } else if (name == "section2") {
2298 return Doc::Section2;
2299 } else if (name == "section3") {
2300 return Doc::Section3;
2301 } else if (name == "section4") {
2302 return Doc::Section4;
2303 } else if (name.isEmpty()) {
2304 return Doc::NoSection;
2305 } else {
2306 location().warning(QStringLiteral("Invalid section '%1'").arg(name));
2307 return Doc::NoSection;
2308 }
2309}
2310
2311/*!
2312 Gets an argument that is enclosed in braces and returns it
2313 without the enclosing braces. On entry, the current character
2314 is the left brace. On exit, the current character is the one
2315 that comes after the right brace.
2316
2317 If \a options is ArgumentParsingOptions::Verbatim, no macro
2318 expansion is performed, nor is the returned string stripped
2319 of extra whitespace.
2320 */
2321QString DocParser::getBracedArgument(ArgumentParsingOptions options)
2322{
2323 QString arg;
2324 int delimDepth = 0;
2325 if (m_position < m_input.size() && m_input[m_position] == '{') {
2326 ++m_position;
2327 while (m_position < m_input.size() && delimDepth >= 0) {
2328 switch (m_input[m_position].unicode()) {
2329 case '{':
2330 ++delimDepth;
2331 arg += QLatin1Char('{');
2332 ++m_position;
2333 break;
2334 case '}':
2335 --delimDepth;
2336 if (delimDepth >= 0)
2337 arg += QLatin1Char('}');
2338 ++m_position;
2339 break;
2340 case '\\':
2341 if (!expandMacro(options))
2342 arg += m_input[m_position++];
2343 break;
2344 default:
2345 if (m_input[m_position].isSpace() && options != ArgumentParsingOptions::Verbatim)
2346 arg += QChar(' ');
2347 else
2348 arg += m_input[m_position];
2349 ++m_position;
2350 }
2351 }
2352 if (delimDepth > 0)
2353 location().warning(QStringLiteral("Missing '}'"));
2354 }
2355 m_endPosition = m_position;
2356 return arg;
2357}
2358
2359/*!
2360 Parses and returns an argument for a command, using
2361 specific parsing \a options.
2362
2363 Typically, an argument ends at the next white-space. However,
2364 braces can be used to group words:
2365
2366 {a few words}
2367
2368 Also, opening and closing parentheses have to match. Thus,
2369
2370 printf("%d\n", x)
2371
2372 is an argument too, although it contains spaces. Finally,
2373 trailing punctuation is not included in an argument, nor is 's.
2374*/
2375QString DocParser::getArgument(ArgumentParsingOptions options)
2376{
2377 skipSpacesOrOneEndl();
2378
2379 int delimDepth = 0;
2380 qsizetype startPos = m_position;
2381 QString arg = getBracedArgument(options);
2382 if (arg.isEmpty()) {
2383 while ((m_position < m_input.size())
2384 && ((delimDepth > 0) || ((delimDepth == 0) && !m_input[m_position].isSpace()))) {
2385 switch (m_input[m_position].unicode()) {
2386 case '(':
2387 case '[':
2388 case '{':
2389 ++delimDepth;
2390 arg += m_input[m_position];
2391 ++m_position;
2392 break;
2393 case ')':
2394 case ']':
2395 case '}':
2396 --delimDepth;
2397 if (m_position == startPos || delimDepth >= 0) {
2398 arg += m_input[m_position];
2399 ++m_position;
2400 }
2401 break;
2402 case '\\':
2403 if (!expandMacro(options))
2404 arg += m_input[m_position++];
2405 break;
2406 default:
2407 arg += m_input[m_position];
2408 ++m_position;
2409 }
2410 }
2411 m_endPosition = m_position;
2412 if ((arg.size() > 1) && (QString(".,:;!?").indexOf(m_input[m_position - 1]) != -1)
2413 && !arg.endsWith("...")) {
2414 arg.truncate(arg.size() - 1);
2415 --m_position;
2416 }
2417 if (arg.size() > 2 && m_input.mid(m_position - 2, 2) == "'s") {
2418 arg.truncate(arg.size() - 2);
2419 m_position -= 2;
2420 }
2421 }
2422 return arg.simplified();
2423}
2424
2425/*!
2426 Gets an argument that is enclosed in brackets and returns it
2427 without the enclosing brackets. On entry, the current character
2428 is the left bracket. On exit, the current character is the one
2429 that comes after the right bracket.
2430 */
2431QString DocParser::getBracketedArgument()
2432{
2433 QString arg;
2434 int delimDepth = 0;
2435 skipSpacesOrOneEndl();
2436 if (m_position < m_input.size() && m_input[m_position] == '[') {
2437 ++m_position;
2438 while (m_position < m_input.size() && delimDepth >= 0) {
2439 switch (m_input[m_position].unicode()) {
2440 case '[':
2441 ++delimDepth;
2442 arg += QLatin1Char('[');
2443 ++m_position;
2444 break;
2445 case ']':
2446 --delimDepth;
2447 if (delimDepth >= 0)
2448 arg += QLatin1Char(']');
2449 ++m_position;
2450 break;
2451 case '\\':
2452 arg += m_input[m_position];
2453 ++m_position;
2454 break;
2455 default:
2456 arg += m_input[m_position];
2457 ++m_position;
2458 }
2459 }
2460 if (delimDepth > 0)
2461 location().warning(QStringLiteral("Missing ']'"));
2462 }
2463 return arg;
2464}
2465
2466
2467/*!
2468 Returns the list of arguments passed to a \a macro with name \a name.
2469
2470 If a macro takes more than a single argument, they are expected to be
2471 wrapped in braces.
2472*/
2473QStringList DocParser::getMacroArguments(const QString &name, const Macro &macro)
2474{
2475 QStringList args;
2476 for (int i = 0; i < macro.numParams; ++i) {
2477 if (macro.numParams == 1 || isLeftBraceAhead()) {
2478 args << getArgument(ArgumentParsingOptions::MacroArguments);
2479 } else {
2480 location().warning(QStringLiteral("Macro '\\%1' invoked with too few"
2481 " arguments (expected %2, got %3)")
2482 .arg(name)
2483 .arg(macro.numParams)
2484 .arg(i));
2485 break;
2486 }
2487 }
2488 return args;
2489}
2490
2491/*!
2492 Returns the next token as an optional argument unless the token is
2493 another command. The next token can be preceded by spaces and an optional
2494 newline.
2495*/
2496QString DocParser::getOptionalArgument()
2497{
2498 skipSpacesOrOneEndl();
2499 if (m_position + 1 < m_input.size() && m_input[m_position] == '\\'
2500 && m_input[m_position + 1].isLetterOrNumber()) {
2501 return QString();
2502 } else {
2503 return getArgument();
2504 }
2505}
2506
2507/*!
2508 \brief Create a string that may optionally span multiple lines as one line.
2509
2510 Process a block of text that may span multiple lines using trailing
2511 backslashes (`\`) as line continuation character. Trailing backslashes and
2512 any newline character that follow them are removed.
2513
2514 Returns a string as if it was one continuous line of text. If trailing
2515 backslashes are removed, the method returns a "simplified" QString, which
2516 means any sequence of internal whitespace is replaced with a single space.
2517
2518 Whitespace at the start and end is always removed from the returned string.
2519
2520 \sa QString::simplified(), QString::trimmed().
2521 */
2522QString DocParser::getRestOfLine()
2523{
2524 auto lineHasTrailingBackslash = [this](bool trailingBackslash) -> bool {
2525 while (m_position < m_inputLength && m_input[m_position] != '\n') {
2526 if (m_input[m_position] == '\\' && !trailingBackslash) {
2527 trailingBackslash = true;
2528 ++m_position;
2529 skipSpacesOnLine();
2530 } else {
2531 trailingBackslash = false;
2532 ++m_position;
2533 }
2534 }
2535 return trailingBackslash;
2536 };
2537
2538 QString rest_of_line;
2539 skipSpacesOnLine();
2540 bool trailing_backslash{ false };
2541 bool return_simplified_string{ false };
2542
2543 for (qsizetype start_position = m_position; m_position < m_inputLength; ++m_position) {
2544 trailing_backslash = lineHasTrailingBackslash(trailing_backslash);
2545
2546 if (!rest_of_line.isEmpty())
2547 rest_of_line += QLatin1Char(' ');
2548 rest_of_line += m_input.sliced(start_position, m_position - start_position);
2549
2550 if (trailing_backslash) {
2551 rest_of_line.truncate(rest_of_line.lastIndexOf('\\'));
2552 return_simplified_string = true;
2553 }
2554
2555 if (m_position < m_inputLength)
2556 ++m_position;
2557
2558 if (!trailing_backslash)
2559 break;
2560 start_position = m_position;
2561 }
2562
2563 if (return_simplified_string)
2564 return rest_of_line.simplified();
2565
2566 return rest_of_line.trimmed();
2567}
2568
2569/*!
2570 The metacommand argument is normally the remaining text to
2571 the right of the metacommand itself. The extra blanks are
2572 stripped and the argument string is returned.
2573 */
2574QString DocParser::getMetaCommandArgument(const QString &cmdStr)
2575{
2576 skipSpacesOnLine();
2577
2578 qsizetype begin = m_position;
2579 int parenDepth = 0;
2580
2581 while (m_position < m_input.size() && (m_input[m_position] != '\n' || parenDepth > 0)) {
2582 if (m_input.at(m_position) == '(')
2583 ++parenDepth;
2584 else if (m_input.at(m_position) == ')')
2585 --parenDepth;
2586 else if (m_input.at(m_position) == '\\' && expandMacro(ArgumentParsingOptions::Default))
2587 continue;
2588 ++m_position;
2589 }
2590 if (m_position == m_input.size() && parenDepth > 0) {
2591 m_position = begin;
2592 location().warning(QStringLiteral("Unbalanced parentheses in '%1'").arg(cmdStr));
2593 }
2594
2595 QString t = m_input.mid(begin, m_position - begin).simplified();
2596 skipSpacesOnLine();
2597 return t;
2598}
2599
2600QString DocParser::getUntilEnd(int cmd)
2601{
2602 int endCmd = endCmdFor(cmd);
2603 QRegularExpression rx("\\\\" + cmdName(endCmd) + "\\b");
2604 QString t;
2605 auto match = rx.match(m_input, m_position);
2606
2607 if (!match.hasMatch()) {
2608 location().warning(QStringLiteral("Missing '\\%1'").arg(cmdName(endCmd)));
2609 m_position = m_input.size();
2610 } else {
2611 qsizetype end = match.capturedStart();
2612 t = m_input.mid(m_position, end - m_position);
2613 m_position = match.capturedEnd();
2614 }
2615 return t;
2616}
2617
2618void DocParser::expandArgumentsInString(QString &str, const QStringList &args)
2619{
2620 if (args.isEmpty())
2621 return;
2622
2623 qsizetype paramNo;
2624 qsizetype j = 0;
2625 while (j < str.size()) {
2626 if (str[j] == '\\' && j < str.size() - 1 && (paramNo = str[j + 1].digitValue()) >= 1
2627 && paramNo <= args.size()) {
2628 const QString &r = args[paramNo - 1];
2629 str.replace(j, 2, r);
2630 j += qMin(1, r.size());
2631 } else {
2632 ++j;
2633 }
2634 }
2635}
2636
2637/*!
2638 Returns the marked-up code following the code-quoting command \a cmd, expanding
2639 any arguments passed in \a argStr.
2640
2641 Uses the \a marker to mark up the code. If it's \c nullptr, resolve the marker
2642 based on the topic and the quoted code itself.
2643*/
2644QString DocParser::getCode(int cmd, CodeMarker *marker, const QString &argStr)
2645{
2646 QString code = untabifyEtc(getUntilEnd(cmd));
2647 expandArgumentsInString(code, argStr.split(" ", Qt::SkipEmptyParts));
2648
2649 int indent = indentLevel(code);
2650 code = dedent(indent, code);
2651
2652 // If we're in a QML topic, check if the QML marker recognizes the code
2653 if (!marker && !m_private->m_topics.isEmpty()
2654 && m_private->m_topics[0].m_topic.startsWith("qml")) {
2655 auto qmlMarker = CodeMarker::markerForLanguage("QML");
2656 marker = (qmlMarker && qmlMarker->recognizeCode(code)) ? qmlMarker : nullptr;
2657 }
2658 if (marker == nullptr)
2659 marker = CodeMarker::markerForCode(code);
2660 return marker->markedUpCode(code, nullptr, location());
2661}
2662
2663bool DocParser::isBlankLine()
2664{
2665 qsizetype i = m_position;
2666
2667 while (i < m_inputLength && m_input[i].isSpace()) {
2668 if (m_input[i] == '\n')
2669 return true;
2670 ++i;
2671 }
2672 return false;
2673}
2674
2675bool DocParser::isLeftBraceAhead()
2676{
2677 int numEndl = 0;
2678 qsizetype i = m_position;
2679
2680 while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) {
2681 // ### bug with '\\'
2682 if (m_input[i] == '\n')
2683 numEndl++;
2684 ++i;
2685 }
2686 return numEndl < 2 && i < m_inputLength && m_input[i] == '{';
2687}
2688
2689bool DocParser::isLeftBracketAhead(int maxNewlines)
2690{
2691 int numEndl = 0;
2692 qsizetype i = m_position;
2693
2694 while (i < m_inputLength && m_input[i].isSpace() && numEndl < 2) {
2695 // ### bug with '\\'
2696 if (m_input[i] == '\n')
2697 numEndl++;
2698 ++i;
2699 }
2700 return numEndl <= maxNewlines && i < m_inputLength && m_input[i] == '[';
2701}
2702
2703/*!
2704 Skips to the next non-space character or EOL.
2705 */
2706void DocParser::skipSpacesOnLine()
2707{
2708 while ((m_position < m_input.size()) && m_input[m_position].isSpace()
2709 && (m_input[m_position].unicode() != '\n'))
2710 ++m_position;
2711}
2712
2713/*!
2714 Skips spaces and one EOL.
2715 */
2716void DocParser::skipSpacesOrOneEndl()
2717{
2718 qsizetype firstEndl = -1;
2719 while (m_position < m_input.size() && m_input[m_position].isSpace()) {
2720 QChar ch = m_input[m_position];
2721 if (ch == '\n') {
2722 if (firstEndl == -1) {
2723 firstEndl = m_position;
2724 } else {
2725 m_position = firstEndl;
2726 break;
2727 }
2728 }
2729 ++m_position;
2730 }
2731}
2732
2733void DocParser::skipAllSpaces()
2734{
2735 while (m_position < m_inputLength && m_input[m_position].isSpace())
2736 ++m_position;
2737}
2738
2739void DocParser::skipToNextPreprocessorCommand()
2740{
2741 QRegularExpression rx("\\\\‍(?:" + cmdName(CMD_IF) + QLatin1Char('|') + cmdName(CMD_ELSE)
2742 + QLatin1Char('|') + cmdName(CMD_ENDIF) + ")\\b");
2743 auto match = rx.match(m_input, m_position + 1); // ### + 1 necessary?
2744
2745 if (!match.hasMatch())
2746 m_position = m_input.size();
2747 else
2748 m_position = match.capturedStart();
2749}
2750
2752{
2753 switch (cmd) {
2754 case CMD_BADCODE:
2755 return CMD_ENDCODE;
2756 case CMD_CODE:
2757 return CMD_ENDCODE;
2758 case CMD_COMPARESWITH:
2759 return CMD_ENDCOMPARESWITH;
2760 case CMD_DETAILS:
2761 return CMD_ENDDETAILS;
2762 case CMD_DIV:
2763 return CMD_ENDDIV;
2764 case CMD_QML:
2765 return CMD_ENDQML;
2766 case CMD_FOOTNOTE:
2767 return CMD_ENDFOOTNOTE;
2768 case CMD_LEGALESE:
2769 return CMD_ENDLEGALESE;
2770 case CMD_LINK:
2771 return CMD_ENDLINK;
2772 case CMD_LIST:
2773 return CMD_ENDLIST;
2774 case CMD_OMIT:
2775 return CMD_ENDOMIT;
2776 case CMD_QUOTATION:
2777 return CMD_ENDQUOTATION;
2778 case CMD_RAW:
2779 return CMD_ENDRAW;
2780 case CMD_SECTION1:
2781 return CMD_ENDSECTION1;
2782 case CMD_SECTION2:
2783 return CMD_ENDSECTION2;
2784 case CMD_SECTION3:
2785 return CMD_ENDSECTION3;
2786 case CMD_SECTION4:
2787 return CMD_ENDSECTION4;
2788 case CMD_SIDEBAR:
2789 return CMD_ENDSIDEBAR;
2790 case CMD_TABLE:
2791 return CMD_ENDTABLE;
2792 case CMD_TOC:
2793 return CMD_ENDTOC;
2794 default:
2795 return cmd;
2796 }
2797}
2798
2799QString DocParser::cmdName(int cmd)
2800{
2801 return cmds[cmd].name;
2802}
2803
2804QString DocParser::endCmdName(int cmd)
2805{
2806 return cmdName(endCmdFor(cmd));
2807}
2808
2809QString DocParser::untabifyEtc(const QString &str)
2810{
2811 QString result;
2812 result.reserve(str.size());
2813 int column = 0;
2814
2815 for (const auto &character : str) {
2816 if (character == QLatin1Char('\r'))
2817 continue;
2818 if (character == QLatin1Char('\t')) {
2819 result += &" "[column % s_tabSize];
2820 column = ((column / s_tabSize) + 1) * s_tabSize;
2821 continue;
2822 }
2823 if (character == QLatin1Char('\n')) {
2824 while (result.endsWith(QLatin1Char(' ')))
2825 result.chop(1);
2826 result += character;
2827 column = 0;
2828 continue;
2829 }
2830 result += character;
2831 ++column;
2832 }
2833
2834 while (result.endsWith("\n\n"))
2835 result.truncate(result.size() - 1);
2836 while (result.startsWith(QLatin1Char('\n')))
2837 result = result.mid(1);
2838
2839 return result;
2840}
2841
2842int DocParser::indentLevel(const QString &str)
2843{
2844 int minIndent = INT_MAX;
2845 int column = 0;
2846
2847 for (const auto &character : str) {
2848 if (character == '\n') {
2849 column = 0;
2850 } else {
2851 if (character != ' ' && column < minIndent)
2852 minIndent = column;
2853 ++column;
2854 }
2855 }
2856 return minIndent;
2857}
2858
2859QString DocParser::dedent(int level, const QString &str)
2860{
2861 if (level == 0)
2862 return str;
2863
2864 QString result;
2865 int column = 0;
2866
2867 for (const auto &character : str) {
2868 if (character == QLatin1Char('\n')) {
2869 result += '\n';
2870 column = 0;
2871 } else {
2872 if (column >= level)
2873 result += character;
2874 ++column;
2875 }
2876 }
2877 return result;
2878}
2879
2880/*!
2881 Returns \c true if \a atom represents a code snippet.
2882 */
2883bool DocParser::isCode(const Atom *atom)
2884{
2885 Atom::AtomType type = atom->type();
2886 return (type == Atom::Code || type == Atom::Qml);
2887}
2888
2889/*!
2890 Returns \c true if \a atom represents quoting information.
2891 */
2892bool DocParser::isQuote(const Atom *atom)
2893{
2894 Atom::AtomType type = atom->type();
2895 return (type == Atom::CodeQuoteArgument || type == Atom::CodeQuoteCommand
2896 || type == Atom::SnippetCommand || type == Atom::SnippetIdentifier
2897 || type == Atom::SnippetLocation);
2898}
2899
2900/*!
2901 \internal
2902 Processes the arguments passed to the \\compareswith block command.
2903 The arguments are stored as text within the first atom of the block
2904 (Atom::ComparesLeft).
2905
2906 Extracts the comparison category and the list of types, and stores
2907 the information into a map accessed via \a priv.
2908*/
2909static void processComparesWithCommand(DocPrivate *priv, const Location &location)
2910{
2911 static auto take_while = [](QStringView input, auto predicate) {
2912 QStringView::size_type end{0};
2913
2914 while (end < input.size() && std::invoke(predicate, input[end]))
2915 ++end;
2916
2917 return std::make_tuple(input.sliced(0, end), input.sliced(end));
2918 };
2919
2920 static auto peek = [](QStringView input, QChar c) {
2921 return !input.empty() && input.first() == c;
2922 };
2923
2924 static auto skip_one = [](QStringView input) {
2925 if (input.empty()) return std::make_tuple(QStringView{}, input);
2926 else return std::make_tuple(input.sliced(0, 1), input.sliced(1));
2927 };
2928
2929 static auto enclosed = [](QStringView input, QChar open, QChar close) {
2930 if (!peek(input, open)) return std::make_tuple(QStringView{}, input);
2931
2932 auto [opened, without_open] = skip_one(input);
2933 auto [parsed, remaining] = take_while(without_open, [close](QChar c){ return c != close; });
2934
2935 if (remaining.empty()) return std::make_tuple(QStringView{}, input);
2936
2937 auto [closed, without_close] = skip_one(remaining);
2938
2939 return std::make_tuple(parsed.trimmed(), without_close);
2940 };
2941
2942 static auto one_of = [](auto first, auto second) {
2943 return [first, second](QStringView input) {
2944 auto [parsed, remaining] = std::invoke(first, input);
2945
2946 if (parsed.empty()) return std::invoke(second, input);
2947 else return std::make_tuple(parsed, remaining);
2948 };
2949 };
2950
2951 static auto collect = [](QStringView input, auto parser) {
2952 QStringList collected{};
2953
2954 while (true) {
2955 auto [parsed, remaining] = std::invoke(parser, input);
2956
2957 if (parsed.empty()) break;
2958 collected.append(parsed.toString());
2959
2960 input = remaining;
2961 };
2962
2963 return collected;
2964 };
2965
2966 static auto spaces = [](QStringView input) {
2967 return take_while(input, [](QChar c){ return c.isSpace(); });
2968 };
2969
2970 static auto word = [](QStringView input) {
2971 return take_while(input, [](QChar c){ return !c.isSpace(); });
2972 };
2973
2974 static auto parse_argument = [](QStringView input) {
2975 auto [_, without_spaces] = spaces(input);
2976
2977 return one_of(
2978 [](QStringView input){ return enclosed(input, '{', '}'); },
2979 word
2980 )(without_spaces);
2981 };
2982
2983 const QString cmd{DocParser::cmdName(CMD_COMPARESWITH)};
2985
2986 auto *atom = text.firstAtom();
2987 QStringList segments = collect(atom->string(), parse_argument);
2988
2989 QString categoryString;
2990 if (!segments.isEmpty())
2991 categoryString = segments.takeFirst();
2992 auto category = comparisonCategoryFromString(categoryString.toStdString());
2993
2994 if (category == ComparisonCategory::None) {
2995 location.warning(u"Invalid argument to \\%1 command: `%2`"_s.arg(cmd, categoryString),
2996 u"Valid arguments are `strong`, `weak`, `partial`, or `equality`."_s);
2997 return;
2998 }
2999
3000 if (segments.isEmpty()) {
3001 location.warning(u"Missing argument to \\%1 command."_s.arg(cmd),
3002 u"Provide at least one type name, or a list of types separated by spaces."_s);
3003 return;
3004 }
3005
3006 // Store cleaned-up type names back into the atom
3007 segments.removeDuplicates();
3008 atom->setString(segments.join(QLatin1Char(';')));
3009
3010 // Add an entry to meta-command map for error handling in CppCodeParser
3011 priv->m_metaCommandMap[cmd].append(ArgPair(categoryString, atom->string()));
3012 priv->m_metacommandsUsed.insert(cmd);
3013
3015 const auto end{priv->extra->m_comparesWithMap.cend()};
3016 priv->extra->m_comparesWithMap.insert(end, category, text);
3017}
3018
3019QT_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
The Config class contains the configuration variables for controlling how qdoc produces documentation...
Definition config.h:85
static QStringList s_ignoreWords
Definition docparser.h:43
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:44
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:31
static void quoteFromFile(const Location &location, Quoter &quoter, ResolvedFile resolved_file)
Definition doc.cpp:462
Sections
Definition doc.h:34
@ NoSection
Definition doc.h:35
@ Section3
Definition doc.h:38
@ Section1
Definition doc.h:36
@ Section2
Definition doc.h:37
@ Section4
Definition doc.h:39
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:440
#define CONFIG_IGNOREWORDS
Definition config.h:396
#define CONFIG_QUOTINGINFORMATION
Definition config.h:428
std::pair< QString, QString > ArgPair
Definition doc.h:26
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