summaryrefslogtreecommitdiffstats
path: root/src/ui/syntax.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/ui/syntax.cpp399
1 files changed, 399 insertions, 0 deletions
diff --git a/src/ui/syntax.cpp b/src/ui/syntax.cpp
new file mode 100644
index 0000000..624bc46
--- /dev/null
+++ b/src/ui/syntax.cpp
@@ -0,0 +1,399 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** @file Syntax coloring via Gtksourceview and Pango markup.
+ */
+/* Authors:
+ * Rafael Siejakowski <rs@rs-math.net>
+ * Mike Kowalski
+ *
+ * Copyright (C) 2022 Authors
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "ui/syntax.h"
+
+#include <glibmm/ustring.h>
+#include <pango/pango-attributes.h>
+#include <sstream>
+#include <stdexcept>
+
+#include "color.h"
+#include "config.h"
+#include "io/resource.h"
+#include "object/sp-factory.h"
+#include "util/trim.h"
+
+#if WITH_GSOURCEVIEW
+# include <gtksourceview/gtksource.h>
+#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 << "<span";
+ if (color) {
+ ost << " color=\"" << color->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() ? "" : "</span>";
+}
+
+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("<!--%1-->", 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<Glib::ustring> {
+ std::optional<Glib::ustring> 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::TextView>(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<Gtk::TextView> _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<char const *> 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<Gtk::TextView>(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::TextView>(Gtk::TextBuffer::create());
+ }
+ init_text_view(_textview.get());
+ }
+
+ ~SyntaxHighlighting() override { g_object_unref(_buffer); }
+private:
+ GtkSourceBuffer *_buffer = nullptr; // Owned by us
+ std::unique_ptr<Gtk::TextView> _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> TextEditView::create(SyntaxMode mode)
+{
+#if WITH_GSOURCEVIEW
+ auto const no_reformat = [](auto &s) { return s; };
+ switch (mode) {
+ case SyntaxMode::PlainText:
+ return std::make_unique<PlainTextView>();
+ case SyntaxMode::InlineCss:
+ return std::make_unique<SyntaxHighlighting>("inline-css", &prettify_css, &minify_css);
+ case SyntaxMode::CssStyle:
+ return std::make_unique<SyntaxHighlighting>("css", no_reformat, no_reformat);
+ case SyntaxMode::SvgPathData:
+ return std::make_unique<SyntaxHighlighting>("svgd", &prettify_svgd, &minify_svgd);
+ case SyntaxMode::SvgPolyPoints:
+ return std::make_unique<SyntaxHighlighting>("svgpoints", no_reformat, no_reformat);
+ default:
+ throw std::runtime_error("Missing case statement in TetxEditView::create()");
+ }
+#else
+ return std::make_unique<PlainTextView>();
+#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 :