Emacs Lisp: Multi-Pair String Replacement with Report

By Xah Lee. Date: . Last updated: .

This page shows you how to write a emacs lisp command that does multi-pair find replace on current buffer or text selection, and print a report of changed items.

Problem

I have this text.

• 〈The Rise of “Worse is Better”〉 (1991) …
• 《The Unix-Hater's Handbook》 (1994) …

I want it to become:

<cite>The Rise of “Worse is Better”</cite> (1991) …
• <cite class="book">The Unix-Hater's Handbook</cite> (1994) …

The command also should generate a report of all changes made, in a separate buffer.

Solution

xah-angle-brackets-to-html

(defun xah-angle-brackets-to-html (&optional @begin @end)
  "Replace all 〈…〉 to <cite>…</cite> and 《…》 to <cite class=\"book\">… in current text block or selection.

When called non-interactively, *begin *end are region positions.

URL `http://xahlee.info/emacs/emacs/elisp_replace_title_tags.html'
version 2017-06-10"
  (interactive)
  (let (($changedItems '())
        (case-fold-search nil)
        $p1 $p2
        )
    (if (and @begin @end)
        (progn
          (setq $p1 (region-beginning))
          (setq $p2 (region-end)))
      (if (use-region-p)
          (progn
            (setq $p1 (region-beginning))
            (setq $p2 (region-end)))
        (save-excursion
          (if (re-search-backward "\n[ \t]*\n" nil "move")
              (progn (re-search-forward "\n[ \t]*\n")
                     (setq $p1 (point)))
            (setq $p1 (point)))
          (if (re-search-forward "\n[ \t]*\n" nil "move")
              (progn (re-search-backward "\n[ \t]*\n")
                     (setq $p2 (point)))
            (setq $p2 (point))))))
    (save-restriction
      (narrow-to-region $p1 $p2)
      (goto-char (point-min))
      (while (re-search-forward "《\\([^》]+?\\)》" nil t)
        (push (match-string-no-properties 1) $changedItems)
        (replace-match "<cite class=\"book\">\\1</cite>" "FIXEDCASE"))
      (goto-char (point-min))
      (while (re-search-forward "〈\\([^〉]+?\\)〉" nil t)
        (push (match-string-no-properties 1) $changedItems)
        (replace-match "<cite>\\1</cite>" t)))
    (if (> (length $changedItems) 0)
        (mapcar
         (lambda ($x)
           (princ $x)
           (terpri))
         (reverse $changedItems))
      (message "No change needed."))))

Here's a outline of the algorithm:

  1. Search forward by regex for 《…》
  2. If found, replace it with cite tag.
  3. Push the replacement into a list (for the report of changed items later).
  4. Repeat the above until no more title brackets found.
  5. goto top, do the same for 〈…〉.
  6. When no more found, print the changed items.

All the functions in this code are very basic and is frequently used for text processing tasks. You should master them. You can just use this function as a template to write your own.

The code is easy to understand. If you find it difficult, have a look at Emacs Lisp: Quick Start and Emacs Lisp Idioms.

Showing the changed items is important, because your text may have a mis-matched bracket. The output lets you verify correctness in a glance.

Example: Remove Wikipedia Citation Mark

In Wikipedia article, there are many citation marks like this: {[1], [2], etc}. If you quote Wikipedia in your blog, those citation marks don't make sense and are distracting.

Here's a command to remove them.

(defun xah-remove-square-brackets (Begin End)
  "Delete any text of the form [1], [2] etc in current text block or selection.

For example
 as Blu-ray Disc [11][12],
becomes
 as Blu-ray Disc,

When called non-interactively, Begin End are region positions.

URL `http://xahlee.info/emacs/emacs/elisp_replace_title_tags.html'
Version: 2017-06-10 2021-03-04 2021-08-17 2023-07-22"
  (interactive
   (let (xp1 xp2)
     (let ((xbds (xah-get-bounds-of-block-or-region))) (setq xp1 (car xbds) xp2 (cdr xbds)))
     (list xp1 xp2)))
  (let ((xp1 Begin) (xp2 End) xchangedItems)
    (save-restriction
      (narrow-to-region xp1 xp2)
      (progn
        (goto-char (point-min))
        (while (re-search-forward "\\(\\[[0-9]+?\\]\\)" nil t)
          (setq xchangedItems (cons (match-string-no-properties 1) xchangedItems ))
          (replace-match "" t)))
      (progn
        (goto-char (point-min))
        (while (search-forward "[citation needed]" nil t)
          (setq xchangedItems (cons "[citation needed]" xchangedItems ))
          (backward-char 17)
          (delete-char 17)))
      (goto-char (point-max)))
    (if (> (length xchangedItems) 0)
        (mapcar
         (lambda (xx)
           (princ xx)
           (terpri))
         (reverse xchangedItems))
      (message "No change needed."))))

requires package Emacs: xah-get-thing.el