mod frames;
mod head;
mod spec;
#[cfg(test)]
mod tests;
mod unit;
use super::syntax::{
find_close_tag, ident_length, match_block, match_tool_call_block, parse_ts_call_from,
skip_heredoc_body, strip_thinking_tags, CloseScan,
};
use crate::text_index::TextIndex;
pub(crate) use head::call_head;
pub(crate) use spec::ScanSpec;
pub(crate) use unit::{Unit, UnitPayload};
pub(crate) struct ScanOutput {
pub(crate) source: String,
pub(crate) units: Vec<Unit>,
}
impl ScanOutput {
pub(crate) fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"source": self.source,
"units": self.units.iter().map(Unit::to_json).collect::<Vec<_>>(),
})
}
}
pub(crate) fn scan_units(text: &str, spec: &ScanSpec) -> ScanOutput {
let source = if spec.strip_thinking {
strip_thinking_tags(text).into_owned()
} else {
text.to_string()
};
let units = Scan::new(&source, spec).run();
ScanOutput { source, units }
}
struct Scan<'a> {
src: &'a str,
spec: &'a ScanSpec,
index: TextIndex,
call_tag_opens: Vec<String>,
cursor: usize,
last_block_end: usize,
units: Vec<Unit>,
}
impl<'a> Scan<'a> {
fn new(src: &'a str, spec: &'a ScanSpec) -> Self {
Self {
src,
spec,
index: TextIndex::build(src),
call_tag_opens: spec.call_tags.iter().map(|tag| format!("<{tag}")).collect(),
cursor: 0,
last_block_end: 0,
units: Vec::new(),
}
}
fn emit(&mut self, end: usize, payload: UnitPayload) {
assert!(
end > self.cursor && end <= self.src.len(),
"scan unit must advance the cursor within the source: {}..{end} of {}",
self.cursor,
self.src.len()
);
self.units.push(Unit {
start: self.cursor,
end,
payload,
});
self.cursor = end;
}
fn emit_block(&mut self, end: usize, payload: UnitPayload) {
self.emit(end, payload);
self.last_block_end = self.cursor;
}
fn skip_whitespace(&mut self) {
let bytes = self.src.as_bytes();
while self.cursor < bytes.len() && bytes[self.cursor].is_ascii_whitespace() {
self.cursor += 1;
}
}
fn run(mut self) -> Vec<Unit> {
while self.cursor < self.src.len() {
self.skip_whitespace();
if self.cursor >= self.src.len() {
break;
}
self.step();
}
self.units
}
fn step(&mut self) {
let cursor = self.cursor;
let bytes = self.src.as_bytes();
if self.step_reserved_opener() {
return;
}
if bytes[cursor] != b'<' {
let end = self.text_run_end(cursor);
self.emit(
end,
UnitPayload::Text {
text: self.src[cursor..end].to_string(),
},
);
return;
}
let harmony = &self.spec.harmony;
if let Some(end) = frames::tool_call_line(harmony, self.src, cursor) {
self.emit_block(
end,
UnitPayload::HarmonyLine {
text: self.src[cursor..end].to_string(),
},
);
return;
}
if let Some(end) = frames::corrupted_opener(harmony, &self.call_tag_opens, self.src, cursor)
.or_else(|| frames::frame_marker(harmony, &self.call_tag_opens, self.src, cursor))
{
self.emit_block(end, UnitPayload::HarmonySkip);
return;
}
let adjacent_to_block = self.last_block_end > 0
&& cursor >= self.last_block_end
&& self.src[self.last_block_end..cursor]
.chars()
.all(char::is_whitespace);
if (!adjacent_to_block && !self.index.is_line_leading(self.src, cursor))
|| self.index.inside_markdown_fence(cursor)
{
let end = self.src[cursor..]
.find('\n')
.map_or(self.src.len(), |offset| cursor + offset);
self.emit(
end,
UnitPayload::FencedLine {
text: self.src[cursor..end].to_string(),
},
);
return;
}
if self.step_call_tag(cursor) {
return;
}
for tag in &self.spec.block_tags {
if let Some((body, end)) = match_block(self.src, cursor, tag) {
let payload = UnitPayload::Block {
tag: tag.clone(),
body: body.to_string(),
head: call_head(body),
reserved: false,
};
self.emit_block(end, payload);
return;
}
}
if let Some((name, arguments, end)) = self.angle_wrapped_call(cursor) {
self.emit_block(end, UnitPayload::AngleCall { name, arguments });
return;
}
for markup in &self.spec.markup_openers {
if self.src[cursor..].starts_with(&markup.opener) {
let end = self.src[cursor..]
.find(&markup.close)
.map_or(self.src.len(), |offset| {
cursor + offset + markup.close.len()
});
let payload = UnitPayload::Markup {
opener: markup.opener.clone(),
text: self.src[cursor..end].to_string(),
};
self.emit_block(end, payload);
return;
}
}
for tag in &self.spec.call_tags {
let close = format!("</{tag}>");
if self.src[cursor..].starts_with(&close) {
let end = cursor + close.len();
self.emit_block(end, UnitPayload::StrayClose { tag: tag.clone() });
return;
}
}
for tag in &self.spec.wrapper_tags {
for form in [format!("<{tag}>"), format!("</{tag}>")] {
if self.src[cursor..].starts_with(&form) {
let end = cursor + form.len();
self.emit_block(end, UnitPayload::Wrapper { tag: tag.clone() });
return;
}
}
}
let mut end = cursor + 1;
while end < bytes.len() && bytes[end] != b'>' && bytes[end] != b'\n' {
end += 1;
}
if end < bytes.len() && bytes[end] == b'>' {
end += 1;
}
let raw = self.src[cursor..end].to_string();
let name = tag_fragment_name(&raw);
self.emit(end, UnitPayload::Tag { raw, name });
}
fn step_reserved_opener(&mut self) -> bool {
let cursor = self.cursor;
let Some(opener) = self
.spec
.reserved_openers
.iter()
.find(|opener| is_truncated_reserved_opener(&self.src[cursor..], opener))
else {
return false;
};
if !self.index.is_line_leading(self.src, cursor) || self.index.inside_markdown_fence(cursor)
{
return false;
}
let Some(canonical_tag) = self.spec.call_tags.first() else {
return false;
};
let tail = &self.src[cursor + opener.len()..];
let canonical_open = format!("<{canonical_tag}>");
let synthetic = format!("{canonical_open}{tail}");
if let Some((body, after)) = match_tool_call_block(&synthetic, 0, canonical_tag) {
let end = cursor + opener.len() + (after - canonical_open.len());
let payload = UnitPayload::Block {
tag: canonical_tag.clone(),
body: body.to_string(),
head: call_head(body),
reserved: true,
};
self.emit_block(end, payload);
return true;
}
let payload = UnitPayload::UnclosedBlock {
tag: canonical_tag.clone(),
body: tail.to_string(),
head: call_head(tail),
reserved: true,
};
self.emit_block(self.src.len(), payload);
true
}
fn step_call_tag(&mut self, cursor: usize) -> bool {
for tag in &self.spec.call_tags {
if let Some((body, end)) = match_tool_call_block(self.src, cursor, tag) {
let payload = UnitPayload::Block {
tag: tag.clone(),
body: body.to_string(),
head: call_head(body),
reserved: false,
};
self.emit_block(end, payload);
return true;
}
}
for tag in &self.spec.call_tags {
let open = format!("<{tag}>");
if !self.src[cursor..].starts_with(&open) {
continue;
}
let close = format!("</{tag}>");
if matches!(
find_close_tag(self.src, cursor + open.len(), &close),
CloseScan::Found(_)
) {
continue;
}
let body = &self.src[cursor + open.len()..];
let payload = UnitPayload::UnclosedBlock {
tag: tag.clone(),
body: body.to_string(),
head: call_head(body),
reserved: false,
};
self.emit_block(self.src.len(), payload);
return true;
}
false
}
fn text_run_end(&self, start: usize) -> usize {
let bytes = self.src.as_bytes();
let mut probe = start;
loop {
while probe < bytes.len() && bytes[probe] != b'<' {
probe += 1;
}
if probe + 1 < bytes.len() && bytes[probe] == b'<' && bytes[probe + 1] == b'<' {
if let Some(after) = skip_heredoc_body(self.src, probe) {
probe = after;
continue;
}
}
if let Some(after) = frames::message_marker_after_tool_call_header(
&self.spec.harmony,
self.src,
start,
probe,
) {
probe = after;
continue;
}
return probe;
}
}
fn angle_wrapped_call(&self, cursor: usize) -> Option<(String, serde_json::Value, usize)> {
let bytes = self.src.as_bytes();
if bytes.get(cursor) != Some(&b'<') {
return None;
}
let name_start = cursor + 1;
let name_len = ident_length(&bytes[name_start..])?;
if bytes.get(name_start + name_len) != Some(&b'(') {
return None;
}
let name = &self.src[name_start..name_start + name_len];
if !self.spec.known_tools.contains(name) {
return None;
}
let (arguments, consumed) =
parse_ts_call_from(&self.src[name_start..], name.to_string()).ok()?;
let mut end = name_start + consumed;
while matches!(bytes.get(end), Some(b' ') | Some(b'\t')) {
end += 1;
}
if bytes.get(end) == Some(&b'>') {
end += 1;
}
Some((name.to_string(), arguments, end))
}
}
fn is_truncated_reserved_opener(rest: &str, opener: &str) -> bool {
let Some(tail) = rest.strip_prefix(opener) else {
return false;
};
match opener.chars().next_back() {
Some(last) => !tail.starts_with(last),
None => false,
}
}
fn tag_fragment_name(fragment: &str) -> String {
let inner = fragment.trim_start().trim_start_matches('<');
let inner = inner.strip_prefix('/').unwrap_or(inner);
match ident_length(inner.as_bytes()) {
Some(len) => inner[..len].to_string(),
None => String::new(),
}
}