File indexing completed on 2024-05-12 05:10:12

0001 /***************************************************************************
0002     Copyright (C) 2003-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "htmlexporter.h"
0026 #include "xslthandler.h"
0027 #include "tellicoxmlexporter.h"
0028 #include "../collection.h"
0029 #include "../document.h"
0030 #include "../core/filehandler.h"
0031 #include "../core/netaccess.h"
0032 #include "../config/tellico_config.h"
0033 #include "../core/tellico_strings.h"
0034 #include "../images/image.h"
0035 #include "../images/imagefactory.h"
0036 #include "../images/imageinfo.h"
0037 #include "../utils/tellico_utils.h"
0038 #include "../utils/string_utils.h"
0039 #include "../utils/datafileregistry.h"
0040 #include "../progressmanager.h"
0041 #include "../utils/cursorsaver.h"
0042 #include "../tellico_debug.h"
0043 
0044 #include <KConfigGroup>
0045 #include <KIO/MkdirJob>
0046 #include <KIO/FileCopyJob>
0047 #include <KIO/DeleteJob>
0048 #include <KLocalizedString>
0049 #include <KUser>
0050 #include <KJobWidgets>
0051 
0052 #include <QDir>
0053 #include <QDomDocument>
0054 #include <QGroupBox>
0055 #include <QCheckBox>
0056 #include <QFile>
0057 #include <QLabel>
0058 #include <QTextStream>
0059 #include <QVBoxLayout>
0060 #include <QFileInfo>
0061 #include <QApplication>
0062 #include <QLocale>
0063 
0064 extern "C" {
0065 #include <libxml/HTMLparser.h>
0066 #include <libxml/HTMLtree.h>
0067 }
0068 
0069 using Tellico::Export::HTMLExporter;
0070 
0071 HTMLExporter::HTMLExporter(Tellico::Data::CollPtr coll_) : Tellico::Export::Exporter(coll_),
0072     m_handler(nullptr),
0073     m_printHeaders(true),
0074     m_printGrouped(false),
0075     m_exportEntryFiles(false),
0076     m_cancelled(false),
0077     m_parseDOM(true),
0078     m_checkCreateDir(true),
0079     m_checkCommonFile(true),
0080     m_imageWidth(0),
0081     m_imageHeight(0),
0082     m_widget(nullptr),
0083     m_checkPrintHeaders(nullptr),
0084     m_checkPrintGrouped(nullptr),
0085     m_checkExportEntryFiles(nullptr),
0086     m_checkExportImages(nullptr),
0087     m_xsltFile(QStringLiteral("tellico2html.xsl")) {
0088 }
0089 
0090 HTMLExporter::~HTMLExporter() {
0091   delete m_handler;
0092   m_handler = nullptr;
0093 }
0094 
0095 QString HTMLExporter::formatString() const {
0096   return QStringLiteral("HTML");
0097 }
0098 
0099 QString HTMLExporter::fileFilter() const {
0100   return i18n("HTML Files") + QLatin1String(" (*.html)") + QLatin1String(";;") + i18n("All Files") + QLatin1String(" (*)");
0101 }
0102 
0103 void HTMLExporter::reset() {
0104   // since the ExportUTF8 option may have changed, need to delete handler
0105   delete m_handler;
0106   m_handler = nullptr;
0107   m_files.clear();
0108   m_links.clear();
0109   m_copiedFiles.clear();
0110 }
0111 
0112 bool HTMLExporter::exec() {
0113   if(url().isEmpty() || !url().isValid()) {
0114     myWarning() << "trying to export to invalid URL";
0115     return false;
0116   }
0117 
0118   // check file exists first
0119   // if we're not forcing, ask use
0120   bool force = (options() & Export::ExportForce) || FileHandler::queryExists(url());
0121   if(!force) {
0122     return false;
0123   }
0124 
0125   m_cancelled = false;
0126   // TODO: maybe need label?
0127   if(options() & ExportProgress) {
0128     ProgressItem& item = ProgressManager::self()->newProgressItem(this, QString(), true);
0129     item.setTotalSteps(100);
0130     connect(&item, &Tellico::ProgressItem::signalCancelled, this, &Tellico::Export::HTMLExporter::slotCancel);
0131   }
0132   // ok if not ExportProgress, no worries
0133   ProgressItem::Done done(this);
0134   ProgressManager::self()->setProgress(this, 20);
0135 
0136   bool success = FileHandler::writeTextURL(url(), text(), options() & Export::ExportUTF8, force);
0137   if(m_parseDOM && !m_cancelled) {
0138     success &= copyFiles() && (!m_exportEntryFiles || writeEntryFiles());
0139   }
0140   return success;
0141 }
0142 
0143 bool HTMLExporter::loadXSLTFile() {
0144   QString xsltFile = DataFileRegistry::self()->locate(m_xsltFile);
0145   if(xsltFile.isEmpty()) {
0146     myDebug() << "no xslt file for" << m_xsltFile;
0147     return false;
0148   }
0149 
0150   QUrl u = QUrl::fromLocalFile(xsltFile);
0151   // do NOT do namespace processing, it messes up the XSL declaration since
0152   // QDom thinks there are no elements in the Tellico namespace and as a result
0153   // removes the namespace declaration
0154   QDomDocument dom = FileHandler::readXMLDocument(u, false);
0155   if(dom.isNull()) {
0156     myDebug() << "error loading xslt file:" << xsltFile;
0157     return false;
0158   }
0159 
0160   // notes about utf-8 encoding:
0161   // all params should be passed to XSLTHandler in utf8
0162   // input string to XSLTHandler should be in utf-8, EVEN IF DOM STRING SAYS OTHERWISE
0163 
0164   // the stylesheet prints utf-8 by default, if using locale encoding, need
0165   // to change the encoding attribute on the xsl:output element
0166   if(!(options() & Export::ExportUTF8)) {
0167     XSLTHandler::setLocaleEncoding(dom);
0168   }
0169 
0170   delete m_handler;
0171   m_handler = new XSLTHandler(dom, QFile::encodeName(xsltFile), true /*translate*/);
0172   if(m_checkCommonFile && !m_handler->isValid()) {
0173     Tellico::checkCommonXSLFile();
0174     m_checkCommonFile = false;
0175     delete m_handler;
0176     m_handler = new XSLTHandler(dom, QFile::encodeName(xsltFile), true /*translate*/);
0177   }
0178   if(!m_handler->isValid()) {
0179     delete m_handler;
0180     m_handler = nullptr;
0181     return false;
0182   }
0183   m_handler->addStringParam("date", QDate::currentDate().toString(Qt::ISODate).toLatin1());
0184   m_handler->addStringParam("time", QTime::currentTime().toString(Qt::ISODate).toLatin1());
0185   m_handler->addStringParam("user", KUser(KUser::UseRealUserID).loginName().toLatin1());
0186   m_handler->addStringParam("basedir", u.url(QUrl::RemoveFilename).toLocal8Bit());
0187 
0188   if(m_exportEntryFiles) {
0189     // export entries to same place as all the other date files
0190     m_handler->addStringParam("entrydir", QFile::encodeName(fileDirName()));
0191     // be sure to link all the entries
0192     m_handler->addParam("link-entries", "true()");
0193   }
0194 
0195   if(!m_collectionURL.isEmpty()) {
0196     QString s = QLatin1String("../") + m_collectionURL.fileName();
0197     m_handler->addStringParam("collection-file", s.toUtf8());
0198   }
0199 
0200   // look for a file that gets installed to know the installation directory
0201   // if parseDOM, that means we want the locations to be the actual location
0202   // otherwise, we assume it'll be relative
0203   if(m_parseDOM && m_dataDir.isEmpty()) {
0204     m_dataDir = Tellico::installationDir();
0205   } else if(!m_parseDOM) {
0206     m_dataDir.clear();
0207   }
0208   if(!m_dataDir.isEmpty()) {
0209     m_handler->addStringParam("datadir", QFile::encodeName(m_dataDir));
0210   }
0211 
0212   setFormattingOptions(collection());
0213 
0214   return m_handler->isValid();
0215 }
0216 
0217 QString HTMLExporter::text() {
0218   // allow caching or overriding the main html text
0219   if(!m_customHtml.isEmpty()) return m_customHtml;
0220   if((!m_handler || !m_handler->isValid()) && !loadXSLTFile()) {
0221     myWarning() << "error loading xslt file:" << m_xsltFile;
0222     return QString();
0223   }
0224 
0225   Data::CollPtr coll = collection();
0226   if(!coll) {
0227     myDebug() << "no collection pointer!";
0228     return QString();
0229   }
0230 
0231   if(m_groupBy.isEmpty()) {
0232     m_printGrouped = false; // can't group if no groups exist
0233   }
0234 
0235   GUI::CursorSaver cs;
0236   writeImages(coll);
0237 
0238   // now grab the XML
0239   TellicoXMLExporter exporter(coll);
0240   exporter.setURL(url());
0241   exporter.setEntries(entries());
0242   exporter.setFields(fields());
0243   exporter.setIncludeGroups(m_printGrouped);
0244 // yes, this should be in utf8, always
0245   exporter.setOptions(options() | Export::ExportUTF8 | Export::ExportImages);
0246   QDomDocument output = exporter.exportXML();
0247 #if 0
0248   QFile f(QLatin1String("/tmp/test.xml"));
0249   if(f.open(QIODevice::WriteOnly)) {
0250     QTextStream t(&f);
0251     t << output.toString();
0252   }
0253   f.close();
0254 #endif
0255 
0256   // need to adjust the basedir if we're exporting to a url()
0257   const auto oldBasedir = m_handler->param("basedir");
0258   if(!url().isEmpty()) {
0259     m_handler->addStringParam("basedir", url().url(QUrl::RemoveFilename).toLocal8Bit());
0260   }
0261   const QString outputText = m_handler->applyStylesheet(output.toString());
0262   m_handler->addParam("basedir", oldBasedir); // not ::addStringParam since it has quotes now
0263 #if 0
0264   myDebug() << "Remove debug2 from htmlexporter.cpp";
0265   QFile f2(QLatin1String("/tmp/test.html"));
0266   if(f2.open(QIODevice::WriteOnly)) {
0267     QTextStream t(&f2);
0268     t << outputText;
0269 //    t << "\n\n-------------------------------------------------------\n\n";
0270 //    t << Tellico::i18nReplace(outputText);
0271   }
0272   f2.close();
0273 #endif
0274 
0275   if(!m_parseDOM) {
0276     return outputText;
0277   }
0278 
0279   htmlDocPtr htmlDoc = htmlParseDoc(reinterpret_cast<xmlChar*>(outputText.toUtf8().data()), nullptr);
0280   xmlNodePtr root = xmlDocGetRootElement(htmlDoc);
0281   if(root == nullptr) {
0282     myDebug() << "no root";
0283     return outputText;
0284   }
0285   parseDOM(root);
0286 
0287   xmlChar* c;
0288   int bytes;
0289   htmlDocDumpMemory(htmlDoc, &c, &bytes);
0290   QString allText;
0291   if(bytes > 0) {
0292     allText = QString::fromUtf8(reinterpret_cast<const char*>(c), bytes);
0293     xmlFree(c);
0294   }
0295   return allText;
0296 }
0297 
0298 void HTMLExporter::setFormattingOptions(Tellico::Data::CollPtr coll) {
0299   QString file = Data::Document::self()->URL().fileName();
0300   if(file != i18n(Tellico::untitledFilename)) {
0301     m_handler->addStringParam("filename", QFile::encodeName(file));
0302   }
0303   m_handler->addStringParam("cdate", QLocale().toString(QDate::currentDate()).toUtf8());
0304   m_handler->addParam("show-headers", m_printHeaders ? "true()" : "false()");
0305   m_handler->addParam("group-entries", m_printGrouped ? "true()" : "false()");
0306 
0307   QStringList sortTitles;
0308   if(!m_sort1.isEmpty()) {
0309     sortTitles << m_sort1;
0310   }
0311   if(!m_sort2.isEmpty()) {
0312     sortTitles << m_sort2;
0313   }
0314 
0315   // the third sort column may be same as first
0316   if(!m_sort3.isEmpty() && sortTitles.indexOf(m_sort3) == -1) {
0317     sortTitles << m_sort3;
0318   }
0319 
0320   if(sortTitles.count() > 0) {
0321     m_handler->addStringParam("sort-name1", coll->fieldNameByTitle(sortTitles[0]).toUtf8());
0322     if(sortTitles.count() > 1) {
0323       m_handler->addStringParam("sort-name2", coll->fieldNameByTitle(sortTitles[1]).toUtf8());
0324       if(sortTitles.count() > 2) {
0325         m_handler->addStringParam("sort-name3", coll->fieldNameByTitle(sortTitles[2]).toUtf8());
0326       }
0327     }
0328   }
0329 
0330   // no longer showing "sorted by..." since the column headers are clickable
0331   // but still use "grouped by"
0332   QString sortString;
0333   if(m_printGrouped) {
0334     if(!m_groupBy.isEmpty()) {
0335       QString s;
0336       // if more than one, then it's the People pseudo-group
0337       if(m_groupBy.count() > 1) {
0338         s = i18n("People");
0339       } else {
0340         s = coll->fieldTitleByName(m_groupBy[0]);
0341       }
0342       sortString = i18n("(grouped by %1)", s);
0343     }
0344 
0345     QString groupFields;
0346     for(QStringList::ConstIterator it = m_groupBy.constBegin(); it != m_groupBy.constEnd(); ++it) {
0347       Data::FieldPtr f = coll->fieldByName(*it);
0348       if(!f) {
0349         continue;
0350       }
0351       if(f->hasFlag(Data::Field::AllowMultiple)) {
0352         groupFields += QLatin1String("tc:") + *it + QLatin1String("s/tc:") + *it;
0353       } else {
0354         groupFields += QLatin1String("tc:") + *it;
0355       }
0356       int ncols = 0;
0357       if(f->type() == Data::Field::Table) {
0358         bool ok;
0359         ncols = Tellico::toUInt(f->property(QStringLiteral("columns")), &ok);
0360         if(!ok) {
0361           ncols = 1;
0362         }
0363       }
0364       if(ncols > 1) {
0365         groupFields += QLatin1String("/tc:column[1]");
0366       }
0367       if(*it != m_groupBy.last()) {
0368         groupFields += QLatin1Char('|');
0369       }
0370     }
0371 //    myDebug() << groupFields;
0372     m_handler->addStringParam("group-fields", groupFields.toUtf8());
0373     m_handler->addStringParam("sort-title", sortString.toUtf8());
0374   }
0375 
0376   QString pageTitle = coll->title();
0377   if(!sortString.isEmpty()) {
0378     pageTitle += QLatin1Char(' ') + sortString;
0379   }
0380   m_handler->addStringParam("page-title", pageTitle.toUtf8());
0381 
0382   QStringList showFields;
0383   foreach(const QString& column, m_columns) {
0384     showFields << coll->fieldNameByTitle(column);
0385   }
0386   if(!showFields.isEmpty()) {
0387     m_handler->addStringParam("column-names", showFields.join(QLatin1String(" ")).toUtf8());
0388   }
0389 
0390   if(m_imageWidth > 0 && m_imageHeight > 0) {
0391     m_handler->addParam("image-width", QByteArray().setNum(m_imageWidth));
0392     m_handler->addParam("image-height", QByteArray().setNum(m_imageHeight));
0393   }
0394 
0395   // add system colors to stylesheet
0396   const int type = coll->type();
0397   m_handler->addStringParam("font",     Config::templateFont(type).family().toLatin1());
0398   m_handler->addStringParam("fontsize", QByteArray().setNum(Config::templateFont(type).pointSize()));
0399   m_handler->addStringParam("bgcolor",  Config::templateBaseColor(type).name().toLatin1());
0400   m_handler->addStringParam("fgcolor",  Config::templateTextColor(type).name().toLatin1());
0401   m_handler->addStringParam("color1",   Config::templateHighlightedTextColor(type).name().toLatin1());
0402   m_handler->addStringParam("color2",   Config::templateHighlightedBaseColor(type).name().toLatin1());
0403   m_handler->addStringParam("linkcolor",Config::templateLinkColor(type).name().toLatin1());
0404 
0405   // add locale code to stylesheet (for sorting)
0406   m_handler->addStringParam("lang", QLocale().name().toLatin1());
0407 }
0408 
0409 void HTMLExporter::writeImages(Tellico::Data::CollPtr coll_) {
0410   // keep track of which image fields to write, this is for field titles
0411   StringSet imageFields;
0412   foreach(const QString& column, m_columns) {
0413     if(coll_->fieldByTitle(column) && coll_->fieldByTitle(column)->type() == Data::Field::Image) {
0414       imageFields.add(column);
0415     }
0416   }
0417 
0418   // all the images potentially used in the HTML export need to be written to disk
0419   // if we're exporting entry files, then we'll certainly want all the image fields written
0420   // if we're not exporting to a file, then we might be exporting an entry template file
0421   // and so we need to write all of them too.
0422   if(m_exportEntryFiles || url().isEmpty()) {
0423     // add all image fields to string list
0424     // take intersection with the fields to be exported
0425     Data::FieldList iFields = Tellico::listIntersection(coll_->imageFields(), fields());
0426     foreach(Data::FieldPtr field, iFields) {
0427       imageFields.add(field->name());
0428     }
0429   }
0430 
0431   // all of them are going to get written to tmp file
0432   bool useTemp = url().isEmpty();
0433   QUrl imgDir;
0434   QString imgDirRelative;
0435   // really some convoluted logic here
0436   // basically, four cases. 1) we're writing to a tmp file, for printing probably
0437   // so then write all the images to the tmp directory, 2) we're exporting to HTML, and
0438   // this is the main collection file, in which case m_parseDOM is always true;
0439   // 3) we're exporting HTML, and this is the first entry file, for which parseDOM is true
0440   // and exportEntryFiles is false. Then the image file will get copied in copyFiles() and is
0441   // probably an image in the entry template. 4) we're exporting HTML, and this is not the
0442   // first entry file, in which case, we want to refer directly to the target dir
0443   if(useTemp) { // everything goes in the tmp dir
0444     imgDir = QUrl::fromLocalFile(ImageFactory::tempDir());
0445     imgDirRelative = imgDir.path();
0446   } else if(m_parseDOM) {
0447     imgDir = fileDir(); // copy to fileDir
0448     imgDirRelative = ImageFactory::imageDir();
0449     createDir();
0450   } else {
0451     imgDir = fileDir();
0452     imgDirRelative = QFileInfo(url().path()).dir().relativeFilePath(imgDir.path());
0453     createDir();
0454   }
0455   if(!imgDirRelative.endsWith(QLatin1Char('/'))) {
0456     imgDirRelative += QLatin1Char('/');
0457   }
0458   m_handler->addStringParam("imgdir", QFile::encodeName(imgDirRelative));
0459 
0460   int count = 0;
0461   const int processCount = 100; // process after every 100 events
0462 
0463   StringSet imageSet; // track which images are written
0464   foreach(const QString& imageField, imageFields) {
0465     foreach(Data::EntryPtr entryIt, entries()) {
0466       QString id = entryIt->field(imageField);
0467       // if no id or is already written, continue
0468       if(id.isEmpty() || imageSet.has(id)) {
0469         continue;
0470       }
0471       imageSet.add(id);
0472       // try writing
0473       bool success = false;
0474       if(useTemp) {
0475         // for link-only images, no need to write it out
0476         success = ImageFactory::imageInfo(id).linkOnly || ImageFactory::writeCachedImage(id, ImageFactory::TempDir);
0477       } else {
0478         const Data::Image& img = ImageFactory::imageById(id);
0479         QUrl target = imgDir;
0480         target = target.adjusted(QUrl::StripTrailingSlash);
0481         target.setPath(target.path() + QLatin1Char('/') + (id));
0482         success = !img.isNull() && FileHandler::writeDataURL(target, img.byteArray(), true);
0483       }
0484       if(!success) {
0485         myWarning() << "unable to write image file: "
0486                     << imgDir.path() << id;
0487       }
0488 
0489       if(++count == processCount) {
0490         qApp->processEvents();
0491         count = 0;
0492       }
0493     }
0494   }
0495 }
0496 
0497 QWidget* HTMLExporter::widget(QWidget* parent_) {
0498   if(m_widget) {
0499     return m_widget;
0500   }
0501 
0502   m_widget = new QWidget(parent_);
0503   QVBoxLayout* l = new QVBoxLayout(m_widget);
0504 
0505   QGroupBox* gbox = new QGroupBox(i18n("HTML Options"), m_widget);
0506   QVBoxLayout* vlay = new QVBoxLayout(gbox);
0507 
0508   m_checkPrintHeaders = new QCheckBox(i18n("Print field headers"), gbox);
0509   m_checkPrintHeaders->setWhatsThis(i18n("If checked, the field names will be "
0510                                          "printed as table headers."));
0511   m_checkPrintHeaders->setChecked(m_printHeaders);
0512 
0513   m_checkPrintGrouped = new QCheckBox(i18n("Group the entries"), gbox);
0514   m_checkPrintGrouped->setWhatsThis(i18n("If checked, the entries will be grouped by "
0515                                          "the selected field."));
0516   m_checkPrintGrouped->setChecked(m_printGrouped);
0517 
0518   m_checkExportEntryFiles = new QCheckBox(i18n("Export individual entry files"), gbox);
0519   m_checkExportEntryFiles->setWhatsThis(i18n("If checked, individual files will be created for each entry."));
0520   m_checkExportEntryFiles->setChecked(m_exportEntryFiles);
0521 
0522   vlay->addWidget(m_checkPrintHeaders);
0523   vlay->addWidget(m_checkPrintGrouped);
0524   vlay->addWidget(m_checkExportEntryFiles);
0525 
0526   l->addWidget(gbox);
0527   l->addStretch(1);
0528   return m_widget;
0529 }
0530 
0531 void HTMLExporter::readOptions(KSharedConfigPtr config_) {
0532   KConfigGroup exportConfig(config_, QStringLiteral("ExportOptions - %1").arg(formatString()));
0533   m_printHeaders = exportConfig.readEntry("Print Field Headers", m_printHeaders);
0534   m_printGrouped = exportConfig.readEntry("Print Grouped", m_printGrouped);
0535   m_exportEntryFiles = exportConfig.readEntry("Export Entry Files", m_exportEntryFiles);
0536 
0537   // read current entry export template
0538   m_entryXSLTFile = Config::templateName(collection()->type());
0539   m_entryXSLTFile = DataFileRegistry::self()->locate(QLatin1String("entry-templates/")
0540                                                      + m_entryXSLTFile + QLatin1String(".xsl"));
0541 }
0542 
0543 void HTMLExporter::saveOptions(KSharedConfigPtr config_) {
0544   KConfigGroup cfg(config_, QStringLiteral("ExportOptions - %1").arg(formatString()));
0545   m_printHeaders = m_checkPrintHeaders->isChecked();
0546   cfg.writeEntry("Print Field Headers", m_printHeaders);
0547   m_printGrouped = m_checkPrintGrouped->isChecked();
0548   cfg.writeEntry("Print Grouped", m_printGrouped);
0549   m_exportEntryFiles = m_checkExportEntryFiles->isChecked();
0550   cfg.writeEntry("Export Entry Files", m_exportEntryFiles);
0551 }
0552 
0553 void HTMLExporter::setXSLTFile(const QString& filename_) {
0554   m_customHtml.clear();
0555   if(m_xsltFile == filename_) {
0556     return;
0557   }
0558 
0559   m_xsltFile = filename_;
0560   m_xsltFilePath.clear();
0561   reset();
0562 }
0563 
0564 void HTMLExporter::setEntryXSLTFile(const QString& fileName_) {
0565   QString fileName = fileName_;
0566   if(!fileName.endsWith(QLatin1String(".xsl"))) {
0567     fileName += QLatin1String(".xsl");
0568   }
0569   QString f = DataFileRegistry::self()->locate(QLatin1String("entry-templates/") + fileName);
0570   if(f.isEmpty()) {
0571     myDebug() << fileName << "entry XSL file is not found";
0572   }
0573   m_entryXSLTFile = f;
0574 }
0575 
0576 QUrl HTMLExporter::fileDir() const {
0577   if(url().isEmpty()) {
0578     return QUrl();
0579   }
0580   QUrl fileDir = url();
0581   // cd to directory of target URL
0582   fileDir = fileDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0583   if(fileDirName().startsWith(QLatin1Char('/'))) {
0584     fileDir.setPath(fileDir.path() + fileDirName());
0585   } else {
0586     fileDir.setPath(fileDir.path() + QLatin1Char('/') + fileDirName());
0587   }
0588   return fileDir;
0589 }
0590 
0591 QString HTMLExporter::fileDirName() const {
0592   if(!m_collectionURL.isEmpty()) {
0593     return QStringLiteral("/");
0594   }
0595   QFileInfo fi(url().fileName());
0596   return fi.completeBaseName() + QLatin1String("_files/");
0597 }
0598 
0599 // how ugly is this?
0600 const xmlChar* HTMLExporter::handleLink(const xmlChar* link_) {
0601   return reinterpret_cast<xmlChar*>(qstrdup(handleLink(QString::fromUtf8(reinterpret_cast<const char*>(link_))).toUtf8().constData()));
0602 }
0603 
0604 QString HTMLExporter::handleLink(const QString& link_) {
0605   if(link_.isEmpty()) {
0606     return link_;
0607   }
0608   if(m_links.contains(link_)) {
0609     return m_links[link_];
0610   }
0611   const QUrl linkUrl(link_);
0612   // assume that if the link_ is not relative, then we don't need to copy it
0613   // also an invalid url is not relative either
0614   if(!linkUrl.isRelative()) {
0615     return link_;
0616   }
0617 
0618   if(m_xsltFilePath.isEmpty()) {
0619     m_xsltFilePath = DataFileRegistry::self()->locate(m_xsltFile);
0620     if(m_xsltFilePath.isEmpty()) {
0621       myWarning() << "no xslt file for " << m_xsltFile;
0622     }
0623   }
0624 
0625   QUrl u = QUrl::fromLocalFile(m_xsltFilePath);
0626   u = u.resolved(linkUrl);
0627 
0628   // one of the "quirks" of the html export is that img src urls are set to point to
0629   // the tmpDir() when exporting entry files from a collection, but those images
0630   // don't actually exist, and they get copied in writeImages() instead.
0631   // so we only need to keep track of the url if it exists
0632   const bool exists = NetAccess::exists(u, false, m_widget);
0633   if(exists) {
0634     m_files.append(u);
0635   }
0636 
0637   // if we're exporting entry files, we want pics/ to
0638   // go in pics/
0639   const bool isPic = link_.startsWith(m_dataDir + QLatin1String("pics/"));
0640   QString midDir;
0641   if(m_exportEntryFiles && isPic) {
0642     midDir = QStringLiteral("pics/");
0643   }
0644   // pictures are special since they might not exist when the HTML is exported, since they might get copied later
0645   // on the other hand, don't change the file location if it doesn't exist
0646   // and only use relative location if an export URL() is set
0647   if((isPic || exists) && !url().isEmpty()) {
0648     m_links.insert(link_, fileDirName() + midDir + u.fileName());
0649   } else {
0650     m_links.insert(link_, link_);
0651   }
0652 //  myDebug() << link_ << linkUrl << u << m_links[link_];
0653   return m_links[link_];
0654 }
0655 
0656 const xmlChar* HTMLExporter::analyzeInternalCSS(const xmlChar* str_) {
0657   return reinterpret_cast<xmlChar*>(qstrdup(analyzeInternalCSS(QString::fromUtf8(reinterpret_cast<const char*>(str_))).toUtf8().constData()));
0658 }
0659 
0660 QString HTMLExporter::analyzeInternalCSS(const QString& str_) {
0661   QString str = str_;
0662   int start = 0;
0663   int end = 0;
0664   const QString url = QStringLiteral("url(");
0665   for(int pos = str.indexOf(url); pos >= 0; pos = str.indexOf(url, pos+1)) {
0666     pos += 4; // url(
0667     if(str[pos] ==  QLatin1Char('"') || str[pos] == QLatin1Char('\'')) {
0668       ++pos;
0669     }
0670 
0671     start = pos;
0672     pos = str.indexOf(QLatin1Char(')'), start);
0673     end = pos;
0674     if(str[pos-1] == QLatin1Char('"') || str[pos-1] == QLatin1Char('\'')) {
0675       --end;
0676     }
0677 
0678     str.replace(start, end-start, handleLink(str.mid(start, end-start)));
0679   }
0680   return str;
0681 }
0682 
0683 void HTMLExporter::createDir() {
0684   if(!m_checkCreateDir) {
0685     return;
0686   }
0687   QUrl dir = fileDir();
0688   if(dir.isEmpty()) {
0689     myDebug() << "called on empty URL!";
0690     return;
0691   }
0692   if(NetAccess::exists(dir, false, m_widget)) {
0693     m_checkCreateDir = false;
0694   } else {
0695     KIO::Job* job = KIO::mkdir(dir);
0696     KJobWidgets::setWindow(job, m_widget);
0697     m_checkCreateDir = !job->exec();
0698   }
0699 }
0700 
0701 bool HTMLExporter::copyFiles() {
0702   if(m_files.isEmpty()) {
0703     return true;
0704   }
0705   const int start = 20;
0706   const int maxProgress = m_exportEntryFiles ? 40 : 80;
0707   const int stepSize = qMax(1, m_files.count()/maxProgress);
0708   int j = 0;
0709 
0710   createDir();
0711   QUrl target;
0712   for(QList<QUrl>::ConstIterator it = m_files.constBegin(); it != m_files.constEnd() && !m_cancelled; ++it, ++j) {
0713     if(m_copiedFiles.has((*it).url())) {
0714       continue;
0715     }
0716 
0717     if(target.isEmpty()) {
0718       target = fileDir();
0719     }
0720     target = target.adjusted(QUrl::RemoveFilename);
0721     target.setPath(target.path() + (*it).fileName());
0722     KIO::JobFlags flags = KIO::Overwrite;
0723     if(!m_widget) flags |= KIO::HideProgressInfo;
0724     KIO::FileCopyJob* job = KIO::file_copy(*it, target, -1, flags);
0725     KJobWidgets::setWindow(job, m_widget);
0726     if(job->exec()) {
0727       m_copiedFiles.add((*it).url());
0728     } else {
0729       myWarning() << "can't copy " << target;
0730       myWarning() << job->errorString();
0731     }
0732     if(j%stepSize == 0) {
0733       if(options() & ExportProgress) {
0734         ProgressManager::self()->setProgress(this, qMin(start+j/stepSize, 99));
0735       }
0736       qApp->processEvents();
0737     }
0738   }
0739   return true;
0740 }
0741 
0742 bool HTMLExporter::writeEntryFiles() {
0743   if(m_entryXSLTFile.isEmpty()) {
0744     myWarning() << "no entry XSLT file";
0745     return false;
0746   }
0747 
0748   const int start = 60;
0749   const int stepSize = qMax(1, entries().count()/40);
0750   int j = 0;
0751 
0752   // now worry about actually exporting entry files
0753   // I can't reliable encode a string as a URI, so I'm punting, and I'll just replace everything but
0754   // a-zA-Z0-9 with an underscore. This MUST match the filename template in tellico2html.xsl
0755   // the id is used so uniqueness is guaranteed
0756   static const QRegularExpression badChars(QLatin1String("[^-a-zA-Z0-9]"));
0757   FieldFormat::Request formatted = (options() & Export::ExportFormatted ?
0758                                                    FieldFormat::ForceFormat :
0759                                                    FieldFormat::AsIsFormat);
0760 
0761   QUrl outputFile = fileDir();
0762 
0763   GUI::CursorSaver cs(Qt::WaitCursor);
0764 
0765   HTMLExporter exporter(collection());
0766   long opt = options() | Export::ExportForce;
0767   opt &= ~ExportProgress;
0768   exporter.setFields(fields());
0769   exporter.setOptions(opt);
0770   exporter.setXSLTFile(m_entryXSLTFile);
0771   exporter.setCollectionURL(url());
0772   bool parseDOM = true;
0773 
0774   const QString title = QStringLiteral("title");
0775   const QString html = QStringLiteral(".html");
0776   bool multipleTitles = collection()->fieldByName(title)->hasFlag(Data::Field::AllowMultiple);
0777   Data::EntryList entries = this->entries(); // not const since the pointer has to be copied
0778   foreach(Data::EntryPtr entryIt, entries) {
0779     QString file = entryIt->title(formatted);
0780 
0781     // but only use the first title if it has multiple
0782     if(multipleTitles) {
0783       file = file.section(QLatin1Char(';'), 0, 0);
0784     }
0785     file.replace(badChars, QStringLiteral("_"));
0786     file += QLatin1Char('-') + QString::number(entryIt->id()) + html;
0787     outputFile = outputFile.adjusted(QUrl::RemoveFilename);
0788     outputFile.setPath(outputFile.path() + file);
0789 
0790     exporter.setEntries(Data::EntryList() << entryIt);
0791     exporter.setURL(outputFile);
0792     exporter.exec();
0793 
0794     // no longer need to parse DOM
0795     if(parseDOM) {
0796       parseDOM = false;
0797       exporter.setParseDOM(false);
0798       // this is rather stupid, but I'm too lazy to figure out the better way
0799       // since we parsed the DOM for the first entry file to grab any
0800       // images used in the template, need to resave it so the image links
0801       // get written correctly
0802       exporter.exec();
0803     }
0804 
0805     if(j%stepSize == 0) {
0806       if(options() & ExportProgress) {
0807         ProgressManager::self()->setProgress(this, qMin(start+j/stepSize, 99));
0808       }
0809       qApp->processEvents();
0810     }
0811     ++j;
0812   }
0813   // the images in "pics/" are special data images, copy them always
0814   // since the entry files may refer to them, but we don't know that
0815   QStringList dataImages;
0816   dataImages.reserve(1 + 10);
0817   dataImages << QStringLiteral("checkmark.png");
0818   for(uint i = 1; i <= 10; ++i) {
0819     dataImages << QStringLiteral("stars%1.png").arg(i);
0820   }
0821   QUrl dataDir = QUrl::fromLocalFile(Tellico::installationDir() + QLatin1String("pics/"));
0822   QUrl target = fileDir();
0823   target = target.adjusted(QUrl::StripTrailingSlash);
0824   target.setPath(target.path() + QLatin1Char('/') + (QLatin1String("pics/")));
0825   KIO::Job* job = KIO::mkdir(target);
0826   KJobWidgets::setWindow(job, m_widget);
0827   job->exec();
0828   KIO::JobFlags flags = KIO::DefaultFlags;
0829   if(!m_widget) flags |= KIO::HideProgressInfo;
0830   foreach(const QString& dataImage, dataImages) {
0831     dataDir = dataDir.adjusted(QUrl::RemoveFilename);
0832     dataDir.setPath(dataDir.path() + dataImage);
0833     target = target.adjusted(QUrl::RemoveFilename);
0834     target.setPath(target.path() + dataImage);
0835     KIO::Job* job = KIO::file_copy(dataDir, target, -1, flags);
0836     KJobWidgets::setWindow(job, m_widget);
0837     job->exec();
0838   }
0839 
0840   return true;
0841 }
0842 
0843 void HTMLExporter::slotCancel() {
0844   m_cancelled = true;
0845 }
0846 
0847 void HTMLExporter::parseDOM(xmlNode* node_) {
0848   if(node_ == nullptr) {
0849     myDebug() << "no node";
0850     return;
0851   }
0852 
0853   bool parseChildren = true;
0854 
0855   if(node_->type == XML_ELEMENT_NODE) {
0856     const QByteArray nodeName = QByteArray(reinterpret_cast<const char*>(node_->name)).toUpper();
0857     xmlElement* elem = reinterpret_cast<xmlElement*>(node_);
0858     // to speed up things, check now for nodename
0859     if(nodeName == "IMG" || nodeName == "SCRIPT" || nodeName == "LINK") {
0860       for(xmlAttribute* attr = elem->attributes; attr; attr = reinterpret_cast<xmlAttribute*>(attr->next)) {
0861         QByteArray attrName = QByteArray(reinterpret_cast<const char*>(attr->name)).toUpper();
0862 
0863         if( (attrName == "SRC" && (nodeName == "IMG" || nodeName == "SCRIPT")) ||
0864             (attrName == "HREF" && nodeName == "LINK")) {
0865 /*          (attrName == "BACKGROUND" && (nodeName == "BODY" ||
0866                                                        nodeName == "TABLE" ||
0867                                                        nodeName == "TH" ||
0868                                                        nodeName == "TD"))) */
0869           xmlChar* value = xmlGetProp(node_, attr->name);
0870           if(value) {
0871             xmlSetProp(node_, attr->name, handleLink(value));
0872             xmlFree(value);
0873           }
0874           // each node only has one significant attribute, so break now
0875           break;
0876         }
0877       }
0878     } else if(nodeName == "STYLE") {
0879       // if the first child is a CDATA, use it, otherwise replace complete node
0880       xmlNode* nodeToReplace = node_;
0881       xmlNode* child = node_->children;
0882       if(child && child->type == XML_CDATA_SECTION_NODE) {
0883         nodeToReplace = child;
0884       }
0885       xmlChar* value = xmlNodeGetContent(nodeToReplace);
0886       if(value) {
0887         xmlNodeSetContent(nodeToReplace, analyzeInternalCSS(value));
0888         xmlFree(value);
0889       }
0890       // no longer need to parse child text nodes
0891       parseChildren = false;
0892     }
0893   }
0894 
0895   if(parseChildren) {
0896     xmlNode* child = node_->children;
0897     while(child) {
0898       parseDOM(child);
0899       child = child->next;
0900     }
0901   }
0902 }