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