{ pkgs, callPackage, runCommand, lib, evaledModules, nixosOptionsDoc, transformOptions, search, lib-docs, # The root directory of the site baseHref ? "/", # A list of all available docs that should be linked to # Each element should contain { branch; nixpkgsBranch; baseHref; status; } availableVersions ? [ ], }: let inherit (evaledModules.config.meta) nixvimInfo; mkMDDoc = options: (nixosOptionsDoc { inherit options transformOptions; }).optionsCommonMark; removeUnwanted = attrs: # FIXME: We incorrectly remove _freeformOptions here. # # However we can't fix the bug because we derive page names from attrnames; # the correct behaviour would be to ignore attrnames and use option locs. # # As a workaround, merge the freeform options at the top of these attrs, # however this can run into name conflicts 😢 # # We should move this workaround to where we decide the page name and # whether to nest into a sub-page, so that we can keep the original # _freeformOptions attr as intended. attrs._freeformOptions or { } // builtins.removeAttrs attrs [ "_module" "_freeformOptions" "warnings" "assertions" "content" ]; removeWhitespace = builtins.replaceStrings [ " " ] [ "" ]; getSubOptions = opts: path: lib.optionalAttrs (isDeeplyVisible opts) (removeUnwanted (opts.type.getSubOptions path)); isVisible = isVisibleWith true; isDeeplyVisible = isVisibleWith false; isVisibleWith = shallow: opts: let test = opt: let internal = opt.internal or false; visible = opt.visible or true; visible' = if visible == "shallow" then shallow else visible; in visible' && !internal; in if lib.isOption opts then test opts else if opts.isOption then test opts.index.options else let filterFunc = lib.filterAttrs (_: v: if lib.isAttrs v then isVisibleWith shallow v else true); hasEmptyIndex = (filterFunc opts.index.options) == { }; hasEmptyComponents = (filterFunc opts.components) == { }; in !hasEmptyIndex || !hasEmptyComponents; wrapModule = path: opts: isOpt: rec { index = { options = if isOpt then opts else lib.filterAttrs (_: component: component.isOption && (isVisible component)) opts; path = removeWhitespace (lib.concatStringsSep "/" path); moduleDoc = let info = lib.attrByPath path { } nixvimInfo; maintainers = lib.unique (evaledModules.config.meta.maintainers.${info.file} or [ ]); maintainersNames = builtins.map maintToMD maintainers; maintToMD = m: if m ? github then "[${m.name}](https://github.com/${m.github})" else m.name; in # Make sure this path has a valid info attrset if info ? file && info ? description && info ? url then "# ${lib.last path}\n\n" + (lib.optionalString (info.url != null) "**URL:** [${info.url}](${info.url})\n\n") + (lib.optionalString ( maintainers != [ ] ) "**Maintainers:** ${lib.concatStringsSep ", " maintainersNames}\n\n") + lib.optionalString (info.description != null) '' --- ${info.description} '' else null; }; components = if isOpt then { } else lib.filterAttrs (_: component: !component.isOption && (isVisible component)) opts; hasComponents = components != { }; isOption = isOpt; }; processModulesRec = modules: let recurse = path: mods: let g = name: opts: if !lib.isOption opts then wrapModule (path ++ [ name ]) (recurse (path ++ [ name ]) opts) false else let subOpts = getSubOptions opts (path ++ [ name ]); in if subOpts != { } then wrapModule (path ++ [ name ]) ( (recurse (path ++ [ name ]) subOpts) // { # This is necessary to include the submodule option's definition in the docs (description, type, etc.) # For instance, this helps submodules like "autoCmd" to include their base definitions and examples in the docs # Though there might be a better, less "hacky" solution than this. ${name} = lib.recursiveUpdate opts { isOption = true; type.getSubOptions = _: _: { }; # Used to exclude suboptions from the submodule definition itself }; } ) false else wrapModule (path ++ [ name ]) opts true; in lib.mapAttrs g mods; in lib.foldlAttrs ( acc: name: opts: let group = if !opts.hasComponents then "Neovim Options" else "none"; last = acc.${group} or { index = { options = { }; path = removeWhitespace "${group}"; moduleDoc = null; }; components = { }; isGroup = true; hasComponents = false; }; isOpt = !opts.hasComponents && (lib.isOption opts.index.options); in acc // { ${group} = lib.recursiveUpdate last { index.options = lib.optionalAttrs isOpt { ${name} = opts.index.options; }; components = lib.optionalAttrs (!isOpt) { ${name} = lib.recursiveUpdate opts { index.path = removeWhitespace ( lib.concatStringsSep "/" ((lib.optional (group != "none") group) ++ [ opts.index.path ]) ); hasComponents = true; }; }; hasComponents = last.components != { }; }; } ) { } (recurse [ ] modules); mapModulesToString = f: modules: let recurse = mods: let g = name: opts: if (opts ? "isGroup") then if name != "none" then (f name opts) + ("\n" + recurse opts.components) else recurse opts.components else if opts.hasComponents then (f name opts) + ("\n" + recurse opts.components) else f name opts; in lib.concatStringsSep "\n" (lib.mapAttrsToList g mods); in recurse modules; docs = rec { modules = processModulesRec (removeUnwanted evaledModules.options); commands = mapModulesToString ( name: opts: let isBranch = if (lib.hasSuffix "index" opts.index.path) then true else opts.hasComponents; # Ensure `path` is escaped because we use it in a shell script path = lib.strings.escapeShellArg ( if isBranch then "${opts.index.path}/index.md" else "${opts.index.path}.md" ); in (lib.optionalString isBranch "mkdir -p ${opts.index.path}\n") + ( if opts.index.moduleDoc == null then "cp ${mkMDDoc opts.index.options} ${path}" else # Including moduleDoc's text directly will result in bash interpreting special chars, # write it to the nix store and `cat` the file instead. '' { cat ${pkgs.writeText "module-doc" opts.index.moduleDoc} cat ${mkMDDoc opts.index.options} } > ${path} '' ) ) modules; }; mdbook = { nixvimOptionsSummary = mapModulesToString ( name: opts: let isBranch = name == "index" || (opts.hasComponents && opts.index.options != { }); path = if isBranch then "${opts.index.path}/index.md" else if !opts.hasComponents then "${opts.index.path}.md" else ""; indentLevel = lib.count (c: c == "/") (lib.stringToCharacters opts.index.path); padding = lib.strings.replicate indentLevel "\t"; in "${padding}- [${name}](${path})" ) docs.modules; # A list of platform-specific docs # [ { name, file, path, configuration } ] wrapperOptions = builtins.map (filename: rec { # Eval a configuration for the platform's module configuration = lib.evalModules { modules = [ ../../wrappers/modules/${filename}.nix { # Ignore definitions for missing options _module.check = false; } ]; }; # Also include display name, filepath, and rendered docs inherit (configuration.config.meta.wrapper) name; file = mkMDDoc (removeUnwanted configuration.options); path = "./platforms/${filename}.md"; }) [ "nixos" "hm" "darwin" ]; # Markdown summary for the table of contents wrapperOptionsSummary = lib.foldl ( text: { name, path, ... }: text + "\n\t- [${name}](${path})" ) "" mdbook.wrapperOptions; # Attrset of { filePath = renderedDocs; } wrapperOptionsFiles = lib.listToAttrs ( builtins.map ( { path, file, ... }: { name = path; value = file; } ) mdbook.wrapperOptions ); }; # Zip the list of attrs into an attr of lists, for use as bash arrays zippedVersions = assert lib.assertMsg (lib.all (o: o ? branch && o ? nixpkgsBranch && o ? baseHref && o ? status) availableVersions) '' Expected all "availableVersions" docs entries to contain { branch, nixpkgsBranch, baseHref, status } attrs! ''; lib.zipAttrs availableVersions; in pkgs.stdenv.mkDerivation (finalAttrs: { name = "nixvim-docs"; # Use structured attrs to avoid "bash: argument list too long" errors __structuredAttrs = true; phases = [ "buildPhase" ]; buildInputs = [ pkgs.mdbook pkgs.mdbook-alerts pkgs.mdbook-pagetoc ]; # Build a source from the fileset containing the following paths, # as well as all .md, .toml, & .js files in this directory src = lib.fileset.toSource { root = ../../.; fileset = lib.fileset.unions [ ../user-guide ../platforms (lib.fileset.fileFilter ( { type, hasExt, ... }: type == "regular" && lib.any hasExt [ "css" "js" "md" "toml" ] ) ./.) ]; }; contributing = finalAttrs.passthru.fix-links ../../CONTRIBUTING.md; maintaining = finalAttrs.passthru.fix-links ../../MAINTAINING.md; buildPhase = '' mkdir -p $out # Copy (and flatten) src into the build directory cp -r --no-preserve=all $src/* ./ mv ./docs/* ./ && rmdir ./docs mv ./mdbook/* ./ && rmdir ./mdbook # Copy the contributing and maintaining files cp $contributing ./CONTRIBUTING.md substitute $maintaining ./MAINTAINING.md \ --replace-fail 'This file' 'This page' # Symlink the function docs for path in ${lib-docs}/* do echo "symlinking \"$path\" to \"$(basename "$path")\"" ln -s "$path" $(basename "$path") done # Copy the generated MD docs into the build directory bash -e ${finalAttrs.passthru.copy-docs} # Copy the generated MD docs for the wrapper options for path in "''${!wrapperOptionsFiles[@]}" do file="''${wrapperOptionsFiles[$path]}" cp "$file" "$path" done # Patch book.toml substituteInPlace ./book.toml \ --replace-fail "@SITE_URL@" "$baseHref" # Patch SUMMARY.md - which defiens mdBook's table of contents substituteInPlace ./SUMMARY.md \ --replace-fail "@FUNCTIONS_MENU@" "$functionsSummary" \ --replace-fail "@PLATFORM_OPTIONS@" "$wrapperOptionsSummary" \ --replace-fail "@NIXVIM_OPTIONS@" "$nixvimOptionsSummary" # Patch index.md substituteInPlace ./index.md \ --replace-fail "@README@" "$(cat ${finalAttrs.passthru.readme})" \ --replace-fail "@DOCS_VERSIONS@" "$(cat ${finalAttrs.passthru.docs-versions})" # Patch user-configs substituteInPlace ./user-guide/config-examples.md \ --replace-fail "@USER_CONFIGS@" "$(cat ${finalAttrs.passthru.user-configs})" mdbook build cp -r ./book/* $out mkdir -p $out/search cp -r ${finalAttrs.passthru.search}/* $out/search ''; inherit baseHref; inherit (mdbook) nixvimOptionsSummary wrapperOptionsSummary wrapperOptionsFiles ; functionsSummary = lib-docs.menu; passthru = { fix-links = callPackage ../fix-links { # FIXME: determine values from availableVersions & baseHref docsUrl = "https://nix-community.github.io/nixvim/"; githubUrl = "https://github.com/nix-community/nixvim/blob/main/"; }; copy-docs = pkgs.writeShellScript "copy-docs" docs.commands; readme = runCommand "readme" { start = ""; end = ""; src = finalAttrs.passthru.fix-links ../../README.md; } '' # extract relevant section of the README sed -n "/$start/,/$end/p" $src > $out ''; search = search.override { baseHref = finalAttrs.baseHref + "search/"; }; docs-versions = runCommand "docs-versions" { __structuredAttrs = true; branches = zippedVersions.branch or [ ]; nixpkgsBranches = zippedVersions.nixpkgsBranch or [ ]; baseHrefs = zippedVersions.baseHref or [ ]; statuses = zippedVersions.status or [ ]; current = baseHref; } '' touch "$out" for i in ''${!branches[@]}; do branch="''${branches[i]}" nixpkgs="''${nixpkgsBranches[i]}" baseHref="''${baseHrefs[i]}" linkText="\`$branch\` branch" status="''${statuses[i]}" link= suffix= if [ "$baseHref" = "$current" ]; then # Don't bother linking to ourselves link="$linkText" suffix=" _(this page)_" else link="[$linkText]($baseHref)" fi statusClass= statusText= if [ "$status" = "beta" ]; then statusClass="label label-info" statusText=Beta elif [ "$status" = "deprecated" ]; then statusClass="label label-warning" statusText=Deprecated fi { echo -n '- ' if [ -n "$statusClass" ] && [ -n "$statusText" ]; then echo -n ''"$statusText"' ' fi echo -n "The $link" echo -n ", for use with nixpkgs \`$nixpkgs\`" echo "$suffix" } >> "$out" done ''; user-configs = callPackage ../user-configs { }; }; })