From 4f03ca05d9add2217554c4706bac84559f1211c6 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Tue, 23 Sep 2025 18:25:39 +0100 Subject: [PATCH] docs/lib: generalise `menu` impl using module system Move the mdbook menu rendering code into the module system and generalise it to apply to multiple "categories" (mdbook parts) and "types" of category (prefix, suffix, etc). --- docs/lib/default.nix | 21 +++------ docs/lib/menu.nix | 31 ------------ docs/lib/pages.nix | 28 ++++++----- docs/mdbook/SUMMARY.md | 2 - docs/modules/category.nix | 89 +++++++++++++++++++++++++++++++++++ docs/modules/menu.nix | 43 +++++++++++++++++ docs/modules/page-options.nix | 31 +++++++++++- docs/modules/to-menu.nix | 43 +++++++++++++++++ 8 files changed, 228 insertions(+), 60 deletions(-) delete mode 100644 docs/lib/menu.nix create mode 100644 docs/modules/category.nix create mode 100644 docs/modules/menu.nix create mode 100644 docs/modules/to-menu.nix diff --git a/docs/lib/default.nix b/docs/lib/default.nix index 8659cad6..3690b50d 100644 --- a/docs/lib/default.nix +++ b/docs/lib/default.nix @@ -10,19 +10,14 @@ }: let - pageConfiguration = lib.evalModules { + menuConfiguration = lib.evalModules { modules = [ pageSpecs - { - freeformType = lib.types.attrsOf ( - lib.types.submoduleWith { - modules = [ ../modules/page.nix ]; - } - ); - } + ../modules/menu.nix ]; }; - pages = pageConfiguration.config; + cfg = menuConfiguration.config; + pages = cfg.functions; # Collect all page nodes into a list of page entries collectPages = @@ -33,7 +28,7 @@ let children = builtins.removeAttrs node [ "_page" ]; in lib.optional (node ? _page) node._page ++ lib.optionals (children != { }) (collectPages children) - ) (builtins.attrValues pages); + ) (builtins.attrValues (builtins.removeAttrs pages [ "_category" ])); # Normalised page specs pageList = collectPages pages; @@ -60,11 +55,9 @@ let } ); - passthru.config = pageConfiguration; + passthru.config = menuConfiguration; - passthru.menu = import ./menu.nix { - inherit lib pages; - }; + passthru.menu = cfg._menu.text; passthru.pages = map (page: "${result}/${page.target}") pagesToRender; } diff --git a/docs/lib/menu.nix b/docs/lib/menu.nix deleted file mode 100644 index cd6eb954..00000000 --- a/docs/lib/menu.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - lib, - pages, - indentSize ? " ", -}: -let - pageToLines = - indent: parent: node: - let - - 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 = 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}](${node._page.target})" - ++ lib.optionals (children != [ ]) ( - builtins.concatMap (pageToLines nextIndent nextParent) children - ); -in -lib.pipe pages [ - builtins.attrValues - (builtins.concatMap (pageToLines "" null)) - lib.concatLines -] diff --git a/docs/lib/pages.nix b/docs/lib/pages.nix index deeb9051..c9cc3625 100644 --- a/docs/lib/pages.nix +++ b/docs/lib/pages.nix @@ -4,19 +4,23 @@ # If there is an issue parsing the file, the resulting markdown will not contain any function docs. { - lib.nixvim = { - _page = { - title = "lib.nixvim: Nixvim's functions"; - source = ./index.md; - }; + functions = { + _category.name = "Functions"; - 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; + 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/mdbook/SUMMARY.md b/docs/mdbook/SUMMARY.md index 77ecb37b..0b1223c1 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 00000000..749a29af --- /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 00000000..eaa90d6b --- /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 index 942c9be9..6300cc05 100644 --- a/docs/modules/page-options.nix +++ b/docs/modules/page-options.nix @@ -49,7 +49,7 @@ in }; functions.loc = lib.mkOption { type = lib.types.listOf lib.types.str; - default = lib.lists.removePrefix [ "lib" ] cfg.loc; + 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" ''; @@ -72,6 +72,30 @@ in 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 = '' @@ -100,5 +124,10 @@ in 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/to-menu.nix b/docs/modules/to-menu.nix new file mode 100644 index 00000000..f0cab7f7 --- /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)