From 779973e5f2af6cdae75c919561c0da9139eb63fa Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Thu, 14 Nov 2024 01:47:01 -0800 Subject: [PATCH] fix(console): handle nested `style` tags (#726) --- composer.json | 2 +- src/Tempest/Console/composer.json | 2 +- .../src/Highlight/DynamicTokenType.php | 52 ++++++++++++++++- .../Injections/DynamicInjection.php | 56 ++++++++----------- .../src/Highlight/TempestTerminalTheme.php | 8 ++- .../Injections/DynamicInjectionTest.php | 19 ++++--- src/Tempest/Debug/composer.json | 2 +- src/Tempest/Http/composer.json | 2 +- .../TempestConsoleLanguageTest.php | 19 ++++--- 9 files changed, 107 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index bb90f4dd2..774974658 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "symfony/uid": "^7.1", "symfony/var-dumper": "^7.1", "symfony/var-exporter": "^7.1", - "tempest/highlight": "^2.0", + "tempest/highlight": "^2.11.2", "vlucas/phpdotenv": "^5.6" }, "require-dev": { diff --git a/src/Tempest/Console/composer.json b/src/Tempest/Console/composer.json index fa17699ad..9bcf4a4dc 100644 --- a/src/Tempest/Console/composer.json +++ b/src/Tempest/Console/composer.json @@ -9,7 +9,7 @@ "tempest/core": "dev-main", "tempest/container": "dev-main", "tempest/debug": "dev-main", - "tempest/highlight": "^2.0", + "tempest/highlight": "^2.11.2", "tempest/log": "dev-main", "tempest/reflection": "dev-main", "tempest/support": "dev-main", diff --git a/src/Tempest/Console/src/Highlight/DynamicTokenType.php b/src/Tempest/Console/src/Highlight/DynamicTokenType.php index 59dbd7a5e..c40b82b96 100644 --- a/src/Tempest/Console/src/Highlight/DynamicTokenType.php +++ b/src/Tempest/Console/src/Highlight/DynamicTokenType.php @@ -15,7 +15,7 @@ public function __construct( ) { } - public function getStyle(): TerminalStyle + public function getBeforeStyle(): TerminalStyle { $normalizedStyle = str($this->style) ->lower() @@ -34,6 +34,56 @@ public function getStyle(): TerminalStyle return TerminalStyle::RESET; } + public function getAfterStyle(): TerminalStyle + { + return match ($this->getBeforeStyle()) { + // Mods + TerminalStyle::BOLD => TerminalStyle::RESET_INTENSITY, + TerminalStyle::DIM => TerminalStyle::RESET_INTENSITY, + TerminalStyle::ITALIC => TerminalStyle::RESET_ITALIC, + TerminalStyle::HIDDEN => TerminalStyle::VISIBLE, + TerminalStyle::UNDERLINE => TerminalStyle::RESET_UNDERLINE, + TerminalStyle::OVERLINE => TerminalStyle::RESET_OVERLINE, + TerminalStyle::STRIKETHROUGH => TerminalStyle::RESET_STRIKETHROUGH, + TerminalStyle::REVERSE_TEXT => TerminalStyle::RESET_REVERSE_TEXT, + // Foregrounds + TerminalStyle::FG_BLACK, + TerminalStyle::FG_DARK_RED, + TerminalStyle::FG_DARK_GREEN, + TerminalStyle::FG_DARK_YELLOW, + TerminalStyle::FG_DARK_BLUE, + TerminalStyle::FG_DARK_MAGENTA, + TerminalStyle::FG_DARK_CYAN, + TerminalStyle::FG_LIGHT_GRAY, + TerminalStyle::FG_GRAY, + TerminalStyle::FG_RED, + TerminalStyle::FG_GREEN, + TerminalStyle::FG_YELLOW, + TerminalStyle::FG_BLUE, + TerminalStyle::FG_MAGENTA, + TerminalStyle::FG_CYAN, + TerminalStyle::FG_WHITE => TerminalStyle::RESET_FOREGROUND, + // Backgrounds + TerminalStyle::BG_BLACK, + TerminalStyle::BG_DARK_RED, + TerminalStyle::BG_DARK_GREEN, + TerminalStyle::BG_DARK_YELLOW, + TerminalStyle::BG_DARK_BLUE, + TerminalStyle::BG_DARK_MAGENTA, + TerminalStyle::BG_DARK_CYAN, + TerminalStyle::BG_LIGHT_GRAY, + TerminalStyle::BG_GRAY, + TerminalStyle::BG_RED, + TerminalStyle::BG_GREEN, + TerminalStyle::BG_YELLOW, + TerminalStyle::BG_BLUE, + TerminalStyle::BG_MAGENTA, + TerminalStyle::BG_CYAN, + TerminalStyle::BG_WHITE => TerminalStyle::RESET_BACKGROUND, + default => TerminalStyle::RESET, + }; + } + public function getValue(): string { return ''; diff --git a/src/Tempest/Console/src/Highlight/TempestConsoleLanguage/Injections/DynamicInjection.php b/src/Tempest/Console/src/Highlight/TempestConsoleLanguage/Injections/DynamicInjection.php index 12949c08c..2454d576c 100644 --- a/src/Tempest/Console/src/Highlight/TempestConsoleLanguage/Injections/DynamicInjection.php +++ b/src/Tempest/Console/src/Highlight/TempestConsoleLanguage/Injections/DynamicInjection.php @@ -20,41 +20,33 @@ public function getTokenType(): ConsoleTokenType public function parse(string $content, Highlighter $highlighter): ParsedInjection { - $pattern = '/(?\(?:[a-z-]+\s*)+)\"\>(.|\n)*\<\/style\>)/'; - - $result = preg_replace_callback( - pattern: $pattern, - callback: function ($matches) use ($highlighter, $pattern) { - $theme = $highlighter->getTheme(); - $content = $matches['match']; - $styles = $matches['styles']; - $before = ''; - $after = ''; - - foreach (explode(' ', $styles) as $style) { - $token = new DynamicTokenType($style); - $before .= $theme->before($token); - $after .= $theme->after($token); - } - - $result = str_replace( - search: $content, - replace: str($content) + $pattern = '/(?\(?:[a-z-]+\s*)+)\"\>(?:(?!\)/'; + + do { + $content = preg_replace_callback( + subject: $content, + pattern: $pattern, + callback: function ($matches) use ($highlighter) { + $theme = $highlighter->getTheme(); + $match = $matches['match']; + $styles = $matches['styles']; + $before = ''; + $after = ''; + + foreach (explode(' ', $styles) as $style) { + $token = new DynamicTokenType($style); + $before .= $theme->before($token); + $after .= $theme->after($token); + } + + return str($match) ->replaceFirst("", $before) ->replaceLast("", $after) - ->toString(), - subject: $matches[0], - ); - - if (preg_match($pattern, $result)) { - return $this->parse($result, $highlighter)->content; + ->toString(); } + ); + } while (preg_match($pattern, $content)); - return $result; - }, - subject: $content, - ); - - return new ParsedInjection($result ?? $content); + return new ParsedInjection($content); } } diff --git a/src/Tempest/Console/src/Highlight/TempestTerminalTheme.php b/src/Tempest/Console/src/Highlight/TempestTerminalTheme.php index 89c0753f2..351250941 100644 --- a/src/Tempest/Console/src/Highlight/TempestTerminalTheme.php +++ b/src/Tempest/Console/src/Highlight/TempestTerminalTheme.php @@ -17,7 +17,7 @@ public function before(TokenType $tokenType): string { if ($tokenType instanceof DynamicTokenType) { - return $this->style($tokenType->getStyle()); + return $this->style($tokenType->getBeforeStyle()); } return match ($tokenType) { @@ -42,6 +42,10 @@ public function before(TokenType $tokenType): string public function after(TokenType $tokenType): string { + if ($tokenType instanceof DynamicTokenType) { + return $this->style($tokenType->getAfterStyle()); + } + return match ($tokenType) { ConsoleTokenType::ERROR, ConsoleTokenType::QUESTION, @@ -57,7 +61,7 @@ private function style(TerminalStyle ...$styles): string return implode( '', array_map( - fn (TerminalStyle $style) => TerminalStyle::ESC->value . $style->value, + fn (TerminalStyle $style) => TerminalStyle::ESC->value . $style->value, $styles, ), ); diff --git a/src/Tempest/Console/tests/TempestConsoleLanguage/Injections/DynamicInjectionTest.php b/src/Tempest/Console/tests/TempestConsoleLanguage/Injections/DynamicInjectionTest.php index 7500edb16..5a660810c 100644 --- a/src/Tempest/Console/tests/TempestConsoleLanguage/Injections/DynamicInjectionTest.php +++ b/src/Tempest/Console/tests/TempestConsoleLanguage/Injections/DynamicInjectionTest.php @@ -16,15 +16,18 @@ */ final class DynamicInjectionTest extends TestCase { - #[TestWith(['foo', "\e[96mfoo\e[0m"])] - #[TestWith(['foo', "\e[101mfoo\e[0m"])] - #[TestWith(['foo', "\e[1mfoo\e[0m"])] - #[TestWith(['foo', "\e[4mfoo\e[0m"])] + #[TestWith(['foo', "\e[96mfoo\e[39m"])] + #[TestWith(['foo', "\e[101mfoo\e[49m"])] + #[TestWith(['foo', "\e[1mfoo\e[22m"])] + #[TestWith(['foo', "\e[4mfoo\e[24m"])] #[TestWith(['foo', "\e[0mfoo\e[0m"])] - #[TestWith(['foo', "\e[7mfoo\e[0m"])] - #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[0m\e[0m\e[0m"])] - #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[0m\e[0m\e[0m"])] - #[TestWith(['foo', "\e[96m\e[41mfoo\e[0m\e[0m"])] + #[TestWith(['foo', "\e[7mfoo\e[27m"])] + #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[49m\e[39m\e[24m"])] + #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[49m\e[39m\e[24m"])] + #[TestWith(['foo', "\e[96m\e[41mfoo\e[49m\e[39m"])] + #[TestWith(['foo', "\e[2m\e[41m\e[97mfoo\e[49m\e[39m\e[22m"])] + #[TestWith(['cyanunstyleddark red', "\e[96mcyan\e[39munstyled\e[41mdark red\e[49m"])] + #[TestWith(['dim-gray just-gray', "\e[2m\e[90mdim-gray\e[39m just-gray\e[22m"])] #[Test] public function language(string $content, string $expected): void { diff --git a/src/Tempest/Debug/composer.json b/src/Tempest/Debug/composer.json index 2ad0a2b86..321493458 100644 --- a/src/Tempest/Debug/composer.json +++ b/src/Tempest/Debug/composer.json @@ -5,7 +5,7 @@ "minimum-stability": "dev", "require": { "php": "^8.3", - "tempest/highlight": "^2.0", + "tempest/highlight": "^2.11.2", "symfony/var-dumper": "^7.1" }, "autoload": { diff --git a/src/Tempest/Http/composer.json b/src/Tempest/Http/composer.json index 82cba9e58..96577b7bd 100644 --- a/src/Tempest/Http/composer.json +++ b/src/Tempest/Http/composer.json @@ -11,7 +11,7 @@ "tempest/view": "dev-main", "tempest/mapper": "dev-main", "tempest/container": "dev-main", - "tempest/highlight": "^2.0", + "tempest/highlight": "^2.11.2", "laminas/laminas-diactoros": "^3.3", "psr/http-factory": "^1.0", "psr/http-message": "^1.0|^2.0", diff --git a/tests/Integration/Console/Highlight/TempestConsoleLanguage/TempestConsoleLanguageTest.php b/tests/Integration/Console/Highlight/TempestConsoleLanguage/TempestConsoleLanguageTest.php index daa2bbe74..8976d16f2 100644 --- a/tests/Integration/Console/Highlight/TempestConsoleLanguage/TempestConsoleLanguageTest.php +++ b/tests/Integration/Console/Highlight/TempestConsoleLanguage/TempestConsoleLanguageTest.php @@ -16,15 +16,18 @@ */ final class TempestConsoleLanguageTest extends TestCase { - #[TestWith(['foo', "\e[96mfoo\e[0m"])] - #[TestWith(['foo', "\e[101mfoo\e[0m"])] - #[TestWith(['foo', "\e[1mfoo\e[0m"])] - #[TestWith(['foo', "\e[4mfoo\e[0m"])] + #[TestWith(['foo', "\e[96mfoo\e[39m"])] + #[TestWith(['foo', "\e[101mfoo\e[49m"])] + #[TestWith(['foo', "\e[1mfoo\e[22m"])] + #[TestWith(['foo', "\e[4mfoo\e[24m"])] #[TestWith(['foo', "\e[0mfoo\e[0m"])] - #[TestWith(['foo', "\e[7mfoo\e[0m"])] - #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[0m\e[0m\e[0m"])] - #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[0m\e[0m\e[0m"])] - #[TestWith(['foo', "\e[96m\e[41mfoo\e[0m\e[0m"])] + #[TestWith(['foo', "\e[7mfoo\e[27m"])] + #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[49m\e[39m\e[24m"])] + #[TestWith(['Tempest', "\e[46m\e[96m\e[4mTempest\e[49m\e[39m\e[24m"])] + #[TestWith(['foo', "\e[96m\e[41mfoo\e[49m\e[39m"])] + #[TestWith(['foo', "\e[2m\e[41m\e[97mfoo\e[49m\e[39m\e[22m"])] + #[TestWith(['cyanunstyleddark red', "\e[96mcyan\e[39munstyled\e[41mdark red\e[49m"])] + #[TestWith(['dim-gray just-gray', "\e[2m\e[90mdim-gray\e[39m just-gray\e[22m"])] #[Test] public function language(string $content, string $expected): void {