summaryrefslogtreecommitdiff
path: root/scss/tool.py
diff options
context:
space:
mode:
authormasklinn <bitbucket.org@masklinn.net>2012-01-31 15:47:50 +0100
committermasklinn <bitbucket.org@masklinn.net>2012-01-31 15:47:50 +0100
commit9d5811a6a2270676402f4bf3d3c21183455ee0a1 (patch)
treec0535c9dce727bb5466631f8de38540c48e334b3 /scss/tool.py
parent2d1dc39b6225f05a5f7132b7464140bf791d5a68 (diff)
downloadpyscss-9d5811a6a2270676402f4bf3d3c21183455ee0a1.tar.gz
Make pyscss usable on the command-line again
Diffstat (limited to 'scss/tool.py')
-rw-r--r--scss/tool.py333
1 files changed, 333 insertions, 0 deletions
diff --git a/scss/tool.py b/scss/tool.py
new file mode 100644
index 0000000..544973c
--- /dev/null
+++ b/scss/tool.py
@@ -0,0 +1,333 @@
+#!/usr/bin/env python
+from __future__ import absolute_import
+
+import logging
+import os
+import re
+import sys
+from collections import deque
+
+from . import Scss, VERBOSITY, log
+from . import spawn_rule, to_str, profiling
+from . import _prop_split_re
+from .scss_meta import BUILD_INFO
+
+log.setLevel(logging.INFO)
+
+
+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:
+STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static/')
+# Assets path, where new sprite files are created:
+ASSETS_ROOT = os.path.join(PROJECT_ROOT, 'static/assets/')
+# Urls for the static and assets:
+STATIC_URL = '/static/'
+ASSETS_URL = '/static/assets/'
+
+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",
+ 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 debuging information")
+ parser.add_option("--no-debug-info", action="store_false",
+ dest="debug_info", default=False,
+ help="Turns off scss's debuging information")
+ parser.add_option("-t", "--test", action="store_true", help=SUPPRESS_HELP)
+ parser.add_option("-C", "--no-compress", action="store_false",
+ dest="compress", 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)")
+ parser.add_option_group(paths_group)
+
+ (options, args) = parser.parse_args()
+
+ # General runtime configuration
+ global LOAD_PATHS, VERBOSITY, STATIC_ROOT, ASSETS_ROOT
+ VERBOSITY = 0
+
+ if options.time:
+ VERBOSITY = 2
+ if options.static_root is not None:
+ STATIC_ROOT = options.static_root
+ if options.assets_root is not None:
+ ASSETS_ROOT = options.assets_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(LOAD_PATHS, 'split'):
+ load_path_list = [p.strip() for p in LOAD_PATHS.split(',')]
+ else:
+ load_path_list = list(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(LOAD_PATHS, 'split'):
+ LOAD_PATHS = ','.join(load_path_list)
+ else:
+ LOAD_PATHS = load_path_list
+
+ # Execution modes
+ if options.test:
+ import doctest
+ doctest.testfile('tests.rst')
+ elif options.version:
+ print BUILD_INFO
+ elif options.interactive:
+ from pprint import pprint
+ try:
+ import atexit
+ import readline
+ histfile = os.path.expanduser('~/.scss-history')
+ try:
+ readline.read_history_file(histfile)
+ except IOError:
+ pass
+ atexit.register(readline.write_history_file, histfile)
+ except ImportError:
+ pass
+
+ css = Scss()
+ context = css.scss_vars
+ options = css.scss_opts
+ rule = spawn_rule(context=context, options=options)
+ print "Welcome to %s interactive shell" % BUILD_INFO
+ while True:
+ try:
+ s = raw_input('>>> ').strip()
+ except EOFError:
+ print
+ break
+ except KeyboardInterrupt:
+ print
+ break
+ if s in ('exit', 'quit'):
+ break
+ for s in s.split(';'):
+ s = css.load_string(s.strip())
+ if not s:
+ continue
+ elif s.startswith('@'):
+ properties = []
+ children = deque()
+ spawn_rule(fileid='<string>', context=context, options=options, properties=properties)
+ code, name = (s.split(None, 1) + [''])[:2]
+ if code == '@option':
+ css._settle_options(rule, [''], set(), children, None, None, s, None, code, name)
+ continue
+ elif code == '@import':
+ css._do_import(rule, [''], set(), children, None, None, s, None, code, name)
+ continue
+ elif code == '@include':
+ final_cont = ''
+ css._do_include(rule, [''], set(), children, None, None, s, None, code, name)
+ code = css._print_properties(properties).rstrip('\n')
+ if code:
+ final_cont += code
+ if children:
+ css.children.extendleft(children)
+ css.parse_children()
+ code = css._create_css(css.rules).rstrip('\n')
+ if code:
+ final_cont += code
+ final_cont = css.post_process(final_cont)
+ print 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()
+ if not name:
+ pprint(sorted(['vars', 'options', 'mixins', 'functions']))
+ elif name in ('v', 'var', 'variable'):
+ if code == '*':
+ d = dict((k, v) for k, v in context.items())
+ pprint(d)
+ elif code:
+ d = dict((k, v) for k, v in context.items() if code in k)
+ pprint(d)
+ else:
+ d = dict((k, v) for k, v in context.items() if k.startswith('$') and not k.startswith('$__'))
+ pprint(d)
+ elif name in ('o', 'opt', 'option'):
+ if code == '*':
+ d = dict((k, v) for k, v in options.items())
+ pprint(d)
+ elif code:
+ d = dict((k, v) for k, v in options.items() if code in k)
+ pprint(d)
+ else:
+ d = dict((k, v) for k, v in options.items() if not k.startswith('@'))
+ pprint(d)
+ elif name in ('m', 'mix', 'mixin', 'f', 'func', 'funct', 'function'):
+ if name.startswith('m'):
+ name = 'mixin'
+ elif name.startswith('f'):
+ name = 'function'
+ if code == '*':
+ d = dict((k[len(name) + 2:], v) for k, v in options.items() if k.startswith('@' + name + ' '))
+ pprint(sorted(d))
+ elif code:
+ d = dict((k, v) for k, v in options.items() if k.startswith('@' + name + ' ') and code in k)
+ seen = set()
+ for k, mixin in d.items():
+ mixin = getattr(mixin, 'mixin', mixin)
+ fn_name, _, _ = k.partition(':')
+ if fn_name not in seen:
+ seen.add(fn_name)
+ print fn_name + '(' + ', '.join(p + (': ' + mixin[1].get(p) if p in mixin[1] else '') for p in mixin[0]) + ') {'
+ print ' ' + '\n '.join(l for l in mixin[2].split('\n'))
+ print '}'
+ else:
+ d = dict((k[len(name) + 2:].split(':')[0], v) for k, v in options.items() if k.startswith('@' + name + ' '))
+ pprint(sorted(d))
+ continue
+ elif s.startswith('$') and (':' in s or '=' in s):
+ prop, value = [a.strip() for a in _prop_split_re.split(s, 1)]
+ prop = css.do_glob_math(prop, context, options, rule, True)
+ value = css.calculate(value, context, options, rule)
+ context[prop] = value
+ continue
+ s = to_str(css.calculate(s, context, options, rule))
+ s = css.post_process(s)
+ print s
+ print "Bye!"
+ elif options.watch:
+ 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={
+ 'compress': options.compress,
+ '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") 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"):
+ 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)
+ src_file = open(src_path)
+ dest_file = open(dest_path, 'w')
+ dest_file.write(self.css.compile(src_file.read()))
+
+ 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")
+ 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()
+
+ else:
+ if options.output is not None:
+ output = open(options.output, 'wt')
+ else:
+ output = sys.stdout
+
+ css = Scss(scss_opts={
+ 'compress': options.compress,
+ 'debug_info': options.debug_info,
+ })
+ if args:
+ for path in args:
+ finput = open(path, 'rt')
+ output.write(css.compile(finput.read()))
+ else:
+ output.write(css.compile(sys.stdin.read()))
+
+ for f, t in profiling.items():
+ print >>sys.stderr, "%s took %03fs" % (f, t)
+
+if __name__ == "__main__":
+ main()