1
/****************************************************************************
3
** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
4
** Contact: http://www.qt-project.org/legal
6
** This file is part of the tools applications of the Qt Toolkit.
8
** $QT_BEGIN_LICENSE:LGPL$
9
** Commercial License Usage
10
** Licensees holding valid commercial Qt licenses may use this file in
11
** accordance with the commercial license agreement provided with the
12
** Software or, alternatively, in accordance with the terms contained in
13
** a written agreement between you and Digia. For licensing terms and
14
** conditions see http://qt.digia.com/licensing. For further information
15
** use the contact form at http://qt.digia.com/contact-us.
17
** GNU Lesser General Public License Usage
18
** Alternatively, this file may be used under the terms of the GNU Lesser
19
** General Public License version 2.1 as published by the Free Software
20
** Foundation and appearing in the file LICENSE.LGPL included in the
21
** packaging of this file. Please review the following information to
22
** ensure the GNU Lesser General Public License version 2.1 requirements
23
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
25
** In addition, as a special exception, Digia gives you certain additional
26
** rights. These rights are described in the Digia Qt LGPL Exception
27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
29
** GNU General Public License Usage
30
** Alternatively, this file may be used under the terms of the GNU
31
** General Public License version 3.0 as published by the Free Software
32
** Foundation and appearing in the file LICENSE.GPL included in the
33
** packaging of this file. Please review the following information to
34
** ensure the GNU General Public License version 3.0 requirements will be
35
** met: http://www.gnu.org/copyleft/gpl.html.
40
****************************************************************************/
42
#include <qcryptographichash.h>
48
#include "helpprojectwriter.h"
49
#include "htmlgenerator.h"
52
#include "qdocdatabase.h"
57
HelpProjectWriter::HelpProjectWriter(const Config &config,
58
const QString &defaultFileName,
63
Get the pointer to the singleton for the qdoc database and
64
store it locally. This replaces all the local accesses to
65
the node tree, which are now private.
67
qdb_ = QDocDatabase::qdocDB();
69
// The output directory should already have been checked by the calling
71
outputDir = config.getOutputDir();
73
QStringList names = config.getStringList(CONFIG_QHP + Config::dot + "projects");
75
foreach (const QString &projectName, names) {
77
project.name = projectName;
79
QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot;
80
project.helpNamespace = config.getString(prefix + "namespace");
81
project.virtualFolder = config.getString(prefix + "virtualFolder");
82
project.fileName = config.getString(prefix + "file");
83
if (project.fileName.isEmpty())
84
project.fileName = defaultFileName;
85
project.extraFiles = config.getStringSet(prefix + "extraFiles");
86
project.extraFiles += config.getStringSet(CONFIG_QHP + Config::dot + "extraFiles");
87
project.indexTitle = config.getString(prefix + "indexTitle");
88
project.indexRoot = config.getString(prefix + "indexRoot");
89
project.filterAttributes = config.getStringList(prefix + "filterAttributes").toSet();
90
project.includeIndexNodes = config.getBool(prefix + "includeIndexNodes");
91
QSet<QString> customFilterNames = config.subVars(prefix + "customFilters");
92
foreach (const QString &filterName, customFilterNames) {
93
QString name = config.getString(prefix + "customFilters" + Config::dot + filterName + Config::dot + "name");
94
QSet<QString> filters = config.getStringList(prefix + "customFilters" + Config::dot + filterName + Config::dot + "filterAttributes").toSet();
95
project.customFilters[name] = filters;
97
//customFilters = config.defs.
99
foreach (QString name, config.getStringSet(prefix + "excluded"))
100
project.excluded.insert(name.replace(QLatin1Char('\\'), QLatin1Char('/')));
102
foreach (const QString &name, config.getStringList(prefix + "subprojects")) {
103
SubProject subproject;
104
QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot;
105
subproject.title = config.getString(subprefix + "title");
106
subproject.indexTitle = config.getString(subprefix + "indexTitle");
107
subproject.sortPages = config.getBool(subprefix + "sortPages");
108
subproject.type = config.getString(subprefix + "type");
109
readSelectors(subproject, config.getStringList(subprefix + "selectors"));
110
project.subprojects[name] = subproject;
113
if (project.subprojects.isEmpty()) {
114
SubProject subproject;
115
readSelectors(subproject, config.getStringList(prefix + "selectors"));
116
project.subprojects.insert(QString(), subproject);
119
projects.append(project);
123
void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors)
125
QHash<QString, Node::Type> typeHash;
126
typeHash["namespace"] = Node::Namespace;
127
typeHash["class"] = Node::Class;
128
typeHash["fake"] = Node::Document;
129
typeHash["enum"] = Node::Enum;
130
typeHash["typedef"] = Node::Typedef;
131
typeHash["function"] = Node::Function;
132
typeHash["property"] = Node::Property;
133
typeHash["variable"] = Node::Variable;
134
typeHash["qmlproperty"] = Node::QmlProperty;
135
typeHash["qmlsignal"] = Node::QmlSignal;
136
typeHash["qmlsignalhandler"] = Node::QmlSignalHandler;
137
typeHash["qmlmethod"] = Node::QmlMethod;
139
QHash<QString, Node::SubType> subTypeHash;
140
subTypeHash["example"] = Node::Example;
141
subTypeHash["headerfile"] = Node::HeaderFile;
142
subTypeHash["file"] = Node::File;
143
subTypeHash["group"] = Node::Group;
144
subTypeHash["module"] = Node::Module;
145
subTypeHash["page"] = Node::Page;
146
subTypeHash["externalpage"] = Node::ExternalPage;
147
subTypeHash["qmlclass"] = Node::QmlClass;
148
subTypeHash["qmlpropertygroup"] = Node::QmlPropertyGroup;
149
subTypeHash["qmlbasictype"] = Node::QmlBasicType;
151
QSet<Node::SubType> allSubTypes = QSet<Node::SubType>::fromList(subTypeHash.values());
153
foreach (const QString &selector, selectors) {
154
QStringList pieces = selector.split(QLatin1Char(':'));
155
if (pieces.size() == 1) {
156
QString lower = selector.toLower();
157
if (typeHash.contains(lower))
158
subproject.selectors[typeHash[lower]] = allSubTypes;
159
} else if (pieces.size() >= 2) {
160
QString lower = pieces[0].toLower();
161
pieces = pieces[1].split(QLatin1Char(','));
162
if (typeHash.contains(lower)) {
163
QSet<Node::SubType> subTypes;
164
for (int i = 0; i < pieces.size(); ++i) {
165
QString lower = pieces[i].toLower();
166
if (subTypeHash.contains(lower))
167
subTypes.insert(subTypeHash[lower]);
169
subproject.selectors[typeHash[lower]] = subTypes;
175
void HelpProjectWriter::addExtraFile(const QString &file)
177
for (int i = 0; i < projects.size(); ++i)
178
projects[i].extraFiles.insert(file);
181
void HelpProjectWriter::addExtraFiles(const QSet<QString> &files)
183
for (int i = 0; i < projects.size(); ++i)
184
projects[i].extraFiles.unite(files);
188
Returns a list of strings describing the keyword details for a given node.
190
The first string is the human-readable name to be shown in Assistant.
191
The second string is a unique identifier.
192
The third string is the location of the documentation for the keyword.
194
QStringList HelpProjectWriter::keywordDetails(const Node *node) const
198
if (node->type() == Node::QmlProperty) {
200
details << node->name();
202
details << node->parent()->parent()->name()+"::"+node->name();
204
else if (node->parent() && !node->parent()->name().isEmpty()) {
206
if (node->type() == Node::Enum || node->type() == Node::Typedef)
207
details << node->parent()->name()+"::"+node->name();
209
details << node->name();
211
details << node->parent()->name()+"::"+node->name();
213
else if (node->type() == Node::Document) {
214
const DocNode *fake = static_cast<const DocNode *>(node);
215
if (fake->subType() == Node::QmlClass) {
216
details << (QmlClassNode::qmlOnly ? fake->name() : fake->fullTitle());
217
details << "QML." + fake->name();
220
details << fake->fullTitle();
221
details << fake->fullTitle();
225
details << node->name();
226
details << node->name();
228
details << gen_->fullDocumentLocation(node,true);
232
bool HelpProjectWriter::generateSection(HelpProject &project,
233
QXmlStreamWriter & /* writer */,
236
if (!node->url().isEmpty() && !(project.includeIndexNodes && !node->url().startsWith("http")))
239
if (node->access() == Node::Private || node->status() == Node::Internal)
242
if (node->name().isEmpty())
245
QString docPath = node->doc().location().filePath();
246
if (!docPath.isEmpty() && project.excluded.contains(docPath))
250
if (node->type() == Node::Document) {
251
const DocNode *fake = static_cast<const DocNode *>(node);
252
objName = fake->fullTitle();
255
objName = node->fullDocumentName();
257
// Only add nodes to the set for each subproject if they match a selector.
258
// Those that match will be listed in the table of contents.
260
foreach (const QString &name, project.subprojects.keys()) {
261
SubProject subproject = project.subprojects[name];
262
// No selectors: accept all nodes.
263
if (subproject.selectors.isEmpty()) {
264
project.subprojects[name].nodes[objName] = node;
266
else if (subproject.selectors.contains(node->type())) {
267
// Accept only the node types in the selectors hash.
268
if (node->type() != Node::Document)
269
project.subprojects[name].nodes[objName] = node;
271
// Accept only fake nodes with subtypes contained in the selector's
273
const DocNode *docNode = static_cast<const DocNode *>(node);
274
if (subproject.selectors[node->type()].contains(docNode->subType()) &&
275
docNode->subType() != Node::ExternalPage &&
276
!docNode->fullTitle().isEmpty()) {
278
project.subprojects[name].nodes[objName] = node;
284
switch (node->type()) {
287
project.keywords.append(keywordDetails(node));
288
project.files.insert(gen_->fullDocumentLocation(node,true));
291
case Node::Namespace:
292
project.keywords.append(keywordDetails(node));
293
project.files.insert(gen_->fullDocumentLocation(node,true));
297
project.keywords.append(keywordDetails(node));
299
const EnumNode *enumNode = static_cast<const EnumNode*>(node);
300
foreach (const EnumItem &item, enumNode->items()) {
303
if (enumNode->itemAccess(item.name()) == Node::Private)
306
if (!node->parent()->name().isEmpty()) {
307
details << node->parent()->name()+"::"+item.name(); // "name"
308
details << node->parent()->name()+"::"+item.name(); // "id"
310
details << item.name(); // "name"
311
details << item.name(); // "id"
313
details << gen_->fullDocumentLocation(node,true);
314
project.keywords.append(details);
320
case Node::QmlProperty:
321
case Node::QmlSignal:
322
case Node::QmlSignalHandler:
323
case Node::QmlMethod:
324
project.keywords.append(keywordDetails(node));
329
const FunctionNode *funcNode = static_cast<const FunctionNode *>(node);
331
// Only insert keywords for non-constructors. Constructors are covered
332
// by the classes themselves.
334
if (funcNode->metaness() != FunctionNode::Ctor)
335
project.keywords.append(keywordDetails(node));
337
// Insert member status flags into the entries for the parent
338
// node of the function, or the node it is related to.
339
// Since parent nodes should have already been inserted into
340
// the set of files, we only need to ensure that related nodes
343
if (node->relates()) {
344
project.memberStatus[node->relates()].insert(node->status());
345
project.files.insert(gen_->fullDocumentLocation(node->relates(),true));
346
} else if (node->parent())
347
project.memberStatus[node->parent()].insert(node->status());
353
const TypedefNode *typedefNode = static_cast<const TypedefNode *>(node);
354
QStringList typedefDetails = keywordDetails(node);
355
const EnumNode *enumNode = typedefNode->associatedEnum();
356
// Use the location of any associated enum node in preference
357
// to that of the typedef.
359
typedefDetails[2] = gen_->fullDocumentLocation(enumNode,true);
361
project.keywords.append(typedefDetails);
367
QString location = gen_->fullDocumentLocation(node,true);
368
project.files.insert(location.left(location.lastIndexOf(QLatin1Char('#'))));
369
project.keywords.append(keywordDetails(node));
373
// Document nodes (such as manual pages) contain subtypes, titles and other
375
case Node::Document: {
376
const DocNode *docNode = static_cast<const DocNode*>(node);
377
if (docNode->subType() != Node::ExternalPage &&
378
!docNode->fullTitle().isEmpty()) {
380
if (docNode->subType() != Node::File) {
381
if (docNode->doc().hasKeywords()) {
382
foreach (const Atom *keyword, docNode->doc().keywords()) {
383
if (!keyword->string().isEmpty()) {
385
details << keyword->string()
387
<< gen_->fullDocumentLocation(node,true) +
388
QLatin1Char('#') + Doc::canonicalTitle(keyword->string());
389
project.keywords.append(details);
391
docNode->doc().location().warning(
392
tr("Bad keyword in %1").arg(gen_->fullDocumentLocation(node,true))
396
project.keywords.append(keywordDetails(node));
398
project.files.insert(gen_->fullDocumentLocation(node,true));
406
// Add all images referenced in the page to the set of files to include.
407
const Atom *atom = node->doc().body().firstAtom();
409
if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) {
410
// Images are all placed within a single directory regardless of
411
// whether the source images are in a nested directory structure.
412
QStringList pieces = atom->string().split(QLatin1Char('/'));
413
project.files.insert("images/" + pieces.last());
421
void HelpProjectWriter::generateSections(HelpProject &project,
422
QXmlStreamWriter &writer, const Node *node)
424
if (!generateSection(project, writer, node))
427
if (node->isInnerNode()) {
428
const InnerNode *inner = static_cast<const InnerNode *>(node);
430
// Ensure that we don't visit nodes more than once.
431
QMap<QString, const Node*> childMap;
432
foreach (const Node *node, inner->childNodes()) {
433
if (node->access() == Node::Private)
435
if (node->type() == Node::Document) {
437
Don't visit QML property group nodes,
438
but visit their children, which are all
441
if (node->subType() == Node::QmlPropertyGroup) {
442
const InnerNode* inner = static_cast<const InnerNode*>(node);
443
foreach (const Node* n, inner->childNodes()) {
444
if (n->access() == Node::Private)
446
childMap[n->fullDocumentName()] = n;
450
childMap[static_cast<const DocNode *>(node)->fullTitle()] = node;
453
if (node->type() == Node::Function) {
454
const FunctionNode *funcNode = static_cast<const FunctionNode *>(node);
455
if (funcNode->isOverload())
458
childMap[node->fullDocumentName()] = node;
462
foreach (const Node *child, childMap)
463
generateSections(project, writer, child);
467
void HelpProjectWriter::generate()
469
for (int i = 0; i < projects.size(); ++i)
470
generateProject(projects[i]);
473
void HelpProjectWriter::writeHashFile(QFile &file)
475
QCryptographicHash hash(QCryptographicHash::Sha1);
478
QFile hashFile(file.fileName() + ".sha1");
479
if (!hashFile.open(QFile::WriteOnly | QFile::Text))
482
hashFile.write(hash.result().toHex());
486
void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer,
489
QString href = gen_->fullDocumentLocation(node,true);
490
QString objName = node->name();
492
switch (node->type()) {
495
writer.writeStartElement("section");
496
writer.writeAttribute("ref", href);
497
if (node->parent() && !node->parent()->name().isEmpty())
498
writer.writeAttribute("title", tr("%1::%2 Class Reference").arg(node->parent()->name()).arg(objName));
500
writer.writeAttribute("title", tr("%1 Class Reference").arg(objName));
502
// Write subsections for all members, obsolete members and Qt 3
504
if (!project.memberStatus[node].isEmpty()) {
505
QString membersPath = href.left(href.size()-5) + "-members.html";
506
writer.writeStartElement("section");
507
writer.writeAttribute("ref", membersPath);
508
writer.writeAttribute("title", tr("List of all members"));
509
writer.writeEndElement(); // section
510
project.files.insert(membersPath);
512
if (project.memberStatus[node].contains(Node::Compat)) {
513
QString compatPath = href.left(href.size()-5) + "-compat.html";
514
writer.writeStartElement("section");
515
writer.writeAttribute("ref", compatPath);
516
writer.writeAttribute("title", tr("Compatibility members"));
517
writer.writeEndElement(); // section
518
project.files.insert(compatPath);
520
if (project.memberStatus[node].contains(Node::Obsolete)) {
521
QString obsoletePath = href.left(href.size()-5) + "-obsolete.html";
522
writer.writeStartElement("section");
523
writer.writeAttribute("ref", obsoletePath);
524
writer.writeAttribute("title", tr("Obsolete members"));
525
writer.writeEndElement(); // section
526
project.files.insert(obsoletePath);
529
writer.writeEndElement(); // section
532
case Node::Namespace:
533
writer.writeStartElement("section");
534
writer.writeAttribute("ref", href);
535
writer.writeAttribute("title", objName);
536
writer.writeEndElement(); // section
539
case Node::Document: {
540
// Document nodes (such as manual pages) contain subtypes, titles and other
542
const DocNode *docNode = static_cast<const DocNode*>(node);
544
writer.writeStartElement("section");
545
writer.writeAttribute("ref", href);
546
if (docNode->subType() == Node::QmlClass)
547
writer.writeAttribute("title", tr("%1 Type Reference").arg(docNode->fullTitle()));
549
writer.writeAttribute("title", docNode->fullTitle());
551
if ((docNode->subType() == Node::HeaderFile) || (docNode->subType() == Node::QmlClass)) {
552
// Write subsections for all members, obsolete members and Qt 3
554
if (!project.memberStatus[node].isEmpty() || (docNode->subType() == Node::QmlClass)) {
555
QString membersPath = href.left(href.size()-5) + "-members.html";
556
writer.writeStartElement("section");
557
writer.writeAttribute("ref", membersPath);
558
writer.writeAttribute("title", tr("List of all members"));
559
writer.writeEndElement(); // section
560
project.files.insert(membersPath);
562
if (project.memberStatus[node].contains(Node::Compat)) {
563
QString compatPath = href.left(href.size()-5) + "-compat.html";
564
writer.writeStartElement("section");
565
writer.writeAttribute("ref", compatPath);
566
writer.writeAttribute("title", tr("Compatibility members"));
567
writer.writeEndElement(); // section
568
project.files.insert(compatPath);
570
if (project.memberStatus[node].contains(Node::Obsolete)) {
571
QString obsoletePath = href.left(href.size()-5) + "-obsolete.html";
572
writer.writeStartElement("section");
573
writer.writeAttribute("ref", obsoletePath);
574
writer.writeAttribute("title", tr("Obsolete members"));
575
writer.writeEndElement(); // section
576
project.files.insert(obsoletePath);
580
writer.writeEndElement(); // section
588
void HelpProjectWriter::generateProject(HelpProject &project)
590
const Node *rootNode;
591
if (!project.indexRoot.isEmpty())
592
rootNode = qdb_->findDocNodeByTitle(project.indexRoot);
594
rootNode = qdb_->treeRoot();
599
project.files.clear();
600
project.keywords.clear();
602
QFile file(outputDir + QDir::separator() + project.fileName);
603
if (!file.open(QFile::WriteOnly | QFile::Text))
606
QXmlStreamWriter writer(&file);
607
writer.setAutoFormatting(true);
608
writer.writeStartDocument();
609
writer.writeStartElement("QtHelpProject");
610
writer.writeAttribute("version", "1.0");
612
// Write metaData, virtualFolder and namespace elements.
613
writer.writeTextElement("namespace", project.helpNamespace);
614
writer.writeTextElement("virtualFolder", project.virtualFolder);
616
// Write customFilter elements.
617
QHash<QString, QSet<QString> >::ConstIterator it;
618
for (it = project.customFilters.constBegin(); it != project.customFilters.constEnd(); ++it) {
619
writer.writeStartElement("customFilter");
620
writer.writeAttribute("name", it.key());
621
foreach (const QString &filter, it.value())
622
writer.writeTextElement("filterAttribute", filter);
623
writer.writeEndElement(); // customFilter
626
// Start the filterSection.
627
writer.writeStartElement("filterSection");
629
// Write filterAttribute elements.
630
foreach (const QString &filterName, project.filterAttributes)
631
writer.writeTextElement("filterAttribute", filterName);
633
writer.writeStartElement("toc");
634
writer.writeStartElement("section");
635
const Node* node = qdb_->findDocNodeByTitle(project.indexTitle);
637
node = qdb_->findNode(QStringList("index.html"));
640
indexPath = gen_->fullDocumentLocation(node,true);
642
indexPath = "index.html";
643
writer.writeAttribute("ref", indexPath);
644
writer.writeAttribute("title", project.indexTitle);
645
project.files.insert(gen_->fullDocumentLocation(rootNode));
647
generateSections(project, writer, rootNode);
649
foreach (const QString &name, project.subprojects.keys()) {
650
SubProject subproject = project.subprojects[name];
652
if (subproject.type == QLatin1String("manual")) {
654
const DocNode *indexPage = qdb_->findDocNodeByTitle(subproject.indexTitle);
656
Text indexBody = indexPage->doc().body();
657
const Atom *atom = indexBody.firstAtom();
658
QStack<int> sectionStack;
662
switch (atom->type()) {
664
sectionStack.push(0);
666
case Atom::ListRight:
667
if (sectionStack.pop() > 0)
668
writer.writeEndElement(); // section
670
case Atom::ListItemLeft:
673
case Atom::ListItemRight:
678
if (sectionStack.top() > 0)
679
writer.writeEndElement(); // section
681
const DocNode *page = qdb_->findDocNodeByTitle(atom->string());
682
writer.writeStartElement("section");
683
QString indexPath = gen_->fullDocumentLocation(page,true);
684
writer.writeAttribute("ref", indexPath);
685
writer.writeAttribute("title", atom->string());
686
project.files.insert(indexPath);
688
sectionStack.top() += 1;
695
if (atom == indexBody.lastAtom())
700
rootNode->doc().location().warning(
701
tr("Failed to find index: %1").arg(subproject.indexTitle)
706
if (!name.isEmpty()) {
707
writer.writeStartElement("section");
708
QString indexPath = gen_->fullDocumentLocation(qdb_->findDocNodeByTitle(subproject.indexTitle),true);
709
writer.writeAttribute("ref", indexPath);
710
writer.writeAttribute("title", subproject.title);
711
project.files.insert(indexPath);
713
if (subproject.sortPages) {
714
QStringList titles = subproject.nodes.keys();
716
foreach (const QString &title, titles) {
717
writeNode(project, writer, subproject.nodes[title]);
720
// Find a contents node and navigate from there, using the NextLink values.
721
QSet<QString> visited;
723
foreach (const Node *node, subproject.nodes) {
724
QString nextTitle = node->links().value(Node::NextLink).first;
725
if (!nextTitle.isEmpty() &&
726
node->links().value(Node::ContentsLink).first.isEmpty()) {
728
DocNode *nextPage = const_cast<DocNode *>(qdb_->findDocNodeByTitle(nextTitle));
730
// Write the contents node.
731
writeNode(project, writer, node);
734
writeNode(project, writer, nextPage);
735
nextTitle = nextPage->links().value(Node::NextLink).first;
736
if (nextTitle.isEmpty() || visited.contains(nextTitle))
738
nextPage = const_cast<DocNode *>(qdb_->findDocNodeByTitle(nextTitle));
739
visited.insert(nextTitle);
747
writer.writeEndElement(); // section
751
writer.writeEndElement(); // section
752
writer.writeEndElement(); // toc
754
writer.writeStartElement("keywords");
755
foreach (const QStringList &details, project.keywords) {
756
writer.writeStartElement("keyword");
757
writer.writeAttribute("name", details[0]);
758
writer.writeAttribute("id", details[1]);
759
writer.writeAttribute("ref", details[2]);
760
writer.writeEndElement(); //keyword
762
writer.writeEndElement(); // keywords
764
writer.writeStartElement("files");
765
foreach (const QString &usedFile, project.files) {
766
if (!usedFile.isEmpty())
767
writer.writeTextElement("file", usedFile);
769
foreach (const QString &usedFile, project.extraFiles)
770
writer.writeTextElement("file", usedFile);
771
writer.writeEndElement(); // files
773
writer.writeEndElement(); // filterSection
774
writer.writeEndElement(); // QtHelpProject
775
writer.writeEndDocument();