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 "webxmlgenerator.h" |
5 | |
6 | #include "aggregate.h" |
7 | #include "collectionnode.h" |
8 | #include "config.h" |
9 | #include "helpprojectwriter.h" |
10 | #include "node.h" |
11 | #include "propertynode.h" |
12 | #include "qdocdatabase.h" |
13 | #include "quoter.h" |
14 | #include "utilities.h" |
15 | |
16 | #include <QtCore/qxmlstream.h> |
17 | |
18 | QT_BEGIN_NAMESPACE |
19 | |
20 | using namespace Qt::StringLiterals; |
21 | |
22 | static CodeMarker *marker_ = nullptr; |
23 | |
24 | WebXMLGenerator::WebXMLGenerator(FileResolver& file_resolver) : HtmlGenerator(file_resolver) {} |
25 | |
26 | void WebXMLGenerator::initializeGenerator() |
27 | { |
28 | HtmlGenerator::initializeGenerator(); |
29 | } |
30 | |
31 | void WebXMLGenerator::terminateGenerator() |
32 | { |
33 | HtmlGenerator::terminateGenerator(); |
34 | } |
35 | |
36 | QString WebXMLGenerator::format() |
37 | { |
38 | return "WebXML" ; |
39 | } |
40 | |
41 | QString WebXMLGenerator::fileExtension() const |
42 | { |
43 | // As this is meant to be an intermediate format, |
44 | // use .html for internal references. The name of |
45 | // the output file is set separately in |
46 | // beginSubPage() calls. |
47 | return "html" ; |
48 | } |
49 | |
50 | /*! |
51 | Most of the output is generated by QDocIndexFiles and the append() callback. |
52 | Some pages produce supplementary output while being generated, and that's |
53 | handled here. |
54 | */ |
55 | qsizetype WebXMLGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker) |
56 | { |
57 | if (m_supplement && currentWriter) |
58 | addAtomElements(writer&: *currentWriter.data(), atom, relative, marker); |
59 | return 0; |
60 | } |
61 | |
62 | void WebXMLGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker * /* marker */) |
63 | { |
64 | QByteArray data; |
65 | QXmlStreamWriter writer(&data); |
66 | writer.setAutoFormatting(true); |
67 | beginSubPage(node: aggregate, fileName: Generator::fileName(node: aggregate, extension: "webxml" )); |
68 | writer.writeStartDocument(); |
69 | writer.writeStartElement(qualifiedName: "WebXML" ); |
70 | writer.writeStartElement(qualifiedName: "document" ); |
71 | |
72 | generateIndexSections(writer, node: aggregate); |
73 | |
74 | writer.writeEndElement(); // document |
75 | writer.writeEndElement(); // WebXML |
76 | writer.writeEndDocument(); |
77 | |
78 | out() << data; |
79 | endSubPage(); |
80 | } |
81 | |
82 | void WebXMLGenerator::generatePageNode(PageNode *pn, CodeMarker * /* marker */) |
83 | { |
84 | QByteArray data; |
85 | currentWriter.reset(other: new QXmlStreamWriter(&data)); |
86 | currentWriter->setAutoFormatting(true); |
87 | beginSubPage(node: pn, fileName: Generator::fileName(node: pn, extension: "webxml" )); |
88 | currentWriter->writeStartDocument(); |
89 | currentWriter->writeStartElement(qualifiedName: "WebXML" ); |
90 | currentWriter->writeStartElement(qualifiedName: "document" ); |
91 | |
92 | generateIndexSections(writer&: *currentWriter.data(), node: pn); |
93 | |
94 | currentWriter->writeEndElement(); // document |
95 | currentWriter->writeEndElement(); // WebXML |
96 | currentWriter->writeEndDocument(); |
97 | |
98 | out() << data; |
99 | endSubPage(); |
100 | } |
101 | |
102 | void WebXMLGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker* /* marker */) |
103 | { |
104 | // TODO: [generator-insufficient-structural-abstraction] |
105 | |
106 | QByteArray data; |
107 | QXmlStreamWriter writer(&data); |
108 | writer.setAutoFormatting(true); |
109 | beginSubPage(node: en, fileName: linkForExampleFile(path: resolved_file.get_query(), fileExt: "webxml" )); |
110 | writer.writeStartDocument(); |
111 | writer.writeStartElement(qualifiedName: "WebXML" ); |
112 | writer.writeStartElement(qualifiedName: "document" ); |
113 | writer.writeStartElement(qualifiedName: "page" ); |
114 | writer.writeAttribute(qualifiedName: "name" , value: resolved_file.get_path()); |
115 | writer.writeAttribute(qualifiedName: "href" , value: linkForExampleFile(path: resolved_file.get_path())); |
116 | QString title = exampleFileTitle(relative: static_cast<const ExampleNode *>(en), fileName: resolved_file.get_path()); |
117 | writer.writeAttribute(qualifiedName: "title" , value: title); |
118 | writer.writeAttribute(qualifiedName: "fulltitle" , value: title); |
119 | writer.writeAttribute(qualifiedName: "subtitle" , value: resolved_file.get_path()); |
120 | writer.writeStartElement(qualifiedName: "description" ); |
121 | |
122 | if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { |
123 | writer.writeAttribute(qualifiedName: "path" , value: resolved_file.get_path()); |
124 | writer.writeAttribute(qualifiedName: "line" , value: "0" ); |
125 | writer.writeAttribute(qualifiedName: "column" , value: "0" ); |
126 | } |
127 | |
128 | Quoter quoter; |
129 | Doc::quoteFromFile(location: en->doc().location(), quoter, resolved_file); |
130 | QString code = quoter.quoteTo(docLocation: en->location(), command: QString(), pattern: QString()); |
131 | writer.writeTextElement(qualifiedName: "code" , text: trimmedTrailing(string: code, prefix: QString(), suffix: QString())); |
132 | |
133 | writer.writeEndElement(); // description |
134 | writer.writeEndElement(); // page |
135 | writer.writeEndElement(); // document |
136 | writer.writeEndElement(); // WebXML |
137 | writer.writeEndDocument(); |
138 | |
139 | out() << data; |
140 | endSubPage(); |
141 | } |
142 | |
143 | void WebXMLGenerator::generateIndexSections(QXmlStreamWriter &writer, Node *node) |
144 | { |
145 | marker_ = CodeMarker::markerForFileName(fileName: node->location().filePath()); |
146 | auto qdocIndexFiles = QDocIndexFiles::qdocIndexFiles(); |
147 | if (qdocIndexFiles) { |
148 | qdocIndexFiles->generateIndexSections(writer, node, post: this); |
149 | // generateIndexSections does nothing for groups, so handle them explicitly |
150 | if (node->isGroup()) |
151 | qdocIndexFiles->generateIndexSection(writer, node, post: this); |
152 | } |
153 | } |
154 | |
155 | // Handles callbacks from QDocIndexFiles to add documentation to node |
156 | void WebXMLGenerator::append(QXmlStreamWriter &writer, Node *node) |
157 | { |
158 | Q_ASSERT(marker_); |
159 | |
160 | writer.writeStartElement(qualifiedName: "description" ); |
161 | if (Config::instance().get(CONFIG_LOCATIONINFO).asBool()) { |
162 | writer.writeAttribute(qualifiedName: "path" , value: node->doc().location().filePath()); |
163 | writer.writeAttribute(qualifiedName: "line" , value: QString::number(node->doc().location().lineNo())); |
164 | writer.writeAttribute(qualifiedName: "column" , value: QString::number(node->doc().location().columnNo())); |
165 | } |
166 | |
167 | if (node->isTextPageNode()) |
168 | generateRelations(writer, node); |
169 | |
170 | if (node->isModule()) { |
171 | writer.writeStartElement(qualifiedName: "generatedlist" ); |
172 | writer.writeAttribute(qualifiedName: "contents" , value: "classesbymodule" ); |
173 | auto *cnn = static_cast<CollectionNode *>(node); |
174 | |
175 | if (cnn->hasNamespaces()) { |
176 | writer.writeStartElement(qualifiedName: "section" ); |
177 | writer.writeStartElement(qualifiedName: "heading" ); |
178 | writer.writeAttribute(qualifiedName: "level" , value: "1" ); |
179 | writer.writeCharacters(text: "Namespaces" ); |
180 | writer.writeEndElement(); // heading |
181 | NodeMap namespaces; |
182 | cnn->getMemberNamespaces(out&: namespaces); |
183 | generateAnnotatedList(writer, relative: node, nodeMap: namespaces); |
184 | writer.writeEndElement(); // section |
185 | } |
186 | if (cnn->hasClasses()) { |
187 | writer.writeStartElement(qualifiedName: "section" ); |
188 | writer.writeStartElement(qualifiedName: "heading" ); |
189 | writer.writeAttribute(qualifiedName: "level" , value: "1" ); |
190 | writer.writeCharacters(text: "Classes" ); |
191 | writer.writeEndElement(); // heading |
192 | NodeMap classes; |
193 | cnn->getMemberClasses(out&: classes); |
194 | generateAnnotatedList(writer, relative: node, nodeMap: classes); |
195 | writer.writeEndElement(); // section |
196 | } |
197 | writer.writeEndElement(); // generatedlist |
198 | } |
199 | |
200 | m_inLink = m_inSectionHeading = m_hasQuotingInformation = false; |
201 | |
202 | const Atom *atom = node->doc().body().firstAtom(); |
203 | while (atom) |
204 | atom = addAtomElements(writer, atom, relative: node, marker: marker_); |
205 | |
206 | QList<Text> alsoList = node->doc().alsoList(); |
207 | supplementAlsoList(node, alsoList); |
208 | |
209 | if (!alsoList.isEmpty()) { |
210 | writer.writeStartElement(qualifiedName: "see-also" ); |
211 | for (const auto &item : alsoList) { |
212 | const auto *atom = item.firstAtom(); |
213 | while (atom) |
214 | atom = addAtomElements(writer, atom, relative: node, marker: marker_); |
215 | } |
216 | writer.writeEndElement(); // see-also |
217 | } |
218 | |
219 | if (node->isExample()) { |
220 | m_supplement = true; |
221 | generateRequiredLinks(node, marker: marker_); |
222 | m_supplement = false; |
223 | } else if (node->isGroup()) { |
224 | auto *cn = static_cast<CollectionNode *>(node); |
225 | if (!cn->noAutoList()) |
226 | generateAnnotatedList(writer, relative: node, nodeList: cn->members()); |
227 | } |
228 | |
229 | writer.writeEndElement(); // description |
230 | } |
231 | |
232 | void WebXMLGenerator::generateDocumentation(Node *node) |
233 | { |
234 | // Don't generate nodes that are already processed, or if they're not supposed to |
235 | // generate output, ie. external, index or images nodes. |
236 | if (!node->url().isNull() || node->isExternalPage() || node->isIndexNode()) |
237 | return; |
238 | |
239 | if (node->isInternal() && !m_showInternal) |
240 | return; |
241 | |
242 | if (node->parent()) { |
243 | if (node->isNamespace() || node->isClassNode() || node->isHeader()) |
244 | generateCppReferencePage(aggregate: static_cast<Aggregate *>(node), nullptr); |
245 | else if (node->isCollectionNode()) { |
246 | if (node->wasSeen()) { |
247 | // see remarks in base class impl. |
248 | m_qdb->mergeCollections(c: static_cast<CollectionNode *>(node)); |
249 | generatePageNode(pn: static_cast<PageNode *>(node), nullptr); |
250 | } |
251 | } else if (node->isTextPageNode()) |
252 | generatePageNode(pn: static_cast<PageNode *>(node), nullptr); |
253 | // else if TODO: anything else? |
254 | } |
255 | |
256 | if (node->isAggregate()) { |
257 | auto *aggregate = static_cast<Aggregate *>(node); |
258 | for (auto c : aggregate->childNodes()) { |
259 | if ((c->isAggregate() || c->isTextPageNode() || c->isCollectionNode()) |
260 | && !c->isPrivate()) |
261 | generateDocumentation(node: c); |
262 | } |
263 | } |
264 | } |
265 | |
266 | const Atom *WebXMLGenerator::addAtomElements(QXmlStreamWriter &writer, const Atom *atom, |
267 | const Node *relative, CodeMarker *marker) |
268 | { |
269 | bool keepQuoting = false; |
270 | |
271 | if (!atom) |
272 | return nullptr; |
273 | |
274 | switch (atom->type()) { |
275 | case Atom::AnnotatedList: { |
276 | const CollectionNode *cn = m_qdb->getCollectionNode(name: atom->string(), type: Node::Group); |
277 | if (cn) |
278 | generateAnnotatedList(writer, relative, nodeList: cn->members()); |
279 | } break; |
280 | case Atom::AutoLink: { |
281 | const Node *node{nullptr}; |
282 | QString link{}; |
283 | |
284 | if (!m_inLink && !m_inSectionHeading) { |
285 | link = getAutoLink(atom, relative, node: &node, Node::API); |
286 | |
287 | if (!link.isEmpty() && node && node->isDeprecated() |
288 | && relative->parent() != node && !relative->isDeprecated()) { |
289 | link.clear(); |
290 | } |
291 | } |
292 | |
293 | startLink(writer, atom, node, link); |
294 | |
295 | writer.writeCharacters(text: atom->string()); |
296 | |
297 | if (m_inLink) { |
298 | writer.writeEndElement(); // link |
299 | m_inLink = false; |
300 | } |
301 | |
302 | break; |
303 | } |
304 | case Atom::BaseName: |
305 | break; |
306 | case Atom::BriefLeft: |
307 | |
308 | writer.writeStartElement(qualifiedName: "brief" ); |
309 | switch (relative->nodeType()) { |
310 | case Node::Property: |
311 | writer.writeCharacters(text: "This property" ); |
312 | break; |
313 | case Node::Variable: |
314 | writer.writeCharacters(text: "This variable" ); |
315 | break; |
316 | default: |
317 | break; |
318 | } |
319 | if (relative->isProperty() || relative->isVariable()) { |
320 | QString str; |
321 | const Atom *a = atom->next(); |
322 | while (a != nullptr && a->type() != Atom::BriefRight) { |
323 | if (a->type() == Atom::String || a->type() == Atom::AutoLink) |
324 | str += a->string(); |
325 | a = a->next(); |
326 | } |
327 | str[0] = str[0].toLower(); |
328 | if (str.endsWith(c: '.')) |
329 | str.chop(n: 1); |
330 | |
331 | const QList<QStringView> words = QStringView{str}.split(sep: ' '); |
332 | if (!words.isEmpty()) { |
333 | QStringView first(words.at(i: 0)); |
334 | if (!(first == u"contains" || first == u"specifies" || first == u"describes" |
335 | || first == u"defines" || first == u"holds" || first == u"determines" )) |
336 | writer.writeCharacters(text: " holds " ); |
337 | else |
338 | writer.writeCharacters(text: " " ); |
339 | } |
340 | } |
341 | break; |
342 | |
343 | case Atom::BriefRight: |
344 | if (relative->isProperty() || relative->isVariable()) |
345 | writer.writeCharacters(text: "." ); |
346 | |
347 | writer.writeEndElement(); // brief |
348 | break; |
349 | |
350 | case Atom::C: |
351 | writer.writeStartElement(qualifiedName: "teletype" ); |
352 | if (m_inLink) |
353 | writer.writeAttribute(qualifiedName: "type" , value: "normal" ); |
354 | else |
355 | writer.writeAttribute(qualifiedName: "type" , value: "highlighted" ); |
356 | |
357 | writer.writeCharacters(text: plainCode(markedCode: atom->string())); |
358 | writer.writeEndElement(); // teletype |
359 | break; |
360 | |
361 | case Atom::Code: |
362 | if (!m_hasQuotingInformation) |
363 | writer.writeTextElement( |
364 | qualifiedName: "code" , text: trimmedTrailing(string: plainCode(markedCode: atom->string()), prefix: QString(), suffix: QString())); |
365 | else |
366 | keepQuoting = true; |
367 | break; |
368 | |
369 | case Atom::CodeBad: |
370 | writer.writeTextElement(qualifiedName: "badcode" , |
371 | text: trimmedTrailing(string: plainCode(markedCode: atom->string()), prefix: QString(), suffix: QString())); |
372 | break; |
373 | |
374 | case Atom::CodeQuoteArgument: |
375 | if (m_quoting) { |
376 | if (quoteCommand == "dots" ) { |
377 | writer.writeAttribute(qualifiedName: "indent" , value: atom->string()); |
378 | writer.writeCharacters(text: "..." ); |
379 | } else { |
380 | writer.writeCharacters(text: atom->string()); |
381 | } |
382 | writer.writeEndElement(); // code |
383 | keepQuoting = true; |
384 | } |
385 | break; |
386 | |
387 | case Atom::CodeQuoteCommand: |
388 | if (m_quoting) { |
389 | quoteCommand = atom->string(); |
390 | writer.writeStartElement(qualifiedName: quoteCommand); |
391 | } |
392 | break; |
393 | |
394 | case Atom::ExampleFileLink: { |
395 | if (!m_inLink) { |
396 | QString link = linkForExampleFile(path: atom->string()); |
397 | if (!link.isEmpty()) |
398 | startLink(writer, atom, node: relative, link); |
399 | } |
400 | } break; |
401 | |
402 | case Atom::ExampleImageLink: { |
403 | if (!m_inLink) { |
404 | QString link = atom->string(); |
405 | if (!link.isEmpty()) |
406 | startLink(writer, atom, node: nullptr, link: "images/used-in-examples/" + link); |
407 | } |
408 | } break; |
409 | |
410 | case Atom::FootnoteLeft: |
411 | writer.writeStartElement(qualifiedName: "footnote" ); |
412 | break; |
413 | |
414 | case Atom::FootnoteRight: |
415 | writer.writeEndElement(); // footnote |
416 | break; |
417 | |
418 | case Atom::FormatEndif: |
419 | writer.writeEndElement(); // raw |
420 | break; |
421 | case Atom::FormatIf: |
422 | writer.writeStartElement(qualifiedName: "raw" ); |
423 | writer.writeAttribute(qualifiedName: "format" , value: atom->string()); |
424 | break; |
425 | case Atom::FormattingLeft: { |
426 | if (atom->string() == ATOM_FORMATTING_BOLD) |
427 | writer.writeStartElement(qualifiedName: "bold" ); |
428 | else if (atom->string() == ATOM_FORMATTING_ITALIC) |
429 | writer.writeStartElement(qualifiedName: "italic" ); |
430 | else if (atom->string() == ATOM_FORMATTING_UNDERLINE) |
431 | writer.writeStartElement(qualifiedName: "underline" ); |
432 | else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) |
433 | writer.writeStartElement(qualifiedName: "subscript" ); |
434 | else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) |
435 | writer.writeStartElement(qualifiedName: "superscript" ); |
436 | else if (atom->string() == ATOM_FORMATTING_TELETYPE) |
437 | writer.writeStartElement(qualifiedName: "teletype" ); |
438 | else if (atom->string() == ATOM_FORMATTING_PARAMETER) |
439 | writer.writeStartElement(qualifiedName: "argument" ); |
440 | else if (atom->string() == ATOM_FORMATTING_INDEX) |
441 | writer.writeStartElement(qualifiedName: "index" ); |
442 | } break; |
443 | |
444 | case Atom::FormattingRight: { |
445 | if (atom->string() == ATOM_FORMATTING_BOLD) |
446 | writer.writeEndElement(); |
447 | else if (atom->string() == ATOM_FORMATTING_ITALIC) |
448 | writer.writeEndElement(); |
449 | else if (atom->string() == ATOM_FORMATTING_UNDERLINE) |
450 | writer.writeEndElement(); |
451 | else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT) |
452 | writer.writeEndElement(); |
453 | else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT) |
454 | writer.writeEndElement(); |
455 | else if (atom->string() == ATOM_FORMATTING_TELETYPE) |
456 | writer.writeEndElement(); |
457 | else if (atom->string() == ATOM_FORMATTING_PARAMETER) |
458 | writer.writeEndElement(); |
459 | else if (atom->string() == ATOM_FORMATTING_INDEX) |
460 | writer.writeEndElement(); |
461 | } |
462 | if (m_inLink) { |
463 | writer.writeEndElement(); // link |
464 | m_inLink = false; |
465 | } |
466 | break; |
467 | |
468 | case Atom::GeneratedList: |
469 | writer.writeStartElement(qualifiedName: "generatedlist" ); |
470 | writer.writeAttribute(qualifiedName: "contents" , value: atom->string()); |
471 | writer.writeEndElement(); |
472 | break; |
473 | |
474 | // TODO: The other generators treat inlineimage and image |
475 | // simultaneously as the diffirences aren't big. It should be |
476 | // possible to do the same for webxmlgenerator instead of |
477 | // repeating the code. |
478 | |
479 | // TODO: [generator-insufficient-structural-abstraction] |
480 | case Atom::Image: { |
481 | auto maybe_resolved_file{file_resolver.resolve(filename: atom->string())}; |
482 | if (!maybe_resolved_file) { |
483 | // TODO: [uncentralized-admonition][failed-resolve-file] |
484 | relative->location().warning(QStringLiteral("Missing image: %1" ).arg(a: atom->string())); |
485 | } else { |
486 | ResolvedFile file{*maybe_resolved_file}; |
487 | QString file_name{QFileInfo{file.get_path()}.fileName()}; |
488 | |
489 | // TODO: [uncentralized-output-directory-structure] |
490 | Config::copyFile(location: relative->doc().location(), sourceFilePath: file.get_path(), userFriendlySourceFilePath: file_name, targetDirPath: outputDir() + QLatin1String("/images" )); |
491 | |
492 | writer.writeStartElement(qualifiedName: "image" ); |
493 | // TODO: [uncentralized-output-directory-structure] |
494 | writer.writeAttribute(qualifiedName: "href" , value: "images/" + file_name); |
495 | writer.writeEndElement(); |
496 | // TODO: [uncentralized-output-directory-structure] |
497 | setImageFileName(relative, fileName: "images/" + file_name); |
498 | } |
499 | break; |
500 | } |
501 | // TODO: [generator-insufficient-structural-abstraction] |
502 | case Atom::InlineImage: { |
503 | auto maybe_resolved_file{file_resolver.resolve(filename: atom->string())}; |
504 | if (!maybe_resolved_file) { |
505 | // TODO: [uncentralized-admonition][failed-resolve-file] |
506 | relative->location().warning(QStringLiteral("Missing image: %1" ).arg(a: atom->string())); |
507 | } else { |
508 | ResolvedFile file{*maybe_resolved_file}; |
509 | QString file_name{QFileInfo{file.get_path()}.fileName()}; |
510 | |
511 | // TODO: [uncentralized-output-directory-structure] |
512 | Config::copyFile(location: relative->doc().location(), sourceFilePath: file.get_path(), userFriendlySourceFilePath: file_name, targetDirPath: outputDir() + QLatin1String("/images" )); |
513 | |
514 | writer.writeStartElement(qualifiedName: "inlineimage" ); |
515 | // TODO: [uncentralized-output-directory-structure] |
516 | writer.writeAttribute(qualifiedName: "href" , value: "images/" + file_name); |
517 | writer.writeEndElement(); |
518 | // TODO: [uncentralized-output-directory-structure] |
519 | setImageFileName(relative, fileName: "images/" + file_name); |
520 | } |
521 | break; |
522 | } |
523 | case Atom::ImageText: |
524 | break; |
525 | |
526 | case Atom::ImportantLeft: |
527 | writer.writeStartElement(qualifiedName: "para" ); |
528 | writer.writeTextElement(qualifiedName: "bold" , text: "Important:" ); |
529 | writer.writeCharacters(text: " " ); |
530 | break; |
531 | |
532 | case Atom::LegaleseLeft: |
533 | writer.writeStartElement(qualifiedName: "legalese" ); |
534 | break; |
535 | |
536 | case Atom::LegaleseRight: |
537 | writer.writeEndElement(); // legalese |
538 | break; |
539 | |
540 | case Atom::Link: |
541 | case Atom::LinkNode: |
542 | if (!m_inLink) { |
543 | const Node *node = nullptr; |
544 | QString link = getLink(atom, relative, node: &node); |
545 | if (!link.isEmpty()) |
546 | startLink(writer, atom, node, link); |
547 | } |
548 | break; |
549 | |
550 | case Atom::ListLeft: |
551 | writer.writeStartElement(qualifiedName: "list" ); |
552 | |
553 | if (atom->string() == ATOM_LIST_BULLET) |
554 | writer.writeAttribute(qualifiedName: "type" , value: "bullet" ); |
555 | else if (atom->string() == ATOM_LIST_TAG) |
556 | writer.writeAttribute(qualifiedName: "type" , value: "definition" ); |
557 | else if (atom->string() == ATOM_LIST_VALUE) { |
558 | if (relative->isEnumType()) |
559 | writer.writeAttribute(qualifiedName: "type" , value: "enum" ); |
560 | else |
561 | writer.writeAttribute(qualifiedName: "type" , value: "definition" ); |
562 | } else { |
563 | writer.writeAttribute(qualifiedName: "type" , value: "ordered" ); |
564 | if (atom->string() == ATOM_LIST_UPPERALPHA) |
565 | writer.writeAttribute(qualifiedName: "start" , value: "A" ); |
566 | else if (atom->string() == ATOM_LIST_LOWERALPHA) |
567 | writer.writeAttribute(qualifiedName: "start" , value: "a" ); |
568 | else if (atom->string() == ATOM_LIST_UPPERROMAN) |
569 | writer.writeAttribute(qualifiedName: "start" , value: "I" ); |
570 | else if (atom->string() == ATOM_LIST_LOWERROMAN) |
571 | writer.writeAttribute(qualifiedName: "start" , value: "i" ); |
572 | else // (atom->string() == ATOM_LIST_NUMERIC) |
573 | writer.writeAttribute(qualifiedName: "start" , value: "1" ); |
574 | } |
575 | break; |
576 | |
577 | case Atom::ListItemNumber: |
578 | break; |
579 | case Atom::ListTagLeft: { |
580 | writer.writeStartElement(qualifiedName: "definition" ); |
581 | |
582 | writer.writeTextElement( |
583 | qualifiedName: "term" , text: plainCode(markedCode: marker->markedUpEnumValue(atom->next()->string(), relative))); |
584 | } break; |
585 | |
586 | case Atom::ListTagRight: |
587 | writer.writeEndElement(); // definition |
588 | break; |
589 | |
590 | case Atom::ListItemLeft: |
591 | writer.writeStartElement(qualifiedName: "item" ); |
592 | break; |
593 | |
594 | case Atom::ListItemRight: |
595 | writer.writeEndElement(); // item |
596 | break; |
597 | |
598 | case Atom::ListRight: |
599 | writer.writeEndElement(); // list |
600 | break; |
601 | |
602 | case Atom::NoteLeft: |
603 | writer.writeStartElement(qualifiedName: "para" ); |
604 | writer.writeTextElement(qualifiedName: "bold" , text: "Note:" ); |
605 | writer.writeCharacters(text: " " ); |
606 | break; |
607 | |
608 | // End admonition elements |
609 | case Atom::ImportantRight: |
610 | case Atom::NoteRight: |
611 | case Atom::WarningRight: |
612 | writer.writeEndElement(); // para |
613 | break; |
614 | |
615 | case Atom::Nop: |
616 | break; |
617 | |
618 | case Atom::CaptionLeft: |
619 | case Atom::ParaLeft: |
620 | writer.writeStartElement(qualifiedName: "para" ); |
621 | break; |
622 | |
623 | case Atom::CaptionRight: |
624 | case Atom::ParaRight: |
625 | writer.writeEndElement(); // para |
626 | break; |
627 | |
628 | case Atom::QuotationLeft: |
629 | writer.writeStartElement(qualifiedName: "quote" ); |
630 | break; |
631 | |
632 | case Atom::QuotationRight: |
633 | writer.writeEndElement(); // quote |
634 | break; |
635 | |
636 | case Atom::RawString: |
637 | writer.writeCharacters(text: atom->string()); |
638 | break; |
639 | |
640 | case Atom::SectionLeft: |
641 | writer.writeStartElement(qualifiedName: "section" ); |
642 | writer.writeAttribute(qualifiedName: "id" , |
643 | value: Utilities::asAsciiPrintable(name: Text::sectionHeading(sectionBegin: atom).toString())); |
644 | break; |
645 | |
646 | case Atom::SectionRight: |
647 | writer.writeEndElement(); // section |
648 | break; |
649 | |
650 | case Atom::SectionHeadingLeft: { |
651 | writer.writeStartElement(qualifiedName: "heading" ); |
652 | int unit = atom->string().toInt(); // + hOffset(relative) |
653 | writer.writeAttribute(qualifiedName: "level" , value: QString::number(unit)); |
654 | m_inSectionHeading = true; |
655 | } break; |
656 | |
657 | case Atom::SectionHeadingRight: |
658 | writer.writeEndElement(); // heading |
659 | m_inSectionHeading = false; |
660 | break; |
661 | |
662 | case Atom::SidebarLeft: |
663 | case Atom::SidebarRight: |
664 | break; |
665 | |
666 | case Atom::SnippetCommand: |
667 | if (m_quoting) { |
668 | writer.writeStartElement(qualifiedName: atom->string()); |
669 | } |
670 | break; |
671 | |
672 | case Atom::SnippetIdentifier: |
673 | if (m_quoting) { |
674 | writer.writeAttribute(qualifiedName: "identifier" , value: atom->string()); |
675 | writer.writeEndElement(); |
676 | keepQuoting = true; |
677 | } |
678 | break; |
679 | |
680 | case Atom::SnippetLocation: |
681 | if (m_quoting) { |
682 | const QString &location = atom->string(); |
683 | writer.writeAttribute(qualifiedName: "location" , value: location); |
684 | auto maybe_resolved_file{file_resolver.resolve(filename: location)}; |
685 | // const QString resolved = Doc::resolveFile(Location(), location); |
686 | if (maybe_resolved_file) |
687 | writer.writeAttribute(qualifiedName: "path" , value: (*maybe_resolved_file).get_path()); |
688 | else { |
689 | // TODO: [uncetnralized-admonition][failed-resolve-file] |
690 | QString details = std::transform_reduce( |
691 | first: file_resolver.get_search_directories().cbegin(), |
692 | last: file_resolver.get_search_directories().cend(), |
693 | init: u"Searched directories:"_s , |
694 | binary_op: std::plus(), |
695 | unary_op: [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); } |
696 | ); |
697 | |
698 | relative->location().warning(message: u"Cannot find file to quote from: %1"_s .arg(a: location), details); |
699 | } |
700 | } |
701 | break; |
702 | |
703 | case Atom::String: |
704 | writer.writeCharacters(text: atom->string()); |
705 | break; |
706 | case Atom::TableLeft: |
707 | writer.writeStartElement(qualifiedName: "table" ); |
708 | if (atom->string().contains(s: "%" )) |
709 | writer.writeAttribute(qualifiedName: "width" , value: atom->string()); |
710 | break; |
711 | |
712 | case Atom::TableRight: |
713 | writer.writeEndElement(); // table |
714 | break; |
715 | |
716 | case Atom::TableHeaderLeft: |
717 | writer.writeStartElement(qualifiedName: "header" ); |
718 | break; |
719 | |
720 | case Atom::TableHeaderRight: |
721 | writer.writeEndElement(); // header |
722 | break; |
723 | |
724 | case Atom::TableRowLeft: |
725 | writer.writeStartElement(qualifiedName: "row" ); |
726 | break; |
727 | |
728 | case Atom::TableRowRight: |
729 | writer.writeEndElement(); // row |
730 | break; |
731 | |
732 | case Atom::TableItemLeft: { |
733 | writer.writeStartElement(qualifiedName: "item" ); |
734 | QStringList spans = atom->string().split(sep: "," ); |
735 | if (spans.size() == 2) { |
736 | if (spans.at(i: 0) != "1" ) |
737 | writer.writeAttribute(qualifiedName: "colspan" , value: spans.at(i: 0).trimmed()); |
738 | if (spans.at(i: 1) != "1" ) |
739 | writer.writeAttribute(qualifiedName: "rowspan" , value: spans.at(i: 1).trimmed()); |
740 | } |
741 | } break; |
742 | case Atom::TableItemRight: |
743 | writer.writeEndElement(); // item |
744 | break; |
745 | |
746 | case Atom::Target: |
747 | writer.writeStartElement(qualifiedName: "target" ); |
748 | writer.writeAttribute(qualifiedName: "name" , value: Utilities::asAsciiPrintable(name: atom->string())); |
749 | writer.writeEndElement(); |
750 | break; |
751 | |
752 | case Atom::WarningLeft: |
753 | writer.writeStartElement(qualifiedName: "para" ); |
754 | writer.writeTextElement(qualifiedName: "bold" , text: "Warning:" ); |
755 | writer.writeCharacters(text: " " ); |
756 | break; |
757 | |
758 | case Atom::UnhandledFormat: |
759 | case Atom::UnknownCommand: |
760 | writer.writeCharacters(text: atom->typeString()); |
761 | break; |
762 | default: |
763 | break; |
764 | } |
765 | |
766 | m_hasQuotingInformation = keepQuoting; |
767 | return atom->next(); |
768 | } |
769 | |
770 | void WebXMLGenerator::startLink(QXmlStreamWriter &writer, const Atom *atom, const Node *node, |
771 | const QString &link) |
772 | { |
773 | QString fullName = link; |
774 | if (node) |
775 | fullName = node->fullName(); |
776 | if (!fullName.isEmpty() && !link.isEmpty()) { |
777 | writer.writeStartElement(qualifiedName: "link" ); |
778 | if (atom && !atom->string().isEmpty()) |
779 | writer.writeAttribute(qualifiedName: "raw" , value: atom->string()); |
780 | else |
781 | writer.writeAttribute(qualifiedName: "raw" , value: fullName); |
782 | writer.writeAttribute(qualifiedName: "href" , value: link); |
783 | writer.writeAttribute(qualifiedName: "type" , value: targetType(node)); |
784 | if (node) { |
785 | switch (node->nodeType()) { |
786 | case Node::Enum: |
787 | writer.writeAttribute(qualifiedName: "enum" , value: fullName); |
788 | break; |
789 | case Node::Example: { |
790 | const auto *en = static_cast<const ExampleNode *>(node); |
791 | const QString fileTitle = atom ? exampleFileTitle(relative: en, fileName: atom->string()) : QString(); |
792 | if (!fileTitle.isEmpty()) { |
793 | writer.writeAttribute(qualifiedName: "page" , value: fileTitle); |
794 | break; |
795 | } |
796 | } |
797 | Q_FALLTHROUGH(); |
798 | case Node::Page: |
799 | writer.writeAttribute(qualifiedName: "page" , value: fullName); |
800 | break; |
801 | case Node::Property: { |
802 | const auto *propertyNode = static_cast<const PropertyNode *>(node); |
803 | if (!propertyNode->getters().empty()) |
804 | writer.writeAttribute(qualifiedName: "getter" , value: propertyNode->getters().at(i: 0)->fullName()); |
805 | } break; |
806 | default: |
807 | break; |
808 | } |
809 | } |
810 | m_inLink = true; |
811 | } |
812 | } |
813 | |
814 | void WebXMLGenerator::endLink(QXmlStreamWriter &writer) |
815 | { |
816 | if (m_inLink) { |
817 | writer.writeEndElement(); // link |
818 | m_inLink = false; |
819 | } |
820 | } |
821 | |
822 | void WebXMLGenerator::generateRelations(QXmlStreamWriter &writer, const Node *node) |
823 | { |
824 | if (node && !node->links().empty()) { |
825 | std::pair<QString, QString> anchorPair; |
826 | const Node *linkNode; |
827 | |
828 | for (auto it = node->links().cbegin(); it != node->links().cend(); ++it) { |
829 | |
830 | linkNode = m_qdb->findNodeForTarget(target: it.value().first, relative: node); |
831 | |
832 | if (!linkNode) |
833 | linkNode = node; |
834 | |
835 | if (linkNode == node) |
836 | anchorPair = it.value(); |
837 | else |
838 | anchorPair = anchorForNode(node: linkNode); |
839 | |
840 | writer.writeStartElement(qualifiedName: "relation" ); |
841 | writer.writeAttribute(qualifiedName: "href" , value: anchorPair.first); |
842 | writer.writeAttribute(qualifiedName: "type" , value: targetType(node: linkNode)); |
843 | |
844 | switch (it.key()) { |
845 | case Node::StartLink: |
846 | writer.writeAttribute(qualifiedName: "meta" , value: "start" ); |
847 | break; |
848 | case Node::NextLink: |
849 | writer.writeAttribute(qualifiedName: "meta" , value: "next" ); |
850 | break; |
851 | case Node::PreviousLink: |
852 | writer.writeAttribute(qualifiedName: "meta" , value: "previous" ); |
853 | break; |
854 | case Node::ContentsLink: |
855 | writer.writeAttribute(qualifiedName: "meta" , value: "contents" ); |
856 | break; |
857 | default: |
858 | writer.writeAttribute(qualifiedName: "meta" , value: "" ); |
859 | } |
860 | writer.writeAttribute(qualifiedName: "description" , value: anchorPair.second); |
861 | writer.writeEndElement(); // link |
862 | } |
863 | } |
864 | } |
865 | |
866 | void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, |
867 | const NodeMap &nodeMap) |
868 | { |
869 | generateAnnotatedList(writer, relative, nodeList: nodeMap.values()); |
870 | } |
871 | |
872 | void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative, |
873 | const NodeList &nodeList) |
874 | { |
875 | writer.writeStartElement(qualifiedName: "table" ); |
876 | writer.writeAttribute(qualifiedName: "width" , value: "100%" ); |
877 | |
878 | for (const auto *node : nodeList) { |
879 | writer.writeStartElement(qualifiedName: "row" ); |
880 | writer.writeStartElement(qualifiedName: "item" ); |
881 | writer.writeStartElement(qualifiedName: "para" ); |
882 | const QString link = linkForNode(node, relative); |
883 | startLink(writer, atom: node->doc().body().firstAtom(), node, link); |
884 | endLink(writer); |
885 | writer.writeEndElement(); // para |
886 | writer.writeEndElement(); // item |
887 | |
888 | writer.writeStartElement(qualifiedName: "item" ); |
889 | writer.writeStartElement(qualifiedName: "para" ); |
890 | writer.writeCharacters(text: node->doc().briefText().toString()); |
891 | writer.writeEndElement(); // para |
892 | writer.writeEndElement(); // item |
893 | writer.writeEndElement(); // row |
894 | } |
895 | writer.writeEndElement(); // table |
896 | } |
897 | |
898 | QString WebXMLGenerator::fileBase(const Node *node) const |
899 | { |
900 | return Generator::fileBase(node); |
901 | } |
902 | |
903 | QT_END_NAMESPACE |
904 | |