use super::super::syntax::scan_heredoc;
use super::BlockError;
pub(super) struct VerbatimSegment {
pub opener: String,
pub value: String,
}
pub(super) struct ToolBlock {
pub header: String,
pub segments: Vec<VerbatimSegment>,
pub error: Option<BlockError>,
pub block_end: usize,
}
pub(super) fn is_heredoc_opener(value: &str) -> bool {
let rest = match value.strip_prefix("<<") {
Some(rest) => rest,
None => return false,
};
let quote = rest.chars().next().filter(|c| *c == '\'' || *c == '"');
let rest = quote.map_or(rest, |_| &rest[1..]);
let tag_len = rest
.bytes()
.take_while(|b| b.is_ascii_alphanumeric() || *b == b'_')
.count();
if tag_len == 0 {
return false;
}
let mut after = &rest[tag_len..];
if let Some(q) = quote {
after = match after.strip_prefix(q) {
Some(after) => after,
None => return false,
};
}
match after.strip_prefix(':') {
Some(digits) => !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()),
None => after.is_empty(),
}
}
fn is_counted_heredoc_opener(value: &str) -> bool {
is_heredoc_opener(value)
&& value.rsplit_once(':').is_some_and(|(_, count)| {
!count.is_empty() && count.bytes().all(|byte| byte.is_ascii_digit())
})
}
pub(super) fn consume_tool_block(src: &str, body_start: usize, close_fence: &str) -> ToolBlock {
let mut header = String::new();
let mut segments: Vec<VerbatimSegment> = Vec::new();
let mut error: Option<BlockError> = None;
let mut in_bodies = false;
let mut cursor = body_start;
let block_end = loop {
if cursor >= src.len() {
break src.len();
}
let (line, line_end, next) = line_at(src, cursor);
if line.trim() == close_fence {
break next;
}
let indent = line.len() - line.trim_start().len();
let trimmed = &line[indent..];
if trimmed.starts_with("<<") {
in_bodies = true;
let opener_off = cursor + indent;
match scan_heredoc(src, opener_off) {
Ok(span) => {
segments.push(VerbatimSegment {
opener: trimmed.trim_end().to_string(),
value: src[span.content].to_string(),
});
cursor = advance_past_line(src, span.end);
}
Err(_) => {
error.get_or_insert_with(|| BlockError::InvalidJson {
detail: format!(
"verbatim heredoc `{}` did not close on the line its `:N` count \
declared; recount the body lines and re-emit the whole call",
trimmed.trim_end()
),
});
cursor = next;
}
}
continue;
}
if !in_bodies && super::fence_open_kind(line).is_some() {
break cursor;
}
header.push_str(&src[cursor..line_end]);
header.push('\n');
cursor = next;
};
ToolBlock {
header,
segments,
error,
block_end,
}
}
fn advance_past_line(src: &str, pos: usize) -> usize {
match src[pos..].find('\n') {
Some(rel) => pos + rel + 1,
None => src.len(),
}
}
pub(super) fn line_at(src: &str, start: usize) -> (&str, usize, usize) {
match src[start..].find('\n') {
Some(rel) => {
let nl = start + rel;
(&src[start..nl], nl, nl + 1)
}
None => (&src[start..], src.len(), src.len()),
}
}
pub(super) fn apply_segments(
args: &mut serde_json::Map<String, serde_json::Value>,
pool: &mut Vec<VerbatimSegment>,
) -> Result<(), BlockError> {
let openers: Vec<(String, String)> = args
.iter()
.filter_map(|(key, value)| {
value
.as_str()
.filter(|value| is_heredoc_opener(value))
.map(|value| (key.clone(), value.to_string()))
})
.collect();
for (_, opener) in &openers {
if !is_counted_heredoc_opener(opener) {
continue;
}
let required = openers
.iter()
.filter(|(_, candidate)| candidate == opener)
.count();
let available = pool
.iter()
.filter(|segment| &segment.opener == opener)
.count();
if available < required {
return Err(BlockError::InvalidJson {
detail: format!(
"counted verbatim declaration `{opener}` has {available} matching bodies, expected {required}"
),
});
}
}
for (key, opener) in openers {
if let Some(index) = pool.iter().position(|segment| segment.opener == opener) {
let segment = pool.remove(index);
args.insert(key, serde_json::Value::String(segment.value));
}
}
Ok(())
}