| 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 | using namespace Qt::Literals::StringLiterals; |
| 14 | |
| 15 | QT_BEGIN_NAMESPACE |
| 16 | |
| 17 | const QRegularExpression XmlGenerator::m_funcLeftParen(QStringLiteral("^\\S+(\\(.*\\))" )); |
| 18 | |
| 19 | XmlGenerator::XmlGenerator(FileResolver& file_resolver) : Generator(file_resolver) {} |
| 20 | |
| 21 | /*! |
| 22 | Do not display \brief for QML types, document and collection nodes |
| 23 | */ |
| 24 | bool 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 | */ |
| 33 | bool 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 | */ |
| 46 | bool 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 | */ |
| 67 | int 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 | */ |
| 96 | void 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 | */ |
| 119 | NodeType 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 | */ |
| 134 | void 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 | */ |
| 150 | std::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 | */ |
| 188 | std::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 | */ |
| 234 | QString 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 | */ |
| 258 | QString 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 | */ |
| 329 | QString 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 | */ |
| 393 | QString 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 | */ |
| 426 | QString 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 | |
| 450 | std::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 | */ |
| 464 | QString 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 | |
| 499 | QT_END_NAMESPACE |
| 500 | |