File indexing completed on 2024-05-12 16:46:32

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 i18n("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   if((!m_handler || !m_handler->isValid()) && !loadXSLTFile()) {
0219     myWarning() << "error loading xslt file:" << m_xsltFile;
0220     return QString();
0221   }
0222 
0223   Data::CollPtr coll = collection();
0224   if(!coll) {
0225     myDebug() << "no collection pointer!";
0226     return QString();
0227   }
0228 
0229   if(m_groupBy.isEmpty()) {
0230     m_printGrouped = false; // can't group if no groups exist
0231   }
0232 
0233   GUI::CursorSaver cs;
0234   writeImages(coll);
0235 
0236   // now grab the XML
0237   TellicoXMLExporter exporter(coll);
0238   exporter.setURL(url());
0239   exporter.setEntries(entries());
0240   exporter.setFields(fields());
0241   exporter.setIncludeGroups(m_printGrouped);
0242 // yes, this should be in utf8, always
0243   exporter.setOptions(options() | Export::ExportUTF8 | Export::ExportImages);
0244   QDomDocument output = exporter.exportXML();
0245 #if 0
0246   QFile f(QLatin1String("/tmp/test.xml"));
0247   if(f.open(QIODevice::WriteOnly)) {
0248     QTextStream t(&f);
0249     t << output.toString();
0250   }
0251   f.close();
0252 #endif
0253 
0254   // need to adjust the basedir if we're exporting to a url()
0255   const auto oldBasedir = m_handler->param("basedir");
0256   if(!url().isEmpty()) {
0257     m_handler->addStringParam("basedir", url().url(QUrl::RemoveFilename).toLocal8Bit());
0258   }
0259   const QString outputText = m_handler->applyStylesheet(output.toString());
0260   m_handler->addParam("basedir", oldBasedir); // not ::addStringParam since it has quotes now
0261 #if 0
0262   myDebug() << "Remove debug2 from htmlexporter.cpp";
0263   QFile f2(QLatin1String("/tmp/test.html"));
0264   if(f2.open(QIODevice::WriteOnly)) {
0265     QTextStream t(&f2);
0266     t << outputText;
0267 //    t << "\n\n-------------------------------------------------------\n\n";
0268 //    t << Tellico::i18nReplace(outputText);
0269   }
0270   f2.close();
0271 #endif
0272 
0273   if(!m_parseDOM) {
0274     return outputText;
0275   }
0276 
0277   htmlDocPtr htmlDoc = htmlParseDoc(reinterpret_cast<xmlChar*>(outputText.toUtf8().data()), nullptr);
0278   xmlNodePtr root = xmlDocGetRootElement(htmlDoc);
0279   if(root == nullptr) {
0280     myDebug() << "no root";
0281     return outputText;
0282   }
0283   parseDOM(root);
0284 
0285   xmlChar* c;
0286   int bytes;
0287   htmlDocDumpMemory(htmlDoc, &c, &bytes);
0288   QString allText;
0289   if(bytes > 0) {
0290     allText = QString::fromUtf8(reinterpret_cast<const char*>(c), bytes);
0291     xmlFree(c);
0292   }
0293   return allText;
0294 }
0295 
0296 void HTMLExporter::setFormattingOptions(Tellico::Data::CollPtr coll) {
0297   QString file = Data::Document::self()->URL().fileName();
0298   if(file != i18n(Tellico::untitledFilename)) {
0299     m_handler->addStringParam("filename", QFile::encodeName(file));
0300   }
0301   m_handler->addStringParam("cdate", QLocale().toString(QDate::currentDate()).toUtf8());
0302   m_handler->addParam("show-headers", m_printHeaders ? "true()" : "false()");
0303   m_handler->addParam("group-entries", m_printGrouped ? "true()" : "false()");
0304 
0305   QStringList sortTitles;
0306   if(!m_sort1.isEmpty()) {
0307     sortTitles << m_sort1;
0308   }
0309   if(!m_sort2.isEmpty()) {
0310     sortTitles << m_sort2;
0311   }
0312 
0313   // the third sort column may be same as first
0314   if(!m_sort3.isEmpty() && sortTitles.indexOf(m_sort3) == -1) {
0315     sortTitles << m_sort3;
0316   }
0317 
0318   if(sortTitles.count() > 0) {
0319     m_handler->addStringParam("sort-name1", coll->fieldNameByTitle(sortTitles[0]).toUtf8());
0320     if(sortTitles.count() > 1) {
0321       m_handler->addStringParam("sort-name2", coll->fieldNameByTitle(sortTitles[1]).toUtf8());
0322       if(sortTitles.count() > 2) {
0323         m_handler->addStringParam("sort-name3", coll->fieldNameByTitle(sortTitles[2]).toUtf8());
0324       }
0325     }
0326   }
0327 
0328   // no longer showing "sorted by..." since the column headers are clickable
0329   // but still use "grouped by"
0330   QString sortString;
0331   if(m_printGrouped) {
0332     if(!m_groupBy.isEmpty()) {
0333       QString s;
0334       // if more than one, then it's the People pseudo-group
0335       if(m_groupBy.count() > 1) {
0336         s = i18n("People");
0337       } else {
0338         s = coll->fieldTitleByName(m_groupBy[0]);
0339       }
0340       sortString = i18n("(grouped by %1)", s);
0341     }
0342 
0343     QString groupFields;
0344     for(QStringList::ConstIterator it = m_groupBy.constBegin(); it != m_groupBy.constEnd(); ++it) {
0345       Data::FieldPtr f = coll->fieldByName(*it);
0346       if(!f) {
0347         continue;
0348       }
0349       if(f->hasFlag(Data::Field::AllowMultiple)) {
0350         groupFields += QLatin1String("tc:") + *it + QLatin1String("s/tc:") + *it;
0351       } else {
0352         groupFields += QLatin1String("tc:") + *it;
0353       }
0354       int ncols = 0;
0355       if(f->type() == Data::Field::Table) {
0356         bool ok;
0357         ncols = Tellico::toUInt(f->property(QStringLiteral("columns")), &ok);
0358         if(!ok) {
0359           ncols = 1;
0360         }
0361       }
0362       if(ncols > 1) {
0363         groupFields += QLatin1String("/tc:column[1]");
0364       }
0365       if(*it != m_groupBy.last()) {
0366         groupFields += QLatin1Char('|');
0367       }
0368     }
0369 //    myDebug() << groupFields;
0370     m_handler->addStringParam("group-fields", groupFields.toUtf8());
0371     m_handler->addStringParam("sort-title", sortString.toUtf8());
0372   }
0373 
0374   QString pageTitle = coll->title();
0375   if(!sortString.isEmpty()) {
0376     pageTitle += QLatin1Char(' ') + sortString;
0377   }
0378   m_handler->addStringParam("page-title", pageTitle.toUtf8());
0379 
0380   QStringList showFields;
0381   foreach(const QString& column, m_columns) {
0382     showFields << coll->fieldNameByTitle(column);
0383   }
0384   if(!showFields.isEmpty()) {
0385     m_handler->addStringParam("column-names", showFields.join(QLatin1String(" ")).toUtf8());
0386   }
0387 
0388   if(m_imageWidth > 0 && m_imageHeight > 0) {
0389     m_handler->addParam("image-width", QByteArray().setNum(m_imageWidth));
0390     m_handler->addParam("image-height", QByteArray().setNum(m_imageHeight));
0391   }
0392 
0393   // add system colors to stylesheet
0394   const int type = coll->type();
0395   m_handler->addStringParam("font",     Config::templateFont(type).family().toLatin1());
0396   m_handler->addStringParam("fontsize", QByteArray().setNum(Config::templateFont(type).pointSize()));
0397   m_handler->addStringParam("bgcolor",  Config::templateBaseColor(type).name().toLatin1());
0398   m_handler->addStringParam("fgcolor",  Config::templateTextColor(type).name().toLatin1());
0399   m_handler->addStringParam("color1",   Config::templateHighlightedTextColor(type).name().toLatin1());
0400   m_handler->addStringParam("color2",   Config::templateHighlightedBaseColor(type).name().toLatin1());
0401 
0402   // add locale code to stylesheet (for sorting)
0403   m_handler->addStringParam("lang", QLocale().name().toLatin1());
0404 }
0405 
0406 void HTMLExporter::writeImages(Tellico::Data::CollPtr coll_) {
0407   // keep track of which image fields to write, this is for field titles
0408   StringSet imageFields;
0409   foreach(const QString& column, m_columns) {
0410     if(coll_->fieldByTitle(column) && coll_->fieldByTitle(column)->type() == Data::Field::Image) {
0411       imageFields.add(column);
0412     }
0413   }
0414 
0415   // all the images potentially used in the HTML export need to be written to disk
0416   // if we're exporting entry files, then we'll certainly want all the image fields written
0417   // if we're not exporting to a file, then we might be exporting an entry template file
0418   // and so we need to write all of them too.
0419   if(m_exportEntryFiles || url().isEmpty()) {
0420     // add all image fields to string list
0421     // take intersection with the fields to be exported
0422     Data::FieldList iFields = Tellico::listIntersection(coll_->imageFields(), fields());
0423     foreach(Data::FieldPtr field, iFields) {
0424       imageFields.add(field->name());
0425     }
0426   }
0427 
0428   // all of them are going to get written to tmp file
0429   bool useTemp = url().isEmpty();
0430   QUrl imgDir;
0431   QString imgDirRelative;
0432   // really some convoluted logic here
0433   // basically, four cases. 1) we're writing to a tmp file, for printing probably
0434   // so then write all the images to the tmp directory, 2) we're exporting to HTML, and
0435   // this is the main collection file, in which case m_parseDOM is always true;
0436   // 3) we're exporting HTML, and this is the first entry file, for which parseDOM is true
0437   // and exportEntryFiles is false. Then the image file will get copied in copyFiles() and is
0438   // probably an image in the entry template. 4) we're exporting HTML, and this is not the
0439   // first entry file, in which case, we want to refer directly to the target dir
0440   if(useTemp) { // everything goes in the tmp dir
0441     imgDir = QUrl::fromLocalFile(ImageFactory::tempDir());
0442     imgDirRelative = imgDir.path();
0443   } else if(m_parseDOM) {
0444     imgDir = fileDir(); // copy to fileDir
0445     imgDirRelative = ImageFactory::imageDir();
0446     createDir();
0447   } else {
0448     imgDir = fileDir();
0449     imgDirRelative = QFileInfo(url().path()).dir().relativeFilePath(imgDir.path());
0450     createDir();
0451   }
0452   if(!imgDirRelative.endsWith(QLatin1Char('/'))) {
0453     imgDirRelative += QLatin1Char('/');
0454   }
0455   m_handler->addStringParam("imgdir", QFile::encodeName(imgDirRelative));
0456 
0457   int count = 0;
0458   const int processCount = 100; // process after every 100 events
0459 
0460   StringSet imageSet; // track which images are written
0461   foreach(const QString& imageField, imageFields) {
0462     foreach(Data::EntryPtr entryIt, entries()) {
0463       QString id = entryIt->field(imageField);
0464       // if no id or is already written, continue
0465       if(id.isEmpty() || imageSet.has(id)) {
0466         continue;
0467       }
0468       imageSet.add(id);
0469       // try writing
0470       bool success = false;
0471       if(useTemp) {
0472         // for link-only images, no need to write it out
0473         success = ImageFactory::imageInfo(id).linkOnly || ImageFactory::writeCachedImage(id, ImageFactory::TempDir);
0474       } else {
0475         const Data::Image& img = ImageFactory::imageById(id);
0476         QUrl target = imgDir;
0477         target = target.adjusted(QUrl::StripTrailingSlash);
0478         target.setPath(target.path() + QLatin1Char('/') + (id));
0479         success = !img.isNull() && FileHandler::writeDataURL(target, img.byteArray(), true);
0480       }
0481       if(!success) {
0482         myWarning() << "unable to write image file: "
0483                     << imgDir.path() << id;
0484       }
0485 
0486       if(++count == processCount) {
0487         qApp->processEvents();
0488         count = 0;
0489       }
0490     }
0491   }
0492 }
0493 
0494 QWidget* HTMLExporter::widget(QWidget* parent_) {
0495   if(m_widget) {
0496     return m_widget;
0497   }
0498 
0499   m_widget = new QWidget(parent_);
0500   QVBoxLayout* l = new QVBoxLayout(m_widget);
0501 
0502   QGroupBox* gbox = new QGroupBox(i18n("HTML Options"), m_widget);
0503   QVBoxLayout* vlay = new QVBoxLayout(gbox);
0504 
0505   m_checkPrintHeaders = new QCheckBox(i18n("Print field headers"), gbox);
0506   m_checkPrintHeaders->setWhatsThis(i18n("If checked, the field names will be "
0507                                          "printed as table headers."));
0508   m_checkPrintHeaders->setChecked(m_printHeaders);
0509 
0510   m_checkPrintGrouped = new QCheckBox(i18n("Group the entries"), gbox);
0511   m_checkPrintGrouped->setWhatsThis(i18n("If checked, the entries will be grouped by "
0512                                          "the selected field."));
0513   m_checkPrintGrouped->setChecked(m_printGrouped);
0514 
0515   m_checkExportEntryFiles = new QCheckBox(i18n("Export individual entry files"), gbox);
0516   m_checkExportEntryFiles->setWhatsThis(i18n("If checked, individual files will be created for each entry."));
0517   m_checkExportEntryFiles->setChecked(m_exportEntryFiles);
0518 
0519   vlay->addWidget(m_checkPrintHeaders);
0520   vlay->addWidget(m_checkPrintGrouped);
0521   vlay->addWidget(m_checkExportEntryFiles);
0522 
0523   l->addWidget(gbox);
0524   l->addStretch(1);
0525   return m_widget;
0526 }
0527 
0528 void HTMLExporter::readOptions(KSharedConfigPtr config_) {
0529   KConfigGroup exportConfig(config_, QStringLiteral("ExportOptions - %1").arg(formatString()));
0530   m_printHeaders = exportConfig.readEntry("Print Field Headers", m_printHeaders);
0531   m_printGrouped = exportConfig.readEntry("Print Grouped", m_printGrouped);
0532   m_exportEntryFiles = exportConfig.readEntry("Export Entry Files", m_exportEntryFiles);
0533 
0534   // read current entry export template
0535   m_entryXSLTFile = Config::templateName(collection()->type());
0536   m_entryXSLTFile = DataFileRegistry::self()->locate(QLatin1String("entry-templates/")
0537                                                      + m_entryXSLTFile + QLatin1String(".xsl"));
0538 }
0539 
0540 void HTMLExporter::saveOptions(KSharedConfigPtr config_) {
0541   KConfigGroup cfg(config_, QStringLiteral("ExportOptions - %1").arg(formatString()));
0542   m_printHeaders = m_checkPrintHeaders->isChecked();
0543   cfg.writeEntry("Print Field Headers", m_printHeaders);
0544   m_printGrouped = m_checkPrintGrouped->isChecked();
0545   cfg.writeEntry("Print Grouped", m_printGrouped);
0546   m_exportEntryFiles = m_checkExportEntryFiles->isChecked();
0547   cfg.writeEntry("Export Entry Files", m_exportEntryFiles);
0548 }
0549 
0550 void HTMLExporter::setXSLTFile(const QString& filename_) {
0551   if(m_xsltFile == filename_) {
0552     return;
0553   }
0554 
0555   m_xsltFile = filename_;
0556   m_xsltFilePath.clear();
0557   reset();
0558 }
0559 
0560 void HTMLExporter::setEntryXSLTFile(const QString& fileName_) {
0561   QString fileName = fileName_;
0562   if(!fileName.endsWith(QLatin1String(".xsl"))) {
0563     fileName += QLatin1String(".xsl");
0564   }
0565   QString f = DataFileRegistry::self()->locate(QLatin1String("entry-templates/") + fileName);
0566   if(f.isEmpty()) {
0567     myDebug() << fileName << "entry XSL file is not found";
0568   }
0569   m_entryXSLTFile = f;
0570 }
0571 
0572 QUrl HTMLExporter::fileDir() const {
0573   if(url().isEmpty()) {
0574     return QUrl();
0575   }
0576   QUrl fileDir = url();
0577   // cd to directory of target URL
0578   fileDir = fileDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0579   if(fileDirName().startsWith(QLatin1Char('/'))) {
0580     fileDir.setPath(fileDir.path() + fileDirName());
0581   } else {
0582     fileDir.setPath(fileDir.path() + QLatin1Char('/') + fileDirName());
0583   }
0584   return fileDir;
0585 }
0586 
0587 QString HTMLExporter::fileDirName() const {
0588   if(!m_collectionURL.isEmpty()) {
0589     return QStringLiteral("/");
0590   }
0591   QFileInfo fi(url().fileName());
0592   return fi.completeBaseName() + QLatin1String("_files/");
0593 }
0594 
0595 // how ugly is this?
0596 const xmlChar* HTMLExporter::handleLink(const xmlChar* link_) {
0597   return reinterpret_cast<xmlChar*>(qstrdup(handleLink(QString::fromUtf8(reinterpret_cast<const char*>(link_))).toUtf8().constData()));
0598 }
0599 
0600 QString HTMLExporter::handleLink(const QString& link_) {
0601   if(link_.isEmpty()) {
0602     return link_;
0603   }
0604   if(m_links.contains(link_)) {
0605     return m_links[link_];
0606   }
0607   const QUrl linkUrl(link_);
0608   // assume that if the link_ is not relative, then we don't need to copy it
0609   // also an invalid url is not relative either
0610   if(!linkUrl.isRelative()) {
0611     return link_;
0612   }
0613 
0614   if(m_xsltFilePath.isEmpty()) {
0615     m_xsltFilePath = DataFileRegistry::self()->locate(m_xsltFile);
0616     if(m_xsltFilePath.isEmpty()) {
0617       myWarning() << "no xslt file for " << m_xsltFile;
0618     }
0619   }
0620 
0621   QUrl u = QUrl::fromLocalFile(m_xsltFilePath);
0622   u = u.resolved(linkUrl);
0623 
0624   // one of the "quirks" of the html export is that img src urls are set to point to
0625   // the tmpDir() when exporting entry files from a collection, but those images
0626   // don't actually exist, and they get copied in writeImages() instead.
0627   // so we only need to keep track of the url if it exists
0628   const bool exists = NetAccess::exists(u, false, m_widget);
0629   if(exists) {
0630     m_files.append(u);
0631   }
0632 
0633   // if we're exporting entry files, we want pics/ to
0634   // go in pics/
0635   const bool isPic = link_.startsWith(m_dataDir + QLatin1String("pics/"));
0636   QString midDir;
0637   if(m_exportEntryFiles && isPic) {
0638     midDir = QStringLiteral("pics/");
0639   }
0640   // pictures are special since they might not exist when the HTML is exported, since they might get copied later
0641   // on the other hand, don't change the file location if it doesn't exist
0642   // and only use relative location if an export URL() is set
0643   if((isPic || exists) && !url().isEmpty()) {
0644     m_links.insert(link_, fileDirName() + midDir + u.fileName());
0645   } else {
0646     m_links.insert(link_, link_);
0647   }
0648 //  myDebug() << link_ << linkUrl << u << m_links[link_];
0649   return m_links[link_];
0650 }
0651 
0652 const xmlChar* HTMLExporter::analyzeInternalCSS(const xmlChar* str_) {
0653   return reinterpret_cast<xmlChar*>(qstrdup(analyzeInternalCSS(QString::fromUtf8(reinterpret_cast<const char*>(str_))).toUtf8().constData()));
0654 }
0655 
0656 QString HTMLExporter::analyzeInternalCSS(const QString& str_) {
0657   QString str = str_;
0658   int start = 0;
0659   int end = 0;
0660   const QString url = QStringLiteral("url(");
0661   for(int pos = str.indexOf(url); pos >= 0; pos = str.indexOf(url, pos+1)) {
0662     pos += 4; // url(
0663     if(str[pos] ==  QLatin1Char('"') || str[pos] == QLatin1Char('\'')) {
0664       ++pos;
0665     }
0666 
0667     start = pos;
0668     pos = str.indexOf(QLatin1Char(')'), start);
0669     end = pos;
0670     if(str[pos-1] == QLatin1Char('"') || str[pos-1] == QLatin1Char('\'')) {
0671       --end;
0672     }
0673 
0674     str.replace(start, end-start, handleLink(str.mid(start, end-start)));
0675   }
0676   return str;
0677 }
0678 
0679 void HTMLExporter::createDir() {
0680   if(!m_checkCreateDir) {
0681     return;
0682   }
0683   QUrl dir = fileDir();
0684   if(dir.isEmpty()) {
0685     myDebug() << "called on empty URL!";
0686     return;
0687   }
0688   if(NetAccess::exists(dir, false, m_widget)) {
0689     m_checkCreateDir = false;
0690   } else {
0691     KIO::Job* job = KIO::mkdir(dir);
0692     KJobWidgets::setWindow(job, m_widget);
0693     m_checkCreateDir = !job->exec();
0694   }
0695 }
0696 
0697 bool HTMLExporter::copyFiles() {
0698   if(m_files.isEmpty()) {
0699     return true;
0700   }
0701   const int start = 20;
0702   const int maxProgress = m_exportEntryFiles ? 40 : 80;
0703   const int stepSize = qMax(1, m_files.count()/maxProgress);
0704   int j = 0;
0705 
0706   createDir();
0707   QUrl target;
0708   for(QList<QUrl>::ConstIterator it = m_files.constBegin(); it != m_files.constEnd() && !m_cancelled; ++it, ++j) {
0709     if(m_copiedFiles.has((*it).url())) {
0710       continue;
0711     }
0712 
0713     if(target.isEmpty()) {
0714       target = fileDir();
0715     }
0716     target = target.adjusted(QUrl::RemoveFilename);
0717     target.setPath(target.path() + (*it).fileName());
0718     KIO::JobFlags flags = KIO::Overwrite;
0719     if(!m_widget) flags |= KIO::HideProgressInfo;
0720     KIO::FileCopyJob* job = KIO::file_copy(*it, target, -1, flags);
0721     KJobWidgets::setWindow(job, m_widget);
0722     if(job->exec()) {
0723       m_copiedFiles.add((*it).url());
0724     } else {
0725       myWarning() << "can't copy " << target;
0726       myWarning() << job->errorString();
0727     }
0728     if(j%stepSize == 0) {
0729       if(options() & ExportProgress) {
0730         ProgressManager::self()->setProgress(this, qMin(start+j/stepSize, 99));
0731       }
0732       qApp->processEvents();
0733     }
0734   }
0735   return true;
0736 }
0737 
0738 bool HTMLExporter::writeEntryFiles() {
0739   if(m_entryXSLTFile.isEmpty()) {
0740     myWarning() << "no entry XSLT file";
0741     return false;
0742   }
0743 
0744   const int start = 60;
0745   const int stepSize = qMax(1, entries().count()/40);
0746   int j = 0;
0747 
0748   // now worry about actually exporting entry files
0749   // I can't reliable encode a string as a URI, so I'm punting, and I'll just replace everything but
0750   // a-zA-Z0-9 with an underscore. This MUST match the filename template in tellico2html.xsl
0751   // the id is used so uniqueness is guaranteed
0752   const QRegularExpression badChars(QLatin1String("[^-a-zA-Z0-9]"));
0753   FieldFormat::Request formatted = (options() & Export::ExportFormatted ?
0754                                                    FieldFormat::ForceFormat :
0755                                                    FieldFormat::AsIsFormat);
0756 
0757   QUrl outputFile = fileDir();
0758 
0759   GUI::CursorSaver cs(Qt::WaitCursor);
0760 
0761   HTMLExporter exporter(collection());
0762   long opt = options() | Export::ExportForce;
0763   opt &= ~ExportProgress;
0764   exporter.setFields(fields());
0765   exporter.setOptions(opt);
0766   exporter.setXSLTFile(m_entryXSLTFile);
0767   exporter.setCollectionURL(url());
0768   bool parseDOM = true;
0769 
0770   const QString title = QStringLiteral("title");
0771   const QString html = QStringLiteral(".html");
0772   bool multipleTitles = collection()->fieldByName(title)->hasFlag(Data::Field::AllowMultiple);
0773   Data::EntryList entries = this->entries(); // not const since the pointer has to be copied
0774   foreach(Data::EntryPtr entryIt, entries) {
0775     QString file = entryIt->formattedField(title, formatted);
0776 
0777     // but only use the first title if it has multiple
0778     if(multipleTitles) {
0779       file = file.section(QLatin1Char(';'), 0, 0);
0780     }
0781     file.replace(badChars, QStringLiteral("_"));
0782     file += QLatin1Char('-') + QString::number(entryIt->id()) + html;
0783     outputFile = outputFile.adjusted(QUrl::RemoveFilename);
0784     outputFile.setPath(outputFile.path() + file);
0785 
0786     exporter.setEntries(Data::EntryList() << entryIt);
0787     exporter.setURL(outputFile);
0788     exporter.exec();
0789 
0790     // no longer need to parse DOM
0791     if(parseDOM) {
0792       parseDOM = false;
0793       exporter.setParseDOM(false);
0794       // this is rather stupid, but I'm too lazy to figure out the better way
0795       // since we parsed the DOM for the first entry file to grab any
0796       // images used in the template, need to resave it so the image links
0797       // get written correctly
0798       exporter.exec();
0799     }
0800 
0801     if(j%stepSize == 0) {
0802       if(options() & ExportProgress) {
0803         ProgressManager::self()->setProgress(this, qMin(start+j/stepSize, 99));
0804       }
0805       qApp->processEvents();
0806     }
0807     ++j;
0808   }
0809   // the images in "pics/" are special data images, copy them always
0810   // since the entry files may refer to them, but we don't know that
0811   QStringList dataImages;
0812   dataImages.reserve(1 + 10);
0813   dataImages << QStringLiteral("checkmark.png");
0814   for(uint i = 1; i <= 10; ++i) {
0815     dataImages << QStringLiteral("stars%1.png").arg(i);
0816   }
0817   QUrl dataDir = QUrl::fromLocalFile(Tellico::installationDir() + QLatin1String("pics/"));
0818   QUrl target = fileDir();
0819   target = target.adjusted(QUrl::StripTrailingSlash);
0820   target.setPath(target.path() + QLatin1Char('/') + (QLatin1String("pics/")));
0821   KIO::Job* job = KIO::mkdir(target);
0822   KJobWidgets::setWindow(job, m_widget);
0823   job->exec();
0824   KIO::JobFlags flags = KIO::DefaultFlags;
0825   if(!m_widget) flags |= KIO::HideProgressInfo;
0826   foreach(const QString& dataImage, dataImages) {
0827     dataDir = dataDir.adjusted(QUrl::RemoveFilename);
0828     dataDir.setPath(dataDir.path() + dataImage);
0829     target = target.adjusted(QUrl::RemoveFilename);
0830     target.setPath(target.path() + dataImage);
0831     KIO::Job* job = KIO::file_copy(dataDir, target, -1, flags);
0832     KJobWidgets::setWindow(job, m_widget);
0833     job->exec();
0834   }
0835 
0836   return true;
0837 }
0838 
0839 void HTMLExporter::slotCancel() {
0840   m_cancelled = true;
0841 }
0842 
0843 void HTMLExporter::parseDOM(xmlNode* node_) {
0844   if(node_ == nullptr) {
0845     myDebug() << "no node";
0846     return;
0847   }
0848 
0849   bool parseChildren = true;
0850 
0851   if(node_->type == XML_ELEMENT_NODE) {
0852     const QByteArray nodeName = QByteArray(reinterpret_cast<const char*>(node_->name)).toUpper();
0853     xmlElement* elem = reinterpret_cast<xmlElement*>(node_);
0854     // to speed up things, check now for nodename
0855     if(nodeName == "IMG" || nodeName == "SCRIPT" || nodeName == "LINK") {
0856       for(xmlAttribute* attr = elem->attributes; attr; attr = reinterpret_cast<xmlAttribute*>(attr->next)) {
0857         QByteArray attrName = QByteArray(reinterpret_cast<const char*>(attr->name)).toUpper();
0858 
0859         if( (attrName == "SRC" && (nodeName == "IMG" || nodeName == "SCRIPT")) ||
0860             (attrName == "HREF" && nodeName == "LINK")) {
0861 /*          (attrName == "BACKGROUND" && (nodeName == "BODY" ||
0862                                                        nodeName == "TABLE" ||
0863                                                        nodeName == "TH" ||
0864                                                        nodeName == "TD"))) */
0865           xmlChar* value = xmlGetProp(node_, attr->name);
0866           if(value) {
0867             xmlSetProp(node_, attr->name, handleLink(value));
0868             xmlFree(value);
0869           }
0870           // each node only has one significant attribute, so break now
0871           break;
0872         }
0873       }
0874     } else if(nodeName == "STYLE") {
0875       // if the first child is a CDATA, use it, otherwise replace complete node
0876       xmlNode* nodeToReplace = node_;
0877       xmlNode* child = node_->children;
0878       if(child && child->type == XML_CDATA_SECTION_NODE) {
0879         nodeToReplace = child;
0880       }
0881       xmlChar* value = xmlNodeGetContent(nodeToReplace);
0882       if(value) {
0883         xmlNodeSetContent(nodeToReplace, analyzeInternalCSS(value));
0884         xmlFree(value);
0885       }
0886       // no longer need to parse child text nodes
0887       parseChildren = false;
0888     }
0889   }
0890 
0891   if(parseChildren) {
0892     xmlNode* child = node_->children;
0893     while(child) {
0894       parseDOM(child);
0895       child = child->next;
0896     }
0897   }
0898 }