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 }