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 "htmlgenerator.h" |
5 | |
6 | #include "access.h" |
7 | #include "aggregate.h" |
8 | #include "classnode.h" |
9 | #include "collectionnode.h" |
10 | #include "config.h" |
11 | #include "codemarker.h" |
12 | #include "codeparser.h" |
13 | #include "enumnode.h" |
14 | #include "functionnode.h" |
15 | #include "helpprojectwriter.h" |
16 | #include "manifestwriter.h" |
17 | #include "node.h" |
18 | #include "propertynode.h" |
19 | #include "qdocdatabase.h" |
20 | #include "qmlpropertynode.h" |
21 | #include "sharedcommentnode.h" |
22 | #include "tagfilewriter.h" |
23 | #include "tree.h" |
24 | #include "quoter.h" |
25 | #include "utilities.h" |
26 | |
27 | #include <QtCore/qlist.h> |
28 | #include <QtCore/qmap.h> |
29 | #include <QtCore/quuid.h> |
30 | #include <QtCore/qversionnumber.h> |
31 | #include <QtCore/qregularexpression.h> |
32 | |
33 | #include <cctype> |
34 | #include <deque> |
35 | #include <string> |
36 | |
37 | QT_BEGIN_NAMESPACE |
38 | |
39 | using namespace Qt::StringLiterals; |
40 | |
41 | bool HtmlGenerator::s_inUnorderedList { false }; |
42 | |
43 | HtmlGenerator::HtmlGenerator(FileResolver& file_resolver) : XmlGenerator(file_resolver) {} |
44 | |
45 | static void addLink(const QString &linkTarget, QStringView nestedStuff, QString *res) |
46 | { |
47 | if (!linkTarget.isEmpty()) { |
48 | *res += QLatin1String("<a href=\"" ); |
49 | *res += linkTarget; |
50 | *res += QLatin1String("\" translate=\"no\">" ); |
51 | *res += nestedStuff; |
52 | *res += QLatin1String("</a>" ); |
53 | } else { |
54 | *res += nestedStuff; |
55 | } |
56 | } |
57 | |
58 | /*! |
59 | \internal |
60 | Convenience method that starts an unordered list if not in one. |
61 | */ |
62 | inline void HtmlGenerator::openUnorderedList() |
63 | { |
64 | if (!s_inUnorderedList) { |
65 | out() << "<ul>\n" ; |
66 | s_inUnorderedList = true; |
67 | } |
68 | } |
69 | |
70 | /*! |
71 | \internal |
72 | Convenience method that closes an unordered list if in one. |
73 | */ |
74 | inline void HtmlGenerator::closeUnorderedList() |
75 | { |
76 | if (s_inUnorderedList) { |
77 | out() << "</ul>\n" ; |
78 | s_inUnorderedList = false; |
79 | } |
80 | } |
81 | |
82 | /*! |
83 | Destroys the HTML output generator. Deletes the singleton |
84 | instance of HelpProjectWriter and the ManifestWriter instance. |
85 | */ |
86 | HtmlGenerator::~HtmlGenerator() |
87 | { |
88 | if (m_helpProjectWriter) { |
89 | delete m_helpProjectWriter; |
90 | m_helpProjectWriter = nullptr; |
91 | } |
92 | |
93 | if (m_manifestWriter) { |
94 | delete m_manifestWriter; |
95 | m_manifestWriter = nullptr; |
96 | } |
97 | } |
98 | |
99 | /*! |
100 | Initializes the HTML output generator's data structures |
101 | from the configuration (Config) singleton. |
102 | */ |
103 | void HtmlGenerator::initializeGenerator() |
104 | { |
105 | static const struct |
106 | { |
107 | const char *key; |
108 | const char *left; |
109 | const char *right; |
110 | } defaults[] = { { ATOM_FORMATTING_BOLD, .left: "<b>" , .right: "</b>" }, |
111 | { ATOM_FORMATTING_INDEX, .left: "<!--" , .right: "-->" }, |
112 | { ATOM_FORMATTING_ITALIC, .left: "<i>" , .right: "</i>" }, |
113 | { ATOM_FORMATTING_PARAMETER, .left: "<i translate=\"no\">" , .right: "</i>" }, |
114 | { ATOM_FORMATTING_SUBSCRIPT, .left: "<sub>" , .right: "</sub>" }, |
115 | { ATOM_FORMATTING_SUPERSCRIPT, .left: "<sup>" , .right: "</sup>" }, |
116 | { ATOM_FORMATTING_TELETYPE, .left: "<code translate=\"no\">" , |
117 | .right: "</code>" }, // <tt> tag is not supported in HTML5 |
118 | { ATOM_FORMATTING_UICONTROL, .left: "<b translate=\"no\">" , .right: "</b>" }, |
119 | { ATOM_FORMATTING_UNDERLINE, .left: "<u>" , .right: "</u>" }, |
120 | { .key: nullptr, .left: nullptr, .right: nullptr } }; |
121 | |
122 | Generator::initializeGenerator(); |
123 | config = &Config::instance(); |
124 | |
125 | /* |
126 | The formatting maps are owned by Generator. They are cleared in |
127 | Generator::terminate(). |
128 | */ |
129 | for (int i = 0; defaults[i].key; ++i) { |
130 | formattingLeftMap().insert(key: QLatin1String(defaults[i].key), value: QLatin1String(defaults[i].left)); |
131 | formattingRightMap().insert(key: QLatin1String(defaults[i].key), |
132 | value: QLatin1String(defaults[i].right)); |
133 | } |
134 | |
135 | QString formatDot{HtmlGenerator::format() + Config::dot}; |
136 | m_endHeader = config->get(var: formatDot + CONFIG_ENDHEADER).asString(); |
137 | m_postHeader = config->get(var: formatDot + HTMLGENERATOR_POSTHEADER).asString(); |
138 | m_postPostHeader = config->get(var: formatDot + HTMLGENERATOR_POSTPOSTHEADER).asString(); |
139 | m_prologue = config->get(var: formatDot + HTMLGENERATOR_PROLOGUE).asString(); |
140 | |
141 | m_footer = config->get(var: formatDot + HTMLGENERATOR_FOOTER).asString(); |
142 | m_address = config->get(var: formatDot + HTMLGENERATOR_ADDRESS).asString(); |
143 | m_noNavigationBar = config->get(var: formatDot + HTMLGENERATOR_NONAVIGATIONBAR).asBool(); |
144 | m_navigationSeparator = config->get(var: formatDot + HTMLGENERATOR_NAVIGATIONSEPARATOR).asString(); |
145 | tocDepth = config->get(var: formatDot + HTMLGENERATOR_TOCDEPTH).asInt(); |
146 | |
147 | m_project = config->get(CONFIG_PROJECT).asString(); |
148 | m_projectDescription = config->get(CONFIG_DESCRIPTION) |
149 | .asString(defaultString: m_project + QLatin1String(" Reference Documentation" )); |
150 | |
151 | m_projectUrl = config->get(CONFIG_URL).asString(); |
152 | tagFile_ = config->get(CONFIG_TAGFILE).asString(); |
153 | naturalLanguage = config->get(CONFIG_NATURALLANGUAGE).asString(defaultString: QLatin1String("en" )); |
154 | |
155 | m_codeIndent = config->get(CONFIG_CODEINDENT).asInt(); |
156 | m_codePrefix = config->get(CONFIG_CODEPREFIX).asString(); |
157 | m_codeSuffix = config->get(CONFIG_CODESUFFIX).asString(); |
158 | |
159 | /* |
160 | The help file write should be allocated once and only once |
161 | per qdoc execution. |
162 | */ |
163 | if (m_helpProjectWriter) |
164 | m_helpProjectWriter->reset(defaultFileName: m_project.toLower() + ".qhp" , g: this); |
165 | else |
166 | m_helpProjectWriter = new HelpProjectWriter(m_project.toLower() + ".qhp" , this); |
167 | |
168 | if (!m_manifestWriter) |
169 | m_manifestWriter = new ManifestWriter(); |
170 | |
171 | // Documentation template handling |
172 | m_headerScripts = config->get(var: formatDot + CONFIG_HEADERSCRIPTS).asString(); |
173 | m_headerStyles = config->get(var: formatDot + CONFIG_HEADERSTYLES).asString(); |
174 | |
175 | // Retrieve the config for the navigation bar |
176 | m_homepage = config->get(CONFIG_NAVIGATION |
177 | + Config::dot + CONFIG_HOMEPAGE).asString(); |
178 | |
179 | m_hometitle = config->get(CONFIG_NAVIGATION |
180 | + Config::dot + CONFIG_HOMETITLE) |
181 | .asString(defaultString: m_homepage); |
182 | |
183 | m_landingpage = config->get(CONFIG_NAVIGATION |
184 | + Config::dot + CONFIG_LANDINGPAGE).asString(); |
185 | |
186 | m_landingtitle = config->get(CONFIG_NAVIGATION |
187 | + Config::dot + CONFIG_LANDINGTITLE) |
188 | .asString(defaultString: m_landingpage); |
189 | |
190 | m_cppclassespage = config->get(CONFIG_NAVIGATION |
191 | + Config::dot + CONFIG_CPPCLASSESPAGE).asString(); |
192 | |
193 | m_cppclassestitle = config->get(CONFIG_NAVIGATION |
194 | + Config::dot + CONFIG_CPPCLASSESTITLE) |
195 | .asString(defaultString: QLatin1String("C++ Classes" )); |
196 | |
197 | m_qmltypespage = config->get(CONFIG_NAVIGATION |
198 | + Config::dot + CONFIG_QMLTYPESPAGE).asString(); |
199 | |
200 | m_qmltypestitle = config->get(CONFIG_NAVIGATION |
201 | + Config::dot + CONFIG_QMLTYPESTITLE) |
202 | .asString(defaultString: QLatin1String("QML Types" )); |
203 | |
204 | m_buildversion = config->get(CONFIG_BUILDVERSION).asString(); |
205 | } |
206 | |
207 | /*! |
208 | Gracefully terminates the HTML output generator. |
209 | */ |
210 | void HtmlGenerator::terminateGenerator() |
211 | { |
212 | Generator::terminateGenerator(); |
213 | } |
214 | |
215 | QString HtmlGenerator::format() |
216 | { |
217 | return "HTML" ; |
218 | } |
219 | |
220 | /*! |
221 | If qdoc is in the \c {-prepare} phase, traverse the primary |
222 | tree to generate the index file for the current module. |
223 | |
224 | If qdoc is in the \c {-generate} phase, traverse the primary |
225 | tree to generate all the HTML documentation for the current |
226 | module. Then generate the help file and the tag file. |
227 | */ |
228 | void HtmlGenerator::generateDocs() |
229 | { |
230 | Node *qflags = m_qdb->findClassNode(path: QStringList("QFlags" )); |
231 | if (qflags) |
232 | m_qflagsHref = linkForNode(node: qflags, relative: nullptr); |
233 | if (!config->preparing()) |
234 | Generator::generateDocs(); |
235 | |
236 | if (!config->generating()) { |
237 | QString fileBase = |
238 | m_project.toLower().simplified().replace(before: QLatin1Char(' '), after: QLatin1Char('-')); |
239 | m_qdb->generateIndex(fileName: outputDir() + QLatin1Char('/') + fileBase + ".index" , url: m_projectUrl, |
240 | title: m_projectDescription, g: this); |
241 | } |
242 | |
243 | if (!config->preparing()) { |
244 | m_helpProjectWriter->generate(); |
245 | m_manifestWriter->generateManifestFiles(); |
246 | /* |
247 | Generate the XML tag file, if it was requested. |
248 | */ |
249 | if (!tagFile_.isEmpty()) { |
250 | TagFileWriter tagFileWriter; |
251 | tagFileWriter.generateTagFile(fileName: tagFile_, generator: this); |
252 | } |
253 | } |
254 | } |
255 | |
256 | /*! |
257 | Generate an html file with the contents of a C++ or QML source file. |
258 | */ |
259 | void HtmlGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker *marker) |
260 | { |
261 | SubTitleSize subTitleSize = LargeSubTitle; |
262 | QString fullTitle = en->fullTitle(); |
263 | |
264 | beginSubPage(node: en, fileName: linkForExampleFile(path: resolved_file.get_query())); |
265 | generateHeader(title: fullTitle, node: en, marker); |
266 | generateTitle(title: fullTitle, subTitle: Text() << en->subtitle(), subTitleSize, relative: en, marker); |
267 | |
268 | Text text; |
269 | Quoter quoter; |
270 | Doc::quoteFromFile(location: en->doc().location(), quoter, resolved_file); |
271 | QString code = quoter.quoteTo(docLocation: en->location(), command: QString(), pattern: QString()); |
272 | CodeMarker *codeMarker = CodeMarker::markerForFileName(fileName: resolved_file.get_path()); |
273 | text << Atom(codeMarker->atomType(), code); |
274 | Atom a(codeMarker->atomType(), code); |
275 | |
276 | generateText(text, relative: en, marker: codeMarker); |
277 | endSubPage(); |
278 | } |
279 | |
280 | /*! |
281 | Generate html from an instance of Atom. |
282 | */ |
283 | qsizetype HtmlGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) |
284 | { |
285 | qsizetype idx, skipAhead = 0; |
286 | static bool in_para = false; |
287 | Node::Genus genus = Node::DontCare; |
288 | |
289 | switch (atom->type()) { |
290 | case Atom::AutoLink: { |
291 | QString name = atom->string(); |
292 | if (relative && relative->name() == name.replace(before: QLatin1String("()" ), after: QLatin1String())) { |
293 | out() << protectEnc(string: atom->string()); |
294 | break; |
295 | } |
296 | // Allow auto-linking to nodes in API reference |
297 | genus = Node::API; |
298 | } |
299 | Q_FALLTHROUGH(); |
300 | case Atom::NavAutoLink: |
301 | if (!m_inLink && !m_inContents && !m_inSectionHeading) { |
302 | const Node *node = nullptr; |
303 | QString link = getAutoLink(atom, relative, node: &node, genus); |
304 | if (link.isEmpty()) { |
305 | if (autolinkErrors() && relative) |
306 | relative->doc().location().warning( |
307 | QStringLiteral("Can't autolink to '%1'" ).arg(a: atom->string())); |
308 | } else if (node && node->isDeprecated()) { |
309 | if (relative && (relative->parent() != node) && !relative->isDeprecated()) |
310 | link.clear(); |
311 | } |
312 | if (link.isEmpty()) { |
313 | out() << protectEnc(string: atom->string()); |
314 | } else { |
315 | beginLink(link, node, relative); |
316 | generateLink(atom); |
317 | endLink(); |
318 | } |
319 | } else { |
320 | out() << protectEnc(string: atom->string()); |
321 | } |
322 | break; |
323 | case Atom::BaseName: |
324 | break; |
325 | case Atom::BriefLeft: |
326 | if (!hasBrief(node: relative)) { |
327 | skipAhead = skipAtoms(atom, type: Atom::BriefRight); |
328 | break; |
329 | } |
330 | out() << "<p>" ; |
331 | rewritePropertyBrief(atom, relative); |
332 | break; |
333 | case Atom::BriefRight: |
334 | if (hasBrief(node: relative)) |
335 | out() << "</p>\n" ; |
336 | break; |
337 | case Atom::C: |
338 | // This may at one time have been used to mark up C++ code but it is |
339 | // now widely used to write teletype text. As a result, text marked |
340 | // with the \c command is not passed to a code marker. |
341 | out() << formattingLeftMap()[ATOM_FORMATTING_TELETYPE]; |
342 | out() << protectEnc(string: plainCode(markedCode: atom->string())); |
343 | out() << formattingRightMap()[ATOM_FORMATTING_TELETYPE]; |
344 | break; |
345 | case Atom::CaptionLeft: |
346 | out() << "<p class=\"figCaption\">" ; |
347 | in_para = true; |
348 | break; |
349 | case Atom::CaptionRight: |
350 | endLink(); |
351 | if (in_para) { |
352 | out() << "</p>\n" ; |
353 | in_para = false; |
354 | } |
355 | break; |
356 | case Atom::Qml: |
357 | out() << "<pre class=\"qml\" translate=\"no\">" |
358 | << trimmedTrailing(string: highlightedCode(markedCode: indent(level: m_codeIndent, markedCode: atom->string()), relative, |
359 | alignNames: false, genus: Node::QML), |
360 | prefix: m_codePrefix, suffix: m_codeSuffix) |
361 | << "</pre>\n" ; |
362 | break; |
363 | case Atom::Code: |
364 | out() << "<pre class=\"cpp\" translate=\"no\">" |
365 | << trimmedTrailing(string: highlightedCode(markedCode: indent(level: m_codeIndent, markedCode: atom->string()), relative), |
366 | prefix: m_codePrefix, suffix: m_codeSuffix) |
367 | << "</pre>\n" ; |
368 | break; |
369 | case Atom::CodeBad: |
370 | out() << "<pre class=\"cpp plain\" translate=\"no\">" |
371 | << trimmedTrailing(string: protectEnc(string: plainCode(markedCode: indent(level: m_codeIndent, markedCode: atom->string()))), |
372 | prefix: m_codePrefix, suffix: m_codeSuffix) |
373 | << "</pre>\n" ; |
374 | break; |
375 | case Atom::DetailsLeft: |
376 | out() << "<details>\n" ; |
377 | if (!atom->string().isEmpty()) |
378 | out() << "<summary>" << protectEnc(string: atom->string()) << "</summary>\n" ; |
379 | else |
380 | out() << "<summary>...</summary>\n" ; |
381 | break; |
382 | case Atom::DetailsRight: |
383 | out() << "</details>\n" ; |
384 | break; |
385 | case Atom::DivLeft: |
386 | out() << "<div" ; |
387 | if (!atom->string().isEmpty()) |
388 | out() << ' ' << atom->string(); |
389 | out() << '>'; |
390 | break; |
391 | case Atom::DivRight: |
392 | out() << "</div>" ; |
393 | break; |
394 | case Atom::FootnoteLeft: |
395 | // ### For now |
396 | if (in_para) { |
397 | out() << "</p>\n" ; |
398 | in_para = false; |
399 | } |
400 | out() << "<!-- " ; |
401 | break; |
402 | case Atom::FootnoteRight: |
403 | // ### For now |
404 | out() << "-->\n" ; |
405 | break; |
406 | case Atom::FormatElse: |
407 | case Atom::FormatEndif: |
408 | case Atom::FormatIf: |
409 | break; |
410 | case Atom::FormattingLeft: |
411 | if (atom->string().startsWith(s: "span " )) { |
412 | out() << '<' + atom->string() << '>'; |
413 | } else |
414 | out() << formattingLeftMap()[atom->string()]; |
415 | if (atom->string() == ATOM_FORMATTING_PARAMETER) { |
416 | if (atom->next() != nullptr && atom->next()->type() == Atom::String) { |
417 | static const QRegularExpression subscriptRegExp("^([a-z]+)_([0-9n])$" ); |
418 | auto match = subscriptRegExp.match(subject: atom->next()->string()); |
419 | if (match.hasMatch()) { |
420 | out() << match.captured(nth: 1) << "<sub>" << match.captured(nth: 2) |
421 | << "</sub>" ; |
422 | skipAhead = 1; |
423 | } |
424 | } |
425 | } |
426 | break; |
427 | case Atom::FormattingRight: |
428 | if (atom->string() == ATOM_FORMATTING_LINK) { |
429 | endLink(); |
430 | } else if (atom->string().startsWith(s: "span " )) { |
431 | out() << "</span>" ; |
432 | } else { |
433 | out() << formattingRightMap()[atom->string()]; |
434 | } |
435 | break; |
436 | case Atom::AnnotatedList: { |
437 | const CollectionNode *cn = m_qdb->getCollectionNode(name: atom->string(), type: Node::Group); |
438 | if (cn) |
439 | generateList(relative: cn, marker, selector: atom->string()); |
440 | } break; |
441 | case Atom::GeneratedList: |
442 | if (atom->string() == QLatin1String("annotatedclasses" )) { |
443 | generateAnnotatedList(relative, marker, nodes: m_qdb->getCppClasses().values()); |
444 | } else if (atom->string() == QLatin1String("annotatedexamples" )) { |
445 | generateAnnotatedLists(relative, marker, nodeMap: m_qdb->getExamples()); |
446 | } else if (atom->string() == QLatin1String("annotatedattributions" )) { |
447 | generateAnnotatedLists(relative, marker, nodeMap: m_qdb->getAttributions()); |
448 | } else if (atom->string() == QLatin1String("classes" )) { |
449 | generateCompactList(listType: Generic, relative, classMap: m_qdb->getCppClasses(), includeAlphabet: true, |
450 | QStringLiteral("" )); |
451 | } else if (atom->string().contains(s: "classes " )) { |
452 | QString rootName = atom->string().mid(position: atom->string().indexOf(s: "classes" ) + 7).trimmed(); |
453 | generateCompactList(listType: Generic, relative, classMap: m_qdb->getCppClasses(), includeAlphabet: true, commonPrefix: rootName); |
454 | } else if (atom->string() == QLatin1String("qmlvaluetypes" ) |
455 | || atom->string() == QLatin1String("qmlbasictypes" )) { |
456 | generateCompactList(listType: Generic, relative, classMap: m_qdb->getQmlValueTypes(), includeAlphabet: true, |
457 | QStringLiteral("" )); |
458 | } else if (atom->string() == QLatin1String("qmltypes" )) { |
459 | generateCompactList(listType: Generic, relative, classMap: m_qdb->getQmlTypes(), includeAlphabet: true, QStringLiteral("" )); |
460 | } else if ((idx = atom->string().indexOf(QStringLiteral("bymodule" ))) != -1) { |
461 | QString moduleName = atom->string().mid(position: idx + 8).trimmed(); |
462 | Node::NodeType type = typeFromString(atom); |
463 | QDocDatabase *qdb = QDocDatabase::qdocDB(); |
464 | const CollectionNode *cn = qdb->getCollectionNode(name: moduleName, type); |
465 | if (cn) { |
466 | if (type == Node::Module) { |
467 | NodeMap m; |
468 | cn->getMemberClasses(out&: m); |
469 | if (!m.isEmpty()) { |
470 | generateAnnotatedList(relative, marker, nodes: m.values()); |
471 | } |
472 | } else |
473 | generateAnnotatedList(relative, marker, nodes: cn->members()); |
474 | } |
475 | } else if (atom->string() == QLatin1String("classhierarchy" )) { |
476 | generateClassHierarchy(relative, classMap&: m_qdb->getCppClasses()); |
477 | } else if (atom->string() == QLatin1String("obsoleteclasses" )) { |
478 | generateCompactList(listType: Generic, relative, classMap: m_qdb->getObsoleteClasses(), includeAlphabet: false, |
479 | QStringLiteral("Q" )); |
480 | } else if (atom->string() == QLatin1String("obsoleteqmltypes" )) { |
481 | generateCompactList(listType: Generic, relative, classMap: m_qdb->getObsoleteQmlTypes(), includeAlphabet: false, |
482 | QStringLiteral("" )); |
483 | } else if (atom->string() == QLatin1String("obsoletecppmembers" )) { |
484 | generateCompactList(listType: Obsolete, relative, classMap: m_qdb->getClassesWithObsoleteMembers(), includeAlphabet: false, |
485 | QStringLiteral("Q" )); |
486 | } else if (atom->string() == QLatin1String("obsoleteqmlmembers" )) { |
487 | generateCompactList(listType: Obsolete, relative, classMap: m_qdb->getQmlTypesWithObsoleteMembers(), includeAlphabet: false, |
488 | QStringLiteral("" )); |
489 | } else if (atom->string() == QLatin1String("functionindex" )) { |
490 | generateFunctionIndex(relative); |
491 | } else if (atom->string() == QLatin1String("attributions" )) { |
492 | generateAnnotatedList(relative, marker, nodes: m_qdb->getAttributions().values()); |
493 | } else if (atom->string() == QLatin1String("legalese" )) { |
494 | generateLegaleseList(relative, marker); |
495 | } else if (atom->string() == QLatin1String("overviews" )) { |
496 | generateList(relative, marker, selector: "overviews" ); |
497 | } else if (atom->string() == QLatin1String("cpp-modules" )) { |
498 | generateList(relative, marker, selector: "cpp-modules" ); |
499 | } else if (atom->string() == QLatin1String("qml-modules" )) { |
500 | generateList(relative, marker, selector: "qml-modules" ); |
501 | } else if (atom->string() == QLatin1String("namespaces" )) { |
502 | generateAnnotatedList(relative, marker, nodes: m_qdb->getNamespaces().values()); |
503 | } else if (atom->string() == QLatin1String("related" )) { |
504 | generateList(relative, marker, selector: "related" ); |
505 | } else { |
506 | const CollectionNode *cn = m_qdb->getCollectionNode(name: atom->string(), type: Node::Group); |
507 | if (cn) { |
508 | if (!generateGroupList(cn: const_cast<CollectionNode *>(cn))) |
509 | relative->location().warning( |
510 | message: QString("'\\generatelist %1' group is empty" ).arg(a: atom->string())); |
511 | } else { |
512 | relative->location().warning( |
513 | message: QString("'\\generatelist %1' no such group" ).arg(a: atom->string())); |
514 | } |
515 | } |
516 | break; |
517 | case Atom::SinceList: { |
518 | const NodeMultiMap &nsmap = m_qdb->getSinceMap(key: atom->string()); |
519 | if (nsmap.isEmpty()) |
520 | break; |
521 | |
522 | const NodeMultiMap &ncmap = m_qdb->getClassMap(key: atom->string()); |
523 | const NodeMultiMap &nqcmap = m_qdb->getQmlTypeMap(key: atom->string()); |
524 | |
525 | Sections sections(nsmap); |
526 | out() << "<ul>\n" ; |
527 | const QList<Section> sinceSections = sections.sinceSections(); |
528 | for (const auto §ion : sinceSections) { |
529 | if (!section.members().isEmpty()) { |
530 | out() << "<li>" |
531 | << "<a href=\"#" << Utilities::asAsciiPrintable(name: section.title()) << "\">" |
532 | << section.title() << "</a></li>\n" ; |
533 | } |
534 | } |
535 | out() << "</ul>\n" ; |
536 | |
537 | int index = 0; |
538 | for (const auto §ion : sinceSections) { |
539 | if (!section.members().isEmpty()) { |
540 | out() << "<h3 id=\"" << Utilities::asAsciiPrintable(name: section.title()) << "\">" |
541 | << protectEnc(string: section.title()) << "</h3>\n" ; |
542 | if (index == Sections::SinceClasses) |
543 | generateCompactList(listType: Generic, relative, classMap: ncmap, includeAlphabet: false, QStringLiteral("Q" )); |
544 | else if (index == Sections::SinceQmlTypes) |
545 | generateCompactList(listType: Generic, relative, classMap: nqcmap, includeAlphabet: false, QStringLiteral("" )); |
546 | else if (index == Sections::SinceMemberFunctions |
547 | || index == Sections::SinceQmlMethods |
548 | || index == Sections::SinceQmlProperties) { |
549 | |
550 | QMap<QString, NodeMultiMap> parentmaps; |
551 | |
552 | const QList<Node *> &members = section.members(); |
553 | for (const auto &member : members) { |
554 | QString parent_full_name = (*member).parent()->fullName(); |
555 | |
556 | auto parent_entry = parentmaps.find(key: parent_full_name); |
557 | if (parent_entry == parentmaps.end()) |
558 | parent_entry = parentmaps.insert(key: parent_full_name, value: NodeMultiMap()); |
559 | parent_entry->insert(key: member->name(), value: member); |
560 | } |
561 | |
562 | for (auto map = parentmaps.begin(); map != parentmaps.end(); ++map) { |
563 | NodeVector nv = map->values().toVector(); |
564 | auto parent = nv.front()->parent(); |
565 | |
566 | out() << ((index == Sections::SinceMemberFunctions) ? "<p>Class " : "<p>QML Type " ); |
567 | |
568 | out() << "<a href=\"" << linkForNode(node: parent, relative) << "\" translate=\"no\">" ; |
569 | QStringList pieces = parent->fullName().split(sep: "::" ); |
570 | out() << protectEnc(string: pieces.last()); |
571 | out() << "</a>" |
572 | << ":</p>\n" ; |
573 | |
574 | generateSection(nv, relative, marker); |
575 | out() << "<br/>" ; |
576 | } |
577 | } else if (index == Sections::SinceEnumValues) { |
578 | out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n" ; |
579 | const auto map_it = m_qdb->newEnumValueMaps().constFind(key: atom->string()); |
580 | for (auto it = map_it->cbegin(); it != map_it->cend(); ++it) { |
581 | out() << "<tr><td class=\"memItemLeft\"> enum value </td><td class=\"memItemRight\">" |
582 | << "<b><a href=\"" << linkForNode(node: it.value(), relative: nullptr) << "\">" |
583 | << it.key() << "</a></b></td></tr>\n" ; |
584 | } |
585 | out() << "</table></div>\n" ; |
586 | } else { |
587 | generateSection(nv: section.members(), relative, marker); |
588 | } |
589 | } |
590 | ++index; |
591 | } |
592 | } break; |
593 | case Atom::BR: |
594 | out() << "<br />\n" ; |
595 | break; |
596 | case Atom::HR: |
597 | out() << "<hr />\n" ; |
598 | break; |
599 | case Atom::Image: |
600 | case Atom::InlineImage: { |
601 | QString text; |
602 | if (atom->next() && atom->next()->type() == Atom::ImageText) |
603 | text = atom->next()->string(); |
604 | if (atom->type() == Atom::Image) |
605 | out() << "<p class=\"centerAlign\">" ; |
606 | |
607 | auto maybe_resolved_file{file_resolver.resolve(filename: atom->string())}; |
608 | if (!maybe_resolved_file) { |
609 | // TODO: [uncentralized-admonition] |
610 | relative->location().warning( |
611 | QStringLiteral("Missing image: %1" ).arg(a: protectEnc(string: atom->string()))); |
612 | out() << "<font color=\"red\">[Missing image " << protectEnc(string: atom->string()) |
613 | << "]</font>" ; |
614 | } else { |
615 | ResolvedFile file{*maybe_resolved_file}; |
616 | QString file_name{QFileInfo{file.get_path()}.fileName()}; |
617 | |
618 | // TODO: [operation-can-fail-making-the-output-incorrect] |
619 | // The operation of copying the file can fail, making the |
620 | // output refer to an image that does not exist. |
621 | // This should be fine as HTML will take care of managing |
622 | // the rendering of a missing image, but what html will |
623 | // render is in stark contrast with what we do when the |
624 | // image does not exist at all. |
625 | // It may be more correct to unify the behavior between |
626 | // the two either by considering images that cannot be |
627 | // copied as missing or letting the HTML renderer |
628 | // always taking care of the two cases. |
629 | // Do notice that effectively doing this might be |
630 | // unnecessary as extracting the output directory logic |
631 | // should ensure that a safe assumption for copy should be |
632 | // made at the API boundary. |
633 | |
634 | // TODO: [uncentralized-output-directory-structure] |
635 | Config::copyFile(location: relative->doc().location(), sourceFilePath: file.get_path(), userFriendlySourceFilePath: file_name, targetDirPath: outputDir() + QLatin1String("/images" )); |
636 | |
637 | // TODO: [uncentralized-output-directory-structure] |
638 | out() << "<img src=\"" << "images/" + protectEnc(string: file_name) << '"'; |
639 | |
640 | // TODO: [same-result-branching] |
641 | // If text is empty protectEnc should return the empty |
642 | // string itself, such that the two branches would still |
643 | // result in the same output. |
644 | // Ensure that this is the case and then flatten the branch if so. |
645 | if (!text.isEmpty()) |
646 | out() << " alt=\"" << protectEnc(string: text) << '"'; |
647 | else |
648 | out() << " alt=\"\"" ; |
649 | |
650 | out() << " />" ; |
651 | |
652 | // TODO: [uncentralized-output-directory-structure] |
653 | m_helpProjectWriter->addExtraFile(file: "images/" + file_name); |
654 | setImageFileName(relative, fileName: "images/" + file_name); |
655 | } |
656 | |
657 | if (atom->type() == Atom::Image) |
658 | out() << "</p>" ; |
659 | } break; |
660 | case Atom::ImageText: |
661 | break; |
662 | // Admonitions |
663 | case Atom::ImportantLeft: |
664 | case Atom::NoteLeft: |
665 | case Atom::WarningLeft: { |
666 | QString admonType = atom->typeString(); |
667 | // Remove 'Left' from atom type to get the admonition type |
668 | admonType.chop(n: 4); |
669 | out() << "<div class=\"admonition " << admonType.toLower() << "\">\n" |
670 | << "<p>" ; |
671 | out() << formattingLeftMap()[ATOM_FORMATTING_BOLD]; |
672 | out() << admonType << ": " ; |
673 | out() << formattingRightMap()[ATOM_FORMATTING_BOLD]; |
674 | } break; |
675 | case Atom::ImportantRight: |
676 | case Atom::NoteRight: |
677 | case Atom::WarningRight: |
678 | out() << "</p>\n" |
679 | << "</div>\n" ; |
680 | break; |
681 | case Atom::LegaleseLeft: |
682 | out() << "<div class=\"LegaleseLeft\">" ; |
683 | break; |
684 | case Atom::LegaleseRight: |
685 | out() << "</div>" ; |
686 | break; |
687 | case Atom::LineBreak: |
688 | out() << "<br/>" ; |
689 | break; |
690 | case Atom::Link: |
691 | // Prevent nested links in table of contents |
692 | if (m_inContents) |
693 | break; |
694 | Q_FALLTHROUGH(); |
695 | case Atom::NavLink: { |
696 | const Node *node = nullptr; |
697 | QString link = getLink(atom, relative, node: &node); |
698 | if (link.isEmpty() && (node != relative) && !noLinkErrors()) { |
699 | relative->doc().location().warning( |
700 | QStringLiteral("Can't link to '%1'" ).arg(a: atom->string())); |
701 | } |
702 | beginLink(link, node: nullptr, relative); |
703 | m_linkNode = node; |
704 | skipAhead = 1; |
705 | } break; |
706 | case Atom::ExampleFileLink: { |
707 | QString link = linkForExampleFile(path: atom->string()); |
708 | if (link.isEmpty() && !noLinkErrors()) |
709 | relative->doc().location().warning( |
710 | QStringLiteral("Can't link to '%1'" ).arg(a: atom->string())); |
711 | beginLink(link); |
712 | skipAhead = 1; |
713 | } break; |
714 | case Atom::ExampleImageLink: { |
715 | QString link = atom->string(); |
716 | if (link.isEmpty() && !noLinkErrors()) |
717 | relative->doc().location().warning( |
718 | QStringLiteral("Can't link to '%1'" ).arg(a: atom->string())); |
719 | link = "images/used-in-examples/" + link; |
720 | beginLink(link); |
721 | skipAhead = 1; |
722 | } break; |
723 | case Atom::LinkNode: { |
724 | const Node *node = CodeMarker::nodeForString(string: atom->string()); |
725 | beginLink(link: linkForNode(node, relative), node, relative); |
726 | skipAhead = 1; |
727 | } break; |
728 | case Atom::ListLeft: |
729 | if (in_para) { |
730 | out() << "</p>\n" ; |
731 | in_para = false; |
732 | } |
733 | if (atom->string() == ATOM_LIST_BULLET) { |
734 | out() << "<ul>\n" ; |
735 | } else if (atom->string() == ATOM_LIST_TAG) { |
736 | out() << "<dl>\n" ; |
737 | } else if (atom->string() == ATOM_LIST_VALUE) { |
738 | out() << R"(<div class="table"><table class="valuelist">)" ; |
739 | m_threeColumnEnumValueTable = isThreeColumnEnumValueTable(atom); |
740 | if (m_threeColumnEnumValueTable) { |
741 | if (++m_numTableRows % 2 == 1) |
742 | out() << R"(<tr valign="top" class="odd">)" ; |
743 | else |
744 | out() << R"(<tr valign="top" class="even">)" ; |
745 | |
746 | out() << "<th class=\"tblConst\">Constant</th>" ; |
747 | |
748 | // If not in \enum topic, skip the value column |
749 | if (relative->isEnumType()) |
750 | out() << "<th class=\"tblval\">Value</th>" ; |
751 | |
752 | out() << "<th class=\"tbldscr\">Description</th></tr>\n" ; |
753 | } else { |
754 | out() << "<tr><th class=\"tblConst\">Constant</th><th " |
755 | "class=\"tblVal\">Value</th></tr>\n" ; |
756 | } |
757 | } else { |
758 | QString olType; |
759 | if (atom->string() == ATOM_LIST_UPPERALPHA) { |
760 | olType = "A" ; |
761 | } else if (atom->string() == ATOM_LIST_LOWERALPHA) { |
762 | olType = "a" ; |
763 | } else if (atom->string() == ATOM_LIST_UPPERROMAN) { |
764 | olType = "I" ; |
765 | } else if (atom->string() == ATOM_LIST_LOWERROMAN) { |
766 | olType = "i" ; |
767 | } else { // (atom->string() == ATOM_LIST_NUMERIC) |
768 | olType = "1" ; |
769 | } |
770 | |
771 | if (atom->next() != nullptr && atom->next()->string().toInt() > 1) { |
772 | out() << QString(R"(<ol class="%1" type="%1" start="%2">)" ) |
773 | .arg(args&: olType, args: atom->next()->string()); |
774 | } else |
775 | out() << QString(R"(<ol class="%1" type="%1">)" ).arg(a: olType); |
776 | } |
777 | break; |
778 | case Atom::ListItemNumber: |
779 | break; |
780 | case Atom::ListTagLeft: |
781 | if (atom->string() == ATOM_LIST_TAG) { |
782 | out() << "<dt>" ; |
783 | } else { // (atom->string() == ATOM_LIST_VALUE) |
784 | std::pair<QString, int> pair = getAtomListValue(atom); |
785 | skipAhead = pair.second; |
786 | QString t = protectEnc(string: plainCode(markedCode: marker->markedUpEnumValue(pair.first, relative))); |
787 | out() << "<tr><td class=\"topAlign\"><code translate=\"no\">" << t << "</code>" ; |
788 | |
789 | if (relative->isEnumType()) { |
790 | out() << "</td><td class=\"topAlign tblval\">" ; |
791 | const auto *enume = static_cast<const EnumNode *>(relative); |
792 | QString itemValue = enume->itemValue(name: atom->next()->string()); |
793 | if (itemValue.isEmpty()) |
794 | out() << '?'; |
795 | else |
796 | out() << "<code translate=\"no\">" << protectEnc(string: itemValue) << "</code>" ; |
797 | } |
798 | } |
799 | break; |
800 | case Atom::SinceTagRight: |
801 | case Atom::ListTagRight: |
802 | if (atom->string() == ATOM_LIST_TAG) |
803 | out() << "</dt>\n" ; |
804 | break; |
805 | case Atom::ListItemLeft: |
806 | if (atom->string() == ATOM_LIST_TAG) { |
807 | out() << "<dd>" ; |
808 | } else if (atom->string() == ATOM_LIST_VALUE) { |
809 | if (m_threeColumnEnumValueTable) { |
810 | out() << "</td><td class=\"topAlign\">" ; |
811 | if (matchAhead(atom, expectedAtomType: Atom::ListItemRight)) |
812 | out() << " " ; |
813 | } |
814 | } else { |
815 | out() << "<li>" ; |
816 | } |
817 | if (matchAhead(atom, expectedAtomType: Atom::ParaLeft)) |
818 | skipAhead = 1; |
819 | break; |
820 | case Atom::ListItemRight: |
821 | if (atom->string() == ATOM_LIST_TAG) { |
822 | out() << "</dd>\n" ; |
823 | } else if (atom->string() == ATOM_LIST_VALUE) { |
824 | out() << "</td></tr>\n" ; |
825 | } else { |
826 | out() << "</li>\n" ; |
827 | } |
828 | break; |
829 | case Atom::ListRight: |
830 | if (atom->string() == ATOM_LIST_BULLET) { |
831 | out() << "</ul>\n" ; |
832 | } else if (atom->string() == ATOM_LIST_TAG) { |
833 | out() << "</dl>\n" ; |
834 | } else if (atom->string() == ATOM_LIST_VALUE) { |
835 | out() << "</table></div>\n" ; |
836 | } else { |
837 | out() << "</ol>\n" ; |
838 | } |
839 | break; |
840 | case Atom::Nop: |
841 | break; |
842 | case Atom::ParaLeft: |
843 | out() << "<p>" ; |
844 | in_para = true; |
845 | break; |
846 | case Atom::ParaRight: |
847 | endLink(); |
848 | if (in_para) { |
849 | out() << "</p>\n" ; |
850 | in_para = false; |
851 | } |
852 | // if (!matchAhead(atom, Atom::ListItemRight) && !matchAhead(atom, Atom::TableItemRight)) |
853 | // out() << "</p>\n"; |
854 | break; |
855 | case Atom::QuotationLeft: |
856 | out() << "<blockquote>" ; |
857 | break; |
858 | case Atom::QuotationRight: |
859 | out() << "</blockquote>\n" ; |
860 | break; |
861 | case Atom::RawString: |
862 | out() << atom->string(); |
863 | break; |
864 | case Atom::SectionLeft: |
865 | case Atom::SectionRight: |
866 | break; |
867 | case Atom::SectionHeadingLeft: { |
868 | int unit = atom->string().toInt() + hOffset(node: relative); |
869 | out() << "<h" + QString::number(unit) + QLatin1Char(' ') << "id=\"" |
870 | << Utilities::asAsciiPrintable(name: Text::sectionHeading(sectionBegin: atom).toString()) << "\">" ; |
871 | m_inSectionHeading = true; |
872 | break; |
873 | } |
874 | case Atom::SectionHeadingRight: |
875 | out() << "</h" + QString::number(atom->string().toInt() + hOffset(node: relative)) + ">\n" ; |
876 | m_inSectionHeading = false; |
877 | break; |
878 | case Atom::SidebarLeft: |
879 | Q_FALLTHROUGH(); |
880 | case Atom::SidebarRight: |
881 | break; |
882 | case Atom::String: |
883 | if (m_inLink && !m_inContents && !m_inSectionHeading) { |
884 | generateLink(atom); |
885 | } else { |
886 | out() << protectEnc(string: atom->string()); |
887 | } |
888 | break; |
889 | case Atom::TableLeft: { |
890 | std::pair<QString, QString> pair = getTableWidthAttr(atom); |
891 | QString attr = pair.second; |
892 | QString width = pair.first; |
893 | |
894 | if (in_para) { |
895 | out() << "</p>\n" ; |
896 | in_para = false; |
897 | } |
898 | |
899 | out() << R"(<div class="table"><table class=")" << attr << '"'; |
900 | if (!width.isEmpty()) |
901 | out() << " width=\"" << width << '"'; |
902 | out() << ">\n " ; |
903 | m_numTableRows = 0; |
904 | } break; |
905 | case Atom::TableRight: |
906 | out() << "</table></div>\n" ; |
907 | break; |
908 | case Atom::TableHeaderLeft: |
909 | out() << "<thead><tr class=\"qt-style\">" ; |
910 | m_inTableHeader = true; |
911 | break; |
912 | case Atom::TableHeaderRight: |
913 | out() << "</tr>" ; |
914 | if (matchAhead(atom, expectedAtomType: Atom::TableHeaderLeft)) { |
915 | skipAhead = 1; |
916 | out() << "\n<tr class=\"qt-style\">" ; |
917 | } else { |
918 | out() << "</thead>\n" ; |
919 | m_inTableHeader = false; |
920 | } |
921 | break; |
922 | case Atom::TableRowLeft: |
923 | if (!atom->string().isEmpty()) |
924 | out() << "<tr " << atom->string() << '>'; |
925 | else if (++m_numTableRows % 2 == 1) |
926 | out() << R"(<tr valign="top" class="odd">)" ; |
927 | else |
928 | out() << R"(<tr valign="top" class="even">)" ; |
929 | break; |
930 | case Atom::TableRowRight: |
931 | out() << "</tr>\n" ; |
932 | break; |
933 | case Atom::TableItemLeft: { |
934 | if (m_inTableHeader) |
935 | out() << "<th " ; |
936 | else |
937 | out() << "<td " ; |
938 | |
939 | for (int i = 0; i < atom->count(); ++i) { |
940 | if (i > 0) |
941 | out() << ' '; |
942 | const QString &p = atom->string(i); |
943 | if (p.contains(c: '=')) { |
944 | out() << p; |
945 | } else { |
946 | QStringList spans = p.split(sep: QLatin1Char(',')); |
947 | if (spans.size() == 2) { |
948 | if (spans.at(i: 0) != "1" ) |
949 | out() << " colspan=\"" << spans.at(i: 0) << '"'; |
950 | if (spans.at(i: 1) != "1" ) |
951 | out() << " rowspan=\"" << spans.at(i: 1) << '"'; |
952 | } |
953 | } |
954 | } |
955 | out() << '>'; |
956 | if (matchAhead(atom, expectedAtomType: Atom::ParaLeft)) |
957 | skipAhead = 1; |
958 | } break; |
959 | case Atom::TableItemRight: |
960 | if (m_inTableHeader) |
961 | out() << "</th>" ; |
962 | else { |
963 | out() << "</td>" ; |
964 | } |
965 | if (matchAhead(atom, expectedAtomType: Atom::ParaLeft)) |
966 | skipAhead = 1; |
967 | break; |
968 | case Atom::TableOfContents: |
969 | Q_FALLTHROUGH(); |
970 | case Atom::Keyword: |
971 | break; |
972 | case Atom::Target: |
973 | out() << "<span id=\"" << Utilities::asAsciiPrintable(name: atom->string()) << "\"></span>" ; |
974 | break; |
975 | case Atom::UnhandledFormat: |
976 | out() << "<b class=\"redFont\"><Missing HTML></b>" ; |
977 | break; |
978 | case Atom::UnknownCommand: |
979 | out() << R"(<b class="redFont"><code translate=\"no\">\)" << protectEnc(string: atom->string()) << "</code></b>" ; |
980 | break; |
981 | case Atom::CodeQuoteArgument: |
982 | case Atom::CodeQuoteCommand: |
983 | case Atom::SnippetCommand: |
984 | case Atom::SnippetIdentifier: |
985 | case Atom::SnippetLocation: |
986 | // no HTML output (ignore) |
987 | break; |
988 | default: |
989 | unknownAtom(atom); |
990 | } |
991 | return skipAhead; |
992 | } |
993 | |
994 | /*! |
995 | * Return a string representing a text that exposes information about |
996 | * the user-visible groups that the \a node is part of. A user-visible |
997 | * group is a group that generates an output page, that is, a \\group |
998 | * topic exists for the group and can be linked to. |
999 | * |
1000 | * The returned string is composed of comma separated links to the |
1001 | * groups, with their title as the user-facing text, surrounded by |
1002 | * some introductory text. |
1003 | * |
1004 | * For example, if a node named N is part of the groups with title A |
1005 | * and B, the line rendered form of the line will be "N is part of the |
1006 | * A, B groups", where A and B are clickable links that target the |
1007 | * respective page of each group. |
1008 | * |
1009 | * If a node has a single group, the comma is removed for readability |
1010 | * pusposes and "groups" is expressed as a singular noun. |
1011 | * For example, "N is part of the A group". |
1012 | * |
1013 | * The returned string is empty when the node is not linked to any |
1014 | * group that has a valid link target. |
1015 | * |
1016 | * This string is used in the summary of c++ classes or qml types to |
1017 | * link them to some of the overview documentation that is generated |
1018 | * through the "\group" command. |
1019 | * |
1020 | * Note that this is currently, incorrectly, a member of |
1021 | * HtmlGenerator as it requires access to some protected/private |
1022 | * members for escaping and linking. |
1023 | */ |
1024 | QString HtmlGenerator::groupReferenceText(PageNode* node) { |
1025 | auto link_for_group = [this](const CollectionNode *group) -> QString { |
1026 | QString target{linkForNode(node: group, relative: nullptr)}; |
1027 | return (target.isEmpty()) ? protectEnc(string: group->name()) : "<a href=\"" + target + "\">" + protectEnc(string: group->fullTitle()) + "</a>" ; |
1028 | }; |
1029 | |
1030 | QString text{}; |
1031 | |
1032 | const QStringList &groups_names{node->groupNames()}; |
1033 | if (groups_names.isEmpty()) |
1034 | return text; |
1035 | |
1036 | std::vector<CollectionNode *> groups_nodes(groups_names.size(), nullptr); |
1037 | std::transform(first: groups_names.cbegin(), last: groups_names.cend(), result: groups_nodes.begin(), |
1038 | unary_op: [this](const QString &group_name) -> CollectionNode* { |
1039 | CollectionNode *group{m_qdb->groups()[group_name]}; |
1040 | m_qdb->mergeCollections(c: group); |
1041 | return (group && group->wasSeen()) ? group : nullptr; |
1042 | }); |
1043 | groups_nodes.erase(first: std::remove(first: groups_nodes.begin(), last: groups_nodes.end(), value: nullptr), last: groups_nodes.end()); |
1044 | |
1045 | if (!groups_nodes.empty()) { |
1046 | text += node->name() + " is part of " ; |
1047 | |
1048 | for (std::vector<CollectionNode *>::size_type index{0}; index < groups_nodes.size(); ++index) { |
1049 | text += link_for_group(groups_nodes[index]) + Utilities::separator(wordPosition: index, numberOfWords: groups_nodes.size()); |
1050 | } |
1051 | } |
1052 | return text; |
1053 | } |
1054 | |
1055 | /*! |
1056 | Generate a reference page for the C++ class, namespace, or |
1057 | header file documented in \a node using the code \a marker |
1058 | provided. |
1059 | */ |
1060 | void HtmlGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker *marker) |
1061 | { |
1062 | QString title; |
1063 | QString rawTitle; |
1064 | QString fullTitle; |
1065 | NamespaceNode *ns = nullptr; |
1066 | SectionVector *summarySections = nullptr; |
1067 | SectionVector *detailsSections = nullptr; |
1068 | |
1069 | Sections sections(aggregate); |
1070 | QString word = aggregate->typeWord(cap: true); |
1071 | QString templateDecl = aggregate->templateDecl(); |
1072 | if (aggregate->isNamespace()) { |
1073 | rawTitle = aggregate->plainName(); |
1074 | fullTitle = aggregate->plainFullName(); |
1075 | title = rawTitle + " Namespace" ; |
1076 | ns = static_cast<NamespaceNode *>(aggregate); |
1077 | summarySections = §ions.stdSummarySections(); |
1078 | detailsSections = §ions.stdDetailsSections(); |
1079 | } else if (aggregate->isClassNode()) { |
1080 | rawTitle = aggregate->plainName(); |
1081 | fullTitle = aggregate->plainFullName(); |
1082 | title = rawTitle + QLatin1Char(' ') + word; |
1083 | summarySections = §ions.stdCppClassSummarySections(); |
1084 | detailsSections = §ions.stdCppClassDetailsSections(); |
1085 | } else if (aggregate->isHeader()) { |
1086 | title = fullTitle = rawTitle = aggregate->fullTitle(); |
1087 | summarySections = §ions.stdSummarySections(); |
1088 | detailsSections = §ions.stdDetailsSections(); |
1089 | } |
1090 | |
1091 | Text subtitleText; |
1092 | if (rawTitle != fullTitle || !templateDecl.isEmpty()) { |
1093 | if (aggregate->isClassNode()) { |
1094 | if (!templateDecl.isEmpty()) |
1095 | subtitleText << templateDecl + QLatin1Char(' '); |
1096 | subtitleText << aggregate->typeWord(cap: false) + QLatin1Char(' '); |
1097 | const QStringList ancestors = fullTitle.split(sep: QLatin1String("::" )); |
1098 | for (const auto &a : ancestors) { |
1099 | if (a == rawTitle) { |
1100 | subtitleText << a; |
1101 | break; |
1102 | } else { |
1103 | subtitleText << Atom(Atom::AutoLink, a) << "::" ; |
1104 | } |
1105 | } |
1106 | } else { |
1107 | subtitleText << fullTitle; |
1108 | } |
1109 | } |
1110 | |
1111 | generateHeader(title, node: aggregate, marker); |
1112 | generateTableOfContents(node: aggregate, marker, sections: summarySections); |
1113 | generateTitle(title, subTitle: subtitleText, subTitleSize: SmallSubTitle, relative: aggregate, marker); |
1114 | if (ns && !ns->hasDoc() && ns->docNode()) { |
1115 | NamespaceNode *NS = ns->docNode(); |
1116 | Text brief; |
1117 | brief << "The " << ns->name() << " namespace includes the following elements from module " |
1118 | << ns->tree()->camelCaseModuleName() << ". The full namespace is " |
1119 | << "documented in module " << NS->tree()->camelCaseModuleName() |
1120 | << Atom(Atom::LinkNode, CodeMarker::stringForNode(node: NS)) |
1121 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, " here." ) |
1122 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
1123 | out() << "<p>" ; |
1124 | generateText(text: brief, relative: ns, marker); |
1125 | out() << "</p>\n" ; |
1126 | } else |
1127 | generateBrief(node: aggregate, marker); |
1128 | |
1129 | const auto parentIsClass = aggregate->parent()->isClassNode(); |
1130 | |
1131 | if (!parentIsClass) |
1132 | generateRequisites(inner: aggregate, marker); |
1133 | generateStatus(node: aggregate, marker); |
1134 | if (parentIsClass) |
1135 | generateSince(node: aggregate, marker); |
1136 | |
1137 | QString membersLink = generateAllMembersFile(section: Sections::allMembersSection(), marker); |
1138 | if (!membersLink.isEmpty()) { |
1139 | openUnorderedList(); |
1140 | out() << "<li><a href=\"" << membersLink << "\">" |
1141 | << "List of all members, including inherited members</a></li>\n" ; |
1142 | } |
1143 | QString obsoleteLink = generateObsoleteMembersFile(sections, marker); |
1144 | if (!obsoleteLink.isEmpty()) { |
1145 | openUnorderedList(); |
1146 | out() << "<li><a href=\"" << obsoleteLink << "\">" |
1147 | << "Deprecated members</a></li>\n" ; |
1148 | } |
1149 | |
1150 | if (QString groups_text{groupReferenceText(node: aggregate)}; !groups_text.isEmpty()) { |
1151 | openUnorderedList(); |
1152 | |
1153 | out() << "<li>" << groups_text << "</li>\n" ; |
1154 | } |
1155 | |
1156 | closeUnorderedList(); |
1157 | generateThreadSafeness(node: aggregate, marker); |
1158 | |
1159 | bool needOtherSection = false; |
1160 | |
1161 | for (const auto §ion : std::as_const(t&: *summarySections)) { |
1162 | if (section.members().isEmpty() && section.reimplementedMembers().isEmpty()) { |
1163 | if (!section.inheritedMembers().isEmpty()) |
1164 | needOtherSection = true; |
1165 | } else { |
1166 | if (!section.members().isEmpty()) { |
1167 | QString ref = registerRef(ref: section.title().toLower()); |
1168 | out() << "<h2 id=\"" << ref << "\">" << protectEnc(string: section.title()) << "</h2>\n" ; |
1169 | generateSection(nv: section.members(), relative: aggregate, marker); |
1170 | } |
1171 | if (!section.reimplementedMembers().isEmpty()) { |
1172 | QString name = QString("Reimplemented " ) + section.title(); |
1173 | QString ref = registerRef(ref: name.toLower()); |
1174 | out() << "<h2 id=\"" << ref << "\">" << protectEnc(string: name) << "</h2>\n" ; |
1175 | generateSection(nv: section.reimplementedMembers(), relative: aggregate, marker); |
1176 | } |
1177 | |
1178 | if (!section.inheritedMembers().isEmpty()) { |
1179 | out() << "<ul>\n" ; |
1180 | generateSectionInheritedList(section, relative: aggregate); |
1181 | out() << "</ul>\n" ; |
1182 | } |
1183 | } |
1184 | } |
1185 | |
1186 | if (needOtherSection) { |
1187 | out() << "<h3>Additional Inherited Members</h3>\n" |
1188 | "<ul>\n" ; |
1189 | |
1190 | for (const auto §ion : std::as_const(t&: *summarySections)) { |
1191 | if (section.members().isEmpty() && !section.inheritedMembers().isEmpty()) |
1192 | generateSectionInheritedList(section, relative: aggregate); |
1193 | } |
1194 | out() << "</ul>\n" ; |
1195 | } |
1196 | |
1197 | if (aggregate->doc().isEmpty()) { |
1198 | QString command = "documentation" ; |
1199 | if (aggregate->isClassNode()) |
1200 | command = R"('\class' comment)" ; |
1201 | if (!ns || ns->isDocumentedHere()) { |
1202 | aggregate->location().warning( |
1203 | QStringLiteral("No %1 for '%2'" ).arg(args&: command, args: aggregate->plainSignature())); |
1204 | } |
1205 | } else { |
1206 | generateExtractionMark(node: aggregate, markType: DetailedDescriptionMark); |
1207 | out() << "<div class=\"descr\">\n" |
1208 | << "<h2 id=\"" << registerRef(ref: "details" ) << "\">" |
1209 | << "Detailed Description" |
1210 | << "</h2>\n" ; |
1211 | generateBody(node: aggregate, marker); |
1212 | out() << "</div>\n" ; |
1213 | generateAlsoList(node: aggregate, marker); |
1214 | generateExtractionMark(node: aggregate, markType: EndMark); |
1215 | } |
1216 | |
1217 | for (const auto §ion : std::as_const(t&: *detailsSections)) { |
1218 | bool = false; |
1219 | if (section.isEmpty()) |
1220 | continue; |
1221 | |
1222 | const QList<Node *> &members = section.members(); |
1223 | for (const auto &member : members) { |
1224 | if (member->access() == Access::Private) // ### check necessary? |
1225 | continue; |
1226 | if (!headerGenerated) { |
1227 | if (!section.divClass().isEmpty()) |
1228 | out() << "<div class=\"" << section.divClass() << "\">\n" ; |
1229 | out() << "<h2>" << protectEnc(string: section.title()) << "</h2>\n" ; |
1230 | headerGenerated = true; |
1231 | } |
1232 | if (!member->isClassNode()) |
1233 | generateDetailedMember(node: member, relative: aggregate, marker); |
1234 | else { |
1235 | out() << "<h3> class " ; |
1236 | generateFullName(apparentNode: member, relative: aggregate); |
1237 | out() << "</h3>" ; |
1238 | generateBrief(node: member, marker, relative: aggregate); |
1239 | } |
1240 | |
1241 | QStringList names; |
1242 | names << member->name(); |
1243 | if (member->isFunction()) { |
1244 | const auto *func = reinterpret_cast<const FunctionNode *>(member); |
1245 | if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0) |
1246 | names.clear(); |
1247 | } else if (member->isProperty()) { |
1248 | const auto *prop = reinterpret_cast<const PropertyNode *>(member); |
1249 | if (!prop->getters().isEmpty() && !names.contains(str: prop->getters().first()->name())) |
1250 | names << prop->getters().first()->name(); |
1251 | if (!prop->setters().isEmpty()) |
1252 | names << prop->setters().first()->name(); |
1253 | if (!prop->resetters().isEmpty()) |
1254 | names << prop->resetters().first()->name(); |
1255 | if (!prop->notifiers().isEmpty()) |
1256 | names << prop->notifiers().first()->name(); |
1257 | } else if (member->isEnumType()) { |
1258 | const auto *enume = reinterpret_cast<const EnumNode *>(member); |
1259 | if (enume->flagsType()) |
1260 | names << enume->flagsType()->name(); |
1261 | const auto &enumItemNameList = enume->doc().enumItemNames(); |
1262 | const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames(); |
1263 | const auto items = QSet<QString>(enumItemNameList.cbegin(), enumItemNameList.cend()) |
1264 | - QSet<QString>(omitEnumItemNameList.cbegin(), omitEnumItemNameList.cend()); |
1265 | for (const QString &enumName : items) { |
1266 | names << plainCode(markedCode: marker->markedUpEnumValue(enumName, enume)); |
1267 | } |
1268 | } |
1269 | } |
1270 | if (headerGenerated && !section.divClass().isEmpty()) |
1271 | out() << "</div>\n" ; |
1272 | } |
1273 | generateFooter(node: aggregate); |
1274 | } |
1275 | |
1276 | void HtmlGenerator::generateProxyPage(Aggregate *aggregate, CodeMarker *marker) |
1277 | { |
1278 | Q_ASSERT(aggregate->isProxyNode()); |
1279 | |
1280 | QString title; |
1281 | QString rawTitle; |
1282 | QString fullTitle; |
1283 | Text subtitleText; |
1284 | SectionVector *summarySections = nullptr; |
1285 | SectionVector *detailsSections = nullptr; |
1286 | |
1287 | Sections sections(aggregate); |
1288 | rawTitle = aggregate->plainName(); |
1289 | fullTitle = aggregate->plainFullName(); |
1290 | title = rawTitle + " Proxy Page" ; |
1291 | summarySections = §ions.stdSummarySections(); |
1292 | detailsSections = §ions.stdDetailsSections(); |
1293 | generateHeader(title, node: aggregate, marker); |
1294 | generateTitle(title, subTitle: subtitleText, subTitleSize: SmallSubTitle, relative: aggregate, marker); |
1295 | generateBrief(node: aggregate, marker); |
1296 | for (auto it = summarySections->constBegin(); it != summarySections->constEnd(); ++it) { |
1297 | if (!it->members().isEmpty()) { |
1298 | QString ref = registerRef(ref: it->title().toLower()); |
1299 | out() << "<h2 id=\"" << ref << "\">" << protectEnc(string: it->title()) << "</h2>\n" ; |
1300 | generateSection(nv: it->members(), relative: aggregate, marker); |
1301 | } |
1302 | } |
1303 | |
1304 | if (!aggregate->doc().isEmpty()) { |
1305 | generateExtractionMark(node: aggregate, markType: DetailedDescriptionMark); |
1306 | out() << "<div class=\"descr\">\n" |
1307 | << "<h2 id=\"" << registerRef(ref: "details" ) << "\">" |
1308 | << "Detailed Description" |
1309 | << "</h2>\n" ; |
1310 | generateBody(node: aggregate, marker); |
1311 | out() << "</div>\n" ; |
1312 | generateAlsoList(node: aggregate, marker); |
1313 | generateExtractionMark(node: aggregate, markType: EndMark); |
1314 | } |
1315 | |
1316 | for (const auto §ion : std::as_const(t&: *detailsSections)) { |
1317 | if (section.isEmpty()) |
1318 | continue; |
1319 | |
1320 | if (!section.divClass().isEmpty()) |
1321 | out() << "<div class=\"" << section.divClass() << "\">\n" ; |
1322 | out() << "<h2>" << protectEnc(string: section.title()) << "</h2>\n" ; |
1323 | |
1324 | const QList<Node *> &members = section.members(); |
1325 | for (const auto &member : members) { |
1326 | if (!member->isPrivate()) { // ### check necessary? |
1327 | if (!member->isClassNode()) |
1328 | generateDetailedMember(node: member, relative: aggregate, marker); |
1329 | else { |
1330 | out() << "<h3> class " ; |
1331 | generateFullName(apparentNode: member, relative: aggregate); |
1332 | out() << "</h3>" ; |
1333 | generateBrief(node: member, marker, relative: aggregate); |
1334 | } |
1335 | |
1336 | QStringList names; |
1337 | names << member->name(); |
1338 | if (member->isFunction()) { |
1339 | const auto *func = reinterpret_cast<const FunctionNode *>(member); |
1340 | if (func->isSomeCtor() || func->isDtor() || func->overloadNumber() != 0) |
1341 | names.clear(); |
1342 | } else if (member->isEnumType()) { |
1343 | const auto *enume = reinterpret_cast<const EnumNode *>(member); |
1344 | if (enume->flagsType()) |
1345 | names << enume->flagsType()->name(); |
1346 | const auto &enumItemNameList = enume->doc().enumItemNames(); |
1347 | const auto &omitEnumItemNameList = enume->doc().omitEnumItemNames(); |
1348 | const auto items = |
1349 | QSet<QString>(enumItemNameList.cbegin(), enumItemNameList.cend()) |
1350 | - QSet<QString>(omitEnumItemNameList.cbegin(), |
1351 | omitEnumItemNameList.cend()); |
1352 | for (const QString &enumName : items) |
1353 | names << plainCode(markedCode: marker->markedUpEnumValue(enumName, enume)); |
1354 | } |
1355 | } |
1356 | } |
1357 | if (!section.divClass().isEmpty()) |
1358 | out() << "</div>\n" ; |
1359 | } |
1360 | generateFooter(node: aggregate); |
1361 | } |
1362 | |
1363 | /*! |
1364 | Generate the HTML page for a QML type. \qcn is the QML type. |
1365 | \marker is the code markeup object. |
1366 | */ |
1367 | void HtmlGenerator::generateQmlTypePage(QmlTypeNode *qcn, CodeMarker *marker) |
1368 | { |
1369 | Generator::setQmlTypeContext(qcn); |
1370 | SubTitleSize subTitleSize = LargeSubTitle; |
1371 | QString htmlTitle = qcn->fullTitle(); |
1372 | if (qcn->isQmlBasicType()) |
1373 | htmlTitle.append(s: " QML Value Type" ); |
1374 | else |
1375 | htmlTitle.append(s: " QML Type" ); |
1376 | |
1377 | |
1378 | generateHeader(title: htmlTitle, node: qcn, marker); |
1379 | Sections sections(qcn); |
1380 | generateTableOfContents(node: qcn, marker, sections: §ions.stdQmlTypeSummarySections()); |
1381 | marker = CodeMarker::markerForLanguage(lang: QLatin1String("QML" )); |
1382 | generateTitle(title: htmlTitle, subTitle: Text() << qcn->subtitle(), subTitleSize, relative: qcn, marker); |
1383 | generateBrief(node: qcn, marker); |
1384 | generateQmlRequisites(qcn, marker); |
1385 | |
1386 | QString allQmlMembersLink; |
1387 | |
1388 | // No 'All Members' file for QML value types |
1389 | if (!qcn->isQmlBasicType()) |
1390 | allQmlMembersLink = generateAllQmlMembersFile(sections, marker); |
1391 | QString obsoleteLink = generateObsoleteQmlMembersFile(sections, marker); |
1392 | if (!allQmlMembersLink.isEmpty() || !obsoleteLink.isEmpty()) { |
1393 | openUnorderedList(); |
1394 | |
1395 | if (!allQmlMembersLink.isEmpty()) { |
1396 | out() << "<li><a href=\"" << allQmlMembersLink << "\">" |
1397 | << "List of all members, including inherited members</a></li>\n" ; |
1398 | } |
1399 | if (!obsoleteLink.isEmpty()) { |
1400 | out() << "<li><a href=\"" << obsoleteLink << "\">" |
1401 | << "Deprecated members</a></li>\n" ; |
1402 | } |
1403 | } |
1404 | |
1405 | if (QString groups_text{groupReferenceText(node: qcn)}; !groups_text.isEmpty()) { |
1406 | openUnorderedList(); |
1407 | |
1408 | out() << "<li>" << groups_text << "</li>\n" ; |
1409 | } |
1410 | |
1411 | closeUnorderedList(); |
1412 | |
1413 | const QList<Section> &stdQmlTypeSummarySections = sections.stdQmlTypeSummarySections(); |
1414 | for (const auto §ion : stdQmlTypeSummarySections) { |
1415 | if (!section.isEmpty()) { |
1416 | QString ref = registerRef(ref: section.title().toLower()); |
1417 | out() << "<h2 id=\"" << ref << "\">" << protectEnc(string: section.title()) << "</h2>\n" ; |
1418 | generateQmlSummary(members: section.members(), relative: qcn, marker); |
1419 | } |
1420 | } |
1421 | |
1422 | generateExtractionMark(node: qcn, markType: DetailedDescriptionMark); |
1423 | out() << "<h2 id=\"" << registerRef(ref: "details" ) << "\">" |
1424 | << "Detailed Description" |
1425 | << "</h2>\n" ; |
1426 | generateBody(node: qcn, marker); |
1427 | generateAlsoList(node: qcn, marker); |
1428 | generateExtractionMark(node: qcn, markType: EndMark); |
1429 | |
1430 | const QList<Section> &stdQmlTypeDetailsSections = sections.stdQmlTypeDetailsSections(); |
1431 | for (const auto §ion : stdQmlTypeDetailsSections) { |
1432 | if (!section.isEmpty()) { |
1433 | out() << "<h2>" << protectEnc(string: section.title()) << "</h2>\n" ; |
1434 | const QList<Node *> &members = section.members(); |
1435 | for (const auto member : members) { |
1436 | generateDetailedQmlMember(node: member, relative: qcn, marker); |
1437 | out() << "<br/>\n" ; |
1438 | } |
1439 | } |
1440 | } |
1441 | generateFooter(node: qcn); |
1442 | Generator::setQmlTypeContext(nullptr); |
1443 | } |
1444 | |
1445 | /*! |
1446 | Generate the HTML page for an entity that doesn't map |
1447 | to any underlying parsable C++ or QML element. |
1448 | */ |
1449 | void HtmlGenerator::generatePageNode(PageNode *pn, CodeMarker *marker) |
1450 | { |
1451 | SubTitleSize subTitleSize = LargeSubTitle; |
1452 | QString fullTitle = pn->fullTitle(); |
1453 | |
1454 | generateHeader(title: fullTitle, node: pn, marker); |
1455 | /* |
1456 | Generate the TOC for the new doc format. |
1457 | Don't generate a TOC for the home page. |
1458 | */ |
1459 | if ((pn->name() != QLatin1String("index.html" ))) |
1460 | generateTableOfContents(node: pn, marker, sections: nullptr); |
1461 | |
1462 | generateTitle(title: fullTitle, subTitle: Text() << pn->subtitle(), subTitleSize, relative: pn, marker); |
1463 | if (pn->isExample()) { |
1464 | generateBrief(node: pn, marker, relative: nullptr, addLink: false); |
1465 | } |
1466 | |
1467 | generateExtractionMark(node: pn, markType: DetailedDescriptionMark); |
1468 | out() << R"(<div class="descr" id=")" << registerRef(ref: "details" ) |
1469 | << "\">\n" ; |
1470 | |
1471 | generateBody(node: pn, marker); |
1472 | out() << "</div>\n" ; |
1473 | generateAlsoList(node: pn, marker); |
1474 | generateExtractionMark(node: pn, markType: EndMark); |
1475 | |
1476 | generateFooter(node: pn); |
1477 | } |
1478 | |
1479 | /*! |
1480 | Generate the HTML page for a group, module, or QML module. |
1481 | */ |
1482 | void HtmlGenerator::generateCollectionNode(CollectionNode *cn, CodeMarker *marker) |
1483 | { |
1484 | SubTitleSize subTitleSize = LargeSubTitle; |
1485 | QString fullTitle = cn->fullTitle(); |
1486 | QString ref; |
1487 | |
1488 | generateHeader(title: fullTitle, node: cn, marker); |
1489 | generateTableOfContents(node: cn, marker, sections: nullptr); |
1490 | generateTitle(title: fullTitle, subTitle: Text() << cn->subtitle(), subTitleSize, relative: cn, marker); |
1491 | |
1492 | // Generate brief for C++ modules, status for all modules. |
1493 | if (cn->genus() != Node::DOC && cn->genus() != Node::DontCare) { |
1494 | if (cn->isModule()) |
1495 | generateBrief(node: cn, marker); |
1496 | generateStatus(node: cn, marker); |
1497 | generateSince(node: cn, marker); |
1498 | } |
1499 | |
1500 | if (cn->isModule()) { |
1501 | if (!cn->noAutoList()) { |
1502 | NodeMap nmm; |
1503 | cn->getMemberNamespaces(out&: nmm); |
1504 | if (!nmm.isEmpty()) { |
1505 | ref = registerRef(ref: "namespaces" ); |
1506 | out() << "<h2 id=\"" << ref << "\">Namespaces</h2>\n" ; |
1507 | generateAnnotatedList(relative: cn, marker, nodes: nmm.values()); |
1508 | } |
1509 | nmm.clear(); |
1510 | cn->getMemberClasses(out&: nmm); |
1511 | if (!nmm.isEmpty()) { |
1512 | ref = registerRef(ref: "classes" ); |
1513 | out() << "<h2 id=\"" << ref << "\">Classes</h2>\n" ; |
1514 | generateAnnotatedList(relative: cn, marker, nodes: nmm.values()); |
1515 | } |
1516 | } |
1517 | } |
1518 | |
1519 | if (cn->isModule() && !cn->doc().briefText().isEmpty()) { |
1520 | generateExtractionMark(node: cn, markType: DetailedDescriptionMark); |
1521 | ref = registerRef(ref: "details" ); |
1522 | out() << "<div class=\"descr\">\n" ; |
1523 | out() << "<h2 id=\"" << ref << "\">" |
1524 | << "Detailed Description" |
1525 | << "</h2>\n" ; |
1526 | } else { |
1527 | generateExtractionMark(node: cn, markType: DetailedDescriptionMark); |
1528 | out() << R"(<div class="descr" id=")" << registerRef(ref: "details" ) |
1529 | << "\">\n" ; |
1530 | } |
1531 | |
1532 | generateBody(node: cn, marker); |
1533 | out() << "</div>\n" ; |
1534 | generateAlsoList(node: cn, marker); |
1535 | generateExtractionMark(node: cn, markType: EndMark); |
1536 | |
1537 | if (!cn->noAutoList()) { |
1538 | if (cn->isGroup() || cn->isQmlModule()) |
1539 | generateAnnotatedList(relative: cn, marker, nodes: cn->members()); |
1540 | } |
1541 | generateFooter(node: cn); |
1542 | } |
1543 | |
1544 | /*! |
1545 | Generate the HTML page for a generic collection. This is usually |
1546 | a collection of C++ elements that are related to an element in |
1547 | a different module. |
1548 | */ |
1549 | void HtmlGenerator::generateGenericCollectionPage(CollectionNode *cn, CodeMarker *marker) |
1550 | { |
1551 | SubTitleSize subTitleSize = LargeSubTitle; |
1552 | QString fullTitle = cn->name(); |
1553 | |
1554 | generateHeader(title: fullTitle, node: cn, marker); |
1555 | generateTitle(title: fullTitle, subTitle: Text() << cn->subtitle(), subTitleSize, relative: cn, marker); |
1556 | |
1557 | Text brief; |
1558 | brief << "Each function or type documented here is related to a class or " |
1559 | << "namespace that is documented in a different module. The reference " |
1560 | << "page for that class or namespace will link to the function or type " |
1561 | << "on this page." ; |
1562 | out() << "<p>" ; |
1563 | generateText(text: brief, relative: cn, marker); |
1564 | out() << "</p>\n" ; |
1565 | |
1566 | const QList<Node *> members = cn->members(); |
1567 | for (const auto &member : members) |
1568 | generateDetailedMember(node: member, relative: cn, marker); |
1569 | |
1570 | generateFooter(node: cn); |
1571 | } |
1572 | |
1573 | /*! |
1574 | Returns "html" for this subclass of Generator. |
1575 | */ |
1576 | QString HtmlGenerator::fileExtension() const |
1577 | { |
1578 | return "html" ; |
1579 | } |
1580 | |
1581 | /*! |
1582 | Output a navigation bar (breadcrumbs) for the html file. |
1583 | For API reference pages, items for the navigation bar are (in order): |
1584 | \table |
1585 | \header \li Item \li Related configuration variable \li Notes |
1586 | \row \li home \li navigation.homepage \li e.g. 'Qt 6.2' |
1587 | \row \li landing \li navigation.landingpage \li Module landing page |
1588 | \row \li types \li navigation.cppclassespage (C++)\br |
1589 | navigation.qmltypespage (QML) \li Types only |
1590 | \row \li module \li n/a (automatic) \li Module page if different |
1591 | from previous item |
1592 | \row \li page \li n/a \li Current page title |
1593 | \endtable |
1594 | |
1595 | For other page types (page nodes) the navigation bar is constructed from home |
1596 | page, landing page, and the chain of PageNode::navigationParent() items (if one exists). |
1597 | This chain is constructed from the \\list structure on a page or pages defined in |
1598 | \c navigation.toctitles configuration variable. |
1599 | |
1600 | Finally, if no other navigation data exists for a page but it is a member of a |
1601 | single group (using \\ingroup), add that group page to the navigation bar. |
1602 | */ |
1603 | void HtmlGenerator::generateNavigationBar(const QString &title, const Node *node, |
1604 | CodeMarker *marker, const QString &buildversion, |
1605 | bool tableItems) |
1606 | { |
1607 | if (m_noNavigationBar || node == nullptr) |
1608 | return; |
1609 | |
1610 | Text navigationbar; |
1611 | |
1612 | // Set list item types based on the navigation bar type |
1613 | // TODO: Do we still need table items? |
1614 | Atom::AtomType itemLeft = tableItems ? Atom::TableItemLeft : Atom::ListItemLeft; |
1615 | Atom::AtomType itemRight = tableItems ? Atom::TableItemRight : Atom::ListItemRight; |
1616 | |
1617 | // Helper to add an item to navigation bar based on a string link target |
1618 | auto addNavItem = [&](const QString &link, const QString &title) { |
1619 | navigationbar << Atom(itemLeft) << Atom(Atom::NavLink, link) |
1620 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
1621 | << Atom(Atom::String, title) |
1622 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom(itemRight); |
1623 | }; |
1624 | |
1625 | // Helper to add an item to navigation bar based on a target node |
1626 | auto addNavItemNode = [&](const Node *node, const QString &title) { |
1627 | navigationbar << Atom(itemLeft) << Atom(Atom::LinkNode, CodeMarker::stringForNode(node)) |
1628 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
1629 | << Atom(Atom::String, title) |
1630 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom(itemRight); |
1631 | }; |
1632 | |
1633 | // Resolve the associated module (collection) node and its 'state' description |
1634 | const auto *moduleNode = m_qdb->getModuleNode(relative: node); |
1635 | QString moduleState; |
1636 | if (moduleNode && !moduleNode->state().isEmpty()) |
1637 | moduleState = QStringLiteral(" (%1)" ).arg(a: moduleNode->state()); |
1638 | |
1639 | if (m_hometitle == title) |
1640 | return; |
1641 | if (!m_homepage.isEmpty()) |
1642 | addNavItem(m_homepage, m_hometitle); |
1643 | if (!m_landingpage.isEmpty() && m_landingtitle != title) |
1644 | addNavItem(m_landingpage, m_landingtitle); |
1645 | |
1646 | if (node->isClassNode()) { |
1647 | if (!m_cppclassespage.isEmpty() && !m_cppclassestitle.isEmpty()) |
1648 | addNavItem(m_cppclassespage, m_cppclassestitle); |
1649 | if (!node->physicalModuleName().isEmpty()) { |
1650 | // Add explicit link to the \module page if: |
1651 | // - It's not the C++ classes page that's already added, OR |
1652 | // - It has a \modulestate associated with it |
1653 | if (moduleNode && (!moduleState.isEmpty() || moduleNode->title() != m_cppclassespage)) |
1654 | addNavItemNode(moduleNode, moduleNode->name() + moduleState); |
1655 | } |
1656 | navigationbar << Atom(itemLeft) << Atom(Atom::String, node->name()) << Atom(itemRight); |
1657 | } else if (node->isQmlType()) { |
1658 | if (!m_qmltypespage.isEmpty() && !m_qmltypestitle.isEmpty()) |
1659 | addNavItem(m_qmltypespage, m_qmltypestitle); |
1660 | // Add explicit link to the \qmlmodule page if: |
1661 | // - It's not the QML types page that's already added, OR |
1662 | // - It has a \modulestate associated with it |
1663 | if (moduleNode && (!moduleState.isEmpty() || moduleNode->title() != m_qmltypespage)) { |
1664 | addNavItemNode(moduleNode, moduleNode->name() + moduleState); |
1665 | } |
1666 | navigationbar << Atom(itemLeft) << Atom(Atom::String, node->name()) << Atom(itemRight); |
1667 | } else { |
1668 | if (node->isPageNode()) { |
1669 | auto currentNode{static_cast<const PageNode*>(node)}; |
1670 | std::deque<const Node *> navNodes; |
1671 | // Cutoff at 16 items in case there's a circular dependency |
1672 | qsizetype navItems = 0; |
1673 | while (currentNode->navigationParent() && ++navItems < 16) { |
1674 | if (std::find(first: navNodes.cbegin(), last: navNodes.cend(), |
1675 | val: currentNode->navigationParent()) == navNodes.cend()) |
1676 | navNodes.push_front(x: currentNode->navigationParent()); |
1677 | currentNode = currentNode->navigationParent(); |
1678 | } |
1679 | // If no nav. parent was found but the page is a \group member, add a link to the |
1680 | // (first) group page. |
1681 | if (navNodes.empty()) { |
1682 | const QStringList groups = static_cast<const PageNode *>(node)->groupNames(); |
1683 | for (const auto &groupName : groups) { |
1684 | const auto *groupNode = m_qdb->findNodeByNameAndType(path: QStringList{groupName}, isMatch: &Node::isGroup); |
1685 | if (groupNode && !groupNode->title().isEmpty()) { |
1686 | navNodes.push_front(x: groupNode); |
1687 | break; |
1688 | } |
1689 | } |
1690 | } |
1691 | while (!navNodes.empty()) { |
1692 | if (navNodes.front()->isPageNode()) |
1693 | addNavItemNode(navNodes.front(), navNodes.front()->title()); |
1694 | navNodes.pop_front(); |
1695 | } |
1696 | } |
1697 | if (!navigationbar.isEmpty()) { |
1698 | navigationbar << Atom(itemLeft) << Atom(Atom::String, title) << Atom(itemRight); |
1699 | } |
1700 | } |
1701 | |
1702 | generateText(text: navigationbar, relative: node, marker); |
1703 | |
1704 | if (buildversion.isEmpty()) |
1705 | return; |
1706 | |
1707 | navigationbar.clear(); |
1708 | |
1709 | if (tableItems) { |
1710 | out() << "</tr></table><table class=\"buildversion\"><tr>\n" |
1711 | << R"(<td id="buildversion" width="100%" align="right">)" ; |
1712 | } else { |
1713 | out() << "<li id=\"buildversion\">" ; |
1714 | } |
1715 | |
1716 | // Link buildversion string to navigation.landingpage |
1717 | if (!m_landingpage.isEmpty() && m_landingtitle != title) { |
1718 | navigationbar << Atom(Atom::NavLink, m_landingpage) |
1719 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
1720 | << Atom(Atom::String, buildversion) |
1721 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
1722 | generateText(text: navigationbar, relative: node, marker); |
1723 | } else { |
1724 | out() << buildversion; |
1725 | } |
1726 | if (tableItems) |
1727 | out() << "</td>\n" ; |
1728 | else |
1729 | out() << "</li>\n" ; |
1730 | } |
1731 | |
1732 | void HtmlGenerator::(const QString &title, const Node *node, CodeMarker *marker) |
1733 | { |
1734 | out() << "<!DOCTYPE html>\n" ; |
1735 | out() << QString("<html lang=\"%1\">\n" ).arg(a: naturalLanguage); |
1736 | out() << "<head>\n" ; |
1737 | out() << " <meta charset=\"utf-8\">\n" ; |
1738 | if (node && !node->doc().location().isEmpty()) |
1739 | out() << "<!-- " << node->doc().location().fileName() << " -->\n" ; |
1740 | |
1741 | if (node && !node->doc().briefText().isEmpty()) { |
1742 | out() << " <meta name=\"description\" content=\"" |
1743 | << protectEnc(string: node->doc().briefText().toString()) |
1744 | << "\">\n" ; |
1745 | } |
1746 | |
1747 | // determine the rest of the <title> element content: "title | titleSuffix version" |
1748 | QString titleSuffix; |
1749 | if (!m_landingtitle.isEmpty()) { |
1750 | // for normal pages: "title | landingtitle version" |
1751 | titleSuffix = m_landingtitle; |
1752 | } else if (!m_hometitle.isEmpty()) { |
1753 | // for pages that set the homepage title but not landing page title: |
1754 | // "title | hometitle version" |
1755 | if (title != m_hometitle) |
1756 | titleSuffix = m_hometitle; |
1757 | } else if (!m_project.isEmpty()) { |
1758 | // for projects outside of Qt or Qt 5: "title | project version" |
1759 | if (title != m_project) |
1760 | titleSuffix = m_project; |
1761 | } else |
1762 | // default: "title | Qt version" |
1763 | titleSuffix = QLatin1String("Qt " ); |
1764 | |
1765 | if (title == titleSuffix) |
1766 | titleSuffix.clear(); |
1767 | |
1768 | QString divider; |
1769 | if (!titleSuffix.isEmpty() && !title.isEmpty()) |
1770 | divider = QLatin1String(" | " ); |
1771 | |
1772 | // Generating page title |
1773 | out() << " <title>" << protectEnc(string: title) << divider << titleSuffix; |
1774 | |
1775 | // append a full version to the suffix if neither suffix nor title |
1776 | // include (a prefix of) version information |
1777 | QVersionNumber projectVersion = QVersionNumber::fromString(string: m_qdb->version()); |
1778 | if (!projectVersion.isNull()) { |
1779 | QVersionNumber titleVersion; |
1780 | static const QRegularExpression re(QLatin1String(R"(\d+\.\d+)" )); |
1781 | const QString &versionedTitle = titleSuffix.isEmpty() ? title : titleSuffix; |
1782 | auto match = re.match(subject: versionedTitle); |
1783 | if (match.hasMatch()) |
1784 | titleVersion = QVersionNumber::fromString(string: match.captured()); |
1785 | if (titleVersion.isNull() || !titleVersion.isPrefixOf(other: projectVersion)) |
1786 | out() << QLatin1Char(' ') << projectVersion.toString(); |
1787 | } |
1788 | out() << "</title>\n" ; |
1789 | |
1790 | // Include style sheet and script links. |
1791 | out() << m_headerStyles; |
1792 | out() << m_headerScripts; |
1793 | if (m_endHeader.isEmpty()) |
1794 | out() << "</head>\n<body>\n" ; |
1795 | else |
1796 | out() << m_endHeader; |
1797 | |
1798 | out() << QString(m_postHeader).replace(before: "\\" + COMMAND_VERSION, after: m_qdb->version()); |
1799 | bool usingTable = m_postHeader.trimmed().endsWith(s: QLatin1String("<tr>" )); |
1800 | generateNavigationBar(title, node, marker, buildversion: m_buildversion, tableItems: usingTable); |
1801 | out() << QString(m_postPostHeader).replace(before: "\\" + COMMAND_VERSION, after: m_qdb->version()); |
1802 | |
1803 | m_navigationLinks.clear(); |
1804 | refMap.clear(); |
1805 | |
1806 | if (node && !node->links().empty()) { |
1807 | std::pair<QString, QString> linkPair; |
1808 | std::pair<QString, QString> anchorPair; |
1809 | const Node *linkNode; |
1810 | bool useSeparator = false; |
1811 | |
1812 | if (node->links().contains(key: Node::PreviousLink)) { |
1813 | linkPair = node->links()[Node::PreviousLink]; |
1814 | linkNode = m_qdb->findNodeForTarget(target: linkPair.first, relative: node); |
1815 | if (linkNode == nullptr && !noLinkErrors()) |
1816 | node->doc().location().warning( |
1817 | QStringLiteral("Cannot link to '%1'" ).arg(a: linkPair.first)); |
1818 | if (linkNode == nullptr || linkNode == node) |
1819 | anchorPair = linkPair; |
1820 | else |
1821 | anchorPair = anchorForNode(node: linkNode); |
1822 | |
1823 | out() << R"( <link rel="prev" href=")" << anchorPair.first << "\" />\n" ; |
1824 | |
1825 | m_navigationLinks += R"(<a class="prevPage" href=")" + anchorPair.first + "\">" ; |
1826 | if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) |
1827 | m_navigationLinks += protect(string: anchorPair.second); |
1828 | else |
1829 | m_navigationLinks += protect(string: linkPair.second); |
1830 | m_navigationLinks += "</a>\n" ; |
1831 | useSeparator = !m_navigationSeparator.isEmpty(); |
1832 | } |
1833 | if (node->links().contains(key: Node::NextLink)) { |
1834 | linkPair = node->links()[Node::NextLink]; |
1835 | linkNode = m_qdb->findNodeForTarget(target: linkPair.first, relative: node); |
1836 | if (linkNode == nullptr && !noLinkErrors()) |
1837 | node->doc().location().warning( |
1838 | QStringLiteral("Cannot link to '%1'" ).arg(a: linkPair.first)); |
1839 | if (linkNode == nullptr || linkNode == node) |
1840 | anchorPair = linkPair; |
1841 | else |
1842 | anchorPair = anchorForNode(node: linkNode); |
1843 | |
1844 | out() << R"( <link rel="next" href=")" << anchorPair.first << "\" />\n" ; |
1845 | |
1846 | if (useSeparator) |
1847 | m_navigationLinks += m_navigationSeparator; |
1848 | |
1849 | m_navigationLinks += R"(<a class="nextPage" href=")" + anchorPair.first + "\">" ; |
1850 | if (linkPair.first == linkPair.second && !anchorPair.second.isEmpty()) |
1851 | m_navigationLinks += protect(string: anchorPair.second); |
1852 | else |
1853 | m_navigationLinks += protect(string: linkPair.second); |
1854 | m_navigationLinks += "</a>\n" ; |
1855 | } |
1856 | if (node->links().contains(key: Node::StartLink)) { |
1857 | linkPair = node->links()[Node::StartLink]; |
1858 | linkNode = m_qdb->findNodeForTarget(target: linkPair.first, relative: node); |
1859 | if (linkNode == nullptr && !noLinkErrors()) |
1860 | node->doc().location().warning( |
1861 | QStringLiteral("Cannot link to '%1'" ).arg(a: linkPair.first)); |
1862 | if (linkNode == nullptr || linkNode == node) |
1863 | anchorPair = linkPair; |
1864 | else |
1865 | anchorPair = anchorForNode(node: linkNode); |
1866 | out() << R"( <link rel="start" href=")" << anchorPair.first << "\" />\n" ; |
1867 | } |
1868 | } |
1869 | |
1870 | if (node && !node->links().empty()) |
1871 | out() << "<p class=\"naviNextPrevious headerNavi\">\n" << m_navigationLinks << "</p>\n" ; |
1872 | } |
1873 | |
1874 | void HtmlGenerator::generateTitle(const QString &title, const Text &subtitle, |
1875 | SubTitleSize subTitleSize, const Node *relative, |
1876 | CodeMarker *marker) |
1877 | { |
1878 | out() << QString(m_prologue).replace(before: "\\" + COMMAND_VERSION, after: m_qdb->version()); |
1879 | QString attribute; |
1880 | if (relative->genus() & Node::API) |
1881 | attribute = R"( translate="no")" ; |
1882 | |
1883 | if (!title.isEmpty()) |
1884 | out() << "<h1 class=\"title\"" << attribute << ">" << protectEnc(string: title) << "</h1>\n" ; |
1885 | if (!subtitle.isEmpty()) { |
1886 | out() << "<span" ; |
1887 | if (subTitleSize == SmallSubTitle) |
1888 | out() << " class=\"small-subtitle\"" << attribute << ">" ; |
1889 | else |
1890 | out() << " class=\"subtitle\"" << attribute << ">" ; |
1891 | generateText(text: subtitle, relative, marker); |
1892 | out() << "</span>\n" ; |
1893 | } |
1894 | } |
1895 | |
1896 | void HtmlGenerator::(const Node *node) |
1897 | { |
1898 | if (node && !node->links().empty()) |
1899 | out() << "<p class=\"naviNextPrevious footerNavi\">\n" << m_navigationLinks << "</p>\n" ; |
1900 | |
1901 | out() << QString(m_footer).replace(before: "\\" + COMMAND_VERSION, after: m_qdb->version()) |
1902 | << QString(m_address).replace(before: "\\" + COMMAND_VERSION, after: m_qdb->version()); |
1903 | |
1904 | out() << "</body>\n" ; |
1905 | out() << "</html>\n" ; |
1906 | } |
1907 | |
1908 | /*! |
1909 | Lists the required imports and includes in a table. |
1910 | The number of rows is known. |
1911 | */ |
1912 | void HtmlGenerator::generateRequisites(Aggregate *aggregate, CodeMarker *marker) |
1913 | { |
1914 | QMap<QString, Text> requisites; |
1915 | Text text; |
1916 | |
1917 | const QString = "Header" ; |
1918 | const QString sinceText = "Since" ; |
1919 | const QString inheritedBytext = "Inherited By" ; |
1920 | const QString inheritsText = "Inherits" ; |
1921 | const QString instantiatedByText = "Instantiated By" ; |
1922 | const QString qtVariableText = "qmake" ; |
1923 | const QString cmakeText = "CMake" ; |
1924 | const QString statusText = "Status" ; |
1925 | |
1926 | // The order of the requisites matter |
1927 | const QStringList requisiteorder { headerText, cmakeText, qtVariableText, sinceText, |
1928 | instantiatedByText, inheritsText, inheritedBytext, statusText }; |
1929 | |
1930 | addIncludeFileToMap(aggregate, marker, requisites, text, headerText); |
1931 | addSinceToMap(aggregate, requisites, text: &text, sinceText); |
1932 | |
1933 | if (aggregate->isClassNode() || aggregate->isNamespace()) { |
1934 | addCMakeInfoToMap(aggregate, requisites, text: &text, CMakeInfo: cmakeText); |
1935 | addQtVariableToMap(aggregate, requisites, text: &text, qtVariableText); |
1936 | } |
1937 | |
1938 | if (aggregate->isClassNode()) { |
1939 | auto *classe = dynamic_cast<ClassNode *>(aggregate); |
1940 | if (classe->qmlElement() != nullptr && !classe->isInternal()) |
1941 | addInstantiatedByToMap(requisites, text: &text, instantiatedByText, classe); |
1942 | |
1943 | addInheritsToMap(requisites, text: &text, inheritsText, classe); |
1944 | addInheritedByToMap(requisites, text: &text, inheritedBytext, classe); |
1945 | } |
1946 | |
1947 | // Add the state description (if any) to the map |
1948 | addStatusToMap(aggregate, requisites, text, statusText); |
1949 | |
1950 | if (!requisites.isEmpty()) { |
1951 | // generate the table |
1952 | generateTheTable(requisiteOrder: requisiteorder, requisites, headerText, aggregate, marker); |
1953 | } |
1954 | } |
1955 | |
1956 | /*! |
1957 | * \internal |
1958 | */ |
1959 | void HtmlGenerator::generateTheTable(const QStringList &requisiteOrder, |
1960 | const QMap<QString, Text> &requisites, |
1961 | const QString &, const Aggregate *aggregate, |
1962 | CodeMarker *marker) |
1963 | { |
1964 | out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n" ; |
1965 | |
1966 | for (auto it = requisiteOrder.constBegin(); it != requisiteOrder.constEnd(); ++it) { |
1967 | |
1968 | if (requisites.contains(key: *it)) { |
1969 | out() << "<tr>" |
1970 | << "<td class=\"memItemLeft rightAlign topAlign\"> " << *it |
1971 | << ":" |
1972 | "</td><td class=\"memItemRight bottomAlign\"> " ; |
1973 | |
1974 | if (*it == headerText) |
1975 | out() << requisites.value(key: *it).toString(); |
1976 | else |
1977 | generateText(text: requisites.value(key: *it), relative: aggregate, marker); |
1978 | out() << "</td></tr>\n" ; |
1979 | } |
1980 | } |
1981 | out() << "</table></div>\n" ; |
1982 | } |
1983 | |
1984 | /*! |
1985 | * \internal |
1986 | * Adds inherited by information to the map. |
1987 | */ |
1988 | void HtmlGenerator::addInheritedByToMap(QMap<QString, Text> &requisites, Text *text, |
1989 | const QString &inheritedBytext, ClassNode *classe) |
1990 | { |
1991 | if (!classe->derivedClasses().isEmpty()) { |
1992 | text->clear(); |
1993 | *text << Atom::ParaLeft; |
1994 | int count = appendSortedNames(text&: *text, classe, classes: classe->derivedClasses()); |
1995 | *text << Atom::ParaRight; |
1996 | if (count > 0) |
1997 | requisites.insert(key: inheritedBytext, value: *text); |
1998 | } |
1999 | } |
2000 | |
2001 | /*! |
2002 | * \internal |
2003 | * Adds base classes to the map. |
2004 | */ |
2005 | void HtmlGenerator::addInheritsToMap(QMap<QString, Text> &requisites, Text *text, |
2006 | const QString &inheritsText, ClassNode *classe) |
2007 | { |
2008 | if (!classe->baseClasses().isEmpty()) { |
2009 | int index = 0; |
2010 | text->clear(); |
2011 | const auto baseClasses = classe->baseClasses(); |
2012 | for (const auto &cls : baseClasses) { |
2013 | if (cls.m_node) { |
2014 | appendFullName(text&: *text, apparentNode: cls.m_node, relative: classe); |
2015 | |
2016 | if (cls.m_access == Access::Protected) { |
2017 | *text << " (protected)" ; |
2018 | } else if (cls.m_access == Access::Private) { |
2019 | *text << " (private)" ; |
2020 | } |
2021 | *text << Utilities::comma(wordPosition: index++, numberOfWords: classe->baseClasses().size()); |
2022 | } |
2023 | } |
2024 | *text << Atom::ParaRight; |
2025 | if (index > 0) |
2026 | requisites.insert(key: inheritsText, value: *text); |
2027 | } |
2028 | } |
2029 | |
2030 | /*! |
2031 | * \internal |
2032 | * Add the instantiated by information to the map. |
2033 | */ |
2034 | void HtmlGenerator::addInstantiatedByToMap(QMap<QString, Text> &requisites, Text *text, |
2035 | const QString &instantiatedByText, |
2036 | ClassNode *classe) const |
2037 | { |
2038 | if (text != nullptr) { |
2039 | text->clear(); |
2040 | *text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node: classe->qmlElement())) |
2041 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) |
2042 | << Atom(Atom::String, classe->qmlElement()->name()) |
2043 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
2044 | requisites.insert(key: instantiatedByText, value: *text); |
2045 | } |
2046 | } |
2047 | |
2048 | /*! |
2049 | * \internal |
2050 | * Adds the CMake package and link library information to the map. |
2051 | */ |
2052 | void HtmlGenerator::addCMakeInfoToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, |
2053 | Text *text, const QString &CMakeInfo) const |
2054 | { |
2055 | if (!aggregate->physicalModuleName().isEmpty() && text != nullptr) { |
2056 | const CollectionNode *cn = |
2057 | m_qdb->getCollectionNode(name: aggregate->physicalModuleName(), type: Node::Module); |
2058 | if (cn && !cn->qtCMakeComponent().isEmpty()) { |
2059 | text->clear(); |
2060 | const QString qtComponent = "Qt" + QString::number(QT_VERSION_MAJOR); |
2061 | const QString findPackageText = "find_package(" + qtComponent + " REQUIRED COMPONENTS " |
2062 | + cn->qtCMakeComponent() + ")" ; |
2063 | const QString targetLinkLibrariesText = "target_link_libraries(mytarget PRIVATE " |
2064 | + qtComponent + "::" + cn->qtCMakeComponent() + ")" ; |
2065 | const Atom lineBreak = Atom(Atom::RawString, " <br/>\n" ); |
2066 | *text << findPackageText << lineBreak << targetLinkLibrariesText; |
2067 | requisites.insert(key: CMakeInfo, value: *text); |
2068 | } |
2069 | } |
2070 | } |
2071 | |
2072 | /*! |
2073 | * \internal |
2074 | * Adds the Qt variable (from the \\qtvariable command) to the map. |
2075 | */ |
2076 | void HtmlGenerator::addQtVariableToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, |
2077 | Text *text, const QString &qtVariableText) const |
2078 | { |
2079 | if (!aggregate->physicalModuleName().isEmpty()) { |
2080 | const CollectionNode *cn = |
2081 | m_qdb->getCollectionNode(name: aggregate->physicalModuleName(), type: Node::Module); |
2082 | |
2083 | if (cn && !cn->qtVariable().isEmpty()) { |
2084 | text->clear(); |
2085 | *text << "QT += " + cn->qtVariable(); |
2086 | requisites.insert(key: qtVariableText, value: *text); |
2087 | } |
2088 | } |
2089 | } |
2090 | |
2091 | /*! |
2092 | * \internal |
2093 | * Adds the since information (from the \\since command) to the map. |
2094 | * |
2095 | */ |
2096 | void HtmlGenerator::addSinceToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, |
2097 | Text *text, const QString &sinceText) const |
2098 | { |
2099 | if (!aggregate->since().isEmpty() && text != nullptr) { |
2100 | text->clear(); |
2101 | *text << formatSince(node: aggregate) << Atom::ParaRight; |
2102 | requisites.insert(key: sinceText, value: *text); |
2103 | } |
2104 | } |
2105 | |
2106 | /*! |
2107 | * \internal |
2108 | * Adds the status description for \a aggregate, together with a <span> element, to the \a |
2109 | * requisites map. |
2110 | * |
2111 | * The span element can be used for adding CSS styling/icon associated with a specific status. |
2112 | * The span class name is constructed by converting the description (sans \\deprecated |
2113 | * version info) to lowercase and replacing all non-alphanum characters with hyphens. In |
2114 | * addition, the span has a class \c status. For example, |
2115 | * 'Tech Preview' -> class="status tech-preview" |
2116 | */ |
2117 | void HtmlGenerator::addStatusToMap(const Aggregate *aggregate, QMap<QString, Text> &requisites, |
2118 | Text &text, const QString &statusText) const |
2119 | { |
2120 | auto status{formatStatus(node: aggregate, qdb: m_qdb)}; |
2121 | if (!status) |
2122 | return; |
2123 | |
2124 | QString spanClass; |
2125 | if (aggregate->status() == Node::Deprecated) |
2126 | spanClass = u"deprecated"_s ; // Disregard any version info |
2127 | else |
2128 | spanClass = Utilities::asAsciiPrintable(name: status.value()); |
2129 | |
2130 | text.clear(); |
2131 | text << Atom(Atom::String, status.value()) |
2132 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_SPAN + |
2133 | "class=\"status %1\""_L1 .arg(args&: spanClass)) |
2134 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_SPAN); |
2135 | requisites.insert(key: statusText, value: text); |
2136 | } |
2137 | |
2138 | /*! |
2139 | * \internal |
2140 | * Adds the includes (from the \\includefile command) to the map. |
2141 | */ |
2142 | void HtmlGenerator::addIncludeFileToMap(const Aggregate *aggregate, CodeMarker *marker, |
2143 | QMap<QString, Text> &requisites, Text& text, |
2144 | const QString &) |
2145 | { |
2146 | if (aggregate->includeFile()) { |
2147 | text.clear(); |
2148 | text << highlightedCode( |
2149 | markedCode: indent(level: m_codeIndent, markedCode: marker->markedUpInclude(*aggregate->includeFile())), |
2150 | relative: aggregate |
2151 | ); |
2152 | |
2153 | requisites.insert(key: headerText, value: text); |
2154 | } |
2155 | } |
2156 | |
2157 | /*! |
2158 | Lists the required imports and includes in a table. |
2159 | The number of rows is known. |
2160 | */ |
2161 | void HtmlGenerator::generateQmlRequisites(QmlTypeNode *qcn, CodeMarker *marker) |
2162 | { |
2163 | if (qcn == nullptr) |
2164 | return; |
2165 | QMap<QString, Text> requisites; |
2166 | Text text; |
2167 | |
2168 | const QString importText = "Import Statement:" ; |
2169 | const QString sinceText = "Since:" ; |
2170 | const QString inheritedBytext = "Inherited By:" ; |
2171 | const QString inheritsText = "Inherits:" ; |
2172 | const QString instantiatesText = "Instantiates:" ; |
2173 | const QString statusText = "Status:" ; |
2174 | |
2175 | // add the module name and version to the map |
2176 | QString logicalModuleVersion; |
2177 | const CollectionNode *collection = qcn->logicalModule(); |
2178 | |
2179 | // skip import statement of \internal collections |
2180 | if (!qcn->logicalModuleName().isEmpty() && (!collection || !collection->isInternal() || m_showInternal)) { |
2181 | QStringList parts = QStringList() << "import" << qcn->logicalModuleName() << qcn->logicalModuleVersion(); |
2182 | text.clear(); |
2183 | text << parts.join(sep: ' ').trimmed(); |
2184 | requisites.insert(key: importText, value: text); |
2185 | } else if (!qcn->isQmlBasicType() && qcn->logicalModuleName().isEmpty()) { |
2186 | qcn->doc().location().warning(QStringLiteral("Could not resolve QML import statement for type '%1'" ).arg(a: qcn->name()), |
2187 | QStringLiteral("Maybe you forgot to use the '\\%1' command?" ).arg(COMMAND_INQMLMODULE)); |
2188 | } |
2189 | |
2190 | // add the since and project into the map |
2191 | if (!qcn->since().isEmpty()) { |
2192 | text.clear(); |
2193 | text << formatSince(node: qcn) << Atom::ParaRight; |
2194 | requisites.insert(key: sinceText, value: text); |
2195 | } |
2196 | |
2197 | // add the instantiates to the map |
2198 | ClassNode *cn = qcn->classNode(); |
2199 | if (cn && !cn->isInternal()) { |
2200 | text.clear(); |
2201 | text << Atom(Atom::LinkNode, CodeMarker::stringForNode(node: cn)); |
2202 | text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK); |
2203 | text << Atom(Atom::String, cn->name()); |
2204 | text << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK); |
2205 | requisites.insert(key: instantiatesText, value: text); |
2206 | } |
2207 | |
2208 | // add the inherits to the map |
2209 | QmlTypeNode *base = qcn->qmlBaseNode(); |
2210 | while (base && base->isInternal()) { |
2211 | base = base->qmlBaseNode(); |
2212 | } |
2213 | if (base) { |
2214 | text.clear(); |
2215 | text << Atom::ParaLeft << Atom(Atom::LinkNode, CodeMarker::stringForNode(node: base)) |
2216 | << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK) << Atom(Atom::String, base->name()) |
2217 | << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK) << Atom::ParaRight; |
2218 | requisites.insert(key: inheritsText, value: text); |
2219 | } |
2220 | |
2221 | // add the inherited-by to the map |
2222 | NodeList subs; |
2223 | QmlTypeNode::subclasses(base: qcn, subs); |
2224 | if (!subs.isEmpty()) { |
2225 | text.clear(); |
2226 | text << Atom::ParaLeft; |
2227 | int count = appendSortedQmlNames(text, base: qcn, subs); |
2228 | text << Atom::ParaRight; |
2229 | if (count > 0) |
2230 | requisites.insert(key: inheritedBytext, value: text); |
2231 | } |
2232 | |
2233 | // Add the state description (if any) to the map |
2234 | addStatusToMap(aggregate: qcn, requisites, text, statusText); |
2235 | |
2236 | // The order of the requisites matter |
2237 | const QStringList requisiteorder { importText, sinceText, instantiatesText, inheritsText, |
2238 | inheritedBytext, statusText }; |
2239 | |
2240 | if (!requisites.isEmpty()) { |
2241 | // generate the table |
2242 | out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n" ; |
2243 | for (const auto &requisite : requisiteorder) { |
2244 | |
2245 | if (requisites.contains(key: requisite)) { |
2246 | out() << "<tr>" |
2247 | << "<td class=\"memItemLeft rightAlign topAlign\"> " << requisite |
2248 | << "</td><td class=\"memItemRight bottomAlign\"> " ; |
2249 | |
2250 | if (requisite == importText) |
2251 | out() << requisites.value(key: requisite).toString(); |
2252 | else |
2253 | generateText(text: requisites.value(key: requisite), relative: qcn, marker); |
2254 | out() << "</td></tr>" ; |
2255 | } |
2256 | } |
2257 | out() << "</table></div>" ; |
2258 | } |
2259 | } |
2260 | |
2261 | void HtmlGenerator::generateBrief(const Node *node, CodeMarker *marker, const Node *relative, |
2262 | bool addLink) |
2263 | { |
2264 | Text brief = node->doc().briefText(); |
2265 | |
2266 | if (!brief.isEmpty()) { |
2267 | if (!brief.lastAtom()->string().endsWith(c: '.')) { |
2268 | brief << Atom(Atom::String, "." ); |
2269 | node->doc().location().warning( |
2270 | QStringLiteral("'\\brief' statement does not end with a full stop." )); |
2271 | } |
2272 | generateExtractionMark(node, markType: BriefMark); |
2273 | out() << "<p>" ; |
2274 | generateText(text: brief, relative: node, marker); |
2275 | |
2276 | if (addLink) { |
2277 | if (!relative || node == relative) |
2278 | out() << " <a href=\"#" ; |
2279 | else |
2280 | out() << " <a href=\"" << linkForNode(node, relative) << '#'; |
2281 | out() << registerRef(ref: "details" ) << "\">More...</a>" ; |
2282 | } |
2283 | |
2284 | out() << "</p>\n" ; |
2285 | generateExtractionMark(node, markType: EndMark); |
2286 | } |
2287 | } |
2288 | |
2289 | /*! |
2290 | Revised for the new doc format. |
2291 | Generates a table of contents beginning at \a node. |
2292 | */ |
2293 | void HtmlGenerator::generateTableOfContents(const Node *node, CodeMarker *marker, |
2294 | QList<Section> *sections) |
2295 | { |
2296 | QList<Atom *> toc; |
2297 | if (node->doc().hasTableOfContents()) |
2298 | toc = node->doc().tableOfContents(); |
2299 | if (tocDepth == 0 || (toc.isEmpty() && !sections && !node->isModule())) { |
2300 | generateSidebar(); |
2301 | return; |
2302 | } |
2303 | |
2304 | int sectionNumber = 1; |
2305 | int detailsBase = 0; |
2306 | |
2307 | // disable nested links in table of contents |
2308 | m_inContents = true; |
2309 | |
2310 | out() << "<div class=\"sidebar\">\n" ; |
2311 | out() << "<div class=\"toc\">\n" ; |
2312 | out() << "<h3 id=\"toc\">Contents</h3>\n" ; |
2313 | |
2314 | if (node->isModule()) { |
2315 | openUnorderedList(); |
2316 | if (!static_cast<const CollectionNode *>(node)->noAutoList()) { |
2317 | if (node->hasNamespaces()) { |
2318 | out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" |
2319 | << registerRef(ref: "namespaces" ) << "\">Namespaces</a></li>\n" ; |
2320 | } |
2321 | if (node->hasClasses()) { |
2322 | out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" |
2323 | << registerRef(ref: "classes" ) << "\">Classes</a></li>\n" ; |
2324 | } |
2325 | } |
2326 | out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" << registerRef(ref: "details" ) |
2327 | << "\">Detailed Description</a></li>\n" ; |
2328 | for (const auto &entry : std::as_const(t&: toc)) { |
2329 | if (entry->string().toInt() == 1) { |
2330 | detailsBase = 1; |
2331 | break; |
2332 | } |
2333 | } |
2334 | } else if (sections && (node->isClassNode() || node->isNamespace() || node->isQmlType())) { |
2335 | for (const auto §ion : std::as_const(t&: *sections)) { |
2336 | if (!section.members().isEmpty()) { |
2337 | openUnorderedList(); |
2338 | out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" |
2339 | << registerRef(ref: section.plural()) << "\">" << section.title() << "</a></li>\n" ; |
2340 | } |
2341 | if (!section.reimplementedMembers().isEmpty()) { |
2342 | openUnorderedList(); |
2343 | QString ref = QString("Reimplemented " ) + section.plural(); |
2344 | out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" |
2345 | << registerRef(ref: ref.toLower()) << "\">" |
2346 | << QString("Reimplemented " ) + section.title() << "</a></li>\n" ; |
2347 | } |
2348 | } |
2349 | if (!node->isNamespace() || node->hasDoc()) { |
2350 | openUnorderedList(); |
2351 | out() << "<li class=\"level" << sectionNumber << "\"><a href=\"#" |
2352 | << registerRef(ref: "details" ) << "\">Detailed Description</a></li>\n" ; |
2353 | } |
2354 | for (const auto &entry : toc) { |
2355 | if (entry->string().toInt() == 1) { |
2356 | detailsBase = 1; |
2357 | break; |
2358 | } |
2359 | } |
2360 | } |
2361 | |
2362 | for (const auto &atom : toc) { |
2363 | sectionNumber = atom->string().toInt() + detailsBase; |
2364 | // restrict the ToC depth to the one set by the HTML.tocdepth variable or |
2365 | // print all levels if tocDepth is not set. |
2366 | if (sectionNumber <= tocDepth || tocDepth < 0) { |
2367 | openUnorderedList(); |
2368 | int numAtoms; |
2369 | Text headingText = Text::sectionHeading(sectionBegin: atom); |
2370 | QString s = headingText.toString(); |
2371 | out() << "<li class=\"level" << sectionNumber << "\">" ; |
2372 | out() << "<a href=\"" << '#' << Utilities::asAsciiPrintable(name: s) << "\">" ; |
2373 | generateAtomList(atom: headingText.firstAtom(), relative: node, marker, generate: true, numGeneratedAtoms&: numAtoms); |
2374 | out() << "</a></li>\n" ; |
2375 | } |
2376 | } |
2377 | closeUnorderedList(); |
2378 | out() << "</div>\n" ; |
2379 | out() << R"(<div class="sidebar-content" id="sidebar-content"></div>)" ; |
2380 | out() << "</div>\n" ; |
2381 | m_inContents = false; |
2382 | m_inLink = false; |
2383 | } |
2384 | |
2385 | /*! |
2386 | Outputs a placeholder div where the style can add customized sidebar content. |
2387 | */ |
2388 | void HtmlGenerator::() |
2389 | { |
2390 | out() << "<div class=\"sidebar\">" ; |
2391 | out() << R"(<div class="sidebar-content" id="sidebar-content"></div>)" ; |
2392 | out() << "</div>\n" ; |
2393 | } |
2394 | |
2395 | QString HtmlGenerator::generateAllMembersFile(const Section §ion, CodeMarker *marker) |
2396 | { |
2397 | if (section.isEmpty()) |
2398 | return QString(); |
2399 | |
2400 | const Aggregate *aggregate = section.aggregate(); |
2401 | QString fileName = fileBase(node: aggregate) + "-members." + fileExtension(); |
2402 | beginSubPage(node: aggregate, fileName); |
2403 | QString title = "List of All Members for " + aggregate->name(); |
2404 | generateHeader(title, node: aggregate, marker); |
2405 | generateSidebar(); |
2406 | generateTitle(title, subtitle: Text(), subTitleSize: SmallSubTitle, relative: aggregate, marker); |
2407 | out() << "<p>This is the complete list of members for " ; |
2408 | generateFullName(apparentNode: aggregate, relative: nullptr); |
2409 | out() << ", including inherited members.</p>\n" ; |
2410 | |
2411 | generateSectionList(section, relative: aggregate, marker); |
2412 | |
2413 | generateFooter(); |
2414 | endSubPage(); |
2415 | return fileName; |
2416 | } |
2417 | |
2418 | /*! |
2419 | This function creates an html page on which are listed all |
2420 | the members of the QML class used to generte the \a sections, |
2421 | including the inherited members. The \a marker is used for |
2422 | formatting stuff. |
2423 | */ |
2424 | QString HtmlGenerator::generateAllQmlMembersFile(const Sections §ions, CodeMarker *marker) |
2425 | { |
2426 | |
2427 | if (sections.allMembersSection().isEmpty()) |
2428 | return QString(); |
2429 | |
2430 | const Aggregate *aggregate = sections.aggregate(); |
2431 | QString fileName = fileBase(node: aggregate) + "-members." + fileExtension(); |
2432 | beginSubPage(node: aggregate, fileName); |
2433 | QString title = "List of All Members for " + aggregate->name(); |
2434 | generateHeader(title, node: aggregate, marker); |
2435 | generateSidebar(); |
2436 | generateTitle(title, subtitle: Text(), subTitleSize: SmallSubTitle, relative: aggregate, marker); |
2437 | out() << "<p>This is the complete list of members for " ; |
2438 | generateFullName(apparentNode: aggregate, relative: nullptr); |
2439 | out() << ", including inherited members.</p>\n" ; |
2440 | |
2441 | ClassNodesList &cknl = sections.allMembersSection().classNodesList(); |
2442 | for (int i = 0; i < cknl.size(); i++) { |
2443 | ClassNodes ckn = cknl[i]; |
2444 | const QmlTypeNode *qcn = ckn.first; |
2445 | NodeVector &nodes = ckn.second; |
2446 | if (nodes.isEmpty()) |
2447 | continue; |
2448 | if (i != 0) { |
2449 | out() << "<p>The following members are inherited from " ; |
2450 | generateFullName(apparentNode: qcn, relative: nullptr); |
2451 | out() << ".</p>\n" ; |
2452 | } |
2453 | openUnorderedList(); |
2454 | for (int j = 0; j < nodes.size(); j++) { |
2455 | Node *node = nodes[j]; |
2456 | if (node->access() == Access::Private || node->isInternal()) |
2457 | continue; |
2458 | if (node->isSharingComment() && node->sharedCommentNode()->isPropertyGroup()) |
2459 | continue; |
2460 | |
2461 | std::function<void(Node *)> generate = [&](Node *n) { |
2462 | out() << "<li class=\"fn\" translate=\"no\">" ; |
2463 | generateQmlItem(node: n, relative: aggregate, marker, summary: true); |
2464 | if (n->isDefault()) |
2465 | out() << " [default]" ; |
2466 | else if (n->isAttached()) |
2467 | out() << " [attached]" ; |
2468 | // Indent property group members |
2469 | if (n->isPropertyGroup()) { |
2470 | out() << "<ul>\n" ; |
2471 | const QList<Node *> &collective = |
2472 | static_cast<SharedCommentNode *>(n)->collective(); |
2473 | std::for_each(first: collective.begin(), last: collective.end(), f: generate); |
2474 | out() << "</ul>\n" ; |
2475 | } |
2476 | out() << "</li>\n" ; |
2477 | }; |
2478 | generate(node); |
2479 | } |
2480 | closeUnorderedList(); |
2481 | } |
2482 | |
2483 | |
2484 | generateFooter(); |
2485 | endSubPage(); |
2486 | return fileName; |
2487 | } |
2488 | |
2489 | QString HtmlGenerator::generateObsoleteMembersFile(const Sections §ions, CodeMarker *marker) |
2490 | { |
2491 | SectionPtrVector summary_spv; |
2492 | SectionPtrVector details_spv; |
2493 | if (!sections.hasObsoleteMembers(summary_spv: &summary_spv, details_spv: &details_spv)) |
2494 | return QString(); |
2495 | |
2496 | Aggregate *aggregate = sections.aggregate(); |
2497 | QString title = "Obsolete Members for " + aggregate->name(); |
2498 | QString fileName = fileBase(node: aggregate) + "-obsolete." + fileExtension(); |
2499 | QString link; |
2500 | if (useOutputSubdirs() && !Generator::outputSubdir().isEmpty()) |
2501 | link = QString("../" + Generator::outputSubdir() + QLatin1Char('/')); |
2502 | link += fileName; |
2503 | |
2504 | beginSubPage(node: aggregate, fileName); |
2505 | generateHeader(title, node: aggregate, marker); |
2506 | generateSidebar(); |
2507 | generateTitle(title, subtitle: Text(), subTitleSize: SmallSubTitle, relative: aggregate, marker); |
2508 | |
2509 | out() << "<p><b>The following members of class " |
2510 | << "<a href=\"" << linkForNode(node: aggregate, relative: nullptr) << "\" translate=\"no\">" |
2511 | << protectEnc(string: aggregate->name()) << "</a>" |
2512 | << " are deprecated.</b> " |
2513 | << "They are provided to keep old source code working. " |
2514 | << "We strongly advise against using them in new code.</p>\n" ; |
2515 | |
2516 | for (const auto §ion : summary_spv) { |
2517 | out() << "<h2>" << protectEnc(string: section->title()) << "</h2>\n" ; |
2518 | generateSectionList(section: *section, relative: aggregate, marker, useObsoloteMembers: true); |
2519 | } |
2520 | |
2521 | for (const auto §ion : details_spv) { |
2522 | out() << "<h2>" << protectEnc(string: section->title()) << "</h2>\n" ; |
2523 | |
2524 | const NodeVector &members = section->obsoleteMembers(); |
2525 | for (const auto &member : members) { |
2526 | if (member->access() != Access::Private) |
2527 | generateDetailedMember(node: member, relative: aggregate, marker); |
2528 | } |
2529 | } |
2530 | |
2531 | generateFooter(); |
2532 | endSubPage(); |
2533 | return fileName; |
2534 | } |
2535 | |
2536 | /*! |
2537 | Generates a separate file where deprecated members of the QML |
2538 | type \a qcn are listed. The \a marker is used to generate |
2539 | the section lists, which are then traversed and output here. |
2540 | */ |
2541 | QString HtmlGenerator::generateObsoleteQmlMembersFile(const Sections §ions, CodeMarker *marker) |
2542 | { |
2543 | SectionPtrVector summary_spv; |
2544 | SectionPtrVector details_spv; |
2545 | if (!sections.hasObsoleteMembers(summary_spv: &summary_spv, details_spv: &details_spv)) |
2546 | return QString(); |
2547 | |
2548 | Aggregate *aggregate = sections.aggregate(); |
2549 | QString title = "Obsolete Members for " + aggregate->name(); |
2550 | QString fileName = fileBase(node: aggregate) + "-obsolete." + fileExtension(); |
2551 | QString link; |
2552 | if (useOutputSubdirs() && !Generator::outputSubdir().isEmpty()) |
2553 | link = QString("../" + Generator::outputSubdir() + QLatin1Char('/')); |
2554 | link += fileName; |
2555 | |
2556 | beginSubPage(node: aggregate, fileName); |
2557 | generateHeader(title, node: aggregate, marker); |
2558 | generateSidebar(); |
2559 | generateTitle(title, subtitle: Text(), subTitleSize: SmallSubTitle, relative: aggregate, marker); |
2560 | |
2561 | out() << "<p><b>The following members of QML type " |
2562 | << "<a href=\"" << linkForNode(node: aggregate, relative: nullptr) << "\">" |
2563 | << protectEnc(string: aggregate->name()) << "</a>" |
2564 | << " are deprecated.</b> " |
2565 | << "They are provided to keep old source code working. " |
2566 | << "We strongly advise against using them in new code.</p>\n" ; |
2567 | |
2568 | for (const auto §ion : summary_spv) { |
2569 | QString ref = registerRef(ref: section->title().toLower()); |
2570 | out() << "<h2 id=\"" << ref << "\">" << protectEnc(string: section->title()) << "</h2>\n" ; |
2571 | generateQmlSummary(members: section->obsoleteMembers(), relative: aggregate, marker); |
2572 | } |
2573 | |
2574 | for (const auto §ion : details_spv) { |
2575 | out() << "<h2>" << protectEnc(string: section->title()) << "</h2>\n" ; |
2576 | const NodeVector &members = section->obsoleteMembers(); |
2577 | for (const auto &member : members) { |
2578 | generateDetailedQmlMember(node: member, relative: aggregate, marker); |
2579 | out() << "<br/>\n" ; |
2580 | } |
2581 | } |
2582 | |
2583 | generateFooter(); |
2584 | endSubPage(); |
2585 | return fileName; |
2586 | } |
2587 | |
2588 | void HtmlGenerator::generateClassHierarchy(const Node *relative, NodeMultiMap &classMap) |
2589 | { |
2590 | if (classMap.isEmpty()) |
2591 | return; |
2592 | |
2593 | NodeMap topLevel; |
2594 | for (const auto &it : classMap) { |
2595 | auto *classe = static_cast<ClassNode *>(it); |
2596 | if (classe->baseClasses().isEmpty()) |
2597 | topLevel.insert(key: classe->name(), value: classe); |
2598 | } |
2599 | |
2600 | QStack<NodeMap> stack; |
2601 | stack.push(t: topLevel); |
2602 | |
2603 | out() << "<ul>\n" ; |
2604 | while (!stack.isEmpty()) { |
2605 | if (stack.top().isEmpty()) { |
2606 | stack.pop(); |
2607 | out() << "</ul>\n" ; |
2608 | } else { |
2609 | ClassNode *child = static_cast<ClassNode *>(*stack.top().begin()); |
2610 | out() << "<li>" ; |
2611 | generateFullName(apparentNode: child, relative); |
2612 | out() << "</li>\n" ; |
2613 | stack.top().erase(it: stack.top().begin()); |
2614 | |
2615 | NodeMap newTop; |
2616 | const auto derivedClasses = child->derivedClasses(); |
2617 | for (const RelatedClass &d : derivedClasses) { |
2618 | if (d.m_node && d.m_node->isInAPI()) |
2619 | newTop.insert(key: d.m_node->name(), value: d.m_node); |
2620 | } |
2621 | if (!newTop.isEmpty()) { |
2622 | stack.push(t: newTop); |
2623 | out() << "<ul>\n" ; |
2624 | } |
2625 | } |
2626 | } |
2627 | } |
2628 | |
2629 | /*! |
2630 | Outputs an annotated list of the nodes in \a unsortedNodes. |
2631 | A two-column table is output. |
2632 | */ |
2633 | void HtmlGenerator::generateAnnotatedList(const Node *relative, CodeMarker *marker, |
2634 | const NodeList &unsortedNodes) |
2635 | { |
2636 | if (unsortedNodes.isEmpty() || relative == nullptr) |
2637 | return; |
2638 | |
2639 | NodeMultiMap nmm; |
2640 | bool allInternal = true; |
2641 | for (auto *node : unsortedNodes) { |
2642 | if (!node->isInternal() && !node->isDeprecated()) { |
2643 | allInternal = false; |
2644 | nmm.insert(key: node->fullName(relative), value: node); |
2645 | } |
2646 | } |
2647 | if (allInternal) |
2648 | return; |
2649 | out() << "<div class=\"table\"><table class=\"annotated\">\n" ; |
2650 | int row = 0; |
2651 | NodeList nodes = nmm.values(); |
2652 | std::sort(first: nodes.begin(), last: nodes.end(), comp: Node::nodeNameLessThan); |
2653 | |
2654 | for (const auto *node : std::as_const(t&: nodes)) { |
2655 | if (++row % 2 == 1) |
2656 | out() << "<tr class=\"odd topAlign\">" ; |
2657 | else |
2658 | out() << "<tr class=\"even topAlign\">" ; |
2659 | out() << "<td class=\"tblName\" translate=\"no\"><p>" ; |
2660 | generateFullName(apparentNode: node, relative); |
2661 | out() << "</p></td>" ; |
2662 | |
2663 | if (!node->isTextPageNode()) { |
2664 | Text brief = node->doc().trimmedBriefText(className: node->name()); |
2665 | if (!brief.isEmpty()) { |
2666 | out() << "<td class=\"tblDescr\"><p>" ; |
2667 | generateText(text: brief, relative: node, marker); |
2668 | out() << "</p></td>" ; |
2669 | } else if (!node->reconstitutedBrief().isEmpty()) { |
2670 | out() << "<td class=\"tblDescr\"><p>" ; |
2671 | out() << node->reconstitutedBrief(); |
2672 | out() << "</p></td>" ; |
2673 | } |
2674 | } else { |
2675 | out() << "<td class=\"tblDescr\"><p>" ; |
2676 | if (!node->reconstitutedBrief().isEmpty()) { |
2677 | out() << node->reconstitutedBrief(); |
2678 | } else |
2679 | out() << protectEnc(string: node->doc().briefText().toString()); |
2680 | out() << "</p></td>" ; |
2681 | } |
2682 | out() << "</tr>\n" ; |
2683 | } |
2684 | out() << "</table></div>\n" ; |
2685 | } |
2686 | |
2687 | /*! |
2688 | Outputs a series of annotated lists from the nodes in \a nmm, |
2689 | divided into sections based by the key names in the multimap. |
2690 | */ |
2691 | void HtmlGenerator::generateAnnotatedLists(const Node *relative, CodeMarker *marker, |
2692 | const NodeMultiMap &nmm) |
2693 | { |
2694 | const auto &uniqueKeys = nmm.uniqueKeys(); |
2695 | for (const QString &name : uniqueKeys) { |
2696 | if (!name.isEmpty()) { |
2697 | out() << "<h2 id=\"" << registerRef(ref: name.toLower()) << "\">" << protectEnc(string: name) |
2698 | << "</h2>\n" ; |
2699 | } |
2700 | generateAnnotatedList(relative, marker, unsortedNodes: nmm.values(key: name)); |
2701 | } |
2702 | } |
2703 | |
2704 | /*! |
2705 | This function finds the common prefix of the names of all |
2706 | the classes in the class map \a nmm and then generates a |
2707 | compact list of the class names alphabetized on the part |
2708 | of the name not including the common prefix. You can tell |
2709 | the function to use \a commonPrefix as the common prefix, |
2710 | but normally you let it figure it out itself by looking at |
2711 | the name of the first and last classes in the class map |
2712 | \a nmm. |
2713 | */ |
2714 | void HtmlGenerator::generateCompactList(ListType listType, const Node *relative, |
2715 | const NodeMultiMap &nmm, bool includeAlphabet, |
2716 | const QString &commonPrefix) |
2717 | { |
2718 | if (nmm.isEmpty()) |
2719 | return; |
2720 | |
2721 | const int NumParagraphs = 37; // '0' to '9', 'A' to 'Z', '_' |
2722 | qsizetype commonPrefixLen = commonPrefix.size(); |
2723 | |
2724 | /* |
2725 | Divide the data into 37 paragraphs: 0, ..., 9, A, ..., Z, |
2726 | underscore (_). QAccel will fall in paragraph 10 (A) and |
2727 | QXtWidget in paragraph 33 (X). This is the only place where we |
2728 | assume that NumParagraphs is 37. Each paragraph is a NodeMultiMap. |
2729 | */ |
2730 | NodeMultiMap paragraph[NumParagraphs + 1]; |
2731 | QString paragraphName[NumParagraphs + 1]; |
2732 | QSet<char> usedParagraphNames; |
2733 | |
2734 | for (auto c = nmm.constBegin(); c != nmm.constEnd(); ++c) { |
2735 | QStringList pieces = c.key().split(sep: "::" ); |
2736 | int idx = commonPrefixLen; |
2737 | if (idx > 0 && !pieces.last().startsWith(s: commonPrefix, cs: Qt::CaseInsensitive)) |
2738 | idx = 0; |
2739 | QString last = pieces.last().toLower(); |
2740 | QString key = last.mid(position: idx); |
2741 | |
2742 | int paragraphNr = NumParagraphs - 1; |
2743 | |
2744 | if (key[0].digitValue() != -1) { |
2745 | paragraphNr = key[0].digitValue(); |
2746 | } else if (key[0] >= QLatin1Char('a') && key[0] <= QLatin1Char('z')) { |
2747 | paragraphNr = 10 + key[0].unicode() - 'a'; |
2748 | } |
2749 | |
2750 | paragraphName[paragraphNr] = key[0].toUpper(); |
2751 | usedParagraphNames.insert(value: key[0].toLower().cell()); |
2752 | paragraph[paragraphNr].insert(key: last, value: c.value()); |
2753 | } |
2754 | |
2755 | /* |
2756 | Each paragraph j has a size: paragraph[j].count(). In the |
2757 | discussion, we will assume paragraphs 0 to 5 will have sizes |
2758 | 3, 1, 4, 1, 5, 9. |
2759 | |
2760 | We now want to compute the paragraph offset. Paragraphs 0 to 6 |
2761 | start at offsets 0, 3, 4, 8, 9, 14, 23. |
2762 | */ |
2763 | qsizetype paragraphOffset[NumParagraphs + 1]; // 37 + 1 |
2764 | paragraphOffset[0] = 0; |
2765 | for (int i = 0; i < NumParagraphs; i++) // i = 0..36 |
2766 | paragraphOffset[i + 1] = paragraphOffset[i] + paragraph[i].size(); |
2767 | |
2768 | /* |
2769 | Output the alphabet as a row of links. |
2770 | */ |
2771 | if (includeAlphabet) { |
2772 | out() << "<p class=\"centerAlign functionIndex\" translate=\"no\"><b>" ; |
2773 | for (int i = 0; i < 26; i++) { |
2774 | QChar ch('a' + i); |
2775 | if (usedParagraphNames.contains(value: char('a' + i))) |
2776 | out() << QString("<a href=\"#%1\">%2</a> " ).arg(a: ch).arg(a: ch.toUpper()); |
2777 | } |
2778 | out() << "</b></p>\n" ; |
2779 | } |
2780 | |
2781 | /* |
2782 | Output a <div> element to contain all the <dl> elements. |
2783 | */ |
2784 | out() << "<div class=\"flowListDiv\" translate=\"no\">\n" ; |
2785 | m_numTableRows = 0; |
2786 | |
2787 | int curParNr = 0; |
2788 | int curParOffset = 0; |
2789 | QString previousName; |
2790 | bool multipleOccurrences = false; |
2791 | |
2792 | for (int i = 0; i < nmm.size(); i++) { |
2793 | while ((curParNr < NumParagraphs) && (curParOffset == paragraph[curParNr].size())) { |
2794 | ++curParNr; |
2795 | curParOffset = 0; |
2796 | } |
2797 | |
2798 | /* |
2799 | Starting a new paragraph means starting a new <dl>. |
2800 | */ |
2801 | if (curParOffset == 0) { |
2802 | if (i > 0) |
2803 | out() << "</dl>\n" ; |
2804 | if (++m_numTableRows % 2 == 1) |
2805 | out() << "<dl class=\"flowList odd\">" ; |
2806 | else |
2807 | out() << "<dl class=\"flowList even\">" ; |
2808 | out() << "<dt class=\"alphaChar\"" ; |
2809 | if (includeAlphabet) |
2810 | out() << QString(" id=\"%1\"" ).arg(a: paragraphName[curParNr][0].toLower()); |
2811 | out() << "><b>" << paragraphName[curParNr] << "</b></dt>\n" ; |
2812 | } |
2813 | |
2814 | /* |
2815 | Output a <dd> for the current offset in the current paragraph. |
2816 | */ |
2817 | out() << "<dd>" ; |
2818 | if ((curParNr < NumParagraphs) && !paragraphName[curParNr].isEmpty()) { |
2819 | NodeMultiMap::Iterator it; |
2820 | NodeMultiMap::Iterator next; |
2821 | it = paragraph[curParNr].begin(); |
2822 | for (int j = 0; j < curParOffset; j++) |
2823 | ++it; |
2824 | |
2825 | if (listType == Generic) { |
2826 | /* |
2827 | Previously, we used generateFullName() for this, but we |
2828 | require some special formatting. |
2829 | */ |
2830 | out() << "<a href=\"" << linkForNode(node: it.value(), relative) << "\">" ; |
2831 | } else if (listType == Obsolete) { |
2832 | QString fileName = fileBase(node: it.value()) + "-obsolete." + fileExtension(); |
2833 | QString link; |
2834 | if (useOutputSubdirs()) { |
2835 | link = QString("../" + it.value()->outputSubdirectory() + QLatin1Char('/')); |
2836 | } |
2837 | link += fileName; |
2838 | out() << "<a href=\"" << link << "\">" ; |
2839 | } |
2840 | |
2841 | QStringList pieces; |
2842 | if (it.value()->isQmlType()) { |
2843 | QString name = it.value()->name(); |
2844 | next = it; |
2845 | ++next; |
2846 | if (name != previousName) |
2847 | multipleOccurrences = false; |
2848 | if ((next != paragraph[curParNr].end()) && (name == next.value()->name())) { |
2849 | multipleOccurrences = true; |
2850 | previousName = name; |
2851 | } |
2852 | if (multipleOccurrences) |
2853 | name += ": " + it.value()->tree()->camelCaseModuleName(); |
2854 | pieces << name; |
2855 | } else |
2856 | pieces = it.value()->fullName(relative).split(sep: "::" ); |
2857 | out() << protectEnc(string: pieces.last()); |
2858 | out() << "</a>" ; |
2859 | if (pieces.size() > 1) { |
2860 | out() << " (" ; |
2861 | generateFullName(apparentNode: it.value()->parent(), relative); |
2862 | out() << ')'; |
2863 | } |
2864 | } |
2865 | out() << "</dd>\n" ; |
2866 | curParOffset++; |
2867 | } |
2868 | if (nmm.size() > 0) |
2869 | out() << "</dl>\n" ; |
2870 | |
2871 | out() << "</div>\n" ; |
2872 | } |
2873 | |
2874 | void HtmlGenerator::generateFunctionIndex(const Node *relative) |
2875 | { |
2876 | out() << "<p class=\"centerAlign functionIndex\" translate=\"no\"><b>" ; |
2877 | for (int i = 0; i < 26; i++) { |
2878 | QChar ch('a' + i); |
2879 | out() << QString("<a href=\"#%1\">%2</a> " ).arg(a: ch).arg(a: ch.toUpper()); |
2880 | } |
2881 | out() << "</b></p>\n" ; |
2882 | |
2883 | char nextLetter = 'a'; |
2884 | |
2885 | out() << "<ul translate=\"no\">\n" ; |
2886 | NodeMapMap &funcIndex = m_qdb->getFunctionIndex(); |
2887 | for (auto fnMap = funcIndex.constBegin(); fnMap != funcIndex.constEnd(); ++fnMap) { |
2888 | const QString &key = fnMap.key(); |
2889 | const QChar firstLetter = key.isEmpty() ? QChar('A') : key.front(); |
2890 | Q_ASSERT_X(firstLetter.unicode() < 256, "generateFunctionIndex" , |
2891 | "Only valid C++ identifiers were expected" ); |
2892 | const char currentLetter = firstLetter.isLower() ? firstLetter.unicode() : nextLetter - 1; |
2893 | |
2894 | if (currentLetter < nextLetter) { |
2895 | out() << "<li>" ; |
2896 | } else { |
2897 | // TODO: This is not covered by our tests |
2898 | while (nextLetter < currentLetter) |
2899 | out() << QStringLiteral("<li id=\"%1\"></li>" ).arg(a: nextLetter++); |
2900 | Q_ASSERT(nextLetter == currentLetter); |
2901 | out() << QStringLiteral("<li id=\"%1\">" ).arg(a: nextLetter++); |
2902 | } |
2903 | out() << protectEnc(string: key) << ':'; |
2904 | |
2905 | for (auto it = (*fnMap).constBegin(); it != (*fnMap).constEnd(); ++it) { |
2906 | out() << ' '; |
2907 | generateFullName(apparentNode: (*it)->parent(), relative, actualNode: *it); |
2908 | } |
2909 | out() << "</li>\n" ; |
2910 | } |
2911 | while (nextLetter <= 'z') |
2912 | out() << QStringLiteral("<li id=\"%1\"></li>" ).arg(a: nextLetter++); |
2913 | out() << "</ul>\n" ; |
2914 | } |
2915 | |
2916 | void HtmlGenerator::generateLegaleseList(const Node *relative, CodeMarker *marker) |
2917 | { |
2918 | TextToNodeMap &legaleseTexts = m_qdb->getLegaleseTexts(); |
2919 | for (auto it = legaleseTexts.cbegin(), end = legaleseTexts.cend(); it != end; ++it) { |
2920 | Text text = it.key(); |
2921 | generateText(text, relative, marker); |
2922 | out() << "<ul>\n" ; |
2923 | do { |
2924 | out() << "<li>" ; |
2925 | generateFullName(apparentNode: it.value(), relative); |
2926 | out() << "</li>\n" ; |
2927 | ++it; |
2928 | } while (it != legaleseTexts.constEnd() && it.key() == text); |
2929 | out() << "</ul>\n" ; |
2930 | } |
2931 | } |
2932 | |
2933 | void HtmlGenerator::generateQmlItem(const Node *node, const Node *relative, CodeMarker *marker, |
2934 | bool summary) |
2935 | { |
2936 | QString marked = marker->markedUpQmlItem(node, summary); |
2937 | static const QRegularExpression templateTag("(<[^@>]*>)" ); |
2938 | auto match = templateTag.match(subject: marked); |
2939 | if (match.hasMatch()) { |
2940 | QString contents = protectEnc(string: match.captured(nth: 1)); |
2941 | marked.replace(i: match.capturedStart(nth: 1), len: match.capturedLength(nth: 1), after: contents); |
2942 | } |
2943 | |
2944 | // Look for the _ character in the member name followed by a number (or n): |
2945 | // this is intended to be rendered as a subscript. |
2946 | static const QRegularExpression re("<@param>([a-z]+)_([0-9]+|n)</@param>" ); |
2947 | marked.replace(re, after: "<i>\\1<sub>\\2</sub></i>" ); |
2948 | // Replace some markup by HTML tags. Do both the opening and the closing tag |
2949 | // in one go (instead of <@param> and </@param> separately, for instance). |
2950 | marked.replace(before: "@param>" , after: "i>" ); |
2951 | |
2952 | marked.replace(before: "@extra>" , after: "code>" ); |
2953 | |
2954 | if (summary) { |
2955 | marked.remove(s: "<@name>" ); |
2956 | marked.remove(s: "</@name>" ); |
2957 | marked.remove(s: "<@type>" ); |
2958 | marked.remove(s: "</@type>" ); |
2959 | } |
2960 | out() << highlightedCode(markedCode: marked, relative, alignNames: false, genus: Node::QML); |
2961 | } |
2962 | |
2963 | /*! |
2964 | This function generates a simple unordered list for the members |
2965 | of collection node \a {cn}. Returns \c true if the list was |
2966 | generated (collection has members), \c false otherwise. |
2967 | */ |
2968 | bool HtmlGenerator::generateGroupList(CollectionNode *cn) |
2969 | { |
2970 | m_qdb->mergeCollections(c: cn); |
2971 | if (cn->members().isEmpty()) |
2972 | return false; |
2973 | |
2974 | NodeList members{cn->members()}; |
2975 | std::sort(first: members.begin(), last: members.end(), comp: Node::nodeNameLessThan); |
2976 | out() << "<ul>\n" ; |
2977 | for (const auto *node : std::as_const(t&: members)) { |
2978 | out() << "<li translate=\"no\">" ; |
2979 | generateFullName(apparentNode: node, relative: nullptr); |
2980 | out() << "</li>\n" ; |
2981 | } |
2982 | out() << "</ul>\n" ; |
2983 | return true; |
2984 | } |
2985 | |
2986 | void HtmlGenerator::generateList(const Node *relative, CodeMarker *marker, const QString &selector) |
2987 | { |
2988 | CNMap cnm; |
2989 | Node::NodeType type = Node::NoType; |
2990 | if (selector == QLatin1String("overviews" )) |
2991 | type = Node::Group; |
2992 | else if (selector == QLatin1String("cpp-modules" )) |
2993 | type = Node::Module; |
2994 | else if (selector == QLatin1String("qml-modules" )) |
2995 | type = Node::QmlModule; |
2996 | if (type != Node::NoType) { |
2997 | NodeList nodeList; |
2998 | m_qdb->mergeCollections(type, cnm, relative); |
2999 | const auto collectionList = cnm.values(); |
3000 | nodeList.reserve(asize: collectionList.size()); |
3001 | for (auto *collectionNode : collectionList) |
3002 | nodeList.append(t: collectionNode); |
3003 | generateAnnotatedList(relative, marker, unsortedNodes: nodeList); |
3004 | } else { |
3005 | /* |
3006 | \generatelist {selector} is only allowed in a |
3007 | comment where the topic is \group, \module, or |
3008 | \qmlmodule. |
3009 | */ |
3010 | if (relative && !relative->isCollectionNode()) { |
3011 | relative->doc().location().warning( |
3012 | QStringLiteral("\\generatelist {%1} is only allowed in \\group, " |
3013 | "\\module and \\qmlmodule comments." ) |
3014 | .arg(a: selector)); |
3015 | return; |
3016 | } |
3017 | auto *node = const_cast<Node *>(relative); |
3018 | auto *collectionNode = static_cast<CollectionNode *>(node); |
3019 | m_qdb->mergeCollections(c: collectionNode); |
3020 | generateAnnotatedList(relative: collectionNode, marker, unsortedNodes: collectionNode->members()); |
3021 | } |
3022 | } |
3023 | |
3024 | void HtmlGenerator::generateSection(const NodeVector &nv, const Node *relative, CodeMarker *marker) |
3025 | { |
3026 | bool alignNames = true; |
3027 | if (!nv.isEmpty()) { |
3028 | bool twoColumn = false; |
3029 | if (nv.first()->isProperty()) { |
3030 | twoColumn = (nv.size() >= 5); |
3031 | alignNames = false; |
3032 | } |
3033 | if (alignNames) { |
3034 | out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n" ; |
3035 | } else { |
3036 | if (twoColumn) |
3037 | out() << "<div class=\"table\"><table class=\"propsummary\" translate=\"no\">\n" |
3038 | << "<tr><td class=\"topAlign\">" ; |
3039 | out() << "<ul>\n" ; |
3040 | } |
3041 | |
3042 | int i = 0; |
3043 | for (const auto &member : nv) { |
3044 | if (member->access() == Access::Private) |
3045 | continue; |
3046 | |
3047 | if (alignNames) { |
3048 | out() << "<tr><td class=\"memItemLeft rightAlign topAlign\"> " ; |
3049 | } else { |
3050 | if (twoColumn && i == (nv.size() + 1) / 2) |
3051 | out() << "</ul></td><td class=\"topAlign\"><ul>\n" ; |
3052 | out() << "<li class=\"fn\" translate=\"no\">" ; |
3053 | } |
3054 | |
3055 | generateSynopsis(node: member, relative, marker, style: Section::Summary, alignNames); |
3056 | if (alignNames) |
3057 | out() << "</td></tr>\n" ; |
3058 | else |
3059 | out() << "</li>\n" ; |
3060 | i++; |
3061 | } |
3062 | if (alignNames) |
3063 | out() << "</table></div>\n" ; |
3064 | else { |
3065 | out() << "</ul>\n" ; |
3066 | if (twoColumn) |
3067 | out() << "</td></tr>\n</table></div>\n" ; |
3068 | } |
3069 | } |
3070 | } |
3071 | |
3072 | void HtmlGenerator::generateSectionList(const Section §ion, const Node *relative, |
3073 | CodeMarker *marker, bool useObsoleteMembers) |
3074 | { |
3075 | bool alignNames = true; |
3076 | const NodeVector &members = |
3077 | (useObsoleteMembers ? section.obsoleteMembers() : section.members()); |
3078 | if (!members.isEmpty()) { |
3079 | bool hasPrivateSignals = false; |
3080 | bool isInvokable = false; |
3081 | bool twoColumn = false; |
3082 | if (section.style() == Section::AllMembers) { |
3083 | alignNames = false; |
3084 | twoColumn = (members.size() >= 16); |
3085 | } else if (members.first()->isProperty()) { |
3086 | twoColumn = (members.size() >= 5); |
3087 | alignNames = false; |
3088 | } |
3089 | if (alignNames) { |
3090 | out() << "<div class=\"table\"><table class=\"alignedsummary\" translate=\"no\">\n" ; |
3091 | } else { |
3092 | if (twoColumn) |
3093 | out() << "<div class=\"table\"><table class=\"propsummary\" translate=\"no\">\n" |
3094 | << "<tr><td class=\"topAlign\">" ; |
3095 | out() << "<ul>\n" ; |
3096 | } |
3097 | |
3098 | int i = 0; |
3099 | for (const auto &member : members) { |
3100 | if (member->access() == Access::Private) |
3101 | continue; |
3102 | |
3103 | if (alignNames) { |
3104 | out() << "<tr><td class=\"memItemLeft topAlign rightAlign\"> " ; |
3105 | } else { |
3106 | if (twoColumn && i == (members.size() + 1) / 2) |
3107 | out() << "</ul></td><td class=\"topAlign\"><ul>\n" ; |
3108 | out() << "<li class=\"fn\" translate=\"no\">" ; |
3109 | } |
3110 | |
3111 | generateSynopsis(node: member, relative, marker, style: section.style(), alignNames); |
3112 | if (member->isFunction()) { |
3113 | const auto *fn = static_cast<const FunctionNode *>(member); |
3114 | if (fn->isPrivateSignal()) { |
3115 | hasPrivateSignals = true; |
3116 | if (alignNames) |
3117 | out() << "</td><td class=\"memItemRight bottomAlign\">[see note below]" ; |
3118 | } else if (fn->isInvokable()) { |
3119 | isInvokable = true; |
3120 | if (alignNames) |
3121 | out() << "</td><td class=\"memItemRight bottomAlign\">[see note below]" ; |
3122 | } |
3123 | } |
3124 | if (alignNames) |
3125 | out() << "</td></tr>\n" ; |
3126 | else |
3127 | out() << "</li>\n" ; |
3128 | i++; |
3129 | } |
3130 | if (alignNames) |
3131 | out() << "</table></div>\n" ; |
3132 | else { |
3133 | out() << "</ul>\n" ; |
3134 | if (twoColumn) |
3135 | out() << "</td></tr>\n</table></div>\n" ; |
3136 | } |
3137 | if (alignNames) { |
3138 | if (hasPrivateSignals) |
3139 | generateAddendum(node: relative, type: Generator::PrivateSignal, marker); |
3140 | if (isInvokable) |
3141 | generateAddendum(node: relative, type: Generator::Invokable, marker); |
3142 | } |
3143 | } |
3144 | |
3145 | if (!useObsoleteMembers && section.style() == Section::Summary |
3146 | && !section.inheritedMembers().isEmpty()) { |
3147 | out() << "<ul>\n" ; |
3148 | generateSectionInheritedList(section, relative); |
3149 | out() << "</ul>\n" ; |
3150 | } |
3151 | } |
3152 | |
3153 | void HtmlGenerator::generateSectionInheritedList(const Section §ion, const Node *relative) |
3154 | { |
3155 | const QList<std::pair<Aggregate *, int>> &inheritedMembers = section.inheritedMembers(); |
3156 | for (const auto &member : inheritedMembers) { |
3157 | out() << "<li class=\"fn\" translate=\"no\">" ; |
3158 | out() << member.second << ' '; |
3159 | if (member.second == 1) { |
3160 | out() << section.singular(); |
3161 | } else { |
3162 | out() << section.plural(); |
3163 | } |
3164 | out() << " inherited from <a href=\"" << fileName(node: member.first) << '#' |
3165 | << Generator::cleanRef(ref: section.title().toLower()) << "\">" |
3166 | << protectEnc(string: member.first->plainFullName(relative)) << "</a></li>\n" ; |
3167 | } |
3168 | } |
3169 | |
3170 | void HtmlGenerator::generateSynopsis(const Node *node, const Node *relative, CodeMarker *marker, |
3171 | Section::Style style, bool alignNames) |
3172 | { |
3173 | QString marked = marker->markedUpSynopsis(node, relative, style); |
3174 | |
3175 | static const QRegularExpression templateTag("(<[^@>]*>)" ); |
3176 | auto match = templateTag.match(subject: marked); |
3177 | if (match.hasMatch()) { |
3178 | QString contents = protectEnc(string: match.captured(nth: 1)); |
3179 | marked.replace(i: match.capturedStart(nth: 1), len: match.capturedLength(nth: 1), after: contents); |
3180 | } |
3181 | |
3182 | static const QRegularExpression re("<@param>([a-z]+)_([1-9n])</@param>" ); |
3183 | marked.replace(re, after: "<i>\\1<sub>\\2</sub></i>" ); |
3184 | marked.replace(before: "<@param>" , after: "<i>" ); |
3185 | marked.replace(before: "</@param>" , after: "</i>" ); |
3186 | |
3187 | if (style == Section::Summary) { |
3188 | marked.remove(s: "<@name>" ); // was "<b>" |
3189 | marked.remove(s: "</@name>" ); // was "</b>" |
3190 | } |
3191 | |
3192 | if (style == Section::AllMembers) { |
3193 | static const QRegularExpression ("<@extra>.*</@extra>" , |
3194 | QRegularExpression::InvertedGreedinessOption); |
3195 | marked.remove(re: extraRegExp); |
3196 | } else { |
3197 | marked.replace(before: "<@extra>" , after: "<code translate=\"no\">" ); |
3198 | marked.replace(before: "</@extra>" , after: "</code>" ); |
3199 | } |
3200 | |
3201 | if (style != Section::Details) { |
3202 | marked.remove(s: "<@type>" ); |
3203 | marked.remove(s: "</@type>" ); |
3204 | } |
3205 | |
3206 | out() << highlightedCode(markedCode: marked, relative, alignNames); |
3207 | } |
3208 | |
3209 | QString HtmlGenerator::highlightedCode(const QString &markedCode, const Node *relative, |
3210 | bool alignNames, Node::Genus genus) |
3211 | { |
3212 | QString src = markedCode; |
3213 | QString html; |
3214 | html.reserve(asize: src.size()); |
3215 | QStringView arg; |
3216 | QStringView par1; |
3217 | |
3218 | const QChar charLangle = '<'; |
3219 | const QChar charAt = '@'; |
3220 | |
3221 | static const QString typeTag("type" ); |
3222 | static const QString ("headerfile" ); |
3223 | static const QString funcTag("func" ); |
3224 | static const QString linkTag("link" ); |
3225 | |
3226 | // replace all <@link> tags: "(<@link node=\"([^\"]+)\">).*(</@link>)" |
3227 | // replace all <@func> tags: "(<@func target=\"([^\"]*)\">)(.*)(</@func>)" |
3228 | // replace all "(<@(type|headerfile)(?: +[^>]*)?>)(.*)(</@\\2>)" tags |
3229 | bool done = false; |
3230 | for (int i = 0, srcSize = src.size(); i < srcSize;) { |
3231 | if (src.at(i) == charLangle && src.at(i: i + 1) == charAt) { |
3232 | if (alignNames && !done) { |
3233 | html += QLatin1String("</td><td class=\"memItemRight bottomAlign\">" ); |
3234 | done = true; |
3235 | } |
3236 | i += 2; |
3237 | if (parseArg(src, tag: linkTag, pos: &i, n: srcSize, contents: &arg, par1: &par1)) { |
3238 | html += QLatin1String("<b>" ); |
3239 | const Node *n = CodeMarker::nodeForString(string: par1.toString()); |
3240 | QString link = linkForNode(node: n, relative); |
3241 | addLink(linkTarget: link, nestedStuff: arg, res: &html); |
3242 | html += QLatin1String("</b>" ); |
3243 | } else if (parseArg(src, tag: funcTag, pos: &i, n: srcSize, contents: &arg, par1: &par1)) { |
3244 | const FunctionNode *fn = m_qdb->findFunctionNode(target: par1.toString(), relative, genus); |
3245 | QString link = linkForNode(node: fn, relative); |
3246 | addLink(linkTarget: link, nestedStuff: arg, res: &html); |
3247 | par1 = QStringView(); |
3248 | } else if (parseArg(src, tag: typeTag, pos: &i, n: srcSize, contents: &arg, par1: &par1)) { |
3249 | par1 = QStringView(); |
3250 | const Node *n = m_qdb->findTypeNode(type: arg.toString(), relative, genus); |
3251 | html += QLatin1String("<span class=\"type\">" ); |
3252 | if (n && (n->isQmlBasicType())) { |
3253 | if (relative && (relative->genus() == n->genus() || genus == n->genus())) |
3254 | addLink(linkTarget: linkForNode(node: n, relative), nestedStuff: arg, res: &html); |
3255 | else |
3256 | html += arg; |
3257 | } else |
3258 | addLink(linkTarget: linkForNode(node: n, relative), nestedStuff: arg, res: &html); |
3259 | html += QLatin1String("</span>" ); |
3260 | } else if (parseArg(src, tag: headerTag, pos: &i, n: srcSize, contents: &arg, par1: &par1)) { |
3261 | par1 = QStringView(); |
3262 | if (arg.startsWith(c: QLatin1Char('&'))) |
3263 | html += arg; |
3264 | else { |
3265 | const Node *n = m_qdb->findNodeForInclude(path: QStringList(arg.toString())); |
3266 | if (n && n != relative) |
3267 | addLink(linkTarget: linkForNode(node: n, relative), nestedStuff: arg, res: &html); |
3268 | else |
3269 | html += arg; |
3270 | } |
3271 | } else { |
3272 | html += charLangle; |
3273 | html += charAt; |
3274 | } |
3275 | } else { |
3276 | html += src.at(i: i++); |
3277 | } |
3278 | } |
3279 | |
3280 | // replace all |
3281 | // "<@comment>" -> "<span class=\"comment\">"; |
3282 | // "<@preprocessor>" -> "<span class=\"preprocessor\">"; |
3283 | // "<@string>" -> "<span class=\"string\">"; |
3284 | // "<@char>" -> "<span class=\"char\">"; |
3285 | // "<@number>" -> "<span class=\"number\">"; |
3286 | // "<@op>" -> "<span class=\"operator\">"; |
3287 | // "<@type>" -> "<span class=\"type\">"; |
3288 | // "<@name>" -> "<span class=\"name\">"; |
3289 | // "<@keyword>" -> "<span class=\"keyword\">"; |
3290 | // "</@(?:comment|preprocessor|string|char|number|op|type|name|keyword)>" -> "</span>" |
3291 | src = html; |
3292 | html = QString(); |
3293 | html.reserve(asize: src.size()); |
3294 | static const QLatin1String spanTags[] = { |
3295 | QLatin1String("comment>" ), QLatin1String("<span class=\"comment\">" ), |
3296 | QLatin1String("preprocessor>" ), QLatin1String("<span class=\"preprocessor\">" ), |
3297 | QLatin1String("string>" ), QLatin1String("<span class=\"string\">" ), |
3298 | QLatin1String("char>" ), QLatin1String("<span class=\"char\">" ), |
3299 | QLatin1String("number>" ), QLatin1String("<span class=\"number\">" ), |
3300 | QLatin1String("op>" ), QLatin1String("<span class=\"operator\">" ), |
3301 | QLatin1String("type>" ), QLatin1String("<span class=\"type\">" ), |
3302 | QLatin1String("name>" ), QLatin1String("<span class=\"name\">" ), |
3303 | QLatin1String("keyword>" ), QLatin1String("<span class=\"keyword\">" ) |
3304 | }; |
3305 | int nTags = 9; |
3306 | // Update the upper bound of k in the following code to match the length |
3307 | // of the above array. |
3308 | for (int i = 0, n = src.size(); i < n;) { |
3309 | if (src.at(i) == QLatin1Char('<')) { |
3310 | if (src.at(i: i + 1) == QLatin1Char('@')) { |
3311 | i += 2; |
3312 | bool handled = false; |
3313 | for (int k = 0; k != nTags; ++k) { |
3314 | const QLatin1String &tag = spanTags[2 * k]; |
3315 | if (i + tag.size() <= src.size() && tag == QStringView(src).mid(pos: i, n: tag.size())) { |
3316 | html += spanTags[2 * k + 1]; |
3317 | i += tag.size(); |
3318 | handled = true; |
3319 | break; |
3320 | } |
3321 | } |
3322 | if (!handled) { |
3323 | // drop 'our' unknown tags (the ones still containing '@') |
3324 | while (i < n && src.at(i) != QLatin1Char('>')) |
3325 | ++i; |
3326 | ++i; |
3327 | } |
3328 | continue; |
3329 | } else if (src.at(i: i + 1) == QLatin1Char('/') && src.at(i: i + 2) == QLatin1Char('@')) { |
3330 | i += 3; |
3331 | bool handled = false; |
3332 | for (int k = 0; k != nTags; ++k) { |
3333 | const QLatin1String &tag = spanTags[2 * k]; |
3334 | if (i + tag.size() <= src.size() && tag == QStringView(src).mid(pos: i, n: tag.size())) { |
3335 | html += QLatin1String("</span>" ); |
3336 | i += tag.size(); |
3337 | handled = true; |
3338 | break; |
3339 | } |
3340 | } |
3341 | if (!handled) { |
3342 | // drop 'our' unknown tags (the ones still containing '@') |
3343 | while (i < n && src.at(i) != QLatin1Char('>')) |
3344 | ++i; |
3345 | ++i; |
3346 | } |
3347 | continue; |
3348 | } |
3349 | } |
3350 | html += src.at(i); |
3351 | ++i; |
3352 | } |
3353 | return html; |
3354 | } |
3355 | |
3356 | void HtmlGenerator::generateLink(const Atom *atom) |
3357 | { |
3358 | Q_ASSERT(m_inLink); |
3359 | |
3360 | if (m_linkNode && m_linkNode->isFunction()) { |
3361 | auto match = XmlGenerator::m_funcLeftParen.match(subject: atom->string()); |
3362 | if (match.hasMatch()) { |
3363 | // C++: move () outside of link |
3364 | qsizetype leftParenLoc = match.capturedStart(nth: 1); |
3365 | out() << protectEnc(string: atom->string().left(n: leftParenLoc)); |
3366 | endLink(); |
3367 | out() << protectEnc(string: atom->string().mid(position: leftParenLoc)); |
3368 | return; |
3369 | } |
3370 | } |
3371 | out() << protectEnc(string: atom->string()); |
3372 | } |
3373 | |
3374 | QString HtmlGenerator::protectEnc(const QString &string) |
3375 | { |
3376 | return protect(string); |
3377 | } |
3378 | |
3379 | QString HtmlGenerator::protect(const QString &string) |
3380 | { |
3381 | #define APPEND(x) \ |
3382 | if (html.isEmpty()) { \ |
3383 | html = string; \ |
3384 | html.truncate(i); \ |
3385 | } \ |
3386 | html += (x); |
3387 | |
3388 | QString html; |
3389 | qsizetype n = string.size(); |
3390 | |
3391 | for (int i = 0; i < n; ++i) { |
3392 | QChar ch = string.at(i); |
3393 | |
3394 | if (ch == QLatin1Char('&')) { |
3395 | APPEND("&" ); |
3396 | } else if (ch == QLatin1Char('<')) { |
3397 | APPEND("<" ); |
3398 | } else if (ch == QLatin1Char('>')) { |
3399 | APPEND(">" ); |
3400 | } else if (ch == QChar(8211)) { |
3401 | APPEND("–" ); |
3402 | } else if (ch == QChar(8212)) { |
3403 | APPEND("—" ); |
3404 | } else if (ch == QLatin1Char('"')) { |
3405 | APPEND(""" ); |
3406 | } else if ((ch == QLatin1Char('*') && i + 1 < n && string.at(i) == QLatin1Char('/')) |
3407 | || (ch == QLatin1Char('.') && i > 2 && string.at(i: i - 2) == QLatin1Char('.'))) { |
3408 | // we escape '*/' and the last dot in 'e.g.' and 'i.e.' for the Javadoc generator |
3409 | APPEND("&#x" ); |
3410 | html += QString::number(ch.unicode(), base: 16); |
3411 | html += QLatin1Char(';'); |
3412 | } else { |
3413 | if (!html.isEmpty()) |
3414 | html += ch; |
3415 | } |
3416 | } |
3417 | |
3418 | if (!html.isEmpty()) |
3419 | return html; |
3420 | return string; |
3421 | |
3422 | #undef APPEND |
3423 | } |
3424 | |
3425 | QString HtmlGenerator::fileBase(const Node *node) const |
3426 | { |
3427 | QString result = Generator::fileBase(node); |
3428 | if (!node->isAggregate() && node->isDeprecated()) |
3429 | result += QLatin1String("-obsolete" ); |
3430 | return result; |
3431 | } |
3432 | |
3433 | QString HtmlGenerator::fileName(const Node *node) |
3434 | { |
3435 | if (node->isExternalPage()) |
3436 | return node->name(); |
3437 | return Generator::fileName(node); |
3438 | } |
3439 | |
3440 | void HtmlGenerator::generateFullName(const Node *apparentNode, const Node *relative, |
3441 | const Node *actualNode) |
3442 | { |
3443 | if (actualNode == nullptr) |
3444 | actualNode = apparentNode; |
3445 | bool link = !linkForNode(node: actualNode, relative).isEmpty(); |
3446 | if (link) { |
3447 | out() << "<a href=\"" << linkForNode(node: actualNode, relative); |
3448 | if (actualNode->isDeprecated()) |
3449 | out() << "\" class=\"obsolete" ; |
3450 | out() << "\">" ; |
3451 | } |
3452 | out() << protectEnc(string: apparentNode->fullName(relative)); |
3453 | if (link) |
3454 | out() << "</a>" ; |
3455 | } |
3456 | |
3457 | void HtmlGenerator::generateDetailedMember(const Node *node, const PageNode *relative, |
3458 | CodeMarker *marker) |
3459 | { |
3460 | const EnumNode *etn; |
3461 | generateExtractionMark(node, markType: MemberMark); |
3462 | QString nodeRef = nullptr; |
3463 | if (node->isSharedCommentNode()) { |
3464 | const auto *scn = reinterpret_cast<const SharedCommentNode *>(node); |
3465 | const QList<Node *> &collective = scn->collective(); |
3466 | if (collective.size() > 1) |
3467 | out() << "<div class=\"fngroup\">\n" ; |
3468 | for (const auto *sharedNode : collective) { |
3469 | nodeRef = refForNode(node: sharedNode); |
3470 | out() << R"(<h3 class="fn fngroupitem" translate="no" id=")" << nodeRef << "\">" ; |
3471 | generateSynopsis(node: sharedNode, relative, marker, style: Section::Details); |
3472 | out() << "</h3>" ; |
3473 | } |
3474 | if (collective.size() > 1) |
3475 | out() << "</div>" ; |
3476 | out() << '\n'; |
3477 | } else { |
3478 | nodeRef = refForNode(node); |
3479 | if (node->isEnumType() && (etn = static_cast<const EnumNode *>(node))->flagsType()) { |
3480 | out() << R"(<h3 class="flags" id=")" << nodeRef << "\">" ; |
3481 | generateSynopsis(node: etn, relative, marker, style: Section::Details); |
3482 | out() << "<br/>" ; |
3483 | generateSynopsis(node: etn->flagsType(), relative, marker, style: Section::Details); |
3484 | out() << "</h3>\n" ; |
3485 | } else { |
3486 | out() << R"(<h3 class="fn" translate="no" id=")" << nodeRef << "\">" ; |
3487 | generateSynopsis(node, relative, marker, style: Section::Details); |
3488 | out() << "</h3>" << '\n'; |
3489 | } |
3490 | } |
3491 | |
3492 | generateStatus(node, marker); |
3493 | generateBody(node, marker); |
3494 | generateOverloadedSignal(node, marker); |
3495 | generateThreadSafeness(node, marker); |
3496 | generateSince(node, marker); |
3497 | generateNoexceptNote(node, marker); |
3498 | |
3499 | if (node->isProperty()) { |
3500 | const auto property = static_cast<const PropertyNode *>(node); |
3501 | if (property->propertyType() == PropertyNode::PropertyType::StandardProperty) { |
3502 | Section section("" , "" , "" , "" , Section::Accessors); |
3503 | |
3504 | section.appendMembers(nv: property->getters().toVector()); |
3505 | section.appendMembers(nv: property->setters().toVector()); |
3506 | section.appendMembers(nv: property->resetters().toVector()); |
3507 | |
3508 | if (!section.members().isEmpty()) { |
3509 | out() << "<p><b>Access functions:</b></p>\n" ; |
3510 | generateSectionList(section, relative: node, marker); |
3511 | } |
3512 | |
3513 | Section notifiers("" , "" , "" , "" , Section::Accessors); |
3514 | notifiers.appendMembers(nv: property->notifiers().toVector()); |
3515 | |
3516 | if (!notifiers.members().isEmpty()) { |
3517 | out() << "<p><b>Notifier signal:</b></p>\n" ; |
3518 | generateSectionList(section: notifiers, relative: node, marker); |
3519 | } |
3520 | } |
3521 | } else if (node->isEnumType()) { |
3522 | const auto *enumTypeNode = static_cast<const EnumNode *>(node); |
3523 | if (enumTypeNode->flagsType()) { |
3524 | out() << "<p>The " << protectEnc(string: enumTypeNode->flagsType()->name()) |
3525 | << " type is a typedef for " |
3526 | << "<a href=\"" << m_qflagsHref << "\">QFlags</a><" |
3527 | << protectEnc(string: enumTypeNode->name()) << ">. It stores an OR combination of " |
3528 | << protectEnc(string: enumTypeNode->name()) << " values.</p>\n" ; |
3529 | } |
3530 | } |
3531 | generateAlsoList(node, marker); |
3532 | generateExtractionMark(node, markType: EndMark); |
3533 | } |
3534 | |
3535 | /*! |
3536 | This version of the function is called when outputting the link |
3537 | to an example file or example image, where the \a link is known |
3538 | to be correct. |
3539 | */ |
3540 | void HtmlGenerator::beginLink(const QString &link) |
3541 | { |
3542 | m_link = link; |
3543 | m_inLink = true; |
3544 | m_linkNode = nullptr; |
3545 | |
3546 | if (!m_link.isEmpty()) |
3547 | out() << "<a href=\"" << m_link << "\" translate=\"no\">" ; |
3548 | } |
3549 | |
3550 | void HtmlGenerator::beginLink(const QString &link, const Node *node, const Node *relative) |
3551 | { |
3552 | m_link = link; |
3553 | m_inLink = true; |
3554 | m_linkNode = node; |
3555 | if (m_link.isEmpty()) |
3556 | return; |
3557 | |
3558 | if (node == nullptr || (relative != nullptr && node->status() == relative->status())) |
3559 | out() << "<a href=\"" << m_link << "\" translate=\"no\">" ; |
3560 | else if (node->isDeprecated()) |
3561 | out() << "<a href=\"" << m_link << "\" class=\"obsolete\" translate=\"no\">" ; |
3562 | else |
3563 | out() << "<a href=\"" << m_link << "\" translate=\"no\">" ; |
3564 | } |
3565 | |
3566 | void HtmlGenerator::endLink() |
3567 | { |
3568 | if (!m_inLink) |
3569 | return; |
3570 | |
3571 | m_inLink = false; |
3572 | m_linkNode = nullptr; |
3573 | |
3574 | if (!m_link.isEmpty()) |
3575 | out() << "</a>" ; |
3576 | } |
3577 | |
3578 | /*! |
3579 | Generates the summary list for the \a members. Only used for |
3580 | sections of QML element documentation. |
3581 | */ |
3582 | void HtmlGenerator::generateQmlSummary(const NodeVector &members, const Node *relative, |
3583 | CodeMarker *marker) |
3584 | { |
3585 | if (!members.isEmpty()) { |
3586 | out() << "<ul>\n" ; |
3587 | for (const auto &member : members) { |
3588 | out() << "<li class=\"fn\" translate=\"no\">" ; |
3589 | generateQmlItem(node: member, relative, marker, summary: true); |
3590 | if (member->isPropertyGroup()) { |
3591 | const auto *scn = static_cast<const SharedCommentNode *>(member); |
3592 | if (scn->count() > 0) { |
3593 | out() << "<ul>\n" ; |
3594 | const QList<Node *> &sharedNodes = scn->collective(); |
3595 | for (const auto &node : sharedNodes) { |
3596 | if (node->isQmlProperty()) { |
3597 | out() << "<li class=\"fn\" translate=\"no\">" ; |
3598 | generateQmlItem(node, relative, marker, summary: true); |
3599 | out() << "</li>\n" ; |
3600 | } |
3601 | } |
3602 | out() << "</ul>\n" ; |
3603 | } |
3604 | } |
3605 | out() << "</li>\n" ; |
3606 | } |
3607 | out() << "</ul>\n" ; |
3608 | } |
3609 | } |
3610 | |
3611 | /*! |
3612 | Outputs the html detailed documentation for a section |
3613 | on a QML element reference page. |
3614 | */ |
3615 | void HtmlGenerator::generateDetailedQmlMember(Node *node, const Aggregate *relative, |
3616 | CodeMarker *marker) |
3617 | { |
3618 | generateExtractionMark(node, markType: MemberMark); |
3619 | |
3620 | QString ("<div class=\"qmlproto\" translate=\"no\">\n" |
3621 | "<div class=\"table\"><table class=\"qmlname\">\n" ); |
3622 | |
3623 | QString qmlItemStart("<tr valign=\"top\" class=\"odd\" id=\"%1\">\n" |
3624 | "<td class=\"%2\"><p>\n" ); |
3625 | QString qmlItemEnd("</p></td></tr>\n" ); |
3626 | |
3627 | QString ("</table></div></div>\n" ); |
3628 | |
3629 | std::function<void(QmlPropertyNode *)> generateQmlProperty = [&](QmlPropertyNode *n) { |
3630 | out() << qmlItemStart.arg(args: refForNode(node: n), args: "tblQmlPropNode" ); |
3631 | |
3632 | QStringList ; |
3633 | if (n->isDefault()) |
3634 | extra << "default" ; |
3635 | else if (n->isReadOnly()) |
3636 | extra << "read-only" ; |
3637 | else if (n->isRequired()) |
3638 | extra << "required" ; |
3639 | else if (!n->defaultValue().isEmpty()) { |
3640 | extra << "default: " + n->defaultValue(); |
3641 | } |
3642 | |
3643 | if (!n->since().isEmpty()) { |
3644 | if (!extra.isEmpty()) |
3645 | extra.last().append(c: ','); |
3646 | extra << "since " + n->since(); |
3647 | } |
3648 | |
3649 | if (!extra.isEmpty()) |
3650 | out() << QString("<span class=\"qmlextra\">[%1] </span>" ) |
3651 | .arg(a: extra.join(sep: QLatin1Char(' '))); |
3652 | |
3653 | generateQmlItem(node: n, relative, marker, summary: false); |
3654 | out() << qmlItemEnd; |
3655 | }; |
3656 | |
3657 | std::function<void(Node *)> generateQmlMethod = [&](Node *n) { |
3658 | out() << qmlItemStart.arg(args: refForNode(node: n), args: "tblQmlFuncNode" ); |
3659 | generateSynopsis(node: n, relative, marker, style: Section::Details, alignNames: false); |
3660 | out() << qmlItemEnd; |
3661 | }; |
3662 | |
3663 | out() << "<div class=\"qmlitem\">" ; |
3664 | if (node->isPropertyGroup()) { |
3665 | const auto *scn = static_cast<const SharedCommentNode *>(node); |
3666 | out() << qmlItemHeader; |
3667 | if (!scn->name().isEmpty()) { |
3668 | const QString nodeRef = refForNode(node: scn); |
3669 | out() << R"(<tr valign="top" class="even" id=")" << nodeRef << "\">" ; |
3670 | out() << "<th class=\"centerAlign\"><p>" ; |
3671 | out() << "<b>" << scn->name() << " group</b>" ; |
3672 | out() << "</p></th></tr>\n" ; |
3673 | } |
3674 | const QList<Node *> sharedNodes = scn->collective(); |
3675 | for (const auto &sharedNode : sharedNodes) { |
3676 | if (sharedNode->isQmlProperty()) |
3677 | generateQmlProperty(static_cast<QmlPropertyNode *>(sharedNode)); |
3678 | } |
3679 | out() << qmlItemFooter; |
3680 | } else if (node->isQmlProperty()) { |
3681 | out() << qmlItemHeader; |
3682 | generateQmlProperty(static_cast<QmlPropertyNode *>(node)); |
3683 | out() << qmlItemFooter; |
3684 | } else if (node->isSharedCommentNode()) { |
3685 | const auto *scn = reinterpret_cast<const SharedCommentNode *>(node); |
3686 | const QList<Node *> &sharedNodes = scn->collective(); |
3687 | if (sharedNodes.size() > 1) |
3688 | out() << "<div class=\"fngroup\">\n" ; |
3689 | out() << qmlItemHeader; |
3690 | for (const auto &sharedNode : sharedNodes) { |
3691 | // Generate the node only if it is relevant for Qt Quick. |
3692 | if (sharedNode->isFunction(g: Node::QML)) |
3693 | generateQmlMethod(sharedNode); |
3694 | else if (sharedNode->isQmlProperty()) |
3695 | generateQmlProperty(static_cast<QmlPropertyNode *>(sharedNode)); |
3696 | } |
3697 | out() << qmlItemFooter; |
3698 | if (sharedNodes.size() > 1) |
3699 | out() << "</div>" ; // fngroup |
3700 | } else { // assume the node is a method/signal handler |
3701 | out() << qmlItemHeader; |
3702 | generateQmlMethod(node); |
3703 | out() << qmlItemFooter; |
3704 | } |
3705 | |
3706 | out() << "<div class=\"qmldoc\">" ; |
3707 | generateStatus(node, marker); |
3708 | generateBody(node, marker); |
3709 | generateThreadSafeness(node, marker); |
3710 | generateSince(node, marker); |
3711 | generateAlsoList(node, marker); |
3712 | out() << "</div></div>" ; |
3713 | generateExtractionMark(node, markType: EndMark); |
3714 | } |
3715 | |
3716 | void HtmlGenerator::(const Node *node, ExtractionMarkType markType) |
3717 | { |
3718 | if (markType != EndMark) { |
3719 | out() << "<!-- $$$" + node->name(); |
3720 | if (markType == MemberMark) { |
3721 | if (node->isFunction()) { |
3722 | const auto *func = static_cast<const FunctionNode *>(node); |
3723 | if (!func->hasAssociatedProperties()) { |
3724 | if (func->overloadNumber() == 0) |
3725 | out() << "[overload1]" ; |
3726 | out() << "$$$" + func->name() + func->parameters().rawSignature().remove(c: ' '); |
3727 | } |
3728 | } else if (node->isProperty()) { |
3729 | out() << "-prop" ; |
3730 | const auto *prop = static_cast<const PropertyNode *>(node); |
3731 | const NodeList &list = prop->functions(); |
3732 | for (const auto *propFuncNode : list) { |
3733 | if (propFuncNode->isFunction()) { |
3734 | const auto *func = static_cast<const FunctionNode *>(propFuncNode); |
3735 | out() << "$$$" + func->name() |
3736 | + func->parameters().rawSignature().remove(c: ' '); |
3737 | } |
3738 | } |
3739 | } else if (node->isEnumType()) { |
3740 | const auto *enumNode = static_cast<const EnumNode *>(node); |
3741 | const auto &items = enumNode->items(); |
3742 | for (const auto &item : items) |
3743 | out() << "$$$" + item.name(); |
3744 | } |
3745 | } else if (markType == BriefMark) { |
3746 | out() << "-brief" ; |
3747 | } else if (markType == DetailedDescriptionMark) { |
3748 | out() << "-description" ; |
3749 | } |
3750 | out() << " -->\n" ; |
3751 | } else { |
3752 | out() << "<!-- @@@" + node->name() + " -->\n" ; |
3753 | } |
3754 | } |
3755 | |
3756 | QT_END_NAMESPACE |
3757 | |