ox-rss.el 14 KB

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