Livereload css
This commit is contained in:
@@ -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']
|
||||
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
@@ -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
@@ -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...')
|
||||
@@ -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
@@ -0,0 +1,3 @@
|
||||
import scss.tool
|
||||
|
||||
scss.tool.main()
|
||||
@@ -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]
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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',)
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# Global cache of image sizes, shared between sprites and images libraries.
|
||||
_image_size_cache = {}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
@@ -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])))
|
||||
@@ -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)])
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user