use bumpalo::Bump;
use bumpalo::collections::Vec as BumpVec;
use super::scan::{comment_end, is_comment_start, matching_close_paren};
use crate::escapes::escape_len;
use crate::lexer::{hyphen_starts_own_token, string_end};
use crate::number::{number_part_len, starts_number};
#[derive(Clone, Copy, PartialEq, Eq)]
enum TokenKind {
Number,
Word,
Closed,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Preceding {
Head,
Operator { colon: bool },
Token(TokenKind),
}
const fn releases_operator(b: u8, colon_is_operator: bool) -> bool {
matches!(b, b'*' | b'+' | b'/' | b'-') || (b == b':' && colon_is_operator)
}
const fn ends_word(b: u8, colon_is_operator: bool) -> bool {
matches!(b, b'*' | b'+') || (b == b':' && colon_is_operator)
}
const fn opens_run(b: u8, colon_is_operator: bool) -> bool {
matches!(b, b'*' | b'/' | b'+') || (b == b':' && colon_is_operator)
}
const fn ends_every_token(b: u8) -> bool {
matches!(b, b'@' | b'{' | b'}')
}
const fn ends_atword(b: u8) -> bool {
matches!(
b,
b'{' | b'}' | b'(' | b')' | b'\'' | b'"' | b',' | b'/' | b'@'
)
}
const fn starts_word(b: Option<u8>, after: Option<u8>) -> bool {
match b {
None => false,
Some(b'-') => matches!(after, Some(b'-')),
Some(b) => !matches!(
b,
b'(' | b')' | b'\'' | b'"' | b'@' | b'{' | b'}' | b',' | b':' | b'*' | b'/' | b'+'
),
}
}
const fn may_open_boundary(b: u8) -> bool {
releases_operator(b, true) || b == b'(' || ends_every_token(b)
}
const _: () = {
let mut b = 0u8;
while b < 128 {
assert!(
!(releases_operator(b, true)
|| ends_word(b, true)
|| opens_run(b, true)
|| ends_every_token(b))
|| may_open_boundary(b),
"a boundary byte the refusal pass cannot see"
);
b += 1;
}
};
fn is_content_pair(bytes: &[u8], i: usize) -> bool {
matches!(bytes[i], b'-' | b'/') && bytes.get(i + 1) == Some(&bytes[i])
}
fn unicode_range_len(bytes: &[u8], i: usize) -> usize {
let is_range_digit = |b: u8| b.is_ascii_hexdigit() || b == b'?';
if !bytes[i].eq_ignore_ascii_case(&b'u')
|| bytes.get(i + 1) != Some(&b'+')
|| !bytes.get(i + 2).copied().is_some_and(is_range_digit)
{
return 0;
}
let mut end = i + 2;
while bytes.get(end).copied().is_some_and(is_range_digit) {
end += 1;
}
if bytes.get(end) == Some(&b'-')
&& bytes
.get(end + 1)
.copied()
.is_some_and(|b| b.is_ascii_hexdigit())
{
end += 1;
while bytes
.get(end)
.copied()
.is_some_and(|b| b.is_ascii_hexdigit())
{
end += 1;
}
}
end - i
}
pub(crate) struct ValueToken {
pub(crate) start: usize,
pub(crate) end: usize,
pub(crate) is_operator: bool,
}
pub(crate) fn split_value_run<'arena>(
run: &str,
colon_is_operator: bool,
head_welds: bool,
arena: &'arena Bump,
) -> Option<BumpVec<'arena, ValueToken>> {
let bytes = run.as_bytes();
let may_split = match bytes {
_ if opens_on_word(bytes) => any_byte_has(bytes, ENDS_WORD_RUN),
[b'-', digit, rest @ ..] if digit.is_ascii_digit() => any_byte_has(rest, MAY_OPEN_BOUNDARY),
_ => any_byte_has(bytes, MAY_OPEN_BOUNDARY),
};
if !may_split {
debug_assert!(
tokenize_run(run, colon_is_operator, head_welds, arena).is_none(),
"the refusal pass declined a run that splits: {run:?}"
);
return None;
}
tokenize_run(run, colon_is_operator, head_welds, arena)
}
fn opens_on_word(bytes: &[u8]) -> bool {
match bytes {
[b, ..] if b.is_ascii_alphabetic() || matches!(b, b'_' | b'#') => true,
[b'-', b'-', ..] => true,
[b'-', b, ..] => b.is_ascii_alphabetic(),
_ => false,
}
}
const fn ends_word_run(b: u8) -> bool {
ends_word(b, true) || b == b'(' || ends_every_token(b)
}
const BOUNDARY_BITS: [u8; 256] = {
let mut table = [0u8; 256];
let mut b = 0usize;
while b < 256 {
table[b] = (may_open_boundary(b as u8) as u8) * MAY_OPEN_BOUNDARY
+ (ends_word_run(b as u8) as u8) * ENDS_WORD_RUN;
b += 1;
}
table
};
const MAY_OPEN_BOUNDARY: u8 = 1;
const ENDS_WORD_RUN: u8 = 2;
#[inline]
fn any_byte_has(bytes: &[u8], bit: u8) -> bool {
let fold = |chunk: &[u8]| {
chunk
.iter()
.fold(0u8, |seen, &b| seen | BOUNDARY_BITS[b as usize])
};
let mut chunks = bytes.chunks_exact(8);
for chunk in &mut chunks {
if fold(chunk) & bit != 0 {
return true;
}
}
fold(chunks.remainder()) & bit != 0
}
fn tokenize_run<'arena>(
run: &str,
colon_is_operator: bool,
head_welds: bool,
arena: &'arena Bump,
) -> Option<BumpVec<'arena, ValueToken>> {
let len = run.len();
if len == 0 {
return None;
}
let mut i = 0usize;
let mut previous = Preceding::Head;
let first = next_member(run, &mut i, &mut previous, colon_is_operator, head_welds);
if i == len && !first.is_operator {
return None;
}
let mut tokens = BumpVec::with_capacity_in(4, arena);
tokens.push(first);
while i < len {
tokens.push(next_member(
run,
&mut i,
&mut previous,
colon_is_operator,
head_welds,
));
}
Some(tokens)
}
#[expect(clippy::inline_always)]
#[inline(always)]
fn next_member(
run: &str,
i: &mut usize,
previous: &mut Preceding,
colon_is_operator: bool,
head_welds: bool,
) -> ValueToken {
let start = *i;
if operator_stands_here(run, start, *previous, colon_is_operator, head_welds) {
*previous = Preceding::Operator {
colon: run.as_bytes()[start] == b':',
};
*i += 1;
return ValueToken {
start,
end: start + 1,
is_operator: true,
};
}
*previous = Preceding::Token(scan_token(run, i, colon_is_operator));
debug_assert!(*i > start, "a token consumed nothing: {run:?} at {start}");
ValueToken {
start,
end: *i,
is_operator: false,
}
}
fn operator_stands_here(
run: &str,
i: usize,
previous: Preceding,
colon_is_operator: bool,
head_welds: bool,
) -> bool {
let bytes = run.as_bytes();
let b = bytes[i];
if is_comment_start(bytes, i) {
return false;
}
if is_content_pair(bytes, i) {
return false;
}
match previous {
Preceding::Token(TokenKind::Number | TokenKind::Closed) => {
releases_operator(b, colon_is_operator)
}
Preceding::Token(TokenKind::Word) => ends_word(b, colon_is_operator),
Preceding::Head => operator_at_operand_position(run, i, colon_is_operator, head_welds),
Preceding::Operator { colon } => {
operator_at_operand_position(run, i, colon_is_operator, !colon)
}
}
}
fn operator_at_operand_position(
run: &str,
i: usize,
colon_is_operator: bool,
plus_welds: bool,
) -> bool {
let bytes = run.as_bytes();
let b = bytes[i];
let next = bytes.get(i + 1).copied();
let after = bytes.get(i + 2).copied();
if b == b'-' {
return !hyphen_starts_own_token(&run[i + 1..]);
}
opens_run(b, colon_is_operator)
&& !starts_number(&run[i..])
&& !(b == b'+' && plus_welds && starts_word(next, after))
}
fn scan_token(run: &str, i: &mut usize, colon_is_operator: bool) -> TokenKind {
let bytes = run.as_bytes();
if bytes[*i] == b'(' {
return close_group(run, i);
}
if bytes[*i] == b'+' {
*i += 1;
return scan_token(run, i, colon_is_operator);
}
if bytes[*i] == b'@' {
return scan_atword(run, i);
}
if matches!(bytes[*i], b'{' | b'}') {
*i += 1;
return TokenKind::Closed;
}
let range_len = unicode_range_len(bytes, *i);
if range_len > 0 {
*i += range_len;
return TokenKind::Closed;
}
let number_len = number_part_len(&run[*i..]);
if number_len > 0 {
*i += number_len;
return scan_token_body(run, i, colon_is_operator, TokenKind::Number);
}
scan_token_body(run, i, colon_is_operator, TokenKind::Word)
}
fn scan_token_body(
run: &str,
i: &mut usize,
colon_is_operator: bool,
kind: TokenKind,
) -> TokenKind {
let bytes = run.as_bytes();
let len = bytes.len();
while *i < len {
let b = bytes[*i];
if b == b'\\'
&& let Some(step) = escape_len(run, *i)
{
*i += step;
continue;
}
if b == b'"' || b == b'\'' {
*i = string_end(bytes, *i).unwrap_or(len);
continue;
}
if is_comment_start(bytes, *i) {
*i = comment_end(bytes, *i);
continue;
}
if b == b'(' {
return close_group(run, i);
}
if b == b'[' {
*i = simple_block_end(run, *i);
continue;
}
if ends_every_token(b) {
return kind;
}
if is_content_pair(bytes, *i) {
*i += 2;
continue;
}
let boundary = match kind {
TokenKind::Number | TokenKind::Closed => releases_operator(b, colon_is_operator),
TokenKind::Word => ends_word(b, colon_is_operator),
};
if boundary {
return kind;
}
*i += 1;
}
kind
}
fn scan_atword(run: &str, i: &mut usize) -> TokenKind {
let bytes = run.as_bytes();
debug_assert_eq!(bytes[*i], b'@', "scan_atword must be entered on a `@`");
*i += 1;
while *i < bytes.len() {
let b = bytes[*i];
if b == b'\\'
&& let Some(step) = escape_len(run, *i)
{
*i += step;
continue;
}
if is_comment_start(bytes, *i) {
*i = comment_end(bytes, *i);
continue;
}
if ends_atword(b) {
break;
}
*i += 1;
}
TokenKind::Closed
}
pub(crate) fn is_whole_value_block(text: &str) -> bool {
text.starts_with('{') && simple_block_end(text, 0) == text.len()
}
fn simple_block_end(run: &str, open: usize) -> usize {
let bytes = run.as_bytes();
debug_assert!(
matches!(bytes.get(open), Some(b'[' | b'{')),
"simple_block_end must be entered on a `[` or a `{{`"
);
let close = if bytes[open] == b'[' { b']' } else { b'}' };
let mut depth = 0u32;
let mut i = open;
while i < bytes.len() {
let b = bytes[i];
if b == b'\\'
&& let Some(step) = escape_len(run, i)
{
i += step;
continue;
}
if b == b'"' || b == b'\'' {
i = string_end(bytes, i).unwrap_or(bytes.len());
continue;
}
if is_comment_start(bytes, i) {
i = comment_end(bytes, i);
continue;
}
if b == bytes[open] {
depth += 1;
} else if b == close {
depth -= 1;
if depth == 0 {
return i + 1;
}
}
i += 1;
}
bytes.len()
}
fn close_group(run: &str, i: &mut usize) -> TokenKind {
match matching_close_paren(run, *i) {
Some(close) => *i = close + 1,
None => *i = run.len(),
}
TokenKind::Closed
}
#[cfg(test)]
mod tests {
use super::{BOUNDARY_BITS, ENDS_WORD_RUN, MAY_OPEN_BOUNDARY, any_byte_has, split_value_run};
#[test]
fn any_byte_has_matches_the_byte_loop() {
let model =
|bytes: &[u8], bit: u8| bytes.iter().any(|&b| BOUNDARY_BITS[b as usize] & bit != 0);
let carrier = |bits: u8| {
(0..=255u8)
.find(|&b| BOUNDARY_BITS[b as usize] == bits)
.unwrap_or_else(|| panic!("no byte carries exactly {bits:#b}"))
};
let neither = carrier(0);
let specials: Vec<u8> = (0..=255u8)
.filter(|&b| BOUNDARY_BITS[b as usize] != 0)
.fold(Vec::new(), |mut seen: Vec<u8>, b| {
if !seen
.iter()
.any(|&s| BOUNDARY_BITS[s as usize] == BOUNDARY_BITS[b as usize])
{
seen.push(b);
}
seen
});
assert!(!specials.is_empty());
for len in 0..=41usize {
let plain = vec![neither; len];
for bit in [MAY_OPEN_BOUNDARY, ENDS_WORD_RUN] {
assert_eq!(any_byte_has(&plain, bit), model(&plain, bit), "plain {len}");
for &special in &specials {
for at in 0..len {
let mut bytes = plain.clone();
bytes[at] = special;
assert_eq!(
any_byte_has(&bytes, bit),
model(&bytes, bit),
"{special:#x} at {at} of {len}, bit {bit}"
);
for &other in &specials {
for second in at + 1..len {
bytes[second] = other;
assert_eq!(
any_byte_has(&bytes, bit),
model(&bytes, bit),
"{special:#x} at {at}, {other:#x} at {second} of {len}"
);
bytes[second] = neither;
}
}
}
}
}
}
}
fn split(run: &str, in_group_interior: bool) -> Option<Vec<String>> {
split_at(run, in_group_interior, !in_group_interior)
}
fn split_at(run: &str, colon_is_operator: bool, head_welds: bool) -> Option<Vec<String>> {
let arena = bumpalo::Bump::new();
split_value_run(run, colon_is_operator, head_welds, &arena).map(|tokens| {
tokens
.into_iter()
.map(|t| {
let text = &run[t.start..t.end];
if t.is_operator {
format!("op {text}")
} else {
text.to_owned()
}
})
.collect()
})
}
#[test]
fn a_single_token_run_does_not_split() {
for run in [
"red",
"1px",
"1.5",
"-1.5",
"1.5e-2",
"#FFF",
"100%",
"a/1.5",
"#FFF/1.5",
"#FFF-5",
"aa-bb",
"a/1.5/2.5",
"1.5--2.5",
"var(--a)",
"f(a, b)",
"(1.5)",
"url(a/1.5)",
r"a\+b",
r"a\/b",
"1.5/*c*/",
"'a/1.5'",
"f(a:1.5)",
"1.50//2.50",
"aa//1.50",
"///1.50",
"//x",
"U+26",
"u+4??",
"U+0025-00FF",
"[*]",
"[1.50]",
"[a*b]",
"[integer]",
"@a",
"@",
"@a*1.50",
"@a+1.50",
"@a-1.50",
"@a1.50",
"@a#FFF",
"@a.50",
"@a[1.50]c",
r"@a\62 c",
"+a",
"+a(2.50)",
"+#FFF",
"+a-1.50",
"+a/1.50",
r"+\61",
"+u+26",
] {
assert_eq!(split(run, false), None, "{run:?} is one token");
}
}
#[test]
fn division_and_subtraction_end_only_a_number_or_a_closed_run() {
assert_eq!(split("1.5/2.5", false).unwrap(), ["1.5", "op /", "2.5"]);
assert_eq!(split("12px/1.5", false).unwrap(), ["12px", "op /", "1.5"]);
assert_eq!(split("100%/2", false).unwrap(), ["100%", "op /", "2"]);
assert_eq!(split("1.5-2.5", false).unwrap(), ["1.5", "op -", "2.5"]);
assert_eq!(split("f(a)/1.5", false).unwrap(), ["f(a)", "op /", "1.5"]);
assert_eq!(split("(1.5)/2.5", false).unwrap(), ["(1.5)", "op /", "2.5"]);
assert_eq!(
split("1.5/2.5/aa", false).unwrap(),
["1.5", "op /", "2.5", "op /", "aa"]
);
}
#[test]
fn multiplication_and_addition_end_every_run() {
assert_eq!(split("1.5*2.5", false).unwrap(), ["1.5", "op *", "2.5"]);
assert_eq!(split("aa*bb", false).unwrap(), ["aa", "op *", "bb"]);
assert_eq!(split("aa+bb", false).unwrap(), ["aa", "op +", "bb"]);
assert_eq!(split("#FFF+5", false).unwrap(), ["#FFF", "op +", "5"]);
assert_eq!(split("aa*bb/cc", false).unwrap(), ["aa", "op *", "bb/cc"]);
}
#[test]
fn a_colon_splits_only_where_it_is_an_operator() {
assert_eq!(split("a:1.5", false), None);
assert_eq!(split("a:1.5", true).unwrap(), ["a", "op :", "1.5"]);
assert_eq!(split("1.5:2.5", true).unwrap(), ["1.5", "op :", "2.5"]);
assert_eq!(split("@a:1.5", true), None);
assert_eq!(split_at("a:1.5", true, true).unwrap(), ["a", "op :", "1.5"]);
assert_eq!(split_at("+a(2.5)", true, true), None);
assert_eq!(split_at("a:1.5", false, true), None);
assert_eq!(split_at(":a", true, true).unwrap(), ["op :", "a"]);
assert_eq!(
split_at("a::1.5", true, true).unwrap(),
["a", "op :", "op :", "1.5"]
);
assert_eq!(
split_at("a:+1.5", true, true).unwrap(),
["a", "op :", "+1.5"]
);
assert_eq!(
split_at("a{--b:1.5}c", true, true).unwrap(),
["a", "{", "--b", "op :", "1.5", "}", "c"]
);
assert_eq!(
split_at("https://a.fuz.dev/b", true, true).unwrap(),
["https", "op :", "//a.fuz.dev/b"]
);
}
#[test]
fn a_one_token_operator_run_is_a_member() {
assert_eq!(split("/", false).unwrap(), ["op /"]);
assert_eq!(split("*", false).unwrap(), ["op *"]);
assert_eq!(split("+", false).unwrap(), ["op +"]);
assert_eq!(split("-", false).unwrap(), ["op -"]);
assert_eq!(split("-[a]", false).unwrap(), ["op -", "[a]"]);
assert_eq!(split("-#a", false).unwrap(), ["op -", "#a"]);
assert_eq!(split("-.a", false).unwrap(), ["op -", ".a"]);
assert_eq!(split("-!a", false).unwrap(), ["op -", "!a"]);
assert_eq!(split(":", true).unwrap(), ["op :"]);
assert_eq!(split(":", false), None);
assert_eq!(split("1.5", false), None);
assert_eq!(split("-a", false), None);
assert_eq!(split("-1.5", false), None);
}
#[test]
fn a_hyphen_is_the_operator_wherever_it_opens_no_token_of_its_own() {
assert_eq!(split("-'x'", false).unwrap(), ["op -", "'x'"]);
assert_eq!(split(r#"-"x""#, false).unwrap(), ["op -", r#""x""#]);
assert_eq!(split("-(2.5)", false).unwrap(), ["op -", "(2.5)"]);
assert_eq!(split("-,a", false).unwrap(), ["op -", ",a"]);
assert_eq!(split("-)", false).unwrap(), ["op -", ")"]);
assert_eq!(split_at("-:a", true, true).unwrap(), ["op -", "op :", "a"]);
assert_eq!(split("-{1.5}", false).unwrap(), ["op -", "{", "1.5", "}"]);
assert_eq!(split("-@a", false).unwrap(), ["op -", "@a"]);
assert_eq!(split("-*a", false).unwrap(), ["op -", "op *", "a"]);
assert_eq!(split("-/2.5", false).unwrap(), ["op -", "op /", "2.5"]);
assert_eq!(split("-+a", false).unwrap(), ["op -", "+a"]);
assert_eq!(split("-+1.5", false).unwrap(), ["op -", "+1.5"]);
assert_eq!(split("-", false).unwrap(), ["op -"]);
assert_eq!(split("+-'x'", false).unwrap(), ["op +", "op -", "'x'"]);
assert_eq!(split("+-(2.5)", false).unwrap(), ["op +", "op -", "(2.5)"]);
assert_eq!(
split("1.5*-+a", false).unwrap(),
["1.5", "op *", "op -", "+a"]
);
assert_eq!(split("-webkit-box", false), None);
assert_eq!(split("-1.5", false), None);
assert_eq!(split("-.5", false), None);
assert_eq!(split(r"-\61", false), None);
assert_eq!(split("-_a", false), None);
assert_eq!(split("-\u{e9}", false), None);
assert_eq!(split("--a", false), None);
assert_eq!(split("+-[a]", false).unwrap(), ["op +", "op -", "[a]"]);
assert_eq!(split("+-a", false).unwrap(), ["op +", "-a"]);
assert_eq!(split("+[a]", false), None);
}
#[test]
fn an_at_word_is_a_member_of_its_own() {
assert_eq!(split("a@b", false).unwrap(), ["a", "@b"]);
assert_eq!(split("@a@b", false).unwrap(), ["@a", "@b"]);
assert_eq!(split("1.5@a", false).unwrap(), ["1.5", "@a"]);
assert_eq!(split("#FFF@a", false).unwrap(), ["#FFF", "@a"]);
assert_eq!(split("(1.5)@a", false).unwrap(), ["(1.5)", "@a"]);
assert_eq!(split("@a(2.5)", false).unwrap(), ["@a", "(2.5)"]);
assert_eq!(split("a@(2.5)", false).unwrap(), ["a", "@", "(2.5)"]);
assert_eq!(split("@(2.5)", false).unwrap(), ["@", "(2.5)"]);
assert_eq!(split("@a/1.5", false).unwrap(), ["@a", "op /", "1.5"]);
assert_eq!(split("1.5/@a", false).unwrap(), ["1.5", "op /", "@a"]);
assert_eq!(split("'x'@a", false).unwrap(), ["'x'", "@a"]);
assert_eq!(split("@a'x'", false).unwrap(), ["@a", "'x'"]);
assert_eq!(split("@a{1.5}", false).unwrap(), ["@a", "{", "1.5", "}"]);
assert_eq!(split(r"a\@b", false), None);
assert_eq!(split("-@a", false).unwrap(), ["op -", "@a"]);
assert_eq!(
split("1.5*-@a", false).unwrap(),
["1.5", "op *", "op -", "@a"]
);
assert_eq!(split("a-@b", false).unwrap(), ["a-", "@b"]);
assert_eq!(split("-a", false), None);
}
#[test]
fn a_brace_is_a_member_of_its_own() {
assert_eq!(
split("a{1.5}c(2.5)", false).unwrap(),
["a", "{", "1.5", "}", "c(2.5)"]
);
assert_eq!(split("{1.5}", false).unwrap(), ["{", "1.5", "}"]);
assert_eq!(split("{", false), None);
assert_eq!(split("}", false), None);
assert_eq!(
split("a{{1.5}}c", false).unwrap(),
["a", "{", "{", "1.5", "}", "}", "c"]
);
assert_eq!(split("a{}c", false).unwrap(), ["a", "{", "}", "c"]);
assert_eq!(
split("a{1.5}/2.5", false).unwrap(),
["a", "{", "1.5", "}", "op /", "2.5"]
);
assert_eq!(
split("a*{1.5}c", false).unwrap(),
["a", "op *", "{", "1.5", "}", "c"]
);
assert_eq!(
split("a{1.5*2.5}c", false).unwrap(),
["a", "{", "1.5", "op *", "2.5", "}", "c"]
);
assert_eq!(split("a[{]c", false), None);
assert_eq!(split("'{'", false), None);
assert_eq!(split(r"a\{b", false), None);
}
#[test]
fn a_plus_welds_at_the_head_and_after_an_operator() {
assert_eq!(split("+a*1.5", false).unwrap(), ["+a", "op *", "1.5"]);
assert_eq!(
split("1.5/+a(2.5)", false).unwrap(),
["1.5", "op /", "+a(2.5)"]
);
assert_eq!(split("1.5*+a", false).unwrap(), ["1.5", "op *", "+a"]);
assert_eq!(
split("1.5/++a", false).unwrap(),
["1.5", "op /", "op +", "+a"]
);
assert_eq!(split("++a", false).unwrap(), ["op +", "+a"]);
assert_eq!(split("+a+b", false).unwrap(), ["+a", "op +", "b"]);
assert_eq!(split("a+b", false).unwrap(), ["a", "op +", "b"]);
assert_eq!(split("+(2.5)", false).unwrap(), ["op +", "(2.5)"]);
assert_eq!(split("+'x'", false).unwrap(), ["op +", "'x'"]);
assert_eq!(split("+@a", false).unwrap(), ["op +", "@a"]);
assert_eq!(split("+-a", false).unwrap(), ["op +", "-a"]);
assert_eq!(split("+/1.5", false).unwrap(), ["op +", "op /", "1.5"]);
assert_eq!(split("1.5*+", false).unwrap(), ["1.5", "op *", "op +"]);
assert_eq!(
split_at("+a(2.5)", true, false).unwrap(),
["op +", "a(2.5)"]
);
assert_eq!(split_at("+a", false, false).unwrap(), ["op +", "a"]);
assert_eq!(
split_at("1.5/+a", true, false).unwrap(),
["1.5", "op /", "+a"]
);
assert_eq!(split("+1.5", false), None);
assert_eq!(split_at("+1.5", true, false), None);
}
#[test]
fn a_plus_welds_onto_a_doubled_dash_word() {
assert_eq!(split("+--a", false), None);
assert_eq!(split("+--", false), None);
assert_eq!(split("+----a", false), None);
assert_eq!(split("+--@a", false).unwrap(), ["+--", "@a"]);
assert_eq!(split("1.5*+--a", false).unwrap(), ["1.5", "op *", "+--a"]);
assert_eq!(split("++--a", false).unwrap(), ["op +", "+--a"]);
assert_eq!(split("+-a", false).unwrap(), ["op +", "-a"]);
assert_eq!(split("+-1.5", false).unwrap(), ["op +", "-1.5"]);
assert_eq!(split_at("+--a", true, false).unwrap(), ["op +", "--a"]);
assert_eq!(split_at("+--a", false, false).unwrap(), ["op +", "--a"]);
assert_eq!(split("1.5+--a", false).unwrap(), ["1.5", "op +", "--a"]);
}
#[test]
fn a_lone_slash_still_splits() {
assert_eq!(split("/1.5", false).unwrap(), ["op /", "1.5"]);
assert_eq!(split("1.5/", false).unwrap(), ["1.5", "op /"]);
}
#[test]
fn an_operand_opening_on_an_operator_byte_is_not_tagged_as_one() {
assert_eq!(split("1.5*-a", false).unwrap(), ["1.5", "op *", "-a"]);
assert_eq!(split("1.5*-1.5", false).unwrap(), ["1.5", "op *", "-1.5"]);
}
#[test]
fn a_trailing_hyphen_is_the_operator_in_both_positions() {
assert_eq!(split("-", false).unwrap(), ["op -"]);
assert_eq!(split("+-", false).unwrap(), ["op +", "op -"]);
assert_eq!(split("1.5/-", false).unwrap(), ["1.5", "op /", "op -"]);
assert_eq!(split("1.5*-", false).unwrap(), ["1.5", "op *", "op -"]);
assert_eq!(split_at("a:-", true, true).unwrap(), ["a", "op :", "op -"]);
}
#[test]
fn a_plus_does_not_weld_after_a_colon() {
assert_eq!(
split_at("a:+b", true, true).unwrap(),
["a", "op :", "op +", "b"]
);
assert_eq!(split_at(":+a", true, true).unwrap(), ["op :", "op +", "a"]);
assert_eq!(
split_at("a:+1.5", true, true).unwrap(),
["a", "op :", "+1.5"]
);
assert_eq!(
split_at("1.5/+b", true, true).unwrap(),
["1.5", "op /", "+b"]
);
}
#[test]
fn a_unicode_range_ends_the_member() {
assert_eq!(split("U+26-1.50", false).unwrap(), ["U+26-1", ".50"]);
}
#[test]
fn a_closed_run_ends_the_member() {
assert_eq!(split("(1.5)(2.5)", false).unwrap(), ["(1.5)", "(2.5)"]);
assert_eq!(split("f(1.5)g(2.5)", false).unwrap(), ["f(1.5)", "g(2.5)"]);
assert_eq!(split("(1.5)aa", false).unwrap(), ["(1.5)", "aa"]);
assert_eq!(split("1.5/(2.5)", false).unwrap(), ["1.5", "op /", "(2.5)"]);
}
#[test]
fn an_unbalanced_paren_swallows_the_run() {
assert_eq!(split("1.5/(2.5", false).unwrap(), ["1.5", "op /", "(2.5"]);
assert_eq!(split("(1.5", false), None);
}
}