aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-external-packages/src/symfony/console/descriptor
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-12 01:01:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-12 01:01:43 +0900
commit1e44f5723e4c0e0903d00a61f254901e612fe5e1 (patch)
tree9c2e1eec364bbf6418a4072ee7767b04c3df0297 /crates/shirabe-external-packages/src/symfony/console/descriptor
parentddf0a624145b618c05c2ee47414545b99b685f37 (diff)
downloadphp-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.gz
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.tar.zst
php-shirabe-1e44f5723e4c0e0903d00a61f254901e612fe5e1.zip
feat(symfony-console): port Symfony Console and make the workspace compile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-external-packages/src/symfony/console/descriptor')
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs185
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs119
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor_interface.rs13
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs426
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs388
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/mod.rs15
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/text_descriptor.rs617
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs428
8 files changed, 2191 insertions, 0 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs
new file mode 100644
index 0000000..3454b78
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/application_description.rs
@@ -0,0 +1,185 @@
+use crate::symfony::console::application::Application;
+use crate::symfony::console::command::command::Command;
+use crate::symfony::console::exception::command_not_found_exception::CommandNotFoundException;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+/// @internal
+#[derive(Debug)]
+pub struct ApplicationDescription {
+ application: Rc<RefCell<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, Rc<RefCell<dyn Command>>>>,
+
+ /// @var array<string, Command>
+ aliases: Option<IndexMap<String, Rc<RefCell<dyn Command>>>>,
+}
+
+impl ApplicationDescription {
+ pub const GLOBAL_NAMESPACE: &'static str = "_global";
+
+ pub fn new(
+ application: Rc<RefCell<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()
+ }
+
+ /// @return Command[]
+ pub fn get_commands(&mut self) -> &IndexMap<String, Rc<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<Rc<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(|n| Box::new(PhpMixed::String(n)))
+ .collect(),
+ ),
+ );
+ self.namespaces.as_mut().unwrap().insert(namespace, entry);
+ }
+ }
+
+ fn sort_commands(
+ &self,
+ commands: IndexMap<String, Rc<RefCell<dyn Command>>>,
+ ) -> IndexMap<String, IndexMap<String, Rc<RefCell<dyn Command>>>> {
+ let mut namespaced_commands: IndexMap<String, IndexMap<String, Rc<RefCell<dyn Command>>>> =
+ IndexMap::new();
+ let mut global_commands: IndexMap<String, Rc<RefCell<dyn Command>>> = IndexMap::new();
+ let mut sorted_commands: IndexMap<String, IndexMap<String, Rc<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_insert_with(IndexMap::new)
+ .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-external-packages/src/symfony/console/descriptor/descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs
new file mode 100644
index 0000000..d4aa575
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor.rs
@@ -0,0 +1,119 @@
+use crate::symfony::console::application::Application;
+use crate::symfony::console::command::command::Command;
+use crate::symfony::console::descriptor::descriptor_interface::DescriptorInterface;
+use crate::symfony::console::exception::invalid_argument_exception::InvalidArgumentException;
+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;
+
+/// @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: PhpMixed,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ self.set_output(output);
+
+ // PHP dispatches via `$object instanceof ...`. The concrete runtime type of
+ // `object` must be recovered to route to the correct describe* method.
+ match true {
+ // case $object instanceof InputArgument:
+ _ if todo!("$object instanceof InputArgument") => {
+ let argument: InputArgument = todo!("downcast object to InputArgument");
+ self.describe_input_argument(&argument, options)?;
+ }
+ // case $object instanceof InputOption:
+ _ if todo!("$object instanceof InputOption") => {
+ let option: InputOption = todo!("downcast object to InputOption");
+ self.describe_input_option(&option, options)?;
+ }
+ // case $object instanceof InputDefinition:
+ _ if todo!("$object instanceof InputDefinition") => {
+ let definition: InputDefinition = todo!("downcast object to InputDefinition");
+ self.describe_input_definition(&definition, options)?;
+ }
+ // case $object instanceof Command:
+ _ if todo!("$object instanceof Command") => {
+ let mut command: Box<dyn Command> = todo!("downcast object to Command");
+ self.describe_command(command.as_mut(), options)?;
+ }
+ // case $object instanceof Application:
+ _ if todo!("$object instanceof Application") => {
+ let application: std::rc::Rc<std::cell::RefCell<Application>> =
+ todo!("downcast object to Application");
+ self.describe_application(application, options)?;
+ }
+ _ => {
+ return Err(
+ InvalidArgumentException(shirabe_php_shim::InvalidArgumentException {
+ message: format!(
+ "Object of type \"{}\" is not describable.",
+ shirabe_php_shim::get_debug_type(&object)
+ ),
+ code: 0,
+ })
+ .into(),
+ );
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Writes content to output.
+ fn write(&self, content: &str, decorated: bool) {
+ self.output().borrow().write(
+ &[content.to_string()],
+ false,
+ if decorated {
+ crate::symfony::console::output::output_interface::OUTPUT_NORMAL
+ } else {
+ crate::symfony::console::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: &mut dyn Command,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+
+ /// Describes an Application instance.
+ fn describe_application(
+ &mut self,
+ application: std::rc::Rc<std::cell::RefCell<Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor_interface.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor_interface.rs
new file mode 100644
index 0000000..9420267
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/descriptor_interface.rs
@@ -0,0 +1,13 @@
+use crate::symfony::console::output::output_interface::OutputInterface;
+use indexmap::IndexMap;
+use shirabe_php_shim::PhpMixed;
+
+/// Descriptor interface.
+pub trait DescriptorInterface {
+ fn describe(
+ &mut self,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ object: PhpMixed,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()>;
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs
new file mode 100644
index 0000000..b651618
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/json_descriptor.rs
@@ -0,0 +1,426 @@
+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::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;
+
+/// 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: &mut 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<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<Box<PhpMixed>> = vec![];
+
+ let short = matches!(options.get("short"), Some(PhpMixed::Bool(true)));
+ let command_list: Vec<_> = description
+ .get_commands()
+ .values()
+ .map(|c| c.borrow().clone_box())
+ .collect();
+ for mut command in command_list {
+ commands.push(Box::new(PhpMixed::Array(
+ self.get_command_data(command.as_mut(), short)?
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .collect(),
+ )));
+ }
+
+ let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
+ if "UNKNOWN" != application.borrow().get_name() {
+ let mut application_data: IndexMap<String, Box<PhpMixed>> = IndexMap::new();
+ application_data.insert(
+ "name".to_string(),
+ Box::new(PhpMixed::String(application.borrow().get_name())),
+ );
+ if "UNKNOWN" != application.borrow().get_version() {
+ application_data.insert(
+ "version".to_string(),
+ Box::new(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| {
+ Box::new(PhpMixed::Array(
+ ns.into_iter().map(|(k, v)| (k, Box::new(v))).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().map(|(k, v)| (k, Box::new(v))).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(
+ "/\\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(
+ "/\\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, Box<PhpMixed>> = IndexMap::new();
+ for (name, argument) in definition.get_arguments() {
+ input_arguments.insert(
+ name.clone(),
+ Box::new(PhpMixed::Array(
+ self.get_input_argument_data(argument)?
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .collect(),
+ )),
+ );
+ }
+
+ let mut input_options: IndexMap<String, Box<PhpMixed>> = IndexMap::new();
+ for (name, option) in definition.get_options() {
+ input_options.insert(
+ name.clone(),
+ Box::new(PhpMixed::Array(
+ self.get_input_option_data(option, false)?
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .collect(),
+ )),
+ );
+ if option.is_negatable() {
+ input_options.insert(
+ format!("no-{}", name),
+ Box::new(PhpMixed::Array(
+ self.get_input_option_data(option, true)?
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .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: &mut 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(|a| Box::new(PhpMixed::String(a)))
+ .collect(),
+ ),
+ );
+ } else {
+ command.merge_application_definition(false);
+
+ let mut usage = vec![Box::new(PhpMixed::String(command.get_synopsis(false)))];
+ usage.extend(
+ command
+ .get_usages()
+ .into_iter()
+ .map(|u| Box::new(PhpMixed::String(u))),
+ );
+ usage.extend(
+ command
+ .get_aliases()
+ .into_iter()
+ .map(|a| Box::new(PhpMixed::String(a))),
+ );
+ 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()
+ .map(|(k, v)| (k, Box::new(v)))
+ .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: PhpMixed,
+ 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: &mut 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<Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ JsonDescriptor::describe_application(self, application, options)
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs
new file mode 100644
index 0000000..5ec5c95
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/markdown_descriptor.rs
@@ -0,0 +1,388 @@
+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::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;
+
+/// 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: PhpMixed,
+ 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().len() > 0;
+ 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().len() > 0 {
+ 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: &mut 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<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(shirabe_php_shim::sprintf(
+ "* [`%s`](#%s)",
+ &[
+ PhpMixed::String(command_name.clone()),
+ PhpMixed::String(shirabe_php_shim::str_replace(
+ ":",
+ "",
+ &description
+ .get_command(command_name)?
+ .borrow()
+ .get_name()
+ .unwrap_or_default(),
+ )),
+ ],
+ ))
+ })
+ .collect::<anyhow::Result<Vec<String>>>()?
+ .join("\n"),
+ true,
+ );
+ }
+
+ let command_list: Vec<_> = description
+ .get_commands()
+ .values()
+ .map(|c| c.borrow().clone_box())
+ .collect();
+ for mut command in command_list {
+ self.write("\n\n", true);
+ // describeCommand returns null; the guarded write never runs.
+ self.describe_command(command.as_mut(), options.clone())?;
+ }
+ Ok(())
+ }
+
+ fn get_application_title(&self, application: &Application) -> String {
+ if "UNKNOWN" != application.get_name() {
+ if "UNKNOWN" != application.get_version() {
+ return shirabe_php_shim::sprintf(
+ "%s %s",
+ &[
+ PhpMixed::String(application.get_name()),
+ PhpMixed::String(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: PhpMixed,
+ 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::symfony::console::output::output_interface::OUTPUT_NORMAL
+ } else {
+ crate::symfony::console::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: &mut 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<Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ MarkdownDescriptor::describe_application(self, application, options)
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/mod.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/mod.rs
new file mode 100644
index 0000000..9f85f92
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/mod.rs
@@ -0,0 +1,15 @@
+pub mod application_description;
+pub mod descriptor;
+pub mod descriptor_interface;
+pub mod json_descriptor;
+pub mod markdown_descriptor;
+pub mod text_descriptor;
+pub mod xml_descriptor;
+
+pub use application_description::*;
+pub use descriptor::*;
+pub use descriptor_interface::*;
+pub use json_descriptor::*;
+pub use markdown_descriptor::*;
+pub use text_descriptor::*;
+pub use xml_descriptor::*;
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)
+ }
+}
diff --git a/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs b/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs
new file mode 100644
index 0000000..209964a
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/descriptor/xml_descriptor.rs
@@ -0,0 +1,428 @@
+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::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;
+
+// NOTE: This descriptor relies on the PHP standard classes \DOMDocument and \DOMNode.
+// No equivalent has been introduced into the project yet, so every DOM operation is left
+// as todo!() pending a decision on how to model the DOM types. The type `DOMDocument` /
+// `DOMNode` placeholders below stand in for those PHP classes.
+
+/// Placeholder for the PHP standard class \DOMDocument.
+#[derive(Debug)]
+pub struct DOMDocument;
+
+/// Placeholder for the PHP standard class \DOMNode.
+#[derive(Debug)]
+pub struct DOMNode;
+
+/// 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 = todo!("new \\DOMDocument('1.0', 'UTF-8')");
+ let definition_xml: DOMNode = todo!("$dom->createElement('definition')");
+ // $dom->appendChild($definitionXML)
+
+ let arguments_xml: DOMNode = todo!("$dom->createElement('arguments')");
+ // $definitionXML->appendChild($argumentsXML)
+ for argument in definition.get_arguments().values() {
+ self.append_document(&arguments_xml, &self.get_input_argument_document(argument));
+ }
+
+ let options_xml: DOMNode = todo!("$dom->createElement('options')");
+ // $definitionXML->appendChild($optionsXML)
+ for option in definition.get_options().values() {
+ self.append_document(&options_xml, &self.get_input_option_document(option));
+ }
+
+ dom
+ }
+
+ pub fn get_command_document(&self, command: &mut dyn Command, short: bool) -> DOMDocument {
+ let dom: DOMDocument = todo!("new \\DOMDocument('1.0', 'UTF-8')");
+ let command_xml: DOMNode = todo!("$dom->createElement('command')");
+ // $dom->appendChild($commandXML)
+
+ // $commandXML->setAttribute('id', $command->getName())
+ let _ = command.get_name();
+ // $commandXML->setAttribute('name', $command->getName())
+ // $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0)
+ let _ = command.is_hidden();
+
+ let usages_xml: DOMNode = todo!("$dom->createElement('usages'); appendChild");
+
+ let _description_xml: DOMNode = todo!("$dom->createElement('description'); appendChild");
+ // $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription())))
+ let _ = shirabe_php_shim::str_replace("\n", "\n ", &command.get_description());
+
+ if short {
+ for usage in command.get_aliases() {
+ let _ = usage;
+ // $usagesXML->appendChild($dom->createElement('usage', $usage))
+ let _ = &usages_xml;
+ todo!("createElement('usage', $usage) and append");
+ }
+ } 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 {
+ let _ = usage;
+ todo!("createElement('usage', $usage) and append");
+ }
+
+ let _help_xml: DOMNode = todo!("$dom->createElement('help'); appendChild");
+ // $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp())))
+ let _ = 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_xml;
+ // $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0))
+ self.append_document(
+ &command_xml,
+ todo!("$definitionXML->getElementsByTagName('definition')->item(0)"),
+ );
+ }
+
+ dom
+ }
+
+ pub fn get_application_document(
+ &self,
+ application: std::rc::Rc<std::cell::RefCell<Application>>,
+ namespace: Option<String>,
+ short: bool,
+ ) -> DOMDocument {
+ let dom: DOMDocument = todo!("new \\DOMDocument('1.0', 'UTF-8')");
+ let root_xml: DOMNode = todo!("$dom->createElement('symfony'); appendChild");
+
+ if "UNKNOWN" != application.borrow().get_name() {
+ // $rootXml->setAttribute('name', $application->getName())
+ if "UNKNOWN" != application.borrow().get_version() {
+ // $rootXml->setAttribute('version', $application->getVersion())
+ }
+ }
+
+ let commands_xml: DOMNode =
+ todo!("$dom->createElement('commands'); appendChild to rootXml");
+
+ let mut description =
+ ApplicationDescription::new(application.clone(), namespace.clone(), true);
+
+ if let Some(ref namespace) = namespace {
+ let _ = namespace;
+ // $commandsXML->setAttribute('namespace', $namespace)
+ }
+
+ let command_list: Vec<_> = description
+ .get_commands()
+ .values()
+ .map(|c| c.borrow().clone_box())
+ .collect();
+ for mut command in command_list {
+ self.append_document(
+ &commands_xml,
+ &self.get_command_document(command.as_mut(), short),
+ );
+ }
+
+ if namespace.is_none() {
+ let _namespaces_xml: DOMNode =
+ todo!("$dom->createElement('namespaces'); appendChild to rootXml");
+ let _ = &root_xml;
+
+ for namespace_description in description.get_namespaces().values() {
+ let _namespace_array_xml: DOMNode =
+ todo!("$dom->createElement('namespace'); append; setAttribute('id', ...)");
+ let _ = match namespace_description.get("id") {
+ Some(PhpMixed::String(s)) => s.clone(),
+ _ => String::new(),
+ };
+
+ if let Some(PhpMixed::List(names)) = namespace_description.get("commands") {
+ for name in names {
+ let _ = name.as_string();
+ // $commandXML = $dom->createElement('command'); append;
+ // $commandXML->appendChild($dom->createTextNode($name))
+ todo!("create command element with text node $name");
+ }
+ }
+ }
+ }
+
+ 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: &mut 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<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.
+ ///
+ /// In PHP `DOMDocument` extends `DOMNode`; the placeholder model accepts a
+ /// `DOMDocument` for `imported_parent` to match the call sites.
+ fn append_document(&self, parent_node: &DOMNode, imported_parent: &DOMDocument) {
+ let _ = (parent_node, imported_parent);
+ // foreach ($importedParent->childNodes as $childNode) {
+ // $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true));
+ // }
+ todo!("DOM import/append of child nodes");
+ }
+
+ /// Writes DOM document.
+ fn write_document(&self, dom: DOMDocument) {
+ // $dom->formatOutput = true;
+ // $this->write($dom->saveXML());
+ let xml: String = todo!("$dom->saveXML() with formatOutput = true");
+ let _ = dom;
+ self.write(&xml, false);
+ }
+
+ fn get_input_argument_document(&self, argument: &InputArgument) -> DOMDocument {
+ let dom: DOMDocument = todo!("new \\DOMDocument('1.0', 'UTF-8')");
+
+ // $objectXML = $dom->createElement('argument'); appendChild
+ // $objectXML->setAttribute('name', $argument->getName())
+ let _ = argument.get_name();
+ // $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0)
+ let _ = argument.is_required();
+ // $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0)
+ let _ = argument.is_array();
+ // $descriptionXML = $dom->createElement('description'); append;
+ // $descriptionXML->appendChild($dom->createTextNode($argument->getDescription()))
+ let _ = argument.get_description();
+
+ // $defaultsXML = $dom->createElement('defaults'); append
+ 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;
+ // $defaultXML = $dom->createElement('default'); append;
+ // $defaultXML->appendChild($dom->createTextNode($default))
+ todo!("create default element with text node");
+ }
+
+ dom
+ }
+
+ fn get_input_option_document(&self, option: &InputOption) -> DOMDocument {
+ let dom: DOMDocument = todo!("new \\DOMDocument('1.0', 'UTF-8')");
+
+ // $objectXML = $dom->createElement('option'); appendChild
+ // $objectXML->setAttribute('name', '--'.$option->getName())
+ let _ = format!("--{}", option.get_name());
+ let pos = shirabe_php_shim::strpos(option.get_shortcut().unwrap_or(""), "|");
+ if let Some(pos) = pos {
+ // $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos))
+ let _ = format!(
+ "-{}",
+ shirabe_php_shim::substr(option.get_shortcut().unwrap(), 0, Some(pos as i64))
+ );
+ // $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut()))
+ let _ = format!(
+ "-{}",
+ shirabe_php_shim::str_replace("|", "|-", option.get_shortcut().unwrap())
+ );
+ } else {
+ // $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : '')
+ let _ = match option.get_shortcut() {
+ Some(s) => format!("-{}", s),
+ None => String::new(),
+ };
+ }
+ // $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0)
+ let _ = option.accept_value();
+ // $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0)
+ let _ = option.is_value_required();
+ // $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0)
+ let _ = option.is_array();
+ // $descriptionXML = $dom->createElement('description'); append;
+ // $descriptionXML->appendChild($dom->createTextNode($option->getDescription()))
+ let _ = 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![],
+ };
+ // $defaultsXML = $dom->createElement('defaults'); append
+
+ if !defaults.is_empty() {
+ for default in defaults {
+ let _ = default;
+ // $defaultXML = $dom->createElement('default'); append;
+ // $defaultXML->appendChild($dom->createTextNode($default))
+ todo!("create default element with text node");
+ }
+ }
+ }
+
+ if option.is_negatable() {
+ // $objectXML = $dom->createElement('option'); $dom->appendChild
+ // $objectXML->setAttribute('name', '--no-'.$option->getName())
+ let _ = format!("--no-{}", option.get_name());
+ // setAttribute('shortcut', ''); setAttribute('accept_value', 0);
+ // setAttribute('is_value_required', 0); setAttribute('is_multiple', 0);
+ // $descriptionXML = $dom->createElement('description'); append;
+ // $descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option'))
+ let _ = 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(|v| shirabe_php_shim::php_to_string(v))
+ .collect(),
+ PhpMixed::Array(arr) => arr
+ .values()
+ .map(|v| shirabe_php_shim::php_to_string(v))
+ .collect(),
+ _ => vec![],
+ }
+ }
+}
+
+impl DescriptorInterface for XmlDescriptor {
+ 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 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: &mut 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<Application>>,
+ options: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<()> {
+ XmlDescriptor::describe_application(self, application, options)
+ }
+}