adp
2024-10-12
Add Documentation, Please. A Common Lisp documentation generator and literate programming tool with Scribble files and @-syntax support.
Add Documentation, Please
Welcome to ADP!
Introduction
Simply put, ADP adds scribble files support to ASDF. Thus, you can add scribble files and use the well-known at-syntax.
Example:
Installation
This project is available on Quicklisp. But, instead of installing this, you should install an exporter.
Exporters
- adp-github: Generates Github flavoured Markdown files with crossreferences, tables of contents, etc.
Getting started
Each exporter defines diferent ways of adding scribble
files, but usually all them have similarities.
- Create a subsystem for the scribble files. It must
defsystem-depends-on
the selected exporter and shoulddepends-on
you project to be able to use everything you've defined there. Let's useadp-github
in this example. This exporter defines the ASDF file typescribble
as well as the ASDF operationadp-github-op
.
;;; my-project.asd (defsystem "my-project" ...) (defsystem "my-project/docs" :defsystem-depends-on ("adp-github") :depends-on ("my-project") :build-operation "adp-github-op" :components ((:file "doc-package") (:scribble "user-guide")))
- Create the files. In this example we need a regular file named
doc-package.lisp
and a scribble one nameduser-guide.scrbl
. In the former we will define the package to use while writing scribble text. Those files could have the following contents:
;;; doc-package.lisp (defpackage "my-project-docs" (:use #:cl #:adp-github)) ;;; user-guide.scrbl (in-package "my-project-docs") @header{User guide} Welcome to my project!
- Generate the documentation. We only need to
make
the new subsystem. In your REPL, evaluate the following expression:
(asdf:make "my-project/docs")
How to create an exporter
Creating an exporter is really easy. Let's do an example of an exporter that creates txt files and princ
s every object that ADP gathers.
An exporter must be its own project. Let's create the asd
file:
(defsystem "adp-princ" :depends-on ("adp") :components ((:file "package") (:file "adp-princ")))
Of course, you can use whatever number of files you want. The exporter can use also whatever package you want; suppose we're using the package ADP-PRINC
. So, let's see now the file adp-princ
:
(in-package #:adp-princ) (adp:define-adp-operation adp-princ-op) ; <- defining a new operation (adp:define-adp-file scribble "scrbl") ; <- defining a new ASDF file type ...
First of all, we need to define an ASDF operation and an ASDF file. The operation will be the responsible of gather all the scribble contents from the new defined scribble
files with extension scrbl
.
After this, we can now implement a method using that operation:
(in-package #:adp-princ) (adp:define-adp-operation adp-princ-op) (adp:define-adp-file scribble "scrbl") (defmethod adp:export-content ((op adp-princ-op) files system) ...)
The argument files
is a vector
containing each scribble
file used in the subsystem (actually, a file
is an object representing a file). Each file contains the elements gathered from a scribble
file. The order in which files are stored in is the same as the order they are loaded.
Each file contains the ASDF
component it represents as well. Files have then two accessors:
adp:file-component
: Retrieves theASDF
component of a file.adp:file-elements
: Retrieves the elements of a file. The elements are stored in avector
.
In this case, file-component
will have always the type scribble
. This type was defined by adp:define-adp-file
. We could have defined more file types. Finally, we can use generic functions to call different methods depending on the type of these files.
Coming back to adp:export-content
, the method receives the operation we just defined, the files ADP
has gathered and the ASDF
system component. Like files
is a vector, we only need to iterate over it:
(defmethod adp:export-content ((op adp-princ-op) files system) (loop for file across files do (let ((target-file (make-pathname :directory (pathname-directory (merge-pathnames file-path (asdf:system-source-directory system))) :name (pathname-name file-path) :type "txt"))) (with-open-file (file-str target-path :direction :output :if-exists :supersede :if-does-not-exist :create) (loop for element across (adp:file-elements file) do (princ element file-str))))))
For every file, we are creating first the target pathname. It is the same as the source file, but with the type "txt". If it doesn't exist, we create the file and princ
every object in it.
And that's all!
There are also generic functions that allow you to make some preparation before/after the system or a file is loaded. These are adp:pre-process-system
, adp:post-process-system
, adp:pre-process-file
and adp:post-process-file
.
Writing scribble with Emacs
Scribble has already its own mode: scribble-mode. However, Scribble was designed to be used with Scheme-like languages. But we can make some magic to get syntax highlighting and autocompletion with Common Lisp.
The scribble mode already gives us some syntax highlighting, but we need to make some changes. On the other hand, we can get autocompletion with company-mode and slime-company.
company
doesn't need any changes, but scribble
and slime-company
do. Here is how I configured these modes in my init.el
:
;; ------ company ------ (use-package company :bind (:map company-active-map ("<tab>" . company-complete-selection)) :config (global-company-mode)) ;; ------ scrbl ------ (use-package scribble-mode ;; We modify the syntax to adapt scribble-mode to the Common Lisp language. ;; With this it will understand prefix packages. :config (modify-syntax-entry ?: "_ " scribble-mode-syntax-table) (push `(,(rx (or space "(" "[" "{") (group (zero-or-one "#") ":" (+ (not (any space ")" "]" "}"))))) (1 font-lock-keyword-face)) scribble-mode-font-lock-keywords)) ;; ------ slime-company ------ (use-package slime-company :after (slime company scribble-mode) :config (push 'scribble-mode slime-company-major-modes) ; <-- This is the important line for slime-company (setq slime-company-completion 'fuzzy))
Happy hacking! :D