# SPDX-FileCopyrightText: 2024 Lubin Bailly # # SPDX-License-Identifier: EUPL-1.2 { lib, config, pkgs, ... }: let inherit (lib) attrNames concatMapStringsSep concatStringsSep escapeXML filter getExe hasPrefix hasSuffix head importJSON mapAttrs' mapAttrsToList mkDefault mkEnableOption mkIf mkOption optionalString pathIsDirectory removePrefix ; inherit (lib.strings) sanitizeDerivationName ; inherit (lib.types) attrs attrsOf deferredModule listOf path submodule str ; yaml = pkgs.formats.yaml { }; json = pkgs.formats.json { }; cfg = config.services.extranix; module-eval = module-name: module: let ignored-eval = lib.evalModules { modules = module.ignored-modules; inherit (module) specialArgs; }; ignored-opts-doc = pkgs.nixosOptionsDoc { inherit (ignored-eval) options; }; ignored-opts = importJSON "${ignored-opts-doc.optionsJSON}/share/doc/nixos/options.json"; eval = lib.evalModules { modules = module.paths ++ module.ignored-modules; inherit (module) specialArgs; }; opts-doc = pkgs.nixosOptionsDoc { inherit (eval) options; }; opts = importJSON "${opts-doc.optionsJSON}/share/doc/nixos/options.json"; filtered-opts = removeAttrs opts (attrNames ignored-opts); path-translation = let translations = map ( { base, url }: { url = "${url}${optionalString (!hasSuffix "/" url) "/"}"; base = let base1 = toString base; in base1 + (optionalString (!hasSuffix "/" base1) "/"); } ) module.path-translations; in path: let fullPath = path + (optionalString (pathIsDirectory path) "/default.nix"); fitting = filter ({ base, ... }: hasPrefix base fullPath) translations; translate-info = head ( fitting ++ [ (throw ( "${fullPath} is not in any base path of ${module-name}. Base paths are " + concatMapStringsSep "\n" ({ base, ... }: base) translations )) ] ); innerPath = removePrefix translate-info.base fullPath; in { name = "<${innerPath}>"; url = "${translate-info.url}${innerPath}"; }; result' = json.generate "options-extranix-fileDesc.json" { last_update = "-/-"; options = mapAttrsToList (title: val: { inherit title; inherit (val) type readOnly loc ; descriptionHTML = pkgs.runCommand "option-${title}.html" { } '' ${getExe pkgs.pandoc} -f markdown-raw_html ${pkgs.writeText "option-${title}.md" val.description} > $out ''; description = escapeXML val.description; example = escapeXML (val.example.text or ""); default = escapeXML (val.default.text or ""); declarations = map path-translation val.declarations; }) filtered-opts; }; result = pkgs.runCommand "options-extranix.json" { nativeBuildInputs = [ pkgs.jq ]; } '' jq -r '.options[].descriptionHTML | "--rawfile\n" + . + "\n" + .' ${result'} | xargs \ jq -c '.options |= map(.descriptionHTML as $desc | .descriptionHTML |= $ARGS.named.[$desc])' ${result'} \ > $out ''; in result; options-files = mapAttrs' (name: value: { name = sanitizeDerivationName name; value = module-eval name value; }) cfg.modules; webroot = pkgs.callPackage ./webroot.nix { inherit options-files; inherit (cfg) static-data; settings = yaml.generate "config.yaml" cfg.settings; hugo-theme-extranix-options-search = pkgs.callPackage ./hugo-theme-extranix-options-search.nix { }; }; in { options.services.extranix = { enable = mkEnableOption "extranix documentation"; modules = mkOption { type = attrsOf (submodule { options = { specialArgs = mkOption { type = attrs; default = { }; description = '' Special arguments to give to evalModules. ''; }; paths = mkOption { type = listOf deferredModule; description = '' Modules to from which to document options. ''; }; ignored-modules = mkOption { type = listOf deferredModule; default = [ ]; description = '' Modules required to make modules of `paths` valid. ''; example = '' import "''${infra-modulesPath}/module-list.nix" ''; }; path-translations = mkOption { type = listOf (submodule { options = { base = mkOption { type = path; description = '' Base path of some module files to be documented. ''; }; url = mkOption { type = str; description = '' Url root to use for files on this path. ''; }; }; }); description = '' Rules to convert file paths to urls in the documentation to indicate where the option is declared. ''; }; }; }); description = '' Sets of modules to be documented separately. The identifier to give for `settings.params.release_current_stable` (which is the default module shown) is the key after passing through `sanitizeDerivationName`. ''; }; settings = mkOption { inherit (yaml) type; description = '' Settings for the `config.yaml` for the hugo instantiation. ''; example = { baseUrl = "https://example.org/"; title = "Module documentation"; languageCode = "en-us"; params = { release_current_stable = "Some-Module"; logo = "logo.png"; footer_credits_line = '' Based on Home Manager Option Search ''; footer_copyright_line = '' Made by catvayor for the DGNum. ''; main_menu = [ { name = ''Sources''; url = "https://git.example.org/"; } ]; }; }; }; static-data = mkOption { type = path; description = '' Static files for the website. Should have `images/favicon.png` for favicon. ''; }; host = mkOption { type = str; description = '' Hostname of the service. ''; }; }; config = mkIf cfg.enable { services = { extranix.settings = { theme = "extranix-options-search"; params = { releases = mapAttrsToList (name: _: { inherit name; value = sanitizeDerivationName name; }) cfg.modules; release_current_stable = mkDefault (head (attrNames options-files)); }; }; nginx = { enable = true; virtualHosts.${cfg.host}.locations."/".alias = "${webroot}/"; }; }; assertions = [ { assertion = cfg.modules != { }; message = '' `services.extranix` can't be enabled without any modules to document. ''; } { assertion = options-files ? ${cfg.settings.params.release_current_stable}; message = '' `services.extranix.settings.params.release_current_stable` should be the `sanitizeDerivationName` of a key of `services.extranix.modules`, here one of: + ${concatStringsSep "\n + " (attrNames options-files)} ''; } ]; }; }