From 829a796f1670a007f7725e05778366f804b10884 Mon Sep 17 00:00:00 2001 From: valentin gagarin Date: Wed, 13 Nov 2024 15:24:41 +0100 Subject: [PATCH] separate templating from file system outputs --- website/presentation/default.nix | 107 ++++++++++++++-------------- website/structure/content-types.nix | 32 +++++---- website/structure/default.nix | 8 ++- 3 files changed, 76 insertions(+), 71 deletions(-) diff --git a/website/presentation/default.nix b/website/presentation/default.nix index 22f9b8b3..aa0dba42 100644 --- a/website/presentation/default.nix +++ b/website/presentation/default.nix @@ -7,16 +7,24 @@ let templates = import ./templates.nix { inherit lib; }; in { - options.templates = mkOption { - description = '' - Collection of named functions to convert page contents to files + 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 = '' + 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. - ''; - type = with types; attrsOf (functionTo (functionTo options.files.type)); - }; + Each template function takes the complete site `config` and the document's data structure. + ''; + type = recursiveAttrs (with types; functionTo (functionTo str)); + }; - config.templates = + config.templates.html = let commonmark = name: markdown: pkgs.runCommand "${name}.html" { @@ -26,37 +34,27 @@ in ''; in { - page = lib.mkDefault (config: page: { - # 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 = '' - ${page.title} - - - ''; - body = '' - ${templates.nav { menu = { menu = config.menus.main; }; }} - ${builtins.readFile (commonmark page.name page.body)} - ''; - }); + page = lib.mkDefault (config: page: templates.html { + head = '' + ${page.title} + + + ''; + body = '' + ${templates.nav { menu = { menu = config.menus.main; }; }} + ${builtins.readFile (commonmark page.name page.body)} + ''; }); - article = lib.mkDefault (config: page: { - # TODO: create static redirects from `tail page.locations` - ${page.outPath} = builtins.toFile "${page.name}.html" (templates.html { - head = '' - ${page.title} - - - ''; - body = '' - ${templates.nav { menu = { menu = config.menus.main; }; }} - ${builtins.readFile (commonmark page.name page.body)} - ''; - }); + article = lib.mkDefault (config: page: templates.html { + head = '' + ${page.title} + + + ''; + body = '' + ${templates.nav { menu = { menu = config.menus.main; }; }} + ${builtins.readFile (commonmark page.name page.body)} + ''; }); }; @@ -71,25 +69,24 @@ in }; config.files = + # TODO: create static redirects from `tail page.locations` let - pages = lib.concatMapAttrs - (name: page: page.template config page) - config.pages; - collections = - let - byCollection = with lib; mapAttrs - (_: collection: - map (entry: entry.template config entry) collection.entry - ) - config.collections; - in - with lib; concatMapAttrs - (collection: entries: - foldl' (acc: entry: acc // entry) { } entries - ) - byCollection; + pages = lib.attrValues config.pages; + collections = with lib; concatMap (collection: collection.entry) (attrValues config.collections); + collections' = with lib; map + ( + entry: recursiveUpdate entry { + locations = map (l: "${entry.collection.name}/${l}") entry.locations; + } + ) + collections; in - pages // collections; + with lib; foldl + (acc: elem: acc // { + "${head elem.locations}" = builtins.toFile "${elem.name}.html" elem.outputs.html; + }) + { } + (pages ++ collections'); options.build = mkOption { description = '' diff --git a/website/structure/content-types.nix b/website/structure/content-types.nix index 98c19c6a..d396010b 100644 --- a/website/structure/content-types.nix +++ b/website/structure/content-types.nix @@ -16,12 +16,15 @@ in config.content-types = { document = { name, config, ... }: { options = { - name = mkOption { description = "Symbolic name, used as a human-readable identifier"; type = types.str; 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 { description = '' List of historic output locations for the resulting file @@ -44,17 +47,11 @@ in type = types.str; default = lib.head config.locations; }; - # TODO: maybe it would even make sense to split routing and rendering altogether. - # 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 { + outputs = mkOption { 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; }; }; - 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 ]; options = { + collection = mkOption { + description = "Collection this article belongs to"; + type = options.collections.type.nestedTypes.elemType; + default = collection; + }; date = mkOption { description = "Publication date"; type = with types; nullOr str; @@ -103,8 +105,10 @@ in }; }; config.name = lib.slug config.title; - config.outPath = "${collectionName}/${lib.head config.locations}"; - config.template = lib.mkForce cfg.templates.article; + # TODO: this should be covered by the TBD `link` function instead, + # 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 = { ... }: { diff --git a/website/structure/default.nix b/website/structure/default.nix index df80b94c..70a473ff 100644 --- a/website/structure/default.nix +++ b/website/structure/default.nix @@ -42,11 +42,15 @@ in description = "Type of entries in the collection"; type = types.deferredModule; }; + name = mkOption { + description = "Symbolic name, used as a human-readable identifier"; + type = types.str; + default = name; + }; entry = mkOption { description = "An entry in the collection"; type = types.collection (types.submodule ({ - _module.args.collection = config.entry; - _module.args.collectionName = name; + _module.args.collection = config; imports = [ config.type ]; })); };