Elisp: Command to Update RSS/Atom Webfeed

By Xah Lee. Date: . Last updated: .

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:

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 🧡