Making an Exercise and Solution Environment

1. The Problem

In a number of my org mode files I include exercise with hints and solutions. In the pdf files, which I make via \(\LaTeX\), I use the answers package print the exercises in the main text and the hints and solutions at the end of the file. However, when exporting the file to html via my website I would like to see the exercises, hints and solutions printed like so:

How to make an excercise environment like this?


And a hint like this?


To see how to do this, read on.

2. My Environment

I use emacs and org mode to write files, and nikola to make my homepage. After some searching on the web, I found the org-special-block-extras package which proved promissing for my goal. The related github page shows how to install it. Here are the steps I followed.

First I installed the org mode plugin for nikola. The file shows that the next command can used to convert an org to an html file that nikola can read.

/usr/bin/emacs --batch -l init.el --eval '(nikola-html-export "" "output.html")'

The org mode plugin also provides an init.el file that I used as a starting point.

The init.el file contains the line (setq package-load-list '((htmlize t))). This should be removed. Why?


What does package-load-list do?


I need to install org-special-block-extras, and this command prevents that.

3. The adapted init.el

Since I need functionality from org-special-block-extras, I have to update the init.el anyway, so here it is.

3.1. Loading the relevant packages

Since I use straight and use-package, I need to put this on the top of the init.el file. I copied this straight from my regular init.el

(defvar bootstrap-version)
(let ((bootstrap-file
       (expand-file-name "straight/repos/straight.el/bootstrap.el" user-emacs-directory))
      (bootstrap-version 5))
  (unless (file-exists-p bootstrap-file)
         'silent 'inhibit-cookies)
      (goto-char (point-max))
  (load bootstrap-file nil 'nomessage))
(setq package-enable-at-startup nil)

(straight-use-package 'use-package)
(setq straight-use-package-by-default t)

Next I load two packages. The hook on org-mode is necessary.

(use-package htmlize
  :ensure t)

(use-package org-special-block-extras
  :ensure t
  ;; All relevant Lisp functions are prefixed ‘o-’; e.g., `o-docs-insert'.
  :hook (org-mode . org-special-block-extras-mode)
     "The places where I keep my ‘#+documentation’"))

(require 'org)
(require 'ox-html)
(require 'org-special-block-extras)

3.2. Updating the code for box and details blocks

I adapted the fontsizes and the padding of the details block of org-special-block-extras. I just copied the relevant part of org-special-block-extras.el, removed the documentation string to keep this file small, and changed the padding and font size.

(org-defblock details (title "Details"
              background-color "#e5f5e5" title-color "green")
   (pcase backend
     (`latex (concat (pcase (substring background-color 0 1)
                       ("#" (format "\\definecolor{osbe-bg}{HTML}{%s}" (substring background-color 1)))
                       (_ (format "\\colorlet{osbe-bg}{%s}" background-color)))
                     (pcase (substring title-color 0 1)
                       ("#" (format "\\definecolor{osbe-fg}{HTML}{%s}" (substring title-color 1)))
                       (_ (format "\\colorlet{osbe-fg}{%s}" title-color)))
                     (format "\\begin{quote}
                              \\begin{tcolorbox}[colback=osbe-bg,colframe=osbe-fg,title={%s},sharp corners,boxrule=0.4pt]
                \\end{quote}" title contents)))
     (_ (format "<details class=\"code-details\"
                 style =\"padding: 0.6em;
                          background-color: %s;
                          border-radius: 15px;
                          color: hsl(157 75% 20%);
                          font-size: 1em;
                          box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;\">
                      <font face=\"Courier\" size=\"3\" color=\"%s\">
               </details>" background-color title-color title contents))))

And also for the box block.

(org-defblock box (title "" background-color nil shadow nil)
  (pcase backend
     (apply #'concat
            `("\\begin{tcolorbox}[title={" ,title "}"
              ",colback=" ,(pp-to-string (or background-color 'red!5!white))
              ",colframe=red!75!black, colbacktitle=yellow!50!red"
              ",coltitle=red!25!black, fonttitle=\\bfseries,"
              "subtitle style={boxrule=0.4pt, colback=yellow!50!red!25!white}]"
    ;; CSS syntax: “box-shadow: specification, specification, ...”
    ;; where a specification is of the shape “[inset] x_offset y_offset [blur [spread]] color”.
    (_ (-let [haze (lambda (left right deep-right deep-left)
                     (format "width: 50%%; margin: auto; box-shadow: %s"
                             (thread-last (list (cons right      "8px 6px 13px 8px %s")
                                                (cons left       "-16px 12px 20px 16px %s")
                                                (cons deep-right "48px 36px 71px 28px %s")
                                                (cons deep-left  "-48px -20px 71px 28px %s"))
                               (--filter (car it))
                               (--map (format (cdr it) (car it)))
                               (s-join ","))))]
         (format "<div style=\"%s\"> <h3>%s</h3> %s </div>"
                 (s-join ";" `( "padding: 0.5em;"
                               ,(format "background-color: %s" (org-subtle-colors (format "%s" (or background-color "green"))))
                               "border-radius: 15px"
                               "font-size: 1em"
                               ,(when shadow
                                   ((equal shadow t)
                                    (funcall haze "hsl(60, 100%, 50%)" "hsl(1, 100%, 50%)" "hsl(180, 100%, 50%)" nil))
                                   ((equal shadow 'inset)
                                    (funcall haze "inset hsl(60, 100%, 50%)" "inset hsl(1, 100%, 50%)" "inset hsl(180, 100%, 50%)" nil))
                                   ((or (stringp shadow) (symbolp shadow))
                                    (format "box-shadow: 10px 10px 20px 0px %s; width: 50%%; margin: auto" (pp-to-string shadow)))
                                   ((json-plist-p shadow)
                                    (-let [(&plist :left X :right Y :deep-right Z :deep-left W) shadow]
                                      (funcall haze X Y Z W)))
                                   (:otherwise (-let [(X Y Z W) shadow]
                                        (funcall haze X Y Z W)))))))
                 title contents)))))

3.3. Making the exercise, hint and solution blocks

The exercise, hint and solution blocks are now simple to implement (after I read the examples on org-special-blocks-extra).

(org-defblock exercise (title nil)
  (org-thread-blockcall raw-contents
    (box )))

(org-defblock solution (title "Solution")
  (org-thread-blockcall raw-contents
    (details title :title-color "red")))

(org-defblock hint (title "Hint")
  (org-thread-blockcall raw-contents
    (details title))) ; :title-color "red")))

I copied the above on top of the init.el file, and kept the rest.

4. Testing

Let's test this on this file. As this file sits in the posts we need to include the path to init.el.

/usr/bin/emacs --batch -l ../plugins/orgmode/init.el --eval '(nikola-html-export "" "../output/output.html")'