diff options
| author | Mike Skec <skec@protonmail.ch> | 2021-03-07 10:50:55 +1100 |
|---|---|---|
| committer | Felix Queißner <felix@ib-queissner.de> | 2021-03-07 03:42:34 +0100 |
| commit | eca5fcc3b70e09052a2ad3087affac30954c0943 (patch) | |
| tree | 9e20724d22c04df7766cf1c2de67fc352d7c2e42 /src/renderers/geminirenderer.cpp | |
| parent | 7f0f312e77041484062d21419de796e991278ba6 (diff) | |
| download | kristall-eca5fcc3b70e09052a2ad3087affac30954c0943.tar.gz | |
GeminiRenderer: restructure code; makes highlighting work on non-paragraphs
Diffstat (limited to 'src/renderers/geminirenderer.cpp')
| -rw-r--r-- | src/renderers/geminirenderer.cpp | 650 |
1 files changed, 328 insertions, 322 deletions
diff --git a/src/renderers/geminirenderer.cpp b/src/renderers/geminirenderer.cpp index 0e62ef3..f81b12a 100644 --- a/src/renderers/geminirenderer.cpp +++ b/src/renderers/geminirenderer.cpp @@ -29,6 +29,7 @@ static QByteArray trim_whitespace(const QByteArray &items) } static QByteArray replace_quotes(QByteArray&); +static void insertText(QTextCursor&, const QByteArray&, const QTextCharFormat&); std::unique_ptr<GeminiDocument> GeminiRenderer::render( const QByteArray &input, @@ -43,8 +44,6 @@ std::unique_ptr<GeminiDocument> GeminiRenderer::render( renderhelpers::setPageMargins(result.get(), themed_style.margin_h, themed_style.margin_v); result->setIndentWidth(themed_style.indent_size); - bool emit_fancy_text = kristall::globals().options.enable_text_decoration; - QTextCursor cursor{result.get()}; bool verbatim = false; @@ -86,370 +85,216 @@ std::unique_ptr<GeminiDocument> GeminiRenderer::render( cursor.setCharFormat(text_style.preformatted); cursor.insertText(line + "\n"); } + + continue; } - else + + // List item + if (line.startsWith("* ")) { - if (line.startsWith("* ")) + if (current_list == nullptr) { - if (current_list == nullptr) - { - cursor.deletePreviousChar(); - cursor.insertBlock(); - cursor.setBlockFormat(text_style.standard_format); - current_list = cursor.createList(text_style.list_format); - } - else - { - cursor.insertBlock(); - } - - replace_quotes(line); - QString item = trim_whitespace(line.mid(1)); - - cursor.insertText(item, text_style.standard); - continue; + cursor.deletePreviousChar(); + cursor.insertBlock(); + cursor.setBlockFormat(text_style.standard_format); + current_list = cursor.createList(text_style.list_format); } else { - if (current_list != nullptr) - { - cursor.insertBlock(); - cursor.setBlockFormat(text_style.standard_format); - } - current_list = nullptr; + cursor.insertBlock(); } - if(line.startsWith(">")) - { - if(!blockquote) - { - // Start blockquote - QTextTable *table = cursor.insertTable(1, 1, text_style.blockquote_tableformat); - cursor.setBlockFormat(text_style.blockquote_format); - QTextTableCell cell = table->cellAt(0, 0); - cell.setFormat(text_style.blockquote); - blockquote = true; - } + replace_quotes(line); + insertText(cursor, trim_whitespace(line.mid(1)), text_style.standard); + continue; + } - replace_quotes(line); - cursor.insertText(trim_whitespace(line.mid(1)) + "\n", text_style.blockquote); + // End of list + if (current_list != nullptr) + { + cursor.insertBlock(); + cursor.setBlockFormat(text_style.standard_format); + } + current_list = nullptr; - continue; - } - else + // Block quote + if(line.startsWith(">")) + { + if(!blockquote) { - if (blockquote) - { - // End blockquote - cursor.deletePreviousChar(); - cursor.movePosition(QTextCursor::NextBlock); - cursor.setBlockFormat(text_style.standard_format); - } - blockquote = false; + // Start blockquote + QTextTable *table = cursor.insertTable(1, 1, text_style.blockquote_tableformat); + cursor.setBlockFormat(text_style.blockquote_format); + QTextTableCell cell = table->cellAt(0, 0); + cell.setFormat(text_style.blockquote); + blockquote = true; } - if (line.startsWith("###")) - { - auto heading = trim_whitespace(line.mid(3)); + replace_quotes(line); + insertText(cursor, trim_whitespace(line.mid(1)), text_style.blockquote); + cursor.insertText("\n", text_style.standard); + continue; + } - auto id = unique_anchor_name(); - auto fmt = text_style.standard_h3; - fmt.setAnchor(true); - fmt.setAnchorNames(QStringList { id }); + // End of blockquote + if (blockquote) + { + cursor.deletePreviousChar(); + cursor.movePosition(QTextCursor::NextBlock); + cursor.setBlockFormat(text_style.standard_format); + } + blockquote = false; - outline.appendH3(heading, id); + // Headings, etc. + if (line.startsWith("###")) + { + auto heading = trim_whitespace(line.mid(3)); - cursor.setBlockFormat(text_style.heading_format); - cursor.insertText(replace_quotes(heading), fmt); - cursor.insertText("\n", text_style.standard); - } - else if (line.startsWith("##")) - { - auto heading = trim_whitespace(line.mid(2)); + auto id = unique_anchor_name(); + auto fmt = text_style.standard_h3; + fmt.setAnchor(true); + fmt.setAnchorNames(QStringList { id }); - auto id = unique_anchor_name(); - auto fmt = text_style.standard_h2; - fmt.setAnchor(true); - fmt.setAnchorNames(QStringList { id }); + outline.appendH3(heading, id); - outline.appendH2(heading, id); + cursor.setBlockFormat(text_style.heading_format); + insertText(cursor, replace_quotes(heading), fmt); + cursor.insertText("\n", text_style.standard); + } + else if (line.startsWith("##")) + { + auto heading = trim_whitespace(line.mid(2)); - cursor.setBlockFormat(text_style.heading_format); - cursor.insertText(replace_quotes(heading), fmt); - cursor.insertText("\n", text_style.standard); - } - else if (line.startsWith("#")) - { - auto heading = trim_whitespace(line.mid(1)); + auto id = unique_anchor_name(); + auto fmt = text_style.standard_h2; + fmt.setAnchor(true); + fmt.setAnchorNames(QStringList { id }); - auto id = unique_anchor_name(); - auto fmt = text_style.standard_h1; - fmt.setAnchor(true); - fmt.setAnchorNames(QStringList { id }); + outline.appendH2(heading, id); - outline.appendH1(heading, id); + cursor.setBlockFormat(text_style.heading_format); + insertText(cursor, replace_quotes(heading), fmt); + cursor.insertText("\n", text_style.standard); + } + else if (line.startsWith("#")) + { + auto heading = trim_whitespace(line.mid(1)); - // Use first heading as the page's title. - if (page_title != nullptr && page_title->isEmpty()) - { - *page_title = heading; - } + auto id = unique_anchor_name(); + auto fmt = text_style.standard_h1; + fmt.setAnchor(true); + fmt.setAnchorNames(QStringList { id }); - // Centre the first heading. We can't use the above code block - // for this because it doesn't get run on every re-render of the page - if (centre_first_h1) - { - auto f = text_style.heading_format; - f.setAlignment(Qt::AlignCenter); - cursor.setBlockFormat(f); - centre_first_h1 = false; - } - else - { - cursor.setBlockFormat(text_style.heading_format); - } + outline.appendH1(heading, id); - cursor.insertText(replace_quotes(heading), fmt); - cursor.insertText("\n", text_style.standard); + // Use first heading as the page's title. + if (page_title != nullptr && page_title->isEmpty()) + { + *page_title = heading; + } + + // Centre the first heading. We can't use the above code block + // for this because it doesn't get run on every re-render of the page + if (centre_first_h1) + { + auto f = text_style.heading_format; + f.setAlignment(Qt::AlignCenter); + cursor.setBlockFormat(f); + centre_first_h1 = false; } - else if (line.startsWith("=>")) + else { - auto const part = line.mid(2).trimmed(); + cursor.setBlockFormat(text_style.heading_format); + } - QByteArray link, title; + insertText(cursor, replace_quotes(heading), fmt); + cursor.insertText("\n", text_style.standard); + } + else if (line.startsWith("=>")) + { + auto const part = line.mid(2).trimmed(); - int index = -1; - for (int i = 0; i < part.size(); i++) - { - if (isspace(part[i])) - { - index = i; - break; - } - } + QByteArray link, title; - if (index > 0) - { - link = trim_whitespace(part.mid(0, index)); - title = trim_whitespace(part.mid(index + 1)); - } - else + int index = -1; + for (int i = 0; i < part.size(); i++) + { + if (isspace(part[i])) { - link = trim_whitespace(part); - title = trim_whitespace(part); + index = i; + break; } - replace_quotes(title); - - auto local_url = QUrl(link); + } - // Makes relative URLs with scheme provided (e.g gemini:///relative) work - // From RFC 1630: "If the scheme parts are different, the whole absolute URI must be given" - // therefor the schemes must be same for this to be allowed. - if (local_url.scheme() == root_url.scheme() && - local_url.authority().isEmpty() && - local_url.scheme() != "about" && - local_url.scheme() != "file") - { - // qDebug() << "Adjusting local url: " << local_url; - local_url = local_url.adjusted(QUrl::RemoveScheme | QUrl::RemoveAuthority); - } - auto absolute_url = root_url.resolved(local_url); + 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); + } + replace_quotes(title); - // qDebug() << link << title; + auto local_url = QUrl(link); - auto fmt = text_style.standard_link; + // Makes relative URLs with scheme provided (e.g gemini:///relative) work + // From RFC 1630: "If the scheme parts are different, the whole absolute URI must be given" + // therefor the schemes must be same for this to be allowed. + if (local_url.scheme() == root_url.scheme() && + local_url.authority().isEmpty() && + local_url.scheme() != "about" && + local_url.scheme() != "file") + { + // qDebug() << "Adjusting local url: " << local_url; + local_url = local_url.adjusted(QUrl::RemoveScheme | QUrl::RemoveAuthority); + } + auto absolute_url = root_url.resolved(local_url); - QString prefix; - if (absolute_url.host() == root_url.host()) - { - prefix = themed_style.internal_link_prefix; - fmt = text_style.standard_link; - } - else - { - prefix = themed_style.external_link_prefix; - fmt = text_style.external_link; - } + // qDebug() << link << title; - QString suffix = ""; - if (absolute_url.scheme() != root_url.scheme()) - { - if(absolute_url.scheme() != "kristall+ctrl") { - suffix = " [" + absolute_url.scheme().toUpper() + "]"; - fmt = text_style.cross_protocol_link; - } - } + auto fmt = text_style.standard_link; - fmt.setAnchor(true); - fmt.setAnchorHref(absolute_url.toString()); - cursor.setBlockFormat(text_style.link_format); - cursor.insertText(prefix + title + suffix + "\n", fmt); - } - else if (line.startsWith("```")) + QString prefix; + if (absolute_url.host() == root_url.host()) { - verbatim = true; + prefix = themed_style.internal_link_prefix; + fmt = text_style.standard_link; } else { - cursor.setBlockFormat(text_style.standard_format); - replace_quotes(line); + prefix = themed_style.external_link_prefix; + fmt = text_style.external_link; + } - if(emit_fancy_text) - { - // Just render lines not containing asterisks/underscores normally. - // This actually helps reduce the small overhead on large pages to - // being almost negligable - if (!line.contains("*") && !line.contains("_")) - { - cursor.insertText(line + "\n", text_style.standard); - continue; - } - - // Easier to work on this as an array of QChars - QString text(line); - - // Whether to hide formatting codes (*, and _). This option - // is mainly here so that the code which strips these is - // more understandable. - static const bool HIDE_FORMATS = true; - - // The first thing we do is convert double-asterisk bolding to single-asterisk. - // This makes it A LOT easier to bold these things. - // - // This is done using this regex. In a simpler, pseudo form, it can be written as: - // (punctuation/whitespace/line-begin)+\*\*(bolded text)\*\*(punctuation/whitespace/EOL) - // Just stare at it a bit and you might figure out how it works... - QRegularExpression BOLD_DBL_REGEX - = QRegularExpression(R"((^|[\s.,!?[\]()\\-])+\*\*([^\*\s]+[^\*]+[^\*\s]+)\*\*($|[\s.,!?[\]()\\-]))"); - text.replace(BOLD_DBL_REGEX, QString(R"(\1*\2*\3)")); - - QTextCharFormat fmt = text_style.standard; - bool bold = false, underline = false; - bool was_bold = false, was_underline = false; - int last = 0; - - // Used to prepare the format before actually drawing the text. - auto format_text = [&bold, &underline, &was_bold, &was_underline, &last, &text, &fmt](int i) -> QString - { - // Makes sure that bold/underline text only gets printed - // if it has a matching * or _. - if (bold && !text.mid(i, text.length() - i).contains("*")) - bold = false; - if (underline && !text.mid(i, text.length() - i).contains("_")) - underline = false; - - // Sets format to bold/underline as necessary. - auto f = fmt.font(); - f.setBold(bold); - fmt.setFont(f); - fmt.setUnderlineStyle(underline ? - QTextCharFormat::SingleUnderline : QTextCharFormat::NoUnderline); - - // Remove formats - QString span = text.mid(last, i - last); - if (HIDE_FORMATS && - span.length() > 1 && - (((bold || was_bold) && span.startsWith("*")) || - ((underline || was_underline) && span.startsWith("_")))) - { - span = span.mid(1, span.length() - 1); - } - - return span; - }; - - for (int i = 0; i < text.length(); ++i) - { - if (text[i] == '*') - { - // Format and insert the text. - cursor.insertText(format_text(i), fmt); - - // 'Toggle' bold state. - if (was_bold) was_bold = false; - if (bold) { - was_bold = true; - bold = false; - } else { - // Only start bold formatting if this looks like bold formatting: - // * Previous char must be either whitespace, nothing - // * Next char must not be: whitespace, comma, full-stop, asterisk, or underscore. - if ((i == 0 || text[i - 1].isSpace()) && - (i + 1) < text.length() && - !text[i + 1].isSpace() && - text[i + 1] != ',' && - text[i + 1] != '.' && - text[i + 1] != '*' && - text[i + 1] != '_') - { - bold = true; - } - } - - last = i; - } - else if (text[i] == '_') - { - // Insert the text - cursor.insertText(format_text(i), fmt); - - // 'Toggle' underline state. - if (was_underline) was_underline = false; - if (underline) { - was_underline = true; - underline = false; - } else { - // Only start underline formatting if it looks like an underline. - // * Previous char must be either whitespace or nothing - // * Next char must not be: whitespace, comma, full-stop, asterisk, or underscore. - if ((i == 0 || text[i - 1].isSpace()) && - (i + 1) < text.length() && - !text[i + 1].isSpace() && - text[i + 1] != ',' && - text[i + 1] != '.' && - text[i + 1] != '*' && - text[i + 1] != '_') - { - underline = true; - } - } - - last = i; - } - - if (i == text.length() - 1) - { - QString span = text.mid(last, i - last + 1); - - // Skip if the span is just an asterisk/underline - if (HIDE_FORMATS && - ((was_bold && span == "*") || - (was_underline && span == "_"))) - { - break; - } - - // Strips previous underline/asterisk - if (HIDE_FORMATS && - span.length() > 1 && - ((was_bold && span.startsWith("*")) || - (was_underline && span.startsWith("_")))) - { - span = span.mid(1, span.length() - 1); - } - - // Draw ending text normally. - cursor.insertText(span, text_style.standard); - break; - } - } - - cursor.insertText("\n", text_style.standard); - } - else { - cursor.insertText(line + "\n", text_style.standard); + QString suffix = ""; + if (absolute_url.scheme() != root_url.scheme()) + { + if(absolute_url.scheme() != "kristall+ctrl") { + suffix = " [" + absolute_url.scheme().toUpper() + "]"; + fmt = text_style.cross_protocol_link; } } + + fmt.setAnchor(true); + fmt.setAnchorHref(absolute_url.toString()); + cursor.setBlockFormat(text_style.link_format); + insertText(cursor, (prefix + title + suffix).toUtf8(), fmt); + cursor.insertText("\n", text_style.standard); + } + else if (line.startsWith("```")) + { + verbatim = true; + } + else + { + cursor.setBlockFormat(text_style.standard_format); + + replace_quotes(line); + insertText(cursor, line, text_style.standard); + cursor.insertText("\n", text_style.standard); } } @@ -537,3 +382,164 @@ static QByteArray replace_quotes(QByteArray &line) return line; } + +/* + * Handles all the fancy text highlighting. + */ +static void insertText(QTextCursor &cursor, const QByteArray &line, + const QTextCharFormat &format) +{ + if (line.isEmpty() || + + // Render text normally if text decoration is disabled. + !kristall::globals().options.enable_text_decoration || + + // Render lines not containing asterisks/underscores normally. + // This actually helps reduce the small overhead on large pages to + // being almost negligable + (!line.contains("*") && !line.contains("_"))) + { + // Empty lines must be in standard format. + cursor.insertText(line, format); + return; + } + + // Easier to work on this as an array of QChars + QString text(line); + + // Whether to hide formatting codes (*, and _). This option + // is mainly here so that the code which strips these is + // more understandable. + static const bool HIDE_FORMATS = true; + + // The first thing we do is convert double-asterisk bolding to single-asterisk. + // This makes it A LOT easier to bold these things. + // + // This is done using this regex. In a simpler, pseudo form, it can be written as: + // (punctuation/whitespace/line-begin)+\*\*(bolded text)\*\*(punctuation/whitespace/EOL) + // Just stare at it a bit and you might figure out how it works... + QRegularExpression BOLD_DBL_REGEX + = QRegularExpression(R"((^|[\s.,!?[\]()\\-])+\*\*([^\*\s]+[^\*]+[^\*\s]+)\*\*($|[\s.,!?[\]()\\-]))"); + text.replace(BOLD_DBL_REGEX, QString(R"(\1*\2*\3)")); + + QTextCharFormat fmt = format; + bool bold = false, underline = false; + bool was_bold = false, was_underline = false; + int last = 0; + + // Used to prepare the format before actually drawing the text. + auto format_text = [&bold, &underline, &was_bold, &was_underline, &last, &text, &fmt](int i) -> QString + { + // Makes sure that bold/underline text only gets printed + // if it has a matching * or _. + if (bold && !text.mid(i, text.length() - i).contains("*")) + bold = false; + if (underline && !text.mid(i, text.length() - i).contains("_")) + underline = false; + + // Sets format to bold/underline as necessary. + auto f = fmt.font(); + f.setBold(bold); + fmt.setFont(f); + fmt.setUnderlineStyle(underline ? + QTextCharFormat::SingleUnderline : QTextCharFormat::NoUnderline); + + // Remove formats + QString span = text.mid(last, i - last); + if (HIDE_FORMATS && + span.length() > 1 && + (((bold || was_bold) && span.startsWith("*")) || + ((underline || was_underline) && span.startsWith("_")))) + { + span = span.mid(1, span.length() - 1); + } + + return span; + }; + + for (int i = 0; i < text.length(); ++i) + { + if (text[i] == '*') + { + // Format and insert the text. + cursor.insertText(format_text(i), fmt); + + // 'Toggle' bold state. + if (was_bold) was_bold = false; + if (bold) { + was_bold = true; + bold = false; + } else { + // Only start bold formatting if this looks like bold formatting: + // * Previous char must be either whitespace, nothing + // * Next char must not be: whitespace, comma, full-stop, asterisk, or underscore. + if ((i == 0 || text[i - 1].isSpace()) && + (i + 1) < text.length() && + !text[i + 1].isSpace() && + text[i + 1] != ',' && + text[i + 1] != '.' && + text[i + 1] != '*' && + text[i + 1] != '_') + { + bold = true; + } + } + + last = i; + } + else if (text[i] == '_') + { + // Insert the text + cursor.insertText(format_text(i), fmt); + + // 'Toggle' underline state. + if (was_underline) was_underline = false; + if (underline) { + was_underline = true; + underline = false; + } else { + // Only start underline formatting if it looks like an underline. + // * Previous char must be either whitespace or nothing + // * Next char must not be: whitespace, comma, full-stop, asterisk, or underscore. + if ((i == 0 || text[i - 1].isSpace()) && + (i + 1) < text.length() && + !text[i + 1].isSpace() && + text[i + 1] != ',' && + text[i + 1] != '.' && + text[i + 1] != '*' && + text[i + 1] != '_') + { + underline = true; + } + } + + last = i; + } + + if (i == text.length() - 1) + { + QString span = text.mid(last, i - last + 1); + + // Skip if the span is just an asterisk/underline + if (HIDE_FORMATS && + ((was_bold && span == "*") || + (was_underline && span == "_"))) + { + break; + } + + // Strips previous underline/asterisk + if (HIDE_FORMATS && + span.length() > 1 && + ((was_bold && span.startsWith("*")) || + (was_underline && span.startsWith("_")))) + { + span = span.mid(1, span.length() - 1); + } + + // Draw ending text normally. + cursor.insertText(span, format); + break; + } + } +} |
