diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0b2ce1c..33806a1 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, windows-latest] - php: [8.3, 8.2, 8.1] - stability: [prefer-lowest, prefer-stable] + os: [ubuntu-latest] + php: [8.4, 8.3, 8.2, 8.1] + stability: [prefer-stable] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/bin/mjml.mjs b/bin/mjml.mjs deleted file mode 100644 index 927e54a..0000000 --- a/bin/mjml.mjs +++ /dev/null @@ -1,28 +0,0 @@ -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 ac51320..4075718 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ ], "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 51967a2..f0d9a73 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 Symfony\Component\Process\Exception\ProcessFailedException; +use Spatie\TemporaryDirectory\TemporaryDirectory; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -23,8 +23,6 @@ class Mjml protected string $filePath = '.'; - protected string $workingDirectory; - protected bool $sidecar = false; public static function new(): self @@ -35,8 +33,6 @@ 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 @@ -93,13 +89,6 @@ 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 { @@ -134,19 +123,11 @@ public function convert(string $mjml, array $options = []): MjmlResult $this->configOptions($options), ]; - $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']); + if ($this->sidecar) { + return $this->getSideCarResult($arguments); } - return new MjmlResult($resultProperties); + return $this->getLocalResult($arguments); } protected function checkForDeprecationWarning(string $result): string @@ -160,24 +141,36 @@ protected function checkForDeprecationWarning(string $result): string return $result; } - protected function getCommand(array $arguments): array + protected function getCommand(string $templatePath, string $outputPath, $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', ]; - $nodePathFromEnv = getenv('MJML_NODE_PATH'); + $mjmlPathFromEnv = getenv('MJML_PATH'); - if ($nodePathFromEnv) { - array_unshift($extraDirectories, $nodePathFromEnv); + if ($mjmlPathFromEnv) { + array_unshift($extraDirectories, $mjmlPathFromEnv); } - return [ - (new ExecutableFinder)->find('node', 'node', $extraDirectories), - 'mjml.mjs', - base64_encode(json_encode(array_values($arguments))), + $command = [ + (new ExecutableFinder)->find('mjml', 'mjml', $extraDirectories), + $templatePath, + '-o', + $outputPath, ]; + + foreach ($arguments as $configKey => $configValue) { + $command[] = "-c.{$configKey}"; + $command[] = $configValue; + } + + return $command; } protected function configOptions(array $overrides): array @@ -194,33 +187,75 @@ protected function configOptions(array $overrides): array return array_merge($defaults, $overrides); } - protected function getSideCarResult(array $arguments): string + protected function getSideCarResult(array $arguments): MjmlResult { if (! class_exists(MjmlFunction::class)) { throw SidecarPackageUnavailable::make(); } - return MjmlFunction::execute([ + $result = 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): string + protected function getLocalResult(array $arguments): MjmlResult { - $process = new Process( - $this->getCommand($arguments), - $this->workingDirectory, - ); + $tempDir = TemporaryDirectory::make(); + $filename = date('U'); + + $templatePath = $tempDir->path("{$filename}.mjml"); + file_put_contents($templatePath, $arguments[0]); + + $outputPath = $tempDir->path("{$filename}.html"); + $command = $this->getCommand($templatePath, $outputPath, $arguments[1]); + + $process = new Process($command); $process->run(); if (! $process->isSuccessful()) { - throw new ProcessFailedException($process); + $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); } - $items = explode("\n", $process->getOutput()); + $html = file_get_contents($outputPath); + + $tempDir->delete(); - return base64_decode(end($items)); + return new MjmlResult([ + 'html' => $html, + 'errors' => $errors, + ]); } } 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 a144254..73422c0 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,6 +40,7 @@ display: block; margin: 13px 0; } + @@ -108,4 +112,4 @@ - \ No newline at end of file +