In my attempts to create a GTK based Markdown Editor, I’ve been working on seeing how I can implement features many electron editors have natively. ThiefMD uses GtkSourceView to render its Markdown, so there’s no DOM to play with.
Hiding Links is a new feature added to ThiefMD.
This feature was accomplished with Gtk.TextTag’s and Regex. Today I want to show you how it’s done.
Setting Up Gtk.SourceView
I want to get into the Gtk.TextTag tricks, so here’s a skeleton template for using Gtk.SourceView
public class KMWriter : Gtk.Application {
private Gtk.SourceView source_view;
private Gtk.SourceBuffer source_buffer;
protected override void activate () {
// Grab application Window
var window = new Gtk.ApplicationWindow (this);
window.set_title ("1.6km Writer");
window.set_default_size (600, 320);
// Scroll view to hold contents
var scroll_box = new Gtk.ScrolledWindow (null, null);
// Get a pointer to the Markdown Language
var manager = Gtk.SourceLanguageManager.get_default ();
var language = manager.guess_language (null, "text/markdown");
// Create a GtkSourceView and create a markdown buffer
source_view = new Gtk.SourceView ();
source_view.margin = 0;
source_buffer = new Gtk.SourceBuffer.with_language (language);
source_buffer.highlight_syntax = true;
source_view.set_buffer (source_buffer);
source_view.set_wrap_mode (Gtk.WrapMode.WORD);
// Set placeholder text
source_buffer.text = "# Hello from 1.6km Writer\nDelete this text and start writing!";
// Add the GtkSourceView to the Scroll Box
scroll_box.add (source_view);
// Populate the Window
window.add (scroll_box);
window.show_all ();
}
public static int main (string[] args) {
return new KMWriter ().run (args);
}
}
If we compile and run this, then we’ll have a basic Markdown editor with Syntax Highlighting!
Finding Inline Links
Links in Markdown are formatted [LinkText](http://link.url)
. We want to identify and capture the text in the square brackets and parantheses. To do that we can use:
Regex is_link = new Regex ("\\[([^\\[]+?)\\](\\([^\\)\\n]+?\\))", RegexCompileFlags.CASELESS, 0);
Now we can write a function to grab the text in the buffer and scan it for links.
private void find_links () {
Gtk.TextIter buffer_start, buffer_end;
source_buffer.get_bounds (out buffer_start, out buffer_end);
// We want to include invisible characters, more on that later
string buffer_text = source_buffer.get_text (buffer_start, buffer_end, true);
// Check for links
MatchInfo matches;
if (is_link.match_full (buffer_text, buffer_text.length, 0, 0, out matches)) {
do {
int start_text_pos, end_text_pos;
int start_url_pos, end_url_pos;
bool have_text = matches.fetch_pos (1, out start_text_pos, out end_text_pos);
bool have_url = matches.fetch_pos (2, out start_url_pos, out end_url_pos);
if (have_text && have_url) {
// Do something?
}
} while (matches.next ());
}
}
This code will loop through all the links in the Markdown and let us do something with the links.
Styling with TextTag
Now that we have the positions of our link’s text and target, it’s time to style them. This is were we can consult the Valadoc on TextTag. If we look, there’s a background attribute that we can use for the link text. There’s also an invisible attribute we can use for the links.
In activate code, we can initialize some TextTags for use throughout our class.
// Members
private Gtk.TextTag markdown_link;
private Gtk.TextTag markdown_url;
/* ... */
protected override void activate () {
/* ..Inside activate after source_buffer initialization.. */
// Link Styles
markdown_link = source_buffer.create_tag ("markdown-link");
markdown_link.background = "#acf3ff";
markdown_link.background_set = true;
markdown_url = source_buffer.create_tag ("markdown-url");
markdown_url.invisible = true;
markdown_url.invisible_set = true;
We now have a tag that will make a link have a light blue background, and a tag to turn text invisible.
So let’s go back and do something with our regex matches.
if (have_text && have_url) {
// Convert byte offset to character offset in buffer (in case of emoji or unicode)
start_text_pos = buffer_text.char_count ((ssize_t) start_text_pos);
end_text_pos = buffer_text.char_count ((ssize_t) end_text_pos);
start_url_pos = buffer_text.char_count ((ssize_t) start_url_pos);
end_url_pos = buffer_text.char_count ((ssize_t) end_url_pos);
// Convert the character offsets to TextIter's
Gtk.TextIter start_text_iter, end_text_iter, start_url_iter, end_url_iter;
source_buffer.get_iter_at_offset (out start_text_iter, start_text_pos);
source_buffer.get_iter_at_offset (out end_text_iter, end_text_pos);
source_buffer.get_iter_at_offset (out start_url_iter, start_url_pos);
source_buffer.get_iter_at_offset (out end_url_iter, end_url_pos);
// Apply our styling
source_buffer.apply_tag (markdown_link, start_text_iter, end_text_iter);
source_buffer.apply_tag (markdown_url, start_url_iter, end_url_iter);
}
Inside our activate ()
code, we have to connect our buffer to the styling code.
// After initializing our source_buffer
source_buffer.changed.connect (find_links);
Now, if we type links in our editor, the URL’s will disappear 😎
Bringing it Back
If you’re playing with your editor at this point, you’re probably pretty ecstatic. You’re also probably pretty terrified that the link isn’t coming back. To do this, we need to clear the styles, and also check where the cursor is.
If the positions of the link contain the cursor position, we shouldn’t apply the styles.
Where we grab the buffer bounds, we also want to remove our styling and find our cursor
Gtk.TextIter buffer_start, buffer_end, cursor_location;
source_buffer.get_bounds (out buffer_start, out buffer_end);
source_buffer.remove_tag (markdown_link, buffer_start, buffer_end);
source_buffer.remove_tag (markdown_url, buffer_start, buffer_end);
var cursor = source_buffer.get_insert ();
source_buffer.get_iter_at_mark (out cursor_location, cursor);
and, where we put in the styling, we want to check the cursor position.
// Convert the character offsets to TextIter's
Gtk.TextIter start_text_iter, end_text_iter, start_url_iter, end_url_iter;
source_buffer.get_iter_at_offset (out start_text_iter, start_text_pos);
source_buffer.get_iter_at_offset (out end_text_iter, end_text_pos);
source_buffer.get_iter_at_offset (out start_url_iter, start_url_pos);
source_buffer.get_iter_at_offset (out end_url_iter, end_url_pos);
// Skip if our cursor is inside the URL text
if (cursor_location.in_range (start_text_iter, end_url_iter)) {
continue;
}
we also want to update on cursor location instead of buffer changed.
// source_buffer.changed.connect (find_links);
source_buffer.notify["cursor-position"].connect (find_links);
and now when we move our cursor:
If we want, we could do the same thing with bold formatting, italics, headings, and more 👨🍳
Wrapping it Up
Now we can make some money 😜 (although, getting rich off automated summaries didn’t go so well). I put the code for this project on GitHub in kmwriter. For homework, you can figure out how to build this project on mac OS or Windows.
In this series of posts, I’m hoping to go over creating something like a WYSIWYG editor for Markdown.
Tune in next time for when we cover Headers.