Skip to content

Commit f7d2d86

Browse files
committed
Merge branch 'improve-eol-handling'
2 parents ee820cd + c5b67c9 commit f7d2d86

File tree

12 files changed

+277
-78
lines changed

12 files changed

+277
-78
lines changed

src/Cli/CliApplication.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,12 @@ private function generateHelp(string $name, $node, int $target, string ...$args)
456456
$formats = ConsoleManPageFormat::getTagFormats();
457457
$progName = $this->getProgramName();
458458
printf(
459-
"%% %s(%d) %s | %s\n\n",
459+
'%% %s(%d) %s | %s%s',
460460
Str::upper(str_replace(' ', '-', trim("$progName $name"))),
461461
(int) ($args[0] ?? '1'),
462462
$args[1] ?? Package::version(),
463463
$args[2] ?? (($name === '' ? $progName : Package::name()) . ' Documentation'),
464+
\PHP_EOL . \PHP_EOL,
464465
);
465466
break;
466467

@@ -477,6 +478,7 @@ private function generateHelp(string $name, $node, int $target, string ...$args)
477478

478479
$usage = $this->getHelp($name, $node, $style);
479480
$usage = $formatter->formatTags($usage);
480-
printf("%s\n", str_replace('\ ', ' ', $usage));
481+
$usage = Str::eolToNative($usage);
482+
printf('%s%s', str_replace('\ ', ' ', $usage), \PHP_EOL);
481483
}
482484
}

src/Cli/CliOption.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ public function getSummary(bool $withFullStop = true): ?string
749749
if ($desc === '') {
750750
return null;
751751
}
752-
if ($withFullStop && $desc[-1] !== '.') {
752+
if ($withFullStop && strpbrk($desc[-1], '.!?') === false) {
753753
$desc .= '.';
754754
}
755755
return Pcre::replace('/\s+/', ' ', $desc);

src/Cli/Support/CliHelpStyle.php

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -208,22 +208,11 @@ public function buildHelp(array $sections): string
208208

209209
if ($this->Target === CliHelpTarget::TTY) {
210210
$content = str_replace("\n", "\n ", $content);
211-
$help .= <<<EOF
212-
## $heading
213-
$content
214-
215-
216-
EOF;
211+
$help .= "## $heading\n $content\n\n";
217212
continue;
218213
}
219214

220-
$help .= <<<EOF
221-
## $heading
222-
223-
$content
224-
225-
226-
EOF;
215+
$help .= "## $heading\n\n$content\n\n";
227216
}
228217

229218
return Pcre::replace('/^\h++$/m', '', rtrim($help));

src/Console/ConsoleFormatter.php

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Lkrms\Console\Support\ConsoleTagAttributes as TagAttributes;
1616
use Lkrms\Console\Support\ConsoleTagFormats as TagFormats;
1717
use Lkrms\Exception\UnexpectedValueException;
18-
use Lkrms\Support\Catalog\RegularExpression as Regex;
1918
use Lkrms\Utility\Convert;
2019
use Lkrms\Utility\Pcre;
2120
use Lkrms\Utility\Str;
@@ -66,8 +65,8 @@ final class ConsoleFormatter
6665
* Splits the subject into formattable paragraphs, fenced code blocks and
6766
* code spans
6867
*/
69-
private const PARSER_REGEX = <<<'REGEX'
70-
(?msx)
68+
private const MARKUP = <<<'REGEX'
69+
/
7170
(?(DEFINE)
7271
(?<endofline> \h*+ \n )
7372
(?<endofblock> ^ \k<indent> \k<fence> \h*+ $ )
@@ -85,7 +84,7 @@ final class ConsoleFormatter
8584
(?<breaks> (?&endofline)+ ) |
8685
# Everything except unescaped backticks until the start of the next
8786
# paragraph
88-
(?<text> (?> (?: [^\\`\n]+ | \\ [-\\!"\#$%&'()*+,./:;<=>?@[\]^_`{|}~\n] | \\ | \n (?! (?&endofline) ) )+ (?&endofline)* ) ) |
87+
(?<text> (?> (?: [^\\`\n]+ | \\ [-\\!"\#$%&'()*+,.\/:;<=>?@[\]^_`{|}~\n] | \\ | \n (?! (?&endofline) ) )+ (?&endofline)* ) ) |
8988
# CommonMark-compliant fenced code blocks
9089
(?> (?(indent)
9190
(?> (?<fence> ```+ ) (?<infostring> [^\n]* ) \n )
@@ -100,39 +99,39 @@ final class ConsoleFormatter
10099
# Unmatched backticks
101100
(?<extra> `+ ) |
102101
\z
103-
)
102+
) /mxs
104103
REGEX;
105104

106105
/**
107106
* Matches inline formatting tags used outside fenced code blocks and code
108107
* spans
109108
*/
110-
private const TAG_REGEX = <<<'REGEX'
111-
(?xm)
109+
private const TAG = <<<'REGEX'
110+
/
112111
(?(DEFINE)
113-
(?<esc> \\ [-\\!"\#$%&'()*+,./:;<=>?@[\]^_`{|}~] | \\ )
112+
(?<esc> \\ [-\\!"\#$%&'()*+,.\/:;<=>?@[\]^_`{|}~] | \\ )
114113
)
115114
(?<! \\ ) (?: \\\\ )* \K (?|
116115
\b (?<tag> _ {1,3}+ ) (?! \s ) (?> (?<text> (?: [^_\\]+ | (?&esc) | (?! (?<! \s ) \k<tag> \b ) _ + )* ) ) (?<! \s ) \k<tag> \b |
117116
(?<tag> \* {1,3}+ ) (?! \s ) (?> (?<text> (?: [^*\\]+ | (?&esc) | (?! (?<! \s ) \k<tag> ) \* + )* ) ) (?<! \s ) \k<tag> |
118117
(?<tag> < ) (?! \s ) (?> (?<text> (?: [^>\\]+ | (?&esc) | (?! (?<! \s ) > ) > + )* ) ) (?<! \s ) > |
119118
(?<tag> ~~ ) (?! \s ) (?> (?<text> (?: [^~\\]+ | (?&esc) | (?! (?<! \s ) ~~ ) ~ + )* ) ) (?<! \s ) ~~ |
120119
^ (?<tag> \#\# ) \h+ (?> (?<text> (?: [^\#\s\\]+ | (?&esc) | \#+ (?! \h* $ ) | \h++ (?! (?: \#+ \h* )? $ ) )* ) ) (?: \h+ \#+ | \h* ) $
121-
)
120+
) /mx
122121
REGEX;
123122

124123
/**
125-
* A CommonMark-compliant backslash escape, or an escaped line break with an
126-
* optional leading space
124+
* Matches a CommonMark-compliant backslash escape, or an escaped line break
125+
* with an optional leading space
127126
*/
128-
private const UNESCAPE_REGEX = <<<'REGEX'
129-
(?x)
127+
private const ESCAPE = <<<'REGEX'
128+
/
130129
(?|
131-
\\ ( [-\\ !"\#$%&'()*+,./:;<=>?@[\]^_`{|}~] ) |
130+
\\ ( [-\\ !"\#$%&'()*+,.\/:;<=>?@[\]^_`{|}~] ) |
132131
# Lookbehind assertions are unnecessary because the first branch
133132
# matches escaped spaces and backslashes
134133
\ ? \\ ( \n )
135-
)
134+
) /x
136135
REGEX;
137136

138137
private static ConsoleFormatter $DefaultFormatter;
@@ -316,7 +315,7 @@ public function formatTags(
316315
// Normalise line endings and split the string into formattable text,
317316
// fenced code blocks and code spans
318317
if (!Pcre::matchAll(
319-
Regex::delimit(self::PARSER_REGEX) . 'u',
318+
self::MARKUP,
320319
Str::setEol($string),
321320
$matches,
322321
\PREG_SET_ORDER | \PREG_UNMATCHED_AS_NULL
@@ -352,7 +351,7 @@ public function formatTags(
352351

353352
$adjust = 0;
354353
$text = Pcre::replaceCallback(
355-
Regex::delimit(self::TAG_REGEX) . 'u',
354+
self::TAG,
356355
function (array $match) use (
357356
&$replace,
358357
$textFormats,
@@ -462,7 +461,7 @@ function (array $match) use (
462461
$adjust = 0;
463462
$placeholders = 0;
464463
$string = Pcre::replaceCallback(
465-
Regex::delimit(self::UNESCAPE_REGEX) . 'u',
464+
self::ESCAPE,
466465
function (array $match) use (
467466
$unescape,
468467
$wrapAfterApply,
@@ -566,10 +565,6 @@ function (array $match) use (
566565
$string = substr_replace($string, $replacement, $offset, $length);
567566
}
568567

569-
if (\PHP_EOL !== "\n") {
570-
$string = str_replace("\n", \PHP_EOL, $string);
571-
}
572-
573568
return $string . $append;
574569
}
575570

@@ -639,7 +634,7 @@ public static function escapeTags(string $string, bool $newlines = false): strin
639634
public static function unescapeTags(string $string): string
640635
{
641636
return Pcre::replace(
642-
Regex::delimit(self::UNESCAPE_REGEX) . 'u',
637+
self::ESCAPE,
643638
'$1',
644639
$string,
645640
);
@@ -688,7 +683,7 @@ private function applyTags(
688683
$tag = $matchHasOffset ? $match['tag'][0] : $match['tag'];
689684

690685
$text = Pcre::replaceCallback(
691-
Regex::delimit(self::TAG_REGEX) . 'u',
686+
self::TAG,
692687
fn(array $match): string =>
693688
$this->applyTags($match, false, $unescape, $formats, $depth + 1),
694689
$text,
@@ -698,7 +693,7 @@ private function applyTags(
698693

699694
if ($unescape) {
700695
$text = Pcre::replace(
701-
Regex::delimit(self::UNESCAPE_REGEX) . 'u',
696+
self::ESCAPE,
702697
'$1',
703698
$text,
704699
);

src/Utility/Json.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,9 @@ public static function stringify($value, int $flags = 0): string
4040
*/
4141
public static function prettyPrint($value, int $flags = 0): string
4242
{
43-
$json = json_encode($value, self::ENCODE_FLAGS | \JSON_PRETTY_PRINT | $flags);
44-
return \PHP_EOL === "\n"
45-
? $json
46-
: str_replace("\n", \PHP_EOL, $json);
43+
return Str::eolToNative(
44+
json_encode($value, self::ENCODE_FLAGS | \JSON_PRETTY_PRINT | $flags)
45+
);
4746
}
4847

4948
/**

src/Utility/Str.php

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,55 @@ public static function upperFirst(string $string): string
6060
/**
6161
* Apply an end-of-line sequence to a string
6262
*/
63-
public static function setEol(string $string, string $eol = "\n", ?string $currentEol = null): string
63+
public static function setEol(string $string, string $eol = "\n"): string
6464
{
65-
if ($currentEol === null) {
66-
$currentEol = Get::eol($string);
67-
// No end-of-line sequence = no line breaks = nothing to do
68-
if ($currentEol === null) {
69-
return $string;
70-
}
71-
}
72-
if ($eol === $currentEol) {
73-
return $string;
65+
switch ($eol) {
66+
case "\n":
67+
return str_replace(["\r\n", "\r"], $eol, $string);
68+
69+
case "\r":
70+
return str_replace(["\r\n", "\n"], $eol, $string);
71+
72+
case "\r\n":
73+
return str_replace(["\r\n", "\r", "\n"], ["\n", "\n", $eol], $string);
74+
75+
default:
76+
return str_replace("\n", $eol, self::setEol($string));
7477
}
75-
return str_replace($currentEol, $eol, $string);
78+
}
79+
80+
/**
81+
* Replace newlines in a string with native end-of-line sequences
82+
*
83+
* @template T of string|null
84+
*
85+
* @param T $string
86+
* @return T
87+
*/
88+
public static function eolToNative(?string $string): ?string
89+
{
90+
return $string === null
91+
? null
92+
: (\PHP_EOL === "\n"
93+
? $string
94+
: str_replace("\n", \PHP_EOL, $string));
95+
}
96+
97+
/**
98+
* Replace native end-of-line sequences in a string with newlines
99+
*
100+
* @template T of string|null
101+
*
102+
* @param T $string
103+
* @return T
104+
*/
105+
public static function eolFromNative(?string $string): ?string
106+
{
107+
return $string === null
108+
? null
109+
: (\PHP_EOL === "\n"
110+
? $string
111+
: str_replace(\PHP_EOL, "\n", $string));
76112
}
77113

78114
/**

tests/unit/Cli/CliApplicationTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public function testCommandSynopsis(): void
5757
$_SERVER['argv'] = ['app.php'];
5858
$app = $this->App->oneCommand(TestOptions::class);
5959
$this->assertSame(1, $app->run()->getLastExitStatus());
60-
$this->assertSame([
60+
$this->assertSameConsoleMessages([
6161
[Level::ERROR, 'Error: --start required'],
6262
[Level::INFO, <<<'EOF'
6363
@@ -73,7 +73,7 @@ public function testCommandHelp(): void
7373
$_SERVER['argv'] = ['app.php', '--help'];
7474
$app = $this->App->oneCommand(TestOptions::class);
7575
$this->assertSame(0, $app->run()->getLastExitStatus());
76-
$this->assertSame([
76+
$this->assertSameConsoleMessages([
7777
[Level::INFO, <<<'EOF'
7878
NAME
7979
app - Test CliCommand options

tests/unit/Console/ConsoleFormatterTest.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Lkrms\Console\Support\ConsoleMarkdownFormat;
88
use Lkrms\Console\ConsoleFormatter as Formatter;
99
use Lkrms\Tests\TestCase;
10+
use Lkrms\Utility\Str;
1011

1112
final class ConsoleFormatterTest extends TestCase
1213
{
@@ -24,7 +25,16 @@ public function testFormat(
2425
bool $unformat = false,
2526
string $break = "\n"
2627
): void {
27-
$this->assertSame($expected, $formatter->formatTags($string, $unwrap, $wrapToWidth, $unformat, $break));
28+
$this->assertSame(
29+
Str::eolFromNative($expected),
30+
$formatter->formatTags(
31+
$string,
32+
$unwrap,
33+
$wrapToWidth,
34+
$unformat,
35+
$break,
36+
)
37+
);
2838
}
2939

3040
/**

tests/unit/Support/PhpDoc/PhpDocTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Lkrms\Support\Catalog\RegularExpression as Regex;
77
use Lkrms\Support\PhpDoc\PhpDoc;
88
use Lkrms\Tests\TestCase;
9+
use Lkrms\Utility\Str;
910

1011
final class PhpDocTest extends TestCase
1112
{
@@ -80,7 +81,7 @@ public function testFromDocBlocks(): void
8081
```php
8182
// code here
8283
```
83-
EOF, $this->eolToNative($phpDoc->Description));
84+
EOF, Str::eolToNative($phpDoc->Description));
8485
$this->assertSame([
8586
'@param $arg1 Description from ClassC (untyped)',
8687
'@param string[] $arg3',
@@ -141,7 +142,7 @@ public function testVarTags(
141142
): void {
142143
$phpDoc = new PhpDoc($docBlock);
143144
$this->assertSame($summary, $phpDoc->Summary);
144-
$this->assertSame($description, $this->eolToNative($phpDoc->Description));
145+
$this->assertSame($description, Str::eolToNative($phpDoc->Description));
145146
$this->assertCount(count($varKeys), $phpDoc->Vars);
146147
foreach ($varKeys as $i => $key) {
147148
$this->assertArrayHasKey($key, $phpDoc->Vars);
@@ -374,7 +375,7 @@ public function testFences(): void
374375
```php
375376
callback(string $value): string
376377
```
377-
EOF, $this->eolToNative($phpDoc->Description));
378+
EOF, Str::eolToNative($phpDoc->Description));
378379
$this->assertCount(1, $phpDoc->Vars);
379380
$this->assertSame(null, $phpDoc->Vars[0]->Name ?? null);
380381
$this->assertSame('?callable', $phpDoc->Vars[0]->Type ?? null);

0 commit comments

Comments
 (0)