Merge branch 'maint'
[org-mode.git] / contrib / lisp / ox-rss.el
blob63ee3bc7b62e1f23995df5855f7f3e78a14e94d1
1 ;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
3 ;; Copyright (C) 2013 Bastien Guerry
5 ;; Author: Bastien Guerry <bzg at gnu dot org>
6 ;; Keywords: org, wp, blog, feed, rss
8 ;; This file is not yet part of GNU Emacs.
10 ;; This program is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; This program is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
23 ;;; Commentary:
25 ;; This library implements a RSS 2.0 back-end for Org exporter, based on
26 ;; the `html' back-end.
28 ;; It requires Emacs 24.1 at least.
30 ;; It provides two commands for export, depending on the desired output:
31 ;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss'
32 ;; (as a ".xml" file).
34 ;; This backend understands two new option keywords:
36 ;; #+RSS_EXTENSION: xml
37 ;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
39 ;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
41 ;; Exporting an Org file to RSS modifies each top-level entry by adding a
42 ;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add
43 ;; an ID property, later used as the guid for the feed's item.
45 ;; You typically want to use it within a publishing project like this:
47 ;; (add-to-list
48 ;; 'org-publish-project-alist
49 ;; '("homepage_rss"
50 ;; :base-directory "~/myhomepage/"
51 ;; :base-extension "org"
52 ;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
53 ;; :html-link-home "http://lumiere.ens.fr/~guerry/"
54 ;; :rss-extension "xml"
55 ;; :publishing-directory "/home/guerry/public_html/"
56 ;; :publishing-function (org-rss-publish-to-rss)
57 ;; :section-numbers nil
58 ;; :exclude ".*" ;; To exclude all files...
59 ;; :include ("index.org") ;; ... except index.org.
60 ;; :table-of-contents nil))
62 ;; ... then rsync /home/guerry/public_html/ with your server.
64 ;;; Code:
66 (require 'ox-html)
67 (declare-function url-encode-url "url-util" (url))
69 ;;; Variables and options
71 (defgroup org-export-rss nil
72 "Options specific to RSS export back-end."
73 :tag "Org RSS"
74 :group 'org-export
75 :version "24.4"
76 :package-version '(Org . "8.0"))
78 (defcustom org-rss-image-url "http://orgmode.org/img/org-mode-unicorn-logo.png"
79 "The URL of the an image for the RSS feed."
80 :group 'org-export-rss
81 :type 'string)
83 (defcustom org-rss-extension "xml"
84 "File extension for the RSS 2.0 feed."
85 :group 'org-export-rss
86 :type 'string)
88 (defcustom org-rss-categories 'from-tags
89 "Where to extract items category information from.
90 The default is to extract categories from the tags of the
91 headlines. When set to another value, extract the category
92 from the :CATEGORY: property of the entry."
93 :group 'org-export-rss
94 :type '(choice
95 (const :tag "From tags" from-tags)
96 (const :tag "From the category property" from-category)))
98 (defcustom org-rss-use-entry-url-as-guid t
99 "Use the URL for the <guid> metatag?
100 When nil, Org will create ids using `org-icalendar-create-uid'."
101 :group 'org-export-rss
102 :type 'boolean)
104 ;;; Define backend
106 (org-export-define-derived-backend 'rss 'html
107 :menu-entry
108 '(?r "Export to RSS"
109 ((?R "As RSS buffer"
110 (lambda (a s v b) (org-rss-export-as-rss a s v)))
111 (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
112 (?o "As RSS file and open"
113 (lambda (a s v b)
114 (if a (org-rss-export-to-rss t s v)
115 (org-open-file (org-rss-export-to-rss nil s v)))))))
116 :options-alist
117 '((:with-toc nil nil nil) ;; Never include HTML's toc
118 (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
119 (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
120 (:rss-categories nil nil org-rss-categories))
121 :filters-alist '((:filter-final-output . org-rss-final-function))
122 :translate-alist '((headline . org-rss-headline)
123 (comment . (lambda (&rest args) ""))
124 (comment-block . (lambda (&rest args) ""))
125 (timestamp . (lambda (&rest args) ""))
126 (plain-text . org-rss-plain-text)
127 (section . org-rss-section)
128 (template . org-rss-template)))
130 ;;; Export functions
132 ;;;###autoload
133 (defun org-rss-export-as-rss (&optional async subtreep visible-only)
134 "Export current buffer to a RSS buffer.
136 If narrowing is active in the current buffer, only export its
137 narrowed part.
139 If a region is active, export that region.
141 A non-nil optional argument ASYNC means the process should happen
142 asynchronously. The resulting buffer should be accessible
143 through the `org-export-stack' interface.
145 When optional argument SUBTREEP is non-nil, export the sub-tree
146 at point, extracting information from the headline properties
147 first.
149 When optional argument VISIBLE-ONLY is non-nil, don't export
150 contents of hidden elements.
152 Export is done in a buffer named \"*Org RSS Export*\", which will
153 be displayed when `org-export-show-temporary-export-buffer' is
154 non-nil."
155 (interactive)
156 (let ((file (buffer-file-name (buffer-base-buffer))))
157 (org-icalendar-create-uid file 'warn-user)
158 (org-rss-add-pubdate-property))
159 (if async
160 (org-export-async-start
161 (lambda (output)
162 (with-current-buffer (get-buffer-create "*Org RSS Export*")
163 (erase-buffer)
164 (insert output)
165 (goto-char (point-min))
166 (text-mode)
167 (org-export-add-to-stack (current-buffer) 'rss)))
168 `(org-export-as 'rss ,subtreep ,visible-only))
169 (let ((outbuf (org-export-to-buffer
170 'rss "*Org RSS Export*" subtreep visible-only)))
171 (with-current-buffer outbuf (text-mode))
172 (when org-export-show-temporary-export-buffer
173 (switch-to-buffer-other-window outbuf)))))
175 ;;;###autoload
176 (defun org-rss-export-to-rss (&optional async subtreep visible-only)
177 "Export current buffer to a RSS file.
179 If narrowing is active in the current buffer, only export its
180 narrowed part.
182 If a region is active, export that region.
184 A non-nil optional argument ASYNC means the process should happen
185 asynchronously. The resulting file should be accessible through
186 the `org-export-stack' interface.
188 When optional argument SUBTREEP is non-nil, export the sub-tree
189 at point, extracting information from the headline properties
190 first.
192 When optional argument VISIBLE-ONLY is non-nil, don't export
193 contents of hidden elements.
195 Return output file's name."
196 (interactive)
197 (let ((file (buffer-file-name (buffer-base-buffer))))
198 (org-icalendar-create-uid file 'warn-user)
199 (org-rss-add-pubdate-property))
200 (let ((outfile (org-export-output-file-name
201 (concat "." org-rss-extension) subtreep)))
202 (if async
203 (org-export-async-start
204 (lambda (f) (org-export-add-to-stack f 'rss))
205 `(expand-file-name
206 (org-export-to-file 'rss ,outfile ,subtreep ,visible-only)))
207 (org-export-to-file 'rss outfile subtreep visible-only))))
209 ;;;###autoload
210 (defun org-rss-publish-to-rss (plist filename pub-dir)
211 "Publish an org file to RSS.
213 FILENAME is the filename of the Org file to be published. PLIST
214 is the property list for the given project. PUB-DIR is the
215 publishing directory.
217 Return output file name."
218 (let ((bf (get-file-buffer filename)))
219 (if bf
220 (with-current-buffer bf
221 (org-rss-add-pubdate-property)
222 (write-file filename))
223 (find-file filename)
224 (org-rss-add-pubdate-property)
225 (write-file filename) (kill-buffer)))
226 (org-publish-org-to
227 'rss filename (concat "." org-rss-extension) plist pub-dir))
229 ;;; Main transcoding functions
231 (defun org-rss-headline (headline contents info)
232 "Transcode HEADLINE element into RSS format.
233 CONTENTS is the headline contents. INFO is a plist used as a
234 communication channel."
235 (unless (or (org-element-property :footnote-section-p headline)
236 ;; Only consider first-level headlines
237 (> (org-export-get-relative-level headline info) 1))
238 (let* ((htmlext (plist-get info :html-extension))
239 (hl-number (org-export-get-headline-number headline info))
240 (anchor
241 (org-export-solidify-link-text
242 (or (org-element-property :CUSTOM_ID headline)
243 (concat "sec-" (mapconcat 'number-to-string hl-number "-")))))
244 (category (org-rss-plain-text
245 (or (org-element-property :CATEGORY headline) "") info))
246 (pubdate
247 (let ((system-time-locale "C"))
248 (format-time-string
249 "%a, %d %h %Y %H:%M:%S %z"
250 (org-time-string-to-time
251 (or (org-element-property :PUBDATE headline)
252 (error "Missing PUBDATE property"))))))
253 (title (org-element-property :raw-value headline))
254 (publink
255 (concat
256 (file-name-as-directory
257 (or (plist-get info :html-link-home)
258 (plist-get info :publishing-directory)))
259 (file-name-nondirectory
260 (file-name-sans-extension
261 (buffer-file-name))) "." htmlext "#" anchor))
262 (guid (if org-rss-use-entry-url-as-guid
263 publink
264 (org-rss-plain-text
265 (or (org-element-property :ID headline)
266 (org-element-property :CUSTOM_ID headline)
267 publink)
268 info))))
269 (format
270 (concat
271 "<item>\n"
272 "<title>%s</title>\n"
273 "<link>%s</link>\n"
274 "<guid isPermaLink=\"false\">%s</guid>\n"
275 "<pubDate>%s</pubDate>\n"
276 (org-rss-build-categories headline info) "\n"
277 "<description><![CDATA[%s]]></description>\n"
278 "</item>\n")
279 title publink guid pubdate contents))))
281 (defun org-rss-build-categories (headline info)
282 "Build categories for the RSS item."
283 (if (eq (plist-get info :rss-categories) 'from-tags)
284 (mapconcat
285 (lambda (c) (format "<category><![CDATA[%s]]></category>" c))
286 (org-element-property :tags headline)
287 "\n")
288 (let ((c (org-element-property :CATEGORY headline)))
289 (format "<category><![CDATA[%s]]></category>" c))))
291 (defun org-rss-template (contents info)
292 "Return complete document string after RSS conversion.
293 CONTENTS is the transcoded contents string. INFO is a plist used
294 as a communication channel."
295 (concat
296 (format "<?xml version=\"1.0\" encoding=\"%s\"?>"
297 (symbol-name org-html-coding-system))
298 "\n<rss version=\"2.0\"
299 xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
300 xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
301 xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
302 xmlns:atom=\"http://www.w3.org/2005/Atom\"
303 xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
304 xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
305 xmlns:georss=\"http://www.georss.org/georss\"
306 xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
307 xmlns:media=\"http://search.yahoo.com/mrss/\">"
308 "<channel>"
309 (org-rss-build-channel-info info) "\n"
310 contents
311 "</channel>\n"
312 "</rss>"))
314 (defun org-rss-build-channel-info (info)
315 "Build the RSS channel information."
316 (let* ((system-time-locale "C")
317 (title (plist-get info :title))
318 (email (org-export-data (plist-get info :email) info))
319 (author (and (plist-get info :with-author)
320 (let ((auth (plist-get info :author)))
321 (and auth (org-export-data auth info)))))
322 (date (format-time-string "%a, %d %h %Y %H:%M:%S %z")) ;; RFC 882
323 (description (org-export-data (plist-get info :description) info))
324 (lang (plist-get info :language))
325 (keywords (plist-get info :keywords))
326 (rssext (plist-get info :rss-extension))
327 (blogurl (or (plist-get info :html-link-home)
328 (plist-get info :publishing-directory)))
329 (image (url-encode-url (plist-get info :rss-image-url)))
330 (publink
331 (concat (file-name-as-directory blogurl)
332 (file-name-nondirectory
333 (file-name-sans-extension (buffer-file-name)))
334 "." rssext)))
335 (format
336 "\n<title>%s</title>
337 <atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
338 <link>%s</link>
339 <description><![CDATA[%s]]></description>
340 <language>%s</language>
341 <pubDate>%s</pubDate>
342 <lastBuildDate>%s</lastBuildDate>
343 <generator>%s</generator>
344 <webMaster>%s (%s)</webMaster>
345 <image>
346 <url>%s</url>
347 <title>%s</title>
348 <link>%s</link>
349 </image>
351 title publink blogurl description lang date date
352 (concat (format "Emacs %d.%d"
353 emacs-major-version
354 emacs-minor-version)
355 " Org-mode " (org-version))
356 email author image title blogurl)))
358 (defun org-rss-section (section contents info)
359 "Transcode SECTION element into RSS format.
360 CONTENTS is the section contents. INFO is a plist used as
361 a communication channel."
362 contents)
364 (defun org-rss-timestamp (timestamp contents info)
365 "Transcode a TIMESTAMP object from Org to RSS.
366 CONTENTS is nil. INFO is a plist holding contextual
367 information."
368 (org-html-encode-plain-text
369 (org-timestamp-translate timestamp)))
371 (defun org-rss-plain-text (contents info)
372 "Convert plain text into RSS encoded text."
373 (let (output)
374 (setq output (org-html-encode-plain-text contents)
375 output (org-export-activate-smart-quotes
376 output :html info))))
378 ;;; Filters
380 (defun org-rss-final-function (contents backend info)
381 "Prettify the RSS output."
382 (with-temp-buffer
383 (xml-mode)
384 (insert contents)
385 (indent-region (point-min) (point-max))
386 (buffer-substring-no-properties (point-min) (point-max))))
388 ;;; Miscellaneous
390 (defun org-rss-add-pubdate-property ()
391 "Set the PUBDATE property for top-level headlines."
392 (let (msg)
393 (org-map-entries
394 (lambda ()
395 (let* ((entry (org-element-at-point))
396 (level (org-element-property :level entry)))
397 (when (= level 1)
398 (unless (org-entry-get (point) "PUBDATE")
399 (setq msg t)
400 (org-set-property
401 "PUBDATE" (format-time-string
402 (cdr org-time-stamp-formats)))))))
403 nil nil 'comment 'archive)
404 (when msg
405 (message "Property PUBDATE added to top-level entries in %s"
406 (buffer-file-name))
407 (sit-for 2))))
409 (provide 'ox-rss)
411 ;;; ox-rss.el ends here