mkdocstrings-nim 0.1.0__py3-none-any.whl

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 (99) hide show
  1. mkdocstrings_handlers/__init__.py +2 -0
  2. mkdocstrings_handlers/nim/__init__.py +27 -0
  3. mkdocstrings_handlers/nim/collector.py +267 -0
  4. mkdocstrings_handlers/nim/docstring.py +129 -0
  5. mkdocstrings_handlers/nim/extractor/extractor.nim +244 -0
  6. mkdocstrings_handlers/nim/extractor/nimdocinfo.nim +16 -0
  7. mkdocstrings_handlers/nim/handler.py +163 -0
  8. mkdocstrings_handlers/nim/templates/material/_base/attribute.html.jinja +128 -0
  9. mkdocstrings_handlers/nim/templates/material/_base/backlinks.html.jinja +66 -0
  10. mkdocstrings_handlers/nim/templates/material/_base/children.html.jinja +197 -0
  11. mkdocstrings_handlers/nim/templates/material/_base/class.html.jinja +288 -0
  12. mkdocstrings_handlers/nim/templates/material/_base/const.html.jinja +31 -0
  13. mkdocstrings_handlers/nim/templates/material/_base/docstring/admonition.html.jinja +20 -0
  14. mkdocstrings_handlers/nim/templates/material/_base/docstring/attributes.html.jinja +109 -0
  15. mkdocstrings_handlers/nim/templates/material/_base/docstring/classes.html.jinja +86 -0
  16. mkdocstrings_handlers/nim/templates/material/_base/docstring/examples.html.jinja +28 -0
  17. mkdocstrings_handlers/nim/templates/material/_base/docstring/functions.html.jinja +92 -0
  18. mkdocstrings_handlers/nim/templates/material/_base/docstring/modules.html.jinja +86 -0
  19. mkdocstrings_handlers/nim/templates/material/_base/docstring/other_parameters.html.jinja +109 -0
  20. mkdocstrings_handlers/nim/templates/material/_base/docstring/parameters.html.jinja +175 -0
  21. mkdocstrings_handlers/nim/templates/material/_base/docstring/raises.html.jinja +102 -0
  22. mkdocstrings_handlers/nim/templates/material/_base/docstring/receives.html.jinja +122 -0
  23. mkdocstrings_handlers/nim/templates/material/_base/docstring/returns.html.jinja +122 -0
  24. mkdocstrings_handlers/nim/templates/material/_base/docstring/type_aliases.html.jinja +86 -0
  25. mkdocstrings_handlers/nim/templates/material/_base/docstring/type_parameters.html.jinja +208 -0
  26. mkdocstrings_handlers/nim/templates/material/_base/docstring/warns.html.jinja +102 -0
  27. mkdocstrings_handlers/nim/templates/material/_base/docstring/yields.html.jinja +122 -0
  28. mkdocstrings_handlers/nim/templates/material/_base/docstring.html.jinja +59 -0
  29. mkdocstrings_handlers/nim/templates/material/_base/expression.html.jinja +172 -0
  30. mkdocstrings_handlers/nim/templates/material/_base/func.html.jinja +80 -0
  31. mkdocstrings_handlers/nim/templates/material/_base/iterator.html.jinja +80 -0
  32. mkdocstrings_handlers/nim/templates/material/_base/labels.html.jinja +25 -0
  33. mkdocstrings_handlers/nim/templates/material/_base/language.html.jinja +18 -0
  34. mkdocstrings_handlers/nim/templates/material/_base/languages/en.html.jinja +56 -0
  35. mkdocstrings_handlers/nim/templates/material/_base/languages/ja.html.jinja +56 -0
  36. mkdocstrings_handlers/nim/templates/material/_base/languages/zh.html.jinja +56 -0
  37. mkdocstrings_handlers/nim/templates/material/_base/macro.html.jinja +80 -0
  38. mkdocstrings_handlers/nim/templates/material/_base/module.html.jinja +30 -0
  39. mkdocstrings_handlers/nim/templates/material/_base/proc.html.jinja +80 -0
  40. mkdocstrings_handlers/nim/templates/material/_base/signature.html.jinja +135 -0
  41. mkdocstrings_handlers/nim/templates/material/_base/summary/attributes.html.jinja +23 -0
  42. mkdocstrings_handlers/nim/templates/material/_base/summary/classes.html.jinja +23 -0
  43. mkdocstrings_handlers/nim/templates/material/_base/summary/functions.html.jinja +23 -0
  44. mkdocstrings_handlers/nim/templates/material/_base/summary/modules.html.jinja +23 -0
  45. mkdocstrings_handlers/nim/templates/material/_base/summary/type_aliases.html.jinja +23 -0
  46. mkdocstrings_handlers/nim/templates/material/_base/summary.html.jinja +30 -0
  47. mkdocstrings_handlers/nim/templates/material/_base/template.html.jinja +80 -0
  48. mkdocstrings_handlers/nim/templates/material/_base/type.html.jinja +31 -0
  49. mkdocstrings_handlers/nim/templates/material/_base/type_alias.html.jinja +117 -0
  50. mkdocstrings_handlers/nim/templates/material/_base/type_parameters.html.jinja +87 -0
  51. mkdocstrings_handlers/nim/templates/material/attribute.html.jinja +1 -0
  52. mkdocstrings_handlers/nim/templates/material/backlinks.html.jinja +1 -0
  53. mkdocstrings_handlers/nim/templates/material/children.html.jinja +1 -0
  54. mkdocstrings_handlers/nim/templates/material/class.html.jinja +1 -0
  55. mkdocstrings_handlers/nim/templates/material/const.html.jinja +1 -0
  56. mkdocstrings_handlers/nim/templates/material/docstring/admonition.html.jinja +1 -0
  57. mkdocstrings_handlers/nim/templates/material/docstring/attributes.html.jinja +1 -0
  58. mkdocstrings_handlers/nim/templates/material/docstring/classes.html.jinja +1 -0
  59. mkdocstrings_handlers/nim/templates/material/docstring/examples.html.jinja +1 -0
  60. mkdocstrings_handlers/nim/templates/material/docstring/functions.html.jinja +1 -0
  61. mkdocstrings_handlers/nim/templates/material/docstring/modules.html.jinja +1 -0
  62. mkdocstrings_handlers/nim/templates/material/docstring/other_parameters.html.jinja +1 -0
  63. mkdocstrings_handlers/nim/templates/material/docstring/parameters.html.jinja +1 -0
  64. mkdocstrings_handlers/nim/templates/material/docstring/raises.html.jinja +1 -0
  65. mkdocstrings_handlers/nim/templates/material/docstring/receives.html.jinja +1 -0
  66. mkdocstrings_handlers/nim/templates/material/docstring/returns.html.jinja +1 -0
  67. mkdocstrings_handlers/nim/templates/material/docstring/type_aliases.html.jinja +1 -0
  68. mkdocstrings_handlers/nim/templates/material/docstring/type_parameters.html.jinja +1 -0
  69. mkdocstrings_handlers/nim/templates/material/docstring/warns.html.jinja +1 -0
  70. mkdocstrings_handlers/nim/templates/material/docstring/yields.html.jinja +1 -0
  71. mkdocstrings_handlers/nim/templates/material/docstring.html.jinja +1 -0
  72. mkdocstrings_handlers/nim/templates/material/expression.html.jinja +1 -0
  73. mkdocstrings_handlers/nim/templates/material/func.html.jinja +1 -0
  74. mkdocstrings_handlers/nim/templates/material/iterator.html.jinja +1 -0
  75. mkdocstrings_handlers/nim/templates/material/labels.html.jinja +1 -0
  76. mkdocstrings_handlers/nim/templates/material/language.html.jinja +1 -0
  77. mkdocstrings_handlers/nim/templates/material/languages/en.html.jinja +1 -0
  78. mkdocstrings_handlers/nim/templates/material/languages/ja.html.jinja +1 -0
  79. mkdocstrings_handlers/nim/templates/material/languages/zh.html.jinja +1 -0
  80. mkdocstrings_handlers/nim/templates/material/macro.html.jinja +1 -0
  81. mkdocstrings_handlers/nim/templates/material/module.html.jinja +1 -0
  82. mkdocstrings_handlers/nim/templates/material/proc.html.jinja +1 -0
  83. mkdocstrings_handlers/nim/templates/material/signature.html.jinja +1 -0
  84. mkdocstrings_handlers/nim/templates/material/style.css +236 -0
  85. mkdocstrings_handlers/nim/templates/material/summary/attributes.html.jinja +1 -0
  86. mkdocstrings_handlers/nim/templates/material/summary/classes.html.jinja +1 -0
  87. mkdocstrings_handlers/nim/templates/material/summary/functions.html.jinja +1 -0
  88. mkdocstrings_handlers/nim/templates/material/summary/modules.html.jinja +1 -0
  89. mkdocstrings_handlers/nim/templates/material/summary/type_aliases.html.jinja +1 -0
  90. mkdocstrings_handlers/nim/templates/material/summary.html.jinja +1 -0
  91. mkdocstrings_handlers/nim/templates/material/template.html.jinja +1 -0
  92. mkdocstrings_handlers/nim/templates/material/type.html.jinja +1 -0
  93. mkdocstrings_handlers/nim/templates/material/type_alias.html.jinja +1 -0
  94. mkdocstrings_handlers/nim/templates/material/type_parameters.html.jinja +1 -0
  95. mkdocstrings_nim-0.1.0.dist-info/METADATA +116 -0
  96. mkdocstrings_nim-0.1.0.dist-info/RECORD +99 -0
  97. mkdocstrings_nim-0.1.0.dist-info/WHEEL +4 -0
  98. mkdocstrings_nim-0.1.0.dist-info/entry_points.txt +2 -0
  99. mkdocstrings_nim-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2 @@
1
+ """mkdocstrings handlers namespace package."""
2
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,27 @@
1
+ """Nim handler for mkdocstrings."""
2
+
3
+ from mkdocstrings_handlers.nim.handler import NimHandler, get_handler
4
+ from mkdocstrings_handlers.nim.collector import NimCollector, NimModule, NimEntry, NimParam
5
+ from mkdocstrings_handlers.nim.docstring import (
6
+ parse_docstring,
7
+ DocstringStyle,
8
+ ParsedDocstring,
9
+ ParamDoc,
10
+ ReturnsDoc,
11
+ RaisesDoc,
12
+ )
13
+
14
+ __all__ = [
15
+ "NimHandler",
16
+ "get_handler",
17
+ "NimCollector",
18
+ "NimModule",
19
+ "NimEntry",
20
+ "NimParam",
21
+ "parse_docstring",
22
+ "DocstringStyle",
23
+ "ParsedDocstring",
24
+ "ParamDoc",
25
+ "ReturnsDoc",
26
+ "RaisesDoc",
27
+ ]
@@ -0,0 +1,267 @@
1
+ """Collector for Nim documentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ from dataclasses import dataclass, field
11
+ from importlib.resources import files, as_file
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from mkdocstrings import CollectionError
16
+
17
+ # Cache directory for compiled nimdocinfo binary
18
+ _CACHE_DIR = Path(tempfile.gettempdir()) / "mkdocstrings-nim-cache"
19
+
20
+
21
+ @dataclass
22
+ class NimParam:
23
+ """A Nim parameter."""
24
+ name: str
25
+ type: str
26
+ description: str = ""
27
+
28
+
29
+ @dataclass
30
+ class NimEntry:
31
+ """A documented Nim entry (proc, type, const, etc.)."""
32
+ name: str
33
+ kind: str
34
+ line: int
35
+ signature: str
36
+ doc: str = ""
37
+ params: list[NimParam] = field(default_factory=list)
38
+ returns: str = ""
39
+ returns_doc: str = ""
40
+ pragmas: list[str] = field(default_factory=list)
41
+ raises: list[str] = field(default_factory=list)
42
+
43
+
44
+ @dataclass
45
+ class NimModule:
46
+ """A documented Nim module."""
47
+ module: str
48
+ file: str
49
+ doc: str = ""
50
+ entries: list[NimEntry] = field(default_factory=list)
51
+
52
+
53
+ class NimCollector:
54
+ """Collects documentation from Nim source files."""
55
+
56
+ def __init__(self, paths: list[str], base_dir: Path):
57
+ """Initialize the collector.
58
+
59
+ Args:
60
+ paths: Search paths for Nim source files.
61
+ base_dir: Base directory of the project.
62
+ """
63
+ self.paths = paths
64
+ self.base_dir = base_dir
65
+ self._cache: dict[str, NimModule] = {}
66
+ # Use importlib.resources for reliable path resolution
67
+ extractor_files = files("mkdocstrings_handlers.nim").joinpath("extractor")
68
+ self._nimdocinfo_source = extractor_files.joinpath("nimdocinfo.nim")
69
+
70
+ def _resolve_identifier(self, identifier: str) -> Path:
71
+ """Resolve a module identifier to a file path.
72
+
73
+ Args:
74
+ identifier: Module identifier like 'lockfreequeues.ops'
75
+
76
+ Returns:
77
+ Path to the Nim source file.
78
+
79
+ Raises:
80
+ CollectionError: If the file cannot be found.
81
+ """
82
+ # Convert dots to path separators
83
+ rel_path = identifier.replace(".", "/") + ".nim"
84
+
85
+ for search_path in self.paths:
86
+ full_path = self.base_dir / search_path / rel_path
87
+ if full_path.exists():
88
+ return full_path
89
+
90
+ # Try without nested path (just filename)
91
+ filename = identifier.split(".")[-1] + ".nim"
92
+ for search_path in self.paths:
93
+ full_path = self.base_dir / search_path / filename
94
+ if full_path.exists():
95
+ return full_path
96
+
97
+ raise CollectionError(f"Could not find Nim file for identifier: {identifier}")
98
+
99
+ def _ensure_nimdocinfo_compiled(self) -> Path:
100
+ """Ensure nimdocinfo is compiled and return path to binary.
101
+
102
+ Copies Nim source files to a cache directory and compiles them there.
103
+ This avoids writing to the installed package directory.
104
+
105
+ Returns:
106
+ Path to the compiled nimdocinfo binary.
107
+
108
+ Raises:
109
+ CollectionError: If compilation fails.
110
+ """
111
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
112
+
113
+ # Get the extractor package files
114
+ extractor_pkg = files("mkdocstrings_handlers.nim").joinpath("extractor")
115
+
116
+ # Copy source files to cache if needed
117
+ with as_file(extractor_pkg.joinpath("nimdocinfo.nim")) as src_main:
118
+ with as_file(extractor_pkg.joinpath("extractor.nim")) as src_extractor:
119
+ cache_main = _CACHE_DIR / "nimdocinfo.nim"
120
+ cache_extractor = _CACHE_DIR / "extractor.nim"
121
+ cache_binary = _CACHE_DIR / "nimdocinfo"
122
+
123
+ # Check if we need to recompile (source newer than binary)
124
+ needs_compile = not cache_binary.exists()
125
+ if not needs_compile:
126
+ binary_mtime = cache_binary.stat().st_mtime
127
+ if src_main.stat().st_mtime > binary_mtime:
128
+ needs_compile = True
129
+ elif src_extractor.stat().st_mtime > binary_mtime:
130
+ needs_compile = True
131
+
132
+ if needs_compile:
133
+ # Copy source files
134
+ shutil.copy2(src_main, cache_main)
135
+ shutil.copy2(src_extractor, cache_extractor)
136
+
137
+ # Compile (first run is slow, ~15s; subsequent runs use cache)
138
+ result = subprocess.run(
139
+ ["nim", "c", "--outdir:" + str(_CACHE_DIR), str(cache_main)],
140
+ capture_output=True,
141
+ text=True,
142
+ timeout=120, # First compile can be slow
143
+ )
144
+
145
+ if result.returncode != 0:
146
+ raise CollectionError(
147
+ f"Failed to compile nimdocinfo:\n{result.stderr}"
148
+ )
149
+
150
+ return cache_binary
151
+
152
+ def _run_nimdocinfo(self, filepath: Path) -> dict[str, Any]:
153
+ """Run nimdocinfo on a Nim file.
154
+
155
+ Args:
156
+ filepath: Path to the Nim source file.
157
+
158
+ Returns:
159
+ Parsed JSON output from nimdocinfo.
160
+
161
+ Raises:
162
+ CollectionError: If nimdocinfo fails.
163
+ """
164
+ try:
165
+ binary_path = self._ensure_nimdocinfo_compiled()
166
+
167
+ result = subprocess.run(
168
+ [str(binary_path), str(filepath)],
169
+ capture_output=True,
170
+ text=True,
171
+ cwd=str(self.base_dir),
172
+ timeout=60,
173
+ )
174
+
175
+ if result.returncode != 0:
176
+ raise CollectionError(
177
+ f"nimdocinfo failed:\n{result.stderr}\n\n"
178
+ f"To debug, run manually:\n"
179
+ f" {binary_path} {filepath}"
180
+ )
181
+
182
+ # Parse JSON from stdout
183
+ lines = result.stdout.strip().split("\n")
184
+ json_lines = []
185
+ in_json = False
186
+ for line in lines:
187
+ if line.strip().startswith("{"):
188
+ in_json = True
189
+ if in_json:
190
+ json_lines.append(line)
191
+ if in_json and line.strip().startswith("}") and len(json_lines) > 1:
192
+ try:
193
+ return json.loads("\n".join(json_lines))
194
+ except json.JSONDecodeError:
195
+ continue
196
+
197
+ if not json_lines:
198
+ raise CollectionError(f"No JSON output from nimdocinfo: {result.stdout}")
199
+
200
+ return json.loads("\n".join(json_lines))
201
+
202
+ except FileNotFoundError:
203
+ raise CollectionError(
204
+ "Nim compiler not found. Install from https://nim-lang.org/install.html\n"
205
+ "Then verify installation: nim --version"
206
+ )
207
+ except subprocess.TimeoutExpired:
208
+ raise CollectionError(
209
+ f"nimdocinfo timed out processing {filepath}. "
210
+ "The file may be too complex or have circular imports."
211
+ )
212
+ except json.JSONDecodeError as e:
213
+ raise CollectionError(f"Invalid JSON from nimdocinfo: {e}")
214
+
215
+ def _parse_module(self, data: dict[str, Any]) -> NimModule:
216
+ """Parse JSON data into NimModule.
217
+
218
+ Args:
219
+ data: JSON data from nimdocinfo.
220
+
221
+ Returns:
222
+ Parsed NimModule.
223
+ """
224
+ entries = []
225
+ for entry_data in data.get("entries", []):
226
+ params = [
227
+ NimParam(name=p["name"], type=p["type"])
228
+ for p in entry_data.get("params", [])
229
+ ]
230
+
231
+ entries.append(NimEntry(
232
+ name=entry_data["name"],
233
+ kind=entry_data["kind"],
234
+ line=entry_data["line"],
235
+ signature=entry_data["signature"],
236
+ doc=entry_data.get("doc", ""),
237
+ params=params,
238
+ returns=entry_data.get("returns", ""),
239
+ pragmas=entry_data.get("pragmas", []),
240
+ raises=entry_data.get("raises", []),
241
+ ))
242
+
243
+ return NimModule(
244
+ module=data["module"],
245
+ file=data["file"],
246
+ doc=data.get("doc", ""),
247
+ entries=entries,
248
+ )
249
+
250
+ def collect(self, identifier: str) -> NimModule:
251
+ """Collect documentation for a module identifier.
252
+
253
+ Args:
254
+ identifier: Module identifier like 'lockfreequeues.ops'
255
+
256
+ Returns:
257
+ NimModule with documentation.
258
+ """
259
+ if identifier in self._cache:
260
+ return self._cache[identifier]
261
+
262
+ filepath = self._resolve_identifier(identifier)
263
+ data = self._run_nimdocinfo(filepath)
264
+ module = self._parse_module(data)
265
+
266
+ self._cache[identifier] = module
267
+ return module
@@ -0,0 +1,129 @@
1
+ """Docstring parsing for Nim documentation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Optional
8
+
9
+ import docstring_parser
10
+ from docstring_parser import DocstringStyle as DPStyle
11
+
12
+
13
+ class DocstringStyle(Enum):
14
+ """Supported docstring styles."""
15
+ RST = "rst"
16
+ GOOGLE = "google"
17
+ NUMPY = "numpy"
18
+ EPYDOC = "epydoc"
19
+ AUTO = "auto"
20
+
21
+
22
+ @dataclass
23
+ class ParamDoc:
24
+ """Parsed parameter documentation."""
25
+ name: str
26
+ description: str = ""
27
+ type: str = ""
28
+
29
+
30
+ @dataclass
31
+ class ReturnsDoc:
32
+ """Parsed return value documentation."""
33
+ description: str = ""
34
+ type: str = ""
35
+
36
+
37
+ @dataclass
38
+ class RaisesDoc:
39
+ """Parsed raises documentation."""
40
+ type: str
41
+ description: str = ""
42
+
43
+
44
+ @dataclass
45
+ class ParsedDocstring:
46
+ """Parsed docstring with structured sections."""
47
+ description: str = ""
48
+ params: list[ParamDoc] = field(default_factory=list)
49
+ returns: Optional[ReturnsDoc] = None
50
+ raises: list[RaisesDoc] = field(default_factory=list)
51
+ examples: list[str] = field(default_factory=list)
52
+
53
+
54
+ # Map our style enum to docstring_parser's style enum
55
+ _STYLE_MAP = {
56
+ DocstringStyle.RST: DPStyle.REST,
57
+ DocstringStyle.GOOGLE: DPStyle.GOOGLE,
58
+ DocstringStyle.NUMPY: DPStyle.NUMPYDOC,
59
+ DocstringStyle.EPYDOC: DPStyle.EPYDOC,
60
+ DocstringStyle.AUTO: DPStyle.AUTO,
61
+ }
62
+
63
+
64
+ def parse_docstring(doc: str, style: DocstringStyle = DocstringStyle.RST) -> ParsedDocstring:
65
+ """Parse a docstring according to the specified style.
66
+
67
+ Uses the docstring_parser library for robust parsing of RST, Google,
68
+ and NumPy docstring formats.
69
+
70
+ Args:
71
+ doc: Raw docstring text.
72
+ style: Docstring style to use for parsing.
73
+
74
+ Returns:
75
+ Parsed docstring structure.
76
+ """
77
+ if not doc:
78
+ return ParsedDocstring()
79
+
80
+ dp_style = _STYLE_MAP.get(style, DPStyle.REST)
81
+
82
+ try:
83
+ parsed = docstring_parser.parse(doc, style=dp_style)
84
+ except Exception:
85
+ # Fall back to auto-detection if specified style fails
86
+ try:
87
+ parsed = docstring_parser.parse(doc)
88
+ except Exception:
89
+ # If all parsing fails, just return description
90
+ return ParsedDocstring(description=doc.strip())
91
+
92
+ result = ParsedDocstring()
93
+
94
+ # Extract description (short + long)
95
+ desc_parts = []
96
+ if parsed.short_description:
97
+ desc_parts.append(parsed.short_description)
98
+ if parsed.long_description:
99
+ desc_parts.append(parsed.long_description)
100
+ result.description = "\n\n".join(desc_parts)
101
+
102
+ # Extract parameters
103
+ for param in parsed.params:
104
+ result.params.append(ParamDoc(
105
+ name=param.arg_name,
106
+ description=param.description or "",
107
+ type=param.type_name or "",
108
+ ))
109
+
110
+ # Extract returns
111
+ if parsed.returns:
112
+ result.returns = ReturnsDoc(
113
+ description=parsed.returns.description or "",
114
+ type=parsed.returns.type_name or "",
115
+ )
116
+
117
+ # Extract raises
118
+ for raises in parsed.raises:
119
+ result.raises.append(RaisesDoc(
120
+ type=raises.type_name or "",
121
+ description=raises.description or "",
122
+ ))
123
+
124
+ # Extract examples
125
+ for example in parsed.examples:
126
+ if example.description:
127
+ result.examples.append(example.description)
128
+
129
+ return result
@@ -0,0 +1,244 @@
1
+ ## AST extraction logic for nimdocinfo
2
+ import std/[json, strutils, sequtils, os]
3
+ import compiler/[ast, parser, idents, options, pathutils, lineinfos, msgs, renderer, llstream]
4
+
5
+ type
6
+ DocEntry* = object
7
+ name*: string
8
+ kind*: string
9
+ line*: int
10
+ signature*: string
11
+ params*: seq[tuple[name, typ: string]]
12
+ returns*: string
13
+ pragmas*: seq[string]
14
+ raises*: seq[string]
15
+ doc*: string
16
+
17
+ ModuleDoc* = object
18
+ module*: string
19
+ file*: string
20
+ doc*: string
21
+ entries*: seq[DocEntry]
22
+
23
+ proc extractDocComment(n: PNode): string =
24
+ ## Extract doc comment from a node
25
+ if n == nil:
26
+ return ""
27
+ if n.comment.len > 0:
28
+ return n.comment.strip
29
+ return ""
30
+
31
+ proc extractPragmas(n: PNode): seq[string] =
32
+ ## Extract pragma names from a pragma node
33
+ result = @[]
34
+ if n == nil or n.kind != nkPragma:
35
+ return
36
+ for child in n:
37
+ if child.kind == nkIdent:
38
+ result.add($child.ident.s)
39
+ elif child.kind == nkExprColonExpr and child[0].kind == nkIdent:
40
+ result.add($child[0].ident.s)
41
+
42
+ proc extractRaises(n: PNode): seq[string] =
43
+ ## Extract exception types from raises pragma
44
+ ## e.g., {.raises: [ValueError, IOError].}
45
+ result = @[]
46
+ if n == nil or n.kind != nkPragma:
47
+ return
48
+ for child in n:
49
+ if child.kind == nkExprColonExpr and child.len >= 2:
50
+ if child[0].kind == nkIdent and $child[0].ident.s == "raises":
51
+ let bracket = child[1]
52
+ if bracket.kind == nkBracket:
53
+ for exc in bracket:
54
+ if exc.kind == nkIdent:
55
+ result.add($exc.ident.s)
56
+ elif exc.kind == nkDotExpr:
57
+ # Handle qualified names like system.Exception
58
+ result.add($exc)
59
+
60
+ proc extractParams(n: PNode): seq[tuple[name, typ: string]] =
61
+ ## Extract parameter list from formal params
62
+ result = @[]
63
+ if n == nil or n.kind != nkFormalParams:
64
+ return
65
+ # Skip first child (return type)
66
+ for i in 1..<n.len:
67
+ let param = n[i]
68
+ if param.kind == nkIdentDefs:
69
+ let typNode = param[^2]
70
+ let typStr = if typNode.kind == nkEmpty: "auto" else: $typNode
71
+ # All names except last two (type and default)
72
+ for j in 0..<param.len - 2:
73
+ if param[j].kind == nkIdent:
74
+ result.add(($param[j].ident.s, typStr))
75
+
76
+ proc extractReturnType(n: PNode): string =
77
+ ## Extract return type from formal params
78
+ if n == nil or n.kind != nkFormalParams or n.len == 0:
79
+ return ""
80
+ let retNode = n[0]
81
+ if retNode.kind == nkEmpty:
82
+ return ""
83
+ return $retNode
84
+
85
+ const
86
+ # Nim AST indices for routine (proc/func/etc) nodes
87
+ routineNameIdx = 0
88
+ routinePatternIdx = 1
89
+ routineGenericParamsIdx = 2
90
+ routineParamsIdx = 3
91
+ routinePragmasIdx = 4
92
+ routineBodyIdx = 6
93
+
94
+ proc renderSignature(n: PNode, kind: string, name: string): string =
95
+ ## Render full signature as string
96
+ result = kind & " " & name
97
+ if n.len > routineParamsIdx and n[routineParamsIdx].kind == nkFormalParams:
98
+ let params = n[routineParamsIdx]
99
+ var paramStrs: seq[string] = @[]
100
+ for i in 1..<params.len:
101
+ paramStrs.add($params[i])
102
+ result &= "(" & paramStrs.join("; ") & ")"
103
+ let ret = extractReturnType(n[routineParamsIdx])
104
+ if ret.len > 0:
105
+ result &= ": " & ret
106
+
107
+ proc extractName(n: PNode): string =
108
+ ## Extract name from a name node (handles postfix for exported symbols)
109
+ if n.kind == nkIdent:
110
+ return $n.ident.s
111
+ elif n.kind == nkPostfix and n.len >= 2 and n[1].kind == nkIdent:
112
+ return $n[1].ident.s
113
+ elif n.kind == nkPostfix and n.len >= 2 and n[1].kind == nkAccQuoted:
114
+ # Handle quoted operators like `+`*
115
+ if n[1].len > 0 and n[1][0].kind == nkIdent:
116
+ return $n[1][0].ident.s
117
+ return ""
118
+
119
+ proc extractProcDoc(n: PNode): string =
120
+ ## Extract doc comment from a proc definition
121
+ ## The doc comment can be on the proc node itself or on the first statement in the body
122
+ result = extractDocComment(n)
123
+ if result.len == 0 and n.len > routineBodyIdx:
124
+ let body = n[routineBodyIdx]
125
+ if body != nil and body.kind == nkStmtList and body.len > 0:
126
+ # Check if first statement has a comment
127
+ result = extractDocComment(body[0])
128
+ if result.len == 0 and body[0].kind == nkCommentStmt:
129
+ result = body[0].comment.strip
130
+
131
+ proc extractProc(n: PNode, kind: string): DocEntry =
132
+ ## Extract documentation from a proc/func/etc definition
133
+ result.name = extractName(n[routineNameIdx])
134
+ result.kind = kind
135
+ result.line = n.info.line.int
136
+ result.doc = extractProcDoc(n)
137
+
138
+ if n.len > routineParamsIdx:
139
+ result.params = extractParams(n[routineParamsIdx])
140
+ result.returns = extractReturnType(n[routineParamsIdx])
141
+
142
+ # Extract pragmas at the correct index
143
+ if n.len > routinePragmasIdx and n[routinePragmasIdx].kind == nkPragma:
144
+ result.pragmas = extractPragmas(n[routinePragmasIdx])
145
+ result.raises = extractRaises(n[routinePragmasIdx])
146
+
147
+ result.signature = renderSignature(n, kind, result.name)
148
+
149
+ proc extractType(n: PNode): DocEntry =
150
+ ## Extract documentation from a type definition
151
+ result.kind = "type"
152
+ result.doc = extractDocComment(n)
153
+ result.line = n.info.line.int
154
+ result.name = extractName(n[0])
155
+ result.signature = "type " & result.name
156
+
157
+ proc extractConst(n: PNode): DocEntry =
158
+ ## Extract documentation from a const definition
159
+ result.kind = "const"
160
+ result.doc = extractDocComment(n)
161
+ result.line = n.info.line.int
162
+ result.name = extractName(n[0])
163
+ result.signature = "const " & result.name
164
+
165
+ proc walkAst(n: PNode, entries: var seq[DocEntry]) =
166
+ ## Walk AST and collect documentation entries
167
+ if n == nil:
168
+ return
169
+
170
+ case n.kind
171
+ of nkProcDef:
172
+ entries.add extractProc(n, "proc")
173
+ of nkFuncDef:
174
+ entries.add extractProc(n, "func")
175
+ of nkIteratorDef:
176
+ entries.add extractProc(n, "iterator")
177
+ of nkTemplateDef:
178
+ entries.add extractProc(n, "template")
179
+ of nkMacroDef:
180
+ entries.add extractProc(n, "macro")
181
+ of nkTypeDef:
182
+ entries.add extractType(n)
183
+ of nkConstDef:
184
+ entries.add extractConst(n)
185
+ else:
186
+ for child in n:
187
+ walkAst(child, entries)
188
+
189
+ proc extractModule*(filepath: string): ModuleDoc =
190
+ ## Extract all documentation from a Nim source file
191
+ result.file = filepath
192
+ result.module = filepath.splitFile.name
193
+ result.entries = @[]
194
+
195
+ # Parse the file
196
+ var conf = newConfigRef()
197
+ conf.verbosity = 0
198
+
199
+ let fileIdx = fileInfoIdx(conf, AbsoluteFile(filepath))
200
+ var parser: Parser
201
+
202
+ let source = readFile(filepath)
203
+ openParser(parser, fileIdx, llStreamOpen(source), newIdentCache(), conf)
204
+
205
+ let ast = parseAll(parser)
206
+ closeParser(parser)
207
+
208
+ # Extract module doc comment
209
+ if ast.len > 0 and ast[0].comment.len > 0:
210
+ result.doc = ast[0].comment.strip
211
+
212
+ walkAst(ast, result.entries)
213
+
214
+ proc toJson*(doc: ModuleDoc): JsonNode =
215
+ ## Convert module documentation to JSON
216
+ result = %*{
217
+ "module": doc.module,
218
+ "file": doc.file,
219
+ "doc": doc.doc,
220
+ "entries": []
221
+ }
222
+
223
+ for entry in doc.entries:
224
+ var entryJson = %*{
225
+ "name": entry.name,
226
+ "kind": entry.kind,
227
+ "line": entry.line,
228
+ "signature": entry.signature,
229
+ "doc": entry.doc
230
+ }
231
+
232
+ if entry.params.len > 0:
233
+ entryJson["params"] = %entry.params.mapIt(%*{"name": it.name, "type": it.typ})
234
+
235
+ if entry.returns.len > 0:
236
+ entryJson["returns"] = %entry.returns
237
+
238
+ if entry.pragmas.len > 0:
239
+ entryJson["pragmas"] = %entry.pragmas
240
+
241
+ if entry.raises.len > 0:
242
+ entryJson["raises"] = %entry.raises
243
+
244
+ result["entries"].add entryJson
@@ -0,0 +1,16 @@
1
+ ## nimdocinfo - Extract documentation from Nim source files
2
+ import std/[json, os]
3
+ import extractor
4
+
5
+ when isMainModule:
6
+ if paramCount() < 1:
7
+ echo "Usage: nimdocinfo <file.nim>"
8
+ quit(1)
9
+
10
+ let filepath = paramStr(1)
11
+ if not fileExists(filepath):
12
+ echo "Error: File not found: ", filepath
13
+ quit(1)
14
+
15
+ let doc = extractModule(filepath)
16
+ echo doc.toJson.pretty