summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2023-01-28 08:49:29 +0100
committerNicolas Werner <nicolas.werner@hotmail.de>2023-01-28 08:51:33 +0100
commit60b1a595e121e6d824cab188abac750be4459058 (patch)
treed3c366c9fe4c090b819d37c2bc1d337b2e53fb41 /src
parentTranslated using Weblate (Ukrainian) (diff)
downloadnheko-60b1a595e121e6d824cab188abac750be4459058.tar.xz
Support simplified spoiler input using || tags
relates to #1231
Diffstat (limited to 'src')
-rw-r--r--src/Utils.cpp279
1 files changed, 211 insertions, 68 deletions
diff --git a/src/Utils.cpp b/src/Utils.cpp
index 7830cc3c..3a989215 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -568,86 +568,229 @@ utils::escapeBlacklistedHtml(const QString &rawStr)
     return QString::fromUtf8(buffer);
 }
 
-QString
-utils::markdownToHtml(const QString &text, bool rainbowify)
+static void
+rainbowify(cmark_node *node)
 {
-    const auto str         = text.toUtf8();
-    cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);
+    // create iterator over node
+    cmark_iter *iter = cmark_iter_new(node);
+
+    // First loop to get total text length
+    int textLen = 0;
+    while (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);
 
-    if (rainbowify) {
-        // create iterator over node
-        cmark_iter *iter = cmark_iter_new(node);
+    // Second loop to rainbowify
+    int charIdx = 0;
+    while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
+        cmark_node *cur = cmark_iter_get_node(iter);
+        // only text nodes (no code or similar)
+        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
+            continue;
 
-        // First loop to get total text length
-        int textLen = 0;
-        while (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)
+        // 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 over graphemes
+        QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText);
+        while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
+            charIdx++;
+            // Split text to get current char
+            auto curChar  = QStringView(nodeText).mid(boundaryStart, boundaryEnd - boundaryStart);
+            boundaryStart = boundaryEnd;
+            // Don't rainbowify whitespaces
+            if (curChar.trimmed().isEmpty() || utils::codepointIsEmoji(curChar.toUcs4().at(0))) {
+                buf.append(curChar);
                 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++;
+            }
+
+            // get correct color for char index
+            // Use colors as described here:
+            // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
+            auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
+            // format color for HTML
+            auto colorString = color.name(QColor::NameFormat::HexRgb);
+            // create HTML element for current char
+            auto curCharColored =
+              QStringLiteral("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar);
+            // append colored HTML element to buffer
+            buf.append(curCharColored);
         }
 
-        // create new iter to start over
-        cmark_iter_free(iter);
-        iter = cmark_iter_new(node);
+        // 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);
+    }
 
-        // Second loop to rainbowify
-        int charIdx = 0;
-        while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
-            cmark_node *cur = cmark_iter_get_node(iter);
-            // only text nodes (no code or similar)
-            if (cmark_node_get_type(cur) != CMARK_NODE_TEXT)
-                continue;
+    cmark_iter_free(iter);
+}
 
-            // 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 over graphemes
-            QTextBoundaryFinder tbf(QTextBoundaryFinder::BoundaryType::Grapheme, nodeText);
-            while ((boundaryEnd = tbf.toNextBoundary()) != -1) {
-                charIdx++;
-                // Split text to get current char
-                auto curChar =
-                  QStringView(nodeText).mid(boundaryStart, boundaryEnd - boundaryStart);
-                boundaryStart = boundaryEnd;
-                // Don't rainbowify whitespaces
-                if (curChar.trimmed().isEmpty() || codepointIsEmoji(curChar.toUcs4().at(0))) {
-                    buf.append(curChar);
-                    continue;
-                }
+static std::string
+extract_spoiler_warning(std::string &inside_spoiler)
+{
+    std::string spoiler_text;
+    if (auto spoilerTextEnd = inside_spoiler.find("|"); spoilerTextEnd != std::string::npos) {
+        spoiler_text   = inside_spoiler.substr(0, spoilerTextEnd);
+        inside_spoiler = inside_spoiler.substr(spoilerTextEnd + 1);
+    }
+    return QString::fromStdString(spoiler_text).replace('"', "&quot;").toStdString();
+}
 
-                // get correct color for char index
-                // Use colors as described here:
-                // https://shark.comfsm.fm/~dleeling/cis/hsl_rainbow.html
-                auto color = QColor::fromHslF((charIdx - 1.0) / textLen * (5. / 6.), 0.9, 0.5);
-                // format color for HTML
-                auto colorString = color.name(QColor::NameFormat::HexRgb);
-                // create HTML element for current char
-                auto curCharColored =
-                  QStringLiteral("<font color=\"%0\">%1</font>").arg(colorString).arg(curChar);
-                // append colored HTML element to buffer
-                buf.append(curCharColored);
-            }
+// TODO(Nico): Add tests :D
+static void
+process_spoilers(cmark_node *node)
+{
+    auto iter = cmark_iter_new(node);
+
+    while (cmark_iter_next(iter) != CMARK_EVENT_DONE) {
+        cmark_node *cur = cmark_iter_get_node(iter);
 
-            // 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);
+        // only text nodes (no code or similar)
+        if (cmark_node_get_type(cur) != CMARK_NODE_TEXT) {
+            continue;
         }
 
-        cmark_iter_free(iter);
+        std::string_view content = cmark_node_get_literal(cur);
+
+        if (auto posStart = content.find("||"); posStart != std::string::npos) {
+            // we have the start of the spoiler
+            if (auto posEnd = content.find("||", posStart + 2); posEnd != std::string::npos) {
+                // we have the end of the spoiler in the same node
+
+                std::string before_spoiler = std::string(content.substr(0, posStart));
+                std::string inside_spoiler =
+                  std::string(content.substr(posStart + 2, posEnd - 2 - posStart));
+                std::string after_spoiler = std::string(content.substr(posEnd + 2));
+
+                std::string spoiler_text = extract_spoiler_warning(inside_spoiler);
+
+                // create the new nodes
+                auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                cmark_node_set_literal(before_node, before_spoiler.c_str());
+                auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                cmark_node_set_literal(after_node, after_spoiler.c_str());
+
+                auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
+                cmark_node_set_on_enter(
+                  block, ("<span data-mx-spoiler=\"" + spoiler_text + "\">").c_str());
+                cmark_node_set_on_exit(block, "</span>");
+                auto child_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                cmark_node_set_literal(child_node, inside_spoiler.c_str());
+                cmark_node_append_child(block, child_node);
+
+                // insert the new nodes into the tree
+                cmark_node_replace(cur, block);
+                cmark_node_insert_before(block, before_node);
+                cmark_node_insert_after(block, after_node);
+
+                // cleanup the replaced node
+                cmark_node_free(cur);
+
+                // fixup the iterator
+                cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);
+
+            } else {
+                // no end found, but lets try sibling nodes
+                for (auto next = cmark_node_next(cur); next != nullptr;
+                     next      = cmark_node_next(next)) {
+                    // only text nodes again
+                    if (cmark_node_get_type(next) != CMARK_NODE_TEXT)
+                        continue;
+
+                    std::string_view next_content = cmark_node_get_literal(next);
+                    if (auto posEnd = next_content.find("||"); posEnd != std::string_view::npos) {
+                        // We found the end of the spoiler
+                        std::string before_spoiler = std::string(content.substr(0, posStart));
+                        std::string after_spoiler  = std::string(next_content.substr(posEnd + 2));
+
+                        std::string inside_spoiler_start =
+                          std::string(content.substr(posStart + 2));
+                        std::string inside_spoiler_end =
+                          std::string(next_content.substr(0, posEnd));
+
+                        std::string spoiler_text = extract_spoiler_warning(inside_spoiler_start);
+
+                        // save all the nodes inside the spoiler for later
+                        std::vector<cmark_node *> child_nodes;
+                        for (auto kid = cmark_node_next(cur); kid != nullptr && kid != next;
+                             kid      = cmark_node_next(kid)) {
+                            child_nodes.push_back(kid);
+                        }
+
+                        // create the new nodes
+                        auto before_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                        cmark_node_set_literal(before_node, before_spoiler.c_str());
+                        auto after_node = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                        cmark_node_set_literal(after_node, after_spoiler.c_str());
+
+                        auto block = cmark_node_new(cmark_node_type::CMARK_NODE_CUSTOM_INLINE);
+                        cmark_node_set_on_enter(
+                          block, ("<span data-mx-spoiler=\"" + spoiler_text + "\">").c_str());
+                        cmark_node_set_on_exit(block, "</span>");
+
+                        // create the content inside the spoiler by adding the old text at the start
+                        // and the end as well as all the existing children
+                        auto child_node_start = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                        cmark_node_set_literal(child_node_start, inside_spoiler_start.c_str());
+                        cmark_node_append_child(block, child_node_start);
+                        for (auto &child : child_nodes)
+                            cmark_node_append_child(block, child);
+                        auto child_node_end = cmark_node_new(cmark_node_type::CMARK_NODE_TEXT);
+                        cmark_node_set_literal(child_node_end, inside_spoiler_end.c_str());
+                        cmark_node_append_child(block, child_node_end);
+
+                        // insert the new nodes into the tree
+                        cmark_node_replace(cur, block);
+                        cmark_node_insert_before(block, before_node);
+                        cmark_node_insert_after(block, after_node);
+
+                        // cleanup removed nodes
+                        cmark_node_free(cur);
+                        cmark_node_free(next);
+
+                        // fixup the iterator
+                        cmark_iter_reset(iter, block, CMARK_EVENT_EXIT);
+
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    cmark_iter_free(iter);
+}
+
+QString
+utils::markdownToHtml(const QString &text, bool rainbowify_)
+{
+    const auto str         = text.toUtf8();
+    cmark_node *const node = cmark_parse_document(str.constData(), str.size(), CMARK_OPT_UNSAFE);
+
+    process_spoilers(node);
+
+    if (rainbowify_) {
+        rainbowify(node);
     }
 
     const char *tmp_buf = cmark_render_html(node, CMARK_OPT_UNSAFE);