talkatu/talkatu

803afd860d6f
Add a devenv that setups up the glade catalog

Testing Done:
ran `meson devenv glade` and verified the catalog was loaded properly.

Reviewed at https://reviews.imfreedom.org/r/1045/
/*
* Talkatu - GTK widgets for chat applications
* Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this library; if not, see <https://www.gnu.org/licenses/>.
*/
#include <gumbo.h>
#include <talkatu/talkatucodeset.h>
#include <talkatu/talkatumarkup.h>
#include <talkatu/talkatutag.h>
/**
* SECTION:talkatumarkup
* @Title: Markup Helpers
* @Short_description: Helpers for reading/writing markup to GtkTextBuffers.
*
* This API provides the ability to serialize and deserialize mark to/from
* GtkTextBuffers.
*/
/******************************************************************************
* Helpers
*****************************************************************************/
static gchar *
talkatu_text_tag_get_name(GtkTextTag *tag) {
gchar *name = NULL;
g_object_get(G_OBJECT(tag), "name", &name, NULL);
return name;
}
/******************************************************************************
* HTML
*****************************************************************************/
static gint
talkatu_markup_deserialize_html_helper(GtkTextBuffer *buffer,
GtkTextIter *pos,
GumboNode *node,
GString *str)
{
TalkatuTagDisplay display = TALKATU_TAG_DISPLAY_INLINE;
GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);
GtkTextTag *tag = NULL;
GumboVector *children = NULL;
const gchar *tag_name = NULL;
gint i = 0, start_pos, length = 0;
start_pos = gtk_text_iter_get_offset(pos);
if(node->type == GUMBO_NODE_ELEMENT) {
switch(node->v.element.tag) {
case GUMBO_TAG_B:
tag_name = TALKATU_TAG_BOLD;
break;
case GUMBO_TAG_CODE:
tag_name = TALKATU_TAG_CODE;
break;
case GUMBO_TAG_I:
tag_name = TALKATU_TAG_ITALIC;
break;
case GUMBO_TAG_U:
tag_name = TALKATU_TAG_UNDERLINE;
break;
case GUMBO_TAG_EM:
break;
case GUMBO_TAG_STRIKE:
tag_name = TALKATU_TAG_STRIKETHROUGH;
break;
case GUMBO_TAG_SUB:
tag_name = TALKATU_TAG_SUBSCRIPT;
break;
case GUMBO_TAG_SUP:
tag_name = TALKATU_TAG_SUPERSCRIPT;
break;
case GUMBO_TAG_PRE:
tag_name = TALKATU_TAG_PRE;
break;
case GUMBO_TAG_H1:
tag_name = TALKATU_TAG_H1;
break;
case GUMBO_TAG_H2:
tag_name = TALKATU_TAG_H2;
break;
case GUMBO_TAG_H3:
tag_name = TALKATU_TAG_H3;
break;
case GUMBO_TAG_H4:
tag_name = TALKATU_TAG_H4;
break;
case GUMBO_TAG_H5:
tag_name = TALKATU_TAG_H5;
break;
case GUMBO_TAG_H6:
tag_name = TALKATU_TAG_H6;
break;
case GUMBO_TAG_A:
tag_name = TALKATU_TAG_ANCHOR;
break;
case GUMBO_TAG_DL:
tag_name = TALKATU_TAG_DL;
break;
case GUMBO_TAG_DT:
tag_name = TALKATU_TAG_DT;
break;
case GUMBO_TAG_DD:
tag_name = TALKATU_TAG_DD;
break;
case GUMBO_TAG_BR:
talkatu_buffer_insert_markup(TALKATU_BUFFER(buffer), pos, "\n", -1);
break;
default:
break;
}
}
if(tag_name != NULL) {
tag = gtk_text_tag_table_lookup(table, tag_name);
if(TALKATU_IS_TAG(tag)) {
GtkTextTag *prefix_tag = NULL;
gchar *prefix_name = NULL;
g_object_get(G_OBJECT(tag), "display", &display, NULL);
prefix_name = g_strdup_printf("%s%s", tag_name, TALKATU_TAG_FORMATTING_START);
prefix_tag = gtk_text_tag_table_lookup(table, prefix_name);
g_free(prefix_name);
if(GTK_IS_TEXT_TAG(prefix_tag)) {
GtkTextIter s;
talkatu_buffer_insert_markup(
TALKATU_BUFFER(buffer),
pos,
"\u200B", // Zero Width Space
-1
);
s = *pos;
gtk_text_iter_backward_char(&s);
gtk_text_buffer_apply_tag(
GTK_TEXT_BUFFER(buffer),
prefix_tag,
&s,
pos
);
}
}
}
children = &node->v.element.children;
for(i = 0; i < children->length; i++) {
GumboNode *child = (GumboNode *)children->data[i];
if(child->type == GUMBO_NODE_TEXT || child->type == GUMBO_NODE_WHITESPACE) {
gint text_length = g_utf8_strlen(child->v.text.text, -1);
/* add the text */
talkatu_buffer_insert_markup(TALKATU_BUFFER(buffer), pos, child->v.text.text, -1);
/* now adjust pos to the new insertion point */
gtk_text_buffer_get_iter_at_offset(
buffer,
pos,
gtk_text_iter_get_offset(pos) + text_length
);
length += text_length;
} else if(child->type == GUMBO_NODE_ELEMENT || child->type == GUMBO_NODE_TEMPLATE) {
length += talkatu_markup_deserialize_html_helper(buffer, pos, child, str);
}
}
if(tag) {
GtkTextIter start, end;
gtk_text_buffer_get_iter_at_offset(buffer, &start, start_pos);
gtk_text_buffer_get_iter_at_offset(buffer, &end, start_pos + length);
gtk_text_buffer_apply_tag(
buffer,
tag,
&start,
&end
);
if(TALKATU_IS_TAG(tag)) {
GtkTextTag *suffix_tag = NULL;
gchar *suffix_name = g_strdup_printf("%s%s", tag_name, TALKATU_TAG_FORMATTING_END);
suffix_tag = gtk_text_tag_table_lookup(table, suffix_name);
g_free(suffix_name);
if(GTK_IS_TEXT_TAG(suffix_tag)) {
GtkTextIter s;
gchar *last = NULL;
gint offset = 0;
gboolean coalescing = FALSE;
/* now determine if we need to coalesce newlines */
s = *pos;
gtk_text_iter_backward_char(pos);
last = gtk_text_iter_get_text(pos, &s);
if(*last == '\n') {
coalescing = TRUE;
}
g_free(last);
/* stash the current position in the buffer so we can get
* back to easily.
*/
offset = gtk_text_iter_get_offset(pos);
/* move pos to the start of the line */
gtk_text_iter_set_line_offset(pos, 0);
/* if our iterator is the start of a word, apply the tag to
* that, if not insert a zero width space and apply to that.
*/
if(gtk_text_iter_starts_word(pos)) {
s = *pos;
gtk_text_iter_forward_char(pos);
} else {
/* now insert the zero width space */
talkatu_buffer_insert_markup(
TALKATU_BUFFER(buffer),
pos,
"\u200B",
-1
);
/* now get an iterator to the start of the line */
s = *pos;
gtk_text_iter_backward_char(&s);
/* increment offset to account for the space */
offset += 1;
}
/* now apply the suffix tag */
gtk_text_buffer_apply_tag(
GTK_TEXT_BUFFER(buffer),
suffix_tag,
&s,
pos
);
/* now move pos back to its original position plus one for
* the zero width space.
*/
gtk_text_iter_set_offset(pos, offset);
/* if we're not coalescing newlines, add one */
if(!coalescing) {
talkatu_buffer_insert_markup(
TALKATU_BUFFER(buffer),
pos,
"\n",
-1
);
}
} else if(display == TALKATU_TAG_DISPLAY_BLOCK) {
talkatu_buffer_insert_markup(TALKATU_BUFFER(buffer), pos, "\n", -1);
}
}
}
return length;
}
/**
* talkatu_markup_deserialize_html:
* @register_buffer: The #GtkTextBuffer the format is registered with.
* @content_buffer: The #GtkTextBuffer to deserialize into.
* @iter: Insertion point for the deserialized text.
* @data: The data to deserialize.
* @length: The length of @data.
* @create_tags: TRUE if deserializing may create tags
* @user_data: User data that was specified when registering the format.
* @error: Return location for a #GError.
*
* This is a #GtkTextBufferDeserializeFunc function that will deserialize
* HTML data from @data into @content_buffer. It should be registered with
* with #gtk_text_buffer_register_deserializer_format in the buffer's
* instance_init function.
*
* Returns: TRUE on success, or FALSE on error with @error set.
*/
gboolean
talkatu_markup_deserialize_html(GtkTextBuffer *register_buffer,
GtkTextBuffer *content_buffer,
GtkTextIter *iter,
const guint8 *data,
gsize length,
gboolean create_tags,
gpointer user_data,
GError **error)
{
GError *real_error = NULL;
gchar *text = NULL;
gsize text_length;
text = talkatu_codeset_coerce_utf8(data, length, &text_length, &real_error);
if(real_error) {
if(error) {
*error = real_error;
}
return FALSE;
}
talkatu_markup_insert_html(
TALKATU_BUFFER(register_buffer),
iter,
text,
text_length
);
g_free(text);
return TRUE;
}
/**
* talkatu_markup_append_html:
* @buffer: The #TalkatuBuffer instance.
* @text: The text to append.
* @len: The len of @text.
*
* Appends @text to @buffer at the current insertion point of @buffer.
*/
void
talkatu_markup_append_html(TalkatuBuffer *buffer, const gchar *text, gint len) {
GtkTextMark *ins_mark;
GtkTextIter ins_iter;
ins_mark = gtk_text_buffer_get_insert(GTK_TEXT_BUFFER(buffer));
gtk_text_buffer_get_iter_at_mark(GTK_TEXT_BUFFER(buffer), &ins_iter, ins_mark);
talkatu_markup_insert_html(buffer, &ins_iter, text, len);
}
/**
* talkatu_markup_insert_html:
* @buffer: The #TalkatuBuffer instance.
* @iter: The insertion point for the deserialized text.
* @text: The text to insert.
* @len: The length of @text.
*
* Inserts @text in @buffer at @iter.
*/
void
talkatu_markup_insert_html(TalkatuBuffer *buffer,
GtkTextIter *iter,
const gchar *text,
gint len)
{
GString *str = NULL;
GumboOutput *output = NULL;
output = gumbo_parse(text);
str = g_string_new("");
talkatu_markup_deserialize_html_helper(
GTK_TEXT_BUFFER(buffer),
iter,
output->document,
str
);
gumbo_destroy_output(&kGumboDefaultOptions, output);
talkatu_buffer_insert_markup(
TALKATU_BUFFER(buffer),
iter,
str->str,
str->len
);
g_string_free(str, TRUE);
}
/**
* talkatu_markup_set_html:
* @buffer: A #TalkatuBuffer instance
* @text: The HTML text to set.
* @len: The length of @text, or -1.
*
* Replaces all text in @buffer with @text.
*/
void
talkatu_markup_set_html(TalkatuBuffer *buffer, const gchar *text, gint len) {
GtkTextIter start, end;
gtk_text_buffer_get_bounds(GTK_TEXT_BUFFER(buffer), &start, &end);
gtk_text_buffer_delete(GTK_TEXT_BUFFER(buffer), &start, &end);
talkatu_markup_insert_html(buffer, &start, text, len);
}
/**
* talkatu_markup_get_html_range:
* @buffer: The #GtkTextBuffer instance.
* @start: The starting point to get.
* @end: The ending point to get.
* @len: A return address for the length of text extracted.
*
* Extracts a section of @buffer into an HTML string.
*
* Returns: (transfer full): The HTML string.
*/
gchar *
talkatu_markup_get_html_range(GtkTextBuffer *buffer,
const GtkTextIter *start,
const GtkTextIter *end,
gsize *len)
{
GSList *stack = NULL, *l = NULL;
GtkTextIter *real_start = NULL, *real_end = NULL, *pos = NULL, *next = NULL;
GString *str = g_string_new("");
gunichar c;
real_start = gtk_text_iter_copy(start);
real_end = gtk_text_iter_copy(end);
/* correctly order the iterators */
gtk_text_iter_order(real_start, real_end);
/* create a copy of the start one which is what we will use to walk the
* buffer.
*/
pos = gtk_text_iter_copy(real_start);
/* create a look ahead iter to help us tell when tags start/end */
next = gtk_text_iter_copy(pos);
gtk_text_iter_forward_char(next);
/* create the stack of tags */
stack = gtk_text_iter_get_tags(pos);
/* add any start tags that are open and don't start here */
for(l = stack; l != NULL; l = l->next) {
GtkTextTag *tag = GTK_TEXT_TAG(l->data);
if(!gtk_text_iter_starts_tag(pos, tag)) {
gchar *name = talkatu_text_tag_get_name(tag);
const gchar *html_name = talkatu_tag_name_to_html(name);
g_free(name);
if(html_name != NULL) {
g_string_append_printf(str, "<%s>", html_name);
}
}
}
/* now clear the stack so we have a clean starting point */
g_slist_free(stack);
stack = NULL;
while((c = gtk_text_iter_get_char(pos)) != 0 && !gtk_text_iter_equal(pos, end)) {
GSList *cur_tags = gtk_text_iter_get_tags(pos);
/* open any tags starting here */
for(l = cur_tags; l != NULL; l = l->next) {
GtkTextTag *tag = GTK_TEXT_TAG(l->data);
if(gtk_text_iter_starts_tag(pos, tag)) {
gchar *name = talkatu_text_tag_get_name(tag);
const gchar *html_name = talkatu_tag_name_to_html(name);
g_free(name);
if(html_name != NULL) {
g_string_append_printf(str, "<%s>", html_name);
stack = g_slist_prepend(stack, tag);
}
}
}
str = g_string_append_unichar(str, c);
/* close any tags ending here */
for(l = cur_tags; l != NULL; l = l->next) {
GtkTextTag *tag = GTK_TEXT_TAG(l->data);
if(!gtk_text_iter_has_tag(next, tag)) {
gchar *name = talkatu_text_tag_get_name(tag);
const gchar *html_name = talkatu_tag_name_to_html(name);
g_free(name);
if(html_name != NULL) {
g_string_append_printf(str, "</%s>", html_name);
stack = g_slist_remove(stack, tag);
}
}
}
gtk_text_iter_forward_char(pos);
gtk_text_iter_forward_char(next);
}
/* close any tags remaining on the stack */
for(l = stack; l != NULL; l = l->next) {
GtkTextTag *tag = GTK_TEXT_TAG(l->data);
if(!gtk_text_iter_ends_tag(real_end, tag)) {
gchar *name = talkatu_text_tag_get_name(tag);
const gchar *html_name = talkatu_tag_name_to_html(name);
g_free(name);
if(html_name != NULL) {
g_string_append_printf(str, "</%s>", html_name);
}
}
}
g_slist_free(stack);
gtk_text_iter_free(pos);
gtk_text_iter_free(real_start);
gtk_text_iter_free(real_end);
return g_string_free(str, FALSE);
}
/**
* talkatu_markup_serialize_html:
* @register_buffer: The #GtkTextBuffer for which the format is registered.
* @content_buffer: The #GtkTextBuffer to serialize.
* @start: Start of the block of text to serialize.
* @end: End of the block of text to serialize.
* @length: Return location for the length of the serialized data.
* @user_data: User data that was specified when registering the format.
*
* This is a #GtkTextBufferSerializeFunc that should be registered with
* #gtk_text_buffer_register_serialize_func in the buffer's instance_init
* method.
*
* Returns: (transfer full): The serialized HTML data.
*/
guint8 *
talkatu_markup_serialize_html(GtkTextBuffer *register_buffer,
GtkTextBuffer *content_buffer,
const GtkTextIter *start,
const GtkTextIter *end,
gsize *length,
gpointer user_data)
{
gchar *html = NULL;
html = talkatu_markup_get_html_range(content_buffer, start, end, length);
return (guint8 *)html;
}
/**
* talkatu_markup_get_html:
* @buffer: The #GtkTextBuffer instance.
* @len: A return address for the length of text extracted.
*
* Extracts all text from @buffer as an HTML string.
*
* Returns: (transfer full): The extracted HTML string.
*/
gchar *
talkatu_markup_get_html(GtkTextBuffer *buffer, gsize *len) {
GtkTextIter start, end;
gtk_text_buffer_get_bounds(buffer, &start, &end);
return talkatu_markup_get_html_range(buffer, &start, &end, len);
}