summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Atkins <atkinssj@serenityos.org>2021-10-21 21:29:10 +0100
committerAndreas Kling <kling@serenityos.org>2021-10-23 19:07:44 +0200
commit38f6140159f7a4737adcfa52af1cfdb94e23d63a (patch)
treeda169727b86e9508736867278ac27965a64af9b7
parentecf53685352e2dfc534a96be07b1c3727319389f (diff)
downloadserenity-38f6140159f7a4737adcfa52af1cfdb94e23d63a.zip
LibWeb: Implement first draft of CSS syntax highlighting :^)
This works at the Token level, which is quick and easy but has drawbacks: We don't know when something is a property name or a value, or if something is part of a selector. But, this works for now.
-rw-r--r--Userland/Libraries/LibSyntax/Highlighter.h1
-rw-r--r--Userland/Libraries/LibWeb/CMakeLists.txt1
-rw-r--r--Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.cpp160
-rw-r--r--Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h29
4 files changed, 191 insertions, 0 deletions
diff --git a/Userland/Libraries/LibSyntax/Highlighter.h b/Userland/Libraries/LibSyntax/Highlighter.h
index 7a2541f96e..04cd49096b 100644
--- a/Userland/Libraries/LibSyntax/Highlighter.h
+++ b/Userland/Libraries/LibSyntax/Highlighter.h
@@ -16,6 +16,7 @@ namespace Syntax {
enum class Language {
Cpp,
+ CSS,
GML,
HTML,
INI,
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index 5334e05ec6..7683b1f884 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -49,6 +49,7 @@ set(SOURCES
CSS/StyleSheetList.cpp
CSS/StyleValue.cpp
CSS/Supports.cpp
+ CSS/SyntaxHighlighter/SyntaxHighlighter.cpp
CSS/ValueID.cpp
CSS/ValueID.h
Cookie/ParsedCookie.cpp
diff --git a/Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.cpp b/Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.cpp
new file mode 100644
index 0000000000..e763cac014
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.cpp
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Debug.h>
+#include <LibWeb/CSS/Parser/Tokenizer.h>
+#include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
+
+namespace Web::CSS {
+
+bool SyntaxHighlighter::is_identifier(u64 token) const
+{
+ return static_cast<CSS::Token::Type>(token) == CSS::Token::Type::Ident;
+}
+
+bool SyntaxHighlighter::is_navigatable(u64) const
+{
+ return false;
+}
+
+void SyntaxHighlighter::rehighlight(Palette const& palette)
+{
+ dbgln_if(SYNTAX_HIGHLIGHTING_DEBUG, "(CSS::SyntaxHighlighter) starting rehighlight");
+ auto text = m_client->get_text();
+
+ Vector<GUI::TextDocumentSpan> spans;
+
+ auto highlight = [&](auto start_line, auto start_column, auto end_line, auto end_column, Gfx::TextAttributes attributes, CSS::Token::Type type) {
+ if (start_line > end_line || (start_line == end_line && start_column >= end_column)) {
+ dbgln_if(SYNTAX_HIGHLIGHTING_DEBUG, "(CSS::SyntaxHighlighter) discarding ({}-{}) to ({}-{}) because it has zero or negative length", start_line, start_column, end_line, end_column);
+ return;
+ }
+ dbgln_if(SYNTAX_HIGHLIGHTING_DEBUG, "(CSS::SyntaxHighlighter) highlighting ({}-{}) to ({}-{}) with color {}", start_line, start_column, end_line, end_column, attributes.color);
+ spans.empend(
+ GUI::TextRange {
+ { start_line, start_column },
+ { end_line, end_column },
+ },
+ move(attributes),
+ static_cast<u64>(type),
+ false);
+ };
+
+ CSS::Tokenizer tokenizer { text, "utf-8" };
+ auto tokens = tokenizer.parse();
+ for (auto const& token : tokens) {
+ if (token.is(Token::Type::EndOfFile))
+ break;
+
+ switch (token.type()) {
+ case Token::Type::Ident:
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_identifier(), {} }, token.type());
+ break;
+
+ case Token::Type::String:
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_string(), {} }, token.type());
+ break;
+
+ case Token::Type::Whitespace:
+ // CSS doesn't produce comment tokens, they're just included as part of whitespace.
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_comment(), {} }, token.type());
+ break;
+
+ case Token::Type::AtKeyword:
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_keyword(), {} }, token.type());
+ break;
+
+ case Token::Type::Function:
+ // Function tokens include the opening '(', so we split that into two tokens for highlighting purposes.
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column - 1, { palette.syntax_keyword(), {} }, token.type());
+ highlight(token.end_position().line, token.end_position().column - 1, token.end_position().line, token.end_position().column, { palette.syntax_punctuation(), {} }, Token::Type::OpenParen);
+ break;
+
+ case Token::Type::Url:
+ // An Url token is a `url()` function with its parameter string unquoted.
+ // url
+ highlight(token.start_position().line, token.start_position().column, token.start_position().line, token.start_position().column + 3, { palette.syntax_keyword(), {} }, token.type());
+ // (
+ highlight(token.start_position().line, token.start_position().column + 3, token.start_position().line, token.start_position().column + 4, { palette.syntax_punctuation(), {} }, Token::Type::OpenParen);
+ // <string>
+ highlight(token.start_position().line, token.start_position().column + 4, token.end_position().line, token.end_position().column - 1, { palette.syntax_string(), {} }, Token::Type::String);
+ // )
+ highlight(token.end_position().line, token.end_position().column - 1, token.end_position().line, token.end_position().column, { palette.syntax_punctuation(), {} }, Token::Type::CloseParen);
+ break;
+
+ case Token::Type::Number:
+ case Token::Type::Dimension:
+ case Token::Type::Percentage:
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_number(), {} }, token.type());
+ break;
+
+ case Token::Type::Delim:
+ case Token::Type::Colon:
+ case Token::Type::Comma:
+ case Token::Type::Semicolon:
+ case Token::Type::OpenCurly:
+ case Token::Type::OpenParen:
+ case Token::Type::OpenSquare:
+ case Token::Type::CloseCurly:
+ case Token::Type::CloseParen:
+ case Token::Type::CloseSquare:
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_punctuation(), {} }, token.type());
+ break;
+
+ case Token::Type::CDO:
+ case Token::Type::CDC:
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_comment(), {} }, token.type());
+ break;
+
+ case Token::Type::Hash:
+ // FIXME: Hash tokens can be ID selectors or colors, we don't know which without parsing properly.
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { palette.syntax_number(), {} }, token.type());
+ break;
+
+ case Token::Type::Invalid:
+ case Token::Type::BadUrl:
+ case Token::Type::BadString:
+ // FIXME: Error highlighting color in palette?
+ highlight(token.start_position().line, token.start_position().column, token.end_position().line, token.end_position().column, { Color(Color::NamedColor::Red), {}, false, true }, token.type());
+ break;
+
+ case Token::Type::EndOfFile:
+ default:
+ break;
+ }
+ }
+
+ if constexpr (SYNTAX_HIGHLIGHTING_DEBUG) {
+ dbgln("(CSS::SyntaxHighlighter) list of all spans:");
+ for (auto& span : spans)
+ dbgln("{}, {} - {}", span.range, span.attributes.color, span.data);
+ dbgln("(CSS::SyntaxHighlighter) end of list");
+ }
+
+ m_client->do_set_spans(move(spans));
+ m_has_brace_buddies = false;
+ highlight_matching_token_pair();
+ m_client->do_update();
+}
+
+Vector<Syntax::Highlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
+{
+ static Vector<Syntax::Highlighter::MatchingTokenPair> pairs;
+ if (pairs.is_empty()) {
+ pairs.append({ static_cast<u64>(CSS::Token::Type::OpenCurly), static_cast<u64>(CSS::Token::Type::CloseCurly) });
+ pairs.append({ static_cast<u64>(CSS::Token::Type::OpenParen), static_cast<u64>(CSS::Token::Type::CloseParen) });
+ pairs.append({ static_cast<u64>(CSS::Token::Type::OpenSquare), static_cast<u64>(CSS::Token::Type::CloseSquare) });
+ pairs.append({ static_cast<u64>(CSS::Token::Type::CDO), static_cast<u64>(CSS::Token::Type::CDC) });
+ }
+ return pairs;
+}
+
+bool SyntaxHighlighter::token_types_equal(u64 token0, u64 token1) const
+{
+ return token0 == token1;
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h b/Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h
new file mode 100644
index 0000000000..0f7669f730
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibSyntax/Highlighter.h>
+
+namespace Web::CSS {
+
+class SyntaxHighlighter : public Syntax::Highlighter {
+public:
+ SyntaxHighlighter() = default;
+ virtual ~SyntaxHighlighter() override = default;
+
+ virtual bool is_identifier(u64) const override;
+ virtual bool is_navigatable(u64) const override;
+
+ virtual Syntax::Language language() const override { return Syntax::Language::CSS; }
+ virtual void rehighlight(Palette const&) override;
+
+protected:
+ virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
+ virtual bool token_types_equal(u64, u64) const override;
+};
+
+}