/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt Assistant of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qhelpgenerator_p.h" #include "qhelpdatainterface_p.h" #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE class QHelpGeneratorPrivate { public: QString error; QSqlQuery *query = nullptr; int namespaceId = -1; int virtualFolderId = -1; QMap fileMap; QMap > fileFilterMap; double progress; double oldProgress; double contentStep; double fileStep; double indexStep; }; /*! \internal \class QHelpGenerator \since 4.4 \brief The QHelpGenerator class generates a new Qt compressed help file (.qch). The help generator takes a help data structure as input for generating a new Qt compressed help files. Since the generation may takes some time, the generator emits various signals to inform about its current state. */ /*! \fn void QHelpGenerator::statusChanged(const QString &msg) This signal is emitted when the generation status changes. The status is basically a specific task like inserting files or building up the keyword index. The parameter \a msg contains the detailed status description. */ /*! \fn void QHelpGenerator::progressChanged(double progress) This signal is emitted when the progress changes. The \a progress ranges from 0 to 100. */ /*! \fn void QHelpGenerator::warning(const QString &msg) This signal is emitted when a non critical error occurs, e.g. when a referenced file cannot be found. \a msg contains the exact warning message. */ /*! Constructs a new help generator with the give \a parent. */ QHelpGenerator::QHelpGenerator(QObject *parent) : QObject(parent) { d = new QHelpGeneratorPrivate; } /*! Destructs the help generator. */ QHelpGenerator::~QHelpGenerator() { delete d; } /*! 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 QHelpGenerator::generate(QHelpDataInterface *helpData, const QString &outputFileName) { emit progressChanged(0); d->error.clear(); if (!helpData || helpData->namespaceName().isEmpty()) { d->error = tr("Invalid help data."); return false; } QString outFileName = outputFileName; if (outFileName.isEmpty()) { d->error = tr("No output file name specified."); return false; } QFileInfo fi(outFileName); if (fi.exists()) { if (!fi.dir().remove(fi.fileName())) { d->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) d->query = new QSqlQuery(db); } if (!openingOk) { d->error = tr("Cannot open data base file %1.").arg(outFileName); cleanupDB(); return false; } d->query->exec(QLatin1String("PRAGMA synchronous=OFF")); d->query->exec(QLatin1String("PRAGMA cache_size=3000")); addProgress(1.0); createTables(); insertFileNotFoundFile(); insertMetaData(helpData->metaData()); if (!registerVirtualFolder(helpData->virtualFolder(), helpData->namespaceName())) { d->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().count())); 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 QHelpGenerator::setupProgress(QHelpDataInterface *helpData) { d->progress = 0; d->oldProgress = 0; int numberOfFiles = 0; int numberOfIndices = 0; for (const QHelpDataFilterSection &fs : helpData->filterSections()) { numberOfFiles += fs.files().count(); numberOfIndices += fs.indices().count(); } // init 2% // filters 1% // contents 10% // files 60% // indices 27% d->contentStep = 10.0/(double)helpData->customFilters().count(); d->fileStep = 60.0/(double)numberOfFiles; d->indexStep = 27.0/(double)numberOfIndices; } void QHelpGenerator::addProgress(double step) { d->progress += step; if ((d->progress - d->oldProgress) >= 1.0 && d->progress <= 100.0) { d->oldProgress = d->progress; emit progressChanged(qCeil(d->progress)); } } void QHelpGenerator::cleanupDB() { if (d->query) { d->query->clear(); delete d->query; d->query = 0; } QSqlDatabase::removeDatabase(QLatin1String("builder")); } void QHelpGenerator::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 QHelpGenerator::error() const { return d->error; } bool QHelpGenerator::createTables() { if (!d->query) return false; d->query->exec(QLatin1String("SELECT COUNT(*) FROM sqlite_master WHERE TYPE=\'table\'" "AND Name=\'NamespaceTable\'")); d->query->next(); if (d->query->value(0).toInt() > 0) { d->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 IndexItemTable (" "Id INTEGER, " "IndexId INTEGER )") << 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 (!d->query->exec(q)) { d->error = tr("Cannot create tables."); return false; } } d->query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('qchVersion', '1.0')")); d->query->exec(QLatin1String("INSERT INTO MetaDataTable VALUES('CreationDate', '2012-12-20T12:00:00')")); return true; } bool QHelpGenerator::insertFileNotFoundFile() { if (!d->query) return false; d->query->exec(QLatin1String("SELECT id FROM FileNameTable WHERE Name=\'\'")); if (d->query->next() && d->query->isValid()) return true; d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES (Null, ?)")); d->query->bindValue(0, QByteArray()); if (!d->query->exec()) return false; const int fileId = d->query->lastInsertId().toInt(); d->query->prepare(QLatin1String("INSERT INTO FileNameTable (FolderId, Name, FileId, Title) " " VALUES (0, '', ?, '')")); d->query->bindValue(0, fileId); if (fileId > -1 && d->query->exec()) { d->fileMap.insert(QString(), fileId); return true; } return false; } bool QHelpGenerator::registerVirtualFolder(const QString &folderName, const QString &ns) { if (!d->query || folderName.isEmpty() || ns.isEmpty()) return false; d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); d->query->bindValue(0, folderName); d->query->exec(); d->query->next(); if (d->query->isValid() && d->query->value(0).toInt() > 0) return true; d->namespaceId = -1; d->query->prepare(QLatin1String("SELECT Id FROM NamespaceTable WHERE Name=?")); d->query->bindValue(0, ns); d->query->exec(); while (d->query->next()) { d->namespaceId = d->query->value(0).toInt(); break; } if (d->namespaceId < 0) { d->query->prepare(QLatin1String("INSERT INTO NamespaceTable VALUES(NULL, ?)")); d->query->bindValue(0, ns); if (d->query->exec()) d->namespaceId = d->query->lastInsertId().toInt(); } if (d->namespaceId > 0) { d->query->prepare(QLatin1String("SELECT Id FROM FolderTable WHERE Name=?")); d->query->bindValue(0, folderName); d->query->exec(); while (d->query->next()) d->virtualFolderId = d->query->value(0).toInt(); if (d->virtualFolderId > 0) return true; d->query->prepare(QLatin1String("INSERT INTO FolderTable (NamespaceId, Name) " "VALUES (?, ?)")); d->query->bindValue(0, d->namespaceId); d->query->bindValue(1, folderName); if (d->query->exec()) { d->virtualFolderId = d->query->lastInsertId().toInt(); return d->virtualFolderId > 0; } } d->error = tr("Cannot register virtual folder."); return false; } bool QHelpGenerator::insertFiles(const QStringList &files, const QString &rootPath, const QStringList &filterAttributes) { if (!d->query) return false; emit statusChanged(tr("Insert files...")); QList filterAtts; for (const QString &filterAtt : filterAttributes) { d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable " "WHERE Name=?")); d->query->bindValue(0, filterAtt); d->query->exec(); if (d->query->next()) filterAtts.append(d->query->value(0).toInt()); } int filterSetId = -1; d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileAttributeSetTable")); if (d->query->next()) filterSetId = d->query->value(0).toInt(); if (filterSetId < 0) return false; ++filterSetId; for (int attId : qAsConst(filterAtts)) { d->query->prepare(QLatin1String("INSERT INTO FileAttributeSetTable " "VALUES(?, ?)")); d->query->bindValue(0, filterSetId); d->query->bindValue(1, attId); d->query->exec(); } int tableFileId = 1; d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); if (d->query->next()) tableFileId = d->query->value(0).toInt() + 1; QString title; QString charSet; FileNameTableData fileNameData; 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"))) { charSet = QHelpGlobal::codecFromData(data); QTextStream stream(&data); stream.setCodec(QTextCodec::codecForName(charSet.toLatin1().constData())); title = QHelpGlobal::documentTitle(stream.readAll()); } else { title = fileName.mid(fileName.lastIndexOf(QLatin1Char('/')) + 1); } int fileId = -1; const auto &it = d->fileMap.constFind(fileName); if (it == d->fileMap.cend()) { fileDataList.append(qCompress(data)); fileNameData.name = fileName; fileNameData.fileId = tableFileId; fileNameData.title = title; fileNameDataList.append(fileNameData); d->fileMap.insert(fileName, tableFileId); d->fileFilterMap.insert(tableFileId, filterAtts.toSet()); tmpFileFilterMap.insert(tableFileId, filterAtts.toSet()); ++tableFileId; } else { fileId = it.value(); QSet &fileFilterSet = d->fileFilterMap[fileId]; QSet &tmpFileFilterSet = tmpFileFilterMap[fileId]; for (int filter : qAsConst(filterAtts)) { if (!fileFilterSet.contains(filter) && !tmpFileFilterSet.contains(filter)) { fileFilterSet.insert(filter); tmpFileFilterSet.insert(filter); } } } } if (!tmpFileFilterMap.isEmpty()) { d->query->exec(QLatin1String("BEGIN")); for (auto it = tmpFileFilterMap.cbegin(), end = tmpFileFilterMap.cend(); it != end; ++it) { QList filterValues = it.value().toList(); std::sort(filterValues.begin(), filterValues.end()); for (int fv : qAsConst(filterValues)) { d->query->prepare(QLatin1String("INSERT INTO FileFilterTable " "VALUES(?, ?)")); d->query->bindValue(0, fv); d->query->bindValue(1, it.key()); d->query->exec(); } } for (const QByteArray &fileData : qAsConst(fileDataList)) { d->query->prepare(QLatin1String("INSERT INTO FileDataTable VALUES " "(Null, ?)")); d->query->bindValue(0, fileData); d->query->exec(); if (++i % 20 == 0) addProgress(d->fileStep * 20.0); } for (const FileNameTableData &fnd : qAsConst(fileNameDataList)) { d->query->prepare(QLatin1String("INSERT INTO FileNameTable " "(FolderId, Name, FileId, Title) VALUES (?, ?, ?, ?)")); d->query->bindValue(0, 1); d->query->bindValue(1, fnd.name); d->query->bindValue(2, fnd.fileId); d->query->bindValue(3, fnd.title); d->query->exec(); } d->query->exec(QLatin1String("COMMIT")); } d->query->exec(QLatin1String("SELECT MAX(Id) FROM FileDataTable")); if (d->query->next() && d->query->value(0).toInt() == tableFileId - 1) { addProgress(d->fileStep*(i % 20)); return true; } return false; } bool QHelpGenerator::registerCustomFilter(const QString &filterName, const QStringList &filterAttribs, bool forceUpdate) { if (!d->query) return false; d->query->exec(QLatin1String("SELECT Id, Name FROM FilterAttributeTable")); QStringList idsToInsert = filterAttribs; QMap attributeMap; while (d->query->next()) { attributeMap.insert(d->query->value(1).toString(), d->query->value(0).toInt()); idsToInsert.removeAll(d->query->value(1).toString()); } for (const QString &id : qAsConst(idsToInsert)) { d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); d->query->bindValue(0, id); d->query->exec(); attributeMap.insert(id, d->query->lastInsertId().toInt()); } int nameId = -1; d->query->prepare(QLatin1String("SELECT Id FROM FilterNameTable WHERE Name=?")); d->query->bindValue(0, filterName); d->query->exec(); while (d->query->next()) { nameId = d->query->value(0).toInt(); break; } if (nameId < 0) { d->query->prepare(QLatin1String("INSERT INTO FilterNameTable VALUES(NULL, ?)")); d->query->bindValue(0, filterName); if (d->query->exec()) nameId = d->query->lastInsertId().toInt(); } else if (!forceUpdate) { d->error = tr("The filter %1 is already registered.").arg(filterName); return false; } if (nameId < 0) { d->error = tr("Cannot register filter %1.").arg(filterName); return false; } d->query->prepare(QLatin1String("DELETE FROM FilterTable WHERE NameId=?")); d->query->bindValue(0, nameId); d->query->exec(); for (const QString &att : filterAttribs) { d->query->prepare(QLatin1String("INSERT INTO FilterTable VALUES(?, ?)")); d->query->bindValue(0, nameId); d->query->bindValue(1, attributeMap[att]); if (!d->query->exec()) return false; } return true; } bool QHelpGenerator::insertKeywords(const QList &keywords, const QStringList &filterAttributes) { if (!d->query) return false; emit statusChanged(tr("Insert indices...")); int indexId = 1; d->query->exec(QLatin1String("SELECT MAX(Id) FROM IndexTable")); if (d->query->next()) indexId = d->query->value(0).toInt() + 1; QList filterAtts; for (const QString &filterAtt : filterAttributes) { d->query->prepare(QLatin1String("SELECT Id FROM FilterAttributeTable WHERE Name=?")); d->query->bindValue(0, filterAtt); d->query->exec(); if (d->query->next()) filterAtts.append(d->query->value(0).toInt()); } QList indexFilterTable; int i = 0; d->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 = d->fileMap.constFind(fName); const int fileId = it == d->fileMap.cend() ? 1 : it.value(); d->query->prepare(QLatin1String("INSERT INTO IndexTable (Name, Identifier, NamespaceId, FileId, Anchor) " "VALUES(?, ?, ?, ?, ?)")); d->query->bindValue(0, itm.name); d->query->bindValue(1, itm.identifier); d->query->bindValue(2, d->namespaceId); d->query->bindValue(3, fileId); d->query->bindValue(4, anchor); d->query->exec(); indexFilterTable.append(indexId++); if (++i % 100 == 0) addProgress(d->indexStep * 100.0); } d->query->exec(QLatin1String("COMMIT")); d->query->exec(QLatin1String("BEGIN")); for (int idx : qAsConst(indexFilterTable)) { for (int a : qAsConst(filterAtts)) { d->query->prepare(QLatin1String("INSERT INTO IndexFilterTable (FilterAttributeId, IndexId) " "VALUES(?, ?)")); d->query->bindValue(0, a); d->query->bindValue(1, idx); d->query->exec(); } } d->query->exec(QLatin1String("COMMIT")); d->query->exec(QLatin1String("SELECT COUNT(Id) FROM IndexTable")); if (d->query->next() && d->query->value(0).toInt() >= indices.count()) return true; return false; } bool QHelpGenerator::insertContents(const QByteArray &ba, const QStringList &filterAttributes) { if (!d->query) return false; emit statusChanged(tr("Insert contents...")); d->query->prepare(QLatin1String("INSERT INTO ContentsTable (NamespaceId, Data) " "VALUES(?, ?)")); d->query->bindValue(0, d->namespaceId); d->query->bindValue(1, ba); d->query->exec(); int contentId = d->query->lastInsertId().toInt(); if (contentId < 1) { d->error = tr("Cannot insert contents."); return false; } // associate the filter attributes for (const QString &filterAtt : filterAttributes) { d->query->prepare(QLatin1String("INSERT INTO ContentsFilterTable (FilterAttributeId, ContentsId) " "SELECT Id, ? FROM FilterAttributeTable WHERE Name=?")); d->query->bindValue(0, contentId); d->query->bindValue(1, filterAtt); d->query->exec(); if (!d->query->isActive()) { d->error = tr("Cannot register contents."); return false; } } addProgress(d->contentStep); return true; } bool QHelpGenerator::insertFilterAttributes(const QStringList &attributes) { if (!d->query) return false; d->query->exec(QLatin1String("SELECT Name FROM FilterAttributeTable")); QSet atts; while (d->query->next()) atts.insert(d->query->value(0).toString()); for (const QString &s : attributes) { if (!atts.contains(s)) { d->query->prepare(QLatin1String("INSERT INTO FilterAttributeTable VALUES(NULL, ?)")); d->query->bindValue(0, s); d->query->exec(); } } return true; } bool QHelpGenerator::insertMetaData(const QMap &metaData) { if (!d->query) return false; for (auto it = metaData.cbegin(), end = metaData.cend(); it != end; ++it) { d->query->prepare(QLatin1String("INSERT INTO MetaDataTable VALUES(?, ?)")); d->query->bindValue(0, it.key()); d->query->bindValue(1, it.value()); d->query->exec(); } return true; } bool QHelpGenerator::checkLinks(const QHelpDataInterface &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 : qAsConst(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 QRegExp linkPattern(QLatin1String("<(?:a href|img src)=\"?([^#\">]+)[#\">]")); QTextStream stream(&htmlFile); const QString codec = QHelpGlobal::codecFromData(htmlFile.read(1000)); stream.setCodec(QTextCodec::codecForName(codec.toLatin1().constData())); const QString &content = stream.readAll(); QStringList invalidLinks; for (int pos = linkPattern.indexIn(content); pos != -1; pos = linkPattern.indexIn(content, pos + 1)) { const QString &linkedFileName = linkPattern.cap(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) d->error = tr("Invalid links in HTML files."); return allLinksOk; } QT_END_NAMESPACE