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..fc760aa 100755 --- a/src/Mjml.php +++ b/src/Mjml.php @@ -5,6 +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,8 +24,6 @@ class Mjml protected string $filePath = '.'; - protected string $workingDirectory; - protected bool $sidecar = false; public static function new(): self @@ -35,8 +34,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 +90,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 +124,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 +142,24 @@ protected function checkForDeprecationWarning(string $result): string return $result; } - protected function getCommand(array $arguments): array + public function getCommand(TemporaryDirectory $tempDir, string $templatePath, string $outputPath, $arguments): array { - $extraDirectories = [ - '/usr/local/bin', - '/opt/homebrew/bin', - ]; + $executableFinder = new ExecutableFinder(); + $mjml = $executableFinder->find('mjml'); - $nodePathFromEnv = getenv('MJML_NODE_PATH'); + if (! $mjml) { + $tempDir->delete(); - if ($nodePathFromEnv) { - array_unshift($extraDirectories, $nodePathFromEnv); + throw CouldNotConvertMjml::make("No MJML binary found. Make sure it is installed on your system."); } - return [ - (new ExecutableFinder)->find('node', 'node', $extraDirectories), - 'mjml.mjs', - base64_encode(json_encode(array_values($arguments))), - ]; + $command = [$mjml, $templatePath, '-o', $outputPath]; + + foreach ($arguments as $configKey => $configValue) { + $command[] = "-c.{$configKey}"; + $command[] = $configValue; + } + return $command; } protected function configOptions(array $overrides): array @@ -194,33 +176,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($tempDir, $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..f811f6a 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 @@ -1,5 +1,5 @@ - + @@ -40,6 +40,7 @@ display: block; margin: 13px 0; } + + + -
+
@@ -108,4 +116,4 @@ - \ No newline at end of file + diff --git a/tests/.pest/snapshots/MjmlTest/it_can_minify_the_rendered_html.snap b/tests/.pest/snapshots/MjmlTest/it_can_minify_the_rendered_html.snap index 9afea35..2cde440 100644 --- a/tests/.pest/snapshots/MjmlTest/it_can_minify_the_rendered_html.snap +++ b/tests/.pest/snapshots/MjmlTest/it_can_minify_the_rendered_html.snap @@ -1,4 +1,4 @@ -
Hello World
\ No newline at end of file + }
Hello World
\ No newline at end of file diff --git a/tests/.pest/snapshots/MjmlTest/it_can_render_mjml_without_any_options.snap b/tests/.pest/snapshots/MjmlTest/it_can_render_mjml_without_any_options.snap index a63f174..031ec0e 100644 --- a/tests/.pest/snapshots/MjmlTest/it_can_render_mjml_without_any_options.snap +++ b/tests/.pest/snapshots/MjmlTest/it_can_render_mjml_without_any_options.snap @@ -1,5 +1,5 @@ - + @@ -48,17 +48,21 @@ .moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; } - + +