...}}) and applies compact layout * without adding visual box styling of its own. * * Syntax: * * {{image>...}} * {{image>...}} * */ 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(']*)?>(?=.*)', $mode, 'plugin_luxtools_grouping'); } /** @inheritdoc */ public function postConnect() { $this->Lexer->addExitPattern('', '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 .= '
'; if ($unknown !== []) { $renderer->doc .= '' . hsc('[grouping: unknown option(s): ' . implode(', ', $unknown) . ']') . ''; } 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 .= '
'; return true; } return true; } /** * Parse opening 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} */ protected function parseOpeningTag(string $match): array { $params = $this->getDefaultParams(); $unknown = []; if (!preg_match('/^$/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); } }