diff options
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/helper')
16 files changed, 0 insertions, 4909 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 deleted file mode 100644 index 6fcbce8e..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/debug_formatter_helper.rs +++ /dev/null @@ -1,175 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/DebugFormatterHelper.php - -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; - -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, - }, - ); - - format!( - "{}<bg=blue;fg=white> {} </> <fg=blue>{}</>\n", - self.get_border(id), - prefix, - message, - ) - } - - /// 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(&format!( - "{}<bg=red;fg=white> {} </> ", - self.get_border(id), - error_prefix, - )); - self.started.get_mut(id).unwrap().err = true; - } - - message.push_str(&shirabe_php_shim::str_replace( - "\n", - &format!( - "\n{}<bg=red;fg=white> {} </> ", - self.get_border(id), - error_prefix, - ), - 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(&format!( - "{}<bg=green;fg=white> {} </> ", - self.get_border(id), - prefix, - )); - self.started.get_mut(id).unwrap().out = true; - } - - message.push_str(&shirabe_php_shim::str_replace( - "\n", - &format!( - "\n{}<bg=green;fg=white> {} </> ", - self.get_border(id), - prefix, - ), - 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 format!( - "{}{}<bg=green;fg=white> {} </> <fg=green>{}</>\n", - trailing_eol, - self.get_border(id), - prefix, - message, - ); - } - - let message = format!( - "{}{}<bg=red;fg=white> {} </> <fg=red>{}</>\n", - trailing_eol, - self.get_border(id), - prefix, - message, - ); - - if let Some(session) = self.started.get_mut(id) { - session.out = false; - session.err = false; - } - - message - } - - fn get_border(&self, id: &str) -> String { - format!("<bg={}> </>", COLORS[self.started[id].border as usize]) - } -} - -impl HelperInterface for DebugFormatterHelper { - fn set_helper_set(&mut self, helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::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 deleted file mode 100644 index 36603b06..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/descriptor_helper.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/DescriptorHelper.php - -use crate::symfony::console::descriptor::descriptor_interface::{ - DescribableObject, 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; - -/// 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(), - }; - this.register("txt", Box::new(TextDescriptor::default())) - .register("xml", Box::new(XmlDescriptor::default())) - .register("json", Box::new(JsonDescriptor::default())) - .register("md", Box::new(MarkdownDescriptor::default())); - 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( - &mut self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - object: DescribableObject, - options: IndexMap<String, shirabe_php_shim::PhpMixed>, - ) -> anyhow::Result<()> { - 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::new(format!( - "Unsupported format \"{}\".", - format.clone() - )) - .into()); - } - - let descriptor = self.descriptors.get_mut(&format).unwrap(); - descriptor.describe(output, object, options) - } - - /// Registers a descriptor. - 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<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::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 deleted file mode 100644 index 056a2ffe..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/formatter_helper.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/FormatterHelper.php - -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; - -/// The Formatter class provides helpers to format messages. -#[derive(Debug, Default)] -pub struct FormatterHelper { - inner: Helper, -} - -impl FormatterHelper { - /// Formats a message within a section. - pub fn format_section(&self, section: &str, message: &str, style: &str) -> String { - format!("<{}>[{}]</{}> {}", style, section, style, message) - } - - /// 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(if large { - format!(" {} ", message) - } else { - format!(" {} ", message) - }); - 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] = format!("<{}>{}</{}>", style, messages[i].clone(), style); - 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 set_helper_set(&mut self, helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::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 deleted file mode 100644 index 471a8d8a..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/helper.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/Helper.php - -use crate::symfony::console::formatter::output_formatter_interface::OutputFormatterInterface; -use crate::symfony::console::helper::helper_set::HelperSet; -use shirabe_php_shim::php_regex; -use shirabe_symfony_string::unicode_string::UnicodeString; - -/// Helper is the base class for all helper classes. -#[derive(Debug, Default)] -pub struct Helper { - pub(crate) helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>, -} - -impl Helper { - pub fn set_helper_set( - &mut self, - helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>, - ) { - self.helper_set = helper_set; - } - - pub fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::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(php_regex!("//u"), string, &mut Vec::new()) { - 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(php_regex!("//u"), string, &mut Vec::new()) { - 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 - && ((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 format!("{:.1} GiB", memory as f64 / 1024.0 / 1024.0 / 1024.0); - } - - if memory >= 1024 * 1024 { - return format!("{:.1} MiB", memory as f64 / 1024.0 / 1024.0); - } - - if memory >= 1024 { - return format!("{} KiB", memory / 1024); - } - - format!("{} B", 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(php_regex!("/\u{1b}\\[[^m]*m/"), "", &string); - // remove terminal hyperlinks - let string = shirabe_php_shim::preg_replace( - php_regex!("/\u{1b}]8;[^;]*;[^\u{1b}]*\u{1b}\\\\/"), - "", - &string, - ); - 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 deleted file mode 100644 index 8dae6c85..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/helper_interface.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/HelperInterface.php - -use crate::symfony::console::helper::helper_set::HelperSet; - -/// 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<std::rc::Rc<std::cell::RefCell<HelperSet>>>); - - /// Gets the helper set associated with this helper. - fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>; - - /// 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 deleted file mode 100644 index b505fedf..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/helper_set.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/HelperSet.php - -use crate::symfony::console::helper::debug_formatter_helper::DebugFormatterHelper; -use crate::symfony::console::helper::formatter_helper::FormatterHelper; -use crate::symfony::console::helper::helper_interface::HelperInterface; -use crate::symfony::console::helper::process_helper::ProcessHelper; -use crate::symfony::console::helper::question_helper::QuestionHelper; - -/// HelperSet represents a set of helpers to be used with a command. -/// -/// Symfony lets arbitrary helpers be registered by name, but Composer only ever uses the four -/// helpers `Application::getDefaultHelperSet()` installs. This port closes the set to exactly those -/// four, instantiates them in the argument-less constructor, and exposes them through typed getters -/// instead of Symfony's string-keyed `get()`/`has()`/`set()`. -/// -/// TODO(plugin): a plugin-defined custom command may register extra helpers dynamically via -/// `getApplication()->getHelperSet()`. Restoring that path (a `set()` equivalent plus name-based -/// lookup) is deferred until the plugin API is implemented. -#[derive(Debug)] -pub struct HelperSet { - formatter_helper: std::rc::Rc<std::cell::RefCell<FormatterHelper>>, - debug_formatter_helper: std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, - process_helper: std::rc::Rc<std::cell::RefCell<ProcessHelper>>, - question_helper: std::rc::Rc<std::cell::RefCell<QuestionHelper>>, -} - -impl HelperSet { - /// Builds the fixed set of helpers and wires each one's back-reference to the owning set, - /// mirroring the `$helper->setHelperSet($this)` call PHP's `HelperSet::set()` performs. - pub fn new() -> std::rc::Rc<std::cell::RefCell<HelperSet>> { - let formatter_helper = - std::rc::Rc::new(std::cell::RefCell::new(FormatterHelper::default())); - let debug_formatter_helper = - std::rc::Rc::new(std::cell::RefCell::new(DebugFormatterHelper::default())); - let process_helper = std::rc::Rc::new(std::cell::RefCell::new(ProcessHelper::default())); - let question_helper = std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper::default())); - - let this = std::rc::Rc::new(std::cell::RefCell::new(HelperSet { - formatter_helper: formatter_helper.clone(), - debug_formatter_helper: debug_formatter_helper.clone(), - process_helper: process_helper.clone(), - question_helper: question_helper.clone(), - })); - - formatter_helper - .borrow_mut() - .set_helper_set(Some(this.clone())); - debug_formatter_helper - .borrow_mut() - .set_helper_set(Some(this.clone())); - process_helper - .borrow_mut() - .set_helper_set(Some(this.clone())); - question_helper - .borrow_mut() - .set_helper_set(Some(this.clone())); - - this - } - - pub fn get_formatter(&self) -> std::rc::Rc<std::cell::RefCell<FormatterHelper>> { - self.formatter_helper.clone() - } - - pub fn get_debug_formatter(&self) -> std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>> { - self.debug_formatter_helper.clone() - } - - pub fn get_process(&self) -> std::rc::Rc<std::cell::RefCell<ProcessHelper>> { - self.process_helper.clone() - } - - pub fn get_question(&self) -> std::rc::Rc<std::cell::RefCell<QuestionHelper>> { - self.question_helper.clone() - } -} 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 deleted file mode 100644 index 303a4569..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/process_helper.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/ProcessHelper.php - -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::ConsoleOutputInterface; -use crate::symfony::console::output::output_interface::{self, OutputInterface}; -use shirabe_symfony_process::exception::process_failed_exception::ProcessFailedException; -use shirabe_symfony_process::process::Process; - -/// 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: std::rc::Rc<std::cell::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(); }`. ConsoleOutput is the only OutputInterface - // implementor that also implements ConsoleOutputInterface, so the check - // reduces to a downcast to the concrete type. - let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = { - let redirected = shirabe_php_shim::AsAny::as_any(&*output.borrow()) - .downcast_ref::<crate::symfony::console::output::console_output::ConsoleOutput>() - .map(|console| console.get_error_output()); - redirected.unwrap_or(output) - }; - - let formatter: std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>> = self - .get_helper_set() - .unwrap() - .borrow() - .get_debug_formatter(); - - // 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, - shirabe_php_shim::PhpMixed::Null, - 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::new(format!( - "Invalid command provided to \"{}()\": 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()), - ))); - } - } - - if verbosity <= output.borrow().get_verbosity() { - let started = Self::formatter_start( - &formatter, - &shirabe_php_shim::spl_object_hash(&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. - let env: indexmap::IndexMap<String, shirabe_php_shim::PhpMixed> = cmd - .iter() - .enumerate() - .filter_map(|(i, element)| match element { - ProcessHelperCmdElement::String(s) => { - Some((i.to_string(), shirabe_php_shim::PhpMixed::String(s.clone()))) - } - ProcessHelperCmdElement::Process(_) => None, - }) - .collect(); - let callback: Option<Box<dyn FnMut(&str, &str) -> bool>> = callback.map(|mut cb| { - Box::new(move |r#type: &str, buffer: &str| -> bool { - cb(r#type, buffer); - false - }) as Box<dyn FnMut(&str, &str) -> bool> - }); - process.run(callback, env)?; - - if verbosity <= output.borrow().get_verbosity() { - let message = if process.is_successful() { - "Command ran successfully".to_string() - } else { - format!( - "{} 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), - &message, - process.is_successful(), - ); - output - .borrow() - .write(&[stopped], false, output_interface::OUTPUT_NORMAL); - } - - if !process.is_successful() - && let Some(error) = error - { - output.borrow().writeln( - &[format!("<error>{}</error>", self.escape_string(error))], - 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: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - cmd: ProcessHelperCmd, - error: Option<&str>, - callback: Option<Box<dyn FnMut(&str, &str)>>, - ) -> anyhow::Result<Process> { - let mut process = self.run( - output, - cmd, - error, - callback, - output_interface::VERBOSITY_VERY_VERBOSE, - )?; - - if !process.is_successful() { - anyhow::bail!(ProcessFailedException::new(&mut process)?); - } - - Ok(process) - } - - /// Wraps a Process callback to add debugging output. - pub fn wrap_callback( - &self, - output: std::rc::Rc<std::cell::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(); }`. ConsoleOutput is the only OutputInterface - // implementor that also implements ConsoleOutputInterface, so the check - // reduces to a downcast to the concrete type. - let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = { - let redirected = shirabe_php_shim::AsAny::as_any(&*output.borrow()) - .downcast_ref::<crate::symfony::console::output::console_output::ConsoleOutput>() - .map(|console| console.get_error_output()); - redirected.unwrap_or(output) - }; - - let formatter: std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>> = self - .get_helper_set() - .unwrap() - .borrow() - .get_debug_formatter(); - - let object_hash = shirabe_php_shim::spl_object_hash(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) - } - - fn formatter_start( - formatter: &std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, - id: &str, - message: &str, - ) -> String { - formatter.borrow_mut().start(id, message, "RUN") - } - - fn formatter_stop( - formatter: &std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, - id: &str, - message: &str, - successful: bool, - ) -> String { - formatter.borrow_mut().stop(id, message, successful, "RES") - } - - fn formatter_progress( - formatter: &std::rc::Rc<std::cell::RefCell<DebugFormatterHelper>>, - id: &str, - buffer: &str, - error: bool, - ) -> String { - formatter - .borrow_mut() - .progress(id, buffer, error, "OUT", "ERR") - } -} - -impl HelperInterface for ProcessHelper { - fn set_helper_set(&mut self, helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::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 deleted file mode 100644 index 7bfbfbcd..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/progress_bar.rs +++ /dev/null @@ -1,835 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/ProgressBar.php - -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::ConsoleOutputInterface; -use crate::symfony::console::output::ConsoleSectionOutput; -use crate::symfony::console::output::OutputInterface; -use crate::symfony::console::output::output_interface; -use crate::symfony::console::terminal::Terminal; -use indexmap::IndexMap; - -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, - &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> anyhow::Result<Result<shirabe_php_shim::PhpMixed, LogicException>>, ->; - -/// The ProgressBar provides helpers to display progress output. -#[derive(Debug)] -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: std::rc::Rc<std::cell::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: std::cell::RefCell<Option<IndexMap<String, PlaceholderFormatter>>> = const { std::cell::RefCell::new(None) }; - static FORMATS: std::cell::RefCell<Option<IndexMap<String, String>>> = const { std::cell::RefCell::new(None) }; -} - -impl ProgressBar { - /// `$max` Maximum steps (0 if unknown) - pub fn new( - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - max: i64, - min_seconds_between_redraws: f64, - ) -> Self { - // PHP: `if ($output instanceof ConsoleOutputInterface) { $output = - // $output->getErrorOutput(); }`. ConsoleOutput is the only OutputInterface - // implementor that also implements ConsoleOutputInterface, so the check - // reduces to a downcast to the concrete type. - let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = { - let redirected = shirabe_php_shim::AsAny::as_any(&*output.borrow()) - .downcast_ref::<crate::symfony::console::output::console_output::ConsoleOutput>() - .map(|console| console.get_error_output()); - redirected.unwrap_or(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 - } - - /// 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); - }); - } - - /// 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(|_| ()) - }) - } - - /// 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()); - }); - } - - /// 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() - }) - } - - /// 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 { - 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 { - f64::floor(if self.max != 0 { - self.percent * self.bar_width as f64 - } else if self.redraw_freq.is_none() { - (((self.bar_width / 15).min(5) * 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 = size.max(1); - } - - 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| freq.max(1)); - } - - 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({ - // 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() - 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 = max.max(0); - 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() { - // PHP: `$this->output instanceof ConsoleSectionOutput`. Downcast the - // shared output handle to the concrete section type. - let output_ref = self.output.borrow(); - if let Some(section) = shirabe_php_shim::AsAny::as_any(&*output_ref) - .downcast_ref::<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 formatter = section.get_formatter(); - let message_line_length = Helper::width(&Helper::remove_decoration( - &mut *formatter.borrow_mut(), - message_line, - )); - if message_line_length > self.terminal.get_width() { - line_count += (message_line_length as f64 - / self.terminal.get_width() as f64) - .floor() as i64; - } - } - section.clear(Some(line_count)); - } else { - drop(output_ref); - 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(); - - self.output - .borrow() - .write(&[message], false, output_interface::OUTPUT_NORMAL); - self.write_count += 1; - } - - 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: &std::rc::Rc<std::cell::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( - &mut *output.borrow().get_formatter().borrow_mut(), - &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 - ) - )); - } - - Ok(Ok(shirabe_php_shim::PhpMixed::String(display))) - }, - ), - ); - - formatters.insert( - "elapsed".to_string(), - Box::new( - |bar: &ProgressBar, - _output: &std::rc::Rc<std::cell::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: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| { - if bar.get_max_steps() == 0 { - return Ok(Err(LogicException::new("Unable to display the remaining time if the maximum number of steps is not set.".to_string()))); - } - - 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: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| { - if bar.get_max_steps() == 0 { - return Ok(Err(LogicException::new("Unable to display the estimated time if the maximum number of steps is not set.".to_string()))); - } - - 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: &std::rc::Rc<std::cell::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: &std::rc::Rc<std::cell::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: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| { - Ok(Ok(shirabe_php_shim::PhpMixed::Int(bar.get_max_steps()))) - }, - ), - ); - - formatters.insert( - "percent".to_string(), - Box::new( - |bar: &ProgressBar, - _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| { - Ok(Ok(shirabe_php_shim::PhpMixed::Float( - (bar.get_progress_percent() * 100.0).floor(), - ))) - }, - ), - ); - - 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( - &mut *self.output.borrow().get_formatter().borrow_mut(), - &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) - }); - formatter_result?? - } 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) => { - format!("{}", f) - } - other => format!("{}", 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 deleted file mode 100644 index 91fb5bf0..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/question_helper.rs +++ /dev/null @@ -1,916 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/QuestionHelper.php - -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::formatter_helper::FormatBlockMessages; -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::ChoiceQuestion; -use crate::symfony::console::question::QuestionInterface; -use crate::symfony::console::terminal::Terminal; -use shirabe_php_shim::PhpMixed; -use shirabe_symfony_string::s; - -/// 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); - -/// PHP dispatches `$this->writePrompt()` / `$this->writeError()` virtually, and -/// `SymfonyQuestionHelper` overrides both protected methods. The embedded-super port expresses -/// that late binding as a trait: the template methods are provided here and reach the overridable -/// hooks through `Self`, while `inner()` reaches the base-class state. PHP has no such interface; -/// the invented name follows the `QuestionInterface` precedent. -pub trait QuestionHelperInterface { - fn inner(&self) -> &QuestionHelper; - - fn inner_mut(&mut self) -> &mut 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 - fn ask( - &mut self, - input: &mut dyn InputInterface, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) -> 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.inner().get_default_answer(question))); - } - - if let Some(streamable) = input.as_streamable() - && let Some(stream) = streamable.get_stream() - { - self.inner_mut().input_stream = Some(stream); - } - - let result: anyhow::Result<Result<PhpMixed, MissingInputException>> = (|| { - if question.get_validator().is_none() { - return self.do_ask(std::rc::Rc::clone(&output), question); - } - - let interviewer = || self.do_ask(std::rc::Rc::clone(&output), question); - - self.validate_attempts(&interviewer, std::rc::Rc::clone(&output), question) - })(); - - let result = result?; - match result { - Ok(value) => Ok(Ok(value)), - Err(exception) => { - input.set_interactive(false); - - let fallback_output = self.inner().get_default_answer(question); - if matches!(fallback_output, PhpMixed::Null) { - return Ok(Err(exception)); - } - - Ok(Ok(fallback_output)) - } - } - } - - /// Asks the question to the user (PHP private; on the trait so it can late-bind the hooks). - /// - /// @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden - fn do_ask( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) -> anyhow::Result<Result<PhpMixed, MissingInputException>> { - self.write_prompt(std::rc::Rc::clone(&output), question); - - let input_stream = self - .inner() - .input_stream - .clone() - .unwrap_or_else(shirabe_php_shim::stdin); - let autocomplete = question.get_autocompleter_callback(); - - let ret: PhpMixed; - - if let Some(autocomplete) = autocomplete - && STTY.load(std::sync::atomic::Ordering::SeqCst) - && Terminal::has_stty_available() - { - let callback = autocomplete; - // 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 = match self.inner().autocomplete( - std::rc::Rc::clone(&output), - question, - &input_stream, - &callback, - ) { - Ok(value) => value, - Err(exception) => return Ok(Err(exception)), - }; - ret = PhpMixed::String(if question.is_trimmable() { - shirabe_php_shim::trim(&autocomplete, None) - } else { - autocomplete - }); - } else { - let mut r: PhpMixed = PhpMixed::Bool(false); - if question.is_hidden() { - match self.inner().get_hidden_response( - std::rc::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") - .cloned() - .unwrap_or(PhpMixed::Bool(true)); - - if !shirabe_php_shim::boolval(&is_blocked) { - shirabe_php_shim::stream_set_blocking(&input_stream, true); - } - - let read = self.inner().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::new("Aborted.".to_string()))); - } - r = read; - if question.is_trimmable() { - r = PhpMixed::String(shirabe_php_shim::trim(&r.to_string(), None)); - } - } - ret = r; - } - - 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)) - } - - /// Validates an attempt (PHP private; on the trait so it can late-bind the hooks). - /// - /// @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: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) -> 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(std::rc::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::with_code( - e.get_message().to_string(), - e.get_code(), - )); - } - } - } - - // throw $error; - Err(anyhow::Error::msg( - error - .map(|e| e.get_message().to_string()) - .unwrap_or_default(), - )) - } - - /// Outputs the question prompt (PHP protected; the overridable hook). - fn write_prompt( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) { - self.inner().write_prompt(output, question); - } - - /// Outputs an error message (PHP protected; the overridable hook). - fn write_error( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - error: &shirabe_php_shim::Exception, - ) { - self.inner().write_error(output, error); - } -} - -impl QuestionHelperInterface for QuestionHelper { - fn inner(&self) -> &QuestionHelper { - self - } - - fn inner_mut(&mut self) -> &mut QuestionHelper { - self - } -} - -impl QuestionHelper { - 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); - } - - fn get_default_answer(&self, question: &impl QuestionInterface) -> 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() { - let choices = choice_question.get_choices(); - - if !choice_question.is_multiselect() { - return choices - .get(&default.to_string()) - .cloned() - .unwrap_or(default); - } - - let default_parts = shirabe_php_shim::explode(",", &default.to_string()); - let mut resolved: indexmap::IndexMap<String, 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).cloned().unwrap_or(PhpMixed::String(v)); - resolved.insert(k.to_string(), value); - } - - return PhpMixed::Array(resolved); - } - - default - } - - /// Outputs the question prompt. - pub(crate) fn write_prompt( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) { - let mut message = question.get_question().to_string(); - - if let Some(choice_question) = question.as_choice() { - 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); - } - - 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(format!( - " [<{tag}>{}{padding}</{tag}>] {}", - key.clone(), - value.clone(), - )); - } - - messages - } - - /// Outputs an error message. - pub(crate) fn write_error( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - error: &shirabe_php_shim::Exception, - ) { - let message = if let Some(helper_set) = self.get_helper_set() { - let formatter = helper_set.borrow().get_formatter(); - - formatter.borrow().format_block( - FormatBlockMessages::String(error.get_message().to_string()), - "error", - false, - ) - } else { - format!("<error>{}</error>", error.get_message()) - }; - - output - .borrow() - .writeln(&[message], output_interface::OUTPUT_NORMAL); - } - - /// Autocompletes a question. - fn autocomplete( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - input_stream: &shirabe_php_shim::PhpResource, - autocomplete: &dyn Fn(&str) -> Vec<PhpMixed>, - ) -> Result<String, MissingInputException> { - let cursor = Cursor::new(std::rc::Rc::clone(&output), Some(input_stream.clone())); - - 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 !shirabe_php_shim::feof(input_stream) { - while is_stdin - && Some(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 = shirabe_php_shim::fread(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)); - return Err(MissingInputException::new("Aborted.".to_string())); - } 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 = shirabe_php_shim::fread(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( - std::slice::from_ref(&remaining_characters), - 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(); - ofs = -1; - } - - if c.as_deref() == Some("\n") { - output.borrow().write( - &[c.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 = shirabe_php_shim::fread(input_stream, len).unwrap_or_default(); - c = Some(format!("{}{}", cur, extra)); - } - - let cur = c.clone().unwrap_or_default(); - output.borrow().write( - std::slice::from_ref(&cur), - 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() - && 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)); - - Ok(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, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - input_stream: &shirabe_php_shim::PhpResource, - trimmable: bool, - ) -> anyhow::Result<Result<String, RuntimeException>> { - if cfg!(windows) { - 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::new( - "Unable to hide the response.".to_string(), - ))); - } - - let value = shirabe_php_shim::fgets(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::new("Aborted.".to_string()).into()); - } - }; - if trimmable { - value = shirabe_php_shim::trim(&value, None); - } - output - .borrow() - .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL); - - Ok(Ok(value)) - } - - 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: &impl QuestionInterface, - ) -> PhpMixed { - if !question.is_multiline() { - let cp = self.set_io_codepage(); - let ret = shirabe_php_shim::fgets(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. - 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") - .cloned() - .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::fopen(&uri, &mode).ok()?; - - // 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).unwrap_or(0); - shirabe_php_shim::rewind(input_stream); - shirabe_php_shim::stream_copy_to_stream(input_stream, &clone_stream); - shirabe_php_shim::fseek(input_stream, offset, shirabe_php_shim::SEEK_SET); - shirabe_php_shim::fseek(&clone_stream, offset, shirabe_php_shim::SEEK_SET); - } - - 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) - } -} - -/// PHP `__FILE__` magic constant. The executing code lives in the Shirabe binary itself, which is -/// the closest analogue for a native executable; it never carries the `phar:` scheme, so the -/// phar-relocation branch in `get_hidden_response` correctly never triggers. -fn magic_file() -> String { - std::env::current_exe() - .expect("current executable path") - .display() - .to_string() -} - -impl HelperInterface for QuestionHelper { - fn set_helper_set(&mut self, helper_set: Option<std::rc::Rc<std::cell::RefCell<HelperSet>>>) { - self.inner.set_helper_set(helper_set); - } - - fn get_helper_set(&self) -> Option<std::rc::Rc<std::cell::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 deleted file mode 100644 index eda54243..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/symfony_question_helper.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/SymfonyQuestionHelper.php - -use crate::symfony::console::formatter::output_formatter::OutputFormatter; -use crate::symfony::console::helper::question_helper::{QuestionHelper, QuestionHelperInterface}; -use crate::symfony::console::output::output_interface; -use crate::symfony::console::output::output_interface::OutputInterface; -use crate::symfony::console::question::QuestionInterface; -use crate::symfony::console::style::style_interface::StyleInterface; -use crate::symfony::console::style::symfony_style::SymfonyStyle; -use shirabe_php_shim::PhpMixed; -use std::ops::{Deref, DerefMut}; - -/// Symfony Style Guide compliant question helper. -#[derive(Debug, Default)] -pub struct SymfonyQuestionHelper { - inner: QuestionHelper, -} - -impl SymfonyQuestionHelper { - pub fn new() -> Self { - Self::default() - } - - 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 QuestionHelperInterface for SymfonyQuestionHelper { - fn inner(&self) -> &QuestionHelper { - &self.inner - } - - fn inner_mut(&mut self) -> &mut QuestionHelper { - &mut self.inner - } - - /// {@inheritdoc} - fn write_prompt( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - question: &impl QuestionInterface, - ) { - let mut text = OutputFormatter::escape_trailing_backslash(question.get_question()); - let default = question.get_default(); - - if question.is_multiline() { - text += &format!(" (press {} to continue)", self.get_eof_shortcut()); - } - - // switch (true) - if matches!(default, PhpMixed::Null) { - text = format!(" <info>{}</info>:", text); - } else if question.as_confirmation().is_some() { - text = format!( - " <info>{} (yes/no)</info> [<comment>{}</comment>]:", - text, - if shirabe_php_shim::boolval(&default) { - "yes" - } else { - "no" - }, - ); - } else if let Some(choice_question) = question.as_choice().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 = format!( - " <info>{}</info> [<comment>{}</comment>]:", - text, - OutputFormatter::escape(&resolved.join(", ")).unwrap(), - ); - } else if let Some(choice_question) = question.as_choice() { - let choices = choice_question.get_choices(); - text = format!( - " <info>{}</info> [<comment>{}</comment>]:", - text, - OutputFormatter::escape( - &choices - .get(&default.to_string()) - .cloned() - .unwrap_or(default) - .to_string(), - ) - .unwrap(), - ); - } else { - text = format!( - " <info>{}</info> [<comment>{}</comment>]:", - text, - 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_choice() { - 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); - } - - /// {@inheritdoc} - fn write_error( - &self, - output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - error: &shirabe_php_shim::Exception, - ) { - { - let mut borrowed = output.borrow_mut(); - if let Some(style) = (*borrowed).as_any_mut().downcast_mut::<SymfonyStyle>() { - style.new_line(1); - style.error(PhpMixed::String(error.get_message().to_string())); - - return; - } - } - - self.inner.write_error(output, error); - } -} - -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 deleted file mode 100644 index 650dfffa..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table.rs +++ /dev/null @@ -1,1443 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/Table.php - -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_pcre::preg::Preg; -use shirabe_php_shim::{PhpMixed, php_regex}; - -/// A single cell within a table row. -/// -/// PHP types a cell as `TableCell|string|int|null` (scalars are stringified). Because -/// `TableSeparator extends TableCell`, a separator can in principle appear as a cell, so it has its -/// own variant; `is_table_cell` reports `true` for it, mirroring `instanceof TableCell`. -#[derive(Debug, Clone)] -pub enum Cell { - Null, - Value(String), - Cell(TableCell), - Separator(TableSeparator), -} - -impl Cell { - /// PHP `$cell instanceof TableCell` (true for `TableSeparator`, which extends `TableCell`). - fn is_table_cell(&self) -> bool { - matches!(self, Cell::Cell(_) | Cell::Separator(_)) - } - - /// PHP `$cell instanceof TableSeparator`. - fn is_table_separator(&self) -> bool { - matches!(self, Cell::Separator(_)) - } - - fn colspan(&self) -> i64 { - match self { - Cell::Cell(c) => c.get_colspan(), - Cell::Separator(s) => s.get_colspan(), - _ => 1, - } - } - - fn rowspan(&self) -> i64 { - match self { - Cell::Cell(c) => c.get_rowspan(), - Cell::Separator(s) => s.get_rowspan(), - _ => 1, - } - } - - fn style(&self) -> Option<std::rc::Rc<TableCellStyle>> { - match self { - Cell::Cell(c) => c.get_style(), - Cell::Separator(s) => s.get_style(), - _ => None, - } - } - - /// PHP `(string) $cell`. - fn to_php_string(&self) -> String { - match self { - Cell::Null | Cell::Separator(_) => String::new(), - Cell::Value(s) => s.clone(), - Cell::Cell(c) => c.to_string(), - } - } - - /// PHP truthiness / `!empty($cell)` for a cell value: only "" and "0" (and null) are falsy; - /// any object is truthy. - fn is_truthy(&self) -> bool { - match self { - Cell::Null => false, - Cell::Value(s) => !s.is_empty() && s != "0", - Cell::Cell(_) | Cell::Separator(_) => true, - } - } - - fn is_null(&self) -> bool { - matches!(self, Cell::Null) - } -} - -impl From<String> for Cell { - fn from(value: String) -> Self { - Cell::Value(value) - } -} - -impl From<&str> for Cell { - fn from(value: &str) -> Self { - Cell::Value(value.to_string()) - } -} - -impl From<TableCell> for Cell { - fn from(value: TableCell) -> Self { - Cell::Cell(value) - } -} - -/// Bridges PHP-typed cell values built elsewhere in the codebase (where rows are still assembled as -/// `PhpMixed`) onto the typed cell. A cell is a scalar or null; collections stringify as PHP would. -impl From<PhpMixed> for Cell { - fn from(value: PhpMixed) -> Self { - match value { - PhpMixed::Null => Cell::Null, - other => Cell::Value(shirabe_php_shim::to_string(&other)), - } - } -} - -/// A table row. -/// -/// PHP types a row as `array<int, Cell>|TableSeparator`. The header/body boundary that PHP creates -/// as a fresh `TableSeparator` and recognizes by object identity (`$divider === $row`) is modeled -/// here by its own [`Row::HeaderDivider`] variant rather than by reference identity: it counts as a -/// separator for layout (`is_table_separator`), but unlike a user-supplied [`Row::Separator`] it -/// renders no separator line — it only flips the header/first-row state during rendering. -#[derive(Debug, Clone)] -pub enum Row { - HeaderDivider, - Separator(TableSeparator), - Cells(Vec<Cell>), -} - -impl Row { - /// PHP `$row instanceof TableSeparator` (also true for the internal header/body divider). - fn is_table_separator(&self) -> bool { - matches!(self, Row::HeaderDivider | Row::Separator(_)) - } - - /// PHP `$divider === $row`: identifies the internal header/body boundary marker. - fn is_divider(&self) -> bool { - matches!(self, Row::HeaderDivider) - } - - /// The row's cells, or an empty list for a separator (PHP `foreach` over a separator object, - /// which exposes no public properties, yields nothing). - fn cells(&self) -> Vec<Cell> { - match self { - Row::Cells(cells) => cells.clone(), - _ => Vec::new(), - } - } - - /// PHP `!$row`: an empty cell array is falsy; a separator is an object and thus truthy. - fn is_truthy(&self) -> bool { - match self { - Row::Cells(cells) => !cells.is_empty(), - _ => true, - } - } - - /// PHP `isset($row[$i])`: `false` for a missing or null cell. - fn get_cell_isset(&self, index: i64) -> Option<Cell> { - match self { - Row::Cells(cells) => match cells.get(index as usize) { - Some(Cell::Null) | None => None, - Some(cell) => Some(cell.clone()), - }, - _ => None, - } - } -} - -impl From<Vec<Cell>> for Row { - fn from(cells: Vec<Cell>) -> Self { - Row::Cells(cells) - } -} - -/// Bridges PHP-typed rows built elsewhere (still assembled as `PhpMixed`) onto the typed row. -impl From<PhpMixed> for Row { - fn from(value: PhpMixed) -> Self { - match value { - PhpMixed::List(items) => Row::Cells(items.into_iter().map(Cell::from).collect()), - PhpMixed::Array(map) => Row::Cells(map.into_values().map(Cell::from).collect()), - PhpMixed::Null => Row::Cells(Vec::new()), - other => Row::Cells(vec![Cell::from(other)]), - } - } -} - -/// A table or column style argument. PHP types it as `string|TableStyle`: either the name of a -/// registered style or a `TableStyle` instance. -#[derive(Debug, Clone)] -pub enum StyleName { - Name(String), - Style(TableStyle), -} - -impl From<&str> for StyleName { - fn from(name: &str) -> Self { - StyleName::Name(name.to_string()) - } -} - -impl From<String> for StyleName { - fn from(name: String) -> Self { - StyleName::Name(name) - } -} - -impl From<TableStyle> for StyleName { - fn from(style: TableStyle) -> Self { - StyleName::Style(style) - } -} - -/// PHP `$row[$index] = $value`: assigns at the integer key, growing the (dense) row with null -/// cells to reach `index` when necessary. -fn set_cell(row: &mut Vec<Cell>, index: i64, value: Cell) { - let index = index as usize; - while row.len() <= index { - row.push(Cell::Null); - } - row[index] = value; -} - -/// Provides helpers to display a table. -#[derive(Debug)] -pub struct Table { - header_title: Option<String>, - footer_title: Option<String>, - - /// Table headers. - headers: Vec<Row>, - - /// Table rows. - rows: Vec<Row>, - horizontal: bool, - - /// Column widths cache. - effective_column_widths: IndexMap<i64, i64>, - - /// Number of columns cache. - number_of_columns: Option<i64>, - - output: std::rc::Rc<std::cell::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 { - 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(StyleName::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) { - return Ok(Ok(style.clone())); - } - - Ok(Err(InvalidArgumentException::new(format!( - "Style \"{}\" is not defined.", - name - )))) - } - - /// Sets table style. - /// - /// `$name` is the style name or a TableStyle instance. - pub fn set_style( - &mut self, - name: StyleName, - ) -> 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: StyleName, - ) -> 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<Cell>) -> &mut Self { - // PHP wraps a flat list of cells into a single header row. (Multi-row headers, which PHP - // also accepts, are not used in this codebase and are not modeled by the typed API.) - self.headers = if headers.is_empty() { - Vec::new() - } else { - vec![Row::Cells(headers)] - }; - - self - } - - pub fn set_rows(&mut self, rows: Vec<Row>) -> &mut Self { - self.rows = Vec::new(); - - self.add_rows(rows) - } - - pub fn add_rows(&mut self, rows: Vec<Row>) -> &mut Self { - for row in rows { - self.add_row(row); - } - - self - } - - pub fn add_row(&mut self, row: Row) -> &mut Self { - // PHP `array_values($row)` reindexing is the identity on a positional cell vector, and the - // "row must be an array or a TableSeparator" check is now guaranteed by the type. - self.rows.push(row); - - self - } - - /// Adds a row to the table, and re-renders the table. - pub fn append_row(&mut self, row: Row) -> anyhow::Result<Result<&mut Self, RuntimeException>> { - if !Self::output_is_console_section(&self.output) { - return Ok(Err(RuntimeException::new(format!( - "Output should be an instance of \"{}\" when calling \"{}\".", - "Symfony\\Component\\Console\\Output\\ConsoleSectionOutput", - "Symfony\\Component\\Console\\Helper\\Table::appendRow", - )))); - } - - if self.rendered { - // TODO(phase-c): 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); - self.render(); - - Ok(Ok(self)) - } - - pub fn set_row(&mut self, column: i64, row: Vec<Cell>) -> &mut Self { - // PHP indexes $this->rows by arbitrary key; sparse assignment over a positional Vec is not - // modeled and has no callers. - let _ = (column, row); - // TODO(phase-c): sparse `$this->rows[$column] = $row` over a positional row vector. - todo!() - } - - 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 rows: Vec<Row> = if self.horizontal { - let mut horizontal_rows: IndexMap<i64, Vec<Cell>> = IndexMap::new(); - let header0 = self.headers.first().map(|h| h.cells()).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 row.is_table_separator() { - continue; - } - if let Some(cell) = row.get_cell_isset(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 c.is_table_cell() => c.colspan() >= 2, - _ => false, - }; - if is_title_noop { - // Noop, there is a "title" - } else { - let entry = horizontal_rows.get_mut(&i).unwrap(); - entry.push(Cell::Null); - } - } - } - } - horizontal_rows.into_values().map(Row::Cells).collect() - } else { - let mut merged = self.headers.clone(); - merged.push(Row::HeaderDivider); - merged.extend(self.rows.clone()); - 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 row.is_divider() { - is_header = false; - is_first_row = true; - - continue; - } - - if row.is_table_separator() { - self.render_row_separator(SEPARATOR_MID, None, None); - - continue; - } - - if !row.is_truthy() { - 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( - row.cells(), - self.style.get_cell_row_format(), - Some(self.style.get_cell_header_format()), - ); - } else { - self.render_row( - row.cells(), - 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 = (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<Cell>, 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: &[Cell], column: i64, cell_format: String) -> String { - let cell = row - .get(column as usize) - .cloned() - .unwrap_or(Cell::Value(String::new())); - let mut width = self.effective_column_widths[&column]; - if cell.is_table_cell() && cell.colspan() > 1 { - // add the width of the following columns(numbers of colspan). - for next_column in (column + 1)..=(column + cell.colspan() - 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 = cell.to_php_string(); - 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); - - if cell.is_table_separator() { - 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 cell.is_table_cell() && cell.style().is_some() { - let is_not_styled_by_tag = !Preg::is_match( - php_regex!("/^<(\\w+|(\\w+=[\\w,]+;?)*)>.+<\\/(\\w+|(\\w+=\\w+;?)*)?>$/"), - &cell_str, - ); - if is_not_styled_by_tag { - let cell_style = cell.style().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 = cell.style().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: &[Row]) { - let mut columns = vec![0i64]; - for row in rows { - if row.is_table_separator() { - continue; - } - - columns.push(self.get_number_of_columns(&row.cells())); - } - - self.number_of_columns = Some(*columns.iter().max().unwrap()); - } - - fn build_table_rows(&mut self, rows: Vec<Row>) -> TableRows { - let mut rows = rows; - let mut unmerged_rows: IndexMap<i64, IndexMap<i64, Vec<Cell>>> = 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 = rows[row_key as usize].cells(); - for (column, cell) in current.iter().enumerate() { - let column = column as i64; - let mut cell = cell.clone(); - let colspan = if cell.is_table_cell() { - cell.colspan() - } else { - 1 - }; - - if self.column_max_widths.contains_key(&column) - && Helper::width(&self.remove_decoration(&cell.to_php_string())) - > self.column_max_widths[&column] - { - let wrapped = self.format_and_wrap( - &cell.to_php_string(), - self.column_max_widths[&column] * colspan, - ); - cell = Cell::Value(wrapped); - } - let cell_str = cell.to_php_string(); - 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 cell.is_table_cell() { - Cell::Cell(TableCell::new2( - &escaped, - Self::table_cell_options_colspan(cell.colspan()), - )) - } else { - Cell::Value(escaped.clone()) - }; - let lines = shirabe_php_shim::explode( - eol, - &shirabe_php_shim::str_replace( - eol, - &format!("<fg=default;bg=default></>{}", eol), - &cell.to_php_string(), - ), - ); - for (line_key, line) in lines.into_iter().enumerate() { - let line_key = line_key as i64; - let mut line = Cell::Value(line); - if colspan > 1 { - line = Cell::Cell(TableCell::new2( - &line.to_php_string(), - Self::table_cell_options_colspan(colspan), - )); - } - if 0 == line_key { - let mut r = rows[row_key as usize].cells(); - set_cell(&mut r, column, line); - rows[row_key as usize] = Row::Cells(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(); - set_cell(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(). Here the row groups are - // precomputed eagerly to preserve behavior, then handed to TableRows. - let mut row_groups: Vec<Vec<Row>> = Vec::new(); - for (row_key, row) in rows.into_iter().enumerate() { - let row_key = row_key as i64; - let mut row_group: Vec<Row> = vec![if row.is_table_separator() { - row - } else { - Row::Cells(self.fill_cells(row.cells())) - }]; - - if let Some(extra) = unmerged_rows.get(&row_key) { - for r in extra.values() { - row_group.push(Row::Cells(self.fill_cells(r.clone()))); - } - } - 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(Row::Separator(TableSeparator::new())); - merged.extend(self.rows.clone()); - let mut number_of_rows = self.build_table_rows(merged).into_row_groups().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<Row>, line: i64) -> Vec<Row> { - let mut rows = rows; - let mut unmerged_rows: IndexMap<i64, IndexMap<i64, Cell>> = IndexMap::new(); - let current = rows[line as usize].cells(); - for (column, cell) in current.iter().enumerate() { - let column = column as i64; - let cell = cell.clone(); - // PHP validates here that a cell is null, a TableCell, a scalar, or a __toString object. - // The `Cell` type makes every variant valid, so the InvalidArgumentException is dead. - if cell.is_table_cell() && cell.rowspan() > 1 { - let mut nb_lines = cell.rowspan() - 1; - let cell_str = cell.to_php_string(); - 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(Cell::Value).collect(); - nb_lines = if (lines.len() as i64) > nb_lines { - shirabe_php_shim::substr_count(&cell_str, eol) - } else { - nb_lines - }; - - let mut r = rows[line as usize].cells(); - set_cell( - &mut r, - column, - Cell::Cell(TableCell::new2( - &lines[0].to_php_string(), - Self::table_cell_options_colspan_style(cell.colspan(), cell.style()), - )), - ); - rows[line as usize] = Row::Cells(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(Cell::Value(String::new())); - unmerged_rows.get_mut(&unmerged_row_key).unwrap().insert( - column, - Cell::Cell(TableCell::new2( - &value.to_php_string(), - Self::table_cell_options_colspan_style(cell.colspan(), cell.style()), - )), - ); - 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() - && matches!(rows[unmerged_row_key as usize], Row::Cells(_)) - && (self.get_number_of_columns(&rows[unmerged_row_key as usize].cells()) - + self.get_number_of_columns( - &unmerged_rows[&unmerged_row_key] - .values() - .cloned() - .collect::<Vec<_>>(), - ) - <= self.number_of_columns.unwrap()); - if fits { - let mut target = rows[unmerged_row_key as usize].cells(); - 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] = Row::Cells(target); - } else { - let mut row = self.copy_row(&rows, unmerged_row_key - 1); - for (column, cell) in &unmerged_row { - if cell.is_truthy() { - set_cell(&mut row, *column, unmerged_row[column].clone()); - } - } - shirabe_php_shim::array_splice( - &mut rows, - unmerged_row_key, - Some(0), - vec![Row::Cells(row)], - ); - } - } - - rows - } - - /// fill cells for a row that contains colspan > 1. - fn fill_cells(&self, row: Vec<Cell>) -> Vec<Cell> { - let mut new_row: Vec<Cell> = Vec::new(); - - for (column, cell) in row.iter().enumerate() { - let column = column as i64; - new_row.push(cell.clone()); - if cell.is_table_cell() && cell.colspan() > 1 { - for _position in (column + 1)..=(column + cell.colspan() - 1) { - // insert empty value at column position - new_row.push(Cell::Value(String::new())); - } - } - } - - if new_row.is_empty() { row } else { new_row } - } - - fn copy_row(&self, rows: &[Row], line: i64) -> Vec<Cell> { - let mut row = rows[line as usize].cells(); - for cell in &mut row { - let cell_value = cell.clone(); - *cell = Cell::Value(String::new()); - if cell_value.is_table_cell() { - *cell = Cell::Cell(TableCell::new2( - "", - Self::table_cell_options_colspan(cell_value.colspan()), - )); - } - } - - row - } - - /// Gets number of columns by row. - fn get_number_of_columns(&self, row: &[Cell]) -> i64 { - let mut columns = row.len() as i64; - for column in row { - columns += if column.is_table_cell() { - column.colspan() - 1 - } else { - 0 - }; - } - - columns - } - - /// Gets list of columns for the given row. - fn get_row_columns(&self, row: &[Cell]) -> 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 cell.is_table_cell() && cell.colspan() > 1 { - // exclude grouped columns. - let excluded: Vec<i64> = - ((cell_key + 1)..=(cell_key + cell.colspan() - 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 row.is_table_separator() { - continue; - } - - let mut row_arr = row.cells(); - for i in 0..row_arr.len() { - let cell = row_arr[i].clone(); - if cell.is_table_cell() { - let text_content = self.remove_decoration(&cell.to_php_string()); - let text_length = Helper::width(&text_content); - if text_length > 0 { - let content_columns = shirabe_php_shim::mb_str_split( - &text_content, - (text_length as f64 / cell.colspan() as f64).ceil() as i64, - ); - for (position, content) in content_columns.into_iter().enumerate() { - set_cell( - &mut row_arr, - i as i64 + position as i64, - Cell::Value(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: &[Cell], column: i64) -> i64 { - let mut cell_width = 0; - - if let Some(cell) = row.get(column as usize) { - cell_width = Helper::width(&self.remove_decoration(&cell.to_php_string())); - } - - 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: StyleName, - ) -> anyhow::Result<Result<TableStyle, InvalidArgumentException>> { - let name = match name { - StyleName::Style(style) => return Ok(Ok(style)), - StyleName::Name(name) => name, - }; - - let styles_guard = styles().lock().unwrap(); - if let Some(style) = styles_guard.as_ref().and_then(|s| s.get(&name)) { - return Ok(Ok(style.clone())); - } - - Ok(Err(InvalidArgumentException::new(format!( - "Style \"{}\" is not defined.", - name - )))) - } - - fn formatter_is_wrappable( - _output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> bool { - // PHP: $this->output->getFormatter() instanceof WrappableOutputFormatterInterface. - // The sole OutputFormatterInterface implementor in this port is OutputFormatter, which - // implements WrappableOutputFormatterInterface, so the instanceof check always holds. - true - } - - /// `setColumnMaxWidth` guarantees the formatter is a `WrappableOutputFormatterInterface`, and - /// `OutputFormatter` is the sole implementor in this port, so `instanceof` reduces to this - /// downcast. - fn format_and_wrap(&self, string: &str, width: i64) -> String { - let formatter = self.output.borrow().get_formatter(); - let mut formatter = formatter.borrow_mut(); - let formatter = formatter - .as_any_mut() - .downcast_mut::<OutputFormatter>() - .expect("formatter must be a WrappableOutputFormatterInterface"); - formatter - .format_and_wrap(Some(string), width) - .unwrap() - .unwrap_or_default() - } - - /// 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: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, - ) -> bool { - // PHP: $this->output instanceof ConsoleSectionOutput - let borrowed = output.borrow(); - (*borrowed) - .as_any() - .downcast_ref::<ConsoleSectionOutput>() - .is_some() - } - - fn table_cell_options_colspan(colspan: i64) -> IndexMap<String, TableCellOption> { - // PHP: ['colspan' => $colspan] - let mut options = IndexMap::new(); - options.insert("colspan".to_string(), TableCellOption::Int(colspan)); - options - } - - fn table_cell_options_colspan_style( - colspan: i64, - style: Option<std::rc::Rc<TableCellStyle>>, - ) -> IndexMap<String, TableCellOption> { - // PHP: ['colspan' => $colspan, 'style' => $style] - let mut options = IndexMap::new(); - options.insert("colspan".to_string(), TableCellOption::Int(colspan)); - options.insert( - "style".to_string(), - match style { - Some(style) => TableCellOption::Style(style), - None => TableCellOption::Null, - }, - ); - options - } -} 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 deleted file mode 100644 index c899e5f7..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table_cell.rs +++ /dev/null @@ -1,99 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/TableCell.php - -use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::console::helper::table_cell_style::TableCellStyle; -use indexmap::IndexMap; - -/// A `TableCell` option value: an integer span, a `TableCellStyle`, or null. -#[derive(Debug, Clone)] -pub enum TableCellOption { - Int(i64), - Style(std::rc::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::new(format!( - "The TableCell does not support the following options: '{}'.", - diff.join("', '"), - ))); - } - - if let Some(style) = options.get("style") - && !matches!(style, TableCellOption::Style(_)) - && !matches!(style, TableCellOption::Null) - { - return Err(InvalidArgumentException::new( - "The style option must be an instance of \"TableCellStyle\".".to_string(), - )); - } - - 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") - } - - /// 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<std::rc::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 deleted file mode 100644 index df24b404..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table_cell_style.rs +++ /dev/null @@ -1,114 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/TableCellStyle.php - -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::new(format!( - "The TableCellStyle does not support the following options: '{}'.", - diff.join("', '"), - ))); - } - - 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::new(format!( - "Wrong align value. Value must be following: '{}'.", - align_map_keys().join("', '"), - ))); - } - } - - 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. - 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 deleted file mode 100644 index 1fc14bbf..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table_rows.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/TableRows.php - -use crate::symfony::console::helper::table::Row; - -/// @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<Row>>, -} - -impl TableRows { - pub fn from_row_groups(row_groups: Vec<Vec<Row>>) -> Self { - Self { row_groups } - } - - pub fn get_iterator(&self) -> std::slice::Iter<'_, Vec<Row>> { - self.row_groups.iter() - } - - pub fn into_row_groups(self) -> Vec<Vec<Row>> { - self.row_groups - } -} - -impl<'a> IntoIterator for &'a TableRows { - type Item = &'a Vec<Row>; - type IntoIter = std::slice::Iter<'a, Vec<Row>>; - - fn into_iter(self) -> Self::IntoIter { - self.row_groups.iter() - } -} - -impl IntoIterator for TableRows { - type Item = Vec<Row>; - type IntoIter = std::vec::IntoIter<Vec<Row>>; - - 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 deleted file mode 100644 index 34626cc6..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table_separator.rs +++ /dev/null @@ -1,47 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/TableSeparator.php - -use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; -use crate::symfony::console::helper::table_cell::{TableCell, TableCellOption}; -use indexmap::IndexMap; - -/// Marks a row as being a separator. -#[derive(Debug, Clone)] -pub struct TableSeparator { - inner: TableCell, -} - -impl TableSeparator { - pub fn new() -> Self { - 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)?, - }) - } - - // PHP `TableSeparator extends TableCell`, so these inherited accessors remain available. - pub fn get_colspan(&self) -> i64 { - self.inner.get_colspan() - } - - pub fn get_rowspan(&self) -> i64 { - self.inner.get_rowspan() - } - - pub fn get_style( - &self, - ) -> Option<std::rc::Rc<crate::symfony::console::helper::table_cell_style::TableCellStyle>> - { - self.inner.get_style() - } -} - -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 deleted file mode 100644 index bbe899af..00000000 --- a/crates/shirabe-external-packages/src/symfony/console/helper/table_style.rs +++ /dev/null @@ -1,289 +0,0 @@ -//! ref: composer/vendor/symfony/console/Helper/TableStyle.php - -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, Clone)] -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::new( - "The padding char must not be empty.".to_string(), - ))); - } - - 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::new("Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH)." - .to_string()))); - } - - 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 - } -} |
