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

1import re 

2from collections import defaultdict 

3 

4from wikitextprocessor import Page, Wtp 

5 

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) 

13 

14 

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

27 

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(("#", "*", ";", ":")) 

31 

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

69 

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 

78 

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

97 

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

122 

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

144 

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) 

159 

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 ) 

168 

169 return included_templates, pre_expand