///|
priv struct SassNumber {
amount : Double
numerator : Array[String]
denominator : Array[String]
}
///|
priv enum SassValue {
Number(SassNumber)
Text(String, Bool)
Boolean(Bool)
Null
List(Array[SassValue], String, Bool)
Dictionary(Array[(SassValue, SassValue)])
}
///|
fn numeric(value : Double, unit? : String = "") -> SassValue {
Number({
amount: value,
numerator: if unit.is_empty() {
[]
} else {
[unit]
},
denominator: [],
})
}
///|
fn unit_basis(unit : String) -> (String, Double) {
match unit {
"px" => ("length", 1.0)
"in" => ("length", 96.0)
"cm" => ("length", 96.0 / 2.54)
"mm" => ("length", 96.0 / 25.4)
"q" => ("length", 96.0 / 101.6)
"pt" => ("length", 96.0 / 72.0)
"pc" => ("length", 16.0)
"deg" => ("angle", 1.0)
"grad" => ("angle", 0.9)
"rad" => ("angle", 180.0 / 3.141592653589793)
"turn" => ("angle", 360.0)
"s" => ("time", 1.0)
"ms" => ("time", 0.001)
"Hz" => ("frequency", 1.0)
"kHz" => ("frequency", 1000.0)
"dpi" => ("resolution", 1.0)
"dpcm" => ("resolution", 2.54)
"dppx" => ("resolution", 96.0)
_ => (unit, 1.0)
}
}
///|
fn normalize_number(n : SassNumber) -> SassNumber {
let top = n.numerator.copy()
let bottom = n.denominator.copy()
let mut amount = n.amount
let mut i = 0
while i < top.length() {
let (kind, factor) = unit_basis(top[i])
let mut found = -1
for j = 0; j < bottom.length(); j = j + 1 {
if unit_basis(bottom[j]).0 == kind {
found = j
break
}
}
if found >= 0 {
amount *= factor / unit_basis(bottom[found]).1
ignore(top.remove(i))
ignore(bottom.remove(found))
} else {
i += 1
}
}
{ amount, numerator: top, denominator: bottom, }
}
///|
fn unit_factor(source : Array[String], target : Array[String]) -> Double? {
if source.length() != target.length() {
return None
}
let remaining = target.copy()
let mut factor = 1.0
for unit in source {
let (kind, scale) = unit_basis(unit)
let mut found = -1
for i = 0; i < remaining.length(); i = i + 1 {
if unit_basis(remaining[i]).0 == kind {
found = i
break
}
}
if found < 0 {
return None
}
factor *= scale / unit_basis(remaining[found]).1
ignore(remaining.remove(found))
}
Some(factor)
}
///|
fn SassNumber::unitless(self : SassNumber) -> Bool {
self.numerator.is_empty() && self.denominator.is_empty()
}
///|
fn SassNumber::convert(
self : SassNumber,
target : SassNumber,
unitless? : Bool = false,
) -> Double? {
if unitless && (self.unitless() || target.unitless()) {
return Some(self.amount)
}
match
(
unit_factor(self.numerator, target.numerator),
unit_factor(self.denominator, target.denominator),
) {
(Some(a), Some(b)) => Some(self.amount * a / b)
_ => None
}
}
///|
fn number_text(value : Double) -> String {
if value.is_nan() {
return "NaN"
}
if value.is_inf() {
return if value < 0.0 { "-infinity" } else { "infinity" }
}
if value == 0.0 {
return "0"
}
// Sass emits at most ten fractional digits; do not overflow the scaling step.
let n = if value.abs() < 900000.0 {
(value * 10000000000.0).round() / 10000000000.0
} else {
value
}
let text = n.to_string()
let parts = text.split("e").to_array()
if parts.length() != 2 {
return text
}
let exponent = @string.parse_int(parts[1]) catch { _ => return text }
let negative = parts[0].has_prefix("-")
let digits = parts[0]
.to_owned()
.replace_all(old="-", new="")
.replace_all(old=".", new="")
let lead = parts[0].split(".").to_array()[0].length() -
(if negative { 1 } else { 0 })
let at = lead + exponent
let plain = if at <= 0 {
"0." + "0".repeat(-at) + digits
} else if at >= digits.length() {
digits + "0".repeat(at - digits.length())
} else {
digits[:at].to_owned() + "." + digits[at:].to_owned()
}
(if negative { "-" } else { "" }) + plain
}
///|
fn SassValue::truth(self : SassValue) -> Bool {
match self {
Null | Boolean(false) => false
_ => true
}
}
///|
fn SassValue::items(self : SassValue) -> Array[SassValue] {
match self {
List(values, _, _) => values.copy()
Dictionary(values) => values.map(pair => List([pair.0, pair.1], " ", false))
_ => [self]
}
}
///|
/// Count expanded value structure, including shared lists, before expensive use.
fn SassValue::check_size(
self : SassValue,
remaining : Ref[Int],
depth : Int,
) -> Unit raise ParseError {
if depth > 64 {
raise Invalid("value nesting limit")
}
remaining.val -= 1
if remaining.val < 0 {
raise Invalid("value structure limit")
}
match self {
Text(text, _) =>
if text.length() > 1000000 {
raise Invalid("string value limit")
}
Number(n) =>
if n.numerator.length() + n.denominator.length() > 128 {
raise Invalid("compound unit limit")
}
List(values, _, _) => {
if values.length() > 4096 {
raise Invalid("list length limit")
}
for value in values {
value.check_size(remaining, depth + 1)
}
}
Dictionary(values) => {
if values.length() > 4096 {
raise Invalid("map length limit")
}
for (key, value) in values {
key.check_size(remaining, depth + 1)
value.check_size(remaining, depth + 1)
}
}
_ => ()
}
}
///|
fn bounded_join(
parts : Array[String],
separator : String,
) -> String raise ParseError {
let mut length = 0
for part in parts {
length += part.length() + separator.length()
if length > 1000000 {
raise Invalid("serialized value limit")
}
}
parts.join(separator)
}
///|
fn SassValue::css(
self : SassValue,
unquote? : Bool = false,
inspect? : Bool = false,
) -> String raise ParseError {
match self {
Null => if inspect { "null" } else { "" }
Boolean(value) => value.to_string()
Number(n) => {
let value = number_text(n.amount)
if n.numerator.length() > 1 || !n.denominator.is_empty() {
let top = if n.numerator.is_empty() {
value
} else {
value +
n.numerator[0] +
n.numerator[1:].to_owned().map(unit => " * 1" + unit).join("")
}
let bottom = n.denominator.map(unit => " / 1" + unit).join("")
"calc(" + top + bottom + ")"
} else if n.amount.is_nan() || n.amount.is_inf() {
"calc(" +
value +
(if n.unitless() { "" } else { " * 1" + n.numerator.join("*") }) +
")"
} else {
value +
n.numerator.join("*") +
(if n.denominator.is_empty() {
""
} else {
"/" + n.denominator.join("*")
})
}
}
Text(text, quoted) =>
if quoted && !unquote {
"\"" +
text
.replace_all(old="\\", new="\\\\")
.replace_all(old="\"", new="\\\"")
.replace_all(old="\n", new="\\a ") +
"\""
} else {
text
}
List(values, separator, bracketed) => {
let items = values
.filter(v => !(v is Null))
.map(v => v.css(unquote~, inspect~))
if items.is_empty() && !bracketed && !inspect {
raise Invalid("empty list is not a CSS value")
}
let inside = bounded_join(
items,
if separator == "," {
", "
} else {
separator
},
)
if bracketed {
"[" + inside + "]"
} else if inspect && (items.is_empty() || separator == ",") {
"(" + inside + (if items.length() == 1 { "," } else { "" }) + ")"
} else {
inside
}
}
Dictionary(values) => {
if !inspect {
raise Invalid("map is not a CSS value")
}
"(" +
bounded_join(
values.map(p => {
bounded_join([p.0.css(inspect=true), p.1.css(inspect=true)], ": ")
}),
", ",
) +
")"
}
}
}
///|
fn SassValue::same(self : SassValue, other : SassValue) -> Bool {
match (self, other) {
(Null, Null) => true
(Boolean(a), Boolean(b)) => a == b
(Text(a, _), Text(b, _)) => a == b
(Number(a), Number(b)) =>
match b.convert(a) {
Some(n) =>
(a.amount - n).abs() <=
0.00000000001 * a.amount.abs().max(n.abs()).max(1.0)
None => false
}
(List(a, sep, bracket), List(b, sep2, bracket2)) =>
sep == sep2 &&
bracket == bracket2 &&
a.length() == b.length() &&
same_items(a, b)
(Dictionary(a), Dictionary(b)) =>
a.length() == b.length() &&
a.iter().all(p => b.iter().any(q => p.0.same(q.0) && p.1.same(q.1)))
(List(a, _, false), Dictionary(b)) | (Dictionary(b), List(a, _, false)) =>
a.is_empty() && b.is_empty()
_ => false
}
}
///|
fn same_items(a : Array[SassValue], b : Array[SassValue]) -> Bool {
if a.length() != b.length() {
return false
}
for i in 0.. SassValue raise ParseError {
if operator == "==" || operator == "!=" {
return Boolean(
if operator == "==" {
left.same(right)
} else {
!left.same(right)
},
)
}
if (left, right) is (Number(a), Number(b)) {
if operator == "*" || operator == "/" {
if a.numerator.length() +
a.denominator.length() +
b.numerator.length() +
b.denominator.length() >
128 {
raise Invalid("compound unit limit")
}
return Number(
normalize_number({
amount: if operator == "*" {
a.amount * b.amount
} else {
a.amount / b.amount
},
numerator: a.numerator +
(if operator == "*" { b.numerator } else { b.denominator }),
denominator: a.denominator +
(if operator == "*" { b.denominator } else { b.numerator }),
}),
)
}
let target = if a.unitless() { b } else { a }
let av = a.convert(target, unitless=true).unwrap()
let bv = match b.convert(target, unitless=true) {
Some(v) => v
None => raise Invalid("incompatible units")
}
match operator {
"+" => return Number({ ..target, amount: av + bv, })
"-" => return Number({ ..target, amount: av - bv, })
"%" => return Number({ ..target, amount: av - bv * (av / bv).floor(), })
"<" => return Boolean(av < bv)
"<=" => return Boolean(av <= bv)
">" => return Boolean(av > bv)
">=" => return Boolean(av >= bv)
_ => ()
}
}
if operator == "+" {
let quoted = match left {
Text(_, q) => q
_ => false
}
return Text(
bounded_join([left.css(unquote=true), right.css(unquote=true)], ""),
quoted,
)
}
if operator == "-" || operator == "/" {
return Text(
bounded_join([left.css(unquote=true), right.css(unquote=true)], operator),
false,
)
}
raise Invalid("undefined operation " + operator)
}