// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "helpgenerator.h" #include "qhelpprojectdata_p.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE class HelpGeneratorPrivate : public QObject { Q_OBJECT public: HelpGeneratorPrivate(QObject *parent = nullptr) : QObject(parent) {} bool generate(QHelpProjectData *helpData, const QString &outputFileName); bool checkLinks(const QHelpProjectData &helpData); QString error() const; Q_SIGNALS: void statusChanged(const QString &msg); void progressChanged(double progress); void warning(const QString &msg); private: struct FileNameTableData { QString name; int fileId; QString title; }; void writeTree(QDataStream &s, QHelpDataContentItem *item, int depth); bool createTables(); bool insertFileNotFoundFile(); bool registerCustomFilter(const QString &filterName, const QStringList &filterAttribs, bool forceUpdate = false); bool registerVirtualFolder(const QString &folderName, const QString &ns); bool insertFilterAttributes(const QStringList &attributes); bool insertKeywords(const QList &keywords, const QStringList &filterAttributes); bool insertFiles(const QStringList &files, const QString &rootPath, const QStringList &filterAttributes); bool insertContents(const QByteArray &ba, const QStringList &filterAttributes); bool insertMetaData(const QMap &metaData); void cleanupDB(); void setupProgress(QHelpProjectData *helpData); void addProgress(double step); QString m_error; QSqlQuery *m_query = nullptr; int m_namespaceId = -1; int m_virtualFolderId = -1; QMap m_fileMap; QMap > m_fileFilterMap; double m_progress; double m_oldProgress; double m_contentStep; double m_fileStep; double m_indexStep; }; /*! Takes the \a helpData and generates a new documentation set from it. The Qt compressed help file is written to \a outputFileName. Returns true on success, otherwise false. */ bool HelpGeneratorPrivate::generate(QHelpProjectData *helpData, const QString &outputFileName) { emit progressChanged(0); m_error.clear(); if (!helpData || helpData->namespaceName().isEmpty()) { m_error = tr("Invalid help data."); return false; } QString outFileName = outputFileName; if (outFileName.isEmpty()) { m_error = tr("No output file name specified."); return false; } QFileInfo fi(outFileName); if (fi.exists()) { if (!fi.dir().remove(fi.fileName())) { m_error = tr("The file %1 cannot be overwritten.").arg(outFileName); return false; } } setupProgress(helpData); emit statusChanged(tr("Building up file structure...")); bool openingOk = true; { QSqlDatabase db = QSqlDatabase::addDatabase(QLatin1String("QSQLITE"), QLatin1String("builder")); db.setDatabaseName(outFileName); openingOk = db.open(); if (openingOk) m_query = new QSqlQuery(db); } if (!openingOk) { m_error = tr("Cannot open data base file %1.").arg(outFileName); cleanupDB(); return false; } m_query->exec(QLatin1String("PRAGMA synchronous=OFF")); m_query->exec(QLatin1String("PRAGMA cache_size=3000")); addProgress(1.0); createTables(); insertFileNotFoundFile(); insertMetaData(helpData->metaData()); if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) { m_error = tr("Cannot register namespace \"%1\".").arg(helpData->namespaceName()); cleanupDB(); return false; } addProgress(1.0); emit statusChanged(tr("Insert custom filters...")); for (const QHelpDataCustomFilter &f : helpData->customFilters()) { if (!registerCustomFilter(f.name, f.filterAttributes, true)) { cleanupDB(); return false; } } addProgress(1.0); int i = 1; for (const QHelpDataFilterSection &fs : helpData->filterSections()) { emit statusChanged(tr("Insert help data for filter section (%1 of %2)...") .arg(i++).arg(helpData->filterSections().size())); insertFilterAttributes(fs.filterAttributes()); QByteArray ba; QDataStream s(&ba, QIODevice::WriteOnly); for (QHelpDataContentItem *itm : fs.contents()) writeTree(s, itm, 0); if (!insertFiles(fs.files(), helpData->rootPath(), fs.filterAttributes()) || !insertContents(ba, fs.filterAttributes()) || !insertKeywords(fs.indices(), fs.filterAttributes())) { cleanupDB(); return false; } } cleanupDB(); emit progressChanged(100); emit statusChanged(tr("Documentation successfully generated.")); return true; } void HelpGeneratorPrivate::setupProgress(QHelpProjectData *helpData) { m_progress = 0; m_oldProgress = 0; int numberOfFiles = 0; int numberOfIndices = 0; for (const QHelpDataFilterSection &fs : helpData->filterSections()) { numberOfFiles += fs.files().size(); numberOfIndices += fs.indices().size(); } // init 2% // filters 1% // contents 10% // files 60% // indices 27% m_contentStep = 10.0 / qMax(helpData->customFilters().size(), 1); m_fileStep = 60.0 / qMax(numberOfFiles, 1); m_indexStep = 27.0 / qMax(numberOfIndices, 1); } void HelpGeneratorPrivate::addProgress(double step) { m_progress += step; if ((m_progress - m_oldProgress) >= 1.0 && m_progress <= 100.0) { m_oldProgress = m_progress; emit progressChanged(qCeil(m_progress)); } } void HelpGeneratorPrivate::cleanupDB() { if (m_query) { m_query->clear(); delete m_query; m_query = nullptr; } QSqlDatabase::removeDatabase(QLatin1String("builder")); } void HelpGeneratorPrivate::writeTree(QDataStream &s, QHelpDataContentItem *item, int depth) { s << depth; s << item->reference(); s << item->title(); for (QHelpDataContentItem *i : item->children()) writeTree(s, i, depth + 1); } /*! Returns the last error message. */ QString HelpGeneratorPrivate::error() const { return m_error; } bool HelpGeneratorPrivate::createTables() { if (!m_query) return false; m_query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'" "AND Name=\'NamespaceTable\'")); m_query->next(); if (m_query->value(0).toInt() > 0) { m_error = tr("Some tables already exist."); return false; } const QStringList tables = QStringList() << QLatin1String("CREATE TABLE NamespaceTable (" "Id INTEGER PRIMARY KEY," "Name TEXT )") << QLatin1String("CREATE TABLE FilterAttributeTable (" "Id INTEGER PRIMARY KEY, " "Name TEXT )") << QLatin1String("CREATE TABLE FilterNameTable (" "Id INTEGER PRIMARY KEY, " "Name TEXT )") << QLatin1String("CREATE TABLE FilterTable (" "NameId INTEGER, " "FilterAttributeId INTEGER )") << QLatin1String("CREATE TABLE IndexTable (" "Id INTEGER PRIMARY KEY, " "Name TEXT, " "Identifier TEXT, " "NamespaceId INTEGER, " "FileId INTEGER, " "Anchor TEXT )") << QLatin1String("CREATE TABLE IndexFilterTable (" "FilterAttributeId INTEGER, " "IndexId INTEGER )") << QLatin1String("CREATE TABLE ContentsTable (" "Id INTEGER PRIMARY KEY, " "NamespaceId INTEGER, " "Data BLOB )") << QLatin1String("CREATE TABLE ContentsFilterTable (" "FilterAttributeId INTEGER, " "ContentsId INTEGER )") << QLatin1String("CREATE TABLE FileAttributeSetTable (" "Id INTEGER, " "FilterAttributeId INTEGER )") << QLatin1String("CREATE TABLE FileDataTable (" "Id INTEGER PRIMARY KEY, " "Data BLOB )") << QLatin1String("CREATE TABLE FileFilterTable (" "FilterAttributeId INTEGER, " "FileId INTEGER )") << QLatin1String("CREATE TABLE FileNameTable (" "FolderId INTEGER, " "Name TEXT, " "FileId INTEGER, " "Title TEXT )") << QLatin1String("CREATE TABLE FolderTable(" "Id INTEGER PRIMARY KEY, " "Name Text, " "NamespaceID INTEGER )") << QLatin1String("CREATE TABLE MetaDataTable(" "Name Text, " "Value BLOB )"); for (const QString &q : tables) { if (!m_query->exec(q)) { m_error = tr("Cannot create tables."); return false; } } m_query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')")); return true; } bool HelpGeneratorPrivate::insertFileNotFoundFile() { if (!m_query) return false; m_query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'")); if (m_query->next() && m_query->isValid()) return true; m_query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)")); m_query->bindValue(0, QByteArray()); if (!m_query->exec()) return false; const int fileId = m_query->lastInsertId().toInt(); m_query->prepare(QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) " " VALUES (0, '', ?, '')")); m_query->bindValue(0, fileId); if (fileId > -1 && m_query->exec()) { m_fileMap.insert({}, fileId); return true; } return false; } bool HelpGeneratorPrivate::registerVirtualFolder(const QString &folderName, const QString &ns) { if (!m_query || folderName.isEmpty() || ns.isEmpty()) return false; m_query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); m_query->bindValue(0, folderName); m_query->exec(); m_query->next(); if (m_query->isValid() && m_query->value(0).toInt() > 0) return true; m_namespaceId = -1; m_query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?")); m_query->bindValue(0, ns); m_query->exec(); while (m_query->next()) { m_namespaceId = m_query->value(0).toInt(); break; } if (m_namespaceId < 0) { m_query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)")); m_query->bindValue(0, ns); if (m_query->exec()) m_namespaceId = m_query->lastInsertId().toInt(); } if (m_namespaceId > 0) { m_query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); m_query->bindValue(0, folderName); m_query->exec(); while (m_query->next()) m_virtualFolderId = m_query->value(0).toInt(); if (m_virtualFolderId > 0) return true; m_query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) " "VALUES (?, ?)")); m_query->bindValue(0, m_namespaceId); m_query->bindValue(1, folderName); if (m_query->exec()) { m_virtualFolderId = m_query->lastInsertId().toInt(); return m_virtualFolderId > 0; } } m_error = tr("Cannot register virtual folder."); return false; } bool HelpGeneratorPrivate::insertFiles(const QStringList &files, const QString &rootPath, const QStringList &filterAttributes) { if (!m_query) return false; emit statusChanged(tr("Insert files...")); QSet filterAtts; for (const QString &filterAtt : filterAttributes) { m_query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable " "WHERE Name=?")); m_query->bindValue(0, filterAtt); m_query->exec(); if (m_query->next()) filterAtts.insert(m_query->value(0).toInt()); } int filterSetId = -1; m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable")); if (m_query->next()) filterSetId = m_query->value(0).toInt(); if (filterSetId < 0) return false; ++filterSetId; QList attValues = filterAtts.values(); std::sort(attValues.begin(), attValues.end()); for (int attId : std::as_const(attValues)) { m_query->prepare(QLatin1String("INSERT INTO FileAttributeSetTable " "VALUES(?, ?)")); m_query->bindValue(0, filterSetId); m_query->bindValue(1, attId); m_query->exec(); } int tableFileId = 1; m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); if (m_query->next()) tableFileId = m_query->value(0).toInt() + 1; QString title; QString charSet; QList fileDataList; QMap > tmpFileFilterMap; QList fileNameDataList; int i = 0; for (const QString &file : files) { const QString fileName = QDir::cleanPath(file); QFile fi(rootPath + QDir::separator() + fileName); if (!fi.exists()) { emit warning(tr("The file %1 does not exist, skipping it...") .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName))); continue; } if (!fi.open(QIODevice::ReadOnly)) { emit warning(tr("Cannot open file %1, skipping it...") .arg(QDir::cleanPath(rootPath + QDir::separator() + fileName))); continue; } QByteArray data = fi.readAll(); if (fileName.endsWith(QLatin1String(".html")) || fileName.endsWith(QLatin1String(".htm"))) { auto encoding = QStringDecoder::encodingForHtml(data); if (!encoding) encoding = QStringDecoder::Utf8; title = QHelpGlobal::documentTitle(QStringDecoder(*encoding)(data)); } else { title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1); } int fileId = -1; const auto &it = m_fileMap.constFind(fileName); if (it == m_fileMap.cend()) { fileDataList.append(qCompress(data)); FileNameTableData fileNameData; fileNameData.name = fileName; fileNameData.fileId = tableFileId; fileNameData.title = title; fileNameDataList.append(fileNameData); m_fileMap.insert(fileName, tableFileId); m_fileFilterMap.insert(tableFileId, filterAtts); tmpFileFilterMap.insert(tableFileId, filterAtts); ++tableFileId; } else { fileId = it.value(); QSet &fileFilterSet = m_fileFilterMap[fileId]; QSet &tmpFileFilterSet = tmpFileFilterMap[fileId]; for (int filter : std::as_const(filterAtts)) { if (!fileFilterSet.contains(filter) && !tmpFileFilterSet.contains(filter)) { fileFilterSet.insert(filter); tmpFileFilterSet.insert(filter); } } } } if (!tmpFileFilterMap.isEmpty()) { m_query->exec(QLatin1String("BEGIN")); for (auto it = tmpFileFilterMap.cbegin(), end = tmpFileFilterMap.cend(); it != end; ++it) { QList filterValues = it.value().values(); std::sort(filterValues.begin(), filterValues.end()); for (int fv : std::as_const(filterValues)) { m_query->prepare(QLatin1String("INSERT INTO FileFilterTable " "VALUES(?, ?)")); m_query->bindValue(0, fv); m_query->bindValue(1, it.key()); m_query->exec(); } } for (const QByteArray &fileData : std::as_const(fileDataList)) { m_query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES " "(Null, ?)")); m_query->bindValue(0, fileData); m_query->exec(); if (++i % 20 == 0) addProgress(m_fileStep * 20.0); } for (const FileNameTableData &fnd : std::as_const(fileNameDataList)) { m_query->prepare(QLatin1String("INSERT INTO FileNameTable " "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)")); m_query->bindValue(0, 1); m_query->bindValue(1, fnd.name); m_query->bindValue(2, fnd.fileId); m_query->bindValue(3, fnd.title); m_query->exec(); } m_query->exec(QLatin1String("COMMIT")); } m_query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); if (m_query->next() && m_query->value(0).toInt() == tableFileId - 1) { addProgress(m_fileStep*(i % 20)); return true; } return false; } bool HelpGeneratorPrivate::registerCustomFilter(const QString &filterName, const QStringList &filterAttribs, bool forceUpdate) { if (!m_query) return false; m_query->exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable")); QStringList idsToInsert = filterAttribs; QMap attributeMap; while (m_query->next()) { attributeMap.insert(m_query->value(1).toString(), m_query->value(0).toInt()); idsToInsert.removeAll(m_query->value(1).toString()); } for (const QString &id : std::as_const(idsToInsert)) { m_query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); m_query->bindValue(0, id); m_query->exec(); attributeMap.insert(id, m_query->lastInsertId().toInt()); } int nameId = -1; m_query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?")); m_query->bindValue(0, filterName); m_query->exec(); while (m_query->next()) { nameId = m_query->value(0).toInt(); break; } if (nameId < 0) { m_query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)")); m_query->bindValue(0, filterName); if (m_query->exec()) nameId = m_query->lastInsertId().toInt(); } else if (!forceUpdate) { m_error = tr("The filter %1 is already registered.").arg(filterName); return false; } if (nameId < 0) { m_error = tr("Cannot register filter %1.").arg(filterName); return false; } m_query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?")); m_query->bindValue(0, nameId); m_query->exec(); for (const QString &att : filterAttribs) { m_query->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)")); m_query->bindValue(0, nameId); m_query->bindValue(1, attributeMap[att]); if (!m_query->exec()) return false; } return true; } bool HelpGeneratorPrivate::insertKeywords(const QList &keywords, const QStringList &filterAttributes) { if (!m_query) return false; emit statusChanged(tr("Insert indices...")); int indexId = 1; m_query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable")); if (m_query->next()) indexId = m_query->value(0).toInt() + 1; QList filterAtts; for (const QString &filterAtt : filterAttributes) { m_query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?")); m_query->bindValue(0, filterAtt); m_query->exec(); if (m_query->next()) filterAtts.append(m_query->value(0).toInt()); } QList indexFilterTable; int i = 0; m_query->exec(QLatin1String("BEGIN")); QSet indices; for (const QHelpDataIndexItem &itm : keywords) { // Identical ids make no sense and just confuse the Assistant user, // so we ignore all repetitions. if (indices.contains(itm.identifier)) continue; // Still empty ids should be ignored, as otherwise we will include only // the first keyword with an empty id. if (!itm.identifier.isEmpty()) indices.insert(itm.identifier); const int pos = itm.reference.indexOf(QLatin1Char('#')); const QString &fileName = itm.reference.left(pos); const QString anchor = pos < 0 ? QString() : itm.reference.mid(pos + 1); const QString &fName = QDir::cleanPath(fileName); const auto &it = m_fileMap.constFind(fName); const int fileId = it == m_fileMap.cend() ? 1 : it.value(); m_query->prepare(QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) " "VALUES(?, ?, ?, ?, ?)")); m_query->bindValue(0, itm.name); m_query->bindValue(1, itm.identifier); m_query->bindValue(2, m_namespaceId); m_query->bindValue(3, fileId); m_query->bindValue(4, anchor); m_query->exec(); indexFilterTable.append(indexId++); if (++i % 100 == 0) addProgress(m_indexStep * 100.0); } m_query->exec(QLatin1String("COMMIT")); m_query->exec(QLatin1String("BEGIN")); for (int idx : std::as_const(indexFilterTable)) { for (int a : std::as_const(filterAtts)) { m_query->prepare(QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) " "VALUES(?, ?)")); m_query->bindValue(0, a); m_query->bindValue(1, idx); m_query->exec(); } } m_query->exec(QLatin1String("COMMIT")); m_query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable")); if (m_query->next() && m_query->value(0).toInt() >= indices.size()) return true; return false; } bool HelpGeneratorPrivate::insertContents(const QByteArray &ba, const QStringList &filterAttributes) { if (!m_query) return false; emit statusChanged(tr("Insert contents...")); m_query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) " "VALUES(?, ?)")); m_query->bindValue(0, m_namespaceId); m_query->bindValue(1, ba); m_query->exec(); int contentId = m_query->lastInsertId().toInt(); if (contentId < 1) { m_error = tr("Cannot insert contents."); return false; } // associate the filter attributes for (const QString &filterAtt : filterAttributes) { m_query->prepare(QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) " "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?")); m_query->bindValue(0, contentId); m_query->bindValue(1, filterAtt); m_query->exec(); if (!m_query->isActive()) { m_error = tr("Cannot register contents."); return false; } } addProgress(m_contentStep); return true; } bool HelpGeneratorPrivate::insertFilterAttributes(const QStringList &attributes) { if (!m_query) return false; m_query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); QSet atts; while (m_query->next()) atts.insert(m_query->value(0).toString()); for (const QString &s : attributes) { if (!atts.contains(s)) { m_query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); m_query->bindValue(0, s); m_query->exec(); } } return true; } bool HelpGeneratorPrivate::insertMetaData(const QMap &metaData) { if (!m_query) return false; for (auto it = metaData.cbegin(), end = metaData.cend(); it != end; ++it) { m_query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)")); m_query->bindValue(0, it.key()); m_query->bindValue(1, it.value()); m_query->exec(); } return true; } bool HelpGeneratorPrivate::checkLinks(const QHelpProjectData &helpData) { /* * Step 1: Gather the canoncal file paths of all files in the project. * We use a set, because there will be a lot of look-ups. */ QSet files; for (const QHelpDataFilterSection &filterSection : helpData.filterSections()) { for (const QString &file : filterSection.files()) { const QFileInfo fileInfo(helpData.rootPath() + QDir::separator() + file); const QString &canonicalFileName = fileInfo.canonicalFilePath(); if (!fileInfo.exists()) emit warning(tr("File \"%1\" does not exist.").arg(file)); else files.insert(canonicalFileName); } } /* * Step 2: Check the hypertext and image references of all HTML files. * Note that we don't parse the files, but simply grep for the * respective HTML elements. Therefore. contents that are e.g. * commented out can cause false warning. */ bool allLinksOk = true; for (const QString &fileName : std::as_const(files)) { if (!fileName.endsWith(QLatin1String("html")) && !fileName.endsWith(QLatin1String("htm"))) continue; QFile htmlFile(fileName); if (!htmlFile.open(QIODevice::ReadOnly)) { emit warning(tr("File \"%1\" cannot be opened.").arg(fileName)); continue; } const QRegularExpression linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]")); QByteArray data = htmlFile.readAll(); auto encoding = QStringDecoder::encodingForHtml(data); if (!encoding) encoding = QStringDecoder::Utf8; const QString &content = QStringDecoder(*encoding)(data); QStringList invalidLinks; QRegularExpressionMatch match; int pos = 0; while ((match = linkPattern.match(content, pos)).hasMatch()) { pos = match.capturedEnd(); const QString &linkedFileName = match.captured(1); if (linkedFileName.contains(QLatin1String("://"))) continue; const QString &curDir = QFileInfo(fileName).dir().path(); const QString &canonicalLinkedFileName = QFileInfo(curDir + QDir::separator() + linkedFileName).canonicalFilePath(); if (!files.contains(canonicalLinkedFileName) && !invalidLinks.contains(canonicalLinkedFileName)) { emit warning(tr("File \"%1\" contains an invalid link to file \"%2\""). arg(fileName).arg(linkedFileName)); allLinksOk = false; invalidLinks.append(canonicalLinkedFileName); } } } if (!allLinksOk) m_error = tr("Invalid links in HTML files."); return allLinksOk; } ////////////////////////////// HelpGenerator::HelpGenerator(bool silent) { m_private = new HelpGeneratorPrivate(this); if (!silent) { connect(m_private, &HelpGeneratorPrivate::statusChanged, this, &HelpGenerator::printStatus); } connect(m_private, &HelpGeneratorPrivate::warning, this, &HelpGenerator::printWarning); } bool HelpGenerator::generate(QHelpProjectData *helpData, const QString &outputFileName) { return m_private->generate(helpData, outputFileName); } bool HelpGenerator::checkLinks(const QHelpProjectData &helpData) { return m_private->checkLinks(helpData); } QString HelpGenerator::error() const { return m_private->error(); } void HelpGenerator::printStatus(const QString &msg) { puts(qPrintable(msg)); } void HelpGenerator::printWarning(const QString &msg) { puts(qPrintable(tr("Warning: %1").arg(msg))); } QT_END_NAMESPACE #include "helpgenerator.moc"