// Copyright (C) 2019 Thibaut Cuvelier // 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 "docbookgenerator.h" #include "access.h" #include "aggregate.h" #include "classnode.h" #include "codemarker.h" #include "collectionnode.h" #include "config.h" #include "enumnode.h" #include "examplenode.h" #include "functionnode.h" #include "generator.h" #include "node.h" #include "propertynode.h" #include "quoter.h" #include "qdocdatabase.h" #include "qmlpropertynode.h" #include "sharedcommentnode.h" #include "typedefnode.h" #include "variablenode.h" #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; static const char dbNamespace[] = "http://docbook.org/ns/docbook"; static const char xlinkNamespace[] = "http://www.w3.org/1999/xlink"; DocBookGenerator::DocBookGenerator(FileResolver& file_resolver) : XmlGenerator(file_resolver) {} inline void DocBookGenerator::newLine() { m_writer->writeCharacters("\n"); } inline void DocBookGenerator::writeRawHtml(const QString &rawCode) { m_writer->writeStartElement(dbNamespace, "programlisting"); m_writer->writeAttribute("role", "raw-html"); m_writer->writeCDATA(rawCode); m_writer->writeEndElement(); // programlisting newLine(); } void DocBookGenerator::writeXmlId(const QString &id) { if (id.isEmpty()) return; m_writer->writeAttribute("xml:id", registerRef(id, true)); } void DocBookGenerator::writeXmlId(const Node *node) { if (!node) return; // Specifically for nodes, do not use the same code path, as refForNode // calls registerRef in all cases. Calling registerRef a second time adds // a character to "disambiguate" the two IDs (the one returned by // refForNode, then the one that is written as xml:id). m_writer->writeAttribute("xml:id", Generator::cleanRef(refForNode(node), true)); } void DocBookGenerator::startSectionBegin(const QString &id) { m_hasSection = true; m_writer->writeStartElement(dbNamespace, "section"); writeXmlId(id); newLine(); m_writer->writeStartElement(dbNamespace, "title"); } void DocBookGenerator::startSectionBegin(const Node *node) { m_writer->writeStartElement(dbNamespace, "section"); writeXmlId(node); newLine(); m_writer->writeStartElement(dbNamespace, "title"); } void DocBookGenerator::startSectionEnd() { m_writer->writeEndElement(); // title newLine(); } void DocBookGenerator::startSection(const QString &id, const QString &title) { startSectionBegin(id); m_writer->writeCharacters(title); startSectionEnd(); } void DocBookGenerator::startSection(const Node *node, const QString &title) { startSectionBegin(node); m_writer->writeCharacters(title); startSectionEnd(); } void DocBookGenerator::startSection(const QString &title) { // No xml:id given: down the calls, "" is interpreted as "no ID". startSection("", title); } void DocBookGenerator::endSection() { m_writer->writeEndElement(); // section newLine(); } void DocBookGenerator::writeAnchor(const QString &id) { if (id.isEmpty()) return; m_writer->writeEmptyElement(dbNamespace, "anchor"); writeXmlId(id); newLine(); } /*! Initializes the DocBook output generator's data structures from the configuration (Config). */ void DocBookGenerator::initializeGenerator() { // Excerpts from HtmlGenerator::initializeGenerator. Generator::initializeGenerator(); m_config = &Config::instance(); m_project = m_config->get(CONFIG_PROJECT).asString(); m_projectDescription = m_config->get(CONFIG_DESCRIPTION).asString(); if (m_projectDescription.isEmpty() && !m_project.isEmpty()) m_projectDescription = m_project + QLatin1String(" Reference Documentation"); m_naturalLanguage = m_config->get(CONFIG_NATURALLANGUAGE).asString(); if (m_naturalLanguage.isEmpty()) m_naturalLanguage = QLatin1String("en"); m_buildVersion = m_config->get(CONFIG_BUILDVERSION).asString(); m_useDocBook52 = m_config->get(CONFIG_DOCBOOKEXTENSIONS).asBool() || m_config->get(format() + Config::dot + "usedocbookextensions").asBool(); } QString DocBookGenerator::format() { return "DocBook"; } /*! Returns "xml" for this subclass of Generator. */ QString DocBookGenerator::fileExtension() const { return "xml"; } /*! Generate the documentation for \a relative. i.e. \a relative is the node that represents the entity where a qdoc comment was found, and \a text represents the qdoc comment. */ bool DocBookGenerator::generateText(const Text &text, const Node *relative) { // From Generator::generateText. if (!text.firstAtom()) return false; int numAtoms = 0; initializeTextOutput(); generateAtomList(text.firstAtom(), relative, true, numAtoms); closeTextSections(); return true; } /*! Generate the text for \a atom relatively to \a relative. \a generate indicates if output to \a writer is expected. The number of generated atoms is returned in the argument \a numAtoms. The returned value is the first atom that was not generated. */ const Atom *DocBookGenerator::generateAtomList(const Atom *atom, const Node *relative, bool generate, int &numAtoms) { Q_ASSERT(m_writer); // From Generator::generateAtomList. while (atom) { switch (atom->type()) { case Atom::FormatIf: { int numAtoms0 = numAtoms; atom = generateAtomList(atom->next(), relative, generate, numAtoms); if (!atom) return nullptr; if (atom->type() == Atom::FormatElse) { ++numAtoms; atom = generateAtomList(atom->next(), relative, false, numAtoms); if (!atom) return nullptr; } if (atom->type() == Atom::FormatEndif) { if (generate && numAtoms0 == numAtoms) { relative->location().warning(QStringLiteral("Output format %1 not handled %2") .arg(format(), outFileName())); Atom unhandledFormatAtom(Atom::UnhandledFormat, format()); generateAtomList(&unhandledFormatAtom, relative, generate, numAtoms); } atom = atom->next(); } } break; case Atom::FormatElse: case Atom::FormatEndif: return atom; default: int n = 1; if (generate) { n += generateAtom(atom, relative); numAtoms += n; } while (n-- > 0) atom = atom->next(); } } return nullptr; } QString removeCodeMarkers(const QString& code) { QString rewritten = code; static const QRegularExpression re("(<@[^>&]*>)|(<\\/@[^&>]*>)"); rewritten.replace(re, ""); return rewritten; } /*! Generate DocBook from an instance of Atom. */ qsizetype DocBookGenerator::generateAtom(const Atom *atom, const Node *relative) { Q_ASSERT(m_writer); // From HtmlGenerator::generateAtom, without warning generation. int idx = 0; int skipAhead = 0; Node::Genus genus = Node::DontCare; switch (atom->type()) { case Atom::AutoLink: // Allow auto-linking to nodes in API reference genus = Node::API; Q_FALLTHROUGH(); case Atom::NavAutoLink: if (!m_inLink && !m_inContents && !m_inSectionHeading) { const Node *node = nullptr; QString link = getAutoLink(atom, relative, &node, genus); if (!link.isEmpty() && node && node->isDeprecated() && relative->parent() != node && !relative->isDeprecated()) { link.clear(); } if (link.isEmpty()) { m_writer->writeCharacters(atom->string()); } else { beginLink(link, node, relative); generateLink(atom); endLink(); } } else { m_writer->writeCharacters(atom->string()); } break; case Atom::BaseName: break; case Atom::BriefLeft: if (!hasBrief(relative)) { skipAhead = skipAtoms(atom, Atom::BriefRight); break; } m_writer->writeStartElement(dbNamespace, "para"); m_inPara = true; rewritePropertyBrief(atom, relative); break; case Atom::BriefRight: if (hasBrief(relative)) { m_writer->writeEndElement(); // para m_inPara = false; newLine(); } break; case Atom::C: // This may at one time have been used to mark up C++ code but it is // now widely used to write teletype text. As a result, text marked // with the \c command is not passed to a code marker. if (m_inTeletype) m_writer->writeCharacters(plainCode(atom->string())); else m_writer->writeTextElement(dbNamespace, "code", plainCode(atom->string())); break; case Atom::CaptionLeft: m_writer->writeStartElement(dbNamespace, "title"); break; case Atom::CaptionRight: endLink(); m_writer->writeEndElement(); // title newLine(); break; case Atom::Qml: m_writer->writeStartElement(dbNamespace, "programlisting"); m_writer->writeAttribute("language", "qml"); m_writer->writeCharacters(removeCodeMarkers(atom->string())); m_writer->writeEndElement(); // programlisting newLine(); break; case Atom::Code: m_writer->writeStartElement(dbNamespace, "programlisting"); m_writer->writeAttribute("language", "cpp"); m_writer->writeCharacters(removeCodeMarkers(atom->string())); m_writer->writeEndElement(); // programlisting newLine(); break; case Atom::CodeBad: m_writer->writeStartElement(dbNamespace, "programlisting"); m_writer->writeAttribute("language", "cpp"); m_writer->writeAttribute("role", "bad"); m_writer->writeCharacters(removeCodeMarkers(atom->string())); m_writer->writeEndElement(); // programlisting newLine(); break; case Atom::DetailsLeft: case Atom::DetailsRight: break; case Atom::DivLeft: case Atom::DivRight: break; case Atom::FootnoteLeft: m_writer->writeStartElement(dbNamespace, "footnote"); newLine(); m_writer->writeStartElement(dbNamespace, "para"); m_inPara = true; break; case Atom::FootnoteRight: m_writer->writeEndElement(); // para m_inPara = false; newLine(); m_writer->writeEndElement(); // footnote break; case Atom::FormatElse: case Atom::FormatEndif: case Atom::FormatIf: break; case Atom::FormattingLeft: if (atom->string() == ATOM_FORMATTING_BOLD) { m_writer->writeStartElement(dbNamespace, "emphasis"); m_writer->writeAttribute("role", "bold"); } else if (atom->string() == ATOM_FORMATTING_ITALIC) { m_writer->writeStartElement(dbNamespace, "emphasis"); } else if (atom->string() == ATOM_FORMATTING_UNDERLINE) { m_writer->writeStartElement(dbNamespace, "emphasis"); m_writer->writeAttribute("role", "underline"); } else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) { m_writer->writeStartElement(dbNamespace, "subscript"); } else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) { m_writer->writeStartElement(dbNamespace, "superscript"); } else if (atom->string() == ATOM_FORMATTING_TELETYPE || atom->string() == ATOM_FORMATTING_PARAMETER) { m_writer->writeStartElement(dbNamespace, "code"); if (atom->string() == ATOM_FORMATTING_PARAMETER) m_writer->writeAttribute("role", "parameter"); else // atom->string() == ATOM_FORMATTING_TELETYPE m_inTeletype = true; // For parameters, understand subscripts. if (atom->string() == ATOM_FORMATTING_PARAMETER) { if (atom->next() != nullptr && atom->next()->type() == Atom::String) { static const QRegularExpression subscriptRegExp("^([a-z]+)_([0-9n])$"); auto match = subscriptRegExp.match(atom->next()->string()); if (match.hasMatch()) { m_writer->writeCharacters(match.captured(1)); m_writer->writeStartElement(dbNamespace, "subscript"); m_writer->writeCharacters(match.captured(2)); m_writer->writeEndElement(); // subscript skipAhead = 1; } } } } else if (atom->string() == ATOM_FORMATTING_UICONTROL) { m_writer->writeStartElement(dbNamespace, "guilabel"); } else { relative->location().warning(QStringLiteral("Unsupported formatting: %1").arg(atom->string())); } break; case Atom::FormattingRight: if (atom->string() == ATOM_FORMATTING_BOLD || atom->string() == ATOM_FORMATTING_ITALIC || atom->string() == ATOM_FORMATTING_UNDERLINE || atom->string() == ATOM_FORMATTING_SUBSCRIPT || atom->string() == ATOM_FORMATTING_SUPERSCRIPT || atom->string() == ATOM_FORMATTING_TELETYPE || atom->string() == ATOM_FORMATTING_PARAMETER || atom->string() == ATOM_FORMATTING_UICONTROL) { m_writer->writeEndElement(); } else if (atom->string() == ATOM_FORMATTING_LINK) { if (atom->string() == ATOM_FORMATTING_TELETYPE) m_inTeletype = false; endLink(); } else { relative->location().warning(QStringLiteral("Unsupported formatting: %1").arg(atom->string())); } break; case Atom::AnnotatedList: if (const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group)) generateList(cn, atom->string()); break; case Atom::GeneratedList: { bool hasGeneratedSomething = false; if (atom->string() == QLatin1String("annotatedclasses") || atom->string() == QLatin1String("attributions") || atom->string() == QLatin1String("namespaces")) { const NodeMultiMap things = atom->string() == QLatin1String("annotatedclasses") ? m_qdb->getCppClasses() : atom->string() == QLatin1String("attributions") ? m_qdb->getAttributions() : m_qdb->getNamespaces(); generateAnnotatedList(relative, things.values(), atom->string()); hasGeneratedSomething = !things.isEmpty(); } else if (atom->string() == QLatin1String("annotatedexamples") || atom->string() == QLatin1String("annotatedattributions")) { const NodeMultiMap things = atom->string() == QLatin1String("annotatedexamples") ? m_qdb->getAttributions() : m_qdb->getExamples(); generateAnnotatedLists(relative, things, atom->string()); hasGeneratedSomething = !things.isEmpty(); } else if (atom->string() == QLatin1String("classes") || atom->string() == QLatin1String("qmlbasictypes") // deprecated! || atom->string() == QLatin1String("qmlvaluetypes") || atom->string() == QLatin1String("qmltypes")) { const NodeMultiMap things = atom->string() == QLatin1String("classes") ? m_qdb->getCppClasses() : (atom->string() == QLatin1String("qmlvaluetypes") || atom->string() == QLatin1String("qmlbasictypes")) ? m_qdb->getQmlValueTypes() : m_qdb->getQmlTypes(); generateCompactList(relative, things, true, QString(), atom->string()); hasGeneratedSomething = !things.isEmpty(); } else if (atom->string().contains("classes ")) { QString rootName = atom->string().mid(atom->string().indexOf("classes") + 7).trimmed(); NodeMultiMap things = m_qdb->getCppClasses(); hasGeneratedSomething = !things.isEmpty(); generateCompactList(relative, things, true, rootName, atom->string()); } else if ((idx = atom->string().indexOf(QStringLiteral("bymodule"))) != -1) { QString moduleName = atom->string().mid(idx + 8).trimmed(); Node::NodeType type = typeFromString(atom); QDocDatabase *qdb = QDocDatabase::qdocDB(); if (const CollectionNode *cn = qdb->getCollectionNode(moduleName, type)) { if (type == Node::Module) { NodeMap m; cn->getMemberClasses(m); if (!m.isEmpty()) generateAnnotatedList(relative, m.values(), atom->string()); hasGeneratedSomething = !m.isEmpty(); } else { generateAnnotatedList(relative, cn->members(), atom->string()); hasGeneratedSomething = !cn->members().isEmpty(); } } } else if (atom->string() == QLatin1String("classhierarchy")) { generateClassHierarchy(relative, m_qdb->getCppClasses()); hasGeneratedSomething = !m_qdb->getCppClasses().isEmpty(); } else if (atom->string().startsWith("obsolete")) { QString prefix = atom->string().contains("cpp") ? QStringLiteral("Q") : QString(); const NodeMultiMap &things = atom->string() == QLatin1String("obsoleteclasses") ? m_qdb->getObsoleteClasses() : atom->string() == QLatin1String("obsoleteqmltypes") ? m_qdb->getObsoleteQmlTypes() : atom->string() == QLatin1String("obsoletecppmembers") ? m_qdb->getClassesWithObsoleteMembers() : m_qdb->getQmlTypesWithObsoleteMembers(); generateCompactList(relative, things, false, prefix, atom->string()); hasGeneratedSomething = !things.isEmpty(); } else if (atom->string() == QLatin1String("functionindex")) { generateFunctionIndex(relative); hasGeneratedSomething = !m_qdb->getFunctionIndex().isEmpty(); } else if (atom->string() == QLatin1String("legalese")) { generateLegaleseList(relative); hasGeneratedSomething = !m_qdb->getLegaleseTexts().isEmpty(); } else if (atom->string() == QLatin1String("overviews") || atom->string() == QLatin1String("cpp-modules") || atom->string() == QLatin1String("qml-modules") || atom->string() == QLatin1String("related")) { generateList(relative, atom->string()); hasGeneratedSomething = true; // Approximation, because there is // some nontrivial logic in generateList. } else if (const auto *cn = m_qdb->getCollectionNode(atom->string(), Node::Group); cn) { generateAnnotatedList(cn, cn->members(), atom->string(), ItemizedList); hasGeneratedSomething = true; // Approximation } // There must still be some content generated for the DocBook document // to be valid (except if already in a paragraph). if (!hasGeneratedSomething && !m_inPara) { m_writer->writeEmptyElement(dbNamespace, "para"); newLine(); } } break; case Atom::SinceList: // Table of contents, should automatically be generated by the DocBook processor. Q_FALLTHROUGH(); case Atom::LineBreak: case Atom::BR: case Atom::HR: // Not supported in DocBook. break; case Atom::Image: // mediaobject // An Image atom is always followed by an ImageText atom, // containing the alternative text. // If no caption is present, we just output a , // avoiding the wrapper as it is not required. // For bordered images, there is another atom before the // caption, DivRight (the corresponding DivLeft being just // before the image). if (atom->next() && matchAhead(atom->next(), Atom::DivRight) && atom->next()->next() && matchAhead(atom->next()->next(), Atom::CaptionLeft)) { // If there is a caption, there must be a // wrapper starting with the caption. Q_ASSERT(atom->next()); Q_ASSERT(atom->next()->next()); Q_ASSERT(atom->next()->next()->next()); Q_ASSERT(atom->next()->next()->next()->next()); Q_ASSERT(atom->next()->next()->next()->next()->next()); m_writer->writeStartElement(dbNamespace, "figure"); newLine(); const Atom *current = atom->next()->next()->next(); skipAhead += 2; Q_ASSERT(current->type() == Atom::CaptionLeft); generateAtom(current, relative); current = current->next(); ++skipAhead; while (current->type() != Atom::CaptionRight) { // The actual caption. generateAtom(current, relative); current = current->next(); ++skipAhead; } Q_ASSERT(current->type() == Atom::CaptionRight); generateAtom(current, relative); current = current->next(); ++skipAhead; m_closeFigureWrapper = true; } if (atom->next() && matchAhead(atom->next(), Atom::CaptionLeft)) { // If there is a caption, there must be a // wrapper starting with the caption. Q_ASSERT(atom->next()); Q_ASSERT(atom->next()->next()); Q_ASSERT(atom->next()->next()->next()); Q_ASSERT(atom->next()->next()->next()->next()); m_writer->writeStartElement(dbNamespace, "figure"); newLine(); const Atom *current = atom->next()->next(); ++skipAhead; Q_ASSERT(current->type() == Atom::CaptionLeft); generateAtom(current, relative); current = current->next(); ++skipAhead; while (current->type() != Atom::CaptionRight) { // The actual caption. generateAtom(current, relative); current = current->next(); ++skipAhead; } Q_ASSERT(current->type() == Atom::CaptionRight); generateAtom(current, relative); current = current->next(); ++skipAhead; m_closeFigureWrapper = true; } Q_FALLTHROUGH(); case Atom::InlineImage: { // inlinemediaobject // TODO: [generator-insufficient-structural-abstraction] // The structure of the computations for this part of the // docbook generation and the same parts in other format // generators is the same. // // The difference, instead, lies in what the generated output // is like. A correct abstraction for a generator would take // this structural equivalence into account and encapsulate it // into a driver for the format generators. // // This would avoid the replication of content, and the // subsequent friction for changes and desynchronization // between generators. // // Review all the generators routines and find the actual // skeleton that is shared between them, then consider it when // extracting the logic for the generation phase. QString tag = atom->type() == Atom::Image ? "mediaobject" : "inlinemediaobject"; m_writer->writeStartElement(dbNamespace, tag); newLine(); auto maybe_resolved_file{file_resolver.resolve(atom->string())}; if (!maybe_resolved_file) { // TODO: [uncetnralized-admonition][failed-resolve-file] relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string())); m_writer->writeStartElement(dbNamespace, "textobject"); newLine(); m_writer->writeStartElement(dbNamespace, "para"); m_writer->writeTextElement(dbNamespace, "emphasis", "[Missing image " + atom->string() + "]"); m_writer->writeEndElement(); // para newLine(); m_writer->writeEndElement(); // textobject newLine(); } 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")); if (atom->next() && !atom->next()->string().isEmpty() && atom->next()->type() == Atom::ImageText) { m_writer->writeTextElement(dbNamespace, "alt", atom->next()->string()); newLine(); } m_writer->writeStartElement(dbNamespace, "imageobject"); newLine(); m_writer->writeEmptyElement(dbNamespace, "imagedata"); // TODO: [uncentralized-output-directory-structure] m_writer->writeAttribute("fileref", "images/" + file_name); newLine(); m_writer->writeEndElement(); // imageobject newLine(); // TODO: [uncentralized-output-directory-structure] setImageFileName(relative, "images/" + file_name); } m_writer->writeEndElement(); // [inline]mediaobject if (atom->type() == Atom::Image) newLine(); if (m_closeFigureWrapper) { m_writer->writeEndElement(); // figure newLine(); m_closeFigureWrapper = false; } } break; case Atom::ImageText: break; case Atom::ImportantLeft: case Atom::NoteLeft: case Atom::WarningLeft: { QString admonType = atom->typeString().toLower(); // Remove 'Left' to get the admonition type admonType.chop(4); m_writer->writeStartElement(dbNamespace, admonType); newLine(); m_writer->writeStartElement(dbNamespace, "para"); m_inPara = true; } break; case Atom::ImportantRight: case Atom::NoteRight: case Atom::WarningRight: m_writer->writeEndElement(); // para m_inPara = false; newLine(); m_writer->writeEndElement(); // note/important newLine(); break; case Atom::LegaleseLeft: case Atom::LegaleseRight: break; case Atom::Link: case Atom::NavLink: { const Node *node = nullptr; QString link = getLink(atom, relative, &node); beginLink(link, node, relative); // Ended at Atom::FormattingRight skipAhead = 1; } break; case Atom::LinkNode: { const Node *node = CodeMarker::nodeForString(atom->string()); beginLink(linkForNode(node, relative), node, relative); skipAhead = 1; } break; case Atom::ListLeft: if (m_inPara) { // The variable m_inPara is not set in a very smart way, because // it ignores nesting. This might in theory create false positives // here. A better solution would be to track the depth of // paragraphs the generator is in, but determining the right check // for this condition is far from trivial (think of nested lists). m_writer->writeEndElement(); // para newLine(); m_inPara = false; } if (atom->string() == ATOM_LIST_BULLET) { m_writer->writeStartElement(dbNamespace, "itemizedlist"); newLine(); } else if (atom->string() == ATOM_LIST_TAG) { m_writer->writeStartElement(dbNamespace, "variablelist"); newLine(); } else if (atom->string() == ATOM_LIST_VALUE) { m_writer->writeStartElement(dbNamespace, "informaltable"); newLine(); m_writer->writeStartElement(dbNamespace, "thead"); newLine(); m_writer->writeStartElement(dbNamespace, "tr"); newLine(); m_writer->writeTextElement(dbNamespace, "th", "Constant"); newLine(); m_threeColumnEnumValueTable = isThreeColumnEnumValueTable(atom); if (m_threeColumnEnumValueTable && relative->nodeType() == Node::Enum) { // With three columns, if not in \enum topic, skip the value column m_writer->writeTextElement(dbNamespace, "th", "Value"); newLine(); } if (!isOneColumnValueTable(atom)) { m_writer->writeTextElement(dbNamespace, "th", "Description"); newLine(); } m_writer->writeEndElement(); // tr newLine(); m_writer->writeEndElement(); // thead newLine(); } else { // No recognized list type. m_writer->writeStartElement(dbNamespace, "orderedlist"); if (atom->next() != nullptr && atom->next()->string().toInt() > 1) m_writer->writeAttribute("startingnumber", atom->next()->string()); if (atom->string() == ATOM_LIST_UPPERALPHA) m_writer->writeAttribute("numeration", "upperalpha"); else if (atom->string() == ATOM_LIST_LOWERALPHA) m_writer->writeAttribute("numeration", "loweralpha"); else if (atom->string() == ATOM_LIST_UPPERROMAN) m_writer->writeAttribute("numeration", "upperroman"); else if (atom->string() == ATOM_LIST_LOWERROMAN) m_writer->writeAttribute("numeration", "lowerroman"); else // (atom->string() == ATOM_LIST_NUMERIC) m_writer->writeAttribute("numeration", "arabic"); newLine(); } m_inList++; break; case Atom::ListItemNumber: break; case Atom::ListTagLeft: if (atom->string() == ATOM_LIST_TAG) { m_writer->writeStartElement(dbNamespace, "varlistentry"); newLine(); m_writer->writeStartElement(dbNamespace, "item"); } else { // (atom->string() == ATOM_LIST_VALUE) std::pair pair = getAtomListValue(atom); skipAhead = pair.second; m_writer->writeStartElement(dbNamespace, "tr"); newLine(); m_writer->writeStartElement(dbNamespace, "td"); newLine(); m_writer->writeStartElement(dbNamespace, "para"); generateEnumValue(pair.first, relative); m_writer->writeEndElement(); // para newLine(); m_writer->writeEndElement(); // td newLine(); if (relative->nodeType() == Node::Enum) { const auto enume = static_cast(relative); QString itemValue = enume->itemValue(atom->next()->string()); m_writer->writeStartElement(dbNamespace, "td"); if (itemValue.isEmpty()) m_writer->writeCharacters("?"); else m_writer->writeTextElement(dbNamespace, "code", itemValue); m_writer->writeEndElement(); // td newLine(); } } m_inList++; break; case Atom::SinceTagRight: if (atom->string() == ATOM_LIST_TAG) { m_writer->writeEndElement(); // item newLine(); } break; case Atom::ListTagRight: if (m_inList > 0 && atom->string() == ATOM_LIST_TAG) { m_writer->writeEndElement(); // item newLine(); m_inList = false; } break; case Atom::ListItemLeft: if (m_inList > 0) { m_inListItemLineOpen = false; if (atom->string() == ATOM_LIST_TAG) { m_writer->writeStartElement(dbNamespace, "listitem"); newLine(); m_writer->writeStartElement(dbNamespace, "para"); m_inPara = true; } else if (atom->string() == ATOM_LIST_VALUE) { if (m_threeColumnEnumValueTable) { if (matchAhead(atom, Atom::ListItemRight)) { m_writer->writeEmptyElement(dbNamespace, "td"); newLine(); m_inListItemLineOpen = false; } else { m_writer->writeStartElement(dbNamespace, "td"); newLine(); m_inListItemLineOpen = true; } } } else { m_writer->writeStartElement(dbNamespace, "listitem"); newLine(); } // Don't skip a paragraph, DocBook requires them within list items. } break; case Atom::ListItemRight: if (m_inList > 0) { if (atom->string() == ATOM_LIST_TAG) { m_writer->writeEndElement(); // para m_inPara = false; newLine(); m_writer->writeEndElement(); // listitem newLine(); m_writer->writeEndElement(); // varlistentry newLine(); } else if (atom->string() == ATOM_LIST_VALUE) { if (m_inListItemLineOpen) { m_writer->writeEndElement(); // td newLine(); m_inListItemLineOpen = false; } m_writer->writeEndElement(); // tr newLine(); } else { m_writer->writeEndElement(); // listitem newLine(); } } break; case Atom::ListRight: // Depending on atom->string(), closing a different item: // - ATOM_LIST_BULLET: itemizedlist // - ATOM_LIST_TAG: variablelist // - ATOM_LIST_VALUE: informaltable // - ATOM_LIST_NUMERIC: orderedlist m_writer->writeEndElement(); newLine(); m_inList--; break; case Atom::Nop: break; case Atom::ParaLeft: m_writer->writeStartElement(dbNamespace, "para"); m_inPara = true; break; case Atom::ParaRight: endLink(); if (m_inPara) { m_writer->writeEndElement(); // para newLine(); m_inPara = false; } break; case Atom::QuotationLeft: m_writer->writeStartElement(dbNamespace, "blockquote"); m_inBlockquote = true; break; case Atom::QuotationRight: m_writer->writeEndElement(); // blockquote newLine(); m_inBlockquote = false; break; case Atom::RawString: { // Many of these transformations are only useful when dealing with // older versions of Qt, with their idiosyncrasies. However, they // also make qdoc hardened against new problematic raw strings. bool hasRewrittenString = false; const QString &str = atom->string().trimmed(); static QHash entitiesMapping; if (entitiesMapping.isEmpty()) { // These mappings come from qtbase\doc\global\macros.qdocconf. entitiesMapping["á"] = "á"; entitiesMapping["Å"] = "Å"; entitiesMapping["å"] = "å"; entitiesMapping["Ä"] = "Ä"; entitiesMapping["©right;"] = "©"; entitiesMapping["é"] = "é"; entitiesMapping["í"] = "í"; entitiesMapping["ø"] = "ø"; entitiesMapping["ö"] = "ö"; entitiesMapping["&rarrow;"] = "→"; entitiesMapping["ü"] = "ü"; entitiesMapping["—"] = "—"; entitiesMapping["Π"] = "Π"; } if (str.startsWith(R"(writeComment(str); } else if (str == "\\sup{*}") { hasRewrittenString = true; m_writer->writeTextElement(dbNamespace, "superscript", "*"); } else if (str.startsWith("") && str.endsWith("")) { hasRewrittenString = true; m_writer->writeTextElement(dbNamespace, "superscript", str.mid(5, str.size() - 5 - 6)); } else if (str.startsWith("
\n\n\n
\n // TODO: No call to file resolver, like the other generators. Would it be required? // auto maybe_resolved_file{file_resolver.resolve(atom->string())}; Q_ASSERT(atom->next()); Q_ASSERT(atom->next()->next()); Q_ASSERT(atom->next()->next()->next()); Q_ASSERT(atom->next()->next()->next()->next()); Q_ASSERT(atom->next()->type() == Atom::String); Q_ASSERT(atom->next()->next()->next()->type() == Atom::String); skipAhead += 4; const QString &videoID = atom->next()->string(); const QString &imageID = atom->next()->next()->next()->string(); m_writer->writeStartElement(dbNamespace, "mediaobject"); newLine(); m_writer->writeStartElement(dbNamespace, "videoobject"); newLine(); m_writer->writeStartElement(dbNamespace, "videodata"); m_writer->writeAttribute("fileref", videoID); newLine(); m_writer->writeEndElement(); // videodata newLine(); m_writer->writeEndElement(); // videoobject newLine(); m_writer->writeStartElement(dbNamespace, "imageobject"); newLine(); m_writer->writeStartElement(dbNamespace, "imagedata"); m_writer->writeAttribute("fileref", "images/" + imageID + ".jpg"); newLine(); m_writer->writeEndElement(); // imagedata newLine(); m_writer->writeEndElement(); // imageobject newLine(); m_writer->writeEndElement(); // mediaobject newLine(); } else if (str.startsWith("= 9) { // : 9 characters. // If qdoc has just closed a section, suppose that the person // writing this RawString knows what they are doing generate a // section. Otherwise, create a bridgehead. bool hasJustClosedASection = !m_writer->device()->isSequential() && m_writer->device()->readAll().trimmed().endsWith(""); // Parse the raw string. If nothing matches, no title is found, // and no rewriting is performed. QChar level = str[2]; QString title {""}; QString id {""}; if (str.startsWith("") && str.endsWith("")) { title = str.mid(4, str.size() - 9); } else if (str.startsWith("")) { // writeStartElement(dbNamespace, "bridgehead"); m_writer->writeAttribute("renderas", "sect" + level); writeXmlId(id); m_writer->writeCharacters(title); m_writer->writeEndElement(); // bridgehead } // If there is an anchor just after with the same ID, skip it. if (matchAhead(atom, Atom::Target) && Doc::canonicalTitle(atom->next()->string()) == id) { ++skipAhead; } } else { // The formatting is not recognized: it starts with a tittle, // then some unknown stuff. It's highly likely some qdoc // example: output that as raw HTML in DocBook too. writeRawHtml(str); hasRewrittenString = true; } } else if (// Formatting of images. str.startsWith(R"(
)") || str.startsWith(R"(
)") || str.simplified().startsWith("
") || str.startsWith(R"(
)") || str.startsWith(R"(
)") || str.startsWith(R"(
)") || str.startsWith("
") || str.startsWith("") || str.simplified().startsWith("
") || str.startsWith(R"(
)") || // Other formatting, only for QMake. str == "
") { // Ignore this part, as it's only for formatting of images. hasRewrittenString = true; } else if (str.startsWith(R"(
next(), Atom::RawString) && matchAhead(atom->next()->next(), Atom::String) && matchAhead(atom->next()->next()->next(), Atom::RawString) && matchAhead(atom->next()->next()->next()->next(), Atom::String) && matchAhead(atom->next()->next()->next()->next()->next(), Atom::RawString)) { hasRewrittenString = true; skipAhead += 6; const QString color = atom->next()->string(); // == atom->next()->next()->next()->string() const QString text = atom->next()->next()->next()->next()->next()->string(); m_writer->writeStartElement(dbNamespace, "phrase"); m_writer->writeAttribute("role", "color:" + color); m_writer->writeCharacters(color); m_writer->writeCharacters(" "); if (text.isEmpty()) m_writer->writeCharacters(text); else m_writer->writeCharacters("          "); m_writer->writeEndElement(); // phrase } // The following two cases handle some specificities of the documentation of Qt Quick // Controls 2. A small subset of pages is involved, as of Qt 6.4.0: // qtquickcontrols2-imagine, qtquickcontrols2-macos, qtquickcontrols2-material, // qtquickcontrols2-universal, qtquickcontrols2-windows. The string to rewrite looks like // the following, with XML comments to indicate the start of atoms: //
Import Statement: // import // QtQuick.Controls.Imagine 2.12 //
// Since: // Qt 5.10 //
// The structure being fixed, a rigid but simple solution is implemented: don't parse the // table, simply output the expected set of tags. An alternative would be to parse the HTML // table and to replicate the tags. The output is identical to // DocBookGenerator::generateQmlRequisites. else if ( str.startsWith( R"(
Import Statement:)") && matchAhead(atom, Atom::String) && matchAhead(atom->next(), Atom::RawString) && matchAhead(atom->next()->next(), Atom::String) && matchAhead(atom->next()->next()->next(), Atom::RawString)) { m_rewritingCustomQmlModuleSummary = true; hasRewrittenString = true; m_writer->writeStartElement(dbNamespace, "variablelist"); newLine(); generateStartRequisite("Import Statement"); m_writer->writeCharacters("import "); } else if (m_rewritingCustomQmlModuleSummary) { if (str.startsWith( R"(
Since:)")) { generateEndRequisite(); generateStartRequisite("Since"); hasRewrittenString = true; } else if (str.startsWith(R"(
)")) { m_rewritingCustomQmlModuleSummary = false; hasRewrittenString = true; generateEndRequisite(); m_writer->writeEndElement(); // variablelist newLine(); } } // Another idiosyncrasy for this module: //

// Universal.accent // : // color //

// Several variants of this template exist, with more than three String between the // RawString. They are defined in // qtdeclarative\src\quickcontrols2\doc\qtquickcontrols.qdocconf. else if ( str.startsWith( R"(
next(), Atom::RawString) && matchAhead(atom->next()->next(), Atom::String) && matchAhead(atom->next()->next()->next(), Atom::RawString)) { hasRewrittenString = true; m_hasSection = true; // Determine which case occurs (property or method). const bool isStyleProperty = atom->next()->next()->string().startsWith( R"(">

)"); const bool isStyleMethod = !isStyleProperty; // atom->next()->next()->string().startsWith(R"(">

)") // Parse the sequence of atoms. const Atom *nextStringAtom = atom->next(); // Invariant: ->type() == Atom::String (except after parsing). const QString id = nextStringAtom->string(); skipAhead += 2; QString name; QString type; QString arg1; QString type1; QString arg2; QString type2; if (isStyleProperty) { nextStringAtom = nextStringAtom->next()->next(); name = nextStringAtom->string(); skipAhead += 2; nextStringAtom = nextStringAtom->next()->next(); type = nextStringAtom->string(); skipAhead += 2; } else if (isStyleMethod) { nextStringAtom = nextStringAtom->next()->next(); type = nextStringAtom->string(); skipAhead += 2; nextStringAtom = nextStringAtom->next()->next(); type = nextStringAtom->string(); skipAhead += 2; nextStringAtom = nextStringAtom->next()->next(); arg1 = nextStringAtom->string(); skipAhead += 2; nextStringAtom = nextStringAtom->next()->next(); type1 = nextStringAtom->string(); skipAhead += 2; if (matchAhead(nextStringAtom, Atom::RawString) && matchAhead(nextStringAtom->next(), Atom::String) && matchAhead(nextStringAtom->next()->next(), Atom::RawString) && matchAhead(nextStringAtom->next()->next()->next(), Atom::String) && matchAhead(nextStringAtom->next()->next()->next()->next(), Atom::RawString)) { nextStringAtom = nextStringAtom->next()->next(); arg2 = nextStringAtom->string(); skipAhead += 2; nextStringAtom = nextStringAtom->next()->next(); type2 = nextStringAtom->string(); skipAhead += 2; } // For now (Qt 6.4.0), the macro is only defined up to two arguments: \stylemethod // and \stylemethod2. } // Write the corresponding DocBook. // This should be wrapped in a section, but there is no mechanism to check for // \endstyleproperty or \endstylemethod within qdoc (it must be done at the macro // level), hence the bridgehead. QString title; if (isStyleProperty) { title = name + " : " + type; } else if (isStyleMethod) { title = type + " " + name; } m_writer->writeStartElement(dbNamespace, "bridgehead"); m_writer->writeAttribute("renderas", "sect2"); writeXmlId(id); m_writer->writeCharacters(title); m_writer->writeEndElement(); // bridgehead newLine(); if (m_useDocBook52) { if (isStyleProperty) { m_writer->writeStartElement(dbNamespace, "fieldsynopsis"); m_writer->writeTextElement(dbNamespace, "type", type); newLine(); m_writer->writeTextElement(dbNamespace, "varname", name); newLine(); m_writer->writeEndElement(); // fieldsynopsis } else if (isStyleMethod) { m_writer->writeStartElement(dbNamespace, "methodsynopsis"); m_writer->writeTextElement(dbNamespace, "type", type); newLine(); m_writer->writeTextElement(dbNamespace, "methodname", name); newLine(); if (!arg1.isEmpty() && !type1.isEmpty()) { m_writer->writeStartElement(dbNamespace, "methodparam"); newLine(); m_writer->writeTextElement(dbNamespace, "type", type1); newLine(); m_writer->writeTextElement(dbNamespace, "parameter", arg1); newLine(); m_writer->writeEndElement(); // methodparam newLine(); } if (!arg2.isEmpty() && !type2.isEmpty()) { m_writer->writeStartElement(dbNamespace, "methodparam"); newLine(); m_writer->writeTextElement(dbNamespace, "type", type2); newLine(); m_writer->writeTextElement(dbNamespace, "parameter", arg2); newLine(); m_writer->writeEndElement(); // methodparam newLine(); } m_writer->writeEndElement(); // methodsynopsis } } } // This time, a specificity of Qt Virtual Keyboard to embed SVG images. Typically, there are // several images at once with the same encoding. else if ( str.startsWith( R"(

", Qt::SkipEmptyParts, Qt::CaseInsensitive); for (const QString& image : images) { // Find the caption. const QStringList parts = image.split(""); const QString svgImage = ""; const QString caption = parts[1].split("
")[1].split("
")[0]; // Output the DocBook equivalent. m_writer->writeStartElement(dbNamespace, "figure"); newLine(); m_writer->writeStartElement(dbNamespace, "title"); m_writer->writeCharacters(caption); m_writer->writeEndElement(); // title newLine(); m_writer->writeStartElement(dbNamespace, "mediaobject"); newLine(); m_writer->writeStartElement(dbNamespace, "imageobject"); newLine(); m_writer->writeStartElement(dbNamespace, "imagedata"); newLine(); m_writer->device()->write(svgImage.toUtf8()); // SVG image as raw XML. m_writer->writeEndElement(); // imagedata newLine(); m_writer->writeEndElement(); // imageobject newLine(); m_writer->writeEndElement(); // mediaobject newLine(); m_writer->writeEndElement(); // figure newLine(); } hasRewrittenString = true; } // This time, a specificity of Qt Virtual Keyboard to embed SVG images. Typically, there are // several images at once with the same encoding. else if ( str.startsWith( R"(
", Qt::SkipEmptyParts, Qt::CaseInsensitive); for (const QString &image : images) { // Find the caption. const QStringList parts = image.split(""); const QString svgImage = ""; const QString caption = parts[1].split("
")[1].split("
")[0]; // Output the DocBook equivalent. m_writer->writeStartElement(dbNamespace, "figure"); newLine(); m_writer->writeStartElement(dbNamespace, "title"); m_writer->writeCharacters(caption); m_writer->writeEndElement(); // title newLine(); m_writer->writeStartElement(dbNamespace, "mediaobject"); newLine(); m_writer->writeStartElement(dbNamespace, "imageobject"); newLine(); m_writer->writeStartElement(dbNamespace, "imagedata"); newLine(); m_writer->device()->write(svgImage.toUtf8()); // SVG image as raw XML. m_writer->writeEndElement(); // imagedata newLine(); m_writer->writeEndElement(); // imageobject newLine(); m_writer->writeEndElement(); // mediaobject newLine(); m_writer->writeEndElement(); // figure newLine(); } hasRewrittenString = true; } // For ActiveQt, there is some raw HTML that has no meaningful // translation into DocBook. else if (str.trimmed().toLower().startsWith(R"(