aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs617
1 files changed, 617 insertions, 0 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs
new file mode 100644
index 0000000..963fcd8
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs
@@ -0,0 +1,617 @@
+use crate::composer::pcre::preg::Preg;
+use crate::symfony::console::application::Application;
+use crate::symfony::console::command::command::Command;
+use crate::symfony::console::descriptor::application_description::ApplicationDescription;
+use crate::symfony::console::descriptor::descriptor::Descriptor;
+use crate::symfony::console::descriptor::descriptor_interface::DescriptorInterface;
+use crate::symfony::console::formatter::output_formatter::OutputFormatter;
+use crate::symfony::console::helper::helper::Helper;
+use crate::symfony::console::input::input_argument::InputArgument;
+use crate::symfony::console::input::input_definition::InputDefinition;
+use crate::symfony::console::input::input_option::InputOption;
+use crate::symfony::console::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// Text descriptor.
+///
+/// @internal
+#[derive(Debug, Default)]
+pub struct TextDescriptor {
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+}
+
+/// Models PHP's `array<Command|string>` passed to `getColumnWidth`.
+/// `PhpMixed` cannot hold console types, so a dedicated enum is used.
+#[derive(Debug)]
+enum CommandOrString {
+ Command(std::rc::Rc<std::cell::RefCell<dyn Command>>),
+ String(String),
+}
+
+impl TextDescriptor {
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let default = if !argument.get_default().is_null()
+ && (!matches!(
+ argument.get_default(),
+ PhpMixed::List(_) | PhpMixed::Array(_)
+ ) || shirabe_php_shim::count(argument.get_default()) != 0)
+ {
+ format!(
+ "<comment> [default: {}]</comment>",
+ self.format_default_value(argument.get_default())?
+ )
+ } else {
+ String::new()
+ };
+
+ let total_width = match options.get("total_width") {
+ Some(PhpMixed::Int(w)) => *w,
+ _ => Helper::width(argument.get_name()),
+ };
+ let spacing_width = total_width - shirabe_php_shim::strlen(argument.get_name());
+
+ self.write_text(
+ &shirabe_php_shim::sprintf(
+ " <info>%s</info> %s%s%s",
+ &[
+ PhpMixed::String(argument.get_name().to_string()),
+ PhpMixed::String(shirabe_php_shim::str_repeat(" ", spacing_width as usize)),
+ // + 4 = 2 spaces before <info>, 2 spaces after </info>
+ PhpMixed::String(Preg::replace(
+ "/\\s*[\\r\\n]\\s*/",
+ &format!(
+ "\n{}",
+ shirabe_php_shim::str_repeat(" ", (total_width + 4) as usize)
+ ),
+ argument.get_description(),
+ )?),
+ PhpMixed::String(default),
+ ],
+ ),
+ &options,
+ );
+ Ok(())
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let default = if option.accept_value()
+ && !option.get_default().is_null()
+ && (!matches!(option.get_default(), PhpMixed::List(_) | PhpMixed::Array(_))
+ || shirabe_php_shim::count(option.get_default()) != 0)
+ {
+ format!(
+ "<comment> [default: {}]</comment>",
+ self.format_default_value(option.get_default())?
+ )
+ } else {
+ String::new()
+ };
+
+ let mut value = String::new();
+ if option.accept_value() {
+ value = format!("={}", shirabe_php_shim::strtoupper(option.get_name()));
+
+ if option.is_value_optional() {
+ value = format!("[{}]", value);
+ }
+ }
+
+ let total_width = match options.get("total_width") {
+ Some(PhpMixed::Int(w)) => *w,
+ _ => self.calculate_total_width_for_options(&[option]),
+ };
+ let synopsis = format!(
+ "{}{}",
+ if option.get_shortcut().is_some() {
+ shirabe_php_shim::sprintf(
+ "-%s, ",
+ &[PhpMixed::String(option.get_shortcut().unwrap().to_string())],
+ )
+ } else {
+ " ".to_string()
+ },
+ if option.is_negatable() {
+ shirabe_php_shim::sprintf(
+ "--%1$s|--no-%1$s",
+ &[
+ PhpMixed::String(option.get_name().to_string()),
+ PhpMixed::String(value.clone()),
+ ],
+ )
+ } else {
+ shirabe_php_shim::sprintf(
+ "--%1$s%2$s",
+ &[
+ PhpMixed::String(option.get_name().to_string()),
+ PhpMixed::String(value.clone()),
+ ],
+ )
+ }
+ );
+
+ let spacing_width = total_width - Helper::width(&synopsis);
+
+ self.write_text(
+ &shirabe_php_shim::sprintf(
+ " <info>%s</info> %s%s%s%s",
+ &[
+ PhpMixed::String(synopsis),
+ PhpMixed::String(shirabe_php_shim::str_repeat(" ", spacing_width as usize)),
+ // + 4 = 2 spaces before <info>, 2 spaces after </info>
+ PhpMixed::String(Preg::replace(
+ "/\\s*[\\r\\n]\\s*/",
+ &format!(
+ "\n{}",
+ shirabe_php_shim::str_repeat(" ", (total_width + 4) as usize)
+ ),
+ option.get_description(),
+ )?),
+ PhpMixed::String(default),
+ PhpMixed::String(if option.is_array() {
+ "<comment> (multiple values allowed)</comment>".to_string()
+ } else {
+ String::new()
+ }),
+ ],
+ ),
+ &options,
+ );
+ Ok(())
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let mut total_width = self.calculate_total_width_for_options(
+ &definition
+ .get_options()
+ .values()
+ .map(|o| o.as_ref())
+ .collect::<Vec<_>>(),
+ );
+ for argument in definition.get_arguments().values() {
+ total_width = std::cmp::max(total_width, Helper::width(argument.get_name()));
+ }
+
+ if !definition.get_arguments().is_empty() {
+ self.write_text("<comment>Arguments:</comment>", &options);
+ self.write_text("\n", &IndexMap::new());
+ for argument in definition.get_arguments().values() {
+ let mut merged = options.clone();
+ merged.insert("total_width".to_string(), PhpMixed::Int(total_width));
+ self.describe_input_argument(argument, merged)?;
+ self.write_text("\n", &IndexMap::new());
+ }
+ }
+
+ if !definition.get_arguments().is_empty() && !definition.get_options().is_empty() {
+ self.write_text("\n", &IndexMap::new());
+ }
+
+ if !definition.get_options().is_empty() {
+ let mut later_options: Vec<&InputOption> = vec![];
+
+ self.write_text("<comment>Options:</comment>", &options);
+ for option in definition.get_options().values() {
+ if shirabe_php_shim::strlen(option.get_shortcut().unwrap_or("")) > 1 {
+ later_options.push(option.as_ref());
+ continue;
+ }
+ self.write_text("\n", &IndexMap::new());
+ let mut merged = options.clone();
+ merged.insert("total_width".to_string(), PhpMixed::Int(total_width));
+ self.describe_input_option(option, merged)?;
+ }
+ for option in later_options {
+ self.write_text("\n", &IndexMap::new());
+ let mut merged = options.clone();
+ merged.insert("total_width".to_string(), PhpMixed::Int(total_width));
+ self.describe_input_option(option, merged)?;
+ }
+ }
+ Ok(())
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &mut dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ command.merge_application_definition(false);
+
+ let description = command.get_description();
+ if !description.is_empty() {
+ self.write_text("<comment>Description:</comment>", &options);
+ self.write_text("\n", &IndexMap::new());
+ self.write_text(&format!(" {}", description), &IndexMap::new());
+ self.write_text("\n\n", &IndexMap::new());
+ }
+
+ self.write_text("<comment>Usage:</comment>", &options);
+ let mut usages = vec![command.get_synopsis(true)];
+ usages.extend(command.get_aliases());
+ usages.extend(command.get_usages());
+ for usage in usages {
+ self.write_text("\n", &IndexMap::new());
+ self.write_text(&format!(" {}", OutputFormatter::escape(&usage)?), &options);
+ }
+ self.write_text("\n", &IndexMap::new());
+
+ let definition = command.get_definition().clone();
+ if !definition.get_options().is_empty() || !definition.get_arguments().is_empty() {
+ self.write_text("\n", &IndexMap::new());
+ self.describe_input_definition(&definition, options.clone())?;
+ self.write_text("\n", &IndexMap::new());
+ }
+
+ let help = command.get_processed_help();
+ if !help.is_empty() && help != description {
+ self.write_text("\n", &IndexMap::new());
+ self.write_text("<comment>Help:</comment>", &options);
+ self.write_text("\n", &IndexMap::new());
+ self.write_text(
+ &format!(" {}", shirabe_php_shim::str_replace("\n", "\n ", &help)),
+ &options,
+ );
+ self.write_text("\n", &IndexMap::new());
+ }
+ Ok(())
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let described_namespace = match options.get("namespace") {
+ Some(PhpMixed::String(s)) => Some(s.clone()),
+ _ => None,
+ };
+ let mut description =
+ ApplicationDescription::new(application.clone(), described_namespace.clone(), false);
+
+ if matches!(options.get("raw_text"), Some(v) if shirabe_php_shim::php_truthy(v)) {
+ let width = self.get_column_width(
+ &description
+ .get_commands()
+ .values()
+ .map(|c| CommandOrString::Command(c.clone()))
+ .collect::<Vec<_>>(),
+ );
+
+ let command_list: Vec<_> = description.get_commands().values().cloned().collect();
+ for command in &command_list {
+ let command = command.borrow();
+ self.write_text(
+ &shirabe_php_shim::sprintf(
+ &format!("%-{}s %s", width),
+ &[
+ PhpMixed::String(command.get_name().unwrap_or_default()),
+ PhpMixed::String(command.get_description()),
+ ],
+ ),
+ &options,
+ );
+ self.write_text("\n", &IndexMap::new());
+ }
+ } else {
+ let help = application.borrow().get_help();
+ if !help.is_empty() {
+ self.write_text(&format!("{}\n\n", help), &options);
+ }
+
+ self.write_text("<comment>Usage:</comment>\n", &options);
+ self.write_text(" command [options] [arguments]\n\n", &options);
+
+ // PHP: new InputDefinition($application->getDefinition()->getOptions()).
+ // `InputOption` is not Clone and lives behind `Rc`, so the option-only
+ // definition cannot be reconstructed by value yet.
+ let definition: InputDefinition =
+ todo!("new InputDefinition($application->getDefinition()->getOptions())");
+ self.describe_input_definition(&definition, options.clone())?;
+
+ self.write_text("\n", &IndexMap::new());
+ self.write_text("\n", &IndexMap::new());
+
+ let mut commands = description.get_commands().clone();
+ let namespaces = description.get_namespaces();
+ if described_namespace.is_some() && !namespaces.is_empty() {
+ // make sure all alias commands are included when describing a specific namespace
+ let described_namespace_info = namespaces.values().next().unwrap();
+ if let Some(PhpMixed::List(names)) = described_namespace_info.get("commands") {
+ let names: Vec<String> = names
+ .iter()
+ .filter_map(|n| n.as_string().map(|s| s.to_string()))
+ .collect();
+ for name in names {
+ let command = description.get_command(&name)?;
+ commands.insert(name, command);
+ }
+ }
+ }
+
+ // calculate max. width based on available commands per namespace
+ let width = self.get_column_width(&{
+ let command_keys: Vec<String> = commands.keys().cloned().collect();
+ let mut merged: Vec<CommandOrString> = vec![];
+ for namespace in namespaces.values() {
+ if let Some(PhpMixed::List(ns_commands)) = namespace.get("commands") {
+ for c in ns_commands {
+ if let PhpMixed::String(name) = c.as_ref() {
+ if command_keys.contains(name) {
+ merged.push(CommandOrString::String(name.clone()));
+ }
+ }
+ }
+ }
+ }
+ merged
+ });
+
+ if let Some(ref described_namespace) = described_namespace {
+ self.write_text(
+ &shirabe_php_shim::sprintf(
+ "<comment>Available commands for the \"%s\" namespace:</comment>",
+ &[PhpMixed::String(described_namespace.clone())],
+ ),
+ &options,
+ );
+ } else {
+ self.write_text("<comment>Available commands:</comment>", &options);
+ }
+
+ for namespace in namespaces.values() {
+ let ns_commands: Vec<String> = match namespace.get("commands") {
+ Some(PhpMixed::List(names)) => names
+ .iter()
+ .filter_map(|n| match n.as_ref() {
+ PhpMixed::String(name) if commands.contains_key(name) => {
+ Some(name.clone())
+ }
+ _ => None,
+ })
+ .collect(),
+ _ => vec![],
+ };
+
+ if ns_commands.is_empty() {
+ continue;
+ }
+
+ let namespace_id = match namespace.get("id") {
+ Some(PhpMixed::String(s)) => s.clone(),
+ _ => String::new(),
+ };
+
+ if described_namespace.is_none()
+ && ApplicationDescription::GLOBAL_NAMESPACE != namespace_id
+ {
+ self.write_text("\n", &IndexMap::new());
+ self.write_text(&format!(" <comment>{}</comment>", namespace_id), &options);
+ }
+
+ for name in ns_commands {
+ self.write_text("\n", &IndexMap::new());
+ let spacing_width = width - Helper::width(&name);
+ let command = commands.get(&name).unwrap().clone();
+ let command = command.borrow();
+ let command_aliases = if command.get_name().as_deref() == Some(name.as_str()) {
+ self.get_command_aliases_text(&*command)
+ } else {
+ String::new()
+ };
+ self.write_text(
+ &shirabe_php_shim::sprintf(
+ " <info>%s</info>%s%s",
+ &[
+ PhpMixed::String(name.clone()),
+ PhpMixed::String(shirabe_php_shim::str_repeat(
+ " ",
+ spacing_width as usize,
+ )),
+ PhpMixed::String(format!(
+ "{}{}",
+ command_aliases,
+ command.get_description()
+ )),
+ ],
+ ),
+ &options,
+ );
+ }
+ }
+
+ self.write_text("\n", &IndexMap::new());
+ }
+ Ok(())
+ }
+
+ fn write_text(&self, content: &str, options: &IndexMap<String, PhpMixed>) {
+ let raw_text =
+ matches!(options.get("raw_text"), Some(v) if shirabe_php_shim::php_truthy(v));
+ let content = if raw_text {
+ shirabe_php_shim::strip_tags(content)
+ } else {
+ content.to_string()
+ };
+ let decorated = match options.get("raw_output") {
+ Some(v) => !shirabe_php_shim::php_truthy(v),
+ None => true,
+ };
+ self.write(&content, decorated);
+ }
+
+ /// Formats command aliases to show them in the command description.
+ fn get_command_aliases_text(&self, command: &dyn Command) -> String {
+ let mut text = String::new();
+ let aliases = command.get_aliases();
+
+ if !aliases.is_empty() {
+ text = format!("[{}] ", aliases.join("|"));
+ }
+
+ text
+ }
+
+ /// Formats input option/argument default value.
+ fn format_default_value(&self, default: &PhpMixed) -> anyhow::Result<String> {
+ if matches!(default, PhpMixed::Float(f) if f.is_infinite() && *f > 0.0) {
+ return Ok("INF".to_string());
+ }
+
+ let default = match default {
+ PhpMixed::String(s) => PhpMixed::String(OutputFormatter::escape(s)?),
+ PhpMixed::Array(arr) => {
+ let mut arr = arr.clone();
+ for (_key, value) in arr.iter_mut() {
+ if let PhpMixed::String(s) = value.as_ref() {
+ *value = Box::new(PhpMixed::String(OutputFormatter::escape(s)?));
+ }
+ }
+ PhpMixed::Array(arr)
+ }
+ PhpMixed::List(list) => {
+ let mut list = list.clone();
+ for value in list.iter_mut() {
+ if let PhpMixed::String(s) = value.as_ref() {
+ *value = Box::new(PhpMixed::String(OutputFormatter::escape(s)?));
+ }
+ }
+ PhpMixed::List(list)
+ }
+ other => other.clone(),
+ };
+
+ Ok(shirabe_php_shim::str_replace(
+ "\\\\",
+ "\\",
+ &shirabe_php_shim::json_encode_ex(
+ &default,
+ shirabe_php_shim::JSON_UNESCAPED_SLASHES | shirabe_php_shim::JSON_UNESCAPED_UNICODE,
+ )
+ .unwrap_or_default(),
+ ))
+ }
+
+ /// @param array<Command|string> $commands
+ fn get_column_width(&self, commands: &[CommandOrString]) -> i64 {
+ let mut widths: Vec<i64> = vec![];
+
+ for command in commands {
+ // case $command instanceof Command
+ match command {
+ CommandOrString::Command(command) => {
+ let command = command.borrow();
+ widths.push(Helper::width(command.get_name().as_deref().unwrap_or("")));
+ for alias in command.get_aliases() {
+ widths.push(Helper::width(&alias));
+ }
+ }
+ CommandOrString::String(s) => {
+ widths.push(Helper::width(s));
+ }
+ }
+ }
+
+ if !widths.is_empty() {
+ widths.into_iter().max().unwrap() + 2
+ } else {
+ 0
+ }
+ }
+
+ /// @param InputOption[] $options
+ fn calculate_total_width_for_options(&self, options: &[&InputOption]) -> i64 {
+ let mut total_width: i64 = 0;
+ for option in options {
+ // "-" + shortcut + ", --" + name
+ let mut name_length = 1
+ + std::cmp::max(Helper::width(option.get_shortcut().unwrap_or("")), 1)
+ + 4
+ + Helper::width(option.get_name());
+ if option.is_negatable() {
+ name_length += 6 + Helper::width(option.get_name()); // |--no- + name
+ } else if option.accept_value() {
+ let mut value_length = 1 + Helper::width(option.get_name()); // = + value
+ value_length += if option.is_value_optional() { 2 } else { 0 }; // [ + ]
+
+ name_length += value_length;
+ }
+ total_width = std::cmp::max(total_width, name_length);
+ }
+
+ total_width
+ }
+}
+
+impl DescriptorInterface for TextDescriptor {
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: PhpMixed,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ Descriptor::describe(self, output, object, options)
+ }
+}
+
+impl Descriptor for TextDescriptor {
+ fn output(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> {
+ self.output.clone().unwrap()
+ }
+
+ fn set_output(&mut self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>) {
+ self.output = Some(output);
+ }
+
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ TextDescriptor::describe_input_argument(self, argument, options)
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ TextDescriptor::describe_input_option(self, option, options)
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ TextDescriptor::describe_input_definition(self, definition, options)
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &mut dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ TextDescriptor::describe_command(self, command, options)
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ TextDescriptor::describe_application(self, application, options)
+ }
+}