1
2
3
4 """
5 This file is part of the web2py Web Framework (Copyrighted, 2007-2011).
6 License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
7
8 Author: Thadeus Burgess
9
10 Contributors:
11
12 - Thank you to Massimo Di Pierro for creating the original gluon/template.py
13 - Thank you to Jonathan Lundell for extensively testing the regex on Jython.
14 - Thank you to Limodou (creater of uliweb) who inspired the block-element support for web2py.
15 """
16
17 import os
18 import cgi
19 import logging
20 from re import compile, sub, escape, DOTALL
21 try:
22 import cStringIO as StringIO
23 except:
24 from io import StringIO
25
26 try:
27
28 from restricted import RestrictedError
29 from globals import current
30 except ImportError:
31
32 current = None
33
35 logging.error(str(a) + ':' + str(b) + ':' + str(c))
36 return RuntimeError
37
38
40 """
41 Basic Container Object
42 """
43 - def __init__(self, value=None, pre_extend=False):
44 self.value = value
45 self.pre_extend = pre_extend
46
48 return str(self.value)
49
50
52 - def __init__(self, name='', pre_extend=False):
53 self.name = name
54 self.value = None
55 self.pre_extend = pre_extend
56
58 if self.value:
59 return str(self.value)
60 else:
61
62 return ''
63
65 return "%s->%s" % (self.name, self.value)
66
67
69
70
71
72
73
74 return (blocks[node.name].output(blocks)
75 if node.name in blocks else
76 node.output(blocks)) \
77 if isinstance(node, BlockNode) \
78 else str(node)
79
80
82 """
83 Block Container.
84
85 This Node can contain other Nodes and will render in a hierarchical order
86 of when nodes were added.
87
88 ie::
89
90 {{ block test }}
91 This is default block test
92 {{ end }}
93 """
94 - def __init__(self, name='', pre_extend=False, delimiters=('{{', '}}')):
95 """
96 name - Name of this Node.
97 """
98 self.nodes = []
99 self.name = name
100 self.pre_extend = pre_extend
101 self.left, self.right = delimiters
102
104 lines = ['%sblock %s%s' % (self.left, self.name, self.right)]
105 lines += [str(node) for node in self.nodes]
106 lines.append('%send%s' % (self.left, self.right))
107 return ''.join(lines)
108
110 """
111 Get this BlockNodes content, not including child Nodes
112 """
113 return ''.join(str(node) for node in self.nodes
114 if not isinstance(node, BlockNode))
115
117 """
118 Add an element to the nodes.
119
120 Keyword Arguments
121
122 - node -- Node object or string to append.
123 """
124 if isinstance(node, str) or isinstance(node, Node):
125 self.nodes.append(node)
126 else:
127 raise TypeError("Invalid type; must be instance of ``str`` or ``BlockNode``. %s" % node)
128
130 """
131 Extend the list of nodes with another BlockNode class.
132
133 Keyword Arguments
134
135 - other -- BlockNode or Content object to extend from.
136 """
137 if isinstance(other, BlockNode):
138 self.nodes.extend(other.nodes)
139 else:
140 raise TypeError(
141 "Invalid type; must be instance of ``BlockNode``. %s" % other)
142
144 """
145 Merges all nodes into a single string.
146 blocks -- Dictionary of blocks that are extending
147 from this template.
148 """
149 return ''.join(output_aux(node, blocks) for node in self.nodes)
150
151
152 -class Content(BlockNode):
153 """
154 Parent Container -- Used as the root level BlockNode.
155
156 Contains functions that operate as such.
157 """
158 - def __init__(self, name="ContentBlock", pre_extend=False):
159 """
160 Keyword Arguments
161
162 name -- Unique name for this BlockNode
163 """
164 self.name = name
165 self.nodes = []
166 self.blocks = {}
167 self.pre_extend = pre_extend
168
170 return ''.join(output_aux(node, self.blocks) for node in self.nodes)
171
172 - def _insert(self, other, index=0):
173 """
174 Inserts object at index.
175 """
176 if isinstance(other, (str, Node)):
177 self.nodes.insert(index, other)
178 else:
179 raise TypeError(
180 "Invalid type, must be instance of ``str`` or ``Node``.")
181
182 - def insert(self, other, index=0):
183 """
184 Inserts object at index.
185
186 You may pass a list of objects and have them inserted.
187 """
188 if isinstance(other, (list, tuple)):
189
190 other.reverse()
191 for item in other:
192 self._insert(item, index)
193 else:
194 self._insert(other, index)
195
196 - def append(self, node):
197 """
198 Adds a node to list. If it is a BlockNode then we assign a block for it.
199 """
200 if isinstance(node, (str, Node)):
201 self.nodes.append(node)
202 if isinstance(node, BlockNode):
203 self.blocks[node.name] = node
204 else:
205 raise TypeError("Invalid type, must be instance of ``str`` or ``BlockNode``. %s" % node)
206
207 - def extend(self, other):
208 """
209 Extends the objects list of nodes with another objects nodes
210 """
211 if isinstance(other, BlockNode):
212 self.nodes.extend(other.nodes)
213 self.blocks.update(other.blocks)
214 else:
215 raise TypeError(
216 "Invalid type; must be instance of ``BlockNode``. %s" % other)
217
218 - def clear_content(self):
220
221
223
224 default_delimiters = ('{{', '}}')
225 r_tag = compile(r'(\{\{.*?\}\})', DOTALL)
226
227 r_multiline = compile(r'(""".*?""")|(\'\'\'.*?\'\'\')', DOTALL)
228
229
230
231 re_block = compile('^(elif |else:|except:|except |finally:).*$', DOTALL)
232
233
234 re_unblock = compile('^(return|continue|break|raise)( .*)?$', DOTALL)
235
236 re_pass = compile('^pass( .*)?$', DOTALL)
237
238 - def __init__(self, text,
239 name="ParserContainer",
240 context=dict(),
241 path='views/',
242 writer='response.write',
243 lexers={},
244 delimiters=('{{', '}}'),
245 _super_nodes = [],
246 ):
247 """
248 text -- text to parse
249 context -- context to parse in
250 path -- folder path to templates
251 writer -- string of writer class to use
252 lexers -- dict of custom lexers to use.
253 delimiters -- for example ('{{','}}')
254 _super_nodes -- a list of nodes to check for inclusion
255 this should only be set by "self.extend"
256 It contains a list of SuperNodes from a child
257 template that need to be handled.
258 """
259
260
261 self.name = name
262
263 self.text = text
264
265
266
267 self.writer = writer
268
269
270 if isinstance(lexers, dict):
271 self.lexers = lexers
272 else:
273 self.lexers = {}
274
275
276 self.path = path
277
278 self.context = context
279
280
281 self.delimiters = delimiters
282 if delimiters != self.default_delimiters:
283 escaped_delimiters = (escape(delimiters[0]),
284 escape(delimiters[1]))
285 self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters, DOTALL)
286 elif hasattr(context.get('response', None), 'delimiters'):
287 if context['response'].delimiters != self.default_delimiters:
288 escaped_delimiters = (
289 escape(context['response'].delimiters[0]),
290 escape(context['response'].delimiters[1]))
291 self.r_tag = compile(r'(%s.*?%s)' % escaped_delimiters,
292 DOTALL)
293
294
295 self.content = Content(name=name)
296
297
298
299
300
301 self.stack = [self.content]
302
303
304
305 self.super_nodes = []
306
307
308
309 self.child_super_nodes = _super_nodes
310
311
312
313 self.blocks = {}
314
315
316 self.parse(text)
317
319 """
320 Return the parsed template with correct indentation.
321
322 Used to make it easier to port to python3.
323 """
324 return self.reindent(str(self.content))
325
327 "Make sure str works exactly the same as python 3"
328 return self.to_string()
329
331 "Make sure str works exactly the same as python 3"
332 return self.to_string()
333
335 """
336 Reindents a string of unindented python code.
337 """
338
339
340 lines = text.split('\n')
341
342
343 new_lines = []
344
345
346
347
348 credit = 0
349
350
351 k = 0
352
353
354
355
356
357
358
359
360
361 for raw_line in lines:
362 line = raw_line.strip()
363
364
365 if not line:
366 continue
367
368
369
370
371 if TemplateParser.re_block.match(line):
372 k = k + credit - 1
373
374
375 k = max(k, 0)
376
377
378 new_lines.append(' ' * (4 * k) + line)
379
380
381 credit = 0
382
383
384 if TemplateParser.re_pass.match(line):
385 k -= 1
386
387
388
389
390
391
392 if TemplateParser.re_unblock.match(line):
393 credit = 1
394 k -= 1
395
396
397
398 if line.endswith(':') and not line.startswith('#'):
399 k += 1
400
401
402
403 new_text = '\n'.join(new_lines)
404
405 if k > 0:
406 self._raise_error('missing "pass" in view', new_text)
407 elif k < 0:
408 self._raise_error('too many "pass" in view', new_text)
409
410 return new_text
411
413 """
414 Raise an error using itself as the filename and textual content.
415 """
416 raise RestrictedError(self.name, text or self.text, message)
417
418 - def _get_file_text(self, filename):
419 """
420 Attempt to open ``filename`` and retrieve its text.
421
422 This will use self.path to search for the file.
423 """
424
425
426 if not filename.strip():
427 self._raise_error('Invalid template filename')
428
429
430 context = self.context
431 if current and not "response" in context:
432 context["response"] = getattr(current, 'response', None)
433
434
435
436 filename = eval(filename, context)
437
438
439 filepath = self.path and os.path.join(self.path, filename) or filename
440
441
442 try:
443 fileobj = open(filepath, 'rb')
444 text = fileobj.read()
445 fileobj.close()
446 except IOError:
447 self._raise_error('Unable to open included view file: ' + filepath)
448
449 return text
450
451 - def include(self, content, filename):
452 """
453 Include ``filename`` here.
454 """
455 text = self._get_file_text(filename)
456
457 t = TemplateParser(text,
458 name=filename,
459 context=self.context,
460 path=self.path,
461 writer=self.writer,
462 delimiters=self.delimiters)
463
464 content.append(t.content)
465
467 """
468 Extend ``filename``. Anything not declared in a block defined by the
469 parent will be placed in the parent templates ``{{include}}`` block.
470 """
471 text = self._get_file_text(filename)
472
473
474 super_nodes = []
475
476 super_nodes.extend(self.child_super_nodes)
477
478 super_nodes.extend(self.super_nodes)
479
480 t = TemplateParser(text,
481 name=filename,
482 context=self.context,
483 path=self.path,
484 writer=self.writer,
485 delimiters=self.delimiters,
486 _super_nodes=super_nodes)
487
488
489
490 buf = BlockNode(
491 name='__include__' + filename, delimiters=self.delimiters)
492 pre = []
493
494
495 for node in self.content.nodes:
496
497 if isinstance(node, BlockNode):
498
499 if node.name in t.content.blocks:
500
501 continue
502
503 if isinstance(node, Node):
504
505
506 if node.pre_extend:
507 pre.append(node)
508 continue
509
510
511
512 buf.append(node)
513 else:
514 buf.append(node)
515
516
517
518 self.content.nodes = []
519
520 t_content = t.content
521
522
523 t_content.blocks['__include__' + filename] = buf
524
525
526 t_content.insert(pre)
527
528
529 t_content.extend(self.content)
530
531
532 self.content = t_content
533
535
536
537
538
539
540
541 in_tag = False
542 extend = None
543 pre_extend = True
544
545
546
547
548 ij = self.r_tag.split(text)
549
550
551 stack = self.stack
552 for j in range(len(ij)):
553 i = ij[j]
554
555 if i:
556 if not stack:
557 self._raise_error('The "end" tag is unmatched, please check if you have a starting "block" tag')
558
559
560 top = stack[-1]
561
562 if in_tag:
563 line = i
564
565
566 line = line[len(self.delimiters[0]):-len(self.delimiters[1])].strip()
567
568
569 if not line:
570 continue
571
572
573
574 def remove_newline(re_val):
575
576
577 return re_val.group(0).replace('\n', '\\n')
578
579
580
581
582 line = sub(TemplateParser.r_multiline,
583 remove_newline,
584 line)
585
586 if line.startswith('='):
587
588 name, value = '=', line[1:].strip()
589 else:
590 v = line.split(' ', 1)
591 if len(v) == 1:
592
593
594
595 name = v[0]
596 value = ''
597 else:
598
599
600
601
602 name = v[0]
603 value = v[1]
604
605
606
607
608
609
610
611 if name in self.lexers:
612
613
614
615
616
617
618 self.lexers[name](parser=self,
619 value=value,
620 top=top,
621 stack=stack)
622
623 elif name == '=':
624
625
626 buf = "\n%s(%s)" % (self.writer, value)
627 top.append(Node(buf, pre_extend=pre_extend))
628
629 elif name == 'block' and not value.startswith('='):
630
631 node = BlockNode(name=value.strip(),
632 pre_extend=pre_extend,
633 delimiters=self.delimiters)
634
635
636 top.append(node)
637
638
639
640
641
642 stack.append(node)
643
644 elif name == 'end' and not value.startswith('='):
645
646
647
648 self.blocks[top.name] = top
649
650
651 stack.pop()
652
653 elif name == 'super' and not value.startswith('='):
654
655
656
657 if value:
658 target_node = value
659 else:
660 target_node = top.name
661
662
663 node = SuperNode(name=target_node,
664 pre_extend=pre_extend)
665
666
667 self.super_nodes.append(node)
668
669
670 top.append(node)
671
672 elif name == 'include' and not value.startswith('='):
673
674 if value:
675 self.include(top, value)
676
677
678
679 else:
680 include_node = BlockNode(
681 name='__include__' + self.name,
682 pre_extend=pre_extend,
683 delimiters=self.delimiters)
684 top.append(include_node)
685
686 elif name == 'extend' and not value.startswith('='):
687
688
689 extend = value
690 pre_extend = False
691
692 else:
693
694
695 if line and in_tag:
696
697
698 tokens = line.split('\n')
699
700
701
702
703
704
705 continuation = False
706 len_parsed = 0
707 for k, token in enumerate(tokens):
708
709 token = tokens[k] = token.strip()
710 len_parsed += len(token)
711
712 if token.startswith('='):
713 if token.endswith('\\'):
714 continuation = True
715 tokens[k] = "\n%s(%s" % (
716 self.writer, token[1:].strip())
717 else:
718 tokens[k] = "\n%s(%s)" % (
719 self.writer, token[1:].strip())
720 elif continuation:
721 tokens[k] += ')'
722 continuation = False
723
724 buf = "\n%s" % '\n'.join(tokens)
725 top.append(Node(buf, pre_extend=pre_extend))
726
727 else:
728
729 buf = "\n%s(%r, escape=False)" % (self.writer, i)
730 top.append(Node(buf, pre_extend=pre_extend))
731
732
733 in_tag = not in_tag
734
735
736 to_rm = []
737
738
739 for node in self.child_super_nodes:
740
741 if node.name in self.blocks:
742
743 node.value = self.blocks[node.name]
744
745
746 to_rm.append(node)
747
748
749 for node in to_rm:
750
751
752 self.child_super_nodes.remove(node)
753
754
755 if extend:
756 self.extend(extend)
757
758
759
760
761 -def parse_template(filename,
762 path='views/',
763 context=dict(),
764 lexers={},
765 delimiters=('{{', '}}')
766 ):
767 """
768 filename can be a view filename in the views folder or an input stream
769 path is the path of a views folder
770 context is a dictionary of symbols used to render the template
771 """
772
773
774 if isinstance(filename, str):
775 try:
776 fp = open(os.path.join(path, filename), 'rb')
777 text = fp.read()
778 fp.close()
779 except IOError:
780 raise RestrictedError(filename, '', 'Unable to find the file')
781 else:
782 text = filename.read()
783
784
785 return str(TemplateParser(text, context=context, path=path, lexers=lexers, delimiters=delimiters))
786
787
789 """
790 Returns the indented python code of text. Useful for unit testing.
791
792 """
793 return str(TemplateParser(text))
794
795
798 self.body = StringIO.StringIO()
799
800 - def write(self, data, escape=True):
801 if not escape:
802 self.body.write(str(data))
803 elif hasattr(data, 'as_html') and callable(data.as_html):
804 self.body.write(data.as_html())
805 else:
806
807 if not isinstance(data, (str, unicode)):
808 data = str(data)
809 elif isinstance(data, unicode):
810 data = data.encode('utf8', 'xmlcharrefreplace')
811 data = cgi.escape(data, True).replace("'", "'")
812 self.body.write(data)
813
814
816 """
817 A little helper to avoid escaping.
818 """
821
824
825
826
827
828
829 -def render(content="hello world",
830 stream=None,
831 filename=None,
832 path=None,
833 context={},
834 lexers={},
835 delimiters=('{{', '}}'),
836 writer='response.write'
837 ):
838 """
839 >>> render()
840 'hello world'
841 >>> render(content='abc')
842 'abc'
843 >>> render(content='abc\\'')
844 "abc'"
845 >>> render(content='a"\\'bc')
846 'a"\\'bc'
847 >>> render(content='a\\nbc')
848 'a\\nbc'
849 >>> render(content='a"bcd"e')
850 'a"bcd"e'
851 >>> render(content="'''a\\nc'''")
852 "'''a\\nc'''"
853 >>> render(content="'''a\\'c'''")
854 "'''a\'c'''"
855 >>> render(content='{{for i in range(a):}}{{=i}}<br />{{pass}}', context=dict(a=5))
856 '0<br />1<br />2<br />3<br />4<br />'
857 >>> render(content='{%for i in range(a):%}{%=i%}<br />{%pass%}', context=dict(a=5),delimiters=('{%','%}'))
858 '0<br />1<br />2<br />3<br />4<br />'
859 >>> render(content="{{='''hello\\nworld'''}}")
860 'hello\\nworld'
861 >>> render(content='{{for i in range(3):\\n=i\\npass}}')
862 '012'
863 """
864
865 try:
866 from globals import Response
867 except ImportError:
868
869 Response = DummyResponse
870
871
872 if not 'NOESCAPE' in context:
873 context['NOESCAPE'] = NOESCAPE
874
875
876 if context and 'response' in context:
877 old_response_body = context['response'].body
878 context['response'].body = StringIO.StringIO()
879 else:
880 old_response_body = None
881 context['response'] = Response()
882
883
884 if not content and not stream and not filename:
885 raise SyntaxError("Must specify a stream or filename or content")
886
887
888
889 close_stream = False
890 if not stream:
891 if filename:
892 stream = open(filename, 'rb')
893 close_stream = True
894 elif content:
895 stream = StringIO.StringIO(content)
896
897
898 code = str(TemplateParser(stream.read(
899 ), context=context, path=path, lexers=lexers, delimiters=delimiters, writer=writer))
900 try:
901 exec(code) in context
902 except Exception:
903
904 raise
905
906 if close_stream:
907 stream.close()
908
909
910 text = context['response'].body.getvalue()
911 if old_response_body is not None:
912 context['response'].body = old_response_body
913 return text
914
915
916 if __name__ == '__main__':
917 import doctest
918 doctest.testmod()
919