Xah Lee, 2008-11-25
This page shows a example of writing a emacs lisp function that update the page navigation tag of several files. If you don't know elisp, see: Emacs Lisp Basics.
I want to write a command, so that, when invoked, emacs will update several html page's page navigation tag.
This lesson will show you 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.
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:
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>
Often, i add new content to the project, so that i may create a new file, let's say “projectB-5.html”. Now, i need to modify all these pages so that their page navigation bar includes page 5, and also insert the navbar in page 5.
This means that i open each file, go to the navbar location, add the link for page 5 in the navbar string. Remember that the navbar string for each page is slightly different, because if we are on page 5, the page navbar should not show 5 as a link.
With emacs, the whole task will take under 1 minute. But sometimes a project has 10 or 20 pages, then manually fixing each page becomes a problem. Also, sometimes the series of pages do not have regular names, for example:
<div class="pages">Goto Page: <a href="review-BNC.html">1</a>, <a href="review-Sparks.html">2</a>, <a href="review-329Z.html">3</a> </div>
and if i created a new page named “review-BlackBird.html” and want it to be the new page 2, the manual labor to fix the navbar string becomes more tedious and error prone.
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.
The solution is quite simple. 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 (filez pageNavStr (i 1)) (setq filez (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 filez)) (setq pageNavStr (concat pageNavStr "<a href=\"" (nth (- i 1) filez) "\">" (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) ) filez) ))
First, we define the function with 2 arguments “p1” and “p2”, and use “(interactive "r")”. This will automatically fill p1 and p2 parameters 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 filez
(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 to go on to each page. 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) ) filez)
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 f list)”, where it will apply f 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 appearance of “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 typing “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, the type “Alt+x xah-update-page-tag”, then all the pages will be updated for me.
Note that since the file names are regular, we can make our function automatically generate the list of files. Let's say the current buffer is the new “projB-5.html”, we could have our elisp function simply grab the current buffer's file name, automatically create the list of all the other files starting with “projB” and make it into the list. It is trivial to make our function behave that way. However, since many of my project's page names do not have such regularity, so i decided to let the function's input be user specified.
For examples of my project pages that this function is useful, see:
Emacs is beautiful!
Related essays: