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

1import re 

2 

3try: 

4 import ast 

5except ImportError: 

6 from chameleon import ast25 as ast 

7 

8try: 

9 str = unicode 

10except NameError: 

11 long = int 

12 

13from functools import partial 

14from copy import copy 

15 

16from ..program import ElementProgram 

17 

18from ..namespaces import XML_NS 

19from ..namespaces import XMLNS_NS 

20from ..namespaces import I18N_NS as I18N 

21from ..namespaces import TAL_NS as TAL 

22from ..namespaces import METAL_NS as METAL 

23from ..namespaces import META_NS as META 

24 

25from ..astutil import Static 

26from ..astutil import Symbol 

27from ..astutil import parse 

28from ..astutil import marker 

29 

30from .. import tal 

31from .. import tales 

32from .. import metal 

33from .. import i18n 

34from .. import nodes 

35 

36from ..exc import LanguageError 

37from ..exc import ParseError 

38from ..exc import CompilationError 

39 

40from ..utils import decode_htmlentities 

41from ..utils import ImportableMarker 

42 

43try: 

44 str = unicode 

45except NameError: 

46 long = int 

47 

48 

49missing = object() 

50 

51re_trim = re.compile(r'($\s+|\s+^)', re.MULTILINE) 

52 

53EMPTY_DICT = Static(ast.Dict(keys=[], values=[])) 

54CANCEL_MARKER = ImportableMarker(__name__, "CANCEL") 

55 

56 

57def skip(node): 

58 return node 

59 

60 

61def wrap(node, *wrappers): 

62 for wrapper in reversed(wrappers): 

63 node = wrapper(node) 

64 return node 

65 

66 

67def validate_attributes(attributes, namespace, whitelist): 

68 for ns, name in attributes: 

69 if ns == namespace and name not in whitelist: 

70 raise CompilationError( 

71 "Bad attribute for namespace '%s'" % ns, name 

72 ) 

73 

74 

75def convert_data_attributes(ns_attrs, attrs, namespaces): 

76 d = 0 

77 for i, attr in list(enumerate(attrs)): 

78 name = attr['name'] 

79 if name.startswith('data-'): 

80 name = name[5:] 

81 if '-' not in name: 

82 continue 

83 prefix, name = name.split('-', 1) 

84 ns_attrs[namespaces[prefix], name] = attr['value'] 

85 attrs.pop(i - d) 

86 d += 1 

87 

88 

89class MacroProgram(ElementProgram): 

90 """Visitor class that generates a program for the ZPT language.""" 

91 

92 DEFAULT_NAMESPACES = { 

93 'xmlns': XMLNS_NS, 

94 'xml': XML_NS, 

95 'tal': TAL, 

96 'metal': METAL, 

97 'i18n': I18N, 

98 'meta': META, 

99 } 

100 

101 DROP_NS = TAL, METAL, I18N, META 

102 

103 VARIABLE_BLACKLIST = "default", "repeat", "nothing", \ 

104 "convert", "decode", "translate" 

105 

106 _interpolation_enabled = True 

107 _whitespace = "\n" 

108 _last = "" 

109 _cancel_marker = Symbol(CANCEL_MARKER) 

110 

111 # Macro name (always trivial for a macro program) 

112 name = None 

113 

114 # This default marker value has the semantics that if an 

115 # expression evaluates to that value, the expression default value 

116 # is returned. For an attribute, if there is no default, this 

117 # means that the attribute is dropped. 

118 default_marker = None 

119 

120 # Escape mode (true value means XML-escape) 

121 escape = True 

122 

123 # Attributes which should have boolean behavior (on true, the 

124 # value takes the attribute name, on false, the attribute is 

125 # dropped) 

126 boolean_attributes = set() 

127 

128 # If provided, this should be a set of attributes for implicit 

129 # translation. Any attribute whose name is included in the set 

130 # will be translated even without explicit markup. Note that all 

131 # values should be lowercase strings. 

132 implicit_i18n_attributes = set() 

133 

134 # If set, text will be translated even without explicit markup. 

135 implicit_i18n_translate = False 

136 

137 # If set, additional attribute whitespace will be stripped. 

138 trim_attribute_space = False 

139 

140 # If set, data attributes can be used instead of namespace 

141 # attributes, e.g. "data-tal-content" instead of "tal:content". 

142 enable_data_attributes = False 

143 

144 # If set, XML namespaces are restricted to the list of those 

145 # defined and used by the page template language. 

146 restricted_namespace = True 

147 

148 # If set, expression interpolation is enabled in comments. 

149 enable_comment_interpolation = True 

150 

151 def __init__(self, *args, **kwargs): 

152 # Internal array for switch statements 

153 self._switches = [] 

154 

155 # Internal array for current use macro level 

156 self._use_macro = [] 

157 

158 # Internal array for current interpolation status 

159 self._interpolation = [True] 

160 

161 # Internal dictionary of macro definitions 

162 self._macros = {} 

163 

164 # Apply default values from **kwargs to self 

165 self._pop_defaults( 

166 kwargs, 

167 'boolean_attributes', 

168 'default_marker', 

169 'escape', 

170 'implicit_i18n_translate', 

171 'implicit_i18n_attributes', 

172 'trim_attribute_space', 

173 'enable_data_attributes', 

174 'enable_comment_interpolation', 

175 'restricted_namespace', 

176 ) 

177 

178 super(MacroProgram, self).__init__(*args, **kwargs) 

179 

180 @property 

181 def macros(self): 

182 macros = list(self._macros.items()) 

183 macros.append((None, nodes.Sequence(self.body))) 

184 

185 return tuple( 

186 nodes.Macro(name, [nodes.Context(node)]) 

187 for name, node in macros 

188 ) 

189 

190 def visit_default(self, node): 

191 return nodes.Text(node) 

192 

193 def visit_element(self, start, end, children): 

194 ns = start['ns_attrs'] 

195 attrs = start['attrs'] 

196 

197 if self.enable_data_attributes: 

198 attrs = list(attrs) 

199 convert_data_attributes(ns, attrs, start['ns_map']) 

200 

201 for (prefix, attr), encoded in tuple(ns.items()): 

202 if prefix == TAL or prefix == METAL: 

203 ns[prefix, attr] = decode_htmlentities(encoded) 

204 

205 # Validate namespace attributes 

206 validate_attributes(ns, TAL, tal.WHITELIST) 

207 validate_attributes(ns, METAL, metal.WHITELIST) 

208 validate_attributes(ns, I18N, i18n.WHITELIST) 

209 

210 # Check attributes for language errors 

211 self._check_attributes(start['namespace'], ns) 

212 

213 # Remember whitespace for item repetition 

214 if self._last is not None: 

215 self._whitespace = "\n" + " " * len(self._last.rsplit('\n', 1)[-1]) 

216 

217 # Set element-local whitespace 

218 whitespace = self._whitespace 

219 

220 # Set up switch 

221 try: 

222 clause = ns[TAL, 'switch'] 

223 except KeyError: 

224 switch = None 

225 else: 

226 value = nodes.Value(clause) 

227 switch = value 

228 

229 self._switches.append(switch) 

230 

231 body = [] 

232 

233 # Include macro 

234 use_macro = ns.get((METAL, 'use-macro')) 

235 extend_macro = ns.get((METAL, 'extend-macro')) 

236 if use_macro or extend_macro: 

237 omit = True 

238 slots = [] 

239 self._use_macro.append(slots) 

240 

241 if use_macro: 

242 inner = nodes.UseExternalMacro( 

243 nodes.Value(use_macro), slots, False 

244 ) 

245 macro_name = use_macro 

246 else: 

247 inner = nodes.UseExternalMacro( 

248 nodes.Value(extend_macro), slots, True 

249 ) 

250 macro_name = extend_macro 

251 

252 # While the macro executes, it should have access to the name it was 

253 # called with as 'macroname'. Splitting on / mirrors zope.tal and is a 

254 # concession to the path expression syntax. 

255 macro_name = macro_name.rsplit('/', 1)[-1] 

256 inner = nodes.Define( 

257 [nodes.Assignment(["macroname"], Static(ast.Str(macro_name)), True)], 

258 inner, 

259 ) 

260 STATIC_ATTRIBUTES = None 

261 # -or- include tag 

262 else: 

263 content = nodes.Sequence(body) 

264 

265 # tal:content 

266 try: 

267 clause = ns[TAL, 'content'] 

268 except KeyError: 

269 pass 

270 else: 

271 key, value = tal.parse_substitution(clause) 

272 translate = ns.get((I18N, 'translate')) == '' 

273 content = self._make_content_node( 

274 value, content, key, translate, 

275 ) 

276 

277 if end is None: 

278 # Make sure start-tag has opening suffix. 

279 start['suffix'] = ">" 

280 

281 # Explicitly set end-tag. 

282 end = { 

283 'prefix': '</', 

284 'name': start['name'], 

285 'space': '', 

286 'suffix': '>' 

287 } 

288 

289 # i18n:translate 

290 try: 

291 clause = ns[I18N, 'translate'] 

292 except KeyError: 

293 pass 

294 else: 

295 dynamic = ns.get((TAL, 'content')) or ns.get((TAL, 'replace')) 

296 

297 if not dynamic: 

298 content = nodes.Translate(clause, content) 

299 

300 # tal:attributes 

301 try: 

302 clause = ns[TAL, 'attributes'] 

303 except KeyError: 

304 TAL_ATTRIBUTES = [] 

305 else: 

306 TAL_ATTRIBUTES = tal.parse_attributes(clause) 

307 

308 # i18n:attributes 

309 try: 

310 clause = ns[I18N, 'attributes'] 

311 except KeyError: 

312 I18N_ATTRIBUTES = {} 

313 else: 

314 I18N_ATTRIBUTES = i18n.parse_attributes(clause) 

315 

316 # Prepare attributes from TAL language 

317 prepared = tal.prepare_attributes( 

318 attrs, TAL_ATTRIBUTES, 

319 I18N_ATTRIBUTES, ns, self.DROP_NS 

320 ) 

321 

322 # Create attribute nodes 

323 STATIC_ATTRIBUTES = self._create_static_attributes(prepared) 

324 ATTRIBUTES = self._create_attributes_nodes( 

325 prepared, I18N_ATTRIBUTES, STATIC_ATTRIBUTES, 

326 ) 

327 

328 # Start- and end nodes 

329 start_tag = nodes.Start( 

330 start['name'], 

331 self._maybe_trim(start['prefix']), 

332 self._maybe_trim(start['suffix']), 

333 ATTRIBUTES 

334 ) 

335 

336 end_tag = nodes.End( 

337 end['name'], 

338 end['space'], 

339 self._maybe_trim(end['prefix']), 

340 self._maybe_trim(end['suffix']), 

341 ) if end is not None else None 

342 

343 # tal:omit-tag 

344 try: 

345 clause = ns[TAL, 'omit-tag'] 

346 except KeyError: 

347 omit = False 

348 else: 

349 clause = clause.strip() 

350 

351 if clause == "": 

352 omit = True 

353 else: 

354 expression = nodes.Negate(nodes.Value(clause)) 

355 omit = expression 

356 

357 # Wrap start- and end-tags in condition 

358 start_tag = nodes.Condition(expression, start_tag) 

359 

360 if end_tag is not None: 

361 end_tag = nodes.Condition(expression, end_tag) 

362 

363 if omit is True or start['namespace'] in self.DROP_NS: 

364 inner = content 

365 else: 

366 inner = nodes.Element( 

367 start_tag, 

368 end_tag, 

369 content, 

370 ) 

371 

372 if omit is not False: 

373 inner = nodes.Cache([omit], inner) 

374 

375 # tal:replace 

376 try: 

377 clause = ns[TAL, 'replace'] 

378 except KeyError: 

379 pass 

380 else: 

381 key, value = tal.parse_substitution(clause) 

382 translate = ns.get((I18N, 'translate')) == '' 

383 inner = self._make_content_node( 

384 value, inner, key, translate 

385 ) 

386 

387 # metal:define-slot 

388 try: 

389 clause = ns[METAL, 'define-slot'] 

390 except KeyError: 

391 DEFINE_SLOT = skip 

392 else: 

393 DEFINE_SLOT = partial(nodes.DefineSlot, clause) 

394 

395 # tal:define 

396 try: 

397 clause = ns[TAL, 'define'] 

398 except KeyError: 

399 defines = [] 

400 else: 

401 defines = tal.parse_defines(clause) 

402 

403 if defines is None: 

404 raise ParseError("Invalid define syntax.", clause) 

405 

406 # i18n:target 

407 try: 

408 target_language = ns[I18N, 'target'] 

409 except KeyError: 

410 pass 

411 else: 

412 # The name "default" is an alias for the target language 

413 # variable. We simply replace it. 

414 target_language = target_language.replace( 

415 'default', 'target_language' 

416 ) 

417 

418 defines.append( 

419 ('local', ("target_language", ), target_language) 

420 ) 

421 

422 assignments = [ 

423 nodes.Assignment( 

424 names, nodes.Value(expr), context == "local") 

425 for (context, names, expr) in defines 

426 ] 

427 

428 # Assign static attributes dictionary to "attrs" value 

429 assignments.insert(0, nodes.Alias(["attrs"], STATIC_ATTRIBUTES or EMPTY_DICT)) 

430 DEFINE = partial(nodes.Define, assignments) 

431 

432 # tal:case 

433 try: 

434 clause = ns[TAL, 'case'] 

435 except KeyError: 

436 CASE = skip 

437 else: 

438 value = nodes.Value(clause) 

439 for switch in reversed(self._switches): 

440 if switch is not None: 

441 break 

442 else: 

443 raise LanguageError( 

444 "Must define switch on a parent element.", clause 

445 ) 

446 

447 CASE = lambda node: nodes.Define( 

448 [nodes.Alias(["default"], self.default_marker)], 

449 nodes.Condition( 

450 nodes.And([ 

451 nodes.BinOp(switch, nodes.IsNot, self._cancel_marker), 

452 nodes.Or([ 

453 nodes.BinOp(value, nodes.Equals, switch), 

454 nodes.BinOp(value, nodes.Equals, self.default_marker) 

455 ]) 

456 ]), 

457 nodes.Cancel([switch], node, self._cancel_marker), 

458 )) 

459 

460 # tal:repeat 

461 try: 

462 clause = ns[TAL, 'repeat'] 

463 except KeyError: 

464 REPEAT = skip 

465 else: 

466 defines = tal.parse_defines(clause) 

467 assert len(defines) == 1 

468 context, names, expr = defines[0] 

469 

470 expression = nodes.Value(expr) 

471 

472 if start['namespace'] == TAL: 

473 self._last = None 

474 self._whitespace = whitespace.lstrip('\n') 

475 whitespace = "" 

476 

477 REPEAT = partial( 

478 nodes.Repeat, 

479 names, 

480 expression, 

481 context == "local", 

482 whitespace 

483 ) 

484 

485 # tal:condition 

486 try: 

487 clause = ns[TAL, 'condition'] 

488 except KeyError: 

489 CONDITION = skip 

490 else: 

491 expression = nodes.Value(clause) 

492 CONDITION = partial(nodes.Condition, expression) 

493 

494 # tal:switch 

495 if switch is None: 

496 SWITCH = skip 

497 else: 

498 SWITCH = partial(nodes.Cache, [switch]) 

499 

500 # i18n:domain 

501 try: 

502 clause = ns[I18N, 'domain'] 

503 except KeyError: 

504 DOMAIN = skip 

505 else: 

506 DOMAIN = partial(nodes.Domain, clause) 

507 

508 # i18n:context 

509 try: 

510 clause = ns[I18N, 'context'] 

511 except KeyError: 

512 CONTEXT = skip 

513 else: 

514 CONTEXT = partial(nodes.TxContext, clause) 

515 

516 # i18n:name 

517 try: 

518 clause = ns[I18N, 'name'] 

519 except KeyError: 

520 NAME = skip 

521 else: 

522 if not clause.strip(): 

523 NAME = skip 

524 else: 

525 NAME = partial(nodes.Name, clause) 

526 

527 # The "slot" node next is the first node level that can serve 

528 # as a macro slot 

529 slot = wrap( 

530 inner, 

531 DEFINE_SLOT, 

532 DEFINE, 

533 CASE, 

534 CONDITION, 

535 REPEAT, 

536 SWITCH, 

537 DOMAIN, 

538 CONTEXT, 

539 ) 

540 

541 # metal:fill-slot 

542 try: 

543 clause = ns[METAL, 'fill-slot'] 

544 except KeyError: 

545 pass 

546 else: 

547 if not clause.strip(): 

548 raise LanguageError( 

549 "Must provide a non-trivial string for metal:fill-slot.", 

550 clause 

551 ) 

552 

553 index = -(1 + int(bool(use_macro or extend_macro))) 

554 

555 try: 

556 slots = self._use_macro[index] 

557 except IndexError: 

558 raise LanguageError( 

559 "Cannot use metal:fill-slot without metal:use-macro.", 

560 clause 

561 ) 

562 

563 slots = self._use_macro[index] 

564 slots.append(nodes.FillSlot(clause, slot)) 

565 

566 # metal:define-macro 

567 try: 

568 clause = ns[METAL, 'define-macro'] 

569 except KeyError: 

570 pass 

571 else: 

572 if ns.get((METAL, 'fill-slot')) is not None: 

573 raise LanguageError( 

574 "Can't have 'fill-slot' and 'define-macro' " 

575 "on the same element.", 

576 clause 

577 ) 

578 

579 self._macros[clause] = slot 

580 slot = nodes.UseInternalMacro(clause) 

581 

582 slot = wrap( 

583 slot, 

584 NAME 

585 ) 

586 

587 # tal:on-error 

588 try: 

589 clause = ns[TAL, 'on-error'] 

590 except KeyError: 

591 ON_ERROR = skip 

592 else: 

593 key, value = tal.parse_substitution(clause) 

594 translate = ns.get((I18N, 'translate')) == '' 

595 fallback = self._make_content_node( 

596 value, None, key, translate, 

597 ) 

598 

599 if omit is False and start['namespace'] not in self.DROP_NS: 

600 start_tag = copy(start_tag) 

601 

602 start_tag.attributes = nodes.Sequence( 

603 start_tag.attributes.extract( 

604 lambda attribute: 

605 isinstance(attribute, nodes.Attribute) and 

606 isinstance(attribute.expression, ast.Str) 

607 ) 

608 ) 

609 

610 if end_tag is None: 

611 # Make sure start-tag has opening suffix. We don't 

612 # allow self-closing element here. 

613 start_tag.suffix = ">" 

614 

615 # Explicitly set end-tag. 

616 end_tag = nodes.End(start_tag.name, '', '</', '>',) 

617 

618 fallback = nodes.Element( 

619 start_tag, 

620 end_tag, 

621 fallback, 

622 ) 

623 

624 ON_ERROR = partial(nodes.OnError, fallback, 'error') 

625 

626 clause = ns.get((META, 'interpolation')) 

627 if clause in ('false', 'off'): 

628 INTERPOLATION = False 

629 elif clause in ('true', 'on'): 

630 INTERPOLATION = True 

631 elif clause is None: 

632 INTERPOLATION = self._interpolation[-1] 

633 else: 

634 raise LanguageError("Bad interpolation setting.", clause) 

635 

636 self._interpolation.append(INTERPOLATION) 

637 

638 # Visit content body 

639 for child in children: 

640 body.append(self.visit(*child)) 

641 

642 self._switches.pop() 

643 self._interpolation.pop() 

644 

645 if use_macro: 

646 self._use_macro.pop() 

647 

648 return wrap( 

649 slot, 

650 ON_ERROR 

651 ) 

652 

653 def visit_start_tag(self, start): 

654 return self.visit_element(start, None, []) 

655 

656 def visit_cdata(self, node): 

657 if not self._interpolation[-1] or not '${' in node: 

658 return nodes.Text(node) 

659 

660 expr = nodes.Substitution(node, ()) 

661 return nodes.Interpolation(expr, True, False) 

662 

663 def visit_comment(self, node): 

664 if node.startswith('<!--!'): 

665 return 

666 

667 if not self.enable_comment_interpolation: 

668 return nodes.Text(node) 

669 

670 if node.startswith('<!--?'): 

671 return nodes.Text('<!--' + node.lstrip('<!-?')) 

672 

673 if not self._interpolation[-1] or not '${' in node: 

674 return nodes.Text(node) 

675 

676 char_escape = ('&', '<', '>') if self.escape else () 

677 expression = nodes.Substitution(node[4:-3], char_escape) 

678 

679 return nodes.Sequence( 

680 [nodes.Text(node[:4]), 

681 nodes.Interpolation(expression, True, False), 

682 nodes.Text(node[-3:]) 

683 ]) 

684 

685 def visit_processing_instruction(self, node): 

686 if node['name'] != 'python': 

687 text = '<?' + node['name'] + node['text'] + '?>' 

688 return self.visit_text(text) 

689 

690 return nodes.CodeBlock(node['text']) 

691 

692 def visit_text(self, node): 

693 self._last = node 

694 

695 translation = self.implicit_i18n_translate 

696 

697 if self._interpolation[-1] and '${' in node: 

698 char_escape = ('&', '<', '>') if self.escape else () 

699 expression = nodes.Substitution(node, char_escape) 

700 return nodes.Interpolation(expression, True, translation) 

701 

702 node = node.replace('$$', '$') 

703 

704 if not translation: 

705 return nodes.Text(node) 

706 

707 match = re.search(r'(\s*)(.*\S)(\s*)', node, flags=re.DOTALL) 

708 if match is not None: 

709 prefix, text, suffix = match.groups() 

710 normalized = re.sub(r'\s+', ' ', text) 

711 return nodes.Sequence([ 

712 nodes.Text(prefix), 

713 nodes.Translate(normalized, nodes.Text(normalized), None), 

714 nodes.Text(suffix), 

715 ]) 

716 else: 

717 return nodes.Text(node) 

718 

719 def _pop_defaults(self, kwargs, *attributes): 

720 for attribute in attributes: 

721 value = kwargs.pop(attribute, None) 

722 if value is not None: 

723 setattr(self, attribute, value) 

724 

725 def _check_attributes(self, namespace, ns): 

726 if namespace in self.DROP_NS and ns.get((TAL, 'attributes')): 

727 raise LanguageError( 

728 "Dynamic attributes not allowed on elements of " 

729 "the namespace: %s." % namespace, 

730 ns[TAL, 'attributes'], 

731 ) 

732 

733 script = ns.get((TAL, 'script')) 

734 if script is not None: 

735 raise LanguageError( 

736 "The script attribute is unsupported.", script) 

737 

738 tal_content = ns.get((TAL, 'content')) 

739 if tal_content and ns.get((TAL, 'replace')): 

740 raise LanguageError( 

741 "You cannot use tal:content and tal:replace at the same time.", 

742 tal_content 

743 ) 

744 

745 if tal_content and ns.get((I18N, 'translate')): 

746 raise LanguageError( 

747 "You cannot use tal:content with non-trivial i18n:translate.", 

748 tal_content 

749 ) 

750 

751 def _make_content_node(self, expression, default, key, translate): 

752 value = nodes.Value(expression) 

753 char_escape = ('&', '<', '>') if key == 'text' else () 

754 content = nodes.Content(value, char_escape, translate) 

755 

756 if default is not None: 

757 content = nodes.Condition( 

758 nodes.BinOp(value, nodes.Is, self.default_marker), 

759 default, 

760 content, 

761 ) 

762 

763 # Cache expression to avoid duplicate evaluation 

764 content = nodes.Cache([value], content) 

765 

766 # Define local marker "default" 

767 content = nodes.Define( 

768 [nodes.Alias(["default"], self.default_marker)], 

769 content 

770 ) 

771 

772 return content 

773 

774 def _create_attributes_nodes(self, prepared, I18N_ATTRIBUTES, STATIC): 

775 attributes = [] 

776 

777 names = [attr[0] for attr in prepared] 

778 filtering = [[]] 

779 

780 for i, (name, text, quote, space, eq, expr) in enumerate(prepared): 

781 implicit_i18n = ( 

782 name is not None and 

783 name.lower() in self.implicit_i18n_attributes 

784 ) 

785 

786 char_escape = ('&', '<', '>', quote) 

787 msgid = I18N_ATTRIBUTES.get(name, missing) 

788 

789 # If (by heuristic) ``text`` contains one or more 

790 # interpolation expressions, apply interpolation 

791 # substitution to the text. 

792 if expr is None and text is not None and '${' in text: 

793 default = None 

794 expr = nodes.Substitution(text, char_escape, default, self.default_marker) 

795 translation = implicit_i18n and msgid is missing 

796 value = nodes.Interpolation(expr, True, translation) 

797 else: 

798 default = ast.Str(s=text) if text is not None else None 

799 

800 # If the expression is non-trivial, the attribute is 

801 # dynamic (computed). 

802 if expr is not None: 

803 if name is None: 

804 expression = nodes.Value( 

805 decode_htmlentities(expr), 

806 default, 

807 self.default_marker 

808 ) 

809 value = nodes.DictAttributes( 

810 expression, ('&', '<', '>', '"'), '"', 

811 set(filter(None, names[i:])) 

812 ) 

813 for fs in filtering: 

814 fs.append(expression) 

815 filtering.append([]) 

816 elif name in self.boolean_attributes: 

817 value = nodes.Boolean(expr, name, default, self.default_marker) 

818 else: 

819 value = nodes.Substitution( 

820 decode_htmlentities(expr), 

821 char_escape, 

822 default, 

823 self.default_marker, 

824 ) 

825 

826 # Otherwise, it's a static attribute. We don't include it 

827 # here if there's one or more "computed" attributes 

828 # (dynamic, from one or more dict values). 

829 else: 

830 value = ast.Str(s=text) 

831 if msgid is missing and implicit_i18n: 

832 msgid = text 

833 

834 if name is not None: 

835 # If translation is required, wrap in a translation 

836 # clause 

837 if msgid is not missing: 

838 value = nodes.Translate(msgid, value) 

839 

840 space = self._maybe_trim(space) 

841 

842 attribute = nodes.Attribute( 

843 name, 

844 value, 

845 quote, 

846 eq, 

847 space, 

848 default, 

849 filtering[-1], 

850 ) 

851 

852 if not isinstance(value, ast.Str): 

853 # Always define a ``default`` alias for non-static 

854 # expressions. 

855 attribute = nodes.Define( 

856 [nodes.Alias(["default"], self.default_marker)], 

857 attribute, 

858 ) 

859 

860 value = attribute 

861 

862 attributes.append(value) 

863 

864 result = nodes.Sequence(attributes) 

865 

866 # We're caching all expressions found during attribute processing to 

867 # enable the filtering functionality which exists to allow later 

868 # definitions to override previous ones. 

869 expressions = filtering[0] 

870 if expressions: 

871 return nodes.Cache(expressions, result) 

872 

873 return result 

874 

875 def _create_static_attributes(self, prepared): 

876 static_attrs = {} 

877 

878 for name, text, quote, space, eq, expr in prepared: 

879 if name is None: 

880 continue 

881 

882 static_attrs[name] = text if text is not None else expr 

883 

884 if not static_attrs: 

885 return 

886 

887 return Static(parse(repr(static_attrs)).body) 

888 

889 def _maybe_trim(self, string): 

890 if self.trim_attribute_space: 

891 return re_trim.sub(" ", string) 

892 

893 return string