123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- /*
- * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
- * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
- #include <AK/StringBuilder.h>
- #include <LibJS/SyntaxHighlighter.h>
- #include <LibJS/Token.h>
- #include <LibURL/URL.h>
- #include <LibWeb/CSS/Parser/Token.h>
- #include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
- #include <LibWeb/DOMURL/DOMURL.h>
- #include <LibWeb/HTML/SyntaxHighlighter/SyntaxHighlighter.h>
- #include <LibWebView/SourceHighlighter.h>
- namespace WebView {
- SourceDocument::SourceDocument(StringView source)
- : m_source(source)
- {
- m_source.for_each_split_view('\n', AK::SplitBehavior::KeepEmpty, [&](auto line) {
- m_lines.append(Syntax::TextDocumentLine { *this, line });
- });
- }
- Syntax::TextDocumentLine& SourceDocument::line(size_t line_index)
- {
- return m_lines[line_index];
- }
- Syntax::TextDocumentLine const& SourceDocument::line(size_t line_index) const
- {
- return m_lines[line_index];
- }
- SourceHighlighterClient::SourceHighlighterClient(StringView source, Syntax::Language language)
- : m_document(SourceDocument::create(source))
- {
- // HACK: Syntax highlighters require a palette, but we don't actually care about the output styling, only the type of token for each span.
- // Also, getting a palette from the chrome is nontrivial. So, create a dummy blank one and use that.
- auto buffer = MUST(Core::AnonymousBuffer::create_with_size(sizeof(Gfx::SystemTheme)));
- auto palette_impl = Gfx::PaletteImpl::create_with_anonymous_buffer(buffer);
- Gfx::Palette dummy_palette { palette_impl };
- switch (language) {
- case Syntax::Language::CSS:
- m_highlighter = make<Web::CSS::SyntaxHighlighter>();
- break;
- case Syntax::Language::HTML:
- m_highlighter = make<Web::HTML::SyntaxHighlighter>();
- break;
- case Syntax::Language::JavaScript:
- m_highlighter = make<JS::SyntaxHighlighter>();
- break;
- default:
- break;
- }
- if (m_highlighter) {
- m_highlighter->attach(*this);
- m_highlighter->rehighlight(dummy_palette);
- }
- }
- Vector<Syntax::TextDocumentSpan> const& SourceHighlighterClient::spans() const
- {
- return document().spans();
- }
- void SourceHighlighterClient::set_span_at_index(size_t index, Syntax::TextDocumentSpan span)
- {
- document().set_span_at_index(index, span);
- }
- Vector<Syntax::TextDocumentFoldingRegion>& SourceHighlighterClient::folding_regions()
- {
- return document().folding_regions();
- }
- Vector<Syntax::TextDocumentFoldingRegion> const& SourceHighlighterClient::folding_regions() const
- {
- return document().folding_regions();
- }
- ByteString SourceHighlighterClient::highlighter_did_request_text() const
- {
- return document().text();
- }
- void SourceHighlighterClient::highlighter_did_request_update()
- {
- // No-op
- }
- Syntax::Document& SourceHighlighterClient::highlighter_did_request_document()
- {
- return document();
- }
- Syntax::TextPosition SourceHighlighterClient::highlighter_did_request_cursor() const
- {
- return {};
- }
- void SourceHighlighterClient::highlighter_did_set_spans(Vector<Syntax::TextDocumentSpan> spans)
- {
- document().set_spans(span_collection_index, move(spans));
- }
- void SourceHighlighterClient::highlighter_did_set_folding_regions(Vector<Syntax::TextDocumentFoldingRegion> folding_regions)
- {
- document().set_folding_regions(move(folding_regions));
- }
- String highlight_source(URL::URL const& url, URL::URL const& base_url, StringView source, Syntax::Language language, HighlightOutputMode mode)
- {
- SourceHighlighterClient highlighter_client { source, language };
- return highlighter_client.to_html_string(url, base_url, mode);
- }
- StringView SourceHighlighterClient::class_for_token(u64 token_type) const
- {
- auto class_for_css_token = [](u64 token_type) {
- switch (static_cast<Web::CSS::Parser::Token::Type>(token_type)) {
- case Web::CSS::Parser::Token::Type::Invalid:
- case Web::CSS::Parser::Token::Type::BadString:
- case Web::CSS::Parser::Token::Type::BadUrl:
- return "invalid"sv;
- case Web::CSS::Parser::Token::Type::Ident:
- return "identifier"sv;
- case Web::CSS::Parser::Token::Type::Function:
- return "function"sv;
- case Web::CSS::Parser::Token::Type::AtKeyword:
- return "at-keyword"sv;
- case Web::CSS::Parser::Token::Type::Hash:
- return "hash"sv;
- case Web::CSS::Parser::Token::Type::String:
- return "string"sv;
- case Web::CSS::Parser::Token::Type::Url:
- return "url"sv;
- case Web::CSS::Parser::Token::Type::Number:
- case Web::CSS::Parser::Token::Type::Dimension:
- case Web::CSS::Parser::Token::Type::Percentage:
- return "number"sv;
- case Web::CSS::Parser::Token::Type::Whitespace:
- return "whitespace"sv;
- case Web::CSS::Parser::Token::Type::Delim:
- case Web::CSS::Parser::Token::Type::Colon:
- case Web::CSS::Parser::Token::Type::Semicolon:
- case Web::CSS::Parser::Token::Type::Comma:
- case Web::CSS::Parser::Token::Type::OpenSquare:
- case Web::CSS::Parser::Token::Type::CloseSquare:
- case Web::CSS::Parser::Token::Type::OpenParen:
- case Web::CSS::Parser::Token::Type::CloseParen:
- case Web::CSS::Parser::Token::Type::OpenCurly:
- case Web::CSS::Parser::Token::Type::CloseCurly:
- return "delimiter"sv;
- case Web::CSS::Parser::Token::Type::CDO:
- case Web::CSS::Parser::Token::Type::CDC:
- return "comment"sv;
- case Web::CSS::Parser::Token::Type::EndOfFile:
- default:
- break;
- }
- return ""sv;
- };
- auto class_for_js_token = [](u64 token_type) {
- auto category = JS::Token::category(static_cast<JS::TokenType>(token_type));
- switch (category) {
- case JS::TokenCategory::Invalid:
- return "invalid"sv;
- case JS::TokenCategory::Number:
- return "number"sv;
- case JS::TokenCategory::String:
- return "string"sv;
- case JS::TokenCategory::Punctuation:
- return "punctuation"sv;
- case JS::TokenCategory::Operator:
- return "operator"sv;
- case JS::TokenCategory::Keyword:
- return "keyword"sv;
- case JS::TokenCategory::ControlKeyword:
- return "control-keyword"sv;
- case JS::TokenCategory::Identifier:
- return "identifier"sv;
- default:
- break;
- }
- return ""sv;
- };
- switch (m_highlighter->language()) {
- case Syntax::Language::CSS:
- return class_for_css_token(token_type);
- case Syntax::Language::JavaScript:
- return class_for_js_token(token_type);
- case Syntax::Language::HTML: {
- // HTML has nested CSS and JS highlighters, so we have to decode their token types.
- // HTML
- if (token_type < Web::HTML::SyntaxHighlighter::JS_TOKEN_START_VALUE) {
- switch (static_cast<Web::HTML::AugmentedTokenKind>(token_type)) {
- case Web::HTML::AugmentedTokenKind::AttributeName:
- return "attribute-name"sv;
- case Web::HTML::AugmentedTokenKind::AttributeValue:
- return "attribute-value"sv;
- case Web::HTML::AugmentedTokenKind::OpenTag:
- case Web::HTML::AugmentedTokenKind::CloseTag:
- return "tag"sv;
- case Web::HTML::AugmentedTokenKind::Comment:
- return "comment"sv;
- case Web::HTML::AugmentedTokenKind::Doctype:
- return "doctype"sv;
- case Web::HTML::AugmentedTokenKind::__Count:
- default:
- return ""sv;
- }
- }
- // JS
- if (token_type < Web::HTML::SyntaxHighlighter::CSS_TOKEN_START_VALUE) {
- return class_for_js_token(token_type - Web::HTML::SyntaxHighlighter::JS_TOKEN_START_VALUE);
- }
- // CSS
- return class_for_css_token(token_type - Web::HTML::SyntaxHighlighter::CSS_TOKEN_START_VALUE);
- }
- default:
- return "unknown"sv;
- }
- }
- String SourceHighlighterClient::to_html_string(URL::URL const& url, URL::URL const& base_url, HighlightOutputMode mode) const
- {
- StringBuilder builder;
- auto append_escaped = [&](Utf32View text) {
- for (auto code_point : text) {
- if (code_point == '&') {
- builder.append("&"sv);
- } else if (code_point == 0xA0) {
- builder.append(" "sv);
- } else if (code_point == '<') {
- builder.append("<"sv);
- } else if (code_point == '>') {
- builder.append(">"sv);
- } else {
- builder.append_code_point(code_point);
- }
- }
- };
- auto start_token = [&](u64 type) {
- builder.appendff("<span class=\"{}\">", class_for_token(type));
- };
- auto end_token = [&]() {
- builder.append("</span>"sv);
- };
- if (mode == HighlightOutputMode::FullDocument) {
- builder.append(R"~~~(
- <!DOCTYPE html>
- <html>
- <head>
- <meta name="color-scheme" content="dark light">)~~~"sv);
- builder.appendff("<title>View Source - {}</title>", escape_html_entities(url.serialize_for_display()));
- builder.appendff("<style type=\"text/css\">{}</style>", HTML_HIGHLIGHTER_STYLE);
- builder.append(R"~~~(
- </head>
- <body>)~~~"sv);
- }
- builder.append("<pre class=\"html\">"sv);
- static constexpr auto href = to_array<u32>({ 'h', 'r', 'e', 'f' });
- static constexpr auto src = to_array<u32>({ 's', 'r', 'c' });
- bool linkify_attribute = false;
- auto resolve_url_for_attribute = [&](Utf32View const& attribute_value) -> Optional<URL::URL> {
- if (!linkify_attribute)
- return {};
- auto attribute_url = MUST(String::formatted("{}", attribute_value));
- auto attribute_url_without_quotes = attribute_url.bytes_as_string_view().trim("\""sv);
- if (auto resolved = Web::DOMURL::parse(attribute_url_without_quotes, base_url); resolved.is_valid())
- return resolved;
- return {};
- };
- size_t span_index = 0;
- for (size_t line_index = 0; line_index < document().line_count(); ++line_index) {
- auto& line = document().line(line_index);
- auto line_view = line.view();
- builder.append("<div class=\"line\">"sv);
- size_t next_column = 0;
- auto draw_text_helper = [&](size_t start, size_t end, Optional<Syntax::TextDocumentSpan const&> span) {
- size_t length = end - start;
- if (length == 0)
- return;
- auto text = line_view.substring_view(start, length);
- if (span.has_value()) {
- bool append_anchor_close = false;
- if (span->data == to_underlying(Web::HTML::AugmentedTokenKind::AttributeName)) {
- linkify_attribute = text == Utf32View { href } || text == Utf32View { src };
- } else if (span->data == to_underlying(Web::HTML::AugmentedTokenKind::AttributeValue)) {
- if (auto href = resolve_url_for_attribute(text); href.has_value()) {
- builder.appendff("<a href=\"{}\">", *href);
- append_anchor_close = true;
- }
- }
- start_token(span->data);
- append_escaped(text);
- end_token();
- if (append_anchor_close)
- builder.append("</a>"sv);
- } else {
- append_escaped(text);
- }
- };
- while (span_index < document().spans().size()) {
- auto& span = document().spans()[span_index];
- if (span.range.start().line() > line_index) {
- // No more spans in this line, moving on
- break;
- }
- size_t span_start;
- if (span.range.start().line() < line_index) {
- span_start = 0;
- } else {
- span_start = span.range.start().column();
- }
- size_t span_end;
- bool span_consumed;
- if (span.range.end().line() > line_index) {
- span_end = line.length();
- span_consumed = false;
- } else {
- span_end = span.range.end().column();
- span_consumed = true;
- }
- if (span_start != next_column) {
- // Draw unspanned text between spans
- draw_text_helper(next_column, span_start, {});
- }
- draw_text_helper(span_start, span_end, span);
- next_column = span_end;
- if (!span_consumed) {
- // Continue with same span on next line
- break;
- } else {
- ++span_index;
- }
- }
- // Draw unspanned text after last span
- if (next_column < line.length()) {
- draw_text_helper(next_column, line.length(), {});
- }
- builder.append("</div>"sv);
- }
- builder.append("</pre>"sv);
- if (mode == HighlightOutputMode::FullDocument) {
- builder.append(R"~~~(
- </body>
- </html>
- )~~~"sv);
- }
- return builder.to_string_without_validation();
- }
- }
|