Explorar o código

LibMarkdown+LibSyntax: Add a Markdown syntax highlighter

It currently supports only headers and code blocks.
Maciej %!s(int64=2) %!d(string=hai) anos
pai
achega
cf52542fcf

+ 11 - 0
Userland/Applications/TextEditor/MainWidget.cpp

@@ -40,6 +40,7 @@
 #include <LibGfx/Painter.h>
 #include <LibJS/SyntaxHighlighter.h>
 #include <LibMarkdown/Document.h>
+#include <LibMarkdown/SyntaxHighlighter.h>
 #include <LibSQL/AST/SyntaxHighlighter.h>
 #include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
 #include <LibWeb/HTML/SyntaxHighlighter/SyntaxHighlighter.h>
@@ -672,6 +673,13 @@ ErrorOr<void> MainWidget::initialize_menubar(GUI::Window& window)
     syntax_actions.add_action(*m_ini_highlight);
     TRY(syntax_menu->try_add_action(*m_ini_highlight));
 
+    m_markdown_highlight = GUI::Action::create_checkable("Ma&rkdown", [&](auto&) {
+        m_editor->set_syntax_highlighter(make<Markdown::SyntaxHighlighter>());
+        m_editor->update();
+    });
+    syntax_actions.add_action(*m_markdown_highlight);
+    TRY(syntax_menu->try_add_action(*m_markdown_highlight));
+
     m_shell_highlight = GUI::Action::create_checkable("Sh&ell File", [&](auto&) {
         m_editor->set_syntax_highlighter(make<Shell::SyntaxHighlighter>());
         m_editor->update();
@@ -718,6 +726,7 @@ ErrorOr<void> MainWidget::initialize_menubar(GUI::Window& window)
     TRY(m_syntax_statusbar_menu->try_add_action(*m_html_highlight));
     TRY(m_syntax_statusbar_menu->try_add_action(*m_ini_highlight));
     TRY(m_syntax_statusbar_menu->try_add_action(*m_js_highlight));
+    TRY(m_syntax_statusbar_menu->try_add_action(*m_markdown_highlight));
     TRY(m_syntax_statusbar_menu->try_add_action(*m_shell_highlight));
     TRY(m_syntax_statusbar_menu->try_add_action(*m_sql_highlight));
 
@@ -752,6 +761,8 @@ void MainWidget::set_path(StringView path)
         m_gml_highlight->activate();
     } else if (m_extension == "ini" || m_extension == "af") {
         m_ini_highlight->activate();
+    } else if (m_extension == "md") {
+        m_markdown_highlight->activate();
     } else if (m_extension == "sh" || m_extension == "bash") {
         m_shell_highlight->activate();
     } else if (m_extension == "sql") {

+ 1 - 0
Userland/Applications/TextEditor/MainWidget.h

@@ -136,6 +136,7 @@ private:
     RefPtr<GUI::Action> m_git_highlight;
     RefPtr<GUI::Action> m_gml_highlight;
     RefPtr<GUI::Action> m_ini_highlight;
+    RefPtr<GUI::Action> m_markdown_highlight;
     RefPtr<GUI::Action> m_shell_highlight;
     RefPtr<GUI::Action> m_sql_highlight;
 

+ 6 - 2
Userland/DevTools/HackStudio/Editor.cpp

@@ -34,6 +34,7 @@
 #include <LibGUI/Window.h>
 #include <LibJS/SyntaxHighlighter.h>
 #include <LibMarkdown/Document.h>
+#include <LibMarkdown/SyntaxHighlighter.h>
 #include <LibSQL/AST/SyntaxHighlighter.h>
 #include <LibSyntax/Language.h>
 #include <LibWeb/CSS/SyntaxHighlighter/SyntaxHighlighter.h>
@@ -653,11 +654,14 @@ void Editor::set_syntax_highlighter_for(CodeDocument const& document)
     case Syntax::Language::HTML:
         set_syntax_highlighter(make<Web::HTML::SyntaxHighlighter>());
         break;
+    case Syntax::Language::INI:
+        set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>());
+        break;
     case Syntax::Language::JavaScript:
         set_syntax_highlighter(make<JS::SyntaxHighlighter>());
         break;
-    case Syntax::Language::INI:
-        set_syntax_highlighter(make<GUI::IniSyntaxHighlighter>());
+    case Syntax::Language::Markdown:
+        set_syntax_highlighter(make<Markdown::SyntaxHighlighter>());
         break;
     case Syntax::Language::Shell:
         set_syntax_highlighter(make<Shell::SyntaxHighlighter>());

+ 2 - 1
Userland/Libraries/LibMarkdown/CMakeLists.txt

@@ -9,9 +9,10 @@ set(SOURCES
     LineIterator.cpp
     List.cpp
     Paragraph.cpp
+    SyntaxHighlighter.cpp
     Table.cpp
     Text.cpp
 )
 
 serenity_lib(LibMarkdown markdown)
-target_link_libraries(LibMarkdown PRIVATE LibJS LibRegex)
+target_link_libraries(LibMarkdown PRIVATE LibJS LibRegex LibSyntax)

+ 103 - 0
Userland/Libraries/LibMarkdown/SyntaxHighlighter.cpp

@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023, Maciej <sppmacd@pm.me>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibMarkdown/SyntaxHighlighter.h>
+
+namespace Markdown {
+
+Syntax::Language SyntaxHighlighter::language() const
+{
+    return Syntax::Language::Markdown;
+}
+
+Optional<StringView> SyntaxHighlighter::comment_prefix() const
+{
+    return {};
+}
+
+Optional<StringView> SyntaxHighlighter::comment_suffix() const
+{
+    return {};
+}
+
+enum class Token {
+    Default,
+    Header,
+    Code
+};
+
+void SyntaxHighlighter::rehighlight(Palette const& palette)
+{
+    auto text = m_client->get_text();
+
+    Vector<GUI::TextDocumentSpan> spans;
+
+    auto append_header = [&](GUI::TextRange const& range) {
+        Gfx::TextAttributes attributes;
+        attributes.color = palette.base_text();
+        attributes.bold = true;
+        GUI::TextDocumentSpan span {
+            .range = range,
+            .attributes = attributes,
+            .data = static_cast<u32>(Token::Header),
+            .is_skippable = false
+        };
+        spans.append(span);
+    };
+
+    auto append_code_block = [&](GUI::TextRange const& range) {
+        Gfx::TextAttributes attributes;
+        attributes.color = palette.syntax_string();
+        GUI::TextDocumentSpan span {
+            .range = range,
+            .attributes = attributes,
+            .data = static_cast<u32>(Token::Code),
+            .is_skippable = false
+        };
+        spans.append(span);
+    };
+
+    // Headers, code blocks
+    {
+        size_t line_index = 0;
+        Optional<size_t> code_block_start;
+        for (auto const& line : StringView(text).lines()) {
+            if (line.starts_with("```"sv)) {
+                if (code_block_start.has_value()) {
+                    append_code_block({ { *code_block_start, 0 }, { line_index, line.length() } });
+                    code_block_start = {};
+                } else {
+                    code_block_start = line_index;
+                }
+            }
+
+            if (!code_block_start.has_value()) {
+                auto trimmed = line.trim_whitespace(TrimMode::Left);
+                size_t indent = line.length() - trimmed.length();
+                if (indent < 4 && trimmed.starts_with("#"sv)) {
+                    append_header({ { line_index, 0 }, { line_index, line.length() } });
+                }
+            }
+            line_index++;
+        }
+    }
+
+    // TODO: Highlight text nodes (em, strong, link, image)
+
+    m_client->do_set_spans(spans);
+}
+
+Vector<SyntaxHighlighter::MatchingTokenPair> SyntaxHighlighter::matching_token_pairs_impl() const
+{
+    return {};
+}
+
+bool SyntaxHighlighter::token_types_equal(u64 lhs, u64 rhs) const
+{
+    return static_cast<Token>(lhs) == static_cast<Token>(rhs);
+}
+
+}

+ 23 - 0
Userland/Libraries/LibMarkdown/SyntaxHighlighter.h

@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023, Maciej <sppmacd@pm.me>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibSyntax/Highlighter.h>
+
+namespace Markdown {
+
+class SyntaxHighlighter : public Syntax::Highlighter {
+
+    virtual Syntax::Language language() const override;
+    virtual Optional<StringView> comment_prefix() const override;
+    virtual Optional<StringView> comment_suffix() const override;
+    virtual void rehighlight(Palette const&) override;
+    virtual Vector<MatchingTokenPair> matching_token_pairs_impl() const override;
+    virtual bool token_types_equal(u64, u64) const override;
+};
+
+}

+ 9 - 1
Userland/Libraries/LibSyntax/Language.cpp

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020-2022, the SerenityOS developers.
+ * Copyright (c) 2020-2023, the SerenityOS developers.
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -32,6 +32,8 @@ StringView language_to_string(Language language)
         return "INI"sv;
     case Language::JavaScript:
         return "JavaScript"sv;
+    case Language::Markdown:
+        return "Markdown"sv;
     case Language::PlainText:
         return "Plain Text"sv;
     case Language::Shell:
@@ -63,6 +65,8 @@ StringView common_language_extension(Language language)
         return "ini"sv;
     case Language::JavaScript:
         return "js"sv;
+    case Language::Markdown:
+        return "md"sv;
     case Language::PlainText:
         return "txt"sv;
     case Language::Shell:
@@ -93,6 +97,8 @@ Optional<Language> language_from_name(StringView name)
         return Language::INI;
     if (name.equals_ignoring_ascii_case("JavaScript"sv))
         return Language::JavaScript;
+    if (name.equals_ignoring_ascii_case("Markdown"sv))
+        return Language::Markdown;
     if (name.equals_ignoring_ascii_case("PlainText"sv))
         return Language::PlainText;
     if (name.equals_ignoring_ascii_case("SQL"sv))
@@ -126,6 +132,8 @@ Optional<Language> language_from_filename(LexicalPath const& file)
         return Language::INI;
     if (extension.is_one_of("js"sv, "mjs"sv, "json"sv))
         return Language::JavaScript;
+    if (extension == "md"sv)
+        return Language::Markdown;
     if (extension.is_one_of("sh"sv, "bash"sv))
         return Language::Shell;
     if (extension == "sql"sv)

+ 1 - 0
Userland/Libraries/LibSyntax/Language.h

@@ -21,6 +21,7 @@ enum class Language {
     HTML,
     INI,
     JavaScript,
+    Markdown,
     PlainText,
     Shell,
     SQL,