Compare commits

..

5 commits

Author SHA1 Message Date
Valentin Gagarin e0bb83610f allow menu entries to be subtypes of page
with an example
2024-10-16 01:16:56 +02:00
Valentin Gagarin 2371eb11a5 split content types into separate files 2024-10-16 00:56:14 +02:00
Valentin Gagarin 79c502671c extract importing Nix files 2024-10-16 00:18:30 +02:00
Valentin Gagarin 6efd5059a5 implement correct-by-construction relative links 2024-10-16 00:08:59 +02:00
Valentin Gagarin 6012c399c1 smarter indent
this allows writing the beginning of the indented string at the desired
indentation
2024-10-15 23:58:06 +02:00
9 changed files with 239 additions and 162 deletions

View file

@ -3,11 +3,11 @@ let
inherit (config) pages;
in
{
imports = with lib.fileset; toList (difference (fileFilter ({ hasExt, ... }: hasExt "nix") ./.) ./default.nix);
imports = lib.nixFiles ./.;
collections.news.type = config.content-types.article;
pages.index = {
pages.index = { link, ... }: {
title = "Fediversity";
locations = [
"index.html"
@ -21,13 +21,13 @@ in
${pages.fediversity.summary}
[Learn more about Fediversity](${pages.fediversity})
[Learn more about Fediversity](${link pages.fediversity})
# Fediversity grants
${pages.grants.summary}
[Learn more about Fediversity grants](${pages.grants})
[Learn more about Fediversity grants](${link pages.grants})
# Consortium
@ -38,7 +38,7 @@ in
${partner.description}
[Read more about ${partner.title}](${partner})
[Read more about ${partner.title}](${link partner})
'') (with pages; [ nlnet oid tweag nordunet ]))}
# News

View file

@ -1,4 +1,4 @@
{ config, ... }:
{ config, lib, ... }:
let
inherit (config) pages;
in
@ -11,11 +11,21 @@ in
menu.items = map (page: { inherit page; }) (with pages; [ nlnet oid tweag nordunet ]);
}
{
page = pages.fediversity;
}
{
page = pages.grants;
menu.label = "News";
menu.items =
let
sorted = with lib; reverseList (sortOn (entry: entry.date) config.collections.news.entry);
in
map
(page: {
page = lib.recursiveUpdate page {
title = "${page.date}: ${page.title}";
};
})
(lib.take 3 sorted);
}
{ page = pages.fediversity; }
{ page = pages.grants; }
];
};
}

40
lib.nix
View file

@ -36,7 +36,45 @@ rec {
splitLines = s: with builtins; filter (x: !isList x) (split "\n" s);
indent = prefix: s:
join "\n" (map (x: if x == "" then x else "${prefix}${x}") (splitLines s));
with lib.lists;
let
lines = splitLines s;
in
join "\n" (
[ (head lines) ]
++
(map (x: if x == "" then x else "${prefix}${x}") (tail lines))
);
relativePath = path1': path2':
let
inherit (lib.path) subpath;
inherit (lib) lists;
path1 = subpath.components path1';
path2 = subpath.components path2';
commonPrefixLength = with lists;
findFirstIndex (i: i.fst != i.snd)
{ fst = null; snd = null; }
(zipLists path1 path2);
relativeComponents = with lists;
[ "." ] ++ (replicate (length path1 - commonPrefixLength - 1) "..") ++
(drop commonPrefixLength path2);
in
join "/" relativeComponents;
/**
Recursively list all Nix files from a directory, except the top-level `default.nix`
Useful for module system `imports` from a top-level module.
**/
nixFiles = dir: with lib.fileset;
toList (difference
(fileFilter ({ hasExt, ... }: hasExt "nix") dir)
(dir + "/default.nix")
);
types = rec {
collection = elemType:

35
structure/article.nix Normal file
View file

@ -0,0 +1,35 @@
{ config, options, lib, ... }:
let
inherit (lib)
mkOption
types
;
cfg = config;
in
{
content-types. 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; str;
default = null;
};
author = mkOption {
description = "Page author";
type = with types; either str (nonEmptyListOf str);
default = null;
};
};
config.name = lib.slug config.title;
# 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);
};
}

View file

@ -1,150 +0,0 @@
{ config, lib, options, ... }:
let
inherit (lib)
mkOption
types
;
cfg = config;
in
{
options = {
content-types = mkOption {
description = "Content types";
type = with types; attrsOf deferredModule;
};
};
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
The first element is the canonical location.
All other elements are used to create redirects to the canonical location.
'';
type = with types; nonEmptyListOf str;
};
link = mkOption {
description = "Helper function for transparent linking to other pages";
type = with types; functionTo str;
default = target: "TODO: compute the relative path based on `locations`";
};
# TODO: may not need it when using `link`; could repurpose it to render the default template
outPath = mkOption {
description = ''
Location of the page, used for transparently creating links
'';
type = types.str;
default = lib.head config.locations;
};
outputs = mkOption {
description = ''
Representations of the document in different formats
'';
type = with types; attrsOf str;
};
};
};
page = { name, config, ... }: {
imports = [ cfg.content-types.document ];
options = {
title = mkOption {
description = "Page title";
type = types.str;
default = name;
};
description = mkOption {
description = ''
One-sentence description of page contents
'';
type = types.str;
};
summary = mkOption {
description = ''
One-paragraph summary of page contents
'';
type = types.str;
};
body = mkOption {
description = ''
Page contents in CommonMark
'';
type = types.str;
};
};
config.outputs.html = cfg.templates.html.page cfg config;
};
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; str;
default = null;
};
author = mkOption {
description = "Page author";
type = with types; either str (nonEmptyListOf str);
default = null;
};
};
config.name = lib.slug config.title;
# 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 = { ... }: {
options = {
label = mkOption {
description = "Link label";
type = types.str;
};
url = mkOption {
description = "Link URL";
type = types.str;
};
};
};
navigation = { name, ... }: {
options = {
name = mkOption {
description = "Symbolic name, used as a human-readable identifier";
type = types.str;
default = name;
};
label = mkOption {
description = "Menu label";
type = types.str;
default = name;
};
items = mkOption {
description = "List of menu items";
type = with types; listOf (attrTag {
menu = mkOption { type = submodule cfg.content-types.navigation; };
page = mkOption { type = submodule cfg.content-types.page; };
link = mkOption { type = submodule cfg.content-types.named-link; };
});
};
};
};
};
}

View file

@ -7,7 +7,12 @@ let
cfg = config;
in
{
imports = [ ./content-types.nix ];
imports = lib.nixFiles ./.;
options.content-types = mkOption {
description = "Content types";
type = with types; attrsOf deferredModule;
};
# TODO: enable i18n, e.g. via a nested attribute for language-specific content
options.pages = mkOption {

51
structure/document.nix Normal file
View file

@ -0,0 +1,51 @@
{ lib, ... }:
let
inherit (lib)
mkOption
types
;
in
{
content-types.document = { name, config, link, ... }: {
config._module.args.link = config.link;
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
The first element is the canonical location.
All other elements are used to create redirects to the canonical location.
'';
type = with types; nonEmptyListOf str;
};
link = mkOption {
description = "Helper function for transparent linking to other pages";
type = with types; functionTo str;
default = target: with lib; relativePath (head config.locations) (head target.locations);
};
# TODO: may not need it when using `link`; could repurpose it to render the default template
outPath = mkOption {
description = ''
Location of the page, used for transparently creating links
'';
type = types.str;
default = lib.head config.locations;
};
outputs = mkOption {
description = ''
Representations of the document in different formats
'';
type = with types; attrsOf str;
};
};
};
}

49
structure/navigation.nix Normal file
View file

@ -0,0 +1,49 @@
{ config, lib, ... }:
let
inherit (lib)
mkOption
types
;
cfg = config;
subtype = baseModule: types.submodule [
baseModule
{ _module.freeformType = types.attrs; }
];
in
{
content-types.named-link = { ... }: {
options = {
label = mkOption {
description = "Link label";
type = types.str;
};
url = mkOption {
description = "Link URL";
type = types.str;
};
};
};
content-types.navigation = { name, ... }: {
options = {
name = mkOption {
description = "Symbolic name, used as a human-readable identifier";
type = types.str;
default = name;
};
label = mkOption {
description = "Menu label";
type = types.str;
default = name;
};
items = mkOption {
description = "List of menu items";
type = with types; listOf (attrTag {
menu = mkOption { type = submodule cfg.content-types.navigation; };
page = mkOption { type = subtype cfg.content-types.page; };
link = mkOption { type = submodule cfg.content-types.named-link; };
});
};
};
};
}

39
structure/page.nix Normal file
View file

@ -0,0 +1,39 @@
{ config, lib, ... }:
let
inherit (lib)
mkOption
types
;
cfg = config;
in
{
content-types.page = { name, config, ... }: {
imports = [ cfg.content-types.document ];
options = {
title = mkOption {
description = "Page title";
type = types.str;
default = name;
};
description = mkOption {
description = ''
One-sentence description of page contents
'';
type = types.str;
};
summary = mkOption {
description = ''
One-paragraph summary of page contents
'';
type = types.str;
};
body = mkOption {
description = ''
Page contents in CommonMark
'';
type = types.str;
};
};
config.outputs.html = cfg.templates.html.page cfg config;
};
}