pub fn strip_code_fences(input: &str) -> &str {
let trimmed = input.trim();
let Some(after_open) = trimmed.strip_prefix("```") else {
return input;
};
let Some(newline) = after_open.find('\n') else {
return input; };
let body = after_open[newline + 1..].trim_end();
let body = body.strip_suffix("```").unwrap_or(body);
body.trim()
}
pub fn extract_json(input: &str) -> Option<&str> {
let blocks = extract_json_blocks(input);
for block in &blocks {
if serde_json::from_str::<serde_json::Value>(block).is_ok() {
return Some(block);
}
}
for block in &blocks {
let sanitized = crate::sanitize::sanitize_json(block);
if serde_json::from_str::<serde_json::Value>(&sanitized).is_ok() {
return Some(block);
}
}
blocks.first().copied()
}
pub fn extract_json_blocks(input: &str) -> Vec<&str> {
let mut blocks = Vec::new();
for segment in fenced_segments(input) {
scan_blocks(segment, &mut blocks);
}
if !blocks.is_empty() {
return blocks;
}
scan_blocks(strip_code_fences(input), &mut blocks);
blocks
}
fn fenced_segments(input: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut rest = input;
while let Some(open) = rest.find("```") {
let after_open = &rest[open + 3..];
let Some(newline) = after_open.find('\n') else {
break;
};
let body = &after_open[newline + 1..];
match body.find("```") {
Some(close) => {
segments.push(&body[..close]);
rest = &body[close + 3..];
}
None => {
segments.push(body); break;
}
}
}
segments
}
fn scan_blocks<'a>(s: &'a str, blocks: &mut Vec<&'a str>) {
let mut pos = 0;
while let Some(offset) = s[pos..].find(['{', '[']) {
let start = pos + offset;
match balanced_end(&s[start..]) {
Some(len) => {
blocks.push(&s[start..start + len]);
pos = start + len;
}
None => {
blocks.push(s[start..].trim_end());
break;
}
}
}
}
fn balanced_end(s: &str) -> Option<usize> {
let mut depth = 0usize;
let mut in_string = false;
let mut escaped = false;
for (i, c) in s.char_indices() {
if in_string {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '"' {
in_string = false;
}
continue;
}
match c {
'"' => in_string = true,
'{' | '[' => depth += 1,
'}' | ']' => {
depth = depth.saturating_sub(1);
if depth == 0 {
return Some(i + c.len_utf8());
}
}
_ => {}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_fence_with_language_tag() {
let input = "```json\n{\"a\": 1}\n```";
assert_eq!(strip_code_fences(input), "{\"a\": 1}");
}
#[test]
fn test_strip_fence_without_language_tag() {
let input = "```\n[1, 2]\n```";
assert_eq!(strip_code_fences(input), "[1, 2]");
}
#[test]
fn test_strip_fence_missing_closing() {
let input = "```json\n{\"a\": 1";
assert_eq!(strip_code_fences(input), "{\"a\": 1");
}
#[test]
fn test_strip_fence_not_fenced() {
let input = "{\"a\": 1}";
assert_eq!(strip_code_fences(input), "{\"a\": 1}");
}
#[test]
fn test_extract_from_prose() {
let input = "Here is the JSON you asked for: {\"a\": 1} — enjoy!";
assert_eq!(extract_json(input), Some("{\"a\": 1}"));
}
#[test]
fn test_extract_from_fenced_prose() {
let input = "Sure!\n\n```json\n{\"type\": \"AddDerive\"}\n```\n\nAnything else?";
assert_eq!(extract_json(input), Some("{\"type\": \"AddDerive\"}"));
}
#[test]
fn test_extract_array_payload() {
let input = "Result: [1, 2, 3] done";
assert_eq!(extract_json(input), Some("[1, 2, 3]"));
}
#[test]
fn test_extract_prefers_parseable_block() {
let input = "In {this} example the payload is {\"a\": 1}.";
assert_eq!(extract_json(input), Some("{\"a\": 1}"));
}
#[test]
fn test_extract_prefers_sanitizable_block() {
let input = "In {this} example the payload is {\"a\": 1,}.";
assert_eq!(extract_json(input), Some("{\"a\": 1,}"));
}
#[test]
fn test_extract_truncated_payload() {
let input = "Here you go: {\"a\": {\"b\": 1";
assert_eq!(extract_json(input), Some("{\"a\": {\"b\": 1"));
}
#[test]
fn test_extract_none_when_no_json() {
assert_eq!(extract_json("no json here"), None);
}
#[test]
fn test_extract_string_aware_braces() {
let input = r#"{"note": "use } carefully", "a": 1}"#;
assert_eq!(extract_json(input), Some(input));
}
#[test]
fn test_extract_escaped_quote_in_string() {
let input = r#"{"note": "say \"hi\" {ok}", "a": 1}"#;
assert_eq!(extract_json(input), Some(input));
}
#[test]
fn test_extract_multiple_blocks() {
let input = r#"First: {"a": 1} and second: {"b": 2}"#;
let blocks = extract_json_blocks(input);
assert_eq!(blocks, vec![r#"{"a": 1}"#, r#"{"b": 2}"#]);
}
#[test]
fn test_extract_blocks_includes_truncated_tail() {
let input = r#"{"a": 1} then {"b": 2"#;
let blocks = extract_json_blocks(input);
assert_eq!(blocks, vec![r#"{"a": 1}"#, r#"{"b": 2"#]);
}
#[test]
fn test_full_pipeline_extract_sanitize() {
let raw = "```json\n{\"items\": [1, 2,], \"done\": true\n```";
let extracted = extract_json(raw).unwrap();
let sanitized = crate::sanitize::sanitize_json(extracted);
let value: serde_json::Value = serde_json::from_str(&sanitized).unwrap();
assert_eq!(value["items"][1], 2);
assert_eq!(value["done"], true);
}
}