summaryrefslogtreecommitdiff
path: root/scss/grammar/expression.g
blob: 17eb810d9cbe5b25ad5a751b4cca34746a52d812 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
"""Grammar for parsing Sass expressions."""
# This is a GENERATED FILE -- DO NOT EDIT DIRECTLY!
# Edit scss/grammar/expression.g, then run:
#
#     python2 yapps2.py scss/grammar/expression.g
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import division

import operator
import re

from scss.ast import Parentheses
from scss.ast import UnaryOp
from scss.ast import BinaryOp
from scss.ast import AnyOp
from scss.ast import AllOp
from scss.ast import NotOp
from scss.ast import CallOp
from scss.ast import Interpolation
from scss.ast import Literal
from scss.ast import Variable
from scss.ast import ListLiteral
from scss.ast import MapLiteral
from scss.ast import ArgspecLiteral
from scss.cssdefs import unescape
from scss.types import Color
from scss.types import Function
from scss.types import Number
from scss.types import String
from scss.types import Url
from scss.util import dequote

from scss.grammar import Parser
from scss.grammar import Scanner


%%
parser SassExpression:
    ignore: "[ \r\t\n]+"
    token LPAR: "\\(|\\["
    token RPAR: "\\)|\\]"
    token END: "$"
    token MUL: "[*]"
    token DIV: "/"
    token ADD: "[+]"
    token SUB: "-\s"
    token SIGN: "-(?![a-zA-Z_])"
    token AND: "(?<![-\w])and(?![-\w])"
    token OR: "(?<![-\w])or(?![-\w])"
    token NOT: "(?<![-\w])not(?![-\w])"
    token NE: "!="
    token INV: "!"
    token EQ: "=="
    token LE: "<="
    token GE: ">="
    token LT: "<"
    token GT: ">"
    token DOTDOTDOT: '[.]{3}'
    token SINGLE_QUOTE: "'"
    token DOUBLE_QUOTE: '"'
    # Don't allow quotes or # unless they're escaped (or the # is alone)
    token SINGLE_STRING_GUTS: '([^\'\\\\#]|[\\\\].|#(?![{]))*'
    token DOUBLE_STRING_GUTS: "([^\"\\\\#]|[\\\\].|#(?![{]))*"
    token STR: "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"
    token QSTR: '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"'
    token UNITS: "(?<!\s)(?:[a-zA-Z]+|%)(?![-\w])"
    token NUM: "(?:\d+(?:\.\d*)?|\.\d+)"
    token COLOR: "#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})(?![a-fA-F0-9])"
    token KWVAR: "\$[-a-zA-Z0-9_]+(?=\s*:)"
    token SLURPYVAR: "\$[-a-zA-Z0-9_]+(?=[.][.][.])"
    token VAR: "\$[-a-zA-Z0-9_]+"
    token FNCT: "[-a-zA-Z_][-a-zA-Z0-9_]*(?=\()"
    # TODO Ruby is a bit more flexible here, for example allowing 1#{2}px
    token BAREWORD: "[-a-zA-Z_][-a-zA-Z0-9_]*"
    token BANG_IMPORTANT: "!important"

    token INTERP_START: "#[{]"
    token INTERP_END: "[}]"
    token INTERP_ANYTHING: "([^#]|#(?![{]))*"
    token INTERP_NO_PARENS: "([^#()]|#(?![{]))*"
    # http://dev.w3.org/csswg/css-syntax-3/#consume-a-url-token0
    # Bare URLs may not contain quotes, parentheses, or unprintables.  Quoted
    # URLs may, of course, contain whatever they like.
    # TODO reify escapes, for this and for strings
    # FIXME: Also, URLs may not contain $ as it breaks urls with variables?
    token BAREURL: "(?:[\\\\].|[^#$'\"()\\x00-\\x08\\x0b\\x0e-\\x1f\\x7f]|#(?![{]))*"

    # Goals:
    rule goal:          expr_lst END                {{ return expr_lst }}

    rule goal_argspec:  argspec END                 {{ return argspec }}

    # Arguments:
    # TODO should support multiple slurpies, and enforce (probably not in the
    # parser) that positional args come first
    rule argspec:
        [
            argspec_items           {{ args, slurpy = argspec_items }}
                                    {{ return ArgspecLiteral(args, slurp=slurpy) }}
        ]                           {{ return ArgspecLiteral([]) }}
        | SLURPYVAR DOTDOTDOT       {{ return ArgspecLiteral([], slurp=SLURPYVAR) }}
        | DOTDOTDOT                 {{ return ArgspecLiteral([], slurp=all) }}

    rule argspec_items:
                                    {{ slurpy = None }}
        argspec_item                {{ args = [argspec_item] }}
        [ "," [
            SLURPYVAR DOTDOTDOT     {{ slurpy = SLURPYVAR }}
            | DOTDOTDOT             {{ slurpy = all }}
            | argspec_items         {{ more_args, slurpy = argspec_items }}
                                    {{ args.extend(more_args) }}
        ] ]                         {{ return args, slurpy }}

    rule argspec_item:
        KWVAR ":" expr_slst         {{ return (Variable(KWVAR), expr_slst) }}
        | expr_slst                 {{ return (None, expr_slst) }}


    # Maps, which necessarily overlap with lists because LL(1):
    rule expr_map_or_list:
        expr_slst                   {{ first = expr_slst }}
        (   # Colon means this is a map
            ":"
            expr_slst               {{ pairs = [(first, expr_slst)] }}
            (
                ","                 {{ map_item = None, None }}
                [ map_item ]        {{ pairs.append(map_item) }}
            )*                      {{ return MapLiteral(pairs) }}
        |   # Comma means this is a comma-delimited list
                                    {{ items = [first]; use_list = False }}
            (
                ","                 {{ use_list = True }}
                expr_slst           {{ items.append(expr_slst) }}
            )*                      {{ return ListLiteral(items) if use_list else items[0] }}
        )

    rule map_item:
        atom ":" expr_slst          {{ return (atom, expr_slst) }}


    # Lists:
    rule expr_lst:
        # TODO a trailing comma makes a list now, i believe
        expr_slst                   {{ v = [expr_slst] }}
        (
            ","
            expr_slst               {{ v.append(expr_slst) }}
        )*                          {{ return ListLiteral(v) if len(v) > 1 else v[0] }}


    # Expressions:
    rule expr_slst:
        or_expr                     {{ v = [or_expr] }}
        (
            or_expr                 {{ v.append(or_expr) }}
        )*                          {{ return ListLiteral(v, comma=False) if len(v) > 1 else v[0] }}

    rule or_expr:
        and_expr                    {{ v = and_expr }}
        (
            OR and_expr             {{ v = AnyOp(v, and_expr) }}
        )*                          {{ return v }}

    rule and_expr:
        not_expr                    {{ v = not_expr }}
        (
            AND not_expr            {{ v = AllOp(v, not_expr) }}
        )*                          {{ return v }}

    rule not_expr:
        comparison                  {{ return comparison }}
        | NOT not_expr              {{ return NotOp(not_expr) }}

    rule comparison:
        a_expr                      {{ v = a_expr }}
        (
            LT a_expr               {{ v = BinaryOp(operator.lt, v, a_expr) }}
            | GT a_expr             {{ v = BinaryOp(operator.gt, v, a_expr) }}
            | LE a_expr             {{ v = BinaryOp(operator.le, v, a_expr) }}
            | GE a_expr             {{ v = BinaryOp(operator.ge, v, a_expr) }}
            | EQ a_expr             {{ v = BinaryOp(operator.eq, v, a_expr) }}
            | NE a_expr             {{ v = BinaryOp(operator.ne, v, a_expr) }}
        )*                          {{ return v }}

    rule a_expr:
        m_expr                      {{ v = m_expr }}
        (
            ADD m_expr              {{ v = BinaryOp(operator.add, v, m_expr) }}
            | SUB m_expr            {{ v = BinaryOp(operator.sub, v, m_expr) }}
        )*                          {{ return v }}

    rule m_expr:
        u_expr                      {{ v = u_expr }}
        (
            MUL u_expr              {{ v = BinaryOp(operator.mul, v, u_expr) }}
            | DIV u_expr            {{ v = BinaryOp(operator.truediv, v, u_expr) }}
        )*                          {{ return v }}

    rule u_expr:
        SIGN u_expr                 {{ return UnaryOp(operator.neg, u_expr) }}
        | ADD u_expr                {{ return UnaryOp(operator.pos, u_expr) }}
        | atom                      {{ return atom }}

    rule atom:
        LPAR (
                                    {{ v = ListLiteral([], comma=False) }}
            | expr_map_or_list      {{ v = expr_map_or_list }}
        ) RPAR                      {{ return Parentheses(v) }}
        # Special functions.  Note that these technically overlap with the
        # regular function rule, which makes this not quite LL -- but they're
        # different tokens so yapps can't tell, and it resolves the conflict by
        # picking the first one.
        # TODO Ruby sass somehow allows a full expression in here too
        | "url" LPAR interpolated_url RPAR
            {{ return interpolated_url }}
        | "expression" LPAR interpolated_function RPAR
            {{ return Interpolation.maybe(interpolated_function, type=Function, function_name='expression') }}
        | FNCT LPAR argspec RPAR    {{ return CallOp(FNCT, argspec) }}
        | BANG_IMPORTANT            {{ return Literal(String(BANG_IMPORTANT, quotes=None)) }}
        | interpolated_bareword     {{ return Interpolation.maybe(interpolated_bareword) }}
        | NUM                       {{ UNITS = None }}
            [ UNITS ]               {{ return Literal(Number(float(NUM), unit=UNITS)) }}
        | interpolated_string       {{ return interpolated_string }}
        | COLOR                     {{ return Literal(Color.from_hex(COLOR, literal=True)) }}
        | VAR                       {{ return Variable(VAR) }}

    # -------------------------------------------------------------------------
    # Interpolation, which is a right mess, because it depends very heavily on
    # context -- what other characters are allowed, and when do we stop?
    # Thankfully these rules all look pretty similar: there's a delimiter, a
    # literal, and some number of interpolations and trailing literals.
    rule interpolation:
        INTERP_START
        expr_lst
        INTERP_END                  {{ return expr_lst }}

    rule interpolated_url:
        # Note: This rule DOES NOT include the url(...) delimiters
        interpolated_bare_url
            {{ return Interpolation.maybe(interpolated_bare_url, type=Url, quotes=None) }}
        | interpolated_string_single
            {{ return Interpolation.maybe(interpolated_string_single, type=Url, quotes="'") }}
        | interpolated_string_double
            {{ return Interpolation.maybe(interpolated_string_double, type=Url, quotes='"') }}

    rule interpolated_bare_url:
        BAREURL                     {{ parts = [BAREURL] }}
        (
            interpolation           {{ parts.append(interpolation) }}
            BAREURL                 {{ parts.append(BAREURL) }}
        )*                          {{ return parts }}

    rule interpolated_string:
        interpolated_string_single
            {{ return Interpolation.maybe(interpolated_string_single, quotes="'") }}
        | interpolated_string_double
            {{ return Interpolation.maybe(interpolated_string_double, quotes='"') }}

    rule interpolated_string_single:
        SINGLE_QUOTE
        SINGLE_STRING_GUTS          {{ parts = [unescape(SINGLE_STRING_GUTS)] }}
        (
            interpolation           {{ parts.append(interpolation) }}
            SINGLE_STRING_GUTS      {{ parts.append(unescape(SINGLE_STRING_GUTS)) }}
        )*
        SINGLE_QUOTE                {{ return parts }}
        
    rule interpolated_string_double:
        DOUBLE_QUOTE
        DOUBLE_STRING_GUTS          {{ parts = [unescape(DOUBLE_STRING_GUTS)] }}
        (
            interpolation           {{ parts.append(interpolation) }}
            DOUBLE_STRING_GUTS      {{ parts.append(unescape(DOUBLE_STRING_GUTS)) }}
        )*
        DOUBLE_QUOTE                {{ return parts }}
        
    rule interpolated_bareword:
        # Again, a bareword has a fairly limited set of allowed characters
        BAREWORD                    {{ parts = [BAREWORD] }}
        (
            interpolation           {{ parts.append(interpolation) }}
            BAREWORD                {{ parts.append(BAREWORD) }}
        )*                          {{ return parts }}

    rule interpolated_function:
        # Completely arbitrary text, but with balanced parentheses.
        interpolated_function_parens        {{ parts = interpolated_function_parens }}
        (
            interpolation                   {{ parts.append(interpolation) }}
            interpolated_function_parens    {{ parts.extend(interpolated_function_parens) }}
        )*                                  {{ return parts }}

    rule interpolated_function_parens:
        INTERP_NO_PARENS            {{ parts = [INTERP_NO_PARENS] }}
        (
            LPAR
            interpolated_function   {{ parts = parts[:-1] + [parts[-1] + LPAR + interpolated_function[0]] + interpolated_function[0:] }}
            RPAR
            INTERP_NO_PARENS        {{ parts[-1] += RPAR + INTERP_NO_PARENS }}
        )*                          {{ return parts }}


    rule goal_interpolated_anything:
        # This isn't part of the grammar, but rather a separate goal, used for
        # text that might contain interpolations but should not be parsed
        # outside of them -- e.g., selector strings.
        INTERP_ANYTHING             {{ parts = [INTERP_ANYTHING] }}
        (
            interpolation           {{ parts.append(interpolation) }}
            INTERP_ANYTHING         {{ parts.append(INTERP_ANYTHING) }}
        )*
        END                         {{ return Interpolation.maybe(parts) }}

%%