From 661158223a532be171957187e7ea47a8d97a7a79 Mon Sep 17 00:00:00 2001 From: valentin gagarin Date: Wed, 13 Nov 2024 15:24:41 +0100 Subject: [PATCH] ensure the section hierarchy is spec-compliant - automatically assign heading levels - check that the maximum nesting depth is not exceeded --- website/presentation/default.nix | 41 +++++++++++++++ website/presentation/dom.nix | 90 +++++++++++++++++++++++--------- 2 files changed, 105 insertions(+), 26 deletions(-) diff --git a/website/presentation/default.nix b/website/presentation/default.nix index a3435028..6638081c 100644 --- a/website/presentation/default.nix +++ b/website/presentation/default.nix @@ -117,4 +117,45 @@ in in pkgs.runCommand "source" { } script; }; + + # TODO: this is an artefact of exploration; needs to be adapted to actual use + config.templates.table-of-contents = { config, ... }: + 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); + }; + }; + }; } diff --git a/website/presentation/dom.nix b/website/presentation/dom.nix index 0f583b7d..5a9227ff 100644 --- a/website/presentation/dom.nix +++ b/website/presentation/dom.nix @@ -26,14 +26,6 @@ let "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 element = { ... }: { # 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 element-types = lib.mapAttrs (name: value: mkOption { type = types.submodule value; }) @@ -369,18 +352,74 @@ let }; body = { config, name, ... }: { - imports = [ element content-element ]; + imports = [ element ]; options = { attrs = mkAttrs { }; content = mkOption { 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 - # TODO: find a reasonable cut-off for where to place raw content - listOf (either str (attrTag categories.flow)); + with-section-constraints + # TODO: find a reasonable cut-off for where to place raw content + (listOf (either str (attrTag categories.flow))); default = [ ]; }; }; - config.section-depth = get-section-depth config.content; + config.categories = [ ]; config.__toString = self: with lib; print-element name self.attrs ( @@ -393,8 +432,9 @@ let self.content) ); }; + section = { config, name, ... }: { - imports = [ element content-element ]; + imports = [ element ]; options = { # setting to an attribute set will wrap the section in
attrs = mkOption { @@ -413,6 +453,7 @@ let options = { attrs = mkAttrs { }; 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)); }; }; @@ -429,14 +470,12 @@ let default = [ ]; }; }; - config.section-depth = get-section-depth config.content + 1; options.heading-level = mkOption { # XXX: this will proudly fail if the invariant is violated, # but the error message will be inscrutable type = with types; ints.between 1 6; internal = true; }; - config.heading-level = cfg.section-depth - config.section-depth + 1; config.categories = [ "flow" "sectioning" "palpable" ]; config.__toString = self: with lib; let @@ -457,12 +496,11 @@ let }; in { - imports = [ element content-element ]; + imports = [ element ]; options = { inherit (element-types) html; }; - config.section-depth = config.html.body.section-depth; config.categories = [ ]; config.__toString = self: ''