aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/json/json_manipulator.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 12:55:27 +0900
committernsfisis <nsfisis@gmail.com>2026-06-25 23:47:47 +0900
commite971877cc24563ae9983413849edde00601903b0 (patch)
tree4b2c0c8420efcb7e6e422fb78261524d9e8102ba /crates/shirabe/src/json/json_manipulator.rs
parente7897eb0136624466ef47526c9548dc56624351c (diff)
downloadphp-shirabe-e971877cc24563ae9983413849edde00601903b0.tar.gz
php-shirabe-e971877cc24563ae9983413849edde00601903b0.tar.zst
php-shirabe-e971877cc24563ae9983413849edde00601903b0.zip
feat(json): port JsonManipulator key/sub-node methods to hand-written scanner
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/json/json_manipulator.rs')
-rw-r--r--crates/shirabe/src/json/json_manipulator.rs189
1 files changed, 85 insertions, 104 deletions
diff --git a/crates/shirabe/src/json/json_manipulator.rs b/crates/shirabe/src/json/json_manipulator.rs
index 30ceedb..73b70b9 100644
--- a/crates/shirabe/src/json/json_manipulator.rs
+++ b/crates/shirabe/src/json/json_manipulator.rs
@@ -11,6 +11,7 @@ use shirabe_php_shim::{
};
use crate::json::JsonFile;
+use crate::json::json_grammar::{self, ValueKind};
use crate::repository::PlatformRepository;
#[derive(Debug)]
@@ -659,18 +660,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>(?&object))(?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::Object,
+ ) {
+ 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)?.is_null()
|| json_decode(&children, false)?.as_bool() == Some(false)
@@ -678,47 +678,34 @@ impl JsonManipulator {
return Ok(false);
}
- // child exists
- let child_regex = format!(
- "{{{}(?P<start>\"{}\"\\s*:\\s*)(?P<content>(?&json))(?P<end>,?)}}x",
- Self::DEFINES,
- preg_quote(&name_owned, None)
+ // child exists. The child pattern looks for the raw `"name"` key token within the children
+ // object and rewrites its value in place; the surrounding `"name"\s*:\s*` and optional comma
+ // are preserved as-is.
+ let child_key = format!("\"{}\"", name_owned);
+ let child = json_grammar::find_top_level_key(
+ children.as_bytes(),
+ child_key.as_bytes(),
+ ValueKind::Json,
);
- let mut child_match_map: IndexMap<String, String> = IndexMap::new();
- if Preg::is_match_named(&child_regex, &children, &mut child_match_map) {
- let value_capture = value.clone();
- let sub_name_capture = sub_name.clone();
- let formatter = ManipulatorFormatter {
- newline: self.newline.clone(),
- indent: self.indent.clone(),
- };
- children = Preg::replace_callback(
- &child_regex,
- move |matches: &IndexMap<CaptureKey, String>| -> String {
- let content_key = CaptureKey::ByName("content".to_string());
- let start_key = CaptureKey::ByName("start".to_string());
- let end_key = CaptureKey::ByName("end".to_string());
- let mut value_local = value_capture.clone();
- if sub_name_capture.is_some() && matches.get(&content_key).is_some() {
- let mut cur_val = json_decode(matches.get(&content_key).unwrap(), true)
- .unwrap_or(PhpMixed::Null);
- if !is_array(&cur_val) {
- cur_val = PhpMixed::Array(IndexMap::new());
- }
- if let Some(arr) = cur_val.as_array_mut() {
- arr.insert(sub_name_capture.clone().unwrap(), value_local.clone());
- }
- value_local = cur_val;
- }
-
- format!(
- "{}{}{}",
- matches.get(&start_key).cloned().unwrap_or_default(),
- formatter.format(&value_local, 1, false).unwrap_or_default(),
- matches.get(&end_key).cloned().unwrap_or_default()
- )
- },
- &children,
+ if let Some(cm) = child {
+ let content_str = children[cm.value_pos..cm.value_end].to_string();
+ let mut value_local = value.clone();
+ if sub_name.is_some() {
+ let mut cur_val = json_decode(&content_str, true).unwrap_or(PhpMixed::Null);
+ if !is_array(&cur_val) {
+ cur_val = PhpMixed::Array(IndexMap::new());
+ }
+ if let Some(arr) = cur_val.as_array_mut() {
+ arr.insert(sub_name.clone().unwrap(), value_local.clone());
+ }
+ value_local = cur_val;
+ }
+ let formatted = self.format(&value_local, 1, false)?;
+ children = format!(
+ "{}{}{}",
+ &children[..cm.value_pos],
+ formatted,
+ &children[cm.value_end..]
);
} else {
let mut leading_match: IndexMap<String, String> = IndexMap::new();
@@ -809,23 +796,7 @@ impl JsonManipulator {
}
}
- 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)
}
@@ -1395,27 +1366,29 @@ impl JsonManipulator {
let content = self.format(&content, 0, false)?;
// key exists already
- let regex = format!(
- "{{{}^(?P<start>\\s*\\{{\\s*(?:(?&string)\\s*:\\s*(?&json)\\s*,\\s*)*?)(?P<key>{}\\s*:\\s*(?&json))(?P<end>.*)}}sx",
- Self::DEFINES,
- preg_quote(&JsonFile::encode(key), None)
- );
- let mut matches: IndexMap<String, String> = IndexMap::new();
- if decoded.as_array().and_then(|a| a.get(key)).is_some()
- && Preg::is_match_named(&regex, &self.contents, &mut matches)
- {
+ let encoded_key = JsonFile::encode(key);
+ let key_match = if decoded.as_array().and_then(|a| a.get(key)).is_some() {
+ json_grammar::find_top_level_key(
+ self.contents.as_bytes(),
+ encoded_key.as_bytes(),
+ ValueKind::Json,
+ )
+ } else {
+ None
+ };
+ if let Some(m) = key_match {
// invalid match due to un-regexable content, abort
- let key_match = matches.get("key").cloned().unwrap_or_default();
- if json_decode(&format!("{{{}}}", key_match), false)?.is_null() {
+ let key_capture = &self.contents[m.key_pos..m.value_end];
+ if json_decode(&format!("{{{}}}", key_capture), false)?.is_null() {
return Ok(false);
}
self.contents = format!(
"{}{}: {}{}",
- matches.get("start").cloned().unwrap_or_default(),
- JsonFile::encode(key),
+ &self.contents[..m.key_pos],
+ encoded_key,
content,
- matches.get("end").cloned().unwrap_or_default()
+ &self.contents[m.value_end..]
);
return Ok(true);
@@ -1475,22 +1448,30 @@ impl JsonManipulator {
}
// key exists already
- let regex = format!(
- "{{{}^(?P<start>\\s*\\{{\\s*(?:(?&string)\\s*:\\s*(?&json)\\s*,\\s*)*?)(?P<removal>{}\\s*:\\s*(?&json))\\s*,?\\s*(?P<end>.*)}}sx",
- Self::DEFINES,
- preg_quote(&JsonFile::encode(key), None)
+ let encoded_key = JsonFile::encode(key);
+ let key_match = json_grammar::find_top_level_key(
+ self.contents.as_bytes(),
+ encoded_key.as_bytes(),
+ ValueKind::Json,
);
- let mut matches: IndexMap<String, String> = IndexMap::new();
- if Preg::is_match_named(&regex, &self.contents, &mut matches) {
+ if let Some(m) = key_match {
// invalid match due to un-regexable content, abort
- let removal = matches.get("removal").cloned().unwrap_or_default();
+ let removal = &self.contents[m.key_pos..m.value_end];
if json_decode(&format!("{{{}}}", removal), false)?.is_null() {
return Ok(false);
}
+ // The pattern consumes `\s*,?\s*` between the removed key and the rest (`end`).
+ let cb = self.contents.as_bytes();
+ let mut e = json_grammar::skip_ws(cb, m.value_end);
+ if cb.get(e) == Some(&b',') {
+ e += 1;
+ }
+ e = json_grammar::skip_ws(cb, e);
+
// check that we are not leaving a dangling comma on the previous line if the last line was removed
- let mut start = matches.get("start").cloned().unwrap_or_default();
- let end = matches.get("end").cloned().unwrap_or_default();
+ let mut start = self.contents[..m.key_pos].to_string();
+ let end = self.contents[e..].to_string();
if Preg::is_match3("#,\\s*$#", &start, None) && Preg::is_match3("#^\\}$#", &end, None) {
start = rtrim(
&Preg::replace("#,(\\s*)$#", "$1", &start),
@@ -1517,24 +1498,24 @@ impl JsonManipulator {
return Ok(true);
}
- let regex = format!(
- "{{{}^(?P<start>\\s*\\{{\\s*(?:(?&string)\\s*:\\s*(?&json)\\s*,\\s*)*?{}\\s*:\\s*)(?P<removal>\\{{(?P<removal_space>\\s*+)\\}})(?P<end>\\s*,?\\s*.*)}}sx",
- Self::DEFINES,
- preg_quote(&JsonFile::encode(key), None)
- );
- let mut matches: IndexMap<String, String> = IndexMap::new();
- if Preg::is_match_named(&regex, &self.contents, &mut matches) {
+ // Match the key only when its value is an empty object `{ <space> }`.
+ let encoded_key = JsonFile::encode(key);
+ let cb = self.contents.as_bytes();
+ let key_match = json_grammar::find_top_level_key(cb, encoded_key.as_bytes(), ValueKind::Object)
+ .filter(|m| json_grammar::skip_ws(cb, m.value_pos + 1) == m.value_end - 1);
+ if let Some(m) = key_match {
// invalid match due to un-regexable content, abort
- let removal = matches.get("removal").cloned().unwrap_or_default();
- if json_decode(&removal, false)?.as_bool() == Some(false) {
+ let removal = &self.contents[m.value_pos..m.value_end];
+ if json_decode(removal, false)?.as_bool() == Some(false) {
return Ok(false);
}
+ let removal_space = &self.contents[m.value_pos + 1..m.value_end - 1];
self.contents = format!(
"{}[{}]{}",
- matches.get("start").cloned().unwrap_or_default(),
- matches.get("removal_space").cloned().unwrap_or_default(),
- matches.get("end").cloned().unwrap_or_default()
+ &self.contents[..m.value_pos],
+ removal_space,
+ &self.contents[m.value_end..]
);
return Ok(true);