// JavaScript value semantics for the generated theme code. Theme variables
// are dynamically typed upstream, so they are modelled as `Json`, with
// `Json::null()` standing in for `undefined`.
///|
fn str(value : String) -> Json {
Json::string(value)
}
///|
fn num(value : Double) -> Json {
Json::number(value)
}
///|
fn boolean(value : Bool) -> Json {
Json::boolean(value)
}
///|
/// JavaScript truthiness.
fn truthy(value : Json) -> Bool {
match value {
Null | False => false
True => true
Number(n, ..) => n != 0.0 && n == n
String(s) => s != ""
Array(_) | Object(_) => true
}
}
///|
/// `left || right()`.
fn or_else(
left : Json,
right : () -> Json raise @color.ColorError,
) -> Json raise @color.ColorError {
if truthy(left) {
left
} else {
right()
}
}
///|
/// `left ?? right()`.
fn nullish(
left : Json,
right : () -> Json raise @color.ColorError,
) -> Json raise @color.ColorError {
if left is Null {
right()
} else {
left
}
}
///|
/// `left && right()`.
fn and_then(
left : Json,
right : () -> Json raise @color.ColorError,
) -> Json raise @color.ColorError {
if truthy(left) {
right()
} else {
left
}
}
///|
/// Number-to-string conversion as JavaScript performs it.
fn number_to_string(value : Double) -> String {
if value == 0.0 {
"0"
} else {
value.to_string()
}
}
///|
/// `String(value)`, as used for computed property names and concatenation.
fn to_string(value : Json) -> String {
match value {
String(s) => s
Number(n, ..) => number_to_string(n)
True => "true"
False => "false"
Null => "undefined"
Array(items) => items.map(to_string).join(",")
Object(_) => "[object Object]"
}
}
///|
fn to_number(value : Json) -> Double {
match value {
Number(n, ..) => n
True => 1.0
False => 0.0
_ => 0.0 / 0.0
}
}
///|
fn to_int(value : Json) -> Int32 {
to_number(value).to_int()
}
///|
/// The exclusive end of `for (let i = start; i < end; i++)` over integers:
/// the loop runs while `i < end`, up to the smallest integer not below a
/// fractional `end`; `NaN` stops it at once.
fn loop_end(value : Json) -> Int32 {
let end = to_number(value)
if end != end {
-0x80000000
} else {
end.ceil().to_int()
}
}
///|
fn to_array(value : Json) -> Array[Json] {
match value {
Array(items) => items
_ => []
}
}
///|
/// `left + right`: concatenation when either side is a string.
fn add(left : Json, right : Json) -> Json {
match (left, right) {
(String(_), _) | (_, String(_)) => str(to_string(left) + to_string(right))
_ => num(to_number(left) + to_number(right))
}
}
///|
fn mul(left : Json, right : Json) -> Json {
num(to_number(left) * to_number(right))
}
///|
fn rem(left : Json, right : Json) -> Json {
num(to_number(left) % to_number(right))
}
///|
fn neg(value : Json) -> Json {
num(-to_number(value))
}
///|
/// `object?.name`.
fn field(object : Json, name : String) -> Json {
match object {
Object(fields) => fields.get(name).unwrap_or(Json::null())
_ => Json::null()
}
}
///|
/// `array[index]`.
fn index(array : Json, index : Json) -> Json {
match array {
Array(items) => items.get(to_int(index)).unwrap_or(Json::null())
Object(fields) => fields.get(to_string(index)).unwrap_or(Json::null())
_ => Json::null()
}
}
///|
/// `array.length`.
fn length(value : Json) -> Json {
match value {
Array(items) => num(items.length().to_double())
String(s) => num(s.length().to_double())
_ => Json::null()
}
}
///|
/// A color argument. khroma fails on anything but a string.
fn color_of(value : Json) -> String raise @color.ColorError {
match value {
String(s) => s
other => raise @color.Unsupported(to_string(other))
}
}
///|
/// A channel delta: `undefined` and `0` leave the channel untouched.
fn delta(value : Json?) -> Double {
match value {
Some(v) if truthy(v) => to_number(v)
_ => 0.0
}
}
///|
fn adjust(
color : Json,
h? : Json,
s? : Json,
l? : Json,
r? : Json,
g? : Json,
b? : Json,
) -> Json raise @color.ColorError {
str(
@color.adjust(
color_of(color),
h=delta(h),
s=delta(s),
l=delta(l),
r=delta(r),
g=delta(g),
b=delta(b),
),
)
}
///|
/// `amount` may be omitted upstream, which makes the lightness `NaN`.
fn lighten(color : Json, amount : Json) -> Json raise @color.ColorError {
str(@color.lighten(color_of(color), to_number(amount)))
}
///|
fn darken(color : Json, amount : Json) -> Json raise @color.ColorError {
str(@color.darken(color_of(color), to_number(amount)))
}
///|
fn invert(color : Json) -> Json raise @color.ColorError {
str(@color.invert(color_of(color)))
}
///|
fn rgba(r : Json, g : Json, b : Json, a : Json) -> Json {
str(@color.rgba(to_number(r), to_number(g), to_number(b), a=to_number(a)))
}
///|
fn is_dark(color : Json) -> Json raise @color.ColorError {
boolean(@color.is_dark(color_of(color)))
}
///|
/// A muted border for `color`: lighter on dark themes, darker otherwise.
fn mk_border(color : Json, dark_mode : Json) -> Json raise @color.ColorError {
let l = if truthy(dark_mode) { 10.0 } else { -10.0 }
str(@color.adjust(color_of(color), s=-40.0, l~))
}