diff --git a/src/Utils.cpp b/src/Utils.cpp index 8a3b9e4c..32d435f0 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -468,11 +469,92 @@ utils::escapeBlacklistedHtml(const QString &rawStr) } QString -utils::markdownToHtml(const QString &text) +utils::markdownToHtml(const QString &text, bool rainbowify) { - const auto str = text.toUtf8(); - const char *tmp_buf = cmark_markdown_to_html(str.constData(), str.size(), CMARK_OPT_UNSAFE); + const auto str = text.toUtf8(); + cmark_node *const node = + cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE); + if (rainbowify) { + // create iterator over node + cmark_iter *iter = cmark_iter_new(node); + + cmark_event_type ev_type; + + // First loop to get total text length + int textLen = 0; + while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { + cmark_node *cur = cmark_iter_get_node(iter); + // only text nodes (no code or semilar) + if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) + continue; + // count up by length of current node's text + QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, + QString(cmark_node_get_literal(cur))); + while (tbf.toNextBoundary() != -1) + textLen++; + } + + // create new iter to start over + cmark_iter_free(iter); + iter = cmark_iter_new(node); + + // Second loop to rainbowify + int charIdx = 0; + while ((ev_type = cmark_iter_next(iter)) != CMARK_EVENT_DONE) { + cmark_node *cur = cmark_iter_get_node(iter); + // only text nodes (no code or semilar) + if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) + continue; + + // get text in current node + QString nodeText(cmark_node_get_literal(cur)); + // create buffer to append rainbow text to + QString buf; + int boundaryStart = 0; + int boundaryEnd = 0; + // use QTextBoundaryFinder to iterate ofer graphemes + QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, + nodeText); + while ((boundaryEnd = tbf.toNextBoundary()) != -1) { + // Split text to get current char + auto curChar = + nodeText.midRef(boundaryStart, boundaryEnd - boundaryStart); + boundaryStart = boundaryEnd; + // Don't rainbowify whitespaces + if (curChar.trimmed().isEmpty()) { + buf.append(curChar.toString()); + continue; + } + + // get correct color for char index + auto color = QColor::fromHsvF(1.0 / textLen * charIdx, 1.0, 1.0); + // format color for HTML + auto colorString = color.name(QColor::NameFormat::HexRgb); + // create HTML element for current char + auto curCharColored = QString("%1") + .arg(colorString) + .arg(curChar); + // append colored HTML element to buffer + buf.append(curCharColored); + + charIdx++; + } + + // create HTML_INLINE node to prevent HTML from being escaped + auto htmlNode = cmark_node_new(CMARK_NODE_HTML_INLINE); + // set content of HTML node to buffer contents + cmark_node_set_literal(htmlNode, buf.toUtf8().data()); + // replace current node with HTML node + cmark_node_replace(cur, htmlNode); + // free memory of old node + cmark_node_free(cur); + } + + cmark_iter_free(iter); + } + + const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE); // Copy the null terminated output buffer. std::string html(tmp_buf); diff --git a/src/Utils.h b/src/Utils.h index f8ead68c..37c1baba 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -268,7 +268,7 @@ linkifyMessage(const QString &body); //! Convert the input markdown text to html. QString -markdownToHtml(const QString &text); +markdownToHtml(const QString &text, bool rainbowify = false); //! Escape every html tag, that was not whitelisted QString diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp index b45827f6..53296efd 100644 --- a/src/timeline/InputBar.cpp +++ b/src/timeline/InputBar.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -203,7 +204,7 @@ InputBar::send() auto wasEdit = !room->edit().isEmpty(); if (text().startsWith('/')) { - int command_end = text().indexOf(' '); + int command_end = text().indexOf(QRegularExpression("\\s")); if (command_end == -1) command_end = text().size(); auto name = text().mid(1, command_end - 1); @@ -255,7 +256,7 @@ InputBar::openFileSelection() } void -InputBar::message(QString msg, MarkdownOverride useMarkdown) +InputBar::message(QString msg, MarkdownOverride useMarkdown, bool rainbowify) { mtx::events::msg::Text text = {}; text.body = msg.trimmed().toStdString(); @@ -263,7 +264,7 @@ InputBar::message(QString msg, MarkdownOverride useMarkdown) if ((ChatPage::instance()->userSettings()->markdown() && useMarkdown == MarkdownOverride::NOT_SPECIFIED) || useMarkdown == MarkdownOverride::ON) { - text.formatted_body = utils::markdownToHtml(msg).toStdString(); + text.formatted_body = utils::markdownToHtml(msg, rainbowify).toStdString(); // Remove markdown links by completer text.body = msg.trimmed().replace(conf::strings::matrixToMarkdownLink, "\\1").toStdString(); @@ -526,6 +527,8 @@ InputBar::command(QString command, QString args) message(args, MarkdownOverride::ON); } else if (command == "plain") { message(args, MarkdownOverride::OFF); + } else if (command == "rainbow") { + message(args, MarkdownOverride::ON, true); } } diff --git a/src/timeline/InputBar.h b/src/timeline/InputBar.h index acd9e22c..f7a60488 100644 --- a/src/timeline/InputBar.h +++ b/src/timeline/InputBar.h @@ -53,7 +53,9 @@ public slots: void updateState(int selectionStart, int selectionEnd, int cursorPosition, QString text); void openFileSelection(); bool uploading() const { return uploading_; } - void message(QString body, MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED); + void message(QString body, + MarkdownOverride useMarkdown = MarkdownOverride::NOT_SPECIFIED, + bool rainbowify = false); private slots: void startTyping();