Coverage Report

Created: 2025-12-18 07:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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("&lt;empty&gt;"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("&lt;already printed Object {:p}&gt;", &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("&nbsp;&nbsp;{}<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
}