Commit | Line | Data |
---|---|---|
c8d0cf5c CD |
1 | ;;; org-feed.el --- Add RSS feed items to Org files |
2 | ;; | |
49f70d46 | 3 | ;; Copyright (C) 2009, 2010, 2011, 2012 Free Software Foundation, Inc. |
c8d0cf5c CD |
4 | ;; |
5 | ;; Author: Carsten Dominik <carsten at orgmode dot org> | |
6 | ;; Keywords: outlines, hypermedia, calendar, wp | |
7 | ;; Homepage: http://orgmode.org | |
5dec9555 | 8 | ;; Version: 6.33x |
c8d0cf5c CD |
9 | ;; |
10 | ;; This file is part of GNU Emacs. | |
11 | ;; | |
12 | ;; GNU Emacs is free software: you can redistribute it and/or modify | |
13 | ;; it under the terms of the GNU General Public License as published by | |
14 | ;; the Free Software Foundation, either version 3 of the License, or | |
15 | ;; (at your option) any later version. | |
16 | ||
17 | ;; GNU Emacs is distributed in the hope that it will be useful, | |
18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | |
19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
20 | ;; GNU General Public License for more details. | |
21 | ||
22 | ;; You should have received a copy of the GNU General Public License | |
23 | ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. | |
24 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
25 | ;; | |
26 | ;;; Commentary: | |
27 | ;; | |
28 | ;; This module allows to create and change entries in an Org-mode | |
29 | ;; file triggered by items in an RSS feed. The basic functionality is | |
30 | ;; geared toward simply adding new items found in a feed as outline nodes | |
31 | ;; to an Org file. Using hooks, arbitrary actions can be triggered for | |
32 | ;; new or changed items. | |
33 | ;; | |
34 | ;; Selecting feeds and target locations | |
35 | ;; ------------------------------------ | |
36 | ;; | |
37 | ;; This module is configured through a single variable, `org-feed-alist'. | |
38 | ;; Here is an example, using a notes/tasks feed from reQall.com. | |
39 | ;; | |
40 | ;; (setq org-feed-alist | |
41 | ;; '(("ReQall" | |
42 | ;; "http://www.reqall.com/user/feeds/rss/a1b2c3....." | |
43 | ;; "~/org/feeds.org" "ReQall Entries") | |
44 | ;; | |
45 | ;; With this setup, the command `M-x org-feed-update-all' will | |
46 | ;; collect new entries in the feed at the given URL and create | |
47 | ;; entries as subheadings under the "ReQall Entries" heading in the | |
48 | ;; file "~/org-feeds.org". Each feed should normally have its own | |
49 | ;; heading - however see the `:drawer' parameter. | |
50 | ;; | |
51 | ;; Besides these standard elements that need to be specified for each | |
52 | ;; feed, keyword-value pairs can set additional options. For example, | |
53 | ;; to de-select transitional entries with a title containing | |
54 | ;; | |
55 | ;; "reQall is typing what you said", | |
56 | ;; | |
57 | ;; you could use the `:filter' argument: | |
58 | ;; | |
59 | ;; (setq org-feed-alist | |
60 | ;; '(("ReQall" | |
61 | ;; "http://www.reqall.com/user/feeds/rss/a1b2c3....." | |
62 | ;; "~/org/feeds.org" "ReQall Entries" | |
63 | ;; :filter my-reqall-filter))) | |
64 | ;; | |
65 | ;; (defun my-reqall-filter (e) | |
66 | ;; (if (string-match "reQall is typing what you said" | |
67 | ;; (plist-get e :title)) | |
68 | ;; nil | |
69 | ;; e)) | |
70 | ;; | |
71 | ;; See the docstring for `org-feed-alist' for more details. | |
72 | ;; | |
73 | ;; | |
74 | ;; Keeping track of previously added entries | |
75 | ;; ----------------------------------------- | |
76 | ;; | |
77 | ;; Since Org allows you to delete, archive, or move outline nodes, | |
78 | ;; org-feed.el needs to keep track of which feed items have been handled | |
79 | ;; before, so that they will not be handled again. For this, org-feed.el | |
80 | ;; stores information in a special drawer, FEEDSTATUS, under the heading | |
81 | ;; that received the input of the feed. You should add FEEDSTATUS | |
82 | ;; to your list of drawers in the files that receive feed input: | |
83 | ;; | |
84 | ;; #+DRAWERS: PROPERTIES LOGBOOK FEEDSTATUS | |
85 | ;; | |
8bfe682a | 86 | ;; Acknowledgements |
c8d0cf5c CD |
87 | ;; ---------------- |
88 | ;; | |
89 | ;; org-feed.el is based on ideas by Brad Bozarth who implemented a | |
90 | ;; similar mechanism using shell and awk scripts. | |
91 | ||
92 | ;;; Code: | |
93 | ||
94 | (require 'org) | |
95 | (require 'sha1) | |
96 | ||
97 | (declare-function url-retrieve-synchronously "url" (url)) | |
98 | (declare-function xml-node-children "xml" (node)) | |
99 | (declare-function xml-get-children "xml" (node child-name)) | |
100 | (declare-function xml-get-attribute "xml" (node attribute)) | |
101 | (declare-function xml-get-attribute-or-nil "xml" (node attribute)) | |
102 | ||
103 | (defgroup org-feed nil | |
104 | "Options concerning RSS feeds as inputs for Org files." | |
105 | :tag "Org ID" | |
106 | :group 'org) | |
107 | ||
108 | (defcustom org-feed-alist nil | |
109 | "Alist specifying RSS feeds that should create inputs for Org. | |
110 | Each entry in this list specified an RSS feed tat should be queried | |
111 | to create inbox items in Org. Each entry is a list with the following items: | |
112 | ||
113 | name a custom name for this feed | |
114 | URL the Feed URL | |
115 | file the target Org file where entries should be listed | |
116 | headline the headline under which entries should be listed | |
117 | ||
118 | Additional arguments can be given using keyword-value pairs. Many of these | |
119 | specify functions that receive one or a list of \"entries\" as their single | |
120 | argument. An entry is a property list that describes a feed item. The | |
121 | property list has properties for each field in the item, for example `:title' | |
122 | for the `<title>' field and `:pubDate' for the publication date. In addition, | |
123 | it contains the following properties: | |
124 | ||
125 | `:item-full-text' the full text in the <item> tag | |
126 | `:guid-permalink' t when the guid property is a permalink | |
127 | ||
128 | Here are the keyword-value pair allows in `org-feed-alist'. | |
129 | ||
130 | :drawer drawer-name | |
131 | The name of the drawer for storing feed information. The default is | |
132 | \"FEEDSTATUS\". Using different drawers for different feeds allows | |
133 | several feeds to target the same inbox heading. | |
134 | ||
135 | :filter filter-function | |
136 | A function to select interesting entries in the feed. It gets a single | |
137 | entry as parameter. It should return the entry if it is relevant, or | |
138 | nil if it is not. | |
139 | ||
140 | :template template-string | |
141 | The default action on new items in the feed is to add them as children | |
142 | under the headline for the feed. The template describes how the entry | |
143 | should be formatted. If not given, it defaults to | |
144 | `org-feed-default-template'. | |
145 | ||
146 | :formatter formatter-function | |
147 | Instead of relying on a template, you may specify a function to format | |
148 | the outline node to be inserted as a child. This function gets passed | |
149 | a property list describing a single feed item, and it should return a | |
150 | string that is a properly formatted Org outline node of level 1. | |
151 | ||
152 | :new-handler function | |
153 | If adding new items as children to the outline is not what you want | |
154 | to do with new items, define a handler function that is called with | |
155 | a list of all new items in the feed, each one represented as a property | |
156 | list. The handler should do what needs to be done, and org-feed will | |
157 | mark all items given to this handler as \"handled\", i.e. they will not | |
158 | be passed to this handler again in future readings of the feed. | |
159 | When the handler is called, point will be at the feed headline. | |
160 | ||
161 | :changed-handler function | |
162 | This function gets passed a list of all entries that have been | |
163 | handled before, but are now still in the feed and have *changed* | |
164 | since last handled (as evidenced by a different sha1 hash). | |
165 | When the handler is called, point will be at the feed headline. | |
166 | ||
167 | :parse-feed function | |
168 | This function gets passed a buffer, and should return a list of entries, | |
169 | each being a property list containing the `:guid' and `:item-full-text' | |
170 | keys. The default is `org-feed-parse-rss-feed'; `org-feed-parse-atom-feed' | |
171 | is an alternative. | |
172 | ||
173 | :parse-entry function | |
174 | This function gets passed an entry as returned by the parse-feed | |
175 | function, and should return the entry with interesting properties added. | |
176 | The default is `org-feed-parse-rss-entry'; `org-feed-parse-atom-entry' | |
177 | is an alternative." | |
178 | :group 'org-feed | |
179 | :type '(repeat | |
180 | (list :value ("" "http://" "" "") | |
181 | (string :tag "Name") | |
182 | (string :tag "Feed URL") | |
183 | (file :tag "File for inbox") | |
184 | (string :tag "Headline for inbox") | |
185 | (repeat :inline t | |
186 | (choice | |
187 | (list :inline t :tag "Filter" | |
188 | (const :filter) | |
189 | (symbol :tag "Filter Function")) | |
190 | (list :inline t :tag "Template" | |
191 | (const :template) | |
192 | (string :tag "Template")) | |
193 | (list :inline t :tag "Formatter" | |
194 | (const :formatter) | |
195 | (symbol :tag "Formatter Function")) | |
196 | (list :inline t :tag "New items handler" | |
197 | (const :new-handler) | |
198 | (symbol :tag "Handler Function")) | |
199 | (list :inline t :tag "Changed items" | |
200 | (const :changed-handler) | |
201 | (symbol :tag "Handler Function")) | |
202 | (list :inline t :tag "Parse Feed" | |
203 | (const :parse-feed) | |
204 | (symbol :tag "Parse Feed Function")) | |
205 | (list :inline t :tag "Parse Entry" | |
206 | (const :parse-entry) | |
207 | (symbol :tag "Parse Entry Function")) | |
208 | ))))) | |
209 | ||
210 | (defcustom org-feed-drawer "FEEDSTATUS" | |
211 | "The name of the drawer for feed status information. | |
212 | Each feed may also specify its own drawer name using the `:drawer' | |
213 | parameter in `org-feed-alist'. | |
214 | Note that in order to make these drawers behave like drawers, they must | |
215 | be added to the variable `org-drawers' or configured with a #+DRAWERS | |
216 | line." | |
217 | :group 'org-feed | |
218 | :type '(string :tag "Drawer Name")) | |
219 | ||
220 | (defcustom org-feed-default-template "\n* %h\n %U\n %description\n %a\n" | |
221 | "Template for the Org node created from RSS feed items. | |
222 | This is just the default, each feed can specify its own. | |
223 | Any fields from the feed item can be interpolated into the template with | |
224 | %name, for example %title, %description, %pubDate etc. In addition, the | |
225 | following special escapes are valid as well: | |
226 | ||
227 | %h the title, or the first line of the description | |
228 | %t the date as a stamp, either from <pubDate> (if present), or | |
229 | the current date. | |
230 | %T date and time | |
231 | %u,%U like %t,%T, but inactive time stamps | |
232 | %a A link, from <guid> if that is a permalink, else from <link>" | |
233 | :group 'org-feed | |
234 | :type '(string :tag "Template")) | |
235 | ||
236 | (defcustom org-feed-save-after-adding t | |
237 | "Non-nil means, save buffer after adding new feed items." | |
238 | :group 'org-feed | |
239 | :type 'boolean) | |
240 | ||
241 | (defcustom org-feed-retrieve-method 'url-retrieve-synchronously | |
242 | "The method to be used to retrieve a feed URL. | |
243 | This can be `curl' or `wget' to call these external programs, or it can be | |
244 | an Emacs Lisp function that will return a buffer containing the content | |
245 | of the file pointed to by the URL." | |
246 | :group 'org-feed | |
247 | :type '(choice | |
248 | (const :tag "Internally with url.el" url-retrieve-synchronously) | |
249 | (const :tag "Externally with curl" curl) | |
250 | (const :tag "Externally with wget" wget) | |
251 | (function :tag "Function"))) | |
252 | ||
253 | (defcustom org-feed-before-adding-hook nil | |
254 | "Hook that is run before adding new feed items to a file. | |
255 | You might want to commit the file in its current state to version control, | |
256 | for example." | |
257 | :group 'org-feed | |
258 | :type 'hook) | |
259 | ||
260 | (defcustom org-feed-after-adding-hook nil | |
261 | "Hook that is run after new items have been added to a file. | |
262 | Depending on `org-feed-save-after-adding', the buffer will already | |
263 | have been saved." | |
264 | :group 'org-feed | |
265 | :type 'hook) | |
266 | ||
267 | (defvar org-feed-buffer "*Org feed*" | |
268 | "The buffer used to retrieve a feed.") | |
269 | ||
270 | ;;;###autoload | |
271 | (defun org-feed-update-all () | |
272 | "Get inbox items from all feeds in `org-feed-alist'." | |
273 | (interactive) | |
274 | (let ((nfeeds (length org-feed-alist)) | |
275 | (nnew (apply '+ (mapcar 'org-feed-update org-feed-alist)))) | |
276 | (message "%s from %d %s" | |
277 | (cond ((= nnew 0) "No new entries") | |
278 | ((= nnew 1) "1 new entry") | |
279 | (t (format "%d new entries" nnew))) | |
280 | nfeeds | |
281 | (if (= nfeeds 1) "feed" "feeds")))) | |
282 | ||
283 | ;;;###autoload | |
284 | (defun org-feed-update (feed &optional retrieve-only) | |
285 | "Get inbox items from FEED. | |
286 | FEED can be a string with an association in `org-feed-alist', or | |
287 | it can be a list structured like an entry in `org-feed-alist'." | |
288 | (interactive (list (org-completing-read "Feed name: " org-feed-alist))) | |
289 | (if (stringp feed) (setq feed (assoc feed org-feed-alist))) | |
290 | (unless feed | |
291 | (error "No such feed in `org-feed-alist")) | |
292 | (catch 'exit | |
293 | (let ((name (car feed)) | |
294 | (url (nth 1 feed)) | |
295 | (file (nth 2 feed)) | |
296 | (headline (nth 3 feed)) | |
297 | (filter (nth 1 (memq :filter feed))) | |
298 | (formatter (nth 1 (memq :formatter feed))) | |
299 | (new-handler (nth 1 (memq :new-handler feed))) | |
300 | (changed-handler (nth 1 (memq :changed-handler feed))) | |
301 | (template (or (nth 1 (memq :template feed)) | |
302 | org-feed-default-template)) | |
303 | (drawer (or (nth 1 (memq :drawer feed)) | |
304 | org-feed-drawer)) | |
305 | (parse-feed (or (nth 1 (memq :parse-feed feed)) | |
306 | 'org-feed-parse-rss-feed)) | |
307 | (parse-entry (or (nth 1 (memq :parse-entry feed)) | |
308 | 'org-feed-parse-rss-entry)) | |
309 | feed-buffer inbox-pos new-formatted | |
310 | entries old-status status new changed guid-alist e guid olds) | |
311 | (setq feed-buffer (org-feed-get-feed url)) | |
312 | (unless (and feed-buffer (bufferp (get-buffer feed-buffer))) | |
313 | (error "Cannot get feed %s" name)) | |
314 | (when retrieve-only | |
315 | (throw 'exit feed-buffer)) | |
316 | (setq entries (funcall parse-feed feed-buffer)) | |
317 | (ignore-errors (kill-buffer feed-buffer)) | |
318 | (save-excursion | |
319 | (save-window-excursion | |
320 | (setq inbox-pos (org-feed-goto-inbox-internal file headline)) | |
321 | (setq old-status (org-feed-read-previous-status inbox-pos drawer)) | |
322 | ;; Add the "handled" status to the appropriate entries | |
323 | (setq entries (mapcar (lambda (e) | |
324 | (setq e (plist-put e :handled | |
325 | (nth 1 (assoc | |
326 | (plist-get e :guid) | |
327 | old-status))))) | |
328 | entries)) | |
329 | ;; Find out which entries are new and which are changed | |
330 | (dolist (e entries) | |
331 | (if (not (plist-get e :handled)) | |
332 | (push e new) | |
333 | (setq olds (nth 2 (assoc (plist-get e :guid) old-status))) | |
334 | (if (and olds | |
335 | (not (string= (sha1 | |
336 | (plist-get e :item-full-text)) | |
337 | olds))) | |
338 | (push e changed)))) | |
339 | ||
340 | ;; Parse the relevant entries fully | |
341 | (setq new (mapcar parse-entry new) | |
342 | changed (mapcar parse-entry changed)) | |
343 | ||
344 | ;; Run the filter | |
345 | (when filter | |
346 | (setq new (delq nil (mapcar filter new)) | |
347 | changed (delq nil (mapcar filter new)))) | |
348 | ||
349 | (when (not (or new changed)) | |
350 | (message "No new items in feed %s" name) | |
351 | (throw 'exit 0)) | |
352 | ||
353 | ;; Get alist based on guid, to look up entries | |
354 | (setq guid-alist | |
355 | (append | |
356 | (mapcar (lambda (e) (list (plist-get e :guid) e)) new) | |
357 | (mapcar (lambda (e) (list (plist-get e :guid) e)) changed))) | |
358 | ||
359 | ;; Construct the new status | |
360 | (setq status | |
361 | (mapcar | |
362 | (lambda (e) | |
363 | (setq guid (plist-get e :guid)) | |
364 | (list guid | |
365 | ;; things count as handled if we handle them now, | |
366 | ;; or if they were handled previously | |
367 | (if (assoc guid guid-alist) t (plist-get e :handled)) | |
368 | ;; A hash, to detect changes | |
369 | (sha1 (plist-get e :item-full-text)))) | |
370 | entries)) | |
371 | ||
372 | ;; Handle new items in the feed | |
373 | (when new | |
374 | (if new-handler | |
375 | (progn | |
376 | (goto-char inbox-pos) | |
377 | (funcall new-handler new)) | |
378 | ;; No custom handler, do the default adding | |
379 | ;; Format the new entries into an alist with GUIDs in the car | |
380 | (setq new-formatted | |
381 | (mapcar | |
382 | (lambda (e) (org-feed-format-entry e template formatter)) | |
383 | new))) | |
384 | ||
385 | ;; Insert the new items | |
386 | (org-feed-add-items inbox-pos new-formatted)) | |
387 | ||
388 | ;; Handle changed items in the feed | |
389 | (when (and changed-handler changed) | |
390 | (goto-char inbox-pos) | |
391 | (funcall changed-handler changed)) | |
392 | ||
393 | ;; Write the new status | |
394 | ;; We do this only now, in case something goes wrong above, so | |
395 | ;; that would would end up with a status that does not reflect | |
396 | ;; which items truely have been handled | |
397 | (org-feed-write-status inbox-pos drawer status) | |
398 | ||
399 | ;; Normalize the visibility of the inbox tree | |
400 | (goto-char inbox-pos) | |
401 | (hide-subtree) | |
402 | (show-children) | |
403 | (org-cycle-hide-drawers 'children) | |
404 | ||
405 | ;; Hooks and messages | |
406 | (when org-feed-save-after-adding (save-buffer)) | |
407 | (message "Added %d new item%s from feed %s to file %s, heading %s" | |
408 | (length new) (if (> (length new) 1) "s" "") | |
409 | name | |
410 | (file-name-nondirectory file) headline) | |
411 | (run-hooks 'org-feed-after-adding-hook) | |
412 | (length new)))))) | |
413 | ||
414 | ;;;###autoload | |
415 | (defun org-feed-goto-inbox (feed) | |
416 | "Go to the inbox that captures the feed named FEED." | |
417 | (interactive | |
418 | (list (if (= (length org-feed-alist) 1) | |
419 | (car org-feed-alist) | |
420 | (org-completing-read "Feed name: " org-feed-alist)))) | |
421 | (if (stringp feed) (setq feed (assoc feed org-feed-alist))) | |
422 | (unless feed | |
423 | (error "No such feed in `org-feed-alist")) | |
424 | (org-feed-goto-inbox-internal (nth 2 feed) (nth 3 feed))) | |
425 | ||
426 | ;;;###autoload | |
427 | (defun org-feed-show-raw-feed (feed) | |
428 | "Show the raw feed buffer of a feed." | |
429 | (interactive | |
430 | (list (if (= (length org-feed-alist) 1) | |
431 | (car org-feed-alist) | |
432 | (org-completing-read "Feed name: " org-feed-alist)))) | |
433 | (if (stringp feed) (setq feed (assoc feed org-feed-alist))) | |
434 | (unless feed | |
435 | (error "No such feed in `org-feed-alist")) | |
436 | (switch-to-buffer | |
437 | (org-feed-update feed 'retrieve-only)) | |
438 | (goto-char (point-min))) | |
439 | ||
440 | (defun org-feed-goto-inbox-internal (file heading) | |
441 | "Find or create HEADING in FILE. | |
442 | Switch to that buffer, and return the position of that headline." | |
443 | (find-file file) | |
444 | (widen) | |
445 | (goto-char (point-min)) | |
446 | (if (re-search-forward | |
447 | (concat "^\\*+[ \t]+" heading "[ \t]*\\(:.*?:[ \t]*\\)?$") | |
448 | nil t) | |
449 | (goto-char (match-beginning 0)) | |
450 | (goto-char (point-max)) | |
451 | (insert "\n\n* " heading "\n\n") | |
452 | (org-back-to-heading t)) | |
453 | (point)) | |
454 | ||
455 | (defun org-feed-read-previous-status (pos drawer) | |
456 | "Get the alist of old GUIDs from the entry at POS. | |
457 | This will find DRAWER and extract the alist." | |
458 | (save-excursion | |
459 | (goto-char pos) | |
460 | (let ((end (save-excursion (org-end-of-subtree t t)))) | |
461 | (if (re-search-forward | |
462 | (concat "^[ \t]*:" drawer ":[ \t]*\n\\([^\000]*?\\)\n[ \t]*:END:") | |
463 | end t) | |
464 | (read (match-string 1)) | |
465 | nil)))) | |
466 | ||
467 | (defun org-feed-write-status (pos drawer status) | |
468 | "Write the feed STATUS to DRAWER in entry at POS." | |
469 | (save-excursion | |
470 | (goto-char pos) | |
471 | (let ((end (save-excursion (org-end-of-subtree t t))) | |
472 | guid) | |
473 | (if (re-search-forward (concat "^[ \t]*:" drawer ":[ \t]*\n") | |
474 | end t) | |
475 | (progn | |
476 | (goto-char (match-end 0)) | |
477 | (delete-region (point) | |
478 | (save-excursion | |
479 | (and (re-search-forward "^[ \t]*:END:" nil t) | |
480 | (match-beginning 0))))) | |
481 | (outline-next-heading) | |
482 | (insert " :" drawer ":\n :END:\n") | |
483 | (beginning-of-line 0)) | |
484 | (insert (pp-to-string status))))) | |
485 | ||
486 | (defun org-feed-add-items (pos entries) | |
487 | "Add the formatted items to the headline as POS." | |
488 | (let (entry level) | |
489 | (save-excursion | |
490 | (goto-char pos) | |
491 | (unless (looking-at org-complex-heading-regexp) | |
492 | (error "Wrong position")) | |
493 | (setq level (org-get-valid-level (length (match-string 1)) 1)) | |
494 | (org-end-of-subtree t t) | |
495 | (skip-chars-backward " \t\n") | |
496 | (beginning-of-line 2) | |
497 | (setq pos (point)) | |
498 | (while (setq entry (pop entries)) | |
499 | (org-paste-subtree level entry 'yank)) | |
500 | (org-mark-ring-push pos)))) | |
501 | ||
502 | (defun org-feed-format-entry (entry template formatter) | |
503 | "Format ENTRY so that it can be inserted into an Org file. | |
504 | ENTRY is a property list. This function adds a `:formatted-for-org' property | |
505 | and returns the full property list. | |
506 | If that property is already present, nothing changes." | |
507 | (if formatter | |
508 | (funcall formatter entry) | |
509 | (let (dlines fmt tmp indent time name | |
510 | v-h v-t v-T v-u v-U v-a) | |
511 | (setq dlines (org-split-string (or (plist-get entry :description) "???") | |
512 | "\n") | |
513 | v-h (or (plist-get entry :title) (car dlines) "???") | |
514 | time (or (if (plist-get entry :pubDate) | |
515 | (org-read-date t t (plist-get entry :pubDate))) | |
516 | (current-time)) | |
517 | v-t (format-time-string (org-time-stamp-format nil nil) time) | |
518 | v-T (format-time-string (org-time-stamp-format t nil) time) | |
519 | v-u (format-time-string (org-time-stamp-format nil t) time) | |
520 | v-U (format-time-string (org-time-stamp-format t t) time) | |
521 | v-a (if (setq tmp (or (and (plist-get entry :guid-permalink) | |
522 | (plist-get entry :guid)) | |
523 | (plist-get entry :link))) | |
524 | (concat "[[" tmp "]]\n") | |
525 | "")) | |
526 | (with-temp-buffer | |
527 | (insert template) | |
528 | (goto-char (point-min)) | |
529 | (while (re-search-forward "%\\([a-zA-Z]+\\)" nil t) | |
530 | (setq name (match-string 1)) | |
531 | (cond | |
532 | ((member name '("h" "t" "T" "u" "U" "a")) | |
533 | (replace-match (symbol-value (intern (concat "v-" name))) t t)) | |
534 | ((setq tmp (plist-get entry (intern (concat ":" name)))) | |
535 | (save-excursion | |
536 | (save-match-data | |
537 | (beginning-of-line 1) | |
538 | (when (looking-at (concat "^\\([ \t]*\\)%" name "[ \t]*$")) | |
539 | (setq tmp (org-feed-make-indented-block | |
540 | tmp (org-get-indentation)))))) | |
541 | (replace-match tmp t t)))) | |
542 | (buffer-string))))) | |
543 | ||
544 | (defun org-feed-make-indented-block (s n) | |
8bfe682a | 545 | "Add indentation of N spaces to a multiline string S." |
c8d0cf5c CD |
546 | (if (not (string-match "\n" s)) |
547 | s | |
548 | (mapconcat 'identity | |
549 | (org-split-string s "\n") | |
550 | (concat "\n" (make-string n ?\ ))))) | |
551 | ||
552 | (defun org-feed-skip-http-headers (buffer) | |
553 | "Remove HTTP headers from BUFFER, and return it. | |
554 | Assumes headers are indeed present!" | |
555 | (with-current-buffer buffer | |
556 | (widen) | |
557 | (goto-char (point-min)) | |
558 | (search-forward "\n\n") | |
559 | (delete-region (point-min) (point)) | |
560 | buffer)) | |
561 | ||
562 | (defun org-feed-get-feed (url) | |
563 | "Get the RSS feed file at URL and return the buffer." | |
564 | (cond | |
565 | ((eq org-feed-retrieve-method 'url-retrieve-synchronously) | |
566 | (org-feed-skip-http-headers (url-retrieve-synchronously url))) | |
567 | ((eq org-feed-retrieve-method 'curl) | |
568 | (ignore-errors (kill-buffer org-feed-buffer)) | |
569 | (call-process "curl" nil org-feed-buffer nil "--silent" url) | |
570 | org-feed-buffer) | |
571 | ((eq org-feed-retrieve-method 'wget) | |
572 | (ignore-errors (kill-buffer org-feed-buffer)) | |
573 | (call-process "wget" nil org-feed-buffer nil "-q" "-O" "-" url) | |
574 | org-feed-buffer) | |
575 | ((functionp org-feed-retrieve-method) | |
576 | (funcall org-feed-retrieve-method url)))) | |
577 | ||
578 | (defun org-feed-parse-rss-feed (buffer) | |
579 | "Parse BUFFER for RSS feed entries. | |
580 | Returns a list of entries, with each entry a property list, | |
581 | containing the properties `:guid' and `:item-full-text'." | |
582 | (let (entries beg end item guid entry) | |
583 | (with-current-buffer buffer | |
584 | (widen) | |
585 | (goto-char (point-min)) | |
586 | (while (re-search-forward "<item>" nil t) | |
587 | (setq beg (point) | |
588 | end (and (re-search-forward "</item>" nil t) | |
589 | (match-beginning 0))) | |
590 | (setq item (buffer-substring beg end) | |
591 | guid (if (string-match "<guid\\>.*?>\\(.*?\\)</guid>" item) | |
592 | (org-match-string-no-properties 1 item))) | |
593 | (setq entry (list :guid guid :item-full-text item)) | |
594 | (push entry entries) | |
595 | (widen) | |
596 | (goto-char end)) | |
597 | (nreverse entries)))) | |
598 | ||
599 | (defun org-feed-parse-rss-entry (entry) | |
600 | "Parse the `:item-full-text' field for xml tags and create new properties." | |
601 | (with-temp-buffer | |
602 | (insert (plist-get entry :item-full-text)) | |
603 | (goto-char (point-min)) | |
604 | (while (re-search-forward "<\\([a-zA-Z]+\\>\\).*?>\\([^\000]*?\\)</\\1>" | |
605 | nil t) | |
606 | (setq entry (plist-put entry | |
607 | (intern (concat ":" (match-string 1))) | |
608 | (match-string 2)))) | |
609 | (goto-char (point-min)) | |
610 | (unless (re-search-forward "isPermaLink[ \t]*=[ \t]*\"false\"" nil t) | |
611 | (setq entry (plist-put entry :guid-permalink t)))) | |
612 | entry) | |
613 | ||
614 | (defun org-feed-parse-atom-feed (buffer) | |
615 | "Parse BUFFER for Atom feed entries. | |
8bfe682a | 616 | Returns a list of entries, with each entry a property list, |
c8d0cf5c CD |
617 | containing the properties `:guid' and `:item-full-text'. |
618 | ||
619 | The `:item-full-text' property actually contains the sexp | |
620 | formatted as a string, not the original XML data." | |
621 | (with-current-buffer buffer | |
622 | (widen) | |
623 | (let ((feed (car (xml-parse-region (point-min) (point-max))))) | |
624 | (mapcar | |
625 | (lambda (entry) | |
626 | (list | |
627 | :guid (car (xml-node-children (car (xml-get-children entry 'id)))) | |
628 | :item-full-text (prin1-to-string entry))) | |
629 | (xml-get-children feed 'entry))))) | |
630 | ||
631 | (defun org-feed-parse-atom-entry (entry) | |
632 | "Parse the `:item-full-text' as a sexp and create new properties." | |
633 | (let ((xml (car (read-from-string (plist-get entry :item-full-text))))) | |
634 | ;; Get first <link href='foo'/>. | |
635 | (setq entry (plist-put entry :link | |
636 | (xml-get-attribute | |
637 | (car (xml-get-children xml 'link)) | |
638 | 'href))) | |
639 | ;; Add <title/> as :title. | |
640 | (setq entry (plist-put entry :title | |
641 | (car (xml-node-children | |
642 | (car (xml-get-children xml 'title)))))) | |
643 | (let* ((content (car (xml-get-children xml 'content))) | |
644 | (type (xml-get-attribute-or-nil content 'type))) | |
645 | (when content | |
646 | (cond | |
647 | ((string= type "text") | |
648 | ;; We like plain text. | |
649 | (setq entry (plist-put entry :description (car (xml-node-children content))))) | |
650 | ((string= type "html") | |
651 | ;; TODO: convert HTML to Org markup. | |
652 | (setq entry (plist-put entry :description (car (xml-node-children content))))) | |
653 | ((string= type "xhtml") | |
654 | ;; TODO: convert XHTML to Org markup. | |
655 | (setq entry (plist-put entry :description (prin1-to-string (xml-node-children content))))) | |
656 | (t | |
657 | (setq entry (plist-put entry :description (format "Unknown '%s' content." type))))))) | |
658 | entry)) | |
659 | ||
660 | (provide 'org-feed) | |
661 | ||
662 | ;; arch-tag: 0929b557-9bc4-47f4-9633-30a12dbb5ae2 | |
c8d0cf5c | 663 | ;;; org-feed.el ends here |