vtcode_commons/
json_extract.rs1#[must_use]
14pub fn first_json_block(text: &str) -> Option<&str> {
15 let (start, opening) = text.char_indices().find(|(_, ch)| matches!(ch, '{' | '['))?;
16 let mut stack = vec![opening];
17 let mut in_string = false;
18 let mut escaped = false;
19
20 for (offset, ch) in text.get(start + opening.len_utf8()..)?.char_indices() {
21 if in_string {
22 if escaped {
23 escaped = false;
24 } else if ch == '\\' {
25 escaped = true;
26 } else if ch == '"' {
27 in_string = false;
28 }
29 continue;
30 }
31
32 match ch {
33 '"' => in_string = true,
34 '{' | '[' => stack.push(ch),
35 '}' => {
36 if stack.pop() != Some('{') {
37 return None;
38 }
39 if stack.is_empty() {
40 let end = start + opening.len_utf8() + offset + ch.len_utf8();
41 return text.get(start..end);
42 }
43 }
44 ']' => {
45 if stack.pop() != Some('[') {
46 return None;
47 }
48 if stack.is_empty() {
49 let end = start + opening.len_utf8() + offset + ch.len_utf8();
50 return text.get(start..end);
51 }
52 }
53 _ => {}
54 }
55 }
56
57 None
58}
59
60#[cfg(test)]
61mod tests {
62 use super::first_json_block;
63
64 #[test]
65 fn finds_object_after_prose_and_fences() {
66 let text = "Here you go:\n```json\n{\"a\": 1}\n```";
67 assert_eq!(first_json_block(text), Some("{\"a\": 1}"));
68 }
69
70 #[test]
71 fn finds_array_block() {
72 assert_eq!(first_json_block("prefix [1, 2, 3] suffix"), Some("[1, 2, 3]"));
73 }
74
75 #[test]
76 fn ignores_braces_inside_strings() {
77 assert_eq!(first_json_block(r#"{"a": "not } here"}"#), Some(r#"{"a": "not } here"}"#));
78 }
79
80 #[test]
81 fn handles_mixed_nesting() {
82 assert_eq!(first_json_block(r#"x {"a": [1, {"b": 2}]} y"#), Some(r#"{"a": [1, {"b": 2}]}"#));
83 }
84
85 #[test]
86 fn rejects_mismatched_delimiters() {
87 assert_eq!(first_json_block("{\"a\": 1]"), None);
88 assert_eq!(first_json_block("[1, 2}"), None);
89 }
90
91 #[test]
92 fn returns_none_for_unterminated_or_missing_json() {
93 assert_eq!(first_json_block("no json here"), None);
94 assert_eq!(first_json_block("{\"a\": 1"), None);
95 assert_eq!(first_json_block(""), None);
96 }
97}