first commit
Some checks are pending
Deploy / Deploy to Production (push) Waiting to run

This commit is contained in:
isUnknown 2025-12-10 15:12:06 +01:00
commit a3620a1f5f
1042 changed files with 226722 additions and 0 deletions

244
kirby/src/Text/KirbyTag.php Normal file
View file

@ -0,0 +1,244 @@
<?php
namespace Kirby\Text;
use AllowDynamicProperties;
use Closure;
use Kirby\Cms\App;
use Kirby\Cms\File;
use Kirby\Cms\ModelWithContent;
use Kirby\Exception\BadMethodCallException;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Uuid\Uri as UuidUri;
use Kirby\Uuid\Uuid;
/**
* Representation and parse of a single KirbyTag.
*
* @package Kirby Text
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*
* @todo remove the following psalm suppress when PHP >= 8.2 required
* @psalm-suppress UndefinedAttributeClass
*/
#[AllowDynamicProperties]
class KirbyTag
{
public static array $aliases = [];
public static array $types = [];
public array $attrs = [];
public array $data = [];
public array $options = [];
public string $type;
public string|null $value = null;
public function __construct(
string $type,
string|null $value = null,
array $attrs = [],
array $data = [],
array $options = []
) {
// type aliases
if (isset(static::$types[$type]) === false) {
if (isset(static::$aliases[$type]) === false) {
throw new InvalidArgumentException(
message: 'Undefined tag type: ' . $type
);
}
$type = static::$aliases[$type];
}
$kirby = $data['kirby'] ?? App::instance();
$defaults = $kirby->option('kirbytext.' . $type, []);
$attrs = array_replace($defaults, $attrs);
// all available tag attributes
$availableAttrs = static::$types[$type]['attr'] ?? [];
foreach ($attrs as $attrName => $attrValue) {
$attrName = strtolower($attrName);
// applies only defined attributes to safely update
if (in_array($attrName, $availableAttrs, true) === true) {
$this->{$attrName} = $attrValue;
}
}
$this->attrs = $attrs;
$this->data = $data;
$this->options = $options;
$this->$type = $value;
$this->type = $type;
$this->value = $value;
}
/**
* Magic data and property getter
*/
public function __call(string $name, array $arguments = [])
{
return $this->data[$name] ?? $this->$name;
}
/**
* Magic call `KirbyTag::myType($parameter1, $parameter2)`
*/
public static function __callStatic(string $type, array $arguments = []): string
{
return (new static($type, ...$arguments))->render();
}
public function __get(string $attr)
{
$attr = strtolower($attr);
return $this->$attr ?? null;
}
public function attr(string $name, $default = null)
{
$name = strtolower($name);
return $this->$name ?? $default;
}
public static function factory(...$arguments): string
{
return (new static(...$arguments))->render();
}
/**
* Finds a file for the given path.
* The method first searches the file
* in the current parent, if it's a page.
* Afterwards it uses Kirby's global file finder.
*/
public function file(string $path): File|null
{
$parent = $this->parent();
// check first for UUID
if (Uuid::is($path, 'file') === true) {
if (
is_object($parent) === true &&
method_exists($parent, 'files') === true
) {
$context = $parent->files();
}
return Uuid::for($path, $context ?? null)->model();
}
if (
is_object($parent) === true &&
method_exists($parent, 'file') === true &&
$file = $parent->file($path)
) {
return $file;
}
if (
$parent instanceof File &&
$file = $parent->page()?->file($path)
) {
return $file;
}
return $this->kirby()->file($path, null, true);
}
/**
* Returns the current Kirby instance
*/
public function kirby(): App
{
return $this->data['kirby'] ?? App::instance();
}
public function option(string $key, $default = null)
{
return $this->options[$key] ?? $default;
}
public static function parse(
string $string,
array $data = [],
array $options = []
): static {
// remove the brackets, extract the first attribute (the tag type)
$tag = trim(ltrim($string, '('));
// use substr instead of rtrim to keep non-tagged brackets
// (link: file.pdf text: Download (PDF))
if (str_ends_with($tag, ')') === true) {
$tag = substr($tag, 0, -1);
}
$pos = strpos($tag, ':');
$type = trim(substr($tag, 0, $pos ?: null));
$type = strtolower($type);
$attr = static::$types[$type]['attr'] ?? [];
// the type should be parsed as an attribute, so we add it here
// to the list of possible attributes
array_unshift($attr, $type);
// ensure that UUIDs protocols aren't matched as attributes
$uuids = sprintf('(?!(%s):\/\/)', implode('|', UuidUri::$schemes));
// extract all attributes
$regex = sprintf('/%s(%s):/i', $uuids, implode('|', $attr));
$search = preg_split($regex, $tag, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
// $search is now an array with alternating keys and values
// convert it to arrays of keys and values
$chunks = array_chunk($search, 2);
$keys = array_column($chunks, 0);
$values = array_map('trim', array_column($chunks, 1));
// ensure that there is a value for each key
// otherwise combining won't work
if (count($values) < count($keys)) {
$values[] = '';
}
// combine the two arrays to an associative array
$attributes = array_combine($keys, $values);
// the first attribute is the type attribute
// extract and pass its value separately
$value = array_shift($attributes);
return new static($type, $value, $attributes, $data, $options);
}
/**
* Returns the parent model
*/
public function parent(): ModelWithContent|null
{
return $this->data['parent'] ?? null;
}
public function render(): string
{
$callback = static::$types[$this->type]['html'] ?? null;
if ($callback instanceof Closure) {
return (string)$callback($this);
}
throw new BadMethodCallException(
message: 'Invalid tag render function in tag: ' . $this->type
);
}
public function type(): string
{
return $this->type;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Kirby\Text;
use Exception;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Toolkit\Str;
/**
* Parses and converts custom kirbytags in any
* given string. KirbyTags are defined via
* `KirbyTag::$types`. The default tags for the
* Cms are located in `kirby/config/tags.php`
*
* @package Kirby Text
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class KirbyTags
{
public static function parse(
string|null $text = null,
array $data = [],
array $options = []
): string {
// make sure $text is a string
$text ??= '';
$regex = '!
(?=[^\]]) # positive lookahead that matches a group after the main expression without including ] in the result
(?=\([a-z0-9_-]+:) # positive lookahead that requires starts with ( and lowercase ASCII letters, digits, underscores or hyphens followed with : immediately to the right of the current location
(\( # capturing group 1
(?:[^()]+|(?1))*+ # repetitions of any chars other than ( and ) or the whole group 1 pattern (recursed)
\)) # end of capturing group 1
!isx';
return preg_replace_callback($regex, function ($match) use ($data, $options) {
$debug = $options['debug'] ?? false;
try {
return KirbyTag::parse($match[0], $data, $options)->render();
} catch (InvalidArgumentException $e) {
// stay silent in production and ignore non-existing tags
if (
$debug !== true ||
Str::startsWith($e->getMessage(), 'Undefined tag type:') === true
) {
return $match[0];
}
throw $e;
} catch (Exception $e) {
if ($debug === true) {
throw $e;
}
return $match[0];
}
}, $text);
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace Kirby\Text;
use Parsedown;
use ParsedownExtra;
/**
* The Markdown class is a wrapper around all sorts of Markdown
* parser libraries and is meant to standardize the Markdown parser
* API for all Kirby packages.
*
* It uses Parsedown and ParsedownExtra by default.
*
* @package Kirby Text
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class Markdown
{
/**
* Array with all configured options
* for the parser
*/
protected array $options = [];
/**
* Returns default values for all
* available parser options
*/
public function defaults(): array
{
return [
'breaks' => true,
'extra' => false,
'safe' => false
];
}
/**
* Creates a new Markdown parser
* with the given options
*/
public function __construct(array $options = [])
{
$this->options = [...$this->defaults(), ...$options];
}
/**
* Parses the given text and returns the HTML
*/
public function parse(
string|null $text = null,
bool $inline = false
): string {
$parser = match ($this->options['extra']) {
true => new ParsedownExtra(),
default => new Parsedown()
};
$parser->setBreaksEnabled($this->options['breaks']);
$parser->setSafeMode($this->options['safe']);
if ($inline === true) {
return @$parser->line($text);
}
return @$parser->text($text);
}
}

View file

@ -0,0 +1,119 @@
<?php
namespace Kirby\Text;
use Michelf\SmartyPantsTypographer;
/**
* Wrapper for Michelf's SmartyPants
* parser, to improve the configurability
* of the parser with default options and
* a simple way to set your own options.
*
* @package Kirby Text
* @author Bastian Allgeier <bastian@getkirby.com>
* @link https://getkirby.com
* @copyright Bastian Allgeier
* @license https://opensource.org/licenses/MIT
*/
class SmartyPants
{
/**
* Array with all configured options
* for the parser
*/
protected array $options = [];
/**
* Michelf's parser object
*/
protected SmartyPantsTypographer $parser;
/**
* Returns default values for all
* available parser options
*/
public function defaults(): array
{
return [
'attr' => 1,
'doublequote.open' => '&#8220;',
'doublequote.close' => '&#8221;',
'doublequote.low' => '&#8222;',
'singlequote.open' => '&#8216;',
'singlequote.close' => '&#8217;',
'backtick.doublequote.open' => '&#8220;',
'backtick.doublequote.close' => '&#8221;',
'backtick.singlequote.open' => '&#8216;',
'backtick.singlequote.close' => '&#8217;',
'emdash' => '&#8212;',
'endash' => '&#8211;',
'ellipsis' => '&#8230;',
'space' => '(?: | |&nbsp;|&#0*160;|&#x0*[aA]0;)',
'space.emdash' => ' ',
'space.endash' => ' ',
'space.colon' => '&#160;',
'space.semicolon' => '&#160;',
'space.marks' => '&#160;',
'space.frenchquote' => '&#160;',
'space.thousand' => '&#160;',
'space.unit' => '&#160;',
'guillemet.leftpointing' => '&#171;',
'guillemet.rightpointing' => '&#187;',
'geresh' => '&#1523;',
'gershayim' => '&#1524;',
'skip' => 'pre|code|kbd|script|style|math',
];
}
/**
* Creates a new SmartyPants parser
* with the given options
*/
public function __construct(array $options = [])
{
$this->options = [...$this->defaults(), ...$options];
$this->parser = new SmartyPantsTypographer($this->options['attr']);
// configuration
$this->parser->smart_doublequote_open = $this->options['doublequote.open'];
$this->parser->smart_doublequote_close = $this->options['doublequote.close'];
$this->parser->smart_singlequote_open = $this->options['singlequote.open'];
$this->parser->smart_singlequote_close = $this->options['singlequote.close'];
$this->parser->backtick_doublequote_open = $this->options['backtick.doublequote.open'];
$this->parser->backtick_doublequote_close = $this->options['backtick.doublequote.close'];
$this->parser->backtick_singlequote_open = $this->options['backtick.singlequote.open'];
$this->parser->backtick_singlequote_close = $this->options['backtick.singlequote.close'];
$this->parser->em_dash = $this->options['emdash'];
$this->parser->en_dash = $this->options['endash'];
$this->parser->ellipsis = $this->options['ellipsis'];
$this->parser->tags_to_skip = $this->options['skip'];
$this->parser->space_emdash = $this->options['space.emdash'];
$this->parser->space_endash = $this->options['space.endash'];
$this->parser->space_colon = $this->options['space.colon'];
$this->parser->space_semicolon = $this->options['space.semicolon'];
$this->parser->space_marks = $this->options['space.marks'];
$this->parser->space_frenchquote = $this->options['space.frenchquote'];
$this->parser->space_thousand = $this->options['space.thousand'];
$this->parser->space_unit = $this->options['space.unit'];
$this->parser->doublequote_low = $this->options['doublequote.low'];
$this->parser->guillemet_leftpointing = $this->options['guillemet.leftpointing'];
$this->parser->guillemet_rightpointing = $this->options['guillemet.rightpointing'];
$this->parser->geresh = $this->options['geresh'];
$this->parser->gershayim = $this->options['gershayim'];
$this->parser->space = $this->options['space'];
}
/**
* Parses the given text
*/
public function parse(string|null $text = null): string
{
// prepare the text
$text ??= '';
$text = str_replace('&quot;', '"', $text);
// parse the text
return $this->parser->transform($text);
}
}