1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "helpprojectwriter.h" |
5 | |
6 | #include "access.h" |
7 | #include "aggregate.h" |
8 | #include "atom.h" |
9 | #include "classnode.h" |
10 | #include "collectionnode.h" |
11 | #include "config.h" |
12 | #include "enumnode.h" |
13 | #include "functionnode.h" |
14 | #include "htmlgenerator.h" |
15 | #include "node.h" |
16 | #include "qdocdatabase.h" |
17 | #include "typedefnode.h" |
18 | |
19 | #include <QtCore/qcryptographichash.h> |
20 | #include <QtCore/qdebug.h> |
21 | #include <QtCore/qhash.h> |
22 | |
23 | QT_BEGIN_NAMESPACE |
24 | |
25 | using namespace Qt::StringLiterals; |
26 | |
27 | HelpProjectWriter::HelpProjectWriter(const QString &defaultFileName, Generator *g) |
28 | { |
29 | reset(defaultFileName, g); |
30 | } |
31 | |
32 | void HelpProjectWriter::reset(const QString &defaultFileName, Generator *g) |
33 | { |
34 | m_projects.clear(); |
35 | m_gen = g; |
36 | /* |
37 | Get the pointer to the singleton for the qdoc database and |
38 | store it locally. This replaces all the local accesses to |
39 | the node tree, which are now private. |
40 | */ |
41 | m_qdb = QDocDatabase::qdocDB(); |
42 | |
43 | // The output directory should already have been checked by the calling |
44 | // generator. |
45 | Config &config = Config::instance(); |
46 | m_outputDir = config.getOutputDir(); |
47 | |
48 | const QStringList names{config.get(CONFIG_QHP + Config::dot + "projects" ).asStringList()}; |
49 | |
50 | for (const auto &projectName : names) { |
51 | HelpProject project; |
52 | project.m_name = projectName; |
53 | |
54 | QString prefix = CONFIG_QHP + Config::dot + projectName + Config::dot; |
55 | project.m_helpNamespace = config.get(var: prefix + "namespace" ).asString(); |
56 | project.m_virtualFolder = config.get(var: prefix + "virtualFolder" ).asString(); |
57 | project.m_version = config.get(CONFIG_VERSION).asString(); |
58 | project.m_fileName = config.get(var: prefix + "file" ).asString(); |
59 | if (project.m_fileName.isEmpty()) |
60 | project.m_fileName = defaultFileName; |
61 | project.m_extraFiles = config.get(var: prefix + "extraFiles" ).asStringSet(); |
62 | project.m_extraFiles += config.get(CONFIG_QHP + Config::dot + "extraFiles" ).asStringSet(); |
63 | project.m_indexTitle = config.get(var: prefix + "indexTitle" ).asString(); |
64 | project.m_indexRoot = config.get(var: prefix + "indexRoot" ).asString(); |
65 | project.m_filterAttributes = config.get(var: prefix + "filterAttributes" ).asStringSet(); |
66 | project.m_includeIndexNodes = config.get(var: prefix + "includeIndexNodes" ).asBool(); |
67 | const QSet<QString> customFilterNames = config.subVars(var: prefix + "customFilters" ); |
68 | for (const auto &filterName : customFilterNames) { |
69 | QString name{config.get(var: prefix + "customFilters" + Config::dot + filterName |
70 | + Config::dot + "name" ).asString()}; |
71 | project.m_customFilters[name] = |
72 | config.get(var: prefix + "customFilters" + Config::dot + filterName |
73 | + Config::dot + "filterAttributes" ).asStringSet(); |
74 | } |
75 | |
76 | const auto excludedPrefixes = config.get(var: prefix + "excluded" ).asStringSet(); |
77 | for (auto name : excludedPrefixes) |
78 | project.m_excluded.insert(value: name.replace(before: QLatin1Char('\\'), after: QLatin1Char('/'))); |
79 | |
80 | const auto subprojectPrefixes{config.get(var: prefix + "subprojects" ).asStringList()}; |
81 | for (const auto &name : subprojectPrefixes) { |
82 | SubProject subproject; |
83 | QString subprefix = prefix + "subprojects" + Config::dot + name + Config::dot; |
84 | subproject.m_title = config.get(var: subprefix + "title" ).asString(); |
85 | if (subproject.m_title.isEmpty()) |
86 | continue; |
87 | subproject.m_indexTitle = config.get(var: subprefix + "indexTitle" ).asString(); |
88 | subproject.m_sortPages = config.get(var: subprefix + "sortPages" ).asBool(); |
89 | subproject.m_type = config.get(var: subprefix + "type" ).asString(); |
90 | readSelectors(subproject, selectors: config.get(var: subprefix + "selectors" ).asStringList()); |
91 | project.m_subprojects.append(t: subproject); |
92 | } |
93 | |
94 | if (project.m_subprojects.isEmpty()) { |
95 | SubProject subproject; |
96 | readSelectors(subproject, selectors: config.get(var: prefix + "selectors" ).asStringList()); |
97 | project.m_subprojects.insert(i: 0, t: subproject); |
98 | } |
99 | |
100 | m_projects.append(t: project); |
101 | } |
102 | } |
103 | |
104 | void HelpProjectWriter::readSelectors(SubProject &subproject, const QStringList &selectors) |
105 | { |
106 | QHash<QString, Node::NodeType> typeHash; |
107 | typeHash["namespace" ] = Node::Namespace; |
108 | typeHash["class" ] = Node::Class; |
109 | typeHash["struct" ] = Node::Struct; |
110 | typeHash["union" ] = Node::Union; |
111 | typeHash["header" ] = Node::HeaderFile; |
112 | typeHash["headerfile" ] = Node::HeaderFile; |
113 | typeHash["doc" ] = Node::Page; // Unused (supported but ignored as a prefix) |
114 | typeHash["fake" ] = Node::Page; // Unused (supported but ignored as a prefix) |
115 | typeHash["page" ] = Node::Page; |
116 | typeHash["enum" ] = Node::Enum; |
117 | typeHash["example" ] = Node::Example; |
118 | typeHash["externalpage" ] = Node::ExternalPage; |
119 | typeHash["typedef" ] = Node::Typedef; |
120 | typeHash["typealias" ] = Node::TypeAlias; |
121 | typeHash["function" ] = Node::Function; |
122 | typeHash["property" ] = Node::Property; |
123 | typeHash["variable" ] = Node::Variable; |
124 | typeHash["group" ] = Node::Group; |
125 | typeHash["module" ] = Node::Module; |
126 | typeHash["qmlmodule" ] = Node::QmlModule; |
127 | typeHash["qmlproperty" ] = Node::QmlProperty; |
128 | typeHash["qmlclass" ] = Node::QmlType; // Legacy alias for 'qmltype' |
129 | typeHash["qmltype" ] = Node::QmlType; |
130 | typeHash["qmlbasictype" ] = Node::QmlValueType; // Legacy alias for 'qmlvaluetype' |
131 | typeHash["qmlvaluetype" ] = Node::QmlValueType; |
132 | |
133 | for (const QString &selector : selectors) { |
134 | QStringList pieces = selector.split(sep: QLatin1Char(':')); |
135 | // Remove doc: or fake: prefix |
136 | if (pieces.size() > 1 && typeHash.value(key: pieces[0].toLower()) == Node::Page) |
137 | pieces.takeFirst(); |
138 | |
139 | QString typeName = pieces.takeFirst().toLower(); |
140 | if (!typeHash.contains(key: typeName)) |
141 | continue; |
142 | |
143 | subproject.m_selectors << typeHash.value(key: typeName); |
144 | if (!pieces.isEmpty()) { |
145 | pieces = pieces[0].split(sep: QLatin1Char(',')); |
146 | for (const auto &piece : std::as_const(t&: pieces)) { |
147 | if (typeHash[typeName] == Node::Group |
148 | || typeHash[typeName] == Node::Module |
149 | || typeHash[typeName] == Node::QmlModule) { |
150 | subproject.m_groups << piece.toLower(); |
151 | } |
152 | } |
153 | } |
154 | } |
155 | } |
156 | |
157 | void HelpProjectWriter::(const QString &file) |
158 | { |
159 | for (HelpProject &project : m_projects) |
160 | project.m_extraFiles.insert(value: file); |
161 | } |
162 | |
163 | Keyword HelpProjectWriter::keywordDetails(const Node *node) const |
164 | { |
165 | QString ref = m_gen->fullDocumentLocation(node, useSubdir: false); |
166 | |
167 | if (node->parent() && !node->parent()->name().isEmpty()) { |
168 | QString name = (node->isEnumType() || node->isTypedef()) |
169 | ? node->parent()->name()+"::" +node->name() |
170 | : node->name(); |
171 | QString id = (!node->isRelatedNonmember()) |
172 | ? node->parent()->name()+"::" +node->name() |
173 | : node->name(); |
174 | return Keyword(name, id, ref); |
175 | } else if (node->isQmlType()) { |
176 | const QString &name = node->name(); |
177 | QString moduleName = node->logicalModuleName(); |
178 | QStringList ids("QML." + name); |
179 | if (!moduleName.isEmpty()) { |
180 | QString majorVersion = node->logicalModule() |
181 | ? node->logicalModule()->logicalModuleVersion().split(sep: '.')[0] |
182 | : QString(); |
183 | ids << "QML." + moduleName + majorVersion + "." + name; |
184 | } |
185 | return Keyword(name, ids, ref); |
186 | } else if (node->isQmlModule()) { |
187 | const QLatin1Char delim('.'); |
188 | QStringList parts = node->logicalModuleName().split(sep: delim) << "QML" ; |
189 | std::reverse(first: parts.begin(), last: parts.end()); |
190 | return Keyword(node->logicalModuleName(), parts.join(sep: delim), ref); |
191 | } else if (node->isTextPageNode()) { |
192 | const auto *pageNode = static_cast<const PageNode *>(node); |
193 | return Keyword(pageNode->fullTitle(), pageNode->fullTitle(), ref); |
194 | } else { |
195 | return Keyword(node->name(), node->name(), ref); |
196 | } |
197 | } |
198 | |
199 | bool HelpProjectWriter::generateSection(HelpProject &project, QXmlStreamWriter & /* writer */, |
200 | const Node *node) |
201 | { |
202 | if (!node->url().isEmpty() && !(project.m_includeIndexNodes && !node->url().startsWith(s: "http" ))) |
203 | return false; |
204 | |
205 | if (node->isPrivate() || node->isInternal() || node->isDontDocument()) |
206 | return false; |
207 | |
208 | if (node->name().isEmpty()) |
209 | return true; |
210 | |
211 | QString docPath = node->doc().location().filePath(); |
212 | if (!docPath.isEmpty() && project.m_excluded.contains(value: docPath)) |
213 | return false; |
214 | |
215 | QString objName = node->isTextPageNode() ? node->fullTitle() : node->fullDocumentName(); |
216 | // Only add nodes to the set for each subproject if they match a selector. |
217 | // Those that match will be listed in the table of contents. |
218 | |
219 | for (int i = 0; i < project.m_subprojects.size(); i++) { |
220 | SubProject subproject = project.m_subprojects[i]; |
221 | // No selectors: accept all nodes. |
222 | if (subproject.m_selectors.isEmpty()) { |
223 | project.m_subprojects[i].m_nodes[objName] = node; |
224 | } else if (subproject.m_selectors.contains(value: node->nodeType())) { |
225 | // Add all group members for '[group|module|qmlmodule]:name' selector |
226 | if (node->isCollectionNode()) { |
227 | if (project.m_subprojects[i].m_groups.contains(str: node->name().toLower())) { |
228 | const auto *cn = static_cast<const CollectionNode *>(node); |
229 | const auto members = cn->members(); |
230 | for (const Node *m : members) { |
231 | if (!m->isInAPI()) |
232 | continue; |
233 | QString memberName = |
234 | m->isTextPageNode() ? m->fullTitle() : m->fullDocumentName(); |
235 | project.m_subprojects[i].m_nodes[memberName] = m; |
236 | } |
237 | continue; |
238 | } else if (!project.m_subprojects[i].m_groups.isEmpty()) { |
239 | continue; // Node does not represent specified group(s) |
240 | } |
241 | } else if (node->isTextPageNode()) { |
242 | if (node->isExternalPage() || node->fullTitle().isEmpty()) |
243 | continue; |
244 | } |
245 | project.m_subprojects[i].m_nodes[objName] = node; |
246 | } |
247 | } |
248 | |
249 | switch (node->nodeType()) { |
250 | |
251 | case Node::Class: |
252 | case Node::Struct: |
253 | case Node::Union: |
254 | project.m_keywords.append(t: keywordDetails(node)); |
255 | break; |
256 | case Node::QmlType: |
257 | case Node::QmlValueType: |
258 | if (node->doc().hasKeywords()) { |
259 | const auto keywords = node->doc().keywords(); |
260 | for (const Atom *keyword : keywords) { |
261 | if (!keyword->string().isEmpty()) { |
262 | project.m_keywords.append(t: Keyword(keyword->string(), keyword->string(), |
263 | m_gen->fullDocumentLocation(node, useSubdir: false))); |
264 | } |
265 | else |
266 | node->doc().location().warning( |
267 | QStringLiteral("Bad keyword in %1" ) |
268 | .arg(a: m_gen->fullDocumentLocation(node, useSubdir: false))); |
269 | } |
270 | } |
271 | project.m_keywords.append(t: keywordDetails(node)); |
272 | break; |
273 | |
274 | case Node::Namespace: |
275 | project.m_keywords.append(t: keywordDetails(node)); |
276 | break; |
277 | |
278 | case Node::Enum: |
279 | project.m_keywords.append(t: keywordDetails(node)); |
280 | { |
281 | const auto *enumNode = static_cast<const EnumNode *>(node); |
282 | const auto items = enumNode->items(); |
283 | for (const auto &item : items) { |
284 | if (enumNode->itemAccess(name: item.name()) == Access::Private) |
285 | continue; |
286 | |
287 | QString name; |
288 | QString id; |
289 | if (!node->parent()->name().isEmpty()) { |
290 | name = id = node->parent()->name() + "::" + item.name(); |
291 | } else { |
292 | name = id = item.name(); |
293 | } |
294 | QString ref = m_gen->fullDocumentLocation(node, useSubdir: false); |
295 | project.m_keywords.append(t: Keyword(name, id, ref)); |
296 | } |
297 | } |
298 | break; |
299 | |
300 | case Node::Group: |
301 | case Node::Module: |
302 | case Node::QmlModule: { |
303 | const auto *cn = static_cast<const CollectionNode *>(node); |
304 | if (!cn->fullTitle().isEmpty()) { |
305 | if (cn->doc().hasKeywords()) { |
306 | const auto keywords = cn->doc().keywords(); |
307 | for (const Atom *keyword : keywords) { |
308 | if (!keyword->string().isEmpty()) { |
309 | project.m_keywords.append( |
310 | t: Keyword(keyword->string(), keyword->string(), |
311 | m_gen->fullDocumentLocation(node, useSubdir: false))); |
312 | } else |
313 | cn->doc().location().warning( |
314 | QStringLiteral("Bad keyword in %1" ) |
315 | .arg(a: m_gen->fullDocumentLocation(node, useSubdir: false))); |
316 | } |
317 | } |
318 | project.m_keywords.append(t: keywordDetails(node)); |
319 | } |
320 | } break; |
321 | |
322 | case Node::Property: |
323 | case Node::QmlProperty: |
324 | project.m_keywords.append(t: keywordDetails(node)); |
325 | break; |
326 | |
327 | case Node::Function: { |
328 | const auto *funcNode = static_cast<const FunctionNode *>(node); |
329 | |
330 | /* |
331 | QML methods, signals, and signal handlers used to be node types, |
332 | but now they are Function nodes with a Metaness value that specifies |
333 | what kind of function they are, QmlSignal, QmlMethod, etc. |
334 | */ |
335 | if (funcNode->isQmlNode()) { |
336 | project.m_keywords.append(t: keywordDetails(node)); |
337 | break; |
338 | } |
339 | // Only insert keywords for non-constructors. Constructors are covered |
340 | // by the classes themselves. |
341 | |
342 | if (!funcNode->isSomeCtor()) |
343 | project.m_keywords.append(t: keywordDetails(node)); |
344 | |
345 | // Insert member status flags into the entries for the parent |
346 | // node of the function, or the node it is related to. |
347 | // Since parent nodes should have already been inserted into |
348 | // the set of files, we only need to ensure that related nodes |
349 | // are inserted. |
350 | |
351 | if (node->parent()) |
352 | project.m_memberStatus[node->parent()].insert(value: node->status()); |
353 | } break; |
354 | case Node::TypeAlias: |
355 | case Node::Typedef: { |
356 | const auto *typedefNode = static_cast<const TypedefNode *>(node); |
357 | Keyword typedefDetails = keywordDetails(node); |
358 | const EnumNode *enumNode = typedefNode->associatedEnum(); |
359 | // Use the location of any associated enum node in preference |
360 | // to that of the typedef. |
361 | if (enumNode) |
362 | typedefDetails.m_ref = m_gen->fullDocumentLocation(node: enumNode, useSubdir: false); |
363 | |
364 | project.m_keywords.append(t: typedefDetails); |
365 | } break; |
366 | |
367 | case Node::Variable: { |
368 | project.m_keywords.append(t: keywordDetails(node)); |
369 | } break; |
370 | |
371 | // Page nodes (such as manual pages) contain subtypes, titles and other |
372 | // attributes. |
373 | case Node::Page: { |
374 | const auto *pn = static_cast<const PageNode *>(node); |
375 | if (!pn->fullTitle().isEmpty()) { |
376 | if (pn->doc().hasKeywords()) { |
377 | const auto keywords = pn->doc().keywords(); |
378 | for (const Atom *keyword : keywords) { |
379 | if (!keyword->string().isEmpty()) { |
380 | project.m_keywords.append( |
381 | t: Keyword(keyword->string(), keyword->string(), |
382 | m_gen->fullDocumentLocation(node, useSubdir: false))); |
383 | } else { |
384 | QString loc = m_gen->fullDocumentLocation(node, useSubdir: false); |
385 | pn->doc().location().warning(QStringLiteral("Bad keyword in %1" ).arg(a: loc)); |
386 | } |
387 | } |
388 | } |
389 | project.m_keywords.append(t: keywordDetails(node)); |
390 | } |
391 | break; |
392 | } |
393 | default:; |
394 | } |
395 | |
396 | // Add all images referenced in the page to the set of files to include. |
397 | const Atom *atom = node->doc().body().firstAtom(); |
398 | while (atom) { |
399 | if (atom->type() == Atom::Image || atom->type() == Atom::InlineImage) { |
400 | // Images are all placed within a single directory regardless of |
401 | // whether the source images are in a nested directory structure. |
402 | QStringList pieces = atom->string().split(sep: QLatin1Char('/')); |
403 | project.m_files.insert(value: "images/" + pieces.last()); |
404 | } |
405 | atom = atom->next(); |
406 | } |
407 | |
408 | return true; |
409 | } |
410 | |
411 | void HelpProjectWriter::generateSections(HelpProject &project, QXmlStreamWriter &writer, |
412 | const Node *node) |
413 | { |
414 | /* |
415 | Don't include index nodes in the help file. |
416 | */ |
417 | if (node->isIndexNode()) |
418 | return; |
419 | if (!generateSection(project, writer, node)) |
420 | return; |
421 | |
422 | if (node->isAggregate()) { |
423 | const auto *aggregate = static_cast<const Aggregate *>(node); |
424 | |
425 | // Ensure that we don't visit nodes more than once. |
426 | NodeList childSet; |
427 | NodeList children = aggregate->childNodes(); |
428 | std::sort(first: children.begin(), last: children.end(), comp: Node::nodeNameLessThan); |
429 | for (auto *child : children) { |
430 | // Skip related non-members adopted by some other aggregate |
431 | if (child->parent() != aggregate) |
432 | continue; |
433 | if (child->isIndexNode() || child->isPrivate()) |
434 | continue; |
435 | if (child->isTextPageNode()) { |
436 | if (!childSet.contains(t: child)) |
437 | childSet << child; |
438 | } else { |
439 | // Store member status of children |
440 | project.m_memberStatus[node].insert(value: child->status()); |
441 | if (child->isFunction() && static_cast<const FunctionNode *>(child)->isOverload()) |
442 | continue; |
443 | if (!childSet.contains(t: child)) |
444 | childSet << child; |
445 | } |
446 | } |
447 | for (const auto *child : std::as_const(t&: childSet)) |
448 | generateSections(project, writer, node: child); |
449 | } |
450 | } |
451 | |
452 | void HelpProjectWriter::generate() |
453 | { |
454 | // Warn if .qhp configuration was expected but not provided |
455 | if (auto &config = Config::instance(); m_projects.isEmpty() && config.get(CONFIG_QHP).asBool()) { |
456 | config.location().warning(message: u"Documentation configuration for '%1' doesn't define a help project (qhp)"_s |
457 | .arg(a: config.get(CONFIG_PROJECT).asString())); |
458 | } |
459 | for (HelpProject &project : m_projects) |
460 | generateProject(project); |
461 | } |
462 | |
463 | void HelpProjectWriter::writeHashFile(QFile &file) |
464 | { |
465 | QCryptographicHash hash(QCryptographicHash::Sha1); |
466 | hash.addData(device: &file); |
467 | |
468 | QFile hashFile(file.fileName() + ".sha1" ); |
469 | if (!hashFile.open(flags: QFile::WriteOnly | QFile::Text)) |
470 | return; |
471 | |
472 | hashFile.write(data: hash.result().toHex()); |
473 | hashFile.close(); |
474 | } |
475 | |
476 | void HelpProjectWriter::writeSection(QXmlStreamWriter &writer, const QString &path, |
477 | const QString &value) |
478 | { |
479 | writer.writeStartElement(QStringLiteral("section" )); |
480 | writer.writeAttribute(QStringLiteral("ref" ), value: path); |
481 | writer.writeAttribute(QStringLiteral("title" ), value); |
482 | writer.writeEndElement(); // section |
483 | } |
484 | |
485 | /*! |
486 | Write subsections for all members, compatibility members and obsolete members. |
487 | */ |
488 | void HelpProjectWriter::addMembers(HelpProject &project, QXmlStreamWriter &writer, const Node *node) |
489 | { |
490 | QString href = m_gen->fullDocumentLocation(node, useSubdir: false); |
491 | href = href.left(n: href.size() - 5); |
492 | if (href.isEmpty()) |
493 | return; |
494 | |
495 | bool derivedClass = false; |
496 | if (node->isClassNode()) |
497 | derivedClass = !(static_cast<const ClassNode *>(node)->baseClasses().isEmpty()); |
498 | |
499 | // Do not generate a 'List of all members' for namespaces or header files, |
500 | // but always generate it for derived classes and QML types (but not QML value types) |
501 | if (!node->isNamespace() && !node->isHeader() && !node->isQmlBasicType() |
502 | && (derivedClass || node->isQmlType() || !project.m_memberStatus[node].isEmpty())) { |
503 | QString membersPath = href + QStringLiteral("-members.html" ); |
504 | writeSection(writer, path: membersPath, QStringLiteral("List of all members" )); |
505 | } |
506 | if (project.m_memberStatus[node].contains(value: Node::Deprecated)) { |
507 | QString obsoletePath = href + QStringLiteral("-obsolete.html" ); |
508 | writeSection(writer, path: obsoletePath, QStringLiteral("Obsolete members" )); |
509 | } |
510 | } |
511 | |
512 | void HelpProjectWriter::writeNode(HelpProject &project, QXmlStreamWriter &writer, const Node *node) |
513 | { |
514 | QString href = m_gen->fullDocumentLocation(node, useSubdir: false); |
515 | QString objName = node->name(); |
516 | |
517 | switch (node->nodeType()) { |
518 | |
519 | case Node::Class: |
520 | case Node::Struct: |
521 | case Node::Union: |
522 | case Node::QmlType: |
523 | case Node::QmlValueType: { |
524 | QString typeStr = m_gen->typeString(node); |
525 | if (!typeStr.isEmpty()) |
526 | typeStr[0] = typeStr[0].toTitleCase(); |
527 | writer.writeStartElement(qualifiedName: "section" ); |
528 | writer.writeAttribute(qualifiedName: "ref" , value: href); |
529 | if (node->parent() && !node->parent()->name().isEmpty()) |
530 | writer.writeAttribute(qualifiedName: "title" , |
531 | QStringLiteral("%1::%2 %3 Reference" ) |
532 | .arg(args: node->parent()->name(), args&: objName, args&: typeStr)); |
533 | else |
534 | writer.writeAttribute(qualifiedName: "title" , QStringLiteral("%1 %2 Reference" ).arg(args&: objName, args&: typeStr)); |
535 | |
536 | addMembers(project, writer, node); |
537 | writer.writeEndElement(); // section |
538 | } break; |
539 | |
540 | case Node::Namespace: |
541 | writeSection(writer, path: href, value: objName); |
542 | break; |
543 | |
544 | case Node::Example: |
545 | case Node::HeaderFile: |
546 | case Node::Page: |
547 | case Node::Group: |
548 | case Node::Module: |
549 | case Node::QmlModule: { |
550 | writer.writeStartElement(qualifiedName: "section" ); |
551 | writer.writeAttribute(qualifiedName: "ref" , value: href); |
552 | writer.writeAttribute(qualifiedName: "title" , value: node->fullTitle()); |
553 | if (node->nodeType() == Node::HeaderFile) |
554 | addMembers(project, writer, node); |
555 | writer.writeEndElement(); // section |
556 | } break; |
557 | default:; |
558 | } |
559 | } |
560 | |
561 | void HelpProjectWriter::generateProject(HelpProject &project) |
562 | { |
563 | const Node *rootNode; |
564 | |
565 | // Restrict searching only to the local (primary) tree |
566 | QList<Tree *> searchOrder = m_qdb->searchOrder(); |
567 | m_qdb->setLocalSearch(); |
568 | |
569 | if (!project.m_indexRoot.isEmpty()) |
570 | rootNode = m_qdb->findPageNodeByTitle(title: project.m_indexRoot); |
571 | else |
572 | rootNode = m_qdb->primaryTreeRoot(); |
573 | |
574 | if (rootNode == nullptr) |
575 | return; |
576 | |
577 | project.m_files.clear(); |
578 | project.m_keywords.clear(); |
579 | |
580 | QFile file(m_outputDir + QDir::separator() + project.m_fileName); |
581 | if (!file.open(flags: QFile::WriteOnly | QFile::Text)) |
582 | return; |
583 | |
584 | QXmlStreamWriter writer(&file); |
585 | writer.setAutoFormatting(true); |
586 | writer.writeStartDocument(); |
587 | writer.writeStartElement(qualifiedName: "QtHelpProject" ); |
588 | writer.writeAttribute(qualifiedName: "version" , value: "1.0" ); |
589 | |
590 | // Write metaData, virtualFolder and namespace elements. |
591 | writer.writeTextElement(qualifiedName: "namespace" , text: project.m_helpNamespace); |
592 | writer.writeTextElement(qualifiedName: "virtualFolder" , text: project.m_virtualFolder); |
593 | writer.writeStartElement(qualifiedName: "metaData" ); |
594 | writer.writeAttribute(qualifiedName: "name" , value: "version" ); |
595 | writer.writeAttribute(qualifiedName: "value" , value: project.m_version); |
596 | writer.writeEndElement(); |
597 | |
598 | // Write customFilter elements. |
599 | for (auto it = project.m_customFilters.constBegin(); it != project.m_customFilters.constEnd(); |
600 | ++it) { |
601 | writer.writeStartElement(qualifiedName: "customFilter" ); |
602 | writer.writeAttribute(qualifiedName: "name" , value: it.key()); |
603 | QStringList sortedAttributes = it.value().values(); |
604 | sortedAttributes.sort(); |
605 | for (const auto &filter : std::as_const(t&: sortedAttributes)) |
606 | writer.writeTextElement(qualifiedName: "filterAttribute" , text: filter); |
607 | writer.writeEndElement(); // customFilter |
608 | } |
609 | |
610 | // Start the filterSection. |
611 | writer.writeStartElement(qualifiedName: "filterSection" ); |
612 | |
613 | // Write filterAttribute elements. |
614 | QStringList sortedFilterAttributes = project.m_filterAttributes.values(); |
615 | sortedFilterAttributes.sort(); |
616 | for (const auto &filterName : std::as_const(t&: sortedFilterAttributes)) |
617 | writer.writeTextElement(qualifiedName: "filterAttribute" , text: filterName); |
618 | |
619 | writer.writeStartElement(qualifiedName: "toc" ); |
620 | writer.writeStartElement(qualifiedName: "section" ); |
621 | const Node *node = m_qdb->findPageNodeByTitle(title: project.m_indexTitle); |
622 | if (!node) |
623 | node = m_qdb->findNodeByNameAndType(path: QStringList(project.m_indexTitle), isMatch: &Node::isPageNode); |
624 | if (!node) |
625 | node = m_qdb->findNodeByNameAndType(path: QStringList("index.html" ), isMatch: &Node::isPageNode); |
626 | QString indexPath; |
627 | if (node) |
628 | indexPath = m_gen->fullDocumentLocation(node, useSubdir: false); |
629 | else |
630 | indexPath = "index.html" ; |
631 | writer.writeAttribute(qualifiedName: "ref" , value: indexPath); |
632 | writer.writeAttribute(qualifiedName: "title" , value: project.m_indexTitle); |
633 | |
634 | generateSections(project, writer, node: rootNode); |
635 | |
636 | for (int i = 0; i < project.m_subprojects.size(); i++) { |
637 | SubProject subproject = project.m_subprojects[i]; |
638 | |
639 | if (subproject.m_type == QLatin1String("manual" )) { |
640 | |
641 | const Node *indexPage = m_qdb->findNodeForTarget(target: subproject.m_indexTitle, relative: nullptr); |
642 | if (indexPage) { |
643 | Text indexBody = indexPage->doc().body(); |
644 | const Atom *atom = indexBody.firstAtom(); |
645 | QStack<int> sectionStack; |
646 | bool inItem = false; |
647 | |
648 | while (atom) { |
649 | switch (atom->type()) { |
650 | case Atom::ListLeft: |
651 | sectionStack.push(t: 0); |
652 | break; |
653 | case Atom::ListRight: |
654 | if (sectionStack.pop() > 0) |
655 | writer.writeEndElement(); // section |
656 | break; |
657 | case Atom::ListItemLeft: |
658 | inItem = true; |
659 | break; |
660 | case Atom::ListItemRight: |
661 | inItem = false; |
662 | break; |
663 | case Atom::Link: |
664 | if (inItem) { |
665 | if (sectionStack.top() > 0) |
666 | writer.writeEndElement(); // section |
667 | |
668 | const Node *page = m_qdb->findNodeForTarget(target: atom->string(), relative: nullptr); |
669 | writer.writeStartElement(qualifiedName: "section" ); |
670 | QString indexPath = m_gen->fullDocumentLocation(node: page, useSubdir: false); |
671 | writer.writeAttribute(qualifiedName: "ref" , value: indexPath); |
672 | writer.writeAttribute(qualifiedName: "title" , value: atom->linkText()); |
673 | |
674 | sectionStack.top() += 1; |
675 | } |
676 | break; |
677 | default:; |
678 | } |
679 | |
680 | if (atom == indexBody.lastAtom()) |
681 | break; |
682 | atom = atom->next(); |
683 | } |
684 | } else |
685 | rootNode->doc().location().warning( |
686 | QStringLiteral("Failed to find index: %1" ).arg(a: subproject.m_indexTitle)); |
687 | |
688 | } else { |
689 | |
690 | writer.writeStartElement(qualifiedName: "section" ); |
691 | QString indexPath = m_gen->fullDocumentLocation( |
692 | node: m_qdb->findNodeForTarget(target: subproject.m_indexTitle, relative: nullptr), useSubdir: false); |
693 | writer.writeAttribute(qualifiedName: "ref" , value: indexPath); |
694 | writer.writeAttribute(qualifiedName: "title" , value: subproject.m_title); |
695 | |
696 | if (subproject.m_sortPages) { |
697 | QStringList titles = subproject.m_nodes.keys(); |
698 | titles.sort(); |
699 | for (const auto &title : std::as_const(t&: titles)) { |
700 | writeNode(project, writer, node: subproject.m_nodes[title]); |
701 | } |
702 | } else { |
703 | // Find a contents node and navigate from there, using the NextLink values. |
704 | QSet<QString> visited; |
705 | bool contentsFound = false; |
706 | for (const auto *node : std::as_const(t&: subproject.m_nodes)) { |
707 | QString nextTitle = node->links().value(key: Node::NextLink).first; |
708 | if (!nextTitle.isEmpty() |
709 | && node->links().value(key: Node::ContentsLink).first.isEmpty()) { |
710 | |
711 | const Node *nextPage = m_qdb->findNodeForTarget(target: nextTitle, relative: nullptr); |
712 | |
713 | // Write the contents node. |
714 | writeNode(project, writer, node); |
715 | contentsFound = true; |
716 | |
717 | while (nextPage) { |
718 | writeNode(project, writer, node: nextPage); |
719 | nextTitle = nextPage->links().value(key: Node::NextLink).first; |
720 | if (nextTitle.isEmpty() || visited.contains(value: nextTitle)) |
721 | break; |
722 | nextPage = m_qdb->findNodeForTarget(target: nextTitle, relative: nullptr); |
723 | visited.insert(value: nextTitle); |
724 | } |
725 | break; |
726 | } |
727 | } |
728 | // No contents/nextpage links found, write all nodes unsorted |
729 | if (!contentsFound) { |
730 | QList<const Node *> subnodes = subproject.m_nodes.values(); |
731 | |
732 | std::sort(first: subnodes.begin(), last: subnodes.end(), comp: Node::nodeNameLessThan); |
733 | |
734 | for (const auto *node : std::as_const(t&: subnodes)) |
735 | writeNode(project, writer, node); |
736 | } |
737 | } |
738 | |
739 | writer.writeEndElement(); // section |
740 | } |
741 | } |
742 | |
743 | // Restore original search order |
744 | m_qdb->setSearchOrder(searchOrder); |
745 | |
746 | writer.writeEndElement(); // section |
747 | writer.writeEndElement(); // toc |
748 | |
749 | writer.writeStartElement(qualifiedName: "keywords" ); |
750 | std::sort(first: project.m_keywords.begin(), last: project.m_keywords.end()); |
751 | for (const auto &k : std::as_const(t&: project.m_keywords)) { |
752 | for (const auto &id : std::as_const(t: k.m_ids)) { |
753 | writer.writeStartElement(qualifiedName: "keyword" ); |
754 | writer.writeAttribute(qualifiedName: "name" , value: k.m_name); |
755 | writer.writeAttribute(qualifiedName: "id" , value: id); |
756 | writer.writeAttribute(qualifiedName: "ref" , value: k.m_ref); |
757 | writer.writeEndElement(); //keyword |
758 | } |
759 | } |
760 | writer.writeEndElement(); // keywords |
761 | |
762 | writer.writeStartElement(qualifiedName: "files" ); |
763 | |
764 | // The list of files to write is the union of generated files and |
765 | // other files (images and extras) included in the project |
766 | QSet<QString> files = |
767 | QSet<QString>(m_gen->outputFileNames().cbegin(), m_gen->outputFileNames().cend()); |
768 | files.unite(other: project.m_files); |
769 | files.unite(other: project.m_extraFiles); |
770 | QStringList sortedFiles = files.values(); |
771 | sortedFiles.sort(); |
772 | for (const auto &usedFile : std::as_const(t&: sortedFiles)) { |
773 | if (!usedFile.isEmpty()) |
774 | writer.writeTextElement(qualifiedName: "file" , text: usedFile); |
775 | } |
776 | writer.writeEndElement(); // files |
777 | |
778 | writer.writeEndElement(); // filterSection |
779 | writer.writeEndElement(); // QtHelpProject |
780 | writer.writeEndDocument(); |
781 | writeHashFile(file); |
782 | file.close(); |
783 | } |
784 | |
785 | QT_END_NAMESPACE |
786 | |