Emacs Lisp: Updating Atom Webfeed

Buy Xah Emacs Tutorial. Master emacs benefits for life.
, ,

This page shows a example of writing a emacs lisp command that updates a web feed file (Atom/RSS) on Local file system.

Problem Description

Summary

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, Programing, Web Dev, Math. 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. 〔➤ Atom Webfeed Basics〕 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:

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's no standardized method on what the string should be. (See: Atom Webfeed Basics.)

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>�</title>
   <id>%s</id>
   <updated>%s</updated>
   <summary>�</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 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 1)
      (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 1)
    (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 1)
      (search-forward "<updated>" nil t)
      (delete-char 25)
      (insert-date-time))
    (search-forward ">�" 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 , so that when this code is done, the cursor is right at the Title tag part for user to edit.

Emacs ♥

Like it?
Buy Xah Emacs Tutorial
or share
blog comments powered by Disqus