Chicken Scratches

Auto-closing Django template tags in Emacs

I have written two previous articles about how I edit Django template files in Emacs and XEmacs.
Here is
How
I edit Django templates
. And here
is More
on editing Django templates in XEmacs
. Here today is another
little tip that can be used in conjunction with those two other
posts or independently.

Django templates involve a lot of punctuation. Between the angle
brackets and slashes of HTML and the curly braces and percent
signs of the Django template language, it’s enough to make your
pinky fingers hurt just thinking about it. Therefore any little
trick to reduce some of this typing burden can be
helpful. Presented here is some Emacs Lisp code to provide
auto-closing of Django template tags. So even if you still have
to type things like curly-brace percent-sign space ifequal blah
blah2 percent-sign close-curly-brace
, you won’t have to type the
{% endifequal %}. (Of course, if you’re using
the abbrev tips I gave previously, you won’t even need
to type the opening tag very often, but sometimes you still do.)

The code

Ok, here it is. You should be able to copy and paste this into
your .emacs file. Then anywhere in your template file, simply call
the new function django-close-tag and it will find the
last open tag and close it. In the sample code below, I have it
bound to C-c]; that is, hold down the control key and
press C, then let go of the control key and press the right
square bracket. Of course, you can change the code to bind the
function to any key combination you want.

(defvar django-closable-tags
  '("for" "block" "comment" "filter" "ifchanged" "ifequal"
    "ifnotequal" "spaceless" "if"))

(defvar django-tag-re
  (concat "{%\\s *\\(end\\)?\\("
          (mapconcat 'identity django-closable-tags "\\|")
          "\\)[^%]*%}"))

(defun django-find-open-tag ()
  (if (search-backward-regexp django-tag-re nil t)
      (if (match-string 1) ; If it's an end tag
          (if (not (string= (match-string 2) (django-find-open-tag)))
              (error "Unmatched Django tag")
            (django-find-open-tag))
        (match-string 2)) ; Otherwise, return the match
    nil))

(defun django-close-tag ()
  (interactive)
  (let ((open-tag (save-excursion (django-find-open-tag))))
    (if open-tag
        (insert "{% end" open-tag " %}")
      (error "Nothing to close"))))

    
(define-key html-mode-map "\C-c]" 'django-close-tag)


    

How it works

The
variable django-closable-tags is
a list of all the Django tags that require closing. As more tags
get added, this list can be expanded.

The meat of the work is done
in django-find-open-tag. The
first thing it does is search backwards through the buffer for
the last Django tag. If the last tag is an end-tag, the function
calls itself recursively to skip over that block. This continues
until it finds an unclosed tag, which it then returns as a string.

The function you actually call
is django-close-tag. This
calls django-find-open-tag to
find a tag to close, then inserts the appropriate end-tag text.

Hopefully that is pretty straightforward. The only tricky part
for a Lisp newbie could be the use of recursion. For a situation
like this, recursion is the perfect approach. In a sense, the
function calls build a list of closed tags on the call-stack,
which then unravels one at a time until an unmatched tag is
found.

Other than that, there is some pretty
straightforward regular
expression matching
. These are extremely powerful, and you
should read up on them if you haven’t already.

%d bloggers like this: