Mention Emacs Muse in README.
[planner-el.git] / planner-rss.el
blob989fbfc2e5621e87f04e01b5072e9e5ac1f90d53
1 ;;; planner-rss.el --- RSS export for the Emacs Planner (planner.el)
3 ;; Copyright (C) 2003, 2004, 2005 Free Software Foundation, Inc.
4 ;; Parts copyright (C) 2005 David D. Smith
6 ;; Emacs Lisp Archive Entry
7 ;; Filename: planner-rss.el
8 ;; Keywords: hypermedia
9 ;; Author: Sacha Chua <sacha@free.net.ph>
10 ;; Description: Export planner entries as an RSS feed
11 ;; URL: http://www.plannerlove.com/
12 ;; Compatibility: Emacs20, Emacs21
14 ;; This file is part of Planner. It is not part of GNU Emacs.
16 ;; Planner is free software; you can redistribute it and/or modify it
17 ;; under the terms of the GNU General Public License as published by
18 ;; the Free Software Foundation; either version 2, or (at your option)
19 ;; any later version.
21 ;; Planner is distributed in the hope that it will be useful, but
22 ;; WITHOUT ANY WARRANTY; without even the implied warranty of
23 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24 ;; General Public License for more details.
26 ;; You should have received a copy of the GNU General Public License
27 ;; along with Planner; see the file COPYING. If not, write to the
28 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
29 ;; Boston, MA 02110-1301, USA.
31 ;;; Commentary:
33 ;; If you use `remember-to-planner' to keep daily notes, you can
34 ;; automatically publish remembered notes as an RSS feed by adding the
35 ;; following code to your .emacs:
37 ;; (add-to-list 'remember-planner-append-hook 'planner-rss-add-note t)
39 ;; You can also invoke `planner-rss-add-note' on any note you would
40 ;; like added.
42 ;;;_ + Contributors
44 ;; David Smith (davidsmith AT acm DOT org) ported this to work with
45 ;; Muse.
47 ;;; Code:
49 (require 'planner)
50 (require 'planner-publish)
52 (defgroup planner-rss nil
53 "Planner options for RSS feeds."
54 :prefix "planner-rss-"
55 :group 'planner)
57 (defcustom planner-rss-base-url
59 "Base URL for blog entries. Should include trailing /.
60 Example: http://sacha.free.net.ph/notebook/wiki/"
61 :type 'string
62 :group 'planner-rss)
64 ;; On my system, this is set to
65 ;;'(("."
66 ;; "/home/sacha/public_html/notebook/wiki/blog.rdf"
67 ;; "<?xml version=\"1.0\"?><rss version=\"2.0\"><channel>
68 ;;<title>sachachua's blog</title>
69 ;;<link>http://sacha.free.net.ph/notebook/wiki/today.php</link>
70 ;;<description>Random notes</description>
71 ;;</channel></rss>\n"))
72 (defcustom planner-rss-category-feeds
73 nil
74 "List of (CONDITION FILENAME INITIAL-CONTENTS).
76 If CONDITION is a regexp, all entries that match the regexp in
77 either title or body will be included in FILENAME. If CONDITION
78 is a function with one argument, it will be called with the
79 marked-up text, and a non-nil return value means include this
80 entry in FILENAME.
82 If INITIAL-CONTENTS is non-nil, it is used to initialize the file if
83 the file is not found or is corrupted.
85 Example:
86 '((\".\"
87 \"/home/sacha/public_html/notebook/wiki/blog.rdf\"
88 \"<?xml version=\\\"1.0\\\"?><rss version=\\\"2.0\\\"><channel>
89 <title>sachachua's blog</title>
90 <link>http://sacha.free.net.ph/notebook/wiki/today.php</link>
91 <description>Random notes</description>
92 </channel></rss>\n\"))"
93 :type '(repeat (group (choice regexp function) file string))
94 :group 'planner-rss)
96 (defcustom planner-rss-feed-limits nil
97 "A list of (REGEX SIZE-LIMIT ITEM-LIMIT).
99 REGEX is a regular expression that matches the filename.
100 SIZE-LIMIT, if non-nil, is the upper limit in characters.
101 ITEM-LIMIT, if non-nil, is the upper limit in items. If the feed
102 exceeds the stated limits, older items are deleted."
103 :type '(alist :key-type regexp
104 :value-type (group (choice
105 :tag "Characters: "
106 (const :tag "No limit" nil)
107 (integer))
108 (choice
109 :tag "Size: "
110 (const :tag "No limit" nil)
111 (integer))))
112 :group 'planner-rss)
114 ;; Determined from planner-rss-category-feeds.
115 ;; You don't need to set this.
116 (defvar planner-rss-file-name nil "Filename of current RSS feed.")
117 (defvar planner-rss-initial-contents nil "Initial contents.")
119 (defun planner-rss-add-item (item)
120 "Add an item to the top of the items list in `planner-rss-file-name'.
121 It will have TITLE, LINK, DESCRIPTION, PUBDATE and CATEGORIES.
122 `planner-rss-initialize' is called if necessary."
123 (save-excursion
124 (save-window-excursion
125 (find-file planner-rss-file-name)
126 (goto-char (point-min))
127 (unless (re-search-forward "<item>\\|</channel>" nil t)
128 (progn
129 (erase-buffer)
130 (insert planner-rss-initial-contents)
131 (muse-publish-markup-region (point-min) (point-max) "*rss" "planner-rss")
132 (goto-char (point-max))
133 (re-search-backward "</channel>")))
134 (goto-char (match-beginning 0))
135 (insert item)
136 (planner-rss-limit)
137 (save-buffer))))
139 (defun planner-rss-strip-tags (string)
140 "Remove all tags from STRING."
141 (planner-replace-regexp-in-string "<[^>]+>" "" string))
143 ;;;###autoload
144 (defun planner-rss-add-note (&optional feed)
145 "Export the current note using `planner-rss-add-item'.
146 If FEED is non-nil, add the note to the specified feed only.
147 Call with the interactive prefix in order to be prompted for FEED."
148 (interactive (list (when current-prefix-arg
149 (read-file-name "Feed: "))))
150 (save-window-excursion
151 (save-excursion
152 (save-restriction
153 (when (planner-narrow-to-note)
154 (let* ((seen)
155 (text (buffer-substring-no-properties (point-min) (point-max)))
156 (muse-publishing-current-file (buffer-file-name))
157 (entry (with-temp-buffer
158 (insert text)
159 (muse-publish-markup-buffer "*rss*" "planner-rss")
160 (buffer-string))))
161 (dolist (feed planner-rss-category-feeds nil)
162 (let ((condition (elt feed 0))
163 (planner-rss-file-name (elt feed 1))
164 (planner-rss-initial-contents (elt feed 2)))
165 (when (cond ((functionp condition)
166 (funcall condition text))
167 ((stringp condition)
168 (string-match condition text))
169 (t condition))
170 (unless (member planner-rss-file-name seen)
171 (add-to-list 'seen planner-rss-file-name)
172 (planner-rss-add-item entry)))))))))))
174 (defun planner-rss-limit ()
175 "Apply limits specified in `planner-rss-feed-limits'."
176 (let ((filename (expand-file-name (planner-current-file))))
177 (mapcar
178 (lambda (item)
179 (when (string-match (elt item 0) filename)
180 (planner-rss-limit-size (elt item 1))
181 (planner-rss-limit-items (elt item 2))))
182 planner-rss-feed-limits)))
184 (defun planner-rss-limit-size (limit)
185 "Delete RSS items that cause this file to go over LIMIT characters."
186 (when limit
187 (widen)
188 (goto-char limit)
189 (unless (eobp)
190 (re-search-backward "<item>" nil t)
191 (let ((start (match-beginning 0)))
192 (re-search-forward "</channel>" nil t)
193 (delete-region start (match-beginning 0))))))
195 (defun planner-rss-limit-items (limit)
196 "Delete RSS items past the LIMIT-th item."
197 (when limit
198 (widen)
199 (goto-char (point-min))
200 (while (and (> limit -1) (re-search-forward "<item>" nil t))
201 (setq limit (1- limit)))
202 (when (= limit -1)
203 (let ((start (match-beginning 0)))
204 (re-search-forward "</channel>" nil t)
205 (delete-region start (match-beginning 0))))))
207 (defun planner-publish-markup-note-rss ()
208 "Replace note with RSS 2.0 representation of note data. Borrowed
209 heavily from Sacha's personal configs."
210 (save-restriction
211 (narrow-to-region
212 (save-excursion (beginning-of-line) (point))
213 (or (save-excursion (and (re-search-forward "<item>\\|</channel>" nil t)
214 (match-beginning 0)))
215 (point-max)))
216 (let ((info (planner-current-note-info t)))
217 (delete-region (point-min) (point-max))
218 (insert "<item>\n")
219 (insert "<title><verbatim>"
220 (muse-publish-escape-specials-in-string (planner-note-title info))
221 "</verbatim></title>\n")
222 (insert "<link><verbatim>"
223 (concat planner-rss-base-url (muse-page-name) ".html#"
224 (planner-note-anchor info))
225 "</verbatim></link>\n")
226 (insert "<guid><verbatim>"
227 (concat planner-rss-base-url (muse-page-name) ".html#"
228 (planner-note-anchor info))
229 "</verbatim></guid>\n")
230 (when (planner-note-body info)
231 (insert "<description><![CDATA["
232 (with-temp-buffer
233 (insert (planner-note-body info))
234 (muse-publish-markup-buffer "*title*" "planner-rss-info")
235 (buffer-string))
236 "]]></description>\n"))
237 (when (planner-note-date info)
238 (insert "<pubDate>"
239 (let ((system-time-locale "C")
240 (timestamp (planner-note-timestamp info))
241 (date (planner-filename-to-calendar-date
242 (planner-note-date info)))
243 (minutes) (hour) (day) (month) (year))
244 (format-time-string
245 "%a, %d %b %Y %T %Z"
246 (when (string-match "\\([0-9]+\\):\\([0-9]+\\)" timestamp)
247 (let ((hour (string-to-number (match-string 1 timestamp)))
248 (minutes (string-to-number
249 (match-string 2 timestamp)))
250 (month (nth 0 date))
251 (day (nth 1 date))
252 (year (nth 2 date)))
253 (encode-time 0 minutes hour day month year)))))
254 "</pubDate>\n"))
255 (insert "</item>\n"))))
257 (defcustom planner-publish-markup-rss-functions
258 '((note . planner-publish-markup-note-rss))
259 "An alist of style types to custom functions for that kind of text for RSS.
260 For more on the structure of this list, see
261 `muse-publish-markup-functions'."
262 :type '(alist :key-type symbol :value-type function)
263 :group 'planner-publish)
265 (unless (assoc "planner-rss" muse-publishing-styles)
266 (muse-derive-style "planner-rss" "planner-xml"
267 :functions 'planner-publish-markup-rss-functions
268 :header ""
269 :footer ""
270 :prefix planner-rss-base-url)
271 (muse-derive-style "planner-rss-info" "planner-html"
272 :header ""
273 :footer ""
274 :prefix planner-rss-base-url))
276 (provide 'planner-rss)
278 ;;; planner-rss.el ends here