Elisp: 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://ergoemacs.org/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 Basics 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], …}. 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 (&optional @begin @end)
  "Delete any text of the form “[‹n›]”, eg [1], [2], … in current text block or selection.

For example
 「… announced as Blu-ray Disc [11][12], and …」
becomes
 「… announced as Blu-ray Disc, and …」.

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

URL `http://ergoemacs.org/emacs/elisp_replace_title_tags.html'
Version 2017-06-10"
  (interactive)
  (let ($p1 $p2 $changedItems)
    (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)
      (progn
        (goto-char 1)
        (while (re-search-forward "\\(\\[[0-9]+?\\]\\)" nil t)
          (setq $changedItems (cons (match-string 1) $changedItems ))
          (replace-match "" t)))
      (progn
        (goto-char 1)
        (while (search-forward "[citation needed]" nil t)
          (setq $changedItems (cons "[citation needed]" $changedItems ))
          (backward-char 17)
          (delete-char 17))))
    (if (> (length $changedItems) 0)
        (mapcar
         (lambda ($x)
           (princ $x)
           (terpri))
         (reverse $changedItems))
      (message "No change needed."))))

For many more examples of using multi-pair find/replace, see: Emacs Lisp Find Replace String-Pairs Commands.

Liket it? Put $5 at patreon. Or Buy Xah Emacs Tutorial. Thanks.