forked from Fediversity/Fediversity
website: format
This commit is contained in:
parent
92563d387a
commit
10f3d15a98
24 changed files with 1679 additions and 1258 deletions
|
@ -9,7 +9,9 @@ in
|
||||||
collections.news.type = cfg.content-types.article;
|
collections.news.type = cfg.content-types.article;
|
||||||
collections.events.type = cfg.content-types.event;
|
collections.events.type = cfg.content-types.event;
|
||||||
|
|
||||||
pages.index = { config, link, ... }: {
|
pages.index =
|
||||||
|
{ config, link, ... }:
|
||||||
|
{
|
||||||
title = "Welcome to the Fediversity project";
|
title = "Welcome to the Fediversity project";
|
||||||
description = "Fediversity web site";
|
description = "Fediversity web site";
|
||||||
summary = ''
|
summary = ''
|
||||||
|
@ -20,7 +22,8 @@ in
|
||||||
|
|
||||||
[Learn more about Fediversity](${link pages.fediversity})
|
[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 = {
|
html = {
|
||||||
head.title.text = "Fediversity";
|
head.title.text = "Fediversity";
|
||||||
head.link.stylesheets = prev.html.head.link.stylesheets ++ [
|
head.link.stylesheets = prev.html.head.link.stylesheets ++ [
|
||||||
|
@ -28,7 +31,13 @@ in
|
||||||
];
|
];
|
||||||
body.content =
|
body.content =
|
||||||
let
|
let
|
||||||
to-section = { heading, body, attrs ? { } }: {
|
to-section =
|
||||||
|
{
|
||||||
|
heading,
|
||||||
|
body,
|
||||||
|
attrs ? { },
|
||||||
|
}:
|
||||||
|
{
|
||||||
section = {
|
section = {
|
||||||
heading.content = heading;
|
heading.content = heading;
|
||||||
inherit attrs;
|
inherit attrs;
|
||||||
|
@ -47,11 +56,11 @@ in
|
||||||
section = {
|
section = {
|
||||||
attrs = { };
|
attrs = { };
|
||||||
heading.content = config.title;
|
heading.content = config.title;
|
||||||
content = [
|
content =
|
||||||
|
[
|
||||||
(cfg.templates.html.markdown { inherit (config) name body; })
|
(cfg.templates.html.markdown { inherit (config) name body; })
|
||||||
]
|
]
|
||||||
++
|
++ (map to-section [
|
||||||
(map to-section [
|
|
||||||
{
|
{
|
||||||
heading = "Fediversity grants";
|
heading = "Fediversity grants";
|
||||||
body = ''
|
body = ''
|
||||||
|
@ -65,68 +74,95 @@ in
|
||||||
body = ''
|
body = ''
|
||||||
The Consortium behind the Fediversity project is a cooperation between NLnet, Open Internet Discourse Foundation, NORDUnet and Tweag.
|
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.title}
|
||||||
|
|
||||||
${partner.summary}
|
${partner.summary}
|
||||||
|
|
||||||
[Read more about ${partner.title}](${link partner})
|
[Read more about ${partner.title}](${link partner})
|
||||||
'') (with pages; [ nlnet oid tweag nordunet ]))}
|
'')
|
||||||
|
(
|
||||||
|
with pages;
|
||||||
|
[
|
||||||
|
nlnet
|
||||||
|
oid
|
||||||
|
tweag
|
||||||
|
nordunet
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)}
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
heading = "Fediverse explained";
|
heading = "Fediverse explained";
|
||||||
body = ''
|
body = ''
|
||||||
${toString (map (role: ''
|
${toString (
|
||||||
|
map
|
||||||
|
(role: ''
|
||||||
### ${role.title}
|
### ${role.title}
|
||||||
|
|
||||||
${role.summary}
|
${role.summary}
|
||||||
|
|
||||||
[Read more about ${role.title}](${link role})
|
[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";
|
heading = "News";
|
||||||
attrs = { class = [ "collection" ]; };
|
attrs = {
|
||||||
|
class = [ "collection" ];
|
||||||
|
};
|
||||||
body =
|
body =
|
||||||
let
|
let
|
||||||
sorted = with lib; reverseList (sortOn (entry: entry.date) cfg.collections.news.entry);
|
sorted = with lib; reverseList (sortOn (entry: entry.date) cfg.collections.news.entry);
|
||||||
in
|
in
|
||||||
lib.join "\n" (map
|
lib.join "\n" (
|
||||||
(article: ''
|
map (article: ''
|
||||||
- ${article.date} [${article.title}](${link article})
|
- ${article.date} [${article.title}](${link article})
|
||||||
'')
|
'') sorted
|
||||||
sorted);
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
heading = "Events";
|
heading = "Events";
|
||||||
attrs = { class = [ "collection" ]; };
|
attrs = {
|
||||||
|
class = [ "collection" ];
|
||||||
|
};
|
||||||
body =
|
body =
|
||||||
let
|
let
|
||||||
sorted = with lib; reverseList (sortOn (entry: entry.start-date) cfg.collections.events.entry);
|
sorted = with lib; reverseList (sortOn (entry: entry.start-date) cfg.collections.events.entry);
|
||||||
in
|
in
|
||||||
lib.join "\n" (map
|
lib.join "\n" (
|
||||||
(article: ''
|
map (article: ''
|
||||||
- ${article.start-date} [${article.title}](${link article})
|
- ${article.start-date} [${article.title}](${link article})
|
||||||
'')
|
'') sorted
|
||||||
sorted);
|
);
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
assets."index.css".path = with lib; builtins.toFile
|
assets."index.css".path =
|
||||||
"index.css"
|
with lib;
|
||||||
''
|
builtins.toFile "index.css" ''
|
||||||
section h1, section h2, section h3
|
section h1, section h2, section h3
|
||||||
{
|
{
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
pages.events = { link, ... }: rec {
|
pages.events =
|
||||||
|
{ link, ... }:
|
||||||
|
rec {
|
||||||
title = "Events";
|
title = "Events";
|
||||||
description = "Events related to the Fediverse and NixOS";
|
description = "Events related to the Fediverse and NixOS";
|
||||||
summary = description;
|
summary = description;
|
||||||
body =
|
body =
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
events = map
|
events = map (
|
||||||
(event: with lib; ''
|
event: with lib; ''
|
||||||
## [${event.title}](${link event})
|
## [${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}
|
${event.start-date} ${
|
||||||
'')
|
optionalString (!isNull event.end-date && event.end-date != event.start-date) "to ${event.end-date}"
|
||||||
config.collections.events.entry;
|
} in ${event.location}
|
||||||
|
''
|
||||||
|
) config.collections.events.entry;
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
${join "\n" events}
|
${join "\n" events}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
collections.events.entry = { link, ... }: {
|
collections.events.entry =
|
||||||
|
{ link, ... }:
|
||||||
|
{
|
||||||
title = "NixOS 24.11 ZHF hackathon";
|
title = "NixOS 24.11 ZHF hackathon";
|
||||||
name = "zhf-24-11";
|
name = "zhf-24-11";
|
||||||
description = "NixOS 24.11 ZHF hackathon in Zürich";
|
description = "NixOS 24.11 ZHF hackathon in Zürich";
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
collections.events.entry = { ... }: {
|
collections.events.entry =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
title = "OW2con 2024";
|
title = "OW2con 2024";
|
||||||
description = "OW2con is the annual European open source conference in Paris";
|
description = "OW2con is the annual European open source conference in Paris";
|
||||||
start-date = "2024-06-11";
|
start-date = "2024-06-11";
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
collections.events.entry = { ... }: {
|
collections.events.entry =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
title = "PublicSpaces Conference 2024";
|
title = "PublicSpaces Conference 2024";
|
||||||
description = "A conference by PublicSpaces, Taking Back the Internet.";
|
description = "A conference by PublicSpaces, Taking Back the Internet.";
|
||||||
start-date = "2024-06-06";
|
start-date = "2024-06-06";
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
collections.events.entry = { ... }: {
|
collections.events.entry =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
title = "State of the Internet 2024";
|
title = "State of the Internet 2024";
|
||||||
description = "The State of the Internet 2024 by Waag";
|
description = "The State of the Internet 2024 by Waag";
|
||||||
start-date = "2024-05-16";
|
start-date = "2024-05-16";
|
||||||
|
|
|
@ -6,16 +6,33 @@ in
|
||||||
menus.main = {
|
menus.main = {
|
||||||
label = "Main";
|
label = "Main";
|
||||||
items = [
|
items = [
|
||||||
{ page = pages.index // { title = "Start"; }; }
|
{
|
||||||
|
page = pages.index // {
|
||||||
|
title = "Start";
|
||||||
|
};
|
||||||
|
}
|
||||||
{
|
{
|
||||||
menu.label = "For you";
|
menu.label = "For you";
|
||||||
menu.items = map (page: { inherit page; })
|
menu.items = map (page: { inherit page; }) (
|
||||||
(with pages; [ individuals developers european-commission ]);
|
with pages;
|
||||||
|
[
|
||||||
|
individuals
|
||||||
|
developers
|
||||||
|
european-commission
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
menu.label = "Consortium";
|
menu.label = "Consortium";
|
||||||
menu.items = map (page: { inherit page; })
|
menu.items = map (page: { inherit page; }) (
|
||||||
(with pages; [ nlnet oid tweag nordunet ]);
|
with pages;
|
||||||
|
[
|
||||||
|
nlnet
|
||||||
|
oid
|
||||||
|
tweag
|
||||||
|
nordunet
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
{ page = pages.fediversity; }
|
{ page = pages.fediversity; }
|
||||||
{ page = pages.grants; }
|
{ page = pages.grants; }
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
pages.news = { link, ... }: rec {
|
pages.news =
|
||||||
|
{ link, ... }:
|
||||||
|
rec {
|
||||||
title = "News";
|
title = "News";
|
||||||
description = "News about Fediversity";
|
description = "News about Fediversity";
|
||||||
summary = description;
|
summary = description;
|
||||||
body =
|
body =
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
news = map
|
news = map (article: ''
|
||||||
(article: ''
|
|
||||||
## [${article.title}](${link article})
|
## [${article.title}](${link article})
|
||||||
|
|
||||||
${article.date} by ${article.author}
|
${article.date} by ${article.author}
|
||||||
|
|
||||||
${article.summary}
|
${article.summary}
|
||||||
'')
|
'') config.collections.news.entry;
|
||||||
config.collections.news.entry;
|
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
${join "\n\n" news}
|
${join "\n\n" news}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
collections.news.entry = { link, ... }: rec {
|
collections.news.entry =
|
||||||
|
{ link, ... }:
|
||||||
|
rec {
|
||||||
name = "zhf-24-11";
|
name = "zhf-24-11";
|
||||||
title = "NixOS 24.11 release hackathon and workshop";
|
title = "NixOS 24.11 release hackathon and workshop";
|
||||||
description = "Fediversity engineers met in Zürich at a NixOS 24.11 ZHF hackathon";
|
description = "Fediversity engineers met in Zürich at a NixOS 24.11 ZHF hackathon";
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
{
|
{
|
||||||
collections.news.entry = { link, ... }: {
|
collections.news.entry =
|
||||||
|
{ link, ... }:
|
||||||
|
{
|
||||||
title = "Fediversity project publicly announced";
|
title = "Fediversity project publicly announced";
|
||||||
description = "The Fediversity project has officially been announced";
|
description = "The Fediversity project has officially been announced";
|
||||||
date = "2024-01-01";
|
date = "2024-01-01";
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
{ sources ? import ../npins
|
{
|
||||||
, system ? builtins.currentSystem
|
sources ? import ../npins,
|
||||||
, pkgs ? import sources.nixpkgs {
|
system ? builtins.currentSystem,
|
||||||
|
pkgs ? import sources.nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config = { };
|
config = { };
|
||||||
overlays = [ ];
|
overlays = [ ];
|
||||||
}
|
},
|
||||||
, lib ? import "${sources.nixpkgs}/lib"
|
lib ? import "${sources.nixpkgs}/lib",
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
lib' = final: prev:
|
lib' =
|
||||||
|
final: prev:
|
||||||
let
|
let
|
||||||
new = import ./lib.nix { lib = final; };
|
new = import ./lib.nix { lib = final; };
|
||||||
in
|
in
|
||||||
|
@ -37,13 +39,19 @@ rec {
|
||||||
let
|
let
|
||||||
run-tests = pkgs.writeShellApplication {
|
run-tests = pkgs.writeShellApplication {
|
||||||
name = "run-tests";
|
name = "run-tests";
|
||||||
text = with pkgs; with lib; ''
|
text =
|
||||||
|
with pkgs;
|
||||||
|
with lib;
|
||||||
|
''
|
||||||
${getExe nix-unit} ${toString ./tests.nix} "$@"
|
${getExe nix-unit} ${toString ./tests.nix} "$@"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
test-loop = pkgs.writeShellApplication {
|
test-loop = pkgs.writeShellApplication {
|
||||||
name = "test-loop";
|
name = "test-loop";
|
||||||
text = with pkgs; with lib; ''
|
text =
|
||||||
|
with pkgs;
|
||||||
|
with lib;
|
||||||
|
''
|
||||||
${getExe watchexec} -w ${toString ./.} -- ${getExe nix-unit} ${toString ./tests.nix}
|
${getExe watchexec} -w ${toString ./.} -- ${getExe nix-unit} ${toString ./tests.nix}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -62,7 +70,9 @@ rec {
|
||||||
};
|
};
|
||||||
|
|
||||||
inherit sources pkgs;
|
inherit sources pkgs;
|
||||||
tests = with pkgs; with lib;
|
tests =
|
||||||
|
with pkgs;
|
||||||
|
with lib;
|
||||||
let
|
let
|
||||||
source = fileset.toSource {
|
source = fileset.toSource {
|
||||||
root = ../.;
|
root = ../.;
|
||||||
|
|
165
website/lib.nix
165
website/lib.nix
|
@ -1,21 +1,25 @@
|
||||||
{ lib }:
|
{ lib }:
|
||||||
rec {
|
rec {
|
||||||
template = g: f: x:
|
template =
|
||||||
|
g: f: x:
|
||||||
let
|
let
|
||||||
base = f x;
|
base = f x;
|
||||||
result = g base;
|
result = g base;
|
||||||
in
|
in
|
||||||
result // {
|
result
|
||||||
override = new:
|
// {
|
||||||
|
override =
|
||||||
|
new:
|
||||||
let
|
let
|
||||||
base' =
|
base' =
|
||||||
if lib.isFunction new
|
if lib.isFunction new then
|
||||||
then lib.recursiveUpdate base (new base' base)
|
lib.recursiveUpdate base (new base' base)
|
||||||
else
|
else
|
||||||
lib.recursiveUpdate base new;
|
lib.recursiveUpdate base new;
|
||||||
result' = g base';
|
result' = g base';
|
||||||
in
|
in
|
||||||
result' // {
|
result'
|
||||||
|
// {
|
||||||
override = new: (template g (x': base') x).override new;
|
override = new: (template g (x': base') x).override new;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -28,7 +32,8 @@ rec {
|
||||||
replaceStringRec "--" "-" "hello-----world"
|
replaceStringRec "--" "-" "hello-----world"
|
||||||
=> "hello-world"
|
=> "hello-world"
|
||||||
*/
|
*/
|
||||||
replaceStringsRec = from: to: string:
|
replaceStringsRec =
|
||||||
|
from: to: string:
|
||||||
let
|
let
|
||||||
replaced = lib.replaceStrings [ from ] [ to ] string;
|
replaced = lib.replaceStrings [ from ] [ to ] string;
|
||||||
in
|
in
|
||||||
|
@ -37,25 +42,24 @@ rec {
|
||||||
/**
|
/**
|
||||||
Create a URL-safe slug from any string
|
Create a URL-safe slug from any string
|
||||||
*/
|
*/
|
||||||
slug = str:
|
slug =
|
||||||
|
str:
|
||||||
let
|
let
|
||||||
# Replace non-alphanumeric characters with hyphens
|
# Replace non-alphanumeric characters with hyphens
|
||||||
replaced = join ""
|
replaced = join "" (
|
||||||
(
|
builtins.map (c: if (c >= "a" && c <= "z") || (c >= "0" && c <= "9") then c else "-") (
|
||||||
builtins.map
|
with lib; stringToCharacters (toLower str)
|
||||||
(c:
|
|
||||||
if (c >= "a" && c <= "z") || (c >= "0" && c <= "9")
|
|
||||||
then c
|
|
||||||
else "-"
|
|
||||||
)
|
)
|
||||||
(with lib; stringToCharacters (toLower str)));
|
);
|
||||||
|
|
||||||
# Remove leading and trailing hyphens
|
# Remove leading and trailing hyphens
|
||||||
trimHyphens = s:
|
trimHyphens =
|
||||||
|
s:
|
||||||
let
|
let
|
||||||
matched = builtins.match "(-*)([^-].*[^-]|[^-])(-*)" s;
|
matched = builtins.match "(-*)([^-].*[^-]|[^-])(-*)" s;
|
||||||
in
|
in
|
||||||
with lib; optionalString (!isNull matched) (builtins.elemAt matched 1);
|
with lib;
|
||||||
|
optionalString (!isNull matched) (builtins.elemAt matched 1);
|
||||||
in
|
in
|
||||||
trimHyphens (replaceStringsRec "--" "-" replaced);
|
trimHyphens (replaceStringsRec "--" "-" replaced);
|
||||||
|
|
||||||
|
@ -64,9 +68,11 @@ rec {
|
||||||
/**
|
/**
|
||||||
Trim trailing spaces and squash non-leading spaces
|
Trim trailing spaces and squash non-leading spaces
|
||||||
*/
|
*/
|
||||||
trim = string:
|
trim =
|
||||||
|
string:
|
||||||
let
|
let
|
||||||
trimLine = line:
|
trimLine =
|
||||||
|
line:
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
# separate leading spaces from the rest
|
# separate leading spaces from the rest
|
||||||
|
@ -76,8 +82,7 @@ rec {
|
||||||
# drop trailing spaces
|
# drop trailing spaces
|
||||||
body = head (split " *$" rest);
|
body = head (split " *$" rest);
|
||||||
in
|
in
|
||||||
if body == "" then "" else
|
if body == "" then "" else spaces + replaceStringsRec " " " " body;
|
||||||
spaces + replaceStringsRec " " " " body;
|
|
||||||
in
|
in
|
||||||
join "\n" (map trimLine (splitLines string));
|
join "\n" (map trimLine (splitLines string));
|
||||||
|
|
||||||
|
@ -85,35 +90,42 @@ rec {
|
||||||
|
|
||||||
splitLines = s: with builtins; filter (x: !isList x) (split "\n" s);
|
splitLines = s: with builtins; filter (x: !isList x) (split "\n" s);
|
||||||
|
|
||||||
indent = prefix: s:
|
indent =
|
||||||
|
prefix: s:
|
||||||
with lib.lists;
|
with lib.lists;
|
||||||
let
|
let
|
||||||
lines = splitLines s;
|
lines = splitLines s;
|
||||||
in
|
in
|
||||||
join "\n" (
|
join "\n" ([ (head lines) ] ++ (map (x: if x == "" then x else "${prefix}${x}") (tail lines)));
|
||||||
[ (head lines) ]
|
|
||||||
++
|
|
||||||
(map (x: if x == "" then x else "${prefix}${x}") (tail lines))
|
|
||||||
);
|
|
||||||
|
|
||||||
relativePath = path1': path2':
|
relativePath =
|
||||||
|
path1': path2':
|
||||||
let
|
let
|
||||||
inherit (lib.path) subpath;
|
inherit (lib.path) subpath;
|
||||||
inherit (lib) lists length take drop min max;
|
inherit (lib)
|
||||||
|
lists
|
||||||
|
length
|
||||||
|
take
|
||||||
|
drop
|
||||||
|
min
|
||||||
|
max
|
||||||
|
;
|
||||||
|
|
||||||
path1 = subpath.components path1';
|
path1 = subpath.components path1';
|
||||||
prefix1 = take (length path1 - 1) path1;
|
prefix1 = take (length path1 - 1) path1;
|
||||||
path2 = subpath.components path2';
|
path2 = subpath.components path2';
|
||||||
prefix2 = take (length path2 - 1) path2;
|
prefix2 = take (length path2 - 1) path2;
|
||||||
|
|
||||||
commonPrefixLength = with lists;
|
commonPrefixLength =
|
||||||
findFirstIndex (i: i.fst != i.snd)
|
with lists;
|
||||||
(min (length prefix1) (length prefix2))
|
findFirstIndex (i: i.fst != i.snd) (min (length prefix1) (length prefix2)) (
|
||||||
(zipLists prefix1 prefix2);
|
zipLists prefix1 prefix2
|
||||||
|
);
|
||||||
|
|
||||||
depth = max 0 (length prefix1 - commonPrefixLength);
|
depth = max 0 (length prefix1 - commonPrefixLength);
|
||||||
|
|
||||||
relativeComponents = with lists;
|
relativeComponents =
|
||||||
|
with lists;
|
||||||
[ "." ] ++ (replicate depth "..") ++ (drop commonPrefixLength path2);
|
[ "." ] ++ (replicate depth "..") ++ (drop commonPrefixLength path2);
|
||||||
in
|
in
|
||||||
join "/" relativeComponents;
|
join "/" relativeComponents;
|
||||||
|
@ -122,47 +134,51 @@ rec {
|
||||||
Recursively list all Nix files from a directory, except the top-level `default.nix`
|
Recursively list all Nix files from a directory, except the top-level `default.nix`
|
||||||
|
|
||||||
Useful for module system `imports` from a top-level module.
|
Useful for module system `imports` from a top-level module.
|
||||||
**/
|
*
|
||||||
nixFiles = dir: with lib.fileset;
|
*/
|
||||||
toList (difference
|
nixFiles =
|
||||||
(fileFilter ({ hasExt, ... }: hasExt "nix") dir)
|
dir:
|
||||||
(dir + "/default.nix")
|
with lib.fileset;
|
||||||
);
|
toList (difference (fileFilter ({ hasExt, ... }: hasExt "nix") dir) (dir + "/default.nix"));
|
||||||
|
|
||||||
types = rec {
|
types = rec {
|
||||||
# arbitrarily nested attribute set where the leaves are of type `type`
|
# arbitrarily nested attribute set where the leaves are of type `type`
|
||||||
# NOTE: this works for anything but attribute sets!
|
# 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,
|
# 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`
|
||||||
attrsOf (either type (recursiveAttrs type));
|
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 of unnamed items that can be added to item-wise, i.e. without wrapping the item in a list
|
||||||
collection = elemType:
|
collection =
|
||||||
|
elemType:
|
||||||
let
|
let
|
||||||
unparenthesize = class: class == "noun";
|
unparenthesize = class: class == "noun";
|
||||||
desc = type:
|
desc = type: types.optionDescriptionPhrase unparenthesize type;
|
||||||
types.optionDescriptionPhrase unparenthesize type;
|
desc' =
|
||||||
desc' = type:
|
type:
|
||||||
let
|
let
|
||||||
typeDesc = lib.types.optionDescriptionPhrase unparenthesize type;
|
typeDesc = lib.types.optionDescriptionPhrase unparenthesize type;
|
||||||
in
|
in
|
||||||
if type.descriptionClass == "noun"
|
if type.descriptionClass == "noun" then typeDesc + "s" else "many instances of ${typeDesc}";
|
||||||
then
|
|
||||||
typeDesc + "s"
|
|
||||||
else
|
|
||||||
"many instances of ${typeDesc}";
|
|
||||||
in
|
in
|
||||||
lib.types.mkOptionType {
|
lib.types.mkOptionType {
|
||||||
name = "collection";
|
name = "collection";
|
||||||
description = "separately specified ${desc elemType} for a collection of ${desc' elemType}";
|
description = "separately specified ${desc elemType} for a collection of ${desc' elemType}";
|
||||||
merge = loc: defs:
|
merge =
|
||||||
map
|
loc: defs:
|
||||||
(def:
|
map (
|
||||||
elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [{ inherit (def) file; value = def.value; }]
|
def:
|
||||||
)
|
elemType.merge (loc ++ [ "[definition ${toString def.file}]" ]) [
|
||||||
defs;
|
{
|
||||||
|
inherit (def) file;
|
||||||
|
value = def.value;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
) defs;
|
||||||
check = elemType.check;
|
check = elemType.check;
|
||||||
getSubOptions = elemType.getSubOptions;
|
getSubOptions = elemType.getSubOptions;
|
||||||
getSubModules = elemType.getSubModules;
|
getSubModules = elemType.getSubModules;
|
||||||
|
@ -175,29 +191,34 @@ rec {
|
||||||
nestedTypes.elemType = elemType;
|
nestedTypes.elemType = elemType;
|
||||||
};
|
};
|
||||||
|
|
||||||
listOfUnique = elemType:
|
listOfUnique =
|
||||||
|
elemType:
|
||||||
let
|
let
|
||||||
baseType = lib.types.listOf elemType;
|
baseType = lib.types.listOf elemType;
|
||||||
in
|
in
|
||||||
baseType // {
|
baseType
|
||||||
merge = loc: defs:
|
// {
|
||||||
|
merge =
|
||||||
|
loc: defs:
|
||||||
let
|
let
|
||||||
# Keep track of which definition each value came from
|
# Keep track of which definition each value came from
|
||||||
defsWithValues = map
|
defsWithValues = map (
|
||||||
(def:
|
def:
|
||||||
map (v: { inherit (def) file; value = v; }) def.value
|
map (v: {
|
||||||
)
|
inherit (def) file;
|
||||||
defs;
|
value = v;
|
||||||
|
}) def.value
|
||||||
|
) defs;
|
||||||
flatDefs = lib.flatten defsWithValues;
|
flatDefs = lib.flatten defsWithValues;
|
||||||
|
|
||||||
# Check for duplicates while preserving source info
|
# Check for duplicates while preserving source info
|
||||||
seen = builtins.foldl'
|
seen = builtins.foldl' (
|
||||||
(acc: def:
|
acc: def:
|
||||||
if lib.lists.any (v: v.value == def.value) acc
|
if lib.lists.any (v: v.value == def.value) acc then
|
||||||
then throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}"
|
throw "The option `${lib.options.showOption loc}` has duplicate values (${toString def.value}) defined in ${def.file}"
|
||||||
else acc ++ [ def ]
|
else
|
||||||
) [ ]
|
acc ++ [ def ]
|
||||||
flatDefs;
|
) [ ] flatDefs;
|
||||||
in
|
in
|
||||||
map (def: def.value) seen;
|
map (def: def.value) seen;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
{ config, options, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
|
@ -8,8 +14,7 @@ in
|
||||||
{
|
{
|
||||||
imports = lib.nixFiles ./.;
|
imports = lib.nixFiles ./.;
|
||||||
|
|
||||||
options.templates =
|
options.templates = mkOption {
|
||||||
mkOption {
|
|
||||||
description = ''
|
description = ''
|
||||||
Collection of named helper functions for conversion different structured representations which can be rendered to a string
|
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;
|
type = types.package;
|
||||||
default =
|
default =
|
||||||
let
|
let
|
||||||
script = ''
|
script =
|
||||||
|
''
|
||||||
mkdir $out
|
mkdir $out
|
||||||
'' + lib.join "\n" copy;
|
''
|
||||||
copy = lib.mapAttrsToList
|
+ lib.join "\n" copy;
|
||||||
(
|
copy = lib.mapAttrsToList (path: file: ''
|
||||||
path: file: ''
|
|
||||||
mkdir -p $out/$(dirname ${path})
|
mkdir -p $out/$(dirname ${path})
|
||||||
cp -r ${file} $out/${path}
|
cp -r ${file} $out/${path}
|
||||||
''
|
'') config.files;
|
||||||
)
|
|
||||||
config.files;
|
|
||||||
in
|
in
|
||||||
pkgs.runCommand "source" { } script;
|
pkgs.runCommand "source" { } script;
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: this is an artefact of exploration; needs to be adapted to actual use
|
# 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
|
let
|
||||||
outline = { ... }: {
|
outline =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
options = {
|
options = {
|
||||||
value = mkOption {
|
value = mkOption {
|
||||||
# null denotes root
|
# null denotes root
|
||||||
type = with types; nullOr (either str (listOf (attrTag categories.phrasing)));
|
type = with types; nullOr (either str (listOf (attrTag categories.phrasing)));
|
||||||
subsections = mkOption {
|
subsections = mkOption {
|
||||||
type = with types; listOf (submodule outline);
|
type = with types; listOf (submodule outline);
|
||||||
default = with lib; map
|
default =
|
||||||
|
with lib;
|
||||||
|
map
|
||||||
# TODO: go into depth manually here,
|
# TODO: go into depth manually here,
|
||||||
# we don't want to pollute the DOM implementation
|
# we don't want to pollute the DOM implementation
|
||||||
(c: (lib.head (attrValues c)).outline)
|
(c: (lib.head (attrValues c)).outline)
|
||||||
|
@ -67,10 +75,11 @@ in
|
||||||
__toString = mkOption {
|
__toString = mkOption {
|
||||||
type = with types; functionTo str;
|
type = with types; functionTo str;
|
||||||
# TODO: convert to HTML
|
# TODO: convert to HTML
|
||||||
default = self: lib.squash ''
|
default =
|
||||||
|
self:
|
||||||
|
lib.squash ''
|
||||||
${if isNull self.value then "root" else self.value}
|
${if isNull self.value then "root" else self.value}
|
||||||
${if self.subsections != [] then
|
${if self.subsections != [ ] then " " + lib.indent " " (lib.join "\n" self.subsections) else ""}
|
||||||
" " + lib.indent " " (lib.join "\n" self.subsections) else ""}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -81,9 +90,11 @@ in
|
||||||
type = types.submodule outline;
|
type = types.submodule outline;
|
||||||
default = {
|
default = {
|
||||||
value = null;
|
value = null;
|
||||||
subsections = with lib;
|
subsections =
|
||||||
map (c: (lib.head (attrValues c)).outline)
|
with lib;
|
||||||
(filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content);
|
map (c: (lib.head (attrValues c)).outline) (
|
||||||
|
filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,7 +28,9 @@ let
|
||||||
];
|
];
|
||||||
|
|
||||||
# base type for all DOM elements
|
# base type for all DOM elements
|
||||||
element = { ... }: {
|
element =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
# TODO: add fields for upstream documentation references
|
# TODO: add fields for upstream documentation references
|
||||||
# TODO: programmatically generate documentation
|
# TODO: programmatically generate documentation
|
||||||
options = with lib; {
|
options = with lib; {
|
||||||
|
@ -43,20 +45,28 @@ 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 = submodule value; }) elements;
|
||||||
(name: value: mkOption { type = submodule value; })
|
|
||||||
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
|
||||||
# elements that belong to these categories
|
# elements that belong to these categories
|
||||||
categories = with lib;
|
categories =
|
||||||
genAttrs
|
with lib;
|
||||||
content-categories
|
genAttrs content-categories (
|
||||||
(category:
|
category:
|
||||||
(mapAttrs (_: e: mkOption { type = 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
|
||||||
# TODO: we may want to do this properly and loop `categories` through the top-level `config`
|
# 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) {
|
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
|
# https://html.spec.whatwg.org/multipage/document-sequences.html#valid-navigable-target-name-or-keyword
|
||||||
type =
|
type =
|
||||||
let
|
let
|
||||||
is-valid-target = s:
|
is-valid-target =
|
||||||
|
s:
|
||||||
let
|
let
|
||||||
inherit (lib) match;
|
inherit (lib) match;
|
||||||
has-lt = s: match ".*<.*" s != null;
|
has-lt = s: match ".*<.*" s != null;
|
||||||
|
@ -140,14 +151,19 @@ let
|
||||||
in
|
in
|
||||||
has-valid-start s && !(has-lt s && has-tab-or-newline s);
|
has-valid-start s && !(has-lt s && has-tab-or-newline s);
|
||||||
in
|
in
|
||||||
with types; either
|
with types;
|
||||||
(enum [ "_blank" "_self" "_parent" "_top" ])
|
either (enum [
|
||||||
(types.addCheck str is-valid-target)
|
"_blank"
|
||||||
;
|
"_self"
|
||||||
|
"_parent"
|
||||||
|
"_top"
|
||||||
|
]) (types.addCheck str is-valid-target);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
mkAttrs = attrs: with lib;
|
mkAttrs =
|
||||||
|
attrs:
|
||||||
|
with lib;
|
||||||
mkOption {
|
mkOption {
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = global-attrs // attrs;
|
options = global-attrs // attrs;
|
||||||
|
@ -155,28 +171,33 @@ let
|
||||||
default = { };
|
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
|
# TODO: figure out how let attributes know how to print themselves without polluting the interface
|
||||||
let
|
let
|
||||||
result = trim (join " "
|
result = trim (
|
||||||
(mapAttrsToList
|
join " " (
|
||||||
|
mapAttrsToList
|
||||||
# TODO: this needs to be smarter for boolean attributes
|
# TODO: this needs to be smarter for boolean attributes
|
||||||
# where the value must be written out explicitly.
|
# where the value must be written out explicitly.
|
||||||
# probably the attribute itself should have its own `__toString`.
|
# probably the attribute itself should have its own `__toString`.
|
||||||
(name: value:
|
(
|
||||||
|
name: value:
|
||||||
if isBool value then
|
if isBool value then
|
||||||
if value then name else ""
|
if value then name else ""
|
||||||
# TODO: some attributes must be explicitly empty
|
# TODO: some attributes must be explicitly empty
|
||||||
else optionalString (toString value != "") ''${name}="${toString value}"''
|
else
|
||||||
|
optionalString (toString value != "") ''${name}="${toString value}"''
|
||||||
|
)
|
||||||
|
attrs
|
||||||
)
|
)
|
||||||
attrs)
|
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
if attrs == null then throw "wat" else
|
if attrs == null then throw "wat" else optionalString (stringLength result > 0) " " + result;
|
||||||
optionalString (stringLength result > 0) " " + result
|
|
||||||
;
|
|
||||||
|
|
||||||
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
|
# TODO: be smarter about content to save some space and repetition at the call sites
|
||||||
squash (trim ''
|
squash (trim ''
|
||||||
|
@ -187,16 +208,20 @@ let
|
||||||
|
|
||||||
print-element' = name: attrs: "<${name}${print-attrs attrs}>";
|
print-element' = name: attrs: "<${name}${print-attrs attrs}>";
|
||||||
|
|
||||||
toString-unwrap = e:
|
toString-unwrap =
|
||||||
|
e:
|
||||||
with lib;
|
with lib;
|
||||||
if isAttrs e
|
if isAttrs e then
|
||||||
then toString (head (attrValues e))
|
toString (head (attrValues e))
|
||||||
else if isList e
|
else if isList e then
|
||||||
then toString (map toString-unwrap e)
|
toString (map toString-unwrap e)
|
||||||
else e;
|
else
|
||||||
|
e;
|
||||||
|
|
||||||
elements = rec {
|
elements = rec {
|
||||||
document = { ... }: {
|
document =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
inherit (element-types) html;
|
inherit (element-types) html;
|
||||||
|
@ -210,7 +235,9 @@ let
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
html = { name, ... }: {
|
html =
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
|
@ -218,13 +245,17 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self: print-element name self.attrs ''
|
config.__toString =
|
||||||
|
self:
|
||||||
|
print-element name self.attrs ''
|
||||||
${self.head}
|
${self.head}
|
||||||
${self.body}
|
${self.body}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
head = { name, ... }: {
|
head =
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = with lib; {
|
options = with lib; {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
|
@ -248,19 +279,17 @@ 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 = 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 {
|
||||||
type = with types; either
|
type = with types; either (ints.between 1 10000) (enum [ "device-width" ]);
|
||||||
(ints.between 1 10000)
|
|
||||||
(enum [ "device-width" ]);
|
|
||||||
default = "device-width"; # not default by standard
|
default = "device-width"; # not default by standard
|
||||||
};
|
};
|
||||||
height = mkOption {
|
height = mkOption {
|
||||||
type = with types; either
|
type = with types; either (ints.between 1 10000) (enum [ "device-height" ]);
|
||||||
(ints.between 1 10000)
|
|
||||||
(enum [ "device-height" ]);
|
|
||||||
default = "device-height"; # not default by standard (but seems to work if you don't set it)
|
default = "device-height"; # not default by standard (but seems to work if you don't set it)
|
||||||
};
|
};
|
||||||
initial-scale = mkOption {
|
initial-scale = mkOption {
|
||||||
|
@ -289,7 +318,8 @@ let
|
||||||
default = "resizes-visual";
|
default = "resizes-visual";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -318,15 +348,18 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self:
|
config.__toString =
|
||||||
|
self:
|
||||||
with lib;
|
with lib;
|
||||||
print-element name self.attrs ''
|
print-element name self.attrs ''
|
||||||
${self.title}
|
${self.title}
|
||||||
${with lib; optionalString (!isNull self.base) self.base}
|
${with lib; optionalString (!isNull self.base) self.base}
|
||||||
<meta charset="${self.meta.charset}" />
|
<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
|
TODO: make proper icon and preload types
|
||||||
-->
|
-->
|
||||||
|
@ -336,25 +369,41 @@ let
|
||||||
|
|
||||||
${print-element' "meta" {
|
${print-element' "meta" {
|
||||||
name = "viewport";
|
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
|
${join "\n" (
|
||||||
(author: print-element' "meta" {
|
map (
|
||||||
|
author:
|
||||||
|
print-element' "meta" {
|
||||||
name = "author";
|
name = "author";
|
||||||
content = "${author}";
|
content = "${author}";
|
||||||
})
|
|
||||||
self.meta.authors)
|
|
||||||
}
|
}
|
||||||
|
) self.meta.authors
|
||||||
|
)}
|
||||||
|
|
||||||
${join "\n" (map
|
${join "\n" (
|
||||||
(stylesheet: print-element' "link" ({ rel = "stylesheet"; } // (removeAttrs stylesheet [ "categories" "__toString" ])))
|
map (
|
||||||
self.link.stylesheets)
|
stylesheet:
|
||||||
|
print-element' "link" (
|
||||||
|
{
|
||||||
|
rel = "stylesheet";
|
||||||
}
|
}
|
||||||
|
// (removeAttrs stylesheet [
|
||||||
|
"categories"
|
||||||
|
"__toString"
|
||||||
|
])
|
||||||
|
)
|
||||||
|
) self.link.stylesheets
|
||||||
|
)}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
title = { name, ... }: {
|
title =
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options.attrs = mkAttrs { };
|
options.attrs = mkAttrs { };
|
||||||
options.text = mkOption {
|
options.text = mkOption {
|
||||||
|
@ -365,15 +414,21 @@ let
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
base = { name, ... }: {
|
base =
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
# TODO: "A base element must have either an href attribute, a target attribute, or both."
|
# 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.categories = [ "metadata" ];
|
||||||
config.__toString = self: "<base${print-attrs self}>";
|
config.__toString = self: "<base${print-attrs self}>";
|
||||||
};
|
};
|
||||||
|
|
||||||
link = { name, ... }: {
|
link =
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = global-attrs // {
|
options = global-attrs // {
|
||||||
# TODO: more attributes
|
# TODO: more attributes
|
||||||
|
@ -382,7 +437,9 @@ let
|
||||||
# XXX: there are variants of `rel` for `link`, `a`/`area`, and `form`
|
# XXX: there are variants of `rel` for `link`, `a`/`area`, and `form`
|
||||||
rel = mkOption {
|
rel = mkOption {
|
||||||
# https://html.spec.whatwg.org/multipage/semantics.html#attr-link-rel
|
# 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
|
# TODO: work out link types in detail, there are lots of additional constraints
|
||||||
# https://html.spec.whatwg.org/multipage/links.html#linkTypes
|
# https://html.spec.whatwg.org/multipage/links.html#linkTypes
|
||||||
[
|
[
|
||||||
|
@ -403,8 +460,7 @@ let
|
||||||
"privacy-policy"
|
"privacy-policy"
|
||||||
"search"
|
"search"
|
||||||
"terms-of-service"
|
"terms-of-service"
|
||||||
]
|
]);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
# TODO: figure out how to make body-ok `link` elements
|
# 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>`
|
# <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>`
|
# semantically it's a standalone thing but syntactically happens to be subsumed under `<link>`
|
||||||
stylesheet = { config, name, ... }: {
|
stylesheet =
|
||||||
|
{ config, name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = global-attrs // {
|
options = global-attrs // {
|
||||||
type = mkOption {
|
type = mkOption {
|
||||||
|
@ -440,49 +498,74 @@ let
|
||||||
inherit (link-attrs) href media integrity;
|
inherit (link-attrs) href media integrity;
|
||||||
};
|
};
|
||||||
# https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:body-ok
|
# https://html.spec.whatwg.org/multipage/links.html#link-type-stylesheet:body-ok
|
||||||
config.categories = [ "metadata" "phrasing" ];
|
config.categories = [
|
||||||
config.__toString = self: print-attrs (removeAttrs self [ "categories" "__toString" ]);
|
"metadata"
|
||||||
|
"phrasing"
|
||||||
|
];
|
||||||
|
config.__toString =
|
||||||
|
self:
|
||||||
|
print-attrs (
|
||||||
|
removeAttrs self [
|
||||||
|
"categories"
|
||||||
|
"__toString"
|
||||||
|
]
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
body = { config, name, ... }: {
|
body =
|
||||||
|
{ config, name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
type = with types;
|
type =
|
||||||
|
with types;
|
||||||
let
|
let
|
||||||
# Type check that ensures spec-compliant section hierarchy
|
# Type check that ensures spec-compliant section hierarchy
|
||||||
# https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines-2:concept-heading-7
|
# https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines-2:concept-heading-7
|
||||||
with-section-constraints = baseType: baseType // {
|
with-section-constraints =
|
||||||
merge = loc: defs:
|
baseType:
|
||||||
|
baseType
|
||||||
|
// {
|
||||||
|
merge =
|
||||||
|
loc: defs:
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
find-and-attach = def:
|
find-and-attach =
|
||||||
|
def:
|
||||||
let
|
let
|
||||||
process-with-depth = depth: content:
|
process-with-depth =
|
||||||
map
|
depth: content:
|
||||||
(x:
|
map (
|
||||||
if isAttrs x && x ? section
|
x:
|
||||||
then x // {
|
if isAttrs x && x ? section then
|
||||||
|
x
|
||||||
|
// {
|
||||||
section = x.section // {
|
section = x.section // {
|
||||||
heading-level = depth;
|
heading-level = depth;
|
||||||
content = process-with-depth (depth + 1) (x.section.content or [ ]);
|
content = process-with-depth (depth + 1) (x.section.content or [ ]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else x
|
else
|
||||||
)
|
x
|
||||||
content;
|
) content;
|
||||||
|
|
||||||
find-with-depth = depth: content:
|
find-with-depth =
|
||||||
|
depth: content:
|
||||||
let
|
let
|
||||||
sections = map (v: { inherit (def) file; value = v; depth = depth; })
|
sections = map (v: {
|
||||||
(filter (x: isAttrs x && x ? section) content);
|
inherit (def) file;
|
||||||
subsections = concatMap
|
value = v;
|
||||||
(x:
|
depth = depth;
|
||||||
if isAttrs x && x ? section && x.section ? content
|
}) (filter (x: isAttrs x && x ? section) content);
|
||||||
then find-with-depth (depth + 1) x.section.content
|
subsections = concatMap (
|
||||||
else [ ])
|
x:
|
||||||
content;
|
if isAttrs x && x ? section && x.section ? content then
|
||||||
|
find-with-depth (depth + 1) x.section.content
|
||||||
|
else
|
||||||
|
[ ]
|
||||||
|
) content;
|
||||||
in
|
in
|
||||||
sections ++ subsections;
|
sections ++ subsections;
|
||||||
|
|
||||||
|
@ -500,9 +583,12 @@ let
|
||||||
if too-deep != [ ] then
|
if too-deep != [ ] then
|
||||||
throw ''
|
throw ''
|
||||||
The option `${lib.options.showOption loc}` has sections nested too deeply:
|
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.''
|
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
|
in
|
||||||
with-section-constraints
|
with-section-constraints
|
||||||
|
@ -513,16 +599,22 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self: with lib;
|
config.__toString =
|
||||||
print-element name self.attrs (join "\n" (map toString-unwrap self.content));
|
self: with lib; print-element name self.attrs (join "\n" (map toString-unwrap 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;
|
||||||
};
|
};
|
||||||
heading = mkOption {
|
heading = mkOption {
|
||||||
|
@ -534,13 +626,21 @@ 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 ({ config, ... }: {
|
type =
|
||||||
|
with types;
|
||||||
|
submodule (
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
# setting to an attribute set will wrap the section in `<hgroup>`
|
# setting to an attribute set will wrap the section in `<hgroup>`
|
||||||
hgroup.attrs = mkOption {
|
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 { };
|
default = with lib; if (config.before == [ ] && config.after == [ ]) then null else { };
|
||||||
};
|
};
|
||||||
# https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element
|
# 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));
|
type = with types; either str (listOf (attrTag categories.phrasing));
|
||||||
};
|
};
|
||||||
after = mkOption {
|
after = mkOption {
|
||||||
type = with types;
|
type = with types; listOf (attrTag ({ inherit (element-types) p; } // categories.scripting));
|
||||||
listOf (attrTag ({ inherit (element-types) p; } // categories.scripting));
|
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
);
|
||||||
};
|
};
|
||||||
# https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines
|
# https://html.spec.whatwg.org/multipage/sections.html#headings-and-outlines
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
|
@ -573,28 +673,35 @@ let
|
||||||
internal = true;
|
internal = true;
|
||||||
};
|
};
|
||||||
config = {
|
config = {
|
||||||
categories = [ "flow" "sectioning" "palpable" ];
|
categories = [
|
||||||
__toString = self: with lib;
|
"flow"
|
||||||
|
"sectioning"
|
||||||
|
"palpable"
|
||||||
|
];
|
||||||
|
__toString =
|
||||||
|
self:
|
||||||
|
with lib;
|
||||||
let
|
let
|
||||||
n = toString config.heading-level;
|
n = toString config.heading-level;
|
||||||
heading = ''<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}>'';
|
||||||
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)}
|
${optionalString (!isNull self.heading.before) (toString-unwrap self.heading.before)}
|
||||||
${heading}
|
${heading}
|
||||||
${optionalString (!isNull self.heading.after) (toString-unwrap self.heading.after)}
|
${optionalString (!isNull self.heading.after) (toString-unwrap self.heading.after)}
|
||||||
'');
|
'');
|
||||||
content =
|
content =
|
||||||
(if isNull self.heading.hgroup.attrs then heading else hgroup)
|
(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
|
in
|
||||||
if !isNull self.attrs
|
if !isNull self.attrs then print-element name self.attrs content else content;
|
||||||
then print-element name self.attrs content
|
|
||||||
else content;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
p = { name, ... }: {
|
p =
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
|
@ -602,20 +709,34 @@ let
|
||||||
type = with types; either str (listOf (attrTag categories.phrasing));
|
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);
|
config.__toString = self: print-element name self.attrs (toString self.content);
|
||||||
};
|
};
|
||||||
|
|
||||||
dl = { config, name, ... }: {
|
dl =
|
||||||
|
{ config, name, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
type = with types; listOf (submodule ({ ... }: {
|
type =
|
||||||
|
with types;
|
||||||
|
listOf (
|
||||||
|
submodule (
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
options = {
|
options = {
|
||||||
# TODO: wrap in `<div>` if set
|
# TODO: wrap in `<div>` if set
|
||||||
div.attrs = mkOption {
|
div.attrs = mkOption {
|
||||||
type = with types; nullOr (submodule { options = global-attrs; });
|
type =
|
||||||
|
with types;
|
||||||
|
nullOr (submodule {
|
||||||
|
options = global-attrs;
|
||||||
|
});
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
before = mkOption {
|
before = mkOption {
|
||||||
|
@ -637,7 +758,9 @@ let
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
# XXX: here we can't express the spec requirement that `dl` is palpable if the list of term-description-pairs is nonempty.
|
# 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.
|
# 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.
|
# 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.categories = [ "flow" ];
|
||||||
config.__toString = self:
|
config.__toString =
|
||||||
|
self:
|
||||||
with lib;
|
with lib;
|
||||||
let
|
let
|
||||||
content = map
|
content = map (
|
||||||
(entry:
|
entry:
|
||||||
let
|
let
|
||||||
list = squash ''
|
list = squash ''
|
||||||
${join "\n" entry.before}
|
${join "\n" entry.before}
|
||||||
|
@ -662,36 +786,53 @@ let
|
||||||
${join "\n" entry.after}
|
${join "\n" entry.after}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
if !isNull entry.div.attrs
|
if !isNull entry.div.attrs then print-element "div" entry.div.attrs list else list
|
||||||
then print-element "div" entry.div.attrs list
|
) self.content;
|
||||||
else list
|
|
||||||
)
|
|
||||||
self.content;
|
|
||||||
in
|
in
|
||||||
print-element name self.attrs (join "\n" content);
|
print-element name self.attrs (join "\n" content);
|
||||||
};
|
};
|
||||||
|
|
||||||
dt = { config, ... }: {
|
dt =
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
dt = mkOption {
|
dt = mkOption {
|
||||||
type = with types; either str (submodule (attrTag (
|
type =
|
||||||
|
with types;
|
||||||
|
either str (
|
||||||
|
submodule (
|
||||||
|
attrTag (
|
||||||
# TODO: test
|
# TODO: test
|
||||||
with lib; removeAttrs
|
with lib;
|
||||||
(filterAttrs
|
removeAttrs
|
||||||
(name: value: ! any (c: elem c [ "sectioning" "heading" ]) value.categories)
|
(filterAttrs (
|
||||||
categories.flow
|
name: value:
|
||||||
|
!any (
|
||||||
|
c:
|
||||||
|
elem c [
|
||||||
|
"sectioning"
|
||||||
|
"heading"
|
||||||
|
]
|
||||||
|
) value.categories
|
||||||
|
) categories.flow)
|
||||||
|
[
|
||||||
|
"header"
|
||||||
|
"footer"
|
||||||
|
]
|
||||||
)
|
)
|
||||||
[ "header" "footer" ]
|
)
|
||||||
)));
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self: print-element "dt" self.attrs self.dt;
|
config.__toString = self: print-element "dt" self.attrs self.dt;
|
||||||
};
|
};
|
||||||
|
|
||||||
dd = { config, ... }: {
|
dd =
|
||||||
|
{ config, ... }:
|
||||||
|
{
|
||||||
imports = [ element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
{ config, lib, pkgs, ... }: {
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
config.assets."style.css".path = ./style.css;
|
config.assets."style.css".path = ./style.css;
|
||||||
config.assets."ngi-fediversity.svg".path = ./ngi-fediversity.svg;
|
config.assets."ngi-fediversity.svg".path = ./ngi-fediversity.svg;
|
||||||
# TODO: auto-generate a bunch from SVG
|
# TODO: auto-generate a bunch from SVG
|
||||||
config.assets."favicon.png".path = ./favicon.png;
|
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: ''
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: '${font.name}';
|
font-family: '${font.name}';
|
||||||
|
@ -13,15 +23,31 @@
|
||||||
}
|
}
|
||||||
'')
|
'')
|
||||||
(
|
(
|
||||||
(crossLists (name: file: weight: { inherit name file weight; })
|
(crossLists (name: file: weight: { inherit name file weight; }) [
|
||||||
[ [ "Signika" ] [ "signika-extended.woff2" "signika.woff2" ] [ 500 700 ] ]
|
[ "Signika" ]
|
||||||
)
|
[
|
||||||
++
|
"signika-extended.woff2"
|
||||||
(crossLists (name: file: weight: { inherit name file weight; })
|
"signika.woff2"
|
||||||
[ [ "Heebo" ] [ "heebo-extended.woff2" "heebo.woff2" ] [ 400 600 ] ]
|
]
|
||||||
|
[
|
||||||
|
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
|
# TODO: get directly from https://github.com/google/fonts
|
||||||
# and compress with https://github.com/fonttools/fonttools
|
# and compress with https://github.com/fonttools/fonttools
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
{ config, options, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
|
@ -7,11 +13,15 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config.templates.html = {
|
config.templates.html = {
|
||||||
dom = document:
|
dom =
|
||||||
|
document:
|
||||||
let
|
let
|
||||||
eval = lib.evalModules {
|
eval = lib.evalModules {
|
||||||
class = "DOM";
|
class = "DOM";
|
||||||
modules = [ document (import ./dom.nix) ];
|
modules = [
|
||||||
|
document
|
||||||
|
(import ./dom.nix)
|
||||||
|
];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
@ -19,27 +29,34 @@ in
|
||||||
value = eval.config;
|
value = eval.config;
|
||||||
};
|
};
|
||||||
|
|
||||||
markdown = { name, body }:
|
markdown =
|
||||||
|
{ name, body }:
|
||||||
let
|
let
|
||||||
commonmark = pkgs.runCommand "${name}.html"
|
commonmark =
|
||||||
|
pkgs.runCommand "${name}.html"
|
||||||
{
|
{
|
||||||
buildInputs = [ pkgs.cmark ];
|
buildInputs = [ pkgs.cmark ];
|
||||||
} ''
|
}
|
||||||
|
''
|
||||||
cmark ${builtins.toFile "${name}.md" body} > $out
|
cmark ${builtins.toFile "${name}.md" body} > $out
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
builtins.readFile commonmark;
|
builtins.readFile commonmark;
|
||||||
nav = { menu, page }:
|
nav =
|
||||||
|
{ menu, page }:
|
||||||
let
|
let
|
||||||
render-item = item:
|
render-item =
|
||||||
if item ? menu then ''
|
item:
|
||||||
|
if item ? menu then
|
||||||
|
''
|
||||||
<li><details><summary>${item.menu.label}</summary>
|
<li><details><summary>${item.menu.label}</summary>
|
||||||
${lib.indent " " (item.menu.outputs.html page)}
|
${lib.indent " " (item.menu.outputs.html page)}
|
||||||
</li>
|
</li>
|
||||||
''
|
''
|
||||||
else if item ? page then ''<li><a href="${page.link item.page}">${item.page.title}</a></li>''
|
else if item ? page then
|
||||||
else ''<li><a href="${item.link.url}">${item.link.label}</a></li>''
|
''<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
|
in
|
||||||
''
|
''
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -50,17 +67,27 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
config.templates.files = fs: with lib;
|
config.templates.files =
|
||||||
|
fs:
|
||||||
|
with lib;
|
||||||
foldl'
|
foldl'
|
||||||
# TODO: create static redirects from `tail <collection>.locations`
|
# 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}";
|
name = head elem.locations + optionalString (type != "") ".${type}";
|
||||||
value = if isStorePath value then value else
|
value =
|
||||||
builtins.toFile
|
if isStorePath value then
|
||||||
(elem.name + optionalString (type != "") ".${type}")
|
value
|
||||||
(toString value);
|
else
|
||||||
}))
|
builtins.toFile (elem.name + optionalString (type != "") ".${type}") (toString value);
|
||||||
elem.outputs)
|
}
|
||||||
|
))
|
||||||
|
elem.outputs
|
||||||
|
)
|
||||||
{ }
|
{ }
|
||||||
fs;
|
fs;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
{ config, options, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib) mkOption
|
inherit (lib)
|
||||||
|
mkOption
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
cfg = config;
|
cfg = config;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
content-types.article = { config, collection, ... }: {
|
content-types.article =
|
||||||
|
{ config, collection, ... }:
|
||||||
|
{
|
||||||
imports = [ cfg.content-types.page ];
|
imports = [ cfg.content-types.page ];
|
||||||
options = {
|
options = {
|
||||||
collection = mkOption {
|
collection = mkOption {
|
||||||
|
@ -26,26 +34,29 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.name = with lib; mkDefault (slug config.title);
|
config.name = with lib; mkDefault (slug config.title);
|
||||||
config.outputs.html = lib.mkForce
|
config.outputs.html = lib.mkForce (
|
||||||
((cfg.templates.html.page config).override (final: prev: {
|
(cfg.templates.html.page config).override (
|
||||||
|
final: prev: {
|
||||||
html = {
|
html = {
|
||||||
# TODO: make authors always a list
|
# TODO: make authors always a list
|
||||||
head.meta.authors = if lib.isList config.author then config.author else [ config.author ];
|
head.meta.authors = if lib.isList config.author then config.author else [ config.author ];
|
||||||
body.content = with lib; map
|
body.content =
|
||||||
(e:
|
with lib;
|
||||||
if isAttrs e && e ? section
|
map (
|
||||||
then
|
e:
|
||||||
recursiveUpdate e
|
if isAttrs e && e ? section then
|
||||||
{
|
recursiveUpdate e {
|
||||||
section.heading = {
|
section.heading = {
|
||||||
before = [ { p.content = "Published ${config.date}"; } ];
|
before = [ { p.content = "Published ${config.date}"; } ];
|
||||||
after = [ { p.content = "Written by ${config.author}"; } ];
|
after = [ { p.content = "Written by ${config.author}"; } ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else e
|
else
|
||||||
)
|
e
|
||||||
prev.html.body.content;
|
) prev.html.body.content;
|
||||||
};
|
};
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,24 +11,34 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Collection of assets, i.e. static files that can be linked to from within documents
|
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 ];
|
imports = [ cfg.content-types.document ];
|
||||||
options.path = mkOption {
|
options.path = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
};
|
};
|
||||||
config.outputs."" = if lib.isStorePath config.path then config.path else "${config.path}";
|
config.outputs."" = if lib.isStorePath config.path then config.path else "${config.path}";
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
default = { };
|
default = { };
|
||||||
};
|
};
|
||||||
|
|
||||||
config.files = with lib;
|
config.files =
|
||||||
|
with lib;
|
||||||
let
|
let
|
||||||
flatten = attrs: mapAttrsToList
|
flatten =
|
||||||
(name: value:
|
attrs:
|
||||||
|
mapAttrsToList (
|
||||||
|
name: value:
|
||||||
# HACK: we somehow have to distinguish a module value from regular attributes.
|
# HACK: we somehow have to distinguish a module value from regular attributes.
|
||||||
# arbitrary choice: the outputs attribute
|
# arbitrary choice: the outputs attribute
|
||||||
if value ? outputs then value else mapAttrsToList value)
|
if value ? outputs then value else mapAttrsToList value
|
||||||
attrs;
|
) attrs;
|
||||||
in
|
in
|
||||||
cfg.templates.files (flatten cfg.assets);
|
cfg.templates.files (flatten cfg.assets);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
{ config, options, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
|
@ -25,7 +31,12 @@ in
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
type = with types; attrsOf (submodule ({ name, config, ... }: {
|
type =
|
||||||
|
with types;
|
||||||
|
attrsOf (
|
||||||
|
submodule (
|
||||||
|
{ name, config, ... }:
|
||||||
|
{
|
||||||
options = {
|
options = {
|
||||||
type = mkOption {
|
type = mkOption {
|
||||||
description = "Type of entries in the collection";
|
description = "Type of entries in the collection";
|
||||||
|
@ -52,7 +63,9 @@ in
|
||||||
};
|
};
|
||||||
entry = mkOption {
|
entry = mkOption {
|
||||||
description = "An entry in the collection";
|
description = "An entry in the collection";
|
||||||
type = with types; collection (submodule ({
|
type =
|
||||||
|
with types;
|
||||||
|
collection (submodule ({
|
||||||
imports = [ config.type ];
|
imports = [ config.type ];
|
||||||
_module.args.collection = config;
|
_module.args.collection = config;
|
||||||
process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls;
|
process-locations = ls: with lib; concatMap (l: map (p: "${p}/${l}") config.prefixes) ls;
|
||||||
|
@ -61,10 +74,19 @@ in
|
||||||
by-name = mkOption {
|
by-name = mkOption {
|
||||||
description = "Entries accessible by symbolic name";
|
description = "Entries accessible by symbolic name";
|
||||||
type = with types; attrsOf attrs;
|
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 =
|
config.files =
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
{ config, options, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
|
@ -14,7 +20,15 @@ in
|
||||||
type = with types; attrsOf deferredModule;
|
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;
|
config._module.args.link = config.link;
|
||||||
options = {
|
options = {
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
|
@ -37,7 +51,10 @@ in
|
||||||
'';
|
'';
|
||||||
type = with types; nonEmptyListOf str;
|
type = with types; nonEmptyListOf str;
|
||||||
apply = config.process-locations;
|
apply = config.process-locations;
|
||||||
example = [ "about/overview" "index" ];
|
example = [
|
||||||
|
"about/overview"
|
||||||
|
"index"
|
||||||
|
];
|
||||||
default = [ config.name ];
|
default = [ config.name ];
|
||||||
};
|
};
|
||||||
process-locations = mkOption {
|
process-locations = mkOption {
|
||||||
|
@ -51,24 +68,26 @@ in
|
||||||
# TODO: we may want links to other representations,
|
# TODO: we may want links to other representations,
|
||||||
# and currently the mapping of output types to output file
|
# and currently the mapping of output types to output file
|
||||||
# names is soft.
|
# names is soft.
|
||||||
default = with lib; target:
|
default =
|
||||||
|
with lib;
|
||||||
|
target:
|
||||||
let
|
let
|
||||||
path = relativePath (head config.locations) (head target.locations);
|
path = relativePath (head config.locations) (head target.locations);
|
||||||
links = mapAttrs
|
links = mapAttrs (
|
||||||
(type: output:
|
type: output: path + optionalString (type != "") ".${type}"
|
||||||
path + optionalString (type != "") ".${type}"
|
|
||||||
# ^^^^^^^^^^^^
|
# ^^^^^^^^^^^^
|
||||||
# convention for raw files
|
# convention for raw files
|
||||||
)
|
) target.outputs;
|
||||||
target.outputs;
|
|
||||||
in
|
in
|
||||||
if length (attrValues links) == 0
|
if length (attrValues links) == 0 then
|
||||||
then throw "no output to link to for '${target.name}'"
|
throw "no output to link to for '${target.name}'"
|
||||||
else if length (attrValues links) == 1
|
else if length (attrValues links) == 1 then
|
||||||
then links // {
|
links
|
||||||
|
// {
|
||||||
__toString = _: head (attrValues links);
|
__toString = _: head (attrValues links);
|
||||||
}
|
}
|
||||||
else links;
|
else
|
||||||
|
links;
|
||||||
};
|
};
|
||||||
outputs = mkOption {
|
outputs = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
{ config, options, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
|
@ -7,7 +12,9 @@ let
|
||||||
cfg = config;
|
cfg = config;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
content-types.event = { config, collection, ... }: {
|
content-types.event =
|
||||||
|
{ config, collection, ... }:
|
||||||
|
{
|
||||||
imports = [ cfg.content-types.page ];
|
imports = [ cfg.content-types.page ];
|
||||||
options = {
|
options = {
|
||||||
collection = mkOption {
|
collection = mkOption {
|
||||||
|
@ -41,41 +48,49 @@ in
|
||||||
};
|
};
|
||||||
config.name = with lib; mkDefault (slug config.title);
|
config.name = with lib; mkDefault (slug config.title);
|
||||||
config.summary = lib.mkDefault config.description;
|
config.summary = lib.mkDefault config.description;
|
||||||
config.outputs.html = lib.mkForce
|
config.outputs.html = lib.mkForce (
|
||||||
((cfg.templates.html.page config).override (final: prev: {
|
(cfg.templates.html.page config).override (
|
||||||
html.body.content = with lib; map
|
final: prev: {
|
||||||
(e:
|
html.body.content =
|
||||||
if isAttrs e && e ? section
|
with lib;
|
||||||
then
|
map (
|
||||||
recursiveUpdate e
|
e:
|
||||||
{
|
if isAttrs e && e ? section then
|
||||||
|
recursiveUpdate e {
|
||||||
section.content = [
|
section.content = [
|
||||||
{
|
{
|
||||||
dl.content = [
|
dl.content =
|
||||||
|
[
|
||||||
{
|
{
|
||||||
terms = [ { dt = "Location"; } ];
|
terms = [ { dt = "Location"; } ];
|
||||||
descriptions = [ { dd = config.location; } ];
|
descriptions = [ { dd = config.location; } ];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
terms = [ { dt = "Start"; } ];
|
terms = [ { dt = "Start"; } ];
|
||||||
descriptions = [{
|
descriptions = [
|
||||||
|
{
|
||||||
dd = config.start-date + lib.optionalString (!isNull config.start-time) " ${config.start-time}";
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
{ config, options, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
options,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
mkOption
|
mkOption
|
||||||
types
|
types
|
||||||
;
|
;
|
||||||
cfg = config;
|
cfg = config;
|
||||||
subtype = baseModule: types.submodule [
|
subtype =
|
||||||
|
baseModule:
|
||||||
|
types.submodule [
|
||||||
baseModule
|
baseModule
|
||||||
{
|
{
|
||||||
_module.freeformType = types.attrs;
|
_module.freeformType = types.attrs;
|
||||||
|
@ -23,7 +30,9 @@ in
|
||||||
type = with types; attrsOf (submodule config.content-types.navigation);
|
type = with types; attrsOf (submodule config.content-types.navigation);
|
||||||
};
|
};
|
||||||
|
|
||||||
config.content-types.named-link = { ... }: {
|
config.content-types.named-link =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
options = {
|
options = {
|
||||||
label = mkOption {
|
label = mkOption {
|
||||||
description = "Link label";
|
description = "Link label";
|
||||||
|
@ -36,7 +45,9 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config.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";
|
||||||
|
@ -50,7 +61,9 @@ in
|
||||||
};
|
};
|
||||||
items = mkOption {
|
items = mkOption {
|
||||||
description = "List of menu items";
|
description = "List of menu items";
|
||||||
type = with types; listOf (attrTag {
|
type =
|
||||||
|
with types;
|
||||||
|
listOf (attrTag {
|
||||||
menu = mkOption { type = submodule cfg.content-types.navigation; };
|
menu = mkOption { type = submodule cfg.content-types.navigation; };
|
||||||
page = mkOption { type = subtype cfg.content-types.page; };
|
page = mkOption { type = subtype cfg.content-types.page; };
|
||||||
link = mkOption { type = submodule cfg.content-types.named-link; };
|
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.
|
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 = page: cfg.templates.html.nav {
|
default.html =
|
||||||
menu = config; inherit page;
|
page:
|
||||||
|
cfg.templates.html.nav {
|
||||||
|
menu = config;
|
||||||
|
inherit page;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,9 @@ in
|
||||||
|
|
||||||
config.files = with lib; cfg.templates.files (attrValues config.pages);
|
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 ];
|
imports = [ cfg.content-types.document ];
|
||||||
options = {
|
options = {
|
||||||
title = mkOption {
|
title = mkOption {
|
||||||
|
|
|
@ -4,14 +4,35 @@ let
|
||||||
inherit (import ./. { }) lib;
|
inherit (import ./. { }) lib;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
test-relativePath = with lib;
|
test-relativePath =
|
||||||
|
with lib;
|
||||||
let
|
let
|
||||||
testData = [
|
testData = [
|
||||||
{ from = "bar"; to = "baz"; expected = "./baz"; }
|
{
|
||||||
{ from = "foo/bar"; to = "foo/baz"; expected = "./baz"; }
|
from = "bar";
|
||||||
{ from = "foo"; to = "bar/baz"; expected = "./bar/baz"; }
|
to = "baz";
|
||||||
{ from = "foo/bar"; to = "baz"; expected = "./../baz"; }
|
expected = "./baz";
|
||||||
{ from = "foo/bar/baz"; to = "foo"; expected = "./../../foo"; }
|
}
|
||||||
|
{
|
||||||
|
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
|
in
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Reference in a new issue