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