use super::super::ts_value_parser::TsValueParser;
pub(super) fn strip_thinking_tags(text: &str) -> std::borrow::Cow<'_, str> {
if !text.contains("<think>") && !text.contains("</think>") {
return std::borrow::Cow::Borrowed(text);
}
let mut result = text.to_string();
while let Some(start) = result.find("<think>") {
if let Some(end) = result[start..].find("</think>") {
result.replace_range(start..start + end + "</think>".len(), "");
} else {
result.replace_range(start..start + "<think>".len(), "");
}
}
while result.contains("</think>") {
result = result.replace("</think>", "");
}
std::borrow::Cow::Owned(result)
}
pub(super) fn match_block<'a>(src: &'a str, start: usize, tag: &str) -> Option<(&'a str, usize)> {
let open = format!("<{tag}>");
if !src[start..].starts_with(&open) {
return None;
}
let body_start = start + open.len();
let close = format!("</{tag}>");
let close_idx = src[body_start..].find(&close)?;
let body_end = body_start + close_idx;
let after = body_end + close.len();
Some((&src[body_start..body_end], after))
}
pub(super) fn render_canonical_call(name: &str, args: &serde_json::Value) -> String {
let rendered_args = serde_json::to_string_pretty(args).unwrap_or_else(|_| "{}".to_string());
format!("{name}({rendered_args})")
}
pub(super) fn preview_str(s: &str, max: usize) -> String {
let chars: Vec<char> = s.chars().collect();
if chars.len() <= max {
return s.to_string();
}
let kept: String = chars.into_iter().take(max).collect();
format!("{kept}…")
}
pub(super) fn has_object_literal_arg_start(text: &str, open_paren_idx: usize) -> bool {
let bytes = text.as_bytes();
let mut idx = open_paren_idx;
while idx < bytes.len() && (bytes[idx] == b' ' || bytes[idx] == b'\t') {
idx += 1;
}
bytes.get(idx) == Some(&b'{')
}
pub(super) fn unwrap_exact_code_wrapper(text: &str) -> Option<&str> {
let trimmed = text.trim();
if let Some(rest) = trimmed.strip_prefix("```") {
let newline = rest.find('\n')?;
let after_opener = &rest[newline + 1..];
let inner = after_opener.strip_suffix("```")?;
return Some(inner.trim());
}
let inner = trimmed.strip_prefix('`')?.strip_suffix('`')?;
if inner.contains('`') {
return None;
}
Some(inner.trim())
}
pub(super) fn collapse_blank_lines(text: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut newline_run = 0usize;
for ch in text.chars() {
if ch == '\n' {
newline_run += 1;
if newline_run <= 2 {
out.push(ch);
}
} else {
newline_run = 0;
out.push(ch);
}
}
out
}
pub(super) fn strip_empty_fences(text: &str) -> String {
let re = regex::Regex::new(r"(?m)^[ \t]*```[^\n]*\n\s*```[ \t]*\n?").unwrap();
re.replace_all(text, "").to_string()
}
pub(super) fn skip_heredoc_body(src: &str, start: usize) -> Option<usize> {
let bytes = src.as_bytes();
if bytes.get(start) != Some(&b'<') || bytes.get(start + 1) != Some(&b'<') {
return None;
}
let mut pos = start + 2;
let has_quote = matches!(bytes.get(pos), Some(b'\'') | Some(b'"'));
let quote_char = bytes.get(pos).copied();
if has_quote {
pos += 1;
}
let tag_start = pos;
while let Some(byte) = bytes.get(pos) {
if byte.is_ascii_alphanumeric() || *byte == b'_' {
pos += 1;
} else {
break;
}
}
if pos == tag_start {
return None;
}
let tag = &src[tag_start..pos];
if has_quote && bytes.get(pos).copied() == quote_char {
pos += 1;
}
if bytes.get(pos) == Some(&b'\r') {
pos += 1;
}
if bytes.get(pos) != Some(&b'\n') {
return None;
}
pos += 1;
while pos < bytes.len() {
let line_start = pos;
while let Some(byte) = bytes.get(pos) {
if *byte == b'\n' {
break;
}
pos += 1;
}
let line = &src[line_start..pos];
let leading_ws_len = line.len() - line.trim_start().len();
let after_ws = &line[leading_ws_len..];
if let Some(rest) = after_ws.strip_prefix(tag) {
let at_word_boundary = rest
.chars()
.next()
.is_none_or(|ch| !(ch.is_ascii_alphanumeric() || ch == '_'));
if at_word_boundary {
return Some(line_start + leading_ws_len + tag.len());
}
}
if bytes.get(pos) == Some(&b'\n') {
pos += 1;
} else {
return None;
}
}
None
}
pub(crate) fn ident_length(bytes: &[u8]) -> Option<usize> {
if bytes.is_empty() {
return None;
}
let first = bytes[0];
if !(first.is_ascii_alphabetic() || first == b'_' || first == b'$') {
return None;
}
let mut i = 1;
while i < bytes.len() {
let byte = bytes[i];
if byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$' {
i += 1;
} else {
break;
}
}
Some(i)
}
pub(crate) fn parse_ts_call_from(
text: &str,
name: String,
) -> Result<(serde_json::Value, usize), String> {
let bytes = text.as_bytes();
let paren_open = name.len();
if bytes.get(paren_open) != Some(&b'(') {
return Err(format!(
"TOOL CALL PARSE ERROR: `{name}(` expected immediately after the tool name."
));
}
let mut parser = TsValueParser::new(&text[paren_open + 1..]);
parser.skip_ws_and_comments();
let args_value = if parser.peek() == Some(b')') {
serde_json::Value::Object(serde_json::Map::new())
} else {
parser.parse_value().map_err(|error| {
format!(
"TOOL CALL PARSE ERROR: `{name}(...)` — {error}. \
Tool arguments must be a TypeScript object literal: `{{ key: value, key: value }}`."
)
})?
};
parser.skip_ws_and_comments();
if parser.peek() != Some(b')') {
return Err(format!(
"TOOL CALL PARSE ERROR: `{name}(...)` — missing closing `)`. \
Every tool call must be a complete TypeScript expression."
));
}
let consumed_in_parser = parser.position();
let total_consumed = paren_open + 1 + consumed_in_parser + 1;
match args_value {
serde_json::Value::Object(map) => Ok((serde_json::Value::Object(map), total_consumed)),
other => Err(format!(
"TOOL CALL PARSE ERROR: `{name}(...)` — expected an object literal argument, \
got `{}`. Wrap the value in braces: `{name}({{ key: value }})`.",
other
)),
}
}