diff --git a/docs/input/cli/command-help.md b/docs/input/cli/command-help.md index b121bb330..f83eca6c0 100644 --- a/docs/input/cli/command-help.md +++ b/docs/input/cli/command-help.md @@ -15,6 +15,34 @@ The help is also context aware and tailored depending on what has been specified `HelpProvider` is the `Spectre.Console` class responsible for determining context and preparing the help text to write to the console. It is an implementation of the public interface `IHelpProvider`. +## Styling the help text + +Basic styling is applied to the generated help text by default, however this is configurable. + +`HelpProviderStyle` is the `Spectre.Console` class that holds the style information for the help text. + +The default theme shipped with Spectre.Console is provided by a factory method, `HelpProviderStyle.Default`. + +However, you can explicitly set a custom theme when configuring a CommandApp, for example: + +```csharp +config.Settings.HelpProviderStyles = new HelpProviderStyle() +{ + Description = new DescriptionStyle() + { + Header = "bold", + }, +}; +``` + +Removing all styling from help text is also possible, a good choice for ensuring maximum accessibility. This is configured by clearing the style provider entirely: + +```csharp +config.Settings.HelpProviderStyles = null; +``` + +See [Markup](../markup) for information about the use of markup in Spectre.Console, and [Styles](xref:styles) for a listing of supported styles. + ## Custom help providers Whilst it shouldn't be common place to implement your own help provider, it is however possible. diff --git a/examples/Cli/Help/Program.cs b/examples/Cli/Help/Program.cs index 039625eae..aba42b037 100644 --- a/examples/Cli/Help/Program.cs +++ b/examples/Cli/Help/Program.cs @@ -1,4 +1,5 @@ using Spectre.Console.Cli; +using Spectre.Console.Cli.Help; namespace Help; @@ -12,6 +13,9 @@ public static int Main(string[] args) { // Register the custom help provider config.SetHelpProvider(new CustomHelpProvider(config.Settings)); + + // Render an unstyled help text for maximum accessibility + config.Settings.HelpProviderStyles = null; }); return app.Run(args); diff --git a/src/Spectre.Console.Cli/Help/HelpProvider.cs b/src/Spectre.Console.Cli/Help/HelpProvider.cs index 889e5b912..222b3d4ad 100644 --- a/src/Spectre.Console.Cli/Help/HelpProvider.cs +++ b/src/Spectre.Console.Cli/Help/HelpProvider.cs @@ -1,5 +1,3 @@ -using Spectre.Console.Cli.Resources; - namespace Spectre.Console.Cli.Help; /// @@ -10,7 +8,8 @@ namespace Spectre.Console.Cli.Help; /// public class HelpProvider : IHelpProvider { - private HelpProviderResources resources; + private readonly HelpProviderResources resources; + private readonly HelpProviderStyle? helpStyles; /// /// Gets a value indicating how many examples from direct children to show in the help text. @@ -27,6 +26,14 @@ public class HelpProvider : IHelpProvider /// protected virtual bool TrimTrailingPeriod { get; } + /// + /// Gets a value indicating whether to emit the markup styles, inline, when rendering the help text. + /// + /// + /// Useful for unit testing different styling of the same help text. + /// + protected virtual bool RenderMarkupInline { get; } = false; + private sealed class HelpArgument { public string Name { get; } @@ -94,6 +101,11 @@ public static IReadOnlyList Get(ICommandInfo? command, HelpProviderR } } + internal Composer NewComposer() + { + return new Composer(RenderMarkupInline); + } + /// /// Initializes a new instance of the class. /// @@ -104,6 +116,10 @@ public HelpProvider(ICommandAppSettings settings) this.MaximumIndirectExamples = settings.MaximumIndirectExamples; this.TrimTrailingPeriod = settings.TrimTrailingPeriod; + // Don't provide a default style if HelpProviderStyles is null, + // as the user will have explicitly done this to output unstyled help text + this.helpStyles = settings.HelpProviderStyles; + resources = new HelpProviderResources(settings.Culture); } @@ -148,8 +164,8 @@ public virtual IEnumerable GetDescription(ICommandModel model, ICom yield break; } - var composer = new Composer(); - composer.Style("yellow", $"{resources.Description}:").LineBreak(); + var composer = NewComposer(); + composer.Style(helpStyles?.Description?.Header ?? Style.Plain, $"{resources.Description}:").LineBreak(); composer.Text(command.Description).LineBreak(); yield return composer.LineBreak(); } @@ -162,16 +178,16 @@ public virtual IEnumerable GetDescription(ICommandModel model, ICom /// An enumerable collection of objects. public virtual IEnumerable GetUsage(ICommandModel model, ICommandInfo? command) { - var composer = new Composer(); - composer.Style("yellow", $"{resources.Usage}:").LineBreak(); + var composer = NewComposer(); + composer.Style(helpStyles?.Usage?.Header ?? Style.Plain, $"{resources.Usage}:").LineBreak(); composer.Tab().Text(model.ApplicationName); - var parameters = new List(); + var parameters = new List(); if (command == null) { - parameters.Add($"[grey][[{resources.Options}]][/]"); - parameters.Add($"[aqua]<{resources.Command}>[/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]")); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>")); } else { @@ -183,11 +199,11 @@ public virtual IEnumerable GetUsage(ICommandModel model, ICommandIn { if (isCurrent) { - parameters.Add($"[underline]{current.Name.EscapeMarkup()}[/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.CurrentCommand ?? Style.Plain, $"{current.Name}")); } else { - parameters.Add($"{current.Name.EscapeMarkup()}"); + parameters.Add(NewComposer().Text(current.Name)); } } @@ -198,7 +214,7 @@ public virtual IEnumerable GetUsage(ICommandModel model, ICommandIn foreach (var argument in current.Parameters.OfType() .Where(a => a.Required).OrderBy(a => a.Position).ToArray()) { - parameters.Add($"[aqua]<{argument.Value.EscapeMarkup()}>[/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.RequiredArgument ?? Style.Plain, $"<{argument.Value}>")); } } @@ -207,27 +223,27 @@ public virtual IEnumerable GetUsage(ICommandModel model, ICommandIn { foreach (var optionalArgument in optionalArguments) { - parameters.Add($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.OptionalArgument ?? Style.Plain, $"[{optionalArgument.Value}]")); } } } if (isCurrent) { - parameters.Add($"[grey][[{resources.Options}]][/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.Options ?? Style.Plain, $"[{resources.Options}]")); } } if (command.IsBranch && command.DefaultCommand == null) { // The user must specify the command - parameters.Add($"[aqua]<{resources.Command}>[/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"<{resources.Command}>")); } else if (command.IsBranch && command.DefaultCommand != null && command.Commands.Count > 0) { // We are on a branch with a default command // The user can optionally specify the command - parameters.Add($"[aqua][[{resources.Command}]][/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]")); } else if (command.IsDefaultCommand) { @@ -237,7 +253,7 @@ public virtual IEnumerable GetUsage(ICommandModel model, ICommandIn { // Commands other than the default are present // So make these optional in the usage statement - parameters.Add($"[aqua][[{resources.Command}]][/]"); + parameters.Add(NewComposer().Style(helpStyles?.Usage?.Command ?? Style.Plain, $"[{resources.Command}]")); } } } @@ -245,10 +261,7 @@ public virtual IEnumerable GetUsage(ICommandModel model, ICommandIn composer.Join(" ", parameters); composer.LineBreak(); - return new[] - { - composer, - }; + return new[] { composer }; } /// @@ -302,14 +315,14 @@ public virtual IEnumerable GetExamples(ICommandModel model, IComman if (Math.Min(maxExamples, examples.Count) > 0) { - var composer = new Composer(); + var composer = NewComposer(); composer.LineBreak(); - composer.Style("yellow", $"{resources.Examples}:").LineBreak(); + composer.Style(helpStyles?.Examples?.Header ?? Style.Plain, $"{resources.Examples}:").LineBreak(); for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++) { var args = string.Join(" ", examples[index]); - composer.Tab().Text(model.ApplicationName).Space().Style("grey", args); + composer.Tab().Text(model.ApplicationName).Space().Style(helpStyles?.Examples?.Arguments ?? Style.Plain, args); composer.LineBreak(); } @@ -334,11 +347,9 @@ public virtual IEnumerable GetArguments(ICommandModel model, IComma } var result = new List - { - new Markup(Environment.NewLine), - new Markup($"[yellow]{resources.Arguments}:[/]"), - new Markup(Environment.NewLine), - }; + { + NewComposer().LineBreak().Style(helpStyles?.Arguments?.Header ?? Style.Plain, $"{resources.Arguments}:").LineBreak(), + }; var grid = new Grid(); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); @@ -347,15 +358,15 @@ public virtual IEnumerable GetArguments(ICommandModel model, IComma foreach (var argument in arguments.Where(x => x.Required).OrderBy(x => x.Position)) { grid.AddRow( - $"[silver]<{argument.Name.EscapeMarkup()}>[/]", - argument.Description?.TrimEnd('.') ?? " "); + NewComposer().Style(helpStyles?.Arguments?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"), + NewComposer().Text(argument.Description?.TrimEnd('.') ?? " ")); } foreach (var argument in arguments.Where(x => !x.Required).OrderBy(x => x.Position)) { grid.AddRow( - $"[grey][[{argument.Name.EscapeMarkup()}]][/]", - argument.Description?.TrimEnd('.') ?? " "); + NewComposer().Style(helpStyles?.Arguments?.OptionalArgument ?? Style.Plain, $"[{argument.Name}]"), + NewComposer().Text(argument.Description?.TrimEnd('.') ?? " ")); } result.Add(grid); @@ -379,11 +390,9 @@ public virtual IEnumerable GetOptions(ICommandModel model, ICommand } var result = new List - { - new Markup(Environment.NewLine), - new Markup($"[yellow]{resources.Options}:[/]"), - new Markup(Environment.NewLine), - }; + { + NewComposer().LineBreak().Style(helpStyles?.Options?.Header ?? Style.Plain, $"{resources.Options}:").LineBreak(), + }; var helpOptions = parameters.ToArray(); var defaultValueColumn = ShowOptionDefaultValues && helpOptions.Any(e => e.DefaultValue != null); @@ -397,78 +406,85 @@ public virtual IEnumerable GetOptions(ICommandModel model, ICommand grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) }); - static string GetOptionParts(HelpOption option) + if (defaultValueColumn) + { + grid.AddRow( + NewComposer().Space(), + NewComposer().Style(helpStyles?.Options?.DefaultValueHeader ?? Style.Plain, resources.Default), + NewComposer().Space()); + } + + foreach (var option in helpOptions) { - var builder = new StringBuilder(); - if (option.Short != null) - { - builder.Append('-').Append(option.Short.EscapeMarkup()); - if (option.Long != null) - { - builder.Append(", "); - } - } - else + var columns = new List() { GetOptionParts(option) }; + + if (defaultValueColumn) { - builder.Append(" "); - if (option.Long != null) - { - builder.Append(" "); - } + columns.Add(GetOptionDefaultValue(option.DefaultValue)); } + columns.Add(NewComposer().Text(option.Description?.TrimEnd('.') ?? " ")); + + grid.AddRow(columns.ToArray()); + } + + result.Add(grid); + + return result; + } + + private IRenderable GetOptionParts(HelpOption option) + { + var composer = NewComposer(); + + if (option.Short != null) + { + composer.Text("-").Text(option.Short); if (option.Long != null) { - builder.Append("--").Append(option.Long.EscapeMarkup()); + composer.Text(", "); } - - if (option.Value != null) + } + else + { + composer.Text(" "); + if (option.Long != null) { - builder.Append(' '); - if (option.ValueIsOptional ?? false) - { - builder.Append("[grey][[").Append(option.Value.EscapeMarkup()).Append("]][/]"); - } - else - { - builder.Append("[silver]<").Append(option.Value.EscapeMarkup()).Append(">[/]"); - } + composer.Text(" "); } - - return builder.ToString(); } - if (defaultValueColumn) + if (option.Long != null) { - grid.AddRow(" ", $"[lime]{resources.Default}[/]", " "); + composer.Text("--").Text(option.Long); } - foreach (var option in helpOptions) + if (option.Value != null) { - var columns = new List { GetOptionParts(option) }; - if (defaultValueColumn) + composer.Text(" "); + if (option.ValueIsOptional ?? false) { - static string Bold(object obj) => $"[bold]{obj.ToString().EscapeMarkup()}[/]"; - - var defaultValue = option.DefaultValue switch - { - null => " ", - "" => " ", - Array { Length: 0 } => " ", - Array array => string.Join(", ", array.Cast().Select(Bold)), - _ => Bold(option.DefaultValue), - }; - columns.Add(defaultValue); + composer.Style(helpStyles?.Options?.OptionalOption ?? Style.Plain, $"[{option.Value}]"); + } + else + { + composer.Style(helpStyles?.Options?.RequiredOption ?? Style.Plain, $"<{option.Value}>"); } - - columns.Add(option.Description?.TrimEnd('.') ?? " "); - - grid.AddRow(columns.ToArray()); } - result.Add(grid); + return composer; + } - return result; + private IRenderable GetOptionDefaultValue(object? defaultValue) + { + return defaultValue switch + { + null => NewComposer().Text(" "), + "" => NewComposer().Text(" "), + Array { Length: 0 } => NewComposer().Text(" "), + Array array => NewComposer().Join(", ", array.Cast().Select(o => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, o.ToString() ?? string.Empty))), + _ => NewComposer().Style(helpStyles?.Options?.DefaultValue ?? Style.Plain, defaultValue?.ToString() ?? string.Empty), + }; } /// @@ -491,11 +507,9 @@ public virtual IEnumerable GetCommands(ICommandModel model, IComman } var result = new List - { - new Markup(Environment.NewLine), - new Markup($"[yellow]{resources.Commands}:[/]"), - new Markup(Environment.NewLine), - }; + { + NewComposer().LineBreak().Style(helpStyles?.Commands?.Header ?? Style.Plain, $"{resources.Commands}:").LineBreak(), + }; var grid = new Grid(); grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); @@ -503,27 +517,27 @@ public virtual IEnumerable GetCommands(ICommandModel model, IComman foreach (var child in commands) { - var arguments = new Composer(); - arguments.Style("silver", child.Name.EscapeMarkup()); + var arguments = NewComposer(); + arguments.Style(helpStyles?.Commands?.ChildCommand ?? Style.Plain, child.Name); arguments.Space(); foreach (var argument in HelpArgument.Get(child).Where(a => a.Required)) { - arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>"); + arguments.Style(helpStyles?.Commands?.RequiredArgument ?? Style.Plain, $"<{argument.Name}>"); arguments.Space(); } if (TrimTrailingPeriod) { grid.AddRow( - arguments.ToString().TrimEnd(), - child.Description?.TrimEnd('.') ?? " "); + NewComposer().Text(arguments.ToString().TrimEnd()), + NewComposer().Text(child.Description?.TrimEnd('.') ?? " ")); } else { grid.AddRow( - arguments.ToString().TrimEnd(), - child.Description ?? " "); + NewComposer().Text(arguments.ToString().TrimEnd()), + NewComposer().Text(child.Description ?? " ")); } } diff --git a/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs b/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs new file mode 100644 index 000000000..a4dee8137 --- /dev/null +++ b/src/Spectre.Console.Cli/Help/HelpProviderStyles.cs @@ -0,0 +1,219 @@ +namespace Spectre.Console.Cli.Help; + +/// +/// Styles for the HelpProvider to use when rendering help text. +/// +public sealed class HelpProviderStyle +{ + /// + /// Gets or sets the style for describing the purpose or details of a command. + /// + public DescriptionStyle? Description { get; set; } + + /// + /// Gets or sets the style for specifying the usage format of a command. + /// + public UsageStyle? Usage { get; set; } + + /// + /// Gets or sets the style for providing examples of command usage. + /// + public ExampleStyle? Examples { get; set; } + + /// + /// Gets or sets the style for specifying arguments in a command. + /// + public ArgumentStyle? Arguments { get; set; } + + /// + /// Gets or sets the style for specifying options or flags in a command. + /// + public OptionStyle? Options { get; set; } + + /// + /// Gets or sets the style for specifying subcommands or nested commands. + /// + public CommandStyle? Commands { get; set; } + + /// + /// Gets the default HelpProvider styles. + /// + public static HelpProviderStyle Default { get; } = + new HelpProviderStyle() + { + Description = new DescriptionStyle() + { + Header = "yellow", + }, + Usage = new UsageStyle() + { + Header = "yellow", + CurrentCommand = "underline", + Command = "aqua", + Options = "grey", + RequiredArgument = "aqua", + OptionalArgument = "silver", + }, + Examples = new ExampleStyle() + { + Header = "yellow", + Arguments = "grey", + }, + Arguments = new ArgumentStyle() + { + Header = "yellow", + RequiredArgument = "silver", + OptionalArgument = "silver", + }, + Commands = new CommandStyle() + { + Header = "yellow", + ChildCommand = "silver", + RequiredArgument = "silver", + }, + Options = new OptionStyle() + { + Header = "yellow", + DefaultValueHeader = "lime", + DefaultValue = "bold", + RequiredOption = "silver", + OptionalOption = "grey", + }, + }; +} + +/// +/// Defines styles for describing the purpose or details of a command. +/// +public sealed class DescriptionStyle +{ + /// + /// Gets or sets the style for the header in the description. + /// + public Style? Header { get; set; } +} + +/// +/// Defines styles for specifying the usage format of a command. +/// +public sealed class UsageStyle +{ + /// + /// Gets or sets the style for the header in the usage. + /// + public Style? Header { get; set; } + + /// + /// Gets or sets the style for the current command in the usage. + /// + public Style? CurrentCommand { get; set; } + + /// + /// Gets or sets the style for general commands in the usage. + /// + public Style? Command { get; set; } + + /// + /// Gets or sets the style for options in the usage. + /// + public Style? Options { get; set; } + + /// + /// Gets or sets the style for required arguments in the usage. + /// + public Style? RequiredArgument { get; set; } + + /// + /// Gets or sets the style for optional arguments in the usage. + /// + public Style? OptionalArgument { get; set; } +} + +/// +/// Defines styles for providing examples of command usage. +/// +public sealed class ExampleStyle +{ + /// + /// Gets or sets the style for the header in the examples. + /// + public Style? Header { get; set; } + + /// + /// Gets or sets the style for arguments in the examples. + /// + public Style? Arguments { get; set; } +} + +/// +/// Defines styles for specifying arguments in a command. +/// +public sealed class ArgumentStyle +{ + /// + /// Gets or sets the style for the header in the arguments. + /// + public Style? Header { get; set; } + + /// + /// Gets or sets the style for required arguments. + /// + public Style? RequiredArgument { get; set; } + + /// + /// Gets or sets the style for optional arguments. + /// + public Style? OptionalArgument { get; set; } +} + +/// +/// Defines styles for specifying subcommands or nested commands. +/// +public sealed class CommandStyle +{ + /// + /// Gets or sets the style for the header in the command section. + /// + public Style? Header { get; set; } + + /// + /// Gets or sets the style for child commands in the command section. + /// + public Style? ChildCommand { get; set; } + + /// + /// Gets or sets the style for required arguments in the command section. + /// + public Style? RequiredArgument { get; set; } +} + +/// +/// Defines styles for specifying options or flags in a command. +/// +public sealed class OptionStyle +{ + /// + /// Gets or sets the style for the header in the options. + /// + public Style? Header { get; set; } + + /// + /// Gets or sets the style for the header of default values in the options. + /// + public Style? DefaultValueHeader { get; set; } + + /// + /// Gets or sets the style for default values in the options. + /// + public Style? DefaultValue { get; set; } + + /// + /// Gets or sets the style for required options. + /// + public Style? RequiredOption { get; set; } + + /// + /// Gets or sets the style for optional options. + /// + public Style? OptionalOption { get; set; } +} diff --git a/src/Spectre.Console.Cli/ICommandAppSettings.cs b/src/Spectre.Console.Cli/ICommandAppSettings.cs index aaed45496..d9ca65609 100644 --- a/src/Spectre.Console.Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console.Cli/ICommandAppSettings.cs @@ -41,6 +41,11 @@ public interface ICommandAppSettings /// bool TrimTrailingPeriod { get; set; } + /// + /// Gets or sets the styles to used when rendering the help text. + /// + HelpProviderStyle? HelpProviderStyles { get; set; } + /// /// Gets or sets the . /// diff --git a/src/Spectre.Console.Cli/Internal/Composer.cs b/src/Spectre.Console.Cli/Internal/Composer.cs index e692e90ce..0032f1458 100644 --- a/src/Spectre.Console.Cli/Internal/Composer.cs +++ b/src/Spectre.Console.Cli/Internal/Composer.cs @@ -4,22 +4,43 @@ internal sealed class Composer : IRenderable { private readonly StringBuilder _content; + /// + /// Whether to emit the markup styles, inline, when rendering the content. + /// + private readonly bool _renderMarkup = false; + public Composer() { _content = new StringBuilder(); } + public Composer(bool renderMarkup) + : this() + { + _renderMarkup = renderMarkup; + } + public Composer Text(string text) { _content.Append(text); return this; } + public Composer Style(Style style, string text) + { + _content.Append('[').Append(style.ToMarkup()).Append(']'); + _content.Append(text.EscapeMarkup()); + _content.Append("[/]"); + + return this; + } + public Composer Style(string style, string text) { _content.Append('[').Append(style).Append(']'); _content.Append(text.EscapeMarkup()); _content.Append("[/]"); + return this; } @@ -28,6 +49,7 @@ public Composer Style(string style, Action action) _content.Append('[').Append(style).Append(']'); action(this); _content.Append("[/]"); + return this; } @@ -72,12 +94,19 @@ public Composer LineBreaks(int count) return this; } - public Composer Join(string separator, IEnumerable composers) + public Composer Join(string separator, IEnumerable composers) { if (composers != null) { - Space(); - Text(string.Join(separator, composers)); + foreach (var composer in composers) + { + if (_content.ToString().Length > 0) + { + Text(separator); + } + + Text(composer.ToString()); + } } return this; @@ -85,12 +114,26 @@ public Composer Join(string separator, IEnumerable composers) public Measurement Measure(RenderOptions options, int maxWidth) { - return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth); + if (_renderMarkup) + { + return ((IRenderable)new Paragraph(_content.ToString())).Measure(options, maxWidth); + } + else + { + return ((IRenderable)new Markup(_content.ToString())).Measure(options, maxWidth); + } } public IEnumerable Render(RenderOptions options, int maxWidth) { - return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth); + if (_renderMarkup) + { + return ((IRenderable)new Paragraph(_content.ToString())).Render(options, maxWidth); + } + else + { + return ((IRenderable)new Markup(_content.ToString())).Render(options, maxWidth); + } } public override string ToString() diff --git a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs index c14d6a901..91b51a498 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs @@ -14,9 +14,10 @@ internal sealed class CommandAppSettings : ICommandAppSettings public CaseSensitivity CaseSensitivity { get; set; } public bool PropagateExceptions { get; set; } public bool ValidateExamples { get; set; } - public bool TrimTrailingPeriod { get; set; } = true; + public bool TrimTrailingPeriod { get; set; } + public HelpProviderStyle? HelpProviderStyles { get; set; } public bool StrictParsing { get; set; } - public bool ConvertFlagsToRemainingArguments { get; set; } = false; + public bool ConvertFlagsToRemainingArguments { get; set; } public ParsingMode ParsingMode => StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; @@ -29,6 +30,9 @@ public CommandAppSettings(ITypeRegistrar registrar) CaseSensitivity = CaseSensitivity.All; ShowOptionDefaultValues = true; MaximumIndirectExamples = 5; + TrimTrailingPeriod = true; + HelpProviderStyles = HelpProviderStyle.Default; + ConvertFlagsToRemainingArguments = false; } public bool IsTrue(Func func, string environmentVariableName) diff --git a/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs b/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs index cc4646441..20583e184 100644 --- a/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs +++ b/src/Spectre.Console/Internal/Text/Markup/MarkupParser.cs @@ -26,7 +26,7 @@ public static Paragraph Parse(string text, Style? style = null) if (token.Kind == MarkupTokenKind.Open) { - var parsedStyle = StyleParser.Parse(token.Value); + var parsedStyle = string.IsNullOrEmpty(token.Value) ? Style.Plain : StyleParser.Parse(token.Value); stack.Push(parsedStyle); } else if (token.Kind == MarkupTokenKind.Close) diff --git a/test/Spectre.Console.Cli.Tests/Data/Help/RenderMarkupHelpProvider.cs b/test/Spectre.Console.Cli.Tests/Data/Help/RenderMarkupHelpProvider.cs new file mode 100644 index 000000000..a2941bfaf --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Data/Help/RenderMarkupHelpProvider.cs @@ -0,0 +1,11 @@ +namespace Spectre.Console.Cli.Tests.Data.Help; + +internal class RenderMarkupHelpProvider : HelpProvider +{ + protected override bool RenderMarkupInline { get; } = true; + + public RenderMarkupHelpProvider(ICommandAppSettings settings) + : base(settings) + { + } +} diff --git a/test/Spectre.Console.Cli.Tests/Data/Settings/LionSettings.cs b/test/Spectre.Console.Cli.Tests/Data/Settings/LionSettings.cs index ea740b5be..543ea3541 100644 --- a/test/Spectre.Console.Cli.Tests/Data/Settings/LionSettings.cs +++ b/test/Spectre.Console.Cli.Tests/Data/Settings/LionSettings.cs @@ -14,4 +14,8 @@ public class LionSettings : CatSettings [Description("The days the lion goes hunting.")] [DefaultValue(new[] { DayOfWeek.Monday, DayOfWeek.Thursday })] public DayOfWeek[] HuntDays { get; set; } + + [CommandOption("-w|--weight [WEIGHT]")] + [Description("The weight of the lion, in kgs.")] + public FlagValue Weight { get; set; } } diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt index aea49d4d1..aa1978d8b 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default.Output.verified.txt @@ -16,4 +16,5 @@ OPTIONS: -n, --name --agility 10 The agility between 0 and 100 -c The number of children the lion has - -d Monday, Thursday The days the lion goes hunting \ No newline at end of file + -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt index aea49d4d1..aa1978d8b 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args.Output.verified.txt @@ -16,4 +16,5 @@ OPTIONS: -n, --name --agility 10 The agility between 0 and 100 -c The number of children the lion has - -d Monday, Thursday The days the lion goes hunting \ No newline at end of file + -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_DE.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_DE.verified.txt index f4a58f4a1..ba0602d87 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_DE.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_DE.verified.txt @@ -20,6 +20,7 @@ OPTIONEN: --agility 10 The agility between 0 and 100 -c The number of children the lion has -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs KOMMANDOS: giraffe The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_EN.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_EN.verified.txt index 6f69b1f2c..d4a593379 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_EN.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_EN.verified.txt @@ -20,6 +20,7 @@ OPTIONS: --agility 10 The agility between 0 and 100 -c The number of children the lion has -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs COMMANDS: giraffe The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_FR.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_FR.verified.txt index 9af442473..126de22d6 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_FR.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_FR.verified.txt @@ -20,6 +20,7 @@ OPTIONS: --agility 10 The agility between 0 and 100 -c The number of children the lion has -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs COMMANDES: giraffe The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_SV.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_SV.verified.txt index 918755998..2292492b8 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_SV.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional.Output_SV.verified.txt @@ -20,6 +20,7 @@ VAL: --agility 10 The agility between 0 and 100 -c The number of children the lion has -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs KOMMANDON: giraffe The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_BoldHeadings.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_BoldHeadings.Output.verified.txt new file mode 100644 index 000000000..4bea738da --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_BoldHeadings.Output.verified.txt @@ -0,0 +1,26 @@ +[bold]DESCRIPTION:[/] +The lion command. + +[bold]USAGE:[/] + myapp [][/] [][[LEGS]][/] [][[OPTIONS]][/] [][[COMMAND]][/] + +[bold]EXAMPLES:[/] + myapp []20 --alive[/] + +[bold]ARGUMENTS:[/] + [][/] The number of teeth the lion has + [][[LEGS]][/] The number of legs + +[]OPTIONS:[/] + []DEFAULT[/] + -h, --help Prints help information + -v, --version Prints version information + -a, --alive Indicates whether or not the animal is alive + -n, --name [][/] + --agility [][/] []10[/] The agility between 0 and 100 + -c [][/] The number of children the lion has + -d [][/] []Monday[/], []Thursday[/] The days the lion goes hunting + -w, --weight [][[WEIGHT]][/] The weight of the lion, in kgs + +[bold]COMMANDS:[/] + []giraffe[/] [][/] The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_Default.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_Default.Output.verified.txt new file mode 100644 index 000000000..30421ace0 --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_Default.Output.verified.txt @@ -0,0 +1,26 @@ +[yellow]DESCRIPTION:[/] +The lion command. + +[yellow]USAGE:[/] + myapp [aqua][/] [silver][[LEGS]][/] [grey][[OPTIONS]][/] [aqua][[COMMAND]][/] + +[yellow]EXAMPLES:[/] + myapp [grey]20 --alive[/] + +[yellow]ARGUMENTS:[/] + [silver][/] The number of teeth the lion has + [silver][[LEGS]][/] The number of legs + +[yellow]OPTIONS:[/] + [lime]DEFAULT[/] + -h, --help Prints help information + -v, --version Prints version information + -a, --alive Indicates whether or not the animal is alive + -n, --name [silver][/] + --agility [silver][/] [bold]10[/] The agility between 0 and 100 + -c [silver][/] The number of children the lion has + -d [silver][/] [bold]Monday[/], [bold]Thursday[/] The days the lion goes hunting + -w, --weight [grey][[WEIGHT]][/] The weight of the lion, in kgs + +[yellow]COMMANDS:[/] + [silver]giraffe[/] [silver][/] The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_None.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_None.Output.verified.txt new file mode 100644 index 000000000..a0bb7b25a --- /dev/null +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Default_Without_Args_Additional_Style_None.Output.verified.txt @@ -0,0 +1,26 @@ +[]DESCRIPTION:[/] +The lion command. + +[]USAGE:[/] + myapp [][/] [][[LEGS]][/] [][[OPTIONS]][/] [][[COMMAND]][/] + +[]EXAMPLES:[/] + myapp []20 --alive[/] + +[]ARGUMENTS:[/] + [][/] The number of teeth the lion has + [][[LEGS]][/] The number of legs + +[]OPTIONS:[/] + []DEFAULT[/] + -h, --help Prints help information + -v, --version Prints version information + -a, --alive Indicates whether or not the animal is alive + -n, --name [][/] + --agility [][/] []10[/] The agility between 0 and 100 + -c [][/] The number of children the lion has + -d [][/] []Monday[/], []Thursday[/] The days the lion goes hunting + -w, --weight [][[WEIGHT]][/] The weight of the lion, in kgs + +[]COMMANDS:[/] + []giraffe[/] [][/] The giraffe command \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Expectations/Help/Leaf.Output.verified.txt b/test/Spectre.Console.Cli.Tests/Expectations/Help/Leaf.Output.verified.txt index 9083b60a0..3134e99e7 100644 --- a/test/Spectre.Console.Cli.Tests/Expectations/Help/Leaf.Output.verified.txt +++ b/test/Spectre.Console.Cli.Tests/Expectations/Help/Leaf.Output.verified.txt @@ -1,4 +1,4 @@ -DESCRIPTION: +DESCRIPTION: The lion command. USAGE: @@ -8,7 +8,8 @@ ARGUMENTS: The number of teeth the lion has OPTIONS: - DEFAULT - -h, --help Prints help information - -c The number of children the lion has - -d Monday, Thursday The days the lion goes hunting \ No newline at end of file + DEFAULT + -h, --help Prints help information + -c The number of children the lion has + -d Monday, Thursday The days the lion goes hunting + -w, --weight [WEIGHT] The weight of the lion, in kgs \ No newline at end of file diff --git a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs index c6aa10487..82621630d 100644 --- a/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs +++ b/test/Spectre.Console.Cli.Tests/Unit/CommandAppTests.Help.cs @@ -264,6 +264,101 @@ public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_C return Verifier.Verify(result.Output, settings).UseTextForParameters(expectationPrefix); } + [Fact] + [Expectation("Default_Without_Args_Additional_Style_Default")] + public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Style_Default() + { + // Given + var fixture = new CommandAppTester(); + fixture.SetDefaultCommand(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddExample("20", "--alive"); + configurator.AddCommand("giraffe"); + configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings)); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Default_Without_Args_Additional_Style_BoldHeadings")] + public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Style_BoldHeadings() + { + // Bold headings in the help text + var styles = new HelpProviderStyle() + { + Description = new DescriptionStyle() + { + Header = "bold", + }, + Usage = new UsageStyle() + { + Header = "bold", + }, + Examples = new ExampleStyle() + { + Header = "bold", + }, + Arguments = new ArgumentStyle() + { + Header = "bold", + }, + Commands = new CommandStyle() + { + Header = "bold", + }, + + // Omit OptionStyle to ensure coverage of at least one null style class + }; + + // Given + var fixture = new CommandAppTester(); + fixture.SetDefaultCommand(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddExample("20", "--alive"); + configurator.AddCommand("giraffe"); + configurator.Settings.HelpProviderStyles = styles; + configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings)); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } + + [Fact] + [Expectation("Default_Without_Args_Additional_Style_None")] + public Task Should_Output_Default_Command_And_Additional_Commands_When_Default_Command_Has_Required_Parameters_And_Is_Called_Without_Args_Style_None() + { + // Given + var fixture = new CommandAppTester(); + fixture.SetDefaultCommand(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddExample("20", "--alive"); + configurator.AddCommand("giraffe"); + configurator.Settings.HelpProviderStyles = null; + configurator.SetHelpProvider(new RenderMarkupHelpProvider(configurator.Settings)); + }); + + // When + var result = fixture.Run(); + + // Then + return Verifier.Verify(result.Output); + } + [Fact] [Expectation("Default_Greeter")] public Task Should_Not_Output_Default_Command_When_Command_Has_No_Required_Parameters_And_Is_Called_Without_Args()