) 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; } }