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

1"""Generate configuration references.""" 

2 

3import re 

4 

5from . import base 

6 

7STRINGS = {} 

8 

9STRINGS['en'] = { 

10 'head_property': 'property', 

11 'head_type': 'type', 

12 'head_default': 'default', 

13 'head_value': 'value', 

14 

15 'tag_variant': 'variant', 

16 'tag_struct': 'struct', 

17 'tag_enum': 'enumeration', 

18 'tag_type': 'type', 

19 

20 'label_added': 'added', 

21 'label_deprecated': 'deprecated', 

22 'label_changed': 'changed', 

23 

24} 

25 

26STRINGS['de'] = { 

27 'head_property': 'Eigenschaft', 

28 'head_type': 'Typ', 

29 'head_default': 'Default', 

30 'head_value': 'Wert', 

31 

32 'tag_variant': 'variant', 

33 'tag_struct': 'struct', 

34 'tag_enum': 'enumeration', 

35 'tag_type': 'type', 

36 

37 'label_added': 'neu', 

38 'label_deprecated': 'veraltet', 

39 'label_changed': 'geändert', 

40} 

41 

42LIST_FORMAT = '**[**{}**]**' 

43 

44LABELS = 'added|deprecated|changed' 

45 

46 

47def create(gen: base.Generator, lang: str): 

48 return _Creator(gen, lang).run() 

49 

50 

51## 

52 

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 = {} 

60 

61 def run(self): 

62 start_tid = 'gws.base.application.core.Config' 

63 

64 self.queue = [start_tid] 

65 self.html = {} 

66 

67 done = set() 

68 

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) 

75 

76 res = self.html.pop(start_tid.lower()) 

77 res += nl(v for _, v in sorted(self.html.items())) 

78 return res 

79 

80 def process(self, tid): 

81 typ = self.gen.types[tid] 

82 

83 if typ.c == base.C.CLASS: 

84 self.html[tid.lower()] = nl(self.process_class(tid)) 

85 

86 if typ.c == base.C.ENUM: 

87 self.html[tid.lower()] = nl(self.process_enum(tid)) 

88 

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)) 

95 

96 if typ.c == base.C.LIST: 

97 self.process(typ.tItem) 

98 

99 def process_class(self, tid): 

100 typ = self.gen.types[tid] 

101 

102 yield header(tid) 

103 yield subhead(self.strings['tag_struct'], self.docstring(tid)) 

104 

105 rows = {False: [], True: []} 

106 

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 ]) 

116 

117 yield table( 

118 [self.strings['head_property'], self.strings['head_type'], self.strings['head_default'], ''], 

119 rows[False] + rows[True], 

120 ) 

121 

122 def process_enum(self, tid): 

123 typ = self.gen.types[tid] 

124 

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 ) 

134 

135 def process_variant(self, tid): 

136 typ = self.gen.types[tid] 

137 target = self.gen.types[typ.tTarget] 

138 

139 yield header(tid) 

140 yield subhead(self.strings['tag_variant'], '') 

141 

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)]) 

146 

147 yield table( 

148 [self.strings['head_type'], ''], 

149 rows 

150 ) 

151 

152 def process_type(self, tid): 

153 yield header(tid) 

154 yield subhead(self.strings['tag_type'], self.docstring(tid)) 

155 

156 def type_string(self, tid): 

157 typ = self.gen.types[tid] 

158 

159 if typ.c in {base.C.CLASS, base.C.TYPE, base.C.ENUM}: 

160 return link(tid, as_typename(tid)) 

161 

162 if typ.c == base.C.DICT: 

163 return as_code('dict') 

164 

165 if typ.c == base.C.LIST: 

166 return LIST_FORMAT.format(self.type_string(typ.tItem)) 

167 

168 if typ.c == base.C.ATOM: 

169 return as_typename(tid) 

170 

171 if typ.c == base.C.LITERAL: 

172 return r' \| '.join(as_literal(s) for s in typ.literalValues) 

173 

174 return '' 

175 

176 def default_string(self, tid): 

177 typ = self.gen.types[tid] 

178 val = typ.tValue 

179 

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) 

188 

189 def docstring(self, tid, enum_value=None): 

190 missing_translation = '' 

191 

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) 

195 

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) 

201 

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) 

207 

208 local_text = first_line(local_text) or spec_text 

209 

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) 

215 

216 return text + label + missing_translation 

217 

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 

227 

228 

229def as_literal(s): 

230 return f'`{s!r}`{{.configref_literal}}' 

231 

232 

233def as_typename(s): 

234 return f'`{s}`{{.configref_typename}}' 

235 

236 

237def as_category(s): 

238 return f'`{s}`{{.configref_category}}' 

239 

240 

241def as_propname(s): 

242 return f'`{s}`{{.configref_propname}}' 

243 

244 

245def as_required(s): 

246 return f'`{s}`{{.configref_required}}' 

247 

248 

249def as_code(s): 

250 return f'`{s}`' 

251 

252 

253def header(tid): 

254 return f'\n## {tid} :{tid}\n' 

255 

256 

257def subhead(category, text): 

258 return as_category(category) + ' ' + text + '\n' 

259 

260 

261def link(target, text): 

262 return f'[{text}](../{target})' 

263 

264 

265def first_line(s): 

266 return (s or '').strip().split('\n')[0].strip() 

267 

268 

269def table(heads, rows): 

270 widths = [len(h) for h in heads] 

271 

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 ] 

280 

281 def field(n, v): 

282 return str(v).ljust(widths[n]) 

283 

284 def row(r): 

285 return ' | '.join(field(n, v) for n, v in enumerate(r)) 

286 

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' 

290 

291 

292def escape(s, quote=True): 

293 s = s.replace("&", "&") 

294 s = s.replace("<", "&lt;") 

295 s = s.replace(">", "&gt;") 

296 if quote: 

297 s = s.replace('"', "&quot;") 

298 return s 

299 

300 

301nl = '\n'.join