xref: /plugin/multimage/syntax.php (revision 3d0502afe0387853d1dec69b59db8664ad1deac4)
1*3d0502afSRüdiger Kessel<?php
2*3d0502afSRüdiger Kessel/**
3*3d0502afSRüdiger Kessel * DokuWiki Plugin multimage (Syntax Component)
4*3d0502afSRüdiger Kessel */
5*3d0502afSRüdiger Kessel
6*3d0502afSRüdiger Kesselif (!defined('DOKU_INC')) die();
7*3d0502afSRüdiger Kessel
8*3d0502afSRüdiger Kesselclass syntax_plugin_multimage extends DokuWiki_Syntax_Plugin
9*3d0502afSRüdiger Kessel{
10*3d0502afSRüdiger Kessel    public function getType(): string
11*3d0502afSRüdiger Kessel    {
12*3d0502afSRüdiger Kessel        // We capture a delimited block of raw text
13*3d0502afSRüdiger Kessel        return 'container';
14*3d0502afSRüdiger Kessel    }
15*3d0502afSRüdiger Kessel
16*3d0502afSRüdiger Kessel    public function getPType(): string
17*3d0502afSRüdiger Kessel    {
18*3d0502afSRüdiger Kessel        // We emit block-level HTML (<div>)
19*3d0502afSRüdiger Kessel        return 'block';
20*3d0502afSRüdiger Kessel    }
21*3d0502afSRüdiger Kessel
22*3d0502afSRüdiger Kessel    public function getSort(): int
23*3d0502afSRüdiger Kessel    {
24*3d0502afSRüdiger Kessel        return 200;
25*3d0502afSRüdiger Kessel    }
26*3d0502afSRüdiger Kessel
27*3d0502afSRüdiger Kessel    public function connectTo($mode): void
28*3d0502afSRüdiger Kessel    {
29*3d0502afSRüdiger Kessel        $this->Lexer->addEntryPattern(
30*3d0502afSRüdiger Kessel            '<images>(?=.*?</images>)',
31*3d0502afSRüdiger Kessel            $mode,
32*3d0502afSRüdiger Kessel            'plugin_multimage'
33*3d0502afSRüdiger Kessel        );
34*3d0502afSRüdiger Kessel    }
35*3d0502afSRüdiger Kessel
36*3d0502afSRüdiger Kessel    public function postConnect(): void
37*3d0502afSRüdiger Kessel    {
38*3d0502afSRüdiger Kessel        $this->Lexer->addExitPattern('</images>', 'plugin_multimage');
39*3d0502afSRüdiger Kessel    }
40*3d0502afSRüdiger Kessel
41*3d0502afSRüdiger Kessel    public function handle($match, $state, $pos, Doku_Handler $handler)
42*3d0502afSRüdiger Kessel    {
43*3d0502afSRüdiger Kessel        switch ($state) {
44*3d0502afSRüdiger Kessel            case DOKU_LEXER_ENTER:
45*3d0502afSRüdiger Kessel                return ['enter'];
46*3d0502afSRüdiger Kessel            case DOKU_LEXER_UNMATCHED:
47*3d0502afSRüdiger Kessel                return ['content', $match];
48*3d0502afSRüdiger Kessel            case DOKU_LEXER_EXIT:
49*3d0502afSRüdiger Kessel                return ['exit'];
50*3d0502afSRüdiger Kessel        }
51*3d0502afSRüdiger Kessel        return [];
52*3d0502afSRüdiger Kessel    }
53*3d0502afSRüdiger Kessel
54*3d0502afSRüdiger Kessel    public function render($mode, Doku_Renderer $renderer, $data): bool
55*3d0502afSRüdiger Kessel    {
56*3d0502afSRüdiger Kessel        if ($mode !== 'xhtml') {
57*3d0502afSRüdiger Kessel            return false;
58*3d0502afSRüdiger Kessel        }
59*3d0502afSRüdiger Kessel
60*3d0502afSRüdiger Kessel        [$type, $content] = array_pad($data, 2, null);
61*3d0502afSRüdiger Kessel
62*3d0502afSRüdiger Kessel        if ($type !== 'content') {
63*3d0502afSRüdiger Kessel            return true;
64*3d0502afSRüdiger Kessel        }
65*3d0502afSRüdiger Kessel
66*3d0502afSRüdiger Kessel        $yaml = trim($content);
67*3d0502afSRüdiger Kessel        if ($yaml === '') {
68*3d0502afSRüdiger Kessel            return true;
69*3d0502afSRüdiger Kessel        }
70*3d0502afSRüdiger Kessel
71*3d0502afSRüdiger Kessel        if (!function_exists('yaml_parse')) {
72*3d0502afSRüdiger Kessel            $renderer->doc .= '<!-- multimage: PHP YAML extension missing -->';
73*3d0502afSRüdiger Kessel            return true;
74*3d0502afSRüdiger Kessel        }
75*3d0502afSRüdiger Kessel
76*3d0502afSRüdiger Kessel        $parsed = @yaml_parse($yaml);
77*3d0502afSRüdiger Kessel        if (!is_array($parsed)) {
78*3d0502afSRüdiger Kessel            $renderer->doc .= '<!-- multimage: invalid YAML -->';
79*3d0502afSRüdiger Kessel            return true;
80*3d0502afSRüdiger Kessel        }
81*3d0502afSRüdiger Kessel
82*3d0502afSRüdiger Kessel        /*
83*3d0502afSRüdiger Kessel         * Normalization:
84*3d0502afSRüdiger Kessel         * - mapping with 'image' => single base image
85*3d0502afSRüdiger Kessel         * - sequence => multiple base images
86*3d0502afSRüdiger Kessel         */
87*3d0502afSRüdiger Kessel        if (array_key_exists('image', $parsed)) {
88*3d0502afSRüdiger Kessel            $baseImages = [$parsed];
89*3d0502afSRüdiger Kessel        } elseif (array_values($parsed) === $parsed) {
90*3d0502afSRüdiger Kessel            $baseImages = $parsed;
91*3d0502afSRüdiger Kessel        } else {
92*3d0502afSRüdiger Kessel            $renderer->doc .= '<!-- multimage: expected image or list of images -->';
93*3d0502afSRüdiger Kessel            return true;
94*3d0502afSRüdiger Kessel        }
95*3d0502afSRüdiger Kessel
96*3d0502afSRüdiger Kessel        $renderer->doc .= '<div class="images">';
97*3d0502afSRüdiger Kessel
98*3d0502afSRüdiger Kessel        foreach ($baseImages as $baseImage) {
99*3d0502afSRüdiger Kessel            if (!is_array($baseImage)) {
100*3d0502afSRüdiger Kessel                continue;
101*3d0502afSRüdiger Kessel            }
102*3d0502afSRüdiger Kessel
103*3d0502afSRüdiger Kessel            $this->renderBaseImage($renderer, $baseImage);
104*3d0502afSRüdiger Kessel
105*3d0502afSRüdiger Kessel
106*3d0502afSRüdiger Kessel            if (!empty($baseImage['ontop']) && is_array($baseImage['ontop'])) {
107*3d0502afSRüdiger Kessel                foreach ($baseImage['ontop'] as $ontopImage) {
108*3d0502afSRüdiger Kessel                    if (is_array($ontopImage)) {
109*3d0502afSRüdiger Kessel                        $this->renderOntopImage($renderer, $ontopImage);
110*3d0502afSRüdiger Kessel                    }
111*3d0502afSRüdiger Kessel                }
112*3d0502afSRüdiger Kessel            }
113*3d0502afSRüdiger Kessel        }
114*3d0502afSRüdiger Kessel
115*3d0502afSRüdiger Kessel        $renderer->doc .= '</div>';
116*3d0502afSRüdiger Kessel
117*3d0502afSRüdiger Kessel        return true;
118*3d0502afSRüdiger Kessel    }
119*3d0502afSRüdiger Kessel
120*3d0502afSRüdiger Kessel    /* ---------- rendering helpers ---------- */
121*3d0502afSRüdiger Kessel
122*3d0502afSRüdiger Kessel    protected function renderBaseImage(Doku_Renderer $renderer, array $entry): void
123*3d0502afSRüdiger Kessel    {
124*3d0502afSRüdiger Kessel        $style = $this->filterStyles($entry['style'] ?? null);
125*3d0502afSRüdiger Kessel
126*3d0502afSRüdiger Kessel        $renderer->doc .= '<div class="images_ontop">';
127*3d0502afSRüdiger Kessel
128*3d0502afSRüdiger Kessel        if (empty($entry['image'])) {
129*3d0502afSRüdiger Kessel            $renderer->doc .= '<div class="images_image"';
130*3d0502afSRüdiger Kessel            if ($style !== '') {
131*3d0502afSRüdiger Kessel                $renderer->doc .= ' style="' . hsc($style) . '"';
132*3d0502afSRüdiger Kessel            }
133*3d0502afSRüdiger Kessel            $renderer->doc .= '>&nbsp;</div>';
134*3d0502afSRüdiger Kessel        } else {
135*3d0502afSRüdiger Kessel          [$src, $alt] = $this->parseImage($entry['image']);
136*3d0502afSRüdiger Kessel          $renderer->doc .= '<img class="images_image" src="' . hsc($src) . '"';
137*3d0502afSRüdiger Kessel          if ($style !== '') {
138*3d0502afSRüdiger Kessel              $renderer->doc .= ' style="' . hsc($style) . '"';
139*3d0502afSRüdiger Kessel          }
140*3d0502afSRüdiger Kessel          $renderer->doc .= ' alt="' . hsc($alt) . '"/>';
141*3d0502afSRüdiger Kessel        }
142*3d0502afSRüdiger Kessel        $renderer->doc .= '</div>';
143*3d0502afSRüdiger Kessel    }
144*3d0502afSRüdiger Kessel
145*3d0502afSRüdiger Kessel    protected function renderOntopImage(Doku_Renderer $renderer, array $entry): void
146*3d0502afSRüdiger Kessel    {
147*3d0502afSRüdiger Kessel        if (empty($entry['image'])) {
148*3d0502afSRüdiger Kessel            return;
149*3d0502afSRüdiger Kessel        }
150*3d0502afSRüdiger Kessel
151*3d0502afSRüdiger Kessel        [$src, $alt] = $this->parseImage($entry['image']);
152*3d0502afSRüdiger Kessel        $style = $this->filterStyles($entry['style'] ?? null);
153*3d0502afSRüdiger Kessel
154*3d0502afSRüdiger Kessel        $renderer->doc .= '<div class="images_ontop">';
155*3d0502afSRüdiger Kessel        $renderer->doc .= '<img class="images_ontop_image" src="' . hsc($src) . '"';
156*3d0502afSRüdiger Kessel        if ($style !== '') {
157*3d0502afSRüdiger Kessel            $renderer->doc .= ' style="' . hsc($style) . '"';
158*3d0502afSRüdiger Kessel        }
159*3d0502afSRüdiger Kessel        $renderer->doc .= ' alt="' . hsc($alt) . '"/>';
160*3d0502afSRüdiger Kessel        $renderer->doc .= '</div>';
161*3d0502afSRüdiger Kessel    }
162*3d0502afSRüdiger Kessel
163*3d0502afSRüdiger Kessel    /* ---------- parsing & validation ---------- */
164*3d0502afSRüdiger Kessel
165*3d0502afSRüdiger Kessel    protected function parseImage(string $spec): array
166*3d0502afSRüdiger Kessel    {
167*3d0502afSRüdiger Kessel        // Format: wiki:img.svg?400|alt text
168*3d0502afSRüdiger Kessel        [$left, $alt] = array_pad(explode('|', $spec, 2), 2, '');
169*3d0502afSRüdiger Kessel        [$id, $params] = array_pad(explode('?', $left, 2), 2, '');
170*3d0502afSRüdiger Kessel
171*3d0502afSRüdiger Kessel        $opts = [];
172*3d0502afSRüdiger Kessel        if ($params !== '') {
173*3d0502afSRüdiger Kessel            parse_str($params, $opts);
174*3d0502afSRüdiger Kessel        }
175*3d0502afSRüdiger Kessel
176*3d0502afSRüdiger Kessel        $src = ml($id, $opts, true, '&');
177*3d0502afSRüdiger Kessel
178*3d0502afSRüdiger Kessel        return [$src, $alt];
179*3d0502afSRüdiger Kessel    }
180*3d0502afSRüdiger Kessel
181*3d0502afSRüdiger Kesselprotected function filterStyles($styles): string
182*3d0502afSRüdiger Kessel{
183*3d0502afSRüdiger Kessel    if (!is_array($styles)) {
184*3d0502afSRüdiger Kessel        return '';
185*3d0502afSRüdiger Kessel    }
186*3d0502afSRüdiger Kessel
187*3d0502afSRüdiger Kessel    $allowed = $this->getAllowedStyles();
188*3d0502afSRüdiger Kessel    $out = [];
189*3d0502afSRüdiger Kessel
190*3d0502afSRüdiger Kessel    foreach ($styles as $key => $value) {
191*3d0502afSRüdiger Kessel        if (!is_string($key) || !is_scalar($value)) {
192*3d0502afSRüdiger Kessel            continue;
193*3d0502afSRüdiger Kessel        }
194*3d0502afSRüdiger Kessel
195*3d0502afSRüdiger Kessel        $key = trim($key);
196*3d0502afSRüdiger Kessel        $value = trim((string)$value);
197*3d0502afSRüdiger Kessel
198*3d0502afSRüdiger Kessel        if (!in_array($key, $allowed, true)) {
199*3d0502afSRüdiger Kessel            continue;
200*3d0502afSRüdiger Kessel        }
201*3d0502afSRüdiger Kessel
202*3d0502afSRüdiger Kessel        // Rewrite url("...") or url('...')
203*3d0502afSRüdiger Kessel        $value = preg_replace_callback(
204*3d0502afSRüdiger Kessel            '/url\(\s*([\'"])([^\'"]+)\1\s*\)/i',
205*3d0502afSRüdiger Kessel            function ($m) {
206*3d0502afSRüdiger Kessel                $target = trim($m[2]);
207*3d0502afSRüdiger Kessel
208*3d0502afSRüdiger Kessel                // Only rewrite DokuWiki media IDs
209*3d0502afSRüdiger Kessel                if (preg_match('/^[a-zA-Z0-9_:-]+(\.[a-zA-Z0-9]+)?$/', $target)) {
210*3d0502afSRüdiger Kessel                    $url = ml($target, [], true, '&');
211*3d0502afSRüdiger Kessel                    return 'url("' . $url . '")';
212*3d0502afSRüdiger Kessel                }
213*3d0502afSRüdiger Kessel
214*3d0502afSRüdiger Kessel                // Leave untouched if not a media ID
215*3d0502afSRüdiger Kessel                return $m[0];
216*3d0502afSRüdiger Kessel            },
217*3d0502afSRüdiger Kessel            $value
218*3d0502afSRüdiger Kessel        );
219*3d0502afSRüdiger Kessel
220*3d0502afSRüdiger Kessel        $out[] = $key . ': ' . $value;
221*3d0502afSRüdiger Kessel    }
222*3d0502afSRüdiger Kessel
223*3d0502afSRüdiger Kessel    return $out ? implode('; ', $out) . ';': '';
224*3d0502afSRüdiger Kessel}
225*3d0502afSRüdiger Kessel
226*3d0502afSRüdiger Kessel    protected function getAllowedStyles(): array
227*3d0502afSRüdiger Kessel    {
228*3d0502afSRüdiger Kessel        static $styles = null;
229*3d0502afSRüdiger Kessel
230*3d0502afSRüdiger Kessel        if ($styles !== null) {
231*3d0502afSRüdiger Kessel            return $styles;
232*3d0502afSRüdiger Kessel        }
233*3d0502afSRüdiger Kessel
234*3d0502afSRüdiger Kessel        $raw = (string)$this->getConf('allowed_styles');
235*3d0502afSRüdiger Kessel
236*3d0502afSRüdiger Kessel        $styles = array_filter(
237*3d0502afSRüdiger Kessel            array_map('trim', explode(',', $raw)),
238*3d0502afSRüdiger Kessel            static fn($s) => $s !== ''
239*3d0502afSRüdiger Kessel        );
240*3d0502afSRüdiger Kessel
241*3d0502afSRüdiger Kessel        return $styles;
242*3d0502afSRüdiger Kessel    }
243*3d0502afSRüdiger Kessel}
244*3d0502afSRüdiger Kessel
245