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
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-16 00:49 +0000
1from wikitextprocessor.parser import HTMLNode, NodeKind, TemplateNode, WikiNode
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
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
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 )
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)
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)
121 translate_raw_tags(page_data[-1])
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
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 )
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
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)
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)
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"))
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)
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)
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"]))
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
335 if colon_i is None:
336 # We did not find a sensible colon
337 return 0, [], []
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, [], []
347 before = nodes[: colon_i + 1]
348 after = nodes[colon_i + 1 : i]
350 raw_tag = clean_node(wxr, None, before).strip(" \n(:")
351 target = clean_node(wxr, None, after).strip(" \n)")
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]
360 return 0, [], []