1"""Utilities for working with gradients.  Inspired by Compass, but not quite
2the same.
3"""
4from __future__ import absolute_import
5from __future__ import print_function
6from __future__ import unicode_literals
7
8import base64
9import logging
10
11import six
12
13from . import CompassExtension
14from .helpers import opposite_position, position
15from scss.types import Color, List, Number, String
16from scss.util import escape, split_params, to_float, to_str
17
18log = logging.getLogger(__name__)
19ns = CompassExtension.namespace
20
21
22def _is_color(value):
23    # currentColor is not a Sass color value, but /is/ a CSS color value
24    return isinstance(value, Color) or value == String('currentColor')
25
26
27def __color_stops(percentages, *args):
28    if len(args) == 1:
29        if isinstance(args[0], (list, tuple, List)):
30            return list(args[0])
31        elif isinstance(args[0], (String, six.string_types)):
32            color_stops = []
33            colors = split_params(getattr(args[0], 'value', args[0]))
34            for color in colors:
35                color = color.strip()
36                if color.startswith('color-stop('):
37                    s, c = split_params(color[11:].rstrip(')'))
38                    s = s.strip()
39                    c = c.strip()
40                else:
41                    c, s = color.split()
42                color_stops.append((to_float(s), c))
43            return color_stops
44
45    colors = []
46    stops = []
47    prev_color = False
48    for c in args:
49        for c in List.from_maybe(c):
50            if _is_color(c):
51                if prev_color:
52                    stops.append(None)
53                colors.append(c)
54                prev_color = True
55            elif isinstance(c, Number):
56                stops.append(c)
57                prev_color = False
58
59    if prev_color:
60        stops.append(None)
61    stops = stops[:len(colors)]
62    if stops[0] is None:
63        stops[0] = Number(0, '%')
64    if stops[-1] is None:
65        stops[-1] = Number(100, '%')
66
67    maxable_stops = [s for s in stops if s and not s.is_simple_unit('%')]
68    if maxable_stops:
69        max_stops = max(maxable_stops)
70    else:
71        max_stops = None
72
73    stops = [_s / max_stops if _s and not _s.is_simple_unit('%') else _s for _s in stops]
74
75    init = 0
76    start = None
77    for i, s in enumerate(stops + [1.0]):
78        if s is None:
79            if start is None:
80                start = i
81            end = i
82        else:
83            final = s
84            if start is not None:
85                stride = (final - init) / Number(end - start + 1 + (1 if i < len(stops) else 0))
86                for j in range(start, end + 1):
87                    stops[j] = init + stride * Number(j - start + 1)
88            init = final
89            start = None
90
91    if not max_stops or percentages:
92        pass
93    else:
94        stops = [s if s.is_simple_unit('%') else s * max_stops for s in stops]
95
96    return List(List(pair) for pair in zip(stops, colors))
97
98
99def _render_standard_color_stops(color_stops):
100    pairs = []
101    for i, (stop, color) in enumerate(color_stops):
102        if ((i == 0 and stop == Number(0, '%')) or
103                (i == len(color_stops) - 1 and stop == Number(100, '%'))):
104            pairs.append(color)
105        else:
106            pairs.append(List([color, stop], use_comma=False))
107
108    return List(pairs, use_comma=True)
109
110
111@ns.declare
112def grad_color_stops(*args):
113    args = List.from_maybe_starargs(args)
114    color_stops = __color_stops(True, *args)
115    ret = ', '.join(['color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops])
116    return String.unquoted(ret)
117
118
119def __grad_end_position(radial, color_stops):
120    return __grad_position(-1, 100, radial, color_stops)
121
122
123@ns.declare
124def grad_point(*p):
125    pos = set()
126    hrz = vrt = Number(0.5, '%')
127    for _p in p:
128        pos.update(String.unquoted(_p).value.split())
129    if 'left' in pos:
130        hrz = Number(0, '%')
131    elif 'right' in pos:
132        hrz = Number(1, '%')
133    if 'top' in pos:
134        vrt = Number(0, '%')
135    elif 'bottom' in pos:
136        vrt = Number(1, '%')
137    return List([v for v in (hrz, vrt) if v is not None])
138
139
140def __grad_position(index, default, radial, color_stops):
141    try:
142        stops = Number(color_stops[index][0])
143        if radial and not stops.is_simple_unit('px') and (index == 0 or index == -1 or index == len(color_stops) - 1):
144            log.warn("Webkit only supports pixels for the start and end stops for radial gradients. Got %s", stops)
145    except IndexError:
146        stops = Number(default)
147    return stops
148
149
150@ns.declare
151def grad_end_position(*color_stops):
152    color_stops = __color_stops(False, *color_stops)
153    return Number(__grad_end_position(False, color_stops))
154
155
156@ns.declare
157def color_stops(*args):
158    args = List.from_maybe_starargs(args)
159    color_stops = __color_stops(False, *args)
160    ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops])
161    return String.unquoted(ret)
162
163
164@ns.declare
165def color_stops_in_percentages(*args):
166    args = List.from_maybe_starargs(args)
167    color_stops = __color_stops(True, *args)
168    ret = ', '.join(['%s %s' % (c.render(), s.render()) for s, c in color_stops])
169    return String.unquoted(ret)
170
171
172def _get_gradient_position_and_angle(args):
173    for arg in args:
174        ret = None
175        skip = False
176        for a in arg:
177            if _is_color(a):
178                skip = True
179                break
180            elif isinstance(a, Number):
181                ret = arg
182        if skip:
183            continue
184        if ret is not None:
185            return ret
186        for seek in (
187            'center',
188            'top', 'bottom',
189            'left', 'right',
190        ):
191            if String(seek) in arg:
192                return arg
193    return None
194
195
196def _get_gradient_shape_and_size(args):
197    for arg in args:
198        for seek in (
199            'circle', 'ellipse',
200            'closest-side', 'closest-corner',
201            'farthest-side', 'farthest-corner',
202            'contain', 'cover',
203        ):
204            if String(seek) in arg:
205                return arg
206    return None
207
208
209def _get_gradient_color_stops(args):
210    color_stops = []
211    for arg in args:
212        for a in List.from_maybe(arg):
213            if _is_color(a):
214                color_stops.append(arg)
215                break
216    return color_stops or None
217
218
219# TODO these functions need to be
220# 1. well-defined
221# 2. guaranteed to never wreck css3 syntax
222# 3. updated to whatever current compass does
223# 4. fixed to use a custom type instead of monkeypatching
224
225
226@ns.declare
227def radial_gradient(*args):
228    args = List.from_maybe_starargs(args)
229
230    try:
231        # Do a rough check for standard syntax first -- `shape at position`
232        at_position = list(args[0]).index(String('at'))
233    except (IndexError, ValueError):
234        shape_and_size = _get_gradient_shape_and_size(args)
235        position_and_angle = _get_gradient_position_and_angle(args)
236    else:
237        shape_and_size = List.maybe_new(args[0][:at_position])
238        position_and_angle = List.maybe_new(args[0][at_position + 1:])
239
240    color_stops = _get_gradient_color_stops(args)
241    if color_stops is None:
242        raise Exception('No color stops provided to radial-gradient function')
243    color_stops = __color_stops(False, *color_stops)
244
245    if position_and_angle:
246        rendered_position = position(position_and_angle)
247    else:
248        rendered_position = None
249    rendered_color_stops = _render_standard_color_stops(color_stops)
250
251    args = []
252    if shape_and_size and rendered_position:
253        args.append(List([shape_and_size, String.unquoted('at'), rendered_position], use_comma=False))
254    elif rendered_position:
255        args.append(rendered_position)
256    elif shape_and_size:
257        args.append(shape_and_size)
258    args.extend(rendered_color_stops)
259
260    legacy_args = []
261    if rendered_position:
262        legacy_args.append(rendered_position)
263    if shape_and_size:
264        legacy_args.append(shape_and_size)
265    legacy_args.extend(rendered_color_stops)
266
267    ret = String.unquoted(
268        'radial-gradient(' + ', '.join(a.render() for a in args) + ')')
269
270    legacy_ret = 'radial-gradient(' + ', '.join(a.render() for a in legacy_args) + ')'
271
272    def to__css2():
273        return String.unquoted('')
274    ret.to__css2 = to__css2
275
276    def to__moz():
277        return String.unquoted('-moz-' + legacy_ret)
278    ret.to__moz = to__moz
279
280    def to__pie():
281        log.warn("PIE does not support radial-gradient.")
282        return String.unquoted('-pie-radial-gradient(unsupported)')
283    ret.to__pie = to__pie
284
285    def to__webkit():
286        return String.unquoted('-webkit-' + legacy_ret)
287    ret.to__webkit = to__webkit
288
289    def to__owg():
290        args = [
291            'radial',
292            grad_point(*position_and_angle) if position_and_angle is not None else 'center',
293            '0',
294            grad_point(*position_and_angle) if position_and_angle is not None else 'center',
295            __grad_end_position(True, color_stops),
296        ]
297        args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops)
298        ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
299        return String.unquoted(ret)
300    ret.to__owg = to__owg
301
302    def to__svg():
303        return radial_svg_gradient(*(list(color_stops) + list(position_and_angle or [String('center')])))
304    ret.to__svg = to__svg
305
306    return ret
307
308
309@ns.declare
310def linear_gradient(*args):
311    args = List.from_maybe_starargs(args)
312
313    position_and_angle = _get_gradient_position_and_angle(args)
314    color_stops = _get_gradient_color_stops(args)
315    if color_stops is None:
316        raise Exception('No color stops provided to linear-gradient function')
317    color_stops = __color_stops(False, *color_stops)
318
319    args = [
320        position(position_and_angle) if position_and_angle is not None else None,
321    ]
322    args.extend(_render_standard_color_stops(color_stops))
323
324    to__s = 'linear-gradient(' + ', '.join(to_str(a) for a in args or [] if a is not None) + ')'
325    ret = String.unquoted(to__s)
326
327    def to__css2():
328        return String.unquoted('')
329    ret.to__css2 = to__css2
330
331    def to__moz():
332        return String.unquoted('-moz-' + to__s)
333    ret.to__moz = to__moz
334
335    def to__pie():
336        return String.unquoted('-pie-' + to__s)
337    ret.to__pie = to__pie
338
339    def to__ms():
340        return String.unquoted('-ms-' + to__s)
341    ret.to__ms = to__ms
342
343    def to__o():
344        return String.unquoted('-o-' + to__s)
345    ret.to__o = to__o
346
347    def to__webkit():
348        return String.unquoted('-webkit-' + to__s)
349    ret.to__webkit = to__webkit
350
351    def to__owg():
352        args = [
353            'linear',
354            position(position_and_angle or None),
355            opposite_position(position_and_angle or None),
356        ]
357        args.extend('color-stop(%s, %s)' % (s.render(), c.render()) for s, c in color_stops)
358        ret = '-webkit-gradient(' + ', '.join(to_str(a) for a in args if a is not None) + ')'
359        return String.unquoted(ret)
360    ret.to__owg = to__owg
361
362    def to__svg():
363        return linear_svg_gradient(color_stops, position_and_angle or 'top')
364    ret.to__svg = to__svg
365
366    return ret
367
368
369@ns.declare
370def radial_svg_gradient(*args):
371    args = List.from_maybe_starargs(args)
372    color_stops = args
373    center = None
374    if isinstance(args[-1], (String, Number)):
375        center = args[-1]
376        color_stops = args[:-1]
377    color_stops = __color_stops(False, *color_stops)
378    cx, cy = grad_point(center)
379    r = __grad_end_position(True, color_stops)
380    svg = __radial_svg(color_stops, cx, cy, r)
381    url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
382    inline = 'url("%s")' % escape(url)
383    return String.unquoted(inline)
384
385
386@ns.declare
387def linear_svg_gradient(*args):
388    args = List.from_maybe_starargs(args)
389    color_stops = args
390    start = None
391    if isinstance(args[-1], (String, Number)):
392        start = args[-1]
393        color_stops = args[:-1]
394    color_stops = __color_stops(False, *color_stops)
395    x1, y1 = grad_point(start)
396    x2, y2 = grad_point(opposite_position(start))
397    svg = _linear_svg(color_stops, x1, y1, x2, y2)
398    url = 'data:' + 'image/svg+xml' + ';base64,' + base64.b64encode(svg)
399    inline = 'url("%s")' % escape(url)
400    return String.unquoted(inline)
401
402
403def __color_stops_svg(color_stops):
404    ret = ''.join('<stop offset="%s" stop-color="%s"/>' % (to_str(s), c) for s, c in color_stops)
405    return ret
406
407
408def __svg_template(gradient):
409    ret = '<?xml version="1.0" encoding="utf-8"?>\
410<svg version="1.1" xmlns="http://www.w3.org/2000/svg">\
411<defs>%s</defs>\
412<rect x="0" y="0" width="100%%" height="100%%" fill="url(#grad)" />\
413</svg>' % gradient
414    return ret
415
416
417def _linear_svg(color_stops, x1, y1, x2, y2):
418    gradient = '<linearGradient id="grad" x1="%s" y1="%s" x2="%s" y2="%s">%s</linearGradient>' % (
419        to_str(Number(x1)),
420        to_str(Number(y1)),
421        to_str(Number(x2)),
422        to_str(Number(y2)),
423        __color_stops_svg(color_stops)
424    )
425    return __svg_template(gradient)
426
427
428def __radial_svg(color_stops, cx, cy, r):
429    gradient = '<radialGradient id="grad" gradientUnits="userSpaceOnUse" cx="%s" cy="%s" r="%s">%s</radialGradient>' % (
430        to_str(Number(cx)),
431        to_str(Number(cy)),
432        to_str(Number(r)),
433        __color_stops_svg(color_stops)
434    )
435    return __svg_template(gradient)
436