2021-05-24 07:25:56 +00:00
|
|
|
/*
|
|
|
|
SPDX-FileCopyrightText: 2017 Julian Wolff <wolff@julianwolff.de>
|
|
|
|
|
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
*/
|
2017-08-24 23:04:54 +00:00
|
|
|
|
|
|
|
#include "converter.h"
|
|
|
|
|
|
|
|
#include <KLocalizedString>
|
|
|
|
|
2024-04-22 17:00:02 +00:00
|
|
|
#include <QDomDocument>
|
2017-08-24 23:04:54 +00:00
|
|
|
#include <QTextCursor>
|
|
|
|
#include <QTextDocument>
|
|
|
|
#include <QTextFrame>
|
|
|
|
#include <QTextStream>
|
|
|
|
|
|
|
|
#include <core/action.h>
|
|
|
|
|
|
|
|
#include "debug_md.h"
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
#include <mkdio.h>
|
|
|
|
}
|
|
|
|
|
2020-08-22 23:32:27 +00:00
|
|
|
#define PAGE_WIDTH 980
|
|
|
|
#define PAGE_HEIGHT 1307
|
|
|
|
#define PAGE_MARGIN 45
|
|
|
|
#define CONTENT_WIDTH (PAGE_WIDTH - 2 * PAGE_MARGIN)
|
|
|
|
|
2017-08-24 23:04:54 +00:00
|
|
|
using namespace Markdown;
|
|
|
|
|
2024-04-22 17:00:02 +00:00
|
|
|
static void recursiveRenameTags(QDomElement &elem)
|
|
|
|
{
|
|
|
|
for (QDomNode node = elem.firstChild(); !node.isNull(); node = node.nextSibling()) {
|
|
|
|
QDomElement child = node.toElement();
|
|
|
|
if (!child.isNull()) {
|
|
|
|
// Discount emits <del> tags for ~~ but Qt doesn't understand them and
|
|
|
|
// removes them. Instead replace them with <s> tags which Qt does
|
|
|
|
// understand.
|
|
|
|
if (child.nodeName() == QStringLiteral("del")) {
|
|
|
|
child.setTagName(QStringLiteral("s"));
|
|
|
|
}
|
|
|
|
recursiveRenameTags(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString detail::fixupHtmlTags(QString &&html)
|
|
|
|
{
|
|
|
|
QDomDocument dom;
|
|
|
|
// Discount emits simplified HTML but QDomDocument will barf if everything isn't
|
|
|
|
// inside a "root" node. Luckily QTextDocument ignores unknown tags so we can just
|
|
|
|
// wrap the original HTML with a fake tag that'll be stripped off later.
|
|
|
|
if (!dom.setContent(QStringLiteral("<ignored_by_qt>") + html + QStringLiteral("</ignored_by_qt>"))) {
|
|
|
|
return std::move(html);
|
|
|
|
}
|
|
|
|
QDomElement elem = dom.documentElement();
|
|
|
|
recursiveRenameTags(elem);
|
|
|
|
// Don't add any indentation otherwise code blocks can gain indents.
|
|
|
|
return dom.toString(-1);
|
|
|
|
}
|
|
|
|
|
2017-08-24 23:04:54 +00:00
|
|
|
Converter::Converter()
|
2019-03-30 22:47:17 +00:00
|
|
|
: m_markdownFile(nullptr)
|
2020-08-22 23:32:27 +00:00
|
|
|
, m_isFancyPantsEnabled(true)
|
2017-08-24 23:04:54 +00:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
Converter::~Converter()
|
|
|
|
{
|
2019-03-30 22:47:17 +00:00
|
|
|
if (m_markdownFile) {
|
|
|
|
fclose(m_markdownFile);
|
|
|
|
}
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
QTextDocument *Converter::convert(const QString &fileName)
|
|
|
|
{
|
2024-03-06 14:52:21 +00:00
|
|
|
if (m_markdownFile) {
|
|
|
|
fclose(m_markdownFile);
|
|
|
|
}
|
2022-04-02 21:21:33 +00:00
|
|
|
m_markdownFile = fopen(fileName.toLocal8Bit().constData(), "rb");
|
2019-03-30 22:47:17 +00:00
|
|
|
if (!m_markdownFile) {
|
2022-03-18 21:35:45 +00:00
|
|
|
Q_EMIT error(i18n("Failed to open the document"), -1);
|
2018-05-01 19:10:13 +00:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-04-02 21:21:33 +00:00
|
|
|
m_fileDir = QDir(fileName.left(fileName.lastIndexOf(QLatin1Char('/'))));
|
2019-03-30 22:47:17 +00:00
|
|
|
|
|
|
|
QTextDocument *doc = convertOpenFile();
|
2020-05-12 22:18:14 +00:00
|
|
|
QHash<QString, QTextFragment> internalLinks;
|
|
|
|
QHash<QString, QTextBlock> documentAnchors;
|
|
|
|
extractLinks(doc->rootFrame(), internalLinks, documentAnchors);
|
|
|
|
|
|
|
|
for (auto linkIt = internalLinks.constBegin(); linkIt != internalLinks.constEnd(); ++linkIt) {
|
|
|
|
auto anchorIt = documentAnchors.constFind(linkIt.key());
|
|
|
|
if (anchorIt != documentAnchors.constEnd()) {
|
|
|
|
const Okular::DocumentViewport viewport = calculateViewport(doc, anchorIt.value());
|
|
|
|
Okular::GotoAction *action = new Okular::GotoAction(QString(), viewport);
|
2022-03-18 21:35:45 +00:00
|
|
|
Q_EMIT addAction(action, linkIt.value().position(), linkIt.value().position() + linkIt.value().length());
|
2020-05-12 22:18:14 +00:00
|
|
|
} else {
|
|
|
|
qDebug() << "Could not find destination for" << linkIt.key();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-30 22:47:17 +00:00
|
|
|
return doc;
|
|
|
|
}
|
|
|
|
|
|
|
|
void Converter::convertAgain()
|
|
|
|
{
|
|
|
|
setDocument(convertOpenFile());
|
|
|
|
}
|
|
|
|
|
|
|
|
QTextDocument *Converter::convertOpenFile()
|
|
|
|
{
|
2023-12-12 12:04:14 +00:00
|
|
|
int result = fseek(m_markdownFile, 0, SEEK_SET);
|
|
|
|
if (result != 0) {
|
|
|
|
Q_EMIT error(i18n("Failed to open the document"), -1);
|
|
|
|
return nullptr;
|
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2023-02-01 21:01:52 +00:00
|
|
|
#if defined(MKD_NOLINKS)
|
|
|
|
// on discount 2 MKD_NOLINKS is a define
|
2019-03-30 22:47:17 +00:00
|
|
|
MMIOT *markdownHandle = mkd_in(m_markdownFile, 0);
|
|
|
|
|
2020-05-12 22:18:14 +00:00
|
|
|
int flags = MKD_FENCEDCODE | MKD_GITHUBTAGS | MKD_AUTOLINK | MKD_TOC | MKD_IDANCHOR;
|
2020-08-22 23:32:27 +00:00
|
|
|
if (!m_isFancyPantsEnabled) {
|
2019-03-30 22:47:17 +00:00
|
|
|
flags |= MKD_NOPANTS;
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2019-03-30 22:47:17 +00:00
|
|
|
if (!mkd_compile(markdownHandle, flags)) {
|
2022-03-18 21:35:45 +00:00
|
|
|
Q_EMIT error(i18n("Failed to compile the Markdown document."), -1);
|
2020-02-19 15:12:21 +00:00
|
|
|
return nullptr;
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
2023-02-01 21:01:52 +00:00
|
|
|
#else
|
|
|
|
// on discount 3 MKD_NOLINKS is an enum value
|
|
|
|
MMIOT *markdownHandle = mkd_in(m_markdownFile, nullptr);
|
|
|
|
|
|
|
|
mkd_flag_t *flags = mkd_flags();
|
2023-08-02 08:55:01 +00:00
|
|
|
// These flags aren't bitflags, so they can't be | together
|
|
|
|
mkd_set_flag_num(flags, MKD_FENCEDCODE);
|
|
|
|
mkd_set_flag_num(flags, MKD_GITHUBTAGS);
|
|
|
|
mkd_set_flag_num(flags, MKD_AUTOLINK);
|
|
|
|
mkd_set_flag_num(flags, MKD_TOC);
|
|
|
|
mkd_set_flag_num(flags, MKD_IDANCHOR);
|
2023-02-01 21:01:52 +00:00
|
|
|
if (!m_isFancyPantsEnabled) {
|
|
|
|
mkd_set_flag_num(flags, MKD_NOPANTS);
|
|
|
|
}
|
|
|
|
if (!mkd_compile(markdownHandle, flags)) {
|
|
|
|
Q_EMIT error(i18n("Failed to compile the Markdown document."), -1);
|
2023-08-02 08:55:01 +00:00
|
|
|
mkd_free_flags(flags);
|
2023-02-01 21:01:52 +00:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
mkd_free_flags(flags);
|
|
|
|
#endif
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2017-08-24 23:04:54 +00:00
|
|
|
char *htmlDocument;
|
2017-08-24 23:14:58 +00:00
|
|
|
const int size = mkd_document(markdownHandle, &htmlDocument);
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2024-04-22 17:00:02 +00:00
|
|
|
const QString html = detail::fixupHtmlTags(QString::fromUtf8(htmlDocument, size));
|
2019-03-30 22:47:17 +00:00
|
|
|
|
2017-08-24 23:14:58 +00:00
|
|
|
QTextDocument *textDocument = new QTextDocument;
|
2020-08-22 23:32:27 +00:00
|
|
|
textDocument->setPageSize(QSizeF(PAGE_WIDTH, PAGE_HEIGHT));
|
2017-08-24 23:14:58 +00:00
|
|
|
textDocument->setHtml(html);
|
2020-08-22 23:32:27 +00:00
|
|
|
if (generator()) {
|
|
|
|
textDocument->setDefaultFont(generator()->generalSettings()->font());
|
2022-03-08 10:10:43 +00:00
|
|
|
}
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2017-08-24 23:04:54 +00:00
|
|
|
mkd_cleanup(markdownHandle);
|
2020-05-12 22:18:14 +00:00
|
|
|
|
2017-08-24 23:04:54 +00:00
|
|
|
QTextFrameFormat frameFormat;
|
2020-08-22 23:32:27 +00:00
|
|
|
frameFormat.setMargin(PAGE_MARGIN);
|
2017-08-24 23:04:54 +00:00
|
|
|
|
2017-08-24 23:14:58 +00:00
|
|
|
QTextFrame *rootFrame = textDocument->rootFrame();
|
2017-08-24 23:04:54 +00:00
|
|
|
rootFrame->setFrameFormat(frameFormat);
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2019-03-30 22:47:17 +00:00
|
|
|
convertImages(rootFrame, m_fileDir, textDocument);
|
2017-08-24 23:04:54 +00:00
|
|
|
|
2017-08-24 23:14:58 +00:00
|
|
|
return textDocument;
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
|
|
|
|
2020-05-12 22:18:14 +00:00
|
|
|
void Converter::extractLinks(QTextFrame *parent, QHash<QString, QTextFragment> &internalLinks, QHash<QString, QTextBlock> &documentAnchors)
|
2017-08-24 23:04:54 +00:00
|
|
|
{
|
|
|
|
for (QTextFrame::iterator it = parent->begin(); !it.atEnd(); ++it) {
|
|
|
|
QTextFrame *textFrame = it.currentFrame();
|
2017-08-24 23:14:58 +00:00
|
|
|
const QTextBlock textBlock = it.currentBlock();
|
2017-08-24 23:04:54 +00:00
|
|
|
|
|
|
|
if (textFrame) {
|
2020-05-12 22:18:14 +00:00
|
|
|
extractLinks(textFrame, internalLinks, documentAnchors);
|
2017-08-24 23:14:58 +00:00
|
|
|
} else if (textBlock.isValid()) {
|
2020-05-12 22:18:14 +00:00
|
|
|
extractLinks(textBlock, internalLinks, documentAnchors);
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-12 22:18:14 +00:00
|
|
|
void Converter::extractLinks(const QTextBlock &parent, QHash<QString, QTextFragment> &internalLinks, QHash<QString, QTextBlock> &documentAnchors)
|
2017-08-24 23:04:54 +00:00
|
|
|
{
|
|
|
|
for (QTextBlock::iterator it = parent.begin(); !it.atEnd(); ++it) {
|
2017-08-24 23:14:58 +00:00
|
|
|
const QTextFragment textFragment = it.fragment();
|
2017-08-24 23:04:54 +00:00
|
|
|
if (textFragment.isValid()) {
|
2017-08-24 23:14:58 +00:00
|
|
|
const QTextCharFormat textCharFormat = textFragment.charFormat();
|
2017-08-24 23:04:54 +00:00
|
|
|
if (textCharFormat.isAnchor()) {
|
2020-05-12 22:18:14 +00:00
|
|
|
const QString href = textCharFormat.anchorHref();
|
2022-04-02 21:21:33 +00:00
|
|
|
if (href.startsWith(QLatin1Char('#'))) { // It's an internal link, store it and we'll resolve it at the end
|
2020-05-12 22:18:14 +00:00
|
|
|
internalLinks.insert(href.mid(1), textFragment);
|
|
|
|
} else {
|
|
|
|
Okular::BrowseAction *action = new Okular::BrowseAction(QUrl(textCharFormat.anchorHref()));
|
2022-03-18 21:35:45 +00:00
|
|
|
Q_EMIT addAction(action, textFragment.position(), textFragment.position() + textFragment.length());
|
2020-05-12 22:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const QStringList anchorNames = textCharFormat.anchorNames();
|
|
|
|
for (const QString &anchorName : anchorNames) {
|
|
|
|
documentAnchors.insert(anchorName, parent);
|
|
|
|
}
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-24 23:14:58 +00:00
|
|
|
void Converter::convertImages(QTextFrame *parent, const QDir &dir, QTextDocument *textDocument)
|
2017-08-24 23:04:54 +00:00
|
|
|
{
|
|
|
|
for (QTextFrame::iterator it = parent->begin(); !it.atEnd(); ++it) {
|
|
|
|
QTextFrame *textFrame = it.currentFrame();
|
2017-08-24 23:14:58 +00:00
|
|
|
const QTextBlock textBlock = it.currentBlock();
|
2017-08-24 23:04:54 +00:00
|
|
|
|
|
|
|
if (textFrame) {
|
2017-08-24 23:14:58 +00:00
|
|
|
convertImages(textFrame, dir, textDocument);
|
|
|
|
} else if (textBlock.isValid()) {
|
|
|
|
convertImages(textBlock, dir, textDocument);
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-24 23:14:58 +00:00
|
|
|
void Converter::convertImages(const QTextBlock &parent, const QDir &dir, QTextDocument *textDocument)
|
2017-08-24 23:04:54 +00:00
|
|
|
{
|
|
|
|
for (QTextBlock::iterator it = parent.begin(); !it.atEnd(); ++it) {
|
2017-08-24 23:14:58 +00:00
|
|
|
const QTextFragment textFragment = it.fragment();
|
2017-08-24 23:04:54 +00:00
|
|
|
if (textFragment.isValid()) {
|
2017-08-24 23:14:58 +00:00
|
|
|
const QTextCharFormat textCharFormat = textFragment.charFormat();
|
2017-08-24 23:04:54 +00:00
|
|
|
if (textCharFormat.isImageFormat()) {
|
|
|
|
QTextImageFormat format;
|
2020-07-10 22:15:05 +00:00
|
|
|
|
2020-08-22 23:32:27 +00:00
|
|
|
const qreal specifiedHeight = textCharFormat.toImageFormat().height();
|
|
|
|
const qreal specifiedWidth = textCharFormat.toImageFormat().width();
|
|
|
|
|
2017-08-24 23:14:58 +00:00
|
|
|
QTextCursor cursor(textDocument);
|
2017-08-24 23:04:54 +00:00
|
|
|
cursor.setPosition(textFragment.position(), QTextCursor::MoveAnchor);
|
|
|
|
cursor.setPosition(textFragment.position() + textFragment.length(), QTextCursor::KeepAnchor);
|
2021-07-04 21:08:33 +00:00
|
|
|
|
2022-04-03 16:02:44 +00:00
|
|
|
const QString imageFilePath = QDir::cleanPath(dir.absoluteFilePath(QUrl::fromPercentEncoding(textCharFormat.toImageFormat().name().toUtf8())));
|
|
|
|
|
2021-07-04 21:08:33 +00:00
|
|
|
if (QFile::exists(imageFilePath)) {
|
2021-09-28 21:19:38 +00:00
|
|
|
cursor.removeSelectedText();
|
2021-07-04 21:08:33 +00:00
|
|
|
format.setName(imageFilePath);
|
|
|
|
const QImage img = QImage(format.name());
|
|
|
|
|
|
|
|
setImageSize(format, specifiedWidth, specifiedHeight, img.width(), img.height());
|
|
|
|
|
|
|
|
cursor.insertImage(format);
|
|
|
|
} else if ((!textCharFormat.toImageFormat().property(QTextFormat::ImageAltText).toString().isEmpty())) {
|
|
|
|
cursor.insertText(textCharFormat.toImageFormat().property(QTextFormat::ImageAltText).toString());
|
|
|
|
}
|
2017-08-24 23:04:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-08-22 23:32:27 +00:00
|
|
|
|
|
|
|
void Converter::setImageSize(QTextImageFormat &format, const qreal specifiedWidth, const qreal specifiedHeight, const qreal originalWidth, const qreal originalHeight)
|
|
|
|
{
|
|
|
|
qreal width = 0;
|
|
|
|
qreal height = 0;
|
|
|
|
|
|
|
|
const bool hasSpecifiedSize = specifiedHeight > 0 || specifiedWidth > 0;
|
|
|
|
if (hasSpecifiedSize) {
|
|
|
|
width = specifiedWidth;
|
|
|
|
height = specifiedHeight;
|
|
|
|
if (width == 0 && originalHeight > 0) {
|
|
|
|
width = originalWidth * height / originalHeight;
|
|
|
|
} else if (height == 0 && originalWidth > 0) {
|
|
|
|
height = originalHeight * width / originalWidth;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
width = originalWidth;
|
|
|
|
height = originalHeight;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (width > CONTENT_WIDTH) {
|
|
|
|
height = height * CONTENT_WIDTH / width;
|
|
|
|
width = CONTENT_WIDTH;
|
|
|
|
}
|
|
|
|
format.setWidth(width);
|
|
|
|
format.setHeight(height);
|
|
|
|
}
|