ELisp: Writing a Interactive Command to Update HTML Page Tags

By Xah Lee. Date: . Last updated: .

This page shows a example of writing a emacs lisp function that update the page navigation tag of several files.

Problem

I want to write a command, so that, when invoked, emacs will update several HTML page's page navigation tag.

You will you learn how to grab the region text, parse them into a list, then use the list to generate a string, then go thru the list to open each file, insert the string, and do other modification on the file.

Detail

I have a website of few thousand pages. Many of them are projects that span several HTML pages. At the bottom of each page is a navigation bar, like this:

page navigation bar

The HTML looks like this:

<div class="pages">Goto Page:
<a href="projectB.html">1</a>,
2,
<a href="projectB-3.html">3</a>,
<a href="projectB-4.html">4</a>
</div>

If i want to add a new file for this series, let's say projectB-5.html, i have to manually edit every file's page navigation bar.

It would be nice, if i could just list the file names in the current new page i just created, like this:

projectB.html
projectB-2.html
projectB-3.html
projectB-4.html
projectB-5.html

Then, select them, press a button, and have all the page tags of all files updated. I decided to write this command.

Solution

Here's the basic steps.

Here's the code:

(defun xah-update-page-tag (p1 p2)
  "Update HTML page navigation tags.

The input is a text selection.
Each line should a file name
Update each file's page navigation tag.

Each file name is a file path without dir, and relative to current dir.

Example text selection for input::

combowords.html
combowords-2.html
combowords-3.html
combowords-4.html
"
  (interactive "r")
  (let (fileList pageNavStr (i 1))
    (setq fileList
          (split-string (buffer-substring-no-properties p1 p2) "\n" t)
          )

    (delete-region p1 p2)

    ;; generate the page nav string
    (setq pageNavStr "<div class=\"pages\">Goto Page: ")

    (while (<= i (length fileList))
      (setq pageNavStr
            (concat pageNavStr
                    "<a href=\""
                    (nth (- i 1) fileList)
                    "\">"
                    (number-to-string i)
                    "</a>, ")
            )
      (setq i (1+ i))
      )

    (setq pageNavStr (substring pageNavStr 0 -2) ) ; remove the last ", "
    (setq pageNavStr (concat pageNavStr "</div>"))

    ;; open each file, inseart the page nav string, remove link in the
    ;; nav string that's the current page
    (mapc
     (lambda (thisFile)
       (message "%s" thisFile)
       (find-file thisFile)
       (goto-char (point-min))
       (search-forward "<div class=\"pages\">")
       (beginning-of-line)
       (kill-line 1)
       (insert pageNavStr)
       (search-backward (file-name-nondirectory buffer-file-name))
       (sgml-delete-tag 1)
;;        (save-buffer)
;;        (kill-buffer)
       )
     fileList)
))

First, we define the function with 2 parameters named {p1, p2}, and use (interactive "r"). This will automatically fill the parameters {p1, p2} with the beginning and ending positions of text selection.

The next task is to grab this block of text, and turn it into a list, using split-string. This is done like this:

(setq fileList
      (split-string (buffer-substring-no-properties p1 p2) "\n" t)
      )

Then, we want to generate the navbar string. This is done by using a while loop with a counter “i”. In each iteration, a string for the current file is generated, and is then appended to pageNavStr.

This gives us the navbar string. The value of pageNavStr may be like this:

<div class="pages">Goto Page:
<a href="projB.html">1</a>,
<a href="projB-2.html">2</a>,
<a href="projB-3.html">3</a>
</div>

However, it is not the final form. If current page is 2, then the navbar string should be like this:

<div class="pages">Goto Page:
<a href="projB.html">1</a>,
2,
<a href="projB-3.html">3</a>
</div>

The next step is to open each file, insert the navbar string in the proper place, then take out the link of the current page. This is done by this code:

(mapc
 (lambda (thisFile)
   (message "%s" thisFile)
   (find-file thisFile)
   (goto-char (point-min))
   (search-forward "<div class=\"pages\">")
   (beginning-of-line)
   (kill-line 1)
   (insert pageNavStr)
   (search-backward (file-name-nondirectory buffer-file-name))
   (sgml-delete-tag 1)
   )
 fileList)

The logic is this: We map a function to each file. The function will locate the existing navbar string, then delete that line, then insert the new navbar string, then move back to the location where the link to current file is at, then remove the link.

The function mapc has this form: (mapc function list), where it will apply function to each element in the list. mapc is different from mapcar. If you want the result to be a list, you need to use mapcar. Since we don't care for the resulting list, so we use mapc.

The lambda above is our function. lambda has the form (lambda (x) body), where x is the function's parameter, and body is one or more lisp expressions. In the “body” part, any x will be replaced with the argument received by lambda.

In our lambda body, first we print out a messag informing user the current file it's working on, then we open the file, then search-forward to move the cursor to the navbar string location, delete it, then insert the new navbar string, then we use search-bacward to search for the current file's name. The current file's name is generated by calling (file-name-nondirectory buffer-file-name). Once the cursor is at the location of current file in the navbar string, we call sgml-delete-tag, which will delete both the opening and closing HTML tags the cursor is on. The sgml-delete-tag is defined in html-mode.

If we want to, we can add a (save-buffer) and (kill-buffer) to save and close the file, but for now i decided to leave the processed files open because sometimes i'm in the middle of editing them. It is easy to save and close a bunch of files using ibuffer.

So, now with this function, suppose i created a new page projB-5.html. All i have to do is to list all the relevant files in the current buffer. This is easily done in emacs by Ctrl+u Alt+x shell-command, then type ls projB*html. Then, emacs will insert to the current buffer this text:

projB.html
projB-2.html
projB-3.html
projB-4.html
projB-5.html

Then, i select them, then Alt+x xah-update-page-tag, then all the pages will be updated for me.

Note that my file names are not necessarily regular like {“1.html”, “2.html”, “3.html”, etc}. Otherwise, our function doesn't need to take a list of file names from the region. It can just take one name and generate all others.