From 4414d8aa142a7737e73c63cad7d802352dda9109 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Tue, 20 May 2025 04:16:40 +0100 Subject: [PATCH] docs/modules: init Modules to represent pages in the docs --- docs/lib/default.nix | 288 ++++++++++++++++------------------ docs/lib/menu.nix | 32 ++-- docs/lib/pages.nix | 24 ++- docs/man/default.nix | 3 +- docs/modules/page-options.nix | 104 ++++++++++++ docs/modules/page.nix | 45 ++++++ 6 files changed, 309 insertions(+), 187 deletions(-) create mode 100644 docs/modules/page-options.nix create mode 100644 docs/modules/page.nix 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 + ]; + }; + }; +}