///|
// The values of a theme, as Ruby asciidoctor-pdf's ThemeLoader stores them:
// YAML scalars and collections (after Psych's scalar resolution), evaluated,
// with colors kept apart from strings (its `HexColorValue`,
// `TransparentColorValue` and `CMYKColorValue`).
///|
/// A theme value.
pub(all) enum Value {
Null
Bool(Bool)
/// a Ruby Integer
Int(Int64)
/// a Ruby Float
Float(Double)
Str(String)
Array(Array[Value])
/// a Ruby Hash (the font catalog, an admonition icon's settings), in
/// insertion order; `symbols` when Ruby's keys are symbols
Hash(Map[String, Value], symbols~ : Bool)
/// `HexColorValue`: six upper-case hex digits (no `#`)
Hex(String)
/// `TransparentColorValue`
Transparent
/// `CMYKColorValue`: four components, each an Int or a Float (0..100)
Cmyk(Array[Value])
} derive(Eq, Debug)
///|
pub extend Value with Eq::{not_equal, equal}
///|
pub extend Value with Debug::{to_repr}
///|
/// Ruby's truthiness: everything but nil and false.
pub fn Value::truthy(self : Value) -> Bool {
!(self is (Null | Bool(false)))
}
///|
/// Ruby `Numeric === value`.
pub fn Value::is_numeric(self : Value) -> Bool {
self is (Int(_) | Float(_))
}
///|
/// A number as a Double (Ruby `to_f` of a Numeric).
pub fn Value::as_number(self : Value) -> Double? {
match self {
Int(i) => Some(i.to_double())
Float(f) => Some(f)
_ => None
}
}
///|
/// Ruby `to_f`: Numeric, String (leading number), nil (0.0).
pub fn Value::to_f(self : Value) -> Double {
match self {
Int(i) => i.to_double()
Float(f) => f
Str(s) | Hex(s) => ruby_string_to_f(s)
_ => 0.0
}
}
///|
/// Ruby's `to_s` of the value (what `"#{value}"` and `gsub` blocks produce).
pub fn Value::to_ruby_s(self : Value) -> String {
match self {
Null => ""
Bool(b) => b.to_string()
Int(i) => i.to_string()
Float(f) => ruby_float_to_s(f)
Str(s) | Hex(s) => s
Transparent => "transparent"
Array(_) | Hash(_, ..) => self.inspect_ruby()
// CMYKColorValue#to_s
Cmyk(items) => "[" + items.map(v => v.to_ruby_s()).join(", ") + "]"
}
}
///|
/// Ruby's `inspect` of the value (Array#to_s and Hash#to_s use it).
pub fn Value::inspect_ruby(self : Value) -> String {
match self {
Null => "nil"
Str(s) | Hex(s) => ruby_string_inspect(s)
Transparent => ruby_string_inspect("transparent")
Array(items) | Cmyk(items) =>
"[" + items.map(v => v.inspect_ruby()).join(", ") + "]"
Hash(map, symbols~) => {
if map.is_empty() {
return "{}"
}
let parts = []
for k, v in map {
if symbols {
parts.push("\{k}: \{v.inspect_ruby()}")
} else {
parts.push("\{ruby_string_inspect(k)} => \{v.inspect_ruby()}")
}
}
"{" + parts.join(", ") + "}"
}
_ => self.to_ruby_s()
}
}
///|
/// Ruby's `String#inspect` for the strings a theme holds: printable
/// characters as they are, the usual escapes for the rest.
fn ruby_string_inspect(s : String) -> String {
let sb = StringBuilder()
sb.write_char('"')
for i, c in s {
match c {
'"' => sb.write_string("\\\"")
'\\' => sb.write_string("\\\\")
'\n' => sb.write_string("\\n")
'\t' => sb.write_string("\\t")
'\r' => sb.write_string("\\r")
'\u{0C}' => sb.write_string("\\f")
'\u{0B}' => sb.write_string("\\v")
'\u{08}' => sb.write_string("\\b")
'\u{07}' => sb.write_string("\\a")
'\u{1B}' => sb.write_string("\\e")
'#' => {
// `#{`, `#$` and `#@` would interpolate
let next = s.get_char(i + 1)
if next is Some('{' | '$' | '@') {
sb.write_string("\\#")
} else {
sb.write_char('#')
}
}
c if c.to_int() < 0x20 || c.to_int() == 0x7F => {
let hex = c.to_int().to_string(radix=16).to_upper()
sb.write_string("\\x")
if hex.length() < 2 {
sb.write_char('0')
}
sb.write_string(hex)
}
c => sb.write_char(c)
}
}
sb.write_char('"')
sb.to_string()
}
///|
/// Ruby's `Float#to_s`: the shortest representation that round-trips, in
/// fixed notation (with at least one fractional digit) when the decimal
/// exponent is between -4 and 16, else `d.ddde+XX`.
pub fn ruby_float_to_s(f : Double) -> String {
if f.is_nan() {
return "NaN"
}
if f.is_pos_inf() {
return "Infinity"
}
if f.is_neg_inf() {
return "-Infinity"
}
if f == 0.0 {
return if 1.0 / f < 0.0 { "-0.0" } else { "0.0" }
}
let negative = f < 0.0
// shortest digits and the decimal point's position (value = 0.DIGITS ×
// 10^decpt), from MoonBit's own shortest representation
let repr = f.abs().to_string()
let (mantissa, exp) = match repr.find("e") {
Some(i) =>
(
repr[:i].to_owned(),
@string.parse_int(repr[i + 1:].to_owned()) catch {
_ => 0
},
)
None => (repr, 0)
}
let (int_part, frac_part) = match mantissa.find(".") {
Some(i) => (mantissa[:i].to_owned(), mantissa[i + 1:].to_owned())
None => (mantissa, "")
}
let mut digits = int_part + frac_part
let mut decpt = int_part.length() + exp
// strip leading zeros (0.0001 → digits 00001)
let mut lead = 0
while lead < digits.length() - 1 && digits[lead] == '0' {
lead += 1
}
digits = digits[lead:].to_owned()
decpt -= lead
// strip trailing zeros (100 → 1)
let mut end = digits.length()
while end > 1 && digits[end - 1] == '0' {
end -= 1
}
digits = digits[:end].to_owned()
let sb = StringBuilder()
if negative {
sb.write_char('-')
}
let n = digits.length()
if decpt > 15 || decpt < -3 {
sb.write_string(digits[:1].to_owned())
sb.write_char('.')
sb.write_string(if n > 1 { digits[1:].to_owned() } else { "0" })
let e = decpt - 1
sb.write_char('e')
sb.write_char(if e < 0 { '-' } else { '+' })
let abs = if e < 0 { -e } else { e }
if abs < 10 {
sb.write_char('0')
}
sb.write_string(abs.to_string())
} else if decpt <= 0 {
sb.write_string("0.")
for _ in 0..<-decpt {
sb.write_char('0')
}
sb.write_string(digits)
} else if decpt >= n {
sb.write_string(digits)
for _ in 0..<(decpt - n) {
sb.write_char('0')
}
sb.write_string(".0")
} else {
sb.write_string(digits[:decpt].to_owned())
sb.write_char('.')
sb.write_string(digits[decpt:].to_owned())
}
sb.to_string()
}
///|
fn is_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn is_ruby_space(c : Char) -> Bool {
c == ' ' ||
c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\u{0B}' ||
c == '\u{0C}'
}
///|
/// Digits with single underscores between them (Ruby's number literals in
/// `to_f`/`to_i`), from `start`; returns the end and the digits.
fn scan_digits(s : String, start : Int) -> (Int, String) {
let sb = StringBuilder()
let mut i = start
while i < s.length() {
let c = s[i].to_int().unsafe_to_char()
if is_digit(c) {
sb.write_char(c)
i += 1
} else if c == '_' &&
sb.to_string() != "" &&
i + 1 < s.length() &&
is_digit(s[i + 1].to_int().unsafe_to_char()) {
i += 1
} else {
break
}
}
(i, sb.to_string())
}
///|
/// Ruby's `String#to_f`: the longest leading decimal number (after
/// whitespace), else 0.0.
pub fn ruby_string_to_f(s : String) -> Double {
let mut i = 0
while i < s.length() && is_ruby_space(s[i].to_int().unsafe_to_char()) {
i += 1
}
let sb = StringBuilder()
if i < s.length() && (s[i] == '-' || s[i] == '+') {
sb.write_char(s[i].to_int().unsafe_to_char())
i += 1
}
let (after_int, int_digits) = scan_digits(s, i)
i = after_int
sb.write_string(if int_digits == "" { "0" } else { int_digits })
if i + 1 < s.length() && s[i] == '.' {
let (after_frac, frac_digits) = scan_digits(s, i + 1)
if frac_digits != "" {
sb.write_char('.')
sb.write_string(frac_digits)
i = after_frac
}
}
if int_digits == "" && !sb.to_string().contains(".") {
return 0.0
}
if i < s.length() && (s[i] == 'e' || s[i] == 'E') {
let mut j = i + 1
let exp = StringBuilder()
if j < s.length() && (s[j] == '-' || s[j] == '+') {
exp.write_char(s[j].to_int().unsafe_to_char())
j += 1
}
let (_, exp_digits) = scan_digits(s, j)
if exp_digits != "" {
sb.write_char('e')
sb.write_string(exp.to_string())
sb.write_string(exp_digits)
}
}
@string.parse_double(sb.to_string()) catch {
_ => 0.0
}
}
///|
/// Ruby's `String#to_i`: the leading decimal integer (after whitespace),
/// else 0.
pub fn ruby_string_to_i(s : String) -> Int64 {
let mut i = 0
while i < s.length() && is_ruby_space(s[i].to_int().unsafe_to_char()) {
i += 1
}
let mut negative = false
if i < s.length() && (s[i] == '-' || s[i] == '+') {
negative = s[i] == '-'
i += 1
}
let (_, digits) = scan_digits(s, i)
if digits == "" {
return 0L
}
let v = @string.parse_int64(digits) catch { _ => 0L }
if negative {
-v
} else {
v
}
}
///|
/// Ruby's `Float#to_i` (truncation), saturating where Ruby would grow a
/// Bignum.
fn float_to_i(f : Double) -> Int64 {
if f.is_nan() {
return 0L
}
let t = if f < 0.0 { -(-f).floor() } else { f.floor() }
if t >= 9.2e18 {
9223372036854775807L
} else if t <= -9.2e18 {
-9223372036854775807L
} else {
t.to_int64()
}
}