Compare commits

..

1 commit

Author SHA1 Message Date
Valentin Gagarin 45345ac541 ensure the section hierarchy is spec-compliant
- check that there is a single section hierarchy

  this also allows us to automatically assign heading levels

- check that the maximum nesting depth is not exceeded
2024-11-06 23:40:31 +01:00
2 changed files with 33 additions and 37 deletions

View file

@ -53,7 +53,7 @@ in
render-item = item: render-item = item:
if item ? menu then '' if item ? menu then ''
<li>${item.menu.label} <li>${item.menu.label}
${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 ''<li><a href="${page.link item.page}">${item.page.title}</a></li>''

View file

@ -147,11 +147,11 @@ let
print-element = name: attrs: content: print-element = name: attrs: content:
with lib; with lib;
squash (trim '' lib.squash ''
<${name}${print-attrs attrs}> <${name}${print-attrs attrs}>
${lib.indent " " content} ${lib.indent " " content}
</${name}> </${name}>
''); '';
elements = rec { elements = rec {
document = { ... }: { document = { ... }: {
@ -369,7 +369,7 @@ let
process-with-depth = depth: content: process-with-depth = depth: content:
map map
(x: (x:
if isAttrs x && x ? section if isAttrs x && x ? section && x.section ? heading
then x // { then x // {
section = x.section // { section = x.section // {
heading-level = depth; heading-level = depth;
@ -383,7 +383,7 @@ let
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: { inherit (def) file; value = v; depth = depth; })
(filter (x: isAttrs x && x ? section) content); (filter (x: isAttrs x && x ? section && x.section ? heading) content);
subsections = concatMap subsections = concatMap
(x: (x:
if isAttrs x && x ? section && x.section ? content if isAttrs x && x ? section && x.section ? content
@ -403,8 +403,14 @@ let
processed = map find-and-attach defs; processed = map find-and-attach defs;
all-sections = flatten (map (p: p.validation) processed); all-sections = flatten (map (p: p.validation) processed);
too-deep = filter (sec: sec.depth > 6) all-sections; too-deep = filter (sec: sec.depth > 6) all-sections;
top-level = filter (sec: sec.depth == 1) all-sections;
in in
if too-deep != [ ] then if length top-level > 1 then
throw ''
The option `${lib.options.showOption loc}` has multiple section hierarchies defined:
${concatMapStrings (def: " - in ${toString def.file}\n") top-level}
Content must have at most one section hierarchy.''
else 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}
@ -441,37 +447,29 @@ let
type = with types; nullOr (submodule { options = global-attrs; }); type = with types; nullOr (submodule { options = global-attrs; });
default = null; default = null;
}; };
# TODO: make `pre-`/`post-heading` wrap the heading in `<hgroup>` if non-empty
# https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element
pre-heading = mkOption {
type = with types; listOf (attrTag ({ inherit p; } // categories.scripting));
default = [ ];
};
heading = mkOption { heading = mkOption {
# XXX: while there are no explicit rules on whether sections should contain headers,
# sections should have content that would be listed in an outline.
#
# https://html.spec.whatwg.org/multipage/sections.html#use-div-for-wrappers
#
# 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 ({ ... }: { type = with types; submodule ({ ... }: {
imports = [ element ]; imports = [ element ];
options = { options = {
attrs = mkAttrs { }; attrs = mkAttrs { };
# TODO: make `before`/`after` wrap the heading in `<hgroup>` if non-empty
# https://html.spec.whatwg.org/multipage/sections.html#the-hgroup-element
before = mkOption {
type = with types; listOf (attrTag ({ inherit p; } // categories.scripting));
default = [ ];
};
content = mkOption { content = mkOption {
# https://html.spec.whatwg.org/multipage/sections.html#the-h1,-h2,-h3,-h4,-h5,-and-h6-elements # 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));
}; };
after = mkOption {
type = with types;
listOf (attrTag ({ inherit p; } // categories.scripting));
default = [ ];
};
}; };
}); });
}; };
post-heading = mkOption {
type = with types;
listOf (attrTag ({ inherit p; } // categories.scripting));
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 {
type = with types; listOf (either str (attrTag categories.flow)); type = with types; listOf (either str (attrTag categories.flow));
@ -484,24 +482,22 @@ let
type = with types; ints.between 1 6; type = with types; ints.between 1 6;
internal = true; internal = true;
}; };
config = { config.categories = [ "flow" "sectioning" "palpable" ];
categories = [ "flow" "sectioning" "palpable" ]; config.__toString = self: with lib;
__toString = self: with lib; let
let n = toString config.heading-level;
n = toString config.heading-level; content =
content = ''<h${n}${print-attrs self.heading.attrs}>${self.heading.content}</h${n}> "<h${n}${print-attrs self.heading.attrs}>${self.heading.content}</h${n}>" + join "\n" (map
'' + join "\n" (map
(e: (e:
if isAttrs e if isAttrs e
then toString (lib.head (attrValues e)) then toString (lib.head (attrValues e))
else e else e
) )
self.content); self.content);
in in
if !isNull self.attrs if !isNull self.attrs
then print-element name self.attrs content then print-element name self.attrs content
else content; else content;
};
}; };
}; };
in in