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

1############################################################################## 

2# 

3# Copyright (c) 2001, 2002 Zope Foundation and Contributors. 

4# All Rights Reserved. 

5# 

6# This software is subject to the provisions of the Zope Public License, 

7# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 

8# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 

9# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 

10# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 

11# FOR A PARTICULAR PURPOSE. 

12# 

13############################################################################## 

14 

15import re 

16import copy 

17 

18from .exc import LanguageError 

19from .utils import descriptorint 

20from .utils import descriptorstr 

21from .namespaces import XMLNS_NS 

22from .parser import groups 

23 

24 

25try: 

26 next 

27except NameError: 

28 from chameleon.py25 import next 

29 

30try: 

31 # optional library: `zope.interface` 

32 from chameleon import interfaces 

33 import zope.interface 

34except ImportError: 

35 interfaces = None 

36 

37 

38NAME = r"[a-zA-Z_][-a-zA-Z0-9_]*" 

39DEFINE_RE = re.compile(r"(?s)\s*(?:(global|local)\s+)?" + 

40 r"(%s|\(%s(?:,\s*%s)*\))\s+(.*)\Z" % (NAME, NAME, NAME), 

41 re.UNICODE) 

42SUBST_RE = re.compile(r"\s*(?:(text|structure)\s+)?(.*)\Z", re.S | re.UNICODE) 

43ATTR_RE = re.compile(r"\s*([^\s{}'\"]+)\s+([^\s].*)\Z", re.S | re.UNICODE) 

44 

45ENTITY_RE = re.compile(r'(&(#?)(x?)(\d{1,5}|\w{1,8});)') 

46 

47WHITELIST = frozenset([ 

48 "define", 

49 "comment", 

50 "condition", 

51 "content", 

52 "replace", 

53 "repeat", 

54 "attributes", 

55 "on-error", 

56 "omit-tag", 

57 "script", 

58 "switch", 

59 "case", 

60 "xmlns", 

61 "xml" 

62 ]) 

63 

64 

65def split_parts(arg): 

66 # Break in pieces at undoubled semicolons and 

67 # change double semicolons to singles: 

68 i = 0 

69 while i < len(arg): 

70 m = ENTITY_RE.search(arg[i:]) 

71 if m is None: 

72 break 

73 arg = arg[:i + m.end()] + ';' + arg[i + m.end():] 

74 i += m.end() 

75 

76 arg = arg.replace(";;", "\0") 

77 parts = arg.split(';') 

78 parts = [p.replace("\0", ";") for p in parts] 

79 if len(parts) > 1 and not parts[-1].strip(): 

80 del parts[-1] # It ended in a semicolon 

81 

82 return parts 

83 

84 

85def parse_attributes(clause): 

86 attrs = [] 

87 seen = set() 

88 for part in split_parts(clause): 

89 m = ATTR_RE.match(part) 

90 if not m: 

91 name, expr = None, part.strip() 

92 else: 

93 name, expr = groups(m, part) 

94 

95 if name in seen: 

96 raise LanguageError( 

97 "Duplicate attribute name in attributes.", part) 

98 

99 seen.add(name) 

100 attrs.append((name, expr)) 

101 

102 return attrs 

103 

104 

105def parse_substitution(clause): 

106 m = SUBST_RE.match(clause) 

107 if m is None: 

108 raise LanguageError( 

109 "Invalid content substitution syntax.", clause) 

110 

111 key, expression = groups(m, clause) 

112 if not key: 

113 key = "text" 

114 

115 return key, expression 

116 

117 

118def parse_defines(clause): 

119 """ 

120 Parses a tal:define value. 

121 

122 # Basic syntax, implicit local 

123 >>> parse_defines('hello lovely') 

124 [('local', ('hello',), 'lovely')] 

125 

126 # Explicit local 

127 >>> parse_defines('local hello lovely') 

128 [('local', ('hello',), 'lovely')] 

129 

130 # With global 

131 >>> parse_defines('global hello lovely') 

132 [('global', ('hello',), 'lovely')] 

133 

134 # Multiple expressions 

135 >>> parse_defines('hello lovely; tea time') 

136 [('local', ('hello',), 'lovely'), ('local', ('tea',), 'time')] 

137 

138 # With multiple names 

139 >>> parse_defines('(hello, howdy) lovely') 

140 [('local', ['hello', 'howdy'], 'lovely')] 

141 

142 # With unicode whitespace 

143 >>> try: 

144 ... s = '\xc2\xa0hello lovely'.decode('utf-8') 

145 ... except AttributeError: 

146 ... s = '\xa0hello lovely' 

147 >>> from chameleon.utils import unicode_string 

148 >>> parse_defines(s) == [ 

149 ... ('local', ('hello',), 'lovely') 

150 ... ] 

151 True 

152 

153 """ 

154 defines = [] 

155 for part in split_parts(clause): 

156 m = DEFINE_RE.match(part) 

157 if m is None: 

158 raise LanguageError("Invalid define syntax", part) 

159 context, name, expr = groups(m, part) 

160 context = context or "local" 

161 

162 if name.startswith('('): 

163 names = [n.strip() for n in name.strip('()').split(',')] 

164 else: 

165 names = (name,) 

166 

167 defines.append((context, names, expr)) 

168 

169 return defines 

170 

171 

172def prepare_attributes(attrs, dyn_attributes, i18n_attributes, 

173 ns_attributes, drop_ns): 

174 drop = set([attribute['name'] for attribute, (ns, value) 

175 in zip(attrs, ns_attributes) 

176 if ns in drop_ns or ( 

177 ns == XMLNS_NS and 

178 attribute['value'] in drop_ns 

179 ) 

180 ]) 

181 

182 attributes = [] 

183 normalized = {} 

184 computed = [] 

185 

186 for attribute in attrs: 

187 name = attribute['name'] 

188 

189 if name in drop: 

190 continue 

191 

192 attributes.append(( 

193 name, 

194 attribute['value'], 

195 attribute['quote'], 

196 attribute['space'], 

197 attribute['eq'], 

198 None, 

199 )) 

200 

201 normalized[name.lower()] = len(attributes) - 1 

202 

203 for name, expr in dyn_attributes: 

204 index = normalized.get(name.lower()) if name else None 

205 

206 if index is not None: 

207 _, text, quote, space, eq, _ = attributes[index] 

208 add = attributes.__setitem__ 

209 else: 

210 text = None 

211 quote = '"' 

212 space = " " 

213 eq = "=" 

214 index = len(attributes) 

215 add = attributes.insert 

216 if name is not None: 

217 normalized[name.lower()] = len(attributes) - 1 

218 

219 attribute = name, text, quote, space, eq, expr 

220 add(index, attribute) 

221 

222 for name in i18n_attributes: 

223 attr = name.lower() 

224 if attr not in normalized: 

225 attributes.append((name, name, '"', " ", "=", None)) 

226 normalized[attr] = len(attributes) - 1 

227 

228 return attributes 

229 

230 

231class RepeatItem(object): 

232 __slots__ = "length", "_iterator" 

233 

234 __allow_access_to_unprotected_subobjects__ = True 

235 

236 def __init__(self, iterator, length): 

237 self.length = length 

238 self._iterator = iterator 

239 

240 def __iter__(self): 

241 return self._iterator 

242 

243 try: 

244 iter(()).__len__ 

245 except AttributeError: 

246 @descriptorint 

247 def index(self): 

248 try: 

249 remaining = self._iterator.__length_hint__() 

250 except AttributeError: 

251 remaining = len(tuple(copy.copy(self._iterator))) 

252 return self.length - remaining - 1 

253 else: 

254 @descriptorint 

255 def index(self): 

256 remaining = self._iterator.__len__() 

257 return self.length - remaining - 1 

258 

259 @descriptorint 

260 def start(self): 

261 return self.index == 0 

262 

263 @descriptorint 

264 def end(self): 

265 return self.index == self.length - 1 

266 

267 @descriptorint 

268 def number(self): 

269 return self.index + 1 

270 

271 @descriptorstr 

272 def odd(self): 

273 """Returns a true value if the item index is odd. 

274 

275 >>> it = RepeatItem(iter(("apple", "pear")), 2) 

276 

277 >>> next(it._iterator) 

278 'apple' 

279 >>> it.odd() 

280 '' 

281 

282 >>> next(it._iterator) 

283 'pear' 

284 >>> it.odd() 

285 'odd' 

286 """ 

287 

288 return self.index % 2 == 1 and 'odd' or '' 

289 

290 @descriptorstr 

291 def even(self): 

292 """Returns a true value if the item index is even. 

293 

294 >>> it = RepeatItem(iter(("apple", "pear")), 2) 

295 

296 >>> next(it._iterator) 

297 'apple' 

298 >>> it.even() 

299 'even' 

300 

301 >>> next(it._iterator) 

302 'pear' 

303 >>> it.even() 

304 '' 

305 """ 

306 

307 return self.index % 2 == 0 and 'even' or '' 

308 

309 @descriptorstr 

310 def parity(self): 

311 """Return 'odd' or 'even' depending on the position's parity 

312 

313 Useful for assigning CSS class names to table rows. 

314 """ 

315 

316 return self.index % 2 == 0 and 'even' or 'odd' 

317 

318 def next(self): 

319 raise NotImplementedError( 

320 "Method not implemented (can't update local variable).") 

321 

322 def _letter(self, base=ord('a'), radix=26): 

323 """Get the iterator position as a lower-case letter 

324 

325 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) 

326 >>> next(it._iterator) 

327 'apple' 

328 >>> it.letter() 

329 'a' 

330 >>> next(it._iterator) 

331 'pear' 

332 >>> it.letter() 

333 'b' 

334 >>> next(it._iterator) 

335 'orange' 

336 >>> it.letter() 

337 'c' 

338 """ 

339 

340 index = self.index 

341 if index < 0: 

342 raise TypeError("No iteration position") 

343 s = "" 

344 while 1: 

345 index, off = divmod(index, radix) 

346 s = chr(base + off) + s 

347 if not index: 

348 return s 

349 

350 letter = descriptorstr(_letter) 

351 

352 @descriptorstr 

353 def Letter(self): 

354 """Get the iterator position as an upper-case letter 

355 

356 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) 

357 >>> next(it._iterator) 

358 'apple' 

359 >>> it.Letter() 

360 'A' 

361 >>> next(it._iterator) 

362 'pear' 

363 >>> it.Letter() 

364 'B' 

365 >>> next(it._iterator) 

366 'orange' 

367 >>> it.Letter() 

368 'C' 

369 """ 

370 

371 return self._letter(base=ord('A')) 

372 

373 @descriptorstr 

374 def Roman(self, rnvalues=( 

375 (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'), 

376 (100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'), 

377 (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I'))): 

378 """Get the iterator position as an upper-case roman numeral 

379 

380 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) 

381 >>> next(it._iterator) 

382 'apple' 

383 >>> it.Roman() 

384 'I' 

385 >>> next(it._iterator) 

386 'pear' 

387 >>> it.Roman() 

388 'II' 

389 >>> next(it._iterator) 

390 'orange' 

391 >>> it.Roman() 

392 'III' 

393 """ 

394 

395 n = self.index + 1 

396 s = "" 

397 for v, r in rnvalues: 

398 rct, n = divmod(n, v) 

399 s = s + r * rct 

400 return s 

401 

402 @descriptorstr 

403 def roman(self): 

404 """Get the iterator position as a lower-case roman numeral 

405 

406 >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) 

407 >>> next(it._iterator) 

408 'apple' 

409 >>> it.roman() 

410 'i' 

411 >>> next(it._iterator) 

412 'pear' 

413 >>> it.roman() 

414 'ii' 

415 >>> next(it._iterator) 

416 'orange' 

417 >>> it.roman() 

418 'iii' 

419 """ 

420 

421 return self.Roman().lower() 

422 

423 

424if interfaces is not None: 

425 zope.interface.classImplements(RepeatItem, interfaces.ITALESIterator) 

426 

427 

428class RepeatDict(object): 

429 """Repeat dictionary implementation. 

430 

431 >>> repeat = RepeatDict({}) 

432 >>> iterator, length = repeat('numbers', range(5)) 

433 >>> length 

434 5 

435 

436 >>> repeat['numbers'] 

437 <chameleon.tal.RepeatItem object at ...> 

438 

439 >>> repeat.numbers 

440 <chameleon.tal.RepeatItem object at ...> 

441 

442 >>> getattr(repeat, 'missing_key', None) is None 

443 True 

444 

445 >>> try: 

446 ... from chameleon import interfaces 

447 ... interfaces.ITALESIterator(repeat,None) is None 

448 ... except ImportError: 

449 ... True 

450 ... 

451 True 

452 """ 

453 

454 __slots__ = "__setitem__", "__getitem__" 

455 

456 def __init__(self, d): 

457 self.__setitem__ = d.__setitem__ 

458 self.__getitem__ = d.__getitem__ 

459 

460 def __getattr__(self,key): 

461 try: 

462 return self[key] 

463 except KeyError: 

464 raise AttributeError(key) 

465 

466 

467 def __call__(self, key, iterable): 

468 """We coerce the iterable to a tuple and return an iterator 

469 after registering it in the repeat dictionary.""" 

470 

471 iterable = list(iterable) if iterable is not None else () 

472 

473 length = len(iterable) 

474 iterator = iter(iterable) 

475 

476 # Insert as repeat item 

477 self[key] = RepeatItem(iterator, length) 

478 

479 return iterator, length 

480 

481 

482class ErrorInfo(object): 

483 """Information about an exception passed to an on-error handler.""" 

484 

485 def __init__(self, err, position=(None, None)): 

486 if isinstance(err, Exception): 

487 self.type = err.__class__ 

488 self.value = err 

489 else: 

490 self.type = err 

491 self.value = None 

492 self.lineno = position[0] 

493 self.offset = position[1] 

494 

495 

496if interfaces is not None: 

497 zope.interface.classImplements(ErrorInfo, interfaces.ITALExpressionErrorInfo)