org-manual: Fix typo
[org-mode/org-tableheadings.git] / contrib / lisp / ox-rss.el
blob10f2cc2441be2d364c3789402aa9acb98c7524e9
1 ;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
3 ;; Copyright (C) 2013-2015 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 three new option keywords:
36 ;; #+RSS_EXTENSION: xml
37 ;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
38 ;; #+RSS_FEED_URL: http://myblog.org/feeds/blog.xml
40 ;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
42 ;; Exporting an Org file to RSS modifies each top-level entry by adding a
43 ;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add
44 ;; an ID property, later used as the guid for the feed's item.
46 ;; The top-level headline is used as the title of each RSS item unless
47 ;; an RSS_TITLE property is set on the headline.
49 ;; You typically want to use it within a publishing project like this:
51 ;; (add-to-list
52 ;; 'org-publish-project-alist
53 ;; '("homepage_rss"
54 ;; :base-directory "~/myhomepage/"
55 ;; :base-extension "org"
56 ;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
57 ;; :html-link-home "http://lumiere.ens.fr/~guerry/"
58 ;; :html-link-use-abs-url t
59 ;; :rss-extension "xml"
60 ;; :publishing-directory "/home/guerry/public_html/"
61 ;; :publishing-function (org-rss-publish-to-rss)
62 ;; :section-numbers nil
63 ;; :exclude ".*" ;; To exclude all files...
64 ;; :include ("index.org") ;; ... except index.org.
65 ;; :table-of-contents nil))
67 ;; ... then rsync /home/guerry/public_html/ with your server.
69 ;; By default, the permalink for a blog entry points to the headline.
70 ;; You can specify a different one by using the :RSS_PERMALINK:
71 ;; property within an entry.
73 ;;; Code:
75 (require 'ox-html)
76 (declare-function url-encode-url "url-util" (url))
78 ;;; Variables and options
80 (defgroup org-export-rss nil
81 "Options specific to RSS export back-end."
82 :tag "Org RSS"
83 :group 'org-export
84 :version "24.4"
85 :package-version '(Org . "8.0"))
87 (defcustom org-rss-image-url "https://orgmode.org/img/org-mode-unicorn-logo.png"
88 "The URL of the an image for the RSS feed."
89 :group 'org-export-rss
90 :type 'string)
92 (defcustom org-rss-extension "xml"
93 "File extension for the RSS 2.0 feed."
94 :group 'org-export-rss
95 :type 'string)
97 (defcustom org-rss-categories 'from-tags
98 "Where to extract items category information from.
99 The default is to extract categories from the tags of the
100 headlines. When set to another value, extract the category
101 from the :CATEGORY: property of the entry."
102 :group 'org-export-rss
103 :type '(choice
104 (const :tag "From tags" from-tags)
105 (const :tag "From the category property" from-category)))
107 (defcustom org-rss-use-entry-url-as-guid t
108 "Use the URL for the <guid> metatag?
109 When nil, Org will create ids using `org-icalendar-create-uid'."
110 :group 'org-export-rss
111 :type 'boolean)
113 ;;; Define backend
115 (org-export-define-derived-backend 'rss 'html
116 :menu-entry
117 '(?r "Export to RSS"
118 ((?R "As RSS buffer"
119 (lambda (a s v b) (org-rss-export-as-rss a s v)))
120 (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
121 (?o "As RSS file and open"
122 (lambda (a s v b)
123 (if a (org-rss-export-to-rss t s v)
124 (org-open-file (org-rss-export-to-rss nil s v)))))))
125 :options-alist
126 '((:description "DESCRIPTION" nil nil newline)
127 (:keywords "KEYWORDS" nil nil space)
128 (:with-toc nil nil nil) ;; Never include HTML's toc
129 (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
130 (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
131 (:rss-feed-url "RSS_FEED_URL" nil nil t)
132 (:rss-categories nil nil org-rss-categories))
133 :filters-alist '((:filter-final-output . org-rss-final-function))
134 :translate-alist '((headline . org-rss-headline)
135 (comment . (lambda (&rest args) ""))
136 (comment-block . (lambda (&rest args) ""))
137 (timestamp . (lambda (&rest args) ""))
138 (plain-text . org-rss-plain-text)
139 (section . org-rss-section)
140 (template . org-rss-template)))
142 ;;; Export functions
144 ;;;###autoload
145 (defun org-rss-export-as-rss (&optional async subtreep visible-only)
146 "Export current buffer to a RSS buffer.
148 If narrowing is active in the current buffer, only export its
149 narrowed part.
151 If a region is active, export that region.
153 A non-nil optional argument ASYNC means the process should happen
154 asynchronously. The resulting buffer should be accessible
155 through the `org-export-stack' interface.
157 When optional argument SUBTREEP is non-nil, export the sub-tree
158 at point, extracting information from the headline properties
159 first.
161 When optional argument VISIBLE-ONLY is non-nil, don't export
162 contents of hidden elements.
164 Export is done in a buffer named \"*Org RSS Export*\", which will
165 be displayed when `org-export-show-temporary-export-buffer' is
166 non-nil."
167 (interactive)
168 (let ((file (buffer-file-name (buffer-base-buffer))))
169 (org-icalendar-create-uid file 'warn-user)
170 (org-rss-add-pubdate-property))
171 (org-export-to-buffer 'rss "*Org RSS Export*"
172 async subtreep visible-only nil nil (lambda () (text-mode))))
174 ;;;###autoload
175 (defun org-rss-export-to-rss (&optional async subtreep visible-only)
176 "Export current buffer to a RSS file.
178 If narrowing is active in the current buffer, only export its
179 narrowed part.
181 If a region is active, export that region.
183 A non-nil optional argument ASYNC means the process should happen
184 asynchronously. The resulting file should be accessible through
185 the `org-export-stack' interface.
187 When optional argument SUBTREEP is non-nil, export the sub-tree
188 at point, extracting information from the headline properties
189 first.
191 When optional argument VISIBLE-ONLY is non-nil, don't export
192 contents of hidden elements.
194 Return output file's name."
195 (interactive)
196 (let ((file (buffer-file-name (buffer-base-buffer))))
197 (org-icalendar-create-uid file 'warn-user)
198 (org-rss-add-pubdate-property))
199 (let ((outfile (org-export-output-file-name
200 (concat "." org-rss-extension) subtreep)))
201 (org-export-to-file 'rss outfile async subtreep visible-only)))
203 ;;;###autoload
204 (defun org-rss-publish-to-rss (plist filename pub-dir)
205 "Publish an org file to RSS.
207 FILENAME is the filename of the Org file to be published. PLIST
208 is the property list for the given project. PUB-DIR is the
209 publishing directory.
211 Return output file name."
212 (let ((bf (get-file-buffer filename)))
213 (if bf
214 (with-current-buffer bf
215 (org-icalendar-create-uid filename 'warn-user)
216 (org-rss-add-pubdate-property)
217 (write-file filename))
218 (find-file filename)
219 (org-icalendar-create-uid filename 'warn-user)
220 (org-rss-add-pubdate-property)
221 (write-file filename) (kill-buffer)))
222 (org-publish-org-to
223 'rss filename (concat "." org-rss-extension) plist pub-dir))
225 ;;; Main transcoding functions
227 (defun org-rss-headline (headline contents info)
228 "Transcode HEADLINE element into RSS format.
229 CONTENTS is the headline contents. INFO is a plist used as a
230 communication channel."
231 (if (> (org-export-get-relative-level headline info) 1)
232 (org-export-data-with-backend headline 'html info)
233 (unless (org-element-property :footnote-section-p headline)
234 (let* ((email (org-export-data (plist-get info :email) info))
235 (author (and (plist-get info :with-author)
236 (let ((auth (plist-get info :author)))
237 (and auth (org-export-data auth info)))))
238 (htmlext (plist-get info :html-extension))
239 (hl-number (org-export-get-headline-number headline info))
240 (hl-home (file-name-as-directory (plist-get info :html-link-home)))
241 (hl-pdir (plist-get info :publishing-directory))
242 (hl-perm (org-element-property :RSS_PERMALINK headline))
243 (anchor (org-export-get-reference headline info))
244 (category (org-rss-plain-text
245 (or (org-element-property :CATEGORY headline) "") info))
246 (pubdate0 (org-element-property :PUBDATE headline))
247 (pubdate (let ((system-time-locale "C"))
248 (if pubdate0
249 (format-time-string
250 "%a, %d %b %Y %H:%M:%S %z"
251 (org-time-string-to-time pubdate0)))))
252 (title (org-rss-plain-text
253 (or (org-element-property :RSS_TITLE headline)
254 (replace-regexp-in-string
255 org-bracket-link-regexp
256 (lambda (m) (or (match-string 3 m)
257 (match-string 1 m)))
258 (org-element-property :raw-value headline))) info))
259 (publink
260 (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
261 (concat
262 (or hl-home hl-pdir)
263 (file-name-nondirectory
264 (file-name-sans-extension
265 (plist-get info :input-file))) "." htmlext "#" anchor)))
266 (guid (if org-rss-use-entry-url-as-guid
267 publink
268 (org-rss-plain-text
269 (or (org-element-property :ID headline)
270 (org-element-property :CUSTOM_ID headline)
271 publink)
272 info))))
273 (if (not pubdate0) "" ;; Skip entries with no PUBDATE prop
274 (format
275 (concat
276 "<item>\n"
277 "<title>%s</title>\n"
278 "<link>%s</link>\n"
279 "<author>%s (%s)</author>\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 email author 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 (org-export-data (plist-get info :title) info))
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 %b %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 (or (plist-get info :rss-feed-url)
339 (concat (file-name-as-directory blogurl)
340 (file-name-nondirectory
341 (file-name-sans-extension ifile))
342 "." rssext))))
343 (format
344 "\n<title>%s</title>
345 <atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
346 <link>%s</link>
347 <description><![CDATA[%s]]></description>
348 <language>%s</language>
349 <pubDate>%s</pubDate>
350 <lastBuildDate>%s</lastBuildDate>
351 <generator>%s</generator>
352 <webMaster>%s (%s)</webMaster>
353 <image>
354 <url>%s</url>
355 <title>%s</title>
356 <link>%s</link>
357 </image>
359 title publink blogurl description lang date date
360 (concat (format "Emacs %d.%d"
361 emacs-major-version
362 emacs-minor-version)
363 " Org-mode " (org-version))
364 email author image title blogurl)))
366 (defun org-rss-section (section contents info)
367 "Transcode SECTION element into RSS format.
368 CONTENTS is the section contents. INFO is a plist used as
369 a communication channel."
370 contents)
372 (defun org-rss-timestamp (timestamp contents info)
373 "Transcode a TIMESTAMP object from Org to RSS.
374 CONTENTS is nil. INFO is a plist holding contextual
375 information."
376 (org-html-encode-plain-text
377 (org-timestamp-translate timestamp)))
379 (defun org-rss-plain-text (contents info)
380 "Convert plain text into RSS encoded text."
381 (let (output)
382 (setq output (org-html-encode-plain-text contents)
383 output (org-export-activate-smart-quotes
384 output :html info))))
386 ;;; Filters
388 (defun org-rss-final-function (contents backend info)
389 "Prettify the RSS output."
390 (with-temp-buffer
391 (xml-mode)
392 (insert contents)
393 (indent-region (point-min) (point-max))
394 (buffer-substring-no-properties (point-min) (point-max))))
396 ;;; Miscellaneous
398 (defun org-rss-add-pubdate-property ()
399 "Set the PUBDATE property for top-level headlines."
400 (let (msg)
401 (org-map-entries
402 (lambda ()
403 (let* ((entry (org-element-at-point))
404 (level (org-element-property :level entry)))
405 (when (= level 1)
406 (unless (org-entry-get (point) "PUBDATE")
407 (setq msg t)
408 (org-set-property
409 "PUBDATE" (format-time-string
410 (cdr org-time-stamp-formats)))))))
411 nil nil 'comment 'archive)
412 (when msg
413 (message "Property PUBDATE added to top-level entries in %s"
414 (buffer-file-name))
415 (sit-for 2))))
417 (provide 'ox-rss)
419 ;;; ox-rss.el ends here