Emacs Lisp: Replace String Based On File Name

Perm url with updates: http://xahlee.org/emacs/nav-bar.html

Emacs Lisp: Replace String Based On File Name

Xah Lee, 2006-11-29

This page is emacs lisp lesson on a real-world task. It show how emacs lisp is used in creating HTML navigational bar for a online book. Specifically, we need to insert proper text to each file for a set of files. You should be familiar with Elisp Language Basics.

The Problem

Summary

I want to insert the “Next Chapter”, “Previous Chapter” links to a series of HTML files that are chapters of a book.

This lesson essentially teaches you how to do a regex pattern replace by a function that return text based on the current file's name.

Detail

I have a lot of books in HTML form. Usually, the file names have this pattern: “chap1.html”, “chap2.html”, “chap3.html”... etc.

Each file is a chapter of a book. And in each file, I need to place a navigation bar, so that there's a Next Chapter and Previous Chapter links at the bottom of each page.

Normally, this can be done by writing a short Perl or Python script. The script will open the file, parse the file name so that it knows which chapter this file is. If the current file is “chapter3.html”, then the script will generate a string like this:

<div class="navbar">
<a href="chapter2.html">PREVIOUS</a>|
<a href="index.html">TOP</a>|
<a href="chapter4.html">NEXT</a>
</div>

For a person familiar with scripting languages, the job can be done in about 20 minutes. Basically, your script will traverse a directory and determine which files to process. For each file, your script will parse the file name and generate the navigation bar string appropriate for the file. Then, your script will open the file, insert the nav bar at the appropriate place, then close the file. Your script will need do a backup if you want it to be robust. With that, you'll also have to make sure that the file's owner, group, permissions etc meta data are kept intact.

In the end, some simple script can end up taking twice or trice the time you expected.

However, if you know elisp, you only need to write half of the code, since the file selection, file opening and reading, text decoding, backing up, saving and encoding, etc are all part of the emacs environment. All you really need to write is a elisp function that takes in a file name and returns the navigation bar string. This significantly saves your time. As a added benefit, you get to do this in a interactive, visual process. So, errors are much reduced, and you don't have to worry about your code making a mistake erasing parts of the file or missing some files.

Solution

First, mark the files you want to process in dired. Then, use “dired-do-query-replace-regexp” to do a find and replace operation on a particular string. For example, replace 「<body>」 with 「<body> someNavbarString」. (For a tutorial on using “dired-do-query-replace-regexp”, see: Interactively Find and Replace String Patterns on Multiple Files. )

The trick lies in your replacement string. You want to use a elisp function that returns the appropriate nav bar for the chapter. (so that the Next and Previous links are correct, according to the chapter number of the current file)

In emacs 22, there's a new feature that allows you to put a elisp function as your replacement string. This is done by giving the replacement string this form 「 \,(functionName)」, where functionName is the name of your function. So, if the function that returns the nav bar string is called “ff”, then in your replacement string you can say 「\,(ff)」

Here is the ff function:

(defun ff ()
  "Returns a navigation bar string with Prev and Next links based on the current file name."
 (interactive)
(let (fname navbarStr chapterNum )
  (setq fname (file-name-nondirectory (buffer-file-name)) )
  (setq chapterNum (string-to-number (substring (file-name-sans-extension fname) 4)))
  (setq navbarStr 
        (concat "<div class=\"nav\">★ <a href=\"" "chap"
                (number-to-string (- chapterNum 1))
                ".html" "\">◀</a> <a href=\"index.html\">▲</a> <a href=\"" "chap"
                (number-to-string (+ chapterNum 1))
                ".html" "\">▶</a> Flatland</div>"))
  navbarStr
  ) )

In the above code, the buffer-file-name returns the full path of the file of the current buffer. The file-name-nondirectory truncates the path to just the file name. The line

(setq chapter-num (string-to-number (substring (file-name-sans-extension fname) 4)))

extract the chapter number from the file name.

For a example of a online book with Next/Previous navigation bar, see: Flatland.

Emacs is beautiful!

Second Example

2010-12-01

Today, i need to do similar again. I have a dir with names like this:

x001-1.html
x001-2.html
x002-1.html
x002-2.html
x003-1.html
x003-2.html
...

These are pages for the novel Journey To The West (Monkey King). The first part of the file name is the chapter number. Each chapter has 2 html pages, indicated in the second part of the file name.

In each page, there's a nav bar code like this:

<div class="nav">
<a href="monkey_king.html" title="up">▲</a>
<a href="x002-2.html" title="next">▶</a>
</div>

It allows readers to go to the Table Of Contents page, or go to the next page. But it is missing a nav bar button to go to the previous page. I'd like to fix it, so it should be like this:

<div class="nav">
<a href="x001-2.html" title="previous">◀</a>
<a href="monkey_king.html" title="up">▲</a>
<a href="x002-2.html" title="next">▶</a>
</div>

So, the task is to add this:

<a href="x001-2.html" title="previous">◀</a>

to every page, and the link depends on the current file name.

Solution

Here's what i do to solve it, in the quickest way i know possible with emacs.

First, use find and replace on all files. Replace all occurance of

<div class="nav">
<a href="monkey_king.html" title="up">▲</a>

with

<div class="nav">
<a href="htnshtns" title="previous">◀</a>
<a href="monkey_king.html" title="up">▲</a>

The 「htnshtns」 is just a random string. We use a fixed random string so that we can do multi-file find replace one more time, where the find string will be this fixed string, and the replace string will be generated by a lisp function.

(If you don't know how to do find replace on multiple files, see: Interactively Find and Replace String Patterns on Multiple Files.)

Now, here's the lisp code quickly written:
(defun ff ()
  "..."
  (interactive)
  (let (fname mylist chapterNum pageNum chapterNumNew pageNumNew )

    (setq fname (file-name-nondirectory (buffer-file-name)) )

    (setq mylist (split-string (substring (file-name-sans-extension fname) 1) "-" ) )
    
    (setq chapterNum (string-to-number (nth 0 mylist) ))
    (setq pageNum (string-to-number (nth 1 mylist) ))

    (if (= pageNum 1) 
        (progn
          (setq chapterNumNew (- chapterNum 1))
          (setq pageNumNew 2)
          )
      (progn
        (setq chapterNumNew chapterNum)
        (setq pageNumNew 1)
        )
      )

    (concat "x" (format "%03d" chapterNumNew) "-" (format "%d" pageNumNew) ) ) )

So, with this code, i just call find replace, with find string 「htnshtns」, and replace value of 「\,(ff)」

Emacs is fantastic!

Popular posts from this blog

11 Years of Writing About Emacs

does md5 creates more randomness?

Google Code shutting down, future of ErgoEmacs