Exemplify-ERT: Clean examples that double as ERT declarations (Emacs package)

Below you find the latest version of (1) the package's README and (2) its main source file.

For the git repository and issues tracker, see the project's page on sr.ht.

For more packages, see Software.


README.org

Overview

Exemplify-ERT helps you write clean-looking examples that double as regression tests. Features:

  1. Write your tests as clean-looking equalities that don't have the noise of shoulds and should-nots — but still use ERT in the background.

    So instead of this:

    (ert-deftest + ()
      (should (equal (+ 21 21) 42))
      (should (equal (+ 30 12) 42))
      (should-error (eval '(+ "4" 2) nil)
                    :type 'error))
    
    (ert-deftest car ()
      (should (equal (car '(:a 1)) :a))
      (should-error (eval '(car) nil)
                    :type 'wrong-number-of-arguments))
    

    you simply write this:

    (exemplify-ert +
      (+ 21 21)  => 42
      (+ 30 12)  => 42
      (+ "4" 2) !!> error)
    
    (exemplify-ert car
      (car '(:a 1)) => :a
      (car) !!> wrong-number-of-arguments)
    

    So:

    • Easier to write tests.
    • Easier to read and understand tests at a glance.
    • Tests easily exportable as they are to be shown as usage examples.
  2. If only should-equal and should-error aren't enough for you, use a list of pre-defined symbols that correspond to pre-defined equality functions. Namely:

    '((   => . equal    )   ; regular equality
      (Hp==> . h-pr==   )   ; equality for hash tables       ├> Available in
      ( Hp=> . h-pr=    )   ; equality for hash tables       │  package 'xht
      ( H==> . h==      )   ; equality for hash tables       │  by the same
      (  H=> . h=       )   ; equality for hash tables       │  author. They
      ( H_=> . h_=      )   ; equality for hash tables       │  allow you to
      ( H~=> . h~=      )   ; equality for hash tables       │  compare objs
      (  O=> . h-orgtbl=)   ; equality for org table strings │  with same kv
      (  A=> . h-alist= )   ; equality for association lists │  pairs.
      (  P=> . h-plist= )   ; equality for property lists    │
      (  L=> . h-lol=   )   ; equality for lists of lists    │
      (  T=> . h-tsv=   )   ; equality for TSVs              │
      (  C=> . h-csv=   )   ; equality for CSVs              │
      (  S=> . h-ssv=   )   ; equality for SSVs              │
      (  J=> . h-json=  )   ; equality for JSONs             │
      (   ~> . exemplify-ert-approx=)) ; equality for floats
    

    So for example, you can write this:

    (exemplify-ert are-these-plists-equivalent-p
      '(:a 1 :b (:c 3))  P=>  '(:b (:c 3) :a 1)
      '(:a 1 :b 2)       P=>  '(:b 2 :a 1)
      '(:a 1 :b 2)       P=>  '(:b 2 :a 1 :a 4))
    

    which behind the scenes expands to this:

    (ert-deftest are-these-plists-equivalent-p nil
      (should (h-plist= '(:a 1 :b (:c 3))
                        '(:b (:c 3) :a 1)))
      (should (h-plist= '(:a 1 :b 2)
                        '(:b 2 :a 1)))
      (should (h-plist= '(:a 1 :b 2)
                        '(:b 2 :a 1 :a 4))))
    

    where h-plist= is a function from xht that tests the equivalence of plists.

    Because, you see:

    (plist-get '(:a 1 :b 2)      :a) => 1
    (plist-get '(:b 2 :a 1 :a 4) :a) => 1
    
    (plist-get '(:a 1 :b 2)      :b) => 2
    (plist-get '(:b 2 :a 1 :a 4) :b) => 2
    
  3. The format is compatible to be used by OrgReadme-fy to export your tests/examples into a nicely-formatted README.org. Like this:

    #+name: examples-+
    #+begin_src emacs-lisp
     (+ 21 21)  => 42
     (+ 30 12)  => 42
     (+ "4" 2) !!> error
    #+end_src
    
    #+name: examples-car
    #+begin_src emacs-lisp
     (car '(:a 1)) => :a
     (car) !!> wrong-number-of-arguments
    #+end_src
    
    #+name: examples-h-plist=
    #+begin_src emacs-lisp
     '(:a 1 :b (:c 3)) P=> '(:b (:c 3) :a 1)
     '(:a 1 :b 2)      P=> '(:b 2 :a 1)
     '(:a 1 :b 2)      P=> '(:b 2 :a 1 :a 4)
    #+end_src
    

Is Exemplify-ERT for you?

More so in proportion to how many of the following questions you can answer ‘yes’ to:

  • Are you going to write regression tests for your Emacs project?
  • Can most of them be expressed as equalities (or error)?
  • Would you like to be able to export them automatically to your library's README (with OrgReadme-fy)?
  • Does the way that the examples are represented in xht's README.org please you?

Background

The idea for this package came from seeing how clean Dash's ERT tests looked, then understanding what was going on by looking at dev/dash-defs.el. I saw that all the functions' descriptions and examples in Dash's README.md were being automatically generated from dash.el and dev/examples.el.

I found that interesting. Much later, when writing xht (an extensive hash tables library for Emacs), I slightly adapted it so that I could more easily and cleanly write my own ERT tests for it.

It worked quite well. My xht/dev/examples.el ended up with more than 9000 lines: I personally wrote more than 1800 such tests for it in this format.

I realized that dash-defs.el was doing two different things that were closely related but fundamentally independent:

  • Providing macros for writing cleaner ERT tests that look like, and double as, examples.
  • Providing functions for automatically generating documentation from these examples and from functions' docstrings.

To build on and further develop these ideas, I wrote two packages: Exemplify-ERT and OrgReadme-fy.

Exemplify-ERT and OrgReadme-fy

These two packages are independent from, but complementary to, each other:

  • Exemplify-ERT is about making it more pleasant to write ERT tests for your project. You can use it just for that, even if you don't plan to ever write a README.
  • OrgReadme-fy is about bootstrapping a README.org from what you already have. If you do have an examples.el created with Exemplify-ERT, it will use that to fetch your examples. If you don't, that's fine: you can use it just for generating the initial skeleton and (more importantly) for automatically producing tables and subtrees with the information that is already there in your package's functions and docstrings.

If you use them together for your library, the effect will be that:

  1. Your dev/examples.el file will have readable examples that double as ERT tests.
  2. Your elisp file will have code (defuns, defvars, defcustoms) plus its documentation.
  3. Your README.org will use both this documentation and the examples from dev/examples.el.

Expanding the above a bit:

  • The elisp file will be the single source of truth for documentation about the functions and commands. This documentation will be seen in context, either inside (as doc strings) or before (as orgreadme-fy-describe strings) the functions' code.
  • This very same documentation can be automatically translated to your README.org. But here, instead of the functions' code, you'll have examples of usage of the functions, coming straight from your dev/examples.el file.
  • Your dev/examples.el file doubles as ERT tests and examples of usage. Because of the clean syntax they use, these examples are imported to the README.org exactly as they are.
  • Hierarchical headers in the elisp file (";;;+") will be automatically translated as hierarchical headings for the Commands and Functions headings of your README.org.
  • Your README.org is generated from a readme-template.org. This file has the code blocks that import both the documentation and the examples.
    • You can configure these code blocks to your liking: what to exclude, at what depth the headings start, whether to include summary tables, where to insert them, spacing, etc.
    • You write there as well the contents of all other headings that you want to include in your README — everything that doesn't seem fit to be in the elisp file (as is the case of the Overview and Background headings you're reading right now).

Differences from Dash's implementation

In Exemplify-ERT some things are different.

Documentation structure and contents happens in the main library file, not in examples.el

I decided to rename the macro defexamples to exemplify-ert and leave them at top level in examples.el after removing def-example-groups. See OrgReadme-fy's README for a longer explanation why.

Flexibility for adding your own equality operators

You'll probably do fine with just equal or any of those in the exemplify-ert-ops-default alist shown above. But if you need more, just setq your (equality-symbol . equality-function) in your examples.el and you're done.

An examples.el template

Just launch M-x exemplify-ert-make-new and you don't have to start from scratch, nor fiddle with adapting dash-defs.el. Your examples.el will be created from a template, with some instructions and examples already there.

Other than that, the format is the same

Here's how it looks like, if you haven't seen it:

(exemplify-ert -uniq
  (-> "abracadabra" (append nil) -uniq  concat)  => "abrcd"
  (-uniq '(4 2 1 4 2 2 5 2 4 1 1 4 5 2))         => '(4 2 1 5)
  (-uniq '(1 1 1 1 1 1))                         => '(1)
  (-uniq '())                                    => '()
  (-uniq 42)                                    !!> error)

So the above exemplifies the use of -uniq to the reader and are ERT tests.

Installation

See my page Software for the most up-to-date instructions on how to download and install any of my Emacs packages.

Having downloaded and installed the package and its dependencies, adapt the configurations below to your init.el file.

(use-package exemplify-ert
  :demand t
  :config
  (global-exemplify-ert-fontify-mode))

Alternatively, if you don’t have ‘use-package’:

(require 'exemplify-ert)

Functions and commands

Here's an overview of this package's functions that are likely to be of your interest:

Function Summary
exemplify-ert Define ERT tests for function FUN and its EXAMPLES.
exemplify-ert-make-new Create an 'examples/tests' file for your new project.
exemplify-ert-fontify-mode Toggle fontification for Exemplify-Ert.
global-exemplify-ert-fontify-mode Toggle Exemplify-Ert-Fontify mode in all buffers.

They are described in more detail below.

Functions

From examples to ERT
exemplify-ert (fun &rest examples)

Define ERT tests for function FUN and its EXAMPLES.

This is the macro to use in your examples.el file.

Commands

Create examples/tests file
exemplify-ert-make-new ()

Create an 'examples/tests' file for your new project.

Run it interactively and confirm filename and directory.
(If in doubt, go with ./dev/examples.el as suggested.)

It will also write (if not yet existent) run-tests.sh. You can use
it to run the tests from the terminal in batch mode instead of, or
in addition to, running them interactively.

Minor mode
exemplify-ert-fontify-mode (&optional arg)

Toggle fontification for Exemplify-Ert.

This is a buffer-local minor mode intended for Emacs Lisp buffers.
Enabling it causes syntax highlighting of exemplify-ert macros and
equality operators.

See also exemplify-ert-fontify-mode-lighter and
global-exemplify-ert-fontify-mode.

global-exemplify-ert-fontify-mode (&optional arg)

Toggle Exemplify-Ert-Fontify mode in all buffers.

With prefix ARG, enable Global Exemplify-Ert-Fontify mode if ARG is positive;
otherwise, disable it. If called from Lisp, enable the mode if
ARG is omitted or nil.

Exemplify-Ert-Fontify mode is enabled in all buffers where
exemplify-ert--turn-on-fontify-mode would do it.
See exemplify-ert-fontify-mode for more information on Exemplify-Ert-Fontify mode.

Contributing

See my page Software for information about how to contribute to any of my Emacs packages.

News

0.6.0

New features
New commands
[BROKEN LINK: exemplify-ert-see-readme]

to open this library's local README.org.

[BROKEN LINK: exemplify-ert-see-news]

to open README.org, narrow into the News heading, skip to the latest, recenter, and show branches.

New equality operators
K=> for [BROKEN LINK: h-kvl=], which was missing
New in templates
Command to run all tests was added
Fixes
defgroup's group has been fixed
A few fixes in requires

0.5.0

Minor public release

License

This project follows the REUSE Specification (FAQ), which in turn is built upon SPDX.

Therefore, license and copyright information can be found in:

  • each file's comment header, or
  • an adjacent file of the same name with the additional extension .license, or
  • the .reuse/dep5 file

The full text of the licenses can be found in the LICENSES subdirectory.


exemplify-ert.el

Structure

;;; exemplify-ert.el --- Clean examples that double as ERT declarations -*- lexical-binding: t -*-
;;; Commentary:
;;;; For all the details, please do see the README
;;; Acknowledgments:
;;; Code:
;;;; Libraries
;;;; Obsolete symbols
;;;; Package metadata
;;;; Customizable variables
;;;; Other variables
;;;; Functions
;;;;; Equality-defining functions
;;;;; From examples to ERT
;;;;; Support
;;;; Commands
;;;;; Create examples/tests file
;;;;; Minor mode
;;;;; See README
;;;;; See News
;;;; Wrapping up
;;; exemplify-ert.el ends here

Contents

;;; exemplify-ert.el --- Clean examples that double as ERT declarations -*- lexical-binding: t -*-

;; SPDX-FileCopyrightText:  © flandrew <https://keyoxide.org/191F5B3E212EF1E515C19918AF32B9A5C1CCCB2D>
;; SPDX-License-Identifier: GPL-3.0-or-later

;;---------------------------------------------------------------------------
;; Author:            flandrew
;; Created:           2022-03-31
;; Version:           0.6.0
;; Homepage:          <https://flandrew.srht.site/listful/software.html>
;; Keywords:          maint, tools, lisp
;; Package-Requires:  ((emacs "25.1") (dash "2.14") (s "1.12") (f "0.20"))
;;---------------------------------------------------------------------------

;; This file is part of Exemplify-ERT, which is NOT part of GNU Emacs.

;; This program is free software: you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by the Free
;; Software Foundation, either version 3 of the License, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful, but WITHOUT
;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
;; FITNESS FOR A PARTICULAR PURPOSE. For more details, see the full license at
;; either LICENSES/GPL-3.0-or-later.txt or <https://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Exemplify-ERT helps you write regression tests that double as clean-looking
;; examples. Features:
;;
;; - Write your tests as clean-looking equalities that don't have the noise of
;;   shoulds and should-nots — but still uses ERT in the background.
;;
;; - If should-equal and should-error aren't enough for you, use a list
;;   of pre-defined symbols that correspond to pre-defined equality functions.
;;
;; It's independent from, but goes hand-in-hand with, OrgReadme-fy, in case
;; you wish to export your tests/examples into a nicely-formatted README.org.
;;
;;;; For all the details, please do see the README
;;
;; Open it easily with any of the below:
;;   (find-file-read-only              "README.org")   <--- C-x C-e here¹, or
;;   (find-file-read-only-other-frame  "README.org")   <--- C-x C-e here¹, or
;;   (find-file-read-only-other-window "README.org")   <--- C-x C-e here¹
;;
;; or read it online:
;;   <https://flandrew.srht.site/listful/sw-emacs-exemplify-ert.html>
;;
;; ¹ or the key that ‘eval-last-sexp’ is bound to, if not C-x C-e.
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; Acknowledgments:
;;
;; The idea for this library came after seeing how clean Dash's ERT tests
;; looked in Dash's dev/examples.el. It uses code from there.
;;
;; After using the examples structure myself while writing XHT, I decided to
;; further develop the idea. The result was two independent packages that go
;; well together: Exemplify-ERT and OrgReadme-fy.
;;
;; A few of this library's functions have borrowed and changed code used in
;; Dash development, most notably the ‘exemplify-ert’ macro itself,
;; exemplify-ert-approx=’, ‘exemplify-ert-example->test’, and the
;; exemplify-ert-fontify-mode’.
;;
;; My thanks to Magnar Sveen and the other Dash contributors.
;;
;; About dash:
;;   SPDX-FileCopyrightText:  © 2012 Free Software Foundation, Inc.
;;   SPDX-License-Identifier: GPL-3.0-or-later
;;   Author:                  Magnar Sveen
;;   Homepage:                <https://github.com/magnars/dash.el>
;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;; Code:
;;;; Libraries

(require 'f)
(require 's)
(require 'rx)
(require 'ert)
(require 'dash)
(require 'lisp-mnt)  ; lm-summary’, ‘lm-version’, ‘lm-homepage

;;;; Obsolete symbols

(define-obsolete-function-alias 'approx= 'exemplify-ert-approx= "0.6")

;;;; Package metadata

(defconst exemplify-ert--name "Exemplify-ERT")

(defconst exemplify-ert--dot-el
  (format "%s.el" (file-name-sans-extension (eval-and-compile
                                              (or load-file-name
                                                  buffer-file-name)))))
(defconst exemplify-ert--readme-org
  (expand-file-name "README.org" (file-name-directory exemplify-ert--dot-el)))

(defconst exemplify-ert--summary  (lm-summary  exemplify-ert--dot-el))
(defconst exemplify-ert--version  (lm-version  exemplify-ert--dot-el))
(defconst exemplify-ert--homepage (lm-homepage exemplify-ert--dot-el))

;;;; Customizable variables

(defgroup exemplify-ert nil
  (format "%s." exemplify-ert--summary)
  :group 'lisp
  :link  '(emacs-library-link :tag "Lisp file" "exemplify-ert.el")
  :link  `(file-link :tag "README.org" ,exemplify-ert--readme-org)
  :link  `(url-link  :tag "Homepage"   ,exemplify-ert--homepage))

(defcustom exemplify-ert-dev-subdir "dev"
  "Subdirectory that will have your examples file.
The examples file's name is defined by ‘exemplify-ert-examples-filename’."
  :type  'string
  :group 'exemplify-ert)

(defcustom exemplify-ert-examples-filename "examples.el"
  "Name of your examples file.
It will be stored under ‘exemplify-ert-dev-subdir’."
  :type  'string
  :group 'exemplify-ert)

;; See also, under Minor mode:
;; exemplify-ert-fontlock-add-equality-operators’,
;; exemplify-ert-fontlock-add-exemplify-ert-macros’,
;; exemplify-ert-fontify-mode-lighter’.
;; exemplify-ert-enable-fontlock

;;;; Other variables

(defconst exemplify-ert-ops-default
  '((   => . equal    )   ; regular equality
    (Hp==> . h-pr==   )   ; equality for hash tables       ├> Available in
    ( Hp=> . h-pr=    )   ; equality for hash tables       │  package 'xht
    ( H==> . h==      )   ; equality for hash tables       │  by the same
    (  H=> . h=       )   ; equality for hash tables       │  author. They
    ( H_=> . h_=      )   ; equality for hash tables       │  allow you to
    ( H~=> . h~=      )   ; equality for hash tables       │  compare objs
    (  O=> . h-orgtbl=)   ; equality for org table strings │  with same kv
    (  A=> . h-alist= )   ; equality for association lists │  pairs.
    (  P=> . h-plist= )   ; equality for property lists    │
    (  L=> . h-lol=   )   ; equality for lists of lists    │
    (  T=> . h-tsv=   )   ; equality for TSVs              │
    (  C=> . h-csv=   )   ; equality for CSVs              │
    (  S=> . h-ssv=   )   ; equality for SSVs              │
    (  J=> . h-json=  )   ; equality for JSONs             │
    (  K=> . h-kvl=   )   ; equality for key–value lines   │
    (   ~> . exemplify-ert-approx=)) ; equality for floats
  "An alist of equality operators that can be used for your examples.

To use any of these starting with 'h-' you'll need to (require 'xht) in your
examples.el. They allow the comparison of hash tables, alists and others. You
don't need to use them, and they being on this list will cause no errors.

The most common one is ‘=>’, which will use ‘equal’ to compare both sides of
the equality. The ‘~>’ one allows small differences between floats. Use ‘!!>
to signal error.

You may add new ones, or even override these, by customizing the variable
exemplify-ert-ops-user’.")

(defvar exemplify-ert-ops-user nil
  "An alist of user-added equality operators.

You may add new ones according to what you normally use. This list will be
checked before ‘exemplify-ert-ops-default’, so you may override the defaults if
you want (though I'd suggest to pick some different symbol instead).

To add your own operators, do it directly in the examples.el file. It should
look somewhat like this:

  (setq exemplify-ert-ops-user '(( str=> . string=   )
                             (char=> . char-equal)))

After which you can now add examples such as:

  (myfunc1 x y     str=>  \"xy\")
  (myfunc2 ?x ?y  char=>  ?x)

\(Although note that plain ‘equal’, which is behind the ‘=>’, could
accomplish the above already, so you probably don't need these two
new equalities.)")

(defvar exemplify-ert-epsilon 1e-15
  "Epsilon used in ‘exemplify-ert-approx=’.")

;;;; Functions
;;;;; Equality-defining functions

(defun exemplify-ert-approx= (u v)
  "Like ‘=’, but compares floats U and V within ‘exemplify-ert-epsilon’.
This allows approximate comparison of floats to work around
differences in implementation between systems. Used in place of
equal’ when testing actual and expected values with ‘~>’."
  (or (= u v)
      (< (/ (abs (- u v))
            (max (abs u) (abs v)))
         exemplify-ert-epsilon)))

;;;;; From examples to ERT

(defun exemplify-ert-example->test (example)
  "Return an ERT assertion form based on EXAMPLE."
  (pcase example
    (`(,actual !!> ,(and (pred symbolp) expected))
     ;; FIXME: Tests fail on Emacs 24-25 without ‘eval’ for some reason.
     `(should-error (eval ',actual ,lexical-binding)
                    :type ',expected))
    (`(,actual !!> ,expected)
     `(should (equal (should-error ,actual) ',expected)))
    (_
     (-let [(actual op expected) example]
       (let* ((alist (-concat exemplify-ert-ops-user
                              exemplify-ert-ops-default))
              (fun   (cdr (assoc op alist)))
              (ops   (-map #'car alist)))
         (unless (member op ops)
           (error "Invalid test case: %S" example))
         `(should (,fun ,actual ,expected)))))))

(defmacro exemplify-ert (fun &rest examples)
  "Define ERT tests for function FUN and its EXAMPLES.
This is the macro to use in your examples.el file."
  (declare (indent defun))
  (setq examples (-partition 3 examples))
  `(ert-deftest ,fun ()
     ,@(mapcar #'exemplify-ert-example->test examples)))

;;;;; Support

(defun exemplify-ert-libname->libfile (libname)
  "Given LIBNAME, load library and return filename it loaded from."
  (require (if (symbolp libname)
               libname
             (intern libname)))
  (--> load-history
       (-map #'car it)
       (car (-filter (lambda (lib)
                       (when (string-match
                              (format "/%s.el[cn]*$" libname)
                              lib)
                         lib))
                     it))))

(defun exemplify-ert--rw-template (from to repl)
  "Read template and make replacements.
Read template in file FROM, then write to TO after replacing
$PACKAGE with REPL."
  (--> from  f-read  (s-replace "$PACKAGE" repl it)
       (f-write it 'utf-8 to)))

;;;; Commands
;;;;; Create examples/tests file

;;;###autoload
(defun exemplify-ert-make-new ()
  "Create an 'examples/tests' file for your new project.
Run it interactively and confirm filename and directory.
\(If in doubt, go with ./dev/examples.el as suggested.)

It will also write (if not yet existent) run-tests.sh. You can use
it to run the tests from the terminal in batch mode instead of, or
in addition to, running them interactively."
  (interactive)
  (let* ((this  (exemplify-ert-libname->libfile 'exemplify-ert))
         (tmpl  (->> this  f-parent  (f-expand "templates/examples")))
         (rnts  (->> this  f-parent  (f-expand "templates/run-tests")))
         (exfl  (read-file-name
                 "Create examples file in...  " nil nil nil
                 (concat exemplify-ert-dev-subdir "/"
                         exemplify-ert-examples-filename)
                 nil))
         (pack  (read-from-minibuffer
                 "Confirm package name: "
                 (-> exfl  f-parent  f-parent  f-base)))
         (libf  (format "%s/%s.el"
                        (->> exfl  f-parent  f-parent
                             ;; in case the folder has .el
                             (s-chop-suffix ".el"))
                        ;; guess: libfile has same name as parent folder
                        pack))
         rtf)
    (mkdir (f-parent exfl) '-p)

    ;; run-tests.sh
    (setq rtf (->> exfl f-parent f-parent (f-expand "run-tests.sh")))
    (unless (f-exists? rtf)
      (exemplify-ert--rw-template rnts rtf pack)
      (set-file-modes rtf #o744))

    ;; examples.el
    (if (not (or (not (f-exists? exfl))
                 (and (y-or-n-p (s-lex-format
                                 "File ${exfl} exists. OVERWRITE?"))
                      (y-or-n-p (s-lex-format
                                 "Will overwrite ${exfl}. Are you sure?")))))
        (message "Ok, nothing done")
      (--> tmpl  f-read
           (s-replace-all `(("$PACKAGE" . ,pack)
                            ("$NAME"    . ,(if (f-exists? libf)
                                               (--> libf  lm-authors
                                                    (-map #'car it)
                                                    (s-join ", " it))
                                             "$NAME"))
                            ("$YEAR"    . ,(->> (decode-time)
                                                (nth 5)
                                                (format "%s"))))
                          it)
           (or (f-write it 'utf-8 exfl)
               (find-file exfl))))))

;;;;; Minor mode

(defcustom exemplify-ert-fontlock-add-equality-operators t
  "If non-nil, fontify equality-operators for def-examples."
  :group 'exemplify-ert
  :type 'boolean)

(defcustom exemplify-ert-fontlock-add-exemplify-ert-macros t
  "If non-nil, fontify ‘exemplify-ert’."
  :group 'exemplify-ert
  :type 'boolean)

(defun exemplify-ert--ops ()
  "Equality operators."
  (->> (-concat exemplify-ert-ops-user
                exemplify-ert-ops-default)
       (-map #'car)   (cons '!!>)   -uniq))

(defun exemplify-ert--fontlock-make-keywords ()
  "Make keywords for fontlock."
  (-non-nil
   (list (when exemplify-ert-fontlock-add-exemplify-ert-macros
           (exemplify-ert--fontlock-rx-exemplify-ert-macros))
         (when exemplify-ert-fontlock-add-equality-operators
           (exemplify-ert--fontlock-make-rx  (exemplify-ert--ops))))))

(defun exemplify-ert--fontlock-rx-exemplify-ert-macros ()
  "The rx for ‘exemplify-ert’ macros."
  `(,(rx ?\( (group (| "exemplify-ert")) symbol-end
         (+ (in "\t "))
         (group (* (| (syntax word) (syntax symbol) (: ?\\ nonl)))))
    (1 font-lock-keyword-face)
    (2 font-lock-function-name-face)))

(defun exemplify-ert--fontlock-make-rx (symbols-list)
  "Make rx using SYMBOLS-LIST."
  (let* ((strings  (-map #'symbol-name (-uniq symbols-list)))
         (rx-1    `(| ,@strings))
         (rx      `(rx symbol-start ,rx-1 symbol-end)))
    (eval rx)))

(defcustom exemplify-ert-fontify-mode-lighter nil
  "Mode line lighter for function ‘exemplify-ert-fontify-mode’.
Either a string to display in the mode line when function
exemplify-ert-fontify-mode’ is on, or nil to display
nothing (the default)."
  :group 'exemplify-ert
  :type '(choice (string :tag "Lighter" :value "")
                 (const  :tag "Nothing" nil)))

;;;###autoload
(define-minor-mode exemplify-ert-fontify-mode
  "Toggle fontification for Exemplify-Ert.
This is a buffer-local minor mode intended for Emacs Lisp buffers.
Enabling it causes syntax highlighting of exemplify-ert macros and
equality operators.

See also ‘exemplify-ert-fontify-mode-lighter’ and
global-exemplify-ert-fontify-mode’."
  :group 'exemplify-ert :lighter exemplify-ert-fontify-mode-lighter
  (if exemplify-ert-fontify-mode
      (font-lock-add-keywords nil (exemplify-ert--fontlock-make-keywords) t)
    (font-lock-remove-keywords nil (exemplify-ert--fontlock-make-keywords)))
  (cond ((fboundp 'font-lock-flush) ;; Added in Emacs 25.
         (font-lock-flush))
        ;; font-lock-fontify-buffer’ unconditionally enables
        ;; font-lock-mode’ and is marked ‘interactive-only’ in later
        ;; Emacs versions which have ‘font-lock-flush’, so we guard
        ;; and pacify as needed, respectively.
        (font-lock-mode
         (with-no-warnings
           (font-lock-fontify-buffer)))))

(defun exemplify-ert--turn-on-fontify-mode ()
  "Enable variable ‘exemplify-ert-fontify-mode’ if in an Emacs Lisp buffer."
  (when (derived-mode-p #'emacs-lisp-mode)
    (exemplify-ert-fontify-mode)))

;;;###autoload
(define-globalized-minor-mode global-exemplify-ert-fontify-mode
  exemplify-ert-fontify-mode exemplify-ert--turn-on-fontify-mode
  :group 'exemplify-ert)

(defcustom exemplify-ert-enable-fontlock nil
  "If t, fontify Exemplify-ERT's equality operators and ‘exemplify-ert’."
  :group 'exemplify-ert
  :set (lambda (sym val)
         (set-default sym val)
         (global-exemplify-ert-fontify-mode (if val 1 0)))
  :type 'boolean)

;;;;; See README

;;;###autoload
(defun exemplify-ert-see-readme (&optional heading narrow)
  "Open exemplify-ert's README.org file.
Search for the file in exemplify-ert.el's directory.

If found, open it read-only.

If optional argument HEADING is passed, try to navigate to the
heading after opening it. HEADING should be a string.

If optional argument NARROW is non-nil, narrow to that heading.
This argument has no effect if HEADING is nil or not found."
  (interactive)
  (let ((readme exemplify-ert--readme-org))
    (if (file-exists-p readme)
        (let ((pr (make-progress-reporter
                   (format "Opening %s ... "
                           (abbreviate-file-name readme)))))
          (find-file-read-only readme)
          (when heading
            (exemplify-ert--goto-org-heading heading narrow))
          (progress-reporter-done pr))
      (message "Couldn't find %s's README.org" exemplify-ert--name))))

(defun exemplify-ert--goto-org-heading (heading &optional narrow)
  "Navigate to org HEADING and optionally NARROW to it."
  (let* ((hrx (format "^[*]+ %s" heading))
         (pos (save-match-data
                (save-excursion
                  (save-restriction
                    (widen)
                    (goto-char (point-max))
                    (re-search-backward hrx nil t 1))))))
    (when pos
      (widen)
      (goto-char pos)
      (if (and narrow (fboundp 'org-narrow-to-subtree))
          (org-narrow-to-subtree)
        (recenter-top-bottom 1))
      (when (fboundp 'outline-show-subtree)
        (outline-show-subtree))
      (when (fboundp 'org-flag-drawer)
        (save-excursion
          (forward-line 1)
          (org-flag-drawer t))))))

;;;;; See News

;;;###autoload
(defun exemplify-ert-see-news ()
  "See the News in exemplify-ert's README.org file."
  (interactive)
  (exemplify-ert-see-readme "News" 'narrow)
  (exemplify-ert--display-org-subtree))

(defun exemplify-ert--display-org-subtree ()
  "Selectively display org subtree."
  (let ((cmds '(outline-hide-subtree
                outline-show-children
                outline-next-heading
                outline-show-branches)))
    (and (fboundp (nth 0 cmds))
         (fboundp (nth 1 cmds))
         (fboundp (nth 2 cmds))
         (fboundp (nth 3 cmds))
         (mapc #'funcall cmds))))

;;;; Wrapping up

(provide 'exemplify-ert)

;; Local Variables:
;; coding:                     utf-8
;; indent-tabs-mode:           nil
;; sentence-end-double-space:  nil
;; outline-regexp:             ";;;;* "
;; End:

;;; exemplify-ert.el ends here