diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 33806a1..0b2ce1c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,9 +8,9 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest] - php: [8.4, 8.3, 8.2, 8.1] - stability: [prefer-stable] + os: [ubuntu-latest, windows-latest] + php: [8.3, 8.2, 8.1] + stability: [prefer-lowest, prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/bin/mjml.mjs b/bin/mjml.mjs new file mode 100644 index 0000000..927e54a --- /dev/null +++ b/bin/mjml.mjs @@ -0,0 +1,28 @@ +import mjml2html from 'mjml' + +const args = JSON.parse(atob(process.argv.slice(2))); + +const mjml = args[0]; +const options = args[1]; + +let result = '' + +try { + result = await mjml2html(mjml, options); +} catch (exception) { + const errorString = JSON.stringify({mjmlError: exception.toString()}); + + process.stdout.write(utoa(errorString)); + process.exit(0); +} + +process.stdout.write(utoa(JSON.stringify(result))); + +/** + * Unicode to ASCII (encode data to Base64) + * @param {string} data + * @return {string} + */ +function utoa(data) { + return btoa(unescape(encodeURIComponent(data))); +} diff --git a/composer.json b/composer.json index 4075718..ac51320 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ ], "require": { "php": "^8.1", - "spatie/temporary-directory": "^2.2", "symfony/process": "^6.3.2|^7.0" }, "require-dev": { diff --git a/src/Mjml.php b/src/Mjml.php index f0d9a73..51967a2 100755 --- a/src/Mjml.php +++ b/src/Mjml.php @@ -5,7 +5,7 @@ use Spatie\Mjml\Exceptions\CouldNotConvertMjml; use Spatie\Mjml\Exceptions\SidecarPackageUnavailable; use Spatie\MjmlSidecar\MjmlFunction; -use Spatie\TemporaryDirectory\TemporaryDirectory; +use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -23,6 +23,8 @@ class Mjml protected string $filePath = '.'; + protected string $workingDirectory; + protected bool $sidecar = false; public static function new(): self @@ -33,6 +35,8 @@ public static function new(): self protected function __construct() { $this->validationLevel = ValidationLevel::Soft; + + $this->workingDirectory = realpath(dirname(__DIR__).'/bin'); } public function keepComments(bool $keepComments = true): self @@ -89,6 +93,13 @@ public function filePath(string $filePath): self return $this; } + public function workingDirectory(string $workingDirectory): self + { + $this->workingDirectory = $workingDirectory; + + return $this; + } + public function canConvert(string $mjml): bool { try { @@ -123,11 +134,19 @@ public function convert(string $mjml, array $options = []): MjmlResult $this->configOptions($options), ]; - if ($this->sidecar) { - return $this->getSideCarResult($arguments); + $resultString = $this->sidecar + ? $this->getSideCarResult($arguments) + : $this->getLocalResult($arguments); + + $resultString = $this->checkForDeprecationWarning($resultString); + + $resultProperties = json_decode($resultString, true); + + if (array_key_exists('mjmlError', $resultProperties)) { + throw CouldNotConvertMjml::make($resultProperties['mjmlError']); } - return $this->getLocalResult($arguments); + return new MjmlResult($resultProperties); } protected function checkForDeprecationWarning(string $result): string @@ -141,36 +160,24 @@ protected function checkForDeprecationWarning(string $result): string return $result; } - protected function getCommand(string $templatePath, string $outputPath, $arguments): array + protected function getCommand(array $arguments): array { - $home = getenv('HOME'); - $extraDirectories = [ '/usr/local/bin', '/opt/homebrew/bin', - $home.'/n/bin', // support https://github.com/tj/n - __DIR__.'/../node_modules/mjml/bin', ]; - $mjmlPathFromEnv = getenv('MJML_PATH'); + $nodePathFromEnv = getenv('MJML_NODE_PATH'); - if ($mjmlPathFromEnv) { - array_unshift($extraDirectories, $mjmlPathFromEnv); + if ($nodePathFromEnv) { + array_unshift($extraDirectories, $nodePathFromEnv); } - $command = [ - (new ExecutableFinder)->find('mjml', 'mjml', $extraDirectories), - $templatePath, - '-o', - $outputPath, + return [ + (new ExecutableFinder)->find('node', 'node', $extraDirectories), + 'mjml.mjs', + base64_encode(json_encode(array_values($arguments))), ]; - - foreach ($arguments as $configKey => $configValue) { - $command[] = "-c.{$configKey}"; - $command[] = $configValue; - } - - return $command; } protected function configOptions(array $overrides): array @@ -187,75 +194,33 @@ protected function configOptions(array $overrides): array return array_merge($defaults, $overrides); } - protected function getSideCarResult(array $arguments): MjmlResult + protected function getSideCarResult(array $arguments): string { if (! class_exists(MjmlFunction::class)) { throw SidecarPackageUnavailable::make(); } - $result = MjmlFunction::execute([ + return MjmlFunction::execute([ 'mjml' => $arguments[0], 'options' => $arguments[1], ])->body(); - - $result = $this->checkForDeprecationWarning($result); - - $resultProperties = json_decode($result, true); - - if (array_key_exists('mjmlError', $resultProperties)) { - throw CouldNotConvertMjml::make($resultProperties['mjmlError']); - } - - return new MjmlResult($resultProperties); } - protected function getLocalResult(array $arguments): MjmlResult + protected function getLocalResult(array $arguments): string { - $tempDir = TemporaryDirectory::make(); - $filename = date('U'); - - $templatePath = $tempDir->path("{$filename}.mjml"); - file_put_contents($templatePath, $arguments[0]); - - $outputPath = $tempDir->path("{$filename}.html"); + $process = new Process( + $this->getCommand($arguments), + $this->workingDirectory, + ); - $command = $this->getCommand($templatePath, $outputPath, $arguments[1]); - - $process = new Process($command); $process->run(); if (! $process->isSuccessful()) { - $output = explode("\n", $process->getErrorOutput()); - $errors = array_filter($output, fn (string $output) => str_contains($output, 'Error')); - - $tempDir->delete(); - - throw CouldNotConvertMjml::make($errors[0] ?? $process->getErrorOutput()); - } - - $errors = []; - - if ($process->getErrorOutput()) { - $errors = array_filter(explode("\n", $process->getErrorOutput())); - $errors = array_map(function (string $error) { - preg_match('/Line (\d+) of (.+) \((.+)\) — (.+)/u', $error, $matches); - [, $line, , $tagName, $message] = $matches; - - return [ - 'line' => $line, - 'message' => $message, - 'tagName' => $tagName, - ]; - }, $errors); + throw new ProcessFailedException($process); } - $html = file_get_contents($outputPath); - - $tempDir->delete(); + $items = explode("\n", $process->getOutput()); - return new MjmlResult([ - 'html' => $html, - 'errors' => $errors, - ]); + return base64_decode(end($items)); } } diff --git a/tests/.pest/snapshots/MjmlTest/it_can_beautify_the_rendered_html.snap b/tests/.pest/snapshots/MjmlTest/it_can_beautify_the_rendered_html.snap index 73422c0..a144254 100644 --- a/tests/.pest/snapshots/MjmlTest/it_can_beautify_the_rendered_html.snap +++ b/tests/.pest/snapshots/MjmlTest/it_can_beautify_the_rendered_html.snap @@ -40,7 +40,6 @@ display: block; margin: 13px 0; } - </style> <!--[if mso]> <noscript> @@ -61,7 +60,6 @@ <link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css"> <style type="text/css"> @import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700); - </style> <!--<![endif]--> <style type="text/css"> @@ -71,14 +69,12 @@ max-width: 100%; } } - </style> <style media="screen and (min-width:480px)"> .moz-text-html .mj-column-per-100 { width: 100% !important; max-width: 100%; } - </style> </head> @@ -112,4 +108,4 @@ </div> </body> -</html> +</html> \ No newline at end of file