Merge branch 'maint'
[org-mode/org-tableheadings.git] / contrib / lisp / ox-rss.el
blobd207be6e81d24cf528ff8a14053cdeaf6fda14d1
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 ;; By default, the permalink for a blog entry points to the headline.
65 ;; You can specify a different one by using the :RSS_PERMALINK:
66 ;; property within an entry.
68 ;;; Code:
70 (require 'ox-html)
71 (declare-function url-encode-url "url-util" (url))
73 ;;; Variables and options
75 (defgroup org-export-rss nil
76 "Options specific to RSS export back-end."
77 :tag "Org RSS"
78 :group 'org-export
79 :version "24.4"
80 :package-version '(Org . "8.0"))
82 (defcustom org-rss-image-url "http://orgmode.org/img/org-mode-unicorn-logo.png"
83 "The URL of the an image for the RSS feed."
84 :group 'org-export-rss
85 :type 'string)
87 (defcustom org-rss-extension "xml"
88 "File extension for the RSS 2.0 feed."
89 :group 'org-export-rss
90 :type 'string)
92 (defcustom org-rss-categories 'from-tags
93 "Where to extract items category information from.
94 The default is to extract categories from the tags of the
95 headlines. When set to another value, extract the category
96 from the :CATEGORY: property of the entry."
97 :group 'org-export-rss
98 :type '(choice
99 (const :tag "From tags" from-tags)
100 (const :tag "From the category property" from-category)))
102 (defcustom org-rss-use-entry-url-as-guid t
103 "Use the URL for the <guid> metatag?
104 When nil, Org will create ids using `org-icalendar-create-uid'."
105 :group 'org-export-rss
106 :type 'boolean)
108 ;;; Define backend
110 (org-export-define-derived-backend 'rss 'html
111 :menu-entry
112 '(?r "Export to RSS"
113 ((?R "As RSS buffer"
114 (lambda (a s v b) (org-rss-export-as-rss a s v)))
115 (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
116 (?o "As RSS file and open"
117 (lambda (a s v b)
118 (if a (org-rss-export-to-rss t s v)
119 (org-open-file (org-rss-export-to-rss nil s v)))))))
120 :options-alist
121 '((:with-toc nil nil nil) ;; Never include HTML's toc
122 (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
123 (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
124 (:rss-categories nil nil org-rss-categories))
125 :filters-alist '((:filter-final-output . org-rss-final-function))
126 :translate-alist '((headline . org-rss-headline)
127 (comment . (lambda (&rest args) ""))
128 (comment-block . (lambda (&rest args) ""))
129 (timestamp . (lambda (&rest args) ""))
130 (plain-text . org-rss-plain-text)
131 (section . org-rss-section)
132 (template . org-rss-template)))
134 ;;; Export functions
136 ;;;###autoload
137 (defun org-rss-export-as-rss (&optional async subtreep visible-only)
138 "Export current buffer to a RSS buffer.
140 If narrowing is active in the current buffer, only export its
141 narrowed part.
143 If a region is active, export that region.
145 A non-nil optional argument ASYNC means the process should happen
146 asynchronously. The resulting buffer should be accessible
147 through the `org-export-stack' interface.
149 When optional argument SUBTREEP is non-nil, export the sub-tree
150 at point, extracting information from the headline properties
151 first.
153 When optional argument VISIBLE-ONLY is non-nil, don't export
154 contents of hidden elements.
156 Export is done in a buffer named \"*Org RSS Export*\", which will
157 be displayed when `org-export-show-temporary-export-buffer' is
158 non-nil."
159 (interactive)
160 (let ((file (buffer-file-name (buffer-base-buffer))))
161 (org-icalendar-create-uid file 'warn-user)
162 (org-rss-add-pubdate-property))
163 (if async
164 (org-export-async-start
165 (lambda (output)
166 (with-current-buffer (get-buffer-create "*Org RSS Export*")
167 (erase-buffer)
168 (insert output)
169 (goto-char (point-min))
170 (text-mode)
171 (org-export-add-to-stack (current-buffer) 'rss)))
172 `(org-export-as 'rss ,subtreep ,visible-only))
173 (let ((outbuf (org-export-to-buffer
174 'rss "*Org RSS Export*" subtreep visible-only)))
175 (with-current-buffer outbuf (text-mode))
176 (when org-export-show-temporary-export-buffer
177 (switch-to-buffer-other-window outbuf)))))
179 ;;;###autoload
180 (defun org-rss-export-to-rss (&optional async subtreep visible-only)
181 "Export current buffer to a RSS file.
183 If narrowing is active in the current buffer, only export its
184 narrowed part.
186 If a region is active, export that region.
188 A non-nil optional argument ASYNC means the process should happen
189 asynchronously. The resulting file should be accessible through
190 the `org-export-stack' interface.
192 When optional argument SUBTREEP is non-nil, export the sub-tree
193 at point, extracting information from the headline properties
194 first.
196 When optional argument VISIBLE-ONLY is non-nil, don't export
197 contents of hidden elements.
199 Return output file's name."
200 (interactive)
201 (let ((file (buffer-file-name (buffer-base-buffer))))
202 (org-icalendar-create-uid file 'warn-user)
203 (org-rss-add-pubdate-property))
204 (let ((outfile (org-export-output-file-name
205 (concat "." org-rss-extension) subtreep)))
206 (if async
207 (org-export-async-start
208 (lambda (f) (org-export-add-to-stack f 'rss))
209 `(expand-file-name
210 (org-export-to-file 'rss ,outfile ,subtreep ,visible-only)))
211 (org-export-to-file 'rss outfile subtreep visible-only))))
213 ;;;###autoload
214 (defun org-rss-publish-to-rss (plist filename pub-dir)
215 "Publish an org file to RSS.
217 FILENAME is the filename of the Org file to be published. PLIST
218 is the property list for the given project. PUB-DIR is the
219 publishing directory.
221 Return output file name."
222 (let ((bf (get-file-buffer filename)))
223 (if bf
224 (with-current-buffer bf
225 (org-rss-add-pubdate-property)
226 (write-file filename))
227 (find-file filename)
228 (org-rss-add-pubdate-property)
229 (write-file filename) (kill-buffer)))
230 (org-publish-org-to
231 'rss filename (concat "." org-rss-extension) plist pub-dir))
233 ;;; Main transcoding functions
235 (defun org-rss-headline (headline contents info)
236 "Transcode HEADLINE element into RSS format.
237 CONTENTS is the headline contents. INFO is a plist used as a
238 communication channel."
239 (unless (or (org-element-property :footnote-section-p headline)
240 ;; Only consider first-level headlines
241 (> (org-export-get-relative-level headline info) 1))
242 (let* ((htmlext (plist-get info :html-extension))
243 (hl-number (org-export-get-headline-number headline info))
244 (hl-home (file-name-as-directory (plist-get info :html-link-home)))
245 (hl-pdir (plist-get info :publishing-directory))
246 (hl-perm (org-element-property :RSS_PERMALINK headline))
247 (anchor
248 (org-export-solidify-link-text
249 (or (org-element-property :CUSTOM_ID headline)
250 (concat "sec-" (mapconcat 'number-to-string hl-number "-")))))
251 (category (org-rss-plain-text
252 (or (org-element-property :CATEGORY headline) "") info))
253 (pubdate
254 (let ((system-time-locale "C"))
255 (format-time-string
256 "%a, %d %h %Y %H:%M:%S %z"
257 (org-time-string-to-time
258 (or (org-element-property :PUBDATE headline)
259 (error "Missing PUBDATE property"))))))
260 (title (org-element-property :raw-value headline))
261 (publink
262 (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
263 (concat
264 (or hl-home hl-pdir)
265 (file-name-nondirectory
266 (file-name-sans-extension
267 (plist-get info :input-file))) "." htmlext "#" anchor)))
268 (guid (if org-rss-use-entry-url-as-guid
269 publink
270 (org-rss-plain-text
271 (or (org-element-property :ID headline)
272 (org-element-property :CUSTOM_ID headline)
273 publink)
274 info))))
275 (format
276 (concat
277 "<item>\n"
278 "<title>%s</title>\n"
279 "<link>%s</link>\n"
280 "<guid isPermaLink=\"false\">%s</guid>\n"
281 "<pubDate>%s</pubDate>\n"
282 (org-rss-build-categories headline info) "\n"
283 "<description><![CDATA[%s]]></description>\n"
284 "</item>\n")
285 title publink guid pubdate contents))))
287 (defun org-rss-build-categories (headline info)
288 "Build categories for the RSS item."
289 (if (eq (plist-get info :rss-categories) 'from-tags)
290 (mapconcat
291 (lambda (c) (format "<category><![CDATA[%s]]></category>" c))
292 (org-element-property :tags headline)
293 "\n")
294 (let ((c (org-element-property :CATEGORY headline)))
295 (format "<category><![CDATA[%s]]></category>" c))))
297 (defun org-rss-template (contents info)
298 "Return complete document string after RSS conversion.
299 CONTENTS is the transcoded contents string. INFO is a plist used
300 as a communication channel."
301 (concat
302 (format "<?xml version=\"1.0\" encoding=\"%s\"?>"
303 (symbol-name org-html-coding-system))
304 "\n<rss version=\"2.0\"
305 xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
306 xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
307 xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
308 xmlns:atom=\"http://www.w3.org/2005/Atom\"
309 xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
310 xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
311 xmlns:georss=\"http://www.georss.org/georss\"
312 xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
313 xmlns:media=\"http://search.yahoo.com/mrss/\">"
314 "<channel>"
315 (org-rss-build-channel-info info) "\n"
316 contents
317 "</channel>\n"
318 "</rss>"))
320 (defun org-rss-build-channel-info (info)
321 "Build the RSS channel information."
322 (let* ((system-time-locale "C")
323 (title (plist-get info :title))
324 (email (org-export-data (plist-get info :email) info))
325 (author (and (plist-get info :with-author)
326 (let ((auth (plist-get info :author)))
327 (and auth (org-export-data auth info)))))
328 (date (format-time-string "%a, %d %h %Y %H:%M:%S %z")) ;; RFC 882
329 (description (org-export-data (plist-get info :description) info))
330 (lang (plist-get info :language))
331 (keywords (plist-get info :keywords))
332 (rssext (plist-get info :rss-extension))
333 (blogurl (or (plist-get info :html-link-home)
334 (plist-get info :publishing-directory)))
335 (image (url-encode-url (plist-get info :rss-image-url)))
336 (ifile (plist-get info :input-file))
337 (publink
338 (concat (file-name-as-directory blogurl)
339 (file-name-nondirectory
340 (file-name-sans-extension ifile))
341 "." rssext)))
342 (format
343 "\n<title>%s</title>
344 <atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
345 <link>%s</link>
346 <description><![CDATA[%s]]></description>
347 <language>%s</language>
348 <pubDate>%s</pubDate>
349 <lastBuildDate>%s</lastBuildDate>
350 <generator>%s</generator>
351 <webMaster>%s (%s)</webMaster>
352 <image>
353 <url>%s</url>
354 <title>%s</title>
355 <link>%s</link>
356 </image>
358 title publink blogurl description lang date date
359 (concat (format "Emacs %d.%d"
360 emacs-major-version
361 emacs-minor-version)
362 " Org-mode " (org-version))
363 email author image title blogurl)))
365 (defun org-rss-section (section contents info)
366 "Transcode SECTION element into RSS format.
367 CONTENTS is the section contents. INFO is a plist used as
368 a communication channel."
369 contents)
371 (defun org-rss-timestamp (timestamp contents info)
372 "Transcode a TIMESTAMP object from Org to RSS.
373 CONTENTS is nil. INFO is a plist holding contextual
374 information."
375 (org-html-encode-plain-text
376 (org-timestamp-translate timestamp)))
378 (defun org-rss-plain-text (contents info)
379 "Convert plain text into RSS encoded text."
380 (let (output)
381 (setq output (org-html-encode-plain-text contents)
382 output (org-export-activate-smart-quotes
383 output :html info))))
385 ;;; Filters
387 (defun org-rss-final-function (contents backend info)
388 "Prettify the RSS output."
389 (with-temp-buffer
390 (xml-mode)
391 (insert contents)
392 (indent-region (point-min) (point-max))
393 (buffer-substring-no-properties (point-min) (point-max))))
395 ;;; Miscellaneous
397 (defun org-rss-add-pubdate-property ()
398 "Set the PUBDATE property for top-level headlines."
399 (let (msg)
400 (org-map-entries
401 (lambda ()
402 (let* ((entry (org-element-at-point))
403 (level (org-element-property :level entry)))
404 (when (= level 1)
405 (unless (org-entry-get (point) "PUBDATE")
406 (setq msg t)
407 (org-set-property
408 "PUBDATE" (format-time-string
409 (cdr org-time-stamp-formats)))))))
410 nil nil 'comment 'archive)
411 (when msg
412 (message "Property PUBDATE added to top-level entries in %s"
413 (buffer-file-name))
414 (sit-for 2))))
416 (provide 'ox-rss)
418 ;;; ox-rss.el ends here