1 ;;; atom.el --- Create an Atom feed
3 ;; Copyright (C) 2011 Frédéric Perrin
5 ;; Author: Frédéric Perrin <frederic.perrin@resel.fr>
6 ;; Keywords: www, hypermedia, atom, rss
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
23 ;; This is a library for creating an Atom feed from a Lisp program.
24 ;; The normal usage is to create a feed with `atom-create', giving it
25 ;; a title and a Web address. Once the feed has been created, entries
26 ;; may be added to the feed, by specifying (at the minimum) a title, a
27 ;; permanent link and the content of the entry. Text-only, HTML and
28 ;; XHTML entries are supported.
30 ;; A feed is really a Lisp structure as used by the `xml.el' package,
31 ;; without the parent `feed' element.
33 ;; A typical usage would look like this:
35 ;; (let ((my-atom-feed (atom-create "My feed" "http://example.org")))
36 ;; ; A simple, text-only entry
37 ;; (atom-add-text-entry
40 ;; "http://example.org/hello"
41 ;; "Hello the world!")
43 ;; ; A text-only entry, with all the optional pieces of data
44 ;; (atom-add-text-entry
47 ;; "http://example.org/bonjour"
48 ;; "Bonjour à tout le monde !"
49 ;; ;; optional: the last modification time
50 ;; (date-to-time "2011-01-30 23:40:12")
51 ;; ;; optional: an identifier for this entry; a common way to generate it is
52 ;; ;; to use the domain name and the creation date of the entry.
53 ;; (atom-generate-id "http://example.org"
54 ;; (date-to-time "2011-01-30 10:01:05"))
55 ;; ;; optional: a summary for this entry
58 ;; (atom-add-xhtml-entry
61 ;; "http://example.org/html-example"
62 ;; "<p>One can also use <acronym>XHTML</acronym> in the entries.</p>")
63 ;; (atom-print my-atom-feed))
69 (require 'cl) ; for setf in url-canonalize
71 (defun atom-create (title link &optional subtitle self id author updated)
72 "Create a new atom structure.
74 TITLE is the title for the feed, a short, text-only, human
77 LINK is the URL of a page responible for the content of this
80 SUBTITLE is a subtitle for the feed; it can be a bit longer than
81 TITLE, maybe a paragraph long.
83 SELF is the canonical URL to this feed.
85 ID is a unique identifier for this feed. If not given, it
88 AUTHOR is the author of the feed. See `atom-massage-author' for
89 the possible ways to specify it. In particular, `nil' uses
90 `user-full-name' and `user-mail-address'.
92 UPDATED is the date the feed was last updated. If not given,
93 `(current-time)' is used."
94 (let ((atom-feed (list (list 'title nil title))))
95 (atom-modify-entry atom-feed 'link `(((href . ,link))))
96 (atom-modify-entry atom-feed 'author (atom-massage-author author))
97 (if subtitle (atom-modify-entry atom-feed 'subtitle subtitle))
98 (if self (atom-modify-entry atom-feed 'link
99 `(((href . ,self) (rel . "self")
100 (type . "application/atom+xml")))))
101 (atom-modify-entry atom-feed 'updated (atom-format-time updated))
102 (atom-modify-entry atom-feed 'id (or id self link))
105 (defun atom-push-entry (atom entry)
106 "Add the entry ENTRY to the feed ATOM."
107 (nconc atom (list `(entry nil ,@entry))))
109 (defun atom-modify-entry (entry name val)
110 "Set the NAME element of ENTRY to VAL."
111 (let ((elem (if (stringp val)
114 (nconc entry (list elem))))
116 (defun atom-add-entry (atom title link content
117 &optional updated id summary)
118 "Add an entry to the atom flux ATOM. Return the newly added
121 TITLE is a short, text-only, human readable string.
123 LINK is a permanent link for this entry. For a given entry, LINK
124 may change between successive generations of the atom feed.
126 CONTENT is the content of the entry; use `atom-add-html-entry'
127 or `atom-add-xhtml-entry' when CONTENT is not text-only.
129 If SUMMARY is not given, the entry will not contain any summary.
131 UPDATED defaults to `(current-time)' if omitted, which is
132 probably not a very good default.
134 ID defaults to LINK, which is not optimal; see `atom-generate-id'
135 for a way to create good identifiers. For a given entry, it must
136 not change between successive generations of the atom feed, even
137 when the content of the entry ."
138 (let ((entry (list (list 'title nil title))))
139 (atom-modify-entry entry 'link (list (list (cons 'href link))))
140 (atom-modify-entry entry 'id (or id link))
141 (atom-modify-entry entry 'updated (atom-format-time updated))
142 (if summary (atom-modify-entry entry 'summary summary))
143 (atom-modify-entry entry 'content content)
144 (atom-push-entry atom entry)
147 (defalias 'atom-add-text-entry 'atom-add-entry
148 "Add an entry to ATOM, with a textual content. See
149 `atom-add-entry' for details.")
151 (defun atom-add-html-entry (atom title link content
152 &optional updated id summary)
153 "Add an entry to ATOM, with some HTML content. CONTENT should
154 be a string enconding a valid HTML fragment. See `atom-add-entry'
155 for additional details."
157 title link (atom-massage-html content)
158 updated id (and summary (atom-massage-html summary))))
160 (defun atom-add-xhtml-entry (atom title link content
161 &optional updated id summary noconvert)
162 "Add an entry to ATOM, with some XHTML content. CONTENT may be
163 given either as a string, or as an XML tree, of a valid XHTML
164 fragment. See `atom-add-entry' for additional details.
166 If CONVERT, translate all links in CONTENT so that they are no
167 longer relative to LINK."
168 (let ((xhtml-content (atom-massage-xhtml content)))
170 (atom-xhtml-convert-links (cadr xhtml-content) link))
172 title link xhtml-content
173 updated id (and summary (atom-massage-xhtml summary)))))
175 (defun atom-print (atom)
176 "Print the Atom feed ATOM in the current buffer."
177 (insert "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
178 (insert "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n")
180 (insert "\n</feed>"))
182 (defun atom-write-file (atom filename)
183 "Writes the feed ATOM to FILENAME."
186 (write-region (point-min) (point-max) filename)))
189 (defun atom-to-rss (atom)
190 "Translate an Atom feed into an RSS one, returning the translation.
192 Some information may be lost or approximated."
193 (let ((rss (list (assoc 'title atom))))
194 (atom-to-rss-translator atom rss '((subtitle . description)
197 (atom-to-rss-modify-time rss)
198 (atom-to-rss-modify-link rss)
199 (dolist (entry (xml-get-children atom 'entry))
200 (push (atom-to-rss-item entry) rss))
203 (defun atom-to-rss-item (entry)
204 "Translates an Atom entry into an RSS item."
205 (let ((item (list (assoc 'title entry))))
206 (atom-to-rss-translator
207 (xml-node-children entry) item
208 '((id . guid) (content . description) (updated . pubDate) (link . link)))
209 (atom-to-rss-modify-time item)
210 (atom-to-rss-modify-link item)
211 (let ((guid (assoc 'guid item))
212 (descr (assoc 'description item)))
214 (setcar (cdr guid) (list (cons 'isPermaLink "false"))))
216 (equal (xml-get-attribute descr 'type) "xhtml"))
217 (setcar (cddr descr) (xml-node-text descr))))
220 (defun atom-to-rss-translator (source target translations)
221 (dolist (translation translations)
222 (let* ((from (car translation))
223 (to (cdr translation))
224 (data (copy-tree (cdr (assoc from source)))))
226 (atom-modify-entry target to data)))))
228 (defun xml-node-text (node)
230 (xml-print (xml-node-children node))
233 (defun atom-to-rss-modify-link (entry)
234 (let* ((link (assoc 'link entry))
235 (link-addr (xml-get-attribute-or-nil link 'href)))
237 (setcar (cdr link) nil)
238 (setcdr (cdr link) (cons link-addr nil)))))
240 (defun atom-print-as-rss (atom)
241 (let ((rss (atom-to-rss atom)))
242 (insert "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
243 (insert "<rss version=\"2.0\">\n")
244 (insert " <channel>\n")
246 (insert "\n </channel>\n")
249 (defun atom-to-rss-time (time)
250 "Translates a string from the format used by Atom into the
252 ;; Same remark as in `atom-format-time'
253 (let ((system-time-locale "C"))
254 (format-time-string "%a, %d %b %Y %T %z" (atom-parse-time time))))
256 (defun atom-to-rss-modify-time (entry)
257 "Modify ENTRY, changing the format of the `pubDate' in it."
258 (let ((pubDate (assoc 'pubDate entry)))
259 (setcar (cddr pubDate)
260 (atom-to-rss-time (car (xml-node-children pubDate))))))
262 (defun atom-to-rss-write-file (atom filename)
263 "Saves ATOM as a RSS feed into FILENAME."
265 (atom-print-as-rss atom)
266 (write-region nil nil filename)))
269 (defvar atom-time-format-string "%Y-%m-%dT%T%z"
270 "The format for string representation of dates.")
272 (defun atom-format-time (&optional time)
273 "Format a time according to RFC3339."
274 ;; The time zone must be specified in numeric form, but with a colon between
275 ;; the hour and minute parts.
276 (replace-regexp-in-string
278 (format-time-string atom-time-format-string time)))
280 (defun atom-parse-time (&optional time)
281 "Parse a time as specified in RFC3339 into Emacs's native format."
282 (date-to-time (replace-regexp-in-string ":\\(..\\)$" "\\1" time)))
284 (defun atom-massage-html (content)
285 "Massage CONTENT so it can be used as an HTML fragment in an
286 Atom feed. CONTENT must be a string."
287 (list '((type . "html")) content))
289 (defun atom-string-to-xml (string)
290 "Convert STRING into a Lisp structure as used by `xml.el'."
292 (insert "<div xmlns=\"http://www.w3.org/1999/xhtml\">")
295 (xml-parse-region (point-min) (point-max))))
297 (defun atom-massage-xhtml (content)
298 "Massage CONTENT so it can be used as an XHTML fragment in an
301 ,@(or (and (stringp content)
302 (atom-string-to-xml content))
305 (defun atom-massage-author (author)
306 "Return an XML node representing the author. AUTHOR can be:
307 - nil, in which case `user-full-name' and `user-mail-address' are
309 - a single string, the full name of the author;
310 - a list with two elements, the full name and the email address
312 - something else, assumed to be a complete `atomPersonConstruct'."
314 ((null author) `((name nil ,user-full-name)
315 (email nil ,user-mail-address)))
316 ((stringp author) `((name nil ,author)))
317 ((= 2 (length author)) `((name nil ,(car author))
318 (email nil ,(cadr author))))
319 (t `(author nil ,author)))))
321 (defun atom-xhtml-convert-links (node base)
322 "Make all links in NODE (a fragment of an XHTML document)
323 absolute, in the context of BASE, an URL."
324 (dolist (attr-name (list 'href 'src))
325 (let ((attr (assoc attr-name (xml-node-attributes node))))
326 (when attr (setcdr attr (url-canonalize (cdr attr) base)))))
327 (dolist (child (xml-node-children node))
328 (when (listp child) (atom-xhtml-convert-links child base))))
330 (defun url-canonalize (address base)
331 "Make ADRESS an absolute URL, taking it in the BASE context."
332 ;; I feel such a function should exist in `url-parse'. Did I miss it?
333 (let ((url-base (url-generic-parse-url base))
334 (url-address (url-generic-parse-url address)))
335 (if (url-host url-address)
337 (setf (url-filename url-base)
338 (expand-file-name address
339 (file-name-directory (url-filename url-base))))
340 (url-recreate-url url-base))))
342 (defun atom-generate-id (link creation-date)
343 "Generate a string suitable for use as an atom:id element. This
344 implements Mark Pilgrom's tag: URI method, using the
345 CREATION-DATE of the entry, and the domain part of LINK."
346 (format "tag:%s,%s:/%s"
347 (url-host (url-generic-parse-url link))
348 (format-time-string "%Y-%m-%d" creation-date)
349 (format-time-string "%Y%m%d%H%M%S" creation-date)))
352 ;;; atom.el ends here