Serenity Operating System
at portability 230 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 30static String unescape(const StringView& text) 31{ 32 StringBuilder builder; 33 for (size_t i = 0; i < text.length(); ++i) { 34 if (text[i] == '\\' && i != text.length() - 1) { 35 builder.append(text[i + 1]); 36 i++; 37 continue; 38 } 39 builder.append(text[i]); 40 } 41 return builder.build(); 42} 43 44String MDText::render_to_html() const 45{ 46 StringBuilder builder; 47 48 Vector<String> open_tags; 49 Style current_style; 50 51 for (auto& span : m_spans) { 52 struct TagAndFlag { 53 String tag; 54 bool Style::*flag; 55 }; 56 TagAndFlag tags_and_flags[] = { 57 { "i", &Style::emph }, 58 { "b", &Style::strong }, 59 { "code", &Style::code } 60 }; 61 auto it = open_tags.find([&](const String& open_tag) { 62 if (open_tag == "a" && current_style.href != span.style.href) 63 return true; 64 for (auto& tag_and_flag : tags_and_flags) { 65 if (open_tag == tag_and_flag.tag && !(span.style.*tag_and_flag.flag)) 66 return true; 67 } 68 return false; 69 }); 70 71 if (!it.is_end()) { 72 // We found an open tag that should 73 // not be open for the new span. Close 74 // it and all the open tags that follow 75 // it. 76 for (auto it2 = --open_tags.end(); it2 >= it; --it2) { 77 const String& tag = *it2; 78 builder.appendf("</%s>", tag.characters()); 79 if (tag == "a") { 80 current_style.href = {}; 81 continue; 82 } 83 for (auto& tag_and_flag : tags_and_flags) 84 if (tag == tag_and_flag.tag) 85 current_style.*tag_and_flag.flag = false; 86 } 87 open_tags.shrink(it.index()); 88 } 89 if (current_style.href.is_null() && !span.style.href.is_null()) { 90 open_tags.append("a"); 91 builder.appendf("<a href=\"%s\">", span.style.href.characters()); 92 } 93 for (auto& tag_and_flag : tags_and_flags) { 94 if (current_style.*tag_and_flag.flag != span.style.*tag_and_flag.flag) { 95 open_tags.append(tag_and_flag.tag); 96 builder.appendf("<%s>", tag_and_flag.tag.characters()); 97 } 98 } 99 100 current_style = span.style; 101 builder.append(span.text); 102 } 103 104 for (auto it = --open_tags.end(); it >= open_tags.begin(); --it) { 105 const String& tag = *it; 106 builder.appendf("</%s>", tag.characters()); 107 } 108 109 return builder.build(); 110} 111 112String MDText::render_for_terminal() const 113{ 114 StringBuilder builder; 115 116 for (auto& span : m_spans) { 117 bool needs_styling = span.style.strong || span.style.emph || span.style.code; 118 if (needs_styling) { 119 builder.append("\033["); 120 bool first = true; 121 if (span.style.strong || span.style.code) { 122 builder.append('1'); 123 first = false; 124 } 125 if (span.style.emph) { 126 if (!first) 127 builder.append(';'); 128 builder.append('4'); 129 } 130 builder.append('m'); 131 } 132 133 builder.append(span.text.characters()); 134 135 if (needs_styling) 136 builder.append("\033[0m"); 137 138 if (!span.style.href.is_null()) { 139 // When rendering for the terminal, ignore any 140 // non-absolute links, because the user has no 141 // chance to follow them anyway. 142 if (strstr(span.style.href.characters(), "://") != nullptr) { 143 builder.appendf(" <%s>", span.style.href.characters()); 144 } 145 } 146 } 147 148 return builder.build(); 149} 150 151bool MDText::parse(const StringView& str) 152{ 153 Style current_style; 154 size_t current_span_start = 0; 155 int first_span_in_the_current_link = -1; 156 157 auto append_span_if_needed = [&](size_t offset) { 158 if (current_span_start != offset) { 159 Span span { 160 unescape(str.substring_view(current_span_start, offset - current_span_start)), 161 current_style 162 }; 163 m_spans.append(move(span)); 164 } 165 }; 166 167 for (size_t offset = 0; offset < str.length(); offset++) { 168 char ch = str[offset]; 169 170 bool is_escape = ch == '\\'; 171 if (is_escape && offset != str.length() - 1) { 172 offset++; 173 continue; 174 } 175 176 bool is_special_character = false; 177 is_special_character |= ch == '`'; 178 if (!current_style.code) 179 is_special_character |= ch == '*' || ch == '_' || ch == '[' || ch == ']'; 180 if (!is_special_character) 181 continue; 182 183 append_span_if_needed(offset); 184 185 switch (ch) { 186 case '`': 187 current_style.code = !current_style.code; 188 break; 189 case '*': 190 case '_': 191 if (offset + 1 < str.length() && str[offset + 1] == ch) { 192 offset++; 193 current_style.strong = !current_style.strong; 194 } else { 195 current_style.emph = !current_style.emph; 196 } 197 break; 198 case '[': 199 ASSERT(first_span_in_the_current_link == -1); 200 first_span_in_the_current_link = m_spans.size(); 201 break; 202 case ']': { 203 ASSERT(first_span_in_the_current_link != -1); 204 ASSERT(offset + 2 < str.length()); 205 offset++; 206 ASSERT(str[offset] == '('); 207 offset++; 208 size_t start_of_href = offset; 209 210 do 211 offset++; 212 while (offset < str.length() && str[offset] != ')'); 213 214 const StringView href = str.substring_view(start_of_href, offset - start_of_href); 215 for (size_t i = first_span_in_the_current_link; i < m_spans.size(); i++) 216 m_spans[i].style.href = href; 217 first_span_in_the_current_link = -1; 218 break; 219 } 220 default: 221 ASSERT_NOT_REACHED(); 222 } 223 224 current_span_start = offset + 1; 225 } 226 227 append_span_if_needed(str.length()); 228 229 return true; 230}