summaryrefslogtreecommitdiff
path: root/src/qdoc/helpprojectwriter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/qdoc/helpprojectwriter.cpp')
-rw-r--r--src/qdoc/helpprojectwriter.cpp858
1 files changed, 858 insertions, 0 deletions
diff --git a/src/qdoc/helpprojectwriter.cpp b/src/qdoc/helpprojectwriter.cpp
new file mode 100644
index 000000000..8161913f1
--- /dev/null
+++ b/src/qdoc/helpprojectwriter.cpp
@@ -0,0 +1,858 @@
+/****************************************************************************
+**
+** Copyright (C) 2015 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <qcryptographichash.h>
+#include <qdebug.h>
+#include <qhash.h>
+#include <qmap.h>
+
+#include "atom.h"
+#include "helpprojectwriter.h"
+#include "htmlgenerator.h"
+#include "config.h"
+#include "node.h"
+#include "qdocdatabase.h"
+#include <qdebug.h>
+
+QT_BEGIN_NAMESPACE
+
+HelpProjectWriter::HelpProjectWriter(const Config &config,
+ const QString &defaultFileName,
+ Generator* g)
+{
+ reset(config, defaultFileName, g);
+}
+
+void HelpProjectWriter::reset(const Config &config,
+ const QString &defaultFileName,
+ Generator* g)
+{
+ projects.clear();
+ gen_ = g;
+ /*
+ Get the pointer to the singleton for the qdoc database and
+ store it locally. This replaces all the local accesses to
+ the node tree, which are now private.
+ */
+ qdb_ = QDocDatabase::qdocDB();
+
+ // The output directory should already have been checked by the calling
+ // generator.
+ outputDir = config.getOutputDir();
+
+ QStringList names = config.getStringList(CONFIG_QHP + Config::dot + "projects");
+
+ foreach (const QString &projectName, names) {
+ HelpProject project;
+ project.name = projectName;
+
+ QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot;
+ project.helpNamespace = config.getString(prefix + "namespace");
+ project.virtualFolder = config.getString(prefix + "virtualFolder");
+ project.fileName = config.getString(prefix + "file");
+ if (project.fileName.isEmpty())
+ project.fileName = defaultFileName;
+ project.extraFiles = config.getStringSet(prefix + "extraFiles");
+ project.extraFiles += config.getStringSet(CONFIG_QHP + Config::dot + "extraFiles");
+ project.indexTitle = config.getString(prefix + "indexTitle");
+ project.indexRoot = config.getString(prefix + "indexRoot");
+ project.filterAttributes = config.getStringList(prefix + "filterAttributes").toSet();
+ project.includeIndexNodes = config.getBool(prefix + "includeIndexNodes");
+ QSet<QString> customFilterNames = config.subVars(prefix + "customFilters");
+ foreach (const QString &filterName, customFilterNames) {
+ QString name = config.getString(prefix + "customFilters" + Config::dot + filterName + Config::dot + "name");
+ QSet<QString> filters = config.getStringList(prefix + "customFilters" + Config::dot + filterName + Config::dot + "filterAttributes").toSet();
+ project.customFilters[name] = filters;
+ }
+ //customFilters = config.defs.
+
+ foreach (QString name, config.getStringSet(prefix + "excluded"))
+ project.excluded.insert(name.replace(QLatin1Char('\\'), QLatin1Char('/')));
+
+ foreach (const QString &name, config.getStringList(prefix + "subprojects")) {
+ SubProject subproject;
+ QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot;
+ subproject.title = config.getString(subprefix + "title");
+ subproject.indexTitle = config.getString(subprefix + "indexTitle");
+ subproject.sortPages = config.getBool(subprefix + "sortPages");
+ subproject.type = config.getString(subprefix + "type");
+ readSelectors(subproject, config.getStringList(subprefix + "selectors"));
+ project.subprojects.append(subproject);
+ }
+
+ if (project.subprojects.isEmpty()) {
+ SubProject subproject;
+ readSelectors(subproject, config.getStringList(prefix + "selectors"));
+ project.subprojects.insert(0, subproject);
+ }
+
+ projects.append(project);
+ }
+}
+
+void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors)
+{
+ QHash<QString, Node::NodeType> typeHash;
+ typeHash["namespace"] = Node::Namespace;
+ typeHash["class"] = Node::Class;
+ typeHash["doc"] = Node::Document;
+ typeHash["fake"] = Node::Document; // Legacy alias for 'doc'
+ typeHash["enum"] = Node::Enum;
+ typeHash["typedef"] = Node::Typedef;
+ typeHash["function"] = Node::Function;
+ typeHash["property"] = Node::Property;
+ typeHash["variable"] = Node::Variable;
+ typeHash["group"] = Node::Group;
+ typeHash["module"] = Node::Module;
+ typeHash["qmlmodule"] = Node::QmlModule;
+ typeHash["qmlproperty"] = Node::QmlProperty;
+ typeHash["qmlsignal"] = Node::QmlSignal;
+ typeHash["qmlsignalhandler"] = Node::QmlSignalHandler;
+ typeHash["qmlmethod"] = Node::QmlMethod;
+ typeHash["qmlpropertygroup"] = Node::QmlPropertyGroup;
+ typeHash["qmlclass"] = Node::QmlType; // Legacy alias for 'qmltype'
+ typeHash["qmltype"] = Node::QmlType;
+ typeHash["qmlbasictype"] = Node::QmlBasicType;
+
+ QHash<QString, Node::DocSubtype> docSubtypeHash;
+ docSubtypeHash["example"] = Node::Example;
+ docSubtypeHash["headerfile"] = Node::HeaderFile;
+ docSubtypeHash["file"] = Node::File;
+ docSubtypeHash["page"] = Node::Page;
+ docSubtypeHash["externalpage"] = Node::ExternalPage;
+
+ QSet<Node::DocSubtype> allSubTypes = QSet<Node::DocSubtype>::fromList(docSubtypeHash.values());
+
+ foreach (const QString &selector, selectors) {
+ QStringList pieces = selector.split(QLatin1Char(':'));
+ if (pieces.size() == 1) {
+ QString lower = selector.toLower();
+ if (typeHash.contains(lower))
+ subproject.selectors[typeHash[lower]] = allSubTypes;
+ } else if (pieces.size() >= 2) {
+ QString docType = pieces[0].toLower();
+ pieces = pieces[1].split(QLatin1Char(','));
+ if (typeHash.contains(docType)) {
+ QSet<Node::DocSubtype> docSubtypes;
+ for (int i = 0; i < pieces.size(); ++i) {
+ QString piece = pieces[i].toLower();
+ if (typeHash[docType] == Node::Group) {
+ subproject.groups << piece;
+ continue;
+ }
+ if (docSubtypeHash.contains(piece))
+ docSubtypes.insert(docSubtypeHash[piece]);
+ }
+ subproject.selectors[typeHash[docType]] = docSubtypes;
+ }
+ }
+ }
+}
+
+void HelpProjectWriter::addExtraFile(const QString &file)
+{
+ for (int i = 0; i < projects.size(); ++i)
+ projects[i].extraFiles.insert(file);
+}
+
+void HelpProjectWriter::addExtraFiles(const QSet<QString> &files)
+{
+ for (int i = 0; i < projects.size(); ++i)
+ projects[i].extraFiles.unite(files);
+}
+
+/*
+ Returns a list of strings describing the keyword details for a given node.
+
+ The first string is the human-readable name to be shown in Assistant.
+ The second string is a unique identifier.
+ The third string is the location of the documentation for the keyword.
+*/
+QStringList HelpProjectWriter::keywordDetails(const Node *node) const
+{
+ QStringList details;
+
+ if (node->parent() && !node->parent()->name().isEmpty()) {
+ // "name"
+ if (node->type() == Node::Enum || node->type() == Node::Typedef)
+ details << node->parent()->name()+"::"+node->name();
+ else
+ details << node->name();
+ // "id"
+ details << node->parent()->name()+"::"+node->name();
+ }
+ else if (node->isQmlType() || node->isQmlBasicType()) {
+ details << node->name();
+ details << "QML." + node->name();
+ }
+ else if (node->isJsType() || node->isJsBasicType()) {
+ details << node->name();
+ details << "JS." + node->name();
+ }
+ else if (node->isDocumentNode()) {
+ const DocumentNode *fake = static_cast<const DocumentNode *>(node);
+ details << fake->fullTitle();
+ details << fake->fullTitle();
+ }
+ else {
+ details << node->name();
+ details << node->name();
+ }
+ details << gen_->fullDocumentLocation(node, false);
+ return details;
+}
+
+bool HelpProjectWriter::generateSection(HelpProject &project,
+ QXmlStreamWriter & /* writer */,
+ const Node *node)
+{
+ if (!node->url().isEmpty() && !(project.includeIndexNodes && !node->url().startsWith("http")))
+ return false;
+
+ if (node->access() == Node::Private || node->status() == Node::Internal)
+ return false;
+
+ if (node->name().isEmpty())
+ return true;
+
+ QString docPath = node->doc().location().filePath();
+ if (!docPath.isEmpty() && project.excluded.contains(docPath))
+ return false;
+
+ QString objName = node->isDocumentNode() ? node->fullTitle() : node->fullDocumentName();
+ // Only add nodes to the set for each subproject if they match a selector.
+ // Those that match will be listed in the table of contents.
+
+ for (int i = 0; i < project.subprojects.length(); i++) {
+ SubProject subproject = project.subprojects[i];
+ // No selectors: accept all nodes.
+ if (subproject.selectors.isEmpty()) {
+ project.subprojects[i].nodes[objName] = node;
+ }
+ else if (subproject.selectors.contains(node->type())) {
+ // Add all group members for 'group:name' selector
+ if (node->isGroup()) {
+ if (project.subprojects[i].groups.contains(node->name())) {
+ const CollectionNode* cn = static_cast<const CollectionNode*>(node);
+ foreach (const Node* m, cn->members()) {
+ QString memberName = m->isDocumentNode()
+ ? m->fullTitle() : m->fullDocumentName();
+ project.subprojects[i].nodes[memberName] = m;
+ }
+ }
+ }
+ // Accept only the node types in the selectors hash.
+ else if (node->type() != Node::Document)
+ project.subprojects[i].nodes[objName] = node;
+ else {
+ // Accept only doc nodes with subtypes contained in the selector's
+ // mask.
+ const DocumentNode *docNode = static_cast<const DocumentNode *>(node);
+ if (subproject.selectors[node->type()].contains(docNode->docSubtype()) &&
+ docNode->docSubtype() != Node::ExternalPage &&
+ !docNode->fullTitle().isEmpty()) {
+
+ project.subprojects[i].nodes[objName] = node;
+ }
+ }
+ }
+ }
+
+ switch (node->type()) {
+
+ case Node::Class:
+ project.keywords.append(keywordDetails(node));
+ break;
+ case Node::QmlType:
+ case Node::QmlBasicType:
+ if (node->doc().hasKeywords()) {
+ foreach (const Atom* keyword, node->doc().keywords()) {
+ if (!keyword->string().isEmpty()) {
+ QStringList details;
+ details << keyword->string()
+ << keyword->string()
+ << gen_->fullDocumentLocation(node, false);
+ project.keywords.append(details);
+ }
+ else
+ node->doc().location().warning(tr("Bad keyword in %1").arg(gen_->fullDocumentLocation(node, false)));
+ }
+ }
+ project.keywords.append(keywordDetails(node));
+ break;
+
+ case Node::Namespace:
+ project.keywords.append(keywordDetails(node));
+ break;
+
+ case Node::Enum:
+ project.keywords.append(keywordDetails(node));
+ {
+ const EnumNode *enumNode = static_cast<const EnumNode*>(node);
+ foreach (const EnumItem &item, enumNode->items()) {
+ QStringList details;
+
+ if (enumNode->itemAccess(item.name()) == Node::Private)
+ continue;
+
+ if (!node->parent()->name().isEmpty()) {
+ details << node->parent()->name()+"::"+item.name(); // "name"
+ details << node->parent()->name()+"::"+item.name(); // "id"
+ } else {
+ details << item.name(); // "name"
+ details << item.name(); // "id"
+ }
+ details << gen_->fullDocumentLocation(node, false);
+ project.keywords.append(details);
+ }
+ }
+ break;
+
+ case Node::Group:
+ case Node::Module:
+ case Node::QmlModule:
+ {
+ const CollectionNode* cn = static_cast<const CollectionNode*>(node);
+ if (!cn->fullTitle().isEmpty()) {
+ if (cn->doc().hasKeywords()) {
+ foreach (const Atom* keyword, cn->doc().keywords()) {
+ if (!keyword->string().isEmpty()) {
+ QStringList details;
+ details << keyword->string()
+ << keyword->string()
+ << gen_->fullDocumentLocation(node, false);
+ project.keywords.append(details);
+ }
+ else
+ cn->doc().location().warning(
+ tr("Bad keyword in %1").arg(gen_->fullDocumentLocation(node, false))
+ );
+ }
+ }
+ project.keywords.append(keywordDetails(node));
+ }
+ }
+ break;
+
+ case Node::Property:
+ case Node::QmlProperty:
+ case Node::QmlSignal:
+ case Node::QmlSignalHandler:
+ case Node::QmlMethod:
+ project.keywords.append(keywordDetails(node));
+ break;
+
+ case Node::Function:
+ {
+ const FunctionNode *funcNode = static_cast<const FunctionNode *>(node);
+
+ // Only insert keywords for non-constructors. Constructors are covered
+ // by the classes themselves.
+
+ if (funcNode->metaness() != FunctionNode::Ctor)
+ project.keywords.append(keywordDetails(node));
+
+ // Insert member status flags into the entries for the parent
+ // node of the function, or the node it is related to.
+ // Since parent nodes should have already been inserted into
+ // the set of files, we only need to ensure that related nodes
+ // are inserted.
+
+ if (node->relates()) {
+ project.memberStatus[node->relates()].insert(node->status());
+ } else if (node->parent())
+ project.memberStatus[node->parent()].insert(node->status());
+ }
+ break;
+
+ case Node::Typedef:
+ {
+ const TypedefNode *typedefNode = static_cast<const TypedefNode *>(node);
+ QStringList typedefDetails = keywordDetails(node);
+ const EnumNode *enumNode = typedefNode->associatedEnum();
+ // Use the location of any associated enum node in preference
+ // to that of the typedef.
+ if (enumNode)
+ typedefDetails[2] = gen_->fullDocumentLocation(enumNode, false);
+
+ project.keywords.append(typedefDetails);
+ }
+ break;
+
+ case Node::Variable:
+ {
+ project.keywords.append(keywordDetails(node));
+ }
+ break;
+
+ // Document nodes (such as manual pages) contain subtypes, titles and other
+ // attributes.
+ case Node::Document: {
+ const DocumentNode *docNode = static_cast<const DocumentNode*>(node);
+ if (docNode->docSubtype() != Node::ExternalPage &&
+ docNode->docSubtype() != Node::Image &&
+ !docNode->fullTitle().isEmpty()) {
+
+ if (docNode->docSubtype() != Node::File) {
+ if (docNode->doc().hasKeywords()) {
+ foreach (const Atom *keyword, docNode->doc().keywords()) {
+ if (!keyword->string().isEmpty()) {
+ QStringList details;
+ details << keyword->string()
+ << keyword->string()
+ << gen_->fullDocumentLocation(node, false);
+ project.keywords.append(details);
+ } else
+ docNode->doc().location().warning(
+ tr("Bad keyword in %1").arg(gen_->fullDocumentLocation(node, false))
+ );
+ }
+ }
+ project.keywords.append(keywordDetails(node));
+ }
+ }
+ break;
+ }
+ default:
+ ;
+ }
+
+ // Add all images referenced in the page to the set of files to include.
+ const Atom *atom = node->doc().body().firstAtom();
+ while (atom) {
+ if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) {
+ // Images are all placed within a single directory regardless of
+ // whether the source images are in a nested directory structure.
+ QStringList pieces = atom->string().split(QLatin1Char('/'));
+ project.files.insert("images/" + pieces.last());
+ }
+ atom = atom->next();
+ }
+
+ return true;
+}
+
+void HelpProjectWriter::generateSections(HelpProject &project,
+ QXmlStreamWriter &writer, const Node *node)
+{
+ /*
+ Don't include index nodes in the help file. Or DITA map nodes.
+ */
+ if (node->isIndexNode() || node->docSubtype() == Node::DitaMap)
+ return;
+ if (!generateSection(project, writer, node))
+ return;
+
+ if (node->isAggregate()) {
+ const Aggregate *inner = static_cast<const Aggregate *>(node);
+
+ // Ensure that we don't visit nodes more than once.
+ QMap<QString, const Node*> childMap;
+ foreach (const Node *childNode, inner->childNodes()) {
+ if (childNode->isIndexNode())
+ continue;
+
+ if (childNode->access() == Node::Private)
+ continue;
+
+ if (childNode->type() == Node::Document) {
+ childMap[static_cast<const DocumentNode *>(childNode)->fullTitle()] = childNode;
+ }
+ else if (childNode->isQmlPropertyGroup() || childNode->isJsPropertyGroup()) {
+ /*
+ Don't visit QML/JS property group nodes,
+ but visit their children, which are all
+ QML/JS property nodes.
+
+ This is probably not correct anymore,
+ because The Qml/Js Property Group is
+ an actual documented thing.
+ */
+ const Aggregate* inner = static_cast<const Aggregate*>(childNode);
+ foreach (const Node* n, inner->childNodes()) {
+ if (n->access() == Node::Private)
+ continue;
+ childMap[n->fullDocumentName()] = n;
+ }
+ }
+ else {
+ // Store member status of children
+ project.memberStatus[node].insert(childNode->status());
+ if (childNode->relates()) {
+ project.memberStatus[childNode->relates()].insert(childNode->status());
+ }
+
+ if (childNode->type() == Node::Function) {
+ const FunctionNode *funcNode = static_cast<const FunctionNode *>(childNode);
+ if (funcNode->isOverload())
+ continue;
+ }
+ childMap[childNode->fullDocumentName()] = childNode;
+ }
+ }
+ foreach (const Node *child, childMap)
+ generateSections(project, writer, child);
+ }
+}
+
+void HelpProjectWriter::generate()
+{
+ for (int i = 0; i < projects.size(); ++i)
+ generateProject(projects[i]);
+}
+
+void HelpProjectWriter::writeHashFile(QFile &file)
+{
+ QCryptographicHash hash(QCryptographicHash::Sha1);
+ hash.addData(&file);
+
+ QFile hashFile(file.fileName() + ".sha1");
+ if (!hashFile.open(QFile::WriteOnly | QFile::Text))
+ return;
+
+ hashFile.write(hash.result().toHex());
+ hashFile.close();
+}
+
+void HelpProjectWriter::writeSection(QXmlStreamWriter &writer, const QString &path,
+ const QString &value)
+{
+ writer.writeStartElement(QStringLiteral("section"));
+ writer.writeAttribute(QStringLiteral("ref"), path);
+ writer.writeAttribute(QStringLiteral("title"), value);
+ writer.writeEndElement(); // section
+}
+
+/*
+ Write subsections for all members, compatibility members and obsolete members.
+*/
+void HelpProjectWriter::addMembers(HelpProject &project, QXmlStreamWriter &writer,
+ const Node *node)
+{
+ QString href = gen_->fullDocumentLocation(node, false);
+ href = href.left(href.size()-5);
+ if (href.isEmpty())
+ return;
+
+ bool derivedClass = false;
+ if (node->type() == Node::Class)
+ derivedClass = !(static_cast<const ClassNode *>(node)->baseClasses().isEmpty());
+
+ // Do not generate a 'List of all members' for namespaces or header files,
+ // but always generate it for derived classes and QML classes
+ if (!node->isNamespace() && !node->isHeaderFile() &&
+ (derivedClass || node->isQmlType() || node->isJsType() ||
+ !project.memberStatus[node].isEmpty())) {
+ QString membersPath = href + QStringLiteral("-members.html");
+ writeSection(writer, membersPath, tr("List of all members"));
+ }
+ if (project.memberStatus[node].contains(Node::Compat)) {
+ QString compatPath = href + QStringLiteral("-compat.html");
+ writeSection(writer, compatPath, tr("Compatibility members"));
+ }
+ if (project.memberStatus[node].contains(Node::Obsolete)) {
+ QString obsoletePath = href + QStringLiteral("-obsolete.html");
+ writeSection(writer, obsoletePath, tr("Obsolete members"));
+ }
+}
+
+void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer,
+ const Node *node)
+{
+ QString href = gen_->fullDocumentLocation(node, false);
+ QString objName = node->name();
+
+ switch (node->type()) {
+
+ case Node::Class:
+ writer.writeStartElement("section");
+ writer.writeAttribute("ref", href);
+ if (node->parent() && !node->parent()->name().isEmpty())
+ writer.writeAttribute("title", tr("%1::%2 Class Reference").arg(node->parent()->name()).arg(objName));
+ else
+ writer.writeAttribute("title", tr("%1 Class Reference").arg(objName));
+
+ addMembers(project, writer, node);
+ writer.writeEndElement(); // section
+ break;
+
+ case Node::Namespace:
+ writeSection(writer, href, objName);
+ break;
+
+ case Node::QmlType:
+ writer.writeStartElement("section");
+ writer.writeAttribute("ref", href);
+ writer.writeAttribute("title", tr("%1 Type Reference").arg(node->fullTitle()));
+ addMembers(project, writer, node);
+ writer.writeEndElement(); // section
+ break;
+
+ case Node::Document: {
+ // Document nodes (such as manual pages) contain subtypes, titles and other
+ // attributes.
+ const DocumentNode *docNode = static_cast<const DocumentNode*>(node);
+
+ writer.writeStartElement("section");
+ writer.writeAttribute("ref", href);
+ writer.writeAttribute("title", docNode->fullTitle());
+
+ if (docNode->docSubtype() == Node::HeaderFile)
+ addMembers(project, writer, node);
+
+ writer.writeEndElement(); // section
+ }
+ break;
+ case Node::Group:
+ case Node::Module:
+ case Node::QmlModule:
+ {
+ const CollectionNode* cn = static_cast<const CollectionNode*>(node);
+ writer.writeStartElement("section");
+ writer.writeAttribute("ref", href);
+ writer.writeAttribute("title", cn->fullTitle());
+ writer.writeEndElement(); // section
+ }
+ break;
+ default:
+ ;
+ }
+}
+
+void HelpProjectWriter::generateProject(HelpProject &project)
+{
+ const Node *rootNode;
+
+ // Restrict searching only to the local (primary) tree
+ QVector<Tree*> searchOrder = qdb_->searchOrder();
+ qdb_->setLocalSearch();
+
+ if (!project.indexRoot.isEmpty())
+ rootNode = qdb_->findDocumentNodeByTitle(project.indexRoot);
+ else
+ rootNode = qdb_->primaryTreeRoot();
+
+ if (!rootNode)
+ return;
+
+ project.files.clear();
+ project.keywords.clear();
+
+ QFile file(outputDir + QDir::separator() + project.fileName);
+ if (!file.open(QFile::WriteOnly | QFile::Text))
+ return;
+
+ QXmlStreamWriter writer(&file);
+ writer.setAutoFormatting(true);
+ writer.writeStartDocument();
+ writer.writeStartElement("QtHelpProject");
+ writer.writeAttribute("version", "1.0");
+
+ // Write metaData, virtualFolder and namespace elements.
+ writer.writeTextElement("namespace", project.helpNamespace);
+ writer.writeTextElement("virtualFolder", project.virtualFolder);
+
+ // Write customFilter elements.
+ QHash<QString, QSet<QString> >::ConstIterator it;
+ for (it = project.customFilters.constBegin(); it != project.customFilters.constEnd(); ++it) {
+ writer.writeStartElement("customFilter");
+ writer.writeAttribute("name", it.key());
+ foreach (const QString &filter, it.value())
+ writer.writeTextElement("filterAttribute", filter);
+ writer.writeEndElement(); // customFilter
+ }
+
+ // Start the filterSection.
+ writer.writeStartElement("filterSection");
+
+ // Write filterAttribute elements.
+ foreach (const QString &filterName, project.filterAttributes)
+ writer.writeTextElement("filterAttribute", filterName);
+
+ writer.writeStartElement("toc");
+ writer.writeStartElement("section");
+ const Node* node = qdb_->findDocumentNodeByTitle(project.indexTitle);
+ if (node == 0)
+ node = qdb_->findNodeByNameAndType(QStringList("index.html"), Node::Document);
+ QString indexPath;
+ if (node)
+ indexPath = gen_->fullDocumentLocation(node, false);
+ else
+ indexPath = "index.html";
+ writer.writeAttribute("ref", indexPath);
+ writer.writeAttribute("title", project.indexTitle);
+
+ generateSections(project, writer, rootNode);
+
+ for (int i = 0; i < project.subprojects.length(); i++) {
+ SubProject subproject = project.subprojects[i];
+
+ if (subproject.type == QLatin1String("manual")) {
+
+ const Node *indexPage = qdb_->findNodeForTarget(subproject.indexTitle, 0);
+ if (indexPage) {
+ Text indexBody = indexPage->doc().body();
+ const Atom *atom = indexBody.firstAtom();
+ QStack<int> sectionStack;
+ bool inItem = false;
+
+ while (atom) {
+ switch (atom->type()) {
+ case Atom::ListLeft:
+ sectionStack.push(0);
+ break;
+ case Atom::ListRight:
+ if (sectionStack.pop() > 0)
+ writer.writeEndElement(); // section
+ break;
+ case Atom::ListItemLeft:
+ inItem = true;
+ break;
+ case Atom::ListItemRight:
+ inItem = false;
+ break;
+ case Atom::Link:
+ if (inItem) {
+ if (sectionStack.top() > 0)
+ writer.writeEndElement(); // section
+
+ const Node *page = qdb_->findNodeForTarget(atom->string(), 0);
+ writer.writeStartElement("section");
+ QString indexPath = gen_->fullDocumentLocation(page, false);
+ writer.writeAttribute("ref", indexPath);
+ writer.writeAttribute("title", atom->string());
+
+ sectionStack.top() += 1;
+ }
+ break;
+ default:
+ ;
+ }
+
+ if (atom == indexBody.lastAtom())
+ break;
+ atom = atom->next();
+ }
+ } else
+ rootNode->doc().location().warning(
+ tr("Failed to find index: %1").arg(subproject.indexTitle)
+ );
+
+ } else {
+
+ writer.writeStartElement("section");
+ QString indexPath = gen_->fullDocumentLocation(qdb_->findNodeForTarget(subproject.indexTitle, 0),
+ false);
+ writer.writeAttribute("ref", indexPath);
+ writer.writeAttribute("title", subproject.title);
+
+ if (subproject.sortPages) {
+ QStringList titles = subproject.nodes.keys();
+ titles.sort();
+ foreach (const QString &title, titles) {
+ writeNode(project, writer, subproject.nodes[title]);
+ }
+ } else {
+ // Find a contents node and navigate from there, using the NextLink values.
+ QSet<QString> visited;
+ bool contentsFound = false;
+ foreach (const Node *node, subproject.nodes) {
+ QString nextTitle = node->links().value(Node::NextLink).first;
+ if (!nextTitle.isEmpty() &&
+ node->links().value(Node::ContentsLink).first.isEmpty()) {
+
+ const Node *nextPage = qdb_->findNodeForTarget(nextTitle, 0);
+
+ // Write the contents node.
+ writeNode(project, writer, node);
+ contentsFound = true;
+
+ while (nextPage) {
+ writeNode(project, writer, nextPage);
+ nextTitle = nextPage->links().value(Node::NextLink).first;
+ if (nextTitle.isEmpty() || visited.contains(nextTitle))
+ break;
+ nextPage = qdb_->findNodeForTarget(nextTitle, 0);
+ visited.insert(nextTitle);
+ }
+ break;
+ }
+ }
+ // No contents/nextpage links found, write all nodes unsorted
+ if (!contentsFound) {
+ foreach (const Node *node, subproject.nodes)
+ writeNode(project, writer, node);
+ }
+ }
+
+ writer.writeEndElement(); // section
+ }
+ }
+
+ // Restore original search order
+ qdb_->setSearchOrder(searchOrder);
+
+ writer.writeEndElement(); // section
+ writer.writeEndElement(); // toc
+
+ writer.writeStartElement("keywords");
+ foreach (const QStringList &details, project.keywords) {
+ writer.writeStartElement("keyword");
+ writer.writeAttribute("name", details[0]);
+ writer.writeAttribute("id", details[1]);
+ writer.writeAttribute("ref", details[2]);
+ writer.writeEndElement(); //keyword
+ }
+ writer.writeEndElement(); // keywords
+
+ writer.writeStartElement("files");
+
+ // The list of files to write is the union of generated files and
+ // other files (images and extras) included in the project
+ QSet<QString> files = QSet<QString>::fromList(gen_->outputFileNames());
+ files.unite(project.files);
+ files.unite(project.extraFiles);
+ foreach (const QString &usedFile, files) {
+ if (!usedFile.isEmpty())
+ writer.writeTextElement("file", usedFile);
+ }
+ writer.writeEndElement(); // files
+
+ writer.writeEndElement(); // filterSection
+ writer.writeEndElement(); // QtHelpProject
+ writer.writeEndDocument();
+ writeHashFile(file);
+ file.close();
+}
+
+QT_END_NAMESPACE