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

source code of qttools/src/qdoc/qdoc/src/qdoc/xmlgenerator.cpp