Elisp: Command to Update RSS/Atom Webfeed
This page shows a example of writing a emacs lisp command that updates a web feed file (Atom/RSS) on local file system.
Problem
Write a command, when called, the current text selection will be added as a entry in a Atom webfeed file.
You'll learn how to write a command that grabs the region text, switch buffer, search string to locate position for inserting text, insert the text, and update date field in a file.
Detail
Let's your blog file name is blog.html
and
the webfeed file is named blog.xml
in the same dir.
You want to be able to write the blog, make a selection, press a button, then the text is automatically inserted into the webfeed file as a new entry.
The webfeed is in Atom format. Basically, it is a XML file with tags for blog entries. 〔see Atom Webfeed Tutorial〕
Solution
here are the major steps:
- Grab the current text selection, lets call this “inputStr”. This is the main content for the webfeed entry.
- Open the Atom file corresponding to the current file.
- Update the
updated
tag in the Atom file. - Insert a
entry
tag template into the Atom file at the right place. - Insert the “inputStr” in the proper location in the template entry.
Here's various pieces of code that is required. I'll start to show, from the smallest components, to the final code that makes all this work.
Insert Time Stamp
Here's a command to insert date stamp.
(defun current-date-time-string () "Returns current date-time string in full ISO 8601 format. Example: 「2012-04-05T21:08:24-07:00」. Note, for the time zone offset, both the formats 「hhmm」 and 「hh:mm」 are valid ISO 8601. However, Atom Webfeed spec seems to require 「hh:mm」." (concat (format-time-string "%Y-%m-%dT%T") ((lambda (x) (format "%s:%s" (substring x 0 3) (substring x 3 5))) (format-time-string "%z")) ) )
(defun insert-date-time () "Insert current date-time string in full ISO 8601 format. Example: 「2010-11-29T23:23:35-08:00」. Replaces currents text selection if there's one. This function calls: `current-date-time-string'." (interactive) (when (use-region-p) (delete-region (region-beginning) (region-end) ) ) (insert (current-date-time-string)))
One returns a string, the other inserts it at current cursor position.
Generate a new Atom Entry ID
Each atom entry has a “id” element like this:
<id>id string</id>
This id should be unique in the world. It should be in a URI format, and some other requirements, but otherwise there is no standardized method on what the string should be. 〔see Atom Webfeed Tutorial〕
Here's the code to generate this id that i've adopted, based on domain name, date, and unix epoch seconds.
(defun new-atom-id-tag (&optional domainName) "Returns a newly generated ATOM webfeed's “id” element string. Example of return value: 「tag:xahlee.org,2010-03-31:022128」 If DOMAINNAME is given, use that for the domain name. Else, use “xahlee.org”." (format "tag:%s%s" (if domainName domainName "xahlee.org") (format-time-string ",%Y-%m-%d:%H%M%S" (current-time) 1)) )
Insert Atom Entry Template
A entry in Atom format looks like this:
<entry> <title>How To Insert Text In Emacs Lisp</title> <id>tag:xahlee.org,2010-01-02:234451</id> <updated>2010-01-02T15:44:51-08:00</updated> <summary>a short tutorial</summary> <content type="xhtml"> <div xmlns="http://www.w3.org/1999/xhtml"> <p>hi there, today i did this and that.</p> <p>and more HTML of the full content here …</p> </div> </content> <link rel="alternate" href="http://xahlee.org/emacs/elisp_examples.html"/> </entry>
So, i need a command to insert this entry template.
(defun insert-atom-entry (altLinkUrl) "Insert a Atom webfeed entry template, in the current buffer's cursor position." (interactive) (let (textToInsert domainName ) (setq domainName "xahlee.org") (insert (format " <entry> <title>xxx</title> <id>%s</id> <updated>%s</updated> <summary>xxx</summary> <content type=\"xhtml\"> <div xmlns=\"http://www.w3.org/1999/xhtml\"> </div> </content> <link rel=\"alternate\" href=\"%s\"/> </entry> " (new-atom-id-tag domainName) (current-date-time-string) altLinkUrl )) ) )
Each Atom entry requires a link element, like this:
<link rel="alternate" href="http://xahlee.org/emacs/elisp_examples.html"/>
This link element is supposed to point to the perm link of the full article. This is set as a argument “altLinkUrl” to this function. The caller will fill it.
The timestamp for the <updated>
tag, and also id string for <id>
tag, are auto-generated from the functions we wrote before.
The content for <title>…</title>
and <summary>…</summary>
are not automatically created, because usually i don't have a title or summary for short blogs.
Title and Summary are required by Atom, so i write them on the spot.
I use string xxx
as a reminder to fill them.
Updating Blog Date
In the Atom file, at top there's a tag named “updated” that looks like this:
<updated>2010-01-02T15:44:51-08:00</updated>
This needs to be updated whenever you have a new entry. So, here's the code for that:
(progn (goto-char (point-min)) (search-forward "<updated>" nil t) (delete-char 25) (insert-date-time))
It uses the function insert-date-time
that we have defined earlier.
Final Code
Finally, here's the command that calls all the above functions to do what i want.
(defun make-blog-entry (begin end) "Create a Atom (RSS) entry of my emacs blog webfeed. Using selected text as Atom entry content. Also update the Atom file's overall “updated” tag. The feed is at [~/web/xahlee_org/emacs/blog.xml]" (interactive "r") (let (inputStr currentFileDir currentFileName blogFileName blogFilePath altUrl) (setq inputStr (buffer-substring-no-properties begin end)) (setq currentFileName (file-name-nondirectory (buffer-file-name))) (setq currentFileDir (file-name-directory (buffer-file-name))) ; ends in slash (setq blogFileName (concat (file-name-sans-extension (file-name-nondirectory currentFileName)) ".xml")) (setq blogFilePath (concat currentFileDir blogFileName)) (setq altUrl "http://xahlee.org/emacs/blog.html") (find-file blogFilePath) (goto-char (point-min)) (search-forward "<entry>" nil t) (beginning-of-line) (insert-atom-entry altUrl) (search-backward "<div xmlns=\"http://www.w3.org/1999/xhtml\">" nil t) (search-forward ">" nil t) (insert "\n" inputStr) ;; update atom date (progn (goto-char (point-min)) (search-forward "<updated>" nil t) (delete-char 25) (insert-date-time)) (search-forward ">xxx" nil t) ) )
The code is pretty simple. First, it sets the current selected text to the variable “inputStr”.
Then, it sets several paths. The current buffer's file path, name, dir, and the corresponding blog file's path, name.
then it opens the webfeed file, go to the beginning of file, search for the first occurrence of entry
tag, and that's the point a new entry should be inserted.
It then call insert-atom-entry
to insert a new entry template.
Then, it searches backward for the string
<div xmlns="http://www.w3.org/1999/xhtml">
. This is where the content part of the entry should be. The code then insert my inputStr there.
After that, we update the blog updated date, then we just move
pointer to the next occurrence of xxx
, so that when this code is done,
the cursor is right at the Title tag part for user to edit.
Emacs 🧡