mustache_templates-8.x-1.0-beta4/modules/mustache_magic/src/Plugin/mustache/Magic/Sync.php
modules/mustache_magic/src/Plugin/mustache/Magic/Sync.php
<?php
namespace Drupal\mustache_magic\Plugin\mustache\Magic;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Url;
use Drupal\mustache\Element\Mustache;
use Drupal\mustache\Helpers\MustacheRenderTemplate;
use Drupal\mustache\Plugin\MustacheMagic;
use Drupal\mustache\Render\IterableMarkup;
use Drupal\mustache\Render\Markup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Enable automated DOM content synchronization by using a lambda.
*
* Usage:
*
* You can use the "magical" {{#sync.<options>}} variable to enable automated
* DOM content synchronization. Examples:
*
* @code
* One time immediate refresh: {{#sync.now}}{{node.title}}{{/sync.now}}
* Unlimited refresh every 10 seconds: {{#sync.now.10.always}}...{{/sync.now.10.always}}
* Up to 10 refreshes every second: {{#sync.now.1.10}}...{{/sync.now.1.10}}
* Up to 10 refreshes every second, delayed by 5 seconds: {{#sync.5.1.10}}...{{/sync.5.1.10}}
* Trigger refresh when clicking a DOM element having the CSS class ".button": {{#sync.trigger.button.click}}...{{/sync.trigger.button.click}}
* @endcode
*
* @see https://git.drupalcode.org/project/mustache_templates/-/blob/2.0.x/README.md#36-magic-synchronization
*
* @MustacheMagic(
* id = "sync",
* label = @Translation("Synchronization"),
* description = @Translation("Use the <b>{{#sync.<options>}}</b> variable to enable automated DOM content synchronization. Examples: <ul><li>One time immediate refresh: {{#sync.now}}{{node.title}}{{/sync.now}}</li><li>Unlimited refresh every 10 seconds: {{#sync.10.always}}...{{/sync.10.always}}</li><li>Up to 10 refreshes every second: {{#sync.1.10}}...{{/sync.1.10}}</li><li>Up to 10 refreshes every second, delayed by 5 seconds: {{#sync.5.1.10}}...{{/sync.5.1.10}}</li><li>Trigger refresh when clicking a DOM element having the CSS class .button: {{#sync.trigger.button.click}}...{{/sync.trigger.button.click}}</li></ul>More examples can be found in the <a href='https://git.drupalcode.org/project/mustache_templates/-/blob/2.0.x/README.md#36-magic-synchronization' target='_blank' rel='noopener noreferrer'>README</a>.")
* )
*/
class Sync extends MustacheMagic {
/**
* A sequence of user-defined synchronization settings.
*
* @var array
*/
protected $keys = [];
/**
* Whether this object is a clone.
*
* @var bool
*/
protected $cloned = FALSE;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The sync storage.
*
* @var \Drupal\mustache_magic\Storage\MustacheSyncStorage
*/
protected $syncStorage;
/**
* The URL to use for server-side synchronization of user-defined templates.
*
* @var string
*/
protected static $syncUrl = '/m/sync?';
/**
* A list of keywords to exclude when a certain scope is being handled.
*
* @var array
*/
protected static $excludes = [
'now',
'always',
'once',
'span',
'form',
'trigger',
'url',
'proxy',
'increment',
'summable',
'template',
'into',
];
/**
* The client library for printing messages.
*
* @var string
*/
public static $library = 'mustache_magic/magic.message';
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->renderer = $container->get('renderer');
$instance->syncStorage = $container->get('mustache.sync_storage');
return $instance;
}
/**
* Implementation of the magic __isset() method.
*/
public function __isset($name): bool {
return !empty($name) || (!is_null($name) && is_scalar($name) && (trim((string) $name) !== ''));
}
/**
* Implementation of the magic __get() method.
*/
public function __get($name) {
if (!$this->cloned) {
$cloned = clone $this;
$cloned->cloned = TRUE;
return $cloned->__get($name);
}
$name = mb_strtolower(trim($name));
if (ctype_digit(strval($name))) {
$this->keys[] = (int) $name;
}
else {
$this->keys[] = $name;
}
return $this;
}
/**
* Implements the magic __invoke() method, used as higher-order section.
*/
public function __invoke($template_content = NULL, $render = NULL) {
if (!isset($template_content, $render)) {
return;
}
if (isset(static::$library) && (empty($this->element['#attached']['library']) || !in_array(static::$library, $this->element['#attached']['library']))) {
$this->element['#attached']['library'][] = static::$library;
}
$template_hash = hash('md4', $template_content);
$element_sync = !empty($this->element['#use_sync']) && isset($this->element['#sync']['items']) ? reset($this->element['#sync']['items']) : NULL;
$bubbleable_metadata = BubbleableMetadata::createFromRenderArray($this->element);
$build = MustacheRenderTemplate::build($template_hash, $template_content);
$sync_options = $build->withClientSynchronization()->morphing();
$build_render_array = &$build->toRenderArray();
$to_merge = [];
$to_copys = [
'#cache',
'#partials',
'#inline_partials',
];
foreach ($to_copys as $to_copy) {
if (!empty($this->element[$to_copy])) {
$to_merge[$to_copy] = $this->element[$to_copy];
}
}
if ($element_sync) {
$to_merge['#sync'] = [];
$to_copys = ['eval', 'behaviors'];
foreach ($to_copys as $to_copy) {
if (!empty($element_sync[$to_copy])) {
$to_merge['#sync'][$to_copy] = $element_sync[$to_copy];
}
}
}
$force_proxy = FALSE;
foreach ($this->keys as $i => $key) {
if ($i === 0) {
if (is_int($key) && $key > 0) {
$sync_options->startsDelayed($key * 1000);
continue;
}
elseif ($key === 'now') {
continue;
}
}
if ($key === 'always') {
$sync_options->unlimited();
continue;
}
if ($key === 'span') {
$sync_options->withWrapperTag('span');
continue;
}
if ($key === 'proxy') {
$force_proxy = TRUE;
continue;
}
if (isset($form_bind) && $form_bind === 0) {
$form_bind = 1;
if (!is_int($key) && !in_array($key, static::$excludes)) {
$form_selector = $key;
if ($form_selector !== 'form'
&& !in_array(mb_substr(
$form_selector, 0, 1), ['#', '[', '*', ':', '.'])) {
$form_selector = '.' . $form_selector;
}
continue;
}
}
if ($key === 'form') {
$form_bind = 0;
$form_selector = 'form';
continue;
}
if (isset($trigger) && $trigger < 3) {
$trigger++;
if ($trigger === 1) {
$trigger_selector = $key;
if (!in_array(mb_substr(
$trigger_selector, 0, 1), ['#', '[', '*', ':', '.'])) {
$trigger_selector = '.' . $trigger_selector;
}
}
elseif ($trigger === 2) {
$trigger_event = !is_int($key) && !in_array($key, static::$excludes) ? $key : 'click';
}
elseif ($trigger === 3) {
if ($key == 'once') {
$trigger_limit = 1;
}
elseif (is_int($key) && $key > 0) {
$trigger_limit = $key;
}
else {
$trigger_limit = -1;
}
}
if (!in_array($key, static::$excludes)) {
continue;
}
}
if ($key === 'trigger' && !isset($trigger)) {
$trigger = 0;
$trigger_selector = '*';
$trigger_event = 'click';
$trigger_limit = -1;
continue;
}
if (is_int($key)) {
if (!isset($period)) {
$period = $key * 1000;
$sync_options->periodicallyRefreshesAt($period);
}
else {
$sync_options->upToNTimes($key);
}
continue;
}
if (isset($url) && $url === '') {
$url = str_replace('_dot_', '.', $key);
if (substr($url, 0, 1) !== '/' && substr($url, 0, 4) !== 'http') {
$url = '/' . $url;
}
continue;
}
if ($key === 'url') {
$url = '';
continue;
}
if (isset($increment) && $increment < 4) {
$increment++;
if ($increment === 1) {
$increment_key = $key;
}
elseif ($increment === 2) {
$increment_offset = $key;
}
elseif ($increment === 3) {
if ($key == 'once') {
$increment_limit = 1;
}
elseif (is_int($key) && $key > 0) {
$increment_limit = $key;
}
else {
$increment_limit = -1;
}
}
elseif ($increment === 4) {
$increment_step_size = $key;
}
if (!in_array($key, static::$excludes)) {
continue;
}
}
if ($key === 'increment' || $key === 'increments') {
$increment = 0;
$increment_key = 'page';
$increment_offset = 0;
$increment_limit = -1;
$increment_step_size = 1;
continue;
}
if ($key === 'summable' || $key === 'sumable') {
$summable = \Drupal::service('mustache.summables')->isEnabled();
continue;
}
if ($key === 'template') {
$template_name = '';
continue;
}
if (isset($template_name) && $template_name === '') {
$template_name = !in_array($key, static::$excludes) ? $key : $template_hash;
continue;
}
if ($key === 'into') {
$into_selector = FALSE;
continue;
}
if (isset($into_selector) && $into_selector === FALSE) {
if (!in_array($key, static::$excludes)) {
$into_selector = $key;
if (
!in_array(mb_substr($into_selector, 0, 1), ['#', '[', '*', ':', '.'])
&& !in_array(mb_substr($into_selector, 0, 4), ['head', 'body', 'meta'])) {
$into_selector = '.' . $into_selector;
}
$sync_options->insertsInto($into_selector);
// We assume that server-side rendering is not desired when using
// a different element target other than the auto-generated wrapper.
$build->withPlaceholder(['#markup' => '']);
continue;
}
}
}
if (isset($form_bind, $form_selector)) {
$sync_options->usingFormValues($form_selector);
}
if (isset($trigger)) {
$sync_options->startsWhenElementWasTriggered($trigger_selector)
->atEvent($trigger_event)
->upToNTimes($trigger_limit);
}
if (isset($increment)) {
$sync_options->increments()
->atParamKey($increment_key)
->startingAt($increment_offset)
->upToNTimes($increment_limit)
->withStepSize($increment_step_size);
}
if (empty($url) && $element_sync && !empty($element_sync['url'])) {
$url = $element_sync['url'];
}
if (!empty($this->element['#data'])) {
if (is_array($this->element['#data'])) {
$data = $this->element['#data'];
}
elseif (empty($url) && (is_string($this->element['#data']) || $this->element['#data'] instanceof Url)) {
$url = $this->element['#data'];
}
}
if (!isset($data)) {
if (isset($this->element['#override_data']) && is_array($this->element['#override_data'])) {
$data = $this->element['#override_data'];
}
else {
$data = [];
}
}
foreach ($data as $d_k => $d_v) {
if ($d_v instanceof IterableMarkup) {
unset($data[$d_k]);
}
}
$url_exists = !empty($url) && ($url = Mustache::getUrlFromParam($url));
if (!$url_exists) {
$build->withPlaceholder(['#markup' => Markup::create($render($template_content))]);
}
elseif (empty($data)) {
$build->usingDataFromUrl($url);
}
else {
$build->usingData($data);
$sync_options->usingDataFromUrl($url);
}
if ($url_exists) {
$url = clone $url;
$url->setAbsolute($url->isExternal());
}
if (!$url_exists || $force_proxy) {
$values = [
'name' => $template_hash,
'content' => $template_content,
'merge' => $to_merge,
'langcode' => \Drupal::languageManager()->getCurrentLanguage()->getId(),
];
$server_side_render = $build->toRenderArray();
$server_side_render['#sync'] = [];
if (!empty($this->element['#with_tokens'])) {
$server_side_render['#with_tokens'] = $this->element['#with_tokens'];
/** @var \Drupal\mustache\MustacheTokenProcessor $token_processor */
$token_processor = \Drupal::service('mustache.token_processor');
$tokenized = $token_processor->tokenizeTemplate($template_hash, $template_content);
if (!empty($tokenized['tokens'])) {
$values['token_options'] = !empty($this->element['#with_tokens']['options']) ? $this->element['#with_tokens']['options'] : [];
$token_data = !empty($this->element['#with_tokens']['data']) ? $this->element['#with_tokens']['data'] : [];
$token_processor->processData($token_data, $tokenized['tokens'], 'view', $bubbleable_metadata);
}
}
$build->withPlaceholder($server_side_render);
unset($build_render_array['#inline']);
$build_render_array['#template'] = 'mustache_magic_sync';
if ($force_proxy && $url_exists) {
$values['url'] = $url->toString();
}
$datas = isset($token_data) ? [
'data' => $data,
'token_data' => $token_data,
] : ['data' => $data];
foreach ($datas as $d_k => $d_v) {
$values[$d_k] = [];
foreach ($d_v as $t_key => $t_value) {
if ($t_value instanceof EntityInterface) {
if (!$t_value->isNew()) {
$values[$d_k][$t_key] = [
$t_value->getEntityTypeId(),
$t_value->language()->getId(),
$t_value->id(),
];
}
elseif ($uuid = $t_value->uuid()) {
$values[$d_k][$t_key] = [$t_value->getEntityTypeId(), $uuid];
}
}
elseif (is_scalar($t_value) || $t_value instanceof \JsonSerializable) {
$values[$d_k][$t_key] = $t_value;
}
else {
$values[$d_k][$t_key] = NULL;
}
}
}
$sync_hash = $this->syncStorage->generateHash($values);
if (is_null($this->syncStorage->get($sync_hash))) {
if ($sync_hash != $this->syncStorage->set($values)) {
throw new \LogicException("The Mustache sync storage is behaving unexpected: Received a different hash for given values other than previously generated.");
}
}
$sync_options
->usingDataFromUrl(static::$syncUrl . 'h=' . urlencode($sync_hash));
}
else {
foreach ($to_merge as $m_key => $m_val) {
if ($m_key === '#sync') {
$sync_render_array = &$sync_options->toRenderArray();
$sync_render_array = NestedArray::mergeDeep($sync_render_array, $to_merge['#sync']);
}
elseif (isset($build_render_array[$m_key]) && is_array($build_render_array[$m_key])) {
$build_render_array[$m_key] = NestedArray::mergeDeep($build_render_array[$m_key], $m_val);
}
else {
$build_render_array[$m_key] = $m_val;
}
}
}
if (isset($build_render_array['#inline']) && (isset($template_name) || !empty($summable))) {
if (!isset($summable)) {
$summable = \Drupal::service('mustache.summables')->isEnabled();
}
$template_name = $template_name ?? $template_hash;
/** @var \Drupal\mustache_magic\Storage\MustacheTemplateStorage $template_storage */
$template_storage = \Drupal::service('mustache.template_storage');
$template_values = [
'name' => $template_name,
'content' => $template_content,
];
if (!$summable) {
$template_values['default']['#summable'] = FALSE;
}
$template_storage_hash = $template_storage->generateHash($template_values);
$existing = $template_storage->get($template_storage_hash);
if (is_null($existing) || ($template_storage->hashValues($existing) !== $template_storage->hashValues($template_values))) {
if ($template_storage_hash != $template_storage->set($template_values)) {
throw new \LogicException("The Mustache template storage is behaving unexpected: Received a different hash for given values other than previously generated.");
}
// Clearing all cached definitions is unfortunate, but required.
mustache_cache_flush();
if ($summable) {
\Drupal::service('mustache.summables')->clearCaches();
}
}
unset($build_render_array['#inline']);
$build_render_array['#template'] = $template_name;
}
$renderer = $this->renderer;
$rendered = $renderer->executeInRenderContext(new RenderContext(), function () use ($renderer, &$build_render_array) {
return $renderer->render($build_render_array);
});
$bubbleable_metadata
->merge(BubbleableMetadata::createFromRenderArray($this->element))
->merge(BubbleableMetadata::createFromRenderArray($build_render_array))
->applyTo($this->element);
// We need to replace the opening curly brackets, so that Mustache.php's
// lambda helper cannot chime in and would try to replace Mustache
// variables within the inline template with current context values.
return Markup::create(str_replace('{{', '\{\{', (string) $rendered));
}
}
