RSS Feed

Lisp Project of the Day

geneva

You can support this project by donating at:

Donate using PatreonDonate using Liberapay

Or see the list of project sponsors.

genevadocumentation

Documentation😀
Docstrings😀
Tests 😀
Examples😀
RepositoryActivity🥺
CI 🥺

This is yet another project by Max Rottenkolber (@eugeneia_). Geneva is the documentation system. It includes a few subsystems:

  • the core;
  • the markup language;
  • renderers for HTML, LaTeX and plain text.

Geneva separates a document structure from its representation.

Core package provides the way to define a document's structure using calls to Lisp functions:

POFTHEDAY> (geneva:make-document
            (list
             (geneva:make-section (list "First section")
                (list
                 (geneva:make-paragraph (list "Foo bar"))
                 (geneva:make-paragraph (list "Blah minor"))))
             (geneva:make-section (list "Second section")
                 (list
                  (geneva:make-paragraph (list (geneva:make-bold "Hello World!")))))))
((:SECTION ("First section")
  ((:PARAGRAPH ("Foo bar"))
   (:PARAGRAPH ("Blah minor"))))
 (:SECTION ("Second section")
  ((:PARAGRAPH ((:BOLD "Hello World!"))))))

POFTHEDAY> (geneva.html:render-html * :index-p nil)

As you can see, the document is a bunch of Lisp lists. It easily can be rendered into the HTML:

Code

<SECTION>
  <HEADER>
    <A NAME="section-1">
      <H1><SPAN CLASS="geneva-index">1</SPAN> First section</H1>
    </A>
  </HEADER>
  <P>Foo bar</P>
  <P>Blah minor</P>
</SECTION>

<SECTION>
  <HEADER>
    <A NAME="section-2">
      <H1><SPAN CLASS="geneva-index">2</SPAN> Second section</H1>
    </A>
  </HEADER>
  <P><B>Hello World!</B></P>
</SECTION>

Result

1 First section

Foo bar

Blah minor

2 Second section

Hello World!

Or you might want to render it into the plain text:

POFTHEDAY> (geneva.plain-text:render-plain-text * :index-p nil)

1 First section

   Foo bar

   Blah minor

2 Second section

   Hello World!

Humans usually prefer to use specialized markup languages. Geneva provides MK2 markup language to define a rich text. For example, text from the previous example can be written like that:

POFTHEDAY> (let ((text "
< First section

  Foo bar

  Blah minor

>

< Second section

  *Hello World!*

>"))
             (with-input-from-string (s text)
               (geneva.mk2:read-mk2 s)))
((:SECTION ("First section")
  ((:PARAGRAPH ("Foo bar"))
   (:PARAGRAPH ("Blah minor"))))
 (:SECTION ("Second section")
  ((:PARAGRAPH ((:BOLD "Hello World!"))))))

There is also a system to process documentation strings into the Geneva document. It can be used to render documentation for your own system. Docstrings can be written in MK2 markup.

Now we'll create a test Lisp package and fill it with a variable, function and macro.

POFTHEDAY> (defpackage foo
             (:use #:cl)
             (:documentation "This is an example
                              of the package.

                              Documentation can be written in *MK2 format*.

                              And include _rich_ text with links."))

POFTHEDAY> (defun foo::bar (minor)
             "Makes some tranformation.

              *Arguments*:

              {minor} - an object to transform."

             ;; do the job
             (values))

POFTHEDAY> (defvar foo::*blah* :blah
             "Switches between two modes: {:blah} and {:minor}")

POFTHEDAY> (defmacro foo::with-minor (&body body)
             "Runs {body} with {:minor} mode applied."
             `(let ((foo::*blah* :minor))
                ,@body))

POFTHEDAY> (export '(foo::*blah* foo::bar foo::with-minor)
                   :foo)

Now we can build documentation for this package in two simple steps:

POFTHEDAY> (geneva.cl:api-document :foo)
((:SECTION ("foo (Package)")
  ((:PARAGRAPH ("This is an example of the package."))
   (:PARAGRAPH ("Documentation can be written in " (:BOLD "MK2 format") "."))
   (:PARAGRAPH ("And include " (:ITALIC "rich") " text with links."))
   (:SECTION ("*blah* (Variable)")
    ((:PARAGRAPH ((:BOLD "Initial Value:")))
     (:PARAGRAPH ((:FIXED-WIDTH ":BLAH")))
     (:PARAGRAPH
      ("Switches between two modes: " (:FIXED-WIDTH ":blah") " and "
       (:FIXED-WIDTH ":minor")))))
   (:SECTION ("bar (Function)")
    ((:PARAGRAPH ((:BOLD "Syntax:")))
     (:PARAGRAPH ("— Function: " (:BOLD "bar") " " (:ITALIC "minor")))
     (:PARAGRAPH ("Makes some tranformation."))
     (:PARAGRAPH ((:BOLD "Arguments") ":"))
     (:PARAGRAPH ((:FIXED-WIDTH "minor") " - an object to transform."))))
   (:SECTION ("with-minor (Macro)")
    ((:PARAGRAPH ((:BOLD "Syntax:")))
     (:PARAGRAPH
      ("— Macro: " (:BOLD "with-minor") " " (:FIXED-WIDTH "&body") " "
       (:ITALIC "body")))
     (:PARAGRAPH
      ("Runs " (:FIXED-WIDTH "body") " with " (:FIXED-WIDTH ":minor")
       " mode applied.")))))))

POFTHEDAY> (geneva.html:render-html * :index-p nil)

It will render this HTML:

1 foo (Package)

This is an example of the package.

Documentation can be written in MK2 format.

And include rich text with links.

1.1 *blah* (Variable)

Initial Value:

:BLAH

Switches between two modes: :blah and :minor

1.2 bar (Function)

Syntax:

— Function: bar minor

Makes some tranformation.

Arguments:

minor - an object to transform.

1.3 with-minor (Macro)

Syntax:

— Macro: with-minor &body body

Runs body with :minor mode applied.

Of cause, you can provide your own CSS stylesheets to make the page looks like you want.

I think Geneva might become a great replacement to reStructured text for documentation of my own libraries. Thank you, @eugeneia_!.

Though, it would be wonderful to add a little extensibility and ability to cross-referencing between different documentation sections.

By the way, in Geneva's sources I found an interesting way to keep DRY principle. This piece of code reuses (content-values text-token) 5 times.

(defun render-text (text)
  "Render TEXT as HTML."
  (dolist (text-token text)
    (ecase (content-type text-token)
      (:plain (text #1=(content-values text-token)))
      (:bold (b #1#))
      (:italic (i #1#))
      (:fixed-width (code #1#))
      (:url (multiple-value-bind (string url) #1#
              (a [:href (or url string)] (or string url)))))))

I don't know the proper name of the Lisp's feature, but it allows to refer to the piece of data-structure defined earslier. The most common usage I've seen before is a circular list's definition:

POFTHEDAY> '#1=(1 2 3 . #1#)
(1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
 1 2 3 1 2 ...)

Update from [2020-11-19 Thu]

I've created an example project which uses Geneva and MK2 markup to build a documentation.

It also shows how to render docs using GitHub Actions.


Brought to you by 40Ants under Creative Commons License