diff options
| author | Felix (xq) Queißner <git@mq32.de> | 2020-06-22 21:10:04 +0200 |
|---|---|---|
| committer | Felix (xq) Queißner <git@mq32.de> | 2020-06-22 21:10:04 +0200 |
| commit | 75ec461eeaa851cb5c53f4cfffc434e3e529ed1d (patch) | |
| tree | 3944737340718ca3675381aa06636045d397e780 /src/renderers | |
| parent | 8dbfb0890560fd1cd698d06fa05ac868c4db8576 (diff) | |
| download | kristall-75ec461eeaa851cb5c53f4cfffc434e3e529ed1d.tar.gz | |
Restructures the project source and cleans up a bit
Diffstat (limited to 'src/renderers')
| -rw-r--r-- | src/renderers/geminirenderer.cpp | 343 | ||||
| -rw-r--r-- | src/renderers/geminirenderer.hpp | 41 | ||||
| -rw-r--r-- | src/renderers/gophermaprenderer.cpp | 192 | ||||
| -rw-r--r-- | src/renderers/gophermaprenderer.hpp | 26 | ||||
| -rw-r--r-- | src/renderers/plaintextrenderer.cpp | 19 | ||||
| -rw-r--r-- | src/renderers/plaintextrenderer.hpp | 25 |
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 |
