Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/genshi/template/text.py: 19%

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

135 statements  

1# -*- coding: utf-8 -*- 

2# 

3# Copyright (C) 2006-2009 Edgewall Software 

4# All rights reserved. 

5# 

6# This software is licensed as described in the file COPYING, which 

7# you should have received as part of this distribution. The terms 

8# are also available at http://genshi.edgewall.org/wiki/License. 

9# 

10# This software consists of voluntary contributions made by many 

11# individuals. For the exact contribution history, see the revision 

12# history and logs, available at http://genshi.edgewall.org/log/. 

13 

14"""Plain text templating engine. 

15 

16This module implements two template language syntaxes, at least for a certain 

17transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines 

18a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other 

19hand is inspired by the syntax of the Django template language, which has more 

20explicit delimiting of directives, and is more flexible with regards to 

21white space and line breaks. 

22 

23In a future release, `OldTextTemplate` will be phased out in favor of 

24`NewTextTemplate`, as the names imply. Therefore the new syntax is strongly 

25recommended for new projects, and existing projects may want to migrate to the 

26new syntax to remain compatible with future Genshi releases. 

27""" 

28 

29import re 

30 

31from genshi.compat import text_type 

32from genshi.core import TEXT 

33from genshi.template.base import BadDirectiveError, Template, \ 

34 TemplateSyntaxError, EXEC, INCLUDE, SUB 

35from genshi.template.eval import Suite 

36from genshi.template.directives import * 

37from genshi.template.interpolation import interpolate 

38 

39__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate'] 

40__docformat__ = 'restructuredtext en' 

41 

42 

43class NewTextTemplate(Template): 

44 r"""Implementation of a simple text-based template engine. This class will 

45 replace `OldTextTemplate` in a future release. 

46  

47 It uses a more explicit delimiting style for directives: instead of the old 

48 style which required putting directives on separate lines that were prefixed 

49 with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs 

50 (by default ``{% ... %}`` and ``{# ... #}``, respectively). 

51  

52 Variable substitution uses the same interpolation syntax as for markup 

53 languages: simple references are prefixed with a dollar sign, more complex 

54 expression enclosed in curly braces. 

55  

56 >>> tmpl = NewTextTemplate('''Dear $name, 

57 ...  

58 ... {# This is a comment #} 

59 ... We have the following items for you: 

60 ... {% for item in items %} 

61 ... * ${'Item %d' % item} 

62 ... {% end %} 

63 ... ''') 

64 >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) 

65 Dear Joe, 

66 <BLANKLINE> 

67 <BLANKLINE> 

68 We have the following items for you: 

69 <BLANKLINE> 

70 * Item 1 

71 <BLANKLINE> 

72 * Item 2 

73 <BLANKLINE> 

74 * Item 3 

75 <BLANKLINE> 

76 <BLANKLINE> 

77  

78 By default, no spaces or line breaks are removed. If a line break should 

79 not be included in the output, prefix it with a backslash: 

80  

81 >>> tmpl = NewTextTemplate('''Dear $name, 

82 ...  

83 ... {# This is a comment #}\ 

84 ... We have the following items for you: 

85 ... {% for item in items %}\ 

86 ... * $item 

87 ... {% end %}\ 

88 ... ''') 

89 >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) 

90 Dear Joe, 

91 <BLANKLINE> 

92 We have the following items for you: 

93 * 1 

94 * 2 

95 * 3 

96 <BLANKLINE> 

97  

98 Backslashes are also used to escape the start delimiter of directives and 

99 comments: 

100 

101 >>> tmpl = NewTextTemplate('''Dear $name, 

102 ...  

103 ... \\{# This is a comment #} 

104 ... We have the following items for you: 

105 ... {% for item in items %}\ 

106 ... * $item 

107 ... {% end %}\ 

108 ... ''') 

109 >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) 

110 Dear Joe, 

111 <BLANKLINE> 

112 {# This is a comment #} 

113 We have the following items for you: 

114 * 1 

115 * 2 

116 * 3 

117 <BLANKLINE> 

118  

119 :since: version 0.5 

120 """ 

121 directives = [('def', DefDirective), 

122 ('when', WhenDirective), 

123 ('otherwise', OtherwiseDirective), 

124 ('for', ForDirective), 

125 ('if', IfDirective), 

126 ('choose', ChooseDirective), 

127 ('with', WithDirective)] 

128 serializer = 'text' 

129 

130 _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)' 

131 _ESCAPE_RE = r'\\\n|\\\r\n|\\(\\)|\\(%s)|\\(%s)' 

132 

133 def __init__(self, source, filepath=None, filename=None, loader=None, 

134 encoding=None, lookup='strict', allow_exec=False, 

135 delims=('{%', '%}', '{#', '#}')): 

136 self.delimiters = delims 

137 Template.__init__(self, source, filepath=filepath, filename=filename, 

138 loader=loader, encoding=encoding, lookup=lookup) 

139 

140 def _get_delims(self): 

141 return self._delims 

142 def _set_delims(self, delims): 

143 if len(delims) != 4: 

144 raise ValueError('delimiers tuple must have exactly four elements') 

145 self._delims = delims 

146 self._directive_re = re.compile(self._DIRECTIVE_RE % tuple( 

147 [re.escape(d) for d in delims] 

148 ), re.DOTALL) 

149 self._escape_re = re.compile(self._ESCAPE_RE % tuple( 

150 [re.escape(d) for d in delims[::2]] 

151 )) 

152 delimiters = property(_get_delims, _set_delims, """\ 

153 The delimiters for directives and comments. This should be a four item tuple 

154 of the form ``(directive_start, directive_end, comment_start, 

155 comment_end)``, where each item is a string. 

156 """) 

157 

158 def _parse(self, source, encoding): 

159 """Parse the template from text input.""" 

160 stream = [] # list of events of the "compiled" template 

161 dirmap = {} # temporary mapping of directives to elements 

162 depth = 0 

163 

164 source = source.read() 

165 if not isinstance(source, text_type): 

166 source = source.decode(encoding or 'utf-8', 'replace') 

167 offset = 0 

168 lineno = 1 

169 

170 _escape_sub = self._escape_re.sub 

171 def _escape_repl(mo): 

172 groups = [g for g in mo.groups() if g] 

173 if not groups: 

174 return '' 

175 return groups[0] 

176 

177 for idx, mo in enumerate(self._directive_re.finditer(source)): 

178 start, end = mo.span(1) 

179 if start > offset: 

180 text = _escape_sub(_escape_repl, source[offset:start]) 

181 for kind, data, pos in interpolate(text, self.filepath, lineno, 

182 lookup=self.lookup): 

183 stream.append((kind, data, pos)) 

184 lineno += len(text.splitlines()) 

185 

186 lineno += len(source[start:end].splitlines()) 

187 command, value = mo.group(2, 3) 

188 

189 if command == 'include': 

190 pos = (self.filename, lineno, 0) 

191 value = list(interpolate(value, self.filepath, lineno, 0, 

192 lookup=self.lookup)) 

193 if len(value) == 1 and value[0][0] is TEXT: 

194 value = value[0][1] 

195 stream.append((INCLUDE, (value, None, []), pos)) 

196 

197 elif command == 'python': 

198 if not self.allow_exec: 

199 raise TemplateSyntaxError('Python code blocks not allowed', 

200 self.filepath, lineno) 

201 try: 

202 suite = Suite(value, self.filepath, lineno, 

203 lookup=self.lookup) 

204 except SyntaxError as err: 

205 raise TemplateSyntaxError(err, self.filepath, 

206 lineno + (err.lineno or 1) - 1) 

207 pos = (self.filename, lineno, 0) 

208 stream.append((EXEC, suite, pos)) 

209 

210 elif command == 'end': 

211 depth -= 1 

212 if depth in dirmap: 

213 directive, start_offset = dirmap.pop(depth) 

214 substream = stream[start_offset:] 

215 stream[start_offset:] = [(SUB, ([directive], substream), 

216 (self.filepath, lineno, 0))] 

217 

218 elif command: 

219 cls = self.get_directive(command) 

220 if cls is None: 

221 raise BadDirectiveError(command) 

222 directive = 0, cls, value, None, (self.filepath, lineno, 0) 

223 dirmap[depth] = (directive, len(stream)) 

224 depth += 1 

225 

226 offset = end 

227 

228 if offset < len(source): 

229 text = _escape_sub(_escape_repl, source[offset:]) 

230 for kind, data, pos in interpolate(text, self.filepath, lineno, 

231 lookup=self.lookup): 

232 stream.append((kind, data, pos)) 

233 

234 return stream 

235 

236 

237class OldTextTemplate(Template): 

238 """Legacy implementation of the old syntax text-based templates. This class 

239 is provided in a transition phase for backwards compatibility. New code 

240 should use the `NewTextTemplate` class and the improved syntax it provides. 

241  

242 >>> tmpl = OldTextTemplate('''Dear $name, 

243 ...  

244 ... We have the following items for you: 

245 ... #for item in items 

246 ... * $item 

247 ... #end 

248 ...  

249 ... All the best, 

250 ... Foobar''') 

251 >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None)) 

252 Dear Joe, 

253 <BLANKLINE> 

254 We have the following items for you: 

255 * 1 

256 * 2 

257 * 3 

258 <BLANKLINE> 

259 All the best, 

260 Foobar 

261 """ 

262 directives = [('def', DefDirective), 

263 ('when', WhenDirective), 

264 ('otherwise', OtherwiseDirective), 

265 ('for', ForDirective), 

266 ('if', IfDirective), 

267 ('choose', ChooseDirective), 

268 ('with', WithDirective)] 

269 serializer = 'text' 

270 

271 _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|' 

272 r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)', 

273 re.MULTILINE) 

274 

275 def _parse(self, source, encoding): 

276 """Parse the template from text input.""" 

277 stream = [] # list of events of the "compiled" template 

278 dirmap = {} # temporary mapping of directives to elements 

279 depth = 0 

280 

281 source = source.read() 

282 if not isinstance(source, text_type): 

283 source = source.decode(encoding or 'utf-8', 'replace') 

284 offset = 0 

285 lineno = 1 

286 

287 for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)): 

288 start, end = mo.span() 

289 if start > offset: 

290 text = source[offset:start] 

291 for kind, data, pos in interpolate(text, self.filepath, lineno, 

292 lookup=self.lookup): 

293 stream.append((kind, data, pos)) 

294 lineno += len(text.splitlines()) 

295 

296 text = source[start:end].lstrip()[1:] 

297 lineno += len(text.splitlines()) 

298 directive = text.split(None, 1) 

299 if len(directive) > 1: 

300 command, value = directive 

301 else: 

302 command, value = directive[0], None 

303 

304 if command == 'end': 

305 depth -= 1 

306 if depth in dirmap: 

307 directive, start_offset = dirmap.pop(depth) 

308 substream = stream[start_offset:] 

309 stream[start_offset:] = [(SUB, ([directive], substream), 

310 (self.filepath, lineno, 0))] 

311 elif command == 'include': 

312 pos = (self.filename, lineno, 0) 

313 stream.append((INCLUDE, (value.strip(), None, []), pos)) 

314 elif command != '#': 

315 cls = self.get_directive(command) 

316 if cls is None: 

317 raise BadDirectiveError(command) 

318 directive = 0, cls, value, None, (self.filepath, lineno, 0) 

319 dirmap[depth] = (directive, len(stream)) 

320 depth += 1 

321 

322 offset = end 

323 

324 if offset < len(source): 

325 text = source[offset:].replace('\\#', '#') 

326 for kind, data, pos in interpolate(text, self.filepath, lineno, 

327 lookup=self.lookup): 

328 stream.append((kind, data, pos)) 

329 

330 return stream 

331 

332 

333TextTemplate = OldTextTemplate