aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src/descriptor
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe-symfony-console/src/descriptor')
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/application_description.rs189
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/descriptor.rs97
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/descriptor_interface.rs29
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs402
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs378
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs592
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs403
7 files changed, 2090 insertions, 0 deletions
diff --git a/crates/shirabe-symfony-console/src/descriptor/application_description.rs b/crates/shirabe-symfony-console/src/descriptor/application_description.rs
new file mode 100644
index 00000000..d63f0be2
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/application_description.rs
@@ -0,0 +1,189 @@
+//! ref: composer/vendor/symfony/console/Descriptor/ApplicationDescription.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::exception::command_not_found_exception::CommandNotFoundException;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// @internal
+#[derive(Debug)]
+pub struct ApplicationDescription {
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ namespace: Option<String>,
+ show_hidden: bool,
+
+ /// @var array
+ /// Each namespace entry is `['id' => string, 'commands' => string[]]`.
+ namespaces: Option<IndexMap<String, IndexMap<String, PhpMixed>>>,
+
+ /// @var array<string, Command>
+ commands: Option<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>>>,
+
+ /// @var array<string, Command>
+ aliases: Option<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>>>,
+}
+
+impl ApplicationDescription {
+ pub const GLOBAL_NAMESPACE: &'static str = "_global";
+
+ pub fn new(
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ namespace: Option<String>,
+ show_hidden: bool,
+ ) -> Self {
+ ApplicationDescription {
+ application,
+ namespace,
+ show_hidden,
+ namespaces: None,
+ commands: None,
+ aliases: None,
+ }
+ }
+
+ pub fn get_namespaces(&mut self) -> IndexMap<String, IndexMap<String, PhpMixed>> {
+ if self.namespaces.is_none() {
+ self.inspect_application();
+ }
+
+ self.namespaces.clone().unwrap()
+ }
+
+ pub fn get_commands(
+ &mut self,
+ ) -> &IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>> {
+ if self.commands.is_none() {
+ self.inspect_application();
+ }
+
+ self.commands.as_ref().unwrap()
+ }
+
+ /// @throws CommandNotFoundException
+ pub fn get_command(
+ &self,
+ name: &str,
+ ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn Command>>> {
+ let in_commands = self
+ .commands
+ .as_ref()
+ .map(|c| c.contains_key(name))
+ .unwrap_or(false);
+ let in_aliases = self
+ .aliases
+ .as_ref()
+ .map(|a| a.contains_key(name))
+ .unwrap_or(false);
+ if !in_commands && !in_aliases {
+ return Err(CommandNotFoundException::new(
+ format!("Command \"{}\" does not exist.", name),
+ vec![],
+ 0,
+ )
+ .into());
+ }
+
+ Ok(self
+ .commands
+ .as_ref()
+ .and_then(|c| c.get(name))
+ .cloned()
+ .unwrap_or_else(|| self.aliases.as_ref().unwrap().get(name).unwrap().clone()))
+ }
+
+ fn inspect_application(&mut self) {
+ self.commands = Some(IndexMap::new());
+ self.namespaces = Some(IndexMap::new());
+
+ let namespace_filter = match &self.namespace {
+ Some(ns) if !ns.is_empty() => {
+ Some(self.application.borrow_mut().find_namespace(ns).unwrap())
+ }
+ _ => None,
+ };
+ let all = self
+ .application
+ .borrow_mut()
+ .all(namespace_filter.as_deref())
+ .unwrap();
+ for (namespace, commands) in self.sort_commands(all) {
+ let mut names: Vec<String> = vec![];
+
+ for (name, command) in commands {
+ let command_name = command.borrow().get_name();
+ let is_hidden = command.borrow().is_hidden();
+ if command_name.is_none()
+ || command_name.as_deref() == Some("")
+ || (!self.show_hidden && is_hidden)
+ {
+ continue;
+ }
+
+ if command_name.as_deref() == Some(name.as_str()) {
+ self.commands
+ .as_mut()
+ .unwrap()
+ .insert(name.clone(), command);
+ } else {
+ self.aliases
+ .get_or_insert_with(IndexMap::new)
+ .insert(name.clone(), command);
+ }
+
+ names.push(name);
+ }
+
+ let mut entry: IndexMap<String, PhpMixed> = IndexMap::new();
+ entry.insert("id".to_string(), PhpMixed::String(namespace.clone()));
+ entry.insert(
+ "commands".to_string(),
+ PhpMixed::List(names.into_iter().map(PhpMixed::String).collect()),
+ );
+ self.namespaces.as_mut().unwrap().insert(namespace, entry);
+ }
+ }
+
+ fn sort_commands(
+ &self,
+ commands: IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>>,
+ ) -> IndexMap<String, IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>>> {
+ let mut namespaced_commands: IndexMap<
+ String,
+ IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>>,
+ > = IndexMap::new();
+ let mut global_commands: IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>> =
+ IndexMap::new();
+ let mut sorted_commands: IndexMap<
+ String,
+ IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn Command>>>,
+ > = IndexMap::new();
+ for (name, command) in commands {
+ let key = self.application.borrow().extract_namespace(&name, Some(1));
+ if ["", Self::GLOBAL_NAMESPACE].contains(&key.as_str()) {
+ global_commands.insert(name, command);
+ } else {
+ namespaced_commands
+ .entry(key)
+ .or_default()
+ .insert(name, command);
+ }
+ }
+
+ if !global_commands.is_empty() {
+ global_commands.sort_keys();
+ sorted_commands.insert(Self::GLOBAL_NAMESPACE.to_string(), global_commands);
+ }
+
+ if !namespaced_commands.is_empty() {
+ // ksort($namespacedCommands, \SORT_STRING)
+ namespaced_commands.sort_keys();
+ for (key, mut commands_set) in namespaced_commands {
+ commands_set.sort_keys();
+ sorted_commands.insert(key, commands_set);
+ }
+ }
+
+ sorted_commands
+ }
+}
diff --git a/crates/shirabe-symfony-console/src/descriptor/descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/descriptor.rs
new file mode 100644
index 00000000..f3647d7c
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/descriptor.rs
@@ -0,0 +1,97 @@
+//! ref: composer/vendor/symfony/console/Descriptor/Descriptor.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::descriptor::descriptor_interface::{DescribableObject, DescriptorInterface};
+use crate::input::input_argument::InputArgument;
+use crate::input::input_definition::InputDefinition;
+use crate::input::input_option::InputOption;
+use crate::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// @internal
+pub trait Descriptor: DescriptorInterface {
+ fn output(&self) -> std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>;
+
+ fn set_output(&mut self, output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>);
+
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: DescribableObject,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.set_output(output);
+
+ // PHP dispatches via `$object instanceof ...`; the explicit `DescribableObject` enum makes
+ // that dispatch a `match`.
+ match object {
+ DescribableObject::InputArgument(argument) => {
+ self.describe_input_argument(&argument, options)?;
+ }
+ DescribableObject::InputOption(option) => {
+ self.describe_input_option(&option, options)?;
+ }
+ DescribableObject::InputDefinition(definition) => {
+ self.describe_input_definition(&definition, options)?;
+ }
+ DescribableObject::Command(command) => {
+ self.describe_command(&*command.borrow(), options)?;
+ }
+ DescribableObject::Application(application) => {
+ self.describe_application(application, options)?;
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Writes content to output.
+ fn write(&self, content: &str, decorated: bool) {
+ self.output().borrow().write(
+ &[content.to_string()],
+ false,
+ if decorated {
+ crate::output::output_interface::OUTPUT_NORMAL
+ } else {
+ crate::output::output_interface::OUTPUT_RAW
+ },
+ );
+ }
+
+ /// Describes an InputArgument instance.
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+
+ /// Describes an InputOption instance.
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+
+ /// Describes an InputDefinition instance.
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+
+ /// Describes a Command instance.
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+
+ /// Describes an Application instance.
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+}
diff --git a/crates/shirabe-symfony-console/src/descriptor/descriptor_interface.rs b/crates/shirabe-symfony-console/src/descriptor/descriptor_interface.rs
new file mode 100644
index 00000000..c50c6a99
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/descriptor_interface.rs
@@ -0,0 +1,29 @@
+//! ref: composer/vendor/symfony/console/Descriptor/DescriptorInterface.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::input::input_argument::InputArgument;
+use crate::input::input_definition::InputDefinition;
+use crate::input::input_option::InputOption;
+use crate::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// The set of objects the descriptors know how to describe.
+pub enum DescribableObject {
+ InputArgument(InputArgument),
+ InputOption(InputOption),
+ InputDefinition(InputDefinition),
+ Command(std::rc::Rc<std::cell::RefCell<dyn Command>>),
+ Application(std::rc::Rc<std::cell::RefCell<dyn Application>>),
+}
+
+/// Descriptor interface.
+pub trait DescriptorInterface {
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: DescribableObject,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+}
diff --git a/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs
new file mode 100644
index 00000000..983d5f08
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/json_descriptor.rs
@@ -0,0 +1,402 @@
+//! ref: composer/vendor/symfony/console/Descriptor/JsonDescriptor.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::descriptor::application_description::ApplicationDescription;
+use crate::descriptor::descriptor::Descriptor;
+use crate::descriptor::descriptor_interface::{DescribableObject, DescriptorInterface};
+use crate::input::input_argument::InputArgument;
+use crate::input::input_definition::InputDefinition;
+use crate::input::input_option::InputOption;
+use crate::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_pcre::preg::Preg;
+use shirabe_php_shim::{PhpMixed, php_regex};
+
+/// JSON descriptor.
+///
+/// @internal
+#[derive(Debug, Default)]
+pub struct JsonDescriptor {
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+}
+
+impl JsonDescriptor {
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.write_data(self.get_input_argument_data(argument)?, &options)?;
+ Ok(())
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.write_data(self.get_input_option_data(option, false)?, &options)?;
+ if option.is_negatable() {
+ self.write_data(self.get_input_option_data(option, true)?, &options)?;
+ }
+ Ok(())
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.write_data(self.get_input_definition_data(definition)?, &options)?;
+ Ok(())
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let short = matches!(options.get("short"), Some(PhpMixed::Bool(true)));
+ self.write_data(self.get_command_data(command, short)?, &options)?;
+ Ok(())
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn 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(), true);
+ let mut commands: Vec<PhpMixed> = vec![];
+
+ let short = matches!(options.get("short"), Some(PhpMixed::Bool(true)));
+ for command in description.get_commands().values() {
+ let command = command.borrow();
+ commands.push(PhpMixed::Array(
+ self.get_command_data(&*command, short)?
+ .into_iter()
+ .collect(),
+ ));
+ }
+
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ if "UNKNOWN" != application.borrow().get_name() {
+ let mut application_data: IndexMap<String, PhpMixed> = IndexMap::new();
+ application_data.insert(
+ "name".to_string(),
+ PhpMixed::String(application.borrow().get_name()),
+ );
+ if "UNKNOWN" != application.borrow().get_version() {
+ application_data.insert(
+ "version".to_string(),
+ PhpMixed::String(application.borrow().get_version()),
+ );
+ }
+ data.insert("application".to_string(), PhpMixed::Array(application_data));
+ }
+
+ data.insert("commands".to_string(), PhpMixed::List(commands));
+
+ if let Some(described_namespace) = described_namespace {
+ data.insert(
+ "namespace".to_string(),
+ PhpMixed::String(described_namespace),
+ );
+ } else {
+ data.insert(
+ "namespaces".to_string(),
+ PhpMixed::List(
+ description
+ .get_namespaces()
+ .into_values()
+ .map(|ns| PhpMixed::Array(ns.into_iter().collect()))
+ .collect(),
+ ),
+ );
+ }
+
+ self.write_data(data, &options)?;
+ Ok(())
+ }
+
+ /// Writes data as json.
+ fn write_data(
+ &self,
+ data: IndexMap<String, PhpMixed>,
+ options: &IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let flags = match options.get("json_encoding") {
+ Some(PhpMixed::Int(f)) => *f,
+ _ => 0,
+ };
+
+ self.write(
+ &shirabe_php_shim::json_encode_ex(&PhpMixed::Array(data.into_iter().collect()), flags)
+ .unwrap_or_default(),
+ false,
+ );
+ Ok(())
+ }
+
+ fn get_input_argument_data(
+ &self,
+ argument: &InputArgument,
+ ) -> anyhow::Result<IndexMap<String, PhpMixed>> {
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "name".to_string(),
+ PhpMixed::String(argument.get_name().to_string()),
+ );
+ data.insert(
+ "is_required".to_string(),
+ PhpMixed::Bool(argument.is_required()),
+ );
+ data.insert("is_array".to_string(), PhpMixed::Bool(argument.is_array()));
+ data.insert(
+ "description".to_string(),
+ PhpMixed::String(Preg::replace(
+ php_regex!("/\\s*[\\r\\n]\\s*/"),
+ " ",
+ argument.get_description(),
+ )),
+ );
+ data.insert(
+ "default".to_string(),
+ if matches!(argument.get_default(), PhpMixed::Float(f) if f.is_infinite() && *f > 0.0) {
+ PhpMixed::String("INF".to_string())
+ } else {
+ argument.get_default().clone()
+ },
+ );
+ Ok(data)
+ }
+
+ fn get_input_option_data(
+ &self,
+ option: &InputOption,
+ negated: bool,
+ ) -> anyhow::Result<IndexMap<String, PhpMixed>> {
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ if negated {
+ data.insert(
+ "name".to_string(),
+ PhpMixed::String(format!("--no-{}", option.get_name())),
+ );
+ data.insert("shortcut".to_string(), PhpMixed::String(String::new()));
+ data.insert("accept_value".to_string(), PhpMixed::Bool(false));
+ data.insert("is_value_required".to_string(), PhpMixed::Bool(false));
+ data.insert("is_multiple".to_string(), PhpMixed::Bool(false));
+ data.insert(
+ "description".to_string(),
+ PhpMixed::String(format!("Negate the \"--{}\" option", option.get_name())),
+ );
+ data.insert("default".to_string(), PhpMixed::Bool(false));
+ } else {
+ data.insert(
+ "name".to_string(),
+ PhpMixed::String(format!("--{}", option.get_name())),
+ );
+ data.insert(
+ "shortcut".to_string(),
+ PhpMixed::String(if let Some(shortcut) = option.get_shortcut() {
+ format!("-{}", shirabe_php_shim::str_replace("|", "|-", shortcut))
+ } else {
+ String::new()
+ }),
+ );
+ data.insert(
+ "accept_value".to_string(),
+ PhpMixed::Bool(option.accept_value()),
+ );
+ data.insert(
+ "is_value_required".to_string(),
+ PhpMixed::Bool(option.is_value_required()),
+ );
+ data.insert("is_multiple".to_string(), PhpMixed::Bool(option.is_array()));
+ data.insert(
+ "description".to_string(),
+ PhpMixed::String(Preg::replace(
+ php_regex!("/\\s*[\\r\\n]\\s*/"),
+ " ",
+ option.get_description(),
+ )),
+ );
+ data.insert(
+ "default".to_string(),
+ if matches!(option.get_default(), PhpMixed::Float(f) if f.is_infinite() && *f > 0.0)
+ {
+ PhpMixed::String("INF".to_string())
+ } else {
+ option.get_default().clone()
+ },
+ );
+ }
+ Ok(data)
+ }
+
+ fn get_input_definition_data(
+ &self,
+ definition: &InputDefinition,
+ ) -> anyhow::Result<IndexMap<String, PhpMixed>> {
+ let mut input_arguments: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (name, argument) in definition.get_arguments() {
+ input_arguments.insert(
+ name.clone(),
+ PhpMixed::Array(
+ self.get_input_argument_data(argument)?
+ .into_iter()
+ .collect(),
+ ),
+ );
+ }
+
+ let mut input_options: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (name, option) in definition.get_options() {
+ input_options.insert(
+ name.clone(),
+ PhpMixed::Array(
+ self.get_input_option_data(option, false)?
+ .into_iter()
+ .collect(),
+ ),
+ );
+ if option.is_negatable() {
+ input_options.insert(
+ format!("no-{}", name),
+ PhpMixed::Array(
+ self.get_input_option_data(option, true)?
+ .into_iter()
+ .collect(),
+ ),
+ );
+ }
+ }
+
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert("arguments".to_string(), PhpMixed::Array(input_arguments));
+ data.insert("options".to_string(), PhpMixed::Array(input_options));
+ Ok(data)
+ }
+
+ fn get_command_data(
+ &self,
+ command: &dyn Command,
+ short: bool,
+ ) -> anyhow::Result<IndexMap<String, PhpMixed>> {
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ data.insert(
+ "name".to_string(),
+ match command.get_name() {
+ Some(name) => PhpMixed::String(name),
+ None => PhpMixed::Null,
+ },
+ );
+ data.insert(
+ "description".to_string(),
+ PhpMixed::String(command.get_description()),
+ );
+
+ if short {
+ data.insert(
+ "usage".to_string(),
+ PhpMixed::List(
+ command
+ .get_aliases()
+ .into_iter()
+ .map(PhpMixed::String)
+ .collect(),
+ ),
+ );
+ } else {
+ command.merge_application_definition(false);
+
+ let mut usage = vec![PhpMixed::String(command.get_synopsis(false))];
+ usage.extend(command.get_usages().into_iter().map(PhpMixed::String));
+ usage.extend(command.get_aliases().into_iter().map(PhpMixed::String));
+ data.insert("usage".to_string(), PhpMixed::List(usage));
+ data.insert(
+ "help".to_string(),
+ PhpMixed::String(command.get_processed_help()),
+ );
+ data.insert(
+ "definition".to_string(),
+ PhpMixed::Array(
+ self.get_input_definition_data(&command.get_definition())?
+ .into_iter()
+ .collect(),
+ ),
+ );
+ }
+
+ data.insert("hidden".to_string(), PhpMixed::Bool(command.is_hidden()));
+
+ Ok(data)
+ }
+}
+
+impl DescriptorInterface for JsonDescriptor {
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: DescribableObject,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ Descriptor::describe(self, output, object, options)
+ }
+}
+
+impl Descriptor for JsonDescriptor {
+ 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<()> {
+ JsonDescriptor::describe_input_argument(self, argument, options)
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ JsonDescriptor::describe_input_option(self, option, options)
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ JsonDescriptor::describe_input_definition(self, definition, options)
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ JsonDescriptor::describe_command(self, command, options)
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ JsonDescriptor::describe_application(self, application, options)
+ }
+}
diff --git a/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs
new file mode 100644
index 00000000..b64c2be0
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs
@@ -0,0 +1,378 @@
+//! ref: composer/vendor/symfony/console/Descriptor/MarkdownDescriptor.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::descriptor::application_description::ApplicationDescription;
+use crate::descriptor::descriptor::Descriptor;
+use crate::descriptor::descriptor_interface::{DescribableObject, DescriptorInterface};
+use crate::helper::helper::Helper;
+use crate::input::input_argument::InputArgument;
+use crate::input::input_definition::InputDefinition;
+use crate::input::input_option::InputOption;
+use crate::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_pcre::preg::Preg;
+use shirabe_php_shim::PhpMixed;
+
+/// Markdown descriptor.
+///
+/// @internal
+#[derive(Debug, Default)]
+pub struct MarkdownDescriptor {
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+}
+
+impl MarkdownDescriptor {
+ pub fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: DescribableObject,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let decorated = output.borrow().is_decorated();
+ output.borrow().set_decorated(false);
+
+ Descriptor::describe(self, output.clone(), object, options)?;
+
+ output.borrow().set_decorated(decorated);
+ Ok(())
+ }
+
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ _options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let name = if !argument.get_name().is_empty() {
+ argument.get_name().to_string()
+ } else {
+ "<none>".to_string()
+ };
+ self.write(
+ &format!(
+ "#### `{}`\n\n{}* Is required: {}\n* Is array: {}\n* Default: `{}`",
+ name,
+ if !argument.get_description().is_empty() {
+ format!(
+ "{}\n\n",
+ Preg::replace("/\\s*[\\r\\n]\\s*/", "\n", argument.get_description())
+ )
+ } else {
+ String::new()
+ },
+ if argument.is_required() { "yes" } else { "no" },
+ if argument.is_array() { "yes" } else { "no" },
+ shirabe_php_shim::str_replace(
+ "\n",
+ "",
+ &shirabe_php_shim::var_export(argument.get_default(), true),
+ ),
+ ),
+ true,
+ );
+ Ok(())
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ _options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let mut name = format!("--{}", option.get_name());
+ if option.is_negatable() {
+ name += &format!("|--no-{}", option.get_name());
+ }
+ if let Some(shortcut) = option.get_shortcut() {
+ name += &format!("|-{}", shirabe_php_shim::str_replace("|", "|-", shortcut));
+ }
+
+ self.write(
+ &format!(
+ "#### `{}`\n\n{}* Accept value: {}\n* Is value required: {}\n* Is multiple: {}\n* Is negatable: {}\n* Default: `{}`",
+ name,
+ if !option.get_description().is_empty() {
+ format!(
+ "{}\n\n",
+ Preg::replace("/\\s*[\\r\\n]\\s*/", "\n", option.get_description())
+ )
+ } else {
+ String::new()
+ },
+ if option.accept_value() { "yes" } else { "no" },
+ if option.is_value_required() { "yes" } else { "no" },
+ if option.is_array() { "yes" } else { "no" },
+ if option.is_negatable() { "yes" } else { "no" },
+ shirabe_php_shim::str_replace(
+ "\n",
+ "",
+ &shirabe_php_shim::var_export(option.get_default(), true),
+ ),
+ ),
+ true,
+ );
+ Ok(())
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ _options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let show_arguments = !definition.get_arguments().is_empty();
+ if show_arguments {
+ self.write("### Arguments", true);
+ for argument in definition.get_arguments().values() {
+ self.write("\n\n", true);
+ // describeInputArgument returns null; the guarded write never runs.
+ self.describe_input_argument(argument, IndexMap::new())?;
+ }
+ }
+
+ if !definition.get_options().is_empty() {
+ if show_arguments {
+ self.write("\n\n", true);
+ }
+
+ self.write("### Options", true);
+ for option in definition.get_options().values() {
+ self.write("\n\n", true);
+ // describeInputOption returns null; the guarded write never runs.
+ self.describe_input_option(option, IndexMap::new())?;
+ }
+ }
+ Ok(())
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ if matches!(options.get("short"), Some(PhpMixed::Bool(true))) {
+ self.write(
+ &format!(
+ "`{}`\n{}\n\n{}### Usage\n\n{}",
+ command.get_name().unwrap_or_default(),
+ shirabe_php_shim::str_repeat(
+ "-",
+ (Helper::width(command.get_name().as_deref().unwrap_or("")) + 2) as usize
+ ),
+ if !command.get_description().is_empty() {
+ format!("{}\n\n", command.get_description())
+ } else {
+ String::new()
+ },
+ command
+ .get_aliases()
+ .iter()
+ .fold(String::new(), |carry, usage| {
+ format!("{}* `{}`\n", carry, usage)
+ }),
+ ),
+ true,
+ );
+
+ return Ok(());
+ }
+
+ command.merge_application_definition(false);
+
+ let mut usages = vec![command.get_synopsis(false)];
+ usages.extend(command.get_aliases());
+ usages.extend(command.get_usages());
+ self.write(
+ &format!(
+ "`{}`\n{}\n\n{}### Usage\n\n{}",
+ command.get_name().unwrap_or_default(),
+ shirabe_php_shim::str_repeat(
+ "-",
+ (Helper::width(command.get_name().as_deref().unwrap_or("")) + 2) as usize
+ ),
+ if !command.get_description().is_empty() {
+ format!("{}\n\n", command.get_description())
+ } else {
+ String::new()
+ },
+ usages.iter().fold(String::new(), |carry, usage| {
+ format!("{}* `{}`\n", carry, usage)
+ }),
+ ),
+ true,
+ );
+
+ let help = command.get_processed_help();
+ if !help.is_empty() {
+ self.write("\n", true);
+ self.write(&help, true);
+ }
+
+ let definition = command.get_definition().clone();
+ if !definition.get_options().is_empty() || !definition.get_arguments().is_empty() {
+ self.write("\n\n", true);
+ self.describe_input_definition(&definition, IndexMap::new())?;
+ }
+ Ok(())
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn 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, false);
+ let title = self.get_application_title(&*application.borrow());
+
+ self.write(
+ &format!(
+ "{}\n{}",
+ title,
+ shirabe_php_shim::str_repeat("=", Helper::width(&title) as usize)
+ ),
+ true,
+ );
+
+ for namespace in description.get_namespaces().values() {
+ let namespace_id = match namespace.get("id") {
+ Some(PhpMixed::String(s)) => s.clone(),
+ _ => String::new(),
+ };
+ if ApplicationDescription::GLOBAL_NAMESPACE != namespace_id {
+ self.write("\n\n", true);
+ self.write(&format!("**{}:**", namespace_id), true);
+ }
+
+ self.write("\n\n", true);
+ let command_names: Vec<String> = match namespace.get("commands") {
+ Some(PhpMixed::List(names)) => names
+ .iter()
+ .filter_map(|n| n.as_string().map(|s| s.to_string()))
+ .collect(),
+ _ => vec![],
+ };
+ self.write(
+ &command_names
+ .iter()
+ .map(|command_name| {
+ Ok(format!(
+ "* [`{}`](#{})",
+ command_name.clone(),
+ shirabe_php_shim::str_replace(
+ ":",
+ "",
+ &description
+ .get_command(command_name)?
+ .borrow()
+ .get_name()
+ .unwrap_or_default(),
+ ),
+ ))
+ })
+ .collect::<anyhow::Result<Vec<String>>>()?
+ .join("\n"),
+ true,
+ );
+ }
+
+ for command in description.get_commands().values() {
+ let command = command.borrow();
+ self.write("\n\n", true);
+ // describeCommand returns null; the guarded write never runs.
+ self.describe_command(&*command, options.clone())?;
+ }
+ Ok(())
+ }
+
+ fn get_application_title(&self, application: &dyn Application) -> String {
+ if "UNKNOWN" != application.get_name() {
+ if "UNKNOWN" != application.get_version() {
+ return format!("{} {}", application.get_name(), application.get_version());
+ }
+
+ return application.get_name();
+ }
+
+ "Console Tool".to_string()
+ }
+}
+
+impl DescriptorInterface for MarkdownDescriptor {
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: DescribableObject,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe(self, output, object, options)
+ }
+}
+
+impl Descriptor for MarkdownDescriptor {
+ 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);
+ }
+
+ /// {@inheritdoc}
+ fn write(&self, content: &str, decorated: bool) {
+ // PHP overrides write() only to flip the default of $decorated to true;
+ // it still delegates to parent::write.
+ let _ = decorated;
+ self.output().borrow().write(
+ &[content.to_string()],
+ false,
+ if decorated {
+ crate::output::output_interface::OUTPUT_NORMAL
+ } else {
+ crate::output::output_interface::OUTPUT_RAW
+ },
+ );
+ }
+
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe_input_argument(self, argument, options)
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe_input_option(self, option, options)
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe_input_definition(self, definition, options)
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe_command(self, command, options)
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe_application(self, application, options)
+ }
+}
diff --git a/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs
new file mode 100644
index 00000000..3ea09355
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/text_descriptor.rs
@@ -0,0 +1,592 @@
+//! ref: composer/vendor/symfony/console/Descriptor/TextDescriptor.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::descriptor::application_description::ApplicationDescription;
+use crate::descriptor::descriptor::Descriptor;
+use crate::descriptor::descriptor_interface::{DescribableObject, DescriptorInterface};
+use crate::formatter::output_formatter::OutputFormatter;
+use crate::helper::helper::Helper;
+use crate::input::input_argument::InputArgument;
+use crate::input::input_definition::InputDefinition;
+use crate::input::input_option::InputOption;
+use crate::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_pcre::preg::Preg;
+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(
+ &format!(
+ " <info>{}</info> {}{}{}",
+ argument.get_name(),
+ shirabe_php_shim::str_repeat(" ", spacing_width as usize),
+ // + 4 = 2 spaces before <info>, 2 spaces after </info>
+ Preg::replace(
+ "/\\s*[\\r\\n]\\s*/",
+ &format!(
+ "\n{}",
+ shirabe_php_shim::str_repeat(" ", (total_width + 4) as usize)
+ ),
+ argument.get_description(),
+ ),
+ 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() {
+ format!("-{}, ", option.get_shortcut().unwrap())
+ } else {
+ " ".to_string()
+ },
+ if option.is_negatable() {
+ format!("--{0}|--no-{0}", option.get_name().to_string())
+ } else {
+ format!("--{0}{1}", option.get_name(), value)
+ }
+ );
+
+ let spacing_width = total_width - Helper::width(&synopsis);
+
+ self.write_text(
+ &format!(
+ " <info>{}</info> {}{}{}{}",
+ synopsis,
+ shirabe_php_shim::str_repeat(" ", spacing_width as usize),
+ // + 4 = 2 spaces before <info>, 2 spaces after </info>
+ Preg::replace(
+ "/\\s*[\\r\\n]\\s*/",
+ &format!(
+ "\n{}",
+ shirabe_php_shim::str_repeat(" ", (total_width + 4) as usize)
+ ),
+ option.get_description(),
+ ),
+ default,
+ 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: &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<dyn 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(
+ &format!(
+ "{:<w$} {}",
+ command.get_name().unwrap_or_default(),
+ command.get_description(),
+ w = width as usize,
+ ),
+ &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);
+
+ let app_definition = application.borrow_mut().get_definition();
+ let options_only: Vec<std::rc::Rc<InputOption>> = app_definition
+ .borrow()
+ .get_options()
+ .values()
+ .cloned()
+ .collect();
+ let definition = InputDefinition::from_options(options_only)?;
+ 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
+ && command_keys.contains(name)
+ {
+ merged.push(CommandOrString::String(name.clone()));
+ }
+ }
+ }
+ }
+ merged
+ });
+
+ if let Some(ref described_namespace) = described_namespace {
+ self.write_text(
+ &format!(
+ "<comment>Available commands for the \"{}\" namespace:</comment>",
+ 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 {
+ 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(
+ &format!(
+ " <info>{}</info>{}{}{}",
+ name.clone(),
+ shirabe_php_shim::str_repeat(" ", spacing_width as usize),
+ 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 {
+ *value = 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 {
+ *value = 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(),
+ ))
+ }
+
+ 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
+ }
+ }
+
+ 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
+ + Helper::width(option.get_shortcut().unwrap_or("")).max(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: DescribableObject,
+ 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: &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<dyn Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ TextDescriptor::describe_application(self, application, options)
+ }
+}
diff --git a/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs b/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs
new file mode 100644
index 00000000..11c26b46
--- /dev/null
+++ b/crates/shirabe-symfony-console/src/descriptor/xml_descriptor.rs
@@ -0,0 +1,403 @@
+//! ref: composer/vendor/symfony/console/Descriptor/XmlDescriptor.php
+
+use crate::application::Application;
+use crate::command::command::Command;
+use crate::descriptor::application_description::ApplicationDescription;
+use crate::descriptor::descriptor::Descriptor;
+use crate::descriptor::descriptor_interface::{DescribableObject, DescriptorInterface};
+use crate::input::input_argument::InputArgument;
+use crate::input::input_definition::InputDefinition;
+use crate::input::input_option::InputOption;
+use crate::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_php_shim::{DOMDocument, DOMNode, PhpMixed};
+
+/// XML descriptor.
+///
+/// @internal
+#[derive(Debug, Default)]
+pub struct XmlDescriptor {
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+}
+
+impl XmlDescriptor {
+ pub fn get_input_definition_document(&self, definition: &InputDefinition) -> DOMDocument {
+ let dom = DOMDocument::new("1.0", "UTF-8");
+ let definition_xml = dom.append_child(dom.create_element("definition"));
+
+ let arguments_xml = definition_xml.append_child(dom.create_element("arguments"));
+ for argument in definition.get_arguments().values() {
+ let argument_xml = self.get_input_argument_document(argument);
+ self.append_document(&arguments_xml, &argument_xml.as_node());
+ }
+
+ let options_xml = definition_xml.append_child(dom.create_element("options"));
+ for option in definition.get_options().values() {
+ let option_xml = self.get_input_option_document(option);
+ self.append_document(&options_xml, &option_xml.as_node());
+ }
+
+ dom
+ }
+
+ pub fn get_command_document(&self, command: &dyn Command, short: bool) -> DOMDocument {
+ let dom = DOMDocument::new("1.0", "UTF-8");
+ let command_xml = dom.append_child(dom.create_element("command"));
+
+ let name = command.get_name().unwrap_or_default();
+ command_xml.set_attribute("id", &name);
+ command_xml.set_attribute("name", &name);
+ command_xml.set_attribute("hidden", if command.is_hidden() { "1" } else { "0" });
+
+ let usages_xml = command_xml.append_child(dom.create_element("usages"));
+
+ let description_xml = command_xml.append_child(dom.create_element("description"));
+ description_xml.append_child(dom.create_text_node(&shirabe_php_shim::str_replace(
+ "\n",
+ "\n ",
+ &command.get_description(),
+ )));
+
+ if short {
+ for usage in command.get_aliases() {
+ usages_xml.append_child(dom.create_element_with_value("usage", &usage));
+ }
+ } else {
+ command.merge_application_definition(false);
+
+ let mut usages = vec![command.get_synopsis(false)];
+ usages.extend(command.get_aliases());
+ usages.extend(command.get_usages());
+ for usage in usages {
+ usages_xml.append_child(dom.create_element_with_value("usage", &usage));
+ }
+
+ let help_xml = command_xml.append_child(dom.create_element("help"));
+ help_xml.append_child(dom.create_text_node(&shirabe_php_shim::str_replace(
+ "\n",
+ "\n ",
+ &command.get_processed_help(),
+ )));
+
+ let command_definition = command.get_definition().clone();
+ let definition_xml = self.get_input_definition_document(&command_definition);
+ let definition_node = definition_xml
+ .get_elements_by_tag_name("definition")
+ .item(0)
+ .expect("input definition document always contains a <definition> element");
+ self.append_document(&command_xml, &definition_node);
+ }
+
+ dom
+ }
+
+ pub fn get_application_document(
+ &self,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ namespace: Option<String>,
+ short: bool,
+ ) -> DOMDocument {
+ let dom = DOMDocument::new("1.0", "UTF-8");
+ let root_xml = dom.append_child(dom.create_element("symfony"));
+
+ let app_name = application.borrow().get_name();
+ if app_name != "UNKNOWN" {
+ root_xml.set_attribute("name", &app_name);
+ let app_version = application.borrow().get_version();
+ if app_version != "UNKNOWN" {
+ root_xml.set_attribute("version", &app_version);
+ }
+ }
+
+ let commands_xml = root_xml.append_child(dom.create_element("commands"));
+
+ let mut description =
+ ApplicationDescription::new(application.clone(), namespace.clone(), true);
+
+ if let Some(ref namespace) = namespace {
+ commands_xml.set_attribute("namespace", namespace);
+ }
+
+ for command in description.get_commands().values() {
+ let command = command.borrow();
+ let command_xml = self.get_command_document(&*command, short);
+ self.append_document(&commands_xml, &command_xml.as_node());
+ }
+
+ if namespace.is_none() {
+ let namespaces_xml = root_xml.append_child(dom.create_element("namespaces"));
+
+ let namespaces = description.get_namespaces();
+ for namespace_description in namespaces.values() {
+ let namespace_array_xml =
+ namespaces_xml.append_child(dom.create_element("namespace"));
+ let id = match namespace_description.get("id") {
+ Some(PhpMixed::String(s)) => s.as_str(),
+ _ => "",
+ };
+ namespace_array_xml.set_attribute("id", id);
+
+ if let Some(PhpMixed::List(names)) = namespace_description.get("commands") {
+ for name in names {
+ let command_xml =
+ namespace_array_xml.append_child(dom.create_element("command"));
+ command_xml
+ .append_child(dom.create_text_node(name.as_string().unwrap_or("")));
+ }
+ }
+ }
+ }
+
+ dom
+ }
+
+ fn describe_input_argument(
+ &mut self,
+ argument: &InputArgument,
+ _options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.write_document(self.get_input_argument_document(argument));
+ Ok(())
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ _options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.write_document(self.get_input_option_document(option));
+ Ok(())
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ _options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.write_document(self.get_input_definition_document(definition));
+ Ok(())
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let short = matches!(options.get("short"), Some(PhpMixed::Bool(true)));
+ self.write_document(self.get_command_document(command, short));
+ Ok(())
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ let namespace = match options.get("namespace") {
+ Some(PhpMixed::String(s)) => Some(s.clone()),
+ _ => None,
+ };
+ let short = matches!(options.get("short"), Some(PhpMixed::Bool(true)));
+ self.write_document(self.get_application_document(application, namespace, short));
+ Ok(())
+ }
+
+ /// Appends document children to parent node.
+ fn append_document(&self, parent_node: &DOMNode, imported_parent: &DOMNode) {
+ for child_node in imported_parent.child_nodes() {
+ parent_node.append_child(parent_node.owner_document().import_node(&child_node, true));
+ }
+ }
+
+ /// Writes DOM document.
+ fn write_document(&self, dom: DOMDocument) {
+ dom.set_format_output(true);
+ let mut buf = Vec::new();
+ dom.save_xml(&mut buf)
+ .expect("serializing XML to an in-memory buffer cannot fail");
+ let xml = String::from_utf8(buf).expect("DOM serialization yields valid UTF-8");
+ self.write(&xml, false);
+ }
+
+ fn get_input_argument_document(&self, argument: &InputArgument) -> DOMDocument {
+ let dom = DOMDocument::new("1.0", "UTF-8");
+
+ let object_xml = dom.append_child(dom.create_element("argument"));
+ object_xml.set_attribute("name", argument.get_name());
+ object_xml.set_attribute(
+ "is_required",
+ if argument.is_required() { "1" } else { "0" },
+ );
+ object_xml.set_attribute("is_array", if argument.is_array() { "1" } else { "0" });
+ let description_xml = object_xml.append_child(dom.create_element("description"));
+ description_xml.append_child(dom.create_text_node(argument.get_description()));
+
+ let defaults_xml = object_xml.append_child(dom.create_element("defaults"));
+ let defaults: Vec<String> = match argument.get_default() {
+ PhpMixed::List(_) | PhpMixed::Array(_) => {
+ self.default_values_as_strings(argument.get_default())
+ }
+ PhpMixed::Bool(_) => vec![shirabe_php_shim::var_export(argument.get_default(), true)],
+ d if shirabe_php_shim::php_truthy(d) => {
+ vec![shirabe_php_shim::php_to_string(argument.get_default())]
+ }
+ _ => vec![],
+ };
+ for default in defaults {
+ let default_xml = defaults_xml.append_child(dom.create_element("default"));
+ default_xml.append_child(dom.create_text_node(&default));
+ }
+
+ dom
+ }
+
+ fn get_input_option_document(&self, option: &InputOption) -> DOMDocument {
+ let dom = DOMDocument::new("1.0", "UTF-8");
+
+ let object_xml = dom.append_child(dom.create_element("option"));
+ object_xml.set_attribute("name", &format!("--{}", option.get_name()));
+ let pos = shirabe_php_shim::strpos(option.get_shortcut().unwrap_or(""), "|");
+ if let Some(pos) = pos {
+ object_xml.set_attribute(
+ "shortcut",
+ &format!(
+ "-{}",
+ shirabe_php_shim::substr(option.get_shortcut().unwrap(), 0, Some(pos as i64))
+ ),
+ );
+ object_xml.set_attribute(
+ "shortcuts",
+ &format!(
+ "-{}",
+ shirabe_php_shim::str_replace("|", "|-", option.get_shortcut().unwrap())
+ ),
+ );
+ } else {
+ object_xml.set_attribute(
+ "shortcut",
+ &match option.get_shortcut() {
+ Some(s) => format!("-{}", s),
+ None => String::new(),
+ },
+ );
+ }
+ object_xml.set_attribute(
+ "accept_value",
+ if option.accept_value() { "1" } else { "0" },
+ );
+ object_xml.set_attribute(
+ "is_value_required",
+ if option.is_value_required() { "1" } else { "0" },
+ );
+ object_xml.set_attribute("is_multiple", if option.is_array() { "1" } else { "0" });
+ let description_xml = object_xml.append_child(dom.create_element("description"));
+ description_xml.append_child(dom.create_text_node(option.get_description()));
+
+ if option.accept_value() {
+ let defaults: Vec<String> = match option.get_default() {
+ PhpMixed::List(_) | PhpMixed::Array(_) => {
+ self.default_values_as_strings(option.get_default())
+ }
+ PhpMixed::Bool(_) => vec![shirabe_php_shim::var_export(option.get_default(), true)],
+ d if shirabe_php_shim::php_truthy(d) => {
+ vec![shirabe_php_shim::php_to_string(option.get_default())]
+ }
+ _ => vec![],
+ };
+ let defaults_xml = object_xml.append_child(dom.create_element("defaults"));
+
+ if !defaults.is_empty() {
+ for default in defaults {
+ let default_xml = defaults_xml.append_child(dom.create_element("default"));
+ default_xml.append_child(dom.create_text_node(&default));
+ }
+ }
+ }
+
+ if option.is_negatable() {
+ let object_xml = dom.append_child(dom.create_element("option"));
+ object_xml.set_attribute("name", &format!("--no-{}", option.get_name()));
+ object_xml.set_attribute("shortcut", "");
+ object_xml.set_attribute("accept_value", "0");
+ object_xml.set_attribute("is_value_required", "0");
+ object_xml.set_attribute("is_multiple", "0");
+ let description_xml = object_xml.append_child(dom.create_element("description"));
+ description_xml.append_child(
+ dom.create_text_node(&format!("Negate the \"--{}\" option", option.get_name())),
+ );
+ }
+
+ dom
+ }
+
+ /// Helper used by the default-value branches of getInputArgumentDocument /
+ /// getInputOptionDocument when the default is an array (returns it verbatim).
+ fn default_values_as_strings(&self, default: &PhpMixed) -> Vec<String> {
+ match default {
+ PhpMixed::List(list) => list.iter().map(shirabe_php_shim::php_to_string).collect(),
+ PhpMixed::Array(arr) => arr.values().map(shirabe_php_shim::php_to_string).collect(),
+ _ => vec![],
+ }
+ }
+}
+
+impl DescriptorInterface for XmlDescriptor {
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: DescribableObject,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ Descriptor::describe(self, output, object, options)
+ }
+}
+
+impl Descriptor for XmlDescriptor {
+ 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<()> {
+ XmlDescriptor::describe_input_argument(self, argument, options)
+ }
+
+ fn describe_input_option(
+ &mut self,
+ option: &InputOption,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ XmlDescriptor::describe_input_option(self, option, options)
+ }
+
+ fn describe_input_definition(
+ &mut self,
+ definition: &InputDefinition,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ XmlDescriptor::describe_input_definition(self, definition, options)
+ }
+
+ fn describe_command(
+ &mut self,
+ command: &dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ XmlDescriptor::describe_command(self, command, options)
+ }
+
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<dyn Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ XmlDescriptor::describe_application(self, application, options)
+ }
+}