diff options
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs')
| -rw-r--r-- | crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs | 752 |
1 files changed, 699 insertions, 53 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs index c26ca30..72579fc 100644 --- a/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs +++ b/crates/shirabe-external-packages/src/symfony/console/style/symfony_style.rs @@ -1,113 +1,759 @@ +use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException; +use crate::symfony::console::exception::runtime_exception::RuntimeException; +use crate::symfony::console::formatter::OutputFormatter; +use crate::symfony::console::formatter::OutputFormatterInterface; +use crate::symfony::console::helper::Helper; +use crate::symfony::console::helper::ProgressBar; +use crate::symfony::console::helper::SymfonyQuestionHelper; +use crate::symfony::console::helper::Table; +use crate::symfony::console::helper::TableCell; +use crate::symfony::console::helper::TableSeparator; use crate::symfony::console::input::InputInterface; +use crate::symfony::console::output::ConsoleOutputInterface; use crate::symfony::console::output::OutputInterface; +use crate::symfony::console::output::TrimmedBufferOutput; +use crate::symfony::console::output::output_interface::OUTPUT_NORMAL; +use crate::symfony::console::question::ChoiceQuestion; +use crate::symfony::console::question::ConfirmationQuestion; +use crate::symfony::console::question::Question; +use crate::symfony::console::style::output_style::OutputStyle; +use crate::symfony::console::style::style_interface::StyleInterface; +use crate::symfony::console::terminal::Terminal; use shirabe_php_shim::PhpMixed; +use std::cell::RefCell; +use std::rc::Rc; +/// Output decorator helpers for the Symfony Style Guide. #[derive(Debug)] -pub struct SymfonyStyle; +pub struct SymfonyStyle { + inner: OutputStyle, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, + question_helper: Option<SymfonyQuestionHelper>, + progress_bar: Option<ProgressBar>, + line_length: i64, + buffered_output: TrimmedBufferOutput, +} + +pub const MAX_LINE_LENGTH: i64 = 120; impl SymfonyStyle { pub fn new( - _input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>, - _output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>, + input: Rc<RefCell<dyn InputInterface>>, + output: Rc<RefCell<dyn OutputInterface>>, ) -> Self { - todo!() + let buffered_output = TrimmedBufferOutput::new( + if std::path::MAIN_SEPARATOR == '\\' { + 4 + } else { + 2 + }, + Some(output.borrow().get_verbosity()), + false, + // TODO(plugin): clone of the formatter; PHP `clone $output->getFormatter()`. + Some(output.borrow().get_formatter()), + ) + .unwrap(); + // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. + let width = { + let w = Terminal::new().get_width(); + if w != 0 { w } else { MAX_LINE_LENGTH } + }; + let line_length = shirabe_php_shim::min( + width - (std::path::MAIN_SEPARATOR == '\\') as i64, + MAX_LINE_LENGTH, + ); + + let inner = OutputStyle::new(output.clone()); + + Self { + inner, + input, + output, + question_helper: None, + progress_bar: None, + line_length, + buffered_output, + } } - pub fn title(&mut self, _message: &str) { - todo!() + /// Formats a message as a block of text. + pub fn block( + &mut self, + messages: PhpMixed, + r#type: Option<&str>, + style: Option<&str>, + prefix: &str, + padding: bool, + escape: bool, + ) { + let messages: Vec<PhpMixed> = if shirabe_php_shim::is_array(&messages) { + // array_values($messages) + todo!() + } else { + vec![messages] + }; + + self.auto_prepend_block(); + let block = self.create_block(messages, r#type, style, prefix, padding, escape); + self.writeln( + PhpMixed::List( + block + .into_iter() + .map(|line| Box::new(PhpMixed::String(line))) + .collect(), + ), + OUTPUT_NORMAL, + ); + self.new_line(1); } - pub fn section(&mut self, _message: &str) { - todo!() + /// Formats a command comment. + pub fn comment(&mut self, message: PhpMixed) { + self.block( + message, + None, + None, + "<fg=default;bg=default> // </>", + false, + false, + ); } - pub fn text(&mut self, _message: &str) { - todo!() + /// Formats an info message. + pub fn info(&mut self, message: PhpMixed) { + self.block(message, Some("INFO"), Some("fg=green"), " ", true, true); } - pub fn comment(&mut self, _message: &str) { - todo!() + /// Formats a horizontal table. + pub fn horizontal_table(&mut self, headers: Vec<PhpMixed>, rows: Vec<PhpMixed>) { + self.create_table() + .set_horizontal(true) + .set_headers(headers) + .set_rows(rows) + .render(); + + self.new_line(1); } - pub fn success(&mut self, _message: PhpMixed) { - todo!() + /// Formats a list of key/value horizontally. + /// + /// Each row can be one of: + /// * 'A title' + /// * ['key' => 'value'] + /// * new TableSeparator() + pub fn definition_list(&mut self, list: Vec<PhpMixed>) { + let mut headers: Vec<PhpMixed> = Vec::new(); + let mut row: Vec<PhpMixed> = Vec::new(); + for value in list { + if Self::is_table_separator(&value) { + headers.push(value.clone()); + row.push(value); + continue; + } + if shirabe_php_shim::is_string(&value) { + // TODO: store a `TableCell` (with colspan => 2) into the mixed array. + let _table_cell = TableCell::new(&Self::php_string(&value), { + let mut options = indexmap::IndexMap::new(); + options.insert( + "colspan".to_string(), + crate::symfony::console::helper::table_cell::TableCellOption::Int(2), + ); + options + }); + let _ = _table_cell; + todo!(); + } + if !shirabe_php_shim::is_array(&value) { + // TODO(plugin): recoverable error path. + let _ = InvalidArgumentException(shirabe_php_shim::InvalidArgumentException { + message: "Value should be an array, string, or an instance of TableSeparator." + .to_string(), + code: 0, + }); + todo!() + } + // $headers[] = key($value); $row[] = current($value); + let _ = shirabe_php_shim::key(value.clone()); + headers.push(todo!()); + row.push(shirabe_php_shim::current(value)); + } + + self.horizontal_table( + headers, + vec![PhpMixed::List(row.into_iter().map(Box::new).collect())], + ); } - pub fn error(&mut self, _message: PhpMixed) { + pub fn progress_iterate( + &mut self, + _iterable: Vec<PhpMixed>, + _max: Option<i64>, + ) -> Vec<PhpMixed> { + // yield from $this->createProgressBar()->iterate($iterable, $max); + // $this->newLine(2); todo!() } - pub fn warning(&mut self, _message: PhpMixed) { - todo!() + pub fn ask_question(&mut self, question: Question) -> PhpMixed { + if self.input.borrow().is_interactive() { + self.auto_prepend_block(); + } + + if self.question_helper.is_none() { + self.question_helper = Some(SymfonyQuestionHelper::new()); + } + + // TODO(plugin): pass `self` as the OutputInterface to the question helper. + let answer = { + let input = self.input.clone(); + let mut input = input.borrow_mut(); + self.question_helper + .as_mut() + .unwrap() + .ask(&mut *input, self.output.clone(), &question) + }; + // PHP `askQuestion` returns the answer directly; exceptions propagate. Phase B + // collapses the double `Result` by panicking on either error. + let answer = answer + .expect("question helper error") + .expect("missing input"); + + if self.input.borrow().is_interactive() { + self.new_line(1); + self.buffered_output + .write(&["\n".to_string()], false, OUTPUT_NORMAL); + } + + answer } - pub fn note(&mut self, _message: PhpMixed) { - todo!() + /// Returns a new instance which makes use of stderr if available. + pub fn get_error_style(&self) -> Self { + Self::new(self.input.clone(), self.inner.get_error_output()) } - pub fn listing(&mut self, _elements: &[String]) { - todo!() + pub fn create_table(&mut self) -> Table { + // TODO(plugin): ConsoleOutputInterface::section() requires runtime type info. + let output = if Self::is_console_output_interface(&self.output) { + Self::as_console_output_interface(&self.output) + .unwrap() + .borrow() + .section() as Rc<RefCell<dyn OutputInterface>> + } else { + self.output.clone() + }; + let mut style = Table::get_style_definition("symfony-style-guide".to_string()) + .expect("style definition lookup") + .expect("undefined style definition"); + style.set_cell_header_format("<info>%s</info>".to_string()); + + let mut table = Table::new(output); + // PHP passes the cloned `TableStyle` instance directly; `set_style` here takes a + // `PhpMixed` name/style. Phase B leaves the polymorphic style passing as a TODO. + let _ = &style; + let _ = table.set_style(PhpMixed::String("symfony-style-guide".to_string())); + table } - pub fn new_line(&mut self, _count: i64) { - todo!() + pub fn create_progress_bar(&self, max: i64) -> ProgressBar { + let mut progress_bar = self.inner.create_progress_bar(max); + + if std::path::MAIN_SEPARATOR != '\\' + || shirabe_php_shim::getenv("TERM_PROGRAM").as_deref() == Some("Hyper") + { + progress_bar.set_empty_bar_character("░"); // light shade character ░ + progress_bar.set_progress_character(""); + progress_bar.set_bar_character("▓"); // dark shade character ▓ + } + + progress_bar } - pub fn ask( + fn get_progress_bar(&mut self) -> &mut ProgressBar { + if self.progress_bar.is_none() { + // TODO(plugin): recoverable error path. + let _ = RuntimeException(shirabe_php_shim::RuntimeException { + message: "The ProgressBar is not started.".to_string(), + code: 0, + }); + todo!() + } + + self.progress_bar.as_mut().unwrap() + } + + fn auto_prepend_block(&mut self) { + let chars = shirabe_php_shim::substr( + &shirabe_php_shim::str_replace( + shirabe_php_shim::PHP_EOL, + "\n", + &self.buffered_output.fetch(), + ), + -2, + None, + ); + + if chars.is_empty() { + self.new_line(1); // empty history, so we should start with a new line. + + return; + } + // Prepend new line for each non LF chars (This means no blank line was output before) + self.new_line(2 - shirabe_php_shim::substr_count(&chars, "\n")); + } + + fn auto_prepend_text(&mut self) { + let fetched = self.buffered_output.fetch(); + // Prepend new line if last char isn't EOL: + if !shirabe_php_shim::str_ends_with(&fetched, "\n") { + self.new_line(1); + } + } + + fn write_buffer(&mut self, message: &str, new_line: bool, r#type: i64) { + // We need to know if the last chars are PHP_EOL + self.buffered_output + .write(&[message.to_string()], new_line, r#type); + } + + fn create_block( &mut self, - _question: &str, - _default: Option<&str>, - _validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>, - ) -> PhpMixed { + messages: Vec<PhpMixed>, + r#type: Option<&str>, + style: Option<&str>, + prefix: &str, + padding: bool, + escape: bool, + ) -> Vec<String> { + let mut indent_length: i64 = 0; + let prefix_length = Helper::width(&Helper::remove_decoration( + &mut *self.get_formatter().borrow_mut(), + prefix, + )); + let mut lines: Vec<String> = Vec::new(); + + let mut r#type = r#type.map(|t| t.to_string()); + let mut line_indentation = String::new(); + if let Some(t) = &r#type { + let formatted = shirabe_php_shim::sprintf("[%s] ", &[PhpMixed::String(t.clone())]); + indent_length = shirabe_php_shim::strlen(&formatted); + line_indentation = shirabe_php_shim::str_repeat(" ", indent_length as usize); + r#type = Some(formatted); + } + + let messages_count = messages.len() as i64; + // wrap and add newlines for each element + for (key, message) in messages.into_iter().enumerate() { + let key = key as i64; + let mut message = Self::php_string(&message); + if escape { + message = OutputFormatter::escape(&message).unwrap(); + } + + let decoration_length = Helper::width(&message) + - Helper::width(&Helper::remove_decoration( + &mut *self.get_formatter().borrow_mut(), + &message, + )); + let message_line_length = shirabe_php_shim::min( + self.line_length - prefix_length - indent_length + decoration_length, + self.line_length, + ); + let message_lines = shirabe_php_shim::explode( + shirabe_php_shim::PHP_EOL, + &shirabe_php_shim::wordwrap( + &message, + message_line_length, + shirabe_php_shim::PHP_EOL, + true, + ), + ); + for message_line in message_lines { + lines.push(message_line); + } + + if messages_count > 1 && key < messages_count - 1 { + lines.push(String::new()); + } + } + + let mut first_line_index: i64 = 0; + if padding && self.inner.is_decorated() { + first_line_index = 1; + shirabe_php_shim::array_unshift(&mut lines, String::new()); + lines.push(String::new()); + } + + for (i, line) in lines.iter_mut().enumerate() { + let i = i as i64; + if let Some(t) = &r#type { + *line = if first_line_index == i { + format!("{}{}", t, line) + } else { + format!("{}{}", line_indentation, line) + }; + } + + *line = format!("{}{}", prefix, line); + line.push_str(&shirabe_php_shim::str_repeat( + " ", + shirabe_php_shim::max( + self.line_length + - Helper::width(&Helper::remove_decoration( + &mut *self.output.borrow().get_formatter().borrow_mut(), + line, + )), + 0, + ) as usize, + )); + + if let Some(style) = style { + *line = shirabe_php_shim::sprintf( + "<%s>%s</>", + &[ + PhpMixed::String(style.to_string()), + PhpMixed::String(line.clone()), + ], + ); + } + } + + lines + } + + fn get_formatter(&self) -> Rc<RefCell<dyn OutputFormatterInterface>> { + self.output.borrow().get_formatter() + } + + fn is_console_output_interface(_output: &Rc<RefCell<dyn OutputInterface>>) -> bool { todo!() } - pub fn ask_hidden( - &mut self, - _question: &str, - _validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>, - ) -> PhpMixed { + fn as_console_output_interface( + _output: &Rc<RefCell<dyn OutputInterface>>, + ) -> Option<Rc<RefCell<dyn ConsoleOutputInterface>>> { todo!() } - pub fn confirm(&mut self, _question: &str, _default: bool) -> bool { + fn is_table_separator(_value: &PhpMixed) -> bool { todo!() } - pub fn choice( + fn php_string(_value: &PhpMixed) -> String { + todo!() + } + + /// Bridges the `StyleInterface` validator (which yields `anyhow::Error`) to the + /// `Question::set_validator` validator (which yields `InvalidArgumentException`) by + /// converting any error into an `InvalidArgumentException` carrying its message. + #[allow(clippy::type_complexity)] + fn adapt_validator( + validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>, + ) -> Option<Box<dyn Fn(Option<PhpMixed>) -> Result<PhpMixed, InvalidArgumentException>>> { + validator.map(|validator| { + Box::new(move |value: Option<PhpMixed>| { + validator(value).map_err(|e| { + InvalidArgumentException(shirabe_php_shim::InvalidArgumentException { + message: e.to_string(), + code: 0, + }) + }) + }) + as Box<dyn Fn(Option<PhpMixed>) -> Result<PhpMixed, InvalidArgumentException>> + }) + } +} + +impl SymfonyStyle { + /// {@inheritdoc} + pub fn writeln(&mut self, messages: PhpMixed, r#type: i64) { + let messages: Vec<PhpMixed> = if !shirabe_php_shim::is_iterable(&messages) { + vec![messages] + } else { + // iterate over the iterable + todo!() + }; + + for message in messages { + let message = Self::php_string(&message); + self.inner.writeln(&[message.clone()], r#type); + self.write_buffer(&message, true, r#type); + } + } + + /// {@inheritdoc} + pub fn write(&mut self, messages: PhpMixed, newline: bool, r#type: i64) { + let messages: Vec<PhpMixed> = if !shirabe_php_shim::is_iterable(&messages) { + vec![messages] + } else { + // iterate over the iterable + todo!() + }; + + for message in messages { + let message = Self::php_string(&message); + self.inner.write(&[message.clone()], newline, r#type); + self.write_buffer(&message, newline, r#type); + } + } +} + +impl StyleInterface for SymfonyStyle { + /// {@inheritdoc} + fn title(&mut self, message: &str) { + self.auto_prepend_block(); + self.writeln( + PhpMixed::List(vec![ + Box::new(PhpMixed::String(shirabe_php_shim::sprintf( + "<comment>%s</>", + &[PhpMixed::String( + OutputFormatter::escape_trailing_backslash(message), + )], + ))), + Box::new(PhpMixed::String(shirabe_php_shim::sprintf( + "<comment>%s</>", + &[PhpMixed::String(shirabe_php_shim::str_repeat( + "=", + Helper::width(&Helper::remove_decoration( + &mut *self.get_formatter().borrow_mut(), + message, + )) as usize, + ))], + ))), + ]), + OUTPUT_NORMAL, + ); + self.new_line(1); + } + + /// {@inheritdoc} + fn section(&mut self, message: &str) { + self.auto_prepend_block(); + self.writeln( + PhpMixed::List(vec![ + Box::new(PhpMixed::String(shirabe_php_shim::sprintf( + "<comment>%s</>", + &[PhpMixed::String( + OutputFormatter::escape_trailing_backslash(message), + )], + ))), + Box::new(PhpMixed::String(shirabe_php_shim::sprintf( + "<comment>%s</>", + &[PhpMixed::String(shirabe_php_shim::str_repeat( + "-", + Helper::width(&Helper::remove_decoration( + &mut *self.get_formatter().borrow_mut(), + message, + )) as usize, + ))], + ))), + ]), + OUTPUT_NORMAL, + ); + self.new_line(1); + } + + /// {@inheritdoc} + fn listing(&mut self, elements: Vec<PhpMixed>) { + self.auto_prepend_text(); + let elements: Vec<PhpMixed> = shirabe_php_shim::array_map( + |element: &PhpMixed| { + PhpMixed::String(shirabe_php_shim::sprintf(" * %s", &[element.clone()])) + }, + &elements, + ); + + self.writeln( + PhpMixed::List(elements.into_iter().map(Box::new).collect()), + OUTPUT_NORMAL, + ); + self.new_line(1); + } + + /// {@inheritdoc} + fn text(&mut self, message: PhpMixed) { + self.auto_prepend_text(); + + let messages: Vec<PhpMixed> = if shirabe_php_shim::is_array(&message) { + // array_values($message) + todo!() + } else { + vec![message] + }; + for message in messages { + self.writeln( + PhpMixed::String(shirabe_php_shim::sprintf(" %s", &[message])), + OUTPUT_NORMAL, + ); + } + } + + /// {@inheritdoc} + fn success(&mut self, message: PhpMixed) { + self.block( + message, + Some("OK"), + Some("fg=black;bg=green"), + " ", + true, + true, + ); + } + + /// {@inheritdoc} + fn error(&mut self, message: PhpMixed) { + self.block( + message, + Some("ERROR"), + Some("fg=white;bg=red"), + " ", + true, + true, + ); + } + + /// {@inheritdoc} + fn warning(&mut self, message: PhpMixed) { + self.block( + message, + Some("WARNING"), + Some("fg=black;bg=yellow"), + " ", + true, + true, + ); + } + + /// {@inheritdoc} + fn note(&mut self, message: PhpMixed) { + self.block(message, Some("NOTE"), Some("fg=yellow"), " ! ", false, true); + } + + /// {@inheritdoc} + fn caution(&mut self, message: PhpMixed) { + self.block( + message, + Some("CAUTION"), + Some("fg=white;bg=red"), + " ! ", + true, + true, + ); + } + + /// {@inheritdoc} + fn table(&mut self, headers: Vec<PhpMixed>, rows: Vec<PhpMixed>) { + self.create_table() + .set_headers(headers) + .set_rows(rows) + .render(); + + self.new_line(1); + } + + /// {@inheritdoc} + fn ask( &mut self, - _question: &str, - _choices: Vec<PhpMixed>, - _default: Option<PhpMixed>, + question: &str, + default: Option<&str>, + validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>, ) -> PhpMixed { - todo!() + let mut question = Question::new( + question.to_string(), + default.map(|d| PhpMixed::String(d.to_string())), + ); + question.set_validator(Self::adapt_validator(validator)); + + self.ask_question(question) } - pub fn table(&mut self, _headers: Vec<PhpMixed>, _rows: Vec<PhpMixed>) { - todo!() + /// {@inheritdoc} + fn ask_hidden( + &mut self, + question: &str, + validator: Option<Box<dyn Fn(Option<PhpMixed>) -> anyhow::Result<PhpMixed>>>, + ) -> PhpMixed { + let mut question = Question::new(question.to_string(), None); + + question.set_hidden(true); + question.set_validator(Self::adapt_validator(validator)); + + self.ask_question(question) } - pub fn progress_start(&mut self, _max: i64) { - todo!() + /// {@inheritdoc} + fn confirm(&mut self, question: &str, default: bool) -> bool { + // PHP: return $this->askQuestion(new ConfirmationQuestion($question, $default)); + // ConfirmationQuestion extends Question, but the ported type embeds a private + // `inner: Question` and cannot be passed to `ask_question(Question)`. The + // Question class hierarchy needs a trait/enum to dispatch polymorphically. + let _confirmation_question = + ConfirmationQuestion::new(question.to_string(), default, "/^y/i".to_string()); + let answer = self.ask_question(todo!()); + // PHP returns the bool answer from ConfirmationQuestion. + match answer { + PhpMixed::Bool(b) => b, + _ => todo!(), + } } - pub fn progress_advance(&mut self, _step: i64) { - todo!() + /// {@inheritdoc} + fn choice( + &mut self, + question: &str, + choices: Vec<PhpMixed>, + default: Option<PhpMixed>, + ) -> PhpMixed { + let default = if default.is_some() { + let default = default.unwrap(); + let values = shirabe_php_shim::array_flip(&PhpMixed::List( + choices.iter().cloned().map(Box::new).collect(), + )); + // $default = $values[$default] ?? $default; + let _ = values; + Some(default) + } else { + default + }; + + // PHP: return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); + // ChoiceQuestion extends Question; see `confirm` for the polymorphism note. + let choices_map: indexmap::IndexMap<String, Box<PhpMixed>> = choices + .into_iter() + .enumerate() + .map(|(i, c)| (i.to_string(), Box::new(c))) + .collect(); + let _choice_question = ChoiceQuestion::new(question.to_string(), choices_map, default); + self.ask_question(todo!()) } - pub fn progress_finish(&mut self) { - todo!() + /// {@inheritdoc} + fn new_line(&mut self, count: i64) { + self.inner.new_line(count); + self.buffered_output.write( + &[shirabe_php_shim::str_repeat("\n", count as usize)], + false, + OUTPUT_NORMAL, + ); } - pub fn is_debug(&self) -> bool { - todo!() + /// {@inheritdoc} + fn progress_start(&mut self, max: i64) { + let mut progress_bar = self.create_progress_bar(max); + progress_bar.start(None); + self.progress_bar = Some(progress_bar); } - pub fn writeln(&mut self, _messages: PhpMixed, _type: i64) { - todo!() + /// {@inheritdoc} + fn progress_advance(&mut self, step: i64) { + self.get_progress_bar().advance(step); } - pub fn write(&mut self, _messages: PhpMixed, _newline: bool, _type: i64) { - todo!() + /// {@inheritdoc} + fn progress_finish(&mut self) { + self.get_progress_bar().finish(); + self.new_line(2); + self.progress_bar = None; } } |
