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('&', '&') 258 .replace('<', '<') 259 .replace('>', '>') 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