summaryrefslogtreecommitdiffstats
path: root/src/ui/syntax.cpp
blob: 624bc46ed591db89773e1392591230745ddf665f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
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 :