File indexing completed on 2024-05-12 16:06:44
0001 /* 0002 SPDX-FileCopyrightText: 2017 Julian Wolff <wolff@julianwolff.de> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "converter.h" 0008 0009 #include <KLocalizedString> 0010 0011 #include <QTextCursor> 0012 #include <QTextDocument> 0013 #include <QTextFrame> 0014 #include <QTextStream> 0015 0016 #include <core/action.h> 0017 0018 #include "debug_md.h" 0019 0020 extern "C" { 0021 #include <mkdio.h> 0022 } 0023 0024 #define PAGE_WIDTH 980 0025 #define PAGE_HEIGHT 1307 0026 #define PAGE_MARGIN 45 0027 #define CONTENT_WIDTH (PAGE_WIDTH - 2 * PAGE_MARGIN) 0028 0029 using namespace Markdown; 0030 0031 Converter::Converter() 0032 : m_markdownFile(nullptr) 0033 , m_isFancyPantsEnabled(true) 0034 { 0035 } 0036 0037 Converter::~Converter() 0038 { 0039 if (m_markdownFile) { 0040 fclose(m_markdownFile); 0041 } 0042 } 0043 0044 QTextDocument *Converter::convert(const QString &fileName) 0045 { 0046 m_markdownFile = fopen(fileName.toLocal8Bit().constData(), "rb"); 0047 if (!m_markdownFile) { 0048 Q_EMIT error(i18n("Failed to open the document"), -1); 0049 return nullptr; 0050 } 0051 0052 m_fileDir = QDir(fileName.left(fileName.lastIndexOf(QLatin1Char('/')))); 0053 0054 QTextDocument *doc = convertOpenFile(); 0055 QHash<QString, QTextFragment> internalLinks; 0056 QHash<QString, QTextBlock> documentAnchors; 0057 extractLinks(doc->rootFrame(), internalLinks, documentAnchors); 0058 0059 for (auto linkIt = internalLinks.constBegin(); linkIt != internalLinks.constEnd(); ++linkIt) { 0060 auto anchorIt = documentAnchors.constFind(linkIt.key()); 0061 if (anchorIt != documentAnchors.constEnd()) { 0062 const Okular::DocumentViewport viewport = calculateViewport(doc, anchorIt.value()); 0063 Okular::GotoAction *action = new Okular::GotoAction(QString(), viewport); 0064 Q_EMIT addAction(action, linkIt.value().position(), linkIt.value().position() + linkIt.value().length()); 0065 } else { 0066 qDebug() << "Could not find destination for" << linkIt.key(); 0067 } 0068 } 0069 0070 return doc; 0071 } 0072 0073 void Converter::convertAgain() 0074 { 0075 setDocument(convertOpenFile()); 0076 } 0077 0078 QTextDocument *Converter::convertOpenFile() 0079 { 0080 int result = fseek(m_markdownFile, 0, SEEK_SET); 0081 if (result != 0) { 0082 Q_EMIT error(i18n("Failed to open the document"), -1); 0083 return nullptr; 0084 } 0085 0086 #if defined(MKD_NOLINKS) 0087 // on discount 2 MKD_NOLINKS is a define 0088 MMIOT *markdownHandle = mkd_in(m_markdownFile, 0); 0089 0090 int flags = MKD_FENCEDCODE | MKD_GITHUBTAGS | MKD_AUTOLINK | MKD_TOC | MKD_IDANCHOR; 0091 if (!m_isFancyPantsEnabled) { 0092 flags |= MKD_NOPANTS; 0093 } 0094 if (!mkd_compile(markdownHandle, flags)) { 0095 Q_EMIT error(i18n("Failed to compile the Markdown document."), -1); 0096 return nullptr; 0097 } 0098 #else 0099 // on discount 3 MKD_NOLINKS is an enum value 0100 MMIOT *markdownHandle = mkd_in(m_markdownFile, nullptr); 0101 0102 mkd_flag_t *flags = mkd_flags(); 0103 // These flags aren't bitflags, so they can't be | together 0104 mkd_set_flag_num(flags, MKD_FENCEDCODE); 0105 mkd_set_flag_num(flags, MKD_GITHUBTAGS); 0106 mkd_set_flag_num(flags, MKD_AUTOLINK); 0107 mkd_set_flag_num(flags, MKD_TOC); 0108 mkd_set_flag_num(flags, MKD_IDANCHOR); 0109 if (!m_isFancyPantsEnabled) { 0110 mkd_set_flag_num(flags, MKD_NOPANTS); 0111 } 0112 if (!mkd_compile(markdownHandle, flags)) { 0113 Q_EMIT error(i18n("Failed to compile the Markdown document."), -1); 0114 mkd_free_flags(flags); 0115 return nullptr; 0116 } 0117 mkd_free_flags(flags); 0118 #endif 0119 0120 char *htmlDocument; 0121 const int size = mkd_document(markdownHandle, &htmlDocument); 0122 0123 const QString html = QString::fromUtf8(htmlDocument, size); 0124 0125 QTextDocument *textDocument = new QTextDocument; 0126 textDocument->setPageSize(QSizeF(PAGE_WIDTH, PAGE_HEIGHT)); 0127 textDocument->setHtml(html); 0128 if (generator()) { 0129 textDocument->setDefaultFont(generator()->generalSettings()->font()); 0130 } 0131 0132 mkd_cleanup(markdownHandle); 0133 0134 QTextFrameFormat frameFormat; 0135 frameFormat.setMargin(PAGE_MARGIN); 0136 0137 QTextFrame *rootFrame = textDocument->rootFrame(); 0138 rootFrame->setFrameFormat(frameFormat); 0139 0140 convertImages(rootFrame, m_fileDir, textDocument); 0141 0142 return textDocument; 0143 } 0144 0145 void Converter::extractLinks(QTextFrame *parent, QHash<QString, QTextFragment> &internalLinks, QHash<QString, QTextBlock> &documentAnchors) 0146 { 0147 for (QTextFrame::iterator it = parent->begin(); !it.atEnd(); ++it) { 0148 QTextFrame *textFrame = it.currentFrame(); 0149 const QTextBlock textBlock = it.currentBlock(); 0150 0151 if (textFrame) { 0152 extractLinks(textFrame, internalLinks, documentAnchors); 0153 } else if (textBlock.isValid()) { 0154 extractLinks(textBlock, internalLinks, documentAnchors); 0155 } 0156 } 0157 } 0158 0159 void Converter::extractLinks(const QTextBlock &parent, QHash<QString, QTextFragment> &internalLinks, QHash<QString, QTextBlock> &documentAnchors) 0160 { 0161 for (QTextBlock::iterator it = parent.begin(); !it.atEnd(); ++it) { 0162 const QTextFragment textFragment = it.fragment(); 0163 if (textFragment.isValid()) { 0164 const QTextCharFormat textCharFormat = textFragment.charFormat(); 0165 if (textCharFormat.isAnchor()) { 0166 const QString href = textCharFormat.anchorHref(); 0167 if (href.startsWith(QLatin1Char('#'))) { // It's an internal link, store it and we'll resolve it at the end 0168 internalLinks.insert(href.mid(1), textFragment); 0169 } else { 0170 Okular::BrowseAction *action = new Okular::BrowseAction(QUrl(textCharFormat.anchorHref())); 0171 Q_EMIT addAction(action, textFragment.position(), textFragment.position() + textFragment.length()); 0172 } 0173 0174 const QStringList anchorNames = textCharFormat.anchorNames(); 0175 for (const QString &anchorName : anchorNames) { 0176 documentAnchors.insert(anchorName, parent); 0177 } 0178 } 0179 } 0180 } 0181 } 0182 0183 void Converter::convertImages(QTextFrame *parent, const QDir &dir, QTextDocument *textDocument) 0184 { 0185 for (QTextFrame::iterator it = parent->begin(); !it.atEnd(); ++it) { 0186 QTextFrame *textFrame = it.currentFrame(); 0187 const QTextBlock textBlock = it.currentBlock(); 0188 0189 if (textFrame) { 0190 convertImages(textFrame, dir, textDocument); 0191 } else if (textBlock.isValid()) { 0192 convertImages(textBlock, dir, textDocument); 0193 } 0194 } 0195 } 0196 0197 void Converter::convertImages(const QTextBlock &parent, const QDir &dir, QTextDocument *textDocument) 0198 { 0199 for (QTextBlock::iterator it = parent.begin(); !it.atEnd(); ++it) { 0200 const QTextFragment textFragment = it.fragment(); 0201 if (textFragment.isValid()) { 0202 const QTextCharFormat textCharFormat = textFragment.charFormat(); 0203 if (textCharFormat.isImageFormat()) { 0204 QTextImageFormat format; 0205 0206 const qreal specifiedHeight = textCharFormat.toImageFormat().height(); 0207 const qreal specifiedWidth = textCharFormat.toImageFormat().width(); 0208 0209 QTextCursor cursor(textDocument); 0210 cursor.setPosition(textFragment.position(), QTextCursor::MoveAnchor); 0211 cursor.setPosition(textFragment.position() + textFragment.length(), QTextCursor::KeepAnchor); 0212 0213 const QString imageFilePath = QDir::cleanPath(dir.absoluteFilePath(QUrl::fromPercentEncoding(textCharFormat.toImageFormat().name().toUtf8()))); 0214 0215 if (QFile::exists(imageFilePath)) { 0216 cursor.removeSelectedText(); 0217 format.setName(imageFilePath); 0218 const QImage img = QImage(format.name()); 0219 0220 setImageSize(format, specifiedWidth, specifiedHeight, img.width(), img.height()); 0221 0222 cursor.insertImage(format); 0223 } else if ((!textCharFormat.toImageFormat().property(QTextFormat::ImageAltText).toString().isEmpty())) { 0224 cursor.insertText(textCharFormat.toImageFormat().property(QTextFormat::ImageAltText).toString()); 0225 } 0226 } 0227 } 0228 } 0229 } 0230 0231 void Converter::setImageSize(QTextImageFormat &format, const qreal specifiedWidth, const qreal specifiedHeight, const qreal originalWidth, const qreal originalHeight) 0232 { 0233 qreal width = 0; 0234 qreal height = 0; 0235 0236 const bool hasSpecifiedSize = specifiedHeight > 0 || specifiedWidth > 0; 0237 if (hasSpecifiedSize) { 0238 width = specifiedWidth; 0239 height = specifiedHeight; 0240 if (width == 0 && originalHeight > 0) { 0241 width = originalWidth * height / originalHeight; 0242 } else if (height == 0 && originalWidth > 0) { 0243 height = originalHeight * width / originalWidth; 0244 } 0245 } else { 0246 width = originalWidth; 0247 height = originalHeight; 0248 } 0249 0250 if (width > CONTENT_WIDTH) { 0251 height = height * CONTENT_WIDTH / width; 0252 width = CONTENT_WIDTH; 0253 } 0254 format.setWidth(width); 0255 format.setHeight(height); 0256 }