// Copyright (C) 2021 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "webxmlgenerator.h" #include "aggregate.h" #include "collectionnode.h" #include "config.h" #include "helpprojectwriter.h" #include "node.h" #include "propertynode.h" #include "qdocdatabase.h" #include "quoter.h" #include "utilities.h" #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; static CodeMarker *marker_ = nullptr; WebXMLGenerator::WebXMLGenerator(FileResolver& file_resolver) : HtmlGenerator(file_resolver) {} void WebXMLGenerator::initializeGenerator() { HtmlGenerator::initializeGenerator(); } void WebXMLGenerator::terminateGenerator() { HtmlGenerator::terminateGenerator(); } QString WebXMLGenerator::format() { return "WebXML"; } QString WebXMLGenerator::fileExtension() const { // As this is meant to be an intermediate format, // use .html for internal references. The name of // the output file is set separately in // beginSubPage() calls. return "html"; } /*! Most of the output is generated by QDocIndexFiles and the append() callback. Some pages produce supplementary output while being generated, and that's handled here. */ qsizetype WebXMLGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) { if (m_supplement && currentWriter) addAtomElements(*currentWriter.data(), atom, relative, marker); return 0; } void WebXMLGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker * /* marker */) { QByteArray data; QXmlStreamWriter writer(&data); writer.setAutoFormatting(true); beginSubPage(aggregate, Generator::fileName(aggregate, "webxml")); writer.writeStartDocument(); writer.writeStartElement("WebXML"); writer.writeStartElement("document"); generateIndexSections(writer, aggregate); writer.writeEndElement(); // document writer.writeEndElement(); // WebXML writer.writeEndDocument(); out() << data; endSubPage(); } void WebXMLGenerator::generatePageNode(PageNode *pn, CodeMarker * /* marker */) { QByteArray data; currentWriter.reset(new QXmlStreamWriter(&data)); currentWriter->setAutoFormatting(true); beginSubPage(pn, Generator::fileName(pn, "webxml")); currentWriter->writeStartDocument(); currentWriter->writeStartElement("WebXML"); currentWriter->writeStartElement("document"); generateIndexSections(*currentWriter.data(), pn); currentWriter->writeEndElement(); // document currentWriter->writeEndElement(); // WebXML currentWriter->writeEndDocument(); out() << data; endSubPage(); } void WebXMLGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker* /* marker */) { // TODO: [generator-insufficient-structural-abstraction] QByteArray data; QXmlStreamWriter writer(&data); writer.setAutoFormatting(true); beginSubPage(en, linkForExampleFile(resolved_file.get_query(), "webxml")); writer.writeStartDocument(); writer.writeStartElement("WebXML"); writer.writeStartElement("document"); writer.writeStartElement("page"); writer.writeAttribute("name", resolved_file.get_path()); writer.writeAttribute("href", linkForExampleFile(resolved_file.get_path())); QString title = exampleFileTitle(static_cast(en), resolved_file.get_path()); writer.writeAttribute("title", title); writer.writeAttribute("fulltitle", title); writer.writeAttribute("subtitle", resolved_file.get_path()); writer.writeStartElement("description"); if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { writer.writeAttribute("path", resolved_file.get_path()); writer.writeAttribute("line", "0"); writer.writeAttribute("column", "0"); } Quoter quoter; Doc::quoteFromFile(en->doc().location(), quoter, resolved_file); QString code = quoter.quoteTo(en->location(), QString(), QString()); writer.writeTextElement("code", trimmedTrailing(code, QString(), QString())); writer.writeEndElement(); // description writer.writeEndElement(); // page writer.writeEndElement(); // document writer.writeEndElement(); // WebXML writer.writeEndDocument(); out() << data; endSubPage(); } void WebXMLGenerator::generateIndexSections(QXmlStreamWriter &writer, Node *node) { marker_ = CodeMarker::markerForFileName(node->location().filePath()); auto qdocIndexFiles = QDocIndexFiles::qdocIndexFiles(); if (qdocIndexFiles) { qdocIndexFiles->generateIndexSections(writer, node, this); // generateIndexSections does nothing for groups, so handle them explicitly if (node->isGroup()) qdocIndexFiles->generateIndexSection(writer, node, this); } } // Handles callbacks from QDocIndexFiles to add documentation to node void WebXMLGenerator::append(QXmlStreamWriter &writer, Node *node) { Q_ASSERT(marker_); writer.writeStartElement("description"); if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { writer.writeAttribute("path", node->doc().location().filePath()); writer.writeAttribute("line", QString::number(node->doc().location().lineNo())); writer.writeAttribute("column", QString::number(node->doc().location().columnNo())); } if (node->isTextPageNode()) generateRelations(writer, node); if (node->isModule()) { writer.writeStartElement("generatedlist"); writer.writeAttribute("contents", "classesbymodule"); auto *cnn = static_cast(node); if (cnn->hasNamespaces()) { writer.writeStartElement("section"); writer.writeStartElement("heading"); writer.writeAttribute("level", "1"); writer.writeCharacters("Namespaces"); writer.writeEndElement(); // heading NodeMap namespaces; cnn->getMemberNamespaces(namespaces); generateAnnotatedList(writer, node, namespaces); writer.writeEndElement(); // section } if (cnn->hasClasses()) { writer.writeStartElement("section"); writer.writeStartElement("heading"); writer.writeAttribute("level", "1"); writer.writeCharacters("Classes"); writer.writeEndElement(); // heading NodeMap classes; cnn->getMemberClasses(classes); generateAnnotatedList(writer, node, classes); writer.writeEndElement(); // section } writer.writeEndElement(); // generatedlist } m_inLink = m_inSectionHeading = m_hasQuotingInformation = false; const Atom *atom = node->doc().body().firstAtom(); while (atom) atom = addAtomElements(writer, atom, node, marker_); QList alsoList = node->doc().alsoList(); supplementAlsoList(node, alsoList); if (!alsoList.isEmpty()) { writer.writeStartElement("see-also"); for (const auto &item : alsoList) { const auto *atom = item.firstAtom(); while (atom) atom = addAtomElements(writer, atom, node, marker_); } writer.writeEndElement(); // see-also } if (node->isExample()) { m_supplement = true; generateRequiredLinks(node, marker_); m_supplement = false; } else if (node->isGroup()) { auto *cn = static_cast(node); if (!cn->noAutoList()) generateAnnotatedList(writer, node, cn->members()); } writer.writeEndElement(); // description } void WebXMLGenerator::generateDocumentation(Node *node) { // Don't generate nodes that are already processed, or if they're not supposed to // generate output, ie. external, index or images nodes. if (!node->url().isNull() || node->isExternalPage() || node->isIndexNode()) return; if (node->isInternal() && !m_showInternal) return; if (node->parent()) { if (node->isNamespace() || node->isClassNode() || node->isHeader()) generateCppReferencePage(static_cast(node), nullptr); else if (node->isCollectionNode()) { if (node->wasSeen()) { // see remarks in base class impl. m_qdb->mergeCollections(static_cast(node)); generatePageNode(static_cast(node), nullptr); } } else if (node->isTextPageNode()) generatePageNode(static_cast(node), nullptr); // else if TODO: anything else? } if (node->isAggregate()) { auto *aggregate = static_cast(node); for (auto c : aggregate->childNodes()) { if ((c->isAggregate() || c->isTextPageNode() || c->isCollectionNode()) && !c->isPrivate()) generateDocumentation(c); } } } const Atom *WebXMLGenerator::addAtomElements(QXmlStreamWriter &writer, const Atom *atom, const Node *relative, CodeMarker *marker) { bool keepQuoting = false; if (!atom) return nullptr; switch (atom->type()) { case Atom::AnnotatedList: { const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group); if (cn) generateAnnotatedList(writer, relative, cn->members()); } break; case Atom::AutoLink: { const Node *node{nullptr}; QString link{}; if (!m_inLink && !m_inSectionHeading) { link = getAutoLink(atom, relative, &node, Node::API); if (!link.isEmpty() && node && node->isDeprecated() && relative->parent() != node && !relative->isDeprecated()) { link.clear(); } } startLink(writer, atom, node, link); writer.writeCharacters(atom->string()); if (m_inLink) { writer.writeEndElement(); // link m_inLink = false; } break; } case Atom::BaseName: break; case Atom::BriefLeft: writer.writeStartElement("brief"); switch (relative->nodeType()) { case Node::Property: writer.writeCharacters("This property"); break; case Node::Variable: writer.writeCharacters("This variable"); break; default: break; } if (relative->isProperty() || relative->isVariable()) { QString str; const Atom *a = atom->next(); while (a != nullptr && a->type() != Atom::BriefRight) { if (a->type() == Atom::String || a->type() == Atom::AutoLink) str += a->string(); a = a->next(); } str[0] = str[0].toLower(); if (str.endsWith('.')) str.chop(1); const QList words = QStringView{str}.split(' '); if (!words.isEmpty()) { QStringView first(words.at(0)); if (!(first == u"contains" || first == u"specifies" || first == u"describes" || first == u"defines" || first == u"holds" || first == u"determines")) writer.writeCharacters(" holds "); else writer.writeCharacters(" "); } } break; case Atom::BriefRight: if (relative->isProperty() || relative->isVariable()) writer.writeCharacters("."); writer.writeEndElement(); // brief break; case Atom::C: writer.writeStartElement("teletype"); if (m_inLink) writer.writeAttribute("type", "normal"); else writer.writeAttribute("type", "highlighted"); writer.writeCharacters(plainCode(atom->string())); writer.writeEndElement(); // teletype break; case Atom::Code: if (!m_hasQuotingInformation) writer.writeTextElement( "code", trimmedTrailing(plainCode(atom->string()), QString(), QString())); else keepQuoting = true; break; case Atom::CodeBad: writer.writeTextElement("badcode", trimmedTrailing(plainCode(atom->string()), QString(), QString())); break; case Atom::CodeQuoteArgument: if (m_quoting) { if (quoteCommand == "dots") { writer.writeAttribute("indent", atom->string()); writer.writeCharacters("..."); } else { writer.writeCharacters(atom->string()); } writer.writeEndElement(); // code keepQuoting = true; } break; case Atom::CodeQuoteCommand: if (m_quoting) { quoteCommand = atom->string(); writer.writeStartElement(quoteCommand); } break; case Atom::ExampleFileLink: { if (!m_inLink) { QString link = linkForExampleFile(atom->string()); if (!link.isEmpty()) startLink(writer, atom, relative, link); } } break; case Atom::ExampleImageLink: { if (!m_inLink) { QString link = atom->string(); if (!link.isEmpty()) startLink(writer, atom, nullptr, "images/used-in-examples/" + link); } } break; case Atom::FootnoteLeft: writer.writeStartElement("footnote"); break; case Atom::FootnoteRight: writer.writeEndElement(); // footnote break; case Atom::FormatEndif: writer.writeEndElement(); // raw break; case Atom::FormatIf: writer.writeStartElement("raw"); writer.writeAttribute("format", atom->string()); break; case Atom::FormattingLeft: { if (atom->string() == ATOM_FORMATTING_BOLD) writer.writeStartElement("bold"); else if (atom->string() == ATOM_FORMATTING_ITALIC) writer.writeStartElement("italic"); else if (atom->string() == ATOM_FORMATTING_UNDERLINE) writer.writeStartElement("underline"); else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) writer.writeStartElement("subscript"); else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) writer.writeStartElement("superscript"); else if (atom->string() == ATOM_FORMATTING_TELETYPE) writer.writeStartElement("teletype"); else if (atom->string() == ATOM_FORMATTING_PARAMETER) writer.writeStartElement("argument"); else if (atom->string() == ATOM_FORMATTING_INDEX) writer.writeStartElement("index"); } break; case Atom::FormattingRight: { if (atom->string() == ATOM_FORMATTING_BOLD) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_ITALIC) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_UNDERLINE) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_TELETYPE) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_PARAMETER) writer.writeEndElement(); else if (atom->string() == ATOM_FORMATTING_INDEX) writer.writeEndElement(); } if (m_inLink) { writer.writeEndElement(); // link m_inLink = false; } break; case Atom::GeneratedList: writer.writeStartElement("generatedlist"); writer.writeAttribute("contents", atom->string()); writer.writeEndElement(); break; // TODO: The other generators treat inlineimage and image // simultaneously as the diffirences aren't big. It should be // possible to do the same for webxmlgenerator instead of // repeating the code. // TODO: [generator-insufficient-structural-abstraction] case Atom::Image: { auto maybe_resolved_file{file_resolver.resolve(atom->string())}; if (!maybe_resolved_file) { // TODO: [uncentralized-admonition][failed-resolve-file] relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string())); } else { ResolvedFile file{*maybe_resolved_file}; QString file_name{QFileInfo{file.get_path()}.fileName()}; // TODO: [uncentralized-output-directory-structure] Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images")); writer.writeStartElement("image"); // TODO: [uncentralized-output-directory-structure] writer.writeAttribute("href", "images/" + file_name); writer.writeEndElement(); // TODO: [uncentralized-output-directory-structure] setImageFileName(relative, "images/" + file_name); } break; } // TODO: [generator-insufficient-structural-abstraction] case Atom::InlineImage: { auto maybe_resolved_file{file_resolver.resolve(atom->string())}; if (!maybe_resolved_file) { // TODO: [uncentralized-admonition][failed-resolve-file] relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string())); } else { ResolvedFile file{*maybe_resolved_file}; QString file_name{QFileInfo{file.get_path()}.fileName()}; // TODO: [uncentralized-output-directory-structure] Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images")); writer.writeStartElement("inlineimage"); // TODO: [uncentralized-output-directory-structure] writer.writeAttribute("href", "images/" + file_name); writer.writeEndElement(); // TODO: [uncentralized-output-directory-structure] setImageFileName(relative, "images/" + file_name); } break; } case Atom::ImageText: break; case Atom::ImportantLeft: writer.writeStartElement("para"); writer.writeTextElement("bold", "Important:"); writer.writeCharacters(" "); break; case Atom::LegaleseLeft: writer.writeStartElement("legalese"); break; case Atom::LegaleseRight: writer.writeEndElement(); // legalese break; case Atom::Link: case Atom::LinkNode: if (!m_inLink) { const Node *node = nullptr; QString link = getLink(atom, relative, &node); if (!link.isEmpty()) startLink(writer, atom, node, link); } break; case Atom::ListLeft: writer.writeStartElement("list"); if (atom->string() == ATOM_LIST_BULLET) writer.writeAttribute("type", "bullet"); else if (atom->string() == ATOM_LIST_TAG) writer.writeAttribute("type", "definition"); else if (atom->string() == ATOM_LIST_VALUE) { if (relative->isEnumType()) writer.writeAttribute("type", "enum"); else writer.writeAttribute("type", "definition"); } else { writer.writeAttribute("type", "ordered"); if (atom->string() == ATOM_LIST_UPPERALPHA) writer.writeAttribute("start", "A"); else if (atom->string() == ATOM_LIST_LOWERALPHA) writer.writeAttribute("start", "a"); else if (atom->string() == ATOM_LIST_UPPERROMAN) writer.writeAttribute("start", "I"); else if (atom->string() == ATOM_LIST_LOWERROMAN) writer.writeAttribute("start", "i"); else // (atom->string() == ATOM_LIST_NUMERIC) writer.writeAttribute("start", "1"); } break; case Atom::ListItemNumber: break; case Atom::ListTagLeft: { writer.writeStartElement("definition"); writer.writeTextElement( "term", plainCode(marker->markedUpEnumValue(atom->next()->string(), relative))); } break; case Atom::ListTagRight: writer.writeEndElement(); // definition break; case Atom::ListItemLeft: writer.writeStartElement("item"); break; case Atom::ListItemRight: writer.writeEndElement(); // item break; case Atom::ListRight: writer.writeEndElement(); // list break; case Atom::NoteLeft: writer.writeStartElement("para"); writer.writeTextElement("bold", "Note:"); writer.writeCharacters(" "); break; // End admonition elements case Atom::ImportantRight: case Atom::NoteRight: case Atom::WarningRight: writer.writeEndElement(); // para break; case Atom::Nop: break; case Atom::CaptionLeft: case Atom::ParaLeft: writer.writeStartElement("para"); break; case Atom::CaptionRight: case Atom::ParaRight: writer.writeEndElement(); // para break; case Atom::QuotationLeft: writer.writeStartElement("quote"); break; case Atom::QuotationRight: writer.writeEndElement(); // quote break; case Atom::RawString: writer.writeCharacters(atom->string()); break; case Atom::SectionLeft: writer.writeStartElement("section"); writer.writeAttribute("id", Utilities::asAsciiPrintable(Text::sectionHeading(atom).toString())); break; case Atom::SectionRight: writer.writeEndElement(); // section break; case Atom::SectionHeadingLeft: { writer.writeStartElement("heading"); int unit = atom->string().toInt(); // + hOffset(relative) writer.writeAttribute("level", QString::number(unit)); m_inSectionHeading = true; } break; case Atom::SectionHeadingRight: writer.writeEndElement(); // heading m_inSectionHeading = false; break; case Atom::SidebarLeft: case Atom::SidebarRight: break; case Atom::SnippetCommand: if (m_quoting) { writer.writeStartElement(atom->string()); } break; case Atom::SnippetIdentifier: if (m_quoting) { writer.writeAttribute("identifier", atom->string()); writer.writeEndElement(); keepQuoting = true; } break; case Atom::SnippetLocation: if (m_quoting) { const QString &location = atom->string(); writer.writeAttribute("location", location); auto maybe_resolved_file{file_resolver.resolve(location)}; // const QString resolved = Doc::resolveFile(Location(), location); if (maybe_resolved_file) writer.writeAttribute("path", (*maybe_resolved_file).get_path()); else { // TODO: [uncetnralized-admonition][failed-resolve-file] QString details = std::transform_reduce( file_resolver.get_search_directories().cbegin(), file_resolver.get_search_directories().cend(), u"Searched directories:"_s, std::plus(), [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } ); relative->location().warning(u"Cannot find file to quote from: %1"_s.arg(location), details); } } break; case Atom::String: writer.writeCharacters(atom->string()); break; case Atom::TableLeft: writer.writeStartElement("table"); if (atom->string().contains("%")) writer.writeAttribute("width", atom->string()); break; case Atom::TableRight: writer.writeEndElement(); // table break; case Atom::TableHeaderLeft: writer.writeStartElement("header"); break; case Atom::TableHeaderRight: writer.writeEndElement(); // header break; case Atom::TableRowLeft: writer.writeStartElement("row"); break; case Atom::TableRowRight: writer.writeEndElement(); // row break; case Atom::TableItemLeft: { writer.writeStartElement("item"); QStringList spans = atom->string().split(","); if (spans.size() == 2) { if (spans.at(0) != "1") writer.writeAttribute("colspan", spans.at(0).trimmed()); if (spans.at(1) != "1") writer.writeAttribute("rowspan", spans.at(1).trimmed()); } } break; case Atom::TableItemRight: writer.writeEndElement(); // item break; case Atom::Target: writer.writeStartElement("target"); writer.writeAttribute("name", Utilities::asAsciiPrintable(atom->string())); writer.writeEndElement(); break; case Atom::WarningLeft: writer.writeStartElement("para"); writer.writeTextElement("bold", "Warning:"); writer.writeCharacters(" "); break; case Atom::UnhandledFormat: case Atom::UnknownCommand: writer.writeCharacters(atom->typeString()); break; default: break; } m_hasQuotingInformation = keepQuoting; return atom->next(); } void WebXMLGenerator::startLink(QXmlStreamWriter &writer, const Atom *atom, const Node *node, const QString &link) { QString fullName = link; if (node) fullName = node->fullName(); if (!fullName.isEmpty() && !link.isEmpty()) { writer.writeStartElement("link"); if (atom && !atom->string().isEmpty()) writer.writeAttribute("raw", atom->string()); else writer.writeAttribute("raw", fullName); writer.writeAttribute("href", link); writer.writeAttribute("type", targetType(node)); if (node) { switch (node->nodeType()) { case Node::Enum: writer.writeAttribute("enum", fullName); break; case Node::Example: { const auto *en = static_cast(node); const QString fileTitle = atom ? exampleFileTitle(en, atom->string()) : QString(); if (!fileTitle.isEmpty()) { writer.writeAttribute("page", fileTitle); break; } } Q_FALLTHROUGH(); case Node::Page: writer.writeAttribute("page", fullName); break; case Node::Property: { const auto *propertyNode = static_cast(node); if (!propertyNode->getters().empty()) writer.writeAttribute("getter", propertyNode->getters().at(0)->fullName()); } break; default: break; } } m_inLink = true; } } void WebXMLGenerator::endLink(QXmlStreamWriter &writer) { if (m_inLink) { writer.writeEndElement(); // link m_inLink = false; } } void WebXMLGenerator::generateRelations(QXmlStreamWriter &writer, const Node *node) { if (node && !node->links().empty()) { std::pair anchorPair; const Node *linkNode; for (auto it = node->links().cbegin(); it != node->links().cend(); ++it) { linkNode = m_qdb->findNodeForTarget(it.value().first, node); if (!linkNode) linkNode = node; if (linkNode == node) anchorPair = it.value(); else anchorPair = anchorForNode(linkNode); writer.writeStartElement("relation"); writer.writeAttribute("href", anchorPair.first); writer.writeAttribute("type", targetType(linkNode)); switch (it.key()) { case Node::StartLink: writer.writeAttribute("meta", "start"); break; case Node::NextLink: writer.writeAttribute("meta", "next"); break; case Node::PreviousLink: writer.writeAttribute("meta", "previous"); break; case Node::ContentsLink: writer.writeAttribute("meta", "contents"); break; default: writer.writeAttribute("meta", ""); } writer.writeAttribute("description", anchorPair.second); writer.writeEndElement(); // link } } } void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, const NodeMap &nodeMap) { generateAnnotatedList(writer, relative, nodeMap.values()); } void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, const NodeList &nodeList) { writer.writeStartElement("table"); writer.writeAttribute("width", "100%"); for (const auto *node : nodeList) { writer.writeStartElement("row"); writer.writeStartElement("item"); writer.writeStartElement("para"); const QString link = linkForNode(node, relative); startLink(writer, node->doc().body().firstAtom(), node, link); endLink(writer); writer.writeEndElement(); // para writer.writeEndElement(); // item writer.writeStartElement("item"); writer.writeStartElement("para"); writer.writeCharacters(node->doc().briefText().toString()); writer.writeEndElement(); // para writer.writeEndElement(); // item writer.writeEndElement(); // row } writer.writeEndElement(); // table } QString WebXMLGenerator::fileBase(const Node *node) const { return Generator::fileBase(node); } QT_END_NAMESPACE