diff --git a/docs/default.nix b/docs/default.nix index bbb94d88..43cd9023 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -1,6 +1,7 @@ { helpers, system, + nixvim, nixpkgs, nuschtosSearch, }: @@ -108,6 +109,10 @@ lib.fix ( gfm-alerts-to-admonitions = pkgs.python3.pkgs.callPackage ./gfm-alerts-to-admonitions { }; man-docs = pkgs.callPackage ./man { inherit options-json; }; + + lib-docs = pkgs.callPackage ./lib { + inherit nixvim lib; + }; } // lib.optionalAttrs (!pkgs.stdenv.isDarwin) { # NuschtOS/search does not seem to work on darwin diff --git a/docs/lib/default.nix b/docs/lib/default.nix new file mode 100644 index 00000000..eeaa43e7 --- /dev/null +++ b/docs/lib/default.nix @@ -0,0 +1,168 @@ +# Generates the documentation for library functions using nixdoc. +# See https://github.com/nix-community/nixdoc +{ + lib, + runCommand, + writers, + nixdoc, + nixvim, + pageSpecs ? import ./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 ])); + + # 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) + ) (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 + +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; + }; + } + '' + 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" ( + { + 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} + '' diff --git a/docs/lib/function-locations.nix b/docs/lib/function-locations.nix new file mode 100644 index 00000000..e232d514 --- /dev/null +++ b/docs/lib/function-locations.nix @@ -0,0 +1,85 @@ +# Generates an attrset of "function name" → "markdown location", +# for use with nixdoc's `--locs` option. +# +# { +# "lib.nixvim.foo.bar" = "[lib/foo.nix:123](https://github.com/nix-community/nixvim/blob/«rev»/lib/foo.nix#L123) in ``"; +# } +{ + rootPath, + lib, + functionSet, + pathsToScan, + revision, + functionSetName ? "lib", + url ? "https://github.com/nix-community/nixvim/blob", +}: +let + rootPathString = toString rootPath; + urlPrefix = "${url}/${revision}"; + + sanitizeId = builtins.replaceStrings [ "'" ] [ "-prime" ]; + + # Like `isAttrs`, but returns `false` if `v` throws + tryIsAttrs = v: (builtins.tryEval (builtins.isAttrs v)).value; + + # Collect position entries from an attrset + # `prefix` is used in the human-readable name, + # and for determining whether to recurse into attrs + collectPositionEntriesInSet = + prefix: set: + builtins.concatMap ( + name: + [ + { + name = lib.showAttrPath ( + builtins.concatMap lib.toList [ + functionSetName + prefix + name + ] + ); + location = builtins.unsafeGetAttrPos name set; + } + ] + ++ lib.optionals (prefix == [ ] && tryIsAttrs set.${name}) ( + collectPositionEntriesInSet (prefix ++ [ name ]) set.${name} + ) + ) (builtins.attrNames set); + + # Collect position entries from each `pathsToScan` in `set` + collectPositionEntriesFromPaths = + set: + builtins.concatMap (loc: collectPositionEntriesInSet loc (lib.getAttrFromPath loc set)) pathsToScan; + + # Remove the tree root (usually the top-level store path) + removeNixvimPrefix = lib.flip lib.pipe [ + (lib.strings.removePrefix rootPathString) + (lib.strings.removePrefix "/") + ]; + + # Create a name-value-pair for use with `listToAttrs` + entryToNameValuePair = + { name, location }: + { + name = sanitizeId name; + value = + let + file = removeNixvimPrefix location.file; + line = builtins.toString location.line; + text = "${file}:${line}"; + target = "${urlPrefix}/${file}#L${line}"; + in + "[${text}](${target}) in ``"; + }; +in +lib.pipe functionSet [ + # Get the entries + collectPositionEntriesFromPaths + # Only include entries that have a location + (builtins.filter (entry: entry.location != null)) + # No need to include out-of-tree entries + (builtins.filter (entry: lib.strings.hasPrefix rootPathString entry.location.file)) + # Convert entries to attrset + (builtins.map entryToNameValuePair) + builtins.listToAttrs +] diff --git a/docs/lib/menu.nix b/docs/lib/menu.nix new file mode 100644 index 00000000..87feb9c9 --- /dev/null +++ b/docs/lib/menu.nix @@ -0,0 +1,31 @@ +{ + lib, + pageSpecs, + indentSize ? " ", +}: +let + pageToLines = + indent: parentName: + { + name, + outFile ? "", + pages ? { }, + ... + }: + let + menuName = lib.strings.removePrefix (parentName + ".") name; + children = builtins.attrValues pages; + # 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; + in + lib.optional useNodeInMenu "${indent}- [${menuName}](${outFile})" + ++ lib.optionals (children != [ ]) ( + builtins.concatMap (pageToLines (indent + indentSize) parentOfChildren) children + ); +in +lib.pipe pageSpecs [ + builtins.attrValues + (builtins.concatMap (pageToLines "" "")) + lib.concatLines +] diff --git a/docs/lib/pages.nix b/docs/lib/pages.nix new file mode 100644 index 00000000..dba6ff08 --- /dev/null +++ b/docs/lib/pages.nix @@ -0,0 +1,8 @@ +# This file contains a list of function reference docs pages that should be generated by `nixdoc` +# +# Note: `nixdoc` uses `rnix` to parse the input file, so the file must return a fairly simple attrset. +# If there is an issue parsing the file, the resulting markdown will not contain any function docs. + +{ + # TODO +} diff --git a/flake/packages.nix b/flake/packages.nix index a9ba410a..03451350 100644 --- a/flake/packages.nix +++ b/flake/packages.nix @@ -1,4 +1,5 @@ { + self, inputs, helpers, ... @@ -12,6 +13,7 @@ }: { packages = import ../docs { + nixvim = self; inherit helpers; inherit system; inherit (inputs) nixpkgs;