aboutsummaryrefslogtreecommitdiff
path: root/src/renderers
diff options
context:
space:
mode:
authorFelix (xq) Queißner <git@mq32.de>2020-06-22 21:10:04 +0200
committerFelix (xq) Queißner <git@mq32.de>2020-06-22 21:10:04 +0200
commit75ec461eeaa851cb5c53f4cfffc434e3e529ed1d (patch)
tree3944737340718ca3675381aa06636045d397e780 /src/renderers
parent8dbfb0890560fd1cd698d06fa05ac868c4db8576 (diff)
downloadkristall-75ec461eeaa851cb5c53f4cfffc434e3e529ed1d.tar.gz
Restructures the project source and cleans up a bit
Diffstat (limited to 'src/renderers')
-rw-r--r--src/renderers/geminirenderer.cpp343
-rw-r--r--src/renderers/geminirenderer.hpp41
-rw-r--r--src/renderers/gophermaprenderer.cpp192
-rw-r--r--src/renderers/gophermaprenderer.hpp26
-rw-r--r--src/renderers/plaintextrenderer.cpp19
-rw-r--r--src/renderers/plaintextrenderer.hpp25
6 files changed, 646 insertions, 0 deletions
diff --git a/src/renderers/geminirenderer.cpp b/src/renderers/geminirenderer.cpp
new file mode 100644
index 0000000..ec00869
--- /dev/null
+++ b/src/renderers/geminirenderer.cpp
@@ -0,0 +1,343 @@
+#include "geminirenderer.hpp"
+
+#include <QTextList>
+#include <QTextBlock>
+#include <QList>
+#include <QStringList>
+#include <QDebug>
+
+#include "kristall.hpp"
+
+static QByteArray trim_whitespace(QByteArray items)
+{
+ int start = 0;
+ while (start < items.size() and isspace(items.at(start)))
+ {
+ start += 1;
+ }
+ int end = items.size() - 1;
+ while (end > 0 and isspace(items.at(end)))
+ {
+ end -= 1;
+ }
+ return items.mid(start, end - start + 1);
+}
+
+std::unique_ptr<GeminiDocument> GeminiRenderer::render(
+ const QByteArray &input,
+ QUrl const &root_url,
+ DocumentStyle const & themed_style,
+ DocumentOutlineModel &outline)
+{
+ QTextCharFormat preformatted;
+ preformatted.setFont(themed_style.preformatted_font);
+ preformatted.setForeground(themed_style.preformatted_color);
+
+ QTextCharFormat standard;
+ standard.setFont(themed_style.standard_font);
+ standard.setForeground(themed_style.standard_color);
+
+ QTextCharFormat standard_link;
+ standard_link.setFont(themed_style.standard_font);
+ standard_link.setForeground(QBrush(themed_style.internal_link_color));
+
+ QTextCharFormat external_link;
+ external_link.setFont(themed_style.standard_font);
+ external_link.setForeground(QBrush(themed_style.external_link_color));
+
+ QTextCharFormat cross_protocol_link;
+ cross_protocol_link.setFont(themed_style.standard_font);
+ cross_protocol_link.setForeground(QBrush(themed_style.cross_scheme_link_color));
+
+ QTextCharFormat standard_h1;
+ standard_h1.setFont(themed_style.h1_font);
+ standard_h1.setForeground(QBrush(themed_style.h1_color));
+
+ QTextCharFormat standard_h2;
+ standard_h2.setFont(themed_style.h2_font);
+ standard_h2.setForeground(QBrush(themed_style.h2_color));
+
+ QTextCharFormat standard_h3;
+ standard_h3.setFont(themed_style.h3_font);
+ standard_h3.setForeground(QBrush(themed_style.h3_color));
+
+ std::unique_ptr<GeminiDocument> result = std::make_unique<GeminiDocument>();
+ result->setDocumentMargin(themed_style.margin);
+ result->background_color = themed_style.background_color;
+ result->setIndentWidth(20);
+
+ bool emit_fancy_text = global_options.enable_text_decoration;
+
+ QTextCursor cursor{result.get()};
+
+ QTextBlockFormat standard_format = cursor.blockFormat();
+
+ QTextBlockFormat preformatted_format = standard_format;
+ preformatted_format.setNonBreakableLines(true);
+
+ QTextBlockFormat block_quote_format = standard_format;
+ block_quote_format.setIndent(1);
+ block_quote_format.setBackground(themed_style.blockquote_color);
+
+
+ bool verbatim = false;
+ QTextList *current_list = nullptr;
+ bool blockquote = false;
+
+ outline.beginBuild();
+
+ int anchor_id = 0;
+
+ auto unique_anchor_name = [&]() -> QString {
+ return QString("auto-title-%1").arg(++anchor_id);
+ };
+
+ QList<QByteArray> lines = input.split('\n');
+ for (auto const &line : lines)
+ {
+ if (verbatim)
+ {
+ if (line.startsWith("```"))
+ {
+ cursor.setBlockFormat(standard_format);
+ verbatim = false;
+ }
+ else
+ {
+ cursor.setBlockFormat(preformatted_format);
+ cursor.setCharFormat(preformatted);
+ cursor.insertText(line + "\n");
+ }
+ }
+ else
+ {
+ if (line.startsWith("* "))
+ {
+ if (current_list == nullptr)
+ {
+ cursor.deletePreviousChar();
+ current_list = cursor.insertList(QTextListFormat::ListDisc);
+ }
+ else
+ {
+ cursor.insertBlock();
+ }
+
+ QString item = trim_whitespace(line.mid(1));
+
+ cursor.insertText(item, standard);
+ continue;
+ }
+ else
+ {
+ if (current_list != nullptr)
+ {
+ cursor.insertBlock();
+ cursor.setBlockFormat(standard_format);
+ }
+ current_list = nullptr;
+ }
+
+ if(line.startsWith(">"))
+ {
+ if(not blockquote ) {
+ // cursor.insertBlock();
+ }
+ blockquote = true;
+
+ cursor.setBlockFormat(block_quote_format);
+ cursor.insertText(trim_whitespace(line.mid(1)) + "\n", standard);
+
+ continue;
+ }
+ else
+ {
+ if(blockquote) {
+ cursor.setBlockFormat(standard_format);
+ }
+ blockquote = false;
+ }
+
+ if (line.startsWith("###"))
+ {
+ auto heading = trim_whitespace(line.mid(3));
+
+ auto id = unique_anchor_name();
+ auto fmt = standard_h3;
+ fmt.setAnchor(true);
+ fmt.setAnchorNames(QStringList { id });
+
+ cursor.insertText(heading + "\n", fmt);
+ outline.appendH3(heading, id);
+ }
+ else if (line.startsWith("##"))
+ {
+ auto heading = trim_whitespace(line.mid(2));
+
+ auto id = unique_anchor_name();
+ auto fmt = standard_h2;
+ fmt.setAnchor(true);
+ fmt.setAnchorNames(QStringList { id });
+
+ cursor.insertText(heading + "\n", fmt);
+ outline.appendH2(heading, id);
+ }
+ else if (line.startsWith("#"))
+ {
+ auto heading = trim_whitespace(line.mid(1));
+
+ auto id = unique_anchor_name();
+ auto fmt = standard_h1;
+ fmt.setAnchor(true);
+ fmt.setAnchorNames(QStringList { id });
+
+ cursor.insertText(heading + "\n", fmt);
+ outline.appendH1(heading, id);
+ }
+ else if (line.startsWith("=>"))
+ {
+ auto const part = line.mid(2).trimmed();
+
+ QByteArray link, title;
+
+ int index = -1;
+ for (int i = 0; i < part.size(); i++)
+ {
+ if (isspace(part[i]))
+ {
+ index = i;
+ break;
+ }
+ }
+
+ if (index > 0)
+ {
+ link = trim_whitespace(part.mid(0, index));
+ title = trim_whitespace(part.mid(index + 1));
+ }
+ else
+ {
+ link = trim_whitespace(part);
+ title = trim_whitespace(part);
+ }
+
+ auto local_url = QUrl(link);
+
+ auto absolute_url = root_url.resolved(QUrl(link));
+
+ // qDebug() << link << title;
+
+ auto fmt = standard_link;
+
+ QString prefix;
+ if (absolute_url.host() == root_url.host())
+ {
+ prefix = themed_style.internal_link_prefix;
+ fmt = standard_link;
+ }
+ else
+ {
+ prefix = themed_style.external_link_prefix;
+ fmt = external_link;
+ }
+
+ QString suffix = "";
+ if (absolute_url.scheme() != root_url.scheme())
+ {
+ if(absolute_url.scheme() != "kristall+ctrl") {
+ suffix = " [" + absolute_url.scheme().toUpper() + "]";
+ fmt = cross_protocol_link;
+ }
+ }
+
+ fmt.setAnchor(true);
+ fmt.setAnchorHref(absolute_url.toString());
+ cursor.insertText(prefix + title + suffix + "\n", fmt);
+ }
+ else if (line.startsWith("```"))
+ {
+ verbatim = true;
+ }
+ else
+ {
+ if(emit_fancy_text)
+ {
+ // TODO: Fix UTF-8 encoding here… Don't emit single characters but always spans!
+
+ bool rendering_bold = false;
+ bool rendering_underlined = false;
+
+ QTextCharFormat fmt = standard;
+
+ QByteArray buffer;
+
+ auto flush = [&]() {
+ if(buffer.size() > 0) {
+ cursor.insertText(QString::fromUtf8(buffer), fmt);
+ buffer.resize(0);
+ }
+ };
+
+ for(int i = 0; i < line.length(); i += 1)
+ {
+ char c = line.at(i);
+ if(c == ' ') {
+ flush();
+ fmt = standard;
+ buffer.append(' ');
+ rendering_bold = false;
+ rendering_underlined = false;
+ }
+ else if(c == '*') {
+ if(rendering_bold) {
+ buffer.append('*');
+ }
+ flush();
+ rendering_bold = not rendering_bold;
+ auto f = fmt.font();
+ f.setBold(rendering_bold);
+ fmt.setFont(f);
+ if(rendering_bold) {
+ buffer.append('*');
+ }
+ }
+ else if(c == '_') {
+ if(rendering_underlined) {
+ buffer.append(' ');
+ }
+ flush();
+ rendering_underlined = not rendering_underlined;
+ auto f = fmt.font();
+ fmt.setUnderlineStyle(rendering_underlined ? QTextCharFormat::SingleUnderline : QTextCharFormat::NoUnderline);
+ if(rendering_underlined) {
+ buffer.append(' ');
+ }
+ }
+ else {
+ buffer.append(c);
+ }
+ }
+
+ flush();
+
+ cursor.insertText("\n", standard);
+ }
+ else {
+ cursor.insertText(line + "\n", standard);
+ }
+ }
+ }
+ }
+
+ outline.endBuild();
+ return result;
+}
+
+GeminiDocument::GeminiDocument(QObject *parent) : QTextDocument(parent),
+ background_color(0x00, 0x00, 0x00)
+{
+}
+
+GeminiDocument::~GeminiDocument()
+{
+}
diff --git a/src/renderers/geminirenderer.hpp b/src/renderers/geminirenderer.hpp
new file mode 100644
index 0000000..7173d50
--- /dev/null
+++ b/src/renderers/geminirenderer.hpp
@@ -0,0 +1,41 @@
+#ifndef GEMINIRENDERER_HPP
+#define GEMINIRENDERER_HPP
+
+#include <memory>
+#include <QTextDocument>
+#include <QColor>
+#include <QSettings>
+
+#include "documentoutlinemodel.hpp"
+
+#include "documentstyle.hpp"
+
+class GeminiDocument :
+ public QTextDocument
+{
+ Q_OBJECT
+public:
+ explicit GeminiDocument(QObject * parent = nullptr);
+ ~GeminiDocument() override;
+
+ QColor background_color;
+};
+
+struct GeminiRenderer
+{
+ GeminiRenderer() = delete;
+
+ //! Renders the given byte sequence into a GeminiDocument.
+ //! @param input The utf8 encoded input string
+ //! @param root_url The url that is used to resolve relative links
+ //! @param style The style which is used to render the document
+ //! @param outline The extracted outline from the document
+ static std::unique_ptr<GeminiDocument> render(
+ QByteArray const & input,
+ QUrl const & root_url,
+ DocumentStyle const & style,
+ DocumentOutlineModel & outline
+ );
+};
+
+#endif // GEMINIRENDERER_HPP
diff --git a/src/renderers/gophermaprenderer.cpp b/src/renderers/gophermaprenderer.cpp
new file mode 100644
index 0000000..6779a9a
--- /dev/null
+++ b/src/renderers/gophermaprenderer.cpp
@@ -0,0 +1,192 @@
+#include "gophermaprenderer.hpp"
+#include <cassert>
+#include <QTextList>
+#include <QTextBlock>
+#include <QList>
+#include <QStringList>
+#include <QTextImageFormat>
+
+#include <QDebug>
+#include <QImage>
+
+#include "kristall.hpp"
+
+
+std::unique_ptr<QTextDocument> GophermapRenderer::render(const QByteArray &input, const QUrl &root_url, const DocumentStyle &themed_style)
+{
+ QTextCharFormat standard;
+ standard.setFont(themed_style.preformatted_font);
+ standard.setForeground(themed_style.preformatted_color);
+
+ QTextCharFormat standard_link;
+ standard_link.setFont(themed_style.preformatted_font);
+ standard_link.setForeground(QBrush(themed_style.internal_link_color));
+
+ QTextCharFormat external_link;
+ external_link.setFont(themed_style.standard_font);
+ external_link.setForeground(QBrush(themed_style.external_link_color));
+
+ bool emit_text_only = (global_options.gophermap_display == GenericSettings::PlainText);
+
+ std::unique_ptr<QTextDocument> result = std::make_unique<QTextDocument>();
+ result->setDocumentMargin(themed_style.margin);
+
+ if(not emit_text_only)
+ {
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/binary"), QVariant::fromValue(QImage(":/icons/gopher/binary.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/directory"), QVariant::fromValue(QImage(":/icons/gopher/directory.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/dns"), QVariant::fromValue(QImage(":/icons/gopher/dns.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/error"), QVariant::fromValue(QImage(":/icons/gopher/error.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/gif"), QVariant::fromValue(QImage(":/icons/gopher/gif.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/html"), QVariant::fromValue(QImage(":/icons/gopher/html.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/image"), QVariant::fromValue(QImage(":/icons/gopher/image.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/mirror"), QVariant::fromValue(QImage(":/icons/gopher/mirror.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/search"), QVariant::fromValue(QImage(":/icons/gopher/search.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/sound"), QVariant::fromValue(QImage(":/icons/gopher/sound.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/telnet"), QVariant::fromValue(QImage(":/icons/gopher/telnet.svg")));
+ result->addResource(QTextDocument::ImageResource, QUrl("gopher/text"), QVariant::fromValue(QImage(":/icons/gopher/text.svg")));
+ }
+
+ QTextCursor cursor{result.get()};
+
+ QTextBlockFormat non_list_format = cursor.blockFormat();
+
+ char last_type = '1';
+
+ QList<QByteArray> lines = input.split('\n');
+ for (auto const &line : lines)
+ {
+ if (line.length() < 2) // skip lines without
+ continue;
+
+ if (line[line.size() - 1] != '\r')
+ continue;
+
+ auto items = line.mid(1, line.length() - 2).split('\t');
+ if (items.size() < 2) // invalid
+ continue;
+
+ QString icon;
+ QString scheme = "gopher";
+
+ auto type = line.at(0);
+ switch (type)
+ {
+ case '0': // Text File
+ icon = "text";
+ break;
+ case '1': // Gopher submenu or link to another gopher server
+ icon = "directory";
+ break;
+ case '2': // CCSO Nameserver
+ icon = "dns";
+ break;
+ case '3': // Error code returned by a Gopher server to indicate failure
+ icon = "error";
+ break;
+ case '4': // BinHex-encoded file (primarily for Macintosh computers)
+ icon = "binary";
+ break;
+ case '5': // DOS file
+ icon = "binary";
+ break;
+ case '6': // uuencoded file
+ icon = "binary";
+ break;
+ case '7': // Gopher full-text search
+ icon = "search";
+ break;
+ case '8': // Telnet
+ icon = "telnet";
+ scheme = "telnet";
+ break;
+ case '9': // Binary file
+ icon = "binary";
+ break;
+ case '+': // Mirror or alternate server (for load balancing or in case of primary server downtime)
+ icon = "mirror";
+ break;
+ case 'g': // GIF file
+ icon = "gif";
+ break;
+ case 'I': // Image file
+ icon = "image";
+ break;
+ case 'T': // Telnet 3270
+ icon = "telnet";
+ scheme = "telnet";
+ break;
+ //Non-Canonical Types
+ case 'h': // HTML file
+ icon = "html";
+ break;
+ case 'i': // Informational message
+ icon = "informational";
+ break;
+ case 's': // Sound file
+ icon = "sound";
+ break;
+ default: // unknown
+ continue;
+ }
+ if(type == '+') {
+ type = last_type;
+ } else {
+ last_type = type;
+ }
+
+ QString title = items.at(0);
+
+ if (type == 'i')
+ {
+ cursor.insertText(title + "\n", standard);
+ }
+ else
+ {
+ QString dst_url;
+ switch (items.size())
+ {
+ case 0:
+ assert(false);
+ case 1:
+ assert(false);
+ case 2:
+ dst_url = root_url.resolved(QUrl(items.at(1))).toString();
+ break;
+ case 3:
+ dst_url = scheme + "://" + items.at(2) + "/" + QString(type) + items.at(1);
+ break;
+ default:
+ dst_url = scheme + "://" + items.at(2) + ":" + items.at(3) + "/" + QString(type) + items.at(1);
+ break;
+ }
+
+ if (not QUrl(dst_url).isValid())
+ {
+ // invlaid URL generated
+ qDebug() << line << dst_url;
+ }
+
+ if(emit_text_only)
+ {
+ cursor.insertText("[" + icon + "] ", standard);
+ }
+ else
+ {
+ QTextImageFormat icon_fmt;
+ icon_fmt.setName(QString("gopher/%1").arg(icon));
+ icon_fmt.setVerticalAlignment(QTextImageFormat::AlignTop);
+
+ cursor.insertImage(icon_fmt);
+ cursor.insertText(" ");
+ }
+
+ QTextCharFormat fmt = standard_link;
+ fmt.setAnchor(true);
+ fmt.setAnchorHref(dst_url);
+ cursor.insertText(title + "\n", fmt);
+ }
+ }
+
+ return result;
+}
diff --git a/src/renderers/gophermaprenderer.hpp b/src/renderers/gophermaprenderer.hpp
new file mode 100644
index 0000000..3835cec
--- /dev/null
+++ b/src/renderers/gophermaprenderer.hpp
@@ -0,0 +1,26 @@
+#ifndef GOPHERMAPRENDERER_HPP
+#define GOPHERMAPRENDERER_HPP
+
+#include "documentstyle.hpp"
+
+#include <memory>
+#include <QTextDocument>
+
+struct GophermapRenderer
+{
+ GophermapRenderer() = delete;
+
+
+ //! Renders the given byte sequence into a GeminiDocument.
+ //! @param input The utf8 encoded input string
+ //! @param root_url The url that is used to resolve relative links
+ //! @param style The style which is used to render the document
+ //! @param outline The extracted outline from the document
+ static std::unique_ptr<QTextDocument> render(
+ QByteArray const & input,
+ QUrl const & root_url,
+ DocumentStyle const & style
+ );
+};
+
+#endif // GOPHERMAPRENDERER_HPP
diff --git a/src/renderers/plaintextrenderer.cpp b/src/renderers/plaintextrenderer.cpp
new file mode 100644
index 0000000..37801a4
--- /dev/null
+++ b/src/renderers/plaintextrenderer.cpp
@@ -0,0 +1,19 @@
+#include "plaintextrenderer.hpp"
+
+#include <QTextImageFormat>
+#include <QTextCursor>
+
+std::unique_ptr<QTextDocument> PlainTextRenderer::render(const QByteArray &input, const DocumentStyle &style)
+{
+ QTextCharFormat standard;
+ standard.setFont(style.preformatted_font);
+ standard.setForeground(style.preformatted_color);
+
+ std::unique_ptr<QTextDocument> result = std::make_unique<QTextDocument>();
+ result->setDocumentMargin(style.margin);
+
+ QTextCursor cursor { result.get() };
+ cursor.insertText(QString::fromUtf8(input), standard);
+
+ return result;
+}
diff --git a/src/renderers/plaintextrenderer.hpp b/src/renderers/plaintextrenderer.hpp
new file mode 100644
index 0000000..cfa2ccb
--- /dev/null
+++ b/src/renderers/plaintextrenderer.hpp
@@ -0,0 +1,25 @@
+#ifndef PLAINTEXTRENDERER_HPP
+#define PLAINTEXTRENDERER_HPP
+
+#include "documentstyle.hpp"
+
+#include <memory>
+#include <QTextDocument>
+
+struct PlainTextRenderer
+{
+ PlainTextRenderer() = delete;
+
+
+ //! Renders the given byte sequence into a GeminiDocument.
+ //! @param input The utf8 encoded input string
+ //! @param root_url The url that is used to resolve relative links
+ //! @param style The style which is used to render the document
+ //! @param outline The extracted outline from the document
+ static std::unique_ptr<QTextDocument> render(
+ QByteArray const & input,
+ DocumentStyle const & style
+ );
+};
+
+#endif // PLAINTEXTRENDERER_HPP