Let’s say you have linearized column data like this:
First name Last name Age -- Barry Yeengdull 33 -- Helga Madbroom 44 -- Zelma Landbeck 55 -- Peyton Glonham 66
(names courtesy of Fictional Character Name Generator) and you want to make a HTML table out of it that will be displayed like this:
First name | Last name | Age |
---|---|---|
Barry | Yeengdull | 33 |
Helga | Madbroom | 44 |
Zelma | Landbeck | 55 |
Peyton | Glonham | 66 |
The HTML for the above table would be:
<table> <tr><th>First name</th><th>Last name</th><th>Age</th></tr> <tr><td>Barry</td><td>Yeengdull</td><td>33</td></tr> <tr><td>Helga</td><td>Madbroom</td><td>44</td></tr> <tr><td>Zelma</td><td>Landbeck</td><td>55</td></tr> <tr><td>Peyton</td><td>Glonham</td><td>66</td></tr> </table>
Doing it in elisp
Here’s a general overview of what needs to be done:
- Split the text by delimiters (in the above case, it’s
--
) to get a list of rows by column - Split column strings into cells
- Transpose columns
- Write the HTML out
Let’s do it one by one.
Split the text by delimiters (in the above case, it’s --
) to get a list of rows by column
Elisp has a function called split-string
that we can use for this purpose – see the docs in the elisp manual. For example, we can do this:
(defun icyrock-get-column-strs (str delim) (split-string str (concat "\n?" delim "\n?"))) ;; Tests (assert (equal (icyrock-get-column-strs "1\n2\n3\n--\n4\n5\n6\n--\n7\n8\n9" "--") '("1\n2\n3" "4\n5\n6" "7\n8\n9")))
which will:
- Split the given string:
1 2 3 -- 4 5 6 -- 7 8 9
into lines using split-string. The result would be a list of strings, i.e. ("1\n\2\n3" "4\n5\n6" "7\n8\n9")
Split column strings into cells
Now that we have a list of strings, each representing a column, we can split each column into cells, again using split-string:
(defun icyrock-split-column-strs (strs) (mapcar (lambda (col-str) (split-string col-str "\n")) strs)) (defun icyrock-get-columns-from-string (str delim) (icyrock-split-column-strs (icyrock-get-column-strs str delim))) ;; Tests (assert (equal (icyrock-get-columns-from-string "1\n2\n3\n--\n4\n5\n6\n--\n7\n8\n9" "--") '(("1" "2" "3") ("4" "5" "6") ("7" "8" "9"))))
Function icyrock-split-column-strs
splits the list of column strings, so we get columns. icyrock-get-columns-from-string
is a combination of this one and icyrock-get-column-strs
, which, given our starting string, generates a list of cells ((1 2 3) (4 5 6) (7 8 9))
.
Transpose columns
The list we got ((1 2 3) (4 5 6) (7 8 9))
seems ordered, however note that this is ordered by column, while we want it ordered by row. The list that we want is a transposition of this, i.e. ((1 4 7) (2 5 8) (3 6 9))
.
(defun icyrock-transpose-table (table) (apply 'mapcar* 'list table)) (defun icyrock-get-rows-from-string (str delim) (icyrock-transpose-table (icyrock-get-columns-from-string str delim))) ;; Tests (assert (equal (icyrock-get-rows-from-string "1\n2\n3\n--\n4\n5\n6\n--\n7\n8\n9" "--") '(("1" "4" "7") ("2" "5" "8") ("3" "6" "9"))))
Table transposition is done simply via mapcar* function, which does exactly what we need.
mapcar* takes the first element (a car in (e)lisp jargon) of all supplied arguments and applies the given function to it. To explain the above, try this:
(message (format "%s" (mapcar* 'list '(1 a x) '(2 b y) '(3 c z))))
What the above does is:
- Get the list of first elements of supplied arguments, yielding
(1 2 3)
- Call the supplied function (“list” in this case) with them – effectively running
(list 1 2 3)
- Append that to the resulting list
- Repeat, going the next list of elements, which in this case is
(a b c)
- …
The result in this case is the following list: ((1 2 3) (a b c) (x y z))
.
Now, to transpose any list, what we need to do is exactly the above – take a look at the starting lists and the ending list. The only thing we need is to call mapcar* with the list of those arguments. This is exactly what apply does. Try this:
(message (format "%s" (apply '+ '(1 2 3))))
The above prints 6. In other words – (apply '+ '(1 2 3))
is the same as (+ 1 2 3)
. To generalize – (apply 'func '(a1 a2 ... aN))
is the same as (func a1 a2 ... aN)
, where func is any function and a1 through aN are its arguments.
Write the HTML out
Writing HTML output is fairly easy:
(defun icyrock-html-td-from-string (str) (format "<td>%s</td>" str)) (defun icyrock-list-to-string (list &optional sep) (mapconcat 'identity list sep)) (defun icyrock-html-tr-from-list (list) (format "<tr>%s</tr>" (icyrock-list-to-string (mapcar 'icyrock-html-td-from-string list)))) (defun icyrock-html-table-from-rows (table) (format "<table>\n%s\n</table>" (icyrock-list-to-string (mapcar 'icyrock-html-tr-from-list table) "\n"))) (defun icyrock-table-from-linearized-string (str delim) (icyrock-html-table-from-rows (icyrock-get-rows-from-string str delim))) ;; Tests (assert (equal (icyrock-table-from-linearized-string "1\n2\n3\n--\n4\n5\n6\n--\n7\n8\n9" "--") (concat "<table>\n" "<tr><td>1</td><td>4</td><td>7</td></tr>\n" "<tr><td>2</td><td>5</td><td>8</td></tr>\n" "<tr><td>3</td><td>6</td><td>9</td></tr>\n" "</table>")))
The above produces the following:
<table> <tr><td>1</td><td>4</td><td>7</td></tr> <tr><td>2</td><td>5</td><td>8</td></tr> <tr><td>3</td><td>6</td><td>9</td></tr> </table>
Making it interactive
Now, obviously you’d want to use this while editing in Emacs. That is, write some linearized list, select the region and apply our function.
Here are the steps:
- Get the currently selected region
- Apply the above function to that, so you get the needed HTML
- Overwrite the region with the HTML code. Alternatively, you can append after the region
(defun icyrock-make-html-table-from-current-region () (interactive) (save-excursion (let ((current-region-string (buffer-substring (mark) (point)))) (delete-region (mark) (point)) (goto-char (mark)) (insert (icyrock-table-from-linearized-string current-region-string "--"))))) (global-set-key (kbd "<f11>") 'icyrock-make-html-table-from-current-region)
Some comments for the above:
save-excursion
is used to save the state of the mark / point, so the user will not “notice” any changes after we do our job. Just a courtesy to the user – it’s very nice when you feel “just right” after everything was converted to a HTML table, instead of having to move your cursor around to where it was(interactive)
is needed so the function can be bound to a key(buffer-substring (mark) (point))
selects everything between the mark and point (essentially last two “important” cursor locations)- global-set-key function together with kbd function are used to bind this to a key
Now, if you are in a buffer which has the first test linearized table in this post, just select it and press F11 and you’ll get a nice formatted table.
Escaping
Obviously, if the table has some HTML-forbidden characters (such as >), the above will not work correctly. Some simple escaping can be done by changing the code like this:
(defun icyrock-html-escape-string (str) (let* ((s1 (replace-regexp-in-string "&" "&" str)) (s2 (replace-regexp-in-string "<" "<" s1)) (res (replace-regexp-in-string ">" ">" s2))) res)) (defun icyrock-html-td-from-string (str) (format "<td>%s</td>" (icyrock-html-escape-string str)))
Similar things
If you have a “text-version” of the HTML table, see this article for an approach Emacs Lisp: How to Write a make-html-table Command.