use std::borrow::Cow;
const OPEN: char = '⟨';
const CLOSE: char = '⟩';
const SEMI: char = '¦';
pub(in crate::shell::acceptance) fn text_of(span: &str) -> String {
span.replace(OPEN, "(")
.replace(CLOSE, ")")
.replace(SEMI, ";")
}
pub(in crate::shell::acceptance) fn skeleton(source: &str) -> String {
let bytes = source.as_bytes();
let mut out = String::new();
let mut i = 0;
while i < bytes.len() {
let (next, emit) = token(bytes, i);
if emit != " " || !out.ends_with(' ') {
out.push_str(&emit);
}
i = next;
}
out.replace(" .", ".")
}
fn token(bytes: &[u8], at: usize) -> (usize, Cow<'static, str>) {
let two = (bytes.get(at), bytes.get(at + 1));
match two {
(Some(b'/'), Some(b'/')) => (line_end(bytes, at), Cow::Borrowed(" ")),
(Some(b'/'), Some(b'*')) => (block_end(bytes, at + 2), Cow::Borrowed(" ")),
_ => literal_or_char(bytes, at),
}
}
fn literal_or_char(bytes: &[u8], at: usize) -> (usize, Cow<'static, str>) {
if let Some((end, body)) = string_at(bytes, at) {
return (end, Cow::Owned(body));
}
let emit = match bytes.get(at) {
Some(b'\'') => return char_at(bytes, at),
Some(c) if c.is_ascii_whitespace() => " ",
Some(b'(') => "(",
Some(b')') => ")",
Some(b'.') => ".",
Some(b'"') => "\"",
Some(c) => kept(*c),
None => "",
};
(at + 1, Cow::Borrowed(emit))
}
fn kept(c: u8) -> &'static str {
const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_:;";
ALPHABET
.as_bytes()
.iter()
.position(|a| *a == c)
.and_then(|i| ALPHABET.get(i..=i))
.unwrap_or(" ")
}
fn string_at(bytes: &[u8], at: usize) -> Option<(usize, String)> {
let mut i = at;
if bytes.get(i) == Some(&b'b') {
i += 1;
}
if bytes.get(i) == Some(&b'r') {
i += 1;
let mut hashes = 0usize;
while bytes.get(i) == Some(&b'#') {
hashes += 1;
i += 1;
}
if bytes.get(i) != Some(&b'"') {
return None;
}
let (end, body) = raw_end(bytes, i + 1, hashes);
return Some((end, quoted(bytes, i + 1, body)));
}
if bytes.get(i) != Some(&b'"') {
return None;
}
let (end, body) = plain_end(bytes, i + 1);
Some((end, quoted(bytes, i + 1, body)))
}
fn quoted(bytes: &[u8], from: usize, to: usize) -> String {
let mut out = String::from("\"");
for c in String::from_utf8_lossy(bytes.get(from..to).unwrap_or_default()).chars() {
match c {
'(' => out.push(OPEN),
')' => out.push(CLOSE),
';' => out.push(SEMI),
'"' | '\\' | ' ' | '\n' | '\r' | '\t' => {
if !out.ends_with(' ') {
out.push(' ');
}
}
_ => out.push(c),
}
}
out.push('"');
out
}
fn plain_end(bytes: &[u8], from: usize) -> (usize, usize) {
let mut i = from;
while let Some(c) = bytes.get(i) {
match c {
b'\\' => i += 2,
b'"' => return (i + 1, i),
_ => i += 1,
}
}
(i, i)
}
fn raw_end(bytes: &[u8], from: usize, hashes: usize) -> (usize, usize) {
let mut i = from;
while let Some(c) = bytes.get(i) {
i += 1;
if *c != b'"' {
continue;
}
let closed = (0..hashes).all(|h| bytes.get(i + h) == Some(&b'#'));
if closed {
return (i + hashes, i - 1);
}
}
(i, i)
}
fn char_at(bytes: &[u8], at: usize) -> (usize, Cow<'static, str>) {
let mut i = at + 1;
if bytes.get(i) == Some(&b'\\') {
i += 1;
}
i += 1;
while bytes
.get(i)
.is_some_and(|c| *c & 0b1100_0000 == 0b1000_0000)
{
i += 1;
}
if bytes.get(i) == Some(&b'\'') {
return (i + 1, Cow::Borrowed(" "));
}
(at + 1, Cow::Borrowed(" "))
}
fn line_end(bytes: &[u8], at: usize) -> usize {
let mut i = at;
while bytes.get(i).is_some_and(|c| *c != b'\n') {
i += 1;
}
i + 1
}
fn block_end(bytes: &[u8], from: usize) -> usize {
let mut i = from;
while let Some(c) = bytes.get(i) {
if *c == b'*' && bytes.get(i + 1) == Some(&b'/') {
return i + 2;
}
i += 1;
}
i
}