Emacs: HTML. Lines to List 📜

By Xah Lee. Date: . Last updated: .

Here's a command that turn lines or paragraphs into HTML list.

(defun xah-html-lines-to-list (&optional Begin End ItemSepRegex zordered-list zinteract)
  "Make each line into a HTML list item, of current text block or selection.

If the region contain empty line, each text block is made into a list item. Else, each line is a list item.

If `universal-argument' is called first, use ordered list ol instead of ul.

if zinteract is t, move cursor forward by skipping newline chars.

When done, the cursor is at end closing list tag.

URL `http://xahlee.info/emacs/emacs/elisp_lines_to_list.html'
Created: 2020-01-17
Version: 2025-09-11"
  (interactive
   (let (Begin
         End
         (ItemSepRegex (if current-prefix-arg (read-string "list item separator regex:" "\n\n+" nil "\n\n+")))
         (zordered-list (if current-prefix-arg (y-or-n-p "zordered-list list?") nil))
         (zinteract t))
     (seq-setq (Begin End)
               (if (region-active-p)
                   (list (region-beginning) (region-end))
                 (list (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point))) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point))))))
     (list Begin End ItemSepRegex zordered-list zinteract)))
  (let (xbeg xend xinput xsepRegex)
    (seq-setq (xbeg xend) (if (and Begin End) (list Begin End) (list (save-excursion (if (re-search-backward "\n[ \t]*\n" nil 1) (match-end 0) (point))) (save-excursion (if (re-search-forward "\n[ \t]*\n" nil 1) (match-beginning 0) (point))))))
    (progn
      (goto-char xbeg)
      (when (re-search-forward "^</" xend :move)
        (user-error "A line cannot start with a closing tag.")))
    (progn
      (goto-char xbeg)
      (while (re-search-forward "<\\([A-Za-z][A-Za-z0-9]*\\)" xend :move)
        (let ((xhead (match-string 1))
              (xinline-tags '("a" "abbr" "b" "bdi" "bdo" "br" "cite" "code" "data" "del" "dfn" "em" "i" "ins" "kbd" "label" "mark" "meter" "output" "progress" "q" "ruby" "s" "samp" "small" "span" "strong" "sub" "sup" "time" "u" "var" "wbr")))
          (when (not (string-match (regexp-opt xinline-tags 'words) xhead))
            (warn "A line contains a block opening tag %s." xhead)))))
    (setq xinput (buffer-substring-no-properties xbeg xend))
    (setq xsepRegex (if ItemSepRegex ItemSepRegex "\n"))
    (let (xitems xsList xlistStr xhasEmptyLine)
      (setq xhasEmptyLine (if (string-match "\n\n" xinput) t nil))
      (setq xitems (split-string xinput xsepRegex t " +"))
      (setq xsList (mapcar (lambda (x) (format "<li>%s</li>" (string-trim x))) xitems))
      (setq xlistStr (mapconcat 'identity xsList (if xhasEmptyLine "\n\n" "\n")))
      (save-restriction
        (narrow-to-region xbeg xend)
        (delete-region (point-min) (point-max))
        (insert
         (let ((xsep2 (if xhasEmptyLine "\n\n" "\n")))
           (if zordered-list
               (concat "<ol>" xsep2 xlistStr xsep2 "</ol>")
             (concat "<ul>" xsep2 xlistStr xsep2 "</ul>"))))
        (progn
          (progn
            (goto-char (point-min))
            (while (re-search-forward "<li>[-.•*] " nil 1)
              (replace-match "<li>" t t)))
          (progn
            (goto-char (point-min))
            (while (re-search-forward "<br /></li>" nil 1)
              (replace-match "</li>" t t))))
        (goto-char (point-max)))
      (when zinteract (skip-chars-forward " \n")))))