// SPDX-License-Identifier: GPL-2.0-or-later /** @file Syntax coloring via Gtksourceview and Pango markup. */ /* Authors: * Rafael Siejakowski * Mike Kowalski * * Copyright (C) 2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "ui/syntax.h" #include #include #include #include #include "color.h" #include "config.h" #include "io/resource.h" #include "object/sp-factory.h" #include "util/trim.h" #if WITH_GSOURCEVIEW # include #endif namespace Inkscape::UI::Syntax { Glib::ustring XMLFormatter::_format(Style const &style, Glib::ustring const &content) const { return _format(style, content.c_str()); } /** Get the opening tag of the Pango markup for this style. */ Glib::ustring Style::openingTag() const { if (isDefault()) { return ""; } std::ostringstream ost; ost << "raw() << '"'; } if (background) { ost << " bgcolor=\"" << background->raw() << '"'; } if (bold) { ost << " weight=\"bold\""; } if (italic) { ost << " font_style=\"italic\""; } if (underline) { ost << " underline=\"single\""; } ost << ">"; return Glib::ustring(ost.str()); } /** Get the closing tag of Pango markup for this style. */ Glib::ustring Style::closingTag() const { return isDefault() ? "" : ""; } Glib::ustring quote(const char* text) { return Glib::ustring::compose("\"%1\"", text); } /** Open a new XML tag with the given tag name. */ void XMLFormatter::openTag(char const *tag_name) { _wip = _format(_style.angular_brackets, "<"); // Highlight as errors unsupported tags in SVG namespace (explicit or implicit). bool error = false; std::string fully_qualified_name(tag_name); if (fully_qualified_name.empty()) { return; } bool is_svg = false; if (fully_qualified_name.find(':') == std::string::npos) { fully_qualified_name = std::string("svg:") + fully_qualified_name; is_svg = true; } else if (fully_qualified_name.find("svg:") == 0) { is_svg = true; } if (is_svg && !SPFactory::supportsType(fully_qualified_name)) { error = true; } _wip += _format(error ? _style.error : _style.tag_name, tag_name); } void XMLFormatter::addAttribute(char const *name, char const *value) { _wip += Glib::ustring::compose(" %1%2%3", _format(_style.attribute_name, name), _format(_style.angular_brackets, "="), _format(_style.attribute_value, quote(value))); } Glib::ustring XMLFormatter::finishTag(bool self_close) { return _wip + _format(_style.angular_brackets, self_close ? "/>" : ">"); } Glib::ustring XMLFormatter::formatContent(char const* content, bool wrap_in_quotes) const { Glib::ustring text = wrap_in_quotes ? quote(content) : content; return _format(_style.content, text); } Glib::ustring XMLFormatter::formatComment(char const* comment, bool wrap_in_marks) const { if (wrap_in_marks) { auto wrapped = Glib::ustring::compose("", comment); return _format(_style.comment, wrapped.c_str()); } return _format(_style.comment, comment); } XMLStyles build_xml_styles(const Glib::ustring& syntax_theme) { XMLStyles styles; #if WITH_GSOURCEVIEW auto manager = gtk_source_style_scheme_manager_get_default(); if (auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, syntax_theme.c_str())) { auto get_color = [](GtkSourceStyle* style, const char* prop) -> std::optional { std::optional maybe_color; Glib::ustring name(prop); gboolean set; gchar* color = 0; g_object_get(style, (name + "-set").c_str(), &set, name.c_str(), &color, nullptr); if (set && color && *color == '#') { maybe_color = Glib::ustring(color); } g_free(color); return maybe_color; }; auto get_bool = [](GtkSourceStyle* style, const char* prop, bool def = false) -> bool { Glib::ustring name(prop); gboolean set; gboolean flag; g_object_get(style, (name + "-set").c_str(), &set, name.c_str(), &flag, nullptr); return set ? !!flag : def; }; auto get_underline = [](GtkSourceStyle* style, bool def = false) -> bool { Glib::ustring name("underline"); gboolean set; PangoUnderline underline; g_object_get(style, (name + "-set").c_str(), &set, ("pango-" + name).c_str(), &underline, nullptr); return set ? underline != PANGO_UNDERLINE_NONE : def; }; auto to_style = [&](char const *id) -> Style { auto s = gtk_source_style_scheme_get_style(scheme, id); if (!s) { return Style(); } Style style; style.color = get_color(s, "foreground"); style.background = get_color(s, "background"); style.bold = get_bool(s, "bold"); style.italic = get_bool(s, "italic"); style.underline = get_underline(s); return style; }; styles.tag_name = to_style("def:statement"); styles.attribute_name = to_style("def:number"); styles.attribute_value = to_style("def:string"); styles.content = to_style("def:string"); styles.comment = to_style("def:comment"); styles.prolog = to_style("def:warning"); styles.angular_brackets = to_style("draw-spaces"); styles.error = to_style("def:error"); } #endif return styles; } /** @brief Reformat CSS for better readability. */ Glib::ustring prettify_css(Glib::ustring const &css) { // Ensure that there's a space after every colon, unless there's a slash (as in a URL). static auto const colon_without_space = Glib::Regex::create(":([^\\s\\/])"); auto reformatted = colon_without_space->replace(css, 0, ": \\1", Glib::RegexMatchFlags::REGEX_MATCH_NOTEMPTY); // Ensure that there's a newline after every semicolon. static auto const semicolon_without_newline = Glib::Regex::create(";([^\r\n])"); reformatted = semicolon_without_newline->replace(reformatted, 0, ";\n\\1", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANYCRLF); // If the last character is not a semicolon, append one. if (auto len = css.size(); len && css[len - 1] != ';') { reformatted += ";"; } return reformatted; } /** Undo the CSS prettification by stripping some whitespace from CSS markup. */ Glib::ustring minify_css(Glib::ustring const &css) { static auto const space_after = Glib::Regex::create("(:|;)[\\s]+"); auto minified = space_after->replace(css, 0, "\\1", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY); // Strip final semicolon if (auto const len = minified.size(); len && minified[len - 1] == ';') { minified = minified.erase(len - 1); } return minified; } /** @brief Reformat a path 'd' attibute for better readability. */ Glib::ustring prettify_svgd(Glib::ustring const &d) { auto result = d; Util::trim(result); // Ensure that a non-M command is preceded only by a newline. static auto const space_b4_command = Glib::Regex::create("(?<=\\S)\\s*(?=[LHVCSQTAZlhvcsqtaz])"); result = space_b4_command->replace(result, 1, "\n", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY); // Before a non-initial M command, we want to have two newlines to visually separate the subpaths. static auto const space_b4_m = Glib::Regex::create("(?<=\\S)\\s*(?=[Mm])"); result = space_b4_m->replace(result, 1, "\n\n", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY); // Ensure that there's a space after each command letter other than Z. static auto const nospace = Glib::Regex::create("([MLHVCSQTAmlhvcsqta])(?=\\S)"); return nospace->replace(result, 0, "\\1 ", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY); } /** @brief Remove excessive space, including newlines, from a path 'd' attibute. */ Glib::ustring minify_svgd(Glib::ustring const &d) { static auto const excessive_space = Glib::Regex::create("[\\s]+"); auto result = excessive_space->replace(d, 0, " ", Glib::RegexMatchFlags::REGEX_MATCH_NEWLINE_ANY); Util::trim(result); return result; } /** Set default options on a TextView widget used for syntax-colored editing. */ static void init_text_view(Gtk::TextView* textview) { textview->set_wrap_mode(Gtk::WrapMode::WRAP_WORD); textview->set_editable(true); textview->show(); } /// Plain text view widget without syntax coloring class PlainTextView : public TextEditView { public: PlainTextView() : _textview(std::make_unique(Gtk::TextBuffer::create())) { init_text_view(_textview.get()); } void setStyle(const Glib::ustring& theme) override { /* no op */ } void setText(const Glib::ustring& text) override { _textview->get_buffer()->set_text(text); } Glib::ustring getText() const override { return _textview->get_buffer()->get_text(); } Gtk::TextView& getTextView() const override { return *_textview; } private: std::unique_ptr _textview; }; #if WITH_GSOURCEVIEW /** @brief Return a pointer to a language manager which is aware of both * default and custom syntaxes. */ static GtkSourceLanguageManager* get_language_manager() { auto ui_path = IO::Resource::get_path_string(IO::Resource::SYSTEM, IO::Resource::UIS); auto default_manager = gtk_source_language_manager_get_default(); auto default_paths = gtk_source_language_manager_get_search_path(default_manager); std::vector all_paths; for (auto path = default_paths; *path; path++) { all_paths.push_back(*path); } all_paths.push_back(ui_path.c_str()); all_paths.push_back(nullptr); auto result = gtk_source_language_manager_new(); gtk_source_language_manager_set_search_path(result, (gchar **)all_paths.data()); return result; } class SyntaxHighlighting : public TextEditView { public: SyntaxHighlighting() = delete; /** @brief Construct a syntax highlighter for a given language. */ SyntaxHighlighting(char const* const language, Glib::ustring (*prettify_func)(Glib::ustring const &), Glib::ustring (*minify_func)(Glib::ustring const &)) : _prettify{prettify_func} , _minify{minify_func} { auto manager = get_language_manager(); auto lang = gtk_source_language_manager_get_language(manager, language); _buffer = gtk_source_buffer_new_with_language(lang); auto view = gtk_source_view_new_with_buffer(_buffer); // Increment Glib's internal refcount to prevent the destruction of the // textview by a parent widget (if any); the textview is owned by us! g_object_ref(view); _textview = std::unique_ptr(Glib::wrap((GtkTextView*)view)); if (!_textview) { // don't crash when sourceview cannot be created; substitute with a regular one; // in this case GTK has already outputted warnings _textview = std::make_unique(Gtk::TextBuffer::create()); } init_text_view(_textview.get()); } ~SyntaxHighlighting() override { g_object_unref(_buffer); } private: GtkSourceBuffer *_buffer = nullptr; // Owned by us std::unique_ptr _textview; Glib::ustring (*_prettify)(Glib::ustring const &); Glib::ustring (*_minify)(Glib::ustring const &); public: void setStyle(Glib::ustring const &theme) override { auto manager = gtk_source_style_scheme_manager_get_default(); auto scheme = gtk_source_style_scheme_manager_get_scheme(manager, theme.c_str()); gtk_source_buffer_set_style_scheme(_buffer, scheme); } /** @brief Set the displayed text to a prettified version of the passed string. */ void setText(Glib::ustring const &text) override { _textview->get_buffer()->set_text(_prettify(text)); } /** @brief Get a minified version of the buffer contents, suitable for inserting into XML. */ Glib::ustring getText() const override { return _minify(_textview->get_buffer()->get_text()); } Gtk::TextView &getTextView() const override { return *_textview; }; }; #endif // WITH_GSOURCEVIEW /** Create a styled text view using the desired syntax highlighting mode. */ std::unique_ptr TextEditView::create(SyntaxMode mode) { #if WITH_GSOURCEVIEW auto const no_reformat = [](auto &s) { return s; }; switch (mode) { case SyntaxMode::PlainText: return std::make_unique(); case SyntaxMode::InlineCss: return std::make_unique("inline-css", &prettify_css, &minify_css); case SyntaxMode::CssStyle: return std::make_unique("css", no_reformat, no_reformat); case SyntaxMode::SvgPathData: return std::make_unique("svgd", &prettify_svgd, &minify_svgd); case SyntaxMode::SvgPolyPoints: return std::make_unique("svgpoints", no_reformat, no_reformat); default: throw std::runtime_error("Missing case statement in TetxEditView::create()"); } #else return std::make_unique(); #endif } } // namespace Inkscape::UI::Syntax /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :