aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 12:57:19 +0900
committernsfisis <nsfisis@gmail.com>2026-06-25 23:47:47 +0900
commitaebe252f47fa5c9b2963fe2f42516e1b54b5c79f (patch)
tree85b39edf95c8cb2f878d99645ab4e56b0447caab
parente971877cc24563ae9983413849edde00601903b0 (diff)
downloadphp-shirabe-aebe252f47fa5c9b2963fe2f42516e1b54b5c79f.tar.gz
php-shirabe-aebe252f47fa5c9b2963fe2f42516e1b54b5c79f.tar.zst
php-shirabe-aebe252f47fa5c9b2963fe2f42516e1b54b5c79f.zip
feat(json): port add_list_item/insert_list_item to hand-written scanner
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
-rw-r--r--crates/shirabe/src/json/json_manipulator.rs142
1 files changed, 50 insertions, 92 deletions
diff --git a/crates/shirabe/src/json/json_manipulator.rs b/crates/shirabe/src/json/json_manipulator.rs
index 73b70b9..fa31fff 100644
--- a/crates/shirabe/src/json/json_manipulator.rs
+++ b/crates/shirabe/src/json/json_manipulator.rs
@@ -1048,18 +1048,17 @@ impl JsonManipulator {
}
// main node content not match-able
- let node_regex = format!(
- "{{{}^(?P<start> \\s* \\{{ \\s* (?: (?&string) \\s* : (?&json) \\s* , \\s* )*?{}\\s*:\\s*)(?P<content>(?&array))(?P<end>.*)}}sx",
- Self::DEFINES,
- preg_quote(&JsonFile::encode(main_node), None)
- );
-
- let mut match_map: IndexMap<String, String> = IndexMap::new();
- if !Preg::is_match_named(&node_regex, &self.contents, &mut match_map) {
- return Ok(false);
- }
-
- let mut children = match_map.get("content").cloned().unwrap_or_default();
+ let node = match json_grammar::find_top_level_key(
+ self.contents.as_bytes(),
+ JsonFile::encode(main_node).as_bytes(),
+ ValueKind::Array,
+ ) {
+ Some(node) => node,
+ None => return Ok(false),
+ };
+ let node_start = self.contents[..node.value_pos].to_string();
+ let node_end = self.contents[node.value_end..].to_string();
+ let mut children = self.contents[node.value_pos..node.value_end].to_string();
// invalid match due to un-regexable content, abort
if json_decode(&children, false)?.as_bool() == Some(false) {
return Ok(false);
@@ -1140,23 +1139,7 @@ impl JsonManipulator {
.into());
}
- let children_owned = children;
- self.contents = Preg::replace_callback(
- &node_regex,
- move |m: &IndexMap<CaptureKey, String>| -> String {
- format!(
- "{}{}{}",
- m.get(&CaptureKey::ByName("start".to_string()))
- .cloned()
- .unwrap_or_default(),
- children_owned,
- m.get(&CaptureKey::ByName("end".to_string()))
- .cloned()
- .unwrap_or_default()
- )
- },
- &self.contents,
- );
+ self.contents = format!("{}{}{}", node_start, children, node_end);
Ok(true)
}
@@ -1199,76 +1182,51 @@ impl JsonManipulator {
}
// main node content not match-able
- let node_regex = format!(
- "{{{}^(?P<start> \\s* \\{{ \\s* (?: (?&string) \\s* : (?&json) \\s* , \\s* )*?{}\\s*:\\s*)(?P<content>(?&array))(?P<end>.*)}}sx",
- Self::DEFINES,
- preg_quote(&JsonFile::encode(main_node), None)
- );
-
- let mut match_map: IndexMap<String, String> = IndexMap::new();
- if !Preg::is_match_named(&node_regex, &self.contents, &mut match_map) {
- return Ok(false);
- }
-
- let mut children = match_map.get("content").cloned().unwrap_or_default();
+ let node = match json_grammar::find_top_level_key(
+ self.contents.as_bytes(),
+ JsonFile::encode(main_node).as_bytes(),
+ ValueKind::Array,
+ ) {
+ Some(node) => node,
+ None => return Ok(false),
+ };
+ let node_start = self.contents[..node.value_pos].to_string();
+ let node_end = self.contents[node.value_end..].to_string();
+ let children = self.contents[node.value_pos..node.value_end].to_string();
// invalid match due to un-regexable content, abort
if json_decode(&children, false)?.as_bool() == Some(false) {
return Ok(false);
}
- let list_skip_to_item_regex = format!(
- "{{{}^(?P<start>\\[\\s*((?&json)\\s*+,\\s*?){{{}}})(?P<space_before_item>(\\s*))(?P<end>.*)}}sx",
- Self::DEFINES,
- index.max(0)
- );
-
- let value_capture = value.clone();
- let formatter = ManipulatorFormatter {
- newline: self.newline.clone(),
- indent: self.indent.clone(),
+ // Skip past the first `index` array items (each `value <ws> , <lazy ws>`) and insert the new
+ // value before the item at `index`, preserving the whitespace that precedes it.
+ let new_children = {
+ let cb = children.as_bytes();
+ let mut i = json_grammar::skip_ws(cb, 1); // '[' then \s*
+ for _ in 0..index.max(0) {
+ i = json_grammar::scan_value(cb, i)
+ .ok_or_else(|| anyhow::anyhow!("insert_list_item: malformed list"))?;
+ i = json_grammar::skip_ws(cb, i);
+ if cb.get(i) != Some(&b',') {
+ return Ok(false);
+ }
+ i += 1;
+ }
+ let start_end = i;
+ let sbi_end = json_grammar::skip_ws(cb, i);
+ let space_before_item = &children[start_end..sbi_end];
+ let formatted = self.format(&value, 1, false)?;
+ format!(
+ "{}{}{},{}{}",
+ &children[..start_end],
+ space_before_item,
+ formatted,
+ space_before_item,
+ &children[sbi_end..]
+ )
};
- children = Preg::replace_callback(
- &list_skip_to_item_regex,
- move |m: &IndexMap<CaptureKey, String>| -> String {
- format!(
- "{}{}{},{}{}",
- m.get(&CaptureKey::ByName("start".to_string()))
- .cloned()
- .unwrap_or_default(),
- m.get(&CaptureKey::ByName("space_before_item".to_string()))
- .cloned()
- .unwrap_or_default(),
- formatter
- .format(&value_capture, 1, false)
- .unwrap_or_default(),
- m.get(&CaptureKey::ByName("space_before_item".to_string()))
- .cloned()
- .unwrap_or_default(),
- m.get(&CaptureKey::ByName("end".to_string()))
- .cloned()
- .unwrap_or_default()
- )
- },
- &children,
- );
- let children_owned = children;
- self.contents = Preg::replace_callback(
- &node_regex,
- move |m: &IndexMap<CaptureKey, String>| -> String {
- format!(
- "{}{}{}",
- m.get(&CaptureKey::ByName("start".to_string()))
- .cloned()
- .unwrap_or_default(),
- children_owned,
- m.get(&CaptureKey::ByName("end".to_string()))
- .cloned()
- .unwrap_or_default()
- )
- },
- &self.contents,
- );
+ self.contents = format!("{}{}{}", node_start, new_children, node_end);
Ok(true)
}