diff options
author | Andreas Kling <kling@serenityos.org> | 2020-04-28 21:04:25 +0200 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-04-28 21:04:25 +0200 |
commit | ea204ef05b99b65f7d7630936e891b595012f9f5 (patch) | |
tree | 54899bb6aabe6c71fae2cace16e7bff7c3c0899a /Libraries/LibMarkdown/Text.cpp | |
parent | 104969a9f5c4eaacbdc89b548729a8edeada5fff (diff) | |
download | serenity-ea204ef05b99b65f7d7630936e891b595012f9f5.zip |
LibMarkdown: Drop MD prefixes and move into "Markdown" namespace :^)
Diffstat (limited to 'Libraries/LibMarkdown/Text.cpp')
-rw-r--r-- | Libraries/LibMarkdown/Text.cpp | 235 |
1 files changed, 235 insertions, 0 deletions
diff --git a/Libraries/LibMarkdown/Text.cpp b/Libraries/LibMarkdown/Text.cpp new file mode 100644 index 0000000000..6bfe89d69c --- /dev/null +++ b/Libraries/LibMarkdown/Text.cpp @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include <AK/StringBuilder.h> +#include <LibMarkdown/Text.h> +#include <string.h> + +namespace Markdown { + +static String unescape(const StringView& text) +{ + StringBuilder builder; + for (size_t i = 0; i < text.length(); ++i) { + if (text[i] == '\\' && i != text.length() - 1) { + builder.append(text[i + 1]); + i++; + continue; + } + builder.append(text[i]); + } + return builder.build(); +} + +String Text::render_to_html() const +{ + StringBuilder builder; + + Vector<String> open_tags; + Style current_style; + + for (auto& span : m_spans) { + struct TagAndFlag { + String tag; + bool Style::*flag; + }; + TagAndFlag tags_and_flags[] = { + { "i", &Style::emph }, + { "b", &Style::strong }, + { "code", &Style::code } + }; + auto it = open_tags.find([&](const String& open_tag) { + if (open_tag == "a" && current_style.href != span.style.href) + return true; + for (auto& tag_and_flag : tags_and_flags) { + if (open_tag == tag_and_flag.tag && !(span.style.*tag_and_flag.flag)) + return true; + } + return false; + }); + + if (!it.is_end()) { + // We found an open tag that should + // not be open for the new span. Close + // it and all the open tags that follow + // it. + for (ssize_t j = open_tags.size() - 1; j >= static_cast<ssize_t>(it.index()); --j) { + auto& tag = open_tags[j]; + builder.appendf("</%s>", tag.characters()); + if (tag == "a") { + current_style.href = {}; + continue; + } + for (auto& tag_and_flag : tags_and_flags) + if (tag == tag_and_flag.tag) + current_style.*tag_and_flag.flag = false; + } + open_tags.shrink(it.index()); + } + if (current_style.href.is_null() && !span.style.href.is_null()) { + open_tags.append("a"); + builder.appendf("<a href=\"%s\">", span.style.href.characters()); + } + for (auto& tag_and_flag : tags_and_flags) { + if (current_style.*tag_and_flag.flag != span.style.*tag_and_flag.flag) { + open_tags.append(tag_and_flag.tag); + builder.appendf("<%s>", tag_and_flag.tag.characters()); + } + } + + current_style = span.style; + builder.append(span.text); + } + + for (ssize_t i = open_tags.size() - 1; i >= 0; --i) { + auto& tag = open_tags[i]; + builder.appendf("</%s>", tag.characters()); + } + + return builder.build(); +} + +String Text::render_for_terminal() const +{ + StringBuilder builder; + + for (auto& span : m_spans) { + bool needs_styling = span.style.strong || span.style.emph || span.style.code; + if (needs_styling) { + builder.append("\033["); + bool first = true; + if (span.style.strong || span.style.code) { + builder.append('1'); + first = false; + } + if (span.style.emph) { + if (!first) + builder.append(';'); + builder.append('4'); + } + builder.append('m'); + } + + builder.append(span.text.characters()); + + if (needs_styling) + builder.append("\033[0m"); + + if (!span.style.href.is_null()) { + // When rendering for the terminal, ignore any + // non-absolute links, because the user has no + // chance to follow them anyway. + if (strstr(span.style.href.characters(), "://") != nullptr) { + builder.appendf(" <%s>", span.style.href.characters()); + } + } + } + + return builder.build(); +} + +bool Text::parse(const StringView& str) +{ + Style current_style; + size_t current_span_start = 0; + int first_span_in_the_current_link = -1; + + auto append_span_if_needed = [&](size_t offset) { + if (current_span_start != offset) { + Span span { + unescape(str.substring_view(current_span_start, offset - current_span_start)), + current_style + }; + m_spans.append(move(span)); + } + }; + + for (size_t offset = 0; offset < str.length(); offset++) { + char ch = str[offset]; + + bool is_escape = ch == '\\'; + if (is_escape && offset != str.length() - 1) { + offset++; + continue; + } + + bool is_special_character = false; + is_special_character |= ch == '`'; + if (!current_style.code) + is_special_character |= ch == '*' || ch == '_' || ch == '[' || ch == ']'; + if (!is_special_character) + continue; + + append_span_if_needed(offset); + + switch (ch) { + case '`': + current_style.code = !current_style.code; + break; + case '*': + case '_': + if (offset + 1 < str.length() && str[offset + 1] == ch) { + offset++; + current_style.strong = !current_style.strong; + } else { + current_style.emph = !current_style.emph; + } + break; + case '[': + ASSERT(first_span_in_the_current_link == -1); + first_span_in_the_current_link = m_spans.size(); + break; + case ']': { + ASSERT(first_span_in_the_current_link != -1); + ASSERT(offset + 2 < str.length()); + offset++; + ASSERT(str[offset] == '('); + offset++; + size_t start_of_href = offset; + + do + offset++; + while (offset < str.length() && str[offset] != ')'); + + const StringView href = str.substring_view(start_of_href, offset - start_of_href); + for (size_t i = first_span_in_the_current_link; i < m_spans.size(); i++) + m_spans[i].style.href = href; + first_span_in_the_current_link = -1; + break; + } + default: + ASSERT_NOT_REACHED(); + } + + current_span_start = offset + 1; + } + + append_span_if_needed(str.length()); + + return true; +} + +} |