1: <?php declare(strict_types=1);
2:
3: namespace Salient\Cli;
4:
5: use Salient\Console\Support\ConsoleLoopbackFormat as LoopbackFormat;
6: use Salient\Console\Support\ConsoleManPageFormat as ManPageFormat;
7: use Salient\Console\Support\ConsoleMarkdownFormat as MarkdownFormat;
8: use Salient\Console\ConsoleFormatter as Formatter;
9: use Salient\Contract\Cli\CliHelpSectionName;
10: use Salient\Contract\Cli\CliHelpStyleInterface;
11: use Salient\Contract\Cli\CliHelpTarget;
12: use Salient\Contract\Cli\CliOptionVisibility;
13: use Salient\Contract\Console\ConsoleFormatterInterface as FormatterInterface;
14: use Salient\Core\Concern\HasMutator;
15: use Salient\Core\Facade\Console;
16: use Salient\Utility\Regex;
17: use LogicException;
18:
19: /**
20: * Formatting instructions for help messages
21: *
22: * @api
23: */
24: final class CliHelpStyle implements CliHelpStyleInterface
25: {
26: use HasMutator;
27:
28: private ?int $Width;
29: private FormatterInterface $Formatter;
30: private string $Bold = '';
31: private string $Italic = '';
32: private string $Escape = '\\';
33: private string $SynopsisPrefix = '';
34: private string $SynopsisNewline = "\n ";
35: private string $SynopsisSoftNewline = '';
36: private bool $CollapseSynopsis = false;
37: private string $OptionIndent = ' ';
38: private string $OptionPrefix = '';
39: private string $OptionDescriptionPrefix = "\n ";
40: /** @var int&CliOptionVisibility::* */
41: private int $Visibility = CliOptionVisibility::HELP;
42:
43: // --
44:
45: /** @var CliHelpTarget::* */
46: private int $Target;
47: private bool $HasMarkup = false;
48: private int $Margin = 0;
49:
50: /**
51: * @param CliHelpTarget::* $target
52: */
53: public function __construct(
54: int $target = CliHelpTarget::PLAIN,
55: ?int $width = null,
56: ?FormatterInterface $formatter = null
57: ) {
58: $this->Target = $target;
59: $this->Width = $width;
60:
61: if ($target === CliHelpTarget::PLAIN) {
62: $this->Formatter = $formatter ?? LoopbackFormat::getFormatter();
63: return;
64: }
65:
66: $this->HasMarkup = true;
67: $this->Italic = '_';
68:
69: switch ($target) {
70: case CliHelpTarget::NORMAL:
71: $this->Bold = '__';
72: $this->Width ??= self::getConsoleWidth();
73: $this->Margin = 4;
74: $this->Formatter = $formatter ?? Console::getFormatter();
75: break;
76:
77: case CliHelpTarget::MARKDOWN:
78: $this->Bold = '`';
79: $this->SynopsisNewline = " \\\n\ \ \ \ ";
80: $this->SynopsisSoftNewline = "\n";
81: $this->OptionIndent = ' ';
82: $this->OptionPrefix = '- ';
83: $this->OptionDescriptionPrefix = "\n\n ";
84: $this->Visibility = CliOptionVisibility::MARKDOWN;
85: $this->Formatter = $formatter ?? MarkdownFormat::getFormatter();
86: break;
87:
88: case CliHelpTarget::MAN_PAGE:
89: $this->Bold = '`';
90: // https://pandoc.org/MANUAL.html#line-blocks
91: $this->SynopsisPrefix = '| ';
92: $this->SynopsisNewline = "\n| ";
93: $this->SynopsisSoftNewline = "\n ";
94: // https://pandoc.org/MANUAL.html#definition-lists
95: $this->OptionDescriptionPrefix = "\n\n: ";
96: $this->Visibility = CliOptionVisibility::MAN_PAGE;
97: $this->Formatter = $formatter ?? ManPageFormat::getFormatter();
98: break;
99:
100: default:
101: throw new LogicException(sprintf('Invalid CliHelpTarget: %d', $target));
102: }
103: }
104:
105: /**
106: * @inheritDoc
107: */
108: public function getFormatter(): FormatterInterface
109: {
110: return $this->Formatter;
111: }
112:
113: /**
114: * @inheritDoc
115: */
116: public function getWidth(): ?int
117: {
118: return $this->Width === null
119: ? null
120: : $this->Width - $this->Margin;
121: }
122:
123: /**
124: * @inheritDoc
125: */
126: public function getBold(): string
127: {
128: return $this->Bold;
129: }
130:
131: /**
132: * @inheritDoc
133: */
134: public function getItalic(): string
135: {
136: return $this->Italic;
137: }
138:
139: /**
140: * @inheritDoc
141: */
142: public function getEscape(): string
143: {
144: return $this->Escape;
145: }
146:
147: /**
148: * @inheritDoc
149: */
150: public function getSynopsisPrefix(): string
151: {
152: return $this->SynopsisPrefix;
153: }
154:
155: /**
156: * @inheritDoc
157: */
158: public function getSynopsisNewline(): string
159: {
160: return $this->SynopsisNewline;
161: }
162:
163: /**
164: * @inheritDoc
165: */
166: public function getSynopsisSoftNewline(): string
167: {
168: return $this->SynopsisSoftNewline;
169: }
170:
171: /**
172: * @inheritDoc
173: */
174: public function getCollapseSynopsis(): bool
175: {
176: return $this->CollapseSynopsis;
177: }
178:
179: /**
180: * @inheritDoc
181: */
182: public function getOptionIndent(): string
183: {
184: return $this->OptionIndent;
185: }
186:
187: /**
188: * @inheritDoc
189: */
190: public function getOptionPrefix(): string
191: {
192: return $this->OptionPrefix;
193: }
194:
195: /**
196: * @inheritDoc
197: */
198: public function getOptionDescriptionPrefix(): string
199: {
200: return $this->OptionDescriptionPrefix;
201: }
202:
203: /**
204: * @inheritDoc
205: */
206: public function getVisibility(): int
207: {
208: return $this->Visibility;
209: }
210:
211: /**
212: * @inheritDoc
213: */
214: public function withCollapseSynopsis(bool $value = true)
215: {
216: return $this->with('CollapseSynopsis', $value);
217: }
218:
219: public function prepareHelp(string $text, string $indent = ''): string
220: {
221: $text = $this->Formatter->format(
222: $text,
223: true,
224: $this->Width === null
225: ? null
226: : $this->Width - $this->Margin - strlen($indent),
227: true,
228: );
229:
230: if ($indent !== '') {
231: return $indent . str_replace("\n", "\n" . $indent, $text);
232: }
233:
234: return $text;
235: }
236:
237: /**
238: * @param array<CliHelpSectionName::*|string,string> $sections
239: */
240: public function buildHelp(array $sections): string
241: {
242: $help = '';
243: foreach ($sections as $heading => $content) {
244: $content = rtrim((string) $content);
245:
246: if ($content === '') {
247: continue;
248: }
249:
250: if ($this->Target === CliHelpTarget::NORMAL) {
251: $content = str_replace("\n", "\n ", $content);
252: $help .= "## $heading\n $content\n\n";
253: continue;
254: }
255:
256: $help .= "## $heading\n\n$content\n\n";
257: }
258:
259: return Regex::replace('/^\h++$/m', '', rtrim($help));
260: }
261:
262: public function maybeEscapeTags(string $string): string
263: {
264: if ($this->HasMarkup) {
265: return $string;
266: }
267: return Formatter::escapeTags($string);
268: }
269:
270: public static function getConsoleWidth(): ?int
271: {
272: $width = Console::getWidth();
273:
274: return $width === null
275: ? null
276: : max(76, $width);
277: }
278: }
279: