)
return 'block';
}
public function getSort(): int
{
return 200;
}
public function connectTo($mode): void
{
$this->Lexer->addEntryPattern(
'(?=.*?)',
$mode,
'plugin_multimage'
);
}
public function postConnect(): void
{
$this->Lexer->addExitPattern('', 'plugin_multimage');
}
public function handle($match, $state, $pos, Doku_Handler $handler)
{
switch ($state) {
case DOKU_LEXER_ENTER:
return ['enter'];
case DOKU_LEXER_UNMATCHED:
return ['content', $match];
case DOKU_LEXER_EXIT:
return ['exit'];
}
return [];
}
public function render($mode, Doku_Renderer $renderer, $data): bool
{
if ($mode !== 'xhtml') {
return false;
}
[$type, $content] = array_pad($data, 2, null);
if ($type !== 'content') {
return true;
}
$yaml = trim($content);
if ($yaml === '') {
return true;
}
if (!function_exists('yaml_parse')) {
$renderer->doc .= '';
return true;
}
$parsed = @yaml_parse($yaml);
if (!is_array($parsed)) {
$renderer->doc .= '';
return true;
}
/*
* Normalization:
* - mapping with 'image' => single base image
* - sequence => multiple base images
*/
if (array_key_exists('image', $parsed)) {
$baseImages = [$parsed];
} elseif (array_values($parsed) === $parsed) {
$baseImages = $parsed;
} else {
$renderer->doc .= '';
return true;
}
$renderer->doc .= '
';
foreach ($baseImages as $baseImage) {
if (!is_array($baseImage)) {
continue;
}
$this->renderBaseImage($renderer, $baseImage);
if (!empty($baseImage['ontop']) && is_array($baseImage['ontop'])) {
foreach ($baseImage['ontop'] as $ontopImage) {
if (is_array($ontopImage)) {
$this->renderOntopImage($renderer, $ontopImage);
}
}
}
}
$renderer->doc .= '
';
return true;
}
/* ---------- rendering helpers ---------- */
protected function renderBaseImage(Doku_Renderer $renderer, array $entry): void
{
$style = $this->filterStyles($entry['style'] ?? null);
$renderer->doc .= '';
if (empty($entry['image'])) {
$renderer->doc .= '
doc .= ' style="' . hsc($style) . '"';
}
$renderer->doc .= '>
';
} else {
[$src, $alt] = $this->parseImage($entry['image']);
$renderer->doc .= '
 . ')
doc .= ' style="' . hsc($style) . '"';
}
$renderer->doc .= ' alt="' . hsc($alt) . '"/>';
}
$renderer->doc .= '
';
}
protected function renderOntopImage(Doku_Renderer $renderer, array $entry): void
{
if (empty($entry['image'])) {
return;
}
[$src, $alt] = $this->parseImage($entry['image']);
$style = $this->filterStyles($entry['style'] ?? null);
$renderer->doc .= '';
$renderer->doc .= '
 . ')
doc .= ' style="' . hsc($style) . '"';
}
$renderer->doc .= ' alt="' . hsc($alt) . '"/>';
$renderer->doc .= '
';
}
/* ---------- parsing & validation ---------- */
protected function parseImage(string $spec): array
{
// Format: wiki:img.svg?400|alt text
[$left, $alt] = array_pad(explode('|', $spec, 2), 2, '');
[$id, $params] = array_pad(explode('?', $left, 2), 2, '');
$opts = [];
if ($params !== '') {
parse_str($params, $opts);
}
$src = ml($id, $opts, true, '&');
return [$src, $alt];
}
protected function filterStyles($styles): string
{
if (!is_array($styles)) {
return '';
}
$allowed = $this->getAllowedStyles();
$out = [];
foreach ($styles as $key => $value) {
if (!is_string($key) || !is_scalar($value)) {
continue;
}
$key = trim($key);
$value = trim((string)$value);
if (!in_array($key, $allowed, true)) {
continue;
}
// Rewrite url("...") or url('...')
$value = preg_replace_callback(
'/url\(\s*([\'"])([^\'"]+)\1\s*\)/i',
function ($m) {
$target = trim($m[2]);
// Only rewrite DokuWiki media IDs
if (preg_match('/^[a-zA-Z0-9_:-]+(\.[a-zA-Z0-9]+)?$/', $target)) {
$url = ml($target, [], true, '&');
return 'url("' . $url . '")';
}
// Leave untouched if not a media ID
return $m[0];
},
$value
);
$out[] = $key . ': ' . $value;
}
return $out ? implode('; ', $out) . ';': '';
}
protected function getAllowedStyles(): array
{
static $styles = null;
if ($styles !== null) {
return $styles;
}
$raw = (string)$this->getConf('allowed_styles');
$styles = array_filter(
array_map('trim', explode(',', $raw)),
static fn($s) => $s !== ''
);
return $styles;
}
}