Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/docutils/transforms/universal.py: 25%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

161 statements  

1# $Id$ 

2# Authors: David Goodger <goodger@python.org>; Ueli Schlaepfer; Günter Milde 

3# Maintainer: docutils-develop@lists.sourceforge.net 

4# Copyright: This module has been placed in the public domain. 

5 

6""" 

7Transforms needed by most or all documents: 

8 

9- `Decorations`: Generate a document's header & footer. 

10- `ExposeInternals`: Expose internal attributes. 

11- `Messages`: Placement of system messages generated after parsing. 

12- `FilterMessages`: Remove system messages below verbosity threshold. 

13- `TestMessages`: Like `Messages`, used on test runs. 

14- `StripComments`: Remove comment elements from the document tree. 

15- `StripClassesAndElements`: Remove elements with classes 

16 in `self.document.settings.strip_elements_with_classes` 

17 and class values in `self.document.settings.strip_classes`. 

18- `SmartQuotes`: Replace ASCII quotation marks with typographic form. 

19""" 

20 

21__docformat__ = 'reStructuredText' 

22 

23import re 

24import time 

25from docutils import nodes, utils 

26from docutils.transforms import Transform 

27from docutils.utils import smartquotes 

28 

29 

30class Decorations(Transform): 

31 

32 """ 

33 Populate a document's decoration element (header, footer). 

34 """ 

35 

36 default_priority = 820 

37 

38 def apply(self): 

39 header_nodes = self.generate_header() 

40 if header_nodes: 

41 decoration = self.document.get_decoration() 

42 header = decoration.get_header() 

43 header.extend(header_nodes) 

44 footer_nodes = self.generate_footer() 

45 if footer_nodes: 

46 decoration = self.document.get_decoration() 

47 footer = decoration.get_footer() 

48 footer.extend(footer_nodes) 

49 

50 def generate_header(self): 

51 return None 

52 

53 def generate_footer(self): 

54 # @@@ Text is hard-coded for now. 

55 # Should be made dynamic (language-dependent). 

56 # @@@ Use timestamp from the `SOURCE_DATE_EPOCH`_ environment variable 

57 # for the datestamp? 

58 # See https://sourceforge.net/p/docutils/patches/132/ 

59 # and https://reproducible-builds.org/specs/source-date-epoch/ 

60 settings = self.document.settings 

61 if (settings.generator or settings.datestamp 

62 or settings.source_link or settings.source_url): 

63 text = [] 

64 if (settings.source_link and settings._source 

65 or settings.source_url): 

66 if settings.source_url: 

67 source = settings.source_url 

68 else: 

69 source = utils.relative_path(settings._destination, 

70 settings._source) 

71 text.extend([ 

72 nodes.reference('', 'View document source', 

73 refuri=source), 

74 nodes.Text('.\n')]) 

75 if settings.datestamp: 

76 datestamp = time.strftime(settings.datestamp, time.gmtime()) 

77 text.append(nodes.Text('Generated on: ' + datestamp + '.\n')) 

78 if settings.generator: 

79 text.extend([ 

80 nodes.Text('Generated by '), 

81 nodes.reference('', 'Docutils', 

82 refuri='https://docutils.sourceforge.io/'), 

83 nodes.Text(' from '), 

84 nodes.reference('', 'reStructuredText', 

85 refuri='https://docutils.sourceforge.io/' 

86 'rst.html'), 

87 nodes.Text(' source.\n')]) 

88 return [nodes.paragraph('', '', *text)] 

89 else: 

90 return None 

91 

92 

93class ExposeInternals(Transform): 

94 

95 """ 

96 Expose internal attributes if ``expose_internals`` setting is set. 

97 """ 

98 

99 default_priority = 840 

100 

101 def not_Text(self, node): 

102 return not isinstance(node, nodes.Text) 

103 

104 def apply(self): 

105 if self.document.settings.expose_internals: 

106 for node in self.document.findall(self.not_Text): 

107 for att in self.document.settings.expose_internals: 

108 value = getattr(node, att, None) 

109 if value is not None: 

110 node['internal:' + att] = value 

111 

112 

113class Messages(Transform): 

114 

115 """ 

116 Place any system messages generated after parsing into a dedicated section 

117 of the document. 

118 """ 

119 

120 default_priority = 860 

121 

122 def apply(self): 

123 messages = self.document.transform_messages 

124 loose_messages = [msg for msg in messages if not msg.parent] 

125 if loose_messages: 

126 section = nodes.section(classes=['system-messages']) 

127 # @@@ get this from the language module? 

128 section += nodes.title('', 'Docutils System Messages') 

129 section += loose_messages 

130 self.document.transform_messages[:] = [] 

131 self.document += section 

132 

133 

134class FilterMessages(Transform): 

135 

136 """ 

137 Remove system messages below verbosity threshold. 

138 

139 Also convert <problematic> nodes referencing removed messages 

140 to <Text> nodes and remove "System Messages" section if empty. 

141 """ 

142 

143 default_priority = 870 

144 

145 def apply(self): 

146 for node in tuple(self.document.findall(nodes.system_message)): 

147 if node['level'] < self.document.reporter.report_level: 

148 node.parent.remove(node) 

149 try: # also remove id-entry 

150 del self.document.ids[node['ids'][0]] 

151 except (IndexError): 

152 pass 

153 for node in tuple(self.document.findall(nodes.problematic)): 

154 if 'refid' in node and node['refid'] not in self.document.ids: 

155 node.parent.replace(node, nodes.Text(node.astext())) 

156 for node in self.document.findall(nodes.section): 

157 if "system-messages" in node['classes'] and len(node) == 1: 

158 node.parent.remove(node) 

159 

160 

161class TestMessages(Transform): 

162 

163 """ 

164 Append all post-parse system messages to the end of the document. 

165 

166 Used for testing purposes. 

167 """ 

168 

169 # marker for pytest to ignore this class during test discovery 

170 __test__ = False 

171 

172 default_priority = 880 

173 

174 def apply(self): 

175 for msg in self.document.transform_messages: 

176 if not msg.parent: 

177 self.document += msg 

178 

179 

180class StripComments(Transform): 

181 

182 """ 

183 Remove comment elements from the document tree (only if the 

184 ``strip_comments`` setting is enabled). 

185 """ 

186 

187 default_priority = 740 

188 

189 def apply(self): 

190 if self.document.settings.strip_comments: 

191 for node in tuple(self.document.findall(nodes.comment)): 

192 node.parent.remove(node) 

193 

194 

195class StripClassesAndElements(Transform): 

196 

197 """ 

198 Remove from the document tree all elements with classes in 

199 `self.document.settings.strip_elements_with_classes` and all "classes" 

200 attribute values in `self.document.settings.strip_classes`. 

201 """ 

202 

203 default_priority = 420 

204 

205 def apply(self): 

206 if self.document.settings.strip_elements_with_classes: 

207 self.strip_elements = {*self.document.settings 

208 .strip_elements_with_classes} 

209 # Iterate over a tuple as removing the current node 

210 # corrupts the iterator returned by `iter`: 

211 for node in tuple(self.document.findall(self.check_classes)): 

212 node.parent.remove(node) 

213 

214 if not self.document.settings.strip_classes: 

215 return 

216 strip_classes = self.document.settings.strip_classes 

217 for node in self.document.findall(nodes.Element): 

218 for class_value in strip_classes: 

219 try: 

220 node['classes'].remove(class_value) 

221 except ValueError: 

222 pass 

223 

224 def check_classes(self, node): 

225 if not isinstance(node, nodes.Element): 

226 return False 

227 for class_value in node['classes'][:]: 

228 if class_value in self.strip_elements: 

229 return True 

230 return False 

231 

232 

233class SmartQuotes(Transform): 

234 

235 """ 

236 Replace ASCII quotation marks with typographic form. 

237 

238 Also replace multiple dashes with em-dash/en-dash characters. 

239 """ 

240 

241 default_priority = 855 

242 

243 nodes_to_skip = (nodes.FixedTextElement, nodes.Special) 

244 """Do not apply "smartquotes" to instances of these block-level nodes.""" 

245 

246 literal_nodes = (nodes.FixedTextElement, nodes.Special, 

247 nodes.image, nodes.literal, nodes.math, 

248 nodes.raw, nodes.problematic) 

249 """Do not apply smartquotes to instances of these inline nodes.""" 

250 

251 smartquotes_action = 'qDe' 

252 """Setting to select smartquote transformations. 

253 

254 The default 'qDe' educates normal quote characters: (", '), 

255 em- and en-dashes (---, --) and ellipses (...). 

256 """ 

257 

258 def __init__(self, document, startnode): 

259 Transform.__init__(self, document, startnode=startnode) 

260 self.unsupported_languages = set() 

261 

262 def get_tokens(self, txtnodes): 

263 # A generator that yields ``(texttype, nodetext)`` tuples for a list 

264 # of "Text" nodes (interface to ``smartquotes.educate_tokens()``). 

265 for node in txtnodes: 

266 if (isinstance(node.parent, self.literal_nodes) 

267 or isinstance(node.parent.parent, self.literal_nodes)): 

268 yield 'literal', str(node) 

269 else: 

270 # SmartQuotes uses backslash escapes instead of null-escapes 

271 # Insert backslashes before escaped "active" characters. 

272 txt = re.sub('(?<=\x00)([-\\\'".`])', r'\\\1', str(node)) 

273 yield 'plain', txt 

274 

275 def apply(self): 

276 smart_quotes = self.document.settings.setdefault('smart_quotes', 

277 False) 

278 if not smart_quotes: 

279 return 

280 try: 

281 alternative = smart_quotes.startswith('alt') 

282 except AttributeError: 

283 alternative = False 

284 

285 document_language = self.document.settings.language_code 

286 lc_smartquotes = self.document.settings.smartquotes_locales 

287 if lc_smartquotes: 

288 smartquotes.smartchars.quotes.update(dict(lc_smartquotes)) 

289 

290 # "Educate" quotes in normal text. Handle each block of text 

291 # (TextElement node) as a unit to keep context around inline nodes: 

292 for node in self.document.findall(nodes.TextElement): 

293 # skip preformatted text blocks and special elements: 

294 if isinstance(node, self.nodes_to_skip): 

295 continue 

296 # nested TextElements are not "block-level" elements: 

297 if isinstance(node.parent, nodes.TextElement): 

298 continue 

299 

300 # list of text nodes in the "text block": 

301 txtnodes = [txtnode for txtnode in node.findall(nodes.Text) 

302 if not isinstance(txtnode.parent, 

303 nodes.option_string)] 

304 

305 # language: use typographical quotes for language "lang" 

306 lang = node.get_language_code(document_language) 

307 # use alternative form if `smart-quotes` setting starts with "alt": 

308 if alternative: 

309 if '-x-altquot' in lang: 

310 lang = lang.replace('-x-altquot', '') 

311 else: 

312 lang += '-x-altquot' 

313 # drop unsupported subtags: 

314 for tag in utils.normalize_language_tag(lang): 

315 if tag in smartquotes.smartchars.quotes: 

316 lang = tag 

317 break 

318 else: # language not supported -- keep ASCII quotes 

319 if lang not in self.unsupported_languages: 

320 self.document.reporter.warning( 

321 'No smart quotes defined for language "%s".' % lang, 

322 base_node=node) 

323 self.unsupported_languages.add(lang) 

324 lang = '' 

325 

326 # Iterator educating quotes in plain text: 

327 # (see "utils/smartquotes.py" for the attribute setting) 

328 teacher = smartquotes.educate_tokens( 

329 self.get_tokens(txtnodes), 

330 attr=self.smartquotes_action, language=lang) 

331 

332 for txtnode, newtext in zip(txtnodes, teacher): 

333 txtnode.parent.replace(txtnode, nodes.Text(newtext)) 

334 

335 self.unsupported_languages.clear()