///|
/// Parsed view of a CSI payload. Missing parameters are represented as `None`;
/// this preserves the semantic difference between an omitted value and zero.
pub(all) struct CsiCommand {
private_prefix : String
params : Array[Int?]
intermediates : String
final_byte : Char
} derive(Debug, Eq)
///|
/// Parsed view of an OSC event.
pub(all) struct OscCommand {
command : String
data : String
} derive(Debug, Eq)
///|
fn is_ascii_digit(c : Char) -> Bool {
c >= '0' && c <= '9'
}
///|
fn parse_decimal(chars : Array[Char]) -> Int? {
if chars.length() == 0 {
return None
}
let mut value = 0
for c in chars {
if !is_ascii_digit(c) {
return None
}
value = value * 10 + c.to_int() - '0'.to_int()
}
Some(value)
}
///|
pub fn parse_csi(event : Event) -> CsiCommand? {
guard event.kind == Csi else { return None }
guard event.final_byte is Some(final_byte) else { return None }
let private_chars : Array[Char] = []
let intermediate_chars : Array[Char] = []
let params : Array[Int?] = []
let current : Array[Char] = []
let mut phase = 0 // 0 private, 1 parameters, 2 intermediates
let mut saw_parameter_syntax = false
for c in event.payload {
let n = c.to_int()
if phase == 0 && n >= 0x3c && n <= 0x3f {
private_chars.push(c)
} else if n >= 0x30 && n <= 0x3b && phase < 2 {
phase = 1
saw_parameter_syntax = true
if c == ';' || c == ':' {
params.push(parse_decimal(current))
current.clear()
} else {
current.push(c)
}
} else if n >= 0x20 && n <= 0x2f {
if phase == 1 {
params.push(parse_decimal(current))
current.clear()
}
phase = 2
intermediate_chars.push(c)
} else {
return None
}
}
if phase == 1 || saw_parameter_syntax {
params.push(parse_decimal(current))
}
Some({
private_prefix: String::from_array(private_chars),
params,
intermediates: String::from_array(intermediate_chars),
final_byte,
})
}
///|
pub fn parse_osc(event : Event) -> OscCommand? {
guard event.kind == Osc else { return None }
let command = StringBuilder::new()
let data = StringBuilder::new()
let mut in_data = false
for c in event.payload {
if !in_data && c == ';' {
in_data = true
} else if in_data {
data.write_char(c)
} else {
command.write_char(c)
}
}
Some({ command: command.to_string(), data: data.to_string() })
}
///|
pub fn csi_param(command : CsiCommand, index : Int, default : Int) -> Int {
if index < 0 || index >= command.params.length() {
return default
}
match command.params[index] {
Some(value) => value
None => default
}
}
///|
fn ascii_lower(c : Char) -> Char {
if c >= 'A' && c <= 'Z' {
Int::unsafe_to_char(c.to_int() + 32)
} else {
c
}
}
///|
pub fn lower_ascii(input : String) -> String {
let out = StringBuilder::new(size_hint=input.length())
for c in input {
out.write_char(ascii_lower(c))
}
out.to_string()
}
///|
pub fn starts_with_ascii_case_insensitive(
input : String,
prefix : String,
) -> Bool {
if prefix.length() > input.length() {
return false
}
let mut i = 0
while i < prefix.length() {
let left = match input.unsafe_get(i).to_char() {
Some(c) => c
None => return false
}
let right = match prefix.unsafe_get(i).to_char() {
Some(c) => c
None => return false
}
if ascii_lower(left) != ascii_lower(right) {
return false
}
i += 1
}
true
}
///|
pub fn contains_char(input : String, needle : Char) -> Bool {
for c in input {
if c == needle {
return true
}
}
false
}
///|
pub fn is_suspicious_uri(uri : String) -> Bool {
let value = lower_ascii(uri)
starts_with_ascii_case_insensitive(value, "javascript:") ||
starts_with_ascii_case_insensitive(value, "data:") ||
starts_with_ascii_case_insensitive(value, "file:") ||
starts_with_ascii_case_insensitive(value, "vbscript:") ||
starts_with_ascii_case_insensitive(value, "ssh:")
}
///|
pub fn is_bidi_control(c : Char) -> Bool {
let n = c.to_int()
n == 0x061c ||
n == 0x200e ||
n == 0x200f ||
n == 0x202a ||
n == 0x202b ||
n == 0x202c ||
n == 0x202d ||
n == 0x202e ||
n == 0x2066 ||
n == 0x2067 ||
n == 0x2068 ||
n == 0x2069
}