Coverage for src/wiktextract/extractor/fr/form_line.py: 84%

170 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-16 00:49 +0000

1from wikitextprocessor.parser import HTMLNode, NodeKind, TemplateNode, WikiNode 

2 

3from ...page import clean_node 

4from ...wxr_context import WiktextractContext 

5from .conjugation import extract_conjugation, extract_declension_page 

6from .models import Form, Linkage, Sound, WordEntry 

7from .pronunciation import ( 

8 ASPIRATED_H_TEMPLATES, 

9 PRON_TEMPLATES, 

10 process_pron_template, 

11) 

12from .tags import translate_raw_tags 

13 

14 

15def extract_form_line( 

16 wxr: WiktextractContext, 

17 page_data: list[WordEntry], 

18 nodes: list[WikiNode | str], 

19) -> None: 

20 """ 

21 Ligne de forme 

22 https://fr.wiktionary.org/wiki/Wiktionnaire:Structure_des_pages#Syntaxe 

23 

24 A line of wikitext between pos subtitle and the first gloss, contains IPA, 

25 gender and inflection forms. 

26 """ 

27 IGNORE_TEMPLATES = frozenset( 

28 ["voir-conj", "genre ?", "nombre ?", "pluriel ?", "réf"] 

29 ) 

30 

31 pre_template_name = "" 

32 first_bold = True 

33 skip_iterations = 0 

34 for index, node in enumerate(nodes): 

35 if skip_iterations > 0: 

36 skip_iterations -= 1 

37 continue 

38 if isinstance(node, TemplateNode): 

39 if node.template_name in IGNORE_TEMPLATES: 39 ↛ 40line 39 didn't jump to line 40 because the condition on line 39 was never true

40 continue 

41 elif node.template_name in PRON_TEMPLATES: 

42 page_data[-1].sounds.extend( 

43 process_pron_template( 

44 wxr, 

45 node, 

46 [], 

47 page_data[-1].lang_code, 

48 nodes[index - 1 : index], 

49 ) 

50 ) 

51 elif node.template_name == "équiv-pour": 

52 process_equiv_pour_template(wxr, node, page_data) 

53 elif node.template_name.startswith("zh-mot"): 53 ↛ 54line 53 didn't jump to line 54 because the condition on line 53 was never true

54 process_zh_mot_template(wxr, node, page_data) 

55 elif node.template_name == "ja-mot": 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

56 process_ja_mot_template(wxr, node, page_data) 

57 elif node.template_name in ( 

58 "conj", 

59 "conjugaison", 

60 ) or node.template_name.startswith(("ja-adj-", "ja-verbe")): 

61 process_conj_template(wxr, node, page_data) 

62 elif node.template_name in ASPIRATED_H_TEMPLATES: 

63 continue 

64 elif node.template_name == "lien pronominal": 

65 process_lien_pronominal(wxr, node, page_data) 

66 elif node.template_name == "note": 

67 note = clean_node(wxr, page_data[-1], nodes[index + 1 :]) 

68 if note != "": 68 ↛ 70line 68 didn't jump to line 70 because the condition on line 68 was always true

69 page_data[-1].notes.append(note) 

70 break 

71 else: 

72 raw_tag = clean_node(wxr, page_data[-1], node) 

73 expanded_template = wxr.wtp.parse( 

74 wxr.wtp.node_to_wikitext(node), expand_all=True 

75 ) 

76 if ( 

77 len( 

78 list( 

79 expanded_template.find_html( 

80 "span", attr_name="id", attr_value="région" 

81 ) 

82 ) 

83 ) 

84 == 1 

85 and pre_template_name in PRON_TEMPLATES 

86 and len(page_data[-1].sounds) > 0 

87 ): 

88 # it's the location of the previous IPA template 

89 # https://fr.wiktionary.org/wiki/Modèle:région 

90 page_data[-1].sounds[-1].raw_tags.append( 

91 raw_tag.strip("()") 

92 ) 

93 elif len(raw_tag.strip("()")) > 0: 

94 if raw_tag.startswith("(") and raw_tag.endswith(")"): 

95 raw_tag = raw_tag.strip("()") 

96 page_data[-1].raw_tags.append(raw_tag) 

97 

98 pre_template_name = node.template_name 

99 elif isinstance(node, WikiNode) and node.kind == NodeKind.ITALIC: 

100 raw_tag = clean_node(wxr, None, node) 

101 if raw_tag != "ou": 

102 page_data[-1].raw_tags.append(raw_tag) 

103 elif isinstance(node, WikiNode) and node.kind == NodeKind.LINK: 

104 process_conj_link_node(wxr, node, page_data) 

105 elif ( 

106 isinstance(node, WikiNode) 

107 and node.kind == NodeKind.BOLD 

108 and first_bold 

109 ): 

110 process_form_line_bold_node(wxr, node, page_data[-1]) 

111 first_bold = False 

112 elif isinstance(node, str) and "(" in node: 

113 skip_iterations, forms, related = handle_parens( 

114 wxr, 

115 nodes[index:], # including this node with the "(" 

116 ) 

117 # No code for forms data implemented yet 

118 # page_data[-1].forms.extend(forms) 

119 page_data[-1].related.extend(related) 

120 

121 translate_raw_tags(page_data[-1]) 

122 

123 

124def process_equiv_pour_template( 

125 wxr: WiktextractContext, node: TemplateNode, page_data: list[WordEntry] 

126) -> list[Form]: 

127 # equivalent form: https://fr.wiktionary.org/wiki/Modèle:équiv-pour 

128 expanded_node = wxr.wtp.parse( 

129 wxr.wtp.node_to_wikitext(node), expand_all=True 

130 ) 

131 raw_gender_tag = "" 

132 gender_tags = { 

133 "un homme": "masculine", 

134 "une femme": "feminine", 

135 "des femmes": "feminine", 

136 "le mâle": "masculine", 

137 "la femelle": "feminine", 

138 "un garçon": "masculine", 

139 "une fille": "feminine", 

140 "une personne non-binaire": "neuter", 

141 } 

142 forms = [] 

143 for child in expanded_node.find_child(NodeKind.ITALIC | NodeKind.HTML): 

144 if child.kind == NodeKind.ITALIC: 

145 raw_gender_tag = clean_node(wxr, None, child).strip("() ") 

146 raw_gender_tag = raw_gender_tag.removeprefix("pour ").rsplit( 

147 ",", 1 

148 )[0] 

149 elif isinstance(child, HTMLNode) and child.tag == "bdi": 149 ↛ 143line 149 didn't jump to line 143 because the condition on line 149 was always true

150 form_data = Form( 

151 form=clean_node(wxr, None, child), 

152 source="form line template 'équiv-pour'", 

153 ) 

154 if len(raw_gender_tag) > 0: 154 ↛ 159line 154 didn't jump to line 159 because the condition on line 154 was always true

155 if raw_gender_tag in gender_tags: 155 ↛ 158line 155 didn't jump to line 158 because the condition on line 155 was always true

156 form_data.tags.append(gender_tags[raw_gender_tag]) 

157 else: 

158 form_data.raw_tags.append(raw_gender_tag) 

159 if len(form_data.form) > 0: 159 ↛ 143line 159 didn't jump to line 143 because the condition on line 159 was always true

160 if len(page_data) > 0: 

161 page_data[-1].forms.append(form_data) 

162 forms.append(form_data) 

163 return forms 

164 

165 

166def process_zh_mot_template( 

167 wxr: WiktextractContext, 

168 node: TemplateNode, 

169 page_data: list[WordEntry], 

170) -> None: 

171 # Chinese form line template: zh-mot, zh-mot-s, zh-mot-t 

172 # https://fr.wiktionary.org/wiki/Modèle:zh-mot 

173 node = wxr.wtp.parse( 

174 wxr.wtp.node_to_wikitext(node), 

175 pre_expand=True, 

176 additional_expand={node.template_name}, 

177 ) 

178 for template_node in node.find_child(NodeKind.TEMPLATE): 

179 if template_node.template_name.lower() == "lang": 

180 page_data[-1].sounds.append( 

181 Sound( 

182 zh_pron=clean_node(wxr, None, template_node), 

183 tags=["Pinyin"], 

184 ) 

185 ) 

186 elif template_node.template_name in ("pron", "prononciation"): 186 ↛ 178line 186 didn't jump to line 178 because the condition on line 186 was always true

187 page_data[-1].sounds.append( 

188 Sound(ipa=clean_node(wxr, None, template_node)) 

189 ) 

190 

191 

192def process_ja_mot_template( 

193 wxr: WiktextractContext, 

194 template_node: TemplateNode, 

195 page_data: list[WordEntry], 

196) -> None: 

197 # Japanese form line template: https://fr.wiktionary.org/wiki/Modèle:ja-mot 

198 expanded_node = wxr.wtp.parse( 

199 wxr.wtp.node_to_wikitext(template_node), expand_all=True 

200 ) 

201 existing_forms = { 

202 existing_form.form for existing_form in page_data[-1].forms 

203 } 

204 for index, node in expanded_node.find_html("span", with_index=True): 

205 # the first span tag is the word, the second is Hepburn romanization 

206 if index == 1: 

207 form_text = clean_node(wxr, None, node) 

208 if form_text not in existing_forms: 

209 # avoid adding duplicated form data extracted from 

210 # inflection table before the form line 

211 page_data[-1].forms.append( 

212 Form(form=form_text, tags=["romanization"]) 

213 ) 

214 break 

215 

216 

217def process_conj_template( 

218 wxr: WiktextractContext, 

219 template_node: TemplateNode, 

220 page_data: list[WordEntry], 

221) -> None: 

222 # https://fr.wiktionary.org/wiki/Modèle:conjugaison 

223 expanded_node = wxr.wtp.parse( 

224 wxr.wtp.node_to_wikitext(template_node), expand_all=True 

225 ) 

226 for link in expanded_node.find_child(NodeKind.LINK): 

227 process_conj_link_node(wxr, link, page_data) 

228 

229 tag = clean_node(wxr, page_data[-1], expanded_node) 

230 if template_node.template_name in ("conj", "conjugaison"): 

231 tag = tag.removesuffix("(voir la conjugaison)").strip() 

232 elif template_node.template_name.startswith("ja-"): 232 ↛ 236line 232 didn't jump to line 236 because the condition on line 232 was always true

233 tag = ( 

234 tag.removesuffix("(conjugaison)").removesuffix("(flexions)").strip() 

235 ) 

236 if len(tag) > 0: 

237 page_data[-1].raw_tags.append(tag) 

238 

239 

240def is_conj_link(wxr: WiktextractContext, link: WikiNode) -> bool: 

241 if len(link.largs) == 0 or len(link.largs[0]) == 0: 241 ↛ 242line 241 didn't jump to line 242 because the condition on line 241 was never true

242 return False 

243 conj_title = clean_node(wxr, None, link.largs[0][0]) 

244 return conj_title.startswith(("Conjugaison:", "Annexe:Déclinaison en")) 

245 

246 

247def process_conj_link_node( 

248 wxr: WiktextractContext, 

249 link: WikiNode, 

250 page_data: list[WordEntry], 

251) -> None: 

252 if not is_conj_link(wxr, link): 

253 return 

254 conj_title = link.largs[0][0] 

255 if "/" not in conj_title: 255 ↛ 256line 255 didn't jump to line 256 because the condition on line 255 was never true

256 return 

257 if "#" in conj_title: 257 ↛ 258line 257 didn't jump to line 258 because the condition on line 257 was never true

258 conj_title = conj_title[: conj_title.index("#")] 

259 conj_word = conj_title.split("/", 1)[-1] 

260 if conj_word in ( 

261 "Premier groupe", 

262 "Deuxième groupe", 

263 "Troisième groupe", 

264 ): 

265 return 

266 if ( 266 ↛ 273line 266 didn't jump to line 273 because the condition on line 266 was never true

267 len(page_data) > 1 

268 and page_data[-2].lang_code == page_data[-1].lang_code 

269 and page_data[-2].pos == page_data[-1].pos 

270 and len(page_data[-2].forms) > 0 

271 and page_data[-2].forms[-1].source == conj_title 

272 ): 

273 if "canonical" in page_data[-2].forms[0].tags: 

274 # An earlier weird head word, like "hodit se" reflexive form 

275 # in hodit/Czech, should not override a later head form, either 

276 # another "canonical" entry or a default nothing. 

277 page_data[-1].forms.extend(page_data[-2].forms[1:]) 

278 else: 

279 page_data[-1].forms.extend(page_data[-2].forms) 

280 elif conj_title.startswith("Conjugaison:"): 

281 extract_conjugation(wxr, page_data[-1], conj_title) 

282 elif conj_title.startswith("Annexe:Déclinaison en"): 282 ↛ exitline 282 didn't return from function 'process_conj_link_node' because the condition on line 282 was always true

283 extract_declension_page(wxr, page_data[-1], conj_title) 

284 

285 

286def process_lien_pronominal( 

287 wxr: WiktextractContext, 

288 template_node: TemplateNode, 

289 page_data: list[WordEntry], 

290) -> None: 

291 # https://fr.wiktionary.org/wiki/Modèle:lien_pronominal 

292 expanded_node = wxr.wtp.parse( 

293 wxr.wtp.node_to_wikitext(template_node), expand_all=True 

294 ) 

295 for bdi_tag in expanded_node.find_html_recursively("bdi"): 

296 form = Form(form=clean_node(wxr, None, bdi_tag), tags=["pronominal"]) 

297 if form.form != "": 297 ↛ 295line 297 didn't jump to line 295 because the condition on line 297 was always true

298 page_data[-1].forms.append(form) 

299 clean_node(wxr, page_data[-1], expanded_node) 

300 

301 

302def process_form_line_bold_node( 

303 wxr: WiktextractContext, bold_node: WikiNode, word_entry: WordEntry 

304): 

305 bold_str = clean_node(wxr, None, bold_node) 

306 if wxr.wtp.title.startswith("Titres non pris en charge/"): 

307 # Unsupported titles: 

308 # https://fr.wiktionary.org/wiki/Annexe:Titres_non_pris_en_charge 

309 # https://fr.wiktionary.org/wiki/Spécial:Index/Titres_non_pris_en_charge 

310 word_entry.word = bold_str 

311 word_entry.original_title = wxr.wtp.title 

312 elif bold_str not in [wxr.wtp.title, ""]: 

313 word_entry.forms.append(Form(form=bold_str, tags=["canonical"])) 

314 

315 

316def handle_parens( 

317 wxr: WiktextractContext, 

318 nodes: list[WikiNode | str], 

319) -> tuple[int, list[Form], list[Linkage]]: 

320 # Scan the line with a parenthesized block with a colon in it, like 

321 # (imperfectif : posílat), and handle that; return number of nodes 

322 # that the main loop should skip, if they've been processed 

323 # here already, and the contents extracted. 

324 end_paren = False 

325 colon_i: None | int = None 

326 for i, node in enumerate(nodes): 

327 if isinstance(node, str): 

328 stripped = node.strip() 

329 if stripped.endswith(":"): 

330 colon_i = i 

331 if stripped.endswith(")"): 

332 end_paren = True 

333 break 

334 

335 if colon_i is None: 

336 # We did not find a sensible colon 

337 return 0, [], [] 

338 

339 if not end_paren: 339 ↛ 340line 339 didn't jump to line 340 because the condition on line 339 was never true

340 wxr.wtp.warning( 

341 f"Did not find sensible end-paren? " 

342 f"Start-paren context {nodes[0:2]=}", 

343 sortid="form_line/handle_parens", 

344 ) 

345 return 0, [], [] 

346 

347 before = nodes[: colon_i + 1] 

348 after = nodes[colon_i + 1 : i] 

349 

350 raw_tag = clean_node(wxr, None, before).strip(" \n(:") 

351 target = clean_node(wxr, None, after).strip(" \n)") 

352 

353 # Currently implemented only detecting "related" words, like 

354 # poslat/czech 

355 if raw_tag and target: 355 ↛ 360line 355 didn't jump to line 360 because the condition on line 355 was always true

356 rel = Linkage(raw_tags=[raw_tag], word=target) 

357 translate_raw_tags(rel) 

358 return i, [], [rel] 

359 

360 return 0, [], []