1import re
2import os
3import six
4
5
6class Compiler(object):
7    RE_INTERPOLATE = re.compile(r'(\\)?([#!]){(.*?)}')
8    doctypes = {
9        '5': '<!DOCTYPE html>',
10        'xml': '<?xml version="1.0" encoding="utf-8" ?>',
11        'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
12        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
13        'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" '
14        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
15        'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '
16        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
17        'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" '
18        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
19        '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" '
20        '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
21        'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" '
22        '"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
23        'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" '
24        '"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">',
25    }
26    inline_tags = [
27        'a',
28        'abbr',
29        'acronym',
30        'b',
31        'br',
32        'code',
33        'em',
34        'font',
35        'i',
36        'img',
37        'ins',
38        'kbd',
39        'map',
40        'samp',
41        'small',
42        'span',
43        'strong',
44        'sub',
45        'sup',
46        'textarea',
47    ]
48    self_closing = ['meta', 'img', 'link', 'input', 'area', 'base', 'col', 'br', 'hr']
49    auto_close_code = [
50        'if',
51        'for',
52        'block',
53        'filter',
54        'autoescape',
55        'with',
56        'trans',
57        'spaceless',
58        'comment',
59        'cache',
60        'macro',
61        'localize',
62        'compress',
63        'ra,',
64    ]
65    filters = {}
66
67    def __init__(self, node, **options):
68        self.options = options
69        self.node = node
70        self.hasCompiledDoctype = False
71        self.hasCompiledTag = False
72        self.pp = options.get('pretty', True)
73        self.debug = options.get('compileDebug', False) is not False
74        self.filters.update(options.get('filters', {}))
75        self.doctypes.update(options.get('doctypes', {}))
76        # self.var_processor = options.get('var_processor', lambda x: x)
77        self.self_closing.extend(options.get('self_closing', []))
78        self.auto_close_code.extend(options.get('auto_close_code', []))
79        self.inline_tags.extend(options.get('inline_tags', []))
80        self.useRuntime = options.get('useRuntime', True)
81        self.extension = options.get('extension', None) or '.pug'
82        self.indents = 0
83        self.doctype = None
84        self.terse = False
85        self.xml = False
86        self.mixing = 0
87        self.variable_start_string = options.get("variable_start_string", "{{")
88        self.variable_end_string = options.get("variable_end_string", "}}")
89        if 'doctype' in self.options:
90            self.setDoctype(options['doctype'])
91        self.instring = False
92
93    def var_processor(self, var):
94        if isinstance(var, six.string_types) and var.startswith('_ '):
95            var = '_("%s")' % var[2:]
96        return var
97
98    def compile_top(self):
99        return ''
100
101    def compile(self):
102        self.buf = [self.compile_top()]
103        self.lastBufferedIdx = -1
104        self.visit(self.node)
105        compiled = u''.join(self.buf)
106        if isinstance(compiled, six.binary_type):
107            compiled = six.text_type(compiled, 'utf8')
108        return compiled
109
110    def setDoctype(self, name):
111        self.doctype = self.doctypes.get(name or 'default', '<!DOCTYPE %s>' % name)
112        self.terse = name in ['5', 'html']
113        self.xml = self.doctype.startswith('<?xml')
114
115    def buffer(self, str):
116        if self.lastBufferedIdx == len(self.buf):
117            self.lastBuffered += str
118            self.buf[self.lastBufferedIdx - 1] = self.lastBuffered
119        else:
120            self.buf.append(str)
121            self.lastBuffered = str
122            self.lastBufferedIdx = len(self.buf)
123
124    def visit(self, node, *args, **kwargs):
125        # debug = self.debug
126        # if debug:
127        #     self.buf.append('__pugjs.unshift({ lineno: %d, filename: %s });' % (
128        #         node.line, ('"%s"' % node.filename) if node.filename else '__pugjs[0].filename'))
129        #
130        # if node.debug==False and self.debug:
131        #     self.buf.pop()
132        #     self.buf.pop()
133
134        self.visitNode(node, *args, **kwargs)
135        # if debug: self.buf.append('__pugjs.shift();')
136
137    def visitNode(self, node, *args, **kwargs):
138        name = node.__class__.__name__
139        if self.instring and name != 'Tag':
140            self.buffer('\n')
141            self.instring = False
142        return getattr(self, 'visit%s' % name)(node, *args, **kwargs)
143
144    def visitLiteral(self, node):
145        self.buffer(node.str)
146
147    def visitBlock(self, block):
148        for node in block.nodes:
149            self.visit(node)
150
151    def visitCodeBlock(self, block):
152        self.buffer('{%% block %s %%}' % block.name)
153        if block.mode == 'prepend':
154            self.buffer(
155                '%ssuper()%s' % (self.variable_start_string, self.variable_end_string)
156            )
157        self.visitBlock(block)
158        if block.mode == 'append':
159            self.buffer(
160                '%ssuper()%s' % (self.variable_start_string, self.variable_end_string)
161            )
162        self.buffer('{% endblock %}')
163
164    def visitDoctype(self, doctype=None):
165        if doctype and (doctype.val or not self.doctype):
166            self.setDoctype(doctype.val or 'default')
167
168        if self.doctype:
169            self.buffer(self.doctype)
170        self.hasCompiledDoctype = True
171
172    def visitMixin(self, mixin):
173        if mixin.block:
174            self.buffer('{%% macro %s(%s) %%}' % (mixin.name, mixin.args))
175            self.visitBlock(mixin.block)
176            self.buffer('{% endmacro %}')
177        else:
178            self.buffer(
179                '%s%s(%s)%s'
180                % (
181                    self.variable_start_string,
182                    mixin.name,
183                    mixin.args,
184                    self.variable_end_string,
185                )
186            )
187
188    def visitTag(self, tag):
189        self.indents += 1
190        name = tag.name
191        if not self.hasCompiledTag:
192            if not self.hasCompiledDoctype and 'html' == name:
193                self.visitDoctype()
194            self.hasCompiledTag = True
195
196        if self.pp and name not in self.inline_tags and not tag.inline:
197            self.buffer('\n' + '  ' * (self.indents - 1))
198        if name in self.inline_tags or tag.inline:
199            self.instring = False
200
201        closed = name in self.self_closing and not self.xml
202        if tag.text:
203            t = tag.text.nodes[0]
204            if t.startswith(u'/'):
205                if len(t) > 1:
206                    raise Exception(
207                        '%s is self closing and should not have content.' % name
208                    )
209                closed = True
210
211        if tag.buffer:
212            self.buffer('<' + self.interpolate(name))
213        else:
214            self.buffer('<%s' % name)
215        self.visitAttributes(tag.attrs)
216        self.buffer('/>' if not self.terse and closed else '>')
217
218        if not closed:
219            if tag.code:
220                self.visitCode(tag.code)
221            if tag.text:
222                self.buffer(self.interpolate(tag.text.nodes[0].lstrip()))
223            self.escape = 'pre' == tag.name
224            # empirically check if we only contain text
225            textOnly = tag.textOnly or not bool(len(tag.block.nodes))
226            self.instring = False
227            self.visit(tag.block)
228
229            if self.pp and name not in self.inline_tags and not textOnly:
230                self.buffer('\n' + '  ' * (self.indents - 1))
231
232            if tag.buffer:
233                self.buffer('</' + self.interpolate(name) + '>')
234            else:
235                self.buffer('</%s>' % name)
236        self.indents -= 1
237
238    def visitFilter(self, filter):
239        if filter.name not in self.filters:
240            if filter.isASTFilter:
241                raise Exception('unknown ast filter "%s"' % filter.name)
242            else:
243                raise Exception('unknown filter "%s"' % filter.name)
244
245        fn = self.filters.get(filter.name)
246        if filter.isASTFilter:
247            self.buf.append(fn(filter.block, self, filter.attrs))
248        else:
249            text = ''.join(filter.block.nodes)
250            text = self.interpolate(text)
251            filter.attrs = filter.attrs or {}
252            filter.attrs['filename'] = self.options.get('filename', None)
253            self.buffer(fn(text, filter.attrs))
254
255    def html_escape(self, s):
256        return (s
257                .replace('&', '&amp;')
258                .replace('<', '&lt;')
259                .replace('>', '&gt;')
260                )
261
262    def _interpolate(self, attr, repl):
263        return self.RE_INTERPOLATE.sub(lambda matchobj: repl(matchobj.group(3)), attr)
264
265    def interpolate(self, text, escape=None):
266        def repl(matchobj):
267            if escape is None:
268                if matchobj.group(2) == '!':
269                    filter_string = ''
270                else:
271                    filter_string = '|escape'
272            else:
273                if escape is True:
274                    filter_string = '|escape'
275                else:
276                    filter_string = ''
277
278            matchvar = matchobj.group(3)
279            if (matchvar[0] in ['"', "'"]
280                    and matchvar[-1] == matchvar[0]
281                    and filter_string == '|escape'):
282                # Django doesn't correctly escape strings. To stay consistent
283                # with Pug, escape these now.
284                return self.html_escape(matchvar[1:-1])
285
286            return (
287                self.variable_start_string
288                + matchvar
289                + filter_string
290                + self.variable_end_string
291            )
292
293        return self.RE_INTERPOLATE.sub(repl, text)
294
295    def visitText(self, text):
296        text = ''.join(text.nodes)
297        text = self.interpolate(text)
298        self.buffer(text)
299        if self.pp:
300            self.buffer('\n')
301
302    def visitString(self, text):
303        instring = not text.inline
304        text = ''.join(text.nodes)
305        text = self.interpolate(text)
306        self.buffer(text)
307        self.instring = instring
308
309    def visitComment(self, comment):
310        if not comment.buffer:
311            return
312        if self.pp:
313            self.buffer('\n' + '  ' * (self.indents))
314        self.buffer('<!--%s-->' % comment.val)
315
316    def visitAssignment(self, assignment):
317        self.buffer('{%% set %s = %s %%}' % (assignment.name, assignment.val))
318
319    def format_path(self, path):
320        has_extension = '.' in os.path.basename(path)
321        if not has_extension:
322            path += self.extension
323        return path
324
325    def visitExtends(self, node):
326        path = self.format_path(node.path)
327        self.buffer('{%% extends "%s" %%}' % (path))
328
329    def visitInclude(self, node):
330        path = self.format_path(node.path)
331        self.buffer('{%% include "%s" %%}' % (path))
332
333    def visitBlockComment(self, comment):
334        if not comment.buffer:
335            return
336        isConditional = comment.val.strip().startswith('if')
337        self.buffer(
338            '<!--[%s]>' % comment.val.strip()
339            if isConditional
340            else '<!--%s' % comment.val
341        )
342        self.visit(comment.block)
343        self.buffer('<![endif]-->' if isConditional else '-->')
344
345    def visitConditional(self, conditional):
346        TYPE_CODE = {
347            'if': lambda x: 'if %s' % x,
348            'unless': lambda x: 'if not %s' % x,
349            'elif': lambda x: 'elif %s' % x,
350            'else': lambda x: 'else',
351        }
352        self.buf.append(
353            '{%% %s %%}' % TYPE_CODE[conditional.type](conditional.sentence)
354        )
355        if conditional.block:
356            self.visit(conditional.block)
357            for next in conditional.next:
358                self.visitConditional(next)
359        if conditional.type in ['if', 'unless']:
360            self.buf.append('{% endif %}')
361
362    def visitVar(self, var, escape=False):
363        var = self.var_processor(var)
364        return '%s%s%s%s' % (
365            self.variable_start_string,
366            var,
367            '|escape' if escape else '',
368            self.variable_end_string,
369        )
370
371    def visitCode(self, code):
372        if code.buffer:
373            val = code.val.lstrip()
374
375            self.buf.append(self.visitVar(val, code.escape))
376        else:
377            self.buf.append('{%% %s %%}' % code.val)
378
379        if code.block:
380            # if not code.buffer: self.buf.append('{')
381            self.visit(code.block)
382            # if not code.buffer: self.buf.append('}')
383
384            if not code.buffer:
385                code_tag = code.val.strip().split(' ', 1)[0]
386                if code_tag in self.auto_close_code:
387                    self.buf.append('{%% end%s %%}' % code_tag)
388
389    def visitEach(self, each):
390        self.buf.append(
391            '{%% for %s in %s|__pypugjs_iter:%d %%}'
392            % (','.join(each.keys), each.obj, len(each.keys))
393        )
394        self.visit(each.block)
395        self.buf.append('{% endfor %}')
396
397    def attributes(self, attrs):
398        return "%s__pypugjs_attrs(%s)%s" % (
399            self.variable_start_string,
400            attrs,
401            self.variable_end_string,
402        )
403
404    def visitDynamicAttributes(self, attrs):
405        buf, classes, params = [], [], {}
406        terse = 'terse=True' if self.terse else ''
407        for attr in attrs:
408            if attr['name'] == 'class':
409                classes.append('(%s)' % attr['val'])
410            else:
411                pair = "('%s',(%s))" % (attr['name'], attr['val'])
412                buf.append(pair)
413
414        if classes:
415            classes = " , ".join(classes)
416            buf.append("('class', (%s))" % classes)
417
418        buf = ', '.join(buf)
419        if self.terse:
420            params['terse'] = 'True'
421        if buf:
422            params['attrs'] = '[%s]' % buf
423        param_string = ', '.join(['%s=%s' % (n, v) for n, v in six.iteritems(params)])
424        if buf or terse:
425            self.buf.append(self.attributes(param_string))
426
427    def visitAttributes(self, attrs):
428        temp_attrs = []
429        for attr in attrs:
430            if (not self.useRuntime and not attr['name'] == 'class') or attr['static']:
431                if temp_attrs:
432                    self.visitDynamicAttributes(temp_attrs)
433                    temp_attrs = []
434                n, v = attr['name'], attr['val']
435                if isinstance(v, six.string_types):
436                    if self.useRuntime or attr['static']:
437                        self.buf.append(' %s=%s' % (n, v))
438                    else:
439                        self.buf.append(' %s="%s"' % (n, self.visitVar(v)))
440                elif v is True:
441                    if self.terse:
442                        self.buf.append(' %s' % (n,))
443                    else:
444                        self.buf.append(' %s="%s"' % (n, n))
445            else:
446                temp_attrs.append(attr)
447
448        if temp_attrs:
449            self.visitDynamicAttributes(temp_attrs)
450
451    @classmethod
452    def register_filter(cls, name, f):
453        cls.filters[name] = f
454
455    @classmethod
456    def register_autoclosecode(cls, name):
457        cls.auto_close_code.append(name)
458