/src/serenity/Userland/Libraries/LibJS/MarkupGenerator.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2020, Hunter Salyer <thefalsehonesty@gmail.com> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <AK/HashTable.h> |
8 | | #include <AK/StringBuilder.h> |
9 | | #include <AK/TypeCasts.h> |
10 | | #include <LibJS/Lexer.h> |
11 | | #include <LibJS/MarkupGenerator.h> |
12 | | #include <LibJS/Runtime/Array.h> |
13 | | #include <LibJS/Runtime/Date.h> |
14 | | #include <LibJS/Runtime/DatePrototype.h> |
15 | | #include <LibJS/Runtime/Error.h> |
16 | | #include <LibJS/Runtime/Object.h> |
17 | | #include <LibJS/Runtime/VM.h> |
18 | | |
19 | | namespace JS { |
20 | | |
21 | | ErrorOr<String> MarkupGenerator::html_from_source(StringView source) |
22 | 0 | { |
23 | 0 | StringBuilder builder; |
24 | 0 | auto lexer = Lexer(source); |
25 | 0 | for (auto token = lexer.next(); token.type() != TokenType::Eof; token = lexer.next()) { |
26 | 0 | TRY(builder.try_append(token.trivia())); |
27 | 0 | TRY(builder.try_append(TRY(wrap_string_in_style(token.value(), style_type_for_token(token))))); |
28 | 0 | } |
29 | 0 | return builder.to_string(); |
30 | 0 | } |
31 | | |
32 | | ErrorOr<String> MarkupGenerator::html_from_value(Value value) |
33 | 0 | { |
34 | 0 | StringBuilder output_html; |
35 | 0 | HashTable<Object*> seen_objects; |
36 | 0 | TRY(value_to_html(value, output_html, seen_objects)); |
37 | 0 | return output_html.to_string(); |
38 | 0 | } |
39 | | |
40 | | ErrorOr<String> MarkupGenerator::html_from_error(Error const& object, bool in_promise) |
41 | 0 | { |
42 | 0 | StringBuilder output_html; |
43 | 0 | TRY(error_to_html(object, output_html, in_promise)); |
44 | 0 | return output_html.to_string(); |
45 | 0 | } |
46 | | |
47 | | ErrorOr<void> MarkupGenerator::value_to_html(Value value, StringBuilder& output_html, HashTable<Object*>& seen_objects) |
48 | 0 | { |
49 | 0 | if (value.is_empty()) { |
50 | 0 | TRY(output_html.try_append("<empty>"sv)); |
51 | 0 | return {}; |
52 | 0 | } |
53 | | |
54 | 0 | if (value.is_object()) { |
55 | 0 | if (seen_objects.contains(&value.as_object())) { |
56 | | // FIXME: Maybe we should only do this for circular references, |
57 | | // not for all reoccurring objects. |
58 | 0 | TRY(output_html.try_appendff("<already printed Object {:p}>", &value.as_object())); |
59 | 0 | return {}; |
60 | 0 | } |
61 | 0 | seen_objects.set(&value.as_object()); |
62 | 0 | } |
63 | | |
64 | 0 | if (value.is_object()) { |
65 | 0 | auto& object = value.as_object(); |
66 | 0 | if (is<Array>(object)) |
67 | 0 | return array_to_html(static_cast<Array const&>(object), output_html, seen_objects); |
68 | 0 | TRY(output_html.try_append(TRY(wrap_string_in_style(object.class_name(), StyleType::ObjectType)))); |
69 | 0 | if (object.is_function()) |
70 | 0 | return function_to_html(object, output_html, seen_objects); |
71 | 0 | if (is<Date>(object)) |
72 | 0 | return date_to_html(object, output_html, seen_objects); |
73 | 0 | return object_to_html(object, output_html, seen_objects); |
74 | 0 | } |
75 | | |
76 | 0 | if (value.is_string()) |
77 | 0 | TRY(output_html.try_append(TRY(open_style_type(StyleType::String)))); |
78 | 0 | else if (value.is_number()) |
79 | 0 | TRY(output_html.try_append(TRY(open_style_type(StyleType::Number)))); |
80 | 0 | else if (value.is_boolean() || value.is_nullish()) |
81 | 0 | TRY(output_html.try_append(TRY(open_style_type(StyleType::KeywordBold)))); |
82 | |
|
83 | 0 | if (value.is_string()) |
84 | 0 | TRY(output_html.try_append('"')); |
85 | 0 | TRY(output_html.try_append(escape_html_entities(value.to_string_without_side_effects()))); |
86 | 0 | if (value.is_string()) |
87 | 0 | TRY(output_html.try_append('"')); |
88 | |
|
89 | 0 | TRY(output_html.try_append("</span>"sv)); |
90 | 0 | return {}; |
91 | 0 | } |
92 | | |
93 | | ErrorOr<void> MarkupGenerator::array_to_html(Array const& array, StringBuilder& html_output, HashTable<Object*>& seen_objects) |
94 | 0 | { |
95 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style("[ "sv, StyleType::Punctuation)))); |
96 | 0 | bool first = true; |
97 | 0 | for (auto it = array.indexed_properties().begin(false); it != array.indexed_properties().end(); ++it) { |
98 | 0 | if (!first) |
99 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(", "sv, StyleType::Punctuation)))); |
100 | 0 | first = false; |
101 | | // FIXME: Exception check |
102 | 0 | TRY(value_to_html(array.get(it.index()).release_value(), html_output, seen_objects)); |
103 | 0 | } |
104 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(" ]"sv, StyleType::Punctuation)))); |
105 | 0 | return {}; |
106 | 0 | } |
107 | | |
108 | | ErrorOr<void> MarkupGenerator::object_to_html(Object const& object, StringBuilder& html_output, HashTable<Object*>& seen_objects) |
109 | 0 | { |
110 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style("{ "sv, StyleType::Punctuation)))); |
111 | 0 | bool first = true; |
112 | 0 | for (auto& entry : object.indexed_properties()) { |
113 | 0 | if (!first) |
114 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(", "sv, StyleType::Punctuation)))); |
115 | 0 | first = false; |
116 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(String::number(entry.index()), StyleType::Number)))); |
117 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(": "sv, StyleType::Punctuation)))); |
118 | | // FIXME: Exception check |
119 | 0 | TRY(value_to_html(object.get(entry.index()).release_value(), html_output, seen_objects)); |
120 | 0 | } |
121 | |
|
122 | 0 | if (!object.indexed_properties().is_empty() && object.shape().property_count()) |
123 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(", "sv, StyleType::Punctuation)))); |
124 | |
|
125 | 0 | size_t index = 0; |
126 | 0 | for (auto& it : object.shape().property_table()) { |
127 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(TRY(String::formatted("\"{}\"", escape_html_entities(it.key.to_display_string()))), StyleType::String)))); |
128 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(": "sv, StyleType::Punctuation)))); |
129 | 0 | TRY(value_to_html(object.get_direct(it.value.offset), html_output, seen_objects)); |
130 | 0 | if (index != object.shape().property_count() - 1) |
131 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(", "sv, StyleType::Punctuation)))); |
132 | 0 | ++index; |
133 | 0 | } |
134 | |
|
135 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(" }"sv, StyleType::Punctuation)))); |
136 | 0 | return {}; |
137 | 0 | } |
138 | | |
139 | | ErrorOr<void> MarkupGenerator::function_to_html(Object const& function, StringBuilder& html_output, HashTable<Object*>&) |
140 | 0 | { |
141 | 0 | TRY(html_output.try_appendff("[{}]", function.class_name())); |
142 | 0 | return {}; |
143 | 0 | } |
144 | | |
145 | | ErrorOr<void> MarkupGenerator::date_to_html(Object const& date, StringBuilder& html_output, HashTable<Object*>&) |
146 | 0 | { |
147 | 0 | TRY(html_output.try_appendff("Date {}", to_date_string(static_cast<Date const&>(date).date_value()))); |
148 | 0 | return {}; |
149 | 0 | } |
150 | | |
151 | | ErrorOr<void> MarkupGenerator::trace_to_html(TracebackFrame const& traceback_frame, StringBuilder& html_output) |
152 | 0 | { |
153 | 0 | auto function_name = escape_html_entities(traceback_frame.function_name); |
154 | 0 | auto [line, column, _] = traceback_frame.source_range().start; |
155 | 0 | auto get_filename_from_path = [&](StringView filename) -> StringView { |
156 | 0 | auto last_slash_index = filename.find_last('/'); |
157 | 0 | return last_slash_index.has_value() ? filename.substring_view(*last_slash_index + 1) : filename; |
158 | 0 | }; |
159 | 0 | auto filename = escape_html_entities(get_filename_from_path(traceback_frame.source_range().filename())); |
160 | 0 | auto trace = TRY(String::formatted("at {} ({}:{}:{})", function_name, filename, line, column)); |
161 | |
|
162 | 0 | TRY(html_output.try_appendff(" {}<br>", trace)); |
163 | 0 | return {}; |
164 | 0 | } |
165 | | |
166 | | ErrorOr<void> MarkupGenerator::error_to_html(Error const& error, StringBuilder& html_output, bool in_promise) |
167 | 0 | { |
168 | 0 | auto& vm = error.vm(); |
169 | 0 | auto name = error.get_without_side_effects(vm.names.name).value_or(js_undefined()); |
170 | 0 | auto message = error.get_without_side_effects(vm.names.message).value_or(js_undefined()); |
171 | 0 | auto name_string = name.to_string_without_side_effects(); |
172 | 0 | auto message_string = message.to_string_without_side_effects(); |
173 | 0 | auto uncaught_message = TRY(String::formatted("Uncaught {}[{}]: ", in_promise ? "(in promise) " : "", name_string)); |
174 | |
|
175 | 0 | TRY(html_output.try_append(TRY(wrap_string_in_style(uncaught_message, StyleType::Invalid)))); |
176 | 0 | TRY(html_output.try_appendff("{}<br>", message_string.is_empty() ? "\"\"" : escape_html_entities(message_string))); |
177 | |
|
178 | 0 | for (size_t i = 0; i < error.traceback().size() - min(error.traceback().size(), 3); i++) { |
179 | 0 | auto& traceback_frame = error.traceback().at(i); |
180 | 0 | TRY(trace_to_html(traceback_frame, html_output)); |
181 | 0 | } |
182 | 0 | return {}; |
183 | 0 | } |
184 | | |
185 | | StringView MarkupGenerator::style_from_style_type(StyleType type) |
186 | 0 | { |
187 | 0 | switch (type) { |
188 | 0 | case StyleType::Invalid: |
189 | 0 | return "color: red;"sv; |
190 | 0 | case StyleType::String: |
191 | 0 | return "color: -libweb-palette-syntax-string;"sv; |
192 | 0 | case StyleType::Number: |
193 | 0 | return "color: -libweb-palette-syntax-number;"sv; |
194 | 0 | case StyleType::KeywordBold: |
195 | 0 | return "color: -libweb-palette-syntax-keyword; font-weight: bold;"sv; |
196 | 0 | case StyleType::Punctuation: |
197 | 0 | return "color: -libweb-palette-syntax-punctuation;"sv; |
198 | 0 | case StyleType::Operator: |
199 | 0 | return "color: -libweb-palette-syntax-operator;"sv; |
200 | 0 | case StyleType::Keyword: |
201 | 0 | return "color: -libweb-palette-syntax-keyword;"sv; |
202 | 0 | case StyleType::ControlKeyword: |
203 | 0 | return "color: -libweb-palette-syntax-control-keyword;"sv; |
204 | 0 | case StyleType::Identifier: |
205 | 0 | return "color: -libweb-palette-syntax-identifier;"sv; |
206 | 0 | case StyleType::ObjectType: |
207 | 0 | return "padding: 2px; background-color: #ddf; color: black; font-weight: bold;"sv; |
208 | 0 | default: |
209 | 0 | VERIFY_NOT_REACHED(); |
210 | 0 | } |
211 | 0 | } |
212 | | |
213 | | MarkupGenerator::StyleType MarkupGenerator::style_type_for_token(Token token) |
214 | 0 | { |
215 | 0 | switch (token.category()) { |
216 | 0 | case TokenCategory::Invalid: |
217 | 0 | return StyleType::Invalid; |
218 | 0 | case TokenCategory::Number: |
219 | 0 | return StyleType::Number; |
220 | 0 | case TokenCategory::String: |
221 | 0 | return StyleType::String; |
222 | 0 | case TokenCategory::Punctuation: |
223 | 0 | return StyleType::Punctuation; |
224 | 0 | case TokenCategory::Operator: |
225 | 0 | return StyleType::Operator; |
226 | 0 | case TokenCategory::Keyword: |
227 | 0 | switch (token.type()) { |
228 | 0 | case TokenType::BoolLiteral: |
229 | 0 | case TokenType::NullLiteral: |
230 | 0 | return StyleType::KeywordBold; |
231 | 0 | default: |
232 | 0 | return StyleType::Keyword; |
233 | 0 | } |
234 | 0 | case TokenCategory::ControlKeyword: |
235 | 0 | return StyleType::ControlKeyword; |
236 | 0 | case TokenCategory::Identifier: |
237 | 0 | return StyleType::Identifier; |
238 | 0 | default: |
239 | 0 | dbgln("Unknown style type for token {}", token.name()); |
240 | 0 | VERIFY_NOT_REACHED(); |
241 | 0 | } |
242 | 0 | } |
243 | | |
244 | | ErrorOr<String> MarkupGenerator::open_style_type(StyleType type) |
245 | 0 | { |
246 | 0 | return String::formatted("<span style=\"{}\">", style_from_style_type(type)); |
247 | 0 | } |
248 | | |
249 | | ErrorOr<String> MarkupGenerator::wrap_string_in_style(StringView source, StyleType type) |
250 | 0 | { |
251 | 0 | return String::formatted("<span style=\"{}\">{}</span>", style_from_style_type(type), escape_html_entities(source)); |
252 | 0 | } |
253 | | |
254 | | } |