{
  pkgs,
  lib,
  evaledModules,
  nixosOptionsDoc,
  transformOptions,
  hmOptions,
  search,
}:
with lib;
let
  inherit (evaledModules.config.meta) nixvimInfo;

  mkMDDoc =
    options:
    (nixosOptionsDoc {
      inherit options transformOptions;
      warningsAreErrors = false;
    }).optionsCommonMark;

  removeUnwanted =
    attrs:
    builtins.removeAttrs attrs [
      "_module"
      "_freeformOptions"
      "warnings"
      "assertions"
      "content"
    ];

  removeWhitespace = builtins.replaceStrings [ " " ] [ "" ];

  getSubOptions =
    opts: path: optionalAttrs (isVisible opts) (removeUnwanted (opts.type.getSubOptions path));

  isVisible =
    opts:
    if isOption opts then
      opts.visible or true
    else if opts.isOption then
      opts.index.options.visible or true
    else
      let
        filterFunc = filterAttrs (_: v: if isAttrs v then isVisible 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
          filterAttrs (_: component: component.isOption && (isVisible component)) opts;
      path = removeWhitespace (concatStringsSep "/" path);
      moduleDoc =
        let
          info = optionalAttrs (hasAttrByPath path nixvimInfo) (getAttrFromPath 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.description != null) "${info.description}\n\n")
          + (lib.optionalString (info.url != null) "**URL:** [${info.url}](${info.url})\n\n")
          + (lib.optionalString (
            maintainers != [ ]
          ) "**Maintainers:** ${lib.concatStringsSep ", " maintainersNames}\n\n")
        else
          null;
    };

    components =
      if isOpt then
        { }
      else
        filterAttrs (_: component: !component.isOption && (isVisible component)) opts;

    hasComponents = components != { };

    isOption = isOpt;
  };

  processModulesRec =
    modules:
    let
      recurse =
        path: mods:
        let
          g =
            name: opts:
            if !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} = recursiveUpdate opts {
                      isOption = true;
                      type.getSubOptions = _: _: { }; # Used to exclude suboptions from the submodule definition itself
                    };
                  }
                ) false
              else
                wrapModule (path ++ [ name ]) opts true;
        in
        mapAttrs g mods;
    in
    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 && (isOption opts.index.options);
      in
      acc
      // {
        ${group} = recursiveUpdate last {
          index.options = optionalAttrs isOpt { ${name} = opts.index.options; };

          components = optionalAttrs (!isOpt) {
            ${name} = recursiveUpdate opts {
              index.path = removeWhitespace (
                concatStringsSep "/" ((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
        concatStringsSep "\n" (mapAttrsToList g mods);
    in
    recurse modules;

  docs = rec {
    modules = processModulesRec (removeUnwanted evaledModules.options);
    commands = mapModulesToString (
      name: opts:
      let
        isBranch = if (hasSuffix "index" opts.index.path) then true else opts.hasComponents;
        path = if isBranch then "${opts.index.path}/index.md" else "${opts.index.path}.md";
      in
      (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 = {
    nixvimOptions = mapModulesToString (
      name: opts:
      let
        isBranch = if name == "index" then true else 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 = with builtins; length (filter isString (split "/" opts.index.path)) - 1;

        padding = concatStrings (builtins.genList (_: "\t") indentLevel);
      in
      "${padding}- [${name}](${path})"
    ) docs.modules;
  };

  prepareMD = ''
    # Copy inputs into the build directory
    cp -r --no-preserve=all $inputs/* ./
    cp ${../../CONTRIBUTING.md} ./CONTRIBUTING.md
    cp -r ${../user-guide} ./user-guide
    cp -r ${../modules} ./modules

    # Copy the generated MD docs into the build directory
    # Using pkgs.writeShellScript helps to avoid the "bash: argument list too long" error
    bash -e ${pkgs.writeShellScript "copy_docs" docs.commands}

    # Prepare SUMMARY.md for mdBook
    # Using pkgs.writeText helps to avoid the same error as above
    substituteInPlace ./SUMMARY.md \
      --replace-fail "@NIXVIM_OPTIONS@" "$(cat ${pkgs.writeText "nixvim-options-summary.md" mdbook.nixvimOptions})"

    substituteInPlace ./modules/hm.md \
      --replace-fail "@HM_OPTIONS@" "$(cat ${mkMDDoc hmOptions})"
  '';
in
pkgs.stdenv.mkDerivation {
  name = "nixvim-docs";

  phases = [ "buildPhase" ];

  buildInputs = [ pkgs.mdbook ];
  inputs = sourceFilesBySuffices ./. [
    ".md"
    ".toml"
    ".js"
  ];

  buildPhase = ''
    dest=$out/share/doc/nixvim
    mkdir -p $dest
    ${prepareMD}
    mdbook build
    cp -r ./book/* $dest
    mkdir -p $dest/search
    cp -r ${search}/* $dest/search
  '';
}