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 }