1 | // Copyright (C) 2019 Thibaut Cuvelier |
2 | // Copyright (C) 2021 The Qt Company Ltd. |
3 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
4 | |
5 | #include "xmlgenerator.h" |
6 | |
7 | #include "enumnode.h" |
8 | #include "examplenode.h" |
9 | #include "functionnode.h" |
10 | #include "qdocdatabase.h" |
11 | #include "typedefnode.h" |
12 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | const QRegularExpression XmlGenerator::m_funcLeftParen(QStringLiteral("^\\S+(\\(.*\\))$" )); |
16 | |
17 | XmlGenerator::XmlGenerator(FileResolver& file_resolver) : Generator(file_resolver) {} |
18 | |
19 | /*! |
20 | Do not display \brief for QML types, document and collection nodes |
21 | */ |
22 | bool XmlGenerator::hasBrief(const Node *node) |
23 | { |
24 | return !(node->isQmlType() || node->isPageNode() || node->isCollectionNode()); |
25 | } |
26 | |
27 | /*! |
28 | Determines whether the list atom should be shown with three columns |
29 | (constant-value-description). |
30 | */ |
31 | bool XmlGenerator::isThreeColumnEnumValueTable(const Atom *atom) |
32 | { |
33 | while (atom && !(atom->type() == Atom::ListRight && atom->string() == ATOM_LIST_VALUE)) { |
34 | if (atom->type() == Atom::ListItemLeft && !matchAhead(atom, expectedAtomType: Atom::ListItemRight)) |
35 | return true; |
36 | atom = atom->next(); |
37 | } |
38 | return false; |
39 | } |
40 | |
41 | /*! |
42 | Determines whether the list atom should be shown with just one column (value). |
43 | */ |
44 | bool XmlGenerator::isOneColumnValueTable(const Atom *atom) |
45 | { |
46 | if (atom->type() != Atom::ListLeft || atom->string() != ATOM_LIST_VALUE) |
47 | return false; |
48 | |
49 | while (atom && atom->type() != Atom::ListTagRight) |
50 | atom = atom->next(); |
51 | |
52 | if (atom) { |
53 | if (!matchAhead(atom, expectedAtomType: Atom::ListItemLeft)) |
54 | return false; |
55 | if (!atom->next()) |
56 | return false; |
57 | return matchAhead(atom: atom->next(), expectedAtomType: Atom::ListItemRight); |
58 | } |
59 | return false; |
60 | } |
61 | |
62 | /*! |
63 | Header offset depending on the type of the node |
64 | */ |
65 | int XmlGenerator::hOffset(const Node *node) |
66 | { |
67 | switch (node->nodeType()) { |
68 | case Node::Namespace: |
69 | case Node::Class: |
70 | case Node::Struct: |
71 | case Node::Union: |
72 | case Node::Module: |
73 | return 2; |
74 | case Node::QmlModule: |
75 | case Node::QmlValueType: |
76 | case Node::QmlType: |
77 | case Node::Page: |
78 | case Node::Group: |
79 | return 1; |
80 | case Node::Enum: |
81 | case Node::TypeAlias: |
82 | case Node::Typedef: |
83 | case Node::Function: |
84 | case Node::Property: |
85 | default: |
86 | return 3; |
87 | } |
88 | } |
89 | |
90 | /*! |
91 | Rewrites the brief of this node depending on its first word. |
92 | Only for properties and variables (does nothing otherwise). |
93 | */ |
94 | void XmlGenerator::rewritePropertyBrief(const Atom *atom, const Node *relative) |
95 | { |
96 | if (relative->nodeType() == Node::Property || relative->nodeType() == Node::Variable) { |
97 | atom = atom->next(); |
98 | if (atom && atom->type() == Atom::String) { |
99 | QString firstWord = |
100 | atom->string().toLower().section(asep: ' ', astart: 0, aend: 0, aflags: QString::SectionSkipEmpty); |
101 | if (firstWord == QLatin1String("the" ) || firstWord == QLatin1String("a" ) |
102 | || firstWord == QLatin1String("an" ) || firstWord == QLatin1String("whether" ) |
103 | || firstWord == QLatin1String("which" )) { |
104 | QString str = QLatin1String("This " ) |
105 | + QLatin1String(relative->nodeType() == Node::Property ? "property" |
106 | : "variable" ) |
107 | + QLatin1String(" holds " ) + atom->string().left(n: 1).toLower() |
108 | + atom->string().mid(position: 1); |
109 | const_cast<Atom *>(atom)->setString(str); |
110 | } |
111 | } |
112 | } |
113 | } |
114 | |
115 | /*! |
116 | Returns the type of this atom as an enumeration. |
117 | */ |
118 | Node::NodeType XmlGenerator::typeFromString(const Atom *atom) |
119 | { |
120 | const auto &name = atom->string(); |
121 | if (name.startsWith(s: QLatin1String("qml" ))) |
122 | return Node::QmlModule; |
123 | else if (name.startsWith(s: QLatin1String("groups" ))) |
124 | return Node::Group; |
125 | else |
126 | return Node::Module; |
127 | } |
128 | |
129 | /*! |
130 | For images shown in examples, set the image file to the one it |
131 | will have once the documentation is generated. |
132 | */ |
133 | void XmlGenerator::setImageFileName(const Node *relative, const QString &fileName) |
134 | { |
135 | if (relative->isExample()) { |
136 | const auto cen = static_cast<const ExampleNode *>(relative); |
137 | if (cen->imageFileName().isEmpty()) { |
138 | auto *en = const_cast<ExampleNode *>(cen); |
139 | en->setImageFileName(fileName); |
140 | } |
141 | } |
142 | } |
143 | |
144 | /*! |
145 | Handles the differences in lists between list tags and since tags, and |
146 | returns the content of the list entry \a atom (first member of the pair). |
147 | It also returns the number of items to skip ahead (second member of the pair). |
148 | */ |
149 | std::pair<QString, int> XmlGenerator::getAtomListValue(const Atom *atom) |
150 | { |
151 | const Atom *lookAhead = atom->next(); |
152 | if (!lookAhead) |
153 | return std::pair<QString, int>(QString(), 1); |
154 | |
155 | QString t = lookAhead->string(); |
156 | lookAhead = lookAhead->next(); |
157 | if (!lookAhead || lookAhead->type() != Atom::ListTagRight) |
158 | return std::pair<QString, int>(QString(), 1); |
159 | |
160 | lookAhead = lookAhead->next(); |
161 | int skipAhead; |
162 | if (lookAhead && lookAhead->type() == Atom::SinceTagLeft) { |
163 | lookAhead = lookAhead->next(); |
164 | Q_ASSERT(lookAhead && lookAhead->type() == Atom::String); |
165 | t += QLatin1String(" (since " ); |
166 | if (lookAhead->string().at(i: 0).isDigit()) |
167 | t += QLatin1String("Qt " ); |
168 | t += lookAhead->string() + QLatin1String(")" ); |
169 | skipAhead = 4; |
170 | } else { |
171 | skipAhead = 1; |
172 | } |
173 | return std::pair<QString, int>(t, skipAhead); |
174 | } |
175 | |
176 | /*! |
177 | Parses the table attributes from the given \a atom. |
178 | This method returns a pair containing the width (%) and |
179 | the attribute for this table (either "generic" or |
180 | "borderless"). |
181 | */ |
182 | std::pair<QString, QString> XmlGenerator::getTableWidthAttr(const Atom *atom) |
183 | { |
184 | QString p0, p1; |
185 | QString attr = "generic" ; |
186 | QString width; |
187 | if (atom->count() > 0) { |
188 | p0 = atom->string(i: 0); |
189 | if (atom->count() > 1) |
190 | p1 = atom->string(i: 1); |
191 | } |
192 | if (!p0.isEmpty()) { |
193 | if (p0 == QLatin1String("borderless" )) |
194 | attr = p0; |
195 | else if (p0.contains(c: QLatin1Char('%'))) |
196 | width = p0; |
197 | } |
198 | if (!p1.isEmpty()) { |
199 | if (p1 == QLatin1String("borderless" )) |
200 | attr = p1; |
201 | else if (p1.contains(c: QLatin1Char('%'))) |
202 | width = p1; |
203 | } |
204 | |
205 | // Many times, in the documentation, there is a space before the % sign: |
206 | // this breaks the parsing logic above. |
207 | if (width == QLatin1String("%" )) { |
208 | // The percentage is typically stored in p0, parse it as an int. |
209 | bool ok = false; |
210 | int widthPercentage = p0.toInt(ok: &ok); |
211 | if (ok) { |
212 | width = QString::number(widthPercentage) + "%" ; |
213 | } else { |
214 | width = {}; |
215 | } |
216 | } |
217 | |
218 | return {width, attr}; |
219 | } |
220 | |
221 | /*! |
222 | Registers an anchor reference and returns a unique |
223 | and cleaned copy of the reference (the one that should be |
224 | used in the output). |
225 | To ensure unicity throughout the document, this method |
226 | uses the \a refMap cache. |
227 | */ |
228 | QString XmlGenerator::registerRef(const QString &ref, bool xmlCompliant) |
229 | { |
230 | QString cleanRef = Generator::cleanRef(ref, xmlCompliant); |
231 | |
232 | for (;;) { |
233 | QString &prevRef = refMap[cleanRef.toLower()]; |
234 | if (prevRef.isEmpty()) { |
235 | // This reference has never been met before for this document: register it. |
236 | prevRef = ref; |
237 | break; |
238 | } else if (prevRef == ref) { |
239 | // This exact same reference was already found. This case typically occurs within refForNode. |
240 | break; |
241 | } |
242 | cleanRef += QLatin1Char('x'); |
243 | } |
244 | return cleanRef; |
245 | } |
246 | |
247 | /*! |
248 | Generates a clean and unique reference for the given \a node. |
249 | This reference may depend on the type of the node (typedef, |
250 | QML signal, etc.) |
251 | */ |
252 | QString XmlGenerator::refForNode(const Node *node) |
253 | { |
254 | QString ref; |
255 | switch (node->nodeType()) { |
256 | case Node::Enum: |
257 | ref = node->name() + "-enum" ; |
258 | break; |
259 | case Node::Typedef: { |
260 | const auto *tdf = static_cast<const TypedefNode *>(node); |
261 | if (tdf->associatedEnum()) |
262 | return refForNode(node: tdf->associatedEnum()); |
263 | } Q_FALLTHROUGH(); |
264 | case Node::TypeAlias: |
265 | ref = node->name() + "-typedef" ; |
266 | break; |
267 | case Node::Function: { |
268 | const auto fn = static_cast<const FunctionNode *>(node); |
269 | switch (fn->metaness()) { |
270 | case FunctionNode::QmlSignal: |
271 | ref = fn->name() + "-signal" ; |
272 | break; |
273 | case FunctionNode::QmlSignalHandler: |
274 | ref = fn->name() + "-signal-handler" ; |
275 | break; |
276 | case FunctionNode::QmlMethod: |
277 | ref = fn->name() + "-method" ; |
278 | if (fn->overloadNumber() != 0) |
279 | ref += QLatin1Char('-') + QString::number(fn->overloadNumber()); |
280 | break; |
281 | default: |
282 | if (fn->hasOneAssociatedProperty() && fn->doc().isEmpty()) { |
283 | return refForNode(node: fn->associatedProperties()[0]); |
284 | } else { |
285 | ref = fn->name(); |
286 | if (fn->overloadNumber() != 0) |
287 | ref += QLatin1Char('-') + QString::number(fn->overloadNumber()); |
288 | } |
289 | break; |
290 | } |
291 | } break; |
292 | case Node::SharedComment: { |
293 | if (!node->isPropertyGroup()) |
294 | break; |
295 | } Q_FALLTHROUGH(); |
296 | case Node::QmlProperty: |
297 | if (node->isAttached()) |
298 | ref = node->name() + "-attached-prop" ; |
299 | else |
300 | ref = node->name() + "-prop" ; |
301 | break; |
302 | case Node::Property: |
303 | ref = node->name() + "-prop" ; |
304 | break; |
305 | case Node::Variable: |
306 | ref = node->name() + "-var" ; |
307 | break; |
308 | default: |
309 | break; |
310 | } |
311 | return registerRef(ref); |
312 | } |
313 | |
314 | /*! |
315 | Construct the link string for the \a node and return it. |
316 | The \a relative node is used to decide whether the link |
317 | we are generating is in the same file as the target. |
318 | Note the relative node can be 0, which pretty much |
319 | guarantees that the link and the target aren't in the |
320 | same file. |
321 | */ |
322 | QString XmlGenerator::linkForNode(const Node *node, const Node *relative) |
323 | { |
324 | if (node == nullptr) |
325 | return QString(); |
326 | if (!node->url().isNull()) |
327 | return node->url(); |
328 | if (fileBase(node).isEmpty()) |
329 | return QString(); |
330 | if (node->isPrivate()) |
331 | return QString(); |
332 | |
333 | QString fn = fileName(node); |
334 | if (node->parent() && node->parent()->isQmlType() && node->parent()->isAbstract()) { |
335 | if (Generator::qmlTypeContext()) { |
336 | if (Generator::qmlTypeContext()->inherits(type: node->parent())) { |
337 | fn = fileName(node: Generator::qmlTypeContext()); |
338 | } else if (node->parent()->isInternal() && !noLinkErrors()) { |
339 | node->doc().location().warning( |
340 | QStringLiteral("Cannot link to property in internal type '%1'" ) |
341 | .arg(a: node->parent()->name())); |
342 | return QString(); |
343 | } |
344 | } |
345 | } |
346 | |
347 | QString link = fn; |
348 | |
349 | if (!node->isPageNode() || node->isPropertyGroup()) { |
350 | QString ref = refForNode(node); |
351 | if (relative && fn == fileName(node: relative) && ref == refForNode(node: relative)) |
352 | return QString(); |
353 | |
354 | link += QLatin1Char('#'); |
355 | link += ref; |
356 | } |
357 | |
358 | /* |
359 | If the output is going to subdirectories, then if the |
360 | two nodes will be output to different directories, then |
361 | the link must go up to the parent directory and then |
362 | back down into the other subdirectory. |
363 | */ |
364 | if (relative && (node != relative)) { |
365 | if (useOutputSubdirs() && !node->isExternalPage() |
366 | && node->outputSubdirectory() != relative->outputSubdirectory()) { |
367 | if (link.startsWith(s: QString(node->outputSubdirectory() + QLatin1Char('/')))) { |
368 | link.prepend(s: QString("../" )); |
369 | } else { |
370 | link.prepend(s: QString("../" + node->outputSubdirectory() + QLatin1Char('/'))); |
371 | } |
372 | } |
373 | } |
374 | return link; |
375 | } |
376 | |
377 | /*! |
378 | This function is called for links, i.e. for words that |
379 | are marked with the qdoc link command. For autolinks |
380 | that are not marked with the qdoc link command, the |
381 | getAutoLink() function is called |
382 | |
383 | It returns the string for a link found by using the data |
384 | in the \a atom to search the database. It also sets \a node |
385 | to point to the target node for that link. \a relative points |
386 | to the node holding the qdoc comment where the link command |
387 | was found. |
388 | */ |
389 | QString XmlGenerator::getLink(const Atom *atom, const Node *relative, const Node **node) |
390 | { |
391 | const QString &t = atom->string(); |
392 | |
393 | if (t.isEmpty()) |
394 | return t; |
395 | |
396 | if (t.at(i: 0) == QChar('h')) { |
397 | if (t.startsWith(s: "http:" ) || t.startsWith(s: "https:" )) |
398 | return t; |
399 | } else if (t.at(i: 0) == QChar('f')) { |
400 | if (t.startsWith(s: "file:" ) || t.startsWith(s: "ftp:" )) |
401 | return t; |
402 | } else if (t.at(i: 0) == QChar('m')) { |
403 | if (t.startsWith(s: "mailto:" )) |
404 | return t; |
405 | } |
406 | return getAutoLink(atom, relative, node); |
407 | } |
408 | |
409 | /*! |
410 | This function is called for autolinks, i.e. for words that |
411 | are not marked with the qdoc link command that qdoc has |
412 | reason to believe should be links. |
413 | |
414 | It returns the string for a link found by using the data |
415 | in the \a atom to search the database. It also sets \a node |
416 | to point to the target node for that link. \a relative points |
417 | to the node holding the qdoc comment where the link command |
418 | was found. |
419 | */ |
420 | QString XmlGenerator::getAutoLink(const Atom *atom, const Node *relative, const Node **node, |
421 | Node::Genus genus) |
422 | { |
423 | QString ref; |
424 | |
425 | *node = m_qdb->findNodeForAtom(atom, relative, ref, genus); |
426 | if (!(*node)) |
427 | return QString(); |
428 | |
429 | QString link = (*node)->url(); |
430 | if (link.isNull()) { |
431 | link = linkForNode(node: *node, relative); |
432 | } else if (link.isEmpty()) { |
433 | return link; // Explicit empty url (node is ignored as a link target) |
434 | } |
435 | if (!ref.isEmpty()) { |
436 | qsizetype hashtag = link.lastIndexOf(c: QChar('#')); |
437 | if (hashtag != -1) |
438 | link.truncate(pos: hashtag); |
439 | link += QLatin1Char('#') + ref; |
440 | } |
441 | return link; |
442 | } |
443 | |
444 | std::pair<QString, QString> XmlGenerator::anchorForNode(const Node *node) |
445 | { |
446 | std::pair<QString, QString> anchorPair; |
447 | |
448 | anchorPair.first = Generator::fileName(node); |
449 | if (node->isTextPageNode()) |
450 | anchorPair.second = node->title(); |
451 | |
452 | return anchorPair; |
453 | } |
454 | |
455 | /*! |
456 | Returns a string describing the \a node type. |
457 | */ |
458 | QString XmlGenerator::targetType(const Node *node) |
459 | { |
460 | if (!node) |
461 | return QStringLiteral("external" ); |
462 | |
463 | switch (node->nodeType()) { |
464 | case Node::Namespace: |
465 | return QStringLiteral("namespace" ); |
466 | case Node::Class: |
467 | case Node::Struct: |
468 | case Node::Union: |
469 | return QStringLiteral("class" ); |
470 | case Node::Page: |
471 | case Node::Example: |
472 | return QStringLiteral("page" ); |
473 | case Node::Enum: |
474 | return QStringLiteral("enum" ); |
475 | case Node::TypeAlias: |
476 | return QStringLiteral("alias" ); |
477 | case Node::Typedef: |
478 | return QStringLiteral("typedef" ); |
479 | case Node::Property: |
480 | return QStringLiteral("property" ); |
481 | case Node::Function: |
482 | return QStringLiteral("function" ); |
483 | case Node::Variable: |
484 | return QStringLiteral("variable" ); |
485 | case Node::Module: |
486 | return QStringLiteral("module" ); |
487 | default: |
488 | break; |
489 | } |
490 | return QString(); |
491 | } |
492 | |
493 | QT_END_NAMESPACE |
494 | |