diff --git a/docs/default.nix b/docs/default.nix index 9c75ca6a..b2b0d103 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -253,9 +253,15 @@ let # Generate the HTML manual pages home-manager-manual = pkgs.callPackage ./home-manager-manual.nix { home-manager-options = { - home-manager = hmOptionsDocs.optionsJSON; - nixos = nixosOptionsDocs.optionsJSON; - nix-darwin = nixDarwinOptionsDocs.optionsJSON; + home-manager = { + json = hmOptionsDocs.optionsJSON; + }; + nixos = { + json = nixosOptionsDocs.optionsJSON; + }; + nix-darwin = { + json = nixDarwinOptionsDocs.optionsJSON; + }; }; inherit revision; }; diff --git a/docs/home-manager-manual.nix b/docs/home-manager-manual.nix index c48cf88e..fc6ab660 100644 --- a/docs/home-manager-manual.nix +++ b/docs/home-manager-manual.nix @@ -1,63 +1,99 @@ { stdenv, lib, - documentation-highlighter, + callPackage, + mdbook, + python3, revision, home-manager-options, - nixos-render-docs, }: let outputPath = "share/doc/home-manager"; + mdbookOptions = callPackage ./mdbook/options.nix { + manpageUrls = ./manual/manpage-urls.json; + inherit revision; + optionDocs = { + home-manager = { + title = "Home Manager Configuration Options"; + path = "home-manager"; + prefix = "opt-"; + json = "${home-manager-options.home-manager.json}"; + }; + nixos = { + title = "NixOS Configuration Options"; + path = "nixos"; + prefix = "nixos-opt-"; + json = "${home-manager-options.nixos.json}"; + }; + nix-darwin = { + title = "nix-darwin Configuration Options"; + path = "nix-darwin"; + prefix = "nix-darwin-opt-"; + json = "${home-manager-options.nix-darwin.json}"; + }; + }; + }; in stdenv.mkDerivation { name = "home-manager-manual"; - nativeBuildInputs = [ nixos-render-docs ]; + nativeBuildInputs = [ + mdbook + python3 + ]; - src = ./manual; + src = ./.; buildPhase = '' - mkdir -p out/{highlightjs,media} + runHook preBuild - cp -t out/highlightjs \ - ${documentation-highlighter}/highlight.pack.js \ - ${documentation-highlighter}/LICENSE \ - ${documentation-highlighter}/mono-blue.css \ - ${documentation-highlighter}/loader.js + mkdir -p source + python3 ${./mdbook/convert-markup.py} "$src/manual" source + python3 ${./mdbook/convert-markup.py} \ + --base-depth 1 \ + "$src/release-notes" \ + source/release-notes - substituteInPlace ./options.md \ - --subst-var-by \ - OPTIONS_JSON \ - ${home-manager-options.home-manager}/share/doc/nixos/options.json + cp -r ${mdbookOptions}/options source/options - substituteInPlace ./nixos-options.md \ - --subst-var-by \ - OPTIONS_JSON \ - ${home-manager-options.nixos}/share/doc/nixos/options.json + python3 ${./mdbook/substitute-summary.py} \ + source/SUMMARY.md \ + ${mdbookOptions}/summary/home-manager.md \ + ${mdbookOptions}/summary/nixos.md \ + ${mdbookOptions}/summary/nix-darwin.md - substituteInPlace ./nix-darwin-options.md \ - --subst-var-by \ - OPTIONS_JSON \ - ${home-manager-options.nix-darwin}/share/doc/nixos/options.json + mdbook build source --dest-dir book - cp ${./options.html} out/options.html + mkdir -p out + cp -r book/* out/ - cp ${./static/style.css} out/style.css - cp ${./static/anchor-links.js} out/anchor-links.js + makeRedirect() { + local target=$1 + local destination=$2 + printf '%s\n' \ + '' \ + '' \ + ' ' \ + ' ' \ + " " \ + " " \ + ' Redirecting...' \ + ' ' \ + ' ' \ + "

Redirecting to $destination.

" \ + ' ' \ + '' \ + > "out/$target" + } - cp -r ${./release-notes} release-notes + makeRedirect index.xhtml index.html + makeRedirect options.html options/home-manager/index.html + makeRedirect options.xhtml options/home-manager/index.html + makeRedirect nixos-options.xhtml options/nixos/index.html + makeRedirect nix-darwin-options.xhtml options/nix-darwin/index.html + makeRedirect release-notes.xhtml release-notes/release-notes.html - nixos-render-docs manual html \ - --manpage-urls ./manpage-urls.json \ - --revision ${lib.trivial.revisionWithDefault revision} \ - --stylesheet style.css \ - --script highlightjs/highlight.pack.js \ - --script highlightjs/loader.js \ - --script anchor-links.js \ - --toc-depth 1 \ - --section-toc-depth 1 \ - manual.md \ - out/index.xhtml + runHook postBuild ''; installPhase = '' @@ -69,7 +105,9 @@ stdenv.mkDerivation { echo "doc manual $dest index.html" >> $out/nix-support/hydra-build-products ''; - passthru = { inherit home-manager-options; }; + passthru = { + inherit home-manager-options mdbookOptions; + }; meta = { maintainers = [ lib.maintainers.considerate ]; diff --git a/docs/html-open-tool.nix b/docs/html-open-tool.nix index 279a8a06..f8e97adf 100644 --- a/docs/html-open-tool.nix +++ b/docs/html-open-tool.nix @@ -25,9 +25,14 @@ let if [[ ! -v BROWSER || -z $BROWSER ]]; then echo "$0: unable to start a web browser; please set \$BROWSER" exit 1 - else - exec "$BROWSER" "${html}/share/doc/${pathName}/index.xhtml" fi + + manualPath="${html}/share/doc/${pathName}/index.html" + if [[ ! -e $manualPath ]]; then + manualPath="${html}/share/doc/${pathName}/index.xhtml" + fi + + exec "$BROWSER" "$manualPath" ''; desktopItem = makeDesktopItem { diff --git a/docs/manual/SUMMARY.md b/docs/manual/SUMMARY.md new file mode 100644 index 00000000..27971e5a --- /dev/null +++ b/docs/manual/SUMMARY.md @@ -0,0 +1,64 @@ +# Summary + +- [Preface](preface.md) +- [Introduction to Home Manager](introduction.md) +- [Installing Home Manager](installation.md) + - [Standalone installation](installation/standalone.md) + - [NixOS module](installation/nixos.md) + - [nix-darwin module](installation/nix-darwin.md) +- [Using Home Manager](usage.md) + - [Configuration Example](usage/configuration.md) + - [Rollbacks](usage/rollbacks.md) + - [Keeping your ~ safe from harm](usage/dotfiles.md) + - [Graphical services](usage/graphical.md) + - [GPU on non-NixOS systems](usage/gpu-non-nixos.md) + - [Modular Services](usage/modular-services.md) + - [Updating](usage/updating.md) + - [Upgrading to a new Home Manager release](usage/upgrading.md) +- [Nix Flakes](nix-flakes.md) + - [Prerequisites](nix-flakes/prerequisites.md) + - [Standalone setup](nix-flakes/standalone.md) + - [NixOS module](nix-flakes/nixos.md) + - [nix-darwin module](nix-flakes/nix-darwin.md) + - [flake-parts module](nix-flakes/flake-parts.md) +- [Writing Home Manager Modules](writing-modules.md) + - [Option Types](writing-modules/types.md) +- [Contributing](contributing.md) + - [Getting started](contributing/getting-started.md) + - [Guidelines](contributing/guidelines.md) + - [Release Notes](contributing/release-notes.md) + - [News](contributing/news.md) + - [Tests](contributing/tests.md) +- [Home Manager Internals](internals.md) + - [Activation](internals/activation.md) +- [Third-Party Tools and Extensions](3rd-party.md) + - [Module Collections](3rd-party/collections.md) +- [Frequently Asked Questions (FAQ)](faq.md) + - [Why is there a collision error when switching generation?](faq/collision.md) + - [Why are the session variables not set?](faq/session-variables.md) + - [How to set up a configuration for multiple users/machines?](faq/multiple-users-machines.md) + - [Why do I get an error message about `ca.desrt.dconf` or `dconf.service`?](faq/ca-desrt-dconf.md) + - [How do I install packages from Nixpkgs unstable?](faq/unstable.md) + - [How do I change the package used by a module?](faq/change-package-module.md) +- [Options](options.md) +@HOME_MANAGER_OPTIONS@ +@NIXOS_OPTIONS@ +@NIX_DARWIN_OPTIONS@ +- [Release Notes](release-notes/release-notes.md) + - [Release 26.11](release-notes/rl-2611.md) + - [Release 26.05](release-notes/rl-2605.md) + - [Release 25.11](release-notes/rl-2511.md) + - [Release 25.05](release-notes/rl-2505.md) + - [Release 24.11](release-notes/rl-2411.md) + - [Release 24.05](release-notes/rl-2405.md) + - [Release 23.11](release-notes/rl-2311.md) + - [Release 23.05](release-notes/rl-2305.md) + - [Release 22.11](release-notes/rl-2211.md) + - [Release 22.05](release-notes/rl-2205.md) + - [Release 21.11](release-notes/rl-2111.md) + - [Release 21.05](release-notes/rl-2105.md) + - [Release 20.09](release-notes/rl-2009.md) + - [Release 20.03](release-notes/rl-2003.md) + - [Release 19.09](release-notes/rl-1909.md) + - [Release 19.03](release-notes/rl-1903.md) + - [Release 18.09](release-notes/rl-1809.md) diff --git a/docs/manual/book.toml b/docs/manual/book.toml new file mode 100644 index 00000000..5cccaffe --- /dev/null +++ b/docs/manual/book.toml @@ -0,0 +1,11 @@ +[book] +title = "Home Manager Manual" +language = "en" +src = "." + +[output.html] +git-repository-url = "https://github.com/nix-community/home-manager" + +[output.html.fold] +enable = true +level = 0 diff --git a/docs/manual/contributing/guidelines.md b/docs/manual/contributing/guidelines.md index ecec4da9..44b9dd9e 100644 --- a/docs/manual/contributing/guidelines.md +++ b/docs/manual/contributing/guidelines.md @@ -108,7 +108,7 @@ Manager Git repository: ``` shell $ nix-build -A docs.html -$ xdg-open ./result/share/doc/home-manager/index.xhtml +$ xdg-open ./result/share/doc/home-manager/index.html ``` When you have made changes to a module, it is a good idea to check that diff --git a/docs/manual/usage/upgrading.md b/docs/manual/usage/upgrading.md index f4e89927..1f318145 100644 --- a/docs/manual/usage/upgrading.md +++ b/docs/manual/usage/upgrading.md @@ -16,7 +16,7 @@ use Home Manager's `master` branch. Home Manager follows NixOS release cycles and provides corresponding branches: -- **release-**: Stable branch for the matching NixOS or +- **release-\**: Stable branch for the matching NixOS or Nixpkgs release, such as `release-25.11`. - **master**: Development branch (tracks nixpkgs-unstable) diff --git a/docs/mdbook/convert-markup.py b/docs/mdbook/convert-markup.py new file mode 100644 index 00000000..0f5924c7 --- /dev/null +++ b/docs/mdbook/convert-markup.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import html +import re +import shutil +import sys +from pathlib import Path + + +SIMPLE_ROLES = ( + "command", + "component", + "description", + "file", + "index", + "system", + "type", +) + +HEADING_ANCHOR = re.compile(r"^(#{1,6}\s+)(.*)\s+\{#([^}]+)\}\s*$") +INLINE_ANCHOR = re.compile(r"\[\]\{#([^}]+)\}") +OPTION_ROLE = re.compile(r"(?[^\]]*)\]\(#(?P(?:opt|nixos-opt|nix-darwin-opt)-[^)]+)\)" +) +LEFTOVER_ROLE = re.compile( + r"(? str: + if anchor.startswith("nix-darwin-opt-"): + option = anchor.removeprefix("nix-darwin-opt-") + option = option.replace("<", "_").replace(">", "_") + anchor = f"nix-darwin-opt-{option}" + base = "options/nix-darwin" + elif anchor.startswith("nixos-opt-"): + option = anchor.removeprefix("nixos-opt-") + option = option.replace("<", "_").replace(">", "_") + anchor = f"nixos-opt-{option}" + base = "options/nixos" + else: + option = anchor.removeprefix("opt-") + option = option.replace("<", "_").replace(">", "_") + anchor = f"opt-{option}" + base = "options/home-manager" + + page_parts = option_page_parts(option) + prefix = "../" * (base_depth + len(current_file.parent.parts)) + return f"{prefix}{base}/{'/'.join(page_parts)}.md#{anchor}" + + +def option_page_parts(option_name: str) -> list[str]: + parts = option_name.split(".") + namespace = parts[0] + if namespace in DEEP_SPLIT_NAMESPACES and len(parts) > 1: + return parts[:2] + return [namespace] + + +def option_label(anchor: str) -> str: + if anchor.startswith("nix-darwin-opt-"): + return anchor.removeprefix("nix-darwin-opt-") + if anchor.startswith("nixos-opt-"): + return anchor.removeprefix("nixos-opt-") + return anchor.removeprefix("opt-") + + +def markdown_label(value: str) -> str: + return value.replace("<", "<").replace(">", ">") + + +def convert_inline(line: str, current_file: Path, base_depth: int) -> str: + line = line.replace("index.xhtml", "index.html") + line = INLINE_ANCHOR.sub( + lambda match: f'', + line, + ) + line = OPTION_ROLE.sub( + lambda match: ( + f"[{markdown_label(match.group(1))}]" + f"({option_target(f'opt-{match.group(1)}', current_file, base_depth)})" + ), + line, + ) + line = OPTION_LINK.sub( + lambda match: ( + f"[{markdown_label(match.group('label') or option_label(match.group('anchor')))}]" + f"({option_target(match.group('anchor'), current_file, base_depth)})" + ), + line, + ) + return SIMPLE_ROLE.sub(lambda match: f"`{match.group(2)}`", line) + + +def convert_heading(line: str, current_file: Path, base_depth: int) -> str: + match = HEADING_ANCHOR.match(line) + if match is None: + return convert_inline(line, current_file, base_depth) + + prefix, title, anchor = match.groups() + return ( + f'\n' + f"{prefix}{convert_inline(title, current_file, base_depth)}" + ) + + +def is_include_fence(line: str) -> tuple[bool, str]: + match = FENCE.match(line) + if match is None: + return False, "" + + info = match.group(2).strip() + return info.startswith(("{=include=}", "include")), match.group(1) + + +def convert_markdown( + text: str, + source: Path, + current_file: Path, + base_depth: int, +) -> str: + output: list[str] = [] + in_code_fence = False + code_fence = "" + in_include = False + include_fence = "" + in_admonition = False + + for raw_line in text.splitlines(keepends=True): + line = raw_line[:-1] if raw_line.endswith("\n") else raw_line + newline = "\n" if raw_line.endswith("\n") else "" + + if in_include: + if FENCE_CLOSE.match(line) and len(line.strip()) >= len(include_fence): + in_include = False + include_fence = "" + continue + + if in_code_fence: + output.append(raw_line) + if FENCE_CLOSE.match(line) and len(line.strip()) >= len(code_fence): + in_code_fence = False + code_fence = "" + continue + + include, fence = is_include_fence(line) + if include: + in_include = True + include_fence = fence + continue + + fence_match = FENCE.match(line) + if fence_match is not None: + in_code_fence = True + code_fence = fence_match.group(1) + output.append(raw_line) + continue + + if in_admonition: + if ADMONITION_CLOSE.match(line): + in_admonition = False + continue + converted = convert_inline(line, current_file, base_depth) + output.append(f"> {converted}{newline}" if converted else ">\n") + continue + + admonition = ADMONITION_OPEN.match(line) + if admonition is not None: + in_admonition = True + output.append(f"> **{admonition.group(1).title()}**\n") + continue + + output.append(convert_heading(line, current_file, base_depth) + newline) + + if in_include: + raise ValueError(f"{source}: unterminated include block") + + converted = "".join(output) + if LEFTOVER_ROLE.search(converted): + raise ValueError(f"{source}: unconverted NixOS-render-docs role remains") + if "```{=include=}" in converted: + raise ValueError(f"{source}: unconverted include block remains") + + return converted + + +def convert_tree(source: Path, destination: Path, base_depth: int) -> None: + for path in source.rglob("*"): + if not path.is_file(): + continue + + target = destination / path.relative_to(source) + target.parent.mkdir(parents=True, exist_ok=True) + + if path.suffix == ".md": + text = path.read_text(encoding="utf-8") + target.write_text( + convert_markdown(text, path, path.relative_to(source), base_depth), + encoding="utf-8", + ) + else: + shutil.copy2(path, target) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--base-depth", type=int, default=0) + parser.add_argument("source", type=Path) + parser.add_argument("destination", type=Path) + args = parser.parse_args() + + if not args.source.is_dir(): + print(f"missing source directory: {args.source}", file=sys.stderr) + return 1 + + args.destination.mkdir(parents=True, exist_ok=True) + convert_tree(args.source, args.destination, args.base_depth) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/mdbook/options.nix b/docs/mdbook/options.nix new file mode 100644 index 00000000..5d3321c2 --- /dev/null +++ b/docs/mdbook/options.nix @@ -0,0 +1,23 @@ +{ + pkgs, + optionDocs, + manpageUrls, + revision, +}: + +pkgs.runCommand "home-manager-mdbook-options" + { + nativeBuildInputs = [ + pkgs.buildPackages.nixos-render-docs + pkgs.buildPackages.python3 + ]; + optionDocsJson = builtins.toJSON optionDocs; + passAsFile = [ "optionDocsJson" ]; + } + '' + python3 ${./render-options.py} \ + "$optionDocsJsonPath" \ + ${manpageUrls} \ + ${revision} \ + "$out" + '' diff --git a/docs/mdbook/render-options.py b/docs/mdbook/render-options.py new file mode 100644 index 00000000..5fd4a28a --- /dev/null +++ b/docs/mdbook/render-options.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path + + +OPTION_LINK = re.compile( + r"\[(?P