Coverage for gws-app/gws/spec/generator/configref.py: 21%
160 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Generate configuration references."""
3import re
5from . import base
7STRINGS = {}
9STRINGS['en'] = {
10 'head_property': 'property',
11 'head_type': 'type',
12 'head_default': 'default',
13 'head_value': 'value',
15 'tag_variant': 'variant',
16 'tag_struct': 'struct',
17 'tag_enum': 'enumeration',
18 'tag_type': 'type',
20 'label_added': 'added',
21 'label_deprecated': 'deprecated',
22 'label_changed': 'changed',
24}
26STRINGS['de'] = {
27 'head_property': 'Eigenschaft',
28 'head_type': 'Typ',
29 'head_default': 'Default',
30 'head_value': 'Wert',
32 'tag_variant': 'variant',
33 'tag_struct': 'struct',
34 'tag_enum': 'enumeration',
35 'tag_type': 'type',
37 'label_added': 'neu',
38 'label_deprecated': 'veraltet',
39 'label_changed': 'geändert',
40}
42LIST_FORMAT = '**[**{}**]**'
44LABELS = 'added|deprecated|changed'
47def create(gen: base.Generator, lang: str):
48 return _Creator(gen, lang).run()
51##
53class _Creator:
54 def __init__(self, gen: base.Generator, lang: str):
55 self.gen = gen
56 self.lang = lang
57 self.strings = STRINGS[lang]
58 self.queue = []
59 self.html = {}
61 def run(self):
62 start_tid = 'gws.base.application.core.Config'
64 self.queue = [start_tid]
65 self.html = {}
67 done = set()
69 while self.queue:
70 tid = self.queue.pop(0)
71 if tid in done:
72 continue
73 done.add(tid)
74 self.process(tid)
76 res = self.html.pop(start_tid.lower())
77 res += nl(v for _, v in sorted(self.html.items()))
78 return res
80 def process(self, tid):
81 typ = self.gen.types[tid]
83 if typ.c == base.C.CLASS:
84 self.html[tid.lower()] = nl(self.process_class(tid))
86 if typ.c == base.C.ENUM:
87 self.html[tid.lower()] = nl(self.process_enum(tid))
89 if typ.c == base.C.TYPE:
90 target = self.gen.types[typ.tTarget]
91 if target.c == base.C.VARIANT:
92 self.html[tid.lower()] = nl(self.process_variant(tid))
93 else:
94 self.html[tid.lower()] = nl(self.process_type(tid))
96 if typ.c == base.C.LIST:
97 self.process(typ.tItem)
99 def process_class(self, tid):
100 typ = self.gen.types[tid]
102 yield header(tid)
103 yield subhead(self.strings['tag_struct'], self.docstring(tid))
105 rows = {False: [], True: []}
107 for prop_name, prop_tid in sorted(typ.tProperties.items()):
108 prop_typ = self.gen.types[prop_tid]
109 self.queue.append(prop_typ.tValue)
110 rows[prop_typ.hasDefault].append([
111 as_propname(prop_name) if prop_typ.hasDefault else as_required(prop_name),
112 self.type_string(prop_typ.tValue),
113 self.default_string(prop_tid),
114 self.docstring(prop_tid),
115 ])
117 yield table(
118 [self.strings['head_property'], self.strings['head_type'], self.strings['head_default'], ''],
119 rows[False] + rows[True],
120 )
122 def process_enum(self, tid):
123 typ = self.gen.types[tid]
125 yield header(tid)
126 yield subhead(self.strings['tag_enum'], self.docstring(tid))
127 yield table(
128 ['', ''],
129 [
130 [as_literal(key), self.docstring(tid, key)]
131 for key in typ.enumValues
132 ]
133 )
135 def process_variant(self, tid):
136 typ = self.gen.types[tid]
137 target = self.gen.types[typ.tTarget]
139 yield header(tid)
140 yield subhead(self.strings['tag_variant'], '')
142 rows = []
143 for member_name, member_tid in sorted(target.tMembers.items()):
144 self.queue.append(member_tid)
145 rows.append([as_literal(member_name), self.type_string(member_tid)])
147 yield table(
148 [self.strings['head_type'], ''],
149 rows
150 )
152 def process_type(self, tid):
153 yield header(tid)
154 yield subhead(self.strings['tag_type'], self.docstring(tid))
156 def type_string(self, tid):
157 typ = self.gen.types[tid]
159 if typ.c in {base.C.CLASS, base.C.TYPE, base.C.ENUM}:
160 return link(tid, as_typename(tid))
162 if typ.c == base.C.DICT:
163 return as_code('dict')
165 if typ.c == base.C.LIST:
166 return LIST_FORMAT.format(self.type_string(typ.tItem))
168 if typ.c == base.C.ATOM:
169 return as_typename(tid)
171 if typ.c == base.C.LITERAL:
172 return r' \| '.join(as_literal(s) for s in typ.literalValues)
174 return ''
176 def default_string(self, tid):
177 typ = self.gen.types[tid]
178 val = typ.tValue
180 if val in self.gen.types and self.gen.types[val].c == base.C.LITERAL:
181 return ''
182 if not typ.hasDefault:
183 return ''
184 v = typ.defaultValue
185 if v is None or v == '':
186 return ''
187 return as_literal(v)
189 def docstring(self, tid, enum_value=None):
190 missing_translation = ''
192 # get the original (spec) docstring
193 typ = self.gen.types[tid]
194 spec_text = first_line(typ.enumDocs.get(enum_value) if enum_value else typ.doc)
196 # try the translated (from strings) docstring
197 key = tid
198 if enum_value:
199 key += '.' + enum_value
200 local_text = self.gen.strings[self.lang].get(key)
202 if not local_text and self.lang != 'en':
203 # translation missing: use the english docstring and warn
204 base.log.debug(f'missing {self.lang} translation for {key!r}')
205 missing_translation = f'`{key}`{{.configref_missing_translation}}'
206 local_text = self.gen.strings['en'].get(key)
208 local_text = first_line(local_text) or spec_text
210 # process a label, like "foobar (added in 8.1)"
211 # it might be missing in a translation, but present in the original (spec) docstring
212 text, label = self.extract_label(local_text)
213 if not label and spec_text != local_text:
214 _, label = self.extract_label(spec_text)
216 return text + label + missing_translation
218 def extract_label(self, text):
219 m = re.match(fr'(.+?)\(({LABELS}) in (\d[\d.]+)\)$', text)
220 if not m:
221 return text, ''
222 kind = m.group(2).strip()
223 name = self.strings[f'label_{kind}']
224 version = m.group(3)
225 label = f'`{name}: {version}`{{.configref_label_{kind}}}'
226 return m.group(1).strip(), label
229def as_literal(s):
230 return f'`{s!r}`{{.configref_literal}}'
233def as_typename(s):
234 return f'`{s}`{{.configref_typename}}'
237def as_category(s):
238 return f'`{s}`{{.configref_category}}'
241def as_propname(s):
242 return f'`{s}`{{.configref_propname}}'
245def as_required(s):
246 return f'`{s}`{{.configref_required}}'
249def as_code(s):
250 return f'`{s}`'
253def header(tid):
254 return f'\n## {tid} :{tid}\n'
257def subhead(category, text):
258 return as_category(category) + ' ' + text + '\n'
261def link(target, text):
262 return f'[{text}](../{target})'
265def first_line(s):
266 return (s or '').strip().split('\n')[0].strip()
269def table(heads, rows):
270 widths = [len(h) for h in heads]
272 for r in rows:
273 widths = [
274 max(a, b)
275 for a, b in zip(
276 widths,
277 [len(str(s)) for s in r]
278 )
279 ]
281 def field(n, v):
282 return str(v).ljust(widths[n])
284 def row(r):
285 return ' | '.join(field(n, v) for n, v in enumerate(r))
287 out = [row(heads), '', *[row(r) for r in rows]]
288 out[1] = '-' * len(out[0])
289 return '\n'.join(f'| {s} |' for s in out) + '\n'
292def escape(s, quote=True):
293 s = s.replace("&", "&")
294 s = s.replace("<", "<")
295 s = s.replace(">", ">")
296 if quote:
297 s = s.replace('"', """)
298 return s
301nl = '\n'.join