123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- ;;; ox-rss.el --- RSS 2.0 Back-End for Org Export Engine
- ;; Copyright (C) 2013-2015 Bastien Guerry
- ;; Author: Bastien Guerry <bzg@gnu.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 requires Emacs 24.1 at least.
- ;;
- ;; 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 three new option keywords:
- ;;
- ;; #+RSS_EXTENSION: xml
- ;; #+RSS_IMAGE_URL: http://myblog.org/mypicture.jpg
- ;; #+RSS_FEED_URL: http://myblog.org/feeds/blog.xml
- ;;
- ;; 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.
- ;;
- ;; The top-level headline is used as the title of each RSS item unless
- ;; an RSS_TITLE property is set on the headline.
- ;;
- ;; 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"
- ;; :html-link-home "http://lumiere.ens.fr/~guerry/"
- ;; :html-link-use-abs-url t
- ;; :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.
- ;;
- ;; By default, the permalink for a blog entry points to the headline.
- ;; You can specify a different one by using the :RSS_PERMALINK:
- ;; property within an entry.
- ;;; 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 "http://orgmode.org/img/org-mode-unicorn-logo.png"
- "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 "As RSS buffer"
- (lambda (a s v b) (org-rss-export-as-rss a s v)))
- (?r "As RSS file" (lambda (a s v b) (org-rss-export-to-rss a s v)))
- (?o "As RSS 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
- '((:description "DESCRIPTION" nil nil newline)
- (:keywords "KEYWORDS" nil nil space)
- (: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-feed-url "RSS_FEED_URL" nil nil t)
- (: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))
- (org-export-to-buffer 'rss "*Org RSS Export*"
- async subtreep visible-only nil nil (lambda () (text-mode))))
- ;;;###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)))
- (org-export-to-file 'rss outfile async 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."
- (let ((bf (get-file-buffer filename)))
- (if bf
- (with-current-buffer bf
- (org-icalendar-create-uid filename 'warn-user)
- (org-rss-add-pubdate-property)
- (write-file filename))
- (find-file filename)
- (org-icalendar-create-uid filename 'warn-user)
- (org-rss-add-pubdate-property)
- (write-file filename) (kill-buffer)))
- (org-publish-org-to
- 'rss filename (concat "." org-rss-extension) 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."
- (if (> (org-export-get-relative-level headline info) 1)
- (org-export-data-with-backend headline 'html info)
- (unless (org-element-property :footnote-section-p headline)
- (let* ((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)))))
- (htmlext (plist-get info :html-extension))
- (hl-number (org-export-get-headline-number headline info))
- (hl-home (file-name-as-directory (plist-get info :html-link-home)))
- (hl-pdir (plist-get info :publishing-directory))
- (hl-perm (org-element-property :RSS_PERMALINK headline))
- (anchor (org-export-get-reference headline info))
- (category (org-rss-plain-text
- (or (org-element-property :CATEGORY headline) "") info))
- (pubdate0 (org-element-property :PUBDATE headline))
- (pubdate (let ((system-time-locale "C"))
- (if pubdate0
- (format-time-string
- "%a, %d %b %Y %H:%M:%S %z"
- (org-time-string-to-time pubdate0)))))
- (title (org-rss-plain-text
- (or (org-element-property :RSS_TITLE headline)
- (replace-regexp-in-string
- org-bracket-link-regexp
- (lambda (m) (or (match-string 3 m)
- (match-string 1 m)))
- (org-element-property :raw-value headline))) info))
- (publink
- (or (and hl-perm (concat (or hl-home hl-pdir) hl-perm))
- (concat
- (or hl-home hl-pdir)
- (file-name-nondirectory
- (file-name-sans-extension
- (plist-get info :input-file))) "." 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))))
- (if (not pubdate0) "" ;; Skip entries with no PUBDATE prop
- (format
- (concat
- "<item>\n"
- "<title>%s</title>\n"
- "<link>%s</link>\n"
- "<author>%s (%s)</author>\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 email author 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 %b %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)))
- (ifile (plist-get info :input-file))
- (publink
- (or (plist-get info :rss-feed-url)
- (concat (file-name-as-directory blogurl)
- (file-name-nondirectory
- (file-name-sans-extension ifile))
- "." 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 (%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 author 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" (format-time-string
- (cdr org-time-stamp-formats)))))))
- nil nil 'comment 'archive)
- (when msg
- (message "Property PUBDATE added to top-level entries in %s"
- (buffer-file-name))
- (sit-for 2))))
- (provide 'ox-rss)
- ;;; ox-rss.el ends here
|