diff options
author | Nicolas Werner <nicolas.werner@hotmail.de> | 2023-01-28 08:49:29 +0100 |
---|---|---|
committer | Nicolas Werner <nicolas.werner@hotmail.de> | 2023-01-28 08:51:33 +0100 |
commit | 60b1a595e121e6d824cab188abac750be4459058 (patch) | |
tree | d3c366c9fe4c090b819d37c2bc1d337b2e53fb41 /src | |
parent | Translated using Weblate (Ukrainian) (diff) | |
download | nheko-60b1a595e121e6d824cab188abac750be4459058.tar.xz |
Support simplified spoiler input using || tags
relates to #1231
Diffstat (limited to 'src')
-rw-r--r-- | src/Utils.cpp | 279 |
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('"', """).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); |