aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/helper
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-12 01:01:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-12 01:01:43 +0900
commit1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch)
tree9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/helper
parentddf0a624145b618c05c2ee47414545b99b685f37 (diff)
downloadphp-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/helper')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/debug_formatter_helper.rs194
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/descriptor_helper.rs135
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs111
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/helper.rs172
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs24
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs108
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/mod.rs16
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs306
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/progress_bar.rs820
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs932
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs182
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table.rs1333
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table_cell.rs113
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table_cell_style.rs126
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table_rows.rs43
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table_separator.rs29
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/helper/table_style.rs293
17 files changed, 4856 insertions, 81 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/debug_formatter_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/debug_formatter_helper.rs
new file mode 100644
index 0000000..26ad169
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/debug_formatter_helper.rs
@@ -0,0 +1,194 @@
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::helper::helper_interface::HelperInterface;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use indexmap::IndexMap;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+const COLORS: [&str; 9] = [
+ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "default",
+];
+
+/// Helps outputting debug information when running an external program from a command.
+///
+/// An external program can be a Process, an HTTP request, or anything else.
+#[derive(Debug)]
+pub struct DebugFormatterHelper {
+ inner: Helper,
+ started: IndexMap<String, DebugFormatterSession>,
+ count: i64,
+}
+
+/// Per-id session state. PHP stores this as `['border' => int, 'out' => true, 'err' => true]`
+/// where presence of `out`/`err` keys is tested via `isset` and removed via `unset`.
+#[derive(Debug, Default)]
+struct DebugFormatterSession {
+ border: i64,
+ out: bool,
+ err: bool,
+}
+
+impl Default for DebugFormatterHelper {
+ fn default() -> Self {
+ Self {
+ inner: Helper::default(),
+ started: IndexMap::new(),
+ count: -1,
+ }
+ }
+}
+
+impl DebugFormatterHelper {
+ /// Starts a debug formatting session.
+ pub fn start(&mut self, id: &str, message: &str, prefix: &str) -> String {
+ self.count += 1;
+ self.started.insert(
+ id.to_string(),
+ DebugFormatterSession {
+ border: self.count % COLORS.len() as i64,
+ out: false,
+ err: false,
+ },
+ );
+
+ shirabe_php_shim::sprintf(
+ "%s<bg=blue;fg=white> %s </> <fg=blue>%s</>\n",
+ &[
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(prefix.to_string()),
+ shirabe_php_shim::PhpMixed::String(message.to_string()),
+ ],
+ )
+ }
+
+ /// Adds progress to a formatting session.
+ pub fn progress(
+ &mut self,
+ id: &str,
+ buffer: &str,
+ error: bool,
+ prefix: &str,
+ error_prefix: &str,
+ ) -> String {
+ let mut message = String::new();
+
+ if error {
+ if self.started[id].out {
+ message.push('\n');
+ self.started.get_mut(id).unwrap().out = false;
+ }
+ if !self.started[id].err {
+ message.push_str(&shirabe_php_shim::sprintf(
+ "%s<bg=red;fg=white> %s </> ",
+ &[
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(error_prefix.to_string()),
+ ],
+ ));
+ self.started.get_mut(id).unwrap().err = true;
+ }
+
+ message.push_str(&shirabe_php_shim::str_replace(
+ "\n",
+ &shirabe_php_shim::sprintf(
+ "\n%s<bg=red;fg=white> %s </> ",
+ &[
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(error_prefix.to_string()),
+ ],
+ ),
+ buffer,
+ ));
+ } else {
+ if self.started[id].err {
+ message.push('\n');
+ self.started.get_mut(id).unwrap().err = false;
+ }
+ if !self.started[id].out {
+ message.push_str(&shirabe_php_shim::sprintf(
+ "%s<bg=green;fg=white> %s </> ",
+ &[
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(prefix.to_string()),
+ ],
+ ));
+ self.started.get_mut(id).unwrap().out = true;
+ }
+
+ message.push_str(&shirabe_php_shim::str_replace(
+ "\n",
+ &shirabe_php_shim::sprintf(
+ "\n%s<bg=green;fg=white> %s </> ",
+ &[
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(prefix.to_string()),
+ ],
+ ),
+ buffer,
+ ));
+ }
+
+ message
+ }
+
+ /// Stops a formatting session.
+ pub fn stop(&mut self, id: &str, message: &str, successful: bool, prefix: &str) -> String {
+ let trailing_eol = if self.started[id].out || self.started[id].err {
+ "\n"
+ } else {
+ ""
+ };
+
+ if successful {
+ return shirabe_php_shim::sprintf(
+ "%s%s<bg=green;fg=white> %s </> <fg=green>%s</>\n",
+ &[
+ shirabe_php_shim::PhpMixed::String(trailing_eol.to_string()),
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(prefix.to_string()),
+ shirabe_php_shim::PhpMixed::String(message.to_string()),
+ ],
+ );
+ }
+
+ let message = shirabe_php_shim::sprintf(
+ "%s%s<bg=red;fg=white> %s </> <fg=red>%s</>\n",
+ &[
+ shirabe_php_shim::PhpMixed::String(trailing_eol.to_string()),
+ shirabe_php_shim::PhpMixed::String(self.get_border(id)),
+ shirabe_php_shim::PhpMixed::String(prefix.to_string()),
+ shirabe_php_shim::PhpMixed::String(message.to_string()),
+ ],
+ );
+
+ if let Some(session) = self.started.get_mut(id) {
+ session.out = false;
+ session.err = false;
+ }
+
+ message
+ }
+
+ fn get_border(&self, id: &str) -> String {
+ shirabe_php_shim::sprintf(
+ "<bg=%s> </>",
+ &[shirabe_php_shim::PhpMixed::String(
+ COLORS[self.started[id].border as usize].to_string(),
+ )],
+ )
+ }
+}
+
+impl HelperInterface for DebugFormatterHelper {
+ fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
+ self.inner.set_helper_set(helper_set);
+ }
+
+ fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> {
+ self.inner.get_helper_set()
+ }
+
+ fn get_name(&self) -> String {
+ "debug_formatter".to_string()
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/descriptor_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/descriptor_helper.rs
new file mode 100644
index 0000000..64c166c
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/descriptor_helper.rs
@@ -0,0 +1,135 @@
+use crate::symfony::console::descriptor::descriptor_interface::DescriptorInterface;
+use crate::symfony::console::descriptor::json_descriptor::JsonDescriptor;
+use crate::symfony::console::descriptor::markdown_descriptor::MarkdownDescriptor;
+use crate::symfony::console::descriptor::text_descriptor::TextDescriptor;
+use crate::symfony::console::descriptor::xml_descriptor::XmlDescriptor;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::helper::helper_interface::HelperInterface;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use crate::symfony::console::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// This class adds helper method to describe objects in various formats.
+#[derive(Default)]
+pub struct DescriptorHelper {
+ inner: Helper,
+ /// @var DescriptorInterface[]
+ descriptors: IndexMap<String, Box<dyn DescriptorInterface>>,
+}
+
+// `DescriptorInterface` does not require `Debug`, so the derive cannot see
+// through the trait object; provide a minimal manual impl.
+impl std::fmt::Debug for DescriptorHelper {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("DescriptorHelper")
+ .field("inner", &self.inner)
+ .field("descriptors", &self.descriptors.keys().collect::<Vec<_>>())
+ .finish()
+ }
+}
+
+impl DescriptorHelper {
+ pub fn new() -> Self {
+ let mut this = Self {
+ inner: Helper::default(),
+ descriptors: IndexMap::new(),
+ };
+ // The XML/JSON/Markdown descriptors do not yet expose a constructor
+ // (no `Default`/`new`); their construction is deferred until those
+ // descriptor types provide one.
+ let xml: Box<dyn DescriptorInterface> = todo!();
+ let json: Box<dyn DescriptorInterface> = todo!();
+ let md: Box<dyn DescriptorInterface> = todo!();
+ this.register("txt", Box::new(TextDescriptor::default()))
+ .register("xml", xml)
+ .register("json", json)
+ .register("md", md);
+ this
+ }
+
+ /// Describes an object if supported.
+ ///
+ /// Available options are:
+ /// * format: string, the output format name
+ /// * raw_text: boolean, sets output type as raw
+ ///
+ /// @throws InvalidArgumentException when the given format is not supported
+ pub fn describe2(
+ &self,
+ output: &dyn OutputInterface,
+ object: Option<shirabe_php_shim::PhpMixed>,
+ options: IndexMap<String, shirabe_php_shim::PhpMixed>,
+ ) -> Result<(), InvalidArgumentException> {
+ let mut merged: IndexMap<String, shirabe_php_shim::PhpMixed> = IndexMap::new();
+ merged.insert(
+ "raw_text".to_string(),
+ shirabe_php_shim::PhpMixed::Bool(false),
+ );
+ merged.insert(
+ "format".to_string(),
+ shirabe_php_shim::PhpMixed::String("txt".to_string()),
+ );
+ for (key, value) in options {
+ merged.insert(key, value);
+ }
+ let options = merged;
+
+ let format = match &options["format"] {
+ shirabe_php_shim::PhpMixed::String(format) => format.clone(),
+ _ => String::new(),
+ };
+
+ if !self.descriptors.contains_key(&format) {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Unsupported format \"%s\".",
+ &[shirabe_php_shim::PhpMixed::String(format.clone())],
+ ),
+ code: 0,
+ },
+ ));
+ }
+
+ let _ = (&self.descriptors[&format], output, object, options);
+ // `DescriptorInterface::describe` takes an owned
+ // `Rc<RefCell<dyn OutputInterface>>` and `&mut self`, while the helper
+ // only holds a shared `&dyn OutputInterface` and `&self`. Reconciling
+ // this ownership/mutability gap is a Phase C decision.
+ todo!("dispatch to DescriptorInterface::describe (Phase C ownership)");
+ }
+
+ /// Registers a descriptor.
+ ///
+ /// @return $this
+ pub fn register(
+ &mut self,
+ format: &str,
+ descriptor: Box<dyn DescriptorInterface>,
+ ) -> &mut Self {
+ self.descriptors.insert(format.to_string(), descriptor);
+
+ self
+ }
+
+ pub fn get_formats(&self) -> Vec<String> {
+ self.descriptors.keys().cloned().collect()
+ }
+}
+
+impl HelperInterface for DescriptorHelper {
+ fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
+ self.inner.set_helper_set(helper_set);
+ }
+
+ fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> {
+ self.inner.get_helper_set()
+ }
+
+ fn get_name(&self) -> String {
+ "descriptor".to_string()
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs
index e55dbce..ef33f93 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs
@@ -1,20 +1,113 @@
-use crate::symfony::console::helper::HelperInterface;
+use crate::symfony::console::formatter::output_formatter::OutputFormatter;
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::helper::helper_interface::HelperInterface;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use std::cell::RefCell;
+use std::rc::Rc;
-#[derive(Debug)]
-pub struct FormatterHelper;
+/// The Formatter class provides helpers to format messages.
+#[derive(Debug, Default)]
+pub struct FormatterHelper {
+ inner: Helper,
+}
impl FormatterHelper {
- pub fn format_section(&self, _section: &str, _message: &str, _style: &str) -> String {
- todo!()
+ /// Formats a message within a section.
+ pub fn format_section(&self, section: &str, message: &str, style: &str) -> String {
+ shirabe_php_shim::sprintf(
+ "<%s>[%s]</%s> %s",
+ &[
+ shirabe_php_shim::PhpMixed::String(style.to_string()),
+ shirabe_php_shim::PhpMixed::String(section.to_string()),
+ shirabe_php_shim::PhpMixed::String(style.to_string()),
+ shirabe_php_shim::PhpMixed::String(message.to_string()),
+ ],
+ )
}
- pub fn format_block(&self, _messages: &[&str], _style: &str, _large: bool) -> String {
- todo!()
+ /// Formats a message as a block of text.
+ ///
+ /// @param string|array $messages The message to write in the block
+ pub fn format_block(&self, messages: FormatBlockMessages, style: &str, large: bool) -> String {
+ let messages = match messages {
+ FormatBlockMessages::String(message) => vec![message],
+ FormatBlockMessages::Array(messages) => messages,
+ };
+
+ let mut len: i64 = 0;
+ let mut lines: Vec<String> = Vec::new();
+ for message in &messages {
+ let message = OutputFormatter::escape(message).unwrap();
+ lines.push(shirabe_php_shim::sprintf(
+ if large { " %s " } else { " %s " },
+ &[shirabe_php_shim::PhpMixed::String(message.clone())],
+ ));
+ len = std::cmp::max(Helper::width(&message) + (if large { 4 } else { 2 }), len);
+ }
+
+ let mut messages: Vec<String> = if large {
+ vec![shirabe_php_shim::str_repeat(" ", len as usize)]
+ } else {
+ vec![]
+ };
+ let mut i = 0;
+ while i < lines.len() {
+ messages.push(format!(
+ "{}{}",
+ lines[i],
+ shirabe_php_shim::str_repeat(" ", (len - Helper::width(&lines[i])) as usize)
+ ));
+ i += 1;
+ }
+ if large {
+ messages.push(shirabe_php_shim::str_repeat(" ", len as usize));
+ }
+
+ let mut i = 0;
+ while i < messages.len() {
+ messages[i] = shirabe_php_shim::sprintf(
+ "<%s>%s</%s>",
+ &[
+ shirabe_php_shim::PhpMixed::String(style.to_string()),
+ shirabe_php_shim::PhpMixed::String(messages[i].clone()),
+ shirabe_php_shim::PhpMixed::String(style.to_string()),
+ ],
+ );
+ i += 1;
+ }
+
+ messages.join("\n")
+ }
+
+ /// Truncates a message to the given length.
+ pub fn truncate(&self, message: &str, length: i64, suffix: &str) -> String {
+ let computed_length = length - Helper::width(suffix);
+
+ if computed_length > Helper::width(message) {
+ return message.to_string();
+ }
+
+ format!("{}{}", Helper::substr(message, 0, Some(length)), suffix)
}
}
impl HelperInterface for FormatterHelper {
- fn as_any(&self) -> &dyn std::any::Any {
- self
+ fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
+ self.inner.set_helper_set(helper_set);
+ }
+
+ fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> {
+ self.inner.get_helper_set()
+ }
+
+ fn get_name(&self) -> String {
+ "formatter".to_string()
}
}
+
+/// `formatBlock` accepts either a single string or an array of strings.
+#[derive(Debug)]
+pub enum FormatBlockMessages {
+ String(String),
+ Array(Vec<String>),
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/helper.rs
index 881aabd..b599f11 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/helper.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/helper.rs
@@ -1,3 +1,171 @@
-pub trait Helper {
- fn get_name(&self) -> String;
+use crate::symfony::console::formatter::output_formatter_interface::OutputFormatterInterface;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use crate::symfony::string::unicode_string::UnicodeString;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// Helper is the base class for all helper classes.
+#[derive(Debug, Default)]
+pub struct Helper {
+ pub(crate) helper_set: Option<Rc<RefCell<HelperSet>>>,
+}
+
+impl Helper {
+ pub fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
+ self.helper_set = helper_set;
+ }
+
+ pub fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> {
+ self.helper_set.clone()
+ }
+
+ /// Returns the length of a string, using mb_strwidth if it is available.
+ ///
+ /// @deprecated since Symfony 5.3
+ pub fn strlen(string: &str) -> i64 {
+ shirabe_php_shim::trigger_deprecation(
+ "symfony/console",
+ "5.3",
+ "Method \"%s()\" is deprecated and will be removed in Symfony 6.0. Use Helper::width() or Helper::length() instead.",
+ "Helper::strlen",
+ );
+
+ Self::width(string)
+ }
+
+ /// Returns the width of a string, using mb_strwidth if it is available.
+ /// The width is how many characters positions the string will use.
+ pub fn width(string: &str) -> i64 {
+ if shirabe_php_shim::preg_match("//u", string, &mut Vec::new()) != 0 {
+ return UnicodeString::new(string).width(false);
+ }
+
+ let encoding = shirabe_php_shim::mb_detect_encoding(string, None, true);
+ let encoding = match encoding {
+ Some(encoding) => encoding,
+ None => return shirabe_php_shim::strlen(string),
+ };
+
+ shirabe_php_shim::mb_strwidth(string, Some(&encoding))
+ }
+
+ /// Returns the length of a string, using mb_strlen if it is available.
+ /// The length is related to how many bytes the string will use.
+ pub fn length(string: &str) -> i64 {
+ if shirabe_php_shim::preg_match("//u", string, &mut Vec::new()) != 0 {
+ return UnicodeString::new(string).length();
+ }
+
+ let encoding = shirabe_php_shim::mb_detect_encoding(string, None, true);
+ let encoding = match encoding {
+ Some(encoding) => encoding,
+ None => return shirabe_php_shim::strlen(string),
+ };
+
+ shirabe_php_shim::mb_strlen(string, &encoding)
+ }
+
+ /// Returns the subset of a string, using mb_substr if it is available.
+ pub fn substr(string: &str, from: i64, length: Option<i64>) -> String {
+ let encoding = shirabe_php_shim::mb_detect_encoding(string, None, true);
+ let encoding = match encoding {
+ Some(encoding) => encoding,
+ None => return shirabe_php_shim::substr(string, from, length),
+ };
+
+ shirabe_php_shim::mb_substr(string, from, length, Some(&encoding))
+ }
+
+ pub fn format_time(secs: f64) -> Option<String> {
+ // [threshold, label, divisor?]
+ let time_formats: [(f64, &str, Option<f64>); 9] = [
+ (0.0, "< 1 sec", None),
+ (1.0, "1 sec", None),
+ (2.0, "secs", Some(1.0)),
+ (60.0, "1 min", None),
+ (120.0, "mins", Some(60.0)),
+ (3600.0, "1 hr", None),
+ (7200.0, "hrs", Some(3600.0)),
+ (86400.0, "1 day", None),
+ (172800.0, "days", Some(86400.0)),
+ ];
+
+ for (index, format) in time_formats.iter().enumerate() {
+ if secs >= format.0 {
+ if (index + 1 < time_formats.len() && secs < time_formats[index + 1].0)
+ || index == time_formats.len() - 1
+ {
+ match format.2 {
+ None => return Some(format.1.to_string()),
+ Some(divisor) => {
+ return Some(format!("{} {}", (secs / divisor).floor(), format.1));
+ }
+ }
+ }
+ }
+ }
+
+ None
+ }
+
+ pub fn format_memory(memory: i64) -> String {
+ if memory >= 1024 * 1024 * 1024 {
+ return shirabe_php_shim::sprintf(
+ "%.1f GiB",
+ &[shirabe_php_shim::PhpMixed::Float(
+ memory as f64 / 1024.0 / 1024.0 / 1024.0,
+ )],
+ );
+ }
+
+ if memory >= 1024 * 1024 {
+ return shirabe_php_shim::sprintf(
+ "%.1f MiB",
+ &[shirabe_php_shim::PhpMixed::Float(
+ memory as f64 / 1024.0 / 1024.0,
+ )],
+ );
+ }
+
+ if memory >= 1024 {
+ return shirabe_php_shim::sprintf(
+ "%d KiB",
+ &[shirabe_php_shim::PhpMixed::Int(memory / 1024)],
+ );
+ }
+
+ shirabe_php_shim::sprintf("%d B", &[shirabe_php_shim::PhpMixed::Int(memory)])
+ }
+
+ /// @deprecated since Symfony 5.3
+ pub fn strlen_without_decoration(
+ formatter: &mut dyn OutputFormatterInterface,
+ string: &str,
+ ) -> i64 {
+ shirabe_php_shim::trigger_deprecation(
+ "symfony/console",
+ "5.3",
+ "Method \"%s()\" is deprecated and will be removed in Symfony 6.0. Use Helper::removeDecoration() instead.",
+ "Helper::strlenWithoutDecoration",
+ );
+
+ Self::width(&Self::remove_decoration(formatter, string))
+ }
+
+ pub fn remove_decoration(formatter: &mut dyn OutputFormatterInterface, string: &str) -> String {
+ let is_decorated = formatter.is_decorated();
+ formatter.set_decorated(false);
+ // remove <...> formatting
+ let string = formatter.format(Some(string)).unwrap().unwrap_or_default();
+ // remove already formatted characters
+ let string =
+ shirabe_php_shim::preg_replace("/\u{1b}\\[[^m]*m/", "", &string).unwrap_or_default();
+ // remove terminal hyperlinks
+ let string =
+ shirabe_php_shim::preg_replace("/\u{1b}]8;[^;]*;[^\u{1b}]*\u{1b}\\\\/", "", &string)
+ .unwrap_or_default();
+ formatter.set_decorated(is_decorated);
+
+ string
+ }
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs b/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs
index cf60bf0..f7481fd 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs
@@ -1,17 +1,15 @@
-use crate::symfony::console::helper::HelperSet;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use std::cell::RefCell;
+use std::rc::Rc;
-pub trait HelperInterface: std::fmt::Debug {
- fn set_helper_set(&mut self, _helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) {
- todo!()
- }
+/// HelperInterface is the interface all helpers must implement.
+pub trait HelperInterface: std::fmt::Debug + shirabe_php_shim::AsAny {
+ /// Sets the helper set associated with this helper.
+ fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>);
- fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::RefCell<HelperSet>>> {
- todo!()
- }
+ /// Gets the helper set associated with this helper.
+ fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>>;
- fn get_name(&self) -> String {
- todo!()
- }
-
- fn as_any(&self) -> &dyn std::any::Any;
+ /// Returns the canonical name of this helper.
+ fn get_name(&self) -> String;
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs b/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs
index e38dbf3..a7ef820 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs
@@ -1,27 +1,115 @@
-use crate::symfony::console::helper::HelperInterface;
+use crate::symfony::console::command::command::Command;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::helper::helper_interface::HelperInterface;
use indexmap::IndexMap;
use std::cell::RefCell;
use std::rc::Rc;
-#[derive(Debug)]
+/// HelperSet represents a set of helpers to be used with a command.
+///
+/// @implements \IteratorAggregate<string, Helper>
+#[derive(Debug, Default, Clone)]
pub struct HelperSet {
helpers: IndexMap<String, Rc<RefCell<dyn HelperInterface>>>,
+ command: Option<Rc<RefCell<dyn Command>>>,
}
impl HelperSet {
- pub fn new(_helpers: Vec<Rc<RefCell<dyn HelperInterface>>>) -> Self {
- todo!()
+ /// @param Helper[] $helpers An array of helper
+ pub fn new(
+ this: &Rc<RefCell<HelperSet>>,
+ helpers: IndexMap<HelperSetKey, Rc<RefCell<dyn HelperInterface>>>,
+ ) {
+ for (alias, helper) in helpers {
+ let alias = match alias {
+ HelperSetKey::Int(_) => None,
+ HelperSetKey::String(alias) => Some(alias),
+ };
+ Self::set(this, helper, alias.as_deref());
+ }
}
- pub fn get<T: HelperInterface + 'static>(&self, _name: &str) -> Rc<RefCell<T>> {
- todo!()
+ pub fn set(
+ this: &Rc<RefCell<HelperSet>>,
+ helper: Rc<RefCell<dyn HelperInterface>>,
+ alias: Option<&str>,
+ ) {
+ let name = helper.borrow().get_name();
+ this.borrow_mut().helpers.insert(name, helper.clone());
+ if let Some(alias) = alias {
+ this.borrow_mut()
+ .helpers
+ .insert(alias.to_string(), helper.clone());
+ }
+
+ helper.borrow_mut().set_helper_set(Some(this.clone()));
+ }
+
+ /// Returns true if the helper if defined.
+ pub fn has(&self, name: &str) -> bool {
+ self.helpers.contains_key(name)
+ }
+
+ /// Gets a helper value.
+ ///
+ /// @throws InvalidArgumentException if the helper is not defined
+ pub fn get(
+ &self,
+ name: &str,
+ ) -> Result<Rc<RefCell<dyn HelperInterface>>, InvalidArgumentException> {
+ if !self.has(name) {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The helper \"%s\" is not defined.",
+ &[shirabe_php_shim::PhpMixed::String(name.to_string())],
+ ),
+ code: 0,
+ },
+ ));
+ }
+
+ Ok(self.helpers[name].clone())
}
- pub fn set(&mut self, _helper: Rc<RefCell<dyn HelperInterface>>, _alias: Option<&str>) {
- todo!()
+ /// @deprecated since Symfony 5.4
+ pub fn set_command(&mut self, command: Option<Rc<RefCell<dyn Command>>>) {
+ shirabe_php_shim::trigger_deprecation(
+ "symfony/console",
+ "5.4",
+ "Method \"%s()\" is deprecated.",
+ "HelperSet::setCommand",
+ );
+
+ self.command = command;
+ }
+
+ /// Gets the command associated with this helper set.
+ ///
+ /// @deprecated since Symfony 5.4
+ pub fn get_command(&self) -> Option<Rc<RefCell<dyn Command>>> {
+ shirabe_php_shim::trigger_deprecation(
+ "symfony/console",
+ "5.4",
+ "Method \"%s()\" is deprecated.",
+ "HelperSet::getCommand",
+ );
+
+ self.command.clone()
}
- pub fn has(&self, _name: &str) -> bool {
- todo!()
+ /// @return \Traversable<string, Helper>
+ pub fn get_iterator(
+ &self,
+ ) -> impl Iterator<Item = (&String, &Rc<RefCell<dyn HelperInterface>>)> {
+ self.helpers.iter()
}
}
+
+/// PHP array keys are either integers or strings; the HelperSet constructor
+/// distinguishes them via `\is_int($alias)`.
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum HelperSetKey {
+ Int(i64),
+ String(String),
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs b/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs
index 191fb6e..8aed672 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/mod.rs
@@ -1,17 +1,33 @@
+pub mod debug_formatter_helper;
+pub mod descriptor_helper;
pub mod formatter_helper;
pub mod helper;
pub mod helper_interface;
pub mod helper_set;
+pub mod process_helper;
pub mod progress_bar;
pub mod question_helper;
+pub mod symfony_question_helper;
pub mod table;
+pub mod table_cell;
+pub mod table_cell_style;
+pub mod table_rows;
pub mod table_separator;
+pub mod table_style;
+pub use debug_formatter_helper::*;
+pub use descriptor_helper::*;
pub use formatter_helper::*;
pub use helper::*;
pub use helper_interface::*;
pub use helper_set::*;
+pub use process_helper::*;
pub use progress_bar::*;
pub use question_helper::*;
+pub use symfony_question_helper::*;
pub use table::*;
+pub use table_cell::*;
+pub use table_cell_style::*;
+pub use table_rows::*;
pub use table_separator::*;
+pub use table_style::*;
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs
new file mode 100644
index 0000000..7652cac
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs
@@ -0,0 +1,306 @@
+use crate::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper;
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::helper::helper_interface::HelperInterface;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use crate::symfony::console::output::console_output_interface::ConsoleOutputInterface;
+use crate::symfony::console::output::output_interface::{self, OutputInterface};
+use crate::symfony::process::exception::process_failed_exception::ProcessFailedException;
+use crate::symfony::process::process::Process;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// The ProcessHelper class provides helpers to run external processes.
+///
+/// @final
+#[derive(Debug, Default)]
+pub struct ProcessHelper {
+ inner: Helper,
+}
+
+/// `$cmd` is either a `Process` instance or an array whose first element is a
+/// binary path (string) or a `Process`, followed by extra environment entries.
+#[derive(Debug)]
+pub enum ProcessHelperCmd {
+ Process(Process),
+ Array(Vec<ProcessHelperCmdElement>),
+}
+
+#[derive(Debug)]
+pub enum ProcessHelperCmdElement {
+ String(String),
+ Process(Process),
+}
+
+impl ProcessHelper {
+ /// Runs an external process.
+ ///
+ /// @param array|Process $cmd An instance of Process or an array of the command and arguments
+ /// @param callable|null $callback A PHP callback to run whenever there is some
+ /// output available on STDOUT or STDERR
+ pub fn run(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ cmd: ProcessHelperCmd,
+ error: Option<&str>,
+ callback: Option<Box<dyn FnMut(&str, &str)>>,
+ verbosity: i64,
+ ) -> anyhow::Result<Process> {
+ // `class_exists(Process::class)` guards against the optional symfony/process
+ // component being absent; in this port the component is always available.
+
+ // PHP: `if ($output instanceof ConsoleOutputInterface) { $output =
+ // $output->getErrorOutput(); }`. Downcasting a shared `dyn
+ // OutputInterface` to `dyn ConsoleOutputInterface` is unresolved;
+ // deferred to Phase C.
+ let output: Rc<RefCell<dyn OutputInterface>> =
+ todo!("$output instanceof ConsoleOutputInterface redirect to error output");
+
+ let formatter: Rc<RefCell<dyn HelperInterface>> = self
+ .get_helper_set()
+ .unwrap()
+ .borrow()
+ .get("debug_formatter")
+ .unwrap();
+
+ // Normalize $cmd: a single Process becomes a one-element array.
+ let mut cmd = match cmd {
+ ProcessHelperCmd::Process(process) => {
+ vec![ProcessHelperCmdElement::Process(process)]
+ }
+ ProcessHelperCmd::Array(cmd) => cmd,
+ };
+
+ // `!\is_array($cmd)` cannot happen given the enum, so the TypeError branch
+ // is unreachable here.
+
+ let mut process: Process;
+ match cmd.first() {
+ Some(ProcessHelperCmdElement::String(_)) => {
+ let command: Vec<String> = cmd
+ .iter()
+ .map(|element| match element {
+ ProcessHelperCmdElement::String(s) => s.clone(),
+ ProcessHelperCmdElement::Process(_) => unreachable!(),
+ })
+ .collect();
+ process = Process::new(command, None, None, None, Some(60.0));
+ cmd = vec![];
+ }
+ Some(ProcessHelperCmdElement::Process(_)) => {
+ let first = cmd.remove(0);
+ process = match first {
+ ProcessHelperCmdElement::Process(process) => process,
+ ProcessHelperCmdElement::String(_) => unreachable!(),
+ };
+ }
+ None => {
+ anyhow::bail!(shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Invalid command provided to \"%s()\": the command should be an array whose first element is either the path to the binary to run or a \"Process\" object.",
+ &[shirabe_php_shim::PhpMixed::String(
+ "ProcessHelper::run".to_string()
+ )],
+ ),
+ code: 0,
+ });
+ }
+ }
+
+ if verbosity <= output.borrow().get_verbosity() {
+ let started = Self::formatter_start(
+ &formatter,
+ &shirabe_php_shim::spl_object_hash_process(&process),
+ &self.escape_string(&process.get_command_line()),
+ );
+ output
+ .borrow()
+ .write(&[started], false, output_interface::OUTPUT_NORMAL);
+ }
+
+ let callback = if output.borrow().is_debug() {
+ Some(self.wrap_callback(output.clone(), &process, callback))
+ } else {
+ callback
+ };
+
+ // PHP passes the remaining `$cmd` array as the `$env` argument to Process::run.
+ process.run(callback);
+
+ if verbosity <= output.borrow().get_verbosity() {
+ let message = if process.is_successful() {
+ "Command ran successfully".to_string()
+ } else {
+ shirabe_php_shim::sprintf(
+ "%s Command did not run successfully",
+ &[match process.get_exit_code() {
+ Some(code) => shirabe_php_shim::PhpMixed::Int(code),
+ None => shirabe_php_shim::PhpMixed::Null,
+ }],
+ )
+ };
+ let stopped = Self::formatter_stop(
+ &formatter,
+ &shirabe_php_shim::spl_object_hash_process(&process),
+ &message,
+ process.is_successful(),
+ );
+ output
+ .borrow()
+ .write(&[stopped], false, output_interface::OUTPUT_NORMAL);
+ }
+
+ if !process.is_successful() && error.is_some() {
+ output.borrow().writeln(
+ &[shirabe_php_shim::sprintf(
+ "<error>%s</error>",
+ &[shirabe_php_shim::PhpMixed::String(
+ self.escape_string(error.unwrap()),
+ )],
+ )],
+ output_interface::OUTPUT_NORMAL,
+ );
+ }
+
+ Ok(process)
+ }
+
+ /// Runs the process.
+ ///
+ /// This is identical to run() except that an exception is thrown if the process
+ /// exits with a non-zero exit code.
+ ///
+ /// @param array|Process $cmd An instance of Process or a command to run
+ /// @param callable|null $callback A PHP callback to run whenever there is some
+ /// output available on STDOUT or STDERR
+ ///
+ /// @throws ProcessFailedException
+ ///
+ /// @see run()
+ pub fn must_run(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ cmd: ProcessHelperCmd,
+ error: Option<&str>,
+ callback: Option<Box<dyn FnMut(&str, &str)>>,
+ ) -> anyhow::Result<Process> {
+ let process = self.run(
+ output,
+ cmd,
+ error,
+ callback,
+ output_interface::VERBOSITY_VERY_VERBOSE,
+ )?;
+
+ if !process.is_successful() {
+ anyhow::bail!(ProcessFailedException::new(&process));
+ }
+
+ Ok(process)
+ }
+
+ /// Wraps a Process callback to add debugging output.
+ pub fn wrap_callback(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ process: &Process,
+ mut callback: Option<Box<dyn FnMut(&str, &str)>>,
+ ) -> Box<dyn FnMut(&str, &str)> {
+ // PHP: `if ($output instanceof ConsoleOutputInterface) { $output =
+ // $output->getErrorOutput(); }`. Downcasting a shared `dyn
+ // OutputInterface` to `dyn ConsoleOutputInterface` is unresolved;
+ // deferred to Phase C.
+ let output: Rc<RefCell<dyn OutputInterface>> =
+ todo!("$output instanceof ConsoleOutputInterface redirect to error output");
+
+ let formatter: Rc<RefCell<dyn HelperInterface>> = self
+ .get_helper_set()
+ .unwrap()
+ .borrow()
+ .get("debug_formatter")
+ .unwrap();
+
+ let object_hash = shirabe_php_shim::spl_object_hash_process(process);
+
+ Box::new(move |r#type: &str, buffer: &str| {
+ let progressed = Self::formatter_progress(
+ &formatter,
+ &object_hash,
+ &Self::escape_string_static(buffer),
+ Process::ERR == r#type,
+ );
+ output
+ .borrow()
+ .write(&[progressed], false, output_interface::OUTPUT_NORMAL);
+
+ if let Some(callback) = callback.as_mut() {
+ callback(r#type, buffer);
+ }
+ })
+ }
+
+ fn escape_string(&self, str: &str) -> String {
+ shirabe_php_shim::str_replace("<", "\\<", str)
+ }
+
+ fn escape_string_static(str: &str) -> String {
+ shirabe_php_shim::str_replace("<", "\\<", str)
+ }
+
+ /// PHP fetches `debug_formatter` from the HelperSet as a `HelperInterface` and
+ /// dynamically dispatches `start`/`stop`/`progress`, which are concrete
+ /// `DebugFormatterHelper` methods not present on the interface. Resolving the
+ /// dynamic helper handle back to the concrete `DebugFormatterHelper` is a
+ /// downcast that requires a Phase C decision (e.g. an `as_any` on
+ /// `HelperInterface`); deferred here.
+ fn debug_formatter(
+ _formatter: &Rc<RefCell<dyn HelperInterface>>,
+ ) -> Rc<RefCell<DebugFormatterHelper>> {
+ todo!("downcast HelperInterface handle to DebugFormatterHelper (Phase C)")
+ }
+
+ fn formatter_start(
+ formatter: &Rc<RefCell<dyn HelperInterface>>,
+ id: &str,
+ message: &str,
+ ) -> String {
+ Self::debug_formatter(formatter)
+ .borrow_mut()
+ .start(id, message, "RUN")
+ }
+
+ fn formatter_stop(
+ formatter: &Rc<RefCell<dyn HelperInterface>>,
+ id: &str,
+ message: &str,
+ successful: bool,
+ ) -> String {
+ Self::debug_formatter(formatter)
+ .borrow_mut()
+ .stop(id, message, successful, "RES")
+ }
+
+ fn formatter_progress(
+ formatter: &Rc<RefCell<dyn HelperInterface>>,
+ id: &str,
+ buffer: &str,
+ error: bool,
+ ) -> String {
+ Self::debug_formatter(formatter)
+ .borrow_mut()
+ .progress(id, buffer, error, "OUT", "ERR")
+ }
+}
+
+impl HelperInterface for ProcessHelper {
+ fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
+ self.inner.set_helper_set(helper_set);
+ }
+
+ fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> {
+ self.inner.get_helper_set()
+ }
+
+ fn get_name(&self) -> String {
+ "process".to_string()
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/progress_bar.rs b/crates/shirabe-external-packages/src/symfony/console/helper/progress_bar.rs
index e2a728f..bd6fe86 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/progress_bar.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/progress_bar.rs
@@ -1,38 +1,826 @@
+use crate::symfony::console::cursor::Cursor;
+use crate::symfony::console::exception::logic_exception::LogicException;
+use crate::symfony::console::helper::helper::Helper;
use crate::symfony::console::output::OutputInterface;
+use crate::symfony::console::output::output_interface;
+use crate::symfony::console::terminal::Terminal;
+use indexmap::IndexMap;
+use std::cell::RefCell;
+use std::rc::Rc;
+pub const FORMAT_VERBOSE: &str = "verbose";
+pub const FORMAT_VERY_VERBOSE: &str = "very_verbose";
+pub const FORMAT_DEBUG: &str = "debug";
+pub const FORMAT_NORMAL: &str = "normal";
+
+const FORMAT_VERBOSE_NOMAX: &str = "verbose_nomax";
+const FORMAT_VERY_VERBOSE_NOMAX: &str = "very_verbose_nomax";
+const FORMAT_DEBUG_NOMAX: &str = "debug_nomax";
+const FORMAT_NORMAL_NOMAX: &str = "normal_nomax";
+
+/// A placeholder formatter callable, receiving the bar and the output.
+pub type PlaceholderFormatter = Box<
+ dyn Fn(
+ &ProgressBar,
+ &Rc<RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<Result<shirabe_php_shim::PhpMixed, LogicException>>,
+>;
+
+/// The ProgressBar provides helpers to display progress output.
#[derive(Debug)]
-pub struct ProgressBar;
+pub struct ProgressBar {
+ bar_width: i64,
+ bar_char: Option<String>,
+ empty_bar_char: String,
+ progress_char: String,
+ format: Option<String>,
+ internal_format: Option<String>,
+ redraw_freq: Option<i64>,
+ write_count: i64,
+ last_write_time: f64,
+ min_seconds_between_redraws: f64,
+ max_seconds_between_redraws: f64,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ step: i64,
+ max: i64,
+ start_time: i64,
+ step_width: i64,
+ percent: f64,
+ messages: IndexMap<String, String>,
+ overwrite: bool,
+ terminal: Terminal,
+ previous_message: Option<String>,
+ cursor: Cursor,
+}
+
+thread_local! {
+ static FORMATTERS: RefCell<Option<IndexMap<String, PlaceholderFormatter>>> = const { RefCell::new(None) };
+ static FORMATS: RefCell<Option<IndexMap<String, String>>> = const { RefCell::new(None) };
+}
impl ProgressBar {
- pub fn new(_output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, _max: i64) -> Self {
- todo!()
+ /// `$max` Maximum steps (0 if unknown)
+ pub fn new(
+ output: Rc<RefCell<dyn OutputInterface>>,
+ max: i64,
+ min_seconds_between_redraws: f64,
+ ) -> Self {
+ let output = if todo!("$output instanceof ConsoleOutputInterface") {
+ todo!("$output = $output->getErrorOutput();")
+ } else {
+ output
+ };
+
+ let mut this = Self {
+ bar_width: 28,
+ bar_char: None,
+ empty_bar_char: "-".to_string(),
+ progress_char: ">".to_string(),
+ format: None,
+ internal_format: None,
+ redraw_freq: Some(1),
+ write_count: 0,
+ last_write_time: 0.0,
+ min_seconds_between_redraws: 0.0,
+ max_seconds_between_redraws: 1.0,
+ output: output.clone(),
+ step: 0,
+ max: 0,
+ start_time: 0,
+ step_width: 0,
+ percent: 0.0,
+ messages: IndexMap::new(),
+ overwrite: true,
+ terminal: Terminal::new(),
+ previous_message: None,
+ cursor: Cursor::new(output.clone(), None),
+ };
+
+ this.set_max_steps(max);
+
+ if 0.0 < min_seconds_between_redraws {
+ this.redraw_freq = None;
+ this.min_seconds_between_redraws = min_seconds_between_redraws;
+ }
+
+ if !this.output.borrow().is_decorated() {
+ // disable overwrite when output does not support ANSI codes.
+ this.overwrite = false;
+
+ // set a reasonable redraw frequency so output isn't flooded
+ this.redraw_freq = None;
+ }
+
+ this.start_time = shirabe_php_shim::time();
+
+ this
}
- pub fn start(&mut self, _max: Option<i64>) {
- todo!()
+ /// Sets a placeholder formatter for a given name.
+ ///
+ /// This method also allow you to override an existing placeholder.
+ ///
+ /// `$name` The placeholder name (including the delimiter char like %)
+ /// `$callable` A PHP callable
+ pub fn set_placeholder_formatter_definition(name: &str, callable: PlaceholderFormatter) {
+ FORMATTERS.with(|formatters| {
+ let mut formatters = formatters.borrow_mut();
+ if formatters.is_none() {
+ *formatters = Some(Self::init_placeholder_formatters());
+ }
+
+ formatters
+ .as_mut()
+ .unwrap()
+ .insert(name.to_string(), callable);
+ });
}
- pub fn advance(&mut self, _step: i64) {
- todo!()
+ /// Gets the placeholder formatter for a given name.
+ ///
+ /// `$name` The placeholder name (including the delimiter char like %)
+ pub fn get_placeholder_formatter_definition(name: &str) -> Option<()> {
+ // Note: the returned callable cannot be cloned out of the thread-local
+ // map; call sites invoke the formatter via the map directly.
+ FORMATTERS.with(|formatters| {
+ let mut formatters = formatters.borrow_mut();
+ if formatters.is_none() {
+ *formatters = Some(Self::init_placeholder_formatters());
+ }
+
+ formatters.as_ref().unwrap().get(name).map(|_| ())
+ })
}
- pub fn finish(&mut self) {
- todo!()
+ /// Sets a format for a given name.
+ ///
+ /// This method also allow you to override an existing format.
+ ///
+ /// `$name` The format name
+ /// `$format` A format string
+ pub fn set_format_definition(name: &str, format: &str) {
+ FORMATS.with(|formats| {
+ let mut formats = formats.borrow_mut();
+ if formats.is_none() {
+ *formats = Some(Self::init_formats());
+ }
+
+ formats
+ .as_mut()
+ .unwrap()
+ .insert(name.to_string(), format.to_string());
+ });
}
- pub fn set_format(&mut self, _format: &str) {
- todo!()
+ /// Gets the format for a given name.
+ ///
+ /// `$name` The format name
+ pub fn get_format_definition(name: &str) -> Option<String> {
+ FORMATS.with(|formats| {
+ let mut formats = formats.borrow_mut();
+ if formats.is_none() {
+ *formats = Some(Self::init_formats());
+ }
+
+ formats.as_ref().unwrap().get(name).cloned()
+ })
}
- pub fn get_progress(&self) -> i64 {
- todo!()
+ /// Associates a text with a named placeholder.
+ ///
+ /// The text is displayed when the progress bar is rendered but only
+ /// when the corresponding placeholder is part of the custom format line
+ /// (by wrapping the name with %).
+ ///
+ /// `$message` The text to associate with the placeholder
+ /// `$name` The name of the placeholder
+ pub fn set_message(&mut self, message: &str, name: &str) {
+ self.messages.insert(name.to_string(), message.to_string());
+ }
+
+ pub fn get_message(&self, name: &str) -> Option<String> {
+ self.messages.get(name).cloned()
+ }
+
+ pub fn get_start_time(&self) -> i64 {
+ self.start_time
}
pub fn get_max_steps(&self) -> i64 {
- todo!()
+ self.max
+ }
+
+ pub fn get_progress(&self) -> i64 {
+ self.step
+ }
+
+ fn get_step_width(&self) -> i64 {
+ self.step_width
+ }
+
+ pub fn get_progress_percent(&self) -> f64 {
+ self.percent
+ }
+
+ pub fn get_bar_offset(&self) -> f64 {
+ shirabe_php_shim::floor(if self.max != 0 {
+ self.percent * self.bar_width as f64
+ } else if self.redraw_freq.is_none() {
+ ((shirabe_php_shim::min(5, self.bar_width / 15) * self.write_count) % self.bar_width)
+ as f64
+ } else {
+ (self.step % self.bar_width) as f64
+ })
+ }
+
+ pub fn get_estimated(&self) -> f64 {
+ if self.step == 0 {
+ return 0.0;
+ }
+
+ shirabe_php_shim::round(
+ (shirabe_php_shim::time() - self.start_time) as f64 / self.step as f64
+ * self.max as f64,
+ 0,
+ )
+ }
+
+ pub fn get_remaining(&self) -> f64 {
+ if self.step == 0 {
+ return 0.0;
+ }
+
+ shirabe_php_shim::round(
+ (shirabe_php_shim::time() - self.start_time) as f64 / self.step as f64
+ * (self.max - self.step) as f64,
+ 0,
+ )
+ }
+
+ pub fn set_bar_width(&mut self, size: i64) {
+ self.bar_width = shirabe_php_shim::max(1, size);
+ }
+
+ pub fn get_bar_width(&self) -> i64 {
+ self.bar_width
+ }
+
+ pub fn set_bar_character(&mut self, char: &str) {
+ self.bar_char = Some(char.to_string());
+ }
+
+ pub fn get_bar_character(&self) -> String {
+ match &self.bar_char {
+ Some(bar_char) => bar_char.clone(),
+ None => {
+ if self.max != 0 {
+ "=".to_string()
+ } else {
+ self.empty_bar_char.clone()
+ }
+ }
+ }
+ }
+
+ pub fn set_empty_bar_character(&mut self, char: &str) {
+ self.empty_bar_char = char.to_string();
+ }
+
+ pub fn get_empty_bar_character(&self) -> String {
+ self.empty_bar_char.clone()
+ }
+
+ pub fn set_progress_character(&mut self, char: &str) {
+ self.progress_char = char.to_string();
+ }
+
+ pub fn get_progress_character(&self) -> String {
+ self.progress_char.clone()
+ }
+
+ pub fn set_format(&mut self, format: &str) {
+ self.format = None;
+ self.internal_format = Some(format.to_string());
+ }
+
+ /// Sets the redraw frequency.
+ ///
+ /// `$freq` The frequency in steps
+ pub fn set_redraw_frequency(&mut self, freq: Option<i64>) {
+ self.redraw_freq = freq.map(|freq| shirabe_php_shim::max(1, freq));
+ }
+
+ pub fn min_seconds_between_redraws(&mut self, seconds: f64) {
+ self.min_seconds_between_redraws = seconds;
+ }
+
+ pub fn max_seconds_between_redraws(&mut self, seconds: f64) {
+ self.max_seconds_between_redraws = seconds;
+ }
+
+ /// Returns an iterator that will automatically update the progress bar when iterated.
+ ///
+ /// `$max` Number of steps to complete the bar (0 if indeterminate), if null it will be
+ /// inferred from `$iterable`
+ pub fn iterate(
+ &mut self,
+ iterable: Vec<(shirabe_php_shim::PhpMixed, shirabe_php_shim::PhpMixed)>,
+ max: Option<i64>,
+ ) -> anyhow::Result<Vec<(shirabe_php_shim::PhpMixed, shirabe_php_shim::PhpMixed)>> {
+ self.start(Some(max.unwrap_or_else(|| {
+ // is_countable($iterable) ? \count($iterable) : 0
+ iterable.len() as i64
+ })))?;
+
+ let mut yielded = Vec::new();
+ for (key, value) in iterable {
+ yielded.push((key, value));
+
+ self.advance(1)?;
+ }
+
+ self.finish()?;
+
+ Ok(yielded)
+ }
+
+ /// Starts the progress output.
+ ///
+ /// `$max` Number of steps to complete the bar (0 if indeterminate), null to leave unchanged
+ pub fn start(&mut self, max: Option<i64>) -> anyhow::Result<()> {
+ self.start_time = shirabe_php_shim::time();
+ self.step = 0;
+ self.percent = 0.0;
+
+ if let Some(max) = max {
+ self.set_max_steps(max);
+ }
+
+ self.display()
+ }
+
+ /// Advances the progress output X steps.
+ ///
+ /// `$step` Number of steps to advance
+ pub fn advance(&mut self, step: i64) -> anyhow::Result<()> {
+ self.set_progress(self.step + step)
+ }
+
+ /// Sets whether to overwrite the progressbar, false for new line.
+ pub fn set_overwrite(&mut self, overwrite: bool) {
+ self.overwrite = overwrite;
+ }
+
+ pub fn set_progress(&mut self, mut step: i64) -> anyhow::Result<()> {
+ if self.max != 0 && step > self.max {
+ self.max = step;
+ } else if step < 0 {
+ step = 0;
+ }
+
+ let redraw_freq = match self.redraw_freq {
+ Some(redraw_freq) => redraw_freq as f64,
+ None => (if self.max != 0 { self.max } else { 10 }) as f64 / 10.0,
+ };
+ let prev_period = (self.step as f64 / redraw_freq) as i64;
+ let curr_period = (step as f64 / redraw_freq) as i64;
+ self.step = step;
+ self.percent = if self.max != 0 {
+ self.step as f64 / self.max as f64
+ } else {
+ 0.0
+ };
+ let time_interval = shirabe_php_shim::microtime(true) - self.last_write_time;
+
+ // Draw regardless of other limits
+ if self.max == step {
+ self.display()?;
+
+ return Ok(());
+ }
+
+ // Throttling
+ if time_interval < self.min_seconds_between_redraws {
+ return Ok(());
+ }
+
+ // Draw each step period, but not too late
+ if prev_period != curr_period || time_interval >= self.max_seconds_between_redraws {
+ self.display()?;
+ }
+
+ Ok(())
+ }
+
+ pub fn set_max_steps(&mut self, max: i64) {
+ self.format = None;
+ self.max = shirabe_php_shim::max(0, max);
+ self.step_width = if self.max != 0 {
+ Helper::width(&self.max.to_string())
+ } else {
+ 4
+ };
+ }
+
+ /// Finishes the progress output.
+ pub fn finish(&mut self) -> anyhow::Result<()> {
+ if self.max == 0 {
+ self.max = self.step;
+ }
+
+ if self.step == self.max && !self.overwrite {
+ // prevent double 100% output
+ return Ok(());
+ }
+
+ self.set_progress(self.max)
+ }
+
+ /// Outputs the current progress string.
+ pub fn display(&mut self) -> anyhow::Result<()> {
+ if output_interface::VERBOSITY_QUIET == self.output.borrow().get_verbosity() {
+ return Ok(());
+ }
+
+ if self.format.is_none() {
+ let format = match &self.internal_format {
+ Some(internal_format) if !internal_format.is_empty() => internal_format.clone(),
+ _ => self.determine_best_format().to_string(),
+ };
+ self.set_real_format(&format);
+ }
+
+ let line = self.build_line()?;
+ self.overwrite(&line);
+
+ Ok(())
+ }
+
+ /// Removes the progress bar from the current line.
+ ///
+ /// This is useful if you wish to write some output
+ /// while a progress bar is running.
+ /// Call display() to show the progress bar again.
+ pub fn clear(&mut self) -> anyhow::Result<()> {
+ if !self.overwrite {
+ return Ok(());
+ }
+
+ if self.format.is_none() {
+ let format = match &self.internal_format {
+ Some(internal_format) if !internal_format.is_empty() => internal_format.clone(),
+ _ => self.determine_best_format().to_string(),
+ };
+ self.set_real_format(&format);
+ }
+
+ self.overwrite("");
+
+ Ok(())
+ }
+
+ fn set_real_format(&mut self, format: &str) {
+ // try to use the _nomax variant if available
+ if self.max == 0 && Self::get_format_definition(&format!("{format}_nomax")).is_some() {
+ self.format = Self::get_format_definition(&format!("{format}_nomax"));
+ } else if Self::get_format_definition(format).is_some() {
+ self.format = Self::get_format_definition(format);
+ } else {
+ self.format = Some(format.to_string());
+ }
+ }
+
+ /// Overwrites a previous message to the output.
+ fn overwrite(&mut self, message: &str) {
+ if self.previous_message.as_deref() == Some(message) {
+ return;
+ }
+
+ let original_message = message.to_string();
+ let mut message = message.to_string();
+
+ if self.overwrite {
+ if let Some(previous_message) = self.previous_message.clone() {
+ if todo!("$this->output instanceof ConsoleSectionOutput") {
+ let message_lines = shirabe_php_shim::explode("\n", &previous_message);
+ let mut line_count = message_lines.len() as i64;
+ for message_line in &message_lines {
+ let message_line_length = Helper::width(&Helper::remove_decoration(
+ todo!("$this->output->getFormatter()"),
+ message_line,
+ ));
+ if message_line_length > self.terminal.get_width() {
+ line_count += shirabe_php_shim::floor(
+ message_line_length as f64 / self.terminal.get_width() as f64,
+ ) as i64;
+ }
+ }
+ todo!("$this->output->clear($lineCount); (ConsoleSectionOutput)");
+ } else {
+ let line_count = shirabe_php_shim::substr_count(&previous_message, "\n");
+ for _i in 0..line_count {
+ self.cursor.move_to_column(1);
+ self.cursor.clear_line();
+ self.cursor.move_up(1);
+ }
+
+ self.cursor.move_to_column(1);
+ self.cursor.clear_line();
+ }
+ }
+ } else if self.step > 0 {
+ message = format!("{}{}", shirabe_php_shim::PHP_EOL, message);
+ }
+
+ self.previous_message = Some(original_message);
+ self.last_write_time = shirabe_php_shim::microtime(true);
+
+ self.output
+ .borrow()
+ .write(&[message], false, output_interface::OUTPUT_NORMAL);
+ self.write_count += 1;
}
- pub fn set_progress(&mut self, _step: i64) {
- todo!()
+ fn determine_best_format(&self) -> &'static str {
+ match self.output.borrow().get_verbosity() {
+ // OutputInterface::VERBOSITY_QUIET: display is disabled anyway
+ output_interface::VERBOSITY_VERBOSE => {
+ if self.max != 0 {
+ FORMAT_VERBOSE
+ } else {
+ FORMAT_VERBOSE_NOMAX
+ }
+ }
+ output_interface::VERBOSITY_VERY_VERBOSE => {
+ if self.max != 0 {
+ FORMAT_VERY_VERBOSE
+ } else {
+ FORMAT_VERY_VERBOSE_NOMAX
+ }
+ }
+ output_interface::VERBOSITY_DEBUG => {
+ if self.max != 0 {
+ FORMAT_DEBUG
+ } else {
+ FORMAT_DEBUG_NOMAX
+ }
+ }
+ _ => {
+ if self.max != 0 {
+ FORMAT_NORMAL
+ } else {
+ FORMAT_NORMAL_NOMAX
+ }
+ }
+ }
+ }
+
+ fn init_placeholder_formatters() -> IndexMap<String, PlaceholderFormatter> {
+ let mut formatters: IndexMap<String, PlaceholderFormatter> = IndexMap::new();
+
+ formatters.insert(
+ "bar".to_string(),
+ Box::new(
+ |bar: &ProgressBar, output: &Rc<RefCell<dyn OutputInterface>>| {
+ let complete_bars = bar.get_bar_offset();
+ let mut display = shirabe_php_shim::str_repeat(
+ &bar.get_bar_character(),
+ complete_bars as usize,
+ );
+ if complete_bars < bar.get_bar_width() as f64 {
+ let empty_bars = bar.get_bar_width() as f64
+ - complete_bars
+ - Helper::length(&Helper::remove_decoration(
+ todo!("$output->getFormatter()"),
+ &bar.get_progress_character(),
+ )) as f64;
+ display.push_str(&format!(
+ "{}{}",
+ bar.get_progress_character(),
+ shirabe_php_shim::str_repeat(
+ &bar.get_empty_bar_character(),
+ empty_bars as usize
+ )
+ ));
+ let _ = output;
+ }
+
+ Ok(Ok(shirabe_php_shim::PhpMixed::String(display)))
+ },
+ ),
+ );
+
+ formatters.insert(
+ "elapsed".to_string(),
+ Box::new(
+ |bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ Ok(Ok(shirabe_php_shim::PhpMixed::String(
+ Helper::format_time(
+ (shirabe_php_shim::time() - bar.get_start_time()) as f64,
+ )
+ .unwrap_or_default(),
+ )))
+ },
+ ),
+ );
+
+ formatters.insert(
+ "remaining".to_string(),
+ Box::new(|bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ if bar.get_max_steps() == 0 {
+ return Ok(Err(LogicException(shirabe_php_shim::LogicException {
+ message: "Unable to display the remaining time if the maximum number of steps is not set.".to_string(),
+ code: 0,
+ })));
+ }
+
+ Ok(Ok(shirabe_php_shim::PhpMixed::String(
+ Helper::format_time(bar.get_remaining()).unwrap_or_default(),
+ )))
+ }),
+ );
+
+ formatters.insert(
+ "estimated".to_string(),
+ Box::new(|bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ if bar.get_max_steps() == 0 {
+ return Ok(Err(LogicException(shirabe_php_shim::LogicException {
+ message: "Unable to display the estimated time if the maximum number of steps is not set.".to_string(),
+ code: 0,
+ })));
+ }
+
+ Ok(Ok(shirabe_php_shim::PhpMixed::String(
+ Helper::format_time(bar.get_estimated()).unwrap_or_default(),
+ )))
+ }),
+ );
+
+ formatters.insert(
+ "memory".to_string(),
+ Box::new(
+ |_bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ Ok(Ok(shirabe_php_shim::PhpMixed::String(
+ Helper::format_memory(shirabe_php_shim::memory_get_usage()),
+ )))
+ },
+ ),
+ );
+
+ formatters.insert(
+ "current".to_string(),
+ Box::new(
+ |bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ Ok(Ok(shirabe_php_shim::PhpMixed::String(
+ shirabe_php_shim::str_pad(
+ &bar.get_progress().to_string(),
+ bar.get_step_width() as usize,
+ " ",
+ shirabe_php_shim::STR_PAD_LEFT,
+ ),
+ )))
+ },
+ ),
+ );
+
+ formatters.insert(
+ "max".to_string(),
+ Box::new(
+ |bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ Ok(Ok(shirabe_php_shim::PhpMixed::Int(bar.get_max_steps())))
+ },
+ ),
+ );
+
+ formatters.insert(
+ "percent".to_string(),
+ Box::new(
+ |bar: &ProgressBar, _output: &Rc<RefCell<dyn OutputInterface>>| {
+ Ok(Ok(shirabe_php_shim::PhpMixed::Float(
+ shirabe_php_shim::floor(bar.get_progress_percent() * 100.0),
+ )))
+ },
+ ),
+ );
+
+ formatters
+ }
+
+ fn init_formats() -> IndexMap<String, String> {
+ let mut formats: IndexMap<String, String> = IndexMap::new();
+
+ formats.insert(
+ FORMAT_NORMAL.to_string(),
+ " %current%/%max% [%bar%] %percent:3s%%".to_string(),
+ );
+ formats.insert(
+ FORMAT_NORMAL_NOMAX.to_string(),
+ " %current% [%bar%]".to_string(),
+ );
+
+ formats.insert(
+ FORMAT_VERBOSE.to_string(),
+ " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%".to_string(),
+ );
+ formats.insert(
+ FORMAT_VERBOSE_NOMAX.to_string(),
+ " %current% [%bar%] %elapsed:6s%".to_string(),
+ );
+
+ formats.insert(
+ FORMAT_VERY_VERBOSE.to_string(),
+ " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%".to_string(),
+ );
+ formats.insert(
+ FORMAT_VERY_VERBOSE_NOMAX.to_string(),
+ " %current% [%bar%] %elapsed:6s%".to_string(),
+ );
+
+ formats.insert(
+ FORMAT_DEBUG.to_string(),
+ " %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%"
+ .to_string(),
+ );
+ formats.insert(
+ FORMAT_DEBUG_NOMAX.to_string(),
+ " %current% [%bar%] %elapsed:6s% %memory:6s%".to_string(),
+ );
+
+ formats
+ }
+
+ fn build_line(&mut self) -> anyhow::Result<String> {
+ let regex = "{%([a-z\\-_]+)(?:\\:([^%]+))?%}i";
+
+ // The callback resolves a placeholder match into its replacement text.
+ // It is invoked by preg_replace_callback over $this->format.
+ let line = self.build_line_apply(regex)?;
+
+ // gets string length for each sub line with multiline format
+ let lines_length: Vec<i64> = shirabe_php_shim::explode("\n", &line)
+ .iter()
+ .map(|sub_line| {
+ Helper::width(&Helper::remove_decoration(
+ todo!("$this->output->getFormatter()"),
+ &shirabe_php_shim::rtrim(sub_line, Some("\r")),
+ ))
+ })
+ .collect();
+
+ let lines_width = *lines_length.iter().max().unwrap();
+
+ let terminal_width = self.terminal.get_width();
+ if lines_width <= terminal_width {
+ return Ok(line);
+ }
+
+ self.set_bar_width(self.bar_width - lines_width + terminal_width);
+
+ self.build_line_apply(regex)
+ }
+
+ /// Applies the placeholder-resolving callback over `$this->format`, mirroring
+ /// the `preg_replace_callback` invocation in PHP's `buildLine()`.
+ fn build_line_apply(&self, regex: &str) -> anyhow::Result<String> {
+ let format = self.format.clone().unwrap_or_default();
+
+ // $callback in PHP, expressed as a closure over $this and the matches.
+ let callback = |matches: &[Option<String>]| -> anyhow::Result<String> {
+ let name = matches[1].clone().unwrap_or_default();
+
+ let text: shirabe_php_shim::PhpMixed =
+ if Self::get_placeholder_formatter_definition(&name).is_some() {
+ // $text = $formatter($this, $this->output);
+ let formatter_result = FORMATTERS.with(|formatters| {
+ let formatters = formatters.borrow();
+ let formatter = formatters.as_ref().unwrap().get(&name).unwrap();
+ formatter(self, &self.output)
+ });
+ match formatter_result? {
+ Ok(text) => text,
+ Err(e) => return Err(anyhow::Error::new(e)),
+ }
+ } else if let Some(message) = self.messages.get(&name) {
+ shirabe_php_shim::PhpMixed::String(message.clone())
+ } else {
+ return Ok(matches[0].clone().unwrap_or_default());
+ };
+
+ if let Some(modifier) = matches.get(2).and_then(|m| m.clone()) {
+ return Ok(shirabe_php_shim::sprintf(&format!("%{modifier}"), &[text]));
+ }
+
+ // PHP implicitly casts the formatter result to string here.
+ Ok(match text {
+ shirabe_php_shim::PhpMixed::String(s) => s,
+ shirabe_php_shim::PhpMixed::Int(i) => i.to_string(),
+ shirabe_php_shim::PhpMixed::Float(f) => {
+ shirabe_php_shim::sprintf("%s", &[shirabe_php_shim::PhpMixed::Float(f)])
+ }
+ other => shirabe_php_shim::sprintf("%s", &[other]),
+ })
+ };
+
+ shirabe_php_shim::preg_replace_callback(regex, callback, &format)
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs
index c17cb6b..7ddb620 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs
@@ -1,25 +1,933 @@
-use crate::symfony::console::helper::HelperInterface;
-use crate::symfony::console::input::InputInterface;
-use crate::symfony::console::output::OutputInterface;
-use crate::symfony::console::question::Question;
+use crate::symfony::console::cursor::Cursor;
+use crate::symfony::console::exception::missing_input_exception::MissingInputException;
+use crate::symfony::console::exception::runtime_exception::RuntimeException;
+use crate::symfony::console::formatter::output_formatter::OutputFormatter;
+use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle;
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::helper::helper_interface::HelperInterface;
+use crate::symfony::console::helper::helper_set::HelperSet;
+use crate::symfony::console::input::input_interface::InputInterface;
+use crate::symfony::console::output::console_output::ConsoleOutput;
+use crate::symfony::console::output::console_output_interface::ConsoleOutputInterface;
+use crate::symfony::console::output::console_section_output::ConsoleSectionOutput;
+use crate::symfony::console::output::output_interface;
+use crate::symfony::console::output::output_interface::OutputInterface;
+use crate::symfony::console::question::choice_question::ChoiceQuestion;
+use crate::symfony::console::question::question::Question;
+use crate::symfony::console::terminal::Terminal;
+use crate::symfony::string::s;
+use shirabe_php_shim::AsAny;
use shirabe_php_shim::PhpMixed;
+use std::cell::RefCell;
+use std::rc::Rc;
-#[derive(Debug)]
-pub struct QuestionHelper;
+/// The QuestionHelper class provides helpers to interact with the user.
+#[derive(Debug, Default)]
+pub struct QuestionHelper {
+ pub(crate) inner: Helper,
+
+ /// @var resource|null
+ input_stream: Option<shirabe_php_shim::PhpResource>,
+}
+
+/// self::$stty
+static STTY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
+/// self::$stdinIsInteractive
+static STDIN_IS_INTERACTIVE: std::sync::Mutex<Option<bool>> = std::sync::Mutex::new(None);
impl QuestionHelper {
+ /// Asks a question to the user.
+ ///
+ /// @return mixed The user answer
+ ///
+ /// @throws RuntimeException If there is no data to read in the input stream
pub fn ask(
+ &mut self,
+ input: &mut dyn InputInterface,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ question: &Question,
+ ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> {
+ let mut output = output;
+ let error_output = {
+ let borrowed = output.borrow();
+ (*borrowed)
+ .as_any()
+ .downcast_ref::<ConsoleOutput>()
+ .map(|console_output| console_output.get_error_output())
+ };
+ if let Some(error_output) = error_output {
+ output = error_output;
+ }
+
+ if !input.is_interactive() {
+ return Ok(Ok(self.get_default_answer(question)));
+ }
+
+ // TODO(phase-b): `$input instanceof StreamableInputInterface` cannot be
+ // expressed as a trait-object-to-trait-object downcast, and no concrete
+ // streamable input type is wired up yet. The stream is left unset so the
+ // helper falls back to STDIN. Revisit once StreamableInputInterface has a
+ // concrete implementor and the PhpResource/PhpMixed split is resolved.
+ let _ = &mut self.input_stream;
+
+ let result: anyhow::Result<Result<PhpMixed, MissingInputException>> = (|| {
+ if question.get_validator().is_none() {
+ return self.do_ask(Rc::clone(&output), question);
+ }
+
+ let interviewer = || self.do_ask(Rc::clone(&output), question);
+
+ self.validate_attempts(&interviewer, Rc::clone(&output), question)
+ })();
+
+ let result = result?;
+ match result {
+ Ok(value) => Ok(Ok(value)),
+ Err(exception) => {
+ input.set_interactive(false);
+
+ let fallback_output = self.get_default_answer(question);
+ if matches!(fallback_output, PhpMixed::Null) {
+ return Ok(Err(exception));
+ }
+
+ Ok(Ok(fallback_output))
+ }
+ }
+ }
+
+ pub fn get_name(&self) -> String {
+ "question".to_string()
+ }
+
+ /// Prevents usage of stty.
+ pub fn disable_stty() {
+ STTY.store(false, std::sync::atomic::Ordering::SeqCst);
+ }
+
+ /// Asks the question to the user.
+ ///
+ /// @return mixed
+ ///
+ /// @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
+ fn do_ask(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ question: &Question,
+ ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> {
+ self.write_prompt(Rc::clone(&output), question);
+
+ let input_stream = self
+ .input_stream
+ .clone()
+ .unwrap_or_else(shirabe_php_shim::stdin);
+ let autocomplete = question.get_autocompleter_callback();
+
+ let ret: PhpMixed;
+ if autocomplete.is_none()
+ || !STTY.load(std::sync::atomic::Ordering::SeqCst)
+ || !Terminal::has_stty_available()
+ {
+ let mut r: PhpMixed = PhpMixed::Bool(false);
+ if question.is_hidden() {
+ match self.get_hidden_response(
+ Rc::clone(&output),
+ &input_stream,
+ question.is_trimmable(),
+ )? {
+ Ok(hidden_response) => {
+ r = PhpMixed::String(if question.is_trimmable() {
+ shirabe_php_shim::trim(&hidden_response, None)
+ } else {
+ hidden_response
+ });
+ }
+ Err(e) => {
+ if !question.is_hidden_fallback() {
+ return Err(e.into());
+ }
+ }
+ }
+ }
+
+ if matches!(r, PhpMixed::Bool(false)) {
+ let is_blocked = shirabe_php_shim::stream_get_meta_data(&input_stream)
+ .get("blocked")
+ .map(|v| (**v).clone())
+ .unwrap_or(PhpMixed::Bool(true));
+
+ if !shirabe_php_shim::boolval(&is_blocked) {
+ shirabe_php_shim::stream_set_blocking(&input_stream, true);
+ }
+
+ let read = self.read_input(&input_stream, question);
+
+ if !shirabe_php_shim::boolval(&is_blocked) {
+ shirabe_php_shim::stream_set_blocking(&input_stream, false);
+ }
+
+ if matches!(read, PhpMixed::Bool(false)) {
+ return Ok(Err(MissingInputException(RuntimeException(
+ shirabe_php_shim::RuntimeException {
+ message: "Aborted.".to_string(),
+ code: 0,
+ },
+ ))));
+ }
+ r = read;
+ if question.is_trimmable() {
+ r = PhpMixed::String(shirabe_php_shim::trim(&r.to_string(), None));
+ }
+ }
+ ret = r;
+ } else {
+ let callback = autocomplete.unwrap();
+ // The autocompleter callback yields an iterable (Option here); PHP
+ // treats a null result as an empty list of suggestions.
+ let callback = move |input: &str| callback(input).unwrap_or_default();
+ let autocomplete =
+ self.autocomplete(Rc::clone(&output), question, &input_stream, &callback);
+ ret = PhpMixed::String(if question.is_trimmable() {
+ shirabe_php_shim::trim(&autocomplete, None)
+ } else {
+ autocomplete
+ });
+ }
+
+ let mut ret = ret;
+ {
+ let borrowed = output.borrow();
+ if let Some(section_output) =
+ (*borrowed).as_any().downcast_ref::<ConsoleSectionOutput>()
+ {
+ section_output.add_content(&ret.to_string());
+ }
+ }
+
+ ret = if shirabe_php_shim::strlen(&ret.to_string()) > 0 {
+ ret
+ } else {
+ question.get_default()
+ };
+
+ if let Some(normalizer) = question.get_normalizer() {
+ return Ok(Ok(normalizer(ret)));
+ }
+
+ Ok(Ok(ret))
+ }
+
+ /// @return mixed
+ fn get_default_answer(&self, question: &Question) -> PhpMixed {
+ let default = question.get_default();
+
+ if matches!(default, PhpMixed::Null) {
+ return default;
+ }
+
+ if let Some(validator) = question.get_validator() {
+ // call_user_func($question->getValidator(), $default)
+ return validator(Some(default)).unwrap();
+ } else if let Some(choice_question) = question_as_choice_question(question) {
+ let choices = choice_question.get_choices();
+
+ if !choice_question.is_multiselect() {
+ return choices
+ .get(&default.to_string())
+ .map(|v| (**v).clone())
+ .unwrap_or(default);
+ }
+
+ let default_parts = shirabe_php_shim::explode(",", &default.to_string());
+ let mut resolved: indexmap::IndexMap<String, Box<PhpMixed>> = indexmap::IndexMap::new();
+ for (k, v) in default_parts.iter().enumerate() {
+ let v = if question.is_trimmable() {
+ shirabe_php_shim::trim(v, None)
+ } else {
+ v.clone()
+ };
+ let value = choices
+ .get(&v)
+ .map(|value| (**value).clone())
+ .unwrap_or(PhpMixed::String(v));
+ resolved.insert(k.to_string(), Box::new(value));
+ }
+
+ return PhpMixed::Array(resolved);
+ }
+
+ default
+ }
+
+ /// Outputs the question prompt.
+ pub(crate) fn write_prompt(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ question: &Question,
+ ) {
+ let mut message = question.get_question().to_string();
+
+ if let Some(choice_question) = question_as_choice_question(question) {
+ let mut lines = vec![question.get_question().to_string()];
+ lines.extend(self.format_choice_question_choices(choice_question, "info"));
+ output
+ .borrow()
+ .writeln(&lines, output_interface::OUTPUT_NORMAL);
+
+ message = choice_question.get_prompt().to_string();
+ }
+
+ output
+ .borrow()
+ .write(&[message], false, output_interface::OUTPUT_NORMAL);
+ }
+
+ /// @return string[]
+ pub(crate) fn format_choice_question_choices(
+ &self,
+ question: &ChoiceQuestion,
+ tag: &str,
+ ) -> Vec<String> {
+ let mut messages: Vec<String> = vec![];
+
+ let choices = question.get_choices();
+ let max_width = choices
+ .keys()
+ .map(|key| Helper::width(key))
+ .max()
+ .unwrap_or(0);
+
+ for (key, value) in choices {
+ let padding =
+ shirabe_php_shim::str_repeat(" ", (max_width - Helper::width(key)) as usize);
+
+ messages.push(shirabe_php_shim::sprintf(
+ &format!(" [<{tag}>%s{padding}</{tag}>] %s"),
+ &[PhpMixed::String(key.clone()), (**value).clone()],
+ ));
+ }
+
+ messages
+ }
+
+ /// Outputs an error message.
+ pub(crate) fn write_error(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ error: &shirabe_php_shim::Exception,
+ ) {
+ let message;
+ if let Some(helper_set) = self.get_helper_set() {
+ if helper_set.borrow().has("formatter") {
+ // PHP: `$this->getHelperSet()->get('formatter')->formatBlock(...)`.
+ // HelperSet::get yields `dyn HelperInterface`, which does not have
+ // `AsAny` as a supertrait, so it cannot be downcast to
+ // FormatterHelper. Resolved once HelperInterface gains AsAny (see
+ // report).
+ let _formatter = helper_set.borrow().get("formatter").unwrap();
+ todo!("downcast dyn HelperInterface to FormatterHelper for format_block");
+ } else {
+ message = format!("<error>{}</error>", error.message);
+ }
+ } else {
+ message = format!("<error>{}</error>", error.message);
+ }
+
+ output
+ .borrow()
+ .writeln(&[message], output_interface::OUTPUT_NORMAL);
+ }
+
+ /// Autocompletes a question.
+ ///
+ /// @param resource $inputStream
+ fn autocomplete(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ question: &Question,
+ input_stream: &shirabe_php_shim::PhpResource,
+ autocomplete: &dyn Fn(&str) -> Vec<PhpMixed>,
+ ) -> String {
+ // TODO(phase-b): Cursor takes Option<PhpMixed>, but the input stream is a
+ // PhpResource and PhpMixed has no resource variant. Defaulting to STDIN
+ // until the PhpResource/PhpMixed stream representation is unified.
+ let cursor = Cursor::new(Rc::clone(&output), None);
+
+ let mut full_choice = String::new();
+ let mut ret = String::new();
+
+ let mut i: i64 = 0;
+ let mut ofs: i64 = -1;
+ let mut matches = autocomplete(&ret);
+ let mut num_matches = matches.len() as i64;
+
+ let stty_mode = shirabe_php_shim::shell_exec("stty -g").unwrap_or_default();
+ let is_stdin = shirabe_php_shim::stream_get_meta_data(input_stream)
+ .get("uri")
+ .map(|uri| uri.to_string() == "php://stdin")
+ .unwrap_or(false);
+ let mut r = vec![input_stream.clone()];
+ let w: Vec<shirabe_php_shim::PhpResource> = vec![];
+
+ // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
+ shirabe_php_shim::shell_exec("stty -icanon -echo");
+
+ // Add highlighted text style
+ output.borrow().get_formatter().borrow_mut().set_style(
+ "hl",
+ Box::new(OutputFormatterStyle::new(
+ Some("black"),
+ Some("white"),
+ vec![],
+ )),
+ );
+
+ // Read a keypress
+ while !feof_resource(input_stream) {
+ while is_stdin
+ && 0 == shirabe_php_shim::stream_select(
+ &mut r,
+ &mut w.clone(),
+ &mut w.clone(),
+ 0,
+ Some(100),
+ )
+ {
+ // Give signal handlers a chance to run
+ r = vec![input_stream.clone()];
+ }
+ let mut c = fread_resource(input_stream, 1);
+
+ // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
+ if c.is_none()
+ || (ret.is_empty()
+ && c.as_deref() == Some("")
+ && matches!(question.get_default(), PhpMixed::Null))
+ {
+ shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode));
+ // throw new MissingInputException('Aborted.');
+ // autocomplete() returns string in PHP; this throw aborts the
+ // whole read. Faithful exception propagation is resolved later.
+ todo!("MissingInputException('Aborted.') thrown inside autocomplete");
+ } else if c.as_deref() == Some("\u{7f}") {
+ // Backspace Character
+ if 0 == num_matches && 0 != i {
+ i -= 1;
+ cursor.move_left(s(&full_choice).slice(-1, None).width(false));
+
+ full_choice = QuestionHelper::substr(Some(&full_choice), 0, Some(i));
+ }
+
+ if 0 == i {
+ ofs = -1;
+ matches = autocomplete(&ret);
+ num_matches = matches.len() as i64;
+ } else {
+ num_matches = 0;
+ }
+
+ // Pop the last character off the end of our string
+ ret = QuestionHelper::substr(Some(&ret), 0, Some(i));
+ } else if c.as_deref() == Some("\u{1b}") {
+ // Did we read an escape sequence?
+ let escape = fread_resource(input_stream, 2).unwrap_or_default();
+ let cc = format!("{}{}", c.clone().unwrap_or_default(), escape);
+ c = Some(cc.clone());
+
+ // A = Up Arrow. B = Down Arrow
+ let c2 = cc.as_bytes().get(2).copied();
+ if c2 == Some(b'A') || c2 == Some(b'B') {
+ if c2 == Some(b'A') && -1 == ofs {
+ ofs = 0;
+ }
+
+ if 0 == num_matches {
+ continue;
+ }
+
+ ofs += if c2 == Some(b'A') { -1 } else { 1 };
+ ofs = (num_matches + ofs) % num_matches;
+ }
+ } else if shirabe_php_shim::ord(c.as_deref().unwrap_or("")) < 32 {
+ if c.as_deref() == Some("\t") || c.as_deref() == Some("\n") {
+ if num_matches > 0 && -1 != ofs {
+ ret = matches[ofs as usize].to_string();
+ // Echo out remaining chars for current match
+ let remaining_characters = shirabe_php_shim::substr(
+ &ret,
+ shirabe_php_shim::strlen(&shirabe_php_shim::trim(
+ &self.most_recently_entered_value(&full_choice),
+ None,
+ )),
+ None,
+ );
+ output.borrow().write(
+ &[remaining_characters.clone()],
+ false,
+ output_interface::OUTPUT_NORMAL,
+ );
+ full_choice.push_str(&remaining_characters);
+ i = match shirabe_php_shim::mb_detect_encoding(&full_choice, None, true) {
+ None => shirabe_php_shim::strlen(&full_choice),
+ Some(encoding) => shirabe_php_shim::mb_strlen(&full_choice, &encoding),
+ };
+
+ let ret_for_filter = ret.clone();
+ matches = autocomplete(&ret)
+ .into_iter()
+ .filter(|m| {
+ ret_for_filter.is_empty()
+ || shirabe_php_shim::str_starts_with(
+ &m.to_string(),
+ &ret_for_filter,
+ )
+ })
+ .collect();
+ num_matches = matches.len() as i64;
+ ofs = -1;
+ }
+
+ if c.as_deref() == Some("\n") {
+ output.borrow().write(
+ &[c.clone().unwrap_or_default()],
+ false,
+ output_interface::OUTPUT_NORMAL,
+ );
+ break;
+ }
+
+ num_matches = 0;
+ }
+
+ continue;
+ } else {
+ let cur = c.clone().unwrap_or_default();
+ if "\u{80}" <= cur.as_str() {
+ let len = match shirabe_php_shim::str_bitand(&cur, "\u{f0}").as_str() {
+ "\u{c0}" => 1,
+ "\u{d0}" => 1,
+ "\u{e0}" => 2,
+ "\u{f0}" => 3,
+ _ => 0,
+ };
+ let extra = fread_resource(input_stream, len).unwrap_or_default();
+ c = Some(format!("{}{}", cur, extra));
+ }
+
+ let cur = c.clone().unwrap_or_default();
+ output
+ .borrow()
+ .write(&[cur.clone()], false, output_interface::OUTPUT_NORMAL);
+ ret.push_str(&cur);
+ full_choice.push_str(&cur);
+ i += 1;
+
+ let mut temp_ret = ret.clone();
+
+ if let Some(choice_question) = question_as_choice_question(question) {
+ if choice_question.is_multiselect() {
+ temp_ret = self.most_recently_entered_value(&full_choice);
+ }
+ }
+
+ num_matches = 0;
+ ofs = 0;
+
+ for value in autocomplete(&ret) {
+ // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
+ if shirabe_php_shim::str_starts_with(&value.to_string(), &temp_ret) {
+ if (num_matches as usize) < matches.len() {
+ matches[num_matches as usize] = value;
+ } else {
+ matches.push(value);
+ }
+ num_matches += 1;
+ }
+ }
+ }
+
+ cursor.clear_line_after();
+
+ if num_matches > 0 && -1 != ofs {
+ cursor.save_position();
+ // Write highlighted text, complete the partially entered response
+ let characters_entered = shirabe_php_shim::strlen(&shirabe_php_shim::trim(
+ &self.most_recently_entered_value(&full_choice),
+ None,
+ ));
+ output.borrow().write(
+ &[format!(
+ "<hl>{}</hl>",
+ OutputFormatter::escape_trailing_backslash(&shirabe_php_shim::substr(
+ &matches[ofs as usize].to_string(),
+ characters_entered,
+ None,
+ ))
+ )],
+ false,
+ output_interface::OUTPUT_NORMAL,
+ );
+ cursor.restore_position();
+ }
+ }
+
+ // Reset stty so it behaves normally again
+ shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode));
+
+ full_choice
+ }
+
+ fn most_recently_entered_value(&self, entered: &str) -> String {
+ // Determine the most recent value that the user entered
+ if !shirabe_php_shim::str_contains(entered, ",") {
+ return entered.to_string();
+ }
+
+ let choices = shirabe_php_shim::explode(",", entered);
+ let last_choice = shirabe_php_shim::trim(&choices[choices.len() - 1], None);
+ if !last_choice.is_empty() {
+ return last_choice;
+ }
+
+ entered.to_string()
+ }
+
+ /// Gets a hidden response from user.
+ ///
+ /// @param resource $inputStream The handler resource
+ /// @param bool $trimmable Is the answer trimmable
+ ///
+ /// @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden
+ fn get_hidden_response(
&self,
- _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- _question: &Question,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ input_stream: &shirabe_php_shim::PhpResource,
+ trimmable: bool,
+ ) -> anyhow::Result<Result<String, RuntimeException>> {
+ if shirabe_php_shim::DIRECTORY_SEPARATOR == "\\" {
+ let mut exe = format!(
+ "{}/../Resources/bin/hiddeninput.exe",
+ shirabe_php_shim::dir()
+ );
+
+ // handle code running from a phar
+ let mut tmp_exe: Option<String> = None;
+ if shirabe_php_shim::substr(&magic_file(), 0, Some(5)) == "phar:" {
+ let tmp = format!("{}/hiddeninput.exe", shirabe_php_shim::sys_get_temp_dir());
+ shirabe_php_shim::copy(&exe, &tmp);
+ exe = tmp.clone();
+ tmp_exe = Some(tmp);
+ }
+
+ let s_exec = shirabe_php_shim::shell_exec(&format!("\"{}\"", exe)).unwrap_or_default();
+ let value = if trimmable {
+ shirabe_php_shim::rtrim(&s_exec, None)
+ } else {
+ s_exec
+ };
+ output
+ .borrow()
+ .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL);
+
+ if let Some(tmp) = tmp_exe {
+ shirabe_php_shim::unlink(&tmp);
+ }
+
+ return Ok(Ok(value));
+ }
+
+ let mut stty_mode = String::new();
+ if STTY.load(std::sync::atomic::Ordering::SeqCst) && Terminal::has_stty_available() {
+ stty_mode = shirabe_php_shim::shell_exec("stty -g").unwrap_or_default();
+ shirabe_php_shim::shell_exec("stty -echo");
+ } else if self.is_interactive_input(input_stream) {
+ return Ok(Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: "Unable to hide the response.".to_string(),
+ code: 0,
+ })));
+ }
+
+ let value = fgets_resource(input_stream, Some(4096));
+
+ if STTY.load(std::sync::atomic::Ordering::SeqCst) && Terminal::has_stty_available() {
+ shirabe_php_shim::shell_exec(&format!("stty {}", stty_mode));
+ }
+
+ let mut value = match value {
+ Some(value) => value,
+ None => {
+ return Err(MissingInputException(RuntimeException(
+ shirabe_php_shim::RuntimeException {
+ message: "Aborted.".to_string(),
+ code: 0,
+ },
+ ))
+ .into());
+ }
+ };
+ if trimmable {
+ value = shirabe_php_shim::trim(&value, None);
+ }
+ output
+ .borrow()
+ .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL);
+
+ Ok(Ok(value))
+ }
+
+ /// Validates an attempt.
+ ///
+ /// @param callable $interviewer A callable that will ask for a question and return the result
+ ///
+ /// @return mixed The validated response
+ ///
+ /// @throws \Exception In case the max number of attempts has been reached and no valid response has been given
+ fn validate_attempts(
+ &self,
+ interviewer: &dyn Fn() -> anyhow::Result<Result<PhpMixed, MissingInputException>>,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ question: &Question,
+ ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> {
+ let mut error: Option<shirabe_php_shim::Exception> = None;
+ let mut attempts = question.get_max_attempts();
+
+ loop {
+ // while (null === $attempts || $attempts--)
+ match attempts {
+ None => {}
+ Some(0) => break,
+ Some(n) => attempts = Some(n - 1),
+ }
+
+ if let Some(ref error) = error {
+ self.write_error(Rc::clone(&output), error);
+ }
+
+ let interviewed = match interviewer()? {
+ Ok(value) => value,
+ Err(missing) => return Ok(Err(missing)),
+ };
+
+ match question.get_validator().unwrap()(Some(interviewed)) {
+ Ok(value) => return Ok(Ok(value)),
+ Err(e) => {
+ // PHP: `catch (RuntimeException $e) { throw $e; } catch (\Exception $error) {}`.
+ // The validator return type is fixed to InvalidArgumentException here, so the
+ // RuntimeException rethrow branch is statically unreachable; record the error
+ // and retry.
+ error = Some(shirabe_php_shim::Exception {
+ message: e.0.message.clone(),
+ code: e.0.code,
+ });
+ }
+ }
+ }
+
+ // throw $error;
+ Err(anyhow::Error::msg(
+ error.map(|e| e.message).unwrap_or_default(),
+ ))
+ }
+
+ fn is_interactive_input(&self, input_stream: &shirabe_php_shim::PhpResource) -> bool {
+ let uri = shirabe_php_shim::stream_get_meta_data(input_stream)
+ .get("uri")
+ .map(|uri| uri.to_string());
+ if uri.as_deref() != Some("php://stdin") {
+ return false;
+ }
+
+ let mut stdin_is_interactive = STDIN_IS_INTERACTIVE.lock().unwrap();
+ if let Some(value) = *stdin_is_interactive {
+ return value;
+ }
+
+ let value = shirabe_php_shim::stream_isatty_resource(
+ &shirabe_php_shim::php_fopen_resource("php://stdin", "r"),
+ );
+ *stdin_is_interactive = Some(value);
+ value
+ }
+
+ /// Reads one or more lines of input and returns what is read.
+ ///
+ /// @param resource $inputStream The handler resource
+ /// @param Question $question The question being asked
+ ///
+ /// @return string|false The input received, false in case input could not be read
+ fn read_input(
+ &self,
+ input_stream: &shirabe_php_shim::PhpResource,
+ question: &Question,
) -> PhpMixed {
- todo!()
+ if !question.is_multiline() {
+ let cp = self.set_io_codepage();
+ let ret = fgets_resource(input_stream, Some(4096));
+
+ return self.reset_io_codepage(
+ cp,
+ ret.map(PhpMixed::String).unwrap_or(PhpMixed::Bool(false)),
+ );
+ }
+
+ let multi_line_stream_reader = self.clone_input_stream(input_stream);
+ let multi_line_stream_reader = match multi_line_stream_reader {
+ Some(reader) => reader,
+ None => return PhpMixed::Bool(false),
+ };
+
+ let mut ret = String::new();
+ let cp = self.set_io_codepage();
+ loop {
+ let char = shirabe_php_shim::fgetc(&multi_line_stream_reader);
+ let char = match char {
+ Some(char) => char,
+ None => break,
+ };
+ if shirabe_php_shim::PHP_EOL == format!("{}{}", ret, char) {
+ break;
+ }
+ ret.push_str(&char);
+ }
+
+ self.reset_io_codepage(cp, PhpMixed::String(ret))
+ }
+
+ /// Sets console I/O to the host code page.
+ ///
+ /// @return int Previous code page in IBM/EBCDIC format
+ fn set_io_codepage(&self) -> i64 {
+ if shirabe_php_shim::function_exists("sapi_windows_cp_set") {
+ let cp = shirabe_php_shim::sapi_windows_cp_get(None);
+ shirabe_php_shim::sapi_windows_cp_set(shirabe_php_shim::sapi_windows_cp_get(Some(
+ "oem",
+ )));
+
+ return cp;
+ }
+
+ 0
}
+
+ /// Sets console I/O to the specified code page and converts the user input.
+ ///
+ /// @param string|false $input
+ ///
+ /// @return string|false
+ fn reset_io_codepage(&self, cp: i64, input: PhpMixed) -> PhpMixed {
+ let mut input = input;
+ if 0 != cp {
+ shirabe_php_shim::sapi_windows_cp_set(cp);
+
+ if !matches!(input, PhpMixed::Bool(false)) && input.to_string() != "" {
+ input = PhpMixed::String(shirabe_php_shim::sapi_windows_cp_conv(
+ shirabe_php_shim::sapi_windows_cp_get(Some("oem")),
+ cp,
+ &input.to_string(),
+ ));
+ }
+ }
+
+ input
+ }
+
+ /// Clones an input stream in order to act on one instance of the same
+ /// stream without affecting the other instance.
+ ///
+ /// @param resource $inputStream The handler resource
+ ///
+ /// @return resource|null The cloned resource, null in case it could not be cloned
+ fn clone_input_stream(
+ &self,
+ input_stream: &shirabe_php_shim::PhpResource,
+ ) -> Option<shirabe_php_shim::PhpResource> {
+ let stream_meta_data = shirabe_php_shim::stream_get_meta_data(input_stream);
+ let seekable = stream_meta_data
+ .get("seekable")
+ .map(|v| (**v).clone())
+ .unwrap_or(PhpMixed::Bool(false));
+ let mode = stream_meta_data
+ .get("mode")
+ .map(|m| m.to_string())
+ .unwrap_or_else(|| "rb".to_string());
+ let uri = stream_meta_data.get("uri").map(|u| u.to_string());
+
+ let uri = uri?;
+
+ let clone_stream = shirabe_php_shim::php_fopen_resource(&uri, &mode);
+
+ // For seekable and writable streams, add all the same data to the
+ // cloned stream and then seek to the same offset.
+ if matches!(seekable, PhpMixed::Bool(true)) && !["r", "rb", "rt"].contains(&mode.as_str()) {
+ let offset = shirabe_php_shim::ftell(input_stream);
+ rewind_resource(input_stream);
+ stream_copy_to_stream_resource(input_stream, &clone_stream);
+ fseek_resource(input_stream, offset);
+ fseek_resource(&clone_stream, offset);
+ }
+
+ Some(clone_stream)
+ }
+
+ /// Helper::substr proxy (inherited static helper).
+ fn substr(string: Option<&str>, from: i64, length: Option<i64>) -> String {
+ Helper::substr(string.unwrap_or(""), from, length)
+ }
+}
+
+/// Downcasts a Question to a ChoiceQuestion if applicable.
+fn question_as_choice_question(question: &Question) -> Option<&ChoiceQuestion> {
+ question.as_any().downcast_ref::<ChoiceQuestion>()
+}
+
+// PHP operates on raw stream resources, but the shim models `feof`/`fread`/
+// `fgets`/`fseek`/`rewind`/`stream_copy_to_stream` over `PhpMixed`, which has no
+// resource variant. These thin resource-typed wrappers stand in until the shim
+// grows `*_resource` counterparts (see report). Each defers to the real
+// implementation once it exists.
+// PHP `__FILE__` magic constant. The shim's `file()` is PHP's file() function,
+// not the magic constant, and there is no `__FILE__` shim yet (see report).
+fn magic_file() -> String {
+ todo!("magic_file: shim needs a __FILE__ magic-constant equivalent")
+}
+
+fn feof_resource(_stream: &shirabe_php_shim::PhpResource) -> bool {
+ todo!("feof_resource: shim needs a PhpResource-based feof")
+}
+
+fn fread_resource(_stream: &shirabe_php_shim::PhpResource, _length: i64) -> Option<String> {
+ todo!("fread_resource: shim needs a PhpResource-based fread")
+}
+
+fn fgets_resource(_stream: &shirabe_php_shim::PhpResource, _length: Option<i64>) -> Option<String> {
+ todo!("fgets_resource: shim needs a PhpResource-based fgets")
+}
+
+fn fseek_resource(_stream: &shirabe_php_shim::PhpResource, _offset: i64) -> i64 {
+ todo!("fseek_resource: shim needs a PhpResource-based fseek")
+}
+
+fn rewind_resource(_stream: &shirabe_php_shim::PhpResource) -> bool {
+ todo!("rewind_resource: shim needs a PhpResource-based rewind")
+}
+
+fn stream_copy_to_stream_resource(
+ _source: &shirabe_php_shim::PhpResource,
+ _dest: &shirabe_php_shim::PhpResource,
+) -> Option<i64> {
+ todo!("stream_copy_to_stream_resource: shim needs a PhpResource-based stream_copy_to_stream")
}
impl HelperInterface for QuestionHelper {
- fn as_any(&self) -> &dyn std::any::Any {
- self
+ fn set_helper_set(&mut self, helper_set: Option<Rc<RefCell<HelperSet>>>) {
+ self.inner.set_helper_set(helper_set);
+ }
+
+ fn get_helper_set(&self) -> Option<Rc<RefCell<HelperSet>>> {
+ self.inner.get_helper_set()
+ }
+
+ fn get_name(&self) -> String {
+ self.get_name()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs b/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs
new file mode 100644
index 0000000..dd14513
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs
@@ -0,0 +1,182 @@
+use crate::symfony::console::formatter::output_formatter::OutputFormatter;
+use crate::symfony::console::helper::question_helper::QuestionHelper;
+use crate::symfony::console::output::output_interface;
+use crate::symfony::console::output::output_interface::OutputInterface;
+use crate::symfony::console::question::choice_question::ChoiceQuestion;
+use crate::symfony::console::question::confirmation_question::ConfirmationQuestion;
+use crate::symfony::console::question::question::Question;
+use crate::symfony::console::style::symfony_style::SymfonyStyle;
+use shirabe_php_shim::AsAny;
+use shirabe_php_shim::PhpMixed;
+use std::cell::RefCell;
+use std::ops::{Deref, DerefMut};
+use std::rc::Rc;
+
+/// Symfony Style Guide compliant question helper.
+#[derive(Debug, Default)]
+pub struct SymfonyQuestionHelper {
+ inner: QuestionHelper,
+}
+
+impl SymfonyQuestionHelper {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub(crate) fn write_prompt(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ question: &Question,
+ ) {
+ let mut text = OutputFormatter::escape_trailing_backslash(&question.get_question());
+ let default = question.get_default();
+
+ if question.is_multiline() {
+ text += &shirabe_php_shim::sprintf(
+ " (press %s to continue)",
+ &[PhpMixed::String(self.get_eof_shortcut())],
+ );
+ }
+
+ // switch (true)
+ if matches!(default, PhpMixed::Null) {
+ text = shirabe_php_shim::sprintf(" <info>%s</info>:", &[PhpMixed::String(text)]);
+ } else if question
+ .as_any()
+ .downcast_ref::<ConfirmationQuestion>()
+ .is_some()
+ {
+ text = shirabe_php_shim::sprintf(
+ " <info>%s (yes/no)</info> [<comment>%s</comment>]:",
+ &[
+ PhpMixed::String(text),
+ PhpMixed::String(
+ if shirabe_php_shim::boolval(&default) {
+ "yes"
+ } else {
+ "no"
+ }
+ .to_string(),
+ ),
+ ],
+ );
+ } else if let Some(choice_question) = question
+ .as_any()
+ .downcast_ref::<ChoiceQuestion>()
+ .filter(|q| q.is_multiselect())
+ {
+ let choices = choice_question.get_choices();
+ let default_parts = shirabe_php_shim::explode(",", &default.to_string());
+
+ let resolved: Vec<String> = default_parts
+ .iter()
+ .map(|value| {
+ choices
+ .get(&shirabe_php_shim::trim(value, None))
+ .map(|v| v.to_string())
+ .unwrap()
+ })
+ .collect();
+
+ text = shirabe_php_shim::sprintf(
+ " <info>%s</info> [<comment>%s</comment>]:",
+ &[
+ PhpMixed::String(text),
+ PhpMixed::String(OutputFormatter::escape(&resolved.join(", ")).unwrap()),
+ ],
+ );
+ } else if let Some(choice_question) = question.as_any().downcast_ref::<ChoiceQuestion>() {
+ let choices = choice_question.get_choices();
+ text = shirabe_php_shim::sprintf(
+ " <info>%s</info> [<comment>%s</comment>]:",
+ &[
+ PhpMixed::String(text),
+ PhpMixed::String(
+ OutputFormatter::escape(
+ &choices
+ .get(&default.to_string())
+ .map(|v| (**v).clone())
+ .unwrap_or(default.clone())
+ .to_string(),
+ )
+ .unwrap(),
+ ),
+ ],
+ );
+ } else {
+ text = shirabe_php_shim::sprintf(
+ " <info>%s</info> [<comment>%s</comment>]:",
+ &[
+ PhpMixed::String(text),
+ PhpMixed::String(OutputFormatter::escape(&default.to_string()).unwrap()),
+ ],
+ );
+ }
+
+ output
+ .borrow()
+ .writeln(&[text], output_interface::OUTPUT_NORMAL);
+
+ let mut prompt = " > ".to_string();
+
+ if let Some(choice_question) = question.as_any().downcast_ref::<ChoiceQuestion>() {
+ output.borrow().writeln(
+ &self
+ .inner
+ .format_choice_question_choices(choice_question, "comment"),
+ output_interface::OUTPUT_NORMAL,
+ );
+
+ prompt = choice_question.get_prompt().to_string();
+ }
+
+ output
+ .borrow()
+ .write(&[prompt], false, output_interface::OUTPUT_NORMAL);
+ }
+
+ pub(crate) fn write_error(
+ &self,
+ output: Rc<RefCell<dyn OutputInterface>>,
+ error: &shirabe_php_shim::Exception,
+ ) {
+ let is_symfony_style = {
+ let borrowed = output.borrow();
+ (*borrowed)
+ .as_any()
+ .downcast_ref::<SymfonyStyle>()
+ .is_some()
+ };
+ if is_symfony_style {
+ // $output->newLine(); $output->error($error->getMessage());
+ // SymfonyStyle's newLine()/error() require mutable access to the
+ // concrete type; mutable downcasting through the trait object is
+ // resolved in a later phase.
+ todo!("SymfonyStyle newLine()/error() require &mut SymfonyStyle");
+ }
+
+ self.inner.write_error(output, error);
+ }
+
+ fn get_eof_shortcut(&self) -> String {
+ if shirabe_php_shim::php_os_family() == "Windows" {
+ return "<comment>Ctrl+Z</comment> then <comment>Enter</comment>".to_string();
+ }
+
+ "<comment>Ctrl+D</comment>".to_string()
+ }
+}
+
+impl Deref for SymfonyQuestionHelper {
+ type Target = QuestionHelper;
+
+ fn deref(&self) -> &Self::Target {
+ &self.inner
+ }
+}
+
+impl DerefMut for SymfonyQuestionHelper {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.inner
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
index 4ff5a5e..6f52458 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table.rs
@@ -1,47 +1,1356 @@
-use crate::symfony::console::output::OutputInterface;
+use crate::composer::pcre::preg::Preg;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::runtime_exception::RuntimeException;
+use crate::symfony::console::formatter::output_formatter::OutputFormatter;
+use crate::symfony::console::formatter::wrappable_output_formatter_interface::WrappableOutputFormatterInterface;
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::helper::table_cell::{TableCell, TableCellOption};
+use crate::symfony::console::helper::table_cell_style::TableCellStyle;
+use crate::symfony::console::helper::table_rows::TableRows;
+use crate::symfony::console::helper::table_separator::TableSeparator;
+use crate::symfony::console::helper::table_style::TableStyle;
+use crate::symfony::console::output::console_section_output::ConsoleSectionOutput;
+use crate::symfony::console::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
use shirabe_php_shim::PhpMixed;
+use std::cell::RefCell;
+use std::rc::Rc;
+/// Provides helpers to display a table.
#[derive(Debug)]
-pub struct Table;
+pub struct Table {
+ header_title: Option<String>,
+ footer_title: Option<String>,
+
+ /// Table headers.
+ headers: Vec<PhpMixed>,
+
+ /// Table rows.
+ rows: Vec<PhpMixed>,
+ horizontal: bool,
+
+ /// Column widths cache.
+ effective_column_widths: IndexMap<i64, i64>,
+
+ /// Number of columns cache.
+ number_of_columns: Option<i64>,
+
+ output: Rc<RefCell<dyn OutputInterface>>,
+
+ style: TableStyle,
+
+ column_styles: IndexMap<i64, TableStyle>,
+
+ /// User set column widths.
+ column_widths: IndexMap<i64, i64>,
+ column_max_widths: IndexMap<i64, i64>,
+
+ rendered: bool,
+}
+
+const SEPARATOR_TOP: i64 = 0;
+const SEPARATOR_TOP_BOTTOM: i64 = 1;
+const SEPARATOR_MID: i64 = 2;
+const SEPARATOR_BOTTOM: i64 = 3;
+const BORDER_OUTSIDE: i64 = 0;
+const BORDER_INSIDE: i64 = 1;
+
+/// Global style definitions, lazily initialized.
+///
+/// In PHP this is `private static $styles`. Here it is a process-global cache.
+fn styles() -> &'static std::sync::Mutex<Option<IndexMap<String, TableStyle>>> {
+ static STYLES: std::sync::Mutex<Option<IndexMap<String, TableStyle>>> =
+ std::sync::Mutex::new(None);
+ &STYLES
+}
impl Table {
- pub fn new(_output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) -> Self {
+ pub fn new(output: Rc<RefCell<dyn OutputInterface>>) -> Self {
+ let mut styles_guard = styles().lock().unwrap();
+ if styles_guard.is_none() {
+ *styles_guard = Some(Self::init_styles());
+ }
+ drop(styles_guard);
+
+ let mut this = Self {
+ header_title: None,
+ footer_title: None,
+ headers: Vec::new(),
+ rows: Vec::new(),
+ horizontal: false,
+ effective_column_widths: IndexMap::new(),
+ number_of_columns: None,
+ output,
+ style: TableStyle::default(),
+ column_styles: IndexMap::new(),
+ column_widths: IndexMap::new(),
+ column_max_widths: IndexMap::new(),
+ rendered: false,
+ };
+
+ this.set_style(PhpMixed::from("default"));
+
+ this
+ }
+
+ /// Sets a style definition.
+ pub fn set_style_definition(name: String, style: TableStyle) {
+ let mut styles_guard = styles().lock().unwrap();
+ if styles_guard.is_none() {
+ *styles_guard = Some(Self::init_styles());
+ }
+
+ styles_guard.as_mut().unwrap().insert(name, style);
+ }
+
+ /// Gets a style definition by name.
+ pub fn get_style_definition(
+ name: String,
+ ) -> anyhow::Result<Result<TableStyle, InvalidArgumentException>> {
+ let mut styles_guard = styles().lock().unwrap();
+ if styles_guard.is_none() {
+ *styles_guard = Some(Self::init_styles());
+ }
+
+ if let Some(_style) = styles_guard.as_ref().unwrap().get(&name) {
+ // TODO(phase-b): TableStyle is not Clone; sharing semantics need resolving.
+ todo!()
+ }
+
+ Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Style \"%s\" is not defined.",
+ &[PhpMixed::from(name)],
+ ),
+ code: 0,
+ },
+ )))
+ }
+
+ /// Sets table style.
+ ///
+ /// `$name` is the style name or a TableStyle instance.
+ pub fn set_style(
+ &mut self,
+ name: PhpMixed,
+ ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> {
+ match self.resolve_style(name)? {
+ Ok(style) => {
+ self.style = style;
+ Ok(Ok(self))
+ }
+ Err(e) => Ok(Err(e)),
+ }
+ }
+
+ /// Gets the current table style.
+ pub fn get_style(&self) -> &TableStyle {
+ &self.style
+ }
+
+ /// Sets table column style.
+ ///
+ /// `$name` is the style name or a TableStyle instance.
+ pub fn set_column_style(
+ &mut self,
+ column_index: i64,
+ name: PhpMixed,
+ ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> {
+ match self.resolve_style(name)? {
+ Ok(style) => {
+ self.column_styles.insert(column_index, style);
+ Ok(Ok(self))
+ }
+ Err(e) => Ok(Err(e)),
+ }
+ }
+
+ /// Gets the current style for a column.
+ ///
+ /// If style was not set, it returns the global table style.
+ pub fn get_column_style(&self, column_index: i64) -> &TableStyle {
+ self.column_styles
+ .get(&column_index)
+ .unwrap_or_else(|| self.get_style())
+ }
+
+ /// Sets the minimum width of a column.
+ pub fn set_column_width(&mut self, column_index: i64, width: i64) -> &mut Self {
+ self.column_widths.insert(column_index, width);
+
+ self
+ }
+
+ /// Sets the minimum width of all columns.
+ pub fn set_column_widths(&mut self, widths: Vec<i64>) -> &mut Self {
+ self.column_widths = IndexMap::new();
+ for (index, width) in widths.into_iter().enumerate() {
+ self.set_column_width(index as i64, width);
+ }
+
+ self
+ }
+
+ /// Sets the maximum width of a column.
+ ///
+ /// Any cell within this column which contents exceeds the specified width will be wrapped into
+ /// multiple lines, while formatted strings are preserved.
+ pub fn set_column_max_width(&mut self, column_index: i64, width: i64) -> &mut Self {
+ if !Self::formatter_is_wrappable(&self.output) {
+ // PHP throws \LogicException here. This represents a programming error: the caller must
+ // supply a WrappableOutputFormatterInterface before setting a maximum column width.
+ panic!(
+ "Setting a maximum column width is only supported when using a \"{}\" formatter, got \"{}\".",
+ "Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface",
+ shirabe_php_shim::get_debug_type(&PhpMixed::from(()))
+ );
+ }
+
+ self.column_max_widths.insert(column_index, width);
+
+ self
+ }
+
+ pub fn set_headers(&mut self, headers: Vec<PhpMixed>) -> &mut Self {
+ // PHP: $headers = array_values($headers). A row is already modeled as a positional Vec,
+ // so reindexing is the identity here.
+ let mut headers = headers;
+ if !headers.is_empty() && !shirabe_php_shim::is_array(&headers[0]) {
+ headers = vec![Self::from_row_vec(headers)];
+ }
+
+ self.headers = headers;
+
+ self
+ }
+
+ pub fn set_rows(&mut self, rows: Vec<PhpMixed>) -> &mut Self {
+ self.rows = Vec::new();
+
+ self.add_rows(rows)
+ }
+
+ pub fn add_rows(&mut self, rows: Vec<PhpMixed>) -> &mut Self {
+ for row in rows {
+ self.add_row(row);
+ }
+
+ self
+ }
+
+ pub fn add_row(
+ &mut self,
+ row: PhpMixed,
+ ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> {
+ if shirabe_php_shim::instance_of::<TableSeparator>(&row) {
+ self.rows.push(row);
+
+ return Ok(Ok(self));
+ }
+
+ if !shirabe_php_shim::is_array(&row) {
+ return Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: "A row must be an array or a TableSeparator instance.".to_string(),
+ code: 0,
+ },
+ )));
+ }
+
+ // PHP: $this->rows[] = array_values($row). The row is modeled as a positional Vec.
+ self.rows.push(Self::from_row_vec(Self::to_row_vec(row)));
+
+ Ok(Ok(self))
+ }
+
+ /// Adds a row to the table, and re-renders the table.
+ pub fn append_row(
+ &mut self,
+ row: PhpMixed,
+ ) -> anyhow::Result<Result<&mut Self, RuntimeException>> {
+ if !Self::output_is_console_section(&self.output) {
+ return Ok(Err(RuntimeException(shirabe_php_shim::RuntimeException {
+ message: shirabe_php_shim::sprintf(
+ "Output should be an instance of \"%s\" when calling \"%s\".",
+ &[
+ PhpMixed::from("Symfony\\Component\\Console\\Output\\ConsoleSectionOutput"),
+ PhpMixed::from("Symfony\\Component\\Console\\Helper\\Table::appendRow"),
+ ],
+ ),
+ code: 0,
+ })));
+ }
+
+ if self.rendered {
+ // TODO(phase-b): downcast output to ConsoleSectionOutput to call clear().
+ let _ = ConsoleSectionOutput::clear;
+ let row_count = self.calculate_row_count();
+ let _ = row_count;
+ todo!()
+ }
+
+ self.add_row(row)?.ok();
+ self.render();
+
+ Ok(Ok(self))
+ }
+
+ pub fn set_row(&mut self, column: i64, row: Vec<PhpMixed>) -> &mut Self {
+ // PHP indexes $this->rows by arbitrary key; here we follow the integer-keyed case.
+ let _ = (column, row);
todo!()
}
- pub fn set_headers(&mut self, _headers: Vec<PhpMixed>) -> &mut Self {
+ pub fn set_header_title(&mut self, title: Option<String>) -> &mut Self {
+ self.header_title = title;
+
+ self
+ }
+
+ pub fn set_footer_title(&mut self, title: Option<String>) -> &mut Self {
+ self.footer_title = title;
+
+ self
+ }
+
+ pub fn set_horizontal(&mut self, horizontal: bool) -> &mut Self {
+ self.horizontal = horizontal;
+
+ self
+ }
+
+ /// Renders table to output.
+ pub fn render(&mut self) {
+ let divider = TableSeparator::new();
+ let rows: Vec<PhpMixed>;
+ if self.horizontal {
+ let mut horizontal_rows: IndexMap<i64, Vec<PhpMixed>> = IndexMap::new();
+ let header0 = self
+ .headers
+ .first()
+ .map(|h| Self::to_row_vec(h.clone()))
+ .unwrap_or_default();
+ for (i, header) in header0.into_iter().enumerate() {
+ let i = i as i64;
+ horizontal_rows.insert(i, vec![header]);
+ for row in &self.rows {
+ if shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ continue;
+ }
+ if let Some(cell) = Self::row_get(row, i) {
+ let entry = horizontal_rows.get_mut(&i).unwrap();
+ entry.push(cell);
+ } else {
+ let first = horizontal_rows.get(&i).unwrap().first().cloned();
+ let is_title_noop = match first {
+ Some(ref c) if shirabe_php_shim::instance_of::<TableCell>(c) => {
+ Self::cell_colspan(c) >= 2
+ }
+ _ => false,
+ };
+ if is_title_noop {
+ // Noop, there is a "title"
+ } else {
+ let entry = horizontal_rows.get_mut(&i).unwrap();
+ entry.push(PhpMixed::from(()));
+ }
+ }
+ }
+ }
+ rows = horizontal_rows
+ .into_values()
+ .map(Self::from_row_vec)
+ .collect();
+ } else {
+ let mut merged = self.headers.clone();
+ merged.push(Self::table_separator_to_mixed(divider.clone()));
+ merged.extend(self.rows.clone());
+ rows = merged;
+ }
+
+ self.calculate_number_of_columns(&rows);
+
+ let row_groups = self.build_table_rows(rows);
+ self.calculate_columns_width(&row_groups);
+
+ let mut is_header = !self.horizontal;
+ let mut is_first_row = self.horizontal;
+ let mut has_title =
+ self.header_title.is_some() && !self.header_title.as_deref().unwrap_or("").is_empty();
+
+ for row_group in &row_groups {
+ let mut is_header_separator_rendered = false;
+
+ for row in row_group {
+ if Self::is_divider(row, &divider) {
+ is_header = false;
+ is_first_row = true;
+
+ continue;
+ }
+
+ if shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ self.render_row_separator(SEPARATOR_MID, None, None);
+
+ continue;
+ }
+
+ if !shirabe_php_shim::to_bool(row) {
+ continue;
+ }
+
+ if is_header && !is_header_separator_rendered {
+ self.render_row_separator(
+ if is_header {
+ SEPARATOR_TOP
+ } else {
+ SEPARATOR_TOP_BOTTOM
+ },
+ if has_title {
+ self.header_title.clone()
+ } else {
+ None
+ },
+ if has_title {
+ Some(self.style.get_header_title_format())
+ } else {
+ None
+ },
+ );
+ has_title = false;
+ is_header_separator_rendered = true;
+ }
+
+ if is_first_row {
+ self.render_row_separator(
+ if is_header {
+ SEPARATOR_TOP
+ } else {
+ SEPARATOR_TOP_BOTTOM
+ },
+ if has_title {
+ self.header_title.clone()
+ } else {
+ None
+ },
+ if has_title {
+ Some(self.style.get_header_title_format())
+ } else {
+ None
+ },
+ );
+ is_first_row = false;
+ has_title = false;
+ }
+
+ if self.horizontal {
+ self.render_row(
+ Self::to_row_vec(row.clone()),
+ self.style.get_cell_row_format(),
+ Some(self.style.get_cell_header_format()),
+ );
+ } else {
+ self.render_row(
+ Self::to_row_vec(row.clone()),
+ if is_header {
+ self.style.get_cell_header_format()
+ } else {
+ self.style.get_cell_row_format()
+ },
+ None,
+ );
+ }
+ }
+ }
+ self.render_row_separator(
+ SEPARATOR_BOTTOM,
+ self.footer_title.clone(),
+ Some(self.style.get_footer_title_format()),
+ );
+
+ self.cleanup();
+ self.rendered = true;
+ }
+
+ /// Renders horizontal header separator.
+ fn render_row_separator(
+ &self,
+ r#type: i64,
+ title: Option<String>,
+ title_format: Option<String>,
+ ) {
+ let count = match self.number_of_columns {
+ Some(0) | None => return,
+ Some(c) => c,
+ };
+
+ let borders = self.style.get_border_chars();
+ if borders[0].is_empty()
+ && borders[2].is_empty()
+ && self.style.get_crossing_char().is_empty()
+ {
+ return;
+ }
+
+ let crossings = self.style.get_crossing_chars();
+ let (horizontal, left_char, mid_char, right_char) = if SEPARATOR_MID == r#type {
+ (
+ borders[2].clone(),
+ crossings[8].clone(),
+ crossings[0].clone(),
+ crossings[4].clone(),
+ )
+ } else if SEPARATOR_TOP == r#type {
+ (
+ borders[0].clone(),
+ crossings[1].clone(),
+ crossings[2].clone(),
+ crossings[3].clone(),
+ )
+ } else if SEPARATOR_TOP_BOTTOM == r#type {
+ (
+ borders[0].clone(),
+ crossings[9].clone(),
+ crossings[10].clone(),
+ crossings[11].clone(),
+ )
+ } else {
+ (
+ borders[0].clone(),
+ crossings[7].clone(),
+ crossings[6].clone(),
+ crossings[5].clone(),
+ )
+ };
+
+ let mut markup = left_char;
+ let mut column = 0;
+ while column < count {
+ markup.push_str(&shirabe_php_shim::str_repeat(
+ &horizontal,
+ self.effective_column_widths[&column] as usize,
+ ));
+ markup.push_str(if column == count - 1 {
+ &right_char
+ } else {
+ &mid_char
+ });
+ column += 1;
+ }
+
+ if let Some(title) = title {
+ let title_format = title_format.unwrap();
+ let formatted_title =
+ shirabe_php_shim::sprintf(&title_format, &[PhpMixed::from(title.clone())]);
+ let mut formatted_title = formatted_title;
+ let mut title_length = Helper::width(&self.remove_decoration(&formatted_title));
+ let markup_length = Helper::width(&markup);
+ let limit = markup_length - 4;
+ if title_length > limit {
+ title_length = limit;
+ let format_length = Helper::width(&self.remove_decoration(
+ &shirabe_php_shim::sprintf(&title_format, &[PhpMixed::from("")]),
+ ));
+ formatted_title = shirabe_php_shim::sprintf(
+ &title_format,
+ &[PhpMixed::from(format!(
+ "{}...",
+ Helper::substr(&title, 0, Some(limit - format_length - 3))
+ ))],
+ );
+ }
+
+ let title_start = shirabe_php_shim::intdiv(markup_length - title_length, 2);
+ if shirabe_php_shim::mb_detect_encoding(&markup, None, true).is_none() {
+ markup = shirabe_php_shim::substr_replace(
+ &markup,
+ &formatted_title,
+ title_start as usize,
+ title_length as usize,
+ );
+ } else {
+ markup = format!(
+ "{}{}{}",
+ shirabe_php_shim::mb_substr(&markup, 0, Some(title_start), None),
+ formatted_title,
+ shirabe_php_shim::mb_substr(&markup, title_start + title_length, None, None),
+ );
+ }
+ }
+
+ self.output.borrow().writeln(
+ &[shirabe_php_shim::sprintf(
+ &self.style.get_border_format(),
+ &[PhpMixed::from(markup)],
+ )],
+ crate::symfony::console::output::output_interface::OUTPUT_NORMAL,
+ );
+ }
+
+ /// Renders vertical column separator.
+ fn render_column_separator(&self, r#type: i64) -> String {
+ let borders = self.style.get_border_chars();
+
+ shirabe_php_shim::sprintf(
+ &self.style.get_border_format(),
+ &[PhpMixed::from(if BORDER_OUTSIDE == r#type {
+ borders[1].clone()
+ } else {
+ borders[3].clone()
+ })],
+ )
+ }
+
+ /// Renders table row.
+ fn render_row(
+ &self,
+ row: Vec<PhpMixed>,
+ cell_format: String,
+ first_cell_format: Option<String>,
+ ) {
+ let mut row_content = self.render_column_separator(BORDER_OUTSIDE);
+ let columns = self.get_row_columns(&row);
+ let last = columns.len() as i64 - 1;
+ for (i, column) in columns.into_iter().enumerate() {
+ let i = i as i64;
+ if first_cell_format.is_some() && 0 == i {
+ row_content.push_str(&self.render_cell(
+ &row,
+ column,
+ first_cell_format.clone().unwrap(),
+ ));
+ } else {
+ row_content.push_str(&self.render_cell(&row, column, cell_format.clone()));
+ }
+ row_content.push_str(&self.render_column_separator(if last == i {
+ BORDER_OUTSIDE
+ } else {
+ BORDER_INSIDE
+ }));
+ }
+ self.output.borrow().writeln(
+ &[row_content],
+ crate::symfony::console::output::output_interface::OUTPUT_NORMAL,
+ );
+ }
+
+ /// Renders table cell with padding.
+ fn render_cell(&self, row: &[PhpMixed], column: i64, cell_format: String) -> String {
+ let cell = Self::row_get_index(row, column).unwrap_or_else(|| PhpMixed::from(""));
+ let mut width = self.effective_column_widths[&column];
+ if shirabe_php_shim::instance_of::<TableCell>(&cell) && Self::cell_colspan(&cell) > 1 {
+ // add the width of the following columns(numbers of colspan).
+ for next_column in (column + 1)..=(column + Self::cell_colspan(&cell) - 1) {
+ width +=
+ self.get_column_separator_width() + self.effective_column_widths[&next_column];
+ }
+ }
+
+ // str_pad won't work properly with multi-byte strings, we need to fix the padding
+ let cell_str = shirabe_php_shim::to_string(&cell);
+ if let Some(encoding) = shirabe_php_shim::mb_detect_encoding(&cell_str, None, true) {
+ width += shirabe_php_shim::strlen(&cell_str)
+ - shirabe_php_shim::mb_strwidth(&cell_str, Some(&encoding));
+ }
+
+ let style = self.get_column_style(column).clone();
+
+ if shirabe_php_shim::instance_of::<TableSeparator>(&cell) {
+ return shirabe_php_shim::sprintf(
+ &style.get_border_format(),
+ &[PhpMixed::from(shirabe_php_shim::str_repeat(
+ &style.get_border_chars()[2],
+ width as usize,
+ ))],
+ );
+ }
+
+ width += Helper::length(&cell_str) - Helper::length(&self.remove_decoration(&cell_str));
+ let mut content = shirabe_php_shim::sprintf(
+ &style.get_cell_row_content_format(),
+ &[PhpMixed::from(cell_str.clone())],
+ );
+
+ let mut cell_format = cell_format;
+ let mut pad_type = style.get_pad_type();
+ if shirabe_php_shim::instance_of::<TableCell>(&cell)
+ && Self::cell_style_is_table_cell_style(&cell)
+ {
+ let is_not_styled_by_tag = !Preg::is_match(
+ "/^<(\\w+|(\\w+=[\\w,]+;?)*)>.+<\\/(\\w+|(\\w+=\\w+;?)*)?>$/",
+ &cell_str,
+ )
+ .unwrap();
+ if is_not_styled_by_tag {
+ let cell_style = Self::cell_get_style(&cell).unwrap();
+ match cell_style.get_cell_format() {
+ Some(fmt) => cell_format = fmt,
+ None => {
+ let tag = shirabe_php_shim::http_build_query_mixed(
+ &cell_style.get_tag_options(),
+ "",
+ ";",
+ );
+ cell_format = format!("<{}>%s</>", tag);
+ }
+ }
+
+ if shirabe_php_shim::strstr(&content, "</>").is_some() {
+ content = shirabe_php_shim::str_replace("</>", "", &content);
+ width -= 3;
+ }
+ if shirabe_php_shim::strstr(&content, "<fg=default;bg=default>").is_some() {
+ content =
+ shirabe_php_shim::str_replace("<fg=default;bg=default>", "", &content);
+ width -= shirabe_php_shim::strlen("<fg=default;bg=default>");
+ }
+ }
+
+ pad_type = Self::cell_get_style(&cell).unwrap().get_pad_by_align();
+ }
+
+ shirabe_php_shim::sprintf(
+ &cell_format,
+ &[PhpMixed::from(shirabe_php_shim::str_pad(
+ &content,
+ width as usize,
+ &style.get_padding_char(),
+ pad_type,
+ ))],
+ )
+ }
+
+ /// Calculate number of columns for this table.
+ fn calculate_number_of_columns(&mut self, rows: &[PhpMixed]) {
+ let mut columns = vec![0i64];
+ for row in rows {
+ if shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ continue;
+ }
+
+ columns.push(self.get_number_of_columns(&Self::to_row_vec(row.clone())));
+ }
+
+ self.number_of_columns = Some(*columns.iter().max().unwrap());
+ }
+
+ fn build_table_rows(&mut self, rows: Vec<PhpMixed>) -> TableRows {
+ let mut rows = rows;
+ let mut unmerged_rows: IndexMap<i64, IndexMap<i64, Vec<PhpMixed>>> = IndexMap::new();
+ let mut row_key = 0i64;
+ while row_key < rows.len() as i64 {
+ rows = self.fill_next_rows(rows, row_key);
+
+ // Remove any new line breaks and replace it with a new line
+ let current = Self::to_row_vec(rows[row_key as usize].clone());
+ for (column, cell) in current.iter().enumerate() {
+ let column = column as i64;
+ let mut cell = cell.clone();
+ let colspan = if shirabe_php_shim::instance_of::<TableCell>(&cell) {
+ Self::cell_colspan(&cell)
+ } else {
+ 1
+ };
+
+ if self.column_max_widths.contains_key(&column)
+ && Helper::width(&self.remove_decoration(&shirabe_php_shim::to_string(&cell)))
+ > self.column_max_widths[&column]
+ {
+ // TODO(phase-b): formatAndWrap requires a WrappableOutputFormatterInterface;
+ // downcasting dyn OutputFormatterInterface to it needs concrete knowledge.
+ let _ = colspan;
+ let wrapped: Option<String> = todo!();
+ cell = PhpMixed::from(wrapped.unwrap_or_default());
+ }
+ let cell_str = shirabe_php_shim::to_string(&cell);
+ if shirabe_php_shim::strstr(&cell_str, "\n").is_none() {
+ continue;
+ }
+ let eol = if shirabe_php_shim::str_contains(&cell_str, "\r\n") {
+ "\r\n"
+ } else {
+ "\n"
+ };
+ let escaped = shirabe_php_shim::implode(
+ eol,
+ &shirabe_php_shim::explode(eol, &cell_str)
+ .iter()
+ .map(|line| OutputFormatter::escape_trailing_backslash(line))
+ .collect::<Vec<_>>(),
+ );
+ cell = if shirabe_php_shim::instance_of::<TableCell>(&cell) {
+ Self::table_cell_to_mixed(TableCell::new2(
+ &escaped,
+ Self::table_cell_options_colspan(Self::cell_colspan(&cell)),
+ ))
+ } else {
+ PhpMixed::from(escaped.clone())
+ };
+ let lines = shirabe_php_shim::explode(
+ eol,
+ &shirabe_php_shim::str_replace(
+ eol,
+ &format!("<fg=default;bg=default></>{}", eol),
+ &shirabe_php_shim::to_string(&cell),
+ ),
+ );
+ for (line_key, line) in lines.into_iter().enumerate() {
+ let line_key = line_key as i64;
+ let mut line = PhpMixed::from(line);
+ if colspan > 1 {
+ line = Self::table_cell_to_mixed(TableCell::new2(
+ &shirabe_php_shim::to_string(&line),
+ Self::table_cell_options_colspan(colspan),
+ ));
+ }
+ if 0 == line_key {
+ let mut r = Self::to_row_vec(rows[row_key as usize].clone());
+ Self::array_set(&mut r, column, line);
+ rows[row_key as usize] = Self::from_row_vec(r);
+ } else {
+ if !unmerged_rows.contains_key(&row_key)
+ || !unmerged_rows[&row_key].contains_key(&line_key)
+ {
+ let copied = self.copy_row(&rows, row_key);
+ unmerged_rows
+ .entry(row_key)
+ .or_default()
+ .insert(line_key, copied);
+ }
+ let target = unmerged_rows
+ .get_mut(&row_key)
+ .unwrap()
+ .get_mut(&line_key)
+ .unwrap();
+ Self::vec_set(target, column, line);
+ }
+ }
+ }
+ row_key += 1;
+ }
+
+ // PHP returns a TableRows wrapping a generator that lazily yields row groups.
+ // The generator borrows $this to call fillCells(). In Phase A we precompute the
+ // row groups eagerly to preserve behavior, then hand them to TableRows.
+ let mut row_groups: Vec<Vec<PhpMixed>> = Vec::new();
+ for (row_key, row) in rows.into_iter().enumerate() {
+ let row_key = row_key as i64;
+ let mut row_group: Vec<PhpMixed> =
+ vec![if shirabe_php_shim::instance_of::<TableSeparator>(&row) {
+ row
+ } else {
+ Self::from_row_vec(self.fill_cells(Self::to_row_vec(row)))
+ }];
+
+ if let Some(extra) = unmerged_rows.get(&row_key) {
+ for r in extra.values() {
+ let r = Self::from_row_vec(r.clone());
+ row_group.push(if shirabe_php_shim::instance_of::<TableSeparator>(&r) {
+ r
+ } else {
+ Self::from_row_vec(self.fill_cells(Self::to_row_vec(r)))
+ });
+ }
+ }
+ row_groups.push(row_group);
+ }
+
+ TableRows::from_row_groups(row_groups)
+ }
+
+ fn calculate_row_count(&mut self) -> i64 {
+ let mut merged = self.headers.clone();
+ merged.push(Self::table_separator_to_mixed(TableSeparator::new()));
+ merged.extend(self.rows.clone());
+ let mut number_of_rows =
+ shirabe_php_shim::iterator_to_array(self.build_table_rows(merged)).len() as i64;
+
+ if !self.headers.is_empty() {
+ number_of_rows += 1; // Add row for header separator
+ }
+
+ if !self.rows.is_empty() {
+ number_of_rows += 1; // Add row for footer separator
+ }
+
+ number_of_rows
+ }
+
+ /// fill rows that contains rowspan > 1.
+ fn fill_next_rows(&self, rows: Vec<PhpMixed>, line: i64) -> Vec<PhpMixed> {
+ let mut rows = rows;
+ let mut unmerged_rows: IndexMap<i64, IndexMap<i64, PhpMixed>> = IndexMap::new();
+ let current = Self::to_row_vec(rows[line as usize].clone());
+ for (column, cell) in current.iter().enumerate() {
+ let column = column as i64;
+ let cell = cell.clone();
+ if !shirabe_php_shim::is_null(&cell)
+ && !shirabe_php_shim::instance_of::<TableCell>(&cell)
+ && !shirabe_php_shim::is_scalar(&cell)
+ && !(shirabe_php_shim::is_object(&cell)
+ && shirabe_php_shim::method_exists(&cell, "__toString"))
+ {
+ // PHP throws InvalidArgumentException; the @throws contract makes this a
+ // recoverable error. In Phase A we keep the panic placeholder as fill_next_rows
+ // does not yet return a Result in the call chain.
+ // TODO(phase-b): thread InvalidArgumentException through fill_next_rows.
+ panic!(
+ "A cell must be a TableCell, a scalar or an object implementing \"__toString()\", \"{}\" given.",
+ shirabe_php_shim::get_debug_type(&cell)
+ );
+ }
+ if shirabe_php_shim::instance_of::<TableCell>(&cell) && Self::cell_rowspan(&cell) > 1 {
+ let mut nb_lines = Self::cell_rowspan(&cell) - 1;
+ let cell_str = shirabe_php_shim::to_string(&cell);
+ let mut lines = vec![cell.clone()];
+ if shirabe_php_shim::strstr(&cell_str, "\n").is_some() {
+ let eol = if shirabe_php_shim::str_contains(&cell_str, "\r\n") {
+ "\r\n"
+ } else {
+ "\n"
+ };
+ let exploded = shirabe_php_shim::explode(
+ eol,
+ &shirabe_php_shim::str_replace(
+ eol,
+ &format!("<fg=default;bg=default>{}</>", eol),
+ &cell_str,
+ ),
+ );
+ lines = exploded.into_iter().map(PhpMixed::from).collect();
+ nb_lines = if (lines.len() as i64) > nb_lines {
+ shirabe_php_shim::substr_count(&cell_str, eol)
+ } else {
+ nb_lines
+ };
+
+ let mut r = Self::to_row_vec(rows[line as usize].clone());
+ Self::array_set(
+ &mut r,
+ column,
+ Self::table_cell_to_mixed(TableCell::new2(
+ &shirabe_php_shim::to_string(&lines[0]),
+ Self::table_cell_options_colspan_style(
+ Self::cell_colspan(&cell),
+ Self::cell_get_style(&cell),
+ ),
+ )),
+ );
+ rows[line as usize] = Self::from_row_vec(r);
+ lines.remove(0);
+ }
+
+ // create a two dimensional array (rowspan x colspan)
+ for k in (line + 1)..=(line + nb_lines) {
+ unmerged_rows.entry(k).or_default();
+ }
+ for unmerged_row_key in unmerged_rows.keys().cloned().collect::<Vec<_>>() {
+ let idx = unmerged_row_key - line;
+ let value = lines
+ .get(idx as usize)
+ .cloned()
+ .unwrap_or_else(|| PhpMixed::from(""));
+ unmerged_rows.get_mut(&unmerged_row_key).unwrap().insert(
+ column,
+ Self::table_cell_to_mixed(TableCell::new2(
+ &shirabe_php_shim::to_string(&value),
+ Self::table_cell_options_colspan_style(
+ Self::cell_colspan(&cell),
+ Self::cell_get_style(&cell),
+ ),
+ )),
+ );
+ if nb_lines == unmerged_row_key - line {
+ break;
+ }
+ }
+ }
+ }
+
+ for (unmerged_row_key, unmerged_row) in unmerged_rows.clone() {
+ // we need to know if $unmergedRow will be merged or inserted into $rows
+ let fits = (unmerged_row_key as usize) < rows.len()
+ && shirabe_php_shim::is_array(&rows[unmerged_row_key as usize])
+ && (self.get_number_of_columns(&Self::to_row_vec(
+ rows[unmerged_row_key as usize].clone(),
+ )) + self.get_number_of_columns(
+ &unmerged_rows[&unmerged_row_key]
+ .values()
+ .cloned()
+ .collect::<Vec<_>>(),
+ ) <= self.number_of_columns.unwrap());
+ if fits {
+ let mut target = Self::to_row_vec(rows[unmerged_row_key as usize].clone());
+ for (cell_key, cell) in unmerged_row {
+ // insert cell into row at cellKey position
+ shirabe_php_shim::array_splice(&mut target, cell_key, Some(0), vec![cell]);
+ }
+ rows[unmerged_row_key as usize] = Self::from_row_vec(target);
+ } else {
+ let mut row = self.copy_row(&rows, unmerged_row_key - 1);
+ for (column, cell) in &unmerged_row {
+ if shirabe_php_shim::to_bool(cell) {
+ Self::vec_set(&mut row, *column, unmerged_row[column].clone());
+ }
+ }
+ shirabe_php_shim::array_splice_mixed(
+ &mut rows,
+ unmerged_row_key,
+ 0,
+ vec![Self::from_row_vec(row)],
+ );
+ }
+ }
+
+ rows
+ }
+
+ /// fill cells for a row that contains colspan > 1.
+ fn fill_cells(&self, row: Vec<PhpMixed>) -> Vec<PhpMixed> {
+ let mut new_row: Vec<PhpMixed> = Vec::new();
+
+ for (column, cell) in row.iter().enumerate() {
+ let column = column as i64;
+ new_row.push(cell.clone());
+ if shirabe_php_shim::instance_of::<TableCell>(cell) && Self::cell_colspan(cell) > 1 {
+ for _position in (column + 1)..=(column + Self::cell_colspan(cell) - 1) {
+ // insert empty value at column position
+ new_row.push(PhpMixed::from(""));
+ }
+ }
+ }
+
+ if new_row.is_empty() { row } else { new_row }
+ }
+
+ fn copy_row(&self, rows: &[PhpMixed], line: i64) -> Vec<PhpMixed> {
+ let mut row = Self::to_row_vec(rows[line as usize].clone());
+ for cell_key in 0..row.len() {
+ let cell_value = row[cell_key].clone();
+ row[cell_key] = PhpMixed::from("");
+ if shirabe_php_shim::instance_of::<TableCell>(&cell_value) {
+ row[cell_key] = Self::table_cell_to_mixed(TableCell::new2(
+ "",
+ Self::table_cell_options_colspan(Self::cell_colspan(&cell_value)),
+ ));
+ }
+ }
+
+ row
+ }
+
+ /// Gets number of columns by row.
+ fn get_number_of_columns(&self, row: &[PhpMixed]) -> i64 {
+ let mut columns = row.len() as i64;
+ for column in row {
+ columns += if shirabe_php_shim::instance_of::<TableCell>(column) {
+ Self::cell_colspan(column) - 1
+ } else {
+ 0
+ };
+ }
+
+ columns
+ }
+
+ /// Gets list of columns for the given row.
+ fn get_row_columns(&self, row: &[PhpMixed]) -> Vec<i64> {
+ let mut columns: Vec<i64> = (0..self.number_of_columns.unwrap()).collect();
+ for (cell_key, cell) in row.iter().enumerate() {
+ let cell_key = cell_key as i64;
+ if shirabe_php_shim::instance_of::<TableCell>(cell) && Self::cell_colspan(cell) > 1 {
+ // exclude grouped columns.
+ let excluded: Vec<i64> =
+ ((cell_key + 1)..=(cell_key + Self::cell_colspan(cell) - 1)).collect();
+ columns.retain(|c| !excluded.contains(c));
+ }
+ }
+
+ columns
+ }
+
+ /// Calculates columns widths.
+ fn calculate_columns_width(&mut self, groups: &TableRows) {
+ let mut column = 0;
+ while column < self.number_of_columns.unwrap() {
+ let mut lengths: Vec<i64> = Vec::new();
+ for group in groups {
+ for row in group {
+ if shirabe_php_shim::instance_of::<TableSeparator>(row) {
+ continue;
+ }
+
+ let mut row_arr = Self::to_row_vec(row.clone());
+ for i in 0..row_arr.len() {
+ let cell = row_arr[i].clone();
+ if shirabe_php_shim::instance_of::<TableCell>(&cell) {
+ let text_content =
+ self.remove_decoration(&shirabe_php_shim::to_string(&cell));
+ let text_length = Helper::width(&text_content);
+ if text_length > 0 {
+ let content_columns = shirabe_php_shim::mb_str_split(
+ &text_content,
+ shirabe_php_shim::ceil(
+ text_length as f64 / Self::cell_colspan(&cell) as f64,
+ ) as i64,
+ );
+ for (position, content) in content_columns.into_iter().enumerate() {
+ Self::vec_set(
+ &mut row_arr,
+ i as i64 + position as i64,
+ PhpMixed::from(content),
+ );
+ }
+ }
+ }
+ }
+
+ lengths.push(self.get_cell_width(&row_arr, column));
+ }
+ }
+
+ self.effective_column_widths.insert(
+ column,
+ *lengths.iter().max().unwrap()
+ + Helper::width(&self.style.get_cell_row_content_format())
+ - 2,
+ );
+ column += 1;
+ }
+ }
+
+ fn get_column_separator_width(&self) -> i64 {
+ Helper::width(&shirabe_php_shim::sprintf(
+ &self.style.get_border_format(),
+ &[PhpMixed::from(self.style.get_border_chars()[3].clone())],
+ ))
+ }
+
+ fn get_cell_width(&self, row: &[PhpMixed], column: i64) -> i64 {
+ let mut cell_width = 0;
+
+ if let Some(cell) = Self::row_get_index(row, column) {
+ cell_width =
+ Helper::width(&self.remove_decoration(&shirabe_php_shim::to_string(&cell)));
+ }
+
+ let column_width = *self.column_widths.get(&column).unwrap_or(&0);
+ cell_width = cell_width.max(column_width);
+
+ if let Some(max) = self.column_max_widths.get(&column) {
+ (*max).min(cell_width)
+ } else {
+ cell_width
+ }
+ }
+
+ /// Called after rendering to cleanup cache data.
+ fn cleanup(&mut self) {
+ self.effective_column_widths = IndexMap::new();
+ self.number_of_columns = None;
+ }
+
+ fn init_styles() -> IndexMap<String, TableStyle> {
+ let mut borderless = TableStyle::default();
+ borderless
+ .set_horizontal_border_chars("=".to_string(), None)
+ .set_vertical_border_chars(" ".to_string(), None)
+ .set_default_crossing_char(" ".to_string());
+
+ let mut compact = TableStyle::default();
+ compact
+ .set_horizontal_border_chars("".to_string(), None)
+ .set_vertical_border_chars("".to_string(), None)
+ .set_default_crossing_char("".to_string())
+ .set_cell_row_content_format("%s ".to_string());
+
+ let mut style_guide = TableStyle::default();
+ style_guide
+ .set_horizontal_border_chars("-".to_string(), None)
+ .set_vertical_border_chars(" ".to_string(), None)
+ .set_default_crossing_char(" ".to_string())
+ .set_cell_header_format("%s".to_string());
+
+ let mut r#box = TableStyle::default();
+ r#box
+ .set_horizontal_border_chars("─".to_string(), None)
+ .set_vertical_border_chars("│".to_string(), None)
+ .set_crossing_chars(
+ "┼".to_string(),
+ "┌".to_string(),
+ "┬".to_string(),
+ "┐".to_string(),
+ "┤".to_string(),
+ "┘".to_string(),
+ "┴".to_string(),
+ "└".to_string(),
+ "├".to_string(),
+ None,
+ None,
+ None,
+ );
+
+ let mut box_double = TableStyle::default();
+ box_double
+ .set_horizontal_border_chars("═".to_string(), Some("─".to_string()))
+ .set_vertical_border_chars("║".to_string(), Some("│".to_string()))
+ .set_crossing_chars(
+ "┼".to_string(),
+ "╔".to_string(),
+ "╤".to_string(),
+ "╗".to_string(),
+ "╢".to_string(),
+ "╝".to_string(),
+ "╧".to_string(),
+ "╚".to_string(),
+ "╟".to_string(),
+ Some("╠".to_string()),
+ Some("╪".to_string()),
+ Some("╣".to_string()),
+ );
+
+ let mut result: IndexMap<String, TableStyle> = IndexMap::new();
+ result.insert("default".to_string(), TableStyle::default());
+ result.insert("borderless".to_string(), borderless);
+ result.insert("compact".to_string(), compact);
+ result.insert("symfony-style-guide".to_string(), style_guide);
+ result.insert("box".to_string(), r#box);
+ result.insert("box-double".to_string(), box_double);
+
+ result
+ }
+
+ fn resolve_style(
+ &self,
+ name: PhpMixed,
+ ) -> anyhow::Result<Result<TableStyle, InvalidArgumentException>> {
+ if shirabe_php_shim::instance_of::<TableStyle>(&name) {
+ // TODO(phase-b): extract the owned TableStyle out of PhpMixed.
+ todo!()
+ }
+
+ let name_str = shirabe_php_shim::to_string(&name);
+ let styles_guard = styles().lock().unwrap();
+ if let Some(_style) = styles_guard.as_ref().and_then(|s| s.get(&name_str)) {
+ // TODO(phase-b): TableStyle is not Clone; sharing semantics need resolving.
+ todo!()
+ }
+
+ Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Style \"%s\" is not defined.",
+ &[PhpMixed::from(name_str)],
+ ),
+ code: 0,
+ },
+ )))
+ }
+
+ // --- Phase A helpers for `mixed`/array semantics over PhpMixed -------------------------------
+ //
+ // These bridge PHP's dynamic typing (a cell is a TableCell, a scalar, null, or an array;
+ // a row is an array or a TableSeparator) onto PhpMixed. Their bodies are deferred to Phase B
+ // where the concrete PhpMixed representation is settled.
+
+ fn formatter_is_wrappable(_output: &Rc<RefCell<dyn OutputInterface>>) -> bool {
+ // PHP: $this->output->getFormatter() instanceof WrappableOutputFormatterInterface
+ // TODO(phase-b): trait-to-trait instanceof check requires concrete formatter knowledge.
+ let _ = std::any::type_name::<dyn WrappableOutputFormatterInterface>();
+ todo!()
+ }
+
+ /// PHP `Helper::removeDecoration($this->output->getFormatter(), $string)`.
+ fn remove_decoration(&self, string: &str) -> String {
+ let formatter = self.output.borrow().get_formatter();
+ let mut formatter = formatter.borrow_mut();
+ Helper::remove_decoration(&mut *formatter, string)
+ }
+
+ fn output_is_console_section(_output: &Rc<RefCell<dyn OutputInterface>>) -> bool {
+ // PHP: $this->output instanceof ConsoleSectionOutput
todo!()
}
- pub fn set_rows(&mut self, _rows: Vec<PhpMixed>) -> &mut Self {
+ fn is_divider(_row: &PhpMixed, _divider: &TableSeparator) -> bool {
+ // PHP: $divider === $row (object identity)
todo!()
}
- pub fn add_row(&mut self, _row: PhpMixed) -> &mut Self {
+ fn cell_colspan(_cell: &PhpMixed) -> i64 {
+ // PHP: $cell->getColspan()
todo!()
}
- pub fn render(&mut self) {
+ fn cell_rowspan(_cell: &PhpMixed) -> i64 {
+ // PHP: $cell->getRowspan()
+ todo!()
+ }
+
+ fn cell_get_style(_cell: &PhpMixed) -> Option<TableCellStyle> {
+ // PHP: $cell->getStyle()
+ todo!()
+ }
+
+ fn cell_style_is_table_cell_style(_cell: &PhpMixed) -> bool {
+ // PHP: $cell->getStyle() instanceof TableCellStyle
+ todo!()
+ }
+
+ fn table_cell_options_colspan(_colspan: i64) -> IndexMap<String, TableCellOption> {
+ // PHP: ['colspan' => $colspan]
+ todo!()
+ }
+
+ fn table_cell_options_colspan_style(
+ _colspan: i64,
+ _style: Option<TableCellStyle>,
+ ) -> IndexMap<String, TableCellOption> {
+ // PHP: ['colspan' => $colspan, 'style' => $style]
+ todo!()
+ }
+
+ fn row_get(_row: &PhpMixed, _index: i64) -> Option<PhpMixed> {
+ // PHP: isset($row[$i]) ? $row[$i] : null, where $row is an array-typed PhpMixed
+ todo!()
+ }
+
+ fn row_get_index(_row: &[PhpMixed], _index: i64) -> Option<PhpMixed> {
+ // PHP: $row[$column] ?? null, honoring sparse integer keys
+ todo!()
+ }
+
+ fn array_set(_row: &mut [PhpMixed], _index: i64, _value: PhpMixed) {
+ // PHP: $row[$index] = $value (sparse assignment)
todo!()
}
- pub fn set_style(&mut self, _style: &str) -> &mut Self {
+ fn vec_set(_row: &mut Vec<PhpMixed>, _index: i64, _value: PhpMixed) {
+ // PHP: $row[$index] = $value (sparse assignment)
todo!()
}
- pub fn set_column_width(&mut self, _column_index: usize, _width: i64) -> &mut Self {
+ /// PHP rows/cells are integer-keyed arrays. We model a row as a positional
+ /// `Vec<PhpMixed>`. A cell that is a TableCell/TableSeparator cannot be carried by
+ /// PhpMixed (php-shim limitation), so such conversions are deferred.
+ fn to_row_vec(_row: PhpMixed) -> Vec<PhpMixed> {
+ // PHP: an array-typed value seen as a list of cells.
todo!()
}
- pub fn set_column_widths(&mut self, _widths: Vec<i64>) -> &mut Self {
+ fn from_row_vec(_row: Vec<PhpMixed>) -> PhpMixed {
+ // PHP: a list of cells seen as an array-typed value.
todo!()
}
- pub fn set_column_max_width(&mut self, _column_index: usize, _width: i64) -> &mut Self {
+ /// PhpMixed cannot hold console value objects (TableCell/TableSeparator). The cell
+ /// representation is deferred to a later phase.
+ fn table_cell_to_mixed(_cell: TableCell) -> PhpMixed {
todo!()
}
- pub fn set_horizontal(&mut self, _horizontal: bool) -> &mut Self {
+ fn table_separator_to_mixed(_separator: TableSeparator) -> PhpMixed {
todo!()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table_cell.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table_cell.rs
new file mode 100644
index 0000000..d8d8339
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table_cell.rs
@@ -0,0 +1,113 @@
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::helper::table_cell_style::TableCellStyle;
+use indexmap::IndexMap;
+use std::rc::Rc;
+
+/// A `TableCell` option value: an integer span, a `TableCellStyle`, or null.
+#[derive(Debug, Clone)]
+pub enum TableCellOption {
+ Int(i64),
+ Style(Rc<TableCellStyle>),
+ Null,
+}
+
+#[derive(Debug, Clone)]
+pub struct TableCell {
+ pub(crate) value: String,
+ options: IndexMap<String, TableCellOption>,
+}
+
+impl TableCell {
+ pub fn new(
+ value: &str,
+ options: IndexMap<String, TableCellOption>,
+ ) -> Result<Self, InvalidArgumentException> {
+ let mut this_options: IndexMap<String, TableCellOption> = IndexMap::new();
+ this_options.insert("rowspan".to_string(), TableCellOption::Int(1));
+ this_options.insert("colspan".to_string(), TableCellOption::Int(1));
+ this_options.insert("style".to_string(), TableCellOption::Null);
+
+ // check option names
+ let diff: Vec<String> = options
+ .keys()
+ .filter(|key| !this_options.contains_key(*key))
+ .cloned()
+ .collect();
+ if !diff.is_empty() {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The TableCell does not support the following options: '%s'.",
+ &[shirabe_php_shim::PhpMixed::String(diff.join("', '"))],
+ ),
+ code: 0,
+ },
+ ));
+ }
+
+ if let Some(style) = options.get("style") {
+ if !matches!(style, TableCellOption::Style(_))
+ && !matches!(style, TableCellOption::Null)
+ {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: "The style option must be an instance of \"TableCellStyle\"."
+ .to_string(),
+ code: 0,
+ },
+ ));
+ }
+ }
+
+ for (key, option) in options {
+ this_options.insert(key, option);
+ }
+
+ Ok(Self {
+ value: value.to_string(),
+ options: this_options,
+ })
+ }
+
+ /// Two-argument constructor (`__construct(string $value, array $options)`).
+ ///
+ /// The options used by the Table helper are internally controlled, so a malformed-option
+ /// error here would be a programming bug rather than a recoverable condition.
+ pub fn new2(value: &str, options: IndexMap<String, TableCellOption>) -> Self {
+ Self::new(value, options).expect("TableCell options built internally are always valid")
+ }
+
+ /// Returns the cell value.
+ pub fn to_string(&self) -> String {
+ self.value.clone()
+ }
+
+ /// Gets number of colspan.
+ pub fn get_colspan(&self) -> i64 {
+ match self.options["colspan"] {
+ TableCellOption::Int(colspan) => colspan,
+ _ => 0,
+ }
+ }
+
+ /// Gets number of rowspan.
+ pub fn get_rowspan(&self) -> i64 {
+ match self.options["rowspan"] {
+ TableCellOption::Int(rowspan) => rowspan,
+ _ => 0,
+ }
+ }
+
+ pub fn get_style(&self) -> Option<Rc<TableCellStyle>> {
+ match &self.options["style"] {
+ TableCellOption::Style(style) => Some(style.clone()),
+ _ => None,
+ }
+ }
+}
+
+impl std::fmt::Display for TableCell {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.value)
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table_cell_style.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table_cell_style.rs
new file mode 100644
index 0000000..9d41c45
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table_cell_style.rs
@@ -0,0 +1,126 @@
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use indexmap::IndexMap;
+
+pub const DEFAULT_ALIGN: &str = "left";
+
+const TAG_OPTIONS: [&str; 3] = ["fg", "bg", "options"];
+
+/// Maps an alignment name to the corresponding `STR_PAD_*` value.
+fn align_map(align: &str) -> Option<i64> {
+ match align {
+ "left" => Some(shirabe_php_shim::STR_PAD_RIGHT),
+ "center" => Some(shirabe_php_shim::STR_PAD_BOTH),
+ "right" => Some(shirabe_php_shim::STR_PAD_LEFT),
+ _ => None,
+ }
+}
+
+fn align_map_keys() -> Vec<&'static str> {
+ vec!["left", "center", "right"]
+}
+
+#[derive(Debug)]
+pub struct TableCellStyle {
+ options: IndexMap<String, shirabe_php_shim::PhpMixed>,
+}
+
+impl TableCellStyle {
+ pub fn new(
+ options: IndexMap<String, shirabe_php_shim::PhpMixed>,
+ ) -> Result<Self, InvalidArgumentException> {
+ let mut this_options: IndexMap<String, shirabe_php_shim::PhpMixed> = IndexMap::new();
+ this_options.insert(
+ "fg".to_string(),
+ shirabe_php_shim::PhpMixed::String("default".to_string()),
+ );
+ this_options.insert(
+ "bg".to_string(),
+ shirabe_php_shim::PhpMixed::String("default".to_string()),
+ );
+ this_options.insert("options".to_string(), shirabe_php_shim::PhpMixed::Null);
+ this_options.insert(
+ "align".to_string(),
+ shirabe_php_shim::PhpMixed::String(DEFAULT_ALIGN.to_string()),
+ );
+ this_options.insert("cellFormat".to_string(), shirabe_php_shim::PhpMixed::Null);
+
+ let diff: Vec<String> = options
+ .keys()
+ .filter(|key| !this_options.contains_key(*key))
+ .cloned()
+ .collect();
+ if !diff.is_empty() {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "The TableCellStyle does not support the following options: '%s'.",
+ &[shirabe_php_shim::PhpMixed::String(diff.join("', '"))],
+ ),
+ code: 0,
+ },
+ ));
+ }
+
+ if let Some(align) = options.get("align") {
+ let align = match align {
+ shirabe_php_shim::PhpMixed::String(align) => align.clone(),
+ _ => String::new(),
+ };
+ if align_map(&align).is_none() {
+ return Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: shirabe_php_shim::sprintf(
+ "Wrong align value. Value must be following: '%s'.",
+ &[shirabe_php_shim::PhpMixed::String(
+ align_map_keys().join("', '"),
+ )],
+ ),
+ code: 0,
+ },
+ ));
+ }
+ }
+
+ for (key, value) in options {
+ this_options.insert(key, value);
+ }
+
+ Ok(Self {
+ options: this_options,
+ })
+ }
+
+ pub fn get_options(&self) -> IndexMap<String, shirabe_php_shim::PhpMixed> {
+ self.options.clone()
+ }
+
+ /// Gets options we need for tag for example fg, bg.
+ ///
+ /// @return string[]
+ pub fn get_tag_options(&self) -> IndexMap<String, shirabe_php_shim::PhpMixed> {
+ let mut result: IndexMap<String, shirabe_php_shim::PhpMixed> = IndexMap::new();
+ for (key, value) in self.get_options() {
+ if TAG_OPTIONS.contains(&key.as_str())
+ && !matches!(self.options[&key], shirabe_php_shim::PhpMixed::Null)
+ {
+ result.insert(key, value);
+ }
+ }
+ result
+ }
+
+ pub fn get_pad_by_align(&self) -> i64 {
+ let align = match &self.get_options()["align"] {
+ shirabe_php_shim::PhpMixed::String(align) => align.clone(),
+ _ => String::new(),
+ };
+ align_map(&align).unwrap()
+ }
+
+ pub fn get_cell_format(&self) -> Option<String> {
+ match &self.get_options()["cellFormat"] {
+ shirabe_php_shim::PhpMixed::String(format) => Some(format.clone()),
+ _ => None,
+ }
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table_rows.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table_rows.rs
new file mode 100644
index 0000000..c8394df
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table_rows.rs
@@ -0,0 +1,43 @@
+use shirabe_php_shim::PhpMixed;
+
+/// @internal
+///
+/// In PHP this wraps a `\Closure` yielding a `\Traversable` of row groups. The generator
+/// borrows the Table to lazily call `fillCells()`. For the Rust port we precompute the row
+/// groups eagerly (see `Table::build_table_rows`) and store them here.
+#[derive(Debug)]
+pub struct TableRows {
+ row_groups: Vec<Vec<PhpMixed>>,
+}
+
+impl TableRows {
+ pub fn from_row_groups(row_groups: Vec<Vec<PhpMixed>>) -> Self {
+ Self { row_groups }
+ }
+
+ pub fn get_iterator(&self) -> std::slice::Iter<'_, Vec<PhpMixed>> {
+ self.row_groups.iter()
+ }
+
+ pub fn into_row_groups(self) -> Vec<Vec<PhpMixed>> {
+ self.row_groups
+ }
+}
+
+impl<'a> IntoIterator for &'a TableRows {
+ type Item = &'a Vec<PhpMixed>;
+ type IntoIter = std::slice::Iter<'a, Vec<PhpMixed>>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.row_groups.iter()
+ }
+}
+
+impl IntoIterator for TableRows {
+ type Item = Vec<PhpMixed>;
+ type IntoIter = std::vec::IntoIter<Vec<PhpMixed>>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.row_groups.into_iter()
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table_separator.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table_separator.rs
index 22ac013..9dac411 100644
--- a/crates/shirabe-external-packages/src/symfony/console/helper/table_separator.rs
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table_separator.rs
@@ -1,14 +1,29 @@
-#[derive(Debug)]
-pub struct TableSeparator;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::helper::table_cell::{TableCell, TableCellOption};
+use indexmap::IndexMap;
-impl Default for TableSeparator {
- fn default() -> Self {
- Self::new()
- }
+/// Marks a row as being a separator.
+#[derive(Debug, Clone)]
+pub struct TableSeparator {
+ inner: TableCell,
}
impl TableSeparator {
pub fn new() -> Self {
- todo!()
+ Self::new1(IndexMap::new()).expect("TableSeparator default options are always valid")
+ }
+
+ pub fn new1(
+ options: IndexMap<String, TableCellOption>,
+ ) -> Result<Self, InvalidArgumentException> {
+ Ok(Self {
+ inner: TableCell::new("", options)?,
+ })
+ }
+}
+
+impl Default for TableSeparator {
+ fn default() -> Self {
+ Self::new()
}
}
diff --git a/crates/shirabe-external-packages/src/symfony/console/helper/table_style.rs b/crates/shirabe-external-packages/src/symfony/console/helper/table_style.rs
new file mode 100644
index 0000000..5820f15
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/helper/table_style.rs
@@ -0,0 +1,293 @@
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+use crate::symfony::console::exception::logic_exception::LogicException;
+
+/// Defines the styles for a Table.
+#[derive(Debug)]
+pub struct TableStyle {
+ padding_char: String,
+ horizontal_outside_border_char: String,
+ horizontal_inside_border_char: String,
+ vertical_outside_border_char: String,
+ vertical_inside_border_char: String,
+ crossing_char: String,
+ crossing_top_right_char: String,
+ crossing_top_mid_char: String,
+ crossing_top_left_char: String,
+ crossing_mid_right_char: String,
+ crossing_bottom_right_char: String,
+ crossing_bottom_mid_char: String,
+ crossing_bottom_left_char: String,
+ crossing_mid_left_char: String,
+ crossing_top_left_bottom_char: String,
+ crossing_top_mid_bottom_char: String,
+ crossing_top_right_bottom_char: String,
+ header_title_format: String,
+ footer_title_format: String,
+ cell_header_format: String,
+ cell_row_format: String,
+ cell_row_content_format: String,
+ border_format: String,
+ pad_type: i64,
+}
+
+impl Default for TableStyle {
+ fn default() -> Self {
+ Self {
+ padding_char: " ".to_string(),
+ horizontal_outside_border_char: "-".to_string(),
+ horizontal_inside_border_char: "-".to_string(),
+ vertical_outside_border_char: "|".to_string(),
+ vertical_inside_border_char: "|".to_string(),
+ crossing_char: "+".to_string(),
+ crossing_top_right_char: "+".to_string(),
+ crossing_top_mid_char: "+".to_string(),
+ crossing_top_left_char: "+".to_string(),
+ crossing_mid_right_char: "+".to_string(),
+ crossing_bottom_right_char: "+".to_string(),
+ crossing_bottom_mid_char: "+".to_string(),
+ crossing_bottom_left_char: "+".to_string(),
+ crossing_mid_left_char: "+".to_string(),
+ crossing_top_left_bottom_char: "+".to_string(),
+ crossing_top_mid_bottom_char: "+".to_string(),
+ crossing_top_right_bottom_char: "+".to_string(),
+ header_title_format: "<fg=black;bg=white;options=bold> %s </>".to_string(),
+ footer_title_format: "<fg=black;bg=white;options=bold> %s </>".to_string(),
+ cell_header_format: "<info>%s</info>".to_string(),
+ cell_row_format: "%s".to_string(),
+ cell_row_content_format: " %s ".to_string(),
+ border_format: "%s".to_string(),
+ pad_type: shirabe_php_shim::STR_PAD_RIGHT,
+ }
+ }
+}
+
+impl TableStyle {
+ /// Sets padding character, used for cell padding.
+ pub fn set_padding_char(
+ &mut self,
+ padding_char: String,
+ ) -> anyhow::Result<Result<&mut Self, LogicException>> {
+ if padding_char.is_empty() {
+ return Ok(Err(LogicException(shirabe_php_shim::LogicException {
+ message: "The padding char must not be empty.".to_string(),
+ code: 0,
+ })));
+ }
+
+ self.padding_char = padding_char;
+
+ Ok(Ok(self))
+ }
+
+ /// Gets padding character, used for cell padding.
+ pub fn get_padding_char(&self) -> String {
+ self.padding_char.clone()
+ }
+
+ /// Sets horizontal border characters.
+ pub fn set_horizontal_border_chars(
+ &mut self,
+ outside: String,
+ inside: Option<String>,
+ ) -> &mut Self {
+ self.horizontal_outside_border_char = outside.clone();
+ self.horizontal_inside_border_char = inside.unwrap_or(outside);
+
+ self
+ }
+
+ /// Sets vertical border characters.
+ pub fn set_vertical_border_chars(
+ &mut self,
+ outside: String,
+ inside: Option<String>,
+ ) -> &mut Self {
+ self.vertical_outside_border_char = outside.clone();
+ self.vertical_inside_border_char = inside.unwrap_or(outside);
+
+ self
+ }
+
+ /// Gets border characters.
+ pub fn get_border_chars(&self) -> Vec<String> {
+ vec![
+ self.horizontal_outside_border_char.clone(),
+ self.vertical_outside_border_char.clone(),
+ self.horizontal_inside_border_char.clone(),
+ self.vertical_inside_border_char.clone(),
+ ]
+ }
+
+ /// Sets crossing characters.
+ #[allow(clippy::too_many_arguments)]
+ pub fn set_crossing_chars(
+ &mut self,
+ cross: String,
+ top_left: String,
+ top_mid: String,
+ top_right: String,
+ mid_right: String,
+ bottom_right: String,
+ bottom_mid: String,
+ bottom_left: String,
+ mid_left: String,
+ top_left_bottom: Option<String>,
+ top_mid_bottom: Option<String>,
+ top_right_bottom: Option<String>,
+ ) -> &mut Self {
+ self.crossing_char = cross.clone();
+ self.crossing_top_left_char = top_left;
+ self.crossing_top_mid_char = top_mid;
+ self.crossing_top_right_char = top_right;
+ self.crossing_mid_right_char = mid_right.clone();
+ self.crossing_bottom_right_char = bottom_right;
+ self.crossing_bottom_mid_char = bottom_mid;
+ self.crossing_bottom_left_char = bottom_left;
+ self.crossing_mid_left_char = mid_left.clone();
+ self.crossing_top_left_bottom_char = top_left_bottom.unwrap_or(mid_left);
+ self.crossing_top_mid_bottom_char = top_mid_bottom.unwrap_or(cross);
+ self.crossing_top_right_bottom_char = top_right_bottom.unwrap_or(mid_right);
+
+ self
+ }
+
+ /// Sets default crossing character used for each cross.
+ pub fn set_default_crossing_char(&mut self, char: String) -> &mut Self {
+ self.set_crossing_chars(
+ char.clone(),
+ char.clone(),
+ char.clone(),
+ char.clone(),
+ char.clone(),
+ char.clone(),
+ char.clone(),
+ char.clone(),
+ char,
+ None,
+ None,
+ None,
+ )
+ }
+
+ /// Gets crossing character.
+ pub fn get_crossing_char(&self) -> String {
+ self.crossing_char.clone()
+ }
+
+ /// Gets crossing characters.
+ pub fn get_crossing_chars(&self) -> Vec<String> {
+ vec![
+ self.crossing_char.clone(),
+ self.crossing_top_left_char.clone(),
+ self.crossing_top_mid_char.clone(),
+ self.crossing_top_right_char.clone(),
+ self.crossing_mid_right_char.clone(),
+ self.crossing_bottom_right_char.clone(),
+ self.crossing_bottom_mid_char.clone(),
+ self.crossing_bottom_left_char.clone(),
+ self.crossing_mid_left_char.clone(),
+ self.crossing_top_left_bottom_char.clone(),
+ self.crossing_top_mid_bottom_char.clone(),
+ self.crossing_top_right_bottom_char.clone(),
+ ]
+ }
+
+ /// Sets header cell format.
+ pub fn set_cell_header_format(&mut self, cell_header_format: String) -> &mut Self {
+ self.cell_header_format = cell_header_format;
+
+ self
+ }
+
+ /// Gets header cell format.
+ pub fn get_cell_header_format(&self) -> String {
+ self.cell_header_format.clone()
+ }
+
+ /// Sets row cell format.
+ pub fn set_cell_row_format(&mut self, cell_row_format: String) -> &mut Self {
+ self.cell_row_format = cell_row_format;
+
+ self
+ }
+
+ /// Gets row cell format.
+ pub fn get_cell_row_format(&self) -> String {
+ self.cell_row_format.clone()
+ }
+
+ /// Sets row cell content format.
+ pub fn set_cell_row_content_format(&mut self, cell_row_content_format: String) -> &mut Self {
+ self.cell_row_content_format = cell_row_content_format;
+
+ self
+ }
+
+ /// Gets row cell content format.
+ pub fn get_cell_row_content_format(&self) -> String {
+ self.cell_row_content_format.clone()
+ }
+
+ /// Sets table border format.
+ pub fn set_border_format(&mut self, border_format: String) -> &mut Self {
+ self.border_format = border_format;
+
+ self
+ }
+
+ /// Gets table border format.
+ pub fn get_border_format(&self) -> String {
+ self.border_format.clone()
+ }
+
+ /// Sets cell padding type.
+ pub fn set_pad_type(
+ &mut self,
+ pad_type: i64,
+ ) -> anyhow::Result<Result<&mut Self, InvalidArgumentException>> {
+ if ![
+ shirabe_php_shim::STR_PAD_LEFT,
+ shirabe_php_shim::STR_PAD_RIGHT,
+ shirabe_php_shim::STR_PAD_BOTH,
+ ]
+ .contains(&pad_type)
+ {
+ return Ok(Err(InvalidArgumentException(
+ shirabe_php_shim::InvalidArgumentException {
+ message: "Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH)."
+ .to_string(),
+ code: 0,
+ },
+ )));
+ }
+
+ self.pad_type = pad_type;
+
+ Ok(Ok(self))
+ }
+
+ /// Gets cell padding type.
+ pub fn get_pad_type(&self) -> i64 {
+ self.pad_type
+ }
+
+ pub fn get_header_title_format(&self) -> String {
+ self.header_title_format.clone()
+ }
+
+ pub fn set_header_title_format(&mut self, format: String) -> &mut Self {
+ self.header_title_format = format;
+
+ self
+ }
+
+ pub fn get_footer_title_format(&self) -> String {
+ self.footer_title_format.clone()
+ }
+
+ pub fn set_footer_title_format(&mut self, format: String) -> &mut Self {
+ self.footer_title_format = format;
+
+ self
+ }
+}