Serenity Operating System
at hosted 231 lines 7.7 kB view raw
1/* 2 * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include <AK/StringBuilder.h> 28#include <LibMarkdown/MDText.h> 29#include <string.h> 30 31static String unescape(const StringView& text) 32{ 33 StringBuilder builder; 34 for (size_t i = 0; i < text.length(); ++i) { 35 if (text[i] == '\\' && i != text.length() - 1) { 36 builder.append(text[i + 1]); 37 i++; 38 continue; 39 } 40 builder.append(text[i]); 41 } 42 return builder.build(); 43} 44 45String MDText::render_to_html() const 46{ 47 StringBuilder builder; 48 49 Vector<String> open_tags; 50 Style current_style; 51 52 for (auto& span : m_spans) { 53 struct TagAndFlag { 54 String tag; 55 bool Style::*flag; 56 }; 57 TagAndFlag tags_and_flags[] = { 58 { "i", &Style::emph }, 59 { "b", &Style::strong }, 60 { "code", &Style::code } 61 }; 62 auto it = open_tags.find([&](const String& open_tag) { 63 if (open_tag == "a" && current_style.href != span.style.href) 64 return true; 65 for (auto& tag_and_flag : tags_and_flags) { 66 if (open_tag == tag_and_flag.tag && !(span.style.*tag_and_flag.flag)) 67 return true; 68 } 69 return false; 70 }); 71 72 if (!it.is_end()) { 73 // We found an open tag that should 74 // not be open for the new span. Close 75 // it and all the open tags that follow 76 // it. 77 for (ssize_t j = open_tags.size() - 1; j >= static_cast<ssize_t>(it.index()); --j) { 78 auto& tag = open_tags[j]; 79 builder.appendf("</%s>", tag.characters()); 80 if (tag == "a") { 81 current_style.href = {}; 82 continue; 83 } 84 for (auto& tag_and_flag : tags_and_flags) 85 if (tag == tag_and_flag.tag) 86 current_style.*tag_and_flag.flag = false; 87 } 88 open_tags.shrink(it.index()); 89 } 90 if (current_style.href.is_null() && !span.style.href.is_null()) { 91 open_tags.append("a"); 92 builder.appendf("<a href=\"%s\">", span.style.href.characters()); 93 } 94 for (auto& tag_and_flag : tags_and_flags) { 95 if (current_style.*tag_and_flag.flag != span.style.*tag_and_flag.flag) { 96 open_tags.append(tag_and_flag.tag); 97 builder.appendf("<%s>", tag_and_flag.tag.characters()); 98 } 99 } 100 101 current_style = span.style; 102 builder.append(span.text); 103 } 104 105 for (ssize_t i = open_tags.size() - 1; i >= 0; --i) { 106 auto& tag = open_tags[i]; 107 builder.appendf("</%s>", tag.characters()); 108 } 109 110 return builder.build(); 111} 112 113String MDText::render_for_terminal() const 114{ 115 StringBuilder builder; 116 117 for (auto& span : m_spans) { 118 bool needs_styling = span.style.strong || span.style.emph || span.style.code; 119 if (needs_styling) { 120 builder.append("\033["); 121 bool first = true; 122 if (span.style.strong || span.style.code) { 123 builder.append('1'); 124 first = false; 125 } 126 if (span.style.emph) { 127 if (!first) 128 builder.append(';'); 129 builder.append('4'); 130 } 131 builder.append('m'); 132 } 133 134 builder.append(span.text.characters()); 135 136 if (needs_styling) 137 builder.append("\033[0m"); 138 139 if (!span.style.href.is_null()) { 140 // When rendering for the terminal, ignore any 141 // non-absolute links, because the user has no 142 // chance to follow them anyway. 143 if (strstr(span.style.href.characters(), "://") != nullptr) { 144 builder.appendf(" <%s>", span.style.href.characters()); 145 } 146 } 147 } 148 149 return builder.build(); 150} 151 152bool MDText::parse(const StringView& str) 153{ 154 Style current_style; 155 size_t current_span_start = 0; 156 int first_span_in_the_current_link = -1; 157 158 auto append_span_if_needed = [&](size_t offset) { 159 if (current_span_start != offset) { 160 Span span { 161 unescape(str.substring_view(current_span_start, offset - current_span_start)), 162 current_style 163 }; 164 m_spans.append(move(span)); 165 } 166 }; 167 168 for (size_t offset = 0; offset < str.length(); offset++) { 169 char ch = str[offset]; 170 171 bool is_escape = ch == '\\'; 172 if (is_escape && offset != str.length() - 1) { 173 offset++; 174 continue; 175 } 176 177 bool is_special_character = false; 178 is_special_character |= ch == '`'; 179 if (!current_style.code) 180 is_special_character |= ch == '*' || ch == '_' || ch == '[' || ch == ']'; 181 if (!is_special_character) 182 continue; 183 184 append_span_if_needed(offset); 185 186 switch (ch) { 187 case '`': 188 current_style.code = !current_style.code; 189 break; 190 case '*': 191 case '_': 192 if (offset + 1 < str.length() && str[offset + 1] == ch) { 193 offset++; 194 current_style.strong = !current_style.strong; 195 } else { 196 current_style.emph = !current_style.emph; 197 } 198 break; 199 case '[': 200 ASSERT(first_span_in_the_current_link == -1); 201 first_span_in_the_current_link = m_spans.size(); 202 break; 203 case ']': { 204 ASSERT(first_span_in_the_current_link != -1); 205 ASSERT(offset + 2 < str.length()); 206 offset++; 207 ASSERT(str[offset] == '('); 208 offset++; 209 size_t start_of_href = offset; 210 211 do 212 offset++; 213 while (offset < str.length() && str[offset] != ')'); 214 215 const StringView href = str.substring_view(start_of_href, offset - start_of_href); 216 for (size_t i = first_span_in_the_current_link; i < m_spans.size(); i++) 217 m_spans[i].style.href = href; 218 first_span_in_the_current_link = -1; 219 break; 220 } 221 default: 222 ASSERT_NOT_REACHED(); 223 } 224 225 current_span_start = offset + 1; 226 } 227 228 append_span_if_needed(str.length()); 229 230 return true; 231}