use alloc::vec::Vec;
use crate::chars::{is_double_quote, is_single_quote, is_ws};
fn is_scalar_delimiter(c: char) -> bool {
c == ',' || c == '}' || c == ']' || c == ')' || c == ';' || is_ws(c)
}
fn is_value_start(c: char, next: Option<char>) -> bool {
match c {
'{' | '[' | '-' | '+' => true,
'.' => matches!(next, Some('0'..='9')),
c => {
c.is_ascii_digit()
|| is_double_quote(c)
|| (is_single_quote(c) && c != '\u{0060}' && c != '\u{00B4}')
}
}
}
#[must_use]
pub fn extract(input: &str) -> Option<&str> {
if let Some((start, end)) = fenced_block(input) {
return Some(input[start..end].trim());
}
let start = first_value_start(input)?;
let (_, end) = scan_value(input, start, false)?;
Some(input[start..end].trim())
}
#[must_use]
pub fn extract_partial(input: &str) -> &str {
match first_value_start(input) {
Some(start) => &input[start..],
None => "",
}
}
#[must_use]
pub fn extract_all(input: &str) -> Vec<&str> {
let mut values = Vec::new();
let mut pos = 0usize;
let mut bracket = first_bracket(input);
while pos < input.len() {
if bracket.is_some_and(|at| at < pos) {
bracket = first_bracket(&input[pos..]).map(|at| pos + at);
}
let start = match bracket {
Some(at) => at,
None => match first_scalar_start(&input[pos..]) {
Some(offset) => pos + offset,
None => break,
},
};
let Some((_, end)) = scan_value(input, start, false) else {
break;
};
values.push(input[start..end].trim());
pos = end.max(start + 1);
}
values
}
fn first_value_start(input: &str) -> Option<usize> {
first_bracket(input).or_else(|| first_scalar_start(input))
}
fn first_bracket(input: &str) -> Option<usize> {
input.bytes().position(|b| b == b'{' || b == b'[')
}
fn first_scalar_start(input: &str) -> Option<usize> {
let mut chars = input.char_indices().peekable();
while let Some((offset, c)) = chars.next() {
let next = chars.peek().map(|&(_, next)| next);
if is_value_start(c, next) {
return Some(offset);
}
}
None
}
fn fenced_block(input: &str) -> Option<(usize, usize)> {
let mut first = None;
let mut search = 0usize;
while let Some(offset) = input[search..].find("```") {
let open = search + offset + 3;
let line_end = input[open..]
.find('\n')
.map_or(input.len(), |o| open + o + 1);
let spec = input[open..line_end].trim();
let is_json = spec
.get(..4)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("json"));
let body_start = line_end.min(input.len());
let body_end = input[body_start..]
.find("```")
.map_or(input.len(), |o| body_start + o);
if body_end > body_start && !input[body_start..body_end].trim().is_empty() {
if is_json {
return Some((body_start, body_end));
}
first.get_or_insert((body_start, body_end));
}
if body_end <= search {
break;
}
search = body_end.saturating_add(3).min(input.len());
}
first
}
fn scan_value(input: &str, start: usize, partial: bool) -> Option<(usize, usize)> {
let first = input[start..].chars().next()?;
match first {
'{' | '[' => scan_container(input, start, partial),
c if is_double_quote(c) || is_single_quote(c) => {
scan_string(input, start, c, partial).map(|end| (start, end))
}
_ => {
let end = input[start..]
.char_indices()
.find(|(_, c)| is_scalar_delimiter(*c))
.map_or(input.len(), |(offset, _)| start + offset);
(end > start).then_some((start, end))
}
}
}
fn scan_container(input: &str, start: usize, partial: bool) -> Option<(usize, usize)> {
let bytes = input.as_bytes();
let mut depth = 0usize;
let mut quote: Option<char> = None;
let mut i = start;
while i < bytes.len() {
if let Some(open) = quote {
if open == '"' || open == '\'' {
i = crate::swar::find_either(bytes, i, open as u8, b'\\');
if i == bytes.len() {
break;
}
}
let c = char_at(input, i);
let next = i + c.len_utf8();
if c == '\\' && next < bytes.len() {
i = next + char_at(input, next).len_utf8();
continue;
}
if closes(open, c) {
quote = None;
}
i = next;
continue;
}
let c = char_at(input, i);
let next = i + c.len_utf8();
match c {
c if is_double_quote(c) || is_single_quote(c) => quote = Some(c),
'/' if matches!(bytes.get(next), Some(b'/' | b'*')) => {
i = comment_end(input, i);
continue;
}
'{' | '[' => depth += 1,
'}' | ']' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some((start, next));
}
}
_ => {}
}
i = next;
}
partial.then_some((start, input.len()))
}
fn char_at(input: &str, i: usize) -> char {
match input.as_bytes()[i] {
byte if byte.is_ascii() => char::from(byte),
_ => input[i..].chars().next().unwrap_or('\0'),
}
}
fn scan_string(input: &str, start: usize, open: char, partial: bool) -> Option<usize> {
let mut escaped = false;
for (offset, c) in input[start..].char_indices().skip(1) {
if escaped {
escaped = false;
continue;
}
if c == '\\' {
escaped = true;
continue;
}
if closes(open, c) {
return Some(start + offset + c.len_utf8());
}
}
partial.then_some(input.len())
}
fn closes(open: char, c: char) -> bool {
if open == '"' {
c == '"'
} else if open == '\'' {
c == '\''
} else if is_double_quote(open) {
is_double_quote(c) || c == '"'
} else {
is_single_quote(c) || c == '\''
}
}
fn comment_end(input: &str, at: usize) -> usize {
let closer = if input[at..].starts_with("//") {
"\n"
} else {
"*/"
};
input[at + 2..]
.find(closer)
.map_or(input.len(), |end| at + 2 + end + closer.len())
}