nvim-doc-reader 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. nvim_doc_reader-0.1.0/PKG-INFO +125 -0
  2. nvim_doc_reader-0.1.0/README.md +114 -0
  3. nvim_doc_reader-0.1.0/pyproject.toml +28 -0
  4. nvim_doc_reader-0.1.0/setup.cfg +4 -0
  5. nvim_doc_reader-0.1.0/src/doc_reader/__init__.py +1 -0
  6. nvim_doc_reader-0.1.0/src/doc_reader/cli.py +109 -0
  7. nvim_doc_reader-0.1.0/src/doc_reader/config.py +124 -0
  8. nvim_doc_reader-0.1.0/src/doc_reader/model.py +281 -0
  9. nvim_doc_reader-0.1.0/src/doc_reader/nvim/lua/doc_reader.lua +43 -0
  10. nvim_doc_reader-0.1.0/src/doc_reader/nvim.py +215 -0
  11. nvim_doc_reader-0.1.0/src/doc_reader/serve.py +235 -0
  12. nvim_doc_reader-0.1.0/src/doc_reader/skills/doc-authoring/SKILL.md +64 -0
  13. nvim_doc_reader-0.1.0/src/doc_reader/static/reader.css +167 -0
  14. nvim_doc_reader-0.1.0/src/doc_reader/static/reader.js +485 -0
  15. nvim_doc_reader-0.1.0/src/doc_reader/urls.py +87 -0
  16. nvim_doc_reader-0.1.0/src/nvim_doc_reader.egg-info/PKG-INFO +125 -0
  17. nvim_doc_reader-0.1.0/src/nvim_doc_reader.egg-info/SOURCES.txt +24 -0
  18. nvim_doc_reader-0.1.0/src/nvim_doc_reader.egg-info/dependency_links.txt +1 -0
  19. nvim_doc_reader-0.1.0/src/nvim_doc_reader.egg-info/entry_points.txt +3 -0
  20. nvim_doc_reader-0.1.0/src/nvim_doc_reader.egg-info/requires.txt +6 -0
  21. nvim_doc_reader-0.1.0/src/nvim_doc_reader.egg-info/top_level.txt +1 -0
  22. nvim_doc_reader-0.1.0/tests/test_model.py +101 -0
  23. nvim_doc_reader-0.1.0/tests/test_nvim.py +179 -0
  24. nvim_doc_reader-0.1.0/tests/test_serve.py +72 -0
  25. nvim_doc_reader-0.1.0/tests/test_skill.py +23 -0
  26. nvim_doc_reader-0.1.0/tests/test_urls_config.py +89 -0
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: nvim-doc-reader
3
+ Version: 0.1.0
4
+ Summary: Keyboard-driven HTML reader for Markdown docs, with jumps into running Neovim
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: markdown-it-py<3,>=2.1
8
+ Requires-Dist: mdit-py-plugins<0.4,>=0.3.4
9
+ Requires-Dist: aiohttp>=3.8
10
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
11
+
12
+ # doc-reader
13
+
14
+ A keyboard-driven HTML reader for Markdown documents, with jumps into the
15
+ Neovim instances you are editing them in. Markdown stays the only source: the
16
+ server renders it on every request, and the page follows edits live.
17
+
18
+ - Nested, foldable sections (`zM` to an outline, `l` to drill in), Vim keys.
19
+ - Math (KaTeX), Mermaid diagrams, SVG/PNG figures, tables.
20
+ - `n` moves every running Neovim that has the file to the section you are
21
+ reading; `nvimd` opens it when none has.
22
+ - `q` copies a question prompt for a coding agent; `y` copies a link.
23
+ - `<leader>h` in Neovim prints the reader URL of the section under the cursor.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install nvim-doc-reader
29
+ docread serve
30
+ ```
31
+
32
+ Requires Python 3.9+ and, for the editor features, Neovim 0.9+.
33
+
34
+ ## Configure
35
+
36
+ `$XDG_CONFIG_HOME/doc-reader/config.toml` (or `$DOCREADER_CONFIG`):
37
+
38
+ ```toml
39
+ # Optional, with their defaults.
40
+ bind = "0.0.0.0"
41
+ port = 8765
42
+ public_url = "http://desknikos:8765" # what `docread url` prints; default http://localhost:<port>
43
+ nvim = "nvim"
44
+ # nvim_socket_glob: default is where Neovim puts its own socket
45
+ # ($XDG_RUNTIME_DIR/nvim.*.0, else $TMPDIR/nvim.<user>/*/nvim.*.0)
46
+ state_file = "$XDG_STATE_HOME/doc-reader/last"
47
+ poll_interval_s = 0.5
48
+
49
+ # Which directories are served, by name. The name is part of the URL:
50
+ # /efs/nikos/notes/a.md -> /v/nikos/notes/a.md
51
+ # Default: the home directory, as "home".
52
+ [roots]
53
+ nikos = "/efs/nikos"
54
+ ```
55
+
56
+ Every scalar key can be overridden with `DOCREADER_<KEY>`, and the roots with
57
+ `DOCREADER_ROOTS=name=/dir:name=/dir`. Nested roots are fine: a file belongs to
58
+ the most specific one, and any other URL for it redirects there.
59
+
60
+ Only files under a root are served. Raw files are served with
61
+ `Content-Security-Policy: sandbox`, and raw HTML in Markdown is shown as text.
62
+ There is no authentication: run it on a trusted network.
63
+
64
+ ## Keys
65
+
66
+ | key | action |
67
+ |---|---|
68
+ | `j` / `k` | next / previous heading |
69
+ | `h` / `l` | collapse or go to parent / expand or go to first child |
70
+ | `za` `zo` `zc` `zO` `zC` | toggle / open / close / open all inside / close all inside |
71
+ | `zM` / `zR` | close all / open all |
72
+ | `gg` / `G`, `Ctrl-d` / `Ctrl-u` | top / bottom, half page |
73
+ | `Tab`, `Enter` | focus and follow links |
74
+ | `n` | open in Neovim |
75
+ | `q` | copy a question prompt |
76
+ | `y` | copy the section's link |
77
+ | `o` | show / hide the outline |
78
+ | `?` | help |
79
+
80
+ ## Neovim
81
+
82
+ `n` needs nothing in the editor: every Neovim listens on its own socket, and
83
+ the server finds them by `nvim_socket_glob`. Windows showing the file move to
84
+ the section; a buffer that is loaded but hidden moves when you next switch to
85
+ it. `nvimd` with no arguments opens the last target; with arguments it is
86
+ plain `nvim`.
87
+
88
+ For `<leader>h`:
89
+
90
+ ```lua
91
+ vim.opt.rtp:append(vim.fn.trim(vim.fn.system({ "docread", "nvim-path" })))
92
+ require("doc_reader").setup({ cmd = { "docread" }, keymap = "<leader>h" })
93
+ ```
94
+
95
+ ## Section IDs
96
+
97
+ A heading's ID is its explicit `{#id}` if it has one, otherwise GitHub's slug
98
+ of its text, with `-1`, `-2` for repeats. Give a section an explicit ID when
99
+ links to it must survive a rename.
100
+
101
+ ## Agents
102
+
103
+ Documents written by coding agents read best when they follow a few rules:
104
+ stable `{#id}`s, short overviews with depth in nested subsections, figures
105
+ kept with their source, no fabricated data, and questions answered without
106
+ editing. The rules ship as a Claude Code skill:
107
+
108
+ ```bash
109
+ docread install-skill # -> ~/.claude/skills/doc-authoring/
110
+ docread install-skill --dir .claude/skills # or per project
111
+ ```
112
+
113
+ Then tell agents to use it, in your global `~/.claude/CLAUDE.md` (or a
114
+ project's `CLAUDE.md`):
115
+
116
+ ```markdown
117
+ ## Writing Documents
118
+
119
+ Markdown documents you write for the user (research notes, reports, plans,
120
+ explainers) follow the `doc-authoring` skill — load it before creating or
121
+ editing one.
122
+ ```
123
+
124
+ Other agents: if they read `SKILL.md` skills, point `--dir` at their skills
125
+ directory; otherwise paste the skill's body into `AGENTS.md` / `GEMINI.md`.
@@ -0,0 +1,114 @@
1
+ # doc-reader
2
+
3
+ A keyboard-driven HTML reader for Markdown documents, with jumps into the
4
+ Neovim instances you are editing them in. Markdown stays the only source: the
5
+ server renders it on every request, and the page follows edits live.
6
+
7
+ - Nested, foldable sections (`zM` to an outline, `l` to drill in), Vim keys.
8
+ - Math (KaTeX), Mermaid diagrams, SVG/PNG figures, tables.
9
+ - `n` moves every running Neovim that has the file to the section you are
10
+ reading; `nvimd` opens it when none has.
11
+ - `q` copies a question prompt for a coding agent; `y` copies a link.
12
+ - `<leader>h` in Neovim prints the reader URL of the section under the cursor.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pip install nvim-doc-reader
18
+ docread serve
19
+ ```
20
+
21
+ Requires Python 3.9+ and, for the editor features, Neovim 0.9+.
22
+
23
+ ## Configure
24
+
25
+ `$XDG_CONFIG_HOME/doc-reader/config.toml` (or `$DOCREADER_CONFIG`):
26
+
27
+ ```toml
28
+ # Optional, with their defaults.
29
+ bind = "0.0.0.0"
30
+ port = 8765
31
+ public_url = "http://desknikos:8765" # what `docread url` prints; default http://localhost:<port>
32
+ nvim = "nvim"
33
+ # nvim_socket_glob: default is where Neovim puts its own socket
34
+ # ($XDG_RUNTIME_DIR/nvim.*.0, else $TMPDIR/nvim.<user>/*/nvim.*.0)
35
+ state_file = "$XDG_STATE_HOME/doc-reader/last"
36
+ poll_interval_s = 0.5
37
+
38
+ # Which directories are served, by name. The name is part of the URL:
39
+ # /efs/nikos/notes/a.md -> /v/nikos/notes/a.md
40
+ # Default: the home directory, as "home".
41
+ [roots]
42
+ nikos = "/efs/nikos"
43
+ ```
44
+
45
+ Every scalar key can be overridden with `DOCREADER_<KEY>`, and the roots with
46
+ `DOCREADER_ROOTS=name=/dir:name=/dir`. Nested roots are fine: a file belongs to
47
+ the most specific one, and any other URL for it redirects there.
48
+
49
+ Only files under a root are served. Raw files are served with
50
+ `Content-Security-Policy: sandbox`, and raw HTML in Markdown is shown as text.
51
+ There is no authentication: run it on a trusted network.
52
+
53
+ ## Keys
54
+
55
+ | key | action |
56
+ |---|---|
57
+ | `j` / `k` | next / previous heading |
58
+ | `h` / `l` | collapse or go to parent / expand or go to first child |
59
+ | `za` `zo` `zc` `zO` `zC` | toggle / open / close / open all inside / close all inside |
60
+ | `zM` / `zR` | close all / open all |
61
+ | `gg` / `G`, `Ctrl-d` / `Ctrl-u` | top / bottom, half page |
62
+ | `Tab`, `Enter` | focus and follow links |
63
+ | `n` | open in Neovim |
64
+ | `q` | copy a question prompt |
65
+ | `y` | copy the section's link |
66
+ | `o` | show / hide the outline |
67
+ | `?` | help |
68
+
69
+ ## Neovim
70
+
71
+ `n` needs nothing in the editor: every Neovim listens on its own socket, and
72
+ the server finds them by `nvim_socket_glob`. Windows showing the file move to
73
+ the section; a buffer that is loaded but hidden moves when you next switch to
74
+ it. `nvimd` with no arguments opens the last target; with arguments it is
75
+ plain `nvim`.
76
+
77
+ For `<leader>h`:
78
+
79
+ ```lua
80
+ vim.opt.rtp:append(vim.fn.trim(vim.fn.system({ "docread", "nvim-path" })))
81
+ require("doc_reader").setup({ cmd = { "docread" }, keymap = "<leader>h" })
82
+ ```
83
+
84
+ ## Section IDs
85
+
86
+ A heading's ID is its explicit `{#id}` if it has one, otherwise GitHub's slug
87
+ of its text, with `-1`, `-2` for repeats. Give a section an explicit ID when
88
+ links to it must survive a rename.
89
+
90
+ ## Agents
91
+
92
+ Documents written by coding agents read best when they follow a few rules:
93
+ stable `{#id}`s, short overviews with depth in nested subsections, figures
94
+ kept with their source, no fabricated data, and questions answered without
95
+ editing. The rules ship as a Claude Code skill:
96
+
97
+ ```bash
98
+ docread install-skill # -> ~/.claude/skills/doc-authoring/
99
+ docread install-skill --dir .claude/skills # or per project
100
+ ```
101
+
102
+ Then tell agents to use it, in your global `~/.claude/CLAUDE.md` (or a
103
+ project's `CLAUDE.md`):
104
+
105
+ ```markdown
106
+ ## Writing Documents
107
+
108
+ Markdown documents you write for the user (research notes, reports, plans,
109
+ explainers) follow the `doc-authoring` skill — load it before creating or
110
+ editing one.
111
+ ```
112
+
113
+ Other agents: if they read `SKILL.md` skills, point `--dir` at their skills
114
+ directory; otherwise paste the skill's body into `AGENTS.md` / `GEMINI.md`.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nvim-doc-reader"
7
+ version = "0.1.0"
8
+ description = "Keyboard-driven HTML reader for Markdown docs, with jumps into running Neovim"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "markdown-it-py>=2.1,<3",
13
+ # 0.3.x is the line that supports markdown-it-py 2.x. Heading {#id} is our
14
+ # own rule, not the attrs plugin, which does not handle it in 0.3.
15
+ "mdit-py-plugins>=0.3.4,<0.4",
16
+ "aiohttp>=3.8",
17
+ "tomli>=2.0; python_version < '3.11'",
18
+ ]
19
+
20
+ [project.scripts]
21
+ docread = "doc_reader.cli:main"
22
+ nvimd = "doc_reader.cli:nvimd_main"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["src"]
26
+
27
+ [tool.setuptools.package-data]
28
+ doc_reader = ["static/*", "nvim/lua/*.lua", "skills/*/*.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """doc-reader: keyboard-driven HTML reader for Markdown, with Neovim jumps."""
@@ -0,0 +1,109 @@
1
+ """`docread` (serve, url, section, nvim-path) and `nvimd`."""
2
+ import argparse
3
+ import os
4
+ import sys
5
+ from importlib import resources
6
+
7
+ from doc_reader import config, model
8
+ from doc_reader.urls import Roots
9
+
10
+
11
+ def _read(path, use_stdin):
12
+ if use_stdin:
13
+ return sys.stdin.read()
14
+ with open(path, encoding="utf-8") as f:
15
+ return f.read()
16
+
17
+
18
+ def cmd_serve(cfg, args):
19
+ from doc_reader import serve # aiohttp only when serving
20
+ serve.serve(cfg)
21
+
22
+
23
+ def cmd_url(cfg, args):
24
+ """The reader URL of the section containing `line` -- computed from the
25
+ buffer text when it is piped in, so unsaved edits are accounted for."""
26
+ url = Roots(cfg.roots).url_path(args.path)
27
+ if url is None:
28
+ print(f"{args.path} is not under any root: {sorted(cfg.roots)}",
29
+ file=sys.stderr)
30
+ return 1
31
+ sec = model.section_at(model.parse(_read(args.path, args.stdin)), args.line)
32
+ print(cfg.base_url + url + (f"#{sec.id}" if sec else ""))
33
+ return 0
34
+
35
+
36
+ def cmd_section(cfg, args):
37
+ """The 1-based line of section `id` in the text (stdin or file), or exit 1."""
38
+ line = model.line_of(model.parse(_read(args.path, args.stdin)), args.id)
39
+ if line is None:
40
+ return 1
41
+ print(line)
42
+ return 0
43
+
44
+
45
+ def cmd_nvim_path(cfg, args):
46
+ print(resources.files("doc_reader").joinpath("nvim"))
47
+ return 0
48
+
49
+
50
+ def cmd_install_skill(cfg, args):
51
+ """Copy the bundled agent skill(s) into a skills directory."""
52
+ dest = os.path.expanduser(args.dir)
53
+ src = resources.files("doc_reader").joinpath("skills")
54
+ for skill in src.iterdir():
55
+ if not skill.is_dir():
56
+ continue
57
+ os.makedirs(os.path.join(dest, skill.name), exist_ok=True)
58
+ for f in skill.iterdir():
59
+ out = os.path.join(dest, skill.name, f.name)
60
+ with open(out, "w", encoding="utf-8") as fh:
61
+ fh.write(f.read_text(encoding="utf-8"))
62
+ print(out)
63
+ return 0
64
+
65
+
66
+ def main(argv=None):
67
+ p = argparse.ArgumentParser(prog="docread", description=__doc__)
68
+ p.add_argument("--config", help="config file (default: $DOCREADER_CONFIG or "
69
+ "$XDG_CONFIG_HOME/doc-reader/config.toml)")
70
+ sub = p.add_subparsers(dest="cmd", required=True)
71
+ s = sub.add_parser("serve", help="run the reader server")
72
+ s.add_argument("--port", type=int)
73
+ s.add_argument("--bind")
74
+ s.add_argument("--root", action="append", metavar="NAME=DIR",
75
+ help="replace the configured roots (repeatable)")
76
+ s.set_defaults(fn=cmd_serve)
77
+ s = sub.add_parser("url", help="reader URL for a file and line")
78
+ s.add_argument("path")
79
+ s.add_argument("line", type=int, nargs="?", default=1)
80
+ s.add_argument("--stdin", action="store_true", help="read the text from stdin")
81
+ s.set_defaults(fn=cmd_url)
82
+ s = sub.add_parser("section", help="line of a section id")
83
+ s.add_argument("path")
84
+ s.add_argument("id")
85
+ s.add_argument("--stdin", action="store_true")
86
+ s.set_defaults(fn=cmd_section)
87
+ s = sub.add_parser("nvim-path", help="directory to add to Neovim's runtimepath")
88
+ s.set_defaults(fn=cmd_nvim_path)
89
+ s = sub.add_parser("install-skill",
90
+ help="install the doc-authoring agent skill")
91
+ s.add_argument("--dir", default="~/.claude/skills",
92
+ help="skills directory (default: ~/.claude/skills)")
93
+ s.set_defaults(fn=cmd_install_skill)
94
+ args = p.parse_args(argv)
95
+ overrides = {"port": getattr(args, "port", None),
96
+ "bind": getattr(args, "bind", None)}
97
+ if getattr(args, "root", None):
98
+ overrides["roots"] = config.parse_roots(":".join(args.root))
99
+ cfg = config.load(args.config, overrides=overrides)
100
+ return args.fn(cfg, args) or 0
101
+
102
+
103
+ def nvimd_main(argv=None):
104
+ from doc_reader import nvim
105
+ return nvim.nvimd(config.load(), sys.argv[1:] if argv is None else argv)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ sys.exit(main())
@@ -0,0 +1,124 @@
1
+ """Settings: defaults, then the TOML file, then DOCREADER_* env, then CLI flags.
2
+
3
+ Nothing here names a host or a directory of ours. The defaults are the
4
+ generic ones (XDG dirs, the home directory as the only root), and a site
5
+ config supplies the rest.
6
+ """
7
+ import os
8
+ import pwd
9
+ import sys
10
+ import tempfile
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+
14
+ if sys.version_info >= (3, 11):
15
+ import tomllib
16
+ else:
17
+ import tomli as tomllib
18
+
19
+
20
+ def _xdg(var, fallback):
21
+ return os.environ.get(var) or os.path.expanduser(fallback)
22
+
23
+
24
+ def _runtime_dir():
25
+ if os.environ.get("XDG_RUNTIME_DIR"):
26
+ return os.environ["XDG_RUNTIME_DIR"]
27
+ run = f"/run/user/{os.getuid()}"
28
+ return run if os.path.isdir(run) else "/tmp"
29
+
30
+
31
+ def default_socket_glob():
32
+ """Where Neovim puts the socket it opens on startup: its stdpath("run"),
33
+ which is $XDG_RUNTIME_DIR or else a private random directory under the
34
+ temp dir (e.g. /tmp/nvim.root/8CVDuf/nvim.3950.0)."""
35
+ if os.environ.get("XDG_RUNTIME_DIR"):
36
+ return os.path.join(os.environ["XDG_RUNTIME_DIR"], "nvim.*.0")
37
+ user = pwd.getpwuid(os.getuid()).pw_name
38
+ return os.path.join(tempfile.gettempdir(), f"nvim.{user}", "*", "nvim.*.0")
39
+
40
+
41
+ def expand(value):
42
+ """`~` and `$VAR`, with the XDG vars filled in when the environment lacks
43
+ them, so `$XDG_STATE_HOME/...` never expands to a path starting at `/`."""
44
+ env = dict(os.environ)
45
+ env.setdefault("XDG_CONFIG_HOME", _xdg("XDG_CONFIG_HOME", "~/.config"))
46
+ env.setdefault("XDG_STATE_HOME", _xdg("XDG_STATE_HOME", "~/.local/state"))
47
+ env.setdefault("XDG_RUNTIME_DIR", _runtime_dir())
48
+ out = os.path.expanduser(str(value))
49
+ for k in sorted(env, key=len, reverse=True):
50
+ out = out.replace("${" + k + "}", env[k]).replace("$" + k, env[k])
51
+ return out
52
+
53
+
54
+ @dataclass
55
+ class Config:
56
+ roots: dict = field(default_factory=lambda: {"home": os.path.expanduser("~")})
57
+ bind: str = "0.0.0.0"
58
+ port: int = 8765
59
+ public_url: str = ""
60
+ nvim: str = "nvim"
61
+ nvim_socket_glob: str = "" # empty: default_socket_glob()
62
+ state_file: str = "$XDG_STATE_HOME/doc-reader/last"
63
+ poll_interval_s: float = 0.5
64
+
65
+ @property
66
+ def base_url(self):
67
+ return (self.public_url or f"http://localhost:{self.port}").rstrip("/")
68
+
69
+
70
+ SCALARS = {"bind": str, "port": int, "public_url": str, "nvim": str,
71
+ "nvim_socket_glob": str,
72
+ "state_file": str, "poll_interval_s": float}
73
+
74
+
75
+ def config_path():
76
+ if os.environ.get("DOCREADER_CONFIG"):
77
+ return Path(expand(os.environ["DOCREADER_CONFIG"]))
78
+ return Path(expand("$XDG_CONFIG_HOME/doc-reader/config.toml"))
79
+
80
+
81
+ def parse_roots(spec):
82
+ """`name=/dir:name=/dir` (the DOCREADER_ROOTS form)."""
83
+ roots = {}
84
+ for part in filter(None, spec.split(":")):
85
+ name, sep, path = part.partition("=")
86
+ if not sep or not name or not path:
87
+ raise ValueError(f"bad root {part!r}: expected name=/dir")
88
+ roots[name] = path
89
+ return roots
90
+
91
+
92
+ def load(path=None, env=None, overrides=None):
93
+ env = os.environ if env is None else env
94
+ cfg = Config()
95
+ path = Path(path) if path else config_path()
96
+ data = {}
97
+ if path.exists():
98
+ with open(path, "rb") as f:
99
+ data = tomllib.load(f)
100
+ unknown = set(data) - set(SCALARS) - {"roots"}
101
+ if unknown:
102
+ raise ValueError(f"{path}: unknown keys {sorted(unknown)}")
103
+ for key, kind in SCALARS.items():
104
+ if key in data:
105
+ setattr(cfg, key, kind(data[key]))
106
+ if env.get(f"DOCREADER_{key.upper()}"):
107
+ setattr(cfg, key, kind(env[f"DOCREADER_{key.upper()}"]))
108
+ if "roots" in data:
109
+ if not isinstance(data["roots"], dict):
110
+ raise ValueError(f"{path}: [roots] must be a table of name = \"/dir\"")
111
+ cfg.roots = dict(data["roots"])
112
+ if env.get("DOCREADER_ROOTS"):
113
+ cfg.roots = parse_roots(env["DOCREADER_ROOTS"])
114
+ for key, value in (overrides or {}).items():
115
+ if value is not None:
116
+ setattr(cfg, key, value)
117
+ for name in cfg.roots:
118
+ if "/" in name or not name:
119
+ raise ValueError(f"root name {name!r} must be a single path segment")
120
+ cfg.roots = {k: os.path.realpath(expand(v)) for k, v in cfg.roots.items()}
121
+ cfg.nvim_socket_glob = (expand(cfg.nvim_socket_glob) if cfg.nvim_socket_glob
122
+ else default_socket_glob())
123
+ cfg.state_file = os.path.abspath(expand(cfg.state_file))
124
+ return cfg