Update Composer packages
This commit is contained in:
parent
67c3d8b307
commit
83cb211fe6
219 changed files with 6487 additions and 4444 deletions
123
kirby/src/Template/Slot.php
Normal file
123
kirby/src/Template/Slot.php
Normal file
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Template;
|
||||
|
||||
use Kirby\Exception\LogicException;
|
||||
|
||||
/**
|
||||
* The slot class catches all content
|
||||
* between the beginning and the end of
|
||||
* a slot. Slot content is then stored
|
||||
* in the Slots collection.
|
||||
*
|
||||
* @package Kirby Template
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Slot
|
||||
{
|
||||
/**
|
||||
* The captured slot content
|
||||
* @internal
|
||||
*/
|
||||
public string|null $content;
|
||||
|
||||
/**
|
||||
* The name that was declared during
|
||||
* the definition of the slot
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* Keeps track of the slot state
|
||||
*/
|
||||
protected bool $open = false;
|
||||
|
||||
/**
|
||||
* Creates a new slot
|
||||
*/
|
||||
public function __construct(string $name, string|null $content = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the slot content or an empty string
|
||||
* if the slot is empty.
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->render() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the slot helper
|
||||
*/
|
||||
public static function begin(string $name = 'default'): static|null
|
||||
{
|
||||
return Snippet::$current?->slot($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a slot and catches all the content
|
||||
* that has been printed since the slot has
|
||||
* been opened
|
||||
*/
|
||||
public function close(): void
|
||||
{
|
||||
if ($this->open === false) {
|
||||
throw new LogicException('The slot has not been opened');
|
||||
}
|
||||
|
||||
$this->content = ob_get_clean();
|
||||
$this->open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the endslot() helper
|
||||
*/
|
||||
public static function end(): void
|
||||
{
|
||||
Snippet::$current?->endslot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the slot is currently
|
||||
* open and being buffered
|
||||
*/
|
||||
public function isOpen(): bool
|
||||
{
|
||||
return $this->open;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slot name
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the slot and starts
|
||||
* output buffering
|
||||
*/
|
||||
public function open(): void
|
||||
{
|
||||
$this->open = true;
|
||||
|
||||
// capture the output
|
||||
ob_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slot content
|
||||
*/
|
||||
public function render(): string|null
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
53
kirby/src/Template/Slots.php
Normal file
53
kirby/src/Template/Slots.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Template;
|
||||
|
||||
use Countable;
|
||||
|
||||
/**
|
||||
* The slots collection is simplifying
|
||||
* slot access. Slots can be accessed with
|
||||
* `$slots->heading()` and accessing a non-existing
|
||||
* slot will simply return null.
|
||||
*
|
||||
* @package Kirby Template
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Slots implements Countable
|
||||
{
|
||||
/**
|
||||
* Creates a new slots collection
|
||||
*/
|
||||
public function __construct(protected array $slots)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter for slots;
|
||||
* e.g. `$slots->heading`
|
||||
*/
|
||||
public function __get(string $name): Slot|null
|
||||
{
|
||||
return $this->slots[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter method for slots;
|
||||
* e.g. `$slots->heading()`
|
||||
*/
|
||||
public function __call(string $name, array $args): Slot|null
|
||||
{
|
||||
return $this->__get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of defined slots
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->slots);
|
||||
}
|
||||
}
|
324
kirby/src/Template/Snippet.php
Normal file
324
kirby/src/Template/Snippet.php
Normal file
|
@ -0,0 +1,324 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Template;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Helpers;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Exception\LogicException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
|
||||
/**
|
||||
* The Snippet class includes shared code parts
|
||||
* in templates and allows to pass data as well as to
|
||||
* optionally pass content to various predefined slots.
|
||||
*
|
||||
* @package Kirby Template
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>,
|
||||
* Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Snippet extends Tpl
|
||||
{
|
||||
/**
|
||||
* Cache for the currently active
|
||||
* snippet. This is used to start
|
||||
* and end slots within this snippet
|
||||
* in the helper functions
|
||||
* @internal
|
||||
*/
|
||||
public static self|null $current = null;
|
||||
|
||||
/**
|
||||
* Contains all slots that are opened
|
||||
* but not yet closed
|
||||
*/
|
||||
protected array $capture = [];
|
||||
|
||||
/**
|
||||
* Associative array with variables that
|
||||
* will be set inside the snippet
|
||||
*/
|
||||
protected array $data;
|
||||
|
||||
/**
|
||||
* An empty dummy slots object used for snippets
|
||||
* that were loaded without passing slots
|
||||
*/
|
||||
protected static Slots|null $dummySlots = null;
|
||||
|
||||
/**
|
||||
* Full path to the PHP file of the snippet;
|
||||
* can be `null` for "dummy" snippets that don't exist
|
||||
*/
|
||||
protected string|null $file;
|
||||
|
||||
/**
|
||||
* Keeps track of the state of the snippet
|
||||
*/
|
||||
protected bool $open = false;
|
||||
|
||||
/**
|
||||
* The parent snippet
|
||||
*/
|
||||
protected self|null $parent = null;
|
||||
|
||||
/**
|
||||
* The collection of closed slots that will be used
|
||||
* to pass down to the template for the snippet.
|
||||
*/
|
||||
protected array $slots = [];
|
||||
|
||||
/**
|
||||
* Creates a new snippet
|
||||
*/
|
||||
public function __construct(string|null $file, array $data = [])
|
||||
{
|
||||
$this->file = $file;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and opens a new snippet. This can be used
|
||||
* directly in a template or via the slots() helper
|
||||
*/
|
||||
public static function begin(string|null $file, array $data = []): static
|
||||
{
|
||||
$snippet = new static($file, $data);
|
||||
return $snippet->open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the snippet and catches
|
||||
* the default slot if no slots have been
|
||||
* defined in between opening and closing.
|
||||
*/
|
||||
public function close(): static
|
||||
{
|
||||
// make sure that ending a snippet
|
||||
// is only supported if the snippet has
|
||||
// been started before
|
||||
if ($this->open === false) {
|
||||
throw new LogicException('The snippet has not been opened');
|
||||
}
|
||||
|
||||
// create a default slot for the content
|
||||
// that has been captured between start and end
|
||||
if (empty($this->slots) === true) {
|
||||
$this->slots['default'] = new Slot('default');
|
||||
$this->slots['default']->content = ob_get_clean();
|
||||
} else {
|
||||
// swallow any "unslotted" content
|
||||
// between start and end
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$this->open = false;
|
||||
|
||||
// switch back to the parent in nested
|
||||
// snippet stacks
|
||||
static::$current = $this->parent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in the endsnippet() helper
|
||||
*/
|
||||
public static function end(): void
|
||||
{
|
||||
echo static::$current?->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the last openend slot
|
||||
*/
|
||||
public function endslot(): void
|
||||
{
|
||||
// take the last slot from the capture stack
|
||||
$slot = array_pop($this->capture);
|
||||
|
||||
// capture the content and close the slot
|
||||
$slot->close();
|
||||
|
||||
// add the slot to the scope
|
||||
$this->slots[$slot->name()] = $slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns either an open snippet capturing slots
|
||||
* or the template string for self-enclosed snippets
|
||||
*/
|
||||
public static function factory(
|
||||
string|array|null $name,
|
||||
array $data = [],
|
||||
bool $slots = false
|
||||
): static|string {
|
||||
// instead of returning empty string when `$name` is null
|
||||
// allow rest of code to run, otherwise the wrong snippet would be closed
|
||||
// and potential issues for nested snippets may occur
|
||||
$file = $name !== null ? static::file($name) : null;
|
||||
|
||||
// for snippets with slots, make sure to open a new
|
||||
// snippet and start capturing slots
|
||||
if ($slots === true) {
|
||||
return static::begin($file, $data);
|
||||
}
|
||||
|
||||
// for snippets without slots, directly load and return
|
||||
// the snippet's template file
|
||||
return static::load($file, static::scope($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to the file for
|
||||
* the snippet/s taking snippets defined in plugins
|
||||
* into account
|
||||
*/
|
||||
public static function file(string|array $name): string|null
|
||||
{
|
||||
$kirby = App::instance();
|
||||
$root = static::root();
|
||||
$names = A::wrap($name);
|
||||
|
||||
foreach ($names as $name) {
|
||||
$name = (string)$name;
|
||||
$file = $root . '/' . $name . '.php';
|
||||
|
||||
if (file_exists($file) === false) {
|
||||
$file = $kirby->extensions('snippets')[$name] ?? null;
|
||||
}
|
||||
|
||||
if ($file) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the snippet and starts output
|
||||
* buffering to catch all slots in between
|
||||
*/
|
||||
public function open(): static
|
||||
{
|
||||
if (static::$current !== null) {
|
||||
$this->parent = static::$current;
|
||||
}
|
||||
|
||||
$this->open = true;
|
||||
static::$current = $this;
|
||||
|
||||
ob_start();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent snippet if it exists
|
||||
*/
|
||||
public function parent(): static|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the snippet and passes the scope
|
||||
* with all slots and data
|
||||
*/
|
||||
public function render(array $data = [], array $slots = []): string
|
||||
{
|
||||
// always make sure that the snippet
|
||||
// is closed before it can be rendered
|
||||
if ($this->open === true) {
|
||||
$this->close();
|
||||
}
|
||||
|
||||
// manually add slots
|
||||
foreach ($slots as $slotName => $slotContent) {
|
||||
$this->slots[$slotName] = new Slot($slotName, $slotContent);
|
||||
}
|
||||
|
||||
// custom data overrides for the data that was passed to the snippet instance
|
||||
$data = array_replace_recursive($this->data, $data);
|
||||
|
||||
return static::load($this->file, static::scope($data, $this->slots()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root directory for all
|
||||
* snippet templates
|
||||
*/
|
||||
public static function root(): string
|
||||
{
|
||||
return App::instance()->root('snippets');
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new slot with the given name
|
||||
*/
|
||||
public function slot(string $name = 'default'): Slot
|
||||
{
|
||||
$slot = new Slot($name);
|
||||
$slot->open();
|
||||
|
||||
// start a new slot
|
||||
$this->capture[] = $slot;
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slots collection
|
||||
*/
|
||||
public function slots(): Slots
|
||||
{
|
||||
return new Slots($this->slots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data variables that get passed to a snippet
|
||||
*
|
||||
* @param \Kirby\Template\Slots|null $slots If null, an empty dummy object is used
|
||||
*/
|
||||
protected static function scope(array $data = [], Slots|null $slots = null): array
|
||||
{
|
||||
// initialize a dummy slots object and cache it for better performance
|
||||
if ($slots === null) {
|
||||
$slots = static::$dummySlots ??= new Slots([]);
|
||||
}
|
||||
|
||||
$data = array_merge(App::instance()->data, $data);
|
||||
|
||||
// TODO 3.10: Replace the following code:
|
||||
// if (
|
||||
// array_key_exists('slot', $data) === true ||
|
||||
// array_key_exists('slots', $data) === true
|
||||
// ) {
|
||||
// throw new InvalidArgumentException('Passing the $slot or $slots variables to snippets is not supported.');
|
||||
// }
|
||||
//
|
||||
// return array_merge($data, [
|
||||
// 'slot' => $slots->default,
|
||||
// 'slots' => $slots,
|
||||
// ]);
|
||||
|
||||
// @codeCoverageIgnoreStart
|
||||
if (
|
||||
array_key_exists('slot', $data) === true ||
|
||||
array_key_exists('slots', $data) === true
|
||||
) {
|
||||
Helpers::deprecated('Passing the $slot or $slots variables to snippets is deprecated and will break in a future version.', 'snippet-pass-slots');
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
return array_merge([
|
||||
'slot' => $slots->default,
|
||||
'slots' => $slots,
|
||||
], $data);
|
||||
}
|
||||
}
|
208
kirby/src/Template/Template.php
Normal file
208
kirby/src/Template/Template.php
Normal file
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Template;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\Tpl;
|
||||
|
||||
/**
|
||||
* Represents a Kirby template and takes care
|
||||
* of loading the correct file.
|
||||
*
|
||||
* @package Kirby Template
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
*/
|
||||
class Template
|
||||
{
|
||||
/**
|
||||
* Global template data
|
||||
*/
|
||||
public static array $data = [];
|
||||
|
||||
/**
|
||||
* Default template type if no specific type is set
|
||||
*/
|
||||
protected string $defaultType;
|
||||
|
||||
/**
|
||||
* The name of the template
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* Template type (html, json, etc.)
|
||||
*/
|
||||
protected string $type;
|
||||
|
||||
/**
|
||||
* Creates a new template object
|
||||
*/
|
||||
public function __construct(string $name, string $type = 'html', string $defaultType = 'html')
|
||||
{
|
||||
$this->name = strtolower($name);
|
||||
$this->type = $type;
|
||||
$this->defaultType = $defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the object to a simple string
|
||||
* This is used in template filters for example
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default template type
|
||||
*/
|
||||
public function defaultType(): string
|
||||
{
|
||||
return $this->defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the template exists
|
||||
*/
|
||||
public function exists(): bool
|
||||
{
|
||||
if ($file = $this->file()) {
|
||||
return file_exists($file);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected template file extension
|
||||
*/
|
||||
public function extension(): string
|
||||
{
|
||||
return 'php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the location of the template file
|
||||
* if it exists.
|
||||
*/
|
||||
public function file(): string|null
|
||||
{
|
||||
$name = $this->name();
|
||||
$extension = $this->extension();
|
||||
$store = $this->store();
|
||||
$root = $this->root();
|
||||
|
||||
if ($this->hasDefaultType() === true) {
|
||||
try {
|
||||
// Try the default template in the default template directory.
|
||||
return F::realpath($root . '/' . $name . '.' . $extension, $root);
|
||||
} catch (Exception) {
|
||||
// ignore errors, continue searching
|
||||
}
|
||||
|
||||
// Look for the default template provided by an extension.
|
||||
$path = App::instance()->extension($store, $name);
|
||||
|
||||
if ($path !== null) {
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
$name .= '.' . $this->type();
|
||||
|
||||
try {
|
||||
// Try the template with type extension in the default template directory.
|
||||
return F::realpath($root . '/' . $name . '.' . $extension, $root);
|
||||
} catch (Exception) {
|
||||
// Look for the template with type extension provided by an extension.
|
||||
// This might be null if the template does not exist.
|
||||
return App::instance()->extension($store, $name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the template uses the default type
|
||||
*/
|
||||
public function hasDefaultType(): bool
|
||||
{
|
||||
return $this->type() === $this->defaultType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the template name
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the template with the given template data
|
||||
*/
|
||||
public function render(array $data = []): string
|
||||
{
|
||||
// if the template is rendered inside a snippet,
|
||||
// we need to keep the "outside" snippet object
|
||||
// to compare it later
|
||||
$snippet = Snippet::$current;
|
||||
|
||||
// load the template
|
||||
$template = Tpl::load($this->file(), $data);
|
||||
|
||||
// if last `endsnippet()` inside the current template
|
||||
// has been omitted (= snippet was used as layout snippet),
|
||||
// `Snippet::$current` will point to a snippet that was
|
||||
// opened inside the template; if that snippet is the direct
|
||||
// child of the snippet that was open before the template was
|
||||
// rendered (which could be `null` if no snippet was open),
|
||||
// take the buffer output from the template as default slot
|
||||
// and render the snippet as final template output
|
||||
if (
|
||||
Snippet::$current === null ||
|
||||
Snippet::$current->parent() !== $snippet
|
||||
) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// no slots have been defined, but the template code
|
||||
// should be used as default slot
|
||||
if (Snippet::$current->slots()->count() === 0) {
|
||||
return Snippet::$current->render($data, [
|
||||
'default' => $template
|
||||
]);
|
||||
}
|
||||
|
||||
// let the snippet close and render natively
|
||||
return Snippet::$current->render($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root to the templates directory
|
||||
*/
|
||||
public function root(): string
|
||||
{
|
||||
return App::instance()->root($this->store());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the place where templates are located
|
||||
* in the site folder and and can be found in extensions
|
||||
*/
|
||||
public function store(): string
|
||||
{
|
||||
return 'templates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the template type
|
||||
*/
|
||||
public function type(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue