Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1try: 

2 import ast 

3except ImportError: 

4 from chameleon import ast25 as ast 

5 

6from functools import partial 

7from os.path import dirname 

8from hashlib import md5 

9 

10from ..i18n import simple_translate 

11from ..tales import PythonExpr 

12from ..tales import StringExpr 

13from ..tales import NotExpr 

14from ..tales import ExistsExpr 

15from ..tales import ImportExpr 

16from ..tales import ProxyExpr 

17from ..tales import StructureExpr 

18from ..tales import ExpressionParser 

19from ..tales import DEFAULT_MARKER 

20from ..tal import RepeatDict 

21 

22from ..template import BaseTemplate 

23from ..template import BaseTemplateFile 

24from ..compiler import ExpressionEngine 

25from ..loader import TemplateLoader 

26from ..utils import decode_string 

27from ..utils import string_type 

28from ..utils import unicode_string 

29from ..astutil import Symbol 

30 

31from .program import MacroProgram 

32 

33try: 

34 bytes 

35except NameError: 

36 bytes = str 

37 

38 

39class PageTemplate(BaseTemplate): 

40 """Constructor for the page template language. 

41 

42 Takes a string input as the only positional argument:: 

43 

44 template = PageTemplate("<div>Hello, ${name}.</div>") 

45 

46 Configuration (keyword arguments): 

47 

48 ``auto_reload`` 

49 

50 Enables automatic reload of templates. This is mostly useful 

51 in a development mode since it takes a significant performance 

52 hit. 

53 

54 ``default_expression`` 

55 

56 Set the default expression type. The default setting is 

57 ``python``. 

58 

59 ``encoding`` 

60 

61 The default text substitution value is a unicode string on 

62 Python 2 or simply string on Python 3. 

63 

64 Pass an encoding to allow encoded byte string input 

65 (e.g. UTF-8). 

66 

67 ``boolean_attributes`` 

68 

69 Attributes included in this set are treated as booleans: if a 

70 true value is provided, the attribute value is the attribute 

71 name, e.g.:: 

72 

73 boolean_attributes = {"selected"} 

74 

75 If we insert an attribute with the name "selected" and 

76 provide a true value, the attribute will be rendered:: 

77 

78 selected="selected" 

79 

80 If a false attribute is provided (including the empty string), 

81 the attribute is dropped. 

82 

83 The special return value ``default`` drops or inserts the 

84 attribute based on the value element attribute value. 

85 

86 ``translate`` 

87 

88 Use this option to set a translation function. 

89 

90 Example:: 

91 

92 def translate(msgid, domain=None, mapping=None, default=None, context=None): 

93 ... 

94 return translation 

95 

96 Note that if ``target_language`` is provided at render time, 

97 the translation function must support this argument. 

98 

99 ``implicit_i18n_translate`` 

100 

101 Enables implicit translation for text appearing inside 

102 elements. Default setting is ``False``. 

103 

104 While implicit translation does work for text that includes 

105 expression interpolation, each expression must be simply a 

106 variable name (e.g. ``${foo}``); otherwise, the text will not 

107 be marked for translation. 

108 

109 ``implicit_i18n_attributes`` 

110 

111 Any attribute contained in this set will be marked for 

112 implicit translation. Each entry must be a lowercase string. 

113 

114 Example:: 

115 

116 implicit_i18n_attributes = set(['alt', 'title']) 

117 

118 ``on_error_handler`` 

119 

120 This is an optional exception handler that is invoked during the 

121 "on-error" fallback block. 

122 

123 ``strict`` 

124 

125 Enabled by default. If disabled, expressions are only required 

126 to be valid at evaluation time. 

127 

128 This setting exists to provide compatibility with the 

129 reference implementation which compiles expressions at 

130 evaluation time. 

131 

132 ``trim_attribute_space`` 

133 

134 If set, additional attribute whitespace will be stripped. 

135 

136 ``restricted_namespace`` 

137 

138 True by default. If set False, ignored all namespace except chameleon default namespaces. It will be useful working with attributes based javascript template renderer like VueJS. 

139 

140 Example: 

141 

142 <div v-bind:id="dynamicId"></div> 

143 <button v-on:click="greet">Greet</button> 

144 <button @click="greet">Greet</button> 

145 

146 ``tokenizer`` 

147 

148 None by default. If provided, this tokenizer is used instead of the default 

149 (which is selected based on the template mode parameter.) 

150 

151 ``value_repr`` 

152 

153 This can be used to override the default value representation 

154 function which is used to format values when formatting an 

155 exception output. The function must not raise an exception (it 

156 should be safe to call with any value). 

157 

158 ``default_marker`` 

159 

160 This default marker is used as the marker object bound to the `default` 

161 name available to any expression. The semantics is such that if an 

162 expression evaluates to the marker object, the default action is used; 

163 for an attribute expression, this is the static attribute text; for an 

164 element this is the static element text. If there is no static text 

165 then the default action is similar to an expression result of `None`. 

166 

167 Output is unicode on Python 2 and string on Python 3. 

168 

169 """ 

170 

171 expression_types = { 

172 'python': PythonExpr, 

173 'string': StringExpr, 

174 'not': NotExpr, 

175 'exists': ExistsExpr, 

176 'import': ImportExpr, 

177 'structure': StructureExpr, 

178 } 

179 

180 default_expression = 'python' 

181 

182 translate = staticmethod(simple_translate) 

183 

184 encoding = None 

185 

186 boolean_attributes = set() 

187 

188 mode = "xml" 

189 

190 implicit_i18n_translate = False 

191 

192 implicit_i18n_attributes = set() 

193 

194 trim_attribute_space = False 

195 

196 enable_data_attributes = False 

197 

198 enable_comment_interpolation = True 

199 

200 on_error_handler = None 

201 

202 restricted_namespace = True 

203 

204 tokenizer = None 

205 

206 default_marker = Symbol(DEFAULT_MARKER) 

207 

208 def __init__(self, body, **config): 

209 self.macros = Macros(self) 

210 super(PageTemplate, self).__init__(body, **config) 

211 

212 def __getitem__(self, name): 

213 return self.macros[name] 

214 

215 @property 

216 def builtins(self): 

217 return self._builtins() 

218 

219 @property 

220 def engine(self): 

221 return partial( 

222 ExpressionEngine, 

223 self.expression_parser, 

224 default_marker=self.default_marker, 

225 ) 

226 

227 @property 

228 def expression_parser(self): 

229 return ExpressionParser(self.expression_types, self.default_expression) 

230 

231 def parse(self, body): 

232 return MacroProgram( 

233 body, self.mode, self.filename, 

234 escape=True if self.mode == "xml" else False, 

235 default_marker=self.default_marker, 

236 boolean_attributes=self.boolean_attributes, 

237 implicit_i18n_translate=self.implicit_i18n_translate, 

238 implicit_i18n_attributes=self.implicit_i18n_attributes, 

239 trim_attribute_space=self.trim_attribute_space, 

240 enable_data_attributes=self.enable_data_attributes, 

241 enable_comment_interpolation=self.enable_comment_interpolation, 

242 restricted_namespace=self.restricted_namespace, 

243 tokenizer=self.tokenizer 

244 ) 

245 

246 def render(self, encoding=None, **_kw): 

247 """Render template to string. 

248 

249 If providd, the ``encoding`` argument overrides the template 

250 default value. 

251 

252 Additional keyword arguments are passed as template variables. 

253 

254 In addition, some also have a special meaning: 

255 

256 ``translate`` 

257 

258 This keyword argument will override the default template 

259 translate function. 

260 

261 ``target_language`` 

262 

263 This will be used as the default argument to the translate 

264 function if no `i18n:target` value is provided. 

265 

266 If not provided, the `translate` function will need to 

267 negotiate a language based on the provided context. 

268 """ 

269 

270 translate = _kw.get('translate') 

271 if translate is not None: 

272 has_translate = True 

273 else: 

274 has_translate = False 

275 translate = self.translate 

276 

277 # This should not be necessary, but we include it for 

278 # backward compatibility. 

279 if translate is None: 

280 translate = type(self).translate 

281 

282 encoding = encoding if encoding is not None else self.encoding 

283 if encoding is not None: 

284 def translate(msgid, txl=translate, encoding=encoding, **kwargs): 

285 if isinstance(msgid, bytes): 

286 msgid = decode_string(msgid, encoding) 

287 return txl(msgid, **kwargs) 

288 

289 def decode(inst, encoding=encoding): 

290 return decode_string(inst, encoding, 'ignore') 

291 else: 

292 decode = decode_string 

293 

294 target_language = _kw.get('target_language') 

295 

296 setdefault = _kw.setdefault 

297 setdefault("__translate", translate) 

298 setdefault("__convert", 

299 partial(translate, target_language=target_language)) 

300 setdefault("__decode", decode) 

301 setdefault("target_language", None) 

302 setdefault("__on_error_handler", self.on_error_handler) 

303 

304 # Make sure we have a repeat dictionary 

305 if 'repeat' not in _kw: _kw['repeat'] = RepeatDict({}) 

306 

307 return super(PageTemplate, self).render(**_kw) 

308 

309 def include(self, *args, **kwargs): 

310 self.cook_check() 

311 self._render(*args, **kwargs) 

312 

313 def digest(self, body, names): 

314 hex = super(PageTemplate, self).digest(body, names) 

315 if isinstance(hex, unicode_string): 

316 hex = hex.encode('utf-8') 

317 digest = md5(hex) 

318 digest.update(';'.join(names).encode('utf-8')) 

319 

320 for attr in ( 

321 'trim_attribute_space', 

322 'implicit_i18n_translate', 

323 'strict' 

324 ): 

325 v = getattr(self, attr) 

326 digest.update( 

327 (";%s=%s" % (attr, str(v))).encode('ascii') 

328 ) 

329 

330 return digest.hexdigest() 

331 

332 def _builtins(self): 

333 return { 

334 'template': self, 

335 'macros': self.macros, 

336 'nothing': None, 

337 } 

338 

339 

340class PageTemplateFile(PageTemplate, BaseTemplateFile): 

341 """File-based constructor. 

342 

343 Takes a string input as the only positional argument:: 

344 

345 template = PageTemplateFile(absolute_path) 

346 

347 Note that the file-based template class comes with the expression 

348 type ``load`` which loads templates relative to the provided 

349 filename. 

350 

351 Below are listed the configuration arguments specific to 

352 file-based templates; see the string-based template class for 

353 general options and documentation: 

354 

355 Configuration (keyword arguments): 

356 

357 ``loader_class`` 

358 

359 The provided class will be used to create the template loader 

360 object. The default implementation supports relative and 

361 absolute path specs. 

362 

363 The class must accept keyword arguments ``search_path`` 

364 (sequence of paths to search for relative a path spec) and 

365 ``default_extension`` (if provided, this should be added to 

366 any path spec). 

367 

368 ``prepend_relative_search_path`` 

369 

370 Inserts the path relative to the provided template file path 

371 into the template search path. 

372 

373 The default setting is ``True``. 

374 

375 ``search_path`` 

376 

377 If provided, this is used as the search path for the ``load:`` 

378 expression. It must be a string or an iterable yielding a 

379 sequence of strings. 

380 

381 """ 

382 

383 expression_types = PageTemplate.expression_types.copy() 

384 expression_types['load'] = partial( 

385 ProxyExpr, '__loader', 

386 ignore_prefix=False 

387 ) 

388 

389 prepend_relative_search_path = True 

390 

391 def __init__(self, filename, search_path=None, loader_class=TemplateLoader, 

392 **config): 

393 super(PageTemplateFile, self).__init__(filename, **config) 

394 

395 if search_path is None: 

396 search_path = [] 

397 else: 

398 if isinstance(search_path, string_type): 

399 search_path = [search_path] 

400 else: 

401 search_path = list(search_path) 

402 

403 # If the flag is set (this is the default), prepend the path 

404 # relative to the template file to the search path 

405 if self.prepend_relative_search_path: 

406 path = dirname(self.filename) 

407 search_path.insert(0, path) 

408 

409 loader = loader_class(search_path=search_path, **config) 

410 template_class = type(self) 

411 

412 # Bind relative template loader instance to the same template 

413 # class, providing the same keyword arguments. 

414 self._loader = loader.bind(template_class) 

415 

416 def _builtins(self): 

417 d = super(PageTemplateFile, self)._builtins() 

418 d['__loader'] = self._loader 

419 return d 

420 

421 

422class PageTextTemplate(PageTemplate): 

423 """Text-based template class. 

424 

425 Takes a non-XML input:: 

426 

427 template = PageTextTemplate("Hello, ${name}.") 

428 

429 This is similar to the standard library class ``string.Template``, 

430 but uses the expression engine to substitute variables. 

431 """ 

432 

433 mode = "text" 

434 

435 

436class PageTextTemplateFile(PageTemplateFile): 

437 """File-based constructor.""" 

438 

439 mode = "text" 

440 

441 def render(self, **vars): 

442 result = super(PageTextTemplateFile, self).render(**vars) 

443 return result.encode(self.encoding or 'utf-8') 

444 

445 

446class Macro(object): 

447 __slots__ = "include", 

448 

449 def __init__(self, render): 

450 self.include = render 

451 

452 

453class Macros(object): 

454 __slots__ = "template", 

455 

456 def __init__(self, template): 

457 self.template = template 

458 

459 def __getitem__(self, name): 

460 name = name.replace('-', '_') 

461 self.template.cook_check() 

462 

463 try: 

464 function = getattr(self.template, "_render_%s" % name) 

465 except AttributeError: 

466 raise KeyError( 

467 "Macro does not exist: '%s'." % name) 

468 

469 return Macro(function) 

470 

471 @property 

472 def names(self): 

473 self.template.cook_check() 

474 

475 result = [] 

476 for name in self.template.__dict__: 

477 if name.startswith('_render_'): 

478 result.append(name[8:]) 

479 return result