|
@@ -0,0 +1,394 @@
|
|
|
+;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
|
|
|
+
|
|
|
+;; Copyright (C) 2013 Bastien Guerry
|
|
|
+
|
|
|
+;; Author: Bastien Guerry <bzg at gnu dot org>
|
|
|
+;; Keywords: org, wp, blog, feed, rss
|
|
|
+
|
|
|
+;; This file is not yet part of GNU Emacs.
|
|
|
+
|
|
|
+;; This program is free software: you can redistribute it and/or modify
|
|
|
+;; it under the terms of the GNU General Public License as published by
|
|
|
+;; the Free Software Foundation, either version 3 of the License, or
|
|
|
+;; (at your option) any later version.
|
|
|
+
|
|
|
+;; This program is distributed in the hope that it will be useful,
|
|
|
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+;; GNU General Public License for more details.
|
|
|
+
|
|
|
+;; You should have received a copy of the GNU General Public License
|
|
|
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+
|
|
|
+;;; Commentary:
|
|
|
+
|
|
|
+;; This library implements a RSS 2.0 back-end for Org exporter, based on
|
|
|
+;; the `html' back-end.
|
|
|
+;;
|
|
|
+;; It provides two commands for export, depending on the desired output:
|
|
|
+;; `org-rss-export-as-rss' (temporary buffer) and `org-rss-export-to-rss'
|
|
|
+;; (as a ".xml" file).
|
|
|
+;;
|
|
|
+;; This backend understands two new option keywords:
|
|
|
+;;
|
|
|
+;; #+RSS_EXTENSION: xml
|
|
|
+;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
|
|
|
+;;
|
|
|
+;; It uses #+HTML_LINK_HOME: to set the base url of the feed.
|
|
|
+;;
|
|
|
+;; Exporting an Org file to RSS modifies each top-level entry by adding a
|
|
|
+;; PUBDATE property. If `org-rss-use-entry-url-as-guid', it will also add
|
|
|
+;; an ID property, later used as the guid for the feed's item.
|
|
|
+;;
|
|
|
+;; You typically want to use it within a publishing project like this:
|
|
|
+;;
|
|
|
+;; (add-to-list
|
|
|
+;; 'org-publish-project-alist
|
|
|
+;; '("homepage_rss"
|
|
|
+;; :base-directory "~/myhomepage/"
|
|
|
+;; :base-extension "org"
|
|
|
+;; :rss-image-url "http://lumiere.ens.fr/~guerry/images/faces/15.png"
|
|
|
+;; :home-link-home "http://lumiere.ens.fr/~guerry/"
|
|
|
+;; :rss-extension "xml"
|
|
|
+;; :publishing-directory "/home/guerry/public_html/"
|
|
|
+;; :publishing-function (org-rss-publish-to-rss)
|
|
|
+;; :section-numbers nil
|
|
|
+;; :exclude ".*" ;; To exclude all files...
|
|
|
+;; :include ("index.org") ;; ... except index.org.
|
|
|
+;; :table-of-contents nil))
|
|
|
+;;
|
|
|
+;; ... then rsync /home/guerry/public_html/ with your server.
|
|
|
+
|
|
|
+;;; Code:
|
|
|
+
|
|
|
+(require 'ox-html)
|
|
|
+(declare-function url-encode-url "url-util" (url))
|
|
|
+
|
|
|
+;;; Variables and options
|
|
|
+
|
|
|
+(defgroup org-export-rss nil
|
|
|
+ "Options specific to RSS export back-end."
|
|
|
+ :tag "Org RSS"
|
|
|
+ :group 'org-export
|
|
|
+ :version "24.4"
|
|
|
+ :package-version '(Org . "8.0"))
|
|
|
+
|
|
|
+(defcustom org-rss-image-url ""
|
|
|
+ "The URL of the an image for the RSS feed."
|
|
|
+ :group 'org-export-rss
|
|
|
+ :type 'string)
|
|
|
+
|
|
|
+(defcustom org-rss-extension "xml"
|
|
|
+ "File extension for the RSS 2.0 feed."
|
|
|
+ :group 'org-export-rss
|
|
|
+ :type 'string)
|
|
|
+
|
|
|
+(defcustom org-rss-categories 'from-tags
|
|
|
+ "Where to extract items category information from.
|
|
|
+The default is to extract categories from the tags of the
|
|
|
+headlines. When set to another value, extract the category
|
|
|
+from the :CATEGORY: property of the entry."
|
|
|
+ :group 'org-export-rss
|
|
|
+ :type '(choice
|
|
|
+ (const :tag "From tags" from-tags)
|
|
|
+ (const :tag "From the category property" from-category)))
|
|
|
+
|
|
|
+(defcustom org-rss-use-entry-url-as-guid t
|
|
|
+ "Use the URL for the <guid> metatag?
|
|
|
+When nil, Org will create ids using `org-icalendar-create-uid'."
|
|
|
+ :group 'org-export-rss
|
|
|
+ :type 'boolean)
|
|
|
+
|
|
|
+;;; Define backend
|
|
|
+
|
|
|
+(org-export-define-derived-backend rss html
|
|
|
+ :menu-entry
|
|
|
+ (?r "Export to RSS"
|
|
|
+ ((?R "To temporary buffer"
|
|
|
+ (lambda (a s v b) (org-rss-export-as-rss a s v)))
|
|
|
+ (?r "To file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
|
|
|
+ (?o "To file and open"
|
|
|
+ (lambda (a s v b)
|
|
|
+ (if a (org-rss-export-to-rss t s v)
|
|
|
+ (org-open-file (org-rss-export-to-rss nil s v)))))))
|
|
|
+ :options-alist
|
|
|
+ ((:with-toc nil nil nil) ;; Never include HTML's toc
|
|
|
+ (:rss-extension "RSS_EXTENSION" nil org-rss-extension)
|
|
|
+ (:rss-image-url "RSS_IMAGE_URL" nil org-rss-image-url)
|
|
|
+ (:rss-categories nil nil org-rss-categories))
|
|
|
+ :filters-alist ((:filter-final-output . org-rss-final-function))
|
|
|
+ :translate-alist ((headline . org-rss-headline)
|
|
|
+ (comment . (lambda (&rest args) ""))
|
|
|
+ (comment-block . (lambda (&rest args) ""))
|
|
|
+ (timestamp . (lambda (&rest args) ""))
|
|
|
+ (plain-text . org-rss-plain-text)
|
|
|
+ (section . org-rss-section)
|
|
|
+ (template . org-rss-template)))
|
|
|
+
|
|
|
+;;; Export functions
|
|
|
+
|
|
|
+;;;###autoload
|
|
|
+(defun org-rss-export-as-rss (&optional async subtreep visible-only)
|
|
|
+ "Export current buffer to a RSS buffer.
|
|
|
+
|
|
|
+If narrowing is active in the current buffer, only export its
|
|
|
+narrowed part.
|
|
|
+
|
|
|
+If a region is active, export that region.
|
|
|
+
|
|
|
+A non-nil optional argument ASYNC means the process should happen
|
|
|
+asynchronously. The resulting buffer should be accessible
|
|
|
+through the `org-export-stack' interface.
|
|
|
+
|
|
|
+When optional argument SUBTREEP is non-nil, export the sub-tree
|
|
|
+at point, extracting information from the headline properties
|
|
|
+first.
|
|
|
+
|
|
|
+When optional argument VISIBLE-ONLY is non-nil, don't export
|
|
|
+contents of hidden elements.
|
|
|
+
|
|
|
+Export is done in a buffer named \"*Org RSS Export*\", which will
|
|
|
+be displayed when `org-export-show-temporary-export-buffer' is
|
|
|
+non-nil."
|
|
|
+ (interactive)
|
|
|
+ (let ((file (buffer-file-name (buffer-base-buffer))))
|
|
|
+ (org-icalendar-create-uid file 'warn-user)
|
|
|
+ (org-rss-add-pubdate-property))
|
|
|
+ (if async
|
|
|
+ (org-export-async-start
|
|
|
+ (lambda (output)
|
|
|
+ (with-current-buffer (get-buffer-create "*Org RSS Export*")
|
|
|
+ (erase-buffer)
|
|
|
+ (insert output)
|
|
|
+ (goto-char (point-min))
|
|
|
+ (text-mode)
|
|
|
+ (org-export-add-to-stack (current-buffer) 'rss)))
|
|
|
+ `(org-export-as 'rss ,subtreep ,visible-only))
|
|
|
+ (let ((outbuf (org-export-to-buffer
|
|
|
+ 'rss "*Org RSS Export*" subtreep visible-only)))
|
|
|
+ (with-current-buffer outbuf (text-mode))
|
|
|
+ (when org-export-show-temporary-export-buffer
|
|
|
+ (switch-to-buffer-other-window outbuf)))))
|
|
|
+
|
|
|
+;;;###autoload
|
|
|
+(defun org-rss-export-to-rss (&optional async subtreep visible-only)
|
|
|
+ "Export current buffer to a RSS file.
|
|
|
+
|
|
|
+If narrowing is active in the current buffer, only export its
|
|
|
+narrowed part.
|
|
|
+
|
|
|
+If a region is active, export that region.
|
|
|
+
|
|
|
+A non-nil optional argument ASYNC means the process should happen
|
|
|
+asynchronously. The resulting file should be accessible through
|
|
|
+the `org-export-stack' interface.
|
|
|
+
|
|
|
+When optional argument SUBTREEP is non-nil, export the sub-tree
|
|
|
+at point, extracting information from the headline properties
|
|
|
+first.
|
|
|
+
|
|
|
+When optional argument VISIBLE-ONLY is non-nil, don't export
|
|
|
+contents of hidden elements.
|
|
|
+
|
|
|
+Return output file's name."
|
|
|
+ (interactive)
|
|
|
+ (let ((file (buffer-file-name (buffer-base-buffer))))
|
|
|
+ (org-icalendar-create-uid file 'warn-user)
|
|
|
+ (org-rss-add-pubdate-property))
|
|
|
+ (let ((outfile (org-export-output-file-name
|
|
|
+ (concat "." org-rss-extension) subtreep)))
|
|
|
+ (if async
|
|
|
+ (org-export-async-start
|
|
|
+ (lambda (f) (org-export-add-to-stack f 'rss))
|
|
|
+ `(expand-file-name
|
|
|
+ (org-export-to-file 'rss ,outfile ,subtreep ,visible-only)))
|
|
|
+ (org-export-to-file 'rss outfile subtreep visible-only))))
|
|
|
+
|
|
|
+;;;###autoload
|
|
|
+(defun org-rss-publish-to-rss (plist filename pub-dir)
|
|
|
+ "Publish an org file to RSS.
|
|
|
+
|
|
|
+FILENAME is the filename of the Org file to be published. PLIST
|
|
|
+is the property list for the given project. PUB-DIR is the
|
|
|
+publishing directory.
|
|
|
+
|
|
|
+Return output file name."
|
|
|
+ (org-publish-org-to 'rss filename ".xml" plist pub-dir))
|
|
|
+
|
|
|
+;;; Main transcoding functions
|
|
|
+
|
|
|
+(defun org-rss-headline (headline contents info)
|
|
|
+ "Transcode HEADLINE element into RSS format.
|
|
|
+CONTENTS is the headline contents. INFO is a plist used as a
|
|
|
+communication channel."
|
|
|
+ (unless (or (org-element-property :footnote-section-p headline)
|
|
|
+ ;; Only consider first-level headlines
|
|
|
+ (> (org-export-get-relative-level headline info) 1))
|
|
|
+ (let* ((htmlext (plist-get info :html-extension))
|
|
|
+ (hl-number (org-export-get-headline-number headline info))
|
|
|
+ (anchor
|
|
|
+ (org-export-solidify-link-text
|
|
|
+ (or (org-element-property :CUSTOM_ID headline)
|
|
|
+ (concat "sec-" (mapconcat 'number-to-string hl-number "-")))))
|
|
|
+ (category (org-rss-plain-text
|
|
|
+ (or (org-element-property :CATEGORY headline) "") info))
|
|
|
+ (pubdate (org-element-property :PUBDATE headline))
|
|
|
+ (title (org-rss-plain-text
|
|
|
+ (org-element-property :raw-value headline) info))
|
|
|
+ (publink
|
|
|
+ (concat
|
|
|
+ (file-name-as-directory
|
|
|
+ (or (plist-get info :html-link-home)
|
|
|
+ (plist-get info :publishing-directory)))
|
|
|
+ (file-name-nondirectory
|
|
|
+ (file-name-sans-extension
|
|
|
+ (buffer-file-name))) "." htmlext "#" anchor))
|
|
|
+ (guid (if org-rss-use-entry-url-as-guid
|
|
|
+ publink
|
|
|
+ (org-rss-plain-text
|
|
|
+ (or (org-element-property :ID headline)
|
|
|
+ (org-element-property :CUSTOM_ID headline)
|
|
|
+ publink)
|
|
|
+ info))))
|
|
|
+ (format
|
|
|
+ (concat
|
|
|
+ "<item>\n"
|
|
|
+ "<title>%s</title>\n"
|
|
|
+ "<link>%s</link>\n"
|
|
|
+ "<guid isPermaLink=\"false\">%s</guid>\n"
|
|
|
+ "<pubDate>%s</pubDate>\n"
|
|
|
+ (org-rss-build-categories headline info) "\n"
|
|
|
+ "<description><![CDATA[%s]]></description>\n"
|
|
|
+ "</item>\n")
|
|
|
+ title publink guid pubdate contents))))
|
|
|
+
|
|
|
+(defun org-rss-build-categories (headline info)
|
|
|
+ "Build categories for the RSS item."
|
|
|
+ (if (eq (plist-get info :rss-categories) 'from-tags)
|
|
|
+ (mapconcat
|
|
|
+ (lambda (c) (format "<category><![CDATA[%s]]></category>" c))
|
|
|
+ (org-element-property :tags headline)
|
|
|
+ "\n")
|
|
|
+ (let ((c (org-element-property :CATEGORY headline)))
|
|
|
+ (format "<category><![CDATA[%s]]></category>" c))))
|
|
|
+
|
|
|
+(defun org-rss-template (contents info)
|
|
|
+ "Return complete document string after RSS conversion.
|
|
|
+CONTENTS is the transcoded contents string. INFO is a plist used
|
|
|
+as a communication channel."
|
|
|
+ (concat
|
|
|
+ (format "<?xml version=\"1.0\" encoding=\"%s\"?>"
|
|
|
+ (symbol-name org-html-coding-system))
|
|
|
+ "\n<rss version=\"2.0\"
|
|
|
+ xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"
|
|
|
+ xmlns:wfw=\"http://wellformedweb.org/CommentAPI/\"
|
|
|
+ xmlns:dc=\"http://purl.org/dc/elements/1.1/\"
|
|
|
+ xmlns:atom=\"http://www.w3.org/2005/Atom\"
|
|
|
+ xmlns:sy=\"http://purl.org/rss/1.0/modules/syndication/\"
|
|
|
+ xmlns:slash=\"http://purl.org/rss/1.0/modules/slash/\"
|
|
|
+ xmlns:georss=\"http://www.georss.org/georss\"
|
|
|
+ xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"
|
|
|
+ xmlns:media=\"http://search.yahoo.com/mrss/\">"
|
|
|
+ "<channel>"
|
|
|
+ (org-rss-build-channel-info info) "\n"
|
|
|
+ contents
|
|
|
+ "</channel>\n"
|
|
|
+ "</rss>"))
|
|
|
+
|
|
|
+(defun org-rss-build-channel-info (info)
|
|
|
+ "Build the RSS channel information."
|
|
|
+ (let* ((system-time-locale "C")
|
|
|
+ (title (org-export-data (plist-get info :title) info))
|
|
|
+ (email (org-export-data (plist-get info :email) info))
|
|
|
+ (author (and (plist-get info :with-author)
|
|
|
+ (let ((auth (plist-get info :author)))
|
|
|
+ (and auth (org-export-data auth info)))))
|
|
|
+ (date (format-time-string "%a, %d %h %Y %H:%M:%S %Z")) ;; RFC 882
|
|
|
+ (description (org-export-data (plist-get info :description) info))
|
|
|
+ (lang (plist-get info :language))
|
|
|
+ (keywords (plist-get info :keywords))
|
|
|
+ (rssext (plist-get info :rss-extension))
|
|
|
+ (blogurl (or (plist-get info :html-link-home)
|
|
|
+ (plist-get info :publishing-directory)))
|
|
|
+ (image (url-encode-url (plist-get info :rss-image-url)))
|
|
|
+ (publink
|
|
|
+ (concat (file-name-as-directory blogurl)
|
|
|
+ (file-name-nondirectory
|
|
|
+ (file-name-sans-extension (buffer-file-name)))
|
|
|
+ "." rssext)))
|
|
|
+ (format
|
|
|
+ "\n<title>%s</title>
|
|
|
+<atom:link href=\"%s\" rel=\"self\" type=\"application/rss+xml\" />
|
|
|
+<link>%s</link>
|
|
|
+<description><![CDATA[%s]]></description>
|
|
|
+<language>%s</language>
|
|
|
+<pubDate>%s</pubDate>
|
|
|
+<lastBuildDate>%s</lastBuildDate>
|
|
|
+<generator>%s</generator>
|
|
|
+<webMaster>%s</webMaster>
|
|
|
+<image>
|
|
|
+<url>%s</url>
|
|
|
+<title>%s</title>
|
|
|
+<link>%s</link>
|
|
|
+</image>
|
|
|
+"
|
|
|
+ title publink blogurl description lang date date
|
|
|
+ (concat (format "Emacs %d.%d"
|
|
|
+ emacs-major-version
|
|
|
+ emacs-minor-version)
|
|
|
+ " Org-mode " (org-version))
|
|
|
+ email image title blogurl)))
|
|
|
+
|
|
|
+(defun org-rss-section (section contents info)
|
|
|
+ "Transcode SECTION element into RSS format.
|
|
|
+CONTENTS is the section contents. INFO is a plist used as
|
|
|
+a communication channel."
|
|
|
+ contents)
|
|
|
+
|
|
|
+(defun org-rss-timestamp (timestamp contents info)
|
|
|
+ "Transcode a TIMESTAMP object from Org to RSS.
|
|
|
+CONTENTS is nil. INFO is a plist holding contextual
|
|
|
+information."
|
|
|
+ (org-html-encode-plain-text
|
|
|
+ (org-timestamp-translate timestamp)))
|
|
|
+
|
|
|
+(defun org-rss-plain-text (contents info)
|
|
|
+ "Convert plain text into RSS encoded text."
|
|
|
+ (let (output)
|
|
|
+ (setq output (org-html-encode-plain-text contents)
|
|
|
+ output (org-export-activate-smart-quotes
|
|
|
+ output :html info))))
|
|
|
+
|
|
|
+;;; Filters
|
|
|
+
|
|
|
+(defun org-rss-final-function (contents backend info)
|
|
|
+ "Prettify the RSS output."
|
|
|
+ (with-temp-buffer
|
|
|
+ (xml-mode)
|
|
|
+ (insert contents)
|
|
|
+ (indent-region (point-min) (point-max))
|
|
|
+ (buffer-substring-no-properties (point-min) (point-max))))
|
|
|
+
|
|
|
+;;; Miscellaneous
|
|
|
+
|
|
|
+(defun org-rss-add-pubdate-property ()
|
|
|
+ "Set the PUBDATE property for top-level headlines."
|
|
|
+ (let (msg)
|
|
|
+ (org-map-entries
|
|
|
+ (lambda ()
|
|
|
+ (let* ((entry (org-element-at-point))
|
|
|
+ (level (org-element-property :level entry)))
|
|
|
+ (when (= level 1)
|
|
|
+ (unless (org-entry-get (point) "PUBDATE")
|
|
|
+ (setq msg t)
|
|
|
+ (org-set-property
|
|
|
+ "PUBDATE"
|
|
|
+ (let ((system-time-locale "C"))
|
|
|
+ (format-time-string "%a, %d %h %Y %H:%M:%S %Z")))))))
|
|
|
+ nil nil 'comment 'archive)
|
|
|
+ (when msg
|
|
|
+ (message "Property PUBDATE added to top-level entries in %s"
|
|
|
+ (buffer-file-name))
|
|
|
+ (sit-for 2))))
|
|
|
+
|
|
|
+;;; ox-rss.el ends here
|