summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2013-10-01 17:50:45 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2013-10-01 17:50:45 -0700
commit7ec86928d51f8595aaa89ddfda894e2fbf0d22ce (patch)
treecf227eb0649ccaf15c81731fff315849d26398eb
parent817a1d0a9ce16d30690bcffe25d6d74fc4f35046 (diff)
downloadpyscss-7ec86928d51f8595aaa89ddfda894e2fbf0d22ce.tar.gz
Support showing compilation problems in-browser. #150
-rw-r--r--scss/__init__.py18
-rw-r--r--scss/errors.py100
-rw-r--r--scss/tests/test_misc.py7
3 files changed, 107 insertions, 18 deletions
diff --git a/scss/__init__.py b/scss/__init__.py
index b0487be..f2c6c80 100644
--- a/scss/__init__.py
+++ b/scss/__init__.py
@@ -293,7 +293,10 @@ class SourceFile(object):
class Scss(object):
- def __init__(self, scss_vars=None, scss_opts=None, scss_files=None, super_selector=None, library=ALL_BUILTINS_LIBRARY, func_registry=None, search_paths=None):
+ def __init__(self,
+ scss_vars=None, scss_opts=None, scss_files=None, super_selector=None,
+ live_errors=False, library=ALL_BUILTINS_LIBRARY, func_registry=None, search_paths=None):
+
if super_selector:
self.super_selector = super_selector + ' '
else:
@@ -319,6 +322,9 @@ class Scss(object):
self._library = func_registry or library
self._search_paths = search_paths
+ # If true, swallow compile errors and embed them in the output instead
+ self.live_errors = live_errors
+
self.reset()
def get_scss_constants(self):
@@ -434,7 +440,15 @@ class Scss(object):
return final_cont
- compile = Compilation
+ def compile(self, *args, **kwargs):
+ try:
+ return self.Compilation(*args, **kwargs)
+ except SassError as e:
+ if self.live_errors:
+ # TODO should this setting also capture and display warnings?
+ return e.to_css()
+ else:
+ raise
def parse_selectors(self, raw_selectors):
"""
diff --git a/scss/errors.py b/scss/errors.py
index c898500..52a8f5b 100644
--- a/scss/errors.py
+++ b/scss/errors.py
@@ -2,6 +2,28 @@ import sys
import traceback
+BROWSER_ERROR_TEMPLATE = """\
+body:before {{
+ content: {0};
+
+ display: block;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+
+ font-size: 14px;
+ margin: 1em;
+ padding: 1em;
+ border: 3px double red;
+
+ white-space: pre;
+ font-family: monospace;
+ background: #fcebeb;
+ color: black;
+}}
+"""
+
def add_error_marker(text, position, start_line=1):
"""Add a caret marking a given position in a string of input.
@@ -47,34 +69,80 @@ class SassError(Exception):
self.rule_stack.append(rule)
def format_prefix(self):
+ """Return the general name of the error and the contents of the rule or
+ property that caused the failure. This is the initial part of the
+ error message and should be error-specific.
+ """
# TODO this contains NULs and line numbers; could be much prettier
if self.rule_stack:
return "Error parsing block:\n" + " " + self.rule_stack[0].unparsed_contents
else:
return "Unknown error"
- def __str__(self):
- prefix = self.format_prefix()
+ def format_sass_stack(self):
+ """Return a "traceback" of Sass imports."""
+ if not self.rule_stack:
+ return ""
- ret = [prefix, "\n\n"]
+ ret = ["From ", self.rule_stack[0].file_and_line, "\n"]
+ last_file = self.rule_stack[0].source_file
- if self.rule_stack:
- # TODO this looks very neat-o but doesn't actually work because
- # manage_children doesn't recurse for imports :)
- ret.extend(("From ", self.rule_stack[0].file_and_line, "\n"))
- last_file = self.rule_stack[0].source_file
+ # TODO this could go away if rules knew their import chains...
+ for rule in self.rule_stack[1:]:
+ if rule.source_file is not last_file:
+ ret.extend(("...imported from ", rule.file_and_line, "\n"))
+ last_file = rule.source_file
- for rule in self.rule_stack[1:]:
- if rule.source_file is not last_file:
- ret.extend(("...imported from ", rule.file_and_line, "\n"))
- last_file = rule.source_file
+ ret.append("\n")
- ret.append("\n")
+ return "".join(ret)
- ret.append("Traceback:\n")
+ def format_python_stack(self):
+ """Return a traceback of Python frames, from where the error occurred
+ to where it was first caught and wrapped.
+ """
+ ret = ["Traceback:\n"]
ret.extend(traceback.format_tb(self.original_traceback))
- ret.extend((type(self.exc).__name__, ": ", str(self.exc), "\n"))
- return ''.join(ret)
+ return "".join(ret)
+
+ def format_original_error(self):
+ """Return the typical "TypeError: blah blah" for the original wrapped
+ error.
+ """
+ # TODO eventually we'll have sass-specific errors that will want nicer
+ # "names" in browser display and stderr
+ return "".join((type(self.exc).__name__, ": ", str(self.exc), "\n"))
+
+ def __str__(self):
+ try:
+ prefix = self.format_prefix()
+ sass_stack = self.format_sass_stack()
+ python_stack = self.format_python_stack()
+ original_error = self.format_original_error()
+
+ return prefix + sass_stack + python_stack + original_error
+ except Exception:
+ # "unprintable error" is not helpful
+ return str(self.exc)
+
+ def to_css(self):
+ """Return a stylesheet that will show the wrapped error at the top of
+ the browser window.
+ """
+ # TODO should this include the traceback? any security concerns?
+ prefix = self.format_prefix()
+ original_error = self.format_original_error()
+ sass_stack = self.format_sass_stack()
+
+ message = prefix + original_error + sass_stack
+
+ # Super simple escaping: only quotes and newlines are illegal in css
+ # strings
+ message = message.replace('\\', '\\\\')
+ message = message.replace('"', '\\"')
+ message = message.replace('\n', '\\A')
+
+ return BROWSER_ERROR_TEMPLATE.format('"' + message + '"')
class SassParseError(SassError):
diff --git a/scss/tests/test_misc.py b/scss/tests/test_misc.py
index 9cf2cb3..d19ab18 100644
--- a/scss/tests/test_misc.py
+++ b/scss/tests/test_misc.py
@@ -57,6 +57,13 @@ table {
assert expected == output
+def test_live_errors():
+ compiler = Scss(live_errors=True)
+ output = compiler.compile("""$foo: unitless(one);""")
+ assert "body:before" in output
+ assert "TypeError: Expected" in output
+
+
def test_extend_across_files():
compiler = Scss(scss_opts=dict(compress=0))
compiler._scss_files = {}