Insert HTML Tags in Emacs (Shallow Thoughts)

Akkana's Musings on Open Source Computing and Technology, Science, and Nature.

Fri, 26 Nov 2021

Insert HTML Tags in Emacs

Emacs has various options for editing HTML, none of them especially good. I gave up on html-mode a while back because it had so many evil tendencies, like not ever letting you type a double dash without reformatting several lines around the current line as an HTML comment. I'm using web-mode now, which is better.

But there was one nice thing that html-mode had: quick key bindings for inserting tags. For instance, C-c i would insert the tag for italics, <i></i>, and if you had something selected it would make that italic: <i>whatever you had selected</i>.

It's a nice idea, but it was too smart (for some value of "smart" that means "dumb and annoying") for its own good. For instance, it loves to randomly insert things like newlines in places where they don't make any sense. And their behavior changed all the time: I would find a workaround preventing one set of newlines and indentation; and then the next time emacs updated, suddenly tag insertion was inserting newlines and spaces I didn't want again and I had to go spend a few more hours finding a new workaround.

Eventually I got tired of that and wrote a couple of simple, not-overly-smart functions of my own. They work in web-mode as well as html-mode, and, most important, they don't change their behavior unless I explicitly change the code.

;; Add an inline HTML tag:
(defun add-html-tag (tag) (interactive "sHTML Tag")
  (let (
        (rstart (if (region-active-p) (region-beginning) (point)))
        (rend   (if (region-active-p) (region-end)       (point))))

    ;; Insert the close tag first, because inserting the open tag
    ;; will mess up the rend position.
    (goto-char rend)
    (insert "</" tag ">")

    ;; Now the open tag:
    (goto-char rstart)
    (insert "<" tag ">")
))

;; Plus one that adds newlines:
(defun add-html-block (tag) (interactive "sHTML Block")
  (let (
        (rstart (if (region-active-p) (region-beginning) (point)))
        (rend   (if (region-active-p) (region-end)       (point))))

    ;; Insert the close tag first, because inserting the open tag
    ;; will mess up the rend position.
    (goto-char rend)
    (insert "\n")

    ;; Now the open tag:
    (goto-char rstart)
    (insert "<" tag ">\n")
))

I've been using that for several years and it's working fine. But I do find that I spend more time than I like typing links by hand, <a href=""></a<%gt; -- clearly that was something else I should automate.

But links are a little trickier than the simple tags I was handling. When you're adding something like a bold or italic tag, if you already have some text selected, there's only one place it can go. But a link has two parts:

<a href="http://the-url">The link text</a>

So it would be nice if the function was smart enough to figure out whether the selection is a URL, in which case it should go inside the href="selected text here", or not, in which case it should be the link text.

First I needed an elisp function that could determine whether the region contained a URL. I had a bunch of false starts there, but in the end the function was simple:

(defun url-in-region (start end) (interactive "r")
  (save-excursion
    (goto-char start)
    (re-search-forward "://" end t)
    ))

The (interactive "r") lets you run the function interactively with M-x url-in-region (helpful for testing). When run like that, it automatically looks for a selected region and uses that for the start and end points. Functions calling it still need to pass in start and end, though.

Then comes the actual link-adding function:

(defun add-html-link () (interactive)
  (let ((href     "")
        (linktext "")
        (start    (point))
        (end      (point))
        )

    (if (region-active-p)
      (progn
        (setq start (region-beginning))
        (setq end   (region-end))

        (if (url-in-region start end)
          (setq href     (buffer-substring-no-properties start end))
          (setq linktext (buffer-substring-no-properties start end))
          )

        (kill-region start end)
      )
    )

    (insert "<a href=\"" href "\">" linktext "</a>")

    ;; Move the cursor to a useful place. By default it's at the end
    ;; of the inserted text.
    (if (string-empty-p href)
        ;; No href, put the cursor there
        (progn
          (goto-char start)
          (forward-char 9))
        (if (string-empty-p linktext)
          ;; href but no link text, put the cursor there
          ;; Already at end, no need to (goto-char end)
          (backward-char 4))
    )
))

Finally, there are the key bindings, defined in the mode hook used for either sgml-mode (the parent mode of html-mode) and web-mode:

(defun html-hook ()
  ;; Define keys for inserting tags in HTML and web modes:
  (local-set-key "\C-cb" (lambda () (interactive) (add-html-tag "b")))
  (local-set-key "\C-ci" (lambda () (interactive) (add-html-tag "i")))
  (local-set-key "\C-cc" (lambda () (interactive) (add-html-tag "code")))
  (local-set-key "\C-c1" (lambda () (interactive) (add-html-tag "h1")))
  (local-set-key "\C-c2" (lambda () (interactive) (add-html-tag "h2")))
  (local-set-key "\C-c3" (lambda () (interactive) (add-html-tag "h3")))
  (local-set-key "\C-c4" (lambda () (interactive) (add-html-tag "h4")))

  (local-set-key "\C-cp" (lambda () (interactive) (add-html-block "pre")))
  (local-set-key "\C-cB" (lambda () (interactive) (add-html-block "blockquote")))

  (local-set-key "\C-cl" 'add-html-link)

  ;; any other settings for autoindent levels, etc.
)

(add-hook 'sgml-mode-hook 'html-hook)
(add-hook 'web-mode-hook 'html-hook)

I spent way too much time on this. But at least I learned some new things about elisp, and I shouldn't ever have to type <a href=""> again. I'm sure in a few years, the time I saved not typing that will justify the time I spent writing the function ...

Tags: , , ,
[ 19:07 Nov 26, 2021    More linux/editors | permalink to this entry | ]

Comments via Disqus:

blog comments powered by Disqus