Livereload css

This commit is contained in:
Ruud
2014-04-09 16:07:59 +02:00
parent 685210aee3
commit e8fe9da602
37 changed files with 13370 additions and 57 deletions
+72 -57
View File
@@ -1,15 +1,14 @@
import os
import re
import traceback
import time
from couchpotato.core.event import addEvent
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
from scss import Scss
from tornado.web import StaticFileHandler
@@ -22,7 +21,7 @@ class ClientScript(Plugin):
core_static = {
'style': [
'style/main.css',
'style/main.scss',
'style/uniform.generic.css',
'style/uniform.css',
'style/settings.css',
@@ -54,8 +53,8 @@ class ClientScript(Plugin):
],
}
urls = {'style': {}, 'script': {}}
minified = {'style': {}, 'script': {}}
watcher = None
paths = {'style': {}, 'script': {}}
comment = {
'style': '/*** %s:%d ***/\n',
@@ -74,11 +73,26 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
if not Env.get('dev'):
addEvent('app.load', self.minify)
addEvent('app.load', self.livereload, priority = 1)
addEvent('app.load', self.compile)
self.addCore()
def livereload(self):
if Env.get('dev'):
from livereload import Server
from livereload.watcher import Watcher
self.livereload_server = Server()
self.livereload_server.watch('%s/minified/*.css' % Env.get('cache_dir'))
self.livereload_server.watch('%s/*.css' % os.path.join(Env.get('app_dir'), 'couchpotato', 'static', 'style'))
self.watcher = Watcher()
fireEvent('schedule.interval', 'livereload.watcher', self.watcher.examine, seconds = .5)
self.livereload_server.serve(port = 35729)
def addCore(self):
for static_type in self.core_static:
@@ -91,7 +105,7 @@ class ClientScript(Plugin):
else:
self.registerStyle(core_url, file_path, position = 'front')
def minify(self):
def compile(self):
# Create cache dir
cache = Env.get('cache_dir')
@@ -105,44 +119,62 @@ class ClientScript(Plugin):
positions = self.paths.get(file_type, {})
for position in positions:
files = positions.get(position)
self._minify(file_type, files, position, position + '.' + ext)
self._compile(file_type, files, position, position + '.' + ext)
def _minify(self, file_type, files, position, out):
def _compile(self, file_type, paths, position, out):
cache = Env.get('cache_dir')
out_name = out
out = os.path.join(cache, 'minified', out_name)
minified_dir = os.path.join(cache, 'minified')
data_combined = ''
raw = []
for file_path in files:
for file_path in paths:
f = open(file_path, 'r').read()
if file_type == 'script':
data = jsmin(f)
else:
data = self.prefix(f)
data = cssmin(data)
data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
# Compile scss
if file_path[-5:] == '.scss':
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
# 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'):
self.watcher.watch(file_path, self.compile)
url_path = paths[file_path].get('original_url')
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
paths[file_path]['url'] = 'minified/%s?%s' % (compiled_file_name, tryInt(time.time()))
if not Env.get('dev'):
if file_type == 'script':
data = f
else:
data = self.prefix(f)
data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
data_combined += self.comment.get(file_type) % (ss(file_path), int(os.path.getmtime(file_path)))
data_combined += data + '\n\n'
del paths[file_path]
# Combine all files together with some comments
data = ''
for r in raw:
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
data += r.get('data') + '\n\n'
if not Env.get('dev'):
self.createFile(out, data.strip())
self.createFile(os.path.join(minified_dir, out_name), data_combined.strip())
if not self.minified.get(file_type):
self.minified[file_type] = {}
if not self.minified[file_type].get(position):
self.minified[file_type][position] = []
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url)
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
self.minified[file_type][position].append(minified_url)
def getStyles(self, *args, **kwargs):
return self.get('style', *args, **kwargs)
@@ -150,22 +182,9 @@ class ClientScript(Plugin):
def getScripts(self, *args, **kwargs):
return self.get('script', *args, **kwargs)
def get(self, type, as_html = False, location = 'head'):
data = '' if as_html else []
try:
try:
if not Env.get('dev'):
return self.minified[type][location]
except:
pass
return self.urls[type][location]
except:
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
return data
def get(self, type, location = 'head'):
paths = self.paths[type][location]
return [paths[x].get('url', paths[x].get('original_url')) for x in paths]
def registerStyle(self, api_path, file_path, position = 'head'):
self.register(api_path, file_path, 'style', position)
@@ -177,13 +196,9 @@ class ClientScript(Plugin):
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
if not self.urls[type].get(location):
self.urls[type][location] = []
self.urls[type][location].append(api_path)
if not self.paths[type].get(location):
self.paths[type][location] = []
self.paths[type][location].append(file_path)
self.paths[type][location] = {}
self.paths[type][location][file_path] = {'original_url': api_path}
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
prefix_tags = ['ms', 'moz', 'webkit']
+16
View File
@@ -0,0 +1,16 @@
"""
livereload
~~~~~~~~~~
A python version of livereload.
:copyright: (c) 2013 by Hsiaoming Yang
"""
__version__ = '2.2.0'
__author__ = 'Hsiaoming Yang <me@lepture.com>'
__homepage__ = 'https://github.com/lepture/python-livereload'
from .server import Server, shell
__all__ = ('Server', 'shell')
+43
View File
@@ -0,0 +1,43 @@
# coding: utf-8
"""
livereload._compat
~~~~~~~~~~~~~~~~~~
Compatible module for python2 and python3.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import sys
PY3 = sys.version_info[0] == 3
if PY3:
unicode_type = str
bytes_type = bytes
text_types = (str,)
else:
unicode_type = unicode
bytes_type = str
text_types = (str, unicode)
def to_unicode(value, encoding='utf-8'):
"""Convert different types of objects to unicode."""
if isinstance(value, unicode_type):
return value
if isinstance(value, bytes_type):
return unicode_type(value, encoding=encoding)
if isinstance(value, int):
return unicode_type(str(value))
return value
def to_bytes(value, encoding='utf-8'):
"""Convert different types of objects to bytes."""
if isinstance(value, bytes_type):
return value
return value.encode(encoding)
+209
View File
@@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
"""
livereload.handlers
~~~~~~~~~~~~~~~~~~~
HTTP and WebSocket handlers for livereload.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import os
import time
import hashlib
import logging
import mimetypes
from tornado import ioloop
from tornado import escape
from tornado.websocket import WebSocketHandler
from tornado.web import RequestHandler
from tornado.util import ObjectDict
from ._compat import to_bytes
class LiveReloadHandler(WebSocketHandler):
waiters = set()
watcher = None
_last_reload_time = None
def allow_draft76(self):
return True
def on_close(self):
if self in LiveReloadHandler.waiters:
LiveReloadHandler.waiters.remove(self)
def send_message(self, message):
if isinstance(message, dict):
message = escape.json_encode(message)
try:
self.write_message(message)
except:
logging.error('Error sending message', exc_info=True)
def poll_tasks(self):
filepath = self.watcher.examine()
if not filepath:
return
logging.info('File %s changed', filepath)
self.watch_tasks()
def watch_tasks(self):
if time.time() - self._last_reload_time < 3:
# if you changed lot of files in one time
# it will refresh too many times
logging.info('ignore this reload action')
return
logging.info('Reload %s waiters', len(self.waiters))
msg = {
'command': 'reload',
'path': self.watcher.filepath or '*',
'liveCSS': True
}
self._last_reload_time = time.time()
for waiter in LiveReloadHandler.waiters:
try:
waiter.write_message(msg)
except:
logging.error('Error sending message', exc_info=True)
LiveReloadHandler.waiters.remove(waiter)
def on_message(self, message):
"""Handshake with livereload.js
1. client send 'hello'
2. server reply 'hello'
3. client send 'info'
http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
"""
message = ObjectDict(escape.json_decode(message))
if message.command == 'hello':
handshake = {}
handshake['command'] = 'hello'
handshake['protocols'] = [
'http://livereload.com/protocols/official-7',
'http://livereload.com/protocols/official-8',
'http://livereload.com/protocols/official-9',
'http://livereload.com/protocols/2.x-origin-version-negotiation',
'http://livereload.com/protocols/2.x-remote-control'
]
handshake['serverName'] = 'livereload-tornado'
self.send_message(handshake)
if message.command == 'info' and 'url' in message:
logging.info('Browser Connected: %s' % message.url)
LiveReloadHandler.waiters.add(self)
if not LiveReloadHandler._last_reload_time:
if not self.watcher._tasks:
logging.info('Watch current working directory')
self.watcher.watch(os.getcwd())
LiveReloadHandler._last_reload_time = time.time()
logging.info('Start watching changes')
if not self.watcher.start(self.poll_tasks):
ioloop.PeriodicCallback(self.poll_tasks, 800).start()
class LiveReloadJSHandler(RequestHandler):
def initialize(self, port):
self._port = port
def get(self):
js = os.path.join(
os.path.abspath(os.path.dirname(__file__)), 'livereload.js',
)
self.set_header('Content-Type', 'application/javascript')
with open(js, 'r') as f:
content = f.read()
content = content.replace('{{port}}', str(self._port))
self.write(content)
class ForceReloadHandler(RequestHandler):
def get(self):
msg = {
'command': 'reload',
'path': self.get_argument('path', default=None) or '*',
'liveCSS': True,
'liveImg': True
}
for waiter in LiveReloadHandler.waiters:
try:
waiter.write_message(msg)
except:
logging.error('Error sending message', exc_info=True)
LiveReloadHandler.waiters.remove(waiter)
self.write('ok')
class StaticHandler(RequestHandler):
def initialize(self, root, fallback=None):
self._root = os.path.abspath(root)
self._fallback = fallback
def filepath(self, url):
url = url.lstrip('/')
url = os.path.join(self._root, url)
if url.endswith('/'):
url += 'index.html'
elif not os.path.exists(url) and not url.endswith('.html'):
url += '.html'
if not os.path.isfile(url):
return None
return url
def get(self, path='/'):
filepath = self.filepath(path)
if not filepath and path.endswith('/'):
rootdir = os.path.join(self._root, path.lstrip('/'))
return self.create_index(rootdir)
if not filepath:
if self._fallback:
self._fallback(self.request)
self._finished = True
return
return self.send_error(404)
mime_type, encoding = mimetypes.guess_type(filepath)
if not mime_type:
mime_type = 'text/html'
self.mime_type = mime_type
self.set_header('Content-Type', mime_type)
with open(filepath, 'r') as f:
data = f.read()
hasher = hashlib.sha1()
hasher.update(to_bytes(data))
self.set_header('Etag', '"%s"' % hasher.hexdigest())
ua = self.request.headers.get('User-Agent', 'bot').lower()
if mime_type == 'text/html' and 'msie' not in ua:
data = data.replace(
'</head>',
'<script src="/livereload.js"></script></head>'
)
self.write(data)
def create_index(self, root):
files = os.listdir(root)
self.write('<ul>')
for f in files:
path = os.path.join(root, f)
self.write('<li>')
if os.path.isdir(path):
self.write('<a href="%s/">%s</a>' % (f, f))
else:
self.write('<a href="%s">%s</a>' % (f, f))
self.write('</li>')
self.write('</ul>')
File diff suppressed because it is too large Load Diff
+224
View File
@@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
"""
livereload.server
~~~~~~~~~~~~~~~~~
WSGI app server for livereload.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import os
import logging
from subprocess import Popen, PIPE
import time
import threading
import webbrowser
from tornado import escape
from tornado.wsgi import WSGIContainer
from tornado.ioloop import IOLoop
from tornado.web import Application, FallbackHandler
from .handlers import LiveReloadHandler, LiveReloadJSHandler
from .handlers import ForceReloadHandler, StaticHandler
from .watcher import Watcher
from ._compat import text_types
from tornado.log import enable_pretty_logging
enable_pretty_logging()
def shell(command, output=None, mode='w'):
"""Command shell command.
You can add a shell command::
server.watch(
'style.less', shell('lessc style.less', output='style.css')
)
:param command: a shell command, string or list
:param output: output stdout to the given file
:param mode: only works with output, mode ``w`` means write,
mode ``a`` means append
"""
if not output:
output = os.devnull
else:
folder = os.path.dirname(output)
if folder and not os.path.isdir(folder):
os.makedirs(folder)
if isinstance(command, (list, tuple)):
cmd = command
else:
cmd = command.split()
def run_shell():
try:
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
except OSError as e:
logging.error(e)
if e.errno == os.errno.ENOENT: # file (command) not found
logging.error("maybe you haven't installed %s", cmd[0])
return e
stdout, stderr = p.communicate()
if stderr:
logging.error(stderr)
return stderr
#: stdout is bytes, decode for python3
code = stdout.decode()
with open(output, mode) as f:
f.write(code)
return run_shell
class WSGIWrapper(WSGIContainer):
"""Insert livereload scripts into response body."""
def __call__(self, request):
data = {}
response = []
def start_response(status, response_headers, exc_info=None):
data["status"] = status
data["headers"] = response_headers
return response.append
app_response = self.wsgi_application(
WSGIContainer.environ(request), start_response)
try:
response.extend(app_response)
body = b"".join(response)
finally:
if hasattr(app_response, "close"):
app_response.close()
if not data:
raise Exception("WSGI app did not call start_response")
status_code = int(data["status"].split()[0])
headers = data["headers"]
header_set = set(k.lower() for (k, v) in headers)
body = escape.utf8(body)
body = body.replace(
b'</head>',
b'<script src="/livereload.js"></script></head>'
)
if status_code != 304:
if "content-length" not in header_set:
headers.append(("Content-Length", str(len(body))))
if "content-type" not in header_set:
headers.append(("Content-Type", "text/html; charset=UTF-8"))
if "server" not in header_set:
headers.append(("Server", "livereload-tornado"))
parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
for key, value in headers:
if key.lower() == 'content-length':
value = str(len(body))
parts.append(
escape.utf8(key) + b": " + escape.utf8(value) + b"\r\n"
)
parts.append(b"\r\n")
parts.append(body)
request.write(b"".join(parts))
request.finish()
self._log(status_code, request)
class Server(object):
"""Livereload server interface.
Initialize a server and watch file changes::
server = Server(wsgi_app)
server.serve()
:param app: a wsgi application instance
:param watcher: A Watcher instance, you don't have to initialize
it by yourself. Under Linux, you will want to install
pyinotify and use INotifyWatcher() to avoid wasted
CPU usage.
"""
def __init__(self, app=None, watcher=None):
self.app = app
self.port = 5500
self.root = None
if not watcher:
watcher = Watcher()
self.watcher = watcher
def watch(self, filepath, func=None):
"""Add the given filepath for watcher list.
Once you have intialized a server, watch file changes before
serve the server::
server.watch('static/*.stylus', 'make static')
def alert():
print('foo')
server.watch('foo.txt', alert)
server.serve()
:param filepath: files to be watched, it can be a filepath,
a directory, or a glob pattern
:param func: the function to be called, it can be a string of
shell command, or any callable object without
parameters
"""
if isinstance(func, text_types):
func = shell(func)
self.watcher.watch(filepath, func)
def application(self, debug=True):
LiveReloadHandler.watcher = self.watcher
handlers = [
(r'/livereload', LiveReloadHandler),
(r'/forcereload', ForceReloadHandler),
(r'/livereload.js', LiveReloadJSHandler, dict(port=self.port)),
]
if self.app:
self.app = WSGIWrapper(self.app)
handlers.append(
(r'.*', FallbackHandler, dict(fallback=self.app))
)
else:
handlers.append(
(r'(.*)', StaticHandler, dict(root=self.root or '.')),
)
return Application(handlers=handlers, debug=debug)
def serve(self, port=None, host=None, root=None, debug=True, open_url=False):
"""Start serve the server with the given port.
:param port: serve on this port, default is 5500
:param host: serve on this hostname, default is 0.0.0.0
:param root: serve static on this root directory
:param open_url: open system browser
"""
if root:
self.root = root
if port:
self.port = port
if host is None:
host = ''
self.application(debug=debug).listen(self.port, address=host)
logging.getLogger().setLevel(logging.INFO)
host = host or '127.0.0.1'
print('Serving on %s:%s' % (host, self.port))
# Async open web browser after 5 sec timeout
if open_url:
def opener():
time.sleep(5)
webbrowser.open('http://%s:%s' % (host, self.port))
threading.Thread(target=opener).start()
try:
IOLoop.instance().start()
except KeyboardInterrupt:
print('Shutting down...')
+132
View File
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""
livereload.watcher
~~~~~~~~~~~~~~~~~~
A file watch management for LiveReload Server.
:copyright: (c) 2013 by Hsiaoming Yang
"""
import os
import glob
import time
class Watcher(object):
"""A file watcher registery."""
def __init__(self):
self._tasks = {}
self._mtimes = {}
# filepath that is changed
self.filepath = None
self._start = time.time()
def ignore(self, filename):
"""Ignore a given filename or not."""
_, ext = os.path.splitext(filename)
return ext in ['.pyc', '.pyo', '.o', '.swp']
def watch(self, path, func=None):
"""Add a task to watcher."""
self._tasks[path] = func
def start(self, callback):
"""Start the watcher running, calling callback when changes are observed. If this returns False,
regular polling will be used."""
return False
def examine(self):
"""Check if there are changes, if true, run the given task."""
# clean filepath
self.filepath = None
for path in self._tasks:
if self.is_changed(path):
func = self._tasks[path]
# run function
func and func()
return self.filepath
def is_changed(self, path):
if os.path.isfile(path):
return self.is_file_changed(path)
elif os.path.isdir(path):
return self.is_folder_changed(path)
return self.is_glob_changed(path)
def is_file_changed(self, path):
if not os.path.isfile(path):
return False
if self.ignore(path):
return False
mtime = os.path.getmtime(path)
if path not in self._mtimes:
self._mtimes[path] = mtime
self.filepath = path
return mtime > self._start
if self._mtimes[path] != mtime:
self._mtimes[path] = mtime
self.filepath = path
return True
self._mtimes[path] = mtime
return False
def is_folder_changed(self, path):
for root, dirs, files in os.walk(path, followlinks=True):
if '.git' in dirs:
dirs.remove('.git')
if '.hg' in dirs:
dirs.remove('.hg')
if '.svn' in dirs:
dirs.remove('.svn')
if '.cvs' in dirs:
dirs.remove('.cvs')
for f in files:
if self.is_file_changed(os.path.join(root, f)):
return True
return False
def is_glob_changed(self, path):
for f in glob.glob(path):
if self.is_file_changed(f):
return True
return False
class INotifyWatcher(Watcher):
def __init__(self):
Watcher.__init__(self)
import pyinotify
self.wm = pyinotify.WatchManager()
self.notifier = None
self.callback = None
def watch(self, path, func=None):
import pyinotify
flag = pyinotify.IN_CREATE | pyinotify.IN_DELETE | pyinotify.IN_MODIFY
self.wm.add_watch(path, flag, rec=True, do_glob=True, auto_add=True)
Watcher.watch(self, path, func)
def inotify_event(self, event):
self.callback()
def start(self, callback):
if not self.notifier:
self.callback = callback
import pyinotify
from tornado import ioloop
self.notifier = pyinotify.TornadoAsyncNotifier(
self.wm, ioloop.IOLoop.instance(),
default_proc_fun=self.inotify_event
)
callback()
return True
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
import scss.tool
scss.tool.main()
+262
View File
@@ -0,0 +1,262 @@
"""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]
+36
View File
@@ -0,0 +1,36 @@
################################################################################
# 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'
+384
View File
@@ -0,0 +1,384 @@
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)
+169
View File
@@ -0,0 +1,169 @@
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)
+905
View File
@@ -0,0 +1,905 @@
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',)
+25
View File
@@ -0,0 +1,25 @@
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
+2
View File
@@ -0,0 +1,2 @@
# Global cache of image sizes, shared between sprites and images libraries.
_image_size_cache = {}
+400
View File
@@ -0,0 +1,400 @@
"""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)
+654
View File
@@ -0,0 +1,654 @@
"""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)
+285
View File
@@ -0,0 +1,285 @@
"""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')
+346
View File
@@ -0,0 +1,346 @@
"""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])))
+524
View File
@@ -0,0 +1,524 @@
"""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)])
+784
View File
@@ -0,0 +1,784 @@
"""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
+471
View File
@@ -0,0 +1,471 @@
"""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)
+81
View File
@@ -0,0 +1,81 @@
"""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]
+524
View File
@@ -0,0 +1,524 @@
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
+67
View File
@@ -0,0 +1,67 @@
#!/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)
+607
View File
@@ -0,0 +1,607 @@
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
+14
View File
@@ -0,0 +1,14 @@
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
)
]
)
+554
View File
@@ -0,0 +1,554 @@
/*
* 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);
}
+547
View File
@@ -0,0 +1,547 @@
/*
* 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;
}
+52
View File
@@ -0,0 +1,52 @@
/*
* 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
+463
View File
@@ -0,0 +1,463 @@
/*
* 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);
}
}
+68
View File
@@ -0,0 +1,68 @@
/*
* 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
+98
View File
@@ -0,0 +1,98 @@
#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));
}
+403
View File
@@ -0,0 +1,403 @@
#!/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
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
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