diff --git a/src/Elements/BodyComponents/MjSocial.php b/src/Elements/BodyComponents/MjSocial.php
new file mode 100644
index 0000000..eb6eebb
--- /dev/null
+++ b/src/Elements/BodyComponents/MjSocial.php
@@ -0,0 +1,63 @@
+ ['unit' => 'string', 'type' => 'alignment', 'default_value' => 'center'],
+ 'border-radius' => ['unit' => 'px', 'type' => 'string', 'default_value' => '3px'],
+ 'color' => ['unit' => 'color', 'type' => 'color', 'default_value' => '#333333'],
+ 'font-family' => [
+ 'unit' => 'string',
+ 'type' => 'string',
+ 'default_value' => 'Ubuntu, Helvetica, Arial, sans-serif',
+ ],
+ 'font-size' => ['unit' => 'px', 'type' => 'string', 'default_value' => '13px'],
+ 'icon-size' => ['unit' => 'px', 'type' => 'string', 'default_value' => '20px'],
+ 'mode' => ['unit' => 'string', 'type' => 'string', 'default_value' => 'horizontal'],
+ 'padding' => ['unit' => 'px', 'type' => 'string', 'default_value' => '10px 25px'],
+ ];
+
+ protected array $defaultAttributes = [
+ 'align' => 'center',
+ 'border-radius' => '3px',
+ 'color' => '#333333',
+ 'font-size' => '13px',
+ 'icon-size' => '20px',
+ 'mode' => 'horizontal',
+ 'padding' => '10px 25px',
+ ];
+
+ public function render(): string
+ {
+ $children = $this->getChildren() ?? [];
+ $content = $this->renderChildren($children, []);
+ $divAttributes = $this->getHtmlAttributes(['style' => 'div']);
+ return "
$content
";
+ }
+
+ public function getChildContext(): array
+ {
+ return [
+ ...$this->context,
+ 'border-radius' => $this->getAttribute('border-radius'),
+ 'color' => $this->getAttribute('color'),
+ 'font-family' => $this->getAttribute('font-family'),
+ 'font-size' => $this->getAttribute('font-size'),
+ 'icon-size' => $this->getAttribute('icon-size'),
+ ];
+ }
+
+ public function getStyles(): array
+ {
+ return ['div' => ['text-align' => $this->getAttribute('align')]];
+ }
+}
diff --git a/src/Elements/BodyComponents/MjSocialElement.php b/src/Elements/BodyComponents/MjSocialElement.php
new file mode 100644
index 0000000..621609a
--- /dev/null
+++ b/src/Elements/BodyComponents/MjSocialElement.php
@@ -0,0 +1,62 @@
+ ['unit' => 'string', 'type' => 'alignment', 'default_value' => 'left'],
+ 'background-color' => ['unit' => 'color', 'type' => 'color', 'default_value' => ''],
+ 'border-radius' => ['unit' => 'px', 'type' => 'string', 'default_value' => ''],
+ 'color' => ['unit' => 'color', 'type' => 'color', 'default_value' => ''],
+ 'font-family' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
+ 'font-size' => ['unit' => 'px', 'type' => 'string', 'default_value' => ''],
+ 'href' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
+ 'icon-size' => ['unit' => 'px', 'type' => 'string', 'default_value' => ''],
+ 'name' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
+ 'padding' => ['unit' => 'px', 'type' => 'string', 'default_value' => '4px'],
+ 'src' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
+ 'target' => ['unit' => 'string', 'type' => 'string', 'default_value' => '_blank'],
+ ];
+
+ protected array $defaultAttributes = [
+ 'align' => 'left',
+ 'padding' => '4px',
+ 'target' => '_blank',
+ ];
+
+ public function render(): string
+ {
+ $href = $this->getAttribute('href');
+ $target = $this->getAttribute('target');
+ $src = $this->getAttribute('src');
+ $iconSize = $this->getAttribute('icon-size') ?: $this->context['icon-size'] ?? '20px';
+ $content = $this->getContent();
+
+ $aAttributes = $this->getHtmlAttributes(['style' => 'a']);
+ $icon = $src ? "
" : '';
+
+ return "$icon $content";
+ }
+
+ public function getStyles(): array
+ {
+ $color = $this->getAttribute('color') ?: $this->context['color'] ?? '#333333';
+ $fontFamily = $this->getAttribute('font-family') ?:
+ $this->context['font-family'] ?? 'Ubuntu, Helvetica, Arial, sans-serif';
+
+ return ['a' => [
+ 'color' => $color,
+ 'font-family' => $fontFamily,
+ 'padding' => $this->getAttribute('padding'),
+ 'text-decoration' => 'none',
+ ]];
+ }
+}
diff --git a/src/Elements/HeadComponents/MjAttributes.php b/src/Elements/HeadComponents/MjAttributes.php
new file mode 100644
index 0000000..f062560
--- /dev/null
+++ b/src/Elements/HeadComponents/MjAttributes.php
@@ -0,0 +1,26 @@
+>
- */
protected array $allowedAttributes = [
- 'width' => [
- 'unit' => 'px',
- 'type' => 'measure',
- 'description' => 'breakpoint width',
- 'default_value' => '480px',
- ],
+ 'width' => ['unit' => 'px', 'type' => 'string', 'default_value' => '480px'],
];
protected array $defaultAttributes = [
@@ -47,14 +21,9 @@ class MjBreakpoint extends AbstractElement
public function render(): string
{
- // mj-breakpoint doesn't render any HTML
- // It's processed by mj-head to set responsive breakpoint
return '';
}
- /**
- * @return array>
- */
public function getStyles(): array
{
return [];
diff --git a/src/Elements/HeadComponents/MjFont.php b/src/Elements/HeadComponents/MjFont.php
index 713ce37..a33077c 100644
--- a/src/Elements/HeadComponents/MjFont.php
+++ b/src/Elements/HeadComponents/MjFont.php
@@ -1,70 +1,35 @@
>
- */
protected array $allowedAttributes = [
- 'name' => [
- 'unit' => 'string',
- 'type' => 'string',
- 'description' => 'font name',
- 'default_value' => '',
- ],
- 'href' => [
- 'unit' => 'string',
- 'type' => 'string',
- 'description' => 'font url',
- 'default_value' => '',
- ],
+ 'name' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
+ 'href' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
];
protected array $defaultAttributes = [];
public function render(): string
{
- $name = $this->getAttribute('name');
$href = $this->getAttribute('href');
+ $name = $this->getAttribute('name');
if (!$href) {
return '';
}
- // Render as a link tag for non-MSO clients
- return "";
+ return "";
}
- /**
- * @return array>
- */
public function getStyles(): array
{
return [];
diff --git a/src/Elements/HeadComponents/MjHead.php b/src/Elements/HeadComponents/MjHead.php
index 7f7426d..9268c0f 100644
--- a/src/Elements/HeadComponents/MjHead.php
+++ b/src/Elements/HeadComponents/MjHead.php
@@ -1,153 +1,25 @@
>
- */
protected array $allowedAttributes = [];
-
protected array $defaultAttributes = [];
public function render(): string
{
$children = $this->getChildren() ?? [];
-
- $title = '';
- $preview = '';
- $styles = '';
- $fonts = [];
- $breakpoint = '480px';
-
- // Process head components
- foreach ($children as $child) {
- $tag = $child->getTag();
- $element = ElementFactory::create($child);
-
- match ($tag) {
- 'mj-title' => $title = $element->render(),
- 'mj-preview' => $preview = $element->render(),
- 'mj-style' => $styles .= $element->render(),
- 'mj-font' => $fonts[] = $element->render(),
- 'mj-breakpoint' => $breakpoint = $child->getAttributeValue('width') ?: '480px',
- default => null,
- };
- }
-
- // Build head content
- $head = '';
- $head .= $title ?: '';
- $head .= '';
- $head .= '';
- $head .= '';
- $head .= $this->renderBaseStyles();
- $head .= $this->renderMsoStyles();
- $head .= $this->renderOutlookStyles();
-
- // Add fonts
- foreach ($fonts as $font) {
- $head .= $font;
- }
-
- // Add responsive styles
- $head .= $this->renderResponsiveStyles($breakpoint);
-
- // Add custom styles
- $head .= $styles;
-
- // Add preview
- $head .= $preview;
-
- $head .= '';
-
- return $head;
- }
-
- private function renderBaseStyles(): string
- {
- $styles = '';
-
- return $styles;
- }
-
- private function renderMsoStyles(): string
- {
- $msoStyles = '';
-
- return $msoStyles;
- }
-
- private function renderOutlookStyles(): string
- {
- return '';
- }
-
- private function renderResponsiveStyles(string $breakpoint): string
- {
- $minWidth = $breakpoint;
-
- $styles = '';
- $styles .= '@media only screen and (min-width:$minWidth) {";
- $styles .= ' .mj-column-per-100 { width:100% !important; max-width: 100%; }';
- $styles .= ' }';
- $styles .= "';
-
- return $styles;
+ return $this->renderChildren($children, []);
}
- /**
- * @return array>
- */
public function getStyles(): array
{
return [];
diff --git a/src/Elements/HeadComponents/MjHtmlAttributes.php b/src/Elements/HeadComponents/MjHtmlAttributes.php
new file mode 100644
index 0000000..2324ba1
--- /dev/null
+++ b/src/Elements/HeadComponents/MjHtmlAttributes.php
@@ -0,0 +1,26 @@
+>
- */
protected array $allowedAttributes = [];
-
protected array $defaultAttributes = [];
public function render(): string
{
$content = $this->getContent();
-
- // Preview text is hidden but available for email clients
- $previewStyle = 'display:none;font-size:1px;color:#ffffff;line-height:1px;';
- $previewStyle .= 'max-height:0px;max-width:0px;opacity:0;overflow:hidden;';
-
- return "$content
";
+ return "$content
";
}
- /**
- * @return array>
- */
public function getStyles(): array
{
return [];
diff --git a/src/Elements/HeadComponents/MjStyle.php b/src/Elements/HeadComponents/MjStyle.php
index 9ba34c7..7339644 100644
--- a/src/Elements/HeadComponents/MjStyle.php
+++ b/src/Elements/HeadComponents/MjStyle.php
@@ -1,44 +1,18 @@
>
- */
protected array $allowedAttributes = [
- 'inline' => [
- 'unit' => 'string',
- 'type' => 'string',
- 'description' => 'whether styles should be inlined',
- 'default_value' => '',
- ],
+ 'inline' => ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
];
protected array $defaultAttributes = [];
@@ -48,14 +22,13 @@ public function render(): string
$content = $this->getContent();
$inline = $this->getAttribute('inline');
- // For now, we'll always render as a style tag in head
- // Inline styles would require processing during body rendering
- return "";
+ if ($inline === 'inline') {
+ return $content;
+ }
+
+ return "";
}
- /**
- * @return array>
- */
public function getStyles(): array
{
return [];
diff --git a/src/Elements/HeadComponents/MjTitle.php b/src/Elements/HeadComponents/MjTitle.php
index a00220d..640623a 100644
--- a/src/Elements/HeadComponents/MjTitle.php
+++ b/src/Elements/HeadComponents/MjTitle.php
@@ -1,51 +1,24 @@
>
- */
protected array $allowedAttributes = [];
-
protected array $defaultAttributes = [];
public function render(): string
{
- $content = $this->getContent();
-
- return "$content";
+ return '' . $this->getContent() . '';
}
- /**
- * @return array>
- */
public function getStyles(): array
{
return [];
diff --git a/src/Elements/Utilities/MjInclude.php b/src/Elements/Utilities/MjInclude.php
new file mode 100644
index 0000000..485b6a5
--- /dev/null
+++ b/src/Elements/Utilities/MjInclude.php
@@ -0,0 +1,53 @@
+ ['unit' => 'string', 'type' => 'string', 'default_value' => ''],
+ 'type' => ['unit' => 'string', 'type' => 'string', 'default_value' => 'mjml'],
+ ];
+
+ protected array $defaultAttributes = [
+ 'type' => 'mjml',
+ ];
+
+ public function render(): string
+ {
+ $path = $this->getAttribute('path');
+ $type = $this->getAttribute('type');
+
+ if (!$path || !file_exists($path)) {
+ return '';
+ }
+
+ $content = file_get_contents($path);
+
+ if ($content === false) {
+ return '';
+ }
+
+ if ($type === 'css') {
+ return "";
+ }
+
+ if ($type === 'html') {
+ return $content;
+ }
+
+ return $content;
+ }
+
+ public function getStyles(): array
+ {
+ return [];
+ }
+}
diff --git a/tests/Unit/Elements/BodyComponents/MjSocialTest.php b/tests/Unit/Elements/BodyComponents/MjSocialTest.php
new file mode 100644
index 0000000..dfac8bd
--- /dev/null
+++ b/tests/Unit/Elements/BodyComponents/MjSocialTest.php
@@ -0,0 +1,19 @@
+ expect((new MjSocial())->getTagName())->toBe('mj-social'));
+it('MjSocial has correct defaults', fn() => expect((new MjSocial())->getAttribute('mode'))->toBe('horizontal'));
+
+it('MjSocialElement has correct tag name', function () {
+ expect((new MjSocialElement())->getTagName())->toBe('mj-social-element');
+});
+
+it('MjSocialElement renders', function () {
+ $element = new MjSocialElement(['href' => 'https://facebook.com'], 'Facebook');
+ $out = $element->render();
+ expect($out)->toContain('toContain('https://facebook.com')->toContain('Facebook');
+});
diff --git a/tests/Unit/Elements/HeadComponents/MjHeadComponentsTest.php b/tests/Unit/Elements/HeadComponents/MjHeadComponentsTest.php
new file mode 100644
index 0000000..3bafde7
--- /dev/null
+++ b/tests/Unit/Elements/HeadComponents/MjHeadComponentsTest.php
@@ -0,0 +1,101 @@
+ expect((new MjHead())->getTagName())->toBe('mj-head'));
+ it('is not ending tag', fn() => expect((new MjHead())->isEndingTag())->toBe(false));
+});
+
+describe('MjTitle', function () {
+ it('has correct tag name', fn() => expect((new MjTitle())->getTagName())->toBe('mj-title'));
+ it('is ending tag', fn() => expect((new MjTitle())->isEndingTag())->toBe(true));
+
+ it('renders title correctly', function () {
+ $element = new MjTitle([], 'My Email Title');
+ expect($element->render())->toBe('My Email Title');
+ });
+});
+
+describe('MjPreview', function () {
+ it('has correct tag name', fn() => expect((new MjPreview())->getTagName())->toBe('mj-preview'));
+ it('is ending tag', fn() => expect((new MjPreview())->isEndingTag())->toBe(true));
+
+ it('renders preview text as hidden div', function () {
+ $element = new MjPreview([], 'Preview text here');
+ $out = $element->render();
+ expect($out)->toContain('display:none');
+ expect($out)->toContain('Preview text here');
+ });
+});
+
+describe('MjFont', function () {
+ it('has correct tag name', fn() => expect((new MjFont())->getTagName())->toBe('mj-font'));
+
+ it('renders font link correctly', function () {
+ $element = new MjFont([
+ 'name' => 'Roboto',
+ 'href' => 'https://fonts.googleapis.com/css?family=Roboto',
+ ]);
+ $out = $element->render();
+ expect($out)->toContain('toContain('https://fonts.googleapis.com/css?family=Roboto');
+ expect($out)->toContain("rel='stylesheet'");
+ });
+
+ it('returns empty string when no href', function () {
+ $element = new MjFont(['name' => 'Roboto']);
+ expect($element->render())->toBe('');
+ });
+});
+
+describe('MjStyle', function () {
+ it('has correct tag name', fn() => expect((new MjStyle())->getTagName())->toBe('mj-style'));
+
+ it('renders style tag by default', function () {
+ $element = new MjStyle([], '.test { color: red; }');
+ $out = $element->render();
+ expect($out)->toContain('