/src/serenity/Userland/Libraries/LibMarkdown/Text.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org> |
3 | | * Copyright (c) 2021, Peter Elliott <pelliott@serenityos.org> |
4 | | * |
5 | | * SPDX-License-Identifier: BSD-2-Clause |
6 | | */ |
7 | | |
8 | | #include <AK/ScopeGuard.h> |
9 | | #include <AK/StringBuilder.h> |
10 | | #include <LibMarkdown/Text.h> |
11 | | #include <LibMarkdown/Visitor.h> |
12 | | #include <ctype.h> |
13 | | #include <string.h> |
14 | | |
15 | | namespace Markdown { |
16 | | |
17 | | void Text::EmphasisNode::render_to_html(StringBuilder& builder) const |
18 | 0 | { |
19 | 0 | builder.append((strong) ? "<strong>"sv : "<em>"sv); |
20 | 0 | child->render_to_html(builder); |
21 | 0 | builder.append((strong) ? "</strong>"sv : "</em>"sv); |
22 | 0 | } |
23 | | |
24 | | void Text::EmphasisNode::render_for_terminal(StringBuilder& builder) const |
25 | 0 | { |
26 | 0 | if (strong) { |
27 | 0 | builder.append("\e[1m"sv); |
28 | 0 | child->render_for_terminal(builder); |
29 | 0 | builder.append("\e[22m"sv); |
30 | 0 | } else { |
31 | 0 | builder.append("\e[3m"sv); |
32 | 0 | child->render_for_terminal(builder); |
33 | 0 | builder.append("\e[23m"sv); |
34 | 0 | } |
35 | 0 | } |
36 | | |
37 | | void Text::EmphasisNode::render_for_raw_print(StringBuilder& builder) const |
38 | 0 | { |
39 | 0 | child->render_for_raw_print(builder); |
40 | 0 | } |
41 | | |
42 | | size_t Text::EmphasisNode::terminal_length() const |
43 | 0 | { |
44 | 0 | return child->terminal_length(); |
45 | 0 | } |
46 | | |
47 | | RecursionDecision Text::EmphasisNode::walk(Visitor& visitor) const |
48 | 0 | { |
49 | 0 | RecursionDecision rd = visitor.visit(*this); |
50 | 0 | if (rd != RecursionDecision::Recurse) |
51 | 0 | return rd; |
52 | | |
53 | 0 | return child->walk(visitor); |
54 | 0 | } |
55 | | |
56 | | void Text::CodeNode::render_to_html(StringBuilder& builder) const |
57 | 0 | { |
58 | 0 | builder.append("<code>"sv); |
59 | 0 | code->render_to_html(builder); |
60 | 0 | builder.append("</code>"sv); |
61 | 0 | } |
62 | | |
63 | | void Text::CodeNode::render_for_terminal(StringBuilder& builder) const |
64 | 0 | { |
65 | 0 | builder.append("\e[1m"sv); |
66 | 0 | code->render_for_terminal(builder); |
67 | 0 | builder.append("\e[22m"sv); |
68 | 0 | } |
69 | | |
70 | | void Text::CodeNode::render_for_raw_print(StringBuilder& builder) const |
71 | 0 | { |
72 | 0 | code->render_for_raw_print(builder); |
73 | 0 | } |
74 | | |
75 | | size_t Text::CodeNode::terminal_length() const |
76 | 0 | { |
77 | 0 | return code->terminal_length(); |
78 | 0 | } |
79 | | |
80 | | RecursionDecision Text::CodeNode::walk(Visitor& visitor) const |
81 | 0 | { |
82 | 0 | RecursionDecision rd = visitor.visit(*this); |
83 | 0 | if (rd != RecursionDecision::Recurse) |
84 | 0 | return rd; |
85 | | |
86 | 0 | return code->walk(visitor); |
87 | 0 | } |
88 | | |
89 | | void Text::BreakNode::render_to_html(StringBuilder& builder) const |
90 | 0 | { |
91 | 0 | builder.append("<br />"sv); |
92 | 0 | } |
93 | | |
94 | | void Text::BreakNode::render_for_terminal(StringBuilder&) const |
95 | 0 | { |
96 | 0 | } |
97 | | |
98 | | void Text::BreakNode::render_for_raw_print(StringBuilder&) const |
99 | 0 | { |
100 | 0 | } |
101 | | |
102 | | size_t Text::BreakNode::terminal_length() const |
103 | 0 | { |
104 | 0 | return 0; |
105 | 0 | } |
106 | | |
107 | | RecursionDecision Text::BreakNode::walk(Visitor& visitor) const |
108 | 0 | { |
109 | 0 | RecursionDecision rd = visitor.visit(*this); |
110 | 0 | if (rd != RecursionDecision::Recurse) |
111 | 0 | return rd; |
112 | | // Normalize return value |
113 | 0 | return RecursionDecision::Continue; |
114 | 0 | } |
115 | | |
116 | | void Text::TextNode::render_to_html(StringBuilder& builder) const |
117 | 0 | { |
118 | 0 | builder.append(escape_html_entities(text)); |
119 | 0 | } |
120 | | |
121 | | void Text::TextNode::render_for_raw_print(StringBuilder& builder) const |
122 | 0 | { |
123 | 0 | builder.append(text); |
124 | 0 | } |
125 | | |
126 | | void Text::TextNode::render_for_terminal(StringBuilder& builder) const |
127 | 0 | { |
128 | 0 | if (collapsible && (text == "\n" || text.is_whitespace())) { |
129 | 0 | builder.append(' '); |
130 | 0 | } else { |
131 | 0 | builder.append(text); |
132 | 0 | } |
133 | 0 | } |
134 | | |
135 | | size_t Text::TextNode::terminal_length() const |
136 | 0 | { |
137 | 0 | if (collapsible && text.is_whitespace()) { |
138 | 0 | return 1; |
139 | 0 | } |
140 | | |
141 | 0 | return text.length(); |
142 | 0 | } |
143 | | |
144 | | RecursionDecision Text::TextNode::walk(Visitor& visitor) const |
145 | 0 | { |
146 | 0 | RecursionDecision rd = visitor.visit(*this); |
147 | 0 | if (rd != RecursionDecision::Recurse) |
148 | 0 | return rd; |
149 | 0 | rd = visitor.visit(text); |
150 | 0 | if (rd != RecursionDecision::Recurse) |
151 | 0 | return rd; |
152 | | // Normalize return value |
153 | 0 | return RecursionDecision::Continue; |
154 | 0 | } |
155 | | |
156 | | void Text::LinkNode::render_to_html(StringBuilder& builder) const |
157 | 0 | { |
158 | 0 | if (is_image) { |
159 | 0 | builder.append("<img src=\""sv); |
160 | 0 | builder.append(escape_html_entities(href)); |
161 | 0 | if (has_image_dimensions()) { |
162 | 0 | builder.append("\" style=\""sv); |
163 | 0 | if (image_width.has_value()) |
164 | 0 | builder.appendff("width: {}px;", *image_width); |
165 | 0 | if (image_height.has_value()) |
166 | 0 | builder.appendff("height: {}px;", *image_height); |
167 | 0 | } |
168 | 0 | builder.append("\" alt=\""sv); |
169 | 0 | text->render_to_html(builder); |
170 | 0 | builder.append("\" >"sv); |
171 | 0 | } else { |
172 | 0 | builder.append("<a href=\""sv); |
173 | 0 | builder.append(escape_html_entities(href)); |
174 | 0 | builder.append("\">"sv); |
175 | 0 | text->render_to_html(builder); |
176 | 0 | builder.append("</a>"sv); |
177 | 0 | } |
178 | 0 | } |
179 | | |
180 | | void Text::LinkNode::render_for_raw_print(StringBuilder& builder) const |
181 | 0 | { |
182 | 0 | text->render_for_raw_print(builder); |
183 | 0 | } |
184 | | |
185 | | void Text::LinkNode::render_for_terminal(StringBuilder& builder) const |
186 | 0 | { |
187 | 0 | bool is_linked = href.contains("://"sv); |
188 | 0 | if (is_linked) { |
189 | 0 | builder.append("\033[0;34m\e]8;;"sv); |
190 | 0 | builder.append(href); |
191 | 0 | builder.append("\e\\"sv); |
192 | 0 | } |
193 | |
|
194 | 0 | text->render_for_terminal(builder); |
195 | |
|
196 | 0 | if (is_linked) { |
197 | 0 | builder.appendff(" <{}>", href); |
198 | 0 | builder.append("\033]8;;\033\\\033[0m"sv); |
199 | 0 | } |
200 | 0 | } |
201 | | |
202 | | size_t Text::LinkNode::terminal_length() const |
203 | 0 | { |
204 | 0 | return text->terminal_length(); |
205 | 0 | } |
206 | | |
207 | | RecursionDecision Text::LinkNode::walk(Visitor& visitor) const |
208 | 0 | { |
209 | 0 | RecursionDecision rd = visitor.visit(*this); |
210 | 0 | if (rd != RecursionDecision::Recurse) |
211 | 0 | return rd; |
212 | | |
213 | | // Don't recurse on href. |
214 | | |
215 | 0 | return text->walk(visitor); |
216 | 0 | } |
217 | | |
218 | | void Text::MultiNode::render_to_html(StringBuilder& builder) const |
219 | 0 | { |
220 | 0 | for (auto& child : children) { |
221 | 0 | child->render_to_html(builder); |
222 | 0 | } |
223 | 0 | } |
224 | | |
225 | | void Text::MultiNode::render_for_raw_print(StringBuilder& builder) const |
226 | 0 | { |
227 | 0 | for (auto& child : children) { |
228 | 0 | child->render_for_raw_print(builder); |
229 | 0 | } |
230 | 0 | } |
231 | | |
232 | | void Text::MultiNode::render_for_terminal(StringBuilder& builder) const |
233 | 0 | { |
234 | 0 | for (auto& child : children) { |
235 | 0 | child->render_for_terminal(builder); |
236 | 0 | } |
237 | 0 | } |
238 | | |
239 | | size_t Text::MultiNode::terminal_length() const |
240 | 0 | { |
241 | 0 | size_t length = 0; |
242 | 0 | for (auto& child : children) { |
243 | 0 | length += child->terminal_length(); |
244 | 0 | } |
245 | 0 | return length; |
246 | 0 | } |
247 | | |
248 | | RecursionDecision Text::MultiNode::walk(Visitor& visitor) const |
249 | 0 | { |
250 | 0 | RecursionDecision rd = visitor.visit(*this); |
251 | 0 | if (rd != RecursionDecision::Recurse) |
252 | 0 | return rd; |
253 | | |
254 | 0 | for (auto const& child : children) { |
255 | 0 | rd = child->walk(visitor); |
256 | 0 | if (rd == RecursionDecision::Break) |
257 | 0 | return rd; |
258 | 0 | } |
259 | | |
260 | 0 | return RecursionDecision::Continue; |
261 | 0 | } |
262 | | |
263 | | void Text::StrikeThroughNode::render_to_html(StringBuilder& builder) const |
264 | 0 | { |
265 | 0 | builder.append("<del>"sv); |
266 | 0 | striked_text->render_to_html(builder); |
267 | 0 | builder.append("</del>"sv); |
268 | 0 | } |
269 | | |
270 | | void Text::StrikeThroughNode::render_for_raw_print(StringBuilder& builder) const |
271 | 0 | { |
272 | 0 | striked_text->render_for_raw_print(builder); |
273 | 0 | } |
274 | | |
275 | | void Text::StrikeThroughNode::render_for_terminal(StringBuilder& builder) const |
276 | 0 | { |
277 | 0 | builder.append("\e[9m"sv); |
278 | 0 | striked_text->render_for_terminal(builder); |
279 | 0 | builder.append("\e[29m"sv); |
280 | 0 | } |
281 | | |
282 | | size_t Text::StrikeThroughNode::terminal_length() const |
283 | 0 | { |
284 | 0 | return striked_text->terminal_length(); |
285 | 0 | } |
286 | | |
287 | | RecursionDecision Text::StrikeThroughNode::walk(Visitor& visitor) const |
288 | 0 | { |
289 | 0 | RecursionDecision rd = visitor.visit(*this); |
290 | 0 | if (rd != RecursionDecision::Recurse) |
291 | 0 | return rd; |
292 | | |
293 | 0 | return striked_text->walk(visitor); |
294 | 0 | } |
295 | | |
296 | | size_t Text::terminal_length() const |
297 | 0 | { |
298 | 0 | return m_node->terminal_length(); |
299 | 0 | } |
300 | | |
301 | | ByteString Text::render_to_html() const |
302 | 0 | { |
303 | 0 | StringBuilder builder; |
304 | 0 | m_node->render_to_html(builder); |
305 | 0 | return builder.to_byte_string().trim(" \n\t"sv); |
306 | 0 | } |
307 | | |
308 | | ByteString Text::render_for_raw_print() const |
309 | 0 | { |
310 | 0 | StringBuilder builder; |
311 | 0 | m_node->render_for_raw_print(builder); |
312 | 0 | return builder.to_byte_string().trim(" \n\t"sv); |
313 | 0 | } |
314 | | |
315 | | ByteString Text::render_for_terminal() const |
316 | 0 | { |
317 | 0 | StringBuilder builder; |
318 | 0 | m_node->render_for_terminal(builder); |
319 | 0 | return builder.to_byte_string().trim(" \n\t"sv); |
320 | 0 | } |
321 | | |
322 | | RecursionDecision Text::walk(Visitor& visitor) const |
323 | 0 | { |
324 | 0 | RecursionDecision rd = visitor.visit(*this); |
325 | 0 | if (rd != RecursionDecision::Recurse) |
326 | 0 | return rd; |
327 | | |
328 | 0 | return m_node->walk(visitor); |
329 | 0 | } |
330 | | |
331 | | Text Text::parse(StringView str) |
332 | 26.2M | { |
333 | 26.2M | Text text; |
334 | 26.2M | auto const tokens = tokenize(str); |
335 | 26.2M | auto iterator = tokens.begin(); |
336 | 26.2M | text.m_node = parse_sequence(iterator, false); |
337 | 26.2M | return text; |
338 | 26.2M | } |
339 | | |
340 | | static bool flanking(StringView str, size_t start, size_t end, int dir) |
341 | 37.0M | { |
342 | 37.0M | ssize_t next = ((dir > 0) ? end : start) + dir; |
343 | 37.0M | if (next < 0 || next >= (ssize_t)str.length()) |
344 | 29.9k | return false; |
345 | | |
346 | 36.9M | if (isspace(str[next])) |
347 | 28.2M | return false; |
348 | | |
349 | 8.70M | if (!ispunct(str[next])) |
350 | 2.34M | return true; |
351 | | |
352 | 6.36M | ssize_t prev = ((dir > 0) ? start : end) - dir; |
353 | 6.36M | if (prev < 0 || prev >= (ssize_t)str.length()) |
354 | 7.33k | return true; |
355 | | |
356 | 6.35M | return isspace(str[prev]) || ispunct(str[prev]); |
357 | 6.36M | } |
358 | | |
359 | | Vector<Text::Token> Text::tokenize(StringView str) |
360 | 26.2M | { |
361 | 26.2M | Vector<Token> tokens; |
362 | 26.2M | StringBuilder current_token; |
363 | | |
364 | 119M | auto flush_run = [&](bool left_flanking, bool right_flanking, bool punct_before, bool punct_after, bool is_run) { |
365 | 119M | if (current_token.is_empty()) |
366 | 63.2M | return; |
367 | | |
368 | 56.2M | tokens.append({ |
369 | 56.2M | current_token.to_byte_string(), |
370 | 56.2M | left_flanking, |
371 | 56.2M | right_flanking, |
372 | 56.2M | punct_before, |
373 | 56.2M | punct_after, |
374 | 56.2M | is_run, |
375 | 56.2M | }); |
376 | 56.2M | current_token.clear(); |
377 | 56.2M | }; |
378 | | |
379 | 101M | auto flush_token = [&]() { |
380 | 101M | flush_run(false, false, false, false, false); |
381 | 101M | }; |
382 | | |
383 | 26.2M | bool in_space = false; |
384 | | |
385 | 128M | for (size_t offset = 0; offset < str.length(); ++offset) { |
386 | 421M | auto has = [&](StringView seq) { |
387 | 421M | if (offset + seq.length() > str.length()) |
388 | 2.83M | return false; |
389 | | |
390 | 419M | return str.substring_view(offset, seq.length()) == seq; |
391 | 421M | }; |
392 | | |
393 | 101M | auto expect = [&](StringView seq) { |
394 | 12.1M | VERIFY(has(seq)); |
395 | 12.1M | flush_token(); |
396 | 12.1M | current_token.append(seq); |
397 | 12.1M | flush_token(); |
398 | 12.1M | offset += seq.length() - 1; |
399 | 12.1M | }; |
400 | | |
401 | 101M | char ch = str[offset]; |
402 | 101M | if (ch != ' ' && in_space) { |
403 | 15.8M | flush_token(); |
404 | 15.8M | in_space = false; |
405 | 15.8M | } |
406 | | |
407 | 101M | if (ch == '\\' && offset + 1 < str.length() && ispunct(str[offset + 1])) { |
408 | 631k | current_token.append(str[offset + 1]); |
409 | 631k | ++offset; |
410 | 101M | } else if (ch == '*' || ch == '_' || ch == '`' || ch == '~') { |
411 | 18.5M | flush_token(); |
412 | | |
413 | 18.5M | char delim = ch; |
414 | 18.5M | size_t run_offset; |
415 | 39.4M | for (run_offset = offset; run_offset < str.length() && str[run_offset] == delim; ++run_offset) { |
416 | 20.8M | current_token.append(str[run_offset]); |
417 | 20.8M | } |
418 | | |
419 | 18.5M | flush_run(flanking(str, offset, run_offset - 1, +1), |
420 | 18.5M | flanking(str, offset, run_offset - 1, -1), |
421 | 18.5M | offset > 0 && ispunct(str[offset - 1]), |
422 | 18.5M | run_offset < str.length() && ispunct(str[run_offset]), |
423 | 18.5M | true); |
424 | 18.5M | offset = run_offset - 1; |
425 | | |
426 | 82.8M | } else if (ch == ' ') { |
427 | 17.5M | if (!in_space) { |
428 | 16.0M | flush_token(); |
429 | 16.0M | in_space = true; |
430 | 16.0M | } |
431 | 17.5M | current_token.append(ch); |
432 | 65.2M | } else if (has("\n"sv)) { |
433 | 5.60M | expect("\n"sv); |
434 | 59.6M | } else if (has("["sv)) { |
435 | 946k | expect("["sv); |
436 | 58.6M | } else if (has(") { |
439 | 812k | expect("]("sv); |
440 | 57.8M | } else if (has(")"sv)) { |
441 | 3.01M | expect(")"sv); |
442 | 54.8M | } else if (has(">"sv)) { |
443 | 64.8k | expect(">"sv); |
444 | 54.7M | } else if (has("<"sv)) { |
445 | 1.75M | expect("<"sv); |
446 | 53.0M | } else { |
447 | 53.0M | current_token.append(ch); |
448 | 53.0M | } |
449 | 101M | } |
450 | 26.2M | flush_token(); |
451 | 26.2M | return tokens; |
452 | 26.2M | } |
453 | | |
454 | | NonnullOwnPtr<Text::MultiNode> Text::parse_sequence(Vector<Token>::ConstIterator& tokens, bool in_link) |
455 | 26.9M | { |
456 | 26.9M | auto node = make<MultiNode>(); |
457 | | |
458 | 52.2M | for (; !tokens.is_end(); ++tokens) { |
459 | 26.0M | if (tokens->is_space()) { |
460 | 6.66M | node->children.append(parse_break(tokens)); |
461 | 19.4M | } else if (*tokens == "\n"sv) { |
462 | 3.11M | node->children.append(parse_newline(tokens)); |
463 | 16.2M | } else if (tokens->is_run) { |
464 | 5.75M | switch (tokens->run_char()) { |
465 | 52.9k | case '*': |
466 | 4.74M | case '_': |
467 | 4.74M | node->children.append(parse_emph(tokens, in_link)); |
468 | 4.74M | break; |
469 | 488k | case '`': |
470 | 488k | node->children.append(parse_code(tokens)); |
471 | 488k | break; |
472 | 521k | case '~': |
473 | 521k | node->children.append(parse_strike_through(tokens)); |
474 | 521k | break; |
475 | 5.75M | } |
476 | 10.5M | } else if (*tokens == "["sv || *tokens == " { |
479 | 581k | return node; |
480 | 9.35M | } else { |
481 | 9.35M | node->children.append(make<TextNode>(tokens->data)); |
482 | 9.35M | } |
483 | | |
484 | 25.4M | if (in_link && !tokens.is_end() && *tokens == "]("sv) |
485 | 18.7k | return node; |
486 | | |
487 | 25.4M | if (tokens.is_end()) |
488 | 113k | break; |
489 | 25.4M | } |
490 | 26.3M | return node; |
491 | 26.9M | } |
492 | | |
493 | | NonnullOwnPtr<Text::Node> Text::parse_break(Vector<Token>::ConstIterator& tokens) |
494 | 13.2M | { |
495 | 13.2M | auto next_tok = tokens + 1; |
496 | 13.2M | if (next_tok.is_end() || *next_tok != "\n"sv) |
497 | 13.0M | return make<TextNode>(tokens->data); |
498 | | |
499 | 146k | if (tokens->data.length() >= 2) |
500 | 250 | return make<BreakNode>(); |
501 | | |
502 | 146k | return make<MultiNode>(); |
503 | 146k | } |
504 | | |
505 | | NonnullOwnPtr<Text::Node> Text::parse_newline(Vector<Token>::ConstIterator& tokens) |
506 | 4.80M | { |
507 | 4.80M | auto node = make<TextNode>(tokens->data); |
508 | 4.80M | auto next_tok = tokens + 1; |
509 | 4.80M | if (!next_tok.is_end() && next_tok->is_space()) |
510 | | // Skip whitespace after newline. |
511 | 782 | ++tokens; |
512 | | |
513 | 4.80M | return node; |
514 | 4.80M | } |
515 | | |
516 | | bool Text::can_open(Token const& opening) |
517 | 11.4M | { |
518 | 11.4M | return (opening.run_char() == '~' && opening.left_flanking) || (opening.run_char() == '*' && opening.left_flanking) || (opening.run_char() == '_' && opening.left_flanking && (!opening.right_flanking || opening.punct_before)); |
519 | 11.4M | } |
520 | | |
521 | | bool Text::can_close_for(Token const& opening, Text::Token const& closing) |
522 | 7.22M | { |
523 | 7.22M | if (opening.run_char() != closing.run_char()) |
524 | 5.25M | return false; |
525 | | |
526 | 1.97M | if (opening.run_length() != closing.run_length()) |
527 | 313k | return false; |
528 | | |
529 | 1.65M | return (opening.run_char() == '~' && closing.right_flanking) || (opening.run_char() == '*' && closing.right_flanking) || (opening.run_char() == '_' && closing.right_flanking && (!closing.left_flanking || closing.punct_after)); |
530 | 1.97M | } |
531 | | |
532 | | NonnullOwnPtr<Text::Node> Text::parse_emph(Vector<Token>::ConstIterator& tokens, bool in_link) |
533 | 11.4M | { |
534 | 11.4M | auto opening = *tokens; |
535 | | |
536 | | // Check that the opening delimiter run is properly flanking. |
537 | 11.4M | if (!can_open(opening)) |
538 | 11.0M | return make<TextNode>(opening.data); |
539 | | |
540 | 443k | auto child = make<MultiNode>(); |
541 | 17.9M | for (++tokens; !tokens.is_end(); ++tokens) { |
542 | 17.9M | if (tokens->is_space()) { |
543 | 6.57M | child->children.append(parse_break(tokens)); |
544 | 11.4M | } else if (*tokens == "\n"sv) { |
545 | 1.68M | child->children.append(parse_newline(tokens)); |
546 | 9.71M | } else if (tokens->is_run) { |
547 | 7.22M | if (can_close_for(opening, *tokens)) { |
548 | 424k | return make<EmphasisNode>(opening.run_length() >= 2, move(child)); |
549 | 424k | } |
550 | | |
551 | 6.80M | switch (tokens->run_char()) { |
552 | 213k | case '*': |
553 | 6.70M | case '_': |
554 | 6.70M | child->children.append(parse_emph(tokens, in_link)); |
555 | 6.70M | break; |
556 | 72.3k | case '`': |
557 | 72.3k | child->children.append(parse_code(tokens)); |
558 | 72.3k | break; |
559 | 30.7k | case '~': |
560 | 30.7k | child->children.append(parse_strike_through(tokens)); |
561 | 30.7k | break; |
562 | 6.80M | } |
563 | 6.80M | } else if (*tokens == "["sv || *tokens == " { |
566 | 1.25k | child->children.prepend(make<TextNode>(opening.data)); |
567 | 1.25k | return child; |
568 | 2.37M | } else { |
569 | 2.37M | child->children.append(make<TextNode>(tokens->data)); |
570 | 2.37M | } |
571 | | |
572 | 17.5M | if (in_link && !tokens.is_end() && *tokens == "]("sv) { |
573 | 1.40k | child->children.prepend(make<TextNode>(opening.data)); |
574 | 1.40k | return child; |
575 | 1.40k | } |
576 | | |
577 | 17.5M | if (tokens.is_end()) |
578 | 10.0k | break; |
579 | 17.5M | } |
580 | 16.0k | child->children.prepend(make<TextNode>(opening.data)); |
581 | 16.0k | return child; |
582 | 443k | } |
583 | | |
584 | | NonnullOwnPtr<Text::Node> Text::parse_code(Vector<Token>::ConstIterator& tokens) |
585 | 560k | { |
586 | 560k | auto opening = *tokens; |
587 | | |
588 | 17.7M | auto is_closing = [&](Token const& token) { |
589 | 17.7M | return token.is_run && token.run_char() == '`' && token.run_length() == opening.run_length(); |
590 | 17.7M | }; |
591 | | |
592 | 560k | bool is_all_whitespace = true; |
593 | 560k | auto code = make<MultiNode>(); |
594 | 17.7M | for (auto iterator = tokens + 1; !iterator.is_end(); ++iterator) { |
595 | 17.7M | if (is_closing(*iterator)) { |
596 | 549k | tokens = iterator; |
597 | | |
598 | | // Strip first and last space, when appropriate. |
599 | 549k | if (!is_all_whitespace) { |
600 | 534k | auto& first = dynamic_cast<TextNode&>(*code->children.first()); |
601 | 534k | auto& last = dynamic_cast<TextNode&>(*code->children.last()); |
602 | 534k | if (first.text.starts_with(' ') && last.text.ends_with(' ')) { |
603 | 492k | first.text = first.text.substring(1); |
604 | 492k | last.text = last.text.substring(0, last.text.length() - 1); |
605 | 492k | } |
606 | 534k | } |
607 | | |
608 | 549k | return make<CodeNode>(move(code)); |
609 | 549k | } |
610 | | |
611 | 17.2M | is_all_whitespace = is_all_whitespace && iterator->data.is_whitespace(); |
612 | 17.2M | code->children.append(make<TextNode>((*iterator == "\n"sv) ? " " : iterator->data, false)); |
613 | 17.2M | } |
614 | | |
615 | 10.7k | return make<TextNode>(opening.data); |
616 | 560k | } |
617 | | |
618 | | NonnullOwnPtr<Text::Node> Text::parse_link(Vector<Token>::ConstIterator& tokens) |
619 | 707k | { |
620 | 707k | auto opening = *tokens++; |
621 | 707k | bool is_image = opening == " { |
626 | 107k | link_text->children.prepend(make<TextNode>(opening.data)); |
627 | 107k | return link_text; |
628 | 107k | } |
629 | 600k | auto separator = *tokens; |
630 | 600k | VERIFY(separator == "]("sv); |
631 | | |
632 | 600k | Optional<int> image_width; |
633 | 600k | Optional<int> image_height; |
634 | | |
635 | 3.73M | auto parse_image_dimensions = [&](StringView dimensions) -> bool { |
636 | 3.73M | if (!dimensions.starts_with('=')) |
637 | 3.72M | return false; |
638 | | |
639 | 10.0k | ArmedScopeGuard clear_image_dimensions = [&] { |
640 | 6.65k | image_width = {}; |
641 | 6.65k | image_height = {}; |
642 | 6.65k | }; |
643 | | |
644 | 10.0k | auto dimension_seperator = dimensions.find('x', 1); |
645 | 10.0k | if (!dimension_seperator.has_value()) |
646 | 2.38k | return false; |
647 | | |
648 | 7.62k | auto width_string = dimensions.substring_view(1, *dimension_seperator - 1); |
649 | 7.62k | if (!width_string.is_empty()) { |
650 | 2.63k | auto width = width_string.to_number<int>(); |
651 | 2.63k | if (!width.has_value()) |
652 | 1.75k | return false; |
653 | 878 | image_width = width; |
654 | 878 | } |
655 | | |
656 | 5.87k | auto height_start = *dimension_seperator + 1; |
657 | 5.87k | if (height_start < dimensions.length()) { |
658 | 5.07k | auto height_string = dimensions.substring_view(height_start); |
659 | 5.07k | auto height = height_string.to_number<int>(); |
660 | 5.07k | if (!height.has_value()) |
661 | 2.51k | return false; |
662 | 2.56k | image_height = height; |
663 | 2.56k | } |
664 | | |
665 | 3.36k | clear_image_dimensions.disarm(); |
666 | 3.36k | return true; |
667 | 5.87k | }; |
668 | | |
669 | 600k | StringBuilder address; |
670 | | |
671 | 600k | auto next = tokens + 1; |
672 | 600k | bool is_escaped = !next.is_end() && *next == "<"sv; |
673 | | // Don't add the angle bracket to the address. |
674 | 600k | if (is_escaped) |
675 | 305 | tokens++; |
676 | | |
677 | 90.1M | for (auto iterator = tokens + 1; !iterator.is_end(); ++iterator) { |
678 | | // FIXME: What to do if there's multiple dimension tokens? |
679 | 90.1M | if (is_image && !address.is_empty() && parse_image_dimensions(iterator->data)) |
680 | 3.36k | continue; |
681 | | |
682 | 90.1M | if (is_escaped && *iterator == ">"sv) { |
683 | | // Will match the below statement in the next iteration. |
684 | 204 | is_escaped = false; |
685 | 204 | continue; |
686 | 204 | } |
687 | | |
688 | 90.1M | if (!is_escaped && *iterator == ")"sv) { |
689 | 578k | tokens = iterator; |
690 | | |
691 | 578k | ByteString href = address.to_byte_string().trim_whitespace(); |
692 | | |
693 | | // Add file:// if the link is an absolute path otherwise it will be assumed relative. |
694 | 578k | if (AK::StringUtils::starts_with(href, "/"sv, CaseSensitivity::CaseSensitive)) |
695 | 2.06k | href = ByteString::formatted("file://{}", href); |
696 | | |
697 | 578k | return make<LinkNode>(is_image, move(link_text), move(href), image_width, image_height); |
698 | 578k | } |
699 | | |
700 | 89.5M | address.append(iterator->data); |
701 | 89.5M | } |
702 | | |
703 | 21.7k | link_text->children.prepend(make<TextNode>(opening.data)); |
704 | 21.7k | link_text->children.append(make<TextNode>(separator.data)); |
705 | 21.7k | return link_text; |
706 | 600k | } |
707 | | |
708 | | NonnullOwnPtr<Text::Node> Text::parse_strike_through(Vector<Token>::ConstIterator& tokens) |
709 | 552k | { |
710 | 552k | auto opening = *tokens; |
711 | | |
712 | 9.62M | auto is_closing = [&](Token const& token) { |
713 | 9.62M | return token.is_run && token.run_char() == '~' && token.run_length() == opening.run_length(); |
714 | 9.62M | }; |
715 | | |
716 | 552k | bool is_all_whitespace = true; |
717 | 552k | auto striked_text = make<MultiNode>(); |
718 | 9.62M | for (auto iterator = tokens + 1; !iterator.is_end(); ++iterator) { |
719 | 9.62M | if (is_closing(*iterator)) { |
720 | 547k | tokens = iterator; |
721 | | |
722 | 547k | if (!is_all_whitespace) { |
723 | 544k | auto& first = dynamic_cast<TextNode&>(*striked_text->children.first()); |
724 | 544k | auto& last = dynamic_cast<TextNode&>(*striked_text->children.last()); |
725 | 544k | if (first.text.starts_with(' ') && last.text.ends_with(' ')) { |
726 | 33.3k | first.text = first.text.substring(1); |
727 | 33.3k | last.text = last.text.substring(0, last.text.length() - 1); |
728 | 33.3k | } |
729 | 544k | } |
730 | | |
731 | 547k | return make<StrikeThroughNode>(move(striked_text)); |
732 | 547k | } |
733 | | |
734 | 9.07M | is_all_whitespace = is_all_whitespace && iterator->data.is_whitespace(); |
735 | 9.07M | striked_text->children.append(make<TextNode>((*iterator == "\n"sv) ? " " : iterator->data, false)); |
736 | 9.07M | } |
737 | | |
738 | 4.99k | return make<TextNode>(opening.data); |
739 | 552k | } |
740 | | |
741 | | } |