diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-12 01:01:43 +0900 |
| commit | 1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch) | |
| tree | 9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/formatter | |
| parent | ddf0a624145b618c05c2ee47414545b99b685f37 (diff) | |
| download | php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip | |
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/formatter')
7 files changed, 568 insertions, 61 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/mod.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/mod.rs index 0431601..716d3fb 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/mod.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/mod.rs @@ -1,7 +1,13 @@ pub mod output_formatter; pub mod output_formatter_interface; pub mod output_formatter_style; +pub mod output_formatter_style_interface; +pub mod output_formatter_style_stack; +pub mod wrappable_output_formatter_interface; pub use output_formatter::*; pub use output_formatter_interface::*; pub use output_formatter_style::*; +pub use output_formatter_style_interface::*; +pub use output_formatter_style_stack::*; +pub use wrappable_output_formatter_interface::*; diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs index f64361e..413f023 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter.rs @@ -1,83 +1,347 @@ +use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; +use crate::symfony::console::formatter::output_formatter_interface::OutputFormatterInterface; +use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle; +use crate::symfony::console::formatter::output_formatter_style_interface::OutputFormatterStyleInterface; +use crate::symfony::console::formatter::output_formatter_style_stack::OutputFormatterStyleStack; +use crate::symfony::console::formatter::wrappable_output_formatter_interface::WrappableOutputFormatterInterface; +use crate::symfony::string::b; + +/// Formatter class for console output. #[derive(Debug)] -pub struct OutputFormatter; +pub struct OutputFormatter { + decorated: bool, + styles: indexmap::IndexMap<String, Box<dyn OutputFormatterStyleInterface>>, + style_stack: OutputFormatterStyleStack, +} impl OutputFormatter { - pub fn new( - _decorated: bool, - _styles: indexmap::IndexMap< - String, - crate::symfony::console::formatter::OutputFormatterStyle, - >, - ) -> Self { - todo!() - } + /// Escapes "<" and ">" special chars in given text. + pub fn escape(text: &str) -> anyhow::Result<String> { + let text = shirabe_php_shim::preg_replace("/([^\\\\]|^)([<>])/", "$1\\\\$2", text) + .expect("preg_replace failed"); - pub fn format(&self, _message: &str) -> String { - todo!() + Ok(Self::escape_trailing_backslash(&text)) } - pub fn is_decorated(&self) -> bool { - todo!() + /// Escapes trailing "\" in given text. + pub fn escape_trailing_backslash(text: &str) -> String { + let mut text = text.to_string(); + if shirabe_php_shim::str_ends_with(&text, "\\") { + let len = shirabe_php_shim::strlen(&text); + text = shirabe_php_shim::rtrim(&text, Some("\\")); + text = shirabe_php_shim::str_replace("\0", "", &text); + text.push_str(&shirabe_php_shim::str_repeat( + "\0", + (len - shirabe_php_shim::strlen(&text)) as usize, + )); + } + + text } - pub fn set_decorated(&mut self, _decorated: bool) { - todo!() + /// Initializes console output formatter. + /// + /// `styles` is an array of "name => FormatterStyle" instances. + pub fn new( + decorated: bool, + styles: indexmap::IndexMap<String, Box<dyn OutputFormatterStyleInterface>>, + ) -> Self { + let mut this = Self { + decorated, + styles: indexmap::IndexMap::new(), + style_stack: OutputFormatterStyleStack::new(None), + }; + + this.set_style( + "error", + Box::new(OutputFormatterStyle::new( + Some("white"), + Some("red"), + vec![], + )), + ); + this.set_style( + "info", + Box::new(OutputFormatterStyle::new(Some("green"), None, vec![])), + ); + this.set_style( + "comment", + Box::new(OutputFormatterStyle::new(Some("yellow"), None, vec![])), + ); + this.set_style( + "question", + Box::new(OutputFormatterStyle::new( + Some("black"), + Some("cyan"), + vec![], + )), + ); + + for (name, style) in styles { + this.set_style(&name, style); + } + + this.style_stack = OutputFormatterStyleStack::new(None); + + this } - pub fn escape(_text: &str) -> String { - todo!() + pub fn get_style_stack(&self) -> &OutputFormatterStyleStack { + &self.style_stack } - pub fn escape_trailing_backslash(_text: &str) -> String { - todo!() + /// Tries to create new style instance from string. + fn create_style_from_string( + &self, + string: &str, + ) -> anyhow::Result<Option<Box<dyn OutputFormatterStyleInterface>>> { + if self.styles.contains_key(string) { + // PHP returns the shared style instance; ownership cannot be expressed without Clone + // on the trait object. + // TODO(human-review): returning a shared style here needs an Rc/Clone strategy in + // Phase C. + return Ok(Some(todo!())); + } + + let mut matches: Vec<Vec<String>> = vec![]; + if shirabe_php_shim::preg_match_all_set_order( + "/([^=]+)=([^;]+)(;|$)/", + string, + &mut matches, + )? == 0 + { + return Ok(None); + } + + let mut style = OutputFormatterStyle::new(None, None, vec![]); + for r#match in &matches { + let mut r#match: Vec<String> = r#match.clone(); + shirabe_php_shim::array_shift(&mut r#match); + r#match[0] = shirabe_php_shim::strtolower(&r#match[0]); + + if r#match[0] == "fg" { + style.set_foreground(Some(&shirabe_php_shim::strtolower(&r#match[1]))); + } else if r#match[0] == "bg" { + style.set_background(Some(&shirabe_php_shim::strtolower(&r#match[1]))); + } else if r#match[0] == "href" { + let url = shirabe_php_shim::preg_replace("{\\\\([<>])}", "$1", &r#match[1]) + .expect("preg_replace failed"); + style.set_href(&url); + } else if r#match[0] == "options" { + let mut options: Vec<Vec<String>> = vec![]; + shirabe_php_shim::preg_match_all_simple( + "([^,;]+)", + &shirabe_php_shim::strtolower(&r#match[1]), + &mut options, + )?; + let options = shirabe_php_shim::array_shift(&mut options).unwrap_or_default(); + for option in &options { + style.set_option(option); + } + } else { + return Ok(None); + } + } + + Ok(Some(Box::new(style))) } - pub fn set_style( + /// Applies current style from stack to text, if must be applied. + fn apply_current_style( &mut self, - _name: &str, - _style: crate::symfony::console::formatter::OutputFormatterStyle, - ) { - todo!() - } + text: &str, + current: &str, + width: i64, + current_line_length: &mut i64, + ) -> String { + if text.is_empty() { + return String::new(); + } - pub fn has_style(&self, _name: &str) -> bool { - todo!() + if width == 0 { + return if self.is_decorated() { + self.style_stack.get_current_mut().apply(text) + } else { + text.to_string() + }; + } + + let mut text = text.to_string(); + + if *current_line_length == 0 && !current.is_empty() { + text = shirabe_php_shim::ltrim(&text, None); + } + + let prefix; + if *current_line_length != 0 { + let i = width - *current_line_length; + prefix = format!("{}\n", shirabe_php_shim::substr(&text, 0, Some(i))); + text = shirabe_php_shim::substr(&text, i, None); + } else { + prefix = String::new(); + } + + let mut matches: Vec<Option<String>> = vec![]; + shirabe_php_shim::preg_match("~(\\n)$~", &text, &mut matches); + text = format!("{}{}", prefix, self.add_line_breaks(&text, width)); + let trailing = matches.get(1).and_then(|m| m.clone()).unwrap_or_default(); + text = format!("{}{}", shirabe_php_shim::rtrim(&text, Some("\n")), trailing); + + if *current_line_length == 0 + && !current.is_empty() + && shirabe_php_shim::substr(current, -1, None) != "\n" + { + text = format!("\n{text}"); + } + + let mut lines = shirabe_php_shim::explode("\n", &text); + + for line in &lines { + *current_line_length += shirabe_php_shim::strlen(line); + if width <= *current_line_length { + *current_line_length = 0; + } + } + + if self.is_decorated() { + for line in lines.iter_mut() { + *line = self.style_stack.get_current_mut().apply(line); + } + } + + shirabe_php_shim::implode("\n", &lines) } - pub fn get_style( - &self, - _name: &str, - ) -> crate::symfony::console::formatter::OutputFormatterStyle { - todo!() + fn add_line_breaks(&self, text: &str, width: i64) -> String { + let encoding = shirabe_php_shim::mb_detect_encoding(text, None, true) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| "UTF-8".to_string()); + + b(text) + .to_code_point_string(&encoding) + .wordwrap(width, "\n", true) + .to_byte_string(&encoding) } } -impl crate::symfony::console::formatter::OutputFormatterInterface for OutputFormatter { - fn is_decorated(&self) -> bool { - self.is_decorated() +impl OutputFormatterInterface for OutputFormatter { + fn set_decorated(&mut self, decorated: bool) { + self.decorated = decorated; } - fn set_decorated(&mut self, decorated: bool) { - self.set_decorated(decorated) + fn is_decorated(&self) -> bool { + self.decorated } - fn set_style( - &mut self, - name: &str, - style: crate::symfony::console::formatter::OutputFormatterStyle, - ) { - self.set_style(name, style) + fn set_style(&mut self, name: &str, style: Box<dyn OutputFormatterStyleInterface>) { + self.styles + .insert(shirabe_php_shim::strtolower(name), style); } fn has_style(&self, name: &str) -> bool { - self.has_style(name) + self.styles + .contains_key(&shirabe_php_shim::strtolower(name)) } - fn get_style(&self, name: &str) -> crate::symfony::console::formatter::OutputFormatterStyle { - self.get_style(name) + fn get_style(&self, name: &str) -> anyhow::Result<Box<dyn OutputFormatterStyleInterface>> { + if !self.has_style(name) { + return Err(anyhow::anyhow!(InvalidArgumentException( + shirabe_php_shim::InvalidArgumentException { + message: shirabe_php_shim::sprintf( + "Undefined style: \"%s\".", + &[shirabe_php_shim::PhpMixed::String(name.to_string())], + ), + code: 0, + }, + ))); + } + + // PHP returns the shared style instance; ownership cannot be expressed without Clone on + // the trait object. + // TODO(human-review): returning a shared style here needs an Rc/Clone strategy in Phase C. + let _ = &self.styles[&shirabe_php_shim::strtolower(name)]; + todo!() } - fn format(&self, message: &str) -> String { - self.format(message) + fn format(&mut self, message: Option<&str>) -> anyhow::Result<Option<String>> { + self.format_and_wrap(message, 0) + } +} + +impl WrappableOutputFormatterInterface for OutputFormatter { + fn format_and_wrap( + &mut self, + message: Option<&str>, + width: i64, + ) -> anyhow::Result<Option<String>> { + let message = match message { + None => return Ok(Some(String::new())), + Some(message) => message, + }; + + let mut offset: i64 = 0; + let mut output = String::new(); + let open_tag_regex = "[a-z](?:[^\\\\<>]*+ | \\\\.)*"; + let close_tag_regex = "[a-z][^<>]*+"; + let mut current_line_length: i64 = 0; + let mut matches: shirabe_php_shim::PregOffsetCaptureMatches = Default::default(); + shirabe_php_shim::preg_match_all_offset_capture( + &format!("#<(({open_tag_regex}) | /({close_tag_regex})?)>#ix"), + message, + &mut matches, + )?; + let count = matches.group(0).len(); + for i in 0..count { + let (text, pos) = matches.group(0)[i].clone(); + let pos = pos as i64; + + if pos != 0 && shirabe_php_shim::byte_at(message, (pos - 1) as usize) == b'\\' { + continue; + } + + // add the text up to the next tag + let segment = shirabe_php_shim::substr(message, offset, Some(pos - offset)); + let applied = + self.apply_current_style(&segment, &output, width, &mut current_line_length); + output.push_str(&applied); + offset = pos + shirabe_php_shim::strlen(&text); + + // opening tag? + let open = shirabe_php_shim::byte_at(&text, 1) != b'/'; + let tag = if open { + matches.group(1)[i].0.clone() + } else { + matches + .group(3) + .get(i) + .map(|m| m.0.clone()) + .unwrap_or_default() + }; + + if !open && tag.is_empty() { + // </> + self.style_stack.pop(None)?.ok(); + } else if let Some(style) = self.create_style_from_string(&tag)? { + if open { + self.style_stack.push(style); + } else { + self.style_stack.pop(Some(style))?.ok(); + } + } else { + let applied = + self.apply_current_style(&text, &output, width, &mut current_line_length); + output.push_str(&applied); + } + } + + let segment = shirabe_php_shim::substr(message, offset, None); + let applied = self.apply_current_style(&segment, &output, width, &mut current_line_length); + output.push_str(&applied); + + let mut pairs = indexmap::IndexMap::new(); + pairs.insert("\0".to_string(), "\\".to_string()); + pairs.insert("\\<".to_string(), "<".to_string()); + pairs.insert("\\>".to_string(), ">".to_string()); + Ok(Some(shirabe_php_shim::strtr_array(&output, &pairs))) } } diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs index 7f6e9b0..03d728c 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_interface.rs @@ -1,10 +1,24 @@ -use crate::symfony::console::formatter::OutputFormatterStyle; +use crate::symfony::console::formatter::output_formatter_style_interface::OutputFormatterStyleInterface; +/// Formatter interface for console output. pub trait OutputFormatterInterface { - fn is_decorated(&self) -> bool; + /// Sets the decorated flag. fn set_decorated(&mut self, decorated: bool); - fn set_style(&mut self, name: &str, style: OutputFormatterStyle); + + /// Whether the output will decorate messages. + fn is_decorated(&self) -> bool; + + /// Sets a new style. + fn set_style(&mut self, name: &str, style: Box<dyn OutputFormatterStyleInterface>); + + /// Checks if output formatter has style with specified name. fn has_style(&self, name: &str) -> bool; - fn get_style(&self, name: &str) -> OutputFormatterStyle; - fn format(&self, message: &str) -> String; + + /// Gets style options from style with specified name. + /// + /// Throws InvalidArgumentException when style isn't defined. + fn get_style(&self, name: &str) -> anyhow::Result<Box<dyn OutputFormatterStyleInterface>>; + + /// Formats a message according to the given styles. + fn format(&mut self, message: Option<&str>) -> anyhow::Result<Option<String>>; } diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs index 6299157..835bcd6 100644 --- a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style.rs @@ -1,12 +1,96 @@ +use crate::symfony::console::color::Color; +use crate::symfony::console::formatter::output_formatter_style_interface::OutputFormatterStyleInterface; + +/// Formatter style class for defining styles. #[derive(Debug)] -pub struct OutputFormatterStyle; +pub struct OutputFormatterStyle { + color: Color, + foreground: String, + background: String, + options: Vec<String>, + href: Option<String>, + handles_href_gracefully: Option<bool>, +} impl OutputFormatterStyle { - pub fn new( - _foreground: Option<&str>, - _background: Option<&str>, - _options: Option<Vec<String>>, - ) -> Self { - todo!() + /// Initializes output formatter style. + pub fn new(foreground: Option<&str>, background: Option<&str>, options: Vec<String>) -> Self { + let foreground = foreground + .filter(|s| !s.is_empty()) + .unwrap_or("") + .to_string(); + let background = background + .filter(|s| !s.is_empty()) + .unwrap_or("") + .to_string(); + let color = Color::new(&foreground, &background, &options.clone()).unwrap(); + Self { + color, + foreground, + background, + options, + href: None, + handles_href_gracefully: None, + } + } + + pub fn set_href(&mut self, url: &str) { + self.href = Some(url.to_string()); + } +} + +impl OutputFormatterStyleInterface for OutputFormatterStyle { + fn set_foreground(&mut self, color: Option<&str>) { + self.foreground = color.filter(|s| !s.is_empty()).unwrap_or("").to_string(); + self.color = Color::new(&self.foreground, &self.background, &self.options.clone()).unwrap(); + } + + fn set_background(&mut self, color: Option<&str>) { + self.background = color.filter(|s| !s.is_empty()).unwrap_or("").to_string(); + self.color = Color::new(&self.foreground, &self.background, &self.options.clone()).unwrap(); + } + + fn set_option(&mut self, option: &str) { + self.options.push(option.to_string()); + self.color = Color::new(&self.foreground, &self.background, &self.options.clone()).unwrap(); + } + + fn unset_option(&mut self, option: &str) { + let pos = shirabe_php_shim::array_search_in_vec(option, &self.options); + if let Some(pos) = pos { + self.options.remove(pos); + } + + self.color = Color::new(&self.foreground, &self.background, &self.options.clone()).unwrap(); + } + + fn set_options(&mut self, options: Vec<String>) { + self.options = options; + self.color = Color::new(&self.foreground, &self.background, &self.options.clone()).unwrap(); + } + + fn apply(&mut self, text: &str) -> String { + let mut text = text.to_string(); + + if self.handles_href_gracefully.is_none() { + self.handles_href_gracefully = Some( + shirabe_php_shim::getenv("TERMINAL_EMULATOR").as_deref() + != Some("JetBrains-JediTerm") + && (shirabe_php_shim::getenv("KONSOLE_VERSION").is_none_or(|v| v.is_empty()) + || shirabe_php_shim::getenv("KONSOLE_VERSION") + .map(|v| v.parse::<i64>().unwrap_or(0)) + .unwrap_or(0) + > 201100) + && !shirabe_php_shim::server_contains_key("IDEA_INITIAL_DIRECTORY"), + ); + } + + if let Some(href) = &self.href { + if self.handles_href_gracefully == Some(true) { + text = format!("\x1b]8;;{href}\x1b\\{text}\x1b]8;;\x1b\\"); + } + } + + self.color.apply(&text) } } diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_interface.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_interface.rs new file mode 100644 index 0000000..c9a2648 --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_interface.rs @@ -0,0 +1,20 @@ +/// Formatter style interface for defining styles. +pub trait OutputFormatterStyleInterface: std::fmt::Debug { + /// Sets style foreground color. + fn set_foreground(&mut self, color: Option<&str>); + + /// Sets style background color. + fn set_background(&mut self, color: Option<&str>); + + /// Sets some specific style option. + fn set_option(&mut self, option: &str); + + /// Unsets some specific style option. + fn unset_option(&mut self, option: &str); + + /// Sets multiple style options at once. + fn set_options(&mut self, options: Vec<String>); + + /// Applies the style to a given text. + fn apply(&mut self, text: &str) -> String; +} diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs new file mode 100644 index 0000000..2b290db --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/output_formatter_style_stack.rs @@ -0,0 +1,108 @@ +use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; +use crate::symfony::console::formatter::output_formatter_style::OutputFormatterStyle; +use crate::symfony::console::formatter::output_formatter_style_interface::OutputFormatterStyleInterface; +use crate::symfony::contracts::service::reset_interface::ResetInterface; + +#[derive(Debug)] +pub struct OutputFormatterStyleStack { + styles: Vec<Box<dyn OutputFormatterStyleInterface>>, + empty_style: Box<dyn OutputFormatterStyleInterface>, +} + +impl OutputFormatterStyleStack { + pub fn new(empty_style: Option<Box<dyn OutputFormatterStyleInterface>>) -> Self { + let empty_style = + empty_style.unwrap_or_else(|| Box::new(OutputFormatterStyle::new(None, None, vec![]))); + let mut this = Self { + styles: vec![], + empty_style, + }; + this.reset(); + this + } + + /// Pushes a style in the stack. + pub fn push(&mut self, style: Box<dyn OutputFormatterStyleInterface>) { + self.styles.push(style); + } + + /// Pops a style from the stack. + /// + /// Throws InvalidArgumentException when style tags incorrectly nested. + pub fn pop( + &mut self, + mut style: Option<Box<dyn OutputFormatterStyleInterface>>, + ) -> anyhow::Result<Result<Box<dyn OutputFormatterStyleInterface>, InvalidArgumentException>> + { + if self.styles.is_empty() { + // Returns the shared empty style; ownership cannot be expressed without Clone on the + // trait object. + // TODO(human-review): empty-style branch needs a Clone/Rc strategy in Phase C. + return Ok(Ok(todo!())); + } + + let style = match style.as_mut() { + None => { + return Ok(Ok(shirabe_php_shim::array_pop(&mut self.styles).unwrap())); + } + Some(style) => style, + }; + + for index in (0..self.styles.len()).rev() { + if style.apply("") == self.styles[index].apply("") { + // PHP: array_slice($this->styles, 0, $index) keeps elements before $index, + // dropping the matched element and everything after it. + let stacked_style = self.styles.remove(index); + self.styles.truncate(index); + + return Ok(Ok(stacked_style)); + } + } + + Ok(Err(InvalidArgumentException( + shirabe_php_shim::InvalidArgumentException { + message: "Incorrectly nested style tag found.".to_string(), + code: 0, + }, + ))) + } + + /// Computes current style with stacks top codes. + pub fn get_current(&self) -> &dyn OutputFormatterStyleInterface { + if self.styles.is_empty() { + return self.empty_style.as_ref(); + } + + self.styles[self.styles.len() - 1].as_ref() + } + + /// Mutable variant of `get_current`, needed because `apply` lazily mutates style state. + pub fn get_current_mut(&mut self) -> &mut dyn OutputFormatterStyleInterface { + if self.styles.is_empty() { + return self.empty_style.as_mut(); + } + + let last = self.styles.len() - 1; + self.styles[last].as_mut() + } + + pub fn set_empty_style( + &mut self, + empty_style: Box<dyn OutputFormatterStyleInterface>, + ) -> &mut Self { + self.empty_style = empty_style; + + self + } + + pub fn get_empty_style(&self) -> &dyn OutputFormatterStyleInterface { + self.empty_style.as_ref() + } +} + +impl ResetInterface for OutputFormatterStyleStack { + /// Resets stack (ie. empty internal arrays). + fn reset(&mut self) { + self.styles = vec![]; + } +} diff --git a/crates/shirabe-external-packages/src/symfony/console/formatter/wrappable_output_formatter_interface.rs b/crates/shirabe-external-packages/src/symfony/console/formatter/wrappable_output_formatter_interface.rs new file mode 100644 index 0000000..be8b466 --- /dev/null +++ b/crates/shirabe-external-packages/src/symfony/console/formatter/wrappable_output_formatter_interface.rs @@ -0,0 +1,11 @@ +use crate::symfony::console::formatter::output_formatter_interface::OutputFormatterInterface; + +/// Formatter interface for console output that supports word wrapping. +pub trait WrappableOutputFormatterInterface: OutputFormatterInterface { + /// Formats a message according to the given styles, wrapping at `width` (0 means no wrapping). + fn format_and_wrap( + &mut self, + message: Option<&str>, + width: i64, + ) -> anyhow::Result<Option<String>>; +} |
