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#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}