Browse Source

Org-feed.el: Make node creation template-based.

Carsten Dominik 16 years ago
parent
commit
7abe1a711d
1 changed files with 127 additions and 64 deletions
  1. 127 64
      lisp/org-feed.el

+ 127 - 64
lisp/org-feed.el

@@ -37,35 +37,36 @@
 ;;    (setq org-feed-alist
 ;;    (setq org-feed-alist
 ;;          '(("ReQall"
 ;;          '(("ReQall"
 ;;             "http://www.reqall.com/user/feeds/rss/a1b2c3....."
 ;;             "http://www.reqall.com/user/feeds/rss/a1b2c3....."
-;;             "~/org/feeds.org" "ReQall Entries" nil)
+;;             "~/org/feeds.org" "ReQall Entries")
 ;;
 ;;
 ;;  With this setup, the command `M-x org-feed-update-all' will
 ;;  With this setup, the command `M-x org-feed-update-all' will
 ;;  collect new entries in the feed at the given URL and create
 ;;  collect new entries in the feed at the given URL and create
-;;  entries as subheading under the "ReQall Entries" heading in the
-;;  file "~/org.feeds.org".  The final element in the alist entry in
-;;  this list can be a filter function to further process the parsed
-;;  information.  For example, here we turn entries with
+;;  entries as subheadings under the "ReQall Entries" heading in the
+;;  file "~/org.feeds.org". 
+;;  In addition to these standard arguments, additional keyword-value
+;;  pairs are possible.  For example, here we turn entries with
 ;;  "<category>Task</category>" into TODO entries by adding the
 ;;  "<category>Task</category>" into TODO entries by adding the
-;;  keyword to the title:
+;;  keyword to the title, usinf the `:filter' argument:
 ;;
 ;;
 ;;    (setq org-feed-alist
 ;;    (setq org-feed-alist
 ;;          '(("ReQall"
 ;;          '(("ReQall"
 ;;             "http://www.reqall.com/user/feeds/rss/a1b2c3....."
 ;;             "http://www.reqall.com/user/feeds/rss/a1b2c3....."
 ;;             "~/org/feeds.org" "ReQall Entries"
 ;;             "~/org/feeds.org" "ReQall Entries"
-;;             my-raquall-filter)))
+;;             :filter my-reqall-filter)))
 ;;
 ;;
-;;    (defun my-requall-filter (e)
+;;    (defun my-reqall-filter (e)
 ;;      (when (equal (plist-get e :category) "Task")
 ;;      (when (equal (plist-get e :category) "Task")
 ;;        (setq e (plist-put e :title
 ;;        (setq e (plist-put e :title
 ;;                          (concat "TODO " (plist-get e :title)))))
 ;;                          (concat "TODO " (plist-get e :title)))))
 ;;      e)
 ;;      e)
 ;;
 ;;
-;;  Another possibility for the filter function would be to format
-;;  the entire Org node for the feed item, by adding the formatted
-;;  entry as a `:formatted-for-org' property:
+;;  A `:template' entry in the alist would override the template
+;;  in `org-feed-default-template' for the construction of the outline
+;;  node to be inserted.  Another possibility would be for the filter
+;;  function to create the Org node for the feed item, by adding the
+;;  formatted entry as a `:formatted-for-org' property:
 ;;
 ;;
-;;
-;;    (defun my-requall-filter (e)
+;;    (defun my-reqall-filter (e)
 ;;      (setq e (plist-put
 ;;      (setq e (plist-put
 ;;               e :formatted-for-org
 ;;               e :formatted-for-org
 ;;               (format "* %s\n%s"
 ;;               (format "* %s\n%s"
@@ -84,7 +85,7 @@
 ;;  org-feed.el needs to keep track of GUIDs in the feed it has
 ;;  org-feed.el needs to keep track of GUIDs in the feed it has
 ;;  already processed.  It does so by listing them in a special
 ;;  already processed.  It does so by listing them in a special
 ;;  drawer, FEEDGUIDS, under the heading that received the input of
 ;;  drawer, FEEDGUIDS, under the heading that received the input of
-;;  te feed.  You should add FEEDGUIDS to your list of drawers
+;;  the feed.  You should add FEEDGUIDS to your list of drawers
 ;;  in the files that receive feed input:
 ;;  in the files that receive feed input:
 ;;
 ;;
 ;;    #+DRAWERS: PROPERTIES LOGBOOK FEEDGUIDS
 ;;    #+DRAWERS: PROPERTIES LOGBOOK FEEDGUIDS
@@ -92,10 +93,14 @@
 ;;  Acknowledgements
 ;;  Acknowledgements
 ;;  ----------------
 ;;  ----------------
 ;;
 ;;
-;;  org-feed.el is based on ideas by Brad Bozarth who implemented it
-;;  using shell and awk scripts, and who in this way made me for the
-;;  first time look into an RSS feed, showing me how simple this really
-;;  was.
+;;  org-feed.el is based on ideas by Brad Bozarth who implemented a
+;;  similar mechanism using shell and awk scripts, and who in this
+;;  way made me for the first time look into an RSS feed, showing
+;;  how simple this really was.  Because I wanted to include a
+;;  solution into Org with as few dependencies as possible, I
+;;  reimplemented his ideas in Emacs Lisp.
+
+;;; Code:
 
 
 (require 'org)
 (require 'org)
 
 
@@ -116,33 +121,66 @@ name         a custom name for this feed
 URL          the Feed URL
 URL          the Feed URL
 file         the target Org file where entries should be listed
 file         the target Org file where entries should be listed
 headline     the headline under which entries should be listed
 headline     the headline under which entries should be listed
-filter       a filter function to modify the property list before
-             an Org entry is created from it.
+
+Additional argumetns can be given using keyword-value pairs:
+
+:template template-string
+             The template to create an Org node from a feed item
+
+:filter filter-function
+             A function to filter entries before Org nodes are
+             created from them.
+
+If no template is given, the one in `org-feed-default-template' is used.
+See the docstring of that variable for information on the syntax of this
+template.  If creating the node required more logic than a template can
+provide, this task can be delegated to the filter function.
 
 
 The filter function gets as a argument a property list describing the item.
 The filter function gets as a argument a property list describing the item.
 That list has a property for each field, for example `:title' for the
 That list has a property for each field, for example `:title' for the
 `<title>' field and `:pubDate' for the publication date.  In addition,
 `<title>' field and `:pubDate' for the publication date.  In addition,
 it contains the following properties:
 it contains the following properties:
 
 
-`:item-full-text'   the full text in the <item> tag.
+`:item-full-text'   the full text in the <item> tag
 `:guid-permalink'   t when the guid property is a permalink
 `:guid-permalink'   t when the guid property is a permalink
 
 
 The filter function can modify the existing fields before an item
 The filter function can modify the existing fields before an item
-is constructed from the `:title', `:pubDate', `:link', `:guid', and
-`:description' fields.  For more control, the filter can construct
-the Org item itself, by adding a `:formatted-for-org' property that
-specifies the complete outline node that should be added.
+is constructed using the template.  Or it can construct the node directly,
+by adding a `:formatted-for-org' property that specifies the complete
+outline node that should be added.
 
 
-If the filter returns nil for some entries, these will be marked as seen
-but *not* inserted into the inbox."
+The filter should return the modified entry property list.  It may also
+return nil to indicate that this entry should not be added to the Org file
+at all."
   :group 'org-feed
   :group 'org-feed
   :type '(repeat
   :type '(repeat
-	  (list :value ("" "http://" "" "" nil)
+	  (list :value ("" "http://" "" "")
 	   (string :tag "Name")
 	   (string :tag "Name")
 	   (string :tag "Feed URL")
 	   (string :tag "Feed URL")
 	   (file :tag "File for inbox")
 	   (file :tag "File for inbox")
 	   (string :tag "Headline for inbox")
 	   (string :tag "Headline for inbox")
-	   (symbol :tag "Filter Function"))))
+	   (repeat :inline t
+		   (choice
+		    (list :inline t :tag "Template"
+			  (const :template) (string :tag "Template"))
+		    (list :inline t :tag "Filter"
+			  (const :filter) (symbol :tag "Filter Function")))))))
+
+(defcustom org-feed-default-template "* %h\n  %U\n  %description\n  %a\n"
+  "Template for the Org node created from RSS feed items.
+This is just the default, each feed can specify its own.
+Any fields from the feed item can be interpolated into the template with
+%name, for example %title, %description, %pubDate etc.  In addition, the
+following special escapes are valid as well:
+
+%h      the title, or the first line of the description
+%t      the date as a stamp, either from <pubDate> (if present), or
+        the current date.
+%T      date and time
+%u,%U   like %t,%T, but inactive time stamps
+%a      A link, from <guid> if that is a permalink, else from <link>"
+  :group 'org-feed
+  :type '(string :tag "Template"))
 
 
 (defcustom org-feed-save-after-adding t
 (defcustom org-feed-save-after-adding t
   "Non-nil means, save buffer after adding new feed items."
   "Non-nil means, save buffer after adding new feed items."
@@ -165,11 +203,12 @@ of the file pointed to by the URL."
   "Non-nil means, assume feeds to be stable.
   "Non-nil means, assume feeds to be stable.
 A stable feed is one which only adds and removes items, but never removes
 A stable feed is one which only adds and removes items, but never removes
 an item with a given GUID and then later adds it back in.  So if the feed
 an item with a given GUID and then later adds it back in.  So if the feed
-is stable, this means we can simple remember the GUIDs present as the ones
-we have seen, and we can forget GUIDs that used to be in the feed but no
-longer are.  So for stable feeds, we only need to remember a limited
-number of GUIDs.  For unstable ones, we need to remember all GUIDs we have
-ever seen, which can be a very long list indeed."
+is stable, this means we can simple remember the GUIDs present in the feed
+at any given time, as the ones we have seen and precessed.  So we can
+forget GUIDs that used to be in the feed but no longer are.
+Thus, for stable feeds, we only need to remember a limited number of GUIDs.
+For unstable ones, we need to remember all GUIDs we have ever seen, which
+can be a very long list indeed."
   :group 'org-feed
   :group 'org-feed
   :type 'boolean)
   :type 'boolean)
 
 
@@ -217,7 +256,9 @@ it can be a list structured like an entry in `org-feed-alist'."
 	(feed-url (nth 1 feed))
 	(feed-url (nth 1 feed))
 	(feed-file (nth 2 feed))
 	(feed-file (nth 2 feed))
 	(feed-headline (nth 3 feed))
 	(feed-headline (nth 3 feed))
-	(feed-formatter (nth 4 feed))
+	(feed-filter (nth 1 (memq :filter feed)))
+	(feed-template (or (nth 1 (memq :template feed))
+			   org-feed-default-template))
 	feed-buffer feed-pos
 	feed-buffer feed-pos
 	entries entries2 old-guids current-guids new new-selected e)
 	entries entries2 old-guids current-guids new new-selected e)
     (setq feed-buffer (org-feed-get-feed feed-url))
     (setq feed-buffer (org-feed-get-feed feed-url))
@@ -238,10 +279,13 @@ it can be a list structured like an entry in `org-feed-alist'."
 	  ;; Format the new entries
 	  ;; Format the new entries
 	  (run-hooks 'org-feed-before-adding-hook)
 	  (run-hooks 'org-feed-before-adding-hook)
 	  (setq new-selected new)
 	  (setq new-selected new)
-	  (when feed-formatter
-	    (setq new-selected (mapcar feed-formatter new-selected)))
-	  (setq new-selected (mapcar 'org-feed-format new-selected))
-	  (setq new-selected (delq nil new-selected))
+	  (when feed-filter
+	    (setq new-selected (mapcar feed-filter new-selected)))
+	  (setq new-selected
+		(delq nil
+		      (mapcar
+		       (lambda (e) (org-feed-format-entry e feed-template))
+		       new-selected)))
 	  ;; Insert the new items
 	  ;; Insert the new items
 	  (apply 'org-feed-add-items feed-pos new-selected)
 	  (apply 'org-feed-add-items feed-pos new-selected)
 	  ;; Update the list of seen GUIDs in a drawer
 	  ;; Update the list of seen GUIDs in a drawer
@@ -337,38 +381,57 @@ When REPLACE is non-nil, replace all GUIDs by the new ones."
 	(org-paste-subtree level (plist-get entry :formatted-for-org) 'yank))
 	(org-paste-subtree level (plist-get entry :formatted-for-org) 'yank))
       (org-mark-ring-push pos))))
       (org-mark-ring-push pos))))
 
 
-(defun org-feed-format (entry)
+(defun org-feed-format-entry (entry template)
   "Format ENTRY so that it can be inserted into an Org file.
   "Format ENTRY so that it can be inserted into an Org file.
 ENTRY is a property list.  This function adds a `:formatted-for-org' property
 ENTRY is a property list.  This function adds a `:formatted-for-org' property
 and returns the full property list.
 and returns the full property list.
 If that property is already present, nothing changes."
 If that property is already present, nothing changes."
   (unless (or (not entry)                            ; not an entry at all
   (unless (or (not entry)                            ; not an entry at all
 	      (plist-get entry :formatted-for-org))  ; already formatted
 	      (plist-get entry :formatted-for-org))  ; already formatted
-    (let (lines fmt tmp indent)
-      (setq lines (org-split-string (or (plist-get entry :description) "???")
+    (let (dlines fmt tmp indent)
+      (setq dlines (org-split-string (or (plist-get entry :description) "???")
 				    "\n")
 				    "\n")
-	    indent "  ")
-      (setq fmt
-	    (concat
-	     "* " (or (plist-get entry :title) (car lines)) "\n"
-	     (if (setq tmp (plist-get entry :pubDate))
-		 (concat
-		  "  ["
-		  (substring
-		   (format-time-string (cdr org-time-stamp-formats)
-				       (org-read-date t t tmp))
-		   1 -1)
-		  "]\n"))
-	     (concat "  :PROPERTIES:\n  :FEED-GUID: "
-		     (plist-get entry :guid)
-		     "\n  :END:\n")
-	     (mapconcat (lambda (x) (concat indent x)) lines "\n") "\n"
-	     (if (setq tmp (or (and (plist-get entry :guid-permalink)
-				    (plist-get entry :guid))
-			       (plist-get entry :link)))
-		 (concat "  [[" tmp "]]\n")))
-	    entry (plist-put entry :formatted-for-org fmt))))
-      entry)
+	    v-h (or (plist-get entry :title) (car dlines) "???")
+	    time (or (if (plist-get entry :pubDate)
+			 (org-read-date t t (plist-get entry :pubDate)))
+		     (current-time))
+	    v-t (format-time-string (org-time-stamp-format nil nil) time)
+	    v-T (format-time-string (org-time-stamp-format t   nil) time)
+	    v-u (format-time-string (org-time-stamp-format nil t)   time)
+	    v-U (format-time-string (org-time-stamp-format t   t)   time)
+	    v-a (if (setq tmp (or (and (plist-get entry :guid-permalink)
+				       (plist-get entry :guid))
+				  (plist-get entry :link)))
+		    (concat "[[" tmp "]]\n")
+		  ""))
+      (with-temp-buffer
+	(insert template)
+	(debug)
+	(goto-char (point-min))
+	(while (re-search-forward "%\\([a-zA-Z]+\\)" nil t)
+	  (setq name (match-string 1))
+	  (cond
+	   ((member name '("h" "t" "T" "u" "U" "a"))
+	    (replace-match (symbol-value (intern (concat "v-" name))) t t))
+	   ((setq tmp (plist-get entry (intern (concat ":" name))))
+	    (save-excursion
+	      (save-match-data
+		(beginning-of-line 1)
+		(when (looking-at (concat "^\\([ \t]*\\)%" name "[ \t]*$"))
+		  (setq tmp (org-feed-make-indented-block
+			     tmp (org-get-indentation))))))
+	    (replace-match tmp t t))
+	   t))
+	(setq entry (plist-put entry :formatted-for-org (buffer-string))))))
+  entry)
+
+(defun org-feed-make-indented-block (s n)
+  "Add indentaton of N spaces to a multiline string S."
+  (if (not (string-match "\n" s))
+      s
+    (mapconcat 'identity
+	       (org-split-string s "\n")
+	       (concat "\n" (make-string n ?\ )))))
 
 
 (defun org-feed-get-feed (url)
 (defun org-feed-get-feed (url)
   "Get the RSS feed file at URL and return the buffer."
   "Get the RSS feed file at URL and return the buffer."