separate templating from file system outputs

This commit is contained in:
Valentin Gagarin 2024-11-13 15:24:41 +01:00 committed by Valentin Gagarin
parent 59a2fed5e2
commit 829a796f16
3 changed files with 76 additions and 71 deletions

View file

@ -7,16 +7,24 @@ let
templates = import ./templates.nix { inherit lib; }; templates = import ./templates.nix { inherit lib; };
in in
{ {
options.templates = mkOption { options.templates =
let
# arbitrarily nested attribute set where the leaves are of type `type`
# NOTE: due to how `either` works, the first match is significant,
# so if `type` happens to be an attrset, the typecheck will consider
# `type`, not `attrsOf`
recursiveAttrs = type: with types; attrsOf (either type (recursiveAttrs type));
in
mkOption {
description = '' description = ''
Collection of named functions to convert page contents to files Collection of named functions to convert document contents to a string representation
Each template function takes the complete site `config` and the page data structure. Each template function takes the complete site `config` and the document's data structure.
''; '';
type = with types; attrsOf (functionTo (functionTo options.files.type)); type = recursiveAttrs (with types; functionTo (functionTo str));
}; };
config.templates = config.templates.html =
let let
commonmark = name: markdown: pkgs.runCommand "${name}.html" commonmark = name: markdown: pkgs.runCommand "${name}.html"
{ {
@ -26,13 +34,7 @@ in
''; '';
in in
{ {
page = lib.mkDefault (config: page: { page = lib.mkDefault (config: page: templates.html {
# TODO: create static redirects from `tail page.locations`
# TODO: reconsider using `page.outPath` and what to put into `locations`.
# maybe we can avoid having ".html" suffixes there.
# since templates can output multiple files, `html` is merely one of many things we *could* produce.
# TODO: maybe it would even make sense to split routing and rendering altogether
${page.outPath} = builtins.toFile "${page.name}.html" (templates.html {
head = '' head = ''
<title>${page.title}</title> <title>${page.title}</title>
<meta name="description" content="${page.description}" /> <meta name="description" content="${page.description}" />
@ -43,10 +45,7 @@ in
${builtins.readFile (commonmark page.name page.body)} ${builtins.readFile (commonmark page.name page.body)}
''; '';
}); });
}); article = lib.mkDefault (config: page: templates.html {
article = lib.mkDefault (config: page: {
# TODO: create static redirects from `tail page.locations`
${page.outPath} = builtins.toFile "${page.name}.html" (templates.html {
head = '' head = ''
<title>${page.title}</title> <title>${page.title}</title>
<meta name="description" content="${page.description}" /> <meta name="description" content="${page.description}" />
@ -57,7 +56,6 @@ in
${builtins.readFile (commonmark page.name page.body)} ${builtins.readFile (commonmark page.name page.body)}
''; '';
}); });
});
}; };
options.files = mkOption { options.files = mkOption {
@ -71,25 +69,24 @@ in
}; };
config.files = config.files =
# TODO: create static redirects from `tail page.locations`
let let
pages = lib.concatMapAttrs pages = lib.attrValues config.pages;
(name: page: page.template config page) collections = with lib; concatMap (collection: collection.entry) (attrValues config.collections);
config.pages; collections' = with lib; map
collections = (
let entry: recursiveUpdate entry {
byCollection = with lib; mapAttrs locations = map (l: "${entry.collection.name}/${l}") entry.locations;
(_: collection: }
map (entry: entry.template config entry) collection.entry
) )
config.collections; collections;
in in
with lib; concatMapAttrs with lib; foldl
(collection: entries: (acc: elem: acc // {
foldl' (acc: entry: acc // entry) { } entries "${head elem.locations}" = builtins.toFile "${elem.name}.html" elem.outputs.html;
) })
byCollection; { }
in (pages ++ collections');
pages // collections;
options.build = mkOption { options.build = mkOption {
description = '' description = ''

View file

@ -16,12 +16,15 @@ in
config.content-types = { config.content-types = {
document = { name, config, ... }: { document = { name, config, ... }: {
options = { options = {
name = mkOption { name = mkOption {
description = "Symbolic name, used as a human-readable identifier"; description = "Symbolic name, used as a human-readable identifier";
type = types.str; type = types.str;
default = name; default = name;
}; };
# TODO: reconsider using `page.outPath` and what to put into `locations`.
# maybe we can avoid having ".html" suffixes there.
# since templates can output multiple files, `html` is merely one of many things we *could* produce.
# TODO: make `apply` configurable so one can programmatically modify locations
locations = mkOption { locations = mkOption {
description = '' description = ''
List of historic output locations for the resulting file List of historic output locations for the resulting file
@ -44,17 +47,11 @@ in
type = types.str; type = types.str;
default = lib.head config.locations; default = lib.head config.locations;
}; };
# TODO: maybe it would even make sense to split routing and rendering altogether. outputs = mkOption {
# in that case, templates would return strings, and a different
# piece of the machinery resolves rendering templates to files
# using `locations`.
# then we'd have e.g. `templates.html` and `templates.atom` for
# different output formats.
template = mkOption {
description = '' description = ''
Function that converts the page contents to files Representations of the document in different formats
''; '';
type = with types; functionTo (functionTo options.files.type); type = with types; attrsOf str;
}; };
}; };
}; };
@ -85,12 +82,17 @@ in
type = types.str; type = types.str;
}; };
}; };
config.template = cfg.templates.page; config.outputs.html = cfg.templates.html.page cfg config;
}; };
article = { config, collectionName, ... }: { article = { config, collection, ... }: {
imports = [ cfg.content-types.page ]; imports = [ cfg.content-types.page ];
options = { options = {
collection = mkOption {
description = "Collection this article belongs to";
type = options.collections.type.nestedTypes.elemType;
default = collection;
};
date = mkOption { date = mkOption {
description = "Publication date"; description = "Publication date";
type = with types; nullOr str; type = with types; nullOr str;
@ -103,8 +105,10 @@ in
}; };
}; };
config.name = lib.slug config.title; config.name = lib.slug config.title;
config.outPath = "${collectionName}/${lib.head config.locations}"; # TODO: this should be covered by the TBD `link` function instead,
config.template = lib.mkForce cfg.templates.article; # taking a historical list of collection names into account
config.outPath = "${collection.name}/${lib.head config.locations}";
config.outputs.html = lib.mkForce (cfg.templates.html.article cfg config);
}; };
named-link = { ... }: { named-link = { ... }: {

View file

@ -42,11 +42,15 @@ in
description = "Type of entries in the collection"; description = "Type of entries in the collection";
type = types.deferredModule; type = types.deferredModule;
}; };
name = mkOption {
description = "Symbolic name, used as a human-readable identifier";
type = types.str;
default = name;
};
entry = mkOption { entry = mkOption {
description = "An entry in the collection"; description = "An entry in the collection";
type = types.collection (types.submodule ({ type = types.collection (types.submodule ({
_module.args.collection = config.entry; _module.args.collection = config;
_module.args.collectionName = name;
imports = [ config.type ]; imports = [ config.type ];
})); }));
}; };