///|
priv enum Expression {
Literal(SassValue)
Variable(String)
Unary(String, Expression)
Binary(String, Expression, Expression)
Sequence(Array[Expression], String, Bool)
Mapping(Array[(Expression, Expression)])
Call(String, Array[(String?, Expression, Bool)])
RawCall(String, String)
Interpolated(Array[Expression], Bool)
Group(Expression)
}
///|
priv struct ExpressionParser {
chars : Array[Char]
mut pos : Int
mut depth : Int
}
///|
fn whitespace(c : Char) -> Bool {
[' ', '\t', '\n', '\r', '\u000c'].contains(c)
}
///|
fn digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn name_char(c : Char) -> Bool {
word(c) || c == '-' || c.to_int() > 127
}
///|
fn ExpressionParser::peek(self : ExpressionParser, offset? : Int = 0) -> Char {
self.chars.get(self.pos + offset).unwrap_or('\u0000')
}
///|
fn ExpressionParser::space(self : ExpressionParser) -> Bool {
let start = self.pos
while whitespace(self.peek()) {
self.pos += 1
}
self.pos > start
}
///|
fn ExpressionParser::word(self : ExpressionParser, text : String) -> Bool {
let chars = text.to_array()
if self.pos + chars.length() > self.chars.length() {
return false
}
for i in 0.. Expression raise ParseError {
let parser = ExpressionParser::{ chars: text.to_array(), pos: 0, depth: 0, }
ignore(parser.space())
if parser.peek() == '\u0000' {
raise Invalid("expected expression")
}
let result = parser.sequence(false)
ignore(parser.space())
if parser.pos != parser.chars.length() {
raise Invalid("unexpected expression token")
}
result
}
///|
fn ExpressionParser::sequence(
self : ExpressionParser,
bracketed : Bool,
) -> Expression raise ParseError {
self.depth += 1
if self.depth > 64 {
raise Invalid("expression nesting limit")
}
let comma = []
let mut trailing = false
while true {
let values = [self.binary(0)]
while true {
let before = self.pos
let gap = self.space()
let c = self.peek()
if !gap ||
['\u0000', ',', ')', ']', ':', '}'].contains(c) ||
(c == '.' && self.peek(offset=1) == '.') {
self.pos = before
break
}
values.push(self.binary(0))
if values.length() > 4096 {
raise Invalid("list length limit")
}
}
comma.push(
if values.length() == 1 {
values[0]
} else {
Sequence(values, " ", false)
},
)
ignore(self.space())
if self.peek() != ',' {
break
}
self.pos += 1
ignore(self.space())
if ['\u0000', ')', ']', '}'].contains(self.peek()) {
trailing = true
break
}
if comma.length() > 4096 {
raise Invalid("list length limit")
}
}
self.depth -= 1
if comma.length() > 1 || trailing {
Sequence(comma, ",", bracketed)
} else if bracketed {
match comma[0] {
Sequence(values, separator, false) => Sequence(values, separator, true)
value => Sequence([value], " ", true)
}
} else {
comma[0]
}
}
///|
fn ExpressionParser::binary(
self : ExpressionParser,
minimum : Int,
) -> Expression raise ParseError {
let mut left = self.atom()
while true {
let before = self.pos
let gap = self.space()
let c = self.peek()
let pair = c.to_string() + self.peek(offset=1).to_string()
let (operator, precedence, length) = if self.word("or") {
("or", 1, 2)
} else if self.word("and") {
("and", 2, 3)
} else if pair == "==" || pair == "!=" {
(pair, 3, 2)
} else if pair == ">=" || pair == "<=" {
(pair, 4, 2)
} else if c == '>' || c == '<' {
(c.to_string(), 4, 1)
} else if c == '+' || c == '-' {
if c == '-' && gap && !whitespace(self.peek(offset=1)) {
("", 0, 0)
} else {
(c.to_string(), 5, 1)
}
} else if c == '*' || c == '/' || c == '%' {
(c.to_string(), 6, 1)
} else {
("", 0, 0)
}
if precedence == 0 || precedence < minimum {
self.pos = before
break
}
self.pos += length
ignore(self.space())
let right = self.binary(precedence + 1)
left = Binary(operator, left, right)
}
left
}
///|
fn ExpressionParser::interpolation(
self : ExpressionParser,
) -> Expression raise ParseError {
self.pos += 2
ignore(self.space())
let value = self.sequence(false)
ignore(self.space())
if self.peek() != '}' {
raise Invalid("unterminated interpolation")
}
self.pos += 1
value
}
///|
fn ExpressionParser::atom(
self : ExpressionParser,
) -> Expression raise ParseError {
self.depth += 1
if self.depth > 64 {
raise Invalid("expression nesting limit")
}
let result = self.atom_inner()
self.depth -= 1
result
}
///|
fn ExpressionParser::atom_inner(
self : ExpressionParser,
) -> Expression raise ParseError {
ignore(self.space())
let c = self.peek()
if c == '\u0000' {
raise Invalid("expected expression")
}
if self.word("not") {
self.pos += 3
return Unary("not", self.atom())
}
if c == '+' ||
(
c == '-' &&
(
digit(self.peek(offset=1)) ||
self.peek(offset=1) == '$' ||
self.peek(offset=1) == '(' ||
self.peek(offset=1) == '.'
)
) {
self.pos += 1
return Unary(c.to_string(), self.atom())
}
if c == '$' {
self.pos += 1
let start = self.pos
while name_char(self.peek()) {
self.pos += 1
}
if start == self.pos {
raise Invalid("invalid variable")
}
return Variable(identifier(String::from_array(self.chars[start:self.pos])))
}
if c == '"' || c == '\'' {
self.pos += 1
let parts = []
let mut text = StringBuilder()
while self.peek() != c {
if self.peek() == '\u0000' {
raise Invalid("unterminated string")
}
if self.peek() == '#' && self.peek(offset=1) == '{' {
parts.push(Literal(Text(text.to_string(), false)))
text = StringBuilder()
parts.push(self.interpolation())
} else if self.peek() == '\\' {
self.pos += 1
let next = self.peek()
if next == '\u0000' {
raise Invalid("unterminated escape")
}
text.write_char(next)
self.pos += 1
} else {
text.write_char(self.peek())
self.pos += 1
}
}
self.pos += 1
parts.push(Literal(Text(text.to_string(), false)))
return Interpolated(parts, true)
}
if c == '(' || c == '[' {
self.pos += 1
ignore(self.space())
let closing = if c == '(' { ')' } else { ']' }
if self.peek() == closing {
self.pos += 1
return Literal(List([], ",", c == '['))
}
let first = self.sequence(c == '[')
if c == '(' && self.peek() == ':' {
let pairs = []
let mut key = first
while true {
self.pos += 1
ignore(self.space())
let value = self.argument()
pairs.push((key, value))
ignore(self.space())
if self.peek() != ',' {
break
}
self.pos += 1
ignore(self.space())
if self.peek() == ')' {
break
}
key = self.argument()
ignore(self.space())
if self.peek() != ':' {
raise Invalid("expected map colon")
}
}
if self.peek() != ')' {
raise Invalid("unterminated map")
}
self.pos += 1
return Mapping(pairs)
}
if self.peek() != closing {
raise Invalid("unterminated group")
}
self.pos += 1
return if c == '(' { Group(first) } else { first }
}
if digit(c) || (c == '.' && digit(self.peek(offset=1))) {
let start = self.pos
while digit(self.peek()) {
self.pos += 1
}
if self.peek() == '.' && digit(self.peek(offset=1)) {
self.pos += 1
while digit(self.peek()) {
self.pos += 1
}
}
if self.peek() == 'e' || self.peek() == 'E' {
let old = self.pos
self.pos += 1
if self.peek() == '+' || self.peek() == '-' {
self.pos += 1
}
if !digit(self.peek()) {
self.pos = old
} else {
while digit(self.peek()) {
self.pos += 1
}
}
}
let value = @string.parse_double(
String::from_array(self.chars[start:self.pos]),
) catch {
_ => raise Invalid("invalid number")
}
let unit_start = self.pos
if self.peek() == '%' {
self.pos += 1
} else {
while name_char(self.peek()) && !digit(self.peek()) && self.peek() != '-' {
self.pos += 1
}
}
return Literal(
numeric(value, unit=String::from_array(self.chars[unit_start:self.pos])),
)
}
let start = self.pos
let parts = []
let mut text = StringBuilder()
let mut interpolated = false
while true {
let x = self.peek()
if x == '#' && self.peek(offset=1) == '{' {
parts.push(Literal(Text(text.to_string(), false)))
text = StringBuilder()
parts.push(self.interpolation())
interpolated = true
} else if name_char(x) ||
['#', '%', '!'].contains(x) ||
(x == '.' && self.peek(offset=1) != '.') ||
(x == '\\' && self.peek(offset=1) != '\u0000') {
text.write_char(x)
self.pos += 1
if x == '\\' {
text.write_char(self.peek())
self.pos += 1
}
} else {
break
}
}
if self.pos == start {
raise Invalid("unexpected expression character")
}
parts.push(Literal(Text(text.to_string(), false)))
if interpolated {
return Interpolated(parts, false)
}
let name = text.to_string()
if name.has_suffix(".") && self.peek() == '$' {
self.pos += 1
let start = self.pos
while name_char(self.peek()) {
self.pos += 1
}
if start == self.pos {
raise Invalid("invalid module variable")
}
return Variable(
reference_name(name + String::from_array(self.chars[start:self.pos])),
)
}
if self.peek() == '(' {
if ["url", "calc", "clamp", "element", "expression"].contains(name) {
self.pos += 1
let start = self.pos
let mut depth = 1
let mut quote = '\u0000'
while self.pos < self.chars.length() && depth > 0 {
let x = self.peek()
self.pos += 1
if x == '\\' {
self.pos += 1
continue
}
if quote != '\u0000' {
if x == quote {
quote = '\u0000'
}
} else if x == '"' || x == '\'' {
quote = x
} else if x == '(' {
depth += 1
} else if x == ')' {
depth -= 1
}
}
if depth != 0 {
raise Invalid("unterminated CSS function")
}
return RawCall(name, String::from_array(self.chars[start:self.pos - 1]))
}
self.pos += 1
ignore(self.space())
let args = []
if self.peek() != ')' {
while true {
let mut value = self.argument()
let mut key : String? = None
ignore(self.space())
if self.peek() == ':' {
guard value is Variable(variable) else {
raise Invalid("keyword argument requires variable")
}
key = Some(variable)
self.pos += 1
ignore(self.space())
value = self.argument()
ignore(self.space())
}
let spread = self.peek() == '.' &&
self.peek(offset=1) == '.' &&
self.peek(offset=2) == '.'
if spread {
self.pos += 3
ignore(self.space())
}
args.push((key, value, spread))
if self.peek() != ',' {
break
}
self.pos += 1
ignore(self.space())
if self.peek() == ')' {
break
}
if args.length() > 4096 {
raise Invalid("argument limit")
}
}
}
if self.peek() != ')' {
raise Invalid("unterminated function")
}
self.pos += 1
return Call(reference_name(name), args)
}
match name {
"true" => Literal(Boolean(true))
"false" => Literal(Boolean(false))
"null" => Literal(Null)
_ => Literal(Text(name, false))
}
}
///|
fn ExpressionParser::argument(
self : ExpressionParser,
) -> Expression raise ParseError {
let values = [self.binary(0)]
while true {
let before = self.pos
if !self.space() ||
[',', ')', ']', ':', '\u0000'].contains(self.peek()) ||
(self.peek() == '.' && self.peek(offset=1) == '.') {
self.pos = before
break
}
values.push(self.binary(0))
if values.length() > 4096 {
raise Invalid("argument length limit")
}
}
if values.length() == 1 {
values[0]
} else {
Sequence(values, " ", false)
}
}