]> gitweb.fperrin.net Git - atom.el.git/blob - atom.el
Add published propety
[atom.el.git] / atom.el
1 ;;; atom.el --- Create an Atom feed  -*- lexical-binding: t -*-
2
3 ;; Copyright (C) 2011  Frédéric Perrin
4
5 ;; Author: Frédéric Perrin <frederic.perrin@resel.fr>
6 ;; Keywords: www, hypermedia, atom, rss
7
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.
12
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.
17
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/>.
20
21 ;;; Commentary:
22
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.
29
30 ;; It is possible to produce both Atom and RSS feeds.
31
32 ;; A typical usage would look like this:
33
34 ;; (let ((my-atom-feed (atom-create "My feed" "http://example.org")))
35 ;;   ; A simple, text-only entry
36 ;;   (atom-add-text-entry
37 ;;    my-atom-feed
38 ;;    "Hello world"
39 ;;    "http://example.org/hello"
40 ;;    "Hello the world!")
41 ;;
42 ;;   (atom-add-xhtml-entry
43 ;;    my-atom-feed
44 ;;    "An XHTML example"
45 ;;    "http://example.org/html-example"
46 ;;    "<p>One can also use <acronym>XHTML</acronym> in the entries.</p>")
47 ;;
48 ;;   (atom-print my-atom-feed)
49 ;;   ;; If you prefer RSS feeds:
50 ;;   (atom-print-as-rss my-atom-feed))
51
52 ;; Full documentation is available at <http://tar-jx.bz/code/atom.html>.
53
54 ;;; Code:
55
56 (require 'xml)
57 (require 'url-parse)
58
59 (defun atom-create (title link &optional subtitle self id author updated)
60   "Create a new atom structure.
61
62 TITLE is the title for the feed, a short, text-only, human
63 readable string.
64
65 LINK is the URL of a page responible for the content of this
66 feed.
67
68 SUBTITLE is a subtitle for the feed; it can be a bit longer than
69 TITLE, maybe a paragraph long.
70
71 SELF is the canonical URL to this feed.
72
73 ID is a unique identifier for this feed. If not given, it
74 defaults to SELF.
75
76 AUTHOR is the author of the feed. See `atom-massage-author' for
77 the possible ways to specify it. In particular, `nil' uses
78 `user-full-name' and `user-mail-address'.
79
80 UPDATED is the date the feed was last updated. If not given,
81 `(current-time)' is used."
82   (let ((atom-feed (list (list 'title nil title))))
83     (atom-modify-entry atom-feed 'link `(((href . ,link))))
84     (atom-modify-entry atom-feed 'author (atom-massage-author author))
85     (if subtitle (atom-modify-entry atom-feed 'subtitle subtitle))
86     (if self (atom-modify-entry atom-feed 'link
87                                 `(((href . ,self) (rel . "self")
88                                    (type . "application/atom+xml")))))
89     (atom-modify-entry atom-feed 'updated (atom-format-time updated))
90     (atom-modify-entry atom-feed 'id (or id self link))
91     atom-feed))
92
93 (defun atom-push-entry (atom entry)
94   "Add the entry ENTRY to the feed ATOM."
95   (nconc atom (list `(entry nil ,@entry))))
96
97 (defun atom-modify-entry (entry name val)
98   "Set the NAME element of ENTRY to VAL."
99   (let ((elem (if (stringp val)
100                   (list name nil val)
101                 (cons name val))))
102     (nconc entry (list elem))))
103
104 (defun atom-add-entry (atom title link content
105                             &optional updated id summary)
106   "Add an entry to the atom flux ATOM. Return the newly added
107 entry.
108
109 TITLE is a short, text-only, human readable string.
110
111 LINK is a permanent link for this entry. For a given entry, LINK
112 may change between successive generations of the atom feed.
113
114 CONTENT is the content of the entry; use `atom-add-html-entry'
115 or `atom-add-xhtml-entry' when CONTENT is not text-only.
116
117 If SUMMARY is not given, the entry will not contain any summary.
118
119 UPDATED defaults to `(current-time)' if omitted, which is
120 probably not a very good default.
121
122 ID defaults to LINK, which is not optimal; see `atom-generate-id'
123 for a way to create good identifiers. For a given entry, it must
124 not change between successive generations of the atom feed, even
125 when the content of the entry changes."
126   (let ((entry (list (list 'title nil title))))
127     (atom-modify-entry entry 'link  (list (list (cons 'href link))))
128     (atom-modify-entry entry 'id (or id link))
129     (atom-modify-entry entry 'updated (atom-format-time updated))
130     (if summary (atom-modify-entry entry 'summary summary))
131     (atom-modify-entry entry 'content content)
132     (atom-push-entry atom entry)
133     entry))
134
135 (defalias 'atom-add-text-entry 'atom-add-entry
136   "Add an entry to ATOM, with a textual content. See
137 `atom-add-entry' for details.")
138
139 (defun atom-add-html-entry (atom title link content
140                                   &optional updated id summary)
141   "Add an entry to ATOM, with some HTML content. CONTENT should
142 be a string enconding a valid HTML fragment. See `atom-add-entry'
143 for additional details."
144   (atom-add-entry atom
145    title link (atom-massage-html content)
146    updated id (and summary (atom-massage-html summary))))
147
148 (defun atom-add-xhtml-entry (atom title link content
149                                   &optional updated id summary noconvert)
150   "Add an entry to ATOM, with some XHTML content. CONTENT may be
151 given either as a string, or as an XML tree, of a valid XHTML
152 fragment. See `atom-add-entry' for additional details.
153
154 If NOCONVERT is nil, translate all links in CONTENT so that they
155 are no longer relative to LINK."
156   (let ((xhtml-content (atom-massage-xhtml content)))
157     (unless noconvert
158       (atom-xhtml-convert-links (cadr xhtml-content) link))
159     (atom-add-entry atom
160                     title link xhtml-content
161                     updated id (and summary (atom-massage-xhtml summary)))))
162
163 (defun atom-print (atom)
164   "Print the Atom feed ATOM in the current buffer."
165   (insert atom-xml-declaration)
166   (insert "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n")
167   (xml-print atom)
168   (insert "\n</feed>"))
169
170 (defun atom-write-file (atom filename)
171   "Writes the feed ATOM to FILENAME."
172   (with-temp-buffer
173     (atom-print atom)
174     (write-file filename)))
175
176 \f
177 (defun atom-to-rss (atom)
178   "Translate an Atom feed into an RSS one, returning the translation.
179
180 Some information may be lost or approximated."
181   (let ((rss (list (assoc 'title atom))))
182     (atom-to-rss-translator atom rss '((subtitle . description)
183                                        (updated . pubDate)
184                                        (link . link)))
185     (atom-to-rss-modify-time rss)
186     (atom-to-rss-modify-link rss)
187     (dolist (entry (xml-get-children atom 'entry))
188       (push (atom-to-rss-item entry) rss))
189     (reverse rss)))
190
191 (defun atom-to-rss-item (entry)
192   "Translates an Atom entry into an RSS item."
193   (let ((item (list (assoc 'title entry))))
194     (atom-to-rss-translator
195      (xml-node-children entry) item
196      '((id . guid) (content . description) (updated . pubDate) (link . link)))
197     (atom-to-rss-modify-time item)
198     (atom-to-rss-modify-link item)
199     (let ((guid (assoc 'guid item))
200           (descr (assoc 'description item)))
201       (if guid
202           (setcar (cdr guid) (list (cons 'isPermaLink "false"))))
203       (if (and descr
204                (equal (xml-get-attribute descr 'type) "xhtml"))
205           (setcar (cddr descr) (xml-node-as-text descr))))
206     `(item nil ,@item)))
207
208 (defun atom-to-rss-translator (source target translations)
209   (dolist (translation translations)
210     (let* ((from (car translation))
211            (to (cdr translation))
212            (data (copy-tree (cdr (assoc from source)))))
213       (when data
214         (atom-modify-entry target to data)))))
215
216 (defun atom-to-rss-modify-link (entry)
217   (let* ((link (assoc 'link entry))
218          (link-addr (xml-get-attribute-or-nil link 'href)))
219     (when link
220       (setcar (cdr link) nil)
221       (setcdr (cdr link) (cons link-addr nil)))))
222
223 (defun atom-print-as-rss (atom)
224   (let ((rss (atom-to-rss atom)))
225     (insert atom-xml-declaration)
226     (insert "<rss version=\"2.0\">\n")
227     (insert "  <channel>\n")
228     (xml-print rss "    ")
229     (insert "\n  </channel>\n")
230     (insert "</rss>")))
231
232 (defun atom-to-rss-time (time)
233   "Translates a string from the format used by Atom into the
234 format used by RSS."
235   (let ((system-time-locale "C"))
236     (format-time-string "%a, %d %b %Y %T %z" (atom-parse-time time))))
237
238 (defun atom-to-rss-modify-time (entry)
239   "Modify ENTRY, changing the format of the `pubDate' in it."
240   (let ((pubDate (assoc 'pubDate entry)))
241     (setcar (cddr pubDate)
242             (atom-to-rss-time (car (xml-node-children pubDate))))))
243
244 (defun atom-to-rss-write-file (atom filename)
245   "Saves ATOM as a RSS feed into FILENAME."
246   (with-temp-buffer
247     (atom-print-as-rss atom)
248     (write-file filename)))
249
250 \f
251 (defvar atom-time-format-string "%Y-%m-%dT%T%z"
252   "The format for string representation of dates.")
253
254 (defvar atom-xhtml-namespace "http://www.w3.org/1999/xhtml")
255
256 (defvar atom-xml-declaration "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
257
258 (defun atom-format-time (&optional time)
259   "Format a time according to RFC3339."
260   ;; The time zone must be specified in numeric form, but with a colon between
261   ;; the hour and minute parts.
262   (replace-regexp-in-string
263    "\\(..\\)$" ":\\1"
264    (format-time-string atom-time-format-string time)))
265
266 (defun atom-parse-time (&optional time)
267   "Parse a time as specified in RFC3339 into Emacs's native format."
268   ;; Same remark as in `atom-format-time'
269   (date-to-time (replace-regexp-in-string ":\\(..\\)$" "\\1" time)))
270
271 (defun atom-massage-html (content)
272   "Massage CONTENT so it can be used as an HTML fragment in an
273 Atom feed. CONTENT must be a string."
274   (list '((type . "html")) content))
275
276 (defun atom-string-to-xml (string)
277   "Convert STRING into a Lisp structure as used by `xml.el'."
278   (require 'xml-xhtml-entities)
279   (let ((xml-entity-alist xml-xhtml-entities)
280         (xml-validating-parser t))
281     (with-temp-buffer
282       (insert "<div xmlns=\"" atom-xhtml-namespace "\">")
283       (insert string)
284       (insert "</div>")
285       ;; `xml-parse-region' returns a list of elements, even though it
286       ;; requires an only root node. We are only interested in the first
287       ;; one, the DIV we just inserted.
288       (car (xml-parse-region (point-min) (point-max))))))
289
290 (defun atom-massage-xhtml (content)
291   "Massage CONTENT so it can be used as an XHTML fragment in an
292 Atom feed."
293   (list '((type . "xhtml"))
294         (or (and (stringp content)
295                  (atom-string-to-xml content))
296             `(div ((xmlns . ,atom-xhtml-namespace)) ,@content))))
297
298 (defun atom-massage-author (author)
299   "Return an XML node representing the author. AUTHOR can be:
300 - nil, in which case `user-full-name' and `user-mail-address' are
301   used;
302 - a single string, the full name of the author; no email address
303   will be included;
304 - a list with two elements, the full name and the email address
305   of the author;
306 - something else, assumed to be a complete `atomPersonConstruct'."
307   `(nil ,@(cond
308            ((null author) `((name nil ,user-full-name)
309                             (email nil ,user-mail-address)))
310            ((stringp author) `((name nil ,author)))
311            ((= 2 (length author)) `((name nil ,(car author))
312                                     (email nil ,(cadr author))))
313            (t `(author nil ,author)))))
314
315 (defun atom-xhtml-convert-links (node base)
316   "Make all links in NODE (a fragment of an XHTML document)
317 absolute, in the context of BASE, an URL."
318   (dolist (attr-name (list 'href 'src))
319     (let ((attr (assoc attr-name (xml-node-attributes node))))
320       (when attr (setcdr attr (url-canonalize (cdr attr) base)))))
321   (dolist (child (xml-node-children node))
322     (when (listp child) (atom-xhtml-convert-links child base))))
323
324 (defun atom-generate-id (link creation-date)
325   "Generate a string suitable for use as an atom:id element. This
326 implements Mark Pilgrom's tag: URI method, using the
327 CREATION-DATE of the entry, and the domain part of LINK."
328     (format "tag:%s,%s:/%s"
329             (url-host (url-generic-parse-url link))
330             (format-time-string "%Y-%m-%d" creation-date)
331             (format-time-string "%Y%m%d%H%M%S" creation-date)))
332
333 \f
334 ;;; Functions that should probably not be there
335
336 (defun url-canonalize (address base)
337   "Make ADRESS an absolute URL, taking it in the BASE context."
338   ;; I feel such a function should exist in `url-parse'. Did I miss it?
339   (let ((url-base (url-generic-parse-url base))
340         (url-address (url-generic-parse-url address)))
341     (if (url-host url-address)
342         address
343       (setf (url-filename url-base)
344             (expand-file-name address
345                               (file-name-directory (url-filename url-base))))
346       (url-recreate-url url-base))))
347
348 (defun xml-node-as-text (node)
349   "Return a string representing NODE, an XML structure."
350   (with-temp-buffer
351     (xml-print (xml-node-children node))
352     (buffer-string)))
353
354 (defun xml-node-create (name attrlist childlist)
355   "Create a new XML node."
356   (list name attrlist . childlist))
357
358 (provide 'atom)
359 ;;; atom.el ends here