]> begriffs open source - cmsis-freertos/blob - Test/litani/doc/bin/schema-to-scdoc
Update README.md - branch main is now the base branch
[cmsis-freertos] / Test / litani / doc / bin / schema-to-scdoc
1 #!/usr/bin/env python3
2 #
3 # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License").
6 # You may not use this file except in compliance with the License.
7 # A copy of the License is located at
8 #
9 #     http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # or in the "license" file accompanying this file. This file is distributed
12 # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13 # express or implied. See the License for the specific language governing
14 # permissions and limitations under the License.
15
16
17 import argparse
18 import json
19 import os
20 import pathlib
21 import re
22 import sys
23
24 import jinja2
25 import yaml
26
27
28 _DESCRIPTION = "Convert a series of commented voluptuous schemas into groff"
29 _EPILOG = """
30 This program is used to automatically generate documentation for Litani's
31 voluptuous schemata. The codebase contains schemata in methods that are preceded
32 by the following magic comment:
33
34 # doc-gen
35 # {
36 #   "page": "PAGE_NAME",
37 #   "order": N,
38 #   "title": "TITLE",
39 # }
40
41 When this script is run with --page-name PAGE_NAME, it reads all the python
42 files in the codebase looking for such blocks with an equal PAGE_NAME. It then
43 converts the schema and associated documentation into scdoc format, ready to be
44 converted into groff and read in a manual pager.
45 """
46
47
48
49 class Comment:
50     def __init__(self, lines):
51         indent_pat = re.compile(r"(?P<indentation>\s*)")
52         match = indent_pat.match(lines[0])
53         self.indent = int(len(match["indentation"]) / 4) - 1
54         self.lines = lines
55
56
57     def print_full(self):
58         return "\n{indentation}{comment}".format(
59             indentation=("\t" * self.indent),
60             comment=" ".join([string.strip()[2:] for string in self.lines]))
61
62
63     def print_compact(self):
64         return ""
65
66
67
68 class Code:
69     def __init__(self, line):
70         indent_pat = re.compile(r"^(?P<indentation>\s*)")
71         match = indent_pat.match(line)
72         self.indent = int(len(match["indentation"]) / 4) - 1
73         self.line = self.format_line(line)
74
75
76     def format_line(self, line):
77         line = line.strip()
78         line = re.sub(r"voluptuous\.", "", line)
79         line = re.sub(r"^return\s+", "", line)
80         pat = re.compile(r"(?P<key>.+?):\s+(?P<value>.*?)(?P<end>[^\w]+)$")
81         m = pat.match(line)
82         if m:
83             return "{indentation}{key}: {value} {end}".format(
84                 indentation="\t" * self.indent,
85                 key=f"*{m['key']}*",
86                 value=f"_{m['value']}_" if m['value'] else '',
87                 end=m['end'])
88         return "{indentation}{line}".format(
89             indentation="\t" * self.indent,
90             line=line)
91
92
93     def print_compact(self):
94         return self.line
95
96
97     def print_full(self):
98         return self.line
99
100
101
102 class Blank:
103     def print_compact(self):
104         return ""
105
106
107     def print_full(self):
108         return "\n"
109
110
111
112 def next_line(iterator):
113     try:
114         return next(iterator).rstrip()
115     except StopIteration:
116         return None
117
118
119 def get_args():
120     pars = argparse.ArgumentParser(description=_DESCRIPTION, epilog=_EPILOG)
121     for arg in [{
122             "flags": ["--page-name"],
123             "required": True,
124     }, {
125             "flags": ["--project-root"],
126             "required": True,
127     }, {
128             "flags": ["--data-path"],
129             "required": True,
130     }, {
131             "flags": ["--template"],
132             "required": True,
133             "type": pathlib.Path,
134     }]:
135         flags = arg.pop("flags")
136         pars.add_argument(*flags, **arg)
137     return pars.parse_args()
138
139
140 def parse_schema(iterator):
141     comment_buf = []
142     ret = []
143
144     line = next_line(iterator)
145     while not (
146             line is None or \
147             line.startswith("def") or \
148             line.startswith("# end-doc-gen")):
149         if line.strip().startswith("# "):
150             comment_buf.append(line)
151         else:
152             if comment_buf:
153                 ret.append(Comment(comment_buf))
154                 comment_buf = []
155             if not line:
156                 ret.append(Blank())
157             else:
158                 ret.append(Code(line))
159         line = next_line(iterator)
160     return ret
161
162
163 def parse_fun_def(iterator):
164     """Skip the first few lines of a schema function and return the schema"""
165     line = next_line(iterator)
166     while line:
167         line = next_line(iterator)
168     return parse_schema(iterator)
169
170
171 def parse_header(iterator, page_name):
172     """Parse the JSON at the top of a single documentation fragment"""
173
174     line = next_line(iterator)
175     buf = []
176     while line is not None:
177         buf.append(line)
178         line = next_line(iterator)
179
180         # { <----- to avoid confusing syntax highlighting
181         if line != "# }":
182             continue
183         buf.append(line)
184         header_str = "\n".join([string[2:] for string in buf])
185         try:
186             header_d = json.loads(header_str)
187         except json.decoder.JSONDecodeError:
188             print(
189                 "Could not parse JSON fragment %s" % header_str,
190                 file=sys.stderr)
191             sys.exit(1)
192         if header_d["page"] != page_name:
193             return []
194         return [{
195             **header_d,
196             "body": parse_fun_def(iterator)
197         }]
198     return []
199
200
201 def parse_file(iterator, page_name):
202     """Return a list of all documentation fragments in a single file"""
203
204     ret = []
205     line = next_line(iterator)
206     while line is not None:
207         if line == "# doc-gen":
208             ret.extend(parse_header(iterator, page_name))
209         line = next_line(iterator)
210     return ret
211
212
213 def get_page_fragments(project_root, page_name):
214     ret = []
215     for root, _, fyles in os.walk(project_root):
216         root = pathlib.Path(root)
217         for fyle in fyles:
218             if not fyle.endswith(".py"):
219                 continue
220             with open(root / fyle) as handle:
221                 ret.extend(parse_file(iter(handle), page_name))
222     return ret
223
224
225 def main():
226     args = get_args()
227     fragments = get_page_fragments(args.project_root, args.page_name)
228     fragments.sort(key=lambda x: x["order"])
229
230     with open(args.data_path) as handle:
231         page = yaml.safe_load(handle)
232
233     env = jinja2.Environment(
234         loader=jinja2.FileSystemLoader(str(args.template.parent)),
235         autoescape=jinja2.select_autoescape(
236             enabled_extensions=('html'),
237             default_for_string=True))
238     templ = env.get_template(args.template.name)
239     out = templ.render(page=page, fragments=fragments, page_name=args.page_name)
240     print(out)
241
242
243 if __name__ == "__main__":
244     main()