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