Serenity Operating System
1/*
2 * Copyright (c) 2019-2020, Sergey Bugaev <bugaevc@serenityos.org>
3 * Copyright (c) 2022, Peter Elliott <pelliott@serenityos.org>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/Forward.h>
9#include <AK/StringBuilder.h>
10#include <LibJS/MarkupGenerator.h>
11#include <LibMarkdown/CodeBlock.h>
12#include <LibMarkdown/Visitor.h>
13#include <LibRegex/Regex.h>
14
15namespace Markdown {
16
17DeprecatedString CodeBlock::render_to_html(bool) const
18{
19 StringBuilder builder;
20
21 builder.append("<pre>"sv);
22
23 if (m_style.length() >= 2)
24 builder.append("<strong>"sv);
25 else if (m_style.length() >= 2)
26 builder.append("<em>"sv);
27
28 if (m_language.is_empty())
29 builder.append("<code>"sv);
30 else
31 builder.appendff("<code class=\"language-{}\">", escape_html_entities(m_language));
32
33 if (m_language == "js") {
34 auto html_or_error = JS::MarkupGenerator::html_from_source(m_code);
35 if (html_or_error.is_error()) {
36 warnln("Could not render js code to html: {}", html_or_error.error());
37 builder.append(escape_html_entities(m_code));
38 } else {
39 builder.append(html_or_error.release_value());
40 }
41 } else {
42 builder.append(escape_html_entities(m_code));
43 }
44
45 builder.append("</code>"sv);
46
47 if (m_style.length() >= 2)
48 builder.append("</strong>"sv);
49 else if (m_style.length() >= 2)
50 builder.append("</em>"sv);
51
52 builder.append("</pre>\n"sv);
53
54 return builder.to_deprecated_string();
55}
56
57Vector<DeprecatedString> CodeBlock::render_lines_for_terminal(size_t) const
58{
59 Vector<DeprecatedString> lines;
60
61 // Do not indent too much if we are in the synopsis
62 auto indentation = " "sv;
63 if (m_current_section != nullptr) {
64 auto current_section_name = m_current_section->render_lines_for_terminal()[0];
65 if (current_section_name.contains("SYNOPSIS"sv))
66 indentation = " "sv;
67 }
68
69 for (auto const& line : m_code.split('\n'))
70 lines.append(DeprecatedString::formatted("{}{}", indentation, line));
71 lines.append("");
72
73 return lines;
74}
75
76RecursionDecision CodeBlock::walk(Visitor& visitor) const
77{
78 RecursionDecision rd = visitor.visit(*this);
79 if (rd != RecursionDecision::Recurse)
80 return rd;
81
82 rd = visitor.visit(m_code);
83 if (rd != RecursionDecision::Recurse)
84 return rd;
85
86 // Don't recurse on m_language and m_style.
87
88 // Normalize return value.
89 return RecursionDecision::Continue;
90}
91
92static Regex<ECMA262> open_fence_re("^ {0,3}(([\\`\\~])\\2{2,})\\s*([\\*_]*)\\s*([^\\*_\\s]*).*$");
93static Regex<ECMA262> close_fence_re("^ {0,3}(([\\`\\~])\\2{2,})\\s*$");
94
95static Optional<int> line_block_prefix(StringView const& line)
96{
97 int characters = 0;
98 int indents = 0;
99
100 for (char ch : line) {
101 if (indents == 4)
102 break;
103
104 if (ch == ' ') {
105 ++characters;
106 ++indents;
107 } else if (ch == '\t') {
108 ++characters;
109 indents = 4;
110 } else {
111 break;
112 }
113 }
114
115 if (indents == 4)
116 return characters;
117
118 return {};
119}
120
121OwnPtr<CodeBlock> CodeBlock::parse(LineIterator& lines, Heading* current_section)
122{
123 if (lines.is_end())
124 return {};
125
126 StringView line = *lines;
127 if (open_fence_re.match(line).success)
128 return parse_backticks(lines, current_section);
129
130 if (line_block_prefix(line).has_value())
131 return parse_indent(lines);
132
133 return {};
134}
135
136OwnPtr<CodeBlock> CodeBlock::parse_backticks(LineIterator& lines, Heading* current_section)
137{
138 StringView line = *lines;
139
140 // Our Markdown extension: we allow
141 // specifying a style and a language
142 // for a code block, like so:
143 //
144 // ```**sh**
145 // $ echo hello friends!
146 // ````
147 //
148 // The code block will be made bold,
149 // and if possible syntax-highlighted
150 // as appropriate for a shell script.
151
152 auto matches = open_fence_re.match(line).capture_group_matches[0];
153 auto fence = matches[0].view.string_view();
154 auto style = matches[2].view.string_view();
155 auto language = matches[3].view.string_view();
156
157 ++lines;
158
159 StringBuilder builder;
160
161 while (true) {
162 if (lines.is_end())
163 break;
164 line = *lines;
165 ++lines;
166
167 auto close_match = close_fence_re.match(line);
168 if (close_match.success) {
169 auto close_fence = close_match.capture_group_matches[0][0].view.string_view();
170 if (close_fence[0] == fence[0] && close_fence.length() >= fence.length())
171 break;
172 }
173 builder.append(line);
174 builder.append('\n');
175 }
176
177 return make<CodeBlock>(language, style, builder.to_deprecated_string(), current_section);
178}
179
180OwnPtr<CodeBlock> CodeBlock::parse_indent(LineIterator& lines)
181{
182 StringBuilder builder;
183
184 while (true) {
185 if (lines.is_end())
186 break;
187 StringView line = *lines;
188
189 auto prefix_length = line_block_prefix(line);
190 if (!prefix_length.has_value())
191 break;
192
193 line = line.substring_view(prefix_length.value());
194 ++lines;
195
196 builder.append(line);
197 builder.append('\n');
198 }
199
200 return make<CodeBlock>("", "", builder.to_deprecated_string(), nullptr);
201}
202}