<?php
/**
* @package Gantry5
* @author RocketTheme http://www.rockettheme.com
* @copyright Copyright (C) 2007 - 2021 RocketTheme, LLC
* @license Dual License: MIT or GNU/GPLv2 and later
*
* http://opensource.org/licenses/MIT
* http://www.gnu.org/licenses/gpl-2.0.html
*
* Gantry Framework code that extends GPL code is considered GNU/GPLv2 and later
*/
namespace Gantry\Component\Layout\Version;
/**
* Read layout from simplified yaml file.
*/
class Format2
{
/** @var array */
protected $scopes = [0 => 'grid', 1 => 'block'];
/** @var array */
protected $sections = ['wrapper', 'container', 'section', 'grid', 'block', 'offcanvas'];
/** @var array */
protected $structures = ['div', 'section', 'aside', 'nav', 'article', 'header', 'footer', 'main'];
/** @var array */
protected $data;
/** @var array */
protected $structure;
/** @var array */
protected $content;
/** @var array */
protected $keys;
/**
* @param array $data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* @return array
*/
public function load()
{
$data = &$this->data;
// Parse layout.
$result = [];
foreach ($data['layout'] as $field => &$params) {
if (!is_array($params)) {
$params = [];
}
$child = $this->parse($field, $params);
unset($child->size);
$result[] = $child;
}
return ['preset' => $data['preset']] + $result;
}
/**
* @param array $preset
* @param array $structure
* @return array
*/
public function store(array $preset, array $structure)
{
$this->structure = [];
$this->content = [];
$structure = ['children' => json_decode(json_encode($structure), true)];
$structure = $this->build($structure);
$result = [
'version' => 2,
'preset' => $preset,
'layout' => $structure
];
if (!empty($this->structure)) {
$result['structure'] = $this->structure;
}
if (!empty($this->content)) {
$result['content'] = $this->content;
}
return $result;
}
/**
* @param int|string $field
* @param array $content
* @param int $scope
* @param object $parent
* @return object
*/
protected function parse($field, array &$content, $scope = 0, $parent = null)
{
if (is_numeric($field)) {
// Row or block
$result = (object)['id' => $this->id($this->scopes[$scope]), 'type' => $this->scopes[$scope], 'subtype' => $this->scopes[$scope], 'layout' => true, 'attributes' => (object)[]];
$scope = ($scope + 1) % 2;
} else {
list ($type, $subtype, $id, $size, $section_id, $boxed) = $this->parseSectionString($field);
if ($type === 'grid') {
$scope = 1;
}
if ($type === 'block') {
$scope = 0;
}
// Build object.
$result = isset($this->data['structure'][$section_id]) ? (array) $this->data['structure'][$section_id] : [];
$result += [
'id' => $section_id,
'layout' => true,
'type' => $type,
'subtype' => $subtype,
'title' => $this->getTitle($type, $subtype, $id),
'attributes' => []
];
if (isset($boxed) && !isset($result['attributes']['boxed'])) {
$result['attributes']['boxed'] = $boxed;
}
if ($parent && $parent->type === 'block' && !empty($result['block'])) {
$parent->attributes = (object) ($result['block'] + (array) $parent->attributes);
}
unset ($result['block']);
$result = (object) $result;
$result->attributes = (object) $result->attributes;
if (isset($result->inherit)) {
$result->inherit = (object) $result->inherit;
}
if ($size) {
$result->size = $size;
}
if (($type === 'grid' || $type === 'block') && !isset($result->attributes->id)) {
$result->attributes->id = $section_id;
}
}
if (!empty($content)) {
$result->children = [];
foreach ($content as $child => &$params) {
if (!$params && !is_array($params)) {
$params = [];
}
if (is_array($params)) {
$child = $this->parse($child, $params, $scope, $result);
} else {
$child = $this->resolve($params, $scope, $result);
}
if (!empty($child->size)) {
$result->attributes->size = $child->size;
}
unset($child->size);
$result->children[] = $child;
}
}
return $result;
}
/**
* @param string $field
* @param int $scope
* @param object $parent
* @return object
*/
protected function resolve($field, $scope, $parent)
{
list ($type, $subtype, $id, $size, $content_id) = $this->parseContentString($field);
$title = $this->getTitle($type, $subtype, $id);
$result = isset($this->data['content'][$content_id]) ? (array) $this->data['content'][$content_id] : [];
$result += ['id' => $this->id($type, $subtype, $id), 'title' => $title, 'type' => $type, 'subtype' => $subtype, 'attributes' => []];
$result['attributes'] = (object) ($result['attributes'] + ['enabled' => 1]);
if (isset($result['inherit'])) {
$result['inherit'] = (object) $result['inherit'];
}
if (isset($result['block'])) {
$block = $result['block'];
unset ($result['block']);
}
$result = (object) $result;
if ($type === 'position' && !isset($result->attributes->key) && !in_array($subtype, ['module', 'widget'])) {
$result->attributes->key = $id;
}
if ($scope > 1) {
if ($parent->type === 'block' && !empty($block)) {
$parent->attributes = (object) ($block + (array) $parent->attributes);
}
if ($size) {
$result->attributes->size = $size;
}
}
if ($scope <= 1) {
$result = (object) ['id' => $this->id('block'), 'type' => 'block', 'subtype' => 'block', 'layout' => true, 'children' => [$result], 'attributes' => new \stdClass()];
if (!empty($block)) {
$result->attributes = (object) $block;
}
if ($size) {
$result->attributes->size = $size;
}
}
if ($scope === 0) {
$result = (object) ['id' => $this->id('grid'), 'type' => 'grid', 'subtype' => 'grid', 'layout' => true, 'children' => [$result], 'attributes' => new \stdClass()];
}
return $result;
}
/**
* @param array $content
* @return array|null
*/
protected function build(array &$content)
{
$result = [];
$ctype = isset($content['type']) ? $content['type'] : null;
if (in_array($ctype, ['grid', 'block'])) {
if (empty($content['attributes']['id']) || $content['attributes']['id'] === $content['id']) {
unset ($content['attributes']['id']);
}
}
if ($ctype === 'block') {
if (empty($content['attributes']['extra'])) {
unset ($content['attributes']['extra']);
}
if (empty($content['attributes']['fixed'])) {
unset ($content['attributes']['fixed']);
}
}
if ($ctype === 'section') {
if (empty($content['attributes']['extra'])) {
unset ($content['attributes']['extra']);
}
}
if (!isset($content['children'])) {
$content['children'] = [];
}
unset ($content['layout']);
// Clean up all items for saving.
foreach ($content['children'] as &$child) {
$size = null;
$id = $child['id'];
$type = $child['type'];
$subtype = $child['subtype'];
$isSection = in_array($type, $this->sections, true);
if (empty($child['inherit']['outline']) || empty($child['inherit']['include'])) {
unset ($child['inherit']);
} else {
foreach ($child['inherit']['include'] as $include) {
switch ($include) {
case 'attributes':
unset($child['attributes']);
break;
case 'block':
if ($ctype === 'block') {
// Keep block size and fixed status.
$attributes = !empty($content['attributes']) ? $content['attributes'] : [];
$content['attributes'] = array_intersect_key($attributes, ['fixed' => 1, 'size' => 1]);
}
break;
case 'children':
$child['children'] = [];
break;
}
}
}
if (!$isSection) {
// Special handling for positions.
if ($type === 'position') {
// TODO: we may want to simplify position id, but we need to take into account multiple instances of the same position key.
/*
if (!$subtype || $subtype === 'position') {
$id = 'position-' . (isset($child['attributes']['key']) ? $child['attributes']['key'] : rand(1000,9999));
unset ($child['attributes']['key']);
}
*/
unset ($child['attributes']['title']);
}
$value = $id;
if (!empty($child['attributes']['enabled'])) {
unset ($child['attributes']['enabled']);
}
} else {
// Recursively handle structure.
$value = $this->build($child);
}
// Clean up defaults.
if (empty($child['title']) || $child['title'] === 'Untitled' || $child['title'] === $this->getTitle($type, $subtype, $id)) {
unset ($child['title']);
}
if (!$subtype || $subtype === $type) {
unset ($child['subtype']);
}
// Remove id and children as we store data in flat structure with id being the key.
unset ($child['id'], $child['children']);
if ($type === 'offcanvas' && isset($child['attributes']['name'])) {
unset ($child['attributes']['name']);
}
if ($ctype === 'block') {
// Embed size into array key/value.
if (isset($content['attributes']['size']) && $content['attributes']['size'] != 100) {
$size = $content['attributes']['size'];
}
unset ($content['attributes']['size']);
// Embed parent block.
if (!empty($content['attributes'])) {
$child['block'] = $content['attributes'];
unset ($content['attributes']);
}
}
if (isset($child['attributes']['size'])) {
if ($child['attributes']['size'] != 100 && is_string($value)) {
$size = $child['attributes']['size'];
}
unset ($child['attributes']['size']);
}
// Remove attributes if there aren't any.
if (empty($child['attributes'])) {
unset ($child['attributes']);
}
// Special handling for grid and block elements.
if (in_array($type, ['grid', 'block'], true) && count($child) === 1 && isset($child['type'])) {
$id = "";
}
// Check if type and subtype can be generated from the id.
if ($subtype && (preg_match("/^{$type}-{$subtype}(-|$)/", $id))
|| (in_array($type, ['section', 'particle']) && preg_match("/^{$subtype}(-|$)/", $id))) {
unset ($child['type'], $child['subtype']);
} elseif (preg_match("/^{$type}(-|$)/", $id)) {
unset ($child['type']);
}
// Add item configuration if not empty.
if ($id && !empty($child)) {
if (!is_string($value)) {
$this->structure[$id] = $child;
} else {
$this->content[$id] = $child;
}
}
// Add item to the layout.
if (!is_string($value)) {
// Add structural item.
if ($id) {
// Sections and other complex items.
$id = isset($child['attributes']['boxed']) ? "/{$id}/" : $id;
$result[trim("{$id} {$size}")] = $value;
} elseif (!empty($value)) {
// Simple grid / block item.
$result[] = $value;
}
} else {
// Add content item.
$result[] = trim("{$value} {$size}");
}
}
unset($child);
// TODO: maybe collapse grid as well?
if ($ctype && $ctype === 'block' && count($result) <= 1 && key($result) === 0) {
unset ($this->structure[$content['id']]);
return reset($result) ?: null;
}
return $result;
}
/**
* @param string $string
* @return array
*/
protected function parseSectionString($string)
{
// Extract: "[section-id] [size]".
$list = explode(' ', $string, 2);
$section_id = array_shift($list);
$size = ((float) array_shift($list)) ?: null;
// Extract slashes from "/[section-id]/".
$boxedLeft = $section_id[0] === '/';
$boxedRight = $section_id[strlen($section_id)-1] === '/';
$boxed = ($boxedLeft && $boxedRight ? '' : ($boxedLeft ? '1' : ($boxedRight ? '0' : null)));
$section_id = trim($section_id, '/');
// Extract section id if it exists: "[section]-[id]".
$list = explode('-', $section_id, 2);
// Get section and its type.
$section = reset($list);
$type = in_array($section, $this->sections, true) ? $section : 'section';
$subtype = ($type !== 'section' || in_array($section, $this->structures, true)) ? $section : 'section';
// Extract id.
if ($type === 'section' && in_array($section, $this->structures, true)) {
$id = array_pop($list);
} else {
$id = $section_id;
}
return [$type, $subtype, $id, $size, $section_id, $boxed];
}
/**
* @param string $string
* @return array
*/
protected function parseContentString($string)
{
// Extract: "[type-subtype] [size]".
$list = explode(' ', $string, 2);
$content_id = array_shift($list);
$size = ((float) array_shift($list)) ?: null;
// Extract sub-type if it exists: "[type]-[subtype]-[id]".
$list = explode('-', $content_id);
// Get type, subtype and id.
$type = reset($list);
$test = end($list);
$id = ((string)(int) $test === (string) $test) ? array_pop($list) : null;
if (in_array($type, ['system', 'position', 'particle', 'spacer'])) {
array_shift($list);
} else {
$type = 'particle';
}
$subtype = implode('-', $list);
if ($type === 'position' && !in_array($subtype, ['module', 'widget'])) {
$id = ($subtype ?: $type) . ($id !== null ? "-{$id}" : '');
$subtype = 'position';
}
return [$type, $subtype ?: $type, $id, $size, $content_id];
}
/**
* @param string $type
* @param string $subtype
* @param string $id
* @return string|null
*/
protected function getTitle($type, $subtype, $id)
{
if (in_array($type, $this->sections, true)) {
if ($type === 'offcanvas') {
return 'Offcanvas';
}
if ($type === 'grid' || $type === 'block') {
return null;
}
return ucfirst((string)(int) $id === (string) $id ? ($subtype ?: $type) . "-{$id}" : $id);
}
if ($type === 'position' && !in_array($subtype, ['module', 'widget'])) {
return ucfirst(preg_replace('/^position-(.*?[a-z])/ui', '\1', $id));
}
if ($type === 'system') {
if ($subtype === 'messages') {
return 'System Messages';
}
if ($subtype === 'content') {
return 'Page Content';
}
}
return ucfirst($subtype ?: $type);
}
/**
* @param string $type
* @param string $subtype
* @param string|int $id
* @return string
*/
protected function id($type, $subtype = null, $id = null)
{
$result = [];
if ($type !== 'particle') {
$result[] = $type;
}
if ($subtype && $subtype !== $type) {
$result[] = $subtype;
}
$key = implode('-', $result);
if (!$id || isset($this->keys[$key][$id])) {
do {
$id = mt_rand(1000, 9999);
if (!isset($this->keys[$key][$id])) {
break;
}
} while (true);
}
$this->keys[$key][$id] = true;
return $key . '-'. $id;
}
}