From b9aec4fe76c833c80195e67c6ceffe0bdc89df75 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 29 Jan 2025 19:55:49 +0000 Subject: [PATCH 01/10] docs/noPkgs: permit access to `callPackage` & `formats` --- docs/default.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/default.nix b/docs/default.nix index bbb94d88cc..3076674895 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -49,11 +49,13 @@ let # The following pkgs attrs are required to eval nixvim, even for the docs: inherit (pkgs) _type + callPackage + formats + runCommand + runCommandLocal stdenv stdenvNoCC symlinkJoin - runCommand - runCommandLocal writeShellApplication ; } From 6c42d5afc3e4575e198d37f4f9f55fdf713926df Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Thu, 23 Jan 2025 15:50:24 +0000 Subject: [PATCH 02/10] modules/docs: move into its own directory --- modules/default.nix | 2 +- modules/{doc.nix => docs/default.nix} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename modules/{doc.nix => docs/default.nix} (100%) diff --git a/modules/default.nix b/modules/default.nix index 49c2ddf619..e0d65bba0e 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -4,13 +4,13 @@ # using this in a submodule nested within another nixvim config. { imports = [ + ./docs ./misc ./autocmd.nix ./clipboard.nix ./colorscheme.nix ./commands.nix ./diagnostics.nix - ./doc.nix ./editorconfig.nix ./files.nix ./filetype.nix diff --git a/modules/doc.nix b/modules/docs/default.nix similarity index 100% rename from modules/doc.nix rename to modules/docs/default.nix From 96d8873d1c0fe06b1c1e8497c5c02581b786d065 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Thu, 23 Jan 2025 14:12:36 +0000 Subject: [PATCH 03/10] modules/docs: construct docs from `docs.*` options Introduce various `docs.*` options where doc pages, menu entries, etc can be defined. The options themselves are responsible for rendering this to markdown and HTML. --- modules/docs/collect-sources.nix | 27 +++++ modules/docs/default.nix | 28 ++++- modules/docs/mdbook/default.nix | 50 ++++++++ modules/docs/mdbook/package.nix | 33 ++++++ modules/docs/menu/default.nix | 28 +++++ modules/docs/menu/sections.nix | 195 +++++++++++++++++++++++++++++++ modules/docs/pages.nix | 132 +++++++++++++++++++++ modules/docs/readme.nix | 69 +++++++++++ 8 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 modules/docs/collect-sources.nix create mode 100644 modules/docs/mdbook/default.nix create mode 100644 modules/docs/mdbook/package.nix create mode 100644 modules/docs/menu/default.nix create mode 100644 modules/docs/menu/sections.nix create mode 100644 modules/docs/pages.nix create mode 100644 modules/docs/readme.nix diff --git a/modules/docs/collect-sources.nix b/modules/docs/collect-sources.nix new file mode 100644 index 0000000000..1f1dd0fbc2 --- /dev/null +++ b/modules/docs/collect-sources.nix @@ -0,0 +1,27 @@ +{ + lib, + runCommandLocal, + pages, +}: +# Implementation based on NixOS's /etc module +runCommandLocal "docs-sources" { } '' + set -euo pipefail + + makeEntry() { + src="$1" + target="$2" + mkdir -p "$out/$(dirname "$target")" + cp "$src" "$out/$target" + } + + mkdir -p "$out" + ${lib.concatMapStringsSep "\n" ( + { target, source, ... }: + lib.escapeShellArgs [ + "makeEntry" + # Force local source paths to be added to the store + "${source}" + target + ] + ) pages} +'' diff --git a/modules/docs/default.nix b/modules/docs/default.nix index b4fb42734f..9d60fa2409 100644 --- a/modules/docs/default.nix +++ b/modules/docs/default.nix @@ -1,8 +1,34 @@ -{ lib, ... }: +{ lib, pkgs, ... }: +let + # Convert links relative to github -> relative to docs + fixLinks = pkgs.callPackage ../../docs/fix-links { }; +in { options.enableMan = lib.mkOption { type = lib.types.bool; default = true; description = "Install the man pages for NixVim options."; }; + + imports = [ + ./mdbook + ./menu + ./pages.nix + ]; + + config.docs = { + pages."" = { + menu.section = "header"; + menu.location = [ "Home" ]; + source = pkgs.callPackage ./readme.nix { + inherit fixLinks; + # TODO: get `availableVersions` and `baseHref` from module options + }; + }; + pages.contributing = { + menu.section = "footer"; + menu.location = [ "Contributing" ]; + source = fixLinks ../../CONTRIBUTING.md; + }; + }; } diff --git a/modules/docs/mdbook/default.nix b/modules/docs/mdbook/default.nix new file mode 100644 index 0000000000..f43ad3dcda --- /dev/null +++ b/modules/docs/mdbook/default.nix @@ -0,0 +1,50 @@ +{ + lib, + config, + pkgs, + ... +}: +let + settingsFormat = pkgs.formats.toml { }; + defaultSettings = { + book = { + language = "en"; + multilingual = false; + title = "nixvim docs"; + }; + build.create-missing = false; + output.html.site-url = "/"; + output.html.fold = { + enable = true; + level = 0; + }; + preprocessor.alerts = { }; + }; +in +{ + options.docs.html = { + site = lib.mkOption { + type = lib.types.package; + description = "HTML docs rendered by mdbook."; + readOnly = true; + }; + settings = lib.mkOption { + inherit (settingsFormat) type; + description = '' + Freeform settings written to `book.toml`. + + See MDBook's [Configuration](https://rust-lang.github.io/mdBook/format/configuration/index.html) docs. + ''; + defaultText = defaultSettings; + }; + }; + config.docs.html = { + site = pkgs.callPackage ./package.nix { + inherit (config.docs) src; + inherit (config.docs.html) settings; + menu = config.docs.menu.src; + writeTOML = settingsFormat.generate; + }; + settings = defaultSettings; + }; +} diff --git a/modules/docs/mdbook/package.nix b/modules/docs/mdbook/package.nix new file mode 100644 index 0000000000..66d054d981 --- /dev/null +++ b/modules/docs/mdbook/package.nix @@ -0,0 +1,33 @@ +{ + mdbook, + mdbook-alerts, + runCommand, + writeTOML, + menu, + settings, + src, +}: +runCommand "html-docs" + { + inherit src; + + nativeBuildInputs = [ + mdbook + mdbook-alerts + ]; + + settings = writeTOML "book.toml" settings; + menu = builtins.toFile "menu.md" (builtins.unsafeDiscardStringContext menu); + } + '' + mkdir src + for input in $src/*; do + name=$(basename "$input") + ln -s "$input" "src/$name" + done + ln -s $settings book.toml + ln -s $menu src/SUMMARY.md + + # Build the website + mdbook build --dest-dir $out + '' diff --git a/modules/docs/menu/default.nix b/modules/docs/menu/default.nix new file mode 100644 index 0000000000..6b97031572 --- /dev/null +++ b/modules/docs/menu/default.nix @@ -0,0 +1,28 @@ +{ + lib, + config, + ... +}: +{ + imports = [ + ./sections.nix + ]; + + options.docs.menu.src = lib.mkOption { + type = lib.types.lines; + description = '' + MDBook SUMMARY menu. Generated from `docs.pages..menu`. + + See MDBook's [SUMMARY.md](https://rust-lang.github.io/mdBook/format/summary.html) docs. + ''; + readOnly = true; + }; + + config.docs.menu.src = lib.pipe config.docs.menu.sections [ + builtins.attrValues + (lib.sortOn (section: section.order)) + (builtins.catAttrs "text") + (builtins.filter (text: text != null)) + lib.mkMerge + ]; +} diff --git a/modules/docs/menu/sections.nix b/modules/docs/menu/sections.nix new file mode 100644 index 0000000000..1a985f37b7 --- /dev/null +++ b/modules/docs/menu/sections.nix @@ -0,0 +1,195 @@ +{ + lib, + config, + ... +}: +let + # A set of menu sections, each mapped to a list of pages + # { section = [page]; } + pagesBySection = builtins.groupBy (page: page.menu.section) (builtins.attrValues config.docs.pages); + + # Converts a list of pages into a tree that defines the shape of the menu + mkPagesTree = + let + # Produce a list of page nodes + go = + nodes: pages: + if pages == [ ] then + nodes + else + let + page = builtins.head pages; + remaining = lib.drop 1 pages; + prefix = page.menu.location; + + # Partition the remaining pages by whether they are children of this page + inherit (builtins.partition (p: lib.lists.hasPrefix prefix p.menu.location) remaining) + right + wrong + ; + + node = page // { + children = processChildren prefix right; + }; + in + go (nodes ++ [ node ]) wrong; + + # Recursively produce a tree of child pages + processChildren = + prefix: pages: + pagesToAttrs ( + builtins.map ( + page: + page + // { + menu = page.menu // { + location = lib.lists.removePrefix prefix page.menu.location; + }; + } + ) (go [ ] pages) + ); + + posLength = page: builtins.length page.menu.location; + in + # Sort by location length _before_ using `go` + # `go` assumes that parent positions come before child positions + list: pagesToAttrs (go [ ] (lib.sortOn posLength list)); + + # Convert a list of nodes into an attrset + # The each page node's menu.location is used to derive its attr name + pagesToAttrs = + let + getName = lib.concatStringsSep "."; + in + nodes: + builtins.listToAttrs ( + builtins.map (node: { + # TODO: allow pages to define their own link-text? + name = getName node.menu.location; + value = node; + }) nodes + ); + + # Render tree of pages to a markdown summary menu + renderMenuPages = + bullet: pages: + let + renderLine = + indent: page: + let + inherit (page.menu) location; + text = lib.showOption location; + # FIXME: mdbook complains "chapter file not found" when creating a dangling link, + # but what if we _want_ to do so, e.g. to link to `search/index.html` or to an external site? + # Maybe dangling/external links are only permitted when they don't end with `.md` ? + target = lib.optionalString (page.source != null) page.target; + in + indent + bullet + "[${text}](${target})"; + renderNode = + indent: page: + [ (renderLine indent page) ] + ++ lib.optionals (page ? children) ( + builtins.concatMap (renderNode (indent + " ")) (builtins.attrValues page.children) + ); + in + builtins.concatMap (renderNode "") (builtins.attrValues pages); + + sectionType = lib.types.submodule ( + { name, config, ... }: + { + options = { + displayName = lib.mkOption { + type = lib.types.str; + description = "The section's display name."; + default = name; + }; + markdown = lib.mkOption { + type = lib.types.str; + description = "The section's display name."; + default = '' + # ${config.displayName} + ''; + defaultText = lib.literalExpression ''"# ''${config.displayName}"''; + }; + order = lib.mkOption { + type = lib.types.ints.unsigned; + description = "Ordering priority"; + default = 1000; + }; + nesting = lib.mkOption { + type = lib.types.bool; + description = "Whether pages in this section can be nested within other menu items."; + default = true; + }; + pages = lib.mkOption { + # NOTE: Use attrs here to avoid defaults & internal definitions from the docsPageType + # TODO: Maybe don't expose this as a module option at all? A privately scoped binding would be fine. + type = with lib.types; listOf attrs; + description = "Pages that belong to this section."; + visible = "shallow"; + readOnly = true; + }; + text = lib.mkOption { + type = with lib.types; nullOr lines; + description = "Lines to include in the menu."; + readOnly = true; + }; + }; + config = { + pages = pagesBySection.${name} or [ ]; + text = + let + pages = (if config.nesting then mkPagesTree else pagesToAttrs) config.pages; + bullet = lib.optionalString config.nesting "- "; + text = lib.mkMerge ( + [ config.markdown ] + ++ renderMenuPages bullet pages + ++ [ + "" + ] + ); + in + if config.pages == [ ] then null else text; + }; + } + ); +in +{ + options.docs.menu.sections = lib.mkOption { + type = with lib.types; attrsOf sectionType; + description = '' + A set of menu sections/parts that pages can belong to. + ''; + defaultText = { }; + }; + + config.docs.menu.sections = { + header = { + displayName = "Menu"; + nesting = false; + order = 0; + }; + user-guide = { + displayName = "User guide"; + order = 100; + }; + platforms = { + displayName = "Platforms-specific options"; + order = 2000; + }; + options = { + displayName = "Options"; + order = 5000; + }; + footer = { + displayName = ""; + markdown = '' + # + + --- + ''; + nesting = false; + order = 10000; + }; + }; +} diff --git a/modules/docs/pages.nix b/modules/docs/pages.nix new file mode 100644 index 0000000000..7dee3523b7 --- /dev/null +++ b/modules/docs/pages.nix @@ -0,0 +1,132 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (config.docs.menu) + sections + ; + + docsPageModule = + { + name, + config, + options, + ... + }: + let + derivationName = builtins.replaceStrings [ "/" ] [ "-" ] name; + in + { + options = { + enable = lib.mkOption { + type = lib.types.bool; + description = "Whether to enable this page/menu item."; + default = true; + example = false; + }; + target = lib.mkOption { + type = lib.types.str; + description = '' + The target filepath, relative to the root of the docs. + ''; + default = lib.optionalString (name != "") (name + "/") + "index.md"; + defaultText = lib.literalMD '' + `` joined with `"index.md"`. Separated by `"/"` if `` is non-empty. + ''; + }; + text = lib.mkOption { + type = with lib.types; nullOr lines; + default = null; + description = "Text of the file."; + }; + source = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + description = '' + Markdown page. Set to null to create a menu entry without a corresponding file. + ''; + }; + menu.location = lib.mkOption { + type = with lib.types; listOf str; + description = '' + A location path that represents the page's position in the menu tree. + + The text displayed in the menu is derived from this value, + after the location of any parent nodes in the tree is removed. + + For example, if this page has the location `[ "foo" "bar" ]` + and there is another page with the location `[ "foo" ]`, + then the menu will render as: + ```markdown + - foo + - bar + ``` + + However if there was no other page with the `[ "foo" ]` location, + the menu would instead render as: + ```markdown + - foo.bar + ``` + ''; + default = + let + list = lib.splitString "/" config.target; + last = lib.last list; + rest = lib.dropEnd 1 list; + in + if last == "index.md" then + rest + else if lib.hasSuffix ".md" last then + rest ++ [ (lib.removeSuffix ".md" last) ] + else + list; + defaultText = lib.literalMD '' + `target`, split by `"/"`, with any trailing `"index.md` or `".md"` suffixes removed. + ''; + }; + menu.section = lib.mkOption { + type = lib.types.enum (builtins.attrNames sections); + description = '' + Determines the menu section. + + Must be a section defined in `docs.menu.sections`. + ''; + }; + }; + + config.source = lib.mkIf (config.text != null) ( + lib.mkDerivedConfig options.text (builtins.toFile derivationName) + ); + }; + + # NOTE: using submoduleWith to avoid shorthandOnlyDefinesConfig + docsPageType = lib.types.submoduleWith { + modules = [ docsPageModule ]; + }; +in +{ + options.docs = { + pages = lib.mkOption { + type = with lib.types; lazyAttrsOf docsPageType; + default = { }; + description = '' + Pages to include in the docs. + ''; + }; + src = lib.mkOption { + type = lib.types.package; + description = "All source files for the docs."; + readOnly = true; + }; + }; + + config.docs = { + # A directory with all the files in it + src = pkgs.callPackage ./collect-sources.nix { + pages = builtins.filter (page: page.source or null != null) (builtins.attrValues config.docs.pages); + }; + }; +} diff --git a/modules/docs/readme.nix b/modules/docs/readme.nix new file mode 100644 index 0000000000..3e16f29764 --- /dev/null +++ b/modules/docs/readme.nix @@ -0,0 +1,69 @@ +{ + lib, + fixLinks, + runCommand, + availableVersions ? [ ], + baseHref ? "/", # TODO: remove & get from module config +}: +let + # Zip the list of attrs into an attr of lists, for use as bash arrays + zippedVersions = + assert lib.assertMsg + (lib.all (o: o ? branch && o ? nixpkgsBranch && o ? baseHref) availableVersions) + "Expected all `availableVersions` docs entries to contain { branch, nixpkgsBranch, baseHref } attrs!"; + lib.zipAttrs availableVersions; +in +runCommand "index.md" + { + template = ../../docs/mdbook/index.md; + + readme = + runCommand "readme" + { + start = ""; + end = ""; + src = fixLinks ../../README.md; + } + '' + # extract relevant section of the README + sed -n "/$start/,/$end/p" $src > $out + ''; + + docs_versions = + runCommand "docs-versions" + { + __structuredAttrs = true; + branches = zippedVersions.branch or [ ]; + nixpkgsBranches = zippedVersions.nixpkgsBranch or [ ]; + baseHrefs = zippedVersions.baseHref or [ ]; + current = baseHref; + } + '' + touch "$out" + for i in ''${!branches[@]}; do + branch="''${branches[i]}" + nixpkgs="''${nixpkgsBranches[i]}" + baseHref="''${baseHrefs[i]}" + linkText="\`$branch\` branch" + + link= + suffix= + if [ "$baseHref" = "$current" ]; then + # Don't bother linking to ourselves + link="$linkText" + suffix=" _(this page)_" + else + link="[$linkText]($baseHref)" + fi + + echo "- The $link, for use with nixpkgs \`$nixpkgs\`$suffix" >> "$out" + done + # link to beta-docs + echo "- The [beta-docs](./beta), for use with " + ''; + } + '' + substitute $template $out \ + --subst-var-by README "$(cat $readme)" \ + --subst-var-by DOCS_VERSIONS "$(cat $docs_versions)" + '' From 5d4f54db395016f6f5bd55a45bea1e9d2bd6d960 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 26 Feb 2025 02:55:33 +0000 Subject: [PATCH 04/10] modules/docs: add user-guide --- modules/docs/default.nix | 1 + modules/docs/user-guide/default.nix | 33 ++++++++++++++++++++++++ modules/docs/user-guide/user-configs.nix | 14 ++++++++++ 3 files changed, 48 insertions(+) create mode 100644 modules/docs/user-guide/default.nix create mode 100644 modules/docs/user-guide/user-configs.nix diff --git a/modules/docs/default.nix b/modules/docs/default.nix index 9d60fa2409..b6d920e4eb 100644 --- a/modules/docs/default.nix +++ b/modules/docs/default.nix @@ -14,6 +14,7 @@ in ./mdbook ./menu ./pages.nix + ./user-guide ]; config.docs = { diff --git a/modules/docs/user-guide/default.nix b/modules/docs/user-guide/default.nix new file mode 100644 index 0000000000..d55e04cd4e --- /dev/null +++ b/modules/docs/user-guide/default.nix @@ -0,0 +1,33 @@ +{ + lib, + pkgs, + ... +}: +let + user-guide = ../../../docs/user-guide; + + sourceTransformers = { + config-examples = + template: + pkgs.callPackage ./user-configs.nix { + inherit template; + }; + }; +in +{ + docs.pages = lib.concatMapAttrs ( + name: type: + let + title = lib.removeSuffix ".md" name; + transformer = sourceTransformers.${title} or lib.id; + in + lib.optionalAttrs (type == "regular") { + "user-guide/${title}" = { + menu.section = "user-guide"; + # TODO: define user-facing titles to show in the menu... + menu.location = [ title ]; + source = transformer "${user-guide}/${name}"; + }; + } + ) (builtins.readDir user-guide); +} diff --git a/modules/docs/user-guide/user-configs.nix b/modules/docs/user-guide/user-configs.nix new file mode 100644 index 0000000000..1df2940f19 --- /dev/null +++ b/modules/docs/user-guide/user-configs.nix @@ -0,0 +1,14 @@ +{ + runCommand, + callPackage, + template, +}: +runCommand "user-configs.md" + { + inherit template; + user_configs = callPackage ../../../docs/user-configs { }; + } + '' + substitute $template $out \ + --subst-var-by USER_CONFIGS "$(cat $user_configs)" + '' From 00d06766241392f36f881ab425f8f998b9b2b8bf Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 26 Feb 2025 03:08:00 +0000 Subject: [PATCH 05/10] modules/docs: add `optionPages` option Provides a way to add a page to the docs that documents a subset of `options` using nixos-render-docs. --- modules/docs/_utils.nix | 74 +++++++++ modules/docs/default.nix | 2 + modules/docs/option-pages/default.nix | 181 ++++++++++++++++++++++ modules/docs/option-pages/render-page.nix | 39 +++++ 4 files changed, 296 insertions(+) create mode 100644 modules/docs/_utils.nix create mode 100644 modules/docs/option-pages/default.nix create mode 100644 modules/docs/option-pages/render-page.nix diff --git a/modules/docs/_utils.nix b/modules/docs/_utils.nix new file mode 100644 index 0000000000..360f5106e6 --- /dev/null +++ b/modules/docs/_utils.nix @@ -0,0 +1,74 @@ +{ lib, ... }: +let + transformOption = + let + root = builtins.toString ../../.; + mkGitHubDeclaration = user: repo: branch: subpath: { + url = "https://github.com/${user}/${repo}/blob/${branch}/${subpath}"; + name = "<${repo}/${subpath}>"; + }; + transformDeclaration = + decl: + if lib.hasPrefix root (builtins.toString decl) then + mkGitHubDeclaration "nix-community" "nixvim" "main" ( + lib.removePrefix "/" (lib.strings.removePrefix root (builtins.toString decl)) + ) + else if decl == "lib/modules.nix" then + mkGitHubDeclaration "NixOS" "nixpkgs" "master" decl + else + decl; + in + opt: opt // { declarations = builtins.map transformDeclaration opt.declarations; }; +in +{ + options.docs._utils = lib.mkOption { + type = with lib.types; lazyAttrsOf raw; + description = "internal utils, modules, functions, etc"; + default = { }; + internal = true; + visible = false; + }; + + config.docs._utils = { + /** + Uses `lib.optionAttrSetToDocList` to produce a list of docs-options. + + A doc-option has the following attrs, as expected by `nixos-render-docs`: + + ``` + { + loc, + name, # rendered with `showOption loc` + description, + declarations, + internal, + visible, # normalised to a boolean + readOnly, + type, # normalised to `type.description` + default,? # rendered with `lib.options.renderOptionValue` + example,? # rendered with `lib.options.renderOptionValue` + relatedPackages,? + } + ``` + + Additionally, sub-options are recursively flattened into the list, + unless `visible == "shallow"` or `visible == false`. + + This function extends `lib.optionAttrSetToDocList` by also filtering out + invisible and internal options, and by applying Nixvim's `transformOption` + function. + + The implementation is based on `pkgs.nixosOptionsDoc`: + https://github.com/NixOS/nixpkgs/blob/e2078ef3/nixos/lib/make-options-doc/default.nix#L117-L126 + */ + mkOptionList = lib.flip lib.pipe [ + (lib.flip builtins.removeAttrs [ "_module" ]) + lib.optionAttrSetToDocList + (builtins.map transformOption) + (builtins.filter (opt: opt.visible && !opt.internal)) + # TODO: consider supporting `relatedPackages` + # See https://github.com/NixOS/nixpkgs/blob/61235d44/lib/options.nix#L103-L104 + # and https://github.com/NixOS/nixpkgs/blob/61235d44/nixos/lib/make-options-doc/default.nix#L128-L165 + ]; + }; +} diff --git a/modules/docs/default.nix b/modules/docs/default.nix index b6d920e4eb..a9b9cc6079 100644 --- a/modules/docs/default.nix +++ b/modules/docs/default.nix @@ -11,8 +11,10 @@ in }; imports = [ + ./_utils.nix ./mdbook ./menu + ./option-pages ./pages.nix ./user-guide ]; diff --git a/modules/docs/option-pages/default.nix b/modules/docs/option-pages/default.nix new file mode 100644 index 0000000000..6f419ec1d6 --- /dev/null +++ b/modules/docs/option-pages/default.nix @@ -0,0 +1,181 @@ +{ + lib, + config, + options, + pkgs, + ... +}: +let + inherit (config.docs._utils) + mkOptionList + ; + + # Gets the page that owns this option. + # We can use `findFirst` because `pageScopes` is a sorted list. + getPageFor = + loc: (lib.findFirst (pair: lib.lists.hasPrefix pair.scope loc) null pageScopePairs).page or null; + + optionsLists = builtins.groupBy (opt: getPageFor opt.loc) (mkOptionList options); + + # A list of { page, scope } pairs, sorted by scope length (longest first) + pageScopePairs = lib.pipe config.docs.optionPages [ + (lib.mapAttrsToList ( + name: page: { + page = name; + scopes = page.optionScopes; + } + )) + (builtins.concatMap ({ page, scopes }: builtins.map (scope: { inherit page scope; }) scopes)) + (lib.sortOn (pair: 0 - builtins.length pair.scope)) + ]; + + # Custom type to simplify type checking & merging. + # `listOf str` is overkill and problematic for our use-case. + optionLocType = lib.mkOptionType { + name = "option-loc"; + description = "option location"; + descriptionClass = "noun"; + check = v: lib.isList v && lib.all lib.isString v; + }; + + optionsPageModule = + { name, config, ... }: + { + options = { + enable = lib.mkOption { + type = lib.types.bool; + default = config.optionsList != [ ]; + defaultText = lib.literalExpression ''optionsList != [ ]''; + example = true; + description = "Whether to define a page derived from this optionsPage."; + }; + optionsList = lib.mkOption { + type = with lib.types; listOf raw; + description = '' + List of options matching `scopes`. + ''; + readOnly = true; + }; + optionsJSON = lib.mkOption { + type = lib.types.package; + description = '' + `options.json` file, as expected by `nixos-render-docs`. + ''; + readOnly = true; + }; + page = lib.mkOption { + type = lib.types.deferredModule; + description = '' + The `page` module, to be assigned to `docs.pages.`. + ''; + defaultText = lib.literalMD '' + Options derived from the outer options-page + ''; + }; + }; + + config = + let + cfg = config; + drvName = lib.replaceStrings [ "/" ] [ "-" ] name; + # Convert the doc-options list into the structure required for options.json + # See https://github.com/NixOS/nixpkgs/blob/e2078ef3/nixos/lib/make-options-doc/default.nix#L167-L176 + optionsSet = builtins.listToAttrs ( + builtins.map (opt: { + inherit (opt) name; + value = builtins.removeAttrs opt [ + "name" + "visible" + "internal" + ]; + }) config.optionsList + ); + in + { + optionsJSON = builtins.toFile "options-${drvName}.json" ( + builtins.unsafeDiscardStringContext (builtins.toJSON optionsSet) + ); + + page = + { config, ... }: + { + # TODO: should this be conditional on something? + text = lib.mkMerge [ + # TODO: title + # TODO: description + ]; + # NOTE: use a _really_ high override priority because there will + # be another definition with the same prio as `text` + source = lib.mkIf (cfg.optionsList != [ ]) ( + lib.mkOverride 1 ( + pkgs.callPackage ./render-page.nix { + inherit (config) text; + inherit (cfg) optionsJSON; + name = drvName; + } + ) + ); + }; + }; + }; + + optionsPageType = lib.types.submodule ( + { name, ... }: + { + imports = [ + optionsPageModule + ]; + options.optionScopes = lib.mkOption { + type = with lib.types; coercedTo optionLocType lib.singleton (nonEmptyListOf optionLocType); + description = '' + A list of option-locations to be included in this page. + ''; + }; + config = { + optionsList = optionsLists.${name} or [ ]; + page.menu.section = lib.mkDefault "options"; + }; + } + ); + + checkDocs = + value: + let + duplicates = lib.pipe value [ + builtins.attrValues + (builtins.concatMap (doc: doc.optionScopes)) + (builtins.groupBy lib.showOption) + (lib.mapAttrs (_: builtins.length)) + (lib.filterAttrs (_: count: count > 1)) + ]; + in + assert lib.assertMsg (duplicates == { }) '' + `docs.options` has conflicting `optionScopes` definitions: + ${lib.concatMapAttrsStringSep "\n" ( + name: count: "- `${name}' defined ${toString count} times" + ) duplicates} + Definitions:${lib.options.showDefs options.docs.options.definitionsWithLocations} + ''; + value; +in +{ + options.docs = { + optionPages = lib.mkOption { + type = with lib.types; lazyAttrsOf optionsPageType; + description = '' + A set of option scopes to include in the docs. + + Each enabled options page will produce a corresponding `pages` page. + ''; + default = { }; + apply = checkDocs; + }; + }; + config.docs = { + # Define pages for each "optionPages" attr + pages = lib.pipe config.docs.optionPages [ + (lib.filterAttrs (_: v: v.enable)) + (builtins.mapAttrs (_: cfg: cfg.page)) + ]; + }; +} diff --git a/modules/docs/option-pages/render-page.nix b/modules/docs/option-pages/render-page.nix new file mode 100644 index 0000000000..1edf152b6c --- /dev/null +++ b/modules/docs/option-pages/render-page.nix @@ -0,0 +1,39 @@ +{ + path, + nixos-render-docs, + runCommand, + name, + text, + optionsJSON, + revision ? "", +}: +runCommand "page-${name}.md" + { + inherit + text + optionsJSON + revision + ; + + # https://github.com/NixOS/nixpkgs/blob/master/doc/manpage-urls.json + manpageUrls = path + "/doc/manpage-urls.json"; + + nativeBuildInputs = [ + nixos-render-docs + ]; + } + '' + nixos-render-docs -j $NIX_BUILD_CORES \ + options commonmark \ + --manpage-urls $manpageUrls \ + --revision "$revision" \ + --anchor-prefix opt- \ + --anchor-style legacy \ + $optionsJSON options.md + + ( + echo "$text" + echo + cat options.md + ) > $out + '' From 83350c7285a72c42b32e91eec9d10f29bf7fe16b Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 26 Feb 2025 03:14:48 +0000 Subject: [PATCH 06/10] modules/docs: add various options pages - options (root) - plugins - colorschemes - docs --- modules/default.nix | 5 +++++ modules/docs/default.nix | 7 +++++++ modules/plugins.nix | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/modules/default.nix b/modules/default.nix index e0d65bba0e..76bfeaf024 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -23,4 +23,9 @@ ./performance.nix ./plugins.nix ]; + + docs.optionPages.options = { + enable = true; + optionScopes = [ ]; + }; } diff --git a/modules/docs/default.nix b/modules/docs/default.nix index a9b9cc6079..80e291e53d 100644 --- a/modules/docs/default.nix +++ b/modules/docs/default.nix @@ -33,5 +33,12 @@ in menu.location = [ "Contributing" ]; source = fixLinks ../../CONTRIBUTING.md; }; + optionPages.docs = { + optionScopes = [ "docs" ]; + page.menu.location = [ "docs" ]; + page.text = '' + Internal options used to construct these docs. + ''; + }; }; } diff --git a/modules/plugins.nix b/modules/plugins.nix index a0d0a67427..bac57efc05 100644 --- a/modules/plugins.nix +++ b/modules/plugins.nix @@ -1,7 +1,7 @@ -{ lib, ... }: +{ lib, options, ... }: let inherit (builtins) readDir; - inherit (lib.attrsets) foldlAttrs; + inherit (lib.attrsets) foldlAttrs mapAttrs'; inherit (lib.lists) optional; by-name = ../plugins/by-name; in @@ -12,4 +12,37 @@ in prev: name: type: prev ++ optional (type == "directory") (by-name + "/${name}") ) [ ] (readDir by-name); + + docs.optionPages = + let + mkPluginPages = + scope: + mapAttrs' ( + name: _: + let + loc = [ + scope + name + ]; + in + { + name = lib.concatStringsSep "/" loc; + value = { + optionScopes = loc; + }; + } + ) options.${scope}; + in + { + colorschemes = { + enable = true; + optionScopes = [ "colorschemes" ]; + }; + plugins = { + enable = true; + optionScopes = [ "plugins" ]; + }; + } + // mkPluginPages "plugins" + // mkPluginPages "colorschemes"; } From 599441a2bf0bd4323ae5356ed2f82cfe55d0e6a9 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 26 Feb 2025 03:22:39 +0000 Subject: [PATCH 07/10] modules/docs: add `platformPages` option --- modules/docs/default.nix | 1 + modules/docs/option-pages/default.nix | 3 ++ modules/docs/platforms.nix | 66 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 modules/docs/platforms.nix diff --git a/modules/docs/default.nix b/modules/docs/default.nix index 80e291e53d..b985d89df1 100644 --- a/modules/docs/default.nix +++ b/modules/docs/default.nix @@ -16,6 +16,7 @@ in ./menu ./option-pages ./pages.nix + ./platforms.nix ./user-guide ]; diff --git a/modules/docs/option-pages/default.nix b/modules/docs/option-pages/default.nix index 6f419ec1d6..fa05f66959 100644 --- a/modules/docs/option-pages/default.nix +++ b/modules/docs/option-pages/default.nix @@ -172,6 +172,9 @@ in }; }; config.docs = { + _utils = { + inherit optionsPageModule; + }; # Define pages for each "optionPages" attr pages = lib.pipe config.docs.optionPages [ (lib.filterAttrs (_: v: v.enable)) diff --git a/modules/docs/platforms.nix b/modules/docs/platforms.nix new file mode 100644 index 0000000000..90ac5274ae --- /dev/null +++ b/modules/docs/platforms.nix @@ -0,0 +1,66 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (config.docs._utils) + optionsPageModule + mkOptionList + ; + + evalModule = + module: + lib.evalModules { + modules = [ + module + { _module.check = false; } + { _module.args.pkgs = lib.mkForce pkgs; } + ]; + }; + + platformPageType = lib.types.submodule ( + { config, ... }: + { + imports = [ + optionsPageModule + ]; + options = { + module = lib.mkOption { + type = lib.types.deferredModule; + description = '' + A module defining platform-specific options. + ''; + }; + }; + config = { + optionsList = lib.pipe config.module [ + evalModule + (lib.getAttr "options") + mkOptionList + ]; + page.menu.section = lib.mkDefault "platforms"; + }; + } + ); +in +{ + options.docs = { + platformPages = lib.mkOption { + type = with lib.types; lazyAttrsOf platformPageType; + description = '' + A set of platform pages to include in the docs. + + Each enabled platform page will produce a corresponding `pages` page. + ''; + default = { }; + }; + }; + + # Define pages for each "platformPages" attr + config.docs.pages = lib.pipe config.docs.platformPages [ + (lib.filterAttrs (_: v: v.enable)) + (builtins.mapAttrs (_: cfg: cfg.page)) + ]; +} From d6eafb1a282515bf371fb261c59f279a4473f4a4 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 26 Feb 2025 03:23:07 +0000 Subject: [PATCH 08/10] modules/docs: add platform pages to the docs --- modules/docs/platforms.nix | 51 ++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/modules/docs/platforms.nix b/modules/docs/platforms.nix index 90ac5274ae..af4f63702c 100644 --- a/modules/docs/platforms.nix +++ b/modules/docs/platforms.nix @@ -58,9 +58,50 @@ in }; }; - # Define pages for each "platformPages" attr - config.docs.pages = lib.pipe config.docs.platformPages [ - (lib.filterAttrs (_: v: v.enable)) - (builtins.mapAttrs (_: cfg: cfg.page)) - ]; + config.docs = { + platformPages = { + "platforms/nixos" = { + page.menu.location = [ + "platforms" + "NixOS" + ]; + module = ../../wrappers/modules/nixos.nix; + }; + "platforms/home-manager" = { + page.menu.location = [ + "platforms" + "home-manager" + ]; + module = ../../wrappers/modules/hm.nix; + }; + "platforms/nix-darwin" = { + page.menu.location = [ + "platforms" + "nix-darwin" + ]; + module = ../../wrappers/modules/darwin.nix; + }; + }; + pages = + { + "platforms" = { + menu.section = "platforms"; + menu.location = [ "platforms" ]; + source = ../../docs/platforms/index.md; + }; + "platforms/standalone" = { + menu.section = "platforms"; + menu.location = [ + "platforms" + "standalone" + ]; + source = ../../docs/platforms/standalone.md; + }; + } + # Define pages for each "platformPages" attr + // lib.pipe config.docs.platformPages [ + (lib.filterAttrs (_: v: v.enable)) + (builtins.mapAttrs (_: cfg: cfg.page)) + ]; + }; } From f09ff46e427231b09ef3d5fd1b8ecbc7294cc65a Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Mon, 27 Jan 2025 19:40:40 +0000 Subject: [PATCH 09/10] lib/plugins: define docs description for plugins --- lib/plugins/utils.nix | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/plugins/utils.nix b/lib/plugins/utils.nix index b687e60a1d..74c81b43c8 100644 --- a/lib/plugins/utils.nix +++ b/lib/plugins/utils.nix @@ -96,11 +96,24 @@ { options, ... }: let opts = lib.getAttrFromPath loc options; + docsfile = lib.concatStringsSep "/" loc; url = if args.url or null == null then opts.package.default.meta.homepage or (throw "unable to get URL for `${lib.showOption loc}`.") else args.url; + maintainersString = + let + toMD = m: if m ? github then "[${m.name}](https://github.com/${m.github})" else m.name; + names = builtins.map toMD (lib.unique maintainers); + count = builtins.length names; + in + if count == 1 then + builtins.head names + else if count == 2 then + lib.concatStringsSep " and " names + else + lib.concatMapStrings (name: "\n- ${name}") names; in { meta = { @@ -110,5 +123,23 @@ path = loc; }; }; + + docs.pages.${docsfile}.text = lib.mkMerge ( + [ + "# ${lib.last loc}" + "" + "**URL:** [${url}](${url})" + "" + ] + ++ lib.optionals (maintainers != [ ]) [ + "**Maintainers:** ${maintainersString}" + "" + ] + ++ lib.optionals (description != null && description != "") [ + "---" + "" + description + ] + ); }; } From ba77348220a10f756efa4fb571becff53fd69798 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Sat, 25 Jan 2025 19:46:06 +0000 Subject: [PATCH 10/10] docs: build beta-docs --- docs/default.nix | 5 ++++- docs/mdbook/default.nix | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/default.nix b/docs/default.nix index 3076674895..2acdc84095 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -124,7 +124,10 @@ lib.fix ( # > sandbox-exec: pattern serialization length 69298 exceeds maximum (65535) docs = pkgs.callPackage ./mdbook { inherit evaledModules transformOptions; - inherit (self) search; + inherit (self) search beta-docs; }; + + # Beta docs + beta-docs = evaledModules.config.docs.html.site; } ) diff --git a/docs/mdbook/default.nix b/docs/mdbook/default.nix index 32f1b132b7..5c6fc7a14b 100644 --- a/docs/mdbook/default.nix +++ b/docs/mdbook/default.nix @@ -7,6 +7,7 @@ nixosOptionsDoc, transformOptions, search, + beta-docs, # The root directory of the site baseHref ? "/", # A list of all available docs that should be linked to @@ -375,6 +376,10 @@ pkgs.stdenv.mkDerivation (finalAttrs: { cp -r ./book/* $dest mkdir -p $dest/search cp -r ${finalAttrs.passthru.search}/* $dest/search + + # Also build the beta docs + mkdir -p $dest/beta + cp -r ${finalAttrs.passthru.beta-docs}/* $dest/beta ''; inherit baseHref; @@ -406,6 +411,11 @@ pkgs.stdenv.mkDerivation (finalAttrs: { search = search.override { baseHref = finalAttrs.baseHref + "search/"; }; + beta-docs = beta-docs.override (old: { + settings = lib.recursiveUpdate old.settings { + output.html.site-url = "${baseHref}/beta"; + }; + }); docs-versions = runCommand "docs-versions" { @@ -435,6 +445,8 @@ pkgs.stdenv.mkDerivation (finalAttrs: { echo "- The $link, for use with nixpkgs \`$nixpkgs\`$suffix" >> "$out" done + # link to beta-docs + echo "- The [beta-docs](./beta), for use with " ''; user-configs = callPackage ../user-configs { }; };