# HG changeset patch
# Node ID 6f86b0dcb54d
# Parent  bde60654600e
diff --git a/pidgin/meson.build b/pidgin/meson.build
--- a/pidgin/meson.build
+++ b/pidgin/meson.build
@@ -61,6 +61,7 @@
 	'pidginstatuseditor.c',
 	'pidginstatusmanager.c',
 	'pidginstatusprimitivechooser.c',
+	'pidgintextbuffer.c',
 	'pidginui.c',
 	'prefs/pidginprefs.c',
 	'prefs/pidginawayprefs.c',
@@ -130,6 +131,7 @@
 	'pidginstatuseditor.h',
 	'pidginstatusmanager.h',
 	'pidginstatusprimitivechooser.h',
+	'pidgintextbuffer.h',
 	'pidginui.h',
 ]
 
@@ -298,3 +300,4 @@
 subdir('data')
 subdir('pixmaps')
 subdir('plugins')
+subdir('tests')
diff --git a/pidgin/pidgintextbuffer.h b/pidgin/pidgintextbuffer.h
new file mode 100644
--- /dev/null
+++ b/pidgin/pidgintextbuffer.h
@@ -0,0 +1,74 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PIDGIN_GLOBAL_HEADER_INSIDE) && !defined(PIDGIN_COMPILATION)
+# error "only <pidgin.h> may be included directly"
+#endif
+
+#ifndef PIDGIN_TEXT_BUFFER_H
+#define PIDGIN_TEXT_BUFFER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+/**
+ * pidgin_text_buffer_get_html:
+ * @buffer: The text buffer.
+ *
+ * Extracts text from @buffer as HTML.
+ *
+ * This will work with any [class@Gtk.TextBuffer] as it deciphers the tags it
+ * finds.
+ *
+ * This is the same as calling [func@text_buffer_get_html_range] with the start
+ * and end iterators of @buffer.
+ *
+ * Returns: (transfer full) (nullable): The HTML string, which could be empty.
+ *
+ * See Also: [func@text_buffer_get_html_range]
+ *
+ * Since: 3.0.0
+ */
+char *pidgin_text_buffer_get_html(GtkTextBuffer *buffer);
+
+/**
+ * pidgin_text_buffer_get_html_range:
+ * @buffer: The text buffer.
+ * @start: The start iterator.
+ * @end: The end iterator.
+ *
+ * Extracts the text between @start and @end from @buffer as HTML.
+ * This will work with any [class@Gtk.TextBuffer] as it deciphers the tags it
+ * finds.
+ *
+ * Returns: (transfer full) (nullable): The HTML string, which could be empty.
+ *
+ * See Also: [func@text_buffer_get_html]
+ *
+ * Since: 3.0.0
+ */
+char *pidgin_text_buffer_get_html_range(GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end);
+
+G_END_DECLS
+
+#endif /* PIDGIN_TEXT_BUFFER_H */
\ No newline at end of file
diff --git a/pidgin/pidgintextbuffer.c b/pidgin/pidgintextbuffer.c
new file mode 100644
--- /dev/null
+++ b/pidgin/pidgintextbuffer.c
@@ -0,0 +1,183 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <purple.h>
+
+#include "pidgintextbuffer.h"
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+pidgin_text_buffer_get_html_convert_tag(GtkTextTag *tag, gboolean start,
+                                        GString *output)
+{
+	/* In practice gtk_text_buffer_insert_markup seems to create a single tag
+	 * for each attribute, but we'll check everything anyways to be try and
+	 * work with any buffer.
+	 */
+	gboolean strikethrough;
+	gboolean strikethrough_set;
+	PangoStyle style;
+	gboolean style_set;
+	PangoUnderline underline;
+	gboolean underline_set;
+	int weight;
+	gboolean weight_set;
+
+	const char *format = "<%s>";
+
+	if(!start) {
+		format = "</%s>";
+	}
+
+	g_object_get(
+		G_OBJECT(tag),
+		"strikethrough", &strikethrough,
+		"strikethrough-set", &strikethrough_set,
+		"style", &style,
+		"style-set", &style_set,
+		"underline", &underline,
+		"underline-set", &underline_set,
+		"weight", &weight,
+		"weight-set", &weight_set,
+		NULL);
+
+	if(style_set && style != PANGO_STYLE_NORMAL) {
+		g_string_append_printf(output, format, "i");
+	}
+
+	if(weight_set && weight > PANGO_WEIGHT_NORMAL) {
+		g_string_append_printf(output, format, "b");
+	}
+
+	if(underline_set && underline != PANGO_UNDERLINE_NONE) {
+		g_string_append_printf(output, format, "u");
+	}
+
+	if(strikethrough_set && strikethrough) {
+		g_string_append_printf(output, format, "s");
+	}
+}
+
+static void
+pidgin_text_buffer_get_html_start_tag(gpointer data, gpointer user_data) {
+	pidgin_text_buffer_get_html_convert_tag(data, TRUE, user_data);
+}
+
+static void
+pidgin_text_buffer_get_html_process_start_tags(GSList *tags, GString *output) {
+	g_slist_foreach(tags, pidgin_text_buffer_get_html_start_tag, output);
+}
+
+static void
+pidgin_text_buffer_get_html_end_tag(gpointer data, gpointer user_data) {
+	pidgin_text_buffer_get_html_convert_tag(data, FALSE, user_data);
+}
+
+static void
+pidgin_text_buffer_get_html_process_end_tags(GSList *tags, GString *output) {
+	g_slist_foreach(tags, pidgin_text_buffer_get_html_end_tag, output);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+char *
+pidgin_text_buffer_get_html(GtkTextBuffer *buffer) {
+	GtkTextIter start;
+	GtkTextIter end;
+
+	g_return_val_if_fail(GTK_IS_TEXT_BUFFER(buffer), NULL);
+
+	gtk_text_buffer_get_start_iter(buffer, &start);
+	gtk_text_buffer_get_end_iter(buffer, &end);
+
+	return pidgin_text_buffer_get_html_range(buffer, &start, &end);
+}
+
+char *
+pidgin_text_buffer_get_html_range(GtkTextBuffer *buffer, GtkTextIter *start,
+                                  GtkTextIter *end)
+{
+	GSList *tags = NULL;
+	GString *output = NULL;
+	GtkTextIter *next = NULL;
+	GtkTextIter *position = NULL;
+	char *text = NULL;
+
+	g_return_val_if_fail(GTK_IS_TEXT_BUFFER(buffer), NULL);
+	g_return_val_if_fail(start != NULL, NULL);
+	g_return_val_if_fail(end != NULL, NULL);
+
+	/* Make sure start is before end. */
+	gtk_text_iter_order(start, end);
+
+	/* Create our iterators that we use to walk the text. */
+	position = gtk_text_iter_copy(start);
+	next = gtk_text_iter_copy(start);
+
+	output = g_string_new("");
+
+	/* Process the tags at our starting position. */
+	tags = gtk_text_iter_get_tags(position);
+	pidgin_text_buffer_get_html_process_start_tags(tags, output);
+	g_clear_slist(&tags, NULL);
+
+	/* Now walk the text looking for tags that are being toggled. */
+	while(gtk_text_iter_forward_to_tag_toggle(next, NULL)) {
+		/* Append the text in the range. */
+		text = gtk_text_buffer_get_text(buffer, position, next, TRUE);
+		g_string_append(output, text);
+		g_clear_pointer(&text, g_free);
+
+		/* Process the closed tags. */
+		tags = gtk_text_iter_get_toggled_tags(next, FALSE);
+		pidgin_text_buffer_get_html_process_end_tags(tags, output);
+		g_clear_slist(&tags, NULL);
+
+		/* Process the opened tags. */
+		tags = gtk_text_iter_get_toggled_tags(next, TRUE);
+		pidgin_text_buffer_get_html_process_start_tags(tags, output);
+		g_clear_slist(&tags, NULL);
+
+		/* Increment our iterators. */
+		gtk_text_iter_forward_to_tag_toggle(position, NULL);
+	}
+
+	/* Copy the remaining text. */
+	text = gtk_text_buffer_get_text(buffer, position, next, TRUE);
+	if(text != NULL) {
+		g_string_append(output, text);
+		g_clear_pointer(&text, g_free);
+	}
+
+	/* Write out any final tags. */
+	tags = gtk_text_iter_get_tags(position);
+	pidgin_text_buffer_get_html_process_end_tags(tags, output);
+	g_clear_slist(&tags, NULL);
+
+	g_clear_pointer(&position, gtk_text_iter_free);
+	g_clear_pointer(&next, gtk_text_iter_free);
+
+	return g_string_free(output, FALSE);
+}
diff --git a/pidgin/tests/meson.build b/pidgin/tests/meson.build
new file mode 100644
--- /dev/null
+++ b/pidgin/tests/meson.build
@@ -0,0 +1,18 @@
+PROGRAMS = [
+	'text_buffer',
+]
+
+TEST_WRAPPER = find_program('./test-wrapper.py', required: true)
+
+testenv.set('XDG_CONFIG_HOME', meson.current_build_dir() / 'config')
+
+foreach program : PROGRAMS
+    e = executable(f'test_@program@', f'test_@program@.c',
+                   c_args : [
+                       '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir()),
+                       '-DTEST_CACHE_DIR="@0@/cache"'.format(meson.current_build_dir()),
+                   ],
+                   dependencies : [libpurple_dep, libpidgin_dep, glib, gtk],
+    )
+    test(program, TEST_WRAPPER, args : e, is_parallel : false, env: testenv)
+endforeach
diff --git a/pidgin/tests/test-wrapper.py b/pidgin/tests/test-wrapper.py
new file mode 100755
--- /dev/null
+++ b/pidgin/tests/test-wrapper.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+# talkatu
+# Copyright (C) 2017-2020 Gary Kramlich <grim@reaperworld.com>
+#
+# This program 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 program 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/>.
+
+"""
+This script will run the command specified via command line arguments,
+making sure broadwayd is running and will be used.
+"""
+
+import os
+import subprocess
+import sys
+
+
+def main():
+	# start broadway
+	broadwayd  = subprocess.Popen(['gtk4-broadwayd'])
+
+	# run the unit test but set the GDK_BACKEND envvar to broadway
+	env = {**os.environ, 'GDK_BACKEND': 'broadway'}
+
+	try:
+		proc = subprocess.run(args=sys.argv[1:], env=env)
+	finally:
+		# kill broadway
+		broadwayd.kill()
+
+	# return the exit code of the unit test
+	sys.exit(proc.returncode)
+
+
+if __name__ == '__main__':
+	main()
diff --git a/pidgin/tests/test_text_buffer.c b/pidgin/tests/test_text_buffer.c
new file mode 100644
--- /dev/null
+++ b/pidgin/tests/test_text_buffer.c
@@ -0,0 +1,106 @@
+/*
+ * Pidgin - Internet Messenger
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program 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 program 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 program; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <gtk/gtk.h>
+
+#include <pidgin.h>
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+test_pidgin_text_buffer_get_html_helper(const char *markup,
+                                        const char *expected)
+{
+	GtkTextBuffer *buffer = NULL;
+	char *actual = NULL;
+
+	buffer = gtk_text_buffer_new(NULL);
+
+	if(!purple_strempty(markup)) {
+		GtkTextIter start;
+
+		gtk_text_buffer_get_start_iter(buffer, &start);
+		gtk_text_buffer_insert_markup(buffer, &start, markup, -1);
+	}
+
+	actual = pidgin_text_buffer_get_html(buffer);
+	g_assert_cmpstr(actual, ==, expected);
+
+	g_clear_pointer(&actual, g_free);
+	g_clear_object(&buffer);
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_pidgin_text_buffer_get_html_empty_string(void) {
+	test_pidgin_text_buffer_get_html_helper(NULL, "");
+}
+
+static void
+test_pidgin_text_buffer_get_html_plain_text(void) {
+	test_pidgin_text_buffer_get_html_helper("the quick brown fox...",
+	                                        "the quick brown fox...");
+}
+
+static void
+test_pidgin_text_buffer_get_html_simple_nested_tags(void) {
+	/* While the order _shouldn't_ matter, Gtk somehow puts the italic tag
+	 * first... This could just be reversed, but more testing needs to be done.
+	 */
+	const char *markup = "<b><i>wat?!</i></b>";
+
+	test_pidgin_text_buffer_get_html_helper(markup, markup);
+}
+
+static void
+test_pidgin_text_buffer_get_html_simple_tags(void) {
+	/* Using metasyntactic variables because they're easy or something... */
+	const char *markup = "foo <b>bar</b> <i>baz</i> <u>qux</u> <s>quux</s> corge";
+
+	test_pidgin_text_buffer_get_html_helper(markup, markup);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	gtk_init();
+
+	g_test_add_func("/pidgin/text-buffer/get-html/empty-string",
+	                test_pidgin_text_buffer_get_html_empty_string);
+	g_test_add_func("/pidgin/text-buffer/get-html/plain-text",
+	                test_pidgin_text_buffer_get_html_plain_text);
+#if 0
+	g_test_add_func("/pidgin/text-buffer/get-html/simple-nested",
+	                test_pidgin_text_buffer_get_html_simple_nested_tags);
+#endif
+	g_test_add_func("/pidgin/text-buffer/get-html/simple-tags",
+	                test_pidgin_text_buffer_get_html_simple_tags);
+
+	return g_test_run();
+}
