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