org.el: use `user-error' instead of `error'.
[org-mode.git] / contrib / lisp / ox-rss.el
blobc69a37b6e0913fb4b95080018d71d6d9958749f4
1 ;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
3 ;; Copyright (C) 2013, 2014 Bastien Guerry
5 ;; Author: Bastien Guerry <bzg@gnu.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 ;; :html-link-use-abs-url t
55 ;; :rss-extension "xml"
56 ;; :publishing-directory "/home/guerry/public_html/"
57 ;; :publishing-function (org-rss-publish-to-rss)
58 ;; :section-numbers nil
59 ;; :exclude ".*" ;; To exclude all files...
60 ;; :include ("index.org") ;; ... except index.org.
61 ;; :table-of-contents nil))
63 ;; ... then rsync /home/guerry/public_html/ with your server.
65 ;; By default, the permalink for a blog entry points to the headline.
66 ;; You can specify a different one by using the :RSS_PERMALINK:
67 ;; property within an entry.
69 ;;; Code:
71 (require 'ox-html)
72 (declare-function url-encode-url "url-util" (url))
74 ;;; Variables and options
76 (defgroup org-export-rss nil
77 "Options specific to RSS export back-end."
78 :tag "Org RSS"
79 :group 'org-export
80 :version "24.4"
81 :package-version '(Org . "8.0"))
83 (defcustom org-rss-image-url "http://orgmode.org/img/org-mode-unicorn-logo.png"
84 "The URL of the an image for the RSS feed."
85 :group 'org-export-rss
86 :type 'string)
88 (defcustom org-rss-extension "xml"
89 "File extension for the RSS 2.0 feed."
90 :group 'org-export-rss
91 :type 'string)
93 (defcustom org-rss-categories 'from-tags
94 "Where to extract items category information from.
95 The default is to extract categories from the tags of the
96 headlines. When set to another value, extract the category
97 from the :CATEGORY: property of the entry."
98 :group 'org-export-rss
99 :type '(choice
100 (const :tag "From tags" from-tags)
101 (const :tag "From the category property" from-category)))
103 (defcustom org-rss-use-entry-url-as-guid t
104 "Use the URL for the <guid> metatag?
105 When nil, Org will create ids using `org-icalendar-create-uid'."
106 :group 'org-export-rss
107 :type 'boolean)
109 ;;; Define backend
111 (org-export-define-derived-backend 'rss 'html
112 :menu-entry
113 '(?r "Export to RSS"
114 ((?R "As RSS buffer"
115 (lambda (a s v b) (org-rss-export-as-rss a s v)))
116 (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
117 (?o "As RSS file and open"
118 (lambda (a s v b)
119 (if a (org-rss-export-to-rss t s v)
120 (org-open-file (org-rss-export-to-rss nil s v)))))))
121 :options-alist
122 '((:with-toc nil nil nil) ;; Never include HTML's toc
123 (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
124 (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
125 (:rss-categories nil nil org-rss-categories))
126 :filters-alist '((:filter-final-output . org-rss-final-function))
127 :translate-alist '((headline . org-rss-headline)
128 (comment . (lambda (&rest args) ""))
129 (comment-block . (lambda (&rest args) ""))
130 (timestamp . (lambda (&rest args) ""))
131 (plain-text . org-rss-plain-text)
132 (section . org-rss-section)
133 (template . org-rss-template)))
135 ;;; Export functions
137 ;;;###autoload
138 (defun org-rss-export-as-rss (&optional async subtreep visible-only)
139 "Export current buffer to a RSS buffer.
141 If narrowing is active in the current buffer, only export its
142 narrowed part.
144 If a region is active, export that region.
146 A non-nil optional argument ASYNC means the process should happen
147 asynchronously. The resulting buffer should be accessible
148 through the `org-export-stack' interface.
150 When optional argument SUBTREEP is non-nil, export the sub-tree
151 at point, extracting information from the headline properties
152 first.
154 When optional argument VISIBLE-ONLY is non-nil, don't export
155 contents of hidden elements.
157 Export is done in a buffer named \"*Org RSS Export*\", which will
158 be displayed when `org-export-show-temporary-export-buffer' is
159 non-nil."
160 (interactive)
161 (let ((file (buffer-file-name (buffer-base-buffer))))
162 (org-icalendar-create-uid file 'warn-user)
163 (org-rss-add-pubdate-property))
164 (org-export-to-buffer 'rss "*Org RSS Export*"
165 async subtreep visible-only nil nil (lambda () (text-mode))))
167 ;;;###autoload
168 (defun org-rss-export-to-rss (&optional async subtreep visible-only)
169 "Export current buffer to a RSS file.
171 If narrowing is active in the current buffer, only export its
172 narrowed part.
174 If a region is active, export that region.
176 A non-nil optional argument ASYNC means the process should happen
177 asynchronously. The resulting file should be accessible through
178 the `org-export-stack' interface.
180 When optional argument SUBTREEP is non-nil, export the sub-tree
181 at point, extracting information from the headline properties
182 first.
184 When optional argument VISIBLE-ONLY is non-nil, don't export
185 contents of hidden elements.
187 Return output file's name."
188 (interactive)
189 (let ((file (buffer-file-name (buffer-base-buffer))))
190 (org-icalendar-create-uid file 'warn-user)
191 (org-rss-add-pubdate-property))
192 (let ((outfile (org-export-output-file-name
193 (concat "." org-rss-extension) subtreep)))
194 (org-export-to-file 'rss outfile async subtreep visible-only)))
196 ;;;###autoload
197 (defun org-rss-publish-to-rss (plist filename pub-dir)
198 "Publish an org file to RSS.
200 FILENAME is the filename of the Org file to be published. PLIST
201 is the property list for the given project. PUB-DIR is the
202 publishing directory.
204 Return output file name."
205 (let ((bf (get-file-buffer filename)))
206 (if bf
207 (progn
208 (org-icalendar-create-uid filename 'warn-user)
209 (with-current-buffer bf
210 (org-rss-add-pubdate-property)
211 (write-file filename)))
212 (find-file filename)
213 (org-icalendar-create-uid filename 'warn-user)
214 (org-rss-add-pubdate-property)
215 (write-file filename) (kill-buffer)))
216 (org-publish-org-to
217 'rss filename (concat "." org-rss-extension) plist pub-dir))
219 ;;; Main transcoding functions
221 (defun org-rss-headline (headline contents info)
222 "Transcode HEADLINE element into RSS format.
223 CONTENTS is the headline contents. INFO is a plist used as a
224 communication channel."
225 (unless (or (org-element-property :footnote-section-p headline)
226 ;; Only consider first-level headlines
227 (> (org-export-get-relative-level headline info) 1))
228 (let* ((author (and (plist-get info :with-author)
229 (let ((auth (plist-get info :author)))
230 (and auth (org-export-data auth info)))))
231 (htmlext (plist-get info :html-extension))
232 (hl-number (org-export-get-headline-number headline info))
233 (hl-home (file-name-as-directory (plist-get info :html-link-home)))
234 (hl-pdir (plist-get info :publishing-directory))
235 (hl-perm (org-element-property :RSS_PERMALINK headline))
236 (anchor
237 (org-export-solidify-link-text
238 (or (org-element-property :CUSTOM_ID headline)
239 (concat "sec-" (mapconcat 'number-to-string hl-number "-")))))
240 (category (org-rss-plain-text
241 (or (org-element-property :CATEGORY headline) "") info))
242 (pubdate
243 (let ((system-time-locale "C"))
244 (format-time-string
245 "%a, %d %b %Y %H:%M:%S %z"
246 (org-time-string-to-time
247 (or (org-element-property :PUBDATE headline)
248 (error "Missing PUBDATE property"))))))
249 (title (replace-regexp-in-string
250 org-bracket-link-regexp
251 (lambda (m) (or (match-string 3 m)
252 (match-string 1 m)))
253 (org-element-property :raw-value headline)))
254 (publink
255 (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
256 (concat
257 (or hl-home hl-pdir)
258 (file-name-nondirectory
259 (file-name-sans-extension
260 (plist-get info :input-file))) "." htmlext "#" anchor)))
261 (guid (if org-rss-use-entry-url-as-guid
262 publink
263 (org-rss-plain-text
264 (or (org-element-property :ID headline)
265 (org-element-property :CUSTOM_ID headline)
266 publink)
267 info))))
268 (format
269 (concat
270 "<item>\n"
271 "<title>%s</title>\n"
272 "<link>%s</link>\n"
273 "<author>%s</author>\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 author 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 %b %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 (ifile (plist-get info :input-file))
331 (publink
332 (concat (file-name-as-directory blogurl)
333 (file-name-nondirectory
334 (file-name-sans-extension ifile))
335 "." rssext)))
336 (format
337 "\n<title>%s</title>
338 <atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
339 <link>%s</link>
340 <description><![CDATA[%s]]></description>
341 <language>%s</language>
342 <pubDate>%s</pubDate>
343 <lastBuildDate>%s</lastBuildDate>
344 <generator>%s</generator>
345 <webMaster>%s (%s)</webMaster>
346 <image>
347 <url>%s</url>
348 <title>%s</title>
349 <link>%s</link>
350 </image>
352 title publink blogurl description lang date date
353 (concat (format "Emacs %d.%d"
354 emacs-major-version
355 emacs-minor-version)
356 " Org-mode " (org-version))
357 email author image title blogurl)))
359 (defun org-rss-section (section contents info)
360 "Transcode SECTION element into RSS format.
361 CONTENTS is the section contents. INFO is a plist used as
362 a communication channel."
363 contents)
365 (defun org-rss-timestamp (timestamp contents info)
366 "Transcode a TIMESTAMP object from Org to RSS.
367 CONTENTS is nil. INFO is a plist holding contextual
368 information."
369 (org-html-encode-plain-text
370 (org-timestamp-translate timestamp)))
372 (defun org-rss-plain-text (contents info)
373 "Convert plain text into RSS encoded text."
374 (let (output)
375 (setq output (org-html-encode-plain-text contents)
376 output (org-export-activate-smart-quotes
377 output :html info))))
379 ;;; Filters
381 (defun org-rss-final-function (contents backend info)
382 "Prettify the RSS output."
383 (with-temp-buffer
384 (xml-mode)
385 (insert contents)
386 (indent-region (point-min) (point-max))
387 (buffer-substring-no-properties (point-min) (point-max))))
389 ;;; Miscellaneous
391 (defun org-rss-add-pubdate-property ()
392 "Set the PUBDATE property for top-level headlines."
393 (let (msg)
394 (org-map-entries
395 (lambda ()
396 (let* ((entry (org-element-at-point))
397 (level (org-element-property :level entry)))
398 (when (= level 1)
399 (unless (org-entry-get (point) "PUBDATE")
400 (setq msg t)
401 (org-set-property
402 "PUBDATE" (format-time-string
403 (cdr org-time-stamp-formats)))))))
404 nil nil 'comment 'archive)
405 (when msg
406 (message "Property PUBDATE added to top-level entries in %s"
407 (buffer-file-name))
408 (sit-for 2))))
410 (provide 'ox-rss)
412 ;;; ox-rss.el ends here