1 /****************************************************************************
3 ** Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
4 ** All rights reserved.
5 ** Contact: Nokia Corporation (qt-info@nokia.com)
7 ** This file is part of the tools applications of the Qt Toolkit.
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
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.
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.
28 ** If you have questions regarding the use of this file, please contact
29 ** Nokia at qt-info@nokia.com.
40 ****************************************************************************/
47 #include "helpprojectwriter.h"
48 #include "htmlgenerator.h"
55 HelpProjectWriter::HelpProjectWriter(const Config
&config
, const QString
&defaultFileName
)
57 // The output directory should already have been checked by the calling
59 outputDir
= config
.getString(CONFIG_OUTPUTDIR
);
61 QStringList names
= config
.getStringList(CONFIG_QHP
+ Config::dot
+ "projects");
63 foreach (const QString
&projectName
, names
) {
65 project
.name
= projectName
;
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
;
83 //customFilters = config.defs.
85 foreach (QString name
, config
.getStringSet(prefix
+ "excluded"))
86 project
.excluded
.insert(name
.replace("\\", "/"));
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
;
98 if (project
.subprojects
.isEmpty()) {
99 SubProject subproject
;
100 readSelectors(subproject
, config
.getStringList(prefix
+ "selectors"));
101 project
.subprojects
[""] = subproject
;
104 projects
.append(project
);
108 void HelpProjectWriter::readSelectors(SubProject
&subproject
, const QStringList
&selectors
)
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
;
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
;
130 subTypeHash
["qmlclass"] = Node::QmlClass
;
133 QSet
<Node::SubType
> allSubTypes
= QSet
<Node::SubType
>::fromList(subTypeHash
.values());
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
]);
151 subproject
.selectors
[typeHash
[lower
]] = subTypes
;
157 void HelpProjectWriter::addExtraFile(const QString
&file
)
159 for (int i
= 0; i
< projects
.size(); ++i
)
160 projects
[i
].extraFiles
.insert(file
);
163 void HelpProjectWriter::addExtraFiles(const QSet
<QString
> &files
)
165 for (int i
= 0; i
< projects
.size(); ++i
)
166 projects
[i
].extraFiles
.unite(files
);
170 Returns a list of strings describing the keyword details for a given node.
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.
176 QStringList
HelpProjectWriter::keywordDetails(const Node
*node
) const
180 if (node
->parent() && !node
->parent()->name().isEmpty()) {
182 if (node
->type() == Node::Enum
|| node
->type() == Node::Typedef
)
183 details
<< node
->parent()->name()+"::"+node
->name();
185 details
<< node
->name();
187 details
<< node
->parent()->name()+"::"+node
->name();
188 } else if (node
->type() == Node::Fake
) {
189 const FakeNode
*fake
= static_cast<const FakeNode
*>(node
);
191 if (fake
->subType() == Node::QmlClass
) {
192 details
<< (QmlClassNode::qmlOnly
? fake
->name() : fake
->fullTitle());
193 details
<< "QML." + fake
->name();
197 details
<< fake
->fullTitle();
198 details
<< fake
->fullTitle();
201 details
<< node
->name();
202 details
<< node
->name();
204 details
<< tree
->fullDocumentLocation(node
);
209 bool HelpProjectWriter::generateSection(HelpProject
&project
,
210 QXmlStreamWriter
& /* writer */, const Node
*node
)
212 if (!node
->url().isEmpty())
215 if (node
->access() == Node::Private
|| node
->status() == Node::Internal
)
218 if (node
->name().isEmpty())
221 QString docPath
= node
->doc().location().filePath();
222 if (!docPath
.isEmpty() && project
.excluded
.contains(docPath
))
226 if (node
->type() == Node::Fake
) {
227 const FakeNode
*fake
= static_cast<const FakeNode
*>(node
);
228 objName
= fake
->fullTitle();
230 objName
= tree
->fullDocumentName(node
);
232 // Only add nodes to the set for each subproject if they match a selector.
233 // Those that match will be listed in the table of contents.
235 foreach (const QString
&name
, project
.subprojects
.keys()) {
236 SubProject subproject
= project
.subprojects
[name
];
237 // No selectors: accept all nodes.
238 if (subproject
.selectors
.isEmpty())
239 project
.subprojects
[name
].nodes
[objName
] = node
;
240 else if (subproject
.selectors
.contains(node
->type())) {
241 // Accept only the node types in the selectors hash.
242 if (node
->type() != Node::Fake
)
243 project
.subprojects
[name
].nodes
[objName
] = node
;
245 // Accept only fake nodes with subtypes contained in the selector's
247 const FakeNode
*fakeNode
= static_cast<const FakeNode
*>(node
);
248 if (subproject
.selectors
[node
->type()].contains(fakeNode
->subType()) &&
249 fakeNode
->subType() != Node::ExternalPage
&&
250 !fakeNode
->fullTitle().isEmpty())
252 project
.subprojects
[name
].nodes
[objName
] = node
;
257 switch (node
->type()) {
260 project
.keywords
.append(keywordDetails(node
));
261 project
.files
.insert(tree
->fullDocumentLocation(node
));
264 case Node::Namespace
:
265 project
.keywords
.append(keywordDetails(node
));
266 project
.files
.insert(tree
->fullDocumentLocation(node
));
270 project
.keywords
.append(keywordDetails(node
));
272 const EnumNode
*enumNode
= static_cast<const EnumNode
*>(node
);
273 foreach (const EnumItem
&item
, enumNode
->items()) {
276 if (enumNode
->itemAccess(item
.name()) == Node::Private
)
279 if (!node
->parent()->name().isEmpty()) {
280 details
<< node
->parent()->name()+"::"+item
.name(); // "name"
281 details
<< node
->parent()->name()+"::"+item
.name(); // "id"
283 details
<< item
.name(); // "name"
284 details
<< item
.name(); // "id"
286 details
<< tree
->fullDocumentLocation(node
);
287 project
.keywords
.append(details
);
293 project
.keywords
.append(keywordDetails(node
));
298 const FunctionNode
*funcNode
= static_cast<const FunctionNode
*>(node
);
300 // Only insert keywords for non-constructors. Constructors are covered
301 // by the classes themselves.
303 if (funcNode
->metaness() != FunctionNode::Ctor
)
304 project
.keywords
.append(keywordDetails(node
));
306 // Insert member status flags into the entries for the parent
307 // node of the function, or the node it is related to.
308 // Since parent nodes should have already been inserted into
309 // the set of files, we only need to ensure that related nodes
312 if (node
->relates()) {
313 project
.memberStatus
[node
->relates()].insert(node
->status());
314 project
.files
.insert(tree
->fullDocumentLocation(node
->relates()));
315 } else if (node
->parent())
316 project
.memberStatus
[node
->parent()].insert(node
->status());
322 const TypedefNode
*typedefNode
= static_cast<const TypedefNode
*>(node
);
323 QStringList typedefDetails
= keywordDetails(node
);
324 const EnumNode
*enumNode
= typedefNode
->associatedEnum();
325 // Use the location of any associated enum node in preference
326 // to that of the typedef.
328 typedefDetails
[2] = tree
->fullDocumentLocation(enumNode
);
330 project
.keywords
.append(typedefDetails
);
334 // Fake nodes (such as manual pages) contain subtypes, titles and other
337 const FakeNode
*fakeNode
= static_cast<const FakeNode
*>(node
);
338 if (fakeNode
->subType() != Node::ExternalPage
&&
339 !fakeNode
->fullTitle().isEmpty()) {
341 if (fakeNode
->subType() != Node::File
) {
342 if (fakeNode
->doc().hasKeywords()) {
343 foreach (const Atom
*keyword
, fakeNode
->doc().keywords()) {
344 if (!keyword
->string().isEmpty()) {
346 details
<< keyword
->string()
348 << tree
->fullDocumentLocation(node
) + "#" + Doc::canonicalTitle(keyword
->string());
349 project
.keywords
.append(details
);
351 fakeNode
->doc().location().warning(
352 tr("Bad keyword in %1").arg(tree
->fullDocumentLocation(node
))
356 project
.keywords
.append(keywordDetails(node
));
359 if (fakeNode->doc().hasTableOfContents()) {
360 foreach (const Atom *item, fakeNode->doc().tableOfContents()) {
361 QString title = Text::sectionHeading(item).toString();
362 if (!title.isEmpty()) {
366 << tree->fullDocumentLocation(node) + "#" + Doc::canonicalTitle(title);
367 project.keywords.append(details);
369 fakeNode->doc().location().warning(
370 tr("Bad contents item in %1").arg(tree->fullDocumentLocation(node))
375 project
.files
.insert(tree
->fullDocumentLocation(node
));
383 // Add all images referenced in the page to the set of files to include.
384 const Atom
*atom
= node
->doc().body().firstAtom();
386 if (atom
->type() == Atom::Image
|| atom
->type() == Atom::InlineImage
) {
387 // Images are all placed within a single directory regardless of
388 // whether the source images are in a nested directory structure.
389 QStringList pieces
= atom
->string().split("/");
390 project
.files
.insert("images/" + pieces
.last());
398 void HelpProjectWriter::generateSections(HelpProject
&project
,
399 QXmlStreamWriter
&writer
, const Node
*node
)
401 if (!generateSection(project
, writer
, node
))
404 if (node
->isInnerNode()) {
405 const InnerNode
*inner
= static_cast<const InnerNode
*>(node
);
407 // Ensure that we don't visit nodes more than once.
408 QMap
<QString
, const Node
*> childMap
;
409 foreach (const Node
*node
, inner
->childNodes()) {
410 if (node
->access() == Node::Private
)
412 if (node
->type() == Node::Fake
)
413 childMap
[static_cast<const FakeNode
*>(node
)->fullTitle()] = node
;
415 if (node
->type() == Node::Function
) {
416 const FunctionNode
*funcNode
= static_cast<const FunctionNode
*>(node
);
417 if (funcNode
->isOverload())
420 childMap
[tree
->fullDocumentName(node
)] = node
;
424 foreach (const Node
*child
, childMap
)
425 generateSections(project
, writer
, child
);
429 void HelpProjectWriter::generate(const Tree
*tre
)
432 for (int i
= 0; i
< projects
.size(); ++i
)
433 generateProject(projects
[i
]);
436 void HelpProjectWriter::writeNode(HelpProject
&project
, QXmlStreamWriter
&writer
,
439 QString href
= tree
->fullDocumentLocation(node
);
440 QString objName
= node
->name();
442 switch (node
->type()) {
445 writer
.writeStartElement("section");
446 writer
.writeAttribute("ref", href
);
447 if (node
->parent() && !node
->parent()->name().isEmpty())
448 writer
.writeAttribute("title", tr("%1::%2 Class Reference").arg(node
->parent()->name()).arg(objName
));
450 writer
.writeAttribute("title", tr("%1 Class Reference").arg(objName
));
452 // Write subsections for all members, obsolete members and Qt 3
454 if (!project
.memberStatus
[node
].isEmpty()) {
455 QString membersPath
= href
.left(href
.size()-5) + "-members.html";
456 writer
.writeStartElement("section");
457 writer
.writeAttribute("ref", membersPath
);
458 writer
.writeAttribute("title", tr("List of all members"));
459 writer
.writeEndElement(); // section
460 project
.files
.insert(membersPath
);
462 if (project
.memberStatus
[node
].contains(Node::Compat
)) {
463 QString compatPath
= href
.left(href
.size()-5) + "-qt3.html";
464 writer
.writeStartElement("section");
465 writer
.writeAttribute("ref", compatPath
);
466 writer
.writeAttribute("title", tr("Qt 3 support members"));
467 writer
.writeEndElement(); // section
468 project
.files
.insert(compatPath
);
470 if (project
.memberStatus
[node
].contains(Node::Obsolete
)) {
471 QString obsoletePath
= href
.left(href
.size()-5) + "-obsolete.html";
472 writer
.writeStartElement("section");
473 writer
.writeAttribute("ref", obsoletePath
);
474 writer
.writeAttribute("title", tr("Obsolete members"));
475 writer
.writeEndElement(); // section
476 project
.files
.insert(obsoletePath
);
479 writer
.writeEndElement(); // section
482 case Node::Namespace
:
483 writer
.writeStartElement("section");
484 writer
.writeAttribute("ref", href
);
485 writer
.writeAttribute("title", objName
);
486 writer
.writeEndElement(); // section
490 // Fake nodes (such as manual pages) contain subtypes, titles and other
492 const FakeNode
*fakeNode
= static_cast<const FakeNode
*>(node
);
494 writer
.writeStartElement("section");
495 writer
.writeAttribute("ref", href
);
496 writer
.writeAttribute("title", fakeNode
->fullTitle());
497 // qDebug() << "Title:" << fakeNode->fullTitle();
499 if (fakeNode
->subType() == Node::HeaderFile
) {
501 // Write subsections for all members, obsolete members and Qt 3
503 if (!project
.memberStatus
[node
].isEmpty()) {
504 QString membersPath
= href
.left(href
.size()-5) + "-members.html";
505 writer
.writeStartElement("section");
506 writer
.writeAttribute("ref", membersPath
);
507 writer
.writeAttribute("title", tr("List of all members"));
508 writer
.writeEndElement(); // section
509 project
.files
.insert(membersPath
);
511 if (project
.memberStatus
[node
].contains(Node::Compat
)) {
512 QString compatPath
= href
.left(href
.size()-5) + "-qt3.html";
513 writer
.writeStartElement("section");
514 writer
.writeAttribute("ref", compatPath
);
515 writer
.writeAttribute("title", tr("Qt 3 support members"));
516 writer
.writeEndElement(); // section
517 project
.files
.insert(compatPath
);
519 if (project
.memberStatus
[node
].contains(Node::Obsolete
)) {
520 QString obsoletePath
= href
.left(href
.size()-5) + "-obsolete.html";
521 writer
.writeStartElement("section");
522 writer
.writeAttribute("ref", obsoletePath
);
523 writer
.writeAttribute("title", tr("Obsolete members"));
524 writer
.writeEndElement(); // section
525 project
.files
.insert(obsoletePath
);
529 writer
.writeEndElement(); // section
537 void HelpProjectWriter::generateProject(HelpProject
&project
)
539 const Node
*rootNode
;
540 if (!project
.indexRoot
.isEmpty())
541 rootNode
= tree
->findFakeNodeByTitle(project
.indexRoot
);
543 rootNode
= tree
->root();
548 project
.files
.clear();
549 project
.keywords
.clear();
551 QFile
file(outputDir
+ QDir::separator() + project
.fileName
);
552 if (!file
.open(QFile::WriteOnly
| QFile::Text
))
555 QXmlStreamWriter
writer(&file
);
556 writer
.setAutoFormatting(true);
557 writer
.writeStartDocument();
558 writer
.writeStartElement("QtHelpProject");
559 writer
.writeAttribute("version", "1.0");
561 // Write metaData, virtualFolder and namespace elements.
562 writer
.writeTextElement("namespace", project
.helpNamespace
);
563 writer
.writeTextElement("virtualFolder", project
.virtualFolder
);
565 // Write customFilter elements.
566 QHash
<QString
, QSet
<QString
> >::ConstIterator it
;
567 for (it
= project
.customFilters
.begin(); it
!= project
.customFilters
.end(); ++it
) {
568 writer
.writeStartElement("customFilter");
569 writer
.writeAttribute("name", it
.key());
570 foreach (const QString
&filter
, it
.value())
571 writer
.writeTextElement("filterAttribute", filter
);
572 writer
.writeEndElement(); // customFilter
575 // Start the filterSection.
576 writer
.writeStartElement("filterSection");
578 // Write filterAttribute elements.
579 foreach (const QString
&filterName
, project
.filterAttributes
)
580 writer
.writeTextElement("filterAttribute", filterName
);
582 writer
.writeStartElement("toc");
583 writer
.writeStartElement("section");
584 QString indexPath
= tree
->fullDocumentLocation(tree
->findFakeNodeByTitle(project
.indexTitle
));
585 if (indexPath
.isEmpty())
586 indexPath
= "index.html";
587 writer
.writeAttribute("ref", HtmlGenerator::cleanRef(indexPath
));
588 writer
.writeAttribute("title", project
.indexTitle
);
589 project
.files
.insert(tree
->fullDocumentLocation(rootNode
));
591 generateSections(project
, writer
, rootNode
);
593 foreach (const QString
&name
, project
.subprojects
.keys()) {
594 SubProject subproject
= project
.subprojects
[name
];
596 if (!name
.isEmpty()) {
597 writer
.writeStartElement("section");
598 QString indexPath
= tree
->fullDocumentLocation(tree
->findFakeNodeByTitle(subproject
.indexTitle
));
599 writer
.writeAttribute("ref", HtmlGenerator::cleanRef(indexPath
));
600 writer
.writeAttribute("title", subproject
.title
);
601 project
.files
.insert(indexPath
);
603 if (subproject
.sortPages
) {
604 QStringList titles
= subproject
.nodes
.keys();
606 foreach (const QString
&title
, titles
)
607 writeNode(project
, writer
, subproject
.nodes
[title
]);
609 // Find a contents node and navigate from there, using the NextLink values.
610 foreach (const Node
*node
, subproject
.nodes
) {
611 QString nextTitle
= node
->links().value(Node::NextLink
).first
;
612 if (!nextTitle
.isEmpty() &&
613 node
->links().value(Node::ContentsLink
).first
.isEmpty()) {
615 FakeNode
*nextPage
= const_cast<FakeNode
*>(tree
->findFakeNodeByTitle(nextTitle
));
617 // Write the contents node.
618 writeNode(project
, writer
, node
);
621 writeNode(project
, writer
, nextPage
);
622 nextTitle
= nextPage
->links().value(Node::NextLink
).first
;
623 if(nextTitle
.isEmpty())
625 nextPage
= const_cast<FakeNode
*>(tree
->findFakeNodeByTitle(nextTitle
));
633 writer
.writeEndElement(); // section
636 writer
.writeEndElement(); // section
637 writer
.writeEndElement(); // toc
639 writer
.writeStartElement("keywords");
640 foreach (const QStringList
&details
, project
.keywords
) {
641 writer
.writeStartElement("keyword");
642 writer
.writeAttribute("name", details
[0]);
643 writer
.writeAttribute("id", details
[1]);
644 writer
.writeAttribute("ref", HtmlGenerator::cleanRef(details
[2]));
645 writer
.writeEndElement(); //keyword
647 writer
.writeEndElement(); // keywords
649 writer
.writeStartElement("files");
650 foreach (const QString
&usedFile
, project
.files
) {
651 if (!usedFile
.isEmpty())
652 writer
.writeTextElement("file", usedFile
);
654 foreach (const QString
&usedFile
, project
.extraFiles
)
655 writer
.writeTextElement("file", usedFile
);
656 writer
.writeEndElement(); // files
658 writer
.writeEndElement(); // filterSection
659 writer
.writeEndElement(); // QtHelpProject
660 writer
.writeEndDocument();