aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-09 11:19:03 +0900
committernsfisis <nsfisis@gmail.com>2026-08-09 11:19:03 +0900
commit6051927c8fa32cfffa102d2a170c5a6cf747a1b9 (patch)
tree3fe6769c90cb03ca8f1b9b825e38ad459182361e /crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs
parente3e8806aec771e482899ed3470e920f7b291fa95 (diff)
downloadphp-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.tar.gz
php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.tar.zst
php-shirabe-6051927c8fa32cfffa102d2a170c5a6cf747a1b9.zip
refactor(symfony-console): extract symfony/console into the shirabe-symfony-console crate
Move `Symfony\Component\Console` out of shirabe-external-packages and into its own crate, so the path is `shirabe_symfony_console::application::Application` instead of `shirabe_external_packages::symfony::console::application::Application`. The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!` macros move with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs')
-rw-r--r--crates/shirabe-symfony-console/src/descriptor/markdown_descriptor.rs378
1 files changed, 378 insertions, 0 deletions
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)
+ }
+}