Remove scss lib
This commit is contained in:
1 parent
5145618c39
commit
3852fc720d
32 files changed
+17
-11698
No files matched your search
@@ -1,14 +1,12 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from scss import Scss
|
|
||||||
from tornado.web import StaticFileHandler
|
from tornado.web import StaticFileHandler
|
||||||
|
|
||||||
|
|
||||||
@@ -21,27 +19,24 @@ class ClientScript(Plugin):
|
|||||||
|
|
||||||
core_static = {
|
core_static = {
|
||||||
'style': [
|
'style': [
|
||||||
'style/main.scss',
|
'style/combined.min.css',
|
||||||
'style/uniform.generic.css',
|
|
||||||
'style/uniform.css',
|
|
||||||
'style/settings.css',
|
|
||||||
],
|
],
|
||||||
'script': [
|
'script': [
|
||||||
'scripts/library/mootools.js',
|
'scripts/vendor/mootools.js',
|
||||||
'scripts/library/mootools_more.js',
|
'scripts/vendor/mootools_more.js',
|
||||||
|
'scripts/vendor/form_replacement/form_check.js',
|
||||||
|
'scripts/vendor/form_replacement/form_radio.js',
|
||||||
|
'scripts/vendor/form_replacement/form_dropdown.js',
|
||||||
|
'scripts/vendor/form_replacement/form_selectoption.js',
|
||||||
|
'scripts/vendor/spin.js',
|
||||||
|
'scripts/vendor/Array.stableSort.js',
|
||||||
|
'scripts/vendor/async.js',
|
||||||
|
'scripts/vendor/history.js',
|
||||||
'scripts/library/uniform.js',
|
'scripts/library/uniform.js',
|
||||||
'scripts/library/form_replacement/form_check.js',
|
|
||||||
'scripts/library/form_replacement/form_radio.js',
|
|
||||||
'scripts/library/form_replacement/form_dropdown.js',
|
|
||||||
'scripts/library/form_replacement/form_selectoption.js',
|
|
||||||
'scripts/library/question.js',
|
'scripts/library/question.js',
|
||||||
'scripts/library/scrollspy.js',
|
'scripts/library/scrollspy.js',
|
||||||
'scripts/library/spin.js',
|
|
||||||
'scripts/library/Array.stableSort.js',
|
|
||||||
'scripts/library/async.js',
|
|
||||||
'scripts/couchpotato.js',
|
'scripts/couchpotato.js',
|
||||||
'scripts/api.js',
|
'scripts/api.js',
|
||||||
'scripts/library/history.js',
|
|
||||||
'scripts/page.js',
|
'scripts/page.js',
|
||||||
'scripts/block.js',
|
'scripts/block.js',
|
||||||
'scripts/block/navigation.js',
|
'scripts/block/navigation.js',
|
||||||
@@ -74,16 +69,10 @@ class ClientScript(Plugin):
|
|||||||
addEvent('clientscript.get_styles', self.getStyles)
|
addEvent('clientscript.get_styles', self.getStyles)
|
||||||
addEvent('clientscript.get_scripts', self.getScripts)
|
addEvent('clientscript.get_scripts', self.getScripts)
|
||||||
|
|
||||||
addEvent('app.load', self.livereload, priority = 1)
|
|
||||||
addEvent('app.load', self.compile)
|
addEvent('app.load', self.compile)
|
||||||
|
|
||||||
self.addCore()
|
self.addCore()
|
||||||
|
|
||||||
def livereload(self):
|
|
||||||
|
|
||||||
if Env.get('dev'):
|
|
||||||
fireEvent('schedule.interval', 'livereload.watcher', self.watcher, seconds = .5)
|
|
||||||
|
|
||||||
def addCore(self):
|
def addCore(self):
|
||||||
|
|
||||||
for static_type in self.core_static:
|
for static_type in self.core_static:
|
||||||
@@ -96,39 +85,6 @@ class ClientScript(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.registerStyle(core_url, file_path, position = 'front')
|
self.registerStyle(core_url, file_path, position = 'front')
|
||||||
|
|
||||||
def watcher(self):
|
|
||||||
|
|
||||||
changed = []
|
|
||||||
|
|
||||||
for file_path in self.watches:
|
|
||||||
info = self.watches[file_path]
|
|
||||||
old_time = info['file_time']
|
|
||||||
file_time = os.path.getmtime(file_path)
|
|
||||||
if file_time > old_time:
|
|
||||||
changed.append(info['api_path'])
|
|
||||||
|
|
||||||
if info['compiled_path']:
|
|
||||||
compiler = Scss(live_errors = True, search_paths = [os.path.dirname(file_path)])
|
|
||||||
f = open(file_path, 'r').read()
|
|
||||||
f = compiler.compile(f)
|
|
||||||
|
|
||||||
self.createFile(info['compiled_path'], f.strip())
|
|
||||||
|
|
||||||
|
|
||||||
# Add file to watchlist again, with current filetime
|
|
||||||
self.watches[file_path]['file_time'] = file_time
|
|
||||||
|
|
||||||
# Notify fronted with changes
|
|
||||||
if changed:
|
|
||||||
fireEvent('notify.frontend', type = 'watcher.changed', data = changed)
|
|
||||||
|
|
||||||
def watchFile(self, file_path, api_path, compiled_path = False):
|
|
||||||
self.watches[file_path] = {
|
|
||||||
'file_time': os.path.getmtime(file_path),
|
|
||||||
'api_path': api_path,
|
|
||||||
'compiled_path': compiled_path,
|
|
||||||
}
|
|
||||||
|
|
||||||
def compile(self):
|
def compile(self):
|
||||||
|
|
||||||
# Create cache dir
|
# Create cache dir
|
||||||
@@ -160,25 +116,6 @@ class ClientScript(Plugin):
|
|||||||
|
|
||||||
f = open(file_path, 'r').read()
|
f = open(file_path, 'r').read()
|
||||||
|
|
||||||
# Compile scss
|
|
||||||
if file_path[-5:] == '.scss':
|
|
||||||
|
|
||||||
# Compile to css
|
|
||||||
compiler = Scss(live_errors = True, search_paths = [os.path.dirname(file_path)])
|
|
||||||
f = compiler.compile(f)
|
|
||||||
|
|
||||||
# Reload watcher
|
|
||||||
if Env.get('dev'):
|
|
||||||
|
|
||||||
compiled_file_name = position + '_%s.css' % url_path.replace('/', '_').split('.scss')[0]
|
|
||||||
compiled_file_path = os.path.join(minified_dir, compiled_file_name)
|
|
||||||
self.createFile(compiled_file_path, f.strip())
|
|
||||||
|
|
||||||
# Remove scss path
|
|
||||||
x = (file_path, 'minified/%s?%s' % (compiled_file_name, tryInt(time.time())))
|
|
||||||
|
|
||||||
self.watchFile(file_path, x[1], compiled_path = compiled_file_path)
|
|
||||||
|
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
|
|
||||||
if file_type == 'script':
|
if file_type == 'script':
|
||||||
@@ -212,8 +149,11 @@ class ClientScript(Plugin):
|
|||||||
return self.get('script', *args, **kwargs)
|
return self.get('script', *args, **kwargs)
|
||||||
|
|
||||||
def get(self, type, location = 'head'):
|
def get(self, type, location = 'head'):
|
||||||
paths = self.paths[type][location]
|
if type in self.paths and location in self.paths[type]:
|
||||||
return [x[1] for x in paths]
|
paths = self.paths[type][location]
|
||||||
|
return [x[1] for x in paths]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
def registerStyle(self, api_path, file_path, position = 'head'):
|
def registerStyle(self, api_path, file_path, position = 'head'):
|
||||||
self.register(api_path, file_path, 'style', position)
|
self.register(api_path, file_path, 'style', position)
|
||||||
@@ -223,8 +163,6 @@ class ClientScript(Plugin):
|
|||||||
|
|
||||||
def register(self, api_path, file_path, type, location):
|
def register(self, api_path, file_path, type, location):
|
||||||
|
|
||||||
self.watchFile(file_path, api_path)
|
|
||||||
|
|
||||||
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
||||||
|
|
||||||
if not self.original_paths[type].get(location):
|
if not self.original_paths[type].get(location):
|
||||||
|
|||||||
File renamed without changes.
File diff suppressed because it is too large.
Load diff
@@ -1,3 +0,0 @@
|
|||||||
import scss.tool
|
|
||||||
|
|
||||||
scss.tool.main()
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
"""Pure-Python scanner and parser, used if _speedups is not available."""
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
from scss.cssdefs import SEPARATOR
|
|
||||||
import re
|
|
||||||
|
|
||||||
DEBUG = False
|
|
||||||
|
|
||||||
# TODO copied from __init__
|
|
||||||
_nl_num_re = re.compile(r'\n.+' + SEPARATOR, re.MULTILINE)
|
|
||||||
_blocks_re = re.compile(r'[{},;()\'"\n]')
|
|
||||||
|
|
||||||
|
|
||||||
def _strip_selprop(selprop, lineno):
|
|
||||||
# Get the line number of the selector or property and strip all other
|
|
||||||
# line numbers that might still be there (from multiline selectors)
|
|
||||||
_lineno, _sep, selprop = selprop.partition(SEPARATOR)
|
|
||||||
if _sep == SEPARATOR:
|
|
||||||
_lineno = _lineno.strip(' \t\n;')
|
|
||||||
try:
|
|
||||||
lineno = int(_lineno)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
selprop = _lineno
|
|
||||||
selprop = _nl_num_re.sub('\n', selprop)
|
|
||||||
selprop = selprop.strip()
|
|
||||||
return selprop, lineno
|
|
||||||
|
|
||||||
|
|
||||||
def _strip(selprop):
|
|
||||||
# Strip all line numbers, ignoring them in the way
|
|
||||||
selprop, _ = _strip_selprop(selprop, None)
|
|
||||||
return selprop
|
|
||||||
|
|
||||||
|
|
||||||
def locate_blocks(codestr):
|
|
||||||
"""
|
|
||||||
For processing CSS like strings.
|
|
||||||
|
|
||||||
Either returns all selectors (that can be "smart" multi-lined, as
|
|
||||||
long as it's joined by `,`, or enclosed in `(` and `)`) with its code block
|
|
||||||
(the one between `{` and `}`, which can be nested), or the "lose" code
|
|
||||||
(properties) that doesn't have any blocks.
|
|
||||||
"""
|
|
||||||
lineno = 0
|
|
||||||
|
|
||||||
par = 0
|
|
||||||
instr = None
|
|
||||||
depth = 0
|
|
||||||
skip = False
|
|
||||||
thin = None
|
|
||||||
i = init = safe = lose = 0
|
|
||||||
start = end = None
|
|
||||||
|
|
||||||
for m in _blocks_re.finditer(codestr):
|
|
||||||
i = m.start(0)
|
|
||||||
c = codestr[i]
|
|
||||||
if instr is not None:
|
|
||||||
if c == instr:
|
|
||||||
instr = None # A string ends (FIXME: needs to accept escaped characters)
|
|
||||||
elif c in ('"', "'"):
|
|
||||||
instr = c # A string starts
|
|
||||||
elif c == '(': # parenthesis begins:
|
|
||||||
par += 1
|
|
||||||
thin = None
|
|
||||||
safe = i + 1
|
|
||||||
elif c == ')': # parenthesis ends:
|
|
||||||
par -= 1
|
|
||||||
elif not par and not instr:
|
|
||||||
if c == '{': # block begins:
|
|
||||||
if depth == 0:
|
|
||||||
if i > 0 and codestr[i - 1] == '#': # Do not process #{...} as blocks!
|
|
||||||
skip = True
|
|
||||||
else:
|
|
||||||
start = i
|
|
||||||
if thin is not None and _strip(codestr[thin:i]):
|
|
||||||
init = thin
|
|
||||||
if lose < init:
|
|
||||||
_property, lineno = _strip_selprop(codestr[lose:init], lineno)
|
|
||||||
if _property:
|
|
||||||
yield lineno, _property, None
|
|
||||||
lose = init
|
|
||||||
thin = None
|
|
||||||
depth += 1
|
|
||||||
elif c == '}': # block ends:
|
|
||||||
if depth > 0:
|
|
||||||
depth -= 1
|
|
||||||
if depth == 0:
|
|
||||||
if not skip:
|
|
||||||
end = i
|
|
||||||
_selectors, lineno = _strip_selprop(codestr[init:start], lineno)
|
|
||||||
_codestr = codestr[start + 1:end].strip()
|
|
||||||
if _selectors:
|
|
||||||
yield lineno, _selectors, _codestr
|
|
||||||
init = safe = lose = end + 1
|
|
||||||
thin = None
|
|
||||||
skip = False
|
|
||||||
elif depth == 0:
|
|
||||||
if c == ';': # End of property (or block):
|
|
||||||
init = i
|
|
||||||
if lose < init:
|
|
||||||
_property, lineno = _strip_selprop(codestr[lose:init], lineno)
|
|
||||||
if _property:
|
|
||||||
yield lineno, _property, None
|
|
||||||
init = safe = lose = i + 1
|
|
||||||
thin = None
|
|
||||||
elif c == ',':
|
|
||||||
if thin is not None and _strip(codestr[thin:i]):
|
|
||||||
init = thin
|
|
||||||
thin = None
|
|
||||||
safe = i + 1
|
|
||||||
elif c == '\n':
|
|
||||||
if thin is not None and _strip(codestr[thin:i]):
|
|
||||||
init = thin
|
|
||||||
thin = i + 1
|
|
||||||
elif thin is None and _strip(codestr[safe:i]):
|
|
||||||
thin = i + 1 # Step on thin ice, if it breaks, it breaks here
|
|
||||||
if depth > 0:
|
|
||||||
if not skip:
|
|
||||||
_selectors, lineno = _strip_selprop(codestr[init:start], lineno)
|
|
||||||
_codestr = codestr[start + 1:].strip()
|
|
||||||
if _selectors:
|
|
||||||
yield lineno, _selectors, _codestr
|
|
||||||
if par:
|
|
||||||
raise Exception("Missing closing parenthesis somewhere in block: '%s'" % _selectors)
|
|
||||||
elif instr:
|
|
||||||
raise Exception("Missing closing string somewhere in block: '%s'" % _selectors)
|
|
||||||
else:
|
|
||||||
raise Exception("Block never closed: '%s'" % _selectors)
|
|
||||||
losestr = codestr[lose:]
|
|
||||||
for _property in losestr.split(';'):
|
|
||||||
_property, lineno = _strip_selprop(_property, lineno)
|
|
||||||
if _property:
|
|
||||||
yield lineno, _property, None
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Parser
|
|
||||||
|
|
||||||
class NoMoreTokens(Exception):
|
|
||||||
"""
|
|
||||||
Another exception object, for when we run out of tokens
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Scanner(object):
|
|
||||||
def __init__(self, patterns, ignore, input=None):
|
|
||||||
"""
|
|
||||||
Patterns is [(terminal,regex)...]
|
|
||||||
Ignore is [terminal,...];
|
|
||||||
Input is a string
|
|
||||||
"""
|
|
||||||
self.reset(input)
|
|
||||||
self.ignore = ignore
|
|
||||||
# The stored patterns are a pair (compiled regex,source
|
|
||||||
# regex). If the patterns variable passed in to the
|
|
||||||
# constructor is None, we assume that the class already has a
|
|
||||||
# proper .patterns list constructed
|
|
||||||
if patterns is not None:
|
|
||||||
self.patterns = []
|
|
||||||
for k, r in patterns:
|
|
||||||
self.patterns.append((k, re.compile(r)))
|
|
||||||
|
|
||||||
def reset(self, input):
|
|
||||||
self.tokens = []
|
|
||||||
self.restrictions = []
|
|
||||||
self.input = input
|
|
||||||
self.pos = 0
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""
|
|
||||||
Print the last 10 tokens that have been scanned in
|
|
||||||
"""
|
|
||||||
output = ''
|
|
||||||
for t in self.tokens[-10:]:
|
|
||||||
output = "%s\n (@%s) %s = %s" % (output, t[0], t[2], repr(t[3]))
|
|
||||||
return output
|
|
||||||
|
|
||||||
def _scan(self, restrict):
|
|
||||||
"""
|
|
||||||
Should scan another token and add it to the list, self.tokens,
|
|
||||||
and add the restriction to self.restrictions
|
|
||||||
"""
|
|
||||||
# Keep looking for a token, ignoring any in self.ignore
|
|
||||||
token = None
|
|
||||||
while True:
|
|
||||||
best_pat = None
|
|
||||||
# Search the patterns for a match, with earlier
|
|
||||||
# tokens in the list having preference
|
|
||||||
best_pat_len = 0
|
|
||||||
for tok, regex in self.patterns:
|
|
||||||
if DEBUG:
|
|
||||||
print("\tTrying %s: %s at pos %d -> %s" % (repr(tok), repr(regex.pattern), self.pos, repr(self.input)))
|
|
||||||
# First check to see if we're restricting to this token
|
|
||||||
if restrict and tok not in restrict and tok not in self.ignore:
|
|
||||||
if DEBUG:
|
|
||||||
print("\tSkipping %r!" % (tok,))
|
|
||||||
continue
|
|
||||||
m = regex.match(self.input, self.pos)
|
|
||||||
if m:
|
|
||||||
# We got a match
|
|
||||||
best_pat = tok
|
|
||||||
best_pat_len = len(m.group(0))
|
|
||||||
if DEBUG:
|
|
||||||
print("Match OK! %s: %s at pos %d" % (repr(tok), repr(regex.pattern), self.pos))
|
|
||||||
break
|
|
||||||
|
|
||||||
# If we didn't find anything, raise an error
|
|
||||||
if best_pat is None:
|
|
||||||
msg = "Bad token found"
|
|
||||||
if restrict:
|
|
||||||
msg = "Bad token found while trying to find one of the restricted tokens: %s" % (", ".join(repr(r) for r in restrict))
|
|
||||||
raise SyntaxError("SyntaxError[@ char %s: %s]" % (repr(self.pos), msg))
|
|
||||||
|
|
||||||
# If we found something that isn't to be ignored, return it
|
|
||||||
if best_pat in self.ignore:
|
|
||||||
# This token should be ignored...
|
|
||||||
self.pos += best_pat_len
|
|
||||||
else:
|
|
||||||
end_pos = self.pos + best_pat_len
|
|
||||||
# Create a token with this data
|
|
||||||
token = (
|
|
||||||
self.pos,
|
|
||||||
end_pos,
|
|
||||||
best_pat,
|
|
||||||
self.input[self.pos:end_pos]
|
|
||||||
)
|
|
||||||
break
|
|
||||||
if token is not None:
|
|
||||||
self.pos = token[1]
|
|
||||||
# Only add this token if it's not in the list
|
|
||||||
# (to prevent looping)
|
|
||||||
if not self.tokens or token != self.tokens[-1]:
|
|
||||||
self.tokens.append(token)
|
|
||||||
self.restrictions.append(restrict)
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def token(self, i, restrict=None):
|
|
||||||
"""
|
|
||||||
Get the i'th token, and if i is one past the end, then scan
|
|
||||||
for another token; restrict is a list of tokens that
|
|
||||||
are allowed, or 0 for any token.
|
|
||||||
"""
|
|
||||||
tokens_len = len(self.tokens)
|
|
||||||
if i == tokens_len: # We are at the end, get the next...
|
|
||||||
tokens_len += self._scan(restrict)
|
|
||||||
if i < tokens_len:
|
|
||||||
if restrict and self.restrictions[i] and restrict > self.restrictions[i]:
|
|
||||||
raise NotImplementedError("Unimplemented: restriction set changed")
|
|
||||||
return self.tokens[i]
|
|
||||||
raise NoMoreTokens
|
|
||||||
|
|
||||||
def rewind(self, i):
|
|
||||||
tokens_len = len(self.tokens)
|
|
||||||
if i <= tokens_len:
|
|
||||||
token = self.tokens[i]
|
|
||||||
self.tokens = self.tokens[:i]
|
|
||||||
self.restrictions = self.restrictions[:i]
|
|
||||||
self.pos = token[0]
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
################################################################################
|
|
||||||
# Configuration:
|
|
||||||
DEBUG = False
|
|
||||||
VERBOSITY = 1
|
|
||||||
|
|
||||||
import os
|
|
||||||
PROJECT_ROOT = os.path.normpath(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
|
|
||||||
# Sass @import load_paths:
|
|
||||||
LOAD_PATHS = os.path.join(PROJECT_ROOT, 'sass/frameworks')
|
|
||||||
|
|
||||||
# Assets path, where new sprite files are created (defaults to STATIC_ROOT + '/assets'):
|
|
||||||
ASSETS_ROOT = None
|
|
||||||
# Cache files path, where cache files are saved (defaults to ASSETS_ROOT):
|
|
||||||
CACHE_ROOT = None
|
|
||||||
# Assets path, where new sprite files are created:
|
|
||||||
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
|
|
||||||
FONTS_ROOT = None # default: STATIC_ROOT
|
|
||||||
IMAGES_ROOT = None # default: STATIC_ROOT
|
|
||||||
|
|
||||||
# Urls for the static and assets:
|
|
||||||
ASSETS_URL = 'static/assets/'
|
|
||||||
STATIC_URL = 'static/'
|
|
||||||
FONTS_URL = None # default: STATIC_URL
|
|
||||||
IMAGES_URL = None # default: STATIC_URL
|
|
||||||
|
|
||||||
# Rendering style. Available values are 'nested', 'expanded', 'compact', 'compressed' and 'legacy' (defaults to 'nested'):
|
|
||||||
STYLE = 'nested'
|
|
||||||
|
|
||||||
# Use a different scope inside control structures create a scope (defaults to create new scopes for control structures, same as Sass):
|
|
||||||
CONTROL_SCOPING = True
|
|
||||||
|
|
||||||
# Throw fatal errors when finding undefined variables:
|
|
||||||
FATAL_UNDEFINED = True
|
|
||||||
|
|
||||||
SPRTE_MAP_DIRECTION = 'vertical'
|
|
||||||
@@ -1,384 +0,0 @@
|
|||||||
from math import pi
|
|
||||||
import re
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Built-in CSS color names
|
|
||||||
# See: http://www.w3.org/TR/css3-color/#svg-color
|
|
||||||
|
|
||||||
COLOR_NAMES = {
|
|
||||||
'aliceblue': (240, 248, 255, 1),
|
|
||||||
'antiquewhite': (250, 235, 215, 1),
|
|
||||||
'aqua': (0, 255, 255, 1),
|
|
||||||
'aquamarine': (127, 255, 212, 1),
|
|
||||||
'azure': (240, 255, 255, 1),
|
|
||||||
'beige': (245, 245, 220, 1),
|
|
||||||
'bisque': (255, 228, 196, 1),
|
|
||||||
'black': (0, 0, 0, 1),
|
|
||||||
'blanchedalmond': (255, 235, 205, 1),
|
|
||||||
'blue': (0, 0, 255, 1),
|
|
||||||
'blueviolet': (138, 43, 226, 1),
|
|
||||||
'brown': (165, 42, 42, 1),
|
|
||||||
'burlywood': (222, 184, 135, 1),
|
|
||||||
'cadetblue': (95, 158, 160, 1),
|
|
||||||
'chartreuse': (127, 255, 0, 1),
|
|
||||||
'chocolate': (210, 105, 30, 1),
|
|
||||||
'coral': (255, 127, 80, 1),
|
|
||||||
'cornflowerblue': (100, 149, 237, 1),
|
|
||||||
'cornsilk': (255, 248, 220, 1),
|
|
||||||
'crimson': (220, 20, 60, 1),
|
|
||||||
'cyan': (0, 255, 255, 1),
|
|
||||||
'darkblue': (0, 0, 139, 1),
|
|
||||||
'darkcyan': (0, 139, 139, 1),
|
|
||||||
'darkgoldenrod': (184, 134, 11, 1),
|
|
||||||
'darkgray': (169, 169, 169, 1),
|
|
||||||
'darkgreen': (0, 100, 0, 1),
|
|
||||||
'darkkhaki': (189, 183, 107, 1),
|
|
||||||
'darkmagenta': (139, 0, 139, 1),
|
|
||||||
'darkolivegreen': (85, 107, 47, 1),
|
|
||||||
'darkorange': (255, 140, 0, 1),
|
|
||||||
'darkorchid': (153, 50, 204, 1),
|
|
||||||
'darkred': (139, 0, 0, 1),
|
|
||||||
'darksalmon': (233, 150, 122, 1),
|
|
||||||
'darkseagreen': (143, 188, 143, 1),
|
|
||||||
'darkslateblue': (72, 61, 139, 1),
|
|
||||||
'darkslategray': (47, 79, 79, 1),
|
|
||||||
'darkturquoise': (0, 206, 209, 1),
|
|
||||||
'darkviolet': (148, 0, 211, 1),
|
|
||||||
'deeppink': (255, 20, 147, 1),
|
|
||||||
'deepskyblue': (0, 191, 255, 1),
|
|
||||||
'dimgray': (105, 105, 105, 1),
|
|
||||||
'dodgerblue': (30, 144, 255, 1),
|
|
||||||
'firebrick': (178, 34, 34, 1),
|
|
||||||
'floralwhite': (255, 250, 240, 1),
|
|
||||||
'forestgreen': (34, 139, 34, 1),
|
|
||||||
'fuchsia': (255, 0, 255, 1),
|
|
||||||
'gainsboro': (220, 220, 220, 1),
|
|
||||||
'ghostwhite': (248, 248, 255, 1),
|
|
||||||
'gold': (255, 215, 0, 1),
|
|
||||||
'goldenrod': (218, 165, 32, 1),
|
|
||||||
'gray': (128, 128, 128, 1),
|
|
||||||
'green': (0, 128, 0, 1),
|
|
||||||
'greenyellow': (173, 255, 47, 1),
|
|
||||||
'honeydew': (240, 255, 240, 1),
|
|
||||||
'hotpink': (255, 105, 180, 1),
|
|
||||||
'indianred': (205, 92, 92, 1),
|
|
||||||
'indigo': (75, 0, 130, 1),
|
|
||||||
'ivory': (255, 255, 240, 1),
|
|
||||||
'khaki': (240, 230, 140, 1),
|
|
||||||
'lavender': (230, 230, 250, 1),
|
|
||||||
'lavenderblush': (255, 240, 245, 1),
|
|
||||||
'lawngreen': (124, 252, 0, 1),
|
|
||||||
'lemonchiffon': (255, 250, 205, 1),
|
|
||||||
'lightblue': (173, 216, 230, 1),
|
|
||||||
'lightcoral': (240, 128, 128, 1),
|
|
||||||
'lightcyan': (224, 255, 255, 1),
|
|
||||||
'lightgoldenrodyellow': (250, 250, 210, 1),
|
|
||||||
'lightgreen': (144, 238, 144, 1),
|
|
||||||
'lightgrey': (211, 211, 211, 1),
|
|
||||||
'lightpink': (255, 182, 193, 1),
|
|
||||||
'lightsalmon': (255, 160, 122, 1),
|
|
||||||
'lightseagreen': (32, 178, 170, 1),
|
|
||||||
'lightskyblue': (135, 206, 250, 1),
|
|
||||||
'lightslategray': (119, 136, 153, 1),
|
|
||||||
'lightsteelblue': (176, 196, 222, 1),
|
|
||||||
'lightyellow': (255, 255, 224, 1),
|
|
||||||
'lime': (0, 255, 0, 1),
|
|
||||||
'limegreen': (50, 205, 50, 1),
|
|
||||||
'linen': (250, 240, 230, 1),
|
|
||||||
'magenta': (255, 0, 255, 1),
|
|
||||||
'maroon': (128, 0, 0, 1),
|
|
||||||
'mediumaquamarine': (102, 205, 170, 1),
|
|
||||||
'mediumblue': (0, 0, 205, 1),
|
|
||||||
'mediumorchid': (186, 85, 211, 1),
|
|
||||||
'mediumpurple': (147, 112, 219, 1),
|
|
||||||
'mediumseagreen': (60, 179, 113, 1),
|
|
||||||
'mediumslateblue': (123, 104, 238, 1),
|
|
||||||
'mediumspringgreen': (0, 250, 154, 1),
|
|
||||||
'mediumturquoise': (72, 209, 204, 1),
|
|
||||||
'mediumvioletred': (199, 21, 133, 1),
|
|
||||||
'midnightblue': (25, 25, 112, 1),
|
|
||||||
'mintcream': (245, 255, 250, 1),
|
|
||||||
'mistyrose': (255, 228, 225, 1),
|
|
||||||
'moccasin': (255, 228, 181, 1),
|
|
||||||
'navajowhite': (255, 222, 173, 1),
|
|
||||||
'navy': (0, 0, 128, 1),
|
|
||||||
'oldlace': (253, 245, 230, 1),
|
|
||||||
'olive': (128, 128, 0, 1),
|
|
||||||
'olivedrab': (107, 142, 35, 1),
|
|
||||||
'orange': (255, 165, 0, 1),
|
|
||||||
'orangered': (255, 69, 0, 1),
|
|
||||||
'orchid': (218, 112, 214, 1),
|
|
||||||
'palegoldenrod': (238, 232, 170, 1),
|
|
||||||
'palegreen': (152, 251, 152, 1),
|
|
||||||
'paleturquoise': (175, 238, 238, 1),
|
|
||||||
'palevioletred': (219, 112, 147, 1),
|
|
||||||
'papayawhip': (255, 239, 213, 1),
|
|
||||||
'peachpuff': (255, 218, 185, 1),
|
|
||||||
'peru': (205, 133, 63, 1),
|
|
||||||
'pink': (255, 192, 203, 1),
|
|
||||||
'plum': (221, 160, 221, 1),
|
|
||||||
'powderblue': (176, 224, 230, 1),
|
|
||||||
'purple': (128, 0, 128, 1),
|
|
||||||
'red': (255, 0, 0, 1),
|
|
||||||
'rosybrown': (188, 143, 143, 1),
|
|
||||||
'royalblue': (65, 105, 225, 1),
|
|
||||||
'saddlebrown': (139, 69, 19, 1),
|
|
||||||
'salmon': (250, 128, 114, 1),
|
|
||||||
'sandybrown': (244, 164, 96, 1),
|
|
||||||
'seagreen': (46, 139, 87, 1),
|
|
||||||
'seashell': (255, 245, 238, 1),
|
|
||||||
'sienna': (160, 82, 45, 1),
|
|
||||||
'silver': (192, 192, 192, 1),
|
|
||||||
'skyblue': (135, 206, 235, 1),
|
|
||||||
'slateblue': (106, 90, 205, 1),
|
|
||||||
'slategray': (112, 128, 144, 1),
|
|
||||||
'snow': (255, 250, 250, 1),
|
|
||||||
'springgreen': (0, 255, 127, 1),
|
|
||||||
'steelblue': (70, 130, 180, 1),
|
|
||||||
'tan': (210, 180, 140, 1),
|
|
||||||
'teal': (0, 128, 128, 1),
|
|
||||||
'thistle': (216, 191, 216, 1),
|
|
||||||
'tomato': (255, 99, 71, 1),
|
|
||||||
'transparent': (0, 0, 0, 0),
|
|
||||||
'turquoise': (64, 224, 208, 1),
|
|
||||||
'violet': (238, 130, 238, 1),
|
|
||||||
'wheat': (245, 222, 179, 1),
|
|
||||||
'white': (255, 255, 255, 1),
|
|
||||||
'whitesmoke': (245, 245, 245, 1),
|
|
||||||
'yellow': (255, 255, 0, 1),
|
|
||||||
'yellowgreen': (154, 205, 50, 1),
|
|
||||||
}
|
|
||||||
COLOR_LOOKUP = dict((v, k) for (k, v) in COLOR_NAMES.items())
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Built-in CSS units
|
|
||||||
# See: http://www.w3.org/TR/2013/CR-css3-values-20130730/#numeric-types
|
|
||||||
|
|
||||||
# Maps units to a set of common units per type, with conversion factors
|
|
||||||
BASE_UNIT_CONVERSIONS = {
|
|
||||||
# Lengths
|
|
||||||
'mm': (1, 'mm'),
|
|
||||||
'cm': (10, 'mm'),
|
|
||||||
'in': (25.4, 'mm'),
|
|
||||||
'px': (25.4 / 96, 'mm'),
|
|
||||||
'pt': (25.4 / 72, 'mm'),
|
|
||||||
'pc': (25.4 / 6, 'mm'),
|
|
||||||
|
|
||||||
# Angles
|
|
||||||
'deg': (1 / 360, 'turn'),
|
|
||||||
'grad': (1 / 400, 'turn'),
|
|
||||||
'rad': (pi / 2, 'turn'),
|
|
||||||
'turn': (1, 'turn'),
|
|
||||||
|
|
||||||
# Times
|
|
||||||
'ms': (1, 'ms'),
|
|
||||||
's': (1000, 'ms'),
|
|
||||||
|
|
||||||
# Frequencies
|
|
||||||
'hz': (1, 'hz'),
|
|
||||||
'khz': (1000, 'hz'),
|
|
||||||
|
|
||||||
# Resolutions
|
|
||||||
'dpi': (1, 'dpi'),
|
|
||||||
'dpcm': (2.54, 'dpi'),
|
|
||||||
'dppx': (96, 'dpi'),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_conversion_factor(unit):
|
|
||||||
"""Look up the "base" unit for this unit and the factor for converting to
|
|
||||||
it.
|
|
||||||
|
|
||||||
Returns a 2-tuple of `factor, base_unit`.
|
|
||||||
"""
|
|
||||||
if unit in BASE_UNIT_CONVERSIONS:
|
|
||||||
return BASE_UNIT_CONVERSIONS[unit]
|
|
||||||
else:
|
|
||||||
return 1, unit
|
|
||||||
|
|
||||||
|
|
||||||
def convert_units_to_base_units(units):
|
|
||||||
"""Convert a set of units into a set of "base" units.
|
|
||||||
|
|
||||||
Returns a 2-tuple of `factor, new_units`.
|
|
||||||
"""
|
|
||||||
total_factor = 1
|
|
||||||
new_units = []
|
|
||||||
for unit in units:
|
|
||||||
if unit not in BASE_UNIT_CONVERSIONS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
factor, new_unit = BASE_UNIT_CONVERSIONS[unit]
|
|
||||||
total_factor *= factor
|
|
||||||
new_units.append(new_unit)
|
|
||||||
|
|
||||||
new_units.sort()
|
|
||||||
return total_factor, tuple(new_units)
|
|
||||||
|
|
||||||
|
|
||||||
def count_base_units(units):
|
|
||||||
"""Returns a dict mapping names of base units to how many times they
|
|
||||||
appear in the given iterable of units. Effectively this counts how
|
|
||||||
many length units you have, how many time units, and so forth.
|
|
||||||
"""
|
|
||||||
ret = {}
|
|
||||||
for unit in units:
|
|
||||||
factor, base_unit = get_conversion_factor(unit)
|
|
||||||
|
|
||||||
ret.setdefault(base_unit, 0)
|
|
||||||
ret[base_unit] += 1
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_base_units(units, to_remove):
|
|
||||||
"""Given a list of units, remove a specified number of each base unit.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
units: an iterable of units
|
|
||||||
to_remove: a mapping of base_unit => count, such as that returned from
|
|
||||||
count_base_units
|
|
||||||
|
|
||||||
Returns a 2-tuple of (factor, remaining_units).
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Copy the dict since we're about to mutate it
|
|
||||||
to_remove = to_remove.copy()
|
|
||||||
remaining_units = []
|
|
||||||
total_factor = 1
|
|
||||||
|
|
||||||
for unit in units:
|
|
||||||
factor, base_unit = get_conversion_factor(unit)
|
|
||||||
if not to_remove.get(base_unit, 0):
|
|
||||||
remaining_units.append(unit)
|
|
||||||
continue
|
|
||||||
|
|
||||||
total_factor *= factor
|
|
||||||
to_remove[base_unit] -= 1
|
|
||||||
|
|
||||||
return total_factor, remaining_units
|
|
||||||
|
|
||||||
|
|
||||||
# A fixed set of units can be omitted when the value is 0
|
|
||||||
# See: http://www.w3.org/TR/2013/CR-css3-values-20130730/#lengths
|
|
||||||
ZEROABLE_UNITS = frozenset((
|
|
||||||
# Relative lengths
|
|
||||||
'em', 'ex', 'ch', 'rem',
|
|
||||||
# Viewport
|
|
||||||
'vw', 'vh', 'vmin', 'vmax',
|
|
||||||
# Absolute lengths
|
|
||||||
'cm', 'mm', 'in', 'px', 'pt', 'pc',
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Built-in CSS function reference
|
|
||||||
|
|
||||||
# Known function names
|
|
||||||
BUILTIN_FUNCTIONS = frozenset([
|
|
||||||
# CSS2
|
|
||||||
'attr', 'counter', 'counters', 'url', 'rgb', 'rect',
|
|
||||||
|
|
||||||
# CSS3 values: http://www.w3.org/TR/css3-values/
|
|
||||||
'calc', 'min', 'max', 'cycle',
|
|
||||||
|
|
||||||
# CSS3 colors: http://www.w3.org/TR/css3-color/
|
|
||||||
'rgba', 'hsl', 'hsla',
|
|
||||||
|
|
||||||
# CSS3 fonts: http://www.w3.org/TR/css3-fonts/
|
|
||||||
'local', 'format',
|
|
||||||
|
|
||||||
# CSS3 images: http://www.w3.org/TR/css3-images/
|
|
||||||
'image', 'element',
|
|
||||||
'linear-gradient', 'radial-gradient',
|
|
||||||
'repeating-linear-gradient', 'repeating-radial-gradient',
|
|
||||||
|
|
||||||
# CSS3 transforms: http://www.w3.org/TR/css3-transforms/
|
|
||||||
'perspective',
|
|
||||||
'matrix', 'matrix3d',
|
|
||||||
'rotate', 'rotateX', 'rotateY', 'rotateZ', 'rotate3d',
|
|
||||||
'translate', 'translateX', 'translateY', 'translateZ', 'translate3d',
|
|
||||||
'scale', 'scaleX', 'scaleY', 'scaleZ', 'scale3d',
|
|
||||||
'skew', 'skewX', 'skewY',
|
|
||||||
|
|
||||||
# CSS3 transitions: http://www.w3.org/TR/css3-transitions/
|
|
||||||
'cubic-bezier', 'steps',
|
|
||||||
|
|
||||||
# CSS filter effects:
|
|
||||||
# https://dvcs.w3.org/hg/FXTF/raw-file/tip/filters/index.html
|
|
||||||
'grayscale', 'sepia', 'saturate', 'hue-rotate', 'invert', 'opacity',
|
|
||||||
'brightness', 'contrast', 'blur', 'drop-shadow', 'custom',
|
|
||||||
|
|
||||||
# CSS4 image module:
|
|
||||||
# http://dev.w3.org/csswg/css-images/
|
|
||||||
'image-set', 'cross-fade',
|
|
||||||
'conic-gradient', 'repeating-conic-gradient',
|
|
||||||
|
|
||||||
# Others
|
|
||||||
'color-stop', # Older version of CSS3 gradients
|
|
||||||
'mask', # ???
|
|
||||||
'from', 'to', # Very old WebKit gradient syntax
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def is_builtin_css_function(name):
|
|
||||||
"""Returns whether the given `name` looks like the name of a builtin CSS
|
|
||||||
function.
|
|
||||||
|
|
||||||
Unrecognized functions not in this list produce warnings.
|
|
||||||
"""
|
|
||||||
name = name.replace('_', '-')
|
|
||||||
|
|
||||||
if name in BUILTIN_FUNCTIONS:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Vendor-specific functions (-foo-bar) are always okay
|
|
||||||
if name[0] == '-' and '-' in name[1:]:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Bits and pieces of grammar, as regexen
|
|
||||||
|
|
||||||
SEPARATOR = '\x00'
|
|
||||||
|
|
||||||
_expr_glob_re = re.compile(r'''
|
|
||||||
\#\{(.*?)\} # Global Interpolation only
|
|
||||||
''', re.VERBOSE)
|
|
||||||
|
|
||||||
# XXX these still need to be fixed; the //-in-functions thing is a chumpy hack
|
|
||||||
_ml_comment_re = re.compile(r'\/\*(.*?)\*\/', re.DOTALL)
|
|
||||||
_sl_comment_re = re.compile(r'(?<!\burl[(])(?<!\w{2}:)\/\/.*')
|
|
||||||
|
|
||||||
_escape_chars_re = re.compile(r'([^-a-zA-Z0-9_])')
|
|
||||||
_interpolate_re = re.compile(r'(#\{\s*)?(\$[-\w]+)(?(1)\s*\})')
|
|
||||||
_spaces_re = re.compile(r'\s+')
|
|
||||||
_expand_rules_space_re = re.compile(r'\s*{')
|
|
||||||
_collapse_properties_space_re = re.compile(r'([:#])\s*{')
|
|
||||||
_variable_re = re.compile('^\\$[-a-zA-Z0-9_]+$')
|
|
||||||
|
|
||||||
_strings_re = re.compile(r'([\'"]).*?\1')
|
|
||||||
|
|
||||||
_has_placeholder_re = re.compile(r'(?<!\w)([a-z]\w*)?%')
|
|
||||||
_prop_split_re = re.compile(r'[:=]')
|
|
||||||
_has_code_re = re.compile('''
|
|
||||||
(?:^|(?<=[{;}])) # the character just before it should be a '{', a ';' or a '}'
|
|
||||||
\s* # ...followed by any number of spaces
|
|
||||||
(?:
|
|
||||||
(?:
|
|
||||||
\+
|
|
||||||
| @include
|
|
||||||
| @warn
|
|
||||||
| @mixin
|
|
||||||
| @function
|
|
||||||
| @if
|
|
||||||
| @else
|
|
||||||
| @for
|
|
||||||
| @each
|
|
||||||
)
|
|
||||||
(?![^(:;}]*['"])
|
|
||||||
|
|
|
||||||
@import
|
|
||||||
)
|
|
||||||
''', re.VERBOSE)
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
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.
|
|
||||||
|
|
||||||
Returns (new_text, caret_line).
|
|
||||||
"""
|
|
||||||
indent = " "
|
|
||||||
lines = []
|
|
||||||
caret_line = start_line
|
|
||||||
for line in text.split("\n"):
|
|
||||||
lines.append(indent + line)
|
|
||||||
|
|
||||||
if 0 <= position <= len(line):
|
|
||||||
lines.append(indent + (" " * position) + "^")
|
|
||||||
caret_line = start_line
|
|
||||||
|
|
||||||
position -= len(line)
|
|
||||||
position -= 1 # for the newline
|
|
||||||
start_line += 1
|
|
||||||
|
|
||||||
return "\n".join(lines), caret_line
|
|
||||||
|
|
||||||
|
|
||||||
class SassError(Exception):
|
|
||||||
"""Error class that wraps another exception and attempts to bolt on some
|
|
||||||
useful context.
|
|
||||||
"""
|
|
||||||
def __init__(self, exc, rule=None, expression=None, expression_pos=None):
|
|
||||||
self.exc = exc
|
|
||||||
|
|
||||||
self.rule_stack = []
|
|
||||||
if rule:
|
|
||||||
self.rule_stack.append(rule)
|
|
||||||
|
|
||||||
self.expression = expression
|
|
||||||
self.expression_pos = expression_pos
|
|
||||||
|
|
||||||
_, _, self.original_traceback = sys.exc_info()
|
|
||||||
|
|
||||||
def add_rule(self, rule):
|
|
||||||
"""Add a new rule to the "stack" of rules -- this is used to track,
|
|
||||||
e.g., how a file was ultimately imported.
|
|
||||||
"""
|
|
||||||
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 + "\n"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return "Unknown error\n"
|
|
||||||
|
|
||||||
def format_sass_stack(self):
|
|
||||||
"""Return a "traceback" of Sass imports."""
|
|
||||||
if not self.rule_stack:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
ret = ["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
|
|
||||||
|
|
||||||
return "".join(ret)
|
|
||||||
|
|
||||||
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))
|
|
||||||
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()
|
|
||||||
|
|
||||||
# TODO not very well-specified whether these parts should already
|
|
||||||
# end in newlines, or how many
|
|
||||||
return prefix + "\n" + 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 + "\n" + sass_stack + original_error
|
|
||||||
|
|
||||||
# Super simple escaping: only quotes and newlines are illegal in css
|
|
||||||
# strings
|
|
||||||
message = message.replace('\\', '\\\\')
|
|
||||||
message = message.replace('"', '\\"')
|
|
||||||
# use the maximum six digits here so it doesn't eat any following
|
|
||||||
# characters that happen to look like hex
|
|
||||||
message = message.replace('\n', '\\00000A')
|
|
||||||
|
|
||||||
return BROWSER_ERROR_TEMPLATE.format('"' + message + '"')
|
|
||||||
|
|
||||||
|
|
||||||
class SassParseError(SassError):
|
|
||||||
"""Error raised when parsing a Sass expression fails."""
|
|
||||||
|
|
||||||
def format_prefix(self):
|
|
||||||
decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
|
|
||||||
return """Error parsing expression:\n{0}\n""".format(decorated_expr)
|
|
||||||
|
|
||||||
|
|
||||||
class SassEvaluationError(SassError):
|
|
||||||
"""Error raised when evaluating a parsed expression fails."""
|
|
||||||
|
|
||||||
def format_prefix(self):
|
|
||||||
# TODO would be nice for the AST to have position information
|
|
||||||
# TODO might be nice to print the AST and indicate where the failure
|
|
||||||
# was?
|
|
||||||
decorated_expr, line = add_error_marker(self.expression, self.expression_pos or -1)
|
|
||||||
return """Error evaluating expression:\n{0}\n""".format(decorated_expr)
|
|
||||||
@@ -1,905 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
import logging
|
|
||||||
import operator
|
|
||||||
import re
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import scss.config as config
|
|
||||||
from scss.cssdefs import COLOR_NAMES, is_builtin_css_function, _expr_glob_re, _interpolate_re
|
|
||||||
from scss.errors import SassError, SassEvaluationError, SassParseError
|
|
||||||
from scss.rule import Namespace
|
|
||||||
from scss.types import Boolean, Color, List, Map, Null, Number, String, Undefined, Value
|
|
||||||
from scss.util import dequote, normalize_var
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Load C acceleration modules
|
|
||||||
Scanner = None
|
|
||||||
try:
|
|
||||||
from scss._speedups import Scanner
|
|
||||||
except ImportError:
|
|
||||||
from scss._native import Scanner
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class Calculator(object):
|
|
||||||
"""Expression evaluator."""
|
|
||||||
|
|
||||||
ast_cache = {}
|
|
||||||
|
|
||||||
def __init__(self, namespace=None):
|
|
||||||
if namespace is None:
|
|
||||||
self.namespace = Namespace()
|
|
||||||
else:
|
|
||||||
self.namespace = namespace
|
|
||||||
|
|
||||||
def _pound_substitute(self, result):
|
|
||||||
expr = result.group(1)
|
|
||||||
value = self.evaluate_expression(expr)
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return self.apply_vars(expr)
|
|
||||||
elif value.is_null:
|
|
||||||
return ""
|
|
||||||
else:
|
|
||||||
return dequote(value.render())
|
|
||||||
|
|
||||||
def do_glob_math(self, cont):
|
|
||||||
"""Performs #{}-interpolation. The result is always treated as a fixed
|
|
||||||
syntactic unit and will not be re-evaluated.
|
|
||||||
"""
|
|
||||||
# TODO that's a lie! this should be in the parser for most cases.
|
|
||||||
cont = str(cont)
|
|
||||||
if '#{' not in cont:
|
|
||||||
return cont
|
|
||||||
cont = _expr_glob_re.sub(self._pound_substitute, cont)
|
|
||||||
return cont
|
|
||||||
|
|
||||||
def apply_vars(self, cont):
|
|
||||||
# TODO this is very complicated. it should go away once everything
|
|
||||||
# valid is actually parseable.
|
|
||||||
if isinstance(cont, six.string_types) and '$' in cont:
|
|
||||||
try:
|
|
||||||
# Optimization: the full cont is a variable in the context,
|
|
||||||
cont = self.namespace.variable(cont)
|
|
||||||
except KeyError:
|
|
||||||
# Interpolate variables:
|
|
||||||
def _av(m):
|
|
||||||
v = None
|
|
||||||
n = m.group(2)
|
|
||||||
try:
|
|
||||||
v = self.namespace.variable(n)
|
|
||||||
except KeyError:
|
|
||||||
if config.FATAL_UNDEFINED:
|
|
||||||
raise SyntaxError("Undefined variable: '%s'." % n)
|
|
||||||
else:
|
|
||||||
if config.VERBOSITY > 1:
|
|
||||||
log.error("Undefined variable '%s'", n, extra={'stack': True})
|
|
||||||
return n
|
|
||||||
else:
|
|
||||||
if v:
|
|
||||||
if not isinstance(v, six.string_types):
|
|
||||||
v = v.render()
|
|
||||||
# TODO this used to test for _dequote
|
|
||||||
if m.group(1):
|
|
||||||
v = dequote(v)
|
|
||||||
else:
|
|
||||||
v = m.group(0)
|
|
||||||
return v
|
|
||||||
|
|
||||||
cont = _interpolate_re.sub(_av, cont)
|
|
||||||
# TODO this is surprising and shouldn't be here
|
|
||||||
cont = self.do_glob_math(cont)
|
|
||||||
return cont
|
|
||||||
|
|
||||||
def calculate(self, _base_str, divide=False):
|
|
||||||
better_expr_str = _base_str
|
|
||||||
|
|
||||||
better_expr_str = self.do_glob_math(better_expr_str)
|
|
||||||
|
|
||||||
better_expr_str = self.evaluate_expression(better_expr_str, divide=divide)
|
|
||||||
|
|
||||||
if better_expr_str is None:
|
|
||||||
better_expr_str = String.unquoted(self.apply_vars(_base_str))
|
|
||||||
|
|
||||||
return better_expr_str
|
|
||||||
|
|
||||||
# TODO only used by magic-import...?
|
|
||||||
def interpolate(self, var):
|
|
||||||
value = self.namespace.variable(var)
|
|
||||||
if var != value and isinstance(value, six.string_types):
|
|
||||||
_vi = self.evaluate_expression(value)
|
|
||||||
if _vi is not None:
|
|
||||||
value = _vi
|
|
||||||
return value
|
|
||||||
|
|
||||||
def evaluate_expression(self, expr, divide=False):
|
|
||||||
try:
|
|
||||||
ast = self.parse_expression(expr)
|
|
||||||
except SassError:
|
|
||||||
if config.DEBUG:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
return ast.evaluate(self, divide=divide)
|
|
||||||
except Exception as e:
|
|
||||||
raise SassEvaluationError(e, expression=expr)
|
|
||||||
|
|
||||||
def parse_expression(self, expr, target='goal'):
|
|
||||||
if not isinstance(expr, six.string_types):
|
|
||||||
raise TypeError("Expected string, got %r" % (expr,))
|
|
||||||
|
|
||||||
key = (target, expr)
|
|
||||||
if key in self.ast_cache:
|
|
||||||
return self.ast_cache[key]
|
|
||||||
|
|
||||||
try:
|
|
||||||
parser = SassExpression(SassExpressionScanner(expr))
|
|
||||||
ast = getattr(parser, target)()
|
|
||||||
except SyntaxError as e:
|
|
||||||
raise SassParseError(e, expression=expr, expression_pos=parser._char_pos)
|
|
||||||
|
|
||||||
self.ast_cache[key] = ast
|
|
||||||
return ast
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Expression classes -- the AST resulting from a parse
|
|
||||||
|
|
||||||
class Expression(object):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s()>' % (self.__class__.__name__)
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
"""Evaluate this AST node, and return a Sass value.
|
|
||||||
|
|
||||||
`divide` indicates whether a descendant node representing a division
|
|
||||||
should be forcibly treated as a division. See the commentary in
|
|
||||||
`BinaryOp`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class Parentheses(Expression):
|
|
||||||
"""An expression of the form `(foo)`.
|
|
||||||
|
|
||||||
Only exists to force a slash to be interpreted as division when contained
|
|
||||||
within parentheses.
|
|
||||||
"""
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s)>' % (self.__class__.__name__, repr(self.contents))
|
|
||||||
|
|
||||||
def __init__(self, contents):
|
|
||||||
self.contents = contents
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
return self.contents.evaluate(calculator, divide=True)
|
|
||||||
|
|
||||||
|
|
||||||
class UnaryOp(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.operand))
|
|
||||||
|
|
||||||
def __init__(self, op, operand):
|
|
||||||
self.op = op
|
|
||||||
self.operand = operand
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
return self.op(self.operand.evaluate(calculator, divide=True))
|
|
||||||
|
|
||||||
|
|
||||||
class BinaryOp(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s, %s, %s)>' % (self.__class__.__name__, repr(self.op), repr(self.left), repr(self.right))
|
|
||||||
|
|
||||||
def __init__(self, op, left, right):
|
|
||||||
self.op = op
|
|
||||||
self.left = left
|
|
||||||
self.right = right
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
left = self.left.evaluate(calculator, divide=True)
|
|
||||||
right = self.right.evaluate(calculator, divide=True)
|
|
||||||
|
|
||||||
# Special handling of division: treat it as a literal slash if both
|
|
||||||
# operands are literals, there are parentheses, or this is part of a
|
|
||||||
# bigger expression.
|
|
||||||
# The first condition is covered by the type check. The other two are
|
|
||||||
# covered by the `divide` argument: other nodes that perform arithmetic
|
|
||||||
# will pass in True, indicating that this should always be a division.
|
|
||||||
if (
|
|
||||||
self.op is operator.truediv
|
|
||||||
and not divide
|
|
||||||
and isinstance(self.left, Literal)
|
|
||||||
and isinstance(self.right, Literal)
|
|
||||||
):
|
|
||||||
return String(left.render() + ' / ' + right.render(), quotes=None)
|
|
||||||
|
|
||||||
return self.op(left, right)
|
|
||||||
|
|
||||||
|
|
||||||
class AnyOp(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(*%s)>' % (self.__class__.__name__, repr(self.op), repr(self.operands))
|
|
||||||
|
|
||||||
def __init__(self, *operands):
|
|
||||||
self.operands = operands
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
for operand in self.operands:
|
|
||||||
value = operand.evaluate(calculator, divide=True)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class AllOp(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(*%s)>' % (self.__class__.__name__, repr(self.operands))
|
|
||||||
|
|
||||||
def __init__(self, *operands):
|
|
||||||
self.operands = operands
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
for operand in self.operands:
|
|
||||||
value = operand.evaluate(calculator, divide=True)
|
|
||||||
if not value:
|
|
||||||
return value
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class NotOp(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s)>' % (self.__class__.__name__, repr(self.operand))
|
|
||||||
|
|
||||||
def __init__(self, operand):
|
|
||||||
self.operand = operand
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
operand = self.operand.evaluate(calculator, divide=True)
|
|
||||||
return Boolean(not(operand))
|
|
||||||
|
|
||||||
|
|
||||||
class CallOp(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s, %s)>' % (self.__class__.__name__, repr(self.func_name), repr(self.argspec))
|
|
||||||
|
|
||||||
def __init__(self, func_name, argspec):
|
|
||||||
self.func_name = func_name
|
|
||||||
self.argspec = argspec
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
# TODO bake this into the context and options "dicts", plus library
|
|
||||||
func_name = normalize_var(self.func_name)
|
|
||||||
|
|
||||||
argspec_node = self.argspec
|
|
||||||
|
|
||||||
# Turn the pairs of arg tuples into *args and **kwargs
|
|
||||||
# TODO unclear whether this is correct -- how does arg, kwarg, arg
|
|
||||||
# work?
|
|
||||||
args, kwargs = argspec_node.evaluate_call_args(calculator)
|
|
||||||
argspec_len = len(args) + len(kwargs)
|
|
||||||
|
|
||||||
# Translate variable names to Python identifiers
|
|
||||||
# TODO what about duplicate kw names? should this happen in argspec?
|
|
||||||
# how does that affect mixins?
|
|
||||||
kwargs = dict(
|
|
||||||
(key.lstrip('$').replace('-', '_'), value)
|
|
||||||
for key, value in kwargs.items())
|
|
||||||
|
|
||||||
# TODO merge this with the library
|
|
||||||
funct = None
|
|
||||||
try:
|
|
||||||
funct = calculator.namespace.function(func_name, argspec_len)
|
|
||||||
# @functions take a ns as first arg. TODO: Python functions possibly
|
|
||||||
# should too
|
|
||||||
if getattr(funct, '__name__', None) == '__call':
|
|
||||||
funct = partial(funct, calculator.namespace)
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
# DEVIATION: Fall back to single parameter
|
|
||||||
funct = calculator.namespace.function(func_name, 1)
|
|
||||||
args = [List(args, use_comma=True)]
|
|
||||||
except KeyError:
|
|
||||||
if not is_builtin_css_function(func_name):
|
|
||||||
log.error("Function not found: %s:%s", func_name, argspec_len, extra={'stack': True})
|
|
||||||
|
|
||||||
if funct:
|
|
||||||
ret = funct(*args, **kwargs)
|
|
||||||
if not isinstance(ret, Value):
|
|
||||||
raise TypeError("Expected Sass type as return value, got %r" % (ret,))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
# No matching function found, so render the computed values as a CSS
|
|
||||||
# function call. Slurpy arguments are expanded and named arguments are
|
|
||||||
# unsupported.
|
|
||||||
if kwargs:
|
|
||||||
raise TypeError("The CSS function %s doesn't support keyword arguments." % (func_name,))
|
|
||||||
|
|
||||||
# TODO another candidate for a "function call" sass type
|
|
||||||
rendered_args = [arg.render() for arg in args]
|
|
||||||
|
|
||||||
return String(
|
|
||||||
u"%s(%s)" % (func_name, u", ".join(rendered_args)),
|
|
||||||
quotes=None)
|
|
||||||
|
|
||||||
|
|
||||||
class Literal(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s)>' % (self.__class__.__name__, repr(self.value))
|
|
||||||
|
|
||||||
def __init__(self, value):
|
|
||||||
if isinstance(value, Undefined) and config.FATAL_UNDEFINED:
|
|
||||||
raise SyntaxError("Undefined literal.")
|
|
||||||
else:
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
class Variable(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s)>' % (self.__class__.__name__, repr(self.name))
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
try:
|
|
||||||
value = calculator.namespace.variable(self.name)
|
|
||||||
except KeyError:
|
|
||||||
if config.FATAL_UNDEFINED:
|
|
||||||
raise SyntaxError("Undefined variable: '%s'." % self.name)
|
|
||||||
else:
|
|
||||||
if config.VERBOSITY > 1:
|
|
||||||
log.error("Undefined variable '%s'", self.name, extra={'stack': True})
|
|
||||||
return Undefined()
|
|
||||||
else:
|
|
||||||
if isinstance(value, six.string_types):
|
|
||||||
evald = calculator.evaluate_expression(value)
|
|
||||||
if evald is not None:
|
|
||||||
return evald
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class ListLiteral(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s, comma=%s)>' % (self.__class__.__name__, repr(self.items), repr(self.comma))
|
|
||||||
|
|
||||||
def __init__(self, items, comma=True):
|
|
||||||
self.items = items
|
|
||||||
self.comma = comma
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
items = [item.evaluate(calculator, divide=divide) for item in self.items]
|
|
||||||
|
|
||||||
# Whether this is a "plain" literal matters for null removal: nulls are
|
|
||||||
# left alone if this is a completely vanilla CSS property
|
|
||||||
is_literal = True
|
|
||||||
if divide:
|
|
||||||
# TODO sort of overloading "divide" here... rename i think
|
|
||||||
is_literal = False
|
|
||||||
elif not all(isinstance(item, Literal) for item in self.items):
|
|
||||||
is_literal = False
|
|
||||||
|
|
||||||
return List(items, use_comma=self.comma, is_literal=is_literal)
|
|
||||||
|
|
||||||
|
|
||||||
class MapLiteral(Expression):
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s)>' % (self.__class__.__name__, repr(self.pairs))
|
|
||||||
|
|
||||||
def __init__(self, pairs):
|
|
||||||
self.pairs = tuple((var, value) for var, value in pairs if value is not None)
|
|
||||||
|
|
||||||
def evaluate(self, calculator, divide=False):
|
|
||||||
scss_pairs = []
|
|
||||||
for key, value in self.pairs:
|
|
||||||
scss_pairs.append((
|
|
||||||
key.evaluate(calculator),
|
|
||||||
value.evaluate(calculator),
|
|
||||||
))
|
|
||||||
|
|
||||||
return Map(scss_pairs)
|
|
||||||
|
|
||||||
|
|
||||||
class ArgspecLiteral(Expression):
|
|
||||||
"""Contains pairs of argument names and values, as parsed from a function
|
|
||||||
definition or function call.
|
|
||||||
|
|
||||||
Note that the semantics are somewhat ambiguous. Consider parsing:
|
|
||||||
|
|
||||||
$foo, $bar: 3
|
|
||||||
|
|
||||||
If this appeared in a function call, $foo would refer to a value; if it
|
|
||||||
appeared in a function definition, $foo would refer to an existing
|
|
||||||
variable. This it's up to the caller to use the right iteration function.
|
|
||||||
"""
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s(%s)>' % (self.__class__.__name__, repr(self.argpairs))
|
|
||||||
|
|
||||||
def __init__(self, argpairs, slurp=None):
|
|
||||||
# argpairs is a list of 2-tuples, parsed as though this were a function
|
|
||||||
# call, so (variable name as string or None, default value as AST
|
|
||||||
# node).
|
|
||||||
# slurp is the name of a variable to receive slurpy arguments.
|
|
||||||
self.argpairs = tuple(argpairs)
|
|
||||||
if slurp is all:
|
|
||||||
# DEVIATION: special syntax to allow injecting arbitrary arguments
|
|
||||||
# from the caller to the callee
|
|
||||||
self.inject = True
|
|
||||||
self.slurp = None
|
|
||||||
elif slurp:
|
|
||||||
self.inject = False
|
|
||||||
self.slurp = Variable(slurp)
|
|
||||||
else:
|
|
||||||
self.inject = False
|
|
||||||
self.slurp = None
|
|
||||||
|
|
||||||
def iter_list_argspec(self):
|
|
||||||
yield None, ListLiteral(zip(*self.argpairs)[1])
|
|
||||||
|
|
||||||
def iter_def_argspec(self):
|
|
||||||
"""Interpreting this literal as a function definition, yields pairs of
|
|
||||||
(variable name as a string, default value as an AST node or None).
|
|
||||||
"""
|
|
||||||
started_kwargs = False
|
|
||||||
seen_vars = set()
|
|
||||||
|
|
||||||
for var, value in self.argpairs:
|
|
||||||
if var is None:
|
|
||||||
# value is actually the name
|
|
||||||
var = value
|
|
||||||
value = None
|
|
||||||
|
|
||||||
if started_kwargs:
|
|
||||||
raise SyntaxError(
|
|
||||||
"Required argument %r must precede optional arguments"
|
|
||||||
% (var.name,))
|
|
||||||
|
|
||||||
else:
|
|
||||||
started_kwargs = True
|
|
||||||
|
|
||||||
if not isinstance(var, Variable):
|
|
||||||
raise SyntaxError("Expected variable name, got %r" % (var,))
|
|
||||||
|
|
||||||
if var.name in seen_vars:
|
|
||||||
raise SyntaxError("Duplicate argument %r" % (var.name,))
|
|
||||||
seen_vars.add(var.name)
|
|
||||||
|
|
||||||
yield var.name, value
|
|
||||||
|
|
||||||
def evaluate_call_args(self, calculator):
|
|
||||||
"""Interpreting this literal as a function call, return a 2-tuple of
|
|
||||||
``(args, kwargs)``.
|
|
||||||
"""
|
|
||||||
args = []
|
|
||||||
kwargs = {}
|
|
||||||
for var_node, value_node in self.argpairs:
|
|
||||||
value = value_node.evaluate(calculator, divide=True)
|
|
||||||
if var_node is None:
|
|
||||||
# Positional
|
|
||||||
args.append(value)
|
|
||||||
else:
|
|
||||||
# Named
|
|
||||||
if not isinstance(var_node, Variable):
|
|
||||||
raise SyntaxError("Expected variable name, got %r" % (var_node,))
|
|
||||||
kwargs[var_node.name] = value
|
|
||||||
|
|
||||||
# Slurpy arguments go on the end of the args
|
|
||||||
if self.slurp:
|
|
||||||
args.extend(self.slurp.evaluate(calculator, divide=True))
|
|
||||||
|
|
||||||
return args, kwargs
|
|
||||||
|
|
||||||
|
|
||||||
def parse_bareword(word):
|
|
||||||
if word in COLOR_NAMES:
|
|
||||||
return Color.from_name(word)
|
|
||||||
elif word == 'null':
|
|
||||||
return Null()
|
|
||||||
elif word == 'undefined':
|
|
||||||
return Undefined()
|
|
||||||
elif word == 'true':
|
|
||||||
return Boolean(True)
|
|
||||||
elif word == 'false':
|
|
||||||
return Boolean(False)
|
|
||||||
else:
|
|
||||||
return String(word, quotes=None)
|
|
||||||
|
|
||||||
|
|
||||||
class Parser(object):
|
|
||||||
def __init__(self, scanner):
|
|
||||||
self._scanner = scanner
|
|
||||||
self._pos = 0
|
|
||||||
self._char_pos = 0
|
|
||||||
|
|
||||||
def reset(self, input):
|
|
||||||
self._scanner.reset(input)
|
|
||||||
self._pos = 0
|
|
||||||
self._char_pos = 0
|
|
||||||
|
|
||||||
def _peek(self, types):
|
|
||||||
"""
|
|
||||||
Returns the token type for lookahead; if there are any args
|
|
||||||
then the list of args is the set of token types to allow
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
tok = self._scanner.token(self._pos, types)
|
|
||||||
return tok[2]
|
|
||||||
except SyntaxError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _scan(self, type):
|
|
||||||
"""
|
|
||||||
Returns the matched text, and moves to the next token
|
|
||||||
"""
|
|
||||||
tok = self._scanner.token(self._pos, set([type]))
|
|
||||||
self._char_pos = tok[0]
|
|
||||||
if tok[2] != type:
|
|
||||||
raise SyntaxError("SyntaxError[@ char %s: %s]" % (repr(tok[0]), "Trying to find " + type))
|
|
||||||
self._pos += 1
|
|
||||||
return tok[3]
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
## Grammar compiled using Yapps:
|
|
||||||
|
|
||||||
class SassExpressionScanner(Scanner):
|
|
||||||
patterns = None
|
|
||||||
_patterns = [
|
|
||||||
('":"', ':'),
|
|
||||||
('","', ','),
|
|
||||||
('[ \r\t\n]+', '[ \r\t\n]+'),
|
|
||||||
('LPAR', '\\(|\\['),
|
|
||||||
('RPAR', '\\)|\\]'),
|
|
||||||
('END', '$'),
|
|
||||||
('MUL', '[*]'),
|
|
||||||
('DIV', '/'),
|
|
||||||
('ADD', '[+]'),
|
|
||||||
('SUB', '-\\s'),
|
|
||||||
('SIGN', '-(?![a-zA-Z_])'),
|
|
||||||
('AND', '(?<![-\\w])and(?![-\\w])'),
|
|
||||||
('OR', '(?<![-\\w])or(?![-\\w])'),
|
|
||||||
('NOT', '(?<![-\\w])not(?![-\\w])'),
|
|
||||||
('NE', '!='),
|
|
||||||
('INV', '!'),
|
|
||||||
('EQ', '=='),
|
|
||||||
('LE', '<='),
|
|
||||||
('GE', '>='),
|
|
||||||
('LT', '<'),
|
|
||||||
('GT', '>'),
|
|
||||||
('DOTDOTDOT', '[.]{3}'),
|
|
||||||
('KWSTR', "'[^']*'(?=\\s*:)"),
|
|
||||||
('STR', "'[^']*'"),
|
|
||||||
('KWQSTR', '"[^"]*"(?=\\s*:)'),
|
|
||||||
('QSTR', '"[^"]*"'),
|
|
||||||
('UNITS', '(?<!\\s)(?:[a-zA-Z]+|%)(?![-\\w])'),
|
|
||||||
('KWNUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?=\\s*:)'),
|
|
||||||
('NUM', '(?:\\d+(?:\\.\\d*)?|\\.\\d+)'),
|
|
||||||
('KWCOLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])(?=\\s*:)'),
|
|
||||||
('COLOR', '#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])'),
|
|
||||||
('KWVAR', '\\$[-a-zA-Z0-9_]+(?=\\s*:)'),
|
|
||||||
('SLURPYVAR', '\\$[-a-zA-Z0-9_]+(?=[.][.][.])'),
|
|
||||||
('VAR', '\\$[-a-zA-Z0-9_]+'),
|
|
||||||
('FNCT', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\()'),
|
|
||||||
('KWID', '[-a-zA-Z_][-a-zA-Z0-9_]*(?=\\s*:)'),
|
|
||||||
('ID', '[-a-zA-Z_][-a-zA-Z0-9_]*'),
|
|
||||||
('BANG_IMPORTANT', '!important'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, input=None):
|
|
||||||
if hasattr(self, 'setup_patterns'):
|
|
||||||
self.setup_patterns(self._patterns)
|
|
||||||
elif self.patterns is None:
|
|
||||||
self.__class__.patterns = []
|
|
||||||
for t, p in self._patterns:
|
|
||||||
self.patterns.append((t, re.compile(p)))
|
|
||||||
super(SassExpressionScanner, self).__init__(None, ['[ \r\t\n]+'], input)
|
|
||||||
|
|
||||||
|
|
||||||
class SassExpression(Parser):
|
|
||||||
def goal(self):
|
|
||||||
expr_lst = self.expr_lst()
|
|
||||||
END = self._scan('END')
|
|
||||||
return expr_lst
|
|
||||||
|
|
||||||
def goal_argspec(self):
|
|
||||||
argspec = self.argspec()
|
|
||||||
END = self._scan('END')
|
|
||||||
return argspec
|
|
||||||
|
|
||||||
def argspec(self):
|
|
||||||
_token_ = self._peek(self.argspec_rsts)
|
|
||||||
if _token_ not in self.argspec_chks:
|
|
||||||
if self._peek(self.argspec_rsts_) not in self.argspec_chks_:
|
|
||||||
argspec_items = self.argspec_items()
|
|
||||||
args, slurpy = argspec_items
|
|
||||||
return ArgspecLiteral(args, slurp=slurpy)
|
|
||||||
return ArgspecLiteral([])
|
|
||||||
elif _token_ == 'SLURPYVAR':
|
|
||||||
SLURPYVAR = self._scan('SLURPYVAR')
|
|
||||||
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
||||||
return ArgspecLiteral([], slurp=SLURPYVAR)
|
|
||||||
else: # == 'DOTDOTDOT'
|
|
||||||
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
||||||
return ArgspecLiteral([], slurp=all)
|
|
||||||
|
|
||||||
def argspec_items(self):
|
|
||||||
slurpy = None
|
|
||||||
argspec_item = self.argspec_item()
|
|
||||||
args = [argspec_item]
|
|
||||||
if self._peek(self.argspec_items_rsts) == '","':
|
|
||||||
self._scan('","')
|
|
||||||
if self._peek(self.argspec_items_rsts_) not in self.argspec_chks_:
|
|
||||||
_token_ = self._peek(self.argspec_items_rsts__)
|
|
||||||
if _token_ == 'SLURPYVAR':
|
|
||||||
SLURPYVAR = self._scan('SLURPYVAR')
|
|
||||||
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
||||||
slurpy = SLURPYVAR
|
|
||||||
elif _token_ == 'DOTDOTDOT':
|
|
||||||
DOTDOTDOT = self._scan('DOTDOTDOT')
|
|
||||||
slurpy = all
|
|
||||||
else: # in self.argspec_items_chks
|
|
||||||
argspec_items = self.argspec_items()
|
|
||||||
more_args, slurpy = argspec_items
|
|
||||||
args.extend(more_args)
|
|
||||||
return args, slurpy
|
|
||||||
|
|
||||||
def argspec_item(self):
|
|
||||||
_token_ = self._peek(self.argspec_items_chks)
|
|
||||||
if _token_ == 'KWVAR':
|
|
||||||
KWVAR = self._scan('KWVAR')
|
|
||||||
self._scan('":"')
|
|
||||||
expr_slst = self.expr_slst()
|
|
||||||
return (Variable(KWVAR), expr_slst)
|
|
||||||
else: # in self.argspec_item_chks
|
|
||||||
expr_slst = self.expr_slst()
|
|
||||||
return (None, expr_slst)
|
|
||||||
|
|
||||||
def expr_map(self):
|
|
||||||
map_item = self.map_item()
|
|
||||||
pairs = [map_item]
|
|
||||||
while self._peek(self.expr_map_rsts) == '","':
|
|
||||||
self._scan('","')
|
|
||||||
map_item = (None, None)
|
|
||||||
if self._peek(self.expr_map_rsts_) not in self.expr_map_rsts:
|
|
||||||
map_item = self.map_item()
|
|
||||||
pairs.append(map_item)
|
|
||||||
return MapLiteral(pairs)
|
|
||||||
|
|
||||||
def map_item(self):
|
|
||||||
kwatom = self.kwatom()
|
|
||||||
self._scan('":"')
|
|
||||||
expr_slst = self.expr_slst()
|
|
||||||
return (kwatom, expr_slst)
|
|
||||||
|
|
||||||
def expr_lst(self):
|
|
||||||
expr_slst = self.expr_slst()
|
|
||||||
v = [expr_slst]
|
|
||||||
while self._peek(self.argspec_items_rsts) == '","':
|
|
||||||
self._scan('","')
|
|
||||||
expr_slst = self.expr_slst()
|
|
||||||
v.append(expr_slst)
|
|
||||||
return ListLiteral(v) if len(v) > 1 else v[0]
|
|
||||||
|
|
||||||
def expr_slst(self):
|
|
||||||
or_expr = self.or_expr()
|
|
||||||
v = [or_expr]
|
|
||||||
while self._peek(self.expr_slst_rsts) not in self.argspec_items_rsts:
|
|
||||||
or_expr = self.or_expr()
|
|
||||||
v.append(or_expr)
|
|
||||||
return ListLiteral(v, comma=False) if len(v) > 1 else v[0]
|
|
||||||
|
|
||||||
def or_expr(self):
|
|
||||||
and_expr = self.and_expr()
|
|
||||||
v = and_expr
|
|
||||||
while self._peek(self.or_expr_rsts) == 'OR':
|
|
||||||
OR = self._scan('OR')
|
|
||||||
and_expr = self.and_expr()
|
|
||||||
v = AnyOp(v, and_expr)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def and_expr(self):
|
|
||||||
not_expr = self.not_expr()
|
|
||||||
v = not_expr
|
|
||||||
while self._peek(self.and_expr_rsts) == 'AND':
|
|
||||||
AND = self._scan('AND')
|
|
||||||
not_expr = self.not_expr()
|
|
||||||
v = AllOp(v, not_expr)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def not_expr(self):
|
|
||||||
_token_ = self._peek(self.argspec_item_chks)
|
|
||||||
if _token_ != 'NOT':
|
|
||||||
comparison = self.comparison()
|
|
||||||
return comparison
|
|
||||||
else: # == 'NOT'
|
|
||||||
NOT = self._scan('NOT')
|
|
||||||
not_expr = self.not_expr()
|
|
||||||
return NotOp(not_expr)
|
|
||||||
|
|
||||||
def comparison(self):
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = a_expr
|
|
||||||
while self._peek(self.comparison_rsts) in self.comparison_chks:
|
|
||||||
_token_ = self._peek(self.comparison_chks)
|
|
||||||
if _token_ == 'LT':
|
|
||||||
LT = self._scan('LT')
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = BinaryOp(operator.lt, v, a_expr)
|
|
||||||
elif _token_ == 'GT':
|
|
||||||
GT = self._scan('GT')
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = BinaryOp(operator.gt, v, a_expr)
|
|
||||||
elif _token_ == 'LE':
|
|
||||||
LE = self._scan('LE')
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = BinaryOp(operator.le, v, a_expr)
|
|
||||||
elif _token_ == 'GE':
|
|
||||||
GE = self._scan('GE')
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = BinaryOp(operator.ge, v, a_expr)
|
|
||||||
elif _token_ == 'EQ':
|
|
||||||
EQ = self._scan('EQ')
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = BinaryOp(operator.eq, v, a_expr)
|
|
||||||
else: # == 'NE'
|
|
||||||
NE = self._scan('NE')
|
|
||||||
a_expr = self.a_expr()
|
|
||||||
v = BinaryOp(operator.ne, v, a_expr)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def a_expr(self):
|
|
||||||
m_expr = self.m_expr()
|
|
||||||
v = m_expr
|
|
||||||
while self._peek(self.a_expr_rsts) in self.a_expr_chks:
|
|
||||||
_token_ = self._peek(self.a_expr_chks)
|
|
||||||
if _token_ == 'ADD':
|
|
||||||
ADD = self._scan('ADD')
|
|
||||||
m_expr = self.m_expr()
|
|
||||||
v = BinaryOp(operator.add, v, m_expr)
|
|
||||||
else: # == 'SUB'
|
|
||||||
SUB = self._scan('SUB')
|
|
||||||
m_expr = self.m_expr()
|
|
||||||
v = BinaryOp(operator.sub, v, m_expr)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def m_expr(self):
|
|
||||||
u_expr = self.u_expr()
|
|
||||||
v = u_expr
|
|
||||||
while self._peek(self.m_expr_rsts) in self.m_expr_chks:
|
|
||||||
_token_ = self._peek(self.m_expr_chks)
|
|
||||||
if _token_ == 'MUL':
|
|
||||||
MUL = self._scan('MUL')
|
|
||||||
u_expr = self.u_expr()
|
|
||||||
v = BinaryOp(operator.mul, v, u_expr)
|
|
||||||
else: # == 'DIV'
|
|
||||||
DIV = self._scan('DIV')
|
|
||||||
u_expr = self.u_expr()
|
|
||||||
v = BinaryOp(operator.truediv, v, u_expr)
|
|
||||||
return v
|
|
||||||
|
|
||||||
def u_expr(self):
|
|
||||||
_token_ = self._peek(self.u_expr_rsts)
|
|
||||||
if _token_ == 'SIGN':
|
|
||||||
SIGN = self._scan('SIGN')
|
|
||||||
u_expr = self.u_expr()
|
|
||||||
return UnaryOp(operator.neg, u_expr)
|
|
||||||
elif _token_ == 'ADD':
|
|
||||||
ADD = self._scan('ADD')
|
|
||||||
u_expr = self.u_expr()
|
|
||||||
return UnaryOp(operator.pos, u_expr)
|
|
||||||
else: # in self.u_expr_chks
|
|
||||||
atom = self.atom()
|
|
||||||
return atom
|
|
||||||
|
|
||||||
def atom(self):
|
|
||||||
_token_ = self._peek(self.u_expr_chks)
|
|
||||||
if _token_ == 'LPAR':
|
|
||||||
LPAR = self._scan('LPAR')
|
|
||||||
_token_ = self._peek(self.atom_rsts)
|
|
||||||
if _token_ not in self.argspec_item_chks:
|
|
||||||
expr_map = self.expr_map()
|
|
||||||
v = expr_map
|
|
||||||
else: # in self.argspec_item_chks
|
|
||||||
expr_lst = self.expr_lst()
|
|
||||||
v = expr_lst
|
|
||||||
RPAR = self._scan('RPAR')
|
|
||||||
return Parentheses(v)
|
|
||||||
elif _token_ == 'FNCT':
|
|
||||||
FNCT = self._scan('FNCT')
|
|
||||||
LPAR = self._scan('LPAR')
|
|
||||||
argspec = self.argspec()
|
|
||||||
RPAR = self._scan('RPAR')
|
|
||||||
return CallOp(FNCT, argspec)
|
|
||||||
elif _token_ == 'BANG_IMPORTANT':
|
|
||||||
BANG_IMPORTANT = self._scan('BANG_IMPORTANT')
|
|
||||||
return Literal(String(BANG_IMPORTANT, quotes=None))
|
|
||||||
elif _token_ == 'ID':
|
|
||||||
ID = self._scan('ID')
|
|
||||||
return Literal(parse_bareword(ID))
|
|
||||||
elif _token_ == 'NUM':
|
|
||||||
NUM = self._scan('NUM')
|
|
||||||
UNITS = None
|
|
||||||
if self._peek(self.atom_rsts_) == 'UNITS':
|
|
||||||
UNITS = self._scan('UNITS')
|
|
||||||
return Literal(Number(float(NUM), unit=UNITS))
|
|
||||||
elif _token_ == 'STR':
|
|
||||||
STR = self._scan('STR')
|
|
||||||
return Literal(String(STR[1:-1], quotes="'"))
|
|
||||||
elif _token_ == 'QSTR':
|
|
||||||
QSTR = self._scan('QSTR')
|
|
||||||
return Literal(String(QSTR[1:-1], quotes='"'))
|
|
||||||
elif _token_ == 'COLOR':
|
|
||||||
COLOR = self._scan('COLOR')
|
|
||||||
return Literal(Color.from_hex(COLOR, literal=True))
|
|
||||||
else: # == 'VAR'
|
|
||||||
VAR = self._scan('VAR')
|
|
||||||
return Variable(VAR)
|
|
||||||
|
|
||||||
def kwatom(self):
|
|
||||||
_token_ = self._peek(self.kwatom_rsts)
|
|
||||||
if _token_ == '":"':
|
|
||||||
pass
|
|
||||||
elif _token_ == 'KWID':
|
|
||||||
KWID = self._scan('KWID')
|
|
||||||
return Literal(parse_bareword(KWID))
|
|
||||||
elif _token_ == 'KWNUM':
|
|
||||||
KWNUM = self._scan('KWNUM')
|
|
||||||
UNITS = None
|
|
||||||
if self._peek(self.kwatom_rsts_) == 'UNITS':
|
|
||||||
UNITS = self._scan('UNITS')
|
|
||||||
return Literal(Number(float(KWNUM), unit=UNITS))
|
|
||||||
elif _token_ == 'KWSTR':
|
|
||||||
KWSTR = self._scan('KWSTR')
|
|
||||||
return Literal(String(KWSTR[1:-1], quotes="'"))
|
|
||||||
elif _token_ == 'KWQSTR':
|
|
||||||
KWQSTR = self._scan('KWQSTR')
|
|
||||||
return Literal(String(KWQSTR[1:-1], quotes='"'))
|
|
||||||
elif _token_ == 'KWCOLOR':
|
|
||||||
KWCOLOR = self._scan('KWCOLOR')
|
|
||||||
return Literal(Color.from_hex(COLOR, literal=True))
|
|
||||||
else: # == 'KWVAR'
|
|
||||||
KWVAR = self._scan('KWVAR')
|
|
||||||
return Variable(KWVAR)
|
|
||||||
|
|
||||||
u_expr_chks = set(['LPAR', 'COLOR', 'QSTR', 'NUM', 'FNCT', 'STR', 'VAR', 'BANG_IMPORTANT', 'ID'])
|
|
||||||
m_expr_rsts = set(['LPAR', 'SUB', 'QSTR', 'RPAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'STR', 'VAR', 'EQ', 'ID', 'AND', 'ADD', 'NOT', 'OR', '","'])
|
|
||||||
argspec_items_rsts = set(['RPAR', 'END', '","'])
|
|
||||||
expr_map_rsts = set(['RPAR', '","'])
|
|
||||||
argspec_items_rsts__ = set(['KWVAR', 'LPAR', 'QSTR', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
||||||
kwatom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'KWQSTR', 'KWCOLOR', '":"', 'KWNUM'])
|
|
||||||
argspec_item_chks = set(['LPAR', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
||||||
a_expr_chks = set(['ADD', 'SUB'])
|
|
||||||
expr_slst_rsts = set(['LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID', '","'])
|
|
||||||
or_expr_rsts = set(['LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'ID', 'BANG_IMPORTANT', 'OR', '","'])
|
|
||||||
and_expr_rsts = set(['AND', 'LPAR', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'ID', 'BANG_IMPORTANT', 'OR', '","'])
|
|
||||||
comparison_rsts = set(['LPAR', 'QSTR', 'RPAR', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'ADD', 'FNCT', 'STR', 'VAR', 'EQ', 'ID', 'AND', 'GE', 'NOT', 'OR', '","'])
|
|
||||||
argspec_chks = set(['DOTDOTDOT', 'SLURPYVAR'])
|
|
||||||
atom_rsts_ = set(['LPAR', 'SUB', 'QSTR', 'RPAR', 'VAR', 'MUL', 'DIV', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'STR', 'UNITS', 'EQ', 'ID', 'AND', 'ADD', 'NOT', 'OR', '","'])
|
|
||||||
expr_map_rsts_ = set(['KWVAR', 'KWID', 'KWSTR', 'KWQSTR', 'RPAR', 'KWCOLOR', '":"', 'KWNUM', '","'])
|
|
||||||
u_expr_rsts = set(['LPAR', 'COLOR', 'QSTR', 'SIGN', 'ADD', 'NUM', 'FNCT', 'STR', 'VAR', 'BANG_IMPORTANT', 'ID'])
|
|
||||||
comparison_chks = set(['GT', 'GE', 'NE', 'LT', 'LE', 'EQ'])
|
|
||||||
argspec_items_rsts_ = set(['KWVAR', 'LPAR', 'QSTR', 'END', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'SIGN', 'VAR', 'ADD', 'NUM', 'RPAR', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
||||||
a_expr_rsts = set(['LPAR', 'SUB', 'QSTR', 'RPAR', 'BANG_IMPORTANT', 'LE', 'COLOR', 'NE', 'LT', 'NUM', 'GT', 'END', 'SIGN', 'GE', 'FNCT', 'STR', 'VAR', 'EQ', 'ID', 'AND', 'ADD', 'NOT', 'OR', '","'])
|
|
||||||
m_expr_chks = set(['MUL', 'DIV'])
|
|
||||||
kwatom_rsts_ = set(['UNITS', '":"'])
|
|
||||||
argspec_items_chks = set(['KWVAR', 'LPAR', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'BANG_IMPORTANT', 'ID'])
|
|
||||||
argspec_rsts = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'SLURPYVAR', 'COLOR', 'DOTDOTDOT', 'RPAR', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'QSTR', 'SIGN', 'ID'])
|
|
||||||
atom_rsts = set(['KWVAR', 'KWID', 'KWSTR', 'BANG_IMPORTANT', 'LPAR', 'COLOR', 'KWQSTR', 'SIGN', 'KWCOLOR', 'VAR', 'ADD', 'NUM', '":"', 'STR', 'NOT', 'QSTR', 'KWNUM', 'ID', 'FNCT'])
|
|
||||||
argspec_chks_ = set(['END', 'RPAR'])
|
|
||||||
argspec_rsts_ = set(['KWVAR', 'LPAR', 'BANG_IMPORTANT', 'END', 'COLOR', 'QSTR', 'SIGN', 'VAR', 'ADD', 'NUM', 'FNCT', 'STR', 'NOT', 'RPAR', 'ID'])
|
|
||||||
|
|
||||||
|
|
||||||
### Grammar ends.
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
__all__ = ('Calculator',)
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
|
|
||||||
from scss.functions.core import CORE_LIBRARY
|
|
||||||
from scss.functions.extra import EXTRA_LIBRARY
|
|
||||||
from scss.functions.compass.sprites import COMPASS_SPRITES_LIBRARY
|
|
||||||
from scss.functions.compass.gradients import COMPASS_GRADIENTS_LIBRARY
|
|
||||||
from scss.functions.compass.helpers import COMPASS_HELPERS_LIBRARY
|
|
||||||
from scss.functions.compass.images import COMPASS_IMAGES_LIBRARY
|
|
||||||
|
|
||||||
|
|
||||||
ALL_BUILTINS_LIBRARY = FunctionLibrary()
|
|
||||||
ALL_BUILTINS_LIBRARY.inherit(
|
|
||||||
CORE_LIBRARY,
|
|
||||||
EXTRA_LIBRARY,
|
|
||||||
COMPASS_GRADIENTS_LIBRARY,
|
|
||||||
COMPASS_HELPERS_LIBRARY,
|
|
||||||
COMPASS_IMAGES_LIBRARY,
|
|
||||||
COMPASS_SPRITES_LIBRARY,
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO back-compat for the only codebase still using the old name :)
|
|
||||||
FunctionRegistry = FunctionLibrary
|
|
||||||
scss_builtins = ALL_BUILTINS_LIBRARY
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Global cache of image sizes, shared between sprites and images libraries.
|
|
||||||
_image_size_cache = {}
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
"""Utilities for working with gradients. Inspired by Compass, but not quite
|
|
||||||
the same.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
from scss.functions.compass.helpers import opposite_position, position
|
|
||||||
from scss.types import Color, List, Number, String
|
|
||||||
from scss.util import escape, split_params, to_float, to_str
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
COMPASS_GRADIENTS_LIBRARY = FunctionLibrary()
|
|
||||||
register = COMPASS_GRADIENTS_LIBRARY.register
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def __color_stops(percentages, *args):
|
|
||||||
if len(args) == 1:
|
|
||||||
if isinstance(args[0], (list, tuple, List)):
|
|
||||||
list(args[0])
|
|
||||||
elif isinstance(args[0], (String, six.string_types)):
|
|
||||||
color_stops = []
|
|
||||||
colors = split_params(getattr(args[0], 'value', args[0]))
|
|
||||||
for color in colors:
|
|
||||||
color = color.strip()
|
|
||||||
if color.startswith('color-stop('):
|
|
||||||
s, c = split_params(color[11:].rstrip(')'))
|
|
||||||
s = s.strip()
|
|
||||||
c = c.strip()
|
|
||||||
else:
|
|
||||||
c, s = color.split()
|
|
||||||
color_stops.append((to_float(s), c))
|
|
||||||
return color_stops
|
|
||||||
|
|
||||||
colors = []
|
|
||||||
stops = []
|
|
||||||
prev_color = False
|
|
||||||
for c in args:
|
|
||||||
for c in List.from_maybe(c):
|
|
||||||
if isinstance(c, Color):
|
|
||||||
if prev_color:
|
|
||||||
stops.append(None)
|
|
||||||
colors.append(c)
|
|
||||||
prev_color = True
|
|
||||||
elif isinstance(c, Number):
|
|
||||||
stops.append(c)
|
|
||||||
prev_color = False
|
|
||||||
|
|
||||||
if prev_color:
|
|
||||||
stops.append(None)
|
|
||||||
stops = stops[:len(colors)]
|
|
||||||
if stops[0] is None:
|
|
||||||
stops[0] = Number(0, '%')
|
|
||||||
if stops[-1] is None:
|
|
||||||
stops[-1] = Number(100, '%')
|
|
||||||
|
|
||||||
maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')]
|
|
||||||
if maxable_stops:
|
|
||||||
max_stops = max(maxable_stops)
|
|
||||||
else:
|
|
||||||
max_stops = None
|
|
||||||
|
|
||||||
stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops]
|
|
||||||
|
|
||||||
init = 0
|
|
||||||
start = None
|
|
||||||
for i, s in enumerate(stops + [1.0]):
|
|
||||||
if s is None:
|
|
||||||
if start is None:
|
|
||||||
start = i
|
|
||||||
end = i
|
|
||||||
else:
|
|
||||||
final = s
|
|
||||||
if start is not None:
|
|
||||||
stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0))
|
|
||||||
for j in range(start, end + 1):
|
|
||||||
stops[j] = init + stride * Number(j - start + 1)
|
|
||||||
init = final
|
|
||||||
start = None
|
|
||||||
|
|
||||||
if not max_stops or percentages:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops]
|
|
||||||
|
|
||||||
return list(zip(stops, colors))
|
|
||||||
|
|
||||||
|
|
||||||
def _render_standard_color_stops(color_stops):
|
|
||||||
pairs = []
|
|
||||||
for i, (stop, color) in enumerate(color_stops):
|
|
||||||
if ((i == 0 and stop == Number(0, '%')) or
|
|
||||||
(i == len(color_stops) - 1 and stop == Number(100, '%'))):
|
|
||||||
pairs.append(color)
|
|
||||||
else:
|
|
||||||
pairs.append(List([color, stop], use_comma=False))
|
|
||||||
|
|
||||||
return List(pairs, use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('grad-color-stops')
|
|
||||||
def grad_color_stops(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
color_stops = __color_stops(True, *args)
|
|
||||||
ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops])
|
|
||||||
return String.unquoted(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def __grad_end_position(radial, color_stops):
|
|
||||||
return __grad_position(-1, 100, radial, color_stops)
|
|
||||||
|
|
||||||
|
|
||||||
@register('grad-point')
|
|
||||||
def grad_point(*p):
|
|
||||||
pos = set()
|
|
||||||
hrz = vrt = Number(0.5, '%')
|
|
||||||
for _p in p:
|
|
||||||
pos.update(String.unquoted(_p).value.split())
|
|
||||||
if 'left' in pos:
|
|
||||||
hrz = Number(0, '%')
|
|
||||||
elif 'right' in pos:
|
|
||||||
hrz = Number(1, '%')
|
|
||||||
if 'top' in pos:
|
|
||||||
vrt = Number(0, '%')
|
|
||||||
elif 'bottom' in pos:
|
|
||||||
vrt = Number(1, '%')
|
|
||||||
return List([v for v in (hrz, vrt) if v is not None])
|
|
||||||
|
|
||||||
|
|
||||||
def __grad_position(index, default, radial, color_stops):
|
|
||||||
try:
|
|
||||||
stops = Number(color_stops[index][0])
|
|
||||||
if radial and not stops.is_simple_unit('px') and (index == 0 or index == -1 or index == len(color_stops) - 1):
|
|
||||||
log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops)
|
|
||||||
except IndexError:
|
|
||||||
stops = Number(default)
|
|
||||||
return stops
|
|
||||||
|
|
||||||
|
|
||||||
@register('grad-end-position')
|
|
||||||
def grad_end_position(*color_stops):
|
|
||||||
color_stops = __color_stops(False, *color_stops)
|
|
||||||
return Number(__grad_end_position(False, color_stops))
|
|
||||||
|
|
||||||
|
|
||||||
@register('color-stops')
|
|
||||||
def color_stops(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
color_stops = __color_stops(False, *args)
|
|
||||||
ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops])
|
|
||||||
return String.unquoted(ret)
|
|
||||||
|
|
||||||
|
|
||||||
@register('color-stops-in-percentages')
|
|
||||||
def color_stops_in_percentages(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
color_stops = __color_stops(True, *args)
|
|
||||||
ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops])
|
|
||||||
return String.unquoted(ret)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_gradient_position_and_angle(args):
|
|
||||||
for arg in args:
|
|
||||||
ret = None
|
|
||||||
skip = False
|
|
||||||
for a in arg:
|
|
||||||
if isinstance(a, Color):
|
|
||||||
skip = True
|
|
||||||
break
|
|
||||||
elif isinstance(a, Number):
|
|
||||||
ret = arg
|
|
||||||
if skip:
|
|
||||||
continue
|
|
||||||
if ret is not None:
|
|
||||||
return ret
|
|
||||||
for seek in (
|
|
||||||
'center',
|
|
||||||
'top', 'bottom',
|
|
||||||
'left', 'right',
|
|
||||||
):
|
|
||||||
if String(seek) in arg:
|
|
||||||
return arg
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_gradient_shape_and_size(args):
|
|
||||||
for arg in args:
|
|
||||||
for seek in (
|
|
||||||
'circle', 'ellipse',
|
|
||||||
'closest-side', 'closest-corner',
|
|
||||||
'farthest-side', 'farthest-corner',
|
|
||||||
'contain', 'cover',
|
|
||||||
):
|
|
||||||
if String(seek) in arg:
|
|
||||||
return arg
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _get_gradient_color_stops(args):
|
|
||||||
color_stops = []
|
|
||||||
for arg in args:
|
|
||||||
for a in List.from_maybe(arg):
|
|
||||||
if isinstance(a, Color):
|
|
||||||
color_stops.append(arg)
|
|
||||||
break
|
|
||||||
return color_stops or None
|
|
||||||
|
|
||||||
|
|
||||||
@register('radial-gradient')
|
|
||||||
def radial_gradient(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
|
|
||||||
position_and_angle = _get_gradient_position_and_angle(args)
|
|
||||||
shape_and_size = _get_gradient_shape_and_size(args)
|
|
||||||
color_stops = _get_gradient_color_stops(args)
|
|
||||||
if color_stops is None:
|
|
||||||
raise Exception('No color stops provided to radial-gradient function')
|
|
||||||
color_stops = __color_stops(False, *color_stops)
|
|
||||||
|
|
||||||
args = [
|
|
||||||
position(position_and_angle) if position_and_angle is not None else None,
|
|
||||||
shape_and_size if shape_and_size is not None else None,
|
|
||||||
]
|
|
||||||
args.extend(_render_standard_color_stops(color_stops))
|
|
||||||
|
|
||||||
to__s = 'radial-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
|
|
||||||
ret = String.unquoted(to__s)
|
|
||||||
|
|
||||||
def to__css2():
|
|
||||||
return String.unquoted('')
|
|
||||||
ret.to__css2 = to__css2
|
|
||||||
|
|
||||||
def to__moz():
|
|
||||||
return String.unquoted('-moz-' + to__s)
|
|
||||||
ret.to__moz = to__moz
|
|
||||||
|
|
||||||
def to__pie():
|
|
||||||
log.warn("PIE does not support radial-gradient.")
|
|
||||||
return String.unquoted('-pie-radial-gradient(unsupported)')
|
|
||||||
ret.to__pie = to__pie
|
|
||||||
|
|
||||||
def to__webkit():
|
|
||||||
return String.unquoted('-webkit-' + to__s)
|
|
||||||
ret.to__webkit = to__webkit
|
|
||||||
|
|
||||||
def to__owg():
|
|
||||||
args = [
|
|
||||||
'radial',
|
|
||||||
grad_point(position_and_angle) if position_and_angle is not None else 'center',
|
|
||||||
'0',
|
|
||||||
grad_point(position_and_angle) if position_and_angle is not None else 'center',
|
|
||||||
__grad_end_position(True, color_stops),
|
|
||||||
]
|
|
||||||
args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops)
|
|
||||||
ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
|
|
||||||
return String.unquoted(ret)
|
|
||||||
ret.to__owg = to__owg
|
|
||||||
|
|
||||||
def to__svg():
|
|
||||||
return radial_svg_gradient(color_stops, position_and_angle or 'center')
|
|
||||||
ret.to__svg = to__svg
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@register('linear-gradient')
|
|
||||||
def linear_gradient(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
|
|
||||||
position_and_angle = _get_gradient_position_and_angle(args)
|
|
||||||
color_stops = _get_gradient_color_stops(args)
|
|
||||||
if color_stops is None:
|
|
||||||
raise Exception('No color stops provided to linear-gradient function')
|
|
||||||
color_stops = __color_stops(False, *color_stops)
|
|
||||||
|
|
||||||
args = [
|
|
||||||
position(position_and_angle) if position_and_angle is not None else None,
|
|
||||||
]
|
|
||||||
args.extend(_render_standard_color_stops(color_stops))
|
|
||||||
|
|
||||||
to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
|
|
||||||
ret = String.unquoted(to__s)
|
|
||||||
|
|
||||||
def to__css2():
|
|
||||||
return String.unquoted('')
|
|
||||||
ret.to__css2 = to__css2
|
|
||||||
|
|
||||||
def to__moz():
|
|
||||||
return String.unquoted('-moz-' + to__s)
|
|
||||||
ret.to__moz = to__moz
|
|
||||||
|
|
||||||
def to__pie():
|
|
||||||
return String.unquoted('-pie-' + to__s)
|
|
||||||
ret.to__pie = to__pie
|
|
||||||
|
|
||||||
def to__ms():
|
|
||||||
return String.unquoted('-ms-' + to__s)
|
|
||||||
ret.to__ms = to__ms
|
|
||||||
|
|
||||||
def to__o():
|
|
||||||
return String.unquoted('-o-' + to__s)
|
|
||||||
ret.to__o = to__o
|
|
||||||
|
|
||||||
def to__webkit():
|
|
||||||
return String.unquoted('-webkit-' + to__s)
|
|
||||||
ret.to__webkit = to__webkit
|
|
||||||
|
|
||||||
def to__owg():
|
|
||||||
args = [
|
|
||||||
'linear',
|
|
||||||
position(position_and_angle or None),
|
|
||||||
opposite_position(position_and_angle or None),
|
|
||||||
]
|
|
||||||
args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops)
|
|
||||||
ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')'
|
|
||||||
return String.unquoted(ret)
|
|
||||||
ret.to__owg = to__owg
|
|
||||||
|
|
||||||
def to__svg():
|
|
||||||
return linear_svg_gradient(color_stops, position_and_angle or 'top')
|
|
||||||
ret.to__svg = to__svg
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@register('radial-svg-gradient')
|
|
||||||
def radial_svg_gradient(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
color_stops = args
|
|
||||||
center = None
|
|
||||||
if isinstance(args[-1], (String, Number, six.string_types)):
|
|
||||||
center = args[-1]
|
|
||||||
color_stops = args[:-1]
|
|
||||||
color_stops = __color_stops(False, *color_stops)
|
|
||||||
cx, cy = zip(*grad_point(center).items())[1]
|
|
||||||
r = __grad_end_position(True, color_stops)
|
|
||||||
svg = __radial_svg(color_stops, cx, cy, r)
|
|
||||||
url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
|
|
||||||
inline = 'url("%s")' % escape(url)
|
|
||||||
return String.unquoted(inline)
|
|
||||||
|
|
||||||
|
|
||||||
@register('linear-svg-gradient')
|
|
||||||
def linear_svg_gradient(*args):
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
color_stops = args
|
|
||||||
start = None
|
|
||||||
if isinstance(args[-1], (String, Number, six.string_types)):
|
|
||||||
start = args[-1]
|
|
||||||
color_stops = args[:-1]
|
|
||||||
color_stops = __color_stops(False, *color_stops)
|
|
||||||
x1, y1 = zip(*grad_point(start).items())[1]
|
|
||||||
x2, y2 = zip(*grad_point(opposite_position(start)).items())[1]
|
|
||||||
svg = _linear_svg(color_stops, x1, y1, x2, y2)
|
|
||||||
url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
|
|
||||||
inline = 'url("%s")' % escape(url)
|
|
||||||
return String.unquoted(inline)
|
|
||||||
|
|
||||||
|
|
||||||
def __color_stops_svg(color_stops):
|
|
||||||
ret = ''.join('<stop offset="%s" stop-color="%s"/>' % (to_str(s), c) for s, c in color_stops)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def __svg_template(gradient):
|
|
||||||
ret = '<?xml version="1.0" encoding="utf-8"?>\
|
|
||||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">\
|
|
||||||
<defs>%s</defs>\
|
|
||||||
<rect x="0" y="0" width="100%%" height="100%%" fill="url(#grad)" />\
|
|
||||||
</svg>' % gradient
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def _linear_svg(color_stops, x1, y1, x2, y2):
|
|
||||||
gradient = '<linearGradient id="grad" x1="%s" y1="%s" x2="%s" y2="%s">%s</linearGradient>' % (
|
|
||||||
to_str(Number(x1)),
|
|
||||||
to_str(Number(y1)),
|
|
||||||
to_str(Number(x2)),
|
|
||||||
to_str(Number(y2)),
|
|
||||||
__color_stops_svg(color_stops)
|
|
||||||
)
|
|
||||||
return __svg_template(gradient)
|
|
||||||
|
|
||||||
|
|
||||||
def __radial_svg(color_stops, cx, cy, r):
|
|
||||||
gradient = '<radialGradient id="grad" gradientUnits="userSpaceOnUse" cx="%s" cy="%s" r="%s">%s</radialGradient>' % (
|
|
||||||
to_str(Number(cx)),
|
|
||||||
to_str(Number(cy)),
|
|
||||||
to_str(Number(r)),
|
|
||||||
__color_stops_svg(color_stops)
|
|
||||||
)
|
|
||||||
return __svg_template(gradient)
|
|
||||||
@@ -1,654 +0,0 @@
|
|||||||
"""Miscellaneous helper functions ported from Compass.
|
|
||||||
|
|
||||||
See: http://compass-style.org/reference/compass/helpers/
|
|
||||||
|
|
||||||
This collection is not necessarily complete or up-to-date.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
import os.path
|
|
||||||
import time
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from scss import config
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
from scss.types import Boolean, List, Null, Number, String
|
|
||||||
from scss.util import escape, to_str
|
|
||||||
import re
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
COMPASS_HELPERS_LIBRARY = FunctionLibrary()
|
|
||||||
register = COMPASS_HELPERS_LIBRARY.register
|
|
||||||
|
|
||||||
FONT_TYPES = {
|
|
||||||
'woff': 'woff',
|
|
||||||
'otf': 'opentype',
|
|
||||||
'opentype': 'opentype',
|
|
||||||
'ttf': 'truetype',
|
|
||||||
'truetype': 'truetype',
|
|
||||||
'svg': 'svg',
|
|
||||||
'eot': 'embedded-opentype'
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def add_cache_buster(url, mtime):
|
|
||||||
fragment = url.split('#')
|
|
||||||
query = fragment[0].split('?')
|
|
||||||
if len(query) > 1 and query[1] != '':
|
|
||||||
cb = '&_=%s' % (mtime)
|
|
||||||
url = '?'.join(query) + cb
|
|
||||||
else:
|
|
||||||
cb = '?_=%s' % (mtime)
|
|
||||||
url = query[0] + cb
|
|
||||||
if len(fragment) > 1:
|
|
||||||
url += '#' + fragment[1]
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Data manipulation
|
|
||||||
|
|
||||||
@register('blank')
|
|
||||||
def blank(*objs):
|
|
||||||
"""Returns true when the object is false, an empty string, or an empty list"""
|
|
||||||
for o in objs:
|
|
||||||
if isinstance(o, Boolean):
|
|
||||||
is_blank = not o
|
|
||||||
elif isinstance(o, String):
|
|
||||||
is_blank = not len(o.value.strip())
|
|
||||||
elif isinstance(o, List):
|
|
||||||
is_blank = all(blank(el) for el in o)
|
|
||||||
else:
|
|
||||||
is_blank = False
|
|
||||||
|
|
||||||
if not is_blank:
|
|
||||||
return Boolean(False)
|
|
||||||
|
|
||||||
return Boolean(True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('compact')
|
|
||||||
def compact(*args):
|
|
||||||
"""Returns a new list after removing any non-true values"""
|
|
||||||
use_comma = True
|
|
||||||
if len(args) == 1 and isinstance(args[0], List):
|
|
||||||
use_comma = args[0].use_comma
|
|
||||||
args = args[0]
|
|
||||||
|
|
||||||
return List(
|
|
||||||
[arg for arg in args if arg],
|
|
||||||
use_comma=use_comma,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register('reject')
|
|
||||||
def reject(lst, *values):
|
|
||||||
"""Removes the given values from the list"""
|
|
||||||
lst = List.from_maybe(lst)
|
|
||||||
values = frozenset(List.from_maybe_starargs(values))
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for item in lst:
|
|
||||||
if item not in values:
|
|
||||||
ret.append(item)
|
|
||||||
return List(ret, use_comma=lst.use_comma)
|
|
||||||
|
|
||||||
|
|
||||||
@register('first-value-of')
|
|
||||||
def first_value_of(*args):
|
|
||||||
if len(args) == 1 and isinstance(args[0], String):
|
|
||||||
first = args[0].value.split()[0]
|
|
||||||
return type(args[0])(first)
|
|
||||||
|
|
||||||
args = List.from_maybe_starargs(args)
|
|
||||||
if len(args):
|
|
||||||
return args[0]
|
|
||||||
else:
|
|
||||||
return Null()
|
|
||||||
|
|
||||||
|
|
||||||
@register('-compass-list')
|
|
||||||
def dash_compass_list(*args):
|
|
||||||
return List.from_maybe_starargs(args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-compass-space-list')
|
|
||||||
def dash_compass_space_list(*lst):
|
|
||||||
"""
|
|
||||||
If the argument is a list, it will return a new list that is space delimited
|
|
||||||
Otherwise it returns a new, single element, space-delimited list.
|
|
||||||
"""
|
|
||||||
ret = dash_compass_list(*lst)
|
|
||||||
ret.value.pop('_', None)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@register('-compass-slice', 3)
|
|
||||||
def dash_compass_slice(lst, start_index, end_index=None):
|
|
||||||
start_index = Number(start_index).value
|
|
||||||
end_index = Number(end_index).value if end_index is not None else None
|
|
||||||
ret = {}
|
|
||||||
lst = List(lst)
|
|
||||||
if end_index:
|
|
||||||
# This function has an inclusive end, but Python slicing is exclusive
|
|
||||||
end_index += 1
|
|
||||||
ret = lst.value[start_index:end_index]
|
|
||||||
return List(ret, use_comma=lst.use_comma)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Property prefixing
|
|
||||||
|
|
||||||
@register('prefixed')
|
|
||||||
def prefixed(prefix, *args):
|
|
||||||
to_fnct_str = 'to_' + to_str(prefix).replace('-', '_')
|
|
||||||
for arg in List.from_maybe_starargs(args):
|
|
||||||
if hasattr(arg, to_fnct_str):
|
|
||||||
return Boolean(True)
|
|
||||||
return Boolean(False)
|
|
||||||
|
|
||||||
|
|
||||||
@register('prefix')
|
|
||||||
def prefix(prefix, *args):
|
|
||||||
to_fnct_str = 'to_' + to_str(prefix).replace('-', '_')
|
|
||||||
args = list(args)
|
|
||||||
for i, arg in enumerate(args):
|
|
||||||
if isinstance(arg, List):
|
|
||||||
_value = []
|
|
||||||
for iarg in arg:
|
|
||||||
to_fnct = getattr(iarg, to_fnct_str, None)
|
|
||||||
if to_fnct:
|
|
||||||
_value.append(to_fnct())
|
|
||||||
else:
|
|
||||||
_value.append(iarg)
|
|
||||||
args[i] = List(_value)
|
|
||||||
else:
|
|
||||||
to_fnct = getattr(arg, to_fnct_str, None)
|
|
||||||
if to_fnct:
|
|
||||||
args[i] = to_fnct()
|
|
||||||
|
|
||||||
return List.maybe_new(args, use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-moz')
|
|
||||||
def dash_moz(*args):
|
|
||||||
return prefix('_moz', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-svg')
|
|
||||||
def dash_svg(*args):
|
|
||||||
return prefix('_svg', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-css2')
|
|
||||||
def dash_css2(*args):
|
|
||||||
return prefix('_css2', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-pie')
|
|
||||||
def dash_pie(*args):
|
|
||||||
return prefix('_pie', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-webkit')
|
|
||||||
def dash_webkit(*args):
|
|
||||||
return prefix('_webkit', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-owg')
|
|
||||||
def dash_owg(*args):
|
|
||||||
return prefix('_owg', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-khtml')
|
|
||||||
def dash_khtml(*args):
|
|
||||||
return prefix('_khtml', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-ms')
|
|
||||||
def dash_ms(*args):
|
|
||||||
return prefix('_ms', *args)
|
|
||||||
|
|
||||||
|
|
||||||
@register('-o')
|
|
||||||
def dash_o(*args):
|
|
||||||
return prefix('_o', *args)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Selector generation
|
|
||||||
|
|
||||||
@register('append-selector', 2)
|
|
||||||
def append_selector(selector, to_append):
|
|
||||||
if isinstance(selector, List):
|
|
||||||
lst = selector.value
|
|
||||||
else:
|
|
||||||
lst = String.unquoted(selector).value.split(',')
|
|
||||||
to_append = String.unquoted(to_append).value.strip()
|
|
||||||
ret = sorted(set(s.strip() + to_append for s in lst if s.strip()))
|
|
||||||
ret = dict(enumerate(ret))
|
|
||||||
ret['_'] = ','
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
_elements_of_type_block = 'address, article, aside, blockquote, center, dd, details, dir, div, dl, dt, fieldset, figcaption, figure, footer, form, frameset, h1, h2, h3, h4, h5, h6, header, hgroup, hr, isindex, menu, nav, noframes, noscript, ol, p, pre, section, summary, ul'
|
|
||||||
_elements_of_type_inline = 'a, abbr, acronym, audio, b, basefont, bdo, big, br, canvas, cite, code, command, datalist, dfn, em, embed, font, i, img, input, kbd, keygen, label, mark, meter, output, progress, q, rp, rt, ruby, s, samp, select, small, span, strike, strong, sub, sup, textarea, time, tt, u, var, video, wbr'
|
|
||||||
_elements_of_type_table = 'table'
|
|
||||||
_elements_of_type_list_item = 'li'
|
|
||||||
_elements_of_type_table_row_group = 'tbody'
|
|
||||||
_elements_of_type_table_header_group = 'thead'
|
|
||||||
_elements_of_type_table_footer_group = 'tfoot'
|
|
||||||
_elements_of_type_table_row = 'tr'
|
|
||||||
_elements_of_type_table_cel = 'td, th'
|
|
||||||
_elements_of_type_html5_block = 'article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary'
|
|
||||||
_elements_of_type_html5_inline = 'audio, canvas, command, datalist, embed, keygen, mark, meter, output, progress, rp, rt, ruby, time, video, wbr'
|
|
||||||
_elements_of_type_html5 = 'article, aside, audio, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, menu, meter, nav, output, progress, rp, rt, ruby, section, summary, time, video, wbr'
|
|
||||||
_elements_of_type = {
|
|
||||||
'block': sorted(_elements_of_type_block.replace(' ', '').split(',')),
|
|
||||||
'inline': sorted(_elements_of_type_inline.replace(' ', '').split(',')),
|
|
||||||
'table': sorted(_elements_of_type_table.replace(' ', '').split(',')),
|
|
||||||
'list-item': sorted(_elements_of_type_list_item.replace(' ', '').split(',')),
|
|
||||||
'table-row-group': sorted(_elements_of_type_table_row_group.replace(' ', '').split(',')),
|
|
||||||
'table-header-group': sorted(_elements_of_type_table_header_group.replace(' ', '').split(',')),
|
|
||||||
'table-footer-group': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
|
|
||||||
'table-row': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
|
|
||||||
'table-cell': sorted(_elements_of_type_table_footer_group.replace(' ', '').split(',')),
|
|
||||||
'html5-block': sorted(_elements_of_type_html5_block.replace(' ', '').split(',')),
|
|
||||||
'html5-inline': sorted(_elements_of_type_html5_inline.replace(' ', '').split(',')),
|
|
||||||
'html5': sorted(_elements_of_type_html5.replace(' ', '').split(',')),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@register('elements-of-type', 1)
|
|
||||||
def elements_of_type(display):
|
|
||||||
d = String.unquoted(display)
|
|
||||||
ret = _elements_of_type.get(d.value, None)
|
|
||||||
if ret is None:
|
|
||||||
raise Exception("Elements of type '%s' not found!" % d.value)
|
|
||||||
return List(ret, use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('enumerate', 3)
|
|
||||||
@register('enumerate', 4)
|
|
||||||
def enumerate_(prefix, frm, through, separator='-'):
|
|
||||||
separator = String.unquoted(separator).value
|
|
||||||
try:
|
|
||||||
frm = int(getattr(frm, 'value', frm))
|
|
||||||
except ValueError:
|
|
||||||
frm = 1
|
|
||||||
try:
|
|
||||||
through = int(getattr(through, 'value', through))
|
|
||||||
except ValueError:
|
|
||||||
through = frm
|
|
||||||
if frm > through:
|
|
||||||
# DEVIATION: allow reversed enumerations (and ranges as range() uses enumerate, like '@for .. from .. through')
|
|
||||||
frm, through = through, frm
|
|
||||||
rev = reversed
|
|
||||||
else:
|
|
||||||
rev = lambda x: x
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for i in rev(range(frm, through + 1)):
|
|
||||||
if prefix and prefix.value:
|
|
||||||
ret.append(String.unquoted(prefix.value + separator + str(i)))
|
|
||||||
else:
|
|
||||||
ret.append(Number(i))
|
|
||||||
|
|
||||||
return List(ret, use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('headers', 0)
|
|
||||||
@register('headers', 1)
|
|
||||||
@register('headers', 2)
|
|
||||||
@register('headings', 0)
|
|
||||||
@register('headings', 1)
|
|
||||||
@register('headings', 2)
|
|
||||||
def headers(frm=None, to=None):
|
|
||||||
if frm and to is None:
|
|
||||||
if isinstance(frm, String) and frm.value.lower() == 'all':
|
|
||||||
frm = 1
|
|
||||||
to = 6
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
to = int(getattr(frm, 'value', frm))
|
|
||||||
except ValueError:
|
|
||||||
to = 6
|
|
||||||
frm = 1
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
frm = 1 if frm is None else int(getattr(frm, 'value', frm))
|
|
||||||
except ValueError:
|
|
||||||
frm = 1
|
|
||||||
try:
|
|
||||||
to = 6 if to is None else int(getattr(to, 'value', to))
|
|
||||||
except ValueError:
|
|
||||||
to = 6
|
|
||||||
ret = [String.unquoted('h' + str(i)) for i in range(frm, to + 1)]
|
|
||||||
return List(ret, use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('nest')
|
|
||||||
def nest(*arguments):
|
|
||||||
if isinstance(arguments[0], List):
|
|
||||||
lst = arguments[0]
|
|
||||||
elif isinstance(arguments[0], String):
|
|
||||||
lst = arguments[0].value.split(',')
|
|
||||||
else:
|
|
||||||
raise TypeError("Expected list or string, got %r" % (arguments[0],))
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for s in lst:
|
|
||||||
if isinstance(s, String):
|
|
||||||
s = s.value
|
|
||||||
elif isinstance(s, six.string_types):
|
|
||||||
s = s
|
|
||||||
else:
|
|
||||||
raise TypeError("Expected string, got %r" % (s,))
|
|
||||||
|
|
||||||
s = s.strip()
|
|
||||||
if not s:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ret.append(s)
|
|
||||||
|
|
||||||
for arg in arguments[1:]:
|
|
||||||
if isinstance(arg, List):
|
|
||||||
lst = arg
|
|
||||||
elif isinstance(arg, String):
|
|
||||||
lst = arg.value.split(',')
|
|
||||||
else:
|
|
||||||
raise TypeError("Expected list or string, got %r" % (arg,))
|
|
||||||
|
|
||||||
new_ret = []
|
|
||||||
for s in lst:
|
|
||||||
if isinstance(s, String):
|
|
||||||
s = s.value
|
|
||||||
elif isinstance(s, six.string_types):
|
|
||||||
s = s
|
|
||||||
else:
|
|
||||||
raise TypeError("Expected string, got %r" % (s,))
|
|
||||||
|
|
||||||
s = s.strip()
|
|
||||||
if not s:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for r in ret:
|
|
||||||
if '&' in s:
|
|
||||||
new_ret.append(s.replace('&', r))
|
|
||||||
else:
|
|
||||||
if not r or r[-1] in ('.', ':', '#'):
|
|
||||||
new_ret.append(r + s)
|
|
||||||
else:
|
|
||||||
new_ret.append(r + ' ' + s)
|
|
||||||
ret = new_ret
|
|
||||||
|
|
||||||
ret = [String.unquoted(s) for s in sorted(set(ret))]
|
|
||||||
return List(ret, use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
# This isn't actually from Compass, but it's just a shortcut for enumerate().
|
|
||||||
# DEVIATION: allow reversed ranges (range() uses enumerate() which allows reversed values, like '@for .. from .. through')
|
|
||||||
@register('range', 1)
|
|
||||||
@register('range', 2)
|
|
||||||
def range_(frm, through=None):
|
|
||||||
if through is None:
|
|
||||||
through = frm
|
|
||||||
frm = 1
|
|
||||||
return enumerate_(None, frm, through)
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Working with CSS constants
|
|
||||||
|
|
||||||
OPPOSITE_POSITIONS = {
|
|
||||||
'top': String.unquoted('bottom'),
|
|
||||||
'bottom': String.unquoted('top'),
|
|
||||||
'left': String.unquoted('right'),
|
|
||||||
'right': String.unquoted('left'),
|
|
||||||
'center': String.unquoted('center'),
|
|
||||||
}
|
|
||||||
DEFAULT_POSITION = [String.unquoted('center'), String.unquoted('top')]
|
|
||||||
|
|
||||||
|
|
||||||
def _position(opposite, positions):
|
|
||||||
if positions is None:
|
|
||||||
positions = DEFAULT_POSITION
|
|
||||||
else:
|
|
||||||
positions = List.from_maybe(positions)
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
for pos in positions:
|
|
||||||
if isinstance(pos, (String, six.string_types)):
|
|
||||||
pos_value = getattr(pos, 'value', pos)
|
|
||||||
if pos_value in OPPOSITE_POSITIONS:
|
|
||||||
if opposite:
|
|
||||||
ret.append(OPPOSITE_POSITIONS[pos_value])
|
|
||||||
else:
|
|
||||||
ret.append(pos)
|
|
||||||
continue
|
|
||||||
elif pos_value == 'to':
|
|
||||||
# Gradient syntax keyword; leave alone
|
|
||||||
ret.append(pos)
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif isinstance(pos, Number):
|
|
||||||
if pos.is_simple_unit('%'):
|
|
||||||
if opposite:
|
|
||||||
ret.append(Number(100 - pos.value, '%'))
|
|
||||||
else:
|
|
||||||
ret.append(pos)
|
|
||||||
continue
|
|
||||||
elif pos.is_simple_unit('deg'):
|
|
||||||
# TODO support other angle types?
|
|
||||||
if opposite:
|
|
||||||
ret.append(Number((pos.value + 180) % 360, 'deg'))
|
|
||||||
else:
|
|
||||||
ret.append(pos)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if opposite:
|
|
||||||
log.warn("Can't find opposite for position %r" % (pos,))
|
|
||||||
ret.append(pos)
|
|
||||||
|
|
||||||
return List(ret, use_comma=False).maybe()
|
|
||||||
|
|
||||||
|
|
||||||
@register('position')
|
|
||||||
def position(p):
|
|
||||||
return _position(False, p)
|
|
||||||
|
|
||||||
|
|
||||||
@register('opposite-position')
|
|
||||||
def opposite_position(p):
|
|
||||||
return _position(True, p)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Math
|
|
||||||
|
|
||||||
@register('pi', 0)
|
|
||||||
def pi():
|
|
||||||
return Number(math.pi)
|
|
||||||
|
|
||||||
|
|
||||||
@register('e', 0)
|
|
||||||
def e():
|
|
||||||
return Number(math.e)
|
|
||||||
|
|
||||||
|
|
||||||
@register('log', 1)
|
|
||||||
@register('log', 2)
|
|
||||||
def log_(number, base=None):
|
|
||||||
if not isinstance(number, Number):
|
|
||||||
raise TypeError("Expected number, got %r" % (number,))
|
|
||||||
elif not number.is_unitless:
|
|
||||||
raise ValueError("Expected unitless number, got %r" % (number,))
|
|
||||||
|
|
||||||
if base is None:
|
|
||||||
pass
|
|
||||||
elif not isinstance(base, Number):
|
|
||||||
raise TypeError("Expected number, got %r" % (base,))
|
|
||||||
elif not base.is_unitless:
|
|
||||||
raise ValueError("Expected unitless number, got %r" % (base,))
|
|
||||||
|
|
||||||
if base is None:
|
|
||||||
ret = math.log(number.value)
|
|
||||||
else:
|
|
||||||
ret = math.log(number.value, base.value)
|
|
||||||
|
|
||||||
return Number(ret)
|
|
||||||
|
|
||||||
|
|
||||||
@register('pow', 2)
|
|
||||||
def pow(number, exponent):
|
|
||||||
return number ** exponent
|
|
||||||
|
|
||||||
|
|
||||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sqrt), 'sqrt', 1)
|
|
||||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.sin), 'sin', 1)
|
|
||||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.cos), 'cos', 1)
|
|
||||||
COMPASS_HELPERS_LIBRARY.add(Number.wrap_python_function(math.tan), 'tan', 1)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Fonts
|
|
||||||
|
|
||||||
def _fonts_root():
|
|
||||||
return config.STATIC_ROOT if config.FONTS_ROOT is None else config.FONTS_ROOT
|
|
||||||
|
|
||||||
|
|
||||||
def _font_url(path, only_path=False, cache_buster=True, inline=False):
|
|
||||||
filepath = String.unquoted(path).value
|
|
||||||
file = None
|
|
||||||
FONTS_ROOT = _fonts_root()
|
|
||||||
if callable(FONTS_ROOT):
|
|
||||||
try:
|
|
||||||
_file, _storage = list(FONTS_ROOT(filepath))[0]
|
|
||||||
d_obj = _storage.modified_time(_file)
|
|
||||||
filetime = int(time.mktime(d_obj.timetuple()))
|
|
||||||
if inline:
|
|
||||||
file = _storage.open(_file)
|
|
||||||
except:
|
|
||||||
filetime = 'NA'
|
|
||||||
else:
|
|
||||||
_path = os.path.join(FONTS_ROOT, filepath.strip('/'))
|
|
||||||
if os.path.exists(_path):
|
|
||||||
filetime = int(os.path.getmtime(_path))
|
|
||||||
if inline:
|
|
||||||
file = open(_path, 'rb')
|
|
||||||
else:
|
|
||||||
filetime = 'NA'
|
|
||||||
|
|
||||||
BASE_URL = config.FONTS_URL or config.STATIC_URL
|
|
||||||
if file and inline:
|
|
||||||
font_type = None
|
|
||||||
if re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value):
|
|
||||||
font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', path.value).groups()[1]).value
|
|
||||||
|
|
||||||
if not FONT_TYPES.get(font_type):
|
|
||||||
raise Exception('Could not determine font type for "%s"' % path.value)
|
|
||||||
|
|
||||||
mime = FONT_TYPES.get(font_type)
|
|
||||||
if font_type == 'woff':
|
|
||||||
mime = 'application/font-woff'
|
|
||||||
elif font_type == 'eot':
|
|
||||||
mime = 'application/vnd.ms-fontobject'
|
|
||||||
url = 'data:' + (mime if '/' in mime else 'font/%s' % mime) + ';base64,' + base64.b64encode(file.read())
|
|
||||||
file.close()
|
|
||||||
else:
|
|
||||||
url = '%s/%s' % (BASE_URL.rstrip('/'), filepath.lstrip('/'))
|
|
||||||
if cache_buster and filetime != 'NA':
|
|
||||||
url = add_cache_buster(url, filetime)
|
|
||||||
|
|
||||||
if not only_path:
|
|
||||||
url = 'url(%s)' % escape(url)
|
|
||||||
return String.unquoted(url)
|
|
||||||
|
|
||||||
|
|
||||||
def _font_files(args, inline):
|
|
||||||
if args == ():
|
|
||||||
return String.unquoted("")
|
|
||||||
|
|
||||||
fonts = []
|
|
||||||
args_len = len(args)
|
|
||||||
skip_next = False
|
|
||||||
for index in range(len(args)):
|
|
||||||
arg = args[index]
|
|
||||||
if not skip_next:
|
|
||||||
font_type = args[index + 1] if args_len > (index + 1) else None
|
|
||||||
if font_type and font_type.value in FONT_TYPES:
|
|
||||||
skip_next = True
|
|
||||||
else:
|
|
||||||
if re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value):
|
|
||||||
font_type = String.unquoted(re.match(r'^([^?]+)[.](.*)([?].*)?$', arg.value).groups()[1])
|
|
||||||
|
|
||||||
if font_type.value in FONT_TYPES:
|
|
||||||
fonts.append(String.unquoted('%s format("%s")' % (_font_url(arg, inline=inline), String.unquoted(FONT_TYPES[font_type.value]).value)))
|
|
||||||
else:
|
|
||||||
raise Exception('Could not determine font type for "%s"' % arg.value)
|
|
||||||
else:
|
|
||||||
skip_next = False
|
|
||||||
|
|
||||||
return List(fonts, separator=',')
|
|
||||||
|
|
||||||
|
|
||||||
@register('font-url', 1)
|
|
||||||
@register('font-url', 2)
|
|
||||||
def font_url(path, only_path=False, cache_buster=True):
|
|
||||||
"""
|
|
||||||
Generates a path to an asset found relative to the project's font directory.
|
|
||||||
Passing a true value as the second argument will cause the only the path to
|
|
||||||
be returned instead of a `url()` function
|
|
||||||
"""
|
|
||||||
return _font_url(path, only_path, cache_buster, False)
|
|
||||||
|
|
||||||
|
|
||||||
@register('font-files')
|
|
||||||
def font_files(*args):
|
|
||||||
return _font_files(args, inline=False)
|
|
||||||
|
|
||||||
|
|
||||||
@register('inline-font-files')
|
|
||||||
def inline_font_files(*args):
|
|
||||||
return _font_files(args, inline=True)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# External stylesheets
|
|
||||||
|
|
||||||
@register('stylesheet-url', 1)
|
|
||||||
@register('stylesheet-url', 2)
|
|
||||||
def stylesheet_url(path, only_path=False, cache_buster=True):
|
|
||||||
"""
|
|
||||||
Generates a path to an asset found relative to the project's css directory.
|
|
||||||
Passing a true value as the second argument will cause the only the path to
|
|
||||||
be returned instead of a `url()` function
|
|
||||||
"""
|
|
||||||
filepath = String.unquoted(path).value
|
|
||||||
if callable(config.STATIC_ROOT):
|
|
||||||
try:
|
|
||||||
_file, _storage = list(config.STATIC_ROOT(filepath))[0]
|
|
||||||
d_obj = _storage.modified_time(_file)
|
|
||||||
filetime = int(time.mktime(d_obj.timetuple()))
|
|
||||||
except:
|
|
||||||
filetime = 'NA'
|
|
||||||
else:
|
|
||||||
_path = os.path.join(config.STATIC_ROOT, filepath.strip('/'))
|
|
||||||
if os.path.exists(_path):
|
|
||||||
filetime = int(os.path.getmtime(_path))
|
|
||||||
else:
|
|
||||||
filetime = 'NA'
|
|
||||||
BASE_URL = config.STATIC_URL
|
|
||||||
|
|
||||||
url = '%s%s' % (BASE_URL, filepath)
|
|
||||||
if cache_buster:
|
|
||||||
url = add_cache_buster(url, filetime)
|
|
||||||
if not only_path:
|
|
||||||
url = 'url("%s")' % (url)
|
|
||||||
return String.unquoted(url)
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
"""Image utilities ported from Compass."""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import mimetypes
|
|
||||||
import os.path
|
|
||||||
import time
|
|
||||||
|
|
||||||
import six
|
|
||||||
from six.moves import xrange
|
|
||||||
|
|
||||||
from scss import config
|
|
||||||
from scss.functions.compass import _image_size_cache
|
|
||||||
from scss.functions.compass.helpers import add_cache_buster
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
from scss.types import Color, List, Number, String
|
|
||||||
from scss.util import escape
|
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import Image
|
|
||||||
except:
|
|
||||||
Image = None
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
COMPASS_IMAGES_LIBRARY = FunctionLibrary()
|
|
||||||
register = COMPASS_IMAGES_LIBRARY.register
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _images_root():
|
|
||||||
return config.STATIC_ROOT if config.IMAGES_ROOT is None else config.IMAGES_ROOT
|
|
||||||
|
|
||||||
|
|
||||||
def _image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, inline=False, mime_type=None, spacing=None, collapse_x=None, collapse_y=None):
|
|
||||||
"""
|
|
||||||
src_color - a list of or a single color to be replaced by each corresponding dst_color colors
|
|
||||||
spacing - spaces to be added to the image
|
|
||||||
collapse_x, collapse_y - collapsable (layered) image of the given size (x, y)
|
|
||||||
"""
|
|
||||||
if inline or dst_color or spacing:
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
filepath = String.unquoted(path).value
|
|
||||||
fileext = os.path.splitext(filepath)[1].lstrip('.').lower()
|
|
||||||
if mime_type:
|
|
||||||
mime_type = String.unquoted(mime_type).value
|
|
||||||
if not mime_type:
|
|
||||||
mime_type = mimetypes.guess_type(filepath)[0]
|
|
||||||
if not mime_type:
|
|
||||||
mime_type = 'image/%s' % fileext
|
|
||||||
path = None
|
|
||||||
IMAGES_ROOT = _images_root()
|
|
||||||
if callable(IMAGES_ROOT):
|
|
||||||
try:
|
|
||||||
_file, _storage = list(IMAGES_ROOT(filepath))[0]
|
|
||||||
d_obj = _storage.modified_time(_file)
|
|
||||||
filetime = int(time.mktime(d_obj.timetuple()))
|
|
||||||
if inline or dst_color or spacing:
|
|
||||||
path = _storage.open(_file)
|
|
||||||
except:
|
|
||||||
filetime = 'NA'
|
|
||||||
else:
|
|
||||||
_path = os.path.join(IMAGES_ROOT.rstrip('/'), filepath.strip('/'))
|
|
||||||
if os.path.exists(_path):
|
|
||||||
filetime = int(os.path.getmtime(_path))
|
|
||||||
if inline or dst_color or spacing:
|
|
||||||
path = open(_path, 'rb')
|
|
||||||
else:
|
|
||||||
filetime = 'NA'
|
|
||||||
|
|
||||||
BASE_URL = config.IMAGES_URL or config.STATIC_URL
|
|
||||||
if path:
|
|
||||||
dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_color) if v]
|
|
||||||
|
|
||||||
src_color = Color.from_name('black') if src_color is None else src_color
|
|
||||||
src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_color)]
|
|
||||||
|
|
||||||
len_colors = max(len(dst_colors), len(src_colors))
|
|
||||||
dst_colors = (dst_colors * len_colors)[:len_colors]
|
|
||||||
src_colors = (src_colors * len_colors)[:len_colors]
|
|
||||||
|
|
||||||
spacing = Number(0) if spacing is None else spacing
|
|
||||||
spacing = [int(Number(v).value) for v in List.from_maybe(spacing)]
|
|
||||||
spacing = (spacing * 4)[:4]
|
|
||||||
|
|
||||||
file_name, file_ext = os.path.splitext(os.path.normpath(filepath).replace('\\', '_').replace('/', '_'))
|
|
||||||
key = (filetime, src_color, dst_color, spacing)
|
|
||||||
key = file_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
|
||||||
asset_file = key + file_ext
|
|
||||||
ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
|
|
||||||
asset_path = os.path.join(ASSETS_ROOT, asset_file)
|
|
||||||
|
|
||||||
if os.path.exists(asset_path):
|
|
||||||
filepath = asset_file
|
|
||||||
BASE_URL = config.ASSETS_URL
|
|
||||||
if inline:
|
|
||||||
path = open(asset_path, 'rb')
|
|
||||||
url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
|
|
||||||
else:
|
|
||||||
url = '%s%s' % (BASE_URL, filepath)
|
|
||||||
if cache_buster:
|
|
||||||
filetime = int(os.path.getmtime(asset_path))
|
|
||||||
url = add_cache_buster(url, filetime)
|
|
||||||
else:
|
|
||||||
simply_process = False
|
|
||||||
image = None
|
|
||||||
|
|
||||||
if fileext in ('cur',):
|
|
||||||
simply_process = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
image = Image.open(path)
|
|
||||||
except IOError:
|
|
||||||
if not collapse_x and not collapse_y and not dst_colors:
|
|
||||||
simply_process = True
|
|
||||||
|
|
||||||
if simply_process:
|
|
||||||
if inline:
|
|
||||||
url = 'data:' + mime_type + ';base64,' + base64.b64encode(path.read())
|
|
||||||
else:
|
|
||||||
url = '%s%s' % (BASE_URL, filepath)
|
|
||||||
if cache_buster:
|
|
||||||
filetime = int(os.path.getmtime(asset_path))
|
|
||||||
url = add_cache_buster(url, filetime)
|
|
||||||
else:
|
|
||||||
width, height = collapse_x or image.size[0], collapse_y or image.size[1]
|
|
||||||
new_image = Image.new(
|
|
||||||
mode='RGBA',
|
|
||||||
size=(width + spacing[1] + spacing[3], height + spacing[0] + spacing[2]),
|
|
||||||
color=(0, 0, 0, 0)
|
|
||||||
)
|
|
||||||
for i, dst_color in enumerate(dst_colors):
|
|
||||||
src_color = src_colors[i]
|
|
||||||
pixdata = image.load()
|
|
||||||
for _y in xrange(image.size[1]):
|
|
||||||
for _x in xrange(image.size[0]):
|
|
||||||
pixel = pixdata[_x, _y]
|
|
||||||
if pixel[:3] == src_color:
|
|
||||||
pixdata[_x, _y] = tuple([int(c) for c in dst_color] + [pixel[3] if len(pixel) == 4 else 255])
|
|
||||||
iwidth, iheight = image.size
|
|
||||||
if iwidth != width or iheight != height:
|
|
||||||
cy = 0
|
|
||||||
while cy < iheight:
|
|
||||||
cx = 0
|
|
||||||
while cx < iwidth:
|
|
||||||
cropped_image = image.crop((cx, cy, cx + width, cy + height))
|
|
||||||
new_image.paste(cropped_image, (int(spacing[3]), int(spacing[0])), cropped_image)
|
|
||||||
cx += width
|
|
||||||
cy += height
|
|
||||||
else:
|
|
||||||
new_image.paste(image, (int(spacing[3]), int(spacing[0])))
|
|
||||||
|
|
||||||
if not inline:
|
|
||||||
try:
|
|
||||||
new_image.save(asset_path)
|
|
||||||
filepath = asset_file
|
|
||||||
BASE_URL = config.ASSETS_URL
|
|
||||||
if cache_buster:
|
|
||||||
filetime = int(os.path.getmtime(asset_path))
|
|
||||||
except IOError:
|
|
||||||
log.exception("Error while saving image")
|
|
||||||
inline = True # Retry inline version
|
|
||||||
url = os.path.join(config.ASSETS_URL.rstrip('/'), asset_file.lstrip('/'))
|
|
||||||
if cache_buster:
|
|
||||||
url = add_cache_buster(url, filetime)
|
|
||||||
if inline:
|
|
||||||
output = six.BytesIO()
|
|
||||||
new_image.save(output, format='PNG')
|
|
||||||
contents = output.getvalue()
|
|
||||||
output.close()
|
|
||||||
url = 'data:' + mime_type + ';base64,' + base64.b64encode(contents)
|
|
||||||
else:
|
|
||||||
url = os.path.join(BASE_URL.rstrip('/'), filepath.lstrip('/'))
|
|
||||||
if cache_buster and filetime != 'NA':
|
|
||||||
url = add_cache_buster(url, filetime)
|
|
||||||
|
|
||||||
if not only_path:
|
|
||||||
url = 'url(%s)' % escape(url)
|
|
||||||
return String.unquoted(url)
|
|
||||||
|
|
||||||
|
|
||||||
@register('inline-image', 1)
|
|
||||||
@register('inline-image', 2)
|
|
||||||
@register('inline-image', 3)
|
|
||||||
@register('inline-image', 4)
|
|
||||||
@register('inline-image', 5)
|
|
||||||
def inline_image(image, mime_type=None, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
|
|
||||||
"""
|
|
||||||
Embeds the contents of a file directly inside your stylesheet, eliminating
|
|
||||||
the need for another HTTP request. For small files such images or fonts,
|
|
||||||
this can be a performance benefit at the cost of a larger generated CSS
|
|
||||||
file.
|
|
||||||
"""
|
|
||||||
return _image_url(image, False, False, dst_color, src_color, True, mime_type, spacing, collapse_x, collapse_y)
|
|
||||||
|
|
||||||
|
|
||||||
@register('image-url', 1)
|
|
||||||
@register('image-url', 2)
|
|
||||||
@register('image-url', 3)
|
|
||||||
@register('image-url', 4)
|
|
||||||
@register('image-url', 5)
|
|
||||||
@register('image-url', 6)
|
|
||||||
def image_url(path, only_path=False, cache_buster=True, dst_color=None, src_color=None, spacing=None, collapse_x=None, collapse_y=None):
|
|
||||||
"""
|
|
||||||
Generates a path to an asset found relative to the project's images
|
|
||||||
directory.
|
|
||||||
Passing a true value as the second argument will cause the only the path to
|
|
||||||
be returned instead of a `url()` function
|
|
||||||
"""
|
|
||||||
return _image_url(path, only_path, cache_buster, dst_color, src_color, False, None, spacing, collapse_x, collapse_y)
|
|
||||||
|
|
||||||
|
|
||||||
@register('image-width', 1)
|
|
||||||
def image_width(image):
|
|
||||||
"""
|
|
||||||
Returns the width of the image found at the path supplied by `image`
|
|
||||||
relative to your project's images directory.
|
|
||||||
"""
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
filepath = String.unquoted(image).value
|
|
||||||
path = None
|
|
||||||
try:
|
|
||||||
width = _image_size_cache[filepath][0]
|
|
||||||
except KeyError:
|
|
||||||
width = 0
|
|
||||||
IMAGES_ROOT = _images_root()
|
|
||||||
if callable(IMAGES_ROOT):
|
|
||||||
try:
|
|
||||||
_file, _storage = list(IMAGES_ROOT(filepath))[0]
|
|
||||||
path = _storage.open(_file)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
|
|
||||||
if os.path.exists(_path):
|
|
||||||
path = open(_path, 'rb')
|
|
||||||
if path:
|
|
||||||
image = Image.open(path)
|
|
||||||
size = image.size
|
|
||||||
width = size[0]
|
|
||||||
_image_size_cache[filepath] = size
|
|
||||||
return Number(width, 'px')
|
|
||||||
|
|
||||||
|
|
||||||
@register('image-height', 1)
|
|
||||||
def image_height(image):
|
|
||||||
"""
|
|
||||||
Returns the height of the image found at the path supplied by `image`
|
|
||||||
relative to your project's images directory.
|
|
||||||
"""
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
filepath = String.unquoted(image).value
|
|
||||||
path = None
|
|
||||||
try:
|
|
||||||
height = _image_size_cache[filepath][1]
|
|
||||||
except KeyError:
|
|
||||||
height = 0
|
|
||||||
IMAGES_ROOT = _images_root()
|
|
||||||
if callable(IMAGES_ROOT):
|
|
||||||
try:
|
|
||||||
_file, _storage = list(IMAGES_ROOT(filepath))[0]
|
|
||||||
path = _storage.open(_file)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_path = os.path.join(IMAGES_ROOT, filepath.strip('/'))
|
|
||||||
if os.path.exists(_path):
|
|
||||||
path = open(_path, 'rb')
|
|
||||||
if path:
|
|
||||||
image = Image.open(path)
|
|
||||||
size = image.size
|
|
||||||
height = size[1]
|
|
||||||
_image_size_cache[filepath] = size
|
|
||||||
return Number(height, 'px')
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
"""Functions used for generating packed CSS sprite maps.
|
|
||||||
|
|
||||||
|
|
||||||
These are ported from the Binary Tree Bin Packing Algorithm:
|
|
||||||
http://codeincomplete.com/posts/2011/5/7/bin_packing/
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors
|
|
||||||
# Copyright (c) 2013 German M. Bravo
|
|
||||||
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
|
|
||||||
|
|
||||||
class LayoutNode(object):
|
|
||||||
def __init__(self, x, y, w, h, down=None, right=None, used=False):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
self.down = down
|
|
||||||
self.right = right
|
|
||||||
self.used = used
|
|
||||||
self.width = 0
|
|
||||||
self.height = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def area(self):
|
|
||||||
return self.width * self.height
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s (%s, %s) [%sx%s]>' % (self.__class__.__name__, self.x, self.y, self.w, self.h)
|
|
||||||
|
|
||||||
|
|
||||||
class SpritesLayout(object):
|
|
||||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None):
|
|
||||||
self.num_blocks = len(blocks)
|
|
||||||
|
|
||||||
if margin is None:
|
|
||||||
margin = [[0] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(margin, (tuple, list)):
|
|
||||||
margin = [[margin] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(margin[0], (tuple, list)):
|
|
||||||
margin = [margin] * self.num_blocks
|
|
||||||
|
|
||||||
if padding is None:
|
|
||||||
padding = [[0] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(padding, (tuple, list)):
|
|
||||||
padding = [[padding] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(padding[0], (tuple, list)):
|
|
||||||
padding = [padding] * self.num_blocks
|
|
||||||
|
|
||||||
if pmargin is None:
|
|
||||||
pmargin = [[0.0] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(pmargin, (tuple, list)):
|
|
||||||
pmargin = [[pmargin] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(pmargin[0], (tuple, list)):
|
|
||||||
pmargin = [pmargin] * self.num_blocks
|
|
||||||
|
|
||||||
if ppadding is None:
|
|
||||||
ppadding = [[0.0] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(ppadding, (tuple, list)):
|
|
||||||
ppadding = [[ppadding] * 4] * self.num_blocks
|
|
||||||
elif not isinstance(ppadding[0], (tuple, list)):
|
|
||||||
ppadding = [ppadding] * self.num_blocks
|
|
||||||
|
|
||||||
self.blocks = tuple((
|
|
||||||
b[0] + padding[i][3] + padding[i][1] + margin[i][3] + margin[i][1] + int(round(b[0] * (ppadding[i][3] + ppadding[i][1] + pmargin[i][3] + pmargin[i][1]))),
|
|
||||||
b[1] + padding[i][0] + padding[i][2] + margin[i][0] + margin[i][2] + int(round(b[1] * (ppadding[i][0] + ppadding[i][2] + pmargin[i][0] + pmargin[i][2]))),
|
|
||||||
b[0],
|
|
||||||
b[1],
|
|
||||||
i
|
|
||||||
) for i, b in enumerate(blocks))
|
|
||||||
|
|
||||||
self.margin = margin
|
|
||||||
self.padding = padding
|
|
||||||
self.pmargin = pmargin
|
|
||||||
self.ppadding = ppadding
|
|
||||||
|
|
||||||
|
|
||||||
class PackedSpritesLayout(SpritesLayout):
|
|
||||||
@staticmethod
|
|
||||||
def MAXSIDE(a, b):
|
|
||||||
"""maxside: Sort pack by maximum sides"""
|
|
||||||
return cmp(max(b[0], b[1]), max(a[0], a[1])) or cmp(min(b[0], b[1]), min(a[0], a[1])) or cmp(b[1], a[1]) or cmp(b[0], a[0])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def WIDTH(a, b):
|
|
||||||
"""width: Sort pack by width"""
|
|
||||||
return cmp(b[0], a[0]) or cmp(b[1], a[1])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def HEIGHT(a, b):
|
|
||||||
"""height: Sort pack by height"""
|
|
||||||
return cmp(b[1], a[1]) or cmp(b[0], a[0])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def AREA(a, b):
|
|
||||||
"""area: Sort pack by area"""
|
|
||||||
return cmp(b[0] * b[1], a[0] * a[1]) or cmp(b[1], a[1]) or cmp(b[0], a[0])
|
|
||||||
|
|
||||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, methods=None):
|
|
||||||
super(PackedSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
|
||||||
|
|
||||||
ratio = 0
|
|
||||||
|
|
||||||
if methods is None:
|
|
||||||
methods = (self.MAXSIDE, self.WIDTH, self.HEIGHT, self.AREA)
|
|
||||||
|
|
||||||
for method in methods:
|
|
||||||
sorted_blocks = sorted(
|
|
||||||
self.blocks,
|
|
||||||
cmp=method,
|
|
||||||
)
|
|
||||||
root = LayoutNode(
|
|
||||||
x=0,
|
|
||||||
y=0,
|
|
||||||
w=sorted_blocks[0][0] if sorted_blocks else 0,
|
|
||||||
h=sorted_blocks[0][1] if sorted_blocks else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
area = 0
|
|
||||||
nodes = [None] * self.num_blocks
|
|
||||||
|
|
||||||
for block in sorted_blocks:
|
|
||||||
w, h, width, height, i = block
|
|
||||||
node = self._findNode(root, w, h)
|
|
||||||
if node:
|
|
||||||
node = self._splitNode(node, w, h)
|
|
||||||
else:
|
|
||||||
root = self._growNode(root, w, h)
|
|
||||||
node = self._findNode(root, w, h)
|
|
||||||
if node:
|
|
||||||
node = self._splitNode(node, w, h)
|
|
||||||
else:
|
|
||||||
node = None
|
|
||||||
nodes[i] = node
|
|
||||||
node.width = width
|
|
||||||
node.height = height
|
|
||||||
area += node.area
|
|
||||||
|
|
||||||
this_ratio = area / float(root.w * root.h)
|
|
||||||
# print method.__doc__, "%g%%" % (this_ratio * 100)
|
|
||||||
if ratio < this_ratio:
|
|
||||||
self.root = root
|
|
||||||
self.nodes = nodes
|
|
||||||
self.method = method
|
|
||||||
ratio = this_ratio
|
|
||||||
if ratio > 0.96:
|
|
||||||
break
|
|
||||||
# print self.method.__doc__, "%g%%" % (ratio * 100)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
for i, node in enumerate(self.nodes):
|
|
||||||
margin, padding = self.margin[i], self.padding[i]
|
|
||||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
|
||||||
cssw = node.width + padding[3] + padding[1] + int(round(node.width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
|
||||||
cssh = node.height + padding[0] + padding[2] + int(round(node.height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
|
||||||
cssx = node.x + margin[3] + int(round(node.width * pmargin[3]))
|
|
||||||
cssy = node.y + margin[0] + int(round(node.height * pmargin[0]))
|
|
||||||
x = cssx + padding[3] + int(round(node.width * ppadding[3]))
|
|
||||||
y = cssy + padding[0] + int(round(node.height * ppadding[0]))
|
|
||||||
yield x, y, node.width, node.height, cssx, cssy, cssw, cssh
|
|
||||||
|
|
||||||
@property
|
|
||||||
def width(self):
|
|
||||||
return self.root.w
|
|
||||||
|
|
||||||
@property
|
|
||||||
def height(self):
|
|
||||||
return self.root.h
|
|
||||||
|
|
||||||
def _findNode(self, root, w, h):
|
|
||||||
if root.used:
|
|
||||||
return self._findNode(root.right, w, h) or self._findNode(root.down, w, h)
|
|
||||||
elif w <= root.w and h <= root.h:
|
|
||||||
return root
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _splitNode(self, node, w, h):
|
|
||||||
node.used = True
|
|
||||||
node.down = LayoutNode(
|
|
||||||
x=node.x,
|
|
||||||
y=node.y + h,
|
|
||||||
w=node.w,
|
|
||||||
h=node.h - h
|
|
||||||
)
|
|
||||||
node.right = LayoutNode(
|
|
||||||
x=node.x + w,
|
|
||||||
y=node.y,
|
|
||||||
w=node.w - w,
|
|
||||||
h=h
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
def _growNode(self, root, w, h):
|
|
||||||
canGrowDown = w <= root.w
|
|
||||||
canGrowRight = h <= root.h
|
|
||||||
|
|
||||||
shouldGrowRight = canGrowRight and (root.h >= root.w + w) # attempt to keep square-ish by growing right when height is much greater than width
|
|
||||||
shouldGrowDown = canGrowDown and (root.w >= root.h + h) # attempt to keep square-ish by growing down when width is much greater than height
|
|
||||||
|
|
||||||
if shouldGrowRight:
|
|
||||||
return self._growRight(root, w, h)
|
|
||||||
elif shouldGrowDown:
|
|
||||||
return self._growDown(root, w, h)
|
|
||||||
elif canGrowRight:
|
|
||||||
return self._growRight(root, w, h)
|
|
||||||
elif canGrowDown:
|
|
||||||
return self._growDown(root, w, h)
|
|
||||||
else:
|
|
||||||
# need to ensure sensible root starting size to avoid this happening
|
|
||||||
assert False, "Blocks must be properly sorted!"
|
|
||||||
|
|
||||||
def _growRight(self, root, w, h):
|
|
||||||
root = LayoutNode(
|
|
||||||
used=True,
|
|
||||||
x=0,
|
|
||||||
y=0,
|
|
||||||
w=root.w + w,
|
|
||||||
h=root.h,
|
|
||||||
down=root,
|
|
||||||
right=LayoutNode(
|
|
||||||
x=root.w,
|
|
||||||
y=0,
|
|
||||||
w=w,
|
|
||||||
h=root.h
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return root
|
|
||||||
|
|
||||||
def _growDown(self, root, w, h):
|
|
||||||
root = LayoutNode(
|
|
||||||
used=True,
|
|
||||||
x=0,
|
|
||||||
y=0,
|
|
||||||
w=root.w,
|
|
||||||
h=root.h + h,
|
|
||||||
down=LayoutNode(
|
|
||||||
x=0,
|
|
||||||
y=root.h,
|
|
||||||
w=root.w,
|
|
||||||
h=h
|
|
||||||
),
|
|
||||||
right=root
|
|
||||||
)
|
|
||||||
return root
|
|
||||||
|
|
||||||
|
|
||||||
class HorizontalSpritesLayout(SpritesLayout):
|
|
||||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None):
|
|
||||||
super(HorizontalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
|
||||||
|
|
||||||
self.width = sum(zip(*self.blocks)[0])
|
|
||||||
self.height = max(zip(*self.blocks)[1])
|
|
||||||
|
|
||||||
if position is None:
|
|
||||||
position = [0.0] * self.num_blocks
|
|
||||||
elif not isinstance(position, (tuple, list)):
|
|
||||||
position = [position] * self.num_blocks
|
|
||||||
self.position = position
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
cx = 0
|
|
||||||
for i, block in enumerate(self.blocks):
|
|
||||||
w, h, width, height, i = block
|
|
||||||
margin, padding = self.margin[i], self.padding[i]
|
|
||||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
|
||||||
position = self.position[i]
|
|
||||||
cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
|
||||||
cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
|
||||||
cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x
|
|
||||||
cssy = int(round((self.height - cssh) * position)) # centered vertically
|
|
||||||
x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding
|
|
||||||
y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding
|
|
||||||
yield x, y, width, height, cssx, cssy, cssw, cssh
|
|
||||||
cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1])))
|
|
||||||
|
|
||||||
|
|
||||||
class VerticalSpritesLayout(SpritesLayout):
|
|
||||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None, position=None):
|
|
||||||
super(VerticalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
|
||||||
|
|
||||||
self.width = max(zip(*self.blocks)[0])
|
|
||||||
self.height = sum(zip(*self.blocks)[1])
|
|
||||||
|
|
||||||
if position is None:
|
|
||||||
position = [0.0] * self.num_blocks
|
|
||||||
elif not isinstance(position, (tuple, list)):
|
|
||||||
position = [position] * self.num_blocks
|
|
||||||
self.position = position
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
cy = 0
|
|
||||||
for i, block in enumerate(self.blocks):
|
|
||||||
w, h, width, height, i = block
|
|
||||||
margin, padding = self.margin[i], self.padding[i]
|
|
||||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
|
||||||
position = self.position[i]
|
|
||||||
cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
|
||||||
cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
|
||||||
cssx = int(round((self.width - cssw) * position)) # centered horizontally
|
|
||||||
cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y
|
|
||||||
x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding
|
|
||||||
y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding
|
|
||||||
yield x, y, width, height, cssx, cssy, cssw, cssh
|
|
||||||
cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2])))
|
|
||||||
|
|
||||||
|
|
||||||
class DiagonalSpritesLayout(SpritesLayout):
|
|
||||||
def __init__(self, blocks, padding=None, margin=None, ppadding=None, pmargin=None):
|
|
||||||
super(DiagonalSpritesLayout, self).__init__(blocks, padding, margin, ppadding, pmargin)
|
|
||||||
self.width = sum(zip(*self.blocks)[0])
|
|
||||||
self.height = sum(zip(*self.blocks)[1])
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
cx, cy = 0, 0
|
|
||||||
for i, block in enumerate(self.blocks):
|
|
||||||
w, h, width, height, i = block
|
|
||||||
margin, padding = self.margin[i], self.padding[i]
|
|
||||||
pmargin, ppadding = self.pmargin[i], self.ppadding[i]
|
|
||||||
cssw = width + padding[3] + padding[1] + int(round(width * (ppadding[3] + ppadding[1]))) # image width plus padding
|
|
||||||
cssh = height + padding[0] + padding[2] + int(round(height * (ppadding[0] + ppadding[2]))) # image height plus padding
|
|
||||||
cssx = cx + margin[3] + int(round(width * pmargin[3])) # anchored at x
|
|
||||||
cssy = cy + margin[0] + int(round(height * pmargin[0])) # anchored at y
|
|
||||||
x = cssx + padding[3] + int(round(width * ppadding[3])) # image drawn offset to account for padding
|
|
||||||
y = cssy + padding[0] + int(round(height * ppadding[0])) # image drawn offset to account for padding
|
|
||||||
yield x, y, width, height, cssx, cssy, cssw, cssh
|
|
||||||
cx += cssw + margin[3] + margin[1] + int(round(width * (pmargin[3] + pmargin[1])))
|
|
||||||
cy += cssh + margin[0] + margin[2] + int(round(height * (pmargin[0] + pmargin[2])))
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
"""Functions used for generating CSS sprites.
|
|
||||||
|
|
||||||
These are ported from the Compass sprite library:
|
|
||||||
http://compass-style.org/reference/compass/utilities/sprites/
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import glob
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import os.path
|
|
||||||
import tempfile
|
|
||||||
import time
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError:
|
|
||||||
import pickle
|
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import Image
|
|
||||||
except:
|
|
||||||
Image = None
|
|
||||||
|
|
||||||
from six.moves import xrange
|
|
||||||
|
|
||||||
from scss import config
|
|
||||||
from scss.functions.compass import _image_size_cache
|
|
||||||
from scss.functions.compass.layouts import PackedSpritesLayout, HorizontalSpritesLayout, VerticalSpritesLayout, DiagonalSpritesLayout
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
from scss.types import Color, List, Number, String, Boolean
|
|
||||||
from scss.util import escape
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
MAX_SPRITE_MAPS = 4096
|
|
||||||
KEEP_SPRITE_MAPS = int(MAX_SPRITE_MAPS * 0.8)
|
|
||||||
|
|
||||||
COMPASS_SPRITES_LIBRARY = FunctionLibrary()
|
|
||||||
register = COMPASS_SPRITES_LIBRARY.register
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Compass-like functionality for sprites and images
|
|
||||||
|
|
||||||
sprite_maps = {}
|
|
||||||
|
|
||||||
|
|
||||||
def alpha_composite(im1, im2, offset=None, box=None, opacity=1):
|
|
||||||
im1size = im1.size
|
|
||||||
im2size = im2.size
|
|
||||||
if offset is None:
|
|
||||||
offset = (0, 0)
|
|
||||||
if box is None:
|
|
||||||
box = (0, 0) + im2size
|
|
||||||
o1x, o1y = offset
|
|
||||||
o2x, o2y, o2w, o2h = box
|
|
||||||
width = o2w - o2x
|
|
||||||
height = o2h - o2y
|
|
||||||
im1_data = im1.load()
|
|
||||||
im2_data = im2.load()
|
|
||||||
for y in xrange(height):
|
|
||||||
for x in xrange(width):
|
|
||||||
pos1 = o1x + x, o1y + y
|
|
||||||
if pos1[0] >= im1size[0] or pos1[1] >= im1size[1]:
|
|
||||||
continue
|
|
||||||
pos2 = o2x + x, o2y + y
|
|
||||||
if pos2[0] >= im2size[0] or pos2[1] >= im2size[1]:
|
|
||||||
continue
|
|
||||||
dr, dg, db, da = im1_data[pos1]
|
|
||||||
sr, sg, sb, sa = im2_data[pos2]
|
|
||||||
da /= 255.0
|
|
||||||
sa /= 255.0
|
|
||||||
sa *= opacity
|
|
||||||
ida = da * (1 - sa)
|
|
||||||
oa = (sa + ida)
|
|
||||||
if oa:
|
|
||||||
pixel = (
|
|
||||||
int(round((sr * sa + dr * ida)) / oa),
|
|
||||||
int(round((sg * sa + dg * ida)) / oa),
|
|
||||||
int(round((sb * sa + db * ida)) / oa),
|
|
||||||
int(round(255 * oa))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
pixel = (0, 0, 0, 0)
|
|
||||||
im1_data[pos1] = pixel
|
|
||||||
return im1
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprite-map')
|
|
||||||
def sprite_map(g, **kwargs):
|
|
||||||
"""
|
|
||||||
Generates a sprite map from the files matching the glob pattern.
|
|
||||||
Uses the keyword-style arguments passed in to control the placement.
|
|
||||||
|
|
||||||
$direction - Sprite map layout. Can be `vertical` (default), `horizontal`, `diagonal` or `smart`.
|
|
||||||
|
|
||||||
$position - For `horizontal` and `vertical` directions, the position of the sprite. (defaults to `0`)
|
|
||||||
$<sprite>-position - Position of a given sprite.
|
|
||||||
|
|
||||||
$padding, $spacing - Adds paddings to sprites (top, right, bottom, left). (defaults to `0, 0, 0, 0`)
|
|
||||||
$<sprite>-padding, $<sprite>-spacing - Padding for a given sprite.
|
|
||||||
|
|
||||||
$dst-color - Together with `$src-color`, forms a map of source colors to be converted to destiny colors (same index of `$src-color` changed to `$dst-color`).
|
|
||||||
$<sprite>-dst-color - Destiny colors for a given sprite. (defaults to `$dst-color`)
|
|
||||||
|
|
||||||
$src-color - Selects source colors to be converted to the corresponding destiny colors. (defaults to `black`)
|
|
||||||
$<sprite>-dst-color - Source colors for a given sprite. (defaults to `$src-color`)
|
|
||||||
|
|
||||||
$collapse - Collapses every image in the sprite map to a fixed size (`x` and `y`).
|
|
||||||
$collapse-x - Collapses a size for `x`.
|
|
||||||
$collapse-y - Collapses a size for `y`.
|
|
||||||
"""
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
|
|
||||||
now_time = time.time()
|
|
||||||
|
|
||||||
g = String(g, quotes=None).value
|
|
||||||
|
|
||||||
if g in sprite_maps:
|
|
||||||
sprite_maps[glob]['*'] = now_time
|
|
||||||
elif '..' not in g: # Protect against going to prohibited places...
|
|
||||||
if callable(config.STATIC_ROOT):
|
|
||||||
glob_path = g
|
|
||||||
rfiles = files = sorted(config.STATIC_ROOT(g))
|
|
||||||
else:
|
|
||||||
glob_path = os.path.join(config.STATIC_ROOT, g)
|
|
||||||
files = glob.glob(glob_path)
|
|
||||||
files = sorted((f, None) for f in files)
|
|
||||||
rfiles = [(rf[len(config.STATIC_ROOT):], s) for rf, s in files]
|
|
||||||
|
|
||||||
if not files:
|
|
||||||
log.error("Nothing found at '%s'", glob_path)
|
|
||||||
return String.unquoted('')
|
|
||||||
|
|
||||||
map_name = os.path.normpath(os.path.dirname(g)).replace('\\', '_').replace('/', '_')
|
|
||||||
key = list(zip(*files)[0]) + [repr(kwargs), config.ASSETS_URL]
|
|
||||||
key = map_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
|
||||||
asset_file = key + '.png'
|
|
||||||
ASSETS_ROOT = config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets')
|
|
||||||
asset_path = os.path.join(ASSETS_ROOT, asset_file)
|
|
||||||
cache_path = os.path.join(config.CACHE_ROOT or ASSETS_ROOT, asset_file + '.cache')
|
|
||||||
|
|
||||||
inline = Boolean(kwargs.get('inline', False))
|
|
||||||
|
|
||||||
sprite_map = None
|
|
||||||
asset = None
|
|
||||||
file_asset = None
|
|
||||||
inline_asset = None
|
|
||||||
if os.path.exists(asset_path) or inline:
|
|
||||||
try:
|
|
||||||
save_time, file_asset, inline_asset, sprite_map, sizes = pickle.load(open(cache_path))
|
|
||||||
if file_asset:
|
|
||||||
sprite_maps[file_asset.render()] = sprite_map
|
|
||||||
if inline_asset:
|
|
||||||
sprite_maps[inline_asset.render()] = sprite_map
|
|
||||||
if inline:
|
|
||||||
asset = inline_asset
|
|
||||||
else:
|
|
||||||
asset = file_asset
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if sprite_map:
|
|
||||||
for file_, storage in files:
|
|
||||||
if storage is not None:
|
|
||||||
d_obj = storage.modified_time(file_)
|
|
||||||
_time = time.mktime(d_obj.timetuple())
|
|
||||||
else:
|
|
||||||
_time = os.path.getmtime(file_)
|
|
||||||
if save_time < _time:
|
|
||||||
if _time > now_time:
|
|
||||||
log.warning("File '%s' has a date in the future (cache ignored)" % file_)
|
|
||||||
sprite_map = None # Invalidate cached sprite map
|
|
||||||
break
|
|
||||||
|
|
||||||
if sprite_map is None or asset is None:
|
|
||||||
cache_buster = Boolean(kwargs.get('cache_buster', True))
|
|
||||||
direction = String.unquoted(kwargs.get('direction', config.SPRTE_MAP_DIRECTION)).value
|
|
||||||
repeat = String.unquoted(kwargs.get('repeat', 'no-repeat')).value
|
|
||||||
collapse = kwargs.get('collapse', Number(0))
|
|
||||||
if isinstance(collapse, List):
|
|
||||||
collapse_x = int(Number(collapse[0]).value)
|
|
||||||
collapse_y = int(Number(collapse[-1]).value)
|
|
||||||
else:
|
|
||||||
collapse_x = collapse_y = int(Number(collapse).value)
|
|
||||||
if 'collapse_x' in kwargs:
|
|
||||||
collapse_x = int(Number(kwargs['collapse_x']).value)
|
|
||||||
if 'collapse_y' in kwargs:
|
|
||||||
collapse_y = int(Number(kwargs['collapse_y']).value)
|
|
||||||
|
|
||||||
position = Number(kwargs.get('position', 0))
|
|
||||||
if not position.is_simple_unit('%') and position.value > 1:
|
|
||||||
position = position.value / 100.0
|
|
||||||
else:
|
|
||||||
position = position.value
|
|
||||||
if position < 0:
|
|
||||||
position = 0.0
|
|
||||||
elif position > 1:
|
|
||||||
position = 1.0
|
|
||||||
|
|
||||||
padding = kwargs.get('padding', kwargs.get('spacing', Number(0)))
|
|
||||||
padding = [int(Number(v).value) for v in List.from_maybe(padding)]
|
|
||||||
padding = (padding * 4)[:4]
|
|
||||||
|
|
||||||
dst_colors = kwargs.get('dst_color')
|
|
||||||
dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(dst_colors) if v]
|
|
||||||
src_colors = kwargs.get('src_color', Color.from_name('black'))
|
|
||||||
src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(src_colors)]
|
|
||||||
len_colors = max(len(dst_colors), len(src_colors))
|
|
||||||
dst_colors = (dst_colors * len_colors)[:len_colors]
|
|
||||||
src_colors = (src_colors * len_colors)[:len_colors]
|
|
||||||
|
|
||||||
def images(f=lambda x: x):
|
|
||||||
for file_, storage in f(files):
|
|
||||||
if storage is not None:
|
|
||||||
_file = storage.open(file_)
|
|
||||||
else:
|
|
||||||
_file = file_
|
|
||||||
_image = Image.open(_file)
|
|
||||||
yield _image
|
|
||||||
|
|
||||||
names = tuple(os.path.splitext(os.path.basename(file_))[0] for file_, storage in files)
|
|
||||||
|
|
||||||
has_dst_colors = False
|
|
||||||
all_dst_colors = []
|
|
||||||
all_src_colors = []
|
|
||||||
all_positions = []
|
|
||||||
all_paddings = []
|
|
||||||
|
|
||||||
for name in names:
|
|
||||||
name = name.replace('-', '_')
|
|
||||||
|
|
||||||
_position = kwargs.get(name + '_position')
|
|
||||||
if _position is None:
|
|
||||||
_position = position
|
|
||||||
else:
|
|
||||||
_position = Number(_position)
|
|
||||||
if not _position.is_simple_unit('%') and _position.value > 1:
|
|
||||||
_position = _position.value / 100.0
|
|
||||||
else:
|
|
||||||
_position = _position.value
|
|
||||||
if _position < 0:
|
|
||||||
_position = 0.0
|
|
||||||
elif _position > 1:
|
|
||||||
_position = 1.0
|
|
||||||
all_positions.append(_position)
|
|
||||||
|
|
||||||
_padding = kwargs.get(name + '_padding', kwargs.get(name + '_spacing'))
|
|
||||||
if _padding is None:
|
|
||||||
_padding = padding
|
|
||||||
else:
|
|
||||||
_padding = [int(Number(v).value) for v in List.from_maybe(_padding)]
|
|
||||||
_padding = (_padding * 4)[:4]
|
|
||||||
all_paddings.append(_padding)
|
|
||||||
|
|
||||||
_dst_colors = kwargs.get(name + '_dst_color')
|
|
||||||
if _dst_colors is None:
|
|
||||||
_dst_colors = dst_colors
|
|
||||||
if dst_colors:
|
|
||||||
has_dst_colors = True
|
|
||||||
else:
|
|
||||||
has_dst_colors = True
|
|
||||||
_dst_colors = [list(Color(v).value[:3]) for v in List.from_maybe(_dst_colors) if v]
|
|
||||||
_src_colors = kwargs.get(name + '_src_color', Color.from_name('black'))
|
|
||||||
if _src_colors is None:
|
|
||||||
_src_colors = src_colors
|
|
||||||
else:
|
|
||||||
_src_colors = [tuple(Color(v).value[:3]) for v in List.from_maybe(_src_colors)]
|
|
||||||
_len_colors = max(len(_dst_colors), len(_src_colors))
|
|
||||||
_dst_colors = (_dst_colors * _len_colors)[:_len_colors]
|
|
||||||
_src_colors = (_src_colors * _len_colors)[:_len_colors]
|
|
||||||
all_dst_colors.append(_dst_colors)
|
|
||||||
all_src_colors.append(_src_colors)
|
|
||||||
|
|
||||||
sizes = tuple((collapse_x or i.size[0], collapse_y or i.size[1]) for i in images())
|
|
||||||
|
|
||||||
if direction == 'horizontal':
|
|
||||||
layout = HorizontalSpritesLayout(sizes, all_paddings, position=all_positions)
|
|
||||||
elif direction == 'vertical':
|
|
||||||
layout = VerticalSpritesLayout(sizes, all_paddings, position=all_positions)
|
|
||||||
elif direction == 'diagonal':
|
|
||||||
layout = DiagonalSpritesLayout(sizes, all_paddings)
|
|
||||||
elif direction == 'smart':
|
|
||||||
layout = PackedSpritesLayout(sizes, all_paddings)
|
|
||||||
else:
|
|
||||||
raise Exception("Invalid direction %r" % (direction,))
|
|
||||||
layout_positions = list(layout)
|
|
||||||
|
|
||||||
new_image = Image.new(
|
|
||||||
mode='RGBA',
|
|
||||||
size=(layout.width, layout.height),
|
|
||||||
color=(0, 0, 0, 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
useless_dst_color = has_dst_colors
|
|
||||||
|
|
||||||
offsets_x = []
|
|
||||||
offsets_y = []
|
|
||||||
for i, image in enumerate(images()):
|
|
||||||
x, y, width, height, cssx, cssy, cssw, cssh = layout_positions[i]
|
|
||||||
iwidth, iheight = image.size
|
|
||||||
|
|
||||||
if has_dst_colors:
|
|
||||||
pixdata = image.load()
|
|
||||||
for _y in xrange(iheight):
|
|
||||||
for _x in xrange(iwidth):
|
|
||||||
pixel = pixdata[_x, _y]
|
|
||||||
a = pixel[3] if len(pixel) == 4 else 255
|
|
||||||
if a:
|
|
||||||
rgb = pixel[:3]
|
|
||||||
for j, dst_color in enumerate(all_dst_colors[i]):
|
|
||||||
if rgb == all_src_colors[i][j]:
|
|
||||||
new_color = tuple([int(c) for c in dst_color] + [a])
|
|
||||||
if pixel != new_color:
|
|
||||||
pixdata[_x, _y] = new_color
|
|
||||||
useless_dst_color = False
|
|
||||||
break
|
|
||||||
|
|
||||||
if iwidth != width or iheight != height:
|
|
||||||
cy = 0
|
|
||||||
while cy < iheight:
|
|
||||||
cx = 0
|
|
||||||
while cx < iwidth:
|
|
||||||
new_image = alpha_composite(new_image, image, (x, y), (cx, cy, cx + width, cy + height))
|
|
||||||
cx += width
|
|
||||||
cy += height
|
|
||||||
else:
|
|
||||||
new_image.paste(image, (x, y))
|
|
||||||
offsets_x.append(cssx)
|
|
||||||
offsets_y.append(cssy)
|
|
||||||
|
|
||||||
if useless_dst_color:
|
|
||||||
log.warning("Useless use of $dst-color in sprite map for files at '%s' (never used for)" % glob_path)
|
|
||||||
|
|
||||||
filetime = int(now_time)
|
|
||||||
|
|
||||||
if not inline:
|
|
||||||
try:
|
|
||||||
new_image.save(asset_path)
|
|
||||||
url = '%s%s' % (config.ASSETS_URL, asset_file)
|
|
||||||
if cache_buster:
|
|
||||||
url += '?_=%s' % filetime
|
|
||||||
except IOError:
|
|
||||||
log.exception("Error while saving image")
|
|
||||||
inline = True
|
|
||||||
if inline:
|
|
||||||
output = six.BytesIO()
|
|
||||||
new_image.save(output, format='PNG')
|
|
||||||
contents = output.getvalue()
|
|
||||||
output.close()
|
|
||||||
mime_type = 'image/png'
|
|
||||||
url = 'data:' + mime_type + ';base64,' + base64.b64encode(contents)
|
|
||||||
|
|
||||||
url = 'url(%s)' % escape(url)
|
|
||||||
if inline:
|
|
||||||
asset = inline_asset = List([String.unquoted(url), String.unquoted(repeat)])
|
|
||||||
else:
|
|
||||||
asset = file_asset = List([String.unquoted(url), String.unquoted(repeat)])
|
|
||||||
|
|
||||||
# Add the new object:
|
|
||||||
sprite_map = dict(zip(names, zip(sizes, rfiles, offsets_x, offsets_y)))
|
|
||||||
sprite_map['*'] = now_time
|
|
||||||
sprite_map['*f*'] = asset_file
|
|
||||||
sprite_map['*k*'] = key
|
|
||||||
sprite_map['*n*'] = map_name
|
|
||||||
sprite_map['*t*'] = filetime
|
|
||||||
|
|
||||||
cache_tmp = tempfile.NamedTemporaryFile(delete=False, dir=ASSETS_ROOT)
|
|
||||||
pickle.dump((now_time, file_asset, inline_asset, sprite_map, zip(files, sizes)), cache_tmp)
|
|
||||||
cache_tmp.close()
|
|
||||||
os.rename(cache_tmp.name, cache_path)
|
|
||||||
|
|
||||||
# Use the sorted list to remove older elements (keep only 500 objects):
|
|
||||||
if len(sprite_maps) > MAX_SPRITE_MAPS:
|
|
||||||
for a in sorted(sprite_maps, key=lambda a: sprite_maps[a]['*'], reverse=True)[KEEP_SPRITE_MAPS:]:
|
|
||||||
del sprite_maps[a]
|
|
||||||
log.warning("Exceeded maximum number of sprite maps (%s)" % MAX_SPRITE_MAPS)
|
|
||||||
sprite_maps[asset.render()] = sprite_map
|
|
||||||
for file_, size in sizes:
|
|
||||||
_image_size_cache[file_] = size
|
|
||||||
# TODO this sometimes returns an empty list, or is never assigned to
|
|
||||||
return asset
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprite-map-name', 1)
|
|
||||||
def sprite_map_name(map):
|
|
||||||
"""
|
|
||||||
Returns the name of a sprite map The name is derived from the folder than
|
|
||||||
contains the sprites.
|
|
||||||
"""
|
|
||||||
map = map.render()
|
|
||||||
sprite_map = sprite_maps.get(map)
|
|
||||||
if not sprite_map:
|
|
||||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
|
||||||
if sprite_map:
|
|
||||||
return String.unquoted(sprite_map['*n*'])
|
|
||||||
return String.unquoted('')
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprite-file', 2)
|
|
||||||
def sprite_file(map, sprite):
|
|
||||||
"""
|
|
||||||
Returns the relative path (from the images directory) to the original file
|
|
||||||
used when construction the sprite. This is suitable for passing to the
|
|
||||||
image_width and image_height helpers.
|
|
||||||
"""
|
|
||||||
map = map.render()
|
|
||||||
sprite_map = sprite_maps.get(map)
|
|
||||||
sprite_name = String.unquoted(sprite).value
|
|
||||||
sprite = sprite_map and sprite_map.get(sprite_name)
|
|
||||||
if not sprite_map:
|
|
||||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
|
||||||
elif not sprite:
|
|
||||||
log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True})
|
|
||||||
if sprite:
|
|
||||||
return String(sprite[1][0])
|
|
||||||
return String.unquoted('')
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprites', 1)
|
|
||||||
@register('sprite-names', 1)
|
|
||||||
def sprites(map):
|
|
||||||
map = map.render()
|
|
||||||
sprite_map = sprite_maps.get(map, {})
|
|
||||||
return List(list(String.unquoted(s) for s in sorted(s for s in sprite_map if not s.startswith('*'))))
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprite', 2)
|
|
||||||
@register('sprite', 3)
|
|
||||||
@register('sprite', 4)
|
|
||||||
@register('sprite', 5)
|
|
||||||
def sprite(map, sprite, offset_x=None, offset_y=None, cache_buster=True):
|
|
||||||
"""
|
|
||||||
Returns the image and background position for use in a single shorthand
|
|
||||||
property
|
|
||||||
"""
|
|
||||||
map = map.render()
|
|
||||||
sprite_map = sprite_maps.get(map)
|
|
||||||
sprite_name = String.unquoted(sprite).value
|
|
||||||
sprite = sprite_map and sprite_map.get(sprite_name)
|
|
||||||
if not sprite_map:
|
|
||||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
|
||||||
elif not sprite:
|
|
||||||
log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True})
|
|
||||||
if sprite:
|
|
||||||
url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*'])
|
|
||||||
if cache_buster:
|
|
||||||
url += '?_=%s' % sprite_map['*t*']
|
|
||||||
x = Number(offset_x or 0, 'px')
|
|
||||||
y = Number(offset_y or 0, 'px')
|
|
||||||
if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'):
|
|
||||||
x -= Number(sprite[2], 'px')
|
|
||||||
if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'):
|
|
||||||
y -= Number(sprite[3], 'px')
|
|
||||||
url = "url(%s)" % escape(url)
|
|
||||||
return List([String.unquoted(url), x, y])
|
|
||||||
return List([Number(0), Number(0)])
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprite-url', 1)
|
|
||||||
@register('sprite-url', 2)
|
|
||||||
def sprite_url(map, cache_buster=True):
|
|
||||||
"""
|
|
||||||
Returns a url to the sprite image.
|
|
||||||
"""
|
|
||||||
map = map.render()
|
|
||||||
sprite_map = sprite_maps.get(map)
|
|
||||||
if not sprite_map:
|
|
||||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
|
||||||
if sprite_map:
|
|
||||||
url = '%s%s' % (config.ASSETS_URL, sprite_map['*f*'])
|
|
||||||
if cache_buster:
|
|
||||||
url += '?_=%s' % sprite_map['*t*']
|
|
||||||
url = "url(%s)" % escape(url)
|
|
||||||
return String.unquoted(url)
|
|
||||||
return String.unquoted('')
|
|
||||||
|
|
||||||
|
|
||||||
@register('sprite-position', 2)
|
|
||||||
@register('sprite-position', 3)
|
|
||||||
@register('sprite-position', 4)
|
|
||||||
def sprite_position(map, sprite, offset_x=None, offset_y=None):
|
|
||||||
"""
|
|
||||||
Returns the position for the original image in the sprite.
|
|
||||||
This is suitable for use as a value to background-position.
|
|
||||||
"""
|
|
||||||
map = map.render()
|
|
||||||
sprite_map = sprite_maps.get(map)
|
|
||||||
sprite_name = String.unquoted(sprite).value
|
|
||||||
sprite = sprite_map and sprite_map.get(sprite_name)
|
|
||||||
if not sprite_map:
|
|
||||||
log.error("No sprite map found: %s", map, extra={'stack': True})
|
|
||||||
elif not sprite:
|
|
||||||
log.error("No sprite found: %s in %s", sprite_name, sprite_map['*n*'], extra={'stack': True})
|
|
||||||
if sprite:
|
|
||||||
x = None
|
|
||||||
if offset_x is not None and not isinstance(offset_x, Number):
|
|
||||||
x = offset_x
|
|
||||||
if not x or x.value not in ('left', 'right', 'center'):
|
|
||||||
if x:
|
|
||||||
offset_x = None
|
|
||||||
x = Number(offset_x or 0, 'px')
|
|
||||||
if not x.value or (x.value <= -1 or x.value >= 1) and not x.is_simple_unit('%'):
|
|
||||||
x -= Number(sprite[2], 'px')
|
|
||||||
y = None
|
|
||||||
if offset_y is not None and not isinstance(offset_y, Number):
|
|
||||||
y = offset_y
|
|
||||||
if not y or y.value not in ('top', 'bottom', 'center'):
|
|
||||||
if y:
|
|
||||||
offset_y = None
|
|
||||||
y = Number(offset_y or 0, 'px')
|
|
||||||
if not y.value or (y.value <= -1 or y.value >= 1) and not y.is_simple_unit('%'):
|
|
||||||
y -= Number(sprite[3], 'px')
|
|
||||||
return List([x, y])
|
|
||||||
return List([Number(0), Number(0)])
|
|
||||||
@@ -1,784 +0,0 @@
|
|||||||
"""Functions from the Sass "standard library", i.e., built into the original
|
|
||||||
Ruby implementation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import division
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import math
|
|
||||||
|
|
||||||
from six.moves import xrange
|
|
||||||
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
from scss.types import Boolean, Color, List, Null, Number, String, Map, expect_type
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
CORE_LIBRARY = FunctionLibrary()
|
|
||||||
register = CORE_LIBRARY.register
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Color creation
|
|
||||||
|
|
||||||
def _interpret_percentage(n, relto=1., clamp=True):
|
|
||||||
expect_type(n, Number, unit='%')
|
|
||||||
|
|
||||||
if n.is_unitless:
|
|
||||||
ret = n.value / relto
|
|
||||||
else:
|
|
||||||
ret = n.value / 100
|
|
||||||
|
|
||||||
if clamp:
|
|
||||||
if ret < 0:
|
|
||||||
return 0
|
|
||||||
elif ret > 1:
|
|
||||||
return 1
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@register('rgba', 4)
|
|
||||||
def rgba(r, g, b, a):
|
|
||||||
r = _interpret_percentage(r, relto=255)
|
|
||||||
g = _interpret_percentage(g, relto=255)
|
|
||||||
b = _interpret_percentage(b, relto=255)
|
|
||||||
a = _interpret_percentage(a, relto=1)
|
|
||||||
|
|
||||||
return Color.from_rgb(r, g, b, a)
|
|
||||||
|
|
||||||
|
|
||||||
@register('rgb', 3)
|
|
||||||
def rgb(r, g, b, type='rgb'):
|
|
||||||
return rgba(r, g, b, Number(1.0))
|
|
||||||
|
|
||||||
|
|
||||||
@register('rgba', 1)
|
|
||||||
@register('rgba', 2)
|
|
||||||
def rgba2(color, a=None):
|
|
||||||
if a is None:
|
|
||||||
alpha = 1
|
|
||||||
else:
|
|
||||||
alpha = _interpret_percentage(a)
|
|
||||||
|
|
||||||
return Color.from_rgb(*color.rgba[:3], alpha=alpha)
|
|
||||||
|
|
||||||
|
|
||||||
@register('rgb', 1)
|
|
||||||
def rgb1(color):
|
|
||||||
return rgba2(color, a=Number(1))
|
|
||||||
|
|
||||||
|
|
||||||
@register('hsla', 4)
|
|
||||||
def hsla(h, s, l, a):
|
|
||||||
return Color.from_hsl(
|
|
||||||
h.value / 360 % 1,
|
|
||||||
# Ruby sass treats plain numbers for saturation and lightness as though
|
|
||||||
# they were percentages, just without the %
|
|
||||||
_interpret_percentage(s, relto=100),
|
|
||||||
_interpret_percentage(l, relto=100),
|
|
||||||
alpha=a.value,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@register('hsl', 3)
|
|
||||||
def hsl(h, s, l):
|
|
||||||
return hsla(h, s, l, Number(1))
|
|
||||||
|
|
||||||
|
|
||||||
@register('hsla', 1)
|
|
||||||
@register('hsla', 2)
|
|
||||||
def hsla2(color, a=None):
|
|
||||||
return rgba2(color, a)
|
|
||||||
|
|
||||||
|
|
||||||
@register('hsl', 1)
|
|
||||||
def hsl1(color):
|
|
||||||
return rgba2(color, a=Number(1))
|
|
||||||
|
|
||||||
|
|
||||||
@register('mix', 2)
|
|
||||||
@register('mix', 3)
|
|
||||||
def mix(color1, color2, weight=Number(50, "%")):
|
|
||||||
"""
|
|
||||||
Mixes together two colors. Specifically, takes the average of each of the
|
|
||||||
RGB components, optionally weighted by the given percentage.
|
|
||||||
The opacity of the colors is also considered when weighting the components.
|
|
||||||
|
|
||||||
Specifically, takes the average of each of the RGB components,
|
|
||||||
optionally weighted by the given percentage.
|
|
||||||
The opacity of the colors is also considered when weighting the components.
|
|
||||||
|
|
||||||
The weight specifies the amount of the first color that should be included
|
|
||||||
in the returned color.
|
|
||||||
50%, means that half the first color
|
|
||||||
and half the second color should be used.
|
|
||||||
25% means that a quarter of the first color
|
|
||||||
and three quarters of the second color should be used.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
mix(#f00, #00f) => #7f007f
|
|
||||||
mix(#f00, #00f, 25%) => #3f00bf
|
|
||||||
mix(rgba(255, 0, 0, 0.5), #00f) => rgba(63, 0, 191, 0.75)
|
|
||||||
"""
|
|
||||||
# This algorithm factors in both the user-provided weight
|
|
||||||
# and the difference between the alpha values of the two colors
|
|
||||||
# to decide how to perform the weighted average of the two RGB values.
|
|
||||||
#
|
|
||||||
# It works by first normalizing both parameters to be within [-1, 1],
|
|
||||||
# where 1 indicates "only use color1", -1 indicates "only use color 0",
|
|
||||||
# and all values in between indicated a proportionately weighted average.
|
|
||||||
#
|
|
||||||
# Once we have the normalized variables w and a,
|
|
||||||
# we apply the formula (w + a)/(1 + w*a)
|
|
||||||
# to get the combined weight (in [-1, 1]) of color1.
|
|
||||||
# This formula has two especially nice properties:
|
|
||||||
#
|
|
||||||
# * When either w or a are -1 or 1, the combined weight is also that number
|
|
||||||
# (cases where w * a == -1 are undefined, and handled as a special case).
|
|
||||||
#
|
|
||||||
# * When a is 0, the combined weight is w, and vice versa
|
|
||||||
#
|
|
||||||
# Finally, the weight of color1 is renormalized to be within [0, 1]
|
|
||||||
# and the weight of color2 is given by 1 minus the weight of color1.
|
|
||||||
#
|
|
||||||
# Algorithm from the Sass project: http://sass-lang.com/
|
|
||||||
|
|
||||||
p = _interpret_percentage(weight)
|
|
||||||
|
|
||||||
# Scale weight to [-1, 1]
|
|
||||||
w = p * 2 - 1
|
|
||||||
# Compute difference in alpha channels
|
|
||||||
a = color1.alpha - color2.alpha
|
|
||||||
|
|
||||||
# Weight of first color
|
|
||||||
if w * a == -1:
|
|
||||||
# Avoid zero-div case
|
|
||||||
scaled_weight1 = w
|
|
||||||
else:
|
|
||||||
scaled_weight1 = (w + a) / (1 + w * a)
|
|
||||||
|
|
||||||
# Unscale back to [0, 1] and get the weight of the other color
|
|
||||||
w1 = (scaled_weight1 + 1) / 2
|
|
||||||
w2 = 1 - w1
|
|
||||||
|
|
||||||
# Do the scaling. Note that alpha isn't scaled by alpha, as that wouldn't
|
|
||||||
# make much sense; it uses the original untwiddled weight, p.
|
|
||||||
channels = [
|
|
||||||
ch1 * w1 + ch2 * w2
|
|
||||||
for (ch1, ch2) in zip(color1.rgba[:3], color2.rgba[:3])]
|
|
||||||
alpha = color1.alpha * p + color2.alpha * (1 - p)
|
|
||||||
return Color.from_rgb(*channels, alpha=alpha)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Color inspection
|
|
||||||
|
|
||||||
@register('red', 1)
|
|
||||||
def red(color):
|
|
||||||
r, g, b, a = color.rgba
|
|
||||||
return Number(r * 255)
|
|
||||||
|
|
||||||
|
|
||||||
@register('green', 1)
|
|
||||||
def green(color):
|
|
||||||
r, g, b, a = color.rgba
|
|
||||||
return Number(g * 255)
|
|
||||||
|
|
||||||
|
|
||||||
@register('blue', 1)
|
|
||||||
def blue(color):
|
|
||||||
r, g, b, a = color.rgba
|
|
||||||
return Number(b * 255)
|
|
||||||
|
|
||||||
|
|
||||||
@register('opacity', 1)
|
|
||||||
@register('alpha', 1)
|
|
||||||
def alpha(color):
|
|
||||||
return Number(color.alpha)
|
|
||||||
|
|
||||||
|
|
||||||
@register('hue', 1)
|
|
||||||
def hue(color):
|
|
||||||
h, s, l = color.hsl
|
|
||||||
return Number(h * 360, "deg")
|
|
||||||
|
|
||||||
|
|
||||||
@register('saturation', 1)
|
|
||||||
def saturation(color):
|
|
||||||
h, s, l = color.hsl
|
|
||||||
return Number(s * 100, "%")
|
|
||||||
|
|
||||||
|
|
||||||
@register('lightness', 1)
|
|
||||||
def lightness(color):
|
|
||||||
h, s, l = color.hsl
|
|
||||||
return Number(l * 100, "%")
|
|
||||||
|
|
||||||
|
|
||||||
@register('ie-hex-str', 1)
|
|
||||||
def ie_hex_str(color):
|
|
||||||
c = Color(color).value
|
|
||||||
return String(u'#%02X%02X%02X%02X' % (round(c[3] * 255), round(c[0]), round(c[1]), round(c[2])))
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Color modification
|
|
||||||
|
|
||||||
@register('fade-in', 2)
|
|
||||||
@register('fadein', 2)
|
|
||||||
@register('opacify', 2)
|
|
||||||
def opacify(color, amount):
|
|
||||||
r, g, b, a = color.rgba
|
|
||||||
return Color.from_rgb(
|
|
||||||
r, g, b,
|
|
||||||
alpha=color.alpha + amount.value)
|
|
||||||
|
|
||||||
|
|
||||||
@register('fade-out', 2)
|
|
||||||
@register('fadeout', 2)
|
|
||||||
@register('transparentize', 2)
|
|
||||||
def transparentize(color, amount):
|
|
||||||
r, g, b, a = color.rgba
|
|
||||||
return Color.from_rgb(
|
|
||||||
r, g, b,
|
|
||||||
alpha=color.alpha - amount.value)
|
|
||||||
|
|
||||||
|
|
||||||
@register('lighten', 2)
|
|
||||||
def lighten(color, amount):
|
|
||||||
return adjust_color(color, lightness=amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('darken', 2)
|
|
||||||
def darken(color, amount):
|
|
||||||
return adjust_color(color, lightness=-amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('saturate', 2)
|
|
||||||
def saturate(color, amount):
|
|
||||||
return adjust_color(color, saturation=amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('desaturate', 2)
|
|
||||||
def desaturate(color, amount):
|
|
||||||
return adjust_color(color, saturation=-amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('greyscale', 1)
|
|
||||||
def greyscale(color):
|
|
||||||
h, s, l = color.hsl
|
|
||||||
return Color.from_hsl(h, 0, l, alpha=color.alpha)
|
|
||||||
|
|
||||||
|
|
||||||
@register('grayscale', 1)
|
|
||||||
def grayscale(color):
|
|
||||||
if isinstance(color, Number) and color.is_unitless:
|
|
||||||
# grayscale(n) is a CSS3 filter and should be left intact, but only
|
|
||||||
# when using the "a" spelling
|
|
||||||
return String.unquoted("grayscale(%d)" % (color.value,))
|
|
||||||
else:
|
|
||||||
return greyscale(color)
|
|
||||||
|
|
||||||
|
|
||||||
@register('spin', 2)
|
|
||||||
@register('adjust-hue', 2)
|
|
||||||
def adjust_hue(color, degrees):
|
|
||||||
h, s, l = color.hsl
|
|
||||||
delta = degrees.value / 360
|
|
||||||
return Color.from_hsl((h + delta) % 1, s, l, alpha=color.alpha)
|
|
||||||
|
|
||||||
|
|
||||||
@register('complement', 1)
|
|
||||||
def complement(color):
|
|
||||||
h, s, l = color.hsl
|
|
||||||
return Color.from_hsl((h + 0.5) % 1, s, l, alpha=color.alpha)
|
|
||||||
|
|
||||||
|
|
||||||
@register('invert', 1)
|
|
||||||
def invert(color):
|
|
||||||
"""
|
|
||||||
Returns the inverse (negative) of a color.
|
|
||||||
The red, green, and blue values are inverted, while the opacity is left alone.
|
|
||||||
"""
|
|
||||||
r, g, b, a = color.rgba
|
|
||||||
return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a)
|
|
||||||
|
|
||||||
|
|
||||||
@register('adjust-lightness', 2)
|
|
||||||
def adjust_lightness(color, amount):
|
|
||||||
return adjust_color(color, lightness=amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('adjust-saturation', 2)
|
|
||||||
def adjust_saturation(color, amount):
|
|
||||||
return adjust_color(color, saturation=amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('scale-lightness', 2)
|
|
||||||
def scale_lightness(color, amount):
|
|
||||||
return scale_color(color, lightness=amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('scale-saturation', 2)
|
|
||||||
def scale_saturation(color, amount):
|
|
||||||
return scale_color(color, saturation=amount)
|
|
||||||
|
|
||||||
|
|
||||||
@register('adjust-color')
|
|
||||||
def adjust_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None):
|
|
||||||
do_rgb = red or green or blue
|
|
||||||
do_hsl = hue or saturation or lightness
|
|
||||||
if do_rgb and do_hsl:
|
|
||||||
raise ValueError("Can't adjust both RGB and HSL channels at the same time")
|
|
||||||
|
|
||||||
zero = Number(0)
|
|
||||||
a = color.alpha + (alpha or zero).value
|
|
||||||
|
|
||||||
if do_rgb:
|
|
||||||
r, g, b = color.rgba[:3]
|
|
||||||
channels = [
|
|
||||||
current + (adjustment or zero).value / 255
|
|
||||||
for (current, adjustment) in zip(color.rgba, (red, green, blue))]
|
|
||||||
return Color.from_rgb(*channels, alpha=a)
|
|
||||||
|
|
||||||
else:
|
|
||||||
h, s, l = color.hsl
|
|
||||||
h = (h + (hue or zero).value / 360) % 1
|
|
||||||
s += _interpret_percentage(saturation or zero, relto=100, clamp=False)
|
|
||||||
l += _interpret_percentage(lightness or zero, relto=100, clamp=False)
|
|
||||||
return Color.from_hsl(h, s, l, a)
|
|
||||||
|
|
||||||
|
|
||||||
def _scale_channel(channel, scaleby):
|
|
||||||
if scaleby is None:
|
|
||||||
return channel
|
|
||||||
|
|
||||||
expect_type(scaleby, Number)
|
|
||||||
if not scaleby.is_simple_unit('%'):
|
|
||||||
raise ValueError("Expected percentage, got %r" % (scaleby,))
|
|
||||||
|
|
||||||
factor = scaleby.value / 100
|
|
||||||
if factor > 0:
|
|
||||||
# Add x% of the remaining range, up to 1
|
|
||||||
return channel + (1 - channel) * factor
|
|
||||||
else:
|
|
||||||
# Subtract x% of the existing channel. We add here because the factor
|
|
||||||
# is already negative
|
|
||||||
return channel * (1 + factor)
|
|
||||||
|
|
||||||
|
|
||||||
@register('scale-color')
|
|
||||||
def scale_color(color, red=None, green=None, blue=None, saturation=None, lightness=None, alpha=None):
|
|
||||||
do_rgb = red or green or blue
|
|
||||||
do_hsl = saturation or lightness
|
|
||||||
if do_rgb and do_hsl:
|
|
||||||
raise ValueError("Can't scale both RGB and HSL channels at the same time")
|
|
||||||
|
|
||||||
scaled_alpha = _scale_channel(color.alpha, alpha)
|
|
||||||
|
|
||||||
if do_rgb:
|
|
||||||
channels = [
|
|
||||||
_scale_channel(channel, scaleby)
|
|
||||||
for channel, scaleby in zip(color.rgba, (red, green, blue))]
|
|
||||||
return Color.from_rgb(*channels, alpha=scaled_alpha)
|
|
||||||
|
|
||||||
else:
|
|
||||||
channels = [
|
|
||||||
_scale_channel(channel, scaleby)
|
|
||||||
for channel, scaleby in zip(color.hsl, (None, saturation, lightness))]
|
|
||||||
return Color.from_hsl(*channels, alpha=scaled_alpha)
|
|
||||||
|
|
||||||
|
|
||||||
@register('change-color')
|
|
||||||
def change_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None):
|
|
||||||
do_rgb = red or green or blue
|
|
||||||
do_hsl = hue or saturation or lightness
|
|
||||||
if do_rgb and do_hsl:
|
|
||||||
raise ValueError("Can't change both RGB and HSL channels at the same time")
|
|
||||||
|
|
||||||
if alpha is None:
|
|
||||||
alpha = color.alpha
|
|
||||||
else:
|
|
||||||
alpha = alpha.value
|
|
||||||
|
|
||||||
if do_rgb:
|
|
||||||
channels = list(color.rgba[:3])
|
|
||||||
if red:
|
|
||||||
channels[0] = _interpret_percentage(red, relto=255)
|
|
||||||
if green:
|
|
||||||
channels[1] = _interpret_percentage(green, relto=255)
|
|
||||||
if blue:
|
|
||||||
channels[2] = _interpret_percentage(blue, relto=255)
|
|
||||||
|
|
||||||
return Color.from_rgb(*channels, alpha=alpha)
|
|
||||||
|
|
||||||
else:
|
|
||||||
channels = list(color.hsl)
|
|
||||||
if hue:
|
|
||||||
expect_type(hue, Number, unit=None)
|
|
||||||
channels[0] = (hue.value / 360) % 1
|
|
||||||
# Ruby sass treats plain numbers for saturation and lightness as though
|
|
||||||
# they were percentages, just without the %
|
|
||||||
if saturation:
|
|
||||||
channels[1] = _interpret_percentage(saturation, relto=100)
|
|
||||||
if lightness:
|
|
||||||
channels[2] = _interpret_percentage(lightness, relto=100)
|
|
||||||
|
|
||||||
return Color.from_hsl(*channels, alpha=alpha)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# String functions
|
|
||||||
|
|
||||||
@register('e', 1)
|
|
||||||
@register('escape', 1)
|
|
||||||
@register('unquote')
|
|
||||||
def unquote(*args):
|
|
||||||
arg = List.from_maybe_starargs(args).maybe()
|
|
||||||
|
|
||||||
if isinstance(arg, String):
|
|
||||||
return String(arg.value, quotes=None)
|
|
||||||
else:
|
|
||||||
return String(arg.render(), quotes=None)
|
|
||||||
|
|
||||||
|
|
||||||
@register('quote')
|
|
||||||
def quote(*args):
|
|
||||||
arg = List.from_maybe_starargs(args).maybe()
|
|
||||||
|
|
||||||
if isinstance(arg, String):
|
|
||||||
return String(arg.value, quotes='"')
|
|
||||||
else:
|
|
||||||
return String(arg.render(), quotes='"')
|
|
||||||
|
|
||||||
|
|
||||||
@register('str-length', 1)
|
|
||||||
def str_length(string):
|
|
||||||
expect_type(string, String)
|
|
||||||
|
|
||||||
# nb: can't use `len(string)`, because that gives the Sass list length,
|
|
||||||
# which is 1
|
|
||||||
return Number(len(string.value))
|
|
||||||
|
|
||||||
|
|
||||||
# TODO this and several others should probably also require integers
|
|
||||||
# TODO and assert that the indexes are valid
|
|
||||||
@register('str-insert', 3)
|
|
||||||
def str_insert(string, insert, index):
|
|
||||||
expect_type(string, String)
|
|
||||||
expect_type(insert, String)
|
|
||||||
expect_type(index, Number, unit=None)
|
|
||||||
|
|
||||||
py_index = index.to_python_index(len(string.value), check_bounds=False)
|
|
||||||
return String(
|
|
||||||
string.value[:py_index] +
|
|
||||||
insert.value +
|
|
||||||
string.value[py_index:],
|
|
||||||
quotes=string.quotes)
|
|
||||||
|
|
||||||
|
|
||||||
@register('str-index', 2)
|
|
||||||
def str_index(string, substring):
|
|
||||||
expect_type(string, String)
|
|
||||||
expect_type(substring, String)
|
|
||||||
|
|
||||||
# 1-based indexing, with 0 for failure
|
|
||||||
return Number(string.value.find(substring.value) + 1)
|
|
||||||
|
|
||||||
|
|
||||||
@register('str-slice', 2)
|
|
||||||
@register('str-slice', 3)
|
|
||||||
def str_slice(string, start_at, end_at=None):
|
|
||||||
expect_type(string, String)
|
|
||||||
expect_type(start_at, Number, unit=None)
|
|
||||||
py_start_at = start_at.to_python_index(len(string.value))
|
|
||||||
|
|
||||||
if end_at is None:
|
|
||||||
py_end_at = None
|
|
||||||
else:
|
|
||||||
expect_type(end_at, Number, unit=None)
|
|
||||||
# Endpoint is inclusive, unlike Python
|
|
||||||
py_end_at = end_at.to_python_index(len(string.value)) + 1
|
|
||||||
|
|
||||||
return String(
|
|
||||||
string.value[py_start_at:py_end_at],
|
|
||||||
quotes=string.quotes)
|
|
||||||
|
|
||||||
|
|
||||||
@register('to-upper-case', 1)
|
|
||||||
def to_upper_case(string):
|
|
||||||
expect_type(string, String)
|
|
||||||
|
|
||||||
return String(string.value.upper(), quotes=string.quotes)
|
|
||||||
|
|
||||||
|
|
||||||
@register('to-lower-case', 1)
|
|
||||||
def to_lower_case(string):
|
|
||||||
expect_type(string, String)
|
|
||||||
|
|
||||||
return String(string.value.lower(), quotes=string.quotes)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Number functions
|
|
||||||
|
|
||||||
@register('percentage', 1)
|
|
||||||
def percentage(value):
|
|
||||||
expect_type(value, Number, unit=None)
|
|
||||||
return value * Number(100, unit='%')
|
|
||||||
|
|
||||||
CORE_LIBRARY.add(Number.wrap_python_function(abs), 'abs', 1)
|
|
||||||
CORE_LIBRARY.add(Number.wrap_python_function(round), 'round', 1)
|
|
||||||
CORE_LIBRARY.add(Number.wrap_python_function(math.ceil), 'ceil', 1)
|
|
||||||
CORE_LIBRARY.add(Number.wrap_python_function(math.floor), 'floor', 1)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# List functions
|
|
||||||
|
|
||||||
def __parse_separator(separator, default_from=None):
|
|
||||||
if separator is None:
|
|
||||||
separator = 'auto'
|
|
||||||
separator = String.unquoted(separator).value
|
|
||||||
|
|
||||||
if separator == 'comma':
|
|
||||||
return True
|
|
||||||
elif separator == 'space':
|
|
||||||
return False
|
|
||||||
elif separator == 'auto':
|
|
||||||
if not default_from:
|
|
||||||
return True
|
|
||||||
elif len(default_from) < 2:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return default_from.use_comma
|
|
||||||
else:
|
|
||||||
raise ValueError('Separator must be auto, comma, or space')
|
|
||||||
|
|
||||||
|
|
||||||
# TODO get the compass bit outta here
|
|
||||||
@register('-compass-list-size')
|
|
||||||
@register('length')
|
|
||||||
def _length(*lst):
|
|
||||||
if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)):
|
|
||||||
lst = lst[0]
|
|
||||||
return Number(len(lst))
|
|
||||||
|
|
||||||
|
|
||||||
@register('set-nth', 3)
|
|
||||||
def set_nth(list, n, value):
|
|
||||||
expect_type(n, Number, unit=None)
|
|
||||||
|
|
||||||
py_n = n.to_python_index(len(list))
|
|
||||||
return List(
|
|
||||||
tuple(list[:py_n]) + (value,) + tuple(list[py_n + 1:]),
|
|
||||||
use_comma=list.use_comma)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO get the compass bit outta here
|
|
||||||
@register('-compass-nth', 2)
|
|
||||||
@register('nth', 2)
|
|
||||||
def nth(lst, n):
|
|
||||||
"""Return the nth item in the list."""
|
|
||||||
expect_type(n, (String, Number), unit=None)
|
|
||||||
|
|
||||||
if isinstance(n, String):
|
|
||||||
if n.value.lower() == 'first':
|
|
||||||
i = 0
|
|
||||||
elif n.value.lower() == 'last':
|
|
||||||
i = -1
|
|
||||||
else:
|
|
||||||
raise ValueError("Invalid index %r" % (n,))
|
|
||||||
else:
|
|
||||||
# DEVIATION: nth treats lists as circular lists
|
|
||||||
i = n.to_python_index(len(lst), circular=True)
|
|
||||||
|
|
||||||
return lst[i]
|
|
||||||
|
|
||||||
|
|
||||||
@register('join', 2)
|
|
||||||
@register('join', 3)
|
|
||||||
def join(lst1, lst2, separator=None):
|
|
||||||
ret = []
|
|
||||||
ret.extend(List.from_maybe(lst1))
|
|
||||||
ret.extend(List.from_maybe(lst2))
|
|
||||||
|
|
||||||
use_comma = __parse_separator(separator, default_from=lst1)
|
|
||||||
return List(ret, use_comma=use_comma)
|
|
||||||
|
|
||||||
|
|
||||||
@register('min')
|
|
||||||
def min_(*lst):
|
|
||||||
if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)):
|
|
||||||
lst = lst[0]
|
|
||||||
return min(lst)
|
|
||||||
|
|
||||||
|
|
||||||
@register('max')
|
|
||||||
def max_(*lst):
|
|
||||||
if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)):
|
|
||||||
lst = lst[0]
|
|
||||||
return max(lst)
|
|
||||||
|
|
||||||
|
|
||||||
@register('append', 2)
|
|
||||||
@register('append', 3)
|
|
||||||
def append(lst, val, separator=None):
|
|
||||||
ret = []
|
|
||||||
ret.extend(List.from_maybe(lst))
|
|
||||||
ret.append(val)
|
|
||||||
|
|
||||||
use_comma = __parse_separator(separator, default_from=lst)
|
|
||||||
return List(ret, use_comma=use_comma)
|
|
||||||
|
|
||||||
|
|
||||||
@register('index', 2)
|
|
||||||
def index(lst, val):
|
|
||||||
for i in xrange(len(lst)):
|
|
||||||
if lst.value[i] == val:
|
|
||||||
return Number(i + 1)
|
|
||||||
return Boolean(False)
|
|
||||||
|
|
||||||
|
|
||||||
@register('zip')
|
|
||||||
def zip_(*lists):
|
|
||||||
return List(
|
|
||||||
[List(zipped) for zipped in zip(*lists)],
|
|
||||||
use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO need a way to use "list" as the arg name without shadowing the builtin
|
|
||||||
@register('list-separator', 1)
|
|
||||||
def list_separator(list):
|
|
||||||
if list.use_comma:
|
|
||||||
return String.unquoted('comma')
|
|
||||||
else:
|
|
||||||
return String.unquoted('space')
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Map functions
|
|
||||||
|
|
||||||
@register('map-get', 2)
|
|
||||||
def map_get(map, key):
|
|
||||||
return map.to_dict().get(key, Null())
|
|
||||||
|
|
||||||
|
|
||||||
@register('map-merge', 2)
|
|
||||||
def map_merge(*maps):
|
|
||||||
key_order = []
|
|
||||||
index = {}
|
|
||||||
for map in maps:
|
|
||||||
for key, value in map.to_pairs():
|
|
||||||
if key not in index:
|
|
||||||
key_order.append(key)
|
|
||||||
|
|
||||||
index[key] = value
|
|
||||||
|
|
||||||
pairs = [(key, index[key]) for key in key_order]
|
|
||||||
return Map(pairs, index=index)
|
|
||||||
|
|
||||||
|
|
||||||
@register('map-keys', 1)
|
|
||||||
def map_keys(map):
|
|
||||||
return List(
|
|
||||||
[k for (k, v) in map.to_pairs()],
|
|
||||||
use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('map-values', 1)
|
|
||||||
def map_values(map):
|
|
||||||
return List(
|
|
||||||
[v for (k, v) in map.to_pairs()],
|
|
||||||
use_comma=True)
|
|
||||||
|
|
||||||
|
|
||||||
@register('map-has-key', 2)
|
|
||||||
def map_has_key(map, key):
|
|
||||||
return Boolean(key in map.to_dict())
|
|
||||||
|
|
||||||
|
|
||||||
# DEVIATIONS: these do not exist in ruby sass
|
|
||||||
|
|
||||||
@register('map-get', 3)
|
|
||||||
def map_get3(map, key, default):
|
|
||||||
return map.to_dict().get(key, default)
|
|
||||||
|
|
||||||
|
|
||||||
@register('map-get-nested', 2)
|
|
||||||
@register('map-get-nested', 3)
|
|
||||||
def map_get_nested3(map, keys, default=Null()):
|
|
||||||
for key in keys:
|
|
||||||
map = map.to_dict().get(key, None)
|
|
||||||
if map is None:
|
|
||||||
return default
|
|
||||||
|
|
||||||
return map
|
|
||||||
|
|
||||||
|
|
||||||
@register('map-merge-deep', 2)
|
|
||||||
def map_merge_deep(*maps):
|
|
||||||
pairs = []
|
|
||||||
keys = set()
|
|
||||||
for map in maps:
|
|
||||||
for key, value in map.to_pairs():
|
|
||||||
keys.add(key)
|
|
||||||
|
|
||||||
for key in keys:
|
|
||||||
values = [map.to_dict().get(key, None) for map in maps]
|
|
||||||
values = [v for v in values if v is not None]
|
|
||||||
if all(isinstance(v, Map) for v in values):
|
|
||||||
pairs.append((key, map_merge_deep(*values)))
|
|
||||||
else:
|
|
||||||
pairs.append((key, values[-1]))
|
|
||||||
|
|
||||||
return Map(pairs)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Meta functions
|
|
||||||
|
|
||||||
@register('type-of', 1)
|
|
||||||
def _type_of(obj): # -> bool, number, string, color, list
|
|
||||||
return String(obj.sass_type_name)
|
|
||||||
|
|
||||||
|
|
||||||
@register('unit', 1)
|
|
||||||
def unit(number): # -> px, em, cm, etc.
|
|
||||||
numer = '*'.join(sorted(number.unit_numer))
|
|
||||||
denom = '*'.join(sorted(number.unit_denom))
|
|
||||||
|
|
||||||
if denom:
|
|
||||||
ret = numer + '/' + denom
|
|
||||||
else:
|
|
||||||
ret = numer
|
|
||||||
return String.unquoted(ret)
|
|
||||||
|
|
||||||
|
|
||||||
@register('unitless', 1)
|
|
||||||
def unitless(value):
|
|
||||||
if not isinstance(value, Number):
|
|
||||||
raise TypeError("Expected number, got %r" % (value,))
|
|
||||||
|
|
||||||
return Boolean(value.is_unitless)
|
|
||||||
|
|
||||||
|
|
||||||
@register('comparable', 2)
|
|
||||||
def comparable(number1, number2):
|
|
||||||
left = number1.to_base_units()
|
|
||||||
right = number2.to_base_units()
|
|
||||||
return Boolean(
|
|
||||||
left.unit_numer == right.unit_numer
|
|
||||||
and left.unit_denom == right.unit_denom)
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Miscellaneous
|
|
||||||
|
|
||||||
@register('if', 2)
|
|
||||||
@register('if', 3)
|
|
||||||
def if_(condition, if_true, if_false=Null()):
|
|
||||||
return if_true if condition else if_false
|
|
||||||
@@ -1,471 +0,0 @@
|
|||||||
"""Functions new to the pyScss library."""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import logging
|
|
||||||
import os.path
|
|
||||||
import random
|
|
||||||
|
|
||||||
import six
|
|
||||||
from six.moves import xrange
|
|
||||||
|
|
||||||
from scss import config
|
|
||||||
from scss.functions.library import FunctionLibrary
|
|
||||||
from scss.types import Color, Number, String, List
|
|
||||||
from scss.util import escape
|
|
||||||
|
|
||||||
try:
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import Image
|
|
||||||
import ImageDraw
|
|
||||||
except:
|
|
||||||
Image = None
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
EXTRA_LIBRARY = FunctionLibrary()
|
|
||||||
register = EXTRA_LIBRARY.register
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
|
||||||
# Image stuff
|
|
||||||
def _image_noise(pixdata, size, density=None, intensity=None, color=None, opacity=None, monochrome=None, background=None):
|
|
||||||
if not density:
|
|
||||||
density = [0.8]
|
|
||||||
elif not isinstance(density, (tuple, list)):
|
|
||||||
density = [density]
|
|
||||||
|
|
||||||
if not intensity:
|
|
||||||
intensity = [0.5]
|
|
||||||
elif not isinstance(intensity, (tuple, list)):
|
|
||||||
intensity = [intensity]
|
|
||||||
|
|
||||||
if not color:
|
|
||||||
color = [(0, 0, 0, 0)]
|
|
||||||
elif not isinstance(color, (tuple, list)) or not isinstance(color[0], (tuple, list)):
|
|
||||||
color = [color]
|
|
||||||
|
|
||||||
if not opacity:
|
|
||||||
opacity = [0.2]
|
|
||||||
elif not isinstance(opacity, (tuple, list)):
|
|
||||||
opacity = [opacity]
|
|
||||||
|
|
||||||
if not monochrome:
|
|
||||||
monochrome = [False]
|
|
||||||
elif not isinstance(monochrome, (tuple, list)):
|
|
||||||
monochrome = [monochrome]
|
|
||||||
|
|
||||||
pixels = {}
|
|
||||||
|
|
||||||
if background:
|
|
||||||
for y in xrange(size):
|
|
||||||
for x in xrange(size):
|
|
||||||
ca = float(background[3])
|
|
||||||
pixels[(x, y)] = (background[0] * ca, background[1] * ca, background[2] * ca, ca)
|
|
||||||
|
|
||||||
loops = max(map(len, (density, intensity, color, opacity, monochrome)))
|
|
||||||
for l in range(loops):
|
|
||||||
_density = density[l % len(density)]
|
|
||||||
_intensity = intensity[l % len(intensity)]
|
|
||||||
_color = color[l % len(color)]
|
|
||||||
_opacity = opacity[l % len(opacity)]
|
|
||||||
_monochrome = monochrome[l % len(monochrome)]
|
|
||||||
_intensity = 1 - _intensity
|
|
||||||
if _intensity < 0.5:
|
|
||||||
cx = 255 * _intensity
|
|
||||||
cm = cx
|
|
||||||
else:
|
|
||||||
cx = 255 * (1 - _intensity)
|
|
||||||
cm = 255 * _intensity
|
|
||||||
xa = int(cm - cx)
|
|
||||||
xb = int(cm + cx)
|
|
||||||
if xa > 0:
|
|
||||||
xa &= 255
|
|
||||||
else:
|
|
||||||
xa = 0
|
|
||||||
if xb > 0:
|
|
||||||
xb &= 255
|
|
||||||
else:
|
|
||||||
xb = 0
|
|
||||||
r, g, b, a = _color
|
|
||||||
for i in xrange(int(round(_density * size ** 2))):
|
|
||||||
x = random.randint(1, size)
|
|
||||||
y = random.randint(1, size)
|
|
||||||
cc = random.randint(xa, xb)
|
|
||||||
cr = (cc) * (1 - a) + a * r
|
|
||||||
cg = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * g
|
|
||||||
cb = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * b
|
|
||||||
ca = random.random() * _opacity
|
|
||||||
ica = 1 - ca
|
|
||||||
pos = (x - 1, y - 1)
|
|
||||||
dst = pixels.get(pos, (0, 0, 0, 0))
|
|
||||||
src = (cr * ca, cg * ca, cb * ca, ca)
|
|
||||||
pixels[pos] = (src[0] + dst[0] * ica, src[1] + dst[1] * ica, src[2] + dst[2] * ica, src[3] + dst[3] * ica)
|
|
||||||
|
|
||||||
for pos, col in pixels.items():
|
|
||||||
ca = col[3]
|
|
||||||
if ca:
|
|
||||||
pixdata[pos] = tuple(int(round(c)) for c in (col[0] / ca, col[1] / ca, col[2] / ca, ca * 255))
|
|
||||||
|
|
||||||
|
|
||||||
def _image_brushed(pixdata, size, density=None, intensity=None, color=None, opacity=None, monochrome=None, direction=None, spread=None, background=None):
|
|
||||||
if not density:
|
|
||||||
density = [0.8]
|
|
||||||
elif not isinstance(density, (tuple, list)):
|
|
||||||
density = [density]
|
|
||||||
|
|
||||||
if not intensity:
|
|
||||||
intensity = [0.5]
|
|
||||||
elif not isinstance(intensity, (tuple, list)):
|
|
||||||
intensity = [intensity]
|
|
||||||
|
|
||||||
if not color:
|
|
||||||
color = [(0, 0, 0, 0)]
|
|
||||||
elif not isinstance(color, (tuple, list)) or not isinstance(color[0], (tuple, list)):
|
|
||||||
color = [color]
|
|
||||||
|
|
||||||
if not opacity:
|
|
||||||
opacity = [0.2]
|
|
||||||
elif not isinstance(opacity, (tuple, list)):
|
|
||||||
opacity = [opacity]
|
|
||||||
|
|
||||||
if not monochrome:
|
|
||||||
monochrome = [False]
|
|
||||||
elif not isinstance(monochrome, (tuple, list)):
|
|
||||||
monochrome = [monochrome]
|
|
||||||
|
|
||||||
if not direction:
|
|
||||||
direction = [0]
|
|
||||||
elif not isinstance(direction, (tuple, list)):
|
|
||||||
direction = [direction]
|
|
||||||
|
|
||||||
if not spread:
|
|
||||||
spread = [0]
|
|
||||||
elif not isinstance(spread, (tuple, list)):
|
|
||||||
spread = [spread]
|
|
||||||
|
|
||||||
def ppgen(d):
|
|
||||||
if d is None:
|
|
||||||
return
|
|
||||||
d = d % 4
|
|
||||||
if d == 0:
|
|
||||||
pp = lambda x, y, o: ((x - o) % size, y)
|
|
||||||
elif d == 1:
|
|
||||||
pp = lambda x, y, o: ((x - o) % size, (y + x - o) % size)
|
|
||||||
elif d == 2:
|
|
||||||
pp = lambda x, y, o: (y, (x - o) % size)
|
|
||||||
else:
|
|
||||||
pp = lambda x, y, o: ((x - o) % size, (y - x - o) % size)
|
|
||||||
return pp
|
|
||||||
|
|
||||||
pixels = {}
|
|
||||||
|
|
||||||
if background:
|
|
||||||
for y in xrange(size):
|
|
||||||
for x in xrange(size):
|
|
||||||
ca = float(background[3])
|
|
||||||
pixels[(x, y)] = (background[0] * ca, background[1] * ca, background[2] * ca, ca)
|
|
||||||
|
|
||||||
loops = max(map(len, (density, intensity, color, opacity, monochrome, direction, spread)))
|
|
||||||
for l in range(loops):
|
|
||||||
_density = density[l % len(density)]
|
|
||||||
_intensity = intensity[l % len(intensity)]
|
|
||||||
_color = color[l % len(color)]
|
|
||||||
_opacity = opacity[l % len(opacity)]
|
|
||||||
_monochrome = monochrome[l % len(monochrome)]
|
|
||||||
_direction = direction[l % len(direction)]
|
|
||||||
_spread = spread[l % len(spread)]
|
|
||||||
_intensity = 1 - _intensity
|
|
||||||
if _intensity < 0.5:
|
|
||||||
cx = 255 * _intensity
|
|
||||||
cm = cx
|
|
||||||
else:
|
|
||||||
cx = 255 * (1 - _intensity)
|
|
||||||
cm = 255 * _intensity
|
|
||||||
xa = int(cm - cx)
|
|
||||||
xb = int(cm + cx)
|
|
||||||
if xa > 0:
|
|
||||||
xa &= 255
|
|
||||||
else:
|
|
||||||
xa = 0
|
|
||||||
if xb > 0:
|
|
||||||
xb &= 255
|
|
||||||
else:
|
|
||||||
xb = 0
|
|
||||||
r, g, b, a = _color
|
|
||||||
pp = ppgen(_direction)
|
|
||||||
if pp:
|
|
||||||
for y in xrange(size):
|
|
||||||
if _spread and (y + (l % 2)) % _spread:
|
|
||||||
continue
|
|
||||||
o = random.randint(1, size)
|
|
||||||
cc = random.randint(xa, xb)
|
|
||||||
cr = (cc) * (1 - a) + a * r
|
|
||||||
cg = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * g
|
|
||||||
cb = (cc if _monochrome else random.randint(xa, xb)) * (1 - a) + a * b
|
|
||||||
da = random.randint(0, 255) * _opacity
|
|
||||||
ip = round((size / 2.0 * _density) / int(1 / _density))
|
|
||||||
iq = round((size / 2.0 * (1 - _density)) / int(1 / _density))
|
|
||||||
if ip:
|
|
||||||
i = da / ip
|
|
||||||
aa = 0
|
|
||||||
else:
|
|
||||||
i = 0
|
|
||||||
aa = da
|
|
||||||
d = 0
|
|
||||||
p = ip
|
|
||||||
for x in xrange(size):
|
|
||||||
if d == 0:
|
|
||||||
if p > 0:
|
|
||||||
p -= 1
|
|
||||||
aa += i
|
|
||||||
else:
|
|
||||||
d = 1
|
|
||||||
q = iq
|
|
||||||
elif d == 1:
|
|
||||||
if q > 0:
|
|
||||||
q -= 1
|
|
||||||
else:
|
|
||||||
d = 2
|
|
||||||
p = ip
|
|
||||||
elif d == 2:
|
|
||||||
if p > 0:
|
|
||||||
p -= 1
|
|
||||||
aa -= i
|
|
||||||
else:
|
|
||||||
d = 3
|
|
||||||
q = iq
|
|
||||||
elif d == 3:
|
|
||||||
if q > 0:
|
|
||||||
q -= 1
|
|
||||||
else:
|
|
||||||
d = 0
|
|
||||||
p = ip
|
|
||||||
if aa > 0:
|
|
||||||
ca = aa / 255.0
|
|
||||||
else:
|
|
||||||
ca = 0.0
|
|
||||||
ica = 1 - ca
|
|
||||||
pos = pp(x, y, o)
|
|
||||||
dst = pixels.get(pos, (0, 0, 0, 0))
|
|
||||||
src = (cr * ca, cg * ca, cb * ca, ca)
|
|
||||||
pixels[pos] = (src[0] + dst[0] * ica, src[1] + dst[1] * ica, src[2] + dst[2] * ica, src[3] + dst[3] * ica)
|
|
||||||
|
|
||||||
for pos, col in pixels.items():
|
|
||||||
ca = col[3]
|
|
||||||
if ca:
|
|
||||||
pixdata[pos] = tuple(int(round(c)) for c in (col[0] / ca, col[1] / ca, col[2] / ca, ca * 255))
|
|
||||||
|
|
||||||
|
|
||||||
@register('background-noise', 0)
|
|
||||||
@register('background-noise', 1)
|
|
||||||
@register('background-noise', 2)
|
|
||||||
@register('background-noise', 3)
|
|
||||||
@register('background-noise', 4)
|
|
||||||
@register('background-noise', 5)
|
|
||||||
@register('background-noise', 6)
|
|
||||||
@register('background-noise', 7)
|
|
||||||
def background_noise(density=None, opacity=None, size=None, monochrome=False, intensity=(), color=None, background=None, inline=False):
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
|
|
||||||
density = [Number(v).value for v in List.from_maybe(density)]
|
|
||||||
intensity = [Number(v).value for v in List.from_maybe(intensity)]
|
|
||||||
color = [Color(v).value for v in List.from_maybe(color) if v]
|
|
||||||
opacity = [Number(v).value for v in List.from_maybe(opacity)]
|
|
||||||
|
|
||||||
size = int(Number(size).value) if size else 0
|
|
||||||
if size < 1 or size > 512:
|
|
||||||
size = 200
|
|
||||||
|
|
||||||
monochrome = bool(monochrome)
|
|
||||||
|
|
||||||
background = Color(background).value if background else None
|
|
||||||
|
|
||||||
new_image = Image.new(
|
|
||||||
mode='RGBA',
|
|
||||||
size=(size, size)
|
|
||||||
)
|
|
||||||
|
|
||||||
pixdata = new_image.load()
|
|
||||||
_image_noise(pixdata, size, density, intensity, color, opacity, monochrome)
|
|
||||||
|
|
||||||
if not inline:
|
|
||||||
key = (size, density, intensity, color, opacity, monochrome)
|
|
||||||
asset_file = 'noise-%s%sx%s' % ('mono-' if monochrome else '', size, size)
|
|
||||||
# asset_file += '-[%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or []))
|
|
||||||
asset_file += '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
|
||||||
asset_file += '.png'
|
|
||||||
asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file)
|
|
||||||
try:
|
|
||||||
new_image.save(asset_path)
|
|
||||||
except IOError:
|
|
||||||
log.exception("Error while saving image")
|
|
||||||
inline = True # Retry inline version
|
|
||||||
url = '%s%s' % (config.ASSETS_URL, asset_file)
|
|
||||||
if inline:
|
|
||||||
output = six.BytesIO()
|
|
||||||
new_image.save(output, format='PNG')
|
|
||||||
contents = output.getvalue()
|
|
||||||
output.close()
|
|
||||||
url = 'data:image/png;base64,' + base64.b64encode(contents)
|
|
||||||
|
|
||||||
inline = 'url("%s")' % escape(url)
|
|
||||||
return String.unquoted(inline)
|
|
||||||
|
|
||||||
|
|
||||||
@register('background-brushed', 0)
|
|
||||||
@register('background-brushed', 1)
|
|
||||||
@register('background-brushed', 2)
|
|
||||||
@register('background-brushed', 3)
|
|
||||||
@register('background-brushed', 4)
|
|
||||||
@register('background-brushed', 5)
|
|
||||||
@register('background-brushed', 6)
|
|
||||||
@register('background-brushed', 7)
|
|
||||||
@register('background-brushed', 8)
|
|
||||||
@register('background-brushed', 9)
|
|
||||||
def background_brushed(density=None, intensity=None, color=None, opacity=None, size=None, monochrome=False, direction=(), spread=(), background=None, inline=False):
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
|
|
||||||
density = [Number(v).value for v in List.from_maybe(density)]
|
|
||||||
intensity = [Number(v).value for v in List.from_maybe(intensity)]
|
|
||||||
color = [Color(v).value for v in List.from_maybe(color) if v]
|
|
||||||
opacity = [Number(v).value for v in List.from_maybe(opacity)]
|
|
||||||
|
|
||||||
size = int(Number(size).value) if size else -1
|
|
||||||
if size < 0 or size > 512:
|
|
||||||
size = 200
|
|
||||||
|
|
||||||
monochrome = bool(monochrome)
|
|
||||||
|
|
||||||
direction = [Number(v).value for v in List.from_maybe(direction)]
|
|
||||||
spread = [Number(v).value for v in List.from_maybe(spread)]
|
|
||||||
|
|
||||||
background = Color(background).value if background else None
|
|
||||||
|
|
||||||
new_image = Image.new(
|
|
||||||
mode='RGBA',
|
|
||||||
size=(size, size)
|
|
||||||
)
|
|
||||||
|
|
||||||
pixdata = new_image.load()
|
|
||||||
_image_brushed(pixdata, size, density, intensity, color, opacity, monochrome, direction, spread, background)
|
|
||||||
|
|
||||||
if not inline:
|
|
||||||
key = (size, density, intensity, color, opacity, monochrome, direction, spread, background)
|
|
||||||
asset_file = 'brushed-%s%sx%s' % ('mono-' if monochrome else '', size, size)
|
|
||||||
# asset_file += '-[%s][%s][%s]' % ('-'.join(to_str(s).replace('.', '_') for s in density or []), '-'.join(to_str(s).replace('.', '_') for s in opacity or []), '-'.join(to_str(s).replace('.', '_') for s in direction or []))
|
|
||||||
asset_file += '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
|
||||||
asset_file += '.png'
|
|
||||||
asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file)
|
|
||||||
try:
|
|
||||||
new_image.save(asset_path)
|
|
||||||
except IOError:
|
|
||||||
log.exception("Error while saving image")
|
|
||||||
inline = True # Retry inline version
|
|
||||||
url = '%s%s' % (config.ASSETS_URL, asset_file)
|
|
||||||
if inline:
|
|
||||||
output = six.BytesIO()
|
|
||||||
new_image.save(output, format='PNG')
|
|
||||||
contents = output.getvalue()
|
|
||||||
output.close()
|
|
||||||
url = 'data:image/png;base64,' + base64.b64encode(contents)
|
|
||||||
|
|
||||||
inline = 'url("%s")' % escape(url)
|
|
||||||
return String.unquoted(inline)
|
|
||||||
|
|
||||||
|
|
||||||
@register('grid-image', 4)
|
|
||||||
@register('grid-image', 5)
|
|
||||||
def _grid_image(left_gutter, width, right_gutter, height, columns=1, grid_color=None, baseline_color=None, background_color=None, inline=False):
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
if grid_color is None:
|
|
||||||
grid_color = (120, 170, 250, 15)
|
|
||||||
else:
|
|
||||||
c = Color(grid_color).value
|
|
||||||
grid_color = (c[0], c[1], c[2], int(c[3] * 255.0))
|
|
||||||
if baseline_color is None:
|
|
||||||
baseline_color = (120, 170, 250, 30)
|
|
||||||
else:
|
|
||||||
c = Color(baseline_color).value
|
|
||||||
baseline_color = (c[0], c[1], c[2], int(c[3] * 255.0))
|
|
||||||
if background_color is None:
|
|
||||||
background_color = (0, 0, 0, 0)
|
|
||||||
else:
|
|
||||||
c = Color(background_color).value
|
|
||||||
background_color = (c[0], c[1], c[2], int(c[3] * 255.0))
|
|
||||||
_height = int(height) if height >= 1 else int(height * 1000.0)
|
|
||||||
_width = int(width) if width >= 1 else int(width * 1000.0)
|
|
||||||
_left_gutter = int(left_gutter) if left_gutter >= 1 else int(left_gutter * 1000.0)
|
|
||||||
_right_gutter = int(right_gutter) if right_gutter >= 1 else int(right_gutter * 1000.0)
|
|
||||||
if _height <= 0 or _width <= 0 or _left_gutter <= 0 or _right_gutter <= 0:
|
|
||||||
raise ValueError
|
|
||||||
_full_width = (_left_gutter + _width + _right_gutter)
|
|
||||||
new_image = Image.new(
|
|
||||||
mode='RGBA',
|
|
||||||
size=(_full_width * int(columns), _height),
|
|
||||||
color=background_color
|
|
||||||
)
|
|
||||||
draw = ImageDraw.Draw(new_image)
|
|
||||||
for i in range(int(columns)):
|
|
||||||
draw.rectangle((i * _full_width + _left_gutter, 0, i * _full_width + _left_gutter + _width - 1, _height - 1), fill=grid_color)
|
|
||||||
if _height > 1:
|
|
||||||
draw.rectangle((0, _height - 1, _full_width * int(columns) - 1, _height - 1), fill=baseline_color)
|
|
||||||
if not inline:
|
|
||||||
grid_name = 'grid_'
|
|
||||||
if left_gutter:
|
|
||||||
grid_name += str(int(left_gutter)) + '+'
|
|
||||||
grid_name += str(int(width))
|
|
||||||
if right_gutter:
|
|
||||||
grid_name += '+' + str(int(right_gutter))
|
|
||||||
if height and height > 1:
|
|
||||||
grid_name += 'x' + str(int(height))
|
|
||||||
key = (columns, grid_color, baseline_color, background_color)
|
|
||||||
key = grid_name + '-' + base64.urlsafe_b64encode(hashlib.md5(repr(key)).digest()).rstrip('=').replace('-', '_')
|
|
||||||
asset_file = key + '.png'
|
|
||||||
asset_path = os.path.join(config.ASSETS_ROOT or os.path.join(config.STATIC_ROOT, 'assets'), asset_file)
|
|
||||||
try:
|
|
||||||
new_image.save(asset_path)
|
|
||||||
except IOError:
|
|
||||||
log.exception("Error while saving image")
|
|
||||||
inline = True # Retry inline version
|
|
||||||
url = '%s%s' % (config.ASSETS_URL, asset_file)
|
|
||||||
if inline:
|
|
||||||
output = six.BytesIO()
|
|
||||||
new_image.save(output, format='PNG')
|
|
||||||
contents = output.getvalue()
|
|
||||||
output.close()
|
|
||||||
url = 'data:image/png;base64,' + base64.b64encode(contents)
|
|
||||||
inline = 'url("%s")' % escape(url)
|
|
||||||
return String.unquoted(inline)
|
|
||||||
|
|
||||||
|
|
||||||
@register('image-color', 1)
|
|
||||||
@register('image-color', 2)
|
|
||||||
@register('image-color', 3)
|
|
||||||
def image_color(color, width=1, height=1):
|
|
||||||
if not Image:
|
|
||||||
raise Exception("Images manipulation require PIL")
|
|
||||||
w = int(Number(width).value)
|
|
||||||
h = int(Number(height).value)
|
|
||||||
if w <= 0 or h <= 0:
|
|
||||||
raise ValueError
|
|
||||||
new_image = Image.new(
|
|
||||||
mode='RGB' if color.alpha == 1 else 'RGBA',
|
|
||||||
size=(w, h),
|
|
||||||
color=color.rgba255,
|
|
||||||
)
|
|
||||||
output = six.BytesIO()
|
|
||||||
new_image.save(output, format='PNG')
|
|
||||||
contents = output.getvalue()
|
|
||||||
output.close()
|
|
||||||
mime_type = 'image/png'
|
|
||||||
url = 'data:' + mime_type + ';base64,' + base64.b64encode(contents)
|
|
||||||
inline = 'url("%s")' % escape(url)
|
|
||||||
return String.unquoted(inline)
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
"""Defines `FunctionLibrary`, a class that collects functions to be exposed to
|
|
||||||
SCSS during compilation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO the constant versions of this should be frozen
|
|
||||||
class FunctionLibrary(object):
|
|
||||||
"""Contains a set of functions to be exposed to SCSS.
|
|
||||||
|
|
||||||
Functions are keyed by both their name _and_ arity; this allows for
|
|
||||||
function overloading somewhat beyond what Python easily allows, and is
|
|
||||||
required for several functions in the standard Sass library.
|
|
||||||
|
|
||||||
Functions may also have arbitrary arity, in which case they accept any
|
|
||||||
number of arguments, though a function of the same name with a specific
|
|
||||||
arity takes precedence.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._functions = {}
|
|
||||||
|
|
||||||
def derive(self):
|
|
||||||
"""Returns a new registry object, pre-populated with all the functions
|
|
||||||
in this registry.
|
|
||||||
"""
|
|
||||||
new = type(self)()
|
|
||||||
new.inherit(self)
|
|
||||||
return new
|
|
||||||
|
|
||||||
def inherit(self, *other_libraries):
|
|
||||||
"""Import all the functions from the given other libraries into this one.
|
|
||||||
|
|
||||||
Note that existing functions ARE NOT replaced -- which also means that
|
|
||||||
functions from the first library take precedence over functions from
|
|
||||||
the second library, etc.
|
|
||||||
"""
|
|
||||||
new_functions = dict()
|
|
||||||
|
|
||||||
# dict.update replaces keys; go through the other libraries in reverse,
|
|
||||||
# so the first one wins
|
|
||||||
for library in reversed(other_libraries):
|
|
||||||
new_functions.update(library._functions)
|
|
||||||
|
|
||||||
new_functions.update(self._functions)
|
|
||||||
self._functions = new_functions
|
|
||||||
|
|
||||||
def register(self, name, argc=None):
|
|
||||||
"""Decorator for adding a function to this library."""
|
|
||||||
# XXX: this should allow specifying names of keyword arguments, as
|
|
||||||
# well. currently none of these functions support kwargs, i think.
|
|
||||||
# XXX automatically inferring the name and args would be...
|
|
||||||
# interesting
|
|
||||||
# XXX also it would be nice if a function which EXISTS but has the
|
|
||||||
# wrong number of args threw a useful error; atm i think it'll become
|
|
||||||
# literal (yikes!)
|
|
||||||
|
|
||||||
key = (name, argc)
|
|
||||||
|
|
||||||
def decorator(f):
|
|
||||||
self._functions[key] = f
|
|
||||||
return f
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def add(self, function, name, argc=None):
|
|
||||||
"""Add a function to this library imperatively."""
|
|
||||||
|
|
||||||
key = (name, argc)
|
|
||||||
self._functions[key] = function
|
|
||||||
|
|
||||||
def lookup(self, name, argc=None):
|
|
||||||
"""Find a function given its name and the number of arguments it takes.
|
|
||||||
Falls back to a function with the same name that takes any number of
|
|
||||||
arguments.
|
|
||||||
"""
|
|
||||||
# Try the given arity first
|
|
||||||
key = name, argc
|
|
||||||
if key in self._functions:
|
|
||||||
return self._functions[key]
|
|
||||||
|
|
||||||
# Fall back to arbitrary-arity (or KeyError if neither exists)
|
|
||||||
return self._functions[name, None]
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import six
|
|
||||||
|
|
||||||
from scss.types import Value, Undefined
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
SORTED_SELECTORS = False
|
|
||||||
|
|
||||||
sort = sorted if SORTED_SELECTORS else lambda it: it
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_var(name):
|
|
||||||
if isinstance(name, six.string_types):
|
|
||||||
return name.replace('_', '-')
|
|
||||||
else:
|
|
||||||
log.warn("Variable name doesn't look like a string: %r", name)
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def extend_unique(seq, more):
|
|
||||||
"""Return a new sequence containing the items in `seq` plus any items in
|
|
||||||
`more` that aren't already in `seq`, preserving the order of both.
|
|
||||||
"""
|
|
||||||
seen = set(seq)
|
|
||||||
new = []
|
|
||||||
for item in more:
|
|
||||||
if item not in seen:
|
|
||||||
seen.add(item)
|
|
||||||
new.append(item)
|
|
||||||
|
|
||||||
return seq + type(seq)(new)
|
|
||||||
|
|
||||||
|
|
||||||
class Scope(object):
|
|
||||||
"""Implements Sass variable scoping.
|
|
||||||
|
|
||||||
Similar to `ChainMap`, except that assigning a new value will replace an
|
|
||||||
existing value, not mask it.
|
|
||||||
"""
|
|
||||||
def __init__(self, maps=()):
|
|
||||||
maps = list(maps)
|
|
||||||
assert all(isinstance(m, dict) or isinstance(m, type(self)) for m in maps) # Check all passed maps are compatible with the current Scope
|
|
||||||
self.maps = [dict()] + maps
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s(%s) at 0x%x>" % (type(self).__name__, ', '.join(repr(map) for map in self.maps), id(self))
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
for map in self.maps:
|
|
||||||
if key in map:
|
|
||||||
return map[key]
|
|
||||||
|
|
||||||
raise KeyError(key)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
self.set(key, value)
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
try:
|
|
||||||
self[key]
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def keys(self):
|
|
||||||
# For mapping interface
|
|
||||||
keys = set()
|
|
||||||
for map in self.maps:
|
|
||||||
keys.update(map.keys())
|
|
||||||
return list(keys)
|
|
||||||
|
|
||||||
def set(self, key, value, force_local=False):
|
|
||||||
if not force_local:
|
|
||||||
for map in self.maps:
|
|
||||||
if key in map:
|
|
||||||
if isinstance(map[key], Undefined):
|
|
||||||
break
|
|
||||||
map[key] = value
|
|
||||||
return
|
|
||||||
|
|
||||||
self.maps[0][key] = value
|
|
||||||
|
|
||||||
def new_child(self):
|
|
||||||
return type(self)(self.maps)
|
|
||||||
|
|
||||||
|
|
||||||
class VariableScope(Scope):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionScope(Scope):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s(%s) at 0x%x>" % (type(self).__name__, ', '.join('[%s]' % ', '.join('%s:%s' % (f, n) for f, n in sorted(map.keys())) for map in self.maps), id(self))
|
|
||||||
|
|
||||||
|
|
||||||
class MixinScope(Scope):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s(%s) at 0x%x>" % (type(self).__name__, ', '.join('[%s]' % ', '.join('%s:%s' % (f, n) for f, n in sorted(map.keys())) for map in self.maps), id(self))
|
|
||||||
|
|
||||||
|
|
||||||
class ImportScope(Scope):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Namespace(object):
|
|
||||||
"""..."""
|
|
||||||
_mutable = True
|
|
||||||
|
|
||||||
def __init__(self, variables=None, functions=None, mixins=None, mutable=True):
|
|
||||||
self._mutable = mutable
|
|
||||||
|
|
||||||
if variables is None:
|
|
||||||
self._variables = VariableScope()
|
|
||||||
else:
|
|
||||||
# TODO parse into sass values once that's a thing, or require them
|
|
||||||
# all to be
|
|
||||||
self._variables = VariableScope([variables])
|
|
||||||
|
|
||||||
if functions is None:
|
|
||||||
self._functions = FunctionScope()
|
|
||||||
else:
|
|
||||||
self._functions = FunctionScope([functions._functions])
|
|
||||||
|
|
||||||
self._mixins = MixinScope()
|
|
||||||
|
|
||||||
self._imports = ImportScope()
|
|
||||||
|
|
||||||
def _assert_mutable(self):
|
|
||||||
if not self._mutable:
|
|
||||||
raise AttributeError("This Namespace instance is immutable")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def derive_from(cls, *others):
|
|
||||||
self = cls()
|
|
||||||
if len(others) == 1:
|
|
||||||
self._variables = others[0]._variables.new_child()
|
|
||||||
self._functions = others[0]._functions.new_child()
|
|
||||||
self._mixins = others[0]._mixins.new_child()
|
|
||||||
self._imports = others[0]._imports.new_child()
|
|
||||||
else:
|
|
||||||
# Note that this will create a 2-dimensional scope where each of
|
|
||||||
# these scopes is checked first in order. TODO is this right?
|
|
||||||
self._variables = VariableScope(other._variables for other in others)
|
|
||||||
self._functions = FunctionScope(other._functions for other in others)
|
|
||||||
self._mixins = MixinScope(other._mixins for other in others)
|
|
||||||
self._imports = ImportScope(other._imports for other in others)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def derive(self):
|
|
||||||
"""Return a new child namespace. All existing variables are still
|
|
||||||
readable and writeable, but any new variables will only exist within a
|
|
||||||
new scope.
|
|
||||||
"""
|
|
||||||
return type(self).derive_from(self)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def variables(self):
|
|
||||||
return dict((k, self._variables[k]) for k in self._variables.keys())
|
|
||||||
|
|
||||||
def variable(self, name, throw=False):
|
|
||||||
name = normalize_var(name)
|
|
||||||
return self._variables[name]
|
|
||||||
|
|
||||||
def set_variable(self, name, value, local_only=False):
|
|
||||||
self._assert_mutable()
|
|
||||||
name = normalize_var(name)
|
|
||||||
if not isinstance(value, Value):
|
|
||||||
raise TypeError("Expected a Sass type, while setting %s got %r" % (name, value,))
|
|
||||||
self._variables.set(name, value, force_local=local_only)
|
|
||||||
|
|
||||||
def has_import(self, import_key):
|
|
||||||
return import_key in self._imports
|
|
||||||
|
|
||||||
def add_import(self, import_key, parent_import_key, file_and_line):
|
|
||||||
self._assert_mutable()
|
|
||||||
if import_key:
|
|
||||||
imports = [0, parent_import_key, file_and_line]
|
|
||||||
self._imports[import_key] = imports
|
|
||||||
|
|
||||||
def use_import(self, import_key):
|
|
||||||
self._assert_mutable()
|
|
||||||
if import_key and import_key in self._imports:
|
|
||||||
imports = self._imports[import_key]
|
|
||||||
imports[0] += 1
|
|
||||||
self.use_import(imports[1])
|
|
||||||
|
|
||||||
def unused_imports(self):
|
|
||||||
unused = []
|
|
||||||
for import_key in self._imports.keys():
|
|
||||||
imports = self._imports[import_key]
|
|
||||||
if not imports[0]:
|
|
||||||
unused.append((import_key[0], imports[2]))
|
|
||||||
return unused
|
|
||||||
|
|
||||||
def _get_callable(self, chainmap, name, arity):
|
|
||||||
name = normalize_var(name)
|
|
||||||
if arity is not None:
|
|
||||||
# With explicit arity, try the particular arity before falling back
|
|
||||||
# to the general case (None)
|
|
||||||
try:
|
|
||||||
return chainmap[name, arity]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return chainmap[name, None]
|
|
||||||
|
|
||||||
def _set_callable(self, chainmap, name, arity, cb):
|
|
||||||
name = normalize_var(name)
|
|
||||||
chainmap[name, arity] = cb
|
|
||||||
|
|
||||||
def mixin(self, name, arity):
|
|
||||||
return self._get_callable(self._mixins, name, arity)
|
|
||||||
|
|
||||||
def set_mixin(self, name, arity, cb):
|
|
||||||
self._assert_mutable()
|
|
||||||
self._set_callable(self._mixins, name, arity, cb)
|
|
||||||
|
|
||||||
def function(self, name, arity):
|
|
||||||
return self._get_callable(self._functions, name, arity)
|
|
||||||
|
|
||||||
def set_function(self, name, arity, cb):
|
|
||||||
self._assert_mutable()
|
|
||||||
self._set_callable(self._functions, name, arity, cb)
|
|
||||||
|
|
||||||
|
|
||||||
class SassRule(object):
|
|
||||||
"""At its heart, a CSS rule: combination of a selector and zero or more
|
|
||||||
properties. But this is Sass, so it also tracks some Sass-flavored
|
|
||||||
metadata, like `@extend` rules and `@media` nesting.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, source_file, import_key=None, unparsed_contents=None,
|
|
||||||
options=None, properties=None,
|
|
||||||
namespace=None,
|
|
||||||
lineno=0, extends_selectors=frozenset(),
|
|
||||||
ancestry=None,
|
|
||||||
nested=0):
|
|
||||||
|
|
||||||
self.source_file = source_file
|
|
||||||
self.import_key = import_key
|
|
||||||
self.lineno = lineno
|
|
||||||
|
|
||||||
self.unparsed_contents = unparsed_contents
|
|
||||||
self.options = options
|
|
||||||
self.extends_selectors = extends_selectors
|
|
||||||
|
|
||||||
if namespace is None:
|
|
||||||
assert False
|
|
||||||
self.namespace = Namespace()
|
|
||||||
else:
|
|
||||||
self.namespace = namespace
|
|
||||||
|
|
||||||
if properties is None:
|
|
||||||
self.properties = []
|
|
||||||
else:
|
|
||||||
self.properties = properties
|
|
||||||
|
|
||||||
if ancestry is None:
|
|
||||||
self.ancestry = RuleAncestry()
|
|
||||||
else:
|
|
||||||
self.ancestry = ancestry
|
|
||||||
|
|
||||||
self.nested = nested
|
|
||||||
|
|
||||||
self.descendants = 0
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<SassRule %s, %d props>" % (
|
|
||||||
self.ancestry,
|
|
||||||
len(self.properties),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def selectors(self):
|
|
||||||
# TEMPORARY
|
|
||||||
if self.ancestry.headers and self.ancestry.headers[-1].is_selector:
|
|
||||||
return self.ancestry.headers[-1].selectors
|
|
||||||
else:
|
|
||||||
return ()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def file_and_line(self):
|
|
||||||
"""Return the filename and line number where this rule originally
|
|
||||||
appears, in the form "foo.scss:3". Used for error messages.
|
|
||||||
"""
|
|
||||||
return "%s:%d" % (self.source_file.filename, self.lineno)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_empty(self):
|
|
||||||
"""Return whether this rule is considered "empty" -- i.e., has no
|
|
||||||
contents that should end up in the final CSS.
|
|
||||||
"""
|
|
||||||
if self.properties:
|
|
||||||
# Rules containing CSS properties are never empty
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.descendants:
|
|
||||||
for header in self.ancestry.headers:
|
|
||||||
if header.is_atrule and header.directive != '@media':
|
|
||||||
# At-rules should always be preserved, UNLESS they are @media
|
|
||||||
# blocks, which are known to be noise if they don't have any
|
|
||||||
# contents of their own
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return type(self)(
|
|
||||||
source_file=self.source_file,
|
|
||||||
lineno=self.lineno,
|
|
||||||
unparsed_contents=self.unparsed_contents,
|
|
||||||
|
|
||||||
options=self.options,
|
|
||||||
#properties=list(self.properties),
|
|
||||||
properties=self.properties,
|
|
||||||
extends_selectors=self.extends_selectors,
|
|
||||||
#ancestry=list(self.ancestry),
|
|
||||||
ancestry=self.ancestry,
|
|
||||||
|
|
||||||
namespace=self.namespace.derive(),
|
|
||||||
nested=self.nested,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RuleAncestry(object):
|
|
||||||
def __init__(self, headers=()):
|
|
||||||
self.headers = tuple(headers)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s %r>" % (type(self).__name__, self.headers)
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.headers)
|
|
||||||
|
|
||||||
def with_nested_selectors(self, c_selectors):
|
|
||||||
if self.headers and self.headers[-1].is_selector:
|
|
||||||
# Need to merge with parent selectors
|
|
||||||
p_selectors = self.headers[-1].selectors
|
|
||||||
|
|
||||||
new_selectors = []
|
|
||||||
for p_selector in p_selectors:
|
|
||||||
for c_selector in c_selectors:
|
|
||||||
new_selectors.append(c_selector.with_parent(p_selector))
|
|
||||||
|
|
||||||
# Replace the last header with the new merged selectors
|
|
||||||
new_headers = self.headers[:-1] + (BlockSelectorHeader(new_selectors),)
|
|
||||||
return RuleAncestry(new_headers)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Whoops, no parent selectors. Just need to double-check that
|
|
||||||
# there are no uses of `&`.
|
|
||||||
for c_selector in c_selectors:
|
|
||||||
if c_selector.has_parent_reference:
|
|
||||||
raise ValueError("Can't use parent selector '&' in top-level rules")
|
|
||||||
|
|
||||||
# Add the children as a new header
|
|
||||||
new_headers = self.headers + (BlockSelectorHeader(c_selectors),)
|
|
||||||
return RuleAncestry(new_headers)
|
|
||||||
|
|
||||||
def with_more_selectors(self, selectors):
|
|
||||||
"""Return a new ancestry that also matches the given selectors. No
|
|
||||||
nesting is done.
|
|
||||||
"""
|
|
||||||
if self.headers and self.headers[-1].is_selector:
|
|
||||||
new_selectors = extend_unique(
|
|
||||||
self.headers[-1].selectors,
|
|
||||||
selectors)
|
|
||||||
new_headers = self.headers[:-1] + (
|
|
||||||
BlockSelectorHeader(new_selectors),)
|
|
||||||
return RuleAncestry(new_headers)
|
|
||||||
else:
|
|
||||||
new_headers = self.headers + (BlockSelectorHeader(selectors),)
|
|
||||||
return RuleAncestry(new_headers)
|
|
||||||
|
|
||||||
|
|
||||||
class BlockHeader(object):
|
|
||||||
"""..."""
|
|
||||||
# TODO doc me depending on how UnparsedBlock is handled...
|
|
||||||
|
|
||||||
is_atrule = False
|
|
||||||
is_scope = False
|
|
||||||
is_selector = False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse(cls, prop, has_contents=False):
|
|
||||||
# Simple pre-processing
|
|
||||||
if prop.startswith('+') and not has_contents:
|
|
||||||
# Expand '+' at the beginning of a rule as @include. But not if
|
|
||||||
# there's a block, because that's probably a CSS selector.
|
|
||||||
# DEVIATION: this is some semi hybrid of Sass and xCSS syntax
|
|
||||||
prop = '@include ' + prop[1:]
|
|
||||||
try:
|
|
||||||
if '(' not in prop or prop.index(':') < prop.index('('):
|
|
||||||
prop = prop.replace(':', '(', 1)
|
|
||||||
if '(' in prop:
|
|
||||||
prop += ')'
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
elif prop.startswith('='):
|
|
||||||
# Expand '=' at the beginning of a rule as @mixin
|
|
||||||
prop = '@mixin ' + prop[1:]
|
|
||||||
elif prop.startswith('@prototype '):
|
|
||||||
# Remove '@prototype '
|
|
||||||
# TODO what is @prototype??
|
|
||||||
prop = prop[11:]
|
|
||||||
|
|
||||||
# Minor parsing
|
|
||||||
if prop.startswith('@'):
|
|
||||||
if prop.lower().startswith('@else if '):
|
|
||||||
directive = '@else if'
|
|
||||||
argument = prop[9:]
|
|
||||||
else:
|
|
||||||
directive, _, argument = prop.partition(' ')
|
|
||||||
directive = directive.lower()
|
|
||||||
|
|
||||||
return BlockAtRuleHeader(directive, argument)
|
|
||||||
else:
|
|
||||||
if prop.endswith(':') or ': ' in prop:
|
|
||||||
# Syntax is "<scope>: [prop]" -- if the optional prop exists,
|
|
||||||
# it becomes the first rule with no suffix
|
|
||||||
scope, unscoped_value = prop.split(':', 1)
|
|
||||||
scope = scope.rstrip()
|
|
||||||
unscoped_value = unscoped_value.lstrip()
|
|
||||||
return BlockScopeHeader(scope, unscoped_value)
|
|
||||||
else:
|
|
||||||
return BlockSelectorHeader(prop)
|
|
||||||
|
|
||||||
|
|
||||||
class BlockAtRuleHeader(BlockHeader):
|
|
||||||
is_atrule = True
|
|
||||||
|
|
||||||
def __init__(self, directive, argument):
|
|
||||||
self.directive = directive
|
|
||||||
self.argument = argument
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s %r %r>" % (type(self).__name__, self.directive, self.argument)
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
if self.argument:
|
|
||||||
return "%s %s" % (self.directive, self.argument)
|
|
||||||
else:
|
|
||||||
return self.directive
|
|
||||||
|
|
||||||
|
|
||||||
class BlockSelectorHeader(BlockHeader):
|
|
||||||
is_selector = True
|
|
||||||
|
|
||||||
def __init__(self, selectors):
|
|
||||||
self.selectors = tuple(selectors)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s %r>" % (type(self).__name__, self.selectors)
|
|
||||||
|
|
||||||
def render(self, sep=', ', super_selector=''):
|
|
||||||
return sep.join(sort(
|
|
||||||
super_selector + s.render()
|
|
||||||
for s in self.selectors
|
|
||||||
if not s.has_placeholder))
|
|
||||||
|
|
||||||
|
|
||||||
class BlockScopeHeader(BlockHeader):
|
|
||||||
is_scope = True
|
|
||||||
|
|
||||||
def __init__(self, scope, unscoped_value):
|
|
||||||
self.scope = scope
|
|
||||||
|
|
||||||
if unscoped_value:
|
|
||||||
self.unscoped_value = unscoped_value
|
|
||||||
else:
|
|
||||||
self.unscoped_value = None
|
|
||||||
|
|
||||||
|
|
||||||
class UnparsedBlock(object):
|
|
||||||
"""A Sass block whose contents have not yet been parsed.
|
|
||||||
|
|
||||||
At the top level, CSS (and Sass) documents consist of a sequence of blocks.
|
|
||||||
A block may be a ruleset:
|
|
||||||
|
|
||||||
selector { block; block; block... }
|
|
||||||
|
|
||||||
Or it may be an @-rule:
|
|
||||||
|
|
||||||
@rule arguments { block; block; block... }
|
|
||||||
|
|
||||||
Or it may be only a single property declaration:
|
|
||||||
|
|
||||||
property: value
|
|
||||||
|
|
||||||
pyScss's first parsing pass breaks the document into these blocks, and each
|
|
||||||
block becomes an instance of this class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent_rule, lineno, prop, unparsed_contents):
|
|
||||||
self.parent_rule = parent_rule
|
|
||||||
self.header = BlockHeader.parse(prop, has_contents=bool(unparsed_contents))
|
|
||||||
|
|
||||||
# Basic properties
|
|
||||||
self.lineno = lineno
|
|
||||||
self.prop = prop
|
|
||||||
self.unparsed_contents = unparsed_contents
|
|
||||||
|
|
||||||
@property
|
|
||||||
def directive(self):
|
|
||||||
return self.header.directive
|
|
||||||
|
|
||||||
@property
|
|
||||||
def argument(self):
|
|
||||||
return self.header.argument
|
|
||||||
|
|
||||||
### What kind of thing is this?
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_atrule(self):
|
|
||||||
return self.header.is_atrule
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_scope(self):
|
|
||||||
return self.header.is_scope
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#-*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
pyScss, a Scss compiler for Python
|
|
||||||
|
|
||||||
@author German M. Bravo (Kronuz) <german.mb@gmail.com>
|
|
||||||
@version 1.2.0 alpha
|
|
||||||
@see https://github.com/Kronuz/pyScss
|
|
||||||
@copyright (c) 2012-2013 German M. Bravo (Kronuz)
|
|
||||||
@license MIT License
|
|
||||||
http://www.opensource.org/licenses/mit-license.php
|
|
||||||
|
|
||||||
pyScss compiles Scss, a superset of CSS that is more powerful, elegant and
|
|
||||||
easier to maintain than plain-vanilla CSS. The library acts as a CSS source code
|
|
||||||
preprocesor which allows you to use variables, nested rules, mixins, andhave
|
|
||||||
inheritance of rules, all with a CSS-compatible syntax which the preprocessor
|
|
||||||
then compiles to standard CSS.
|
|
||||||
|
|
||||||
Scss, as an extension of CSS, helps keep large stylesheets well-organized. It
|
|
||||||
borrows concepts and functionality from projects such as OOCSS and other similar
|
|
||||||
frameworks like as Sass. It's build on top of the original PHP xCSS codebase
|
|
||||||
structure but it's been completely rewritten, many bugs have been fixed and it
|
|
||||||
has been extensively extended to support almost the full range of Sass' Scss
|
|
||||||
syntax and functionality.
|
|
||||||
|
|
||||||
Bits of code in pyScss come from various projects:
|
|
||||||
Compass:
|
|
||||||
(c) 2009 Christopher M. Eppstein
|
|
||||||
http://compass-style.org/
|
|
||||||
Sass:
|
|
||||||
(c) 2006-2009 Hampton Catlin and Nathan Weizenbaum
|
|
||||||
http://sass-lang.com/
|
|
||||||
xCSS:
|
|
||||||
(c) 2010 Anton Pawlik
|
|
||||||
http://xcss.antpaw.org/docs/
|
|
||||||
|
|
||||||
This file defines Meta data, according to PEP314
|
|
||||||
(http://www.python.org/dev/peps/pep-0314/) which is common to both pyScss
|
|
||||||
and setup.py distutils.
|
|
||||||
|
|
||||||
We create this here so this information can be compatible with BOTH
|
|
||||||
Python 2.x and Python 3.x so setup.py can use it when building pyScss
|
|
||||||
for both Py3.x and Py2.x
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
VERSION_INFO = (1, 2, 0, 'post3')
|
|
||||||
DATE_INFO = (2013, 10, 8) # YEAR, MONTH, DAY
|
|
||||||
VERSION = '.'.join(str(i) for i in VERSION_INFO)
|
|
||||||
REVISION = '%04d%02d%02d' % DATE_INFO
|
|
||||||
BUILD_INFO = "pyScss v" + VERSION + " (" + REVISION + ")"
|
|
||||||
AUTHOR = "German M. Bravo (Kronuz)"
|
|
||||||
AUTHOR_EMAIL = 'german.mb@gmail.com'
|
|
||||||
URL = 'http://github.com/Kronuz/pyScss'
|
|
||||||
DOWNLOAD_URL = 'http://github.com/Kronuz/pyScss/tarball/v' + VERSION
|
|
||||||
LICENSE = "MIT"
|
|
||||||
PROJECT = "pyScss"
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print('VERSION = ' + VERSION)
|
|
||||||
print('REVISION = ' + REVISION)
|
|
||||||
print('BUILD_INFO = ' + BUILD_INFO)
|
|
||||||
print('AUTHOR = ' + AUTHOR)
|
|
||||||
print('AUTHOR_EMAIL = ' + AUTHOR_EMAIL)
|
|
||||||
print('URL = ' + URL)
|
|
||||||
print('LICENSE = ' + LICENSE)
|
|
||||||
print('PROJECT = ' + PROJECT)
|
|
||||||
@@ -1,607 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
# Super dumb little selector parser.
|
|
||||||
|
|
||||||
# Yes, yes, this is a regex tokenizer. The actual meaning of the
|
|
||||||
# selector doesn't matter; the parts are just important for matching up
|
|
||||||
# during @extend.
|
|
||||||
|
|
||||||
# Selectors have three levels: simple, combinator, comma-delimited.
|
|
||||||
# Each combinator can only appear once as a delimiter between simple
|
|
||||||
# selectors, so it can be thought of as a prefix.
|
|
||||||
# So this:
|
|
||||||
# a.b + c, d#e
|
|
||||||
# parses into two Selectors with these structures:
|
|
||||||
# [[' ', 'a', '.b'], ['+', 'c']]
|
|
||||||
# [[' ', 'd', '#e']]
|
|
||||||
# Note that the first simple selector has an implied descendant
|
|
||||||
# combinator -- i.e., it is a descendant of the root element.
|
|
||||||
# TODO `*html` is incorrectly parsed as a single selector
|
|
||||||
# TODO this oughta be touched up for css4 selectors
|
|
||||||
SELECTOR_TOKENIZER = re.compile(r'''
|
|
||||||
# Colons introduce pseudo-selectors, sometimes with parens
|
|
||||||
# TODO doesn't handle quoted )
|
|
||||||
[:]+ [-\w]+ (?: [(] .+? [)] )?
|
|
||||||
|
|
||||||
# These guys are combinators -- note that a single space counts too
|
|
||||||
| \s* [ +>~,] \s*
|
|
||||||
|
|
||||||
# Square brackets are attribute tests
|
|
||||||
# TODO: this doesn't handle ] within a string
|
|
||||||
| [[] .+? []]
|
|
||||||
|
|
||||||
# Dot and pound start class/id selectors. Percent starts a Sass
|
|
||||||
# extend-target faux selector.
|
|
||||||
| [.#%] [-\w]+
|
|
||||||
|
|
||||||
# Percentages are used for @keyframes
|
|
||||||
| [-.\d]+ [%]
|
|
||||||
|
|
||||||
# Plain identifiers, or single asterisks, are element names
|
|
||||||
| [-\w]+
|
|
||||||
| [*]
|
|
||||||
|
|
||||||
# & is the sass replacement token
|
|
||||||
| [&]
|
|
||||||
|
|
||||||
# And as a last-ditch effort, just eat up to whitespace
|
|
||||||
| (\S+)
|
|
||||||
''', re.VERBOSE | re.MULTILINE)
|
|
||||||
|
|
||||||
|
|
||||||
# Maps the first character of a token to a rough ordering. The default
|
|
||||||
# (element names) is zero.
|
|
||||||
TOKEN_TYPE_ORDER = {
|
|
||||||
'#': 2,
|
|
||||||
'.': 3,
|
|
||||||
'[': 3,
|
|
||||||
':': 3,
|
|
||||||
'%': 4,
|
|
||||||
}
|
|
||||||
TOKEN_SORT_KEY = lambda token: TOKEN_TYPE_ORDER.get(token[0], 0)
|
|
||||||
|
|
||||||
|
|
||||||
def _is_combinator_subset_of(specific, general, is_first=True):
|
|
||||||
"""Return whether `specific` matches a non-strict subset of what `general`
|
|
||||||
matches.
|
|
||||||
"""
|
|
||||||
if is_first and general == ' ':
|
|
||||||
# First selector always has a space to mean "descendent of root", which
|
|
||||||
# still holds if any other selector appears above it
|
|
||||||
return True
|
|
||||||
|
|
||||||
if specific == general:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if specific == '>' and general == ' ':
|
|
||||||
return True
|
|
||||||
|
|
||||||
if specific == '+' and general == '~':
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleSelector(object):
|
|
||||||
"""A simple selector, by CSS 2.1 terminology: a combination of element
|
|
||||||
name, class selectors, id selectors, and other criteria that all apply to a
|
|
||||||
single element.
|
|
||||||
|
|
||||||
Note that CSS 3 considers EACH of those parts to be a "simple selector",
|
|
||||||
and calls a group of them a "sequence of simple selectors". That's a
|
|
||||||
terrible class name, so we're going with 2.1 here.
|
|
||||||
|
|
||||||
For lack of a better name, each of the individual parts is merely called a
|
|
||||||
"token".
|
|
||||||
"""
|
|
||||||
def __init__(self, combinator, tokens):
|
|
||||||
self.combinator = combinator
|
|
||||||
# TODO enforce that only one element name (including *) appears in a
|
|
||||||
# selector
|
|
||||||
# TODO remove duplicates
|
|
||||||
self.tokens = tuple(sorted(tokens, key=TOKEN_SORT_KEY))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s: %r>" % (type(self).__name__, self.render())
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((self.combinator, self.tokens))
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, SimpleSelector):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return (
|
|
||||||
self.combinator == other.combinator and
|
|
||||||
self.tokens == other.tokens)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_parent_reference(self):
|
|
||||||
return '&' in self.tokens or 'self' in self.tokens
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_placeholder(self):
|
|
||||||
return any(
|
|
||||||
token[0] == '%'
|
|
||||||
for token in self.tokens)
|
|
||||||
|
|
||||||
def is_superset_of(self, other, soft_combinator=False):
|
|
||||||
"""Return True iff this selector matches the same elements as `other`,
|
|
||||||
and perhaps others.
|
|
||||||
|
|
||||||
That is, ``.foo`` is a superset of ``.foo.bar``, because the latter is
|
|
||||||
more specific.
|
|
||||||
|
|
||||||
Set `soft_combinator` true to ignore the specific case of this selector
|
|
||||||
having a descendent combinator and `other` having anything else. This
|
|
||||||
is for superset checking for ``@extend``, where a space combinator
|
|
||||||
really means "none".
|
|
||||||
"""
|
|
||||||
# Combinators must match, OR be compatible -- space is a superset of >,
|
|
||||||
# ~ is a superset of +
|
|
||||||
if soft_combinator and self.combinator == ' ':
|
|
||||||
combinator_superset = True
|
|
||||||
else:
|
|
||||||
combinator_superset = (
|
|
||||||
self.combinator == other.combinator or
|
|
||||||
(self.combinator == ' ' and other.combinator == '>') or
|
|
||||||
(self.combinator == '~' and other.combinator == '+'))
|
|
||||||
|
|
||||||
return (
|
|
||||||
combinator_superset and
|
|
||||||
set(self.tokens) <= set(other.tokens))
|
|
||||||
|
|
||||||
def replace_parent(self, parent_simples):
|
|
||||||
"""If ``&`` (or the legacy xCSS equivalent ``self``) appears in this
|
|
||||||
selector, replace it with the given iterable of parent selectors.
|
|
||||||
|
|
||||||
Returns a tuple of simple selectors.
|
|
||||||
"""
|
|
||||||
assert parent_simples
|
|
||||||
|
|
||||||
ancestors = parent_simples[:-1]
|
|
||||||
parent = parent_simples[-1]
|
|
||||||
|
|
||||||
did_replace = False
|
|
||||||
new_tokens = []
|
|
||||||
for token in self.tokens:
|
|
||||||
if not did_replace and token in ('&', 'self'):
|
|
||||||
did_replace = True
|
|
||||||
new_tokens.extend(parent.tokens)
|
|
||||||
else:
|
|
||||||
new_tokens.append(token)
|
|
||||||
|
|
||||||
if not did_replace:
|
|
||||||
# This simple selector doesn't contain a parent reference so just
|
|
||||||
# stick it on the end
|
|
||||||
return parent_simples + (self,)
|
|
||||||
|
|
||||||
# This simple selector was merged into the direct parent.
|
|
||||||
merged_self = type(self)(parent.combinator, new_tokens)
|
|
||||||
selector = ancestors + (merged_self,)
|
|
||||||
# Our combinator goes on the first ancestor, i.e., substituting "foo
|
|
||||||
# bar baz" into "+ &.quux" produces "+ foo bar baz.quux". This means a
|
|
||||||
# potential conflict with the first ancestor's combinator!
|
|
||||||
root = selector[0]
|
|
||||||
if not _is_combinator_subset_of(self.combinator, root.combinator):
|
|
||||||
raise ValueError(
|
|
||||||
"Can't sub parent {0!r} into {1!r}: "
|
|
||||||
"combinators {2!r} and {3!r} conflict!"
|
|
||||||
.format(
|
|
||||||
parent_simples, self, self.combinator, root.combinator))
|
|
||||||
|
|
||||||
root = type(self)(self.combinator, root.tokens)
|
|
||||||
selector = (root,) + selector[1:]
|
|
||||||
return tuple(selector)
|
|
||||||
|
|
||||||
# TODO just use set ops for these, once the constructor removes dupes
|
|
||||||
def merge_with(self, other):
|
|
||||||
new_tokens = self.tokens + tuple(token for token in other.tokens if token not in set(self.tokens))
|
|
||||||
return type(self)(self.combinator, new_tokens)
|
|
||||||
|
|
||||||
def difference(self, other):
|
|
||||||
new_tokens = tuple(token for token in self.tokens if token not in set(other.tokens))
|
|
||||||
return type(self)(self.combinator, new_tokens)
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
# TODO fail if there are no tokens, or if one is a placeholder?
|
|
||||||
rendered = ''.join(self.tokens)
|
|
||||||
if self.combinator != ' ':
|
|
||||||
rendered = ' '.join((self.combinator, rendered))
|
|
||||||
|
|
||||||
return rendered
|
|
||||||
|
|
||||||
|
|
||||||
class Selector(object):
|
|
||||||
"""A single CSS selector."""
|
|
||||||
|
|
||||||
def __init__(self, simples):
|
|
||||||
"""Return a selector containing a sequence of `SimpleSelector`s.
|
|
||||||
|
|
||||||
You probably want to use `parse_many` or `parse_one` instead.
|
|
||||||
"""
|
|
||||||
# TODO enforce uniqueness
|
|
||||||
self.simple_selectors = tuple(simples)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse_many(cls, selector):
|
|
||||||
selector = selector.strip()
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
pending = dict(
|
|
||||||
simples=[],
|
|
||||||
combinator=' ',
|
|
||||||
tokens=[],
|
|
||||||
)
|
|
||||||
|
|
||||||
def promote_simple():
|
|
||||||
if pending['tokens']:
|
|
||||||
pending['simples'].append(
|
|
||||||
SimpleSelector(pending['combinator'], pending['tokens']))
|
|
||||||
pending['combinator'] = ' '
|
|
||||||
pending['tokens'] = []
|
|
||||||
|
|
||||||
def promote_selector():
|
|
||||||
promote_simple()
|
|
||||||
if pending['simples']:
|
|
||||||
ret.append(cls(pending['simples']))
|
|
||||||
pending['simples'] = []
|
|
||||||
|
|
||||||
pos = 0
|
|
||||||
while pos < len(selector):
|
|
||||||
# TODO i don't think this deals with " + " correctly. anywhere.
|
|
||||||
# TODO this used to turn "1.5%" into empty string; why does error
|
|
||||||
# not work?
|
|
||||||
m = SELECTOR_TOKENIZER.match(selector, pos)
|
|
||||||
if not m:
|
|
||||||
# TODO prettify me
|
|
||||||
raise SyntaxError("Couldn't parse selector: %r" % (selector,))
|
|
||||||
|
|
||||||
token = m.group(0)
|
|
||||||
pos += len(token)
|
|
||||||
|
|
||||||
# Kill any extraneous space, BUT make sure not to turn a lone space
|
|
||||||
# into an empty string
|
|
||||||
token = token.strip() or ' '
|
|
||||||
|
|
||||||
if token == ',':
|
|
||||||
# End current selector
|
|
||||||
# TODO what about "+ ,"? what do i even do with that
|
|
||||||
promote_selector()
|
|
||||||
elif token in ' +>~':
|
|
||||||
# End current simple selector
|
|
||||||
promote_simple()
|
|
||||||
pending['combinator'] = token
|
|
||||||
else:
|
|
||||||
# Add to pending simple selector
|
|
||||||
pending['tokens'].append(token)
|
|
||||||
|
|
||||||
# Deal with any remaining pending bits
|
|
||||||
promote_selector()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse_one(cls, selector_string):
|
|
||||||
selectors = cls.parse_many(selector_string)
|
|
||||||
if len(selectors) != 1:
|
|
||||||
# TODO better error
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
return selectors[0]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<%s: %r>" % (type(self).__name__, self.render())
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash(self.simple_selectors)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, Selector):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self.simple_selectors == other.simple_selectors
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_parent_reference(self):
|
|
||||||
return any(
|
|
||||||
simple.has_parent_reference
|
|
||||||
for simple in self.simple_selectors)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_placeholder(self):
|
|
||||||
return any(
|
|
||||||
simple.has_placeholder
|
|
||||||
for simple in self.simple_selectors)
|
|
||||||
|
|
||||||
def with_parent(self, parent):
|
|
||||||
saw_parent_ref = False
|
|
||||||
|
|
||||||
new_simples = []
|
|
||||||
for simple in self.simple_selectors:
|
|
||||||
if simple.has_parent_reference:
|
|
||||||
new_simples.extend(simple.replace_parent(parent.simple_selectors))
|
|
||||||
saw_parent_ref = True
|
|
||||||
else:
|
|
||||||
new_simples.append(simple)
|
|
||||||
|
|
||||||
if not saw_parent_ref:
|
|
||||||
new_simples = parent.simple_selectors + tuple(new_simples)
|
|
||||||
|
|
||||||
return type(self)(new_simples)
|
|
||||||
|
|
||||||
def lookup_key(self):
|
|
||||||
"""Build a key from the "important" parts of a selector: elements,
|
|
||||||
classes, ids.
|
|
||||||
"""
|
|
||||||
parts = set()
|
|
||||||
for node in self.simple_selectors:
|
|
||||||
for token in node.tokens:
|
|
||||||
if token[0] not in ':[':
|
|
||||||
parts.add(token)
|
|
||||||
|
|
||||||
if not parts:
|
|
||||||
# Should always have at least ONE key; selectors with no elements,
|
|
||||||
# no classes, and no ids can be indexed as None to avoid a scan of
|
|
||||||
# every selector in the entire document
|
|
||||||
parts.add(None)
|
|
||||||
|
|
||||||
return frozenset(parts)
|
|
||||||
|
|
||||||
def is_superset_of(self, other):
|
|
||||||
assert isinstance(other, Selector)
|
|
||||||
|
|
||||||
idx = 0
|
|
||||||
for other_node in other.simple_selectors:
|
|
||||||
if idx >= len(self.simple_selectors):
|
|
||||||
return False
|
|
||||||
|
|
||||||
while idx < len(self.simple_selectors):
|
|
||||||
node = self.simple_selectors[idx]
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
if node.is_superset_of(other_node):
|
|
||||||
break
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def substitute(self, target, replacement):
|
|
||||||
"""Return a list of selectors obtained by replacing the `target`
|
|
||||||
selector with `replacement`.
|
|
||||||
|
|
||||||
Herein lie the guts of the Sass @extend directive.
|
|
||||||
|
|
||||||
In general, for a selector ``a X b Y c``, a target ``X Y``, and a
|
|
||||||
replacement ``q Z``, return the selectors ``a q X b Z c`` and ``q a X b
|
|
||||||
Z c``. Note in particular that no more than two selectors will be
|
|
||||||
returned, and the permutation of ancestors will never insert new simple
|
|
||||||
selectors "inside" the target selector.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Find the target in the parent selector, and split it into
|
|
||||||
# before/after
|
|
||||||
p_before, p_extras, p_after = self.break_around(target.simple_selectors)
|
|
||||||
|
|
||||||
# The replacement has no hinge; it only has the most specific simple
|
|
||||||
# selector (which is the part that replaces "self" in the parent) and
|
|
||||||
# whatever preceding simple selectors there may be
|
|
||||||
r_trail = replacement.simple_selectors[:-1]
|
|
||||||
r_extras = replacement.simple_selectors[-1]
|
|
||||||
|
|
||||||
# TODO what if the prefix doesn't match? who wins? should we even get
|
|
||||||
# this far?
|
|
||||||
focal_nodes = (p_extras.merge_with(r_extras),)
|
|
||||||
|
|
||||||
befores = _merge_selectors(p_before, r_trail)
|
|
||||||
|
|
||||||
cls = type(self)
|
|
||||||
return [
|
|
||||||
cls(before + focal_nodes + p_after)
|
|
||||||
for before in befores]
|
|
||||||
|
|
||||||
def break_around(self, hinge):
|
|
||||||
"""Given a simple selector node contained within this one (a "hinge"),
|
|
||||||
break it in half and return a parent selector, extra specifiers for the
|
|
||||||
hinge, and a child selector.
|
|
||||||
|
|
||||||
That is, given a hinge X, break the selector A + X.y B into A, + .y,
|
|
||||||
and B.
|
|
||||||
"""
|
|
||||||
hinge_start = hinge[0]
|
|
||||||
for i, node in enumerate(self.simple_selectors):
|
|
||||||
# In this particular case, a ' ' combinator actually means "no" (or
|
|
||||||
# any) combinator, so it should be ignored
|
|
||||||
if hinge_start.is_superset_of(node, soft_combinator=True):
|
|
||||||
start_idx = i
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"Couldn't find hinge %r in compound selector %r" %
|
|
||||||
(hinge_start, self.simple_selectors))
|
|
||||||
|
|
||||||
for i, hinge_node in enumerate(hinge):
|
|
||||||
if i == 0:
|
|
||||||
# We just did this
|
|
||||||
continue
|
|
||||||
|
|
||||||
self_node = self.simple_selectors[start_idx + i]
|
|
||||||
if hinge_node.is_superset_of(self_node):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO this isn't true; consider finding `a b` in `a c a b`
|
|
||||||
raise ValueError(
|
|
||||||
"Couldn't find hinge %r in compound selector %r" %
|
|
||||||
(hinge_node, self.simple_selectors))
|
|
||||||
|
|
||||||
end_idx = start_idx + len(hinge) - 1
|
|
||||||
|
|
||||||
focal_node = self.simple_selectors[end_idx]
|
|
||||||
extras = focal_node.difference(hinge[-1])
|
|
||||||
|
|
||||||
return (
|
|
||||||
self.simple_selectors[:start_idx],
|
|
||||||
extras,
|
|
||||||
self.simple_selectors[end_idx + 1:])
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
return ' '.join(simple.render() for simple in self.simple_selectors)
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_selectors(left, right):
|
|
||||||
"""Given two selector chains (lists of simple selectors), return a list of
|
|
||||||
selector chains representing elements matched by both of them.
|
|
||||||
|
|
||||||
This operation is not exact, and involves some degree of fudging -- the
|
|
||||||
wackier and more divergent the input, the more fudging. It's meant to be
|
|
||||||
what a human might expect rather than a precise covering of all possible
|
|
||||||
cases. Most notably, when the two input chains have absolutely nothing in
|
|
||||||
common, the output is merely ``left + right`` and ``right + left`` rather
|
|
||||||
than all possible interleavings.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not left or not right:
|
|
||||||
# At least one is empty, so there are no conflicts; just return
|
|
||||||
# whichever isn't empty. Remember to return a LIST, though
|
|
||||||
return [left or right]
|
|
||||||
|
|
||||||
lcs = longest_common_subsequence(left, right, _merge_simple_selectors)
|
|
||||||
|
|
||||||
ret = [()] # start with a dummy empty chain or weaving won't work
|
|
||||||
|
|
||||||
left_last = 0
|
|
||||||
right_last = 0
|
|
||||||
for left_next, right_next, merged in lcs:
|
|
||||||
ret = _weave_conflicting_selectors(
|
|
||||||
ret,
|
|
||||||
left[left_last:left_next],
|
|
||||||
right[right_last:right_next],
|
|
||||||
(merged,))
|
|
||||||
|
|
||||||
left_last = left_next + 1
|
|
||||||
right_last = right_next + 1
|
|
||||||
|
|
||||||
ret = _weave_conflicting_selectors(
|
|
||||||
ret,
|
|
||||||
left[left_last:],
|
|
||||||
right[right_last:])
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def _weave_conflicting_selectors(prefixes, a, b, suffix=()):
|
|
||||||
"""Part of the selector merge algorithm above. Not useful on its own. Pay
|
|
||||||
no attention to the man behind the curtain.
|
|
||||||
"""
|
|
||||||
# OK, what this actually does: given a list of selector chains, two
|
|
||||||
# "conflicting" selector chains, and an optional suffix, return a new list
|
|
||||||
# of chains like this:
|
|
||||||
# prefix[0] + a + b + suffix,
|
|
||||||
# prefix[0] + b + a + suffix,
|
|
||||||
# prefix[1] + a + b + suffix,
|
|
||||||
# ...
|
|
||||||
# In other words, this just appends a new chain to each of a list of given
|
|
||||||
# chains, except that the new chain might be the superposition of two
|
|
||||||
# other incompatible chains.
|
|
||||||
both = a and b
|
|
||||||
for prefix in prefixes:
|
|
||||||
yield prefix + a + b + suffix
|
|
||||||
if both:
|
|
||||||
# Only use both orderings if there's an actual conflict!
|
|
||||||
yield prefix + b + a + suffix
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_simple_selectors(a, b):
|
|
||||||
"""Merge two simple selectors, for the purposes of the LCS algorithm below.
|
|
||||||
|
|
||||||
In practice this returns the more specific selector if one is a subset of
|
|
||||||
the other, else it returns None.
|
|
||||||
"""
|
|
||||||
# TODO what about combinators
|
|
||||||
if a.is_superset_of(b):
|
|
||||||
return b
|
|
||||||
elif b.is_superset_of(a):
|
|
||||||
return a
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def longest_common_subsequence(a, b, mergefunc=None):
|
|
||||||
"""Find the longest common subsequence between two iterables.
|
|
||||||
|
|
||||||
The longest common subsequence is the core of any diff algorithm: it's the
|
|
||||||
longest sequence of elements that appears in both parent sequences in the
|
|
||||||
same order, but NOT necessarily consecutively.
|
|
||||||
|
|
||||||
Original algorithm borrowed from Wikipedia:
|
|
||||||
http://en.wikipedia.org/wiki/Longest_common_subsequence_problem#Code_for_the_dynamic_programming_solution
|
|
||||||
|
|
||||||
This function is used only to implement @extend, largely because that's
|
|
||||||
what the Ruby implementation does. Thus it's been extended slightly from
|
|
||||||
the simple diff-friendly algorithm given above.
|
|
||||||
|
|
||||||
What @extend wants to know is whether two simple selectors are compatible,
|
|
||||||
not just equal. To that end, you must pass in a "merge" function to
|
|
||||||
compare a pair of elements manually. It should return `None` if they are
|
|
||||||
incompatible, and a MERGED element if they are compatible -- in the case of
|
|
||||||
selectors, this is whichever one is more specific.
|
|
||||||
|
|
||||||
Because of this fuzzier notion of equality, the return value is a list of
|
|
||||||
``(a_index, b_index, value)`` tuples rather than items alone.
|
|
||||||
"""
|
|
||||||
if mergefunc is None:
|
|
||||||
# Stupid default, just in case
|
|
||||||
def mergefunc(a, b):
|
|
||||||
if a == b:
|
|
||||||
return a
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Precalculate equality, since it can be a tad expensive and every pair is
|
|
||||||
# compared at least once
|
|
||||||
eq = {}
|
|
||||||
for ai, aval in enumerate(a):
|
|
||||||
for bi, bval in enumerate(b):
|
|
||||||
eq[ai, bi] = mergefunc(aval, bval)
|
|
||||||
|
|
||||||
# Build the "length" matrix, which provides the length of the LCS for
|
|
||||||
# arbitrary-length prefixes. -1 exists only to support the base case
|
|
||||||
prefix_lcs_length = {}
|
|
||||||
for ai in range(-1, len(a)):
|
|
||||||
for bi in range(-1, len(b)):
|
|
||||||
if ai == -1 or bi == -1:
|
|
||||||
l = 0
|
|
||||||
elif eq[ai, bi]:
|
|
||||||
l = prefix_lcs_length[ai - 1, bi - 1] + 1
|
|
||||||
else:
|
|
||||||
l = max(
|
|
||||||
prefix_lcs_length[ai, bi - 1],
|
|
||||||
prefix_lcs_length[ai - 1, bi])
|
|
||||||
|
|
||||||
prefix_lcs_length[ai, bi] = l
|
|
||||||
|
|
||||||
# The interesting part. The key insight is that the bottom-right value in
|
|
||||||
# the length matrix must be the length of the LCS because of how the matrix
|
|
||||||
# is defined, so all that's left to do is backtrack from the ends of both
|
|
||||||
# sequences in whatever way keeps the LCS as long as possible, and keep
|
|
||||||
# track of the equal pairs of elements we see along the way.
|
|
||||||
# Wikipedia does this with recursion, but the algorithm is trivial to
|
|
||||||
# rewrite as a loop, as below.
|
|
||||||
ai = len(a) - 1
|
|
||||||
bi = len(b) - 1
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
while ai >= 0 and bi >= 0:
|
|
||||||
merged = eq[ai, bi]
|
|
||||||
if merged is not None:
|
|
||||||
ret.append((ai, bi, merged))
|
|
||||||
ai -= 1
|
|
||||||
bi -= 1
|
|
||||||
elif prefix_lcs_length[ai, bi - 1] > prefix_lcs_length[ai - 1, bi]:
|
|
||||||
bi -= 1
|
|
||||||
else:
|
|
||||||
ai -= 1
|
|
||||||
|
|
||||||
# ret has the latest items first, which is backwards
|
|
||||||
ret.reverse()
|
|
||||||
return ret
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from distutils.core import setup, Extension
|
|
||||||
|
|
||||||
setup(name='pyScss',
|
|
||||||
version='1.1.1',
|
|
||||||
description='pyScss',
|
|
||||||
ext_modules=[
|
|
||||||
Extension(
|
|
||||||
'_scss',
|
|
||||||
sources=['src/_scss.c', 'src/block_locator.c', 'src/scanner.c'],
|
|
||||||
libraries=['pcre'],
|
|
||||||
optional=True
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
/*
|
|
||||||
* pyScss, a Scss compiler for Python
|
|
||||||
* SCSS blocks scanner.
|
|
||||||
*
|
|
||||||
* German M. Bravo (Kronuz) <german.mb@gmail.com>
|
|
||||||
* https://github.com/Kronuz/pyScss
|
|
||||||
*
|
|
||||||
* MIT license (http://www.opensource.org/licenses/mit-license.php)
|
|
||||||
* Copyright (c) 2011 German M. Bravo (Kronuz), All rights reserved.
|
|
||||||
*/
|
|
||||||
#include <Python.h>
|
|
||||||
#include "block_locator.h"
|
|
||||||
#include "scanner.h"
|
|
||||||
|
|
||||||
/* BlockLocator */
|
|
||||||
staticforward PyTypeObject scss_BlockLocatorType;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD
|
|
||||||
BlockLocator *locator;
|
|
||||||
} scss_BlockLocator;
|
|
||||||
|
|
||||||
static int
|
|
||||||
scss_BlockLocator_init(scss_BlockLocator *self, PyObject *args, PyObject *kwds)
|
|
||||||
{
|
|
||||||
char *codestr;
|
|
||||||
int codestr_sz;
|
|
||||||
|
|
||||||
self->locator = NULL;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "s#", &codestr, &codestr_sz)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
self->locator = BlockLocator_new(codestr, codestr_sz);
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
PySys_WriteStderr("Scss BlockLocator object initialized! (%lu bytes)\n", sizeof(scss_BlockLocator));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
scss_BlockLocator_dealloc(scss_BlockLocator *self)
|
|
||||||
{
|
|
||||||
if (self->locator != NULL) BlockLocator_del(self->locator);
|
|
||||||
|
|
||||||
self->ob_type->tp_free((PyObject*)self);
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
PySys_WriteStderr("Scss BlockLocator object destroyed!\n");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
scss_BlockLocator*
|
|
||||||
scss_BlockLocator_iter(scss_BlockLocator *self)
|
|
||||||
{
|
|
||||||
Py_INCREF(self);
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
PyObject*
|
|
||||||
scss_BlockLocator_iternext(scss_BlockLocator *self)
|
|
||||||
{
|
|
||||||
Block *block;
|
|
||||||
|
|
||||||
if (self->locator != NULL) {
|
|
||||||
block = BlockLocator_iternext(self->locator);
|
|
||||||
|
|
||||||
if (block->error > 0) {
|
|
||||||
return Py_BuildValue(
|
|
||||||
"is#s#",
|
|
||||||
block->lineno,
|
|
||||||
block->selprop,
|
|
||||||
block->selprop_sz,
|
|
||||||
block->codestr,
|
|
||||||
block->codestr_sz
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block->error > 0) {
|
|
||||||
PyErr_SetString(PyExc_Exception, self->locator->exc);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Raising of standard StopIteration exception with empty value. */
|
|
||||||
PyErr_SetNone(PyExc_StopIteration);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Type definition */
|
|
||||||
|
|
||||||
static PyTypeObject scss_BlockLocatorType = {
|
|
||||||
PyObject_HEAD_INIT(NULL)
|
|
||||||
0, /* ob_size */
|
|
||||||
"scss._BlockLocator", /* tp_name */
|
|
||||||
sizeof(scss_BlockLocator), /* tp_basicsize */
|
|
||||||
0, /* tp_itemsize */
|
|
||||||
(destructor)scss_BlockLocator_dealloc, /* tp_dealloc */
|
|
||||||
0, /* tp_print */
|
|
||||||
0, /* tp_getattr */
|
|
||||||
0, /* tp_setattr */
|
|
||||||
0, /* tp_compare */
|
|
||||||
0, /* tp_repr */
|
|
||||||
0, /* tp_as_number */
|
|
||||||
0, /* tp_as_sequence */
|
|
||||||
0, /* tp_as_mapping */
|
|
||||||
0, /* tp_hash */
|
|
||||||
0, /* tp_call */
|
|
||||||
0, /* tp_str */
|
|
||||||
0, /* tp_getattro */
|
|
||||||
0, /* tp_setattro */
|
|
||||||
0, /* tp_as_buffer */
|
|
||||||
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_ITER, /* tp_flags */
|
|
||||||
"Internal BlockLocator iterator object.", /* tp_doc */
|
|
||||||
0, /* tp_traverse */
|
|
||||||
0, /* tp_clear */
|
|
||||||
0, /* tp_richcompare */
|
|
||||||
0, /* tp_weaklistoffset */
|
|
||||||
(getiterfunc)scss_BlockLocator_iter, /* tp_iter: __iter__() method */
|
|
||||||
(iternextfunc)scss_BlockLocator_iternext, /* tp_iternext: next() method */
|
|
||||||
0, /* tp_methods */
|
|
||||||
0, /* tp_members */
|
|
||||||
0, /* tp_getset */
|
|
||||||
0, /* tp_base */
|
|
||||||
0, /* tp_dict */
|
|
||||||
0, /* tp_descr_get */
|
|
||||||
0, /* tp_descr_set */
|
|
||||||
0, /* tp_dictoffset */
|
|
||||||
(initproc)scss_BlockLocator_init, /* tp_init */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/* Scanner */
|
|
||||||
static PyObject *PyExc_scss_NoMoreTokens;
|
|
||||||
|
|
||||||
staticforward PyTypeObject scss_ScannerType;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD
|
|
||||||
Scanner *scanner;
|
|
||||||
} scss_Scanner;
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
scss_Scanner_rewind(scss_Scanner *self, PyObject *args)
|
|
||||||
{
|
|
||||||
int token_num;
|
|
||||||
if (self->scanner != NULL) {
|
|
||||||
if (PyArg_ParseTuple(args, "i", &token_num)) {
|
|
||||||
Scanner_rewind(self->scanner, token_num);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
return (PyObject *)Py_None;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
scss_Scanner_token(scss_Scanner *self, PyObject *args)
|
|
||||||
{
|
|
||||||
PyObject *iter;
|
|
||||||
PyObject *item;
|
|
||||||
long size;
|
|
||||||
|
|
||||||
Token *p_token;
|
|
||||||
|
|
||||||
int token_num;
|
|
||||||
PyObject *restrictions = NULL;
|
|
||||||
Pattern *_restrictions = NULL;
|
|
||||||
int restrictions_sz = 0;
|
|
||||||
if (self->scanner != NULL) {
|
|
||||||
if (PyArg_ParseTuple(args, "i|O", &token_num, &restrictions)) {
|
|
||||||
if (restrictions != NULL) {
|
|
||||||
size = PySequence_Size(restrictions);
|
|
||||||
if (size != -1) {
|
|
||||||
_restrictions = PyMem_New(Pattern, size);
|
|
||||||
iter = PyObject_GetIter(restrictions);
|
|
||||||
while ((item = PyIter_Next(iter))) {
|
|
||||||
if (PyString_Check(item)) {
|
|
||||||
_restrictions[restrictions_sz].tok = PyString_AsString(item);
|
|
||||||
_restrictions[restrictions_sz].expr = NULL;
|
|
||||||
restrictions_sz++;
|
|
||||||
}
|
|
||||||
Py_DECREF(item);
|
|
||||||
}
|
|
||||||
Py_DECREF(iter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p_token = Scanner_token(self->scanner, token_num, _restrictions, restrictions_sz);
|
|
||||||
|
|
||||||
if (_restrictions != NULL) PyMem_Del(_restrictions);
|
|
||||||
|
|
||||||
if (p_token == (Token *)SCANNER_EXC_BAD_TOKEN) {
|
|
||||||
PyErr_SetString(PyExc_SyntaxError, self->scanner->exc);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
if (p_token == (Token *)SCANNER_EXC_RESTRICTED) {
|
|
||||||
PyErr_SetString(PyExc_SyntaxError, self->scanner->exc);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
if (p_token == (Token *)SCANNER_EXC_UNIMPLEMENTED) {
|
|
||||||
PyErr_SetString(PyExc_NotImplementedError, self->scanner->exc);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
if (p_token == (Token *)SCANNER_EXC_NO_MORE_TOKENS) {
|
|
||||||
PyErr_SetNone(PyExc_scss_NoMoreTokens);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
if (p_token < 0) {
|
|
||||||
PyErr_SetNone(PyExc_Exception);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return Py_BuildValue(
|
|
||||||
"iiss#",
|
|
||||||
p_token->string - self->scanner->input,
|
|
||||||
p_token->string - self->scanner->input + p_token->string_sz,
|
|
||||||
p_token->regex->tok,
|
|
||||||
p_token->string,
|
|
||||||
p_token->string_sz
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
return (PyObject *)Py_None;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
scss_Scanner_reset(scss_Scanner *self, PyObject *args, PyObject *kwds)
|
|
||||||
{
|
|
||||||
char *input = NULL;
|
|
||||||
int input_sz = 0;
|
|
||||||
|
|
||||||
if (self->scanner != NULL) {
|
|
||||||
if (PyArg_ParseTuple(args, "|z#", &input, &input_sz)) {
|
|
||||||
Scanner_reset(self->scanner, input, input_sz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
return (PyObject *)Py_None;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
scss_Scanner_setup_patterns(PyObject *self, PyObject *patterns)
|
|
||||||
{
|
|
||||||
PyObject *item, *item0, *item1;
|
|
||||||
int i, is_tuple, _is_tuple;
|
|
||||||
long size;
|
|
||||||
|
|
||||||
Pattern *_patterns = NULL;
|
|
||||||
int patterns_sz = 0;
|
|
||||||
if (!Scanner_initialized()) {
|
|
||||||
is_tuple = PyTuple_Check(patterns);
|
|
||||||
if (is_tuple || PyList_Check(patterns)) {
|
|
||||||
size = is_tuple ? PyTuple_Size(patterns) : PyList_Size(patterns);
|
|
||||||
_patterns = PyMem_New(Pattern, size);
|
|
||||||
for (i = 0; i < size; ++i) {
|
|
||||||
item = is_tuple ? PyTuple_GetItem(patterns, i) : PyList_GetItem(patterns, i);
|
|
||||||
_is_tuple = PyTuple_Check(item);
|
|
||||||
if (_is_tuple || PyList_Check(item)) {
|
|
||||||
item0 = _is_tuple ? PyTuple_GetItem(item, 0) : PyList_GetItem(item, 0);
|
|
||||||
item1 = _is_tuple ? PyTuple_GetItem(item, 1) : PyList_GetItem(item, 1);
|
|
||||||
if (PyString_Check(item0) && PyString_Check(item1)) {
|
|
||||||
_patterns[patterns_sz].tok = PyString_AsString(item0);
|
|
||||||
_patterns[patterns_sz].expr = PyString_AsString(item1);
|
|
||||||
patterns_sz++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Scanner_initialize(_patterns, patterns_sz);
|
|
||||||
if (_patterns != NULL) PyMem_Del(_patterns);
|
|
||||||
}
|
|
||||||
Py_INCREF(Py_None);
|
|
||||||
return (PyObject *)Py_None;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
scss_Scanner_init(scss_Scanner *self, PyObject *args, PyObject *kwds)
|
|
||||||
{
|
|
||||||
PyObject *item, *item0, *item1;
|
|
||||||
int i, is_tuple, _is_tuple;
|
|
||||||
long size;
|
|
||||||
|
|
||||||
PyObject *patterns, *ignore;
|
|
||||||
Pattern *_patterns = NULL;
|
|
||||||
int patterns_sz = 0;
|
|
||||||
Pattern *_ignore = NULL;
|
|
||||||
int ignore_sz = 0;
|
|
||||||
char *input = NULL;
|
|
||||||
int input_sz = 0;
|
|
||||||
|
|
||||||
self->scanner = NULL;
|
|
||||||
|
|
||||||
if (!PyArg_ParseTuple(args, "OO|z#", &patterns, &ignore, &input, &input_sz)) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Scanner_initialized()) {
|
|
||||||
is_tuple = PyTuple_Check(patterns);
|
|
||||||
if (is_tuple || PyList_Check(patterns)) {
|
|
||||||
size = is_tuple ? PyTuple_Size(patterns) : PyList_Size(patterns);
|
|
||||||
_patterns = PyMem_New(Pattern, size);
|
|
||||||
for (i = 0; i < size; ++i) {
|
|
||||||
item = is_tuple ? PyTuple_GetItem(patterns, i) : PyList_GetItem(patterns, i);
|
|
||||||
_is_tuple = PyTuple_Check(item);
|
|
||||||
if (_is_tuple || PyList_Check(item)) {
|
|
||||||
item0 = _is_tuple ? PyTuple_GetItem(item, 0) : PyList_GetItem(item, 0);
|
|
||||||
item1 = _is_tuple ? PyTuple_GetItem(item, 1) : PyList_GetItem(item, 1);
|
|
||||||
if (PyString_Check(item0) && PyString_Check(item1)) {
|
|
||||||
_patterns[patterns_sz].tok = PyString_AsString(item0);
|
|
||||||
_patterns[patterns_sz].expr = PyString_AsString(item1);
|
|
||||||
patterns_sz++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Scanner_initialize(_patterns, patterns_sz);
|
|
||||||
}
|
|
||||||
|
|
||||||
is_tuple = PyTuple_Check(ignore);
|
|
||||||
if (is_tuple || PyList_Check(ignore)) {
|
|
||||||
size = is_tuple ? PyTuple_Size(ignore) : PyList_Size(ignore);
|
|
||||||
_ignore = PyMem_New(Pattern, size);
|
|
||||||
for (i = 0; i < size; ++i) {
|
|
||||||
item = is_tuple ? PyTuple_GetItem(ignore, i) : PyList_GetItem(ignore, i);
|
|
||||||
if (PyString_Check(item)) {
|
|
||||||
_ignore[ignore_sz].tok = PyString_AsString(item);
|
|
||||||
_ignore[ignore_sz].expr = NULL;
|
|
||||||
ignore_sz++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self->scanner = Scanner_new(_patterns, patterns_sz, _ignore, ignore_sz, input, input_sz);
|
|
||||||
|
|
||||||
if (_patterns != NULL) PyMem_Del(_patterns);
|
|
||||||
if (_ignore != NULL) PyMem_Del(_ignore);
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
PySys_WriteStderr("Scss Scanner object initialized! (%lu bytes)\n", sizeof(scss_Scanner));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
scss_Scanner_repr(scss_Scanner *self)
|
|
||||||
{
|
|
||||||
/* Print the last 10 tokens that have been scanned in */
|
|
||||||
PyObject *repr, *tmp;
|
|
||||||
Token *p_token;
|
|
||||||
int i, start, pos;
|
|
||||||
|
|
||||||
if (self->scanner != NULL && self->scanner->tokens_sz) {
|
|
||||||
start = self->scanner->tokens_sz - 10;
|
|
||||||
repr = PyString_FromString("");
|
|
||||||
for (i = (start < 0) ? 0 : start; i < self->scanner->tokens_sz; i++) {
|
|
||||||
p_token = &self->scanner->tokens[i];
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromString("\n"));
|
|
||||||
pos = (int)(p_token->string - self->scanner->input);
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromFormat(" (@%d) %s = ",
|
|
||||||
pos, p_token->regex->tok));
|
|
||||||
tmp = PyString_FromStringAndSize(p_token->string, p_token->string_sz);
|
|
||||||
PyString_ConcatAndDel(&repr, PyObject_Repr(tmp));
|
|
||||||
Py_XDECREF(tmp);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
repr = PyString_FromString("None");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (PyObject *)repr;
|
|
||||||
|
|
||||||
/*
|
|
||||||
PyObject *repr, *tmp, *tmp2;
|
|
||||||
Token *p_token;
|
|
||||||
char *tok;
|
|
||||||
int i, start, first = 1, cur, max=0, pos;
|
|
||||||
|
|
||||||
if (self->scanner != NULL && self->scanner->tokens_sz) {
|
|
||||||
start = self->scanner->tokens_sz - 10;
|
|
||||||
repr = PyString_FromString("");
|
|
||||||
for (i = (start < 0) ? 0 : start; i < self->scanner->tokens_sz; i++) {
|
|
||||||
p_token = self->scanner->tokens[i];
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromString("\n"));
|
|
||||||
pos = (int)(p_token->string - self->scanner->input);
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromFormat(" (@%d) %s = ",
|
|
||||||
pos, p_token->regex->tok));
|
|
||||||
tmp = PyString_FromString(p_token->string);
|
|
||||||
PyString_ConcatAndDel(&repr, PyObject_Repr(tmp));
|
|
||||||
Py_XDECREF(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
start = self->scanner->tokens_sz - 10;
|
|
||||||
for (i = (start < 0) ? 0 : start; i < self->scanner->tokens_sz; i++) {
|
|
||||||
p_token = self->scanner->tokens[i];
|
|
||||||
cur = strlen(p_token->regex->tok) * 2;
|
|
||||||
if (cur > max) max = cur;
|
|
||||||
}
|
|
||||||
tok = PyMem_New(char, max + 4);
|
|
||||||
repr = PyString_FromString("");
|
|
||||||
for (i = (start < 0) ? 0 : start; i < self->scanner->tokens_sz; i++) {
|
|
||||||
p_token = self->scanner->tokens[i];
|
|
||||||
if (!first) PyString_ConcatAndDel(&repr, PyString_FromString("\n"));
|
|
||||||
|
|
||||||
pos = (int)(p_token->string - self->scanner->input);
|
|
||||||
if (pos < 10) PyString_ConcatAndDel(&repr, PyString_FromString(" "));
|
|
||||||
if (pos < 100) PyString_ConcatAndDel(&repr, PyString_FromString(" "));
|
|
||||||
if (pos < 1000) PyString_ConcatAndDel(&repr, PyString_FromString(" "));
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromFormat("(@%d) ",
|
|
||||||
pos));
|
|
||||||
|
|
||||||
tmp = PyString_FromString(p_token->regex->tok);
|
|
||||||
tmp2 = PyObject_Repr(tmp);
|
|
||||||
memset(tok, ' ', max + 4);
|
|
||||||
tok[max + 3 - PyString_Size(tmp2)] = '\0';
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromString(tok));
|
|
||||||
PyString_ConcatAndDel(&repr, tmp2);
|
|
||||||
Py_XDECREF(tmp);
|
|
||||||
|
|
||||||
PyString_ConcatAndDel(&repr, PyString_FromString(" = "));
|
|
||||||
tmp = PyString_FromString(p_token->string);
|
|
||||||
PyString_ConcatAndDel(&repr, PyObject_Repr(tmp));
|
|
||||||
Py_XDECREF(tmp);
|
|
||||||
|
|
||||||
first = 0;
|
|
||||||
}
|
|
||||||
PyMem_Del(tok);
|
|
||||||
} else {
|
|
||||||
repr = PyString_FromString("None");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (PyObject *)repr;
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
scss_Scanner_dealloc(scss_Scanner *self)
|
|
||||||
{
|
|
||||||
if (self->scanner != NULL) Scanner_del(self->scanner);
|
|
||||||
|
|
||||||
self->ob_type->tp_free((PyObject*)self);
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
PySys_WriteStderr("Scss Scanner object destroyed!\n");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static PyMethodDef scss_Scanner_methods[] = {
|
|
||||||
{"reset", (PyCFunction)scss_Scanner_reset, METH_VARARGS, "Scan the next token"},
|
|
||||||
{"token", (PyCFunction)scss_Scanner_token, METH_VARARGS, "Get the nth token"},
|
|
||||||
{"rewind", (PyCFunction)scss_Scanner_rewind, METH_VARARGS, "Rewind scanner"},
|
|
||||||
{"setup_patterns", (PyCFunction)scss_Scanner_setup_patterns, METH_O | METH_STATIC, "Initialize patterns."},
|
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
||||||
};
|
|
||||||
|
|
||||||
static PyTypeObject scss_ScannerType = {
|
|
||||||
PyObject_HEAD_INIT(NULL)
|
|
||||||
0, /* ob_size */
|
|
||||||
"scss.Scanner", /* tp_name */
|
|
||||||
sizeof(scss_Scanner), /* tp_basicsize */
|
|
||||||
0, /* tp_itemsize */
|
|
||||||
(destructor)scss_Scanner_dealloc, /* tp_dealloc */
|
|
||||||
0, /* tp_print */
|
|
||||||
0, /* tp_getattr */
|
|
||||||
0, /* tp_setattr */
|
|
||||||
0, /* tp_compare */
|
|
||||||
(reprfunc)scss_Scanner_repr, /* tp_repr */
|
|
||||||
0, /* tp_as_number */
|
|
||||||
0, /* tp_as_sequence */
|
|
||||||
0, /* tp_as_mapping */
|
|
||||||
0, /* tp_hash */
|
|
||||||
0, /* tp_call */
|
|
||||||
0, /* tp_str */
|
|
||||||
0, /* tp_getattro */
|
|
||||||
0, /* tp_setattro */
|
|
||||||
0, /* tp_as_buffer */
|
|
||||||
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
|
|
||||||
"Scanner object.", /* tp_doc */
|
|
||||||
0, /* tp_traverse */
|
|
||||||
0, /* tp_clear */
|
|
||||||
0, /* tp_richcompare */
|
|
||||||
0, /* tp_weaklistoffset */
|
|
||||||
0, /* tp_iter: __iter__() method */
|
|
||||||
0, /* tp_iternext: next() method */
|
|
||||||
scss_Scanner_methods, /* tp_methods */
|
|
||||||
0, /* tp_members */
|
|
||||||
0, /* tp_getset */
|
|
||||||
0, /* tp_base */
|
|
||||||
0, /* tp_dict */
|
|
||||||
0, /* tp_descr_get */
|
|
||||||
0, /* tp_descr_set */
|
|
||||||
0, /* tp_dictoffset */
|
|
||||||
(initproc)scss_Scanner_init, /* tp_init */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/* Python constructor */
|
|
||||||
|
|
||||||
static PyObject *
|
|
||||||
scss_locate_blocks(PyObject *self, PyObject *args)
|
|
||||||
{
|
|
||||||
scss_BlockLocator *result = NULL;
|
|
||||||
|
|
||||||
result = PyObject_New(scss_BlockLocator, &scss_BlockLocatorType);
|
|
||||||
if (result) {
|
|
||||||
scss_BlockLocator_init(result, args, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (PyObject *)result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Module functions */
|
|
||||||
|
|
||||||
static PyMethodDef scss_methods[] = {
|
|
||||||
{"locate_blocks", (PyCFunction)scss_locate_blocks, METH_VARARGS, "Locate Scss blocks."},
|
|
||||||
{NULL, NULL, 0, NULL} /* Sentinel */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/* Module init function */
|
|
||||||
|
|
||||||
PyMODINIT_FUNC
|
|
||||||
init_speedups(void)
|
|
||||||
{
|
|
||||||
PyObject* m;
|
|
||||||
|
|
||||||
scss_BlockLocatorType.tp_new = PyType_GenericNew;
|
|
||||||
if (PyType_Ready(&scss_BlockLocatorType) < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
scss_ScannerType.tp_new = PyType_GenericNew;
|
|
||||||
if (PyType_Ready(&scss_ScannerType) < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
BlockLocator_initialize();
|
|
||||||
Scanner_initialize(NULL, 0);
|
|
||||||
|
|
||||||
m = Py_InitModule("_speedups", scss_methods);
|
|
||||||
|
|
||||||
Py_INCREF(&scss_BlockLocatorType);
|
|
||||||
PyModule_AddObject(m, "_BlockLocator", (PyObject *)&scss_BlockLocatorType);
|
|
||||||
|
|
||||||
Py_INCREF(&scss_ScannerType);
|
|
||||||
PyModule_AddObject(m, "Scanner", (PyObject *)&scss_ScannerType);
|
|
||||||
|
|
||||||
PyExc_scss_NoMoreTokens = PyErr_NewException("_speedups.NoMoreTokens", NULL, NULL);
|
|
||||||
Py_INCREF(PyExc_scss_NoMoreTokens);
|
|
||||||
PyModule_AddObject(m, "NoMoreTokens", (PyObject *)PyExc_scss_NoMoreTokens);
|
|
||||||
}
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
/*
|
|
||||||
* pyScss, a Scss compiler for Python
|
|
||||||
* SCSS blocks scanner.
|
|
||||||
*
|
|
||||||
* German M. Bravo (Kronuz) <german.mb@gmail.com>
|
|
||||||
* https://github.com/Kronuz/pyScss
|
|
||||||
*
|
|
||||||
* MIT license (http://www.opensource.org/licenses/mit-license.php)
|
|
||||||
* Copyright (c) 2011 German M. Bravo (Kronuz), All rights reserved.
|
|
||||||
*/
|
|
||||||
#include <Python.h>
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include "block_locator.h"
|
|
||||||
|
|
||||||
int _strip(char *begin, char *end, int *lineno) {
|
|
||||||
// " 1\0 some, \n 2\0 aca "
|
|
||||||
int _cnt,
|
|
||||||
cnt = 0,
|
|
||||||
pass = 1,
|
|
||||||
addnl = 0;
|
|
||||||
char c,
|
|
||||||
*line = NULL,
|
|
||||||
*first = begin,
|
|
||||||
*last = begin,
|
|
||||||
*write = lineno ? begin : NULL;
|
|
||||||
while (begin < end) {
|
|
||||||
c = *begin;
|
|
||||||
if (c == '\0') {
|
|
||||||
if (lineno && line == NULL) {
|
|
||||||
line = first - 1;
|
|
||||||
do {
|
|
||||||
c = *++line;
|
|
||||||
} while (c == ' ' || c == '\t' || c == '\n' || c == ';');
|
|
||||||
if (c != '\0') {
|
|
||||||
sscanf(line, "%d", lineno);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
first = last = begin + 1;
|
|
||||||
pass = 1;
|
|
||||||
} else if (c == '\n') {
|
|
||||||
_cnt = (int)(last - first);
|
|
||||||
if (_cnt > 0) {
|
|
||||||
cnt += _cnt + addnl;
|
|
||||||
if (write != NULL) {
|
|
||||||
if (addnl) {
|
|
||||||
*write++ = '\n';
|
|
||||||
}
|
|
||||||
while (first < last) {
|
|
||||||
*write++ = *first++;
|
|
||||||
}
|
|
||||||
addnl = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
first = last = begin + 1;
|
|
||||||
pass = 1;
|
|
||||||
} else if (c == ' ' || c == '\t') {
|
|
||||||
if (pass) {
|
|
||||||
first = last = begin + 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
last = begin + 1;
|
|
||||||
pass = 0;
|
|
||||||
}
|
|
||||||
begin++;
|
|
||||||
}
|
|
||||||
_cnt = (int)(last - first);
|
|
||||||
if (_cnt > 0) {
|
|
||||||
cnt += _cnt + addnl;
|
|
||||||
if (write != NULL) {
|
|
||||||
if (addnl) {
|
|
||||||
*write++ = '\n';
|
|
||||||
}
|
|
||||||
while (first < last) {
|
|
||||||
*write++ = *first++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cnt;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* BlockLocator */
|
|
||||||
|
|
||||||
typedef void _BlockLocator_Callback(BlockLocator*);
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_start_string(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// A string starts
|
|
||||||
self->instr = *(self->codestr_ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_end_string(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// A string ends (FIXME: needs to accept escaped characters)
|
|
||||||
self->instr = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_start_parenthesis(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// parenthesis begins:
|
|
||||||
self->par++;
|
|
||||||
self->thin = NULL;
|
|
||||||
self->safe = self->codestr_ptr + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_end_parenthesis(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
self->par--;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_flush_properties(BlockLocator *self) {
|
|
||||||
int len, lineno = -1;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Flush properties
|
|
||||||
if (self->lose <= self->init) {
|
|
||||||
len = _strip(self->lose, self->init, &lineno);
|
|
||||||
if (len) {
|
|
||||||
if (lineno != -1) {
|
|
||||||
self->lineno = lineno;
|
|
||||||
}
|
|
||||||
|
|
||||||
self->block.selprop = self->lose;
|
|
||||||
self->block.selprop_sz = len;
|
|
||||||
self->block.codestr = NULL;
|
|
||||||
self->block.codestr_sz = 0;
|
|
||||||
self->block.lineno = self->lineno;
|
|
||||||
self->block.error = 1;
|
|
||||||
}
|
|
||||||
self->lose = self->init;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_start_block1(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Start block:
|
|
||||||
if (self->codestr_ptr > self->codestr && *(self->codestr_ptr - 1) == '#') {
|
|
||||||
self->skip = 1;
|
|
||||||
} else {
|
|
||||||
self->start = self->codestr_ptr;
|
|
||||||
if (self->thin != NULL && _strip(self->thin, self->codestr_ptr, NULL)) {
|
|
||||||
self->init = self->thin;
|
|
||||||
}
|
|
||||||
_BlockLocator_flush_properties(self);
|
|
||||||
self->thin = NULL;
|
|
||||||
}
|
|
||||||
self->depth++;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_start_block(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Start block:
|
|
||||||
self->depth++;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_end_block1(BlockLocator *self) {
|
|
||||||
int len, lineno = -1;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Block ends:
|
|
||||||
self->depth--;
|
|
||||||
if (!self->skip) {
|
|
||||||
self->end = self->codestr_ptr;
|
|
||||||
len = _strip(self->init, self->start, &lineno);
|
|
||||||
if (lineno != -1) {
|
|
||||||
self->lineno = lineno;
|
|
||||||
}
|
|
||||||
|
|
||||||
self->block.selprop = self->init;
|
|
||||||
self->block.selprop_sz = len;
|
|
||||||
self->block.codestr = (self->start + 1);
|
|
||||||
self->block.codestr_sz = (int)(self->end - (self->start + 1));
|
|
||||||
self->block.lineno = self->lineno;
|
|
||||||
self->block.error = 1;
|
|
||||||
|
|
||||||
self->init = self->safe = self->lose = self->end + 1;
|
|
||||||
self->thin = NULL;
|
|
||||||
}
|
|
||||||
self->skip = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_end_block(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Block ends:
|
|
||||||
self->depth--;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_end_property(BlockLocator *self) {
|
|
||||||
int len, lineno = -1;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// End of property (or block):
|
|
||||||
self->init = self->codestr_ptr;
|
|
||||||
if (self->lose <= self->init) {
|
|
||||||
len = _strip(self->lose, self->init, &lineno);
|
|
||||||
if (len) {
|
|
||||||
if (lineno != -1) {
|
|
||||||
self->lineno = lineno;
|
|
||||||
}
|
|
||||||
|
|
||||||
self->block.selprop = self->lose;
|
|
||||||
self->block.selprop_sz = len;
|
|
||||||
self->block.codestr = NULL;
|
|
||||||
self->block.codestr_sz = 0;
|
|
||||||
self->block.lineno = self->lineno;
|
|
||||||
self->block.error = 1;
|
|
||||||
}
|
|
||||||
self->init = self->safe = self->lose = self->codestr_ptr + 1;
|
|
||||||
}
|
|
||||||
self->thin = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_mark_safe(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// We are on a safe zone
|
|
||||||
if (self->thin != NULL && _strip(self->thin, self->codestr_ptr, NULL)) {
|
|
||||||
self->init = self->thin;
|
|
||||||
}
|
|
||||||
self->thin = NULL;
|
|
||||||
self->safe = self->codestr_ptr + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
_BlockLocator_mark_thin(BlockLocator *self) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Step on thin ice, if it breaks, it breaks here
|
|
||||||
if (self->thin != NULL && _strip(self->thin, self->codestr_ptr, NULL)) {
|
|
||||||
self->init = self->thin;
|
|
||||||
self->thin = self->codestr_ptr + 1;
|
|
||||||
} else if (self->thin == NULL && _strip(self->safe, self->codestr_ptr, NULL)) {
|
|
||||||
self->thin = self->codestr_ptr + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int function_map_initialized = 0;
|
|
||||||
_BlockLocator_Callback* scss_function_map[256 * 256 * 2 * 3]; // (c, instr, par, depth)
|
|
||||||
|
|
||||||
static void
|
|
||||||
init_function_map(void) {
|
|
||||||
int i;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (function_map_initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
function_map_initialized = 1;
|
|
||||||
|
|
||||||
for (i = 0; i < 256 * 256 * 2 * 3; i++) {
|
|
||||||
scss_function_map[i] = NULL;
|
|
||||||
}
|
|
||||||
scss_function_map[(int)'\"' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*0 + 256*256*1 + 256*256*2*0] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*0 + 256*256*1 + 256*256*2*0] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*0 + 256*256*0 + 256*256*2*1] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*0 + 256*256*0 + 256*256*2*1] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*0 + 256*256*1 + 256*256*2*1] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*0 + 256*256*1 + 256*256*2*1] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*0 + 256*256*0 + 256*256*2*2] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*0 + 256*256*0 + 256*256*2*2] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*0 + 256*256*1 + 256*256*2*2] = _BlockLocator_start_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*0 + 256*256*1 + 256*256*2*2] = _BlockLocator_start_string;
|
|
||||||
|
|
||||||
scss_function_map[(int)'\"' + 256*(int)'\"' + 256*256*0 + 256*256*2*0] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*(int)'\'' + 256*256*0 + 256*256*2*0] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*(int)'\"' + 256*256*1 + 256*256*2*0] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*(int)'\'' + 256*256*1 + 256*256*2*0] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*(int)'\"' + 256*256*0 + 256*256*2*1] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*(int)'\'' + 256*256*0 + 256*256*2*1] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*(int)'\"' + 256*256*1 + 256*256*2*1] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*(int)'\'' + 256*256*1 + 256*256*2*1] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*(int)'\"' + 256*256*0 + 256*256*2*2] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*(int)'\'' + 256*256*0 + 256*256*2*2] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\"' + 256*(int)'\"' + 256*256*1 + 256*256*2*2] = _BlockLocator_end_string;
|
|
||||||
scss_function_map[(int)'\'' + 256*(int)'\'' + 256*256*1 + 256*256*2*2] = _BlockLocator_end_string;
|
|
||||||
|
|
||||||
scss_function_map[(int)'(' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_start_parenthesis;
|
|
||||||
scss_function_map[(int)'(' + 256*0 + 256*256*1 + 256*256*2*0] = _BlockLocator_start_parenthesis;
|
|
||||||
scss_function_map[(int)'(' + 256*0 + 256*256*0 + 256*256*2*1] = _BlockLocator_start_parenthesis;
|
|
||||||
scss_function_map[(int)'(' + 256*0 + 256*256*1 + 256*256*2*1] = _BlockLocator_start_parenthesis;
|
|
||||||
scss_function_map[(int)'(' + 256*0 + 256*256*0 + 256*256*2*2] = _BlockLocator_start_parenthesis;
|
|
||||||
scss_function_map[(int)'(' + 256*0 + 256*256*1 + 256*256*2*2] = _BlockLocator_start_parenthesis;
|
|
||||||
|
|
||||||
scss_function_map[(int)')' + 256*0 + 256*256*1 + 256*256*2*0] = _BlockLocator_end_parenthesis;
|
|
||||||
scss_function_map[(int)')' + 256*0 + 256*256*1 + 256*256*2*1] = _BlockLocator_end_parenthesis;
|
|
||||||
scss_function_map[(int)')' + 256*0 + 256*256*1 + 256*256*2*2] = _BlockLocator_end_parenthesis;
|
|
||||||
|
|
||||||
scss_function_map[(int)'{' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_start_block1;
|
|
||||||
scss_function_map[(int)'{' + 256*0 + 256*256*0 + 256*256*2*1] = _BlockLocator_start_block;
|
|
||||||
scss_function_map[(int)'{' + 256*0 + 256*256*0 + 256*256*2*2] = _BlockLocator_start_block;
|
|
||||||
|
|
||||||
scss_function_map[(int)'}' + 256*0 + 256*256*0 + 256*256*2*1] = _BlockLocator_end_block1;
|
|
||||||
scss_function_map[(int)'}' + 256*0 + 256*256*0 + 256*256*2*2] = _BlockLocator_end_block;
|
|
||||||
|
|
||||||
scss_function_map[(int)';' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_end_property;
|
|
||||||
|
|
||||||
scss_function_map[(int)',' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_mark_safe;
|
|
||||||
|
|
||||||
scss_function_map[(int)'\n' + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_mark_thin;
|
|
||||||
|
|
||||||
scss_function_map[0 + 256*0 + 256*256*0 + 256*256*2*0] = _BlockLocator_flush_properties;
|
|
||||||
scss_function_map[0 + 256*0 + 256*256*0 + 256*256*2*1] = _BlockLocator_flush_properties;
|
|
||||||
scss_function_map[0 + 256*0 + 256*256*0 + 256*256*2*2] = _BlockLocator_flush_properties;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tScss function maps initialized!\n");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* BlockLocator public interface */
|
|
||||||
|
|
||||||
void
|
|
||||||
BlockLocator_initialize(void)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
init_function_map();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
BlockLocator_finalize(void)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockLocator *
|
|
||||||
BlockLocator_new(char *codestr, int codestr_sz)
|
|
||||||
{
|
|
||||||
BlockLocator *self;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
self = PyMem_New(BlockLocator, 1);
|
|
||||||
if (self) {
|
|
||||||
memset(self, 0, sizeof(BlockLocator));
|
|
||||||
self->_codestr = PyMem_New(char, codestr_sz);
|
|
||||||
memcpy(self->_codestr, codestr, codestr_sz);
|
|
||||||
self->codestr_sz = codestr_sz;
|
|
||||||
self->codestr = PyMem_New(char, self->codestr_sz);
|
|
||||||
memcpy(self->codestr, self->_codestr, self->codestr_sz);
|
|
||||||
self->codestr_ptr = self->codestr;
|
|
||||||
self->lineno = 0;
|
|
||||||
self->par = 0;
|
|
||||||
self->instr = 0;
|
|
||||||
self->depth = 0;
|
|
||||||
self->skip = 0;
|
|
||||||
self->thin = self->codestr;
|
|
||||||
self->init = self->codestr;
|
|
||||||
self->safe = self->codestr;
|
|
||||||
self->lose = self->codestr;
|
|
||||||
self->start = NULL;
|
|
||||||
self->end = NULL;
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tScss BlockLocator object created (%d bytes)!\n", codestr_sz);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
BlockLocator_del(BlockLocator *self)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
free(self->codestr);
|
|
||||||
free(self->_codestr);
|
|
||||||
free(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
BlockLocator_rewind(BlockLocator *self)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
free(self->codestr);
|
|
||||||
self->codestr = PyMem_New(char, self->codestr_sz);
|
|
||||||
memcpy(self->codestr, self->_codestr, self->codestr_sz);
|
|
||||||
self->codestr_ptr = self->codestr;
|
|
||||||
self->lineno = 0;
|
|
||||||
self->par = 0;
|
|
||||||
self->instr = 0;
|
|
||||||
self->depth = 0;
|
|
||||||
self->skip = 0;
|
|
||||||
self->thin = self->codestr;
|
|
||||||
self->init = self->codestr;
|
|
||||||
self->safe = self->codestr;
|
|
||||||
self->lose = self->codestr;
|
|
||||||
self->start = NULL;
|
|
||||||
self->end = NULL;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tScss BlockLocator object rewound!\n");
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
Block*
|
|
||||||
BlockLocator_iternext(BlockLocator *self)
|
|
||||||
{
|
|
||||||
_BlockLocator_Callback *fn;
|
|
||||||
unsigned char c = 0;
|
|
||||||
char *codestr_end = self->codestr + self->codestr_sz;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
memset(&self->block, 0, sizeof(Block));
|
|
||||||
|
|
||||||
while (self->codestr_ptr < codestr_end) {
|
|
||||||
c = *(self->codestr_ptr);
|
|
||||||
if (!c) {
|
|
||||||
self->codestr_ptr++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
repeat:
|
|
||||||
|
|
||||||
fn = scss_function_map[
|
|
||||||
(int)c +
|
|
||||||
256 * self->instr +
|
|
||||||
256 * 256 * (int)(self->par != 0) +
|
|
||||||
256 * 256 * 2 * (int)(self->depth > 1 ? 2 : self->depth)
|
|
||||||
];
|
|
||||||
|
|
||||||
if (fn != NULL) {
|
|
||||||
fn(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
self->codestr_ptr++;
|
|
||||||
if (self->codestr_ptr > codestr_end) {
|
|
||||||
self->codestr_ptr = codestr_end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self->block.error) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
if (self->block.error > 0) {
|
|
||||||
fprintf(stderr, "\tBlock found!\n");
|
|
||||||
} else {
|
|
||||||
fprintf(stderr, "\tException!\n");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return &self->block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (self->par > 0) {
|
|
||||||
if (self->block.error >= 0) {
|
|
||||||
self->block.error = -1;
|
|
||||||
sprintf(self->exc, "Missing closing parenthesis somewhere in block");
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\t%s\n", self->exc);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else if (self->instr != 0) {
|
|
||||||
if (self->block.error >= 0) {
|
|
||||||
self->block.error = -2;
|
|
||||||
sprintf(self->exc, "Missing closing string somewhere in block");
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\t%s\n", self->exc);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else if (self->depth > 0) {
|
|
||||||
if (self->block.error >= 0) {
|
|
||||||
self->block.error = -3;
|
|
||||||
sprintf(self->exc, "Missing closing string somewhere in block");
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\t%s\n", self->exc);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
if (self->init < codestr_end) {
|
|
||||||
c = '}';
|
|
||||||
goto repeat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (self->init < codestr_end) {
|
|
||||||
self->init = codestr_end;
|
|
||||||
c = 0;
|
|
||||||
goto repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
BlockLocator_rewind(self);
|
|
||||||
|
|
||||||
return &self->block;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* pyScss, a Scss compiler for Python
|
|
||||||
* SCSS blocks scanner.
|
|
||||||
*
|
|
||||||
* German M. Bravo (Kronuz) <german.mb@gmail.com>
|
|
||||||
* https://github.com/Kronuz/pyScss
|
|
||||||
*
|
|
||||||
* MIT license (http://www.opensource.org/licenses/mit-license.php)
|
|
||||||
* Copyright (c) 2011 German M. Bravo (Kronuz), All rights reserved.
|
|
||||||
*/
|
|
||||||
#ifndef BLOCK_LOCATOR_H
|
|
||||||
#define BLOCK_LOCATOR_H
|
|
||||||
|
|
||||||
#define MAX_EXC_STRING 4096
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
int error;
|
|
||||||
int lineno;
|
|
||||||
char *selprop;
|
|
||||||
int selprop_sz;
|
|
||||||
char *codestr;
|
|
||||||
int codestr_sz;
|
|
||||||
} Block;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char exc[MAX_EXC_STRING];
|
|
||||||
char *_codestr;
|
|
||||||
char *codestr;
|
|
||||||
char *codestr_ptr;
|
|
||||||
int codestr_sz;
|
|
||||||
int lineno;
|
|
||||||
int par;
|
|
||||||
char instr;
|
|
||||||
int depth;
|
|
||||||
int skip;
|
|
||||||
char *thin;
|
|
||||||
char *init;
|
|
||||||
char *safe;
|
|
||||||
char *lose;
|
|
||||||
char *start;
|
|
||||||
char *end;
|
|
||||||
Block block;
|
|
||||||
} BlockLocator;
|
|
||||||
|
|
||||||
void BlockLocator_initialize(void);
|
|
||||||
void BlockLocator_finalize(void);
|
|
||||||
|
|
||||||
Block* BlockLocator_iternext(BlockLocator *self);
|
|
||||||
BlockLocator *BlockLocator_new(char *codestr, int codestr_sz);
|
|
||||||
void BlockLocator_del(BlockLocator *self);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
/*
|
|
||||||
* pyScss, a Scss compiler for Python
|
|
||||||
* SCSS blocks scanner.
|
|
||||||
*
|
|
||||||
* German M. Bravo (Kronuz) <german.mb@gmail.com>
|
|
||||||
* https://github.com/Kronuz/pyScss
|
|
||||||
*
|
|
||||||
* MIT license (http://www.opensource.org/licenses/mit-license.php)
|
|
||||||
* Copyright (c) 2011 German M. Bravo (Kronuz), All rights reserved.
|
|
||||||
*/
|
|
||||||
#include <Python.h>
|
|
||||||
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include "scanner.h"
|
|
||||||
|
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
int Pattern_patterns_sz = 0;
|
|
||||||
int Pattern_patterns_bsz = 0;
|
|
||||||
Pattern *Pattern_patterns = NULL;
|
|
||||||
int Pattern_patterns_initialized = 0;
|
|
||||||
|
|
||||||
Pattern*
|
|
||||||
Pattern_regex(char *tok, char *expr) {
|
|
||||||
int j;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (j = 0; j < Pattern_patterns_sz; j++) {
|
|
||||||
if (strcmp(Pattern_patterns[j].tok, tok) == 0) {
|
|
||||||
return &Pattern_patterns[j];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (expr) {
|
|
||||||
if (j >= Pattern_patterns_bsz) {
|
|
||||||
/* Needs to expand block */
|
|
||||||
Pattern_patterns_bsz = Pattern_patterns_bsz + BLOCK_SIZE_PATTERNS;
|
|
||||||
PyMem_Resize(Pattern_patterns, Pattern, Pattern_patterns_bsz);
|
|
||||||
}
|
|
||||||
Pattern_patterns[j].tok = PyMem_Strdup(tok);
|
|
||||||
Pattern_patterns[j].expr = PyMem_Strdup(expr);
|
|
||||||
Pattern_patterns[j].pattern = NULL;
|
|
||||||
Pattern_patterns_sz = j + 1;
|
|
||||||
return &Pattern_patterns[j];
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
Pattern_match(Pattern *regex, char *string, int string_sz, int start_at, Token *p_token) {
|
|
||||||
int options = PCRE_ANCHORED;
|
|
||||||
const char *errptr;
|
|
||||||
int ret, erroffset, ovector[3];
|
|
||||||
pcre *p_pattern = regex->pattern;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (p_pattern == NULL) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tpcre_compile %s\n", repr(regex->expr));
|
|
||||||
#endif
|
|
||||||
p_pattern = regex->pattern = pcre_compile(regex->expr, options, &errptr, &erroffset, NULL);
|
|
||||||
}
|
|
||||||
ret = pcre_exec(
|
|
||||||
p_pattern,
|
|
||||||
NULL, /* no extra data */
|
|
||||||
string,
|
|
||||||
string_sz,
|
|
||||||
start_at,
|
|
||||||
PCRE_ANCHORED, /* default options */
|
|
||||||
ovector, /* output vector for substring information */
|
|
||||||
3 /* number of elements in the output vector */
|
|
||||||
);
|
|
||||||
if (ret >= 0) {
|
|
||||||
if (p_token) {
|
|
||||||
p_token->regex = regex;
|
|
||||||
p_token->string = string + ovector[0];
|
|
||||||
p_token->string_sz = ovector[1] - ovector[0];
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void Pattern_initialize(Pattern *, int);
|
|
||||||
static void Pattern_setup(Pattern *, int);
|
|
||||||
static void Pattern_finalize(void);
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
Pattern_initialize(Pattern *patterns, int patterns_sz) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!Pattern_patterns_initialized) {
|
|
||||||
if (patterns_sz) {
|
|
||||||
Pattern_patterns_initialized = 1;
|
|
||||||
Pattern_setup(patterns, patterns_sz);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
Pattern_setup(Pattern *patterns, int patterns_sz) {
|
|
||||||
int i;
|
|
||||||
Pattern *regex;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!Pattern_patterns_initialized) {
|
|
||||||
Pattern_initialize(patterns, patterns_sz);
|
|
||||||
} else {
|
|
||||||
for (i = 0; i < patterns_sz; i++) {
|
|
||||||
regex = Pattern_regex(patterns[i].tok, patterns[i].expr);
|
|
||||||
#ifdef DEBUG
|
|
||||||
if (regex) {
|
|
||||||
fprintf(stderr, "\tAdded regex pattern %s: %s\n", repr(regex->tok), repr(regex->expr));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
Pattern_finalize(void) {
|
|
||||||
int j;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (Pattern_patterns_initialized) {
|
|
||||||
for (j = 0; j < Pattern_patterns_sz; j++) {
|
|
||||||
PyMem_Del(Pattern_patterns[j].tok);
|
|
||||||
PyMem_Del(Pattern_patterns[j].expr);
|
|
||||||
if (Pattern_patterns[j].pattern != NULL) {
|
|
||||||
pcre_free(Pattern_patterns[j].pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PyMem_Del(Pattern_patterns);
|
|
||||||
Pattern_patterns = NULL;
|
|
||||||
Pattern_patterns_sz = 0;
|
|
||||||
Pattern_patterns_bsz = 0;
|
|
||||||
Pattern_patterns_initialized = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scanner */
|
|
||||||
|
|
||||||
|
|
||||||
static long
|
|
||||||
_Scanner_scan(Scanner *self, Pattern *restrictions, int restrictions_sz)
|
|
||||||
{
|
|
||||||
Token best_token, *p_token;
|
|
||||||
Restriction *p_restriction;
|
|
||||||
Pattern *regex;
|
|
||||||
int j, k, max, skip;
|
|
||||||
size_t len;
|
|
||||||
char *aux;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
regex = NULL;
|
|
||||||
best_token.regex = NULL;
|
|
||||||
/* Search the patterns for a match, with earlier
|
|
||||||
tokens in the list having preference */
|
|
||||||
for (j = 0; j < Pattern_patterns_sz; j++) {
|
|
||||||
regex = &Pattern_patterns[j];
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tTrying %s: %s at pos %d -> %s\n", repr(regex->tok), repr(regex->expr), self->pos, repr(self->input));
|
|
||||||
#endif
|
|
||||||
/* First check to see if we're restricting to this token */
|
|
||||||
skip = restrictions_sz;
|
|
||||||
if (skip) {
|
|
||||||
max = (restrictions_sz > self->ignore_sz) ? restrictions_sz : self->ignore_sz;
|
|
||||||
for (k = 0; k < max; k++) {
|
|
||||||
if (k < restrictions_sz && strcmp(regex->tok, restrictions[k].tok) == 0) {
|
|
||||||
skip = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (k < self->ignore_sz && regex == self->ignore[k]) {
|
|
||||||
skip = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (skip) {
|
|
||||||
continue;
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tSkipping %s!\n", repr(regex->tok));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Pattern_match(
|
|
||||||
regex,
|
|
||||||
self->input,
|
|
||||||
self->input_sz,
|
|
||||||
self->pos,
|
|
||||||
&best_token
|
|
||||||
)) {
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "Match OK! %s: %s at pos %d\n", repr(regex->tok), repr(regex->expr), self->pos);
|
|
||||||
#endif
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* If we didn't find anything, raise an error */
|
|
||||||
if (best_token.regex == NULL) {
|
|
||||||
if (restrictions_sz) {
|
|
||||||
sprintf(self->exc, "SyntaxError[@ char %d: Bad token found while trying to find one of the restricted tokens: ", self->pos);
|
|
||||||
aux = self->exc + strlen(self->exc);
|
|
||||||
for (k=0; k<restrictions_sz; k++) {
|
|
||||||
len = strlen(restrictions[k].tok);
|
|
||||||
if (aux + len > self->exc + sizeof(self->exc) - 10) {
|
|
||||||
sprintf(aux, (k > 0) ? ", ..." : "...");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sprintf(aux, (k > 0) ? ", %s" : "%s", repr(restrictions[k].tok));
|
|
||||||
aux += len + 2;
|
|
||||||
}
|
|
||||||
sprintf(aux, "]");
|
|
||||||
return SCANNER_EXC_RESTRICTED;
|
|
||||||
}
|
|
||||||
sprintf(self->exc, "SyntaxError[@ char %d: Bad token found]", self->pos);
|
|
||||||
return SCANNER_EXC_BAD_TOKEN;
|
|
||||||
}
|
|
||||||
/* If we found something that isn't to be ignored, return it */
|
|
||||||
skip = 0;
|
|
||||||
for (k = 0; k < self->ignore_sz; k++) {
|
|
||||||
if (best_token.regex == self->ignore[k]) {
|
|
||||||
/* This token should be ignored... */
|
|
||||||
self->pos += best_token.string_sz;
|
|
||||||
skip = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!skip) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (best_token.regex) {
|
|
||||||
self->pos = (int)(best_token.string - self->input + best_token.string_sz);
|
|
||||||
/* Only add this token if it's not in the list (to prevent looping) */
|
|
||||||
p_token = &self->tokens[self->tokens_sz - 1];
|
|
||||||
if (self->tokens_sz == 0 ||
|
|
||||||
p_token->regex != best_token.regex ||
|
|
||||||
p_token->string != best_token.string ||
|
|
||||||
p_token->string_sz != best_token.string_sz
|
|
||||||
) {
|
|
||||||
if (self->tokens_sz >= self->tokens_bsz) {
|
|
||||||
/* Needs to expand block */
|
|
||||||
self->tokens_bsz = self->tokens_bsz + BLOCK_SIZE_PATTERNS;
|
|
||||||
PyMem_Resize(self->tokens, Token, self->tokens_bsz);
|
|
||||||
PyMem_Resize(self->restrictions, Restriction, self->tokens_bsz);
|
|
||||||
}
|
|
||||||
memcpy(&self->tokens[self->tokens_sz], &best_token, sizeof(Token));
|
|
||||||
p_restriction = &self->restrictions[self->tokens_sz];
|
|
||||||
if (restrictions_sz) {
|
|
||||||
p_restriction->patterns = PyMem_New(Pattern *, restrictions_sz);
|
|
||||||
p_restriction->patterns_sz = 0;
|
|
||||||
for (j = 0; j < restrictions_sz; j++) {
|
|
||||||
regex = Pattern_regex(restrictions[j].tok, restrictions[j].expr);
|
|
||||||
if (regex) {
|
|
||||||
p_restriction->patterns[p_restriction->patterns_sz++] = regex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p_restriction->patterns = NULL;
|
|
||||||
p_restriction->patterns_sz = 0;
|
|
||||||
}
|
|
||||||
self->tokens_sz++;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Scanner public interface */
|
|
||||||
|
|
||||||
void
|
|
||||||
Scanner_reset(Scanner *self, char *input, int input_sz) {
|
|
||||||
int i;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
for (i = 0; i < self->tokens_sz; i++) {
|
|
||||||
PyMem_Del(self->restrictions[i].patterns);
|
|
||||||
self->restrictions[i].patterns = NULL;
|
|
||||||
self->restrictions[i].patterns_sz = 0;
|
|
||||||
}
|
|
||||||
self->tokens_sz = 0;
|
|
||||||
|
|
||||||
if (self->input != NULL) {
|
|
||||||
PyMem_Del(self->input);
|
|
||||||
}
|
|
||||||
self->input = PyMem_Strndup(input, input_sz);
|
|
||||||
self->input_sz = input_sz;
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "Scanning in %s\n", repr(self->input));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
self->pos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scanner_del(Scanner *self) {
|
|
||||||
int i;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (self->ignore != NULL) {
|
|
||||||
PyMem_Del(self->ignore);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self->tokens != NULL) {
|
|
||||||
for (i = 0; i < self->tokens_sz; i++) {
|
|
||||||
PyMem_Del(self->restrictions[i].patterns);
|
|
||||||
}
|
|
||||||
PyMem_Del(self->tokens);
|
|
||||||
PyMem_Del(self->restrictions);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self->input != NULL) {
|
|
||||||
PyMem_Del(self->input);
|
|
||||||
}
|
|
||||||
|
|
||||||
PyMem_Del(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
Scanner*
|
|
||||||
Scanner_new(Pattern patterns[], int patterns_sz, Pattern ignore[], int ignore_sz, char *input, int input_sz)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
Scanner *self;
|
|
||||||
Pattern *regex;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
self = PyMem_New(Scanner, 1);
|
|
||||||
memset(self, 0, sizeof(Scanner));
|
|
||||||
if (self) {
|
|
||||||
for (i = 0; i < patterns_sz; i++) {
|
|
||||||
regex = Pattern_regex(patterns[i].tok, patterns[i].expr);
|
|
||||||
#ifdef DEBUG
|
|
||||||
if (regex) {
|
|
||||||
fprintf(stderr, "\tAdded regex pattern %s: %s\n", repr(regex->tok), repr(regex->expr));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
if (ignore_sz) {
|
|
||||||
self->ignore = PyMem_New(Pattern *, ignore_sz);
|
|
||||||
for (i = 0; i < ignore_sz; i++) {
|
|
||||||
regex = Pattern_regex(ignore[i].tok, ignore[i].expr);
|
|
||||||
if (regex) {
|
|
||||||
self->ignore[self->ignore_sz++] = regex;
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "\tIgnoring token %s\n", repr(regex->tok));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self->ignore = NULL;
|
|
||||||
}
|
|
||||||
Scanner_reset(self, input, input_sz);
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
Scanner_initialized(void)
|
|
||||||
{
|
|
||||||
return Pattern_patterns_initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scanner_initialize(Pattern patterns[], int patterns_sz)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Pattern_initialize(patterns, patterns_sz);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scanner_finalize(void)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
Pattern_finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
Token*
|
|
||||||
Scanner_token(Scanner *self, int i, Pattern restrictions[], int restrictions_sz)
|
|
||||||
{
|
|
||||||
int j, k, found;
|
|
||||||
Pattern *regex;
|
|
||||||
long result;
|
|
||||||
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (i == self->tokens_sz) {
|
|
||||||
result = _Scanner_scan(self, restrictions, restrictions_sz);
|
|
||||||
if (result < 0) {
|
|
||||||
return (Token *)result;
|
|
||||||
}
|
|
||||||
} else if (i >= 0 && i < self->tokens_sz) {
|
|
||||||
if (self->restrictions[i].patterns_sz) {
|
|
||||||
for (j = 0; j < restrictions_sz; j++) {
|
|
||||||
found = 0;
|
|
||||||
for (k = 0; k < self->restrictions[i].patterns_sz; k++) {
|
|
||||||
regex = Pattern_regex(restrictions[j].tok, restrictions[j].expr);
|
|
||||||
if (strcmp(restrictions[j].tok, self->restrictions[i].patterns[k]->tok) == 0) {
|
|
||||||
found = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
sprintf(self->exc, "Unimplemented: restriction set changed");
|
|
||||||
return (Token *)SCANNER_EXC_UNIMPLEMENTED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i >= 0 && i < self->tokens_sz) {
|
|
||||||
return &self->tokens[i];
|
|
||||||
}
|
|
||||||
return (Token *)SCANNER_EXC_NO_MORE_TOKENS;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
Scanner_rewind(Scanner *self, int i)
|
|
||||||
{
|
|
||||||
#ifdef DEBUG
|
|
||||||
fprintf(stderr, "%s\n", __PRETTY_FUNCTION__);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (i >= 0 && i < self->tokens_sz) {
|
|
||||||
self->tokens_sz = i;
|
|
||||||
self->pos = (int)(self->tokens[i].string - self->input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/*
|
|
||||||
* pyScss, a Scss compiler for Python
|
|
||||||
* SCSS blocks scanner.
|
|
||||||
*
|
|
||||||
* German M. Bravo (Kronuz) <german.mb@gmail.com>
|
|
||||||
* https://github.com/Kronuz/pyScss
|
|
||||||
*
|
|
||||||
* MIT license (http://www.opensource.org/licenses/mit-license.php)
|
|
||||||
* Copyright (c) 2011 German M. Bravo (Kronuz), All rights reserved.
|
|
||||||
*/
|
|
||||||
#ifndef SCANNER_H
|
|
||||||
#define SCANNER_H
|
|
||||||
|
|
||||||
#define PCRE_STATIC
|
|
||||||
#include <pcre.h>
|
|
||||||
|
|
||||||
#define BLOCK_SIZE_PATTERNS 50
|
|
||||||
#define BLOCK_SIZE_TOKENS 50
|
|
||||||
|
|
||||||
#define MAX_EXC_STRING 4096
|
|
||||||
|
|
||||||
#define SCANNER_EXC_BAD_TOKEN (long)-1
|
|
||||||
#define SCANNER_EXC_RESTRICTED (long)-2
|
|
||||||
#define SCANNER_EXC_UNIMPLEMENTED (long)-3
|
|
||||||
#define SCANNER_EXC_NO_MORE_TOKENS (long)-4
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char *tok;
|
|
||||||
char *expr;
|
|
||||||
pcre *pattern;
|
|
||||||
} Pattern;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
Pattern *regex;
|
|
||||||
char *string;
|
|
||||||
int string_sz;
|
|
||||||
} Token;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
int patterns_sz;
|
|
||||||
Pattern **patterns;
|
|
||||||
} Restriction;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char exc[MAX_EXC_STRING];
|
|
||||||
int ignore_sz;
|
|
||||||
Pattern **ignore;
|
|
||||||
int tokens_sz;
|
|
||||||
int tokens_bsz;
|
|
||||||
Token *tokens;
|
|
||||||
Restriction *restrictions;
|
|
||||||
int input_sz;
|
|
||||||
char *input;
|
|
||||||
int pos;
|
|
||||||
} Scanner;
|
|
||||||
|
|
||||||
int Scanner_initialized(void);
|
|
||||||
void Scanner_initialize(Pattern *, int);
|
|
||||||
void Scanner_finalize(void);
|
|
||||||
|
|
||||||
void Scanner_reset(Scanner *self, char *input, int input_sz);
|
|
||||||
Scanner *Scanner_new(Pattern *, int, Pattern *, int, char *, int);
|
|
||||||
void Scanner_del(Scanner *);
|
|
||||||
|
|
||||||
Token* Scanner_token(Scanner *, int, Pattern *, int);
|
|
||||||
void Scanner_rewind(Scanner *, int);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
#include <Python.h>
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
char *
|
|
||||||
PyMem_Strndup(const char *str, size_t len)
|
|
||||||
{
|
|
||||||
if (str != NULL) {
|
|
||||||
char *copy = PyMem_New(char, len + 1);
|
|
||||||
if (copy != NULL)
|
|
||||||
memcpy(copy, str, len);
|
|
||||||
copy[len] = '\0';
|
|
||||||
return copy;
|
|
||||||
}
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *
|
|
||||||
PyMem_Strdup(const char *str)
|
|
||||||
{
|
|
||||||
return PyMem_Strndup(str, strlen(str));
|
|
||||||
}
|
|
||||||
|
|
||||||
char *
|
|
||||||
reprn(char *str, size_t len) {
|
|
||||||
static char strings[10240];
|
|
||||||
static size_t current = 0;
|
|
||||||
size_t reqlen = 2;
|
|
||||||
char c,
|
|
||||||
*out,
|
|
||||||
*write,
|
|
||||||
*begin = str,
|
|
||||||
*end = str + len;
|
|
||||||
while (begin < end) {
|
|
||||||
c = *begin;
|
|
||||||
if (c == '\'') {
|
|
||||||
reqlen += 2;
|
|
||||||
} else if (c == '\r') {
|
|
||||||
reqlen += 2;
|
|
||||||
} else if (c == '\n') {
|
|
||||||
reqlen += 2;
|
|
||||||
} else if (c == '\t') {
|
|
||||||
reqlen += 2;
|
|
||||||
} else if (c < ' ') {
|
|
||||||
reqlen += 3;
|
|
||||||
} else {
|
|
||||||
reqlen++;
|
|
||||||
}
|
|
||||||
begin++;
|
|
||||||
}
|
|
||||||
if (reqlen > 10240) {
|
|
||||||
reqlen = 10240;
|
|
||||||
}
|
|
||||||
if (current + reqlen > 10240) {
|
|
||||||
current = 0;
|
|
||||||
}
|
|
||||||
begin = str;
|
|
||||||
end = str + len;
|
|
||||||
out = write = strings + current;
|
|
||||||
*write++ = '\'';
|
|
||||||
while (begin < end) {
|
|
||||||
c = *begin;
|
|
||||||
if (c == '\'') {
|
|
||||||
if (write + 5 >= strings + 10240) break;
|
|
||||||
sprintf(write, "\\'");
|
|
||||||
write += 2;
|
|
||||||
} else if (c == '\r') {
|
|
||||||
if (write + 5 >= strings + 10240) break;
|
|
||||||
sprintf(write, "\\r");
|
|
||||||
write += 2;
|
|
||||||
} else if (c == '\n') {
|
|
||||||
if (write + 5 >= strings + 10240) break;
|
|
||||||
sprintf(write, "\\n");
|
|
||||||
write += 2;
|
|
||||||
} else if (c == '\t') {
|
|
||||||
if (write + 5 >= strings + 10240) break;
|
|
||||||
sprintf(write, "\\t");
|
|
||||||
write += 2;
|
|
||||||
} else if (c < ' ') {
|
|
||||||
if (write + 6 >= strings + 10240) break;
|
|
||||||
sprintf(write, "\\x%02x", c);
|
|
||||||
write += 3;
|
|
||||||
} else {
|
|
||||||
if (write + 4 >= strings + 10240) break;
|
|
||||||
*write++ = c;
|
|
||||||
}
|
|
||||||
begin++;
|
|
||||||
}
|
|
||||||
*write++ = '\'';
|
|
||||||
*write++ = '\0';
|
|
||||||
current += (size_t)(write - out);
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *
|
|
||||||
repr(char *str) {
|
|
||||||
return reprn(str, strlen(str));
|
|
||||||
}
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
from scss import config
|
|
||||||
from scss.util import profiling
|
|
||||||
from scss import Scss, SourceFile, log
|
|
||||||
from scss import _prop_split_re
|
|
||||||
from scss.rule import SassRule
|
|
||||||
from scss.rule import UnparsedBlock
|
|
||||||
from scss.expression import Calculator
|
|
||||||
from scss.scss_meta import BUILD_INFO
|
|
||||||
from scss.errors import SassEvaluationError
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw_input
|
|
||||||
except NameError:
|
|
||||||
raw_input = input
|
|
||||||
|
|
||||||
log.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
|
||||||
|
|
||||||
from optparse import OptionGroup, OptionParser, SUPPRESS_HELP
|
|
||||||
|
|
||||||
parser = OptionParser(usage="Usage: %prog [options] [file]",
|
|
||||||
description="Converts Scss files to CSS.",
|
|
||||||
add_help_option=False)
|
|
||||||
parser.add_option("-i", "--interactive", action="store_true",
|
|
||||||
help="Run an interactive Scss shell")
|
|
||||||
parser.add_option("-w", "--watch", metavar="DIR",
|
|
||||||
help="Watch the files in DIR, and recompile when they change")
|
|
||||||
parser.add_option("-r", "--recursive", action="store_true", default=False,
|
|
||||||
help="Also watch directories inside of the watch directory")
|
|
||||||
parser.add_option("-o", "--output", metavar="PATH",
|
|
||||||
help="Write output to PATH (a directory if using watch, a file otherwise)")
|
|
||||||
parser.add_option("-s", "--suffix", metavar="STRING",
|
|
||||||
help="If using watch, a suffix added to the output filename (i.e. filename.STRING.css)")
|
|
||||||
parser.add_option("--time", action="store_true",
|
|
||||||
help="Display compliation times")
|
|
||||||
parser.add_option("--debug-info", action="store_true",
|
|
||||||
help="Turns on scss's debugging information")
|
|
||||||
parser.add_option("--no-debug-info", action="store_false",
|
|
||||||
dest="debug_info", default=False,
|
|
||||||
help="Turns off scss's debugging information")
|
|
||||||
parser.add_option("-T", "--test", action="store_true", help=SUPPRESS_HELP)
|
|
||||||
parser.add_option("-t", "--style", metavar="NAME",
|
|
||||||
dest="style", default='nested',
|
|
||||||
help="Output style. Can be nested (default), compact, compressed, or expanded.")
|
|
||||||
parser.add_option("-C", "--no-compress", action="store_false", dest="style", default=True,
|
|
||||||
help="Don't minify outputted CSS")
|
|
||||||
parser.add_option("-?", action="help", help=SUPPRESS_HELP)
|
|
||||||
parser.add_option("-h", "--help", action="help",
|
|
||||||
help="Show this message and exit")
|
|
||||||
parser.add_option("-v", "--version", action="store_true",
|
|
||||||
help="Print version and exit")
|
|
||||||
|
|
||||||
paths_group = OptionGroup(parser, "Resource Paths")
|
|
||||||
paths_group.add_option("-I", "--load-path", metavar="PATH",
|
|
||||||
action="append", dest="load_paths",
|
|
||||||
help="Add a scss import path, may be given multiple times")
|
|
||||||
paths_group.add_option("-S", "--static-root", metavar="PATH", dest="static_root",
|
|
||||||
help="Static root path (Where images and static resources are located)")
|
|
||||||
paths_group.add_option("-A", "--assets-root", metavar="PATH", dest="assets_root",
|
|
||||||
help="Assets root path (Sprite images will be created here)")
|
|
||||||
paths_group.add_option("-a", "--assets-url", metavar="URL", dest="assets_url",
|
|
||||||
help="URL to reach the files in your assets_root")
|
|
||||||
paths_group.add_option("-F", "--fonts-root", metavar="PATH", dest="fonts_root",
|
|
||||||
help="Fonts root path (Where fonts are located)")
|
|
||||||
paths_group.add_option("-f", "--fonts-url", metavar="PATH", dest="fonts_url",
|
|
||||||
help="URL to reach the fonts in your fonts_root")
|
|
||||||
paths_group.add_option("--images-root", metavar="PATH", dest="images_root",
|
|
||||||
help="Images root path (Where images are located)")
|
|
||||||
paths_group.add_option("--images-url", metavar="PATH", dest="images_url",
|
|
||||||
help="URL to reach the images in your images_root")
|
|
||||||
paths_group.add_option("--cache-root", metavar="PATH", dest="cache_root",
|
|
||||||
help="Cache root path (Cache files will be created here)")
|
|
||||||
parser.add_option_group(paths_group)
|
|
||||||
|
|
||||||
parser.add_option("--sass", action="store_true",
|
|
||||||
dest="is_sass", default=None,
|
|
||||||
help="Sass mode")
|
|
||||||
|
|
||||||
(options, args) = parser.parse_args()
|
|
||||||
|
|
||||||
# General runtime configuration
|
|
||||||
config.VERBOSITY = 0
|
|
||||||
if options.time:
|
|
||||||
config.VERBOSITY = 2
|
|
||||||
|
|
||||||
if options.static_root is not None:
|
|
||||||
config.STATIC_ROOT = options.static_root
|
|
||||||
if options.assets_root is not None:
|
|
||||||
config.ASSETS_ROOT = options.assets_root
|
|
||||||
|
|
||||||
if options.fonts_root is not None:
|
|
||||||
config.FONTS_ROOT = options.fonts_root
|
|
||||||
if options.fonts_url is not None:
|
|
||||||
config.FONTS_URL = options.fonts_url
|
|
||||||
|
|
||||||
if options.images_root is not None:
|
|
||||||
config.IMAGES_ROOT = options.images_root
|
|
||||||
if options.images_url is not None:
|
|
||||||
config.IMAGES_URL = options.images_url
|
|
||||||
|
|
||||||
if options.cache_root is not None:
|
|
||||||
config.CACHE_ROOT = options.cache_root
|
|
||||||
if options.load_paths is not None:
|
|
||||||
# TODO: Convert global LOAD_PATHS to a list. Use it directly.
|
|
||||||
# Doing the above will break backwards compatibility!
|
|
||||||
if hasattr(config.LOAD_PATHS, 'split'):
|
|
||||||
load_path_list = [p.strip() for p in config.LOAD_PATHS.split(',')]
|
|
||||||
else:
|
|
||||||
load_path_list = list(config.LOAD_PATHS)
|
|
||||||
|
|
||||||
for path_param in options.load_paths:
|
|
||||||
for p in path_param.replace(os.pathsep, ',').replace(';', ',').split(','):
|
|
||||||
p = p.strip()
|
|
||||||
if p and p not in load_path_list:
|
|
||||||
load_path_list.append(p)
|
|
||||||
|
|
||||||
# TODO: Remove this once global LOAD_PATHS is a list.
|
|
||||||
if hasattr(config.LOAD_PATHS, 'split'):
|
|
||||||
config.LOAD_PATHS = ','.join(load_path_list)
|
|
||||||
else:
|
|
||||||
config.LOAD_PATHS = load_path_list
|
|
||||||
if options.assets_url is not None:
|
|
||||||
config.ASSETS_URL = options.assets_url
|
|
||||||
|
|
||||||
# Execution modes
|
|
||||||
if options.test:
|
|
||||||
run_tests()
|
|
||||||
elif options.version:
|
|
||||||
print_version()
|
|
||||||
elif options.interactive:
|
|
||||||
run_repl(options)
|
|
||||||
elif options.watch:
|
|
||||||
watch_sources(options)
|
|
||||||
else:
|
|
||||||
do_build(options, args)
|
|
||||||
|
|
||||||
|
|
||||||
def print_version():
|
|
||||||
print(BUILD_INFO)
|
|
||||||
|
|
||||||
|
|
||||||
def run_tests():
|
|
||||||
try:
|
|
||||||
import pytest
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError("You need py.test installed to run the test suite.")
|
|
||||||
pytest.main("") # don't let py.test re-consume our arguments
|
|
||||||
|
|
||||||
|
|
||||||
def do_build(options, args):
|
|
||||||
if options.output is not None:
|
|
||||||
output = open(options.output, 'wt')
|
|
||||||
else:
|
|
||||||
output = sys.stdout
|
|
||||||
|
|
||||||
css = Scss(scss_opts={
|
|
||||||
'style': options.style,
|
|
||||||
'debug_info': options.debug_info,
|
|
||||||
})
|
|
||||||
if args:
|
|
||||||
for path in args:
|
|
||||||
output.write(css.compile(scss_file=path, is_sass=options.is_sass))
|
|
||||||
else:
|
|
||||||
output.write(css.compile(sys.stdin.read(), is_sass=options.is_sass))
|
|
||||||
|
|
||||||
for f, t in profiling.items():
|
|
||||||
sys.stderr.write("%s took %03fs" % (f, t))
|
|
||||||
|
|
||||||
|
|
||||||
def watch_sources(options):
|
|
||||||
import time
|
|
||||||
try:
|
|
||||||
from watchdog.observers import Observer
|
|
||||||
from watchdog.events import PatternMatchingEventHandler
|
|
||||||
except ImportError:
|
|
||||||
sys.stderr.write("Using watch functionality requires the `watchdog` library: http://pypi.python.org/pypi/watchdog/")
|
|
||||||
sys.exit(1)
|
|
||||||
if options.output and not os.path.isdir(options.output):
|
|
||||||
sys.stderr.write("watch file output directory is invalid: '%s'" % (options.output))
|
|
||||||
sys.exit(2)
|
|
||||||
|
|
||||||
class ScssEventHandler(PatternMatchingEventHandler):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ScssEventHandler, self).__init__(*args, **kwargs)
|
|
||||||
self.css = Scss(scss_opts={
|
|
||||||
'style': options.style,
|
|
||||||
'debug_info': options.debug_info,
|
|
||||||
})
|
|
||||||
self.output = options.output
|
|
||||||
self.suffix = options.suffix
|
|
||||||
|
|
||||||
def is_valid(self, path):
|
|
||||||
return os.path.isfile(path) and (path.endswith('.scss') or path.endswith('.sass')) and not os.path.basename(path).startswith('_')
|
|
||||||
|
|
||||||
def process(self, path):
|
|
||||||
if os.path.isdir(path):
|
|
||||||
for f in os.listdir(path):
|
|
||||||
full = os.path.join(path, f)
|
|
||||||
if self.is_valid(full):
|
|
||||||
self.compile(full)
|
|
||||||
elif self.is_valid(path):
|
|
||||||
self.compile(path)
|
|
||||||
|
|
||||||
def compile(self, src_path):
|
|
||||||
fname = os.path.basename(src_path)
|
|
||||||
if fname.endswith('.scss') or fname.endswith('.sass'):
|
|
||||||
fname = fname[:-5]
|
|
||||||
if self.suffix:
|
|
||||||
fname += '.' + self.suffix
|
|
||||||
fname += '.css'
|
|
||||||
else:
|
|
||||||
# you didn't give me a file of the correct type!
|
|
||||||
return False
|
|
||||||
|
|
||||||
if self.output:
|
|
||||||
dest_path = os.path.join(self.output, fname)
|
|
||||||
else:
|
|
||||||
dest_path = os.path.join(os.path.dirname(src_path), fname)
|
|
||||||
|
|
||||||
print("Compiling %s => %s" % (src_path, dest_path))
|
|
||||||
dest_file = open(dest_path, 'w')
|
|
||||||
dest_file.write(self.css.compile(scss_file=src_path))
|
|
||||||
|
|
||||||
def on_moved(self, event):
|
|
||||||
super(ScssEventHandler, self).on_moved(event)
|
|
||||||
self.process(event.dest_path)
|
|
||||||
|
|
||||||
def on_created(self, event):
|
|
||||||
super(ScssEventHandler, self).on_created(event)
|
|
||||||
self.process(event.src_path)
|
|
||||||
|
|
||||||
def on_modified(self, event):
|
|
||||||
super(ScssEventHandler, self).on_modified(event)
|
|
||||||
self.process(event.src_path)
|
|
||||||
|
|
||||||
event_handler = ScssEventHandler(patterns=['*.scss', '*.sass'])
|
|
||||||
observer = Observer()
|
|
||||||
observer.schedule(event_handler, path=options.watch, recursive=options.recursive)
|
|
||||||
observer.start()
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
time.sleep(1)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
observer.stop()
|
|
||||||
observer.join()
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def readline_history(fn):
|
|
||||||
try:
|
|
||||||
import readline
|
|
||||||
except ImportError:
|
|
||||||
yield
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
readline.read_history_file(fn)
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
readline.write_history_file(fn)
|
|
||||||
except IOError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def run_repl(is_sass=False):
|
|
||||||
repl = SassRepl()
|
|
||||||
|
|
||||||
with readline_history(os.path.expanduser('~/.scss-history')):
|
|
||||||
print("Welcome to %s interactive shell" % (BUILD_INFO,))
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
in_ = raw_input('>>> ').strip()
|
|
||||||
for output in repl(in_):
|
|
||||||
print(output)
|
|
||||||
except (EOFError, KeyboardInterrupt):
|
|
||||||
print("Bye!")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
class SassRepl(object):
|
|
||||||
def __init__(self, is_sass=False):
|
|
||||||
self.css = Scss()
|
|
||||||
self.namespace = self.css.root_namespace
|
|
||||||
self.options = self.css.scss_opts
|
|
||||||
self.source_file = SourceFile.from_string('', '<shell>', line_numbers=False, is_sass=is_sass)
|
|
||||||
self.calculator = Calculator(self.namespace)
|
|
||||||
|
|
||||||
def __call__(self, s):
|
|
||||||
from pprint import pformat
|
|
||||||
|
|
||||||
if s in ('exit', 'quit'):
|
|
||||||
raise KeyboardInterrupt
|
|
||||||
|
|
||||||
for s in s.split(';'):
|
|
||||||
s = self.source_file.prepare_source(s.strip())
|
|
||||||
if not s:
|
|
||||||
continue
|
|
||||||
elif s.startswith('@'):
|
|
||||||
scope = None
|
|
||||||
properties = []
|
|
||||||
children = deque()
|
|
||||||
rule = SassRule(self.source_file, namespace=self.namespace, options=self.options, properties=properties)
|
|
||||||
block = UnparsedBlock(rule, 1, s, None)
|
|
||||||
code, name = (s.split(None, 1) + [''])[:2]
|
|
||||||
if code == '@option':
|
|
||||||
self.css._settle_options(rule, children, scope, block)
|
|
||||||
continue
|
|
||||||
elif code == '@import':
|
|
||||||
self.css._do_import(rule, children, scope, block)
|
|
||||||
continue
|
|
||||||
elif code == '@include':
|
|
||||||
final_cont = ''
|
|
||||||
self.css._do_include(rule, children, scope, block)
|
|
||||||
code = self.css._print_properties(properties).rstrip('\n')
|
|
||||||
if code:
|
|
||||||
final_cont += code
|
|
||||||
if children:
|
|
||||||
self.css.children.extendleft(children)
|
|
||||||
self.css.parse_children()
|
|
||||||
code = self.css._create_css(self.css.rules).rstrip('\n')
|
|
||||||
if code:
|
|
||||||
final_cont += code
|
|
||||||
yield final_cont
|
|
||||||
continue
|
|
||||||
elif s == 'ls' or s.startswith('show(') or s.startswith('show ') or s.startswith('ls(') or s.startswith('ls '):
|
|
||||||
m = re.match(r'(?:show|ls)(\()?\s*([^,/\\) ]*)(?:[,/\\ ]([^,/\\ )]+))*(?(1)\))', s, re.IGNORECASE)
|
|
||||||
if m:
|
|
||||||
name = m.group(2)
|
|
||||||
code = m.group(3)
|
|
||||||
name = name and name.strip().rstrip('s') # remove last 's' as in functions
|
|
||||||
code = code and code.strip()
|
|
||||||
ns = self.namespace
|
|
||||||
if not name:
|
|
||||||
yield pformat(sorted(['vars', 'options', 'mixins', 'functions']))
|
|
||||||
elif name in ('v', 'var', 'variable'):
|
|
||||||
variables = dict(ns._variables)
|
|
||||||
if code == '*':
|
|
||||||
pass
|
|
||||||
elif code:
|
|
||||||
variables = dict((k, v) for k, v in variables.items() if code in k)
|
|
||||||
else:
|
|
||||||
variables = dict((k, v) for k, v in variables.items() if not k.startswith('$--'))
|
|
||||||
yield pformat(variables)
|
|
||||||
|
|
||||||
elif name in ('o', 'opt', 'option'):
|
|
||||||
opts = self.options
|
|
||||||
if code == '*':
|
|
||||||
pass
|
|
||||||
elif code:
|
|
||||||
opts = dict((k, v) for k, v in opts.items() if code in k)
|
|
||||||
else:
|
|
||||||
opts = dict((k, v) for k, v in opts.items() if not k.startswith('@'))
|
|
||||||
yield pformat(opts)
|
|
||||||
|
|
||||||
elif name in ('m', 'mix', 'mixin', 'f', 'func', 'funct', 'function'):
|
|
||||||
if name.startswith('m'):
|
|
||||||
funcs = dict(ns._mixins)
|
|
||||||
elif name.startswith('f'):
|
|
||||||
funcs = dict(ns._functions)
|
|
||||||
if code == '*':
|
|
||||||
pass
|
|
||||||
elif code:
|
|
||||||
funcs = dict((k, v) for k, v in funcs.items() if code in k[0])
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
# TODO print source when possible
|
|
||||||
yield pformat(funcs)
|
|
||||||
continue
|
|
||||||
elif s.startswith('$') and (':' in s or '=' in s):
|
|
||||||
prop, value = [a.strip() for a in _prop_split_re.split(s, 1)]
|
|
||||||
prop = self.calculator.do_glob_math(prop)
|
|
||||||
value = self.calculator.calculate(value)
|
|
||||||
self.namespace.set_variable(prop, value)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# TODO respect compress?
|
|
||||||
try:
|
|
||||||
yield(self.calculator.calculate(s).render())
|
|
||||||
except (SyntaxError, SassEvaluationError) as e:
|
|
||||||
print("%s" % e, file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
-1128
File diff suppressed because it is too large.
Load diff
@@ -1,146 +0,0 @@
|
|||||||
from __future__ import absolute_import
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from scss import config
|
|
||||||
|
|
||||||
|
|
||||||
def split_params(params):
|
|
||||||
params = params.split(',') or []
|
|
||||||
if params:
|
|
||||||
final_params = []
|
|
||||||
param = params.pop(0)
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
while param.count('(') != param.count(')'):
|
|
||||||
try:
|
|
||||||
param = param + ',' + params.pop(0)
|
|
||||||
except IndexError:
|
|
||||||
break
|
|
||||||
final_params.append(param)
|
|
||||||
param = params.pop(0)
|
|
||||||
except IndexError:
|
|
||||||
pass
|
|
||||||
params = final_params
|
|
||||||
return params
|
|
||||||
|
|
||||||
|
|
||||||
def dequote(s):
|
|
||||||
if s and s[0] in ('"', "'") and s[-1] == s[0]:
|
|
||||||
s = s[1:-1]
|
|
||||||
s = unescape(s)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def depar(s):
|
|
||||||
while s and s[0] == '(' and s[-1] == ')':
|
|
||||||
s = s[1:-1]
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def to_str(num):
|
|
||||||
try:
|
|
||||||
render = num.render
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return render()
|
|
||||||
|
|
||||||
if isinstance(num, dict):
|
|
||||||
s = sorted(num.items())
|
|
||||||
sp = num.get('_', '')
|
|
||||||
return (sp + ' ').join(to_str(v) for n, v in s if n != '_')
|
|
||||||
elif isinstance(num, float):
|
|
||||||
num = ('%0.05f' % round(num, 5)).rstrip('0').rstrip('.')
|
|
||||||
return num
|
|
||||||
elif isinstance(num, bool):
|
|
||||||
return 'true' if num else 'false'
|
|
||||||
elif num is None:
|
|
||||||
return ''
|
|
||||||
return str(num)
|
|
||||||
|
|
||||||
|
|
||||||
def to_float(num):
|
|
||||||
if isinstance(num, (float, int)):
|
|
||||||
return float(num)
|
|
||||||
num = to_str(num)
|
|
||||||
if num and num[-1] == '%':
|
|
||||||
return float(num[:-1]) / 100.0
|
|
||||||
else:
|
|
||||||
return float(num)
|
|
||||||
|
|
||||||
|
|
||||||
def escape(s):
|
|
||||||
return re.sub(r'''(["'])''', r'\\\1', s)
|
|
||||||
|
|
||||||
|
|
||||||
def unescape(s):
|
|
||||||
return re.sub(r'''\\(['"])''', r'\1', s)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_var(var):
|
|
||||||
"""Sass defines `foo_bar` and `foo-bar` as being identical, both in
|
|
||||||
variable names and functions/mixins. This normalizes everything to use
|
|
||||||
dashes.
|
|
||||||
"""
|
|
||||||
return var.replace('_', '-')
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Function timing decorator
|
|
||||||
profiling = {}
|
|
||||||
|
|
||||||
|
|
||||||
def print_timing(level=0):
|
|
||||||
def _print_timing(func):
|
|
||||||
if config.VERBOSITY:
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
if config.VERBOSITY >= level:
|
|
||||||
t1 = time.time()
|
|
||||||
res = func(*args, **kwargs)
|
|
||||||
t2 = time.time()
|
|
||||||
profiling.setdefault(func.func_name, 0)
|
|
||||||
profiling[func.func_name] += (t2 - t1)
|
|
||||||
return res
|
|
||||||
else:
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
else:
|
|
||||||
return func
|
|
||||||
return _print_timing
|
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
|
||||||
# Profiler decorator
|
|
||||||
def profile(fn):
|
|
||||||
import cProfile
|
|
||||||
import pstats
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
profiler = cProfile.Profile()
|
|
||||||
stream = six.StringIO()
|
|
||||||
profiler.enable()
|
|
||||||
try:
|
|
||||||
res = fn(*args, **kwargs)
|
|
||||||
finally:
|
|
||||||
profiler.disable()
|
|
||||||
stats = pstats.Stats(profiler, stream=stream)
|
|
||||||
stats.sort_stats('time')
|
|
||||||
print >>stream, ""
|
|
||||||
print >>stream, "=" * 100
|
|
||||||
print >>stream, "Stats:"
|
|
||||||
stats.print_stats()
|
|
||||||
print >>stream, "=" * 100
|
|
||||||
print >>stream, "Callers:"
|
|
||||||
stats.print_callers()
|
|
||||||
print >>stream, "=" * 100
|
|
||||||
print >>stream, "Callees:"
|
|
||||||
stats.print_callees()
|
|
||||||
print >>sys.stderr, stream.getvalue()
|
|
||||||
stream.close()
|
|
||||||
return res
|
|
||||||
return wrapper
|
|
||||||
Reference in new issue
Block a user