Coverage Report

Created: 2026-02-16 07:47

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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("!["sv)) {
437
3.49k
            expect("!["sv);
438
58.6M
        } else if (has("]("sv)) {
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 == "!["sv) {
477
599k
            node->children.append(parse_link(tokens));
478
9.93M
        } else if (in_link && *tokens == "]("sv) {
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 == "!["sv) {
564
108k
            child->children.append(parse_link(tokens));
565
2.37M
        } else if (in_link && *tokens == "]("sv) {
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 == "!["sv;
622
623
707k
    auto link_text = parse_sequence(tokens, true);
624
625
707k
    if (tokens.is_end() || *tokens != "]("sv) {
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
}