diff --git a/docs/lib/default.nix b/docs/lib/default.nix
index fb37706f..641e5e0e 100644
--- a/docs/lib/default.nix
+++ b/docs/lib/default.nix
@@ -6,173 +6,147 @@
writers,
nixdoc,
nixvim,
- pageSpecs ? import ./pages.nix,
+ pageSpecs ? ./pages.nix,
}:
let
- # Some pages are just menu entries, others have an actual markdown page that
- # needs rendering.
- shouldRenderPage = page: page ? file || page ? markdown;
-
- # Normalise a page node, recursively normalise its children
- elaboratePage =
- loc:
- {
- title ? "",
- markdown ? null,
- file ? null,
- pages ? { },
- }@page:
- {
- name = lib.attrsets.showAttrPath loc;
- loc = lib.throwIfNot (
- builtins.head loc == "lib"
- ) "All pages must be within `lib`, unexpected root `${builtins.head loc}`" (builtins.tail loc);
- }
- // lib.optionalAttrs (shouldRenderPage page) {
- inherit
- file
- title
- ;
- markdown =
- if builtins.isString markdown then
- builtins.toFile "${lib.strings.replaceStrings [ "/" "-" ] (lib.lists.last loc)}.md" markdown
- else
- markdown;
- outFile = lib.strings.concatStringsSep "/" (loc ++ [ "index.md" ]);
- }
- // lib.optionalAttrs (page ? pages) {
- pages = elaboratePages loc pages;
- };
-
- # Recursively normalise page nodes
- elaboratePages = prefix: builtins.mapAttrs (name: elaboratePage (prefix ++ [ name ]));
+ pageConfiguration = lib.evalModules {
+ modules = [
+ pageSpecs
+ {
+ freeformType = lib.types.attrsOf (
+ lib.types.submoduleWith {
+ modules = [ ../modules/page.nix ];
+ }
+ );
+ }
+ ];
+ };
+ pages = pageConfiguration.config;
# Collect all page nodes into a list of page entries
collectPages =
pages:
builtins.concatMap (
- page:
- [ (builtins.removeAttrs page [ "pages" ]) ]
- ++ lib.optionals (page ? pages) (collectPages page.pages)
+ node:
+ let
+ children = builtins.removeAttrs node [ "_page" ];
+ in
+ lib.optional (node ? _page) node._page ++ lib.optionals (children != { }) (collectPages children)
) (builtins.attrValues pages);
# Normalised page specs
- elaboratedPageSpecs = elaboratePages [ ] pageSpecs;
- pageList = collectPages elaboratedPageSpecs;
- pagesToRender = builtins.filter (page: page ? outFile) pageList;
- pagesWithFunctions = builtins.filter (page: page.file or null != null) pageList;
-in
+ pageList = collectPages pages;
+ pagesToRender = builtins.filter (page: page.hasContent) pageList;
-runCommand "nixvim-lib-docs"
- {
- nativeBuildInputs = [
- nixdoc
- ];
-
- locations = writers.writeJSON "locations.json" (
- import ./function-locations.nix {
- inherit lib;
- rootPath = nixvim;
- functionSet = lib.extend nixvim.lib.overlay;
- pathsToScan = builtins.catAttrs "loc" pagesWithFunctions;
- revision = nixvim.rev or "main";
- }
- );
-
- passthru.menu = import ./menu.nix {
- inherit lib;
- pageSpecs = elaboratedPageSpecs;
- };
-
- passthru.pages = builtins.listToAttrs (
- builtins.map (
- { name, outFile, ... }:
- {
- inherit name;
- value = outFile;
- }
- ) pagesToRender
- );
- }
- ''
- function docgen {
- md_file="$1"
- in_file="$2"
- name="$3"
- out_file="$out/$4"
- title="$5"
-
- if [[ -z "$in_file" ]]; then
- if [[ -z "$md_file" ]]; then
- >&2 echo "No markdown or nix file for $name"
- exit 1
- fi
- elif [[ -f "$in_file/default.nix" ]]; then
- in_file+="/default.nix"
- elif [[ ! -f "$in_file" ]]; then
- >&2 echo "File not found: $in_file"
- exit 1
- fi
-
- if [[ -n "$in_file" ]]; then
- nixdoc \
- --file "$in_file" \
- --locs "$locations" \
- --category "$name" \
- --description "REMOVED BY TAIL" \
- --prefix "" \
- --anchor-prefix "" \
- | tail --lines +2 \
- > functions.md
- fi
-
- default_heading="# $name"
- if [[ -n "$title" ]]; then
- default_heading+=": $title"
- fi
-
- print_heading=true
- if [[ -f "$md_file" ]] && [[ "$(head --lines 1 "$md_file")" == '# '* ]]; then
- >&2 echo "NOTE: markdown file for $name starts with a
heading. Skipping default heading \"$default_heading\"."
- >&2 echo " Found \"$(head --lines 1 "$md_file")\" in: $md_file"
- print_heading=false
- fi
-
- mkdir -p $(dirname "$out_file")
- (
- if [[ "$print_heading" = true ]]; then
- echo "$default_heading"
- echo
- fi
- if [[ -f "$md_file" ]]; then
- cat "$md_file"
- echo
- fi
- if [[ -f functions.md ]]; then
- cat functions.md
- fi
- ) > "$out_file"
- }
-
- mkdir -p "$out"
-
- ${lib.concatMapStringsSep "\n" (
+ result =
+ runCommand "nixvim-lib-docs"
{
- name,
- file,
- markdown,
- outFile,
- title ? "",
- ...
- }:
- lib.escapeShellArgs [
- "docgen"
- "${lib.optionalString (markdown != null) markdown}" # md_file
- "${lib.optionalString (file != null) file}" # in_file
- name # name
- outFile # out_file
- title # title
- ]
- ) pagesToRender}
- ''
+ nativeBuildInputs = [
+ nixdoc
+ ];
+
+ locations = writers.writeJSON "locations.json" (
+ import ./function-locations.nix {
+ inherit lib;
+ rootPath = nixvim;
+ functionSet = lib.extend nixvim.lib.overlay;
+ pathsToScan = lib.pipe pageList [
+ (map (x: x.functions))
+ (builtins.filter (x: x.file != null))
+ (map (x: x.loc))
+ ];
+ revision = nixvim.rev or "main";
+ }
+ );
+
+ passthru.config = pageConfiguration;
+
+ passthru.menu = import ./menu.nix {
+ inherit lib pages;
+ };
+
+ passthru.pages = map (page: "${result}/${page.target}") pagesToRender;
+ }
+ ''
+ function docgen {
+ md_file="$1"
+ in_file="$2"
+ name="$3"
+ out_file="$out/$4"
+ title="$5"
+
+ if [[ -z "$in_file" ]]; then
+ if [[ -z "$md_file" ]]; then
+ >&2 echo "No markdown or nix file for $name"
+ exit 1
+ fi
+ elif [[ -f "$in_file/default.nix" ]]; then
+ in_file+="/default.nix"
+ elif [[ ! -f "$in_file" ]]; then
+ >&2 echo "File not found: $in_file"
+ exit 1
+ fi
+
+ if [[ -n "$in_file" ]]; then
+ nixdoc \
+ --file "$in_file" \
+ --locs "$locations" \
+ --category "$name" \
+ --description "REMOVED BY TAIL" \
+ --prefix "lib" \
+ --anchor-prefix "" \
+ | tail --lines +2 \
+ > functions.md
+ fi
+
+ default_heading="# $name"
+ if [[ -n "$title" ]]; then
+ default_heading+=": $title"
+ fi
+
+ print_heading=true
+ if [[ -f "$md_file" ]] && [[ "$(head --lines 1 "$md_file")" == '# '* ]]; then
+ >&2 echo "NOTE: markdown file for $name starts with a heading. Skipping default heading \"$default_heading\"."
+ >&2 echo " Found \"$(head --lines 1 "$md_file")\" in: $md_file"
+ print_heading=false
+ fi
+
+ mkdir -p $(dirname "$out_file")
+ (
+ if [[ "$print_heading" = true ]]; then
+ echo "$default_heading"
+ echo
+ fi
+ if [[ -f "$md_file" ]]; then
+ cat "$md_file"
+ echo
+ fi
+ if [[ -f functions.md ]]; then
+ cat functions.md
+ fi
+ ) > "$out_file"
+ }
+
+ mkdir -p "$out"
+
+ ${lib.concatMapStringsSep "\n" (
+ {
+ functions,
+ source,
+ target,
+ title ? "",
+ ...
+ }:
+ lib.escapeShellArgs [
+ "docgen"
+ "${lib.optionalString (source != null) source}" # md_file
+ "${lib.optionalString (functions.file != null) functions.file}" # in_file
+ (lib.showAttrPath functions.loc) # name
+ target # out_file
+ title # title
+ ]
+ ) pagesToRender}
+ '';
+in
+result
diff --git a/docs/lib/menu.nix b/docs/lib/menu.nix
index 87feb9c9..cd6eb954 100644
--- a/docs/lib/menu.nix
+++ b/docs/lib/menu.nix
@@ -1,31 +1,31 @@
{
lib,
- pageSpecs,
+ pages,
indentSize ? " ",
}:
let
pageToLines =
- indent: parentName:
- {
- name,
- outFile ? "",
- pages ? { },
- ...
- }:
+ indent: parent: node:
let
- menuName = lib.strings.removePrefix (parentName + ".") name;
- children = builtins.attrValues pages;
+
+ children = lib.pipe node [
+ (lib.flip builtins.removeAttrs [ "_page" ])
+ builtins.attrValues
+ ];
# Only add node to the menu if it has content or multiple children
- useNodeInMenu = outFile != "" || builtins.length children > 1;
- parentOfChildren = if useNodeInMenu then name else parentName;
+ useNodeInMenu = node._page.target != "" || node._page.children > 1;
+ nextParent = if useNodeInMenu then node else parent;
+ nextIndent = if useNodeInMenu then indent + indentSize else indent;
+ loc = lib.lists.removePrefix (parent._page.loc or [ ]) node._page.loc;
+ menuName = lib.attrsets.showAttrPath loc;
in
- lib.optional useNodeInMenu "${indent}- [${menuName}](${outFile})"
+ lib.optional useNodeInMenu "${indent}- [${menuName}](${node._page.target})"
++ lib.optionals (children != [ ]) (
- builtins.concatMap (pageToLines (indent + indentSize) parentOfChildren) children
+ builtins.concatMap (pageToLines nextIndent nextParent) children
);
in
-lib.pipe pageSpecs [
+lib.pipe pages [
builtins.attrValues
- (builtins.concatMap (pageToLines "" ""))
+ (builtins.concatMap (pageToLines "" null))
lib.concatLines
]
diff --git a/docs/lib/pages.nix b/docs/lib/pages.nix
index f519339e..b5e42f82 100644
--- a/docs/lib/pages.nix
+++ b/docs/lib/pages.nix
@@ -4,21 +4,19 @@
# If there is an issue parsing the file, the resulting markdown will not contain any function docs.
{
- lib.pages = {
- nixvim = {
+ lib.nixvim = {
+ _page = {
title = "Nixvim's functions";
- markdown = ./index.md;
+ source = ./index.md;
+ };
- pages = {
- utils = {
- file = ../../lib/utils.nix;
- title = "utility functions";
- };
- lua = {
- file = ../../lib/to-lua.nix;
- title = "lua functions";
- };
- };
+ utils._page = {
+ title = "utility functions";
+ functions.file = ../../lib/utils.nix;
+ };
+ lua._page = {
+ title = "lua functions";
+ functions.file = ../../lib/to-lua.nix;
};
};
}
diff --git a/docs/man/default.nix b/docs/man/default.nix
index 92e69cde..96f19312 100644
--- a/docs/man/default.nix
+++ b/docs/man/default.nix
@@ -12,7 +12,8 @@ let
../user-guide/faq.md
../user-guide/config-examples.md
]
- ++ lib.mapAttrsToList (name: file: "${lib-docs}/${file}") lib-docs.pages;
+ ++ lib-docs.pages;
+
manHeader =
runCommand "nixvim-general-doc-manpage"
{
diff --git a/docs/modules/page-options.nix b/docs/modules/page-options.nix
new file mode 100644
index 00000000..942c9be9
--- /dev/null
+++ b/docs/modules/page-options.nix
@@ -0,0 +1,104 @@
+{
+ lib,
+ prefix,
+ name,
+ config,
+ options,
+ ...
+}:
+let
+ cfg = config._page;
+ opts = options._page;
+in
+{
+ options._page = {
+ loc = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ description = "Page's location in the menu.";
+ default = prefix ++ [ name ];
+ defaultText = lib.literalExpression "prefix ++ [ name ]";
+ readOnly = true;
+ };
+ target = lib.mkOption {
+ type = lib.types.str;
+ default = lib.optionalString cfg.hasContent (lib.concatStringsSep "/" (cfg.loc ++ [ "index.md" ]));
+ defaultText = lib.literalMD ''
+ `""` if page has no content, otherwise a filepath derived from the page's `loc`.
+ '';
+ description = "Where to render content and link menu entries.";
+ };
+ title = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Page's heading title.";
+ };
+ text = lib.mkOption {
+ type = lib.types.nullOr lib.types.lines;
+ default = null;
+ description = "Optional markdown text to include after the title.";
+ };
+ source = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = "Optional markdown file to include after the title.";
+ };
+ functions.file = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = "Optional nix file to scan for RFC145 doc comments.";
+ };
+ functions.loc = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = lib.lists.removePrefix [ "lib" ] cfg.loc;
+ defaultText = lib.literalMD ''
+ `loc`'s attrpath, without any leading "lib"
+ '';
+ description = ''
+ Optional attrpath where functions are defined.
+ Provided to `nixdoc` as `--category`.
+
+ Will scan `lib` for attribute locations in the functions set at this attrpath.
+
+ Used in conjunction with `nix`.
+ '';
+ };
+ options = lib.mkOption {
+ type = lib.types.nullOr lib.types.raw;
+ default = null;
+ apply = opts: if builtins.isAttrs opts then lib.options.optionAttrSetToDocList opts else opts;
+ description = ''
+ Optional set of options or list of option docs-templates.
+
+ If an attrset is provided, it will be coerced using `lib.options.optionAttrSetToDocList`.
+ '';
+ };
+ children = lib.mkOption {
+ type = lib.types.ints.unsigned;
+ description = ''
+ The number of child pages.
+ '';
+ readOnly = true;
+ };
+ hasContent = lib.mkOption {
+ type = lib.types.bool;
+ description = ''
+ Whether this page has any docs content.
+
+ When `false`, this page represents an _empty_ menu entry.
+ '';
+ readOnly = true;
+ };
+ };
+
+ config._page = {
+ source = lib.mkIf (cfg.text != null) (
+ lib.mkDerivedConfig opts.text (builtins.toFile "docs-${lib.attrsets.showAttrPath cfg.loc}-text.md")
+ );
+
+ hasContent = builtins.any (x: x != null) [
+ cfg.source # markdown
+ cfg.functions.file # doc-comments
+ cfg.options # module options
+ ];
+ };
+}
diff --git a/docs/modules/page.nix b/docs/modules/page.nix
new file mode 100644
index 00000000..1224eaa3
--- /dev/null
+++ b/docs/modules/page.nix
@@ -0,0 +1,45 @@
+# This module represents a node in a tree of pages.
+# Its freeformType is is recursive: attrs of another node submodule.
+{
+ lib,
+ prefix,
+ name,
+ config,
+ options,
+ ...
+}:
+{
+ freeformType = lib.types.attrsOf (
+ lib.types.submoduleWith {
+ specialArgs.prefix = prefix ++ [ name ];
+ modules = [ ./page.nix ];
+ }
+ // {
+ description = "page submodule";
+ descriptionClass = "noun";
+ # Alternative to `visible = "shallow"`, avoid inf-recursion when collecting options for docs
+ getSubOptions = _: { };
+ }
+ );
+
+ # The _page option contains options for this page node
+ imports = [
+ ./page-options.nix
+ ];
+
+ config = {
+ # Ensure the `prefix` arg exists
+ # Usually shadowed by `specialArgs.prefix`
+ _module.args.prefix = [ ];
+
+ _page = {
+ # Freeform definitions are children; count definitions without a
+ # corresponding option
+ children = lib.pipe config [
+ builtins.attrNames
+ (lib.count (name: !(options ? ${name})))
+ lib.mkForce
+ ];
+ };
+ };
+}