xref: /plugin/infobox/syntax.php (revision a5ba79f0f6270e6c53fe8c5a8a579c49432238f8)
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) {
23*a5ba79f0SKanaru92        $this->Lexer->addEntryPattern('\{\{infobox>', $mode, 'plugin_infobox');
24*a5ba79f0SKanaru92    }
25*a5ba79f0SKanaru92
26*a5ba79f0SKanaru92    public function postConnect() {
27*a5ba79f0SKanaru92        $this->Lexer->addExitPattern('\}\}', 'plugin_infobox');
28971218fcSKanaru92    }
29971218fcSKanaru92
30971218fcSKanaru92    public function handle($match, $state, $pos, Doku_Handler $handler) {
31*a5ba79f0SKanaru92        switch ($state) {
32*a5ba79f0SKanaru92            case DOKU_LEXER_ENTER:
33*a5ba79f0SKanaru92                return array('state' => 'enter');
34*a5ba79f0SKanaru92
35*a5ba79f0SKanaru92            case DOKU_LEXER_UNMATCHED:
36*a5ba79f0SKanaru92                // This contains the actual content between {{infobox> and }}
37*a5ba79f0SKanaru92                $lines = explode("\n", $match);
38ffecb261SKanaru92
39ffecb261SKanaru92                $params = [
40ffecb261SKanaru92                    'fields' => [],
41ffecb261SKanaru92                    'images' => [],
42ffecb261SKanaru92                    'sections' => [],
43ffecb261SKanaru92                    'collapsed_sections' => []
44ffecb261SKanaru92                ];
45ffecb261SKanaru92
46ffecb261SKanaru92                $currentSection = null;
47*a5ba79f0SKanaru92                $currentSubgroup = null;
48ffecb261SKanaru92                $imageIndex = 0;
49*a5ba79f0SKanaru92                $currentKey = null;
50*a5ba79f0SKanaru92                $currentValue = '';
51ffecb261SKanaru92
52971218fcSKanaru92                foreach ($lines as $line) {
53*a5ba79f0SKanaru92                    // Don't trim the line yet - we need to preserve indentation for multi-line values
54ffecb261SKanaru92
55*a5ba79f0SKanaru92                    // Check if we're currently capturing a multi-line value
56*a5ba79f0SKanaru92                    if ($currentKey !== null) {
57*a5ba79f0SKanaru92                        // Continue capturing multi-line value
58*a5ba79f0SKanaru92                        $currentValue .= "\n" . $line;
59*a5ba79f0SKanaru92
60*a5ba79f0SKanaru92                        // Check if all plugin syntaxes are closed
61*a5ba79f0SKanaru92                        if (substr_count($currentValue, '{{') === substr_count($currentValue, '}}')) {
62*a5ba79f0SKanaru92                            $this->_saveField($params, $currentKey, trim($currentValue), $currentSection, $currentSubgroup);
63*a5ba79f0SKanaru92                            $currentKey = null;
64*a5ba79f0SKanaru92                            $currentValue = '';
65*a5ba79f0SKanaru92                        }
66*a5ba79f0SKanaru92                        continue;
67*a5ba79f0SKanaru92                    }
68*a5ba79f0SKanaru92
69*a5ba79f0SKanaru92                    $trimmedLine = trim($line);
70*a5ba79f0SKanaru92                    if (empty($trimmedLine)) continue;
71*a5ba79f0SKanaru92
72*a5ba79f0SKanaru92                    // Check for section headers
73*a5ba79f0SKanaru92                    if (preg_match('/^(={2,3})\s*(.+?)\s*\1$/', $trimmedLine, $sectionMatches)) {
74*a5ba79f0SKanaru92                        $currentSection = $sectionMatches[2];
75*a5ba79f0SKanaru92                        $currentSubgroup = null; // Reset subgroup when entering new section
76ffecb261SKanaru92                        $params['sections'][$currentSection] = [];
77*a5ba79f0SKanaru92                        if ($sectionMatches[1] === '===') {
78ffecb261SKanaru92                            $params['collapsed_sections'][$currentSection] = true;
79ffecb261SKanaru92                        }
80ffecb261SKanaru92                        continue;
81ffecb261SKanaru92                    }
82ffecb261SKanaru92
83*a5ba79f0SKanaru92                    // Check for subgroup headers (:::)
84*a5ba79f0SKanaru92                    if (preg_match('/^:::\s*(.+?)\s*:::$/', $trimmedLine, $subgroupMatches)) {
85*a5ba79f0SKanaru92                        if ($currentSection !== null) {
86*a5ba79f0SKanaru92                            $currentSubgroup = $subgroupMatches[1];
87*a5ba79f0SKanaru92                            if (!isset($params['sections'][$currentSection]['_subgroups'])) {
88*a5ba79f0SKanaru92                                $params['sections'][$currentSection]['_subgroups'] = [];
89*a5ba79f0SKanaru92                            }
90*a5ba79f0SKanaru92                            $params['sections'][$currentSection]['_subgroups'][$currentSubgroup] = [];
91ffecb261SKanaru92                        }
92ffecb261SKanaru92                        continue;
93ffecb261SKanaru92                    }
94ffecb261SKanaru92
95*a5ba79f0SKanaru92                    // Check if this line contains a key=value pair
96*a5ba79f0SKanaru92                    if (strpos($trimmedLine, '=') !== false) {
97*a5ba79f0SKanaru92                        // Split only on the first = to handle values containing =
98*a5ba79f0SKanaru92                        $pos = strpos($trimmedLine, '=');
99*a5ba79f0SKanaru92                        $key = trim(substr($trimmedLine, 0, $pos));
100*a5ba79f0SKanaru92                        $value = trim(substr($trimmedLine, $pos + 1));
101ffecb261SKanaru92
102*a5ba79f0SKanaru92                        // Check if value contains unclosed plugin syntax
103*a5ba79f0SKanaru92                        $openCount = substr_count($value, '{{');
104*a5ba79f0SKanaru92                        $closeCount = substr_count($value, '}}');
105*a5ba79f0SKanaru92
106*a5ba79f0SKanaru92                        if ($openCount > $closeCount) {
107*a5ba79f0SKanaru92                            // Value contains unclosed plugin syntax, start multi-line capture
108*a5ba79f0SKanaru92                            $currentKey = $key;
109*a5ba79f0SKanaru92                            $currentValue = $value;
110*a5ba79f0SKanaru92                        } else {
111*a5ba79f0SKanaru92                            // Value is complete on this line
112*a5ba79f0SKanaru92                            $this->_saveField($params, $key, $value, $currentSection, $currentSubgroup);
113*a5ba79f0SKanaru92                        }
114*a5ba79f0SKanaru92                    }
115*a5ba79f0SKanaru92                }
116*a5ba79f0SKanaru92
117*a5ba79f0SKanaru92                // Save any remaining multi-line value
118*a5ba79f0SKanaru92                if ($currentKey !== null) {
119*a5ba79f0SKanaru92                    $this->_saveField($params, $currentKey, trim($currentValue), $currentSection, $currentSubgroup);
120*a5ba79f0SKanaru92                }
121*a5ba79f0SKanaru92
122*a5ba79f0SKanaru92                return array('state' => 'content', 'params' => $params);
123*a5ba79f0SKanaru92
124*a5ba79f0SKanaru92            case DOKU_LEXER_EXIT:
125*a5ba79f0SKanaru92                return array('state' => 'exit');
126*a5ba79f0SKanaru92        }
127*a5ba79f0SKanaru92
128*a5ba79f0SKanaru92        return false;
129*a5ba79f0SKanaru92    }
130*a5ba79f0SKanaru92
131*a5ba79f0SKanaru92    private function _saveField(&$params, $key, $value, $currentSection, $currentSubgroup = null) {
132ffecb261SKanaru92        // Skip empty values unless explicitly showing them
133ffecb261SKanaru92        if (empty($value) && $value !== '0') {
134*a5ba79f0SKanaru92            return;
135ffecb261SKanaru92        }
136ffecb261SKanaru92
137*a5ba79f0SKanaru92        // Handle image fields
138*a5ba79f0SKanaru92        if (preg_match('/^image(\d*)$/', $key, $matches)) {
139*a5ba79f0SKanaru92            $imgNum = $matches[1] ?: '1';
140ffecb261SKanaru92            // Check if image has a caption (format: filename|caption)
141ffecb261SKanaru92            if (strpos($value, '|') !== false) {
142ffecb261SKanaru92                list($imgPath, $caption) = explode('|', $value, 2);
143*a5ba79f0SKanaru92                $params['images'][$imgNum] = [
144ffecb261SKanaru92                    'path' => trim($imgPath),
145ffecb261SKanaru92                    'caption' => trim($caption)
146ffecb261SKanaru92                ];
147ffecb261SKanaru92            } else {
148*a5ba79f0SKanaru92                $params['images'][$imgNum] = [
149*a5ba79f0SKanaru92                    'path' => trim($value),
150ffecb261SKanaru92                    'caption' => ''
151ffecb261SKanaru92                ];
152ffecb261SKanaru92            }
153ffecb261SKanaru92        } elseif ($key === 'name' || $key === 'title') {
154ffecb261SKanaru92            $params['name'] = $value;
155ffecb261SKanaru92        } elseif ($key === 'header_image') {
156ffecb261SKanaru92            $params['header_image'] = $value;
157*a5ba79f0SKanaru92        } elseif ($key === 'class') {
158*a5ba79f0SKanaru92            $params['class'] = $value;
159ffecb261SKanaru92        } else {
160*a5ba79f0SKanaru92            // Add to current section/subgroup or main fields
161ffecb261SKanaru92            if ($currentSection !== null) {
162*a5ba79f0SKanaru92                if ($currentSubgroup !== null) {
163*a5ba79f0SKanaru92                    // Add to subgroup
164*a5ba79f0SKanaru92                    $params['sections'][$currentSection]['_subgroups'][$currentSubgroup][$key] = $value;
165ffecb261SKanaru92                } else {
166*a5ba79f0SKanaru92                    // Add to section (not in a subgroup)
167*a5ba79f0SKanaru92                    $params['sections'][$currentSection][$key] = $value;
168*a5ba79f0SKanaru92                }
169*a5ba79f0SKanaru92            } else {
170*a5ba79f0SKanaru92                // Add to main fields
171ffecb261SKanaru92                $params['fields'][$key] = $value;
172971218fcSKanaru92            }
173971218fcSKanaru92        }
174ffecb261SKanaru92    }
175971218fcSKanaru92
176971218fcSKanaru92    public function render($mode, Doku_Renderer $renderer, $data) {
177971218fcSKanaru92        if ($mode != 'xhtml') return false;
178971218fcSKanaru92
179*a5ba79f0SKanaru92        if (!is_array($data) || !isset($data['state'])) return false;
180*a5ba79f0SKanaru92
181*a5ba79f0SKanaru92        switch ($data['state']) {
182*a5ba79f0SKanaru92            case 'enter':
183*a5ba79f0SKanaru92                // Start of infobox - nothing to do
184*a5ba79f0SKanaru92                break;
185*a5ba79f0SKanaru92
186*a5ba79f0SKanaru92            case 'content':
187*a5ba79f0SKanaru92                // Render the actual infobox
188*a5ba79f0SKanaru92                $params = $data['params'];
189*a5ba79f0SKanaru92                $this->_renderInfobox($renderer, $params);
190*a5ba79f0SKanaru92                break;
191*a5ba79f0SKanaru92
192*a5ba79f0SKanaru92            case 'exit':
193*a5ba79f0SKanaru92                // End of infobox - nothing to do
194*a5ba79f0SKanaru92                break;
195*a5ba79f0SKanaru92        }
196*a5ba79f0SKanaru92
197*a5ba79f0SKanaru92        return true;
198*a5ba79f0SKanaru92    }
199*a5ba79f0SKanaru92
200*a5ba79f0SKanaru92    private function _renderInfobox($renderer, $data) {
201ffecb261SKanaru92        // Generate unique ID for this infobox
202ffecb261SKanaru92        $boxId = 'infobox_' . md5(serialize($data));
203971218fcSKanaru92
204ffecb261SKanaru92        // Allow custom CSS classes
205*a5ba79f0SKanaru92        $customClass = isset($data['class']) ? ' ' . hsc($data['class']) : '';
206ffecb261SKanaru92
207ffecb261SKanaru92        $renderer->doc .= '<div class="infobox' . $customClass . '" id="' . $boxId . '" role="complementary" aria-label="Information box">';
208ffecb261SKanaru92
209ffecb261SKanaru92        // Header image (optional)
210ffecb261SKanaru92        if (isset($data['header_image'])) {
211ffecb261SKanaru92            $renderer->doc .= '<div class="infobox-header-image">';
212ffecb261SKanaru92            $renderer->internalmedia(
213ffecb261SKanaru92                $data['header_image'],
214ffecb261SKanaru92                null,
215ffecb261SKanaru92                null,
216ffecb261SKanaru92                null,
217ffecb261SKanaru92                null,
218ffecb261SKanaru92                'cache',
219ffecb261SKanaru92                'details'
220ffecb261SKanaru92            );
221ffecb261SKanaru92            $renderer->doc .= '</div>';
222ffecb261SKanaru92        }
223ffecb261SKanaru92
224ffecb261SKanaru92        // Title
225971218fcSKanaru92        if (isset($data['name'])) {
226ad4c48d3Sbpmcneilly            $renderer->doc .= '<div class="infobox-title">' . $this->_parseWikiText($data['name']) . '</div>';
227971218fcSKanaru92        }
228971218fcSKanaru92
229ffecb261SKanaru92        // Multiple images with tabs
230ffecb261SKanaru92        if (!empty($data['images'])) {
231ffecb261SKanaru92            $renderer->doc .= '<div class="infobox-images">';
232ffecb261SKanaru92
233ffecb261SKanaru92            // Image tabs
234ffecb261SKanaru92            if (count($data['images']) > 1) {
235ffecb261SKanaru92                $renderer->doc .= '<div class="infobox-image-tabs" role="tablist">';
236ffecb261SKanaru92                $first = true;
237ffecb261SKanaru92                $tabCount = 1;
238ffecb261SKanaru92                foreach ($data['images'] as $num => $imgData) {
239ffecb261SKanaru92                    $tabLabel = $imgData['caption'] ?: 'Image ' . $tabCount;
240ffecb261SKanaru92                    $activeClass = $first ? ' active' : '';
241ffecb261SKanaru92                    $tabIndex = $first ? '0' : '-1';
242ffecb261SKanaru92                    $ariaSelected = $first ? 'true' : 'false';
243ffecb261SKanaru92                    $renderer->doc .= '<button class="infobox-tab' . $activeClass . '" role="tab" aria-selected="' . $ariaSelected . '" onclick="showInfoboxImage(\'' . $boxId . '\', ' . $num . ')" aria-label="View ' . hsc($tabLabel) . '" tabindex="' . $tabIndex . '">';
244ffecb261SKanaru92                    // Add tab number if no custom caption provided
245ffecb261SKanaru92                    if (!$imgData['caption'] && count($data['images']) > 1) {
246ffecb261SKanaru92                        $renderer->doc .= '<span class="infobox-tab-number">' . $tabCount . '. </span>';
247ffecb261SKanaru92                    }
248ffecb261SKanaru92                    $renderer->doc .= hsc($tabLabel);
249ffecb261SKanaru92                    $renderer->doc .= '</button>';
250ffecb261SKanaru92                    $first = false;
251ffecb261SKanaru92                    $tabCount++;
252ffecb261SKanaru92                }
253ffecb261SKanaru92                $renderer->doc .= '</div>';
254971218fcSKanaru92            }
255971218fcSKanaru92
256ffecb261SKanaru92            // Image containers
257ffecb261SKanaru92            $first = true;
258ffecb261SKanaru92            foreach ($data['images'] as $num => $imgData) {
259ffecb261SKanaru92                $activeClass = $first ? ' active' : '';
260ffecb261SKanaru92                $renderer->doc .= '<div class="infobox-image-container' . $activeClass . '" id="' . $boxId . '_img_' . $num . '" role="tabpanel">';
261ffecb261SKanaru92
262ffecb261SKanaru92                // Use DokuWiki's internal media rendering for proper lightbox support
263ffecb261SKanaru92                $renderer->internalmedia(
264ffecb261SKanaru92                    $imgData['path'],
265ffecb261SKanaru92                    $imgData['caption'] ?: null,
266ffecb261SKanaru92                    null,
267ffecb261SKanaru92                    300,
268ffecb261SKanaru92                    null,
269ffecb261SKanaru92                    'cache',
270ffecb261SKanaru92                    'details'
271ffecb261SKanaru92                );
272ffecb261SKanaru92
273ffecb261SKanaru92                if ($imgData['caption'] && count($data['images']) == 1) {
274ffecb261SKanaru92                    $renderer->doc .= '<div class="infobox-image-caption">' . hsc($imgData['caption']) . '</div>';
275971218fcSKanaru92                }
276ffecb261SKanaru92                $renderer->doc .= '</div>';
277ffecb261SKanaru92                $first = false;
278971218fcSKanaru92            }
279971218fcSKanaru92
280971218fcSKanaru92            $renderer->doc .= '</div>';
281ffecb261SKanaru92        }
282ffecb261SKanaru92
283ffecb261SKanaru92        // Main fields table
284ffecb261SKanaru92        if (!empty($data['fields'])) {
285ffecb261SKanaru92            $renderer->doc .= '<table class="infobox-table">';
286ffecb261SKanaru92            foreach ($data['fields'] as $key => $value) {
287ffecb261SKanaru92                $renderer->doc .= '<tr>';
288ffecb261SKanaru92                $renderer->doc .= '<th>' . hsc($this->_formatKey($key)) . '</th>';
289ffecb261SKanaru92                $renderer->doc .= '<td>' . $this->_parseWikiText($value) . '</td>';
290ffecb261SKanaru92                $renderer->doc .= '</tr>';
291ffecb261SKanaru92            }
292ffecb261SKanaru92            $renderer->doc .= '</table>';
293ffecb261SKanaru92        }
294ffecb261SKanaru92
295ffecb261SKanaru92        // Sections
296*a5ba79f0SKanaru92        foreach ($data['sections'] as $sectionName => $sectionData) {
297ffecb261SKanaru92            $sectionId = $boxId . '_section_' . md5($sectionName);
298ffecb261SKanaru92            $isCollapsed = isset($data['collapsed_sections'][$sectionName]);
299ffecb261SKanaru92            $collapsibleClass = $isCollapsed ? ' collapsible collapsed' : '';
300ffecb261SKanaru92
301ffecb261SKanaru92            $renderer->doc .= '<div class="infobox-section' . $collapsibleClass . '">';
302ffecb261SKanaru92            if ($isCollapsed) {
303ffecb261SKanaru92                $renderer->doc .= '<div class="infobox-section-header" onclick="toggleInfoboxSection(\'' . $sectionId . '\')" ' .
304ffecb261SKanaru92                                 'role="button" tabindex="0" aria-expanded="false" aria-controls="' . $sectionId . '">';
305ffecb261SKanaru92            } else {
306ffecb261SKanaru92                $renderer->doc .= '<div class="infobox-section-header">';
307ffecb261SKanaru92            }
308ffecb261SKanaru92            $renderer->doc .= hsc($sectionName);
309ffecb261SKanaru92            if ($isCollapsed) {
310ffecb261SKanaru92                $renderer->doc .= '<span class="infobox-section-toggle">▼</span>';
311ffecb261SKanaru92            }
312ffecb261SKanaru92            $renderer->doc .= '</div>';
313ffecb261SKanaru92
314ffecb261SKanaru92            $contentClass = $isCollapsed ? 'infobox-section-content collapsed' : 'infobox-section-content';
315ffecb261SKanaru92            $renderer->doc .= '<div class="' . $contentClass . '" id="' . $sectionId . '">';
316ffecb261SKanaru92
317*a5ba79f0SKanaru92            // Check if this section has subgroups
318*a5ba79f0SKanaru92            $hasSubgroups = isset($sectionData['_subgroups']) && !empty($sectionData['_subgroups']);
319*a5ba79f0SKanaru92
320*a5ba79f0SKanaru92            if ($hasSubgroups) {
321*a5ba79f0SKanaru92                // Render subgroups
322*a5ba79f0SKanaru92                $renderer->doc .= '<div class="infobox-subgroups">';
323*a5ba79f0SKanaru92                foreach ($sectionData['_subgroups'] as $subgroupName => $subgroupFields) {
324*a5ba79f0SKanaru92                    $renderer->doc .= '<div class="infobox-subgroup">';
325*a5ba79f0SKanaru92                    $renderer->doc .= '<div class="infobox-subgroup-header">' . hsc($subgroupName) . '</div>';
326*a5ba79f0SKanaru92
327*a5ba79f0SKanaru92                    if (!empty($subgroupFields)) {
328ffecb261SKanaru92                        $renderer->doc .= '<table class="infobox-table">';
329*a5ba79f0SKanaru92                        foreach ($subgroupFields as $key => $value) {
330ffecb261SKanaru92                            $renderer->doc .= '<tr>';
331ffecb261SKanaru92                            $renderer->doc .= '<th>' . hsc($this->_formatKey($key)) . '</th>';
332ffecb261SKanaru92                            $renderer->doc .= '<td>' . $this->_parseWikiText($value) . '</td>';
333ffecb261SKanaru92                            $renderer->doc .= '</tr>';
334ffecb261SKanaru92                        }
335ffecb261SKanaru92                        $renderer->doc .= '</table>';
336ffecb261SKanaru92                    }
337ffecb261SKanaru92                    $renderer->doc .= '</div>';
338*a5ba79f0SKanaru92                }
339*a5ba79f0SKanaru92                $renderer->doc .= '</div>';
340*a5ba79f0SKanaru92            }
341*a5ba79f0SKanaru92
342*a5ba79f0SKanaru92            // Render regular section fields (not in subgroups)
343*a5ba79f0SKanaru92            $regularFields = array_filter($sectionData, function($key) {
344*a5ba79f0SKanaru92                return $key !== '_subgroups';
345*a5ba79f0SKanaru92            }, ARRAY_FILTER_USE_KEY);
346*a5ba79f0SKanaru92
347*a5ba79f0SKanaru92            if (!empty($regularFields)) {
348*a5ba79f0SKanaru92                $renderer->doc .= '<table class="infobox-table">';
349*a5ba79f0SKanaru92                foreach ($regularFields as $key => $value) {
350*a5ba79f0SKanaru92                    $renderer->doc .= '<tr>';
351*a5ba79f0SKanaru92                    $renderer->doc .= '<th>' . hsc($this->_formatKey($key)) . '</th>';
352*a5ba79f0SKanaru92                    $renderer->doc .= '<td>' . $this->_parseWikiText($value) . '</td>';
353*a5ba79f0SKanaru92                    $renderer->doc .= '</tr>';
354*a5ba79f0SKanaru92                }
355*a5ba79f0SKanaru92                $renderer->doc .= '</table>';
356*a5ba79f0SKanaru92            }
357*a5ba79f0SKanaru92
358*a5ba79f0SKanaru92            $renderer->doc .= '</div>';
359ffecb261SKanaru92            $renderer->doc .= '</div>';
360ffecb261SKanaru92        }
361ffecb261SKanaru92
362ffecb261SKanaru92        $renderer->doc .= '</div>';
363ffecb261SKanaru92
364ffecb261SKanaru92        // Add JavaScript for image tabs (only once per page)
365ffecb261SKanaru92        static $jsAdded = false;
366ffecb261SKanaru92        if (!$jsAdded) {
367ffecb261SKanaru92            $renderer->doc .= '<script>
368ffecb261SKanaru92            function showInfoboxImage(boxId, imageNum) {
369ffecb261SKanaru92                // Hide all images in this infobox
370ffecb261SKanaru92                var containers = document.querySelectorAll("#" + boxId + " .infobox-image-container");
371ffecb261SKanaru92                containers.forEach(function(container) {
372ffecb261SKanaru92                    container.classList.remove("active");
373ffecb261SKanaru92                });
374ffecb261SKanaru92
375ffecb261SKanaru92                // Remove active class from all tabs in this infobox
376ffecb261SKanaru92                var tabs = document.querySelectorAll("#" + boxId + " .infobox-tab");
377ffecb261SKanaru92                tabs.forEach(function(tab) {
378ffecb261SKanaru92                    tab.classList.remove("active");
379ffecb261SKanaru92                    tab.setAttribute("tabindex", "-1");
380ffecb261SKanaru92                    tab.setAttribute("aria-selected", "false");
381ffecb261SKanaru92                });
382ffecb261SKanaru92
383ffecb261SKanaru92                // Show selected image
384ffecb261SKanaru92                document.getElementById(boxId + "_img_" + imageNum).classList.add("active");
385ffecb261SKanaru92
386ffecb261SKanaru92                // Add active class to clicked tab
387ffecb261SKanaru92                var activeTab = tabs[imageNum - 1];
388ffecb261SKanaru92                activeTab.classList.add("active");
389ffecb261SKanaru92                activeTab.setAttribute("tabindex", "0");
390ffecb261SKanaru92                activeTab.setAttribute("aria-selected", "true");
391ffecb261SKanaru92            }
392ffecb261SKanaru92
393ffecb261SKanaru92            function toggleInfoboxSection(sectionId) {
394ffecb261SKanaru92                var content = document.getElementById(sectionId);
395ffecb261SKanaru92                var header = content.previousElementSibling;
396ffecb261SKanaru92                var toggle = header.querySelector(".infobox-section-toggle");
397ffecb261SKanaru92
398ffecb261SKanaru92                if (content.classList.contains("collapsed")) {
399ffecb261SKanaru92                    content.classList.remove("collapsed");
400ffecb261SKanaru92                    header.setAttribute("aria-expanded", "true");
401ffecb261SKanaru92                    if (toggle) toggle.textContent = "▲";
402ffecb261SKanaru92                } else {
403ffecb261SKanaru92                    content.classList.add("collapsed");
404ffecb261SKanaru92                    header.setAttribute("aria-expanded", "false");
405ffecb261SKanaru92                    if (toggle) toggle.textContent = "▼";
406ffecb261SKanaru92                }
407ffecb261SKanaru92            }
408ffecb261SKanaru92
409ffecb261SKanaru92            // Add keyboard support
410ffecb261SKanaru92            document.addEventListener("DOMContentLoaded", function() {
411ffecb261SKanaru92                // Keyboard navigation for image tabs
412ffecb261SKanaru92                var tabGroups = document.querySelectorAll(".infobox-image-tabs");
413ffecb261SKanaru92                tabGroups.forEach(function(tabGroup) {
414ffecb261SKanaru92                    var tabs = tabGroup.querySelectorAll(".infobox-tab");
415ffecb261SKanaru92
416ffecb261SKanaru92                    tabs.forEach(function(tab, index) {
417ffecb261SKanaru92                        tab.addEventListener("keydown", function(e) {
418ffecb261SKanaru92                            var newIndex = -1;
419ffecb261SKanaru92
420ffecb261SKanaru92                            switch(e.key) {
421ffecb261SKanaru92                                case "ArrowLeft":
422ffecb261SKanaru92                                    e.preventDefault();
423ffecb261SKanaru92                                    newIndex = index - 1;
424ffecb261SKanaru92                                    if (newIndex < 0) newIndex = tabs.length - 1;
425ffecb261SKanaru92                                    break;
426ffecb261SKanaru92                                case "ArrowRight":
427ffecb261SKanaru92                                    e.preventDefault();
428ffecb261SKanaru92                                    newIndex = index + 1;
429ffecb261SKanaru92                                    if (newIndex >= tabs.length) newIndex = 0;
430ffecb261SKanaru92                                    break;
431ffecb261SKanaru92                                case "Home":
432ffecb261SKanaru92                                    e.preventDefault();
433ffecb261SKanaru92                                    newIndex = 0;
434ffecb261SKanaru92                                    break;
435ffecb261SKanaru92                                case "End":
436ffecb261SKanaru92                                    e.preventDefault();
437ffecb261SKanaru92                                    newIndex = tabs.length - 1;
438ffecb261SKanaru92                                    break;
439ffecb261SKanaru92                            }
440ffecb261SKanaru92
441ffecb261SKanaru92                            if (newIndex >= 0) {
442ffecb261SKanaru92                                tabs[newIndex].focus();
443ffecb261SKanaru92                                tabs[newIndex].click();
444ffecb261SKanaru92                            }
445ffecb261SKanaru92                        });
446ffecb261SKanaru92                    });
447ffecb261SKanaru92                });
448ffecb261SKanaru92
449ffecb261SKanaru92                // Keyboard support for collapsible sections
450ffecb261SKanaru92                var headers = document.querySelectorAll(".infobox-section.collapsible .infobox-section-header");
451ffecb261SKanaru92                headers.forEach(function(header) {
452ffecb261SKanaru92                    header.addEventListener("keydown", function(e) {
453ffecb261SKanaru92                        if (e.key === "Enter" || e.key === " ") {
454ffecb261SKanaru92                            e.preventDefault();
455ffecb261SKanaru92                            header.click();
456ffecb261SKanaru92                        }
457ffecb261SKanaru92                    });
458ffecb261SKanaru92                });
459ffecb261SKanaru92
460ffecb261SKanaru92                // Fix for section header lines
461ffecb261SKanaru92                var infoboxes = document.querySelectorAll(".infobox");
462ffecb261SKanaru92                if (infoboxes.length > 0) {
463ffecb261SKanaru92                    var headers = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
464ffecb261SKanaru92                    headers.forEach(function(header) {
465ffecb261SKanaru92                        header.style.overflow = "hidden";
466ffecb261SKanaru92                    });
467ffecb261SKanaru92
468ffecb261SKanaru92                    var pageContent = document.querySelector(".dokuwiki .page");
469ffecb261SKanaru92                    if (pageContent && !pageContent.classList.contains("has-infobox")) {
470ffecb261SKanaru92                        pageContent.classList.add("has-infobox");
471ffecb261SKanaru92                    }
472ffecb261SKanaru92                }
473ffecb261SKanaru92            });
474ffecb261SKanaru92            </script>';
475ffecb261SKanaru92            $jsAdded = true;
476ffecb261SKanaru92        }
477ffecb261SKanaru92
478971218fcSKanaru92        return true;
479971218fcSKanaru92    }
480971218fcSKanaru92
481971218fcSKanaru92    private function _parseWikiText($text) {
482ffecb261SKanaru92        // Handle multi-line values properly
483ffecb261SKanaru92        $text = str_replace('\n', "\n", $text);
484ffecb261SKanaru92
485971218fcSKanaru92        $info = array();
486971218fcSKanaru92        $xhtml = p_render('xhtml', p_get_instructions($text), $info);
487ffecb261SKanaru92
488ffecb261SKanaru92        // Remove wrapping <p> tags if it's a single paragraph
489ffecb261SKanaru92        if (substr_count($xhtml, '<p>') == 1) {
490ffecb261SKanaru92            $xhtml = preg_replace('/^\s*<p>(.*)<\/p>\s*$/s', '$1', $xhtml);
491ffecb261SKanaru92        }
492ffecb261SKanaru92
493971218fcSKanaru92        return $xhtml;
494971218fcSKanaru92    }
495ffecb261SKanaru92
496ffecb261SKanaru92    private function _formatKey($key) {
497ffecb261SKanaru92        // Convert underscores to spaces and capitalize words
498ffecb261SKanaru92        return ucwords(str_replace('_', ' ', $key));
499ffecb261SKanaru92    }
500971218fcSKanaru92}