From 1cff7862e22bbbaaa0aa0bff2bcb5e80ea332d80 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Wed, 29 Jan 2025 19:55:49 +0000 Subject: [PATCH 1/5] 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 ac29a332df98bd885284bce487bf5fd8c59c6e87 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Thu, 23 Jan 2025 15:50:24 +0000 Subject: [PATCH 2/5] 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 2df01b686d5d16d86bb9155a616121955d062c34 Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Thu, 23 Jan 2025 14:12:36 +0000 Subject: [PATCH 3/5] 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/default.nix | 5 + modules/docs/_util.nix | 166 +++++++++++++++++++++++++++ modules/docs/all.nix | 49 ++++++++ modules/docs/default.nix | 20 ++++ modules/docs/files.nix | 89 +++++++++++++++ modules/docs/mdbook/default.nix | 50 ++++++++ modules/docs/mdbook/package.nix | 33 ++++++ modules/docs/menu/default.nix | 30 +++++ modules/docs/menu/sections.nix | 195 ++++++++++++++++++++++++++++++++ modules/docs/options.nix | 179 +++++++++++++++++++++++++++++ modules/docs/platforms.nix | 101 +++++++++++++++++ modules/docs/readme.nix | 72 ++++++++++++ modules/docs/render-page.nix | 43 +++++++ modules/docs/src.nix | 27 +++++ modules/docs/user-configs.nix | 14 +++ modules/plugins.nix | 37 +++++- 16 files changed, 1108 insertions(+), 2 deletions(-) create mode 100644 modules/docs/_util.nix create mode 100644 modules/docs/all.nix create mode 100644 modules/docs/files.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/options.nix create mode 100644 modules/docs/platforms.nix create mode 100644 modules/docs/readme.nix create mode 100644 modules/docs/render-page.nix create mode 100644 modules/docs/src.nix create mode 100644 modules/docs/user-configs.nix diff --git a/modules/default.nix b/modules/default.nix index e0d65bba0e..64b373b1cc 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -23,4 +23,9 @@ ./performance.nix ./plugins.nix ]; + + docs.options.options = { + enable = true; + optionScopes = [ ]; + }; } diff --git a/modules/docs/_util.nix b/modules/docs/_util.nix new file mode 100644 index 0000000000..d100ee8ed3 --- /dev/null +++ b/modules/docs/_util.nix @@ -0,0 +1,166 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (config.docs.menu) + sections + ; + + 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; }; + + docsPageModule = + { name, config, ... }: + { + 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. + ''; + }; + source = lib.mkOption { + type = with lib.types; nullOr path; + 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`. + ''; + }; + }; + }; +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 = { + # A liberal type that permits any superset of docsPageModule + docsPageLiberalType = lib.types.submodule [ + { _module.check = false; } + docsPageModule + ]; + + /** + 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 + ]; + + inherit docsPageModule; + }; +} diff --git a/modules/docs/all.nix b/modules/docs/all.nix new file mode 100644 index 0000000000..17a19dbe60 --- /dev/null +++ b/modules/docs/all.nix @@ -0,0 +1,49 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (config.docs._utils) + docsPageLiberalType + ; +in +{ + options.docs = { + _allInputs = lib.mkOption { + type = with lib.types; listOf str; + description = "`docs.*` option names that should be included in `docs.all`."; + defaultText = config.docs._allInputs; + default = [ ]; + internal = true; + visible = false; + }; + all = lib.mkOption { + type = with lib.types; listOf docsPageLiberalType; + description = '' + All enabled doc pages defined in: + ${lib.concatMapStringsSep "\n" (name: "- `docs.${name}`") config.docs._allInputs}. + ''; + visible = "shallow"; + readOnly = true; + }; + src = lib.mkOption { + type = lib.types.package; + description = "All source files for the docs."; + readOnly = true; + }; + }; + + config.docs = { + # Copy all pages from options listed in _allInputs + all = builtins.filter (page: page.enable or true) ( + builtins.concatMap (name: builtins.attrValues config.docs.${name}) config.docs._allInputs + ); + + # A directory with all the files in it + src = pkgs.callPackage ./src.nix { + pages = builtins.filter (page: page.source or null != null) config.docs.all; + }; + }; +} diff --git a/modules/docs/default.nix b/modules/docs/default.nix index b4fb42734f..5829013f41 100644 --- a/modules/docs/default.nix +++ b/modules/docs/default.nix @@ -5,4 +5,24 @@ default = true; description = "Install the man pages for NixVim options."; }; + + imports = [ + ./_util.nix + ./all.nix + ./files.nix + ./mdbook + ./menu + ./options.nix + ./platforms.nix + ]; + + config.docs.options = { + docs = { + menu.location = [ "docs" ]; + optionScopes = [ "docs" ]; + description = '' + Internal options used to construct these docs. + ''; + }; + }; } diff --git a/modules/docs/files.nix b/modules/docs/files.nix new file mode 100644 index 0000000000..a245f49a9b --- /dev/null +++ b/modules/docs/files.nix @@ -0,0 +1,89 @@ +{ + lib, + config, + pkgs, + ... +}: +let + inherit (config.docs._utils) + docsPageModule + ; + + docsPageType = lib.types.submodule ( + { + name, + config, + options, + ... + }: + let + derivationName = builtins.replaceStrings [ "/" ] [ "-" ] name; + in + { + imports = [ + docsPageModule + ]; + options.text = lib.mkOption { + type = with lib.types; nullOr lines; + default = null; + description = "Text of the file."; + }; + config.source = lib.mkIf (config.text != null) ( + lib.mkDerivedConfig options.text (builtins.toFile derivationName) + ); + } + ); + + user-guide = ../../docs/user-guide; + + sourceTransformers = { + config-examples = + template: + pkgs.callPackage ./user-configs.nix { + inherit template; + }; + }; +in +{ + options.docs = { + files = lib.mkOption { + type = with lib.types; lazyAttrsOf docsPageType; + description = '' + A set of pages to include in the docs. + ''; + default = { }; + }; + }; + + config.docs = { + files = + # TODO: contributing file + { + "" = { + menu.section = "header"; + menu.location = [ "Home" ]; + source = pkgs.callPackage ./readme.nix { + # TODO: get `availableVersions` and `baseHref` from module options + }; + }; + } + // 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); + + # Register for inclusion in `all` + _allInputs = [ "files" ]; + }; +} 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..d3c4668290 --- /dev/null +++ b/modules/docs/menu/default.nix @@ -0,0 +1,30 @@ +{ + lib, + config, + ... +}: +let + sortedMenuSections = lib.sortOn (section: section.order) ( + builtins.attrValues config.docs.menu.sections + ); +in +{ + imports = [ + ./sections.nix + ]; + + options.docs.menu.src = lib.mkOption { + type = lib.types.lines; + description = '' + MDBook SUMMARY menu. Generated from pages defined in + ${lib.concatMapStringsSep ", " (name: "`docs.${name}`") config.docs._allInputs}. + + See MDBook's [SUMMARY.md](https://rust-lang.github.io/mdBook/format/summary.html) docs. + ''; + readOnly = true; + }; + + config.docs.menu.src = lib.mkMerge ( + builtins.filter (text: text != null) (builtins.catAttrs "text" sortedMenuSections) + ); +} diff --git a/modules/docs/menu/sections.nix b/modules/docs/menu/sections.nix new file mode 100644 index 0000000000..d8b2208b63 --- /dev/null +++ b/modules/docs/menu/sections.nix @@ -0,0 +1,195 @@ +{ + lib, + config, + ... +}: +let + inherit (config.docs._utils) + docsPageLiberalType + ; + + pagesBySection = builtins.groupBy (page: page.menu.section) config.docs.all; + + # 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 { + type = with lib.types; listOf docsPageLiberalType; + 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/options.nix b/modules/docs/options.nix new file mode 100644 index 0000000000..37e3a18865 --- /dev/null +++ b/modules/docs/options.nix @@ -0,0 +1,179 @@ +{ + lib, + config, + options, + pkgs, + ... +}: +let + inherit (config.docs._utils) + docsPageModule + 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.options [ + (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, ... }: + { + imports = [ + docsPageModule + ]; + options = { + enable = lib.mkOption { + defaultText = lib.literalExpression ''required || optionsList != [ ]''; + }; + title = lib.mkOption { + type = lib.types.str; + description = '' + The page title. Also used as text for the menu item link. + ''; + default = lib.last config.menu.location; + defaultText = lib.literalMD "The last element in `menu.location`"; + }; + description = lib.mkOption { + type = lib.types.lines; + description = '' + An optional description included at the start of the index page. + ''; + default = ""; + }; + required = lib.mkOption { + type = lib.types.bool; + description = "Whether a page should be rendered, even when there are no visible options."; + default = config.description != ""; + defaultText = lib.literalMD '' + true when `description` is not empty + ''; + }; + 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; + }; + }; + + config = + let + drvName = lib.replaceStrings [ "/" ] [ "-" ] name; + markdown = pkgs.callPackage ./render-page.nix { + inherit (config) title description optionsJSON; + name = drvName; + }; + # 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 + { + enable = lib.mkDefault (config.required || config.optionsList != [ ]); + source = if config.required || config.optionsList != [ ] then markdown else null; + optionsJSON = builtins.toFile "options-${drvName}.json" ( + builtins.unsafeDiscardStringContext (builtins.toJSON optionsSet) + ); + }; + }; + + 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. + ''; + }; + menu.section = lib.mkOption { + default = "options"; + }; + }; + config.optionsList = optionsLists.${name} or [ ]; + } + ); + + 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 = { + options = lib.mkOption { + type = with lib.types; lazyAttrsOf optionsPageType; + description = '' + A set of option scopes to include in the docs. + ''; + default = { }; + apply = checkDocs; + }; + }; + config.docs = { + # Register for inclusion in `all` + _allInputs = [ "options" ]; + _utils = { + inherit optionsPageModule; + }; + }; +} diff --git a/modules/docs/platforms.nix b/modules/docs/platforms.nix new file mode 100644 index 0000000000..0494e9356c --- /dev/null +++ b/modules/docs/platforms.nix @@ -0,0 +1,101 @@ +{ + 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 = '' + The module containing platform-specific options. + ''; + }; + menu.section = lib.mkOption { + default = "platforms"; + }; + }; + config.optionsList = lib.pipe config.module [ + evalModule + (lib.getAttr "options") + mkOptionList + ]; + } + ); +in +{ + options.docs = { + platforms = lib.mkOption { + type = with lib.types; lazyAttrsOf platformPageType; + description = '' + A set of platform wrapper modules to include in the docs. + ''; + default = { }; + }; + }; + + config.docs = { + platforms = { + "platforms/nixos" = { + menu.location = [ + "platforms" + "NixOS" + ]; + module = ../../wrappers/modules/nixos.nix; + }; + "platforms/home-manager" = { + menu.location = [ + "platforms" + "home-manager" + ]; + module = ../../wrappers/modules/hm.nix; + }; + "platforms/nix-darwin" = { + menu.location = [ + "platforms" + "nix-darwin" + ]; + module = ../../wrappers/modules/darwin.nix; + }; + }; + files = { + "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; + }; + }; + # Register for inclusion in `all` + _allInputs = [ "platforms" ]; + }; +} diff --git a/modules/docs/readme.nix b/modules/docs/readme.nix new file mode 100644 index 0000000000..2b45f9524c --- /dev/null +++ b/modules/docs/readme.nix @@ -0,0 +1,72 @@ +{ + lib, + 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 = ""; + baseurl = "https://nix-community.github.io/nixvim/"; + src = ../../README.md; + } + '' + # extract relevant section of the README + sed -n "/$start/,/$end/p" $src > $out + # replace absolute links + substituteInPlace $out --replace-quiet "$baseurl" "./" + # TODO: replace .html with .md + ''; + + 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)" + '' diff --git a/modules/docs/render-page.nix b/modules/docs/render-page.nix new file mode 100644 index 0000000000..785b5524b2 --- /dev/null +++ b/modules/docs/render-page.nix @@ -0,0 +1,43 @@ +{ + path, + nixos-render-docs, + runCommand, + name, + title, + description ? "", + optionsJSON, + revision ? "", +}: +runCommand "page-${name}.md" + { + inherit + title + description + 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 "# $title" + echo + echo "$description" + echo + cat options.md + ) > $out + '' diff --git a/modules/docs/src.nix b/modules/docs/src.nix new file mode 100644 index 0000000000..1f1dd0fbc2 --- /dev/null +++ b/modules/docs/src.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/user-configs.nix b/modules/docs/user-configs.nix new file mode 100644 index 0000000000..c3ccee7a70 --- /dev/null +++ b/modules/docs/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)" + '' diff --git a/modules/plugins.nix b/modules/plugins.nix index a0d0a67427..ab5a191f08 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.options = + 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 6fdfa80bf1d704ed16a658c182bf164b85563d3a Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Mon, 27 Jan 2025 19:40:40 +0000 Subject: [PATCH 4/5] lib/plugins: define docs description for plugins --- lib/plugins/utils.nix | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/plugins/utils.nix b/lib/plugins/utils.nix index b687e60a1d..204c33f3ce 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,24 @@ path = loc; }; }; + + docs.options.${docsfile} = { + title = lib.last loc; + description = lib.concatLines ( + [ + "**URL:** [${url}](${url})" + "" + ] + ++ lib.optionals (maintainers != [ ]) [ + "**Maintainers:** ${maintainersString}" + "" + ] + ++ lib.optionals (description != null && description != "") [ + "---" + "" + description + ] + ); + }; }; } From fc972cb8b6998d1658ee4abbd26bbbca699755ae Mon Sep 17 00:00:00 2001 From: Matt Sturgeon Date: Sat, 25 Jan 2025 19:46:06 +0000 Subject: [PATCH 5/5] 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 2c2e6775f1..c054d3a862 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 @@ -371,6 +372,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; @@ -401,6 +406,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" { @@ -430,6 +440,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 { }; };