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 gluon.restricted import RestrictedError
29 from gluon.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 if not filename:
440 return ''
441
442
443 filepath = self.path and os.path.join(self.path, filename) or filename
444
445
446 try:
447 fileobj = open(filepath, 'rb')
448 text = fileobj.read()
449 fileobj.close()
450 except IOError:
451 self._raise_error('Unable to open included view file: ' + filepath)
452
453 return text
454
455 - def include(self, content, filename):
456 """
457 Include ``filename`` here.
458 """
459 text = self._get_file_text(filename)
460
461 t = TemplateParser(text,
462 name=filename,
463 context=self.context,
464 path=self.path,
465 writer=self.writer,
466 delimiters=self.delimiters)
467
468 content.append(t.content)
469
471 """
472 Extend ``filename``. Anything not declared in a block defined by the
473 parent will be placed in the parent templates ``{{include}}`` block.
474 """
475
476 text = self._get_file_text(filename) or '%sinclude%s' % tuple(self.delimiters)
477
478
479 super_nodes = []
480
481 super_nodes.extend(self.child_super_nodes)
482
483 super_nodes.extend(self.super_nodes)
484
485 t = TemplateParser(text,
486 name=filename,
487 context=self.context,
488 path=self.path,
489 writer=self.writer,
490 delimiters=self.delimiters,
491 _super_nodes=super_nodes)
492
493
494
495 buf = BlockNode(
496 name='__include__' + filename, delimiters=self.delimiters)
497 pre = []
498
499
500 for node in self.content.nodes:
501
502 if isinstance(node, BlockNode):
503
504 if node.name in t.content.blocks:
505
506 continue
507
508 if isinstance(node, Node):
509
510
511 if node.pre_extend:
512 pre.append(node)
513 continue
514
515
516
517 buf.append(node)
518 else:
519 buf.append(node)
520
521
522
523 self.content.nodes = []
524
525 t_content = t.content
526
527
528 t_content.blocks['__include__' + filename] = buf
529
530
531 t_content.insert(pre)
532
533
534 t_content.extend(self.content)
535
536
537 self.content = t_content
538
540
541
542
543
544
545
546 in_tag = False
547 extend = None
548 pre_extend = True
549
550
551
552
553 ij = self.r_tag.split(text)
554
555
556 stack = self.stack
557 for j in range(len(ij)):
558 i = ij[j]
559
560 if i:
561 if not stack:
562 self._raise_error('The "end" tag is unmatched, please check if you have a starting "block" tag')
563
564
565 top = stack[-1]
566
567 if in_tag:
568 line = i
569
570
571 line = line[len(self.delimiters[0]):-len(self.delimiters[1])].strip()
572
573
574 if not line:
575 continue
576
577
578
579 def remove_newline(re_val):
580
581
582 return re_val.group(0).replace('\n', '\\n')
583
584
585
586
587 line = sub(TemplateParser.r_multiline,
588 remove_newline,
589 line)
590
591 if line.startswith('='):
592
593 name, value = '=', line[1:].strip()
594 else:
595 v = line.split(' ', 1)
596 if len(v) == 1:
597
598
599
600 name = v[0]
601 value = ''
602 else:
603
604
605
606
607 name = v[0]
608 value = v[1]
609
610
611
612
613
614
615
616 if name in self.lexers:
617
618
619
620
621
622
623 self.lexers[name](parser=self,
624 value=value,
625 top=top,
626 stack=stack)
627
628 elif name == '=':
629
630
631 buf = "\n%s(%s)" % (self.writer, value)
632 top.append(Node(buf, pre_extend=pre_extend))
633
634 elif name == 'block' and not value.startswith('='):
635
636 node = BlockNode(name=value.strip(),
637 pre_extend=pre_extend,
638 delimiters=self.delimiters)
639
640
641 top.append(node)
642
643
644
645
646
647 stack.append(node)
648
649 elif name == 'end' and not value.startswith('='):
650
651
652
653 self.blocks[top.name] = top
654
655
656 stack.pop()
657
658 elif name == 'super' and not value.startswith('='):
659
660
661
662 if value:
663 target_node = value
664 else:
665 target_node = top.name
666
667
668 node = SuperNode(name=target_node,
669 pre_extend=pre_extend)
670
671
672 self.super_nodes.append(node)
673
674
675 top.append(node)
676
677 elif name == 'include' and not value.startswith('='):
678
679 if value:
680 self.include(top, value)
681
682
683
684 else:
685 include_node = BlockNode(
686 name='__include__' + self.name,
687 pre_extend=pre_extend,
688 delimiters=self.delimiters)
689 top.append(include_node)
690
691 elif name == 'extend' and not value.startswith('='):
692
693
694 extend = value
695 pre_extend = False
696
697 else:
698
699
700 if line and in_tag:
701
702
703 tokens = line.split('\n')
704
705
706
707
708
709
710 continuation = False
711 len_parsed = 0
712 for k, token in enumerate(tokens):
713
714 token = tokens[k] = token.strip()
715 len_parsed += len(token)
716
717 if token.startswith('='):
718 if token.endswith('\\'):
719 continuation = True
720 tokens[k] = "\n%s(%s" % (
721 self.writer, token[1:].strip())
722 else:
723 tokens[k] = "\n%s(%s)" % (
724 self.writer, token[1:].strip())
725 elif continuation:
726 tokens[k] += ')'
727 continuation = False
728
729 buf = "\n%s" % '\n'.join(tokens)
730 top.append(Node(buf, pre_extend=pre_extend))
731
732 else:
733
734 buf = "\n%s(%r, escape=False)" % (self.writer, i)
735 top.append(Node(buf, pre_extend=pre_extend))
736
737
738 in_tag = not in_tag
739
740
741 to_rm = []
742
743
744 for node in self.child_super_nodes:
745
746 if node.name in self.blocks:
747
748 node.value = self.blocks[node.name]
749
750
751 to_rm.append(node)
752
753
754 for node in to_rm:
755
756
757 self.child_super_nodes.remove(node)
758
759
760 if extend:
761 self.extend(extend)
762
763
764
765
766 -def parse_template(filename,
767 path='views/',
768 context=dict(),
769 lexers={},
770 delimiters=('{{', '}}')
771 ):
772 """
773 filename can be a view filename in the views folder or an input stream
774 path is the path of a views folder
775 context is a dictionary of symbols used to render the template
776 """
777
778
779 if isinstance(filename, str):
780 try:
781 fp = open(os.path.join(path, filename), 'rb')
782 text = fp.read()
783 fp.close()
784 except IOError:
785 raise RestrictedError(filename, '', 'Unable to find the file')
786 else:
787 text = filename.read()
788
789
790 return str(TemplateParser(text, context=context, path=path, lexers=lexers, delimiters=delimiters))
791
792
794 """
795 Returns the indented python code of text. Useful for unit testing.
796
797 """
798 return str(TemplateParser(text))
799
800
803 self.body = StringIO.StringIO()
804
805 - def write(self, data, escape=True):
806 if not escape:
807 self.body.write(str(data))
808 elif hasattr(data, 'xml') and callable(data.xml):
809 self.body.write(data.xml())
810 else:
811
812 if not isinstance(data, (str, unicode)):
813 data = str(data)
814 elif isinstance(data, unicode):
815 data = data.encode('utf8', 'xmlcharrefreplace')
816 data = cgi.escape(data, True).replace("'", "'")
817 self.body.write(data)
818
819
821 """
822 A little helper to avoid escaping.
823 """
826
829
830
831
832
833
834 -def render(content="hello world",
835 stream=None,
836 filename=None,
837 path=None,
838 context={},
839 lexers={},
840 delimiters=('{{', '}}'),
841 writer='response.write'
842 ):
843 """
844 >>> render()
845 'hello world'
846 >>> render(content='abc')
847 'abc'
848 >>> render(content='abc\'')
849 "abc'"
850 >>> render(content='a"\'bc')
851 'a"\\'bc'
852 >>> render(content='a\\nbc')
853 'a\\nbc'
854 >>> render(content='a"bcd"e')
855 'a"bcd"e'
856 >>> render(content="'''a\\nc'''")
857 "'''a\\nc'''"
858 >>> render(content="'''a\\'c'''")
859 "'''a\'c'''"
860 >>> render(content='{{for i in range(a):}}{{=i}}<br />{{pass}}', context=dict(a=5))
861 '0<br />1<br />2<br />3<br />4<br />'
862 >>> render(content='{%for i in range(a):%}{%=i%}<br />{%pass%}', context=dict(a=5),delimiters=('{%','%}'))
863 '0<br />1<br />2<br />3<br />4<br />'
864 >>> render(content="{{='''hello\\nworld'''}}")
865 'hello\\nworld'
866 >>> render(content='{{for i in range(3):\\n=i\\npass}}')
867 '012'
868 """
869
870 try:
871 from globals import Response
872 except ImportError:
873
874 Response = DummyResponse
875
876
877 if not 'NOESCAPE' in context:
878 context['NOESCAPE'] = NOESCAPE
879
880
881 if context and 'response' in context:
882 old_response_body = context['response'].body
883 context['response'].body = StringIO.StringIO()
884 else:
885 old_response_body = None
886 context['response'] = Response()
887
888
889 if not content and not stream and not filename:
890 raise SyntaxError("Must specify a stream or filename or content")
891
892
893
894 close_stream = False
895 if not stream:
896 if filename:
897 stream = open(filename, 'rb')
898 close_stream = True
899 elif content:
900 stream = StringIO.StringIO(content)
901
902
903 code = str(TemplateParser(stream.read(
904 ), context=context, path=path, lexers=lexers, delimiters=delimiters, writer=writer))
905 try:
906 exec(code) in context
907 except Exception:
908
909 raise
910
911 if close_stream:
912 stream.close()
913
914
915 text = context['response'].body.getvalue()
916 if old_response_body is not None:
917 context['response'].body = old_response_body
918 return text
919
920
921 if __name__ == '__main__':
922 import doctest
923 doctest.testmod()
924