todo-txt: Rework parser

This commit introduces an improved and more robust
Todo.txt parser, but has one major drawback that we
have to figure out before the release: the Todo.txt
provider stopped supporting parent/child tasks.
parent 8af61ded
......@@ -18,6 +18,7 @@
#define G_LOG_DOMAIN "GtdPluginTodoTxt"
#include "gtd-debug.h"
#include "gtd-plugin-todo-txt.h"
#include "gtd-provider-todo-txt.h"
......@@ -53,88 +54,29 @@ enum
static void gtd_activatable_iface_init (GtdActivatableInterface *iface);
G_DEFINE_DYNAMIC_TYPE_EXTENDED (GtdPluginTodoTxt, gtd_plugin_todo_txt, PEAS_TYPE_EXTENSION_BASE,
0,
G_IMPLEMENT_INTERFACE_DYNAMIC (GTD_TYPE_ACTIVATABLE,
gtd_activatable_iface_init))
G_DEFINE_DYNAMIC_TYPE_EXTENDED (GtdPluginTodoTxt, gtd_plugin_todo_txt, PEAS_TYPE_EXTENSION_BASE, 0,
G_IMPLEMENT_INTERFACE_DYNAMIC (GTD_TYPE_ACTIVATABLE, gtd_activatable_iface_init))
/*
* GtdActivatable interface implementation
*/
static void
gtd_plugin_todo_txt_activate (GtdActivatable *activatable)
{
;
}
static void
gtd_plugin_todo_txt_deactivate (GtdActivatable *activatable)
{
;
}
static GList*
gtd_plugin_todo_txt_get_header_widgets (GtdActivatable *activatable)
{
return NULL;
}
static GtkWidget*
gtd_plugin_todo_txt_get_preferences_panel (GtdActivatable *activatable)
{
GtdPluginTodoTxt *plugin = GTD_PLUGIN_TODO_TXT (activatable);
return plugin->preferences_box;
}
static GList*
gtd_plugin_todo_txt_get_panels (GtdActivatable *activatable)
{
return NULL;
}
static GList*
gtd_plugin_todo_txt_get_providers (GtdActivatable *activatable)
{
GtdPluginTodoTxt *plugin = GTD_PLUGIN_TODO_TXT (activatable);
return plugin->providers;
}
static void
gtd_activatable_iface_init (GtdActivatableInterface *iface)
{
iface->activate = gtd_plugin_todo_txt_activate;
iface->deactivate = gtd_plugin_todo_txt_deactivate;
iface->get_header_widgets = gtd_plugin_todo_txt_get_header_widgets;
iface->get_preferences_panel = gtd_plugin_todo_txt_get_preferences_panel;
iface->get_panels = gtd_plugin_todo_txt_get_panels;
iface->get_providers = gtd_plugin_todo_txt_get_providers;
}
/*
* Init
* Auxiliary methods
*/
static gboolean
gtd_plugin_todo_txt_set_default_source (GtdPluginTodoTxt *self)
set_default_source (GtdPluginTodoTxt *self)
{
g_autofree gchar *default_file;
GError *error;
g_autofree gchar *default_file = NULL;
g_autoptr (GError) error = NULL;
error = NULL;
default_file = g_build_filename (g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS),
"todo.txt",
NULL);
GTD_ENTRY;
default_file = g_build_filename (g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS), "todo.txt", NULL);
self->source_file = g_file_new_for_path (default_file);
if (g_file_query_exists (self->source_file, NULL))
return TRUE;
GTD_RETURN (TRUE);
g_file_create (self->source_file,
G_FILE_CREATE_NONE,
NULL,
&error);
g_file_create (self->source_file, G_FILE_CREATE_NONE, NULL, &error);
if (error)
{
......@@ -143,39 +85,35 @@ gtd_plugin_todo_txt_set_default_source (GtdPluginTodoTxt *self)
error->message,
NULL,
NULL);
g_clear_error (&error);
return FALSE;
GTD_RETURN (FALSE);
}
return TRUE;
GTD_RETURN (TRUE);
}
static gboolean
gtd_plugin_todo_txt_set_source (GtdPluginTodoTxt *self)
setup_source (GtdPluginTodoTxt *self)
{
GError *error;
gchar *source;
g_autoptr (GError) error = NULL;
g_autofree gchar *source = NULL;
GTD_ENTRY;
error = NULL;
source = g_settings_get_string (self->settings, "file");
if (!source || source[0] == '\0')
{
if (!gtd_plugin_todo_txt_set_default_source (self))
return FALSE;
if (!set_default_source (self))
GTD_RETURN (FALSE);
}
else
{
self->source_file = g_file_new_for_uri (source);
self->source_file = g_file_new_for_path (source);
}
if (!g_file_query_exists (self->source_file, NULL))
{
g_file_create (self->source_file,
G_FILE_CREATE_NONE,
NULL,
&error);
g_file_create (self->source_file, G_FILE_CREATE_NONE, NULL, &error);
if (error)
{
......@@ -184,46 +122,52 @@ gtd_plugin_todo_txt_set_source (GtdPluginTodoTxt *self)
error->message,
NULL,
NULL);
g_clear_error (&error);
return FALSE;
GTD_RETURN (FALSE);
}
}
return TRUE;
GTD_RETURN (TRUE);
}
/*
* Callbacks
*/
static void
gtd_plugin_todo_txt_source_changed_finished_cb (GtdPluginTodoTxt *self)
on_source_changed_finished_cb (GtdPluginTodoTxt *self)
{
GtdProviderTodoTxt *provider;
gboolean set;
set = gtd_plugin_todo_txt_set_source (self);
GTD_ENTRY;
set = setup_source (self);
if (!set)
return;
GTD_RETURN ();
provider = gtd_provider_todo_txt_new (self->source_file);
self->providers = g_list_append (self->providers, provider);
g_signal_emit_by_name (self, "provider-added", provider);
GTD_EXIT;
}
static void
gtd_plugin_todo_txt_source_changed_cb (GtkWidget *preference_panel,
gpointer user_data)
on_source_changed_cb (GtkWidget *preference_panel,
GtdPluginTodoTxt *self)
{
GtdPluginTodoTxt *self;
GtdProviderTodoTxt *provider;
self = GTD_PLUGIN_TODO_TXT (user_data);
GTD_ENTRY;
g_clear_object (&self->source_file);
g_settings_set_string (self->settings,
"file",
gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (self->preferences)));
gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->preferences)));
if (self->providers)
{
......@@ -235,9 +179,72 @@ gtd_plugin_todo_txt_source_changed_cb (GtkWidget *preference_panel,
g_signal_emit_by_name (self, "provider-removed", provider);
}
gtd_plugin_todo_txt_source_changed_finished_cb (self);
on_source_changed_finished_cb (self);
GTD_EXIT;
}
/*
* GtdActivatable implementation
*/
static void
gtd_plugin_todo_txt_activate (GtdActivatable *activatable)
{
;
}
static void
gtd_plugin_todo_txt_deactivate (GtdActivatable *activatable)
{
;
}
static GList*
gtd_plugin_todo_txt_get_header_widgets (GtdActivatable *activatable)
{
return NULL;
}
static GtkWidget*
gtd_plugin_todo_txt_get_preferences_panel (GtdActivatable *activatable)
{
GtdPluginTodoTxt *plugin = GTD_PLUGIN_TODO_TXT (activatable);
return plugin->preferences_box;
}
static GList*
gtd_plugin_todo_txt_get_panels (GtdActivatable *activatable)
{
return NULL;
}
static GList*
gtd_plugin_todo_txt_get_providers (GtdActivatable *activatable)
{
GtdPluginTodoTxt *plugin = GTD_PLUGIN_TODO_TXT (activatable);
return plugin->providers;
}
static void
gtd_activatable_iface_init (GtdActivatableInterface *iface)
{
iface->activate = gtd_plugin_todo_txt_activate;
iface->deactivate = gtd_plugin_todo_txt_deactivate;
iface->get_header_widgets = gtd_plugin_todo_txt_get_header_widgets;
iface->get_preferences_panel = gtd_plugin_todo_txt_get_preferences_panel;
iface->get_panels = gtd_plugin_todo_txt_get_panels;
iface->get_providers = gtd_plugin_todo_txt_get_providers;
}
/*
* GObject overrides
*/
static void
gtd_plugin_todo_txt_finalize (GObject *object)
{
......@@ -272,12 +279,10 @@ gtd_plugin_todo_txt_class_init (GtdPluginTodoTxtClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->finalize = gtd_plugin_todo_txt_finalize;
object_class->finalize = gtd_plugin_todo_txt_finalize;
object_class->get_property = gtd_plugin_todo_txt_get_property;
g_object_class_override_property (object_class,
PROP_PREFERENCES_PANEL,
"preferences-panel");
g_object_class_override_property (object_class, PROP_PREFERENCES_PANEL, "preferences-panel");
}
static void
......@@ -288,7 +293,7 @@ gtd_plugin_todo_txt_init (GtdPluginTodoTxt *self)
gboolean set;
self->settings = g_settings_new ("org.gnome.todo.plugins.todo-txt");
set = gtd_plugin_todo_txt_set_source (self);
set = setup_source (self);
self->providers = NULL;
if (set)
......@@ -317,13 +322,9 @@ gtd_plugin_todo_txt_init (GtdPluginTodoTxt *self)
gtk_widget_show_all (self->preferences_box);
g_signal_connect (self->preferences,
"file-set",
G_CALLBACK (gtd_plugin_todo_txt_source_changed_cb),
self);
g_signal_connect (self->preferences, "file-set", G_CALLBACK (on_source_changed_cb), self);
}
/* Empty class_finalize method */
static void
gtd_plugin_todo_txt_class_finalize (GtdPluginTodoTxtClass *klass)
{
......
This diff is collapsed.
......@@ -18,31 +18,30 @@
#define G_LOG_DOMAIN "GtdTodoTxtParser"
#include "gtd-debug.h"
#include "gtd-todo-txt-parser.h"
#include "gtd-provider-todo-txt.h"
#include <glib/gi18n.h>
struct _GtdTodoTxtParser
{
GtdObject parent;
};
enum
G_DEFINE_QUARK (GtdTodoTxtParserError, gtd_todo_txt_parser_error)
typedef enum
{
TASK_COMPLETE,
TASK_PRIORITY,
TASK_DATE,
TASK_TITLE,
TASK_LIST_NAME,
ROOT_TASK_NAME,
TASK_DUE_DATE
};
G_DEFINE_TYPE (GtdTodoTxtParser, gtd_todo_txt_parser, GTD_TYPE_OBJECT);
gint
gtd_todo_txt_parser_get_priority (gchar *token)
TOKEN_START,
TOKEN_COMPLETE,
TOKEN_PRIORITY,
TOKEN_DATE,
TOKEN_TITLE,
TOKEN_LIST_NAME,
TOKEN_LIST_COLOR,
TOKEN_DUE_DATE
} Token;
static gint
parse_priority (const gchar *token)
{
switch (token[1])
{
......@@ -62,8 +61,8 @@ gtd_todo_txt_parser_get_priority (gchar *token)
return 0;
}
GDateTime*
gtd_todo_txt_parser_get_date (gchar *token)
static GDateTime*
parse_date (const gchar *token)
{
GDateTime *dt;
GDate date;
......@@ -87,8 +86,8 @@ gtd_todo_txt_parser_get_date (gchar *token)
return dt;
}
gboolean
gtd_todo_txt_parser_is_date (gchar *dt)
static gboolean
is_date (const gchar *dt)
{
GDate date;
......@@ -98,393 +97,321 @@ gtd_todo_txt_parser_is_date (gchar *dt)
return g_date_valid (&date);
}
gboolean
gtd_todo_txt_parser_is_word (gchar *token)
{
guint pos;
guint token_length;
token_length = g_utf8_strlen (token, -1);
for (pos = 0; pos < token_length; pos++)
{
if (!g_unichar_isalnum (token[pos]))
return FALSE;
}
return TRUE;
}
gint
gtd_todo_txt_parser_get_token_id (gchar *token,
gint last_read)
static Token
parse_token_id (const gchar *token,
gint last_read)
{
gint token_length;
token_length = strlen (token);
if (!g_strcmp0 (token, "x"))
return TASK_COMPLETE;
return TOKEN_COMPLETE;
if (token_length == 3 && token[0] == '(' && token[2] == ')')
return TASK_PRIORITY;
return TOKEN_PRIORITY;
if (!g_str_has_prefix (token , "due:") && gtd_todo_txt_parser_is_date (token))
return TASK_DATE;
if (!g_str_has_prefix (token , "due:") && is_date (token))
return TOKEN_DATE;
if (gtd_todo_txt_parser_is_word (token) &&
(last_read == TASK_DATE ||
last_read == TASK_PRIORITY ||
last_read == TASK_COMPLETE||
last_read == TASK_TITLE))
{
return TASK_TITLE;
}
if (g_str_has_prefix (token , "color:"))
return TOKEN_LIST_COLOR;
if (token_length > 1 && token[0] == '@')
return TASK_LIST_NAME;
return TOKEN_LIST_NAME;
if (g_str_has_prefix (token , "due:"))
return TOKEN_DUE_DATE;
if (token_length > 1 && token[0] == '+')
return ROOT_TASK_NAME;
if (last_read == TOKEN_START ||
last_read == TOKEN_DATE ||
last_read == TOKEN_PRIORITY ||
last_read == TOKEN_COMPLETE||
last_read == TOKEN_TITLE)
{
return TOKEN_TITLE;
}
else if (last_read == TOKEN_LIST_NAME)
{
return TOKEN_LIST_NAME;
}
if (gtd_todo_txt_parser_is_word (token) && last_read == TASK_LIST_NAME)
return TASK_LIST_NAME;
return -1;
}
if (gtd_todo_txt_parser_is_word (token) && last_read == ROOT_TASK_NAME)
return ROOT_TASK_NAME;
static GStrv
tokenize_line (const gchar *line)
{
GStrv tokens = NULL;
gsize i;
if (g_str_has_prefix (token , "due:"))
return TASK_DUE_DATE;
tokens = g_strsplit (line, " ", -1);
return -1;
for (i = 0; tokens && tokens[i]; i++)
g_strstrip (tokens[i]);
return tokens;
}
void
gtd_todo_txt_parser_parse_tokens (GtdTask *task,
GList *tokens)
GtdTask*
gtd_todo_txt_parser_parse_task (GtdProvider *provider,
const gchar *line,
gchar **out_list_name)
{
g_autoptr (GtdTask) task = NULL;
g_auto (GStrv) tokens = NULL;
GDateTime *dt;
GString *list_name;
GString *title;
GString *root_task_name;
GList *l;
GString *parent_task_name;
Token last_token;
Token token_id;
gboolean is_subtask;
gint last_read_token;
gint token_id;
guint i;
l = NULL;
dt = NULL;
is_subtask = FALSE;
title = g_string_new (NULL);
list_name = g_string_new (NULL);
root_task_name = g_string_new (NULL);
parent_task_name = g_string_new (NULL);
last_token = TOKEN_START;
last_read_token = TASK_COMPLETE;
task = gtd_provider_generate_task (provider);
tokens = tokenize_line (line);
for (l = tokens; l != NULL; l = l->next)
for (i = 0; tokens && tokens[i]; i++)
{
const gchar *token;
gchar *str;
g_strstrip (l->data);
str = l->data;
token_id = gtd_todo_txt_parser_get_token_id (l->data, last_read_token);
token = tokens[i];
token_id = parse_token_id (token, last_token);
switch (token_id)
{
case TASK_COMPLETE:
last_read_token = TASK_COMPLETE;
case TOKEN_COMPLETE:
gtd_task_set_complete (task, TRUE);
break;
case TASK_PRIORITY:
last_read_token = TASK_PRIORITY;
gtd_task_set_priority (task, gtd_todo_txt_parser_get_priority (l->data));
case TOKEN_PRIORITY:
last_token = TOKEN_PRIORITY;
gtd_task_set_priority (task, parse_priority (token));
break;
case TASK_DATE:
last_read_token = TASK_DATE;
dt = gtd_todo_txt_parser_get_date (l->data);
case TOKEN_DATE:
dt = parse_date (token);
break;
case TASK_TITLE:
last_read_token = TASK_TITLE;
g_string_append (title, l->data);
case TOKEN_TITLE:
g_string_append (title, token);
g_string_append (title, " ");
break;
case TASK_LIST_NAME:
last_read_token = TASK_LIST_NAME;
g_string_append (list_name, l->data);
case TOKEN_LIST_NAME:
g_string_append (list_name, token);
g_string_append (list_name, " ");
break;
case ROOT_TASK_NAME:
last_read_token = ROOT_TASK_NAME;
is_subtask = TRUE;
g_string_append (root_task_name, l->data);
g_string_append (root_task_name, " ");
break;
case TASK_DUE_DATE:
last_read_token = TASK_DUE_DATE;
dt = gtd_todo_txt_parser_get_date (&str[4]);
case TOKEN_DUE_DATE:
dt = parse_date (token + strlen ("due:"));
gtd_task_set_due_date (task, dt);
break;
case TOKEN_LIST_COLOR:
case TOKEN_START:
default:
return;
break;
}
last_token = token_id;
}
g_strstrip (title->str);
g_strstrip (parent_task_name->str);
g_strstrip (list_name->str);
g_strstrip (root_task_name->str);
g_strstrip (title->str);
gtd_task_set_title (task, title->str);
g_object_set_data_full (G_OBJECT (task), "list_name", g_strdup (list_name->str + 1), g_free);
if (is_subtask)
g_object_set_data_full (G_OBJECT (task), "root_task_name", g_strdup (root_task_name->str + 1), g_free);
if (out_list_name)
*out_list_name = g_strdup (list_name->str + 1);
g_string_free (root_task_name, TRUE);
g_string_free (parent_task_name, TRUE);
g_string_free (list_name, TRUE);
g_string_free (title, TRUE);
return g_steal_pointer (&task);
}
gboolean
gtd_todo_txt_parser_validate_token_format (GList *tokens)
/**
* gtd_todo_txt_parser_parse_task_list:
* provider: the @GtdProvider of the new tasklist
* @line: the tasklist line to be parsed
*
* Parses a @GtdTaskList from @line. If there is a 'color:' token,
* it is taken into account.
*
* Returns: (transfer full)(nullable): A @GtdTaskList
*/
GtdTaskList*
gtd_todo_txt_parser_parse_task_list (GtdProvider *provider,
const gchar *line)
{
GList *it = NULL;
gint token_id;
gint position = 0;
g_autofree gchar *color = NULL;
g_auto (GStrv) tokens = NULL;
GtdTaskList *new_list;
GString *list_name;
guint i;
gboolean complete_tk = FALSE;
gboolean priority_tk = FALSE;
gboolean task_list_name_tk = FALSE;
tokens = tokenize_line (line);
list_name = g_string_new (NULL);
gint last_read = TASK_COMPLETE;
GTD_TRACE_MSG ("Parsing tasklist from line '%s'", line);
for (it = tokens; it != NULL; it = it->next)
for (i = 0; tokens && tokens[i]; i++)
{
gchar *str;
const gchar *token = tokens[i];
str = it->data;
token_id = gtd_todo_txt_parser_get_token_id (it->data, last_read);
position++;
if (!token)
break;
switch (token_id)
{
case TASK_COMPLETE:
last_read = TASK_COMPLETE;
if (position != 1)
return FALSE;
else
complete_tk = TRUE;
if (g_str_has_prefix (token, "color:"))
color = g_strdup (token + strlen ("color:"));
else
g_string_append_printf (list_name, "%s ", token[0] == '@' ? token + 1 : token);
}
break;
if (list_name->len == 0)
{
g_string_free (list_name, TRUE);
return NULL;
}
case TASK_PRIORITY:
last_read = TASK_PRIORITY;
g_strstrip (list_name->str);
if (position != (complete_tk + 1))
return FALSE;
else
priority_tk = TRUE;
new_list = g_object_new (GTD_TYPE_TASK_LIST,
"provider", provider,
"name", list_name->str,
"is-removable", TRUE,
NULL);
break;
if (color)
{
GdkRGBA rgba;
case TASK_DATE:
last_read = TASK_DATE;
gdk_rgba_parse (&rgba, color);