Serenity Operating System
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}