From 941b028ca3287574aeaec470d66ce085a9ccb100 Mon Sep 17 00:00:00 2001 From: FalseHonesty Date: Mon, 25 May 2020 15:24:46 -0400 Subject: [PATCH] LibJS: Create JS to HTML markup generator The new JS::MarkupGenerator class can convert both a JS source string and a JS Runtime Value into properly formatted HTML using the new LibWeb System Palette css color values. It makes more sense for this JS -> HTML process to occur in LibJS so that it can be used elsewhere, namely Markdown code block syntax highlighting. It also means the Browser can worry less about LibJS implementation details. --- Applications/Browser/ConsoleWidget.cpp | 156 +----------- Applications/Browser/ConsoleWidget.h | 7 - Libraries/LibJS/CMakeLists.txt | 1 + Libraries/LibJS/Forward.h | 1 + Libraries/LibJS/MarkupGenerator.cpp | 323 +++++++++++++++++++++++++ Libraries/LibJS/MarkupGenerator.h | 64 +++++ 6 files changed, 392 insertions(+), 160 deletions(-) create mode 100644 Libraries/LibJS/MarkupGenerator.cpp create mode 100644 Libraries/LibJS/MarkupGenerator.h diff --git a/Applications/Browser/ConsoleWidget.cpp b/Applications/Browser/ConsoleWidget.cpp index 9743e87bc9..a5ae9e61aa 100644 --- a/Applications/Browser/ConsoleWidget.cpp +++ b/Applications/Browser/ConsoleWidget.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -93,16 +94,14 @@ ConsoleWidget::ConsoleWidget() if (m_interpreter->exception()) { StringBuilder output_html; output_html.append("Uncaught exception: "); - print_value(m_interpreter->exception()->value(), output_html); + output_html.append(JS::MarkupGenerator::html_from_value(m_interpreter->exception()->value())); print_html(output_html.string_view()); m_interpreter->clear_exception(); return; } - StringBuilder output_html; - print_value(m_interpreter->last_value(), output_html); - print_html(output_html.string_view()); + print_html(JS::MarkupGenerator::html_from_value(m_interpreter->last_value())); }; } @@ -174,155 +173,6 @@ void ConsoleWidget::print_source_line(const StringView& source) print_html(html.string_view()); } -void ConsoleWidget::print_value(JS::Value value, StringBuilder& output_html, HashTable seen_objects) -{ - // FIXME: Support output highlighting - - if (value.is_empty()) { - output_html.append(""); - output_html.append("<empty>"); - output_html.append(""); - return; - } - - if (value.is_object()) { - if (seen_objects.contains(&value.as_object())) { - // FIXME: Maybe we should only do this for circular references, - // not for all reoccurring objects. - output_html.append(""); - output_html.appendf("<already printed Object %p>", &value.as_object()); - output_html.append(""); - return; - } - seen_objects.set(&value.as_object()); - } - - if (value.is_array()) - return print_array(static_cast(value.as_object()), output_html, seen_objects); - - if (value.is_object()) { - auto& object = value.as_object(); - if (object.is_function()) - return print_function(object, output_html, seen_objects); - if (object.is_date()) - return print_date(object, output_html, seen_objects); - if (object.is_error()) - return print_error(object, output_html, seen_objects); - return print_object(object, output_html, seen_objects); - } - - if (value.is_string()) - output_html.append(""); - else if (value.is_number()) - output_html.append(""); - else if (value.is_boolean()) - output_html.append(""); - else if (value.is_null()) - output_html.append(""); - else if (value.is_undefined()) - output_html.append(""); - - if (value.is_string()) - output_html.append('"'); - output_html.append(value.to_string_without_side_effects()); - if (value.is_string()) - output_html.append('"'); - - output_html.append(""); -} - -void ConsoleWidget::print_array(const JS::Array& array, StringBuilder& html_output, HashTable& seen_objects) -{ - html_output.append(""); - html_output.append("[ "); - html_output.append(""); - for (size_t i = 0; i < array.elements().size(); ++i) { - print_value(array.elements()[i], html_output, seen_objects); - if (i != array.elements().size() - 1) { - html_output.append(""); - html_output.append(", "); - html_output.append(""); - } - } - html_output.append(""); - html_output.append(" ]"); - html_output.append(""); -} - -void ConsoleWidget::print_object(const JS::Object& object, StringBuilder& html_output, HashTable& seen_objects) -{ - html_output.append(""); - html_output.append("{ "); - html_output.append(""); - - for (size_t i = 0; i < object.elements().size(); ++i) { - if (object.elements()[i].is_empty()) - continue; - html_output.append(""); - html_output.appendf("%zu", i); - html_output.append(""); - html_output.append(": "); - print_value(object.elements()[i], html_output, seen_objects); - if (i != object.elements().size() - 1) { - html_output.append(""); - html_output.append(", "); - html_output.append(""); - } - } - - if (!object.elements().is_empty() && object.shape().property_count()) { - html_output.append(""); - html_output.append(", "); - html_output.append(""); - } - - size_t index = 0; - for (auto& it : object.shape().property_table_ordered()) { - html_output.append(""); - html_output.appendf("\"%s\"", it.key.characters()); - html_output.append(""); - html_output.append(": "); - print_value(object.get_direct(it.value.offset), html_output, seen_objects); - if (index != object.shape().property_count() - 1) { - html_output.append(""); - html_output.append(", "); - html_output.append(""); - } - ++index; - } - - html_output.append(""); - html_output.append(" }"); - html_output.append(""); -} - -void ConsoleWidget::print_function(const JS::Object& function, StringBuilder& html_output, HashTable&) -{ - html_output.append(""); - html_output.appendf("[%s]", function.class_name()); - html_output.append(""); -} - -void ConsoleWidget::print_date(const JS::Object& date, StringBuilder& html_output, HashTable&) -{ - html_output.append(""); - html_output.appendf("Date %s", static_cast(date).string().characters()); - html_output.append(""); -} - -void ConsoleWidget::print_error(const JS::Object& object, StringBuilder& html_output, HashTable&) -{ - auto& error = static_cast(object); - html_output.append(""); - html_output.appendf("[%s]", error.name().characters()); - html_output.append(""); - if (!error.message().is_empty()) { - html_output.append(""); - html_output.appendf(": %s", error.message().characters()); - html_output.append(""); - } -} - void ConsoleWidget::print_html(const StringView& line) { auto paragraph = create_element(m_console_output_container->document(), "p"); diff --git a/Applications/Browser/ConsoleWidget.h b/Applications/Browser/ConsoleWidget.h index 5a0d0d7412..50516ed5e6 100644 --- a/Applications/Browser/ConsoleWidget.h +++ b/Applications/Browser/ConsoleWidget.h @@ -47,13 +47,6 @@ private: String create_document_style(); - void print_value(JS::Value, StringBuilder& output_html, HashTable seen_objects = {}); - void print_array(const JS::Array&, StringBuilder& output_html, HashTable&); - void print_object(const JS::Object&, StringBuilder& output_html, HashTable&); - void print_function(const JS::Object&, StringBuilder& output_html, HashTable&); - void print_date(const JS::Object&, StringBuilder& output_html, HashTable&); - void print_error(const JS::Object&, StringBuilder& output_html, HashTable&); - RefPtr m_console_input; RefPtr m_console_output_view; RefPtr m_console_output_container; diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index d9928f9188..f47cf3dbb8 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES Heap/Heap.cpp Interpreter.cpp Lexer.cpp + MarkupGenerator.cpp Parser.cpp Runtime/ArrayConstructor.cpp Runtime/Array.cpp diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index 32b4887193..0776632dc2 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -73,6 +73,7 @@ class ScopeNode; class Shape; class Statement; class Symbol; +class Token; class Uint8ClampedArray; class Value; enum class DeclarationKind; diff --git a/Libraries/LibJS/MarkupGenerator.cpp b/Libraries/LibJS/MarkupGenerator.cpp new file mode 100644 index 0000000000..7724d7d1a0 --- /dev/null +++ b/Libraries/LibJS/MarkupGenerator.cpp @@ -0,0 +1,323 @@ +/* + * Copyright (c) 2020, Hunter Salyer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +String MarkupGenerator::html_from_source(String source) +{ + auto lexer = Lexer(source); + StringBuilder builder; + size_t source_cursor = 0; + + for (auto token = lexer.next(); token.type() != TokenType::Eof; token = lexer.next()) { + auto length = token.value().length(); + auto start = token.line_column(); + // FIXME: Why do we need to do this magic math? This math isn't even accurate enough, code like "let x = 10" renders incorrectly. + start = start < 2 ? 0 : start - 2; + + if (start > source_cursor) { + builder.append(source.substring_view(source_cursor, start - source_cursor)); + } + + builder.append(wrap_string_in_style(token.value(), style_type_for_token(token))); + source_cursor = length; + } + + if (source_cursor < source.length()) + builder.append(source.substring_view(source_cursor, source.length() - source_cursor)); + + return builder.to_string(); +} + +String MarkupGenerator::html_from_value(Value value) +{ + StringBuilder output_html; + value_to_html(value, output_html); + return output_html.to_string(); +} + +void MarkupGenerator::value_to_html(Value value, StringBuilder& output_html, HashTable seen_objects) +{ + if (value.is_empty()) { + output_html.append("<empty>"); + return; + } + + if (value.is_object()) { + if (seen_objects.contains(&value.as_object())) { + // FIXME: Maybe we should only do this for circular references, + // not for all reoccurring objects. + output_html.appendf("<already printed Object %p>", &value.as_object()); + return; + } + seen_objects.set(&value.as_object()); + } + + if (value.is_array()) + return array_to_html(static_cast(value.as_object()), output_html, seen_objects); + + if (value.is_object()) { + auto& object = value.as_object(); + if (object.is_function()) + return function_to_html(object, output_html, seen_objects); + if (object.is_date()) + return date_to_html(object, output_html, seen_objects); + if (object.is_error()) + return error_to_html(object, output_html, seen_objects); + return object_to_html(object, output_html, seen_objects); + } + + if (value.is_string()) + output_html.append(open_style_type(StyleType::String)); + else if (value.is_number()) + output_html.append(open_style_type(StyleType::Number)); + else if (value.is_boolean() || value.is_null() || value.is_undefined()) + output_html.append(open_style_type(StyleType::KeywordBold)); + + if (value.is_string()) + output_html.append('"'); + output_html.append(value.to_string_without_side_effects()); + if (value.is_string()) + output_html.append('"'); + + output_html.append(""); +} + +void MarkupGenerator::array_to_html(const Array& array, StringBuilder& html_output, HashTable& seen_objects) +{ + html_output.append(wrap_string_in_style("[ ", StyleType::Punctuation)); + for (size_t i = 0; i < array.elements().size(); ++i) { + value_to_html(array.elements()[i], html_output, seen_objects); + if (i != array.elements().size() - 1) + html_output.append(wrap_string_in_style(", ", StyleType::Punctuation)); + } + html_output.append(wrap_string_in_style(" ]", StyleType::Punctuation)); +} + +void MarkupGenerator::object_to_html(const Object& object, StringBuilder& html_output, HashTable& seen_objects) +{ + html_output.append(wrap_string_in_style("{ ", StyleType::Punctuation)); + + for (size_t i = 0; i < object.elements().size(); ++i) { + if (object.elements()[i].is_empty()) + continue; + html_output.append(wrap_string_in_style(String::format("%zu", i), StyleType::Number)); + html_output.append(wrap_string_in_style(": ", StyleType::Punctuation)); + value_to_html(object.elements()[i], html_output, seen_objects); + if (i != object.elements().size() - 1) + html_output.append(wrap_string_in_style(", ", StyleType::Punctuation)); + } + + if (!object.elements().is_empty() && object.shape().property_count()) + html_output.append(wrap_string_in_style(", ", StyleType::Punctuation)); + + size_t index = 0; + for (auto& it : object.shape().property_table_ordered()) { + html_output.append(wrap_string_in_style(String::format("\"%s\"", it.key.characters()), StyleType::String)); + html_output.append(wrap_string_in_style(": ", StyleType::Punctuation)); + value_to_html(object.get_direct(it.value.offset), html_output, seen_objects); + if (index != object.shape().property_count() - 1) + html_output.append(wrap_string_in_style(", ", StyleType::Punctuation)); + ++index; + } + + html_output.append(wrap_string_in_style(" }", StyleType::Punctuation)); +} + +void MarkupGenerator::function_to_html(const Object& function, StringBuilder& html_output, HashTable&) +{ + html_output.appendf("[%s]", function.class_name()); +} + +void MarkupGenerator::date_to_html(const Object& date, StringBuilder& html_output, HashTable&) +{ + html_output.appendf("Date %s", static_cast(date).string().characters()); +} + +void MarkupGenerator::error_to_html(const Object& object, StringBuilder& html_output, HashTable&) +{ + auto& error = static_cast(object); + html_output.append(wrap_string_in_style(String::format("[%s]", error.name().characters()), StyleType::Invalid)); + if (!error.message().is_empty()) { + html_output.appendf(": %s", error.message().characters()); + } +} + +String MarkupGenerator::style_from_style_type(StyleType type) +{ + switch (type) { + case StyleType::Invalid: + return "color: red;"; + case StyleType::String: + return "color: -libweb-palette-syntax-string;"; + case StyleType::Number: + return "color: -libweb-palette-syntax-number;"; + case StyleType::KeywordBold: + return "color: -libweb-palette-syntax-keyword; font-weight: bold;"; + case StyleType::Punctuation: + return "color: -libweb-palette-syntax-punctuation;"; + case StyleType::Operator: + return "color: -libweb-palette-syntax-operator;"; + case StyleType::Keyword: + return "color: -libweb-palette-syntax-keyword;"; + case StyleType::ControlKeyword: + return "color: -libweb-palette-syntax-control-keyword;"; + case StyleType::Identifier: + return "color: -libweb-palette-syntax-identifier;"; + default: + ASSERT_NOT_REACHED(); + } +} + +MarkupGenerator::StyleType MarkupGenerator::style_type_for_token(Token token) +{ + switch (token.type()) { + case TokenType::Invalid: + case TokenType::Eof: + return StyleType::Invalid; + case TokenType::NumericLiteral: + return StyleType::Number; + case TokenType::StringLiteral: + case TokenType::TemplateLiteralStart: + case TokenType::TemplateLiteralEnd: + case TokenType::TemplateLiteralString: + case TokenType::RegexLiteral: + case TokenType::UnterminatedStringLiteral: + return StyleType::String; + case TokenType::BracketClose: + case TokenType::BracketOpen: + case TokenType::Comma: + case TokenType::CurlyClose: + case TokenType::CurlyOpen: + case TokenType::ParenClose: + case TokenType::ParenOpen: + case TokenType::Semicolon: + case TokenType::Period: + return StyleType::Punctuation; + case TokenType::Ampersand: + case TokenType::AmpersandEquals: + case TokenType::Asterisk: + case TokenType::DoubleAsteriskEquals: + case TokenType::AsteriskEquals: + case TokenType::Caret: + case TokenType::CaretEquals: + case TokenType::DoubleAmpersand: + case TokenType::DoubleAsterisk: + case TokenType::DoublePipe: + case TokenType::DoubleQuestionMark: + case TokenType::Equals: + case TokenType::EqualsEquals: + case TokenType::EqualsEqualsEquals: + case TokenType::ExclamationMark: + case TokenType::ExclamationMarkEquals: + case TokenType::ExclamationMarkEqualsEquals: + case TokenType::GreaterThan: + case TokenType::GreaterThanEquals: + case TokenType::LessThan: + case TokenType::LessThanEquals: + case TokenType::Minus: + case TokenType::MinusEquals: + case TokenType::MinusMinus: + case TokenType::Percent: + case TokenType::PercentEquals: + case TokenType::Pipe: + case TokenType::PipeEquals: + case TokenType::Plus: + case TokenType::PlusEquals: + case TokenType::PlusPlus: + case TokenType::QuestionMark: + case TokenType::QuestionMarkPeriod: + case TokenType::ShiftLeft: + case TokenType::ShiftLeftEquals: + case TokenType::ShiftRight: + case TokenType::ShiftRightEquals: + case TokenType::Slash: + case TokenType::SlashEquals: + case TokenType::Tilde: + case TokenType::UnsignedShiftRight: + case TokenType::UnsignedShiftRightEquals: + return StyleType::Operator; + case TokenType::BoolLiteral: + case TokenType::NullLiteral: + return StyleType::KeywordBold; + case TokenType::Class: + case TokenType::Const: + case TokenType::Debugger: + case TokenType::Delete: + case TokenType::Function: + case TokenType::In: + case TokenType::Instanceof: + case TokenType::Interface: + case TokenType::Let: + case TokenType::New: + case TokenType::TemplateLiteralExprStart: + case TokenType::TemplateLiteralExprEnd: + case TokenType::Throw: + case TokenType::Typeof: + case TokenType::Var: + case TokenType::Void: + return StyleType::Keyword; + case TokenType::Await: + case TokenType::Case: + case TokenType::Catch: + case TokenType::Do: + case TokenType::Else: + case TokenType::Finally: + case TokenType::For: + case TokenType::If: + case TokenType::Return: + case TokenType::Switch: + case TokenType::Try: + case TokenType::While: + case TokenType::Yield: + return StyleType::ControlKeyword; + case TokenType::Identifier: + return StyleType::Identifier; + default: + ASSERT_NOT_REACHED(); + } +} + +String MarkupGenerator::open_style_type(StyleType type) +{ + return String::format("", style_from_style_type(type).characters()); +} + +String MarkupGenerator::wrap_string_in_style(String source, StyleType type) +{ + return String::format("%s", style_from_style_type(type).characters(), source.characters()); +} + +} \ No newline at end of file diff --git a/Libraries/LibJS/MarkupGenerator.h b/Libraries/LibJS/MarkupGenerator.h new file mode 100644 index 0000000000..94909d50ea --- /dev/null +++ b/Libraries/LibJS/MarkupGenerator.h @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2020, Hunter Salyer + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +namespace JS { + +class MarkupGenerator { +public: + static String html_from_source(String); + static String html_from_value(Value); + +private: + enum class StyleType { + Invalid, + String, + Number, + KeywordBold, + Punctuation, + Operator, + Keyword, + ControlKeyword, + Identifier + }; + + static void value_to_html(Value, StringBuilder& output_html, HashTable seen_objects = {}); + static void array_to_html(const Array&, StringBuilder& output_html, HashTable&); + static void object_to_html(const Object&, StringBuilder& output_html, HashTable&); + static void function_to_html(const Object&, StringBuilder& output_html, HashTable&); + static void date_to_html(const Object&, StringBuilder& output_html, HashTable&); + static void error_to_html(const Object&, StringBuilder& output_html, HashTable&); + + static String style_from_style_type(StyleType); + static StyleType style_type_for_token(Token); + static String open_style_type(StyleType type); + static String wrap_string_in_style(String source, StyleType type); +}; + +} \ No newline at end of file