Coverage for src/wiktextract/extractor/en/analyze_template.py: 89%
62 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
1import re
2from collections import defaultdict
4from wikitextprocessor import Page, Wtp
6# Templates whose body is nothing but a "{{#invoke:...}}" call.
7FORCE_PRE_EXPAND = frozenset(
8 [
9 "Template:inflection-table-top",
10 "Template:inflection-table-bottom",
11 ]
12)
15def analyze_template(wtp: Wtp, page: Page) -> tuple[set[str], bool]:
16 """Analyzes a template body and returns a set of the canonicalized
17 names of all other templates it calls and a boolean that is True
18 if it should be pre-expanded before final parsing and False if it
19 need not be pre-expanded. The pre-expanded flag is determined
20 based on that body only; the caller should propagate it to
21 templates that include the given template. This does not work for
22 template and template function calls where the name is generated by
23 other expansions."""
24 if page.redirect_to is not None or page.body is None: 24 ↛ 25line 24 didn't jump to line 25 because the condition on line 24 was never true
25 return set(), False
26 included_templates: set[str] = set()
28 # Determine if the template starts with a list item
29 # XXX should we expand other templates that produce list items???
30 contains_list = page.body.startswith(("#", "*", ";", ":"))
32 # Remove paired tables.
33 # What is left is unpaired tables, which is an indication that a
34 # template somewhere should be generating those table eventually,
35 # and thus needs to be pre-expanded.
36 table_start_pos = []
37 table_end_pos = []
38 # `[[wikt:/|}]]` in Template:Mon standard keyboard
39 # and `{{l|mul|} }}` in Template:punctuation are not end of table token
40 # but `|}]]` in Template:Lithuania map is a table
41 for m in re.finditer(
42 r"""
43 (?<!{){\| # `{|` not after `{`, like `{{{|}}}`
44 |
45 \|}(?!\s*}) # `|}` not before ` }`
46 """,
47 page.body,
48 re.VERBOSE,
49 ):
50 if m.group() == "{|":
51 table_start_pos.append(m.start())
52 else:
53 table_end_pos.append(m.end())
54 num_table_start = len(table_start_pos)
55 num_table_end = len(table_end_pos)
56 contains_unpaired_table = num_table_start != num_table_end
57 table_start = len(page.body)
58 table_end = table_start
59 if num_table_start > num_table_end and num_table_end > 0: 59 ↛ 60line 59 didn't jump to line 60 because the condition on line 59 was never true
60 table_start = table_start_pos[num_table_start - num_table_end - 1]
61 table_end = table_end_pos[-1]
62 elif num_table_start < num_table_end and num_table_start > 0: 62 ↛ 63line 62 didn't jump to line 63 because the condition on line 62 was never true
63 table_start = table_start_pos[0]
64 table_end = table_end_pos[num_table_start]
65 elif num_table_start > 0 and num_table_end > 0:
66 table_start = table_start_pos[0]
67 table_end = table_end_pos[-1]
68 unpaired_text = page.body[:table_start] + page.body[table_end:]
70 # Determine if the template contains table element tokens
71 # outside paired table start/end. We only try to look for
72 # these outside templates, as it is common to write each
73 # template argument on its own line starting with a "|".
74 outside = unpaired_text
75 while True:
76 # print("=== OUTSIDE ITER")
77 prev = outside
79 # handle {{{ }}} parameters without templates inside them
80 while True:
81 newt = re.sub(
82 # re.X, ignore white space and comments
83 r"""(?sx)\{\{\{ # {{{
84 ( [^{}] # no {} except...
85 | \}[^}] # no }} unless...
86 | \}\}[^}] # they're definitely not }}}
87 )*?
88 \}\}\} # }}}
89 """,
90 "",
91 prev,
92 )
93 if newt == prev:
94 break
95 prev = newt
96 # print("After arg elim: {!r}".format(newt))
98 # Handle templates
99 newt = re.sub(
100 r"""(?sx)\{\{
101 ( [^{}]
102 | \}[^}]
103 )*?
104 \}\}""",
105 "",
106 newt,
107 )
108 # print("After templ elim: {!r}".format(newt))
109 if newt == outside:
110 break
111 outside = newt
112 # Check if the template contains certain table elements
113 # start of line plus |+, |- or |!
114 m = re.search(r"(?s)(^|\n)(\|\+|\|-|\!)", outside)
115 m2 = re.match(r"(?si)\s*(<includeonly>|<!--.*?-->)(\|\||!!)", outside)
116 contains_table_element = m is not None or m2 is not None
117 # if contains_table_element:
118 # print("contains_table_element {!r} at {}"
119 # .format(m.group(0), m.start()))
120 # print("... {!r} ...".format(outside[m.start() - 10:m.end() + 10]))
121 # print(repr(outside))
123 # Check for unpaired HTML tags
124 tag_cnts: defaultdict[str, int] = defaultdict(int)
125 for m in re.finditer(
126 r"(?si)<(/)?({})\b\s*[^>]*(/)?>" r"".format(
127 "|".join(wtp.paired_html_tags)
128 ),
129 outside,
130 ):
131 start_slash = m.group(1)
132 tagname = m.group(2)
133 end_slash = m.group(3)
134 if start_slash:
135 tag_cnts[tagname] -= 1
136 elif not end_slash: 136 ↛ 125line 136 didn't jump to line 125 because the condition on line 136 was always true
137 tag_cnts[tagname] += 1
138 contains_unbalanced_html = any(v != 0 for v in tag_cnts.values())
139 # if contains_unbalanced_html:
140 # print(name, "UNBALANCED HTML")
141 # for k, v in tag_cnts.items():
142 # if v != 0:
143 # print(" {} {}".format(v, k))
145 # Determine which other templates are called from unpaired text.
146 # None of the flags we currently gather propagate outside a paired
147 # table start/end.
148 for m in re.finditer(
149 # capture the first parameter of a template, ie. the name
150 r"""(?sx)(^ | [^{]) # start
151 (\{\{)?\{\{([^{]*?) # ( ({{) {{ (name) )
152 (\| | \}\}) # | or }}""",
153 unpaired_text,
154 ):
155 called_template = m.group(3)
156 called_template = re.sub(r"(?si)<nowiki\s*/>", "", called_template)
157 if len(called_template) > 0: 157 ↛ 148line 157 didn't jump to line 148 because the condition on line 157 was always true
158 included_templates.add(called_template)
160 # Determine whether this template should be pre-expanded
161 pre_expand = (
162 page.title in FORCE_PRE_EXPAND
163 or contains_list
164 or contains_unpaired_table
165 or contains_table_element
166 or contains_unbalanced_html
167 )
169 return included_templates, pre_expand