Skip to content
This repository was archived by the owner on Aug 17, 2023. It is now read-only.

Add less, sass and escaped filters #55

Merged
merged 4 commits into from
Feb 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions hamlpy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Compiler:
'django_inline_style': False, # support both #{...} and ={...}
'tag_config': 'django', # Django vs Jinja2 tags
'custom_self_closing_tags': {}, # additional self-closing tags
'cdata': True, # wrap CSS, Javascript etc content in CDATA section
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CDATA usage isn't required in HTML5 so making it optional here like in Ruby Haml

'debug_tree': False,
}

Expand Down
124 changes: 83 additions & 41 deletions hamlpy/parser/filters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
from __future__ import unicode_literals
"""
Core HamlPy filters.

The implementation of these should match https://github.com/haml/haml/blob/master/lib/haml/filters.rb as closely as
possible. Where we differ is that we don't compile Stylus, Coffeescript etc into CSS or Javascript - but place the
content into suitable <style> and <script> that can be transformed later by something like django-compressor.
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should consider this.. that is turning :sass blocks into native CSS.

Copy link
Member Author

@rowanseymour rowanseymour Feb 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like I only just about understand how a :coffee section currently ends up as compressed js, but wouldn't that mean that the coffee→js / sass→css compilation has to happen for every request? Is it time we tried using cached templates?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We of course need the compress tag to have sass/less/coffee converted during offline compression, e.g.

- compress js
  :coffee
    foo=bar
- compress css
  :sass
    .mystyle {...}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya, I think what I was thinking of was closer to:

-compress css inline
  :sass
    .mystyle {...}

In that you want it converted and inserted directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure and that means the sass compilation happens every time that template is requested? Maybe that's not a big deal and/or caching of templates works?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well Simpla will def be compressing offline.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant just template caching - to avoid haml→html compilation for every request

Copy link

@nicpottier nicpottier Feb 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well now I'm confused, won't the offline compression do that? Or is that not converting haml to html at that point?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that just extracted the js and css? Each request still requires compiling the Haml into html. Now I'm confused

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right.. well sheesh, that seems like something we should fix. I don't even like that happening with per-process caching.


import sys
import textwrap

# Required on Python 2 to accept non-unicode output
try:
Expand Down Expand Up @@ -31,67 +37,69 @@
from future.utils import raise_from

from .core import ParseException
from .utils import html_escape


# ----------------------------------------------------------------------------------
# Core filters
# ----------------------------------------------------------------------------------

def plain(text, options):
return text
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently each filter handles indentation separately but now, like the Ruby implementation, indentation is handled outside of the filter.



def preserve(text, options):
text = text.rstrip()
text = text.replace('\n', '&#x000A;')
return text.replace('\r', '')

def plain(content, indent, options):
return textwrap.dedent(content)

def escaped(text, options):
return html_escape(text)

def preserve(content, indent, options):
s = content.rstrip()
s = s.replace('\n', '&#x000A;')
s = s.replace('\r', '')
return textwrap.dedent(s)

def cdata(text, options):
text = '\n' + text.rstrip()
text = text.replace("\n", "\n ")
return '<![CDATA[%s\n]]>' % text

def cdata(content, indent, options):
return indent + '<![CDATA[\n' \
+ content + '\n' \
+ indent + ']]>'

def css(text, options):
return style_filter(text, 'text/css', options)

def css(content, indent, options):
return '<style type=%(attr_wrapper)stext/css%(attr_wrapper)s>\n' \
'/*<![CDATA[*/\n' % {'attr_wrapper': options['attr_wrapper']} \
+ content + '\n' \
+ '/*]]>*/\n</style>'

def stylus(text, options):
return style_filter(text, 'text/stylus', options)

def stylus(content, indent, options):
return indent + '<style type=%(attr_wrapper)stext/stylus%(attr_wrapper)s>\n' \
'/*<![CDATA[*/\n' % {'attr_wrapper': options['attr_wrapper']} \
+ textwrap.dedent(content) + '\n' \
+ indent + '/*]]>*/\n</style>'

def less(text, options):
return style_filter(text, 'text/less', options)

def javascript(content, indent, options):
return '<script type=%(attr_wrapper)stext/javascript%(attr_wrapper)s>\n' \
'// <![CDATA[\n' % {'attr_wrapper': options['attr_wrapper']} \
+ (content + '\n' if content else '') \
+ '// ]]>\n</script>'

def sass(text, options):
return style_filter(text, 'text/sass', options)

def coffeescript(content, indent, options):
return '<script type=%(attr_wrapper)stext/coffeescript%(attr_wrapper)s>\n' \
'#<![CDATA[\n' % {'attr_wrapper': options['attr_wrapper']} \
+ (content + '\n' if content else '') \
+ '#]]>\n</script>'

def javascript(text, options):
return script_filter(text, 'text/javascript', '//', options)

def markdown(content, indent, options):

def coffee(text, options):
return script_filter(text, 'text/coffeescript', '#', options)


def markdown(content, options):
if not _markdown_available:
raise ParseException("Markdown is not available")

return markdown_lib(textwrap.dedent(content))
return markdown_lib(content)


def highlight(content, indent, options):
def highlight(content, options):
if not _pygments_available:
raise ParseException("Pygments is not available")

if content:
content = textwrap.dedent(content)

# let Pygments try to guess syntax but default to Python
try:
lexer = guess_lexer(content)
Expand All @@ -103,9 +111,8 @@ def highlight(content, indent, options):
return ''


def python(content, indent, options):
def python(content, options):
if content:
content = textwrap.dedent(content)
compiled_code = compile(content, "", "exec")
output_buffer = StringIO()
sys.stdout = output_buffer
Expand All @@ -123,21 +130,56 @@ def python(content, indent, options):
return ''


# ----------------------------------------------------------------------------------
# Helper functions
# ----------------------------------------------------------------------------------

def style_filter(text, mime_type, options):
indent = ' ' if options['cdata'] else ' '
text = text.rstrip().replace('\n', '\n' + indent)
type_attr = ' type=%(attr_wrapper)s%(mime_type)s%(attr_wrapper)s' % \
{'attr_wrapper': options['attr_wrapper'], 'mime_type': mime_type}
before, after = (' /*<![CDATA[*/\n', ' /*]]>*/\n') if options['cdata'] else ('', '')

return '<style%s>\n%s%s%s\n%s</style>' % (type_attr, before, indent, text, after)


def script_filter(text, mime_type, comment, options):
indent = ' ' if options['cdata'] else ' '
text = text.rstrip().replace('\n', '\n' + indent)
type_attr = ' type=%(attr_wrapper)s%(mime_type)s%(attr_wrapper)s' % \
{'attr_wrapper': options['attr_wrapper'], 'mime_type': mime_type}
before, after = (' %s<![CDATA[\n' % comment, ' %s]]>\n' % comment) if options['cdata'] else ('', '')

return '<script%s>\n%s%s%s\n%s</script>' % (type_attr, before, indent, text, after)


# ----------------------------------------------------------------------------------
# Filter registration
# ----------------------------------------------------------------------------------

FILTERS = {
'plain': plain,
'preserve': preserve,
'escaped': escaped,
'cdata': cdata,
'css': css,
'stylus': stylus,
'less': less,
'sass': sass,
'javascript': javascript,
'coffee': coffeescript,
'coffeescript': coffeescript,
'coffee': coffee,
'coffeescript': coffee,
'markdown': markdown,
'highlight': highlight,
'python': python
}


def register_filter(name, callback):
FILTERS[name] = callback


def get_filter(name):
if name not in FILTERS:
raise ParseException("No such filter: " + name)
Expand Down
10 changes: 8 additions & 2 deletions hamlpy/parser/nodes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import print_function, unicode_literals

import textwrap

from .core import ParseException, TreeNode, read_line, read_whitespace, peek_indentation
from .elements import read_element
from .filters import get_filter
Expand Down Expand Up @@ -460,10 +462,14 @@ def __init__(self, filter_name, content, indent, compiler):
self.content = content

def _render(self):
content = textwrap.dedent(self.content)

filter_func = get_filter(self.filter_name)
filtered_content = filter_func(self.content, self.indent, self.compiler.options)
content = filter_func(content, self.compiler.options)

content = self.indent + content.replace('\n', '\n' + self.indent)

self.before = filtered_content
self.before = content
self.after = self.render_newlines() if self.content else ''

def _post_render(self):
Expand Down
13 changes: 13 additions & 0 deletions hamlpy/parser/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import unicode_literals


def html_escape(s):
"""
Escapes HTML entities, matching substitutions used the Ruby Haml library
"""
s = s.replace("&", "&amp;")
s = s.replace("<", "&lt;")
s = s.replace(">", "&gt;")
s = s.replace('"', "&quot;")
s = s.replace("'", "&#039;")
return s
14 changes: 7 additions & 7 deletions hamlpy/test/templates/filterMultilineIgnore.hamlpy
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@

should } not be interpreted as a multiline string
:css
.test {
display: inline;
}
.test {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly replacing tabs with spaces

display: inline;
}
:javascript
These {
Braces should {
also
} be { ignored
These {
Braces should {
also
} be { ignored

.multilinetest2{id:'{{myId}}',
class:'{{myClass}}',
Expand Down
109 changes: 54 additions & 55 deletions hamlpy/test/templates/filterMultilineIgnore.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,78 +2,77 @@
These { { braces

should } not be interpreted as a multiline string
These { { braces&#x000A;&#x000A; should } not be interpreted as a multiline string
These { { braces&#x000A;&#x000A;should } not be interpreted as a multiline string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is consistent with how the Haml filter works

<style type='text/css'>
/*<![CDATA[*/
.test {
display: inline;
}
/*]]>*/
/*<![CDATA[*/
.test {
display: inline;
}
/*]]>*/
</style>
<script type='text/javascript'>
// <![CDATA[
These {
Braces should {
also
} be { ignored

// ]]>
//<![CDATA[
These {
Braces should {
also
} be { ignored
//]]>
</script>
<div id='{{myId}}' class='multilinetest2 {{myClass}}' alt=''></div>
<!-- The following is from hjonathan, issue #67 -->
<head>
<div class='blah'>
<script type='text/javascript'>
// <![CDATA[
<script type='text/javascript'>
//<![CDATA[
$(document).ready(function(){
$("#form{{form.initial.id}}").submit(form_submit);
//Double nesting
$(function() {
blahblahblah
});

// Javascript comment
});
//]]>
</script>
</div>
<script type='text/javascript'>
//<![CDATA[
$(document).ready(function(){
$("#form{{form.initial.id}}").submit(form_submit);
//Double nesting
$(function() {
blahblahblah
});

// Javascript comment
});
// ]]>
</script>
</div>
<script type='text/javascript'>
// <![CDATA[
$(document).ready(function(){
$("#form{{form.initial.id}}").submit(form_submit);
// Javascript comment
});
// ]]>
</script>
<style type='text/css'>
/*<![CDATA[*/
.someClass {
width: 100px;
}
/*]]>*/
</style>
//]]>
</script>
<style type='text/css'>
/*<![CDATA[*/
.someClass {
width: 100px;
}
/*]]>*/
</style>
<![CDATA[
if (a < b && a < 0)
{
return 1;
}
if (a < b && a < 0)
{
return 1;
}
]]>
</head>
<script type='text/javascript'>
// <![CDATA[
(
{
a
}
);
// ]]>
//<![CDATA[
(
{
a
}
);
//]]>
</script>
<script type='text/javascript'>
// <![CDATA[
//<![CDATA[

{
a
}
)
// ]]>
{
a
}
)
//]]>
</script>
Loading