iterative closest points
CEV ICP algorithm library
Loading...
Searching...
No Matches
icp_doc_builder.py
Go to the documentation of this file.
1# Copyright (C) 2024 Ethan Uppal.
2# SPDX-License-Identifier: MIT
3
4import os
5import re
6import sys
7
8
9def change_extension(filename, new_extension):
10 name, _ = os.path.splitext(filename)
11 return f"{name}.{new_extension}"
12
13
14# yes this documentation was mostly generated by ChatGPT
15# no I don't care, it's Python
17 """
18 A class used to build documentation for an ICP implementation.
19
20 ...
21
22 Attributes
23 ----------
24 search_dir : str
25 The directory to search for source files.
26 out_dir : str
27 The directory to write the output markdown files.
28 main_file : str
29 The main documentation file.
30 all_sources : set
31 A set of all sources found in the source files.
32 registration_names : dict
33 Associates each ICP type name with its registration name.
34
35 Methods
36 -------
37 extract(file, contents):
38 Extracts the documentation from the given file contents and writes it to a markdown file.
39 build():
40 Searches for source files in the search directory and extracts their documentation.
41 """
42
43 def __init__(self, search_dir, out_dir, main_file):
44 """
45 Constructs a new ICPDocumentationBuilder.
46
47 Parameters
48 ----------
49 search_dir : str
50 The directory to search for source files.
51 out_dir : str
52 The directory to write the output markdown files.
53 main_file : str
54 A path to the main markdown file.
55 """
56 self.search_dir = search_dir
57 self.out_dir = out_dir
58 self.main_file = main_file
59 self.all_sources = set()
60
61 def extract(self, file, contents):
62 """
63 Extracts the documentation from the given file contents and writes it to a markdown file.
64
65 Parameters
66 ----------
67 file : str
68 The name of the file to extract documentation from.
69 contents : str
70 The contents of the file.
71 """
72
73 pattern = r"/\*\s*(#step|#name|#desc)(.*?)\*/"
74 comments = re.findall(pattern, contents, re.DOTALL)
75 if not comments:
76 return
77 md_filename = "icp_" + change_extension(file, "md")
78 made_description = False
79 with open(self.out_dir + "/" + md_filename, "w") as md_file:
80 step_cnt = 1
81 for kind, comment in comments:
82 comment_parts = comment.strip().split("Sources:", 1)
83 comment_text = (
84 re.sub(r"\n *", "\n", comment_parts[0])
85 .replace("\n*", "\n")
86 .replace("\n *", "\n")
87 )
88 sources = (
89 re.findall(r"https?://[^\s]+", comment_parts[1])
90 if len(comment_parts) > 1
91 else []
92 )
93 self.all_sources.update(sources)
94 if kind == "#name":
95 doxygen_ref = f"{comment_text.lower()}_icp".replace("-", "_")
96 md_file.write(f"\\page {doxygen_ref} {comment_text} ICP\n")
97
98 register_pattern = r"/\*\s*#register(.*?)\*/"
99 registers = re.findall(register_pattern, contents, re.DOTALL)
100 assert len(registers) == 1
101 register_name = registers[0].strip()
102
103 conf_pattern = r'/\*\s*#conf\s+"([^"]*)"\s+(.*?)\*/'
104 confs = re.findall(conf_pattern, contents, re.DOTALL)
105
106 if not confs:
107 md_file.write(
108 f'\\par Usage\nYou can construct a new instance of {comment_text} ICP with `icp::ICP::from_method("{register_name}")`.'
109 )
110 else:
111 md_file.write(
112 f'\\par Usage\nYou can construct a new instance of {comment_text} ICP with `icp::ICP::from_method("{register_name}", config)`. Supply the following parameters to `config` (via icp::ICP::Config::set):\n'
113 )
114 md_file.write("\nKey | Description\n--- | ---\n")
115 for key, descr in confs:
116 descr_lines = descr.split("\n")
117 cleaned_lines = [
118 line.strip().removeprefix("*") for line in descr_lines
119 ]
120 descr = " ".join(cleaned_lines)
121 md_file.write(f'`"{key}"` | {descr}\n')
122 elif kind == "#step":
123 if not made_description:
124 md_file.write("\n\\par Description\n")
125 made_description = True
126 lines = comment_text.splitlines()
127 first_line_parts = lines[0].split(":", 1)
128 if len(first_line_parts) == 2:
129 lines[0] = f"**{first_line_parts[0]}**:{first_line_parts[1]}"
130 else:
131 lines[0] = f"**{first_line_parts[0]}**"
132 step_text = "\n".join(" " + line for line in lines)
133 md_file.write(f"\n{step_cnt}. {step_text}\n")
134 step_cnt += 1
135 if sources:
136 md_file.write(" Sources: \n")
137 for source in sources:
138 md_file.write(f" - {source}\n")
139 md_file.write("\n")
140 elif kind == "#desc":
141 icp_description = comment_text
142 md_file.write(f"\n\\par Description\n{comment_text}\n")
143 made_description = True
144 md_file.write(
145 "\n\nRead \\ref icp_sources for a list of all resources used in this project."
146 )
147 md_file.write(
148 f"\nThis page was automatically generated from {file} with {os.path.basename(__file__)}."
149 )
150 return doxygen_ref, icp_description
151
152 def update_main(self, doxygen_refs):
153 with open(self.main_file, "r") as file:
154 content = file.read()
155
156 start_marker = "<!-- ICP_DOCS_BUILDER EDIT MARKER START -->"
157 end_marker = "<!-- ICP_DOCS_BUILDER EDIT MARKER END -->"
158
159 start_index = content.find(start_marker)
160 end_index = content.find(end_marker)
161
162 if start_index != -1 and end_index != -1:
163 new_content = (
164 content[: start_index + len(start_marker) + 1]
165 + "\n".join(
166 sorted(
167 [
168 f"- \\ref {doxygen_ref} ({icp_description})"
169 for (doxygen_ref, icp_description) in doxygen_refs
170 ]
171 )
172 )
173 + "\n"
174 + content[end_index:]
175 )
176
177 with open(self.main_file, "w") as file:
178 file.write(new_content)
179
180 def build(self):
181 """
182 Searches for source files in the search directory and extracts their documentation.
183 """
184 doxygen_refs = []
185 for root, _, files in os.walk(self.search_dir):
186 for file in files:
187 if file.endswith(".cpp"):
188 with open(os.path.join(root, file), "r") as f:
189 result = self.extract(file, f.read())
190 if result:
191 doxygen_ref, icp_description = result
192 doxygen_refs.append((doxygen_ref, icp_description))
193 self.update_main(doxygen_refs)
194 os.makedirs(self.out_dir + "/extra", exist_ok=True)
195 with open(self.out_dir + "/extra/sources.md", "w") as md_file:
196 md_file.write("\\page icp_sources ICP Sources\n\n")
197 md_file.write(
198 "This list contains all resources used in implementing ICP for this project in alphabetical order.\n\n"
199 )
200 for source in sorted(list(self.all_sources)):
201 md_file.write(f" - {source}\n")
202
203
204if __name__ == "__main__":
205 if len(sys.argv) != 4:
206 print(
207 f"usage: python3 {os.path.basename(__file__)} search_dir out_dir main_file"
208 )
209 sys.exit(1)
210
211 os.makedirs(sys.argv[2], exist_ok=True)
212
213 doc_builder = ICPDocumentationBuilder(sys.argv[1], sys.argv[2], sys.argv[3])
214 doc_builder.build()
__init__(self, search_dir, out_dir, main_file)
change_extension(filename, new_extension)