cl-hssg/HACKING.rst

179 lines
7.3 KiB
ReStructuredText
Raw Normal View History

2022-09-11 22:46:26 +02:00
.. default-role:: code
###############################
Hacking on the HSSG internals
###############################
This is interim documentation about the project. Eventually it will become a
proper user manual, for now I am using this document to keep notes before they
become lost forever.
Project overview
################
The idea of HSSG is have a static site generator which acts as a library. Each
website project is its own Common Lisp project that depends on HSSG. It is up
to the website generator how to actually structure and build the website. This
makes it a little harder to set up the website initially, but in return there
are close to no limits as to what kind of website to build. Want a blog? Want
multiple blogs? Multiple languages? Sub-sites? All of that is possible.
Commonly used features like a blog are provided as plugins. These are not
necessarily actual plugins that hook into HSSG (although they can be), they are
generally additional libraries which also depend on HSSG. Two plugins are
provided as part of this project: the blog plugin and the CommonMark reader
plugin. They are not part of core because they have extra dependencies which
not all users of HSSG might need.
Project structure
=================
All source code is in the `src` directory. HSSG and the plugins each have their
own directory there.
Each system has a `package.lisp` file which serves as the initial entry point.
This file defines (almost) all systems and packages, but some files may define
small one-off packages which are not used anywhere else
Core concepts
#############
Artifacts
=========
An artifact is an abstract representation of something that will produce output
when it is written. It can be something like a file that will be copied
verbatim, or a potential HTML page. Artifact can also be of higher order,
meaning they wrap around other artifacts. The compound artifact is a simple
2022-11-05 15:33:05 +01:00
case of just a plain container artifact. On the other hand, the blog is a
higher-order artifact that also has its own data such as the blog title.
2022-09-11 22:46:26 +02:00
Metadata
========
Metadata is an association list of key-value pairs. The key is a keyword symbol
which names the kind of data, while the value is whatever. Metadata gets
transformed by templates.
There are no fixed rules as to what metadata is supposed to represent. Usually
it is information about a web page, but it can also represent a CSS file, or
even something abstract like a product showcase. A product showcase could then
be transformed into metadata representing multiple HTML pages, turning an
abstract domain-specific concept into a more general concept.
Templates
=========
A template is a function which transforms metadata to metadata. There are no
further rules, it is the responsibility of the user to chain templates in a
meaningful way.
There are convenience macros (e.g. `DEFTEMPLATE`) for defining templates and
there are combinator functions which combine templates into other templates
(e.g. `CHAIN-TEMPLATES`).
Readers
=======
We have metadata to represent information, we have templates to transform them,
but where do we get metadata from? In theory metadata can come from anywhere,
but in practice it will usually be read from a file. A reader is a function
which takes a path to a file in a certain forma and returns metadata.
Usually readers will be stored in a key-value variable where the key is the
file format. Another function can then dispatch on the file format to the
correct reader.
2022-11-05 15:33:05 +01:00
File systems and instructions
=============================
Ultimately every artifact will be written to a file. Hard-coding this would
require intermixing the concern of "represents some content" and "will produce
a file". The lack of separation of concerns makes the artifact classes harder
to reason about and harder to test. The act of producing an actual on-disk file
needs to be separate from the individual artifacts. The usual solution would be
to have some sort of "file system service" object which we can inject it as a
dependency into a function, but this is not an elegant solution.
My solution are file systems and instructions. A file system is an object which
abstracts away access to an actual file system. An instruction is an object
which represents an action to perform, such as copying a file or writing a
string to a file.
.. code:: lisp
(defclass base-file-system (file-system)
((directory :initarg :directory :reader file-system-directory :type pathname
:documentation "Actual directory within the file system."))
(:documentation
"A file system which accesses files of the host OS relative to a given base
directory."))
(defclass write-string-contents (file-system-instruction)
((path :initarg :path :reader instruction-path :type pathname
:documentation "Path to the output file")
(contents :initarg :contents :initform (make-string 0) :type string
:documentation "The file content as a string"))
(:documentation
"Instruction which creates a file with the given content."))
We can apply an instruction to a file system, which carries out the action in
the directory of the file system. We use the fact that CLOS supports multiple
dispatch to dispatch on both the file system and the instruction.
.. code:: lisp
(defmethod write-to-filesystem ((instruction write-string-contents)
(file-system base-file-system))
"A primitive implementation producing one file for fixed contents and an
absolute file system."
(let ((path (fad:merge-pathnames-as-file
(fad:pathname-as-directory (file-system-directory file-system))
(instruction-path instruction))))
(with-slots (contents) instruction
(write-string-to-file contents path))))
But wait, if we have `n` file systems and `m` instructions, does that mean we
need `n * m` implementations? No, most file systems and instructions are
actually of a higher-order and reduce down to the most elemental ones. Consider
the compound instruction, a container instruction which wraps other
instructions:
.. code:: lisp
(defmethod write-to-filesystem ((instruction compound-instruction)
file-system)
(with-slots (instructions) instruction
(dolist (instruction instructions)
(write-to-filesystem instruction file-system))))
Instructions a produced by deriving an artifact; e.g. to derive an HTML
artifact we generate the file contents as a string and produce a
`WRITE-STRING-CONTENTS` instruction with the content and file name. We do not
care where the file is written to, that part is the responsibility of the file
system.
The most elemental file system simply references an on-disc directory. A
higher-order file system is the overlay file system which adds a path on top of
another file system. We are not bound by on-disc directories though: an FTP
file system could abstract away access to an FTP server, a ZIP file system
might abstract away access to a ZIP file.
2022-09-11 22:46:26 +02:00
The blog plugin
###############
This plugin implements blogging. Each blog is one giant artifact which wraps
around other artifacts. Those other artifacts are private though.
The CommonMark reader plugin
############################
This plugin provides a reader for CommonMark files. Metadata is given as
key-value pairs before a separator line `---`. Everything after the separator
becomes the content of the file.