/* Copyright (C) 2007-2008 Christian Dywan This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. See the file COPYING for the full license text. */ /** * TODO: * - Support info > metadata, alias, added, modified, visited * - Compatibility: The Nokia 770 *requires* metadata and folder * - Compatibility: Kazehakase's bookmarks * - Compatibility: Epiphany's bookmarks * - XML Indentation **/ #include "katze-xbel.h" #include #include #include #include #include #include "katze-utils.h" struct _KatzeXbelItem { GObject parent_instance; guint refs; KatzeXbelItemKind kind; KatzeXbelItem* parent; GList* items; gboolean folded; gchar* title; gchar* desc; gchar* href; }; G_DEFINE_TYPE (KatzeXbelItem, katze_xbel_item, G_TYPE_OBJECT) static void katze_xbel_item_finalize (GObject* object); static void katze_xbel_item_class_init (KatzeXbelItemClass* class) { GObjectClass* gobject_class = G_OBJECT_CLASS (class); gobject_class->finalize = katze_xbel_item_finalize; } static void katze_xbel_item_init (KatzeXbelItem* item) { item->parent = NULL; } static void katze_xbel_item_finalize (GObject* object) { KatzeXbelItem* item = KATZE_XBEL_ITEM (object); if (item->parent) katze_xbel_folder_remove_item (item->parent, item); if (item->kind == KATZE_XBEL_ITEM_KIND_FOLDER) { guint n = katze_xbel_folder_get_n_items (item); guint i; for (i = 0; i < n; i++) { KatzeXbelItem* _item = katze_xbel_folder_get_nth_item (item, i); _item->parent = NULL; katze_xbel_item_unref (_item); } g_list_free (item->items); } if (item->kind != KATZE_XBEL_ITEM_KIND_SEPARATOR) { g_free (item->title); g_free (item->desc); } if (item->kind == KATZE_XBEL_ITEM_KIND_BOOKMARK) g_free (item->href); G_OBJECT_CLASS (katze_xbel_item_parent_class)->finalize (object); } /* Create a new item of a certain type */ static KatzeXbelItem* katze_xbel_item_new (KatzeXbelItemKind kind) { KatzeXbelItem* item = g_object_new (KATZE_TYPE_XBEL_ITEM, NULL); item->kind = kind; if (kind == KATZE_XBEL_ITEM_KIND_FOLDER) { item->items = NULL; item->folded = TRUE; } if (kind != KATZE_XBEL_ITEM_KIND_SEPARATOR) { item->title = NULL; item->desc = NULL; } if (kind == KATZE_XBEL_ITEM_KIND_BOOKMARK) item->href = g_strdup (""); return item; } /** * katze_xbel_bookmark_new: * * Create a new empty bookmark. * * Return value: a newly allocated bookmark **/ KatzeXbelItem* katze_xbel_bookmark_new (void) { return katze_xbel_item_new (KATZE_XBEL_ITEM_KIND_BOOKMARK); } static KatzeXbelItem* katze_xbel_bookmark_from_xmlNodePtr (xmlNodePtr cur) { g_return_val_if_fail (cur, NULL); KatzeXbelItem* bookmark = katze_xbel_bookmark_new (); xmlChar* key = xmlGetProp (cur, (xmlChar*)"href"); katze_xbel_bookmark_set_href (bookmark, (gchar*)key); cur = cur->xmlChildrenNode; while (cur) { if (!xmlStrcmp (cur->name, (const xmlChar*)"title")) { xmlChar* key = xmlNodeGetContent (cur); katze_xbel_item_set_title (bookmark, g_strstrip ((gchar*)key)); } else if (!xmlStrcmp (cur->name, (const xmlChar*)"desc")) { xmlChar* key = xmlNodeGetContent (cur); katze_xbel_item_set_desc (bookmark, g_strstrip ((gchar*)key)); } cur = cur->next; } return bookmark; } /** * katze_katze_xbel_separator_new: * * Create a new separator. * * The returned item must be freed eventually. * * Return value: a newly allocated separator. **/ KatzeXbelItem* katze_xbel_separator_new (void) { return katze_xbel_item_new (KATZE_XBEL_ITEM_KIND_SEPARATOR); } /** * katze_xbel_folder_new: * * Create a new empty folder. * * The returned item must be freed eventually. * * Return value: a newly allocated folder. **/ KatzeXbelItem* katze_xbel_folder_new (void) { return katze_xbel_item_new (KATZE_XBEL_ITEM_KIND_FOLDER); } /* Create a folder from an xmlNodePtr */ static KatzeXbelItem* katze_xbel_folder_from_xmlNodePtr (xmlNodePtr cur) { g_return_val_if_fail (cur, NULL); KatzeXbelItem* folder = katze_xbel_folder_new (); xmlChar* key = xmlGetProp (cur, (xmlChar*)"folded"); if (key) { if (!g_ascii_strncasecmp ((gchar*)key, "yes", 3)) folder->folded = TRUE; else if (!g_ascii_strncasecmp ((gchar*)key, "no", 2)) folder->folded = FALSE; else g_warning ("XBEL: Unknown value for folded."); xmlFree (key); } cur = cur->xmlChildrenNode; while (cur) { if (!xmlStrcmp (cur->name, (const xmlChar*)"title")) { xmlChar* key = xmlNodeGetContent (cur); katze_assign (folder->title, g_strstrip ((gchar*)key)); } else if (!xmlStrcmp (cur->name, (const xmlChar*)"desc")) { xmlChar* key = xmlNodeGetContent (cur); katze_assign (folder->desc, g_strstrip ((gchar*)key)); } else if (!xmlStrcmp (cur->name, (const xmlChar*)"folder")) { KatzeXbelItem* item = katze_xbel_folder_from_xmlNodePtr (cur); item->parent = folder; folder->items = g_list_prepend (folder->items, item); } else if (!xmlStrcmp (cur->name, (const xmlChar*)"bookmark")) { KatzeXbelItem* item = katze_xbel_bookmark_from_xmlNodePtr (cur); item->parent = folder; folder->items = g_list_prepend (folder->items, item); } else if (!xmlStrcmp (cur->name, (const xmlChar*)"separator")) { KatzeXbelItem* item = katze_xbel_separator_new (); item->parent = folder; folder->items = g_list_prepend (folder->items, item); } cur = cur->next; } /* Prepending and reversing is faster than appending */ folder->items = g_list_reverse (folder->items); return folder; } /* Loads the contents from an xmlNodePtr into a folder. */ static gboolean katze_xbel_folder_from_xmlDocPtr (KatzeXbelItem* folder, xmlDocPtr doc) { g_return_val_if_fail (katze_xbel_folder_is_empty (folder), FALSE); g_return_val_if_fail (doc, FALSE); xmlNodePtr cur = xmlDocGetRootElement (doc); xmlChar* version = xmlGetProp (cur, (xmlChar*)"version"); if (xmlStrcmp (version, (xmlChar*)"1.0")) g_warning ("XBEL version is not 1.0."); xmlFree (version); katze_assign (folder->title, (gchar*)xmlGetProp (cur, (xmlChar*)"title")); katze_assign (folder->desc, (gchar*)xmlGetProp (cur, (xmlChar*)"desc")); if ((cur = xmlDocGetRootElement (doc)) == NULL) { /* Empty document */ return FALSE; } if (xmlStrcmp (cur->name, (const xmlChar*)"xbel")) { /* Wrong document kind */ return FALSE; } cur = cur->xmlChildrenNode; while (cur) { KatzeXbelItem* item = NULL; if (!xmlStrcmp (cur->name, (const xmlChar*)"folder")) item = katze_xbel_folder_from_xmlNodePtr (cur); else if (!xmlStrcmp (cur->name, (const xmlChar*)"bookmark")) item = katze_xbel_bookmark_from_xmlNodePtr (cur); else if (!xmlStrcmp (cur->name, (const xmlChar*)"separator")) item = katze_xbel_separator_new (); /*else if (!xmlStrcmp (cur->name, (const xmlChar*)"info")) item = katze_xbel_parse_info (xbel, cur);*/ if (item) { item->parent = folder; folder->items = g_list_prepend (folder->items, item); } cur = cur->next; } /* Prepending and reversing is faster than appending */ folder->items = g_list_reverse (folder->items); return TRUE; } /** * katze_xbel_item_ref: * @item: a valid item * * Ref an KatzeXbelItem. * * Ref means that the reference count is increased by one. * * This has no effect on children of a folder. **/ void katze_xbel_item_ref (KatzeXbelItem* item) { g_return_if_fail (KATZE_IS_XBEL_ITEM (item)); g_object_ref (item); } /** * katze_xbel_item_unref: * @item: a valid item * * Unref an KatzeXbelItem. If @item is a folder all of its children will also * be unreffed automatically. * * Unref means that the reference count is decreased. If there are no * references left, the memory will be freed and if needed removed from * its containing folder. **/ void katze_xbel_item_unref (KatzeXbelItem* item) { g_return_if_fail (KATZE_IS_XBEL_ITEM (item)); g_object_unref (item); } /** * katze_xbel_item_copy: * @item: the item to copy * * Copy an KatzeXbelItem. * * The returned item must be unreffed eventually. * * Return value: a copy of @item **/ KatzeXbelItem* katze_xbel_item_copy (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), NULL); KatzeXbelItem* copy = katze_xbel_item_new (item->kind); if (katze_xbel_item_is_folder (item)) { guint n = katze_xbel_folder_get_n_items (item); guint i; for (i = 0; i < n; i++) { KatzeXbelItem* _item = katze_xbel_folder_get_nth_item (item, i); katze_xbel_folder_append_item (copy, katze_xbel_item_copy (_item)); } } if (item->kind != KATZE_XBEL_ITEM_KIND_SEPARATOR) { katze_xbel_item_set_title (copy, item->title); katze_xbel_item_set_desc (copy, item->desc); } if (item->kind == KATZE_XBEL_ITEM_KIND_BOOKMARK) katze_xbel_bookmark_set_href (copy, item->href); return copy; } /** * katze_xbel_folder_append_item: * @folder: a folder * @item: the item to append * * Append the given item to a folder. * * The item is actually moved and must not be contained in another folder. * **/ void katze_xbel_folder_append_item (KatzeXbelItem* folder, KatzeXbelItem* item) { g_return_if_fail (!katze_xbel_item_get_parent (item)); g_return_if_fail (katze_xbel_item_is_folder (folder)); folder->items = g_list_append (folder->items, item); item->parent = folder; } /** * katze_xbel_folder_prepend_item: * @folder: a folder * @item: the item to prepend * * Prepend the given item to a folder. * * The item is actually moved and must not be contained in another folder. * **/ void katze_xbel_folder_prepend_item (KatzeXbelItem* folder, KatzeXbelItem* item) { g_return_if_fail (!katze_xbel_item_get_parent (item)); g_return_if_fail (katze_xbel_item_is_folder (folder)); folder->items = g_list_prepend (folder->items, item); item->parent = folder; } /** * katze_xbel_folder_remove_item: * @folder: a folder * @item: the item to remove * * Remove the given @item from a @folder. **/ void katze_xbel_folder_remove_item (KatzeXbelItem* folder, KatzeXbelItem* item) { g_return_if_fail (KATZE_IS_XBEL_ITEM (item)); g_return_if_fail (katze_xbel_item_get_parent(folder) != item); /* Fortunately we know that items are unique */ folder->items = g_list_remove (folder->items, item); item->parent = NULL; } /** * katze_xbel_folder_get_n_items: * @folder: a folder * * Retrieve the number of items contained in the given @folder. * * Return value: number of items **/ guint katze_xbel_folder_get_n_items (KatzeXbelItem* folder) { g_return_val_if_fail (katze_xbel_item_is_folder (folder), 0); return g_list_length (folder->items); } /** * katze_xbel_folder_get_nth_item: * @folder: a folder * @n: the index of an item * * Retrieve an item contained in the given @folder by its index. * * Return value: the item at the given index or %NULL **/ KatzeXbelItem* katze_xbel_folder_get_nth_item (KatzeXbelItem* folder, guint n) { g_return_val_if_fail (katze_xbel_item_is_folder(folder), NULL); return (KatzeXbelItem*) g_list_nth_data (folder->items, n); } /** * katze_xbel_folder_is_empty: * @folder: A folder. * * Determines wether or not a folder contains no items. This is significantly * faster than katze_xbel_folder_get_n_items for this particular purpose. * * Return value: Wether the given @folder is folded. **/ gboolean katze_xbel_folder_is_empty (KatzeXbelItem* folder) { return !katze_xbel_folder_get_nth_item (folder, 0); } /** * katze_xbel_folder_get_folded: * @folder: A folder. * * Determines wether or not a folder is folded. If a folder is not folded * it should not be exposed in a user interface. * * New folders are folded by default. * * Return value: Wether the given @folder is folded. **/ gboolean katze_xbel_folder_get_folded (KatzeXbelItem* folder) { g_return_val_if_fail (katze_xbel_item_is_folder (folder), TRUE); return folder->folded; } /** * katze_xbel_item_get_kind: * @item: A item. * * Determines the kind of an item. * * Return value: The kind of the given @item. **/ KatzeXbelItemKind katze_xbel_item_get_kind (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), KATZE_XBEL_ITEM_KIND_BOOKMARK); return item->kind; } /** * katze_xbel_item_get_parent: * @item: A valid item. * * Retrieves the parent folder of an item. * * Return value: The parent folder of the given @item or %NULL. **/ KatzeXbelItem* katze_xbel_item_get_parent (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), NULL); return item->parent; } /** * katze_xbel_item_get_title: * @item: A valid item. * * Retrieves the title of an item. * * Return value: The title of the given @item or %NULL. **/ G_CONST_RETURN gchar* katze_xbel_item_get_title (KatzeXbelItem* item) { g_return_val_if_fail (!katze_xbel_item_is_separator (item), NULL); return item->title; } /** * katze_xbel_item_get_desc: * @item: A valid item. * * Retrieves the description of an item. * * Return value: The description of the @item or %NULL. **/ G_CONST_RETURN gchar* katze_xbel_item_get_desc (KatzeXbelItem* item) { g_return_val_if_fail (!katze_xbel_item_is_separator (item), NULL); return item->desc; } /** * katze_xbel_bookmark_get_href: * @bookmark: A bookmark. * * Retrieves the uri of a bookmark. The value is guaranteed to not be %NULL. * * Return value: The uri of the @bookmark. **/ G_CONST_RETURN gchar* katze_xbel_bookmark_get_href (KatzeXbelItem* bookmark) { g_return_val_if_fail (katze_xbel_item_is_bookmark (bookmark), NULL); return bookmark->href; } /** * katze_xbel_item_is_bookmark: * @item: A valid item. * * Determines wether or not an item is a bookmark. * * Return value: %TRUE if the @item is a bookmark. **/ gboolean katze_xbel_item_is_bookmark (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), FALSE); return item->kind == KATZE_XBEL_ITEM_KIND_BOOKMARK; } /** * katze_xbel_item_is_separator: * @item: A valid item. * * Determines wether or not an item is a separator. * * Return value: %TRUE if the @item is a separator. **/ gboolean katze_xbel_item_is_separator (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), FALSE); return item->kind == KATZE_XBEL_ITEM_KIND_SEPARATOR; } /** * katze_xbel_item_is_folder: * @item: A valid item. * * Determines wether or not an item is a folder. * * Return value: %TRUE if the item is a folder. **/ gboolean katze_xbel_item_is_folder (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), FALSE); return item->kind == KATZE_XBEL_ITEM_KIND_FOLDER; } /** * katze_xbel_folder_set_folded: * @folder: A folder. * @folded: TRUE if the folder is folded. * * Sets the foldedness of the @folder. **/ void katze_xbel_folder_set_folded (KatzeXbelItem* folder, gboolean folded) { g_return_if_fail (katze_xbel_item_is_folder (folder)); folder->folded = folded; } /** * katze_xbel_item_set_title: * @item: A valid item. * @title: A string to use for the title. * * Sets the title of the @item. **/ void katze_xbel_item_set_title (KatzeXbelItem* item, const gchar* title) { g_return_if_fail (!katze_xbel_item_is_separator (item)); katze_assign (item->title, g_strdup (title)); } /** * katze_xbel_item_set_desc: * @item: A valid item. * @title: A string to use for the description. * * Sets the description of the @item. **/ void katze_xbel_item_set_desc (KatzeXbelItem* item, const gchar* desc) { g_return_if_fail (!katze_xbel_item_is_separator (item)); katze_assign (item->desc, g_strdup (desc)); } /** * katze_xbel_bookmark_set_href: * @bookmark: A bookmark. * @href: A string containing a valid uri. * * Sets the uri of the bookmark. * * The uri must not be %NULL. * * This uri is not currently validated in any way. This may change in the future. **/ void katze_xbel_bookmark_set_href (KatzeXbelItem* bookmark, const gchar* href) { g_return_if_fail (katze_xbel_item_is_bookmark (bookmark)); g_return_if_fail (href); katze_assign (bookmark->href, g_strdup (href)); } gboolean katze_xbel_folder_from_data (KatzeXbelItem* folder, const gchar* data, GError** error) { g_return_val_if_fail (katze_xbel_folder_is_empty (folder), FALSE); g_return_val_if_fail (data, FALSE); xmlDocPtr doc; if((doc = xmlParseMemory (data, strlen (data))) == NULL) { /* No valid xml or broken encoding */ *error = g_error_new_literal (KATZE_XBEL_ERROR, KATZE_XBEL_ERROR_READ, _("Malformed document.")); return FALSE; } if (!katze_xbel_folder_from_xmlDocPtr (folder, doc)) { /* Parsing failed */ xmlFreeDoc(doc); *error = g_error_new_literal (KATZE_XBEL_ERROR, KATZE_XBEL_ERROR_READ, _("Malformed document.")); return FALSE; } xmlFreeDoc(doc); return TRUE; } /** * katze_xbel_folder_from_file: * @folder: An empty folder. * @file: A relative path to a file. * @error: return location for a GError or %NULL * * Tries to load @file. * * Return value: %TRUE on success or %FALSE when an error occured. **/ gboolean katze_xbel_folder_from_file (KatzeXbelItem* folder, const gchar* file, GError** error) { g_return_val_if_fail (katze_xbel_folder_is_empty (folder), FALSE); g_return_val_if_fail (file, FALSE); if (!g_file_test (file, G_FILE_TEST_EXISTS)) { /* File doesn't exist */ *error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, _("File not found.")); return FALSE; } xmlDocPtr doc; if ((doc = xmlParseFile (file)) == NULL) { /* No valid xml or broken encoding */ *error = g_error_new_literal (KATZE_XBEL_ERROR, KATZE_XBEL_ERROR_READ, _("Malformed document.")); return FALSE; } if (!katze_xbel_folder_from_xmlDocPtr (folder, doc)) { /* Parsing failed */ xmlFreeDoc (doc); *error = g_error_new_literal (KATZE_XBEL_ERROR, KATZE_XBEL_ERROR_READ, _("Malformed document.")); return FALSE; } xmlFreeDoc (doc); return TRUE; } static gchar* katze_xbel_xml_element (const gchar* name, const gchar* value) { if (!value) return g_strdup (""); gchar* valueEscaped = g_markup_escape_text (value, -1); gchar* markup = g_strdup_printf ("<%s>%s\n", name, valueEscaped, name); g_free (valueEscaped); return markup; } gchar* katze_xbel_item_to_data (KatzeXbelItem* item) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (item), NULL); gchar* markup = NULL; switch (item->kind) { case KATZE_XBEL_ITEM_KIND_FOLDER: { GString* _markup = g_string_new (NULL); guint n = katze_xbel_folder_get_n_items (item); guint i; for (i = 0; i < n; i++) { KatzeXbelItem* _item = katze_xbel_folder_get_nth_item (item, i); gchar* item_markup = katze_xbel_item_to_data (_item); g_string_append (_markup, item_markup); g_free (item_markup); } gchar* folded = item->folded ? NULL : g_strdup_printf (" folded=\"no\""); gchar* title = katze_xbel_xml_element ("title", item->title); gchar* desc = katze_xbel_xml_element ("desc", item->desc); markup = g_strdup_printf ("\n%s%s%s\n", folded ? folded : "", title, desc, g_string_free (_markup, FALSE)); g_free (folded); g_free (title); g_free (desc); break; } case KATZE_XBEL_ITEM_KIND_BOOKMARK: { gchar* href_escaped = g_markup_escape_text (item->href, -1); gchar* href = g_strdup_printf (" href=\"%s\"", href_escaped); g_free (href_escaped); gchar* title = katze_xbel_xml_element ("title", item->title); gchar* desc = katze_xbel_xml_element ("desc", item->desc); markup = g_strdup_printf ("\n%s%s%s\n", href, title, desc, ""); g_free (href); g_free (title); g_free (desc); break; } case KATZE_XBEL_ITEM_KIND_SEPARATOR: markup = g_strdup ("\n"); break; default: g_warning ("XBEL: Unknown item kind"); } return markup; } static gchar* katze_xbel_folder_to_data (KatzeXbelItem* folder, gsize* length, GError** error) { g_return_val_if_fail (katze_xbel_item_is_folder (folder), FALSE); GString* inner_markup = g_string_new (NULL); guint n = katze_xbel_folder_get_n_items (folder); guint i; for (i = 0; i < n; i++) { KatzeXbelItem* item = katze_xbel_folder_get_nth_item (folder, i); gchar* sItem = katze_xbel_item_to_data (item); g_string_append (inner_markup, sItem); g_free (sItem); } gchar* title = katze_xbel_xml_element ("title", folder->title); gchar* desc = katze_xbel_xml_element ("desc", folder->desc); gchar* outer_markup; outer_markup = g_strdup_printf ( "%s%s\n%s%s%s\n", "\n", "\n", title, desc, g_string_free (inner_markup, FALSE)); g_free (title); g_free (desc); if (length) *length = strlen (outer_markup); return outer_markup; } /** * katze_xbel_folder_to_file: * @folder: A folder. * @file: The destination filename. * @error: return location for a GError or %NULL * * Write the contents of @folder to the specified file, creating it if necessary. * * Return value: %TRUE on success or %FALSE when an error occured. **/ gboolean katze_xbel_folder_to_file (KatzeXbelItem* folder, const gchar* file, GError** error) { g_return_val_if_fail (KATZE_IS_XBEL_ITEM (folder), FALSE); g_return_val_if_fail (file, FALSE); gchar* data; if (!(data = katze_xbel_folder_to_data (folder, NULL, error))) return FALSE; FILE* fp; if (!(fp = fopen (file, "w"))) { *error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_ACCES, _("Writing failed.")); return FALSE; } fputs (data, fp); fclose (fp); g_free (data); return TRUE; }