diff --git a/docs/lib/default.nix b/docs/lib/default.nix index fb37706f0d..3690b50dab 100644 --- a/docs/lib/default.nix +++ b/docs/lib/default.nix @@ -6,173 +6,137 @@ 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 ])); + menuConfiguration = lib.evalModules { + modules = [ + pageSpecs + ../modules/menu.nix + ]; + }; + cfg = menuConfiguration.config; + pages = cfg.functions; # 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); + node: + let + children = builtins.removeAttrs node [ "_page" ]; + in + lib.optional (node ? _page) node._page ++ lib.optionals (children != { }) (collectPages children) + ) (builtins.attrValues (builtins.removeAttrs pages [ "_category" ])); # 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"; + result = + 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 = lib.pipe pageList [ + (map (x: x.functions)) + (builtins.filter (x: x.file != null)) + (map (x: x.loc)) + ]; + revision = nixvim.rev or "main"; + } + ); + + passthru.config = menuConfiguration; + + passthru.menu = cfg._menu.text; + + passthru.pages = map (page: "${result}/${page.target}") pagesToRender; } - ); - - passthru.menu = import ./menu.nix { - inherit lib; - pageSpecs = elaboratedPageSpecs; - }; - - passthru.pages = builtins.listToAttrs ( - builtins.map ( - { name, outFile, ... }: - { - inherit name; - value = outFile; + '' + function docgen { + md_file="$1" + in_file="$2" + category="$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 $category" + 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 "$category" \ + --description "REMOVED BY TAIL" \ + --prefix "lib" \ + --anchor-prefix "" \ + | tail --lines +2 \ + > functions.md + fi + + print_title=true + if [[ -f "$md_file" ]] && [[ "$(head --lines 1 "$md_file")" == '# '* ]]; then + if [[ -n "$title" ]]; then + >&2 echo "NOTE: markdown file for $category starts with a

heading. Skipping title \"$title\"." + >&2 echo " Found \"$(head --lines 1 "$md_file")\" in: $md_file" + fi + print_title=false + fi + + mkdir -p $(dirname "$out_file") + ( + if [[ "$print_title" = true ]]; then + echo "# $title" + echo + fi + if [[ -f "$md_file" ]]; then + cat "$md_file" + echo + fi + if [[ -f functions.md ]]; then + cat functions.md + fi + ) > "$out_file" } - ) 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" ( - { - 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} - '' + 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) # category + target # out_file + title # title + ] + ) pagesToRender} + ''; +in +result diff --git a/docs/lib/menu.nix b/docs/lib/menu.nix deleted file mode 100644 index 87feb9c9cd..0000000000 --- a/docs/lib/menu.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - 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 index f519339e38..c9cc3625a4 100644 --- a/docs/lib/pages.nix +++ b/docs/lib/pages.nix @@ -4,20 +4,22 @@ # If there is an issue parsing the file, the resulting markdown will not contain any function docs. { - lib.pages = { - nixvim = { - title = "Nixvim's functions"; - markdown = ./index.md; + functions = { + _category.name = "Functions"; - pages = { - utils = { - file = ../../lib/utils.nix; - title = "utility functions"; - }; - lua = { - file = ../../lib/to-lua.nix; - title = "lua functions"; - }; + lib.nixvim = { + _page = { + title = "lib.nixvim: Nixvim's functions"; + source = ./index.md; + }; + + utils._page = { + title = "lib.nixvim.utils: utility functions"; + functions.file = ../../lib/utils.nix; + }; + lua._page = { + title = "lib.nixvim.lua: lua functions"; + functions.file = ../../lib/to-lua.nix; }; }; }; diff --git a/docs/man/default.nix b/docs/man/default.nix index 92e69cde94..96f19312d1 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/mdbook/SUMMARY.md b/docs/mdbook/SUMMARY.md index 77ecb37b4e..0b1223c198 100644 --- a/docs/mdbook/SUMMARY.md +++ b/docs/mdbook/SUMMARY.md @@ -9,8 +9,6 @@ - [Configuration examples](./user-guide/config-examples.md) - [Lazy Loading](./user-guide/lazy-loading.md) -# Functions - @FUNCTIONS_MENU@ # Platforms diff --git a/docs/modules/category.nix b/docs/modules/category.nix new file mode 100644 index 0000000000..749a29afef --- /dev/null +++ b/docs/modules/category.nix @@ -0,0 +1,89 @@ +{ + lib, + name, + config, + options, + ... +}: +let + cfg = config._category; + + pageType = lib.types.submoduleWith { + modules = [ ./page.nix ]; + }; + + pages = builtins.removeAttrs config (builtins.attrNames options); +in +{ + freeformType = lib.types.attrsOf pageType; + + options._category = { + name = lib.mkOption { + type = lib.types.str; + default = name; + defaultText = lib.literalMD "attribute name"; + }; + + order = lib.mkOption { + type = lib.types.int; + default = 100; + description = "Priority for where this category will appear in the menu."; + }; + + type = lib.mkOption { + type = lib.types.enum [ + "prefix" + "normal" + "suffix" + ]; + default = "normal"; + description = '' + The kind of mdbook chapters this category contains. + + **Prefix Chapter** + : Before the main numbered chapters, prefix chapters can be added that + will not be numbered. This is useful for forewords, introductions, etc. + There are, however, some constraints. + Prefix chapters cannot be nested; they should all be on the root level. + And you cannot add prefix chapters once you have added numbered chapters. + + **Normal Chapter** + : Called a "Numbered Chapter" in the MDBook docs. + Numbered chapters outline the main content of the book and can be + nested, resulting in a nice hierarchy (chapters, sub-chapters, etc.). + + **Suffix Chapter** + : Like prefix chapters, suffix chapters are unnumbered, but they come + after numbered chapters. + + See . + ''; + }; + + text = lib.mkOption { + type = lib.types.str; + description = "The rendered menu."; + readOnly = true; + }; + }; + + config._category = { + text = lib.optionalString (pages != { }) '' + # ${cfg.name} + + ${lib.pipe pages [ + builtins.attrValues + (map ( + page: + page._page.toMenu { + nested = cfg.type == "normal"; + indent = ""; + prefix = [ ]; + inherit page; + } + )) + (builtins.concatStringsSep "\n") + ]} + ''; + }; +} diff --git a/docs/modules/menu.nix b/docs/modules/menu.nix new file mode 100644 index 0000000000..eaa90d6be6 --- /dev/null +++ b/docs/modules/menu.nix @@ -0,0 +1,43 @@ +{ + lib, + config, + options, + ... +}: +let + categoryType = lib.types.submoduleWith { + modules = [ ./category.nix ]; + }; + + categories = builtins.removeAttrs config (builtins.attrNames options); +in +{ + freeformType = lib.types.attrsOf categoryType; + + options._menu = { + text = lib.mkOption { + type = lib.types.str; + description = "The rendered menu."; + readOnly = true; + }; + }; + + config._menu = { + text = lib.pipe categories [ + builtins.attrValues + (map (x: x._category)) + (lib.sortOn (x: x.order)) + (builtins.groupBy (x: x.type)) + ( + { + prefix ? [ ], + normal ? [ ], + suffix ? [ ], + }: + prefix ++ normal ++ suffix + ) + (map (x: x.text)) + (builtins.concatStringsSep "\n\n") + ]; + }; +} diff --git a/docs/modules/page-options.nix b/docs/modules/page-options.nix new file mode 100644 index 0000000000..6300cc057c --- /dev/null +++ b/docs/modules/page-options.nix @@ -0,0 +1,133 @@ +{ + 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 = if lib.lists.hasPrefix [ "lib" ] cfg.loc then builtins.tail cfg.loc else 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`. + ''; + }; + toMenu = lib.mkOption { + type = lib.types.functionTo lib.types.str; + description = '' + A function to render the menu for this sub-tree. + + Typically, this involves invoking `_page.toMenu` for all children. + + **Inputs** + + `settings` + : `nested` + : Whether this menu category supports nesting. + + `indent` + : The indentation to use before non-empty lines. + + `page` + : This page node. + + `prefix` + : The menu loc prefix, to be omitted from menu entry text. + Usually the `loc` of the parent page node. + ''; + }; + 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 + ]; + + toMenu = import ./to-menu.nix { + inherit lib; + optionNames = builtins.attrNames options; + }; + }; +} diff --git a/docs/modules/page.nix b/docs/modules/page.nix new file mode 100644 index 0000000000..1224eaa3e3 --- /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 + ]; + }; + }; +} diff --git a/docs/modules/to-menu.nix b/docs/modules/to-menu.nix new file mode 100644 index 0000000000..f0cab7f74a --- /dev/null +++ b/docs/modules/to-menu.nix @@ -0,0 +1,43 @@ +{ + lib, + optionNames, +}: +/** + The default `toMenu` function renders a page node into a menu subtree. +*/ +{ + page, + prefix ? [ ], + indent ? "", + nested ? true, +}: +let + inherit (page._page) loc target; + count = page._page.children; + + # Only add node to the menu if it has content or multiple children + showInMenu = target != "" || count > 1; + nextPrefix = if showInMenu then loc else prefix; + nextIndent = if showInMenu && nested then indent + " " else indent; + + children = builtins.removeAttrs page optionNames; + submenu = lib.pipe children [ + builtins.attrValues + (map ( + subpage: + page._page.toMenu { + inherit nested; + page = subpage; + indent = nextIndent; + prefix = nextPrefix; + } + )) + ]; + + loc' = if lib.lists.hasPrefix prefix loc then lib.lists.drop (builtins.length prefix) loc else loc; + menuText = lib.attrsets.showAttrPath loc'; + menuitem = lib.optionals showInMenu [ + (indent + lib.optionalString nested "- " + "[${menuText}](${target})") + ]; +in +builtins.concatStringsSep "\n" (menuitem ++ submenu)