Introduce mail partials + twig

Improve layout seeding
This commit is contained in:
Samuel Georges 2017-07-19 21:14:08 +10:00
parent 52e61f6a3b
commit 4663531de2
31 changed files with 1010 additions and 94 deletions

View File

@ -51,10 +51,10 @@ class ServiceProvider extends ModuleServiceProvider
*/
protected function registerMailer()
{
MailManager::instance()->registerCallback(function ($template) {
$template->registerMailTemplates([
'backend::mail.invite' => 'Invitation for newly created administrators.',
'backend::mail.restore' => 'Password reset instructions for backend-end administrators.',
MailManager::instance()->registerCallback(function ($manager) {
$manager->registerMailTemplates([
'backend::mail.invite',
'backend::mail.restore',
]);
});
}

View File

@ -1,5 +1,6 @@
subject = "Welcome to October CMS"
layout = "system"
description = "Invite new admin to the site"
==
Hi {{ name }},

View File

@ -1,5 +1,6 @@
subject = "Password Reset"
layout = "system"
description = "Reset an admin password"
==
Hello {{ name }},

View File

@ -287,6 +287,21 @@ class ServiceProvider extends ModuleServiceProvider
*/
protected function registerMailer()
{
/*
* Register system layouts
*/
MailManager::instance()->registerCallback(function ($manager) {
$manager->registerMailLayouts([
'default' => 'system::mail.default',
'system' => 'system::mail.system',
]);
$manager->registerMailPartials([
'button' => 'system::mail.button',
'table' => 'system::mail.table',
]);
});
/*
* Override system mailer with mail settings
*/

View File

@ -2,9 +2,13 @@
use Twig;
use Markdown;
use System\Models\MailPartial;
use System\Models\MailTemplate;
use System\Helpers\View as ViewHelper;
use System\Classes\PluginManager;
use System\Classes\MarkupManager;
use System\Twig\MailPartialTokenParser;
use System\Twig\MailComponentTokenParser;
/**
* This class manages Mail sending functions
@ -31,6 +35,21 @@ class MailManager
*/
protected $registeredTemplates;
/**
* @var array List of registered partials in the system
*/
protected $registeredPartials;
/**
* @var array List of registered layouts in the system
*/
protected $registeredLayouts;
/**
* @var bool Internal marker for rendering mode
*/
protected $isHtmlRenderMode;
/**
* This function hijacks the `addContent` method of the `October\Rain\Mail\Mailer`
* class, using the `mailer.beforeAddContent` event.
@ -44,6 +63,16 @@ class MailManager
$this->templateCache[$code] = $template = MailTemplate::findOrMakeTemplate($code);
}
/*
* Start twig transaction
*/
$markupManager = MarkupManager::instance();
$markupManager->beginTransaction();
$markupManager->registerTokenParsers([
new MailPartialTokenParser,
new MailComponentTokenParser
]);
/*
* Inject global view variables
*/
@ -63,6 +92,31 @@ class MailManager
/*
* HTML contents
*/
$html = $this->renderHtmlContents($template, $data);
$message->setBody($html, 'text/html');
/*
* Text contents
*/
$text = $this->renderTextContents($template, $data);
$message->addPart($text, 'text/plain');
/*
* End twig transaction
*/
$markupManager->endTransaction();
}
//
// Rendering
//
public function renderHtmlContents($template, $data)
{
$this->isHtmlRenderMode = true;
$templateHtml = $template->content_html;
$html = Twig::parse($templateHtml, $data);
@ -75,11 +129,13 @@ class MailManager
] + (array) $data);
}
$message->setBody($html, 'text/html');
return $html;
}
public function renderTextContents($template, $data)
{
$this->isHtmlRenderMode = false;
/*
* Text contents
*/
$templateText = $template->content_text;
if (!strlen($template->content_text)) {
@ -93,7 +149,45 @@ class MailManager
] + (array) $data);
}
$message->addPart($text, 'text/plain');
return $text;
}
public function renderPartial($code, $params)
{
if (!$partial = MailPartial::whereCode($code)->first()) {
return '<!-- Missing partial: '.$code.' -->';
}
if ($this->isHtmlRenderMode) {
return $this->renderHtmlPartial($partial, $params);
}
else {
return $this->renderTextPartial($partial, $params);
}
}
public function renderHtmlPartial($partial, $params)
{
$content = $partial->content_html;
if (!strlen(trim($content))) {
return '';
}
$params['body'] = Markdown::parse(array_get($params, 'body'));
return Twig::parse($content, $params);
}
public function renderTextPartial($partial, $params)
{
$content = $partial->content_text ?: $partial->content_html;
if (!strlen(trim($content))) {
return '';
}
return Twig::parse($content, $params);
}
//
@ -134,6 +228,32 @@ class MailManager
return $this->registeredTemplates;
}
/**
* Returns a list of the registered partials.
* @return array
*/
public function listRegisteredPartials()
{
if ($this->registeredPartials === null) {
$this->loadRegisteredTemplates();
}
return $this->registeredPartials;
}
/**
* Returns a list of the registered layouts.
* @return array
*/
public function listRegisteredLayouts()
{
if ($this->registeredLayouts === null) {
$this->loadRegisteredTemplates();
}
return $this->registeredLayouts;
}
/**
* Registers a callback function that defines mail templates.
* The callback function should register templates by calling the manager's
@ -160,6 +280,37 @@ class MailManager
$this->registeredTemplates = [];
}
$this->registeredTemplates = array_merge($this->registeredTemplates, $definitions);
// Prior sytax where (key) code => (value) description
if (!isset($definitions[0])) {
$definitions = array_keys($definitions);
}
$definitions = array_combine($definitions, $definitions);
$this->registeredTemplates = $definitions + $this->registeredTemplates;
}
/**
* Registers mail views and manageable layouts.
*/
public function registerMailPartials(array $definitions)
{
if (!$this->registeredPartials) {
$this->registeredPartials = [];
}
$this->registeredPartials = $definitions + $this->registeredPartials;
}
/**
* Registers mail views and manageable layouts.
*/
public function registerMailLayouts(array $definitions)
{
if (!$this->registeredLayouts) {
$this->registeredLayouts = [];
}
$this->registeredLayouts = $definitions + $this->registeredLayouts;
}
}

View File

@ -27,7 +27,7 @@ class MarkupManager
protected $callbacks = [];
/**
* @var array Registered extension items
* @var array Globally registered extension items
*/
protected $items;
@ -36,6 +36,16 @@ class MarkupManager
*/
protected $pluginManager;
/**
* @var array Transaction based extension items
*/
protected $tranItems;
/**
* @var bool Manager is in transaction mode
*/
protected $transactionMode = false;
/**
* Initialize this singleton.
*/
@ -71,7 +81,6 @@ class MarkupManager
$this->registerExtensions($type, $definitions);
}
}
}
@ -105,23 +114,25 @@ class MarkupManager
*/
public function registerExtensions($type, array $definitions)
{
if (is_null($this->items)) {
$this->items = [];
$items = $this->transactionMode ? 'tranItems' : 'items';
if (is_null($this->$items)) {
$this->$items = [];
}
if (!array_key_exists($type, $this->items)) {
$this->items[$type] = [];
if (!array_key_exists($type, $this->$items)) {
$this->$items[$type] = [];
}
foreach ($definitions as $name => $definition) {
switch ($type) {
case self::EXTENSION_TOKEN_PARSER:
$this->items[$type][] = $definition;
$this->$items[$type][] = $definition;
break;
case self::EXTENSION_FILTER:
case self::EXTENSION_FUNCTION:
$this->items[$type][$name] = $definition;
$this->$items[$type][$name] = $definition;
break;
}
}
@ -161,15 +172,21 @@ class MarkupManager
*/
public function listExtensions($type)
{
$results = [];
if ($this->items === null) {
$this->loadExtensions();
}
if (!isset($this->items[$type]) || !is_array($this->items[$type])) {
return [];
if (isset($this->items[$type]) && is_array($this->items[$type])) {
$results = $this->items[$type];
}
return $this->items[$type];
if ($this->tranItems !== null && isset($this->tranItems[$type])) {
$results = array_merge($results, $this->tranItems[$type]);
}
return $results;
}
/**
@ -329,4 +346,41 @@ class MarkupManager
return $isWild;
}
//
// Transactions
//
/**
* Execute a single serving transaction, containing filters, functions,
* and token parsers that are disposed of afterwards.
* @param \Closure $callback
* @return void
*/
public function transaction(Closure $callback)
{
$this->beginTransaction();
$callback($this);
$this->endTransaction();
}
/**
* Start a new transaction.
* @return void
*/
public function beginTransaction()
{
$this->transactionMode = true;
}
/**
* Ends an active transaction.
* @return void
*/
public function endTransaction()
{
$this->transactionMode = false;
$this->tranItems = null;
}
}

View File

@ -0,0 +1,38 @@
<?php namespace System\Controllers;
use Str;
use Lang;
use File;
use Flash;
use Backend;
use Redirect;
use BackendMenu;
use Backend\Classes\Controller;
use ApplicationException;
use System\Classes\SettingsManager;
use Exception;
/**
* Mail partials controller
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class MailPartials extends Controller
{
public $implement = [
'Backend.Behaviors.FormController',
];
public $requiredPermissions = ['system.manage_mail_templates'];
public $formConfig = 'config_form.yaml';
public function __construct()
{
parent::__construct();
BackendMenu::setContext('October.System', 'system', 'settings');
SettingsManager::setContext('October.System', 'mail_templates');
}
}

View File

@ -29,7 +29,12 @@ class MailTemplates extends Controller
public $requiredPermissions = ['system.manage_mail_templates'];
public $listConfig = ['templates' => 'config_templates_list.yaml', 'layouts' => 'config_layouts_list.yaml'];
public $listConfig = [
'templates' => 'config_templates_list.yaml',
'layouts' => 'config_layouts_list.yaml',
'partials' => 'config_partials_list.yaml'
];
public $formConfig = 'config_form.yaml';
public function __construct()

View File

@ -13,4 +13,4 @@ create:
update:
redirect: system/mailtemplates
redirectClose: system/mailtemplates
redirectClose: system/mailtemplates

View File

@ -0,0 +1,16 @@
# ===================================
# Form Behavior Config
# ===================================
name: system::lang.mail_templates.partial
form: ~/modules/system/models/mailpartial/fields.yaml
modelClass: System\Models\MailPartial
defaultRedirect: system/mailtemplates
create:
redirect: system/mailpartials/update/:id
redirectClose: system/mailtemplates
update:
redirect: system/mailtemplates
redirectClose: system/mailtemplates

View File

@ -0,0 +1,48 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('system/mailtemplates') ?>"><?= e(trans('system::lang.mail_templates.menu_partials_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class'=>'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons p-t">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('system::lang.mail_templates.creating_partial')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.create')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('system::lang.mail_templates.creating_partial')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.create_and_close')) ?>
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('system/mailtemplates') ?>"><?= e(trans('backend::lang.form.close')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('system/mailtemplates') ?>" class="btn btn-default"><?= e(trans('system::lang.mail_templates.return')) ?></a></p>
<?php endif ?>

View File

@ -0,0 +1,69 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('system/mailtemplates') ?>"><?= e(trans('system::lang.mail_templates.menu_partials_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?= Form::open(['class'=>'layout']) ?>
<div class="layout-row min-size">
<div class="scoreboard">
<div data-control="toolbar">
<div class="scoreboard-item title-value">
<h4><?= e(trans('system::lang.mail_templates.layout')) ?></h4>
<p><?= $formModel->code ?></p>
</div>
</div>
</div>
</div>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons p-t">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('system::lang.mail_templates.saving_layout')) ?>"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
data-request="onSave"
data-request-data="close:1"
data-hotkey="ctrl+enter, cmd+enter"
data-load-indicator="<?= e(trans('system::lang.mail_templates.saving_layout')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<?php if (!$formModel->is_locked): ?>
<button
type="button"
class="oc-icon-trash-o btn-icon danger pull-right"
data-request="onDelete"
data-load-indicator="<?= e(trans('system::lang.mail_templates.deleting_layout')) ?>"
data-request-confirm="<?= e(trans('system::lang.mail_templates.delete_layout_confirm')) ?>">
</button>
<?php endif ?>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('system/mailtemplates') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<p><a href="<?= Backend::url('system/mailtemplates') ?>" class="btn btn-default"><?= e(trans('system::lang.mail_templates.return')) ?></a></p>
<?php endif ?>

View File

@ -0,0 +1,7 @@
<div data-control="toolbar">
<a
href="<?= Backend::url('system/mailpartials/create') ?>"
class="btn btn-primary oc-icon-plus">
<?= e(trans('system::lang.mail_templates.new_partial')) ?>
</a>
</div>

View File

@ -0,0 +1,16 @@
# ===================================
# List Behavior Config
# ===================================
title: system::lang.mail_partials.menu_label
list: ~/modules/system/models/mailpartial/columns.yaml
modelClass: System\Models\MailPartial
recordUrl: system/mailpartials/update/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage: 20
showSetup: true
toolbar:
buttons: list_partials_toolbar
search:
prompt: backend::lang.list.search_prompt

View File

@ -2,6 +2,7 @@
<ul class="nav nav-tabs">
<li class="active"><a href="#templates"><?= e(trans('system::lang.mail_templates.templates')) ?></a></li>
<li><a href="#layouts"><?= e(trans('system::lang.mail_templates.layouts')) ?></a></li>
<li><a href="#partials"><?= e(trans('system::lang.mail_templates.partials')) ?></a></li>
</ul>
<div class="tab-content">
<div class="tab-pane active">
@ -10,5 +11,8 @@
<div class="tab-pane">
<?= $this->listRender('layouts') ?>
</div>
<div class="tab-pane">
<?= $this->listRender('partials') ?>
</div>
</div>
</div>

View File

@ -0,0 +1,26 @@
<?php
use October\Rain\Database\Schema\Blueprint;
use October\Rain\Database\Updates\Migration;
class DbSystemMailPartials extends Migration
{
public function up()
{
Schema::create('system_mail_partials', function (Blueprint $table) {
$table->engine = 'InnoDB';
$table->increments('id');
$table->string('name')->nullable();
$table->string('code')->nullable();
$table->text('content_html')->nullable();
$table->text('content_text')->nullable();
$table->boolean('is_custom')->default(0);
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('system_mail_partials');
}
}

View File

@ -5,74 +5,8 @@ use System\Models\MailLayout;
class SeedSetupMailLayouts extends Seeder
{
public function run()
{
$css = 'a, a:hover {
text-decoration: none;
color: #0862A2;
font-weight: bold;
}
td, tr, th, table {
padding: 0px;
margin: 0px;
}
p {
margin: 10px 0;
}';
$html = '<html>
<head>
<style type="text/css" media="screen">
{{ css|raw }}
</style>
</head>
<body>
{{ content|raw }}
</body>
</html>';
$text = '{{ content|raw }}';
MailLayout::create([
'is_locked' => true,
'name' => 'Default',
'code' => 'default',
'content_html' => $html,
'content_css' => $css,
'content_text' => $text,
]);
$html = '<html>
<head>
<style type="text/css" media="screen">
{{ css|raw }}
</style>
</head>
<body>
{{ content|raw }}
<hr />
<p>This is an automatic message. Please do not reply to it.</p>
</body>
</html>';
$text = '{{ content|raw }}
---
This is an automatic message. Please do not reply to it.
';
MailLayout::create([
'is_locked' => true,
'name' => 'System',
'code' => 'system',
'content_html' => $html,
'content_css' => $css,
'content_text' => $text,
]);
MailLayout::createLayouts();
}
}

View File

@ -190,9 +190,13 @@ return [
'menu_description' => 'Modify the mail templates that are sent to users and administrators, manage email layouts.',
'new_template' => 'New Template',
'new_layout' => 'New Layout',
'new_partial' => 'New Partial',
'template' => 'Template',
'templates' => 'Templates',
'partial' => 'Partial',
'partials' => 'Partials',
'menu_layouts_label' => 'Mail Layouts',
'menu_partials_label' => 'Mail Partials',
'layout' => 'Layout',
'layouts' => 'Layouts',
'no_layout' => '-- No layout --',

View File

@ -1,6 +1,10 @@
<?php namespace System\Models;
use File;
use View;
use Model;
use System\Classes\MailManager;
use October\Rain\Mail\MailParser;
use ApplicationException;
/**
@ -18,6 +22,19 @@ class MailLayout extends Model
*/
protected $table = 'system_mail_layouts';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Validation rules
*/
public $rules = [
'code' => 'required|unique:system_mail_layouts',
'name' => 'required',
@ -47,4 +64,64 @@ class MailLayout extends Model
return array_get(self::listCodes(), $code);
}
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
* @return void
*/
public static function createLayouts()
{
$dbLayouts = self::lists('code', 'code');
$definitions = MailManager::instance()->listRegisteredLayouts();
foreach ($definitions as $code => $path) {
if (array_key_exists($code, $dbLayouts)) {
continue;
}
self::createLayoutFromFile($code, $path);
}
}
/**
* Creates a layout using the contents of a specified file.
* @param string $code New Layout code
* @param string $viewPath View path
* @return void
*/
public static function createLayoutFromFile($code, $viewPath)
{
$sections = self::getTemplateSections($viewPath);
$name = array_get($sections, 'settings.name', '???');
$css = 'a, a:hover {
text-decoration: none;
color: #0862A2;
font-weight: bold;
}
td, tr, th, table {
padding: 0px;
margin: 0px;
}
p {
margin: 10px 0;
}';
self::create([
'is_locked' => true,
'name' => $name,
'code' => $code,
'content_css' => $css,
'content_html' => array_get($sections, 'html'),
'content_text' => array_get($sections, 'text')
]);
}
protected static function getTemplateSections($code)
{
return MailParser::parse(File::get(View::make($code)->getPath()));
}
}

View File

@ -0,0 +1,90 @@
<?php namespace System\Models;
use Twig;
use File;
use View;
use Model;
use Markdown;
use System\Classes\MailManager;
use October\Rain\Mail\MailParser;
use ApplicationException;
/**
* Mail partial
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class MailPartial extends Model
{
use \October\Rain\Database\Traits\Validation;
/**
* @var string The database table used by the model.
*/
protected $table = 'system_mail_partials';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Validation rules
*/
public $rules = [
'code' => 'required|unique:system_mail_partials',
'name' => 'required',
'content_html' => 'required',
];
/**
* Loops over each mail layout and ensures the system has a layout,
* if the layout does not exist, it will create one.
* @return void
*/
public static function createPartials()
{
$dbPartials = self::lists('code', 'code');
$definitions = MailManager::instance()->listRegisteredPartials();
foreach ($definitions as $code => $path) {
if (array_key_exists($code, $dbPartials)) {
continue;
}
self::createPartialFromFile($code, $path);
}
}
/**
* Creates a layout using the contents of a specified file.
* @param string $code New Partial code
* @param string $viewPath View path
* @return void
*/
public static function createPartialFromFile($code, $viewPath)
{
$sections = self::getTemplateSections($viewPath);
$name = array_get($sections, 'settings.name', '???');
self::create([
'name' => $name,
'code' => $code,
'is_custom' => 0,
'content_html' => array_get($sections, 'html'),
'content_text' => array_get($sections, 'text')
]);
}
protected static function getTemplateSections($code)
{
return MailParser::parse(File::get(View::make($code)->getPath()));
}
}

View File

@ -22,6 +22,19 @@ class MailTemplate extends Model
*/
protected $table = 'system_mail_templates';
/**
* @var array Guarded fields
*/
protected $guarded = [];
/**
* @var array Fillable fields
*/
protected $fillable = [];
/**
* @var array Validation rules
*/
public $rules = [
'code' => 'required|unique:system_mail_templates',
'subject' => 'required',
@ -40,7 +53,7 @@ class MailTemplate extends Model
public static function listAllTemplates()
{
$fileTemplates = (array) MailManager::instance()->listRegisteredTemplates();
$dbTemplates = (array) self::lists('description', 'code');
$dbTemplates = (array) self::lists('code', 'code');
$templates = $fileTemplates + $dbTemplates;
ksort($templates);
return $templates;
@ -52,6 +65,9 @@ class MailTemplate extends Model
*/
public static function syncAll()
{
MailLayout::createLayouts();
MailPartial::createPartials();
$templates = MailManager::instance()->listRegisteredTemplates();
$dbTemplates = self::lists('is_custom', 'code');
$newTemplates = array_diff_key($templates, $dbTemplates);
@ -59,8 +75,8 @@ class MailTemplate extends Model
/*
* Clean up non-customized templates
*/
foreach ($dbTemplates as $code => $is_custom) {
if ($is_custom) {
foreach ($dbTemplates as $code => $isCustom) {
if ($isCustom) {
continue;
}
@ -72,9 +88,10 @@ class MailTemplate extends Model
/*
* Create new templates
*/
foreach ($newTemplates as $code => $description) {
foreach ($newTemplates as $code) {
$sections = self::getTemplateSections($code);
$layoutCode = array_get($sections, 'settings.layout', 'default');
$description = array_get($sections, 'settings.description');
$template = self::make();
$template->code = $code;

View File

@ -0,0 +1,13 @@
# ===================================
# Column Definitions
# ===================================
columns:
name:
label: system::lang.mail_templates.name
searchable: true
code:
label: system::lang.mail_templates.code
searchable: true

View File

@ -0,0 +1,34 @@
# ===================================
# Field Definitions
# ===================================
fields:
code:
label: system::lang.mail_templates.code
comment: system::lang.mail_templates.code_comment
span: left
context: create
name@create:
label: system::lang.mail_templates.name
span: right
name@update:
label: system::lang.mail_templates.name
secondaryTabs:
fields:
content_html:
type: codeeditor
size: giant
tab: system::lang.mail_templates.content_html
language: html
stretch: true
content_text:
type: textarea
size: giant
tab: system::lang.mail_templates.content_text
stretch: true

View File

@ -0,0 +1,51 @@
<?php namespace System\Twig;
use Twig_Node;
use Twig_Compiler;
/**
* Represents a component node
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class MailComponentNode extends Twig_Node
{
public function __construct(Twig_Node $nodes, $paramNames, $body, $lineno, $tag = 'component')
{
parent::__construct(['nodes' => $nodes, 'body' => $body], ['names' => $paramNames], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param Twig_Compiler $compiler A Twig_Compiler instance
*/
public function compile(Twig_Compiler $compiler)
{
$compiler->addDebugInfo($this);
$compiler->write("\$context['__system_component_params'] = [];\n");
$compiler
->addDebugInfo($this)
->write('ob_start();')
->subcompile($this->getNode('body'))
->write("\$context['__system_component_params']['body'] = ob_get_clean();");
for ($i = 1; $i < count($this->getNode('nodes')); $i++) {
$compiler->write("\$context['__system_component_params']['".$this->getAttribute('names')[$i-1]."'] = ");
$compiler->subcompile($this->getNode('nodes')->getNode($i));
$compiler->write(";\n");
}
$compiler
->write("echo \System\Classes\MailManager::instance()->renderPartial(")
->subcompile($this->getNode('nodes')->getNode(0))
->write(", \$context['__system_component_params']")
->write(");\n")
;
$compiler->write("unset(\$context['__system_component_params']);\n");
}
}

View File

@ -0,0 +1,82 @@
<?php namespace System\Twig;
use Twig_Node;
use Twig_Token;
use Twig_TokenParser;
use Twig_Error_Syntax;
/**
* Parser for the `{% component %}` Twig tag.
*
* {% component "sidebar" %}
*
* {% component "sidebar" name='John' %}
*
* {% component "sidebar" name='John', year=2013 %}
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class MailComponentTokenParser extends Twig_TokenParser
{
/**
* Parses a token and returns a node.
*
* @param Twig_Token $token A Twig_Token instance
* @return Twig_Node A Twig_Node instance
*/
public function parse(Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $this->parser->getExpressionParser()->parseExpression();
$paramNames = [];
$nodes = [$name];
$end = false;
while (!$end) {
$current = $stream->next();
switch ($current->getType()) {
case Twig_Token::NAME_TYPE:
$paramNames[] = $current->getValue();
$stream->expect(Twig_Token::OPERATOR_TYPE, '=');
$nodes[] = $this->parser->getExpressionParser()->parseExpression();
break;
case Twig_Token::BLOCK_END_TYPE:
$end = true;
break;
default:
throw new Twig_Error_Syntax(
sprintf('Invalid syntax in the partial tag. Line %s', $lineno),
$stream->getCurrent()->getLine(),
$stream->getFilename()
);
break;
}
}
$body = $this->parser->subparse([$this, 'decideComponentEnd'], true);
$stream->expect(Twig_Token::BLOCK_END_TYPE);
return new MailComponentNode(new Twig_Node($nodes), $paramNames, $body, $token->getLine(), $this->getTag());
}
public function decideComponentEnd(Twig_Token $token)
{
return $token->test('endcomponent');
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'component';
}
}

View File

@ -0,0 +1,45 @@
<?php namespace System\Twig;
use Twig_Node;
use Twig_Compiler;
/**
* Represents a partial node
*
* @package october\cms
* @author Alexey Bobkov, Samuel Georges
*/
class MailPartialNode extends Twig_Node
{
public function __construct(Twig_Node $nodes, $paramNames, $lineno, $tag = 'partial')
{
parent::__construct(['nodes' => $nodes], ['names' => $paramNames], $lineno, $tag);
}
/**
* Compiles the node to PHP.
*
* @param Twig_Compiler $compiler A Twig_Compiler instance
*/
public function compile(Twig_Compiler $compiler)
{
$compiler->addDebugInfo($this);
$compiler->write("\$context['__system_partial_params'] = [];\n");
for ($i = 1; $i < count($this->getNode('nodes')); $i++) {
$compiler->write("\$context['__system_partial_params']['".$this->getAttribute('names')[$i-1]."'] = ");
$compiler->subcompile($this->getNode('nodes')->getNode($i));
$compiler->write(";\n");
}
$compiler
->write("echo \$this->env->getExtension('Cms\Twig\Extension')->partialFunction(")
->subcompile($this->getNode('nodes')->getNode(0))
->write(", \$context['__system_partial_params']")
->write(");\n")
;
$compiler->write("unset(\$context['__system_partial_params']);\n");
}
}

View File

@ -0,0 +1,74 @@
<?php namespace System\Twig;
use Twig_Node;
use Twig_Token;
use Twig_TokenParser;
use Twig_Error_Syntax;
/**
* Parser for the `{% partial %}` Twig tag.
*
* {% partial "sidebar" %}
*
* {% partial "sidebar" name='John' %}
*
* {% partial "sidebar" name='John', year=2013 %}
*
* @package october\system
* @author Alexey Bobkov, Samuel Georges
*/
class MailPartialTokenParser extends Twig_TokenParser
{
/**
* Parses a token and returns a node.
*
* @param Twig_Token $token A Twig_Token instance
* @return Twig_Node A Twig_Node instance
*/
public function parse(Twig_Token $token)
{
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$name = $this->parser->getExpressionParser()->parseExpression();
$paramNames = [];
$nodes = [$name];
$end = false;
while (!$end) {
$current = $stream->next();
switch ($current->getType()) {
case Twig_Token::NAME_TYPE:
$paramNames[] = $current->getValue();
$stream->expect(Twig_Token::OPERATOR_TYPE, '=');
$nodes[] = $this->parser->getExpressionParser()->parseExpression();
break;
case Twig_Token::BLOCK_END_TYPE:
$end = true;
break;
default:
throw new Twig_Error_Syntax(
sprintf('Invalid syntax in the partial tag. Line %s', $lineno),
$stream->getCurrent()->getLine(),
$stream->getFilename()
);
break;
}
}
return new MailPartialNode(new Twig_Node($nodes), $paramNames, $token->getLine(), $this->getTag());
}
/**
* Gets the tag name associated with this token parser.
*
* @return string The tag name
*/
public function getTag()
{
return 'partial';
}
}

View File

@ -0,0 +1,5 @@
name = "Button"
==
{{ body|raw }}
==
<button>{{ body|raw }}</button>

View File

@ -0,0 +1,14 @@
name = "Default layout"
==
{{ content|raw }}
==
<html>
<head>
<style type="text/css" media="screen">
{{ css|raw }}
</style>
</head>
<body>
{{ content|raw }}
</body>
</html>

View File

@ -0,0 +1,20 @@
name = "System layout"
==
{{ content|raw }}
---
This is an automatic message. Please do not reply to it.
==
<html>
<head>
<style type="text/css" media="screen">
{{ css|raw }}
</style>
</head>
<body>
{{ content|raw }}
<hr />
<p>This is an automatic message. Please do not reply to it.</p>
</body>
</html>

View File

@ -0,0 +1,5 @@
name = "Table"
==
{{ body|raw }}
==
<div class="table">{{ body|raw }}</div>