| 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 | |
| 4 | #include "webxmlgenerator.h" |
| 5 | |
| 6 | #include "aggregate.h" |
| 7 | #include "collectionnode.h" |
| 8 | #include "config.h" |
| 9 | #include "helpprojectwriter.h" |
| 10 | #include "node.h" |
| 11 | #include "propertynode.h" |
| 12 | #include "qdocdatabase.h" |
| 13 | #include "quoter.h" |
| 14 | #include "utilities.h" |
| 15 | |
| 16 | #include <QtCore/qxmlstream.h> |
| 17 | |
| 18 | QT_BEGIN_NAMESPACE |
| 19 | |
| 20 | using namespace Qt::StringLiterals; |
| 21 | |
| 22 | static CodeMarker *marker_ = nullptr; |
| 23 | |
| 24 | WebXMLGenerator::WebXMLGenerator(FileResolver& file_resolver) : HtmlGenerator(file_resolver) {} |
| 25 | |
| 26 | void WebXMLGenerator::initializeGenerator() |
| 27 | { |
| 28 | HtmlGenerator::initializeGenerator(); |
| 29 | } |
| 30 | |
| 31 | void WebXMLGenerator::terminateGenerator() |
| 32 | { |
| 33 | HtmlGenerator::terminateGenerator(); |
| 34 | } |
| 35 | |
| 36 | QString WebXMLGenerator::format() |
| 37 | { |
| 38 | return "WebXML" ; |
| 39 | } |
| 40 | |
| 41 | QString WebXMLGenerator::fileExtension() const |
| 42 | { |
| 43 | // As this is meant to be an intermediate format, |
| 44 | // use .html for internal references. The name of |
| 45 | // the output file is set separately in |
| 46 | // beginSubPage() calls. |
| 47 | return "html" ; |
| 48 | } |
| 49 | |
| 50 | /*! |
| 51 | Most of the output is generated by QDocIndexFiles and the append() callback. |
| 52 | Some pages produce supplementary output while being generated, and that's |
| 53 | handled here. |
| 54 | */ |
| 55 | qsizetype WebXMLGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) |
| 56 | { |
| 57 | if (m_supplement && currentWriter) |
| 58 | addAtomElements(writer&: *currentWriter.data(), atom, relative, marker); |
| 59 | return 0; |
| 60 | } |
| 61 | |
| 62 | void WebXMLGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker * /* marker */) |
| 63 | { |
| 64 | QByteArray data; |
| 65 | QXmlStreamWriter writer(&data); |
| 66 | writer.setAutoFormatting(true); |
| 67 | beginSubPage(node: aggregate, fileName: Generator::fileName(node: aggregate, extension: "webxml" )); |
| 68 | writer.writeStartDocument(); |
| 69 | writer.writeStartElement(qualifiedName: "WebXML" ); |
| 70 | writer.writeStartElement(qualifiedName: "document" ); |
| 71 | |
| 72 | generateIndexSections(writer, node: aggregate); |
| 73 | |
| 74 | writer.writeEndElement(); // document |
| 75 | writer.writeEndElement(); // WebXML |
| 76 | writer.writeEndDocument(); |
| 77 | |
| 78 | out() << data; |
| 79 | endSubPage(); |
| 80 | } |
| 81 | |
| 82 | void WebXMLGenerator::generatePageNode(PageNode *pn, CodeMarker * /* marker */) |
| 83 | { |
| 84 | QByteArray data; |
| 85 | currentWriter.reset(other: new QXmlStreamWriter(&data)); |
| 86 | currentWriter->setAutoFormatting(true); |
| 87 | beginSubPage(node: pn, fileName: Generator::fileName(node: pn, extension: "webxml" )); |
| 88 | currentWriter->writeStartDocument(); |
| 89 | currentWriter->writeStartElement(qualifiedName: "WebXML" ); |
| 90 | currentWriter->writeStartElement(qualifiedName: "document" ); |
| 91 | |
| 92 | generateIndexSections(writer&: *currentWriter.data(), node: pn); |
| 93 | |
| 94 | currentWriter->writeEndElement(); // document |
| 95 | currentWriter->writeEndElement(); // WebXML |
| 96 | currentWriter->writeEndDocument(); |
| 97 | |
| 98 | out() << data; |
| 99 | endSubPage(); |
| 100 | } |
| 101 | |
| 102 | void WebXMLGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker* /* marker */) |
| 103 | { |
| 104 | // TODO: [generator-insufficient-structural-abstraction] |
| 105 | |
| 106 | QByteArray data; |
| 107 | QXmlStreamWriter writer(&data); |
| 108 | writer.setAutoFormatting(true); |
| 109 | beginSubPage(node: en, fileName: linkForExampleFile(path: resolved_file.get_query(), fileExt: "webxml" )); |
| 110 | writer.writeStartDocument(); |
| 111 | writer.writeStartElement(qualifiedName: "WebXML" ); |
| 112 | writer.writeStartElement(qualifiedName: "document" ); |
| 113 | writer.writeStartElement(qualifiedName: "page" ); |
| 114 | writer.writeAttribute(qualifiedName: "name" , value: resolved_file.get_query()); |
| 115 | writer.writeAttribute(qualifiedName: "href" , value: linkForExampleFile(path: resolved_file.get_query())); |
| 116 | const QString title = exampleFileTitle(relative: static_cast<const ExampleNode *>(en), fileName: resolved_file.get_query()); |
| 117 | writer.writeAttribute(qualifiedName: "title" , value: title); |
| 118 | writer.writeAttribute(qualifiedName: "fulltitle" , value: title); |
| 119 | writer.writeAttribute(qualifiedName: "subtitle" , value: resolved_file.get_query()); |
| 120 | writer.writeStartElement(qualifiedName: "description" ); |
| 121 | |
| 122 | if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { |
| 123 | writer.writeAttribute(qualifiedName: "path" , value: resolved_file.get_path()); |
| 124 | writer.writeAttribute(qualifiedName: "line" , value: "0" ); |
| 125 | writer.writeAttribute(qualifiedName: "column" , value: "0" ); |
| 126 | } |
| 127 | |
| 128 | Quoter quoter; |
| 129 | Doc::quoteFromFile(location: en->doc().location(), quoter, resolved_file: std::move(resolved_file)); |
| 130 | QString code = quoter.quoteTo(docLocation: en->location(), command: QString(), pattern: QString()); |
| 131 | writer.writeTextElement(qualifiedName: "code" , text: trimmedTrailing(string: code, prefix: QString(), suffix: QString())); |
| 132 | |
| 133 | writer.writeEndElement(); // description |
| 134 | writer.writeEndElement(); // page |
| 135 | writer.writeEndElement(); // document |
| 136 | writer.writeEndElement(); // WebXML |
| 137 | writer.writeEndDocument(); |
| 138 | |
| 139 | out() << data; |
| 140 | endSubPage(); |
| 141 | } |
| 142 | |
| 143 | void WebXMLGenerator::generateIndexSections(QXmlStreamWriter &writer, Node *node) |
| 144 | { |
| 145 | marker_ = CodeMarker::markerForFileName(fileName: node->location().filePath()); |
| 146 | auto qdocIndexFiles = QDocIndexFiles::qdocIndexFiles(); |
| 147 | if (qdocIndexFiles) { |
| 148 | qdocIndexFiles->generateIndexSections(writer, node, post: this); |
| 149 | // generateIndexSections does nothing for groups, so handle them explicitly |
| 150 | if (node->isGroup()) |
| 151 | std::ignore = qdocIndexFiles->generateIndexSection(writer, node, post: this); |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | // Handles callbacks from QDocIndexFiles to add documentation to node |
| 156 | void WebXMLGenerator::append(QXmlStreamWriter &writer, Node *node) |
| 157 | { |
| 158 | Q_ASSERT(marker_); |
| 159 | |
| 160 | writer.writeStartElement(qualifiedName: "description" ); |
| 161 | if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { |
| 162 | writer.writeAttribute(qualifiedName: "path" , value: node->doc().location().filePath()); |
| 163 | writer.writeAttribute(qualifiedName: "line" , value: QString::number(node->doc().location().lineNo())); |
| 164 | writer.writeAttribute(qualifiedName: "column" , value: QString::number(node->doc().location().columnNo())); |
| 165 | } |
| 166 | |
| 167 | if (node->isTextPageNode()) |
| 168 | generateRelations(writer, node); |
| 169 | |
| 170 | if (node->isModule()) { |
| 171 | writer.writeStartElement(qualifiedName: "generatedlist" ); |
| 172 | writer.writeAttribute(qualifiedName: "contents" , value: "classesbymodule" ); |
| 173 | auto *cnn = static_cast<CollectionNode *>(node); |
| 174 | |
| 175 | if (cnn->hasNamespaces()) { |
| 176 | writer.writeStartElement(qualifiedName: "section" ); |
| 177 | writer.writeStartElement(qualifiedName: "heading" ); |
| 178 | writer.writeAttribute(qualifiedName: "level" , value: "1" ); |
| 179 | writer.writeCharacters(text: "Namespaces" ); |
| 180 | writer.writeEndElement(); // heading |
| 181 | NodeMap namespaces{cnn->getMembers(type: NodeType::Namespace)}; |
| 182 | generateAnnotatedList(writer, relative: node, nodeMap: namespaces); |
| 183 | writer.writeEndElement(); // section |
| 184 | } |
| 185 | if (cnn->hasClasses()) { |
| 186 | writer.writeStartElement(qualifiedName: "section" ); |
| 187 | writer.writeStartElement(qualifiedName: "heading" ); |
| 188 | writer.writeAttribute(qualifiedName: "level" , value: "1" ); |
| 189 | writer.writeCharacters(text: "Classes" ); |
| 190 | writer.writeEndElement(); // heading |
| 191 | NodeMap classes{cnn->getMembers(predicate: [](const Node *n){ return n->isClassNode(); })}; |
| 192 | generateAnnotatedList(writer, relative: node, nodeMap: classes); |
| 193 | writer.writeEndElement(); // section |
| 194 | } |
| 195 | writer.writeEndElement(); // generatedlist |
| 196 | } |
| 197 | |
| 198 | m_inLink = m_inSectionHeading = m_hasQuotingInformation = false; |
| 199 | |
| 200 | const Atom *atom = node->doc().body().firstAtom(); |
| 201 | while (atom) |
| 202 | atom = addAtomElements(writer, atom, relative: node, marker: marker_); |
| 203 | |
| 204 | QList<Text> alsoList = node->doc().alsoList(); |
| 205 | supplementAlsoList(node, alsoList); |
| 206 | |
| 207 | if (!alsoList.isEmpty()) { |
| 208 | writer.writeStartElement(qualifiedName: "see-also" ); |
| 209 | for (const auto &item : alsoList) { |
| 210 | const auto *atom = item.firstAtom(); |
| 211 | while (atom) |
| 212 | atom = addAtomElements(writer, atom, relative: node, marker: marker_); |
| 213 | } |
| 214 | writer.writeEndElement(); // see-also |
| 215 | } |
| 216 | |
| 217 | if (node->isExample()) { |
| 218 | m_supplement = true; |
| 219 | generateRequiredLinks(node, marker: marker_); |
| 220 | m_supplement = false; |
| 221 | } else if (node->isGroup()) { |
| 222 | auto *cn = static_cast<CollectionNode *>(node); |
| 223 | if (!cn->noAutoList()) |
| 224 | generateAnnotatedList(writer, relative: node, nodeList: cn->members()); |
| 225 | } |
| 226 | |
| 227 | writer.writeEndElement(); // description |
| 228 | } |
| 229 | |
| 230 | void WebXMLGenerator::generateDocumentation(Node *node) |
| 231 | { |
| 232 | // Don't generate nodes that are already processed, or if they're not supposed to |
| 233 | // generate output, ie. external, index or images nodes. |
| 234 | if (!node->url().isNull() || node->isExternalPage() || node->isIndexNode()) |
| 235 | return; |
| 236 | |
| 237 | if (node->isInternal() && !m_showInternal) |
| 238 | return; |
| 239 | |
| 240 | if (node->parent()) { |
| 241 | if (node->isNamespace() || node->isClassNode() || node->isHeader()) |
| 242 | generateCppReferencePage(aggregate: static_cast<Aggregate *>(node), nullptr); |
| 243 | else if (node->isCollectionNode()) { |
| 244 | if (node->wasSeen()) { |
| 245 | // see remarks in base class impl. |
| 246 | m_qdb->mergeCollections(c: static_cast<CollectionNode *>(node)); |
| 247 | generatePageNode(pn: static_cast<PageNode *>(node), nullptr); |
| 248 | } |
| 249 | } else if (node->isTextPageNode()) |
| 250 | generatePageNode(pn: static_cast<PageNode *>(node), nullptr); |
| 251 | // else if TODO: anything else? |
| 252 | } |
| 253 | |
| 254 | if (node->isAggregate()) { |
| 255 | auto *aggregate = static_cast<Aggregate *>(node); |
| 256 | for (auto c : aggregate->childNodes()) { |
| 257 | if ((c->isAggregate() || c->isTextPageNode() || c->isCollectionNode()) |
| 258 | && !c->isPrivate()) |
| 259 | generateDocumentation(node: c); |
| 260 | } |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | const Atom *WebXMLGenerator::addAtomElements(QXmlStreamWriter &writer, const Atom *atom, |
| 265 | const Node *relative, CodeMarker *marker) |
| 266 | { |
| 267 | bool keepQuoting = false; |
| 268 | |
| 269 | if (!atom) |
| 270 | return nullptr; |
| 271 | |
| 272 | switch (atom->type()) { |
| 273 | case Atom::AnnotatedList: { |
| 274 | const CollectionNode *cn = m_qdb->getCollectionNode(name: atom->string(), type: NodeType::Group); |
| 275 | if (cn) |
| 276 | generateAnnotatedList(writer, relative, nodeList: cn->members()); |
| 277 | } break; |
| 278 | case Atom::AutoLink: { |
| 279 | const Node *node{nullptr}; |
| 280 | QString link{}; |
| 281 | |
| 282 | if (!m_inLink && !m_inSectionHeading) { |
| 283 | link = getAutoLink(atom, relative, node: &node, Genus::API); |
| 284 | |
| 285 | if (!link.isEmpty() && node && node->isDeprecated() |
| 286 | && relative->parent() != node && !relative->isDeprecated()) { |
| 287 | link.clear(); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | startLink(writer, atom, node, link); |
| 292 | |
| 293 | writer.writeCharacters(text: atom->string()); |
| 294 | |
| 295 | if (m_inLink) { |
| 296 | writer.writeEndElement(); // link |
| 297 | m_inLink = false; |
| 298 | } |
| 299 | |
| 300 | break; |
| 301 | } |
| 302 | case Atom::BaseName: |
| 303 | break; |
| 304 | case Atom::BriefLeft: |
| 305 | |
| 306 | writer.writeStartElement(qualifiedName: "brief" ); |
| 307 | switch (relative->nodeType()) { |
| 308 | case NodeType::Property: |
| 309 | writer.writeCharacters(text: "This property" ); |
| 310 | break; |
| 311 | case NodeType::Variable: |
| 312 | writer.writeCharacters(text: "This variable" ); |
| 313 | break; |
| 314 | default: |
| 315 | break; |
| 316 | } |
| 317 | if (relative->isProperty() || relative->isVariable()) { |
| 318 | QString str; |
| 319 | const Atom *a = atom->next(); |
| 320 | while (a != nullptr && a->type() != Atom::BriefRight) { |
| 321 | if (a->type() == Atom::String || a->type() == Atom::AutoLink) |
| 322 | str += a->string(); |
| 323 | a = a->next(); |
| 324 | } |
| 325 | str[0] = str[0].toLower(); |
| 326 | if (str.endsWith(c: '.')) |
| 327 | str.chop(n: 1); |
| 328 | |
| 329 | const QList<QStringView> words = QStringView{str}.split(sep: ' '); |
| 330 | if (!words.isEmpty()) { |
| 331 | QStringView first(words.at(i: 0)); |
| 332 | if (!(first == u"contains" || first == u"specifies" || first == u"describes" |
| 333 | || first == u"defines" || first == u"holds" || first == u"determines" )) |
| 334 | writer.writeCharacters(text: " holds " ); |
| 335 | else |
| 336 | writer.writeCharacters(text: " " ); |
| 337 | } |
| 338 | } |
| 339 | break; |
| 340 | |
| 341 | case Atom::BriefRight: |
| 342 | if (relative->isProperty() || relative->isVariable()) |
| 343 | writer.writeCharacters(text: "." ); |
| 344 | |
| 345 | writer.writeEndElement(); // brief |
| 346 | break; |
| 347 | |
| 348 | case Atom::C: |
| 349 | writer.writeStartElement(qualifiedName: "teletype" ); |
| 350 | if (m_inLink) |
| 351 | writer.writeAttribute(qualifiedName: "type" , value: "normal" ); |
| 352 | else |
| 353 | writer.writeAttribute(qualifiedName: "type" , value: "highlighted" ); |
| 354 | |
| 355 | writer.writeCharacters(text: plainCode(markedCode: atom->string())); |
| 356 | writer.writeEndElement(); // teletype |
| 357 | break; |
| 358 | |
| 359 | case Atom::Code: |
| 360 | if (!m_hasQuotingInformation) |
| 361 | writer.writeTextElement( |
| 362 | qualifiedName: "code" , text: trimmedTrailing(string: plainCode(markedCode: atom->string()), prefix: QString(), suffix: QString())); |
| 363 | else |
| 364 | keepQuoting = true; |
| 365 | break; |
| 366 | |
| 367 | case Atom::CodeBad: |
| 368 | writer.writeTextElement(qualifiedName: "badcode" , |
| 369 | text: trimmedTrailing(string: plainCode(markedCode: atom->string()), prefix: QString(), suffix: QString())); |
| 370 | break; |
| 371 | |
| 372 | case Atom::CodeQuoteArgument: |
| 373 | if (m_quoting) { |
| 374 | if (quoteCommand == "dots" ) { |
| 375 | writer.writeAttribute(qualifiedName: "indent" , value: atom->string()); |
| 376 | writer.writeCharacters(text: "..." ); |
| 377 | } else { |
| 378 | writer.writeCharacters(text: atom->string()); |
| 379 | } |
| 380 | writer.writeEndElement(); // code |
| 381 | keepQuoting = true; |
| 382 | } |
| 383 | break; |
| 384 | |
| 385 | case Atom::CodeQuoteCommand: |
| 386 | if (m_quoting) { |
| 387 | quoteCommand = atom->string(); |
| 388 | writer.writeStartElement(qualifiedName: quoteCommand); |
| 389 | } |
| 390 | break; |
| 391 | |
| 392 | case Atom::ExampleFileLink: { |
| 393 | if (!m_inLink) { |
| 394 | QString link = linkForExampleFile(path: atom->string()); |
| 395 | if (!link.isEmpty()) |
| 396 | startLink(writer, atom, node: relative, link); |
| 397 | } |
| 398 | } break; |
| 399 | |
| 400 | case Atom::ExampleImageLink: { |
| 401 | if (!m_inLink) { |
| 402 | QString link = atom->string(); |
| 403 | if (!link.isEmpty()) |
| 404 | startLink(writer, atom, node: nullptr, link: "images/used-in-examples/" + link); |
| 405 | } |
| 406 | } break; |
| 407 | |
| 408 | case Atom::FootnoteLeft: |
| 409 | writer.writeStartElement(qualifiedName: "footnote" ); |
| 410 | break; |
| 411 | |
| 412 | case Atom::FootnoteRight: |
| 413 | writer.writeEndElement(); // footnote |
| 414 | break; |
| 415 | |
| 416 | case Atom::FormatEndif: |
| 417 | writer.writeEndElement(); // raw |
| 418 | break; |
| 419 | case Atom::FormatIf: |
| 420 | writer.writeStartElement(qualifiedName: "raw" ); |
| 421 | writer.writeAttribute(qualifiedName: "format" , value: atom->string()); |
| 422 | break; |
| 423 | case Atom::FormattingLeft: { |
| 424 | if (atom->string() == ATOM_FORMATTING_BOLD) |
| 425 | writer.writeStartElement(qualifiedName: "bold" ); |
| 426 | else if (atom->string() == ATOM_FORMATTING_ITALIC) |
| 427 | writer.writeStartElement(qualifiedName: "italic" ); |
| 428 | else if (atom->string() == ATOM_FORMATTING_UNDERLINE) |
| 429 | writer.writeStartElement(qualifiedName: "underline" ); |
| 430 | else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) |
| 431 | writer.writeStartElement(qualifiedName: "subscript" ); |
| 432 | else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) |
| 433 | writer.writeStartElement(qualifiedName: "superscript" ); |
| 434 | else if (atom->string() == ATOM_FORMATTING_TELETYPE || atom->string() == ATOM_FORMATTING_NOTRANSLATE) |
| 435 | writer.writeStartElement(qualifiedName: "teletype" ); |
| 436 | else if (atom->string() == ATOM_FORMATTING_PARAMETER) |
| 437 | writer.writeStartElement(qualifiedName: "argument" ); |
| 438 | else if (atom->string() == ATOM_FORMATTING_INDEX) |
| 439 | writer.writeStartElement(qualifiedName: "index" ); |
| 440 | } break; |
| 441 | |
| 442 | case Atom::FormattingRight: { |
| 443 | if (atom->string() == ATOM_FORMATTING_BOLD) |
| 444 | writer.writeEndElement(); |
| 445 | else if (atom->string() == ATOM_FORMATTING_ITALIC) |
| 446 | writer.writeEndElement(); |
| 447 | else if (atom->string() == ATOM_FORMATTING_UNDERLINE) |
| 448 | writer.writeEndElement(); |
| 449 | else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) |
| 450 | writer.writeEndElement(); |
| 451 | else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) |
| 452 | writer.writeEndElement(); |
| 453 | else if (atom->string() == ATOM_FORMATTING_TELETYPE || atom->string() == ATOM_FORMATTING_NOTRANSLATE) |
| 454 | writer.writeEndElement(); |
| 455 | else if (atom->string() == ATOM_FORMATTING_PARAMETER) |
| 456 | writer.writeEndElement(); |
| 457 | else if (atom->string() == ATOM_FORMATTING_INDEX) |
| 458 | writer.writeEndElement(); |
| 459 | else if (atom->string() == ATOM_FORMATTING_TRADEMARK && appendTrademark(atom)) |
| 460 | writer.writeCharacters(text: QChar(0x2122)); // 'TM' symbol |
| 461 | } |
| 462 | if (m_inLink) { |
| 463 | writer.writeEndElement(); // link |
| 464 | m_inLink = false; |
| 465 | } |
| 466 | break; |
| 467 | |
| 468 | case Atom::GeneratedList: |
| 469 | writer.writeStartElement(qualifiedName: "generatedlist" ); |
| 470 | writer.writeAttribute(qualifiedName: "contents" , value: atom->string()); |
| 471 | writer.writeEndElement(); |
| 472 | break; |
| 473 | |
| 474 | // TODO: The other generators treat inlineimage and image |
| 475 | // simultaneously as the diffirences aren't big. It should be |
| 476 | // possible to do the same for webxmlgenerator instead of |
| 477 | // repeating the code. |
| 478 | |
| 479 | // TODO: [generator-insufficient-structural-abstraction] |
| 480 | case Atom::Image: { |
| 481 | auto maybe_resolved_file{file_resolver.resolve(filename: atom->string())}; |
| 482 | if (!maybe_resolved_file) { |
| 483 | // TODO: [uncentralized-admonition][failed-resolve-file] |
| 484 | relative->location().warning(QStringLiteral("Missing image: %1" ).arg(a: atom->string())); |
| 485 | } else { |
| 486 | ResolvedFile file{*maybe_resolved_file}; |
| 487 | QString file_name{QFileInfo{file.get_path()}.fileName()}; |
| 488 | |
| 489 | // TODO: [uncentralized-output-directory-structure] |
| 490 | Config::copyFile(location: relative->doc().location(), sourceFilePath: file.get_path(), userFriendlySourceFilePath: file_name, targetDirPath: outputDir() + QLatin1String("/images" )); |
| 491 | |
| 492 | writer.writeStartElement(qualifiedName: "image" ); |
| 493 | // TODO: [uncentralized-output-directory-structure] |
| 494 | writer.writeAttribute(qualifiedName: "href" , value: "images/" + file_name); |
| 495 | writer.writeEndElement(); |
| 496 | // TODO: [uncentralized-output-directory-structure] |
| 497 | setImageFileName(relative, fileName: "images/" + file_name); |
| 498 | } |
| 499 | break; |
| 500 | } |
| 501 | // TODO: [generator-insufficient-structural-abstraction] |
| 502 | case Atom::InlineImage: { |
| 503 | auto maybe_resolved_file{file_resolver.resolve(filename: atom->string())}; |
| 504 | if (!maybe_resolved_file) { |
| 505 | // TODO: [uncentralized-admonition][failed-resolve-file] |
| 506 | relative->location().warning(QStringLiteral("Missing image: %1" ).arg(a: atom->string())); |
| 507 | } else { |
| 508 | ResolvedFile file{*maybe_resolved_file}; |
| 509 | QString file_name{QFileInfo{file.get_path()}.fileName()}; |
| 510 | |
| 511 | // TODO: [uncentralized-output-directory-structure] |
| 512 | Config::copyFile(location: relative->doc().location(), sourceFilePath: file.get_path(), userFriendlySourceFilePath: file_name, targetDirPath: outputDir() + QLatin1String("/images" )); |
| 513 | |
| 514 | writer.writeStartElement(qualifiedName: "inlineimage" ); |
| 515 | // TODO: [uncentralized-output-directory-structure] |
| 516 | writer.writeAttribute(qualifiedName: "href" , value: "images/" + file_name); |
| 517 | writer.writeEndElement(); |
| 518 | // TODO: [uncentralized-output-directory-structure] |
| 519 | setImageFileName(relative, fileName: "images/" + file_name); |
| 520 | } |
| 521 | break; |
| 522 | } |
| 523 | case Atom::ImageText: |
| 524 | break; |
| 525 | |
| 526 | case Atom::ImportantLeft: |
| 527 | writer.writeStartElement(qualifiedName: "para" ); |
| 528 | writer.writeTextElement(qualifiedName: "bold" , text: "Important:" ); |
| 529 | writer.writeCharacters(text: " " ); |
| 530 | break; |
| 531 | |
| 532 | case Atom::LegaleseLeft: |
| 533 | writer.writeStartElement(qualifiedName: "legalese" ); |
| 534 | break; |
| 535 | |
| 536 | case Atom::LegaleseRight: |
| 537 | writer.writeEndElement(); // legalese |
| 538 | break; |
| 539 | |
| 540 | case Atom::Link: |
| 541 | case Atom::LinkNode: |
| 542 | if (!m_inLink) { |
| 543 | const Node *node = nullptr; |
| 544 | QString link = getLink(atom, relative, node: &node); |
| 545 | if (!link.isEmpty()) |
| 546 | startLink(writer, atom, node, link); |
| 547 | } |
| 548 | break; |
| 549 | |
| 550 | case Atom::ListLeft: |
| 551 | writer.writeStartElement(qualifiedName: "list" ); |
| 552 | |
| 553 | if (atom->string() == ATOM_LIST_BULLET) |
| 554 | writer.writeAttribute(qualifiedName: "type" , value: "bullet" ); |
| 555 | else if (atom->string() == ATOM_LIST_TAG) |
| 556 | writer.writeAttribute(qualifiedName: "type" , value: "definition" ); |
| 557 | else if (atom->string() == ATOM_LIST_VALUE) { |
| 558 | if (relative->isEnumType()) |
| 559 | writer.writeAttribute(qualifiedName: "type" , value: "enum" ); |
| 560 | else |
| 561 | writer.writeAttribute(qualifiedName: "type" , value: "definition" ); |
| 562 | } else { |
| 563 | writer.writeAttribute(qualifiedName: "type" , value: "ordered" ); |
| 564 | if (atom->string() == ATOM_LIST_UPPERALPHA) |
| 565 | writer.writeAttribute(qualifiedName: "start" , value: "A" ); |
| 566 | else if (atom->string() == ATOM_LIST_LOWERALPHA) |
| 567 | writer.writeAttribute(qualifiedName: "start" , value: "a" ); |
| 568 | else if (atom->string() == ATOM_LIST_UPPERROMAN) |
| 569 | writer.writeAttribute(qualifiedName: "start" , value: "I" ); |
| 570 | else if (atom->string() == ATOM_LIST_LOWERROMAN) |
| 571 | writer.writeAttribute(qualifiedName: "start" , value: "i" ); |
| 572 | else // (atom->string() == ATOM_LIST_NUMERIC) |
| 573 | writer.writeAttribute(qualifiedName: "start" , value: "1" ); |
| 574 | } |
| 575 | break; |
| 576 | |
| 577 | case Atom::ListItemNumber: |
| 578 | break; |
| 579 | case Atom::ListTagLeft: { |
| 580 | writer.writeStartElement(qualifiedName: "definition" ); |
| 581 | |
| 582 | writer.writeTextElement( |
| 583 | qualifiedName: "term" , text: plainCode(markedCode: marker->markedUpEnumValue(atom->next()->string(), relative))); |
| 584 | } break; |
| 585 | |
| 586 | case Atom::ListTagRight: |
| 587 | writer.writeEndElement(); // definition |
| 588 | break; |
| 589 | |
| 590 | case Atom::ListItemLeft: |
| 591 | writer.writeStartElement(qualifiedName: "item" ); |
| 592 | break; |
| 593 | |
| 594 | case Atom::ListItemRight: |
| 595 | writer.writeEndElement(); // item |
| 596 | break; |
| 597 | |
| 598 | case Atom::ListRight: |
| 599 | writer.writeEndElement(); // list |
| 600 | break; |
| 601 | |
| 602 | case Atom::NoteLeft: |
| 603 | writer.writeStartElement(qualifiedName: "para" ); |
| 604 | writer.writeTextElement(qualifiedName: "bold" , text: "Note:" ); |
| 605 | writer.writeCharacters(text: " " ); |
| 606 | break; |
| 607 | |
| 608 | // End admonition elements |
| 609 | case Atom::ImportantRight: |
| 610 | case Atom::NoteRight: |
| 611 | case Atom::WarningRight: |
| 612 | writer.writeEndElement(); // para |
| 613 | break; |
| 614 | |
| 615 | case Atom::Nop: |
| 616 | break; |
| 617 | |
| 618 | case Atom::CaptionLeft: |
| 619 | case Atom::ParaLeft: |
| 620 | writer.writeStartElement(qualifiedName: "para" ); |
| 621 | break; |
| 622 | |
| 623 | case Atom::CaptionRight: |
| 624 | case Atom::ParaRight: |
| 625 | writer.writeEndElement(); // para |
| 626 | break; |
| 627 | |
| 628 | case Atom::QuotationLeft: |
| 629 | writer.writeStartElement(qualifiedName: "quote" ); |
| 630 | break; |
| 631 | |
| 632 | case Atom::QuotationRight: |
| 633 | writer.writeEndElement(); // quote |
| 634 | break; |
| 635 | |
| 636 | case Atom::RawString: |
| 637 | writer.writeCharacters(text: atom->string()); |
| 638 | break; |
| 639 | |
| 640 | case Atom::SectionLeft: |
| 641 | writer.writeStartElement(qualifiedName: "section" ); |
| 642 | writer.writeAttribute(qualifiedName: "id" , |
| 643 | value: Utilities::asAsciiPrintable(name: Text::sectionHeading(sectionBegin: atom).toString())); |
| 644 | break; |
| 645 | |
| 646 | case Atom::SectionRight: |
| 647 | writer.writeEndElement(); // section |
| 648 | break; |
| 649 | |
| 650 | case Atom::SectionHeadingLeft: { |
| 651 | writer.writeStartElement(qualifiedName: "heading" ); |
| 652 | int unit = atom->string().toInt(); // + hOffset(relative) |
| 653 | writer.writeAttribute(qualifiedName: "level" , value: QString::number(unit)); |
| 654 | m_inSectionHeading = true; |
| 655 | } break; |
| 656 | |
| 657 | case Atom::SectionHeadingRight: |
| 658 | writer.writeEndElement(); // heading |
| 659 | m_inSectionHeading = false; |
| 660 | break; |
| 661 | |
| 662 | case Atom::SidebarLeft: |
| 663 | case Atom::SidebarRight: |
| 664 | break; |
| 665 | |
| 666 | case Atom::SnippetCommand: |
| 667 | if (m_quoting) { |
| 668 | writer.writeStartElement(qualifiedName: atom->string()); |
| 669 | } |
| 670 | break; |
| 671 | |
| 672 | case Atom::SnippetIdentifier: |
| 673 | if (m_quoting) { |
| 674 | writer.writeAttribute(qualifiedName: "identifier" , value: atom->string()); |
| 675 | writer.writeEndElement(); |
| 676 | keepQuoting = true; |
| 677 | } |
| 678 | break; |
| 679 | |
| 680 | case Atom::SnippetLocation: |
| 681 | if (m_quoting) { |
| 682 | const QString &location = atom->string(); |
| 683 | writer.writeAttribute(qualifiedName: "location" , value: location); |
| 684 | auto maybe_resolved_file{file_resolver.resolve(filename: location)}; |
| 685 | // const QString resolved = Doc::resolveFile(Location(), location); |
| 686 | if (maybe_resolved_file) |
| 687 | writer.writeAttribute(qualifiedName: "path" , value: (*maybe_resolved_file).get_path()); |
| 688 | else { |
| 689 | // TODO: [uncetnralized-admonition][failed-resolve-file] |
| 690 | QString details = std::transform_reduce( |
| 691 | first: file_resolver.get_search_directories().cbegin(), |
| 692 | last: file_resolver.get_search_directories().cend(), |
| 693 | init: u"Searched directories:"_s , |
| 694 | binary_op: std::plus(), |
| 695 | unary_op: [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } |
| 696 | ); |
| 697 | |
| 698 | relative->location().warning(message: u"Cannot find file to quote from: %1"_s .arg(a: location), details); |
| 699 | } |
| 700 | } |
| 701 | break; |
| 702 | |
| 703 | case Atom::String: |
| 704 | writer.writeCharacters(text: atom->string()); |
| 705 | break; |
| 706 | case Atom::TableLeft: |
| 707 | writer.writeStartElement(qualifiedName: "table" ); |
| 708 | if (atom->string().contains(s: "%" )) |
| 709 | writer.writeAttribute(qualifiedName: "width" , value: atom->string()); |
| 710 | break; |
| 711 | |
| 712 | case Atom::TableRight: |
| 713 | writer.writeEndElement(); // table |
| 714 | break; |
| 715 | |
| 716 | case Atom::TableHeaderLeft: |
| 717 | writer.writeStartElement(qualifiedName: "header" ); |
| 718 | break; |
| 719 | |
| 720 | case Atom::TableHeaderRight: |
| 721 | writer.writeEndElement(); // header |
| 722 | break; |
| 723 | |
| 724 | case Atom::TableRowLeft: |
| 725 | writer.writeStartElement(qualifiedName: "row" ); |
| 726 | break; |
| 727 | |
| 728 | case Atom::TableRowRight: |
| 729 | writer.writeEndElement(); // row |
| 730 | break; |
| 731 | |
| 732 | case Atom::TableItemLeft: { |
| 733 | writer.writeStartElement(qualifiedName: "item" ); |
| 734 | QStringList spans = atom->string().split(sep: "," ); |
| 735 | if (spans.size() == 2) { |
| 736 | if (spans.at(i: 0) != "1" ) |
| 737 | writer.writeAttribute(qualifiedName: "colspan" , value: spans.at(i: 0).trimmed()); |
| 738 | if (spans.at(i: 1) != "1" ) |
| 739 | writer.writeAttribute(qualifiedName: "rowspan" , value: spans.at(i: 1).trimmed()); |
| 740 | } |
| 741 | } break; |
| 742 | case Atom::TableItemRight: |
| 743 | writer.writeEndElement(); // item |
| 744 | break; |
| 745 | |
| 746 | case Atom::Target: |
| 747 | writer.writeStartElement(qualifiedName: "target" ); |
| 748 | writer.writeAttribute(qualifiedName: "name" , value: Utilities::asAsciiPrintable(name: atom->string())); |
| 749 | writer.writeEndElement(); |
| 750 | break; |
| 751 | |
| 752 | case Atom::WarningLeft: |
| 753 | writer.writeStartElement(qualifiedName: "para" ); |
| 754 | writer.writeTextElement(qualifiedName: "bold" , text: "Warning:" ); |
| 755 | writer.writeCharacters(text: " " ); |
| 756 | break; |
| 757 | |
| 758 | case Atom::UnhandledFormat: |
| 759 | case Atom::UnknownCommand: |
| 760 | writer.writeCharacters(text: atom->typeString()); |
| 761 | break; |
| 762 | default: |
| 763 | break; |
| 764 | } |
| 765 | |
| 766 | m_hasQuotingInformation = keepQuoting; |
| 767 | return atom->next(); |
| 768 | } |
| 769 | |
| 770 | void WebXMLGenerator::startLink(QXmlStreamWriter &writer, const Atom *atom, const Node *node, |
| 771 | const QString &link) |
| 772 | { |
| 773 | QString fullName = link; |
| 774 | if (node) |
| 775 | fullName = node->fullName(); |
| 776 | if (!fullName.isEmpty() && !link.isEmpty()) { |
| 777 | writer.writeStartElement(qualifiedName: "link" ); |
| 778 | if (atom && !atom->string().isEmpty()) |
| 779 | writer.writeAttribute(qualifiedName: "raw" , value: atom->string()); |
| 780 | else |
| 781 | writer.writeAttribute(qualifiedName: "raw" , value: fullName); |
| 782 | writer.writeAttribute(qualifiedName: "href" , value: link); |
| 783 | writer.writeAttribute(qualifiedName: "type" , value: targetType(node)); |
| 784 | if (node) { |
| 785 | switch (node->nodeType()) { |
| 786 | case NodeType::Enum: |
| 787 | writer.writeAttribute(qualifiedName: "enum" , value: fullName); |
| 788 | break; |
| 789 | case NodeType::Example: { |
| 790 | const auto *en = static_cast<const ExampleNode *>(node); |
| 791 | const QString fileTitle = atom ? exampleFileTitle(relative: en, fileName: atom->string()) : QString(); |
| 792 | if (!fileTitle.isEmpty()) { |
| 793 | writer.writeAttribute(qualifiedName: "page" , value: fileTitle); |
| 794 | break; |
| 795 | } |
| 796 | } |
| 797 | Q_FALLTHROUGH(); |
| 798 | case NodeType::Page: |
| 799 | writer.writeAttribute(qualifiedName: "page" , value: fullName); |
| 800 | break; |
| 801 | case NodeType::Property: { |
| 802 | const auto *propertyNode = static_cast<const PropertyNode *>(node); |
| 803 | if (!propertyNode->getters().empty()) |
| 804 | writer.writeAttribute(qualifiedName: "getter" , value: propertyNode->getters().at(i: 0)->fullName()); |
| 805 | } break; |
| 806 | default: |
| 807 | break; |
| 808 | } |
| 809 | } |
| 810 | m_inLink = true; |
| 811 | } |
| 812 | } |
| 813 | |
| 814 | void WebXMLGenerator::endLink(QXmlStreamWriter &writer) |
| 815 | { |
| 816 | if (m_inLink) { |
| 817 | writer.writeEndElement(); // link |
| 818 | m_inLink = false; |
| 819 | } |
| 820 | } |
| 821 | |
| 822 | void WebXMLGenerator::generateRelations(QXmlStreamWriter &writer, const Node *node) |
| 823 | { |
| 824 | if (node && !node->links().empty()) { |
| 825 | std::pair<QString, QString> anchorPair; |
| 826 | const Node *linkNode; |
| 827 | |
| 828 | for (auto it = node->links().cbegin(); it != node->links().cend(); ++it) { |
| 829 | |
| 830 | linkNode = m_qdb->findNodeForTarget(target: it.value().first, relative: node); |
| 831 | |
| 832 | if (!linkNode) |
| 833 | linkNode = node; |
| 834 | |
| 835 | if (linkNode == node) |
| 836 | anchorPair = it.value(); |
| 837 | else |
| 838 | anchorPair = anchorForNode(node: linkNode); |
| 839 | |
| 840 | writer.writeStartElement(qualifiedName: "relation" ); |
| 841 | writer.writeAttribute(qualifiedName: "href" , value: anchorPair.first); |
| 842 | writer.writeAttribute(qualifiedName: "type" , value: targetType(node: linkNode)); |
| 843 | |
| 844 | switch (it.key()) { |
| 845 | case Node::StartLink: |
| 846 | writer.writeAttribute(qualifiedName: "meta" , value: "start" ); |
| 847 | break; |
| 848 | case Node::NextLink: |
| 849 | writer.writeAttribute(qualifiedName: "meta" , value: "next" ); |
| 850 | break; |
| 851 | case Node::PreviousLink: |
| 852 | writer.writeAttribute(qualifiedName: "meta" , value: "previous" ); |
| 853 | break; |
| 854 | case Node::ContentsLink: |
| 855 | writer.writeAttribute(qualifiedName: "meta" , value: "contents" ); |
| 856 | break; |
| 857 | default: |
| 858 | writer.writeAttribute(qualifiedName: "meta" , value: "" ); |
| 859 | } |
| 860 | writer.writeAttribute(qualifiedName: "description" , value: anchorPair.second); |
| 861 | writer.writeEndElement(); // link |
| 862 | } |
| 863 | } |
| 864 | } |
| 865 | |
| 866 | void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, |
| 867 | const NodeMap &nodeMap) |
| 868 | { |
| 869 | generateAnnotatedList(writer, relative, nodeList: nodeMap.values()); |
| 870 | } |
| 871 | |
| 872 | void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, |
| 873 | const NodeList &nodeList) |
| 874 | { |
| 875 | writer.writeStartElement(qualifiedName: "table" ); |
| 876 | writer.writeAttribute(qualifiedName: "width" , value: "100%" ); |
| 877 | |
| 878 | for (const auto *node : nodeList) { |
| 879 | writer.writeStartElement(qualifiedName: "row" ); |
| 880 | writer.writeStartElement(qualifiedName: "item" ); |
| 881 | writer.writeStartElement(qualifiedName: "para" ); |
| 882 | const QString link = linkForNode(node, relative); |
| 883 | startLink(writer, atom: node->doc().body().firstAtom(), node, link); |
| 884 | endLink(writer); |
| 885 | writer.writeEndElement(); // para |
| 886 | writer.writeEndElement(); // item |
| 887 | |
| 888 | writer.writeStartElement(qualifiedName: "item" ); |
| 889 | writer.writeStartElement(qualifiedName: "para" ); |
| 890 | writer.writeCharacters(text: node->doc().briefText().toString()); |
| 891 | writer.writeEndElement(); // para |
| 892 | writer.writeEndElement(); // item |
| 893 | writer.writeEndElement(); // row |
| 894 | } |
| 895 | writer.writeEndElement(); // table |
| 896 | } |
| 897 | |
| 898 | QString WebXMLGenerator::fileBase(const Node *node) const |
| 899 | { |
| 900 | return Generator::fileBase(node); |
| 901 | } |
| 902 | |
| 903 | QT_END_NAMESPACE |
| 904 | |