Emacs markdown-mode in-browser preview with GitHub’s theme

posted on 2022-09-18

Just for the reference - a snippet that configures markdown-mode so the export and live in-browser preview (using impatient-mode) are using the same CSS template (in this case on of GitHub’s).

Some of the solutions out there use JavaScript library to render the markdown but this will work against impatient-mode’s ability to keep the preview scrolled where you had scrolled in the web browser. So this approach uses the markdown rendering on the Emacs side and just wraps the output sent to impatient-mode in proper HTML that references CSS theme file.

First markdown-mode configuration and making sure the markdown-export function will produce output referencing the CSS:

(use-package markdown-mode
  :hook ((markdown-mode . auto-fill-mode))
  :mode ((".md\\'" . gfm-mode))
  :config
  (setq
   markdown-enable-wiki-links t
   markdown-italic-underscore t
   markdown-asymmetric-header t
   markdown-make-gfm-checkboxes-buttons t
   markdown-gfm-uppercase-checkbox t
   markdown-enable-math t
   markdown-content-type "application/xhtml+xml"
   markdown-css-paths '("https://cdn.jsdelivr.net/npm/github-markdown-css/github-markdown.min.css")
   markdown-xhtml-header-content "
      <style>
      body {
        box-sizing: border-box;
        max-width: 740px;
        width: 100%;
        margin: 40px auto;
        padding: 0 10px;
      }
      </style>
      <script>
      document.addEventListener('DOMContentLoaded', () => {
        document.body.classList.add('markdown-body');
      });
      </script>
      " ))

Next goes a filter function for impatient-mode to produce the same output:

(defun markdown-filter-impatient-mode (buffer)
  "Markdown filter for impatient-mode"
  (princ
   (with-temp-buffer
     (let ((tmpname (buffer-name)))
       (set-buffer buffer)
       (set-buffer (markdown tmpname))
       (format "
 <!DOCTYPE html>
  <html>
  <head>
      <title>Markdown Preview</title>
      <meta name='viewport' content=
      'width=device-width, initial-scale=1'>
      <link rel='stylesheet' href=
      'https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/4.0.0/github-markdown.min.css'
      integrity=
      'sha512-Oy18vBnbSJkXTndr2n6lDMO5NN31UljR8e/ICzVPrGpSud4Gkckb8yUpqhKuUNoE+o9gAb4O/rAxxw1ojyUVzg=='
      crossorigin='anonymous'>
      <!-- https://github.com/sindresorhus/github-markdown-css -->
      <link rel='stylesheet' href=
      'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/styles/github.min.css'>
      <!-- https://highlightjs.org -->

      <style>
      .markdown-body {
          box-sizing: border-box;
          margin: 0 auto;
          max-width: 980px;
          min-width: 200px;
          padding: 45px;
       }

       @media (max-width: 767px) {
           .markdown-body {
               padding: 15px;
           }
       }
      </style>
  </head>
  <body>
      <article class='markdown-body'>
          %s
      </article>
      <script src=
      'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.2.0/highlight.min.js'></script>

      <script>

      hljs.highlightAll();
      </script>
  </body>
  </html>"
               (buffer-string))))
   (current-buffer)))

And the last is a utility function we can call while editing markdown file to start the local webserver, set up impatient-mode’s filter and visit the preview in the web browser:

(defun impatient-markdown-preview ()
  (interactive)
  (impatient-mode)
  (imp-set-user-filter `markdown-filter-impatient-mode)
  (httpd-start)
  (imp-visit-buffer))

You can now bind impatient-markdown-preview function or execute it from the M-x command prompt. Thanks to impatient-mode’s the preview will keep updating as you type.

Happy hacking!