forked from Fediversity/fediversity.eu
Compare commits
5 commits
bcd432d0c2
...
166dc5058a
Author | SHA1 | Date | |
---|---|---|---|
166dc5058a | |||
6437ee43dc | |||
71dc57b7d5 | |||
2ddd94faa5 | |||
38379f8a8f |
|
@ -1,13 +1,14 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (config) pages;
|
inherit (config) pages;
|
||||||
|
cfg = config;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = lib.nixFiles ./.;
|
imports = lib.nixFiles ./.;
|
||||||
|
|
||||||
collections.news.type = config.content-types.article;
|
collections.news.type = cfg.content-types.article;
|
||||||
|
|
||||||
pages.index = { link, ... }: {
|
pages.index = { config, link, ... }: {
|
||||||
title = "Fediversity";
|
title = "Fediversity";
|
||||||
description = "Fediversity web site";
|
description = "Fediversity web site";
|
||||||
summary = ''
|
summary = ''
|
||||||
|
@ -52,12 +53,19 @@ in
|
||||||
|
|
||||||
${
|
${
|
||||||
let
|
let
|
||||||
sorted = with lib; reverseList (sortOn (entry: entry.date) config.collections.news.entry);
|
sorted = with lib; reverseList (sortOn (entry: entry.date) cfg.collections.news.entry);
|
||||||
in
|
in
|
||||||
lib.join "\n" (map (article: ''
|
lib.join "\n" (map (article: ''
|
||||||
- ${article.date} [${article.title}](${link article})
|
- ${article.date} [${article.title}](${link article})
|
||||||
'') sorted)
|
'') sorted)
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
outputs.html = (cfg.templates.html.page config).override {
|
||||||
|
html.body.content = lib.mkForce [
|
||||||
|
# don't show the page title as a heading
|
||||||
|
(cfg.menus.main.outputs.html config)
|
||||||
|
(cfg.templates.html.markdown { inherit (config) name body; })
|
||||||
|
];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,8 @@ let
|
||||||
new // { types = prev.recursiveUpdate prev.types new.types; };
|
new // { types = prev.recursiveUpdate prev.types new.types; };
|
||||||
lib'' = lib.extend lib';
|
lib'' = lib.extend lib';
|
||||||
in
|
in
|
||||||
{
|
rec {
|
||||||
lib = import ./lib.nix { inherit lib; };
|
lib = import ./lib.nix { inherit lib; };
|
||||||
|
|
||||||
build =
|
|
||||||
let
|
|
||||||
result = lib''.evalModules {
|
result = lib''.evalModules {
|
||||||
modules = [
|
modules = [
|
||||||
./structure
|
./structure
|
||||||
|
@ -32,8 +29,8 @@ in
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
in
|
|
||||||
result.config.build;
|
inherit (result.config) build;
|
||||||
|
|
||||||
shell = pkgs.mkShellNoCC {
|
shell = pkgs.mkShellNoCC {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
|
3
lib.nix
3
lib.nix
|
@ -1,5 +1,8 @@
|
||||||
{ lib }:
|
{ lib }:
|
||||||
rec {
|
rec {
|
||||||
|
template = g: f: x:
|
||||||
|
(g (f x)) // { override = o: g (lib.recursiveUpdate (f x) o); };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Recursively replace occurrences of `from` with `to` within `string`
|
Recursively replace occurrences of `from` with `to` within `string`
|
||||||
|
|
||||||
|
|
|
@ -4,98 +4,35 @@ let
|
||||||
mkOption
|
mkOption
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
templates = import ./templates.nix { inherit lib; };
|
|
||||||
# TODO: optionally run the whole thing through the validator
|
|
||||||
# https://github.com/validator/validator
|
|
||||||
render-html = document:
|
|
||||||
let
|
|
||||||
eval = lib.evalModules {
|
|
||||||
class = "DOM";
|
|
||||||
modules = [ document (import ./dom.nix) ];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
toString eval.config;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
imports = lib.nixFiles ./.;
|
||||||
|
|
||||||
options.templates =
|
options.templates =
|
||||||
let
|
let
|
||||||
# arbitrarily nested attribute set where the leaves are of type `type`
|
# arbitrarily nested attribute set where the leaves are of type `type`
|
||||||
|
recursiveAttrs = type: with types;
|
||||||
# NOTE: due to how `either` works, the first match is significant,
|
# NOTE: due to how `either` works, the first match is significant,
|
||||||
# so if `type` happens to be an attrset, the typecheck will consider
|
# so if `type` happens to be an attrset, the typecheck will consider
|
||||||
# `type`, not `attrsOf`
|
# `type`, not `attrsOf`
|
||||||
recursiveAttrs = type: with types; attrsOf (either type (recursiveAttrs type));
|
attrsOf (either type (recursiveAttrs type));
|
||||||
in
|
in
|
||||||
mkOption {
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Collection of named functions to convert document contents to a string representation
|
Collection of named helper functions for conversion different structured representations which can be rendered to a string
|
||||||
|
|
||||||
Each template function takes the complete site `config` and the document's data structure.
|
|
||||||
'';
|
'';
|
||||||
# TODO: this function should probably take a single attrs,
|
type = recursiveAttrs (with types; functionTo (either str attrs));
|
||||||
# otherwise it's quite inflexible.
|
|
||||||
# named parameters would also help with readability at the call site
|
|
||||||
type = recursiveAttrs (with types; functionTo (functionTo str));
|
|
||||||
};
|
|
||||||
|
|
||||||
config.templates.html = {
|
|
||||||
markdown = name: text:
|
|
||||||
let
|
|
||||||
commonmark = pkgs.runCommand "${name}.html"
|
|
||||||
{
|
|
||||||
buildInputs = [ pkgs.cmark ];
|
|
||||||
} ''
|
|
||||||
cmark ${builtins.toFile "${name}.md" text} > $out
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
builtins.readFile commonmark;
|
|
||||||
nav = menu: page:
|
|
||||||
let
|
|
||||||
render-item = item:
|
|
||||||
if item ? menu then ''
|
|
||||||
<li>${item.menu.label}
|
|
||||||
${lib.indent " " (item.menu.outputs.html page)}
|
|
||||||
</li>
|
|
||||||
''
|
|
||||||
else if item ? page then ''<li><a href="${page.link item.page}">${item.page.title}</a></li>''
|
|
||||||
else ''<li><a href="${item.link.url}">${item.link.label}</a></li>''
|
|
||||||
;
|
|
||||||
in
|
|
||||||
''
|
|
||||||
<nav>
|
|
||||||
<ul>
|
|
||||||
${with lib; indent " " (join "\n" (map render-item menu.items))}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
'';
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
options.files = mkOption {
|
options.files = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Files that make up the site, mapping from output path to contents
|
Files that make up the site, mapping from output path to contents
|
||||||
|
|
||||||
By default, all elements in `option`{pages} are converted to files using their template or the default template.
|
|
||||||
Add more files to the output by assigning to this attribute set.
|
Add more files to the output by assigning to this attribute set.
|
||||||
'';
|
'';
|
||||||
# TODO: this should be attrsOf string-coercible instead.
|
|
||||||
# we can convert this to file at the very end.
|
|
||||||
type = with types; attrsOf path;
|
type = with types; attrsOf path;
|
||||||
};
|
};
|
||||||
|
|
||||||
config.files =
|
|
||||||
# TODO: create static redirects from `tail page.locations`
|
|
||||||
let
|
|
||||||
pages = lib.attrValues config.pages;
|
|
||||||
collections = with lib; concatMap (collection: collection.entry) (attrValues config.collections);
|
|
||||||
in
|
|
||||||
with lib; foldl
|
|
||||||
(acc: elem: acc // {
|
|
||||||
# TODO: we may or may not want to enforce the mapping of file types to output file name suffixes
|
|
||||||
"${head elem.locations}.html" = builtins.toFile "${elem.name}.html" elem.outputs.html;
|
|
||||||
})
|
|
||||||
{ }
|
|
||||||
(pages ++ collections);
|
|
||||||
|
|
||||||
options.build = mkOption {
|
options.build = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
The final output of the web site
|
The final output of the web site
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
let
|
let
|
||||||
cfg = config;
|
cfg = config;
|
||||||
inherit (lib) mkOption types;
|
inherit (lib) mkOption types;
|
||||||
|
inherit (types) submodule;
|
||||||
|
|
||||||
# https://html.spec.whatwg.org/multipage/dom.html#content-models
|
# https://html.spec.whatwg.org/multipage/dom.html#content-models
|
||||||
# https://html.spec.whatwg.org/multipage/dom.html#kinds-of-content
|
# https://html.spec.whatwg.org/multipage/dom.html#kinds-of-content
|
||||||
|
@ -43,7 +44,7 @@ let
|
||||||
|
|
||||||
# options with types for all the defined DOM elements
|
# options with types for all the defined DOM elements
|
||||||
element-types = lib.mapAttrs
|
element-types = lib.mapAttrs
|
||||||
(name: value: mkOption { type = types.submodule value; })
|
(name: value: mkOption { type = submodule value; })
|
||||||
elements;
|
elements;
|
||||||
|
|
||||||
# attrset of categories, where values are module options with the type of the
|
# attrset of categories, where values are module options with the type of the
|
||||||
|
@ -52,7 +53,7 @@ let
|
||||||
genAttrs
|
genAttrs
|
||||||
content-categories
|
content-categories
|
||||||
(category:
|
(category:
|
||||||
(mapAttrs (_: e: mkOption { type = types.submodule e; })
|
(mapAttrs (_: e: mkOption { type = submodule e; })
|
||||||
# HACK: don't evaluate the submodule types, just grab the config directly
|
# HACK: don't evaluate the submodule types, just grab the config directly
|
||||||
(filterAttrs (_: e: elem category (e { name = "dummy"; config = { }; }).config.categories) elements))
|
(filterAttrs (_: e: elem category (e { name = "dummy"; config = { }; }).config.categories) elements))
|
||||||
);
|
);
|
||||||
|
@ -118,7 +119,7 @@ let
|
||||||
|
|
||||||
mkAttrs = attrs: with lib;
|
mkAttrs = attrs: with lib;
|
||||||
mkOption {
|
mkOption {
|
||||||
type = types.submodule {
|
type = submodule {
|
||||||
options = global-attrs // attrs;
|
options = global-attrs // attrs;
|
||||||
};
|
};
|
||||||
default = { };
|
default = { };
|
||||||
|
@ -147,12 +148,21 @@ let
|
||||||
|
|
||||||
print-element = name: attrs: content:
|
print-element = name: attrs: content:
|
||||||
with lib;
|
with lib;
|
||||||
|
# TODO: be smarter about content to save some space and repetition at the call sites
|
||||||
squash (trim ''
|
squash (trim ''
|
||||||
<${name}${print-attrs attrs}>
|
<${name}${print-attrs attrs}>
|
||||||
${lib.indent " " content}
|
${lib.indent " " content}
|
||||||
</${name}>
|
</${name}>
|
||||||
'');
|
'');
|
||||||
|
|
||||||
|
toString-unwrap = e:
|
||||||
|
with lib;
|
||||||
|
if isAttrs e
|
||||||
|
then toString (head (attrValues e))
|
||||||
|
else if isList e
|
||||||
|
then toString (map toString-unwrap e)
|
||||||
|
else e;
|
||||||
|
|
||||||
elements = rec {
|
elements = rec {
|
||||||
document = { ... }: {
|
document = { ... }: {
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
|
@ -206,7 +216,7 @@ let
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#viewport_width_and_screen_width
|
# https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag#viewport_width_and_screen_width
|
||||||
# this should not exist and no one should ever have to think about it
|
# this should not exist and no one should ever have to think about it
|
||||||
meta.viewport = mkOption {
|
meta.viewport = mkOption {
|
||||||
type = types.submodule ({ ... }: {
|
type = submodule ({ ... }: {
|
||||||
# TODO: figure out how to render only non-default values
|
# TODO: figure out how to render only non-default values
|
||||||
options = {
|
options = {
|
||||||
width = mkOption {
|
width = mkOption {
|
||||||
|
@ -422,21 +432,13 @@ let
|
||||||
|
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self: with lib;
|
config.__toString = self: with lib;
|
||||||
print-element name self.attrs (
|
print-element name self.attrs (join "\n" (map toString-unwrap self.content));
|
||||||
join "\n" (map
|
|
||||||
(e:
|
|
||||||
if isAttrs e
|
|
||||||
then toString (lib.head (attrValues e))
|
|
||||||
else e
|
|
||||||
)
|
|
||||||
self.content)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
section = { config, name, ... }: {
|
section = { config, name, ... }: {
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
# setting to an attribute set will wrap the section in <section>
|
# setting to an attribute set will wrap the section in `<section>`
|
||||||
attrs = mkOption {
|
attrs = mkOption {
|
||||||
type = with types; nullOr (submodule { options = global-attrs; });
|
type = with types; nullOr (submodule { options = global-attrs; });
|
||||||
default = null;
|
default = null;
|
||||||
|
@ -450,14 +452,18 @@ let
|
||||||
# such an outline is rather meaningless without headings for navigation,
|
# such an outline is rather meaningless without headings for navigation,
|
||||||
# which is why we enforce headings in sections.
|
# which is why we enforce headings in sections.
|
||||||
# arguably, and this is encoded here, a section *is defined* by its heading.
|
# arguably, and this is encoded here, a section *is defined* by its heading.
|
||||||
type = with types; submodule ({ ... }: {
|
type = with types; submodule ({ config, ... }: {
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
# TODO: make `before`/`after` wrap the heading in `<hgroup>` if non-empty
|
# setting to an attribute set will wrap the section in `<hgroup>`
|
||||||
|
hgroup.attrs = mkOption {
|
||||||
|
type = with types; nullOr (submodule { options = global-attrs; });
|
||||||
|
default = with lib; mkIf (!isNull config.before || !isNull config.after) { };
|
||||||
|
};
|
||||||
# https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element
|
# https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element
|
||||||
before = mkOption {
|
before = mkOption {
|
||||||
type = with types; listOf (attrTag ({ inherit p; } // categories.scripting));
|
type = with types; listOf (attrTag ({ inherit (element-types) p; } // categories.scripting));
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
|
@ -466,7 +472,7 @@ let
|
||||||
};
|
};
|
||||||
after = mkOption {
|
after = mkOption {
|
||||||
type = with types;
|
type = with types;
|
||||||
listOf (attrTag ({ inherit p; } // categories.scripting));
|
listOf (attrTag ({ inherit (element-types) p; } // categories.scripting));
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -489,20 +495,32 @@ let
|
||||||
__toString = self: with lib;
|
__toString = self: with lib;
|
||||||
let
|
let
|
||||||
n = toString config.heading-level;
|
n = toString config.heading-level;
|
||||||
content = ''<h${n}${print-attrs self.heading.attrs}>${self.heading.content}</h${n}>
|
heading = ''<h${n}${print-attrs self.heading.attrs}>${self.heading.content}</h${n}>'';
|
||||||
'' + join "\n" (map
|
hgroup = with lib; print-element "hgroup" self.heading.hgroup.attrs (squash ''
|
||||||
(e:
|
${optionalString (!isNull self.heading.before) (toString-unwrap self.heading.before)}
|
||||||
if isAttrs e
|
${heading}
|
||||||
then toString (lib.head (attrValues e))
|
${optionalString (!isNull self.heading.after) (toString-unwrap self.heading.after)}
|
||||||
else e
|
'');
|
||||||
)
|
content = if isNull self.heading.hgroup.attrs then heading else hgroup
|
||||||
self.content);
|
+ join "\n" (map toString-unwrap self.content);
|
||||||
in
|
in
|
||||||
if !isNull self.attrs
|
if !isNull self.attrs
|
||||||
then print-element name self.attrs content
|
then print-element name self.attrs content
|
||||||
else content;
|
else content;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
p = { name, ... }: {
|
||||||
|
imports = [ element ];
|
||||||
|
options = {
|
||||||
|
attrs = mkAttrs { };
|
||||||
|
content = mkOption {
|
||||||
|
type = with types; either str (listOf (attrTag categories.phrasing));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
config.categories = [ "flow" "palpable" ];
|
||||||
|
config.__toString = self: print-element name self.attrs (toString self.content);
|
||||||
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
52
presentation/templates.nix
Normal file
52
presentation/templates.nix
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{ config, options, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
mkOption
|
||||||
|
types
|
||||||
|
;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config.templates.html = {
|
||||||
|
dom = document:
|
||||||
|
let
|
||||||
|
eval = lib.evalModules {
|
||||||
|
class = "DOM";
|
||||||
|
modules = [ document (import ./dom.nix) ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
__toString = _: toString eval.config;
|
||||||
|
value = eval.config;
|
||||||
|
};
|
||||||
|
|
||||||
|
markdown = { name, body }:
|
||||||
|
let
|
||||||
|
commonmark = pkgs.runCommand "${name}.html"
|
||||||
|
{
|
||||||
|
buildInputs = [ pkgs.cmark ];
|
||||||
|
} ''
|
||||||
|
cmark ${builtins.toFile "${name}.md" body} > $out
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
builtins.readFile commonmark;
|
||||||
|
nav = { menu, page }:
|
||||||
|
let
|
||||||
|
render-item = item:
|
||||||
|
if item ? menu then ''
|
||||||
|
<li>${item.menu.label}
|
||||||
|
${lib.indent " " (item.menu.outputs.html page)}
|
||||||
|
</li>
|
||||||
|
''
|
||||||
|
else if item ? page then ''<li><a href="${page.link item.page}">${item.page.title}</a></li>''
|
||||||
|
else ''<li><a href="${item.link.url}">${item.link.label}</a></li>''
|
||||||
|
;
|
||||||
|
in
|
||||||
|
''
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
${with lib; indent " " (join "\n" (map render-item menu.items))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
|
@ -5,14 +5,6 @@ let
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
cfg = config;
|
cfg = config;
|
||||||
render-html = document:
|
|
||||||
let
|
|
||||||
eval = lib.evalModules {
|
|
||||||
class = "DOM";
|
|
||||||
modules = [ document (import ../presentation/dom.nix) ];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
toString eval.config;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
content-types.article = { config, collection, ... }: {
|
content-types.article = { config, collection, ... }: {
|
||||||
|
@ -35,18 +27,22 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.name = lib.slug config.title;
|
config.name = lib.slug config.title;
|
||||||
config.outputs.html = lib.mkForce (render-html {
|
config.outputs.html = lib.mkForce ((cfg.templates.html.page config).override {
|
||||||
html = {
|
html = {
|
||||||
head = {
|
# TODO: make authors always a list
|
||||||
title.text = config.title;
|
head.meta.authors = if lib.isList config.author then config.author else [ config.author ];
|
||||||
meta.description = config.description;
|
body.content = lib.mkForce [
|
||||||
meta.authors = if lib.isList config.author then config.author else [ config.author ];
|
|
||||||
link.canonical = lib.head config.locations;
|
|
||||||
};
|
|
||||||
body.content = [
|
|
||||||
(cfg.menus.main.outputs.html config)
|
(cfg.menus.main.outputs.html config)
|
||||||
{ section.heading.content = config.title; }
|
{
|
||||||
(cfg.templates.html.markdown config.name config.body)
|
section.heading = {
|
||||||
|
# TODO: i18n support
|
||||||
|
# TODO: structured dates
|
||||||
|
before = [{ p.content = "Published ${config.date}"; }];
|
||||||
|
content = config.title;
|
||||||
|
after = [{ p.content = "Written by ${config.author}"; }];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
(cfg.templates.html.markdown { inherit (config) name body; })
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
75
structure/collections.nix
Normal file
75
structure/collections.nix
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
{ config, options, lib, pkgs, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib)
|
||||||
|
mkOption
|
||||||
|
types
|
||||||
|
;
|
||||||
|
cfg = config;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.collections = mkOption {
|
||||||
|
description = ''
|
||||||
|
Named collections of unnamed pages
|
||||||
|
|
||||||
|
Define the content type of a new collection `example` to be `article`:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.collections.example.type = config.types.article;
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a new entry to the `example` collection:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
config.collections.example.entry = {
|
||||||
|
# contents here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
type = with types; attrsOf (submodule ({ name, config, ... }: {
|
||||||
|
options = {
|
||||||
|
type = mkOption {
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
prefixes = mkOption {
|
||||||
|
description = ''
|
||||||
|
List of historic output locations for files in the collection
|
||||||
|
|
||||||
|
The first element is the canonical location.
|
||||||
|
All other elements are used to create redirects to the canonical location.
|
||||||
|
|
||||||
|
The default entry is the symbolic name of the collection.
|
||||||
|
When changing the symbolic name, append the old one to your custom list and use `lib.mkForce` to make sure the default element will be overridden.
|
||||||
|
'';
|
||||||
|
type = with types; nonEmptyListOf str;
|
||||||
|
example = [ "." ];
|
||||||
|
default = [ config.name ];
|
||||||
|
};
|
||||||
|
entry = mkOption {
|
||||||
|
description = "An entry in the collection";
|
||||||
|
type = types.collection (types.submodule ({
|
||||||
|
imports = [ config.type ];
|
||||||
|
_module.args.collection = config;
|
||||||
|
process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls;
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
config.files =
|
||||||
|
# TODO: create static redirects from `tail <collection>.locations`
|
||||||
|
let
|
||||||
|
collections = with lib; concatMap (collection: collection.entry) (attrValues config.collections);
|
||||||
|
in
|
||||||
|
with lib; foldl
|
||||||
|
(acc: elem: acc // {
|
||||||
|
"${head elem.locations}.html" = builtins.toFile "${elem.name}.html" "${elem.outputs.html}";
|
||||||
|
})
|
||||||
|
{ }
|
||||||
|
collections;
|
||||||
|
}
|
|
@ -14,73 +14,51 @@ in
|
||||||
type = with types; attrsOf deferredModule;
|
type = with types; attrsOf deferredModule;
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: enable i18n, e.g. via a nested attribute for language-specific content
|
config.content-types.document = { name, config, options, link, ... }: {
|
||||||
options.pages = mkOption {
|
config._module.args.link = config.link;
|
||||||
description = ''
|
|
||||||
Collection of pages on the site
|
|
||||||
'';
|
|
||||||
type = with types; attrsOf (submodule config.content-types.page);
|
|
||||||
};
|
|
||||||
|
|
||||||
options.collections = mkOption {
|
|
||||||
description = ''
|
|
||||||
Named collections of unnamed pages
|
|
||||||
|
|
||||||
Define the content type of a new collection `example` to be `article`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
config.collections.example.type = config.types.article;
|
|
||||||
```
|
|
||||||
|
|
||||||
Add a new entry to the `example` collection:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
config.collections.example.entry = {
|
|
||||||
# contents here
|
|
||||||
}
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
type = with types; attrsOf (submodule ({ name, config, ... }: {
|
|
||||||
options = {
|
options = {
|
||||||
type = mkOption {
|
|
||||||
description = "Type of entries in the collection";
|
|
||||||
type = types.deferredModule;
|
|
||||||
};
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
prefixes = mkOption {
|
locations = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
List of historic output locations for files in the collection
|
List of historic output locations for the resulting file
|
||||||
|
|
||||||
|
Elements are relative paths to output files, without suffix.
|
||||||
|
The suffix will be added depending on output file type.
|
||||||
|
|
||||||
The first element is the canonical location.
|
The first element is the canonical location.
|
||||||
All other elements are used to create redirects to the canonical location.
|
All other elements are used to create redirects to the canonical location.
|
||||||
|
|
||||||
The default entry is the symbolic name of the collection.
|
The default entry is the symbolic name of the document.
|
||||||
When changing the symbolic name, append the old one to your custom list and use `lib.mkForce` to make sure the default element will be overridden.
|
When changing the symbolic name, append the old one to your custom list and use `lib.mkForce` to make sure the default element will be overridden.
|
||||||
'';
|
'';
|
||||||
type = with types; nonEmptyListOf str;
|
type = with types; nonEmptyListOf str;
|
||||||
example = [ "." ];
|
apply = config.process-locations;
|
||||||
|
example = [ "about/overview" "index" ];
|
||||||
default = [ config.name ];
|
default = [ config.name ];
|
||||||
};
|
};
|
||||||
entry = mkOption {
|
process-locations = mkOption {
|
||||||
description = "An entry in the collection";
|
description = "Function to post-process the output locations of contained document";
|
||||||
type = types.collection (types.submodule ({
|
type = types.functionTo options.locations.type;
|
||||||
imports = [ config.type ];
|
default = lib.id;
|
||||||
_module.args.collection = config;
|
|
||||||
process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls;
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
link = mkOption {
|
||||||
|
description = "Helper function for transparent linking to other pages";
|
||||||
|
type = with types; functionTo str;
|
||||||
|
# TODO: we may want links to other representations,
|
||||||
|
# and currently the mapping of output types to output file
|
||||||
|
# names is soft.
|
||||||
|
default = target: with lib; "${relativePath (head config.locations) (head target.locations)}.html";
|
||||||
};
|
};
|
||||||
}));
|
outputs = mkOption {
|
||||||
};
|
|
||||||
|
|
||||||
options.menus = mkOption {
|
|
||||||
description = ''
|
description = ''
|
||||||
Collection navigation menus
|
Representations of the document in different formats
|
||||||
'';
|
'';
|
||||||
type = with types; attrsOf (submodule config.content-types.navigation);
|
type = with types; attrsOf (either str attrs);
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
{ lib, ... }:
|
|
||||||
let
|
|
||||||
inherit (lib)
|
|
||||||
mkOption
|
|
||||||
types
|
|
||||||
;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
content-types.document = { name, config, options, link, ... }: {
|
|
||||||
config._module.args.link = config.link;
|
|
||||||
options = {
|
|
||||||
name = mkOption {
|
|
||||||
description = "Symbolic name, used as a human-readable identifier";
|
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
};
|
|
||||||
locations = mkOption {
|
|
||||||
description = ''
|
|
||||||
List of historic output locations for the resulting file
|
|
||||||
|
|
||||||
Elements are relative paths to output files, without suffix.
|
|
||||||
The suffix will be added depending on output file type.
|
|
||||||
|
|
||||||
The first element is the canonical location.
|
|
||||||
All other elements are used to create redirects to the canonical location.
|
|
||||||
|
|
||||||
The default entry is the symbolic name of the document.
|
|
||||||
When changing the symbolic name, append the old one to your custom list and use `lib.mkForce` to make sure the default element will be overridden.
|
|
||||||
'';
|
|
||||||
type = with types; nonEmptyListOf str;
|
|
||||||
apply = config.process-locations;
|
|
||||||
example = [ "about/overview" "index" ];
|
|
||||||
default = [ config.name ];
|
|
||||||
};
|
|
||||||
process-locations = mkOption {
|
|
||||||
description = "Function to post-process the output locations of contained document";
|
|
||||||
type = types.functionTo options.locations.type;
|
|
||||||
default = lib.id;
|
|
||||||
};
|
|
||||||
link = mkOption {
|
|
||||||
description = "Helper function for transparent linking to other pages";
|
|
||||||
type = with types; functionTo str;
|
|
||||||
# TODO: we may want links to other representations,
|
|
||||||
# and currently the mapping of output types to output file
|
|
||||||
# names is soft.
|
|
||||||
default = target: with lib; "${relativePath (head config.locations) (head target.locations)}.html";
|
|
||||||
};
|
|
||||||
outputs.html = mkOption {
|
|
||||||
# TODO: make this of type DOM and convert to string at the output.
|
|
||||||
# the output aggregator then only needs something string-coercible
|
|
||||||
description = ''
|
|
||||||
Representations of the document in different formats
|
|
||||||
'';
|
|
||||||
type = with types; str;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -16,7 +16,14 @@ let
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
content-types.named-link = { ... }: {
|
options.menus = mkOption {
|
||||||
|
description = ''
|
||||||
|
Collection navigation menus
|
||||||
|
'';
|
||||||
|
type = with types; attrsOf (submodule config.content-types.navigation);
|
||||||
|
};
|
||||||
|
|
||||||
|
config.content-types.named-link = { ... }: {
|
||||||
options = {
|
options = {
|
||||||
label = mkOption {
|
label = mkOption {
|
||||||
description = "Link label";
|
description = "Link label";
|
||||||
|
@ -29,7 +36,7 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
content-types.navigation = { name, config, ... }: {
|
config.content-types.navigation = { 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";
|
||||||
|
@ -56,7 +63,9 @@ in
|
||||||
It must be a function that takes the page on which the navigation is to be shown, such that relative links get computed correctly.
|
It must be a function that takes the page on which the navigation is to be shown, such that relative links get computed correctly.
|
||||||
'';
|
'';
|
||||||
type = with types; attrsOf (functionTo str);
|
type = with types; attrsOf (functionTo str);
|
||||||
default.html = cfg.templates.html.nav config;
|
default.html = page: cfg.templates.html.nav {
|
||||||
|
menu = config; inherit page;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,17 +5,26 @@ let
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
cfg = config;
|
cfg = config;
|
||||||
render-html = document:
|
|
||||||
let
|
|
||||||
eval = lib.evalModules {
|
|
||||||
class = "DOM";
|
|
||||||
modules = [ document (import ../presentation/dom.nix) ];
|
|
||||||
};
|
|
||||||
in
|
|
||||||
toString eval.config;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
content-types.page = { name, config, ... }: {
|
# TODO: enable i18n, e.g. via a nested attribute for language-specific content
|
||||||
|
options.pages = mkOption {
|
||||||
|
description = ''
|
||||||
|
Collection of pages on the site
|
||||||
|
'';
|
||||||
|
type = with types; attrsOf (submodule config.content-types.page);
|
||||||
|
};
|
||||||
|
config.files = with lib;
|
||||||
|
foldl'
|
||||||
|
(acc: elem: acc // {
|
||||||
|
# TODO: create static redirects from `tail page.locations`
|
||||||
|
# TODO: the file name could correspond to the canonical location in the HTML representation
|
||||||
|
"${head elem.locations}.html" = builtins.toFile "${elem.name}.html" "${elem.outputs.html}";
|
||||||
|
})
|
||||||
|
{ }
|
||||||
|
(attrValues config.pages);
|
||||||
|
|
||||||
|
config.content-types.page = { name, config, ... }: {
|
||||||
imports = [ cfg.content-types.document ];
|
imports = [ cfg.content-types.document ];
|
||||||
options = {
|
options = {
|
||||||
title = mkOption {
|
title = mkOption {
|
||||||
|
@ -42,19 +51,22 @@ in
|
||||||
type = types.str;
|
type = types.str;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.outputs.html = render-html {
|
|
||||||
|
config.outputs.html = cfg.templates.html.page config;
|
||||||
|
};
|
||||||
|
|
||||||
|
config.templates.html.page = lib.template cfg.templates.html.dom (page: {
|
||||||
html = {
|
html = {
|
||||||
head = {
|
head = {
|
||||||
title.text = config.title;
|
title.text = page.title;
|
||||||
meta.description = config.description;
|
meta.description = page.description;
|
||||||
link.canonical = lib.head config.locations;
|
link.canonical = lib.head page.locations;
|
||||||
};
|
};
|
||||||
body.content = [
|
body.content = [
|
||||||
(cfg.menus.main.outputs.html config)
|
(cfg.menus.main.outputs.html page)
|
||||||
{ section.heading.content = config.title; }
|
{ section.heading.content = page.title; }
|
||||||
(cfg.templates.html.markdown config.name config.body)
|
(cfg.templates.html.markdown { inherit (page) name body; })
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
});
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue