forked from Fediversity/fediversity.eu
ensure the section hierarchy is spec-compliant
- automatically assign heading levels - check that the maximum nesting depth is not exceeded
This commit is contained in:
parent
4160b6e976
commit
179482d043
|
@ -117,4 +117,45 @@ in
|
||||||
in
|
in
|
||||||
pkgs.runCommand "source" { } script;
|
pkgs.runCommand "source" { } script;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# TODO: this is an artefact of exploration; needs to be adapted to actual use
|
||||||
|
config.templates.table-of-contents = { config, ... }:
|
||||||
|
let
|
||||||
|
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
|
||||||
|
# TODO: go into depth manually here,
|
||||||
|
# we don't want to pollute the DOM implementation
|
||||||
|
(c: (lib.head (attrValues c)).outline)
|
||||||
|
(filter (c: isAttrs c && (lib.head (attrValues c)) ? outline) config.content);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
__toString = mkOption {
|
||||||
|
type = with types; functionTo str;
|
||||||
|
# TODO: convert to HTML
|
||||||
|
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 ""}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options.outline = mkOption {
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,14 +26,6 @@ let
|
||||||
"scripting" # https://html.spec.whatwg.org/multipage/dom.html#script-supporting-elements
|
"scripting" # https://html.spec.whatwg.org/multipage/dom.html#script-supporting-elements
|
||||||
];
|
];
|
||||||
|
|
||||||
get-section-depth = content: with lib; foldl'
|
|
||||||
(acc: elem:
|
|
||||||
if isAttrs elem
|
|
||||||
then max acc (lib.head (attrValues elem)).section-depth
|
|
||||||
else acc # TODO: parse with e.g. https://github.com/remarkjs/remark to avoid raw strings
|
|
||||||
) 0
|
|
||||||
content;
|
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -49,15 +41,6 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: rename to something about sectioning
|
|
||||||
content-element = { ... }: {
|
|
||||||
options.section-depth = mkOption {
|
|
||||||
type = types.ints.unsigned;
|
|
||||||
default = 0;
|
|
||||||
internal = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# options with types for all the defined DOM elements
|
# options with types for all the defined DOM elements
|
||||||
element-types = lib.mapAttrs
|
element-types = lib.mapAttrs
|
||||||
(name: value: mkOption { type = types.submodule value; })
|
(name: value: mkOption { type = types.submodule value; })
|
||||||
|
@ -369,18 +352,74 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
body = { config, name, ... }: {
|
body = { config, name, ... }: {
|
||||||
imports = [ element content-element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
content = mkOption {
|
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 lib;
|
||||||
|
let
|
||||||
|
find-and-attach = def:
|
||||||
|
let
|
||||||
|
process-with-depth = depth: content:
|
||||||
|
map
|
||||||
|
(x:
|
||||||
|
if isAttrs x && x ? section && x.section ? heading
|
||||||
|
then x // {
|
||||||
|
section = x.section // {
|
||||||
|
heading-level = depth;
|
||||||
|
content = process-with-depth (depth + 1) (x.section.content or [ ]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else x
|
||||||
|
)
|
||||||
|
content;
|
||||||
|
|
||||||
|
find-with-depth = depth: content:
|
||||||
|
let
|
||||||
|
sections = map (v: { inherit (def) file; value = v; depth = depth; })
|
||||||
|
(filter (x: isAttrs x && x ? section && x.section ? heading) 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;
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit def;
|
||||||
|
processed = process-with-depth 1 def.value;
|
||||||
|
validation = find-with-depth 1 def.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
processed = map find-and-attach defs;
|
||||||
|
all-sections = flatten (map (p: p.validation) processed);
|
||||||
|
too-deep = filter (sec: sec.depth > 6) all-sections;
|
||||||
|
in
|
||||||
|
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}
|
||||||
|
Section hierarchy must not be deeper than 6 levels.''
|
||||||
|
else baseType.merge loc (map (p: p.def // { value = p.processed; }) processed);
|
||||||
|
};
|
||||||
|
in
|
||||||
# HACK: bail out for now
|
# HACK: bail out for now
|
||||||
|
with-section-constraints
|
||||||
# TODO: find a reasonable cut-off for where to place raw content
|
# TODO: find a reasonable cut-off for where to place raw content
|
||||||
listOf (either str (attrTag categories.flow));
|
(listOf (either str (attrTag categories.flow)));
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.section-depth = get-section-depth config.content;
|
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self: with lib;
|
config.__toString = self: with lib;
|
||||||
print-element name self.attrs (
|
print-element name self.attrs (
|
||||||
|
@ -393,8 +432,9 @@ let
|
||||||
self.content)
|
self.content)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
section = { config, name, ... }: {
|
section = { config, name, ... }: {
|
||||||
imports = [ element content-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 {
|
||||||
|
@ -413,6 +453,7 @@ let
|
||||||
options = {
|
options = {
|
||||||
attrs = mkAttrs { };
|
attrs = mkAttrs { };
|
||||||
content = mkOption {
|
content = mkOption {
|
||||||
|
# https://html.spec.whatwg.org/multipage/sections.html#the-h1,-h2,-h3,-h4,-h5,-and-h6-elements
|
||||||
type = with types; either str (listOf (attrTag categories.phrasing));
|
type = with types; either str (listOf (attrTag categories.phrasing));
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -429,14 +470,12 @@ let
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
config.section-depth = get-section-depth config.content + 1;
|
|
||||||
options.heading-level = mkOption {
|
options.heading-level = mkOption {
|
||||||
# XXX: this will proudly fail if the invariant is violated,
|
# XXX: this will proudly fail if the invariant is violated,
|
||||||
# but the error message will be inscrutable
|
# but the error message will be inscrutable
|
||||||
type = with types; ints.between 1 6;
|
type = with types; ints.between 1 6;
|
||||||
internal = true;
|
internal = true;
|
||||||
};
|
};
|
||||||
config.heading-level = cfg.section-depth - config.section-depth + 1;
|
|
||||||
config.categories = [ "flow" "sectioning" "palpable" ];
|
config.categories = [ "flow" "sectioning" "palpable" ];
|
||||||
config.__toString = self: with lib;
|
config.__toString = self: with lib;
|
||||||
let
|
let
|
||||||
|
@ -457,12 +496,11 @@ let
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [ element content-element ];
|
imports = [ element ];
|
||||||
options = {
|
options = {
|
||||||
inherit (element-types) html;
|
inherit (element-types) html;
|
||||||
};
|
};
|
||||||
|
|
||||||
config.section-depth = config.html.body.section-depth;
|
|
||||||
config.categories = [ ];
|
config.categories = [ ];
|
||||||
config.__toString = self: ''
|
config.__toString = self: ''
|
||||||
<!DOCTYPE HTML >
|
<!DOCTYPE HTML >
|
||||||
|
|
Reference in a new issue