aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-string/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-symfony-string/src')
-rw-r--r--crates/shirabe-symfony-string/src/byte_string.rs45
-rw-r--r--crates/shirabe-symfony-string/src/code_point_string.rs93
-rw-r--r--crates/shirabe-symfony-string/src/lib.rs17
-rw-r--r--crates/shirabe-symfony-string/src/unicode_string.rs56
4 files changed, 211 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-string/src/byte_string.rs b/crates/shirabe-symfony-string/src/byte_string.rs
new file mode 100644
index 00000000..cf597bbf
--- /dev/null
+++ b/crates/shirabe-symfony-string/src/byte_string.rs
@@ -0,0 +1,45 @@
+//! ref: composer/vendor/symfony/string/ByteString.php
+
+use crate::code_point_string::CodePointString;
+
+#[derive(Debug, Clone)]
+pub struct ByteString {
+ pub(crate) string: String,
+}
+
+impl ByteString {
+ pub fn new(string: &str) -> Self {
+ Self {
+ string: string.to_string(),
+ }
+ }
+
+ /// `from_encoding` is `""` for PHP's `null`.
+ pub fn to_code_point_string(&self, from_encoding: &str) -> CodePointString {
+ // The source `string` is always valid UTF-8 (Rust `String`/`&str` guarantee), so
+ // `preg_match('//u', ...)` always holds.
+ if matches!(from_encoding, "" | "utf8" | "utf-8" | "UTF8" | "UTF-8") {
+ return CodePointString {
+ string: self.string.clone(),
+ };
+ }
+
+ let valid_encoding = shirabe_php_shim::mb_detect_encoding(
+ &self.string,
+ Some(vec![from_encoding.to_string()]),
+ true,
+ )
+ .is_some();
+ // PHP throws InvalidArgumentException. Callers detect `from_encoding` from this very
+ // string, so a mismatch is a programming error.
+ assert!(valid_encoding, "Invalid \"{}\" string.", from_encoding);
+
+ CodePointString {
+ string: shirabe_php_shim::mb_convert_encoding(
+ self.string.clone().into_bytes(),
+ "UTF-8",
+ from_encoding,
+ ),
+ }
+ }
+}
diff --git a/crates/shirabe-symfony-string/src/code_point_string.rs b/crates/shirabe-symfony-string/src/code_point_string.rs
new file mode 100644
index 00000000..dcbaa1f6
--- /dev/null
+++ b/crates/shirabe-symfony-string/src/code_point_string.rs
@@ -0,0 +1,93 @@
+//! ref: composer/vendor/symfony/string/CodePointString.php
+
+#[derive(Debug, Clone)]
+pub struct CodePointString {
+ pub(crate) string: String,
+}
+
+impl CodePointString {
+ /// Port of `AbstractString::wordwrap()`, specialised to the non-`ignoreCase` code-point case.
+ pub fn wordwrap(&self, width: i64, r#break: &str, cut: bool) -> Self {
+ // `split($break)` with no flags reduces to `explode($break, $string)` here, then `chunk()`
+ // yields one entry per code point. `ignoreCase` is always false for freshly built instances.
+ let lines: Vec<&str> = if !r#break.is_empty() {
+ self.string.split(r#break).collect()
+ } else {
+ vec![&self.string]
+ };
+
+ let mut chars: Vec<String> = Vec::new();
+ let mut mask = String::new();
+
+ if lines.len() == 1 && lines[0].is_empty() {
+ return Self {
+ string: String::new(),
+ };
+ }
+
+ for (i, line) in lines.iter().enumerate() {
+ if i != 0 {
+ chars.push(r#break.to_string());
+ mask.push('#');
+ }
+
+ for ch in line.chars() {
+ let s = ch.to_string();
+ mask.push(if s == " " { ' ' } else { '?' });
+ chars.push(s);
+ }
+ }
+
+ let mut string = String::new();
+ let mut j: usize = 0;
+ // PHP seeds both `$b` and `$i` at -1; mirror with signed indices.
+ let mut i: i64 = -1;
+ let mask = shirabe_php_shim::wordwrap(&mask, width, "#", cut);
+ let mask_bytes = mask.as_bytes();
+
+ let mut b: i64 = -1;
+ loop {
+ // strpos($mask, '#', $b + 1)
+ let from = (b + 1) as usize;
+ let Some(rel) = mask_bytes[from..].iter().position(|&c| c == b'#') else {
+ break;
+ };
+ b = (from + rel) as i64;
+
+ i += 1;
+ while i < b {
+ string.push_str(&chars[j]);
+ j += 1;
+ i += 1;
+ }
+
+ if chars[j] == r#break || chars[j] == " " {
+ j += 1;
+ }
+
+ string.push_str(r#break);
+ }
+
+ for c in &chars[j..] {
+ string.push_str(c);
+ }
+
+ Self { string }
+ }
+
+ /// `to_encoding` is `""` for PHP's `null`.
+ pub fn to_byte_string(&self, to_encoding: &str) -> String {
+ // A CodePointString is an AbstractUnicodeString, so PHP's `$fromEncoding` is always
+ // 'UTF-8' and the string is returned verbatim for a null/UTF-8 target.
+ if matches!(to_encoding, "" | "utf8" | "utf-8" | "UTF8" | "UTF-8") {
+ return self.string.clone();
+ }
+
+ // PHP falls back to iconv() only when mb_convert_encoding() rejects the target encoding.
+ shirabe_php_shim::mb_convert_encoding(
+ self.string.clone().into_bytes(),
+ to_encoding,
+ "UTF-8",
+ )
+ }
+}
diff --git a/crates/shirabe-symfony-string/src/lib.rs b/crates/shirabe-symfony-string/src/lib.rs
new file mode 100644
index 00000000..139d726f
--- /dev/null
+++ b/crates/shirabe-symfony-string/src/lib.rs
@@ -0,0 +1,17 @@
+pub mod byte_string;
+pub mod code_point_string;
+pub mod unicode_string;
+
+pub use byte_string::*;
+pub use code_point_string::*;
+pub use unicode_string::*;
+
+/// Mirror of Symfony's `b()` helper function.
+pub fn b(string: &str) -> ByteString {
+ ByteString::new(string)
+}
+
+/// Mirror of Symfony's `u()` helper function.
+pub fn s(string: &str) -> UnicodeString {
+ UnicodeString::new(string)
+}
diff --git a/crates/shirabe-symfony-string/src/unicode_string.rs b/crates/shirabe-symfony-string/src/unicode_string.rs
new file mode 100644
index 00000000..9048f77b
--- /dev/null
+++ b/crates/shirabe-symfony-string/src/unicode_string.rs
@@ -0,0 +1,56 @@
+//! ref: composer/vendor/symfony/string/UnicodeString.php
+
+#[derive(Debug, Clone)]
+pub struct UnicodeString {
+ pub(crate) string: String,
+}
+
+impl UnicodeString {
+ // TODO(phase-c): the real constructor runs `normalizer_normalize` (Unicode NFC normalization),
+ // which has no Rust std equivalent and would need a dedicated normalization implementation.
+ // Normalization is skipped here, which is only correct for already-NFC input such as ASCII.
+ pub fn new(string: &str) -> Self {
+ Self {
+ string: string.to_string(),
+ }
+ }
+
+ // TODO(phase-c): ASCII-only provisional implementation. The faithful `width()` uses `wcswidth`
+ // with the Unicode width tables to treat wide characters as width 2 and skip zero-width /
+ // combining characters; here every character counts as width 1, correct only for ASCII. The
+ // ANSI/control-character stripping driven by `ignore_ansi_decoration` is likewise not handled.
+ pub fn width(&self, _ignore_ansi_decoration: bool) -> i64 {
+ let s = self.string.replace(['\x00', '\x05', '\x07'], "");
+ let s = s.replace("\r\n", "\n").replace('\r', "\n");
+
+ let mut width: i64 = 0;
+ for line in s.split('\n') {
+ let line_width = line.chars().count() as i64;
+ if line_width > width {
+ width = line_width;
+ }
+ }
+
+ width
+ }
+
+ // TODO(phase-c): the faithful `length()` uses `grapheme_strlen` (extended grapheme clusters),
+ // which needs Unicode segmentation tables with no Rust std equivalent and no permitted crate.
+ // Approximated with the code-point count, exact only when no combining/multi-code-point
+ // clusters are present (e.g. ASCII).
+ pub fn length(&self) -> i64 {
+ shirabe_php_shim::mb_strlen(&self.string, "UTF-8")
+ }
+
+ // TODO(phase-c): the faithful `slice()` uses `grapheme_substr` (grapheme-cluster offsets), which
+ // needs Unicode segmentation tables with no std equivalent and no permitted crate. Approximated
+ // with code-point offsets via `mb_substr`, exact only without combining/multi-code-point clusters.
+ pub fn slice(&self, start: i64, length: Option<i64>) -> Self {
+ Self::new(&shirabe_php_shim::mb_substr(
+ &self.string,
+ start,
+ length,
+ Some("UTF-8"),
+ ))
+ }
+}