xref: /plugin/infobox/syntax.php (revision 9e9b4c22289540b28728a8e7e16a871fa549906f) !
1971218fcSKanaru92<?php
2971218fcSKanaru92/**
3971218fcSKanaru92 * DokuWiki Plugin infobox (Syntax Component)
4971218fcSKanaru92 *
5971218fcSKanaru92 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6971218fcSKanaru92 */
7971218fcSKanaru92if (!defined('DOKU_INC')) die();
8971218fcSKanaru92
9971218fcSKanaru92class syntax_plugin_infobox extends DokuWiki_Syntax_Plugin {
10971218fcSKanaru92    public function getType() {
11971218fcSKanaru92        return 'substition';
12971218fcSKanaru92    }
13971218fcSKanaru92
14971218fcSKanaru92    public function getPType() {
15971218fcSKanaru92        return 'block';
16971218fcSKanaru92    }
17971218fcSKanaru92
18971218fcSKanaru92    public function getSort() {
19971218fcSKanaru92        return 199;
20971218fcSKanaru92    }
21971218fcSKanaru92
22971218fcSKanaru92    public function connectTo($mode) {
23a5ba79f0SKanaru92        $this->Lexer->addEntryPattern('\{\{infobox>', $mode, 'plugin_infobox');
24a5ba79f0SKanaru92    }
25a5ba79f0SKanaru92
26a5ba79f0SKanaru92    public function postConnect() {
27a5ba79f0SKanaru92        $this->Lexer->addExitPattern('\}\}', 'plugin_infobox');
28971218fcSKanaru92    }
29971218fcSKanaru92
30971218fcSKanaru92    public function handle($match, $state, $pos, Doku_Handler $handler) {
31a5ba79f0SKanaru92        switch ($state) {
32a5ba79f0SKanaru92            case DOKU_LEXER_ENTER:
33a5ba79f0SKanaru92                return array('state' => 'enter');
34a5ba79f0SKanaru92
35a5ba79f0SKanaru92            case DOKU_LEXER_UNMATCHED:
36a5ba79f0SKanaru92                // This contains the actual content between {{infobox> and }}
37a5ba79f0SKanaru92                $lines = explode("\n", $match);
38ffecb261SKanaru92
39ffecb261SKanaru92                $params = [
40ffecb261SKanaru92                    'fields' => [],
41ffecb261SKanaru92                    'images' => [],
42ffecb261SKanaru92                    'sections' => [],
43ffecb261SKanaru92                    'collapsed_sections' => []
44ffecb261SKanaru92                ];
45ffecb261SKanaru92
46ffecb261SKanaru92                $currentSection = null;
47a5ba79f0SKanaru92                $currentSubgroup = null;
48a5ba79f0SKanaru92                $currentKey = null;
49a5ba79f0SKanaru92                $currentValue = '';
50*9e9b4c22SKanaru92                $headerlessCounter = 0;
51ffecb261SKanaru92
52971218fcSKanaru92                foreach ($lines as $line) {
53a5ba79f0SKanaru92                    // Don't trim the line yet - we need to preserve indentation for multi-line values
54ffecb261SKanaru92
55a5ba79f0SKanaru92                    // Check if we're currently capturing a multi-line value
56a5ba79f0SKanaru92                    if ($currentKey !== null) {
57a5ba79f0SKanaru92                        // Continue capturing multi-line value
58a5ba79f0SKanaru92                        $currentValue .= "\n" . $line;
59a5ba79f0SKanaru92
60a5ba79f0SKanaru92                        // Check if all plugin syntaxes are closed
61a5ba79f0SKanaru92                        if (substr_count($currentValue, '{{') === substr_count($currentValue, '}}')) {
62a5ba79f0SKanaru92                            $this->_saveField($params, $currentKey, trim($currentValue), $currentSection, $currentSubgroup);
63a5ba79f0SKanaru92                            $currentKey = null;
64a5ba79f0SKanaru92                            $currentValue = '';
65a5ba79f0SKanaru92                        }
66a5ba79f0SKanaru92                        continue;
67a5ba79f0SKanaru92                    }
68a5ba79f0SKanaru92
69a5ba79f0SKanaru92                    $trimmedLine = trim($line);
70a5ba79f0SKanaru92                    if (empty($trimmedLine)) continue;
71a5ba79f0SKanaru92
72*9e9b4c22SKanaru92                    // Check for headerless section (====)
73*9e9b4c22SKanaru92                    if ($trimmedLine === '====') {
74*9e9b4c22SKanaru92                        $headerlessCounter++;
75*9e9b4c22SKanaru92                        $currentSection = '_headerless_' . $headerlessCounter;
76*9e9b4c22SKanaru92                        $currentSubgroup = null;
77*9e9b4c22SKanaru92                        $params['sections'][$currentSection] = [];
78*9e9b4c22SKanaru92                        continue;
79*9e9b4c22SKanaru92                    }
80*9e9b4c22SKanaru92
81a5ba79f0SKanaru92                    // Check for section headers
82a5ba79f0SKanaru92                    if (preg_match('/^(={2,3})\s*(.+?)\s*\1$/', $trimmedLine, $sectionMatches)) {
83a5ba79f0SKanaru92                        $currentSection = $sectionMatches[2];
84a5ba79f0SKanaru92                        $currentSubgroup = null; // Reset subgroup when entering new section
85ffecb261SKanaru92                        $params['sections'][$currentSection] = [];
86a5ba79f0SKanaru92                        if ($sectionMatches[1] === '===') {
87ffecb261SKanaru92                            $params['collapsed_sections'][$currentSection] = true;
88ffecb261SKanaru92                        }
89ffecb261SKanaru92                        continue;
90ffecb261SKanaru92                    }
91ffecb261SKanaru92
92*9e9b4c22SKanaru92                    // Check for headerless subgroup (::::::)
93*9e9b4c22SKanaru92                    if ($trimmedLine === '::::::') {
94*9e9b4c22SKanaru92                        if ($currentSection !== null) {
95*9e9b4c22SKanaru92                            $headerlessCounter++;
96*9e9b4c22SKanaru92                            $currentSubgroup = '_headerless_' . $headerlessCounter;
97*9e9b4c22SKanaru92                            if (!isset($params['sections'][$currentSection]['_subgroups'])) {
98*9e9b4c22SKanaru92                                $params['sections'][$currentSection]['_subgroups'] = [];
99*9e9b4c22SKanaru92                            }
100*9e9b4c22SKanaru92                            $params['sections'][$currentSection]['_subgroups'][$currentSubgroup] = [];
101*9e9b4c22SKanaru92                        }
102*9e9b4c22SKanaru92                        continue;
103*9e9b4c22SKanaru92                    }
104*9e9b4c22SKanaru92
105a5ba79f0SKanaru92                    // Check for subgroup headers (:::)
106a5ba79f0SKanaru92                    if (preg_match('/^:::\s*(.+?)\s*:::$/', $trimmedLine, $subgroupMatches)) {
107a5ba79f0SKanaru92                        if ($currentSection !== null) {
108a5ba79f0SKanaru92                            $currentSubgroup = $subgroupMatches[1];
109a5ba79f0SKanaru92                            if (!isset($params['sections'][$currentSection]['_subgroups'])) {
110a5ba79f0SKanaru92                                $params['sections'][$currentSection]['_subgroups'] = [];
111a5ba79f0SKanaru92                            }
112a5ba79f0SKanaru92                            $params['sections'][$currentSection]['_subgroups'][$currentSubgroup] = [];
113ffecb261SKanaru92                        }
114ffecb261SKanaru92                        continue;
115ffecb261SKanaru92                    }
116ffecb261SKanaru92
117cce5e697SKanaru92                    // Check for divider lines
118cce5e697SKanaru92                    if (preg_match('/^divider\s*=\s*(.+)$/i', $trimmedLine, $matches)) {
119cce5e697SKanaru92                        $dividerText = trim($matches[1]);
120cce5e697SKanaru92                        if ($currentSection !== null) {
121cce5e697SKanaru92                            if ($currentSubgroup !== null) {
122cce5e697SKanaru92                                // Add divider to subgroup
123cce5e697SKanaru92                                $params['sections'][$currentSection]['_subgroups'][$currentSubgroup]['_divider_' . md5($dividerText)] = [
124cce5e697SKanaru92                                    'type' => 'divider',
125cce5e697SKanaru92                                    'text' => $dividerText
126cce5e697SKanaru92                                ];
127cce5e697SKanaru92                            } else {
128cce5e697SKanaru92                                // Add divider to section
129cce5e697SKanaru92                                $params['sections'][$currentSection]['_divider_' . md5($dividerText)] = [
130cce5e697SKanaru92                                    'type' => 'divider',
131cce5e697SKanaru92                                    'text' => $dividerText
132cce5e697SKanaru92                                ];
133cce5e697SKanaru92                            }
134cce5e697SKanaru92                        } else {
135cce5e697SKanaru92                            // Add divider to main fields
136cce5e697SKanaru92                            $params['fields']['_divider_' . md5($dividerText)] = [
137cce5e697SKanaru92                                'type' => 'divider',
138cce5e697SKanaru92                                'text' => $dividerText
139cce5e697SKanaru92                            ];
140cce5e697SKanaru92                        }
141cce5e697SKanaru92                    }
142*9e9b4c22SKanaru92                    // Check for full-width value (= value =)
143*9e9b4c22SKanaru92                    elseif (preg_match('/^=\s+(.+?)\s+=$/', $trimmedLine, $fullwidthMatches)) {
144*9e9b4c22SKanaru92                        $fullwidthValue = $fullwidthMatches[1];
145*9e9b4c22SKanaru92                        $fullwidthKey = '_fullwidth_' . md5($fullwidthValue . microtime());
146*9e9b4c22SKanaru92                        $fieldData = [
147*9e9b4c22SKanaru92                            'type' => 'fullwidth',
148*9e9b4c22SKanaru92                            'value' => $fullwidthValue
149*9e9b4c22SKanaru92                        ];
150*9e9b4c22SKanaru92                        if ($currentSection !== null) {
151*9e9b4c22SKanaru92                            if ($currentSubgroup !== null) {
152*9e9b4c22SKanaru92                                $params['sections'][$currentSection]['_subgroups'][$currentSubgroup][$fullwidthKey] = $fieldData;
153*9e9b4c22SKanaru92                            } else {
154*9e9b4c22SKanaru92                                $params['sections'][$currentSection][$fullwidthKey] = $fieldData;
155*9e9b4c22SKanaru92                            }
156*9e9b4c22SKanaru92                        } else {
157*9e9b4c22SKanaru92                            $params['fields'][$fullwidthKey] = $fieldData;
158*9e9b4c22SKanaru92                        }
159*9e9b4c22SKanaru92                    }
160a5ba79f0SKanaru92                    // Check if this line contains a key=value pair
161cce5e697SKanaru92                    elseif (strpos($trimmedLine, '=') !== false) {
162a5ba79f0SKanaru92                        // Split only on the first = to handle values containing =
163a5ba79f0SKanaru92                        $pos = strpos($trimmedLine, '=');
164a5ba79f0SKanaru92                        $key = trim(substr($trimmedLine, 0, $pos));
165a5ba79f0SKanaru92                        $value = trim(substr($trimmedLine, $pos + 1));
166ffecb261SKanaru92
167a5ba79f0SKanaru92                        // Check if value contains unclosed plugin syntax
168a5ba79f0SKanaru92                        $openCount = substr_count($value, '{{');
169a5ba79f0SKanaru92                        $closeCount = substr_count($value, '}}');
170a5ba79f0SKanaru92
171a5ba79f0SKanaru92                        if ($openCount > $closeCount) {
172a5ba79f0SKanaru92                            // Value contains unclosed plugin syntax, start multi-line capture
173a5ba79f0SKanaru92                            $currentKey = $key;
174a5ba79f0SKanaru92                            $currentValue = $value;
175a5ba79f0SKanaru92                        } else {
176a5ba79f0SKanaru92                            // Value is complete on this line
177a5ba79f0SKanaru92                            $this->_saveField($params, $key, $value, $currentSection, $currentSubgroup);
178a5ba79f0SKanaru92                        }
179a5ba79f0SKanaru92                    }
180a5ba79f0SKanaru92                }
181a5ba79f0SKanaru92
182a5ba79f0SKanaru92                // Save any remaining multi-line value
183a5ba79f0SKanaru92                if ($currentKey !== null) {
184a5ba79f0SKanaru92                    $this->_saveField($params, $currentKey, trim($currentValue), $currentSection, $currentSubgroup);
185a5ba79f0SKanaru92                }
186a5ba79f0SKanaru92
187a5ba79f0SKanaru92                return array('state' => 'content', 'params' => $params);
188a5ba79f0SKanaru92
189a5ba79f0SKanaru92            case DOKU_LEXER_EXIT:
190a5ba79f0SKanaru92                return array('state' => 'exit');
191a5ba79f0SKanaru92        }
192a5ba79f0SKanaru92
193a5ba79f0SKanaru92        return false;
194a5ba79f0SKanaru92    }
195a5ba79f0SKanaru92
196a5ba79f0SKanaru92    private function _saveField(&$params, $key, $value, $currentSection, $currentSubgroup = null) {
197ffecb261SKanaru92        // Skip empty values unless explicitly showing them
198ffecb261SKanaru92        if (empty($value) && $value !== '0') {
199a5ba79f0SKanaru92            return;
200ffecb261SKanaru92        }
201ffecb261SKanaru92
202cce5e697SKanaru92        // Handle spoiler/blur functionality with ! prefix
203cce5e697SKanaru92        $blurKey = false;
204cce5e697SKanaru92        $blurValue = false;
205cce5e697SKanaru92
206cce5e697SKanaru92        // Check for ! in key (e.g., !name = david)
207cce5e697SKanaru92        if (strpos($key, '!') === 0) {
208cce5e697SKanaru92            $blurKey = true;
209cce5e697SKanaru92            $blurValue = true;
210cce5e697SKanaru92            $key = ltrim($key, '!');
211cce5e697SKanaru92        }
212cce5e697SKanaru92
213cce5e697SKanaru92        // Check for ! in value (e.g., name = !david)
214cce5e697SKanaru92        if (strpos($value, '!') === 0) {
215cce5e697SKanaru92            $blurValue = true;
216cce5e697SKanaru92            $value = ltrim($value, '!');
217cce5e697SKanaru92        }
218cce5e697SKanaru92
219a5ba79f0SKanaru92        // Handle image fields
220a5ba79f0SKanaru92        if (preg_match('/^image(\d*)$/', $key, $matches)) {
221a5ba79f0SKanaru92            $imgNum = $matches[1] ?: '1';
222ffecb261SKanaru92            // Check if image has a caption (format: filename|caption)
223ffecb261SKanaru92            if (strpos($value, '|') !== false) {
224ffecb261SKanaru92                list($imgPath, $caption) = explode('|', $value, 2);
225a5ba79f0SKanaru92                $params['images'][$imgNum] = [
226ffecb261SKanaru92                    'path' => trim($imgPath),
227ffecb261SKanaru92                    'caption' => trim($caption)
228ffecb261SKanaru92                ];
229ffecb261SKanaru92            } else {
230a5ba79f0SKanaru92                $params['images'][$imgNum] = [
231a5ba79f0SKanaru92                    'path' => trim($value),
232ffecb261SKanaru92                    'caption' => ''
233ffecb261SKanaru92                ];
234ffecb261SKanaru92            }
235ffecb261SKanaru92        } elseif ($key === 'name' || $key === 'title') {
236ffecb261SKanaru92            $params['name'] = $value;
237ffecb261SKanaru92        } elseif ($key === 'header_image') {
238ffecb261SKanaru92            $params['header_image'] = $value;
239a5ba79f0SKanaru92        } elseif ($key === 'class') {
240a5ba79f0SKanaru92            $params['class'] = $value;
241ffecb261SKanaru92        } else {
242a5ba79f0SKanaru92            // Add to current section/subgroup or main fields
243ffecb261SKanaru92            if ($currentSection !== null) {
244a5ba79f0SKanaru92                if ($currentSubgroup !== null) {
245a5ba79f0SKanaru92                    // Add to subgroup
246cce5e697SKanaru92                    $params['sections'][$currentSection]['_subgroups'][$currentSubgroup][$key] = [
247cce5e697SKanaru92                        'value' => $value,
248cce5e697SKanaru92                        'blur_key' => $blurKey,
249cce5e697SKanaru92                        'blur_value' => $blurValue
250cce5e697SKanaru92                    ];
251ffecb261SKanaru92                } else {
252a5ba79f0SKanaru92                    // Add to section (not in a subgroup)
253cce5e697SKanaru92                    $params['sections'][$currentSection][$key] = [
254cce5e697SKanaru92                        'value' => $value,
255cce5e697SKanaru92                        'blur_key' => $blurKey,
256cce5e697SKanaru92                        'blur_value' => $blurValue
257cce5e697SKanaru92                    ];
258a5ba79f0SKanaru92                }
259a5ba79f0SKanaru92            } else {
260a5ba79f0SKanaru92                // Add to main fields
261cce5e697SKanaru92                $params['fields'][$key] = [
262cce5e697SKanaru92                    'value' => $value,
263cce5e697SKanaru92                    'blur_key' => $blurKey,
264cce5e697SKanaru92                    'blur_value' => $blurValue
265cce5e697SKanaru92                ];
266971218fcSKanaru92            }
267971218fcSKanaru92        }
268ffecb261SKanaru92    }
269971218fcSKanaru92
270971218fcSKanaru92    public function render($mode, Doku_Renderer $renderer, $data) {
271971218fcSKanaru92        if ($mode != 'xhtml') return false;
272971218fcSKanaru92
273a5ba79f0SKanaru92        if (!is_array($data) || !isset($data['state'])) return false;
274a5ba79f0SKanaru92
275a5ba79f0SKanaru92        switch ($data['state']) {
276a5ba79f0SKanaru92            case 'enter':
277a5ba79f0SKanaru92                // Start of infobox - nothing to do
278a5ba79f0SKanaru92                break;
279a5ba79f0SKanaru92
280a5ba79f0SKanaru92            case 'content':
281a5ba79f0SKanaru92                // Render the actual infobox
282a5ba79f0SKanaru92                $params = $data['params'];
283a5ba79f0SKanaru92                $this->_renderInfobox($renderer, $params);
284a5ba79f0SKanaru92                break;
285a5ba79f0SKanaru92
286a5ba79f0SKanaru92            case 'exit':
287a5ba79f0SKanaru92                // End of infobox - nothing to do
288a5ba79f0SKanaru92                break;
289a5ba79f0SKanaru92        }
290a5ba79f0SKanaru92
291a5ba79f0SKanaru92        return true;
292a5ba79f0SKanaru92    }
293a5ba79f0SKanaru92
294a5ba79f0SKanaru92    private function _renderInfobox($renderer, $data) {
295ffecb261SKanaru92        // Generate unique ID for this infobox
296ffecb261SKanaru92        $boxId = 'infobox_' . md5(serialize($data));
297971218fcSKanaru92
298ffecb261SKanaru92        // Allow custom CSS classes
299a5ba79f0SKanaru92        $customClass = isset($data['class']) ? ' ' . hsc($data['class']) : '';
300ffecb261SKanaru92
301ffecb261SKanaru92        $renderer->doc .= '<div class="infobox' . $customClass . '" id="' . $boxId . '" role="complementary" aria-label="Information box">';
302ffecb261SKanaru92
303ffecb261SKanaru92        // Header image (optional)
304ffecb261SKanaru92        if (isset($data['header_image'])) {
305ffecb261SKanaru92            $renderer->doc .= '<div class="infobox-header-image">';
306ffecb261SKanaru92            $renderer->internalmedia(
307ffecb261SKanaru92                $data['header_image'],
308ffecb261SKanaru92                null,
309ffecb261SKanaru92                null,
310ffecb261SKanaru92                null,
311ffecb261SKanaru92                null,
312ffecb261SKanaru92                'cache',
313ffecb261SKanaru92                'details'
314ffecb261SKanaru92            );
315ffecb261SKanaru92            $renderer->doc .= '</div>';
316ffecb261SKanaru92        }
317ffecb261SKanaru92
318ffecb261SKanaru92        // Title
319971218fcSKanaru92        if (isset($data['name'])) {
320ad4c48d3Sbpmcneilly            $renderer->doc .= '<div class="infobox-title">' . $this->_parseWikiText($data['name']) . '</div>';
321971218fcSKanaru92        }
322971218fcSKanaru92
323ffecb261SKanaru92        // Multiple images with tabs
324ffecb261SKanaru92        if (!empty($data['images'])) {
325ffecb261SKanaru92            $renderer->doc .= '<div class="infobox-images">';
326ffecb261SKanaru92
327ffecb261SKanaru92            // Image tabs
328ffecb261SKanaru92            if (count($data['images']) > 1) {
329ffecb261SKanaru92                $renderer->doc .= '<div class="infobox-image-tabs" role="tablist">';
330ffecb261SKanaru92                $first = true;
331ffecb261SKanaru92                $tabCount = 1;
332ffecb261SKanaru92                foreach ($data['images'] as $num => $imgData) {
333ffecb261SKanaru92                    $tabLabel = $imgData['caption'] ?: 'Image ' . $tabCount;
334ffecb261SKanaru92                    $activeClass = $first ? ' active' : '';
335ffecb261SKanaru92                    $tabIndex = $first ? '0' : '-1';
336ffecb261SKanaru92                    $ariaSelected = $first ? 'true' : 'false';
337ffecb261SKanaru92                    $renderer->doc .= '<button class="infobox-tab' . $activeClass . '" role="tab" aria-selected="' . $ariaSelected . '" onclick="showInfoboxImage(\'' . $boxId . '\', ' . $num . ')" aria-label="View ' . hsc($tabLabel) . '" tabindex="' . $tabIndex . '">';
338ffecb261SKanaru92                    // Add tab number if no custom caption provided
339ffecb261SKanaru92                    if (!$imgData['caption'] && count($data['images']) > 1) {
340ffecb261SKanaru92                        $renderer->doc .= '<span class="infobox-tab-number">' . $tabCount . '. </span>';
341ffecb261SKanaru92                    }
342ffecb261SKanaru92                    $renderer->doc .= hsc($tabLabel);
343ffecb261SKanaru92                    $renderer->doc .= '</button>';
344ffecb261SKanaru92                    $first = false;
345ffecb261SKanaru92                    $tabCount++;
346ffecb261SKanaru92                }
347ffecb261SKanaru92                $renderer->doc .= '</div>';
348971218fcSKanaru92            }
349971218fcSKanaru92
350ffecb261SKanaru92            // Image containers
351ffecb261SKanaru92            $first = true;
352ffecb261SKanaru92            foreach ($data['images'] as $num => $imgData) {
353ffecb261SKanaru92                $activeClass = $first ? ' active' : '';
354ffecb261SKanaru92                $renderer->doc .= '<div class="infobox-image-container' . $activeClass . '" id="' . $boxId . '_img_' . $num . '" role="tabpanel">';
355ffecb261SKanaru92
356ffecb261SKanaru92                // Use DokuWiki's internal media rendering for proper lightbox support
357ffecb261SKanaru92                $renderer->internalmedia(
358ffecb261SKanaru92                    $imgData['path'],
359ffecb261SKanaru92                    $imgData['caption'] ?: null,
360ffecb261SKanaru92                    null,
361ffecb261SKanaru92                    300,
362ffecb261SKanaru92                    null,
363ffecb261SKanaru92                    'cache',
364ffecb261SKanaru92                    'details'
365ffecb261SKanaru92                );
366ffecb261SKanaru92
367ffecb261SKanaru92                if ($imgData['caption'] && count($data['images']) == 1) {
368ffecb261SKanaru92                    $renderer->doc .= '<div class="infobox-image-caption">' . hsc($imgData['caption']) . '</div>';
369971218fcSKanaru92                }
370ffecb261SKanaru92                $renderer->doc .= '</div>';
371ffecb261SKanaru92                $first = false;
372971218fcSKanaru92            }
373971218fcSKanaru92
374971218fcSKanaru92            $renderer->doc .= '</div>';
375ffecb261SKanaru92        }
376ffecb261SKanaru92
377ffecb261SKanaru92        // Main fields table
378ffecb261SKanaru92        if (!empty($data['fields'])) {
379ffecb261SKanaru92            $renderer->doc .= '<table class="infobox-table">';
380cce5e697SKanaru92            foreach ($data['fields'] as $key => $fieldData) {
381cce5e697SKanaru92                // Handle dividers
382cce5e697SKanaru92                if (is_array($fieldData) && isset($fieldData['type']) && $fieldData['type'] === 'divider') {
383cce5e697SKanaru92                    $renderer->doc .= '</table><div class="infobox-divider">' . hsc($fieldData['text']) . '</div><table class="infobox-table">';
384cce5e697SKanaru92                    continue;
385cce5e697SKanaru92                }
386cce5e697SKanaru92
387*9e9b4c22SKanaru92                // Handle full-width values
388*9e9b4c22SKanaru92                if (is_array($fieldData) && isset($fieldData['type']) && $fieldData['type'] === 'fullwidth') {
389*9e9b4c22SKanaru92                    $renderer->doc .= '<tr><td colspan="2" class="infobox-fullwidth">' . $this->_parseWikiText($fieldData['value']) . '</td></tr>';
390*9e9b4c22SKanaru92                    continue;
391*9e9b4c22SKanaru92                }
392*9e9b4c22SKanaru92
393cce5e697SKanaru92                // Handle both old string format and new array format for backwards compatibility
394cce5e697SKanaru92                if (is_array($fieldData)) {
395cce5e697SKanaru92                    $value = $fieldData['value'];
396*9e9b4c22SKanaru92                    $blurKey = isset($fieldData['blur_key']) ? $fieldData['blur_key'] : false;
397*9e9b4c22SKanaru92                    $blurValue = isset($fieldData['blur_value']) ? $fieldData['blur_value'] : false;
398cce5e697SKanaru92                } else {
399cce5e697SKanaru92                    $value = $fieldData;
400cce5e697SKanaru92                    $blurKey = false;
401cce5e697SKanaru92                    $blurValue = false;
402cce5e697SKanaru92                }
403cce5e697SKanaru92
404cce5e697SKanaru92                $keyClass = $blurKey ? ' class="infobox-spoiler"' : '';
405cce5e697SKanaru92                $valueClass = $blurValue ? ' class="infobox-spoiler"' : '';
406cce5e697SKanaru92
407ffecb261SKanaru92                $renderer->doc .= '<tr>';
408cce5e697SKanaru92                $renderer->doc .= '<th' . $keyClass . '>' . $this->_renderFieldName($key) . '</th>';
409cce5e697SKanaru92                $renderer->doc .= '<td' . $valueClass . '>' . $this->_parseWikiText($value) . '</td>';
410ffecb261SKanaru92                $renderer->doc .= '</tr>';
411ffecb261SKanaru92            }
412ffecb261SKanaru92            $renderer->doc .= '</table>';
413ffecb261SKanaru92        }
414ffecb261SKanaru92
415ffecb261SKanaru92        // Sections
416a5ba79f0SKanaru92        foreach ($data['sections'] as $sectionName => $sectionData) {
417ffecb261SKanaru92            $sectionId = $boxId . '_section_' . md5($sectionName);
418ffecb261SKanaru92            $isCollapsed = isset($data['collapsed_sections'][$sectionName]);
419*9e9b4c22SKanaru92            $isHeaderless = strpos($sectionName, '_headerless_') === 0;
420ffecb261SKanaru92            $collapsibleClass = $isCollapsed ? ' collapsible collapsed' : '';
421*9e9b4c22SKanaru92            $headerlessClass = $isHeaderless ? ' headerless' : '';
422ffecb261SKanaru92
423*9e9b4c22SKanaru92            $renderer->doc .= '<div class="infobox-section' . $collapsibleClass . $headerlessClass . '">';
424*9e9b4c22SKanaru92
425*9e9b4c22SKanaru92            // Only render section header if not headerless
426*9e9b4c22SKanaru92            if (!$isHeaderless) {
427ffecb261SKanaru92                if ($isCollapsed) {
428ffecb261SKanaru92                    $renderer->doc .= '<div class="infobox-section-header" onclick="toggleInfoboxSection(\'' . $sectionId . '\')" ' .
429ffecb261SKanaru92                                     'role="button" tabindex="0" aria-expanded="false" aria-controls="' . $sectionId . '">';
430ffecb261SKanaru92                } else {
431ffecb261SKanaru92                    $renderer->doc .= '<div class="infobox-section-header">';
432ffecb261SKanaru92                }
433ffecb261SKanaru92                $renderer->doc .= hsc($sectionName);
434ffecb261SKanaru92                if ($isCollapsed) {
435ffecb261SKanaru92                    $renderer->doc .= '<span class="infobox-section-toggle">▼</span>';
436ffecb261SKanaru92                }
437ffecb261SKanaru92                $renderer->doc .= '</div>';
438*9e9b4c22SKanaru92            }
439ffecb261SKanaru92
440ffecb261SKanaru92            $contentClass = $isCollapsed ? 'infobox-section-content collapsed' : 'infobox-section-content';
441ffecb261SKanaru92            $renderer->doc .= '<div class="' . $contentClass . '" id="' . $sectionId . '">';
442ffecb261SKanaru92
443a5ba79f0SKanaru92            // Check if this section has subgroups
444a5ba79f0SKanaru92            $hasSubgroups = isset($sectionData['_subgroups']) && !empty($sectionData['_subgroups']);
445a5ba79f0SKanaru92
446a5ba79f0SKanaru92            if ($hasSubgroups) {
447a5ba79f0SKanaru92                // Render subgroups
448a5ba79f0SKanaru92                $renderer->doc .= '<div class="infobox-subgroups">';
449a5ba79f0SKanaru92                foreach ($sectionData['_subgroups'] as $subgroupName => $subgroupFields) {
450*9e9b4c22SKanaru92                    $isSubgroupHeaderless = strpos($subgroupName, '_headerless_') === 0;
451*9e9b4c22SKanaru92                    $subgroupClass = $isSubgroupHeaderless ? 'infobox-subgroup headerless' : 'infobox-subgroup';
452*9e9b4c22SKanaru92                    $renderer->doc .= '<div class="' . $subgroupClass . '">';
453*9e9b4c22SKanaru92                    if (!$isSubgroupHeaderless) {
454a5ba79f0SKanaru92                        $renderer->doc .= '<div class="infobox-subgroup-header">' . hsc($subgroupName) . '</div>';
455*9e9b4c22SKanaru92                    }
456a5ba79f0SKanaru92
457a5ba79f0SKanaru92                    if (!empty($subgroupFields)) {
458ffecb261SKanaru92                        $renderer->doc .= '<table class="infobox-table">';
459cce5e697SKanaru92                        foreach ($subgroupFields as $key => $fieldData) {
460cce5e697SKanaru92                            // Handle dividers
461cce5e697SKanaru92                            if (is_array($fieldData) && isset($fieldData['type']) && $fieldData['type'] === 'divider') {
462cce5e697SKanaru92                                $renderer->doc .= '</table><div class="infobox-divider">' . hsc($fieldData['text']) . '</div><table class="infobox-table">';
463cce5e697SKanaru92                                continue;
464cce5e697SKanaru92                            }
465cce5e697SKanaru92
466*9e9b4c22SKanaru92                            // Handle full-width values (spans column width in subgroups)
467*9e9b4c22SKanaru92                            if (is_array($fieldData) && isset($fieldData['type']) && $fieldData['type'] === 'fullwidth') {
468*9e9b4c22SKanaru92                                $renderer->doc .= '<tr><td colspan="2" class="infobox-fullwidth">' . $this->_parseWikiText($fieldData['value']) . '</td></tr>';
469*9e9b4c22SKanaru92                                continue;
470*9e9b4c22SKanaru92                            }
471*9e9b4c22SKanaru92
472cce5e697SKanaru92                            // Handle both old string format and new array format for backwards compatibility
473cce5e697SKanaru92                            if (is_array($fieldData)) {
474cce5e697SKanaru92                                $value = $fieldData['value'];
475*9e9b4c22SKanaru92                                $blurKey = isset($fieldData['blur_key']) ? $fieldData['blur_key'] : false;
476*9e9b4c22SKanaru92                                $blurValue = isset($fieldData['blur_value']) ? $fieldData['blur_value'] : false;
477cce5e697SKanaru92                            } else {
478cce5e697SKanaru92                                $value = $fieldData;
479cce5e697SKanaru92                                $blurKey = false;
480cce5e697SKanaru92                                $blurValue = false;
481cce5e697SKanaru92                            }
482cce5e697SKanaru92
483cce5e697SKanaru92                            $keyClass = $blurKey ? ' class="infobox-spoiler"' : '';
484cce5e697SKanaru92                            $valueClass = $blurValue ? ' class="infobox-spoiler"' : '';
485cce5e697SKanaru92
486ffecb261SKanaru92                            $renderer->doc .= '<tr>';
487cce5e697SKanaru92                            $renderer->doc .= '<th' . $keyClass . '>' . $this->_renderFieldName($key) . '</th>';
488cce5e697SKanaru92                            $renderer->doc .= '<td' . $valueClass . '>' . $this->_parseWikiText($value) . '</td>';
489ffecb261SKanaru92                            $renderer->doc .= '</tr>';
490ffecb261SKanaru92                        }
491ffecb261SKanaru92                        $renderer->doc .= '</table>';
492ffecb261SKanaru92                    }
493ffecb261SKanaru92                    $renderer->doc .= '</div>';
494a5ba79f0SKanaru92                }
495a5ba79f0SKanaru92                $renderer->doc .= '</div>';
496a5ba79f0SKanaru92            }
497a5ba79f0SKanaru92
498a5ba79f0SKanaru92            // Render regular section fields (not in subgroups)
499a5ba79f0SKanaru92            $regularFields = array_filter($sectionData, function($key) {
500a5ba79f0SKanaru92                return $key !== '_subgroups';
501a5ba79f0SKanaru92            }, ARRAY_FILTER_USE_KEY);
502a5ba79f0SKanaru92
503a5ba79f0SKanaru92            if (!empty($regularFields)) {
504a5ba79f0SKanaru92                $renderer->doc .= '<table class="infobox-table">';
505cce5e697SKanaru92                foreach ($regularFields as $key => $fieldData) {
506cce5e697SKanaru92                    // Handle dividers
507cce5e697SKanaru92                    if (is_array($fieldData) && isset($fieldData['type']) && $fieldData['type'] === 'divider') {
508cce5e697SKanaru92                        $renderer->doc .= '</table><div class="infobox-divider">' . hsc($fieldData['text']) . '</div><table class="infobox-table">';
509cce5e697SKanaru92                        continue;
510cce5e697SKanaru92                    }
511cce5e697SKanaru92
512*9e9b4c22SKanaru92                    // Handle full-width values
513*9e9b4c22SKanaru92                    if (is_array($fieldData) && isset($fieldData['type']) && $fieldData['type'] === 'fullwidth') {
514*9e9b4c22SKanaru92                        $renderer->doc .= '<tr><td colspan="2" class="infobox-fullwidth">' . $this->_parseWikiText($fieldData['value']) . '</td></tr>';
515*9e9b4c22SKanaru92                        continue;
516*9e9b4c22SKanaru92                    }
517*9e9b4c22SKanaru92
518cce5e697SKanaru92                    // Handle both old string format and new array format for backwards compatibility
519cce5e697SKanaru92                    if (is_array($fieldData)) {
520cce5e697SKanaru92                        $value = $fieldData['value'];
521*9e9b4c22SKanaru92                        $blurKey = isset($fieldData['blur_key']) ? $fieldData['blur_key'] : false;
522*9e9b4c22SKanaru92                        $blurValue = isset($fieldData['blur_value']) ? $fieldData['blur_value'] : false;
523cce5e697SKanaru92                    } else {
524cce5e697SKanaru92                        $value = $fieldData;
525cce5e697SKanaru92                        $blurKey = false;
526cce5e697SKanaru92                        $blurValue = false;
527cce5e697SKanaru92                    }
528cce5e697SKanaru92
529cce5e697SKanaru92                    $keyClass = $blurKey ? ' class="infobox-spoiler"' : '';
530cce5e697SKanaru92                    $valueClass = $blurValue ? ' class="infobox-spoiler"' : '';
531cce5e697SKanaru92
532a5ba79f0SKanaru92                    $renderer->doc .= '<tr>';
533cce5e697SKanaru92                    $renderer->doc .= '<th' . $keyClass . '>' . $this->_renderFieldName($key) . '</th>';
534cce5e697SKanaru92                    $renderer->doc .= '<td' . $valueClass . '>' . $this->_parseWikiText($value) . '</td>';
535a5ba79f0SKanaru92                    $renderer->doc .= '</tr>';
536a5ba79f0SKanaru92                }
537a5ba79f0SKanaru92                $renderer->doc .= '</table>';
538a5ba79f0SKanaru92            }
539a5ba79f0SKanaru92
540a5ba79f0SKanaru92            $renderer->doc .= '</div>';
541ffecb261SKanaru92            $renderer->doc .= '</div>';
542ffecb261SKanaru92        }
543ffecb261SKanaru92
544ffecb261SKanaru92        $renderer->doc .= '</div>';
545ffecb261SKanaru92
546ffecb261SKanaru92        // Add JavaScript for image tabs (only once per page)
547ffecb261SKanaru92        static $jsAdded = false;
548ffecb261SKanaru92        if (!$jsAdded) {
549ffecb261SKanaru92            $renderer->doc .= '<script>
550cce5e697SKanaru92            // Spoiler click-to-reveal functionality
551cce5e697SKanaru92            function revealSpoiler(element) {
552cce5e697SKanaru92                element.classList.add("revealed");
553cce5e697SKanaru92                element.removeAttribute("tabindex");
554cce5e697SKanaru92                element.setAttribute("aria-label", "Content revealed");
555cce5e697SKanaru92            }
556cce5e697SKanaru92
557ffecb261SKanaru92            function showInfoboxImage(boxId, imageNum) {
558ffecb261SKanaru92                // Hide all images in this infobox
559ffecb261SKanaru92                var containers = document.querySelectorAll("#" + boxId + " .infobox-image-container");
560ffecb261SKanaru92                containers.forEach(function(container) {
561ffecb261SKanaru92                    container.classList.remove("active");
562ffecb261SKanaru92                });
563ffecb261SKanaru92
564ffecb261SKanaru92                // Remove active class from all tabs in this infobox
565ffecb261SKanaru92                var tabs = document.querySelectorAll("#" + boxId + " .infobox-tab");
566ffecb261SKanaru92                tabs.forEach(function(tab) {
567ffecb261SKanaru92                    tab.classList.remove("active");
568ffecb261SKanaru92                    tab.setAttribute("tabindex", "-1");
569ffecb261SKanaru92                    tab.setAttribute("aria-selected", "false");
570ffecb261SKanaru92                });
571ffecb261SKanaru92
572ffecb261SKanaru92                // Show selected image
573ffecb261SKanaru92                document.getElementById(boxId + "_img_" + imageNum).classList.add("active");
574ffecb261SKanaru92
575ffecb261SKanaru92                // Add active class to clicked tab
576ffecb261SKanaru92                var activeTab = tabs[imageNum - 1];
577ffecb261SKanaru92                activeTab.classList.add("active");
578ffecb261SKanaru92                activeTab.setAttribute("tabindex", "0");
579ffecb261SKanaru92                activeTab.setAttribute("aria-selected", "true");
580ffecb261SKanaru92            }
581ffecb261SKanaru92
582ffecb261SKanaru92            function toggleInfoboxSection(sectionId) {
583ffecb261SKanaru92                var content = document.getElementById(sectionId);
584ffecb261SKanaru92                var header = content.previousElementSibling;
585ffecb261SKanaru92                var toggle = header.querySelector(".infobox-section-toggle");
586ffecb261SKanaru92
587ffecb261SKanaru92                if (content.classList.contains("collapsed")) {
588ffecb261SKanaru92                    content.classList.remove("collapsed");
589ffecb261SKanaru92                    header.setAttribute("aria-expanded", "true");
590ffecb261SKanaru92                    if (toggle) toggle.textContent = "▲";
591ffecb261SKanaru92                } else {
592ffecb261SKanaru92                    content.classList.add("collapsed");
593ffecb261SKanaru92                    header.setAttribute("aria-expanded", "false");
594ffecb261SKanaru92                    if (toggle) toggle.textContent = "▼";
595ffecb261SKanaru92                }
596ffecb261SKanaru92            }
597ffecb261SKanaru92
598ffecb261SKanaru92            // Add keyboard support
599ffecb261SKanaru92            document.addEventListener("DOMContentLoaded", function() {
600cce5e697SKanaru92                // Initialize spoiler elements
601cce5e697SKanaru92                var spoilers = document.querySelectorAll(".infobox-spoiler");
602cce5e697SKanaru92                spoilers.forEach(function(spoiler) {
603cce5e697SKanaru92                    spoiler.setAttribute("tabindex", "0");
604cce5e697SKanaru92                    spoiler.setAttribute("role", "button");
605cce5e697SKanaru92                    spoiler.setAttribute("aria-label", "Blurred content - click or press Enter to reveal");
606cce5e697SKanaru92
607cce5e697SKanaru92                    spoiler.addEventListener("click", function() {
608cce5e697SKanaru92                        if (!this.classList.contains("revealed")) {
609cce5e697SKanaru92                            revealSpoiler(this);
610cce5e697SKanaru92                        }
611cce5e697SKanaru92                    });
612cce5e697SKanaru92
613cce5e697SKanaru92                    spoiler.addEventListener("keydown", function(e) {
614cce5e697SKanaru92                        if ((e.key === "Enter" || e.key === " ") && !this.classList.contains("revealed")) {
615cce5e697SKanaru92                            e.preventDefault();
616cce5e697SKanaru92                            revealSpoiler(this);
617cce5e697SKanaru92                        }
618cce5e697SKanaru92                    });
619cce5e697SKanaru92                });
620cce5e697SKanaru92
621ffecb261SKanaru92                // Keyboard navigation for image tabs
622ffecb261SKanaru92                var tabGroups = document.querySelectorAll(".infobox-image-tabs");
623ffecb261SKanaru92                tabGroups.forEach(function(tabGroup) {
624ffecb261SKanaru92                    var tabs = tabGroup.querySelectorAll(".infobox-tab");
625ffecb261SKanaru92
626ffecb261SKanaru92                    tabs.forEach(function(tab, index) {
627ffecb261SKanaru92                        tab.addEventListener("keydown", function(e) {
628ffecb261SKanaru92                            var newIndex = -1;
629ffecb261SKanaru92
630ffecb261SKanaru92                            switch(e.key) {
631ffecb261SKanaru92                                case "ArrowLeft":
632ffecb261SKanaru92                                    e.preventDefault();
633ffecb261SKanaru92                                    newIndex = index - 1;
634ffecb261SKanaru92                                    if (newIndex < 0) newIndex = tabs.length - 1;
635ffecb261SKanaru92                                    break;
636ffecb261SKanaru92                                case "ArrowRight":
637ffecb261SKanaru92                                    e.preventDefault();
638ffecb261SKanaru92                                    newIndex = index + 1;
639ffecb261SKanaru92                                    if (newIndex >= tabs.length) newIndex = 0;
640ffecb261SKanaru92                                    break;
641ffecb261SKanaru92                                case "Home":
642ffecb261SKanaru92                                    e.preventDefault();
643ffecb261SKanaru92                                    newIndex = 0;
644ffecb261SKanaru92                                    break;
645ffecb261SKanaru92                                case "End":
646ffecb261SKanaru92                                    e.preventDefault();
647ffecb261SKanaru92                                    newIndex = tabs.length - 1;
648ffecb261SKanaru92                                    break;
649ffecb261SKanaru92                            }
650ffecb261SKanaru92
651ffecb261SKanaru92                            if (newIndex >= 0) {
652ffecb261SKanaru92                                tabs[newIndex].focus();
653ffecb261SKanaru92                                tabs[newIndex].click();
654ffecb261SKanaru92                            }
655ffecb261SKanaru92                        });
656ffecb261SKanaru92                    });
657ffecb261SKanaru92                });
658ffecb261SKanaru92
659ffecb261SKanaru92                // Keyboard support for collapsible sections
660ffecb261SKanaru92                var headers = document.querySelectorAll(".infobox-section.collapsible .infobox-section-header");
661ffecb261SKanaru92                headers.forEach(function(header) {
662ffecb261SKanaru92                    header.addEventListener("keydown", function(e) {
663ffecb261SKanaru92                        if (e.key === "Enter" || e.key === " ") {
664ffecb261SKanaru92                            e.preventDefault();
665ffecb261SKanaru92                            header.click();
666ffecb261SKanaru92                        }
667ffecb261SKanaru92                    });
668ffecb261SKanaru92                });
669ffecb261SKanaru92
670ffecb261SKanaru92                // Fix for section header lines
671ffecb261SKanaru92                var infoboxes = document.querySelectorAll(".infobox");
672ffecb261SKanaru92                if (infoboxes.length > 0) {
673ffecb261SKanaru92                    var headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
674ffecb261SKanaru92                    headers.forEach(function(header) {
675ffecb261SKanaru92                        header.style.overflow = "hidden";
676ffecb261SKanaru92                    });
677ffecb261SKanaru92
678ffecb261SKanaru92                    var pageContent = document.querySelector(".dokuwiki .page");
679ffecb261SKanaru92                    if (pageContent && !pageContent.classList.contains("has-infobox")) {
680ffecb261SKanaru92                        pageContent.classList.add("has-infobox");
681ffecb261SKanaru92                    }
682ffecb261SKanaru92                }
683ffecb261SKanaru92            });
684ffecb261SKanaru92            </script>';
685ffecb261SKanaru92            $jsAdded = true;
686ffecb261SKanaru92        }
687ffecb261SKanaru92
688971218fcSKanaru92        return true;
689971218fcSKanaru92    }
690971218fcSKanaru92
691971218fcSKanaru92    private function _parseWikiText($text) {
692ffecb261SKanaru92        // Handle multi-line values properly
693ffecb261SKanaru92        $text = str_replace('\n', "\n", $text);
694ffecb261SKanaru92
695971218fcSKanaru92        $info = array();
696971218fcSKanaru92        $xhtml = p_render('xhtml', p_get_instructions($text), $info);
697ffecb261SKanaru92
698ffecb261SKanaru92        // Remove wrapping <p> tags if it's a single paragraph
699ffecb261SKanaru92        if (substr_count($xhtml, '<p>') == 1) {
700ffecb261SKanaru92            $xhtml = preg_replace('/^\s*<p>(.*)<\/p>\s*$/s', '$1', $xhtml);
701ffecb261SKanaru92        }
702ffecb261SKanaru92
703971218fcSKanaru92        return $xhtml;
704971218fcSKanaru92    }
705ffecb261SKanaru92
706ffecb261SKanaru92    private function _formatKey($key) {
707ffecb261SKanaru92        // Convert underscores to spaces and capitalize words
708ffecb261SKanaru92        return ucwords(str_replace('_', ' ', $key));
709ffecb261SKanaru92    }
71004e6edfbSKanaru92
71104e6edfbSKanaru92    private function _renderFieldName($key) {
71204e6edfbSKanaru92        // Handle pipe syntax for icons: "icon.png|Field Name"
71304e6edfbSKanaru92        if (strpos($key, '|') !== false && preg_match('/^([^|]+)\|(.+)$/', $key, $matches)) {
71404e6edfbSKanaru92            $iconFile = trim($matches[1]);
71504e6edfbSKanaru92            $label = trim($matches[2]);
71604e6edfbSKanaru92
71704e6edfbSKanaru92            // Check if the first part looks like an image file
71804e6edfbSKanaru92            if (preg_match('/\.(png|jpg|jpeg|gif|svg)$/i', $iconFile)) {
71904e6edfbSKanaru92                // Use DokuWiki's media resolution instead of manual path construction
72004e6edfbSKanaru92                global $conf;
72104e6edfbSKanaru92
72204e6edfbSKanaru92                // Try to resolve the media file using DokuWiki's functions
72304e6edfbSKanaru92                $mediaFile = cleanID($iconFile);
72404e6edfbSKanaru92                $file = mediaFN($mediaFile);
72504e6edfbSKanaru92
72604e6edfbSKanaru92                if (file_exists($file)) {
72704e6edfbSKanaru92                    // File exists, use DokuWiki's media URL
72804e6edfbSKanaru92                    $iconHtml = '<img src="' . ml($mediaFile) . '" alt="" class="infobox-field-icon" />';
72904e6edfbSKanaru92                } else {
73004e6edfbSKanaru92                    // Fallback: try as direct media reference with debugging
73104e6edfbSKanaru92                    $iconHtml = '<img src="' . DOKU_URL . 'data/media/' . hsc($iconFile) . '" alt="[' . hsc($iconFile) . ']" class="infobox-field-icon" title="Icon: ' . hsc($iconFile) . '" />';
73204e6edfbSKanaru92                }
73304e6edfbSKanaru92
73404e6edfbSKanaru92                return $iconHtml . hsc($this->_formatKey($label));
73504e6edfbSKanaru92            }
73604e6edfbSKanaru92        }
73704e6edfbSKanaru92
73804e6edfbSKanaru92        // No icons, just format normally
73904e6edfbSKanaru92        return hsc($this->_formatKey($key));
74004e6edfbSKanaru92    }
741971218fcSKanaru92}