Initial commit
This commit is contained in:
commit
388079e6bb
1108 changed files with 330121 additions and 0 deletions
134
kirby/src/Panel/Lab/Category.php
Normal file
134
kirby/src/Panel/Lab/Category.php
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Category of lab examples located in
|
||||
* `kirby/panel/lab` and `site/lab`.
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 4.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Category
|
||||
{
|
||||
protected string $root;
|
||||
|
||||
public function __construct(
|
||||
protected string $id,
|
||||
string|null $root = null,
|
||||
protected array $props = []
|
||||
) {
|
||||
$this->root = $root ?? static::base() . '/' . $this->id;
|
||||
|
||||
if (F::exists($this->root . '/index.php', static::base()) === true) {
|
||||
$this->props = array_merge(
|
||||
require $this->root . '/index.php',
|
||||
$this->props
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function all(): array
|
||||
{
|
||||
// all core lab examples from `kirby/panel/lab`
|
||||
$examples = A::map(
|
||||
Dir::inventory(static::base())['children'],
|
||||
fn ($props) => (new static($props['dirname']))->toArray()
|
||||
);
|
||||
|
||||
// all custom lab examples from `site/lab`
|
||||
$custom = static::factory('site')->toArray();
|
||||
|
||||
array_push($examples, $custom);
|
||||
|
||||
return $examples;
|
||||
}
|
||||
|
||||
public static function base(): string
|
||||
{
|
||||
return App::instance()->root('panel') . '/lab';
|
||||
}
|
||||
|
||||
public function example(string $id, string|null $tab = null): Example
|
||||
{
|
||||
return new Example(parent: $this, id: $id, tab: $tab);
|
||||
}
|
||||
|
||||
public function examples(): array
|
||||
{
|
||||
return A::map(
|
||||
Dir::inventory($this->root)['children'],
|
||||
fn ($props) => $this->example($props['dirname'])->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
public static function factory(string $id)
|
||||
{
|
||||
return match ($id) {
|
||||
'site' => static::site(),
|
||||
default => new static($id)
|
||||
};
|
||||
}
|
||||
|
||||
public function icon(): string
|
||||
{
|
||||
return $this->props['icon'] ?? 'palette';
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public static function isInstalled(): bool
|
||||
{
|
||||
return Dir::exists(static::base()) === true;
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return $this->props['name'] ?? Str::label($this->id);
|
||||
}
|
||||
|
||||
public function root(): string
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
public static function site(): static
|
||||
{
|
||||
return new static(
|
||||
'site',
|
||||
App::instance()->root('site') . '/lab',
|
||||
[
|
||||
'name' => 'Your examples',
|
||||
'icon' => 'live'
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name(),
|
||||
'examples' => $this->examples(),
|
||||
'icon' => $this->icon(),
|
||||
'path' => Str::after(
|
||||
$this->root(),
|
||||
App::instance()->root('index')
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
194
kirby/src/Panel/Lab/Doc.php
Normal file
194
kirby/src/Panel/Lab/Doc.php
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Data;
|
||||
use Kirby\Panel\Lab\Doc\Event;
|
||||
use Kirby\Panel\Lab\Doc\Method;
|
||||
use Kirby\Panel\Lab\Doc\Prop;
|
||||
use Kirby\Panel\Lab\Doc\Slot;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Documentation for a single Vue component
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Doc
|
||||
{
|
||||
protected array $data;
|
||||
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $source,
|
||||
public string|null $description = null,
|
||||
public string|null $deprecated = null,
|
||||
public string|null $docBlock = null,
|
||||
public array $events = [],
|
||||
public array $examples = [],
|
||||
public bool $isUnstable = false,
|
||||
public array $methods = [],
|
||||
public array $props = [],
|
||||
public string|null $since = null,
|
||||
public array $slots = [],
|
||||
) {
|
||||
$this->description = Doc::kt($this->description ?? '');
|
||||
$this->deprecated = Doc::kt($this->deprecated ?? '');
|
||||
$this->docBlock = Doc::kt($this->docBlock ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a documentation file exists for the component
|
||||
*/
|
||||
public static function exists(string $name): bool
|
||||
{
|
||||
return
|
||||
file_exists(static::file($name, 'dist')) ||
|
||||
file_exists(static::file($name, 'dev'));
|
||||
}
|
||||
|
||||
public static function factory(string $name): static|null
|
||||
{
|
||||
// protect against path traversal
|
||||
$name = basename($name);
|
||||
|
||||
// read data
|
||||
$file = static::file($name, 'dev');
|
||||
|
||||
if (file_exists($file) === false) {
|
||||
$file = static::file($name, 'dist');
|
||||
}
|
||||
|
||||
$data = Data::read($file);
|
||||
|
||||
// filter internal components
|
||||
if (isset($data['tags']['internal']) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// helper function for gathering parts
|
||||
$gather = function (string $part, string $class) use ($data) {
|
||||
$parts = A::map(
|
||||
$data[$part] ?? [],
|
||||
fn ($x) => $class::factory($x)?->toArray()
|
||||
);
|
||||
|
||||
$parts = array_filter($parts);
|
||||
usort($parts, fn ($a, $b) => $a['name'] <=> $b['name']);
|
||||
|
||||
return $parts;
|
||||
};
|
||||
|
||||
return new static(
|
||||
name: $name,
|
||||
source: $data['sourceFile'],
|
||||
description: $data['description'] ?? null,
|
||||
deprecated: $data['tags']['deprecated'][0]['description'] ?? null,
|
||||
docBlock: $data['docsBlocks'][0] ?? null,
|
||||
examples: $data['tags']['examples'] ?? [],
|
||||
events: $gather('events', Event::class),
|
||||
isUnstable: isset($data['tags']['unstable']) === true,
|
||||
methods: $gather('methods', Method::class),
|
||||
props: $gather('props', Prop::class),
|
||||
since: $data['tags']['since'][0]['description'] ?? null,
|
||||
slots: $gather('slots', Slot::class)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the documentation file for the component
|
||||
*/
|
||||
public static function file(string $name, string $context): string
|
||||
{
|
||||
$root = match ($context) {
|
||||
'dev' => App::instance()->root('panel') . '/tmp',
|
||||
'dist' => App::instance()->root('panel') . '/dist/ui',
|
||||
};
|
||||
|
||||
$name = Str::after($name, 'k-');
|
||||
$name = Str::kebabToCamel($name);
|
||||
return $root . '/' . $name . '.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to resolve KirbyText
|
||||
*/
|
||||
public static function kt(string $text, bool $inline = false): string
|
||||
{
|
||||
return App::instance()->kirbytext($text, [
|
||||
'markdown' => [
|
||||
'breaks' => false,
|
||||
'inline' => $inline,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the Lab examples, if available
|
||||
*/
|
||||
public function lab(): string|null
|
||||
{
|
||||
$root = App::instance()->root('panel') . '/lab';
|
||||
|
||||
foreach (glob($root . '/{,*/,*/*/,*/*/*/}index.php', GLOB_BRACE) as $example) {
|
||||
$props = require $example;
|
||||
|
||||
if (($props['docs'] ?? null) === $this->name) {
|
||||
return Str::before(Str::after($example, $root), 'index.php');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function source(): string
|
||||
{
|
||||
return 'https://github.com/getkirby/kirby/tree/main/panel/' . $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data for this documentation
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'component' => $this->name,
|
||||
'deprecated' => $this->deprecated,
|
||||
'description' => $this->description,
|
||||
'docBlock' => $this->docBlock,
|
||||
'events' => $this->events,
|
||||
'examples' => $this->examples,
|
||||
'isUnstable' => $this->isUnstable,
|
||||
'methods' => $this->methods,
|
||||
'props' => $this->props,
|
||||
'since' => $this->since,
|
||||
'slots' => $this->slots,
|
||||
'source' => $this->source(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the information to display as
|
||||
* entry in a collection (e.g. on the Lab index view)
|
||||
*/
|
||||
public function toItem(): array
|
||||
{
|
||||
return [
|
||||
'image' => [
|
||||
'icon' => $this->isUnstable ? 'lab' : 'book',
|
||||
'back' => 'light-dark(white, var(--color-gray-800))',
|
||||
],
|
||||
'text' => $this->name,
|
||||
'link' => '/lab/docs/' . $this->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
46
kirby/src/Panel/Lab/Doc/Argument.php
Normal file
46
kirby/src/Panel/Lab/Doc/Argument.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab\Doc;
|
||||
|
||||
use Kirby\Panel\Lab\Doc;
|
||||
|
||||
/**
|
||||
* Documentation for a single argument for an event, slot or method
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Argument
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string|null $type = null,
|
||||
public string|null $description = null,
|
||||
) {
|
||||
$this->description = Doc::kt($this->description ?? '', true);
|
||||
}
|
||||
|
||||
public static function factory(array $data): static
|
||||
{
|
||||
return new static(
|
||||
name: $data['name'],
|
||||
type: $data['type']['names'][0] ?? null,
|
||||
description: $data['description'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'type' => $this->type,
|
||||
];
|
||||
}
|
||||
}
|
||||
57
kirby/src/Panel/Lab/Doc/Event.php
Normal file
57
kirby/src/Panel/Lab/Doc/Event.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab\Doc;
|
||||
|
||||
use Kirby\Panel\Lab\Doc;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
/**
|
||||
* Documentation for a single Vue emittable event
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Event
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string|null $description = null,
|
||||
public string|null $deprecated = null,
|
||||
public string|null $since = null,
|
||||
public array $properties = [],
|
||||
) {
|
||||
$this->description = Doc::kt($this->description ?? '');
|
||||
$this->deprecated = Doc::kt($this->deprecated ?? '');
|
||||
}
|
||||
|
||||
public static function factory(array $data): static
|
||||
{
|
||||
return new static(
|
||||
name: $data['name'],
|
||||
description: $data['description'] ?? null,
|
||||
deprecated: $data['tags']['deprecated'][0]['description'] ?? null,
|
||||
since: $data['tags']['since'][0]['description'] ?? null,
|
||||
properties: A::map(
|
||||
$data['properties'] ?? [],
|
||||
fn ($property) => Argument::factory($property)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'deprecated' => $this->deprecated,
|
||||
'properties' => $this->properties,
|
||||
'since' => $this->since,
|
||||
];
|
||||
}
|
||||
}
|
||||
60
kirby/src/Panel/Lab/Doc/Method.php
Normal file
60
kirby/src/Panel/Lab/Doc/Method.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab\Doc;
|
||||
|
||||
use Kirby\Panel\Lab\Doc;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
/**
|
||||
* Documentation for a single Vue component method
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Method
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string|null $description = null,
|
||||
public string|null $deprecated = null,
|
||||
public string|null $since = null,
|
||||
public string|null $returns = null,
|
||||
public array $params = [],
|
||||
) {
|
||||
$this->description = Doc::kt($this->description ?? '');
|
||||
$this->deprecated = Doc::kt($this->deprecated ?? '');
|
||||
}
|
||||
|
||||
public static function factory(array $data): static
|
||||
{
|
||||
return new static(
|
||||
name: $data['name'],
|
||||
description: $data['description'] ?? null,
|
||||
deprecated: $data['tags']['deprecated'][0]['description'] ?? null,
|
||||
since: $data['tags']['since'][0]['description'] ?? null,
|
||||
returns: $data['returns']['type']['name'] ?? null,
|
||||
params: A::map(
|
||||
$data['params'] ?? [],
|
||||
fn ($param) => Argument::factory($param)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'deprecated' => $this->deprecated,
|
||||
'params' => $this->params,
|
||||
'returns' => $this->returns,
|
||||
'since' => $this->since,
|
||||
];
|
||||
}
|
||||
}
|
||||
113
kirby/src/Panel/Lab/Doc/Prop.php
Normal file
113
kirby/src/Panel/Lab/Doc/Prop.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab\Doc;
|
||||
|
||||
use Kirby\Panel\Lab\Doc;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Documentation for a single Vue component prop
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Prop
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string|null $type = null,
|
||||
public string|null $description = null,
|
||||
public string|null $default = null,
|
||||
public string|null $deprecated = null,
|
||||
public string|null $example = null,
|
||||
public bool $required = false,
|
||||
public string|null $since = null,
|
||||
public string|null $value = null,
|
||||
public array $values = []
|
||||
) {
|
||||
$this->description = Doc::kt($this->description ?? '');
|
||||
$this->deprecated = Doc::kt($this->deprecated ?? '');
|
||||
}
|
||||
|
||||
public static function factory(array $data): static|null
|
||||
{
|
||||
// filter internal props
|
||||
if (isset($data['tags']['internal']) === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// filter unset props
|
||||
if (($type = $data['type']['name'] ?? null) === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new static(
|
||||
name: $data['name'],
|
||||
type: $type,
|
||||
default: self::normalizeDefault($data['defaultValue']['value'] ?? null, $type),
|
||||
description: $data['description'] ?? null,
|
||||
deprecated: $data['tags']['deprecated'][0]['description'] ?? null,
|
||||
example: $data['tags']['example'][0]['description'] ?? null,
|
||||
required: $data['required'] ?? false,
|
||||
since: $data['tags']['since'][0]['description'] ?? null,
|
||||
value: $data['tags']['value'][0]['description'] ?? null,
|
||||
values: $data['values'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
protected static function normalizeDefault(
|
||||
string|null $default,
|
||||
string|null $type
|
||||
): string|null {
|
||||
if ($default === null) {
|
||||
// if type is boolean primarily and no default
|
||||
// value has been set, add `false` as default
|
||||
// for clarity
|
||||
if (Str::startsWith($type, 'boolean')) {
|
||||
return 'false';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// normalize longform function
|
||||
if (preg_match('/function\(\) {.*return (.*);.*}/si', $default, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// normalize object shorthand function
|
||||
if (preg_match('/\(\) => \((.*)\)/si', $default, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// normalize all other defaults from shorthand function
|
||||
if (preg_match('/\(\) => (.*)/si', $default, $matches) === 1) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'default' => $this->default,
|
||||
'description' => $this->description,
|
||||
'deprecated' => $this->deprecated,
|
||||
'example' => $this->example,
|
||||
'required' => $this->required,
|
||||
'since' => $this->since,
|
||||
'type' => $this->type,
|
||||
'value' => $this->value,
|
||||
'values' => $this->values,
|
||||
];
|
||||
}
|
||||
}
|
||||
57
kirby/src/Panel/Lab/Doc/Slot.php
Normal file
57
kirby/src/Panel/Lab/Doc/Slot.php
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab\Doc;
|
||||
|
||||
use Kirby\Panel\Lab\Doc;
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
/**
|
||||
* Documentation for a single Vue slot
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Slot
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string|null $description = null,
|
||||
public string|null $deprecated = null,
|
||||
public string|null $since = null,
|
||||
public array $bindings = [],
|
||||
) {
|
||||
$this->description = Doc::kt($this->description ?? '');
|
||||
$this->deprecated = Doc::kt($this->deprecated ?? '');
|
||||
}
|
||||
|
||||
public static function factory(array $data): static
|
||||
{
|
||||
return new static(
|
||||
name: $data['name'],
|
||||
description: $data['description'] ?? null,
|
||||
deprecated: $data['tags']['deprecated'][0]['description'] ?? null,
|
||||
since: $data['tags']['since'][0]['description'] ?? null,
|
||||
bindings: A::map(
|
||||
$data['bindings'] ?? [],
|
||||
fn ($binding) => Argument::factory($binding)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'bindings' => $this->bindings,
|
||||
'description' => $this->description,
|
||||
'deprecated' => $this->deprecated,
|
||||
'since' => $this->since,
|
||||
];
|
||||
}
|
||||
}
|
||||
73
kirby/src/Panel/Lab/Docs.php
Normal file
73
kirby/src/Panel/Lab/Docs.php
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* Docs for Vue components
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Nico Hoffmann <nico@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 4.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Docs
|
||||
{
|
||||
/**
|
||||
* Returns list of all component docs
|
||||
* for the Lab index view
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$docs = [];
|
||||
$dist = static::root();
|
||||
$tmp = static::root(true);
|
||||
$files = Dir::inventory($dist)['files'];
|
||||
|
||||
if (Dir::exists($tmp) === true) {
|
||||
$files = [...$files, ...Dir::inventory($tmp)['files']];
|
||||
}
|
||||
|
||||
$docs = A::map(
|
||||
$files,
|
||||
function ($file) {
|
||||
$component = 'k-' . Str::camelToKebab(F::name($file['filename']));
|
||||
return Doc::factory($component)?->toItem();
|
||||
}
|
||||
);
|
||||
|
||||
$docs = array_filter($docs);
|
||||
usort($docs, fn ($a, $b) => $a['text'] <=> $b['text']);
|
||||
|
||||
return $docs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the Lab docs are installed
|
||||
*/
|
||||
public static function isInstalled(): bool
|
||||
{
|
||||
return Dir::exists(static::root()) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root path to directory where
|
||||
* the JSON files for each component are stored by vite
|
||||
*/
|
||||
public static function root(bool $tmp = false): string
|
||||
{
|
||||
return App::instance()->root('panel') . '/' . match ($tmp) {
|
||||
true => 'tmp',
|
||||
default => 'dist/ui',
|
||||
};
|
||||
}
|
||||
}
|
||||
297
kirby/src/Panel/Lab/Example.php
Normal file
297
kirby/src/Panel/Lab/Example.php
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Exception\NotFoundException;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Http\Response;
|
||||
|
||||
/**
|
||||
* One or multiple lab examples with one or multiple tabs
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 4.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Example
|
||||
{
|
||||
protected string $root;
|
||||
protected string|null $tab = null;
|
||||
protected array $tabs;
|
||||
|
||||
public function __construct(
|
||||
protected Category $parent,
|
||||
protected string $id,
|
||||
string|null $tab = null,
|
||||
) {
|
||||
$this->root = $this->parent->root() . '/' . $this->id;
|
||||
|
||||
if ($this->exists() === false) {
|
||||
throw new NotFoundException(
|
||||
message: 'The example could not be found'
|
||||
);
|
||||
}
|
||||
|
||||
$this->tabs = $this->collectTabs();
|
||||
$this->tab = $this->collectTab($tab);
|
||||
}
|
||||
|
||||
public function collectTab(string|null $tab): string|null
|
||||
{
|
||||
if ($this->tabs === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($tab !== null && array_key_exists($tab, $this->tabs) === true) {
|
||||
return $tab;
|
||||
}
|
||||
|
||||
return array_key_first($this->tabs);
|
||||
}
|
||||
|
||||
public function collectTabs(): array
|
||||
{
|
||||
$tabs = [];
|
||||
|
||||
foreach (Dir::inventory($this->root)['children'] as $child) {
|
||||
$tabs[$child['dirname']] = [
|
||||
'name' => $child['dirname'],
|
||||
'label' => $child['slug'],
|
||||
'link' => '/lab/' . $this->parent->id() . '/' . $this->id . '/' . $child['dirname']
|
||||
];
|
||||
}
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
public function exists(): bool
|
||||
{
|
||||
return Dir::exists($this->root, $this->parent->root()) === true;
|
||||
}
|
||||
|
||||
public function file(string $filename): string
|
||||
{
|
||||
return $this->parent->root() . '/' . $this->path() . '/' . $filename;
|
||||
}
|
||||
|
||||
public function id(): string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function load(string $filename): array|null
|
||||
{
|
||||
if ($file = $this->file($filename)) {
|
||||
return F::load($file);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function module(): string
|
||||
{
|
||||
return $this->url() . '/index.vue';
|
||||
}
|
||||
|
||||
public function path(): string
|
||||
{
|
||||
return match ($this->tab) {
|
||||
null => $this->id,
|
||||
default => $this->id . '/' . $this->tab
|
||||
};
|
||||
}
|
||||
|
||||
public function props(): array
|
||||
{
|
||||
if ($this->tab !== null) {
|
||||
$props = $this->load('../index.php');
|
||||
}
|
||||
|
||||
return array_replace_recursive(
|
||||
$props ?? [],
|
||||
$this->load('index.php') ?? []
|
||||
);
|
||||
}
|
||||
|
||||
public function read(string $filename): string|null
|
||||
{
|
||||
$file = $this->file($filename);
|
||||
|
||||
if (is_file($file) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return F::read($file);
|
||||
}
|
||||
|
||||
public function root(): string
|
||||
{
|
||||
return $this->root;
|
||||
}
|
||||
|
||||
public function serve(): Response
|
||||
{
|
||||
return new Response($this->vue()['script'], 'application/javascript');
|
||||
}
|
||||
|
||||
public function tab(): string|null
|
||||
{
|
||||
return $this->tab;
|
||||
}
|
||||
|
||||
public function tabs(): array
|
||||
{
|
||||
return $this->tabs;
|
||||
}
|
||||
|
||||
public function template(string $filename): string|null
|
||||
{
|
||||
$file = $this->file($filename);
|
||||
|
||||
if (is_file($file) === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $this->props();
|
||||
return (new Template($file))->render($data);
|
||||
}
|
||||
|
||||
public function title(): string
|
||||
{
|
||||
return basename($this->id);
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'image' => [
|
||||
'icon' => $this->parent->icon(),
|
||||
'back' => 'light-dark(white, var(--color-gray-800))',
|
||||
],
|
||||
'text' => $this->title(),
|
||||
'link' => $this->url()
|
||||
];
|
||||
}
|
||||
|
||||
public function url(): string
|
||||
{
|
||||
return '/lab/' . $this->parent->id() . '/' . $this->path();
|
||||
}
|
||||
|
||||
public function vue(): array
|
||||
{
|
||||
// read the index.vue file (or programmabel Vue PHP file)
|
||||
$file = $this->read('index.vue');
|
||||
$file ??= $this->template('index.vue.php');
|
||||
$file ??= '';
|
||||
|
||||
// extract parts
|
||||
$parts['script'] = $this->vueScript($file);
|
||||
$parts['template'] = $this->vueTemplate($file);
|
||||
$parts['examples'] = $this->vueExamples($parts['template'], $parts['script']);
|
||||
$parts['style'] = $this->vueStyle($file);
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
public function vueExamples(string|null $template, string|null $script): array
|
||||
{
|
||||
$template ??= '';
|
||||
$examples = [];
|
||||
$scripts = [];
|
||||
|
||||
if (preg_match_all('!\/\*\* \@script: (.*?)\*\/(.*?)\/\*\* \@script-end \*\/!s', $script, $matches)) {
|
||||
foreach ($matches[1] as $key => $name) {
|
||||
$code = $matches[2][$key];
|
||||
$code = preg_replace('!const (.*?) \=!', 'default', $code);
|
||||
|
||||
$scripts[trim($name)] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match_all('!<k-lab-example[\s|\n].*?label="(.*?)"(.*?)>(.*?)<\/k-lab-example>!s', $template, $matches)) {
|
||||
foreach ($matches[1] as $key => $name) {
|
||||
$tail = $matches[2][$key];
|
||||
$code = $matches[3][$key];
|
||||
|
||||
$scriptId = trim(preg_replace_callback(
|
||||
'!script="(.*?)"!',
|
||||
fn ($match) => trim($match[1]),
|
||||
$tail
|
||||
));
|
||||
|
||||
$scriptBlock = $scripts[$scriptId] ?? null;
|
||||
|
||||
if (empty($scriptBlock) === false) {
|
||||
$js = PHP_EOL . PHP_EOL;
|
||||
$js .= '<script>';
|
||||
$js .= $scriptBlock;
|
||||
$js .= '</script>';
|
||||
} else {
|
||||
$js = '';
|
||||
}
|
||||
|
||||
// only use the code between the @code and @code-end comments
|
||||
if (preg_match('$<!-- @code -->(.*?)<!-- @code-end -->$s', $code, $match)) {
|
||||
$code = $match[1];
|
||||
}
|
||||
|
||||
if (preg_match_all('/^(\t*)\S/m', $code, $indents)) {
|
||||
// get minimum indent
|
||||
$indents = array_map(fn ($i) => strlen($i), $indents[1]);
|
||||
$indents = min($indents);
|
||||
|
||||
if (empty($js) === false) {
|
||||
$indents--;
|
||||
}
|
||||
|
||||
// strip minimum indent from each line
|
||||
$code = preg_replace('/^\t{' . $indents . '}/m', '', $code);
|
||||
}
|
||||
|
||||
$code = trim($code);
|
||||
|
||||
if (empty($js) === false) {
|
||||
$code = '<template>' . PHP_EOL . "\t" . $code . PHP_EOL . '</template>';
|
||||
}
|
||||
|
||||
$examples[$name] = $code . $js;
|
||||
}
|
||||
}
|
||||
|
||||
return $examples;
|
||||
}
|
||||
|
||||
public function vueScript(string $file): string
|
||||
{
|
||||
if (preg_match('!<script>(.*)</script>!s', $file, $match)) {
|
||||
return trim($match[1]);
|
||||
}
|
||||
|
||||
return 'export default {}';
|
||||
}
|
||||
|
||||
public function vueStyle(string $file): string|null
|
||||
{
|
||||
if (preg_match('!<style>(.*)</style>!s', $file, $match)) {
|
||||
return trim($match[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function vueTemplate(string $file): string|null
|
||||
{
|
||||
if (preg_match('!<template>(.*)</template>!s', $file, $match)) {
|
||||
return preg_replace('!^\n!', '', $match[1]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
49
kirby/src/Panel/Lab/Responses.php
Normal file
49
kirby/src/Panel/Lab/Responses.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Http\Response;
|
||||
|
||||
/**
|
||||
* Example responses for test requests
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 5.2.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Responses
|
||||
{
|
||||
public static function errorResponseByType(string|null $type = null): Response|Exception
|
||||
{
|
||||
return match ($type) {
|
||||
'form' => new InvalidArgumentException(
|
||||
fallback: 'Exception with details',
|
||||
details: [
|
||||
'a' => [
|
||||
'label' => 'Detail A',
|
||||
'message' => [
|
||||
'This is a message for Detail A',
|
||||
],
|
||||
],
|
||||
'b' => [
|
||||
'label' => 'Detail B',
|
||||
'message' => [
|
||||
'This is the first message for Detail B',
|
||||
'This is the second message for Detail B',
|
||||
],
|
||||
],
|
||||
]
|
||||
),
|
||||
'html' => new Response('<h1>Hello</h1>', 'html'),
|
||||
'invalid-json' => new Response('invalid json', 'json'),
|
||||
default => new Exception('This is a custom backend error'),
|
||||
};
|
||||
}
|
||||
}
|
||||
25
kirby/src/Panel/Lab/Snippet.php
Normal file
25
kirby/src/Panel/Lab/Snippet.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Template\Snippet as BaseSnippet;
|
||||
|
||||
/**
|
||||
* Custom snippet class for lab examples
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 4.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Snippet extends BaseSnippet
|
||||
{
|
||||
public static function root(): string
|
||||
{
|
||||
return __DIR__ . '/snippets';
|
||||
}
|
||||
}
|
||||
33
kirby/src/Panel/Lab/Template.php
Normal file
33
kirby/src/Panel/Lab/Template.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace Kirby\Panel\Lab;
|
||||
|
||||
use Kirby\Template\Template as BaseTemplate;
|
||||
|
||||
/**
|
||||
* Custom template class for lab examples
|
||||
*
|
||||
* @package Kirby Panel
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link https://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license https://getkirby.com/license
|
||||
* @since 4.0.0
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class Template extends BaseTemplate
|
||||
{
|
||||
public function __construct(
|
||||
public string $file
|
||||
) {
|
||||
parent::__construct(
|
||||
name: basename($this->file)
|
||||
);
|
||||
}
|
||||
|
||||
public function file(): string|null
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue