///|
fn trim(s : String) -> String {
s.trim().to_owned()
}
///|
fn replace_all(s : String, old : String, replacement : String) -> String {
if old == "" {
return s
}
s.split(old).map(fn(part) { part.to_owned() }).collect().join(replacement)
}
///|
/// Split `input` on every `separator` that is not nested inside brackets or a
/// string, mirroring the upstream `segment` helper.
fn split_top_level(input : String, separator : Char) -> Array[String] {
let parts : Array[String] = []
let stack : Array[Char] = []
let mut current = StringBuilder()
let mut escaped = false
let mut quote : Char? = None
for character in input {
if escaped {
current.write_char(character)
escaped = false
continue
}
if quote is Some(open) {
current.write_char(character)
if character == '\\' {
escaped = true
} else if character == open {
quote = None
}
continue
}
if stack.is_empty() && character == separator {
parts.push(current.to_string())
current = StringBuilder()
continue
}
match character {
'\\' => escaped = true
'"' | '\'' => quote = Some(character)
'(' => stack.push(')')
'[' => stack.push(']')
'{' => stack.push('}')
')' | ']' | '}' =>
if stack.last() is Some(expected) && expected == character {
stack.pop() |> ignore
}
_ => ()
}
current.write_char(character)
}
parts.push(current.to_string())
parts
}
///|
/// Return the index of the first occurrence of `needle` in `haystack`.
///
/// `String::find` is Boyer-Moore-Horspool for needles over four code units. This
/// used to be a hand-written scan comparing `haystack[start:start + n] == needle`
/// at every offset, which the profiler put at ~45% of a full compile: the
/// `@property` trigger table searches the whole generated stylesheet once per
/// trigger, so the matcher runs over the entire output ~66 times per build.
fn index_of(haystack : String, needle : String) -> Int? {
haystack.find(needle)
}
///|
fn escape_class_name(s : String) -> String {
let out = StringBuilder()
for c in s {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => out.write_char(c)
_ => {
out.write_char('\\')
out.write_char(c)
}
}
}
out.to_string()
}
///|
fn decode_underscores(input : String, preserve~ : Bool) -> String {
let output = StringBuilder()
let mut index = 0
while index < input.length() {
if input[index] == '\\' &&
index + 1 < input.length() &&
input[index + 1] == '_' {
output.write_char('_')
index += 2
} else if input[index] == '_' && !preserve {
output.write_char(' ')
index += 1
} else {
output.write_view(input[index:index + 1])
index += 1
}
}
output.to_string()
}
///|
fn decode_arbitrary_node(node : ValueNode) -> ValueNode {
match node {
ValueWord(value) => ValueWord(decode_underscores(value, preserve=false))
ValueSeparator(value) =>
ValueSeparator(decode_underscores(value, preserve=false))
ValueFunction(name, children) => {
let decoded_name = decode_underscores(name, preserve=false)
if name == "url" || name.has_suffix("_url") {
ValueFunction(decoded_name, children)
} else if name == "var" ||
name.has_suffix("_var") ||
name == "theme" ||
name.has_suffix("_theme") {
let decoded_children : Array[ValueNode] = []
for index, child in children {
decoded_children.push(
match child {
ValueWord(value) if index == 0 =>
ValueWord(decode_underscores(value, preserve=true))
_ => decode_arbitrary_node(child)
},
)
}
ValueFunction(decoded_name, decoded_children)
} else {
ValueFunction(decoded_name, children.map(decode_arbitrary_node))
}
}
}
}
///|
fn is_math_function(name : String) -> Bool {
[
"calc", "min", "max", "clamp", "mod", "rem", "sin", "cos", "tan", "asin", "acos",
"atan", "atan2", "pow", "sqrt", "hypot", "log", "exp", "round",
].contains(name)
}
///|
fn add_math_whitespace(input : String) -> String {
let output = StringBuilder()
let math_stack : Array[Bool] = []
let mut index = 0
let mut previous_output : UInt16? = None
let mut previous_significant : UInt16? = None
while index < input.length() {
let current = input[index]
if current == '(' {
let mut start = index
while start > 0 {
let previous = input[start - 1]
if (previous >= 'a' && previous <= 'z') ||
(previous >= '0' && previous <= '9') {
start -= 1
} else {
break
}
}
let name = input[start:index].to_owned()
let parent_math = math_stack.last().unwrap_or(false)
math_stack.push(is_math_function(name) || (parent_math && name == ""))
output.write_char('(')
previous_output = Some('(')
previous_significant = Some('(')
index += 1
continue
}
if current == ')' {
ignore(math_stack.pop())
output.write_char(')')
previous_output = Some(')')
previous_significant = Some(')')
index += 1
continue
}
let in_math = math_stack.last().unwrap_or(false)
if in_math && current == ',' {
output.write_string(", ")
previous_output = Some(' ')
previous_significant = Some(',')
index += 1
while index < input.length() && input[index] == ' ' {
index += 1
}
continue
}
if in_math && current == ' ' && previous_output == Some(' ') {
index += 1
continue
}
if in_math &&
(current == '+' || current == '-' || current == '*' || current == '/') {
let previous_input = if index > 0 { Some(input[index - 1]) } else { None }
let next = if index + 1 < input.length() {
Some(input[index + 1])
} else {
None
}
let exponent = (
previous_input == Some('e') || previous_input == Some('E')
) &&
index > 1 &&
input[index - 2] >= '0' &&
input[index - 2] <= '9'
let unary = previous_significant is None ||
previous_significant == Some('(') ||
previous_significant == Some(',') ||
previous_significant == Some('+') ||
previous_significant == Some('-') ||
previous_significant == Some('*') ||
previous_significant == Some('/')
let custom_property_dash = current == '-' &&
(previous_input == Some('-') || next == Some('-'))
if !exponent && !unary && !custom_property_dash {
if previous_output != Some(' ') {
output.write_char(' ')
}
output.write_view(input[index:index + 1])
if next != Some(' ') {
output.write_char(' ')
previous_output = Some(' ')
} else {
previous_output = Some(current)
}
previous_significant = Some(current)
index += 1
continue
}
}
output.write_view(input[index:index + 1])
previous_output = Some(current)
if current != ' ' {
previous_significant = Some(current)
}
index += 1
}
output.to_string()
}
///|
fn decode_arbitrary(s : String) -> String {
if !s.contains("(") {
return decode_underscores(s, preserve=false)
}
let decoded = parse_value(s).map(decode_arbitrary_node)
add_math_whitespace(render_value(decoded))
}
///|
fn is_valid_arbitrary(input : String) -> Bool {
let closing : Array[UInt16] = []
let mut index = 0
while index < input.length() {
let current = input[index]
if current == '\\' {
index += 2
continue
}
if current == '\'' || current == '"' {
let quote = current
index += 1
while index < input.length() && input[index] != quote {
if input[index] == '\\' {
index += 2
} else {
index += 1
}
}
index += 1
continue
}
match current {
'(' => closing.push(')')
'[' => closing.push(']')
'{' => ()
')' | ']' | '}' => {
let valid = match closing.pop() {
Some(expected) => current == expected
None => false
}
if !valid {
return false
}
}
';' => if closing.is_empty() { return false }
_ => ()
}
index += 1
}
closing.is_empty()
}
///|
fn lexical_compare(a : String, b : String) -> Int {
let common = if a.length() < b.length() { a.length() } else { b.length() }
for i in 0.. b[i] {
return 1
}
}
a.length().compare(b.length())
}
///|
fn candidate_lexical_compare(a : String, b : String) -> Int {
let a_gradient_arbitrary = a.has_prefix("bg-linear-[")
let b_gradient_arbitrary = b.has_prefix("bg-linear-[")
if a_gradient_arbitrary != b_gradient_arbitrary {
return if a_gradient_arbitrary { 1 } else { -1 }
}
let a_typography = a.has_prefix("font-") || a.has_prefix("text-")
let b_typography = b.has_prefix("font-") || b.has_prefix("text-")
let a_typography_rank = if a_typography && a.contains("/") {
2
} else if a_typography && (a.has_prefix("font-[") || a.has_prefix("text-[")) {
1
} else {
0
}
let b_typography_rank = if b_typography && b.contains("/") {
2
} else if b_typography && (b.has_prefix("font-[") || b.has_prefix("text-[")) {
1
} else {
0
}
if a_typography_rank != b_typography_rank {
a_typography_rank.compare(b_typography_rank)
} else {
lexical_compare(a, b)
}
}
///|
fn split_variants(candidate : String) -> Array[String] {
let parts : Array[String] = []
let buf = StringBuilder()
let mut depth = 0
for c in candidate {
match c {
'[' | '(' => {
depth += 1
buf.write_char(c)
}
']' | ')' => {
depth -= 1
buf.write_char(c)
}
':' =>
if depth == 0 {
parts.push(buf.to_string())
buf.reset()
} else {
buf.write_char(c)
}
_ => buf.write_char(c)
}
}
parts.push(buf.to_string())
parts
}