summaryrefslogtreecommitdiffstats
path: root/src/gs-css.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/gs-css.c308
1 files changed, 308 insertions, 0 deletions
diff --git a/src/gs-css.c b/src/gs-css.c
new file mode 100644
index 0000000..ff81855
--- /dev/null
+++ b/src/gs-css.c
@@ -0,0 +1,308 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-css
+ * @title: GsCss
+ * @stability: Unstable
+ * @short_description: Parse, validate and rewrite CSS resources
+ */
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <appstream.h>
+
+#include "gs-css.h"
+
+struct _GsCss
+{
+ GObject parent_instance;
+ GHashTable *ids;
+ GsCssRewriteFunc rewrite_func;
+ gpointer rewrite_func_data;
+};
+
+G_DEFINE_TYPE (GsCss, gs_css, G_TYPE_OBJECT)
+
+static void
+_cleanup_string (GString *str)
+{
+ /* remove leading newlines */
+ while (g_str_has_prefix (str->str, "\n") || g_str_has_prefix (str->str, " "))
+ g_string_erase (str, 0, 1);
+
+ /* remove trailing newlines */
+ while (g_str_has_suffix (str->str, "\n") || g_str_has_suffix (str->str, " "))
+ g_string_truncate (str, str->len - 1);
+}
+
+/**
+ * gs_css_parse:
+ * @self: a #GsCss
+ * @markup: come CSS, or %NULL
+ * @error: a #GError or %NULL
+ *
+ * Parses the CSS markup and does some basic validation checks on the input.
+ *
+ * Returns: %TRUE for success
+ */
+gboolean
+gs_css_parse (GsCss *self, const gchar *markup, GError **error)
+{
+ g_auto(GStrv) parts = NULL;
+ g_autoptr(GString) markup_str = NULL;
+
+ /* no data */
+ if (markup == NULL || markup[0] == '\0')
+ return TRUE;
+
+ /* old style, no IDs */
+ markup_str = g_string_new (markup);
+ as_gstring_replace (markup_str, "@datadir@", DATADIR);
+ if (!g_str_has_prefix (markup_str->str, "#")) {
+ g_hash_table_insert (self->ids,
+ g_strdup ("tile"),
+ g_string_free (g_steal_pointer (&markup_str), FALSE));
+ return TRUE;
+ }
+
+ /* split up CSS into ID chunks, e.g.
+ *
+ * #tile {border-radius: 0;}
+ * #name {color: white;}
+ */
+ parts = g_strsplit (markup_str->str + 1, "\n#", -1);
+ for (guint i = 0; parts[i] != NULL; i++) {
+ g_autoptr(GString) current_css = NULL;
+ g_autoptr(GString) current_key = NULL;
+ for (guint j = 1; parts[i][j] != '\0'; j++) {
+ const gchar ch = parts[i][j];
+ if (ch == '{') {
+ if (current_key != NULL || current_css != NULL) {
+ g_set_error_literal (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "invalid '{'");
+ return FALSE;
+ }
+ current_key = g_string_new_len (parts[i], j);
+ current_css = g_string_new (NULL);
+ _cleanup_string (current_key);
+
+ /* already added */
+ if (g_hash_table_lookup (self->ids, current_key->str) != NULL) {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "duplicate ID '%s'",
+ current_key->str);
+ return FALSE;
+ }
+ continue;
+ }
+ if (ch == '}') {
+ if (current_key == NULL || current_css == NULL) {
+ g_set_error_literal (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "invalid '}'");
+ return FALSE;
+ }
+ _cleanup_string (current_css);
+ g_hash_table_insert (self->ids,
+ g_string_free (current_key, FALSE),
+ g_string_free (current_css, FALSE));
+ current_key = NULL;
+ current_css = NULL;
+ continue;
+ }
+ if (current_css != NULL)
+ g_string_append_c (current_css, ch);
+ }
+ if (current_key != NULL || current_css != NULL) {
+ g_set_error_literal (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "missing '}'");
+ return FALSE;
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+/**
+ * gs_css_get_markup_for_id:
+ * @self: a #GsCss
+ * @id: an ID, or %NULL for the default
+ *
+ * Gets the CSS markup for a specific ID.
+ *
+ * Returns: %TRUE for success
+ */
+const gchar *
+gs_css_get_markup_for_id (GsCss *self, const gchar *id)
+{
+ if (id == NULL)
+ id = "tile";
+ return g_hash_table_lookup (self->ids, id);
+}
+
+static void
+_css_parsing_error_cb (GtkCssProvider *provider,
+ GtkCssSection *section,
+ GError *error,
+ gpointer user_data)
+{
+ GError **error_parse = (GError **) user_data;
+ if (*error_parse != NULL) {
+ const GtkCssLocation *start_location;
+
+ start_location = gtk_css_section_get_start_location (section);
+ g_warning ("ignoring parse error %" G_GSIZE_FORMAT ":%" G_GSIZE_FORMAT ": %s",
+ start_location->lines + 1,
+ start_location->line_chars,
+ error->message);
+ return;
+ }
+ *error_parse = g_error_copy (error);
+}
+
+static gboolean
+gs_css_validate_part (GsCss *self, const gchar *markup, GError **error)
+{
+ g_autofree gchar *markup_new = NULL;
+ g_autoptr(GError) error_parse = NULL;
+ g_autoptr(GString) str = NULL;
+ g_autoptr(GtkCssProvider) provider = NULL;
+
+ /* nothing set */
+ if (markup == NULL)
+ return TRUE;
+
+ /* remove custom class if NULL */
+ str = g_string_new (NULL);
+ g_string_append (str, ".themed-widget {");
+ if (self->rewrite_func != NULL) {
+ markup_new = self->rewrite_func (self->rewrite_func_data,
+ markup,
+ error);
+ if (markup_new == NULL)
+ return FALSE;
+ } else {
+ markup_new = g_strdup (markup);
+ }
+ g_string_append (str, markup_new);
+ g_string_append (str, "}");
+
+ /* set up custom provider */
+ provider = gtk_css_provider_new ();
+ g_signal_connect (provider, "parsing-error",
+ G_CALLBACK (_css_parsing_error_cb), &error_parse);
+ gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+ GTK_STYLE_PROVIDER (provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+ gtk_css_provider_load_from_data (provider, str->str, -1);
+ if (error_parse != NULL) {
+ if (error != NULL)
+ *error = g_error_copy (error_parse);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_css_validate:
+ * @self: a #GsCss
+ * @error: a #GError or %NULL
+ *
+ * Validates each part of the CSS markup.
+ *
+ * Returns: %TRUE for success
+ */
+gboolean
+gs_css_validate (GsCss *self, GError **error)
+{
+ g_autoptr(GList) keys = NULL;
+
+ /* check each CSS ID */
+ keys = g_hash_table_get_keys (self->ids);
+ for (GList *l = keys; l != NULL; l = l->next) {
+ const gchar *tmp;
+ const gchar *id = l->data;
+ if (g_strcmp0 (id, "tile") != 0 &&
+ g_strcmp0 (id, "name") != 0 &&
+ g_strcmp0 (id, "summary") != 0) {
+ g_set_error (error,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_DATA,
+ "Invalid CSS ID '%s'",
+ id);
+ return FALSE;
+ }
+ tmp = g_hash_table_lookup (self->ids, id);
+ if (!gs_css_validate_part (self, tmp, error))
+ return FALSE;
+ }
+
+ /* success */
+ return TRUE;
+}
+
+/**
+ * gs_css_set_rewrite_func:
+ * @self: a #GsCss
+ * @func: a #GsCssRewriteFunc or %NULL
+ * @user_data: user data to pass to @func
+ *
+ * Sets a function to be used when rewriting CSS before it is parsed.
+ *
+ * Returns: %TRUE for success
+ */
+void
+gs_css_set_rewrite_func (GsCss *self, GsCssRewriteFunc func, gpointer user_data)
+{
+ self->rewrite_func = func;
+ self->rewrite_func_data = user_data;
+}
+
+static void
+gs_css_finalize (GObject *object)
+{
+ GsCss *self = GS_CSS (object);
+ g_hash_table_unref (self->ids);
+ G_OBJECT_CLASS (gs_css_parent_class)->finalize (object);
+}
+
+static void
+gs_css_class_init (GsCssClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_css_finalize;
+}
+
+static void
+gs_css_init (GsCss *self)
+{
+ self->ids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+}
+
+/**
+ * gs_css_new:
+ *
+ * Return value: a new #GsCss object.
+ **/
+GsCss *
+gs_css_new (void)
+{
+ GsCss *self;
+ self = g_object_new (GS_TYPE_CSS, NULL);
+ return GS_CSS (self);
+}