Common Lisp Docs Builder
DOCS-BUILDER ASDF System Details
Description: A meta documentation builder for Common Lisp projects.
Licence: Unlicense
Author: Alexander Artemenko
Homepage: https://40ants.com/docs-builder
Bug tracker: https://github.com/40ants/docs-builder/issues
Source control: GIT
Depends on: 40ants-doc, alexandria, docs-config, log4cl, uiop
This system is a generic documentation builder for Common Lisp Systems.
It able to generate HTML
documentation for specified ASDF
system.
The idea is to use docs-builder
as an universal HTML
documentation builders
which can be used in a continuous integration pipeline. For example, it is
used inside build-docs GitHub action, which can be
used to build docs update gh-pages for any Common Lisp library (if it is uses
documentation generator supported by docs-builder
).
Currently Docs Builder supports only 40ants-doc
, MGL-PAX
and Geneva, but it
can be extended to support other documentation builders, covered by examples in here:
cl-doc-systems.github.io.
Usage
Documentation can be built in a few ways: from the lisp REPL
, command-line and
using the GitHub action.
REPL
From the REPL
, you need first to call a docs-builder:build
function:
Builds HTML
documentation for ASDF
system and returns absolute path to the dir with docs.
Inside, it will try to guess which documentation builder should be used:
Returns a builder object which can be passed to the docs-builder/builder:build
generic-function along with system.
The builder type is guessed using different euristics which depends on a documentation system.
If you want to add support for a new documentation generator, use defguesser
macro.
Then it will pass the builder object and ASDF
system to the docs-builder/builder:build
generic-function:
Builds HTML
documentation for ASDF
system and returns absolute path to the dir with docs.
Here is an example how to build documentation for :docs-builder
ASDF
system:
CL-USER> (docs-builder:build :docs-builder)
<INFO> [02:12:00] docs-builder/core core.lisp (build :before system) -
Building docs for system #<PACKAGE-INFERRED-SYSTEM "docs-builder"> found at /Users/art/projects/docs-builder/
<INFO> [02:12:00] docs-builder/builders/mgl-pax/builder builder.lisp (build builder system) -
Building docs in "/Users/art/projects/docs-builder/docs/build/" dir
#P"/Users/art/projects/docs-builder/docs/build/"
Command-line
You can use builder from command-line. To do this, first install it using Roswell:
# Note, we need to install this patched mgl-pax
# first, to be able to load docs-builder.
# This step in necessary until this pull
# will be merged:
# https://github.com/melisgl/mgl-pax/pull/8
$ ros install 40ants/doc
$ ros install 40ants/docs-builder
Here we call it to build documentation for "docs-builder" ASDF
system:
$ build-docs docs-builder
<INFO> [02:26:32] docs-builder/main main.lisp (main) -
Quickloading system "docs-builder"
<INFO> [02:26:34] docs-builder/core core.lisp (build :before system) -
Building docs for system #<PACKAGE-INFERRED-SYSTEM "docs-builder"> found at /Users/art/projects/docs-builder/
<INFO> [02:26:34] docs-builder/builders/mgl-pax/builder builder.lisp (build builder system) -
Building docs in "/Users/art/projects/docs-builder/docs/build/" dir
Scan was called 2146 times.
GitHub Action
If you host your project on the GitHub, then the most easy way to build and host documentation would be to use Github Pages.
To build docs and update the site, create a file .github/workflows/docs.yml
with a content like this:
name: 'Docs'
on:
# This will run tests on pushes
# to master branch and every monday:
push:
branches:
- 'main'
- 'master'
schedule:
- cron: '0 10 * * 1'
jobs:
build-docs:
runs-on: ubuntu-latest
env:
LISP: sbcl-bin
steps:
- uses: actions/checkout@v1
- uses: 40ants/setup-lisp@v1
- uses: 40ants/build-docs@v1
with:
asdf-system: cl-info
You'll find more info in the action's documentation.
Additional Params
You can customize a builder by defining a method for this generic function:
Should return a plist which will be passed as keyword arguments to the documentation builder when building docs for a given asdf-system.
Implement a method, EQL
specialized on a concrete ASDF
system.
Here is a typical method I use for my own libraries to set
a custom theme for 40ants-doc
system:
(defmethod docs-config ((system (eql (asdf:registered-system "cl-info"))))
;; 40ANTS-DOC-THEME-40ANTS system will bring
;; as dependency a full 40ANTS-DOC but we don't want
;; unnecessary dependencies here:
(uiop:symbol-call :ql :quickload :40ants-doc-theme-40ants)
(list :theme
(find-symbol "40ANTS-THEME"
(find-package "40ANTS-DOC-THEME-40ANTS"))))
Try to load additional dependencies inside the method. This users of your library will not download dependencies needed only for building documentation.
For some special cases it might be useful to return a special key DYNAMIC-BINDINGS
.
This could be useful, for configuring some custom extensions like it did with
interactive demos for the Weblocks. Here is how
a method looks like when I configure Weblocks documentation builder:
(defmethod docs-config ((system (eql (asdf:registered-system "weblocks"))))
;; ...
(list :theme
(find-symbol "40ANTS-THEME"
(find-package "40ANTS-DOC-THEME-40ANTS"))
:dynamic-bindings (list (cons 'weblocks/doc/example:*server-url*
;; When local examples server is running,
;; we'll be using it instead of production:
(unless weblocks/doc/example::*port*
"http://examples.40ants.com/"))))
Extending
Docs builder was made to be extensible and here we see how to add support for a new documentation generator. A documentation builder we'll use is a Geneva.
Some time ago I've wrote a template system to demonstrate how to
document ASDF
system using Geneva. Lets try to automate docs building and add Geneva support to the docs-builder
!
First, we need a way to detect if the system uses the Geneva. This is done using "guesser".
Let's define a guesser which will consider we are working with Geneva, if there is a docs/source/index.mk2
file. Files with *.mk2
extensions are special markup used by Geneva.
Add a Builder Class
First thing we need to do is to create a builder class.
Create a src/builders/geneva/builder.lisp
file with
following content:
(defclass builder ()
())
Guessing a Doc Generator
DOCS-BUILDER
uses a number of euristics to determine the CLOS
class
to be used for documentation generation. Euristics are incapulated in
"guessers". Let's create a src/builders/geneva/guesser.lisp
file
and define a simple guesser, which will return the builder
class defined
in the previous section if a file docs/sources/index.mk2
exists.
To define a guesser, we'll be using docs-builder/guesser:defguesser
macro:
(docs-builder/guesser:defguesser geneva (system)
(when (probe-file
(asdf:system-relative-pathname system
"docs/source/index.mk2"))
(uiop:symbol-call :ql :quickload :docs-builder/builders/geneva/builder
:silent t)
(make-instance (intern "BUILDER" "DOCS-BUILDER/BUILDERS/GENEVA/BUILDER"))))
When all rules matched, guesser loads the builder package and all its dependencies.
In our case, Geneva
system will be loaded.
Add this guesser file to the docs-builder.asd
file:
(defsystem "docs-builder"
:class :package-inferred-system
:author "Alexander Artemenko"
:license "Unlicense"
:pathname "src"
:description ""
:defsystem-depends-on ("40ants-doc")
:depends-on ("docs-builder/core"
"docs-builder/builders/40ants-doc/guesser"
"docs-builder/builders/mgl-pax/guesser"
"docs-builder/builders/geneva/guesser"))
This way, it will be loaded along with the primary system while
geneva/builder
and it's dependencies will be loaded only
if the system we are building documentation for is using Geneva.
Now we can call docs-builder/guesser:guess-builder
generic-function to create a builder for example
system:
CL-USER> (docs-builder/guesser:guess-builder :example)
#<DOCS-BUILDER/BUILDERS/GENEVA/BUILDER::BUILDER {1003D89313}>
Add a Build Method
Now open a src/builders/geneva/builder.lisp
file again and
add a method to the docs-builder/builder:build
generic-function.
The method should build HTML
documentation and return a path to the folder.
(defmethod docs-builder/builder:build ((builder builder) (system asdf:system))
(let* ((docs-source-dir
(asdf:system-relative-pathname system
"docs/source/"))
(docs-output-dir
(asdf:system-relative-pathname system
"docs/build/"))
(index-input-filename
(uiop:merge-pathnames* docs-source-dir
"index.mk2"))
(index-output-filename
(uiop:merge-pathnames* docs-output-dir
"index.html"))
(document (with-open-file (s index-input-filename)
(geneva.mk2:read-mk2 s))))
(ensure-directories-exist docs-output-dir)
(uiop:with-output-file (s index-output-filename
:if-exists :supersede)
(geneva.html:render-html document :stream s)
;; Return a directory with resulting docs:
docs-output-dir)))
Finally, we can build our documentation:
CL-USER> (docs-builder:build :example)
<INFO> [23:53:48] docs-builder/builder builder.lisp (build :around system) -
Building docs for system #<PACKAGE-INFERRED-SYSTEM "example"> found at /Users/art/cl-doc-systems/geneva/
#P"/Users/art/cl-doc-systems/geneva/docs/build/"
Of cause, in reality this method could be a more complex. It should process all *.mk2
files
and build API
reference for the primary system and all package inferred subsystems.
Supported Docs Generators
40ANTS-DOC
This guesser tries to find if your system depends on 40ants-doc
system.
If it is, then the 40ANTS-DOC
will be used to build documentation.
During the build
phase, the builder will try to find documentation sections not refereced
from any other sections. For each root section, builder will create a separate HTML
page. If there are few root sections, make sure one of them is having 40ants-doc
name.
Otherwise index.html
page will not be created.
Algorithm searches section amongh all exported symbols. If you don't want it to find
some root section, just pass :export nil
to the 40ants-doc:defsection
macro.
If you want your documentation link back to the GitHub sources, make sure
you have either :homepage
or :source-control
in your ASDF
definition:
(asdf:defsystem #:example
:licence "MIT"
:version "0.0.3"
:author "John Doe"
:mailto "john@doe.me"
:homepage "https://github.com/john-doe/example"
:source-control (:git "https://github.com/john-doe/example")
...)
Note, that this builder not only renders HTML
documentation, but also updates
README
files in the system's root directory.
What is next
build a ChangeLog.md out of changelog.lisp, if it is exists
MGL-PAX
This guesser tries to find if your system depends on MGL-PAX
system.
If it is, then the MGL-PAX
will be used to build documentation.
During the build
phase, the builder will try to find MGL-PAX
sections not refereced
from any other sections. For each root section, builder will create a separate HTML
page. If there are few root sections, make sure one of them is having \mgl-pax
name.
Otherwise index.html
page will not be created.
Algorithm searches section amongh all exported symbols. If you don't want it to find
some root section, just pass :export nil
to the MGL-PAX:DEFSECTION
macro.
If you want your documentation link back to the GitHub sources, make sure
you have either :homepage
or :source-control
in your ASDF
definition:
(asdf:defsystem #:example
:licence "MIT"
:version "0.0.3"
:author "John Doe"
:mailto "john@doe.me"
:homepage "https://github.com/john-doe/example"
:source-control (:git "https://github.com/john-doe/example")
...)
Note, that this builder not only renders HTML
documentation, but also updates
README
files in the system's root directory.
Geneva
This guesser tries to find a file docs/sources/index.mk2
and if it exists
then Geneva documentation generator will be used.
What is next
make builder to process all
*.mk2
files in thedocs/sources/
dir.build
API
reference pages for all packages created by the system.
API
Utilities
Returns a list of string with names of external dependencies of the system.
It works with package-inferred systems too, recursively collecting external-dependencies of all subsystems.
Warning! By default, this function does not return dependencies of dependencies.
To get them all, add :ALL
T option.
Returns a list of packages created by ASDF
system.
Default implementation returns a package having the same name as a system and all packages matched to package-inferred subsystems:
CL-USER> (docs-builder/utils:system-packages :docs-builder)
(#<PACKAGE "DOCS-BUILDER">
#<PACKAGE "DOCS-BUILDER/UTILS">
#<PACKAGE "DOCS-BUILDER/GUESSER">
#<PACKAGE "DOCS-BUILDER/BUILDERS/GENEVA/GUESSER">
#<PACKAGE "DOCS-BUILDER/BUILDER">
#<PACKAGE "DOCS-BUILDER/BUILDERS/MGL-PAX/GUESSER">
#<PACKAGE "DOCS-BUILDER/DOCS">
#<PACKAGE "DOCS-BUILDER/BUILDERS/MGL-PAX/BUILDER">)
This function can be used by builder to find pieces of documentation.
For example, mgl-pax
builder uses it to find documentation sections.
Roadmap
Use eazy-documentation as default fallback when no other builder was guessed.
Support other documentation generators, collected at https://cl-doc-systems.github.io/
Add ability to put a configuration file into the reporitory, for fine-tunning the builder.