{ lib, ... }: let inherit (lib) optional concatStrings flatten mkOption mapAttrs isString replicate flip getAttr concatStringsSep ; inherit (lib.types) lazyAttrsOf oneOf submodule str listOf ; textType = oneOf [ str (submodule { options = { heading = mkOption { type = str; default = ""; }; description = mkOption { type = str; default = ""; }; order = mkOption { type = listOf str; }; parts = mkOption { type = lazyAttrsOf textType; }; }; }) ]; mkListFromAttrs = prefix: { name, value }: let sectionHeading = result: "${concatStrings (replicate prefix "#")} ${result}"; in if isString value then [ (sectionHeading name) value ] else # TODO: handle order being empty flatten [ [ (sectionHeading (if value.heading == "" then name else value.heading)) ] (optional (value.description != "") value.description) (map (mkListFromAttrs (prefix + 1)) ( map (x: { name = x; value = flip getAttr value.parts x; }) value.order )) ]; in { options.text = mkOption { default = { }; type = lazyAttrsOf textType; apply = mapAttrs ( name: value: concatStringsSep "\n" (flatten (mkListFromAttrs 1 { inherit name value; })) ); }; config.text.readme.parts.helpers.parts.text-helper = "The option `text. supports either a string or a submodule with attributes order and parts. The parts attribute can either be a string, which will get concatenated in the order laid out in `text..order`, or can itself have the attributes order and parts, in which case it will be evaluated recursively."; }