Emacs Lisp: 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
I run several blogs on my personal website. For example, blog on Emacs, keyboard Programing. Each of these has a webfeed in Atom format.
Let's take the emacs blog for example. The file name is blog.html
. Typically, i open that file, write there, then save. The file sits on my local disk, and is periodically synced to my web server. For each of the blog file, there's also a corresponding webfeed, so that readers can subscribe to it.
To create a webfeed, i've chosen the Atom format. Basically, it is a XML file with tags for blog entries. [see Atom Webfeed Tutorial]
The Atom file is named blog.xml
in the same dir.
After i wrote some entry in my blog file blog.html
, i'd like to be able to press a button, so the current text selection will automatically be added into my atom webfeed file blog.xml
as a new entry.
Solution
In the beginning few months, i just manually add the new writing from blog.html
into the blog.xml
file. But after a while, the pattern is clear, and can be automated. So, here are the major steps:
- Grab the current text selection, lets call this “inputStr”. This will be 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 a Unicode symbol REPLACEMENT CHARACTER xxx as a marker/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”, by (setq inputStr (buffer-substring-no-properties begin end))
.
Then, it sets several paths. The current buffer's file path, name, dir, and the corresponding blog file's path, name.
After the several setq
, then it opens the webfeed file, go to the beginning of file, search for the first occurrence of <entry>
, and that's the point a new entry should be inserted.
It then call (insert-atom-entry altUrl)
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 content “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 ♥