summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE21
-rw-r--r--README.rst24
-rw-r--r--xcss.py1963
3 files changed, 2008 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ea94979
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2010 Anton Pawlik
+
+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. \ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..b399bcb
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,24 @@
+nginx-gridfs
+============
+:Authors:
+ German M. Bravo (Kronuz) <german.mb@gmail.com>
+ Based on some code from the original xCSS project by Anton Pawlik
+ http://xcss.antpaw.org/about
+
+About
+=====
+xCSS for Python is a superset of CSS that is more powerful, elegant and easier
+to maintain than plain-vanilla CSS. The framework works as a CSS source code
+preprocesor which allows you to use variables, nested rules, mixins, and extend
+classes, all with a CSS-compatible syntax which the preprocessor then compiles
+to standard CSS.
+
+xCSS, 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 and many bugs have been fixed.
+
+License
+=======
+MIT License. See *LICENSE* for details.
+http://www.opensource.org/licenses/mit-license.php
diff --git a/xcss.py b/xcss.py
new file mode 100644
index 0000000..efa1f08
--- /dev/null
+++ b/xcss.py
@@ -0,0 +1,1963 @@
+#!/usr/bin/python
+#-*- coding: utf-8 -*-
+"""
+xCSS Framework for Python
+
+@author German M. Bravo (Kronuz)
+ Based on some code from the original xCSS project by Anton Pawlik
+@version 0.2
+@see http://xcss.antpaw.org/docs/
+ http://sass-lang.com/
+ http://oocss.org/spec/css-object-model.html
+@copyright (c) 2011 German M. Bravo (Kronuz)
+ (c) 2010 Anton Pawlik
+@license MIT License
+ http://www.opensource.org/licenses/mit-license.php
+
+xCSS for Python is a superset of CSS that is more powerful, elegant and easier
+to maintain than plain-vanilla CSS. The framework works as a CSS source code
+preprocesor which allows you to use variables, nested rules, mixins, and extend
+classes, all with a CSS-compatible syntax which the preprocessor then compiles
+to standard CSS.
+
+xCSS, 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 and many bugs have been fixed.
+
+"""
+
+import re
+import sys
+from math import *
+
+FILEID = 0
+POSITION = 1
+CODESTR = 2
+DEPS = 3
+CONTEXT = 4
+OPTIONS = 5
+SELECTORS = 6
+PROPERTIES = 7
+
+def safe_eval(str, context=None):
+ # Safe eval:
+
+ #make a list of safe functions
+ safe_locals = ['math','acos', 'asin', 'atan', 'atan2', 'ceil', 'cos',
+ 'cosh', 'degrees', 'e', 'exp', 'fabs', 'floor', 'fmod',
+ 'frexp', 'hypot', 'ldexp', 'log', 'log10', 'modf', 'pi',
+ 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh']
+ safe_builtins = ['abs', 'max', 'min', 'int', 'pow']
+
+ safe_dict = {}
+
+ # use the list to filter the local namespace
+ safe_dict.update(dict([ (k, globals().get(k, None)) for k in safe_locals ]))
+
+ # add any needed builtins back in
+ safe_dict.update(dict([ (k, getattr(__builtins__, k, None)) for k in safe_builtins ]))
+
+ # add user context
+ safe_dict.update(context or {})
+
+ return eval(str, {"__builtins__": None}, safe_dict)
+
+
+# this does not increase asymptotical complexity
+# but can still waste more time than it saves. TODO: profile
+# from http://stackoverflow.com/questions/2892931/longest-common-substring-from-more-than-two-strings-python
+def long_substr(strings):
+ substr = ""
+ if not strings:
+ return substr
+ reference = min(strings, key=len) #strings[0]
+ length = len(reference)
+ #find a suitable slice i:j
+ for i in xrange(length):
+ #only consider strings long at least len(substr) + 1
+ for j in xrange(i + len(substr) + 1, length + 1):
+ candidate = reference[i:j] # is the slice recalculated every time?
+ if all(candidate in text for text in strings):
+ substr = candidate
+ return substr
+
+
+class xCSS(object):
+ # configuration:
+ construct = 'self'
+ short_colors = True
+ reverse_colors = True
+
+ #optinos
+ verbosity = 0
+
+ # units and conversions
+ _units = ['em', 'ex', 'px', 'cm', 'mm', 'in', 'pt', 'pc', 'deg', 'rad'
+ 'grad', 'ms', 's', 'hz', 'khz', '%']
+ _conv = {
+ 'size': {
+ 'em': 13.0,
+ 'px': 1.0
+ },
+ 'length': {
+ 'mm': 1.0,
+ 'cm': 10.0,
+ 'in': 25.4,
+ 'pt': 25.4 / 72,
+ 'pc': 25.4 / 6
+ },
+ 'time': {
+ 'ms': 1.0,
+ 's': 1000.0
+ },
+ 'freq': {
+ 'hz': 1.0,
+ 'khz': 1000.0
+ }
+ }
+ _conv_mapping = {}
+ for t, m in _conv.items():
+ for k in m:
+ _conv_mapping[k] = t
+ del t, m, k
+
+ # color literals
+ _colors = {
+ 'aliceblue': '#f0f8ff',
+ 'antiquewhite': '#faebd7',
+ 'aqua': '#00ffff',
+ 'aquamarine': '#7fffd4',
+ 'azure': '#f0ffff',
+ 'beige': '#f5f5dc',
+ 'bisque': '#ffe4c4',
+ 'black': '#000000',
+ 'blanchedalmond': '#ffebcd',
+ 'blue': '#0000ff',
+ 'blueviolet': '#8a2be2',
+ 'brown': '#a52a2a',
+ 'burlywood': '#deb887',
+ 'cadetblue': '#5f9ea0',
+ 'chartreuse': '#7fff00',
+ 'chocolate': '#d2691e',
+ 'coral': '#ff7f50',
+ 'cornflowerblue': '#6495ed',
+ 'cornsilk': '#fff8dc',
+ 'crimson': '#dc143c',
+ 'cyan': '#00ffff',
+ 'darkblue': '#00008b',
+ 'darkcyan': '#008b8b',
+ 'darkgoldenrod': '#b8860b',
+ 'darkgray': '#a9a9a9',
+ 'darkgreen': '#006400',
+ 'darkkhaki': '#bdb76b',
+ 'darkmagenta': '#8b008b',
+ 'darkolivegreen': '#556b2f',
+ 'darkorange': '#ff8c00',
+ 'darkorchid': '#9932cc',
+ 'darkred': '#8b0000',
+ 'darksalmon': '#e9967a',
+ 'darkseagreen': '#8fbc8f',
+ 'darkslateblue': '#483d8b',
+ 'darkslategray': '#2f4f4f',
+ 'darkturquoise': '#00ced1',
+ 'darkviolet': '#9400d3',
+ 'deeppink': '#ff1493',
+ 'deepskyblue': '#00bfff',
+ 'dimgray': '#696969',
+ 'dodgerblue': '#1e90ff',
+ 'firebrick': '#b22222',
+ 'floralwhite': '#fffaf0',
+ 'forestgreen': '#228b22',
+ 'fuchsia': '#ff00ff',
+ 'gainsboro': '#dcdcdc',
+ 'ghostwhite': '#f8f8ff',
+ 'gold': '#ffd700',
+ 'goldenrod': '#daa520',
+ 'gray': '#808080',
+ 'green': '#008000',
+ 'greenyellow': '#adff2f',
+ 'honeydew': '#f0fff0',
+ 'hotpink': '#ff69b4',
+ 'indianred': '#cd5c5c',
+ 'indigo': '#4b0082',
+ 'ivory': '#fffff0',
+ 'khaki': '#f0e68c',
+ 'lavender': '#e6e6fa',
+ 'lavenderblush': '#fff0f5',
+ 'lawngreen': '#7cfc00',
+ 'lemonchiffon': '#fffacd',
+ 'lightblue': '#add8e6',
+ 'lightcoral': '#f08080',
+ 'lightcyan': '#e0ffff',
+ 'lightgoldenrodyellow': '#fafad2',
+ 'lightgreen': '#90ee90',
+ 'lightgrey': '#d3d3d3',
+ 'lightpink': '#ffb6c1',
+ 'lightsalmon': '#ffa07a',
+ 'lightseagreen': '#20b2aa',
+ 'lightskyblue': '#87cefa',
+ 'lightslategray': '#778899',
+ 'lightsteelblue': '#b0c4de',
+ 'lightyellow': '#ffffe0',
+ 'lime': '#00ff00',
+ 'limegreen': '#32cd32',
+ 'linen': '#faf0e6',
+ 'magenta': '#ff00ff',
+ 'maroon': '#800000',
+ 'mediumaquamarine': '#66cdaa',
+ 'mediumblue': '#0000cd',
+ 'mediumorchid': '#ba55d3',
+ 'mediumpurple': '#9370db',
+ 'mediumseagreen': '#3cb371',
+ 'mediumslateblue': '#7b68ee',
+ 'mediumspringgreen': '#00fa9a',
+ 'mediumturquoise': '#48d1cc',
+ 'mediumvioletred': '#c71585',
+ 'midnightblue': '#191970',
+ 'mintcream': '#f5fffa',
+ 'mistyrose': '#ffe4e1',
+ 'moccasin': '#ffe4b5',
+ 'navajowhite': '#ffdead',
+ 'navy': '#000080',
+ 'oldlace': '#fdf5e6',
+ 'olive': '#808000',
+ 'olivedrab': '#6b8e23',
+ 'orange': '#ffa500',
+ 'orangered': '#ff4500',
+ 'orchid': '#da70d6',
+ 'palegoldenrod': '#eee8aa',
+ 'palegreen': '#98fb98',
+ 'paleturquoise': '#afeeee',
+ 'palevioletred': '#db7093',
+ 'papayawhip': '#ffefd5',
+ 'peachpuff': '#ffdab9',
+ 'peru': '#cd853f',
+ 'pink': '#ffc0cb',
+ 'plum': '#dda0dd',
+ 'powderblue': '#b0e0e6',
+ 'purple': '#800080',
+ 'red': '#ff0000',
+ 'rosybrown': '#bc8f8f',
+ 'royalblue': '#4169e1',
+ 'saddlebrown': '#8b4513',
+ 'salmon': '#fa8072',
+ 'sandybrown': '#f4a460',
+ 'seagreen': '#2e8b57',
+ 'seashell': '#fff5ee',
+ 'sienna': '#a0522d',
+ 'silver': '#c0c0c0',
+ 'skyblue': '#87ceeb',
+ 'slateblue': '#6a5acd',
+ 'slategray': '#708090',
+ 'snow': '#fffafa',
+ 'springgreen': '#00ff7f',
+ 'steelblue': '#4682b4',
+ 'tan': '#d2b48c',
+ 'teal': '#008080',
+ 'thistle': '#d8bfd8',
+ 'tomato': '#ff6347',
+ 'turquoise': '#40e0d0',
+ 'violet': '#ee82ee',
+ 'wheat': '#f5deb3',
+ 'white': '#ffffff',
+ 'whitesmoke': '#f5f5f5',
+ 'yellow': '#ffff00',
+ 'yellowgreen': '#9acd32'
+ }
+
+ _default_xcss_vars = {
+ # unsafe chars will be hidden as vars
+ '$__doubleslash': '//',
+ '$__bigcopen': '/*',
+ '$__bigcclose': '*/',
+ '$__doubledot': ':',
+ '$__semicolon': ';',
+ '$__curlybracketopen': '{',
+ '$__curlybracketclosed': '}',
+
+ # shortcuts (it's "a hidden feature" for now)
+ 'bg:': 'background:',
+ 'bgc:': 'background-color:',
+ }
+
+ _default_xcss_opts = {
+ 'verbosity': 1,
+ }
+
+ _short_color_re = re.compile(r'(?<!\w)#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3\b', re.IGNORECASE)
+ _long_color_re = re.compile(r'(?<!\w)#([a-f0-9]){2}([a-f0-9]){2}([a-f0-9]){2}\b', re.IGNORECASE)
+ _reverse_colors = dict((v, k) for k, v in _colors.items())
+ for long_k, v in _colors.items():
+ # Calculate the different possible representations of a color:
+ short_k = _short_color_re.sub(r'#\1\2\3', v).lower()
+ rgb_k = _long_color_re.sub(lambda m: 'rgb(%d, %d, %d)' % (int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)), v)
+ rgba_k = _long_color_re.sub(lambda m: 'rgba(%d, %d, %d, 255)' % (int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)), v)
+ # get the shortest of all to use it:
+ k = min([short_k, long_k, rgb_k, rgba_k], key=len)
+ _reverse_colors[short_k] = k
+ _reverse_colors[rgb_k] = k
+ _reverse_colors[rgba_k] = k
+ _reverse_colors_re = re.compile(r'(?<!\w)(' + '|'.join(map(re.escape, _reverse_colors.keys()))+r')\b', re.IGNORECASE)
+ _colors_re = re.compile(r'\b(' + '|'.join(map(re.escape, _colors.keys()))+r')\b', re.IGNORECASE)
+
+ _color_re = re.compile(r'''
+ (# RGB in the hex form (#RRGGBB):
+ (?<!\w)
+ \#
+ (?P<short_R>[a-f0-9])
+ (?P<short_G>[a-f0-9])
+ (?P<short_B>[a-f0-9])
+ \b
+ | # RGB in the hex form (#RRGGBB):
+ (?<!\w)
+ \#
+ (?P<long_R>[a-f0-9]{2})
+ (?P<long_G>[a-f0-9]{2})
+ (?P<long_B>[a-f0-9]{2})
+ \b
+ | # RGB using the rgb():
+ \b
+ rgb\(
+ \s*(?P<rgb_R>\d+)(?P<rgbX>%?)\s*,
+ \s*(?P<rgb_G>\d+)(?P=rgbX)\s*,
+ \s*(?P<rgb_B>\d+)(?P=rgbX)\s*
+ \)
+ | # RGBA using the rgba():
+ \b
+ rgba\(
+ \s*(?P<rgba_R>\d+)(?P<rgbaX>%?)\s*,
+ \s*(?P<rgba_G>\d+)(?P=rgbaX)\s*,
+ \s*(?P<rgba_B>\d+)(?P=rgbaX)\s*,
+ \s*(?P<rgba_A>\d+)(?P=rgbaX)\s*
+ \)
+ )
+ ''', re.IGNORECASE | re.VERBOSE)
+ _expr_re = re.compile(r'''
+ (?P<expr>
+ \#\{.*?\}
+ |
+ (?:
+ [[(][\][()\s]*
+ )?
+ [#%.\w]+ # Accept a variable or constant or number (preceded by spaces or parenthesis)
+ (?:
+ (?:
+ \( # Accept either the start of a function call... ("function_call(")
+ |
+ [\][()\s]* # ...or an operation ("+-*/")
+ (?:\s-\s|[+*/]) # (dash needs a surrounding space always)
+ )
+ [\][()\s]*
+ [#%.\w]+ # Accept a variable or constant or number (preceded by spaces or parenthesis)
+ )* # ...take n arguments,
+ (?:
+ [\][()\s]*[\])] # but then finish accepting any missing parenthesis and spaces
+ )?
+ (?!.*:)
+ (?=.*;)
+ )
+ (?P<unit>
+ \s*[;}]
+ |
+ \s*$
+ |
+ [^;}\s]+
+ |
+ \s
+ )
+ ''', re.VERBOSE)
+ #_expr_re = re.compile(r'(\[.*?\])([\s;}]|$|.+?\S)') # <- This is the old method, required parenthesis around the expression
+ _ml_comment_re = re.compile(r'\/\*(.*?)\*\/', re.DOTALL)
+ _sl_comment_re = re.compile(r'(?<!\w{2}:)\/\/.*')
+ _unit_re = re.compile(r'(?<=(\s|\d))' + '|'.join(map(re.escape, _units)) + r'(?!\w)', re.IGNORECASE)
+ _full_unit_re = re.compile(r'([\d.]+)(' + '|'.join(map(re.escape, _units)) + r')?(?!\w)', re.IGNORECASE)
+ _zero_units_re = re.compile(r'\b0(' + '|'.join(map(re.escape, _units)) + r')(?!\w)', re.IGNORECASE)
+
+ _includes_re = re.compile(r'@include\s+(.*?)\s*(\((.+?,)*(.+?)\))?\s*([;}]|$)', re.DOTALL)
+ _remove_decls_re = re.compile(r'(@option\b.*?([;}]|$))', re.DOTALL | re.IGNORECASE)
+ _fix_extends_re = re.compile(r'\s+extends\s+', re.IGNORECASE)
+ _collapse_properties_space_re = re.compile(r'([:#])\s*{')
+ _expand_rules_space_re = re.compile(r'\s*{')
+
+ _reverse_default_xcss_vars = dict((v, k) for k, v in _default_xcss_vars.items())
+ _reverse_default_xcss_vars_re = re.compile(r'(content.*:.*(\'|").*)(' + '|'.join(map(re.escape, _reverse_default_xcss_vars.keys())) + ')(.*\2)')
+
+ def __init__(self):
+ pass
+
+ def locate_blocks(self, str, open='{', close='}', joiner=','):
+ """
+ Returns all code blocks between `open` and `close` and a proper key
+ that can be multilined as long as it's joined by `joiner`.
+ Returns the lose code that's not part of the code as a third item.
+ """
+ depth = 0
+ losing = 0
+ skip = False
+ i = init = lose = 0
+ start = end = None
+ str_len = len(str)
+ open_len = len(open)
+ close_len = len(close)
+ joiner_len = len(joiner)
+ while i < str_len:
+ if i > 0 and str[i-1] == '#' and str[i:i+open_len] == open:
+ skip = True
+ if not skip and str[i:i+open_len] == open:
+ depth += 1
+ if depth == 1:
+ start = i
+ if lose < init:
+ losestr = str[lose:init]
+ yield None, None, losestr
+ lose = init
+ losing = 0
+ i += open_len
+ elif not skip and str[i:i+close_len] == close:
+ if depth > 0:
+ depth -= 1
+ if depth == 0:
+ end = i
+ selectors = str[init:start].strip()
+ codestr = str[start+open_len:end].strip()
+ if selectors:
+ yield selectors, codestr, None
+ init = lose = end + close_len
+ losing = 0
+ else:
+ init = lose = i + close_len
+ i += close_len
+ else:
+ if skip and str[i:i+close_len] == close:
+ skip = False
+ if depth == 0:
+ if str[i] == '\n':
+ if losing:
+ losing = 2 # If last thing we saw was a real character, we're now walking on ice...
+ elif str[i:i+joiner_len] == joiner:
+ losing = 0 # We're safe not (for the moment)
+ i += joiner_len - 1
+ elif str[i] not in ('\t', ' '):
+ if losing == 2:
+ init = i # We were walking on ice, now it's broken!
+ losing = 0
+ else:
+ losing = 1 # We saw a real character!
+ i += 1
+ else:
+ i += 1
+ losestr = str[lose:]
+ yield None, None, losestr
+
+ def compile(self, input_xcss=None):
+ # Initialize
+ self.rules = []
+ self._rules = {}
+ self.parts = {}
+ self.css_files = []
+ self.xcss_vars = self._default_xcss_vars.copy()
+ self.xcss_opts = self._default_xcss_opts.copy()
+
+ self._contexts = {}
+ self._replaces = {}
+
+ if input_xcss is not None:
+ self.xcss_files = {}
+ self.xcss_files['string'] = input_xcss + '\n'
+ self.xcss_files = self.xcss_files or {}
+
+ # Compile
+ for fileid, str in self.xcss_files.items():
+ self.parse_xcss_string(fileid, str)
+
+ if self.parts:
+ # this will manage xCSS rule: child objects inside of a node
+ self.parse_children()
+
+ # this will manage xCSS rule: ' extends '
+ self.parse_extends()
+
+ # this will manage properties in rules
+ self.parse_properties()
+
+ # this will manage the order of the rules
+ self.manage_order()
+
+ final_cont = ''
+ for fileid in self.css_files:
+ if fileid != 'string':
+ final_cont += '/* Generated frm: ' + fileid + ' */\n'
+ fcont = self.create_css(fileid)
+ fcont = self.do_math(fcont)
+ fcont = self.post_process(fcont)
+ final_cont += fcont
+
+ return final_cont
+
+ def post_process(self, cont):
+ # short colors:
+ if self.short_colors:
+ cont = self._short_color_re.sub(r'#\1\2\3', cont)
+
+ # color names:
+ if self.reverse_colors:
+ cont = self._reverse_colors_re.sub(lambda m: self._reverse_colors[m.group(0).lower()], cont)
+
+ # zero units out (i.e. 0px or 0em -> 0):
+ cont = self._zero_units_re.sub('0', cont)
+ return cont
+
+ def use_vars(self, cont, context=None, options=None):
+ xcss_vars = self.xcss_vars.copy()
+ xcss_vars.update(context or {})
+ vars = xcss_vars.keys()
+ try:
+ remove_vars_re, interpolate_re = self._contexts[tuple(vars)]
+ except KeyError:
+ vars1 = []
+ vars2 = []
+ for var in vars:
+ if var[0] == '$':
+ vars1.append(re.escape(var))
+ else:
+ vars2.append(re.escape(var))
+ remove_vars_re = re.compile(r'(?<![-\w])(((' + '|'.join(vars1) + r')\s*[:=]|(' + '|'.join(vars2) + r')\s*=).*?([;}]|$))')
+ interpolate_re = re.compile(r'(?<![-\w])(' + '|'.join(map(re.escape, vars)) + r')(?![-\w])')
+ self._contexts[tuple(vars)] = remove_vars_re, interpolate_re
+
+ # remove variables declarations from the rules
+ cont = self._remove_decls_re.sub('', cont)
+ cont = remove_vars_re.sub('', cont)
+
+ cnt = 0
+ old_cont = None
+ while cont != old_cont and cnt < 5:
+ cnt += 1
+ old_cont = cont
+
+ # interpolate variables:
+ cont = interpolate_re.sub(lambda m: xcss_vars[m.group(0)], cont)
+
+ return cont
+
+ def do_math(self, content):
+ def calculate(result):
+ _base_str = result.group(0)
+ _expr_str = result.group('expr')
+ _unit_str = result.group('unit')
+ if _expr_str.startswith('url(') or _expr_str.startswith('expression('):
+ return _base_str
+ try:
+ better_expr_str = self._replaces[_base_str]
+ except KeyError:
+ if _expr_str[:2] == '#{' and _expr_str[-1] == '}':
+ _expr_str = _expr_str[2:-1].strip('[]')
+
+ better_expr_str = self._colors_re.sub(lambda m: self._colors.get(m.group(0), m.group(0)), _expr_str).replace('[', '(').replace(']', ')')
+
+ rgba = None
+ for color in self._color_re.finditer(better_expr_str):
+ c = color.groupdict()
+ if c.get('short_R'):
+ r = int(c.get('short_R')*2, 16)
+ g = int(c.get('short_G')*2, 16)
+ b = int(c.get('short_B')*2, 16)
+ a = 0
+ elif c.get('long_R'):
+ r = int(c.get('long_R'), 16)
+ g = int(c.get('long_G'), 16)
+ b = int(c.get('long_B'), 16)
+ a = 0
+ elif c.get('rgb_R') is not None:
+ r = int(c.get('rgb_R'))
+ g = int(c.get('rgb_G'))
+ b = int(c.get('rgb_B'))
+ a = 0
+ else:
+ r = int(c.get('rgba_R'))
+ g = int(c.get('rgba_G'))
+ b = int(c.get('rgba_B'))
+ a = 255 - int(c.get('rgba_A'))
+ if c.get('rgb_X') or c.get('rgba_X'):
+ r = '%02x' % (r * 255 / 100)
+ g = '%02x' % (g * 255 / 100)
+ b = '%02x' % (b * 255 / 100)
+ a = '%02x' % (a * 255 / 100)
+ else:
+ r = '%02x' % r
+ g = '%02x' % g
+ b = '%02x' % b
+ a = '%02x' % a
+ _base_color_str = color.group(0)
+ if not rgba:
+ rgba = [
+ better_expr_str.replace(_base_color_str, '0x'+r),
+ better_expr_str.replace(_base_color_str, '0x'+g),
+ better_expr_str.replace(_base_color_str, '0x'+b),
+ better_expr_str.replace(_base_color_str, '0x'+a),
+ ]
+ else:
+ rgba = [
+ rgba[0].replace(_base_color_str, '0x'+r),
+ rgba[1].replace(_base_color_str, '0x'+g),
+ rgba[2].replace(_base_color_str, '0x'+b),
+ rgba[3].replace(_base_color_str, '0x'+a),
+ ]
+
+ if rgba:
+ try:
+ r = int(safe_eval(rgba[0])); r = 0 if r < 0 else 255 if r > 255 else r
+ g = int(safe_eval(rgba[1])); g = 0 if g < 0 else 255 if g > 255 else g
+ b = int(safe_eval(rgba[2])); b = 0 if b < 0 else 255 if b > 255 else b
+ a = int(safe_eval(rgba[3])); a = 0 if a < 0 else 255 if a > 255 else a
+
+ if a == 0:
+ _better_expr_str = '#%02x%02x%02x' % (r, g, b)
+ else:
+ _better_expr_str = 'rgba(%d, %d, %d, %d)' % (r, g, b, 255-a)
+ except NameError:
+ pass
+ except ValueError:
+ pass
+ except SyntaxError, e:
+ if self.xcss_opts.get('verbosity', self.verbosity) > 2:
+ better_expr_str += ' /* ERROR: %s! */' % e
+ except:
+ better_expr_str += ' /* ERROR: %s! */' % e
+ else:
+ better_expr_str = _better_expr_str
+ else:
+ try:
+ global_factor = 1.0
+ if _unit_str == '%':
+ global_factor = 100.0
+ else:
+ for m in self._full_unit_re.finditer(better_expr_str):
+ unit = m.group(2)
+ if unit == '%':
+ global_factor = 100.0
+ break
+
+ unit_types = set()
+ my_units = {}
+ def reunit(m):
+ value = m.group(1)
+ unit = m.group(2)
+ unit = unit and unit.lower()
+ my_units.setdefault(unit, 0)
+ my_units[unit] += 1
+ my_units.setdefault('count', 0)
+ if my_units['count'] < my_units[unit]:
+ my_units['count'] = my_units[unit]
+ if unit == '%' or not unit:
+ unit_type = None
+ factor = 1.0
+ else:
+ unit_type = self._conv_mapping.get(unit, 'unknown')
+ factor = self._conv.get(unit_type, {}).get(unit, 1.0)
+ unit_types.add(unit_type)
+ return str(float(value) * factor / global_factor)
+ better_expr_str = self._full_unit_re.sub(reunit, better_expr_str)
+
+ new_unit = ''
+ final_unit = _unit_str.lower()
+ if final_unit in (';', '\t', ' '):
+ for unit, count in my_units.items():
+ if unit != 'count' and count == my_units['count']:
+ new_unit = unit
+ break
+ final_unit = new_unit = new_unit or ''
+ if final_unit == '%' or not final_unit:
+ final_unit_type = None
+ final_factor = 1.0
+ elif final_unit:
+ final_unit_type = self._conv_mapping.get(final_unit, 'unknown')
+ final_factor = self._conv.get(final_unit_type, {}).get(final_unit, 1.0)
+ unit_types.add(final_unit_type)
+ unit_types.discard(None)
+ _better_expr_str = self._unit_re.sub('', better_expr_str)
+ num = str('%0.04f' % round(safe_eval(_better_expr_str) * global_factor / final_factor)).rstrip('0').rstrip('.')
+ _better_expr_str = '%s%s' % (num, new_unit)
+ if len(unit_types) > 1:
+ _better_expr_str += ' /* ERROR: units mismatch: %s! */' % ' vs. '.join(unit_types)
+ except NameError:
+ pass
+ except ValueError:
+ pass
+ except SyntaxError, e:
+ if self.xcss_opts.get('verbosity', self.verbosity) > 2:
+ better_expr_str += ' /* ERROR: %s! */' % e
+ except Exception, e:
+ better_expr_str += ' /* ERROR: %s! */' % e
+ else:
+ better_expr_str = _better_expr_str
+ self._replaces[_base_str] = better_expr_str
+ return better_expr_str + _unit_str
+ #print >>sys.stderr, self._expr_re.findall(content)
+ content = self._expr_re.sub(calculate, content)
+ return content
+
+ def parse_xcss_string(self, fileid, str):
+ # protects content: "..." strings
+ str = self._reverse_default_xcss_vars_re.sub(lambda m: m.group(0) + self._reverse_default_xcss_vars.get(m.group(2)) + m.group(3), str)
+
+ # removes multiple line comments
+ str = self._ml_comment_re.sub('', str)
+
+ # removes inline comments, but not :// (protocol)
+ str = self._sl_comment_re.sub('', str)
+
+ # expand the space in rules
+ str = self._expand_rules_space_re.sub(' {', str)
+ # collapse the space in properties blocks
+ str = self._collapse_properties_space_re.sub(r'\1{', str)
+ # Fixe tabs and spaces in ' extends '
+ str = self._fix_extends_re.sub(' extends ', str)
+
+ my_rules = []
+ context = {}
+ options = {}
+ for _selectors, codestr, lose in self.locate_blocks(str):
+ if lose is not None:
+ self.process_properties(lose, context, options)
+ elif _selectors[-1] == ':':
+ self.process_properties(_selectors + '{' + codestr + '}', context, options)
+ else:
+ rule, name = (_selectors.split(None, 1)+[''])[:2]
+ rule = rule.strip()
+ name = name.strip()
+ if rule == '@variables' or rule == 'vars': # vars for backwards xCSS compatibility
+ if name:
+ name = name + '.'
+ self.process_properties(codestr, self.xcss_vars, self.xcss_opts, self.xcss_vars, name)
+ _selectors = None
+ elif rule == '@mixin':
+ if name:
+ funct, _, params = name.partition('(')
+ funct = funct.strip()
+ params = params.strip('()').split(',')
+ q = 0
+ vars = {}
+ defaults = {}
+ new_params = []
+ for param in params:
+ id = chr(97+q)
+ param, _, default = param.partition(':')
+ param = param.strip()
+ default = default.strip()
+ if param:
+ if default:
+ defaults['$__'+id+'__'] = default.strip()
+ vars[param] = '[$__'+id+'__]'
+ new_params.append(id)
+ q += 1
+ if vars:
+ rename_vars_re = re.compile(r'(?<!\w)(' + '|'.join(map(re.escape, vars.keys())) + r')(?!\w)')
+ codestr = rename_vars_re.sub(lambda m: vars[m.group(0)], codestr)
+ mixin = [ defaults, codestr ]
+ if not new_params:
+ self.xcss_opts['@mixin ' + funct] = mixin
+ while len(new_params):
+ self.xcss_opts['@mixin ' + funct + '('+','.join(new_params)+')'] = mixin
+ param = '$__'+new_params.pop()+'__'
+ if param not in defaults:
+ break
+ _selectors = None
+ elif rule == '@prototype':
+ _selectors = name # prototype keyword simply ignored (all selectors are prototypes)
+ if _selectors:
+ # normalizing selectors by stripping them and joining them using a single ','
+ _selectors = self.normalize_selectors(_selectors)
+ rule = [ fileid, len(self.rules), codestr, set(), None, None, None, None ]
+ my_rules.append(rule)
+ self.rules.append(rule)
+ self.parts.setdefault(_selectors, [])
+ self.parts[_selectors].append(rule)
+
+ def expand_includes(m):
+ funct = m.group(1)
+ funct = funct.strip()
+ params = m.group(2)
+ params = params and params.strip('()').split(',') or []
+ q = 0
+ _vars = {}
+ new_params = []
+ for param in params:
+ id = chr(97+q)
+ param = param.strip()
+ _vars['$__'+id+'__'] = param
+ new_params.append(id)
+ q += 1
+ new_params = new_params and '('+','.join(new_params)+')' or ''
+ mixin = self.xcss_opts.get('@mixin ' + funct + new_params)
+ if mixin:
+ codestr = mixin[1]
+ vars = mixin[0].copy()
+ vars.update(_vars)
+ if vars:
+ rename_vars_re = re.compile(r'(?<!\w)(' + '|'.join(map(re.escape, vars.keys())) + r')(?!\w)')
+ codestr = rename_vars_re.sub(lambda m: vars[m.group(0)], codestr)
+ return codestr.replace('[', '(').replace(']', ')')
+ return m.group(0)
+
+ for rule in my_rules:
+ # give each rule a new copy of the context and its options
+ rule[CONTEXT] = context.copy()
+ rule[OPTIONS] = options.copy()
+ rule[CODESTR] = self._includes_re.sub(expand_includes, rule[CODESTR])
+
+ def process_properties(self, codestr, context, options, properties=None, scope=''):
+ for p_selectors, p_codestr, lose in self.locate_blocks(codestr):
+ if lose is not None:
+ codestr = lose
+ codes = [ s.strip() for s in codestr.split(';') if s.strip() ]
+ for code in codes:
+ prop, value = (re.split(r'[:=]', code, 1) + [''])[:2]
+ try:
+ is_var = (code[len(prop)] == '=')
+ except IndexError:
+ is_var = False
+
+ prop = prop.strip()
+ if prop:
+ value = value.strip()
+ _prop = scope + prop
+ if prop[0] == '@':
+ prop, name = (prop.split(None, 1)+[''])[:2]
+ prop = prop.strip()
+ name = name.strip()
+ options[prop + ' ' + name] = value
+ elif is_var or prop[0] == '$':
+ if value:
+ context[_prop] = value
+ else:
+ if properties is not None and value:
+ properties[_prop] = value
+ elif p_selectors[-1] == ':':
+ self.process_properties(p_codestr, context, options, properties, scope + p_selectors[:-1] + '-')
+
+ def parse_properties(self):
+ # build final properties:
+ for _selectors, rules in self.parts.items():
+ for rule in rules:
+ fileid, position, codestr, deps, context, options, selectors, properties = rule
+ if position is not None and codestr and properties is None:
+ assert selectors is None, "Rule body is repeated for different selectors!"
+ rule[SELECTORS] = _selectors
+ rule[PROPERTIES] = properties = rule[PROPERTIES] or {}
+ rule[CONTEXT] = context = rule[CONTEXT] or {}
+ rule[OPTIONS] = context = rule[OPTIONS] or {}
+ self.process_properties(codestr, context, options, properties)
+
+ def manage_order(self):
+ # order rules according with their dependencies
+ for rule in self.rules:
+ if rule[POSITION] is not None:
+ rule[DEPS].add(rule[POSITION]+1)
+ # This moves the rules just above the topmost dependency during the sorted() below:
+ rule[POSITION] = min(rule[DEPS])
+ self.rules = sorted(self.rules, key=lambda o: o[POSITION])
+
+ self.css_files = []
+ self._rules = {}
+ css_files = set()
+ old_fileid = None
+ for rule in self.rules:
+ fileid, position, codestr, deps, context, options, selectors, properties = rule
+ self._rules.setdefault(fileid, [])
+ self._rules[fileid].append(rule)
+ if position is not None and properties:
+ if old_fileid != fileid:
+ old_fileid = fileid
+ if fileid not in css_files:
+ css_files.add(fileid)
+ self.css_files.append(fileid)
+
+ def create_css(self, fileid=None):
+ # generate the final CSS string
+ result = ''
+ if fileid:
+ rules = self._rules.get(fileid) or []
+ else:
+ rules = self.rules
+
+ for rule in rules:
+ fileid, position, codestr, deps, context, options, selectors, properties = rule
+ #print >>sys.stderr, fileid, position, context, options, selectors, properties
+ if position is not None and properties:
+ # feel free to modifie the indentations the way you like it
+ result += ',\n'.join(selectors.split(',')) + ' {\n'
+ if options.get('verbosity', self.verbosity) > 0:
+ result += '\t/* file: ' + fileid + ' */\n'
+ if context:
+ result += '\t/* vars:\n'
+ for k, v in context.items():
+ result += '\t\t' + k + ' = ' + v + ';\n'
+ result += '\t*/\n'
+ for prop, value in properties.items():
+ result += '\t' + prop + ': ' + value + ';\n'
+ result += '}\n'
+ return result
+
+ def search_for_parent(self, parent, c_selectors, c_rules):
+ parent_found = None
+ for p_selectors, p_rules in self.parts.items():
+ _p_selectors, _, _ = p_selectors.partition(' extends ')
+ _p_selectors = _p_selectors.split(',')
+ new_selectors = set()
+ # finds all the parent selectors and parent selectors with another
+ # bind selectors behind. For example, if `.specialClass extends .baseClass`,
+ # and there is a `.baseClass` selector, the extension should create
+ # `.specialClass` for that rule, but if there's also a `.baseClass a`
+ # it also should create `.specialClass a`
+ for p_selector in _p_selectors:
+ if parent in p_selector:
+ # get the new child selector to add (same as the parent selector but with the child name)
+ # since selectors can be together, separated with # or . (i.e. something.parent) check that too:
+ for c_selector in c_selectors.split(','):
+ new_selectors.add(re.sub(parent[0] not in ('#', '.') and r'(?<!\w|-)' or '' + parent + r'(?!\w|-)', c_selector, p_selector))
+ if new_selectors:
+ new_selectors = self.normalize_selectors(p_selectors, new_selectors)
+ # rename node:
+ if new_selectors != p_selectors:
+ self.parts.setdefault(new_selectors, [])
+ self.parts[new_selectors].extend(p_rules)
+ del self.parts[p_selectors]
+
+ # save child dependencies:
+ for c_rule in c_rules or []:
+ for p_rule in p_rules:
+ p_rule[DEPS].add(c_rule[POSITION]) # position is the "index" of the object
+
+ # add parent:
+ parent_found = parent_found or []
+ parent_found.extend(p_rules)
+
+ return parent_found
+
+ def manage_multiple_extends(self, parts):
+ # To be able to manage multiple extends, you need to
+ # destroy the actual node and create many nodes that have
+ # mono extend. The first one gets all the css rules
+ for _selectors, rules in parts.items():
+ if ' extends ' in _selectors:
+ selectors, _, parent = _selectors.partition(' extends ')
+ parents = parent.split('&')
+ del parts[_selectors]
+ for parent in parents:
+ new_selectors = selectors + ' extends ' + parent
+ parts.setdefault(new_selectors, [])
+ parts[new_selectors].extend(rules)
+ rules = [] # further rules extending other parents will be empty
+
+ def parse_extends(self):
+ self.manage_multiple_extends(self.parts)
+ for _selectors, rules in self.parts.items():
+ selectors, _, parent = _selectors.partition(' extends ')
+ if parent:
+ self.parts.setdefault(selectors, [])
+ self.parts[selectors].extend(rules)
+ if _selectors in self.parts:
+ # It might be that search_for_parent or previous iterations
+ # already have removed the selectors...
+ del self.parts[_selectors]
+ parents = self.search_for_parent(parent, selectors, rules)
+
+ assert parents is not None, "Parent not found: %s (%s)" % (parent, selectors)
+
+ # from the parent, copy the context and the options:
+ new_context = {}
+ new_options = {}
+ for parent in parents:
+ new_context.update(parent[CONTEXT])
+ new_options.update(parent[OPTIONS])
+ for rule in rules:
+ _new_context = new_context.copy()
+ _new_context.update(rule[CONTEXT])
+ rule[CONTEXT] = _new_context
+ _new_options = new_options.copy()
+ _new_options.update(rule[OPTIONS])
+ rule[OPTIONS] = _new_options
+
+ def normalize_selectors(self, _selectors, selector=None):
+ if isinstance(selector, basestring):
+ selector = set([selector])
+ if ' extends ' in _selectors:
+ parents = set()
+ selectors = set()
+ for key in _selectors.split(','):
+ child, _, parent = key.partition(' extends ')
+ child = child.strip()
+ parent = parent.strip()
+ selectors.add(child)
+ parents.update(s.strip() for s in parent.split('&') if s.strip())
+ if selector:
+ selectors.update(selector)
+ selectors.discard('')
+ if not selectors:
+ return None
+ parents.discard('')
+ if parents:
+ return ','.join(sorted(selectors)) + ' extends ' + '&'.join(sorted(parents))
+ return ','.join(sorted(selectors))
+ else:
+ selectors = set(s.strip() for s in _selectors.split(',') if s.strip())
+ if selector:
+ selectors.update(selector)
+ selectors.discard('')
+ if not selectors:
+ return None
+ return ','.join(sorted(selectors))
+
+ def parse_children(self):
+ cnt = 0
+ children_left = True
+ while children_left and cnt < 5:
+ cnt += 1
+ children_left = False
+ for _selectors, rules in self.parts.items():
+ nested = False
+ interpolated = False
+ new_selectors = _selectors
+ for rule in rules:
+ fileid, position, codestr, deps, context, options, selectors, properties = rule
+ if not interpolated:
+ # First time only, interpolate the final selectors with whatever variables were inherited:
+ interpolated = True
+ new_selectors = self.use_vars(_selectors, context, options)
+ new_selectors = self.do_math(new_selectors)
+ new_selectors = self.normalize_selectors(new_selectors)
+ if ' {' in codestr:
+ nested = True
+ if nested:
+ # remove old selector:
+ del self.parts[_selectors]
+ # manage children or expand children:
+ self.manage_children(new_selectors, rules)
+ # maybe there are some children still left...
+ children_left = True
+ else:
+ # No more children, interpolate the plain CSS rule:
+ for rule in rules:
+ fileid, position, codestr, deps, context, options, selectors, properties = rule
+ options = dict(filter(lambda x: not x[0].startswith('@extend '), options.items())) # clean options
+ self.process_properties(codestr, context, options)
+ rule[CODESTR] = self.use_vars(codestr, context, options)
+ for o in options:
+ if o.startswith('@extend '):
+ parents = o[8:].replace(',', '&')
+ if ' extends ' in new_selectors:
+ new_selectors += '&'+parents
+ else:
+ new_selectors += ' extends ' + parents
+ new_selectors = self.normalize_selectors(new_selectors)
+ # rename selectors (if interpolated)
+ if new_selectors != _selectors:
+ del self.parts[_selectors]
+ self.parts.setdefault(new_selectors, [])
+ self.parts[new_selectors].extend(rules)
+
+ def manage_children(self, _selectors, rules):
+ _selectors, _, _parents = _selectors.partition(' extends ')
+ p_selectors = _selectors.split(',')
+ construct = self.construct
+ if _parents:
+ construct += ' extends ' + _parents
+
+ for rule in rules:
+ fileid, position, codestr, deps, context, options, selectors, properties = rule
+
+ # Check if the block has nested blocks and work it out:
+ if ' {' not in codestr:
+ self.process_properties(codestr, context, options)
+ c_codestr = self.use_vars(codestr, context, options)
+ codestr = construct + ' {' + codestr + '}\n'
+ else:
+ codestr = construct + ' {}\n' + codestr
+
+ my_rules = []
+ def _create_children(c_selectors, c_codestr):
+ better_selectors = set()
+ c_selectors, _, c_parents = c_selectors.partition(' extends ')
+ c_selectors = c_selectors.split(',')
+ for c_selector in c_selectors:
+ for p_selector in p_selectors:
+ if c_selector == self.construct:
+ better_selectors.add(p_selector)
+ elif c_selector[0] == '&': # Parent References (SASS)
+ better_selectors.add(p_selector + c_selector[1:])
+ else:
+ better_selectors.add(p_selector + ' ' + c_selector)
+ better_selectors = ','.join(sorted(better_selectors))
+ if c_parents:
+ better_selectors += ' extends ' + c_parents
+
+ rule[POSITION] = None # Disable old rule
+ _rule = [ fileid, position, c_codestr, deps, None, None, None, None ]
+ my_rules.append(_rule)
+ self.rules.append(_rule)
+ self.parts.setdefault(better_selectors, [])
+ self.parts[better_selectors].append(_rule)
+
+ for c_selectors, c_codestr, lose in self.locate_blocks(codestr):
+ if lose is not None or c_selectors[-1] == ':':
+ # This is either a raw rule or a nested property...
+ if lose is None:
+ # ...it was a nested property or varialble, treat as raw
+ lose = c_selectors + '{' + c_codestr + '}'
+ c_selectors = construct
+ c_codestr = lose.strip()
+ if c_codestr:
+ self.process_properties(c_codestr, context, options)
+ c_codestr = self.use_vars(c_codestr, context, options)
+ _create_children(c_selectors, c_codestr)
+ else:
+ _create_children(c_selectors, c_codestr)
+
+ for rule in my_rules:
+ rule[CONTEXT] = context.copy()
+ rule[OPTIONS] = options.copy()
+
+
+__doc__ += """
+>>> css = xCSS()
+
+VARIABLES
+--------------------------------------------------------------------------------
+http://xcss.antpaw.org/docs/syntax/variables
+
+>>> print css.compile('''
+... vars {
+... $path = ../img/tmpl1/png;
+... $color1 = #FF00FF;
+... $border = border-top: 1px solid $color1;
+... }
+... .selector {
+... background-image: url($path/head_bg.png);
+... background-color: $color1;
+... $border;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector {
+ background-image: url(../img/tmpl1/png/head_bg.png);
+ background-color: #f0f;
+ border-top: 1px solid #f0f;
+}
+
+
+NESTING CHILD OBJECTS
+--------------------------------------------------------------------------------
+http://xcss.antpaw.org/docs/syntax/children
+
+>>> print css.compile('''
+... .selector {
+... a {
+... display: block;
+... }
+... strong {
+... color: blue;
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector a {
+ display: block;
+}
+.selector strong {
+ color: #00f;
+}
+
+
+>>> print css.compile('''
+... .selector {
+... self {
+... margin: 20px;
+... }
+... a {
+... display: block;
+... }
+... strong {
+... color: blue;
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector {
+ margin: 20px;
+}
+.selector a {
+ display: block;
+}
+.selector strong {
+ color: #00f;
+}
+
+
+>>> print css.compile('''
+... .selector {
+... self {
+... margin: 20px;
+... }
+... a {
+... display: block;
+... }
+... dl {
+... dt {
+... color: red;
+... }
+... dd {
+... self {
+... color: gray;
+... }
+... span {
+... text-decoration: underline;
+... }
+... }
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector {
+ margin: 20px;
+}
+.selector a {
+ display: block;
+}
+.selector dl dt {
+ color: red;
+}
+.selector dl dd {
+ color: gray;
+}
+.selector dl dd span {
+ text-decoration: underline;
+}
+
+
+EXTENDING OBJECTS
+--------------------------------------------------------------------------------
+http://xcss.antpaw.org/docs/syntax/extends
+
+>>> print css.compile('''
+...
+... .basicClass {
+... padding: 20px;
+... background-color: #FF0000;
+... }
+... .specialClass extends .basicClass {}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.basicClass,
+.specialClass {
+ padding: 20px;
+ background-color: red;
+}
+
+
+>>> print css.compile('''
+... .basicClass {
+... padding: 20px;
+... background-color: #FF0000;
+... }
+... .specialClass extends .basicClass {
+... padding: 10px;
+... font-size: 14px;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.basicClass,
+.specialClass {
+ padding: 20px;
+ background-color: red;
+}
+.specialClass {
+ padding: 10px;
+ font-size: 14px;
+}
+
+>>> print css.compile('''
+... .specialClass extends .basicClass {
+... padding: 10px;
+... font-size: 14px;
+... }
+... .specialLink extends .basicClass a {}
+... .basicClass {
+... self {
+... padding: 20px;
+... background-color: #FF0000;
+... }
+... a {
+... text-decoration: none;
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.basicClass,
+.specialClass {
+ padding: 20px;
+ background-color: red;
+}
+.basicClass a,
+.specialClass a,
+.specialLink {
+ text-decoration: none;
+}
+.specialClass {
+ padding: 10px;
+ font-size: 14px;
+}
+
+>>> print css.compile('''
+... .basicList {
+... li {
+... padding: 5px 10px;
+... border-bottom: 1px solid #000000;
+... }
+... dd {
+... margin: 4px;
+... }
+... span {
+... display: inline-block;
+... }
+... }
+... .roundBox {
+... some: props;
+... }
+... .specialClass extends .basicList & .roundBox {}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.basicList li,
+.specialClass li {
+ padding: 5px 10px;
+ border-bottom: 1px solid #000;
+}
+.basicList dd,
+.specialClass dd {
+ margin: 4px;
+}
+.basicList span,
+.specialClass span {
+ display: inline-block;
+}
+.roundBox,
+.specialClass {
+ some: props;
+}
+
+>>> print css.compile('''.basicList {
+... li {
+... padding: 5px 10px;
+... border-bottom: 1px solid #000000;
+... }
+... dd {
+... margin: 4px;
+... }
+... span {
+... display: inline-block;
+... }
+... }
+... .specialClass {
+... dt extends .basicList li {}
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.basicList li,
+.specialClass dt {
+ padding: 5px 10px;
+ border-bottom: 1px solid #000;
+}
+.basicList dd {
+ margin: 4px;
+}
+.basicList span {
+ display: inline-block;
+}
+
+MATH OPERATIONS
+--------------------------------------------------------------------------------
+http://xcss.antpaw.org/docs/syntax/math
+
+>>> print css.compile('''
+... vars {
+... $color = #FFF555;
+... }
+... .selector {
+... padding: [5px * 2];
+... color: [#ccc * 2];
+... // lets assume $color is '#FFF555'
+... background-color: [$color - #222 + #101010];
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector {
+ padding: 10px;
+ color: #fff;
+ background-color: #ede343;
+}
+
+
+>>> print css.compile('''
+... .selector {
+... padding: [(5px - 3) * (5px - 3)];
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector {
+ padding: 4px;
+}
+
+
+>>> print css.compile('''
+... .selector {
+... padding: [5em - 3em + 5px]px;
+... margin: [20 - 10] [30% - 10];
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.selector {
+ padding: 31px;
+ margin: 10 20%;
+}
+
+
+SASS NESTING COMPATIBILITY
+--------------------------------------------------------------------------------
+http://sass-lang.com/tutorial.html
+
+>>> print css.compile('''
+... /* style.scss */
+... #navbar {
+... width: 80%;
+... height: 23px;
+...
+... ul { list-style-type: none; }
+... li {
+... float: left;
+... a { font-weight: bold; }
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+#navbar {
+ width: 80%;
+ height: 23px;
+}
+#navbar ul {
+ list-style-type: none;
+}
+#navbar li {
+ float: left;
+}
+#navbar li a {
+ font-weight: bold;
+}
+
+
+>>> print css.compile('''
+... /* style.scss */
+... .fakeshadow {
+... border: {
+... style: solid;
+... left: {
+... width: 4px;
+... color: #888;
+... }
+... right: {
+... width: 2px;
+... color: #ccc;
+... }
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.fakeshadow {
+ border-style: solid;
+ border-left-width: 4px;
+ border-left-color: #888;
+ border-right-width: 2px;
+ border-right-color: #ccc;
+}
+
+
+>>> print css.compile('''
+... /* style.scss */
+... a {
+... color: #ce4dd6;
+... &:hover { color: #ffb3ff; }
+... &:visited { color: #c458cb; }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+a {
+ color: #ce4dd6;
+}
+a:hover {
+ color: #ffb3ff;
+}
+a:visited {
+ color: #c458cb;
+}
+
+
+SASS VARIABLES COMPATIBILITY
+--------------------------------------------------------------------------------
+http://sass-lang.com/tutorial.html
+
+>>> print css.compile('''
+... /* style.scss */
+... $main-color: #ce4dd6;
+... $style: solid;
+...
+... #navbar {
+... border-bottom: {
+... color: $main-color;
+... style: $style;
+... }
+... }
+...
+... a {
+... color: $main-color;
+... &:hover { border-bottom: $style 1px; }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+#navbar {
+ border-bottom-color: #ce4dd6;
+ border-bottom-style: solid;
+}
+a {
+ color: #ce4dd6;
+}
+a:hover {
+ border-bottom: solid 1px;
+}
+
+
+SASS INTERPOLATION COMPATIBILITY
+--------------------------------------------------------------------------------
+http://sass-lang.com/tutorial.html
+
+>>> print css.compile('''
+... /* style.scss */
+... $side: top;
+... $radius: 10px;
+...
+... .rounded-#{$side} {
+... border-#{$side}-radius: $radius;
+... -moz-border-radius-#{$side}: $radius;
+... -webkit-border-#{$side}-radius: $radius;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.rounded-top {
+ border-top-radius: 10px;
+ -moz-border-radius-top: 10px;
+ -webkit-border-top-radius: 10px;
+}
+
+
+SASS MIXINS COMPATIBILITY
+--------------------------------------------------------------------------------
+http://sass-lang.com/tutorial.html
+
+>>> print css.compile('''
+... /* style.scss */
+...
+... @mixin rounded-top {
+... $side: top;
+... $radius: 10px;
+...
+... border-#{$side}-radius: $radius;
+... -moz-border-radius-#{$side}: $radius;
+... -webkit-border-#{$side}-radius: $radius;
+... }
+...
+... #navbar li { @include rounded-top; }
+... #footer { @include rounded-top; }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+#navbar li {
+ border-top-radius: 10px;
+ -moz-border-radius-top: 10px;
+ -webkit-border-top-radius: 10px;
+}
+#footer {
+ border-top-radius: 10px;
+ -moz-border-radius-top: 10px;
+ -webkit-border-top-radius: 10px;
+}
+
+
+>>> print css.compile('''
+... /* style.scss */
+...
+... @mixin rounded($side, $radius: 10px) {
+... border-#{$side}-radius: $radius;
+... -moz-border-radius-#{$side}: $radius;
+... -webkit-border-#{$side}-radius: $radius;
+... }
+...
+... #navbar li { @include rounded(top); }
+... #footer { @include rounded(top, 5px); }
+... #sidebar { @include rounded(left, 8px); }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+#navbar li {
+ border-(top)-radius: 10px;
+ -moz-border-radius-(top): 10px;
+ -webkit-border-(top)-radius: 10px;
+}
+#footer {
+ border-(top)-radius: 5px;
+ -moz-border-radius-(top): 5px;
+ -webkit-border-(top)-radius: 5px;
+}
+#sidebar {
+ border-(left)-radius: 8px;
+ -moz-border-radius-(left): 8px;
+ -webkit-border-(left)-radius: 8px;
+}
+
+
+SASS EXTEND COMPATIBILITY
+--------------------------------------------------------------------------------
+http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#extend
+
+>>> from xcss import *
+>>> css = xCSS()
+>>>
+>>>
+>>> print css.compile('''
+... .error {
+... border: 1px #f00;
+... background-color: #fdd;
+... }
+... .error.intrusion {
+... background-image: url("/image/hacked.png");
+... }
+... .seriousError {
+... @extend .error;
+... border-width: 3px;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.error,
+.seriousError {
+ border: 1px red;
+ background-color: #fdd;
+}
+.error.intrusion,
+.seriousError.intrusion {
+ background-image: url("/image/hacked.png");
+}
+.seriousError {
+ border-width: 3px;
+}
+
+
+Multiple Extends
+>>> print css.compile('''
+... .error {
+... border: 1px #f00;
+... background-color: #fdd;
+... }
+... .attention {
+... font-size: 3em;
+... background-color: #ff0;
+... }
+... .seriousError {
+... @extend .error;
+... @extend .attention;
+... border-width: 3px;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.error,
+.seriousError {
+ border: 1px red;
+ background-color: #fdd;
+}
+.attention,
+.seriousError {
+ font-size: 3em;
+ background-color: #ff0;
+}
+.seriousError {
+ border-width: 3px;
+}
+
+
+
+
+FROM THE FORUM
+--------------------------------------------------------------------------------
+
+http://groups.google.com/group/xcss/browse_thread/thread/6989243973938362#
+>>> print css.compile('''
+... body {
+... _width: expression(document.body.clientWidth > 1440? "1440px" : "auto");
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+body {
+ _width: expression(document.body.clientWidth > 1440? "1440px" : "auto");
+}
+
+
+http://groups.google.com/group/xcss/browse_thread/thread/2d27ddec3c15c385#
+>>> print css.compile('''
+... vars {
+... $ie6 = *html;
+... $ie7 = *:first-child+html;
+... }
+... $ie6 {
+... .a { color:white; }
+... .b { color:black; }
+... }
+... $ie7 {
+... .a { color:white; }
+... .b { color:black; }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+*html .a {
+ color: #fff;
+}
+*html .b {
+ color: #000;
+}
+*:first-child+html .a {
+ color: #fff;
+}
+*:first-child+html .b {
+ color: #000;
+}
+
+
+http://groups.google.com/group/xcss/browse_thread/thread/04faafb4ef178984#
+>>> print css.compile('''
+... .basicClass {
+... padding: 20px;
+... background-color: #FF0000;
+... }
+... .specialClass extends .basicClass {
+... padding: 10px;
+... font-size: 14px;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.basicClass,
+.specialClass {
+ padding: 20px;
+ background-color: red;
+}
+.specialClass {
+ padding: 10px;
+ font-size: 14px;
+}
+
+
+TESTS
+--------------------------------------------------------------------------------
+
+>>> print css.compile('''
+... .coloredClass {
+... $mycolor: green;
+... padding: 20px;
+... background-color: $mycolor;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+ .coloredClass {
+ padding: 20px;
+ background-color: green;
+ }
+
+
+>>> css.xcss_files = {}
+>>> css.xcss_files['first.css'] = '''
+... .specialClass extends .basicClass {
+... padding: 10px;
+... font-size: 14px;
+... }
+... '''
+>>> css.xcss_files['second.css'] = '''
+... .basicClass {
+... padding: 20px;
+... background-color: #FF0000;
+... }
+... '''
+>>> print css.compile() #doctest: +NORMALIZE_WHITESPACE
+/* Generated frm: second.css */
+.basicClass,
+.specialClass {
+ padding: 20px;
+ background-color: red;
+}
+/* Generated frm: first.css */
+.specialClass {
+ padding: 10px;
+ font-size: 14px;
+}
+
+
+>>> print css.compile('''
+... .mod {
+... margin: 10px;
+... }
+... .mod h1 {
+... font-size: 40px;
+... }
+... .cleanBox h1 extends .mod {
+... font-size: 60px;
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.cleanBox h1,
+.mod {
+ margin: 10px;
+}
+.cleanBox h1,
+.mod h1 {
+ font-size: 40px;
+}
+.cleanBox h1 {
+ font-size: 60px;
+}
+
+
+ERRORS
+--------------------------------------------------------------------------------
+
+http://groups.google.com/group/xcss/browse_thread/thread/5f4f3af046883c3b#
+>>> print css.compile('''
+... .some-selector { some:prop; }
+... .some-selector-more { some:proop; }
+... .parent {
+... self extends .some-selector {
+... height: auto
+... }
+... .children {
+... self extends .some-selector-more {
+... height: autoo
+... }
+... }
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.parent,
+.some-selector {
+ some: prop;
+}
+.parent .children,
+.some-selector-more {
+ some: proop;
+}
+.parent {
+ height: auto;
+}
+.parent .children {
+ height: autoo;
+}
+
+
+http://groups.google.com/group/xcss/browse_thread/thread/540f8ad0771c053b#
+>>> print css.compile('''
+... .noticeBox {
+... self {
+... background-color:red;
+... }
+... span, p {
+... some: props
+... }
+... }
+... .errorBox extends .noticeBox {}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.errorBox,
+.noticeBox {
+ background-color: red;
+}
+.errorBox p,
+.errorBox span,
+.noticeBox p,
+.noticeBox span {
+ some: props;
+}
+
+
+http://groups.google.com/group/xcss/browse_thread/thread/b5757c24586c1519#
+>>> print css.compile('''
+... .mod {
+... self {
+... margin: 10px;
+... }
+... h1 {
+... font-size:40px;
+... }
+... }
+... .cleanBox extends .mod {
+... h1 {
+... font-size:60px;
+... }
+... }
+... .cleanBoxExtended extends .cleanBox {}
+... .articleBox extends .cleanBox {}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.articleBox,
+.cleanBox,
+.cleanBoxExtended,
+.mod {
+ margin: 10px;
+}
+.articleBox h1,
+.cleanBox h1,
+.cleanBoxExtended h1,
+.mod h1 {
+ font-size: 40px;
+}
+.articleBox h1,
+.cleanBox h1,
+.cleanBoxExtended h1 {
+ font-size: 60px;
+}
+
+"""
+"""
+
+
+>>> print css.compile('''
+... .hoverlink {@extend a:hover}
+... a:hover {text-decoration: underline}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.hoverlink,
+a:hover {
+ text-decoration: underline;
+}
+
+>>> print css.compile('''
+... .hoverlink {@extend a:hover}
+... .comment a.user:hover {font-weight: bold}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+.comment a.user:hover,
+.comment .hoverlink.user {
+ font-weight: bold;
+}
+
+
+>>> print css.compile('''
+... #fake-links .link {@extend a}
+...
+... a {
+... color: blue;
+... &:hover {text-decoration: underline}
+... }
+... ''') #doctest: +NORMALIZE_WHITESPACE
+#fake-links .linka,
+a {
+ color: #00f;
+}
+#fake-links .link:hover ,
+a:hover {
+ text-decoration: underline;
+}
+
+
+>>> print css.compile('''
+... #admin .tabbar a {font-weight: bold}
+... #demo .overview .fakelink {@extend a}
+... ''') #doctest: +NORMALIZE_WHITESPACE
+#admin .tabbar a,
+#admin .tabbar #demo .overview .fakelink,
+#demo .overview #admin .tabbar .fakelink {
+ font-weight: bold;
+}
+
+--------------------------------------------------------------------------------
+"""
+
+if __name__ == "__main__":
+ import getopt
+ # parse options for module imports
+ opts, args = getopt.getopt(sys.argv[1:], 't')
+ opts = dict(opts)
+ if '-t' in opts:
+ import doctest
+ doctest.testmod()
+ else:
+ css = xCSS()
+ sys.stdout.write(css.compile(sys.stdin.read()))