HomeBlogClose last XML tag (Emacs)

Close last XML tag (Emacs)

I'm using Emacs for almost 10 years, but still I never wrote too much about it.  Partly, the reason is that I'm not an Emacs Guru (although I'm using it for 10 years, imagine that).  Emacs is so functional in itself that you don't have to become a “guru” to like it   (by “guru” here I mean someone who can bring substantial improvements by writing Lisp code, such as writing a major mode).

I'm using Emacs for most my editing needs.  These include, sometimes, typing text in a <textarea> in Firefox (there is a nifty extension called “it's all text” which allows you to use a decent editor to type in those horrible boxes).  I also frequently use Emacs for typing email (check Wanderlust if you haven't already, one of the best email clients I ever used).  And obviously for writing source code of any kind—XML, JavaScript, Java, C, Perl, PHP, Python, Ruby—you name it, we have it.  I remember a great saying by Paul Graham: “all other things being equal, it's a bad decision not to chose the best programming language for your problem” (that's not ad literam what he said, but you get the point).

Solving a frequent problem

So if you're like me, and I think you are, then you need to enter various XML data in various kinds of files.  Many times you need to embed XML (or derived languages) in JavaScript, or in PHP, or sometimes in Java or Perl.  One of the things that always bothered me was that I had to look back and count tags so that I know what tag should I close at some point.

For example, if I type the following:

var html = [];
html.push("<table>");
if (foo.rating)
    html.push("<tr><td>Rating:</td><td>", foo.rating, "</td></tr>");
if (foo.user)
    html.push("<tr><td>User:</td><td>", foo.user, "</td></tr>");
html.push("</table>");

Let's count.  I have 7 close tags in the above sample.  Let's say you need 2 seconds to type a closing tag (the average is even worse).  Then I've spent like 14 seconds only to close those damn XML tags.

I've done this so many times that I naturally learned to live with it.  If you're working with Notepad, then there's no way you can think of helpers—you have to live with it.

However I'm not with Notepad.  When I edit XML files, I use NXHTML mode, which is a masterpiece, and saves me a lot of time from closing tags and validating my work.  So why shouldn't I be able to have at least a minor benefit when using other modes, such as JS?

Turns out it's easy.  It would have been a million times easier if I knew Lisp, but I don't know Lisp so I needed one whole hour to write the following function.  I have to use the manual at every line I type.  That's a problem, but I hope I'll improve that.  Yet, this function will save me many minutes a day, so it worthed spending the efforts.

Here's how I solved the “closing tags problem”:

(scroll down for a newer version)

;; define the function
(defun msh-close-tag ()
  "Close the previously defined XML tag"
  (interactive)
  (let ((tag nil))
    (save-excursion
      (do ((skip 1))
          ((= 0 skip))
        (re-search-backward "</?\\([a-zA-Z0-9_-]+\\)")
        (cond ((looking-at "</")
               (setq skip (+ skip 1)))
              ((setq skip (- skip 1)))))
      (when (looking-at "<\\([a-zA-Z0-9_-]+\\)")
        (setq tag (match-string 1))))
    (insert (concat "</" tag ">"))))

;; and bind it to some key, so we can be fast
(define-key global-map [(control c) (/)] 'msh-close-tag)

Put this in your ~/.emacs and whenever you need to close a tag, you just press “C-c /”.  It will look for the closest opened tag and it'll close it.  It also works when you have, say, “<foo><bar></bar>|” (where “|” is the caret position)—at this point it'll enter “</foo>”.

It's not bullet-proof—it just counts closing tags and discards as many open tags, before finding the one that you need to close.  This means, if you have: “<foo><bar></foo>|” and press “C-c /”, it will still enter “</foo>” although your XML is obviously invalid.  I decided not to care about this case because my XML is always valid, period. ;-)

That's the first Emacs article that I publish, and I hope to write a lot more in the future.  It's really the best text Editor there is and, to quote Paul Graham again, all other things being equal, it's a mistake not to use the best tool.

Update (April 2009) — I wrote since a better version of this function, pasted below.

(defun msh-close-tag ()
  "Close the previously defined XML tag"
  (interactive)
  (let ((tag nil)
        (quote nil))
    (save-excursion
      (do ((skip 1))
          ((= 0 skip))
        (re-search-backward "</?[a-zA-Z0-9_-]+")
        (cond ((looking-at "</")
               (setq skip (+ skip 1)))
              ((not (looking-at "<[a-zA-Z0-9_-]+[^>]*?/>"))
               (setq skip (- skip 1)))))
      (when (looking-at "<\\([a-zA-Z0-9_-]+\\)")
        (setq tag (match-string 1)))
      (if (eq (get-text-property (point) 'face)
              'font-lock-string-face)
          (setq quote t)))
    (when tag
      (setq quote (and quote
                       (not (eq (get-text-property (- (point) 1) 'face)
                                'font-lock-string-face))))
      (if quote
          (insert "\""))
      (insert "</" tag ">")
      (if quote
          (insert "\"")))))

Fixes are:

  • If the XML tag was defined in a string context and msh-close-tag is not called in a string context, then quotes are added automatically (this is based on font locking actually).  It turns out to work best generally.
  • It will properly skip "<quiclky />" closed tags now.

Comments

Page info
Created:
2007/11/22 22:40
Modified:
2009/04/28 23:23
Author:
Mihai Bazon
Comments:
1
Tags:
emacs, programming
See also