283 lines
8.1 KiB
PHP
283 lines
8.1 KiB
PHP
<?php
|
|
|
|
use dokuwiki\Extension\SyntaxPlugin;
|
|
|
|
require_once(__DIR__ . '/../autoload.php');
|
|
|
|
/**
|
|
* luxtools Plugin: Grouping wrapper syntax.
|
|
*
|
|
* Wraps multiple blocks (typically {{image>...}}) and applies compact layout
|
|
* without adding visual box styling of its own.
|
|
*
|
|
* Syntax:
|
|
* <grouping layout="flex" gap="0" justify="start" align="start">
|
|
* {{image>...}}
|
|
* {{image>...}}
|
|
* </grouping>
|
|
*/
|
|
class syntax_plugin_luxtools_grouping extends SyntaxPlugin
|
|
{
|
|
/** @inheritdoc */
|
|
public function getType()
|
|
{
|
|
return 'container';
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function getPType()
|
|
{
|
|
return 'block';
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function getSort()
|
|
{
|
|
// Slightly after image syntax
|
|
return 316;
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function getAllowedTypes()
|
|
{
|
|
return ['container', 'substition', 'protected', 'disabled', 'formatting'];
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function connectTo($mode)
|
|
{
|
|
$this->Lexer->addEntryPattern('<grouping(?:\s+[^>]*)?>(?=.*</grouping>)', $mode, 'plugin_luxtools_grouping');
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function postConnect()
|
|
{
|
|
$this->Lexer->addExitPattern('</grouping>', 'plugin_luxtools_grouping');
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function handle($match, $state, $pos, Doku_Handler $handler)
|
|
{
|
|
if ($state === DOKU_LEXER_ENTER) {
|
|
$parsed = $this->parseOpeningTag($match);
|
|
return [
|
|
'state' => $state,
|
|
'params' => $parsed['params'],
|
|
'unknown' => $parsed['unknown'],
|
|
];
|
|
}
|
|
|
|
if ($state === DOKU_LEXER_UNMATCHED) {
|
|
return [
|
|
'state' => $state,
|
|
'text' => $match,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'state' => $state,
|
|
];
|
|
}
|
|
|
|
/** @inheritdoc */
|
|
public function render($format, Doku_Renderer $renderer, $data)
|
|
{
|
|
if (!is_array($data) || !isset($data['state'])) {
|
|
return false;
|
|
}
|
|
|
|
$state = (int)$data['state'];
|
|
|
|
if ($format !== 'xhtml') {
|
|
if ($state === DOKU_LEXER_UNMATCHED && isset($data['text'])) {
|
|
$renderer->cdata((string)$data['text']);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!($renderer instanceof Doku_Renderer_xhtml)) {
|
|
return true;
|
|
}
|
|
|
|
if ($state === DOKU_LEXER_ENTER) {
|
|
$params = isset($data['params']) && is_array($data['params']) ? $data['params'] : $this->getDefaultParams();
|
|
$unknown = isset($data['unknown']) && is_array($data['unknown']) ? $data['unknown'] : [];
|
|
|
|
$layout = ($params['layout'] === 'flex') ? 'flex' : 'grid';
|
|
$cols = (int)$params['cols'];
|
|
if ($cols < 1) {
|
|
$cols = 2;
|
|
}
|
|
|
|
$gap = (string)$params['gap'];
|
|
if (!$this->isValidCssLength($gap)) {
|
|
$gap = '0';
|
|
}
|
|
|
|
$justify = (string)$params['justify'];
|
|
if (!$this->isValidJustify($justify)) {
|
|
$justify = 'start';
|
|
}
|
|
|
|
$align = (string)$params['align'];
|
|
if (!$this->isValidAlign($align)) {
|
|
$align = 'start';
|
|
}
|
|
|
|
$renderer->doc .= '<div class="luxtools-grouping luxtools-grouping--' . hsc($layout) . '"'
|
|
. ' style="--luxtools-grouping-cols: ' . $cols
|
|
. '; --luxtools-grouping-gap: ' . hsc($gap)
|
|
. '; --luxtools-grouping-justify: ' . hsc($justify)
|
|
. '; --luxtools-grouping-align: ' . hsc($align)
|
|
. ';">';
|
|
|
|
if ($unknown !== []) {
|
|
$renderer->doc .= '<span class="luxtools-grouping-warning">'
|
|
. hsc('[grouping: unknown option(s): ' . implode(', ', $unknown) . ']')
|
|
. '</span>';
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if ($state === DOKU_LEXER_UNMATCHED) {
|
|
if (isset($data['text'])) {
|
|
$renderer->cdata((string)$data['text']);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if ($state === DOKU_LEXER_EXIT) {
|
|
$renderer->doc .= '</div>';
|
|
return true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse opening <grouping ...> tag attributes.
|
|
*
|
|
* Supports attribute style only, e.g.
|
|
* layout="grid" cols="3" gap="8px" justify="center" align="stretch".
|
|
* Unknown or invalid values are ignored and defaults are used.
|
|
*
|
|
* @param string $match
|
|
* @return array{params:array{layout:string,cols:int,gap:string,justify:string,align:string},unknown:array<int,string>}
|
|
*/
|
|
protected function parseOpeningTag(string $match): array
|
|
{
|
|
$params = $this->getDefaultParams();
|
|
$unknown = [];
|
|
|
|
if (!preg_match('/^<grouping\b(.*?)>$/is', $match, $tagMatch)) {
|
|
return ['params' => $params, 'unknown' => $unknown];
|
|
}
|
|
|
|
$attrPart = (string)$tagMatch[1];
|
|
if ($attrPart === '') {
|
|
return ['params' => $params, 'unknown' => $unknown];
|
|
}
|
|
|
|
if (preg_match_all('/([a-zA-Z_:][a-zA-Z0-9:._-]*)\s*=\s*(["\'])(.*?)\2/s', $attrPart, $attrMatches, PREG_SET_ORDER)) {
|
|
foreach ($attrMatches as $item) {
|
|
$name = strtolower(trim((string)$item[1]));
|
|
$value = trim((string)$item[3]);
|
|
|
|
if ($name === 'layout') {
|
|
$value = strtolower($value);
|
|
if (in_array($value, ['grid', 'flex'], true)) {
|
|
$params['layout'] = $value;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($name === 'cols') {
|
|
if (preg_match('/^\d+$/', $value)) {
|
|
$cols = (int)$value;
|
|
if ($cols > 0) {
|
|
$params['cols'] = min($cols, 12);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($name === 'gap') {
|
|
if ($this->isValidCssLength($value)) {
|
|
$params['gap'] = $value;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($name === 'justify') {
|
|
$value = strtolower($value);
|
|
if ($this->isValidJustify($value)) {
|
|
$params['justify'] = $value;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ($name === 'align') {
|
|
$value = strtolower($value);
|
|
if ($this->isValidAlign($value)) {
|
|
$params['align'] = $value;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$unknown[] = $name;
|
|
}
|
|
}
|
|
|
|
if ($unknown !== []) {
|
|
$unknown = array_values(array_unique($unknown));
|
|
}
|
|
|
|
return ['params' => $params, 'unknown' => $unknown];
|
|
}
|
|
|
|
/**
|
|
* @return array{layout:string,cols:int,gap:string,justify:string,align:string}
|
|
*/
|
|
protected function getDefaultParams(): array
|
|
{
|
|
return [
|
|
'layout' => 'flex',
|
|
'cols' => 2,
|
|
'gap' => '0',
|
|
'justify' => 'start',
|
|
'align' => 'start',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Validate a simple CSS length token.
|
|
*
|
|
* Allows "0" and common explicit units used in docs/examples.
|
|
*/
|
|
protected function isValidCssLength(string $value): bool
|
|
{
|
|
$value = trim($value);
|
|
if ($value === '0') {
|
|
return true;
|
|
}
|
|
|
|
return (bool)preg_match('/^(?:\d+(?:\.\d+)?|\.\d+)(?:px|em|rem|%|vw|vh)$/', $value);
|
|
}
|
|
|
|
/**
|
|
* Validate justify-content compatible values.
|
|
*/
|
|
protected function isValidJustify(string $value): bool
|
|
{
|
|
return in_array($value, ['start', 'center', 'end', 'space-between', 'space-around', 'space-evenly'], true);
|
|
}
|
|
|
|
/**
|
|
* Validate align-items compatible values.
|
|
*/
|
|
protected function isValidAlign(string $value): bool
|
|
{
|
|
return in_array($value, ['start', 'center', 'end', 'stretch', 'baseline'], true);
|
|
}
|
|
}
|