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 @@