1 /****************************************************************************
3 ** Copyright (C) 2012 Nokia Corporation and/or its subsidiary(-ies).
4 ** Contact: http://www.qt-project.org/
6 ** This file is part of the tools applications of the Qt Toolkit.
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** GNU Lesser General Public License Usage
10 ** This file may be used under the terms of the GNU Lesser General Public
11 ** License version 2.1 as published by the Free Software Foundation and
12 ** appearing in the file LICENSE.LGPL included in the packaging of this
13 ** file. Please review the following information to ensure the GNU Lesser
14 ** General Public License version 2.1 requirements will be met:
15 ** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
17 ** In addition, as a special exception, Nokia gives you certain additional
18 ** rights. These rights are described in the Nokia Qt LGPL Exception
19 ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
21 ** GNU General Public License Usage
22 ** Alternatively, this file may be used under the terms of the GNU General
23 ** Public License version 3.0 as published by the Free Software Foundation
24 ** and appearing in the file LICENSE.GPL included in the packaging of this
25 ** file. Please review the following information to ensure the GNU General
26 ** Public License version 3.0 requirements will be met:
27 ** http://www.gnu.org/copyleft/gpl.html.
30 ** Alternatively, this file may be used in accordance with the terms and
31 ** conditions contained in a signed written agreement between you and Nokia.
40 ****************************************************************************/
47 #include "codemarker.h"
49 #include "ditaxmlgenerator.h"
51 #include "editdistance.h"
52 #include "generator.h"
54 #include "openedlist.h"
56 #include "separator.h"
57 #include "tokenizer.h"
62 QString Generator::baseDir_;
63 QStringList Generator::exampleDirs;
64 QStringList Generator::exampleImgExts;
65 QMap<QString, QMap<QString, QString> > Generator::fmtLeftMaps;
66 QMap<QString, QMap<QString, QString> > Generator::fmtRightMaps;
67 QList<Generator *> Generator::generators;
68 QStringList Generator::imageDirs;
69 QStringList Generator::imageFiles;
70 QMap<QString, QStringList> Generator::imgFileExts;
71 QString Generator::outDir_;
72 QSet<QString> Generator::outputFormats;
73 QHash<QString, QString> Generator::outputPrefixes;
74 QString Generator::project;
75 QStringList Generator::scriptDirs;
76 QStringList Generator::scriptFiles;
77 QString Generator::sinceTitles[] =
81 " New Member Functions",
82 " New Functions in Namespaces",
83 " New Global Functions",
90 " New QML Properties",
92 " New QML Signal Handlers",
96 QStringList Generator::styleDirs;
97 QStringList Generator::styleFiles;
100 Generator::Generator()
108 generators.prepend(this);
111 Generator::~Generator()
113 generators.removeAll(this);
116 void Generator::appendFullName(Text& text,
117 const Node *apparentNode,
118 const Node *relative,
120 const Node *actualNode)
123 actualNode = apparentNode;
124 text << Atom(Atom::LinkNode, CodeMarker::stringForNode(actualNode))
125 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
126 << Atom(Atom::String, marker->plainFullName(apparentNode, relative))
127 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
130 void Generator::appendFullName(Text& text,
131 const Node *apparentNode,
132 const QString& fullName,
133 const Node *actualNode)
136 actualNode = apparentNode;
137 text << Atom(Atom::LinkNode, CodeMarker::stringForNode(actualNode))
138 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
139 << Atom(Atom::String, fullName)
140 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
143 void Generator::appendFullNames(Text& text,
144 const NodeList& nodes,
145 const Node* relative,
148 NodeList::ConstIterator n = nodes.begin();
150 while (n != nodes.end()) {
151 appendFullName(text,*n,relative,marker);
152 text << comma(index++,nodes.count());
157 void Generator::appendSortedNames(Text& text,
158 const ClassNode *classe,
159 const QList<RelatedClass> &classes,
162 QList<RelatedClass>::ConstIterator r;
163 QMap<QString,Text> classMap;
167 while (r != classes.end()) {
168 if ((*r).node->access() == Node::Public &&
169 (*r).node->status() != Node::Internal
170 && !(*r).node->doc().isEmpty()) {
172 appendFullName(className, (*r).node, classe, marker);
173 classMap[className.toString().toLower()] = className;
178 QStringList classNames = classMap.keys();
181 foreach (const QString &className, classNames) {
182 text << classMap[className];
183 text << separator(index++, classNames.count());
187 void Generator::appendSortedQmlNames(Text& text,
189 const NodeList& subs,
192 QMap<QString,Text> classMap;
195 for (int i = 0; i < subs.size(); ++i) {
197 if (!base->isQtQuickNode() || !subs[i]->isQtQuickNode() ||
198 (base->qmlModuleIdentifier() == subs[i]->qmlModuleIdentifier())) {
199 appendFullName(t, subs[i], base, marker);
200 classMap[t.toString().toLower()] = t;
204 QStringList names = classMap.keys();
207 foreach (const QString &name, names) {
208 text << classMap[name];
209 text << separator(index++, names.count());
214 Creates the file named \a fileName in the output directory.
215 Attaches a QTextStream to the created file, which is written
216 to all over the place using out().
218 void Generator::beginSubPage(const InnerNode* node, const QString& fileName)
220 QString path = outputDir() + QLatin1Char('/');
221 if (!node->outputSubdirectory().isEmpty())
222 path += node->outputSubdirectory() + QLatin1Char('/');
224 QFile* outFile = new QFile(path);
225 if (!outFile->open(QFile::WriteOnly))
226 node->location().fatal(tr("Cannot open output file '%1'").arg(outFile->fileName()));
227 QTextStream* out = new QTextStream(outFile);
230 out->setCodec(outputCodec);
231 outStreamStack.push(out);
232 const_cast<InnerNode*>(node)->setOutputFileName(fileName);
236 Flush the text stream associated with the subpage, and
237 then pop it off the text stream stack and delete it.
238 This terminates output of the subpage.
240 void Generator::endSubPage()
242 outStreamStack.top()->flush();
243 delete outStreamStack.top()->device();
244 delete outStreamStack.pop();
247 void Generator::endText(const Node * /* relative */,
248 CodeMarker * /* marker */)
252 QString Generator::fileBase(const Node *node) const
255 node = node->relates();
256 else if (!node->isInnerNode())
257 node = node->parent();
258 if (node->subType() == Node::QmlPropertyGroup) {
259 node = node->parent();
262 QString base = node->doc().baseName();
266 const Node *p = node;
269 const Node *pp = p->parent();
270 base.prepend(p->name());
271 if (!p->qmlModuleIdentifier().isEmpty())
272 base.prepend(p->qmlModuleIdentifier()+QChar('-'));
274 To avoid file name conflicts in the html directory,
275 we prepend a prefix (by default, "qml-") to the file name of QML
278 if ((p->subType() == Node::QmlClass) ||
279 (p->subType() == Node::QmlBasicType)) {
280 base.prepend(outputPrefix(QLatin1String("QML")));
282 if (!pp || pp->name().isEmpty() || pp->type() == Node::Fake)
284 base.prepend(QLatin1Char('-'));
287 if (node->type() == Node::Fake) {
288 if (node->subType() == Node::Collision) {
289 const NameCollisionNode* ncn = static_cast<const NameCollisionNode*>(node);
290 if (ncn->currentChild())
291 return fileBase(ncn->currentChild());
292 base.prepend("collision-");
294 //Was QDOC2_COMPAT, required for index.html
295 if (base.endsWith(".html"))
296 base.truncate(base.length() - 5);
298 if (node->subType() == Node::QmlModule) {
299 base.prepend("qmlmodule-");
301 if (node->subType() == Node::Module) {
302 base.append("-module");
306 // the code below is effectively equivalent to:
307 // base.replace(QRegExp("[^A-Za-z0-9]+"), " ");
308 // base = base.trimmed();
309 // base.replace(QLatin1Char(' '), QLatin1Char('-'));
310 // base = base.toLower();
311 // as this function accounted for ~8% of total running time
312 // we optimize a bit...
315 // +5 prevents realloc in fileName() below
316 res.reserve(base.size() + 5);
318 for (int i = 0; i != base.size(); ++i) {
319 QChar c = base.at(i);
320 uint u = c.unicode();
321 if (u >= 'A' && u <= 'Z')
323 if ((u >= 'a' && u <= 'z') || (u >= '0' && u <= '9')) {
324 res += QLatin1Char(u);
328 res += QLatin1Char('-');
332 while (res.endsWith(QLatin1Char('-')))
338 If the \a node has a URL, return the URL as the file name.
339 Otherwise, construct the file name from the fileBase() and
340 the fileExtension(), and return the constructed name.
342 QString Generator::fileName(const Node* node) const
344 if (!node->url().isEmpty())
347 QString name = fileBase(node);
348 name += QLatin1Char('.');
349 name += fileExtension(node);
354 For generating the "New Classes... in x.y" section on the
355 What's New in Qt x.y" page.
357 void Generator::findAllSince(const InnerNode *node)
359 NodeList::const_iterator child = node->childNodes().constBegin();
361 // Traverse the tree, starting at the node supplied.
363 while (child != node->childNodes().constEnd()) {
365 QString sinceString = (*child)->since();
367 if (((*child)->access() != Node::Private) && !sinceString.isEmpty()) {
369 // Insert a new entry into each map for each new since string found.
370 NewSinceMaps::iterator nsmap = newSinceMaps.find(sinceString);
371 if (nsmap == newSinceMaps.end())
372 nsmap = newSinceMaps.insert(sinceString,NodeMultiMap());
374 NewClassMaps::iterator ncmap = newClassMaps.find(sinceString);
375 if (ncmap == newClassMaps.end())
376 ncmap = newClassMaps.insert(sinceString,NodeMap());
378 NewClassMaps::iterator nqcmap = newQmlClassMaps.find(sinceString);
379 if (nqcmap == newQmlClassMaps.end())
380 nqcmap = newQmlClassMaps.insert(sinceString,NodeMap());
382 if ((*child)->type() == Node::Function) {
383 // Insert functions into the general since map.
384 FunctionNode *func = static_cast<FunctionNode *>(*child);
385 if ((func->status() > Node::Obsolete) &&
386 (func->metaness() != FunctionNode::Ctor) &&
387 (func->metaness() != FunctionNode::Dtor)) {
388 nsmap.value().insert(func->name(),(*child));
391 else if ((*child)->url().isEmpty()) {
392 if ((*child)->type() == Node::Class && !(*child)->doc().isEmpty()) {
393 // Insert classes into the since and class maps.
394 QString className = (*child)->name();
395 if ((*child)->parent() &&
396 (*child)->parent()->type() == Node::Namespace &&
397 !(*child)->parent()->name().isEmpty())
398 className = (*child)->parent()->name()+"::"+className;
400 nsmap.value().insert(className,(*child));
401 ncmap.value().insert(className,(*child));
403 else if ((*child)->subType() == Node::QmlClass) {
404 // Insert QML elements into the since and element maps.
405 QString className = (*child)->name();
406 if ((*child)->parent() &&
407 (*child)->parent()->type() == Node::Namespace &&
408 !(*child)->parent()->name().isEmpty())
409 className = (*child)->parent()->name()+"::"+className;
411 nsmap.value().insert(className,(*child));
412 nqcmap.value().insert(className,(*child));
414 else if ((*child)->type() == Node::QmlProperty) {
415 // Insert QML properties into the since map.
416 QString propertyName = (*child)->name();
417 nsmap.value().insert(propertyName,(*child));
421 // Insert external documents into the general since map.
422 QString name = (*child)->name();
423 if ((*child)->parent() &&
424 (*child)->parent()->type() == Node::Namespace &&
425 !(*child)->parent()->name().isEmpty())
426 name = (*child)->parent()->name()+"::"+name;
428 nsmap.value().insert(name,(*child));
431 // Find child nodes with since commands.
432 if ((*child)->isInnerNode()) {
433 findAllSince(static_cast<InnerNode *>(*child));
440 QMap<QString, QString>& Generator::formattingLeftMap()
442 return fmtLeftMaps[format()];
445 QMap<QString, QString>& Generator::formattingRightMap()
447 return fmtRightMaps[format()];
450 QString Generator::fullName(const Node *node,
451 const Node *relative,
452 CodeMarker *marker) const
454 if (node->type() == Node::Fake) {
455 const FakeNode* fn = static_cast<const FakeNode *>(node);
457 // Removed for QTBUG-22870
458 // Unremoved by mws 30/03/12
459 if (!fn->qmlModuleIdentifier().isEmpty())
460 return fn->qmlModuleIdentifier() + "::" + fn->title();
464 else if (node->type() == Node::Class &&
465 !(static_cast<const ClassNode *>(node))->serviceName().isEmpty())
466 return (static_cast<const ClassNode *>(node))->serviceName();
468 return marker->plainFullName(node, relative);
471 void Generator::generateAlsoList(const Node *node, CodeMarker *marker)
473 QList<Text> alsoList = node->doc().alsoList();
474 supplementAlsoList(node, alsoList);
476 if (!alsoList.isEmpty()) {
478 text << Atom::ParaLeft
479 << Atom(Atom::FormattingLeft,ATOM_FORMATTING_BOLD)
481 << Atom(Atom::FormattingRight,ATOM_FORMATTING_BOLD);
483 for (int i = 0; i < alsoList.size(); ++i)
484 text << alsoList.at(i) << separator(i, alsoList.size());
486 text << Atom::ParaRight;
487 generateText(text, node, marker);
491 int Generator::generateAtom(const Atom * /* atom */,
492 const Node * /* relative */,
493 CodeMarker * /* marker */)
498 const Atom *Generator::generateAtomList(const Atom *atom,
499 const Node *relative,
505 if (atom->type() == Atom::FormatIf) {
506 int numAtoms0 = numAtoms;
507 bool rightFormat = canHandleFormat(atom->string());
508 atom = generateAtomList(atom->next(),
511 generate && rightFormat,
516 if (atom->type() == Atom::FormatElse) {
518 atom = generateAtomList(atom->next(),
521 generate && !rightFormat,
527 if (atom->type() == Atom::FormatEndif) {
528 if (generate && numAtoms0 == numAtoms) {
529 relative->location().warning(tr("Output format %1 not handled %2")
530 .arg(format()).arg(outFileName()));
531 Atom unhandledFormatAtom(Atom::UnhandledFormat, format());
532 generateAtomList(&unhandledFormatAtom,
541 else if (atom->type() == Atom::FormatElse ||
542 atom->type() == Atom::FormatEndif) {
548 n += generateAtom(atom, relative, marker);
558 void Generator::generateBody(const Node *node, CodeMarker *marker)
562 if (node->type() == Node::Fake) {
563 const FakeNode *fake = static_cast<const FakeNode *>(node);
564 if (fake->subType() == Node::Example) {
565 generateExampleFiles(fake, marker);
567 else if ((fake->subType() == Node::File) || (fake->subType() == Node::Image)) {
571 if (node->doc().isEmpty()) {
572 if (!quiet && !node->isReimp()) { // ### might be unnecessary
573 node->location().warning(tr("No documentation for '%1'")
574 .arg(marker->plainFullName(node)));
578 if (node->type() == Node::Function) {
579 const FunctionNode *func = static_cast<const FunctionNode *>(node);
580 if (func->reimplementedFrom() != 0)
581 generateReimplementedFrom(func, marker);
584 if (!generateText(node->doc().body(), node, marker)) {
589 if (node->type() == Node::Enum) {
590 const EnumNode *enume = (const EnumNode *) node;
592 QSet<QString> definedItems;
593 QList<EnumItem>::ConstIterator it = enume->items().begin();
594 while (it != enume->items().end()) {
595 definedItems.insert((*it).name());
599 QSet<QString> documentedItems = enume->doc().enumItemNames().toSet();
600 QSet<QString> allItems = definedItems + documentedItems;
601 if (allItems.count() > definedItems.count() ||
602 allItems.count() > documentedItems.count()) {
603 QSet<QString>::ConstIterator a = allItems.begin();
604 while (a != allItems.end()) {
605 if (!definedItems.contains(*a)) {
607 QString best = nearestName(*a, definedItems);
608 if (!best.isEmpty() && !documentedItems.contains(best))
609 details = tr("Maybe you meant '%1'?").arg(best);
611 node->doc().location().warning(
612 tr("No such enum item '%1' in %2").arg(*a).arg(marker->plainFullName(node)),
615 else if (!documentedItems.contains(*a)) {
616 node->doc().location().warning(
617 tr("Undocumented enum item '%1' in %2").arg(*a).arg(marker->plainFullName(node)));
623 else if (node->type() == Node::Function) {
624 const FunctionNode *func = static_cast<const FunctionNode *>(node);
625 QSet<QString> definedParams;
626 QList<Parameter>::ConstIterator p = func->parameters().begin();
627 while (p != func->parameters().end()) {
628 if ((*p).name().isEmpty() && (*p).leftType() != QLatin1String("...")
629 && func->name() != QLatin1String("operator++")
630 && func->name() != QLatin1String("operator--")) {
631 node->doc().location().warning(tr("Missing parameter name"));
634 definedParams.insert((*p).name());
639 QSet<QString> documentedParams = func->doc().parameterNames();
640 QSet<QString> allParams = definedParams + documentedParams;
641 if (allParams.count() > definedParams.count()
642 || allParams.count() > documentedParams.count()) {
643 QSet<QString>::ConstIterator a = allParams.begin();
644 while (a != allParams.end()) {
645 if (!definedParams.contains(*a)) {
647 QString best = nearestName(*a, definedParams);
649 details = tr("Maybe you meant '%1'?").arg(best);
651 node->doc().location().warning(
652 tr("No such parameter '%1' in %2").arg(*a).arg(marker->plainFullName(node)),
655 else if (!(*a).isEmpty() && !documentedParams.contains(*a)) {
656 bool needWarning = (func->status() > Node::Obsolete);
657 if (func->overloadNumber() > 1) {
658 FunctionNode *primaryFunc =
659 func->parent()->findFunctionNode(func->name());
661 foreach (const Parameter ¶m,
662 primaryFunc->parameters()) {
663 if (param.name() == *a) {
670 if (needWarning && !func->isReimp())
671 node->doc().location().warning(
672 tr("Undocumented parameter '%1' in %2")
673 .arg(*a).arg(marker->plainFullName(node)));
679 Something like this return value check should
680 be implemented at some point.
682 if (func->status() > Node::Obsolete && func->returnType() == "bool"
683 && func->reimplementedFrom() == 0 && !func->isOverload()) {
684 QString body = func->doc().body().toString();
685 if (!body.contains("return", Qt::CaseInsensitive))
686 node->doc().location().warning(tr("Undocumented return value"));
691 if (node->type() == Node::Fake) {
692 const FakeNode *fake = static_cast<const FakeNode *>(node);
693 if (fake->subType() == Node::File) {
696 Doc::quoteFromFile(fake->doc().location(), quoter, fake->name());
697 QString code = quoter.quoteTo(fake->location(), "", "");
698 CodeMarker *codeMarker = CodeMarker::markerForFileName(fake->name());
699 text << Atom(codeMarker->atomType(), code);
700 generateText(text, fake, codeMarker);
705 void Generator::generateClassLikeNode(InnerNode* /* classe */, CodeMarker* /* marker */)
709 void Generator::generateExampleFiles(const FakeNode *fake, CodeMarker *marker)
711 if (fake->childNodes().isEmpty())
713 generateFileList(fake, marker, Node::File, QString("Files:"));
714 generateFileList(fake, marker, Node::Image, QString("Images:"));
717 void Generator::generateFakeNode(FakeNode* /* fake */, CodeMarker* /* marker */)
722 This function is called when the documentation for an
723 example is being formatted. It outputs the list of source
724 files comprising the example, and the list of images used
725 by the example. The images are copied into a subtree of
726 \c{...doc/html/images/used-in-examples/...}
728 void Generator::generateFileList(const FakeNode* fake,
730 Node::SubType subtype,
735 OpenedList openedList(OpenedList::Bullet);
737 text << Atom::ParaLeft << tag << Atom::ParaRight
738 << Atom(Atom::ListLeft, openedList.styleString());
740 foreach (const Node* child, fake->childNodes()) {
741 if (child->subType() == subtype) {
743 QString file = child->name();
744 if (subtype == Node::Image) {
745 if (!file.isEmpty()) {
747 QString userFriendlyFilePath;
748 QString srcPath = Config::findFile(fake->location(),
753 userFriendlyFilePath);
754 userFriendlyFilePath.truncate(userFriendlyFilePath.lastIndexOf('/'));
756 QString imgOutDir = outDir_ + "/images/used-in-examples/" + userFriendlyFilePath;
757 if (!dirInfo.mkpath(imgOutDir))
758 fake->location().fatal(tr("Cannot create output directory '%1'")
761 QString imgOutName = Config::copyFile(fake->location(),
770 text << Atom(Atom::ListItemNumber, openedList.numberString())
771 << Atom(Atom::ListItemLeft, openedList.styleString())
773 << Atom(Atom::Link, file)
774 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
776 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK)
778 << Atom(Atom::ListItemRight, openedList.styleString());
781 text << Atom(Atom::ListRight, openedList.styleString());
783 generateText(text, fake, marker);
786 void Generator::generateInheritedBy(const ClassNode *classe,
789 if (!classe->derivedClasses().isEmpty()) {
791 text << Atom::ParaLeft
792 << Atom(Atom::FormattingLeft,ATOM_FORMATTING_BOLD)
794 << Atom(Atom::FormattingRight,ATOM_FORMATTING_BOLD);
796 appendSortedNames(text, classe, classe->derivedClasses(), marker);
797 text << Atom::ParaRight;
798 generateText(text, classe, marker);
802 void Generator::generateInherits(const ClassNode *classe, CodeMarker *marker)
804 QList<RelatedClass>::ConstIterator r;
807 if (!classe->baseClasses().isEmpty()) {
809 text << Atom::ParaLeft
810 << Atom(Atom::FormattingLeft,ATOM_FORMATTING_BOLD)
812 << Atom(Atom::FormattingRight,ATOM_FORMATTING_BOLD);
814 r = classe->baseClasses().begin();
816 while (r != classe->baseClasses().end()) {
817 text << Atom(Atom::LinkNode, CodeMarker::stringForNode((*r).node))
818 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
819 << Atom(Atom::String, (*r).dataTypeWithTemplateArgs)
820 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
822 if ((*r).access == Node::Protected) {
823 text << " (protected)";
825 else if ((*r).access == Node::Private) {
826 text << " (private)";
828 text << separator(index++, classe->baseClasses().count());
831 text << Atom::ParaRight;
832 generateText(text, classe, marker);
837 Recursive writing of HTML files from the root \a node.
839 \note NameCollisionNodes are skipped here and processed
840 later. See HtmlGenerator::generateCollisionPages() for
843 void Generator::generateInnerNode(InnerNode* node)
845 if (!node->url().isNull())
848 if (node->type() == Node::Fake) {
849 FakeNode* fakeNode = static_cast<FakeNode*>(node);
850 if (fakeNode->subType() == Node::ExternalPage)
852 if (fakeNode->subType() == Node::Image)
854 if (fakeNode->subType() == Node::QmlPropertyGroup)
856 if (fakeNode->subType() == Node::Page) {
857 if (node->count() > 0)
858 qDebug("PAGE %s HAS CHILDREN", qPrintable(fakeNode->title()));
863 Obtain a code marker for the source file.
865 CodeMarker *marker = CodeMarker::markerForFileName(node->location().filePath());
867 if (node->parent() != 0) {
869 Skip name collision nodes here and process them
870 later in generateCollisionPages(). Each one is
871 appended to a list for later.
873 if ((node->type() == Node::Fake) && (node->subType() == Node::Collision)) {
874 NameCollisionNode* ncn = static_cast<NameCollisionNode*>(node);
875 collisionNodes.append(const_cast<NameCollisionNode*>(ncn));
878 beginSubPage(node, fileName(node));
879 if (node->type() == Node::Namespace || node->type() == Node::Class) {
880 generateClassLikeNode(node, marker);
882 else if (node->type() == Node::Fake) {
883 generateFakeNode(static_cast<FakeNode*>(node), marker);
889 NodeList::ConstIterator c = node->childNodes().begin();
890 while (c != node->childNodes().end()) {
891 if ((*c)->isInnerNode() && (*c)->access() != Node::Private) {
892 generateInnerNode((InnerNode*)*c);
899 Generate a list of maintainers in the output
901 void Generator::generateMaintainerList(const InnerNode* node, CodeMarker* marker)
903 QStringList sl = getMetadataElements(node,"maintainer");
907 text << Atom::ParaLeft
908 << Atom(Atom::FormattingLeft,ATOM_FORMATTING_BOLD)
910 << Atom(Atom::FormattingRight,ATOM_FORMATTING_BOLD);
912 for (int i = 0; i < sl.size(); ++i)
913 text << sl.at(i) << separator(i, sl.size());
915 text << Atom::ParaRight;
916 generateText(text, node, marker);
922 Output the "Inherit by" list for the QML element,
923 if it is inherited by any other elements.
925 void Generator::generateQmlInheritedBy(const QmlClassNode* qcn,
930 QmlClassNode::subclasses(qcn->name(),subs);
931 if (!subs.isEmpty()) {
933 text << Atom::ParaLeft << "Inherited by ";
934 appendSortedQmlNames(text,qcn,subs,marker);
935 text << Atom::ParaRight;
936 generateText(text, qcn, marker);
945 void Generator::generateQmlInherits(const QmlClassNode* , CodeMarker* )
953 Extract sections of markup text surrounded by \e qmltext
954 and \e endqmltext and output them.
956 bool Generator::generateQmlText(const Text& text,
957 const Node *relative,
959 const QString& /* qmlName */ )
961 const Atom* atom = text.firstAtom();
965 startText(relative, marker);
967 if (atom->type() != Atom::QmlText)
971 while (atom && (atom->type() != Atom::EndQmlText)) {
972 int n = 1 + generateAtom(atom, relative, marker);
978 endText(relative, marker);
985 void Generator::generateReimplementedFrom(const FunctionNode *func,
988 if (func->reimplementedFrom() != 0) {
989 const FunctionNode *from = func->reimplementedFrom();
990 if (from->access() != Node::Private &&
991 from->parent()->access() != Node::Private) {
993 text << Atom::ParaLeft << "Reimplemented from ";
994 QString fullName = from->parent()->name() + "::" + from->name() + "()";
995 appendFullName(text, from->parent(), fullName, from);
996 text << "." << Atom::ParaRight;
997 generateText(text, func, marker);
1002 void Generator::generateSince(const Node *node, CodeMarker *marker)
1004 if (!node->since().isEmpty()) {
1006 text << Atom::ParaLeft
1008 << typeString(node);
1009 if (node->type() == Node::Enum)
1010 text << " was introduced or modified in ";
1012 text << " was introduced in ";
1014 QStringList since = node->since().split(QLatin1Char(' '));
1015 if (since.count() == 1) {
1016 // Handle legacy use of \since <version>.
1017 if (project.isEmpty())
1021 text << " " << since[0];
1023 // Reconstruct the <project> <version> string.
1024 text << " " << since.join(" ");
1027 text << "." << Atom::ParaRight;
1028 generateText(text, node, marker);
1032 void Generator::generateStatus(const Node *node, CodeMarker *marker)
1036 switch (node->status()) {
1037 case Node::Commendable:
1040 case Node::Preliminary:
1041 text << Atom::ParaLeft
1042 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1045 << " is under development and is subject to change."
1046 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD)
1049 case Node::Deprecated:
1050 text << Atom::ParaLeft;
1051 if (node->isInnerNode())
1052 text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD);
1053 text << "This " << typeString(node) << " is deprecated.";
1054 if (node->isInnerNode())
1055 text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
1056 text << Atom::ParaRight;
1058 case Node::Obsolete:
1059 text << Atom::ParaLeft;
1060 if (node->isInnerNode())
1061 text << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD);
1062 text << "This " << typeString(node) << " is obsolete.";
1063 if (node->isInnerNode())
1064 text << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD);
1065 text << " It is provided to keep old source code working. "
1066 << "We strongly advise against "
1067 << "using it in new code." << Atom::ParaRight;
1070 // reimplemented in HtmlGenerator subclass
1071 if (node->isInnerNode()) {
1072 text << Atom::ParaLeft
1073 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_BOLD)
1076 << " is part of the Qt compatibility layer."
1077 << Atom(Atom::FormattingRight, ATOM_FORMATTING_BOLD)
1078 << " It is provided to keep old source code working. "
1079 << "We strongly advise against using it in new code."
1083 case Node::Internal:
1087 generateText(text, node, marker);
1090 bool Generator::generateText(const Text& text,
1091 const Node *relative,
1094 bool result = false;
1095 if (text.firstAtom() != 0) {
1097 startText(relative, marker);
1098 generateAtomList(text.firstAtom(),
1103 endText(relative, marker);
1109 void Generator::generateThreadSafeness(const Node *node, CodeMarker *marker)
1112 Node::ThreadSafeness threadSafeness = node->threadSafeness();
1115 rlink << Atom(Atom::Link,"reentrant")
1116 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1118 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1121 tlink << Atom(Atom::Link,"thread-safe")
1122 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1124 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1126 switch (threadSafeness) {
1127 case Node::UnspecifiedSafeness:
1129 case Node::NonReentrant:
1130 text << Atom::ParaLeft
1131 << Atom(Atom::FormattingLeft,ATOM_FORMATTING_BOLD)
1133 << Atom(Atom::FormattingRight,ATOM_FORMATTING_BOLD)
1141 case Node::Reentrant:
1142 case Node::ThreadSafe:
1143 text << Atom::ParaLeft
1144 << Atom(Atom::FormattingLeft,ATOM_FORMATTING_BOLD)
1146 << Atom(Atom::FormattingRight,ATOM_FORMATTING_BOLD)
1149 if (node->isInnerNode()) {
1150 const InnerNode* innerNode = static_cast<const InnerNode*>(node);
1151 text << "All functions in this "
1154 if (threadSafeness == Node::ThreadSafe)
1159 bool exceptions = false;
1161 NodeList threadsafe;
1162 NodeList nonreentrant;
1163 NodeList::ConstIterator c = innerNode->childNodes().begin();
1164 while (c != innerNode->childNodes().end()) {
1166 if ((*c)->status() != Node::Obsolete){
1167 switch ((*c)->threadSafeness()) {
1168 case Node::Reentrant:
1169 reentrant.append(*c);
1170 if (threadSafeness == Node::ThreadSafe)
1173 case Node::ThreadSafe:
1174 threadsafe.append(*c);
1175 if (threadSafeness == Node::Reentrant)
1178 case Node::NonReentrant:
1179 nonreentrant.append(*c);
1190 else if (threadSafeness == Node::Reentrant) {
1191 if (nonreentrant.isEmpty()) {
1192 if (!threadsafe.isEmpty()) {
1194 appendFullNames(text,threadsafe,innerNode,marker);
1195 singularPlural(text,threadsafe);
1196 text << " also " << tlink << ".";
1202 text << ", except for ";
1203 appendFullNames(text,nonreentrant,innerNode,marker);
1205 singularPlural(text,nonreentrant);
1206 text << " nonreentrant.";
1207 if (!threadsafe.isEmpty()) {
1209 appendFullNames(text,threadsafe,innerNode,marker);
1210 singularPlural(text,threadsafe);
1211 text << " " << tlink << ".";
1215 else { // thread-safe
1216 if (!nonreentrant.isEmpty() || !reentrant.isEmpty()) {
1217 text << ", except for ";
1218 if (!reentrant.isEmpty()) {
1219 appendFullNames(text,reentrant,innerNode,marker);
1221 singularPlural(text,reentrant);
1222 text << " only " << rlink;
1223 if (!nonreentrant.isEmpty())
1226 if (!nonreentrant.isEmpty()) {
1227 appendFullNames(text,nonreentrant,innerNode,marker);
1229 singularPlural(text,nonreentrant);
1230 text << " nonreentrant.";
1237 text << "This " << typeString(node) << " is ";
1238 if (threadSafeness == Node::ThreadSafe)
1244 text << Atom::ParaRight;
1246 generateText(text,node,marker);
1250 This function is recursive.
1252 void Generator::generateTree(Tree *tree)
1255 generateInnerNode(tree->root());
1258 Generator *Generator::generatorForFormat(const QString& format)
1260 QList<Generator *>::ConstIterator g = generators.begin();
1261 while (g != generators.end()) {
1262 if ((*g)->format() == format)
1270 This function can be called if getLink() returns an empty
1271 string. It tests the \a atom string to see if it is a link
1272 of the form <element> :: <name>, where <element> is a QML
1273 element or component without a module qualifier. If so, it
1274 constructs a link to the <name> clause on the disambiguation
1275 page for <element> and returns that link string. It also
1276 adds the <name> as a target in the NameCollisionNode for
1277 <element>. These clauses are then constructed when the
1278 disambiguation page is actually generated.
1280 QString Generator::getCollisionLink(const Atom* atom)
1283 if (!atom->string().contains("::"))
1285 QStringList path = atom->string().split("::");
1286 NameCollisionNode* ncn = tree_->findCollisionNode(path[0]);
1289 if (atom->next() && atom->next()->next()) {
1290 if (atom->next()->type() == Atom::FormattingLeft &&
1291 atom->next()->next()->type() == Atom::String)
1292 label = atom->next()->next()->string();
1294 ncn->addLinkTarget(path[1],label);
1295 link = fileName(ncn);
1296 link += QLatin1Char('#');
1297 link += Doc::canonicalTitle(path[1]);
1304 Looks up the tag \a t in the map of metadata values for the
1305 current topic in \a inner. If a value for the tag is found,
1306 the value is returned.
1308 \note If \a t is found in the metadata map, it is erased.
1309 i.e. Once you call this function for a particular \a t,
1312 QString Generator::getMetadataElement(const InnerNode* inner, const QString& t)
1315 QStringMultiMap& metaTagMap = const_cast<QStringMultiMap&>(inner->doc().metaTagMap());
1316 QStringMultiMap::iterator i = metaTagMap.find(t);
1317 if (i != metaTagMap.end()) {
1319 metaTagMap.erase(i);
1325 Looks up the tag \a t in the map of metadata values for the
1326 current topic in \a inner. If values for the tag are found,
1327 they are returned in a string list.
1329 \note If \a t is found in the metadata map, all the pairs
1330 having the key \a t are erased. i.e. Once you call this
1331 function for a particular \a t, you consume \a t.
1333 QStringList Generator::getMetadataElements(const InnerNode* inner, const QString& t)
1336 QStringMultiMap& metaTagMap = const_cast<QStringMultiMap&>(inner->doc().metaTagMap());
1337 s = metaTagMap.values(t);
1339 metaTagMap.remove(t);
1344 Returns a relative path name for an image.
1346 QString Generator::imageFileName(const Node *relative, const QString& fileBase)
1348 QString userFriendlyFilePath;
1349 QString filePath = Config::findFile(
1350 relative->doc().location(), imageFiles, imageDirs, fileBase,
1351 imgFileExts[format()], userFriendlyFilePath);
1353 if (filePath.isEmpty())
1356 QString path = Config::copyFile(relative->doc().location(),
1358 userFriendlyFilePath,
1359 outputDir() + QLatin1String("/images"));
1360 QString images = "images";
1362 images.append(QLatin1Char('/'));
1363 return images + path;
1366 QString Generator::indent(int level, const QString& markedCode)
1375 while (i < (int) markedCode.length()) {
1376 if (markedCode.at(i) == QLatin1Char('\n')) {
1381 for (int j = 0; j < level; j++)
1382 t += QLatin1Char(' ');
1386 t += markedCode.at(i++);
1391 void Generator::initialize(const Config &config)
1393 outputFormats = config.getOutputFormats();
1394 if (!outputFormats.isEmpty()) {
1395 outDir_ = config.getOutputDir();
1396 baseDir_ = config.getString(CONFIG_BASEDIR);
1397 if (!baseDir_.isEmpty())
1398 config.location().warning(tr("\"basedir\" specified in config file. "
1399 "All output will be in module directories of the output directory"));
1400 if (outDir_.isEmpty())
1401 config.lastLocation().fatal(tr("No output directory specified in configuration file or on the command line"));
1404 if (dirInfo.exists(outDir_)) {
1405 if (!Config::removeDirContents(outDir_))
1406 config.lastLocation().error(tr("Cannot empty output directory '%1'").arg(outDir_));
1409 if (!dirInfo.mkpath(outDir_))
1410 config.lastLocation().fatal(tr("Cannot create output directory '%1'").arg(outDir_));
1413 if (!dirInfo.mkdir(outDir_ + "/images"))
1414 config.lastLocation().fatal(tr("Cannot create output directory '%1'")
1415 .arg(outDir_ + "/images"));
1416 if (!dirInfo.mkdir(outDir_ + "/images/used-in-examples"))
1417 config.lastLocation().fatal(tr("Cannot create output directory '%1'")
1418 .arg(outDir_ + "/images/used-in-examples"));
1419 if (!dirInfo.mkdir(outDir_ + "/scripts"))
1420 config.lastLocation().fatal(tr("Cannot create output directory '%1'")
1421 .arg(outDir_ + "/scripts"));
1422 if (!dirInfo.mkdir(outDir_ + "/style"))
1423 config.lastLocation().fatal(tr("Cannot create output directory '%1'")
1424 .arg(outDir_ + "/style"));
1427 imageFiles = config.getCleanPathList(CONFIG_IMAGES);
1428 imageDirs = config.getCleanPathList(CONFIG_IMAGEDIRS);
1429 scriptFiles = config.getCleanPathList(CONFIG_SCRIPTS);
1430 scriptDirs = config.getCleanPathList(CONFIG_SCRIPTDIRS);
1431 styleFiles = config.getCleanPathList(CONFIG_STYLES);
1432 styleDirs = config.getCleanPathList(CONFIG_STYLEDIRS);
1433 exampleDirs = config.getCleanPathList(CONFIG_EXAMPLEDIRS);
1434 exampleImgExts = config.getStringList(CONFIG_EXAMPLES + Config::dot +
1435 CONFIG_IMAGEEXTENSIONS);
1437 QString imagesDotFileExtensions =
1438 CONFIG_IMAGES + Config::dot + CONFIG_FILEEXTENSIONS;
1439 QSet<QString> formats = config.subVars(imagesDotFileExtensions);
1440 QSet<QString>::ConstIterator f = formats.begin();
1441 while (f != formats.end()) {
1442 imgFileExts[*f] = config.getStringList(imagesDotFileExtensions +
1447 QList<Generator *>::ConstIterator g = generators.begin();
1448 while (g != generators.end()) {
1449 if (outputFormats.contains((*g)->format())) {
1450 (*g)->initializeGenerator(config);
1451 QStringList extraImages =
1452 config.getCleanPathList(CONFIG_EXTRAIMAGES+Config::dot+(*g)->format());
1453 QStringList::ConstIterator e = extraImages.begin();
1454 while (e != extraImages.end()) {
1455 QString userFriendlyFilePath;
1456 QString filePath = Config::findFile(config.lastLocation(),
1460 imgFileExts[(*g)->format()],
1461 userFriendlyFilePath);
1462 if (!filePath.isEmpty())
1463 Config::copyFile(config.lastLocation(),
1465 userFriendlyFilePath,
1471 // Documentation template handling
1472 QString templateDir = config.getString(
1473 (*g)->format() + Config::dot + CONFIG_TEMPLATEDIR);
1475 if (!templateDir.isEmpty()) {
1477 QStringList searchDirs = QStringList() << templateDir;
1478 QStringList scripts =
1479 config.getCleanPathList((*g)->format()+Config::dot+CONFIG_SCRIPTS);
1480 e = scripts.begin();
1481 while (e != scripts.end()) {
1482 QString userFriendlyFilePath;
1483 QString filePath = Config::findFile(config.lastLocation(),
1488 userFriendlyFilePath);
1489 if (!filePath.isEmpty())
1490 Config::copyFile(config.lastLocation(),
1492 userFriendlyFilePath,
1498 QStringList styles =
1499 config.getCleanPathList((*g)->format()+Config::dot+CONFIG_STYLESHEETS);
1501 while (e != styles.end()) {
1502 QString userFriendlyFilePath;
1503 QString filePath = Config::findFile(config.lastLocation(),
1508 userFriendlyFilePath);
1509 if (!filePath.isEmpty())
1510 Config::copyFile(config.lastLocation(),
1512 userFriendlyFilePath,
1522 QRegExp secondParamAndAbove("[\2-\7]");
1523 QSet<QString> formattingNames = config.subVars(CONFIG_FORMATTING);
1524 QSet<QString>::ConstIterator n = formattingNames.begin();
1525 while (n != formattingNames.end()) {
1526 QString formattingDotName = CONFIG_FORMATTING + Config::dot + *n;
1528 QSet<QString> formats = config.subVars(formattingDotName);
1529 QSet<QString>::ConstIterator f = formats.begin();
1530 while (f != formats.end()) {
1531 QString def = config.getString(formattingDotName +
1533 if (!def.isEmpty()) {
1534 int numParams = Config::numParams(def);
1535 int numOccs = def.count("\1");
1537 if (numParams != 1) {
1538 config.lastLocation().warning(tr("Formatting '%1' must "
1540 "parameter (found %2)")
1541 .arg(*n).arg(numParams));
1543 else if (numOccs > 1) {
1544 config.lastLocation().fatal(tr("Formatting '%1' must "
1545 "contain exactly one "
1546 "occurrence of '\\1' "
1548 .arg(*n).arg(numOccs));
1551 int paramPos = def.indexOf("\1");
1552 fmtLeftMaps[*f].insert(*n, def.left(paramPos));
1553 fmtRightMaps[*f].insert(*n, def.mid(paramPos + 1));
1561 project = config.getString(CONFIG_PROJECT);
1563 QStringList prefixes = config.getStringList(CONFIG_OUTPUTPREFIXES);
1564 if (!prefixes.isEmpty()) {
1565 foreach (QString prefix, prefixes)
1566 outputPrefixes[prefix] = config.getString(
1567 CONFIG_OUTPUTPREFIXES + Config::dot + prefix);
1569 outputPrefixes[QLatin1String("QML")] = QLatin1String("qml-");
1572 void Generator::initializeGenerator(const Config & /* config */)
1576 bool Generator::matchAhead(const Atom *atom, Atom::Type expectedAtomType)
1578 return atom->next() != 0 && atom->next()->type() == expectedAtomType;
1582 Used for writing to the current output stream. Returns a
1583 reference to the current output stream, which is then used
1584 with the \c {<<} operator for writing.
1586 QTextStream &Generator::out()
1588 return *outStreamStack.top();
1591 QString Generator::outFileName()
1593 return QFileInfo(static_cast<QFile*>(out().device())->fileName()).fileName();
1596 QString Generator::outputPrefix(const QString &nodeType)
1598 return outputPrefixes[nodeType];
1601 bool Generator::parseArg(const QString& src,
1605 QStringRef* contents,
1609 #define SKIP_CHAR(c) \
1611 qDebug() << "looking for " << c << " at " << QString(src.data() + i, n - i); \
1612 if (i >= n || src[i] != c) { \
1614 qDebug() << " char '" << c << "' not found"; \
1620 #define SKIP_SPACE \
1621 while (i < n && src[i] == ' ') \
1627 // assume "<@" has been parsed outside
1631 if (tag != QStringRef(&src, i, tag.length())) {
1633 qDebug() << "tag " << tag << " not found at " << i;
1638 qDebug() << "haystack:" << src << "needle:" << tag << "i:" <<i;
1643 // parse stuff like: linkTag("(<@link node=\"([^\"]+)\">).*(</@link>)");
1646 // read parameter name
1648 while (i < n && src[i].isLetter())
1650 if (src[i] == '=') {
1652 qDebug() << "read parameter" << QString(src.data() + j, i - j);
1655 // skip parameter name
1657 while (i < n && src[i] != '"')
1659 *par1 = QStringRef(&src, j, i - j);
1664 qDebug() << "no optional parameter found";
1670 // find contents up to closing "</@tag>
1673 if (i + 4 + tag.length() > n)
1677 if (src[i + 1] != '/')
1679 if (src[i + 2] != '@')
1681 if (tag != QStringRef(&src, i + 3, tag.length()))
1683 if (src[i + 3 + tag.length()] != '>')
1688 *contents = QStringRef(&src, j, i - j);
1690 i += tag.length() + 4;
1694 qDebug() << " tag " << tag << " found: pos now: " << i;
1699 QString Generator::plainCode(const QString& markedCode)
1701 QString t = markedCode;
1702 t.replace(tag, QString());
1703 t.replace(quot, QLatin1String("\""));
1704 t.replace(gt, QLatin1String(">"));
1705 t.replace(lt, QLatin1String("<"));
1706 t.replace(amp, QLatin1String("&"));
1710 void Generator::setImageFileExtensions(const QStringList& extensions)
1712 imgFileExts[format()] = extensions;
1715 void Generator::singularPlural(Text& text, const NodeList& nodes)
1717 if (nodes.count() == 1)
1723 int Generator::skipAtoms(const Atom *atom, Atom::Type type) const
1726 atom = atom->next();
1727 while (atom != 0 && atom->type() != type) {
1729 atom = atom->next();
1734 void Generator::startText(const Node * /* relative */,
1735 CodeMarker * /* marker */)
1739 void Generator::supplementAlsoList(const Node *node, QList<Text> &alsoList)
1741 if (node->type() == Node::Function) {
1742 const FunctionNode *func = static_cast<const FunctionNode *>(node);
1743 if (func->overloadNumber() == 1) {
1744 QString alternateName;
1745 const FunctionNode *alternateFunc = 0;
1747 if (func->name().startsWith("set") && func->name().size() >= 4) {
1748 alternateName = func->name()[3].toLower();
1749 alternateName += func->name().mid(4);
1750 alternateFunc = func->parent()->findFunctionNode(alternateName);
1752 if (!alternateFunc) {
1753 alternateName = "is" + func->name().mid(3);
1754 alternateFunc = func->parent()->findFunctionNode(alternateName);
1755 if (!alternateFunc) {
1756 alternateName = "has" + func->name().mid(3);
1757 alternateFunc = func->parent()->findFunctionNode(alternateName);
1761 else if (!func->name().isEmpty()) {
1762 alternateName = "set";
1763 alternateName += func->name()[0].toUpper();
1764 alternateName += func->name().mid(1);
1765 alternateFunc = func->parent()->findFunctionNode(alternateName);
1768 if (alternateFunc && alternateFunc->access() != Node::Private) {
1770 for (i = 0; i < alsoList.size(); ++i) {
1771 if (alsoList.at(i).toString().contains(alternateName))
1775 if (i == alsoList.size()) {
1776 alternateName += "()";
1779 also << Atom(Atom::Link, alternateName)
1780 << Atom(Atom::FormattingLeft, ATOM_FORMATTING_LINK)
1782 << Atom(Atom::FormattingRight, ATOM_FORMATTING_LINK);
1783 alsoList.prepend(also);
1790 void Generator::terminate()
1792 QList<Generator *>::ConstIterator g = generators.begin();
1793 while (g != generators.end()) {
1794 if (outputFormats.contains((*g)->format()))
1795 (*g)->terminateGenerator();
1799 fmtLeftMaps.clear();
1800 fmtRightMaps.clear();
1801 imgFileExts.clear();
1805 QmlClassNode::terminate();
1806 ExampleNode::terminate();
1809 void Generator::terminateGenerator()
1814 Trims trailing whitespace off the \a string and returns
1817 QString Generator::trimmedTrailing(const QString& string)
1819 QString trimmed = string;
1820 while (trimmed.length() > 0 && trimmed[trimmed.length() - 1].isSpace())
1821 trimmed.truncate(trimmed.length() - 1);
1825 QString Generator::typeString(const Node *node)
1827 switch (node->type()) {
1828 case Node::Namespace:
1834 switch (node->subType()) {
1835 case Node::QmlClass:
1837 case Node::QmlPropertyGroup:
1838 return "property group";
1839 case Node::QmlBasicType:
1842 return "documentation";
1849 case Node::Function:
1851 case Node::Property:
1854 return "documentation";
1858 void Generator::unknownAtom(const Atom *atom)
1860 Location::internalError(tr("unknown atom type '%1' in %2 generator")
1861 .arg(atom->typeString()).arg(format()));