tools/qdoc3/helpprojectwriter.cpp
changeset 0 1918ee327afb
child 3 41300fa6a67c
equal deleted inserted replaced
-1:000000000000 0:1918ee327afb
       
     1 /****************************************************************************
       
     2 **
       
     3 ** Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
       
     4 ** All rights reserved.
       
     5 ** Contact: Nokia Corporation (qt-info@nokia.com)
       
     6 **
       
     7 ** This file is part of the tools applications of the Qt Toolkit.
       
     8 **
       
     9 ** $QT_BEGIN_LICENSE:LGPL$
       
    10 ** No Commercial Usage
       
    11 ** This file contains pre-release code and may not be distributed.
       
    12 ** You may use this file in accordance with the terms and conditions
       
    13 ** contained in the Technology Preview License Agreement accompanying
       
    14 ** this package.
       
    15 **
       
    16 ** GNU Lesser General Public License Usage
       
    17 ** Alternatively, this file may be used under the terms of the GNU Lesser
       
    18 ** General Public License version 2.1 as published by the Free Software
       
    19 ** Foundation and appearing in the file LICENSE.LGPL included in the
       
    20 ** packaging of this file.  Please review the following information to
       
    21 ** ensure the GNU Lesser General Public License version 2.1 requirements
       
    22 ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
       
    23 **
       
    24 ** In addition, as a special exception, Nokia gives you certain additional
       
    25 ** rights.  These rights are described in the Nokia Qt LGPL Exception
       
    26 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
       
    27 **
       
    28 ** If you have questions regarding the use of this file, please contact
       
    29 ** Nokia at qt-info@nokia.com.
       
    30 **
       
    31 **
       
    32 **
       
    33 **
       
    34 **
       
    35 **
       
    36 **
       
    37 **
       
    38 ** $QT_END_LICENSE$
       
    39 **
       
    40 ****************************************************************************/
       
    41 
       
    42 #include <QtXml>
       
    43 #include <QHash>
       
    44 #include <QMap>
       
    45 
       
    46 #include "atom.h"
       
    47 #include "helpprojectwriter.h"
       
    48 #include "htmlgenerator.h"
       
    49 #include "config.h"
       
    50 #include "node.h"
       
    51 #include "tree.h"
       
    52 
       
    53 QT_BEGIN_NAMESPACE
       
    54 
       
    55 HelpProjectWriter::HelpProjectWriter(const Config &config, const QString &defaultFileName)
       
    56 {
       
    57     // The output directory should already have been checked by the calling
       
    58     // generator.
       
    59     outputDir = config.getString(CONFIG_OUTPUTDIR);
       
    60 
       
    61     QStringList names = config.getStringList(CONFIG_QHP + Config::dot + "projects");
       
    62 
       
    63     foreach (const QString &projectName, names) {
       
    64         HelpProject project;
       
    65         project.name = projectName;
       
    66 
       
    67         QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot;
       
    68         project.helpNamespace = config.getString(prefix + "namespace");
       
    69         project.virtualFolder = config.getString(prefix + "virtualFolder");
       
    70         project.fileName = config.getString(prefix + "file");
       
    71         if (project.fileName.isEmpty())
       
    72             project.fileName = defaultFileName;
       
    73         project.extraFiles = config.getStringSet(prefix + "extraFiles");
       
    74         project.indexTitle = config.getString(prefix + "indexTitle");
       
    75         project.indexRoot = config.getString(prefix + "indexRoot");
       
    76         project.filterAttributes = config.getStringList(prefix + "filterAttributes").toSet();
       
    77         QSet<QString> customFilterNames = config.subVars(prefix + "customFilters");
       
    78         foreach (const QString &filterName, customFilterNames) {
       
    79             QString name = config.getString(prefix + "customFilters" + Config::dot + filterName + Config::dot + "name");
       
    80             QSet<QString> filters = config.getStringList(prefix + "customFilters" + Config::dot + filterName + Config::dot + "filterAttributes").toSet();
       
    81             project.customFilters[name] = filters;
       
    82         }
       
    83         //customFilters = config.defs.
       
    84 
       
    85         foreach (QString name, config.getStringSet(prefix + "excluded"))
       
    86             project.excluded.insert(name.replace("\\", "/"));
       
    87 
       
    88         foreach (const QString &name, config.getStringList(prefix + "subprojects")) {
       
    89             SubProject subproject;
       
    90             QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot;
       
    91             subproject.title = config.getString(subprefix + "title");
       
    92             subproject.indexTitle = config.getString(subprefix + "indexTitle");
       
    93             subproject.sortPages = config.getBool(subprefix + "sortPages");
       
    94             readSelectors(subproject, config.getStringList(subprefix + "selectors"));
       
    95             project.subprojects[name] = subproject;
       
    96         }
       
    97 
       
    98         if (project.subprojects.isEmpty()) {
       
    99             SubProject subproject;
       
   100             readSelectors(subproject, config.getStringList(prefix + "selectors"));
       
   101             project.subprojects[""] = subproject;
       
   102         }
       
   103 
       
   104         projects.append(project);
       
   105     }
       
   106 }
       
   107 
       
   108 void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors)
       
   109 {
       
   110     QHash<QString, Node::Type> typeHash;
       
   111     typeHash["namespace"] = Node::Namespace;
       
   112     typeHash["class"] = Node::Class;
       
   113     typeHash["fake"] = Node::Fake;
       
   114     typeHash["enum"] = Node::Enum;
       
   115     typeHash["typedef"] = Node::Typedef;
       
   116     typeHash["function"] = Node::Function;
       
   117     typeHash["property"] = Node::Property;
       
   118     typeHash["variable"] = Node::Variable;
       
   119     typeHash["target"] = Node::Target;
       
   120 
       
   121     QHash<QString, Node::SubType> subTypeHash;
       
   122     subTypeHash["example"] = Node::Example;
       
   123     subTypeHash["headerfile"] = Node::HeaderFile;
       
   124     subTypeHash["file"] = Node::File;
       
   125     subTypeHash["group"] = Node::Group;
       
   126     subTypeHash["module"] = Node::Module;
       
   127     subTypeHash["page"] = Node::Page;
       
   128     subTypeHash["externalpage"] = Node::ExternalPage;
       
   129 #ifdef QDOC_QML
       
   130     subTypeHash["qmlclass"] = Node::QmlClass;
       
   131 #endif
       
   132 
       
   133     QSet<Node::SubType> allSubTypes = QSet<Node::SubType>::fromList(subTypeHash.values());
       
   134 
       
   135     foreach (const QString &selector, selectors) {
       
   136         QStringList pieces = selector.split(":");
       
   137         if (pieces.size() == 1) {
       
   138             QString lower = selector.toLower();
       
   139             if (typeHash.contains(lower))
       
   140                 subproject.selectors[typeHash[lower]] = allSubTypes;
       
   141         } else if (pieces.size() >= 2) {
       
   142             QString lower = pieces[0].toLower();
       
   143             pieces = pieces[1].split(",");
       
   144             if (typeHash.contains(lower)) {
       
   145                 QSet<Node::SubType> subTypes;
       
   146                 for (int i = 0; i < pieces.size(); ++i) {
       
   147                     QString lower = pieces[i].toLower();
       
   148                     if (subTypeHash.contains(lower))
       
   149                         subTypes.insert(subTypeHash[lower]);
       
   150                 }
       
   151                 subproject.selectors[typeHash[lower]] = subTypes;
       
   152             }
       
   153         }
       
   154     }
       
   155 }
       
   156 
       
   157 void HelpProjectWriter::addExtraFile(const QString &file)
       
   158 {
       
   159     for (int i = 0; i < projects.size(); ++i)
       
   160         projects[i].extraFiles.insert(file);
       
   161 }
       
   162 
       
   163 void HelpProjectWriter::addExtraFiles(const QSet<QString> &files)
       
   164 {
       
   165     for (int i = 0; i < projects.size(); ++i)
       
   166         projects[i].extraFiles.unite(files);
       
   167 }
       
   168 
       
   169 /*
       
   170     Returns a list of strings describing the keyword details for a given node.
       
   171 
       
   172     The first string is the human-readable name to be shown in Assistant.
       
   173     The second string is a unique identifier.
       
   174     The third string is the location of the documentation for the keyword.
       
   175 */
       
   176 QStringList HelpProjectWriter::keywordDetails(const Node *node) const
       
   177 {
       
   178     QStringList details;
       
   179 
       
   180     if (node->parent() && !node->parent()->name().isEmpty()) {
       
   181         // "name"
       
   182         if (node->type() == Node::Enum || node->type() == Node::Typedef)
       
   183             details << node->parent()->name()+"::"+node->name();
       
   184         else
       
   185             details << node->name();
       
   186         // "id"
       
   187         details << node->parent()->name()+"::"+node->name();
       
   188     } else if (node->type() == Node::Fake) {
       
   189         const FakeNode *fake = static_cast<const FakeNode *>(node);
       
   190         details << fake->fullTitle();
       
   191         details << fake->fullTitle();
       
   192     } else {
       
   193         details << node->name();
       
   194         details << node->name();
       
   195     }
       
   196     details << tree->fullDocumentLocation(node);
       
   197 
       
   198     return details;
       
   199 }
       
   200 
       
   201 bool HelpProjectWriter::generateSection(HelpProject &project,
       
   202                         QXmlStreamWriter & /* writer */, const Node *node)
       
   203 {
       
   204     if (!node->url().isEmpty())
       
   205         return false;
       
   206 
       
   207     if (node->access() == Node::Private || node->status() == Node::Internal)
       
   208         return false;
       
   209 
       
   210     if (node->name().isEmpty())
       
   211         return true;
       
   212 
       
   213     QString docPath = node->doc().location().filePath();
       
   214     if (!docPath.isEmpty() && project.excluded.contains(docPath))
       
   215         return false;
       
   216 
       
   217     QString objName;
       
   218     if (node->type() == Node::Fake) {
       
   219         const FakeNode *fake = static_cast<const FakeNode *>(node);
       
   220         objName = fake->fullTitle();
       
   221     } else
       
   222         objName = tree->fullDocumentName(node);
       
   223 
       
   224     // Only add nodes to the set for each subproject if they match a selector.
       
   225     // Those that match will be listed in the table of contents.
       
   226 
       
   227     foreach (const QString &name, project.subprojects.keys()) {
       
   228         SubProject subproject = project.subprojects[name];
       
   229         // No selectors: accept all nodes.
       
   230         if (subproject.selectors.isEmpty())
       
   231             project.subprojects[name].nodes[objName] = node;
       
   232         else if (subproject.selectors.contains(node->type())) {
       
   233             // Accept only the node types in the selectors hash.
       
   234             if (node->type() != Node::Fake)
       
   235                 project.subprojects[name].nodes[objName] = node;
       
   236             else {
       
   237                 // Accept only fake nodes with subtypes contained in the selector's
       
   238                 // mask.
       
   239                 const FakeNode *fakeNode = static_cast<const FakeNode *>(node);
       
   240                 if (subproject.selectors[node->type()].contains(fakeNode->subType()) &&
       
   241                     fakeNode->subType() != Node::ExternalPage &&
       
   242                     !fakeNode->fullTitle().isEmpty())
       
   243 
       
   244                     project.subprojects[name].nodes[objName] = node;
       
   245             }
       
   246         }
       
   247     }
       
   248 
       
   249     switch (node->type()) {
       
   250 
       
   251         case Node::Class:
       
   252             project.keywords.append(keywordDetails(node));
       
   253             project.files.insert(tree->fullDocumentLocation(node));
       
   254             break;
       
   255 
       
   256         case Node::Namespace:
       
   257             project.keywords.append(keywordDetails(node));
       
   258             project.files.insert(tree->fullDocumentLocation(node));
       
   259             break;
       
   260 
       
   261         case Node::Enum:
       
   262             project.keywords.append(keywordDetails(node));
       
   263             {
       
   264                 const EnumNode *enumNode = static_cast<const EnumNode*>(node);
       
   265                 foreach (const EnumItem &item, enumNode->items()) {
       
   266                     QStringList details;
       
   267                     
       
   268                     if (enumNode->itemAccess(item.name()) == Node::Private)
       
   269                         continue;
       
   270 
       
   271                     if (!node->parent()->name().isEmpty()) {
       
   272                         details << node->parent()->name()+"::"+item.name(); // "name"
       
   273                         details << node->parent()->name()+"::"+item.name(); // "id"
       
   274                     } else {
       
   275                         details << item.name(); // "name"
       
   276                         details << item.name(); // "id"
       
   277                     }
       
   278                     details << tree->fullDocumentLocation(node);
       
   279                     project.keywords.append(details);
       
   280                 }
       
   281             }
       
   282             break;
       
   283 
       
   284         case Node::Property:
       
   285             project.keywords.append(keywordDetails(node));
       
   286             break;
       
   287 
       
   288         case Node::Function:
       
   289             {
       
   290                 const FunctionNode *funcNode = static_cast<const FunctionNode *>(node);
       
   291 
       
   292                 // Only insert keywords for non-constructors. Constructors are covered
       
   293                 // by the classes themselves.
       
   294 
       
   295                 if (funcNode->metaness() != FunctionNode::Ctor)
       
   296                     project.keywords.append(keywordDetails(node));
       
   297 
       
   298                 // Insert member status flags into the entries for the parent
       
   299                 // node of the function, or the node it is related to.
       
   300                 // Since parent nodes should have already been inserted into
       
   301                 // the set of files, we only need to ensure that related nodes
       
   302                 // are inserted.
       
   303 
       
   304                 if (node->relates()) {
       
   305                     project.memberStatus[node->relates()].insert(node->status());
       
   306                     project.files.insert(tree->fullDocumentLocation(node->relates()));
       
   307                 } else if (node->parent())
       
   308                     project.memberStatus[node->parent()].insert(node->status());
       
   309             }
       
   310             break;
       
   311 
       
   312         case Node::Typedef:
       
   313             {
       
   314                 const TypedefNode *typedefNode = static_cast<const TypedefNode *>(node);
       
   315                 QStringList typedefDetails = keywordDetails(node);
       
   316                 const EnumNode *enumNode = typedefNode->associatedEnum();
       
   317                 // Use the location of any associated enum node in preference
       
   318                 // to that of the typedef.
       
   319                 if (enumNode)
       
   320                     typedefDetails[2] = tree->fullDocumentLocation(enumNode);
       
   321 
       
   322                 project.keywords.append(typedefDetails);
       
   323             }
       
   324             break;
       
   325 
       
   326         // Fake nodes (such as manual pages) contain subtypes, titles and other
       
   327         // attributes.
       
   328         case Node::Fake: {
       
   329             const FakeNode *fakeNode = static_cast<const FakeNode*>(node);
       
   330             if (fakeNode->subType() != Node::ExternalPage &&
       
   331                 !fakeNode->fullTitle().isEmpty()) {
       
   332 
       
   333                 if (fakeNode->subType() != Node::File) {
       
   334                     if (fakeNode->doc().hasKeywords()) {
       
   335                         foreach (const Atom *keyword, fakeNode->doc().keywords()) {
       
   336                             if (!keyword->string().isEmpty()) {
       
   337                                 QStringList details;
       
   338                                 details << keyword->string()
       
   339                                         << keyword->string()
       
   340                                         << tree->fullDocumentLocation(node) + "#" + Doc::canonicalTitle(keyword->string());
       
   341                                 project.keywords.append(details);
       
   342                             } else
       
   343                                 fakeNode->doc().location().warning(
       
   344                                     tr("Bad keyword in %1").arg(tree->fullDocumentLocation(node))
       
   345                                     );
       
   346                         }
       
   347                     }
       
   348                     project.keywords.append(keywordDetails(node));
       
   349                 }
       
   350 /*
       
   351                 if (fakeNode->doc().hasTableOfContents()) {
       
   352                     foreach (const Atom *item, fakeNode->doc().tableOfContents()) {
       
   353                         QString title = Text::sectionHeading(item).toString();
       
   354                         if (!title.isEmpty()) {
       
   355                             QStringList details;
       
   356                             details << title
       
   357                                     << title
       
   358                                     << tree->fullDocumentLocation(node) + "#" + Doc::canonicalTitle(title);
       
   359                             project.keywords.append(details);
       
   360                         } else
       
   361                             fakeNode->doc().location().warning(
       
   362                                 tr("Bad contents item in %1").arg(tree->fullDocumentLocation(node))
       
   363                                 );
       
   364                     }
       
   365                 }
       
   366 */
       
   367                 project.files.insert(tree->fullDocumentLocation(node));
       
   368             }
       
   369             break;
       
   370             }
       
   371         default:
       
   372             ;
       
   373     }
       
   374 
       
   375     // Add all images referenced in the page to the set of files to include.
       
   376     const Atom *atom = node->doc().body().firstAtom();
       
   377     while (atom) {
       
   378         if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) {
       
   379             // Images are all placed within a single directory regardless of
       
   380             // whether the source images are in a nested directory structure.
       
   381             QStringList pieces = atom->string().split("/");
       
   382             project.files.insert("images/" + pieces.last());
       
   383         }
       
   384         atom = atom->next();
       
   385     }
       
   386 
       
   387     return true;
       
   388 }
       
   389 
       
   390 void HelpProjectWriter::generateSections(HelpProject &project,
       
   391                         QXmlStreamWriter &writer, const Node *node)
       
   392 {
       
   393     if (!generateSection(project, writer, node))
       
   394         return;
       
   395 
       
   396     if (node->isInnerNode()) {
       
   397         const InnerNode *inner = static_cast<const InnerNode *>(node);
       
   398 
       
   399         // Ensure that we don't visit nodes more than once.
       
   400         QMap<QString, const Node*> childMap;
       
   401         foreach (const Node *node, inner->childNodes()) {
       
   402             if (node->access() == Node::Private)
       
   403                 continue;
       
   404             if (node->type() == Node::Fake)
       
   405                 childMap[static_cast<const FakeNode *>(node)->fullTitle()] = node;
       
   406             else {
       
   407                 if (node->type() == Node::Function) {
       
   408                     const FunctionNode *funcNode = static_cast<const FunctionNode *>(node);
       
   409                     if (funcNode->isOverload())
       
   410                         continue;
       
   411                 }
       
   412                 childMap[tree->fullDocumentName(node)] = node;
       
   413             }
       
   414         }
       
   415 
       
   416         foreach (const Node *child, childMap)
       
   417             generateSections(project, writer, child);
       
   418     }
       
   419 }
       
   420 
       
   421 void HelpProjectWriter::generate(const Tree *tre)
       
   422 {
       
   423     this->tree = tre;
       
   424     for (int i = 0; i < projects.size(); ++i)
       
   425         generateProject(projects[i]);
       
   426 }
       
   427 
       
   428 void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer,
       
   429                                   const Node *node)
       
   430 {
       
   431     QString href = tree->fullDocumentLocation(node);
       
   432     QString objName = node->name();
       
   433 
       
   434     switch (node->type()) {
       
   435 
       
   436         case Node::Class:
       
   437             writer.writeStartElement("section");
       
   438             writer.writeAttribute("ref", href);
       
   439             if (node->parent() && !node->parent()->name().isEmpty())
       
   440                 writer.writeAttribute("title", tr("%1::%2 Class Reference").arg(node->parent()->name()).arg(objName));
       
   441             else
       
   442                 writer.writeAttribute("title", tr("%1 Class Reference").arg(objName));
       
   443 
       
   444             // Write subsections for all members, obsolete members and Qt 3
       
   445             // members.
       
   446             if (!project.memberStatus[node].isEmpty()) {
       
   447                 QString membersPath = href.left(href.size()-5) + "-members.html";
       
   448                 writer.writeStartElement("section");
       
   449                 writer.writeAttribute("ref", membersPath);
       
   450                 writer.writeAttribute("title", tr("List of all members"));
       
   451                 writer.writeEndElement(); // section
       
   452                 project.files.insert(membersPath);
       
   453             }
       
   454             if (project.memberStatus[node].contains(Node::Compat)) {
       
   455                 QString compatPath = href.left(href.size()-5) + "-qt3.html";
       
   456                 writer.writeStartElement("section");
       
   457                 writer.writeAttribute("ref", compatPath);
       
   458                 writer.writeAttribute("title", tr("Qt 3 support members"));
       
   459                 writer.writeEndElement(); // section
       
   460                 project.files.insert(compatPath);
       
   461             }
       
   462             if (project.memberStatus[node].contains(Node::Obsolete)) {
       
   463                 QString obsoletePath = href.left(href.size()-5) + "-obsolete.html";
       
   464                 writer.writeStartElement("section");
       
   465                 writer.writeAttribute("ref", obsoletePath);
       
   466                 writer.writeAttribute("title", tr("Obsolete members"));
       
   467                 writer.writeEndElement(); // section
       
   468                 project.files.insert(obsoletePath);
       
   469             }
       
   470 
       
   471             writer.writeEndElement(); // section
       
   472             break;
       
   473 
       
   474         case Node::Namespace:
       
   475             writer.writeStartElement("section");
       
   476             writer.writeAttribute("ref", href);
       
   477             writer.writeAttribute("title", objName);
       
   478             writer.writeEndElement(); // section
       
   479             break;
       
   480 
       
   481         case Node::Fake: {
       
   482             // Fake nodes (such as manual pages) contain subtypes, titles and other
       
   483             // attributes.
       
   484             const FakeNode *fakeNode = static_cast<const FakeNode*>(node);
       
   485 
       
   486             writer.writeStartElement("section");
       
   487             writer.writeAttribute("ref", href);
       
   488             writer.writeAttribute("title", fakeNode->fullTitle());
       
   489             //            qDebug() << "Title:" << fakeNode->fullTitle();
       
   490             
       
   491             if (fakeNode->subType() == Node::HeaderFile) {
       
   492 
       
   493                 // Write subsections for all members, obsolete members and Qt 3
       
   494                 // members.
       
   495                 if (!project.memberStatus[node].isEmpty()) {
       
   496                     QString membersPath = href.left(href.size()-5) + "-members.html";
       
   497                     writer.writeStartElement("section");
       
   498                     writer.writeAttribute("ref", membersPath);
       
   499                     writer.writeAttribute("title", tr("List of all members"));
       
   500                     writer.writeEndElement(); // section
       
   501                     project.files.insert(membersPath);
       
   502                 }
       
   503                 if (project.memberStatus[node].contains(Node::Compat)) {
       
   504                     QString compatPath = href.left(href.size()-5) + "-qt3.html";
       
   505                     writer.writeStartElement("section");
       
   506                     writer.writeAttribute("ref", compatPath);
       
   507                     writer.writeAttribute("title", tr("Qt 3 support members"));
       
   508                     writer.writeEndElement(); // section
       
   509                     project.files.insert(compatPath);
       
   510                 }
       
   511                 if (project.memberStatus[node].contains(Node::Obsolete)) {
       
   512                     QString obsoletePath = href.left(href.size()-5) + "-obsolete.html";
       
   513                     writer.writeStartElement("section");
       
   514                     writer.writeAttribute("ref", obsoletePath);
       
   515                     writer.writeAttribute("title", tr("Obsolete members"));
       
   516                     writer.writeEndElement(); // section
       
   517                     project.files.insert(obsoletePath);
       
   518                 }
       
   519             }
       
   520 
       
   521             writer.writeEndElement(); // section
       
   522             }
       
   523             break;
       
   524         default:
       
   525             ;
       
   526     }
       
   527 }
       
   528 
       
   529 void HelpProjectWriter::generateProject(HelpProject &project)
       
   530 {
       
   531     const Node *rootNode;
       
   532     if (!project.indexRoot.isEmpty())
       
   533         rootNode = tree->findFakeNodeByTitle(project.indexRoot);
       
   534     else
       
   535         rootNode = tree->root();
       
   536 
       
   537     if (!rootNode)
       
   538         return;
       
   539 
       
   540     project.files.clear();
       
   541     project.keywords.clear();
       
   542 
       
   543     QFile file(outputDir + QDir::separator() + project.fileName);
       
   544     if (!file.open(QFile::WriteOnly | QFile::Text))
       
   545         return;
       
   546 
       
   547     QXmlStreamWriter writer(&file);
       
   548     writer.setAutoFormatting(true);
       
   549     writer.writeStartDocument();
       
   550     writer.writeStartElement("QtHelpProject");
       
   551     writer.writeAttribute("version", "1.0");
       
   552 
       
   553     // Write metaData, virtualFolder and namespace elements.
       
   554     writer.writeTextElement("namespace", project.helpNamespace);
       
   555     writer.writeTextElement("virtualFolder", project.virtualFolder);
       
   556 
       
   557     // Write customFilter elements.
       
   558     QHash<QString, QSet<QString> >::ConstIterator it;
       
   559     for (it = project.customFilters.begin(); it != project.customFilters.end(); ++it) {
       
   560         writer.writeStartElement("customFilter");
       
   561         writer.writeAttribute("name", it.key());
       
   562         foreach (const QString &filter, it.value())
       
   563             writer.writeTextElement("filterAttribute", filter);
       
   564         writer.writeEndElement(); // customFilter
       
   565     }
       
   566 
       
   567     // Start the filterSection.
       
   568     writer.writeStartElement("filterSection");
       
   569 
       
   570     // Write filterAttribute elements.
       
   571     foreach (const QString &filterName, project.filterAttributes)
       
   572         writer.writeTextElement("filterAttribute", filterName);
       
   573 
       
   574     writer.writeStartElement("toc");
       
   575     writer.writeStartElement("section");
       
   576     QString indexPath = tree->fullDocumentLocation(tree->findFakeNodeByTitle(project.indexTitle));
       
   577     if (indexPath.isEmpty())
       
   578         indexPath = "index.html";
       
   579     writer.writeAttribute("ref", HtmlGenerator::cleanRef(indexPath));
       
   580     writer.writeAttribute("title", project.indexTitle);
       
   581     project.files.insert(tree->fullDocumentLocation(rootNode));
       
   582 
       
   583     generateSections(project, writer, rootNode);
       
   584 
       
   585     foreach (const QString &name, project.subprojects.keys()) {
       
   586         SubProject subproject = project.subprojects[name];
       
   587 
       
   588         if (!name.isEmpty()) {
       
   589             writer.writeStartElement("section");
       
   590             QString indexPath = tree->fullDocumentLocation(tree->findFakeNodeByTitle(subproject.indexTitle));
       
   591             writer.writeAttribute("ref", HtmlGenerator::cleanRef(indexPath));
       
   592             writer.writeAttribute("title", subproject.title);
       
   593             project.files.insert(indexPath);
       
   594         }
       
   595         if (subproject.sortPages) {
       
   596             QStringList titles = subproject.nodes.keys();
       
   597             titles.sort();
       
   598             foreach (const QString &title, titles)
       
   599                 writeNode(project, writer, subproject.nodes[title]);
       
   600         } else {
       
   601             // Find a contents node and navigate from there, using the NextLink values.
       
   602             foreach (const Node *node, subproject.nodes) {
       
   603                 QString nextTitle = node->links().value(Node::NextLink).first;
       
   604                 if (!nextTitle.isEmpty() &&
       
   605                     node->links().value(Node::ContentsLink).first.isEmpty()) {
       
   606 
       
   607                     FakeNode *nextPage = const_cast<FakeNode *>(tree->findFakeNodeByTitle(nextTitle));
       
   608 
       
   609                     // Write the contents node.
       
   610                     writeNode(project, writer, node);
       
   611 
       
   612                     while (nextPage) {
       
   613                         writeNode(project, writer, nextPage);
       
   614                         nextTitle = nextPage->links().value(Node::NextLink).first;
       
   615                         if(nextTitle.isEmpty())
       
   616                             break;
       
   617                         nextPage = const_cast<FakeNode *>(tree->findFakeNodeByTitle(nextTitle));
       
   618                     }
       
   619                     break;
       
   620                 }
       
   621             }
       
   622         }
       
   623 
       
   624         if (!name.isEmpty())
       
   625             writer.writeEndElement(); // section
       
   626     }
       
   627 
       
   628     writer.writeEndElement(); // section
       
   629     writer.writeEndElement(); // toc
       
   630 
       
   631     writer.writeStartElement("keywords");
       
   632     foreach (const QStringList &details, project.keywords) {
       
   633         writer.writeStartElement("keyword");
       
   634         writer.writeAttribute("name", details[0]);
       
   635         writer.writeAttribute("id", details[1]);
       
   636         writer.writeAttribute("ref", HtmlGenerator::cleanRef(details[2]));
       
   637         writer.writeEndElement(); //keyword
       
   638     }
       
   639     writer.writeEndElement(); // keywords
       
   640 
       
   641     writer.writeStartElement("files");
       
   642     foreach (const QString &usedFile, project.files) {
       
   643         if (!usedFile.isEmpty())
       
   644             writer.writeTextElement("file", usedFile);
       
   645     }
       
   646     foreach (const QString &usedFile, project.extraFiles)
       
   647         writer.writeTextElement("file", usedFile);
       
   648     writer.writeEndElement(); // files
       
   649 
       
   650     writer.writeEndElement(); // filterSection
       
   651     writer.writeEndElement(); // QtHelpProject
       
   652     writer.writeEndDocument();
       
   653     file.close();
       
   654 }
       
   655 
       
   656 QT_END_NAMESPACE