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