diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-14 05:06:27 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-14 05:08:54 +0900 |
| commit | ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243 (patch) | |
| tree | a955d0cf277cee8adcfeea8f35c672f0a35afddf | |
| parent | 4116323e9d55745c17b2d8008affa3394b726f6b (diff) | |
| download | php-shirabe-ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243.tar.gz php-shirabe-ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243.tar.zst php-shirabe-ef9118c788c1cbb22ca7721b6a9e40c2bf2fe243.zip | |
feat(pcre): implement preg_* functions
Replace the todo!() stubs backing the Preg class and the standalone
preg_* shim helpers with regex-crate implementations.
| -rw-r--r-- | crates/shirabe-external-packages/src/composer/pcre/preg.rs | 347 | ||||
| -rw-r--r-- | crates/shirabe-php-shim/src/lib.rs | 261 |
2 files changed, 531 insertions, 77 deletions
diff --git a/crates/shirabe-external-packages/src/composer/pcre/preg.rs b/crates/shirabe-external-packages/src/composer/pcre/preg.rs index f14cb03..14be7a2 100644 --- a/crates/shirabe-external-packages/src/composer/pcre/preg.rs +++ b/crates/shirabe-external-packages/src/composer/pcre/preg.rs @@ -388,73 +388,334 @@ pub enum CaptureKey { ByName(String), } -pub fn preg_last_error() -> i64 { - todo!() -} - -pub fn preg_last_error_msg() -> String { - todo!() -} - // Returns Some(0|1) on success or None when the underlying preg_match returned // false. Unmatched groups are reported as None (PREG_UNMATCHED_AS_NULL). pub fn preg_match( - _pattern: &str, - _subject: &str, - _matches: Option<&mut IndexMap<CaptureKey, Option<String>>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap<CaptureKey, Option<String>>>, + flags: i64, + offset: usize, ) -> Option<i64> { - todo!() + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; + let caps = re.captures_at(subject, offset); + + if let Some(out) = matches { + out.clear(); + if let Some(caps) = &caps { + let names: Vec<Option<&str>> = re.capture_names().collect(); + *out = single_match_map(caps, &names, unmatched_as_null); + } + } + + Some(if caps.is_some() { 1 } else { 0 }) } pub fn preg_match_all( - _pattern: &str, - _subject: &str, - _matches: Option<&mut IndexMap<CaptureKey, Vec<Option<String>>>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap<CaptureKey, Vec<Option<String>>>>, + flags: i64, + offset: usize, ) -> Option<i64> { - todo!() + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; + let group_count = re.captures_len(); + let names: Vec<Option<&str>> = re.capture_names().collect(); + + // PREG_PATTERN_ORDER: one column per group, one row per match occurrence. + let mut groups: Vec<Vec<Option<String>>> = vec![Vec::new(); group_count]; + let mut count = 0i64; + for caps in re.captures_iter(&subject[offset..]) { + count += 1; + for (g, column) in groups.iter_mut().enumerate() { + let value = caps.get(g).map(|m| m.as_str().to_string()); + column.push(if unmatched_as_null { + value + } else { + Some(value.unwrap_or_default()) + }); + } + } + + if let Some(out) = matches { + out.clear(); + for (g, column) in groups.into_iter().enumerate() { + if let Some(Some(name)) = names.get(g) { + out.insert(CaptureKey::ByName((*name).to_string()), column.clone()); + } + out.insert(CaptureKey::ByIndex(g), column); + } + } + + Some(count) } pub fn preg_match_all_offset_capture( - _pattern: &str, - _subject: &str, - _matches: Option<&mut IndexMap<CaptureKey, Vec<(Option<String>, i64)>>>, - _flags: i64, - _offset: usize, + pattern: &str, + subject: &str, + matches: Option<&mut IndexMap<CaptureKey, Vec<(Option<String>, i64)>>>, + flags: i64, + offset: usize, ) -> Option<i64> { - todo!() + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; + let group_count = re.captures_len(); + let names: Vec<Option<&str>> = re.capture_names().collect(); + + let mut groups: Vec<Vec<(Option<String>, i64)>> = vec![Vec::new(); group_count]; + let mut count = 0i64; + for caps in re.captures_iter(&subject[offset..]) { + count += 1; + for (g, column) in groups.iter_mut().enumerate() { + let entry = match caps.get(g) { + Some(m) => (Some(m.as_str().to_string()), (m.start() + offset) as i64), + None if unmatched_as_null => (None, -1), + None => (Some(String::new()), -1), + }; + column.push(entry); + } + } + + if let Some(out) = matches { + out.clear(); + for (g, column) in groups.into_iter().enumerate() { + if let Some(Some(name)) = names.get(g) { + out.insert(CaptureKey::ByName((*name).to_string()), column.clone()); + } + out.insert(CaptureKey::ByIndex(g), column); + } + } + + Some(count) } pub fn preg_replace( - _pattern: &str, - _replacement: &str, - _subject: &str, - _limit: i64, - _count: Option<&mut usize>, + pattern: &str, + replacement: &str, + subject: &str, + limit: i64, + count: Option<&mut usize>, ) -> Option<String> { - todo!() + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let limit = if limit < 0 { + usize::MAX + } else { + limit as usize + }; + + let mut out: Vec<u8> = Vec::new(); + let mut last = 0usize; + let mut n = 0usize; + for caps in re.captures_iter(subject) { + if n >= limit { + break; + } + let m = caps.get(0).unwrap(); + out.extend_from_slice(&subject.as_bytes()[last..m.start()]); + expand_php_replacement(replacement, &caps, &mut out); + last = m.end(); + n += 1; + } + out.extend_from_slice(&subject.as_bytes()[last..]); + + if let Some(count) = count { + *count = n; + } + Some(String::from_utf8_lossy(&out).into_owned()) } pub fn preg_replace_callback<F: FnMut(&IndexMap<CaptureKey, Option<String>>) -> String>( - _pattern: &str, - _callback: F, - _subject: &str, - _limit: i64, - _count: Option<&mut usize>, - _flags: i64, + pattern: &str, + mut callback: F, + subject: &str, + limit: i64, + count: Option<&mut usize>, + flags: i64, ) -> Option<String> { - todo!() + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let unmatched_as_null = flags & PREG_UNMATCHED_AS_NULL != 0; + let names: Vec<Option<&str>> = re.capture_names().collect(); + let limit = if limit < 0 { + usize::MAX + } else { + limit as usize + }; + + let mut out: Vec<u8> = Vec::new(); + let mut last = 0usize; + let mut n = 0usize; + for caps in re.captures_iter(subject) { + if n >= limit { + break; + } + let m = caps.get(0).unwrap(); + out.extend_from_slice(&subject.as_bytes()[last..m.start()]); + let map = single_match_map(&caps, &names, unmatched_as_null); + out.extend_from_slice(callback(&map).as_bytes()); + last = m.end(); + n += 1; + } + out.extend_from_slice(&subject.as_bytes()[last..]); + + if let Some(count) = count { + *count = n; + } + Some(String::from_utf8_lossy(&out).into_owned()) +} + +pub fn preg_split(pattern: &str, subject: &str, limit: i64, flags: i64) -> Option<Vec<String>> { + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let no_empty = flags & PREG_SPLIT_NO_EMPTY != 0; + let delim_capture = flags & PREG_SPLIT_DELIM_CAPTURE != 0; + // `limit` counts the resulting pieces; a non-positive value means no limit. + let max_delims = if limit > 0 { + (limit as usize).saturating_sub(1) + } else { + usize::MAX + }; + + let mut result: Vec<String> = Vec::new(); + let mut push = |s: &str, result: &mut Vec<String>| { + if !(no_empty && s.is_empty()) { + result.push(s.to_string()); + } + }; + + let mut last = 0usize; + let mut delims = 0usize; + for caps in re.captures_iter(subject) { + if delims >= max_delims { + break; + } + let m = caps.get(0).unwrap(); + push(&subject[last..m.start()], &mut result); + if delim_capture { + // Mirror preg_match: trailing unmatched groups are dropped, interior + // unmatched groups are emitted as "". + if let Some(last_g) = (1..caps.len()).rev().find(|&g| caps.get(g).is_some()) { + for g in 1..=last_g { + push(caps.get(g).map(|x| x.as_str()).unwrap_or(""), &mut result); + } + } + } + last = m.end(); + delims += 1; + } + push(&subject[last..], &mut result); + + Some(result) } -pub fn preg_split(_pattern: &str, _subject: &str, _limit: i64, _flags: i64) -> Option<Vec<String>> { - todo!() +pub fn preg_grep(pattern: &str, array: &[&str], flags: i64) -> Option<Vec<String>> { + let re = shirabe_php_shim::compile_php_pattern(pattern).ok()?; + let invert = flags & PREG_GREP_INVERT != 0; + Some( + array + .iter() + .filter(|s| re.is_match(s) != invert) + .map(|s| s.to_string()) + .collect(), + ) } -pub fn preg_grep(_pattern: &str, _array: &[&str], _flags: i64) -> Option<Vec<String>> { - todo!() +// Builds a single match's `$matches` map with both named and numbered keys +// (the named key precedes its number). With PREG_UNMATCHED_AS_NULL, every group +// is present and non-participating ones are None; otherwise classic semantics +// apply: trailing unmatched groups are dropped and interior ones become "". +fn single_match_map( + caps: ®ex::Captures, + names: &[Option<&str>], + unmatched_as_null: bool, +) -> IndexMap<CaptureKey, Option<String>> { + let mut out = IndexMap::new(); + let group_count = caps.len(); + let last_participating = (0..group_count).rev().find(|&i| caps.get(i).is_some()); + + for i in 0..group_count { + let m = caps.get(i); + if !unmatched_as_null && m.is_none() { + if let Some(last) = last_participating { + if i > last { + break; + } + } + } + let value = if unmatched_as_null { + m.map(|m| m.as_str().to_string()) + } else { + Some(m.map(|m| m.as_str().to_string()).unwrap_or_default()) + }; + if let Some(Some(name)) = names.get(i) { + out.insert(CaptureKey::ByName((*name).to_string()), value.clone()); + } + out.insert(CaptureKey::ByIndex(i), value); + } + out +} + +// Expands a PHP preg replacement template against `caps`, appending bytes to +// `out`. Backreferences are written as `$1`, `${1}`, `\1` or `\\1`; a literal +// `$` or `\` not forming a reference is emitted verbatim. Out-of-range or +// non-participating groups expand to nothing. +fn expand_php_replacement(template: &str, caps: ®ex::Captures, out: &mut Vec<u8>) { + let bytes = template.as_bytes(); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'\\' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => { + let (group, consumed) = replacement_group(&bytes[i + 1..]); + if let Some(m) = caps.get(group) { + out.extend_from_slice(m.as_str().as_bytes()); + } + i += 1 + consumed; + } + b'\\' if i + 1 < bytes.len() && bytes[i + 1] == b'\\' => { + out.push(b'\\'); + i += 2; + } + b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'{' => { + let rest = &bytes[i + 2..]; + match rest.iter().position(|&b| b == b'}') { + Some(c) if c > 0 && rest[..c].iter().all(|b| b.is_ascii_digit()) => { + let group: usize = + std::str::from_utf8(&rest[..c]).unwrap().parse().unwrap(); + if let Some(m) = caps.get(group) { + out.extend_from_slice(m.as_str().as_bytes()); + } + i += 2 + c + 1; + } + _ => { + out.push(b'$'); + i += 1; + } + } + } + b'$' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => { + let (group, consumed) = replacement_group(&bytes[i + 1..]); + if let Some(m) = caps.get(group) { + out.extend_from_slice(m.as_str().as_bytes()); + } + i += 1 + consumed; + } + b => { + out.push(b); + i += 1; + } + } + } +} + +// Reads up to two leading ASCII digits as a PHP backreference group number. +fn replacement_group(bytes: &[u8]) -> (usize, usize) { + let mut group = 0usize; + let mut consumed = 0usize; + while consumed < 2 && consumed < bytes.len() && bytes[consumed].is_ascii_digit() { + group = group * 10 + (bytes[consumed] - b'0') as usize; + consumed += 1; + } + (group, consumed) } /// Panics if a pattern is invalid instead of throwing a PcreException. diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 4a0a3d9..00973ac 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -1041,24 +1041,57 @@ pub fn json_encode<T: serde::Serialize + ?Sized>(_value: &T) -> Option<String> { todo!() } -pub fn preg_quote(_str: &str, _delimiter: Option<char>) -> String { - todo!() +pub fn preg_quote(str: &str, delimiter: Option<char>) -> String { + const SPECIAL: &str = ".\\+*?[^]$(){}=!<>|:-#"; + let mut out = String::new(); + for c in str.chars() { + if c == '\0' { + out.push_str("\\000"); + } else if SPECIAL.contains(c) || Some(c) == delimiter { + out.push('\\'); + out.push(c); + } else { + out.push(c); + } + } + out } // Returns 1 on match, 0 on no match; populates matches[0]=full match, matches[1..]=captures. // Optional groups that did not participate in the match are stored as None. -pub fn preg_match(_pattern: &str, _subject: &str, _matches: &mut Vec<Option<String>>) -> i64 { - todo!() +pub fn preg_match(pattern: &str, subject: &str, matches: &mut Vec<Option<String>>) -> i64 { + let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}")); + matches.clear(); + match re.captures(subject) { + Some(caps) => { + for g in 0..caps.len() { + matches.push(caps.get(g).map(|m| m.as_str().to_string())); + } + 1 + } + None => 0, + } } // Returns Some(result) on success, None on error. -pub fn preg_replace(_pattern: &str, _replacement: &str, _subject: &str) -> Option<String> { - todo!() +pub fn preg_replace(pattern: &str, replacement: &str, subject: &str) -> Option<String> { + let re = compile_php_pattern(pattern).ok()?; + let mut out: Vec<u8> = Vec::new(); + let mut last = 0; + for caps in re.captures_iter(subject) { + let m = caps.get(0).unwrap(); + out.extend_from_slice(&subject.as_bytes()[last..m.start()]); + php_replacement_expand(replacement, &caps, &mut out); + last = m.end(); + } + out.extend_from_slice(&subject.as_bytes()[last..]); + Some(String::from_utf8_lossy(&out).into_owned()) } // Returns Some(parts) on success, None on error. -pub fn preg_split(_pattern: &str, _subject: &str) -> Option<Vec<String>> { - todo!() +pub fn preg_split(pattern: &str, subject: &str) -> Option<Vec<String>> { + let re = compile_php_pattern(pattern).ok()?; + Some(php_split_impl(&re, subject)) } pub fn dirname(_path: &str) -> String { @@ -2904,40 +2937,92 @@ pub fn exit(status: i64) -> ! { std::process::exit(status as i32); } -pub fn preg_match_all(_pattern: &str, _subject: &str) -> Vec<Vec<String>> { - todo!() +// PREG_PATTERN_ORDER: the outer vec is indexed by capture group, the inner by +// match occurrence. Non-participating groups are reported as "". +pub fn preg_match_all(pattern: &str, subject: &str) -> Vec<Vec<String>> { + let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}")); + let group_count = re.captures_len(); + let mut groups: Vec<Vec<String>> = vec![Vec::new(); group_count]; + for caps in re.captures_iter(subject) { + for g in 0..group_count { + groups[g].push( + caps.get(g) + .map(|m| m.as_str().to_string()) + .unwrap_or_default(), + ); + } + } + groups } pub fn preg_match_all_simple( - _pattern: &str, - _subject: &str, - _matches: &mut Vec<Vec<String>>, + pattern: &str, + subject: &str, + matches: &mut Vec<Vec<String>>, ) -> anyhow::Result<i64> { - todo!() + let re = compile_php_pattern(pattern)?; + let group_count = re.captures_len(); + let mut groups: Vec<Vec<String>> = vec![Vec::new(); group_count]; + let mut count = 0i64; + for caps in re.captures_iter(subject) { + count += 1; + for g in 0..group_count { + groups[g].push( + caps.get(g) + .map(|m| m.as_str().to_string()) + .unwrap_or_default(), + ); + } + } + *matches = groups; + Ok(count) } +// PREG_SET_ORDER: the outer vec is indexed by match occurrence, the inner by +// capture group (a classic `$matches` row). pub fn preg_match_all_set_order( - _pattern: &str, - _subject: &str, - _matches: &mut Vec<Vec<String>>, + pattern: &str, + subject: &str, + matches: &mut Vec<Vec<String>>, ) -> anyhow::Result<i64> { - todo!() + let re = compile_php_pattern(pattern)?; + let mut rows: Vec<Vec<String>> = Vec::new(); + for caps in re.captures_iter(subject) { + rows.push(php_match_row(&caps)); + } + let count = rows.len() as i64; + *matches = rows; + Ok(count) } pub fn preg_match_offset( - _pattern: &str, - _subject: &str, - _matches: &mut Vec<String>, + pattern: &str, + subject: &str, + matches: &mut Vec<String>, _flags: i64, - _offset: i64, + offset: i64, ) -> bool { - todo!() + let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}")); + match re.captures_at(subject, offset as usize) { + Some(caps) => { + *matches = php_match_row(&caps); + true + } + None => { + matches.clear(); + false + } + } } -pub fn preg_match_groups(_pattern: &str, _subject: &str) -> Option<Vec<String>> { - todo!() +pub fn preg_match_groups(pattern: &str, subject: &str) -> Option<Vec<String>> { + let re = compile_php_pattern(pattern).ok()?; + let caps = re.captures(subject)?; + Some(php_match_row(&caps)) } -pub fn preg_grep(_pattern: &str, _input: &Vec<String>) -> Vec<String> { - todo!() +pub fn preg_grep(pattern: &str, input: &Vec<String>) -> Vec<String> { + let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}")); + input.iter().filter(|s| re.is_match(s)).cloned().collect() } -pub fn preg_split_chars(_pattern: &str, _subject: &str) -> Vec<String> { - todo!() +pub fn preg_split_chars(pattern: &str, subject: &str) -> Vec<String> { + let re = compile_php_pattern(pattern).unwrap_or_else(|e| panic!("invalid regex: {e}")); + php_split_impl(&re, subject) } #[derive(Debug, Default)] @@ -2955,7 +3040,7 @@ impl PregOffsetCaptureMatches { // lookaround, backreferences) are not supported by `regex` and must be avoided // in the caller's pattern. // TODO(phase-c): replace with a faithful PCRE engine to restore full semantics. -fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> { +pub fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> { let delimiter = pattern .chars() .next() @@ -2981,6 +3066,100 @@ fn compile_php_pattern(pattern: &str) -> anyhow::Result<regex::Regex> { Ok(regex::Regex::new(&translated)?) } +// Expands a PHP preg replacement template against `caps`, appending bytes to +// `out`. Backreferences are written as `$1`, `${1}`, `\1` or `\\1`; a literal +// `$` or `\` not forming a reference is emitted verbatim. Out-of-range or +// non-participating groups expand to nothing. +fn php_replacement_expand(template: &str, caps: ®ex::Captures, out: &mut Vec<u8>) { + let bytes = template.as_bytes(); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'\\' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => { + let (group, consumed) = php_replacement_group(&bytes[i + 1..]); + if let Some(m) = caps.get(group) { + out.extend_from_slice(m.as_str().as_bytes()); + } + i += 1 + consumed; + } + b'\\' if i + 1 < bytes.len() && bytes[i + 1] == b'\\' => { + out.push(b'\\'); + i += 2; + } + b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'{' => { + let rest = &bytes[i + 2..]; + match rest.iter().position(|&b| b == b'}') { + Some(c) if c > 0 && rest[..c].iter().all(|b| b.is_ascii_digit()) => { + let group: usize = + std::str::from_utf8(&rest[..c]).unwrap().parse().unwrap(); + if let Some(m) = caps.get(group) { + out.extend_from_slice(m.as_str().as_bytes()); + } + i += 2 + c + 1; + } + _ => { + out.push(b'$'); + i += 1; + } + } + } + b'$' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => { + let (group, consumed) = php_replacement_group(&bytes[i + 1..]); + if let Some(m) = caps.get(group) { + out.extend_from_slice(m.as_str().as_bytes()); + } + i += 1 + consumed; + } + b => { + out.push(b); + i += 1; + } + } + } +} + +// Reads up to two leading ASCII digits as a PHP backreference group number. +fn php_replacement_group(bytes: &[u8]) -> (usize, usize) { + let mut group = 0usize; + let mut consumed = 0usize; + while consumed < 2 && consumed < bytes.len() && bytes[consumed].is_ascii_digit() { + group = group * 10 + (bytes[consumed] - b'0') as usize; + consumed += 1; + } + (group, consumed) +} + +// PHP `preg_split($pattern, $subject)` with no flags or limit: the text between +// successive matches, including the leading and trailing pieces (which may be +// empty). Zero-width matches split between every position. +fn php_split_impl(re: ®ex::Regex, subject: &str) -> Vec<String> { + let mut result = Vec::new(); + let mut last = 0; + for caps in re.captures_iter(subject) { + let m = caps.get(0).unwrap(); + result.push(subject[last..m.start()].to_string()); + last = m.end(); + } + result.push(subject[last..].to_string()); + result +} + +// Classic preg_match `$matches` row: index 0 is the full match, trailing +// unmatched groups are truncated and interior unmatched groups become "". +fn php_match_row(caps: ®ex::Captures) -> Vec<String> { + let last = (0..caps.len()) + .rev() + .find(|&g| caps.get(g).is_some()) + .unwrap_or(0); + (0..=last) + .map(|g| { + caps.get(g) + .map(|m| m.as_str().to_string()) + .unwrap_or_default() + }) + .collect() +} + pub fn preg_match_all_offset_capture( pattern: &str, subject: &str, @@ -3008,14 +3187,28 @@ pub fn preg_match_all_offset_capture( Ok(count) } pub fn preg_replace_callback<F>( - _pattern: &str, - _callback: F, - _subject: &str, + pattern: &str, + mut callback: F, + subject: &str, ) -> anyhow::Result<String> where F: FnMut(&[Option<String>]) -> anyhow::Result<String>, { - todo!() + let re = compile_php_pattern(pattern)?; + let mut out: Vec<u8> = Vec::new(); + let mut last = 0; + for caps in re.captures_iter(subject) { + let m = caps.get(0).unwrap(); + out.extend_from_slice(&subject.as_bytes()[last..m.start()]); + let groups: Vec<Option<String>> = (0..caps.len()) + .map(|g| caps.get(g).map(|x| x.as_str().to_string())) + .collect(); + let replaced = callback(&groups)?; + out.extend_from_slice(replaced.as_bytes()); + last = m.end(); + } + out.extend_from_slice(&subject.as_bytes()[last..]); + Ok(String::from_utf8_lossy(&out).into_owned()) } pub fn is_resource_value(_resource: &PhpResource) -> bool { |
