website: format

This commit is contained in:
Nicolas Jeannerod 2025-02-19 18:34:19 +01:00 committed by Valentin Gagarin
parent 92563d387a
commit 10f3d15a98
24 changed files with 1679 additions and 1258 deletions

View file

@ -9,7 +9,9 @@ in
collections.news.type = cfg.content-types.article;
collections.events.type = cfg.content-types.event;
pages.index = { config, link, ... }: {
pages.index =
{ config, link, ... }:
{
title = "Welcome to the Fediversity project";
description = "Fediversity web site";
summary = ''
@ -20,7 +22,8 @@ in
[Learn more about Fediversity](${link pages.fediversity})
'';
outputs.html = (cfg.templates.html.page config).override (final: prev: {
outputs.html = (cfg.templates.html.page config).override (
final: prev: {
html = {
head.title.text = "Fediversity";
head.link.stylesheets = prev.html.head.link.stylesheets ++ [
@ -28,7 +31,13 @@ in
];
body.content =
let
to-section = { heading, body, attrs ? { } }: {
to-section =
{
heading,
body,
attrs ? { },
}:
{
section = {
heading.content = heading;
inherit attrs;
@ -47,11 +56,11 @@ in
section = {
attrs = { };
heading.content = config.title;
content = [
content =
[
(cfg.templates.html.markdown { inherit (config) name body; })
]
++
(map to-section [
++ (map to-section [
{
heading = "Fediversity grants";
body = ''
@ -65,68 +74,95 @@ in
body = ''
The Consortium behind the Fediversity project is a cooperation between NLnet, Open Internet Discourse Foundation, NORDUnet and Tweag.
${toString (map (partner: ''
${toString (
map
(partner: ''
### ${partner.title}
${partner.summary}
[Read more about ${partner.title}](${link partner})
'') (with pages; [ nlnet oid tweag nordunet ]))}
'')
(
with pages;
[
nlnet
oid
tweag
nordunet
]
)
)}
'';
}
{
heading = "Fediverse explained";
body = ''
${toString (map (role: ''
${toString (
map
(role: ''
### ${role.title}
${role.summary}
[Read more about ${role.title}](${link role})
'') (with pages; [ individuals developers european-commission ]))}
'')
(
with pages;
[
individuals
developers
european-commission
]
)
)}
'';
}
]);
};
}
]
++
(map to-section [
++ (map to-section [
{
heading = "News";
attrs = { class = [ "collection" ]; };
attrs = {
class = [ "collection" ];
};
body =
let
sorted = with lib; reverseList (sortOn (entry: entry.date) cfg.collections.news.entry);
in
lib.join "\n" (map
(article: ''
lib.join "\n" (
map (article: ''
- ${article.date} [${article.title}](${link article})
'')
sorted);
'') sorted
);
}
{
heading = "Events";
attrs = { class = [ "collection" ]; };
attrs = {
class = [ "collection" ];
};
body =
let
sorted = with lib; reverseList (sortOn (entry: entry.start-date) cfg.collections.events.entry);
in
lib.join "\n" (map
(article: ''
lib.join "\n" (
map (article: ''
- ${article.start-date} [${article.title}](${link article})
'')
sorted);
'') sorted
);
}
]);
};
});
}
);
};
assets."index.css".path = with lib; builtins.toFile
"index.css"
''
assets."index.css".path =
with lib;
builtins.toFile "index.css" ''
section h1, section h2, section h3
{
text-align: center;

View file

@ -1,19 +1,23 @@
{ config, lib, ... }:
{
pages.events = { link, ... }: rec {
pages.events =
{ link, ... }:
rec {
title = "Events";
description = "Events related to the Fediverse and NixOS";
summary = description;
body =
with lib;
let
events = map
(event: with lib; ''
events = map (
event: with lib; ''
## [${event.title}](${link event})
${event.start-date} ${optionalString (!isNull event.end-date && event.end-date != event.start-date) "to ${event.end-date}"} in ${event.location}
'')
config.collections.events.entry;
${event.start-date} ${
optionalString (!isNull event.end-date && event.end-date != event.start-date) "to ${event.end-date}"
} in ${event.location}
''
) config.collections.events.entry;
in
''
${join "\n" events}

View file

@ -1,6 +1,8 @@
{ config, lib, ... }:
{
collections.events.entry = { link, ... }: {
collections.events.entry =
{ link, ... }:
{
title = "NixOS 24.11 ZHF hackathon";
name = "zhf-24-11";
description = "NixOS 24.11 ZHF hackathon in Zürich";

View file

@ -1,6 +1,8 @@
{ ... }:
{
collections.events.entry = { ... }: {
collections.events.entry =
{ ... }:
{
title = "OW2con 2024";
description = "OW2con is the annual European open source conference in Paris";
start-date = "2024-06-11";

View file

@ -1,6 +1,8 @@
{ ... }:
{
collections.events.entry = { ... }: {
collections.events.entry =
{ ... }:
{
title = "PublicSpaces Conference 2024";
description = "A conference by PublicSpaces, Taking Back the Internet.";
start-date = "2024-06-06";

View file

@ -1,6 +1,8 @@
{ ... }:
{
collections.events.entry = { ... }: {
collections.events.entry =
{ ... }:
{
title = "State of the Internet 2024";
description = "The State of the Internet 2024 by Waag";
start-date = "2024-05-16";

View file

@ -6,16 +6,33 @@ in
menus.main = {
label = "Main";
items = [
{ page = pages.index // { title = "Start"; }; }
{
page = pages.index // {
title = "Start";
};
}
{
menu.label = "For you";
menu.items = map (page: { inherit page; })
(with pages; [ individuals developers european-commission ]);
menu.items = map (page: { inherit page; }) (
with pages;
[
individuals
developers
european-commission
]
);
}
{
menu.label = "Consortium";
menu.items = map (page: { inherit page; })
(with pages; [ nlnet oid tweag nordunet ]);
menu.items = map (page: { inherit page; }) (
with pages;
[
nlnet
oid
tweag
nordunet
]
);
}
{ page = pages.fediversity; }
{ page = pages.grants; }

View file

@ -1,21 +1,21 @@
{ config, lib, ... }:
{
pages.news = { link, ... }: rec {
pages.news =
{ link, ... }:
rec {
title = "News";
description = "News about Fediversity";
summary = description;
body =
with lib;
let
news = map
(article: ''
news = map (article: ''
## [${article.title}](${link article})
${article.date} by ${article.author}
${article.summary}
'')
config.collections.news.entry;
'') config.collections.news.entry;
in
''
${join "\n\n" news}

View file

@ -1,6 +1,8 @@
{ config, lib, ... }:
{
collections.news.entry = { link, ... }: rec {
collections.news.entry =
{ link, ... }:
rec {
name = "zhf-24-11";
title = "NixOS 24.11 release hackathon and workshop";
description = "Fediversity engineers met in Zürich at a NixOS 24.11 ZHF hackathon";

View file

@ -1,6 +1,8 @@
{ config, lib, ... }:
{
collections.news.entry = { link, ... }: {
collections.news.entry =
{ link, ... }:
{
title = "Fediversity project publicly announced";
description = "The Fediversity project has officially been announced";
date = "2024-01-01";

View file

@ -1,14 +1,16 @@
{ sources ? import ../npins
, system ? builtins.currentSystem
, pkgs ? import sources.nixpkgs {
{
sources ? import ../npins,
system ? builtins.currentSystem,
pkgs ? import sources.nixpkgs {
inherit system;
config = { };
overlays = [ ];
}
, lib ? import "${sources.nixpkgs}/lib"
},
lib ? import "${sources.nixpkgs}/lib",
}:
let
lib' = final: prev:
lib' =
final: prev:
let
new = import ./lib.nix { lib = final; };
in
@ -37,13 +39,19 @@ rec {
let
run-tests = pkgs.writeShellApplication {
name = "run-tests";
text = with pkgs; with lib; ''
text =
with pkgs;
with lib;
''
${getExe nix-unit} ${toString ./tests.nix} "$@"
'';
};
test-loop = pkgs.writeShellApplication {
name = "test-loop";
text = with pkgs; with lib; ''
text =
with pkgs;
with lib;
''
${getExe watchexec} -w ${toString ./.} -- ${getExe nix-unit} ${toString ./tests.nix}
'';
};
@ -62,7 +70,9 @@ rec {
};
inherit sources pkgs;
tests = with pkgs; with lib;
tests =
with pkgs;
with lib;
let
source = fileset.toSource {
root = ../.;

View file

@ -1,21 +1,25 @@
{ lib }:
rec {
template = g: f: x:
template =
g: f: x:
let
base = f x;
result = g base;
in
result // {
override = new:
result
// {
override =
new:
let
base' =
if lib.isFunction new
then lib.recursiveUpdate base (new base' base)
if lib.isFunction new then
lib.recursiveUpdate base (new base' base)
else
lib.recursiveUpdate base new;
result' = g base';
in
result' // {
result'
// {
override = new: (template g (x': base') x).override new;
};
};
@ -28,7 +32,8 @@ rec {
replaceStringRec "--" "-" "hello-----world"
=> "hello-world"
*/
replaceStringsRec = from: to: string:
replaceStringsRec =
from: to: string:
let
replaced = lib.replaceStrings [ from ] [ to ] string;
in
@ -37,25 +42,24 @@ rec {
/**
Create a URL-safe slug from any string
*/
slug = str:
slug =
str:
let
# Replace non-alphanumeric characters with hyphens
replaced = join ""
(
builtins.map
(c:
if (c >= "a" && c <= "z") || (c >= "0" && c <= "9")
then c
else "-"
replaced = join "" (
builtins.map (c: if (c >= "a" && c <= "z") || (c >= "0" && c <= "9") then c else "-") (
with lib; stringToCharacters (toLower str)
)
(with lib; stringToCharacters (toLower str)));
);
# Remove leading and trailing hyphens
trimHyphens = s:
trimHyphens =
s:
let
matched = builtins.match "(-*)([^-].*[^-]|[^-])(-*)" s;
in
with lib; optionalString (!isNull matched) (builtins.elemAt matched 1);
with lib;
optionalString (!isNull matched) (builtins.elemAt matched 1);
in
trimHyphens (replaceStringsRec "--" "-" replaced);
@ -64,9 +68,11 @@ rec {
/**
Trim trailing spaces and squash non-leading spaces
*/
trim = string:
trim =
string:
let
trimLine = line:
trimLine =
line:
with lib;
let
# separate leading spaces from the rest
@ -76,8 +82,7 @@ rec {
# drop trailing spaces
body = head (split " *$" rest);
in
if body == "" then "" else
spaces + replaceStringsRec " " " " body;
if body == "" then "" else spaces + replaceStringsRec " " " " body;
in
join "\n" (map trimLine (splitLines string));
@ -85,35 +90,42 @@ rec {
splitLines = s: with builtins; filter (x: !isList x) (split "\n" s);
indent = prefix: s:
indent =
prefix: s:
with lib.lists;
let
lines = splitLines s;
in
join "\n" (
[ (head lines) ]
++
(map (x: if x == "" then x else "${prefix}${x}") (tail lines))
);
join "\n" ([ (head lines) ] ++ (map (x: if x == "" then x else "${prefix}${x}") (tail lines)));
relativePath = path1': path2':
relativePath =
path1': path2':
let
inherit (lib.path) subpath;
inherit (lib) lists length take drop min max;
inherit (lib)
lists
length
take
drop
min
max
;
path1 = subpath.components path1';
prefix1 = take (length path1 - 1) path1;
path2 = subpath.components path2';
prefix2 = take (length path2 - 1) path2;
commonPrefixLength = with lists;
findFirstIndex (i: i.fst != i.snd)
(min (length prefix1) (length prefix2))
(zipLists prefix1 prefix2);
commonPrefixLength =
with lists;
findFirstIndex (i: i.fst != i.snd) (min (length prefix1) (length prefix2)) (
zipLists prefix1 prefix2
);
depth = max 0 (length prefix1 - commonPrefixLength);
relativeComponents = with lists;
relativeComponents =
with lists;
[ "." ] ++ (replicate depth "..") ++ (drop commonPrefixLength path2);
in
join "/" relativeComponents;
@ -122,47 +134,51 @@ rec {
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")
);
*
*/
nixFiles =
dir:
with lib.fileset;
toList (difference (fileFilter ({ hasExt, ... }: hasExt "nix") dir) (dir + "/default.nix"));
types = rec {
# arbitrarily nested attribute set where the leaves are of type `type`
# NOTE: this works for anything but attribute sets!
recursiveAttrs = type: with lib.types;
recursiveAttrs =
type:
with lib.types;
# 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`
attrsOf (either type (recursiveAttrs type));
# collection of unnamed items that can be added to item-wise, i.e. without wrapping the item in a list
collection = elemType:
collection =
elemType:
let
unparenthesize = class: class == "noun";
desc = type:
types.optionDescriptionPhrase unparenthesize type;
desc' = type:
desc = type: types.optionDescriptionPhrase unparenthesize type;
desc' =
type:
let
typeDesc = lib.types.optionDescriptionPhrase unparenthesize type;
in
if type.descriptionClass == "noun"
then
typeDesc + "s"
else
"many instances of ${typeDesc}";
if type.descriptionClass == "noun" then typeDesc + "s" else "many instances of ${typeDesc}";
in
lib.types.mkOptionType {
name = "collection";
description = "separately specified ${desc elemType} for a collection of ${desc' elemType}";
merge = loc: defs:
map
(def:
elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [{ inherit (def) file; value = def.value; }]
)
defs;
merge =
loc: defs:
map (
def:
elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [
{
inherit (def) file;
value = def.value;
}
]
) defs;
check = elemType.check;
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
@ -175,29 +191,34 @@ rec {
nestedTypes.elemType = elemType;
};
listOfUnique = elemType:
listOfUnique =
elemType:
let
baseType = lib.types.listOf elemType;
in
baseType // {
merge = loc: defs:
baseType
// {
merge =
loc: defs:
let
# Keep track of which definition each value came from
defsWithValues = map
(def:
map (v: { inherit (def) file; value = v; }) def.value
)
defs;
defsWithValues = map (
def:
map (v: {
inherit (def) file;
value = v;
}) def.value
) defs;
flatDefs = lib.flatten defsWithValues;
# Check for duplicates while preserving source info
seen = builtins.foldl'
(acc: def:
if lib.lists.any (v: v.value == def.value) acc
then throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}"
else acc ++ [ def ]
) [ ]
flatDefs;
seen = builtins.foldl' (
acc: def:
if lib.lists.any (v: v.value == def.value) acc then
throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}"
else
acc ++ [ def ]
) [ ] flatDefs;
in
map (def: def.value) seen;
};

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }:
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
mkOption
@ -8,8 +14,7 @@ in
{
imports = lib.nixFiles ./.;
options.templates =
mkOption {
options.templates = mkOption {
description = ''
Collection of named helper functions for conversion different structured representations which can be rendered to a string
'';
@ -32,32 +37,35 @@ in
type = types.package;
default =
let
script = ''
script =
''
mkdir $out
'' + lib.join "\n" copy;
copy = lib.mapAttrsToList
(
path: file: ''
''
+ lib.join "\n" copy;
copy = lib.mapAttrsToList (path: file: ''
mkdir -p $out/$(dirname ${path})
cp -r ${file} $out/${path}
''
)
config.files;
'') config.files;
in
pkgs.runCommand "source" { } script;
};
# TODO: this is an artefact of exploration; needs to be adapted to actual use
config.templates.table-of-contents = { config, ... }:
config.templates.table-of-contents =
{ config, ... }:
let
outline = { ... }: {
outline =
{ ... }:
{
options = {
value = mkOption {
# null denotes root
type = with types; nullOr (either str (listOf (attrTag categories.phrasing)));
subsections = mkOption {
type = with types; listOf (submodule outline);
default = with lib; map
default =
with lib;
map
# TODO: go into depth manually here,
# we don't want to pollute the DOM implementation
(c: (lib.head (attrValues c)).outline)
@ -67,10 +75,11 @@ in
__toString = mkOption {
type = with types; functionTo str;
# TODO: convert to HTML
default = self: lib.squash ''
default =
self:
lib.squash ''
${if isNull self.value then "root" else self.value}
${if self.subsections != [] then
" " + lib.indent " " (lib.join "\n" self.subsections) else ""}
${if self.subsections != [ ] then " " + lib.indent " " (lib.join "\n" self.subsections) else ""}
'';
};
};
@ -81,9 +90,11 @@ in
type = types.submodule outline;
default = {
value = null;
subsections = with lib;
map (c: (lib.head (attrValues c)).outline)
(filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content);
subsections =
with lib;
map (c: (lib.head (attrValues c)).outline) (
filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content
);
};
};
};

View file

@ -28,7 +28,9 @@ let
];
# base type for all DOM elements
element = { ... }: {
element =
{ ... }:
{
# TODO: add fields for upstream documentation references
# TODO: programmatically generate documentation
options = with lib; {
@ -43,20 +45,28 @@ let
};
# options with types for all the defined DOM elements
element-types = lib.mapAttrs
(name: value: mkOption { type = submodule value; })
elements;
element-types = lib.mapAttrs (name: value: mkOption { type = submodule value; }) elements;
# attrset of categories, where values are module options with the type of the
# elements that belong to these categories
categories = with lib;
genAttrs
content-categories
(category:
categories =
with lib;
genAttrs content-categories (
category:
(mapAttrs (_: e: mkOption { type = submodule e; })
# HACK: don't evaluate the submodule types, just grab the config directly
# TODO: we may want to do this properly and loop `categories` through the top-level `config`
(filterAttrs (_: e: elem category (e { name = "dummy"; config = { }; }).config.categories) elements))
(
filterAttrs (
_: e:
elem category
(e {
name = "dummy";
config = { };
}).config.categories
) elements
)
)
);
global-attrs = lib.mapAttrs (name: value: mkOption value) {
@ -131,7 +141,8 @@ let
# https://html.spec.whatwg.org/multipage/document-sequences.html#valid-navigable-target-name-or-keyword
type =
let
is-valid-target = s:
is-valid-target =
s:
let
inherit (lib) match;
has-lt = s: match ".*<.*" s != null;
@ -140,14 +151,19 @@ let
in
has-valid-start s && !(has-lt s && has-tab-or-newline s);
in
with types; either
(enum [ "_blank" "_self" "_parent" "_top" ])
(types.addCheck str is-valid-target)
;
with types;
either (enum [
"_blank"
"_self"
"_parent"
"_top"
]) (types.addCheck str is-valid-target);
};
};
mkAttrs = attrs: with lib;
mkAttrs =
attrs:
with lib;
mkOption {
type = submodule {
options = global-attrs // attrs;
@ -155,28 +171,33 @@ let
default = { };
};
print-attrs = with lib; attrs:
print-attrs =
with lib;
attrs:
# TODO: figure out how let attributes know how to print themselves without polluting the interface
let
result = trim (join " "
(mapAttrsToList
result = trim (
join " " (
mapAttrsToList
# TODO: this needs to be smarter for boolean attributes
# where the value must be written out explicitly.
# probably the attribute itself should have its own `__toString`.
(name: value:
(
name: value:
if isBool value then
if value then name else ""
# TODO: some attributes must be explicitly empty
else optionalString (toString value != "") ''${name}="${toString value}"''
else
optionalString (toString value != "") ''${name}="${toString value}"''
)
attrs
)
attrs)
);
in
if attrs == null then throw "wat" else
optionalString (stringLength result > 0) " " + result
;
if attrs == null then throw "wat" else optionalString (stringLength result > 0) " " + result;
print-element = name: attrs: content:
print-element =
name: attrs: content:
with lib;
# TODO: be smarter about content to save some space and repetition at the call sites
squash (trim ''
@ -187,16 +208,20 @@ let
print-element' = name: attrs: "<${name}${print-attrs attrs}>";
toString-unwrap = e:
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;
if isAttrs e then
toString (head (attrValues e))
else if isList e then
toString (map toString-unwrap e)
else
e;
elements = rec {
document = { ... }: {
document =
{ ... }:
{
imports = [ element ];
options = {
inherit (element-types) html;
@ -210,7 +235,9 @@ let
'';
};
html = { name, ... }: {
html =
{ name, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };
@ -218,13 +245,17 @@ let
};
config.categories = [ ];
config.__toString = self: print-element name self.attrs ''
config.__toString =
self:
print-element name self.attrs ''
${self.head}
${self.body}
'';
};
head = { name, ... }: {
head =
{ name, ... }:
{
imports = [ element ];
options = with lib; {
attrs = mkAttrs { };
@ -248,19 +279,17 @@ let
# 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
meta.viewport = mkOption {
type = submodule ({ ... }: {
type = submodule (
{ ... }:
{
# TODO: figure out how to render only non-default values
options = {
width = mkOption {
type = with types; either
(ints.between 1 10000)
(enum [ "device-width" ]);
type = with types; either (ints.between 1 10000) (enum [ "device-width" ]);
default = "device-width"; # not default by standard
};
height = mkOption {
type = with types; either
(ints.between 1 10000)
(enum [ "device-height" ]);
type = with types; either (ints.between 1 10000) (enum [ "device-height" ]);
default = "device-height"; # not default by standard (but seems to work if you don't set it)
};
initial-scale = mkOption {
@ -289,7 +318,8 @@ let
default = "resizes-visual";
};
};
});
}
);
default = { };
};
@ -318,15 +348,18 @@ let
};
config.categories = [ ];
config.__toString = self:
config.__toString =
self:
with lib;
print-element name self.attrs ''
${self.title}
${with lib; optionalString (!isNull self.base) self.base}
<meta charset="${self.meta.charset}" />
${/* https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-x-ua-compatible */
""}<meta http-equiv="X-UA-Compatible" content="IE=edge" />
${
# https://html.spec.whatwg.org/multipage/semantics.html#attr-meta-http-equiv-x-ua-compatible
""
}<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--
TODO: make proper icon and preload types
-->
@ -336,25 +369,41 @@ let
${print-element' "meta" {
name = "viewport";
content = "${join ", " (mapAttrsToList (name: value: "${name}=${toString value}") self.meta.viewport) }";
content = "${join ", " (
mapAttrsToList (name: value: "${name}=${toString value}") self.meta.viewport
)}";
}}
${join "\n" (map
(author: print-element' "meta" {
${join "\n" (
map (
author:
print-element' "meta" {
name = "author";
content = "${author}";
})
self.meta.authors)
}
) self.meta.authors
)}
${join "\n" (map
(stylesheet: print-element' "link" ({ rel = "stylesheet"; } // (removeAttrs stylesheet [ "categories" "__toString" ])))
self.link.stylesheets)
${join "\n" (
map (
stylesheet:
print-element' "link" (
{
rel = "stylesheet";
}
// (removeAttrs stylesheet [
"categories"
"__toString"
])
)
) self.link.stylesheets
)}
'';
};
title = { name, ... }: {
title =
{ name, ... }:
{
imports = [ element ];
options.attrs = mkAttrs { };
options.text = mkOption {
@ -365,15 +414,21 @@ let
};
base = { name, ... }: {
base =
{ name, ... }:
{
imports = [ element ];
# TODO: "A base element must have either an href attribute, a target attribute, or both."
options = global-attrs // { inherit (attrs) href target; };
options = global-attrs // {
inherit (attrs) href target;
};
config.categories = [ "metadata" ];
config.__toString = self: "<base${print-attrs self}>";
};
link = { name, ... }: {
link =
{ name, ... }:
{
imports = [ element ];
options = global-attrs // {
# TODO: more attributes
@ -382,7 +437,9 @@ let
# XXX: there are variants of `rel` for `link`, `a`/`area`, and `form`
rel = mkOption {
# https://html.spec.whatwg.org/multipage/semantics.html#attr-link-rel
type = with types; listOfUnique str (enum
type =
with types;
listOfUnique str (enum
# TODO: work out link types in detail, there are lots of additional constraints
# https://html.spec.whatwg.org/multipage/links.html#linkTypes
[
@ -403,8 +460,7 @@ let
"privacy-policy"
"search"
"terms-of-service"
]
);
]);
};
};
# TODO: figure out how to make body-ok `link` elements
@ -415,7 +471,9 @@ let
# <link rel="stylesheet"> is implemented separately because it can be used both in `<head>` and `<body>`
# semantically it's a standalone thing but syntactically happens to be subsumed under `<link>`
stylesheet = { config, name, ... }: {
stylesheet =
{ config, name, ... }:
{
imports = [ element ];
options = global-attrs // {
type = mkOption {
@ -440,49 +498,74 @@ let
inherit (link-attrs) href media integrity;
};
# https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:body-ok
config.categories = [ "metadata" "phrasing" ];
config.__toString = self: print-attrs (removeAttrs self [ "categories" "__toString" ]);
config.categories = [
"metadata"
"phrasing"
];
config.__toString =
self:
print-attrs (
removeAttrs self [
"categories"
"__toString"
]
);
};
body = { config, name, ... }: {
body =
{ config, name, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };
content = mkOption {
type = with types;
type =
with types;
let
# Type check that ensures spec-compliant section hierarchy
# https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines-2:concept-heading-7
with-section-constraints = baseType: baseType // {
merge = loc: defs:
with-section-constraints =
baseType:
baseType
// {
merge =
loc: defs:
with lib;
let
find-and-attach = def:
find-and-attach =
def:
let
process-with-depth = depth: content:
map
(x:
if isAttrs x && x ? section
then x // {
process-with-depth =
depth: content:
map (
x:
if isAttrs x && x ? section then
x
// {
section = x.section // {
heading-level = depth;
content = process-with-depth (depth + 1) (x.section.content or [ ]);
};
}
else x
)
content;
else
x
) content;
find-with-depth = depth: content:
find-with-depth =
depth: content:
let
sections = map (v: { inherit (def) file; value = v; depth = depth; })
(filter (x: isAttrs x && x ? section) content);
subsections = concatMap
(x:
if isAttrs x && x ? section && x.section ? content
then find-with-depth (depth + 1) x.section.content
else [ ])
content;
sections = map (v: {
inherit (def) file;
value = v;
depth = depth;
}) (filter (x: isAttrs x && x ? section) content);
subsections = concatMap (
x:
if isAttrs x && x ? section && x.section ? content then
find-with-depth (depth + 1) x.section.content
else
[ ]
) content;
in
sections ++ subsections;
@ -500,9 +583,12 @@ let
if too-deep != [ ] then
throw ''
The option `${lib.options.showOption loc}` has sections nested too deeply:
${concatMapStrings (sec: " - depth ${toString sec.depth} section in ${toString sec.file}\n") too-deep}
${concatMapStrings (
sec: " - depth ${toString sec.depth} section in ${toString sec.file}\n"
) too-deep}
Section hierarchy must not be deeper than 6 levels.''
else baseType.merge loc (map (p: p.def // { value = p.processed; }) processed);
else
baseType.merge loc (map (p: p.def // { value = p.processed; }) processed);
};
in
with-section-constraints
@ -513,16 +599,22 @@ let
};
config.categories = [ ];
config.__toString = self: with lib;
print-element name self.attrs (join "\n" (map toString-unwrap self.content));
config.__toString =
self: with lib; print-element name self.attrs (join "\n" (map toString-unwrap self.content));
};
section = { config, name, ... }: {
section =
{ config, name, ... }:
{
imports = [ element ];
options = {
# setting to an attribute set will wrap the section in `<section>`
attrs = mkOption {
type = with types; nullOr (submodule { options = global-attrs; });
type =
with types;
nullOr (submodule {
options = global-attrs;
});
default = null;
};
heading = mkOption {
@ -534,13 +626,21 @@ let
# such an outline is rather meaningless without headings for navigation,
# which is why we enforce headings in sections.
# arguably, and this is encoded here, a section *is defined* by its heading.
type = with types; submodule ({ config, ... }: {
type =
with types;
submodule (
{ config, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };
# setting to an attribute set will wrap the section in `<hgroup>`
hgroup.attrs = mkOption {
type = with types; nullOr (submodule { options = global-attrs; });
type =
with types;
nullOr (submodule {
options = global-attrs;
});
default = with lib; if (config.before == [ ] && config.after == [ ]) then null else { };
};
# https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element
@ -553,12 +653,12 @@ let
type = with types; either str (listOf (attrTag categories.phrasing));
};
after = mkOption {
type = with types;
listOf (attrTag ({ inherit (element-types) p; } // categories.scripting));
type = with types; listOf (attrTag ({ inherit (element-types) p; } // categories.scripting));
default = [ ];
};
};
});
}
);
};
# https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines
content = mkOption {
@ -573,28 +673,35 @@ let
internal = true;
};
config = {
categories = [ "flow" "sectioning" "palpable" ];
__toString = self: with lib;
categories = [
"flow"
"sectioning"
"palpable"
];
__toString =
self:
with lib;
let
n = toString config.heading-level;
heading = ''<h${n}${print-attrs self.heading.attrs}>${self.heading.content}</h${n}>'';
hgroup = with lib; print-element "hgroup" self.heading.hgroup.attrs (squash ''
hgroup =
with lib;
print-element "hgroup" self.heading.hgroup.attrs (squash ''
${optionalString (!isNull self.heading.before) (toString-unwrap self.heading.before)}
${heading}
${optionalString (!isNull self.heading.after) (toString-unwrap self.heading.after)}
'');
content =
(if isNull self.heading.hgroup.attrs then heading else hgroup)
+
join "\n" (map toString-unwrap self.content);
+ join "\n" (map toString-unwrap self.content);
in
if !isNull self.attrs
then print-element name self.attrs content
else content;
if !isNull self.attrs then print-element name self.attrs content else content;
};
};
p = { name, ... }: {
p =
{ name, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };
@ -602,20 +709,34 @@ let
type = with types; either str (listOf (attrTag categories.phrasing));
};
};
config.categories = [ "flow" "palpable" ];
config.categories = [
"flow"
"palpable"
];
config.__toString = self: print-element name self.attrs (toString self.content);
};
dl = { config, name, ... }: {
dl =
{ config, name, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };
content = mkOption {
type = with types; listOf (submodule ({ ... }: {
type =
with types;
listOf (
submodule (
{ ... }:
{
options = {
# TODO: wrap in `<div>` if set
div.attrs = mkOption {
type = with types; nullOr (submodule { options = global-attrs; });
type =
with types;
nullOr (submodule {
options = global-attrs;
});
default = null;
};
before = mkOption {
@ -637,7 +758,9 @@ let
default = [ ];
};
};
}));
}
)
);
};
};
# XXX: here we can't express the spec requirement that `dl` is palpable if the list of term-description-pairs is nonempty.
@ -648,11 +771,12 @@ let
# it does help to concisely express type constraints on an element's children, but it seems that most of the categories in the spec can be ignored entirely in this implementation.
# the cleanup task would be to identify which categories are really helpful, and document the rationale for using that mechanism as well as the specific choice of categories to keep.
config.categories = [ "flow" ];
config.__toString = self:
config.__toString =
self:
with lib;
let
content = map
(entry:
content = map (
entry:
let
list = squash ''
${join "\n" entry.before}
@ -662,36 +786,53 @@ let
${join "\n" entry.after}
'';
in
if !isNull entry.div.attrs
then print-element "div" entry.div.attrs list
else list
)
self.content;
if !isNull entry.div.attrs then print-element "div" entry.div.attrs list else list
) self.content;
in
print-element name self.attrs (join "\n" content);
};
dt = { config, ... }: {
dt =
{ config, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };
dt = mkOption {
type = with types; either str (submodule (attrTag (
type =
with types;
either str (
submodule (
attrTag (
# TODO: test
with lib; removeAttrs
(filterAttrs
(name: value: ! any (c: elem c [ "sectioning" "heading" ]) value.categories)
categories.flow
with lib;
removeAttrs
(filterAttrs (
name: value:
!any (
c:
elem c [
"sectioning"
"heading"
]
) value.categories
) categories.flow)
[
"header"
"footer"
]
)
[ "header" "footer" ]
)));
)
);
};
};
config.categories = [ ];
config.__toString = self: print-element "dt" self.attrs self.dt;
};
dd = { config, ... }: {
dd =
{ config, ... }:
{
imports = [ element ];
options = {
attrs = mkAttrs { };

View file

@ -1,9 +1,19 @@
{ config, lib, pkgs, ... }: {
{
config,
lib,
pkgs,
...
}:
{
config.assets."style.css".path = ./style.css;
config.assets."ngi-fediversity.svg".path = ./ngi-fediversity.svg;
# TODO: auto-generate a bunch from SVG
config.assets."favicon.png".path = ./favicon.png;
config.assets."fonts.css".path = with lib; builtins.toFile "fonts.css" (join "\n" (map
config.assets."fonts.css".path =
with lib;
builtins.toFile "fonts.css" (
join "\n" (
map
(font: ''
@font-face {
font-family: '${font.name}';
@ -13,15 +23,31 @@
}
'')
(
(crossLists (name: file: weight: { inherit name file weight; })
[ [ "Signika" ] [ "signika-extended.woff2" "signika.woff2" ] [ 500 700 ] ]
)
++
(crossLists (name: file: weight: { inherit name file weight; })
[ [ "Heebo" ] [ "heebo-extended.woff2" "heebo.woff2" ] [ 400 600 ] ]
(crossLists (name: file: weight: { inherit name file weight; }) [
[ "Signika" ]
[
"signika-extended.woff2"
"signika.woff2"
]
[
500
700
]
])
++ (crossLists (name: file: weight: { inherit name file weight; }) [
[ "Heebo" ]
[
"heebo-extended.woff2"
"heebo.woff2"
]
[
400
600
]
])
)
)
));
);
# TODO: get directly from https://github.com/google/fonts
# and compress with https://github.com/fonttools/fonttools

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }:
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
mkOption
@ -7,11 +13,15 @@ let
in
{
config.templates.html = {
dom = document:
dom =
document:
let
eval = lib.evalModules {
class = "DOM";
modules = [ document (import ./dom.nix) ];
modules = [
document
(import ./dom.nix)
];
};
in
{
@ -19,27 +29,34 @@ in
value = eval.config;
};
markdown = { name, body }:
markdown =
{ name, body }:
let
commonmark = pkgs.runCommand "${name}.html"
commonmark =
pkgs.runCommand "${name}.html"
{
buildInputs = [ pkgs.cmark ];
} ''
}
''
cmark ${builtins.toFile "${name}.md" body} > $out
'';
in
builtins.readFile commonmark;
nav = { menu, page }:
nav =
{ menu, page }:
let
render-item = item:
if item ? menu then ''
render-item =
item:
if item ? menu then
''
<li><details><summary>${item.menu.label}</summary>
${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>''
;
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>
@ -50,17 +67,27 @@ in
'';
};
config.templates.files = fs: with lib;
config.templates.files =
fs:
with lib;
foldl'
# TODO: create static redirects from `tail <collection>.locations`
(acc: elem: acc // (mapAttrs' (type: value: {
(
acc: elem:
acc
//
(mapAttrs' (
type: value: {
name = head elem.locations + optionalString (type != "") ".${type}";
value = if isStorePath value then value else
builtins.toFile
(elem.name + optionalString (type != "") ".${type}")
(toString value);
}))
elem.outputs)
value =
if isStorePath value then
value
else
builtins.toFile (elem.name + optionalString (type != "") ".${type}") (toString value);
}
))
elem.outputs
)
{ }
fs;
}

View file

@ -1,12 +1,20 @@
{ config, options, lib, ... }:
{
config,
options,
lib,
...
}:
let
inherit (lib) mkOption
inherit (lib)
mkOption
types
;
cfg = config;
in
{
content-types.article = { config, collection, ... }: {
content-types.article =
{ config, collection, ... }:
{
imports = [ cfg.content-types.page ];
options = {
collection = mkOption {
@ -26,26 +34,29 @@ in
};
};
config.name = with lib; mkDefault (slug config.title);
config.outputs.html = lib.mkForce
((cfg.templates.html.page config).override (final: prev: {
config.outputs.html = lib.mkForce (
(cfg.templates.html.page config).override (
final: prev: {
html = {
# TODO: make authors always a list
head.meta.authors = if lib.isList config.author then config.author else [ config.author ];
body.content = with lib; map
(e:
if isAttrs e && e ? section
then
recursiveUpdate e
{
body.content =
with lib;
map (
e:
if isAttrs e && e ? section then
recursiveUpdate e {
section.heading = {
before = [{ p.content = "Published ${config.date}"; }];
after = [{ p.content = "Written by ${config.author}"; }];
before = [ { p.content = "Published ${config.date}"; } ];
after = [ { p.content = "Written by ${config.author}"; } ];
};
}
else e
)
prev.html.body.content;
else
e
) prev.html.body.content;
};
}));
}
)
);
};
}

View file

@ -11,24 +11,34 @@ in
description = ''
Collection of assets, i.e. static files that can be linked to from within documents
'';
type = with types; attrsOf (submodule ({ config, ... }: {
type =
with types;
attrsOf (
submodule (
{ config, ... }:
{
imports = [ cfg.content-types.document ];
options.path = mkOption {
type = types.path;
};
config.outputs."" = if lib.isStorePath config.path then config.path else "${config.path}";
}));
}
)
);
default = { };
};
config.files = with lib;
config.files =
with lib;
let
flatten = attrs: mapAttrsToList
(name: value:
flatten =
attrs:
mapAttrsToList (
name: value:
# HACK: we somehow have to distinguish a module value from regular attributes.
# arbitrary choice: the outputs attribute
if value ? outputs then value else mapAttrsToList value)
attrs;
if value ? outputs then value else mapAttrsToList value
) attrs;
in
cfg.templates.files (flatten cfg.assets);
}

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }:
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
mkOption
@ -25,7 +31,12 @@ in
}
```
'';
type = with types; attrsOf (submodule ({ name, config, ... }: {
type =
with types;
attrsOf (
submodule (
{ name, config, ... }:
{
options = {
type = mkOption {
description = "Type of entries in the collection";
@ -52,7 +63,9 @@ in
};
entry = mkOption {
description = "An entry in the collection";
type = with types; collection (submodule ({
type =
with types;
collection (submodule ({
imports = [ config.type ];
_module.args.collection = config;
process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls;
@ -61,10 +74,19 @@ in
by-name = mkOption {
description = "Entries accessible by symbolic name";
type = with types; attrsOf attrs;
default = with lib; listToAttrs (map (e: { name = e.name; value = e; }) config.entry);
default =
with lib;
listToAttrs (
map (e: {
name = e.name;
value = e;
}) config.entry
);
};
};
}));
}
)
);
};
config.files =

View file

@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }:
{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
mkOption
@ -14,7 +20,15 @@ in
type = with types; attrsOf deferredModule;
};
config.content-types.document = { name, config, options, link, ... }: {
config.content-types.document =
{
name,
config,
options,
link,
...
}:
{
config._module.args.link = config.link;
options = {
name = mkOption {
@ -37,7 +51,10 @@ in
'';
type = with types; nonEmptyListOf str;
apply = config.process-locations;
example = [ "about/overview" "index" ];
example = [
"about/overview"
"index"
];
default = [ config.name ];
};
process-locations = mkOption {
@ -51,24 +68,26 @@ in
# TODO: we may want links to other representations,
# and currently the mapping of output types to output file
# names is soft.
default = with lib; target:
default =
with lib;
target:
let
path = relativePath (head config.locations) (head target.locations);
links = mapAttrs
(type: output:
path + optionalString (type != "") ".${type}"
links = mapAttrs (
type: output: path + optionalString (type != "") ".${type}"
# ^^^^^^^^^^^^
# convention for raw files
)
target.outputs;
) target.outputs;
in
if length (attrValues links) == 0
then throw "no output to link to for '${target.name}'"
else if length (attrValues links) == 1
then links // {
if length (attrValues links) == 0 then
throw "no output to link to for '${target.name}'"
else if length (attrValues links) == 1 then
links
// {
__toString = _: head (attrValues links);
}
else links;
else
links;
};
outputs = mkOption {
description = ''

View file

@ -1,4 +1,9 @@
{ config, options, lib, ... }:
{
config,
options,
lib,
...
}:
let
inherit (lib)
mkOption
@ -7,7 +12,9 @@ let
cfg = config;
in
{
content-types.event = { config, collection, ... }: {
content-types.event =
{ config, collection, ... }:
{
imports = [ cfg.content-types.page ];
options = {
collection = mkOption {
@ -41,41 +48,49 @@ in
};
config.name = with lib; mkDefault (slug config.title);
config.summary = lib.mkDefault config.description;
config.outputs.html = lib.mkForce
((cfg.templates.html.page config).override (final: prev: {
html.body.content = with lib; map
(e:
if isAttrs e && e ? section
then
recursiveUpdate e
{
config.outputs.html = lib.mkForce (
(cfg.templates.html.page config).override (
final: prev: {
html.body.content =
with lib;
map (
e:
if isAttrs e && e ? section then
recursiveUpdate e {
section.content = [
{
dl.content = [
dl.content =
[
{
terms = [{ dt = "Location"; }];
descriptions = [{ dd = config.location; }];
terms = [ { dt = "Location"; } ];
descriptions = [ { dd = config.location; } ];
}
{
terms = [{ dt = "Start"; }];
descriptions = [{
terms = [ { dt = "Start"; } ];
descriptions = [
{
dd = config.start-date + lib.optionalString (!isNull config.start-time) " ${config.start-time}";
}];
}
] ++ lib.optional (!isNull config.end-date) {
terms = [{ dt = "End"; }];
descriptions = [{
dd = config.end-date + lib.optionalString (!isNull config.end-time) " ${config.end-time}";
}];
};
];
}
]
++ e.section.content;
++ lib.optional (!isNull config.end-date) {
terms = [ { dt = "End"; } ];
descriptions = [
{
dd = config.end-date + lib.optionalString (!isNull config.end-time) " ${config.end-time}";
}
else e
)
prev.html.body.content;
];
};
}
] ++ e.section.content;
}
else
e
) prev.html.body.content;
}));
}
)
);
};
}

View file

@ -1,11 +1,18 @@
{ config, options, lib, ... }:
{
config,
options,
lib,
...
}:
let
inherit (lib)
mkOption
types
;
cfg = config;
subtype = baseModule: types.submodule [
subtype =
baseModule:
types.submodule [
baseModule
{
_module.freeformType = types.attrs;
@ -23,7 +30,9 @@ in
type = with types; attrsOf (submodule config.content-types.navigation);
};
config.content-types.named-link = { ... }: {
config.content-types.named-link =
{ ... }:
{
options = {
label = mkOption {
description = "Link label";
@ -36,7 +45,9 @@ in
};
};
config.content-types.navigation = { name, config, ... }: {
config.content-types.navigation =
{ name, config, ... }:
{
options = {
name = mkOption {
description = "Symbolic name, used as a human-readable identifier";
@ -50,7 +61,9 @@ in
};
items = mkOption {
description = "List of menu items";
type = with types; listOf (attrTag {
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; };
@ -63,8 +76,11 @@ 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.
'';
type = with types; attrsOf (functionTo str);
default.html = page: cfg.templates.html.nav {
menu = config; inherit page;
default.html =
page:
cfg.templates.html.nav {
menu = config;
inherit page;
};
};
};

View file

@ -17,7 +17,9 @@ in
config.files = with lib; cfg.templates.files (attrValues config.pages);
config.content-types.page = { name, config, ... }: {
config.content-types.page =
{ name, config, ... }:
{
imports = [ cfg.content-types.document ];
options = {
title = mkOption {

View file

@ -4,14 +4,35 @@ let
inherit (import ./. { }) lib;
in
{
test-relativePath = with lib;
test-relativePath =
with lib;
let
testData = [
{ from = "bar"; to = "baz"; expected = "./baz"; }
{ from = "foo/bar"; to = "foo/baz"; expected = "./baz"; }
{ from = "foo"; to = "bar/baz"; expected = "./bar/baz"; }
{ from = "foo/bar"; to = "baz"; expected = "./../baz"; }
{ from = "foo/bar/baz"; to = "foo"; expected = "./../../foo"; }
{
from = "bar";
to = "baz";
expected = "./baz";
}
{
from = "foo/bar";
to = "foo/baz";
expected = "./baz";
}
{
from = "foo";
to = "bar/baz";
expected = "./bar/baz";
}
{
from = "foo/bar";
to = "baz";
expected = "./../baz";
}
{
from = "foo/bar/baz";
to = "foo";
expected = "./../../foo";
}
];
in
{