ox-rss.el 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. ;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
  2. ;; Copyright (C) 2013 Bastien Guerry
  3. ;; Author: Bastien Guerry <bzg at gnu dot org>
  4. ;; Keywords: org, wp, blog, feed, rss
  5. ;; This file is not yet part of GNU Emacs.
  6. ;; This program is free software: you can redistribute it and/or modify
  7. ;; it under the terms of the GNU General Public License as published by
  8. ;; the Free Software Foundation, either version 3 of the License, or
  9. ;; (at your option) any later version.
  10. ;; This program is distributed in the hope that it will be useful,
  11. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. ;; GNU General Public License for more details.
  14. ;; You should have received a copy of the GNU General Public License
  15. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  16. ;;; Commentary:
  17. ;; This library implements a RSS 2.0 back-end for Org exporter, based on
  18. ;; the `html' back-end.
  19. ;;
  20. ;; It provides two commands for export, depending on the desired output:
  21. ;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss'
  22. ;; (as a ".xml" file).
  23. ;;
  24. ;; This backend understands two new option keywords:
  25. ;;
  26. ;; #+RSS_EXTENSION: xml
  27. ;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
  28. ;;
  29. ;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
  30. ;;
  31. ;; Exporting an Org file to RSS modifies each top-level entry by adding a
  32. ;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add
  33. ;; an ID property, later used as the guid for the feed's item.
  34. ;;
  35. ;; You typically want to use it within a publishing project like this:
  36. ;;
  37. ;; (add-to-list
  38. ;; 'org-publish-project-alist
  39. ;; '("homepage_rss"
  40. ;; :base-directory "~/myhomepage/"
  41. ;; :base-extension "org"
  42. ;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
  43. ;; :home-link-home "http://lumiere.ens.fr/~guerry/"
  44. ;; :rss-extension "xml"
  45. ;; :publishing-directory "/home/guerry/public_html/"
  46. ;; :publishing-function (org-rss-publish-to-rss)
  47. ;; :section-numbers nil
  48. ;; :exclude ".*" ;; To exclude all files...
  49. ;; :include ("index.org") ;; ... except index.org.
  50. ;; :table-of-contents nil))
  51. ;;
  52. ;; ... then rsync /home/guerry/public_html/ with your server.
  53. ;;; Code:
  54. (require 'ox-html)
  55. (declare-function url-encode-url "url-util" (url))
  56. ;;; Variables and options
  57. (defgroup org-export-rss nil
  58. "Options specific to RSS export back-end."
  59. :tag "Org RSS"
  60. :group 'org-export
  61. :version "24.4"
  62. :package-version '(Org . "8.0"))
  63. (defcustom org-rss-image-url "http://orgmode.org/img/org-mode-unicorn-logo.png"
  64. "The URL of the an image for the RSS feed."
  65. :group 'org-export-rss
  66. :type 'string)
  67. (defcustom org-rss-extension "xml"
  68. "File extension for the RSS 2.0 feed."
  69. :group 'org-export-rss
  70. :type 'string)
  71. (defcustom org-rss-categories 'from-tags
  72. "Where to extract items category information from.
  73. The default is to extract categories from the tags of the
  74. headlines. When set to another value, extract the category
  75. from the :CATEGORY: property of the entry."
  76. :group 'org-export-rss
  77. :type '(choice
  78. (const :tag "From tags" from-tags)
  79. (const :tag "From the category property" from-category)))
  80. (defcustom org-rss-use-entry-url-as-guid t
  81. "Use the URL for the <guid> metatag?
  82. When nil, Org will create ids using `org-icalendar-create-uid'."
  83. :group 'org-export-rss
  84. :type 'boolean)
  85. ;;; Define backend
  86. (org-export-define-derived-backend 'rss 'html
  87. :menu-entry
  88. '(?r "Export to RSS"
  89. ((?R "As RSS buffer"
  90. (lambda (a s v b) (org-rss-export-as-rss a s v)))
  91. (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
  92. (?o "As RSS file and open"
  93. (lambda (a s v b)
  94. (if a (org-rss-export-to-rss t s v)
  95. (org-open-file (org-rss-export-to-rss nil s v)))))))
  96. :options-alist
  97. '((:with-toc nil nil nil) ;; Never include HTML's toc
  98. (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
  99. (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
  100. (:rss-categories nil nil org-rss-categories))
  101. :filters-alist '((:filter-final-output . org-rss-final-function))
  102. :translate-alist '((headline . org-rss-headline)
  103. (comment . (lambda (&rest args) ""))
  104. (comment-block . (lambda (&rest args) ""))
  105. (timestamp . (lambda (&rest args) ""))
  106. (plain-text . org-rss-plain-text)
  107. (section . org-rss-section)
  108. (template . org-rss-template)))
  109. ;;; Export functions
  110. ;;;###autoload
  111. (defun org-rss-export-as-rss (&optional async subtreep visible-only)
  112. "Export current buffer to a RSS buffer.
  113. If narrowing is active in the current buffer, only export its
  114. narrowed part.
  115. If a region is active, export that region.
  116. A non-nil optional argument ASYNC means the process should happen
  117. asynchronously. The resulting buffer should be accessible
  118. through the `org-export-stack' interface.
  119. When optional argument SUBTREEP is non-nil, export the sub-tree
  120. at point, extracting information from the headline properties
  121. first.
  122. When optional argument VISIBLE-ONLY is non-nil, don't export
  123. contents of hidden elements.
  124. Export is done in a buffer named \"*Org RSS Export*\", which will
  125. be displayed when `org-export-show-temporary-export-buffer' is
  126. non-nil."
  127. (interactive)
  128. (let ((file (buffer-file-name (buffer-base-buffer))))
  129. (org-icalendar-create-uid file 'warn-user)
  130. (org-rss-add-pubdate-property))
  131. (if async
  132. (org-export-async-start
  133. (lambda (output)
  134. (with-current-buffer (get-buffer-create "*Org RSS Export*")
  135. (erase-buffer)
  136. (insert output)
  137. (goto-char (point-min))
  138. (text-mode)
  139. (org-export-add-to-stack (current-buffer) 'rss)))
  140. `(org-export-as 'rss ,subtreep ,visible-only))
  141. (let ((outbuf (org-export-to-buffer
  142. 'rss "*Org RSS Export*" subtreep visible-only)))
  143. (with-current-buffer outbuf (text-mode))
  144. (when org-export-show-temporary-export-buffer
  145. (switch-to-buffer-other-window outbuf)))))
  146. ;;;###autoload
  147. (defun org-rss-export-to-rss (&optional async subtreep visible-only)
  148. "Export current buffer to a RSS file.
  149. If narrowing is active in the current buffer, only export its
  150. narrowed part.
  151. If a region is active, export that region.
  152. A non-nil optional argument ASYNC means the process should happen
  153. asynchronously. The resulting file should be accessible through
  154. the `org-export-stack' interface.
  155. When optional argument SUBTREEP is non-nil, export the sub-tree
  156. at point, extracting information from the headline properties
  157. first.
  158. When optional argument VISIBLE-ONLY is non-nil, don't export
  159. contents of hidden elements.
  160. Return output file's name."
  161. (interactive)
  162. (let ((file (buffer-file-name (buffer-base-buffer))))
  163. (org-icalendar-create-uid file 'warn-user)
  164. (org-rss-add-pubdate-property))
  165. (let ((outfile (org-export-output-file-name
  166. (concat "." org-rss-extension) subtreep)))
  167. (if async
  168. (org-export-async-start
  169. (lambda (f) (org-export-add-to-stack f 'rss))
  170. `(expand-file-name
  171. (org-export-to-file 'rss ,outfile ,subtreep ,visible-only)))
  172. (org-export-to-file 'rss outfile subtreep visible-only))))
  173. ;;;###autoload
  174. (defun org-rss-publish-to-rss (plist filename pub-dir)
  175. "Publish an org file to RSS.
  176. FILENAME is the filename of the Org file to be published. PLIST
  177. is the property list for the given project. PUB-DIR is the
  178. publishing directory.
  179. Return output file name."
  180. (org-publish-org-to
  181. 'rss filename (concat "." org-rss-extension) plist pub-dir))
  182. ;;; Main transcoding functions
  183. (defun org-rss-headline (headline contents info)
  184. "Transcode HEADLINE element into RSS format.
  185. CONTENTS is the headline contents. INFO is a plist used as a
  186. communication channel."
  187. (unless (or (org-element-property :footnote-section-p headline)
  188. ;; Only consider first-level headlines
  189. (> (org-export-get-relative-level headline info) 1))
  190. (let* ((htmlext (plist-get info :html-extension))
  191. (hl-number (org-export-get-headline-number headline info))
  192. (anchor
  193. (org-export-solidify-link-text
  194. (or (org-element-property :CUSTOM_ID headline)
  195. (concat "sec-" (mapconcat 'number-to-string hl-number "-")))))
  196. (category (org-rss-plain-text
  197. (or (org-element-property :CATEGORY headline) "") info))
  198. (pubdate
  199. (let ((system-time-locale "C"))
  200. (format-time-string
  201. "%a, %d %h %Y %H:%M:%S %Z"
  202. (org-time-string-to-time
  203. (or (org-element-property :PUBDATE headline)
  204. (error "Missing PUBDATE property"))))))
  205. (title (org-rss-plain-text
  206. (org-element-property :raw-value headline) info))
  207. (publink
  208. (concat
  209. (file-name-as-directory
  210. (or (plist-get info :html-link-home)
  211. (plist-get info :publishing-directory)))
  212. (file-name-nondirectory
  213. (file-name-sans-extension
  214. (buffer-file-name))) "." htmlext "#" anchor))
  215. (guid (if org-rss-use-entry-url-as-guid
  216. publink
  217. (org-rss-plain-text
  218. (or (org-element-property :ID headline)
  219. (org-element-property :CUSTOM_ID headline)
  220. publink)
  221. info))))
  222. (format
  223. (concat
  224. "<item>\n"
  225. "<title>%s</title>\n"
  226. "<link>%s</link>\n"
  227. "<guid isPermaLink=\"false\">%s</guid>\n"
  228. "<pubDate>%s</pubDate>\n"
  229. (org-rss-build-categories headline info) "\n"
  230. "<description><![CDATA[%s]]></description>\n"
  231. "</item>\n")
  232. title publink guid pubdate contents))))
  233. (defun org-rss-build-categories (headline info)
  234. "Build categories for the RSS item."
  235. (if (eq (plist-get info :rss-categories) 'from-tags)
  236. (mapconcat
  237. (lambda (c) (format "<category><![CDATA[%s]]></category>" c))
  238. (org-element-property :tags headline)
  239. "\n")
  240. (let ((c (org-element-property :CATEGORY headline)))
  241. (format "<category><![CDATA[%s]]></category>" c))))
  242. (defun org-rss-template (contents info)
  243. "Return complete document string after RSS conversion.
  244. CONTENTS is the transcoded contents string. INFO is a plist used
  245. as a communication channel."
  246. (concat
  247. (format "<?xml version=\"1.0\" encoding=\"%s\"?>"
  248. (symbol-name org-html-coding-system))
  249. "\n<rss version=\"2.0\"
  250. xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
  251. xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
  252. xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
  253. xmlns:atom=\"http://www.w3.org/2005/Atom\"
  254. xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
  255. xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
  256. xmlns:georss=\"http://www.georss.org/georss\"
  257. xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
  258. xmlns:media=\"http://search.yahoo.com/mrss/\">"
  259. "<channel>"
  260. (org-rss-build-channel-info info) "\n"
  261. contents
  262. "</channel>\n"
  263. "</rss>"))
  264. (defun org-rss-build-channel-info (info)
  265. "Build the RSS channel information."
  266. (let* ((system-time-locale "C")
  267. (title (org-export-data (plist-get info :title) info))
  268. (email (org-export-data (plist-get info :email) info))
  269. (author (and (plist-get info :with-author)
  270. (let ((auth (plist-get info :author)))
  271. (and auth (org-export-data auth info)))))
  272. (date (format-time-string "%a, %d %h %Y %H:%M:%S %Z")) ;; RFC 882
  273. (description (org-export-data (plist-get info :description) info))
  274. (lang (plist-get info :language))
  275. (keywords (plist-get info :keywords))
  276. (rssext (plist-get info :rss-extension))
  277. (blogurl (or (plist-get info :html-link-home)
  278. (plist-get info :publishing-directory)))
  279. (image (url-encode-url (plist-get info :rss-image-url)))
  280. (publink
  281. (concat (file-name-as-directory blogurl)
  282. (file-name-nondirectory
  283. (file-name-sans-extension (buffer-file-name)))
  284. "." rssext)))
  285. (format
  286. "\n<title>%s</title>
  287. <atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
  288. <link>%s</link>
  289. <description><![CDATA[%s]]></description>
  290. <language>%s</language>
  291. <pubDate>%s</pubDate>
  292. <lastBuildDate>%s</lastBuildDate>
  293. <generator>%s</generator>
  294. <webMaster>%s</webMaster>
  295. <image>
  296. <url>%s</url>
  297. <title>%s</title>
  298. <link>%s</link>
  299. </image>
  300. "
  301. title publink blogurl description lang date date
  302. (concat (format "Emacs %d.%d"
  303. emacs-major-version
  304. emacs-minor-version)
  305. " Org-mode " (org-version))
  306. email image title blogurl)))
  307. (defun org-rss-section (section contents info)
  308. "Transcode SECTION element into RSS format.
  309. CONTENTS is the section contents. INFO is a plist used as
  310. a communication channel."
  311. contents)
  312. (defun org-rss-timestamp (timestamp contents info)
  313. "Transcode a TIMESTAMP object from Org to RSS.
  314. CONTENTS is nil. INFO is a plist holding contextual
  315. information."
  316. (org-html-encode-plain-text
  317. (org-timestamp-translate timestamp)))
  318. (defun org-rss-plain-text (contents info)
  319. "Convert plain text into RSS encoded text."
  320. (let (output)
  321. (setq output (org-html-encode-plain-text contents)
  322. output (org-export-activate-smart-quotes
  323. output :html info))))
  324. ;;; Filters
  325. (defun org-rss-final-function (contents backend info)
  326. "Prettify the RSS output."
  327. (with-temp-buffer
  328. (xml-mode)
  329. (insert contents)
  330. (indent-region (point-min) (point-max))
  331. (buffer-substring-no-properties (point-min) (point-max))))
  332. ;;; Miscellaneous
  333. (defun org-rss-add-pubdate-property ()
  334. "Set the PUBDATE property for top-level headlines."
  335. (let (msg)
  336. (org-map-entries
  337. (lambda ()
  338. (let* ((entry (org-element-at-point))
  339. (level (org-element-property :level entry)))
  340. (when (= level 1)
  341. (unless (org-entry-get (point) "PUBDATE")
  342. (setq msg t)
  343. (org-set-property
  344. "PUBDATE" (format-time-string
  345. (cdr org-time-stamp-formats)))))))
  346. nil nil 'comment 'archive)
  347. (when msg
  348. (message "Property PUBDATE added to top-level entries in %s"
  349. (buffer-file-name))
  350. (sit-for 2))))
  351. (provide 'ox-rss)
  352. ;;; ox-rss.el ends here